Skip to content

[client] Release wasm js.FuncOf callbacks in ssh and rdp paths#5982

Open
lixmal wants to merge 1 commit intomainfrom
wasm-js-func-release
Open

[client] Release wasm js.FuncOf callbacks in ssh and rdp paths#5982
lixmal wants to merge 1 commit intomainfrom
wasm-js-func-release

Conversation

@lixmal
Copy link
Copy Markdown
Collaborator

@lixmal lixmal commented Apr 24, 2026

Describe your changes

js.FuncOf pins the Go closure in the syscall/js callback table until Release() is called. The existing WASM SSH / RDP code creates per-connection js.Func callbacks (write, resize, close, onGoMessage, onGoClose, the RDCleanPath per-proxy handshake handler, cert-validation then/catch) without ever releasing them, so each session permanently leaks the captured Go closure plus its JS wrapper. This PR releases them at the correct teardown point.

  • Store write / resize / close as named js.Func in SSH and release them when the read loop exits.
  • Track the per-proxy RDCleanPath handshake handler in a pending map, attach it to the connection on handshake, and release it alongside onGoMessage / onGoClose in cleanupConnection. Also delete the handleRDCleanPathWebSocket_<id> global and clean up destinations / pendingHandlers entries.
  • Release the cert-validation then / catch callbacks when the promise resolves. Channels made buffered so the defer Release is still safe on the timeout path.

Issue ticket number and link

Stack

Checklist

  • Is it a bug fix
  • Is a typo/documentation fix
  • Is a feature enhancement
  • It is a refactor
  • Created tests that fail without the change (if possible)

By submitting this pull request, you confirm that you have read and agree to the terms of the Contributor License Agreement.

Documentation

Select exactly one:

  • I added/updated documentation for this change
  • Documentation is not needed for this change (explain why)

Internal WASM memory-management fix, no public-facing docs page.

Docs PR URL (required if "docs added" is checked)

Paste the PR link from https://github.com/netbirdio/docs here:

https://github.com/netbirdio/docs/pull/__

Summary by CodeRabbit

  • Bug Fixes
    • Prevented JS/WASM callback/resource leaks and made cleanup idempotent.
    • Improved WebSocket proxy handling to avoid stale or duplicated handlers during connection lifecycle.
    • Made certificate validation more tolerant by increasing its timeout and ensuring non-blocking resolution.
    • Made SSH session callbacks terminate safely so late calls no longer invoke released handlers.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 2026

📝 Walkthrough

Walkthrough

Refactors Go/WASM JS callback lifecycle: cert validation uses buffered result/error channels, named promise handlers with sync.Once-coordinated Release and timeout increased to 5 minutes; RDP proxy creates atomic proxyID, stores pending JS handlers and transfers them on connect, hardens cleanup and js.Func Release; SSH captures and releases terminal callbacks after detaching properties. (49 words)

Changes

Cohort / File(s) Summary
RDP: cert validation
client/wasm/internal/rdp/cert_validation.go
Use buffered resultChan/errorChan (cap 1); assign thenFn/catchFn to named js.Func vars; coordinate Release() with sync.Once via deferred release() inside callbacks to avoid double-release; timeout increased from 60s to 5m.
RDP: proxy & WebSocket lifecycle
client/wasm/internal/rdp/rdcleanpath.go
Assign globally unique proxyID via atomic counter; store created handler in pendingHandlers[proxyID] and expose per-proxy global handler name; on WS open transfer pending handler into proxyConnection; persist onMessageFn/onCloseFn/wsHandlerFn on connection; make cleanup idempotent (cleanupOnce), delete global handler, detach JS callbacks (onGoMessage/onGoClose = js.Undefined()), call Release() on stored js.Funcs, and remove IDs from destinations, pendingHandlers, and activeConnections.
SSH: terminal JS interface handlers
client/wasm/internal/ssh/handlers.go
Capture write, resize, close callbacks into local js.Func vars and assign to the JS interface; run readLoop inside a goroutine wrapper that, on exit, sets interface properties to js.Undefined() then Release()s the captured funcs to avoid invoking released functions.

