Skip to content

Commit 7dfa664

Browse files
committed
feat: add scoped clients to server sdks
1 parent 2f470aa commit 7dfa664

14 files changed

Lines changed: 1089 additions & 1 deletion

File tree

packages/sdk/react/__tests__/server/createLDServerSession.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,58 @@ it('allFlagsState() forwards options to base client', async () => {
150150
expect(client.allFlagsState).toHaveBeenCalledWith(context, options);
151151
});
152152

153+
describe('given a client with forContext', () => {
154+
function makeMockScopedBaseClient() {
155+
const base = makeMockBaseClient();
156+
const forContext = jest.fn((ctx: LDContext) => ({
157+
currentContext: () => ctx,
158+
boolVariation: (key: string, def: boolean) => base.boolVariation(key, ctx, def),
159+
numberVariation: (key: string, def: number) => base.numberVariation(key, ctx, def),
160+
stringVariation: (key: string, def: string) => base.stringVariation(key, ctx, def),
161+
jsonVariation: (key: string, def: unknown) => base.jsonVariation(key, ctx, def),
162+
boolVariationDetail: (key: string, def: boolean) => base.boolVariationDetail(key, ctx, def),
163+
numberVariationDetail: (key: string, def: number) =>
164+
base.numberVariationDetail(key, ctx, def),
165+
stringVariationDetail: (key: string, def: string) =>
166+
base.stringVariationDetail(key, ctx, def),
167+
jsonVariationDetail: (key: string, def: unknown) => base.jsonVariationDetail(key, ctx, def),
168+
allFlagsState: (options?: LDFlagsStateOptions) => base.allFlagsState(ctx, options),
169+
}));
170+
return { ...base, forContext } as any;
171+
}
172+
173+
it('uses forContext when available', () => {
174+
const client = makeMockScopedBaseClient();
175+
createLDServerSession(client, context);
176+
expect(client.forContext).toHaveBeenCalledWith(context, {
177+
wrapperName: 'react-client-sdk',
178+
wrapperVersion: '0.0.0',
179+
});
180+
});
181+
182+
it('delegates boolVariation through scoped client', async () => {
183+
const client = makeMockScopedBaseClient();
184+
client.boolVariation.mockResolvedValue(true);
185+
const session = createLDServerSession(client, context);
186+
const result = await session.boolVariation('my-flag', false);
187+
expect(result).toBe(true);
188+
expect(client.boolVariation).toHaveBeenCalledWith('my-flag', context, false);
189+
});
190+
191+
it('getContext() returns the scoped client currentContext', () => {
192+
const client = makeMockScopedBaseClient();
193+
const session = createLDServerSession(client, context);
194+
expect(session.getContext()).toEqual(context);
195+
});
196+
197+
it('initialized() delegates to the base client', () => {
198+
const client = makeMockScopedBaseClient();
199+
client.initialized.mockReturnValue(false);
200+
const session = createLDServerSession(client, context);
201+
expect(session.initialized()).toBe(false);
202+
});
203+
});
204+
153205
describe('given a browser environment (window defined)', () => {
154206
let originalWindow: typeof globalThis.window;
155207

packages/sdk/react/src/server/LDServerBaseClient.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import {
33
LDEvaluationDetailTyped,
44
LDFlagsState,
55
LDFlagsStateOptions,
6+
LDScopedClient,
7+
LDScopedClientOptions,
68
} from '@launchdarkly/js-server-sdk-common';
79

810
/**
@@ -88,4 +90,14 @@ export interface LDServerBaseClient {
8890
* Builds an object encapsulating the state of all feature flags for a given context.
8991
*/
9092
allFlagsState(context: LDContext, options?: LDFlagsStateOptions): Promise<LDFlagsState>;
93+
94+
/**
95+
* Creates a scoped client bound to the given evaluation context.
96+
*
97+
* @remarks
98+
* When present, {@link createLDServerSession} will delegate to the scoped client
99+
* instead of manually wrapping each method. Clients that do not implement this
100+
* method (e.g., edge SDKs) fall back to the manual wrapping behavior.
101+
*/
102+
forContext?(context: LDContext, options?: LDScopedClientOptions): LDScopedClient;
91103
}

packages/sdk/react/src/server/LDServerSession.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,27 @@ export function createLDServerWrapper(
5353
);
5454
}
5555

56+
if (client.forContext) {
57+
const scoped = client.forContext(context, {
58+
wrapperName: 'react-client-sdk',
59+
wrapperVersion: '0.0.0', // x-release-please-version
60+
});
61+
return {
62+
initialized: () => client.initialized(),
63+
getContext: () => scoped.currentContext(),
64+
boolVariation: scoped.boolVariation,
65+
numberVariation: scoped.numberVariation,
66+
stringVariation: scoped.stringVariation,
67+
jsonVariation: scoped.jsonVariation,
68+
boolVariationDetail: scoped.boolVariationDetail,
69+
numberVariationDetail: scoped.numberVariationDetail,
70+
stringVariationDetail: scoped.stringVariationDetail,
71+
jsonVariationDetail: scoped.jsonVariationDetail,
72+
allFlagsState: scoped.allFlagsState,
73+
};
74+
}
75+
76+
// Fallback for clients without forContext (e.g., edge SDKs)
5677
return {
5778
initialized: () => client.initialized(),
5879
getContext: () => context,

packages/shared/common/src/internal/events/EventProcessor.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,17 @@ export default class EventProcessor implements LDEventProcessor {
205205
this._eventSender.sendEventData(LDEventType.DiagnosticEvent, event);
206206
}
207207

208+
sendScopedClientDiagnosticEvent(wrapperName: string, wrapperVersion?: string): void {
209+
if (!this._diagnosticsManager) {
210+
return;
211+
}
212+
const event = this._diagnosticsManager.createInitEvent();
213+
event.sdk.wrapperName = wrapperName;
214+
event.sdk.wrapperVersion = wrapperVersion;
215+
event.creationDate = Date.now();
216+
this._postDiagnosticEvent(event);
217+
}
218+
208219
close() {
209220
clearInterval(this._flushTimer);
210221
if (this._flushUsersTimer) {

0 commit comments

Comments
 (0)