diff --git a/lib/shared/bucketing-assembly-script/__tests__/bucketing/bucketing.test.ts b/lib/shared/bucketing-assembly-script/__tests__/bucketing/bucketing.test.ts index 3cfc976ab..3d5e44a7e 100644 --- a/lib/shared/bucketing-assembly-script/__tests__/bucketing/bucketing.test.ts +++ b/lib/shared/bucketing-assembly-script/__tests__/bucketing/bucketing.test.ts @@ -153,8 +153,8 @@ describe('User Hashing and Bucketing', () => { ], } - // run 100,000 times to get a good distribution - for (let i = 0; i < 100000; i++) { + // run 200,000 times to get a good distribution + for (let i = 0; i < 200000; i++) { const user_id = uuid.v4() const { bucketingHash } = generateBoundedHashes( user_id, diff --git a/lib/shared/bucketing/__tests__/bucketing.test.ts b/lib/shared/bucketing/__tests__/bucketing.test.ts index 1733e27cc..e622c6f21 100644 --- a/lib/shared/bucketing/__tests__/bucketing.test.ts +++ b/lib/shared/bucketing/__tests__/bucketing.test.ts @@ -32,8 +32,8 @@ describe('User Hashing and Bucketing', () => { ], } - // run 100,000 times to get a good distribution - times(100000, () => { + // run 200,000 times to get a good distribution + times(200000, () => { const user_id = uuid.v4() const { bucketingHash } = generateBoundedHashes( user_id, diff --git a/lib/shared/types/src/index.ts b/lib/shared/types/src/index.ts index 9b7d3b756..dd3d2e7a8 100644 --- a/lib/shared/types/src/index.ts +++ b/lib/shared/types/src/index.ts @@ -11,3 +11,4 @@ export * from './utils' export * from './types/ConfigSource' export * from './types/UserError' export * from './types/variableKeys' +export * from './types/SSETypes' diff --git a/lib/shared/types/src/types/SSETypes.ts b/lib/shared/types/src/types/SSETypes.ts new file mode 100644 index 000000000..dd462fe1b --- /dev/null +++ b/lib/shared/types/src/types/SSETypes.ts @@ -0,0 +1,16 @@ +import type { DVCLogger } from '../logger' + +export interface SSEConnectionInterface { + updateURL(url: string): void + isConnected(): boolean + reopen(): void + close(): void +} + +export interface SSEConnectionConstructor { + new ( + url: string, + onMessage: (message: unknown) => void, + logger: DVCLogger, + ): SSEConnectionInterface +} diff --git a/package.json b/package.json index adc355cca..1862aa25d 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "react-native": "0.72.4", "react-native-device-info": "^8.7.0", "react-native-get-random-values": "^1.7.2", + "react-native-sse": "^1.2.1", "reflect-metadata": "^0.1.13", "regenerator-runtime": "0.13.7", "server-only": "^0.0.1", diff --git a/sdk/js/src/Client.ts b/sdk/js/src/Client.ts index 6e48c09b0..393665c86 100644 --- a/sdk/js/src/Client.ts +++ b/sdk/js/src/Client.ts @@ -21,14 +21,17 @@ import { checkParamDefined } from './utils' import { EventEmitter } from './EventEmitter' import type { BucketedUserConfig, - InferredVariableType, VariableDefinitions, VariableTypeAlias, } from '@devcycle/types' import { getVariableTypeFromValue } from '@devcycle/types' import { ConfigRequestConsolidator } from './ConfigRequestConsolidator' import { dvcDefaultLogger } from './logger' -import type { DVCLogger } from '@devcycle/types' +import type { + DVCLogger, + SSEConnectionInterface, + SSEConnectionConstructor, +} from '@devcycle/types' import { StreamingConnection } from './StreamingConnection' type variableUpdatedHandler = ( @@ -89,7 +92,7 @@ export class DevCycleClient< private eventQueue?: EventQueue private requestConsolidator: ConfigRequestConsolidator eventEmitter: EventEmitter - private streamingConnection?: StreamingConnection + private streamingConnection?: SSEConnectionInterface private pageVisibilityHandler?: () => void private inactivityHandlerId?: number private windowMessageHandler?: (event: MessageEvent) => void @@ -747,10 +750,11 @@ export class DevCycleClient< // Update the streaming connection URL if it has changed (for ex. if the current user has targeting overrides) if (config?.sse?.url) { - // construct the streamingConnection if necessary if (!this.streamingConnection) { if (!this.options.disableRealtimeUpdates) { - this.streamingConnection = new StreamingConnection( + const SSEConnectionClass = + this.options.sseConnectionClass || StreamingConnection + this.streamingConnection = new SSEConnectionClass( config.sse.url, this.onSSEMessage.bind(this), this.logger, diff --git a/sdk/js/src/StreamingConnection.ts b/sdk/js/src/StreamingConnection.ts index bd444f8cc..dbdf53a2e 100644 --- a/sdk/js/src/StreamingConnection.ts +++ b/sdk/js/src/StreamingConnection.ts @@ -1,6 +1,6 @@ -import type { DVCLogger } from '@devcycle/types' +import type { DVCLogger, SSEConnectionInterface } from '@devcycle/types' -export class StreamingConnection { +export class StreamingConnection implements SSEConnectionInterface { private connection?: EventSource constructor( diff --git a/sdk/js/src/types.ts b/sdk/js/src/types.ts index d4e931626..be3c4dcc4 100644 --- a/sdk/js/src/types.ts +++ b/sdk/js/src/types.ts @@ -9,6 +9,7 @@ import type { BucketedUserConfig, VariableKey, InferredVariableType, + SSEConnectionConstructor, } from '@devcycle/types' export { UserError } from '@devcycle/types' @@ -81,6 +82,10 @@ export interface DevCycleOptions { * Used to know if we are running in a React Native environment. */ reactNative?: boolean + /** + * Custom SSE connection class to use for the SDK. + */ + sseConnectionClass?: SSEConnectionConstructor /** * Disable Realtime Update and their SSE connection. */ diff --git a/sdk/react-native/package.json b/sdk/react-native/package.json index 4e7bf796f..940ebf930 100644 --- a/sdk/react-native/package.json +++ b/sdk/react-native/package.json @@ -28,9 +28,11 @@ "dependencies": { "@devcycle/js-client-sdk": "^1.32.2", "@devcycle/react-client-sdk": "^1.30.2", + "@devcycle/types": "^1.19.2", "@react-native-async-storage/async-storage": "^1.17.11", "react-native-device-info": "^8.7.0", - "react-native-get-random-values": "^1.7.2" + "react-native-get-random-values": "^1.7.2", + "react-native-sse": "^1.2.1" }, "peerDependencies": { "react": ">=17.0.2", diff --git a/sdk/react-native/src/DevCycleProvider.tsx b/sdk/react-native/src/DevCycleProvider.tsx index 735b0efba..ae04ddae4 100644 --- a/sdk/react-native/src/DevCycleProvider.tsx +++ b/sdk/react-native/src/DevCycleProvider.tsx @@ -1,6 +1,7 @@ import React from 'react' import { DevCycleProvider as ReactDVCProvider } from '@devcycle/react-client-sdk' import ReactNativeStore from './ReactNativeCacheStore' +import { ReactNativeSSEConnection } from './ReactNativeSSEConnection' type PropsType = Parameters[0] @@ -24,6 +25,7 @@ export const getReactNativeConfig = ( ...config.options, sdkPlatform: 'react-native', reactNative: true, + sseConnectionClass: ReactNativeSSEConnection, }, } if (!config.options?.storage) { diff --git a/sdk/react-native/src/ReactNativeSSEConnection.ts b/sdk/react-native/src/ReactNativeSSEConnection.ts new file mode 100644 index 000000000..e6e9f5973 --- /dev/null +++ b/sdk/react-native/src/ReactNativeSSEConnection.ts @@ -0,0 +1,70 @@ +import EventSource from 'react-native-sse' +import type { DVCLogger, SSEConnectionInterface } from '@devcycle/types' + +export class ReactNativeSSEConnection implements SSEConnectionInterface { + private connection?: EventSource + private isConnectionOpen = false + + constructor( + private url: string, + private onMessage: (message: unknown) => void, + private logger: DVCLogger, + ) { + this.openConnection() + } + + public updateURL(url: string): void { + this.close() + this.url = url + this.openConnection() + } + + private openConnection() { + this.connection = new EventSource(this.url, { + debug: false, + // start connection immediately + timeoutBeforeConnection: 0, + // disable request timeout so connections are kept open + timeout: 0, + // enable withCredentials so we can send cookies + withCredentials: true, + }) + + this.connection.addEventListener('message', (event) => { + this.logger.debug(`ReactNativeSSEConnection message. ${event.data}`) + this.onMessage(event.data) + }) + + this.connection.addEventListener('error', (error) => { + this.logger.error( + `ReactNativeSSEConnection error. ${ + (error as any)?.message || JSON.stringify(error) + }`, + ) + }) + + this.connection.addEventListener('open', () => { + this.logger.debug('ReactNativeSSEConnection opened') + this.isConnectionOpen = true + }) + this.connection.addEventListener('close', () => { + this.logger.debug('ReactNativeSSEConnection closed') + this.isConnectionOpen = false + }) + } + + isConnected(): boolean { + return this.isConnectionOpen + } + + reopen(): void { + if (!this.isConnected()) { + this.close() + this.openConnection() + } + } + + close(): void { + this.connection?.close() + } +} diff --git a/yarn.lock b/yarn.lock index fc88ede69..44e34b198 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4790,9 +4790,11 @@ __metadata: dependencies: "@devcycle/js-client-sdk": ^1.32.2 "@devcycle/react-client-sdk": ^1.30.2 + "@devcycle/types": ^1.19.2 "@react-native-async-storage/async-storage": ^1.17.11 react-native-device-info: ^8.7.0 react-native-get-random-values: ^1.7.2 + react-native-sse: ^1.2.1 peerDependencies: react: ">=17.0.2" react-native: ">=0.68.0" @@ -15420,6 +15422,7 @@ __metadata: react-native-config: 1.5.0 react-native-device-info: ^8.7.0 react-native-get-random-values: ^1.7.2 + react-native-sse: ^1.2.1 react-native-svg: 13.9.0 react-native-svg-transformer: ^1.0.0 react-refresh: ^0.10.0 @@ -27439,6 +27442,13 @@ __metadata: languageName: node linkType: hard +"react-native-sse@npm:^1.2.1": + version: 1.2.1 + resolution: "react-native-sse@npm:1.2.1" + checksum: 424910fa1bcc6643a7e9f628f2710bf185c862b63375ea6ec4b8eecfb5b714055480757ea1250ce6cdae66c2785f6a64432cdf9833e0ec0411b59f9791f6cce8 + languageName: node + linkType: hard + "react-native-svg-transformer@npm:^1.0.0": version: 1.0.0 resolution: "react-native-svg-transformer@npm:1.0.0"