Skip to content

fix: EventEmitter memory leak - #39

Draft
jaredperreault-okta wants to merge 6 commits into
masterfrom
jp-emitter-leak-fix
Draft

fix: EventEmitter memory leak#39
jaredperreault-okta wants to merge 6 commits into
masterfrom
jp-emitter-leak-fix

Conversation

@jaredperreault-okta

Copy link
Copy Markdown
Contributor

No description provided.

@jaredperreault-okta jaredperreault-okta changed the title first pass fix: EventEmitter memory leak Aug 31, 2026
});

// bind listener to Derived class instance
this.coordinator.emitter.on('metadata_updated', async ({ id, metadata }) => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This listener bound on every Credential instance, but filter to only fire on the specific Credential. It's incorrectly space in method space. Moved this logic to static section where other listeners are bound

this.emitter.emit('credential_removed', { id });
});

this.coordinator.emitter.on('metadata_updated', async ({ id, metadata }) => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The tags_updated trigger moved from observeToken

/**
* Cleans up resourece associated with the Credential instance to prevent leaks.
*/
public dispose () {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a method to clean resources used by Credential instances. Since the tags_updated listener was moved to static space, the only instance-level listener is token_did_refresh of the OAuth2 instance with Credential. oauth2.dispose clears that listener.

Also added an AbortController instance of each Credential for future proofing of clearing resources

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The pattern of clearing this.oauth2.emitter listeners also solves this nested listener which contributed to the memory leak

https://github.com/okta/okta-client-javascript/blob/master/packages/auth-foundation/src/Credential/CredentialCoordinator.ts#L128

const id = typeof cred === 'string' ? cred : cred.id;
if (this.credentials.has(id)) {
const cred = this.credentials.get(id)!;
cred.dispose();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

calls new .dipose method on Credentials during .remove()

/**
* Cleans up resourece associated with the client instance to prevent leaks.
*/
public dispose () {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Used within Credential via OAuth2.dispose. Clears active listeners on the EventEmitter to prevent similar leaks

/**
* Cleans up resourece associated with the client instance to prevent leaks.
*/
public dispose () {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Used within Credential.dispose. Clears active listeners on this.emitter via super.dispose to prevent similar leaks. Also clears the #httpCache

}
this.listeners[eventName]!.push(handler);

if (signal && !signal?.aborted) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This pattern isn't really utilized yet, but for future-proofing. Listens on a provided AbortSignal (presumably via an AbortController) and clears the event listener bound at the same time as the AbortSignal. Could useful to assist resource clean up of other entities in the future

log('removal');
this.credentialDataSource.remove(id);

// // removal messages never carried a token body; `id` is all `hasCredential` actually reads

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Need to confirm with testing this section is no longer needed

// @ts-expect-error - Credential `set token()` is a private setter to avoid exposing this to the public API
credential.token = token;
this.emitter.emit('credential_refreshed', { credential });
const credential = this.credentialDataSource.credentialFor(token);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Per AI suggestion, there is probably no need to broadcast the token value when synchronizing tabs, each tab can access the token value from storage (the source of truth) to save on memory usage

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