Skip to content
Closed
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
9 changes: 7 additions & 2 deletions src/identity.js
Original file line number Diff line number Diff line change
Expand Up @@ -1577,8 +1577,13 @@
}
mpInstance._Store.isInitialized = true;

if (mpInstance._RoktManager.isReady()) {
mpInstance._RoktManager.currentUser = mpInstance.Identity.getCurrentUser();
// Notify RoktManager that identity call has completed so it can process any queued selectPlacements calls
if (mpInstance._RoktManager) {
mpInstance._RoktManager.onIdentityComplete();

Check failure on line 1583 in src/identity.js

View workflow job for this annotation

GitHub Actions / Build Distribution Bundle

Delete `············`
if (mpInstance._RoktManager.isReady()) {
mpInstance._RoktManager.currentUser = mpInstance.Identity.getCurrentUser();
}
}

mpInstance._preInit.readyQueue = processReadyQueue(
Expand Down
67 changes: 45 additions & 22 deletions src/roktManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,6 @@ export type IRoktLauncherOptions = Dictionary<any>;
//
// https://github.com/mparticle-integrations/mparticle-javascript-integration-rokt
export default class RoktManager {
private static readonly IDENTITY_CALL_POLL_INTERVAL_MS = 50; // Polling interval for checking identityCallInFlight
private static readonly IDENTITY_CALL_MAX_WAIT_MS = 5000; // Maximum time to wait for identity call to complete

public kit: IRoktKit = null;
public filters: RoktKitFilterSettings = {};
private currentUser: IMParticleUser | null = null;
Expand All @@ -105,6 +102,13 @@ export default class RoktManager {
private domain?: string;
private mappedEmailShaIdentityType?: string | null;

// Queue for selectPlacements calls waiting for identity to complete
private identityQueue: Array<{
options: IRoktSelectPlacementsOptions;
resolve: (value: IRoktSelection) => void;
reject: (reason?: any) => void;
}> = [];

/**
* Initializes the RoktManager with configuration settings and user data.
*
Expand Down Expand Up @@ -179,32 +183,51 @@ export default class RoktManager {
* }
* });
*/
public async selectPlacements(options: IRoktSelectPlacementsOptions): Promise<IRoktSelection> {
public selectPlacements(options: IRoktSelectPlacementsOptions): Promise<IRoktSelection> {
if (!this.isReady()) {
return this.deferredCall<IRoktSelection>('selectPlacements', options);
}

// If an identity call is in flight, queue this call to be processed when it completes
if (this.store?.identityCallInFlight) {
this.logger?.verbose('RoktManager: Identity call in flight, queueing selectPlacements');
return new Promise<IRoktSelection>((resolve, reject) => {
this.identityQueue.push({ options, resolve, reject });
});
}

return this.executeSelectPlacements(options);
}

/**
* Called by the identity service when an identity call completes.
* Processes any queued selectPlacements calls that were waiting for identity.
*/
public onIdentityComplete(): void {
if (this.identityQueue.length === 0) {
return;
}

this.logger?.verbose(`RoktManager: Identity complete, processing ${this.identityQueue.length} queued selectPlacements calls`);

// Copy and clear the queue before processing to avoid issues if new items are added during processing
const queueCopy = [...this.identityQueue];
this.identityQueue = [];

for (const { options, resolve, reject } of queueCopy) {
this.executeSelectPlacements(options).then(resolve).catch(reject);
}
}

/**
* Internal method that executes the selectPlacements logic.
* Called directly when no identity call is in progress, or from the queue when identity completes.
*/
private async executeSelectPlacements(options: IRoktSelectPlacementsOptions): Promise<IRoktSelection> {
try {
const { attributes } = options;
const sandboxValue = attributes?.sandbox || null;
const mappedAttributes = this.mapPlacementAttributes(attributes, this.placementAttributesMapping);

// If an identify call is in flight (e.g., during SDK initialization), poll until it completes
if (this.store?.identityCallInFlight) {
const startTime = Date.now();
this.logger.verbose('Rokt Manager: identity call in flight, polling until complete');

// Poll until identity call completes or max wait time is reached
while (this.store?.identityCallInFlight && (Date.now() - startTime) < RoktManager.IDENTITY_CALL_MAX_WAIT_MS) {
await new Promise(resolve => setTimeout(resolve, RoktManager.IDENTITY_CALL_POLL_INTERVAL_MS));
}

if (this.store?.identityCallInFlight) {
this.logger.warning('Rokt Manager: identity call still in flight after max wait time, proceeding anyway');
} else {
this.logger.verbose('Rokt Manager: identity call completed');
}
}

this.currentUser = this.identityService.getCurrentUser();
const currentUserIdentities = this.currentUser?.getUserIdentities()?.userIdentities || {};
Expand Down Expand Up @@ -409,7 +432,7 @@ export default class RoktManager {
this.store.setLocalSessionAttribute(key, value);
}

private isReady(): boolean {
public isReady(): boolean {
// The Rokt Manager is ready when a kit is attached and has a launcher
return Boolean(this.kit && this.kit.launcher);
}
Expand Down
112 changes: 87 additions & 25 deletions test/jest/roktManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1926,8 +1926,7 @@ describe('RoktManager', () => {
expect(roktManager.filters.filteredUser).not.toBe(initialUser);
});

it('should wait briefly when identity call is in flight', async () => {
jest.setTimeout(10000); // Increase timeout for this test
it('should queue selectPlacements when identity call is in flight and process when complete', async () => {
const kit: Partial<IRoktKit> = {
launcher: {
selectPlacements: jest.fn(),
Expand All @@ -1944,46 +1943,109 @@ describe('RoktManager', () => {

const mockIdentity = {
getCurrentUser: jest.fn().mockReturnValue({
getUserIdentities: () => ({ userIdentities: {} }),
getUserIdentities: () => ({
userIdentities: {
email: 'user@example.com'
}
}),
setUserAttributes: jest.fn()
}),
identify: jest.fn()
} as unknown as SDKIdentityApi;

roktManager['identityService'] = mockIdentity;

// Create a store mock that simulates identity call completing after a short delay
// Create a store mock that simulates identity call in flight
const mockStore: any = {
identityCallInFlight: true
};
roktManager['store'] = mockStore as IStore;

// Simulate identity call completing after ~150ms
// Use a promise to ensure the setTimeout completes
const identityCompletePromise = new Promise<void>((resolve) => {
setTimeout(() => {
mockStore.identityCallInFlight = false;
resolve();
}, 150);
});

const startTime = Date.now();
const options: IRoktSelectPlacementsOptions = {
attributes: {}
attributes: {
firstname: 'John'
}
};

// Start selectPlacements and wait for identity to complete
// Call selectPlacements while identity is in flight - should be queued
const selectPlacementsPromise = roktManager.selectPlacements(options);

// Verify the call was queued
expect(roktManager['identityQueue'].length).toBe(1);

// Wait for both identity to complete and selectPlacements to finish
await Promise.all([identityCompletePromise, selectPlacementsPromise]);

const elapsedTime = Date.now() - startTime;
// Should have waited until identity call completed (allowing for polling interval variance)
// With 50ms polling interval, it should complete around 150-200ms
expect(elapsedTime).toBeGreaterThanOrEqual(100);
expect(elapsedTime).toBeLessThan(500); // Should complete well before max wait time
expect(kit.selectPlacements).toHaveBeenCalled();
// Kit's selectPlacements should NOT have been called yet
expect(kit.selectPlacements).not.toHaveBeenCalled();

// Simulate identity call completing
mockStore.identityCallInFlight = false;
roktManager.onIdentityComplete();

// Wait for the promise to resolve
await selectPlacementsPromise;

// Verify selectPlacements was called with email propagated from user identities
expect(kit.selectPlacements).toHaveBeenCalledWith(
expect.objectContaining({
attributes: expect.objectContaining({
email: 'user@example.com',
firstname: 'John'
})
})
);

// Queue should be empty now
expect(roktManager['identityQueue'].length).toBe(0);
});

it('should process multiple queued selectPlacements calls when identity completes', async () => {
const kit: Partial<IRoktKit> = {
launcher: {
selectPlacements: jest.fn(),
hashAttributes: jest.fn(),
use: jest.fn(),
},
selectPlacements: jest.fn().mockResolvedValue({}),
hashAttributes: jest.fn(),
setExtensionData: jest.fn(),
};

roktManager.kit = kit as IRoktKit;
roktManager['placementAttributesMapping'] = [];

const mockIdentity = {
getCurrentUser: jest.fn().mockReturnValue({
getUserIdentities: () => ({ userIdentities: {} }),
setUserAttributes: jest.fn()
}),
identify: jest.fn()
} as unknown as SDKIdentityApi;

roktManager['identityService'] = mockIdentity;

const mockStore: any = {
identityCallInFlight: true
};
roktManager['store'] = mockStore as IStore;

// Queue multiple calls
const promise1 = roktManager.selectPlacements({ attributes: { call: '1' } });
const promise2 = roktManager.selectPlacements({ attributes: { call: '2' } });
const promise3 = roktManager.selectPlacements({ attributes: { call: '3' } });

// Verify all calls were queued
expect(roktManager['identityQueue'].length).toBe(3);
expect(kit.selectPlacements).not.toHaveBeenCalled();

// Simulate identity completing
mockStore.identityCallInFlight = false;
roktManager.onIdentityComplete();

// Wait for all promises
await Promise.all([promise1, promise2, promise3]);

// All three should have been processed
expect(kit.selectPlacements).toHaveBeenCalledTimes(3);
expect(roktManager['identityQueue'].length).toBe(0);
});

it('should update filters.filteredUser with newly identified user when anonymous user is identified through selectPlacements', async () => {
Expand Down
Loading