-
Notifications
You must be signed in to change notification settings - Fork 1
Sam/memory leaks #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Sam/memory leaks #11
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
…ction close The handleClose() function rejected pending calls but never cleared the remoteCalls map, and the remoteSubscriptions map was never cleared at all. This caused memory to accumulate over the lifetime of each codec instance: - remoteCalls retained rejected promise handlers - remoteSubscriptions grew unbounded with each subscription request Now both maps are explicitly cleared after rejecting pending calls, allowing the garbage collector to reclaim the memory.
The unconfirmedTxWatchlist Map tracks unconfirmed transactions per address, but entries were never removed when: - An address was explicitly unsubscribed via removeAddressConnection() - A WebSocket connection closed unexpectedly This caused orphaned transaction IDs to accumulate indefinitely, as transactions that are dropped from the mempool (never confirmed) would remain in the watchlist forever. Now the watchlist is cleaned up in both scenarios: - When removeAddressConnection() is called during normal unsubscribe - When a connection closes and addresses are cleaned up in the close handler
Plugins had resources that were never cleaned up: periodic timers, block watchers, and WebSocket connections. This caused resource leaks during server shutdown or plugin recreation. Changes: - Added optional 'destroy' method to AddressPlugin interface - Implemented destroy() in blockbook plugin: - Stops the pingTask periodic timer - Closes all WebSocket connections - Clears internal maps - Added 'destroyed' flag to prevent reconnection after destroy - Added destroy() to AddressHub that calls destroy() on all plugins - Added graceful shutdown in server workers (SIGTERM/SIGINT handlers) that closes connections and calls hub.destroy()
The client.watchBlocks() call returns an unwatch function that was being ignored. This meant the block watcher would run indefinitely with no way to stop it, even if the plugin was recreated or the server shut down. If multiple plugin instances were created (e.g., during hot reload or testing), multiple watchers would be active simultaneously, each consuming resources and potentially causing duplicate event emissions. Now the unwatch function is stored and called in the new destroy() method, which also clears the subscribedAddresses map.
The transportUrlMap was using a regular Map to store transport instance to URL mappings. Since Map holds strong references to its keys, transport instances would never be garbage collected even after they were no longer in use by viem's fallback transport mechanism. By switching to WeakMap, transport instances that are no longer referenced elsewhere can be garbage collected, preventing gradual memory growth over the lifetime of the plugin.
The fake plugin's subscribe() method created a setTimeout that could not be cancelled. If unsubscribe() was called before the timeout fired: - The timeout would still execute and emit an update for an address that was no longer subscribed - This could cause unexpected behavior in tests Now the plugin: - Tracks pending timeouts in a Map keyed by address - Clears the timeout when unsubscribe() is called - Implements destroy() to clear all pending timeouts This makes the fake plugin behave more predictably in test scenarios and prevents orphaned timer callbacks.
The CLI tool was not calling codec.handleClose() when the WebSocket connection closed. This left pending RPC promises unresolved, which could cause the process to hang or produce confusing errors. Now handleClose() is called to properly reject any pending method calls before closing the readline interface and WebSocket.
When asResult() threw an exception, pendingCall.reject() was called but then pendingCall.resolve(cleanResult) was also called with undefined. This could cause unpredictable behavior since the promise would be both rejected and resolved. Now resolve() only executes if asResult() succeeds.
3aff55e to
856046e
Compare
swansontec
approved these changes
Dec 10, 2025
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
CHANGELOG
Does this branch warrant an entry to the CHANGELOG?
Dependencies
noneDescription
none