-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: adding nodejs cac and experimentation client wrapper
- Loading branch information
Showing
10 changed files
with
578 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.