@schematichq/schematic-js 1.2.6 → 1.2.7

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 H=Object.defineProperty;var se=Object.getOwnPropertyDescriptor;var ie=Object.getOwnPropertyNames;var ae=Object.getPrototypeOf,oe=Object.prototype.hasOwnProperty;var ce=(r,e)=>()=>(e||r((e={exports:{}}).exports,e),e.exports);var le=(r,e,t,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of ie(e))!oe.call(r,i)&&i!==t&&H(r,i,{get:()=>e[i],enumerable:!(s=se(e,i))||s.enumerable});return r};var ue=(r,e,t)=>(t=r!=null?re(ae(r)):{},le(e||!r||!r.__esModule?H(t,"default",{value:r,enumerable:!0}):t,r));var z=ce(Q=>{(function(r){var e=(function(t){var s=typeof globalThis<"u"&&globalThis||typeof r<"u"&&r||typeof global<"u"&&global||{},i={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(i.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 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 w(n){var a={next:function(){var u=n.shift();return{done:u===void 0,value:u}}};return i.iterable&&(a[Symbol.iterator]=function(){return a}),a}function p(n){this.map={},n instanceof p?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)}p.prototype.append=function(n,a){n=f(n),a=y(a);var u=this.map[n];this.map[n]=u?u+", "+a:a},p.prototype.delete=function(n){delete this.map[f(n)]},p.prototype.get=function(n){return n=f(n),this.has(n)?this.map[n]:null},p.prototype.has=function(n){return this.map.hasOwnProperty(f(n))},p.prototype.set=function(n,a){this.map[f(n)]=y(a)},p.prototype.forEach=function(n,a){for(var u in this.map)this.map.hasOwnProperty(u)&&n.call(a,this.map[u],u,this)},p.prototype.keys=function(){var n=[];return this.forEach(function(a,u){n.push(u)}),w(n)},p.prototype.values=function(){var n=[];return this.forEach(function(a){n.push(a)}),w(n)},p.prototype.entries=function(){var n=[];return this.forEach(function(a,u){n.push([u,a])}),w(n)},i.iterable&&(p.prototype[Symbol.iterator]=p.prototype.entries);function v(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 R(n){var a=new FileReader,u=m(a);return a.readAsArrayBuffer(n),u}function V(n){var a=new FileReader,u=m(a),h=/charset=([A-Za-z0-9_-]+)/.exec(n.type),g=h?h[1]:"utf-8";return a.readAsText(n,g),u}function Y(n){for(var a=new Uint8Array(n),u=new Array(a.length),h=0;h<a.length;h++)u[h]=String.fromCharCode(a[h]);return u.join("")}function $(n){if(n.slice)return n.slice(0);var a=new Uint8Array(n.byteLength);return a.set(new Uint8Array(n)),a.buffer}function q(){return this.bodyUsed=!1,this._initBody=function(n){this.bodyUsed=this.bodyUsed,this._bodyInit=n,n?typeof n=="string"?this._bodyText=n:i.blob&&Blob.prototype.isPrototypeOf(n)?this._bodyBlob=n:i.formData&&FormData.prototype.isPrototypeOf(n)?this._bodyFormData=n:i.searchParams&&URLSearchParams.prototype.isPrototypeOf(n)?this._bodyText=n.toString():i.arrayBuffer&&i.blob&&c(n)?(this._bodyArrayBuffer=$(n.buffer),this._bodyInit=new Blob([this._bodyArrayBuffer])):i.arrayBuffer&&(ArrayBuffer.prototype.isPrototypeOf(n)||l(n))?this._bodyArrayBuffer=$(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):i.searchParams&&URLSearchParams.prototype.isPrototypeOf(n)&&this.headers.set("content-type","application/x-www-form-urlencoded;charset=UTF-8"))},i.blob&&(this.blob=function(){var n=v(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=v(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(i.blob)return this.blob().then(R);throw new Error("could not read as ArrayBuffer")}},this.text=function(){var n=v(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)},i.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 C(n,a){if(!(this instanceof C))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 C){if(n.bodyUsed)throw new TypeError("Already read");this.url=n.url,this.credentials=n.credentials,a.headers||(this.headers=new p(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 p(a.headers)),this.method=j(a.method||this.method||"GET"),this.mode=a.mode||this.mode||null,this.signal=a.signal||this.signal||(function(){if("AbortController"in s){var d=new AbortController;return d.signal}})(),this.referrer=null,(this.method==="GET"||this.method==="HEAD")&&u)throw new TypeError("Body not allowed for GET or HEAD requests");if(this._initBody(u),(this.method==="GET"||this.method==="HEAD")&&(a.cache==="no-store"||a.cache==="no-cache")){var h=/([?&])_=[^&]*/;if(h.test(this.url))this.url=this.url.replace(h,"$1_="+new Date().getTime());else{var g=/\?/;this.url+=(g.test(this.url)?"&":"?")+"_="+new Date().getTime()}}}C.prototype.clone=function(){return new C(this,{body:this._bodyInit})};function ee(n){var a=new FormData;return n.trim().split("&").forEach(function(u){if(u){var h=u.split("="),g=h.shift().replace(/\+/g," "),d=h.join("=").replace(/\+/g," ");a.append(decodeURIComponent(g),decodeURIComponent(d))}}),a}function te(n){var a=new p,u=n.replace(/\r?\n[\t ]+/g," ");return u.split("\r").map(function(h){return h.indexOf(`
2
+ `)===0?h.substr(1,h.length):h}).forEach(function(h){var g=h.split(":"),d=g.shift().trim();if(d){var _=g.join(":").trim();try{a.append(d,_)}catch(I){console.warn("Response "+I.message)}}}),a}q.call(C.prototype);function E(n,a){if(!(this instanceof E))throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.');if(a||(a={}),this.type="default",this.status=a.status===void 0?200:a.status,this.status<200||this.status>599)throw new RangeError("Failed to construct 'Response': The status provided (0) is outside the range [200, 599].");this.ok=this.status>=200&&this.status<300,this.statusText=a.statusText===void 0?"":""+a.statusText,this.headers=new p(a.headers),this.url=a.url||"",this._initBody(n)}q.call(E.prototype),E.prototype.clone=function(){return new E(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new p(this.headers),url:this.url})},E.error=function(){var n=new E(null,{status:200,statusText:""});return n.ok=!1,n.status=0,n.type="error",n};var ne=[301,302,303,307,308];E.redirect=function(n,a){if(ne.indexOf(a)===-1)throw new RangeError("Invalid status code");return new E(null,{status:a,headers:{location:n}})},t.DOMException=s.DOMException;try{new t.DOMException}catch{t.DOMException=function(a,u){this.message=a,this.name=u;var h=Error(a);this.stack=h.stack},t.DOMException.prototype=Object.create(Error.prototype),t.DOMException.prototype.constructor=t.DOMException}function A(n,a){return new Promise(function(u,h){var g=new C(n,a);if(g.signal&&g.signal.aborted)return h(new t.DOMException("Aborted","AbortError"));var d=new XMLHttpRequest;function _(){d.abort()}d.onload=function(){var b={statusText:d.statusText,headers:te(d.getAllResponseHeaders()||"")};g.url.indexOf("file://")===0&&(d.status<200||d.status>599)?b.status=200:b.status=d.status,b.url="responseURL"in d?d.responseURL:b.headers.get("X-Request-URL");var S="response"in d?d.response:d.responseText;setTimeout(function(){u(new E(S,b))},0)},d.onerror=function(){setTimeout(function(){h(new TypeError("Network request failed"))},0)},d.ontimeout=function(){setTimeout(function(){h(new TypeError("Network request timed out"))},0)},d.onabort=function(){setTimeout(function(){h(new t.DOMException("Aborted","AbortError"))},0)};function I(b){try{return b===""&&s.location.href?s.location.href:b}catch{return b}}if(d.open(g.method,I(g.url),!0),g.credentials==="include"?d.withCredentials=!0:g.credentials==="omit"&&(d.withCredentials=!1),"responseType"in d&&(i.blob?d.responseType="blob":i.arrayBuffer&&(d.responseType="arraybuffer")),a&&typeof a.headers=="object"&&!(a.headers instanceof p||s.Headers&&a.headers instanceof s.Headers)){var W=[];Object.getOwnPropertyNames(a.headers).forEach(function(b){W.push(f(b)),d.setRequestHeader(b,y(a.headers[b]))}),g.headers.forEach(function(b,S){W.indexOf(S)===-1&&d.setRequestHeader(S,b)})}else g.headers.forEach(function(b,S){d.setRequestHeader(S,b)});g.signal&&(g.signal.addEventListener("abort",_),d.onreadystatechange=function(){d.readyState===4&&g.signal.removeEventListener("abort",_)}),d.send(typeof g._bodyInit>"u"?null:g._bodyInit)})}return A.polyfill=!0,s.fetch||(s.fetch=A,s.Headers=p,s.Request=C,s.Response=E),t.Headers=p,t.Request=C,t.Response=E,t.fetch=A,t})({})})(typeof self<"u"?self:Q)});var k=[];for(let r=0;r<256;++r)k.push((r+256).toString(16).slice(1));function K(r,e=0){return(k[r[e+0]]+k[r[e+1]]+k[r[e+2]]+k[r[e+3]]+"-"+k[r[e+4]]+k[r[e+5]]+"-"+k[r[e+6]]+k[r[e+7]]+"-"+k[r[e+8]]+k[r[e+9]]+"-"+k[r[e+10]]+k[r[e+11]]+k[r[e+12]]+k[r[e+13]]+k[r[e+14]]+k[r[e+15]]).toLowerCase()}var P,de=new Uint8Array(16);function B(){if(!P){if(typeof crypto>"u"||!crypto.getRandomValues)throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");P=crypto.getRandomValues.bind(crypto)}return P(de)}var fe=typeof crypto<"u"&&crypto.randomUUID&&crypto.randomUUID.bind(crypto),L={randomUUID:fe};function he(r,e,t){r=r||{};let s=r.random??r.rng?.()??B();if(s.length<16)throw new Error("Random bytes length must be >= 16");if(s[6]=s[6]&15|64,s[8]=s[8]&63|128,e){if(t=t||0,t<0||t+16>e.length)throw new RangeError(`UUID byte range ${t}:${t+15} is out of buffer bounds`);for(let i=0;i<16;++i)e[t+i]=s[i];return e}return K(s)}function pe(r,e,t){return L.randomUUID&&!e&&!r?L.randomUUID():he(r,e,t)}var x=pe;var st=ue(z());function T(r){return ge(r,!1)}function ge(r,e){return r==null?r:{companyId:r.company_id==null?void 0:r.company_id,error:r.error==null?void 0:r.error,featureAllocation:r.feature_allocation==null?void 0:r.feature_allocation,featureUsage:r.feature_usage==null?void 0:r.feature_usage,featureUsageEvent:r.feature_usage_event==null?void 0:r.feature_usage_event,featureUsagePeriod:r.feature_usage_period==null?void 0:r.feature_usage_period,featureUsageResetAt:r.feature_usage_reset_at==null?void 0:new Date(r.feature_usage_reset_at),flag:r.flag,flagId:r.flag_id==null?void 0:r.flag_id,reason:r.reason,ruleId:r.rule_id==null?void 0:r.rule_id,ruleType:r.rule_type==null?void 0:r.rule_type,userId:r.user_id==null?void 0:r.user_id,value:r.value}}function N(r){return ye(r,!1)}function ye(r,e=!1){return r==null?r:{company_id:r.companyId,error:r.error,flag_id:r.flagId,flag_key:r.flagKey,reason:r.reason,req_company:r.reqCompany,req_user:r.reqUser,rule_id:r.ruleId,user_id:r.userId,value:r.value}}function D(r){return be(r,!1)}function be(r,e){return r==null?r:{data:T(r.data),params:r.params}}function X(r){return ke(r,!1)}function ke(r,e){return r==null?r:{flags:r.flags.map(T)}}function M(r){return ve(r,!1)}function ve(r,e){return r==null?r:{data:X(r.data),params:r.params}}var U=r=>{let{companyId:e,error:t,featureAllocation:s,featureUsage:i,featureUsageEvent:c,featureUsagePeriod:o,featureUsageResetAt:l,flag:f,flagId:y,reason:w,ruleId:p,ruleType:v,userId:m,value:R}=T(r);return{featureUsageExceeded:!R&&(v=="company_override_usage_exceeded"||v=="plan_entitlement_usage_exceeded"),companyId:e??void 0,error:t??void 0,featureAllocation:s??void 0,featureUsage:i??void 0,featureUsageEvent:c===null?void 0:c,featureUsagePeriod:o??void 0,featureUsageResetAt:l??void 0,flag:f,flagId:y??void 0,reason:w,ruleId:p??void 0,ruleType:v??void 0,userId:m??void 0,value:R}};function F(r){let e=Object.keys(r).reduce((t,s)=>{let c=Object.keys(r[s]||{}).sort().reduce((o,l)=>(o[l]=r[s][l],o),{});return t[s]=c,t},{});return JSON.stringify(e)}var J="1.2.7";var G="schematicId";var O=class{additionalHeaders={};apiKey;apiUrl="https://api.schematichq.com";conn=null;context={};debugEnabled=!1;offlineEnabled=!1;eventQueue;contextDependentEventQueue;eventUrl="https://c.schematichq.com";flagCheckListeners={};flagValueListeners={};isPending=!0;isPendingListeners=new Set;storage;useWebSocket=!1;checks={};featureUsageEventMap={};webSocketUrl="wss://api.schematichq.com";webSocketConnectionTimeout=1e4;webSocketReconnect=!0;webSocketMaxReconnectAttempts=7;webSocketInitialRetryDelay=1e3;webSocketMaxRetryDelay=3e4;wsReconnectAttempts=0;wsReconnectTimer=null;wsIntentionalDisconnect=!1;constructor(e,t){if(this.apiKey=e,this.eventQueue=[],this.contextDependentEventQueue=[],this.useWebSocket=t?.useWebSocket??!1,this.debugEnabled=t?.debug??!1,this.offlineEnabled=t?.offline??!1,typeof window<"u"&&typeof window.location<"u"){let s=new URLSearchParams(window.location.search),i=s.get("schematic_debug");i!==null&&(i===""||i==="true"||i==="1")&&(this.debugEnabled=!0);let c=s.get("schematic_offline");c!==null&&(c===""||c==="true"||c==="1")&&(this.offlineEnabled=!0,this.debugEnabled=!0)}this.offlineEnabled&&t?.debug!==!1&&(this.debugEnabled=!0),this.offlineEnabled&&this.setIsPending(!1),this.additionalHeaders={"X-Schematic-Client-Version":`schematic-js@${J}`,...t?.additionalHeaders??{}},t?.storage?this.storage=t.storage:typeof localStorage<"u"&&(this.storage=localStorage),t?.apiUrl!==void 0&&(this.apiUrl=t.apiUrl),t?.eventUrl!==void 0&&(this.eventUrl=t.eventUrl),t?.webSocketUrl!==void 0&&(this.webSocketUrl=t.webSocketUrl),t?.webSocketConnectionTimeout!==void 0&&(this.webSocketConnectionTimeout=t.webSocketConnectionTimeout),t?.webSocketReconnect!==void 0&&(this.webSocketReconnect=t.webSocketReconnect),t?.webSocketMaxReconnectAttempts!==void 0&&(this.webSocketMaxReconnectAttempts=t.webSocketMaxReconnectAttempts),t?.webSocketInitialRetryDelay!==void 0&&(this.webSocketInitialRetryDelay=t.webSocketInitialRetryDelay),t?.webSocketMaxRetryDelay!==void 0&&(this.webSocketMaxRetryDelay=t.webSocketMaxRetryDelay),typeof window<"u"&&window?.addEventListener&&(window.addEventListener("beforeunload",()=>{this.flushEventQueue(),this.flushContextDependentEventQueue()}),this.useWebSocket&&(window.addEventListener("offline",()=>{this.debug("Browser went offline, closing WebSocket connection"),this.handleNetworkOffline()}),window.addEventListener("online",()=>{this.debug("Browser came online, attempting to reconnect WebSocket"),this.handleNetworkOnline()}))),this.offlineEnabled?this.debug("Initialized with offline mode enabled - no network requests will be made"):this.debugEnabled&&this.debug("Initialized with debug mode enabled")}async checkFlag(e){let{fallback:t=!1,key:s}=e,i=e.context||this.context,c=F(i);if(this.debug(`checkFlag: ${s}`,{context:i,fallback:t}),this.isOffline())return this.debug(`checkFlag offline result: ${s}`,{value:t,offlineMode:!0}),t;if(!this.useWebSocket){let o=`${this.apiUrl}/flags/${s}/check`;return fetch(o,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8","X-Schematic-Api-Key":this.apiKey},body:JSON.stringify(i)}).then(l=>{if(!l.ok)throw new Error("Network response was not ok");return l.json()}).then(l=>{let f=D(l);this.debug(`checkFlag result: ${s}`,f);let y=U(f.data);return typeof y.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(y),this.submitFlagCheckEvent(s,y,i),y.value}).catch(l=>{console.error("There was a problem with the fetch operation:",l);let f={flag:s,value:t,reason:"API request failed",error:l instanceof Error?l.message:String(l)};return this.submitFlagCheckEvent(s,f,i),t})}try{let o=this.checks[c];if(this.conn!==null&&typeof o<"u"&&typeof o[s]<"u")return this.debug(`checkFlag cached result: ${s}`,o[s]),o[s].value;if(this.isOffline())return t;try{await this.setContext(i)}catch(w){return console.error("WebSocket connection failed, falling back to REST:",w),this.fallbackToRest(s,i,t)}let f=(this.checks[c]??{})[s],y=f?.value??t;return this.debug(`checkFlag WebSocket result: ${s}`,typeof f<"u"?f:{value:t,fallbackUsed:!0}),typeof f<"u"&&this.submitFlagCheckEvent(s,f,i),y}catch(o){console.error("Unexpected error in checkFlag:",o);let l={flag:s,value:t,reason:"Unexpected error in flag check",error:o instanceof Error?o.message:String(o)};return this.submitFlagCheckEvent(s,l,i),t}}debug(e,...t){this.debugEnabled&&console.log(`[Schematic] ${e}`,...t)}isOffline(){return this.offlineEnabled}submitFlagCheckEvent(e,t,s){let i={flagKey:e,value:t.value,reason:t.reason,flagId:t.flagId,ruleId:t.ruleId,companyId:t.companyId,userId:t.userId,error:t.error,reqCompany:s.company,reqUser:s.user};return this.debug("submitting flag check event:",i),this.handleEvent("flag_check",N(i))}async fallbackToRest(e,t,s){if(this.isOffline())return this.debug(`fallbackToRest offline result: ${e}`,{value:s,offlineMode:!0}),s;try{let i=`${this.apiUrl}/flags/${e}/check`,c=await fetch(i,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8","X-Schematic-Api-Key":this.apiKey},body:JSON.stringify(t)});if(!c.ok)throw new Error("Network response was not ok");let o=await c.json(),l=D(o);this.debug(`fallbackToRest result: ${e}`,l);let f=U(l.data);return typeof f.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(f),this.submitFlagCheckEvent(e,f,t),f.value}catch(i){console.error("REST API call failed, using fallback value:",i);let c={flag:e,value:s,reason:"API request failed (fallback)",error:i instanceof Error?i.message:String(i)};return this.submitFlagCheckEvent(e,c,t),s}}checkFlags=async e=>{if(e=e||this.context,this.debug("checkFlags",{context:e}),this.isOffline())return this.debug("checkFlags offline result: returning empty object"),{};let t=`${this.apiUrl}/flags/check`,s=JSON.stringify(e);return fetch(t,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8","X-Schematic-Api-Key":this.apiKey},body:s}).then(i=>{if(!i.ok)throw new Error("Network response was not ok");return i.json()}).then(i=>{let c=M(i);return this.debug("checkFlags result:",c),(c?.data?.flags??[]).reduce((o,l)=>(o[l.flag]=l.value,o),{})}).catch(i=>(console.error("There was a problem with the fetch operation:",i),{}))};identify=e=>{this.debug("identify:",e);try{this.setContext({company:e.company?.keys,user:e.keys})}catch(t){console.error("Error setting context:",t)}return this.handleEvent("identify",e)};setContext=async e=>{if(this.isOffline()||!this.useWebSocket)return this.context=e,this.flushContextDependentEventQueue(),this.setIsPending(!1),Promise.resolve();try{this.setIsPending(!0),this.conn||(this.wsReconnectTimer!==null&&(this.debug("Cancelling scheduled reconnection, connecting immediately"),clearTimeout(this.wsReconnectTimer),this.wsReconnectTimer=null),this.conn=this.wsConnect());let t=await this.conn;await this.wsSendMessage(t,e)}catch(t){throw console.error("Failed to establish WebSocket connection:",t),t}};track=e=>{let{company:t,user:s,event:i,traits:c,quantity:o=1}=e;if(!this.hasContext(t,s)){this.debug(`track: queuing event "${i}" until context is available`);let f={api_key:this.apiKey,body:{company:t,event:i,traits:c??{},user:s,quantity:o},sent_at:new Date().toISOString(),tracker_event_id:x(),tracker_user_id:this.getAnonymousId(),type:"track"};return this.contextDependentEventQueue.push(f),Promise.resolve()}let l={company:t??this.context.company,event:i,traits:c??{},user:s??this.context.user,quantity:o};return this.debug("track:",l),i in this.featureUsageEventMap&&this.optimisticallyUpdateFeatureUsage(i,o),this.handleEvent("track",l)};optimisticallyUpdateFeatureUsage=(e,t=1)=>{let s=this.featureUsageEventMap[e];s!=null&&(this.debug(`Optimistically updating feature usage for event: ${e}`,{quantity:t}),Object.entries(s).forEach(([i,c])=>{if(c===void 0)return;let o={...c};if(typeof o.featureUsage=="number"){if(o.featureUsage+=t,typeof o.featureAllocation=="number"){let f=o.featureUsageExceeded===!0,y=o.featureUsage>=o.featureAllocation;y!==f&&(o.featureUsageExceeded=y,y&&(o.value=!1),this.debug(`Usage limit status changed for flag: ${i}`,{was:f?"exceeded":"within limits",now:y?"exceeded":"within limits",featureUsage:o.featureUsage,featureAllocation:o.featureAllocation,value:o.value}))}this.featureUsageEventMap[e]!==void 0&&(this.featureUsageEventMap[e][i]=o);let l=F(this.context);this.checks[l]!==void 0&&this.checks[l]!==null&&(this.checks[l][i]=o),this.notifyFlagCheckListeners(i,o),this.notifyFlagValueListeners(i,o.value)}}))};hasContext=(e,t)=>{let s=e!=null&&Object.keys(e).length>0||t!=null&&Object.keys(t).length>0,i=this.context.company!==void 0&&this.context.company!==null&&Object.keys(this.context.company).length>0||this.context.user!==void 0&&this.context.user!==null&&Object.keys(this.context.user).length>0;return s||i};flushContextDependentEventQueue=()=>{for(this.debug(`flushing ${this.contextDependentEventQueue.length} context-dependent events`);this.contextDependentEventQueue.length>0;){let e=this.contextDependentEventQueue.shift();if(e)if(e.type==="track"&&typeof e.body=="object"&&e.body!==null){let t=e.body,s={...t,company:t.company??this.context.company,user:t.user??this.context.user},i={...e,body:s,sent_at:new Date().toISOString()};this.sendEvent(i)}else this.sendEvent(e)}};flushEventQueue=()=>{for(;this.eventQueue.length>0;){let e=this.eventQueue.shift();e&&this.sendEvent(e)}};getAnonymousId=()=>{if(!this.storage)return x();let e=this.storage.getItem(G);if(typeof e<"u")return e;let t=x();return this.storage.setItem(G,t),t};handleEvent=(e,t)=>{let s={api_key:this.apiKey,body:t,sent_at:new Date().toISOString(),tracker_event_id:x(),tracker_user_id:this.getAnonymousId(),type:e};return typeof document<"u"&&document?.hidden?this.storeEvent(s):this.sendEvent(s)};sendEvent=async e=>{let t=`${this.eventUrl}/e`,s=JSON.stringify(e);if(this.debug("sending event:",{url:t,event:e}),this.isOffline())return this.debug("event not sent (offline mode):",{event:e}),Promise.resolve();try{let i=await fetch(t,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8"},body:s});this.debug("event sent:",{status:i.status,statusText:i.statusText})}catch(i){console.error("Error sending Schematic event: ",i)}return Promise.resolve()};storeEvent=e=>(this.eventQueue.push(e),Promise.resolve());cleanup=async()=>{if(this.isOffline())return this.debug("cleanup: skipped (offline mode)"),Promise.resolve();if(this.wsIntentionalDisconnect=!0,this.wsReconnectTimer!==null&&(clearTimeout(this.wsReconnectTimer),this.wsReconnectTimer=null),this.conn)try{(await this.conn).close()}catch(e){console.error("Error during cleanup:",e)}finally{this.conn=null}};calculateReconnectDelay=()=>{let e=this.webSocketInitialRetryDelay*Math.pow(2,this.wsReconnectAttempts),t=Math.min(e,this.webSocketMaxRetryDelay),s=Math.random()*t*.5,i=t+s;return this.debug(`Reconnect delay calculated: ${i.toFixed(0)}ms (attempt ${this.wsReconnectAttempts+1}/${this.webSocketMaxReconnectAttempts})`),i};handleNetworkOffline=async()=>{if(this.conn!==null){try{(await this.conn).close()}catch(e){this.debug("Error closing connection on offline:",e)}this.conn=null}this.wsReconnectTimer!==null&&(clearTimeout(this.wsReconnectTimer),this.wsReconnectTimer=null)};handleNetworkOnline=()=>{if(this.context.company===void 0&&this.context.user===void 0){this.debug("No context set, skipping reconnection");return}this.wsReconnectAttempts=0,this.wsReconnectTimer!==null&&(clearTimeout(this.wsReconnectTimer),this.wsReconnectTimer=null),this.debug("Network online, reconnecting immediately"),this.attemptReconnect()};attemptReconnect=()=>{if(this.wsReconnectAttempts>=this.webSocketMaxReconnectAttempts){this.debug(`Maximum reconnection attempts (${this.webSocketMaxReconnectAttempts}) reached, giving up`);return}this.wsReconnectTimer!==null&&clearTimeout(this.wsReconnectTimer);let e=this.calculateReconnectDelay();this.debug(`Scheduling reconnection attempt ${this.wsReconnectAttempts+1}/${this.webSocketMaxReconnectAttempts} in ${e.toFixed(0)}ms`),this.wsReconnectTimer=setTimeout(async()=>{this.wsReconnectTimer=null,this.wsReconnectAttempts++,this.debug(`Attempting to reconnect (attempt ${this.wsReconnectAttempts}/${this.webSocketMaxReconnectAttempts})`);try{this.conn=this.wsConnect();let t=await this.conn;(this.context.company!==void 0||this.context.user!==void 0)&&(this.debug("Reconnected, re-sending context"),await this.wsSendMessage(t,this.context)),this.debug("Reconnection successful")}catch(t){this.debug("Reconnection attempt failed:",t)}},e)};wsConnect=()=>this.isOffline()?(this.debug("wsConnect: skipped (offline mode)"),Promise.reject(new Error("WebSocket connection skipped in offline mode"))):new Promise((e,t)=>{let s=`${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;this.debug("connecting to WebSocket:",s);let i=new WebSocket(s),c=null,o=!1;c=setTimeout(()=>{o||(this.debug(`WebSocket connection timeout after ${this.webSocketConnectionTimeout}ms`),i.close(),t(new Error("WebSocket connection timeout")))},this.webSocketConnectionTimeout),i.onopen=()=>{o=!0,c!==null&&clearTimeout(c),this.wsReconnectAttempts=0,this.wsIntentionalDisconnect=!1,this.debug("WebSocket connection opened"),e(i)},i.onerror=l=>{o=!0,c!==null&&clearTimeout(c),this.debug("WebSocket connection error:",l),t(l)},i.onclose=()=>{o=!0,c!==null&&clearTimeout(c),this.debug("WebSocket connection closed"),this.conn=null,!this.wsIntentionalDisconnect&&this.webSocketReconnect&&this.attemptReconnect()}});wsSendMessage=(e,t)=>this.isOffline()?(this.debug("wsSendMessage: skipped (offline mode)"),this.setIsPending(!1),Promise.resolve()):new Promise((s,i)=>{if(F(t)==F(this.context))return this.debug("WebSocket context unchanged, skipping update"),s(this.setIsPending(!1));this.debug("WebSocket context updated:",t),this.context=t;let c=()=>{let o=!1,l=w=>{let p=JSON.parse(w.data);this.debug("WebSocket message received:",p),F(t)in this.checks||(this.checks[F(t)]={}),(p.flags??[]).forEach(v=>{let m=U(v),R=F(t);this.checks[R]===void 0&&(this.checks[R]={}),this.checks[R][m.flag]=m,this.debug("WebSocket flag update:",{flag:m.flag,value:m.value,flagCheck:m}),typeof m.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(m),(this.flagCheckListeners[v.flag]?.size>0||this.flagValueListeners[v.flag]?.size>0)&&this.submitFlagCheckEvent(m.flag,m,t),this.notifyFlagCheckListeners(v.flag,m),this.notifyFlagValueListeners(v.flag,m.value)}),this.flushContextDependentEventQueue(),this.setIsPending(!1),o||(o=!0,s())};e.addEventListener("message",l);let f=this.additionalHeaders["X-Schematic-Client-Version"]??`schematic-js@${J}`,y={apiKey:this.apiKey,clientVersion:f,data:t};this.debug("WebSocket sending message:",y),e.send(JSON.stringify(y))};e.readyState===WebSocket.OPEN?(this.debug("WebSocket already open, sending message"),c()):e.readyState===WebSocket.CONNECTING?(this.debug("WebSocket connecting, waiting for open to send message"),e.addEventListener("open",c)):(this.debug("WebSocket is closed, cannot send message"),i("WebSocket is not open or connecting"))});getIsPending=()=>this.isPending;addIsPendingListener=e=>(this.isPendingListeners.add(e),()=>{this.isPendingListeners.delete(e)});setIsPending=e=>{this.isPending=e,this.isPendingListeners.forEach(t=>Ee(t,e))};getFlagCheck=e=>{let t=F(this.context);return(this.checks[t]??{})[e]};getFlagValue=e=>this.getFlagCheck(e)?.value;addFlagValueListener=(e,t)=>(e in this.flagValueListeners||(this.flagValueListeners[e]=new Set),this.flagValueListeners[e].add(t),()=>{this.flagValueListeners[e].delete(t)});addFlagCheckListener=(e,t)=>(e in this.flagCheckListeners||(this.flagCheckListeners[e]=new Set),this.flagCheckListeners[e].add(t),()=>{this.flagCheckListeners[e].delete(t)});notifyFlagCheckListeners=(e,t)=>{let s=this.flagCheckListeners?.[e]??[];s.size>0&&this.debug(`Notifying ${s.size} flag check listeners for ${e}`,t),typeof t.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(t),s.forEach(i=>we(i,t))};updateFeatureUsageEventMap=e=>{if(typeof e.featureUsageEvent!="string")return;let t=e.featureUsageEvent;(this.featureUsageEventMap[t]===void 0||this.featureUsageEventMap[t]===null)&&(this.featureUsageEventMap[t]={}),this.featureUsageEventMap[t]!==void 0&&(this.featureUsageEventMap[t][e.flag]=e),this.debug(`Updated featureUsageEventMap for event: ${t}, flag: ${e.flag}`,e)};notifyFlagValueListeners=(e,t)=>{let s=this.flagValueListeners?.[e]??[];s.size>0&&this.debug(`Notifying ${s.size} flag value listeners for ${e}`,{value:t}),s.forEach(i=>Fe(i,t))}},Ee=(r,e)=>{r.length>0?r(e):r()},we=(r,e)=>{r.length>0?r(e):r()},Fe=(r,e)=>{r.length>0?r(e):r()};window.Schematic=O;})();
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.7";
804
804
 
805
805
  // src/index.ts
806
806
  var anonymousIdKey = "schematicId";
@@ -824,6 +824,14 @@ 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;
827
835
  constructor(apiKey, options) {
828
836
  this.apiKey = apiKey;
829
837
  this.eventQueue = [];
@@ -867,11 +875,36 @@ var Schematic = class {
867
875
  if (options?.webSocketUrl !== void 0) {
868
876
  this.webSocketUrl = options.webSocketUrl;
869
877
  }
878
+ if (options?.webSocketConnectionTimeout !== void 0) {
879
+ this.webSocketConnectionTimeout = options.webSocketConnectionTimeout;
880
+ }
881
+ if (options?.webSocketReconnect !== void 0) {
882
+ this.webSocketReconnect = options.webSocketReconnect;
883
+ }
884
+ if (options?.webSocketMaxReconnectAttempts !== void 0) {
885
+ this.webSocketMaxReconnectAttempts = options.webSocketMaxReconnectAttempts;
886
+ }
887
+ if (options?.webSocketInitialRetryDelay !== void 0) {
888
+ this.webSocketInitialRetryDelay = options.webSocketInitialRetryDelay;
889
+ }
890
+ if (options?.webSocketMaxRetryDelay !== void 0) {
891
+ this.webSocketMaxRetryDelay = options.webSocketMaxRetryDelay;
892
+ }
870
893
  if (typeof window !== "undefined" && window?.addEventListener) {
871
894
  window.addEventListener("beforeunload", () => {
872
895
  this.flushEventQueue();
873
896
  this.flushContextDependentEventQueue();
874
897
  });
898
+ if (this.useWebSocket) {
899
+ window.addEventListener("offline", () => {
900
+ this.debug("Browser went offline, closing WebSocket connection");
901
+ this.handleNetworkOffline();
902
+ });
903
+ window.addEventListener("online", () => {
904
+ this.debug("Browser came online, attempting to reconnect WebSocket");
905
+ this.handleNetworkOnline();
906
+ });
907
+ }
875
908
  }
876
909
  if (this.offlineEnabled) {
877
910
  this.debug(
@@ -1136,6 +1169,13 @@ var Schematic = class {
1136
1169
  try {
1137
1170
  this.setIsPending(true);
1138
1171
  if (!this.conn) {
1172
+ if (this.wsReconnectTimer !== null) {
1173
+ this.debug(
1174
+ `Cancelling scheduled reconnection, connecting immediately`
1175
+ );
1176
+ clearTimeout(this.wsReconnectTimer);
1177
+ this.wsReconnectTimer = null;
1178
+ }
1139
1179
  this.conn = this.wsConnect();
1140
1180
  }
1141
1181
  const socket = await this.conn;
@@ -1342,6 +1382,11 @@ var Schematic = class {
1342
1382
  this.debug("cleanup: skipped (offline mode)");
1343
1383
  return Promise.resolve();
1344
1384
  }
1385
+ this.wsIntentionalDisconnect = true;
1386
+ if (this.wsReconnectTimer !== null) {
1387
+ clearTimeout(this.wsReconnectTimer);
1388
+ this.wsReconnectTimer = null;
1389
+ }
1345
1390
  if (this.conn) {
1346
1391
  try {
1347
1392
  const socket = await this.conn;
@@ -1353,6 +1398,91 @@ var Schematic = class {
1353
1398
  }
1354
1399
  }
1355
1400
  };
1401
+ /**
1402
+ * Calculate the delay for the next reconnection attempt using exponential backoff with jitter.
1403
+ * This helps prevent dogpiling when the server recovers from an outage.
1404
+ */
1405
+ calculateReconnectDelay = () => {
1406
+ const exponentialDelay = this.webSocketInitialRetryDelay * Math.pow(2, this.wsReconnectAttempts);
1407
+ const cappedDelay = Math.min(exponentialDelay, this.webSocketMaxRetryDelay);
1408
+ const jitter = Math.random() * cappedDelay * 0.5;
1409
+ const totalDelay = cappedDelay + jitter;
1410
+ this.debug(
1411
+ `Reconnect delay calculated: ${totalDelay.toFixed(0)}ms (attempt ${this.wsReconnectAttempts + 1}/${this.webSocketMaxReconnectAttempts})`
1412
+ );
1413
+ return totalDelay;
1414
+ };
1415
+ /**
1416
+ * Handle browser going offline
1417
+ */
1418
+ handleNetworkOffline = async () => {
1419
+ if (this.conn !== null) {
1420
+ try {
1421
+ const socket = await this.conn;
1422
+ socket.close();
1423
+ } catch (error) {
1424
+ this.debug("Error closing connection on offline:", error);
1425
+ }
1426
+ this.conn = null;
1427
+ }
1428
+ if (this.wsReconnectTimer !== null) {
1429
+ clearTimeout(this.wsReconnectTimer);
1430
+ this.wsReconnectTimer = null;
1431
+ }
1432
+ };
1433
+ /**
1434
+ * Handle browser coming back online
1435
+ */
1436
+ handleNetworkOnline = () => {
1437
+ if (this.context.company === void 0 && this.context.user === void 0) {
1438
+ this.debug("No context set, skipping reconnection");
1439
+ return;
1440
+ }
1441
+ this.wsReconnectAttempts = 0;
1442
+ if (this.wsReconnectTimer !== null) {
1443
+ clearTimeout(this.wsReconnectTimer);
1444
+ this.wsReconnectTimer = null;
1445
+ }
1446
+ this.debug("Network online, reconnecting immediately");
1447
+ this.attemptReconnect();
1448
+ };
1449
+ /**
1450
+ * Attempt to reconnect the WebSocket connection with exponential backoff.
1451
+ * Called automatically when the connection closes unexpectedly.
1452
+ */
1453
+ attemptReconnect = () => {
1454
+ if (this.wsReconnectAttempts >= this.webSocketMaxReconnectAttempts) {
1455
+ this.debug(
1456
+ `Maximum reconnection attempts (${this.webSocketMaxReconnectAttempts}) reached, giving up`
1457
+ );
1458
+ return;
1459
+ }
1460
+ if (this.wsReconnectTimer !== null) {
1461
+ clearTimeout(this.wsReconnectTimer);
1462
+ }
1463
+ const delay = this.calculateReconnectDelay();
1464
+ this.debug(
1465
+ `Scheduling reconnection attempt ${this.wsReconnectAttempts + 1}/${this.webSocketMaxReconnectAttempts} in ${delay.toFixed(0)}ms`
1466
+ );
1467
+ this.wsReconnectTimer = setTimeout(async () => {
1468
+ this.wsReconnectTimer = null;
1469
+ this.wsReconnectAttempts++;
1470
+ this.debug(
1471
+ `Attempting to reconnect (attempt ${this.wsReconnectAttempts}/${this.webSocketMaxReconnectAttempts})`
1472
+ );
1473
+ try {
1474
+ this.conn = this.wsConnect();
1475
+ const socket = await this.conn;
1476
+ if (this.context.company !== void 0 || this.context.user !== void 0) {
1477
+ this.debug(`Reconnected, re-sending context`);
1478
+ await this.wsSendMessage(socket, this.context);
1479
+ }
1480
+ this.debug(`Reconnection successful`);
1481
+ } catch (error) {
1482
+ this.debug(`Reconnection attempt failed:`, error);
1483
+ }
1484
+ }, delay);
1485
+ };
1356
1486
  // Open a websocket connection
1357
1487
  wsConnect = () => {
1358
1488
  if (this.isOffline()) {
@@ -1365,17 +1495,45 @@ var Schematic = class {
1365
1495
  const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
1366
1496
  this.debug(`connecting to WebSocket:`, wsUrl);
1367
1497
  const webSocket = new WebSocket(wsUrl);
1498
+ let timeoutId = null;
1499
+ let isResolved = false;
1500
+ timeoutId = setTimeout(() => {
1501
+ if (!isResolved) {
1502
+ this.debug(
1503
+ `WebSocket connection timeout after ${this.webSocketConnectionTimeout}ms`
1504
+ );
1505
+ webSocket.close();
1506
+ reject(new Error("WebSocket connection timeout"));
1507
+ }
1508
+ }, this.webSocketConnectionTimeout);
1368
1509
  webSocket.onopen = () => {
1510
+ isResolved = true;
1511
+ if (timeoutId !== null) {
1512
+ clearTimeout(timeoutId);
1513
+ }
1514
+ this.wsReconnectAttempts = 0;
1515
+ this.wsIntentionalDisconnect = false;
1369
1516
  this.debug(`WebSocket connection opened`);
1370
1517
  resolve(webSocket);
1371
1518
  };
1372
1519
  webSocket.onerror = (error) => {
1520
+ isResolved = true;
1521
+ if (timeoutId !== null) {
1522
+ clearTimeout(timeoutId);
1523
+ }
1373
1524
  this.debug(`WebSocket connection error:`, error);
1374
1525
  reject(error);
1375
1526
  };
1376
1527
  webSocket.onclose = () => {
1528
+ isResolved = true;
1529
+ if (timeoutId !== null) {
1530
+ clearTimeout(timeoutId);
1531
+ }
1377
1532
  this.debug(`WebSocket connection closed`);
1378
1533
  this.conn = null;
1534
+ if (!this.wsIntentionalDisconnect && this.webSocketReconnect) {
1535
+ this.attemptReconnect();
1536
+ }
1379
1537
  };
1380
1538
  });
1381
1539
  };
@@ -368,6 +368,14 @@ export declare class Schematic {
368
368
  private checks;
369
369
  private featureUsageEventMap;
370
370
  private webSocketUrl;
371
+ private webSocketConnectionTimeout;
372
+ private webSocketReconnect;
373
+ private webSocketMaxReconnectAttempts;
374
+ private webSocketInitialRetryDelay;
375
+ private webSocketMaxRetryDelay;
376
+ private wsReconnectAttempts;
377
+ private wsReconnectTimer;
378
+ private wsIntentionalDisconnect;
371
379
  constructor(apiKey: string, options?: SchematicOptions);
372
380
  /**
373
381
  * Get value for a single flag.
@@ -447,6 +455,24 @@ export declare class Schematic {
447
455
  * In offline mode, this is a no-op.
448
456
  */
449
457
  cleanup: () => Promise<void>;
458
+ /**
459
+ * Calculate the delay for the next reconnection attempt using exponential backoff with jitter.
460
+ * This helps prevent dogpiling when the server recovers from an outage.
461
+ */
462
+ private calculateReconnectDelay;
463
+ /**
464
+ * Handle browser going offline
465
+ */
466
+ private handleNetworkOffline;
467
+ /**
468
+ * Handle browser coming back online
469
+ */
470
+ private handleNetworkOnline;
471
+ /**
472
+ * Attempt to reconnect the WebSocket connection with exponential backoff.
473
+ * Called automatically when the connection closes unexpectedly.
474
+ */
475
+ private attemptReconnect;
450
476
  private wsConnect;
451
477
  private wsSendMessage;
452
478
  /**
@@ -493,6 +519,16 @@ export declare type SchematicOptions = {
493
519
  useWebSocket?: boolean;
494
520
  /** Optionally provide a custom WebSocket URL */
495
521
  webSocketUrl?: string;
522
+ /** WebSocket connection timeout in milliseconds (default: 10000) */
523
+ webSocketConnectionTimeout?: number;
524
+ /** Enable automatic reconnection on WebSocket disconnect (default: true) */
525
+ webSocketReconnect?: boolean;
526
+ /** Maximum number of reconnection attempts (default: 7, set to Infinity for unlimited) */
527
+ webSocketMaxReconnectAttempts?: number;
528
+ /** Initial retry delay in milliseconds for exponential backoff (default: 1000) */
529
+ webSocketInitialRetryDelay?: number;
530
+ /** Maximum retry delay in milliseconds for exponential backoff (default: 30000) */
531
+ webSocketMaxRetryDelay?: number;
496
532
  };
497
533
 
498
534
  /** 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.7";
785
785
 
786
786
  // src/index.ts
787
787
  var anonymousIdKey = "schematicId";
@@ -805,6 +805,14 @@ 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;
808
816
  constructor(apiKey, options) {
809
817
  this.apiKey = apiKey;
810
818
  this.eventQueue = [];
@@ -848,11 +856,36 @@ var Schematic = class {
848
856
  if (options?.webSocketUrl !== void 0) {
849
857
  this.webSocketUrl = options.webSocketUrl;
850
858
  }
859
+ if (options?.webSocketConnectionTimeout !== void 0) {
860
+ this.webSocketConnectionTimeout = options.webSocketConnectionTimeout;
861
+ }
862
+ if (options?.webSocketReconnect !== void 0) {
863
+ this.webSocketReconnect = options.webSocketReconnect;
864
+ }
865
+ if (options?.webSocketMaxReconnectAttempts !== void 0) {
866
+ this.webSocketMaxReconnectAttempts = options.webSocketMaxReconnectAttempts;
867
+ }
868
+ if (options?.webSocketInitialRetryDelay !== void 0) {
869
+ this.webSocketInitialRetryDelay = options.webSocketInitialRetryDelay;
870
+ }
871
+ if (options?.webSocketMaxRetryDelay !== void 0) {
872
+ this.webSocketMaxRetryDelay = options.webSocketMaxRetryDelay;
873
+ }
851
874
  if (typeof window !== "undefined" && window?.addEventListener) {
852
875
  window.addEventListener("beforeunload", () => {
853
876
  this.flushEventQueue();
854
877
  this.flushContextDependentEventQueue();
855
878
  });
879
+ if (this.useWebSocket) {
880
+ window.addEventListener("offline", () => {
881
+ this.debug("Browser went offline, closing WebSocket connection");
882
+ this.handleNetworkOffline();
883
+ });
884
+ window.addEventListener("online", () => {
885
+ this.debug("Browser came online, attempting to reconnect WebSocket");
886
+ this.handleNetworkOnline();
887
+ });
888
+ }
856
889
  }
857
890
  if (this.offlineEnabled) {
858
891
  this.debug(
@@ -1117,6 +1150,13 @@ var Schematic = class {
1117
1150
  try {
1118
1151
  this.setIsPending(true);
1119
1152
  if (!this.conn) {
1153
+ if (this.wsReconnectTimer !== null) {
1154
+ this.debug(
1155
+ `Cancelling scheduled reconnection, connecting immediately`
1156
+ );
1157
+ clearTimeout(this.wsReconnectTimer);
1158
+ this.wsReconnectTimer = null;
1159
+ }
1120
1160
  this.conn = this.wsConnect();
1121
1161
  }
1122
1162
  const socket = await this.conn;
@@ -1323,6 +1363,11 @@ var Schematic = class {
1323
1363
  this.debug("cleanup: skipped (offline mode)");
1324
1364
  return Promise.resolve();
1325
1365
  }
1366
+ this.wsIntentionalDisconnect = true;
1367
+ if (this.wsReconnectTimer !== null) {
1368
+ clearTimeout(this.wsReconnectTimer);
1369
+ this.wsReconnectTimer = null;
1370
+ }
1326
1371
  if (this.conn) {
1327
1372
  try {
1328
1373
  const socket = await this.conn;
@@ -1334,6 +1379,91 @@ var Schematic = class {
1334
1379
  }
1335
1380
  }
1336
1381
  };
1382
+ /**
1383
+ * Calculate the delay for the next reconnection attempt using exponential backoff with jitter.
1384
+ * This helps prevent dogpiling when the server recovers from an outage.
1385
+ */
1386
+ calculateReconnectDelay = () => {
1387
+ const exponentialDelay = this.webSocketInitialRetryDelay * Math.pow(2, this.wsReconnectAttempts);
1388
+ const cappedDelay = Math.min(exponentialDelay, this.webSocketMaxRetryDelay);
1389
+ const jitter = Math.random() * cappedDelay * 0.5;
1390
+ const totalDelay = cappedDelay + jitter;
1391
+ this.debug(
1392
+ `Reconnect delay calculated: ${totalDelay.toFixed(0)}ms (attempt ${this.wsReconnectAttempts + 1}/${this.webSocketMaxReconnectAttempts})`
1393
+ );
1394
+ return totalDelay;
1395
+ };
1396
+ /**
1397
+ * Handle browser going offline
1398
+ */
1399
+ handleNetworkOffline = async () => {
1400
+ if (this.conn !== null) {
1401
+ try {
1402
+ const socket = await this.conn;
1403
+ socket.close();
1404
+ } catch (error) {
1405
+ this.debug("Error closing connection on offline:", error);
1406
+ }
1407
+ this.conn = null;
1408
+ }
1409
+ if (this.wsReconnectTimer !== null) {
1410
+ clearTimeout(this.wsReconnectTimer);
1411
+ this.wsReconnectTimer = null;
1412
+ }
1413
+ };
1414
+ /**
1415
+ * Handle browser coming back online
1416
+ */
1417
+ handleNetworkOnline = () => {
1418
+ if (this.context.company === void 0 && this.context.user === void 0) {
1419
+ this.debug("No context set, skipping reconnection");
1420
+ return;
1421
+ }
1422
+ this.wsReconnectAttempts = 0;
1423
+ if (this.wsReconnectTimer !== null) {
1424
+ clearTimeout(this.wsReconnectTimer);
1425
+ this.wsReconnectTimer = null;
1426
+ }
1427
+ this.debug("Network online, reconnecting immediately");
1428
+ this.attemptReconnect();
1429
+ };
1430
+ /**
1431
+ * Attempt to reconnect the WebSocket connection with exponential backoff.
1432
+ * Called automatically when the connection closes unexpectedly.
1433
+ */
1434
+ attemptReconnect = () => {
1435
+ if (this.wsReconnectAttempts >= this.webSocketMaxReconnectAttempts) {
1436
+ this.debug(
1437
+ `Maximum reconnection attempts (${this.webSocketMaxReconnectAttempts}) reached, giving up`
1438
+ );
1439
+ return;
1440
+ }
1441
+ if (this.wsReconnectTimer !== null) {
1442
+ clearTimeout(this.wsReconnectTimer);
1443
+ }
1444
+ const delay = this.calculateReconnectDelay();
1445
+ this.debug(
1446
+ `Scheduling reconnection attempt ${this.wsReconnectAttempts + 1}/${this.webSocketMaxReconnectAttempts} in ${delay.toFixed(0)}ms`
1447
+ );
1448
+ this.wsReconnectTimer = setTimeout(async () => {
1449
+ this.wsReconnectTimer = null;
1450
+ this.wsReconnectAttempts++;
1451
+ this.debug(
1452
+ `Attempting to reconnect (attempt ${this.wsReconnectAttempts}/${this.webSocketMaxReconnectAttempts})`
1453
+ );
1454
+ try {
1455
+ this.conn = this.wsConnect();
1456
+ const socket = await this.conn;
1457
+ if (this.context.company !== void 0 || this.context.user !== void 0) {
1458
+ this.debug(`Reconnected, re-sending context`);
1459
+ await this.wsSendMessage(socket, this.context);
1460
+ }
1461
+ this.debug(`Reconnection successful`);
1462
+ } catch (error) {
1463
+ this.debug(`Reconnection attempt failed:`, error);
1464
+ }
1465
+ }, delay);
1466
+ };
1337
1467
  // Open a websocket connection
1338
1468
  wsConnect = () => {
1339
1469
  if (this.isOffline()) {
@@ -1346,17 +1476,45 @@ var Schematic = class {
1346
1476
  const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
1347
1477
  this.debug(`connecting to WebSocket:`, wsUrl);
1348
1478
  const webSocket = new WebSocket(wsUrl);
1479
+ let timeoutId = null;
1480
+ let isResolved = false;
1481
+ timeoutId = setTimeout(() => {
1482
+ if (!isResolved) {
1483
+ this.debug(
1484
+ `WebSocket connection timeout after ${this.webSocketConnectionTimeout}ms`
1485
+ );
1486
+ webSocket.close();
1487
+ reject(new Error("WebSocket connection timeout"));
1488
+ }
1489
+ }, this.webSocketConnectionTimeout);
1349
1490
  webSocket.onopen = () => {
1491
+ isResolved = true;
1492
+ if (timeoutId !== null) {
1493
+ clearTimeout(timeoutId);
1494
+ }
1495
+ this.wsReconnectAttempts = 0;
1496
+ this.wsIntentionalDisconnect = false;
1350
1497
  this.debug(`WebSocket connection opened`);
1351
1498
  resolve(webSocket);
1352
1499
  };
1353
1500
  webSocket.onerror = (error) => {
1501
+ isResolved = true;
1502
+ if (timeoutId !== null) {
1503
+ clearTimeout(timeoutId);
1504
+ }
1354
1505
  this.debug(`WebSocket connection error:`, error);
1355
1506
  reject(error);
1356
1507
  };
1357
1508
  webSocket.onclose = () => {
1509
+ isResolved = true;
1510
+ if (timeoutId !== null) {
1511
+ clearTimeout(timeoutId);
1512
+ }
1358
1513
  this.debug(`WebSocket connection closed`);
1359
1514
  this.conn = null;
1515
+ if (!this.wsIntentionalDisconnect && this.webSocketReconnect) {
1516
+ this.attemptReconnect();
1517
+ }
1360
1518
  };
1361
1519
  });
1362
1520
  };
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.7",
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.0",
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.46.4",
54
+ "vitest": "^4.0.8"
58
55
  },
59
56
  "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
60
57
  }