Sequence Diagram(s)

sequenceDiagram
    participant Browser
    participant GoWASM
    participant WebSocketServer
    Browser->>GoWASM: Create proxy -> registers global handler handleRDCleanPathWebSocket_<id>
    GoWASM->>GoWASM: generate atomic proxyID, store handler in pendingHandlers[proxyID]
    Browser->>WebSocketServer: Open WebSocket to proxy endpoint
    WebSocketServer->>Browser: WebSocket connected (onopen)
    Browser->>GoWASM: Invoke global handler -> transfer pending handler to proxyConnection
    GoWASM->>Browser: Attach onMessage/onClose callbacks (store js.Funcs on connection)
    Browser->>GoWASM: Send messages / Close
    GoWASM->>GoWASM: Handle messages/close via stored callbacks
    GoWASM->>GoWASM: cleanupOnce -> detach callbacks, delete global handler, Release() js.Funcs, remove pendingHandlers/destinations/activeConnections
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I buffered the hops when promises arrive,

Named every func so no one takes a dive,
I handed handlers from nest to den,
Released my bindings and tidied the glen,
Now callbacks rest — I’m a careful rabbit.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: releasing js.FuncOf callbacks in WASM SSH and RDP code paths to fix memory leaks.
Description check ✅ Passed The description covers all critical template sections: detailed change explanation, issue/PR reference, checklist completed (bug fix selected), and documentation status explained.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wasm-js-func-release

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.

Copy link
Copy Markdown
Contributor

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
client/wasm/internal/rdp/rdcleanpath.go (1)

283-311: ⚠️ Potential issue | 🔴 Critical

Guard cleanupConnection with sync.Once to prevent double-release panic, and conditionally release zero-value js.Func fields.

Two real edge cases exist:

  1. Double-cleanup panic: onCloseFn (line 184) calls conn.cancel() at any time, which can fire ctx.Done() in setupTLSConnection (line 153) while handleDirectRDP's defer cleanupConnection is pending. The second call to conn.wsHandlerFn.Release() panics with syscall/js: Release of already released Func. Add a sync.Once field to the connection struct to ensure cleanup runs only once.

  2. Zero-value Release crash: If HandleWebSocketConnection returns at line 141 (no destination found) or other error paths, setupWebSocketHandlers is never called, leaving wsHandlerFn, onMessageFn, and onCloseFn as zero-value js.Func fields. Calling .Release() on a zero js.Func is undefined behavior and crashes. Check each field before releasing, or initialize them with no-op functions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/wasm/internal/rdp/rdcleanpath.go` around lines 283 - 311, Add
idempotent cleanup to avoid double-release panics by adding a sync.Once field
(e.g., cleanupOnce) to proxyConnection and wrap the body of
RDCleanPathProxy.cleanupConnection in conn.cleanupOnce.Do(func(){ ... }). Also
guard releasing JS callbacks by checking for the zero-value js.Func before
calling Release (e.g., if conn.wsHandlerFn != (js.Func{}) {
conn.wsHandlerFn.Release() } and similarly for onMessageFn and onCloseFn).
Ensure the new once field is used everywhere that may call cleanup
(handleDirectRDP defer, onCloseFn handler, setupTLSConnection cancel paths) so
cleanupConnection runs only once and only releases initialized js.Funcs; this
prevents syscall/js: Release of already released Func and crashes from releasing
zero-value js.Funcs.
🧹 Nitpick comments (2)
client/wasm/internal/rdp/rdcleanpath.go (1)

96-105: Pre‑existing: proxyID derived from len(activeConnections) can collide under concurrent CreateProxy calls.

len(p.activeConnections) is read without the mutex held, and more importantly the counter is reused once a connection is cleaned up (it decrements). Two concurrent/subsequent CreateProxy calls can produce the same proxyID, which now also collides in pendingHandlers and destinations, and will js.Global().Set the same global name — overwriting the prior handlerFn and leaking it (no Release).

Since the PR's new maps key off proxyID, the collision blast radius is slightly larger than before. Not strictly in scope, but worth considering a UUID or monotonic counter:

-proxyID := fmt.Sprintf("proxy_%d", len(p.activeConnections))
+proxyID := fmt.Sprintf("proxy_%s", uuid.NewString())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/wasm/internal/rdp/rdcleanpath.go` around lines 96 - 105, proxyID
generation using len(p.activeConnections) in CreateProxy is racy and can
collide; compute a unique id under p.mu and use a monotonic counter (e.g.,
p.nextProxyID) or a UUID instead of len(...) so concurrent CreateProxy calls
cannot produce the same proxyID. Acquire p.mu before generating proxyID,
increment the counter (or generate UUID) to derive proxyID, then set
p.destinations[proxyID], p.pendingHandlers, and call js.Global().Set for
handlerFn while still holding consistent ownership; ensure any existing code
that expects numeric IDs uses the new monotonic counter field (e.g.,
p.nextProxyID) or string UUID and initialize that field where p is constructed.
client/wasm/internal/rdp/cert_validation.go (1)

