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