Skip to content

Commit c66aa6e

Browse files
kinyoklionyusintoLaunchDarklyReleaseBot
authored
feat!: Implement Migrations. Refactor for client SDKs. (#293)
Co-authored-by: Yusinto Ngadiman <[email protected]> Co-authored-by: Yusinto Ngadiman <[email protected]> Co-authored-by: LaunchDarklyReleaseBot <[email protected]>
1 parent a9531ad commit c66aa6e

File tree

202 files changed

+9361
-2929
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

202 files changed

+9361
-2929
lines changed

.eslintrc.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ module.exports = {
1010
plugins: ['@typescript-eslint', 'prettier'],
1111
ignorePatterns: ['**/dist/**', '**/vercel/examples/**'],
1212
rules: {
13+
'@typescript-eslint/lines-between-class-members': 'off',
14+
'@typescript-eslint/no-unused-vars': [
15+
'error',
16+
{ ignoreRestSiblings: true, argsIgnorePattern: '^_', varsIgnorePattern: '^__' },
17+
],
1318
'prettier/prettier': ['error'],
1419
'class-methods-use-this': 'off',
1520
'import/no-extraneous-dependencies': [
@@ -18,5 +23,11 @@ module.exports = {
1823
devDependencies: ['**/jest*.ts', '**/*.test.ts', '**/rollup.config.ts'],
1924
},
2025
],
26+
'import/default': 'error',
27+
'import/export': 'error',
28+
'import/no-self-import': 'error',
29+
'import/no-cycle': 'error',
30+
'import/no-useless-path-segments': 'error',
31+
'import/no-duplicates': 'error',
2132
},
2233
};

.github/workflows/mocks.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: shared/mocks
2+
3+
on:
4+
push:
5+
branches: [main, 'feat/**']
6+
paths-ignore:
7+
- '**.md' #Do not need to run CI for markdown changes.
8+
pull_request:
9+
branches: [main, 'feat/**']
10+
paths-ignore:
11+
- '**.md'
12+
13+
jobs:
14+
build-test-mocks:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v3
18+
- uses: actions/setup-node@v3
19+
- id: shared
20+
name: Shared CI Steps
21+
uses: ./actions/ci
22+
with:
23+
workspace_name: '@launchdarkly/private-js-mocks'
24+
workspace_path: packages/shared/mocks
25+
should_build_docs: false

actions/ci/action.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ inputs:
1111
workspace_path:
1212
description: 'Path to the package to release.'
1313
required: true
14-
14+
should_build_docs:
15+
description: 'Whether docs should be built. It will be by default.'
16+
default: true
1517
runs:
1618
using: composite
1719
steps:
@@ -40,4 +42,5 @@ runs:
4042

4143
- name: Build Docs
4244
shell: bash
45+
if: ${{inputs.should_build_docs == 'true'}}
4346
run: yarn build:doc -- ${{ inputs.workspace_path }}

contract-tests/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ app.get('/', (req, res) => {
2828
'tags',
2929
'big-segments',
3030
'user-type',
31+
'migrations',
32+
'event-sampling',
33+
'strongly-typed',
3134
],
3235
});
3336
});

contract-tests/sdkClientEntity.js

Lines changed: 187 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import ld from 'node-server-sdk';
1+
import got from 'got';
2+
import ld, {
3+
createMigration,
4+
LDConcurrentExecution,
5+
LDExecutionOrdering,
6+
LDMigrationError,
7+
LDMigrationSuccess,
8+
LDSerialExecution,
9+
} from 'node-server-sdk';
210