52-65: The defer Release() pattern here is safe from a panic perspective, but the approach has a subtle inefficiency.

When the 60s timeout fires at line 77, validateCertificateWithJS returns and releases the handlers. If the user accepts/rejects after the timeout (61s+), JavaScript invokes the released callbacks, which log a safe error to the browser console rather than panic. However, the resultChan/errorChan writes are then lost because the timeout goroutine has already moved on.

While the buffered channel prevents blocking, the current pattern discards late callbacks silently. Consider releasing from within the callbacks instead—at the cost of a trivial leak if the promise never settles—to make the timeout semantics explicit and avoid silent discards:

Alternative approach: release from within handlers
 	resultChan := make(chan bool, 1)
 	errorChan := make(chan error, 1)
 
-	thenFn := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
-		result := args[0].Bool()
-		resultChan <- result
-		return nil
-	})
-	defer thenFn.Release()
-
-	catchFn := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
-		errorChan <- fmt.Errorf("certificate validation failed")
-		return nil
-	})
-	defer catchFn.Release()
-
+	var thenFn, catchFn js.Func
+	var releaseOnce sync.Once
+	release := func() {
+		releaseOnce.Do(func() {
+			thenFn.Release()
+			catchFn.Release()
+		})
+	}
+	thenFn = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
+		defer release()
+		resultChan <- args[0].Bool()
+		return nil
+	})
+	catchFn = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
+		defer release()
+		errorChan <- fmt.Errorf("certificate validation failed")
+		return nil
+	})
 	promise.Call("then", thenFn).Call("catch", catchFn)

