Skip to content

Commit

Permalink
chore: implement initial flag fetch (#294)
Browse files Browse the repository at this point in the history
First attempt to implement an initial flag fetch followed by emitting
events. I also added comments like this:

```tsx
Dom api usage: xxx
```

There are three right now: fetch, btoa and EventTarget. I left comments
in the code for react native how to deal with these.

---------

Co-authored-by: LaunchDarklyReleaseBot <[email protected]>
Co-authored-by: Ryan Lamb <[email protected]>
  • Loading branch information
3 people authored Oct 10, 2023
1 parent a0dac0d commit 8f34dfa
Show file tree
Hide file tree
Showing 27 changed files with 623 additions and 73 deletions.
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

0 comments on commit 8f34dfa

Please sign in to comment.