Skip to content

Commit

Permalink
wip: partial solution; needs collab
Browse files Browse the repository at this point in the history
  • Loading branch information
Mike Chu committed Mar 28, 2024
1 parent 1c1c501 commit 92f09fb
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 41 deletions.
1 change: 1 addition & 0 deletions src/Provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export class OptimizelyProvider extends React.Component<OptimizelyProviderProps,
finalUser = DefaultUser;
}

// if user is a promise, setUser occurs in the then block above
if (finalUser) {
try {
await optimizely.onReady();
Expand Down
90 changes: 60 additions & 30 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ type OnUserUpdateHandler = (userInfo: UserInfo) => void;

type OnForcedVariationsUpdateHandler = () => void;

type NotReadyReason = 'TIMEOUT' | 'NO_CLIENT' | 'USER_NOT_READY';
export enum NotReadyReason {
TIMEOUT = 'TIMEOUT',
NO_CLIENT = 'NO_CLIENT',
USER_NOT_READY = 'USER_NOT_READY',
}

type ResolveResult = {
success: boolean;
Expand Down Expand Up @@ -192,21 +196,27 @@ export const DEFAULT_ON_READY_TIMEOUT = 5_000;
class OptimizelyReactSDKClient implements ReactSDKClient {
private userContext: optimizely.OptimizelyUserContext | null = null;
private onUserUpdateHandlers: OnUserUpdateHandler[] = [];
private userPromiseResolver: (resolveResult: ResolveResult) => void;
private isUserPromiseResolved = false;
// Its usually true from the beginning when user is provided as an object in the `OptimizelyProvider`
// This becomes more significant when a promise is provided instead.
private isUserReady = false;

private onForcedVariationsUpdateHandlers: OnForcedVariationsUpdateHandler[] = [];
private forcedDecisionFlagKeys: Set<string> = new Set<string>();

// Is the javascript SDK instance ready.
private isClientReady = false;

// We need to add autoupdate listener to the hooks after the instance became fully ready to avoid redundant updates to hooks
private isReadyPromiseFulfilled = false;
private clientAndUserReadyPromiseFulfilled = false;

private isUsingSdkKey = false;

private readonly _client: optimizely.Client | null;

// promise keeping track of async requests for initializing client instance
private dataReadyPromise: Promise<OnReadyResult>;
private clientAndUserReadyPromise: Promise<OnReadyResult>;

public initialConfig: optimizely.Config;
public user: UserInfo = { ...DefaultUser };
Expand All @@ -217,36 +227,45 @@ class OptimizelyReactSDKClient implements ReactSDKClient {
*/
constructor(config: optimizely.Config) {
this.initialConfig = config;

const configWithClientInfo = {
...config,
clientEngine: REACT_SDK_CLIENT_ENGINE,
clientVersion: REACT_SDK_CLIENT_VERSION,
};

this.userPromiseResolver = () => {};
const userReadyPromise = new Promise<OnReadyResult>(resolve => {
this.userPromiseResolver = resolve;
});

this._client = optimizely.createInstance(configWithClientInfo);
this.isClientReady = !!configWithClientInfo.datafile;
this.isUsingSdkKey = !!configWithClientInfo.sdkKey;

if (this._client) {
this.dataReadyPromise = this._client.onReady().then((clientResult: { success: boolean }) => {
this.isReadyPromiseFulfilled = true;
this.isClientReady = true;
const clientReadyPromise = this._client.onReady();
this.clientAndUserReadyPromise = Promise.all([userReadyPromise, clientReadyPromise]).then(
([userResult, clientResult]) => {
console.log('||| results', userResult, clientResult);
this.isClientReady = clientResult.success;
this.isUserReady = userResult.success;

return {
success: true,
message: clientResult.success
? 'Successfully resolved client datafile.'
: 'Client datafile was not not ready.',
};
});
// Client and user can become ready synchronously and/or asynchronously. This flag specifically indicates that they became ready asynchronously.
this.clientAndUserReadyPromiseFulfilled = true;

return {
success: true, // needs to always be true
message: this.isReady() ? 'Client and user are both.' : 'Client or user did not become ready.',
};
}
);
} else {
logger.warn('Unable to resolve datafile and user information because Optimizely client failed to initialize.');

this.dataReadyPromise = new Promise(resolve => {
this.clientAndUserReadyPromise = new Promise(resolve => {
resolve({
success: false,
reason: 'NO_CLIENT',
reason: NotReadyReason.NO_CLIENT,
message: 'Optimizely client failed to initialize.',
});
});
Expand All @@ -265,39 +284,42 @@ class OptimizelyReactSDKClient implements ReactSDKClient {
}

public getIsReadyPromiseFulfilled(): boolean {
return this.isReadyPromiseFulfilled;
return this.clientAndUserReadyPromiseFulfilled;
}

public getIsUsingSdkKey(): boolean {
return this.isUsingSdkKey;
}

private get odpExplicitlyOff() {
return this.initialConfig.odpOptions?.disabled;
}

public onReady(config: { timeout?: number } = {}): Promise<OnReadyResult> {
let timeoutId: number | undefined;
let timeout: number = DEFAULT_ON_READY_TIMEOUT;
if (config && config.timeout !== undefined) {
timeout = config.timeout;
}

const timeoutPromise = new Promise<OnReadyResult>(resolve => {
timeoutId = setTimeout(() => {
resolve({
success: false,
reason: 'TIMEOUT',
message: 'Failed to initialize onReady before timeout, data was not set before the timeout period',
dataReadyPromise: this.dataReadyPromise,
reason: NotReadyReason.TIMEOUT,
message: 'Failed to initialize before timeout',
dataReadyPromise: this.clientAndUserReadyPromise,
});
}, timeout) as any;
});

return Promise.race([this.dataReadyPromise, timeoutPromise]).then(async res => {
return Promise.race([this.clientAndUserReadyPromise, timeoutPromise]).then(async res => {
clearTimeout(timeoutId);
if (res.success && !this.initialConfig.odpOptions?.disabled) {
if (res.success && !this.odpExplicitlyOff) {
const isSegmentsFetched = await this.fetchQualifiedSegments();
if (!isSegmentsFetched) {
return {
success: false,
reason: 'USER_NOT_READY',
reason: NotReadyReason.USER_NOT_READY,
message: 'Failed to fetch qualified segments',
};
}
Expand Down Expand Up @@ -328,6 +350,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient {

if (!this.userContext || (this.userContext && !areUsersEqual(userInfo, this.user))) {
this.userContext = this._client.createUserContext(userInfo.id || undefined, userInfo.attributes);
console.log('||| user context now', this.userContext);
}
}

Expand All @@ -343,7 +366,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient {
}

public async fetchQualifiedSegments(options?: optimizely.OptimizelySegmentOption[]): Promise<boolean> {
if (!this.userContext || !this.isReady()) {
if (!this.userContext || !this.isReady() || this.odpExplicitlyOff || !this.getIsReadyPromiseFulfilled()) {
return false;
}

Expand All @@ -354,13 +377,19 @@ class OptimizelyReactSDKClient implements ReactSDKClient {
this.setCurrentUserContext(userInfo);

this.user = {
id: userInfo.id || DefaultUser.id,
id: userInfo.id || this.getUserContext()?.getUserId() || DefaultUser.id,
attributes: userInfo.attributes || DefaultUser.attributes,
};

if (this.getIsReadyPromiseFulfilled()) {
await this.fetchQualifiedSegments();
}
// Set user can occur before the client is ready or later if the user is updated.
await this.fetchQualifiedSegments();

this.isUserReady = true;

if (!this.isUserPromiseResolved) {
this.userPromiseResolver({ success: true });
this.isUserPromiseResolved = true;
}

this.onUserUpdateHandlers.forEach(handler => handler(this.user));
}
Expand Down Expand Up @@ -394,7 +423,8 @@ class OptimizelyReactSDKClient implements ReactSDKClient {
}

public isReady(): boolean {
return this.isClientReady;
// React SDK Instance only becomes ready when both JS SDK client and the user info are ready.
return this.isUserReady && this.isClientReady;
}

/**
Expand Down
30 changes: 22 additions & 8 deletions src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { useCallback, useContext, useEffect, useState, useRef } from 'react';
import { UserAttributes, OptimizelyDecideOption, getLogger } from '@optimizely/optimizely-sdk';

import { setupAutoUpdateListeners } from './autoUpdate';
import { ReactSDKClient, VariableValuesObject, OnReadyResult } from './client';
import { ReactSDKClient, VariableValuesObject, OnReadyResult, NotReadyReason } from './client';
import { notifier } from './notifier';
import { OptimizelyContext } from './Context';
import { areAttributesEqual, OptimizelyDecision, createFailedDecision } from './utils';
Expand Down Expand Up @@ -127,7 +127,7 @@ function subscribeToInitialization(
.onReady({ timeout })
.then((res: OnReadyResult) => {
if (res.success) {
hooksLogger.info('Client became ready');
hooksLogger.info('Client immediately ready');
onInitStateChange({
clientReady: true,
didTimeout: false,
Expand All @@ -137,7 +137,7 @@ function subscribeToInitialization(

switch (res.reason) {
// Optimizely client failed to initialize.
case 'NO_CLIENT':
case NotReadyReason.NO_CLIENT:
hooksLogger.warn(`Client not ready, reason="${res.message}"`);
onInitStateChange({
clientReady: false,
Expand All @@ -151,22 +151,36 @@ function subscribeToInitialization(
});
});
break;
// Assume timeout for all other cases.
// TODO: Other reasons may fall into this case - need to update later to specify 'TIMEOUT' case and general fallback case.
default:
hooksLogger.info(`Client did not become ready before timeout of ${timeout}ms, reason="${res.message}"`);
case NotReadyReason.USER_NOT_READY:
hooksLogger.warn(`User was not ready, reason="${res.message}"`);
onInitStateChange({
clientReady: false,
didTimeout: false,
});
res.dataReadyPromise?.then(() => {
hooksLogger.info('User became ready later.');
onInitStateChange({
clientReady: true,
didTimeout: false,
});
});
break;
case NotReadyReason.TIMEOUT:
hooksLogger.info(`Client did not become ready before timeout of ${timeout} ms, reason="${res.message}"`);
onInitStateChange({
clientReady: false,
didTimeout: true,
});
// dataReadyPromise? is optional in the OnReadyResult interface in client.ts
res.dataReadyPromise?.then(() => {
hooksLogger.info('Client became ready after timeout already elapsed');
onInitStateChange({
clientReady: true,
didTimeout: true,
});
});
break;
default:
hooksLogger.warn(`Other reason client not ready, reason="${res.message}"`);
}
})
.catch(() => {
Expand Down
6 changes: 3 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1624,7 +1624,7 @@ decode-uri-component@^0.2.0:

decompress-response@^4.2.1:
version "4.2.1"
resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz"
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986"
integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==
dependencies:
mimic-response "^2.0.0"
Expand Down Expand Up @@ -3311,7 +3311,7 @@ json-schema-traverse@^0.4.1:

json-schema@^0.4.0:
version "0.4.0"
resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz"
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5"
integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==

json-stable-stringify-without-jsonify@^1.0.1:
Expand Down Expand Up @@ -3511,7 +3511,7 @@ mimic-fn@^2.1.0:

mimic-response@^2.0.0:
version "2.1.0"
resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz"
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43"
integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==

min-indent@^1.0.0:
Expand Down

0 comments on commit 92f09fb

Please sign in to comment.