Requires adding "sync" to imports.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/wasm/internal/rdp/cert_validation.go` around lines 52 - 65, The
current pattern defers thenFn.Release() and catchFn.Release(), which can lead to
silently discarded late callbacks after the timeout; change to releasing the JS
handlers from inside the callbacks using a sync.Once to ensure they are only
released once: remove the defer Release() calls, add "sync" to imports and a var
once sync.Once, and in both thenFn and catchFn call once.Do(func(){
thenFn.Release(); catchFn.Release() }) before sending to resultChan/errorChan so
late JS invocations explicitly release handlers and avoid silent discards.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client/wasm/internal/rdp/rdcleanpath.go`:
- Around line 110-125: The handlerFn stored in p.pendingHandlers and registered
on js.Global is leaked if CreateProxy runs but HandleWebSocketConnection never
executes; add a revocation path (e.g., implement func (p *RDCleanPathProxy)
revokeProxy(proxyID string)) that locks p.mu, removes p.pendingHandlers[proxyID]
and p.destinations[proxyID], then (if the js.Func existed) calls
js.Global().Delete(fmt.Sprintf("handleRDCleanPathWebSocket_%s", proxyID)) and
fn.Release(); wire this revokeProxy so callers can drop orphaned proxies (either
expose it to JS or call it from any timeout/parent-context cancellation path)
and ensure existing cleanupConnection/HandleWebSocketConnection still call it
where appropriate.
- Around line 300-310: In cleanupConnection, detach the WebSocket handlers
before releasing the js.Funcs: find where conn.ws (the ws js.Value set up with
onGoMessage and onGoClose) is referenced and either removeEventListener for the
corresponding functions or set ws.Set("onmessage", js.Undefined()) and
ws.Set("onclose", js.Undefined()) to prevent JavaScript from calling released
js.Funcs; then release conn.onMessageFn and conn.onCloseFn and proceed with
deleting conn.wsHandlerFn,
js.Global().Delete(fmt.Sprintf("handleRDCleanPathWebSocket_%s", conn.id)), and
removing entries from p.activeConnections, p.destinations, and
p.pendingHandlers.

In `@client/wasm/internal/ssh/handlers.go`:
- Around line 55-60: The exported jsInterface still holds references to released
js.Funcs causing a panic when JS calls write/resize/close after readLoop
returns; before calling writeFunc.Release(), resizeFunc.Release(), and
closeFunc.Release() in the goroutine that wraps readLoop, first clear the
properties on jsInterface (e.g., set jsInterface.Get("write"), Get("resize"),
Get("close") to js.Undefined() or delete them) so JS callers get a normal
undefined/function error instead of triggering "syscall/js: use of released
function"; update the goroutine around readLoop (and any onclose handler
cleanup) to clear jsInterface.write/resize/close prior to calling Release on
writeFunc, resizeFunc, and closeFunc.

---

Outside diff comments:
In `@client/wasm/internal/rdp/rdcleanpath.go`:
- Around line 283-311: Add idempotent cleanup to avoid double-release panics by
adding a sync.Once field (e.g., cleanupOnce) to proxyConnection and wrap the
body of RDCleanPathProxy.cleanupConnection in conn.cleanupOnce.Do(func(){ ...
}). Also guard releasing JS callbacks by checking for the zero-value js.Func
before calling Release (e.g., if conn.wsHandlerFn != (js.Func{}) {
conn.wsHandlerFn.Release() } and similarly for onMessageFn and onCloseFn).
Ensure the new once field is used everywhere that may call cleanup
(handleDirectRDP defer, onCloseFn handler, setupTLSConnection cancel paths) so
cleanupConnection runs only once and only releases initialized js.Funcs; this
prevents syscall/js: Release of already released Func and crashes from releasing
zero-value js.Funcs.

---

Nitpick comments:
In `@client/wasm/internal/rdp/cert_validation.go`:
- Around line 52-65: The current pattern defers thenFn.Release() and
catchFn.Release(), which can lead to silently discarded late callbacks after the
timeout; change to releasing the JS handlers from inside the callbacks using a
sync.Once to ensure they are only released once: remove the defer Release()
calls, add "sync" to imports and a var once sync.Once, and in both thenFn and
catchFn call once.Do(func(){ thenFn.Release(); catchFn.Release() }) before
sending to resultChan/errorChan so late JS invocations explicitly release
handlers and avoid silent discards.

In `@client/wasm/internal/rdp/rdcleanpath.go`:
- Around line 96-105: proxyID generation using len(p.activeConnections) in
CreateProxy is racy and can collide; compute a unique id under p.mu and use a
monotonic counter (e.g., p.nextProxyID) or a UUID instead of len(...) so
concurrent CreateProxy calls cannot produce the same proxyID. Acquire p.mu
before generating proxyID, increment the counter (or generate UUID) to derive
proxyID, then set p.destinations[proxyID], p.pendingHandlers, and call
js.Global().Set for handlerFn while still holding consistent ownership; ensure
any existing code that expects numeric IDs uses the new monotonic counter field
(e.g., p.nextProxyID) or string UUID and initialize that field where p is
constructed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 632871ee-894b-437b-bbc0-1458d0e81c5a

