Skip to content

Commit e369992

Browse files
feat: js local resolver
1 parent 20284b9 commit e369992

24 files changed

+3487
-4
lines changed

confidence-resolver/protos/confidence/flags/admin/v1/resolver.proto

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ message ResolverStateUriResponse {
5656
string signed_uri = 1;
5757
// At what time the state uri expires
5858
google.protobuf.Timestamp expire_time = 2;
59+
// The account the referenced state belongs to
60+
string account = 3;
5961
}
6062

6163
// Request to get the resolver state for the whole account
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
root = true
2+
3+
[*]
4+
end_of_line = lf
5+
insert_final_newline = true
6+
7+
[*.{js,json,yml}]
8+
charset = utf-8
9+
indent_style = space
10+
indent_size = 2
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/.yarn/** linguist-vendored
2+
/.yarn/releases/* binary
3+
/.yarn/plugins/**/* binary
4+
/.pnp.* binary linguist-generated

openfeature-provider/js/.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.yarn/*
2+
!.yarn/patches
3+
!.yarn/plugins
4+
!.yarn/releases
5+
!.yarn/sdks
6+
!.yarn/versions
7+
node_modules/
8+
src/proto/
9+
dist/
10+
.env.test
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
nodeLinker: node-modules

openfeature-provider/js/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# js
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"name": "@spotify-confidence/openfeature-server-provider-local",
3+
"version": "0.0.0",
4+
"private": true,
5+
"description": "Spotify Condfidence Open Feature provider",
6+
"type": "module",
7+
"files": [
8+
"dist"
9+
],
10+
"main": "./dist/index.node.js",
11+
"module": "./dist/index.browser.js",
12+
"types": "./dist/index.node.d.ts",
13+
"exports": {
14+
".": {
15+
"node": {
16+
"types": "./dist/index.node.d.ts",
17+
"default": "./dist/index.node.js"
18+
},
19+
"browser": {
20+
"types": "./dist/index.browser.d.ts",
21+
"default": "./dist/index.browser.js"
22+
},
23+
"default": {
24+
"types": "./dist/index.node.d.ts",
25+
"default": "./dist/index.node.js"
26+
}
27+
},
28+
"./package.json": "./package.json"
29+
},
30+
"scripts": {
31+
"build": "tsdown",
32+
"dev": "tsdown --watch",
33+
"test": "vitest",
34+
"proto:gen": "mkdir -p src/proto && rm -rf src/proto/* && protoc --plugin=node_modules/.bin/protoc-gen-ts_proto --ts_proto_opt useOptionals=messages --ts_proto_opt esModuleInterop=true --ts_proto_out src/proto -Iproto api.proto messages.proto"
35+
},
36+
"devDependencies": {
37+
"@openfeature/core": "^1.9.0",
38+
"@openfeature/server-sdk": "^1.19.0",
39+
"@types/node": "^24.0.1",
40+
"@vitest/coverage-v8": "^3.2.4",
41+
"dotenv": "^17.2.2",
42+
"rolldown": "1.0.0-beta.38",
43+
"ts-proto": "^2.7.3",
44+
"tsdown": "latest",
45+
"vitest": "^3.2.4"
46+
},
47+
"peerDependencies": {
48+
"@openfeature/server-sdk": "^1.19.0"
49+
},
50+
"packageManager": "[email protected]"
51+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
syntax = "proto3";
2+
3+
import "google/protobuf/struct.proto";
4+
import "google/protobuf/timestamp.proto";
5+
6+
message ResolveFlagsRequest {
7+
// If non-empty, the specific flags are resolved, otherwise all flags
8+
// available to the client will be resolved.
9+
repeated string flags = 1;
10+
11+
// An object that contains data used in the flag resolve. For example,
12+
// the targeting key e.g. the id of the randomization unit, other attributes
13+
// like country or version that are used for targeting.
14+
google.protobuf.Struct evaluation_context = 2;
15+
16+
// Credentials for the client. It is used to identify the client and find
17+
// the flags that are available to it.
18+
string client_secret = 3;
19+
20+
// Determines whether the flags should be applied directly as part of the
21+
// resolve, or delayed until `ApplyFlag` is called. A flag is typically
22+
// applied when it is used, if this occurs much later than the resolve, then
23+
// `apply` should likely be set to false.
24+
bool apply = 4;
25+
26+
// Information about the SDK used to initiate the request.
27+
// Sdk sdk = 5;
28+
}
29+
30+
message ResolveFlagsResponse {
31+
// The list of all flags that could be resolved. Note: if any flag was
32+
// archived it will not be included in this list.
33+
repeated ResolvedFlag resolved_flags = 1;
34+
35+
// An opaque token that is used when `apply` is set to false in `ResolveFlags`.
36+
// When `apply` is set to false, the token must be passed to `ApplyFlags`.
37+
bytes resolve_token = 2;
38+
39+
// Unique identifier for this particular resolve request.
40+
string resolve_id = 3;
41+
}
42+
43+
44+
message ResolvedFlag {
45+
// The id of the flag that as resolved.
46+
string flag = 1;
47+
48+
// The id of the resolved variant has the format `flags/abc/variants/xyz`.
49+
string variant = 2;
50+
51+
// The value corresponding to the variant. It will always be a json object,
52+
// for example `{ "color": "red", "size": 12 }`.
53+
google.protobuf.Struct value = 3;
54+
55+
// The schema of the value that was returned. For example:
56+
// ```
57+
// {
58+
// "schema": {
59+
// "color": { "stringSchema": {} },
60+
// "size": { "intSchema": {} }
61+
// }
62+
// }
63+
// ```
64+
// types.v1.FlagSchema.StructFlagSchema flag_schema = 4;
65+
66+
// The reason to why the flag could be resolved or not.
67+
ResolveReason reason = 5;
68+
}
69+
70+
71+
enum ResolveReason {
72+
// Unspecified enum.
73+
RESOLVE_REASON_UNSPECIFIED = 0;
74+
// The flag was successfully resolved because one rule matched.
75+
RESOLVE_REASON_MATCH = 1;
76+
// The flag could not be resolved because no rule matched.
77+
RESOLVE_REASON_NO_SEGMENT_MATCH = 2;
78+
// The flag could not be resolved because the matching rule had no variant
79+
// that could be assigned.
80+
RESOLVE_REASON_NO_TREATMENT_MATCH = 3 [deprecated = true];
81+
// The flag could not be resolved because it was archived.
82+
RESOLVE_REASON_FLAG_ARCHIVED = 4;
83+
// The flag could not be resolved because the targeting key field was invalid
84+
RESOLVE_REASON_TARGETING_KEY_ERROR = 5;
85+
// Unknown error occurred during the resolve
86+
RESOLVE_REASON_ERROR = 6;
87+
}
88+
89+
message SetResolverStateRequest {
90+
bytes state = 1;
91+
string account_id = 2;
92+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
syntax = "proto3";
2+
3+
message Void {}
4+
5+
message Request {
6+
bytes data = 1;
7+
}
8+
9+
message Response {
10+
oneof result {
11+
bytes data = 1;
12+
string error = 2;
13+
}
14+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
2+
import { OpenFeature } from '@openfeature/server-sdk';
3+
import { ConfidenceServerProviderLocal } from './ConfidenceServerProviderLocal';
4+
import { readFileSync } from 'node:fs';
5+
import { WasmResolver } from './WasmResolver';
6+
7+
const {
8+
CONFIDENCE_API_CLIENT_ID,
9+
CONFIDENCE_API_CLIENT_SECRET,
10+
} = requireEnv('CONFIDENCE_API_CLIENT_ID', 'CONFIDENCE_API_CLIENT_SECRET');
11+
12+
const moduleBytes = readFileSync(__dirname + '/../../../wasm/confidence_resolver.wasm');
13+
const module = new WebAssembly.Module(moduleBytes);
14+
const resolver = await WasmResolver.load(module);
15+
const confidenceProvider = new ConfidenceServerProviderLocal(resolver, {
16+
clientKey: 'RxDVTrXvc6op1XxiQ4OaR31dKbJ39aYV',
17+
clientId: CONFIDENCE_API_CLIENT_ID,
18+
clientSecret: CONFIDENCE_API_CLIENT_SECRET
19+
});
20+
21+
describe('ConfidenceServerProvider E2E tests', () => {
22+
beforeAll( async () => {
23+
24+
await OpenFeature.setProviderAndWait(confidenceProvider);
25+
OpenFeature.setContext({
26+
targetingKey: 'test-a', // control
27+
});
28+
});
29+
30+
afterAll(() => OpenFeature.close())
31+
32+
it('should resolve a boolean e2e', async () => {
33+
const client = OpenFeature.getClient();
34+
35+
expect(await client.getBooleanValue('web-sdk-e2e-flag.bool', true)).toBeFalsy();
36+
});
37+
38+
it('should resolve an int', async () => {
39+
const client = OpenFeature.getClient();
40+
41+
expect(await client.getNumberValue('web-sdk-e2e-flag.int', 10)).toEqual(3);
42+
});
43+
44+
it('should resolve a double', async () => {
45+
const client = OpenFeature.getClient();
46+
47+
expect(await client.getNumberValue('web-sdk-e2e-flag.double', 10)).toEqual(3.5);
48+
});
49+
50+
it('should resolve a string', async () => {
51+
const client = OpenFeature.getClient();
52+
53+
expect(await client.getStringValue('web-sdk-e2e-flag.str', 'default')).toEqual('control');
54+
});
55+
56+
it('should resolve a struct', async () => {
57+
const client = OpenFeature.getClient();
58+
const expectedObject = {
59+
int: 4,
60+
str: 'obj control',
61+
bool: false,
62+
double: 3.6,
63+
['obj-obj']: {},
64+
};
65+
66+
expect(await client.getObjectValue('web-sdk-e2e-flag.obj', {})).toEqual(expectedObject);
67+
});
68+
69+
it('should resolve a sub value from a struct', async () => {
70+
const client = OpenFeature.getClient();
71+
72+
expect(await client.getBooleanValue('web-sdk-e2e-flag.obj.bool', true)).toBeFalsy();
73+
});
74+
75+
it('should resolve a sub value from a struct with details with resolve token for client side apply call', async () => {
76+
const client = OpenFeature.getClient();
77+
const expectedObject = {
78+
flagKey: 'web-sdk-e2e-flag.obj.double',
79+
reason: 'MATCH',
80+
variant: 'flags/web-sdk-e2e-flag/variants/control',
81+
flagMetadata: {},
82+
value: 3.6,
83+
};
84+
85+
expect(await client.getNumberDetails('web-sdk-e2e-flag.obj.double', 1)).toEqual(expectedObject);
86+
});
87+
});
88+
89+
function requireEnv<const N extends string[]>(...names:N): Record<N[number],string> {
90+
return names.reduce((acc, name) => {
91+
const value = process.env[name];
92+
if(!value) throw new Error(`Missing environment variable ${name}`)
93+
return {
94+
...acc,
95+
[name]: value
96+
};
97+
}, {}) as Record<N[number],string>;
98+
}

0 commit comments

Comments
 (0)