Skip to content

Commit

Permalink
feat: adds ping stream support (#624)
Browse files Browse the repository at this point in the history
**Requirements**

- [x] I have added test coverage for new or changed functionality
- [x] I have followed the repository's [pull request submission
guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests)

- [ ] I have validated my changes against all supported platform
versions
-I have tested this against browser and mobile.

**Related issues**

SDK-198

**Describe the solution you've provided**

Added ping pathing
Passing Requestor into StreamingProcessor that now uses it when a ping
event is received.
Commonized requestor creation logic and using that helper function in
various data managers.
  • Loading branch information
tanderson-ld authored Oct 15, 2024
1 parent 8c84e01 commit dee53af
Show file tree
Hide file tree
Showing 17 changed files with 627 additions and 242 deletions.
76 changes: 50 additions & 26 deletions packages/sdk/browser/__tests__/BrowserDataManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,18 +144,24 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
browserConfig,
() => ({
pathGet(encoding: Encoding, _plainContextString: string): string {
return `/msdk/evalx/contexts/${base64UrlEncode(_plainContextString, encoding)}`;
return `/path/get/${base64UrlEncode(_plainContextString, encoding)}`;
},
pathReport(_encoding: Encoding, _plainContextString: string): string {
return `/msdk/evalx/context`;
pathReport(encoding: Encoding, _plainContextString: string): string {
return `/path/report/${base64UrlEncode(_plainContextString, encoding)}`;
},
pathPing(encoding: Encoding, _plainContextString: string): string {
return `/path/ping/${base64UrlEncode(_plainContextString, encoding)}`;
},
}),
() => ({
pathGet(encoding: Encoding, _plainContextString: string): string {
return `/meval/${base64UrlEncode(_plainContextString, encoding)}`;
return `/path/get/${base64UrlEncode(_plainContextString, encoding)}`;
},
pathReport(encoding: Encoding, _plainContextString: string): string {
return `/path/report/${base64UrlEncode(_plainContextString, encoding)}`;
},
pathReport(_encoding: Encoding, _plainContextString: string): string {
return `/meval`;
pathPing(encoding: Encoding, _plainContextString: string): string {
return `/path/ping/${base64UrlEncode(_plainContextString, encoding)}`;
},
}),
baseHeaders,
Expand All @@ -177,18 +183,24 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
validateOptions({ streaming: true }, logger),
() => ({
pathGet(encoding: Encoding, _plainContextString: string): string {
return `/msdk/evalx/contexts/${base64UrlEncode(_plainContextString, encoding)}`;
return `/path/get/${base64UrlEncode(_plainContextString, encoding)}`;
},
pathReport(_encoding: Encoding, _plainContextString: string): string {
return `/msdk/evalx/context`;
pathReport(encoding: Encoding, _plainContextString: string): string {
return `/path/report/${base64UrlEncode(_plainContextString, encoding)}`;
},
pathPing(encoding: Encoding, _plainContextString: string): string {
return `/path/ping/${base64UrlEncode(_plainContextString, encoding)}`;
},
}),
() => ({
pathGet(encoding: Encoding, _plainContextString: string): string {
return `/meval/${base64UrlEncode(_plainContextString, encoding)}`;
return `/path/get/${base64UrlEncode(_plainContextString, encoding)}`;
},
pathReport(encoding: Encoding, _plainContextString: string): string {
return `/path/report/${base64UrlEncode(_plainContextString, encoding)}`;
},
pathReport(_encoding: Encoding, _plainContextString: string): string {
return `/meval`;
pathPing(encoding: Encoding, _plainContextString: string): string {
return `/path/ping/${base64UrlEncode(_plainContextString, encoding)}`;
},
}),
baseHeaders,
Expand All @@ -215,18 +227,24 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
validateOptions({ streaming: true }, logger),
() => ({
pathGet(encoding: Encoding, _plainContextString: string): string {
return `/msdk/evalx/contexts/${base64UrlEncode(_plainContextString, encoding)}`;
return `/path/get/${base64UrlEncode(_plainContextString, encoding)}`;
},
pathReport(_encoding: Encoding, _plainContextString: string): string {
return `/msdk/evalx/context`;
pathReport(encoding: Encoding, _plainContextString: string): string {
return `/path/report/${base64UrlEncode(_plainContextString, encoding)}`;
},
pathPing(encoding: Encoding, _plainContextString: string): string {
return `/path/ping/${base64UrlEncode(_plainContextString, encoding)}`;
},
}),
() => ({
pathGet(encoding: Encoding, _plainContextString: string): string {
return `/meval/${base64UrlEncode(_plainContextString, encoding)}`;
return `/path/get/${base64UrlEncode(_plainContextString, encoding)}`;
},
pathReport(encoding: Encoding, _plainContextString: string): string {
return `/path/report/${base64UrlEncode(_plainContextString, encoding)}`;
},
pathReport(_encoding: Encoding, _plainContextString: string): string {
return `/meval`;
pathPing(encoding: Encoding, _plainContextString: string): string {
return `/path/ping/${base64UrlEncode(_plainContextString, encoding)}`;
},
}),
baseHeaders,
Expand All @@ -242,7 +260,7 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);

expect(platform.requests.createEventSource).toHaveBeenCalledWith(
'/meval/eyJraW5kIjoidXNlciIsImtleSI6InRlc3QtdXNlciJ9?h=potato&withReasons=true',
'/path/get/eyJraW5kIjoidXNlciIsImtleSI6InRlc3QtdXNlciJ9?h=potato&withReasons=true',
expect.anything(),
);
});
Expand All @@ -256,18 +274,24 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
validateOptions({ streaming: false }, logger),
() => ({
pathGet(encoding: Encoding, _plainContextString: string): string {
return `/msdk/evalx/contexts/${base64UrlEncode(_plainContextString, encoding)}`;
return `/path/get/${base64UrlEncode(_plainContextString, encoding)}`;
},
pathReport(_encoding: Encoding, _plainContextString: string): string {
return `/msdk/evalx/context`;
pathReport(encoding: Encoding, _plainContextString: string): string {
return `/path/report/${base64UrlEncode(_plainContextString, encoding)}`;
},
pathPing(encoding: Encoding, _plainContextString: string): string {
return `/path/ping/${base64UrlEncode(_plainContextString, encoding)}`;
},
}),
() => ({
pathGet(encoding: Encoding, _plainContextString: string): string {
return `/meval/${base64UrlEncode(_plainContextString, encoding)}`;
return `/path/get/${base64UrlEncode(_plainContextString, encoding)}`;
},
pathReport(encoding: Encoding, _plainContextString: string): string {
return `/path/report/${base64UrlEncode(_plainContextString, encoding)}`;
},
pathReport(_encoding: Encoding, _plainContextString: string): string {
return `/meval`;
pathPing(encoding: Encoding, _plainContextString: string): string {
return `/path/ping/${base64UrlEncode(_plainContextString, encoding)}`;
},
}),
baseHeaders,
Expand All @@ -283,7 +307,7 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);

expect(platform.requests.fetch).toHaveBeenCalledWith(
'/msdk/evalx/contexts/eyJraW5kIjoidXNlciIsImtleSI6InRlc3QtdXNlciJ9?withReasons=true&h=potato',
'/path/get/eyJraW5kIjoidXNlciIsImtleSI6InRlc3QtdXNlciJ9?withReasons=true&h=potato',
expect.anything(),
);
});
Expand Down
8 changes: 8 additions & 0 deletions packages/sdk/browser/src/BrowserClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ export class BrowserClient extends LDClientImpl implements LDClient {
pathReport(_encoding: Encoding, _plainContextString: string): string {
return `/sdk/evalx/${clientSideId}/context`;
},
pathPing(_encoding: Encoding, _plainContextString: string): string {
// Note: if you are seeing this error, it is a coding error. This DataSourcePaths implementation is for polling endpoints. /ping is not currently
// used in a polling situation. It is probably the case that this was called by streaming logic erroneously.
throw new Error('Ping for polling unsupported.');
},
}),
() => ({
pathGet(encoding: Encoding, _plainContextString: string): string {
Expand All @@ -148,6 +153,9 @@ export class BrowserClient extends LDClientImpl implements LDClient {
pathReport(_encoding: Encoding, _plainContextString: string): string {
return `/eval/${clientSideId}`;
},
pathPing(_encoding: Encoding, _plainContextString: string): string {
return `/ping/${clientSideId}`;
},
}),
baseHeaders,
emitter,
Expand Down
75 changes: 40 additions & 35 deletions packages/sdk/browser/src/BrowserDataManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@ import {
DataSourcePaths,
DataSourceState,
FlagManager,
getPollingUri,
internal,
LDEmitter,
LDHeaders,
LDIdentifyOptions,
makeRequestor,
Platform,
Requestor,
} from '@launchdarkly/js-client-sdk-common';

import { readFlagsFromBootstrap } from './bootstrap';
Expand Down Expand Up @@ -92,23 +91,35 @@ export default class BrowserDataManager extends BaseDataManager {
if (await this.flagManager.loadCached(context)) {
this._debugLog('Identify - Flags loaded from cache. Continuing to initialize via a poll.');
}
const plainContextString = JSON.stringify(Context.toLDContext(context));
const requestor = this._getRequestor(plainContextString);
await this._finishIdentifyFromPoll(requestor, context, identifyResolve, identifyReject);
}

await this._finishIdentifyFromPoll(context, identifyResolve, identifyReject);
}
this._updateStreamingState();
}

private async _finishIdentifyFromPoll(
requestor: Requestor,
context: Context,
identifyResolve: () => void,
identifyReject: (err: Error) => void,
) {
try {
this.dataSourceStatusManager.requestStateUpdate(DataSourceState.Initializing);
const payload = await requestor.requestPayload();

const plainContextString = JSON.stringify(Context.toLDContext(context));
const pollingRequestor = makeRequestor(
plainContextString,
this.config.serviceEndpoints,
this.getPollingPaths(),
this.platform.requests,
this.platform.encoding!,
this.baseHeaders,
[],
this.config.withReasons,
this.config.useReport,
this._secureModeHash,
);

const payload = await pollingRequestor.requestPayload();
try {
const listeners = this.createStreamListeners(context, identifyResolve);
const putListener = listeners.get('put');
Expand Down Expand Up @@ -196,35 +207,29 @@ export default class BrowserDataManager extends BaseDataManager {
const rawContext = Context.toLDContext(context)!;

this.updateProcessor?.close();
this.createStreamingProcessor(rawContext, context, identifyResolve, identifyReject);

this.updateProcessor!.start();
}

private _getRequestor(plainContextString: string): Requestor {
const paths = this.getPollingPaths();
const path = this.config.useReport
? paths.pathReport(this.platform.encoding!, plainContextString)
: paths.pathGet(this.platform.encoding!, plainContextString);

const parameters: { key: string; value: string }[] = [];
if (this.config.withReasons) {
parameters.push({ key: 'withReasons', value: 'true' });
}
if (this._secureModeHash) {
parameters.push({ key: 'h', value: this._secureModeHash });
}
const plainContextString = JSON.stringify(Context.toLDContext(context));
const pollingRequestor = makeRequestor(
plainContextString,
this.config.serviceEndpoints,
this.getPollingPaths(),
this.platform.requests,
this.platform.encoding!,
this.baseHeaders,
[],
this.config.withReasons,
this.config.useReport,
this._secureModeHash,
);

const headers: { [key: string]: string } = { ...this.baseHeaders };
let body;
let method = 'GET';
if (this.config.useReport) {
method = 'REPORT';
headers['content-type'] = 'application/json';
body = plainContextString; // context is in body for REPORT
}
this.createStreamingProcessor(
rawContext,
context,
pollingRequestor,
identifyResolve,
identifyReject,
);

const uri = getPollingUri(this.config.serviceEndpoints, path, parameters);
return new Requestor(this.platform.requests, uri, headers, method, body);
this.updateProcessor!.start();
}
}
8 changes: 8 additions & 0 deletions packages/sdk/react-native/__tests__/MobileDataManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ describe('given a MobileDataManager with mocked dependencies', () => {
pathReport(_encoding: Encoding, _plainContextString: string): string {
return `/msdk/evalx/context`;
},
pathPing(_encoding: Encoding, _plainContextString: string): string {
// Note: if you are seeing this error, it is a coding error. This DataSourcePaths implementation is for polling endpoints. /ping is not currently
// used in a polling situation. It is probably the case that this was called by streaming logic erroneously.
throw new Error('Ping for polling unsupported.');
},
}),
() => ({
pathGet(encoding: Encoding, _plainContextString: string): string {
Expand All @@ -141,6 +146,9 @@ describe('given a MobileDataManager with mocked dependencies', () => {
pathReport(_encoding: Encoding, _plainContextString: string): string {
return `/meval`;
},
pathPing(_encoding: Encoding, _plainContextString: string): string {
return `/mping`;
},
}),
baseHeaders,
emitter,
Expand Down
30 changes: 28 additions & 2 deletions packages/sdk/react-native/src/MobileDataManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
LDEmitter,
LDHeaders,
LDIdentifyOptions,
makeRequestor,
Platform,
} from '@launchdarkly/js-client-sdk-common';

Expand Down Expand Up @@ -95,13 +96,38 @@ export default class MobileDataManager extends BaseDataManager {
) {
const rawContext = Context.toLDContext(context)!;

const plainContextString = JSON.stringify(Context.toLDContext(context));
const requestor = makeRequestor(
plainContextString,
this.config.serviceEndpoints,
this.getPollingPaths(),
this.platform.requests,
this.platform.encoding!,
this.baseHeaders,
[],
this.config.useReport,
this.config.withReasons,
);

this.updateProcessor?.close();
switch (this.connectionMode) {
case 'streaming':
this.createStreamingProcessor(rawContext, context, identifyResolve, identifyReject);
this.createStreamingProcessor(
rawContext,
context,
requestor,
identifyResolve,
identifyReject,
);
break;
case 'polling':
this.createPollingProcessor(rawContext, context, identifyResolve, identifyReject);
this.createPollingProcessor(
rawContext,
context,
requestor,
identifyResolve,
identifyReject,
);
break;
default:
break;
Expand Down
8 changes: 8 additions & 0 deletions packages/sdk/react-native/src/ReactNativeLDClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ export default class ReactNativeLDClient extends LDClientImpl {
pathReport(_encoding: Encoding, _plainContextString: string): string {
return `/msdk/evalx/context`;
},
pathPing(_encoding: Encoding, _plainContextString: string): string {
// Note: if you are seeing this error, it is a coding error. This DataSourcePaths implementation is for polling endpoints. /ping is not currently
// used in a polling situation. It is probably the case that this was called by streaming logic erroneously.
throw new Error('Ping for polling unsupported.');
},
}),
() => ({
pathGet(encoding: Encoding, _plainContextString: string): string {
Expand All @@ -97,6 +102,9 @@ export default class ReactNativeLDClient extends LDClientImpl {
pathReport(_encoding: Encoding, _plainContextString: string): string {
return `/meval`;
},
pathPing(_encoding: Encoding, _plainContextString: string): string {
return `/mping`;
},
}),
baseHeaders,
emitter,
Expand Down
5 changes: 3 additions & 2 deletions packages/shared/sdk-client/__tests__/LDClientImpl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,8 +273,9 @@ describe('sdk-client object', () => {
const carContext: LDContext = { kind: 'car', key: 'test-car' };

await expect(ldc.identify(carContext)).rejects.toThrow('test-error');
expect(logger.error).toHaveBeenCalledTimes(1);
expect(logger.error).toHaveBeenCalledWith(expect.stringMatching(/^error:.*test-error/));
expect(logger.error).toHaveBeenCalledTimes(2);
expect(logger.error).toHaveBeenNthCalledWith(1, expect.stringMatching(/^error:.*test-error/));
expect(logger.error).toHaveBeenNthCalledWith(2, expect.stringContaining('Received error 404'));
});

test('identify change and error listeners', async () => {
Expand Down
Loading

0 comments on commit dee53af

Please sign in to comment.