Skip to content

Commit

Permalink
feat: local evaluation (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
bgiori authored Jul 17, 2022
1 parent 1a6a57f commit 6be8686
Show file tree
Hide file tree
Showing 24 changed files with 3,160 additions and 2,599 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ dist/
.cache
.DS_Store

# WebStorm IDE
.idea
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
dist/
CHANGELOG.md
gen/
CHANGELOG.md
7 changes: 5 additions & 2 deletions packages/node/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@amplitude/experiment-node-server",
"version": "1.0.3",
"version": "1.1.0-alpha.10",
"description": "Javascript Server SDK for Amplitude Experiment",
"main": "dist/src/index.js",
"files": [
Expand Down Expand Up @@ -28,5 +28,8 @@
"url": "https://github.com/amplitude/experiment-node-server/issues"
},
"homepage": "https://github.com/amplitude/experiment-node-server#readme",
"gitHead": "e8ceef5275b6daf4f7f26c6ac2e1683eeba7aca6"
"gitHead": "e8ceef5275b6daf4f7f26c6ac2e1683eeba7aca6",
"dependencies": {
"@amplitude/evaluation-js": "0.0.4"
}
}
75 changes: 0 additions & 75 deletions packages/node/src/config.ts

This file was deleted.

File renamed without changes.
70 changes: 62 additions & 8 deletions packages/node/src/factory.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,84 @@
import { ExperimentClient } from './client';
import { ExperimentConfig } from './config';
import { InMemoryFlagConfigCache } from './local/cache';
import { LocalEvaluationClient } from './local/client';
import { ExperimentClient, RemoteEvaluationClient } from './remote/client';
import {
ExperimentConfig,
RemoteEvaluationConfig,
LocalEvaluationConfig,
} from './types/config';

const remoteEvaluationInstances = {};
const localEvaluationInstances = {};

const instances = {};
const defaultInstance = '$default_instance';