311
import BigSegmentTestStore from './BigSegmentTestStore.js';
412
import { Log, sdkLogger } from './log.js';
@@ -9,7 +17,7 @@ export { badCommandError };
917
export function makeSdkConfig(options, tag) {
1018
const cf = {
1119
logger: sdkLogger(tag),
12-
diagnosticOptOut: true
20+
diagnosticOptOut: true,
1321
};
1422
const maybeTime = (seconds) =>
1523
seconds === undefined || seconds === null ? undefined : seconds / 1000;
@@ -55,6 +63,30 @@ export function makeSdkConfig(options, tag) {
5563
return cf;
5664
}
5765

66+
function getExecution(order) {
67+
switch (order) {
68+
case 'serial': {
69+
return new LDSerialExecution(LDExecutionOrdering.Fixed);
70+
}
71+
case 'random': {
72+
return new LDSerialExecution(LDExecutionOrdering.Random);
73+
}
74+
case 'concurrent': {
75+
return new LDConcurrentExecution();
76+
}
77+
default: {
78+
throw new Error('Unsupported execution order.');
79+
}
80+
}
81+
}
82+
83+
function makeMigrationPostOptions(payload) {
84+
if (payload) {
85+
return { body: payload };
86+
}
87+
return {};
88+
}
89+
5890
export async function newSdkClientEntity(options) {
5991
const c = {};
6092
const log = Log(options.tag);
@@ -93,10 +125,65 @@ export async function newSdkClientEntity(options) {
93125
case 'evaluate': {
94126
const pe = params.evaluate;
95127
if (pe.detail) {
96-
return await client.variationDetail(pe.flagKey, pe.context || pe.user, pe.defaultValue);
128+
switch (pe.valueType) {
129+
case 'bool':
130+
return await client.boolVariationDetail(
131+
pe.flagKey,
132+
pe.context || pe.user,
133+
pe.defaultValue,
134+
);
135+
case 'int': // Intentional fallthrough.
136+
case 'double':
137+
return await client.numberVariationDetail(
138+
pe.flagKey,
139+
pe.context || pe.user,
140+
pe.defaultValue,
141+
);
142+
case 'string':
143+
return await client.stringVariationDetail(
144+
pe.flagKey,
145+
pe.context || pe.user,
146+
pe.defaultValue,
147+
);
148+
default:
149+
return await client.variationDetail(
150+
pe.flagKey,
151+
pe.context || pe.user,
152+
pe.defaultValue,
153+
);
154+
}
97155
} else {
98-
const value = await client.variation(pe.flagKey, pe.context || pe.user, pe.defaultValue);
99-
return { value };
156+
switch (pe.valueType) {
157+
case 'bool':
158+
return {
159+
value: await client.boolVariation(
160+
pe.flagKey,
161+
pe.context || pe.user,
162+
pe.defaultValue,
163+
),
164+
};
165+
case 'int': // Intentional fallthrough.
166+
case 'double':
167+
return {
168+
value: await client.numberVariation(
169+
pe.flagKey,
170+
pe.context || pe.user,
171+
pe.defaultValue,
172+
),
173+
};
174+
case 'string':
175+
return {
176+
value: await client.stringVariation(
177+
pe.flagKey,
178+
pe.context || pe.user,
179+
pe.defaultValue,
180+
),
181+
};
182+
default:
183+
return {
184+
value: await client.variation(pe.flagKey, pe.context || pe.user, pe.defaultValue),
185+
};
186+
}
100187
}
101188
}
102189

@@ -127,6 +214,101 @@ export async function newSdkClientEntity(options) {
127214
case 'getBigSegmentStoreStatus':
128215
return await client.bigSegmentStoreStatusProvider.requireStatus();
129216

217+
case 'migrationVariation':
218+
const migrationVariation = params.migrationVariation;
219+
const res = await client.migrationVariation(
220+
migrationVariation.key,
221+
migrationVariation.context,
222+
migrationVariation.defaultStage,
223+
);
224+
return { result: res.value };
225+
226+
case 'migrationOperation':
227+
const migrationOperation = params.migrationOperation;
228+
const readExecutionOrder = migrationOperation.readExecutionOrder;
229+
230+
const migration = createMigration(client, {
231+
execution: getExecution(readExecutionOrder),
232+
latencyTracking: migrationOperation.trackLatency,
233+
errorTracking: migrationOperation.trackErrors,
234+
check: migrationOperation.trackConsistency ? (a, b) => a === b : undefined,
235+
readNew: async (payload) => {
236+
try {
237+
const res = await got.post(
238+
migrationOperation.newEndpoint,
239+
makeMigrationPostOptions(payload),
240+
);
241+
return LDMigrationSuccess(res.body);
242+
} catch (err) {
243+
return LDMigrationError(err.message);
244+
}
245+
},
246+
writeNew: async (payload) => {
247+
try {
248+
const res = await got.post(
249+
migrationOperation.newEndpoint,
250+
makeMigrationPostOptions(payload),
251+
);
252+
return LDMigrationSuccess(res.body);
253+
} catch (err) {
254+
return LDMigrationError(err.message);
255+
}
256+
},
257+
readOld: async (payload) => {
258+
try {
259+
const res = await got.post(
260+
migrationOperation.oldEndpoint,
261+
makeMigrationPostOptions(payload),
262+
);
263+
return LDMigrationSuccess(res.body);
264+
} catch (err) {
265+
return LDMigrationError(err.message);
266+
}
267+
},
268+
writeOld: async (payload) => {
269+
try {
270+
const res = await got.post(
271+
migrationOperation.oldEndpoint,
272+
makeMigrationPostOptions(payload),
273+
);
274+
return LDMigrationSuccess(res.body);
275+
} catch (err) {
276+
return LDMigrationError(err.message);
277+
}
278+
},
279+
});
280+
281+
switch (migrationOperation.operation) {
282+
case 'read': {
283+
const res = await migration.read(
284+
migrationOperation.key,
285+
migrationOperation.context,
286+
migrationOperation.defaultStage,
287+
migrationOperation.payload,
288+
);
289+
if (res.success) {
290+
return { result: res.result };
291+
} else {
292+
return { result: res.error };
293+
}
294+
}
295+
case 'write': {
296+
const res = await migration.write(
297+
migrationOperation.key,
298+
migrationOperation.context,
299+
migrationOperation.defaultStage,
300+
migrationOperation.payload,
301+
);
302+
303+
if (res.authoritative.success) {
304+
return { result: res.authoritative.result };
305+
} else {
306+
return { result: res.authoritative.error };
307+
}
308+
}
309+
}
310+
return undefined;
311+
130312
default:
131313
throw badCommandError;
132314
}

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
"name": "@launchdarkly/js-core",
33
"workspaces": [
44
"packages/shared/common",
5+
"packages/shared/mocks",
6+
"packages/shared/sdk-client",
57
"packages/shared/sdk-server",
68
"packages/shared/sdk-server-edge",
79
"packages/shared/akamai-edgeworker-sdk",

packages/sdk/akamai-base/example/ldClient.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ const flagData = `
3939

4040
class MyCustomStoreProvider implements EdgeProvider {
4141
// root key is formatted as LD-Env-{Launchdarkly environment client ID}
42-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
43-
async get(rootKey: string): Promise<string> {
42+
async get(_rootKey: string): Promise<string> {
4443
// you should provide an implementation to retrieve your flags from launchdarkly's https://sdk.launchdarkly.com/sdk/latest-all endpoint.
4544
// see https://docs.launchdarkly.com/sdk/features/flags-from-files for more information.
4645
return flagData;

packages/sdk/server-node/__tests__/LDClientNode.test.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { LDContext } from '@launchdarkly/js-server-sdk-common';
2+
import { logger } from '@launchdarkly/private-js-mocks';
23

34
import { init } from '../src';
45

@@ -10,23 +11,24 @@ it('fires ready event in offline mode', (done) => {
1011
});
1112
});
1213

13-
it('fires the failed event if initialization fails', (done) => {
14+
it('fires the failed event if initialization fails', async () => {
15+
jest.useFakeTimers();
16+
17+
const failedHandler = jest.fn().mockName('failedHandler');
1418
const client = init('sdk_key', {
15-
updateProcessor: {
16-
start: (fn: (err: any) => void) => {
17-
setTimeout(() => {
18-
fn(new Error('BAD THINGS'));
19-
}, 0);
19+
sendEvents: false,
20+
logger,
21+
updateProcessor: (clientContext, dataSourceUpdates, initSuccessHandler, errorHandler) => ({
22+
start: () => {
23+
setTimeout(() => errorHandler?.(new Error('Something unexpected happened')), 0);
2024
},
21-
stop: () => {},
22-
close: () => {},
23-
sendEvents: false,
24-
},
25-
});
26-
client.on('failed', () => {
27-
client.close();
28-
done();
25+
close: jest.fn(),
26+
}),
2927
});
28+
client.on('failed', failedHandler);
29+
jest.runAllTimers();
30+
31+
expect(failedHandler).toBeCalledWith(new Error('Something unexpected happened'));
3032
});
3133

3234
// These tests are done in the node implementation because common doesn't have a crypto

packages/sdk/server-node/__tests__/LDClientNode.tls.test.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,14 @@ import {
66
TestHttpServer,
77
} from 'launchdarkly-js-test-helpers';
88

9-
import { basicLogger, LDClient, LDLogger } from '../src';
9+
import { logger } from '@launchdarkly/private-js-mocks';
10+
11+
import { LDClient } from '../src';
1012
import LDClientNode from '../src/LDClientNode';
1113

1214
describe('When using a TLS connection', () => {
1315
let client: LDClient;
1416
let server: TestHttpServer;
15-
let logger: LDLogger;
16-
17-
beforeEach(() => {
18-
logger = basicLogger({
19-
destination: () => {},
20-
});
21-
});
2217

2318
it('can connect via HTTPS to a server with a self-signed certificate, if CA is specified', async () => {
2419
server = await TestHttpServer.startSecure();
@@ -87,6 +82,7 @@ describe('When using a TLS connection', () => {
8782
stream: false,
8883
tlsParams: { ca: server.certificate },
8984
diagnosticOptOut: true,
85+
logger,
9086
});
9187

9288
await client.waitForInitialization();

0 commit comments

Comments
 (0)