Skip to content

Commit 206d682

Browse files
committed
refactor: make it a builder
1 parent 43db32a commit 206d682

File tree

16 files changed

+615
-771
lines changed

16 files changed

+615
-771
lines changed
Lines changed: 20 additions & 193 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,22 @@
11
import {
22
createClient,
33
InitializerEntry,
4-
LDClient,
5-
LDLogger,
64
LDOptions,
75
ModeDefinition,
86
SynchronizerEntry,
97
} from '@launchdarkly/js-client-sdk';
108
import {
11-
CommandParams,
12-
CommandType,
9+
ClientEntity,
1310
CreateInstanceParams,
14-
makeLogger,
11+
IClientEntity,
12+
makeDefaultInitialContext,
13+
makeSdkConfig,
1514
SDKConfigDataInitializer,
1615
SDKConfigDataSynchronizer,
1716
SDKConfigModeDefinition,
1817
SDKConfigParams,
19-
ClientSideTestHook as TestHook,
20-
ValueType,
2118
} from '@launchdarkly/js-contract-test-utils/client';
2219

23-
export const badCommandError = new Error('unsupported command');
24-
export const malformedCommand = new Error('command was malformed');
25-
2620
function translateInitializer(init: SDKConfigDataInitializer): InitializerEntry | undefined {
2721
if (init.polling) {
2822
return {
@@ -76,25 +70,15 @@ function translateModeDefinition(modeDef: SDKConfigModeDefinition): ModeDefiniti
7670
return { initializers, synchronizers };
7771
}
7872

79-
function makeSdkConfig(options: SDKConfigParams, tag: string) {
80-
if (!options.clientSide) {
81-
throw new Error('configuration did not include clientSide options');
82-
}
83-
73+
/**
74+
* Browser-specific makeSdkConfig that wraps the shared base config with
75+
* FDv2 data system translation and browser-specific options.
76+
*/
77+
function makeBrowserSdkConfig(options: SDKConfigParams, tag: string): LDOptions {
8478
const isSet = (x?: unknown) => x !== null && x !== undefined;
85-
const maybeTime = (seconds?: number) => (isSet(seconds) ? seconds / 1000 : undefined);
79+
const maybeTime = (seconds?: number) => (isSet(seconds) ? seconds! / 1000 : undefined);
8680

87-
const cf: LDOptions = {
88-
withReasons: options.clientSide.evaluationReasons,
89-
logger: makeLogger(`${tag}.sdk`),
90-
useReport: options.clientSide.useReport ?? undefined,
91-
};
92-
93-
if (options.serviceEndpoints) {
94-
cf.streamUri = options.serviceEndpoints.streaming;
95-
cf.baseUri = options.serviceEndpoints.polling;
96-
cf.eventsUri = options.serviceEndpoints.events;
97-
}
81+
const cf = { ...makeSdkConfig(options, tag), fetchGoals: false } as LDOptions;
9882

9983
if (options.dataSystem?.payloadFilter) {
10084
cf.payloadFilterKey = options.dataSystem.payloadFilter;
@@ -136,189 +120,33 @@ function makeSdkConfig(options: SDKConfigParams, tag: string) {
136120
}
137121

138122
(cf as any).dataSystem = dataSystem;
139-
} else {
140-
if (options.polling) {
141-
if (options.polling.baseUri) {
142-
cf.baseUri = options.polling.baseUri;
143-
}
144-
}
145-
146-
if (options.streaming) {
147-
if (options.streaming.baseUri) {
148-
cf.streamUri = options.streaming.baseUri;
149-
}
150-
cf.streaming = true;
151-
cf.streamInitialReconnectDelay = maybeTime(options.streaming.initialRetryDelayMs);
152-
}
153123
}
154124

155-
if (options.events) {
156-
if (options.events.baseUri) {
157-
cf.eventsUri = options.events.baseUri;
158-
}
159-
cf.allAttributesPrivate = options.events.allAttributesPrivate;
160-
cf.capacity = options.events.capacity;
161-
cf.diagnosticOptOut = !options.events.enableDiagnostics;
162-
cf.flushInterval = maybeTime(options.events.flushIntervalMs);
163-
cf.privateAttributes = options.events.globalPrivateAttributes;
164-
} else {
165-
cf.sendEvents = false;
166-
}
167-
168-
if (options.tags) {
169-
cf.applicationInfo = {
170-
id: options.tags.applicationId,
171-
version: options.tags.applicationVersion,
172-
};
173-
}
174-
175-
if (options.hooks) {
176-
cf.hooks = options.hooks.hooks.map(
177-
(hook) => new TestHook(hook.name, hook.callbackUri, hook.data, hook.errors),
178-
);
179-
}
180-
181-
cf.fetchGoals = false;
182-
183125
return cf;
184126
}
185127

186-
function makeDefaultInitialContext() {
187-
return { kind: 'user', key: 'key-not-specified' };
188-
}
189-
190-
export class ClientEntity {
191-
constructor(
192-
private readonly _client: LDClient,
193-
private readonly _logger: LDLogger,
194-
) {}
195-
196-
close() {
197-
this._client.close();
198-
this._logger.info('Test ended');
199-
}
200-
201-
async doCommand(params: CommandParams) {
202-
this._logger.info(`Received command: ${params.command}`);
203-
switch (params.command) {
204-
case CommandType.EvaluateFlag: {
205-
const evaluationParams = params.evaluate;
206-
if (!evaluationParams) {
207-
throw malformedCommand;
208-
}
209-
if (evaluationParams.detail) {
210-
switch (evaluationParams.valueType) {
211-
case ValueType.Bool:
212-
return this._client.boolVariationDetail(
213-
evaluationParams.flagKey,
214-
evaluationParams.defaultValue as boolean,
215-
);
216-
case ValueType.Int: // Intentional fallthrough.
217-
case ValueType.Double:
218-
return this._client.numberVariationDetail(
219-
evaluationParams.flagKey,
220-
evaluationParams.defaultValue as number,
221-
);
222-
case ValueType.String:
223-
return this._client.stringVariationDetail(
224-
evaluationParams.flagKey,
225-
evaluationParams.defaultValue as string,
226-
);
227-
default:
228-
return this._client.variationDetail(
229-
evaluationParams.flagKey,
230-
evaluationParams.defaultValue,
231-
);
232-
}
233-
}
234-
switch (evaluationParams.valueType) {
235-
case ValueType.Bool:
236-
return {
237-
value: this._client.boolVariation(
238-
evaluationParams.flagKey,
239-
evaluationParams.defaultValue as boolean,
240-
),
241-
};
242-
case ValueType.Int: // Intentional fallthrough.
243-
case ValueType.Double:
244-
return {
245-
value: this._client.numberVariation(
246-
evaluationParams.flagKey,
247-
evaluationParams.defaultValue as number,
248-
),
249-
};
250-
case ValueType.String:
251-
return {
252-
value: this._client.stringVariation(
253-
evaluationParams.flagKey,
254-
evaluationParams.defaultValue as string,
255-
),
256-
};
257-
default:
258-
return {
259-
value: this._client.variation(
260-
evaluationParams.flagKey,
261-
evaluationParams.defaultValue,
262-
),
263-
};
264-
}
265-
}
266-
267-
case CommandType.EvaluateAllFlags:
268-
return { state: this._client.allFlags() };
269-
270-
case CommandType.IdentifyEvent: {
271-
const identifyParams = params.identifyEvent;
272-
if (!identifyParams) {
273-
throw malformedCommand;
274-
}
275-
await this._client.identify(identifyParams.user || identifyParams.context);
276-
return undefined;
277-
}
278-
279-
case CommandType.CustomEvent: {
280-
const customEventParams = params.customEvent;
281-
if (!customEventParams) {
282-
throw malformedCommand;
283-
}
284-
this._client.track(
285-
customEventParams.eventKey,
286-
customEventParams.data,
287-
customEventParams.metricValue,
288-
);
289-
return undefined;
290-
}
291-
292-
case CommandType.FlushEvents:
293-
this._client.flush();
294-
return undefined;
295-
296-
default:
297-
throw badCommandError;
298-
}
299-
}
300-
}
301-
302-
export async function newSdkClientEntity(options: CreateInstanceParams) {
303-
const logger = makeLogger(options.tag);
304-
305-
logger.info(`Creating client with configuration: ${JSON.stringify(options.configuration)}`);
306-
128+
export async function newSdkClientEntity(
129+
_id: string,
130+
options: CreateInstanceParams,
131+
): Promise<IClientEntity> {
307132
const timeout =
308133
options.configuration.startWaitTimeMs !== null &&
309134
options.configuration.startWaitTimeMs !== undefined
310135
? options.configuration.startWaitTimeMs
311136
: 5000;
312-
const sdkConfig = makeSdkConfig(options.configuration, options.tag);
137+
138+
const sdkConfig = makeBrowserSdkConfig(options.configuration, options.tag);
313139
const initialContext =
314140
options.configuration.clientSide?.initialUser ||
315141
options.configuration.clientSide?.initialContext ||
316142
makeDefaultInitialContext();
143+
317144
const client = createClient(
318145
options.configuration.credential || 'unknown-env-id',
319146
initialContext,
320147
sdkConfig,
321148
);
149+
322150
let failed = false;
323151
try {
324152
await Promise.race([
@@ -328,13 +156,12 @@ export async function newSdkClientEntity(options: CreateInstanceParams) {
328156
}),
329157
]);
330158
} catch (_) {
331-
// we get here if waitForInitialization() rejects or if we timed out
332159
failed = true;
333160
}
334161
if (failed && !options.configuration.initCanFail) {
335162
client.close();
336163
throw new Error('client initialization failed');
337164
}
338165

339-
return new ClientEntity(client, logger);
166+
return new ClientEntity(client, options.tag);
340167
}
Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
1-
import { TestHarnessWebSocket as BaseTestHarnessWebSocket } from '@launchdarkly/js-contract-test-utils/client';
1+
import {
2+
CLIENT_SIDE_CAPABILITIES,
3+
IClientEntity,
4+
TestHarnessWebSocketBuilder,
5+
} from '@launchdarkly/js-contract-test-utils/client';
26

37
import { newSdkClientEntity } from './ClientEntity';
48

5-
const CAPABILITIES = [
6-
'client-side',
7-
'service-endpoints',
8-
'tags',
9-
'user-type',
10-
'inline-context-all',
11-
'anonymous-redaction',
12-
'strongly-typed',
13-
'client-prereq-events',
14-
'client-per-context-summaries',
15-
'track-hooks',
16-
];
9+
export function createTestHarnessWebSocket() {
10+
const entities = new Map<string, IClientEntity>();
1711

18-
export default class TestHarnessWebSocket extends BaseTestHarnessWebSocket {
19-
constructor(url: string) {
20-
super(url, CAPABILITIES, newSdkClientEntity);
21-
}
12+
return new TestHarnessWebSocketBuilder()
13+
.setCapabilities(CLIENT_SIDE_CAPABILITIES)
14+
.onCreateClient(async (id, params) => {
15+
const entity = await newSdkClientEntity(id, params);
16+
entities.set(id, entity);
17+
return entity;
18+
})
19+
.onGetClient((id) => entities.get(id))
20+
.onDeleteClient((id) => {
21+
entities.get(id)?.close();
22+
entities.delete(id);
23+
})
24+
.build();
2225
}

packages/sdk/browser/contract-tests/entity/src/main.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
// eslint-disable-next-line prettier/prettier
22
import './style.css';
3-
import TestHarnessWebSocket from './TestHarnessWebSocket';
3+
import { createTestHarnessWebSocket } from './TestHarnessWebSocket';
44

55
async function runContractTests() {
6-
const ws = new TestHarnessWebSocket('ws://localhost:8001');
6+
const ws = createTestHarnessWebSocket();
77
ws.connect();
88
}
99

packages/sdk/react-native/contract-tests/entity/App.tsx

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,16 @@
11
import React, { useEffect, useState } from 'react';
22
import { StyleSheet, Text, View } from 'react-native';
33

4-
import { TestHarnessWebSocket } from '@launchdarkly/js-contract-test-utils/client';
4+
import {
5+
Capability,
6+
CLIENT_SIDE_CAPABILITIES,
7+
IClientEntity,
8+
TestHarnessWebSocketBuilder,
9+
} from '@launchdarkly/js-contract-test-utils/client';
510

611
import { newSdkClientEntity } from './src/ClientEntity';
712

8-
const RN_CAPABILITIES = [
9-
'client-side',
10-
'mobile',
11-
'service-endpoints',
12-
'tags',
13-
'user-type',
14-
'inline-context-all',
15-
'anonymous-redaction',
16-
'strongly-typed',
17-
'client-prereq-events',
18-
'client-per-context-summaries',
19-
'track-hooks',
20-
];
13+
const RN_CAPABILITIES: Capability[] = [...CLIENT_SIDE_CAPABILITIES, 'mobile'];
2114

2215
const styles = StyleSheet.create({
2316
container: {
@@ -40,12 +33,21 @@ export default function App() {
4033
const [connected, setConnected] = useState(false);
4134

4235
useEffect(() => {
43-
const ws = new TestHarnessWebSocket(
44-
'ws://localhost:8001',
45-
RN_CAPABILITIES,
46-
newSdkClientEntity,
47-
setConnected,
48-
);
36+
const entities = new Map<string, IClientEntity>();
37+
const ws = new TestHarnessWebSocketBuilder()
38+
.setCapabilities(RN_CAPABILITIES)
39+
.onCreateClient(async (id, params) => {
40+
const entity = await newSdkClientEntity(id, params);
41+
entities.set(id, entity);
42+
return entity;
43+
})
44+
.onGetClient((id) => entities.get(id))
45+
.onDeleteClient((id) => {
46+
entities.get(id)?.close();
47+
entities.delete(id);
48+
})
49+
.onConnectionChange(setConnected)
50+
.build();
4951
ws.connect();
5052
return () => ws.disconnect();
5153
}, []);

0 commit comments

Comments
 (0)