/**
* Initializes a singleton {@link ExperimentClient}.
* Initializes a singleton {@link ExperimentClient} for remote evaluation.
*
* @param apiKey The environment API Key
* @param config See {@link ExperimentConfig} for config options
* @deprecated use initializeRemote
*/
const initialize = (
apiKey: string,
config?: ExperimentConfig,
): ExperimentClient => {
if (!instances[defaultInstance]) {
instances[defaultInstance] = new ExperimentClient(apiKey, config);
return initializeRemote(apiKey, config) as ExperimentClient;
};

/**
* Initializes a singleton {@link ExperimentClient} for remote evaluation.
*
* @param apiKey The environment API Key
* @param config See {@link ExperimentConfig} for config options
*/
const initializeRemote = (
apiKey: string,
config?: RemoteEvaluationConfig,
): RemoteEvaluationClient => {
if (!remoteEvaluationInstances[defaultInstance]) {
remoteEvaluationInstances[defaultInstance] = new RemoteEvaluationClient(
apiKey,
config,
);
}
return remoteEvaluationInstances[defaultInstance];
};

/**
* Initialize a local evaluation client.
*
* A local evaluation client can evaluate local flags or experiments for a user
* without requiring a remote call to the amplitude evaluation server. In order
* to best leverage local evaluation, all flags and experiments being evaluated
* server side should be configured as local.
*
* @param apiKey The environment API Key
* @param config See {@link ExperimentConfig} for config options
* @returns The local evaluation client.
*/
const initializeLocal = (
apiKey: string,
config?: LocalEvaluationConfig,
): LocalEvaluationClient => {
if (!localEvaluationInstances[apiKey]) {
localEvaluationInstances[apiKey] = new LocalEvaluationClient(
apiKey,
config,
new InMemoryFlagConfigCache(),
);
}
return instances[defaultInstance];
return localEvaluationInstances[apiKey];
};

/**
* Provides factory methods for storing singleton instances of
* {@link ExperimentClient}.
*
* Provides factory methods for storing singleton instances of {@link ExperimentClient}
* @category Core Usage
*/
export const Experiment = {
initialize,
initializeRemote,
initializeLocal,
};
19 changes: 13 additions & 6 deletions packages/node/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
/**
* This is the API Reference for the Experiment JS Server SDK.
* For more details on implementing this SDK, view the documentation
* [here](https://amplitude-lab.readme.io/docs/javascript-server-sdk).
* This is the API Reference for the Experiment Node.js Server SDK.
* For more details on implementing this SDK, view the [documentation]
* (https://docs.developers.amplitude.com/experiment/sdks/nodejs-sdk).
* @module experiment-node-server
*/

export { AmplitudeCookie } from './amplitude';
export { ExperimentClient } from './client';
export { ExperimentConfig, Defaults } from './config';
export { AmplitudeCookie } from './cookie';
export { ExperimentClient } from './remote/client';
export { ExperimentConfig, RemoteEvaluationDefaults } from './types/config';
export { Experiment } from './factory';
export { ExperimentUser } from './types/user';
export { Variant, Variants } from './types/variant';

export { LocalEvaluationClient } from './local/client';
export { LocalEvaluationConfig } from './types/config';
export { FlagConfigFetcher } from './local/fetcher';
export { FlagConfigPoller } from './local/poller';
export { InMemoryFlagConfigCache } from './local/cache';
export { FlagConfig, FlagConfigCache } from './types/flag';
32 changes: 32 additions & 0 deletions packages/node/src/local/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { FlagConfigCache, FlagConfig } from '../types/flag';

export class InMemoryFlagConfigCache implements FlagConfigCache {
private cache: Record<string, FlagConfig> = {};

public constructor(flagConfigs: Record<string, FlagConfig> = {}) {
this.cache = flagConfigs;
}
public async get(flagKey: string): Promise<FlagConfig> {
return this.cache[flagKey];
}
public async getAll(): Promise<Record<string, FlagConfig>> {
return { ...this.cache };
}
public async put(flagKey: string, flagConfig: FlagConfig): Promise<void> {
this.cache[flagKey] = flagConfig;
}
public async putAll(flagConfigs: Record<string, FlagConfig>): Promise<void> {
for (const key in flagConfigs) {
const flag = flagConfigs[key];
if (flag) {
this.cache[key] = flag;
}
}
}
public async delete(flagKey: string): Promise<void> {
delete this.cache[flagKey];
}
public async clear(): Promise<void> {
this.cache = {};
}
}
123 changes: 123 additions & 0 deletions packages/node/src/local/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import evaluation from '@amplitude/evaluation-js';

import { FetchHttpClient } from '../transport/http';
import {
LocalEvaluationConfig,
LocalEvaluationDefaults,
} from '../types/config';
import { FlagConfig, FlagConfigCache } from '../types/flag';
import { HttpClient } from '../types/transport';
import { ExperimentUser } from '../types/user';
import { Variants } from '../types/variant';
import { ConsoleLogger } from '../util/logger';
import { Logger } from '../util/logger';

import { InMemoryFlagConfigCache } from './cache';
import { FlagConfigFetcher } from './fetcher';
import { FlagConfigPoller } from './poller';

/**
* Experiment client for evaluating variants for a user locally.
* @category Core Usage
*/
export class LocalEvaluationClient {
private readonly logger: Logger;
private readonly config: LocalEvaluationConfig;
private readonly poller: FlagConfigPoller;

/**
* Directly access the client's flag config cache.
*
* Used for directly manipulating the flag configs used for evaluation.
*/
public readonly cache: FlagConfigCache;

constructor(
apiKey: string,
config: LocalEvaluationConfig,
flagConfigCache: FlagConfigCache = new InMemoryFlagConfigCache(
config?.bootstrap,
),
httpClient: HttpClient = new FetchHttpClient(config?.httpAgent),
) {
this.config = { ...LocalEvaluationDefaults, ...config };
const fetcher = new FlagConfigFetcher(
apiKey,
httpClient,
this.config.serverUrl,
this.config.debug,
);
this.cache = flagConfigCache;
this.logger = new ConsoleLogger(this.config.debug);
this.poller = new FlagConfigPoller(
fetcher,
this.cache,
this.config.flagConfigPollingIntervalMillis,
this.config.debug,
);
}

/**
* Locally evaluates flag variants for a user.
*
* This function will only evaluate flags for the keys specified in the
* {@link flagKeys} argument. If {@link flagKeys} is missing, all flags in the
* {@link FlagConfigCache} will be evaluated.
*
* @param user The user to evaluate
* @param flagKeys The flags to evaluate with the user. If empty, all flags
* from the flag cache are evaluated.
* @returns The evaluated variants
*/
public async evaluate(
user: ExperimentUser,
flagKeys?: string[],
): Promise<Variants> {
const flagConfigs = await this.getFlagConfigs(flagKeys);
this.logger.debug(
'[Experiment] evaluate - user:',
user,
'flagConfigs:',
flagConfigs,
);
const results: Variants = evaluation.evaluate(flagConfigs, user);
this.logger.debug('[Experiment] evaluate - result: ', results);
return results;
}

/**
* Fetch initial flag configurations and start polling for updates.
*
* You must call this function to begin polling for flag config updates.
* The promise returned by this function is resolved when the initial call
* to fetch the flag configuration completes.
*
* Calling this function while the poller is already running does nothing.
*/
public async start(): Promise<void> {
return await this.poller.start();
}

/**
* Stop polling for flag configurations.
*
* Calling this function while the poller is not running will do nothing.
*/
public stop(): void {
return this.poller.stop();
}

private async getFlagConfigs(flagKeys?: string[]): Promise<FlagConfig[]> {
if (!flagKeys) {
return Object.values(await this.cache.getAll());
}
const result: FlagConfig[] = [];
for (const key of flagKeys) {
const flagConfig = await this.cache.get(key);
if (flagConfig) {
result.push(flagConfig);
}
}
return result;
}
}
Loading

0 comments on commit 6be8686

Please sign in to comment.