UNPKG

@harnessio/ff-javascript-client-sdk

Version:

Basic library for integrating CF into javascript applications.

3 lines (2 loc) 17.8 kB
var Ae=Object.defineProperty,Ce=Object.defineProperties;var Te=Object.getOwnPropertyDescriptors;var ae=Object.getOwnPropertySymbols;var we=Object.prototype.hasOwnProperty,De=Object.prototype.propertyIsEnumerable;var oe=(r,e,n)=>e in r?Ae(r,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):r[e]=n,I=(r,e)=>{for(var n in e||(e={}))we.call(e,n)&&oe(r,n,e[n]);if(ae)for(var n of ae(e))De.call(e,n)&&oe(r,n,e[n]);return r},F=(r,e)=>Ce(r,Te(e));var E=(r,e,n)=>new Promise((a,s)=>{var c=R=>{try{p(n.next(R))}catch(d){s(d)}},g=R=>{try{p(n.throw(R))}catch(d){s(d)}},p=R=>R.done?a(R.value):Promise.resolve(R.value).then(c,g);p((n=n.apply(r,e)).next())});import ke from"jwt-decode";import Le from"mitt";var x=(l=>(l.READY="ready",l.CONNECTED="connected",l.DISCONNECTED="disconnected",l.STOPPED="stopped",l.POLLING="polling",l.POLLING_STOPPED="polling stopped",l.FLAGS_LOADED="flags loaded",l.CACHE_LOADED="cache loaded",l.CHANGED="changed",l.ERROR="error",l.ERROR_CACHE="cache error",l.ERROR_METRICS="metrics error",l.ERROR_AUTH="auth error",l.ERROR_FETCH_FLAGS="fetch flags error",l.ERROR_FETCH_FLAG="fetch flag error",l.ERROR_STREAM="stream error",l.ERROR_DEFAULT_VARIATION_RETURNED="default variation returned",l))(x||{});var Oe={debug:!1,baseUrl:"https://config.ff.harness.io/api/1.0",eventUrl:"https://events.ff.harness.io/api/1.0",eventsSyncInterval:6e4,pollingInterval:6e4,enableAnalytics:!0,streamEnabled:!0,cache:!1,authRequestReadTimeout:0,maxStreamRetries:1/0},se=r=>{let e=I(I({},Oe),r);return e.pollingEnabled===void 0&&(e.pollingEnabled=e.streamEnabled),e.eventsSyncInterval<6e4&&(e.eventsSyncInterval=6e4),e.pollingInterval<6e4&&(e.pollingInterval=6e4),(!e.logger||!e.logger.debug||!e.logger.error||!e.logger.info||!e.logger.warn)&&(e.logger=console),e},W=(r,e=!0)=>{e?setTimeout(r,0):r()},_=(r,e)=>Math.round(Math.random()*(e-r)+r),le=r=>{let e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",n="",a=0,s=Pe(JSON.stringify(r));for(;a<s.length;){let c=s.charCodeAt(a++),g=s.charCodeAt(a++),p=s.charCodeAt(a++),R=c>>2,d=(c&3)<<4|g>>4,b=(g&15)<<2|p>>6,D=p&63;isNaN(g)?b=D=64:isNaN(p)&&(D=64),n+=e.charAt(R)+e.charAt(d)+e.charAt(b)+e.charAt(D)}return n},Pe=r=>r.replace(/\r\n/g,` `).split("").map(e=>{let n=e.charCodeAt(0);return n<128?String.fromCharCode(n):n>127&&n<2048?String.fromCharCode(n>>6|192)+String.fromCharCode(n&63|128):String.fromCharCode(n>>12|224)+String.fromCharCode(n>>6&63|128)+String.fromCharCode(n&63|128)}).join("");function k(r){return[...r].sort(({flag:e},{flag:n})=>e<n?-1:1)}function J(r){return function(...n){let[a,s]=r(n);return fetch(a,s)}}var ce=3e4,L=class{constructor(e,n,a,s,c,g,p,R,d,b,D){this.eventBus=e;this.configurations=n;this.url=a;this.apiKey=s;this.standardHeaders=c;this.fallbackPoller=g;this.logDebug=p;this.logError=R;this.eventCallback=d;this.maxRetries=b;this.middleware=D;this.closed=!1;this.connectionOpened=!1;this.disconnectEventEmitted=!1;this.reconnectAttempts=0;this.retriesExhausted=!1}registerAPIRequestMiddleware(e){this.middleware=e}start(){let e=d=>{d.toString().split(/\r?\n/).forEach(n)},n=d=>{if(d.startsWith("data:")){let b=JSON.parse(d.substring(5));this.logDebugMessage("Received event from stream: ",b),this.eventCallback(b)}},a=()=>{this.logDebugMessage("Stream connected"),this.eventBus.emit("connected"),this.reconnectAttempts=0},s=()=>{clearInterval(this.readTimeoutCheckerId);let d=_(1e3,3e4);if(this.reconnectAttempts++,this.logDebugMessage("Stream disconnected, will reconnect in "+d+"ms"),this.disconnectEventEmitted||(this.eventBus.emit("disconnected"),this.disconnectEventEmitted=!0),this.reconnectAttempts>=5&&this.reconnectAttempts%5===0&&this.logErrorMessage(`Reconnection failed after ${this.reconnectAttempts} attempts; attempting further reconnections.`),this.reconnectAttempts>=this.maxRetries){this.retriesExhausted=!0,this.configurations.pollingEnabled?this.logErrorMessage("Max streaming retries reached. Staying in polling mode."):this.logErrorMessage("Max streaming retries reached. Polling mode is disabled and will receive no further flag updates until SDK client is restarted.");return}setTimeout(()=>this.start(),d)},c=d=>{this.retriesExhausted||(d&&this.logDebugMessage("Stream has issue",d),this.fallBackToPolling(),this.eventBus.emit("stream error",d),this.eventBus.emit("error",d),s())},g=I({"Cache-Control":"no-cache",Accept:"text/event-stream","API-Key":this.apiKey},this.standardHeaders);if(this.middleware){let[d,b]=this.middleware([this.url,{headers:g}]);this.url=d,g=(b==null?void 0:b.headers)||{}}this.logDebugMessage("SSE HTTP start request",this.url),this.xhr=new XMLHttpRequest,this.xhr.open("GET",this.url);for(let[d,b]of Object.entries(g))this.xhr.setRequestHeader(d,b);this.xhr.timeout=24*60*60*1e3,this.xhr.onerror=()=>{this.connectionOpened=!1,c("XMLHttpRequest error on SSE stream")},this.xhr.onabort=()=>{this.connectionOpened=!1,this.logDebugMessage("SSE aborted"),this.closed||c(null)},this.xhr.ontimeout=()=>{this.connectionOpened=!1,c("SSE timeout")},this.xhr.onload=()=>{c(`Received XMLHttpRequest onLoad event: ${this.xhr.status}`)};let p=0,R=Date.now();this.xhr.onprogress=()=>{this.connectionOpened||(a(),this.connectionOpened=!0,this.disconnectEventEmitted=!1),this.stopFallBackPolling(),R=Date.now();let d=this.xhr.responseText.slice(p);p+=d.length,this.logDebugMessage("SSE GOT: "+d),e(d)},this.readTimeoutCheckerId=setInterval(()=>{R<Date.now()-ce&&(this.logDebugMessage("SSE read timeout"),this.xhr.abort())},ce),this.xhr.send()}close(){this.connectionOpened=!1,this.closed=!0,this.xhr&&this.xhr.abort(),clearInterval(this.readTimeoutCheckerId),this.eventBus.emit("stopped"),this.stopFallBackPolling()}fallBackToPolling(){!this.fallbackPoller.isPolling()&&this.configurations.pollingEnabled&&(this.logDebugMessage("Falling back to polling mode while stream recovers"),this.fallbackPoller.start())}stopFallBackPolling(){this.fallbackPoller.isPolling()&&(this.logDebugMessage("Stopping fallback polling mode"),this.fallbackPoller.stop())}logDebugMessage(e,...n){this.configurations.debug&&this.logDebug(`Streaming: ${e}`,...n)}logErrorMessage(e,...n){this.logError(`Streaming: ${e}`,...n)}};function ue(r,e,n,a,s,c){let g=r in n,p=g?n[r]:e;if(g)a(r,p);else{let R={flag:r,defaultVariation:e};s.emit("default variation returned",R)}return c?{value:p,isDefaultValue:!g}:p}var N=class{constructor(e,n,a,s,c){this.fetchFlagsFn=e;this.configurations=n;this.eventBus=a;this.logDebug=s;this.logError=c;this.maxAttempts=5}start(){if(this.isPolling()){this.logDebugMessage("Already polling.");return}this.isRunning=!0,this.eventBus.emit("polling"),this.logDebugMessage(`Starting poller, first poll will be in ${this.configurations.pollingInterval}ms`),this.timeoutId=setTimeout(()=>this.poll(),this.configurations.pollingInterval)}poll(){this.isRunning&&this.attemptFetch().finally(()=>{this.isRunning&&(this.timeoutId=setTimeout(()=>this.poll(),this.configurations.pollingInterval))})}attemptFetch(){return E(this,null,function*(){for(let e=1;e<=this.maxAttempts;e++){let n=yield this.fetchFlagsFn();if(n.type==="success"){this.logDebugMessage(`Successfully polled for flag updates, next poll in ${this.configurations.pollingInterval}ms. `);return}if(this.logErrorMessage("Error when polling for flag updates",n.error),e>=this.maxAttempts){this.logDebugMessage(`Maximum attempts reached for polling for flags. Next poll in ${this.configurations.pollingInterval}ms.`);return}this.logDebugMessage(`Polling for flags attempt #${e} failed. Remaining attempts: ${this.maxAttempts-e}`,n.error);let a=_(1e3,1e4);yield new Promise(s=>setTimeout(s,a))}})}stop(){this.timeoutId&&(this.isRunning=!1,clearTimeout(this.timeoutId),this.timeoutId=void 0,this.eventBus.emit("polling stopped"),this.logDebugMessage("Polling stopped"))}isPolling(){return this.isRunning}logDebugMessage(e,...n){this.configurations.debug&&this.logDebug(`Poller: ${e}`,...n)}logErrorMessage(e,...n){this.logError(`Poller: ${e}`,...n)}};function de(n){return E(this,arguments,function*(r,e={}){let a=yield Ne(r),s=Ve(e);return{loadFromCache:()=>Y(a,s,e),saveToCache:c=>X(a,s,c),updateCachedEvaluation:c=>Fe(a,s,c),removeCachedEvaluation:c=>xe(a,s,c)}})}function Y(a,s){return E(this,arguments,function*(r,e,n={}){let c=parseInt(yield e.getItem(r+".ts"));if(n!=null&&n.ttl&&!isNaN(c)&&c+n.ttl<Date.now())return yield Me(r,e),[];let g=yield e.getItem(r);if(g)try{return JSON.parse(g)}catch(p){}return[]})}function Me(r,e){return E(this,null,function*(){yield e.removeItem(r),yield e.removeItem(r+".ts")})}function X(r,e,n){return E(this,null,function*(){yield e.setItem(r,JSON.stringify(k(n))),yield e.setItem(r+".ts",Date.now().toString())})}function Fe(r,e,n){return E(this,null,function*(){let a=yield Y(r,e),s=a.find(({flag:c})=>c===n.flag);s?Object.assign(s,n):a.push(n),yield X(r,e,a)})}function xe(r,e,n){return E(this,null,function*(){let a=yield Y(r,e),s=a.findIndex(({flag:c})=>c===n);s>-1&&(a.splice(s,1),yield X(r,e,a))})}function ge(r,e,n={}){return n.deriveKeyFromTargetAttributes?JSON.stringify(Object.keys(r.attributes||{}).sort().filter(a=>!Array.isArray(n.deriveKeyFromTargetAttributes)||n.deriveKeyFromTargetAttributes.includes(a)).reduce((a,s)=>F(I({},a),{[s]:r.attributes[s]}),{}))+r.identifier+e:r.identifier+e}function Ne(r){return E(this,null,function*(){var n,a;let e=r;if(globalThis!=null&&globalThis.TextEncoder&&((a=(n=globalThis==null?void 0:globalThis.crypto)==null?void 0:n.subtle)!=null&&a.digest)){let c=new TextEncoder().encode(r),g=yield crypto.subtle.digest("SHA-256",c);e=Array.from(new Uint8Array(g)).map(R=>R.toString(16).padStart(2,"0")).join("")}else globalThis.btoa&&(e=btoa(r));return"HARNESS_FF_CACHE_"+e})}function Ve(r){let e;return!r.storage||typeof r.storage!="object"||!("getItem"in r.storage)||!("setItem"in r.storage)||!("removeItem"in r.storage)?globalThis.localStorage?e=globalThis.localStorage:globalThis.sessionStorage?e=globalThis.sessionStorage:e=_e:e=r.storage,{getItem(a){return E(this,null,function*(){let s=e.getItem(a);return s instanceof Promise?yield s:s})},setItem(a,s){return E(this,null,function*(){let c=e.setItem(a,s);c instanceof Promise&&(yield c)})},removeItem(a){return E(this,null,function*(){let s=e.removeItem(a);s instanceof Promise&&(yield s)})}}}var _e={getItem:()=>null,setItem:()=>{},removeItem:()=>{}};var he="1.32.0",fe=`Javascript ${he} Client`,He=500,z=!!globalThis.Proxy,yt=(r,e,n)=>{let a=!1,s,c,g,p,R,d,b={},D=t=>t,M=J(D),Q=0,Z=!1,H=!1,A=[],l=se(n),me=l.enableAnalytics,h=Le(),$=()=>{H=!0},q=()=>{H=!1},O=()=>me&&!H,y=(t,...i)=>{l.debug&&l.logger.debug(`[FF-SDK] ${t}`,...i)},C=(t,...i)=>{l.logger.error(`[FF-SDK] ${t}`,...i)},ve=(t,...i)=>{l.logger.warn(`[FF-SDK] ${t}`,...i)},G=t=>{let{value:i}=t;try{switch(t.kind.toLowerCase()){case"int":case"number":i=Number(i);break;case"boolean":i=i.toString().toLowerCase()==="true";break;case"json":i=JSON.parse(i);break}}catch(u){C(u)}return i},B=t=>{if(O()){let i=Date.now();i-t.lastAccessed>He&&(t.count++,t.lastAccessed=i)}},pe=()=>E(void 0,null,function*(){if(l.cache){y("initializing cache");try{let t=!0,i=typeof l.cache=="boolean"?{}:l.cache,u=yield de(ge(e,r,i),i),m=yield u.loadFromCache();m!=null&&m.length&&W(()=>{y("loading from cache",m),ne(m,!1),h.emit("cache loaded",m)}),j("flags loaded",v=>E(void 0,null,function*(){yield u.saveToCache(v),t=!1})),j("changed",v=>E(void 0,null,function*(){t||(v.deleted?yield u.removeCachedEvaluation(v.flag):yield u.updateCachedEvaluation(v))}))}catch(t){C("Cache error: ",t),h.emit("cache error",t),h.emit("error",t)}}}),Ee=(t,i)=>E(void 0,null,function*(){let u=`${i.baseUrl}/client/auth`,m={method:"POST",headers:{"Content-Type":"application/json","Harness-SDK-Info":fe},body:JSON.stringify({apiKey:t,target:F(I({},e),{identifier:String(e.identifier)})})},v,o;window.AbortController&&l.authRequestReadTimeout>0?(o=new AbortController,m.signal=o.signal,v=window.setTimeout(()=>o.abort(),i.authRequestReadTimeout)):i.authRequestReadTimeout>0&&ve("AbortController is not available, auth request will not timeout");try{let f=yield M(u,m);if(!f.ok)throw new Error(`${f.status}: ${f.statusText}`);return(yield f.json()).authToken}catch(f){if(o&&o.signal.aborted)throw new Error(`Request to ${u} failed: Request timeout via configured authRequestTimeout of ${l.authRequestReadTimeout}`);let w=f instanceof Error?f.message:String(f);throw new Error(`Request to ${u} failed: ${w}`)}finally{v&&clearTimeout(v)}}),K=0,U=()=>{if(O())if(A.length){y("Sending metrics...",{metrics:A,evaluations:S});let t={metricsData:A.map(i=>({timestamp:Date.now(),count:i.count,metricsType:"FFMETRICS",attributes:[{key:"featureIdentifier",value:i.featureIdentifier},{key:"featureName",value:i.featureIdentifier},{key:"variationIdentifier",value:i.variationIdentifier},{key:"target",value:e.identifier},{key:"SDK_NAME",value:"JavaScript"},{key:"SDK_LANGUAGE",value:"JavaScript"},{key:"SDK_TYPE",value:"client"},{key:"SDK_VERSION",value:he}]}))};M(`${l.eventUrl}/metrics/${s}?cluster=${c}`,{method:"POST",headers:I({"Content-Type":"application/json"},b),body:JSON.stringify(t)}).then(()=>{A=[],K=0}).catch(i=>{K++&&(A=[],K=0),y(i),h.emit("metrics error",i)}).finally(()=>{d=window.setTimeout(U,l.eventsSyncInterval)})}else d=window.setTimeout(U,l.eventsSyncInterval)},S={},Re=t=>{y("Sending event for",t.flag),z?h.emit("changed",new Proxy(t,{get(i,u){var m;if(O()&&i.hasOwnProperty(u)&&u==="value"){let v=i.flag,o=t.value,f=A.find(w=>w.featureIdentifier===v&&w.featureValue===o);f?(B(f),f.variationIdentifier=((m=S[v])==null?void 0:m.identifier)||""):A.push({featureIdentifier:v,featureValue:String(o),variationIdentifier:S[v].identifier||"",count:1,lastAccessed:Date.now()}),y("Metrics event: Flag",u,"has been read with value via stream update",o)}return u==="value"?G(t):t[u]}})):h.emit("changed",{deleted:t.deleted,flag:t.flag,value:G(t)})},ee=function(){return z?new Proxy({},{get(t,i){var m,v,o;let u=t[i];if(O()&&t.hasOwnProperty(i)){let f=t[i],w=A.find(re=>re.featureIdentifier===i&&f===re.featureValue);w?(w.variationIdentifier=((m=S[i])==null?void 0:m.identifier)||"",B(w)):A.push({featureIdentifier:i,featureValue:f,variationIdentifier:((v=S[i])==null?void 0:v.identifier)||"",count:1,lastAccessed:Date.now()}),y("Metrics event: Flag:",i,"has been read with value:",f,"variationIdentifier:",(o=S[i])==null?void 0:o.identifier)}return u}}):{}},T=ee();pe().then(()=>Ee(r,l).then(t=>E(void 0,null,function*(){if(a)return;R=t;let i=ke(t);b={Authorization:`Bearer ${R}`,"Harness-AccountID":i.accountID,"Harness-EnvironmentID":i.environmentIdentifier,"Harness-SDK-Info":fe};let u=le(e);u.length<262144&&(b["Harness-Target"]=u),y("Authenticated",i),O()&&(d=window.setTimeout(U,l.eventsSyncInterval)),s=i.environment,c=i.clusterIdentifier;let m=!!Object.keys(S).length;if((yield V()).type==="success"&&y("Fetch all flags ok",T),!a){if(l.streamEnabled?(y("Streaming mode enabled"),be()):l.pollingEnabled?(y("Polling mode enabled"),ye()):y("Streaming and polling mode disabled"),!m){$();let o=I({},T);q(),h.emit("ready",o)}Z=!0}})).catch(t=>{C("Authentication error: ",t),h.emit("auth error",t),h.emit("error",t)}));let V=()=>E(void 0,null,function*(){try{let t=yield M(`${l.baseUrl}/client/env/${s}/target/${e.identifier}/evaluations?cluster=${c}`,{headers:b});if(t.ok){let i=k(yield t.json());return i.forEach(P),h.emit("flags loaded",i),{type:"success",data:i}}else return C("Features fetch operation error: ",t),h.emit("fetch flags error",t),h.emit("error",t),{type:"error",error:t}}catch(t){return C("Features fetch operation error: ",t),h.emit("fetch flags error",t),h.emit("error",t),{type:"error",error:t}}}),te=t=>E(void 0,null,function*(){try{let i=yield M(`${l.baseUrl}/client/env/${s}/target/${e.identifier}/evaluations/${t}?cluster=${c}`,{headers:b});if(i.ok){let u=yield i.json();P(u)}else C("Feature fetch operation error: ",i),h.emit("fetch flag error",i),h.emit("error",i)}catch(i){C("Feature fetch operation error: ",i),h.emit("fetch flag error",i),h.emit("error",i)}}),P=t=>{$();let i=G(t);i!==T[t.flag]&&(y("Flag variation has changed for ",t.identifier),T[t.flag]=i,S[t.flag]=F(I({},t),{value:i}),Re(t)),q()};p=new N(V,l,h,y,C);let be=()=>{let t=o=>{switch(o.event){case"create":u(o.evaluations)?o.evaluations.forEach(f=>{P(f)}):setTimeout(()=>te(o.identifier),1e3);break;case"patch":u(o.evaluations)?o.evaluations.forEach(f=>{P(f)}):te(o.identifier);break;case"delete":delete T[o.identifier],h.emit("changed",{flag:o.identifier,value:void 0,deleted:!0}),y("Evaluation deleted",{message:o,storage:T});break}},i=o=>!(!o||!o.flag||!o.identifier||!o.kind||!o.value),u=o=>!(!o||o.length==0||!o.every(f=>i(f))),m=o=>{o.event==="patch"&&(u(o.evaluations)?o.evaluations.forEach(f=>{P(f)}):V())},v=`${l.baseUrl}/stream?cluster=${c}`;g=new L(h,l,v,r,b,p,y,C,o=>{o.domain==="flag"?t(o):o.domain==="target-segment"&&m(o)},l.maxStreamRetries,D),g.start()},ye=()=>{p.start()},j=(t,i)=>h.on(t,i),Ie=(t,i)=>{t?h.off(t,i):ie()},Se=(t,i)=>{var o;if(!O()||z||i===void 0)return;let u=i,m=t,v=A.find(f=>f.featureIdentifier===m&&f.featureValue===u);v?(B(v),v.variationIdentifier=((o=S[m])==null?void 0:o.identifier)||""):A.push({featureIdentifier:m,featureValue:u,count:1,variationIdentifier:S[m].identifier||"",lastAccessed:Date.now()})},ie=()=>{a=!0,l.streamEnabled&&(y("Closing event stream"),typeof(g==null?void 0:g.close)=="function"&&g.close(),h.all.clear()),l.pollingEnabled&&p.isPolling()&&(y("Closing Poller"),p.stop()),T=ee(),S={},clearTimeout(d)},ne=(t,i=!0)=>{t.length&&W(()=>{let u=!!Object.keys(S).length;if(t.forEach(P),!u){$();let m=I({},T);q(),h.emit("ready",m)}},i)};return{on:j,off:Ie,close:ie,setEvaluations:ne,registerAPIRequestMiddleware:t=>{D=t,M=J(t),g&&g.registerAPIRequestMiddleware(t)},refreshEvaluations:()=>{Z&&!a&&Date.now()-Q>=6e4&&(V(),Q=Date.now())},variation:(t,i,u=!1)=>ue(t,i,T,Se,h,u)}};export{x as Event,yt as initialize};