@schematichq/schematic-js 1.2.6 → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023-2025 Schematic, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,3 +1,3 @@
1
- "use strict";(()=>{var re=Object.create;var W=Object.defineProperty;var se=Object.getOwnPropertyDescriptor;var ae=Object.getOwnPropertyNames;var ie=Object.getPrototypeOf,oe=Object.prototype.hasOwnProperty;var le=(r,e)=>()=>(e||r((e={exports:{}}).exports,e),e.exports);var ue=(r,e,t,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of ae(e))!oe.call(r,a)&&a!==t&&W(r,a,{get:()=>e[a],enumerable:!(s=se(e,a))||s.enumerable});return r};var ce=(r,e,t)=>(t=r!=null?re(ie(r)):{},ue(e||!r||!r.__esModule?W(t,"default",{value:r,enumerable:!0}):t,r));var z=le(Q=>{(function(r){var e=(function(t){var s=typeof globalThis<"u"&&globalThis||typeof r<"u"&&r||typeof global<"u"&&global||{},a={searchParams:"URLSearchParams"in s,iterable:"Symbol"in s&&"iterator"in Symbol,blob:"FileReader"in s&&"Blob"in s&&(function(){try{return new Blob,!0}catch{return!1}})(),formData:"FormData"in s,arrayBuffer:"ArrayBuffer"in s};function c(n){return n&&DataView.prototype.isPrototypeOf(n)}if(a.arrayBuffer)var o=["[object Int8Array]","[object Uint8Array]","[object Uint8ClampedArray]","[object Int16Array]","[object Uint16Array]","[object Int32Array]","[object Uint32Array]","[object Float32Array]","[object Float64Array]"],u=ArrayBuffer.isView||function(n){return n&&o.indexOf(Object.prototype.toString.call(n))>-1};function f(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 y(n){return typeof n!="string"&&(n=String(n)),n}function F(n){var i={next:function(){var l=n.shift();return{done:l===void 0,value:l}}};return a.iterable&&(i[Symbol.iterator]=function(){return i}),i}function g(n){this.map={},n instanceof g?n.forEach(function(i,l){this.append(l,i)},this):Array.isArray(n)?n.forEach(function(i){if(i.length!=2)throw new TypeError("Headers constructor: expected name/value pair to be length 2, found"+i.length);this.append(i[0],i[1])},this):n&&Object.getOwnPropertyNames(n).forEach(function(i){this.append(i,n[i])},this)}g.prototype.append=function(n,i){n=f(n),i=y(i);var l=this.map[n];this.map[n]=l?l+", "+i:i},g.prototype.delete=function(n){delete this.map[f(n)]},g.prototype.get=function(n){return n=f(n),this.has(n)?this.map[n]:null},g.prototype.has=function(n){return this.map.hasOwnProperty(f(n))},g.prototype.set=function(n,i){this.map[f(n)]=y(i)},g.prototype.forEach=function(n,i){for(var l in this.map)this.map.hasOwnProperty(l)&&n.call(i,this.map[l],l,this)},g.prototype.keys=function(){var n=[];return this.forEach(function(i,l){n.push(l)}),F(n)},g.prototype.values=function(){var n=[];return this.forEach(function(i){n.push(i)}),F(n)},g.prototype.entries=function(){var n=[];return this.forEach(function(i,l){n.push([l,i])}),F(n)},a.iterable&&(g.prototype[Symbol.iterator]=g.prototype.entries);function k(n){if(!n._noBody){if(n.bodyUsed)return Promise.reject(new TypeError("Already read"));n.bodyUsed=!0}}function m(n){return new Promise(function(i,l){n.onload=function(){i(n.result)},n.onerror=function(){l(n.error)}})}function x(n){var i=new FileReader,l=m(i);return i.readAsArrayBuffer(n),l}function V(n){var i=new FileReader,l=m(i),h=/charset=([A-Za-z0-9_-]+)/.exec(n.type),p=h?h[1]:"utf-8";return i.readAsText(n,p),l}function Y(n){for(var i=new Uint8Array(n),l=new Array(i.length),h=0;h<i.length;h++)l[h]=String.fromCharCode(i[h]);return l.join("")}function q(n){if(n.slice)return n.slice(0);var i=new Uint8Array(n.byteLength);return i.set(new Uint8Array(n)),i.buffer}function $(){return this.bodyUsed=!1,this._initBody=function(n){this.bodyUsed=this.bodyUsed,this._bodyInit=n,n?typeof n=="string"?this._bodyText=n:a.blob&&Blob.prototype.isPrototypeOf(n)?this._bodyBlob=n:a.formData&&FormData.prototype.isPrototypeOf(n)?this._bodyFormData=n:a.searchParams&&URLSearchParams.prototype.isPrototypeOf(n)?this._bodyText=n.toString():a.arrayBuffer&&a.blob&&c(n)?(this._bodyArrayBuffer=q(n.buffer),this._bodyInit=new Blob([this._bodyArrayBuffer])):a.arrayBuffer&&(ArrayBuffer.prototype.isPrototypeOf(n)||u(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):a.searchParams&&URLSearchParams.prototype.isPrototypeOf(n)&&this.headers.set("content-type","application/x-www-form-urlencoded;charset=UTF-8"))},a.blob&&(this.blob=function(){var n=k(this);if(n)return n;if(this._bodyBlob)return Promise.resolve(this._bodyBlob);if(this._bodyArrayBuffer)return Promise.resolve(new Blob([this._bodyArrayBuffer]));if(this._bodyFormData)throw new Error("could not read FormData body as blob");return Promise.resolve(new Blob([this._bodyText]))}),this.arrayBuffer=function(){if(this._bodyArrayBuffer){var n=k(this);return n||(ArrayBuffer.isView(this._bodyArrayBuffer)?Promise.resolve(this._bodyArrayBuffer.buffer.slice(this._bodyArrayBuffer.byteOffset,this._bodyArrayBuffer.byteOffset+this._bodyArrayBuffer.byteLength)):Promise.resolve(this._bodyArrayBuffer))}else{if(a.blob)return this.blob().then(x);throw new Error("could not read as ArrayBuffer")}},this.text=function(){var n=k(this);if(n)return n;if(this._bodyBlob)return V(this._bodyBlob);if(this._bodyArrayBuffer)return Promise.resolve(Y(this._bodyArrayBuffer));if(this._bodyFormData)throw new Error("could not read FormData body as text");return Promise.resolve(this._bodyText)},a.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 i=n.toUpperCase();return Z.indexOf(i)>-1?i:n}function S(n,i){if(!(this instanceof S))throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.');i=i||{};var l=i.body;if(n instanceof S){if(n.bodyUsed)throw new TypeError("Already read");this.url=n.url,this.credentials=n.credentials,i.headers||(this.headers=new g(n.headers)),this.method=n.method,this.mode=n.mode,this.signal=n.signal,!l&&n._bodyInit!=null&&(l=n._bodyInit,n.bodyUsed=!0)}else this.url=String(n);if(this.credentials=i.credentials||this.credentials||"same-origin",(i.headers||!this.headers)&&(this.headers=new g(i.headers)),this.method=j(i.method||this.method||"GET"),this.mode=i.mode||this.mode||null,this.signal=i.signal||this.signal||(function(){if("AbortController"in s){var d=new AbortController;return d.signal}})(),this.referrer=null,(this.method==="GET"||this.method==="HEAD")&&l)throw new TypeError("Body not allowed for GET or HEAD requests");if(this._initBody(l),(this.method==="GET"||this.method==="HEAD")&&(i.cache==="no-store"||i.cache==="no-cache")){var h=/([?&])_=[^&]*/;if(h.test(this.url))this.url=this.url.replace(h,"$1_="+new Date().getTime());else{var p=/\?/;this.url+=(p.test(this.url)?"&":"?")+"_="+new Date().getTime()}}}S.prototype.clone=function(){return new S(this,{body:this._bodyInit})};function ee(n){var i=new FormData;return n.trim().split("&").forEach(function(l){if(l){var h=l.split("="),p=h.shift().replace(/\+/g," "),d=h.join("=").replace(/\+/g," ");i.append(decodeURIComponent(p),decodeURIComponent(d))}}),i}function te(n){var i=new g,l=n.replace(/\r?\n[\t ]+/g," ");return l.split("\r").map(function(h){return h.indexOf(`
2
- `)===0?h.substr(1,h.length):h}).forEach(function(h){var p=h.split(":"),d=p.shift().trim();if(d){var U=p.join(":").trim();try{i.append(d,U)}catch(A){console.warn("Response "+A.message)}}}),i}$.call(S.prototype);function E(n,i){if(!(this instanceof E))throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.');if(i||(i={}),this.type="default",this.status=i.status===void 0?200:i.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=i.statusText===void 0?"":""+i.statusText,this.headers=new g(i.headers),this.url=i.url||"",this._initBody(n)}$.call(E.prototype),E.prototype.clone=function(){return new E(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new g(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,i){if(ne.indexOf(i)===-1)throw new RangeError("Invalid status code");return new E(null,{status:i,headers:{location:n}})},t.DOMException=s.DOMException;try{new t.DOMException}catch{t.DOMException=function(i,l){this.message=i,this.name=l;var h=Error(i);this.stack=h.stack},t.DOMException.prototype=Object.create(Error.prototype),t.DOMException.prototype.constructor=t.DOMException}function I(n,i){return new Promise(function(l,h){var p=new S(n,i);if(p.signal&&p.signal.aborted)return h(new t.DOMException("Aborted","AbortError"));var d=new XMLHttpRequest;function U(){d.abort()}d.onload=function(){var b={statusText:d.statusText,headers:te(d.getAllResponseHeaders()||"")};p.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 R="response"in d?d.response:d.responseText;setTimeout(function(){l(new E(R,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 A(b){try{return b===""&&s.location.href?s.location.href:b}catch{return b}}if(d.open(p.method,A(p.url),!0),p.credentials==="include"?d.withCredentials=!0:p.credentials==="omit"&&(d.withCredentials=!1),"responseType"in d&&(a.blob?d.responseType="blob":a.arrayBuffer&&(d.responseType="arraybuffer")),i&&typeof i.headers=="object"&&!(i.headers instanceof g||s.Headers&&i.headers instanceof s.Headers)){var H=[];Object.getOwnPropertyNames(i.headers).forEach(function(b){H.push(f(b)),d.setRequestHeader(b,y(i.headers[b]))}),p.headers.forEach(function(b,R){H.indexOf(R)===-1&&d.setRequestHeader(R,b)})}else p.headers.forEach(function(b,R){d.setRequestHeader(R,b)});p.signal&&(p.signal.addEventListener("abort",U),d.onreadystatechange=function(){d.readyState===4&&p.signal.removeEventListener("abort",U)}),d.send(typeof p._bodyInit>"u"?null:p._bodyInit)})}return I.polyfill=!0,s.fetch||(s.fetch=I,s.Headers=g,s.Request=S,s.Response=E),t.Headers=g,t.Request=S,t.Response=E,t.fetch=I,t})({})})(typeof self<"u"?self:Q)});var v=[];for(let r=0;r<256;++r)v.push((r+256).toString(16).slice(1));function K(r,e=0){return(v[r[e+0]]+v[r[e+1]]+v[r[e+2]]+v[r[e+3]]+"-"+v[r[e+4]]+v[r[e+5]]+"-"+v[r[e+6]]+v[r[e+7]]+"-"+v[r[e+8]]+v[r[e+9]]+"-"+v[r[e+10]]+v[r[e+11]]+v[r[e+12]]+v[r[e+13]]+v[r[e+14]]+v[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 a=0;a<16;++a)e[t+a]=s[a];return e}return K(s)}function ge(r,e,t){return L.randomUUID&&!e&&!r?L.randomUUID():he(r,e,t)}var _=ge;var st=ce(z());function w(r){return pe(r,!1)}function pe(r,e){return r==null?r:{companyId:r.company_id==null?void 0:r.company_id,error:r.error==null?void 0:r.error,featureAllocation:r.feature_allocation==null?void 0:r.feature_allocation,featureUsage:r.feature_usage==null?void 0:r.feature_usage,featureUsageEvent:r.feature_usage_event==null?void 0:r.feature_usage_event,featureUsagePeriod:r.feature_usage_period==null?void 0:r.feature_usage_period,featureUsageResetAt:r.feature_usage_reset_at==null?void 0:new Date(r.feature_usage_reset_at),flag:r.flag,flagId:r.flag_id==null?void 0:r.flag_id,reason:r.reason,ruleId:r.rule_id==null?void 0:r.rule_id,ruleType:r.rule_type==null?void 0:r.rule_type,userId:r.user_id==null?void 0:r.user_id,value:r.value}}function 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 T(r){return be(r,!1)}function be(r,e){return r==null?r:{data:w(r.data),params:r.params}}function X(r){return ve(r,!1)}function ve(r,e){return r==null?r:{flags:r.flags.map(w)}}function J(r){return ke(r,!1)}function ke(r,e){return r==null?r:{data:X(r.data),params:r.params}}var O=r=>{let{companyId:e,error:t,featureAllocation:s,featureUsage:a,featureUsageEvent:c,featureUsagePeriod:o,featureUsageResetAt:u,flag:f,flagId:y,reason:F,ruleId:g,ruleType:k,userId:m,value:x}=w(r);return{featureUsageExceeded:!x&&(k=="company_override_usage_exceeded"||k=="plan_entitlement_usage_exceeded"),companyId:e??void 0,error:t??void 0,featureAllocation:s??void 0,featureUsage:a??void 0,featureUsageEvent:c===null?void 0:c,featureUsagePeriod:o??void 0,featureUsageResetAt:u??void 0,flag:f,flagId:y??void 0,reason:F,ruleId:g??void 0,ruleType:k??void 0,userId:m??void 0,value:x}};function C(r){let e=Object.keys(r).reduce((t,s)=>{let c=Object.keys(r[s]||{}).sort().reduce((o,u)=>(o[u]=r[s][u],o),{});return t[s]=c,t},{});return JSON.stringify(e)}var M="1.2.6";var G="schematicId";var D=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";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),a=s.get("schematic_debug");a!==null&&(a===""||a==="true"||a==="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@${M}`,...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),typeof window<"u"&&window?.addEventListener&&window.addEventListener("beforeunload",()=>{this.flushEventQueue(),this.flushContextDependentEventQueue()}),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,a=e.context||this.context,c=C(a);if(this.debug(`checkFlag: ${s}`,{context:a,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(a)}).then(u=>{if(!u.ok)throw new Error("Network response was not ok");return u.json()}).then(u=>{let f=T(u);this.debug(`checkFlag result: ${s}`,f);let y=O(f.data);return typeof y.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(y),this.submitFlagCheckEvent(s,y,a),y.value}).catch(u=>{console.error("There was a problem with the fetch operation:",u);let f={flag:s,value:t,reason:"API request failed",error:u instanceof Error?u.message:String(u)};return this.submitFlagCheckEvent(s,f,a),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(a)}catch(F){return console.error("WebSocket connection failed, falling back to REST:",F),this.fallbackToRest(s,a,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,a),y}catch(o){console.error("Unexpected error in checkFlag:",o);let u={flag:s,value:t,reason:"Unexpected error in flag check",error:o instanceof Error?o.message:String(o)};return this.submitFlagCheckEvent(s,u,a),t}}debug(e,...t){this.debugEnabled&&console.log(`[Schematic] ${e}`,...t)}isOffline(){return this.offlineEnabled}submitFlagCheckEvent(e,t,s){let a={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:",a),this.handleEvent("flag_check",N(a))}async fallbackToRest(e,t,s){if(this.isOffline())return this.debug(`fallbackToRest offline result: ${e}`,{value:s,offlineMode:!0}),s;try{let a=`${this.apiUrl}/flags/${e}/check`,c=await fetch(a,{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(),u=T(o);this.debug(`fallbackToRest result: ${e}`,u);let f=O(u.data);return typeof f.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(f),this.submitFlagCheckEvent(e,f,t),f.value}catch(a){console.error("REST API call failed, using fallback value:",a);let c={flag:e,value:s,reason:"API request failed (fallback)",error:a instanceof Error?a.message:String(a)};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(a=>{if(!a.ok)throw new Error("Network response was not ok");return a.json()}).then(a=>{let c=J(a);return this.debug("checkFlags result:",c),(c?.data?.flags??[]).reduce((o,u)=>(o[u.flag]=u.value,o),{})}).catch(a=>(console.error("There was a problem with the fetch operation:",a),{}))};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.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:a,traits:c,quantity:o=1}=e;if(!this.hasContext(t,s)){this.debug(`track: queuing event "${a}" until context is available`);let f={api_key:this.apiKey,body:{company:t,event:a,traits:c??{},user:s,quantity:o},sent_at:new Date().toISOString(),tracker_event_id:_(),tracker_user_id:this.getAnonymousId(),type:"track"};return this.contextDependentEventQueue.push(f),Promise.resolve()}let u={company:t??this.context.company,event:a,traits:c??{},user:s??this.context.user,quantity:o};return this.debug("track:",u),a in this.featureUsageEventMap&&this.optimisticallyUpdateFeatureUsage(a,o),this.handleEvent("track",u)};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(([a,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: ${a}`,{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][a]=o);let u=C(this.context);this.checks[u]!==void 0&&this.checks[u]!==null&&(this.checks[u][a]=o),this.notifyFlagCheckListeners(a,o),this.notifyFlagValueListeners(a,o.value)}}))};hasContext=(e,t)=>{let s=e!=null&&Object.keys(e).length>0||t!=null&&Object.keys(t).length>0,a=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||a};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},a={...e,body:s,sent_at:new Date().toISOString()};this.sendEvent(a)}else this.sendEvent(e)}};flushEventQueue=()=>{for(;this.eventQueue.length>0;){let e=this.eventQueue.shift();e&&this.sendEvent(e)}};getAnonymousId=()=>{if(!this.storage)return _();let e=this.storage.getItem(G);if(typeof e<"u")return e;let t=_();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:_(),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 a=await fetch(t,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8"},body:s});this.debug("event sent:",{status:a.status,statusText:a.statusText})}catch(a){console.error("Error sending Schematic event: ",a)}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.conn)try{(await this.conn).close()}catch(e){console.error("Error during cleanup:",e)}finally{this.conn=null}};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 a=new WebSocket(s);a.onopen=()=>{this.debug("WebSocket connection opened"),e(a)},a.onerror=c=>{this.debug("WebSocket connection error:",c),t(c)},a.onclose=()=>{this.debug("WebSocket connection closed"),this.conn=null}});wsSendMessage=(e,t)=>this.isOffline()?(this.debug("wsSendMessage: skipped (offline mode)"),this.setIsPending(!1),Promise.resolve()):new Promise((s,a)=>{if(C(t)==C(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,u=F=>{let g=JSON.parse(F.data);this.debug("WebSocket message received:",g),C(t)in this.checks||(this.checks[C(t)]={}),(g.flags??[]).forEach(k=>{let m=O(k),x=C(t);this.checks[x]===void 0&&(this.checks[x]={}),this.checks[x][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[k.flag]?.size>0||this.flagValueListeners[k.flag]?.size>0)&&this.submitFlagCheckEvent(m.flag,m,t),this.notifyFlagCheckListeners(k.flag,m),this.notifyFlagValueListeners(k.flag,m.value)}),this.flushContextDependentEventQueue(),this.setIsPending(!1),o||(o=!0,s())};e.addEventListener("message",u);let f=this.additionalHeaders["X-Schematic-Client-Version"]??`schematic-js@${M}`,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"),a("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=C(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(a=>Fe(a,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(a=>Ce(a,t))}},Ee=(r,e)=>{r.length>0?r(e):r()},Fe=(r,e)=>{r.length>0?r(e):r()},Ce=(r,e)=>{r.length>0?r(e):r()};window.Schematic=D;})();
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 */
@@ -800,7 +800,7 @@ function contextString(context) {
800
800
  }
801
801
 
802
802
  // src/version.ts
803
- var version = "1.2.6";
803
+ var version = "1.2.8";
804
804
 
805
805
  // src/index.ts
806
806
  var anonymousIdKey = "schematicId";
@@ -824,6 +824,23 @@ var Schematic = class {
824
824
  checks = {};
825
825
  featureUsageEventMap = {};
826
826
  webSocketUrl = "wss://api.schematichq.com";
827
+ webSocketConnectionTimeout = 1e4;
828
+ webSocketReconnect = true;
829
+ webSocketMaxReconnectAttempts = 7;
830
+ webSocketInitialRetryDelay = 1e3;
831
+ webSocketMaxRetryDelay = 3e4;
832
+ wsReconnectAttempts = 0;
833
+ wsReconnectTimer = null;
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;
827
844
  constructor(apiKey, options) {
828
845
  this.apiKey = apiKey;
829
846
  this.eventQueue = [];
@@ -867,11 +884,48 @@ var Schematic = class {
867
884
  if (options?.webSocketUrl !== void 0) {
868
885
  this.webSocketUrl = options.webSocketUrl;
869
886
  }
887
+ if (options?.webSocketConnectionTimeout !== void 0) {
888
+ this.webSocketConnectionTimeout = options.webSocketConnectionTimeout;
889
+ }
890
+ if (options?.webSocketReconnect !== void 0) {
891
+ this.webSocketReconnect = options.webSocketReconnect;
892
+ }
893
+ if (options?.webSocketMaxReconnectAttempts !== void 0) {
894
+ this.webSocketMaxReconnectAttempts = options.webSocketMaxReconnectAttempts;
895
+ }
896
+ if (options?.webSocketInitialRetryDelay !== void 0) {
897
+ this.webSocketInitialRetryDelay = options.webSocketInitialRetryDelay;
898
+ }
899
+ if (options?.webSocketMaxRetryDelay !== void 0) {
900
+ this.webSocketMaxRetryDelay = options.webSocketMaxRetryDelay;
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
+ }
870
914
  if (typeof window !== "undefined" && window?.addEventListener) {
871
915
  window.addEventListener("beforeunload", () => {
872
916
  this.flushEventQueue();
873
917
  this.flushContextDependentEventQueue();
874
918
  });
919
+ if (this.useWebSocket) {
920
+ window.addEventListener("offline", () => {
921
+ this.debug("Browser went offline, closing WebSocket connection");
922
+ this.handleNetworkOffline();
923
+ });
924
+ window.addEventListener("online", () => {
925
+ this.debug("Browser came online, attempting to reconnect WebSocket");
926
+ this.handleNetworkOnline();
927
+ });
928
+ }
875
929
  }
876
930
  if (this.offlineEnabled) {
877
931
  this.debug(
@@ -1136,6 +1190,13 @@ var Schematic = class {
1136
1190
  try {
1137
1191
  this.setIsPending(true);
1138
1192
  if (!this.conn) {
1193
+ if (this.wsReconnectTimer !== null) {
1194
+ this.debug(
1195
+ `Cancelling scheduled reconnection, connecting immediately`
1196
+ );
1197
+ clearTimeout(this.wsReconnectTimer);
1198
+ this.wsReconnectTimer = null;
1199
+ }
1139
1200
  this.conn = this.wsConnect();
1140
1201
  }
1141
1202
  const socket = await this.conn;
@@ -1265,11 +1326,53 @@ var Schematic = class {
1265
1326
  }
1266
1327
  }
1267
1328
  };
1268
- flushEventQueue = () => {
1269
- while (this.eventQueue.length > 0) {
1270
- const event = this.eventQueue.shift();
1271
- if (event) {
1272
- this.sendEvent(event);
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);
1273
1376
  }
1274
1377
  }
1275
1378
  };
@@ -1317,12 +1420,37 @@ var Schematic = class {
1317
1420
  },
1318
1421
  body: payload
1319
1422
  });
1423
+ if (!response.ok) {
1424
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1425
+ }
1320
1426
  this.debug(`event sent:`, {
1321
1427
  status: response.status,
1322
1428
  statusText: response.statusText
1323
1429
  });
1324
1430
  } catch (error) {
1325
- console.error("Error sending Schematic event: ", error);
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
+ }
1326
1454
  }
1327
1455
  return Promise.resolve();
1328
1456
  };
@@ -1342,6 +1470,12 @@ var Schematic = class {
1342
1470
  this.debug("cleanup: skipped (offline mode)");
1343
1471
  return Promise.resolve();
1344
1472
  }
1473
+ this.wsIntentionalDisconnect = true;
1474
+ if (this.wsReconnectTimer !== null) {
1475
+ clearTimeout(this.wsReconnectTimer);
1476
+ this.wsReconnectTimer = null;
1477
+ }
1478
+ this.stopRetryTimer();
1345
1479
  if (this.conn) {
1346
1480
  try {
1347
1481
  const socket = await this.conn;
@@ -1353,6 +1487,100 @@ var Schematic = class {
1353
1487
  }
1354
1488
  }
1355
1489
  };
1490
+ /**
1491
+ * Calculate the delay for the next reconnection attempt using exponential backoff with jitter.
1492
+ * This helps prevent dogpiling when the server recovers from an outage.
1493
+ */
1494
+ calculateReconnectDelay = () => {
1495
+ const exponentialDelay = this.webSocketInitialRetryDelay * Math.pow(2, this.wsReconnectAttempts);
1496
+ const cappedDelay = Math.min(exponentialDelay, this.webSocketMaxRetryDelay);
1497
+ const jitter = Math.random() * cappedDelay * 0.5;
1498
+ const totalDelay = cappedDelay + jitter;
1499
+ this.debug(
1500
+ `Reconnect delay calculated: ${totalDelay.toFixed(0)}ms (attempt ${this.wsReconnectAttempts + 1}/${this.webSocketMaxReconnectAttempts})`
1501
+ );
1502
+ return totalDelay;
1503
+ };
1504
+ /**
1505
+ * Handle browser going offline
1506
+ */
1507
+ handleNetworkOffline = async () => {
1508
+ if (this.conn !== null) {
1509
+ try {
1510
+ const socket = await this.conn;
1511
+ socket.close();
1512
+ } catch (error) {
1513
+ this.debug("Error closing connection on offline:", error);
1514
+ }
1515
+ this.conn = null;
1516
+ }
1517
+ if (this.wsReconnectTimer !== null) {
1518
+ clearTimeout(this.wsReconnectTimer);
1519
+ this.wsReconnectTimer = null;
1520
+ }
1521
+ };
1522
+ /**
1523
+ * Handle browser coming back online
1524
+ */
1525
+ handleNetworkOnline = () => {
1526
+ this.debug("Network online, attempting reconnection and flushing queued events");
1527
+ this.wsReconnectAttempts = 0;
1528
+ if (this.wsReconnectTimer !== null) {
1529
+ clearTimeout(this.wsReconnectTimer);
1530
+ this.wsReconnectTimer = null;
1531
+ }
1532
+ this.flushEventQueue().catch((error) => {
1533
+ this.debug("Error flushing event queue on network online:", error);
1534
+ });
1535
+ this.attemptReconnect();
1536
+ };
1537
+ /**
1538
+ * Attempt to reconnect the WebSocket connection with exponential backoff.
1539
+ * Called automatically when the connection closes unexpectedly.
1540
+ */
1541
+ attemptReconnect = () => {
1542
+ if (this.wsReconnectAttempts >= this.webSocketMaxReconnectAttempts) {
1543
+ this.debug(
1544
+ `Maximum reconnection attempts (${this.webSocketMaxReconnectAttempts}) reached, giving up`
1545
+ );
1546
+ return;
1547
+ }
1548
+ if (this.wsReconnectTimer !== null) {
1549
+ clearTimeout(this.wsReconnectTimer);
1550
+ }
1551
+ const delay = this.calculateReconnectDelay();
1552
+ this.debug(
1553
+ `Scheduling reconnection attempt ${this.wsReconnectAttempts + 1}/${this.webSocketMaxReconnectAttempts} in ${delay.toFixed(0)}ms`
1554
+ );
1555
+ this.wsReconnectTimer = setTimeout(async () => {
1556
+ this.wsReconnectTimer = null;
1557
+ this.wsReconnectAttempts++;
1558
+ this.debug(
1559
+ `Attempting to reconnect (attempt ${this.wsReconnectAttempts}/${this.webSocketMaxReconnectAttempts})`
1560
+ );
1561
+ try {
1562
+ this.conn = this.wsConnect();
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
+ });
1569
+ if (this.context.company !== void 0 || this.context.user !== void 0) {
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`);
1574
+ }
1575
+ this.flushEventQueue().catch((error) => {
1576
+ this.debug("Error flushing event queue after websocket reconnection:", error);
1577
+ });
1578
+ this.debug(`Reconnection successful`);
1579
+ } catch (error) {
1580
+ this.debug(`Reconnection attempt failed:`, error);
1581
+ }
1582
+ }, delay);
1583
+ };
1356
1584
  // Open a websocket connection
1357
1585
  wsConnect = () => {
1358
1586
  if (this.isOffline()) {
@@ -1365,20 +1593,103 @@ var Schematic = class {
1365
1593
  const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
1366
1594
  this.debug(`connecting to WebSocket:`, wsUrl);
1367
1595
  const webSocket = new WebSocket(wsUrl);
1596
+ let timeoutId = null;
1597
+ let isResolved = false;
1598
+ timeoutId = setTimeout(() => {
1599
+ if (!isResolved) {
1600
+ this.debug(
1601
+ `WebSocket connection timeout after ${this.webSocketConnectionTimeout}ms`
1602
+ );
1603
+ webSocket.close();
1604
+ reject(new Error("WebSocket connection timeout"));
1605
+ }
1606
+ }, this.webSocketConnectionTimeout);
1368
1607
  webSocket.onopen = () => {
1608
+ isResolved = true;
1609
+ if (timeoutId !== null) {
1610
+ clearTimeout(timeoutId);
1611
+ }
1612
+ this.wsReconnectAttempts = 0;
1613
+ this.wsIntentionalDisconnect = false;
1369
1614
  this.debug(`WebSocket connection opened`);
1370
1615
  resolve(webSocket);
1371
1616
  };
1372
1617
  webSocket.onerror = (error) => {
1618
+ isResolved = true;
1619
+ if (timeoutId !== null) {
1620
+ clearTimeout(timeoutId);
1621
+ }
1373
1622
  this.debug(`WebSocket connection error:`, error);
1374
1623
  reject(error);
1375
1624
  };
1376
1625
  webSocket.onclose = () => {
1626
+ isResolved = true;
1627
+ if (timeoutId !== null) {
1628
+ clearTimeout(timeoutId);
1629
+ }
1377
1630
  this.debug(`WebSocket connection closed`);
1378
1631
  this.conn = null;
1632
+ if (!this.wsIntentionalDisconnect && this.webSocketReconnect) {
1633
+ this.attemptReconnect();
1634
+ }
1379
1635
  };
1380
1636
  });
1381
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
+ };
1382
1693
  // Send a message on the websocket indicating interest in a particular evaluation context
1383
1694
  // and wait for the initial set of flag values to be returned
1384
1695
  wsSendMessage = (socket, context) => {
@@ -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
 
@@ -368,6 +370,19 @@ export declare class Schematic {
368
370
  private checks;
369
371
  private featureUsageEventMap;
370
372
  private webSocketUrl;
373
+ private webSocketConnectionTimeout;
374
+ private webSocketReconnect;
375
+ private webSocketMaxReconnectAttempts;
376
+ private webSocketInitialRetryDelay;
377
+ private webSocketMaxRetryDelay;
378
+ private wsReconnectAttempts;
379
+ private wsReconnectTimer;
380
+ private wsIntentionalDisconnect;
381
+ private maxEventQueueSize;
382
+ private maxEventRetries;
383
+ private eventRetryInitialDelay;
384
+ private eventRetryMaxDelay;
385
+ private retryTimer;
371
386
  constructor(apiKey: string, options?: SchematicOptions);
372
387
  /**
373
388
  * Get value for a single flag.
@@ -434,6 +449,8 @@ export declare class Schematic {
434
449
  */
435
450
  private hasContext;
436
451
  private flushContextDependentEventQueue;
452
+ private startRetryTimer;
453
+ private stopRetryTimer;
437
454
  private flushEventQueue;
438
455
  private getAnonymousId;
439
456
  private handleEvent;
@@ -447,7 +464,26 @@ export declare class Schematic {
447
464
  * In offline mode, this is a no-op.
448
465
  */
449
466
  cleanup: () => Promise<void>;
467
+ /**
468
+ * Calculate the delay for the next reconnection attempt using exponential backoff with jitter.
469
+ * This helps prevent dogpiling when the server recovers from an outage.
470
+ */
471
+ private calculateReconnectDelay;
472
+ /**
473
+ * Handle browser going offline
474
+ */
475
+ private handleNetworkOffline;
476
+ /**
477
+ * Handle browser coming back online
478
+ */
479
+ private handleNetworkOnline;
480
+ /**
481
+ * Attempt to reconnect the WebSocket connection with exponential backoff.
482
+ * Called automatically when the connection closes unexpectedly.
483
+ */
484
+ private attemptReconnect;
450
485
  private wsConnect;
486
+ private wsSendContextAfterReconnection;
451
487
  private wsSendMessage;
452
488
  /**
453
489
  * State management
@@ -493,6 +529,24 @@ export declare type SchematicOptions = {
493
529
  useWebSocket?: boolean;
494
530
  /** Optionally provide a custom WebSocket URL */
495
531
  webSocketUrl?: string;
532
+ /** WebSocket connection timeout in milliseconds (default: 10000) */
533
+ webSocketConnectionTimeout?: number;
534
+ /** Enable automatic reconnection on WebSocket disconnect (default: true) */
535
+ webSocketReconnect?: boolean;
536
+ /** Maximum number of reconnection attempts (default: 7, set to Infinity for unlimited) */
537
+ webSocketMaxReconnectAttempts?: number;
538
+ /** Initial retry delay in milliseconds for exponential backoff (default: 1000) */
539
+ webSocketInitialRetryDelay?: number;
540
+ /** Maximum retry delay in milliseconds for exponential backoff (default: 30000) */
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;
496
550
  };
497
551
 
498
552
  /** Optional type for implementing custom client-side storage */
@@ -781,7 +781,7 @@ function contextString(context) {
781
781
  }
782
782
 
783
783
  // src/version.ts
784
- var version = "1.2.6";
784
+ var version = "1.2.8";
785
785
 
786
786
  // src/index.ts
787
787
  var anonymousIdKey = "schematicId";
@@ -805,6 +805,23 @@ var Schematic = class {
805
805
  checks = {};
806
806
  featureUsageEventMap = {};
807
807
  webSocketUrl = "wss://api.schematichq.com";
808
+ webSocketConnectionTimeout = 1e4;
809
+ webSocketReconnect = true;
810
+ webSocketMaxReconnectAttempts = 7;
811
+ webSocketInitialRetryDelay = 1e3;
812
+ webSocketMaxRetryDelay = 3e4;
813
+ wsReconnectAttempts = 0;
814
+ wsReconnectTimer = null;
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;
808
825
  constructor(apiKey, options) {
809
826
  this.apiKey = apiKey;
810
827
  this.eventQueue = [];
@@ -848,11 +865,48 @@ var Schematic = class {
848
865
  if (options?.webSocketUrl !== void 0) {
849
866
  this.webSocketUrl = options.webSocketUrl;
850
867
  }
868
+ if (options?.webSocketConnectionTimeout !== void 0) {
869
+ this.webSocketConnectionTimeout = options.webSocketConnectionTimeout;
870
+ }
871
+ if (options?.webSocketReconnect !== void 0) {
872
+ this.webSocketReconnect = options.webSocketReconnect;
873
+ }
874
+ if (options?.webSocketMaxReconnectAttempts !== void 0) {
875
+ this.webSocketMaxReconnectAttempts = options.webSocketMaxReconnectAttempts;
876
+ }
877
+ if (options?.webSocketInitialRetryDelay !== void 0) {
878
+ this.webSocketInitialRetryDelay = options.webSocketInitialRetryDelay;
879
+ }
880
+ if (options?.webSocketMaxRetryDelay !== void 0) {
881
+ this.webSocketMaxRetryDelay = options.webSocketMaxRetryDelay;
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
+ }
851
895
  if (typeof window !== "undefined" && window?.addEventListener) {
852
896
  window.addEventListener("beforeunload", () => {
853
897
  this.flushEventQueue();
854
898
  this.flushContextDependentEventQueue();
855
899
  });
900
+ if (this.useWebSocket) {
901
+ window.addEventListener("offline", () => {
902
+ this.debug("Browser went offline, closing WebSocket connection");
903
+ this.handleNetworkOffline();
904
+ });
905
+ window.addEventListener("online", () => {
906
+ this.debug("Browser came online, attempting to reconnect WebSocket");
907
+ this.handleNetworkOnline();
908
+ });
909
+ }
856
910
  }
857
911
  if (this.offlineEnabled) {
858
912
  this.debug(
@@ -1117,6 +1171,13 @@ var Schematic = class {
1117
1171
  try {
1118
1172
  this.setIsPending(true);
1119
1173
  if (!this.conn) {
1174
+ if (this.wsReconnectTimer !== null) {
1175
+ this.debug(
1176
+ `Cancelling scheduled reconnection, connecting immediately`
1177
+ );
1178
+ clearTimeout(this.wsReconnectTimer);
1179
+ this.wsReconnectTimer = null;
1180
+ }
1120
1181
  this.conn = this.wsConnect();
1121
1182
  }
1122
1183
  const socket = await this.conn;
@@ -1246,11 +1307,53 @@ var Schematic = class {
1246
1307
  }
1247
1308
  }
1248
1309
  };
1249
- flushEventQueue = () => {
1250
- while (this.eventQueue.length > 0) {
1251
- const event = this.eventQueue.shift();
1252
- if (event) {
1253
- this.sendEvent(event);
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);
1254
1357
  }
1255
1358
  }
1256
1359
  };
@@ -1298,12 +1401,37 @@ var Schematic = class {
1298
1401
  },
1299
1402
  body: payload
1300
1403
  });
1404
+ if (!response.ok) {
1405
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1406
+ }
1301
1407
  this.debug(`event sent:`, {
1302
1408
  status: response.status,
1303
1409
  statusText: response.statusText
1304
1410
  });
1305
1411
  } catch (error) {
1306
- console.error("Error sending Schematic event: ", error);
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
+ }
1307
1435
  }
1308
1436
  return Promise.resolve();
1309
1437
  };
@@ -1323,6 +1451,12 @@ var Schematic = class {
1323
1451
  this.debug("cleanup: skipped (offline mode)");
1324
1452
  return Promise.resolve();
1325
1453
  }
1454
+ this.wsIntentionalDisconnect = true;
1455
+ if (this.wsReconnectTimer !== null) {
1456
+ clearTimeout(this.wsReconnectTimer);
1457
+ this.wsReconnectTimer = null;
1458
+ }
1459
+ this.stopRetryTimer();
1326
1460
  if (this.conn) {
1327
1461
  try {
1328
1462
  const socket = await this.conn;
@@ -1334,6 +1468,100 @@ var Schematic = class {
1334
1468
  }
1335
1469
  }
1336
1470
  };
1471
+ /**
1472
+ * Calculate the delay for the next reconnection attempt using exponential backoff with jitter.
1473
+ * This helps prevent dogpiling when the server recovers from an outage.
1474
+ */
1475
+ calculateReconnectDelay = () => {
1476
+ const exponentialDelay = this.webSocketInitialRetryDelay * Math.pow(2, this.wsReconnectAttempts);
1477
+ const cappedDelay = Math.min(exponentialDelay, this.webSocketMaxRetryDelay);
1478
+ const jitter = Math.random() * cappedDelay * 0.5;
1479
+ const totalDelay = cappedDelay + jitter;
1480
+ this.debug(
1481
+ `Reconnect delay calculated: ${totalDelay.toFixed(0)}ms (attempt ${this.wsReconnectAttempts + 1}/${this.webSocketMaxReconnectAttempts})`
1482
+ );
1483
+ return totalDelay;
1484
+ };
1485
+ /**
1486
+ * Handle browser going offline
1487
+ */
1488
+ handleNetworkOffline = async () => {
1489
+ if (this.conn !== null) {
1490
+ try {
1491
+ const socket = await this.conn;
1492
+ socket.close();
1493
+ } catch (error) {
1494
+ this.debug("Error closing connection on offline:", error);
1495
+ }
1496
+ this.conn = null;
1497
+ }
1498
+ if (this.wsReconnectTimer !== null) {
1499
+ clearTimeout(this.wsReconnectTimer);
1500
+ this.wsReconnectTimer = null;
1501
+ }
1502
+ };
1503
+ /**
1504
+ * Handle browser coming back online
1505
+ */
1506
+ handleNetworkOnline = () => {
1507
+ this.debug("Network online, attempting reconnection and flushing queued events");
1508
+ this.wsReconnectAttempts = 0;
1509
+ if (this.wsReconnectTimer !== null) {
1510
+ clearTimeout(this.wsReconnectTimer);
1511
+ this.wsReconnectTimer = null;
1512
+ }
1513
+ this.flushEventQueue().catch((error) => {
1514
+ this.debug("Error flushing event queue on network online:", error);
1515
+ });
1516
+ this.attemptReconnect();
1517
+ };
1518
+ /**
1519
+ * Attempt to reconnect the WebSocket connection with exponential backoff.
1520
+ * Called automatically when the connection closes unexpectedly.
1521
+ */
1522
+ attemptReconnect = () => {
1523
+ if (this.wsReconnectAttempts >= this.webSocketMaxReconnectAttempts) {
1524
+ this.debug(
1525
+ `Maximum reconnection attempts (${this.webSocketMaxReconnectAttempts}) reached, giving up`
1526
+ );
1527
+ return;
1528
+ }
1529
+ if (this.wsReconnectTimer !== null) {
1530
+ clearTimeout(this.wsReconnectTimer);
1531
+ }
1532
+ const delay = this.calculateReconnectDelay();
1533
+ this.debug(
1534
+ `Scheduling reconnection attempt ${this.wsReconnectAttempts + 1}/${this.webSocketMaxReconnectAttempts} in ${delay.toFixed(0)}ms`
1535
+ );
1536
+ this.wsReconnectTimer = setTimeout(async () => {
1537
+ this.wsReconnectTimer = null;
1538
+ this.wsReconnectAttempts++;
1539
+ this.debug(
1540
+ `Attempting to reconnect (attempt ${this.wsReconnectAttempts}/${this.webSocketMaxReconnectAttempts})`
1541
+ );
1542
+ try {
1543
+ this.conn = this.wsConnect();
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
+ });
1550
+ if (this.context.company !== void 0 || this.context.user !== void 0) {
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`);
1555
+ }
1556
+ this.flushEventQueue().catch((error) => {
1557
+ this.debug("Error flushing event queue after websocket reconnection:", error);
1558
+ });
1559
+ this.debug(`Reconnection successful`);
1560
+ } catch (error) {
1561
+ this.debug(`Reconnection attempt failed:`, error);
1562
+ }
1563
+ }, delay);
1564
+ };
1337
1565
  // Open a websocket connection
1338
1566
  wsConnect = () => {
1339
1567
  if (this.isOffline()) {
@@ -1346,20 +1574,103 @@ var Schematic = class {
1346
1574
  const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
1347
1575
  this.debug(`connecting to WebSocket:`, wsUrl);
1348
1576
  const webSocket = new WebSocket(wsUrl);
1577
+ let timeoutId = null;
1578
+ let isResolved = false;
1579
+ timeoutId = setTimeout(() => {
1580
+ if (!isResolved) {
1581
+ this.debug(
1582
+ `WebSocket connection timeout after ${this.webSocketConnectionTimeout}ms`
1583
+ );
1584
+ webSocket.close();
1585
+ reject(new Error("WebSocket connection timeout"));
1586
+ }
1587
+ }, this.webSocketConnectionTimeout);
1349
1588
  webSocket.onopen = () => {
1589
+ isResolved = true;
1590
+ if (timeoutId !== null) {
1591
+ clearTimeout(timeoutId);
1592
+ }
1593
+ this.wsReconnectAttempts = 0;
1594
+ this.wsIntentionalDisconnect = false;
1350
1595
  this.debug(`WebSocket connection opened`);
1351
1596
  resolve(webSocket);
1352
1597
  };
1353
1598
  webSocket.onerror = (error) => {
1599
+ isResolved = true;
1600
+ if (timeoutId !== null) {
1601
+ clearTimeout(timeoutId);
1602
+ }
1354
1603
  this.debug(`WebSocket connection error:`, error);
1355
1604
  reject(error);
1356
1605
  };
1357
1606
  webSocket.onclose = () => {
1607
+ isResolved = true;
1608
+ if (timeoutId !== null) {
1609
+ clearTimeout(timeoutId);
1610
+ }
1358
1611
  this.debug(`WebSocket connection closed`);
1359
1612
  this.conn = null;
1613
+ if (!this.wsIntentionalDisconnect && this.webSocketReconnect) {
1614
+ this.attemptReconnect();
1615
+ }
1360
1616
  };
1361
1617
  });
1362
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
+ };
1363
1674
  // Send a message on the websocket indicating interest in a particular evaluation context
1364
1675
  // and wait for the initial set of flag values to be returned
1365
1676
  wsSendMessage = (socket, context) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schematichq/schematic-js",
3
- "version": "1.2.6",
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",
@@ -27,8 +27,9 @@
27
27
  "lint": "eslint src --report-unused-disable-directives --fix",
28
28
  "openapi": "rm -rf src/types/api/ && npx openapi-generator-cli generate -c openapi-config.yaml --global-property models=\"EventBody:EventBodyFlagCheck:EventBodyIdentify:EventBodyIdentifyCompany:EventBodyTrack:CheckFlagResponse:CheckFlagResponseData:CheckFlagsResponse:CheckFlagsResponseData\",supportingFiles=runtime.ts && prettier --write \"src/types/api/**/*.{ts,tsx}\"",
29
29
  "prepare": "husky",
30
- "test": "jest --config jest.config.js",
31
- "test:reactnative": "jest --config jest.config.reactnative.js",
30
+ "test": "vitest run",
31
+ "test:reactnative": "vitest run --config vitest.config.reactnative.ts",
32
+ "test:watch": "vitest",
32
33
  "tsc": "npx tsc"
33
34
  },
34
35
  "dependencies": {
@@ -36,25 +37,21 @@
36
37
  "uuid": "^13.0.0"
37
38
  },
38
39
  "devDependencies": {
39
- "@eslint/js": "^9.24.0",
40
- "@microsoft/api-extractor": "^7.52.2",
41
- "@openapitools/openapi-generator-cli": "^2.23.4",
42
- "@types/jest": "^30.0.0",
43
- "@types/uuid": "^11.0.0",
44
- "esbuild": "^0.25.10",
45
- "esbuild-jest": "^0.5.0",
46
- "eslint": "^9.24.0",
47
- "globals": "^16.0.0",
40
+ "@eslint/js": "^9.39.1",
41
+ "@microsoft/api-extractor": "^7.55.0",
42
+ "@openapitools/openapi-generator-cli": "^2.25.1",
43
+ "@vitest/browser": "^4.0.8",
44
+ "esbuild": "^0.27.0",
45
+ "eslint": "^9.39.1",
46
+ "globals": "^16.5.0",
47
+ "happy-dom": "^20.0.10",
48
48
  "husky": "^9.1.7",
49
- "jest": "^30.0.0",
50
- "jest-environment-jsdom": "^30.0.0",
51
- "jest-esbuild": "^0.4.0",
52
- "jest-fetch-mock": "^3.0.3",
49
+ "jsdom": "^27.2.0",
53
50
  "mock-socket": "^9.3.1",
54
51
  "prettier": "^3.6.2",
55
- "ts-jest": "^29.4.3",
56
- "typescript": "^5.9.2",
57
- "typescript-eslint": "^8.44.0"
52
+ "typescript": "^5.9.3",
53
+ "typescript-eslint": "^8.47.0",
54
+ "vitest": "^4.0.8"
58
55
  },
59
56
  "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
60
57
  }