Skip to content

Commit e1b006e

Browse files
committed
refactor: make it a builder
1 parent 2aa8dc5 commit e1b006e

16 files changed

Lines changed: 612 additions & 780 deletions

File tree

Lines changed: 17 additions & 202 deletions
Original file line numberDiff line numberDiff line change
@@ -1,221 +1,37 @@
1-
import { createClient, LDClient, LDLogger, LDOptions } from '@launchdarkly/js-client-sdk';
1+
import { createClient, LDOptions } from '@launchdarkly/js-client-sdk';
22
import {
3-
CommandParams,
4-
CommandType,
3+
ClientEntity,
54
CreateInstanceParams,
6-
makeLogger,
7-
SDKConfigParams,
8-
ClientSideTestHook as TestHook,
9-
ValueType,
5+
IClientEntity,
6+
makeDefaultInitialContext,
7+
makeSdkConfig,
108
} from '@launchdarkly/js-contract-test-utils/client';
119

12-
export const badCommandError = new Error('unsupported command');
13-
export const malformedCommand = new Error('command was malformed');
14-
15-
function makeSdkConfig(options: SDKConfigParams, tag: string) {
16-
if (!options.clientSide) {
17-
throw new Error('configuration did not include clientSide options');
18-
}
19-
20-
const isSet = (x?: unknown) => x !== null && x !== undefined;
21-
const maybeTime = (seconds?: number) => (isSet(seconds) ? seconds / 1000 : undefined);
22-
23-
const cf: LDOptions = {
24-
withReasons: options.clientSide.evaluationReasons,
25-
logger: makeLogger(`${tag}.sdk`),
26-
useReport: options.clientSide.useReport,
27-
};
28-
29-
if (options.serviceEndpoints) {
30-
cf.streamUri = options.serviceEndpoints.streaming;
31-
cf.baseUri = options.serviceEndpoints.polling;
32-
cf.eventsUri = options.serviceEndpoints.events;
33-
}
34-
35-
if (options.polling) {
36-
if (options.polling.baseUri) {
37-
cf.baseUri = options.polling.baseUri;
38-
}
39-
}
40-
41-
// Can contain streaming and polling, if streaming is set override the initial connection
42-
// mode. This can be removed when we add JS specific initialization that uses polling
43-
// and then streaming.
44-
if (options.streaming) {
45-
if (options.streaming.baseUri) {
46-
cf.streamUri = options.streaming.baseUri;
47-
}
48-
cf.streaming = true;
49-
cf.streamInitialReconnectDelay = maybeTime(options.streaming.initialRetryDelayMs);
50-
}
51-
52-
if (options.events) {
53-
if (options.events.baseUri) {
54-
cf.eventsUri = options.events.baseUri;
55-
}
56-
cf.allAttributesPrivate = options.events.allAttributesPrivate;
57-
cf.capacity = options.events.capacity;
58-
cf.diagnosticOptOut = !options.events.enableDiagnostics;
59-
cf.flushInterval = maybeTime(options.events.flushIntervalMs);
60-
cf.privateAttributes = options.events.globalPrivateAttributes;
61-
} else {
62-
cf.sendEvents = false;
63-
}
64-
65-
if (options.tags) {
66-
cf.applicationInfo = {
67-
id: options.tags.applicationId,
68-
version: options.tags.applicationVersion,
69-
};
70-
}
71-
72-
if (options.hooks) {
73-
cf.hooks = options.hooks.hooks.map(
74-
(hook) => new TestHook(hook.name, hook.callbackUri, hook.data, hook.errors),
75-
);
76-
}
77-
78-
cf.fetchGoals = false;
79-
80-
return cf;
81-
}
82-
83-
function makeDefaultInitialContext() {
84-
return { kind: 'user', key: 'key-not-specified' };
85-
}
86-
87-
export class ClientEntity {
88-
constructor(
89-
private readonly _client: LDClient,
90-
private readonly _logger: LDLogger,
91-
) {}
92-
93-
close() {
94-
this._client.close();
95-
this._logger.info('Test ended');
96-
}
97-
98-
async doCommand(params: CommandParams) {
99-
this._logger.info(`Received command: ${params.command}`);
100-
switch (params.command) {
101-
case CommandType.EvaluateFlag: {
102-
const evaluationParams = params.evaluate;
103-
if (!evaluationParams) {
104-
throw malformedCommand;
105-
}
106-
if (evaluationParams.detail) {
107-
switch (evaluationParams.valueType) {
108-
case ValueType.Bool:
109-
return this._client.boolVariationDetail(
110-
evaluationParams.flagKey,
111-
evaluationParams.defaultValue as boolean,
112-
);
113-
case ValueType.Int: // Intentional fallthrough.
114-
case ValueType.Double:
115-
return this._client.numberVariationDetail(
116-
evaluationParams.flagKey,
117-
evaluationParams.defaultValue as number,
118-
);
119-
case ValueType.String:
120-
return this._client.stringVariationDetail(
121-
evaluationParams.flagKey,
122-
evaluationParams.defaultValue as string,
123-
);
124-
default:
125-
return this._client.variationDetail(
126-
evaluationParams.flagKey,
127-
evaluationParams.defaultValue,
128-
);
129-
}
130-
}
131-
switch (evaluationParams.valueType) {
132-
case ValueType.Bool:
133-
return {
134-
value: this._client.boolVariation(
135-
evaluationParams.flagKey,
136-
evaluationParams.defaultValue as boolean,
137-
),
138-
};
139-
case ValueType.Int: // Intentional fallthrough.
140-
case ValueType.Double:
141-
return {
142-
value: this._client.numberVariation(
143-
evaluationParams.flagKey,
144-
evaluationParams.defaultValue as number,
145-
),
146-
};
147-
case ValueType.String:
148-
return {
149-
value: this._client.stringVariation(
150-
evaluationParams.flagKey,
151-
evaluationParams.defaultValue as string,
152-
),
153-
};
154-
default:
155-
return {
156-
value: this._client.variation(
157-
evaluationParams.flagKey,
158-
evaluationParams.defaultValue,
159-
),
160-
};
161-
}
162-
}
163-
164-
case CommandType.EvaluateAllFlags:
165-
return { state: this._client.allFlags() };
166-
167-
case CommandType.IdentifyEvent: {
168-
const identifyParams = params.identifyEvent;
169-
if (!identifyParams) {
170-
throw malformedCommand;
171-
}
172-
await this._client.identify(identifyParams.user || identifyParams.context);
173-
return undefined;
174-
}
175-
176-
case CommandType.CustomEvent: {
177-
const customEventParams = params.customEvent;
178-
if (!customEventParams) {
179-
throw malformedCommand;
180-
}
181-
this._client.track(
182-
customEventParams.eventKey,
183-
customEventParams.data,
184-
customEventParams.metricValue,
185-
);
186-
return undefined;
187-
}
188-
189-
case CommandType.FlushEvents:
190-
this._client.flush();
191-
return undefined;
192-
193-
default:
194-
throw badCommandError;
195-
}
196-
}
197-
}
198-
199-
export async function newSdkClientEntity(options: CreateInstanceParams) {
200-
const logger = makeLogger(options.tag);
201-
202-
logger.info(`Creating client with configuration: ${JSON.stringify(options.configuration)}`);
203-
10+
export async function newSdkClientEntity(
11+
_id: string,
12+
options: CreateInstanceParams,
13+
): Promise<IClientEntity> {
20414
const timeout =
20515
options.configuration.startWaitTimeMs !== null &&
20616
options.configuration.startWaitTimeMs !== undefined
20717
? options.configuration.startWaitTimeMs
20818
: 5000;
209-
const sdkConfig = makeSdkConfig(options.configuration, options.tag);
19+
20+
const sdkConfig = {
21+
...makeSdkConfig(options.configuration, options.tag),
22+
fetchGoals: false,
23+
} as LDOptions;
21024
const initialContext =
21125
options.configuration.clientSide?.initialUser ||
21226
options.configuration.clientSide?.initialContext ||
21327
makeDefaultInitialContext();
28+
21429
const client = createClient(
21530
options.configuration.credential || 'unknown-env-id',
21631
initialContext,
21732
sdkConfig,
21833
);
34+
21935
let failed = false;
22036
try {
22137
await Promise.race([
@@ -225,13 +41,12 @@ export async function newSdkClientEntity(options: CreateInstanceParams) {
22541
}),
22642
]);
22743
} catch (_) {
228-
// we get here if waitForInitialization() rejects or if we timed out
22944
failed = true;
23045
}
23146
if (failed && !options.configuration.initCanFail) {
23247
client.close();
23348
throw new Error('client initialization failed');
23449
}
23550

236-
return new ClientEntity(client, logger);
51+
return new ClientEntity(client, options.tag);
23752
}
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)