Skip to content

Commit 5246b1a

Browse files
Enhance RoktManager with deferred call handling and message queue processing
1 parent fe95e95 commit 5246b1a

File tree

2 files changed

+193
-20
lines changed

2 files changed

+193
-20
lines changed

src/roktManager.ts

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { IKitConfigs } from "./configAPIClient";
22
import { UserAttributeFilters } from "./forwarders.interfaces";
33
import { IMParticleUser } from "./identity-user-interfaces";
44
import KitFilterHelper from "./kitFilterHelper";
5-
import { Dictionary, parseSettingsString } from "./utils";
5+
import { Dictionary, parseSettingsString, generateUniqueId, isFunction } from "./utils";
66
import { SDKIdentityApi } from "./identity.interfaces";
77
import { SDKLoggerApi } from "./sdkRuntimeModels";
88

@@ -35,6 +35,7 @@ export interface IRoktLauncher {
3535

3636
export interface IRoktMessage {
3737
methodName: string;
38+
messageId?: string;
3839
payload: any;
3940
}
4041

@@ -77,6 +78,7 @@ export default class RoktManager {
7778
public filters: RoktKitFilterSettings = {};
7879
private currentUser: IMParticleUser | null = null;
7980
private messageQueue: IRoktMessage[] = [];
81+
private pendingPromises: Map<string, {resolve: Function, reject: Function}> = new Map();
8082
private sandbox: boolean | null = null;
8183
private placementAttributesMapping: Dictionary<string>[] = [];
8284
private identityService: SDKIdentityApi;
@@ -156,11 +158,7 @@ export default class RoktManager {
156158
*/
157159
public async selectPlacements(options: IRoktSelectPlacementsOptions): Promise<IRoktSelection> {
158160
if (!this.isReady()) {
159-
this.queueMessage({
160-
methodName: 'selectPlacements',
161-
payload: options,
162-
});
163-
return Promise.resolve({} as IRoktSelection);
161+
return this.deferredCall<IRoktSelection>('selectPlacements', options);
164162
}
165163

166164
try {
@@ -218,11 +216,7 @@ export default class RoktManager {
218216

219217
public hashAttributes(attributes: IRoktPartnerAttributes): Promise<Record<string, string>> {
220218
if (!this.isReady()) {
221-
this.queueMessage({
222-
methodName: 'hashAttributes',
223-
payload: attributes,
224-
});
225-
return Promise.resolve({} as Record<string, string>);
219+
return this.deferredCall<Record<string, string>>('hashAttributes', attributes);
226220
}
227221

228222
try {
@@ -288,17 +282,68 @@ export default class RoktManager {
288282
}
289283

290284
private processMessageQueue(): void {
291-
if (this.messageQueue.length > 0 && this.isReady()) {
292-
this.messageQueue.forEach(async (message) => {
293-
if (this.kit && message.methodName in this.kit) {
294-
await (this.kit[message.methodName] as Function)(message.payload);
295-
}
296-
});
297-
this.messageQueue = [];
285+
if (!this.isReady() || this.messageQueue.length === 0) {
286+
return;
287+
}
288+
289+
const messagesToProcess = [...this.messageQueue];
290+
this.messageQueue = [];
291+
292+
this.logger?.verbose(`RoktManager: Processing ${messagesToProcess.length} queued messages`);
293+
294+
for (const message of messagesToProcess) {
295+
if(!(message.methodName in this.kit) || !isFunction(this.kit[message.methodName])) {
296+
this.logger?.error(`RoktManager: Method ${message.methodName} not found in kit`);
297+
continue;
298+
}
299+
300+
this.logger?.verbose(`RoktManager: Processing queued message: ${message.methodName} with payload: ${JSON.stringify(message.payload)}`);
301+
302+
try {
303+
const result = (this.kit[message.methodName] as Function)(message.payload);
304+
this.completePendingPromise(message.messageId, result);
305+
} catch (error) {
306+
const errorMessage = error instanceof Error ? error.message : String(error);
307+
this.logger?.error(`RoktManager: Error processing message '${message.methodName}': ${errorMessage}`);
308+
this.completePendingPromise(message.messageId, Promise.reject(error));
309+
}
298310
}
299311
}
300312

301313
private queueMessage(message: IRoktMessage): void {
302314
this.messageQueue.push(message);
303315
}
316+
317+
private deferredCall<T>(methodName: string, payload: any): Promise<T> {
318+
return new Promise<T>((resolve, reject) => {
319+
const messageId = generateUniqueId();
320+
321+
// Store the promise resolvers
322+
this.pendingPromises.set(messageId, { resolve, reject });
323+
324+
// Queue the message with the ID
325+
this.queueMessage({
326+
messageId,
327+
methodName,
328+
payload,
329+
});
330+
});
331+
}
332+
333+
private completePendingPromise(messageId: string | undefined, resultOrError: any): void {
334+
// Early exit if no pending promise to handle
335+
if (!messageId || !this.pendingPromises.has(messageId)) {
336+
return;
337+
}
338+
339+
const pendingPromise = this.pendingPromises.get(messageId)!;
340+
341+
// Always use Promise.resolve to handle both sync and async results uniformly
342+
Promise.resolve(resultOrError)
343+
.then((result) => pendingPromise.resolve(result))
344+
.catch((error) => pendingPromise.reject(error));
345+
346+
// Clean up the pending promise
347+
this.pendingPromises.delete(messageId);
348+
}
304349
}

test/jest/roktManager.spec.ts

Lines changed: 130 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@ describe('RoktManager', () => {
449449
});
450450

451451
it('should process queued selectPlacements calls once the launcher and kit are attached', async () => {
452+
const expectedResult = { placements: ['placement1', 'placement2'] };
452453
const kit: IRoktKit = {
453454
launcher: {
454455
selectPlacements: jest.fn(),
@@ -457,7 +458,7 @@ describe('RoktManager', () => {
457458
filters: undefined,
458459
filteredUser: undefined,
459460
userAttributes: undefined,
460-
selectPlacements: jest.fn(),
461+
selectPlacements: jest.fn().mockResolvedValue(expectedResult),
461462
hashAttributes: jest.fn(),
462463
setExtensionData: jest.fn(),
463464
};
@@ -466,16 +467,27 @@ describe('RoktManager', () => {
466467
attributes: {}
467468
} as IRoktSelectPlacementsOptions;
468469

469-
roktManager.selectPlacements(options);
470+
// Call selectPlacements and get the promise (should be deferred since kit not ready)
471+
const selectionPromise = roktManager.selectPlacements(options);
472+
473+
// Verify the call was queued
470474
expect(roktManager['kit']).toBeNull();
471475
expect(roktManager['messageQueue'].length).toBe(1);
472476
expect(roktManager['messageQueue'][0].methodName).toBe('selectPlacements');
473477
expect(roktManager['messageQueue'][0].payload).toBe(options);
478+
expect(roktManager['messageQueue'][0].messageId).toBeDefined();
474479

480+
// Attach kit (should trigger processing of queued messages)
475481
roktManager.attachKit(kit);
482+
483+
// Verify kit was attached and queue was processed
476484
expect(roktManager['kit']).not.toBeNull();
477485
expect(roktManager['messageQueue'].length).toBe(0);
478486
expect(kit.selectPlacements).toHaveBeenCalledWith(options);
487+
488+
// Most importantly: verify the original promise resolves with the actual result
489+
const result = await selectionPromise;
490+
expect(result).toEqual(expectedResult);
479491
});
480492

481493
it('should pass through the correct attributes to kit.selectPlacements', () => {
@@ -1027,4 +1039,120 @@ describe('RoktManager', () => {
10271039
}).toThrow('Error setting extension data: ' + mockError.message);
10281040
});
10291041
});
1042+
1043+
describe('#deferredCall', () => {
1044+
it('should create a deferred promise with unique messageId', () => {
1045+
const testPayload = { test: 'data' };
1046+
1047+
// Call deferredCall
1048+
const promise = roktManager['deferredCall']<string>('testMethod', testPayload);
1049+
1050+
// Verify promise was created
1051+
expect(promise).toBeInstanceOf(Promise);
1052+
1053+
// Verify message was queued with unique messageId
1054+
expect(roktManager['messageQueue'].length).toBe(1);
1055+
const queuedMessage = roktManager['messageQueue'][0];
1056+
expect(queuedMessage.methodName).toBe('testMethod');
1057+
expect(queuedMessage.payload).toBe(testPayload);
1058+
expect(queuedMessage.messageId).toBeDefined();
1059+
expect(typeof queuedMessage.messageId).toBe('string');
1060+
1061+
// Verify pending promise is tracked with that messageId
1062+
expect(roktManager['pendingPromises'].has(queuedMessage.messageId!)).toBe(true);
1063+
});
1064+
1065+
it('should generate unique messageIds for multiple calls', () => {
1066+
// Make multiple deferred calls
1067+
const promise1 = roktManager['deferredCall']<string>('method1', { data: 1 });
1068+
const promise2 = roktManager['deferredCall']<string>('method2', { data: 2 });
1069+
const promise3 = roktManager['deferredCall']<string>('method3', { data: 3 });
1070+
1071+
// Verify all promises are created
1072+
expect(promise1).toBeInstanceOf(Promise);
1073+
expect(promise2).toBeInstanceOf(Promise);
1074+
expect(promise3).toBeInstanceOf(Promise);
1075+
1076+
// Verify all messages are queued
1077+
expect(roktManager['messageQueue'].length).toBe(3);
1078+
1079+
// Extract messageIds
1080+
const messageId1 = roktManager['messageQueue'][0].messageId!;
1081+
const messageId2 = roktManager['messageQueue'][1].messageId!;
1082+
const messageId3 = roktManager['messageQueue'][2].messageId!;
1083+
1084+
// Verify all messageIds are unique
1085+
expect(messageId1).toBeDefined();
1086+
expect(messageId2).toBeDefined();
1087+
expect(messageId3).toBeDefined();
1088+
expect(messageId1).not.toBe(messageId2);
1089+
expect(messageId2).not.toBe(messageId3);
1090+
expect(messageId1).not.toBe(messageId3);
1091+
1092+
// Verify all are tracked in pendingPromises
1093+
expect(roktManager['pendingPromises'].has(messageId1)).toBe(true);
1094+
expect(roktManager['pendingPromises'].has(messageId2)).toBe(true);
1095+
expect(roktManager['pendingPromises'].has(messageId3)).toBe(true);
1096+
});
1097+
});
1098+
1099+
describe('#completePendingPromise', () => {
1100+
it('should resolve pending promise with success result', async () => {
1101+
const promise = roktManager['deferredCall']<string>('testMethod', {});
1102+
const messageId = roktManager['messageQueue'][0].messageId!;
1103+
1104+
// Complete the promise with success result
1105+
roktManager['completePendingPromise'](messageId, 'success result');
1106+
1107+
// Promise should resolve with the result
1108+
await expect(promise).resolves.toBe('success result');
1109+
1110+
// Should clean up the pending promise
1111+
expect(roktManager['pendingPromises'].has(messageId)).toBe(false);
1112+
});
1113+
1114+
it('should reject pending promise with error', async () => {
1115+
const promise = roktManager['deferredCall']<string>('testMethod', {});
1116+
const messageId = roktManager['messageQueue'][0].messageId!;
1117+
const error = new Error('test error');
1118+
1119+
// Complete the promise with error (wrapped in rejected promise)
1120+
roktManager['completePendingPromise'](messageId, Promise.reject(error));
1121+
1122+
// Promise should reject with the error
1123+
await expect(promise).rejects.toThrow('test error');
1124+
1125+
// Should clean up the pending promise
1126+
expect(roktManager['pendingPromises'].has(messageId)).toBe(false);
1127+
});
1128+
1129+
it('should handle async results correctly', async () => {
1130+
const promise = roktManager['deferredCall']<any>('testMethod', {});
1131+
const messageId = roktManager['messageQueue'][0].messageId!;
1132+
const asyncResult = { data: 'async data' };
1133+
1134+
// Complete with an async promise
1135+
roktManager['completePendingPromise'](messageId, Promise.resolve(asyncResult));
1136+
1137+
// Should get the unwrapped result, not the promise
1138+
const result = await promise;
1139+
expect(result).toEqual(asyncResult);
1140+
expect(result).not.toBeInstanceOf(Promise);
1141+
1142+
// Should clean up the pending promise
1143+
expect(roktManager['pendingPromises'].has(messageId)).toBe(false);
1144+
});
1145+
1146+
it('should handle missing messageId gracefully', () => {
1147+
// Should not throw when messageId is undefined
1148+
expect(() => {
1149+
roktManager['completePendingPromise'](undefined, 'result');
1150+
}).not.toThrow();
1151+
1152+
// Should not throw when messageId does not exist
1153+
expect(() => {
1154+
roktManager['completePendingPromise']('nonexistent', 'result');
1155+
}).not.toThrow();
1156+
});
1157+
});
10301158
});

0 commit comments

Comments
 (0)