📥 Commits

Reviewing files that changed from the base of the PR and between f732b01 and 5ec3c56.

📒 Files selected for processing (3)
  • client/wasm/internal/rdp/cert_validation.go
  • client/wasm/internal/rdp/rdcleanpath.go
  • client/wasm/internal/ssh/handlers.go

Comment thread client/wasm/internal/rdp/rdcleanpath.go
Comment thread client/wasm/internal/rdp/rdcleanpath.go Outdated
Comment thread client/wasm/internal/ssh/handlers.go
@lixmal lixmal force-pushed the wasm-js-func-release branch from 5ec3c56 to a3eec0e Compare April 24, 2026 10:27
Copy link
Copy Markdown
Contributor

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
client/wasm/internal/rdp/rdcleanpath.go (1)

98-106: ⚠️ Potential issue | 🟡 Minor

proxyID generation is racy and can collide — new pendingHandler leak vector.

len(p.activeConnections) is read at line 99 without holding p.mu, so it races with other HandleWebSocketConnection / cleanupConnection mutations of the map. More importantly, activeConnections is only populated by HandleWebSocketConnection, so two sequential CreateProxy calls issued before either WebSocket connects both observe len == 0 and both generate proxy_0. On collision:

  • destinations["proxy_0"] is overwritten → first proxy's destination is lost.
  • pendingHandlers["proxy_0"] is overwritten → the first handlerFn is orphaned with no path to Release() (only cleanupConnection releases it, and it never runs for a proxy whose handler got shadowed).
  • js.Global().handleRDCleanPathWebSocket_proxy_0 is overwritten.

This defeats the leak fix the PR is introducing. Use a monotonic counter under the lock (or atomic.Uint64).

🛠️ Proposed fix
 type RDCleanPathProxy struct {
 	nbClient interface {
 		Dial(ctx context.Context, network, address string) (net.Conn, error)
 	}
 	activeConnections map[string]*proxyConnection
 	destinations      map[string]string
 	pendingHandlers   map[string]js.Func
+	nextProxyID       uint64
 	mu                sync.Mutex
 }
