@schematichq/schematic-js 1.2.11 → 1.2.13
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 +147 -64
- package/dist/schematic.d.ts +40 -7
- package/dist/schematic.esm.js +147 -64
- package/package.json +1 -1
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
"use strict";(()=>{var re=Object.create;var Q=Object.defineProperty;var ie=Object.getOwnPropertyDescriptor;var se=Object.getOwnPropertyNames;var ae=Object.getPrototypeOf,oe=Object.prototype.hasOwnProperty;var ce=(
|
|
2
|
-
`)===0?h.substr(1,h.length):h}).forEach(function(h){var y=h.split(":"),d=y.shift().trim();if(d){var D=y.join(":").trim();try{a.append(d,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=s.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 d=new XMLHttpRequest;function D(){d.abort()}d.onload=function(){var m={statusText:d.statusText,headers:te(d.getAllResponseHeaders()||"")};y.url.indexOf("file://")===0&&(d.status<200||d.status>599)?m.status=200:m.status=d.status,m.url="responseURL"in d?d.responseURL:m.headers.get("X-Request-URL");var C="response"in d?d.response:d.responseText;setTimeout(function(){u(new v(C,m))},0)},d.onerror=function(){setTimeout(function(){h(new TypeError("Network request failed"))},0)},d.ontimeout=function(){setTimeout(function(){h(new TypeError("Network request timed out"))},0)},d.onabort=function(){setTimeout(function(){h(new t.DOMException("Aborted","AbortError"))},0)};function A(m){try{return m===""&&s.location.href?s.location.href:m}catch{return m}}if(d.open(y.method,A(y.url),!0),y.credentials==="include"?d.withCredentials=!0:y.credentials==="omit"&&(d.withCredentials=!1),"responseType"in d&&(r.blob?d.responseType="blob":r.arrayBuffer&&(d.responseType="arraybuffer")),a&&typeof a.headers=="object"&&!(a.headers instanceof g||s.Headers&&a.headers instanceof s.Headers)){var q=[];Object.getOwnPropertyNames(a.headers).forEach(function(m){q.push(f(m)),d.setRequestHeader(m,p(a.headers[m]))}),y.headers.forEach(function(m,C){q.indexOf(C)===-1&&d.setRequestHeader(C,m)})}else y.headers.forEach(function(m,C){d.setRequestHeader(C,m)});y.signal&&(y.signal.addEventListener("abort",D),d.onreadystatechange=function(){d.readyState===4&&y.signal.removeEventListener("abort",D)}),d.send(typeof y._bodyInit>"u"?null:y._bodyInit)})}return I.polyfill=!0,s.fetch||(s.fetch=I,s.Headers=g,s.Request=w,s.Response=v),t.Headers=g,t.Request=w,t.Response=v,t.fetch=I,t})({})})(typeof self<"u"?self:z)});var b=[];for(let i=0;i<256;++i)b.push((i+256).toString(16).slice(1));function H(i,e=0){return(b[i[e+0]]+b[i[e+1]]+b[i[e+2]]+b[i[e+3]]+"-"+b[i[e+4]]+b[i[e+5]]+"-"+b[i[e+6]]+b[i[e+7]]+"-"+b[i[e+8]]+b[i[e+9]]+"-"+b[i[e+10]]+b[i[e+11]]+b[i[e+12]]+b[i[e+13]]+b[i[e+14]]+b[i[e+15]]).toLowerCase()}var P,fe=new Uint8Array(16);function N(){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(fe)}var de=typeof crypto<"u"&&crypto.randomUUID&&crypto.randomUUID.bind(crypto),B={randomUUID:de};function he(i,e,t){i=i||{};let s=i.random??i.rng?.()??N();if(s.length<16)throw new Error("Random bytes length must be >= 16");if(s[6]=s[6]&15|64,s[8]=s[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]=s[r];return e}return H(s)}function ge(i,e,t){return B.randomUUID&&!e&&!i?B.randomUUID():he(i,e,t)}var S=ge;var it=ue(G());function F(i){return pe(i,!1)}function pe(i,e){return i==null?i:{companyId:i.company_id==null?void 0:i.company_id,error:i.error==null?void 0:i.error,featureAllocation:i.feature_allocation==null?void 0:i.feature_allocation,featureUsage:i.feature_usage==null?void 0:i.feature_usage,featureUsageEvent:i.feature_usage_event==null?void 0:i.feature_usage_event,featureUsagePeriod:i.feature_usage_period==null?void 0:i.feature_usage_period,featureUsageResetAt:i.feature_usage_reset_at==null?void 0:new Date(i.feature_usage_reset_at),flag:i.flag,flagId:i.flag_id==null?void 0:i.flag_id,reason:i.reason,ruleId:i.rule_id==null?void 0:i.rule_id,ruleType:i.rule_type==null?void 0:i.rule_type,userId:i.user_id==null?void 0:i.user_id,value:i.value}}function L(i){return ye(i,!1)}function ye(i,e=!1){return i==null?i:{company_id:i.companyId,error:i.error,flag_id:i.flagId,flag_key:i.flagKey,reason:i.reason,req_company:i.reqCompany,req_user:i.reqUser,rule_id:i.ruleId,user_id:i.userId,value:i.value}}function _(i){return be(i,!1)}function be(i,e){return i==null?i:{data:F(i.data),params:i.params}}function K(i){return ve(i,!1)}function ve(i,e){return i==null?i:{flags:i.flags.map(F)}}function M(i){return ke(i,!1)}function ke(i,e){return i==null?i:{data:K(i.data),params:i.params}}var U=i=>{let{companyId:e,error:t,featureAllocation:s,featureUsage:r,featureUsageEvent:c,featureUsagePeriod:o,featureUsageResetAt:l,flag:f,flagId:p,reason:E,ruleId:g,ruleType:k,userId:x,value:T}=F(i);return{featureUsageExceeded:!T&&(k=="company_override_usage_exceeded"||k=="plan_entitlement_usage_exceeded"),companyId:e??void 0,error:t??void 0,featureAllocation:s??void 0,featureUsage:r??void 0,featureUsageEvent:c===null?void 0:c,featureUsagePeriod:o??void 0,featureUsageResetAt:l??void 0,flag:f,flagId:p??void 0,reason:E,ruleId:g??void 0,ruleType:k??void 0,userId:x??void 0,value:T}};function R(i){let e=Object.keys(i).reduce((t,s)=>{let c=Object.keys(i[s]||{}).sort().reduce((o,l)=>(o[l]=i[s][l],o),{});return t[s]=c,t},{});return JSON.stringify(e)}var $="1.2.11";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 s=new URLSearchParams(window.location.search),r=s.get("schematic_debug");r!==null&&(r===""||r==="true"||r==="1")&&(this.debugEnabled=!0);let c=s.get("schematic_offline");c!==null&&(c===""||c==="true"||c==="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,s="Fallback value used",r){if(t!==void 0)return{flag:e,value:t,reason:s,error:r};if(e in this.flagCheckDefaults){let c=this.flagCheckDefaults[e];return{...c,flag:e,reason:r!==void 0?s:c.reason,error:r}}return e in this.flagValueDefaults?{flag:e,value:this.flagValueDefaults[e],reason:s,error:r}:{flag:e,value:!1,reason:s,error:r}}async checkFlag(e){let{fallback:t,key:s}=e,r=e.context||this.context,c=R(r);if(this.debug(`checkFlag: ${s}`,{context:r,fallback:t}),this.isOffline()){let o=this.resolveFallbackCheckFlagReturn(s,t,"Offline mode - using initialization defaults");return this.debug(`checkFlag offline result: ${s}`,{value:o.value,offlineMode:!0}),o.value}if(!this.useWebSocket){let o=`${this.apiUrl}/flags/${s}/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(l=>{if(!l.ok)throw new Error("Network response was not ok");return l.json()}).then(l=>{let f=_(l);this.debug(`checkFlag result: ${s}`,f);let p=U(f.data);return typeof p.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(p),this.submitFlagCheckEvent(s,p,r),p.value}).catch(l=>{console.error("There was a problem with the fetch operation:",l);let f=this.resolveFallbackCheckFlagReturn(s,t,"API request failed",l instanceof Error?l.message:String(l));return this.submitFlagCheckEvent(s,f,r),f.value})}try{let o=this.checks[c];if(this.conn!==null&&typeof o<"u"&&typeof o[s]<"u")return this.debug(`checkFlag cached result: ${s}`,o[s]),o[s].value;if(this.isOffline())return this.resolveFallbackValue(s,t);try{await this.setContext(r)}catch(E){return console.error("WebSocket connection failed, falling back to REST:",E),this.fallbackToRest(s,r,t)}let f=(this.checks[c]??{})[s],p=f?.value??this.resolveFallbackValue(s,t);return this.debug(`checkFlag WebSocket result: ${s}`,typeof f<"u"?f:{value:p,fallbackUsed:!0}),typeof f<"u"&&this.submitFlagCheckEvent(s,f,r),p}catch(o){console.error("Unexpected error in checkFlag:",o);let l=this.resolveFallbackCheckFlagReturn(s,t,"Unexpected error in flag check",o instanceof Error?o.message:String(o));return this.submitFlagCheckEvent(s,l,r),l.value}}debug(e,...t){this.debugEnabled&&console.log(`[Schematic] ${e}`,...t)}createPersistentMessageHandler(e){return t=>{let s=JSON.parse(t.data);this.debug("WebSocket persistent message received:",s),R(e)in this.checks||(this.checks[R(e)]={}),(s.flags??[]).forEach(r=>{let c=U(r),o=R(e);this.checks[o]===void 0&&(this.checks[o]={}),this.checks[o][c.flag]=c,this.debug("WebSocket flag update:",{flag:c.flag,value:c.value,flagCheck:c}),typeof c.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(c),((this.flagCheckListeners[r.flag]?.size??0)>0||(this.flagValueListeners[r.flag]?.size??0)>0)&&this.submitFlagCheckEvent(c.flag,c,e),this.debug(`About to notify listeners for flag ${r.flag}`,{flag:r.flag,value:c.value}),this.notifyFlagCheckListeners(r.flag,c),this.notifyFlagValueListeners(r.flag,c.value),this.debug(`Finished notifying listeners for flag ${r.flag}`,{flag:r.flag,value:c.value})}),this.flushContextDependentEventQueue(),this.setIsPending(!1)}}isOffline(){return this.offlineEnabled}submitFlagCheckEvent(e,t,s){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:s.company,reqUser:s.user};return this.debug("submitting flag check event:",r),this.handleEvent("flag_check",L(r))}async fallbackToRest(e,t,s){if(this.isOffline()){let r=this.resolveFallbackValue(e,s);return this.debug(`fallbackToRest offline result: ${e}`,{value:r,offlineMode:!0}),r}try{let r=`${this.apiUrl}/flags/${e}/check`,c=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(!c.ok)throw new Error("Network response was not ok");let o=await c.json(),l=_(o);this.debug(`fallbackToRest result: ${e}`,l);let f=U(l.data);return typeof f.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(f),this.submitFlagCheckEvent(e,f,t),f.value}catch(r){console.error("REST API call failed, using fallback value:",r);let c=this.resolveFallbackCheckFlagReturn(e,s,"API request failed (fallback)",r instanceof Error?r.message:String(r));return this.submitFlagCheckEvent(e,c,t),c.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`,s=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:s}).then(r=>{if(!r.ok)throw new Error("Network response was not ok");return r.json()}).then(r=>{let c=M(r);return this.debug("checkFlags result:",c),(c?.data?.flags??[]).reduce((o,l)=>(o[l.flag]=l.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(s=>setTimeout(s,10));if(this.conn!==null){let s=await this.conn;await this.wsSendMessage(s,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 s=await this.conn;this.isConnecting=!1,await this.wsSendMessage(s,e);return}catch(s){throw this.isConnecting=!1,s}}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:s,event:r,traits:c,quantity:o=1}=e;if(!this.hasContext(t,s)){this.debug(`track: queuing event "${r}" until context is available`);let f={api_key:this.apiKey,body:{company:t,event:r,traits:c??{},user:s,quantity:o},sent_at:new Date().toISOString(),tracker_event_id:S(),tracker_user_id:this.getAnonymousId(),type:"track"};return this.contextDependentEventQueue.push(f),Promise.resolve()}let l={company:t??this.context.company,event:r,traits:c??{},user:s??this.context.user,quantity:o};return this.debug("track:",l),r in this.featureUsageEventMap&&this.optimisticallyUpdateFeatureUsage(r,o),this.handleEvent("track",l)};optimisticallyUpdateFeatureUsage=(e,t=1)=>{let s=this.featureUsageEventMap[e];s!=null&&(this.debug(`Optimistically updating feature usage for event: ${e}`,{quantity:t}),Object.entries(s).forEach(([r,c])=>{if(c===void 0)return;let o={...c};if(typeof o.featureUsage=="number"){if(o.featureUsage+=t,typeof o.featureAllocation=="number"){let f=o.featureUsageExceeded===!0,p=o.featureUsage>=o.featureAllocation;p!==f&&(o.featureUsageExceeded=p,p&&(o.value=!1),this.debug(`Usage limit status changed for flag: ${r}`,{was:f?"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 l=R(this.context);this.checks[l]!==void 0&&this.checks[l]!==null&&(this.checks[l][r]=o),this.notifyFlagCheckListeners(r,o),this.notifyFlagValueListeners(r,o.value)}}))};hasContext=(e,t)=>{let s=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 s||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,s={...t,company:t.company??this.context.company,user:t.user??this.context.user},r={...e,body:s,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=[],s=[];for(let r of this.eventQueue)r.next_retry_at===void 0||r.next_retry_at<=e?t.push(r):s.push(r);if(t.length===0){this.debug(`No events ready for retry yet (${s.length} still in backoff)`);return}this.debug(`Flushing event queue: ${t.length} ready, ${s.length} waiting`),this.eventQueue=s;for(let r of t)try{await this.sendEvent(r),this.debug("Queued event sent successfully:",r.type)}catch(c){this.debug("Failed to send queued event:",c)}};getAnonymousId=()=>{if(!this.storage)return S();let e=this.storage.getItem(X);if(typeof e<"u")return e;let t=S();return this.storage.setItem(X,t),t};handleEvent=(e,t)=>{let s={api_key:this.apiKey,body:t,sent_at:new Date().toISOString(),tracker_event_id:S(),tracker_user_id:this.getAnonymousId(),type:e};return typeof document<"u"&&document?.hidden?this.storeEvent(s):this.sendEvent(s)};sendEvent=async e=>{let t=`${this.eventUrl}/e`,s=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:s});if(!r.ok)throw new Error(`HTTP ${r.status}: ${r.statusText}`);this.debug("event sent:",{status:r.status,statusText:r.statusText})}catch(r){let c=(e.retry_count??0)+1;if(c<=this.maxEventRetries){this.debug(`Event failed to send (attempt ${c}/${this.maxEventRetries}), queueing for retry:`,r);let o=this.eventRetryInitialDelay*Math.pow(2,c-1),l=Math.min(o,this.eventRetryMaxDelay),f=Date.now()+l,p={...e,retry_count:c,next_retry_at:f};this.eventQueue.length<this.maxEventQueueSize?(this.eventQueue.push(p),this.debug(`Event queued for retry in ${l}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}};forceReconnect=async()=>{if(this.isOffline())return this.debug("forceReconnect: skipped (offline mode)"),Promise.resolve();if(this.debug("forceReconnect: forcing immediate reconnection"),this.wsIntentionalDisconnect=!1,this.wsReconnectTimer!==null&&(this.debug("forceReconnect: cancelling pending reconnection timer"),clearTimeout(this.wsReconnectTimer),this.wsReconnectTimer=null),this.wsReconnectAttempts=0,this.conn!==null){this.debug("forceReconnect: closing existing connection");try{let e=await this.conn;this.currentWebSocket===e&&(this.currentWebSocket=null),(e.readyState===WebSocket.OPEN||e.readyState===WebSocket.CONNECTING)&&e.close()}catch(e){this.debug("forceReconnect: error closing existing connection:",e)}this.conn=null,this.isConnecting=!1}if(this.context.company!==void 0||this.context.user!==void 0){this.debug("forceReconnect: reconnecting with existing context");try{this.isConnecting=!0,this.conn=this.wsConnect();let e=await this.conn;this.isConnecting=!1,await this.wsSendMessage(e,this.context,!0),this.debug("forceReconnect: reconnection successful")}catch(e){this.isConnecting=!1,this.debug("forceReconnect: reconnection failed:",e),this.attemptReconnect()}}else this.debug("forceReconnect: no context available, websocket will connect when context is set")};calculateReconnectDelay=()=>{let e=this.webSocketInitialRetryDelay*Math.pow(2,this.wsReconnectAttempts),t=Math.min(e,this.webSocketMaxRetryDelay),s=Math.random()*t*.5,r=t+s;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(s=>{this.debug("Error flushing event queue after websocket reconnection:",s)}),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 s=`${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;this.debug("connecting to WebSocket:",s);let r=new WebSocket(s),c=Math.random().toString(36).substring(7);this.debug(`Creating WebSocket connection ${c} to ${s}`);let o=null,l=!1;o=setTimeout(()=>{l||(this.debug(`WebSocket connection timeout after ${this.webSocketConnectionTimeout}ms`),r.close(),t(new Error("WebSocket connection timeout")))},this.webSocketConnectionTimeout),r.onopen=()=>{l=!0,o!==null&&clearTimeout(o),this.wsReconnectAttempts=0,this.wsIntentionalDisconnect=!1,this.debug(`WebSocket connection ${c} opened successfully`),e(r)},r.onerror=f=>{l=!0,o!==null&&clearTimeout(o),this.debug(`WebSocket connection ${c} error:`,f),t(f)},r.onclose=()=>{l=!0,o!==null&&clearTimeout(o),this.debug(`WebSocket connection ${c} closed`),this.conn=null,this.currentWebSocket===r&&(this.currentWebSocket=null,this.isConnecting=!1),!this.wsIntentionalDisconnect&&this.webSocketReconnect&&this.attemptReconnect()}});wsSendMessage=(e,t,s=!1)=>this.isOffline()?(this.debug("wsSendMessage: skipped (offline mode)"),this.setIsPending(!1),Promise.resolve()):new Promise((r,c)=>{if(!s&&R(t)==R(this.context))return this.debug("WebSocket context unchanged, skipping update"),r(this.setIsPending(!1));this.debug(s?"WebSocket force sending context (reconnection):":"WebSocket context updated:",t),this.context=t;let o=()=>{let l=!1,f=this.createPersistentMessageHandler(t),p=k=>{f(k),l||(l=!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"),c("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 s=this.flagCheckListeners?.[e]??[];s.size>0&&this.debug(`Notifying ${s.size} flag check listeners for ${e}`,t),typeof t.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(t),s.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 s=this.flagValueListeners?.[e]??[];s.size>0&&this.debug(`Notifying ${s.size} flag value listeners for ${e}`,{value:t}),s.forEach((r,c)=>{this.debug(`Calling listener ${c} for flag ${e}`,{flagKey:e,value:t}),we(r,t),this.debug(`Listener ${c} for flag ${e} completed`,{flagKey:e,value:t})})}},Ee=(i,e)=>{i.length>0?i(e):i()},Re=(i,e)=>{i.length>0?i(e):i()},we=(i,e)=>{i.length>0?i(e):i()};window.Schematic=O;})();
|
|
1
|
+
"use strict";(()=>{var re=Object.create;var Q=Object.defineProperty;var ie=Object.getOwnPropertyDescriptor;var se=Object.getOwnPropertyNames;var ae=Object.getPrototypeOf,oe=Object.prototype.hasOwnProperty;var ce=(s,e)=>()=>(e||s((e={exports:{}}).exports,e),e.exports);var le=(s,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of se(e))!oe.call(s,n)&&n!==t&&Q(s,n,{get:()=>e[n],enumerable:!(r=ie(e,n))||r.enumerable});return s};var ue=(s,e,t)=>(t=s!=null?re(ae(s)):{},le(e||!s||!s.__esModule?Q(t,"default",{value:s,enumerable:!0}):t,s));var G=ce(z=>{(function(s){var e=(function(t){var r=typeof globalThis<"u"&&globalThis||typeof s<"u"&&s||typeof global<"u"&&global||{},n={searchParams:"URLSearchParams"in r,iterable:"Symbol"in r&&"iterator"in Symbol,blob:"FileReader"in r&&"Blob"in r&&(function(){try{return new Blob,!0}catch{return!1}})(),formData:"FormData"in r,arrayBuffer:"ArrayBuffer"in r};function c(i){return i&&DataView.prototype.isPrototypeOf(i)}if(n.arrayBuffer)var o=["[object Int8Array]","[object Uint8Array]","[object Uint8ClampedArray]","[object Int16Array]","[object Uint16Array]","[object Int32Array]","[object Uint32Array]","[object Float32Array]","[object Float64Array]"],l=ArrayBuffer.isView||function(i){return i&&o.indexOf(Object.prototype.toString.call(i))>-1};function u(i){if(typeof i!="string"&&(i=String(i)),/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(i)||i==="")throw new TypeError('Invalid character in header field name: "'+i+'"');return i.toLowerCase()}function h(i){return typeof i!="string"&&(i=String(i)),i}function v(i){var a={next:function(){var d=i.shift();return{done:d===void 0,value:d}}};return n.iterable&&(a[Symbol.iterator]=function(){return a}),a}function p(i){this.map={},i instanceof p?i.forEach(function(a,d){this.append(d,a)},this):Array.isArray(i)?i.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):i&&Object.getOwnPropertyNames(i).forEach(function(a){this.append(a,i[a])},this)}p.prototype.append=function(i,a){i=u(i),a=h(a);var d=this.map[i];this.map[i]=d?d+", "+a:a},p.prototype.delete=function(i){delete this.map[u(i)]},p.prototype.get=function(i){return i=u(i),this.has(i)?this.map[i]:null},p.prototype.has=function(i){return this.map.hasOwnProperty(u(i))},p.prototype.set=function(i,a){this.map[u(i)]=h(a)},p.prototype.forEach=function(i,a){for(var d in this.map)this.map.hasOwnProperty(d)&&i.call(a,this.map[d],d,this)},p.prototype.keys=function(){var i=[];return this.forEach(function(a,d){i.push(d)}),v(i)},p.prototype.values=function(){var i=[];return this.forEach(function(a){i.push(a)}),v(i)},p.prototype.entries=function(){var i=[];return this.forEach(function(a,d){i.push([d,a])}),v(i)},n.iterable&&(p.prototype[Symbol.iterator]=p.prototype.entries);function E(i){if(!i._noBody){if(i.bodyUsed)return Promise.reject(new TypeError("Already read"));i.bodyUsed=!0}}function x(i){return new Promise(function(a,d){i.onload=function(){a(i.result)},i.onerror=function(){d(i.error)}})}function T(i){var a=new FileReader,d=x(a);return a.readAsArrayBuffer(i),d}function W(i){var a=new FileReader,d=x(a),g=/charset=([A-Za-z0-9_-]+)/.exec(i.type),y=g?g[1]:"utf-8";return a.readAsText(i,y),d}function Y(i){for(var a=new Uint8Array(i),d=new Array(a.length),g=0;g<a.length;g++)d[g]=String.fromCharCode(a[g]);return d.join("")}function V(i){if(i.slice)return i.slice(0);var a=new Uint8Array(i.byteLength);return a.set(new Uint8Array(i)),a.buffer}function J(){return this.bodyUsed=!1,this._initBody=function(i){this.bodyUsed=this.bodyUsed,this._bodyInit=i,i?typeof i=="string"?this._bodyText=i:n.blob&&Blob.prototype.isPrototypeOf(i)?this._bodyBlob=i:n.formData&&FormData.prototype.isPrototypeOf(i)?this._bodyFormData=i:n.searchParams&&URLSearchParams.prototype.isPrototypeOf(i)?this._bodyText=i.toString():n.arrayBuffer&&n.blob&&c(i)?(this._bodyArrayBuffer=V(i.buffer),this._bodyInit=new Blob([this._bodyArrayBuffer])):n.arrayBuffer&&(ArrayBuffer.prototype.isPrototypeOf(i)||l(i))?this._bodyArrayBuffer=V(i):this._bodyText=i=Object.prototype.toString.call(i):(this._noBody=!0,this._bodyText=""),this.headers.get("content-type")||(typeof i=="string"?this.headers.set("content-type","text/plain;charset=UTF-8"):this._bodyBlob&&this._bodyBlob.type?this.headers.set("content-type",this._bodyBlob.type):n.searchParams&&URLSearchParams.prototype.isPrototypeOf(i)&&this.headers.set("content-type","application/x-www-form-urlencoded;charset=UTF-8"))},n.blob&&(this.blob=function(){var i=E(this);if(i)return i;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 i=E(this);return i||(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(n.blob)return this.blob().then(T);throw new Error("could not read as ArrayBuffer")}},this.text=function(){var i=E(this);if(i)return i;if(this._bodyBlob)return W(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)},n.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(i){var a=i.toUpperCase();return Z.indexOf(a)>-1?a:i}function R(i,a){if(!(this instanceof R))throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.');a=a||{};var d=a.body;if(i instanceof R){if(i.bodyUsed)throw new TypeError("Already read");this.url=i.url,this.credentials=i.credentials,a.headers||(this.headers=new p(i.headers)),this.method=i.method,this.mode=i.mode,this.signal=i.signal,!d&&i._bodyInit!=null&&(d=i._bodyInit,i.bodyUsed=!0)}else this.url=String(i);if(this.credentials=a.credentials||this.credentials||"same-origin",(a.headers||!this.headers)&&(this.headers=new p(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 r){var f=new AbortController;return f.signal}})(),this.referrer=null,(this.method==="GET"||this.method==="HEAD")&&d)throw new TypeError("Body not allowed for GET or HEAD requests");if(this._initBody(d),(this.method==="GET"||this.method==="HEAD")&&(a.cache==="no-store"||a.cache==="no-cache")){var g=/([?&])_=[^&]*/;if(g.test(this.url))this.url=this.url.replace(g,"$1_="+new Date().getTime());else{var y=/\?/;this.url+=(y.test(this.url)?"&":"?")+"_="+new Date().getTime()}}}R.prototype.clone=function(){return new R(this,{body:this._bodyInit})};function ee(i){var a=new FormData;return i.trim().split("&").forEach(function(d){if(d){var g=d.split("="),y=g.shift().replace(/\+/g," "),f=g.join("=").replace(/\+/g," ");a.append(decodeURIComponent(y),decodeURIComponent(f))}}),a}function te(i){var a=new p,d=i.replace(/\r?\n[\t ]+/g," ");return d.split("\r").map(function(g){return g.indexOf(`
|
|
2
|
+
`)===0?g.substr(1,g.length):g}).forEach(function(g){var y=g.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(R.prototype);function k(i,a){if(!(this instanceof k))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 p(a.headers),this.url=a.url||"",this._initBody(i)}J.call(k.prototype),k.prototype.clone=function(){return new k(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new p(this.headers),url:this.url})},k.error=function(){var i=new k(null,{status:200,statusText:""});return i.ok=!1,i.status=0,i.type="error",i};var ne=[301,302,303,307,308];k.redirect=function(i,a){if(ne.indexOf(a)===-1)throw new RangeError("Invalid status code");return new k(null,{status:a,headers:{location:i}})},t.DOMException=r.DOMException;try{new t.DOMException}catch{t.DOMException=function(a,d){this.message=a,this.name=d;var g=Error(a);this.stack=g.stack},t.DOMException.prototype=Object.create(Error.prototype),t.DOMException.prototype.constructor=t.DOMException}function I(i,a){return new Promise(function(d,g){var y=new R(i,a);if(y.signal&&y.signal.aborted)return g(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 S="response"in f?f.response:f.responseText;setTimeout(function(){d(new k(S,m))},0)},f.onerror=function(){setTimeout(function(){g(new TypeError("Network request failed"))},0)},f.ontimeout=function(){setTimeout(function(){g(new TypeError("Network request timed out"))},0)},f.onabort=function(){setTimeout(function(){g(new t.DOMException("Aborted","AbortError"))},0)};function A(m){try{return m===""&&r.location.href?r.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&&(n.blob?f.responseType="blob":n.arrayBuffer&&(f.responseType="arraybuffer")),a&&typeof a.headers=="object"&&!(a.headers instanceof p||r.Headers&&a.headers instanceof r.Headers)){var q=[];Object.getOwnPropertyNames(a.headers).forEach(function(m){q.push(u(m)),f.setRequestHeader(m,h(a.headers[m]))}),y.headers.forEach(function(m,S){q.indexOf(S)===-1&&f.setRequestHeader(S,m)})}else y.headers.forEach(function(m,S){f.setRequestHeader(S,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,r.fetch||(r.fetch=I,r.Headers=p,r.Request=R,r.Response=k),t.Headers=p,t.Request=R,t.Response=k,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 N(){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),B={randomUUID:fe};function he(s,e,t){s=s||{};let r=s.random??s.rng?.()??N();if(r.length<16)throw new Error("Random bytes length must be >= 16");if(r[6]=r[6]&15|64,r[8]=r[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 n=0;n<16;++n)e[t+n]=r[n];return e}return H(r)}function ge(s,e,t){return B.randomUUID&&!e&&!s?B.randomUUID():he(s,e,t)}var C=ge;var it=ue(G());function F(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 L(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:F(s.data),params:s.params}}function K(s){return ve(s,!1)}function ve(s,e){return s==null?s:{flags:s.flags.map(F)}}function M(s){return ke(s,!1)}function ke(s,e){return s==null?s:{data:K(s.data),params:s.params}}var O=s=>{let{companyId:e,error:t,featureAllocation:r,featureUsage:n,featureUsageEvent:c,featureUsagePeriod:o,featureUsageResetAt:l,flag:u,flagId:h,reason:v,ruleId:p,ruleType:E,userId:x,value:T}=F(s);return{featureUsageExceeded:!T&&(E=="company_override_usage_exceeded"||E=="plan_entitlement_usage_exceeded"),companyId:e??void 0,error:t??void 0,featureAllocation:r??void 0,featureUsage:n??void 0,featureUsageEvent:c===null?void 0:c,featureUsagePeriod:o??void 0,featureUsageResetAt:l??void 0,flag:u,flagId:h??void 0,reason:v,ruleId:p??void 0,ruleType:E??void 0,userId:x??void 0,value:T}};function w(s){let e=Object.keys(s).reduce((t,r)=>{let c=Object.keys(s[r]||{}).sort().reduce((o,l)=>(o[l]=s[r][l],o),{});return t[r]=c,t},{});return JSON.stringify(e)}var $="1.2.13";var X="schematicId";var U=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;webSocketMaxConnectionAttempts=3;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 r=new URLSearchParams(window.location.search),n=r.get("schematic_debug");n!==null&&(n===""||n==="true"||n==="1")&&(this.debugEnabled=!0);let c=r.get("schematic_offline");c!==null&&(c===""||c==="true"||c==="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,r="Fallback value used",n){if(t!==void 0)return{flag:e,value:t,reason:r,error:n};if(e in this.flagCheckDefaults){let c=this.flagCheckDefaults[e];return{...c,flag:e,reason:n!==void 0?r:c.reason,error:n}}return e in this.flagValueDefaults?{flag:e,value:this.flagValueDefaults[e],reason:r,error:n}:{flag:e,value:!1,reason:r,error:n}}async checkFlag(e){let{fallback:t,key:r}=e,n=e.context||this.context,c=w(n);if(this.debug(`checkFlag: ${r}`,{context:n,fallback:t}),this.isOffline()){let o=this.resolveFallbackCheckFlagReturn(r,t,"Offline mode - using initialization defaults");return this.debug(`checkFlag offline result: ${r}`,{value:o.value,offlineMode:!0}),o.value}if(!this.useWebSocket){let o=`${this.apiUrl}/flags/${r}/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(n)}).then(l=>{if(!l.ok)throw new Error("Network response was not ok");return l.json()}).then(l=>{let u=_(l);this.debug(`checkFlag result: ${r}`,u);let h=O(u.data);return typeof h.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(h),this.submitFlagCheckEvent(r,h,n),h.value}).catch(l=>{console.warn("There was a problem with the fetch operation:",l);let u=this.resolveFallbackCheckFlagReturn(r,t,"API request failed",l instanceof Error?l.message:String(l));return this.submitFlagCheckEvent(r,u,n),u.value})}try{let o=this.checks[c];if(this.conn!==null&&typeof o<"u"&&typeof o[r]<"u")return this.debug(`checkFlag cached result: ${r}`,o[r]),o[r].value;if(this.isOffline())return this.resolveFallbackValue(r,t);try{await this.setContext(n)}catch(v){return console.warn("WebSocket connection failed, falling back to REST:",v),this.fallbackToRest(r,n,t)}let u=(this.checks[c]??{})[r],h=u?.value??this.resolveFallbackValue(r,t);return this.debug(`checkFlag WebSocket result: ${r}`,typeof u<"u"?u:{value:h,fallbackUsed:!0}),typeof u<"u"&&this.submitFlagCheckEvent(r,u,n),h}catch(o){console.error("Unexpected error in checkFlag:",o);let l=this.resolveFallbackCheckFlagReturn(r,t,"Unexpected error in flag check",o instanceof Error?o.message:String(o));return this.submitFlagCheckEvent(r,l,n),l.value}}debug(e,...t){this.debugEnabled&&console.log(`[Schematic] ${e}`,...t)}createPersistentMessageHandler(e){return t=>{let r=JSON.parse(t.data);this.debug("WebSocket persistent message received:",r),w(e)in this.checks||(this.checks[w(e)]={}),(r.flags??[]).forEach(n=>{let c=O(n),o=w(e);this.checks[o]===void 0&&(this.checks[o]={}),this.checks[o][c.flag]=c,this.debug("WebSocket flag update:",{flag:c.flag,value:c.value,flagCheck:c}),typeof c.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(c),((this.flagCheckListeners[n.flag]?.size??0)>0||(this.flagValueListeners[n.flag]?.size??0)>0)&&this.submitFlagCheckEvent(c.flag,c,e),this.debug(`About to notify listeners for flag ${n.flag}`,{flag:n.flag,value:c.value}),this.notifyFlagCheckListeners(n.flag,c),this.notifyFlagValueListeners(n.flag,c.value),this.debug(`Finished notifying listeners for flag ${n.flag}`,{flag:n.flag,value:c.value})}),this.flushContextDependentEventQueue(),this.setIsPending(!1)}}isOffline(){return this.offlineEnabled}submitFlagCheckEvent(e,t,r){let n={flagKey:e,value:t.value,reason:t.reason,flagId:t.flagId,ruleId:t.ruleId,companyId:t.companyId,userId:t.userId,error:t.error,reqCompany:r.company,reqUser:r.user};return this.debug("submitting flag check event:",n),this.handleEvent("flag_check",L(n))}async fallbackToRest(e,t,r){if(this.isOffline()){let n=this.resolveFallbackValue(e,r);return this.debug(`fallbackToRest offline result: ${e}`,{value:n,offlineMode:!0}),n}try{let n=`${this.apiUrl}/flags/${e}/check`,c=await fetch(n,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8","X-Schematic-Api-Key":this.apiKey},body:JSON.stringify(t)});if(!c.ok)throw new Error("Network response was not ok");let o=await c.json(),l=_(o);this.debug(`fallbackToRest result: ${e}`,l);let u=O(l.data);return typeof u.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(u),this.submitFlagCheckEvent(e,u,t),u.value}catch(n){console.warn("REST API call failed, using fallback value:",n);let c=this.resolveFallbackCheckFlagReturn(e,r,"API request failed (fallback)",n instanceof Error?n.message:String(n));return this.submitFlagCheckEvent(e,c,t),c.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`,r=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:r}).then(n=>{if(!n.ok)throw new Error("Network response was not ok");return n.json()}).then(n=>{let c=M(n);return this.debug("checkFlags result:",c),(c?.data?.flags??[]).reduce((o,l)=>(o[l.flag]=l.value,o),{})}).catch(n=>(console.warn("There was a problem with the fetch operation:",n),{}))};identify=e=>{this.debug("identify:",e);try{this.setContext({company:e.company?.keys,user:e.keys})}catch(t){console.warn("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(r=>setTimeout(r,10));if(this.conn!==null){let r=await this.conn;await this.wsSendMessage(r,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 r=await this.conn;this.isConnecting=!1,await this.wsSendMessage(r,e);return}catch(r){throw this.isConnecting=!1,r}}let t=await this.conn;await this.wsSendMessage(t,e)}catch(t){throw console.warn("Failed to establish WebSocket connection:",t),t}};track=e=>{let{company:t,user:r,event:n,traits:c,quantity:o=1}=e;if(!this.hasContext(t,r)){this.debug(`track: queuing event "${n}" until context is available`);let u={api_key:this.apiKey,body:{company:t,event:n,traits:c??{},user:r,quantity:o},sent_at:new Date().toISOString(),tracker_event_id:C(),tracker_user_id:this.getAnonymousId(),type:"track"};return this.contextDependentEventQueue.push(u),Promise.resolve()}let l={company:t??this.context.company,event:n,traits:c??{},user:r??this.context.user,quantity:o};return this.debug("track:",l),n in this.featureUsageEventMap&&this.optimisticallyUpdateFeatureUsage(n,o),this.handleEvent("track",l)};optimisticallyUpdateFeatureUsage=(e,t=1)=>{let r=this.featureUsageEventMap[e];r!=null&&(this.debug(`Optimistically updating feature usage for event: ${e}`,{quantity:t}),Object.entries(r).forEach(([n,c])=>{if(c===void 0)return;let o={...c};if(typeof o.featureUsage=="number"){if(o.featureUsage+=t,typeof o.featureAllocation=="number"){let u=o.featureUsageExceeded===!0,h=o.featureUsage>=o.featureAllocation;h!==u&&(o.featureUsageExceeded=h,h&&(o.value=!1),this.debug(`Usage limit status changed for flag: ${n}`,{was:u?"exceeded":"within limits",now:h?"exceeded":"within limits",featureUsage:o.featureUsage,featureAllocation:o.featureAllocation,value:o.value}))}this.featureUsageEventMap[e]!==void 0&&(this.featureUsageEventMap[e][n]=o);let l=w(this.context);this.checks[l]!==void 0&&this.checks[l]!==null&&(this.checks[l][n]=o),this.notifyFlagCheckListeners(n,o),this.notifyFlagValueListeners(n,o.value)}}))};hasContext=(e,t)=>{let r=e!=null&&Object.keys(e).length>0||t!=null&&Object.keys(t).length>0,n=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 r||n};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,r={...t,company:t.company??this.context.company,user:t.user??this.context.user},n={...e,body:r,sent_at:new Date().toISOString()};this.sendEvent(n)}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=[],r=[];for(let n of this.eventQueue)n.next_retry_at===void 0||n.next_retry_at<=e?t.push(n):r.push(n);if(t.length===0){this.debug(`No events ready for retry yet (${r.length} still in backoff)`);return}this.debug(`Flushing event queue: ${t.length} ready, ${r.length} waiting`),this.eventQueue=r;for(let n of t)try{await this.sendEvent(n),this.debug("Queued event sent successfully:",n.type)}catch(c){this.debug("Failed to send queued event:",c)}};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 r={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(r):this.sendEvent(r)};sendEvent=async e=>{let t=`${this.eventUrl}/e`,r=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 n=await fetch(t,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8"},body:r});if(!n.ok)throw new Error(`HTTP ${n.status}: ${n.statusText}`);this.debug("event sent:",{status:n.status,statusText:n.statusText})}catch(n){let c=(e.retry_count??0)+1;if(c<=this.maxEventRetries){this.debug(`Event failed to send (attempt ${c}/${this.maxEventRetries}), queueing for retry:`,n);let o=this.eventRetryInitialDelay*Math.pow(2,c-1),l=Math.min(o,this.eventRetryMaxDelay),u=Date.now()+l,h={...e,retry_count:c,next_retry_at:u};this.eventQueue.length<this.maxEventQueueSize?(this.eventQueue.push(h),this.debug(`Event queued for retry in ${l}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`)):(this.debug(`Event queue full (${this.maxEventQueueSize}), dropping oldest event`),this.eventQueue.shift(),this.eventQueue.push(h)),this.startRetryTimer()}else this.debug(`Event failed permanently after ${this.maxEventRetries} attempts, dropping:`,n)}return Promise.resolve()};storeEvent=e=>(this.eventQueue.push(e),Promise.resolve());forceReconnect=async()=>this.reconnect({force:!0});reconnectIfNeeded=async()=>this.reconnect({force:!1});reconnect=async e=>{let{force:t}=e,r=t?"forceReconnect":"reconnectIfNeeded";if(this.isOffline())return this.debug(`${r}: skipped (offline mode)`),Promise.resolve();if(!t&&this.conn!==null)try{if((await this.conn).readyState===WebSocket.OPEN)return this.debug(`${r}: connection is healthy, skipping`),Promise.resolve()}catch{}if(this.debug(`${r}: ${t?"forcing immediate reconnection":"reconnecting"}`),this.wsIntentionalDisconnect=!1,this.wsReconnectTimer!==null&&(this.debug(`${r}: cancelling pending reconnection timer`),clearTimeout(this.wsReconnectTimer),this.wsReconnectTimer=null),this.wsReconnectAttempts=0,this.conn!==null){this.debug(`${r}: closing existing connection`);try{let n=await this.conn;this.currentWebSocket===n&&(this.currentWebSocket=null),(n.readyState===WebSocket.OPEN||n.readyState===WebSocket.CONNECTING)&&n.close()}catch(n){this.debug(`${r}: error closing existing connection:`,n)}this.conn=null,this.isConnecting=!1}if(this.context.company!==void 0||this.context.user!==void 0){this.debug(`${r}: reconnecting with existing context`);try{this.isConnecting=!0,this.conn=this.wsConnect();let n=await this.conn;this.isConnecting=!1,await this.wsSendMessage(n,this.context,!0),this.debug(`${r}: reconnection successful`)}catch(n){this.isConnecting=!1,this.debug(`${r}: reconnection failed:`,n)}}else this.debug(`${r}: no context set, skipping reconnection`);return 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.warn("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),r=Math.random()*t*.5,n=t+r;return this.debug(`Reconnect delay calculated: ${n.toFixed(0)}ms (attempt ${this.wsReconnectAttempts+1}/${this.webSocketMaxReconnectAttempts})`),n};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(r=>{this.debug("Error flushing event queue after websocket reconnection:",r)}),this.debug("Reconnection successful")}catch(t){throw this.isConnecting=!1,t}}catch(t){this.debug("Reconnection attempt failed:",t)}},e)};wsConnect=async()=>{if(this.isOffline())throw this.debug("wsConnect: skipped (offline mode)"),new Error("WebSocket connection skipped in offline mode");let e=null,t=this.webSocketMaxConnectionAttempts;for(let r=0;r<t;r++)try{let n=await this.wsConnectOnce();return this.wsReconnectAttempts=0,n}catch(n){if(e=n instanceof Error?n:new Error(String(n)),!(e.message==="WebSocket connection timeout"))throw this.debug("WebSocket connection failed with non-timeout error, not retrying:",e.message),e;if(r<t-1){let o=this.webSocketInitialRetryDelay*Math.pow(2,r),l=Math.min(o,this.webSocketMaxRetryDelay),u=l*.2*Math.random(),h=l+u;this.debug(`WebSocket connection timeout (attempt ${r+1}/${t}), retrying in ${h.toFixed(0)}ms`),await new Promise(v=>setTimeout(v,h))}else this.debug(`WebSocket connection timeout (attempt ${r+1}/${t}), no more retries`)}throw e??new Error("WebSocket connection failed")};wsConnectOnce=()=>new Promise((e,t)=>{let r=`${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;this.debug("connecting to WebSocket:",r);let n=new WebSocket(r),c=Math.random().toString(36).substring(7);this.debug(`Creating WebSocket connection ${c} to ${r}`);let o=null,l=!1;o=setTimeout(()=>{l||(l=!0,this.debug(`WebSocket connection timeout after ${this.webSocketConnectionTimeout}ms`),n.close(),t(new Error("WebSocket connection timeout")))},this.webSocketConnectionTimeout),n.onopen=()=>{l||(l=!0,o!==null&&clearTimeout(o),this.wsIntentionalDisconnect=!1,this.debug(`WebSocket connection ${c} opened successfully`),e(n))},n.onerror=u=>{l||(l=!0,o!==null&&clearTimeout(o),this.debug(`WebSocket connection ${c} error:`,u),t(u))},n.onclose=()=>{o!==null&&clearTimeout(o),this.debug(`WebSocket connection ${c} closed`),this.conn=null,this.currentWebSocket===n&&(this.currentWebSocket=null,this.isConnecting=!1),!l&&!this.wsIntentionalDisconnect&&this.webSocketReconnect&&this.attemptReconnect()}});wsSendMessage=(e,t,r=!1)=>this.isOffline()?(this.debug("wsSendMessage: skipped (offline mode)"),this.setIsPending(!1),Promise.resolve()):new Promise((n,c)=>{if(!r&&w(t)==w(this.context))return this.debug("WebSocket context unchanged, skipping update"),n(this.setIsPending(!1));this.debug(r?"WebSocket force sending context (reconnection):":"WebSocket context updated:",t),this.context=t;let o=()=>{let l=!1,u=this.createPersistentMessageHandler(t),h=E=>{u(E),l||(l=!0,n())};e.addEventListener("message",h),this.currentWebSocket=e;let v=this.additionalHeaders["X-Schematic-Client-Version"]??`schematic-js@${$}`,p={apiKey:this.apiKey,clientVersion:v,data:t};this.debug("WebSocket sending message:",p),e.send(JSON.stringify(p))};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"),c("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=w(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 r=this.flagCheckListeners?.[e]??[];r.size>0&&this.debug(`Notifying ${r.size} flag check listeners for ${e}`,t),typeof t.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(t),r.forEach(n=>we(n,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 r=this.flagValueListeners?.[e]??[];r.size>0&&this.debug(`Notifying ${r.size} flag value listeners for ${e}`,{value:t}),r.forEach((n,c)=>{this.debug(`Calling listener ${c} for flag ${e}`,{flagKey:e,value:t}),Re(n,t),this.debug(`Listener ${c} for flag ${e} completed`,{flagKey:e,value:t})})}},Ee=(s,e)=>{s.length>0?s(e):s()},we=(s,e)=>{s.length>0?s(e):s()},Re=(s,e)=>{s.length>0?s(e):s()};window.Schematic=U;})();
|
|
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.13";
|
|
804
804
|
|
|
805
805
|
// src/index.ts
|
|
806
806
|
var anonymousIdKey = "schematicId";
|
|
@@ -827,6 +827,9 @@ var Schematic = class {
|
|
|
827
827
|
webSocketConnectionTimeout = 1e4;
|
|
828
828
|
webSocketReconnect = true;
|
|
829
829
|
webSocketMaxReconnectAttempts = 7;
|
|
830
|
+
// Max attempts after connection disrupted
|
|
831
|
+
webSocketMaxConnectionAttempts = 3;
|
|
832
|
+
// Max attempts for initial connection
|
|
830
833
|
webSocketInitialRetryDelay = 1e3;
|
|
831
834
|
webSocketMaxRetryDelay = 3e4;
|
|
832
835
|
wsReconnectAttempts = 0;
|
|
@@ -1050,7 +1053,7 @@ var Schematic = class {
|
|
|
1050
1053
|
this.submitFlagCheckEvent(key, result, context);
|
|
1051
1054
|
return result.value;
|
|
1052
1055
|
}).catch((error) => {
|
|
1053
|
-
console.
|
|
1056
|
+
console.warn("There was a problem with the fetch operation:", error);
|
|
1054
1057
|
const errorResult = this.resolveFallbackCheckFlagReturn(
|
|
1055
1058
|
key,
|
|
1056
1059
|
fallback,
|
|
@@ -1073,10 +1076,7 @@ var Schematic = class {
|
|
|
1073
1076
|
try {
|
|
1074
1077
|
await this.setContext(context);
|
|
1075
1078
|
} catch (error) {
|
|
1076
|
-
console.error
|
|
1077
|
-
"WebSocket connection failed, falling back to REST:",
|
|
1078
|
-
error
|
|
1079
|
-
);
|
|
1079
|
+
console.warn("WebSocket connection failed, falling back to REST:", error);
|
|
1080
1080
|
return this.fallbackToRest(key, context, fallback);
|
|
1081
1081
|
}
|
|
1082
1082
|
const contextVals = this.checks[contextStr] ?? {};
|
|
@@ -1216,7 +1216,7 @@ var Schematic = class {
|
|
|
1216
1216
|
this.submitFlagCheckEvent(key, result, context);
|
|
1217
1217
|
return result.value;
|
|
1218
1218
|
} catch (error) {
|
|
1219
|
-
console.
|
|
1219
|
+
console.warn("REST API call failed, using fallback value:", error);
|
|
1220
1220
|
const errorResult = this.resolveFallbackCheckFlagReturn(
|
|
1221
1221
|
key,
|
|
1222
1222
|
fallback,
|
|
@@ -1265,7 +1265,7 @@ var Schematic = class {
|
|
|
1265
1265
|
{}
|
|
1266
1266
|
);
|
|
1267
1267
|
}).catch((error) => {
|
|
1268
|
-
console.
|
|
1268
|
+
console.warn("There was a problem with the fetch operation:", error);
|
|
1269
1269
|
return {};
|
|
1270
1270
|
});
|
|
1271
1271
|
};
|
|
@@ -1282,7 +1282,7 @@ var Schematic = class {
|
|
|
1282
1282
|
user: body.keys
|
|
1283
1283
|
});
|
|
1284
1284
|
} catch (error) {
|
|
1285
|
-
console.
|
|
1285
|
+
console.warn("Error setting context:", error);
|
|
1286
1286
|
}
|
|
1287
1287
|
return this.handleEvent("identify", body);
|
|
1288
1288
|
};
|
|
@@ -1340,7 +1340,7 @@ var Schematic = class {
|
|
|
1340
1340
|
const socket = await this.conn;
|
|
1341
1341
|
await this.wsSendMessage(socket, context);
|
|
1342
1342
|
} catch (error) {
|
|
1343
|
-
console.
|
|
1343
|
+
console.warn("Failed to establish WebSocket connection:", error);
|
|
1344
1344
|
throw error;
|
|
1345
1345
|
}
|
|
1346
1346
|
};
|
|
@@ -1613,38 +1613,6 @@ var Schematic = class {
|
|
|
1613
1613
|
/**
|
|
1614
1614
|
* Websocket management
|
|
1615
1615
|
*/
|
|
1616
|
-
/**
|
|
1617
|
-
* If using websocket mode, close the connection when done.
|
|
1618
|
-
* In offline mode, this is a no-op.
|
|
1619
|
-
*/
|
|
1620
|
-
cleanup = async () => {
|
|
1621
|
-
if (this.isOffline()) {
|
|
1622
|
-
this.debug("cleanup: skipped (offline mode)");
|
|
1623
|
-
return Promise.resolve();
|
|
1624
|
-
}
|
|
1625
|
-
this.wsIntentionalDisconnect = true;
|
|
1626
|
-
if (this.wsReconnectTimer !== null) {
|
|
1627
|
-
clearTimeout(this.wsReconnectTimer);
|
|
1628
|
-
this.wsReconnectTimer = null;
|
|
1629
|
-
}
|
|
1630
|
-
this.stopRetryTimer();
|
|
1631
|
-
if (this.conn) {
|
|
1632
|
-
try {
|
|
1633
|
-
const socket = await this.conn;
|
|
1634
|
-
if (this.currentWebSocket === socket) {
|
|
1635
|
-
this.debug(`Cleaning up current websocket tracking`);
|
|
1636
|
-
this.currentWebSocket = null;
|
|
1637
|
-
}
|
|
1638
|
-
socket.close();
|
|
1639
|
-
} catch (error) {
|
|
1640
|
-
console.error("Error during cleanup:", error);
|
|
1641
|
-
} finally {
|
|
1642
|
-
this.conn = null;
|
|
1643
|
-
this.currentWebSocket = null;
|
|
1644
|
-
this.isConnecting = false;
|
|
1645
|
-
}
|
|
1646
|
-
}
|
|
1647
|
-
};
|
|
1648
1616
|
/**
|
|
1649
1617
|
* Force an immediate WebSocket reconnection.
|
|
1650
1618
|
* This is useful when the application returns from a background state (e.g., mobile app
|
|
@@ -1654,11 +1622,11 @@ var Schematic = class {
|
|
|
1654
1622
|
* This method will:
|
|
1655
1623
|
* - Cancel any pending reconnection timer
|
|
1656
1624
|
* - Reset the reconnection attempt counter
|
|
1625
|
+
* - Close any existing connection
|
|
1657
1626
|
* - Immediately attempt to reconnect
|
|
1658
1627
|
* - Re-send the current context to get fresh flag values
|
|
1659
1628
|
*
|
|
1660
|
-
*
|
|
1661
|
-
* If you just want to ensure a connection exists, check the connection state first.
|
|
1629
|
+
* Use this when you need guaranteed fresh values (e.g., after an in-app purchase).
|
|
1662
1630
|
*
|
|
1663
1631
|
* @example
|
|
1664
1632
|
* ```typescript
|
|
@@ -1674,20 +1642,69 @@ var Schematic = class {
|
|
|
1674
1642
|
* ```
|
|
1675
1643
|
*/
|
|
1676
1644
|
forceReconnect = async () => {
|
|
1645
|
+
return this.reconnect({ force: true });
|
|
1646
|
+
};
|
|
1647
|
+
/**
|
|
1648
|
+
* Reconnect the WebSocket connection only if the current connection is unhealthy.
|
|
1649
|
+
* This is useful when the application returns from a background state and wants to
|
|
1650
|
+
* ensure a healthy connection exists, but doesn't need to force a reconnection if
|
|
1651
|
+
* the connection is still active.
|
|
1652
|
+
*
|
|
1653
|
+
* This method will:
|
|
1654
|
+
* - Check if an existing connection is healthy (readyState === OPEN)
|
|
1655
|
+
* - If healthy, return immediately without reconnecting
|
|
1656
|
+
* - If unhealthy, perform the same reconnection logic as forceReconnect()
|
|
1657
|
+
*
|
|
1658
|
+
* Use this when you want efficient reconnection that avoids unnecessary disconnects.
|
|
1659
|
+
*
|
|
1660
|
+
* @example
|
|
1661
|
+
* ```typescript
|
|
1662
|
+
* // React Native example: reconnect only if needed when app comes to foreground
|
|
1663
|
+
* useEffect(() => {
|
|
1664
|
+
* const subscription = AppState.addEventListener("change", (state) => {
|
|
1665
|
+
* if (state === "active") {
|
|
1666
|
+
* client.reconnectIfNeeded();
|
|
1667
|
+
* }
|
|
1668
|
+
* });
|
|
1669
|
+
* return () => subscription.remove();
|
|
1670
|
+
* }, [client]);
|
|
1671
|
+
* ```
|
|
1672
|
+
*/
|
|
1673
|
+
reconnectIfNeeded = async () => {
|
|
1674
|
+
return this.reconnect({ force: false });
|
|
1675
|
+
};
|
|
1676
|
+
/**
|
|
1677
|
+
* Internal method to handle reconnection logic for both forceReconnect and reconnectIfNeeded.
|
|
1678
|
+
*/
|
|
1679
|
+
reconnect = async (options) => {
|
|
1680
|
+
const { force } = options;
|
|
1681
|
+
const methodName = force ? "forceReconnect" : "reconnectIfNeeded";
|
|
1677
1682
|
if (this.isOffline()) {
|
|
1678
|
-
this.debug(
|
|
1683
|
+
this.debug(`${methodName}: skipped (offline mode)`);
|
|
1679
1684
|
return Promise.resolve();
|
|
1680
1685
|
}
|
|
1681
|
-
this.
|
|
1686
|
+
if (!force && this.conn !== null) {
|
|
1687
|
+
try {
|
|
1688
|
+
const existingSocket = await this.conn;
|
|
1689
|
+
if (existingSocket.readyState === WebSocket.OPEN) {
|
|
1690
|
+
this.debug(`${methodName}: connection is healthy, skipping`);
|
|
1691
|
+
return Promise.resolve();
|
|
1692
|
+
}
|
|
1693
|
+
} catch {
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
this.debug(
|
|
1697
|
+
`${methodName}: ${force ? "forcing immediate reconnection" : "reconnecting"}`
|
|
1698
|
+
);
|
|
1682
1699
|
this.wsIntentionalDisconnect = false;
|
|
1683
1700
|
if (this.wsReconnectTimer !== null) {
|
|
1684
|
-
this.debug(
|
|
1701
|
+
this.debug(`${methodName}: cancelling pending reconnection timer`);
|
|
1685
1702
|
clearTimeout(this.wsReconnectTimer);
|
|
1686
1703
|
this.wsReconnectTimer = null;
|
|
1687
1704
|
}
|
|
1688
1705
|
this.wsReconnectAttempts = 0;
|
|
1689
1706
|
if (this.conn !== null) {
|
|
1690
|
-
this.debug(
|
|
1707
|
+
this.debug(`${methodName}: closing existing connection`);
|
|
1691
1708
|
try {
|
|
1692
1709
|
const existingSocket = await this.conn;
|
|
1693
1710
|
if (this.currentWebSocket === existingSocket) {
|
|
@@ -1697,29 +1714,59 @@ var Schematic = class {
|
|
|
1697
1714
|
existingSocket.close();
|
|
1698
1715
|
}
|
|
1699
1716
|
} catch (error) {
|
|
1700
|
-
this.debug(
|
|
1717
|
+
this.debug(`${methodName}: error closing existing connection:`, error);
|
|
1701
1718
|
}
|
|
1702
1719
|
this.conn = null;
|
|
1703
1720
|
this.isConnecting = false;
|
|
1704
1721
|
}
|
|
1705
1722
|
if (this.context.company !== void 0 || this.context.user !== void 0) {
|
|
1706
|
-
this.debug(
|
|
1723
|
+
this.debug(`${methodName}: reconnecting with existing context`);
|
|
1707
1724
|
try {
|
|
1708
1725
|
this.isConnecting = true;
|
|
1709
1726
|
this.conn = this.wsConnect();
|
|
1710
1727
|
const socket = await this.conn;
|
|
1711
1728
|
this.isConnecting = false;
|
|
1712
1729
|
await this.wsSendMessage(socket, this.context, true);
|
|
1713
|
-
this.debug(
|
|
1730
|
+
this.debug(`${methodName}: reconnection successful`);
|
|
1714
1731
|
} catch (error) {
|
|
1715
1732
|
this.isConnecting = false;
|
|
1716
|
-
this.debug(
|
|
1717
|
-
this.attemptReconnect();
|
|
1733
|
+
this.debug(`${methodName}: reconnection failed:`, error);
|
|
1718
1734
|
}
|
|
1719
1735
|
} else {
|
|
1720
|
-
this.debug(
|
|
1721
|
-
|
|
1722
|
-
|
|
1736
|
+
this.debug(`${methodName}: no context set, skipping reconnection`);
|
|
1737
|
+
}
|
|
1738
|
+
return Promise.resolve();
|
|
1739
|
+
};
|
|
1740
|
+
/**
|
|
1741
|
+
* If using websocket mode, close the connection when done.
|
|
1742
|
+
* In offline mode, this is a no-op.
|
|
1743
|
+
*/
|
|
1744
|
+
cleanup = async () => {
|
|
1745
|
+
if (this.isOffline()) {
|
|
1746
|
+
this.debug("cleanup: skipped (offline mode)");
|
|
1747
|
+
return Promise.resolve();
|
|
1748
|
+
}
|
|
1749
|
+
this.wsIntentionalDisconnect = true;
|
|
1750
|
+
if (this.wsReconnectTimer !== null) {
|
|
1751
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1752
|
+
this.wsReconnectTimer = null;
|
|
1753
|
+
}
|
|
1754
|
+
this.stopRetryTimer();
|
|
1755
|
+
if (this.conn) {
|
|
1756
|
+
try {
|
|
1757
|
+
const socket = await this.conn;
|
|
1758
|
+
if (this.currentWebSocket === socket) {
|
|
1759
|
+
this.debug(`Cleaning up current websocket tracking`);
|
|
1760
|
+
this.currentWebSocket = null;
|
|
1761
|
+
}
|
|
1762
|
+
socket.close();
|
|
1763
|
+
} catch (error) {
|
|
1764
|
+
console.warn("Error during cleanup:", error);
|
|
1765
|
+
} finally {
|
|
1766
|
+
this.conn = null;
|
|
1767
|
+
this.currentWebSocket = null;
|
|
1768
|
+
this.isConnecting = false;
|
|
1769
|
+
}
|
|
1723
1770
|
}
|
|
1724
1771
|
};
|
|
1725
1772
|
/**
|
|
@@ -1857,14 +1904,49 @@ var Schematic = class {
|
|
|
1857
1904
|
}
|
|
1858
1905
|
}, delay);
|
|
1859
1906
|
};
|
|
1860
|
-
// Open a websocket connection
|
|
1861
|
-
wsConnect = () => {
|
|
1907
|
+
// Open a websocket connection with retry logic for timeouts
|
|
1908
|
+
wsConnect = async () => {
|
|
1862
1909
|
if (this.isOffline()) {
|
|
1863
1910
|
this.debug("wsConnect: skipped (offline mode)");
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1911
|
+
throw new Error("WebSocket connection skipped in offline mode");
|
|
1912
|
+
}
|
|
1913
|
+
let lastError = null;
|
|
1914
|
+
const maxAttempts = this.webSocketMaxConnectionAttempts;
|
|
1915
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1916
|
+
try {
|
|
1917
|
+
const socket = await this.wsConnectOnce();
|
|
1918
|
+
this.wsReconnectAttempts = 0;
|
|
1919
|
+
return socket;
|
|
1920
|
+
} catch (error) {
|
|
1921
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1922
|
+
const isTimeout = lastError.message === "WebSocket connection timeout";
|
|
1923
|
+
if (!isTimeout) {
|
|
1924
|
+
this.debug(
|
|
1925
|
+
`WebSocket connection failed with non-timeout error, not retrying:`,
|
|
1926
|
+
lastError.message
|
|
1927
|
+
);
|
|
1928
|
+
throw lastError;
|
|
1929
|
+
}
|
|
1930
|
+
if (attempt < maxAttempts - 1) {
|
|
1931
|
+
const baseDelay = this.webSocketInitialRetryDelay * Math.pow(2, attempt);
|
|
1932
|
+
const cappedDelay = Math.min(baseDelay, this.webSocketMaxRetryDelay);
|
|
1933
|
+
const jitter = cappedDelay * 0.2 * Math.random();
|
|
1934
|
+
const delay = cappedDelay + jitter;
|
|
1935
|
+
this.debug(
|
|
1936
|
+
`WebSocket connection timeout (attempt ${attempt + 1}/${maxAttempts}), retrying in ${delay.toFixed(0)}ms`
|
|
1937
|
+
);
|
|
1938
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1939
|
+
} else {
|
|
1940
|
+
this.debug(
|
|
1941
|
+
`WebSocket connection timeout (attempt ${attempt + 1}/${maxAttempts}), no more retries`
|
|
1942
|
+
);
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1867
1945
|
}
|
|
1946
|
+
throw lastError ?? new Error("WebSocket connection failed");
|
|
1947
|
+
};
|
|
1948
|
+
// Single attempt to open a websocket connection (no retry logic)
|
|
1949
|
+
wsConnectOnce = () => {
|
|
1868
1950
|
return new Promise((resolve, reject) => {
|
|
1869
1951
|
const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
|
|
1870
1952
|
this.debug(`connecting to WebSocket:`, wsUrl);
|
|
@@ -1875,6 +1957,7 @@ var Schematic = class {
|
|
|
1875
1957
|
let isResolved = false;
|
|
1876
1958
|
timeoutId = setTimeout(() => {
|
|
1877
1959
|
if (!isResolved) {
|
|
1960
|
+
isResolved = true;
|
|
1878
1961
|
this.debug(
|
|
1879
1962
|
`WebSocket connection timeout after ${this.webSocketConnectionTimeout}ms`
|
|
1880
1963
|
);
|
|
@@ -1883,16 +1966,17 @@ var Schematic = class {
|
|
|
1883
1966
|
}
|
|
1884
1967
|
}, this.webSocketConnectionTimeout);
|
|
1885
1968
|
webSocket.onopen = () => {
|
|
1969
|
+
if (isResolved) return;
|
|
1886
1970
|
isResolved = true;
|
|
1887
1971
|
if (timeoutId !== null) {
|
|
1888
1972
|
clearTimeout(timeoutId);
|
|
1889
1973
|
}
|
|
1890
|
-
this.wsReconnectAttempts = 0;
|
|
1891
1974
|
this.wsIntentionalDisconnect = false;
|
|
1892
1975
|
this.debug(`WebSocket connection ${connectionId} opened successfully`);
|
|
1893
1976
|
resolve(webSocket);
|
|
1894
1977
|
};
|
|
1895
1978
|
webSocket.onerror = (error) => {
|
|
1979
|
+
if (isResolved) return;
|
|
1896
1980
|
isResolved = true;
|
|
1897
1981
|
if (timeoutId !== null) {
|
|
1898
1982
|
clearTimeout(timeoutId);
|
|
@@ -1901,7 +1985,6 @@ var Schematic = class {
|
|
|
1901
1985
|
reject(error);
|
|
1902
1986
|
};
|
|
1903
1987
|
webSocket.onclose = () => {
|
|
1904
|
-
isResolved = true;
|
|
1905
1988
|
if (timeoutId !== null) {
|
|
1906
1989
|
clearTimeout(timeoutId);
|
|
1907
1990
|
}
|
|
@@ -1911,7 +1994,7 @@ var Schematic = class {
|
|
|
1911
1994
|
this.currentWebSocket = null;
|
|
1912
1995
|
this.isConnecting = false;
|
|
1913
1996
|
}
|
|
1914
|
-
if (!this.wsIntentionalDisconnect && this.webSocketReconnect) {
|
|
1997
|
+
if (!isResolved && !this.wsIntentionalDisconnect && this.webSocketReconnect) {
|
|
1915
1998
|
this.attemptReconnect();
|
|
1916
1999
|
}
|
|
1917
2000
|
};
|
package/dist/schematic.d.ts
CHANGED
|
@@ -373,6 +373,7 @@ export declare class Schematic {
|
|
|
373
373
|
private webSocketConnectionTimeout;
|
|
374
374
|
private webSocketReconnect;
|
|
375
375
|
private webSocketMaxReconnectAttempts;
|
|
376
|
+
private webSocketMaxConnectionAttempts;
|
|
376
377
|
private webSocketInitialRetryDelay;
|
|
377
378
|
private webSocketMaxRetryDelay;
|
|
378
379
|
private wsReconnectAttempts;
|
|
@@ -482,11 +483,6 @@ export declare class Schematic {
|
|
|
482
483
|
/**
|
|
483
484
|
* Websocket management
|
|
484
485
|
*/
|
|
485
|
-
/**
|
|
486
|
-
* If using websocket mode, close the connection when done.
|
|
487
|
-
* In offline mode, this is a no-op.
|
|
488
|
-
*/
|
|
489
|
-
cleanup: () => Promise<void>;
|
|
490
486
|
/**
|
|
491
487
|
* Force an immediate WebSocket reconnection.
|
|
492
488
|
* This is useful when the application returns from a background state (e.g., mobile app
|
|
@@ -496,11 +492,11 @@ export declare class Schematic {
|
|
|
496
492
|
* This method will:
|
|
497
493
|
* - Cancel any pending reconnection timer
|
|
498
494
|
* - Reset the reconnection attempt counter
|
|
495
|
+
* - Close any existing connection
|
|
499
496
|
* - Immediately attempt to reconnect
|
|
500
497
|
* - Re-send the current context to get fresh flag values
|
|
501
498
|
*
|
|
502
|
-
*
|
|
503
|
-
* If you just want to ensure a connection exists, check the connection state first.
|
|
499
|
+
* Use this when you need guaranteed fresh values (e.g., after an in-app purchase).
|
|
504
500
|
*
|
|
505
501
|
* @example
|
|
506
502
|
* ```typescript
|
|
@@ -516,6 +512,42 @@ export declare class Schematic {
|
|
|
516
512
|
* ```
|
|
517
513
|
*/
|
|
518
514
|
forceReconnect: () => Promise<void>;
|
|
515
|
+
/**
|
|
516
|
+
* Reconnect the WebSocket connection only if the current connection is unhealthy.
|
|
517
|
+
* This is useful when the application returns from a background state and wants to
|
|
518
|
+
* ensure a healthy connection exists, but doesn't need to force a reconnection if
|
|
519
|
+
* the connection is still active.
|
|
520
|
+
*
|
|
521
|
+
* This method will:
|
|
522
|
+
* - Check if an existing connection is healthy (readyState === OPEN)
|
|
523
|
+
* - If healthy, return immediately without reconnecting
|
|
524
|
+
* - If unhealthy, perform the same reconnection logic as forceReconnect()
|
|
525
|
+
*
|
|
526
|
+
* Use this when you want efficient reconnection that avoids unnecessary disconnects.
|
|
527
|
+
*
|
|
528
|
+
* @example
|
|
529
|
+
* ```typescript
|
|
530
|
+
* // React Native example: reconnect only if needed when app comes to foreground
|
|
531
|
+
* useEffect(() => {
|
|
532
|
+
* const subscription = AppState.addEventListener("change", (state) => {
|
|
533
|
+
* if (state === "active") {
|
|
534
|
+
* client.reconnectIfNeeded();
|
|
535
|
+
* }
|
|
536
|
+
* });
|
|
537
|
+
* return () => subscription.remove();
|
|
538
|
+
* }, [client]);
|
|
539
|
+
* ```
|
|
540
|
+
*/
|
|
541
|
+
reconnectIfNeeded: () => Promise<void>;
|
|
542
|
+
/**
|
|
543
|
+
* Internal method to handle reconnection logic for both forceReconnect and reconnectIfNeeded.
|
|
544
|
+
*/
|
|
545
|
+
private reconnect;
|
|
546
|
+
/**
|
|
547
|
+
* If using websocket mode, close the connection when done.
|
|
548
|
+
* In offline mode, this is a no-op.
|
|
549
|
+
*/
|
|
550
|
+
cleanup: () => Promise<void>;
|
|
519
551
|
/**
|
|
520
552
|
* Calculate the delay for the next reconnection attempt using exponential backoff with jitter.
|
|
521
553
|
* This helps prevent dogpiling when the server recovers from an outage.
|
|
@@ -535,6 +567,7 @@ export declare class Schematic {
|
|
|
535
567
|
*/
|
|
536
568
|
private attemptReconnect;
|
|
537
569
|
private wsConnect;
|
|
570
|
+
private wsConnectOnce;
|
|
538
571
|
private wsSendMessage;
|
|
539
572
|
/**
|
|
540
573
|
* 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.13";
|
|
785
785
|
|
|
786
786
|
// src/index.ts
|
|
787
787
|
var anonymousIdKey = "schematicId";
|
|
@@ -808,6 +808,9 @@ var Schematic = class {
|
|
|
808
808
|
webSocketConnectionTimeout = 1e4;
|
|
809
809
|
webSocketReconnect = true;
|
|
810
810
|
webSocketMaxReconnectAttempts = 7;
|
|
811
|
+
// Max attempts after connection disrupted
|
|
812
|
+
webSocketMaxConnectionAttempts = 3;
|
|
813
|
+
// Max attempts for initial connection
|
|
811
814
|
webSocketInitialRetryDelay = 1e3;
|
|
812
815
|
webSocketMaxRetryDelay = 3e4;
|
|
813
816
|
wsReconnectAttempts = 0;
|
|
@@ -1031,7 +1034,7 @@ var Schematic = class {
|
|
|
1031
1034
|
this.submitFlagCheckEvent(key, result, context);
|
|
1032
1035
|
return result.value;
|
|
1033
1036
|
}).catch((error) => {
|
|
1034
|
-
console.
|
|
1037
|
+
console.warn("There was a problem with the fetch operation:", error);
|
|
1035
1038
|
const errorResult = this.resolveFallbackCheckFlagReturn(
|
|
1036
1039
|
key,
|
|
1037
1040
|
fallback,
|
|
@@ -1054,10 +1057,7 @@ var Schematic = class {
|
|
|
1054
1057
|
try {
|
|
1055
1058
|
await this.setContext(context);
|
|
1056
1059
|
} catch (error) {
|
|
1057
|
-
console.error
|
|
1058
|
-
"WebSocket connection failed, falling back to REST:",
|
|
1059
|
-
error
|
|
1060
|
-
);
|
|
1060
|
+
console.warn("WebSocket connection failed, falling back to REST:", error);
|
|
1061
1061
|
return this.fallbackToRest(key, context, fallback);
|
|
1062
1062
|
}
|
|
1063
1063
|
const contextVals = this.checks[contextStr] ?? {};
|
|
@@ -1197,7 +1197,7 @@ var Schematic = class {
|
|
|
1197
1197
|
this.submitFlagCheckEvent(key, result, context);
|
|
1198
1198
|
return result.value;
|
|
1199
1199
|
} catch (error) {
|
|
1200
|
-
console.
|
|
1200
|
+
console.warn("REST API call failed, using fallback value:", error);
|
|
1201
1201
|
const errorResult = this.resolveFallbackCheckFlagReturn(
|
|
1202
1202
|
key,
|
|
1203
1203
|
fallback,
|
|
@@ -1246,7 +1246,7 @@ var Schematic = class {
|
|
|
1246
1246
|
{}
|
|
1247
1247
|
);
|
|
1248
1248
|
}).catch((error) => {
|
|
1249
|
-
console.
|
|
1249
|
+
console.warn("There was a problem with the fetch operation:", error);
|
|
1250
1250
|
return {};
|
|
1251
1251
|
});
|
|
1252
1252
|
};
|
|
@@ -1263,7 +1263,7 @@ var Schematic = class {
|
|
|
1263
1263
|
user: body.keys
|
|
1264
1264
|
});
|
|
1265
1265
|
} catch (error) {
|
|
1266
|
-
console.
|
|
1266
|
+
console.warn("Error setting context:", error);
|
|
1267
1267
|
}
|
|
1268
1268
|
return this.handleEvent("identify", body);
|
|
1269
1269
|
};
|
|
@@ -1321,7 +1321,7 @@ var Schematic = class {
|
|
|
1321
1321
|
const socket = await this.conn;
|
|
1322
1322
|
await this.wsSendMessage(socket, context);
|
|
1323
1323
|
} catch (error) {
|
|
1324
|
-
console.
|
|
1324
|
+
console.warn("Failed to establish WebSocket connection:", error);
|
|
1325
1325
|
throw error;
|
|
1326
1326
|
}
|
|
1327
1327
|
};
|
|
@@ -1594,38 +1594,6 @@ var Schematic = class {
|
|
|
1594
1594
|
/**
|
|
1595
1595
|
* Websocket management
|
|
1596
1596
|
*/
|
|
1597
|
-
/**
|
|
1598
|
-
* If using websocket mode, close the connection when done.
|
|
1599
|
-
* In offline mode, this is a no-op.
|
|
1600
|
-
*/
|
|
1601
|
-
cleanup = async () => {
|
|
1602
|
-
if (this.isOffline()) {
|
|
1603
|
-
this.debug("cleanup: skipped (offline mode)");
|
|
1604
|
-
return Promise.resolve();
|
|
1605
|
-
}
|
|
1606
|
-
this.wsIntentionalDisconnect = true;
|
|
1607
|
-
if (this.wsReconnectTimer !== null) {
|
|
1608
|
-
clearTimeout(this.wsReconnectTimer);
|
|
1609
|
-
this.wsReconnectTimer = null;
|
|
1610
|
-
}
|
|
1611
|
-
this.stopRetryTimer();
|
|
1612
|
-
if (this.conn) {
|
|
1613
|
-
try {
|
|
1614
|
-
const socket = await this.conn;
|
|
1615
|
-
if (this.currentWebSocket === socket) {
|
|
1616
|
-
this.debug(`Cleaning up current websocket tracking`);
|
|
1617
|
-
this.currentWebSocket = null;
|
|
1618
|
-
}
|
|
1619
|
-
socket.close();
|
|
1620
|
-
} catch (error) {
|
|
1621
|
-
console.error("Error during cleanup:", error);
|
|
1622
|
-
} finally {
|
|
1623
|
-
this.conn = null;
|
|
1624
|
-
this.currentWebSocket = null;
|
|
1625
|
-
this.isConnecting = false;
|
|
1626
|
-
}
|
|
1627
|
-
}
|
|
1628
|
-
};
|
|
1629
1597
|
/**
|
|
1630
1598
|
* Force an immediate WebSocket reconnection.
|
|
1631
1599
|
* This is useful when the application returns from a background state (e.g., mobile app
|
|
@@ -1635,11 +1603,11 @@ var Schematic = class {
|
|
|
1635
1603
|
* This method will:
|
|
1636
1604
|
* - Cancel any pending reconnection timer
|
|
1637
1605
|
* - Reset the reconnection attempt counter
|
|
1606
|
+
* - Close any existing connection
|
|
1638
1607
|
* - Immediately attempt to reconnect
|
|
1639
1608
|
* - Re-send the current context to get fresh flag values
|
|
1640
1609
|
*
|
|
1641
|
-
*
|
|
1642
|
-
* If you just want to ensure a connection exists, check the connection state first.
|
|
1610
|
+
* Use this when you need guaranteed fresh values (e.g., after an in-app purchase).
|
|
1643
1611
|
*
|
|
1644
1612
|
* @example
|
|
1645
1613
|
* ```typescript
|
|
@@ -1655,20 +1623,69 @@ var Schematic = class {
|
|
|
1655
1623
|
* ```
|
|
1656
1624
|
*/
|
|
1657
1625
|
forceReconnect = async () => {
|
|
1626
|
+
return this.reconnect({ force: true });
|
|
1627
|
+
};
|
|
1628
|
+
/**
|
|
1629
|
+
* Reconnect the WebSocket connection only if the current connection is unhealthy.
|
|
1630
|
+
* This is useful when the application returns from a background state and wants to
|
|
1631
|
+
* ensure a healthy connection exists, but doesn't need to force a reconnection if
|
|
1632
|
+
* the connection is still active.
|
|
1633
|
+
*
|
|
1634
|
+
* This method will:
|
|
1635
|
+
* - Check if an existing connection is healthy (readyState === OPEN)
|
|
1636
|
+
* - If healthy, return immediately without reconnecting
|
|
1637
|
+
* - If unhealthy, perform the same reconnection logic as forceReconnect()
|
|
1638
|
+
*
|
|
1639
|
+
* Use this when you want efficient reconnection that avoids unnecessary disconnects.
|
|
1640
|
+
*
|
|
1641
|
+
* @example
|
|
1642
|
+
* ```typescript
|
|
1643
|
+
* // React Native example: reconnect only if needed when app comes to foreground
|
|
1644
|
+
* useEffect(() => {
|
|
1645
|
+
* const subscription = AppState.addEventListener("change", (state) => {
|
|
1646
|
+
* if (state === "active") {
|
|
1647
|
+
* client.reconnectIfNeeded();
|
|
1648
|
+
* }
|
|
1649
|
+
* });
|
|
1650
|
+
* return () => subscription.remove();
|
|
1651
|
+
* }, [client]);
|
|
1652
|
+
* ```
|
|
1653
|
+
*/
|
|
1654
|
+
reconnectIfNeeded = async () => {
|
|
1655
|
+
return this.reconnect({ force: false });
|
|
1656
|
+
};
|
|
1657
|
+
/**
|
|
1658
|
+
* Internal method to handle reconnection logic for both forceReconnect and reconnectIfNeeded.
|
|
1659
|
+
*/
|
|
1660
|
+
reconnect = async (options) => {
|
|
1661
|
+
const { force } = options;
|
|
1662
|
+
const methodName = force ? "forceReconnect" : "reconnectIfNeeded";
|
|
1658
1663
|
if (this.isOffline()) {
|
|
1659
|
-
this.debug(
|
|
1664
|
+
this.debug(`${methodName}: skipped (offline mode)`);
|
|
1660
1665
|
return Promise.resolve();
|
|
1661
1666
|
}
|
|
1662
|
-
this.
|
|
1667
|
+
if (!force && this.conn !== null) {
|
|
1668
|
+
try {
|
|
1669
|
+
const existingSocket = await this.conn;
|
|
1670
|
+
if (existingSocket.readyState === WebSocket.OPEN) {
|
|
1671
|
+
this.debug(`${methodName}: connection is healthy, skipping`);
|
|
1672
|
+
return Promise.resolve();
|
|
1673
|
+
}
|
|
1674
|
+
} catch {
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
this.debug(
|
|
1678
|
+
`${methodName}: ${force ? "forcing immediate reconnection" : "reconnecting"}`
|
|
1679
|
+
);
|
|
1663
1680
|
this.wsIntentionalDisconnect = false;
|
|
1664
1681
|
if (this.wsReconnectTimer !== null) {
|
|
1665
|
-
this.debug(
|
|
1682
|
+
this.debug(`${methodName}: cancelling pending reconnection timer`);
|
|
1666
1683
|
clearTimeout(this.wsReconnectTimer);
|
|
1667
1684
|
this.wsReconnectTimer = null;
|
|
1668
1685
|
}
|
|
1669
1686
|
this.wsReconnectAttempts = 0;
|
|
1670
1687
|
if (this.conn !== null) {
|
|
1671
|
-
this.debug(
|
|
1688
|
+
this.debug(`${methodName}: closing existing connection`);
|
|
1672
1689
|
try {
|
|
1673
1690
|
const existingSocket = await this.conn;
|
|
1674
1691
|
if (this.currentWebSocket === existingSocket) {
|
|
@@ -1678,29 +1695,59 @@ var Schematic = class {
|
|
|
1678
1695
|
existingSocket.close();
|
|
1679
1696
|
}
|
|
1680
1697
|
} catch (error) {
|
|
1681
|
-
this.debug(
|
|
1698
|
+
this.debug(`${methodName}: error closing existing connection:`, error);
|
|
1682
1699
|
}
|
|
1683
1700
|
this.conn = null;
|
|
1684
1701
|
this.isConnecting = false;
|
|
1685
1702
|
}
|
|
1686
1703
|
if (this.context.company !== void 0 || this.context.user !== void 0) {
|
|
1687
|
-
this.debug(
|
|
1704
|
+
this.debug(`${methodName}: reconnecting with existing context`);
|
|
1688
1705
|
try {
|
|
1689
1706
|
this.isConnecting = true;
|
|
1690
1707
|
this.conn = this.wsConnect();
|
|
1691
1708
|
const socket = await this.conn;
|
|
1692
1709
|
this.isConnecting = false;
|
|
1693
1710
|
await this.wsSendMessage(socket, this.context, true);
|
|
1694
|
-
this.debug(
|
|
1711
|
+
this.debug(`${methodName}: reconnection successful`);
|
|
1695
1712
|
} catch (error) {
|
|
1696
1713
|
this.isConnecting = false;
|
|
1697
|
-
this.debug(
|
|
1698
|
-
this.attemptReconnect();
|
|
1714
|
+
this.debug(`${methodName}: reconnection failed:`, error);
|
|
1699
1715
|
}
|
|
1700
1716
|
} else {
|
|
1701
|
-
this.debug(
|
|
1702
|
-
|
|
1703
|
-
|
|
1717
|
+
this.debug(`${methodName}: no context set, skipping reconnection`);
|
|
1718
|
+
}
|
|
1719
|
+
return Promise.resolve();
|
|
1720
|
+
};
|
|
1721
|
+
/**
|
|
1722
|
+
* If using websocket mode, close the connection when done.
|
|
1723
|
+
* In offline mode, this is a no-op.
|
|
1724
|
+
*/
|
|
1725
|
+
cleanup = async () => {
|
|
1726
|
+
if (this.isOffline()) {
|
|
1727
|
+
this.debug("cleanup: skipped (offline mode)");
|
|
1728
|
+
return Promise.resolve();
|
|
1729
|
+
}
|
|
1730
|
+
this.wsIntentionalDisconnect = true;
|
|
1731
|
+
if (this.wsReconnectTimer !== null) {
|
|
1732
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1733
|
+
this.wsReconnectTimer = null;
|
|
1734
|
+
}
|
|
1735
|
+
this.stopRetryTimer();
|
|
1736
|
+
if (this.conn) {
|
|
1737
|
+
try {
|
|
1738
|
+
const socket = await this.conn;
|
|
1739
|
+
if (this.currentWebSocket === socket) {
|
|
1740
|
+
this.debug(`Cleaning up current websocket tracking`);
|
|
1741
|
+
this.currentWebSocket = null;
|
|
1742
|
+
}
|
|
1743
|
+
socket.close();
|
|
1744
|
+
} catch (error) {
|
|
1745
|
+
console.warn("Error during cleanup:", error);
|
|
1746
|
+
} finally {
|
|
1747
|
+
this.conn = null;
|
|
1748
|
+
this.currentWebSocket = null;
|
|
1749
|
+
this.isConnecting = false;
|
|
1750
|
+
}
|
|
1704
1751
|
}
|
|
1705
1752
|
};
|
|
1706
1753
|
/**
|
|
@@ -1838,14 +1885,49 @@ var Schematic = class {
|
|
|
1838
1885
|
}
|
|
1839
1886
|
}, delay);
|
|
1840
1887
|
};
|
|
1841
|
-
// Open a websocket connection
|
|
1842
|
-
wsConnect = () => {
|
|
1888
|
+
// Open a websocket connection with retry logic for timeouts
|
|
1889
|
+
wsConnect = async () => {
|
|
1843
1890
|
if (this.isOffline()) {
|
|
1844
1891
|
this.debug("wsConnect: skipped (offline mode)");
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1892
|
+
throw new Error("WebSocket connection skipped in offline mode");
|
|
1893
|
+
}
|
|
1894
|
+
let lastError = null;
|
|
1895
|
+
const maxAttempts = this.webSocketMaxConnectionAttempts;
|
|
1896
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1897
|
+
try {
|
|
1898
|
+
const socket = await this.wsConnectOnce();
|
|
1899
|
+
this.wsReconnectAttempts = 0;
|
|
1900
|
+
return socket;
|
|
1901
|
+
} catch (error) {
|
|
1902
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1903
|
+
const isTimeout = lastError.message === "WebSocket connection timeout";
|
|
1904
|
+
if (!isTimeout) {
|
|
1905
|
+
this.debug(
|
|
1906
|
+
`WebSocket connection failed with non-timeout error, not retrying:`,
|
|
1907
|
+
lastError.message
|
|
1908
|
+
);
|
|
1909
|
+
throw lastError;
|
|
1910
|
+
}
|
|
1911
|
+
if (attempt < maxAttempts - 1) {
|
|
1912
|
+
const baseDelay = this.webSocketInitialRetryDelay * Math.pow(2, attempt);
|
|
1913
|
+
const cappedDelay = Math.min(baseDelay, this.webSocketMaxRetryDelay);
|
|
1914
|
+
const jitter = cappedDelay * 0.2 * Math.random();
|
|
1915
|
+
const delay = cappedDelay + jitter;
|
|
1916
|
+
this.debug(
|
|
1917
|
+
`WebSocket connection timeout (attempt ${attempt + 1}/${maxAttempts}), retrying in ${delay.toFixed(0)}ms`
|
|
1918
|
+
);
|
|
1919
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1920
|
+
} else {
|
|
1921
|
+
this.debug(
|
|
1922
|
+
`WebSocket connection timeout (attempt ${attempt + 1}/${maxAttempts}), no more retries`
|
|
1923
|
+
);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1848
1926
|
}
|
|
1927
|
+
throw lastError ?? new Error("WebSocket connection failed");
|
|
1928
|
+
};
|
|
1929
|
+
// Single attempt to open a websocket connection (no retry logic)
|
|
1930
|
+
wsConnectOnce = () => {
|
|
1849
1931
|
return new Promise((resolve, reject) => {
|
|
1850
1932
|
const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
|
|
1851
1933
|
this.debug(`connecting to WebSocket:`, wsUrl);
|
|
@@ -1856,6 +1938,7 @@ var Schematic = class {
|
|
|
1856
1938
|
let isResolved = false;
|
|
1857
1939
|
timeoutId = setTimeout(() => {
|
|
1858
1940
|
if (!isResolved) {
|
|
1941
|
+
isResolved = true;
|
|
1859
1942
|
this.debug(
|
|
1860
1943
|
`WebSocket connection timeout after ${this.webSocketConnectionTimeout}ms`
|
|
1861
1944
|
);
|
|
@@ -1864,16 +1947,17 @@ var Schematic = class {
|
|
|
1864
1947
|
}
|
|
1865
1948
|
}, this.webSocketConnectionTimeout);
|
|
1866
1949
|
webSocket.onopen = () => {
|
|
1950
|
+
if (isResolved) return;
|
|
1867
1951
|
isResolved = true;
|
|
1868
1952
|
if (timeoutId !== null) {
|
|
1869
1953
|
clearTimeout(timeoutId);
|
|
1870
1954
|
}
|
|
1871
|
-
this.wsReconnectAttempts = 0;
|
|
1872
1955
|
this.wsIntentionalDisconnect = false;
|
|
1873
1956
|
this.debug(`WebSocket connection ${connectionId} opened successfully`);
|
|
1874
1957
|
resolve(webSocket);
|
|
1875
1958
|
};
|
|
1876
1959
|
webSocket.onerror = (error) => {
|
|
1960
|
+
if (isResolved) return;
|
|
1877
1961
|
isResolved = true;
|
|
1878
1962
|
if (timeoutId !== null) {
|
|
1879
1963
|
clearTimeout(timeoutId);
|
|
@@ -1882,7 +1966,6 @@ var Schematic = class {
|
|
|
1882
1966
|
reject(error);
|
|
1883
1967
|
};
|
|
1884
1968
|
webSocket.onclose = () => {
|
|
1885
|
-
isResolved = true;
|
|
1886
1969
|
if (timeoutId !== null) {
|
|
1887
1970
|
clearTimeout(timeoutId);
|
|
1888
1971
|
}
|
|
@@ -1892,7 +1975,7 @@ var Schematic = class {
|
|
|
1892
1975
|
this.currentWebSocket = null;
|
|
1893
1976
|
this.isConnecting = false;
|
|
1894
1977
|
}
|
|
1895
|
-
if (!this.wsIntentionalDisconnect && this.webSocketReconnect) {
|
|
1978
|
+
if (!isResolved && !this.wsIntentionalDisconnect && this.webSocketReconnect) {
|
|
1896
1979
|
this.attemptReconnect();
|
|
1897
1980
|
}
|
|
1898
1981
|
};
|