Skip to content

Commit 0a1d084

Browse files
committed
feat(contract-tests): add flag change listener support for Node server SDK
Implement contract test support for flag change listeners in the Node server SDK test service: - Add 'flag-change-listeners' capability to the service - Handle registerFlagChangeListener, registerFlagValueChangeListener, and unregisterListener commands - Use SDK 'update' and 'update:${key}' events for config and value changes - Post listener callbacks to the test harness via HTTP - Unregister listeners on client close
1 parent 8ead8ad commit 0a1d084

2 files changed

Lines changed: 90 additions & 0 deletions

File tree

packages/sdk/server-node/contract-tests/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ app.get('/', (req: Request, res: Response) => {
4242
'client-prereq-events',
4343
'event-gzip',
4444
'optional-event-gzip',
45+
'flag-change-listeners',
4546
],
4647
});
4748
});

packages/sdk/server-node/contract-tests/src/sdkClientEntity.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,21 @@ interface CommandParams {
140140
newEndpoint: string;
141141
oldEndpoint: string;
142142
};
143+
registerFlagChangeListener?: {
144+
listenerId: string;
145+
flagKey: string;
146+
callbackUri: string;
147+
};
148+
registerFlagValueChangeListener?: {
149+
listenerId: string;
150+
flagKey: string;
151+
context: LDContext;
152+
defaultValue: LDFlagValue;
153+
callbackUri: string;
154+
};
155+
unregisterListener?: {
156+
listenerId: string;
157+
};
143158
}
144159

145160
export function makeSdkConfig(options: SdkConfigOptions, tag: string): LDOptions {
@@ -316,9 +331,15 @@ export interface SdkClientEntity {
316331
doCommand: (params: CommandParams) => Promise<any>;
317332
}
318333

334+
interface ListenerEntry {
335+
eventName: string;
336+
handler: (...args: any[]) => void;
337+
}
338+
319339
export async function newSdkClientEntity(options: any): Promise<SdkClientEntity> {
320340
const c: any = {};
321341
const log = Log(options.tag);
342+
const listeners = new Map<string, ListenerEntry>();
322343

323344
log.info(`Creating client with configuration: ${JSON.stringify(options.configuration)}`);
324345
const timeout =
@@ -341,6 +362,11 @@ export async function newSdkClientEntity(options: any): Promise<SdkClientEntity>
341362
}
342363

343364
c.close = () => {
365+
// Unregister all listeners before closing to avoid firing callbacks after shutdown.
366+
listeners.forEach((entry) => {
367+
client.off(entry.eventName, entry.handler);
368+
});
369+
listeners.clear();
344370
client.close();
345371
log.info('Test ended');
346372
};
@@ -514,6 +540,69 @@ export async function newSdkClientEntity(options: any): Promise<SdkClientEntity>
514540
}
515541
}
516542

543+
case 'registerFlagChangeListener': {
544+
const p = params.registerFlagChangeListener!;
545+
// 'update:key' fires for a specific flag; 'update' (no key) fires for any flag change.
546+
const eventName = p.flagKey ? `update:${p.flagKey}` : 'update';
547+
548+
const handler = (eventParams: { key: string }) => {
549+
got
550+
.post(p.callbackUri, {
551+
json: {
552+
listenerId: p.listenerId,
553+
flagKey: eventParams.key,
554+
},
555+
})
556+
.catch(() => {});
557+
};
558+
559+
listeners.set(p.listenerId, { eventName, handler });
560+
client.on(eventName, handler);
561+
return undefined;
562+
}
563+
564+
case 'registerFlagValueChangeListener': {
565+
const p = params.registerFlagValueChangeListener!;
566+
const eventName = `update:${p.flagKey}`;
567+
568+
// Snapshot the current evaluated value so we can detect actual value changes.
569+
// On each SDK update event, re-evaluate and only notify the harness if the
570+
// evaluated value differs (using JSON comparison for deep equality).
571+
let oldValue = await client.variation(p.flagKey, p.context, p.defaultValue);
572+
573+
const handler = async () => {
574+
const newValue = await client.variation(p.flagKey, p.context, p.defaultValue);
575+
if (JSON.stringify(newValue) !== JSON.stringify(oldValue)) {
576+
const previousValue = oldValue;
577+
oldValue = newValue;
578+
got
579+
.post(p.callbackUri, {
580+
json: {
581+
listenerId: p.listenerId,
582+
flagKey: p.flagKey,
583+
oldValue: previousValue,
584+
newValue,
585+
},
586+
})
587+
.catch(() => {});
588+
}
589+
};
590+
591+
listeners.set(p.listenerId, { eventName, handler });
592+
client.on(eventName, handler);
593+
return undefined;
594+
}
595+
596+
case 'unregisterListener': {
597+
const p = params.unregisterListener!;
598+
const entry = listeners.get(p.listenerId);
599+
if (entry) {
600+
client.off(entry.eventName, entry.handler);
601+
listeners.delete(p.listenerId);
602+
}
603+
return undefined;
604+
}
605+
517606
default:
518607
throw badCommandError;
519608
}

0 commit comments

Comments
 (0)