Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Client] adding nodejs cac and experimentation client wrapper #170

Open
wants to merge 1 commit into
base: saas
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
170 changes: 170 additions & 0 deletions clients/nodejs/cac-client/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
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) => {
try {
message = JSON.parse(message);
if (message.event === "startPollingUpdate") {
let {
tenant,
pollingFrequency,
cacHostName,
} = message;
let tenantClient = cac_client.get(tenant);
if (tenantClient) {
tenantClient.startPollingUpdate();
} else {
tenantClient = new CacClient(tenant, pollingFrequency, cacHostName);
cac_client.set(tenant, tenantClient);
tenantClient.startPollingUpdate();
}
}
} catch (error) {
console.log("Error While starting polling Update for cac client ", error);
}
})
}
// -----------------------------------------

export enum MergeStrategy {
MERGE = "MERGE",
REPLACE = "REPLACE"
}

export class CacClient {
tenant: string | null = null;
cacHostName: string | null = null;
pollingFrequency: number = 10;
delimeter: string = ",";

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

});

constructor(tenantName: string, pollingFrequency: number, cacHostName: string) {
if (!tenantName || tenantName == "") {
throw Error("tenantName cannot be null or empty")
}
if (!cacHostName || cacHostName == "") {
throw Error("cacHostName cannot be null or empty")
}
this.tenant = tenantName;
this.pollingFrequency = pollingFrequency;
this.cacHostName = cacHostName;
let resp = this.rustLib.cac_new_client(this.tenant, this.pollingFrequency, this.cacHostName);
if (resp == 1) {
let errorMessage = this.getLastErrorMessage();
throw Error("Some Error Occur while creating new client " + errorMessage);
}
}

public getLastErrorMessage(): string {
return this.rustLib.cac_last_error_message() || "";
}

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

public getClient(): ref.Pointer<unknown> {
return this.rustLib.cac_get_client(this.tenant);
}

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

public getConfig(filterQuery: Object | undefined, filterPrefix: string[] | undefined): string {
let strFilterQuery = filterQuery ? ref.allocCString(JSON.stringify(filterQuery)) : ref.NULL;
let strFilterPrefix = filterPrefix ? ref.allocCString(filterPrefix.join(this.delimeter)) : ref.NULL;
let clientPtr = this.getClient();
let resp = this.rustLib.cac_get_config(clientPtr, strFilterQuery, strFilterPrefix) || this.getLastErrorMessage();
return resp;
}

freeClient(clientPtr: ref.Pointer<unknown>) {
this.rustLib.cac_free_client(clientPtr);
}

freeString(str: ref.Pointer<unknown>) {
this.rustLib.cac_free_string(str);
}

public getLastModified(): string {
return this.rustLib.cac_get_last_modified(this.getClient()) || this.getLastErrorMessage();
}

public getResolvedConfig(query: Object, filterKeys: string[] | undefined, mergeStrategy: MergeStrategy): string {
let strQuery = JSON.stringify(query);
let strFilterKeys = filterKeys ? ref.allocCString (filterKeys.join("|")) : ref.NULL ;
let resp = this.rustLib.cac_get_resolved_config(
this.getClient(), strQuery, strFilterKeys, mergeStrategy
) || this.getLastErrorMessage();
return resp;
}
public getDefaultConfig(filterKeys: string[] | undefined): string {
let strFilterKeys = filterKeys ? ref.allocCString(filterKeys.join("|")) : ref.NULL;
let resp = this.rustLib.cac_get_default_config(
this.getClient(), strFilterKeys
) || this.getLastErrorMessage();
return resp;
}
}
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"]
}
Loading
Loading