@configdirector/server-sdk 0.2.1 → 0.2.2

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.
@@ -1,3 +1,3 @@
1
1
  Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let c=require(`node:events`);c=s(c);let l=require(`@jsonjoy.com/json-pointer`),u=require(`semver`);u=s(u);let d=require(`rapidhash-js`);var f=class e extends Error{constructor(t,n){super(t),this.name=`ConfigDirectorConnectionError`,this.status=n,Object.setPrototypeOf(this,e.prototype)}},p=class e extends Error{constructor(t){super(t),this.name=`ConfigDirectorValidationError`,Object.setPrototypeOf(this,e.prototype)}},m=class e extends Error{constructor(t){super(t),this.name=`StreamClosedError`,Object.setPrototypeOf(this,e.prototype)}},h=class e extends Error{constructor(t){super(t),this.name=`MissingResponseBodyError`,Object.setPrototypeOf(this,e.prototype)}},g=class e extends Error{constructor(t){super(t),this.name=`InvalidOptionError`,Object.setPrototypeOf(this,e.prototype)}},_=class e extends Error{constructor(t){super(t),this.name=`ValueOutOfRangeError`,Object.setPrototypeOf(this,e.prototype)}},v=class{constructor(e){this.isFirstChunk=!0,this.previousIncompleteLine=``,this.currentEvent={data:``},this.onEvent=e.onEvent??(()=>{}),this.onRetry=e.onRetry,this.onComment=e.onComment}parse(e){let t=this.prepareChunk(e),n=this.splitLines(t);for(let e of n)this.parseAndProcessLine(e)}finish(){this.previousIncompleteLine!==``&&this.parseAndProcessLine(this.previousIncompleteLine)}parseAndProcessLine(e){if(e.startsWith(`:`)){this.onComment?.(this.getValueFromLine(0,e));return}if(e===``){this.publishEvent();return}let t=this.parseContentLine(e);this.processContent(t)}publishEvent(){let e=this.currentEvent.data.endsWith(`
2
2
  `)?this.currentEvent.data.slice(0,-1):this.currentEvent.data;e&&this.onEvent({...this.currentEvent,id:this.lastEventId,data:e}),this.currentEvent={data:``}}processContent(e){switch(e.field){case`event`:this.currentEvent.type=e.value;break;case`data`:this.currentEvent.data+=e.value+`
3
- `;break;case`id`:e.value.includes(`\0`)||(this.lastEventId=e.value);break;case`retry`:/^\d+$/.test(e.value)&&this.onRetry?.(Number.parseInt(e.value,10));break;default:break}}parseContentLine(e){let t=e.indexOf(`:`);return t===-1?{field:e,value:``}:{field:e.slice(0,t),value:this.getValueFromLine(t,e)}}splitLines(e){let t=e.split(/\r\n|\r|\n/);return this.previousIncompleteLine=t.pop()??``,t}getValueFromLine(e,t){if(e===-1)return``;let n=t[e+1]===` `?e+2:e+1;return t.slice(n)}prepareChunk(e){return this.isFirstChunk?(this.isFirstChunk=!1,this.previousIncompleteLine+e.replace(/^\xEF\xBB\xBF/,``).replace(/^\uFEFF/,``)):this.previousIncompleteLine+e}};let y=function(e){return e.OPEN=`open`,e.CLOSED=`closed`,e.CONNECTING=`connecting`,e}({});var b=class{constructor(e){this.reconnectAttempt=0,this.abortController=new AbortController,this._readyState=y.CLOSED,this.serverReconnectionTime=2e3,this.url=new URL(e.url),this.lastEventId=e.lastEventId,this.headers={Accept:`text/event-stream`,...e.headers},this.requestOptions={body:e.body,method:e.method??`GET`,mode:e.mode,credentials:e.credentials,redirect:e.redirect,referrer:e.referrer,referrerPolicy:e.referrerPolicy},this._onError=this.getValidatedFunctionOrNoOp(e.onError,`onError`),this._onConnect=this.getValidatedFunctionOrNoOp(e.onConnect,`onConnect`),this._onDisconnect=this.getValidatedFunctionOrNoOp(e.onDisconnect,`onDisconnect`),this._onMessage=this.getValidatedFunctionOrNoOp(e.onMessage,`onMessage`),this._onComment=this.getValidatedFunctionOrNoOp(e.onComment,`onComment`),this._shouldReconnect=this.getValidatedFunctionOr(e.shouldReconnect,()=>!0,`shouldReconnect`),this._calculateReconnectDelay=this.getValidatedFunctionOr(e.calculateReconnectDelay,()=>this.serverReconnectionTime,`calculateReconnectDelay`)}set onMessage(e){this._onMessage=this.getValidatedFunctionOrNoOp(e,`onMessage`)}set onComment(e){this._onComment=this.getValidatedFunctionOrNoOp(e,`onComment`)}set onConnect(e){this._onConnect=this.getValidatedFunctionOrNoOp(e,`onConnect`)}set onDisconnect(e){this._onDisconnect=this.getValidatedFunctionOrNoOp(e,`onDisconnect`)}get readyState(){return this._readyState}connect(){this._readyState===y.CLOSED&&(this.reconnectAttempt=0,this.initiateConnection())}initiateConnection(){this.abortController=new AbortController,this._readyState=y.CONNECTING,fetch(this.url,{...this.requestOptions,headers:this.buildHeaders(),signal:this.abortController.signal,cache:`no-store`}).then(this.handleFetchResponse.bind(this)).catch(e=>{if(e.name===`AbortError`||this.abortController.signal.aborted){this.close();return}this._onError(e),this.scheduleReconnect({error:e})})}close(){clearTimeout(this.reconnectTimeout),this._readyState=y.CLOSED,this.abortController.abort()}disconnected(){clearTimeout(this.reconnectTimeout),this._readyState=y.CLOSED,this._onDisconnect()}buildHeaders(){let e=this.lastEventId?{"Last-Event-ID":this.lastEventId}:void 0;return{...this.headers,...e}}async handleFetchResponse(e){let{body:t,status:n}=e;if(n===204){this.disconnected();return}if(n>=400){this.scheduleReconnect({status:n});return}if(!t)throw new h(`The server response did not have a body`);this._readyState=y.OPEN,this._onConnect(),this.reconnectAttempt=0;let r=(t?.pipeThrough(new TextDecoderStream)).getReader(),i=new v({onEvent:e=>{typeof e.id==`string`&&(this.lastEventId=e.id),this._onMessage(e)},onComment:e=>this._onComment(e),onRetry:e=>this.serverReconnectionTime=e}),a=!1;for(;!a;){let{done:e,value:t}=await r.read();t&&i.parse(t),a=e}i.finish(),this.scheduleReconnect({status:n,error:new m(`The server response stream was closed`)})}scheduleReconnect(e){if(this.abortController.signal.aborted)return;clearTimeout(this.reconnectTimeout),this.reconnectAttempt+=1;let t={attempt:this.reconnectAttempt,status:e.status,error:e.error,serverReconnectionTime:this.serverReconnectionTime};if(this._shouldReconnect(t)){this._readyState=y.CONNECTING;let e=this._calculateReconnectDelay(t);(!Number.isFinite(e)||e<1||e>1e3*60*60)&&(this._onError(new _(`The calculated reconnect delay is out of range: ${e}. Defaulting to ${this.serverReconnectionTime}`)),e=this.serverReconnectionTime),this.reconnectTimeout=setTimeout(()=>this.initiateConnection(),e)}else{this.disconnected();return}}getValidatedFunctionOrNoOp(e,t){return this.getValidatedFunctionOr(e,()=>{},t)}getValidatedFunctionOr(e,t,n){if(e==null)return t;if(typeof e==`function`)return e;throw new g(`${n} must be a function`)}};const x=e=>new b(e);var S=class{constructor(e){this.options=e,this.eventEmitter=new c.EventEmitter,this.options=e,this.logger=e.logger,this.url=new URL(`server/sse/v1`,e.baseUrl)}async connect(e){this.eventSource&&this.close();let t=new Promise((e,t)=>{this.eventSource=x({url:this.url,method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({metaContext:this.options.metaContext,serverSdkKey:this.options.serverSdkKey}),onMessage:({data:e})=>{this.dispatchMessage(e)},onConnect:()=>{this.logger.debug(`[EventSourceTransport] Connected`),e(this)},shouldReconnect:e=>{let n=!this.isStatusFatal(e.status);return n||t(this.prepareFatalError(e.status,e.error)),n},calculateReconnectDelay:e=>{let t=2e3*e.attempt;return this.logger.warn(`[EventSourceTransport] Scheduling reconnect in ${t}ms.`),t},onDisconnect:()=>{this.logger.debug(`[EventSourceTransport] Disconnected`)}}),this.eventSource.connect()});return Promise.race([t,new Promise(t=>{setTimeout(()=>t(this),e)})])}dispatchMessage(e){try{let t=JSON.parse(e);this.eventEmitter.emit(`configBundleReceived`,t)}catch(e){this.logger.error(`[EventSourceTransport] Error parsing and dispatching config data update: `,e)}}prepareFatalError(e,t){let n=e??0;return new f(`${`Connection failed with status: ${e??`unknown`}.`}${t?` Error: ${t}.`:``} This is an unrecoverable error, will not attempt to reconnect.`,n)}isStatusFatal(e){return!!e&&e>=400&&e<500}get isConnected(){return this.eventSource?.readyState==`open`}on(e,t){this.eventEmitter.on(e,t)}off(e,t){t?this.eventEmitter.off(e,t):this.eventEmitter.removeAllListeners(e)}clear(){this.eventEmitter.removeAllListeners()}close(){this.eventSource?.close()}dispose(){this.close(),this.clear()}};const C=(e,t,n)=>{switch(t.toLowerCase()){case`=`:case`equals`:return e==n[0];case`!=`:case`does not equal`:return e!=n[0];case`is one of`:return n.includes(e);case`is not one of`:return!n.includes(e);case`starts with any of`:return n.findIndex(t=>e?.startsWith(t))>=0;case`does not start with any of`:return n.findIndex(t=>e?.startsWith(t))<0;case`ends with any of`:return n.findIndex(t=>e?.endsWith(t))>=0;case`does not end with any of`:return n.findIndex(t=>e?.endsWith(t))<0;case`matches regex`:return w(n[0],e);case`does not match regex`:return!w(n[0],e);default:return!1}},w=(e,t)=>{try{return new RegExp(e).test(t)}catch{return!1}},T=(e,t,n)=>{if(typeof e==`boolean`)return!1;let r=t.toLowerCase();if(e==null)return r==`!=`||r==`does not equal`;let{value:i,target:a}=E(e,n[0]);switch(r){case`=`:case`equals`:return i==a;case`!=`:case`does not equal`:return i!=a;case`<`:return i<a;case`<=`:return i<=a;case`>`:return i>a;case`>=`:return i>=a;default:return!1}},E=(e,t)=>typeof e==`number`?{value:e,target:D(t)}:typeof e==`bigint`?{value:e,target:BigInt(t)}:{value:D(e),target:D(t)},D=e=>{let t=Number.parseFloat(e.toString());if(isNaN(t)||!isFinite(t))throw TypeError(`Invalid numeric value: ${e}`);return t},O=(e,t,n)=>{if(e==null)return!1;try{let r=new Date(e),i=new Date(n[0]);switch(t){case`is after`:return r>i;case`is before`:return r<i;default:return!1}}catch{return!1}},k=(e,t,n)=>{let r=t.toLowerCase();if(e==null||e.trim().length==0)return r==`is not one of`;let i=u.coerce(e)??e,a=n.map(e=>u.coerce(e)??e);try{switch(r){case`=`:return A(i,a[0]);case`<`:return u.lt(i,a[0]);case`<=`:return u.lte(i,a[0]);case`>`:return u.gt(i,a[0]);case`>=`:return u.gte(i,a[0]);case`is one of`:return a.findIndex(e=>A(i,e))>=0;case`is not one of`:return a.findIndex(e=>A(i,e))<0;default:return!1}}catch{return!1}},A=(e,t)=>{try{return u.eq(e,t)}catch{return!1}},j=(e,t,n)=>{let r=t.toLowerCase();if(!Array.isArray(e))return r==`does not contain any of`;switch(r){case`contains any of`:return e.some(e=>n.includes(e));case`does not contain any of`:return!e.some(e=>n.includes(e));default:return!1}};var M=class{evaluate(e,t){if(!this.isConditionValidForComparison(e))return!1;switch(e.attribute){case`identifier`:return this.compare(t?.context?.id,e);case`name`:return this.compare(t?.context?.name,e);case`appName`:return this.compare(t?.metadata?.appName,e);case`appVersion`:return this.compare(t?.metadata?.appVersion,e);case`traits`:if(e.trait){let n=this.findTraitValue(t?.context?.traits,e.trait);return this.compare(n,e)}}return!1}compare(e,t){switch(t.targetType){case`text`:return C(e?.toString(),t.operator,t.targetValues);case`number`:return T(e,t.operator,t.targetValues);case`datetime`:return O(e?.toString(),t.operator,t.targetValues);case`semver`:return k(e?.toString(),t.operator,t.targetValues);case`array`:return j(e,t.operator,t.targetValues)}return!1}findTraitValue(e,t){try{return(0,l.findByPointer)(t,e).val}catch{return}}isConditionValidForComparison(e){return Array.isArray(e.targetValues)&&e.targetValues.length>0}};const N=e=>{let t=`${e.contextIdentifier}-${e.configId}`;return Number((0,d.rapidhash)(t,{seed:15426857351n})%1000n)/10};var P=class{constructor(e){if(this.logger=e,this.conditionEvaluator=new M,!e||!e.warn||!e.error||!e.info)throw TypeError(`The provided logger is not a valid 'ConfigDirectorLogger'`);this.logger=e}evaluate(e,t){return{id:e.id,key:e.key,type:e.type,value:this.getConfigValue(e,t)}}getConfigValue(e,t){let n=[...e.target?.rules??[]].sort((e,t)=>(e.order??2**53-1)-(t.order??2**53-1));for(let r of n){let n=this.evaluateRule(r,e,t);if(n.success)return n.value}return e.target?.defaultValue}evaluateRule(e,t,n){try{if(e.type==`percentage`)return this.evaluatePercentageRule(e,t,n);if(e.type==`conditional`)return this.evaluateConditionalRule(e,t,n)}catch(n){this.logger.warn(`There was an error while evaluating a targeting rule '${e?.id}' for '${t?.key}'. The rule will be disregarded.`,{error:n,configKey:t?.key,ruleId:e?.id})}return{success:!1}}evaluatePercentageRule(e,t,n){return this.evaluatePercentage(e.percentages??[],t,n)}evaluatePercentage(e,t,n){let r=N({configId:t.id,contextIdentifier:n?.context?.id??crypto.randomUUID()}),i=0,a=0,o;for(;!o&&i<e.length;)r<=e[i].percentage+a&&(o=e[i]),a+=e[i].percentage,i+=1;return o?.value==null?{success:!1}:{success:!0,value:o.value.toString()}}evaluateConditionalRule(e,t,n){let r=e.conditions??[],i=0,a;for(;!a&&i<r.length;)this.conditionEvaluator.evaluate(r[i],n)&&(a=r[i]),i+=1;return a&&e.target==`value`&&e.value!=null?{success:!0,value:e.value.toString()}:a&&e.target==`percentage`?this.evaluatePercentage(e.percentages??[],t,n):{success:!1}}};const F=e=>{let t=typeof e;if(t===`object`)try{return e.constructor?.name??t}catch{return t}else if(t===`function`)try{let n=e.name;return n?`function: ${n}`:t}catch{return t}return t},I=e=>e===`number`||e===`bigint`,L=(e,t)=>{let n=e.value,r=F(t);if(!n)return{parsedValue:t,requestedType:r,usedDefault:!0,reason:`value-missing`};if(typeof t==`string`)return{parsedValue:n,requestedType:r,usedDefault:!1,reason:`found-match`};if(typeof t==`boolean`&&e.type===`boolean`){let e=R(n),i=typeof e==`boolean`;return{parsedValue:i?e:t,requestedType:r,usedDefault:!i,reason:i?`found-match`:`invalid-boolean`}}if(I(typeof t)&&e.type===`integer`){let e=ee(n),i=typeof e==`number`;return{parsedValue:i?e:t,requestedType:r,usedDefault:!i,reason:i?`found-match`:`invalid-number`}}if(I(typeof t)&&(e.type===`float`||e.type===`enum`)){let e=z(n),i=typeof e==`number`;return{parsedValue:i?e:t,requestedType:r,usedDefault:!i,reason:i?`found-match`:`invalid-number`}}return{parsedValue:n,requestedType:r,usedDefault:!1,reason:`found-match`}},R=e=>{if(!e)return;let t=e.toLowerCase();if(!(t!=`true`&&t!=`false`))return t===`true`},ee=e=>{if(!e)return;let t=Number.parseInt(e);if(!(isNaN(t)||!isFinite(t)))return t},z=e=>{if(!e)return;let t=Number.parseFloat(e);if(!(isNaN(t)||!isFinite(t)))return t},B={off:-1,error:0,warn:1,info:2,debug:3},V=()=>{let e={year:`numeric`,month:`2-digit`,day:`2-digit`,hour:`2-digit`,minute:`2-digit`,second:`2-digit`,timeZoneName:`short`};try{return new Intl.DateTimeFormat(void 0,{...e,fractionalSecondDigits:3})}catch{return new Intl.DateTimeFormat(void 0,e)}};var H=class{constructor(e,t){this.level=e,this.decorator=t,this.level=e,this.decorator=t,this.dateFormatter=V()}debug(e,...t){this.log(console.debug,`debug`,e,...t)}info(e,...t){this.log(console.info,`info`,e,...t)}warn(e,...t){this.log(console.warn,`warn`,e,...t)}error(e,...t){this.log(console.error,`error`,e,...t)}log(e,t,n,...r){B[this.level]>=B[t]&&e(`[${this.dateFormatter.format(new Date)}] ${this.decorator?.decorateMessage(n)}`,...r)}},U=class{decorateMessage(e){return`[ConfigDirector:js-server-sdk] ${e}`}};const W=(e,t)=>new H(e??`warn`,t??new U),G=e=>K(new TextEncoder().encode(e)).toString(16).padStart(8,`0`),K=e=>{let t=5381;for(let n=0;n<e.length;n++)t=(t<<5)+t+e[n],t>>>=0;return t},q=e=>{let t=Object.keys(e);if(t.length==0)return!0;for(let n of t)if((e[n]?.length??0)>0)return!1;return!0},J=e=>{if(!e)return!0;let t=Object.keys(e);if(t.length==0)return!0;for(let n of t)if((e[n]??0)>0)return!1;return!0},Y=async(e,t,n,r)=>{let i=new AbortController,a=setTimeout(()=>{r.debug(`[fetchWithTimeout] Reached timeout, aborting request`),i.abort()},e);try{let e=await fetch(t,{...n,signal:i.signal});return clearTimeout(a),e}catch(e){throw r.warn(`[fetchWithTimeout] Fetch error: `,e),clearTimeout(a),e}};var X=class{constructor(e){this.executeRequests=!0,this.sdkKey=e.sdkKey,this.logger=e.logger,this.url=new URL(`server/telemetry/v1`,e.baseUrl)}async report({discreteEvents:e,aggregatedEvents:t,droppedEvents:n}){if(!this.executeRequests)return{success:!1,fatalError:!0};let r={serverSdkKey:this.sdkKey,discreteEvents:e,aggregatedEvents:t,droppedEvents:n};if(this.isReportEmpty(r))return{success:!0,fatalError:!1};let i=await this.sendReport(r);return i.fatalError&&(this.executeRequests=!1),i}isReportEmpty(e){return q(e.discreteEvents)&&q(e.aggregatedEvents)&&J(e.droppedEvents)}async sendReport(e){try{let t=await Y(5e3,this.url,{method:`POST`,body:JSON.stringify(e)},this.logger),n=this.isStatusFatal(t.status);return n&&this.logger.warn(`[EventReporter] Received a fatal status response (${t.status}) from the telemetry endpoint. No more telemetry data will be sent.`),{success:t.ok,fatalError:n}}catch(e){return this.logger.warn(`[EventReporter] Error attempting to send telemetry data: ${e}`,{error:e}),{success:!1,fatalError:!1}}}isStatusFatal(e){return!!e&&e>=400&&e<500}},Z=class{aggregate(e){if(e.events.length==0)return[];let t=new Map;for(let n of e.events){let e=JSON.stringify(n),r=t.get(e)?.count||0;t.set(e,{event:n,count:r+1})}return Array.from(t).map(([,t])=>({startTime:e.startTime,endTime:e.endTime,count:t.count,event:t.event}))}},Q=class{constructor(e){this._events=[],this._droppedEventCount=0,this.limit=e??1e3}get events(){return this._events}get reachedLimit(){return this._events.length>=this.limit}get droppedEventCount(){return this._droppedEventCount}push(...e){this.startTime||=new Date;let t=Math.max(0,e.length-this.limit),n=e.slice(t,e.length);if(this._droppedEventCount+=t,this._events.length+n.length>this.limit){let e=this._events.length+n.length-this.limit;this._events.splice(0,e),this._droppedEventCount+=e}return this._events.push(...n)}takeSnapshot(){let e=new Date,t=this.startTime??e,n=this._events.splice(0),r=this._droppedEventCount;return this.startTime=void 0,this._droppedEventCount=0,{startTime:t,endTime:e,events:n,droppedCount:r}}clear(){this._events=[],this.startTime=void 0,this._droppedEventCount=0}},$=class{constructor(e){this.aggregator=new Z,this.collectEvents=!0,this.logger=e.logger,this.evaluationEventQueue=new Q(e.evaluationQueueLimit??1e3),this.flushIntervalDelay=e.flushIntervalDelay??3e4;let t=e.initialFlushIntervalDelay??5e3;this.flushTimeout=setTimeout(()=>this.flushAndScheduleNext(),t)}sanitizeValue(e,t){if(t===`json`)try{return G(JSON.stringify(e))}catch{return e.toString().slice(0,500)}return e.toString().slice(0,500)}async flushAndScheduleNext(){(await this.flush()).fatalError?(this.collectEvents=!1,this.close(),this.logger.warn(`[TelemetryEventCollector] Received a fatal error from telemetry collection. No longer collecting events.`)):this.flushTimeout=setTimeout(()=>this.flushAndScheduleNext(),this.flushIntervalDelay)}async flush(){if(!this.reporter)return{success:!1,fatalError:!1};let e=this.evaluationEventQueue.takeSnapshot();return await this.reporter?.report({context:this._context,discreteEvents:{},aggregatedEvents:{evaluatedConfig:this.aggregator.aggregate(e)},droppedEvents:{evaluatedConfig:e.droppedCount}})}cancelScheduledFlush(){clearTimeout(this.flushTimeout)}close(){this.collectEvents=!1,clearTimeout(this.flushTimeout),this.flush(),this.evaluationEventQueue.clear()}dispose(){this.close()}},te=class extends Map{constructor(e,t){super(t),this._droppedCount=0,this.limit=e,this.enforceLimit()}set(e,t){return super.set(e,t),this.enforceLimit(),this}get droppedCount(){return this._droppedCount}clearAndReset(){this.clear(),this._droppedCount=0}enforceLimit(){for(;this.size>this.limit;){let e=this.keys().next().value;e!==void 0&&(this.delete(e),this._droppedCount+=1)}}},ne=class extends ${constructor(e){super(e),this.reporter=new X(e),this.contexts=new te(e.contextLimit??1e3)}evaluatedConfig(e){if(!this.collectEvents)return;e.context?.id&&this.contexts.set(e.context.id,e.context);let t={...e.evaluation,contextId:e.context?.id};this.evaluationEventQueue.push(this.sanitizeEvaluatedConfigEvent(t))}sanitizeEvaluatedConfigEvent(e){return{...e,defaultValue:this.sanitizeValue(e.defaultValue,e.type),evaluatedValue:this.sanitizeValue(e.evaluatedValue,e.type)}}snapshotContexts(){let e=Array.from(this.contexts.values()),t=this.contexts.droppedCount;return this.contexts.clearAndReset(),{contexts:e,droppedCount:t}}async flush(){if(!this.reporter)return{success:!1,fatalError:!1};let e=this.evaluationEventQueue.takeSnapshot(),t=this.snapshotContexts();return await this.reporter?.report({context:this._context,discreteEvents:{capturedContexts:t.contexts},aggregatedEvents:{evaluatedConfig:this.aggregator.aggregate(e)},droppedEvents:{evaluatedConfig:e.droppedCount,capturedContexts:t.droppedCount}})}};const re=new URL(`https://server-sdk-api.configdirector.com`);var ie=class{constructor(e,t,n){this.handlersMap=new Map,this.eventEmitter=new c.default,this.ready=!1,this.logger=n?.logger??W(),this.timeout=n?.connection?.timeout??3e3,this.metaContext=n?.metadata??{},this.configEvaluator=new P(this.logger);let r=this.parseUrl(n?.connection?.url)??re;this.streaming=n?.connection?.streaming!==!1;let i=S,a=n?.telemetry?.eventQueueLimit??5e3;this.usageEventCollector=new ne({sdkKey:e,logger:this.logger,baseUrl:r,flushIntervalDelay:n?.telemetry?.flushInterval??3e4,evaluationQueueLimit:Math.ceil(a*.7),contextLimit:Math.floor(a*.3)}),this.transport=new i({serverSdkKey:e,baseUrl:r,metaContext:{...n?.metadata,sdkName:t.sdkName,sdkVersion:t.sdkVersion},logger:this.logger}),this.transport.on(`configBundleReceived`,e=>{this.readyResolve?.();let t=Object.keys(e.configs);!this.configSet||e.kind==`full`?(this.configSet=e,this.eventEmitter.emit(`configsUpdated`,{keys:t}),this.updateWatchers(e.configs)):(this.configSet.configs={...this.configSet.configs,...e.configs},this.eventEmitter.emit(`configsUpdated`,{keys:t}),this.updateWatchers(e.configs)),this.logger.debug(`[ConfigDirectorClient] ConfigSet updated from server with ${t.length} key(s)`,{keys:t})})}async initialize(){try{this.ready=!1,this.readyPromise=new Promise(e=>{this.readyResolve=e}).then(()=>{this.ready=!0,this.eventEmitter.emit(`clientReady`),this.logger.debug(`[ConfigDirectorClient] Received initial payload from the server, client is ready`)});let e=new Date().getTime();await this.transport.connect(this.timeout);let t=new Date().getTime()-e,n=this.timeout-t;if(n>0&&await Promise.race([this.readyPromise,new Promise(e=>{setTimeout(()=>e(),n)})]),!this.ready){let e=this.streaming?`The client will continue to retry since there were no fatal errors detected. Configs will return the default value until the connection succeeds.`:`Since the client was configured without streaming, configs may not update and always return the default value.`;this.logger.warn(`[ConfigDirectorClient] Timed out waiting for initialization after ${this.timeout}ms. ${e}`)}}catch(e){this.logger.error(`[ConfigDirectorClient] An error occurred during initialization: `,e)}}updateWatchers(e){Object.values(e).forEach(e=>this.updateWatchersForConfig(e))}updateWatchersForConfig(e){this.handlersMap.get(e.key)?.forEach(t=>{let n=this.getValueFromConfigDefinition(e.key,e,t.defaultValue,t.context);t.handler(n)})}watch(e,t,n,r){this.validateDefaultValue(t);let i=this.handlersMap.get(e),a={handler:n,defaultValue:t,requestedType:typeof t,context:r};return i?i.push(a):this.handlersMap.set(e,[a]),()=>this.unwatch(e,n)}unwatch(e,t){let n=this.handlersMap.get(e);if(n)if(t){let e=n.findIndex(e=>e.handler==t);e>=0&&n?.splice(e,1)}else n.splice(0)}getValue(e,t,n){this.validateDefaultValue(t);let r=this.configSet?.configs[e];return this.getValueFromConfigDefinition(e,r,t,n)}getValueFromConfigDefinition(e,t,n,r){if(t){let i=L(this.configEvaluator.evaluate(t,{context:r,metadata:this.metaContext}),n);return this.usageEventCollector.evaluatedConfig({context:r,evaluation:{key:e,defaultValue:n,requestedType:i.requestedType,evaluatedValue:i.parsedValue,usedDefault:i.usedDefault,evaluationReason:i.reason}}),this.logger.debug(`[ConfigDirectorClient] Evaluated '${e}' to '${i.parsedValue}'`),i.parsedValue}else return this.logger.debug(`[ConfigDirectorClient] No config state found for '${e}', returning default value '${n}'`),this.usageEventCollector.evaluatedConfig({context:r,evaluation:{key:e,defaultValue:n,requestedType:F(n),evaluatedValue:n,usedDefault:!0,evaluationReason:`config-state-missing`}}),n}parseUrl(e){if(e)try{return new URL(e)}catch(t){throw new p(`Invalid base URL '${e}'. Parsing failed: ${t}`)}}validateDefaultValue(e){if(e==null)throw new p(`Invalid default value. The default value for a config must be defined and non-null.`);if(typeof e==`function`)throw new p(`Invalid default value. The default value for a config cannot be a function.`)}get isReady(){return this.ready}on(e,t){this.eventEmitter.on(e,t)}off(e,t){t?this.eventEmitter.off(e,t):this.eventEmitter.removeAllListeners(e)}removeAllObservers(){this.logger.debug(`[ConfigDirectorClient] removeAllObservers() has been called, removing all observers`),this.eventEmitter.removeAllListeners(),this.handlersMap.clear()}unwatchAll(){this.handlersMap.clear()}closeConnection(){this.logger.debug(`[ConfigDirectorClient] closeConnection() has been called, closing connection to server`),this.transport.close(),this.ready=!1}dispose(){this.removeAllObservers(),this.closeConnection()}};const ae=(e,t)=>new ie(e,{sdkName:`js-server-sdk`,sdkVersion:`0.2.0`},t),oe=(e,t)=>W(e,t);exports.createClient=ae,exports.createConsoleLogger=oe;
3
+ `;break;case`id`:e.value.includes(`\0`)||(this.lastEventId=e.value);break;case`retry`:/^\d+$/.test(e.value)&&this.onRetry?.(Number.parseInt(e.value,10));break;default:break}}parseContentLine(e){let t=e.indexOf(`:`);return t===-1?{field:e,value:``}:{field:e.slice(0,t),value:this.getValueFromLine(t,e)}}splitLines(e){let t=e.split(/\r\n|\r|\n/);return this.previousIncompleteLine=t.pop()??``,t}getValueFromLine(e,t){if(e===-1)return``;let n=t[e+1]===` `?e+2:e+1;return t.slice(n)}prepareChunk(e){return this.isFirstChunk?(this.isFirstChunk=!1,this.previousIncompleteLine+e.replace(/^\xEF\xBB\xBF/,``).replace(/^\uFEFF/,``)):this.previousIncompleteLine+e}};let y=function(e){return e.OPEN=`open`,e.CLOSED=`closed`,e.CONNECTING=`connecting`,e}({});var b=class{constructor(e){this.reconnectAttempt=0,this.abortController=new AbortController,this._readyState=y.CLOSED,this.serverReconnectionTime=2e3,this.url=new URL(e.url),this.lastEventId=e.lastEventId,this.headers={Accept:`text/event-stream`,...e.headers},this.requestOptions={body:e.body,method:e.method??`GET`,mode:e.mode,credentials:e.credentials,redirect:e.redirect,referrer:e.referrer,referrerPolicy:e.referrerPolicy},this._onError=this.getValidatedFunctionOrNoOp(e.onError,`onError`),this._onConnect=this.getValidatedFunctionOrNoOp(e.onConnect,`onConnect`),this._onDisconnect=this.getValidatedFunctionOrNoOp(e.onDisconnect,`onDisconnect`),this._onMessage=this.getValidatedFunctionOrNoOp(e.onMessage,`onMessage`),this._onComment=this.getValidatedFunctionOrNoOp(e.onComment,`onComment`),this._shouldReconnect=this.getValidatedFunctionOr(e.shouldReconnect,()=>!0,`shouldReconnect`),this._calculateReconnectDelay=this.getValidatedFunctionOr(e.calculateReconnectDelay,()=>this.serverReconnectionTime,`calculateReconnectDelay`)}set onMessage(e){this._onMessage=this.getValidatedFunctionOrNoOp(e,`onMessage`)}set onComment(e){this._onComment=this.getValidatedFunctionOrNoOp(e,`onComment`)}set onConnect(e){this._onConnect=this.getValidatedFunctionOrNoOp(e,`onConnect`)}set onDisconnect(e){this._onDisconnect=this.getValidatedFunctionOrNoOp(e,`onDisconnect`)}get readyState(){return this._readyState}connect(){this._readyState===y.CLOSED&&(this.reconnectAttempt=0,this.initiateConnection())}initiateConnection(){this.abortController=new AbortController,this._readyState=y.CONNECTING,fetch(this.url,{...this.requestOptions,headers:this.buildHeaders(),signal:this.abortController.signal,cache:`no-store`}).then(this.handleFetchResponse.bind(this)).catch(e=>{if(e.name===`AbortError`||this.abortController.signal.aborted){this.close();return}this._onError(e),this.scheduleReconnect({error:e})})}close(){clearTimeout(this.reconnectTimeout),this._readyState=y.CLOSED,this.abortController.abort()}disconnected(){clearTimeout(this.reconnectTimeout),this._readyState=y.CLOSED,this._onDisconnect()}buildHeaders(){let e=this.lastEventId?{"Last-Event-ID":this.lastEventId}:void 0;return{...this.headers,...e}}async handleFetchResponse(e){let{body:t,status:n}=e;if(n===204){this.disconnected();return}if(n>=400){this.scheduleReconnect({status:n});return}if(!t)throw new h(`The server response did not have a body`);this._readyState=y.OPEN,this._onConnect(),this.reconnectAttempt=0;let r=(t?.pipeThrough(new TextDecoderStream)).getReader(),i=new v({onEvent:e=>{typeof e.id==`string`&&(this.lastEventId=e.id),this._onMessage(e)},onComment:e=>this._onComment(e),onRetry:e=>this.serverReconnectionTime=e}),a=!1;for(;!a;){let{done:e,value:t}=await r.read();t&&i.parse(t),a=e}i.finish(),this.scheduleReconnect({status:n,error:new m(`The server response stream was closed`)})}scheduleReconnect(e){if(this.abortController.signal.aborted)return;clearTimeout(this.reconnectTimeout),this.reconnectAttempt+=1;let t={attempt:this.reconnectAttempt,status:e.status,error:e.error,serverReconnectionTime:this.serverReconnectionTime};if(this._shouldReconnect(t)){this._readyState=y.CONNECTING;let e=this._calculateReconnectDelay(t);(!Number.isFinite(e)||e<1||e>1e3*60*60)&&(this._onError(new _(`The calculated reconnect delay is out of range: ${e}. Defaulting to ${this.serverReconnectionTime}`)),e=this.serverReconnectionTime),this.reconnectTimeout=setTimeout(()=>this.initiateConnection(),e)}else{this.disconnected();return}}getValidatedFunctionOrNoOp(e,t){return this.getValidatedFunctionOr(e,()=>{},t)}getValidatedFunctionOr(e,t,n){if(e==null)return t;if(typeof e==`function`)return e;throw new g(`${n} must be a function`)}};const x=e=>new b(e);var S=class{constructor(e){this.options=e,this.eventEmitter=new c.EventEmitter,this.options=e,this.logger=e.logger,this.url=new URL(`server/sse/v1`,e.baseUrl)}async connect(e){this.eventSource&&this.close();let t=new Promise((e,t)=>{this.eventSource=x({url:this.url,method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({metaContext:this.options.metaContext,serverSdkKey:this.options.serverSdkKey}),onMessage:({data:e})=>{this.dispatchMessage(e)},onConnect:()=>{this.logger.debug(`[EventSourceTransport] Connected`),e(this)},shouldReconnect:e=>{let n=!this.isStatusFatal(e.status);return n||t(this.prepareFatalError(e.status,e.error)),n},calculateReconnectDelay:e=>{let t=2e3*e.attempt;return this.logger.warn(`[EventSourceTransport] Scheduling reconnect in ${t}ms.`),t},onDisconnect:()=>{this.logger.debug(`[EventSourceTransport] Disconnected`)}}),this.eventSource.connect()});return Promise.race([t,new Promise(t=>{setTimeout(()=>t(this),e)})])}dispatchMessage(e){try{let t=JSON.parse(e);this.eventEmitter.emit(`configBundleReceived`,t)}catch(e){this.logger.error(`[EventSourceTransport] Error parsing and dispatching config data update: `,e)}}prepareFatalError(e,t){let n=e??0;return new f(`${`Connection failed with status: ${e??`unknown`}.`}${t?` Error: ${t}.`:``} This is an unrecoverable error, will not attempt to reconnect.`,n)}isStatusFatal(e){return!!e&&e>=400&&e<500}get isConnected(){return this.eventSource?.readyState==`open`}on(e,t){this.eventEmitter.on(e,t)}off(e,t){t?this.eventEmitter.off(e,t):this.eventEmitter.removeAllListeners(e)}clear(){this.eventEmitter.removeAllListeners()}close(){this.eventSource?.close()}dispose(){this.close(),this.clear()}};const C=(e,t,n)=>{switch(t.toLowerCase()){case`=`:case`equals`:return e==n[0];case`!=`:case`does not equal`:return e!=n[0];case`is one of`:return n.includes(e);case`is not one of`:return!n.includes(e);case`starts with any of`:return n.findIndex(t=>e?.startsWith(t))>=0;case`does not start with any of`:return n.findIndex(t=>e?.startsWith(t))<0;case`ends with any of`:return n.findIndex(t=>e?.endsWith(t))>=0;case`does not end with any of`:return n.findIndex(t=>e?.endsWith(t))<0;case`matches regex`:return w(n[0],e);case`does not match regex`:return!w(n[0],e);default:return!1}},w=(e,t)=>{try{return new RegExp(e).test(t)}catch{return!1}},T=(e,t,n)=>{if(typeof e==`boolean`)return!1;let r=t.toLowerCase();if(e==null)return r==`!=`||r==`does not equal`;let{value:i,target:a}=E(e,n[0]);switch(r){case`=`:case`equals`:return i==a;case`!=`:case`does not equal`:return i!=a;case`<`:return i<a;case`<=`:return i<=a;case`>`:return i>a;case`>=`:return i>=a;default:return!1}},E=(e,t)=>typeof e==`number`?{value:e,target:D(t)}:typeof e==`bigint`?{value:e,target:BigInt(t)}:{value:D(e),target:D(t)},D=e=>{let t=Number.parseFloat(e.toString());if(isNaN(t)||!isFinite(t))throw TypeError(`Invalid numeric value: ${e}`);return t},O=(e,t,n)=>{if(e==null)return!1;try{let r=new Date(e),i=new Date(n[0]);switch(t){case`is after`:return r>i;case`is before`:return r<i;default:return!1}}catch{return!1}},k=(e,t,n)=>{let r=t.toLowerCase();if(e==null||e.trim().length==0)return r==`is not one of`;let i=u.coerce(e)??e,a=n.map(e=>u.coerce(e)??e);try{switch(r){case`=`:return A(i,a[0]);case`<`:return u.lt(i,a[0]);case`<=`:return u.lte(i,a[0]);case`>`:return u.gt(i,a[0]);case`>=`:return u.gte(i,a[0]);case`is one of`:return a.findIndex(e=>A(i,e))>=0;case`is not one of`:return a.findIndex(e=>A(i,e))<0;default:return!1}}catch{return!1}},A=(e,t)=>{try{return u.eq(e,t)}catch{return!1}},j=(e,t,n)=>{let r=t.toLowerCase();if(!Array.isArray(e))return r==`does not contain any of`;switch(r){case`contains any of`:return e.some(e=>n.includes(e));case`does not contain any of`:return!e.some(e=>n.includes(e));default:return!1}};var M=class{evaluate(e,t){if(!this.isConditionValidForComparison(e))return!1;switch(e.attribute){case`identifier`:return this.compare(t?.context?.id,e);case`name`:return this.compare(t?.context?.name,e);case`appName`:return this.compare(t?.metadata?.appName,e);case`appVersion`:return this.compare(t?.metadata?.appVersion,e);case`traits`:if(e.trait){let n=this.findTraitValue(t?.context?.traits,e.trait);return this.compare(n,e)}}return!1}compare(e,t){switch(t.targetType){case`text`:return C(e?.toString(),t.operator,t.targetValues);case`number`:return T(e,t.operator,t.targetValues);case`datetime`:return O(e?.toString(),t.operator,t.targetValues);case`semver`:return k(e?.toString(),t.operator,t.targetValues);case`array`:return j(e,t.operator,t.targetValues)}return!1}findTraitValue(e,t){try{return(0,l.findByPointer)(t,e).val}catch{return}}isConditionValidForComparison(e){return Array.isArray(e.targetValues)&&e.targetValues.length>0}};const N=e=>{let t=`${e.contextIdentifier}-${e.configId}`;return Number((0,d.rapidhash)(t,{seed:15426857351n})%1000n)/10};var P=class{constructor(e){if(this.logger=e,this.conditionEvaluator=new M,!e||!e.warn||!e.error||!e.info)throw TypeError(`The provided logger is not a valid 'ConfigDirectorLogger'`);this.logger=e}evaluate(e,t){return{id:e.id,key:e.key,type:e.type,value:this.getConfigValue(e,t)}}getConfigValue(e,t){let n=[...e.target?.rules??[]].sort((e,t)=>(e.order??2**53-1)-(t.order??2**53-1));for(let r of n){let n=this.evaluateRule(r,e,t);if(n.success)return n.value}return e.target?.defaultValue}evaluateRule(e,t,n){try{if(e.type==`percentage`)return this.evaluatePercentageRule(e,t,n);if(e.type==`conditional`)return this.evaluateConditionalRule(e,t,n)}catch(n){this.logger.warn(`There was an error while evaluating a targeting rule '${e?.id}' for '${t?.key}'. The rule will be disregarded.`,{error:n,configKey:t?.key,ruleId:e?.id})}return{success:!1}}evaluatePercentageRule(e,t,n){return this.evaluatePercentage(e.percentages??[],t,n)}evaluatePercentage(e,t,n){let r=N({configId:t.id,contextIdentifier:n?.context?.id??crypto.randomUUID()}),i=0,a=0,o;for(;!o&&i<e.length;)r<=e[i].percentage+a&&(o=e[i]),a+=e[i].percentage,i+=1;return o?.value==null?{success:!1}:{success:!0,value:o.value.toString()}}evaluateConditionalRule(e,t,n){let r=e.conditions??[],i=0,a;for(;!a&&i<r.length;)this.conditionEvaluator.evaluate(r[i],n)&&(a=r[i]),i+=1;return a&&e.target==`value`&&e.value!=null?{success:!0,value:e.value.toString()}:a&&e.target==`percentage`?this.evaluatePercentage(e.percentages??[],t,n):{success:!1}}};const F=e=>{let t=typeof e;if(t===`object`)try{return e.constructor?.name??t}catch{return t}else if(t===`function`)try{let n=e.name;return n?`function: ${n}`:t}catch{return t}return t},I=e=>e===`number`||e===`bigint`,L=(e,t)=>{let n=e.value,r=F(t);if(!n)return{parsedValue:t,requestedType:r,usedDefault:!0,reason:`value-missing`};if(typeof t==`string`)return{parsedValue:n,requestedType:r,usedDefault:!1,reason:`found-match`};if(typeof t==`boolean`&&e.type===`boolean`){let e=R(n),i=typeof e==`boolean`;return{parsedValue:i?e:t,requestedType:r,usedDefault:!i,reason:i?`found-match`:`invalid-boolean`}}if(I(typeof t)&&e.type===`integer`){let e=ee(n),i=typeof e==`number`;return{parsedValue:i?e:t,requestedType:r,usedDefault:!i,reason:i?`found-match`:`invalid-number`}}if(I(typeof t)&&(e.type===`float`||e.type===`enum`)){let e=z(n),i=typeof e==`number`;return{parsedValue:i?e:t,requestedType:r,usedDefault:!i,reason:i?`found-match`:`invalid-number`}}return{parsedValue:n,requestedType:r,usedDefault:!1,reason:`found-match`}},R=e=>{if(!e)return;let t=e.toLowerCase();if(!(t!=`true`&&t!=`false`))return t===`true`},ee=e=>{if(!e)return;let t=Number.parseInt(e);if(!(isNaN(t)||!isFinite(t)))return t},z=e=>{if(!e)return;let t=Number.parseFloat(e);if(!(isNaN(t)||!isFinite(t)))return t},B={off:-1,error:0,warn:1,info:2,debug:3},V=()=>{let e={year:`numeric`,month:`2-digit`,day:`2-digit`,hour:`2-digit`,minute:`2-digit`,second:`2-digit`,timeZoneName:`short`};try{return new Intl.DateTimeFormat(void 0,{...e,fractionalSecondDigits:3})}catch{return new Intl.DateTimeFormat(void 0,e)}};var H=class{constructor(e,t){this.level=e,this.decorator=t,this.level=e,this.decorator=t,this.dateFormatter=V()}debug(e,...t){this.log(console.debug,`debug`,e,...t)}info(e,...t){this.log(console.info,`info`,e,...t)}warn(e,...t){this.log(console.warn,`warn`,e,...t)}error(e,...t){this.log(console.error,`error`,e,...t)}log(e,t,n,...r){B[this.level]>=B[t]&&e(`[${this.dateFormatter.format(new Date)}] ${this.decorator?.decorateMessage(n)}`,...r)}},U=class{decorateMessage(e){return`[ConfigDirector:js-server-sdk] ${e}`}};const W=(e,t)=>new H(e??`warn`,t??new U),G=e=>K(new TextEncoder().encode(e)).toString(16).padStart(8,`0`),K=e=>{let t=5381;for(let n=0;n<e.length;n++)t=(t<<5)+t+e[n],t>>>=0;return t},q=e=>{let t=Object.keys(e);if(t.length==0)return!0;for(let n of t)if((e[n]?.length??0)>0)return!1;return!0},J=e=>{if(!e)return!0;let t=Object.keys(e);if(t.length==0)return!0;for(let n of t)if((e[n]??0)>0)return!1;return!0},Y=async(e,t,n,r)=>{let i=new AbortController,a=setTimeout(()=>{r.debug(`[fetchWithTimeout] Reached timeout, aborting request`),i.abort()},e);try{let e=await fetch(t,{...n,signal:i.signal});return clearTimeout(a),e}catch(e){throw r.warn(`[fetchWithTimeout] Fetch error: `,e),clearTimeout(a),e}};var X=class{constructor(e){this.executeRequests=!0,this.sdkKey=e.sdkKey,this.logger=e.logger,this.url=new URL(`server/telemetry/v1`,e.baseUrl)}async report({discreteEvents:e,aggregatedEvents:t,droppedEvents:n}){if(!this.executeRequests)return{success:!1,fatalError:!0};let r={serverSdkKey:this.sdkKey,discreteEvents:e,aggregatedEvents:t,droppedEvents:n};if(this.isReportEmpty(r))return{success:!0,fatalError:!1};let i=await this.sendReport(r);return i.fatalError&&(this.executeRequests=!1),i}isReportEmpty(e){return q(e.discreteEvents)&&q(e.aggregatedEvents)&&J(e.droppedEvents)}async sendReport(e){try{let t=await Y(5e3,this.url,{method:`POST`,body:JSON.stringify(e)},this.logger),n=this.isStatusFatal(t.status);return n&&this.logger.warn(`[EventReporter] Received a fatal status response (${t.status}) from the telemetry endpoint. No more telemetry data will be sent.`),{success:t.ok,fatalError:n}}catch(e){return this.logger.warn(`[EventReporter] Error attempting to send telemetry data: ${e}`,{error:e}),{success:!1,fatalError:!1}}}isStatusFatal(e){return!!e&&e>=400&&e<500}},Z=class{aggregate(e){if(e.events.length==0)return[];let t=new Map;for(let n of e.events){let e=JSON.stringify(n),r=t.get(e)?.count||0;t.set(e,{event:n,count:r+1})}return Array.from(t).map(([,t])=>({startTime:e.startTime,endTime:e.endTime,count:t.count,event:t.event}))}},Q=class{constructor(e){this._events=[],this._droppedEventCount=0,this.limit=e??1e3}get events(){return this._events}get reachedLimit(){return this._events.length>=this.limit}get droppedEventCount(){return this._droppedEventCount}push(...e){this.startTime||=new Date;let t=Math.max(0,e.length-this.limit),n=e.slice(t,e.length);if(this._droppedEventCount+=t,this._events.length+n.length>this.limit){let e=this._events.length+n.length-this.limit;this._events.splice(0,e),this._droppedEventCount+=e}return this._events.push(...n)}takeSnapshot(){let e=new Date,t=this.startTime??e,n=this._events.splice(0),r=this._droppedEventCount;return this.startTime=void 0,this._droppedEventCount=0,{startTime:t,endTime:e,events:n,droppedCount:r}}clear(){this._events=[],this.startTime=void 0,this._droppedEventCount=0}},$=class{constructor(e){this.aggregator=new Z,this.collectEvents=!0,this.logger=e.logger,this.evaluationEventQueue=new Q(e.evaluationQueueLimit??1e3),this.flushIntervalDelay=e.flushIntervalDelay??3e4;let t=e.initialFlushIntervalDelay??5e3;this.flushTimeout=setTimeout(()=>this.flushAndScheduleNext(),t)}sanitizeValue(e,t){if(t===`json`)try{return G(JSON.stringify(e))}catch{return e.toString().slice(0,500)}return e.toString().slice(0,500)}async flushAndScheduleNext(){(await this.flush()).fatalError?(this.collectEvents=!1,this.close(),this.logger.warn(`[TelemetryEventCollector] Received a fatal error from telemetry collection. No longer collecting events.`)):this.flushTimeout=setTimeout(()=>this.flushAndScheduleNext(),this.flushIntervalDelay)}async flush(){if(!this.reporter)return{success:!1,fatalError:!1};let e=this.evaluationEventQueue.takeSnapshot();return await this.reporter?.report({context:this._context,discreteEvents:{},aggregatedEvents:{evaluatedConfig:this.aggregator.aggregate(e)},droppedEvents:{evaluatedConfig:e.droppedCount}})}cancelScheduledFlush(){clearTimeout(this.flushTimeout)}close(){this.collectEvents=!1,clearTimeout(this.flushTimeout),this.flush(),this.evaluationEventQueue.clear()}dispose(){this.close()}},te=class extends Map{constructor(e,t){super(t),this._droppedCount=0,this.limit=e,this.enforceLimit()}set(e,t){return super.set(e,t),this.enforceLimit(),this}get droppedCount(){return this._droppedCount}clearAndReset(){this.clear(),this._droppedCount=0}enforceLimit(){for(;this.size>this.limit;){let e=this.keys().next().value;e!==void 0&&(this.delete(e),this._droppedCount+=1)}}},ne=class extends ${constructor(e){super(e),this.reporter=new X(e),this.contexts=new te(e.contextLimit??1e3)}evaluatedConfig(e){if(!this.collectEvents)return;e.context?.id&&this.contexts.set(e.context.id,e.context);let t={...e.evaluation,contextId:e.context?.id};this.evaluationEventQueue.push(this.sanitizeEvaluatedConfigEvent(t))}sanitizeEvaluatedConfigEvent(e){return{...e,defaultValue:this.sanitizeValue(e.defaultValue,e.type),evaluatedValue:this.sanitizeValue(e.evaluatedValue,e.type)}}snapshotContexts(){let e=Array.from(this.contexts.values()),t=this.contexts.droppedCount;return this.contexts.clearAndReset(),{contexts:e,droppedCount:t}}async flush(){if(!this.reporter)return{success:!1,fatalError:!1};let e=this.evaluationEventQueue.takeSnapshot(),t=this.snapshotContexts();return await this.reporter?.report({context:this._context,discreteEvents:{capturedContexts:t.contexts},aggregatedEvents:{evaluatedConfig:this.aggregator.aggregate(e)},droppedEvents:{evaluatedConfig:e.droppedCount,capturedContexts:t.droppedCount}})}};const re=new URL(`https://server-sdk-api.configdirector.com`);var ie=class{constructor(e,t,n){this.handlersMap=new Map,this.eventEmitter=new c.default,this.ready=!1,this.logger=n?.logger??W(),this.timeout=n?.connection?.timeout??3e3,this.metaContext=n?.metadata??{},this.configEvaluator=new P(this.logger);let r=this.parseUrl(n?.connection?.url)??re;this.streaming=n?.connection?.streaming!==!1;let i=S,a=n?.telemetry?.eventQueueLimit??5e3;this.usageEventCollector=new ne({sdkKey:e,logger:this.logger,baseUrl:r,flushIntervalDelay:n?.telemetry?.flushInterval??3e4,evaluationQueueLimit:Math.ceil(a*.7),contextLimit:Math.floor(a*.3)}),this.transport=new i({serverSdkKey:e,baseUrl:r,metaContext:{...n?.metadata,sdkName:t.sdkName,sdkVersion:t.sdkVersion},logger:this.logger}),this.transport.on(`configBundleReceived`,e=>{this.readyResolve?.();let t=Object.keys(e.configs);!this.configSet||e.kind==`full`?(this.configSet=e,this.eventEmitter.emit(`configsUpdated`,{keys:t}),this.updateWatchers(e.configs)):(this.configSet.configs={...this.configSet.configs,...e.configs},this.eventEmitter.emit(`configsUpdated`,{keys:t}),this.updateWatchers(e.configs)),this.logger.debug(`[ConfigDirectorClient] ConfigSet updated from server with ${t.length} key(s)`,{keys:t})})}async initialize(){try{this.ready=!1,this.readyPromise=new Promise(e=>{this.readyResolve=e}).then(()=>{this.ready=!0,this.eventEmitter.emit(`clientReady`),this.logger.debug(`[ConfigDirectorClient] Received initial payload from the server, client is ready`)});let e=new Date().getTime();await this.transport.connect(this.timeout);let t=new Date().getTime()-e,n=this.timeout-t;if(n>0&&await Promise.race([this.readyPromise,new Promise(e=>{setTimeout(()=>e(),n)})]),!this.ready){let e=this.streaming?`The client will continue to retry since there were no fatal errors detected. Configs will return the default value until the connection succeeds.`:`Since the client was configured without streaming, configs may not update and always return the default value.`;this.logger.warn(`[ConfigDirectorClient] Timed out waiting for initialization after ${this.timeout}ms. ${e}`)}}catch(e){this.logger.error(`[ConfigDirectorClient] An error occurred during initialization: `,e)}}updateWatchers(e){Object.values(e).forEach(e=>this.updateWatchersForConfig(e))}updateWatchersForConfig(e){this.handlersMap.get(e.key)?.forEach(t=>{let n=this.getValueFromConfigDefinition(e.key,e,t.defaultValue,t.context);t.handler(n)})}watch(e,t,n,r){this.validateDefaultValue(t);let i=this.handlersMap.get(e),a={handler:n,defaultValue:t,requestedType:typeof t,context:r};return i?i.push(a):this.handlersMap.set(e,[a]),()=>this.unwatch(e,n)}unwatch(e,t){let n=this.handlersMap.get(e);if(n)if(t){let e=n.findIndex(e=>e.handler==t);e>=0&&n?.splice(e,1)}else n.splice(0)}getValue(e,t,n){this.validateDefaultValue(t);let r=this.configSet?.configs[e];return this.getValueFromConfigDefinition(e,r,t,n)}getValueFromConfigDefinition(e,t,n,r){if(t){let i=L(this.configEvaluator.evaluate(t,{context:r,metadata:this.metaContext}),n);return this.usageEventCollector.evaluatedConfig({context:r,evaluation:{key:e,defaultValue:n,requestedType:i.requestedType,evaluatedValue:i.parsedValue,usedDefault:i.usedDefault,evaluationReason:i.reason}}),this.logger.debug(`[ConfigDirectorClient] Evaluated '${e}' to '${i.parsedValue}'`),i.parsedValue}else return this.logger.debug(`[ConfigDirectorClient] No config state found for '${e}', returning default value '${n}'`),this.usageEventCollector.evaluatedConfig({context:r,evaluation:{key:e,defaultValue:n,requestedType:F(n),evaluatedValue:n,usedDefault:!0,evaluationReason:`config-state-missing`}}),n}parseUrl(e){if(e)try{return new URL(e)}catch(t){throw new p(`Invalid base URL '${e}'. Parsing failed: ${t}`)}}validateDefaultValue(e){if(e==null)throw new p(`Invalid default value. The default value for a config must be defined and non-null.`);if(typeof e==`function`)throw new p(`Invalid default value. The default value for a config cannot be a function.`)}get isReady(){return this.ready}on(e,t){this.eventEmitter.on(e,t)}off(e,t){t?this.eventEmitter.off(e,t):this.eventEmitter.removeAllListeners(e)}removeAllObservers(){this.logger.debug(`[ConfigDirectorClient] removeAllObservers() has been called, removing all observers`),this.eventEmitter.removeAllListeners(),this.handlersMap.clear()}unwatchAll(){this.handlersMap.clear()}closeConnection(){this.logger.debug(`[ConfigDirectorClient] closeConnection() has been called, closing connection to server`),this.transport.close(),this.ready=!1}dispose(){this.removeAllObservers(),this.closeConnection()}};const ae=(e,t)=>new ie(e,{sdkName:`js-server-sdk`,sdkVersion:`0.2.1`},t),oe=(e,t)=>W(e,t);exports.createClient=ae,exports.createConsoleLogger=oe;
@@ -1,4 +1,4 @@
1
1
  import e,{EventEmitter as t}from"node:events";import{findByPointer as n}from"@jsonjoy.com/json-pointer";import*as r from"semver";import{rapidhash as i}from"rapidhash-js";var a=class e extends Error{constructor(t,n){super(t),this.name=`ConfigDirectorConnectionError`,this.status=n,Object.setPrototypeOf(this,e.prototype)}},o=class e extends Error{constructor(t){super(t),this.name=`ConfigDirectorValidationError`,Object.setPrototypeOf(this,e.prototype)}},s=class e extends Error{constructor(t){super(t),this.name=`StreamClosedError`,Object.setPrototypeOf(this,e.prototype)}},c=class e extends Error{constructor(t){super(t),this.name=`MissingResponseBodyError`,Object.setPrototypeOf(this,e.prototype)}},l=class e extends Error{constructor(t){super(t),this.name=`InvalidOptionError`,Object.setPrototypeOf(this,e.prototype)}},u=class e extends Error{constructor(t){super(t),this.name=`ValueOutOfRangeError`,Object.setPrototypeOf(this,e.prototype)}},d=class{constructor(e){this.isFirstChunk=!0,this.previousIncompleteLine=``,this.currentEvent={data:``},this.onEvent=e.onEvent??(()=>{}),this.onRetry=e.onRetry,this.onComment=e.onComment}parse(e){let t=this.prepareChunk(e),n=this.splitLines(t);for(let e of n)this.parseAndProcessLine(e)}finish(){this.previousIncompleteLine!==``&&this.parseAndProcessLine(this.previousIncompleteLine)}parseAndProcessLine(e){if(e.startsWith(`:`)){this.onComment?.(this.getValueFromLine(0,e));return}if(e===``){this.publishEvent();return}let t=this.parseContentLine(e);this.processContent(t)}publishEvent(){let e=this.currentEvent.data.endsWith(`
2
2
  `)?this.currentEvent.data.slice(0,-1):this.currentEvent.data;e&&this.onEvent({...this.currentEvent,id:this.lastEventId,data:e}),this.currentEvent={data:``}}processContent(e){switch(e.field){case`event`:this.currentEvent.type=e.value;break;case`data`:this.currentEvent.data+=e.value+`
3
- `;break;case`id`:e.value.includes(`\0`)||(this.lastEventId=e.value);break;case`retry`:/^\d+$/.test(e.value)&&this.onRetry?.(Number.parseInt(e.value,10));break;default:break}}parseContentLine(e){let t=e.indexOf(`:`);return t===-1?{field:e,value:``}:{field:e.slice(0,t),value:this.getValueFromLine(t,e)}}splitLines(e){let t=e.split(/\r\n|\r|\n/);return this.previousIncompleteLine=t.pop()??``,t}getValueFromLine(e,t){if(e===-1)return``;let n=t[e+1]===` `?e+2:e+1;return t.slice(n)}prepareChunk(e){return this.isFirstChunk?(this.isFirstChunk=!1,this.previousIncompleteLine+e.replace(/^\xEF\xBB\xBF/,``).replace(/^\uFEFF/,``)):this.previousIncompleteLine+e}};let f=function(e){return e.OPEN=`open`,e.CLOSED=`closed`,e.CONNECTING=`connecting`,e}({});var p=class{constructor(e){this.reconnectAttempt=0,this.abortController=new AbortController,this._readyState=f.CLOSED,this.serverReconnectionTime=2e3,this.url=new URL(e.url),this.lastEventId=e.lastEventId,this.headers={Accept:`text/event-stream`,...e.headers},this.requestOptions={body:e.body,method:e.method??`GET`,mode:e.mode,credentials:e.credentials,redirect:e.redirect,referrer:e.referrer,referrerPolicy:e.referrerPolicy},this._onError=this.getValidatedFunctionOrNoOp(e.onError,`onError`),this._onConnect=this.getValidatedFunctionOrNoOp(e.onConnect,`onConnect`),this._onDisconnect=this.getValidatedFunctionOrNoOp(e.onDisconnect,`onDisconnect`),this._onMessage=this.getValidatedFunctionOrNoOp(e.onMessage,`onMessage`),this._onComment=this.getValidatedFunctionOrNoOp(e.onComment,`onComment`),this._shouldReconnect=this.getValidatedFunctionOr(e.shouldReconnect,()=>!0,`shouldReconnect`),this._calculateReconnectDelay=this.getValidatedFunctionOr(e.calculateReconnectDelay,()=>this.serverReconnectionTime,`calculateReconnectDelay`)}set onMessage(e){this._onMessage=this.getValidatedFunctionOrNoOp(e,`onMessage`)}set onComment(e){this._onComment=this.getValidatedFunctionOrNoOp(e,`onComment`)}set onConnect(e){this._onConnect=this.getValidatedFunctionOrNoOp(e,`onConnect`)}set onDisconnect(e){this._onDisconnect=this.getValidatedFunctionOrNoOp(e,`onDisconnect`)}get readyState(){return this._readyState}connect(){this._readyState===f.CLOSED&&(this.reconnectAttempt=0,this.initiateConnection())}initiateConnection(){this.abortController=new AbortController,this._readyState=f.CONNECTING,fetch(this.url,{...this.requestOptions,headers:this.buildHeaders(),signal:this.abortController.signal,cache:`no-store`}).then(this.handleFetchResponse.bind(this)).catch(e=>{if(e.name===`AbortError`||this.abortController.signal.aborted){this.close();return}this._onError(e),this.scheduleReconnect({error:e})})}close(){clearTimeout(this.reconnectTimeout),this._readyState=f.CLOSED,this.abortController.abort()}disconnected(){clearTimeout(this.reconnectTimeout),this._readyState=f.CLOSED,this._onDisconnect()}buildHeaders(){let e=this.lastEventId?{"Last-Event-ID":this.lastEventId}:void 0;return{...this.headers,...e}}async handleFetchResponse(e){let{body:t,status:n}=e;if(n===204){this.disconnected();return}if(n>=400){this.scheduleReconnect({status:n});return}if(!t)throw new c(`The server response did not have a body`);this._readyState=f.OPEN,this._onConnect(),this.reconnectAttempt=0;let r=(t?.pipeThrough(new TextDecoderStream)).getReader(),i=new d({onEvent:e=>{typeof e.id==`string`&&(this.lastEventId=e.id),this._onMessage(e)},onComment:e=>this._onComment(e),onRetry:e=>this.serverReconnectionTime=e}),a=!1;for(;!a;){let{done:e,value:t}=await r.read();t&&i.parse(t),a=e}i.finish(),this.scheduleReconnect({status:n,error:new s(`The server response stream was closed`)})}scheduleReconnect(e){if(this.abortController.signal.aborted)return;clearTimeout(this.reconnectTimeout),this.reconnectAttempt+=1;let t={attempt:this.reconnectAttempt,status:e.status,error:e.error,serverReconnectionTime:this.serverReconnectionTime};if(this._shouldReconnect(t)){this._readyState=f.CONNECTING;let e=this._calculateReconnectDelay(t);(!Number.isFinite(e)||e<1||e>1e3*60*60)&&(this._onError(new u(`The calculated reconnect delay is out of range: ${e}. Defaulting to ${this.serverReconnectionTime}`)),e=this.serverReconnectionTime),this.reconnectTimeout=setTimeout(()=>this.initiateConnection(),e)}else{this.disconnected();return}}getValidatedFunctionOrNoOp(e,t){return this.getValidatedFunctionOr(e,()=>{},t)}getValidatedFunctionOr(e,t,n){if(e==null)return t;if(typeof e==`function`)return e;throw new l(`${n} must be a function`)}};const m=e=>new p(e);var h=class{constructor(e){this.options=e,this.eventEmitter=new t,this.options=e,this.logger=e.logger,this.url=new URL(`server/sse/v1`,e.baseUrl)}async connect(e){this.eventSource&&this.close();let t=new Promise((e,t)=>{this.eventSource=m({url:this.url,method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({metaContext:this.options.metaContext,serverSdkKey:this.options.serverSdkKey}),onMessage:({data:e})=>{this.dispatchMessage(e)},onConnect:()=>{this.logger.debug(`[EventSourceTransport] Connected`),e(this)},shouldReconnect:e=>{let n=!this.isStatusFatal(e.status);return n||t(this.prepareFatalError(e.status,e.error)),n},calculateReconnectDelay:e=>{let t=2e3*e.attempt;return this.logger.warn(`[EventSourceTransport] Scheduling reconnect in ${t}ms.`),t},onDisconnect:()=>{this.logger.debug(`[EventSourceTransport] Disconnected`)}}),this.eventSource.connect()});return Promise.race([t,new Promise(t=>{setTimeout(()=>t(this),e)})])}dispatchMessage(e){try{let t=JSON.parse(e);this.eventEmitter.emit(`configBundleReceived`,t)}catch(e){this.logger.error(`[EventSourceTransport] Error parsing and dispatching config data update: `,e)}}prepareFatalError(e,t){let n=e??0;return new a(`${`Connection failed with status: ${e??`unknown`}.`}${t?` Error: ${t}.`:``} This is an unrecoverable error, will not attempt to reconnect.`,n)}isStatusFatal(e){return!!e&&e>=400&&e<500}get isConnected(){return this.eventSource?.readyState==`open`}on(e,t){this.eventEmitter.on(e,t)}off(e,t){t?this.eventEmitter.off(e,t):this.eventEmitter.removeAllListeners(e)}clear(){this.eventEmitter.removeAllListeners()}close(){this.eventSource?.close()}dispose(){this.close(),this.clear()}};const g=(e,t,n)=>{switch(t.toLowerCase()){case`=`:case`equals`:return e==n[0];case`!=`:case`does not equal`:return e!=n[0];case`is one of`:return n.includes(e);case`is not one of`:return!n.includes(e);case`starts with any of`:return n.findIndex(t=>e?.startsWith(t))>=0;case`does not start with any of`:return n.findIndex(t=>e?.startsWith(t))<0;case`ends with any of`:return n.findIndex(t=>e?.endsWith(t))>=0;case`does not end with any of`:return n.findIndex(t=>e?.endsWith(t))<0;case`matches regex`:return _(n[0],e);case`does not match regex`:return!_(n[0],e);default:return!1}},_=(e,t)=>{try{return new RegExp(e).test(t)}catch{return!1}},v=(e,t,n)=>{if(typeof e==`boolean`)return!1;let r=t.toLowerCase();if(e==null)return r==`!=`||r==`does not equal`;let{value:i,target:a}=y(e,n[0]);switch(r){case`=`:case`equals`:return i==a;case`!=`:case`does not equal`:return i!=a;case`<`:return i<a;case`<=`:return i<=a;case`>`:return i>a;case`>=`:return i>=a;default:return!1}},y=(e,t)=>typeof e==`number`?{value:e,target:b(t)}:typeof e==`bigint`?{value:e,target:BigInt(t)}:{value:b(e),target:b(t)},b=e=>{let t=Number.parseFloat(e.toString());if(isNaN(t)||!isFinite(t))throw TypeError(`Invalid numeric value: ${e}`);return t},x=(e,t,n)=>{if(e==null)return!1;try{let r=new Date(e),i=new Date(n[0]);switch(t){case`is after`:return r>i;case`is before`:return r<i;default:return!1}}catch{return!1}},S=(e,t,n)=>{let i=t.toLowerCase();if(e==null||e.trim().length==0)return i==`is not one of`;let a=r.coerce(e)??e,o=n.map(e=>r.coerce(e)??e);try{switch(i){case`=`:return C(a,o[0]);case`<`:return r.lt(a,o[0]);case`<=`:return r.lte(a,o[0]);case`>`:return r.gt(a,o[0]);case`>=`:return r.gte(a,o[0]);case`is one of`:return o.findIndex(e=>C(a,e))>=0;case`is not one of`:return o.findIndex(e=>C(a,e))<0;default:return!1}}catch{return!1}},C=(e,t)=>{try{return r.eq(e,t)}catch{return!1}},w=(e,t,n)=>{let r=t.toLowerCase();if(!Array.isArray(e))return r==`does not contain any of`;switch(r){case`contains any of`:return e.some(e=>n.includes(e));case`does not contain any of`:return!e.some(e=>n.includes(e));default:return!1}};var T=class{evaluate(e,t){if(!this.isConditionValidForComparison(e))return!1;switch(e.attribute){case`identifier`:return this.compare(t?.context?.id,e);case`name`:return this.compare(t?.context?.name,e);case`appName`:return this.compare(t?.metadata?.appName,e);case`appVersion`:return this.compare(t?.metadata?.appVersion,e);case`traits`:if(e.trait){let n=this.findTraitValue(t?.context?.traits,e.trait);return this.compare(n,e)}}return!1}compare(e,t){switch(t.targetType){case`text`:return g(e?.toString(),t.operator,t.targetValues);case`number`:return v(e,t.operator,t.targetValues);case`datetime`:return x(e?.toString(),t.operator,t.targetValues);case`semver`:return S(e?.toString(),t.operator,t.targetValues);case`array`:return w(e,t.operator,t.targetValues)}return!1}findTraitValue(e,t){try{return n(t,e).val}catch{return}}isConditionValidForComparison(e){return Array.isArray(e.targetValues)&&e.targetValues.length>0}};const E=e=>{let t=`${e.contextIdentifier}-${e.configId}`;return Number(i(t,{seed:15426857351n})%1000n)/10};var D=class{constructor(e){if(this.logger=e,this.conditionEvaluator=new T,!e||!e.warn||!e.error||!e.info)throw TypeError(`The provided logger is not a valid 'ConfigDirectorLogger'`);this.logger=e}evaluate(e,t){return{id:e.id,key:e.key,type:e.type,value:this.getConfigValue(e,t)}}getConfigValue(e,t){let n=[...e.target?.rules??[]].sort((e,t)=>(e.order??2**53-1)-(t.order??2**53-1));for(let r of n){let n=this.evaluateRule(r,e,t);if(n.success)return n.value}return e.target?.defaultValue}evaluateRule(e,t,n){try{if(e.type==`percentage`)return this.evaluatePercentageRule(e,t,n);if(e.type==`conditional`)return this.evaluateConditionalRule(e,t,n)}catch(n){this.logger.warn(`There was an error while evaluating a targeting rule '${e?.id}' for '${t?.key}'. The rule will be disregarded.`,{error:n,configKey:t?.key,ruleId:e?.id})}return{success:!1}}evaluatePercentageRule(e,t,n){return this.evaluatePercentage(e.percentages??[],t,n)}evaluatePercentage(e,t,n){let r=E({configId:t.id,contextIdentifier:n?.context?.id??crypto.randomUUID()}),i=0,a=0,o;for(;!o&&i<e.length;)r<=e[i].percentage+a&&(o=e[i]),a+=e[i].percentage,i+=1;return o?.value==null?{success:!1}:{success:!0,value:o.value.toString()}}evaluateConditionalRule(e,t,n){let r=e.conditions??[],i=0,a;for(;!a&&i<r.length;)this.conditionEvaluator.evaluate(r[i],n)&&(a=r[i]),i+=1;return a&&e.target==`value`&&e.value!=null?{success:!0,value:e.value.toString()}:a&&e.target==`percentage`?this.evaluatePercentage(e.percentages??[],t,n):{success:!1}}};const O=e=>{let t=typeof e;if(t===`object`)try{return e.constructor?.name??t}catch{return t}else if(t===`function`)try{let n=e.name;return n?`function: ${n}`:t}catch{return t}return t},k=e=>e===`number`||e===`bigint`,A=(e,t)=>{let n=e.value,r=O(t);if(!n)return{parsedValue:t,requestedType:r,usedDefault:!0,reason:`value-missing`};if(typeof t==`string`)return{parsedValue:n,requestedType:r,usedDefault:!1,reason:`found-match`};if(typeof t==`boolean`&&e.type===`boolean`){let e=j(n),i=typeof e==`boolean`;return{parsedValue:i?e:t,requestedType:r,usedDefault:!i,reason:i?`found-match`:`invalid-boolean`}}if(k(typeof t)&&e.type===`integer`){let e=M(n),i=typeof e==`number`;return{parsedValue:i?e:t,requestedType:r,usedDefault:!i,reason:i?`found-match`:`invalid-number`}}if(k(typeof t)&&(e.type===`float`||e.type===`enum`)){let e=N(n),i=typeof e==`number`;return{parsedValue:i?e:t,requestedType:r,usedDefault:!i,reason:i?`found-match`:`invalid-number`}}return{parsedValue:n,requestedType:r,usedDefault:!1,reason:`found-match`}},j=e=>{if(!e)return;let t=e.toLowerCase();if(!(t!=`true`&&t!=`false`))return t===`true`},M=e=>{if(!e)return;let t=Number.parseInt(e);if(!(isNaN(t)||!isFinite(t)))return t},N=e=>{if(!e)return;let t=Number.parseFloat(e);if(!(isNaN(t)||!isFinite(t)))return t},P={off:-1,error:0,warn:1,info:2,debug:3},F=()=>{let e={year:`numeric`,month:`2-digit`,day:`2-digit`,hour:`2-digit`,minute:`2-digit`,second:`2-digit`,timeZoneName:`short`};try{return new Intl.DateTimeFormat(void 0,{...e,fractionalSecondDigits:3})}catch{return new Intl.DateTimeFormat(void 0,e)}};var I=class{constructor(e,t){this.level=e,this.decorator=t,this.level=e,this.decorator=t,this.dateFormatter=F()}debug(e,...t){this.log(console.debug,`debug`,e,...t)}info(e,...t){this.log(console.info,`info`,e,...t)}warn(e,...t){this.log(console.warn,`warn`,e,...t)}error(e,...t){this.log(console.error,`error`,e,...t)}log(e,t,n,...r){P[this.level]>=P[t]&&e(`[${this.dateFormatter.format(new Date)}] ${this.decorator?.decorateMessage(n)}`,...r)}},L=class{decorateMessage(e){return`[ConfigDirector:js-server-sdk] ${e}`}};const R=(e,t)=>new I(e??`warn`,t??new L),z=e=>B(new TextEncoder().encode(e)).toString(16).padStart(8,`0`),B=e=>{let t=5381;for(let n=0;n<e.length;n++)t=(t<<5)+t+e[n],t>>>=0;return t},V=e=>{let t=Object.keys(e);if(t.length==0)return!0;for(let n of t)if((e[n]?.length??0)>0)return!1;return!0},H=e=>{if(!e)return!0;let t=Object.keys(e);if(t.length==0)return!0;for(let n of t)if((e[n]??0)>0)return!1;return!0},U=async(e,t,n,r)=>{let i=new AbortController,a=setTimeout(()=>{r.debug(`[fetchWithTimeout] Reached timeout, aborting request`),i.abort()},e);try{let e=await fetch(t,{...n,signal:i.signal});return clearTimeout(a),e}catch(e){throw r.warn(`[fetchWithTimeout] Fetch error: `,e),clearTimeout(a),e}};var W=class{constructor(e){this.executeRequests=!0,this.sdkKey=e.sdkKey,this.logger=e.logger,this.url=new URL(`server/telemetry/v1`,e.baseUrl)}async report({discreteEvents:e,aggregatedEvents:t,droppedEvents:n}){if(!this.executeRequests)return{success:!1,fatalError:!0};let r={serverSdkKey:this.sdkKey,discreteEvents:e,aggregatedEvents:t,droppedEvents:n};if(this.isReportEmpty(r))return{success:!0,fatalError:!1};let i=await this.sendReport(r);return i.fatalError&&(this.executeRequests=!1),i}isReportEmpty(e){return V(e.discreteEvents)&&V(e.aggregatedEvents)&&H(e.droppedEvents)}async sendReport(e){try{let t=await U(5e3,this.url,{method:`POST`,body:JSON.stringify(e)},this.logger),n=this.isStatusFatal(t.status);return n&&this.logger.warn(`[EventReporter] Received a fatal status response (${t.status}) from the telemetry endpoint. No more telemetry data will be sent.`),{success:t.ok,fatalError:n}}catch(e){return this.logger.warn(`[EventReporter] Error attempting to send telemetry data: ${e}`,{error:e}),{success:!1,fatalError:!1}}}isStatusFatal(e){return!!e&&e>=400&&e<500}},G=class{aggregate(e){if(e.events.length==0)return[];let t=new Map;for(let n of e.events){let e=JSON.stringify(n),r=t.get(e)?.count||0;t.set(e,{event:n,count:r+1})}return Array.from(t).map(([,t])=>({startTime:e.startTime,endTime:e.endTime,count:t.count,event:t.event}))}},K=class{constructor(e){this._events=[],this._droppedEventCount=0,this.limit=e??1e3}get events(){return this._events}get reachedLimit(){return this._events.length>=this.limit}get droppedEventCount(){return this._droppedEventCount}push(...e){this.startTime||(this.startTime=new Date);let t=Math.max(0,e.length-this.limit),n=e.slice(t,e.length);if(this._droppedEventCount+=t,this._events.length+n.length>this.limit){let e=this._events.length+n.length-this.limit;this._events.splice(0,e),this._droppedEventCount+=e}return this._events.push(...n)}takeSnapshot(){let e=new Date,t=this.startTime??e,n=this._events.splice(0),r=this._droppedEventCount;return this.startTime=void 0,this._droppedEventCount=0,{startTime:t,endTime:e,events:n,droppedCount:r}}clear(){this._events=[],this.startTime=void 0,this._droppedEventCount=0}},q=class{constructor(e){this.aggregator=new G,this.collectEvents=!0,this.logger=e.logger,this.evaluationEventQueue=new K(e.evaluationQueueLimit??1e3),this.flushIntervalDelay=e.flushIntervalDelay??3e4;let t=e.initialFlushIntervalDelay??5e3;this.flushTimeout=setTimeout(()=>this.flushAndScheduleNext(),t)}sanitizeValue(e,t){if(t===`json`)try{return z(JSON.stringify(e))}catch{return e.toString().slice(0,500)}return e.toString().slice(0,500)}async flushAndScheduleNext(){(await this.flush()).fatalError?(this.collectEvents=!1,this.close(),this.logger.warn(`[TelemetryEventCollector] Received a fatal error from telemetry collection. No longer collecting events.`)):this.flushTimeout=setTimeout(()=>this.flushAndScheduleNext(),this.flushIntervalDelay)}async flush(){if(!this.reporter)return{success:!1,fatalError:!1};let e=this.evaluationEventQueue.takeSnapshot();return await this.reporter?.report({context:this._context,discreteEvents:{},aggregatedEvents:{evaluatedConfig:this.aggregator.aggregate(e)},droppedEvents:{evaluatedConfig:e.droppedCount}})}cancelScheduledFlush(){clearTimeout(this.flushTimeout)}close(){this.collectEvents=!1,clearTimeout(this.flushTimeout),this.flush(),this.evaluationEventQueue.clear()}dispose(){this.close()}},J=class extends Map{constructor(e,t){super(t),this._droppedCount=0,this.limit=e,this.enforceLimit()}set(e,t){return super.set(e,t),this.enforceLimit(),this}get droppedCount(){return this._droppedCount}clearAndReset(){this.clear(),this._droppedCount=0}enforceLimit(){for(;this.size>this.limit;){let e=this.keys().next().value;e!==void 0&&(this.delete(e),this._droppedCount+=1)}}},Y=class extends q{constructor(e){super(e),this.reporter=new W(e),this.contexts=new J(e.contextLimit??1e3)}evaluatedConfig(e){if(!this.collectEvents)return;e.context?.id&&this.contexts.set(e.context.id,e.context);let t={...e.evaluation,contextId:e.context?.id};this.evaluationEventQueue.push(this.sanitizeEvaluatedConfigEvent(t))}sanitizeEvaluatedConfigEvent(e){return{...e,defaultValue:this.sanitizeValue(e.defaultValue,e.type),evaluatedValue:this.sanitizeValue(e.evaluatedValue,e.type)}}snapshotContexts(){let e=Array.from(this.contexts.values()),t=this.contexts.droppedCount;return this.contexts.clearAndReset(),{contexts:e,droppedCount:t}}async flush(){if(!this.reporter)return{success:!1,fatalError:!1};let e=this.evaluationEventQueue.takeSnapshot(),t=this.snapshotContexts();return await this.reporter?.report({context:this._context,discreteEvents:{capturedContexts:t.contexts},aggregatedEvents:{evaluatedConfig:this.aggregator.aggregate(e)},droppedEvents:{evaluatedConfig:e.droppedCount,capturedContexts:t.droppedCount}})}};const X=new URL(`https://server-sdk-api.configdirector.com`);var Z=class{constructor(t,n,r){this.handlersMap=new Map,this.eventEmitter=new e,this.ready=!1,this.logger=r?.logger??R(),this.timeout=r?.connection?.timeout??3e3,this.metaContext=r?.metadata??{},this.configEvaluator=new D(this.logger);let i=this.parseUrl(r?.connection?.url)??X;this.streaming=r?.connection?.streaming!==!1;let a=h,o=r?.telemetry?.eventQueueLimit??5e3;this.usageEventCollector=new Y({sdkKey:t,logger:this.logger,baseUrl:i,flushIntervalDelay:r?.telemetry?.flushInterval??3e4,evaluationQueueLimit:Math.ceil(o*.7),contextLimit:Math.floor(o*.3)}),this.transport=new a({serverSdkKey:t,baseUrl:i,metaContext:{...r?.metadata,sdkName:n.sdkName,sdkVersion:n.sdkVersion},logger:this.logger}),this.transport.on(`configBundleReceived`,e=>{this.readyResolve?.();let t=Object.keys(e.configs);!this.configSet||e.kind==`full`?(this.configSet=e,this.eventEmitter.emit(`configsUpdated`,{keys:t}),this.updateWatchers(e.configs)):(this.configSet.configs={...this.configSet.configs,...e.configs},this.eventEmitter.emit(`configsUpdated`,{keys:t}),this.updateWatchers(e.configs)),this.logger.debug(`[ConfigDirectorClient] ConfigSet updated from server with ${t.length} key(s)`,{keys:t})})}async initialize(){try{this.ready=!1,this.readyPromise=new Promise(e=>{this.readyResolve=e}).then(()=>{this.ready=!0,this.eventEmitter.emit(`clientReady`),this.logger.debug(`[ConfigDirectorClient] Received initial payload from the server, client is ready`)});let e=new Date().getTime();await this.transport.connect(this.timeout);let t=new Date().getTime()-e,n=this.timeout-t;if(n>0&&await Promise.race([this.readyPromise,new Promise(e=>{setTimeout(()=>e(),n)})]),!this.ready){let e=this.streaming?`The client will continue to retry since there were no fatal errors detected. Configs will return the default value until the connection succeeds.`:`Since the client was configured without streaming, configs may not update and always return the default value.`;this.logger.warn(`[ConfigDirectorClient] Timed out waiting for initialization after ${this.timeout}ms. ${e}`)}}catch(e){this.logger.error(`[ConfigDirectorClient] An error occurred during initialization: `,e)}}updateWatchers(e){Object.values(e).forEach(e=>this.updateWatchersForConfig(e))}updateWatchersForConfig(e){this.handlersMap.get(e.key)?.forEach(t=>{let n=this.getValueFromConfigDefinition(e.key,e,t.defaultValue,t.context);t.handler(n)})}watch(e,t,n,r){this.validateDefaultValue(t);let i=this.handlersMap.get(e),a={handler:n,defaultValue:t,requestedType:typeof t,context:r};return i?i.push(a):this.handlersMap.set(e,[a]),()=>this.unwatch(e,n)}unwatch(e,t){let n=this.handlersMap.get(e);if(n)if(t){let e=n.findIndex(e=>e.handler==t);e>=0&&n?.splice(e,1)}else n.splice(0)}getValue(e,t,n){this.validateDefaultValue(t);let r=this.configSet?.configs[e];return this.getValueFromConfigDefinition(e,r,t,n)}getValueFromConfigDefinition(e,t,n,r){if(t){let i=A(this.configEvaluator.evaluate(t,{context:r,metadata:this.metaContext}),n);return this.usageEventCollector.evaluatedConfig({context:r,evaluation:{key:e,defaultValue:n,requestedType:i.requestedType,evaluatedValue:i.parsedValue,usedDefault:i.usedDefault,evaluationReason:i.reason}}),this.logger.debug(`[ConfigDirectorClient] Evaluated '${e}' to '${i.parsedValue}'`),i.parsedValue}else return this.logger.debug(`[ConfigDirectorClient] No config state found for '${e}', returning default value '${n}'`),this.usageEventCollector.evaluatedConfig({context:r,evaluation:{key:e,defaultValue:n,requestedType:O(n),evaluatedValue:n,usedDefault:!0,evaluationReason:`config-state-missing`}}),n}parseUrl(e){if(e)try{return new URL(e)}catch(t){throw new o(`Invalid base URL '${e}'. Parsing failed: ${t}`)}}validateDefaultValue(e){if(e==null)throw new o(`Invalid default value. The default value for a config must be defined and non-null.`);if(typeof e==`function`)throw new o(`Invalid default value. The default value for a config cannot be a function.`)}get isReady(){return this.ready}on(e,t){this.eventEmitter.on(e,t)}off(e,t){t?this.eventEmitter.off(e,t):this.eventEmitter.removeAllListeners(e)}removeAllObservers(){this.logger.debug(`[ConfigDirectorClient] removeAllObservers() has been called, removing all observers`),this.eventEmitter.removeAllListeners(),this.handlersMap.clear()}unwatchAll(){this.handlersMap.clear()}closeConnection(){this.logger.debug(`[ConfigDirectorClient] closeConnection() has been called, closing connection to server`),this.transport.close(),this.ready=!1}dispose(){this.removeAllObservers(),this.closeConnection()}};const Q=(e,t)=>new Z(e,{sdkName:`js-server-sdk`,sdkVersion:`0.2.0`},t),$=(e,t)=>R(e,t);export{Q as createClient,$ as createConsoleLogger};
3
+ `;break;case`id`:e.value.includes(`\0`)||(this.lastEventId=e.value);break;case`retry`:/^\d+$/.test(e.value)&&this.onRetry?.(Number.parseInt(e.value,10));break;default:break}}parseContentLine(e){let t=e.indexOf(`:`);return t===-1?{field:e,value:``}:{field:e.slice(0,t),value:this.getValueFromLine(t,e)}}splitLines(e){let t=e.split(/\r\n|\r|\n/);return this.previousIncompleteLine=t.pop()??``,t}getValueFromLine(e,t){if(e===-1)return``;let n=t[e+1]===` `?e+2:e+1;return t.slice(n)}prepareChunk(e){return this.isFirstChunk?(this.isFirstChunk=!1,this.previousIncompleteLine+e.replace(/^\xEF\xBB\xBF/,``).replace(/^\uFEFF/,``)):this.previousIncompleteLine+e}};let f=function(e){return e.OPEN=`open`,e.CLOSED=`closed`,e.CONNECTING=`connecting`,e}({});var p=class{constructor(e){this.reconnectAttempt=0,this.abortController=new AbortController,this._readyState=f.CLOSED,this.serverReconnectionTime=2e3,this.url=new URL(e.url),this.lastEventId=e.lastEventId,this.headers={Accept:`text/event-stream`,...e.headers},this.requestOptions={body:e.body,method:e.method??`GET`,mode:e.mode,credentials:e.credentials,redirect:e.redirect,referrer:e.referrer,referrerPolicy:e.referrerPolicy},this._onError=this.getValidatedFunctionOrNoOp(e.onError,`onError`),this._onConnect=this.getValidatedFunctionOrNoOp(e.onConnect,`onConnect`),this._onDisconnect=this.getValidatedFunctionOrNoOp(e.onDisconnect,`onDisconnect`),this._onMessage=this.getValidatedFunctionOrNoOp(e.onMessage,`onMessage`),this._onComment=this.getValidatedFunctionOrNoOp(e.onComment,`onComment`),this._shouldReconnect=this.getValidatedFunctionOr(e.shouldReconnect,()=>!0,`shouldReconnect`),this._calculateReconnectDelay=this.getValidatedFunctionOr(e.calculateReconnectDelay,()=>this.serverReconnectionTime,`calculateReconnectDelay`)}set onMessage(e){this._onMessage=this.getValidatedFunctionOrNoOp(e,`onMessage`)}set onComment(e){this._onComment=this.getValidatedFunctionOrNoOp(e,`onComment`)}set onConnect(e){this._onConnect=this.getValidatedFunctionOrNoOp(e,`onConnect`)}set onDisconnect(e){this._onDisconnect=this.getValidatedFunctionOrNoOp(e,`onDisconnect`)}get readyState(){return this._readyState}connect(){this._readyState===f.CLOSED&&(this.reconnectAttempt=0,this.initiateConnection())}initiateConnection(){this.abortController=new AbortController,this._readyState=f.CONNECTING,fetch(this.url,{...this.requestOptions,headers:this.buildHeaders(),signal:this.abortController.signal,cache:`no-store`}).then(this.handleFetchResponse.bind(this)).catch(e=>{if(e.name===`AbortError`||this.abortController.signal.aborted){this.close();return}this._onError(e),this.scheduleReconnect({error:e})})}close(){clearTimeout(this.reconnectTimeout),this._readyState=f.CLOSED,this.abortController.abort()}disconnected(){clearTimeout(this.reconnectTimeout),this._readyState=f.CLOSED,this._onDisconnect()}buildHeaders(){let e=this.lastEventId?{"Last-Event-ID":this.lastEventId}:void 0;return{...this.headers,...e}}async handleFetchResponse(e){let{body:t,status:n}=e;if(n===204){this.disconnected();return}if(n>=400){this.scheduleReconnect({status:n});return}if(!t)throw new c(`The server response did not have a body`);this._readyState=f.OPEN,this._onConnect(),this.reconnectAttempt=0;let r=(t?.pipeThrough(new TextDecoderStream)).getReader(),i=new d({onEvent:e=>{typeof e.id==`string`&&(this.lastEventId=e.id),this._onMessage(e)},onComment:e=>this._onComment(e),onRetry:e=>this.serverReconnectionTime=e}),a=!1;for(;!a;){let{done:e,value:t}=await r.read();t&&i.parse(t),a=e}i.finish(),this.scheduleReconnect({status:n,error:new s(`The server response stream was closed`)})}scheduleReconnect(e){if(this.abortController.signal.aborted)return;clearTimeout(this.reconnectTimeout),this.reconnectAttempt+=1;let t={attempt:this.reconnectAttempt,status:e.status,error:e.error,serverReconnectionTime:this.serverReconnectionTime};if(this._shouldReconnect(t)){this._readyState=f.CONNECTING;let e=this._calculateReconnectDelay(t);(!Number.isFinite(e)||e<1||e>1e3*60*60)&&(this._onError(new u(`The calculated reconnect delay is out of range: ${e}. Defaulting to ${this.serverReconnectionTime}`)),e=this.serverReconnectionTime),this.reconnectTimeout=setTimeout(()=>this.initiateConnection(),e)}else{this.disconnected();return}}getValidatedFunctionOrNoOp(e,t){return this.getValidatedFunctionOr(e,()=>{},t)}getValidatedFunctionOr(e,t,n){if(e==null)return t;if(typeof e==`function`)return e;throw new l(`${n} must be a function`)}};const m=e=>new p(e);var h=class{constructor(e){this.options=e,this.eventEmitter=new t,this.options=e,this.logger=e.logger,this.url=new URL(`server/sse/v1`,e.baseUrl)}async connect(e){this.eventSource&&this.close();let t=new Promise((e,t)=>{this.eventSource=m({url:this.url,method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({metaContext:this.options.metaContext,serverSdkKey:this.options.serverSdkKey}),onMessage:({data:e})=>{this.dispatchMessage(e)},onConnect:()=>{this.logger.debug(`[EventSourceTransport] Connected`),e(this)},shouldReconnect:e=>{let n=!this.isStatusFatal(e.status);return n||t(this.prepareFatalError(e.status,e.error)),n},calculateReconnectDelay:e=>{let t=2e3*e.attempt;return this.logger.warn(`[EventSourceTransport] Scheduling reconnect in ${t}ms.`),t},onDisconnect:()=>{this.logger.debug(`[EventSourceTransport] Disconnected`)}}),this.eventSource.connect()});return Promise.race([t,new Promise(t=>{setTimeout(()=>t(this),e)})])}dispatchMessage(e){try{let t=JSON.parse(e);this.eventEmitter.emit(`configBundleReceived`,t)}catch(e){this.logger.error(`[EventSourceTransport] Error parsing and dispatching config data update: `,e)}}prepareFatalError(e,t){let n=e??0;return new a(`${`Connection failed with status: ${e??`unknown`}.`}${t?` Error: ${t}.`:``} This is an unrecoverable error, will not attempt to reconnect.`,n)}isStatusFatal(e){return!!e&&e>=400&&e<500}get isConnected(){return this.eventSource?.readyState==`open`}on(e,t){this.eventEmitter.on(e,t)}off(e,t){t?this.eventEmitter.off(e,t):this.eventEmitter.removeAllListeners(e)}clear(){this.eventEmitter.removeAllListeners()}close(){this.eventSource?.close()}dispose(){this.close(),this.clear()}};const g=(e,t,n)=>{switch(t.toLowerCase()){case`=`:case`equals`:return e==n[0];case`!=`:case`does not equal`:return e!=n[0];case`is one of`:return n.includes(e);case`is not one of`:return!n.includes(e);case`starts with any of`:return n.findIndex(t=>e?.startsWith(t))>=0;case`does not start with any of`:return n.findIndex(t=>e?.startsWith(t))<0;case`ends with any of`:return n.findIndex(t=>e?.endsWith(t))>=0;case`does not end with any of`:return n.findIndex(t=>e?.endsWith(t))<0;case`matches regex`:return _(n[0],e);case`does not match regex`:return!_(n[0],e);default:return!1}},_=(e,t)=>{try{return new RegExp(e).test(t)}catch{return!1}},v=(e,t,n)=>{if(typeof e==`boolean`)return!1;let r=t.toLowerCase();if(e==null)return r==`!=`||r==`does not equal`;let{value:i,target:a}=y(e,n[0]);switch(r){case`=`:case`equals`:return i==a;case`!=`:case`does not equal`:return i!=a;case`<`:return i<a;case`<=`:return i<=a;case`>`:return i>a;case`>=`:return i>=a;default:return!1}},y=(e,t)=>typeof e==`number`?{value:e,target:b(t)}:typeof e==`bigint`?{value:e,target:BigInt(t)}:{value:b(e),target:b(t)},b=e=>{let t=Number.parseFloat(e.toString());if(isNaN(t)||!isFinite(t))throw TypeError(`Invalid numeric value: ${e}`);return t},x=(e,t,n)=>{if(e==null)return!1;try{let r=new Date(e),i=new Date(n[0]);switch(t){case`is after`:return r>i;case`is before`:return r<i;default:return!1}}catch{return!1}},S=(e,t,n)=>{let i=t.toLowerCase();if(e==null||e.trim().length==0)return i==`is not one of`;let a=r.coerce(e)??e,o=n.map(e=>r.coerce(e)??e);try{switch(i){case`=`:return C(a,o[0]);case`<`:return r.lt(a,o[0]);case`<=`:return r.lte(a,o[0]);case`>`:return r.gt(a,o[0]);case`>=`:return r.gte(a,o[0]);case`is one of`:return o.findIndex(e=>C(a,e))>=0;case`is not one of`:return o.findIndex(e=>C(a,e))<0;default:return!1}}catch{return!1}},C=(e,t)=>{try{return r.eq(e,t)}catch{return!1}},w=(e,t,n)=>{let r=t.toLowerCase();if(!Array.isArray(e))return r==`does not contain any of`;switch(r){case`contains any of`:return e.some(e=>n.includes(e));case`does not contain any of`:return!e.some(e=>n.includes(e));default:return!1}};var T=class{evaluate(e,t){if(!this.isConditionValidForComparison(e))return!1;switch(e.attribute){case`identifier`:return this.compare(t?.context?.id,e);case`name`:return this.compare(t?.context?.name,e);case`appName`:return this.compare(t?.metadata?.appName,e);case`appVersion`:return this.compare(t?.metadata?.appVersion,e);case`traits`:if(e.trait){let n=this.findTraitValue(t?.context?.traits,e.trait);return this.compare(n,e)}}return!1}compare(e,t){switch(t.targetType){case`text`:return g(e?.toString(),t.operator,t.targetValues);case`number`:return v(e,t.operator,t.targetValues);case`datetime`:return x(e?.toString(),t.operator,t.targetValues);case`semver`:return S(e?.toString(),t.operator,t.targetValues);case`array`:return w(e,t.operator,t.targetValues)}return!1}findTraitValue(e,t){try{return n(t,e).val}catch{return}}isConditionValidForComparison(e){return Array.isArray(e.targetValues)&&e.targetValues.length>0}};const E=e=>{let t=`${e.contextIdentifier}-${e.configId}`;return Number(i(t,{seed:15426857351n})%1000n)/10};var D=class{constructor(e){if(this.logger=e,this.conditionEvaluator=new T,!e||!e.warn||!e.error||!e.info)throw TypeError(`The provided logger is not a valid 'ConfigDirectorLogger'`);this.logger=e}evaluate(e,t){return{id:e.id,key:e.key,type:e.type,value:this.getConfigValue(e,t)}}getConfigValue(e,t){let n=[...e.target?.rules??[]].sort((e,t)=>(e.order??2**53-1)-(t.order??2**53-1));for(let r of n){let n=this.evaluateRule(r,e,t);if(n.success)return n.value}return e.target?.defaultValue}evaluateRule(e,t,n){try{if(e.type==`percentage`)return this.evaluatePercentageRule(e,t,n);if(e.type==`conditional`)return this.evaluateConditionalRule(e,t,n)}catch(n){this.logger.warn(`There was an error while evaluating a targeting rule '${e?.id}' for '${t?.key}'. The rule will be disregarded.`,{error:n,configKey:t?.key,ruleId:e?.id})}return{success:!1}}evaluatePercentageRule(e,t,n){return this.evaluatePercentage(e.percentages??[],t,n)}evaluatePercentage(e,t,n){let r=E({configId:t.id,contextIdentifier:n?.context?.id??crypto.randomUUID()}),i=0,a=0,o;for(;!o&&i<e.length;)r<=e[i].percentage+a&&(o=e[i]),a+=e[i].percentage,i+=1;return o?.value==null?{success:!1}:{success:!0,value:o.value.toString()}}evaluateConditionalRule(e,t,n){let r=e.conditions??[],i=0,a;for(;!a&&i<r.length;)this.conditionEvaluator.evaluate(r[i],n)&&(a=r[i]),i+=1;return a&&e.target==`value`&&e.value!=null?{success:!0,value:e.value.toString()}:a&&e.target==`percentage`?this.evaluatePercentage(e.percentages??[],t,n):{success:!1}}};const O=e=>{let t=typeof e;if(t===`object`)try{return e.constructor?.name??t}catch{return t}else if(t===`function`)try{let n=e.name;return n?`function: ${n}`:t}catch{return t}return t},k=e=>e===`number`||e===`bigint`,A=(e,t)=>{let n=e.value,r=O(t);if(!n)return{parsedValue:t,requestedType:r,usedDefault:!0,reason:`value-missing`};if(typeof t==`string`)return{parsedValue:n,requestedType:r,usedDefault:!1,reason:`found-match`};if(typeof t==`boolean`&&e.type===`boolean`){let e=j(n),i=typeof e==`boolean`;return{parsedValue:i?e:t,requestedType:r,usedDefault:!i,reason:i?`found-match`:`invalid-boolean`}}if(k(typeof t)&&e.type===`integer`){let e=M(n),i=typeof e==`number`;return{parsedValue:i?e:t,requestedType:r,usedDefault:!i,reason:i?`found-match`:`invalid-number`}}if(k(typeof t)&&(e.type===`float`||e.type===`enum`)){let e=N(n),i=typeof e==`number`;return{parsedValue:i?e:t,requestedType:r,usedDefault:!i,reason:i?`found-match`:`invalid-number`}}return{parsedValue:n,requestedType:r,usedDefault:!1,reason:`found-match`}},j=e=>{if(!e)return;let t=e.toLowerCase();if(!(t!=`true`&&t!=`false`))return t===`true`},M=e=>{if(!e)return;let t=Number.parseInt(e);if(!(isNaN(t)||!isFinite(t)))return t},N=e=>{if(!e)return;let t=Number.parseFloat(e);if(!(isNaN(t)||!isFinite(t)))return t},P={off:-1,error:0,warn:1,info:2,debug:3},F=()=>{let e={year:`numeric`,month:`2-digit`,day:`2-digit`,hour:`2-digit`,minute:`2-digit`,second:`2-digit`,timeZoneName:`short`};try{return new Intl.DateTimeFormat(void 0,{...e,fractionalSecondDigits:3})}catch{return new Intl.DateTimeFormat(void 0,e)}};var I=class{constructor(e,t){this.level=e,this.decorator=t,this.level=e,this.decorator=t,this.dateFormatter=F()}debug(e,...t){this.log(console.debug,`debug`,e,...t)}info(e,...t){this.log(console.info,`info`,e,...t)}warn(e,...t){this.log(console.warn,`warn`,e,...t)}error(e,...t){this.log(console.error,`error`,e,...t)}log(e,t,n,...r){P[this.level]>=P[t]&&e(`[${this.dateFormatter.format(new Date)}] ${this.decorator?.decorateMessage(n)}`,...r)}},L=class{decorateMessage(e){return`[ConfigDirector:js-server-sdk] ${e}`}};const R=(e,t)=>new I(e??`warn`,t??new L),z=e=>B(new TextEncoder().encode(e)).toString(16).padStart(8,`0`),B=e=>{let t=5381;for(let n=0;n<e.length;n++)t=(t<<5)+t+e[n],t>>>=0;return t},V=e=>{let t=Object.keys(e);if(t.length==0)return!0;for(let n of t)if((e[n]?.length??0)>0)return!1;return!0},H=e=>{if(!e)return!0;let t=Object.keys(e);if(t.length==0)return!0;for(let n of t)if((e[n]??0)>0)return!1;return!0},U=async(e,t,n,r)=>{let i=new AbortController,a=setTimeout(()=>{r.debug(`[fetchWithTimeout] Reached timeout, aborting request`),i.abort()},e);try{let e=await fetch(t,{...n,signal:i.signal});return clearTimeout(a),e}catch(e){throw r.warn(`[fetchWithTimeout] Fetch error: `,e),clearTimeout(a),e}};var W=class{constructor(e){this.executeRequests=!0,this.sdkKey=e.sdkKey,this.logger=e.logger,this.url=new URL(`server/telemetry/v1`,e.baseUrl)}async report({discreteEvents:e,aggregatedEvents:t,droppedEvents:n}){if(!this.executeRequests)return{success:!1,fatalError:!0};let r={serverSdkKey:this.sdkKey,discreteEvents:e,aggregatedEvents:t,droppedEvents:n};if(this.isReportEmpty(r))return{success:!0,fatalError:!1};let i=await this.sendReport(r);return i.fatalError&&(this.executeRequests=!1),i}isReportEmpty(e){return V(e.discreteEvents)&&V(e.aggregatedEvents)&&H(e.droppedEvents)}async sendReport(e){try{let t=await U(5e3,this.url,{method:`POST`,body:JSON.stringify(e)},this.logger),n=this.isStatusFatal(t.status);return n&&this.logger.warn(`[EventReporter] Received a fatal status response (${t.status}) from the telemetry endpoint. No more telemetry data will be sent.`),{success:t.ok,fatalError:n}}catch(e){return this.logger.warn(`[EventReporter] Error attempting to send telemetry data: ${e}`,{error:e}),{success:!1,fatalError:!1}}}isStatusFatal(e){return!!e&&e>=400&&e<500}},G=class{aggregate(e){if(e.events.length==0)return[];let t=new Map;for(let n of e.events){let e=JSON.stringify(n),r=t.get(e)?.count||0;t.set(e,{event:n,count:r+1})}return Array.from(t).map(([,t])=>({startTime:e.startTime,endTime:e.endTime,count:t.count,event:t.event}))}},K=class{constructor(e){this._events=[],this._droppedEventCount=0,this.limit=e??1e3}get events(){return this._events}get reachedLimit(){return this._events.length>=this.limit}get droppedEventCount(){return this._droppedEventCount}push(...e){this.startTime||(this.startTime=new Date);let t=Math.max(0,e.length-this.limit),n=e.slice(t,e.length);if(this._droppedEventCount+=t,this._events.length+n.length>this.limit){let e=this._events.length+n.length-this.limit;this._events.splice(0,e),this._droppedEventCount+=e}return this._events.push(...n)}takeSnapshot(){let e=new Date,t=this.startTime??e,n=this._events.splice(0),r=this._droppedEventCount;return this.startTime=void 0,this._droppedEventCount=0,{startTime:t,endTime:e,events:n,droppedCount:r}}clear(){this._events=[],this.startTime=void 0,this._droppedEventCount=0}},q=class{constructor(e){this.aggregator=new G,this.collectEvents=!0,this.logger=e.logger,this.evaluationEventQueue=new K(e.evaluationQueueLimit??1e3),this.flushIntervalDelay=e.flushIntervalDelay??3e4;let t=e.initialFlushIntervalDelay??5e3;this.flushTimeout=setTimeout(()=>this.flushAndScheduleNext(),t)}sanitizeValue(e,t){if(t===`json`)try{return z(JSON.stringify(e))}catch{return e.toString().slice(0,500)}return e.toString().slice(0,500)}async flushAndScheduleNext(){(await this.flush()).fatalError?(this.collectEvents=!1,this.close(),this.logger.warn(`[TelemetryEventCollector] Received a fatal error from telemetry collection. No longer collecting events.`)):this.flushTimeout=setTimeout(()=>this.flushAndScheduleNext(),this.flushIntervalDelay)}async flush(){if(!this.reporter)return{success:!1,fatalError:!1};let e=this.evaluationEventQueue.takeSnapshot();return await this.reporter?.report({context:this._context,discreteEvents:{},aggregatedEvents:{evaluatedConfig:this.aggregator.aggregate(e)},droppedEvents:{evaluatedConfig:e.droppedCount}})}cancelScheduledFlush(){clearTimeout(this.flushTimeout)}close(){this.collectEvents=!1,clearTimeout(this.flushTimeout),this.flush(),this.evaluationEventQueue.clear()}dispose(){this.close()}},J=class extends Map{constructor(e,t){super(t),this._droppedCount=0,this.limit=e,this.enforceLimit()}set(e,t){return super.set(e,t),this.enforceLimit(),this}get droppedCount(){return this._droppedCount}clearAndReset(){this.clear(),this._droppedCount=0}enforceLimit(){for(;this.size>this.limit;){let e=this.keys().next().value;e!==void 0&&(this.delete(e),this._droppedCount+=1)}}},Y=class extends q{constructor(e){super(e),this.reporter=new W(e),this.contexts=new J(e.contextLimit??1e3)}evaluatedConfig(e){if(!this.collectEvents)return;e.context?.id&&this.contexts.set(e.context.id,e.context);let t={...e.evaluation,contextId:e.context?.id};this.evaluationEventQueue.push(this.sanitizeEvaluatedConfigEvent(t))}sanitizeEvaluatedConfigEvent(e){return{...e,defaultValue:this.sanitizeValue(e.defaultValue,e.type),evaluatedValue:this.sanitizeValue(e.evaluatedValue,e.type)}}snapshotContexts(){let e=Array.from(this.contexts.values()),t=this.contexts.droppedCount;return this.contexts.clearAndReset(),{contexts:e,droppedCount:t}}async flush(){if(!this.reporter)return{success:!1,fatalError:!1};let e=this.evaluationEventQueue.takeSnapshot(),t=this.snapshotContexts();return await this.reporter?.report({context:this._context,discreteEvents:{capturedContexts:t.contexts},aggregatedEvents:{evaluatedConfig:this.aggregator.aggregate(e)},droppedEvents:{evaluatedConfig:e.droppedCount,capturedContexts:t.droppedCount}})}};const X=new URL(`https://server-sdk-api.configdirector.com`);var Z=class{constructor(t,n,r){this.handlersMap=new Map,this.eventEmitter=new e,this.ready=!1,this.logger=r?.logger??R(),this.timeout=r?.connection?.timeout??3e3,this.metaContext=r?.metadata??{},this.configEvaluator=new D(this.logger);let i=this.parseUrl(r?.connection?.url)??X;this.streaming=r?.connection?.streaming!==!1;let a=h,o=r?.telemetry?.eventQueueLimit??5e3;this.usageEventCollector=new Y({sdkKey:t,logger:this.logger,baseUrl:i,flushIntervalDelay:r?.telemetry?.flushInterval??3e4,evaluationQueueLimit:Math.ceil(o*.7),contextLimit:Math.floor(o*.3)}),this.transport=new a({serverSdkKey:t,baseUrl:i,metaContext:{...r?.metadata,sdkName:n.sdkName,sdkVersion:n.sdkVersion},logger:this.logger}),this.transport.on(`configBundleReceived`,e=>{this.readyResolve?.();let t=Object.keys(e.configs);!this.configSet||e.kind==`full`?(this.configSet=e,this.eventEmitter.emit(`configsUpdated`,{keys:t}),this.updateWatchers(e.configs)):(this.configSet.configs={...this.configSet.configs,...e.configs},this.eventEmitter.emit(`configsUpdated`,{keys:t}),this.updateWatchers(e.configs)),this.logger.debug(`[ConfigDirectorClient] ConfigSet updated from server with ${t.length} key(s)`,{keys:t})})}async initialize(){try{this.ready=!1,this.readyPromise=new Promise(e=>{this.readyResolve=e}).then(()=>{this.ready=!0,this.eventEmitter.emit(`clientReady`),this.logger.debug(`[ConfigDirectorClient] Received initial payload from the server, client is ready`)});let e=new Date().getTime();await this.transport.connect(this.timeout);let t=new Date().getTime()-e,n=this.timeout-t;if(n>0&&await Promise.race([this.readyPromise,new Promise(e=>{setTimeout(()=>e(),n)})]),!this.ready){let e=this.streaming?`The client will continue to retry since there were no fatal errors detected. Configs will return the default value until the connection succeeds.`:`Since the client was configured without streaming, configs may not update and always return the default value.`;this.logger.warn(`[ConfigDirectorClient] Timed out waiting for initialization after ${this.timeout}ms. ${e}`)}}catch(e){this.logger.error(`[ConfigDirectorClient] An error occurred during initialization: `,e)}}updateWatchers(e){Object.values(e).forEach(e=>this.updateWatchersForConfig(e))}updateWatchersForConfig(e){this.handlersMap.get(e.key)?.forEach(t=>{let n=this.getValueFromConfigDefinition(e.key,e,t.defaultValue,t.context);t.handler(n)})}watch(e,t,n,r){this.validateDefaultValue(t);let i=this.handlersMap.get(e),a={handler:n,defaultValue:t,requestedType:typeof t,context:r};return i?i.push(a):this.handlersMap.set(e,[a]),()=>this.unwatch(e,n)}unwatch(e,t){let n=this.handlersMap.get(e);if(n)if(t){let e=n.findIndex(e=>e.handler==t);e>=0&&n?.splice(e,1)}else n.splice(0)}getValue(e,t,n){this.validateDefaultValue(t);let r=this.configSet?.configs[e];return this.getValueFromConfigDefinition(e,r,t,n)}getValueFromConfigDefinition(e,t,n,r){if(t){let i=A(this.configEvaluator.evaluate(t,{context:r,metadata:this.metaContext}),n);return this.usageEventCollector.evaluatedConfig({context:r,evaluation:{key:e,defaultValue:n,requestedType:i.requestedType,evaluatedValue:i.parsedValue,usedDefault:i.usedDefault,evaluationReason:i.reason}}),this.logger.debug(`[ConfigDirectorClient] Evaluated '${e}' to '${i.parsedValue}'`),i.parsedValue}else return this.logger.debug(`[ConfigDirectorClient] No config state found for '${e}', returning default value '${n}'`),this.usageEventCollector.evaluatedConfig({context:r,evaluation:{key:e,defaultValue:n,requestedType:O(n),evaluatedValue:n,usedDefault:!0,evaluationReason:`config-state-missing`}}),n}parseUrl(e){if(e)try{return new URL(e)}catch(t){throw new o(`Invalid base URL '${e}'. Parsing failed: ${t}`)}}validateDefaultValue(e){if(e==null)throw new o(`Invalid default value. The default value for a config must be defined and non-null.`);if(typeof e==`function`)throw new o(`Invalid default value. The default value for a config cannot be a function.`)}get isReady(){return this.ready}on(e,t){this.eventEmitter.on(e,t)}off(e,t){t?this.eventEmitter.off(e,t):this.eventEmitter.removeAllListeners(e)}removeAllObservers(){this.logger.debug(`[ConfigDirectorClient] removeAllObservers() has been called, removing all observers`),this.eventEmitter.removeAllListeners(),this.handlersMap.clear()}unwatchAll(){this.handlersMap.clear()}closeConnection(){this.logger.debug(`[ConfigDirectorClient] closeConnection() has been called, closing connection to server`),this.transport.close(),this.ready=!1}dispose(){this.removeAllObservers(),this.closeConnection()}};const Q=(e,t)=>new Z(e,{sdkName:`js-server-sdk`,sdkVersion:`0.2.1`},t),$=(e,t)=>R(e,t);export{Q as createClient,$ as createConsoleLogger};
4
4
  //# sourceMappingURL=configdirector-server.mjs.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@configdirector/server-sdk",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Javascript server SDK for ConfigDirector. ConfigDirector is a remote configuration and feature flag service.",
5
5
  "license": "MIT",
6
6
  "author": "ConfigDirector",