@schematichq/schematic-js 1.2.9 → 1.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/schematic.browser.js +2 -2
- package/dist/schematic.cjs.js +185 -117
- package/dist/schematic.d.ts +7 -2
- package/dist/schematic.esm.js +185 -117
- package/package.json +2 -2
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
"use strict";(()=>{var re=Object.create;var Q=Object.defineProperty;var se=Object.getOwnPropertyDescriptor;var ie=Object.getOwnPropertyNames;var ae=Object.getPrototypeOf,oe=Object.prototype.hasOwnProperty;var le=(
|
|
2
|
-
`)===0?p.substr(1,p.length):p}).forEach(function(p){var y=p.split(":"),f=y.shift().trim();if(f){var _=y.join(":").trim();try{a.append(f,_)}catch(P){console.warn("Response "+P.message)}}}),a}q.call(S.prototype);function w(n,a){if(!(this instanceof w))throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.');if(a||(a={}),this.type="default",this.status=a.status===void 0?200:a.status,this.status<200||this.status>599)throw new RangeError("Failed to construct 'Response': The status provided (0) is outside the range [200, 599].");this.ok=this.status>=200&&this.status<300,this.statusText=a.statusText===void 0?"":""+a.statusText,this.headers=new h(a.headers),this.url=a.url||"",this._initBody(n)}q.call(w.prototype),w.prototype.clone=function(){return new w(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new h(this.headers),url:this.url})},w.error=function(){var n=new w(null,{status:200,statusText:""});return n.ok=!1,n.status=0,n.type="error",n};var ne=[301,302,303,307,308];w.redirect=function(n,a){if(ne.indexOf(a)===-1)throw new RangeError("Invalid status code");return new w(null,{status:a,headers:{location:n}})},t.DOMException=i.DOMException;try{new t.DOMException}catch{t.DOMException=function(a,u){this.message=a,this.name=u;var p=Error(a);this.stack=p.stack},t.DOMException.prototype=Object.create(Error.prototype),t.DOMException.prototype.constructor=t.DOMException}function A(n,a){return new Promise(function(u,p){var y=new S(n,a);if(y.signal&&y.signal.aborted)return p(new t.DOMException("Aborted","AbortError"));var f=new XMLHttpRequest;function _(){f.abort()}f.onload=function(){var v={statusText:f.statusText,headers:te(f.getAllResponseHeaders()||"")};y.url.indexOf("file://")===0&&(f.status<200||f.status>599)?v.status=200:v.status=f.status,v.url="responseURL"in f?f.responseURL:v.headers.get("X-Request-URL");var x="response"in f?f.response:f.responseText;setTimeout(function(){u(new w(x,v))},0)},f.onerror=function(){setTimeout(function(){p(new TypeError("Network request failed"))},0)},f.ontimeout=function(){setTimeout(function(){p(new TypeError("Network request timed out"))},0)},f.onabort=function(){setTimeout(function(){p(new t.DOMException("Aborted","AbortError"))},0)};function P(v){try{return v===""&&i.location.href?i.location.href:v}catch{return v}}if(f.open(y.method,P(y.url),!0),y.credentials==="include"?f.withCredentials=!0:y.credentials==="omit"&&(f.withCredentials=!1),"responseType"in f&&(s.blob?f.responseType="blob":s.arrayBuffer&&(f.responseType="arraybuffer")),a&&typeof a.headers=="object"&&!(a.headers instanceof h||i.Headers&&a.headers instanceof i.Headers)){var W=[];Object.getOwnPropertyNames(a.headers).forEach(function(v){W.push(d(v)),f.setRequestHeader(v,g(a.headers[v]))}),y.headers.forEach(function(v,x){W.indexOf(x)===-1&&f.setRequestHeader(x,v)})}else y.headers.forEach(function(v,x){f.setRequestHeader(x,v)});y.signal&&(y.signal.addEventListener("abort",_),f.onreadystatechange=function(){f.readyState===4&&y.signal.removeEventListener("abort",_)}),f.send(typeof y._bodyInit>"u"?null:y._bodyInit)})}return A.polyfill=!0,i.fetch||(i.fetch=A,i.Headers=h,i.Request=S,i.Response=w),t.Headers=h,t.Request=S,t.Response=w,t.fetch=A,t})({})})(typeof self<"u"?self:z)});var k=[];for(let r=0;r<256;++r)k.push((r+256).toString(16).slice(1));function H(r,e=0){return(k[r[e+0]]+k[r[e+1]]+k[r[e+2]]+k[r[e+3]]+"-"+k[r[e+4]]+k[r[e+5]]+"-"+k[r[e+6]]+k[r[e+7]]+"-"+k[r[e+8]]+k[r[e+9]]+"-"+k[r[e+10]]+k[r[e+11]]+k[r[e+12]]+k[r[e+13]]+k[r[e+14]]+k[r[e+15]]).toLowerCase()}var L,de=new Uint8Array(16);function B(){if(!L){if(typeof crypto>"u"||!crypto.getRandomValues)throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");L=crypto.getRandomValues.bind(crypto)}return L(de)}var fe=typeof crypto<"u"&&crypto.randomUUID&&crypto.randomUUID.bind(crypto),N={randomUUID:fe};function he(r,e,t){r=r||{};let i=r.random??r.rng?.()??B();if(i.length<16)throw new Error("Random bytes length must be >= 16");if(i[6]=i[6]&15|64,i[8]=i[8]&63|128,e){if(t=t||0,t<0||t+16>e.length)throw new RangeError(`UUID byte range ${t}:${t+15} is out of buffer bounds`);for(let s=0;s<16;++s)e[t+s]=i[s];return e}return H(i)}function ge(r,e,t){return N.randomUUID&&!e&&!r?N.randomUUID():he(r,e,t)}var C=ge;var st=ue(K());function T(r){return pe(r,!1)}function pe(r,e){return r==null?r:{companyId:r.company_id==null?void 0:r.company_id,error:r.error==null?void 0:r.error,featureAllocation:r.feature_allocation==null?void 0:r.feature_allocation,featureUsage:r.feature_usage==null?void 0:r.feature_usage,featureUsageEvent:r.feature_usage_event==null?void 0:r.feature_usage_event,featureUsagePeriod:r.feature_usage_period==null?void 0:r.feature_usage_period,featureUsageResetAt:r.feature_usage_reset_at==null?void 0:new Date(r.feature_usage_reset_at),flag:r.flag,flagId:r.flag_id==null?void 0:r.flag_id,reason:r.reason,ruleId:r.rule_id==null?void 0:r.rule_id,ruleType:r.rule_type==null?void 0:r.rule_type,userId:r.user_id==null?void 0:r.user_id,value:r.value}}function M(r){return ye(r,!1)}function ye(r,e=!1){return r==null?r:{company_id:r.companyId,error:r.error,flag_id:r.flagId,flag_key:r.flagKey,reason:r.reason,req_company:r.reqCompany,req_user:r.reqUser,rule_id:r.ruleId,user_id:r.userId,value:r.value}}function U(r){return be(r,!1)}function be(r,e){return r==null?r:{data:T(r.data),params:r.params}}function X(r){return ve(r,!1)}function ve(r,e){return r==null?r:{flags:r.flags.map(T)}}function V(r){return ke(r,!1)}function ke(r,e){return r==null?r:{data:X(r.data),params:r.params}}var D=r=>{let{companyId:e,error:t,featureAllocation:i,featureUsage:s,featureUsageEvent:l,featureUsagePeriod:o,featureUsageResetAt:c,flag:d,flagId:g,reason:E,ruleId:h,ruleType:b,userId:m,value:F}=T(r);return{featureUsageExceeded:!F&&(b=="company_override_usage_exceeded"||b=="plan_entitlement_usage_exceeded"),companyId:e??void 0,error:t??void 0,featureAllocation:i??void 0,featureUsage:s??void 0,featureUsageEvent:l===null?void 0:l,featureUsagePeriod:o??void 0,featureUsageResetAt:c??void 0,flag:d,flagId:g??void 0,reason:E,ruleId:h??void 0,ruleType:b??void 0,userId:m??void 0,value:F}};function R(r){let e=Object.keys(r).reduce((t,i)=>{let l=Object.keys(r[i]||{}).sort().reduce((o,c)=>(o[c]=r[i][c],o),{});return t[i]=l,t},{});return JSON.stringify(e)}var O="1.2.9";var G="schematicId";var I=class{additionalHeaders={};apiKey;apiUrl="https://api.schematichq.com";conn=null;context={};debugEnabled=!1;offlineEnabled=!1;eventQueue;contextDependentEventQueue;eventUrl="https://c.schematichq.com";flagCheckListeners={};flagValueListeners={};isPending=!0;isPendingListeners=new Set;storage;useWebSocket=!1;checks={};featureUsageEventMap={};webSocketUrl="wss://api.schematichq.com";webSocketConnectionTimeout=1e4;webSocketReconnect=!0;webSocketMaxReconnectAttempts=7;webSocketInitialRetryDelay=1e3;webSocketMaxRetryDelay=3e4;wsReconnectAttempts=0;wsReconnectTimer=null;wsIntentionalDisconnect=!1;maxEventQueueSize=100;maxEventRetries=5;eventRetryInitialDelay=1e3;eventRetryMaxDelay=3e4;retryTimer=null;flagValueDefaults={};flagCheckDefaults={};constructor(e,t){if(this.apiKey=e,this.eventQueue=[],this.contextDependentEventQueue=[],this.useWebSocket=t?.useWebSocket??!1,this.debugEnabled=t?.debug??!1,this.offlineEnabled=t?.offline??!1,typeof window<"u"&&typeof window.location<"u"){let i=new URLSearchParams(window.location.search),s=i.get("schematic_debug");s!==null&&(s===""||s==="true"||s==="1")&&(this.debugEnabled=!0);let l=i.get("schematic_offline");l!==null&&(l===""||l==="true"||l==="1")&&(this.offlineEnabled=!0,this.debugEnabled=!0)}this.offlineEnabled&&t?.debug!==!1&&(this.debugEnabled=!0),this.offlineEnabled&&this.setIsPending(!1),this.additionalHeaders={"X-Schematic-Client-Version":`schematic-js@${O}`,...t?.additionalHeaders??{}},t?.storage?this.storage=t.storage:typeof localStorage<"u"&&(this.storage=localStorage),t?.apiUrl!==void 0&&(this.apiUrl=t.apiUrl),t?.eventUrl!==void 0&&(this.eventUrl=t.eventUrl),t?.webSocketUrl!==void 0&&(this.webSocketUrl=t.webSocketUrl),t?.webSocketConnectionTimeout!==void 0&&(this.webSocketConnectionTimeout=t.webSocketConnectionTimeout),t?.webSocketReconnect!==void 0&&(this.webSocketReconnect=t.webSocketReconnect),t?.webSocketMaxReconnectAttempts!==void 0&&(this.webSocketMaxReconnectAttempts=t.webSocketMaxReconnectAttempts),t?.webSocketInitialRetryDelay!==void 0&&(this.webSocketInitialRetryDelay=t.webSocketInitialRetryDelay),t?.webSocketMaxRetryDelay!==void 0&&(this.webSocketMaxRetryDelay=t.webSocketMaxRetryDelay),t?.maxEventQueueSize!==void 0&&(this.maxEventQueueSize=t.maxEventQueueSize),t?.maxEventRetries!==void 0&&(this.maxEventRetries=t.maxEventRetries),t?.eventRetryInitialDelay!==void 0&&(this.eventRetryInitialDelay=t.eventRetryInitialDelay),t?.eventRetryMaxDelay!==void 0&&(this.eventRetryMaxDelay=t.eventRetryMaxDelay),t?.flagValueDefaults!==void 0&&(this.flagValueDefaults=t.flagValueDefaults),t?.flagCheckDefaults!==void 0&&(this.flagCheckDefaults=t.flagCheckDefaults),typeof window<"u"&&window?.addEventListener&&(window.addEventListener("beforeunload",()=>{this.flushEventQueue(),this.flushContextDependentEventQueue()}),this.useWebSocket&&(window.addEventListener("offline",()=>{this.debug("Browser went offline, closing WebSocket connection"),this.handleNetworkOffline()}),window.addEventListener("online",()=>{this.debug("Browser came online, attempting to reconnect WebSocket"),this.handleNetworkOnline()}))),this.offlineEnabled?this.debug("Initialized with offline mode enabled - no network requests will be made"):this.debugEnabled&&this.debug("Initialized with debug mode enabled")}resolveFallbackValue(e,t){return t!==void 0?t:e in this.flagValueDefaults?this.flagValueDefaults[e]:!1}resolveFallbackCheckFlagReturn(e,t,i="Fallback value used",s){if(t!==void 0)return{flag:e,value:t,reason:i,error:s};if(e in this.flagCheckDefaults){let l=this.flagCheckDefaults[e];return{...l,flag:e,reason:s!==void 0?i:l.reason,error:s}}return e in this.flagValueDefaults?{flag:e,value:this.flagValueDefaults[e],reason:i,error:s}:{flag:e,value:!1,reason:i,error:s}}async checkFlag(e){let{fallback:t,key:i}=e,s=e.context||this.context,l=R(s);if(this.debug(`checkFlag: ${i}`,{context:s,fallback:t}),this.isOffline()){let o=this.resolveFallbackCheckFlagReturn(i,t,"Offline mode - using initialization defaults");return this.debug(`checkFlag offline result: ${i}`,{value:o.value,offlineMode:!0}),o.value}if(!this.useWebSocket){let o=`${this.apiUrl}/flags/${i}/check`;return fetch(o,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8","X-Schematic-Api-Key":this.apiKey},body:JSON.stringify(s)}).then(c=>{if(!c.ok)throw new Error("Network response was not ok");return c.json()}).then(c=>{let d=U(c);this.debug(`checkFlag result: ${i}`,d);let g=D(d.data);return typeof g.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(g),this.submitFlagCheckEvent(i,g,s),g.value}).catch(c=>{console.error("There was a problem with the fetch operation:",c);let d=this.resolveFallbackCheckFlagReturn(i,t,"API request failed",c instanceof Error?c.message:String(c));return this.submitFlagCheckEvent(i,d,s),d.value})}try{let o=this.checks[l];if(this.conn!==null&&typeof o<"u"&&typeof o[i]<"u")return this.debug(`checkFlag cached result: ${i}`,o[i]),o[i].value;if(this.isOffline())return this.resolveFallbackValue(i,t);try{await this.setContext(s)}catch(E){return console.error("WebSocket connection failed, falling back to REST:",E),this.fallbackToRest(i,s,t)}let d=(this.checks[l]??{})[i],g=d?.value??this.resolveFallbackValue(i,t);return this.debug(`checkFlag WebSocket result: ${i}`,typeof d<"u"?d:{value:g,fallbackUsed:!0}),typeof d<"u"&&this.submitFlagCheckEvent(i,d,s),g}catch(o){console.error("Unexpected error in checkFlag:",o);let c=this.resolveFallbackCheckFlagReturn(i,t,"Unexpected error in flag check",o instanceof Error?o.message:String(o));return this.submitFlagCheckEvent(i,c,s),c.value}}debug(e,...t){this.debugEnabled&&console.log(`[Schematic] ${e}`,...t)}isOffline(){return this.offlineEnabled}submitFlagCheckEvent(e,t,i){let s={flagKey:e,value:t.value,reason:t.reason,flagId:t.flagId,ruleId:t.ruleId,companyId:t.companyId,userId:t.userId,error:t.error,reqCompany:i.company,reqUser:i.user};return this.debug("submitting flag check event:",s),this.handleEvent("flag_check",M(s))}async fallbackToRest(e,t,i){if(this.isOffline()){let s=this.resolveFallbackValue(e,i);return this.debug(`fallbackToRest offline result: ${e}`,{value:s,offlineMode:!0}),s}try{let s=`${this.apiUrl}/flags/${e}/check`,l=await fetch(s,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8","X-Schematic-Api-Key":this.apiKey},body:JSON.stringify(t)});if(!l.ok)throw new Error("Network response was not ok");let o=await l.json(),c=U(o);this.debug(`fallbackToRest result: ${e}`,c);let d=D(c.data);return typeof d.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(d),this.submitFlagCheckEvent(e,d,t),d.value}catch(s){console.error("REST API call failed, using fallback value:",s);let l=this.resolveFallbackCheckFlagReturn(e,i,"API request failed (fallback)",s instanceof Error?s.message:String(s));return this.submitFlagCheckEvent(e,l,t),l.value}}checkFlags=async e=>{if(e=e||this.context,this.debug("checkFlags",{context:e}),this.isOffline())return this.debug("checkFlags offline result: returning empty object"),{};let t=`${this.apiUrl}/flags/check`,i=JSON.stringify(e);return fetch(t,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8","X-Schematic-Api-Key":this.apiKey},body:i}).then(s=>{if(!s.ok)throw new Error("Network response was not ok");return s.json()}).then(s=>{let l=V(s);return this.debug("checkFlags result:",l),(l?.data?.flags??[]).reduce((o,c)=>(o[c.flag]=c.value,o),{})}).catch(s=>(console.error("There was a problem with the fetch operation:",s),{}))};identify=e=>{this.debug("identify:",e);try{this.setContext({company:e.company?.keys,user:e.keys})}catch(t){console.error("Error setting context:",t)}return this.handleEvent("identify",e)};setContext=async e=>{if(this.isOffline()||!this.useWebSocket)return this.context=e,this.flushContextDependentEventQueue(),this.setIsPending(!1),Promise.resolve();try{this.setIsPending(!0),this.conn||(this.wsReconnectTimer!==null&&(this.debug("Cancelling scheduled reconnection, connecting immediately"),clearTimeout(this.wsReconnectTimer),this.wsReconnectTimer=null),this.conn=this.wsConnect());let t=await this.conn;await this.wsSendMessage(t,e)}catch(t){throw console.error("Failed to establish WebSocket connection:",t),t}};track=e=>{let{company:t,user:i,event:s,traits:l,quantity:o=1}=e;if(!this.hasContext(t,i)){this.debug(`track: queuing event "${s}" until context is available`);let d={api_key:this.apiKey,body:{company:t,event:s,traits:l??{},user:i,quantity:o},sent_at:new Date().toISOString(),tracker_event_id:C(),tracker_user_id:this.getAnonymousId(),type:"track"};return this.contextDependentEventQueue.push(d),Promise.resolve()}let c={company:t??this.context.company,event:s,traits:l??{},user:i??this.context.user,quantity:o};return this.debug("track:",c),s in this.featureUsageEventMap&&this.optimisticallyUpdateFeatureUsage(s,o),this.handleEvent("track",c)};optimisticallyUpdateFeatureUsage=(e,t=1)=>{let i=this.featureUsageEventMap[e];i!=null&&(this.debug(`Optimistically updating feature usage for event: ${e}`,{quantity:t}),Object.entries(i).forEach(([s,l])=>{if(l===void 0)return;let o={...l};if(typeof o.featureUsage=="number"){if(o.featureUsage+=t,typeof o.featureAllocation=="number"){let d=o.featureUsageExceeded===!0,g=o.featureUsage>=o.featureAllocation;g!==d&&(o.featureUsageExceeded=g,g&&(o.value=!1),this.debug(`Usage limit status changed for flag: ${s}`,{was:d?"exceeded":"within limits",now:g?"exceeded":"within limits",featureUsage:o.featureUsage,featureAllocation:o.featureAllocation,value:o.value}))}this.featureUsageEventMap[e]!==void 0&&(this.featureUsageEventMap[e][s]=o);let c=R(this.context);this.checks[c]!==void 0&&this.checks[c]!==null&&(this.checks[c][s]=o),this.notifyFlagCheckListeners(s,o),this.notifyFlagValueListeners(s,o.value)}}))};hasContext=(e,t)=>{let i=e!=null&&Object.keys(e).length>0||t!=null&&Object.keys(t).length>0,s=this.context.company!==void 0&&this.context.company!==null&&Object.keys(this.context.company).length>0||this.context.user!==void 0&&this.context.user!==null&&Object.keys(this.context.user).length>0;return i||s};flushContextDependentEventQueue=()=>{for(this.debug(`flushing ${this.contextDependentEventQueue.length} context-dependent events`);this.contextDependentEventQueue.length>0;){let e=this.contextDependentEventQueue.shift();if(e)if(e.type==="track"&&typeof e.body=="object"&&e.body!==null){let t=e.body,i={...t,company:t.company??this.context.company,user:t.user??this.context.user},s={...e,body:i,sent_at:new Date().toISOString()};this.sendEvent(s)}else this.sendEvent(e)}};startRetryTimer=()=>{this.retryTimer===null&&(this.retryTimer=setInterval(()=>{this.flushEventQueue().catch(e=>{this.debug("Error in retry timer flush:",e)}),this.eventQueue.length===0&&this.stopRetryTimer()},5e3),this.debug("Started retry timer"))};stopRetryTimer=()=>{this.retryTimer!==null&&(clearInterval(this.retryTimer),this.retryTimer=null,this.debug("Stopped retry timer"))};flushEventQueue=async()=>{if(this.eventQueue.length===0)return;let e=Date.now(),t=[],i=[];for(let s of this.eventQueue)s.next_retry_at===void 0||s.next_retry_at<=e?t.push(s):i.push(s);if(t.length===0){this.debug(`No events ready for retry yet (${i.length} still in backoff)`);return}this.debug(`Flushing event queue: ${t.length} ready, ${i.length} waiting`),this.eventQueue=i;for(let s of t)try{await this.sendEvent(s),this.debug("Queued event sent successfully:",s.type)}catch(l){this.debug("Failed to send queued event:",l)}};getAnonymousId=()=>{if(!this.storage)return C();let e=this.storage.getItem(G);if(typeof e<"u")return e;let t=C();return this.storage.setItem(G,t),t};handleEvent=(e,t)=>{let i={api_key:this.apiKey,body:t,sent_at:new Date().toISOString(),tracker_event_id:C(),tracker_user_id:this.getAnonymousId(),type:e};return typeof document<"u"&&document?.hidden?this.storeEvent(i):this.sendEvent(i)};sendEvent=async e=>{let t=`${this.eventUrl}/e`,i=JSON.stringify(e);if(this.debug("sending event:",{url:t,event:e}),this.isOffline())return this.debug("event not sent (offline mode):",{event:e}),Promise.resolve();try{let s=await fetch(t,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8"},body:i});if(!s.ok)throw new Error(`HTTP ${s.status}: ${s.statusText}`);this.debug("event sent:",{status:s.status,statusText:s.statusText})}catch(s){let l=(e.retry_count??0)+1;if(l<=this.maxEventRetries){this.debug(`Event failed to send (attempt ${l}/${this.maxEventRetries}), queueing for retry:`,s);let o=this.eventRetryInitialDelay*Math.pow(2,l-1),c=Math.min(o,this.eventRetryMaxDelay),d=Date.now()+c,g={...e,retry_count:l,next_retry_at:d};this.eventQueue.length<this.maxEventQueueSize?(this.eventQueue.push(g),this.debug(`Event queued for retry in ${c}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`)):(this.debug(`Event queue full (${this.maxEventQueueSize}), dropping oldest event`),this.eventQueue.shift(),this.eventQueue.push(g)),this.startRetryTimer()}else this.debug(`Event failed permanently after ${this.maxEventRetries} attempts, dropping:`,s)}return Promise.resolve()};storeEvent=e=>(this.eventQueue.push(e),Promise.resolve());cleanup=async()=>{if(this.isOffline())return this.debug("cleanup: skipped (offline mode)"),Promise.resolve();if(this.wsIntentionalDisconnect=!0,this.wsReconnectTimer!==null&&(clearTimeout(this.wsReconnectTimer),this.wsReconnectTimer=null),this.stopRetryTimer(),this.conn)try{(await this.conn).close()}catch(e){console.error("Error during cleanup:",e)}finally{this.conn=null}};calculateReconnectDelay=()=>{let e=this.webSocketInitialRetryDelay*Math.pow(2,this.wsReconnectAttempts),t=Math.min(e,this.webSocketMaxRetryDelay),i=Math.random()*t*.5,s=t+i;return this.debug(`Reconnect delay calculated: ${s.toFixed(0)}ms (attempt ${this.wsReconnectAttempts+1}/${this.webSocketMaxReconnectAttempts})`),s};handleNetworkOffline=async()=>{if(this.conn!==null){try{(await this.conn).close()}catch(e){this.debug("Error closing connection on offline:",e)}this.conn=null}this.wsReconnectTimer!==null&&(clearTimeout(this.wsReconnectTimer),this.wsReconnectTimer=null)};handleNetworkOnline=()=>{this.debug("Network online, attempting reconnection and flushing queued events"),this.wsReconnectAttempts=0,this.wsReconnectTimer!==null&&(clearTimeout(this.wsReconnectTimer),this.wsReconnectTimer=null),this.flushEventQueue().catch(e=>{this.debug("Error flushing event queue on network online:",e)}),this.attemptReconnect()};attemptReconnect=()=>{if(this.wsReconnectAttempts>=this.webSocketMaxReconnectAttempts){this.debug(`Maximum reconnection attempts (${this.webSocketMaxReconnectAttempts}) reached, giving up`);return}this.wsReconnectTimer!==null&&clearTimeout(this.wsReconnectTimer);let e=this.calculateReconnectDelay();this.debug(`Scheduling reconnection attempt ${this.wsReconnectAttempts+1}/${this.webSocketMaxReconnectAttempts} in ${e.toFixed(0)}ms`),this.wsReconnectTimer=setTimeout(async()=>{this.wsReconnectTimer=null,this.wsReconnectAttempts++,this.debug(`Attempting to reconnect (attempt ${this.wsReconnectAttempts}/${this.webSocketMaxReconnectAttempts})`);try{this.conn=this.wsConnect();let t=await this.conn;this.debug("Reconnection context check:",{hasCompany:this.context.company!==void 0,hasUser:this.context.user!==void 0,context:this.context}),this.context.company!==void 0||this.context.user!==void 0?(this.debug("Reconnected, force re-sending context"),await this.wsSendContextAfterReconnection(t,this.context)):this.debug("No context to re-send after reconnection - websocket ready for new context"),this.flushEventQueue().catch(i=>{this.debug("Error flushing event queue after websocket reconnection:",i)}),this.debug("Reconnection successful")}catch(t){this.debug("Reconnection attempt failed:",t)}},e)};wsConnect=()=>this.isOffline()?(this.debug("wsConnect: skipped (offline mode)"),Promise.reject(new Error("WebSocket connection skipped in offline mode"))):new Promise((e,t)=>{let i=`${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;this.debug("connecting to WebSocket:",i);let s=new WebSocket(i),l=null,o=!1;l=setTimeout(()=>{o||(this.debug(`WebSocket connection timeout after ${this.webSocketConnectionTimeout}ms`),s.close(),t(new Error("WebSocket connection timeout")))},this.webSocketConnectionTimeout),s.onopen=()=>{o=!0,l!==null&&clearTimeout(l),this.wsReconnectAttempts=0,this.wsIntentionalDisconnect=!1,this.debug("WebSocket connection opened"),e(s)},s.onerror=c=>{o=!0,l!==null&&clearTimeout(l),this.debug("WebSocket connection error:",c),t(c)},s.onclose=()=>{o=!0,l!==null&&clearTimeout(l),this.debug("WebSocket connection closed"),this.conn=null,!this.wsIntentionalDisconnect&&this.webSocketReconnect&&this.attemptReconnect()}});wsSendContextAfterReconnection=(e,t)=>this.isOffline()?(this.debug("wsSendContextAfterReconnection: skipped (offline mode)"),this.setIsPending(!1),Promise.resolve()):new Promise(i=>{this.debug("WebSocket force sending context after reconnection:",t),this.context=t;let s=()=>{let l=!1,o=g=>{let E=JSON.parse(g.data);this.debug("WebSocket message received after reconnection:",E),R(t)in this.checks||(this.checks[R(t)]={}),(E.flags??[]).forEach(h=>{let b=D(h),m=R(t);this.checks[m]===void 0&&(this.checks[m]={}),this.checks[m][b.flag]=b}),this.useWebSocket=!0,e.removeEventListener("message",o),l||(l=!0,i(this.setIsPending(!1)))};e.addEventListener("message",o);let c=this.additionalHeaders["X-Schematic-Client-Version"]??`schematic-js@${O}`,d={apiKey:this.apiKey,clientVersion:c,data:t};this.debug("WebSocket sending forced message after reconnection:",d),e.send(JSON.stringify(d))};e.readyState===WebSocket.OPEN?(this.debug("WebSocket already open, sending forced message after reconnection"),s()):e.addEventListener("open",()=>{this.debug("WebSocket opened, sending forced message after reconnection"),s()})});wsSendMessage=(e,t)=>this.isOffline()?(this.debug("wsSendMessage: skipped (offline mode)"),this.setIsPending(!1),Promise.resolve()):new Promise((i,s)=>{if(R(t)==R(this.context))return this.debug("WebSocket context unchanged, skipping update"),i(this.setIsPending(!1));this.debug("WebSocket context updated:",t),this.context=t;let l=()=>{let o=!1,c=E=>{let h=JSON.parse(E.data);this.debug("WebSocket message received:",h),R(t)in this.checks||(this.checks[R(t)]={}),(h.flags??[]).forEach(b=>{let m=D(b),F=R(t);this.checks[F]===void 0&&(this.checks[F]={}),this.checks[F][m.flag]=m,this.debug("WebSocket flag update:",{flag:m.flag,value:m.value,flagCheck:m}),typeof m.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(m),(this.flagCheckListeners[b.flag]?.size>0||this.flagValueListeners[b.flag]?.size>0)&&this.submitFlagCheckEvent(m.flag,m,t),this.notifyFlagCheckListeners(b.flag,m),this.notifyFlagValueListeners(b.flag,m.value)}),this.flushContextDependentEventQueue(),this.setIsPending(!1),o||(o=!0,i())};e.addEventListener("message",c);let d=this.additionalHeaders["X-Schematic-Client-Version"]??`schematic-js@${O}`,g={apiKey:this.apiKey,clientVersion:d,data:t};this.debug("WebSocket sending message:",g),e.send(JSON.stringify(g))};e.readyState===WebSocket.OPEN?(this.debug("WebSocket already open, sending message"),l()):e.readyState===WebSocket.CONNECTING?(this.debug("WebSocket connecting, waiting for open to send message"),e.addEventListener("open",l)):(this.debug("WebSocket is closed, cannot send message"),s("WebSocket is not open or connecting"))});getIsPending=()=>this.isPending;addIsPendingListener=e=>(this.isPendingListeners.add(e),()=>{this.isPendingListeners.delete(e)});setIsPending=e=>{this.isPending=e,this.isPendingListeners.forEach(t=>Ee(t,e))};getFlagCheck=e=>{let t=R(this.context);return(this.checks[t]??{})[e]};getFlagValue=e=>this.getFlagCheck(e)?.value;addFlagValueListener=(e,t)=>(e in this.flagValueListeners||(this.flagValueListeners[e]=new Set),this.flagValueListeners[e].add(t),()=>{this.flagValueListeners[e].delete(t)});addFlagCheckListener=(e,t)=>(e in this.flagCheckListeners||(this.flagCheckListeners[e]=new Set),this.flagCheckListeners[e].add(t),()=>{this.flagCheckListeners[e].delete(t)});notifyFlagCheckListeners=(e,t)=>{let i=this.flagCheckListeners?.[e]??[];i.size>0&&this.debug(`Notifying ${i.size} flag check listeners for ${e}`,t),typeof t.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(t),i.forEach(s=>Re(s,t))};updateFeatureUsageEventMap=e=>{if(typeof e.featureUsageEvent!="string")return;let t=e.featureUsageEvent;(this.featureUsageEventMap[t]===void 0||this.featureUsageEventMap[t]===null)&&(this.featureUsageEventMap[t]={}),this.featureUsageEventMap[t]!==void 0&&(this.featureUsageEventMap[t][e.flag]=e),this.debug(`Updated featureUsageEventMap for event: ${t}, flag: ${e.flag}`,e)};notifyFlagValueListeners=(e,t)=>{let i=this.flagValueListeners?.[e]??[];i.size>0&&this.debug(`Notifying ${i.size} flag value listeners for ${e}`,{value:t}),i.forEach(s=>we(s,t))}},Ee=(r,e)=>{r.length>0?r(e):r()},Re=(r,e)=>{r.length>0?r(e):r()},we=(r,e)=>{r.length>0?r(e):r()};window.Schematic=I;})();
|
|
1
|
+
"use strict";(()=>{var re=Object.create;var Q=Object.defineProperty;var se=Object.getOwnPropertyDescriptor;var ie=Object.getOwnPropertyNames;var ae=Object.getPrototypeOf,oe=Object.prototype.hasOwnProperty;var le=(s,e)=>()=>(e||s((e={exports:{}}).exports,e),e.exports);var ce=(s,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of ie(e))!oe.call(s,r)&&r!==t&&Q(s,r,{get:()=>e[r],enumerable:!(i=se(e,r))||i.enumerable});return s};var ue=(s,e,t)=>(t=s!=null?re(ae(s)):{},ce(e||!s||!s.__esModule?Q(t,"default",{value:s,enumerable:!0}):t,s));var K=le(z=>{(function(s){var e=(function(t){var i=typeof globalThis<"u"&&globalThis||typeof s<"u"&&s||typeof global<"u"&&global||{},r={searchParams:"URLSearchParams"in i,iterable:"Symbol"in i&&"iterator"in Symbol,blob:"FileReader"in i&&"Blob"in i&&(function(){try{return new Blob,!0}catch{return!1}})(),formData:"FormData"in i,arrayBuffer:"ArrayBuffer"in i};function l(n){return n&&DataView.prototype.isPrototypeOf(n)}if(r.arrayBuffer)var o=["[object Int8Array]","[object Uint8Array]","[object Uint8ClampedArray]","[object Int16Array]","[object Uint16Array]","[object Int32Array]","[object Uint32Array]","[object Float32Array]","[object Float64Array]"],c=ArrayBuffer.isView||function(n){return n&&o.indexOf(Object.prototype.toString.call(n))>-1};function d(n){if(typeof n!="string"&&(n=String(n)),/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(n)||n==="")throw new TypeError('Invalid character in header field name: "'+n+'"');return n.toLowerCase()}function p(n){return typeof n!="string"&&(n=String(n)),n}function E(n){var a={next:function(){var u=n.shift();return{done:u===void 0,value:u}}};return r.iterable&&(a[Symbol.iterator]=function(){return a}),a}function g(n){this.map={},n instanceof g?n.forEach(function(a,u){this.append(u,a)},this):Array.isArray(n)?n.forEach(function(a){if(a.length!=2)throw new TypeError("Headers constructor: expected name/value pair to be length 2, found"+a.length);this.append(a[0],a[1])},this):n&&Object.getOwnPropertyNames(n).forEach(function(a){this.append(a,n[a])},this)}g.prototype.append=function(n,a){n=d(n),a=p(a);var u=this.map[n];this.map[n]=u?u+", "+a:a},g.prototype.delete=function(n){delete this.map[d(n)]},g.prototype.get=function(n){return n=d(n),this.has(n)?this.map[n]:null},g.prototype.has=function(n){return this.map.hasOwnProperty(d(n))},g.prototype.set=function(n,a){this.map[d(n)]=p(a)},g.prototype.forEach=function(n,a){for(var u in this.map)this.map.hasOwnProperty(u)&&n.call(a,this.map[u],u,this)},g.prototype.keys=function(){var n=[];return this.forEach(function(a,u){n.push(u)}),E(n)},g.prototype.values=function(){var n=[];return this.forEach(function(a){n.push(a)}),E(n)},g.prototype.entries=function(){var n=[];return this.forEach(function(a,u){n.push([u,a])}),E(n)},r.iterable&&(g.prototype[Symbol.iterator]=g.prototype.entries);function k(n){if(!n._noBody){if(n.bodyUsed)return Promise.reject(new TypeError("Already read"));n.bodyUsed=!0}}function x(n){return new Promise(function(a,u){n.onload=function(){a(n.result)},n.onerror=function(){u(n.error)}})}function T(n){var a=new FileReader,u=x(a);return a.readAsArrayBuffer(n),u}function V(n){var a=new FileReader,u=x(a),h=/charset=([A-Za-z0-9_-]+)/.exec(n.type),y=h?h[1]:"utf-8";return a.readAsText(n,y),u}function Y(n){for(var a=new Uint8Array(n),u=new Array(a.length),h=0;h<a.length;h++)u[h]=String.fromCharCode(a[h]);return u.join("")}function W(n){if(n.slice)return n.slice(0);var a=new Uint8Array(n.byteLength);return a.set(new Uint8Array(n)),a.buffer}function J(){return this.bodyUsed=!1,this._initBody=function(n){this.bodyUsed=this.bodyUsed,this._bodyInit=n,n?typeof n=="string"?this._bodyText=n:r.blob&&Blob.prototype.isPrototypeOf(n)?this._bodyBlob=n:r.formData&&FormData.prototype.isPrototypeOf(n)?this._bodyFormData=n:r.searchParams&&URLSearchParams.prototype.isPrototypeOf(n)?this._bodyText=n.toString():r.arrayBuffer&&r.blob&&l(n)?(this._bodyArrayBuffer=W(n.buffer),this._bodyInit=new Blob([this._bodyArrayBuffer])):r.arrayBuffer&&(ArrayBuffer.prototype.isPrototypeOf(n)||c(n))?this._bodyArrayBuffer=W(n):this._bodyText=n=Object.prototype.toString.call(n):(this._noBody=!0,this._bodyText=""),this.headers.get("content-type")||(typeof n=="string"?this.headers.set("content-type","text/plain;charset=UTF-8"):this._bodyBlob&&this._bodyBlob.type?this.headers.set("content-type",this._bodyBlob.type):r.searchParams&&URLSearchParams.prototype.isPrototypeOf(n)&&this.headers.set("content-type","application/x-www-form-urlencoded;charset=UTF-8"))},r.blob&&(this.blob=function(){var n=k(this);if(n)return n;if(this._bodyBlob)return Promise.resolve(this._bodyBlob);if(this._bodyArrayBuffer)return Promise.resolve(new Blob([this._bodyArrayBuffer]));if(this._bodyFormData)throw new Error("could not read FormData body as blob");return Promise.resolve(new Blob([this._bodyText]))}),this.arrayBuffer=function(){if(this._bodyArrayBuffer){var n=k(this);return n||(ArrayBuffer.isView(this._bodyArrayBuffer)?Promise.resolve(this._bodyArrayBuffer.buffer.slice(this._bodyArrayBuffer.byteOffset,this._bodyArrayBuffer.byteOffset+this._bodyArrayBuffer.byteLength)):Promise.resolve(this._bodyArrayBuffer))}else{if(r.blob)return this.blob().then(T);throw new Error("could not read as ArrayBuffer")}},this.text=function(){var n=k(this);if(n)return n;if(this._bodyBlob)return V(this._bodyBlob);if(this._bodyArrayBuffer)return Promise.resolve(Y(this._bodyArrayBuffer));if(this._bodyFormData)throw new Error("could not read FormData body as text");return Promise.resolve(this._bodyText)},r.formData&&(this.formData=function(){return this.text().then(ee)}),this.json=function(){return this.text().then(JSON.parse)},this}var Z=["CONNECT","DELETE","GET","HEAD","OPTIONS","PATCH","POST","PUT","TRACE"];function j(n){var a=n.toUpperCase();return Z.indexOf(a)>-1?a:n}function w(n,a){if(!(this instanceof w))throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.');a=a||{};var u=a.body;if(n instanceof w){if(n.bodyUsed)throw new TypeError("Already read");this.url=n.url,this.credentials=n.credentials,a.headers||(this.headers=new g(n.headers)),this.method=n.method,this.mode=n.mode,this.signal=n.signal,!u&&n._bodyInit!=null&&(u=n._bodyInit,n.bodyUsed=!0)}else this.url=String(n);if(this.credentials=a.credentials||this.credentials||"same-origin",(a.headers||!this.headers)&&(this.headers=new g(a.headers)),this.method=j(a.method||this.method||"GET"),this.mode=a.mode||this.mode||null,this.signal=a.signal||this.signal||(function(){if("AbortController"in i){var f=new AbortController;return f.signal}})(),this.referrer=null,(this.method==="GET"||this.method==="HEAD")&&u)throw new TypeError("Body not allowed for GET or HEAD requests");if(this._initBody(u),(this.method==="GET"||this.method==="HEAD")&&(a.cache==="no-store"||a.cache==="no-cache")){var h=/([?&])_=[^&]*/;if(h.test(this.url))this.url=this.url.replace(h,"$1_="+new Date().getTime());else{var y=/\?/;this.url+=(y.test(this.url)?"&":"?")+"_="+new Date().getTime()}}}w.prototype.clone=function(){return new w(this,{body:this._bodyInit})};function ee(n){var a=new FormData;return n.trim().split("&").forEach(function(u){if(u){var h=u.split("="),y=h.shift().replace(/\+/g," "),f=h.join("=").replace(/\+/g," ");a.append(decodeURIComponent(y),decodeURIComponent(f))}}),a}function te(n){var a=new g,u=n.replace(/\r?\n[\t ]+/g," ");return u.split("\r").map(function(h){return h.indexOf(`
|
|
2
|
+
`)===0?h.substr(1,h.length):h}).forEach(function(h){var y=h.split(":"),f=y.shift().trim();if(f){var D=y.join(":").trim();try{a.append(f,D)}catch(A){console.warn("Response "+A.message)}}}),a}J.call(w.prototype);function v(n,a){if(!(this instanceof v))throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.');if(a||(a={}),this.type="default",this.status=a.status===void 0?200:a.status,this.status<200||this.status>599)throw new RangeError("Failed to construct 'Response': The status provided (0) is outside the range [200, 599].");this.ok=this.status>=200&&this.status<300,this.statusText=a.statusText===void 0?"":""+a.statusText,this.headers=new g(a.headers),this.url=a.url||"",this._initBody(n)}J.call(v.prototype),v.prototype.clone=function(){return new v(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new g(this.headers),url:this.url})},v.error=function(){var n=new v(null,{status:200,statusText:""});return n.ok=!1,n.status=0,n.type="error",n};var ne=[301,302,303,307,308];v.redirect=function(n,a){if(ne.indexOf(a)===-1)throw new RangeError("Invalid status code");return new v(null,{status:a,headers:{location:n}})},t.DOMException=i.DOMException;try{new t.DOMException}catch{t.DOMException=function(a,u){this.message=a,this.name=u;var h=Error(a);this.stack=h.stack},t.DOMException.prototype=Object.create(Error.prototype),t.DOMException.prototype.constructor=t.DOMException}function I(n,a){return new Promise(function(u,h){var y=new w(n,a);if(y.signal&&y.signal.aborted)return h(new t.DOMException("Aborted","AbortError"));var f=new XMLHttpRequest;function D(){f.abort()}f.onload=function(){var m={statusText:f.statusText,headers:te(f.getAllResponseHeaders()||"")};y.url.indexOf("file://")===0&&(f.status<200||f.status>599)?m.status=200:m.status=f.status,m.url="responseURL"in f?f.responseURL:m.headers.get("X-Request-URL");var F="response"in f?f.response:f.responseText;setTimeout(function(){u(new v(F,m))},0)},f.onerror=function(){setTimeout(function(){h(new TypeError("Network request failed"))},0)},f.ontimeout=function(){setTimeout(function(){h(new TypeError("Network request timed out"))},0)},f.onabort=function(){setTimeout(function(){h(new t.DOMException("Aborted","AbortError"))},0)};function A(m){try{return m===""&&i.location.href?i.location.href:m}catch{return m}}if(f.open(y.method,A(y.url),!0),y.credentials==="include"?f.withCredentials=!0:y.credentials==="omit"&&(f.withCredentials=!1),"responseType"in f&&(r.blob?f.responseType="blob":r.arrayBuffer&&(f.responseType="arraybuffer")),a&&typeof a.headers=="object"&&!(a.headers instanceof g||i.Headers&&a.headers instanceof i.Headers)){var q=[];Object.getOwnPropertyNames(a.headers).forEach(function(m){q.push(d(m)),f.setRequestHeader(m,p(a.headers[m]))}),y.headers.forEach(function(m,F){q.indexOf(F)===-1&&f.setRequestHeader(F,m)})}else y.headers.forEach(function(m,F){f.setRequestHeader(F,m)});y.signal&&(y.signal.addEventListener("abort",D),f.onreadystatechange=function(){f.readyState===4&&y.signal.removeEventListener("abort",D)}),f.send(typeof y._bodyInit>"u"?null:y._bodyInit)})}return I.polyfill=!0,i.fetch||(i.fetch=I,i.Headers=g,i.Request=w,i.Response=v),t.Headers=g,t.Request=w,t.Response=v,t.fetch=I,t})({})})(typeof self<"u"?self:z)});var b=[];for(let s=0;s<256;++s)b.push((s+256).toString(16).slice(1));function H(s,e=0){return(b[s[e+0]]+b[s[e+1]]+b[s[e+2]]+b[s[e+3]]+"-"+b[s[e+4]]+b[s[e+5]]+"-"+b[s[e+6]]+b[s[e+7]]+"-"+b[s[e+8]]+b[s[e+9]]+"-"+b[s[e+10]]+b[s[e+11]]+b[s[e+12]]+b[s[e+13]]+b[s[e+14]]+b[s[e+15]]).toLowerCase()}var P,de=new Uint8Array(16);function B(){if(!P){if(typeof crypto>"u"||!crypto.getRandomValues)throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");P=crypto.getRandomValues.bind(crypto)}return P(de)}var fe=typeof crypto<"u"&&crypto.randomUUID&&crypto.randomUUID.bind(crypto),L={randomUUID:fe};function he(s,e,t){s=s||{};let i=s.random??s.rng?.()??B();if(i.length<16)throw new Error("Random bytes length must be >= 16");if(i[6]=i[6]&15|64,i[8]=i[8]&63|128,e){if(t=t||0,t<0||t+16>e.length)throw new RangeError(`UUID byte range ${t}:${t+15} is out of buffer bounds`);for(let r=0;r<16;++r)e[t+r]=i[r];return e}return H(i)}function ge(s,e,t){return L.randomUUID&&!e&&!s?L.randomUUID():he(s,e,t)}var C=ge;var st=ue(K());function S(s){return pe(s,!1)}function pe(s,e){return s==null?s:{companyId:s.company_id==null?void 0:s.company_id,error:s.error==null?void 0:s.error,featureAllocation:s.feature_allocation==null?void 0:s.feature_allocation,featureUsage:s.feature_usage==null?void 0:s.feature_usage,featureUsageEvent:s.feature_usage_event==null?void 0:s.feature_usage_event,featureUsagePeriod:s.feature_usage_period==null?void 0:s.feature_usage_period,featureUsageResetAt:s.feature_usage_reset_at==null?void 0:new Date(s.feature_usage_reset_at),flag:s.flag,flagId:s.flag_id==null?void 0:s.flag_id,reason:s.reason,ruleId:s.rule_id==null?void 0:s.rule_id,ruleType:s.rule_type==null?void 0:s.rule_type,userId:s.user_id==null?void 0:s.user_id,value:s.value}}function N(s){return ye(s,!1)}function ye(s,e=!1){return s==null?s:{company_id:s.companyId,error:s.error,flag_id:s.flagId,flag_key:s.flagKey,reason:s.reason,req_company:s.reqCompany,req_user:s.reqUser,rule_id:s.ruleId,user_id:s.userId,value:s.value}}function _(s){return be(s,!1)}function be(s,e){return s==null?s:{data:S(s.data),params:s.params}}function G(s){return ve(s,!1)}function ve(s,e){return s==null?s:{flags:s.flags.map(S)}}function M(s){return ke(s,!1)}function ke(s,e){return s==null?s:{data:G(s.data),params:s.params}}var U=s=>{let{companyId:e,error:t,featureAllocation:i,featureUsage:r,featureUsageEvent:l,featureUsagePeriod:o,featureUsageResetAt:c,flag:d,flagId:p,reason:E,ruleId:g,ruleType:k,userId:x,value:T}=S(s);return{featureUsageExceeded:!T&&(k=="company_override_usage_exceeded"||k=="plan_entitlement_usage_exceeded"),companyId:e??void 0,error:t??void 0,featureAllocation:i??void 0,featureUsage:r??void 0,featureUsageEvent:l===null?void 0:l,featureUsagePeriod:o??void 0,featureUsageResetAt:c??void 0,flag:d,flagId:p??void 0,reason:E,ruleId:g??void 0,ruleType:k??void 0,userId:x??void 0,value:T}};function R(s){let e=Object.keys(s).reduce((t,i)=>{let l=Object.keys(s[i]||{}).sort().reduce((o,c)=>(o[c]=s[i][c],o),{});return t[i]=l,t},{});return JSON.stringify(e)}var $="1.2.10";var X="schematicId";var O=class{additionalHeaders={};apiKey;apiUrl="https://api.schematichq.com";conn=null;context={};debugEnabled=!1;offlineEnabled=!1;eventQueue;contextDependentEventQueue;eventUrl="https://c.schematichq.com";flagCheckListeners={};flagValueListeners={};isPending=!0;isPendingListeners=new Set;storage;useWebSocket=!1;checks={};featureUsageEventMap={};webSocketUrl="wss://api.schematichq.com";webSocketConnectionTimeout=1e4;webSocketReconnect=!0;webSocketMaxReconnectAttempts=7;webSocketInitialRetryDelay=1e3;webSocketMaxRetryDelay=3e4;wsReconnectAttempts=0;wsReconnectTimer=null;wsIntentionalDisconnect=!1;currentWebSocket=null;isConnecting=!1;maxEventQueueSize=100;maxEventRetries=5;eventRetryInitialDelay=1e3;eventRetryMaxDelay=3e4;retryTimer=null;flagValueDefaults={};flagCheckDefaults={};constructor(e,t){if(this.apiKey=e,this.eventQueue=[],this.contextDependentEventQueue=[],this.useWebSocket=t?.useWebSocket??!1,this.debugEnabled=t?.debug??!1,this.offlineEnabled=t?.offline??!1,typeof window<"u"&&typeof window.location<"u"){let i=new URLSearchParams(window.location.search),r=i.get("schematic_debug");r!==null&&(r===""||r==="true"||r==="1")&&(this.debugEnabled=!0);let l=i.get("schematic_offline");l!==null&&(l===""||l==="true"||l==="1")&&(this.offlineEnabled=!0,this.debugEnabled=!0)}this.offlineEnabled&&t?.debug!==!1&&(this.debugEnabled=!0),this.offlineEnabled&&this.setIsPending(!1),this.additionalHeaders={"X-Schematic-Client-Version":`schematic-js@${$}`,...t?.additionalHeaders??{}},t?.storage?this.storage=t.storage:typeof localStorage<"u"&&(this.storage=localStorage),t?.apiUrl!==void 0&&(this.apiUrl=t.apiUrl),t?.eventUrl!==void 0&&(this.eventUrl=t.eventUrl),t?.webSocketUrl!==void 0&&(this.webSocketUrl=t.webSocketUrl),t?.webSocketConnectionTimeout!==void 0&&(this.webSocketConnectionTimeout=t.webSocketConnectionTimeout),t?.webSocketReconnect!==void 0&&(this.webSocketReconnect=t.webSocketReconnect),t?.webSocketMaxReconnectAttempts!==void 0&&(this.webSocketMaxReconnectAttempts=t.webSocketMaxReconnectAttempts),t?.webSocketInitialRetryDelay!==void 0&&(this.webSocketInitialRetryDelay=t.webSocketInitialRetryDelay),t?.webSocketMaxRetryDelay!==void 0&&(this.webSocketMaxRetryDelay=t.webSocketMaxRetryDelay),t?.maxEventQueueSize!==void 0&&(this.maxEventQueueSize=t.maxEventQueueSize),t?.maxEventRetries!==void 0&&(this.maxEventRetries=t.maxEventRetries),t?.eventRetryInitialDelay!==void 0&&(this.eventRetryInitialDelay=t.eventRetryInitialDelay),t?.eventRetryMaxDelay!==void 0&&(this.eventRetryMaxDelay=t.eventRetryMaxDelay),t?.flagValueDefaults!==void 0&&(this.flagValueDefaults=t.flagValueDefaults),t?.flagCheckDefaults!==void 0&&(this.flagCheckDefaults=t.flagCheckDefaults),typeof window<"u"&&window?.addEventListener&&(window.addEventListener("beforeunload",()=>{this.flushEventQueue(),this.flushContextDependentEventQueue()}),this.useWebSocket&&(window.addEventListener("offline",()=>{this.debug("Browser went offline, closing WebSocket connection"),this.handleNetworkOffline()}),window.addEventListener("online",()=>{this.debug("Browser came online, attempting to reconnect WebSocket"),this.handleNetworkOnline()}))),this.offlineEnabled?this.debug("Initialized with offline mode enabled - no network requests will be made"):this.debugEnabled&&this.debug("Initialized with debug mode enabled")}resolveFallbackValue(e,t){return t!==void 0?t:e in this.flagValueDefaults?this.flagValueDefaults[e]:!1}resolveFallbackCheckFlagReturn(e,t,i="Fallback value used",r){if(t!==void 0)return{flag:e,value:t,reason:i,error:r};if(e in this.flagCheckDefaults){let l=this.flagCheckDefaults[e];return{...l,flag:e,reason:r!==void 0?i:l.reason,error:r}}return e in this.flagValueDefaults?{flag:e,value:this.flagValueDefaults[e],reason:i,error:r}:{flag:e,value:!1,reason:i,error:r}}async checkFlag(e){let{fallback:t,key:i}=e,r=e.context||this.context,l=R(r);if(this.debug(`checkFlag: ${i}`,{context:r,fallback:t}),this.isOffline()){let o=this.resolveFallbackCheckFlagReturn(i,t,"Offline mode - using initialization defaults");return this.debug(`checkFlag offline result: ${i}`,{value:o.value,offlineMode:!0}),o.value}if(!this.useWebSocket){let o=`${this.apiUrl}/flags/${i}/check`;return fetch(o,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8","X-Schematic-Api-Key":this.apiKey},body:JSON.stringify(r)}).then(c=>{if(!c.ok)throw new Error("Network response was not ok");return c.json()}).then(c=>{let d=_(c);this.debug(`checkFlag result: ${i}`,d);let p=U(d.data);return typeof p.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(p),this.submitFlagCheckEvent(i,p,r),p.value}).catch(c=>{console.error("There was a problem with the fetch operation:",c);let d=this.resolveFallbackCheckFlagReturn(i,t,"API request failed",c instanceof Error?c.message:String(c));return this.submitFlagCheckEvent(i,d,r),d.value})}try{let o=this.checks[l];if(this.conn!==null&&typeof o<"u"&&typeof o[i]<"u")return this.debug(`checkFlag cached result: ${i}`,o[i]),o[i].value;if(this.isOffline())return this.resolveFallbackValue(i,t);try{await this.setContext(r)}catch(E){return console.error("WebSocket connection failed, falling back to REST:",E),this.fallbackToRest(i,r,t)}let d=(this.checks[l]??{})[i],p=d?.value??this.resolveFallbackValue(i,t);return this.debug(`checkFlag WebSocket result: ${i}`,typeof d<"u"?d:{value:p,fallbackUsed:!0}),typeof d<"u"&&this.submitFlagCheckEvent(i,d,r),p}catch(o){console.error("Unexpected error in checkFlag:",o);let c=this.resolveFallbackCheckFlagReturn(i,t,"Unexpected error in flag check",o instanceof Error?o.message:String(o));return this.submitFlagCheckEvent(i,c,r),c.value}}debug(e,...t){this.debugEnabled&&console.log(`[Schematic] ${e}`,...t)}createPersistentMessageHandler(e){return t=>{let i=JSON.parse(t.data);this.debug("WebSocket persistent message received:",i),R(e)in this.checks||(this.checks[R(e)]={}),(i.flags??[]).forEach(r=>{let l=U(r),o=R(e);this.checks[o]===void 0&&(this.checks[o]={}),this.checks[o][l.flag]=l,this.debug("WebSocket flag update:",{flag:l.flag,value:l.value,flagCheck:l}),typeof l.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(l),((this.flagCheckListeners[r.flag]?.size??0)>0||(this.flagValueListeners[r.flag]?.size??0)>0)&&this.submitFlagCheckEvent(l.flag,l,e),this.debug(`About to notify listeners for flag ${r.flag}`,{flag:r.flag,value:l.value}),this.notifyFlagCheckListeners(r.flag,l),this.notifyFlagValueListeners(r.flag,l.value),this.debug(`Finished notifying listeners for flag ${r.flag}`,{flag:r.flag,value:l.value})}),this.flushContextDependentEventQueue(),this.setIsPending(!1)}}isOffline(){return this.offlineEnabled}submitFlagCheckEvent(e,t,i){let r={flagKey:e,value:t.value,reason:t.reason,flagId:t.flagId,ruleId:t.ruleId,companyId:t.companyId,userId:t.userId,error:t.error,reqCompany:i.company,reqUser:i.user};return this.debug("submitting flag check event:",r),this.handleEvent("flag_check",N(r))}async fallbackToRest(e,t,i){if(this.isOffline()){let r=this.resolveFallbackValue(e,i);return this.debug(`fallbackToRest offline result: ${e}`,{value:r,offlineMode:!0}),r}try{let r=`${this.apiUrl}/flags/${e}/check`,l=await fetch(r,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8","X-Schematic-Api-Key":this.apiKey},body:JSON.stringify(t)});if(!l.ok)throw new Error("Network response was not ok");let o=await l.json(),c=_(o);this.debug(`fallbackToRest result: ${e}`,c);let d=U(c.data);return typeof d.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(d),this.submitFlagCheckEvent(e,d,t),d.value}catch(r){console.error("REST API call failed, using fallback value:",r);let l=this.resolveFallbackCheckFlagReturn(e,i,"API request failed (fallback)",r instanceof Error?r.message:String(r));return this.submitFlagCheckEvent(e,l,t),l.value}}checkFlags=async e=>{if(e=e||this.context,this.debug("checkFlags",{context:e}),this.isOffline())return this.debug("checkFlags offline result: returning empty object"),{};let t=`${this.apiUrl}/flags/check`,i=JSON.stringify(e);return fetch(t,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8","X-Schematic-Api-Key":this.apiKey},body:i}).then(r=>{if(!r.ok)throw new Error("Network response was not ok");return r.json()}).then(r=>{let l=M(r);return this.debug("checkFlags result:",l),(l?.data?.flags??[]).reduce((o,c)=>(o[c.flag]=c.value,o),{})}).catch(r=>(console.error("There was a problem with the fetch operation:",r),{}))};identify=e=>{this.debug("identify:",e);try{this.setContext({company:e.company?.keys,user:e.keys})}catch(t){console.error("Error setting context:",t)}return this.handleEvent("identify",e)};setContext=async e=>{if(this.isOffline()||!this.useWebSocket)return this.context=e,this.flushContextDependentEventQueue(),this.setIsPending(!1),Promise.resolve();try{if(this.setIsPending(!0),!this.conn){if(this.isConnecting){for(this.debug("Connection already in progress, waiting for it to complete");this.isConnecting&&this.conn===null;)await new Promise(i=>setTimeout(i,10));if(this.conn!==null){let i=await this.conn;await this.wsSendMessage(i,e);return}}this.wsReconnectTimer!==null&&(this.debug("Cancelling scheduled reconnection, connecting immediately"),clearTimeout(this.wsReconnectTimer),this.wsReconnectTimer=null),this.isConnecting=!0;try{this.conn=this.wsConnect();let i=await this.conn;this.isConnecting=!1,await this.wsSendMessage(i,e);return}catch(i){throw this.isConnecting=!1,i}}let t=await this.conn;await this.wsSendMessage(t,e)}catch(t){throw console.error("Failed to establish WebSocket connection:",t),t}};track=e=>{let{company:t,user:i,event:r,traits:l,quantity:o=1}=e;if(!this.hasContext(t,i)){this.debug(`track: queuing event "${r}" until context is available`);let d={api_key:this.apiKey,body:{company:t,event:r,traits:l??{},user:i,quantity:o},sent_at:new Date().toISOString(),tracker_event_id:C(),tracker_user_id:this.getAnonymousId(),type:"track"};return this.contextDependentEventQueue.push(d),Promise.resolve()}let c={company:t??this.context.company,event:r,traits:l??{},user:i??this.context.user,quantity:o};return this.debug("track:",c),r in this.featureUsageEventMap&&this.optimisticallyUpdateFeatureUsage(r,o),this.handleEvent("track",c)};optimisticallyUpdateFeatureUsage=(e,t=1)=>{let i=this.featureUsageEventMap[e];i!=null&&(this.debug(`Optimistically updating feature usage for event: ${e}`,{quantity:t}),Object.entries(i).forEach(([r,l])=>{if(l===void 0)return;let o={...l};if(typeof o.featureUsage=="number"){if(o.featureUsage+=t,typeof o.featureAllocation=="number"){let d=o.featureUsageExceeded===!0,p=o.featureUsage>=o.featureAllocation;p!==d&&(o.featureUsageExceeded=p,p&&(o.value=!1),this.debug(`Usage limit status changed for flag: ${r}`,{was:d?"exceeded":"within limits",now:p?"exceeded":"within limits",featureUsage:o.featureUsage,featureAllocation:o.featureAllocation,value:o.value}))}this.featureUsageEventMap[e]!==void 0&&(this.featureUsageEventMap[e][r]=o);let c=R(this.context);this.checks[c]!==void 0&&this.checks[c]!==null&&(this.checks[c][r]=o),this.notifyFlagCheckListeners(r,o),this.notifyFlagValueListeners(r,o.value)}}))};hasContext=(e,t)=>{let i=e!=null&&Object.keys(e).length>0||t!=null&&Object.keys(t).length>0,r=this.context.company!==void 0&&this.context.company!==null&&Object.keys(this.context.company).length>0||this.context.user!==void 0&&this.context.user!==null&&Object.keys(this.context.user).length>0;return i||r};flushContextDependentEventQueue=()=>{for(this.debug(`flushing ${this.contextDependentEventQueue.length} context-dependent events`);this.contextDependentEventQueue.length>0;){let e=this.contextDependentEventQueue.shift();if(e)if(e.type==="track"&&typeof e.body=="object"&&e.body!==null){let t=e.body,i={...t,company:t.company??this.context.company,user:t.user??this.context.user},r={...e,body:i,sent_at:new Date().toISOString()};this.sendEvent(r)}else this.sendEvent(e)}};startRetryTimer=()=>{this.retryTimer===null&&(this.retryTimer=setInterval(()=>{this.flushEventQueue().catch(e=>{this.debug("Error in retry timer flush:",e)}),this.eventQueue.length===0&&this.stopRetryTimer()},5e3),this.debug("Started retry timer"))};stopRetryTimer=()=>{this.retryTimer!==null&&(clearInterval(this.retryTimer),this.retryTimer=null,this.debug("Stopped retry timer"))};flushEventQueue=async()=>{if(this.eventQueue.length===0)return;let e=Date.now(),t=[],i=[];for(let r of this.eventQueue)r.next_retry_at===void 0||r.next_retry_at<=e?t.push(r):i.push(r);if(t.length===0){this.debug(`No events ready for retry yet (${i.length} still in backoff)`);return}this.debug(`Flushing event queue: ${t.length} ready, ${i.length} waiting`),this.eventQueue=i;for(let r of t)try{await this.sendEvent(r),this.debug("Queued event sent successfully:",r.type)}catch(l){this.debug("Failed to send queued event:",l)}};getAnonymousId=()=>{if(!this.storage)return C();let e=this.storage.getItem(X);if(typeof e<"u")return e;let t=C();return this.storage.setItem(X,t),t};handleEvent=(e,t)=>{let i={api_key:this.apiKey,body:t,sent_at:new Date().toISOString(),tracker_event_id:C(),tracker_user_id:this.getAnonymousId(),type:e};return typeof document<"u"&&document?.hidden?this.storeEvent(i):this.sendEvent(i)};sendEvent=async e=>{let t=`${this.eventUrl}/e`,i=JSON.stringify(e);if(this.debug("sending event:",{url:t,event:e}),this.isOffline())return this.debug("event not sent (offline mode):",{event:e}),Promise.resolve();try{let r=await fetch(t,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8"},body:i});if(!r.ok)throw new Error(`HTTP ${r.status}: ${r.statusText}`);this.debug("event sent:",{status:r.status,statusText:r.statusText})}catch(r){let l=(e.retry_count??0)+1;if(l<=this.maxEventRetries){this.debug(`Event failed to send (attempt ${l}/${this.maxEventRetries}), queueing for retry:`,r);let o=this.eventRetryInitialDelay*Math.pow(2,l-1),c=Math.min(o,this.eventRetryMaxDelay),d=Date.now()+c,p={...e,retry_count:l,next_retry_at:d};this.eventQueue.length<this.maxEventQueueSize?(this.eventQueue.push(p),this.debug(`Event queued for retry in ${c}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`)):(this.debug(`Event queue full (${this.maxEventQueueSize}), dropping oldest event`),this.eventQueue.shift(),this.eventQueue.push(p)),this.startRetryTimer()}else this.debug(`Event failed permanently after ${this.maxEventRetries} attempts, dropping:`,r)}return Promise.resolve()};storeEvent=e=>(this.eventQueue.push(e),Promise.resolve());cleanup=async()=>{if(this.isOffline())return this.debug("cleanup: skipped (offline mode)"),Promise.resolve();if(this.wsIntentionalDisconnect=!0,this.wsReconnectTimer!==null&&(clearTimeout(this.wsReconnectTimer),this.wsReconnectTimer=null),this.stopRetryTimer(),this.conn)try{let e=await this.conn;this.currentWebSocket===e&&(this.debug("Cleaning up current websocket tracking"),this.currentWebSocket=null),e.close()}catch(e){console.error("Error during cleanup:",e)}finally{this.conn=null,this.currentWebSocket=null,this.isConnecting=!1}};calculateReconnectDelay=()=>{let e=this.webSocketInitialRetryDelay*Math.pow(2,this.wsReconnectAttempts),t=Math.min(e,this.webSocketMaxRetryDelay),i=Math.random()*t*.5,r=t+i;return this.debug(`Reconnect delay calculated: ${r.toFixed(0)}ms (attempt ${this.wsReconnectAttempts+1}/${this.webSocketMaxReconnectAttempts})`),r};handleNetworkOffline=async()=>{if(this.conn!==null){try{let e=await this.conn;(e.readyState===WebSocket.OPEN||e.readyState===WebSocket.CONNECTING)&&e.close()}catch(e){this.debug("Error closing connection on offline:",e)}this.conn=null}this.wsReconnectTimer!==null&&(clearTimeout(this.wsReconnectTimer),this.wsReconnectTimer=null)};handleNetworkOnline=()=>{this.debug("Network online, attempting reconnection and flushing queued events"),this.wsReconnectAttempts=0,this.wsReconnectTimer!==null&&(clearTimeout(this.wsReconnectTimer),this.wsReconnectTimer=null),this.flushEventQueue().catch(e=>{this.debug("Error flushing event queue on network online:",e)}),this.attemptReconnect()};attemptReconnect=()=>{if(this.wsReconnectAttempts>=this.webSocketMaxReconnectAttempts){this.debug(`Maximum reconnection attempts (${this.webSocketMaxReconnectAttempts}) reached, giving up`);return}if(this.wsReconnectTimer!==null){this.debug("Reconnection attempt already scheduled, ignoring duplicate request");return}let e=this.calculateReconnectDelay();this.debug(`Scheduling reconnection attempt ${this.wsReconnectAttempts+1}/${this.webSocketMaxReconnectAttempts} in ${e.toFixed(0)}ms`),this.wsReconnectTimer=setTimeout(async()=>{this.wsReconnectTimer=null,this.wsReconnectAttempts++,this.debug(`Attempting to reconnect (attempt ${this.wsReconnectAttempts}/${this.webSocketMaxReconnectAttempts})`);try{if(this.conn!==null){this.debug("Cleaning up existing connection before reconnection");try{let t=await this.conn;this.currentWebSocket===t&&(this.debug("Existing websocket is current, will be replaced"),this.currentWebSocket=null),(t.readyState===WebSocket.OPEN||t.readyState===WebSocket.CONNECTING)&&t.close()}catch(t){this.debug("Error cleaning up existing connection:",t)}this.conn=null,this.currentWebSocket=null,this.isConnecting=!1}this.isConnecting=!0;try{this.conn=this.wsConnect();let t=await this.conn;this.isConnecting=!1,this.debug("Reconnection context check:",{hasCompany:this.context.company!==void 0,hasUser:this.context.user!==void 0,context:this.context}),this.context.company!==void 0||this.context.user!==void 0?(this.debug("Reconnected, force re-sending context"),await this.wsSendMessage(t,this.context,!0)):(this.debug("No context to re-send after reconnection - websocket ready for new context"),this.debug("Setting up tracking for reconnected websocket (no context to send)"),this.currentWebSocket=t),this.flushEventQueue().catch(i=>{this.debug("Error flushing event queue after websocket reconnection:",i)}),this.debug("Reconnection successful")}catch(t){throw this.isConnecting=!1,t}}catch(t){this.debug("Reconnection attempt failed:",t)}},e)};wsConnect=()=>this.isOffline()?(this.debug("wsConnect: skipped (offline mode)"),Promise.reject(new Error("WebSocket connection skipped in offline mode"))):new Promise((e,t)=>{let i=`${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;this.debug("connecting to WebSocket:",i);let r=new WebSocket(i),l=Math.random().toString(36).substring(7);this.debug(`Creating WebSocket connection ${l} to ${i}`);let o=null,c=!1;o=setTimeout(()=>{c||(this.debug(`WebSocket connection timeout after ${this.webSocketConnectionTimeout}ms`),r.close(),t(new Error("WebSocket connection timeout")))},this.webSocketConnectionTimeout),r.onopen=()=>{c=!0,o!==null&&clearTimeout(o),this.wsReconnectAttempts=0,this.wsIntentionalDisconnect=!1,this.debug(`WebSocket connection ${l} opened successfully`),e(r)},r.onerror=d=>{c=!0,o!==null&&clearTimeout(o),this.debug(`WebSocket connection ${l} error:`,d),t(d)},r.onclose=()=>{c=!0,o!==null&&clearTimeout(o),this.debug(`WebSocket connection ${l} closed`),this.conn=null,this.currentWebSocket===r&&(this.currentWebSocket=null,this.isConnecting=!1),!this.wsIntentionalDisconnect&&this.webSocketReconnect&&this.attemptReconnect()}});wsSendMessage=(e,t,i=!1)=>this.isOffline()?(this.debug("wsSendMessage: skipped (offline mode)"),this.setIsPending(!1),Promise.resolve()):new Promise((r,l)=>{if(!i&&R(t)==R(this.context))return this.debug("WebSocket context unchanged, skipping update"),r(this.setIsPending(!1));this.debug(i?"WebSocket force sending context (reconnection):":"WebSocket context updated:",t),this.context=t;let o=()=>{let c=!1,d=this.createPersistentMessageHandler(t),p=k=>{d(k),c||(c=!0,r())};e.addEventListener("message",p),this.currentWebSocket=e;let E=this.additionalHeaders["X-Schematic-Client-Version"]??`schematic-js@${$}`,g={apiKey:this.apiKey,clientVersion:E,data:t};this.debug("WebSocket sending message:",g),e.send(JSON.stringify(g))};e.readyState===WebSocket.OPEN?(this.debug("WebSocket already open, sending message"),o()):e.readyState===WebSocket.CONNECTING?(this.debug("WebSocket connecting, waiting for open to send message"),e.addEventListener("open",o)):(this.debug("WebSocket is closed, cannot send message"),l("WebSocket is not open or connecting"))});getIsPending=()=>this.isPending;addIsPendingListener=e=>(this.isPendingListeners.add(e),()=>{this.isPendingListeners.delete(e)});setIsPending=e=>{this.isPending=e,this.isPendingListeners.forEach(t=>Ee(t,e))};getFlagCheck=e=>{let t=R(this.context);return(this.checks[t]??{})[e]};getFlagValue=e=>this.getFlagCheck(e)?.value;addFlagValueListener=(e,t)=>(e in this.flagValueListeners||(this.flagValueListeners[e]=new Set),this.flagValueListeners[e].add(t),()=>{this.flagValueListeners[e].delete(t)});addFlagCheckListener=(e,t)=>(e in this.flagCheckListeners||(this.flagCheckListeners[e]=new Set),this.flagCheckListeners[e].add(t),()=>{this.flagCheckListeners[e].delete(t)});notifyFlagCheckListeners=(e,t)=>{let i=this.flagCheckListeners?.[e]??[];i.size>0&&this.debug(`Notifying ${i.size} flag check listeners for ${e}`,t),typeof t.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(t),i.forEach(r=>Re(r,t))};updateFeatureUsageEventMap=e=>{if(typeof e.featureUsageEvent!="string")return;let t=e.featureUsageEvent;(this.featureUsageEventMap[t]===void 0||this.featureUsageEventMap[t]===null)&&(this.featureUsageEventMap[t]={}),this.featureUsageEventMap[t]!==void 0&&(this.featureUsageEventMap[t][e.flag]=e),this.debug(`Updated featureUsageEventMap for event: ${t}, flag: ${e.flag}`,e)};notifyFlagValueListeners=(e,t)=>{let i=this.flagValueListeners?.[e]??[];i.size>0&&this.debug(`Notifying ${i.size} flag value listeners for ${e}`,{value:t}),i.forEach((r,l)=>{this.debug(`Calling listener ${l} for flag ${e}`,{flagKey:e,value:t}),we(r,t),this.debug(`Listener ${l} for flag ${e} completed`,{flagKey:e,value:t})})}},Ee=(s,e)=>{s.length>0?s(e):s()},Re=(s,e)=>{s.length>0?s(e):s()},we=(s,e)=>{s.length>0?s(e):s()};window.Schematic=O;})();
|
|
3
3
|
/* @preserve */
|
package/dist/schematic.cjs.js
CHANGED
|
@@ -800,7 +800,7 @@ function contextString(context) {
|
|
|
800
800
|
}
|
|
801
801
|
|
|
802
802
|
// src/version.ts
|
|
803
|
-
var version = "1.2.
|
|
803
|
+
var version = "1.2.10";
|
|
804
804
|
|
|
805
805
|
// src/index.ts
|
|
806
806
|
var anonymousIdKey = "schematicId";
|
|
@@ -832,6 +832,8 @@ var Schematic = class {
|
|
|
832
832
|
wsReconnectAttempts = 0;
|
|
833
833
|
wsReconnectTimer = null;
|
|
834
834
|
wsIntentionalDisconnect = false;
|
|
835
|
+
currentWebSocket = null;
|
|
836
|
+
isConnecting = false;
|
|
835
837
|
maxEventQueueSize = 100;
|
|
836
838
|
// Prevent memory issues with very long network outages
|
|
837
839
|
maxEventRetries = 5;
|
|
@@ -1109,6 +1111,49 @@ var Schematic = class {
|
|
|
1109
1111
|
console.log(`[Schematic] ${message}`, ...args);
|
|
1110
1112
|
}
|
|
1111
1113
|
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Create a persistent message handler for websocket flag updates
|
|
1116
|
+
*/
|
|
1117
|
+
createPersistentMessageHandler(context) {
|
|
1118
|
+
return (event) => {
|
|
1119
|
+
const message = JSON.parse(event.data);
|
|
1120
|
+
this.debug(`WebSocket persistent message received:`, message);
|
|
1121
|
+
if (!(contextString(context) in this.checks)) {
|
|
1122
|
+
this.checks[contextString(context)] = {};
|
|
1123
|
+
}
|
|
1124
|
+
(message.flags ?? []).forEach((flag) => {
|
|
1125
|
+
const flagCheck = CheckFlagReturnFromJSON(flag);
|
|
1126
|
+
const contextStr = contextString(context);
|
|
1127
|
+
if (this.checks[contextStr] === void 0) {
|
|
1128
|
+
this.checks[contextStr] = {};
|
|
1129
|
+
}
|
|
1130
|
+
this.checks[contextStr][flagCheck.flag] = flagCheck;
|
|
1131
|
+
this.debug(`WebSocket flag update:`, {
|
|
1132
|
+
flag: flagCheck.flag,
|
|
1133
|
+
value: flagCheck.value,
|
|
1134
|
+
flagCheck
|
|
1135
|
+
});
|
|
1136
|
+
if (typeof flagCheck.featureUsageEvent === "string") {
|
|
1137
|
+
this.updateFeatureUsageEventMap(flagCheck);
|
|
1138
|
+
}
|
|
1139
|
+
if ((this.flagCheckListeners[flag.flag]?.size ?? 0) > 0 || (this.flagValueListeners[flag.flag]?.size ?? 0) > 0) {
|
|
1140
|
+
this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
|
|
1141
|
+
}
|
|
1142
|
+
this.debug(`About to notify listeners for flag ${flag.flag}`, {
|
|
1143
|
+
flag: flag.flag,
|
|
1144
|
+
value: flagCheck.value
|
|
1145
|
+
});
|
|
1146
|
+
this.notifyFlagCheckListeners(flag.flag, flagCheck);
|
|
1147
|
+
this.notifyFlagValueListeners(flag.flag, flagCheck.value);
|
|
1148
|
+
this.debug(`Finished notifying listeners for flag ${flag.flag}`, {
|
|
1149
|
+
flag: flag.flag,
|
|
1150
|
+
value: flagCheck.value
|
|
1151
|
+
});
|
|
1152
|
+
});
|
|
1153
|
+
this.flushContextDependentEventQueue();
|
|
1154
|
+
this.setIsPending(false);
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1112
1157
|
/**
|
|
1113
1158
|
* Helper function to check if client is in offline mode
|
|
1114
1159
|
*/
|
|
@@ -1260,6 +1305,19 @@ var Schematic = class {
|
|
|
1260
1305
|
try {
|
|
1261
1306
|
this.setIsPending(true);
|
|
1262
1307
|
if (!this.conn) {
|
|
1308
|
+
if (this.isConnecting) {
|
|
1309
|
+
this.debug(
|
|
1310
|
+
`Connection already in progress, waiting for it to complete`
|
|
1311
|
+
);
|
|
1312
|
+
while (this.isConnecting && this.conn === null) {
|
|
1313
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1314
|
+
}
|
|
1315
|
+
if (this.conn !== null) {
|
|
1316
|
+
const socket2 = await this.conn;
|
|
1317
|
+
await this.wsSendMessage(socket2, context);
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1263
1321
|
if (this.wsReconnectTimer !== null) {
|
|
1264
1322
|
this.debug(
|
|
1265
1323
|
`Cancelling scheduled reconnection, connecting immediately`
|
|
@@ -1267,7 +1325,17 @@ var Schematic = class {
|
|
|
1267
1325
|
clearTimeout(this.wsReconnectTimer);
|
|
1268
1326
|
this.wsReconnectTimer = null;
|
|
1269
1327
|
}
|
|
1270
|
-
this.
|
|
1328
|
+
this.isConnecting = true;
|
|
1329
|
+
try {
|
|
1330
|
+
this.conn = this.wsConnect();
|
|
1331
|
+
const socket2 = await this.conn;
|
|
1332
|
+
this.isConnecting = false;
|
|
1333
|
+
await this.wsSendMessage(socket2, context);
|
|
1334
|
+
return;
|
|
1335
|
+
} catch (error) {
|
|
1336
|
+
this.isConnecting = false;
|
|
1337
|
+
throw error;
|
|
1338
|
+
}
|
|
1271
1339
|
}
|
|
1272
1340
|
const socket = await this.conn;
|
|
1273
1341
|
await this.wsSendMessage(socket, context);
|
|
@@ -1432,10 +1500,14 @@ var Schematic = class {
|
|
|
1432
1500
|
}
|
|
1433
1501
|
}
|
|
1434
1502
|
if (readyEvents.length === 0) {
|
|
1435
|
-
this.debug(
|
|
1503
|
+
this.debug(
|
|
1504
|
+
`No events ready for retry yet (${notReadyEvents.length} still in backoff)`
|
|
1505
|
+
);
|
|
1436
1506
|
return;
|
|
1437
1507
|
}
|
|
1438
|
-
this.debug(
|
|
1508
|
+
this.debug(
|
|
1509
|
+
`Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`
|
|
1510
|
+
);
|
|
1439
1511
|
this.eventQueue = notReadyEvents;
|
|
1440
1512
|
for (const event of readyEvents) {
|
|
1441
1513
|
try {
|
|
@@ -1500,7 +1572,10 @@ var Schematic = class {
|
|
|
1500
1572
|
} catch (error) {
|
|
1501
1573
|
const retryCount = (event.retry_count ?? 0) + 1;
|
|
1502
1574
|
if (retryCount <= this.maxEventRetries) {
|
|
1503
|
-
this.debug(
|
|
1575
|
+
this.debug(
|
|
1576
|
+
`Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`,
|
|
1577
|
+
error
|
|
1578
|
+
);
|
|
1504
1579
|
const baseDelay = this.eventRetryInitialDelay * Math.pow(2, retryCount - 1);
|
|
1505
1580
|
const jitterDelay = Math.min(baseDelay, this.eventRetryMaxDelay);
|
|
1506
1581
|
const nextRetryAt = Date.now() + jitterDelay;
|
|
@@ -1511,15 +1586,22 @@ var Schematic = class {
|
|
|
1511
1586
|
};
|
|
1512
1587
|
if (this.eventQueue.length < this.maxEventQueueSize) {
|
|
1513
1588
|
this.eventQueue.push(retryEvent);
|
|
1514
|
-
this.debug(
|
|
1589
|
+
this.debug(
|
|
1590
|
+
`Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`
|
|
1591
|
+
);
|
|
1515
1592
|
} else {
|
|
1516
|
-
this.debug(
|
|
1593
|
+
this.debug(
|
|
1594
|
+
`Event queue full (${this.maxEventQueueSize}), dropping oldest event`
|
|
1595
|
+
);
|
|
1517
1596
|
this.eventQueue.shift();
|
|
1518
1597
|
this.eventQueue.push(retryEvent);
|
|
1519
1598
|
}
|
|
1520
1599
|
this.startRetryTimer();
|
|
1521
1600
|
} else {
|
|
1522
|
-
this.debug(
|
|
1601
|
+
this.debug(
|
|
1602
|
+
`Event failed permanently after ${this.maxEventRetries} attempts, dropping:`,
|
|
1603
|
+
error
|
|
1604
|
+
);
|
|
1523
1605
|
}
|
|
1524
1606
|
}
|
|
1525
1607
|
return Promise.resolve();
|
|
@@ -1549,11 +1631,17 @@ var Schematic = class {
|
|
|
1549
1631
|
if (this.conn) {
|
|
1550
1632
|
try {
|
|
1551
1633
|
const socket = await this.conn;
|
|
1634
|
+
if (this.currentWebSocket === socket) {
|
|
1635
|
+
this.debug(`Cleaning up current websocket tracking`);
|
|
1636
|
+
this.currentWebSocket = null;
|
|
1637
|
+
}
|
|
1552
1638
|
socket.close();
|
|
1553
1639
|
} catch (error) {
|
|
1554
1640
|
console.error("Error during cleanup:", error);
|
|
1555
1641
|
} finally {
|
|
1556
1642
|
this.conn = null;
|
|
1643
|
+
this.currentWebSocket = null;
|
|
1644
|
+
this.isConnecting = false;
|
|
1557
1645
|
}
|
|
1558
1646
|
}
|
|
1559
1647
|
};
|
|
@@ -1578,7 +1666,9 @@ var Schematic = class {
|
|
|
1578
1666
|
if (this.conn !== null) {
|
|
1579
1667
|
try {
|
|
1580
1668
|
const socket = await this.conn;
|
|
1581
|
-
socket.
|
|
1669
|
+
if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
|
|
1670
|
+
socket.close();
|
|
1671
|
+
}
|
|
1582
1672
|
} catch (error) {
|
|
1583
1673
|
this.debug("Error closing connection on offline:", error);
|
|
1584
1674
|
}
|
|
@@ -1593,7 +1683,9 @@ var Schematic = class {
|
|
|
1593
1683
|
* Handle browser coming back online
|
|
1594
1684
|
*/
|
|
1595
1685
|
handleNetworkOnline = () => {
|
|
1596
|
-
this.debug(
|
|
1686
|
+
this.debug(
|
|
1687
|
+
"Network online, attempting reconnection and flushing queued events"
|
|
1688
|
+
);
|
|
1597
1689
|
this.wsReconnectAttempts = 0;
|
|
1598
1690
|
if (this.wsReconnectTimer !== null) {
|
|
1599
1691
|
clearTimeout(this.wsReconnectTimer);
|
|
@@ -1616,7 +1708,10 @@ var Schematic = class {
|
|
|
1616
1708
|
return;
|
|
1617
1709
|
}
|
|
1618
1710
|
if (this.wsReconnectTimer !== null) {
|
|
1619
|
-
|
|
1711
|
+
this.debug(
|
|
1712
|
+
`Reconnection attempt already scheduled, ignoring duplicate request`
|
|
1713
|
+
);
|
|
1714
|
+
return;
|
|
1620
1715
|
}
|
|
1621
1716
|
const delay = this.calculateReconnectDelay();
|
|
1622
1717
|
this.debug(
|
|
@@ -1629,23 +1724,57 @@ var Schematic = class {
|
|
|
1629
1724
|
`Attempting to reconnect (attempt ${this.wsReconnectAttempts}/${this.webSocketMaxReconnectAttempts})`
|
|
1630
1725
|
);
|
|
1631
1726
|
try {
|
|
1632
|
-
this.conn
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1727
|
+
if (this.conn !== null) {
|
|
1728
|
+
this.debug(`Cleaning up existing connection before reconnection`);
|
|
1729
|
+
try {
|
|
1730
|
+
const existingSocket = await this.conn;
|
|
1731
|
+
if (this.currentWebSocket === existingSocket) {
|
|
1732
|
+
this.debug(`Existing websocket is current, will be replaced`);
|
|
1733
|
+
this.currentWebSocket = null;
|
|
1734
|
+
}
|
|
1735
|
+
if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
|
|
1736
|
+
existingSocket.close();
|
|
1737
|
+
}
|
|
1738
|
+
} catch (error) {
|
|
1739
|
+
this.debug(`Error cleaning up existing connection:`, error);
|
|
1740
|
+
}
|
|
1741
|
+
this.conn = null;
|
|
1742
|
+
this.currentWebSocket = null;
|
|
1743
|
+
this.isConnecting = false;
|
|
1744
|
+
}
|
|
1745
|
+
this.isConnecting = true;
|
|
1746
|
+
try {
|
|
1747
|
+
this.conn = this.wsConnect();
|
|
1748
|
+
const socket = await this.conn;
|
|
1749
|
+
this.isConnecting = false;
|
|
1750
|
+
this.debug(`Reconnection context check:`, {
|
|
1751
|
+
hasCompany: this.context.company !== void 0,
|
|
1752
|
+
hasUser: this.context.user !== void 0,
|
|
1753
|
+
context: this.context
|
|
1754
|
+
});
|
|
1755
|
+
if (this.context.company !== void 0 || this.context.user !== void 0) {
|
|
1756
|
+
this.debug(`Reconnected, force re-sending context`);
|
|
1757
|
+
await this.wsSendMessage(socket, this.context, true);
|
|
1758
|
+
} else {
|
|
1759
|
+
this.debug(
|
|
1760
|
+
`No context to re-send after reconnection - websocket ready for new context`
|
|
1761
|
+
);
|
|
1762
|
+
this.debug(
|
|
1763
|
+
`Setting up tracking for reconnected websocket (no context to send)`
|
|
1764
|
+
);
|
|
1765
|
+
this.currentWebSocket = socket;
|
|
1766
|
+
}
|
|
1767
|
+
this.flushEventQueue().catch((error) => {
|
|
1768
|
+
this.debug(
|
|
1769
|
+
"Error flushing event queue after websocket reconnection:",
|
|
1770
|
+
error
|
|
1771
|
+
);
|
|
1772
|
+
});
|
|
1773
|
+
this.debug(`Reconnection successful`);
|
|
1774
|
+
} catch (error) {
|
|
1775
|
+
this.isConnecting = false;
|
|
1776
|
+
throw error;
|
|
1644
1777
|
}
|
|
1645
|
-
this.flushEventQueue().catch((error) => {
|
|
1646
|
-
this.debug("Error flushing event queue after websocket reconnection:", error);
|
|
1647
|
-
});
|
|
1648
|
-
this.debug(`Reconnection successful`);
|
|
1649
1778
|
} catch (error) {
|
|
1650
1779
|
this.debug(`Reconnection attempt failed:`, error);
|
|
1651
1780
|
}
|
|
@@ -1663,6 +1792,8 @@ var Schematic = class {
|
|
|
1663
1792
|
const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
|
|
1664
1793
|
this.debug(`connecting to WebSocket:`, wsUrl);
|
|
1665
1794
|
const webSocket = new WebSocket(wsUrl);
|
|
1795
|
+
const connectionId = Math.random().toString(36).substring(7);
|
|
1796
|
+
this.debug(`Creating WebSocket connection ${connectionId} to ${wsUrl}`);
|
|
1666
1797
|
let timeoutId = null;
|
|
1667
1798
|
let isResolved = false;
|
|
1668
1799
|
timeoutId = setTimeout(() => {
|
|
@@ -1681,7 +1812,7 @@ var Schematic = class {
|
|
|
1681
1812
|
}
|
|
1682
1813
|
this.wsReconnectAttempts = 0;
|
|
1683
1814
|
this.wsIntentionalDisconnect = false;
|
|
1684
|
-
this.debug(`WebSocket connection opened`);
|
|
1815
|
+
this.debug(`WebSocket connection ${connectionId} opened successfully`);
|
|
1685
1816
|
resolve(webSocket);
|
|
1686
1817
|
};
|
|
1687
1818
|
webSocket.onerror = (error) => {
|
|
@@ -1689,7 +1820,7 @@ var Schematic = class {
|
|
|
1689
1820
|
if (timeoutId !== null) {
|
|
1690
1821
|
clearTimeout(timeoutId);
|
|
1691
1822
|
}
|
|
1692
|
-
this.debug(`WebSocket connection error:`, error);
|
|
1823
|
+
this.debug(`WebSocket connection ${connectionId} error:`, error);
|
|
1693
1824
|
reject(error);
|
|
1694
1825
|
};
|
|
1695
1826
|
webSocket.onclose = () => {
|
|
@@ -1697,121 +1828,48 @@ var Schematic = class {
|
|
|
1697
1828
|
if (timeoutId !== null) {
|
|
1698
1829
|
clearTimeout(timeoutId);
|
|
1699
1830
|
}
|
|
1700
|
-
this.debug(`WebSocket connection closed`);
|
|
1831
|
+
this.debug(`WebSocket connection ${connectionId} closed`);
|
|
1701
1832
|
this.conn = null;
|
|
1833
|
+
if (this.currentWebSocket === webSocket) {
|
|
1834
|
+
this.currentWebSocket = null;
|
|
1835
|
+
this.isConnecting = false;
|
|
1836
|
+
}
|
|
1702
1837
|
if (!this.wsIntentionalDisconnect && this.webSocketReconnect) {
|
|
1703
1838
|
this.attemptReconnect();
|
|
1704
1839
|
}
|
|
1705
1840
|
};
|
|
1706
1841
|
});
|
|
1707
1842
|
};
|
|
1708
|
-
// Send a message on the websocket after reconnection, forcing the send even if context appears unchanged
|
|
1709
|
-
// because the server has lost all state and needs the initial context
|
|
1710
|
-
wsSendContextAfterReconnection = (socket, context) => {
|
|
1711
|
-
if (this.isOffline()) {
|
|
1712
|
-
this.debug("wsSendContextAfterReconnection: skipped (offline mode)");
|
|
1713
|
-
this.setIsPending(false);
|
|
1714
|
-
return Promise.resolve();
|
|
1715
|
-
}
|
|
1716
|
-
return new Promise((resolve) => {
|
|
1717
|
-
this.debug(`WebSocket force sending context after reconnection:`, context);
|
|
1718
|
-
this.context = context;
|
|
1719
|
-
const sendMessage = () => {
|
|
1720
|
-
let resolved = false;
|
|
1721
|
-
const messageHandler = (event) => {
|
|
1722
|
-
const message = JSON.parse(event.data);
|
|
1723
|
-
this.debug(`WebSocket message received after reconnection:`, message);
|
|
1724
|
-
if (!(contextString(context) in this.checks)) {
|
|
1725
|
-
this.checks[contextString(context)] = {};
|
|
1726
|
-
}
|
|
1727
|
-
(message.flags ?? []).forEach((flag) => {
|
|
1728
|
-
const flagCheck = CheckFlagReturnFromJSON(flag);
|
|
1729
|
-
const contextStr = contextString(context);
|
|
1730
|
-
if (this.checks[contextStr] === void 0) {
|
|
1731
|
-
this.checks[contextStr] = {};
|
|
1732
|
-
}
|
|
1733
|
-
this.checks[contextStr][flagCheck.flag] = flagCheck;
|
|
1734
|
-
});
|
|
1735
|
-
this.useWebSocket = true;
|
|
1736
|
-
socket.removeEventListener("message", messageHandler);
|
|
1737
|
-
if (!resolved) {
|
|
1738
|
-
resolved = true;
|
|
1739
|
-
resolve(this.setIsPending(false));
|
|
1740
|
-
}
|
|
1741
|
-
};
|
|
1742
|
-
socket.addEventListener("message", messageHandler);
|
|
1743
|
-
const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
|
|
1744
|
-
const messagePayload = {
|
|
1745
|
-
apiKey: this.apiKey,
|
|
1746
|
-
clientVersion,
|
|
1747
|
-
data: context
|
|
1748
|
-
};
|
|
1749
|
-
this.debug(`WebSocket sending forced message after reconnection:`, messagePayload);
|
|
1750
|
-
socket.send(JSON.stringify(messagePayload));
|
|
1751
|
-
};
|
|
1752
|
-
if (socket.readyState === WebSocket.OPEN) {
|
|
1753
|
-
this.debug(`WebSocket already open, sending forced message after reconnection`);
|
|
1754
|
-
sendMessage();
|
|
1755
|
-
} else {
|
|
1756
|
-
socket.addEventListener("open", () => {
|
|
1757
|
-
this.debug(`WebSocket opened, sending forced message after reconnection`);
|
|
1758
|
-
sendMessage();
|
|
1759
|
-
});
|
|
1760
|
-
}
|
|
1761
|
-
});
|
|
1762
|
-
};
|
|
1763
1843
|
// Send a message on the websocket indicating interest in a particular evaluation context
|
|
1764
1844
|
// and wait for the initial set of flag values to be returned
|
|
1765
|
-
wsSendMessage = (socket, context) => {
|
|
1845
|
+
wsSendMessage = (socket, context, forceContextSend = false) => {
|
|
1766
1846
|
if (this.isOffline()) {
|
|
1767
1847
|
this.debug("wsSendMessage: skipped (offline mode)");
|
|
1768
1848
|
this.setIsPending(false);
|
|
1769
1849
|
return Promise.resolve();
|
|
1770
1850
|
}
|
|
1771
1851
|
return new Promise((resolve, reject) => {
|
|
1772
|
-
if (contextString(context) == contextString(this.context)) {
|
|
1852
|
+
if (!forceContextSend && contextString(context) == contextString(this.context)) {
|
|
1773
1853
|
this.debug(`WebSocket context unchanged, skipping update`);
|
|
1774
1854
|
return resolve(this.setIsPending(false));
|
|
1775
1855
|
}
|
|
1776
|
-
this.debug(
|
|
1856
|
+
this.debug(
|
|
1857
|
+
forceContextSend ? `WebSocket force sending context (reconnection):` : `WebSocket context updated:`,
|
|
1858
|
+
context
|
|
1859
|
+
);
|
|
1777
1860
|
this.context = context;
|
|
1778
1861
|
const sendMessage = () => {
|
|
1779
1862
|
let resolved = false;
|
|
1863
|
+
const persistentMessageHandler = this.createPersistentMessageHandler(context);
|
|
1780
1864
|
const messageHandler = (event) => {
|
|
1781
|
-
|
|
1782
|
-
this.debug(`WebSocket message received:`, message);
|
|
1783
|
-
if (!(contextString(context) in this.checks)) {
|
|
1784
|
-
this.checks[contextString(context)] = {};
|
|
1785
|
-
}
|
|
1786
|
-
(message.flags ?? []).forEach((flag) => {
|
|
1787
|
-
const flagCheck = CheckFlagReturnFromJSON(flag);
|
|
1788
|
-
const contextStr = contextString(context);
|
|
1789
|
-
if (this.checks[contextStr] === void 0) {
|
|
1790
|
-
this.checks[contextStr] = {};
|
|
1791
|
-
}
|
|
1792
|
-
this.checks[contextStr][flagCheck.flag] = flagCheck;
|
|
1793
|
-
this.debug(`WebSocket flag update:`, {
|
|
1794
|
-
flag: flagCheck.flag,
|
|
1795
|
-
value: flagCheck.value,
|
|
1796
|
-
flagCheck
|
|
1797
|
-
});
|
|
1798
|
-
if (typeof flagCheck.featureUsageEvent === "string") {
|
|
1799
|
-
this.updateFeatureUsageEventMap(flagCheck);
|
|
1800
|
-
}
|
|
1801
|
-
if (this.flagCheckListeners[flag.flag]?.size > 0 || this.flagValueListeners[flag.flag]?.size > 0) {
|
|
1802
|
-
this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
|
|
1803
|
-
}
|
|
1804
|
-
this.notifyFlagCheckListeners(flag.flag, flagCheck);
|
|
1805
|
-
this.notifyFlagValueListeners(flag.flag, flagCheck.value);
|
|
1806
|
-
});
|
|
1807
|
-
this.flushContextDependentEventQueue();
|
|
1808
|
-
this.setIsPending(false);
|
|
1865
|
+
persistentMessageHandler(event);
|
|
1809
1866
|
if (!resolved) {
|
|
1810
1867
|
resolved = true;
|
|
1811
1868
|
resolve();
|
|
1812
1869
|
}
|
|
1813
1870
|
};
|
|
1814
1871
|
socket.addEventListener("message", messageHandler);
|
|
1872
|
+
this.currentWebSocket = socket;
|
|
1815
1873
|
const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
|
|
1816
1874
|
const messagePayload = {
|
|
1817
1875
|
apiKey: this.apiKey,
|
|
@@ -1919,7 +1977,17 @@ var Schematic = class {
|
|
|
1919
1977
|
{ value }
|
|
1920
1978
|
);
|
|
1921
1979
|
}
|
|
1922
|
-
listeners.forEach((listener) =>
|
|
1980
|
+
listeners.forEach((listener, index) => {
|
|
1981
|
+
this.debug(`Calling listener ${index} for flag ${flagKey}`, {
|
|
1982
|
+
flagKey,
|
|
1983
|
+
value
|
|
1984
|
+
});
|
|
1985
|
+
notifyFlagValueListener(listener, value);
|
|
1986
|
+
this.debug(`Listener ${index} for flag ${flagKey} completed`, {
|
|
1987
|
+
flagKey,
|
|
1988
|
+
value
|
|
1989
|
+
});
|
|
1990
|
+
});
|
|
1923
1991
|
};
|
|
1924
1992
|
};
|
|
1925
1993
|
var notifyPendingListener = (listener, value) => {
|
package/dist/schematic.d.ts
CHANGED
|
@@ -378,6 +378,8 @@ export declare class Schematic {
|
|
|
378
378
|
private wsReconnectAttempts;
|
|
379
379
|
private wsReconnectTimer;
|
|
380
380
|
private wsIntentionalDisconnect;
|
|
381
|
+
private currentWebSocket;
|
|
382
|
+
private isConnecting;
|
|
381
383
|
private maxEventQueueSize;
|
|
382
384
|
private maxEventRetries;
|
|
383
385
|
private eventRetryInitialDelay;
|
|
@@ -413,7 +415,11 @@ export declare class Schematic {
|
|
|
413
415
|
* Helper function to log debug messages
|
|
414
416
|
* Only logs if debug mode is enabled
|
|
415
417
|
*/
|
|
416
|
-
|
|
418
|
+
debug(message: string, ...args: unknown[]): void;
|
|
419
|
+
/**
|
|
420
|
+
* Create a persistent message handler for websocket flag updates
|
|
421
|
+
*/
|
|
422
|
+
private createPersistentMessageHandler;
|
|
417
423
|
/**
|
|
418
424
|
* Helper function to check if client is in offline mode
|
|
419
425
|
*/
|
|
@@ -500,7 +506,6 @@ export declare class Schematic {
|
|
|
500
506
|
*/
|
|
501
507
|
private attemptReconnect;
|
|
502
508
|
private wsConnect;
|
|
503
|
-
private wsSendContextAfterReconnection;
|
|
504
509
|
private wsSendMessage;
|
|
505
510
|
/**
|
|
506
511
|
* State management
|
package/dist/schematic.esm.js
CHANGED
|
@@ -781,7 +781,7 @@ function contextString(context) {
|
|
|
781
781
|
}
|
|
782
782
|
|
|
783
783
|
// src/version.ts
|
|
784
|
-
var version = "1.2.
|
|
784
|
+
var version = "1.2.10";
|
|
785
785
|
|
|
786
786
|
// src/index.ts
|
|
787
787
|
var anonymousIdKey = "schematicId";
|
|
@@ -813,6 +813,8 @@ var Schematic = class {
|
|
|
813
813
|
wsReconnectAttempts = 0;
|
|
814
814
|
wsReconnectTimer = null;
|
|
815
815
|
wsIntentionalDisconnect = false;
|
|
816
|
+
currentWebSocket = null;
|
|
817
|
+
isConnecting = false;
|
|
816
818
|
maxEventQueueSize = 100;
|
|
817
819
|
// Prevent memory issues with very long network outages
|
|
818
820
|
maxEventRetries = 5;
|
|
@@ -1090,6 +1092,49 @@ var Schematic = class {
|
|
|
1090
1092
|
console.log(`[Schematic] ${message}`, ...args);
|
|
1091
1093
|
}
|
|
1092
1094
|
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Create a persistent message handler for websocket flag updates
|
|
1097
|
+
*/
|
|
1098
|
+
createPersistentMessageHandler(context) {
|
|
1099
|
+
return (event) => {
|
|
1100
|
+
const message = JSON.parse(event.data);
|
|
1101
|
+
this.debug(`WebSocket persistent message received:`, message);
|
|
1102
|
+
if (!(contextString(context) in this.checks)) {
|
|
1103
|
+
this.checks[contextString(context)] = {};
|
|
1104
|
+
}
|
|
1105
|
+
(message.flags ?? []).forEach((flag) => {
|
|
1106
|
+
const flagCheck = CheckFlagReturnFromJSON(flag);
|
|
1107
|
+
const contextStr = contextString(context);
|
|
1108
|
+
if (this.checks[contextStr] === void 0) {
|
|
1109
|
+
this.checks[contextStr] = {};
|
|
1110
|
+
}
|
|
1111
|
+
this.checks[contextStr][flagCheck.flag] = flagCheck;
|
|
1112
|
+
this.debug(`WebSocket flag update:`, {
|
|
1113
|
+
flag: flagCheck.flag,
|
|
1114
|
+
value: flagCheck.value,
|
|
1115
|
+
flagCheck
|
|
1116
|
+
});
|
|
1117
|
+
if (typeof flagCheck.featureUsageEvent === "string") {
|
|
1118
|
+
this.updateFeatureUsageEventMap(flagCheck);
|
|
1119
|
+
}
|
|
1120
|
+
if ((this.flagCheckListeners[flag.flag]?.size ?? 0) > 0 || (this.flagValueListeners[flag.flag]?.size ?? 0) > 0) {
|
|
1121
|
+
this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
|
|
1122
|
+
}
|
|
1123
|
+
this.debug(`About to notify listeners for flag ${flag.flag}`, {
|
|
1124
|
+
flag: flag.flag,
|
|
1125
|
+
value: flagCheck.value
|
|
1126
|
+
});
|
|
1127
|
+
this.notifyFlagCheckListeners(flag.flag, flagCheck);
|
|
1128
|
+
this.notifyFlagValueListeners(flag.flag, flagCheck.value);
|
|
1129
|
+
this.debug(`Finished notifying listeners for flag ${flag.flag}`, {
|
|
1130
|
+
flag: flag.flag,
|
|
1131
|
+
value: flagCheck.value
|
|
1132
|
+
});
|
|
1133
|
+
});
|
|
1134
|
+
this.flushContextDependentEventQueue();
|
|
1135
|
+
this.setIsPending(false);
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1093
1138
|
/**
|
|
1094
1139
|
* Helper function to check if client is in offline mode
|
|
1095
1140
|
*/
|
|
@@ -1241,6 +1286,19 @@ var Schematic = class {
|
|
|
1241
1286
|
try {
|
|
1242
1287
|
this.setIsPending(true);
|
|
1243
1288
|
if (!this.conn) {
|
|
1289
|
+
if (this.isConnecting) {
|
|
1290
|
+
this.debug(
|
|
1291
|
+
`Connection already in progress, waiting for it to complete`
|
|
1292
|
+
);
|
|
1293
|
+
while (this.isConnecting && this.conn === null) {
|
|
1294
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1295
|
+
}
|
|
1296
|
+
if (this.conn !== null) {
|
|
1297
|
+
const socket2 = await this.conn;
|
|
1298
|
+
await this.wsSendMessage(socket2, context);
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1244
1302
|
if (this.wsReconnectTimer !== null) {
|
|
1245
1303
|
this.debug(
|
|
1246
1304
|
`Cancelling scheduled reconnection, connecting immediately`
|
|
@@ -1248,7 +1306,17 @@ var Schematic = class {
|
|
|
1248
1306
|
clearTimeout(this.wsReconnectTimer);
|
|
1249
1307
|
this.wsReconnectTimer = null;
|
|
1250
1308
|
}
|
|
1251
|
-
this.
|
|
1309
|
+
this.isConnecting = true;
|
|
1310
|
+
try {
|
|
1311
|
+
this.conn = this.wsConnect();
|
|
1312
|
+
const socket2 = await this.conn;
|
|
1313
|
+
this.isConnecting = false;
|
|
1314
|
+
await this.wsSendMessage(socket2, context);
|
|
1315
|
+
return;
|
|
1316
|
+
} catch (error) {
|
|
1317
|
+
this.isConnecting = false;
|
|
1318
|
+
throw error;
|
|
1319
|
+
}
|
|
1252
1320
|
}
|
|
1253
1321
|
const socket = await this.conn;
|
|
1254
1322
|
await this.wsSendMessage(socket, context);
|
|
@@ -1413,10 +1481,14 @@ var Schematic = class {
|
|
|
1413
1481
|
}
|
|
1414
1482
|
}
|
|
1415
1483
|
if (readyEvents.length === 0) {
|
|
1416
|
-
this.debug(
|
|
1484
|
+
this.debug(
|
|
1485
|
+
`No events ready for retry yet (${notReadyEvents.length} still in backoff)`
|
|
1486
|
+
);
|
|
1417
1487
|
return;
|
|
1418
1488
|
}
|
|
1419
|
-
this.debug(
|
|
1489
|
+
this.debug(
|
|
1490
|
+
`Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`
|
|
1491
|
+
);
|
|
1420
1492
|
this.eventQueue = notReadyEvents;
|
|
1421
1493
|
for (const event of readyEvents) {
|
|
1422
1494
|
try {
|
|
@@ -1481,7 +1553,10 @@ var Schematic = class {
|
|
|
1481
1553
|
} catch (error) {
|
|
1482
1554
|
const retryCount = (event.retry_count ?? 0) + 1;
|
|
1483
1555
|
if (retryCount <= this.maxEventRetries) {
|
|
1484
|
-
this.debug(
|
|
1556
|
+
this.debug(
|
|
1557
|
+
`Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`,
|
|
1558
|
+
error
|
|
1559
|
+
);
|
|
1485
1560
|
const baseDelay = this.eventRetryInitialDelay * Math.pow(2, retryCount - 1);
|
|
1486
1561
|
const jitterDelay = Math.min(baseDelay, this.eventRetryMaxDelay);
|
|
1487
1562
|
const nextRetryAt = Date.now() + jitterDelay;
|
|
@@ -1492,15 +1567,22 @@ var Schematic = class {
|
|
|
1492
1567
|
};
|
|
1493
1568
|
if (this.eventQueue.length < this.maxEventQueueSize) {
|
|
1494
1569
|
this.eventQueue.push(retryEvent);
|
|
1495
|
-
this.debug(
|
|
1570
|
+
this.debug(
|
|
1571
|
+
`Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`
|
|
1572
|
+
);
|
|
1496
1573
|
} else {
|
|
1497
|
-
this.debug(
|
|
1574
|
+
this.debug(
|
|
1575
|
+
`Event queue full (${this.maxEventQueueSize}), dropping oldest event`
|
|
1576
|
+
);
|
|
1498
1577
|
this.eventQueue.shift();
|
|
1499
1578
|
this.eventQueue.push(retryEvent);
|
|
1500
1579
|
}
|
|
1501
1580
|
this.startRetryTimer();
|
|
1502
1581
|
} else {
|
|
1503
|
-
this.debug(
|
|
1582
|
+
this.debug(
|
|
1583
|
+
`Event failed permanently after ${this.maxEventRetries} attempts, dropping:`,
|
|
1584
|
+
error
|
|
1585
|
+
);
|
|
1504
1586
|
}
|
|
1505
1587
|
}
|
|
1506
1588
|
return Promise.resolve();
|
|
@@ -1530,11 +1612,17 @@ var Schematic = class {
|
|
|
1530
1612
|
if (this.conn) {
|
|
1531
1613
|
try {
|
|
1532
1614
|
const socket = await this.conn;
|
|
1615
|
+
if (this.currentWebSocket === socket) {
|
|
1616
|
+
this.debug(`Cleaning up current websocket tracking`);
|
|
1617
|
+
this.currentWebSocket = null;
|
|
1618
|
+
}
|
|
1533
1619
|
socket.close();
|
|
1534
1620
|
} catch (error) {
|
|
1535
1621
|
console.error("Error during cleanup:", error);
|
|
1536
1622
|
} finally {
|
|
1537
1623
|
this.conn = null;
|
|
1624
|
+
this.currentWebSocket = null;
|
|
1625
|
+
this.isConnecting = false;
|
|
1538
1626
|
}
|
|
1539
1627
|
}
|
|
1540
1628
|
};
|
|
@@ -1559,7 +1647,9 @@ var Schematic = class {
|
|
|
1559
1647
|
if (this.conn !== null) {
|
|
1560
1648
|
try {
|
|
1561
1649
|
const socket = await this.conn;
|
|
1562
|
-
socket.
|
|
1650
|
+
if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
|
|
1651
|
+
socket.close();
|
|
1652
|
+
}
|
|
1563
1653
|
} catch (error) {
|
|
1564
1654
|
this.debug("Error closing connection on offline:", error);
|
|
1565
1655
|
}
|
|
@@ -1574,7 +1664,9 @@ var Schematic = class {
|
|
|
1574
1664
|
* Handle browser coming back online
|
|
1575
1665
|
*/
|
|
1576
1666
|
handleNetworkOnline = () => {
|
|
1577
|
-
this.debug(
|
|
1667
|
+
this.debug(
|
|
1668
|
+
"Network online, attempting reconnection and flushing queued events"
|
|
1669
|
+
);
|
|
1578
1670
|
this.wsReconnectAttempts = 0;
|
|
1579
1671
|
if (this.wsReconnectTimer !== null) {
|
|
1580
1672
|
clearTimeout(this.wsReconnectTimer);
|
|
@@ -1597,7 +1689,10 @@ var Schematic = class {
|
|
|
1597
1689
|
return;
|
|
1598
1690
|
}
|
|
1599
1691
|
if (this.wsReconnectTimer !== null) {
|
|
1600
|
-
|
|
1692
|
+
this.debug(
|
|
1693
|
+
`Reconnection attempt already scheduled, ignoring duplicate request`
|
|
1694
|
+
);
|
|
1695
|
+
return;
|
|
1601
1696
|
}
|
|
1602
1697
|
const delay = this.calculateReconnectDelay();
|
|
1603
1698
|
this.debug(
|
|
@@ -1610,23 +1705,57 @@ var Schematic = class {
|
|
|
1610
1705
|
`Attempting to reconnect (attempt ${this.wsReconnectAttempts}/${this.webSocketMaxReconnectAttempts})`
|
|
1611
1706
|
);
|
|
1612
1707
|
try {
|
|
1613
|
-
this.conn
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1708
|
+
if (this.conn !== null) {
|
|
1709
|
+
this.debug(`Cleaning up existing connection before reconnection`);
|
|
1710
|
+
try {
|
|
1711
|
+
const existingSocket = await this.conn;
|
|
1712
|
+
if (this.currentWebSocket === existingSocket) {
|
|
1713
|
+
this.debug(`Existing websocket is current, will be replaced`);
|
|
1714
|
+
this.currentWebSocket = null;
|
|
1715
|
+
}
|
|
1716
|
+
if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
|
|
1717
|
+
existingSocket.close();
|
|
1718
|
+
}
|
|
1719
|
+
} catch (error) {
|
|
1720
|
+
this.debug(`Error cleaning up existing connection:`, error);
|
|
1721
|
+
}
|
|
1722
|
+
this.conn = null;
|
|
1723
|
+
this.currentWebSocket = null;
|
|
1724
|
+
this.isConnecting = false;
|
|
1725
|
+
}
|
|
1726
|
+
this.isConnecting = true;
|
|
1727
|
+
try {
|
|
1728
|
+
this.conn = this.wsConnect();
|
|
1729
|
+
const socket = await this.conn;
|
|
1730
|
+
this.isConnecting = false;
|
|
1731
|
+
this.debug(`Reconnection context check:`, {
|
|
1732
|
+
hasCompany: this.context.company !== void 0,
|
|
1733
|
+
hasUser: this.context.user !== void 0,
|
|
1734
|
+
context: this.context
|
|
1735
|
+
});
|
|
1736
|
+
if (this.context.company !== void 0 || this.context.user !== void 0) {
|
|
1737
|
+
this.debug(`Reconnected, force re-sending context`);
|
|
1738
|
+
await this.wsSendMessage(socket, this.context, true);
|
|
1739
|
+
} else {
|
|
1740
|
+
this.debug(
|
|
1741
|
+
`No context to re-send after reconnection - websocket ready for new context`
|
|
1742
|
+
);
|
|
1743
|
+
this.debug(
|
|
1744
|
+
`Setting up tracking for reconnected websocket (no context to send)`
|
|
1745
|
+
);
|
|
1746
|
+
this.currentWebSocket = socket;
|
|
1747
|
+
}
|
|
1748
|
+
this.flushEventQueue().catch((error) => {
|
|
1749
|
+
this.debug(
|
|
1750
|
+
"Error flushing event queue after websocket reconnection:",
|
|
1751
|
+
error
|
|
1752
|
+
);
|
|
1753
|
+
});
|
|
1754
|
+
this.debug(`Reconnection successful`);
|
|
1755
|
+
} catch (error) {
|
|
1756
|
+
this.isConnecting = false;
|
|
1757
|
+
throw error;
|
|
1625
1758
|
}
|
|
1626
|
-
this.flushEventQueue().catch((error) => {
|
|
1627
|
-
this.debug("Error flushing event queue after websocket reconnection:", error);
|
|
1628
|
-
});
|
|
1629
|
-
this.debug(`Reconnection successful`);
|
|
1630
1759
|
} catch (error) {
|
|
1631
1760
|
this.debug(`Reconnection attempt failed:`, error);
|
|
1632
1761
|
}
|
|
@@ -1644,6 +1773,8 @@ var Schematic = class {
|
|
|
1644
1773
|
const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
|
|
1645
1774
|
this.debug(`connecting to WebSocket:`, wsUrl);
|
|
1646
1775
|
const webSocket = new WebSocket(wsUrl);
|
|
1776
|
+
const connectionId = Math.random().toString(36).substring(7);
|
|
1777
|
+
this.debug(`Creating WebSocket connection ${connectionId} to ${wsUrl}`);
|
|
1647
1778
|
let timeoutId = null;
|
|
1648
1779
|
let isResolved = false;
|
|
1649
1780
|
timeoutId = setTimeout(() => {
|
|
@@ -1662,7 +1793,7 @@ var Schematic = class {
|
|
|
1662
1793
|
}
|
|
1663
1794
|
this.wsReconnectAttempts = 0;
|
|
1664
1795
|
this.wsIntentionalDisconnect = false;
|
|
1665
|
-
this.debug(`WebSocket connection opened`);
|
|
1796
|
+
this.debug(`WebSocket connection ${connectionId} opened successfully`);
|
|
1666
1797
|
resolve(webSocket);
|
|
1667
1798
|
};
|
|
1668
1799
|
webSocket.onerror = (error) => {
|
|
@@ -1670,7 +1801,7 @@ var Schematic = class {
|
|
|
1670
1801
|
if (timeoutId !== null) {
|
|
1671
1802
|
clearTimeout(timeoutId);
|
|
1672
1803
|
}
|
|
1673
|
-
this.debug(`WebSocket connection error:`, error);
|
|
1804
|
+
this.debug(`WebSocket connection ${connectionId} error:`, error);
|
|
1674
1805
|
reject(error);
|
|
1675
1806
|
};
|
|
1676
1807
|
webSocket.onclose = () => {
|
|
@@ -1678,121 +1809,48 @@ var Schematic = class {
|
|
|
1678
1809
|
if (timeoutId !== null) {
|
|
1679
1810
|
clearTimeout(timeoutId);
|
|
1680
1811
|
}
|
|
1681
|
-
this.debug(`WebSocket connection closed`);
|
|
1812
|
+
this.debug(`WebSocket connection ${connectionId} closed`);
|
|
1682
1813
|
this.conn = null;
|
|
1814
|
+
if (this.currentWebSocket === webSocket) {
|
|
1815
|
+
this.currentWebSocket = null;
|
|
1816
|
+
this.isConnecting = false;
|
|
1817
|
+
}
|
|
1683
1818
|
if (!this.wsIntentionalDisconnect && this.webSocketReconnect) {
|
|
1684
1819
|
this.attemptReconnect();
|
|
1685
1820
|
}
|
|
1686
1821
|
};
|
|
1687
1822
|
});
|
|
1688
1823
|
};
|
|
1689
|
-
// Send a message on the websocket after reconnection, forcing the send even if context appears unchanged
|
|
1690
|
-
// because the server has lost all state and needs the initial context
|
|
1691
|
-
wsSendContextAfterReconnection = (socket, context) => {
|
|
1692
|
-
if (this.isOffline()) {
|
|
1693
|
-
this.debug("wsSendContextAfterReconnection: skipped (offline mode)");
|
|
1694
|
-
this.setIsPending(false);
|
|
1695
|
-
return Promise.resolve();
|
|
1696
|
-
}
|
|
1697
|
-
return new Promise((resolve) => {
|
|
1698
|
-
this.debug(`WebSocket force sending context after reconnection:`, context);
|
|
1699
|
-
this.context = context;
|
|
1700
|
-
const sendMessage = () => {
|
|
1701
|
-
let resolved = false;
|
|
1702
|
-
const messageHandler = (event) => {
|
|
1703
|
-
const message = JSON.parse(event.data);
|
|
1704
|
-
this.debug(`WebSocket message received after reconnection:`, message);
|
|
1705
|
-
if (!(contextString(context) in this.checks)) {
|
|
1706
|
-
this.checks[contextString(context)] = {};
|
|
1707
|
-
}
|
|
1708
|
-
(message.flags ?? []).forEach((flag) => {
|
|
1709
|
-
const flagCheck = CheckFlagReturnFromJSON(flag);
|
|
1710
|
-
const contextStr = contextString(context);
|
|
1711
|
-
if (this.checks[contextStr] === void 0) {
|
|
1712
|
-
this.checks[contextStr] = {};
|
|
1713
|
-
}
|
|
1714
|
-
this.checks[contextStr][flagCheck.flag] = flagCheck;
|
|
1715
|
-
});
|
|
1716
|
-
this.useWebSocket = true;
|
|
1717
|
-
socket.removeEventListener("message", messageHandler);
|
|
1718
|
-
if (!resolved) {
|
|
1719
|
-
resolved = true;
|
|
1720
|
-
resolve(this.setIsPending(false));
|
|
1721
|
-
}
|
|
1722
|
-
};
|
|
1723
|
-
socket.addEventListener("message", messageHandler);
|
|
1724
|
-
const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
|
|
1725
|
-
const messagePayload = {
|
|
1726
|
-
apiKey: this.apiKey,
|
|
1727
|
-
clientVersion,
|
|
1728
|
-
data: context
|
|
1729
|
-
};
|
|
1730
|
-
this.debug(`WebSocket sending forced message after reconnection:`, messagePayload);
|
|
1731
|
-
socket.send(JSON.stringify(messagePayload));
|
|
1732
|
-
};
|
|
1733
|
-
if (socket.readyState === WebSocket.OPEN) {
|
|
1734
|
-
this.debug(`WebSocket already open, sending forced message after reconnection`);
|
|
1735
|
-
sendMessage();
|
|
1736
|
-
} else {
|
|
1737
|
-
socket.addEventListener("open", () => {
|
|
1738
|
-
this.debug(`WebSocket opened, sending forced message after reconnection`);
|
|
1739
|
-
sendMessage();
|
|
1740
|
-
});
|
|
1741
|
-
}
|
|
1742
|
-
});
|
|
1743
|
-
};
|
|
1744
1824
|
// Send a message on the websocket indicating interest in a particular evaluation context
|
|
1745
1825
|
// and wait for the initial set of flag values to be returned
|
|
1746
|
-
wsSendMessage = (socket, context) => {
|
|
1826
|
+
wsSendMessage = (socket, context, forceContextSend = false) => {
|
|
1747
1827
|
if (this.isOffline()) {
|
|
1748
1828
|
this.debug("wsSendMessage: skipped (offline mode)");
|
|
1749
1829
|
this.setIsPending(false);
|
|
1750
1830
|
return Promise.resolve();
|
|
1751
1831
|
}
|
|
1752
1832
|
return new Promise((resolve, reject) => {
|
|
1753
|
-
if (contextString(context) == contextString(this.context)) {
|
|
1833
|
+
if (!forceContextSend && contextString(context) == contextString(this.context)) {
|
|
1754
1834
|
this.debug(`WebSocket context unchanged, skipping update`);
|
|
1755
1835
|
return resolve(this.setIsPending(false));
|
|
1756
1836
|
}
|
|
1757
|
-
this.debug(
|
|
1837
|
+
this.debug(
|
|
1838
|
+
forceContextSend ? `WebSocket force sending context (reconnection):` : `WebSocket context updated:`,
|
|
1839
|
+
context
|
|
1840
|
+
);
|
|
1758
1841
|
this.context = context;
|
|
1759
1842
|
const sendMessage = () => {
|
|
1760
1843
|
let resolved = false;
|
|
1844
|
+
const persistentMessageHandler = this.createPersistentMessageHandler(context);
|
|
1761
1845
|
const messageHandler = (event) => {
|
|
1762
|
-
|
|
1763
|
-
this.debug(`WebSocket message received:`, message);
|
|
1764
|
-
if (!(contextString(context) in this.checks)) {
|
|
1765
|
-
this.checks[contextString(context)] = {};
|
|
1766
|
-
}
|
|
1767
|
-
(message.flags ?? []).forEach((flag) => {
|
|
1768
|
-
const flagCheck = CheckFlagReturnFromJSON(flag);
|
|
1769
|
-
const contextStr = contextString(context);
|
|
1770
|
-
if (this.checks[contextStr] === void 0) {
|
|
1771
|
-
this.checks[contextStr] = {};
|
|
1772
|
-
}
|
|
1773
|
-
this.checks[contextStr][flagCheck.flag] = flagCheck;
|
|
1774
|
-
this.debug(`WebSocket flag update:`, {
|
|
1775
|
-
flag: flagCheck.flag,
|
|
1776
|
-
value: flagCheck.value,
|
|
1777
|
-
flagCheck
|
|
1778
|
-
});
|
|
1779
|
-
if (typeof flagCheck.featureUsageEvent === "string") {
|
|
1780
|
-
this.updateFeatureUsageEventMap(flagCheck);
|
|
1781
|
-
}
|
|
1782
|
-
if (this.flagCheckListeners[flag.flag]?.size > 0 || this.flagValueListeners[flag.flag]?.size > 0) {
|
|
1783
|
-
this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
|
|
1784
|
-
}
|
|
1785
|
-
this.notifyFlagCheckListeners(flag.flag, flagCheck);
|
|
1786
|
-
this.notifyFlagValueListeners(flag.flag, flagCheck.value);
|
|
1787
|
-
});
|
|
1788
|
-
this.flushContextDependentEventQueue();
|
|
1789
|
-
this.setIsPending(false);
|
|
1846
|
+
persistentMessageHandler(event);
|
|
1790
1847
|
if (!resolved) {
|
|
1791
1848
|
resolved = true;
|
|
1792
1849
|
resolve();
|
|
1793
1850
|
}
|
|
1794
1851
|
};
|
|
1795
1852
|
socket.addEventListener("message", messageHandler);
|
|
1853
|
+
this.currentWebSocket = socket;
|
|
1796
1854
|
const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
|
|
1797
1855
|
const messagePayload = {
|
|
1798
1856
|
apiKey: this.apiKey,
|
|
@@ -1900,7 +1958,17 @@ var Schematic = class {
|
|
|
1900
1958
|
{ value }
|
|
1901
1959
|
);
|
|
1902
1960
|
}
|
|
1903
|
-
listeners.forEach((listener) =>
|
|
1961
|
+
listeners.forEach((listener, index) => {
|
|
1962
|
+
this.debug(`Calling listener ${index} for flag ${flagKey}`, {
|
|
1963
|
+
flagKey,
|
|
1964
|
+
value
|
|
1965
|
+
});
|
|
1966
|
+
notifyFlagValueListener(listener, value);
|
|
1967
|
+
this.debug(`Listener ${index} for flag ${flagKey} completed`, {
|
|
1968
|
+
flagKey,
|
|
1969
|
+
value
|
|
1970
|
+
});
|
|
1971
|
+
});
|
|
1904
1972
|
};
|
|
1905
1973
|
};
|
|
1906
1974
|
var notifyPendingListener = (listener, value) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schematichq/schematic-js",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.10",
|
|
4
4
|
"main": "dist/schematic.cjs.js",
|
|
5
5
|
"module": "dist/schematic.esm.js",
|
|
6
6
|
"types": "dist/schematic.d.ts",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@eslint/js": "^9.39.1",
|
|
41
41
|
"@microsoft/api-extractor": "^7.55.0",
|
|
42
|
-
"@openapitools/openapi-generator-cli": "^2.25.
|
|
42
|
+
"@openapitools/openapi-generator-cli": "^2.25.2",
|
|
43
43
|
"@vitest/browser": "^4.0.10",
|
|
44
44
|
"esbuild": "^0.27.0",
|
|
45
45
|
"eslint": "^9.39.1",
|