@@
 	return js.Global().Get("Promise").New(js.FuncOf(func(_ js.Value, args []js.Value) any {
 		resolve := args[0]
 
 		go func() {
-			proxyID := fmt.Sprintf("proxy_%d", len(p.activeConnections))
-
 			p.mu.Lock()
+			proxyID := fmt.Sprintf("proxy_%d", p.nextProxyID)
+			p.nextProxyID++
 			if p.destinations == nil {
 				p.destinations = make(map[string]string)
 			}
 			p.destinations[proxyID] = destination
 			p.mu.Unlock()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/wasm/internal/rdp/rdcleanpath.go` around lines 98 - 106, The proxyID
generation using fmt.Sprintf("proxy_%d", len(p.activeConnections)) is racy and
can collide because len(p.activeConnections) is read without p.mu, causing
overwritten destinations/pendingHandlers and leaked pending handler fns; fix by
replacing this scheme with a monotonic counter accessed under p.mu (or an
atomic.Uint64) to generate unique IDs, i.e., add/maintain a counter field on the
struct or use atomic increment, acquire p.mu when assigning the new proxyID and
updating p.destinations so the ID, destinations map update and pendingHandlers
registration are atomic with respect to HandleWebSocketConnection and
cleanupConnection to prevent collisions and leaks (refer to proxyID,
p.activeConnections, p.mu, p.destinations, pendingHandlers,
HandleWebSocketConnection, cleanupConnection, CreateProxy).
🧹 Nitpick comments (1)
client/wasm/internal/rdp/rdcleanpath.go (1)

85-89: Initialize destinations and pendingHandlers in the constructor for consistency.

Right now activeConnections is initialized up front while destinations and pendingHandlers are lazily initialized inside CreateProxy under the lock, forcing if x == nil { x = make(...) } boilerplate and leaving room for nil-map writes on future call sites that forget the guard. Preallocating here removes the check and makes the struct invariants clearer.

♻️ Proposed refactor
 	return &RDCleanPathProxy{
 		nbClient:          client,
 		activeConnections: make(map[string]*proxyConnection),
+		destinations:      make(map[string]string),
+		pendingHandlers:   make(map[string]js.Func),
 	}

And drop the lazy if p.destinations == nil { ... } / if p.pendingHandlers == nil { ... } blocks in CreateProxy.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/wasm/internal/rdp/rdcleanpath.go` around lines 85 - 89, The
RDCleanPathProxy constructor currently initializes activeConnections but leaves
destinations and pendingHandlers nil, causing boilerplate nil-checks and risking
nil-map writes; update the constructor that returns *RDCleanPathProxy to also
initialize destinations and pendingHandlers with make(map[...]) so the struct
invariants are set up up-front, then remove the now-unnecessary lazy nil guards
inside CreateProxy (the if p.destinations == nil / if p.pendingHandlers == nil
blocks) and rely on the preallocated maps instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@client/wasm/internal/rdp/rdcleanpath.go`:
- Around line 98-106: The proxyID generation using fmt.Sprintf("proxy_%d",
len(p.activeConnections)) is racy and can collide because
len(p.activeConnections) is read without p.mu, causing overwritten
destinations/pendingHandlers and leaked pending handler fns; fix by replacing
this scheme with a monotonic counter accessed under p.mu (or an atomic.Uint64)
to generate unique IDs, i.e., add/maintain a counter field on the struct or use
atomic increment, acquire p.mu when assigning the new proxyID and updating
p.destinations so the ID, destinations map update and pendingHandlers
registration are atomic with respect to HandleWebSocketConnection and
cleanupConnection to prevent collisions and leaks (refer to proxyID,
p.activeConnections, p.mu, p.destinations, pendingHandlers,
HandleWebSocketConnection, cleanupConnection, CreateProxy).

---

Nitpick comments:
In `@client/wasm/internal/rdp/rdcleanpath.go`:
- Around line 85-89: The RDCleanPathProxy constructor currently initializes
activeConnections but leaves destinations and pendingHandlers nil, causing
boilerplate nil-checks and risking nil-map writes; update the constructor that
returns *RDCleanPathProxy to also initialize destinations and pendingHandlers
with make(map[...]) so the struct invariants are set up up-front, then remove
the now-unnecessary lazy nil guards inside CreateProxy (the if p.destinations ==
nil / if p.pendingHandlers == nil blocks) and rely on the preallocated maps
instead.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d5f77030-29c5-42b5-97ae-5b91ecb72301

📥 Commits

Reviewing files that changed from the base of the PR and between 5ec3c56 and a3eec0e.

📒 Files selected for processing (3)
  • client/wasm/internal/rdp/cert_validation.go
  • client/wasm/internal/rdp/rdcleanpath.go
  • client/wasm/internal/ssh/handlers.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • client/wasm/internal/ssh/handlers.go

@lixmal lixmal force-pushed the wasm-js-func-release branch from a3eec0e to b6bd6b9 Compare April 24, 2026 10:40
Copy link
Copy Markdown
Contributor

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
client/wasm/internal/rdp/cert_validation.go (1)

49-79: ⚠️ Potential issue | 🟡 Minor

Timeout path releases thenFn/catchFn while the Promise chain is still pending — late JS invocation will log a console error and drop the callback.

The <-time.After(...) branch returns and both deferred Release()s fire while the Promise chain registered at line 65 is still pending. If the user accepts/reject after the 60s timeout (or any async JS resolution), the JS runtime invokes released Go funcs. Rather than panicking, syscall/js detects the released function, logs "call to released function" to the JavaScript console, and returns without executing the Go handler. This silently drops the callback with a console error instead of a crash. The buffered channels prevent blocking, but they don't prevent the Promise from holding stale references.

Consider releasing only after the Promise settles. One approach: move Release() calls into the callbacks themselves using a sync.Once guard to ensure they execute exactly once, regardless of which callback fires or the timing of timeout.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 882467fe-b13c-411c-8007-3f899827ed24

📥 Commits

Reviewing files that changed from the base of the PR and between a3eec0e and b6bd6b9.

📒 Files selected for processing (3)
  • client/wasm/internal/rdp/cert_validation.go
  • client/wasm/internal/rdp/rdcleanpath.go
  • client/wasm/internal/ssh/handlers.go

@lixmal lixmal force-pushed the wasm-js-func-release branch from b6bd6b9 to b786534 Compare April 24, 2026 11:28
@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown
Contributor

@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.

🧹 Nitpick comments (1)
client/wasm/internal/rdp/rdcleanpath.go (1)

87-91: Initialize destinations and pendingHandlers in the constructor for consistency.

activeConnections is initialized here, but destinations and pendingHandlers rely on lazy init inside CreateProxy (lines 108-110, 127-129). The lazy init is functionally fine (writes are guarded, and delete/reads on a nil map are safe in the cleanup path), but initializing all three maps together reduces cognitive load and removes a foot-gun if a future caller touches these maps outside CreateProxy.

♻️ Proposed refactor
 	return &RDCleanPathProxy{
 		nbClient:          client,
 		activeConnections: make(map[string]*proxyConnection),
+		destinations:      make(map[string]string),
+		pendingHandlers:   make(map[string]js.Func),
 	}

And drop the nil-guards in CreateProxy:

 			p.mu.Lock()
-			if p.destinations == nil {
-				p.destinations = make(map[string]string)
-			}
 			p.destinations[proxyID] = destination
 			p.mu.Unlock()
 			p.mu.Lock()
-			if p.pendingHandlers == nil {
-				p.pendingHandlers = make(map[string]js.Func)
-			}
 			p.pendingHandlers[proxyID] = handlerFn
 			p.mu.Unlock()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/wasm/internal/rdp/rdcleanpath.go` around lines 87 - 91,
RDCleanPathProxy constructor only initializes activeConnections but leaves
destinations and pendingHandlers nil; update the RDCleanPathProxy initializer to
also allocate destinations and pendingHandlers maps so all three
(activeConnections, destinations, pendingHandlers) are created consistently,
then remove the nil-guards in CreateProxy that check/allocate destinations and
pendingHandlers (refer to RDCleanPathProxy, CreateProxy, destinations,
pendingHandlers, activeConnections) to rely on the constructor-initialized maps.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@client/wasm/internal/rdp/rdcleanpath.go`:
- Around line 87-91: RDCleanPathProxy constructor only initializes
activeConnections but leaves destinations and pendingHandlers nil; update the
RDCleanPathProxy initializer to also allocate destinations and pendingHandlers
maps so all three (activeConnections, destinations, pendingHandlers) are created
consistently, then remove the nil-guards in CreateProxy that check/allocate
destinations and pendingHandlers (refer to RDCleanPathProxy, CreateProxy,
destinations, pendingHandlers, activeConnections) to rely on the
constructor-initialized maps.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5299d2ba-3817-4bde-8a76-0d8e27c2114b

📥 Commits

Reviewing files that changed from the base of the PR and between b6bd6b9 and b786534.

📒 Files selected for processing (3)
  • client/wasm/internal/rdp/cert_validation.go
  • client/wasm/internal/rdp/rdcleanpath.go
  • client/wasm/internal/ssh/handlers.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • client/wasm/internal/ssh/handlers.go

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.

1 participant