Skip to content

Commit

Permalink
feat: adding nodejs cac and experimentation client wrapper
Browse files Browse the repository at this point in the history
  • Loading branch information
namitgoel committed Aug 4, 2024
1 parent 710c69d commit 5364c56
Show file tree
Hide file tree
Showing 10 changed files with 578 additions and 0 deletions.
33 changes: 33 additions & 0 deletions clients/nodejs/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
Set directory path that contains superposition object files in <span style="color: red" > SUPERPOSITION_LIB_PATH </span> env variable;

## [<u> CAC Client </u>](./cac-client)

1. This exports a class that exposes functions that internally call rust functions.
2. For Different platform it read different superposition object files.
* <span style="color: #808080" >For Mac </span> -> libcac_client.dylib
* <span style="color: #357EC7" >For Windows </span> -> libcac_client.so
* <span style="color: orange" >For Linux </span> -> libcac_client.dll
3. This run CAC CLient in two thread one is main thread another is worker thread.
4. Worker thread is used to do polling updates ([ref](./cac-client/client.ts#L31)).


## [<u> Experimentation Client </u>](./exp-client)

1. This exports a class that exposes functions that internally call rust functions.
2. For Different platform it read different superposition object files.
* <span style="color: #808080" >For Mac </span> -> libexperimentation_client.dylib
* <span style="color: #357EC7" >For Windows </span> -> libexperimentation_client.so
* <span style="color: orange" >For Linux </span> -> libexperimentation_client.dll
3. This run Experimentation CLient in two thread one is main thread another is worker thread.
4. Worker thread is used to do polling updates ([ref](./exp-client/client.ts#L31)).

## [<u> Test </u>](./index.ts)

1. To test this sample project follow below steps.
* Run superposition client.
* Run <u> **npm install** </u> (make sure your node version is <span style="color: red"> >18 </span>).
* Run <u> **npm run test** </u> this will start a server that runs on port 7000.
2. By Default this sample code uses [dev](./index.ts#L11) tenant.
3. By Default this sample code assumes superposition is running on [8080](./index.ts#L12) port.
3. By Default this sample code polls superposition every [1 second](./index.ts#L13) port.
4. This sample code creates both [CAC CLient](./index.ts#L15) and [Experimentation Client](./index.ts#L16) with above default values.
156 changes: 156 additions & 0 deletions clients/nodejs/cac-client/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import * as ffi from 'ffi-napi';
import * as ref from 'ref-napi';
import * as path from 'path';
import os from 'os';
import { Worker, isMainThread} from "node:worker_threads";
import { parentPort } from 'worker_threads';

let libPathEnc: string | undefined = process.env.SUPERPOSITION_LIB_PATH;

if (libPathEnc == "" || libPathEnc == undefined) {
throw new Error("SUPERPOSITION_LIB_PATH not found in env");
}

let platform = os.platform();
let fileName =
platform == "darwin" ?
'libcac_client.dylib' :
platform == "linux" ?
'libcac_client.dll' :
'libcac_client.so'

const libPath = path.join(libPathEnc, fileName);

const refType = ref.types;
const int = refType.int;
const string = refType.CString;
const voidType = refType.void;

// -------------------------------------
// this code is running on Main Thread
const worker = new Worker(__filename);
// -------------------------------------


// ----------------------------------------
// this code runs on worker thread
if (!isMainThread && parentPort) {
let cac_client: Map < String, CacClient > = new Map();
parentPort.on("message", (message) => {
if (message) {
if (message.event === "startCACPollingUpdate") {
let {
tenant,
pollingFrequency,
cacHostName,
} = message;
let tenantClient = cac_client.get(tenant);
if (tenantClient) {
tenantClient.startCACPollingUpdate();
} else {
tenantClient = new CacClient(tenant, pollingFrequency, cacHostName);
cac_client.set(tenant, tenantClient);
tenantClient.startCACPollingUpdate();
}
}
}
})
}
// -----------------------------------------

class CacClient {
tenant: string | null = null;
cacHostName: string | null = null;
pollingFrequency: number = 10;

rustLib = ffi.Library(libPath, {
'cac_new_client': [int, [string, int, string]],
'cac_get_client': [string, [string]],
'cac_start_polling_update': [voidType, [string]],
'cac_free_client': [voidType, [string]],
'cac_last_error_message': [string, []],
'cac_get_config': [string, [string, string, string]],
'cac_last_error_length': [int, [voidType]],
'cac_free_string': [voidType, [string]],
'cac_get_last_modified': [string, [string]],
'cac_get_resolved_config': [string, [string, string, string, string]],
'cac_get_default_config': [string, [string, string]],

});

constructor(tenantName: string, pollingFrequency: number, cacHostName: string) {
if (tenantName == "" || cacHostName == "") {
throw Error("tenantName cannot be null or empty")
}
this.tenant = tenantName;
this.pollingFrequency = pollingFrequency;
this.cacHostName = cacHostName;
}

public getCACLastErrorMessage(): string | null {
return this.rustLib.cac_last_error_message();
}

public getCACLastErrorLength(): number {
return this.rustLib.cac_last_error_length()
}

public getCACClient(): string | null {
return this.rustLib.cac_get_client(this.tenant);
}

public createNewCACClient(): number {
let resp = this.rustLib.cac_new_client(this.tenant, this.pollingFrequency, this.cacHostName);
if (resp == 1) {
let errorMessage = this.getCACLastErrorMessage();
console.error("Some Error Occur while creating new client ", errorMessage);
}
return resp;
}

public async startCACPollingUpdate() {
if (isMainThread && worker) {
worker.postMessage(
{ tenant: this.tenant
, event : "startCACPollingUpdate"
, pollingFrequency: this.pollingFrequency
, cacHostName: this.cacHostName
}
);
return;
}
if (!isMainThread) {
this.rustLib.cac_start_polling_update(this.tenant)
}
}

public getCACConfig(filterQuery: string, filterPrefix: string): string | null {
let clientPtr = this.getCACClient();
return this.rustLib.cac_get_config(clientPtr, filterQuery, filterPrefix);
}

public freeCACClient(clientPtr: string) {
this.rustLib.cac_free_client(clientPtr)
}

public freeCACString(str: string) {
this.rustLib.cac_free_string(str)
}

public getLastModified(): string | null {
return this.rustLib.cac_get_last_modified(this.getCACClient());
}

public getResolvedConfig(query: string, filterKeys: string, mergeStrategy: string): string | null {
return this.rustLib.cac_get_resolved_config(
this.getCACClient(), query, filterKeys, mergeStrategy
)
}
public getDefaultConfig(filterKeys: string): string | null {
return this.rustLib.cac_get_default_config(
this.getCACClient(), filterKeys
)
}
}

export default CacClient;
24 changes: 24 additions & 0 deletions clients/nodejs/cac-client/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "nodejs_cac_client",
"version": "1.0.0",
"main": "dist/client.js",
"types": "dist/client.d.ts",
"files": [
"/dist"
],
"devDependencies": {
"@types/express": "^4.17.21",
"@types/ffi-napi": "^4.0.10",
"@types/node": "^20.14.10",
"@types/ref-napi": "^3.0.12",
"@types/typescript": "^2.0.0",
"express": "^4.19.2",
"ffi-napi": "^4.0.3",
"ref-napi": "^3.0.3",
"ts-node": "^10.9.2",
"typescript": "^5.5.3"
},
"scripts": {
"postinstall" : "npx tsc"
}
}
11 changes: 11 additions & 0 deletions clients/nodejs/cac-client/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "CommonJS",
"target": "ES2015",
"outDir": "./dist",
"declaration": true,
"esModuleInterop": true
},
"exclude": ["node_modules"],
"include": ["./client.ts"]
}
159 changes: 159 additions & 0 deletions clients/nodejs/exp-client/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import * as ffi from 'ffi-napi';
import * as ref from 'ref-napi';
import * as path from 'path';
import os from 'os';
import { isMainThread, parentPort, Worker } from 'worker_threads';

let platform = os.platform();


let libPathEnc: string | undefined = process.env.SUPERPOSITION_LIB_PATH;

if (libPathEnc == "" || libPathEnc == undefined) {
throw new Error("SUPERPOSITION_LIB_PATH not found in env");
}

let fileName =
platform == "darwin" ?
'libexperimentation_client.dylib' :
platform == "linux" ?
'libexperimentation_client.dll' :
'libexperimentation_client.so'

const libPath = path.join(libPathEnc, fileName);

const refType = ref.types;
const int = refType.int;
const string = refType.CString;
const voidType = refType.void;

// -------------------------------------
// this code is running on Main Thread
const worker = new Worker(__filename);
// -------------------------------------


// ----------------------------------------
// this code runs on worker thread
if (!isMainThread && parentPort) {
let cac_client: Map < String, ExperimentationClient > = new Map();
parentPort.on("message", (message) => {
if (message) {
if (message.event === "startExperimantionPollingUpdate") {
let {
tenant,
pollingFrequency,
cacHostName,
} = message;
let tenantClient = cac_client.get(tenant);
if (tenantClient) {
tenantClient.startExperimantionPollingUpdate();
} else {
tenantClient = new ExperimentationClient(tenant, pollingFrequency, cacHostName);
cac_client.set(tenant, tenantClient);
tenantClient.startExperimantionPollingUpdate();
}
}
}
})
}
// -----------------------------------------

class ExperimentationClient {
tenant: string | null = null;
pollingFrequency: number = 10;
cacHostName: string | null = null;

rustLib = ffi.Library(libPath, {
'expt_new_client': [int, [string, int, string]],
'expt_start_polling_update': [voidType, [string]],
'expt_get_client': [string, [string]],
'expt_get_applicable_variant': [string, [string, string, int]],
'expt_get_satisfied_experiments': [string, [string, string, string]],
'expt_get_filtered_satisfied_experiments': [string, [string, string, string]],
'expt_get_running_experiments': [string, [string]],
'expt_free_string': [voidType, [string]],
'expt_last_error_message': [string, []],
'expt_last_error_length': [int, []],
'expt_free_client': [voidType, [string]]
});

constructor(tenantName: string, pollingFrequency: number, hostName: string) {
this.tenant = tenantName;
this.cacHostName = hostName;
this.pollingFrequency = pollingFrequency;
}

public getExperimentationLastErrorMessage(): string | null {
return this.rustLib.expt_last_error_message();
}

public createNewExperimentaionClient(): number {
let respCode = this.rustLib.expt_new_client(
this.tenant, this.pollingFrequency, this.cacHostName
);
if (respCode == 1) {
let errorMessage = this.getExperimentationLastErrorMessage();
console.log("Some Error Occured while creating new experimentation client ", errorMessage);
throw Error("Client Creation Error");
}
return respCode;
}

public getExperimentationClient(): string | null {
return this.rustLib.expt_get_client(this.tenant);
}

public getRunningExpriments(): string | null {
let clientPtr = this.getExperimentationClient();
return this.rustLib.expt_get_running_experiments(clientPtr);
}

public freeString(str: string) {
this.rustLib.expt_free_string(str);
}

public async startExperimantionPollingUpdate() {
if (isMainThread && worker) {
worker.postMessage(
{ tenant: this.tenant
, event : "startExperimantionPollingUpdate"
, pollingFrequency: this.pollingFrequency
, cacHostName: this.cacHostName
}
);
return;
}
if (!isMainThread) {
this.rustLib.expt_start_polling_update(this.tenant)
}
}

public getExperimentationLastErrorLength(): number {
return this.rustLib.expt_last_error_length()
}

public freeExprementationClient() {
this.rustLib.expt_free_client(this.getExperimentationClient());
}

public getFilteredSatisfiedExperiments(context: string, filterPrefix: string): string | null {
return this.rustLib.expt_get_filtered_satisfied_experiments(
this.getExperimentationClient(), context, filterPrefix
);
}

public getApplicableVariant(context: string, toss: number): string | null {
return this.rustLib.expt_get_applicable_variant(
this.getExperimentationClient(), context, toss
);
}

public getSatisfiedExperiments(context: string, filterPrefix: string): string | null {
return this.rustLib.expt_get_satisfied_experiments(
this.getExperimentationClient(), context, filterPrefix
);
}
}

export default ExperimentationClient;
Loading

0 comments on commit 5364c56

Please sign in to comment.