@schematichq/schematic-js 1.2.7 → 1.2.8
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 +167 -14
- package/dist/schematic.d.ts +18 -0
- package/dist/schematic.esm.js +167 -14
- package/package.json +3 -3
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
"use strict";(()=>{var re=Object.create;var
|
|
2
|
-
`)===0?h.substr(1,h.length):h}).forEach(function(h){var g=h.split(":"),d=g.shift().trim();if(d){var _=g.join(":").trim();try{a.append(d,_)}catch(I){console.warn("Response "+I.message)}}}),a}q.call(C.prototype);function E(n,a){if(!(this instanceof E))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(n)}q.call(E.prototype),E.prototype.clone=function(){return new E(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new p(this.headers),url:this.url})},E.error=function(){var n=new E(null,{status:200,statusText:""});return n.ok=!1,n.status=0,n.type="error",n};var ne=[301,302,303,307,308];E.redirect=function(n,a){if(ne.indexOf(a)===-1)throw new RangeError("Invalid status code");return new E(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 A(n,a){return new Promise(function(u,h){var g=new C(n,a);if(g.signal&&g.signal.aborted)return h(new t.DOMException("Aborted","AbortError"));var d=new XMLHttpRequest;function _(){d.abort()}d.onload=function(){var b={statusText:d.statusText,headers:te(d.getAllResponseHeaders()||"")};g.url.indexOf("file://")===0&&(d.status<200||d.status>599)?b.status=200:b.status=d.status,b.url="responseURL"in d?d.responseURL:b.headers.get("X-Request-URL");var S="response"in d?d.response:d.responseText;setTimeout(function(){u(new E(S,b))},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 I(b){try{return b===""&&s.location.href?s.location.href:b}catch{return b}}if(d.open(g.method,I(g.url),!0),g.credentials==="include"?d.withCredentials=!0:g.credentials==="omit"&&(d.withCredentials=!1),"responseType"in d&&(i.blob?d.responseType="blob":i.arrayBuffer&&(d.responseType="arraybuffer")),a&&typeof a.headers=="object"&&!(a.headers instanceof p||s.Headers&&a.headers instanceof s.Headers)){var W=[];Object.getOwnPropertyNames(a.headers).forEach(function(b){W.push(f(b)),d.setRequestHeader(b,y(a.headers[b]))}),g.headers.forEach(function(b,S){W.indexOf(S)===-1&&d.setRequestHeader(S,b)})}else g.headers.forEach(function(b,S){d.setRequestHeader(S,b)});g.signal&&(g.signal.addEventListener("abort",_),d.onreadystatechange=function(){d.readyState===4&&g.signal.removeEventListener("abort",_)}),d.send(typeof g._bodyInit>"u"?null:g._bodyInit)})}return A.polyfill=!0,s.fetch||(s.fetch=A,s.Headers=p,s.Request=C,s.Response=E),t.Headers=p,t.Request=C,t.Response=E,t.fetch=A,t})({})})(typeof self<"u"?self:Q)});var k=[];for(let r=0;r<256;++r)k.push((r+256).toString(16).slice(1));function K(r,e=0){return(k[r[e+0]]+k[r[e+1]]+k[r[e+2]]+k[r[e+3]]+"-"+k[r[e+4]]+k[r[e+5]]+"-"+k[r[e+6]]+k[r[e+7]]+"-"+k[r[e+8]]+k[r[e+9]]+"-"+k[r[e+10]]+k[r[e+11]]+k[r[e+12]]+k[r[e+13]]+k[r[e+14]]+k[r[e+15]]).toLowerCase()}var P,de=new Uint8Array(16);function B(){if(!P){if(typeof crypto>"u"||!crypto.getRandomValues)throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");P=crypto.getRandomValues.bind(crypto)}return P(de)}var fe=typeof crypto<"u"&&crypto.randomUUID&&crypto.randomUUID.bind(crypto),L={randomUUID:fe};function he(r,e,t){r=r||{};let s=r.random??r.rng?.()??B();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 i=0;i<16;++i)e[t+i]=s[i];return e}return K(s)}function pe(r,e,t){return L.randomUUID&&!e&&!r?L.randomUUID():he(r,e,t)}var x=pe;var st=ue(z());function T(r){return ge(r,!1)}function ge(r,e){return r==null?r:{companyId:r.company_id==null?void 0:r.company_id,error:r.error==null?void 0:r.error,featureAllocation:r.feature_allocation==null?void 0:r.feature_allocation,featureUsage:r.feature_usage==null?void 0:r.feature_usage,featureUsageEvent:r.feature_usage_event==null?void 0:r.feature_usage_event,featureUsagePeriod:r.feature_usage_period==null?void 0:r.feature_usage_period,featureUsageResetAt:r.feature_usage_reset_at==null?void 0:new Date(r.feature_usage_reset_at),flag:r.flag,flagId:r.flag_id==null?void 0:r.flag_id,reason:r.reason,ruleId:r.rule_id==null?void 0:r.rule_id,ruleType:r.rule_type==null?void 0:r.rule_type,userId:r.user_id==null?void 0:r.user_id,value:r.value}}function N(r){return ye(r,!1)}function ye(r,e=!1){return r==null?r:{company_id:r.companyId,error:r.error,flag_id:r.flagId,flag_key:r.flagKey,reason:r.reason,req_company:r.reqCompany,req_user:r.reqUser,rule_id:r.ruleId,user_id:r.userId,value:r.value}}function D(r){return be(r,!1)}function be(r,e){return r==null?r:{data:T(r.data),params:r.params}}function X(r){return ke(r,!1)}function ke(r,e){return r==null?r:{flags:r.flags.map(T)}}function M(r){return ve(r,!1)}function ve(r,e){return r==null?r:{data:X(r.data),params:r.params}}var U=r=>{let{companyId:e,error:t,featureAllocation:s,featureUsage:i,featureUsageEvent:c,featureUsagePeriod:o,featureUsageResetAt:l,flag:f,flagId:y,reason:w,ruleId:p,ruleType:v,userId:m,value:R}=T(r);return{featureUsageExceeded:!R&&(v=="company_override_usage_exceeded"||v=="plan_entitlement_usage_exceeded"),companyId:e??void 0,error:t??void 0,featureAllocation:s??void 0,featureUsage:i??void 0,featureUsageEvent:c===null?void 0:c,featureUsagePeriod:o??void 0,featureUsageResetAt:l??void 0,flag:f,flagId:y??void 0,reason:w,ruleId:p??void 0,ruleType:v??void 0,userId:m??void 0,value:R}};function F(r){let e=Object.keys(r).reduce((t,s)=>{let c=Object.keys(r[s]||{}).sort().reduce((o,l)=>(o[l]=r[s][l],o),{});return t[s]=c,t},{});return JSON.stringify(e)}var J="1.2.7";var G="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;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),i=s.get("schematic_debug");i!==null&&(i===""||i==="true"||i==="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@${J}`,...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),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")}async checkFlag(e){let{fallback:t=!1,key:s}=e,i=e.context||this.context,c=F(i);if(this.debug(`checkFlag: ${s}`,{context:i,fallback:t}),this.isOffline())return this.debug(`checkFlag offline result: ${s}`,{value:t,offlineMode:!0}),t;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(i)}).then(l=>{if(!l.ok)throw new Error("Network response was not ok");return l.json()}).then(l=>{let f=D(l);this.debug(`checkFlag result: ${s}`,f);let y=U(f.data);return typeof y.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(y),this.submitFlagCheckEvent(s,y,i),y.value}).catch(l=>{console.error("There was a problem with the fetch operation:",l);let f={flag:s,value:t,reason:"API request failed",error:l instanceof Error?l.message:String(l)};return this.submitFlagCheckEvent(s,f,i),t})}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 t;try{await this.setContext(i)}catch(w){return console.error("WebSocket connection failed, falling back to REST:",w),this.fallbackToRest(s,i,t)}let f=(this.checks[c]??{})[s],y=f?.value??t;return this.debug(`checkFlag WebSocket result: ${s}`,typeof f<"u"?f:{value:t,fallbackUsed:!0}),typeof f<"u"&&this.submitFlagCheckEvent(s,f,i),y}catch(o){console.error("Unexpected error in checkFlag:",o);let l={flag:s,value:t,reason:"Unexpected error in flag check",error:o instanceof Error?o.message:String(o)};return this.submitFlagCheckEvent(s,l,i),t}}debug(e,...t){this.debugEnabled&&console.log(`[Schematic] ${e}`,...t)}isOffline(){return this.offlineEnabled}submitFlagCheckEvent(e,t,s){let i={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:",i),this.handleEvent("flag_check",N(i))}async fallbackToRest(e,t,s){if(this.isOffline())return this.debug(`fallbackToRest offline result: ${e}`,{value:s,offlineMode:!0}),s;try{let i=`${this.apiUrl}/flags/${e}/check`,c=await fetch(i,{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=D(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(i){console.error("REST API call failed, using fallback value:",i);let c={flag:e,value:s,reason:"API request failed (fallback)",error:i instanceof Error?i.message:String(i)};return this.submitFlagCheckEvent(e,c,t),s}}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(i=>{if(!i.ok)throw new Error("Network response was not ok");return i.json()}).then(i=>{let c=M(i);return this.debug("checkFlags result:",c),(c?.data?.flags??[]).reduce((o,l)=>(o[l.flag]=l.value,o),{})}).catch(i=>(console.error("There was a problem with the fetch operation:",i),{}))};identify=e=>{this.debug("identify:",e);try{this.setContext({company:e.company?.keys,user:e.keys})}catch(t){console.error("Error setting context:",t)}return this.handleEvent("identify",e)};setContext=async e=>{if(this.isOffline()||!this.useWebSocket)return this.context=e,this.flushContextDependentEventQueue(),this.setIsPending(!1),Promise.resolve();try{this.setIsPending(!0),this.conn||(this.wsReconnectTimer!==null&&(this.debug("Cancelling scheduled reconnection, connecting immediately"),clearTimeout(this.wsReconnectTimer),this.wsReconnectTimer=null),this.conn=this.wsConnect());let t=await this.conn;await this.wsSendMessage(t,e)}catch(t){throw console.error("Failed to establish WebSocket connection:",t),t}};track=e=>{let{company:t,user:s,event:i,traits:c,quantity:o=1}=e;if(!this.hasContext(t,s)){this.debug(`track: queuing event "${i}" until context is available`);let f={api_key:this.apiKey,body:{company:t,event:i,traits:c??{},user:s,quantity:o},sent_at:new Date().toISOString(),tracker_event_id:x(),tracker_user_id:this.getAnonymousId(),type:"track"};return this.contextDependentEventQueue.push(f),Promise.resolve()}let l={company:t??this.context.company,event:i,traits:c??{},user:s??this.context.user,quantity:o};return this.debug("track:",l),i in this.featureUsageEventMap&&this.optimisticallyUpdateFeatureUsage(i,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(([i,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,y=o.featureUsage>=o.featureAllocation;y!==f&&(o.featureUsageExceeded=y,y&&(o.value=!1),this.debug(`Usage limit status changed for flag: ${i}`,{was:f?"exceeded":"within limits",now:y?"exceeded":"within limits",featureUsage:o.featureUsage,featureAllocation:o.featureAllocation,value:o.value}))}this.featureUsageEventMap[e]!==void 0&&(this.featureUsageEventMap[e][i]=o);let l=F(this.context);this.checks[l]!==void 0&&this.checks[l]!==null&&(this.checks[l][i]=o),this.notifyFlagCheckListeners(i,o),this.notifyFlagValueListeners(i,o.value)}}))};hasContext=(e,t)=>{let s=e!=null&&Object.keys(e).length>0||t!=null&&Object.keys(t).length>0,i=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||i};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},i={...e,body:s,sent_at:new Date().toISOString()};this.sendEvent(i)}else this.sendEvent(e)}};flushEventQueue=()=>{for(;this.eventQueue.length>0;){let e=this.eventQueue.shift();e&&this.sendEvent(e)}};getAnonymousId=()=>{if(!this.storage)return x();let e=this.storage.getItem(G);if(typeof e<"u")return e;let t=x();return this.storage.setItem(G,t),t};handleEvent=(e,t)=>{let s={api_key:this.apiKey,body:t,sent_at:new Date().toISOString(),tracker_event_id:x(),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 i=await fetch(t,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8"},body:s});this.debug("event sent:",{status:i.status,statusText:i.statusText})}catch(i){console.error("Error sending Schematic event: ",i)}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.conn)try{(await this.conn).close()}catch(e){console.error("Error during cleanup:",e)}finally{this.conn=null}};calculateReconnectDelay=()=>{let e=this.webSocketInitialRetryDelay*Math.pow(2,this.wsReconnectAttempts),t=Math.min(e,this.webSocketMaxRetryDelay),s=Math.random()*t*.5,i=t+s;return this.debug(`Reconnect delay calculated: ${i.toFixed(0)}ms (attempt ${this.wsReconnectAttempts+1}/${this.webSocketMaxReconnectAttempts})`),i};handleNetworkOffline=async()=>{if(this.conn!==null){try{(await this.conn).close()}catch(e){this.debug("Error closing connection on offline:",e)}this.conn=null}this.wsReconnectTimer!==null&&(clearTimeout(this.wsReconnectTimer),this.wsReconnectTimer=null)};handleNetworkOnline=()=>{if(this.context.company===void 0&&this.context.user===void 0){this.debug("No context set, skipping reconnection");return}this.wsReconnectAttempts=0,this.wsReconnectTimer!==null&&(clearTimeout(this.wsReconnectTimer),this.wsReconnectTimer=null),this.debug("Network online, reconnecting immediately"),this.attemptReconnect()};attemptReconnect=()=>{if(this.wsReconnectAttempts>=this.webSocketMaxReconnectAttempts){this.debug(`Maximum reconnection attempts (${this.webSocketMaxReconnectAttempts}) reached, giving up`);return}this.wsReconnectTimer!==null&&clearTimeout(this.wsReconnectTimer);let e=this.calculateReconnectDelay();this.debug(`Scheduling reconnection attempt ${this.wsReconnectAttempts+1}/${this.webSocketMaxReconnectAttempts} in ${e.toFixed(0)}ms`),this.wsReconnectTimer=setTimeout(async()=>{this.wsReconnectTimer=null,this.wsReconnectAttempts++,this.debug(`Attempting to reconnect (attempt ${this.wsReconnectAttempts}/${this.webSocketMaxReconnectAttempts})`);try{this.conn=this.wsConnect();let t=await this.conn;(this.context.company!==void 0||this.context.user!==void 0)&&(this.debug("Reconnected, re-sending context"),await this.wsSendMessage(t,this.context)),this.debug("Reconnection successful")}catch(t){this.debug("Reconnection attempt failed:",t)}},e)};wsConnect=()=>this.isOffline()?(this.debug("wsConnect: skipped (offline mode)"),Promise.reject(new Error("WebSocket connection skipped in offline mode"))):new Promise((e,t)=>{let s=`${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;this.debug("connecting to WebSocket:",s);let i=new WebSocket(s),c=null,o=!1;c=setTimeout(()=>{o||(this.debug(`WebSocket connection timeout after ${this.webSocketConnectionTimeout}ms`),i.close(),t(new Error("WebSocket connection timeout")))},this.webSocketConnectionTimeout),i.onopen=()=>{o=!0,c!==null&&clearTimeout(c),this.wsReconnectAttempts=0,this.wsIntentionalDisconnect=!1,this.debug("WebSocket connection opened"),e(i)},i.onerror=l=>{o=!0,c!==null&&clearTimeout(c),this.debug("WebSocket connection error:",l),t(l)},i.onclose=()=>{o=!0,c!==null&&clearTimeout(c),this.debug("WebSocket connection closed"),this.conn=null,!this.wsIntentionalDisconnect&&this.webSocketReconnect&&this.attemptReconnect()}});wsSendMessage=(e,t)=>this.isOffline()?(this.debug("wsSendMessage: skipped (offline mode)"),this.setIsPending(!1),Promise.resolve()):new Promise((s,i)=>{if(F(t)==F(this.context))return this.debug("WebSocket context unchanged, skipping update"),s(this.setIsPending(!1));this.debug("WebSocket context updated:",t),this.context=t;let c=()=>{let o=!1,l=w=>{let p=JSON.parse(w.data);this.debug("WebSocket message received:",p),F(t)in this.checks||(this.checks[F(t)]={}),(p.flags??[]).forEach(v=>{let m=U(v),R=F(t);this.checks[R]===void 0&&(this.checks[R]={}),this.checks[R][m.flag]=m,this.debug("WebSocket flag update:",{flag:m.flag,value:m.value,flagCheck:m}),typeof m.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(m),(this.flagCheckListeners[v.flag]?.size>0||this.flagValueListeners[v.flag]?.size>0)&&this.submitFlagCheckEvent(m.flag,m,t),this.notifyFlagCheckListeners(v.flag,m),this.notifyFlagValueListeners(v.flag,m.value)}),this.flushContextDependentEventQueue(),this.setIsPending(!1),o||(o=!0,s())};e.addEventListener("message",l);let f=this.additionalHeaders["X-Schematic-Client-Version"]??`schematic-js@${J}`,y={apiKey:this.apiKey,clientVersion:f,data:t};this.debug("WebSocket sending message:",y),e.send(JSON.stringify(y))};e.readyState===WebSocket.OPEN?(this.debug("WebSocket already open, sending message"),c()):e.readyState===WebSocket.CONNECTING?(this.debug("WebSocket connecting, waiting for open to send message"),e.addEventListener("open",c)):(this.debug("WebSocket is closed, cannot send message"),i("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=F(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(i=>we(i,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(i=>Fe(i,t))}},Ee=(r,e)=>{r.length>0?r(e):r()},we=(r,e)=>{r.length>0?r(e):r()},Fe=(r,e)=>{r.length>0?r(e):r()};window.Schematic=O;})();
|
|
1
|
+
"use strict";(()=>{var re=Object.create;var Q=Object.defineProperty;var se=Object.getOwnPropertyDescriptor;var ie=Object.getOwnPropertyNames;var ae=Object.getPrototypeOf,oe=Object.prototype.hasOwnProperty;var ce=(r,t)=>()=>(t||r((t={exports:{}}).exports,t),t.exports);var le=(r,t,e,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of ie(t))!oe.call(r,s)&&s!==e&&Q(r,s,{get:()=>t[s],enumerable:!(i=se(t,s))||i.enumerable});return r};var ue=(r,t,e)=>(e=r!=null?re(ae(r)):{},le(t||!r||!r.__esModule?Q(e,"default",{value:r,enumerable:!0}):e,r));var K=ce(z=>{(function(r){var t=(function(e){var i=typeof globalThis<"u"&&globalThis||typeof r<"u"&&r||typeof global<"u"&&global||{},s={searchParams:"URLSearchParams"in i,iterable:"Symbol"in i&&"iterator"in Symbol,blob:"FileReader"in i&&"Blob"in i&&(function(){try{return new Blob,!0}catch{return!1}})(),formData:"FormData"in i,arrayBuffer:"ArrayBuffer"in i};function c(n){return n&&DataView.prototype.isPrototypeOf(n)}if(s.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(n){return n&&o.indexOf(Object.prototype.toString.call(n))>-1};function d(n){if(typeof n!="string"&&(n=String(n)),/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(n)||n==="")throw new TypeError('Invalid character in header field name: "'+n+'"');return n.toLowerCase()}function p(n){return typeof n!="string"&&(n=String(n)),n}function E(n){var a={next:function(){var u=n.shift();return{done:u===void 0,value:u}}};return s.iterable&&(a[Symbol.iterator]=function(){return a}),a}function h(n){this.map={},n instanceof h?n.forEach(function(a,u){this.append(u,a)},this):Array.isArray(n)?n.forEach(function(a){if(a.length!=2)throw new TypeError("Headers constructor: expected name/value pair to be length 2, found"+a.length);this.append(a[0],a[1])},this):n&&Object.getOwnPropertyNames(n).forEach(function(a){this.append(a,n[a])},this)}h.prototype.append=function(n,a){n=d(n),a=p(a);var u=this.map[n];this.map[n]=u?u+", "+a:a},h.prototype.delete=function(n){delete this.map[d(n)]},h.prototype.get=function(n){return n=d(n),this.has(n)?this.map[n]:null},h.prototype.has=function(n){return this.map.hasOwnProperty(d(n))},h.prototype.set=function(n,a){this.map[d(n)]=p(a)},h.prototype.forEach=function(n,a){for(var u in this.map)this.map.hasOwnProperty(u)&&n.call(a,this.map[u],u,this)},h.prototype.keys=function(){var n=[];return this.forEach(function(a,u){n.push(u)}),E(n)},h.prototype.values=function(){var n=[];return this.forEach(function(a){n.push(a)}),E(n)},h.prototype.entries=function(){var n=[];return this.forEach(function(a,u){n.push([u,a])}),E(n)},s.iterable&&(h.prototype[Symbol.iterator]=h.prototype.entries);function b(n){if(!n._noBody){if(n.bodyUsed)return Promise.reject(new TypeError("Already read"));n.bodyUsed=!0}}function m(n){return new Promise(function(a,u){n.onload=function(){a(n.result)},n.onerror=function(){u(n.error)}})}function S(n){var a=new FileReader,u=m(a);return a.readAsArrayBuffer(n),u}function J(n){var a=new FileReader,u=m(a),g=/charset=([A-Za-z0-9_-]+)/.exec(n.type),y=g?g[1]:"utf-8";return a.readAsText(n,y),u}function Y(n){for(var a=new Uint8Array(n),u=new Array(a.length),g=0;g<a.length;g++)u[g]=String.fromCharCode(a[g]);return u.join("")}function q(n){if(n.slice)return n.slice(0);var a=new Uint8Array(n.byteLength);return a.set(new Uint8Array(n)),a.buffer}function V(){return this.bodyUsed=!1,this._initBody=function(n){this.bodyUsed=this.bodyUsed,this._bodyInit=n,n?typeof n=="string"?this._bodyText=n:s.blob&&Blob.prototype.isPrototypeOf(n)?this._bodyBlob=n:s.formData&&FormData.prototype.isPrototypeOf(n)?this._bodyFormData=n:s.searchParams&&URLSearchParams.prototype.isPrototypeOf(n)?this._bodyText=n.toString():s.arrayBuffer&&s.blob&&c(n)?(this._bodyArrayBuffer=q(n.buffer),this._bodyInit=new Blob([this._bodyArrayBuffer])):s.arrayBuffer&&(ArrayBuffer.prototype.isPrototypeOf(n)||l(n))?this._bodyArrayBuffer=q(n):this._bodyText=n=Object.prototype.toString.call(n):(this._noBody=!0,this._bodyText=""),this.headers.get("content-type")||(typeof n=="string"?this.headers.set("content-type","text/plain;charset=UTF-8"):this._bodyBlob&&this._bodyBlob.type?this.headers.set("content-type",this._bodyBlob.type):s.searchParams&&URLSearchParams.prototype.isPrototypeOf(n)&&this.headers.set("content-type","application/x-www-form-urlencoded;charset=UTF-8"))},s.blob&&(this.blob=function(){var n=b(this);if(n)return n;if(this._bodyBlob)return Promise.resolve(this._bodyBlob);if(this._bodyArrayBuffer)return Promise.resolve(new Blob([this._bodyArrayBuffer]));if(this._bodyFormData)throw new Error("could not read FormData body as blob");return Promise.resolve(new Blob([this._bodyText]))}),this.arrayBuffer=function(){if(this._bodyArrayBuffer){var n=b(this);return n||(ArrayBuffer.isView(this._bodyArrayBuffer)?Promise.resolve(this._bodyArrayBuffer.buffer.slice(this._bodyArrayBuffer.byteOffset,this._bodyArrayBuffer.byteOffset+this._bodyArrayBuffer.byteLength)):Promise.resolve(this._bodyArrayBuffer))}else{if(s.blob)return this.blob().then(S);throw new Error("could not read as ArrayBuffer")}},this.text=function(){var n=b(this);if(n)return n;if(this._bodyBlob)return J(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)},s.formData&&(this.formData=function(){return this.text().then(ee)}),this.json=function(){return this.text().then(JSON.parse)},this}var Z=["CONNECT","DELETE","GET","HEAD","OPTIONS","PATCH","POST","PUT","TRACE"];function j(n){var a=n.toUpperCase();return Z.indexOf(a)>-1?a:n}function F(n,a){if(!(this instanceof F))throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.');a=a||{};var u=a.body;if(n instanceof F){if(n.bodyUsed)throw new TypeError("Already read");this.url=n.url,this.credentials=n.credentials,a.headers||(this.headers=new h(n.headers)),this.method=n.method,this.mode=n.mode,this.signal=n.signal,!u&&n._bodyInit!=null&&(u=n._bodyInit,n.bodyUsed=!0)}else this.url=String(n);if(this.credentials=a.credentials||this.credentials||"same-origin",(a.headers||!this.headers)&&(this.headers=new h(a.headers)),this.method=j(a.method||this.method||"GET"),this.mode=a.mode||this.mode||null,this.signal=a.signal||this.signal||(function(){if("AbortController"in i){var f=new AbortController;return f.signal}})(),this.referrer=null,(this.method==="GET"||this.method==="HEAD")&&u)throw new TypeError("Body not allowed for GET or HEAD requests");if(this._initBody(u),(this.method==="GET"||this.method==="HEAD")&&(a.cache==="no-store"||a.cache==="no-cache")){var 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()}}}F.prototype.clone=function(){return new F(this,{body:this._bodyInit})};function ee(n){var a=new FormData;return n.trim().split("&").forEach(function(u){if(u){var g=u.split("="),y=g.shift().replace(/\+/g," "),f=g.join("=").replace(/\+/g," ");a.append(decodeURIComponent(y),decodeURIComponent(f))}}),a}function te(n){var a=new h,u=n.replace(/\r?\n[\t ]+/g," ");return u.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(P){console.warn("Response "+P.message)}}}),a}V.call(F.prototype);function w(n,a){if(!(this instanceof w))throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.');if(a||(a={}),this.type="default",this.status=a.status===void 0?200:a.status,this.status<200||this.status>599)throw new RangeError("Failed to construct 'Response': The status provided (0) is outside the range [200, 599].");this.ok=this.status>=200&&this.status<300,this.statusText=a.statusText===void 0?"":""+a.statusText,this.headers=new h(a.headers),this.url=a.url||"",this._initBody(n)}V.call(w.prototype),w.prototype.clone=function(){return new w(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new h(this.headers),url:this.url})},w.error=function(){var n=new w(null,{status:200,statusText:""});return n.ok=!1,n.status=0,n.type="error",n};var ne=[301,302,303,307,308];w.redirect=function(n,a){if(ne.indexOf(a)===-1)throw new RangeError("Invalid status code");return new w(null,{status:a,headers:{location:n}})},e.DOMException=i.DOMException;try{new e.DOMException}catch{e.DOMException=function(a,u){this.message=a,this.name=u;var g=Error(a);this.stack=g.stack},e.DOMException.prototype=Object.create(Error.prototype),e.DOMException.prototype.constructor=e.DOMException}function A(n,a){return new Promise(function(u,g){var y=new F(n,a);if(y.signal&&y.signal.aborted)return g(new e.DOMException("Aborted","AbortError"));var f=new XMLHttpRequest;function D(){f.abort()}f.onload=function(){var v={statusText:f.statusText,headers:te(f.getAllResponseHeaders()||"")};y.url.indexOf("file://")===0&&(f.status<200||f.status>599)?v.status=200:v.status=f.status,v.url="responseURL"in f?f.responseURL:v.headers.get("X-Request-URL");var x="response"in f?f.response:f.responseText;setTimeout(function(){u(new w(x,v))},0)},f.onerror=function(){setTimeout(function(){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 e.DOMException("Aborted","AbortError"))},0)};function P(v){try{return v===""&&i.location.href?i.location.href:v}catch{return v}}if(f.open(y.method,P(y.url),!0),y.credentials==="include"?f.withCredentials=!0:y.credentials==="omit"&&(f.withCredentials=!1),"responseType"in f&&(s.blob?f.responseType="blob":s.arrayBuffer&&(f.responseType="arraybuffer")),a&&typeof a.headers=="object"&&!(a.headers instanceof h||i.Headers&&a.headers instanceof i.Headers)){var W=[];Object.getOwnPropertyNames(a.headers).forEach(function(v){W.push(d(v)),f.setRequestHeader(v,p(a.headers[v]))}),y.headers.forEach(function(v,x){W.indexOf(x)===-1&&f.setRequestHeader(x,v)})}else y.headers.forEach(function(v,x){f.setRequestHeader(x,v)});y.signal&&(y.signal.addEventListener("abort",D),f.onreadystatechange=function(){f.readyState===4&&y.signal.removeEventListener("abort",D)}),f.send(typeof y._bodyInit>"u"?null:y._bodyInit)})}return A.polyfill=!0,i.fetch||(i.fetch=A,i.Headers=h,i.Request=F,i.Response=w),e.Headers=h,e.Request=F,e.Response=w,e.fetch=A,e})({})})(typeof self<"u"?self:z)});var k=[];for(let r=0;r<256;++r)k.push((r+256).toString(16).slice(1));function H(r,t=0){return(k[r[t+0]]+k[r[t+1]]+k[r[t+2]]+k[r[t+3]]+"-"+k[r[t+4]]+k[r[t+5]]+"-"+k[r[t+6]]+k[r[t+7]]+"-"+k[r[t+8]]+k[r[t+9]]+"-"+k[r[t+10]]+k[r[t+11]]+k[r[t+12]]+k[r[t+13]]+k[r[t+14]]+k[r[t+15]]).toLowerCase()}var L,de=new Uint8Array(16);function B(){if(!L){if(typeof crypto>"u"||!crypto.getRandomValues)throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");L=crypto.getRandomValues.bind(crypto)}return L(de)}var fe=typeof crypto<"u"&&crypto.randomUUID&&crypto.randomUUID.bind(crypto),N={randomUUID:fe};function he(r,t,e){r=r||{};let i=r.random??r.rng?.()??B();if(i.length<16)throw new Error("Random bytes length must be >= 16");if(i[6]=i[6]&15|64,i[8]=i[8]&63|128,t){if(e=e||0,e<0||e+16>t.length)throw new RangeError(`UUID byte range ${e}:${e+15} is out of buffer bounds`);for(let s=0;s<16;++s)t[e+s]=i[s];return t}return H(i)}function ge(r,t,e){return N.randomUUID&&!t&&!r?N.randomUUID():he(r,t,e)}var C=ge;var st=ue(K());function T(r){return pe(r,!1)}function pe(r,t){return r==null?r:{companyId:r.company_id==null?void 0:r.company_id,error:r.error==null?void 0:r.error,featureAllocation:r.feature_allocation==null?void 0:r.feature_allocation,featureUsage:r.feature_usage==null?void 0:r.feature_usage,featureUsageEvent:r.feature_usage_event==null?void 0:r.feature_usage_event,featureUsagePeriod:r.feature_usage_period==null?void 0:r.feature_usage_period,featureUsageResetAt:r.feature_usage_reset_at==null?void 0:new Date(r.feature_usage_reset_at),flag:r.flag,flagId:r.flag_id==null?void 0:r.flag_id,reason:r.reason,ruleId:r.rule_id==null?void 0:r.rule_id,ruleType:r.rule_type==null?void 0:r.rule_type,userId:r.user_id==null?void 0:r.user_id,value:r.value}}function M(r){return ye(r,!1)}function ye(r,t=!1){return r==null?r:{company_id:r.companyId,error:r.error,flag_id:r.flagId,flag_key:r.flagKey,reason:r.reason,req_company:r.reqCompany,req_user:r.reqUser,rule_id:r.ruleId,user_id:r.userId,value:r.value}}function U(r){return be(r,!1)}function be(r,t){return r==null?r:{data:T(r.data),params:r.params}}function X(r){return ve(r,!1)}function ve(r,t){return r==null?r:{flags:r.flags.map(T)}}function $(r){return ke(r,!1)}function ke(r,t){return r==null?r:{data:X(r.data),params:r.params}}var _=r=>{let{companyId:t,error:e,featureAllocation:i,featureUsage:s,featureUsageEvent:c,featureUsagePeriod:o,featureUsageResetAt:l,flag:d,flagId:p,reason:E,ruleId:h,ruleType:b,userId:m,value:S}=T(r);return{featureUsageExceeded:!S&&(b=="company_override_usage_exceeded"||b=="plan_entitlement_usage_exceeded"),companyId:t??void 0,error:e??void 0,featureAllocation:i??void 0,featureUsage:s??void 0,featureUsageEvent:c===null?void 0:c,featureUsagePeriod:o??void 0,featureUsageResetAt:l??void 0,flag:d,flagId:p??void 0,reason:E,ruleId:h??void 0,ruleType:b??void 0,userId:m??void 0,value:S}};function R(r){let t=Object.keys(r).reduce((e,i)=>{let c=Object.keys(r[i]||{}).sort().reduce((o,l)=>(o[l]=r[i][l],o),{});return e[i]=c,e},{});return JSON.stringify(t)}var O="1.2.8";var G="schematicId";var I=class{additionalHeaders={};apiKey;apiUrl="https://api.schematichq.com";conn=null;context={};debugEnabled=!1;offlineEnabled=!1;eventQueue;contextDependentEventQueue;eventUrl="https://c.schematichq.com";flagCheckListeners={};flagValueListeners={};isPending=!0;isPendingListeners=new Set;storage;useWebSocket=!1;checks={};featureUsageEventMap={};webSocketUrl="wss://api.schematichq.com";webSocketConnectionTimeout=1e4;webSocketReconnect=!0;webSocketMaxReconnectAttempts=7;webSocketInitialRetryDelay=1e3;webSocketMaxRetryDelay=3e4;wsReconnectAttempts=0;wsReconnectTimer=null;wsIntentionalDisconnect=!1;maxEventQueueSize=100;maxEventRetries=5;eventRetryInitialDelay=1e3;eventRetryMaxDelay=3e4;retryTimer=null;constructor(t,e){if(this.apiKey=t,this.eventQueue=[],this.contextDependentEventQueue=[],this.useWebSocket=e?.useWebSocket??!1,this.debugEnabled=e?.debug??!1,this.offlineEnabled=e?.offline??!1,typeof window<"u"&&typeof window.location<"u"){let i=new URLSearchParams(window.location.search),s=i.get("schematic_debug");s!==null&&(s===""||s==="true"||s==="1")&&(this.debugEnabled=!0);let c=i.get("schematic_offline");c!==null&&(c===""||c==="true"||c==="1")&&(this.offlineEnabled=!0,this.debugEnabled=!0)}this.offlineEnabled&&e?.debug!==!1&&(this.debugEnabled=!0),this.offlineEnabled&&this.setIsPending(!1),this.additionalHeaders={"X-Schematic-Client-Version":`schematic-js@${O}`,...e?.additionalHeaders??{}},e?.storage?this.storage=e.storage:typeof localStorage<"u"&&(this.storage=localStorage),e?.apiUrl!==void 0&&(this.apiUrl=e.apiUrl),e?.eventUrl!==void 0&&(this.eventUrl=e.eventUrl),e?.webSocketUrl!==void 0&&(this.webSocketUrl=e.webSocketUrl),e?.webSocketConnectionTimeout!==void 0&&(this.webSocketConnectionTimeout=e.webSocketConnectionTimeout),e?.webSocketReconnect!==void 0&&(this.webSocketReconnect=e.webSocketReconnect),e?.webSocketMaxReconnectAttempts!==void 0&&(this.webSocketMaxReconnectAttempts=e.webSocketMaxReconnectAttempts),e?.webSocketInitialRetryDelay!==void 0&&(this.webSocketInitialRetryDelay=e.webSocketInitialRetryDelay),e?.webSocketMaxRetryDelay!==void 0&&(this.webSocketMaxRetryDelay=e.webSocketMaxRetryDelay),e?.maxEventQueueSize!==void 0&&(this.maxEventQueueSize=e.maxEventQueueSize),e?.maxEventRetries!==void 0&&(this.maxEventRetries=e.maxEventRetries),e?.eventRetryInitialDelay!==void 0&&(this.eventRetryInitialDelay=e.eventRetryInitialDelay),e?.eventRetryMaxDelay!==void 0&&(this.eventRetryMaxDelay=e.eventRetryMaxDelay),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")}async checkFlag(t){let{fallback:e=!1,key:i}=t,s=t.context||this.context,c=R(s);if(this.debug(`checkFlag: ${i}`,{context:s,fallback:e}),this.isOffline())return this.debug(`checkFlag offline result: ${i}`,{value:e,offlineMode:!0}),e;if(!this.useWebSocket){let o=`${this.apiUrl}/flags/${i}/check`;return fetch(o,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8","X-Schematic-Api-Key":this.apiKey},body:JSON.stringify(s)}).then(l=>{if(!l.ok)throw new Error("Network response was not ok");return l.json()}).then(l=>{let d=U(l);this.debug(`checkFlag result: ${i}`,d);let p=_(d.data);return typeof p.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(p),this.submitFlagCheckEvent(i,p,s),p.value}).catch(l=>{console.error("There was a problem with the fetch operation:",l);let d={flag:i,value:e,reason:"API request failed",error:l instanceof Error?l.message:String(l)};return this.submitFlagCheckEvent(i,d,s),e})}try{let o=this.checks[c];if(this.conn!==null&&typeof o<"u"&&typeof o[i]<"u")return this.debug(`checkFlag cached result: ${i}`,o[i]),o[i].value;if(this.isOffline())return e;try{await this.setContext(s)}catch(E){return console.error("WebSocket connection failed, falling back to REST:",E),this.fallbackToRest(i,s,e)}let d=(this.checks[c]??{})[i],p=d?.value??e;return this.debug(`checkFlag WebSocket result: ${i}`,typeof d<"u"?d:{value:e,fallbackUsed:!0}),typeof d<"u"&&this.submitFlagCheckEvent(i,d,s),p}catch(o){console.error("Unexpected error in checkFlag:",o);let l={flag:i,value:e,reason:"Unexpected error in flag check",error:o instanceof Error?o.message:String(o)};return this.submitFlagCheckEvent(i,l,s),e}}debug(t,...e){this.debugEnabled&&console.log(`[Schematic] ${t}`,...e)}isOffline(){return this.offlineEnabled}submitFlagCheckEvent(t,e,i){let s={flagKey:t,value:e.value,reason:e.reason,flagId:e.flagId,ruleId:e.ruleId,companyId:e.companyId,userId:e.userId,error:e.error,reqCompany:i.company,reqUser:i.user};return this.debug("submitting flag check event:",s),this.handleEvent("flag_check",M(s))}async fallbackToRest(t,e,i){if(this.isOffline())return this.debug(`fallbackToRest offline result: ${t}`,{value:i,offlineMode:!0}),i;try{let s=`${this.apiUrl}/flags/${t}/check`,c=await fetch(s,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8","X-Schematic-Api-Key":this.apiKey},body:JSON.stringify(e)});if(!c.ok)throw new Error("Network response was not ok");let o=await c.json(),l=U(o);this.debug(`fallbackToRest result: ${t}`,l);let d=_(l.data);return typeof d.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(d),this.submitFlagCheckEvent(t,d,e),d.value}catch(s){console.error("REST API call failed, using fallback value:",s);let c={flag:t,value:i,reason:"API request failed (fallback)",error:s instanceof Error?s.message:String(s)};return this.submitFlagCheckEvent(t,c,e),i}}checkFlags=async t=>{if(t=t||this.context,this.debug("checkFlags",{context:t}),this.isOffline())return this.debug("checkFlags offline result: returning empty object"),{};let e=`${this.apiUrl}/flags/check`,i=JSON.stringify(t);return fetch(e,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8","X-Schematic-Api-Key":this.apiKey},body:i}).then(s=>{if(!s.ok)throw new Error("Network response was not ok");return s.json()}).then(s=>{let c=$(s);return this.debug("checkFlags result:",c),(c?.data?.flags??[]).reduce((o,l)=>(o[l.flag]=l.value,o),{})}).catch(s=>(console.error("There was a problem with the fetch operation:",s),{}))};identify=t=>{this.debug("identify:",t);try{this.setContext({company:t.company?.keys,user:t.keys})}catch(e){console.error("Error setting context:",e)}return this.handleEvent("identify",t)};setContext=async t=>{if(this.isOffline()||!this.useWebSocket)return this.context=t,this.flushContextDependentEventQueue(),this.setIsPending(!1),Promise.resolve();try{this.setIsPending(!0),this.conn||(this.wsReconnectTimer!==null&&(this.debug("Cancelling scheduled reconnection, connecting immediately"),clearTimeout(this.wsReconnectTimer),this.wsReconnectTimer=null),this.conn=this.wsConnect());let e=await this.conn;await this.wsSendMessage(e,t)}catch(e){throw console.error("Failed to establish WebSocket connection:",e),e}};track=t=>{let{company:e,user:i,event:s,traits:c,quantity:o=1}=t;if(!this.hasContext(e,i)){this.debug(`track: queuing event "${s}" until context is available`);let d={api_key:this.apiKey,body:{company:e,event:s,traits:c??{},user:i,quantity:o},sent_at:new Date().toISOString(),tracker_event_id:C(),tracker_user_id:this.getAnonymousId(),type:"track"};return this.contextDependentEventQueue.push(d),Promise.resolve()}let l={company:e??this.context.company,event:s,traits:c??{},user:i??this.context.user,quantity:o};return this.debug("track:",l),s in this.featureUsageEventMap&&this.optimisticallyUpdateFeatureUsage(s,o),this.handleEvent("track",l)};optimisticallyUpdateFeatureUsage=(t,e=1)=>{let i=this.featureUsageEventMap[t];i!=null&&(this.debug(`Optimistically updating feature usage for event: ${t}`,{quantity:e}),Object.entries(i).forEach(([s,c])=>{if(c===void 0)return;let o={...c};if(typeof o.featureUsage=="number"){if(o.featureUsage+=e,typeof o.featureAllocation=="number"){let d=o.featureUsageExceeded===!0,p=o.featureUsage>=o.featureAllocation;p!==d&&(o.featureUsageExceeded=p,p&&(o.value=!1),this.debug(`Usage limit status changed for flag: ${s}`,{was:d?"exceeded":"within limits",now:p?"exceeded":"within limits",featureUsage:o.featureUsage,featureAllocation:o.featureAllocation,value:o.value}))}this.featureUsageEventMap[t]!==void 0&&(this.featureUsageEventMap[t][s]=o);let l=R(this.context);this.checks[l]!==void 0&&this.checks[l]!==null&&(this.checks[l][s]=o),this.notifyFlagCheckListeners(s,o),this.notifyFlagValueListeners(s,o.value)}}))};hasContext=(t,e)=>{let i=t!=null&&Object.keys(t).length>0||e!=null&&Object.keys(e).length>0,s=this.context.company!==void 0&&this.context.company!==null&&Object.keys(this.context.company).length>0||this.context.user!==void 0&&this.context.user!==null&&Object.keys(this.context.user).length>0;return i||s};flushContextDependentEventQueue=()=>{for(this.debug(`flushing ${this.contextDependentEventQueue.length} context-dependent events`);this.contextDependentEventQueue.length>0;){let t=this.contextDependentEventQueue.shift();if(t)if(t.type==="track"&&typeof t.body=="object"&&t.body!==null){let e=t.body,i={...e,company:e.company??this.context.company,user:e.user??this.context.user},s={...t,body:i,sent_at:new Date().toISOString()};this.sendEvent(s)}else this.sendEvent(t)}};startRetryTimer=()=>{this.retryTimer===null&&(this.retryTimer=setInterval(()=>{this.flushEventQueue().catch(t=>{this.debug("Error in retry timer flush:",t)}),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 t=Date.now(),e=[],i=[];for(let s of this.eventQueue)s.next_retry_at===void 0||s.next_retry_at<=t?e.push(s):i.push(s);if(e.length===0){this.debug(`No events ready for retry yet (${i.length} still in backoff)`);return}this.debug(`Flushing event queue: ${e.length} ready, ${i.length} waiting`),this.eventQueue=i;for(let s of e)try{await this.sendEvent(s),this.debug("Queued event sent successfully:",s.type)}catch(c){this.debug("Failed to send queued event:",c)}};getAnonymousId=()=>{if(!this.storage)return C();let t=this.storage.getItem(G);if(typeof t<"u")return t;let e=C();return this.storage.setItem(G,e),e};handleEvent=(t,e)=>{let i={api_key:this.apiKey,body:e,sent_at:new Date().toISOString(),tracker_event_id:C(),tracker_user_id:this.getAnonymousId(),type:t};return typeof document<"u"&&document?.hidden?this.storeEvent(i):this.sendEvent(i)};sendEvent=async t=>{let e=`${this.eventUrl}/e`,i=JSON.stringify(t);if(this.debug("sending event:",{url:e,event:t}),this.isOffline())return this.debug("event not sent (offline mode):",{event:t}),Promise.resolve();try{let s=await fetch(e,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8"},body:i});if(!s.ok)throw new Error(`HTTP ${s.status}: ${s.statusText}`);this.debug("event sent:",{status:s.status,statusText:s.statusText})}catch(s){let c=(t.retry_count??0)+1;if(c<=this.maxEventRetries){this.debug(`Event failed to send (attempt ${c}/${this.maxEventRetries}), queueing for retry:`,s);let o=this.eventRetryInitialDelay*Math.pow(2,c-1),l=Math.min(o,this.eventRetryMaxDelay),d=Date.now()+l,p={...t,retry_count:c,next_retry_at:d};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:`,s)}return Promise.resolve()};storeEvent=t=>(this.eventQueue.push(t),Promise.resolve());cleanup=async()=>{if(this.isOffline())return this.debug("cleanup: skipped (offline mode)"),Promise.resolve();if(this.wsIntentionalDisconnect=!0,this.wsReconnectTimer!==null&&(clearTimeout(this.wsReconnectTimer),this.wsReconnectTimer=null),this.stopRetryTimer(),this.conn)try{(await this.conn).close()}catch(t){console.error("Error during cleanup:",t)}finally{this.conn=null}};calculateReconnectDelay=()=>{let t=this.webSocketInitialRetryDelay*Math.pow(2,this.wsReconnectAttempts),e=Math.min(t,this.webSocketMaxRetryDelay),i=Math.random()*e*.5,s=e+i;return this.debug(`Reconnect delay calculated: ${s.toFixed(0)}ms (attempt ${this.wsReconnectAttempts+1}/${this.webSocketMaxReconnectAttempts})`),s};handleNetworkOffline=async()=>{if(this.conn!==null){try{(await this.conn).close()}catch(t){this.debug("Error closing connection on offline:",t)}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(t=>{this.debug("Error flushing event queue on network online:",t)}),this.attemptReconnect()};attemptReconnect=()=>{if(this.wsReconnectAttempts>=this.webSocketMaxReconnectAttempts){this.debug(`Maximum reconnection attempts (${this.webSocketMaxReconnectAttempts}) reached, giving up`);return}this.wsReconnectTimer!==null&&clearTimeout(this.wsReconnectTimer);let t=this.calculateReconnectDelay();this.debug(`Scheduling reconnection attempt ${this.wsReconnectAttempts+1}/${this.webSocketMaxReconnectAttempts} in ${t.toFixed(0)}ms`),this.wsReconnectTimer=setTimeout(async()=>{this.wsReconnectTimer=null,this.wsReconnectAttempts++,this.debug(`Attempting to reconnect (attempt ${this.wsReconnectAttempts}/${this.webSocketMaxReconnectAttempts})`);try{this.conn=this.wsConnect();let e=await this.conn;this.debug("Reconnection context check:",{hasCompany:this.context.company!==void 0,hasUser:this.context.user!==void 0,context:this.context}),this.context.company!==void 0||this.context.user!==void 0?(this.debug("Reconnected, force re-sending context"),await this.wsSendContextAfterReconnection(e,this.context)):this.debug("No context to re-send after reconnection - websocket ready for new context"),this.flushEventQueue().catch(i=>{this.debug("Error flushing event queue after websocket reconnection:",i)}),this.debug("Reconnection successful")}catch(e){this.debug("Reconnection attempt failed:",e)}},t)};wsConnect=()=>this.isOffline()?(this.debug("wsConnect: skipped (offline mode)"),Promise.reject(new Error("WebSocket connection skipped in offline mode"))):new Promise((t,e)=>{let i=`${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;this.debug("connecting to WebSocket:",i);let s=new WebSocket(i),c=null,o=!1;c=setTimeout(()=>{o||(this.debug(`WebSocket connection timeout after ${this.webSocketConnectionTimeout}ms`),s.close(),e(new Error("WebSocket connection timeout")))},this.webSocketConnectionTimeout),s.onopen=()=>{o=!0,c!==null&&clearTimeout(c),this.wsReconnectAttempts=0,this.wsIntentionalDisconnect=!1,this.debug("WebSocket connection opened"),t(s)},s.onerror=l=>{o=!0,c!==null&&clearTimeout(c),this.debug("WebSocket connection error:",l),e(l)},s.onclose=()=>{o=!0,c!==null&&clearTimeout(c),this.debug("WebSocket connection closed"),this.conn=null,!this.wsIntentionalDisconnect&&this.webSocketReconnect&&this.attemptReconnect()}});wsSendContextAfterReconnection=(t,e)=>this.isOffline()?(this.debug("wsSendContextAfterReconnection: skipped (offline mode)"),this.setIsPending(!1),Promise.resolve()):new Promise(i=>{this.debug("WebSocket force sending context after reconnection:",e),this.context=e;let s=()=>{let c=!1,o=p=>{let E=JSON.parse(p.data);this.debug("WebSocket message received after reconnection:",E),R(e)in this.checks||(this.checks[R(e)]={}),(E.flags??[]).forEach(h=>{let b=_(h),m=R(e);this.checks[m]===void 0&&(this.checks[m]={}),this.checks[m][b.flag]=b}),this.useWebSocket=!0,t.removeEventListener("message",o),c||(c=!0,i(this.setIsPending(!1)))};t.addEventListener("message",o);let l=this.additionalHeaders["X-Schematic-Client-Version"]??`schematic-js@${O}`,d={apiKey:this.apiKey,clientVersion:l,data:e};this.debug("WebSocket sending forced message after reconnection:",d),t.send(JSON.stringify(d))};t.readyState===WebSocket.OPEN?(this.debug("WebSocket already open, sending forced message after reconnection"),s()):t.addEventListener("open",()=>{this.debug("WebSocket opened, sending forced message after reconnection"),s()})});wsSendMessage=(t,e)=>this.isOffline()?(this.debug("wsSendMessage: skipped (offline mode)"),this.setIsPending(!1),Promise.resolve()):new Promise((i,s)=>{if(R(e)==R(this.context))return this.debug("WebSocket context unchanged, skipping update"),i(this.setIsPending(!1));this.debug("WebSocket context updated:",e),this.context=e;let c=()=>{let o=!1,l=E=>{let h=JSON.parse(E.data);this.debug("WebSocket message received:",h),R(e)in this.checks||(this.checks[R(e)]={}),(h.flags??[]).forEach(b=>{let m=_(b),S=R(e);this.checks[S]===void 0&&(this.checks[S]={}),this.checks[S][m.flag]=m,this.debug("WebSocket flag update:",{flag:m.flag,value:m.value,flagCheck:m}),typeof m.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(m),(this.flagCheckListeners[b.flag]?.size>0||this.flagValueListeners[b.flag]?.size>0)&&this.submitFlagCheckEvent(m.flag,m,e),this.notifyFlagCheckListeners(b.flag,m),this.notifyFlagValueListeners(b.flag,m.value)}),this.flushContextDependentEventQueue(),this.setIsPending(!1),o||(o=!0,i())};t.addEventListener("message",l);let d=this.additionalHeaders["X-Schematic-Client-Version"]??`schematic-js@${O}`,p={apiKey:this.apiKey,clientVersion:d,data:e};this.debug("WebSocket sending message:",p),t.send(JSON.stringify(p))};t.readyState===WebSocket.OPEN?(this.debug("WebSocket already open, sending message"),c()):t.readyState===WebSocket.CONNECTING?(this.debug("WebSocket connecting, waiting for open to send message"),t.addEventListener("open",c)):(this.debug("WebSocket is closed, cannot send message"),s("WebSocket is not open or connecting"))});getIsPending=()=>this.isPending;addIsPendingListener=t=>(this.isPendingListeners.add(t),()=>{this.isPendingListeners.delete(t)});setIsPending=t=>{this.isPending=t,this.isPendingListeners.forEach(e=>Ee(e,t))};getFlagCheck=t=>{let e=R(this.context);return(this.checks[e]??{})[t]};getFlagValue=t=>this.getFlagCheck(t)?.value;addFlagValueListener=(t,e)=>(t in this.flagValueListeners||(this.flagValueListeners[t]=new Set),this.flagValueListeners[t].add(e),()=>{this.flagValueListeners[t].delete(e)});addFlagCheckListener=(t,e)=>(t in this.flagCheckListeners||(this.flagCheckListeners[t]=new Set),this.flagCheckListeners[t].add(e),()=>{this.flagCheckListeners[t].delete(e)});notifyFlagCheckListeners=(t,e)=>{let i=this.flagCheckListeners?.[t]??[];i.size>0&&this.debug(`Notifying ${i.size} flag check listeners for ${t}`,e),typeof e.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(e),i.forEach(s=>Re(s,e))};updateFeatureUsageEventMap=t=>{if(typeof t.featureUsageEvent!="string")return;let e=t.featureUsageEvent;(this.featureUsageEventMap[e]===void 0||this.featureUsageEventMap[e]===null)&&(this.featureUsageEventMap[e]={}),this.featureUsageEventMap[e]!==void 0&&(this.featureUsageEventMap[e][t.flag]=t),this.debug(`Updated featureUsageEventMap for event: ${e}, flag: ${t.flag}`,t)};notifyFlagValueListeners=(t,e)=>{let i=this.flagValueListeners?.[t]??[];i.size>0&&this.debug(`Notifying ${i.size} flag value listeners for ${t}`,{value:e}),i.forEach(s=>we(s,e))}},Ee=(r,t)=>{r.length>0?r(t):r()},Re=(r,t)=>{r.length>0?r(t):r()},we=(r,t)=>{r.length>0?r(t):r()};window.Schematic=I;})();
|
|
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.8";
|
|
804
804
|
|
|
805
805
|
// src/index.ts
|
|
806
806
|
var anonymousIdKey = "schematicId";
|
|
@@ -832,6 +832,15 @@ var Schematic = class {
|
|
|
832
832
|
wsReconnectAttempts = 0;
|
|
833
833
|
wsReconnectTimer = null;
|
|
834
834
|
wsIntentionalDisconnect = false;
|
|
835
|
+
maxEventQueueSize = 100;
|
|
836
|
+
// Prevent memory issues with very long network outages
|
|
837
|
+
maxEventRetries = 5;
|
|
838
|
+
// Maximum retry attempts for failed events
|
|
839
|
+
eventRetryInitialDelay = 1e3;
|
|
840
|
+
// Initial retry delay in ms
|
|
841
|
+
eventRetryMaxDelay = 3e4;
|
|
842
|
+
// Maximum retry delay in ms
|
|
843
|
+
retryTimer = null;
|
|
835
844
|
constructor(apiKey, options) {
|
|
836
845
|
this.apiKey = apiKey;
|
|
837
846
|
this.eventQueue = [];
|
|
@@ -890,6 +899,18 @@ var Schematic = class {
|
|
|
890
899
|
if (options?.webSocketMaxRetryDelay !== void 0) {
|
|
891
900
|
this.webSocketMaxRetryDelay = options.webSocketMaxRetryDelay;
|
|
892
901
|
}
|
|
902
|
+
if (options?.maxEventQueueSize !== void 0) {
|
|
903
|
+
this.maxEventQueueSize = options.maxEventQueueSize;
|
|
904
|
+
}
|
|
905
|
+
if (options?.maxEventRetries !== void 0) {
|
|
906
|
+
this.maxEventRetries = options.maxEventRetries;
|
|
907
|
+
}
|
|
908
|
+
if (options?.eventRetryInitialDelay !== void 0) {
|
|
909
|
+
this.eventRetryInitialDelay = options.eventRetryInitialDelay;
|
|
910
|
+
}
|
|
911
|
+
if (options?.eventRetryMaxDelay !== void 0) {
|
|
912
|
+
this.eventRetryMaxDelay = options.eventRetryMaxDelay;
|
|
913
|
+
}
|
|
893
914
|
if (typeof window !== "undefined" && window?.addEventListener) {
|
|
894
915
|
window.addEventListener("beforeunload", () => {
|
|
895
916
|
this.flushEventQueue();
|
|
@@ -1305,11 +1326,53 @@ var Schematic = class {
|
|
|
1305
1326
|
}
|
|
1306
1327
|
}
|
|
1307
1328
|
};
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1329
|
+
startRetryTimer = () => {
|
|
1330
|
+
if (this.retryTimer !== null) {
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
this.retryTimer = setInterval(() => {
|
|
1334
|
+
this.flushEventQueue().catch((error) => {
|
|
1335
|
+
this.debug("Error in retry timer flush:", error);
|
|
1336
|
+
});
|
|
1337
|
+
if (this.eventQueue.length === 0) {
|
|
1338
|
+
this.stopRetryTimer();
|
|
1339
|
+
}
|
|
1340
|
+
}, 5e3);
|
|
1341
|
+
this.debug("Started retry timer");
|
|
1342
|
+
};
|
|
1343
|
+
stopRetryTimer = () => {
|
|
1344
|
+
if (this.retryTimer !== null) {
|
|
1345
|
+
clearInterval(this.retryTimer);
|
|
1346
|
+
this.retryTimer = null;
|
|
1347
|
+
this.debug("Stopped retry timer");
|
|
1348
|
+
}
|
|
1349
|
+
};
|
|
1350
|
+
flushEventQueue = async () => {
|
|
1351
|
+
if (this.eventQueue.length === 0) {
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
const now = Date.now();
|
|
1355
|
+
const readyEvents = [];
|
|
1356
|
+
const notReadyEvents = [];
|
|
1357
|
+
for (const event of this.eventQueue) {
|
|
1358
|
+
if (event.next_retry_at === void 0 || event.next_retry_at <= now) {
|
|
1359
|
+
readyEvents.push(event);
|
|
1360
|
+
} else {
|
|
1361
|
+
notReadyEvents.push(event);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
if (readyEvents.length === 0) {
|
|
1365
|
+
this.debug(`No events ready for retry yet (${notReadyEvents.length} still in backoff)`);
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
this.debug(`Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`);
|
|
1369
|
+
this.eventQueue = notReadyEvents;
|
|
1370
|
+
for (const event of readyEvents) {
|
|
1371
|
+
try {
|
|
1372
|
+
await this.sendEvent(event);
|
|
1373
|
+
this.debug(`Queued event sent successfully:`, event.type);
|
|
1374
|
+
} catch (error) {
|
|
1375
|
+
this.debug(`Failed to send queued event:`, error);
|
|
1313
1376
|
}
|
|
1314
1377
|
}
|
|
1315
1378
|
};
|
|
@@ -1357,12 +1420,37 @@ var Schematic = class {
|
|
|
1357
1420
|
},
|
|
1358
1421
|
body: payload
|
|
1359
1422
|
});
|
|
1423
|
+
if (!response.ok) {
|
|
1424
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1425
|
+
}
|
|
1360
1426
|
this.debug(`event sent:`, {
|
|
1361
1427
|
status: response.status,
|
|
1362
1428
|
statusText: response.statusText
|
|
1363
1429
|
});
|
|
1364
1430
|
} catch (error) {
|
|
1365
|
-
|
|
1431
|
+
const retryCount = (event.retry_count ?? 0) + 1;
|
|
1432
|
+
if (retryCount <= this.maxEventRetries) {
|
|
1433
|
+
this.debug(`Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`, error);
|
|
1434
|
+
const baseDelay = this.eventRetryInitialDelay * Math.pow(2, retryCount - 1);
|
|
1435
|
+
const jitterDelay = Math.min(baseDelay, this.eventRetryMaxDelay);
|
|
1436
|
+
const nextRetryAt = Date.now() + jitterDelay;
|
|
1437
|
+
const retryEvent = {
|
|
1438
|
+
...event,
|
|
1439
|
+
retry_count: retryCount,
|
|
1440
|
+
next_retry_at: nextRetryAt
|
|
1441
|
+
};
|
|
1442
|
+
if (this.eventQueue.length < this.maxEventQueueSize) {
|
|
1443
|
+
this.eventQueue.push(retryEvent);
|
|
1444
|
+
this.debug(`Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`);
|
|
1445
|
+
} else {
|
|
1446
|
+
this.debug(`Event queue full (${this.maxEventQueueSize}), dropping oldest event`);
|
|
1447
|
+
this.eventQueue.shift();
|
|
1448
|
+
this.eventQueue.push(retryEvent);
|
|
1449
|
+
}
|
|
1450
|
+
this.startRetryTimer();
|
|
1451
|
+
} else {
|
|
1452
|
+
this.debug(`Event failed permanently after ${this.maxEventRetries} attempts, dropping:`, error);
|
|
1453
|
+
}
|
|
1366
1454
|
}
|
|
1367
1455
|
return Promise.resolve();
|
|
1368
1456
|
};
|
|
@@ -1387,6 +1475,7 @@ var Schematic = class {
|
|
|
1387
1475
|
clearTimeout(this.wsReconnectTimer);
|
|
1388
1476
|
this.wsReconnectTimer = null;
|
|
1389
1477
|
}
|
|
1478
|
+
this.stopRetryTimer();
|
|
1390
1479
|
if (this.conn) {
|
|
1391
1480
|
try {
|
|
1392
1481
|
const socket = await this.conn;
|
|
@@ -1434,16 +1523,15 @@ var Schematic = class {
|
|
|
1434
1523
|
* Handle browser coming back online
|
|
1435
1524
|
*/
|
|
1436
1525
|
handleNetworkOnline = () => {
|
|
1437
|
-
|
|
1438
|
-
this.debug("No context set, skipping reconnection");
|
|
1439
|
-
return;
|
|
1440
|
-
}
|
|
1526
|
+
this.debug("Network online, attempting reconnection and flushing queued events");
|
|
1441
1527
|
this.wsReconnectAttempts = 0;
|
|
1442
1528
|
if (this.wsReconnectTimer !== null) {
|
|
1443
1529
|
clearTimeout(this.wsReconnectTimer);
|
|
1444
1530
|
this.wsReconnectTimer = null;
|
|
1445
1531
|
}
|
|
1446
|
-
this.
|
|
1532
|
+
this.flushEventQueue().catch((error) => {
|
|
1533
|
+
this.debug("Error flushing event queue on network online:", error);
|
|
1534
|
+
});
|
|
1447
1535
|
this.attemptReconnect();
|
|
1448
1536
|
};
|
|
1449
1537
|
/**
|
|
@@ -1473,10 +1561,20 @@ var Schematic = class {
|
|
|
1473
1561
|
try {
|
|
1474
1562
|
this.conn = this.wsConnect();
|
|
1475
1563
|
const socket = await this.conn;
|
|
1564
|
+
this.debug(`Reconnection context check:`, {
|
|
1565
|
+
hasCompany: this.context.company !== void 0,
|
|
1566
|
+
hasUser: this.context.user !== void 0,
|
|
1567
|
+
context: this.context
|
|
1568
|
+
});
|
|
1476
1569
|
if (this.context.company !== void 0 || this.context.user !== void 0) {
|
|
1477
|
-
this.debug(`Reconnected, re-sending context`);
|
|
1478
|
-
await this.
|
|
1570
|
+
this.debug(`Reconnected, force re-sending context`);
|
|
1571
|
+
await this.wsSendContextAfterReconnection(socket, this.context);
|
|
1572
|
+
} else {
|
|
1573
|
+
this.debug(`No context to re-send after reconnection - websocket ready for new context`);
|
|
1479
1574
|
}
|
|
1575
|
+
this.flushEventQueue().catch((error) => {
|
|
1576
|
+
this.debug("Error flushing event queue after websocket reconnection:", error);
|
|
1577
|
+
});
|
|
1480
1578
|
this.debug(`Reconnection successful`);
|
|
1481
1579
|
} catch (error) {
|
|
1482
1580
|
this.debug(`Reconnection attempt failed:`, error);
|
|
@@ -1537,6 +1635,61 @@ var Schematic = class {
|
|
|
1537
1635
|
};
|
|
1538
1636
|
});
|
|
1539
1637
|
};
|
|
1638
|
+
// Send a message on the websocket after reconnection, forcing the send even if context appears unchanged
|
|
1639
|
+
// because the server has lost all state and needs the initial context
|
|
1640
|
+
wsSendContextAfterReconnection = (socket, context) => {
|
|
1641
|
+
if (this.isOffline()) {
|
|
1642
|
+
this.debug("wsSendContextAfterReconnection: skipped (offline mode)");
|
|
1643
|
+
this.setIsPending(false);
|
|
1644
|
+
return Promise.resolve();
|
|
1645
|
+
}
|
|
1646
|
+
return new Promise((resolve) => {
|
|
1647
|
+
this.debug(`WebSocket force sending context after reconnection:`, context);
|
|
1648
|
+
this.context = context;
|
|
1649
|
+
const sendMessage = () => {
|
|
1650
|
+
let resolved = false;
|
|
1651
|
+
const messageHandler = (event) => {
|
|
1652
|
+
const message = JSON.parse(event.data);
|
|
1653
|
+
this.debug(`WebSocket message received after reconnection:`, message);
|
|
1654
|
+
if (!(contextString(context) in this.checks)) {
|
|
1655
|
+
this.checks[contextString(context)] = {};
|
|
1656
|
+
}
|
|
1657
|
+
(message.flags ?? []).forEach((flag) => {
|
|
1658
|
+
const flagCheck = CheckFlagReturnFromJSON(flag);
|
|
1659
|
+
const contextStr = contextString(context);
|
|
1660
|
+
if (this.checks[contextStr] === void 0) {
|
|
1661
|
+
this.checks[contextStr] = {};
|
|
1662
|
+
}
|
|
1663
|
+
this.checks[contextStr][flagCheck.flag] = flagCheck;
|
|
1664
|
+
});
|
|
1665
|
+
this.useWebSocket = true;
|
|
1666
|
+
socket.removeEventListener("message", messageHandler);
|
|
1667
|
+
if (!resolved) {
|
|
1668
|
+
resolved = true;
|
|
1669
|
+
resolve(this.setIsPending(false));
|
|
1670
|
+
}
|
|
1671
|
+
};
|
|
1672
|
+
socket.addEventListener("message", messageHandler);
|
|
1673
|
+
const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
|
|
1674
|
+
const messagePayload = {
|
|
1675
|
+
apiKey: this.apiKey,
|
|
1676
|
+
clientVersion,
|
|
1677
|
+
data: context
|
|
1678
|
+
};
|
|
1679
|
+
this.debug(`WebSocket sending forced message after reconnection:`, messagePayload);
|
|
1680
|
+
socket.send(JSON.stringify(messagePayload));
|
|
1681
|
+
};
|
|
1682
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
1683
|
+
this.debug(`WebSocket already open, sending forced message after reconnection`);
|
|
1684
|
+
sendMessage();
|
|
1685
|
+
} else {
|
|
1686
|
+
socket.addEventListener("open", () => {
|
|
1687
|
+
this.debug(`WebSocket opened, sending forced message after reconnection`);
|
|
1688
|
+
sendMessage();
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
});
|
|
1692
|
+
};
|
|
1540
1693
|
// Send a message on the websocket indicating interest in a particular evaluation context
|
|
1541
1694
|
// and wait for the initial set of flag values to be returned
|
|
1542
1695
|
wsSendMessage = (socket, context) => {
|
package/dist/schematic.d.ts
CHANGED
|
@@ -213,6 +213,8 @@ declare type Event_2 = {
|
|
|
213
213
|
tracker_event_id: string;
|
|
214
214
|
tracker_user_id: string;
|
|
215
215
|
type: EventType;
|
|
216
|
+
retry_count?: number;
|
|
217
|
+
next_retry_at?: number;
|
|
216
218
|
};
|
|
217
219
|
export { Event_2 as Event }
|
|
218
220
|
|
|
@@ -376,6 +378,11 @@ export declare class Schematic {
|
|
|
376
378
|
private wsReconnectAttempts;
|
|
377
379
|
private wsReconnectTimer;
|
|
378
380
|
private wsIntentionalDisconnect;
|
|
381
|
+
private maxEventQueueSize;
|
|
382
|
+
private maxEventRetries;
|
|
383
|
+
private eventRetryInitialDelay;
|
|
384
|
+
private eventRetryMaxDelay;
|
|
385
|
+
private retryTimer;
|
|
379
386
|
constructor(apiKey: string, options?: SchematicOptions);
|
|
380
387
|
/**
|
|
381
388
|
* Get value for a single flag.
|
|
@@ -442,6 +449,8 @@ export declare class Schematic {
|
|
|
442
449
|
*/
|
|
443
450
|
private hasContext;
|
|
444
451
|
private flushContextDependentEventQueue;
|
|
452
|
+
private startRetryTimer;
|
|
453
|
+
private stopRetryTimer;
|
|
445
454
|
private flushEventQueue;
|
|
446
455
|
private getAnonymousId;
|
|
447
456
|
private handleEvent;
|
|
@@ -474,6 +483,7 @@ export declare class Schematic {
|
|
|
474
483
|
*/
|
|
475
484
|
private attemptReconnect;
|
|
476
485
|
private wsConnect;
|
|
486
|
+
private wsSendContextAfterReconnection;
|
|
477
487
|
private wsSendMessage;
|
|
478
488
|
/**
|
|
479
489
|
* State management
|
|
@@ -529,6 +539,14 @@ export declare type SchematicOptions = {
|
|
|
529
539
|
webSocketInitialRetryDelay?: number;
|
|
530
540
|
/** Maximum retry delay in milliseconds for exponential backoff (default: 30000) */
|
|
531
541
|
webSocketMaxRetryDelay?: number;
|
|
542
|
+
/** Maximum number of events to queue for retry when network is down (default: 100) */
|
|
543
|
+
maxEventQueueSize?: number;
|
|
544
|
+
/** Maximum number of retry attempts for failed events (default: 5) */
|
|
545
|
+
maxEventRetries?: number;
|
|
546
|
+
/** Initial retry delay in milliseconds for failed events (default: 1000) */
|
|
547
|
+
eventRetryInitialDelay?: number;
|
|
548
|
+
/** Maximum retry delay in milliseconds for failed events (default: 30000) */
|
|
549
|
+
eventRetryMaxDelay?: number;
|
|
532
550
|
};
|
|
533
551
|
|
|
534
552
|
/** Optional type for implementing custom client-side storage */
|
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.8";
|
|
785
785
|
|
|
786
786
|
// src/index.ts
|
|
787
787
|
var anonymousIdKey = "schematicId";
|
|
@@ -813,6 +813,15 @@ var Schematic = class {
|
|
|
813
813
|
wsReconnectAttempts = 0;
|
|
814
814
|
wsReconnectTimer = null;
|
|
815
815
|
wsIntentionalDisconnect = false;
|
|
816
|
+
maxEventQueueSize = 100;
|
|
817
|
+
// Prevent memory issues with very long network outages
|
|
818
|
+
maxEventRetries = 5;
|
|
819
|
+
// Maximum retry attempts for failed events
|
|
820
|
+
eventRetryInitialDelay = 1e3;
|
|
821
|
+
// Initial retry delay in ms
|
|
822
|
+
eventRetryMaxDelay = 3e4;
|
|
823
|
+
// Maximum retry delay in ms
|
|
824
|
+
retryTimer = null;
|
|
816
825
|
constructor(apiKey, options) {
|
|
817
826
|
this.apiKey = apiKey;
|
|
818
827
|
this.eventQueue = [];
|
|
@@ -871,6 +880,18 @@ var Schematic = class {
|
|
|
871
880
|
if (options?.webSocketMaxRetryDelay !== void 0) {
|
|
872
881
|
this.webSocketMaxRetryDelay = options.webSocketMaxRetryDelay;
|
|
873
882
|
}
|
|
883
|
+
if (options?.maxEventQueueSize !== void 0) {
|
|
884
|
+
this.maxEventQueueSize = options.maxEventQueueSize;
|
|
885
|
+
}
|
|
886
|
+
if (options?.maxEventRetries !== void 0) {
|
|
887
|
+
this.maxEventRetries = options.maxEventRetries;
|
|
888
|
+
}
|
|
889
|
+
if (options?.eventRetryInitialDelay !== void 0) {
|
|
890
|
+
this.eventRetryInitialDelay = options.eventRetryInitialDelay;
|
|
891
|
+
}
|
|
892
|
+
if (options?.eventRetryMaxDelay !== void 0) {
|
|
893
|
+
this.eventRetryMaxDelay = options.eventRetryMaxDelay;
|
|
894
|
+
}
|
|
874
895
|
if (typeof window !== "undefined" && window?.addEventListener) {
|
|
875
896
|
window.addEventListener("beforeunload", () => {
|
|
876
897
|
this.flushEventQueue();
|
|
@@ -1286,11 +1307,53 @@ var Schematic = class {
|
|
|
1286
1307
|
}
|
|
1287
1308
|
}
|
|
1288
1309
|
};
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1310
|
+
startRetryTimer = () => {
|
|
1311
|
+
if (this.retryTimer !== null) {
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
this.retryTimer = setInterval(() => {
|
|
1315
|
+
this.flushEventQueue().catch((error) => {
|
|
1316
|
+
this.debug("Error in retry timer flush:", error);
|
|
1317
|
+
});
|
|
1318
|
+
if (this.eventQueue.length === 0) {
|
|
1319
|
+
this.stopRetryTimer();
|
|
1320
|
+
}
|
|
1321
|
+
}, 5e3);
|
|
1322
|
+
this.debug("Started retry timer");
|
|
1323
|
+
};
|
|
1324
|
+
stopRetryTimer = () => {
|
|
1325
|
+
if (this.retryTimer !== null) {
|
|
1326
|
+
clearInterval(this.retryTimer);
|
|
1327
|
+
this.retryTimer = null;
|
|
1328
|
+
this.debug("Stopped retry timer");
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
flushEventQueue = async () => {
|
|
1332
|
+
if (this.eventQueue.length === 0) {
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
const now = Date.now();
|
|
1336
|
+
const readyEvents = [];
|
|
1337
|
+
const notReadyEvents = [];
|
|
1338
|
+
for (const event of this.eventQueue) {
|
|
1339
|
+
if (event.next_retry_at === void 0 || event.next_retry_at <= now) {
|
|
1340
|
+
readyEvents.push(event);
|
|
1341
|
+
} else {
|
|
1342
|
+
notReadyEvents.push(event);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
if (readyEvents.length === 0) {
|
|
1346
|
+
this.debug(`No events ready for retry yet (${notReadyEvents.length} still in backoff)`);
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
this.debug(`Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`);
|
|
1350
|
+
this.eventQueue = notReadyEvents;
|
|
1351
|
+
for (const event of readyEvents) {
|
|
1352
|
+
try {
|
|
1353
|
+
await this.sendEvent(event);
|
|
1354
|
+
this.debug(`Queued event sent successfully:`, event.type);
|
|
1355
|
+
} catch (error) {
|
|
1356
|
+
this.debug(`Failed to send queued event:`, error);
|
|
1294
1357
|
}
|
|
1295
1358
|
}
|
|
1296
1359
|
};
|
|
@@ -1338,12 +1401,37 @@ var Schematic = class {
|
|
|
1338
1401
|
},
|
|
1339
1402
|
body: payload
|
|
1340
1403
|
});
|
|
1404
|
+
if (!response.ok) {
|
|
1405
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1406
|
+
}
|
|
1341
1407
|
this.debug(`event sent:`, {
|
|
1342
1408
|
status: response.status,
|
|
1343
1409
|
statusText: response.statusText
|
|
1344
1410
|
});
|
|
1345
1411
|
} catch (error) {
|
|
1346
|
-
|
|
1412
|
+
const retryCount = (event.retry_count ?? 0) + 1;
|
|
1413
|
+
if (retryCount <= this.maxEventRetries) {
|
|
1414
|
+
this.debug(`Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`, error);
|
|
1415
|
+
const baseDelay = this.eventRetryInitialDelay * Math.pow(2, retryCount - 1);
|
|
1416
|
+
const jitterDelay = Math.min(baseDelay, this.eventRetryMaxDelay);
|
|
1417
|
+
const nextRetryAt = Date.now() + jitterDelay;
|
|
1418
|
+
const retryEvent = {
|
|
1419
|
+
...event,
|
|
1420
|
+
retry_count: retryCount,
|
|
1421
|
+
next_retry_at: nextRetryAt
|
|
1422
|
+
};
|
|
1423
|
+
if (this.eventQueue.length < this.maxEventQueueSize) {
|
|
1424
|
+
this.eventQueue.push(retryEvent);
|
|
1425
|
+
this.debug(`Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`);
|
|
1426
|
+
} else {
|
|
1427
|
+
this.debug(`Event queue full (${this.maxEventQueueSize}), dropping oldest event`);
|
|
1428
|
+
this.eventQueue.shift();
|
|
1429
|
+
this.eventQueue.push(retryEvent);
|
|
1430
|
+
}
|
|
1431
|
+
this.startRetryTimer();
|
|
1432
|
+
} else {
|
|
1433
|
+
this.debug(`Event failed permanently after ${this.maxEventRetries} attempts, dropping:`, error);
|
|
1434
|
+
}
|
|
1347
1435
|
}
|
|
1348
1436
|
return Promise.resolve();
|
|
1349
1437
|
};
|
|
@@ -1368,6 +1456,7 @@ var Schematic = class {
|
|
|
1368
1456
|
clearTimeout(this.wsReconnectTimer);
|
|
1369
1457
|
this.wsReconnectTimer = null;
|
|
1370
1458
|
}
|
|
1459
|
+
this.stopRetryTimer();
|
|
1371
1460
|
if (this.conn) {
|
|
1372
1461
|
try {
|
|
1373
1462
|
const socket = await this.conn;
|
|
@@ -1415,16 +1504,15 @@ var Schematic = class {
|
|
|
1415
1504
|
* Handle browser coming back online
|
|
1416
1505
|
*/
|
|
1417
1506
|
handleNetworkOnline = () => {
|
|
1418
|
-
|
|
1419
|
-
this.debug("No context set, skipping reconnection");
|
|
1420
|
-
return;
|
|
1421
|
-
}
|
|
1507
|
+
this.debug("Network online, attempting reconnection and flushing queued events");
|
|
1422
1508
|
this.wsReconnectAttempts = 0;
|
|
1423
1509
|
if (this.wsReconnectTimer !== null) {
|
|
1424
1510
|
clearTimeout(this.wsReconnectTimer);
|
|
1425
1511
|
this.wsReconnectTimer = null;
|
|
1426
1512
|
}
|
|
1427
|
-
this.
|
|
1513
|
+
this.flushEventQueue().catch((error) => {
|
|
1514
|
+
this.debug("Error flushing event queue on network online:", error);
|
|
1515
|
+
});
|
|
1428
1516
|
this.attemptReconnect();
|
|
1429
1517
|
};
|
|
1430
1518
|
/**
|
|
@@ -1454,10 +1542,20 @@ var Schematic = class {
|
|
|
1454
1542
|
try {
|
|
1455
1543
|
this.conn = this.wsConnect();
|
|
1456
1544
|
const socket = await this.conn;
|
|
1545
|
+
this.debug(`Reconnection context check:`, {
|
|
1546
|
+
hasCompany: this.context.company !== void 0,
|
|
1547
|
+
hasUser: this.context.user !== void 0,
|
|
1548
|
+
context: this.context
|
|
1549
|
+
});
|
|
1457
1550
|
if (this.context.company !== void 0 || this.context.user !== void 0) {
|
|
1458
|
-
this.debug(`Reconnected, re-sending context`);
|
|
1459
|
-
await this.
|
|
1551
|
+
this.debug(`Reconnected, force re-sending context`);
|
|
1552
|
+
await this.wsSendContextAfterReconnection(socket, this.context);
|
|
1553
|
+
} else {
|
|
1554
|
+
this.debug(`No context to re-send after reconnection - websocket ready for new context`);
|
|
1460
1555
|
}
|
|
1556
|
+
this.flushEventQueue().catch((error) => {
|
|
1557
|
+
this.debug("Error flushing event queue after websocket reconnection:", error);
|
|
1558
|
+
});
|
|
1461
1559
|
this.debug(`Reconnection successful`);
|
|
1462
1560
|
} catch (error) {
|
|
1463
1561
|
this.debug(`Reconnection attempt failed:`, error);
|
|
@@ -1518,6 +1616,61 @@ var Schematic = class {
|
|
|
1518
1616
|
};
|
|
1519
1617
|
});
|
|
1520
1618
|
};
|
|
1619
|
+
// Send a message on the websocket after reconnection, forcing the send even if context appears unchanged
|
|
1620
|
+
// because the server has lost all state and needs the initial context
|
|
1621
|
+
wsSendContextAfterReconnection = (socket, context) => {
|
|
1622
|
+
if (this.isOffline()) {
|
|
1623
|
+
this.debug("wsSendContextAfterReconnection: skipped (offline mode)");
|
|
1624
|
+
this.setIsPending(false);
|
|
1625
|
+
return Promise.resolve();
|
|
1626
|
+
}
|
|
1627
|
+
return new Promise((resolve) => {
|
|
1628
|
+
this.debug(`WebSocket force sending context after reconnection:`, context);
|
|
1629
|
+
this.context = context;
|
|
1630
|
+
const sendMessage = () => {
|
|
1631
|
+
let resolved = false;
|
|
1632
|
+
const messageHandler = (event) => {
|
|
1633
|
+
const message = JSON.parse(event.data);
|
|
1634
|
+
this.debug(`WebSocket message received after reconnection:`, message);
|
|
1635
|
+
if (!(contextString(context) in this.checks)) {
|
|
1636
|
+
this.checks[contextString(context)] = {};
|
|
1637
|
+
}
|
|
1638
|
+
(message.flags ?? []).forEach((flag) => {
|
|
1639
|
+
const flagCheck = CheckFlagReturnFromJSON(flag);
|
|
1640
|
+
const contextStr = contextString(context);
|
|
1641
|
+
if (this.checks[contextStr] === void 0) {
|
|
1642
|
+
this.checks[contextStr] = {};
|
|
1643
|
+
}
|
|
1644
|
+
this.checks[contextStr][flagCheck.flag] = flagCheck;
|
|
1645
|
+
});
|
|
1646
|
+
this.useWebSocket = true;
|
|
1647
|
+
socket.removeEventListener("message", messageHandler);
|
|
1648
|
+
if (!resolved) {
|
|
1649
|
+
resolved = true;
|
|
1650
|
+
resolve(this.setIsPending(false));
|
|
1651
|
+
}
|
|
1652
|
+
};
|
|
1653
|
+
socket.addEventListener("message", messageHandler);
|
|
1654
|
+
const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
|
|
1655
|
+
const messagePayload = {
|
|
1656
|
+
apiKey: this.apiKey,
|
|
1657
|
+
clientVersion,
|
|
1658
|
+
data: context
|
|
1659
|
+
};
|
|
1660
|
+
this.debug(`WebSocket sending forced message after reconnection:`, messagePayload);
|
|
1661
|
+
socket.send(JSON.stringify(messagePayload));
|
|
1662
|
+
};
|
|
1663
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
1664
|
+
this.debug(`WebSocket already open, sending forced message after reconnection`);
|
|
1665
|
+
sendMessage();
|
|
1666
|
+
} else {
|
|
1667
|
+
socket.addEventListener("open", () => {
|
|
1668
|
+
this.debug(`WebSocket opened, sending forced message after reconnection`);
|
|
1669
|
+
sendMessage();
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
});
|
|
1673
|
+
};
|
|
1521
1674
|
// Send a message on the websocket indicating interest in a particular evaluation context
|
|
1522
1675
|
// and wait for the initial set of flag values to be returned
|
|
1523
1676
|
wsSendMessage = (socket, context) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schematichq/schematic-js",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.8",
|
|
4
4
|
"main": "dist/schematic.cjs.js",
|
|
5
5
|
"module": "dist/schematic.esm.js",
|
|
6
6
|
"types": "dist/schematic.d.ts",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@eslint/js": "^9.39.1",
|
|
41
41
|
"@microsoft/api-extractor": "^7.55.0",
|
|
42
|
-
"@openapitools/openapi-generator-cli": "^2.25.
|
|
42
|
+
"@openapitools/openapi-generator-cli": "^2.25.1",
|
|
43
43
|
"@vitest/browser": "^4.0.8",
|
|
44
44
|
"esbuild": "^0.27.0",
|
|
45
45
|
"eslint": "^9.39.1",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"mock-socket": "^9.3.1",
|
|
51
51
|
"prettier": "^3.6.2",
|
|
52
52
|
"typescript": "^5.9.3",
|
|
53
|
-
"typescript-eslint": "^8.
|
|
53
|
+
"typescript-eslint": "^8.47.0",
|
|
54
54
|
"vitest": "^4.0.8"
|
|
55
55
|
},
|
|
56
56
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|