@nexussdk/flags 0.0.1 → 0.0.3
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 +31 -0
- package/dist/index.cjs +1 -2
- package/dist/index.global.js +1 -2
- package/dist/index.mjs +1 -2
- package/package.json +31 -3
- package/.turbo/turbo-build.log +0 -26
- package/dist/index.cjs.map +0 -1
- package/dist/index.global.js.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/src/client.ts +0 -273
- package/src/evaluator.ts +0 -268
- package/src/index.ts +0 -14
- package/src/sse-manager.ts +0 -165
- package/src/storage.ts +0 -187
- package/tsconfig.json +0 -8
- package/tsup.config.ts +0 -19
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @nexussdk/flags
|
|
2
|
+
|
|
3
|
+
Ultra-lightweight feature flags and remote config client SDK for browsers and edge runtimes (<8KB gzipped).
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@nexussdk/flags)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @nexussdk/flags
|
|
14
|
+
# or
|
|
15
|
+
pnpm add @nexussdk/flags
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- **Deterministic Rollout**: Murmur3 hashing ensures identical evaluation across client and server.
|
|
23
|
+
- **Rule Engine**: Attribute-based access control (ABAC) evaluation against user context.
|
|
24
|
+
- **Real-time Streaming**: Server-Sent Events (SSE) updates flags dynamically without polling.
|
|
25
|
+
- **Local Storage Cache**: Instant boot with cached flags before network connection establishes.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## License
|
|
30
|
+
|
|
31
|
+
MIT © [Nexus Platform](https://github.com/Huynhdung295/NexusSDK)
|
package/dist/index.cjs
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
'use strict';var core=require('@nexussdk/core');function p(r,e=0){let t=e>>>0,s=3432918353,n=461845907,a=0,f=Math.floor(r.length/4)*4;for(;a<f;){let o=r.charCodeAt(a)&255|(r.charCodeAt(a+1)&255)<<8|(r.charCodeAt(a+2)&255)<<16|(r.charCodeAt(a+3)&255)<<24;o=Math.imul(o,s),o=o<<15|o>>>17,o=Math.imul(o,n),t^=o,t=t<<13|t>>>19,t=Math.imul(t,5)+3864292196>>>0,a+=4;}let i=0,h=r.length&3;return h>=3&&(i^=(r.charCodeAt(a+2)&255)<<16),h>=2&&(i^=(r.charCodeAt(a+1)&255)<<8),h>=1&&(i^=r.charCodeAt(a)&255,i=Math.imul(i,s),i=i<<15|i>>>17,i=Math.imul(i,n),t^=i),t^=r.length,t^=t>>>16,t=Math.imul(t,2246822507),t^=t>>>13,t=Math.imul(t,3266489909),t^=t>>>16,t>>>0}function v(r,e){let t=`${r}:${e}`;return p(t)%100}function g(r,e){let t=r.replace(/^v/,"").split(".").map(Number),s=e.replace(/^v/,"").split(".").map(Number);for(let n=0;n<3;n++){let a=(t[n]??0)-(s[n]??0);if(a!==0)return a}return 0}function y(r,e){let t=e.split("."),s=r;for(let n of t){if(s==null||typeof s!="object")return;s=s[n];}return s}function b(r,e){let t=y(e,r.attribute),s=r.values;switch(r.operator){case "EQUALS":return t===s[0];case "NOT_EQUALS":return t!==s[0];case "IN":return s.includes(t);case "NOT_IN":return !s.includes(t);case "CONTAINS":return typeof t=="string"&&t.includes(String(s[0]));case "NOT_CONTAINS":return typeof t=="string"&&!t.includes(String(s[0]));case "STARTS_WITH":return typeof t=="string"&&t.startsWith(String(s[0]));case "ENDS_WITH":return typeof t=="string"&&t.endsWith(String(s[0]));case "GREATER_THAN":return typeof t=="number"&&t>Number(s[0]);case "LESS_THAN":return typeof t=="number"&&t<Number(s[0]);case "SEMVER_GTE":return typeof t=="string"&&typeof s[0]=="string"&&g(t,String(s[0]))>=0;case "SEMVER_LTE":return typeof t=="string"&&typeof s[0]=="string"&&g(t,String(s[0]))<=0;default:return false}}function u(r,e){if(!r.isEnabled)return {key:r.key,enabled:false,variants:{},reason:"KILL_SWITCH",version:r.version};if(r.targetingRules.length>0){if(!r.targetingRules.every(s=>b(s,e)))return {key:r.key,enabled:false,variants:{},reason:"FALLBACK",version:r.version};if(r.rolloutPercentage>=100)return {key:r.key,enabled:true,variants:r.variants,reason:"TARGETING_MATCH",version:r.version}}if(r.rolloutPercentage>0){let t=e.id??"anon";return v(t,r.key)<r.rolloutPercentage?{key:r.key,enabled:true,variants:r.variants,reason:r.targetingRules.length>0?"TARGETING_MATCH":"ROLLOUT_MATCH",version:r.version}:{key:r.key,enabled:false,variants:{},reason:"FALLBACK",version:r.version}}return {key:r.key,enabled:false,variants:{},reason:"KILL_SWITCH",version:r.version}}var l=class{options;eventSource=null;connected=false;destroyed=false;reconnectAttempt=0;reconnectTimer=null;maxReconnects;constructor(e){this.options=e,this.maxReconnects=e.maxReconnects??1/0;}get isConnected(){return this.connected}connect(){this.destroyed||this.connected||this.eventSource||this.openConnection();}disconnect(){this.destroyed=true,this.cleanup();}openConnection(){if(!this.destroyed)try{let e=new URL(this.options.url);e.searchParams.set("apiKey",this.options.apiKey),this.eventSource=new EventSource(e.toString()),this.eventSource.addEventListener("open",()=>{this.connected=!0,this.reconnectAttempt=0,this.options.onStateChange?.(!0);}),this.eventSource.addEventListener("message",t=>{this.handleMessage(t.data);}),this.eventSource.addEventListener("flag_update",t=>{this.handleMessage(t.data);}),this.eventSource.addEventListener("error",()=>{this.connected=!1,this.options.onStateChange?.(!1),this.cleanup(),this.scheduleReconnect();});}catch{this.scheduleReconnect();}}handleMessage(e){try{let t=JSON.parse(e);this.options.onEvent(t);}catch{}}cleanup(){this.eventSource&&(this.eventSource.close(),this.eventSource=null),this.reconnectTimer!==null&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null);}scheduleReconnect(){if(this.destroyed||this.reconnectAttempt>=this.maxReconnects)return;let e=core.computeBackoffMs(this.reconnectAttempt,1e3,3e4);this.reconnectAttempt++,this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.destroyed||this.openConnection();},e);}};var E="nexus_flags_",m="nexus_anon_id",c=class{memory=new Map;storageKey;localStorageAvailable;broadcastChannel=null;constructor(e){this.storageKey=`${E}${e}`,this.localStorageAvailable=this.testLocalStorage(),this.hydrate(),this.setupBroadcastChannel();}set(e,t){this.memory.set(e,t),this.persist(),this.broadcastUpdate(e,t);}get(e){return this.memory.get(e)}setAll(e){for(let[t,s]of Object.entries(e))this.memory.set(t,s);this.persist();}delete(e){this.memory.delete(e),this.persist();}getAll(){return Object.fromEntries(this.memory.entries())}clear(){if(this.memory.clear(),this.localStorageAvailable)try{window.localStorage.removeItem(this.storageKey);}catch{}this.broadcastChannel?.close();}getOrCreateAnonymousId(){if(typeof window>"u")return "anon-ssr-node";if(this.localStorageAvailable)try{let e=window.localStorage.getItem(m);return e||(e=`anon_${Math.random().toString(36).substring(2,11)}`,window.localStorage.setItem(m,e)),e}catch{}return `anon_${Math.random().toString(36).substring(2,11)}`}testLocalStorage(){try{if(typeof window>"u")return !1;let e="__nexus_test__";return window.localStorage.setItem(e,"1"),window.localStorage.removeItem(e),!0}catch{return false}}hydrate(){if(this.localStorageAvailable)try{let e=window.localStorage.getItem(this.storageKey);if(e){let t=JSON.parse(e);for(let[s,n]of Object.entries(t))this.memory.set(s,n);}}catch{}}persist(){if(this.localStorageAvailable)try{window.localStorage.setItem(this.storageKey,JSON.stringify(Object.fromEntries(this.memory.entries())));}catch{}}setupBroadcastChannel(){try{typeof BroadcastChannel<"u"&&(this.broadcastChannel=new BroadcastChannel(`nexus_flags_${this.storageKey}`),this.broadcastChannel.onmessage=e=>{e.data?.key&&e.data?.result&&this.memory.set(e.data.key,e.data.result);});}catch{}}broadcastUpdate(e,t){try{this.broadcastChannel?.postMessage({key:e,result:t});}catch{}}};var d=class{apiKey;baseUrl;timeoutMs;user;storage;listeners=new Map;sseManager;flagDefinitions=new Map;constructor(e={}){this.apiKey=core.resolveApiKey(e.apiKey),this.baseUrl=core.resolveBaseUrl(e.baseUrl),this.timeoutMs=e.timeoutMs??3e3,this.storage=new c(this.apiKey.substring(0,16)),this.user=e.user??{id:this.storage.getOrCreateAnonymousId()},e.bootstrap&&this.storage.setAll(e.bootstrap),e.realtime!==false&&this.initRealtimeSync(),this.refreshFlags();}isEnabled(e,t=false){let s=this.storage.get(e);return s!==void 0?s.enabled:t}getVariant(e,t,s){let n=this.storage.get(e);if(!n?.enabled||!n.variants)return s;let a=n.variants[t];return a!==void 0?a:s}async identify(e){this.user={...this.user,...e},await this.refreshFlags();}reset(){this.user={id:this.storage.getOrCreateAnonymousId()},this.refreshFlags();}onFlagChange(e,t){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t),()=>this.listeners.get(e)?.delete(t)}destroy(){this.sseManager?.disconnect(),this.listeners.clear(),this.storage.clear();}initRealtimeSync(){this.sseManager=new l({url:`${this.baseUrl}/api/v1/flags/stream`,apiKey:this.apiKey,onEvent:e=>{if(e.type==="FLAG_UPDATE"&&e.data){let t=this.flagDefinitions.get(e.key),s=t?u(t,this.user):e.data;this.storage.set(e.key,s),this.listeners.get(e.key)?.forEach(n=>n(s));}else e.type==="FLAG_DELETE"&&(this.storage.delete(e.key),this.flagDefinitions.delete(e.key));}}),this.sseManager.connect();}async refreshFlags(){try{let e=await core.fetchWithRetry({url:`${this.baseUrl}/api/v1/flags/eval`,method:"GET",headers:{Authorization:`Bearer ${this.apiKey}`,"X-Nexus-User-Id":this.user.id??"anon","X-Nexus-Country":this.user.country??""},timeoutMs:this.timeoutMs,maxRetries:2});for(let[t,s]of Object.entries(e.data)){this.flagDefinitions.set(t,s);let n=u(s,this.user);this.storage.set(t,n),this.listeners.get(t)?.forEach(a=>a(n));}}catch{}}};exports.FlagStorage=c;exports.NexusFlagsClient=d;exports.SSEManager=l;exports.computeRolloutBucket=v;exports.evaluateFlag=u;exports.murmur3=p
|
|
2
|
-
//# sourceMappingURL=index.cjs.map
|
|
1
|
+
'use strict';var core=require('@nexussdk/core');function p(r,e=0){let t=e>>>0,s=3432918353,n=461845907,a=0,f=Math.floor(r.length/4)*4;for(;a<f;){let o=r.charCodeAt(a)&255|(r.charCodeAt(a+1)&255)<<8|(r.charCodeAt(a+2)&255)<<16|(r.charCodeAt(a+3)&255)<<24;o=Math.imul(o,s),o=o<<15|o>>>17,o=Math.imul(o,n),t^=o,t=t<<13|t>>>19,t=Math.imul(t,5)+3864292196>>>0,a+=4;}let i=0,h=r.length&3;return h>=3&&(i^=(r.charCodeAt(a+2)&255)<<16),h>=2&&(i^=(r.charCodeAt(a+1)&255)<<8),h>=1&&(i^=r.charCodeAt(a)&255,i=Math.imul(i,s),i=i<<15|i>>>17,i=Math.imul(i,n),t^=i),t^=r.length,t^=t>>>16,t=Math.imul(t,2246822507),t^=t>>>13,t=Math.imul(t,3266489909),t^=t>>>16,t>>>0}function v(r,e){let t=`${r}:${e}`;return p(t)%100}function g(r,e){let t=r.replace(/^v/,"").split(".").map(Number),s=e.replace(/^v/,"").split(".").map(Number);for(let n=0;n<3;n++){let a=(t[n]??0)-(s[n]??0);if(a!==0)return a}return 0}function y(r,e){let t=e.split("."),s=r;for(let n of t){if(s==null||typeof s!="object")return;s=s[n];}return s}function b(r,e){let t=y(e,r.attribute),s=r.values;switch(r.operator){case "EQUALS":return t===s[0];case "NOT_EQUALS":return t!==s[0];case "IN":return s.includes(t);case "NOT_IN":return !s.includes(t);case "CONTAINS":return typeof t=="string"&&t.includes(String(s[0]));case "NOT_CONTAINS":return typeof t=="string"&&!t.includes(String(s[0]));case "STARTS_WITH":return typeof t=="string"&&t.startsWith(String(s[0]));case "ENDS_WITH":return typeof t=="string"&&t.endsWith(String(s[0]));case "GREATER_THAN":return typeof t=="number"&&t>Number(s[0]);case "LESS_THAN":return typeof t=="number"&&t<Number(s[0]);case "SEMVER_GTE":return typeof t=="string"&&typeof s[0]=="string"&&g(t,String(s[0]))>=0;case "SEMVER_LTE":return typeof t=="string"&&typeof s[0]=="string"&&g(t,String(s[0]))<=0;default:return false}}function u(r,e){if(!r.isEnabled)return {key:r.key,enabled:false,variants:{},reason:"KILL_SWITCH",version:r.version};if(r.targetingRules.length>0){if(!r.targetingRules.every(s=>b(s,e)))return {key:r.key,enabled:false,variants:{},reason:"FALLBACK",version:r.version};if(r.rolloutPercentage>=100)return {key:r.key,enabled:true,variants:r.variants,reason:"TARGETING_MATCH",version:r.version}}if(r.rolloutPercentage>0){let t=e.id??"anon";return v(t,r.key)<r.rolloutPercentage?{key:r.key,enabled:true,variants:r.variants,reason:r.targetingRules.length>0?"TARGETING_MATCH":"ROLLOUT_MATCH",version:r.version}:{key:r.key,enabled:false,variants:{},reason:"FALLBACK",version:r.version}}return {key:r.key,enabled:false,variants:{},reason:"KILL_SWITCH",version:r.version}}var l=class{options;eventSource=null;connected=false;destroyed=false;reconnectAttempt=0;reconnectTimer=null;maxReconnects;constructor(e){this.options=e,this.maxReconnects=e.maxReconnects??1/0;}get isConnected(){return this.connected}connect(){this.destroyed||this.connected||this.eventSource||this.openConnection();}disconnect(){this.destroyed=true,this.cleanup();}openConnection(){if(!this.destroyed)try{let e=new URL(this.options.url);e.searchParams.set("apiKey",this.options.apiKey),this.eventSource=new EventSource(e.toString()),this.eventSource.addEventListener("open",()=>{this.connected=!0,this.reconnectAttempt=0,this.options.onStateChange?.(!0);}),this.eventSource.addEventListener("message",t=>{this.handleMessage(t.data);}),this.eventSource.addEventListener("flag_update",t=>{this.handleMessage(t.data);}),this.eventSource.addEventListener("error",()=>{this.connected=!1,this.options.onStateChange?.(!1),this.cleanup(),this.scheduleReconnect();});}catch{this.scheduleReconnect();}}handleMessage(e){try{let t=JSON.parse(e);this.options.onEvent(t);}catch{}}cleanup(){this.eventSource&&(this.eventSource.close(),this.eventSource=null),this.reconnectTimer!==null&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null);}scheduleReconnect(){if(this.destroyed||this.reconnectAttempt>=this.maxReconnects)return;let e=core.computeBackoffMs(this.reconnectAttempt,1e3,3e4);this.reconnectAttempt++,this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.destroyed||this.openConnection();},e);}};var E="nexus_flags_",m="nexus_anon_id",c=class{memory=new Map;storageKey;localStorageAvailable;broadcastChannel=null;constructor(e){this.storageKey=`${E}${e}`,this.localStorageAvailable=this.testLocalStorage(),this.hydrate(),this.setupBroadcastChannel();}set(e,t){this.memory.set(e,t),this.persist(),this.broadcastUpdate(e,t);}get(e){return this.memory.get(e)}setAll(e){for(let[t,s]of Object.entries(e))this.memory.set(t,s);this.persist();}delete(e){this.memory.delete(e),this.persist();}getAll(){return Object.fromEntries(this.memory.entries())}clear(){if(this.memory.clear(),this.localStorageAvailable)try{window.localStorage.removeItem(this.storageKey);}catch{}this.broadcastChannel?.close();}getOrCreateAnonymousId(){if(typeof window>"u")return "anon-ssr-node";if(this.localStorageAvailable)try{let e=window.localStorage.getItem(m);return e||(e=`anon_${Math.random().toString(36).substring(2,11)}`,window.localStorage.setItem(m,e)),e}catch{}return `anon_${Math.random().toString(36).substring(2,11)}`}testLocalStorage(){try{if(typeof window>"u")return !1;let e="__nexus_test__";return window.localStorage.setItem(e,"1"),window.localStorage.removeItem(e),!0}catch{return false}}hydrate(){if(this.localStorageAvailable)try{let e=window.localStorage.getItem(this.storageKey);if(e){let t=JSON.parse(e);for(let[s,n]of Object.entries(t))this.memory.set(s,n);}}catch{}}persist(){if(this.localStorageAvailable)try{window.localStorage.setItem(this.storageKey,JSON.stringify(Object.fromEntries(this.memory.entries())));}catch{}}setupBroadcastChannel(){try{typeof BroadcastChannel<"u"&&(this.broadcastChannel=new BroadcastChannel(`nexus_flags_${this.storageKey}`),this.broadcastChannel.onmessage=e=>{e.data?.key&&e.data?.result&&this.memory.set(e.data.key,e.data.result);});}catch{}}broadcastUpdate(e,t){try{this.broadcastChannel?.postMessage({key:e,result:t});}catch{}}};var d=class{apiKey;baseUrl;timeoutMs;user;storage;listeners=new Map;sseManager;flagDefinitions=new Map;constructor(e={}){this.apiKey=core.resolveApiKey(e.apiKey),this.baseUrl=core.resolveBaseUrl(e.baseUrl),this.timeoutMs=e.timeoutMs??3e3,this.storage=new c(this.apiKey.substring(0,16)),this.user=e.user??{id:this.storage.getOrCreateAnonymousId()},e.bootstrap&&this.storage.setAll(e.bootstrap),e.realtime!==false&&this.initRealtimeSync(),this.refreshFlags();}isEnabled(e,t=false){let s=this.storage.get(e);return s!==void 0?s.enabled:t}getVariant(e,t,s){let n=this.storage.get(e);if(!n?.enabled||!n.variants)return s;let a=n.variants[t];return a!==void 0?a:s}async identify(e){this.user={...this.user,...e},await this.refreshFlags();}reset(){this.user={id:this.storage.getOrCreateAnonymousId()},this.refreshFlags();}onFlagChange(e,t){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t),()=>this.listeners.get(e)?.delete(t)}destroy(){this.sseManager?.disconnect(),this.listeners.clear(),this.storage.clear();}initRealtimeSync(){this.sseManager=new l({url:`${this.baseUrl}/api/v1/flags/stream`,apiKey:this.apiKey,onEvent:e=>{if(e.type==="FLAG_UPDATE"&&e.data){let t=this.flagDefinitions.get(e.key),s=t?u(t,this.user):e.data;this.storage.set(e.key,s),this.listeners.get(e.key)?.forEach(n=>n(s));}else e.type==="FLAG_DELETE"&&(this.storage.delete(e.key),this.flagDefinitions.delete(e.key));}}),this.sseManager.connect();}async refreshFlags(){try{let e=await core.fetchWithRetry({url:`${this.baseUrl}/api/v1/flags/eval`,method:"GET",headers:{Authorization:`Bearer ${this.apiKey}`,"X-Nexus-User-Id":this.user.id??"anon","X-Nexus-Country":this.user.country??""},timeoutMs:this.timeoutMs,maxRetries:2});for(let[t,s]of Object.entries(e.data)){this.flagDefinitions.set(t,s);let n=u(s,this.user);this.storage.set(t,n),this.listeners.get(t)?.forEach(a=>a(n));}}catch{}}};exports.FlagStorage=c;exports.NexusFlagsClient=d;exports.SSEManager=l;exports.computeRolloutBucket=v;exports.evaluateFlag=u;exports.murmur3=p;
|
package/dist/index.global.js
CHANGED
|
@@ -5,5 +5,4 @@ var NexusFlags=(function(exports){'use strict';function m(r,e=1e3,t=3e4){let n=e
|
|
|
5
5
|
4. NEXT_PUBLIC_NEXUS_API_KEY (Next.js)
|
|
6
6
|
5. VITE_NEXUS_API_KEY (Vite)
|
|
7
7
|
6. NUXT_PUBLIC_NEXUS_API_KEY (Nuxt)`);return e.trim()}function w(r,e="https://api.nexus.dev"){return (r||p("NEXT_PUBLIC_NEXUS_URL")||A("VITE_NEXUS_URL")||e).replace(/\/$/,"")}function C(r,e=0){let t=e>>>0,n=3432918353,s=461845907,a=0,d=Math.floor(r.length/4)*4;for(;a<d;){let o=r.charCodeAt(a)&255|(r.charCodeAt(a+1)&255)<<8|(r.charCodeAt(a+2)&255)<<16|(r.charCodeAt(a+3)&255)<<24;o=Math.imul(o,n),o=o<<15|o>>>17,o=Math.imul(o,s),t^=o,t=t<<13|t>>>19,t=Math.imul(t,5)+3864292196>>>0,a+=4;}let i=0,c=r.length&3;return c>=3&&(i^=(r.charCodeAt(a+2)&255)<<16),c>=2&&(i^=(r.charCodeAt(a+1)&255)<<8),c>=1&&(i^=r.charCodeAt(a)&255,i=Math.imul(i,n),i=i<<15|i>>>17,i=Math.imul(i,s),t^=i),t^=r.length,t^=t>>>16,t=Math.imul(t,2246822507),t^=t>>>13,t=Math.imul(t,3266489909),t^=t>>>16,t>>>0}function N(r,e){let t=`${r}:${e}`;return C(t)%100}function R(r,e){let t=r.replace(/^v/,"").split(".").map(Number),n=e.replace(/^v/,"").split(".").map(Number);for(let s=0;s<3;s++){let a=(t[s]??0)-(n[s]??0);if(a!==0)return a}return 0}function k(r,e){let t=e.split("."),n=r;for(let s of t){if(n==null||typeof n!="object")return;n=n[s];}return n}function O(r,e){let t=k(e,r.attribute),n=r.values;switch(r.operator){case "EQUALS":return t===n[0];case "NOT_EQUALS":return t!==n[0];case "IN":return n.includes(t);case "NOT_IN":return !n.includes(t);case "CONTAINS":return typeof t=="string"&&t.includes(String(n[0]));case "NOT_CONTAINS":return typeof t=="string"&&!t.includes(String(n[0]));case "STARTS_WITH":return typeof t=="string"&&t.startsWith(String(n[0]));case "ENDS_WITH":return typeof t=="string"&&t.endsWith(String(n[0]));case "GREATER_THAN":return typeof t=="number"&&t>Number(n[0]);case "LESS_THAN":return typeof t=="number"&&t<Number(n[0]);case "SEMVER_GTE":return typeof t=="string"&&typeof n[0]=="string"&&R(t,String(n[0]))>=0;case "SEMVER_LTE":return typeof t=="string"&&typeof n[0]=="string"&&R(t,String(n[0]))<=0;default:return false}}function v(r,e){if(!r.isEnabled)return {key:r.key,enabled:false,variants:{},reason:"KILL_SWITCH",version:r.version};if(r.targetingRules.length>0){if(!r.targetingRules.every(n=>O(n,e)))return {key:r.key,enabled:false,variants:{},reason:"FALLBACK",version:r.version};if(r.rolloutPercentage>=100)return {key:r.key,enabled:true,variants:r.variants,reason:"TARGETING_MATCH",version:r.version}}if(r.rolloutPercentage>0){let t=e.id??"anon";return N(t,r.key)<r.rolloutPercentage?{key:r.key,enabled:true,variants:r.variants,reason:r.targetingRules.length>0?"TARGETING_MATCH":"ROLLOUT_MATCH",version:r.version}:{key:r.key,enabled:false,variants:{},reason:"FALLBACK",version:r.version}}return {key:r.key,enabled:false,variants:{},reason:"KILL_SWITCH",version:r.version}}var f=class{options;eventSource=null;connected=false;destroyed=false;reconnectAttempt=0;reconnectTimer=null;maxReconnects;constructor(e){this.options=e,this.maxReconnects=e.maxReconnects??1/0;}get isConnected(){return this.connected}connect(){this.destroyed||this.connected||this.eventSource||this.openConnection();}disconnect(){this.destroyed=true,this.cleanup();}openConnection(){if(!this.destroyed)try{let e=new URL(this.options.url);e.searchParams.set("apiKey",this.options.apiKey),this.eventSource=new EventSource(e.toString()),this.eventSource.addEventListener("open",()=>{this.connected=!0,this.reconnectAttempt=0,this.options.onStateChange?.(!0);}),this.eventSource.addEventListener("message",t=>{this.handleMessage(t.data);}),this.eventSource.addEventListener("flag_update",t=>{this.handleMessage(t.data);}),this.eventSource.addEventListener("error",()=>{this.connected=!1,this.options.onStateChange?.(!1),this.cleanup(),this.scheduleReconnect();});}catch{this.scheduleReconnect();}}handleMessage(e){try{let t=JSON.parse(e);this.options.onEvent(t);}catch{}}cleanup(){this.eventSource&&(this.eventSource.close(),this.eventSource=null),this.reconnectTimer!==null&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null);}scheduleReconnect(){if(this.destroyed||this.reconnectAttempt>=this.maxReconnects)return;let e=m(this.reconnectAttempt,1e3,3e4);this.reconnectAttempt++,this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.destroyed||this.openConnection();},e);}};var P="nexus_flags_",F="nexus_anon_id",g=class{memory=new Map;storageKey;localStorageAvailable;broadcastChannel=null;constructor(e){this.storageKey=`${P}${e}`,this.localStorageAvailable=this.testLocalStorage(),this.hydrate(),this.setupBroadcastChannel();}set(e,t){this.memory.set(e,t),this.persist(),this.broadcastUpdate(e,t);}get(e){return this.memory.get(e)}setAll(e){for(let[t,n]of Object.entries(e))this.memory.set(t,n);this.persist();}delete(e){this.memory.delete(e),this.persist();}getAll(){return Object.fromEntries(this.memory.entries())}clear(){if(this.memory.clear(),this.localStorageAvailable)try{window.localStorage.removeItem(this.storageKey);}catch{}this.broadcastChannel?.close();}getOrCreateAnonymousId(){if(typeof window>"u")return "anon-ssr-node";if(this.localStorageAvailable)try{let e=window.localStorage.getItem(F);return e||(e=`anon_${Math.random().toString(36).substring(2,11)}`,window.localStorage.setItem(F,e)),e}catch{}return `anon_${Math.random().toString(36).substring(2,11)}`}testLocalStorage(){try{if(typeof window>"u")return !1;let e="__nexus_test__";return window.localStorage.setItem(e,"1"),window.localStorage.removeItem(e),!0}catch{return false}}hydrate(){if(this.localStorageAvailable)try{let e=window.localStorage.getItem(this.storageKey);if(e){let t=JSON.parse(e);for(let[n,s]of Object.entries(t))this.memory.set(n,s);}}catch{}}persist(){if(this.localStorageAvailable)try{window.localStorage.setItem(this.storageKey,JSON.stringify(Object.fromEntries(this.memory.entries())));}catch{}}setupBroadcastChannel(){try{typeof BroadcastChannel<"u"&&(this.broadcastChannel=new BroadcastChannel(`nexus_flags_${this.storageKey}`),this.broadcastChannel.onmessage=e=>{e.data?.key&&e.data?.result&&this.memory.set(e.data.key,e.data.result);});}catch{}}broadcastUpdate(e,t){try{this.broadcastChannel?.postMessage({key:e,result:t});}catch{}}};var b=class{apiKey;baseUrl;timeoutMs;user;storage;listeners=new Map;sseManager;flagDefinitions=new Map;constructor(e={}){this.apiKey=T(e.apiKey),this.baseUrl=w(e.baseUrl),this.timeoutMs=e.timeoutMs??3e3,this.storage=new g(this.apiKey.substring(0,16)),this.user=e.user??{id:this.storage.getOrCreateAnonymousId()},e.bootstrap&&this.storage.setAll(e.bootstrap),e.realtime!==false&&this.initRealtimeSync(),this.refreshFlags();}isEnabled(e,t=false){let n=this.storage.get(e);return n!==void 0?n.enabled:t}getVariant(e,t,n){let s=this.storage.get(e);if(!s?.enabled||!s.variants)return n;let a=s.variants[t];return a!==void 0?a:n}async identify(e){this.user={...this.user,...e},await this.refreshFlags();}reset(){this.user={id:this.storage.getOrCreateAnonymousId()},this.refreshFlags();}onFlagChange(e,t){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t),()=>this.listeners.get(e)?.delete(t)}destroy(){this.sseManager?.disconnect(),this.listeners.clear(),this.storage.clear();}initRealtimeSync(){this.sseManager=new f({url:`${this.baseUrl}/api/v1/flags/stream`,apiKey:this.apiKey,onEvent:e=>{if(e.type==="FLAG_UPDATE"&&e.data){let t=this.flagDefinitions.get(e.key),n=t?v(t,this.user):e.data;this.storage.set(e.key,n),this.listeners.get(e.key)?.forEach(s=>s(n));}else e.type==="FLAG_DELETE"&&(this.storage.delete(e.key),this.flagDefinitions.delete(e.key));}}),this.sseManager.connect();}async refreshFlags(){try{let e=await x({url:`${this.baseUrl}/api/v1/flags/eval`,method:"GET",headers:{Authorization:`Bearer ${this.apiKey}`,"X-Nexus-User-Id":this.user.id??"anon","X-Nexus-Country":this.user.country??""},timeoutMs:this.timeoutMs,maxRetries:2});for(let[t,n]of Object.entries(e.data)){this.flagDefinitions.set(t,n);let s=v(n,this.user);this.storage.set(t,s),this.listeners.get(t)?.forEach(a=>a(s));}}catch{}}};
|
|
8
|
-
exports.FlagStorage=g;exports.NexusFlagsClient=b;exports.SSEManager=f;exports.computeRolloutBucket=N;exports.evaluateFlag=v;exports.murmur3=C;return exports;})({})
|
|
9
|
-
//# sourceMappingURL=index.global.js.map
|
|
8
|
+
exports.FlagStorage=g;exports.NexusFlagsClient=b;exports.SSEManager=f;exports.computeRolloutBucket=N;exports.evaluateFlag=v;exports.murmur3=C;return exports;})({});
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
import {computeBackoffMs,resolveApiKey,resolveBaseUrl,fetchWithRetry}from'@nexussdk/core';function p(r,e=0){let t=e>>>0,s=3432918353,n=461845907,a=0,f=Math.floor(r.length/4)*4;for(;a<f;){let o=r.charCodeAt(a)&255|(r.charCodeAt(a+1)&255)<<8|(r.charCodeAt(a+2)&255)<<16|(r.charCodeAt(a+3)&255)<<24;o=Math.imul(o,s),o=o<<15|o>>>17,o=Math.imul(o,n),t^=o,t=t<<13|t>>>19,t=Math.imul(t,5)+3864292196>>>0,a+=4;}let i=0,h=r.length&3;return h>=3&&(i^=(r.charCodeAt(a+2)&255)<<16),h>=2&&(i^=(r.charCodeAt(a+1)&255)<<8),h>=1&&(i^=r.charCodeAt(a)&255,i=Math.imul(i,s),i=i<<15|i>>>17,i=Math.imul(i,n),t^=i),t^=r.length,t^=t>>>16,t=Math.imul(t,2246822507),t^=t>>>13,t=Math.imul(t,3266489909),t^=t>>>16,t>>>0}function v(r,e){let t=`${r}:${e}`;return p(t)%100}function g(r,e){let t=r.replace(/^v/,"").split(".").map(Number),s=e.replace(/^v/,"").split(".").map(Number);for(let n=0;n<3;n++){let a=(t[n]??0)-(s[n]??0);if(a!==0)return a}return 0}function y(r,e){let t=e.split("."),s=r;for(let n of t){if(s==null||typeof s!="object")return;s=s[n];}return s}function b(r,e){let t=y(e,r.attribute),s=r.values;switch(r.operator){case "EQUALS":return t===s[0];case "NOT_EQUALS":return t!==s[0];case "IN":return s.includes(t);case "NOT_IN":return !s.includes(t);case "CONTAINS":return typeof t=="string"&&t.includes(String(s[0]));case "NOT_CONTAINS":return typeof t=="string"&&!t.includes(String(s[0]));case "STARTS_WITH":return typeof t=="string"&&t.startsWith(String(s[0]));case "ENDS_WITH":return typeof t=="string"&&t.endsWith(String(s[0]));case "GREATER_THAN":return typeof t=="number"&&t>Number(s[0]);case "LESS_THAN":return typeof t=="number"&&t<Number(s[0]);case "SEMVER_GTE":return typeof t=="string"&&typeof s[0]=="string"&&g(t,String(s[0]))>=0;case "SEMVER_LTE":return typeof t=="string"&&typeof s[0]=="string"&&g(t,String(s[0]))<=0;default:return false}}function u(r,e){if(!r.isEnabled)return {key:r.key,enabled:false,variants:{},reason:"KILL_SWITCH",version:r.version};if(r.targetingRules.length>0){if(!r.targetingRules.every(s=>b(s,e)))return {key:r.key,enabled:false,variants:{},reason:"FALLBACK",version:r.version};if(r.rolloutPercentage>=100)return {key:r.key,enabled:true,variants:r.variants,reason:"TARGETING_MATCH",version:r.version}}if(r.rolloutPercentage>0){let t=e.id??"anon";return v(t,r.key)<r.rolloutPercentage?{key:r.key,enabled:true,variants:r.variants,reason:r.targetingRules.length>0?"TARGETING_MATCH":"ROLLOUT_MATCH",version:r.version}:{key:r.key,enabled:false,variants:{},reason:"FALLBACK",version:r.version}}return {key:r.key,enabled:false,variants:{},reason:"KILL_SWITCH",version:r.version}}var l=class{options;eventSource=null;connected=false;destroyed=false;reconnectAttempt=0;reconnectTimer=null;maxReconnects;constructor(e){this.options=e,this.maxReconnects=e.maxReconnects??1/0;}get isConnected(){return this.connected}connect(){this.destroyed||this.connected||this.eventSource||this.openConnection();}disconnect(){this.destroyed=true,this.cleanup();}openConnection(){if(!this.destroyed)try{let e=new URL(this.options.url);e.searchParams.set("apiKey",this.options.apiKey),this.eventSource=new EventSource(e.toString()),this.eventSource.addEventListener("open",()=>{this.connected=!0,this.reconnectAttempt=0,this.options.onStateChange?.(!0);}),this.eventSource.addEventListener("message",t=>{this.handleMessage(t.data);}),this.eventSource.addEventListener("flag_update",t=>{this.handleMessage(t.data);}),this.eventSource.addEventListener("error",()=>{this.connected=!1,this.options.onStateChange?.(!1),this.cleanup(),this.scheduleReconnect();});}catch{this.scheduleReconnect();}}handleMessage(e){try{let t=JSON.parse(e);this.options.onEvent(t);}catch{}}cleanup(){this.eventSource&&(this.eventSource.close(),this.eventSource=null),this.reconnectTimer!==null&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null);}scheduleReconnect(){if(this.destroyed||this.reconnectAttempt>=this.maxReconnects)return;let e=computeBackoffMs(this.reconnectAttempt,1e3,3e4);this.reconnectAttempt++,this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.destroyed||this.openConnection();},e);}};var E="nexus_flags_",m="nexus_anon_id",c=class{memory=new Map;storageKey;localStorageAvailable;broadcastChannel=null;constructor(e){this.storageKey=`${E}${e}`,this.localStorageAvailable=this.testLocalStorage(),this.hydrate(),this.setupBroadcastChannel();}set(e,t){this.memory.set(e,t),this.persist(),this.broadcastUpdate(e,t);}get(e){return this.memory.get(e)}setAll(e){for(let[t,s]of Object.entries(e))this.memory.set(t,s);this.persist();}delete(e){this.memory.delete(e),this.persist();}getAll(){return Object.fromEntries(this.memory.entries())}clear(){if(this.memory.clear(),this.localStorageAvailable)try{window.localStorage.removeItem(this.storageKey);}catch{}this.broadcastChannel?.close();}getOrCreateAnonymousId(){if(typeof window>"u")return "anon-ssr-node";if(this.localStorageAvailable)try{let e=window.localStorage.getItem(m);return e||(e=`anon_${Math.random().toString(36).substring(2,11)}`,window.localStorage.setItem(m,e)),e}catch{}return `anon_${Math.random().toString(36).substring(2,11)}`}testLocalStorage(){try{if(typeof window>"u")return !1;let e="__nexus_test__";return window.localStorage.setItem(e,"1"),window.localStorage.removeItem(e),!0}catch{return false}}hydrate(){if(this.localStorageAvailable)try{let e=window.localStorage.getItem(this.storageKey);if(e){let t=JSON.parse(e);for(let[s,n]of Object.entries(t))this.memory.set(s,n);}}catch{}}persist(){if(this.localStorageAvailable)try{window.localStorage.setItem(this.storageKey,JSON.stringify(Object.fromEntries(this.memory.entries())));}catch{}}setupBroadcastChannel(){try{typeof BroadcastChannel<"u"&&(this.broadcastChannel=new BroadcastChannel(`nexus_flags_${this.storageKey}`),this.broadcastChannel.onmessage=e=>{e.data?.key&&e.data?.result&&this.memory.set(e.data.key,e.data.result);});}catch{}}broadcastUpdate(e,t){try{this.broadcastChannel?.postMessage({key:e,result:t});}catch{}}};var d=class{apiKey;baseUrl;timeoutMs;user;storage;listeners=new Map;sseManager;flagDefinitions=new Map;constructor(e={}){this.apiKey=resolveApiKey(e.apiKey),this.baseUrl=resolveBaseUrl(e.baseUrl),this.timeoutMs=e.timeoutMs??3e3,this.storage=new c(this.apiKey.substring(0,16)),this.user=e.user??{id:this.storage.getOrCreateAnonymousId()},e.bootstrap&&this.storage.setAll(e.bootstrap),e.realtime!==false&&this.initRealtimeSync(),this.refreshFlags();}isEnabled(e,t=false){let s=this.storage.get(e);return s!==void 0?s.enabled:t}getVariant(e,t,s){let n=this.storage.get(e);if(!n?.enabled||!n.variants)return s;let a=n.variants[t];return a!==void 0?a:s}async identify(e){this.user={...this.user,...e},await this.refreshFlags();}reset(){this.user={id:this.storage.getOrCreateAnonymousId()},this.refreshFlags();}onFlagChange(e,t){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t),()=>this.listeners.get(e)?.delete(t)}destroy(){this.sseManager?.disconnect(),this.listeners.clear(),this.storage.clear();}initRealtimeSync(){this.sseManager=new l({url:`${this.baseUrl}/api/v1/flags/stream`,apiKey:this.apiKey,onEvent:e=>{if(e.type==="FLAG_UPDATE"&&e.data){let t=this.flagDefinitions.get(e.key),s=t?u(t,this.user):e.data;this.storage.set(e.key,s),this.listeners.get(e.key)?.forEach(n=>n(s));}else e.type==="FLAG_DELETE"&&(this.storage.delete(e.key),this.flagDefinitions.delete(e.key));}}),this.sseManager.connect();}async refreshFlags(){try{let e=await fetchWithRetry({url:`${this.baseUrl}/api/v1/flags/eval`,method:"GET",headers:{Authorization:`Bearer ${this.apiKey}`,"X-Nexus-User-Id":this.user.id??"anon","X-Nexus-Country":this.user.country??""},timeoutMs:this.timeoutMs,maxRetries:2});for(let[t,s]of Object.entries(e.data)){this.flagDefinitions.set(t,s);let n=u(s,this.user);this.storage.set(t,n),this.listeners.get(t)?.forEach(a=>a(n));}}catch{}}};export{c as FlagStorage,d as NexusFlagsClient,l as SSEManager,v as computeRolloutBucket,u as evaluateFlag,p as murmur3}
|
|
2
|
-
//# sourceMappingURL=index.mjs.map
|
|
1
|
+
import {computeBackoffMs,resolveApiKey,resolveBaseUrl,fetchWithRetry}from'@nexussdk/core';function p(r,e=0){let t=e>>>0,s=3432918353,n=461845907,a=0,f=Math.floor(r.length/4)*4;for(;a<f;){let o=r.charCodeAt(a)&255|(r.charCodeAt(a+1)&255)<<8|(r.charCodeAt(a+2)&255)<<16|(r.charCodeAt(a+3)&255)<<24;o=Math.imul(o,s),o=o<<15|o>>>17,o=Math.imul(o,n),t^=o,t=t<<13|t>>>19,t=Math.imul(t,5)+3864292196>>>0,a+=4;}let i=0,h=r.length&3;return h>=3&&(i^=(r.charCodeAt(a+2)&255)<<16),h>=2&&(i^=(r.charCodeAt(a+1)&255)<<8),h>=1&&(i^=r.charCodeAt(a)&255,i=Math.imul(i,s),i=i<<15|i>>>17,i=Math.imul(i,n),t^=i),t^=r.length,t^=t>>>16,t=Math.imul(t,2246822507),t^=t>>>13,t=Math.imul(t,3266489909),t^=t>>>16,t>>>0}function v(r,e){let t=`${r}:${e}`;return p(t)%100}function g(r,e){let t=r.replace(/^v/,"").split(".").map(Number),s=e.replace(/^v/,"").split(".").map(Number);for(let n=0;n<3;n++){let a=(t[n]??0)-(s[n]??0);if(a!==0)return a}return 0}function y(r,e){let t=e.split("."),s=r;for(let n of t){if(s==null||typeof s!="object")return;s=s[n];}return s}function b(r,e){let t=y(e,r.attribute),s=r.values;switch(r.operator){case "EQUALS":return t===s[0];case "NOT_EQUALS":return t!==s[0];case "IN":return s.includes(t);case "NOT_IN":return !s.includes(t);case "CONTAINS":return typeof t=="string"&&t.includes(String(s[0]));case "NOT_CONTAINS":return typeof t=="string"&&!t.includes(String(s[0]));case "STARTS_WITH":return typeof t=="string"&&t.startsWith(String(s[0]));case "ENDS_WITH":return typeof t=="string"&&t.endsWith(String(s[0]));case "GREATER_THAN":return typeof t=="number"&&t>Number(s[0]);case "LESS_THAN":return typeof t=="number"&&t<Number(s[0]);case "SEMVER_GTE":return typeof t=="string"&&typeof s[0]=="string"&&g(t,String(s[0]))>=0;case "SEMVER_LTE":return typeof t=="string"&&typeof s[0]=="string"&&g(t,String(s[0]))<=0;default:return false}}function u(r,e){if(!r.isEnabled)return {key:r.key,enabled:false,variants:{},reason:"KILL_SWITCH",version:r.version};if(r.targetingRules.length>0){if(!r.targetingRules.every(s=>b(s,e)))return {key:r.key,enabled:false,variants:{},reason:"FALLBACK",version:r.version};if(r.rolloutPercentage>=100)return {key:r.key,enabled:true,variants:r.variants,reason:"TARGETING_MATCH",version:r.version}}if(r.rolloutPercentage>0){let t=e.id??"anon";return v(t,r.key)<r.rolloutPercentage?{key:r.key,enabled:true,variants:r.variants,reason:r.targetingRules.length>0?"TARGETING_MATCH":"ROLLOUT_MATCH",version:r.version}:{key:r.key,enabled:false,variants:{},reason:"FALLBACK",version:r.version}}return {key:r.key,enabled:false,variants:{},reason:"KILL_SWITCH",version:r.version}}var l=class{options;eventSource=null;connected=false;destroyed=false;reconnectAttempt=0;reconnectTimer=null;maxReconnects;constructor(e){this.options=e,this.maxReconnects=e.maxReconnects??1/0;}get isConnected(){return this.connected}connect(){this.destroyed||this.connected||this.eventSource||this.openConnection();}disconnect(){this.destroyed=true,this.cleanup();}openConnection(){if(!this.destroyed)try{let e=new URL(this.options.url);e.searchParams.set("apiKey",this.options.apiKey),this.eventSource=new EventSource(e.toString()),this.eventSource.addEventListener("open",()=>{this.connected=!0,this.reconnectAttempt=0,this.options.onStateChange?.(!0);}),this.eventSource.addEventListener("message",t=>{this.handleMessage(t.data);}),this.eventSource.addEventListener("flag_update",t=>{this.handleMessage(t.data);}),this.eventSource.addEventListener("error",()=>{this.connected=!1,this.options.onStateChange?.(!1),this.cleanup(),this.scheduleReconnect();});}catch{this.scheduleReconnect();}}handleMessage(e){try{let t=JSON.parse(e);this.options.onEvent(t);}catch{}}cleanup(){this.eventSource&&(this.eventSource.close(),this.eventSource=null),this.reconnectTimer!==null&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null);}scheduleReconnect(){if(this.destroyed||this.reconnectAttempt>=this.maxReconnects)return;let e=computeBackoffMs(this.reconnectAttempt,1e3,3e4);this.reconnectAttempt++,this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.destroyed||this.openConnection();},e);}};var E="nexus_flags_",m="nexus_anon_id",c=class{memory=new Map;storageKey;localStorageAvailable;broadcastChannel=null;constructor(e){this.storageKey=`${E}${e}`,this.localStorageAvailable=this.testLocalStorage(),this.hydrate(),this.setupBroadcastChannel();}set(e,t){this.memory.set(e,t),this.persist(),this.broadcastUpdate(e,t);}get(e){return this.memory.get(e)}setAll(e){for(let[t,s]of Object.entries(e))this.memory.set(t,s);this.persist();}delete(e){this.memory.delete(e),this.persist();}getAll(){return Object.fromEntries(this.memory.entries())}clear(){if(this.memory.clear(),this.localStorageAvailable)try{window.localStorage.removeItem(this.storageKey);}catch{}this.broadcastChannel?.close();}getOrCreateAnonymousId(){if(typeof window>"u")return "anon-ssr-node";if(this.localStorageAvailable)try{let e=window.localStorage.getItem(m);return e||(e=`anon_${Math.random().toString(36).substring(2,11)}`,window.localStorage.setItem(m,e)),e}catch{}return `anon_${Math.random().toString(36).substring(2,11)}`}testLocalStorage(){try{if(typeof window>"u")return !1;let e="__nexus_test__";return window.localStorage.setItem(e,"1"),window.localStorage.removeItem(e),!0}catch{return false}}hydrate(){if(this.localStorageAvailable)try{let e=window.localStorage.getItem(this.storageKey);if(e){let t=JSON.parse(e);for(let[s,n]of Object.entries(t))this.memory.set(s,n);}}catch{}}persist(){if(this.localStorageAvailable)try{window.localStorage.setItem(this.storageKey,JSON.stringify(Object.fromEntries(this.memory.entries())));}catch{}}setupBroadcastChannel(){try{typeof BroadcastChannel<"u"&&(this.broadcastChannel=new BroadcastChannel(`nexus_flags_${this.storageKey}`),this.broadcastChannel.onmessage=e=>{e.data?.key&&e.data?.result&&this.memory.set(e.data.key,e.data.result);});}catch{}}broadcastUpdate(e,t){try{this.broadcastChannel?.postMessage({key:e,result:t});}catch{}}};var d=class{apiKey;baseUrl;timeoutMs;user;storage;listeners=new Map;sseManager;flagDefinitions=new Map;constructor(e={}){this.apiKey=resolveApiKey(e.apiKey),this.baseUrl=resolveBaseUrl(e.baseUrl),this.timeoutMs=e.timeoutMs??3e3,this.storage=new c(this.apiKey.substring(0,16)),this.user=e.user??{id:this.storage.getOrCreateAnonymousId()},e.bootstrap&&this.storage.setAll(e.bootstrap),e.realtime!==false&&this.initRealtimeSync(),this.refreshFlags();}isEnabled(e,t=false){let s=this.storage.get(e);return s!==void 0?s.enabled:t}getVariant(e,t,s){let n=this.storage.get(e);if(!n?.enabled||!n.variants)return s;let a=n.variants[t];return a!==void 0?a:s}async identify(e){this.user={...this.user,...e},await this.refreshFlags();}reset(){this.user={id:this.storage.getOrCreateAnonymousId()},this.refreshFlags();}onFlagChange(e,t){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t),()=>this.listeners.get(e)?.delete(t)}destroy(){this.sseManager?.disconnect(),this.listeners.clear(),this.storage.clear();}initRealtimeSync(){this.sseManager=new l({url:`${this.baseUrl}/api/v1/flags/stream`,apiKey:this.apiKey,onEvent:e=>{if(e.type==="FLAG_UPDATE"&&e.data){let t=this.flagDefinitions.get(e.key),s=t?u(t,this.user):e.data;this.storage.set(e.key,s),this.listeners.get(e.key)?.forEach(n=>n(s));}else e.type==="FLAG_DELETE"&&(this.storage.delete(e.key),this.flagDefinitions.delete(e.key));}}),this.sseManager.connect();}async refreshFlags(){try{let e=await fetchWithRetry({url:`${this.baseUrl}/api/v1/flags/eval`,method:"GET",headers:{Authorization:`Bearer ${this.apiKey}`,"X-Nexus-User-Id":this.user.id??"anon","X-Nexus-Country":this.user.country??""},timeoutMs:this.timeoutMs,maxRetries:2});for(let[t,s]of Object.entries(e.data)){this.flagDefinitions.set(t,s);let n=u(s,this.user);this.storage.set(t,n),this.listeners.get(t)?.forEach(a=>a(n));}}catch{}}};export{c as FlagStorage,d as NexusFlagsClient,l as SSEManager,v as computeRolloutBucket,u as evaluateFlag,p as murmur3};
|
package/package.json
CHANGED
|
@@ -1,11 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexussdk/flags",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
8
8
|
"description": "Ultra-lightweight feature flags and remote config client SDK for browsers and edge runtimes",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"author": "Nexus",
|
|
11
|
+
"contributors": [
|
|
12
|
+
{
|
|
13
|
+
"name": "Hồ Huỳnh Dũng",
|
|
14
|
+
"email": "hohuynhdung@gmail.com",
|
|
15
|
+
"url": "https://github.com/Huynhdung295"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/Huynhdung295/NexusSDK.git",
|
|
21
|
+
"directory": "packages/sdk-flags"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/Huynhdung295/NexusSDK#readme",
|
|
24
|
+
"keywords": [
|
|
25
|
+
"nexus",
|
|
26
|
+
"nexussdk",
|
|
27
|
+
"feature-flags",
|
|
28
|
+
"remote-config",
|
|
29
|
+
"abac",
|
|
30
|
+
"sse",
|
|
31
|
+
"murmur3"
|
|
32
|
+
],
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"README.md"
|
|
36
|
+
],
|
|
9
37
|
"main": "./dist/index.cjs",
|
|
10
38
|
"module": "./dist/index.mjs",
|
|
11
39
|
"types": "./dist/index.d.ts",
|
|
@@ -17,8 +45,8 @@
|
|
|
17
45
|
}
|
|
18
46
|
},
|
|
19
47
|
"dependencies": {
|
|
20
|
-
"@nexussdk/contracts": "0.0.
|
|
21
|
-
"@nexussdk/core": "0.0.
|
|
48
|
+
"@nexussdk/contracts": "0.0.3",
|
|
49
|
+
"@nexussdk/core": "0.0.3"
|
|
22
50
|
},
|
|
23
51
|
"devDependencies": {
|
|
24
52
|
"tsup": "^8.0.2",
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @nexussdk/flags@0.0.1 build /home/runner/work/NexusSDK/NexusSDK/packages/sdk-flags
|
|
3
|
-
> tsup
|
|
4
|
-
|
|
5
|
-
[34mCLI[39m Building entry: src/index.ts
|
|
6
|
-
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
7
|
-
[34mCLI[39m tsup v8.5.1
|
|
8
|
-
[34mCLI[39m Using tsup config: /home/runner/work/NexusSDK/NexusSDK/packages/sdk-flags/tsup.config.ts
|
|
9
|
-
[34mCLI[39m Target: es2022
|
|
10
|
-
[34mCLI[39m Cleaning output folder
|
|
11
|
-
[34mESM[39m Build start
|
|
12
|
-
[34mCJS[39m Build start
|
|
13
|
-
[34mIIFE[39m Build start
|
|
14
|
-
[32mIIFE[39m [1mdist/index.global.js [22m[32m10.14 KB[39m
|
|
15
|
-
[32mIIFE[39m [1mdist/index.global.js.map [22m[32m56.35 KB[39m
|
|
16
|
-
[32mIIFE[39m ⚡️ Build success in 543ms
|
|
17
|
-
[32mESM[39m [1mdist/index.mjs [22m[32m7.85 KB[39m
|
|
18
|
-
[32mESM[39m [1mdist/index.mjs.map [22m[32m40.53 KB[39m
|
|
19
|
-
[32mESM[39m ⚡️ Build success in 543ms
|
|
20
|
-
[32mCJS[39m [1mdist/index.cjs [22m[32m7.85 KB[39m
|
|
21
|
-
[32mCJS[39m [1mdist/index.cjs.map [22m[32m40.54 KB[39m
|
|
22
|
-
[32mCJS[39m ⚡️ Build success in 543ms
|
|
23
|
-
[34mDTS[39m Build start
|
|
24
|
-
[32mDTS[39m ⚡️ Build success in 1501ms
|
|
25
|
-
[32mDTS[39m [1mdist/index.d.mts [22m[32m11.63 KB[39m
|
|
26
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[32m11.63 KB[39m
|
package/dist/index.cjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/evaluator.ts","../src/sse-manager.ts","../src/storage.ts","../src/client.ts"],"names":["murmur3","key","seed","h","c1","c2","i","length4","k","k2","rem","computeRolloutBucket","userId","flagKey","hashInput","compareSemver","a","b","partsA","partsB","diff","resolveAttribute","ctx","attributePath","parts","current","part","evaluateSingleRule","rule","userCtx","userValue","targets","evaluateFlag","flag","SSEManager","options","url","evt","data","event","delay","computeBackoffMs","STORAGE_KEY_PREFIX","ANON_ID_KEY","FlagStorage","apiKeyPrefix","result","flags","value","id","testKey","raw","parsed","NexusFlagsClient","resolveApiKey","resolveBaseUrl","defaultValue","cached","variantKey","user","callback","flagDef","cb","fetchWithRetry","evaluated"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @fileoverview MurmurHash3 32-bit implementation and ABAC rule evaluator.\n * Pure TypeScript — no external dependencies. Implements deterministic rollout bucketing.\n * @module @nexus/sdk-flags/evaluator\n */\n\nimport type { FeatureFlag, FlagEvaluationResult, TargetingRule, UserContext } from '@nexussdk/contracts';\n\n// ---------------------------------------------------------------------------\n// MurmurHash3 32-bit Implementation (Pure TypeScript)\n// Used for deterministic user-to-bucket mapping without server round-trips.\n// ---------------------------------------------------------------------------\n\n/**\n * Converts a string to a Uint32 MurmurHash3 hash.\n * Implements the MurmurHash3 32-bit algorithm (x86 variant).\n *\n * @param key - Input string to hash.\n * @param seed - Optional seed value. Defaults to 0.\n * @returns Unsigned 32-bit integer hash value.\n *\n * @example\n * const hash = murmur3('user123:checkout_v2', 0);\n * const bucket = hash % 100; // 0-99 deterministic bucket\n */\nexport function murmur3(key: string, seed = 0): number {\n let h = seed >>> 0;\n const c1 = 0xcc9e2d51;\n const c2 = 0x1b873593;\n\n let i = 0;\n // Process 4-byte chunks\n const length4 = Math.floor(key.length / 4) * 4;\n while (i < length4) {\n let k =\n ((key.charCodeAt(i) & 0xff)) |\n ((key.charCodeAt(i + 1) & 0xff) << 8) |\n ((key.charCodeAt(i + 2) & 0xff) << 16) |\n ((key.charCodeAt(i + 3) & 0xff) << 24);\n\n k = Math.imul(k, c1);\n k = (k << 15) | (k >>> 17);\n k = Math.imul(k, c2);\n\n h ^= k;\n h = (h << 13) | (h >>> 19);\n h = (Math.imul(h, 5) + 0xe6546b64) >>> 0;\n i += 4;\n }\n\n // Process remaining bytes\n let k2 = 0;\n const rem = key.length & 3;\n if (rem >= 3) k2 ^= (key.charCodeAt(i + 2) & 0xff) << 16;\n if (rem >= 2) k2 ^= (key.charCodeAt(i + 1) & 0xff) << 8;\n if (rem >= 1) {\n k2 ^= key.charCodeAt(i) & 0xff;\n k2 = Math.imul(k2, c1);\n k2 = (k2 << 15) | (k2 >>> 17);\n k2 = Math.imul(k2, c2);\n h ^= k2;\n }\n\n // Finalization mix\n h ^= key.length;\n h ^= h >>> 16;\n h = Math.imul(h, 0x85ebca6b);\n h ^= h >>> 13;\n h = Math.imul(h, 0xc2b2ae35);\n h ^= h >>> 16;\n\n return h >>> 0; // Ensure unsigned\n}\n\n/**\n * Computes the deterministic rollout bucket (0-99) for a given user+flag combination.\n * Uses MurmurHash3 for even distribution without server state.\n *\n * @param userId - User's unique identifier (anonymous ID if not authenticated).\n * @param flagKey - Flag programmatic key.\n * @returns Bucket value between 0 and 99 (inclusive).\n *\n * @example\n * const bucket = computeRolloutBucket('usr_12345', 'checkout_v2');\n * // isEnabled = bucket < flag.rolloutPercentage\n */\nexport function computeRolloutBucket(userId: string, flagKey: string): number {\n const hashInput = `${userId}:${flagKey}`;\n return murmur3(hashInput) % 100;\n}\n\n// ---------------------------------------------------------------------------\n// ABAC Rule Evaluator\n// ---------------------------------------------------------------------------\n\n/**\n * Performs a simple semantic version comparison.\n * Compares two semver strings in \"MAJOR.MINOR.PATCH\" format.\n *\n * @param a - First version string.\n * @param b - Second version string.\n * @returns Positive if a > b, negative if a < b, 0 if equal.\n */\nfunction compareSemver(a: string, b: string): number {\n const partsA = a.replace(/^v/, '').split('.').map(Number);\n const partsB = b.replace(/^v/, '').split('.').map(Number);\n for (let i = 0; i < 3; i++) {\n const diff = (partsA[i] ?? 0) - (partsB[i] ?? 0);\n if (diff !== 0) return diff;\n }\n return 0;\n}\n\n/**\n * Extracts a nested attribute value from the UserContext by dot-notation path.\n *\n * @param ctx - UserContext object.\n * @param attributePath - Dot-separated path (e.g. \"custom.tier\", \"country\").\n * @returns The attribute value or `undefined` if not found.\n */\nfunction resolveAttribute(ctx: UserContext, attributePath: string): unknown {\n const parts = attributePath.split('.');\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let current: any = ctx;\n for (const part of parts) {\n if (current == null || typeof current !== 'object') return undefined;\n current = current[part];\n }\n return current;\n}\n\n/**\n * Evaluates a single targeting rule against the user context.\n *\n * @param rule - The ABAC targeting rule to evaluate.\n * @param userCtx - The current user context.\n * @returns `true` if the rule passes for this user.\n */\nfunction evaluateSingleRule(rule: TargetingRule, userCtx: UserContext): boolean {\n const userValue = resolveAttribute(userCtx, rule.attribute);\n const targets = rule.values;\n\n switch (rule.operator) {\n case 'EQUALS':\n return userValue === targets[0];\n case 'NOT_EQUALS':\n return userValue !== targets[0];\n case 'IN':\n return targets.includes(userValue as string | number | boolean);\n case 'NOT_IN':\n return !targets.includes(userValue as string | number | boolean);\n case 'CONTAINS':\n return typeof userValue === 'string' && userValue.includes(String(targets[0]));\n case 'NOT_CONTAINS':\n return typeof userValue === 'string' && !userValue.includes(String(targets[0]));\n case 'STARTS_WITH':\n return typeof userValue === 'string' && userValue.startsWith(String(targets[0]));\n case 'ENDS_WITH':\n return typeof userValue === 'string' && userValue.endsWith(String(targets[0]));\n case 'GREATER_THAN':\n return typeof userValue === 'number' && userValue > Number(targets[0]);\n case 'LESS_THAN':\n return typeof userValue === 'number' && userValue < Number(targets[0]);\n case 'SEMVER_GTE':\n return (\n typeof userValue === 'string' &&\n typeof targets[0] === 'string' &&\n compareSemver(userValue, String(targets[0])) >= 0\n );\n case 'SEMVER_LTE':\n return (\n typeof userValue === 'string' &&\n typeof targets[0] === 'string' &&\n compareSemver(userValue, String(targets[0])) <= 0\n );\n default:\n return false;\n }\n}\n\n/**\n * Evaluates a FeatureFlag against a UserContext using the full ABAC + rollout engine.\n *\n * Evaluation Order:\n * 1. Kill-switch check (`isEnabled === false` → KILL_SWITCH)\n * 2. All targeting rules must pass (ABAC evaluation)\n * 3. Percentage rollout via MurmurHash3 bucketing\n * 4. Default enabled state\n *\n * @param flag - The feature flag definition from the cache.\n * @param userCtx - Current user context for targeting evaluation.\n * @returns Full {@link FlagEvaluationResult} with reason explanation.\n *\n * @example\n * const result = evaluateFlag(flag, { id: 'usr_12345', country: 'VN' });\n * if (result.enabled) {\n * console.log(result.variants.discount_rate); // 20\n * }\n */\nexport function evaluateFlag(flag: FeatureFlag, userCtx: UserContext): FlagEvaluationResult {\n // Step 1: Kill-switch\n if (!flag.isEnabled) {\n return {\n key: flag.key,\n enabled: false,\n variants: {},\n reason: 'KILL_SWITCH',\n version: flag.version,\n };\n }\n\n // Step 2: ABAC targeting rules — all must pass (AND logic)\n if (flag.targetingRules.length > 0) {\n const allRulesPass = flag.targetingRules.every((rule) =>\n evaluateSingleRule(rule, userCtx),\n );\n if (!allRulesPass) {\n return {\n key: flag.key,\n enabled: false,\n variants: {},\n reason: 'FALLBACK',\n version: flag.version,\n };\n }\n // All targeting rules matched\n if (flag.rolloutPercentage >= 100) {\n return {\n key: flag.key,\n enabled: true,\n variants: flag.variants,\n reason: 'TARGETING_MATCH',\n version: flag.version,\n };\n }\n }\n\n // Step 3: Percentage rollout (MurmurHash3)\n if (flag.rolloutPercentage > 0) {\n const userId = userCtx.id ?? 'anon';\n const bucket = computeRolloutBucket(userId, flag.key);\n if (bucket < flag.rolloutPercentage) {\n return {\n key: flag.key,\n enabled: true,\n variants: flag.variants,\n reason: flag.targetingRules.length > 0 ? 'TARGETING_MATCH' : 'ROLLOUT_MATCH',\n version: flag.version,\n };\n }\n return {\n key: flag.key,\n enabled: false,\n variants: {},\n reason: 'FALLBACK',\n version: flag.version,\n };\n }\n\n // Step 4: Full kill-switch off (rolloutPercentage === 0 with no rules)\n return {\n key: flag.key,\n enabled: false,\n variants: {},\n reason: 'KILL_SWITCH',\n version: flag.version,\n };\n}\n","/**\n * @fileoverview Singleton SSE Manager with ref-counting and auto-reconnect.\n * Ensures exactly one EventSource connection per API key across all flag subscribers.\n * @module @nexus/sdk-flags/sse-manager\n */\n\nimport type { FlagStreamEvent } from '@nexussdk/contracts';\nimport { computeBackoffMs } from '@nexussdk/core';\n\n/**\n * Options for creating an SSE connection.\n */\nexport interface SSEManagerOptions {\n /** Full SSE stream URL (e.g. \"http://localhost:8080/api/v1/flags/stream\"). */\n url: string;\n /** Public API key for authentication. */\n apiKey: string;\n /** Callback invoked for each received flag stream event. */\n onEvent: (event: FlagStreamEvent) => void;\n /** Callback invoked on connection state changes (optional). */\n onStateChange?: (connected: boolean) => void;\n /** Maximum reconnect attempts. Defaults to Infinity. */\n maxReconnects?: number;\n}\n\n/**\n * Ref-counted Singleton SSE Manager.\n *\n * Multiple consumers sharing the same `apiKey` receive updates\n * from a single shared SSE connection. Connection is torn down when all\n * consumers have called `disconnect()`.\n *\n * Connection lifecycle:\n * 1. `connect()` → opens EventSource or streaming fetch\n * 2. On disconnect: exponential backoff reconnect (max 30s)\n * 3. `disconnect()` → decrements ref count; tears down on 0\n *\n * @example\n * const manager = new SSEManager({\n * url: 'http://localhost:8080/api/v1/flags/stream',\n * apiKey: 'pk_live_...',\n * onEvent: (event) => updateCache(event),\n * });\n * manager.connect();\n * // Later:\n * manager.disconnect();\n */\nexport class SSEManager {\n private readonly options: SSEManagerOptions;\n private eventSource: EventSource | null = null;\n private connected = false;\n private destroyed = false;\n private reconnectAttempt = 0;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private readonly maxReconnects: number;\n\n constructor(options: SSEManagerOptions) {\n this.options = options;\n this.maxReconnects = options.maxReconnects ?? Infinity;\n }\n\n /**\n * Returns whether the SSE connection is currently active.\n *\n * @returns `true` if EventSource is open and receiving events.\n */\n get isConnected(): boolean {\n return this.connected;\n }\n\n /**\n * Opens the SSE connection to the Go-Gin stream endpoint.\n * Safe to call multiple times — no-op if already connected.\n *\n * @example\n * manager.connect();\n */\n connect(): void {\n if (this.destroyed || this.connected || this.eventSource) return;\n this.openConnection();\n }\n\n /**\n * Closes the SSE connection and cancels any pending reconnect timers.\n *\n * @example\n * manager.disconnect();\n */\n disconnect(): void {\n this.destroyed = true;\n this.cleanup();\n }\n\n private openConnection(): void {\n if (this.destroyed) return;\n\n try {\n // Append API key as query parameter for SSE (EventSource doesn't support custom headers)\n const url = new URL(this.options.url);\n url.searchParams.set('apiKey', this.options.apiKey);\n\n this.eventSource = new EventSource(url.toString());\n\n this.eventSource.addEventListener('open', () => {\n this.connected = true;\n this.reconnectAttempt = 0;\n this.options.onStateChange?.(true);\n });\n\n this.eventSource.addEventListener('message', (evt: MessageEvent<string>) => {\n this.handleMessage(evt.data);\n });\n\n // Go-Gin sends events with event type 'message' by default\n // Also listen for explicitly typed events\n this.eventSource.addEventListener('flag_update', (evt: MessageEvent<string>) => {\n this.handleMessage(evt.data);\n });\n\n this.eventSource.addEventListener('error', () => {\n this.connected = false;\n this.options.onStateChange?.(false);\n this.cleanup();\n this.scheduleReconnect();\n });\n } catch {\n this.scheduleReconnect();\n }\n }\n\n private handleMessage(data: string): void {\n try {\n const event = JSON.parse(data) as FlagStreamEvent;\n this.options.onEvent(event);\n } catch {\n // Ignore malformed SSE payloads — never crash the host application\n }\n }\n\n private cleanup(): void {\n if (this.eventSource) {\n this.eventSource.close();\n this.eventSource = null;\n }\n if (this.reconnectTimer !== null) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n }\n\n private scheduleReconnect(): void {\n if (this.destroyed) return;\n if (this.reconnectAttempt >= this.maxReconnects) return;\n\n const delay = computeBackoffMs(this.reconnectAttempt, 1000, 30_000);\n this.reconnectAttempt++;\n\n this.reconnectTimer = setTimeout(() => {\n this.reconnectTimer = null;\n if (!this.destroyed) {\n this.openConnection();\n }\n }, delay);\n }\n}\n","/**\n * @fileoverview In-memory flag storage with LocalStorage sync and incognito fallback.\n * @module @nexus/sdk-flags/storage\n */\n\nimport type { FlagEvaluationResult } from '@nexussdk/contracts';\n\nconst STORAGE_KEY_PREFIX = 'nexus_flags_';\nconst ANON_ID_KEY = 'nexus_anon_id';\n\n/**\n * In-memory flag cache with optional LocalStorage persistence.\n * Falls back to pure in-memory storage in SSR / Incognito / Worker contexts.\n *\n * @example\n * const store = new FlagStorage('pk_live_abc123');\n * store.set('checkout_v2', { key: 'checkout_v2', enabled: true, variants: {}, reason: 'ROLLOUT_MATCH', version: 3 });\n * const result = store.get('checkout_v2');\n */\nexport class FlagStorage {\n private readonly memory = new Map<string, FlagEvaluationResult>();\n private readonly storageKey: string;\n private readonly localStorageAvailable: boolean;\n private broadcastChannel: BroadcastChannel | null = null;\n\n constructor(apiKeyPrefix: string) {\n this.storageKey = `${STORAGE_KEY_PREFIX}${apiKeyPrefix}`;\n this.localStorageAvailable = this.testLocalStorage();\n this.hydrate();\n this.setupBroadcastChannel();\n }\n\n /**\n * Stores a flag evaluation result in memory (and localStorage if available).\n *\n * @param key - Flag programmatic key.\n * @param result - Evaluation result to persist.\n */\n set(key: string, result: FlagEvaluationResult): void {\n this.memory.set(key, result);\n this.persist();\n this.broadcastUpdate(key, result);\n }\n\n /**\n * Retrieves a flag evaluation result by key.\n *\n * @param key - Flag programmatic key.\n * @returns The cached evaluation result or `undefined`.\n */\n get(key: string): FlagEvaluationResult | undefined {\n return this.memory.get(key);\n }\n\n /**\n * Bulk-sets multiple flag results (e.g. after a batch fetch).\n *\n * @param flags - Record of flag key → evaluation result.\n */\n setAll(flags: Record<string, FlagEvaluationResult>): void {\n for (const [key, value] of Object.entries(flags)) {\n this.memory.set(key, value);\n }\n this.persist();\n }\n\n /**\n * Removes a single flag from storage (e.g. on FLAG_DELETE SSE event).\n *\n * @param key - Flag programmatic key.\n */\n delete(key: string): void {\n this.memory.delete(key);\n this.persist();\n }\n\n /**\n * Returns all cached flag results.\n *\n * @returns All stored flag results as a record.\n */\n getAll(): Record<string, FlagEvaluationResult> {\n return Object.fromEntries(this.memory.entries());\n }\n\n /**\n * Clears all cached flags from memory and localStorage.\n */\n clear(): void {\n this.memory.clear();\n if (this.localStorageAvailable) {\n try {\n window.localStorage.removeItem(this.storageKey);\n } catch {\n // Storage access denied\n }\n }\n this.broadcastChannel?.close();\n }\n\n /**\n * Retrieves or creates a persistent anonymous user ID.\n *\n * @returns Anonymous ID string (e.g. \"anon_xyz123abc\").\n */\n getOrCreateAnonymousId(): string {\n if (typeof window === 'undefined') return 'anon-ssr-node';\n\n if (this.localStorageAvailable) {\n try {\n let id = window.localStorage.getItem(ANON_ID_KEY);\n if (!id) {\n id = `anon_${Math.random().toString(36).substring(2, 11)}`;\n window.localStorage.setItem(ANON_ID_KEY, id);\n }\n return id;\n } catch {\n // Incognito / locked storage\n }\n }\n\n return `anon_${Math.random().toString(36).substring(2, 11)}`;\n }\n\n private testLocalStorage(): boolean {\n try {\n if (typeof window === 'undefined') return false;\n const testKey = '__nexus_test__';\n window.localStorage.setItem(testKey, '1');\n window.localStorage.removeItem(testKey);\n return true;\n } catch {\n return false;\n }\n }\n\n private hydrate(): void {\n if (!this.localStorageAvailable) return;\n try {\n const raw = window.localStorage.getItem(this.storageKey);\n if (raw) {\n const parsed = JSON.parse(raw) as Record<string, FlagEvaluationResult>;\n for (const [key, value] of Object.entries(parsed)) {\n this.memory.set(key, value);\n }\n }\n } catch {\n // Corrupt storage — start fresh\n }\n }\n\n private persist(): void {\n if (!this.localStorageAvailable) return;\n try {\n window.localStorage.setItem(\n this.storageKey,\n JSON.stringify(Object.fromEntries(this.memory.entries())),\n );\n } catch {\n // Storage quota exceeded — in-memory only\n }\n }\n\n private setupBroadcastChannel(): void {\n try {\n if (typeof BroadcastChannel !== 'undefined') {\n this.broadcastChannel = new BroadcastChannel(`nexus_flags_${this.storageKey}`);\n this.broadcastChannel.onmessage = (evt: MessageEvent<{ key: string; result: FlagEvaluationResult }>) => {\n // Sync updates from other tabs into local memory cache\n if (evt.data?.key && evt.data?.result) {\n this.memory.set(evt.data.key, evt.data.result);\n }\n };\n }\n } catch {\n // BroadcastChannel unavailable (e.g. Worker context)\n }\n }\n\n private broadcastUpdate(key: string, result: FlagEvaluationResult): void {\n try {\n this.broadcastChannel?.postMessage({ key, result });\n } catch {\n // Broadcast failed — no-op\n }\n }\n}\n","/**\n * @fileoverview NexusFlagsClient — Full-featured feature flags SDK client.\n * In-memory evaluation, SSE real-time sync, ABAC targeting, and MurmurHash3 rollout.\n * @module @nexus/sdk-flags/client\n */\n\nimport type {\n FlagEvaluationResult,\n FlagVariants,\n FeatureFlag,\n UserContext,\n} from '@nexussdk/contracts';\nimport { resolveApiKey, resolveBaseUrl, fetchWithRetry } from '@nexussdk/core';\nimport { evaluateFlag } from './evaluator.js';\nimport { SSEManager } from './sse-manager.js';\nimport { FlagStorage } from './storage.js';\n\n/**\n * Options for initializing the NexusFlagsClient.\n *\n * @example\n * const client = new NexusFlagsClient({\n * apiKey: 'pk_live_...',\n * baseUrl: 'http://localhost:8080',\n * user: { id: 'usr_12345', country: 'VN' },\n * realtime: true,\n * });\n */\nexport interface NexusFlagsOptions {\n /**\n * Public API Key ('pk_live_...' or 'pk_test_...').\n * If omitted, resolved automatically via env variables.\n */\n apiKey?: string;\n /**\n * Base ingestion URL. Defaults to 'https://api.nexus.dev'.\n */\n baseUrl?: string;\n /**\n * Initial user identity context for targeting and percentage rollouts.\n */\n user?: UserContext;\n /**\n * Pre-hydrated flags evaluated on the server (SSR) to prevent client-side UI flicker.\n */\n bootstrap?: Record<string, FlagEvaluationResult>;\n /**\n * Toggle real-time SSE updates. Defaults to true.\n */\n realtime?: boolean;\n /**\n * Network timeout in milliseconds for evaluation fetch. Defaults to 3000ms.\n */\n timeoutMs?: number;\n}\n\n/**\n * Public interface for the NexusFlagsClient.\n */\nexport interface INexusFlagsClient {\n /**\n * Synchronously checks if a flag is active for the current user.\n *\n * @param key - Unique flag identifier.\n * @param defaultValue - Fallback returned if flag is absent or evaluating.\n * @returns `true` if the flag is enabled.\n *\n * @example\n * const showBanner = client.isEnabled('promo_banner_v2', false);\n */\n isEnabled(key: string, defaultValue?: boolean): boolean;\n\n /**\n * Synchronously retrieves a specific dynamic configuration variant.\n *\n * @param key - Unique flag identifier.\n * @param variantKey - Property inside the variant object.\n * @param defaultValue - Fallback returned if flag/variant is missing.\n * @returns The variant value cast to type T.\n *\n * @example\n * const rate = client.getVariant<number>('promo_banner_v2', 'discount_rate', 10);\n */\n getVariant<T = unknown>(key: string, variantKey: string, defaultValue?: T): T;\n\n /**\n * Updates current user context and re-evaluates all flags.\n *\n * @param user - New user context to apply.\n * @returns Promise that resolves after flags are refreshed.\n *\n * @example\n * await client.identify({ id: 'usr_99', country: 'SG' });\n */\n identify(user: UserContext): Promise<void>;\n\n /**\n * Resets user context to an anonymous persistent device identifier.\n *\n * @example\n * client.reset(); // called on logout\n */\n reset(): void;\n\n /**\n * Subscribes to runtime flag changes triggered by SSE updates.\n *\n * @param key - Flag key to observe.\n * @param callback - Called with new evaluation result when the flag changes.\n * @returns Unsubscribe function — call to remove the listener.\n *\n * @example\n * const unsub = client.onFlagChange('checkout_v2', (result) => {\n * setCheckoutEnabled(result.enabled);\n * });\n * // On component unmount:\n * unsub();\n */\n onFlagChange(key: string, callback: (result: FlagEvaluationResult) => void): () => void;\n\n /**\n * Gracefully shuts down active SSE connections and observers.\n *\n * @example\n * client.destroy();\n */\n destroy(): void;\n}\n\n/**\n * Feature Flags Client — the primary SDK entry point for flag evaluation.\n *\n * @implements {INexusFlagsClient}\n *\n * @example\n * const client = new NexusFlagsClient({ apiKey: 'pk_live_...' });\n * const enabled = client.isEnabled('new_checkout', false);\n */\nexport class NexusFlagsClient implements INexusFlagsClient {\n private readonly apiKey: string;\n private readonly baseUrl: string;\n private readonly timeoutMs: number;\n private user: UserContext;\n private readonly storage: FlagStorage;\n private readonly listeners = new Map<string, Set<(res: FlagEvaluationResult) => void>>();\n private sseManager?: SSEManager;\n private flagDefinitions = new Map<string, FeatureFlag>();\n\n constructor(options: NexusFlagsOptions = {}) {\n this.apiKey = resolveApiKey(options.apiKey);\n this.baseUrl = resolveBaseUrl(options.baseUrl);\n this.timeoutMs = options.timeoutMs ?? 3_000;\n this.storage = new FlagStorage(this.apiKey.substring(0, 16));\n this.user = options.user ?? { id: this.storage.getOrCreateAnonymousId() };\n\n // Hydrate bootstrap flags (SSR pre-evaluation)\n if (options.bootstrap) {\n this.storage.setAll(options.bootstrap);\n }\n\n if (options.realtime !== false) {\n this.initRealtimeSync();\n }\n\n // Prefetch flags asynchronously on init\n void this.refreshFlags();\n }\n\n /**\n * Evaluates whether a given feature flag is enabled for the current context.\n */\n public isEnabled(key: string, defaultValue = false): boolean {\n const cached = this.storage.get(key);\n return cached !== undefined ? cached.enabled : defaultValue;\n }\n\n /**\n * Retrieves a typed variant configuration for an active feature flag.\n */\n public getVariant<T = unknown>(key: string, variantKey: string, defaultValue?: T): T {\n const cached = this.storage.get(key);\n if (!cached?.enabled || !cached.variants) {\n return defaultValue as T;\n }\n const value = (cached.variants as FlagVariants)[variantKey];\n return value !== undefined ? (value as T) : (defaultValue as T);\n }\n\n /**\n * Identifies an authenticated user and triggers a flag evaluation refresh.\n */\n public async identify(user: UserContext): Promise<void> {\n this.user = { ...this.user, ...user };\n await this.refreshFlags();\n }\n\n /**\n * Resets the active user context to a new anonymous identifier.\n */\n public reset(): void {\n this.user = { id: this.storage.getOrCreateAnonymousId() };\n void this.refreshFlags();\n }\n\n /**\n * Registers a listener callback invoked when a flag's evaluation state changes.\n */\n public onFlagChange(key: string, callback: (result: FlagEvaluationResult) => void): () => void {\n if (!this.listeners.has(key)) {\n this.listeners.set(key, new Set());\n }\n this.listeners.get(key)!.add(callback);\n return () => this.listeners.get(key)?.delete(callback);\n }\n\n /**\n * Destroys the client, terminating SSE connections and clearing listeners.\n */\n public destroy(): void {\n this.sseManager?.disconnect();\n this.listeners.clear();\n this.storage.clear();\n }\n\n private initRealtimeSync(): void {\n this.sseManager = new SSEManager({\n url: `${this.baseUrl}/api/v1/flags/stream`,\n apiKey: this.apiKey,\n onEvent: (event) => {\n if (event.type === 'FLAG_UPDATE' && event.data) {\n // Use locally evaluated result if we have the flag definition\n const flagDef = this.flagDefinitions.get(event.key);\n const result = flagDef\n ? evaluateFlag(flagDef, this.user)\n : event.data;\n\n this.storage.set(event.key, result);\n this.listeners.get(event.key)?.forEach((cb) => cb(result));\n } else if (event.type === 'FLAG_DELETE') {\n this.storage.delete(event.key);\n this.flagDefinitions.delete(event.key);\n }\n },\n });\n this.sseManager.connect();\n }\n\n private async refreshFlags(): Promise<void> {\n try {\n const result = await fetchWithRetry<Record<string, FeatureFlag>>({\n url: `${this.baseUrl}/api/v1/flags/eval`,\n method: 'GET',\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n 'X-Nexus-User-Id': this.user.id ?? 'anon',\n 'X-Nexus-Country': this.user.country ?? '',\n },\n timeoutMs: this.timeoutMs,\n maxRetries: 2,\n });\n\n for (const [key, flag] of Object.entries(result.data)) {\n this.flagDefinitions.set(key, flag);\n const evaluated = evaluateFlag(flag, this.user);\n this.storage.set(key, evaluated);\n this.listeners.get(key)?.forEach((cb) => cb(evaluated));\n }\n } catch {\n // Offline fallback: retain existing in-memory cache silently\n // Never throw from a background refresh — host app must not be affected\n }\n }\n}\n"]}
|
package/dist/index.global.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../core/src/http-client.ts","../../core/src/env-resolver.ts","../src/evaluator.ts","../src/sse-manager.ts","../src/storage.ts","../src/client.ts"],"names":["computeBackoffMs","attempt","baseMs","maxMs","exponential","capped","sleep","ms","signal","resolve","reject","timer","isRetryableStatus","status","fetchWithRetry","options","url","method","headers","body","timeoutMs","maxRetries","retryBaseMs","retryMaxMs","externalSignal","lastError","controller","timeoutId","requestInit","response","delay","errorBody","err","readProcessEnv","key","proc","readViteEnv","meta","dynamicMeta","readWindowGlobal","resolveApiKey","directValue","resolved","resolveBaseUrl","defaultUrl","murmur3","seed","h","c1","c2","i","length4","k","k2","rem","computeRolloutBucket","userId","flagKey","hashInput","compareSemver","a","b","partsA","partsB","diff","resolveAttribute","ctx","attributePath","parts","current","part","evaluateSingleRule","rule","userCtx","userValue","targets","evaluateFlag","flag","SSEManager","evt","data","event","STORAGE_KEY_PREFIX","ANON_ID_KEY","FlagStorage","apiKeyPrefix","result","flags","value","id","testKey","raw","parsed","NexusFlagsClient","N","U","defaultValue","cached","variantKey","user","callback","flagDef","cb","A","evaluated"],"mappings":"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","file":"index.global.js","sourcesContent":["/**\n * @fileoverview Resilient HTTP client with exponential backoff and jitter.\n * Zero external dependencies — uses only native browser fetch and AbortController.\n * @module @nexus/core/http-client\n */\n\n/**\n * Options for configuring a single HTTP request with retry behaviour.\n *\n * @example\n * const opts: HttpClientOptions = {\n * url: 'https://api.nexus.dev/api/v1/flags/eval',\n * method: 'GET',\n * headers: { Authorization: 'Bearer pk_live_...' },\n * timeoutMs: 3000,\n * maxRetries: 3,\n * };\n */\nexport interface HttpClientOptions {\n /** Request URL. */\n url: string;\n /** HTTP method. Defaults to 'GET'. */\n method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n /** Request headers. */\n headers?: Record<string, string>;\n /** Request body (will be JSON-serialized if object). */\n body?: unknown;\n /** Request timeout in milliseconds. Defaults to 5000ms. */\n timeoutMs?: number;\n /** Maximum retry attempts on 5xx / network errors. Defaults to 3. */\n maxRetries?: number;\n /** Base delay in milliseconds for exponential backoff. Defaults to 1000ms. */\n retryBaseMs?: number;\n /** Maximum backoff delay in milliseconds. Defaults to 30000ms. */\n retryMaxMs?: number;\n /** Optional AbortSignal for external cancellation. */\n signal?: AbortSignal;\n}\n\n/**\n * Result of a successful HTTP fetch.\n *\n * @template T The expected response body type.\n */\nexport interface HttpClientResult<T> {\n /** Parsed response body. */\n data: T;\n /** HTTP status code. */\n status: number;\n /** Response headers. */\n headers: Headers;\n}\n\n/**\n * Computes the exponential backoff sleep duration with full random jitter.\n * Formula: sleep = random(0, min(maxMs, baseMs * 2^attempt))\n *\n * @param attempt - Zero-based retry attempt index.\n * @param baseMs - Base delay in milliseconds.\n * @param maxMs - Maximum delay cap in milliseconds.\n * @returns Sleep duration in milliseconds.\n *\n * @example\n * const delay = computeBackoffMs(2, 1000, 30000); // ~0-4000ms\n */\nexport function computeBackoffMs(attempt: number, baseMs = 1000, maxMs = 30_000): number {\n const exponential = baseMs * Math.pow(2, attempt);\n const capped = Math.min(maxMs, exponential);\n // Full jitter: random value in [0, capped]\n return Math.random() * capped;\n}\n\n/**\n * Sleeps for the given number of milliseconds.\n *\n * @param ms - Delay in milliseconds.\n * @param signal - Optional AbortSignal to cancel the sleep.\n * @returns Promise that resolves after delay, or rejects if aborted.\n */\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(new DOMException('Aborted', 'AbortError'));\n return;\n }\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener('abort', () => {\n clearTimeout(timer);\n reject(new DOMException('Aborted', 'AbortError'));\n });\n });\n}\n\n/**\n * Determines if an HTTP response status warrants a retry attempt.\n *\n * @param status - HTTP status code.\n * @returns `true` if the request should be retried.\n */\nfunction isRetryableStatus(status: number): boolean {\n return status >= 500 || status === 429;\n}\n\n/**\n * Performs a resilient HTTP fetch with exponential backoff and jitter.\n * Automatically retries on network failures and 5xx/429 responses.\n *\n * @template T The expected response body type.\n * @param options - Request configuration options.\n * @returns Promise resolving to {@link HttpClientResult}.\n * @throws {Error} When all retry attempts are exhausted or request is aborted.\n *\n * @example\n * const result = await fetchWithRetry<BatchFlagEvaluation>({\n * url: 'https://api.nexus.dev/api/v1/flags/eval',\n * method: 'GET',\n * headers: { Authorization: 'Bearer pk_live_...' },\n * timeoutMs: 3000,\n * maxRetries: 3,\n * });\n * console.log(result.data); // { checkout_v2: { enabled: true, ... } }\n */\nexport async function fetchWithRetry<T = unknown>(\n options: HttpClientOptions,\n): Promise<HttpClientResult<T>> {\n const {\n url,\n method = 'GET',\n headers = {},\n body,\n timeoutMs = 5_000,\n maxRetries = 3,\n retryBaseMs = 1_000,\n retryMaxMs = 30_000,\n signal: externalSignal,\n } = options;\n\n let lastError: Error = new Error('Request failed');\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n // Abort if external signal is already triggered\n if (externalSignal?.aborted) {\n throw new DOMException('Request aborted by caller.', 'AbortError');\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeoutMs);\n\n // Merge external abort signal\n externalSignal?.addEventListener('abort', () => controller.abort());\n\n try {\n const requestInit: RequestInit = {\n method,\n headers: {\n 'Content-Type': 'application/json',\n ...headers,\n },\n signal: controller.signal,\n };\n\n if (body !== undefined) {\n requestInit.body = typeof body === 'string' ? body : JSON.stringify(body);\n }\n\n const response = await fetch(url, requestInit);\n clearTimeout(timeoutId);\n\n if (response.ok) {\n const data = (await response.json()) as T;\n return { data, status: response.status, headers: response.headers };\n }\n\n // Non-ok response: check if retryable\n if (isRetryableStatus(response.status) && attempt < maxRetries) {\n lastError = new Error(`HTTP ${response.status}: ${response.statusText}`);\n const delay = computeBackoffMs(attempt, retryBaseMs, retryMaxMs);\n await sleep(delay, externalSignal);\n continue;\n }\n\n // Non-retryable error (400, 401, 403, 404, etc.)\n const errorBody = await response.text().catch(() => '');\n throw new Error(`HTTP ${response.status}: ${errorBody}`);\n } catch (err) {\n clearTimeout(timeoutId);\n\n if (err instanceof DOMException && err.name === 'AbortError') {\n throw err; // Propagate abort without retry\n }\n\n lastError = err instanceof Error ? err : new Error(String(err));\n\n if (attempt < maxRetries) {\n const delay = computeBackoffMs(attempt, retryBaseMs, retryMaxMs);\n await sleep(delay, externalSignal);\n }\n }\n }\n\n throw lastError;\n}\n","/**\n * @fileoverview Universal API key resolver across all JavaScript runtimes.\n * Supports: Browser globals, Node.js process.env, Vite, Next.js, and Nuxt.\n * @module @nexus/core/env-resolver\n */\n\n/**\n * Resolution priority for API key lookup.\n * 1. Direct parameter passed into init options\n * 2. Browser global: window.__NEXUS_API_KEY__\n * 3. Node/Next.js: process.env.NEXUS_API_KEY\n * 4. Next.js client bundler: process.env.NEXT_PUBLIC_NEXUS_API_KEY\n * 5. Vite bundler: import.meta.env.VITE_NEXUS_API_KEY\n * 6. Nuxt: process.env.NUXT_PUBLIC_NEXUS_API_KEY\n */\n\n// Augment global types for browser injection\ndeclare global {\n interface Window {\n /** Browser-injected API key for Nexus SDK. */\n __NEXUS_API_KEY__?: string;\n }\n}\n\n/**\n * Safely reads an environment variable from process.env without throwing.\n *\n * @param key - The environment variable name.\n * @returns The value or `undefined`.\n */\nfunction readProcessEnv(key: string): string | undefined {\n try {\n const proc = (globalThis as unknown as { process?: { env?: Record<string, string | undefined> } }).process;\n if (typeof proc !== 'undefined' && proc && proc.env) {\n return proc.env[key] ?? undefined;\n }\n } catch {\n // process is not defined in pure browser environments\n }\n return undefined;\n}\n\n/**\n * Safely reads a Vite environment variable from import.meta.env.\n *\n * @param key - The Vite env variable name (VITE_* prefix required by Vite).\n * @returns The value or `undefined`.\n */\nfunction readViteEnv(key: string): string | undefined {\n try {\n const meta = typeof globalThis !== 'undefined' && (globalThis as unknown as { __import_meta__?: { env?: Record<string, string | undefined> } }).__import_meta__;\n if (meta && meta.env) {\n return meta.env[key] ?? undefined;\n }\n // Safe dynamic evaluation avoiding CJS compile-time import.meta error\n const dynamicMeta = new Function('try { return import.meta; } catch(e) { return undefined; }')() as { env?: Record<string, string | undefined> } | undefined;\n if (dynamicMeta && dynamicMeta.env) {\n return dynamicMeta.env[key] ?? undefined;\n }\n } catch {\n // Not a Vite runtime\n }\n return undefined;\n}\n\n/**\n * Safely reads the browser window global injection.\n *\n * @returns The window-injected API key or `undefined`.\n */\nfunction readWindowGlobal(): string | undefined {\n try {\n if (typeof window !== 'undefined' && window.__NEXUS_API_KEY__) {\n return window.__NEXUS_API_KEY__;\n }\n } catch {\n // window is not accessible (SSR/Worker context)\n }\n return undefined;\n}\n\n/**\n * Resolves the Nexus API key from the environment using a prioritized lookup chain.\n *\n * Resolution order:\n * 1. `directValue` — Parameter passed directly into SDK init options\n * 2. `window.__NEXUS_API_KEY__` — Browser global injection (CDN/script embed use cases)\n * 3. `process.env.NEXUS_API_KEY` — Node.js / Docker / CI environments\n * 4. `process.env.NEXT_PUBLIC_NEXUS_API_KEY` — Next.js client-side bundling\n * 5. `import.meta.env.VITE_NEXUS_API_KEY` — Vite / Vitest bundling\n * 6. `process.env.NUXT_PUBLIC_NEXUS_API_KEY` — Nuxt 3 public runtime config\n *\n * @param directValue - Explicitly provided API key (highest priority).\n * @returns The resolved API key string.\n * @throws {Error} If no API key can be resolved from any source.\n *\n * @example\n * // In a Next.js application:\n * // process.env.NEXT_PUBLIC_NEXUS_API_KEY = 'pk_live_abc123'\n * const key = resolveApiKey(); // 'pk_live_abc123'\n *\n * @example\n * // Passing directly (overrides all env vars):\n * const key = resolveApiKey('pk_live_directkey'); // 'pk_live_directkey'\n */\nexport function resolveApiKey(directValue?: string): string {\n const resolved =\n directValue ||\n readWindowGlobal() ||\n readProcessEnv('NEXUS_API_KEY') ||\n readProcessEnv('NEXT_PUBLIC_NEXUS_API_KEY') ||\n readViteEnv('VITE_NEXUS_API_KEY') ||\n readProcessEnv('NUXT_PUBLIC_NEXUS_API_KEY');\n\n if (!resolved || resolved.trim() === '') {\n throw new Error(\n '[Nexus SDK] No API key found. ' +\n 'Please provide one via:\\n' +\n ' 1. Nexus.init({ apiKey: \"pk_live_...\" })\\n' +\n ' 2. window.__NEXUS_API_KEY__ = \"pk_live_...\"\\n' +\n ' 3. NEXUS_API_KEY env var\\n' +\n ' 4. NEXT_PUBLIC_NEXUS_API_KEY (Next.js)\\n' +\n ' 5. VITE_NEXUS_API_KEY (Vite)\\n' +\n ' 6. NUXT_PUBLIC_NEXUS_API_KEY (Nuxt)',\n );\n }\n\n return resolved.trim();\n}\n\n/**\n * Resolves the Nexus base URL from configuration or environment variables.\n *\n * @param directValue - Explicitly provided base URL.\n * @param defaultUrl - Default URL if no env var is found.\n * @returns The resolved base URL string (trailing slash stripped).\n *\n * @example\n * // Resolves to http://localhost:8080 in development\n * const url = resolveBaseUrl(undefined, 'https://api.nexus.dev');\n */\nexport function resolveBaseUrl(\n directValue?: string,\n defaultUrl = 'https://api.nexus.dev',\n): string {\n const resolved =\n directValue ||\n readProcessEnv('NEXT_PUBLIC_NEXUS_URL') ||\n readViteEnv('VITE_NEXUS_URL') ||\n defaultUrl;\n\n return resolved.replace(/\\/$/, '');\n}\n","/**\n * @fileoverview MurmurHash3 32-bit implementation and ABAC rule evaluator.\n * Pure TypeScript — no external dependencies. Implements deterministic rollout bucketing.\n * @module @nexus/sdk-flags/evaluator\n */\n\nimport type { FeatureFlag, FlagEvaluationResult, TargetingRule, UserContext } from '@nexussdk/contracts';\n\n// ---------------------------------------------------------------------------\n// MurmurHash3 32-bit Implementation (Pure TypeScript)\n// Used for deterministic user-to-bucket mapping without server round-trips.\n// ---------------------------------------------------------------------------\n\n/**\n * Converts a string to a Uint32 MurmurHash3 hash.\n * Implements the MurmurHash3 32-bit algorithm (x86 variant).\n *\n * @param key - Input string to hash.\n * @param seed - Optional seed value. Defaults to 0.\n * @returns Unsigned 32-bit integer hash value.\n *\n * @example\n * const hash = murmur3('user123:checkout_v2', 0);\n * const bucket = hash % 100; // 0-99 deterministic bucket\n */\nexport function murmur3(key: string, seed = 0): number {\n let h = seed >>> 0;\n const c1 = 0xcc9e2d51;\n const c2 = 0x1b873593;\n\n let i = 0;\n // Process 4-byte chunks\n const length4 = Math.floor(key.length / 4) * 4;\n while (i < length4) {\n let k =\n ((key.charCodeAt(i) & 0xff)) |\n ((key.charCodeAt(i + 1) & 0xff) << 8) |\n ((key.charCodeAt(i + 2) & 0xff) << 16) |\n ((key.charCodeAt(i + 3) & 0xff) << 24);\n\n k = Math.imul(k, c1);\n k = (k << 15) | (k >>> 17);\n k = Math.imul(k, c2);\n\n h ^= k;\n h = (h << 13) | (h >>> 19);\n h = (Math.imul(h, 5) + 0xe6546b64) >>> 0;\n i += 4;\n }\n\n // Process remaining bytes\n let k2 = 0;\n const rem = key.length & 3;\n if (rem >= 3) k2 ^= (key.charCodeAt(i + 2) & 0xff) << 16;\n if (rem >= 2) k2 ^= (key.charCodeAt(i + 1) & 0xff) << 8;\n if (rem >= 1) {\n k2 ^= key.charCodeAt(i) & 0xff;\n k2 = Math.imul(k2, c1);\n k2 = (k2 << 15) | (k2 >>> 17);\n k2 = Math.imul(k2, c2);\n h ^= k2;\n }\n\n // Finalization mix\n h ^= key.length;\n h ^= h >>> 16;\n h = Math.imul(h, 0x85ebca6b);\n h ^= h >>> 13;\n h = Math.imul(h, 0xc2b2ae35);\n h ^= h >>> 16;\n\n return h >>> 0; // Ensure unsigned\n}\n\n/**\n * Computes the deterministic rollout bucket (0-99) for a given user+flag combination.\n * Uses MurmurHash3 for even distribution without server state.\n *\n * @param userId - User's unique identifier (anonymous ID if not authenticated).\n * @param flagKey - Flag programmatic key.\n * @returns Bucket value between 0 and 99 (inclusive).\n *\n * @example\n * const bucket = computeRolloutBucket('usr_12345', 'checkout_v2');\n * // isEnabled = bucket < flag.rolloutPercentage\n */\nexport function computeRolloutBucket(userId: string, flagKey: string): number {\n const hashInput = `${userId}:${flagKey}`;\n return murmur3(hashInput) % 100;\n}\n\n// ---------------------------------------------------------------------------\n// ABAC Rule Evaluator\n// ---------------------------------------------------------------------------\n\n/**\n * Performs a simple semantic version comparison.\n * Compares two semver strings in \"MAJOR.MINOR.PATCH\" format.\n *\n * @param a - First version string.\n * @param b - Second version string.\n * @returns Positive if a > b, negative if a < b, 0 if equal.\n */\nfunction compareSemver(a: string, b: string): number {\n const partsA = a.replace(/^v/, '').split('.').map(Number);\n const partsB = b.replace(/^v/, '').split('.').map(Number);\n for (let i = 0; i < 3; i++) {\n const diff = (partsA[i] ?? 0) - (partsB[i] ?? 0);\n if (diff !== 0) return diff;\n }\n return 0;\n}\n\n/**\n * Extracts a nested attribute value from the UserContext by dot-notation path.\n *\n * @param ctx - UserContext object.\n * @param attributePath - Dot-separated path (e.g. \"custom.tier\", \"country\").\n * @returns The attribute value or `undefined` if not found.\n */\nfunction resolveAttribute(ctx: UserContext, attributePath: string): unknown {\n const parts = attributePath.split('.');\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let current: any = ctx;\n for (const part of parts) {\n if (current == null || typeof current !== 'object') return undefined;\n current = current[part];\n }\n return current;\n}\n\n/**\n * Evaluates a single targeting rule against the user context.\n *\n * @param rule - The ABAC targeting rule to evaluate.\n * @param userCtx - The current user context.\n * @returns `true` if the rule passes for this user.\n */\nfunction evaluateSingleRule(rule: TargetingRule, userCtx: UserContext): boolean {\n const userValue = resolveAttribute(userCtx, rule.attribute);\n const targets = rule.values;\n\n switch (rule.operator) {\n case 'EQUALS':\n return userValue === targets[0];\n case 'NOT_EQUALS':\n return userValue !== targets[0];\n case 'IN':\n return targets.includes(userValue as string | number | boolean);\n case 'NOT_IN':\n return !targets.includes(userValue as string | number | boolean);\n case 'CONTAINS':\n return typeof userValue === 'string' && userValue.includes(String(targets[0]));\n case 'NOT_CONTAINS':\n return typeof userValue === 'string' && !userValue.includes(String(targets[0]));\n case 'STARTS_WITH':\n return typeof userValue === 'string' && userValue.startsWith(String(targets[0]));\n case 'ENDS_WITH':\n return typeof userValue === 'string' && userValue.endsWith(String(targets[0]));\n case 'GREATER_THAN':\n return typeof userValue === 'number' && userValue > Number(targets[0]);\n case 'LESS_THAN':\n return typeof userValue === 'number' && userValue < Number(targets[0]);\n case 'SEMVER_GTE':\n return (\n typeof userValue === 'string' &&\n typeof targets[0] === 'string' &&\n compareSemver(userValue, String(targets[0])) >= 0\n );\n case 'SEMVER_LTE':\n return (\n typeof userValue === 'string' &&\n typeof targets[0] === 'string' &&\n compareSemver(userValue, String(targets[0])) <= 0\n );\n default:\n return false;\n }\n}\n\n/**\n * Evaluates a FeatureFlag against a UserContext using the full ABAC + rollout engine.\n *\n * Evaluation Order:\n * 1. Kill-switch check (`isEnabled === false` → KILL_SWITCH)\n * 2. All targeting rules must pass (ABAC evaluation)\n * 3. Percentage rollout via MurmurHash3 bucketing\n * 4. Default enabled state\n *\n * @param flag - The feature flag definition from the cache.\n * @param userCtx - Current user context for targeting evaluation.\n * @returns Full {@link FlagEvaluationResult} with reason explanation.\n *\n * @example\n * const result = evaluateFlag(flag, { id: 'usr_12345', country: 'VN' });\n * if (result.enabled) {\n * console.log(result.variants.discount_rate); // 20\n * }\n */\nexport function evaluateFlag(flag: FeatureFlag, userCtx: UserContext): FlagEvaluationResult {\n // Step 1: Kill-switch\n if (!flag.isEnabled) {\n return {\n key: flag.key,\n enabled: false,\n variants: {},\n reason: 'KILL_SWITCH',\n version: flag.version,\n };\n }\n\n // Step 2: ABAC targeting rules — all must pass (AND logic)\n if (flag.targetingRules.length > 0) {\n const allRulesPass = flag.targetingRules.every((rule) =>\n evaluateSingleRule(rule, userCtx),\n );\n if (!allRulesPass) {\n return {\n key: flag.key,\n enabled: false,\n variants: {},\n reason: 'FALLBACK',\n version: flag.version,\n };\n }\n // All targeting rules matched\n if (flag.rolloutPercentage >= 100) {\n return {\n key: flag.key,\n enabled: true,\n variants: flag.variants,\n reason: 'TARGETING_MATCH',\n version: flag.version,\n };\n }\n }\n\n // Step 3: Percentage rollout (MurmurHash3)\n if (flag.rolloutPercentage > 0) {\n const userId = userCtx.id ?? 'anon';\n const bucket = computeRolloutBucket(userId, flag.key);\n if (bucket < flag.rolloutPercentage) {\n return {\n key: flag.key,\n enabled: true,\n variants: flag.variants,\n reason: flag.targetingRules.length > 0 ? 'TARGETING_MATCH' : 'ROLLOUT_MATCH',\n version: flag.version,\n };\n }\n return {\n key: flag.key,\n enabled: false,\n variants: {},\n reason: 'FALLBACK',\n version: flag.version,\n };\n }\n\n // Step 4: Full kill-switch off (rolloutPercentage === 0 with no rules)\n return {\n key: flag.key,\n enabled: false,\n variants: {},\n reason: 'KILL_SWITCH',\n version: flag.version,\n };\n}\n","/**\n * @fileoverview Singleton SSE Manager with ref-counting and auto-reconnect.\n * Ensures exactly one EventSource connection per API key across all flag subscribers.\n * @module @nexus/sdk-flags/sse-manager\n */\n\nimport type { FlagStreamEvent } from '@nexussdk/contracts';\nimport { computeBackoffMs } from '@nexussdk/core';\n\n/**\n * Options for creating an SSE connection.\n */\nexport interface SSEManagerOptions {\n /** Full SSE stream URL (e.g. \"http://localhost:8080/api/v1/flags/stream\"). */\n url: string;\n /** Public API key for authentication. */\n apiKey: string;\n /** Callback invoked for each received flag stream event. */\n onEvent: (event: FlagStreamEvent) => void;\n /** Callback invoked on connection state changes (optional). */\n onStateChange?: (connected: boolean) => void;\n /** Maximum reconnect attempts. Defaults to Infinity. */\n maxReconnects?: number;\n}\n\n/**\n * Ref-counted Singleton SSE Manager.\n *\n * Multiple consumers sharing the same `apiKey` receive updates\n * from a single shared SSE connection. Connection is torn down when all\n * consumers have called `disconnect()`.\n *\n * Connection lifecycle:\n * 1. `connect()` → opens EventSource or streaming fetch\n * 2. On disconnect: exponential backoff reconnect (max 30s)\n * 3. `disconnect()` → decrements ref count; tears down on 0\n *\n * @example\n * const manager = new SSEManager({\n * url: 'http://localhost:8080/api/v1/flags/stream',\n * apiKey: 'pk_live_...',\n * onEvent: (event) => updateCache(event),\n * });\n * manager.connect();\n * // Later:\n * manager.disconnect();\n */\nexport class SSEManager {\n private readonly options: SSEManagerOptions;\n private eventSource: EventSource | null = null;\n private connected = false;\n private destroyed = false;\n private reconnectAttempt = 0;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private readonly maxReconnects: number;\n\n constructor(options: SSEManagerOptions) {\n this.options = options;\n this.maxReconnects = options.maxReconnects ?? Infinity;\n }\n\n /**\n * Returns whether the SSE connection is currently active.\n *\n * @returns `true` if EventSource is open and receiving events.\n */\n get isConnected(): boolean {\n return this.connected;\n }\n\n /**\n * Opens the SSE connection to the Go-Gin stream endpoint.\n * Safe to call multiple times — no-op if already connected.\n *\n * @example\n * manager.connect();\n */\n connect(): void {\n if (this.destroyed || this.connected || this.eventSource) return;\n this.openConnection();\n }\n\n /**\n * Closes the SSE connection and cancels any pending reconnect timers.\n *\n * @example\n * manager.disconnect();\n */\n disconnect(): void {\n this.destroyed = true;\n this.cleanup();\n }\n\n private openConnection(): void {\n if (this.destroyed) return;\n\n try {\n // Append API key as query parameter for SSE (EventSource doesn't support custom headers)\n const url = new URL(this.options.url);\n url.searchParams.set('apiKey', this.options.apiKey);\n\n this.eventSource = new EventSource(url.toString());\n\n this.eventSource.addEventListener('open', () => {\n this.connected = true;\n this.reconnectAttempt = 0;\n this.options.onStateChange?.(true);\n });\n\n this.eventSource.addEventListener('message', (evt: MessageEvent<string>) => {\n this.handleMessage(evt.data);\n });\n\n // Go-Gin sends events with event type 'message' by default\n // Also listen for explicitly typed events\n this.eventSource.addEventListener('flag_update', (evt: MessageEvent<string>) => {\n this.handleMessage(evt.data);\n });\n\n this.eventSource.addEventListener('error', () => {\n this.connected = false;\n this.options.onStateChange?.(false);\n this.cleanup();\n this.scheduleReconnect();\n });\n } catch {\n this.scheduleReconnect();\n }\n }\n\n private handleMessage(data: string): void {\n try {\n const event = JSON.parse(data) as FlagStreamEvent;\n this.options.onEvent(event);\n } catch {\n // Ignore malformed SSE payloads — never crash the host application\n }\n }\n\n private cleanup(): void {\n if (this.eventSource) {\n this.eventSource.close();\n this.eventSource = null;\n }\n if (this.reconnectTimer !== null) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n }\n\n private scheduleReconnect(): void {\n if (this.destroyed) return;\n if (this.reconnectAttempt >= this.maxReconnects) return;\n\n const delay = computeBackoffMs(this.reconnectAttempt, 1000, 30_000);\n this.reconnectAttempt++;\n\n this.reconnectTimer = setTimeout(() => {\n this.reconnectTimer = null;\n if (!this.destroyed) {\n this.openConnection();\n }\n }, delay);\n }\n}\n","/**\n * @fileoverview In-memory flag storage with LocalStorage sync and incognito fallback.\n * @module @nexus/sdk-flags/storage\n */\n\nimport type { FlagEvaluationResult } from '@nexussdk/contracts';\n\nconst STORAGE_KEY_PREFIX = 'nexus_flags_';\nconst ANON_ID_KEY = 'nexus_anon_id';\n\n/**\n * In-memory flag cache with optional LocalStorage persistence.\n * Falls back to pure in-memory storage in SSR / Incognito / Worker contexts.\n *\n * @example\n * const store = new FlagStorage('pk_live_abc123');\n * store.set('checkout_v2', { key: 'checkout_v2', enabled: true, variants: {}, reason: 'ROLLOUT_MATCH', version: 3 });\n * const result = store.get('checkout_v2');\n */\nexport class FlagStorage {\n private readonly memory = new Map<string, FlagEvaluationResult>();\n private readonly storageKey: string;\n private readonly localStorageAvailable: boolean;\n private broadcastChannel: BroadcastChannel | null = null;\n\n constructor(apiKeyPrefix: string) {\n this.storageKey = `${STORAGE_KEY_PREFIX}${apiKeyPrefix}`;\n this.localStorageAvailable = this.testLocalStorage();\n this.hydrate();\n this.setupBroadcastChannel();\n }\n\n /**\n * Stores a flag evaluation result in memory (and localStorage if available).\n *\n * @param key - Flag programmatic key.\n * @param result - Evaluation result to persist.\n */\n set(key: string, result: FlagEvaluationResult): void {\n this.memory.set(key, result);\n this.persist();\n this.broadcastUpdate(key, result);\n }\n\n /**\n * Retrieves a flag evaluation result by key.\n *\n * @param key - Flag programmatic key.\n * @returns The cached evaluation result or `undefined`.\n */\n get(key: string): FlagEvaluationResult | undefined {\n return this.memory.get(key);\n }\n\n /**\n * Bulk-sets multiple flag results (e.g. after a batch fetch).\n *\n * @param flags - Record of flag key → evaluation result.\n */\n setAll(flags: Record<string, FlagEvaluationResult>): void {\n for (const [key, value] of Object.entries(flags)) {\n this.memory.set(key, value);\n }\n this.persist();\n }\n\n /**\n * Removes a single flag from storage (e.g. on FLAG_DELETE SSE event).\n *\n * @param key - Flag programmatic key.\n */\n delete(key: string): void {\n this.memory.delete(key);\n this.persist();\n }\n\n /**\n * Returns all cached flag results.\n *\n * @returns All stored flag results as a record.\n */\n getAll(): Record<string, FlagEvaluationResult> {\n return Object.fromEntries(this.memory.entries());\n }\n\n /**\n * Clears all cached flags from memory and localStorage.\n */\n clear(): void {\n this.memory.clear();\n if (this.localStorageAvailable) {\n try {\n window.localStorage.removeItem(this.storageKey);\n } catch {\n // Storage access denied\n }\n }\n this.broadcastChannel?.close();\n }\n\n /**\n * Retrieves or creates a persistent anonymous user ID.\n *\n * @returns Anonymous ID string (e.g. \"anon_xyz123abc\").\n */\n getOrCreateAnonymousId(): string {\n if (typeof window === 'undefined') return 'anon-ssr-node';\n\n if (this.localStorageAvailable) {\n try {\n let id = window.localStorage.getItem(ANON_ID_KEY);\n if (!id) {\n id = `anon_${Math.random().toString(36).substring(2, 11)}`;\n window.localStorage.setItem(ANON_ID_KEY, id);\n }\n return id;\n } catch {\n // Incognito / locked storage\n }\n }\n\n return `anon_${Math.random().toString(36).substring(2, 11)}`;\n }\n\n private testLocalStorage(): boolean {\n try {\n if (typeof window === 'undefined') return false;\n const testKey = '__nexus_test__';\n window.localStorage.setItem(testKey, '1');\n window.localStorage.removeItem(testKey);\n return true;\n } catch {\n return false;\n }\n }\n\n private hydrate(): void {\n if (!this.localStorageAvailable) return;\n try {\n const raw = window.localStorage.getItem(this.storageKey);\n if (raw) {\n const parsed = JSON.parse(raw) as Record<string, FlagEvaluationResult>;\n for (const [key, value] of Object.entries(parsed)) {\n this.memory.set(key, value);\n }\n }\n } catch {\n // Corrupt storage — start fresh\n }\n }\n\n private persist(): void {\n if (!this.localStorageAvailable) return;\n try {\n window.localStorage.setItem(\n this.storageKey,\n JSON.stringify(Object.fromEntries(this.memory.entries())),\n );\n } catch {\n // Storage quota exceeded — in-memory only\n }\n }\n\n private setupBroadcastChannel(): void {\n try {\n if (typeof BroadcastChannel !== 'undefined') {\n this.broadcastChannel = new BroadcastChannel(`nexus_flags_${this.storageKey}`);\n this.broadcastChannel.onmessage = (evt: MessageEvent<{ key: string; result: FlagEvaluationResult }>) => {\n // Sync updates from other tabs into local memory cache\n if (evt.data?.key && evt.data?.result) {\n this.memory.set(evt.data.key, evt.data.result);\n }\n };\n }\n } catch {\n // BroadcastChannel unavailable (e.g. Worker context)\n }\n }\n\n private broadcastUpdate(key: string, result: FlagEvaluationResult): void {\n try {\n this.broadcastChannel?.postMessage({ key, result });\n } catch {\n // Broadcast failed — no-op\n }\n }\n}\n","/**\n * @fileoverview NexusFlagsClient — Full-featured feature flags SDK client.\n * In-memory evaluation, SSE real-time sync, ABAC targeting, and MurmurHash3 rollout.\n * @module @nexus/sdk-flags/client\n */\n\nimport type {\n FlagEvaluationResult,\n FlagVariants,\n FeatureFlag,\n UserContext,\n} from '@nexussdk/contracts';\nimport { resolveApiKey, resolveBaseUrl, fetchWithRetry } from '@nexussdk/core';\nimport { evaluateFlag } from './evaluator.js';\nimport { SSEManager } from './sse-manager.js';\nimport { FlagStorage } from './storage.js';\n\n/**\n * Options for initializing the NexusFlagsClient.\n *\n * @example\n * const client = new NexusFlagsClient({\n * apiKey: 'pk_live_...',\n * baseUrl: 'http://localhost:8080',\n * user: { id: 'usr_12345', country: 'VN' },\n * realtime: true,\n * });\n */\nexport interface NexusFlagsOptions {\n /**\n * Public API Key ('pk_live_...' or 'pk_test_...').\n * If omitted, resolved automatically via env variables.\n */\n apiKey?: string;\n /**\n * Base ingestion URL. Defaults to 'https://api.nexus.dev'.\n */\n baseUrl?: string;\n /**\n * Initial user identity context for targeting and percentage rollouts.\n */\n user?: UserContext;\n /**\n * Pre-hydrated flags evaluated on the server (SSR) to prevent client-side UI flicker.\n */\n bootstrap?: Record<string, FlagEvaluationResult>;\n /**\n * Toggle real-time SSE updates. Defaults to true.\n */\n realtime?: boolean;\n /**\n * Network timeout in milliseconds for evaluation fetch. Defaults to 3000ms.\n */\n timeoutMs?: number;\n}\n\n/**\n * Public interface for the NexusFlagsClient.\n */\nexport interface INexusFlagsClient {\n /**\n * Synchronously checks if a flag is active for the current user.\n *\n * @param key - Unique flag identifier.\n * @param defaultValue - Fallback returned if flag is absent or evaluating.\n * @returns `true` if the flag is enabled.\n *\n * @example\n * const showBanner = client.isEnabled('promo_banner_v2', false);\n */\n isEnabled(key: string, defaultValue?: boolean): boolean;\n\n /**\n * Synchronously retrieves a specific dynamic configuration variant.\n *\n * @param key - Unique flag identifier.\n * @param variantKey - Property inside the variant object.\n * @param defaultValue - Fallback returned if flag/variant is missing.\n * @returns The variant value cast to type T.\n *\n * @example\n * const rate = client.getVariant<number>('promo_banner_v2', 'discount_rate', 10);\n */\n getVariant<T = unknown>(key: string, variantKey: string, defaultValue?: T): T;\n\n /**\n * Updates current user context and re-evaluates all flags.\n *\n * @param user - New user context to apply.\n * @returns Promise that resolves after flags are refreshed.\n *\n * @example\n * await client.identify({ id: 'usr_99', country: 'SG' });\n */\n identify(user: UserContext): Promise<void>;\n\n /**\n * Resets user context to an anonymous persistent device identifier.\n *\n * @example\n * client.reset(); // called on logout\n */\n reset(): void;\n\n /**\n * Subscribes to runtime flag changes triggered by SSE updates.\n *\n * @param key - Flag key to observe.\n * @param callback - Called with new evaluation result when the flag changes.\n * @returns Unsubscribe function — call to remove the listener.\n *\n * @example\n * const unsub = client.onFlagChange('checkout_v2', (result) => {\n * setCheckoutEnabled(result.enabled);\n * });\n * // On component unmount:\n * unsub();\n */\n onFlagChange(key: string, callback: (result: FlagEvaluationResult) => void): () => void;\n\n /**\n * Gracefully shuts down active SSE connections and observers.\n *\n * @example\n * client.destroy();\n */\n destroy(): void;\n}\n\n/**\n * Feature Flags Client — the primary SDK entry point for flag evaluation.\n *\n * @implements {INexusFlagsClient}\n *\n * @example\n * const client = new NexusFlagsClient({ apiKey: 'pk_live_...' });\n * const enabled = client.isEnabled('new_checkout', false);\n */\nexport class NexusFlagsClient implements INexusFlagsClient {\n private readonly apiKey: string;\n private readonly baseUrl: string;\n private readonly timeoutMs: number;\n private user: UserContext;\n private readonly storage: FlagStorage;\n private readonly listeners = new Map<string, Set<(res: FlagEvaluationResult) => void>>();\n private sseManager?: SSEManager;\n private flagDefinitions = new Map<string, FeatureFlag>();\n\n constructor(options: NexusFlagsOptions = {}) {\n this.apiKey = resolveApiKey(options.apiKey);\n this.baseUrl = resolveBaseUrl(options.baseUrl);\n this.timeoutMs = options.timeoutMs ?? 3_000;\n this.storage = new FlagStorage(this.apiKey.substring(0, 16));\n this.user = options.user ?? { id: this.storage.getOrCreateAnonymousId() };\n\n // Hydrate bootstrap flags (SSR pre-evaluation)\n if (options.bootstrap) {\n this.storage.setAll(options.bootstrap);\n }\n\n if (options.realtime !== false) {\n this.initRealtimeSync();\n }\n\n // Prefetch flags asynchronously on init\n void this.refreshFlags();\n }\n\n /**\n * Evaluates whether a given feature flag is enabled for the current context.\n */\n public isEnabled(key: string, defaultValue = false): boolean {\n const cached = this.storage.get(key);\n return cached !== undefined ? cached.enabled : defaultValue;\n }\n\n /**\n * Retrieves a typed variant configuration for an active feature flag.\n */\n public getVariant<T = unknown>(key: string, variantKey: string, defaultValue?: T): T {\n const cached = this.storage.get(key);\n if (!cached?.enabled || !cached.variants) {\n return defaultValue as T;\n }\n const value = (cached.variants as FlagVariants)[variantKey];\n return value !== undefined ? (value as T) : (defaultValue as T);\n }\n\n /**\n * Identifies an authenticated user and triggers a flag evaluation refresh.\n */\n public async identify(user: UserContext): Promise<void> {\n this.user = { ...this.user, ...user };\n await this.refreshFlags();\n }\n\n /**\n * Resets the active user context to a new anonymous identifier.\n */\n public reset(): void {\n this.user = { id: this.storage.getOrCreateAnonymousId() };\n void this.refreshFlags();\n }\n\n /**\n * Registers a listener callback invoked when a flag's evaluation state changes.\n */\n public onFlagChange(key: string, callback: (result: FlagEvaluationResult) => void): () => void {\n if (!this.listeners.has(key)) {\n this.listeners.set(key, new Set());\n }\n this.listeners.get(key)!.add(callback);\n return () => this.listeners.get(key)?.delete(callback);\n }\n\n /**\n * Destroys the client, terminating SSE connections and clearing listeners.\n */\n public destroy(): void {\n this.sseManager?.disconnect();\n this.listeners.clear();\n this.storage.clear();\n }\n\n private initRealtimeSync(): void {\n this.sseManager = new SSEManager({\n url: `${this.baseUrl}/api/v1/flags/stream`,\n apiKey: this.apiKey,\n onEvent: (event) => {\n if (event.type === 'FLAG_UPDATE' && event.data) {\n // Use locally evaluated result if we have the flag definition\n const flagDef = this.flagDefinitions.get(event.key);\n const result = flagDef\n ? evaluateFlag(flagDef, this.user)\n : event.data;\n\n this.storage.set(event.key, result);\n this.listeners.get(event.key)?.forEach((cb) => cb(result));\n } else if (event.type === 'FLAG_DELETE') {\n this.storage.delete(event.key);\n this.flagDefinitions.delete(event.key);\n }\n },\n });\n this.sseManager.connect();\n }\n\n private async refreshFlags(): Promise<void> {\n try {\n const result = await fetchWithRetry<Record<string, FeatureFlag>>({\n url: `${this.baseUrl}/api/v1/flags/eval`,\n method: 'GET',\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n 'X-Nexus-User-Id': this.user.id ?? 'anon',\n 'X-Nexus-Country': this.user.country ?? '',\n },\n timeoutMs: this.timeoutMs,\n maxRetries: 2,\n });\n\n for (const [key, flag] of Object.entries(result.data)) {\n this.flagDefinitions.set(key, flag);\n const evaluated = evaluateFlag(flag, this.user);\n this.storage.set(key, evaluated);\n this.listeners.get(key)?.forEach((cb) => cb(evaluated));\n }\n } catch {\n // Offline fallback: retain existing in-memory cache silently\n // Never throw from a background refresh — host app must not be affected\n }\n }\n}\n"]}
|
package/dist/index.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/evaluator.ts","../src/sse-manager.ts","../src/storage.ts","../src/client.ts"],"names":["murmur3","key","seed","h","c1","c2","i","length4","k","k2","rem","computeRolloutBucket","userId","flagKey","hashInput","compareSemver","a","b","partsA","partsB","diff","resolveAttribute","ctx","attributePath","parts","current","part","evaluateSingleRule","rule","userCtx","userValue","targets","evaluateFlag","flag","SSEManager","options","url","evt","data","event","delay","computeBackoffMs","STORAGE_KEY_PREFIX","ANON_ID_KEY","FlagStorage","apiKeyPrefix","result","flags","value","id","testKey","raw","parsed","NexusFlagsClient","resolveApiKey","resolveBaseUrl","defaultValue","cached","variantKey","user","callback","flagDef","cb","fetchWithRetry","evaluated"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @fileoverview MurmurHash3 32-bit implementation and ABAC rule evaluator.\n * Pure TypeScript — no external dependencies. Implements deterministic rollout bucketing.\n * @module @nexus/sdk-flags/evaluator\n */\n\nimport type { FeatureFlag, FlagEvaluationResult, TargetingRule, UserContext } from '@nexussdk/contracts';\n\n// ---------------------------------------------------------------------------\n// MurmurHash3 32-bit Implementation (Pure TypeScript)\n// Used for deterministic user-to-bucket mapping without server round-trips.\n// ---------------------------------------------------------------------------\n\n/**\n * Converts a string to a Uint32 MurmurHash3 hash.\n * Implements the MurmurHash3 32-bit algorithm (x86 variant).\n *\n * @param key - Input string to hash.\n * @param seed - Optional seed value. Defaults to 0.\n * @returns Unsigned 32-bit integer hash value.\n *\n * @example\n * const hash = murmur3('user123:checkout_v2', 0);\n * const bucket = hash % 100; // 0-99 deterministic bucket\n */\nexport function murmur3(key: string, seed = 0): number {\n let h = seed >>> 0;\n const c1 = 0xcc9e2d51;\n const c2 = 0x1b873593;\n\n let i = 0;\n // Process 4-byte chunks\n const length4 = Math.floor(key.length / 4) * 4;\n while (i < length4) {\n let k =\n ((key.charCodeAt(i) & 0xff)) |\n ((key.charCodeAt(i + 1) & 0xff) << 8) |\n ((key.charCodeAt(i + 2) & 0xff) << 16) |\n ((key.charCodeAt(i + 3) & 0xff) << 24);\n\n k = Math.imul(k, c1);\n k = (k << 15) | (k >>> 17);\n k = Math.imul(k, c2);\n\n h ^= k;\n h = (h << 13) | (h >>> 19);\n h = (Math.imul(h, 5) + 0xe6546b64) >>> 0;\n i += 4;\n }\n\n // Process remaining bytes\n let k2 = 0;\n const rem = key.length & 3;\n if (rem >= 3) k2 ^= (key.charCodeAt(i + 2) & 0xff) << 16;\n if (rem >= 2) k2 ^= (key.charCodeAt(i + 1) & 0xff) << 8;\n if (rem >= 1) {\n k2 ^= key.charCodeAt(i) & 0xff;\n k2 = Math.imul(k2, c1);\n k2 = (k2 << 15) | (k2 >>> 17);\n k2 = Math.imul(k2, c2);\n h ^= k2;\n }\n\n // Finalization mix\n h ^= key.length;\n h ^= h >>> 16;\n h = Math.imul(h, 0x85ebca6b);\n h ^= h >>> 13;\n h = Math.imul(h, 0xc2b2ae35);\n h ^= h >>> 16;\n\n return h >>> 0; // Ensure unsigned\n}\n\n/**\n * Computes the deterministic rollout bucket (0-99) for a given user+flag combination.\n * Uses MurmurHash3 for even distribution without server state.\n *\n * @param userId - User's unique identifier (anonymous ID if not authenticated).\n * @param flagKey - Flag programmatic key.\n * @returns Bucket value between 0 and 99 (inclusive).\n *\n * @example\n * const bucket = computeRolloutBucket('usr_12345', 'checkout_v2');\n * // isEnabled = bucket < flag.rolloutPercentage\n */\nexport function computeRolloutBucket(userId: string, flagKey: string): number {\n const hashInput = `${userId}:${flagKey}`;\n return murmur3(hashInput) % 100;\n}\n\n// ---------------------------------------------------------------------------\n// ABAC Rule Evaluator\n// ---------------------------------------------------------------------------\n\n/**\n * Performs a simple semantic version comparison.\n * Compares two semver strings in \"MAJOR.MINOR.PATCH\" format.\n *\n * @param a - First version string.\n * @param b - Second version string.\n * @returns Positive if a > b, negative if a < b, 0 if equal.\n */\nfunction compareSemver(a: string, b: string): number {\n const partsA = a.replace(/^v/, '').split('.').map(Number);\n const partsB = b.replace(/^v/, '').split('.').map(Number);\n for (let i = 0; i < 3; i++) {\n const diff = (partsA[i] ?? 0) - (partsB[i] ?? 0);\n if (diff !== 0) return diff;\n }\n return 0;\n}\n\n/**\n * Extracts a nested attribute value from the UserContext by dot-notation path.\n *\n * @param ctx - UserContext object.\n * @param attributePath - Dot-separated path (e.g. \"custom.tier\", \"country\").\n * @returns The attribute value or `undefined` if not found.\n */\nfunction resolveAttribute(ctx: UserContext, attributePath: string): unknown {\n const parts = attributePath.split('.');\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let current: any = ctx;\n for (const part of parts) {\n if (current == null || typeof current !== 'object') return undefined;\n current = current[part];\n }\n return current;\n}\n\n/**\n * Evaluates a single targeting rule against the user context.\n *\n * @param rule - The ABAC targeting rule to evaluate.\n * @param userCtx - The current user context.\n * @returns `true` if the rule passes for this user.\n */\nfunction evaluateSingleRule(rule: TargetingRule, userCtx: UserContext): boolean {\n const userValue = resolveAttribute(userCtx, rule.attribute);\n const targets = rule.values;\n\n switch (rule.operator) {\n case 'EQUALS':\n return userValue === targets[0];\n case 'NOT_EQUALS':\n return userValue !== targets[0];\n case 'IN':\n return targets.includes(userValue as string | number | boolean);\n case 'NOT_IN':\n return !targets.includes(userValue as string | number | boolean);\n case 'CONTAINS':\n return typeof userValue === 'string' && userValue.includes(String(targets[0]));\n case 'NOT_CONTAINS':\n return typeof userValue === 'string' && !userValue.includes(String(targets[0]));\n case 'STARTS_WITH':\n return typeof userValue === 'string' && userValue.startsWith(String(targets[0]));\n case 'ENDS_WITH':\n return typeof userValue === 'string' && userValue.endsWith(String(targets[0]));\n case 'GREATER_THAN':\n return typeof userValue === 'number' && userValue > Number(targets[0]);\n case 'LESS_THAN':\n return typeof userValue === 'number' && userValue < Number(targets[0]);\n case 'SEMVER_GTE':\n return (\n typeof userValue === 'string' &&\n typeof targets[0] === 'string' &&\n compareSemver(userValue, String(targets[0])) >= 0\n );\n case 'SEMVER_LTE':\n return (\n typeof userValue === 'string' &&\n typeof targets[0] === 'string' &&\n compareSemver(userValue, String(targets[0])) <= 0\n );\n default:\n return false;\n }\n}\n\n/**\n * Evaluates a FeatureFlag against a UserContext using the full ABAC + rollout engine.\n *\n * Evaluation Order:\n * 1. Kill-switch check (`isEnabled === false` → KILL_SWITCH)\n * 2. All targeting rules must pass (ABAC evaluation)\n * 3. Percentage rollout via MurmurHash3 bucketing\n * 4. Default enabled state\n *\n * @param flag - The feature flag definition from the cache.\n * @param userCtx - Current user context for targeting evaluation.\n * @returns Full {@link FlagEvaluationResult} with reason explanation.\n *\n * @example\n * const result = evaluateFlag(flag, { id: 'usr_12345', country: 'VN' });\n * if (result.enabled) {\n * console.log(result.variants.discount_rate); // 20\n * }\n */\nexport function evaluateFlag(flag: FeatureFlag, userCtx: UserContext): FlagEvaluationResult {\n // Step 1: Kill-switch\n if (!flag.isEnabled) {\n return {\n key: flag.key,\n enabled: false,\n variants: {},\n reason: 'KILL_SWITCH',\n version: flag.version,\n };\n }\n\n // Step 2: ABAC targeting rules — all must pass (AND logic)\n if (flag.targetingRules.length > 0) {\n const allRulesPass = flag.targetingRules.every((rule) =>\n evaluateSingleRule(rule, userCtx),\n );\n if (!allRulesPass) {\n return {\n key: flag.key,\n enabled: false,\n variants: {},\n reason: 'FALLBACK',\n version: flag.version,\n };\n }\n // All targeting rules matched\n if (flag.rolloutPercentage >= 100) {\n return {\n key: flag.key,\n enabled: true,\n variants: flag.variants,\n reason: 'TARGETING_MATCH',\n version: flag.version,\n };\n }\n }\n\n // Step 3: Percentage rollout (MurmurHash3)\n if (flag.rolloutPercentage > 0) {\n const userId = userCtx.id ?? 'anon';\n const bucket = computeRolloutBucket(userId, flag.key);\n if (bucket < flag.rolloutPercentage) {\n return {\n key: flag.key,\n enabled: true,\n variants: flag.variants,\n reason: flag.targetingRules.length > 0 ? 'TARGETING_MATCH' : 'ROLLOUT_MATCH',\n version: flag.version,\n };\n }\n return {\n key: flag.key,\n enabled: false,\n variants: {},\n reason: 'FALLBACK',\n version: flag.version,\n };\n }\n\n // Step 4: Full kill-switch off (rolloutPercentage === 0 with no rules)\n return {\n key: flag.key,\n enabled: false,\n variants: {},\n reason: 'KILL_SWITCH',\n version: flag.version,\n };\n}\n","/**\n * @fileoverview Singleton SSE Manager with ref-counting and auto-reconnect.\n * Ensures exactly one EventSource connection per API key across all flag subscribers.\n * @module @nexus/sdk-flags/sse-manager\n */\n\nimport type { FlagStreamEvent } from '@nexussdk/contracts';\nimport { computeBackoffMs } from '@nexussdk/core';\n\n/**\n * Options for creating an SSE connection.\n */\nexport interface SSEManagerOptions {\n /** Full SSE stream URL (e.g. \"http://localhost:8080/api/v1/flags/stream\"). */\n url: string;\n /** Public API key for authentication. */\n apiKey: string;\n /** Callback invoked for each received flag stream event. */\n onEvent: (event: FlagStreamEvent) => void;\n /** Callback invoked on connection state changes (optional). */\n onStateChange?: (connected: boolean) => void;\n /** Maximum reconnect attempts. Defaults to Infinity. */\n maxReconnects?: number;\n}\n\n/**\n * Ref-counted Singleton SSE Manager.\n *\n * Multiple consumers sharing the same `apiKey` receive updates\n * from a single shared SSE connection. Connection is torn down when all\n * consumers have called `disconnect()`.\n *\n * Connection lifecycle:\n * 1. `connect()` → opens EventSource or streaming fetch\n * 2. On disconnect: exponential backoff reconnect (max 30s)\n * 3. `disconnect()` → decrements ref count; tears down on 0\n *\n * @example\n * const manager = new SSEManager({\n * url: 'http://localhost:8080/api/v1/flags/stream',\n * apiKey: 'pk_live_...',\n * onEvent: (event) => updateCache(event),\n * });\n * manager.connect();\n * // Later:\n * manager.disconnect();\n */\nexport class SSEManager {\n private readonly options: SSEManagerOptions;\n private eventSource: EventSource | null = null;\n private connected = false;\n private destroyed = false;\n private reconnectAttempt = 0;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private readonly maxReconnects: number;\n\n constructor(options: SSEManagerOptions) {\n this.options = options;\n this.maxReconnects = options.maxReconnects ?? Infinity;\n }\n\n /**\n * Returns whether the SSE connection is currently active.\n *\n * @returns `true` if EventSource is open and receiving events.\n */\n get isConnected(): boolean {\n return this.connected;\n }\n\n /**\n * Opens the SSE connection to the Go-Gin stream endpoint.\n * Safe to call multiple times — no-op if already connected.\n *\n * @example\n * manager.connect();\n */\n connect(): void {\n if (this.destroyed || this.connected || this.eventSource) return;\n this.openConnection();\n }\n\n /**\n * Closes the SSE connection and cancels any pending reconnect timers.\n *\n * @example\n * manager.disconnect();\n */\n disconnect(): void {\n this.destroyed = true;\n this.cleanup();\n }\n\n private openConnection(): void {\n if (this.destroyed) return;\n\n try {\n // Append API key as query parameter for SSE (EventSource doesn't support custom headers)\n const url = new URL(this.options.url);\n url.searchParams.set('apiKey', this.options.apiKey);\n\n this.eventSource = new EventSource(url.toString());\n\n this.eventSource.addEventListener('open', () => {\n this.connected = true;\n this.reconnectAttempt = 0;\n this.options.onStateChange?.(true);\n });\n\n this.eventSource.addEventListener('message', (evt: MessageEvent<string>) => {\n this.handleMessage(evt.data);\n });\n\n // Go-Gin sends events with event type 'message' by default\n // Also listen for explicitly typed events\n this.eventSource.addEventListener('flag_update', (evt: MessageEvent<string>) => {\n this.handleMessage(evt.data);\n });\n\n this.eventSource.addEventListener('error', () => {\n this.connected = false;\n this.options.onStateChange?.(false);\n this.cleanup();\n this.scheduleReconnect();\n });\n } catch {\n this.scheduleReconnect();\n }\n }\n\n private handleMessage(data: string): void {\n try {\n const event = JSON.parse(data) as FlagStreamEvent;\n this.options.onEvent(event);\n } catch {\n // Ignore malformed SSE payloads — never crash the host application\n }\n }\n\n private cleanup(): void {\n if (this.eventSource) {\n this.eventSource.close();\n this.eventSource = null;\n }\n if (this.reconnectTimer !== null) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n }\n\n private scheduleReconnect(): void {\n if (this.destroyed) return;\n if (this.reconnectAttempt >= this.maxReconnects) return;\n\n const delay = computeBackoffMs(this.reconnectAttempt, 1000, 30_000);\n this.reconnectAttempt++;\n\n this.reconnectTimer = setTimeout(() => {\n this.reconnectTimer = null;\n if (!this.destroyed) {\n this.openConnection();\n }\n }, delay);\n }\n}\n","/**\n * @fileoverview In-memory flag storage with LocalStorage sync and incognito fallback.\n * @module @nexus/sdk-flags/storage\n */\n\nimport type { FlagEvaluationResult } from '@nexussdk/contracts';\n\nconst STORAGE_KEY_PREFIX = 'nexus_flags_';\nconst ANON_ID_KEY = 'nexus_anon_id';\n\n/**\n * In-memory flag cache with optional LocalStorage persistence.\n * Falls back to pure in-memory storage in SSR / Incognito / Worker contexts.\n *\n * @example\n * const store = new FlagStorage('pk_live_abc123');\n * store.set('checkout_v2', { key: 'checkout_v2', enabled: true, variants: {}, reason: 'ROLLOUT_MATCH', version: 3 });\n * const result = store.get('checkout_v2');\n */\nexport class FlagStorage {\n private readonly memory = new Map<string, FlagEvaluationResult>();\n private readonly storageKey: string;\n private readonly localStorageAvailable: boolean;\n private broadcastChannel: BroadcastChannel | null = null;\n\n constructor(apiKeyPrefix: string) {\n this.storageKey = `${STORAGE_KEY_PREFIX}${apiKeyPrefix}`;\n this.localStorageAvailable = this.testLocalStorage();\n this.hydrate();\n this.setupBroadcastChannel();\n }\n\n /**\n * Stores a flag evaluation result in memory (and localStorage if available).\n *\n * @param key - Flag programmatic key.\n * @param result - Evaluation result to persist.\n */\n set(key: string, result: FlagEvaluationResult): void {\n this.memory.set(key, result);\n this.persist();\n this.broadcastUpdate(key, result);\n }\n\n /**\n * Retrieves a flag evaluation result by key.\n *\n * @param key - Flag programmatic key.\n * @returns The cached evaluation result or `undefined`.\n */\n get(key: string): FlagEvaluationResult | undefined {\n return this.memory.get(key);\n }\n\n /**\n * Bulk-sets multiple flag results (e.g. after a batch fetch).\n *\n * @param flags - Record of flag key → evaluation result.\n */\n setAll(flags: Record<string, FlagEvaluationResult>): void {\n for (const [key, value] of Object.entries(flags)) {\n this.memory.set(key, value);\n }\n this.persist();\n }\n\n /**\n * Removes a single flag from storage (e.g. on FLAG_DELETE SSE event).\n *\n * @param key - Flag programmatic key.\n */\n delete(key: string): void {\n this.memory.delete(key);\n this.persist();\n }\n\n /**\n * Returns all cached flag results.\n *\n * @returns All stored flag results as a record.\n */\n getAll(): Record<string, FlagEvaluationResult> {\n return Object.fromEntries(this.memory.entries());\n }\n\n /**\n * Clears all cached flags from memory and localStorage.\n */\n clear(): void {\n this.memory.clear();\n if (this.localStorageAvailable) {\n try {\n window.localStorage.removeItem(this.storageKey);\n } catch {\n // Storage access denied\n }\n }\n this.broadcastChannel?.close();\n }\n\n /**\n * Retrieves or creates a persistent anonymous user ID.\n *\n * @returns Anonymous ID string (e.g. \"anon_xyz123abc\").\n */\n getOrCreateAnonymousId(): string {\n if (typeof window === 'undefined') return 'anon-ssr-node';\n\n if (this.localStorageAvailable) {\n try {\n let id = window.localStorage.getItem(ANON_ID_KEY);\n if (!id) {\n id = `anon_${Math.random().toString(36).substring(2, 11)}`;\n window.localStorage.setItem(ANON_ID_KEY, id);\n }\n return id;\n } catch {\n // Incognito / locked storage\n }\n }\n\n return `anon_${Math.random().toString(36).substring(2, 11)}`;\n }\n\n private testLocalStorage(): boolean {\n try {\n if (typeof window === 'undefined') return false;\n const testKey = '__nexus_test__';\n window.localStorage.setItem(testKey, '1');\n window.localStorage.removeItem(testKey);\n return true;\n } catch {\n return false;\n }\n }\n\n private hydrate(): void {\n if (!this.localStorageAvailable) return;\n try {\n const raw = window.localStorage.getItem(this.storageKey);\n if (raw) {\n const parsed = JSON.parse(raw) as Record<string, FlagEvaluationResult>;\n for (const [key, value] of Object.entries(parsed)) {\n this.memory.set(key, value);\n }\n }\n } catch {\n // Corrupt storage — start fresh\n }\n }\n\n private persist(): void {\n if (!this.localStorageAvailable) return;\n try {\n window.localStorage.setItem(\n this.storageKey,\n JSON.stringify(Object.fromEntries(this.memory.entries())),\n );\n } catch {\n // Storage quota exceeded — in-memory only\n }\n }\n\n private setupBroadcastChannel(): void {\n try {\n if (typeof BroadcastChannel !== 'undefined') {\n this.broadcastChannel = new BroadcastChannel(`nexus_flags_${this.storageKey}`);\n this.broadcastChannel.onmessage = (evt: MessageEvent<{ key: string; result: FlagEvaluationResult }>) => {\n // Sync updates from other tabs into local memory cache\n if (evt.data?.key && evt.data?.result) {\n this.memory.set(evt.data.key, evt.data.result);\n }\n };\n }\n } catch {\n // BroadcastChannel unavailable (e.g. Worker context)\n }\n }\n\n private broadcastUpdate(key: string, result: FlagEvaluationResult): void {\n try {\n this.broadcastChannel?.postMessage({ key, result });\n } catch {\n // Broadcast failed — no-op\n }\n }\n}\n","/**\n * @fileoverview NexusFlagsClient — Full-featured feature flags SDK client.\n * In-memory evaluation, SSE real-time sync, ABAC targeting, and MurmurHash3 rollout.\n * @module @nexus/sdk-flags/client\n */\n\nimport type {\n FlagEvaluationResult,\n FlagVariants,\n FeatureFlag,\n UserContext,\n} from '@nexussdk/contracts';\nimport { resolveApiKey, resolveBaseUrl, fetchWithRetry } from '@nexussdk/core';\nimport { evaluateFlag } from './evaluator.js';\nimport { SSEManager } from './sse-manager.js';\nimport { FlagStorage } from './storage.js';\n\n/**\n * Options for initializing the NexusFlagsClient.\n *\n * @example\n * const client = new NexusFlagsClient({\n * apiKey: 'pk_live_...',\n * baseUrl: 'http://localhost:8080',\n * user: { id: 'usr_12345', country: 'VN' },\n * realtime: true,\n * });\n */\nexport interface NexusFlagsOptions {\n /**\n * Public API Key ('pk_live_...' or 'pk_test_...').\n * If omitted, resolved automatically via env variables.\n */\n apiKey?: string;\n /**\n * Base ingestion URL. Defaults to 'https://api.nexus.dev'.\n */\n baseUrl?: string;\n /**\n * Initial user identity context for targeting and percentage rollouts.\n */\n user?: UserContext;\n /**\n * Pre-hydrated flags evaluated on the server (SSR) to prevent client-side UI flicker.\n */\n bootstrap?: Record<string, FlagEvaluationResult>;\n /**\n * Toggle real-time SSE updates. Defaults to true.\n */\n realtime?: boolean;\n /**\n * Network timeout in milliseconds for evaluation fetch. Defaults to 3000ms.\n */\n timeoutMs?: number;\n}\n\n/**\n * Public interface for the NexusFlagsClient.\n */\nexport interface INexusFlagsClient {\n /**\n * Synchronously checks if a flag is active for the current user.\n *\n * @param key - Unique flag identifier.\n * @param defaultValue - Fallback returned if flag is absent or evaluating.\n * @returns `true` if the flag is enabled.\n *\n * @example\n * const showBanner = client.isEnabled('promo_banner_v2', false);\n */\n isEnabled(key: string, defaultValue?: boolean): boolean;\n\n /**\n * Synchronously retrieves a specific dynamic configuration variant.\n *\n * @param key - Unique flag identifier.\n * @param variantKey - Property inside the variant object.\n * @param defaultValue - Fallback returned if flag/variant is missing.\n * @returns The variant value cast to type T.\n *\n * @example\n * const rate = client.getVariant<number>('promo_banner_v2', 'discount_rate', 10);\n */\n getVariant<T = unknown>(key: string, variantKey: string, defaultValue?: T): T;\n\n /**\n * Updates current user context and re-evaluates all flags.\n *\n * @param user - New user context to apply.\n * @returns Promise that resolves after flags are refreshed.\n *\n * @example\n * await client.identify({ id: 'usr_99', country: 'SG' });\n */\n identify(user: UserContext): Promise<void>;\n\n /**\n * Resets user context to an anonymous persistent device identifier.\n *\n * @example\n * client.reset(); // called on logout\n */\n reset(): void;\n\n /**\n * Subscribes to runtime flag changes triggered by SSE updates.\n *\n * @param key - Flag key to observe.\n * @param callback - Called with new evaluation result when the flag changes.\n * @returns Unsubscribe function — call to remove the listener.\n *\n * @example\n * const unsub = client.onFlagChange('checkout_v2', (result) => {\n * setCheckoutEnabled(result.enabled);\n * });\n * // On component unmount:\n * unsub();\n */\n onFlagChange(key: string, callback: (result: FlagEvaluationResult) => void): () => void;\n\n /**\n * Gracefully shuts down active SSE connections and observers.\n *\n * @example\n * client.destroy();\n */\n destroy(): void;\n}\n\n/**\n * Feature Flags Client — the primary SDK entry point for flag evaluation.\n *\n * @implements {INexusFlagsClient}\n *\n * @example\n * const client = new NexusFlagsClient({ apiKey: 'pk_live_...' });\n * const enabled = client.isEnabled('new_checkout', false);\n */\nexport class NexusFlagsClient implements INexusFlagsClient {\n private readonly apiKey: string;\n private readonly baseUrl: string;\n private readonly timeoutMs: number;\n private user: UserContext;\n private readonly storage: FlagStorage;\n private readonly listeners = new Map<string, Set<(res: FlagEvaluationResult) => void>>();\n private sseManager?: SSEManager;\n private flagDefinitions = new Map<string, FeatureFlag>();\n\n constructor(options: NexusFlagsOptions = {}) {\n this.apiKey = resolveApiKey(options.apiKey);\n this.baseUrl = resolveBaseUrl(options.baseUrl);\n this.timeoutMs = options.timeoutMs ?? 3_000;\n this.storage = new FlagStorage(this.apiKey.substring(0, 16));\n this.user = options.user ?? { id: this.storage.getOrCreateAnonymousId() };\n\n // Hydrate bootstrap flags (SSR pre-evaluation)\n if (options.bootstrap) {\n this.storage.setAll(options.bootstrap);\n }\n\n if (options.realtime !== false) {\n this.initRealtimeSync();\n }\n\n // Prefetch flags asynchronously on init\n void this.refreshFlags();\n }\n\n /**\n * Evaluates whether a given feature flag is enabled for the current context.\n */\n public isEnabled(key: string, defaultValue = false): boolean {\n const cached = this.storage.get(key);\n return cached !== undefined ? cached.enabled : defaultValue;\n }\n\n /**\n * Retrieves a typed variant configuration for an active feature flag.\n */\n public getVariant<T = unknown>(key: string, variantKey: string, defaultValue?: T): T {\n const cached = this.storage.get(key);\n if (!cached?.enabled || !cached.variants) {\n return defaultValue as T;\n }\n const value = (cached.variants as FlagVariants)[variantKey];\n return value !== undefined ? (value as T) : (defaultValue as T);\n }\n\n /**\n * Identifies an authenticated user and triggers a flag evaluation refresh.\n */\n public async identify(user: UserContext): Promise<void> {\n this.user = { ...this.user, ...user };\n await this.refreshFlags();\n }\n\n /**\n * Resets the active user context to a new anonymous identifier.\n */\n public reset(): void {\n this.user = { id: this.storage.getOrCreateAnonymousId() };\n void this.refreshFlags();\n }\n\n /**\n * Registers a listener callback invoked when a flag's evaluation state changes.\n */\n public onFlagChange(key: string, callback: (result: FlagEvaluationResult) => void): () => void {\n if (!this.listeners.has(key)) {\n this.listeners.set(key, new Set());\n }\n this.listeners.get(key)!.add(callback);\n return () => this.listeners.get(key)?.delete(callback);\n }\n\n /**\n * Destroys the client, terminating SSE connections and clearing listeners.\n */\n public destroy(): void {\n this.sseManager?.disconnect();\n this.listeners.clear();\n this.storage.clear();\n }\n\n private initRealtimeSync(): void {\n this.sseManager = new SSEManager({\n url: `${this.baseUrl}/api/v1/flags/stream`,\n apiKey: this.apiKey,\n onEvent: (event) => {\n if (event.type === 'FLAG_UPDATE' && event.data) {\n // Use locally evaluated result if we have the flag definition\n const flagDef = this.flagDefinitions.get(event.key);\n const result = flagDef\n ? evaluateFlag(flagDef, this.user)\n : event.data;\n\n this.storage.set(event.key, result);\n this.listeners.get(event.key)?.forEach((cb) => cb(result));\n } else if (event.type === 'FLAG_DELETE') {\n this.storage.delete(event.key);\n this.flagDefinitions.delete(event.key);\n }\n },\n });\n this.sseManager.connect();\n }\n\n private async refreshFlags(): Promise<void> {\n try {\n const result = await fetchWithRetry<Record<string, FeatureFlag>>({\n url: `${this.baseUrl}/api/v1/flags/eval`,\n method: 'GET',\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n 'X-Nexus-User-Id': this.user.id ?? 'anon',\n 'X-Nexus-Country': this.user.country ?? '',\n },\n timeoutMs: this.timeoutMs,\n maxRetries: 2,\n });\n\n for (const [key, flag] of Object.entries(result.data)) {\n this.flagDefinitions.set(key, flag);\n const evaluated = evaluateFlag(flag, this.user);\n this.storage.set(key, evaluated);\n this.listeners.get(key)?.forEach((cb) => cb(evaluated));\n }\n } catch {\n // Offline fallback: retain existing in-memory cache silently\n // Never throw from a background refresh — host app must not be affected\n }\n }\n}\n"]}
|