-
Notifications
You must be signed in to change notification settings - Fork 4
fix: EventEmitter memory leak #39
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
base: master
Are you sure you want to change the base?
Changes from all commits
53c8e1f
1022fef
bb7a297
2779b06
8ca2341
1fe4e41
86562c9
160c0fa
b7c2850
b2f6359
7975c75
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| version: 2.1 | ||
|
|
||
| # Placeholder pipeline: this branch predates the real CircleCI config being added on another, | ||
| # not-yet-merged branch. This just gives CircleCI a valid config to parse so the pipeline | ||
| # succeeds instead of failing on a missing/empty config.yml. Replace once merged with the | ||
| # branch that introduces the real pipeline definition. | ||
| jobs: | ||
| noop: | ||
| docker: | ||
| - image: cimg/base:current | ||
| steps: | ||
| - run: echo "No-op CI config - real pipeline will be introduced when merged from its source branch." | ||
|
|
||
| workflows: | ||
| noop-workflow: | ||
| jobs: | ||
| - noop |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,10 +20,8 @@ import { CredentialError, OAuth2Error } from '../errors/index.ts'; | |
|
|
||
|
|
||
| type CredentialEvents = { | ||
| 'credential_added': { credential: Credential }; | ||
| 'credential_removed': { id: string }; | ||
| 'tags_updated': { id: string, tags: string[] }; | ||
| } & Omit<CredentialCoordinatorEvents, 'credential_added' | 'credential_removed'>; | ||
| } & CredentialCoordinatorEvents; | ||
|
|
||
| /** | ||
| * Wrapper around a {@link Token.Token | Token}, providing methods to interact with Tokens without the hassle of managing them | ||
|
|
@@ -49,19 +47,22 @@ export class Credential implements RequestAuthorizer, JSONSerializable { | |
|
|
||
| // unbinds listeners of previous coordinator | ||
| ( [ | ||
| 'credential_added', 'credential_removed', 'credential_refreshed', 'default_changed', 'cleared' | ||
| 'credential_added', | ||
| 'credential_removed', | ||
| 'credential_refreshed', | ||
| 'default_changed', | ||
| 'cleared', | ||
| 'metadata_updated' | ||
| ] satisfies (keyof CredentialCoordinatorEvents)[] | ||
| ).forEach((evt) => previousCoordinator.emitter.off(evt)); | ||
|
|
||
| // binds listeners (and event relays) from coordinator to Credential.emitter | ||
| this.emitter.relay(this.coordinator.emitter, ['cleared', 'default_changed', 'credential_refreshed']); | ||
| this.emitter.relay(this.coordinator.emitter, [ | ||
| 'credential_added', 'credential_removed', 'cleared', 'default_changed', 'credential_refreshed' | ||
| ]); | ||
|
|
||
| this.coordinator.emitter.on('credential_added', ({ credential }) => { | ||
| this.emitter.emit('credential_added', { credential }); | ||
| }); | ||
|
|
||
| this.coordinator.emitter.on('credential_removed', ({ id }) => { | ||
| this.emitter.emit('credential_removed', { id }); | ||
| this.coordinator.emitter.on('metadata_updated', async ({ id, metadata }) => { | ||
| this.emitter.emit('tags_updated', { id, tags: metadata?.tags ?? [] }); | ||
| }); | ||
| } | ||
|
|
||
|
|
@@ -87,6 +88,9 @@ export class Credential implements RequestAuthorizer, JSONSerializable { | |
| /** @internal */ | ||
| protected _userInfo: UserInfo | undefined; | ||
|
|
||
| /** @internal */ | ||
| #controller = new AbortController(); | ||
|
|
||
| /** | ||
| * @remarks | ||
| * Do not use directly, use {@link store | Credential.store} instead | ||
|
|
@@ -321,6 +325,20 @@ export class Credential implements RequestAuthorizer, JSONSerializable { | |
|
|
||
| /////// public instances methods /////// | ||
|
|
||
| /** | ||
| * Cleans up resources associated with the Credential instance, so that it may be garbage collected. | ||
| * | ||
| * @remarks | ||
| * This method is meant to be used in conjunction with {@link CredentialDataSource.remove}. Calling this | ||
| * method on an active {@link Credential} may have significant consequences | ||
| * | ||
| * @internal | ||
| */ | ||
| public dispose () { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a method to clean resources used by Also added an
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The pattern of clearing |
||
| this.oauth2.dispose(); | ||
| this.#controller.abort('dispose'); | ||
| } | ||
|
|
||
| /** | ||
| * Updates tags associated with {@link Credential} | ||
| * | ||
|
|
@@ -383,14 +401,7 @@ export class Credential implements RequestAuthorizer, JSONSerializable { | |
| this.oauth2.emitter.on('token_did_refresh', ({ token }) => { | ||
| if (Token.isEqual(token, this.token)) { return; } | ||
| this.token = token; | ||
| }); | ||
|
|
||
| // bind listener to Derived class instance | ||
| this.coordinator.emitter.on('metadata_updated', async ({ id, metadata }) => { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This listener bound on every |
||
| if (this.id === id) { | ||
| Credential.emitter.emit('tags_updated', { id, tags: metadata?.tags ?? [] }); | ||
| } | ||
| }); | ||
| }, { signal: this.#controller.signal }); | ||
| } | ||
|
|
||
| // oauth2 methods | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,6 +34,7 @@ export interface CredentialDataSource { | |
| * represents the provided {@link Token.Token | Token}. | ||
| */ | ||
| hasCredential (token: Token): boolean; | ||
| hasCredential (id: string): boolean; | ||
| /** | ||
| * Checks {@link CredentialDataSource} for an existing {@link Credential} instance which | ||
| * represents the provided {@link Token.Token | Token}. If one does not exist, a new {@link Credential} | ||
|
|
@@ -80,8 +81,8 @@ export class DefaultCredentialDataSource implements CredentialDataSource { | |
| return new this.CredentialConstructor(token, client, metadata); | ||
| } | ||
|
|
||
| public hasCredential (token: Token): boolean { | ||
| return this.credentials.has(token.id); | ||
| public hasCredential (key: string | Token): boolean { | ||
| return this.credentials.has(typeof key === 'string' ? key : key.id); | ||
| } | ||
|
|
||
| public credentialFor (token: Token, metadata?: Token.Metadata): Credential { | ||
|
|
@@ -101,12 +102,14 @@ export class DefaultCredentialDataSource implements CredentialDataSource { | |
| const id = typeof cred === 'string' ? cred : cred.id; | ||
| if (this.credentials.has(id)) { | ||
| const cred = this.credentials.get(id)!; | ||
| cred.dispose(); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. calls new |
||
| this.credentials.delete(id); | ||
| this.emitter.emit('credential_removed', { dataSource: this, id: cred.id }); | ||
| } | ||
| } | ||
|
|
||
| public clear () { | ||
| this.credentials.forEach(cred => cred.dispose()); | ||
| this.credentials.clear(); | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,6 +56,19 @@ export abstract class APIClient<E extends APIClient.Events = APIClient.Events> { | |
| await this.dpopNonceCache.cacheNonce(this.getDPoPNonceCacheKey(request), nonce); | ||
| } | ||
|
|
||
| /** | ||
| * Cleans up resources associated with the client instance, so that it may be garbage collected. | ||
| * | ||
| * > [!Warning] | ||
| * > **DO NOT** use this method on active clients. | ||
| * | ||
| * @internal | ||
| */ | ||
| public dispose () { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Used within |
||
| this.emitter.clear(); | ||
| this.interceptors.splice(0, this.interceptors.length); // clears array in place | ||
| } | ||
|
|
||
| /** | ||
| * Registers an {@link APIClient.RequestInterceptor} on the {@link APIClient} | ||
| * | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -145,6 +145,14 @@ export class OAuth2Client<E extends OAuth2Client.Events = OAuth2Client.Events> e | |
| return json; | ||
| } | ||
|
|
||
| /** | ||
| * Cleans up resourece associated with the client instance to prevent leaks. | ||
| */ | ||
| public dispose () { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Used within |
||
| super.dispose(); | ||
| this.#httpCache.clear(); | ||
| } | ||
|
|
||
| /** | ||
| * Retrieves the Authorization Server's OpenID configuration | ||
| */ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
tags_updatedtrigger moved fromobserveToken