@omniretail/omniflags-core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -0
- package/dist/bucket.d.ts +23 -0
- package/dist/bucket.js +50 -0
- package/dist/bucket.js.map +1 -0
- package/dist/cache.d.ts +12 -0
- package/dist/cache.js +30 -0
- package/dist/cache.js.map +1 -0
- package/dist/client.d.ts +46 -0
- package/dist/client.js +265 -0
- package/dist/client.js.map +1 -0
- package/dist/context.d.ts +11 -0
- package/dist/context.js +31 -0
- package/dist/context.js.map +1 -0
- package/dist/evaluator.d.ts +6 -0
- package/dist/evaluator.js +252 -0
- package/dist/evaluator.js.map +1 -0
- package/dist/http-provider.d.ts +15 -0
- package/dist/http-provider.js +110 -0
- package/dist/http-provider.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics.d.ts +20 -0
- package/dist/metrics.js +89 -0
- package/dist/metrics.js.map +1 -0
- package/dist/types.d.ts +135 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# @omniretail/omniflags-core
|
|
2
|
+
|
|
3
|
+
JavaScript/TypeScript SDK for OmniFlags.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @omniretail/omniflags-core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { OmniFlagsClient } from "@omniretail/omniflags-core";
|
|
15
|
+
|
|
16
|
+
const client = new OmniFlagsClient({ sdkKey: "your-sdk-key" });
|
|
17
|
+
|
|
18
|
+
await client.waitForReady();
|
|
19
|
+
|
|
20
|
+
const enabled = client.isEnabled("store.dark-mode");
|
|
21
|
+
const theme = client.getString("store.theme", "light");
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Configuration
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
const client = new OmniFlagsClient({
|
|
28
|
+
sdkKey: "your-sdk-key", // required
|
|
29
|
+
hooks: [loggingHook], // optional evaluation hooks
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
| Option | Type | Description |
|
|
34
|
+
|--------|------|-------------|
|
|
35
|
+
| `sdkKey` | `string` | **Required.** SDK key from the OmniFlags dashboard. |
|
|
36
|
+
| `hooks` | `Hook[]` | Lifecycle hooks called before/after each evaluation. |
|
|
37
|
+
|
|
38
|
+
## Evaluation
|
|
39
|
+
|
|
40
|
+
Flag keys are namespaced by project: `{projectKey}.{flagKey}`.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// Boolean
|
|
44
|
+
client.isEnabled("store.show-banner");
|
|
45
|
+
client.isEnabled("store.show-banner", { customerId: "cust-123" }); // with context
|
|
46
|
+
|
|
47
|
+
// Typed value methods
|
|
48
|
+
client.getBoolean("store.dark-mode", false);
|
|
49
|
+
client.getString("store.theme", "light");
|
|
50
|
+
client.getNumber("store.max-items", 10);
|
|
51
|
+
|
|
52
|
+
// Generic — useful when the type is only known at runtime
|
|
53
|
+
client.getValue<{ color: string }>("store.config", defaultConfig);
|
|
54
|
+
|
|
55
|
+
// Full evaluation result (variant, reason, rule matched)
|
|
56
|
+
const result = client.getVariant("store.show-banner");
|
|
57
|
+
// result.value, result.variant, result.reason, result.ruleId, result.errorCode
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Context
|
|
61
|
+
|
|
62
|
+
Pass context per evaluation call — customer, business, branch, country, etc. There is no persistent context state on the client.
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
const ctx = {
|
|
66
|
+
customerId: request.customerId,
|
|
67
|
+
agentId: request.agentId,
|
|
68
|
+
businessId: request.businessId,
|
|
69
|
+
businessBranchId: request.businessBranchId,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
client.isEnabled("store.show-banner", ctx);
|
|
73
|
+
client.getString("store.theme", "light", ctx);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Loading state
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
const state = client.loadingState;
|
|
80
|
+
// state.isFetching — a network request is in flight
|
|
81
|
+
// state.isLoading — no flags have been loaded yet (first fetch pending)
|
|
82
|
+
// state.origin — "NONE" | "CACHE" | "SERVER"
|
|
83
|
+
// state.error — last fetch error, or null
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Subscribe to changes:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
const unsub = client.on("PROVIDER_LOADING_STATE_CHANGED", () => {
|
|
90
|
+
console.log(client.loadingState);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
unsub(); // remove the listener
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Status & lifecycle
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
client.status; // "not_ready" | "ready" | "stale" | "error"
|
|
100
|
+
await client.waitForReady(); // resolves after the first fetch or cache hit
|
|
101
|
+
await client.shutdown(); // stop polling and flush metrics
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The SDK polls for flag updates automatically on a server-driven interval (default: 5 minutes).
|
|
105
|
+
|
|
106
|
+
## Events
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
const unsub = client.on("PROVIDER_CONFIGURATION_CHANGED", () => {
|
|
110
|
+
// flags updated
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
unsub(); // remove the listener
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
| Event | When |
|
|
117
|
+
|-------|------|
|
|
118
|
+
| `PROVIDER_READY` | First snapshot loaded |
|
|
119
|
+
| `PROVIDER_STALE` | Fetch failed but cached flags are available |
|
|
120
|
+
| `PROVIDER_ERROR` | Fetch failed and no cache |
|
|
121
|
+
| `PROVIDER_CONFIGURATION_CHANGED` | Flags updated by a background poll |
|
|
122
|
+
| `PROVIDER_LOADING_STATE_CHANGED` | Loading state changed |
|
|
123
|
+
|
|
124
|
+
## Hooks
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import type { Hook } from "@omniretail/omniflags-core";
|
|
128
|
+
|
|
129
|
+
const loggingHook: Hook = {
|
|
130
|
+
after(flagKey, result) {
|
|
131
|
+
console.log(`[flag] ${flagKey} → ${result.variant} (${result.reason})`);
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const client = new OmniFlagsClient({ sdkKey: "your-sdk-key", hooks: [loggingHook] });
|
|
136
|
+
|
|
137
|
+
// Or add after construction
|
|
138
|
+
client.addHook(loggingHook);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Metrics
|
|
142
|
+
|
|
143
|
+
Flag evaluations are tracked and flushed to OmniFlags automatically. The flush interval and batch size are driven by the server — no configuration needed.
|
package/dist/bucket.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FNV-1a (32-bit) deterministic bucketing.
|
|
3
|
+
*
|
|
4
|
+
* Canonical input: `{flagKey}|{environmentKey}|{targetingKey}|{salt}`
|
|
5
|
+
* Result range: [1, 100_000]
|
|
6
|
+
*/
|
|
7
|
+
/** Compute FNV-1a 32-bit hash of a UTF-8 string */
|
|
8
|
+
export declare function fnv1a32(input: string): number;
|
|
9
|
+
/**
|
|
10
|
+
* Compute the deterministic bucket for a flag + targeting key combination.
|
|
11
|
+
* @returns bucket in range [1, 100_000]
|
|
12
|
+
* @throws BucketError with code MISSING_TARGETING_KEY if targetingKey is empty
|
|
13
|
+
*/
|
|
14
|
+
export declare function computeBucket(flagKey: string, environmentKey: string, targetingKey: string, salt?: string): number;
|
|
15
|
+
/** Resolve rollout variant from bucket value and weighted partitions */
|
|
16
|
+
export declare function resolveRolloutVariant(bucket: number, rollout: {
|
|
17
|
+
variant: string;
|
|
18
|
+
weight: number;
|
|
19
|
+
}[]): string;
|
|
20
|
+
export declare class BucketError extends Error {
|
|
21
|
+
readonly code: string;
|
|
22
|
+
constructor(code: string);
|
|
23
|
+
}
|
package/dist/bucket.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FNV-1a (32-bit) deterministic bucketing.
|
|
3
|
+
*
|
|
4
|
+
* Canonical input: `{flagKey}|{environmentKey}|{targetingKey}|{salt}`
|
|
5
|
+
* Result range: [1, 100_000]
|
|
6
|
+
*/
|
|
7
|
+
const FNV_OFFSET_BASIS = 0x811c9dc5;
|
|
8
|
+
const FNV_PRIME = 0x01000193;
|
|
9
|
+
/** Compute FNV-1a 32-bit hash of a UTF-8 string */
|
|
10
|
+
export function fnv1a32(input) {
|
|
11
|
+
let hash = FNV_OFFSET_BASIS;
|
|
12
|
+
const bytes = new TextEncoder().encode(input);
|
|
13
|
+
for (const byte of bytes) {
|
|
14
|
+
hash ^= byte;
|
|
15
|
+
hash = Math.imul(hash, FNV_PRIME) >>> 0;
|
|
16
|
+
}
|
|
17
|
+
return hash >>> 0;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Compute the deterministic bucket for a flag + targeting key combination.
|
|
21
|
+
* @returns bucket in range [1, 100_000]
|
|
22
|
+
* @throws BucketError with code MISSING_TARGETING_KEY if targetingKey is empty
|
|
23
|
+
*/
|
|
24
|
+
export function computeBucket(flagKey, environmentKey, targetingKey, salt = "v1") {
|
|
25
|
+
if (!targetingKey) {
|
|
26
|
+
throw new BucketError("MISSING_TARGETING_KEY");
|
|
27
|
+
}
|
|
28
|
+
const input = `${flagKey}|${environmentKey}|${targetingKey}|${salt}`;
|
|
29
|
+
return (fnv1a32(input) % 100_000) + 1;
|
|
30
|
+
}
|
|
31
|
+
/** Resolve rollout variant from bucket value and weighted partitions */
|
|
32
|
+
export function resolveRolloutVariant(bucket, rollout) {
|
|
33
|
+
let cumulative = 0;
|
|
34
|
+
for (const split of rollout) {
|
|
35
|
+
cumulative += split.weight;
|
|
36
|
+
if (bucket <= cumulative) {
|
|
37
|
+
return split.variant;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return rollout[rollout.length - 1].variant;
|
|
41
|
+
}
|
|
42
|
+
export class BucketError extends Error {
|
|
43
|
+
code;
|
|
44
|
+
constructor(code) {
|
|
45
|
+
super(code);
|
|
46
|
+
this.code = code;
|
|
47
|
+
this.name = "BucketError";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=bucket.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bucket.js","sourceRoot":"","sources":["../src/bucket.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,gBAAgB,GAAG,UAAU,CAAC;AACpC,MAAM,SAAS,GAAG,UAAU,CAAC;AAE7B,mDAAmD;AACnD,MAAM,UAAU,OAAO,CAAC,KAAa;IACnC,IAAI,IAAI,GAAG,gBAAgB,CAAC;IAC5B,MAAM,KAAK,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC9C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,IAAI,CAAC;QACb,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,IAAI,KAAK,CAAC,CAAC;AACpB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAC3B,OAAe,EACf,cAAsB,EACtB,YAAoB,EACpB,IAAI,GAAG,IAAI;IAEX,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,MAAM,IAAI,WAAW,CAAC,uBAAuB,CAAC,CAAC;IACjD,CAAC;IACD,MAAM,KAAK,GAAG,GAAG,OAAO,IAAI,cAAc,IAAI,YAAY,IAAI,IAAI,EAAE,CAAC;IACrE,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;AACxC,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,qBAAqB,CACnC,MAAc,EACd,OAA8C;IAE9C,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,UAAU,IAAI,KAAK,CAAC,MAAM,CAAC;QAC3B,IAAI,MAAM,IAAI,UAAU,EAAE,CAAC;YACzB,OAAO,KAAK,CAAC,OAAO,CAAC;QACvB,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC;AAC7C,CAAC;AAED,MAAM,OAAO,WAAY,SAAQ,KAAK;IACR;IAA5B,YAA4B,IAAY;QACtC,KAAK,CAAC,IAAI,CAAC,CAAC;QADc,SAAI,GAAJ,IAAI,CAAQ;QAEtC,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;IAC5B,CAAC;CACF"}
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CacheAdapter } from "./types.js";
|
|
2
|
+
export declare class LocalStorageCache implements CacheAdapter {
|
|
3
|
+
get(key: string): Promise<string | null>;
|
|
4
|
+
set(key: string, value: string): Promise<void>;
|
|
5
|
+
remove(key: string): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export declare class MemoryCache implements CacheAdapter {
|
|
8
|
+
private store;
|
|
9
|
+
get(key: string): Promise<string | null>;
|
|
10
|
+
set(key: string, value: string): Promise<void>;
|
|
11
|
+
remove(key: string): Promise<void>;
|
|
12
|
+
}
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const PREFIX = "omniflags:snapshot:";
|
|
2
|
+
export class LocalStorageCache {
|
|
3
|
+
async get(key) {
|
|
4
|
+
try {
|
|
5
|
+
return globalThis.localStorage.getItem(PREFIX + key);
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
async set(key, value) {
|
|
12
|
+
try {
|
|
13
|
+
globalThis.localStorage.setItem(PREFIX + key, value);
|
|
14
|
+
}
|
|
15
|
+
catch { /* full or unavailable */ }
|
|
16
|
+
}
|
|
17
|
+
async remove(key) {
|
|
18
|
+
try {
|
|
19
|
+
globalThis.localStorage.removeItem(PREFIX + key);
|
|
20
|
+
}
|
|
21
|
+
catch { /* ignore */ }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export class MemoryCache {
|
|
25
|
+
store = new Map();
|
|
26
|
+
async get(key) { return this.store.get(key) ?? null; }
|
|
27
|
+
async set(key, value) { this.store.set(key, value); }
|
|
28
|
+
async remove(key) { this.store.delete(key); }
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,GAAG,qBAAqB,CAAC;AAErC,MAAM,OAAO,iBAAiB;IAC5B,KAAK,CAAC,GAAG,CAAC,GAAW;QACnB,IAAI,CAAC;YAAC,OAAO,UAAU,CAAC,YAAY,CAAC,OAAO,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC;YAAC,OAAO,IAAI,CAAC;QAAC,CAAC;IACtF,CAAC;IACD,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,KAAa;QAClC,IAAI,CAAC;YAAC,UAAU,CAAC,YAAY,CAAC,OAAO,CAAC,MAAM,GAAG,GAAG,EAAE,KAAK,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,yBAAyB,CAAC,CAAC;IACnG,CAAC;IACD,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,IAAI,CAAC;YAAC,UAAU,CAAC,YAAY,CAAC,UAAU,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;IAClF,CAAC;CACF;AAED,MAAM,OAAO,WAAW;IACd,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC1C,KAAK,CAAC,GAAG,CAAC,GAAW,IAA4B,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC;IACtF,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,KAAa,IAAmB,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IACpF,KAAK,CAAC,MAAM,CAAC,GAAW,IAAmB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CACrE"}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { CacheAdapter, EvaluationContext, EvaluationResult, Hook, LoadingState, OmniFlagsOptions, ProviderEvent, ProviderStatus } from "./types.js";
|
|
2
|
+
export declare class OmniFlagsClient {
|
|
3
|
+
private flags;
|
|
4
|
+
private hooks;
|
|
5
|
+
private provider;
|
|
6
|
+
private cache?;
|
|
7
|
+
private readonly sdkKey;
|
|
8
|
+
private pollingTimer;
|
|
9
|
+
private _status;
|
|
10
|
+
private _readyPromise;
|
|
11
|
+
private _resolveReady;
|
|
12
|
+
private metrics?;
|
|
13
|
+
private eventHandlers;
|
|
14
|
+
private _isFetching;
|
|
15
|
+
private _origin;
|
|
16
|
+
private _fetchError;
|
|
17
|
+
private envKey;
|
|
18
|
+
private pollingIntervalMs;
|
|
19
|
+
constructor(opts: OmniFlagsOptions, cache?: CacheAdapter);
|
|
20
|
+
get status(): ProviderStatus;
|
|
21
|
+
get loadingState(): LoadingState;
|
|
22
|
+
waitForReady(): Promise<void>;
|
|
23
|
+
/** Trigger an immediate poll without waiting for the next scheduled interval. */
|
|
24
|
+
refresh(): void;
|
|
25
|
+
shutdown(): Promise<void>;
|
|
26
|
+
isEnabled(flagKey: string, context?: EvaluationContext, defaultValue?: boolean): boolean;
|
|
27
|
+
getValue<T = unknown>(flagKey: string, context?: EvaluationContext, defaultValue?: T): T;
|
|
28
|
+
getVariant(flagKey: string, context?: EvaluationContext): EvaluationResult;
|
|
29
|
+
getBoolean(flagKey: string, defaultValue: boolean, context?: EvaluationContext): boolean;
|
|
30
|
+
getString(flagKey: string, defaultValue: string, context?: EvaluationContext): string;
|
|
31
|
+
getNumber(flagKey: string, defaultValue: number, context?: EvaluationContext): number;
|
|
32
|
+
getObject<T = Record<string, unknown>>(flagKey: string, defaultValue: T, context?: EvaluationContext): T;
|
|
33
|
+
getBooleanDetail(flagKey: string, defaultValue: boolean, context?: EvaluationContext): EvaluationResult<boolean>;
|
|
34
|
+
getStringDetail(flagKey: string, defaultValue: string, context?: EvaluationContext): EvaluationResult<string>;
|
|
35
|
+
addHook(hook: Hook): void;
|
|
36
|
+
on(event: ProviderEvent, handler: () => void): () => void;
|
|
37
|
+
private evaluateSync;
|
|
38
|
+
private applySnapshot;
|
|
39
|
+
private init;
|
|
40
|
+
private startPolling;
|
|
41
|
+
private stopPolling;
|
|
42
|
+
private poll;
|
|
43
|
+
private loadFromCache;
|
|
44
|
+
private saveToCache;
|
|
45
|
+
private emitEvent;
|
|
46
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { PermanentProviderError } from "./types.js";
|
|
2
|
+
import { evaluate } from "./evaluator.js";
|
|
3
|
+
import { HttpProvider } from "./http-provider.js";
|
|
4
|
+
import { MetricsReporter } from "./metrics.js";
|
|
5
|
+
import { LocalStorageCache } from "./cache.js";
|
|
6
|
+
export class OmniFlagsClient {
|
|
7
|
+
flags = new Map();
|
|
8
|
+
hooks = [];
|
|
9
|
+
provider;
|
|
10
|
+
cache;
|
|
11
|
+
sdkKey;
|
|
12
|
+
pollingTimer = null;
|
|
13
|
+
_status = "not_ready";
|
|
14
|
+
_readyPromise;
|
|
15
|
+
_resolveReady;
|
|
16
|
+
metrics;
|
|
17
|
+
eventHandlers = new Map();
|
|
18
|
+
// Loading state
|
|
19
|
+
_isFetching = false;
|
|
20
|
+
_origin = "NONE";
|
|
21
|
+
_fetchError = null;
|
|
22
|
+
// Derived from the first snapshot — not required from the caller
|
|
23
|
+
envKey = "";
|
|
24
|
+
pollingIntervalMs = 30_000; // overwritten by server on first fetch
|
|
25
|
+
constructor(opts, cache) {
|
|
26
|
+
if (opts.sdkKey.startsWith("sk_")) {
|
|
27
|
+
throw new Error("OmniFlags: server-side SDK keys (sk_) cannot be used in client-side SDKs. " +
|
|
28
|
+
"Create a client key (pk_) for use in browser and mobile applications.");
|
|
29
|
+
}
|
|
30
|
+
this.sdkKey = opts.sdkKey;
|
|
31
|
+
this.cache = cache ?? new LocalStorageCache();
|
|
32
|
+
this.hooks = opts.hooks ?? [];
|
|
33
|
+
this.provider = opts.provider ?? new HttpProvider();
|
|
34
|
+
this._readyPromise = new Promise((resolve) => { this._resolveReady = resolve; });
|
|
35
|
+
void this.init();
|
|
36
|
+
}
|
|
37
|
+
// ── Lifecycle ──────────────────────────────────────────────────────────
|
|
38
|
+
get status() { return this._status; }
|
|
39
|
+
get loadingState() {
|
|
40
|
+
return {
|
|
41
|
+
isFetching: this._isFetching,
|
|
42
|
+
isLoading: this._origin === "NONE",
|
|
43
|
+
origin: this._origin,
|
|
44
|
+
error: this._fetchError,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
waitForReady() { return this._readyPromise; }
|
|
48
|
+
/** Trigger an immediate poll without waiting for the next scheduled interval. */
|
|
49
|
+
refresh() { void this.poll(); }
|
|
50
|
+
async shutdown() {
|
|
51
|
+
this.stopPolling();
|
|
52
|
+
await this.metrics?.shutdown();
|
|
53
|
+
await this.provider.shutdown?.();
|
|
54
|
+
}
|
|
55
|
+
// ── Typed Evaluation ───────────────────────────────────────────────────
|
|
56
|
+
isEnabled(flagKey, context, defaultValue = false) {
|
|
57
|
+
return this.evaluateSync(flagKey, "boolean", defaultValue, context).value;
|
|
58
|
+
}
|
|
59
|
+
getValue(flagKey, context, defaultValue) {
|
|
60
|
+
const flag = this.flags.get(flagKey);
|
|
61
|
+
const type = flag?.type ?? "string";
|
|
62
|
+
return this.evaluateSync(flagKey, type, defaultValue, context).value;
|
|
63
|
+
}
|
|
64
|
+
getVariant(flagKey, context) {
|
|
65
|
+
const flag = this.flags.get(flagKey);
|
|
66
|
+
const type = flag?.type ?? "boolean";
|
|
67
|
+
return this.evaluateSync(flagKey, type, null, context);
|
|
68
|
+
}
|
|
69
|
+
getBoolean(flagKey, defaultValue, context) {
|
|
70
|
+
return this.evaluateSync(flagKey, "boolean", defaultValue, context).value;
|
|
71
|
+
}
|
|
72
|
+
getString(flagKey, defaultValue, context) {
|
|
73
|
+
return this.evaluateSync(flagKey, "string", defaultValue, context).value;
|
|
74
|
+
}
|
|
75
|
+
getNumber(flagKey, defaultValue, context) {
|
|
76
|
+
return this.evaluateSync(flagKey, "number", defaultValue, context).value;
|
|
77
|
+
}
|
|
78
|
+
getObject(flagKey, defaultValue, context) {
|
|
79
|
+
return this.evaluateSync(flagKey, "json", defaultValue, context).value;
|
|
80
|
+
}
|
|
81
|
+
getBooleanDetail(flagKey, defaultValue, context) {
|
|
82
|
+
return this.evaluateSync(flagKey, "boolean", defaultValue, context);
|
|
83
|
+
}
|
|
84
|
+
getStringDetail(flagKey, defaultValue, context) {
|
|
85
|
+
return this.evaluateSync(flagKey, "string", defaultValue, context);
|
|
86
|
+
}
|
|
87
|
+
// ── Hooks & Events ─────────────────────────────────────────────────────
|
|
88
|
+
addHook(hook) { this.hooks.push(hook); }
|
|
89
|
+
on(event, handler) {
|
|
90
|
+
if (!this.eventHandlers.has(event))
|
|
91
|
+
this.eventHandlers.set(event, new Set());
|
|
92
|
+
this.eventHandlers.get(event).add(handler);
|
|
93
|
+
return () => { this.eventHandlers.get(event)?.delete(handler); };
|
|
94
|
+
}
|
|
95
|
+
// ── Internal ───────────────────────────────────────────────────────────
|
|
96
|
+
evaluateSync(flagKey, expectedType, callerDefault, invocationContext) {
|
|
97
|
+
if (this._status === "not_ready") {
|
|
98
|
+
return { value: callerDefault, variant: null, reason: "ERROR", ruleId: null, errorCode: "PROVIDER_NOT_READY" };
|
|
99
|
+
}
|
|
100
|
+
const ctx = invocationContext ?? {};
|
|
101
|
+
for (const h of this.hooks) {
|
|
102
|
+
try {
|
|
103
|
+
h.before?.(flagKey, ctx);
|
|
104
|
+
}
|
|
105
|
+
catch { /* swallow */ }
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const flag = this.flags.get(flagKey);
|
|
109
|
+
const result = evaluate(flag, flagKey, expectedType, callerDefault, ctx, this.envKey);
|
|
110
|
+
if (result.variant)
|
|
111
|
+
this.metrics?.track(flagKey, result.variant);
|
|
112
|
+
for (const h of this.hooks) {
|
|
113
|
+
try {
|
|
114
|
+
h.after?.(flagKey, result);
|
|
115
|
+
}
|
|
116
|
+
catch { /* swallow */ }
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
for (const h of this.hooks) {
|
|
122
|
+
try {
|
|
123
|
+
h.error?.(flagKey, err);
|
|
124
|
+
}
|
|
125
|
+
catch { /* swallow */ }
|
|
126
|
+
}
|
|
127
|
+
return { value: callerDefault, variant: null, reason: "ERROR", ruleId: null, errorCode: "UNKNOWN" };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
applySnapshot(snapshot) {
|
|
131
|
+
// Derive envKey and polling interval from the server response
|
|
132
|
+
if (snapshot.envKey)
|
|
133
|
+
this.envKey = snapshot.envKey;
|
|
134
|
+
if (snapshot.pollingIntervalMs > 0)
|
|
135
|
+
this.pollingIntervalMs = snapshot.pollingIntervalMs;
|
|
136
|
+
const m = new Map();
|
|
137
|
+
for (const f of snapshot.flags)
|
|
138
|
+
m.set(f.key, f);
|
|
139
|
+
this.flags = m;
|
|
140
|
+
// Wire up metrics once we know envKey (first snapshot)
|
|
141
|
+
if (!this.metrics && this.envKey) {
|
|
142
|
+
this.metrics = new MetricsReporter(this.sdkKey, this.envKey);
|
|
143
|
+
}
|
|
144
|
+
// Apply server-driven metrics config on every snapshot
|
|
145
|
+
if (snapshot.metricsFlushIntervalMs > 0 && snapshot.metricsMaxBatchSize > 0) {
|
|
146
|
+
this.metrics?.updateConfig(snapshot.metricsFlushIntervalMs, snapshot.metricsMaxBatchSize);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// ── Init & Polling ─────────────────────────────────────────────────────
|
|
150
|
+
async init() {
|
|
151
|
+
await this.loadFromCache();
|
|
152
|
+
this._isFetching = true;
|
|
153
|
+
this.emitEvent("PROVIDER_LOADING_STATE_CHANGED");
|
|
154
|
+
try {
|
|
155
|
+
const snapshot = await this.provider.initialize(this.sdkKey);
|
|
156
|
+
if (snapshot) {
|
|
157
|
+
this.applySnapshot(snapshot);
|
|
158
|
+
await this.saveToCache(snapshot);
|
|
159
|
+
}
|
|
160
|
+
this._origin = "SERVER";
|
|
161
|
+
this._fetchError = null;
|
|
162
|
+
this._status = "ready";
|
|
163
|
+
this._resolveReady();
|
|
164
|
+
this.emitEvent("PROVIDER_READY");
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
this._fetchError = err instanceof Error ? err : new Error(String(err));
|
|
168
|
+
if (this.flags.size > 0) {
|
|
169
|
+
this._status = "stale";
|
|
170
|
+
this._resolveReady();
|
|
171
|
+
this.emitEvent("PROVIDER_STALE");
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
this._status = "error";
|
|
175
|
+
this._resolveReady();
|
|
176
|
+
this.emitEvent("PROVIDER_ERROR");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
this._isFetching = false;
|
|
181
|
+
this.emitEvent("PROVIDER_LOADING_STATE_CHANGED");
|
|
182
|
+
}
|
|
183
|
+
this.startPolling();
|
|
184
|
+
this.metrics?.start();
|
|
185
|
+
}
|
|
186
|
+
startPolling() {
|
|
187
|
+
if (this.pollingTimer)
|
|
188
|
+
return;
|
|
189
|
+
// Use server-driven interval, re-schedule if interval changes after next fetch
|
|
190
|
+
const scheduleNext = () => {
|
|
191
|
+
this.pollingTimer = setTimeout(() => void this.poll().then(scheduleNext), this.pollingIntervalMs);
|
|
192
|
+
};
|
|
193
|
+
scheduleNext();
|
|
194
|
+
}
|
|
195
|
+
stopPolling() {
|
|
196
|
+
if (this.pollingTimer) {
|
|
197
|
+
clearTimeout(this.pollingTimer);
|
|
198
|
+
this.pollingTimer = null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async poll() {
|
|
202
|
+
this._isFetching = true;
|
|
203
|
+
this.emitEvent("PROVIDER_LOADING_STATE_CHANGED");
|
|
204
|
+
try {
|
|
205
|
+
const snap = await this.provider.refresh?.(this.sdkKey);
|
|
206
|
+
if (snap) {
|
|
207
|
+
this.applySnapshot(snap);
|
|
208
|
+
await this.saveToCache(snap);
|
|
209
|
+
this._origin = "SERVER";
|
|
210
|
+
this._fetchError = null;
|
|
211
|
+
this._status = "ready";
|
|
212
|
+
this.emitEvent("PROVIDER_CONFIGURATION_CHANGED");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
this._fetchError = err instanceof Error ? err : new Error(String(err));
|
|
217
|
+
if (err instanceof PermanentProviderError) {
|
|
218
|
+
this.stopPolling();
|
|
219
|
+
this._status = "error";
|
|
220
|
+
this.emitEvent("PROVIDER_ERROR");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (this._status === "ready") {
|
|
224
|
+
this._status = "stale";
|
|
225
|
+
this.emitEvent("PROVIDER_STALE");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
this._isFetching = false;
|
|
230
|
+
this.emitEvent("PROVIDER_LOADING_STATE_CHANGED");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async loadFromCache() {
|
|
234
|
+
if (!this.cache)
|
|
235
|
+
return;
|
|
236
|
+
try {
|
|
237
|
+
// Cache key is sdkKey — each key maps to exactly one environment
|
|
238
|
+
const raw = await this.cache.get(this.sdkKey);
|
|
239
|
+
if (raw) {
|
|
240
|
+
this.applySnapshot(JSON.parse(raw));
|
|
241
|
+
this._origin = "CACHE";
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch { /* corrupt cache */ }
|
|
245
|
+
}
|
|
246
|
+
async saveToCache(snapshot) {
|
|
247
|
+
if (!this.cache)
|
|
248
|
+
return;
|
|
249
|
+
try {
|
|
250
|
+
await this.cache.set(this.sdkKey, JSON.stringify(snapshot));
|
|
251
|
+
}
|
|
252
|
+
catch { /* non-fatal */ }
|
|
253
|
+
}
|
|
254
|
+
emitEvent(event) {
|
|
255
|
+
const handlers = this.eventHandlers.get(event);
|
|
256
|
+
if (handlers)
|
|
257
|
+
for (const h of handlers) {
|
|
258
|
+
try {
|
|
259
|
+
h();
|
|
260
|
+
}
|
|
261
|
+
catch { /* swallow */ }
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { EvaluationContext } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Merge contexts with precedence: global < client < invocation.
|
|
4
|
+
* Higher precedence overwrites lower on key collision.
|
|
5
|
+
*/
|
|
6
|
+
export declare function mergeContexts(...contexts: (EvaluationContext | undefined)[]): EvaluationContext;
|
|
7
|
+
/**
|
|
8
|
+
* Resolve a dot-path attribute from the evaluation context.
|
|
9
|
+
* e.g. "user.plan" resolves ctx.user.plan if nested.
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveAttribute(ctx: EvaluationContext, attribute: string): unknown;
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge contexts with precedence: global < client < invocation.
|
|
3
|
+
* Higher precedence overwrites lower on key collision.
|
|
4
|
+
*/
|
|
5
|
+
export function mergeContexts(...contexts) {
|
|
6
|
+
const result = {};
|
|
7
|
+
for (const ctx of contexts) {
|
|
8
|
+
if (ctx) {
|
|
9
|
+
Object.assign(result, ctx);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Resolve a dot-path attribute from the evaluation context.
|
|
16
|
+
* e.g. "user.plan" resolves ctx.user.plan if nested.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveAttribute(ctx, attribute) {
|
|
19
|
+
if (attribute in ctx) {
|
|
20
|
+
return ctx[attribute];
|
|
21
|
+
}
|
|
22
|
+
const parts = attribute.split(".");
|
|
23
|
+
let current = ctx;
|
|
24
|
+
for (const part of parts) {
|
|
25
|
+
if (current == null || typeof current !== "object")
|
|
26
|
+
return undefined;
|
|
27
|
+
current = current[part];
|
|
28
|
+
}
|
|
29
|
+
return current;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,UAAU,aAAa,CAC3B,GAAG,QAA2C;IAE9C,MAAM,MAAM,GAAsB,EAAE,CAAC;IACrC,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;QAC3B,IAAI,GAAG,EAAE,CAAC;YACR,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAsB,EAAE,SAAiB;IACxE,IAAI,SAAS,IAAI,GAAG,EAAE,CAAC;QACrB,OAAO,GAAG,CAAC,SAAS,CAAC,CAAC;IACxB,CAAC;IACD,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,OAAO,GAAY,GAAG,CAAC;IAC3B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,OAAO,IAAI,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC;QACrE,OAAO,GAAI,OAAmC,CAAC,IAAI,CAAC,CAAC;IACvD,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { EvaluationContext, EvaluationResult, FlagConfig, FlagType } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Core evaluation engine implementing evaluation-spec v2 §5.
|
|
4
|
+
* All SDKs must produce identical results for identical inputs.
|
|
5
|
+
*/
|
|
6
|
+
export declare function evaluate(flag: FlagConfig | undefined, _flagKey: string, expectedType: FlagType, callerDefault: unknown, context: EvaluationContext, envKey: string): EvaluationResult;
|