From df805b77c1a9c4aefbf778386d3ec1d427b0299b Mon Sep 17 00:00:00 2001 From: James Newman Date: Tue, 18 Aug 2026 11:09:06 -0400 Subject: [PATCH] feat: forward terminate from the kit to the Rokt launcher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backs the new `window.mParticle.Rokt.terminate()` entry point in the core SDK (mParticle/mparticle-web-sdk). The manager delegates to the kit, which forwards to the launcher, giving partners a supported teardown path in place of the undocumented `window.Rokt.currentLauncher?.terminate()`. The launcher reference is deliberately left in place. The Rokt Web SDK memoizes a single launcher per page, so clearing it could not buy the caller a fresh one — it would only flip the kit to not-ready and leave later calls queued forever. Co-Authored-By: Claude Opus 5 (1M context) --- src/Rokt-Kit.ts | 19 ++++++ test/src/tests.spec.ts | 133 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) diff --git a/src/Rokt-Kit.ts b/src/Rokt-Kit.ts index d3b5b9b..d4d30ad 100644 --- a/src/Rokt-Kit.ts +++ b/src/Rokt-Kit.ts @@ -86,6 +86,7 @@ interface RoktLauncher { selectPlacements(options: Record): RoktSelection | Promise; hashAttributes(attributes: Record): Promise>; use(extensionName: string): Promise; + terminate(): Promise; } interface RoktGlobal { @@ -1521,6 +1522,24 @@ class RoktKit implements KitInterface { return this.launcher!.use(extensionName); } + /** + * Tears down the Rokt launcher and the placements it rendered. + * + * The launcher reference is deliberately left in place. The Rokt Web SDK + * memoizes a single launcher per page, so clearing it here could not buy the + * caller a fresh one — it would only flip the kit to not-ready and leave + * later calls queued forever. Leaving state untouched makes this exactly + * equivalent to the `window.Rokt.currentLauncher.terminate()` call partners + * use today, just reachable through a supported API. + */ + public terminate(): Promise { + if (!this.isKitReady()) { + console.error('Rokt Kit: Not initialized'); + return Promise.resolve(); + } + return this.launcher!.terminate(); + } + /** * Registers a callback to be invoked once rokt-thank-you-element.js becomes available. */ diff --git a/test/src/tests.spec.ts b/test/src/tests.spec.ts index 40c6ed2..95552d6 100644 --- a/test/src/tests.spec.ts +++ b/test/src/tests.spec.ts @@ -3249,6 +3249,139 @@ describe('Rokt Forwarder', () => { }); }); + describe('#terminate', () => { + beforeEach(() => { + (window as any).Rokt = new (MockRoktForwarder as any)(); + (window as any).mParticle.Rokt = (window as any).Rokt; + (window as any).mParticle.Rokt.attachKitCalled = false; + (window as any).mParticle.Rokt.attachKit = async (kit: any) => { + (window as any).mParticle.Rokt.attachKitCalled = true; + (window as any).mParticle.Rokt.kit = kit; + Promise.resolve(); + }; + }); + + it('should call launcher.terminate when fully initialized', async () => { + let terminateCalled = false; + (window as any).mParticle.forwarder.isInitialized = true; + (window as any).mParticle.forwarder.launcher = { + terminate: function () { + terminateCalled = true; + return Promise.resolve(); + }, + }; + + await (window as any).mParticle.forwarder.terminate(); + + expect(terminateCalled).toBe(true); + }); + + it('should return the promise from launcher.terminate', async () => { + let resolved = false; + (window as any).mParticle.forwarder.isInitialized = true; + (window as any).mParticle.forwarder.launcher = { + terminate: () => new Promise((resolve) => setTimeout(resolve, 0)), + }; + + await (window as any).mParticle.forwarder.terminate().then(() => { + resolved = true; + }); + + expect(resolved).toBe(true); + }); + + it('should resolve without calling the launcher when called before initialization', async () => { + const originalConsoleError = window.console.error; + let errorMessage = null; + window.console.error = function (message: any) { + errorMessage = message; + }; + + (window as any).mParticle.forwarder.isInitialized = false; + (window as any).mParticle.forwarder.launcher = null; + + try { + await expect((window as any).mParticle.forwarder.terminate()).resolves.toBeUndefined(); + } finally { + window.console.error = originalConsoleError; + } + + expect(errorMessage).toBe('Rokt Kit: Not initialized'); + }); + + it('should resolve without calling the launcher when initialized but the launcher is missing', async () => { + const originalConsoleError = window.console.error; + let errorMessage = null; + window.console.error = function (message: any) { + errorMessage = message; + }; + + (window as any).mParticle.forwarder.isInitialized = true; + (window as any).mParticle.forwarder.launcher = null; + + try { + await expect((window as any).mParticle.forwarder.terminate()).resolves.toBeUndefined(); + } finally { + window.console.error = originalConsoleError; + } + + expect(errorMessage).toBe('Rokt Kit: Not initialized'); + }); + + // The Rokt Web SDK memoizes one launcher per page, so terminate must not + // clear the kit's launcher references — doing so would flip the kit to + // not-ready with no way back. + it('should leave the launcher references intact so the kit stays ready', async () => { + const launcher = { + terminate: () => Promise.resolve(), + }; + (window as any).mParticle.forwarder.isInitialized = true; + (window as any).mParticle.forwarder.launcher = launcher; + (window as any).Rokt.currentLauncher = launcher; + + await (window as any).mParticle.forwarder.terminate(); + + expect((window as any).mParticle.forwarder.launcher).toBe(launcher); + expect((window as any).Rokt.currentLauncher).toBe(launcher); + }); + + it('should call launcher.terminate after init (test mode) and attach', async () => { + let terminateCalled = false; + + (window as any).mParticle.Rokt.attachKitCalled = false; + (window as any).mParticle.Rokt.attachKit = async (kit: any) => { + (window as any).mParticle.Rokt.attachKitCalled = true; + (window as any).mParticle.Rokt.kit = kit; + Promise.resolve(); + }; + + (window as any).Rokt.createLauncher = async function () { + return Promise.resolve({ + terminate: function () { + terminateCalled = true; + return Promise.resolve(); + }, + }); + }; + + await (window as any).mParticle.forwarder.init( + { + accountId: '123456', + }, + reportService.cb, + true, + null, + {}, + ); + + await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled); + + await (window as any).mParticle.forwarder.terminate(); + + expect(terminateCalled).toBe(true); + }); + }); + describe('#setUserAttribute', () => { beforeEach(() => { (window as any).mParticle.sessionManager = {