Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: implement initial flag fetch #294

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 56 additions & 21 deletions contract-tests/sdkClientEntity.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import got from 'got';
import ld, {
createMigration,
LDConcurrentExecution,
LDExecutionOrdering,
LDMigrationError,
LDMigrationSuccess,
LDSerialExecution,
createMigration,
} from 'node-server-sdk';

import BigSegmentTestStore from './BigSegmentTestStore.js';
Expand All @@ -17,7 +17,7 @@ export { badCommandError };
export function makeSdkConfig(options, tag) {
const cf = {
logger: sdkLogger(tag),
diagnosticOptOut: true
diagnosticOptOut: true,
};
const maybeTime = (seconds) =>
seconds === undefined || seconds === null ? undefined : seconds / 1000;
Expand Down Expand Up @@ -125,29 +125,64 @@ export async function newSdkClientEntity(options) {
case 'evaluate': {
const pe = params.evaluate;
if (pe.detail) {
switch(pe.valueType) {
case "bool":
return await client.boolVariationDetail(pe.flagKey, pe.context || pe.user, pe.defaultValue);
case "int": // Intentional fallthrough.
case "double":
return await client.numberVariationDetail(pe.flagKey, pe.context || pe.user, pe.defaultValue);
case "string":
return await client.stringVariationDetail(pe.flagKey, pe.context || pe.user, pe.defaultValue);
switch (pe.valueType) {
case 'bool':
return await client.boolVariationDetail(
pe.flagKey,
pe.context || pe.user,
pe.defaultValue,
);
case 'int': // Intentional fallthrough.
case 'double':
return await client.numberVariationDetail(
pe.flagKey,
pe.context || pe.user,
pe.defaultValue,
);
case 'string':
return await client.stringVariationDetail(
pe.flagKey,
pe.context || pe.user,
pe.defaultValue,
);
default:
return await client.variationDetail(pe.flagKey, pe.context || pe.user, pe.defaultValue);
return await client.variationDetail(
pe.flagKey,
pe.context || pe.user,
pe.defaultValue,
);
}

} else {
switch(pe.valueType) {
case "bool":
return {value: await client.boolVariation(pe.flagKey, pe.context || pe.user, pe.defaultValue)};
case "int": // Intentional fallthrough.
case "double":
return {value: await client.numberVariation(pe.flagKey, pe.context || pe.user, pe.defaultValue)};
case "string":
return {value: await client.stringVariation(pe.flagKey, pe.context || pe.user, pe.defaultValue)};
switch (pe.valueType) {
case 'bool':
return {
value: await client.boolVariation(
pe.flagKey,
pe.context || pe.user,
pe.defaultValue,
),
};
case 'int': // Intentional fallthrough.
case 'double':
return {
value: await client.numberVariation(
pe.flagKey,
pe.context || pe.user,
pe.defaultValue,
),
};
case 'string':
return {
value: await client.stringVariation(
pe.flagKey,
pe.context || pe.user,
pe.defaultValue,
),
};
default:
return {value: await client.variation(pe.flagKey, pe.context || pe.user, pe.defaultValue)};
return {
value: await client.variation(pe.flagKey, pe.context || pe.user, pe.defaultValue),
};
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions packages/shared/common/src/api/platform/Encoding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface Encoding {
btoa(data: string): string;
}
6 changes: 6 additions & 0 deletions packages/shared/common/src/api/platform/Platform.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { Crypto } from './Crypto';
import { Encoding } from './Encoding';
import { Filesystem } from './Filesystem';
import { Info } from './Info';
import { Requests } from './Requests';

export interface Platform {
/**
* The interface for performing encoding operations.
*/
encoding?: Encoding;

/**
* The interface for getting information about the platform and the execution
* environment.
Expand Down
1 change: 1 addition & 0 deletions packages/shared/common/src/api/platform/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './Encoding';
export * from './Crypto';
export * from './Filesystem';
export * from './Info';
Expand Down
4 changes: 2 additions & 2 deletions packages/shared/mocks/src/clientContext.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { ClientContext } from '@common';

import platform from './platform';
import basicPlatform from './platform';

const clientContext: ClientContext = {
basicConfiguration: {
sdkKey: 'testSdkKey',
serviceEndpoints: { events: '', polling: '', streaming: 'https://mockstream.ld.com' },
},
platform,
platform: basicPlatform,
};

export default clientContext;
2 changes: 2 additions & 0 deletions packages/shared/mocks/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import clientContext from './clientContext';
import ContextDeduplicator from './contextDeduplicator';
import { crypto, hasher } from './hasher';
import logger from './logger';
import mockFetch from './mockFetch';
import basicPlatform from './platform';
import { MockStreamingProcessor, setupMockStreamingProcessor } from './streamingProcessor';

export {
basicPlatform,
clientContext,
mockFetch,
crypto,
logger,
hasher,
Expand Down
32 changes: 32 additions & 0 deletions packages/shared/mocks/src/mockFetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Response } from '@common';

import basicPlatform from './platform';

const createMockResponse = (remoteJson: any, statusCode: number) => {
const response: Response = {
headers: {
get: jest.fn(),
keys: jest.fn(),
values: jest.fn(),
entries: jest.fn(),
has: jest.fn(),
},
status: statusCode,
text: jest.fn(),
json: () => Promise.resolve(remoteJson),
};
return Promise.resolve(response);
};

/**
* Mocks basicPlatform fetch. Returns the fetch jest.Mock object.
* @param remoteJson
* @param statusCode
*/
const mockFetch = (remoteJson: any, statusCode: number = 200): jest.Mock => {
const f = basicPlatform.requests.fetch as jest.Mock;
f.mockResolvedValue(createMockResponse(remoteJson, statusCode));
return f;
};

export default mockFetch;
7 changes: 6 additions & 1 deletion packages/shared/mocks/src/platform.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import type { Info, Platform, PlatformData, Requests, SdkData } from '@common';
import type { Encoding, Info, Platform, PlatformData, Requests, SdkData } from '@common';

import { crypto } from './hasher';

const encoding: Encoding = {
btoa: (s: string) => Buffer.from(s).toString('base64'),
};

const info: Info = {
platformData(): PlatformData {
return {
Expand Down Expand Up @@ -33,6 +37,7 @@ const requests: Requests = {
};

const basicPlatform: Platform = {
encoding,
info,
crypto,
requests,
Expand Down
1 change: 1 addition & 0 deletions packages/shared/sdk-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"semver": "7.5.4"
},
"devDependencies": {
"@launchdarkly/private-js-mocks": "0.0.1",
"@testing-library/dom": "^9.3.1",
"@testing-library/jest-dom": "^5.16.5",
"@types/jest": "^29.5.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,76 @@

/* eslint-disable @typescript-eslint/no-unused-vars */
import {
Context,
internal,
LDContext,
LDEvaluationDetail,
LDFlagSet,
LDFlagValue,
LDLogger,
Platform,
subsystem,
} from '@launchdarkly/js-sdk-common';

import { LDClientDom } from './api/LDClientDom';
import { LDClient } from './api/LDClient';
import LDEmitter, { EventName } from './api/LDEmitter';
import LDOptions from './api/LDOptions';
import Configuration from './configuration';
import createDiagnosticsManager from './diagnostics/createDiagnosticsManager';
import fetchFlags, { Flags } from './evaluation/fetchFlags';
import createEventProcessor from './events/createEventProcessor';
import { PlatformDom, Storage } from './platform/PlatformDom';

export default class LDClientDomImpl implements LDClientDom {
export default class LDClientImpl implements LDClient {
config: Configuration;
diagnosticsManager?: internal.DiagnosticsManager;
eventProcessor: subsystem.LDEventProcessor;
storage: Storage;
private emitter: LDEmitter;
private flags: Flags = {};
private logger: LDLogger;

/**
* Creates the client object synchronously. No async, no network calls.
*/
constructor(
public readonly sdkKey: string,
public readonly context: LDContext,
public readonly platform: Platform,
options: LDOptions,
) {
if (!sdkKey) {
throw new Error('You must configure the client with a client-side SDK key');
}

const checkedContext = Context.fromLDContext(context);
if (!checkedContext.valid) {
throw new Error('Context was unspecified or had no key');
}

if (!platform.encoding) {
throw new Error('Platform must implement Encoding because btoa is required.');
}

constructor(clientSideID: string, context: LDContext, options: LDOptions, platform: PlatformDom) {
this.config = new Configuration(options);
this.storage = platform.storage;
this.diagnosticsManager = createDiagnosticsManager(clientSideID, this.config, platform);
this.logger = this.config.logger;
this.diagnosticsManager = createDiagnosticsManager(sdkKey, this.config, platform);
this.eventProcessor = createEventProcessor(
clientSideID,
sdkKey,
this.config,
platform,
this.diagnosticsManager,
);
this.emitter = new LDEmitter();
}

async start() {
try {
this.flags = await fetchFlags(this.sdkKey, this.context, this.config, this.platform);
this.emitter.emit('ready');
} catch (error: any) {
this.logger.error(error);
this.emitter.emit('error', error);
this.emitter.emit('failed', error);
}
}

allFlags(): LDFlagSet {
Expand All @@ -59,9 +98,13 @@ export default class LDClientDomImpl implements LDClientDom {
return Promise.resolve({});
}

off(key: string, callback: (...args: any[]) => void, context?: any): void {}
off(eventName: EventName, listener?: Function): void {
this.emitter.off(eventName, listener);
}

on(key: string, callback: (...args: any[]) => void, context?: any): void {}
on(eventName: EventName, listener: Function): void {
this.emitter.on(eventName, listener);
}

setStreaming(value?: boolean): void {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { LDContext, LDEvaluationDetail, LDFlagSet, LDFlagValue } from '@launchda
*
* @ignore (don't need to show this separately in TypeDoc output; all methods will be shown in LDClient)
*/
export interface LDClientDom {
export interface LDClient {
/**
* Returns a Promise that tracks the client's initialization state.
*
Expand Down Expand Up @@ -172,7 +172,7 @@ export interface LDClientDom {
*
* If this is true, the client will always attempt to maintain a streaming connection; if false,
* it never will. If you leave the value undefined (the default), the client will open a streaming
* connection if you subscribe to `"change"` or `"change:flag-key"` events (see {@link LDClientDom.on}).
* connection if you subscribe to `"change"` or `"change:flag-key"` events (see {@link LDClient.on}).
*
* This can also be set as the `streaming` property of {@link LDOptions}.
*/
Expand Down Expand Up @@ -206,7 +206,7 @@ export interface LDClientDom {
* The `"change"` and `"change:FLAG-KEY"` events have special behavior: by default, the
* client will open a streaming connection to receive live changes if and only if
* you are listening for one of these events. This behavior can be overridden by
* setting `streaming` in {@link LDOptions} or calling {@link LDClientDom.setStreaming}.
* setting `streaming` in {@link LDOptions} or calling {@link LDClient.setStreaming}.
*
* @param key
* The name of the event for which to listen.
Expand Down
Loading