Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 39 additions & 5 deletions src/batchUploader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ export class BatchUploader {
private uploader: AsyncUploader;
private lastASTEventTime: number = 0;
private readonly AST_DEBOUNCE_MS: number = 1000; // 1 second debounce
private uploadIntervalTimerId: ReturnType<typeof setTimeout> | null = null;
private exitHandler: (() => void) | null = null;
private visibilityChangeHandler: (() => void) | null = null;
private destroyed = false;

/**
* Creates an instance of a BatchUploader
Expand Down Expand Up @@ -105,6 +109,31 @@ export class BatchUploader {
this.addEventListeners();
}

/**
* Cleans up timers and event listeners to prevent leaks when the SDK
* is re-initialized (e.g. between tests or on repeated init calls).
*/
public destroy(): void {
this.destroyed = true;

if (this.uploadIntervalTimerId !== null) {
clearTimeout(this.uploadIntervalTimerId);
this.uploadIntervalTimerId = null;
}

if (this.visibilityChangeHandler) {
document.removeEventListener('visibilitychange', this.visibilityChangeHandler);
}

if (this.exitHandler) {
window.removeEventListener('beforeunload', this.exitHandler);
window.removeEventListener('pagehide', this.exitHandler);
}

this.exitHandler = null;
this.visibilityChangeHandler = null;
}

private isOfflineStorageAvailable(): boolean {
const {
_Helpers: { getFeatureFlag },
Expand Down Expand Up @@ -218,12 +247,17 @@ export class BatchUploader {
// Then trigger the upload with beacon
_this.prepareAndUpload(false, _this.isBeaconAvailable());
};
// visibility change is a document property, not window
document.addEventListener('visibilitychange', () => {

this.exitHandler = handleExit;

this.visibilityChangeHandler = () => {
if (document.visibilityState === 'hidden') {
handleExit();
}
});
};

// visibility change is a document property, not window
document.addEventListener('visibilitychange', this.visibilityChangeHandler);
window.addEventListener('beforeunload', handleExit);
window.addEventListener('pagehide', handleExit);
}
Expand All @@ -240,7 +274,7 @@ export class BatchUploader {
triggerFuture: boolean = false,
useBeacon: boolean = false
): void {
setTimeout(() => {
this.uploadIntervalTimerId = setTimeout(() => {
Comment thread
cursor[bot] marked this conversation as resolved.
this.prepareAndUpload(triggerFuture, useBeacon);
}, this.uploadIntervalMillis);
}
Expand Down Expand Up @@ -445,7 +479,7 @@ export class BatchUploader {
this.batchesQueuedForProcessing = [];
}

if (triggerFuture) {
if (triggerFuture && !this.destroyed) {
this.triggerUploadInterval(triggerFuture, false);
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/mp-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,12 @@
this._instanceName = instanceName;
this._NativeSdkHelpers = new NativeSdkHelpers(this);
this._SessionManager = new SessionManager(this);
this._Persistence = new Persistence(this);

Check failure on line 129 in src/mp-instance.ts

View workflow job for this annotation

GitHub Actions / Core Tests / Core SDK Tests

Type '_Persistence' is not assignable to type 'IPersistence'.
this._Helpers = new Helpers(this);

Check failure on line 130 in src/mp-instance.ts

View workflow job for this annotation

GitHub Actions / Core Tests / Core SDK Tests

Type 'Helpers' is not assignable to type 'SDKHelpersApi'.
this._Events = new Events(this);
this._CookieSyncManager = new CookieSyncManager(this);
this._ServerModel = new ServerModel(this);
this._Ecommerce = new Ecommerce(this);

Check failure on line 134 in src/mp-instance.ts

View workflow job for this annotation

GitHub Actions / Core Tests / Core SDK Tests

Type 'Ecommerce' is not assignable to type 'IECommerce'.
this._ForwardingStatsUploader = new ForwardingStatsUploader(this);
this._Consent = new Consent(this);
this._IdentityAPIClient = new IdentityAPIClient(this);
Expand Down Expand Up @@ -189,7 +189,7 @@
this.ProductActionType = ProductActionType;


this._Identity = new Identity(this);

Check failure on line 192 in src/mp-instance.ts

View workflow job for this annotation

GitHub Actions / Core Tests / Core SDK Tests

Type 'Identity' is not assignable to type 'IIdentity'.
this.Identity = this._Identity.IdentityAPI;
this.generateHash = this._Helpers.generateHash;

Expand All @@ -198,9 +198,9 @@
this.getDeviceId = this._Persistence.getDeviceId;

if (typeof window !== 'undefined') {
if (window.mParticle && window.mParticle.config) {

Check failure on line 201 in src/mp-instance.ts

View workflow job for this annotation

GitHub Actions / Core Tests / Core SDK Tests

Property 'config' does not exist on type 'typeof import("/home/runner/work/mparticle-web-sdk/mparticle-web-sdk/node_modules/@types/mparticle__web-sdk/index")'.
if (window.mParticle.config.hasOwnProperty('rq')) {

Check failure on line 202 in src/mp-instance.ts

View workflow job for this annotation

GitHub Actions / Core Tests / Core SDK Tests

Property 'config' does not exist on type 'typeof import("/home/runner/work/mparticle-web-sdk/mparticle-web-sdk/node_modules/@types/mparticle__web-sdk/index")'.
this._preInit.readyQueue = window.mParticle.config.rq;

Check failure on line 203 in src/mp-instance.ts

View workflow job for this annotation

GitHub Actions / Core Tests / Core SDK Tests

Property 'config' does not exist on type 'typeof import("/home/runner/work/mparticle-web-sdk/mparticle-web-sdk/node_modules/@types/mparticle__web-sdk/index")'.
}
}
}
Expand Down Expand Up @@ -1446,6 +1446,11 @@
const kitBlocker = createKitBlocker(config, mpInstance);
const { getFeatureFlag } = mpInstance._Helpers;

// Destroy previous batch uploader to prevent leaked timers and event listeners
if (mpInstance._APIClient?.uploader) {
mpInstance._APIClient.uploader.destroy();
}

mpInstance._APIClient = new APIClient(mpInstance, kitBlocker);
mpInstance._Forwarders = new Forwarders(mpInstance, kitBlocker);
mpInstance._Store.processConfig(config);
Expand Down Expand Up @@ -1648,7 +1653,7 @@
mpInstance._ErrorReportingDispatcher.logger = mpInstance.Logger;
mpInstance._LoggingDispatcher.logger = mpInstance.Logger;
mpInstance._Store = new Store(config, mpInstance, apiKey);
window.mParticle.Store = mpInstance._Store;

Check failure on line 1656 in src/mp-instance.ts

View workflow job for this annotation

GitHub Actions / Core Tests / Core SDK Tests

Property 'Store' does not exist on type 'typeof import("/home/runner/work/mparticle-web-sdk/mparticle-web-sdk/node_modules/@types/mparticle__web-sdk/index")'.
mpInstance.Logger.verbose(StartingInitialization);

// Initialize CookieConsentManager with privacy flags from launcherOptions
Expand Down
1 change: 1 addition & 0 deletions test/jest/batchUploader.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ describe('BatchUploader', () => {
});

afterEach(() => {
batchUploader.destroy();
jest.useRealTimers();
global.fetch = originalFetch;
});
Expand Down
7 changes: 7 additions & 0 deletions test/src/config/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ beforeEach(function() {
clearInterval(forwardingStatsTimer);
mParticleSDK._forwardingStatsTimer = 0;
}

// Destroy the batch uploader to prevent leaked timers and event listeners
// from accumulating across tests
const uploader = mpInstance?._APIClient?.uploader;
if (uploader) {
uploader.destroy();
}

// mocha can't clean up after itself, so this lets
// tests mock the current user and restores in between runs.
Expand Down
Loading