@schematichq/schematic-react 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 +21 -0
- package/README.md +2 -0
- package/dist/schematic-react.cjs.js +319 -8
- package/dist/schematic-react.esm.js +319 -8
- package/package.json +21 -23
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.
|
package/README.md
CHANGED
|
@@ -134,6 +134,8 @@ const MyComponent = () => {
|
|
|
134
134
|
};
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
+
*Note: `useSchematicIsPending` is checking if entitlement data has been loaded, typically via `identify`. It should, therefore, be used to wrap flag and entitlement checks, but never the initial call to `identify`.*
|
|
138
|
+
|
|
137
139
|
## Troubleshooting
|
|
138
140
|
|
|
139
141
|
For debugging and development, Schematic supports two special modes:
|
|
@@ -799,7 +799,7 @@ function contextString(context) {
|
|
|
799
799
|
}, {});
|
|
800
800
|
return JSON.stringify(sortedContext);
|
|
801
801
|
}
|
|
802
|
-
var version = "1.2.
|
|
802
|
+
var version = "1.2.8";
|
|
803
803
|
var anonymousIdKey = "schematicId";
|
|
804
804
|
var Schematic = class {
|
|
805
805
|
additionalHeaders = {};
|
|
@@ -821,6 +821,23 @@ var Schematic = class {
|
|
|
821
821
|
checks = {};
|
|
822
822
|
featureUsageEventMap = {};
|
|
823
823
|
webSocketUrl = "wss://api.schematichq.com";
|
|
824
|
+
webSocketConnectionTimeout = 1e4;
|
|
825
|
+
webSocketReconnect = true;
|
|
826
|
+
webSocketMaxReconnectAttempts = 7;
|
|
827
|
+
webSocketInitialRetryDelay = 1e3;
|
|
828
|
+
webSocketMaxRetryDelay = 3e4;
|
|
829
|
+
wsReconnectAttempts = 0;
|
|
830
|
+
wsReconnectTimer = null;
|
|
831
|
+
wsIntentionalDisconnect = false;
|
|
832
|
+
maxEventQueueSize = 100;
|
|
833
|
+
// Prevent memory issues with very long network outages
|
|
834
|
+
maxEventRetries = 5;
|
|
835
|
+
// Maximum retry attempts for failed events
|
|
836
|
+
eventRetryInitialDelay = 1e3;
|
|
837
|
+
// Initial retry delay in ms
|
|
838
|
+
eventRetryMaxDelay = 3e4;
|
|
839
|
+
// Maximum retry delay in ms
|
|
840
|
+
retryTimer = null;
|
|
824
841
|
constructor(apiKey, options) {
|
|
825
842
|
this.apiKey = apiKey;
|
|
826
843
|
this.eventQueue = [];
|
|
@@ -864,11 +881,48 @@ var Schematic = class {
|
|
|
864
881
|
if (options?.webSocketUrl !== void 0) {
|
|
865
882
|
this.webSocketUrl = options.webSocketUrl;
|
|
866
883
|
}
|
|
884
|
+
if (options?.webSocketConnectionTimeout !== void 0) {
|
|
885
|
+
this.webSocketConnectionTimeout = options.webSocketConnectionTimeout;
|
|
886
|
+
}
|
|
887
|
+
if (options?.webSocketReconnect !== void 0) {
|
|
888
|
+
this.webSocketReconnect = options.webSocketReconnect;
|
|
889
|
+
}
|
|
890
|
+
if (options?.webSocketMaxReconnectAttempts !== void 0) {
|
|
891
|
+
this.webSocketMaxReconnectAttempts = options.webSocketMaxReconnectAttempts;
|
|
892
|
+
}
|
|
893
|
+
if (options?.webSocketInitialRetryDelay !== void 0) {
|
|
894
|
+
this.webSocketInitialRetryDelay = options.webSocketInitialRetryDelay;
|
|
895
|
+
}
|
|
896
|
+
if (options?.webSocketMaxRetryDelay !== void 0) {
|
|
897
|
+
this.webSocketMaxRetryDelay = options.webSocketMaxRetryDelay;
|
|
898
|
+
}
|
|
899
|
+
if (options?.maxEventQueueSize !== void 0) {
|
|
900
|
+
this.maxEventQueueSize = options.maxEventQueueSize;
|
|
901
|
+
}
|
|
902
|
+
if (options?.maxEventRetries !== void 0) {
|
|
903
|
+
this.maxEventRetries = options.maxEventRetries;
|
|
904
|
+
}
|
|
905
|
+
if (options?.eventRetryInitialDelay !== void 0) {
|
|
906
|
+
this.eventRetryInitialDelay = options.eventRetryInitialDelay;
|
|
907
|
+
}
|
|
908
|
+
if (options?.eventRetryMaxDelay !== void 0) {
|
|
909
|
+
this.eventRetryMaxDelay = options.eventRetryMaxDelay;
|
|
910
|
+
}
|
|
867
911
|
if (typeof window !== "undefined" && window?.addEventListener) {
|
|
868
912
|
window.addEventListener("beforeunload", () => {
|
|
869
913
|
this.flushEventQueue();
|
|
870
914
|
this.flushContextDependentEventQueue();
|
|
871
915
|
});
|
|
916
|
+
if (this.useWebSocket) {
|
|
917
|
+
window.addEventListener("offline", () => {
|
|
918
|
+
this.debug("Browser went offline, closing WebSocket connection");
|
|
919
|
+
this.handleNetworkOffline();
|
|
920
|
+
});
|
|
921
|
+
window.addEventListener("online", () => {
|
|
922
|
+
this.debug("Browser came online, attempting to reconnect WebSocket");
|
|
923
|
+
this.handleNetworkOnline();
|
|
924
|
+
});
|
|
925
|
+
}
|
|
872
926
|
}
|
|
873
927
|
if (this.offlineEnabled) {
|
|
874
928
|
this.debug(
|
|
@@ -1133,6 +1187,13 @@ var Schematic = class {
|
|
|
1133
1187
|
try {
|
|
1134
1188
|
this.setIsPending(true);
|
|
1135
1189
|
if (!this.conn) {
|
|
1190
|
+
if (this.wsReconnectTimer !== null) {
|
|
1191
|
+
this.debug(
|
|
1192
|
+
`Cancelling scheduled reconnection, connecting immediately`
|
|
1193
|
+
);
|
|
1194
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1195
|
+
this.wsReconnectTimer = null;
|
|
1196
|
+
}
|
|
1136
1197
|
this.conn = this.wsConnect();
|
|
1137
1198
|
}
|
|
1138
1199
|
const socket = await this.conn;
|
|
@@ -1262,11 +1323,53 @@ var Schematic = class {
|
|
|
1262
1323
|
}
|
|
1263
1324
|
}
|
|
1264
1325
|
};
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1326
|
+
startRetryTimer = () => {
|
|
1327
|
+
if (this.retryTimer !== null) {
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
this.retryTimer = setInterval(() => {
|
|
1331
|
+
this.flushEventQueue().catch((error) => {
|
|
1332
|
+
this.debug("Error in retry timer flush:", error);
|
|
1333
|
+
});
|
|
1334
|
+
if (this.eventQueue.length === 0) {
|
|
1335
|
+
this.stopRetryTimer();
|
|
1336
|
+
}
|
|
1337
|
+
}, 5e3);
|
|
1338
|
+
this.debug("Started retry timer");
|
|
1339
|
+
};
|
|
1340
|
+
stopRetryTimer = () => {
|
|
1341
|
+
if (this.retryTimer !== null) {
|
|
1342
|
+
clearInterval(this.retryTimer);
|
|
1343
|
+
this.retryTimer = null;
|
|
1344
|
+
this.debug("Stopped retry timer");
|
|
1345
|
+
}
|
|
1346
|
+
};
|
|
1347
|
+
flushEventQueue = async () => {
|
|
1348
|
+
if (this.eventQueue.length === 0) {
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
const now = Date.now();
|
|
1352
|
+
const readyEvents = [];
|
|
1353
|
+
const notReadyEvents = [];
|
|
1354
|
+
for (const event of this.eventQueue) {
|
|
1355
|
+
if (event.next_retry_at === void 0 || event.next_retry_at <= now) {
|
|
1356
|
+
readyEvents.push(event);
|
|
1357
|
+
} else {
|
|
1358
|
+
notReadyEvents.push(event);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
if (readyEvents.length === 0) {
|
|
1362
|
+
this.debug(`No events ready for retry yet (${notReadyEvents.length} still in backoff)`);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
this.debug(`Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`);
|
|
1366
|
+
this.eventQueue = notReadyEvents;
|
|
1367
|
+
for (const event of readyEvents) {
|
|
1368
|
+
try {
|
|
1369
|
+
await this.sendEvent(event);
|
|
1370
|
+
this.debug(`Queued event sent successfully:`, event.type);
|
|
1371
|
+
} catch (error) {
|
|
1372
|
+
this.debug(`Failed to send queued event:`, error);
|
|
1270
1373
|
}
|
|
1271
1374
|
}
|
|
1272
1375
|
};
|
|
@@ -1314,12 +1417,37 @@ var Schematic = class {
|
|
|
1314
1417
|
},
|
|
1315
1418
|
body: payload
|
|
1316
1419
|
});
|
|
1420
|
+
if (!response.ok) {
|
|
1421
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1422
|
+
}
|
|
1317
1423
|
this.debug(`event sent:`, {
|
|
1318
1424
|
status: response.status,
|
|
1319
1425
|
statusText: response.statusText
|
|
1320
1426
|
});
|
|
1321
1427
|
} catch (error) {
|
|
1322
|
-
|
|
1428
|
+
const retryCount = (event.retry_count ?? 0) + 1;
|
|
1429
|
+
if (retryCount <= this.maxEventRetries) {
|
|
1430
|
+
this.debug(`Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`, error);
|
|
1431
|
+
const baseDelay = this.eventRetryInitialDelay * Math.pow(2, retryCount - 1);
|
|
1432
|
+
const jitterDelay = Math.min(baseDelay, this.eventRetryMaxDelay);
|
|
1433
|
+
const nextRetryAt = Date.now() + jitterDelay;
|
|
1434
|
+
const retryEvent = {
|
|
1435
|
+
...event,
|
|
1436
|
+
retry_count: retryCount,
|
|
1437
|
+
next_retry_at: nextRetryAt
|
|
1438
|
+
};
|
|
1439
|
+
if (this.eventQueue.length < this.maxEventQueueSize) {
|
|
1440
|
+
this.eventQueue.push(retryEvent);
|
|
1441
|
+
this.debug(`Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`);
|
|
1442
|
+
} else {
|
|
1443
|
+
this.debug(`Event queue full (${this.maxEventQueueSize}), dropping oldest event`);
|
|
1444
|
+
this.eventQueue.shift();
|
|
1445
|
+
this.eventQueue.push(retryEvent);
|
|
1446
|
+
}
|
|
1447
|
+
this.startRetryTimer();
|
|
1448
|
+
} else {
|
|
1449
|
+
this.debug(`Event failed permanently after ${this.maxEventRetries} attempts, dropping:`, error);
|
|
1450
|
+
}
|
|
1323
1451
|
}
|
|
1324
1452
|
return Promise.resolve();
|
|
1325
1453
|
};
|
|
@@ -1339,6 +1467,12 @@ var Schematic = class {
|
|
|
1339
1467
|
this.debug("cleanup: skipped (offline mode)");
|
|
1340
1468
|
return Promise.resolve();
|
|
1341
1469
|
}
|
|
1470
|
+
this.wsIntentionalDisconnect = true;
|
|
1471
|
+
if (this.wsReconnectTimer !== null) {
|
|
1472
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1473
|
+
this.wsReconnectTimer = null;
|
|
1474
|
+
}
|
|
1475
|
+
this.stopRetryTimer();
|
|
1342
1476
|
if (this.conn) {
|
|
1343
1477
|
try {
|
|
1344
1478
|
const socket = await this.conn;
|
|
@@ -1350,6 +1484,100 @@ var Schematic = class {
|
|
|
1350
1484
|
}
|
|
1351
1485
|
}
|
|
1352
1486
|
};
|
|
1487
|
+
/**
|
|
1488
|
+
* Calculate the delay for the next reconnection attempt using exponential backoff with jitter.
|
|
1489
|
+
* This helps prevent dogpiling when the server recovers from an outage.
|
|
1490
|
+
*/
|
|
1491
|
+
calculateReconnectDelay = () => {
|
|
1492
|
+
const exponentialDelay = this.webSocketInitialRetryDelay * Math.pow(2, this.wsReconnectAttempts);
|
|
1493
|
+
const cappedDelay = Math.min(exponentialDelay, this.webSocketMaxRetryDelay);
|
|
1494
|
+
const jitter = Math.random() * cappedDelay * 0.5;
|
|
1495
|
+
const totalDelay = cappedDelay + jitter;
|
|
1496
|
+
this.debug(
|
|
1497
|
+
`Reconnect delay calculated: ${totalDelay.toFixed(0)}ms (attempt ${this.wsReconnectAttempts + 1}/${this.webSocketMaxReconnectAttempts})`
|
|
1498
|
+
);
|
|
1499
|
+
return totalDelay;
|
|
1500
|
+
};
|
|
1501
|
+
/**
|
|
1502
|
+
* Handle browser going offline
|
|
1503
|
+
*/
|
|
1504
|
+
handleNetworkOffline = async () => {
|
|
1505
|
+
if (this.conn !== null) {
|
|
1506
|
+
try {
|
|
1507
|
+
const socket = await this.conn;
|
|
1508
|
+
socket.close();
|
|
1509
|
+
} catch (error) {
|
|
1510
|
+
this.debug("Error closing connection on offline:", error);
|
|
1511
|
+
}
|
|
1512
|
+
this.conn = null;
|
|
1513
|
+
}
|
|
1514
|
+
if (this.wsReconnectTimer !== null) {
|
|
1515
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1516
|
+
this.wsReconnectTimer = null;
|
|
1517
|
+
}
|
|
1518
|
+
};
|
|
1519
|
+
/**
|
|
1520
|
+
* Handle browser coming back online
|
|
1521
|
+
*/
|
|
1522
|
+
handleNetworkOnline = () => {
|
|
1523
|
+
this.debug("Network online, attempting reconnection and flushing queued events");
|
|
1524
|
+
this.wsReconnectAttempts = 0;
|
|
1525
|
+
if (this.wsReconnectTimer !== null) {
|
|
1526
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1527
|
+
this.wsReconnectTimer = null;
|
|
1528
|
+
}
|
|
1529
|
+
this.flushEventQueue().catch((error) => {
|
|
1530
|
+
this.debug("Error flushing event queue on network online:", error);
|
|
1531
|
+
});
|
|
1532
|
+
this.attemptReconnect();
|
|
1533
|
+
};
|
|
1534
|
+
/**
|
|
1535
|
+
* Attempt to reconnect the WebSocket connection with exponential backoff.
|
|
1536
|
+
* Called automatically when the connection closes unexpectedly.
|
|
1537
|
+
*/
|
|
1538
|
+
attemptReconnect = () => {
|
|
1539
|
+
if (this.wsReconnectAttempts >= this.webSocketMaxReconnectAttempts) {
|
|
1540
|
+
this.debug(
|
|
1541
|
+
`Maximum reconnection attempts (${this.webSocketMaxReconnectAttempts}) reached, giving up`
|
|
1542
|
+
);
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
if (this.wsReconnectTimer !== null) {
|
|
1546
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1547
|
+
}
|
|
1548
|
+
const delay = this.calculateReconnectDelay();
|
|
1549
|
+
this.debug(
|
|
1550
|
+
`Scheduling reconnection attempt ${this.wsReconnectAttempts + 1}/${this.webSocketMaxReconnectAttempts} in ${delay.toFixed(0)}ms`
|
|
1551
|
+
);
|
|
1552
|
+
this.wsReconnectTimer = setTimeout(async () => {
|
|
1553
|
+
this.wsReconnectTimer = null;
|
|
1554
|
+
this.wsReconnectAttempts++;
|
|
1555
|
+
this.debug(
|
|
1556
|
+
`Attempting to reconnect (attempt ${this.wsReconnectAttempts}/${this.webSocketMaxReconnectAttempts})`
|
|
1557
|
+
);
|
|
1558
|
+
try {
|
|
1559
|
+
this.conn = this.wsConnect();
|
|
1560
|
+
const socket = await this.conn;
|
|
1561
|
+
this.debug(`Reconnection context check:`, {
|
|
1562
|
+
hasCompany: this.context.company !== void 0,
|
|
1563
|
+
hasUser: this.context.user !== void 0,
|
|
1564
|
+
context: this.context
|
|
1565
|
+
});
|
|
1566
|
+
if (this.context.company !== void 0 || this.context.user !== void 0) {
|
|
1567
|
+
this.debug(`Reconnected, force re-sending context`);
|
|
1568
|
+
await this.wsSendContextAfterReconnection(socket, this.context);
|
|
1569
|
+
} else {
|
|
1570
|
+
this.debug(`No context to re-send after reconnection - websocket ready for new context`);
|
|
1571
|
+
}
|
|
1572
|
+
this.flushEventQueue().catch((error) => {
|
|
1573
|
+
this.debug("Error flushing event queue after websocket reconnection:", error);
|
|
1574
|
+
});
|
|
1575
|
+
this.debug(`Reconnection successful`);
|
|
1576
|
+
} catch (error) {
|
|
1577
|
+
this.debug(`Reconnection attempt failed:`, error);
|
|
1578
|
+
}
|
|
1579
|
+
}, delay);
|
|
1580
|
+
};
|
|
1353
1581
|
// Open a websocket connection
|
|
1354
1582
|
wsConnect = () => {
|
|
1355
1583
|
if (this.isOffline()) {
|
|
@@ -1362,18 +1590,101 @@ var Schematic = class {
|
|
|
1362
1590
|
const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
|
|
1363
1591
|
this.debug(`connecting to WebSocket:`, wsUrl);
|
|
1364
1592
|
const webSocket = new WebSocket(wsUrl);
|
|
1593
|
+
let timeoutId = null;
|
|
1594
|
+
let isResolved = false;
|
|
1595
|
+
timeoutId = setTimeout(() => {
|
|
1596
|
+
if (!isResolved) {
|
|
1597
|
+
this.debug(
|
|
1598
|
+
`WebSocket connection timeout after ${this.webSocketConnectionTimeout}ms`
|
|
1599
|
+
);
|
|
1600
|
+
webSocket.close();
|
|
1601
|
+
reject(new Error("WebSocket connection timeout"));
|
|
1602
|
+
}
|
|
1603
|
+
}, this.webSocketConnectionTimeout);
|
|
1365
1604
|
webSocket.onopen = () => {
|
|
1605
|
+
isResolved = true;
|
|
1606
|
+
if (timeoutId !== null) {
|
|
1607
|
+
clearTimeout(timeoutId);
|
|
1608
|
+
}
|
|
1609
|
+
this.wsReconnectAttempts = 0;
|
|
1610
|
+
this.wsIntentionalDisconnect = false;
|
|
1366
1611
|
this.debug(`WebSocket connection opened`);
|
|
1367
1612
|
resolve(webSocket);
|
|
1368
1613
|
};
|
|
1369
1614
|
webSocket.onerror = (error) => {
|
|
1615
|
+
isResolved = true;
|
|
1616
|
+
if (timeoutId !== null) {
|
|
1617
|
+
clearTimeout(timeoutId);
|
|
1618
|
+
}
|
|
1370
1619
|
this.debug(`WebSocket connection error:`, error);
|
|
1371
1620
|
reject(error);
|
|
1372
1621
|
};
|
|
1373
1622
|
webSocket.onclose = () => {
|
|
1623
|
+
isResolved = true;
|
|
1624
|
+
if (timeoutId !== null) {
|
|
1625
|
+
clearTimeout(timeoutId);
|
|
1626
|
+
}
|
|
1374
1627
|
this.debug(`WebSocket connection closed`);
|
|
1375
1628
|
this.conn = null;
|
|
1629
|
+
if (!this.wsIntentionalDisconnect && this.webSocketReconnect) {
|
|
1630
|
+
this.attemptReconnect();
|
|
1631
|
+
}
|
|
1632
|
+
};
|
|
1633
|
+
});
|
|
1634
|
+
};
|
|
1635
|
+
// Send a message on the websocket after reconnection, forcing the send even if context appears unchanged
|
|
1636
|
+
// because the server has lost all state and needs the initial context
|
|
1637
|
+
wsSendContextAfterReconnection = (socket, context) => {
|
|
1638
|
+
if (this.isOffline()) {
|
|
1639
|
+
this.debug("wsSendContextAfterReconnection: skipped (offline mode)");
|
|
1640
|
+
this.setIsPending(false);
|
|
1641
|
+
return Promise.resolve();
|
|
1642
|
+
}
|
|
1643
|
+
return new Promise((resolve) => {
|
|
1644
|
+
this.debug(`WebSocket force sending context after reconnection:`, context);
|
|
1645
|
+
this.context = context;
|
|
1646
|
+
const sendMessage = () => {
|
|
1647
|
+
let resolved = false;
|
|
1648
|
+
const messageHandler = (event) => {
|
|
1649
|
+
const message = JSON.parse(event.data);
|
|
1650
|
+
this.debug(`WebSocket message received after reconnection:`, message);
|
|
1651
|
+
if (!(contextString(context) in this.checks)) {
|
|
1652
|
+
this.checks[contextString(context)] = {};
|
|
1653
|
+
}
|
|
1654
|
+
(message.flags ?? []).forEach((flag) => {
|
|
1655
|
+
const flagCheck = CheckFlagReturnFromJSON(flag);
|
|
1656
|
+
const contextStr = contextString(context);
|
|
1657
|
+
if (this.checks[contextStr] === void 0) {
|
|
1658
|
+
this.checks[contextStr] = {};
|
|
1659
|
+
}
|
|
1660
|
+
this.checks[contextStr][flagCheck.flag] = flagCheck;
|
|
1661
|
+
});
|
|
1662
|
+
this.useWebSocket = true;
|
|
1663
|
+
socket.removeEventListener("message", messageHandler);
|
|
1664
|
+
if (!resolved) {
|
|
1665
|
+
resolved = true;
|
|
1666
|
+
resolve(this.setIsPending(false));
|
|
1667
|
+
}
|
|
1668
|
+
};
|
|
1669
|
+
socket.addEventListener("message", messageHandler);
|
|
1670
|
+
const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
|
|
1671
|
+
const messagePayload = {
|
|
1672
|
+
apiKey: this.apiKey,
|
|
1673
|
+
clientVersion,
|
|
1674
|
+
data: context
|
|
1675
|
+
};
|
|
1676
|
+
this.debug(`WebSocket sending forced message after reconnection:`, messagePayload);
|
|
1677
|
+
socket.send(JSON.stringify(messagePayload));
|
|
1376
1678
|
};
|
|
1679
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
1680
|
+
this.debug(`WebSocket already open, sending forced message after reconnection`);
|
|
1681
|
+
sendMessage();
|
|
1682
|
+
} else {
|
|
1683
|
+
socket.addEventListener("open", () => {
|
|
1684
|
+
this.debug(`WebSocket opened, sending forced message after reconnection`);
|
|
1685
|
+
sendMessage();
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1377
1688
|
});
|
|
1378
1689
|
};
|
|
1379
1690
|
// Send a message on the websocket indicating interest in a particular evaluation context
|
|
@@ -1564,7 +1875,7 @@ var notifyFlagValueListener = (listener, value) => {
|
|
|
1564
1875
|
var import_react = __toESM(require("react"));
|
|
1565
1876
|
|
|
1566
1877
|
// src/version.ts
|
|
1567
|
-
var version2 = "1.2.
|
|
1878
|
+
var version2 = "1.2.8";
|
|
1568
1879
|
|
|
1569
1880
|
// src/context/schematic.tsx
|
|
1570
1881
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
@@ -754,7 +754,7 @@ function contextString(context) {
|
|
|
754
754
|
}, {});
|
|
755
755
|
return JSON.stringify(sortedContext);
|
|
756
756
|
}
|
|
757
|
-
var version = "1.2.
|
|
757
|
+
var version = "1.2.8";
|
|
758
758
|
var anonymousIdKey = "schematicId";
|
|
759
759
|
var Schematic = class {
|
|
760
760
|
additionalHeaders = {};
|
|
@@ -776,6 +776,23 @@ var Schematic = class {
|
|
|
776
776
|
checks = {};
|
|
777
777
|
featureUsageEventMap = {};
|
|
778
778
|
webSocketUrl = "wss://api.schematichq.com";
|
|
779
|
+
webSocketConnectionTimeout = 1e4;
|
|
780
|
+
webSocketReconnect = true;
|
|
781
|
+
webSocketMaxReconnectAttempts = 7;
|
|
782
|
+
webSocketInitialRetryDelay = 1e3;
|
|
783
|
+
webSocketMaxRetryDelay = 3e4;
|
|
784
|
+
wsReconnectAttempts = 0;
|
|
785
|
+
wsReconnectTimer = null;
|
|
786
|
+
wsIntentionalDisconnect = false;
|
|
787
|
+
maxEventQueueSize = 100;
|
|
788
|
+
// Prevent memory issues with very long network outages
|
|
789
|
+
maxEventRetries = 5;
|
|
790
|
+
// Maximum retry attempts for failed events
|
|
791
|
+
eventRetryInitialDelay = 1e3;
|
|
792
|
+
// Initial retry delay in ms
|
|
793
|
+
eventRetryMaxDelay = 3e4;
|
|
794
|
+
// Maximum retry delay in ms
|
|
795
|
+
retryTimer = null;
|
|
779
796
|
constructor(apiKey, options) {
|
|
780
797
|
this.apiKey = apiKey;
|
|
781
798
|
this.eventQueue = [];
|
|
@@ -819,11 +836,48 @@ var Schematic = class {
|
|
|
819
836
|
if (options?.webSocketUrl !== void 0) {
|
|
820
837
|
this.webSocketUrl = options.webSocketUrl;
|
|
821
838
|
}
|
|
839
|
+
if (options?.webSocketConnectionTimeout !== void 0) {
|
|
840
|
+
this.webSocketConnectionTimeout = options.webSocketConnectionTimeout;
|
|
841
|
+
}
|
|
842
|
+
if (options?.webSocketReconnect !== void 0) {
|
|
843
|
+
this.webSocketReconnect = options.webSocketReconnect;
|
|
844
|
+
}
|
|
845
|
+
if (options?.webSocketMaxReconnectAttempts !== void 0) {
|
|
846
|
+
this.webSocketMaxReconnectAttempts = options.webSocketMaxReconnectAttempts;
|
|
847
|
+
}
|
|
848
|
+
if (options?.webSocketInitialRetryDelay !== void 0) {
|
|
849
|
+
this.webSocketInitialRetryDelay = options.webSocketInitialRetryDelay;
|
|
850
|
+
}
|
|
851
|
+
if (options?.webSocketMaxRetryDelay !== void 0) {
|
|
852
|
+
this.webSocketMaxRetryDelay = options.webSocketMaxRetryDelay;
|
|
853
|
+
}
|
|
854
|
+
if (options?.maxEventQueueSize !== void 0) {
|
|
855
|
+
this.maxEventQueueSize = options.maxEventQueueSize;
|
|
856
|
+
}
|
|
857
|
+
if (options?.maxEventRetries !== void 0) {
|
|
858
|
+
this.maxEventRetries = options.maxEventRetries;
|
|
859
|
+
}
|
|
860
|
+
if (options?.eventRetryInitialDelay !== void 0) {
|
|
861
|
+
this.eventRetryInitialDelay = options.eventRetryInitialDelay;
|
|
862
|
+
}
|
|
863
|
+
if (options?.eventRetryMaxDelay !== void 0) {
|
|
864
|
+
this.eventRetryMaxDelay = options.eventRetryMaxDelay;
|
|
865
|
+
}
|
|
822
866
|
if (typeof window !== "undefined" && window?.addEventListener) {
|
|
823
867
|
window.addEventListener("beforeunload", () => {
|
|
824
868
|
this.flushEventQueue();
|
|
825
869
|
this.flushContextDependentEventQueue();
|
|
826
870
|
});
|
|
871
|
+
if (this.useWebSocket) {
|
|
872
|
+
window.addEventListener("offline", () => {
|
|
873
|
+
this.debug("Browser went offline, closing WebSocket connection");
|
|
874
|
+
this.handleNetworkOffline();
|
|
875
|
+
});
|
|
876
|
+
window.addEventListener("online", () => {
|
|
877
|
+
this.debug("Browser came online, attempting to reconnect WebSocket");
|
|
878
|
+
this.handleNetworkOnline();
|
|
879
|
+
});
|
|
880
|
+
}
|
|
827
881
|
}
|
|
828
882
|
if (this.offlineEnabled) {
|
|
829
883
|
this.debug(
|
|
@@ -1088,6 +1142,13 @@ var Schematic = class {
|
|
|
1088
1142
|
try {
|
|
1089
1143
|
this.setIsPending(true);
|
|
1090
1144
|
if (!this.conn) {
|
|
1145
|
+
if (this.wsReconnectTimer !== null) {
|
|
1146
|
+
this.debug(
|
|
1147
|
+
`Cancelling scheduled reconnection, connecting immediately`
|
|
1148
|
+
);
|
|
1149
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1150
|
+
this.wsReconnectTimer = null;
|
|
1151
|
+
}
|
|
1091
1152
|
this.conn = this.wsConnect();
|
|
1092
1153
|
}
|
|
1093
1154
|
const socket = await this.conn;
|
|
@@ -1217,11 +1278,53 @@ var Schematic = class {
|
|
|
1217
1278
|
}
|
|
1218
1279
|
}
|
|
1219
1280
|
};
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1281
|
+
startRetryTimer = () => {
|
|
1282
|
+
if (this.retryTimer !== null) {
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
this.retryTimer = setInterval(() => {
|
|
1286
|
+
this.flushEventQueue().catch((error) => {
|
|
1287
|
+
this.debug("Error in retry timer flush:", error);
|
|
1288
|
+
});
|
|
1289
|
+
if (this.eventQueue.length === 0) {
|
|
1290
|
+
this.stopRetryTimer();
|
|
1291
|
+
}
|
|
1292
|
+
}, 5e3);
|
|
1293
|
+
this.debug("Started retry timer");
|
|
1294
|
+
};
|
|
1295
|
+
stopRetryTimer = () => {
|
|
1296
|
+
if (this.retryTimer !== null) {
|
|
1297
|
+
clearInterval(this.retryTimer);
|
|
1298
|
+
this.retryTimer = null;
|
|
1299
|
+
this.debug("Stopped retry timer");
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
flushEventQueue = async () => {
|
|
1303
|
+
if (this.eventQueue.length === 0) {
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
const now = Date.now();
|
|
1307
|
+
const readyEvents = [];
|
|
1308
|
+
const notReadyEvents = [];
|
|
1309
|
+
for (const event of this.eventQueue) {
|
|
1310
|
+
if (event.next_retry_at === void 0 || event.next_retry_at <= now) {
|
|
1311
|
+
readyEvents.push(event);
|
|
1312
|
+
} else {
|
|
1313
|
+
notReadyEvents.push(event);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
if (readyEvents.length === 0) {
|
|
1317
|
+
this.debug(`No events ready for retry yet (${notReadyEvents.length} still in backoff)`);
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
this.debug(`Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`);
|
|
1321
|
+
this.eventQueue = notReadyEvents;
|
|
1322
|
+
for (const event of readyEvents) {
|
|
1323
|
+
try {
|
|
1324
|
+
await this.sendEvent(event);
|
|
1325
|
+
this.debug(`Queued event sent successfully:`, event.type);
|
|
1326
|
+
} catch (error) {
|
|
1327
|
+
this.debug(`Failed to send queued event:`, error);
|
|
1225
1328
|
}
|
|
1226
1329
|
}
|
|
1227
1330
|
};
|
|
@@ -1269,12 +1372,37 @@ var Schematic = class {
|
|
|
1269
1372
|
},
|
|
1270
1373
|
body: payload
|
|
1271
1374
|
});
|
|
1375
|
+
if (!response.ok) {
|
|
1376
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1377
|
+
}
|
|
1272
1378
|
this.debug(`event sent:`, {
|
|
1273
1379
|
status: response.status,
|
|
1274
1380
|
statusText: response.statusText
|
|
1275
1381
|
});
|
|
1276
1382
|
} catch (error) {
|
|
1277
|
-
|
|
1383
|
+
const retryCount = (event.retry_count ?? 0) + 1;
|
|
1384
|
+
if (retryCount <= this.maxEventRetries) {
|
|
1385
|
+
this.debug(`Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`, error);
|
|
1386
|
+
const baseDelay = this.eventRetryInitialDelay * Math.pow(2, retryCount - 1);
|
|
1387
|
+
const jitterDelay = Math.min(baseDelay, this.eventRetryMaxDelay);
|
|
1388
|
+
const nextRetryAt = Date.now() + jitterDelay;
|
|
1389
|
+
const retryEvent = {
|
|
1390
|
+
...event,
|
|
1391
|
+
retry_count: retryCount,
|
|
1392
|
+
next_retry_at: nextRetryAt
|
|
1393
|
+
};
|
|
1394
|
+
if (this.eventQueue.length < this.maxEventQueueSize) {
|
|
1395
|
+
this.eventQueue.push(retryEvent);
|
|
1396
|
+
this.debug(`Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`);
|
|
1397
|
+
} else {
|
|
1398
|
+
this.debug(`Event queue full (${this.maxEventQueueSize}), dropping oldest event`);
|
|
1399
|
+
this.eventQueue.shift();
|
|
1400
|
+
this.eventQueue.push(retryEvent);
|
|
1401
|
+
}
|
|
1402
|
+
this.startRetryTimer();
|
|
1403
|
+
} else {
|
|
1404
|
+
this.debug(`Event failed permanently after ${this.maxEventRetries} attempts, dropping:`, error);
|
|
1405
|
+
}
|
|
1278
1406
|
}
|
|
1279
1407
|
return Promise.resolve();
|
|
1280
1408
|
};
|
|
@@ -1294,6 +1422,12 @@ var Schematic = class {
|
|
|
1294
1422
|
this.debug("cleanup: skipped (offline mode)");
|
|
1295
1423
|
return Promise.resolve();
|
|
1296
1424
|
}
|
|
1425
|
+
this.wsIntentionalDisconnect = true;
|
|
1426
|
+
if (this.wsReconnectTimer !== null) {
|
|
1427
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1428
|
+
this.wsReconnectTimer = null;
|
|
1429
|
+
}
|
|
1430
|
+
this.stopRetryTimer();
|
|
1297
1431
|
if (this.conn) {
|
|
1298
1432
|
try {
|
|
1299
1433
|
const socket = await this.conn;
|
|
@@ -1305,6 +1439,100 @@ var Schematic = class {
|
|
|
1305
1439
|
}
|
|
1306
1440
|
}
|
|
1307
1441
|
};
|
|
1442
|
+
/**
|
|
1443
|
+
* Calculate the delay for the next reconnection attempt using exponential backoff with jitter.
|
|
1444
|
+
* This helps prevent dogpiling when the server recovers from an outage.
|
|
1445
|
+
*/
|
|
1446
|
+
calculateReconnectDelay = () => {
|
|
1447
|
+
const exponentialDelay = this.webSocketInitialRetryDelay * Math.pow(2, this.wsReconnectAttempts);
|
|
1448
|
+
const cappedDelay = Math.min(exponentialDelay, this.webSocketMaxRetryDelay);
|
|
1449
|
+
const jitter = Math.random() * cappedDelay * 0.5;
|
|
1450
|
+
const totalDelay = cappedDelay + jitter;
|
|
1451
|
+
this.debug(
|
|
1452
|
+
`Reconnect delay calculated: ${totalDelay.toFixed(0)}ms (attempt ${this.wsReconnectAttempts + 1}/${this.webSocketMaxReconnectAttempts})`
|
|
1453
|
+
);
|
|
1454
|
+
return totalDelay;
|
|
1455
|
+
};
|
|
1456
|
+
/**
|
|
1457
|
+
* Handle browser going offline
|
|
1458
|
+
*/
|
|
1459
|
+
handleNetworkOffline = async () => {
|
|
1460
|
+
if (this.conn !== null) {
|
|
1461
|
+
try {
|
|
1462
|
+
const socket = await this.conn;
|
|
1463
|
+
socket.close();
|
|
1464
|
+
} catch (error) {
|
|
1465
|
+
this.debug("Error closing connection on offline:", error);
|
|
1466
|
+
}
|
|
1467
|
+
this.conn = null;
|
|
1468
|
+
}
|
|
1469
|
+
if (this.wsReconnectTimer !== null) {
|
|
1470
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1471
|
+
this.wsReconnectTimer = null;
|
|
1472
|
+
}
|
|
1473
|
+
};
|
|
1474
|
+
/**
|
|
1475
|
+
* Handle browser coming back online
|
|
1476
|
+
*/
|
|
1477
|
+
handleNetworkOnline = () => {
|
|
1478
|
+
this.debug("Network online, attempting reconnection and flushing queued events");
|
|
1479
|
+
this.wsReconnectAttempts = 0;
|
|
1480
|
+
if (this.wsReconnectTimer !== null) {
|
|
1481
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1482
|
+
this.wsReconnectTimer = null;
|
|
1483
|
+
}
|
|
1484
|
+
this.flushEventQueue().catch((error) => {
|
|
1485
|
+
this.debug("Error flushing event queue on network online:", error);
|
|
1486
|
+
});
|
|
1487
|
+
this.attemptReconnect();
|
|
1488
|
+
};
|
|
1489
|
+
/**
|
|
1490
|
+
* Attempt to reconnect the WebSocket connection with exponential backoff.
|
|
1491
|
+
* Called automatically when the connection closes unexpectedly.
|
|
1492
|
+
*/
|
|
1493
|
+
attemptReconnect = () => {
|
|
1494
|
+
if (this.wsReconnectAttempts >= this.webSocketMaxReconnectAttempts) {
|
|
1495
|
+
this.debug(
|
|
1496
|
+
`Maximum reconnection attempts (${this.webSocketMaxReconnectAttempts}) reached, giving up`
|
|
1497
|
+
);
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
if (this.wsReconnectTimer !== null) {
|
|
1501
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1502
|
+
}
|
|
1503
|
+
const delay = this.calculateReconnectDelay();
|
|
1504
|
+
this.debug(
|
|
1505
|
+
`Scheduling reconnection attempt ${this.wsReconnectAttempts + 1}/${this.webSocketMaxReconnectAttempts} in ${delay.toFixed(0)}ms`
|
|
1506
|
+
);
|
|
1507
|
+
this.wsReconnectTimer = setTimeout(async () => {
|
|
1508
|
+
this.wsReconnectTimer = null;
|
|
1509
|
+
this.wsReconnectAttempts++;
|
|
1510
|
+
this.debug(
|
|
1511
|
+
`Attempting to reconnect (attempt ${this.wsReconnectAttempts}/${this.webSocketMaxReconnectAttempts})`
|
|
1512
|
+
);
|
|
1513
|
+
try {
|
|
1514
|
+
this.conn = this.wsConnect();
|
|
1515
|
+
const socket = await this.conn;
|
|
1516
|
+
this.debug(`Reconnection context check:`, {
|
|
1517
|
+
hasCompany: this.context.company !== void 0,
|
|
1518
|
+
hasUser: this.context.user !== void 0,
|
|
1519
|
+
context: this.context
|
|
1520
|
+
});
|
|
1521
|
+
if (this.context.company !== void 0 || this.context.user !== void 0) {
|
|
1522
|
+
this.debug(`Reconnected, force re-sending context`);
|
|
1523
|
+
await this.wsSendContextAfterReconnection(socket, this.context);
|
|
1524
|
+
} else {
|
|
1525
|
+
this.debug(`No context to re-send after reconnection - websocket ready for new context`);
|
|
1526
|
+
}
|
|
1527
|
+
this.flushEventQueue().catch((error) => {
|
|
1528
|
+
this.debug("Error flushing event queue after websocket reconnection:", error);
|
|
1529
|
+
});
|
|
1530
|
+
this.debug(`Reconnection successful`);
|
|
1531
|
+
} catch (error) {
|
|
1532
|
+
this.debug(`Reconnection attempt failed:`, error);
|
|
1533
|
+
}
|
|
1534
|
+
}, delay);
|
|
1535
|
+
};
|
|
1308
1536
|
// Open a websocket connection
|
|
1309
1537
|
wsConnect = () => {
|
|
1310
1538
|
if (this.isOffline()) {
|
|
@@ -1317,18 +1545,101 @@ var Schematic = class {
|
|
|
1317
1545
|
const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
|
|
1318
1546
|
this.debug(`connecting to WebSocket:`, wsUrl);
|
|
1319
1547
|
const webSocket = new WebSocket(wsUrl);
|
|
1548
|
+
let timeoutId = null;
|
|
1549
|
+
let isResolved = false;
|
|
1550
|
+
timeoutId = setTimeout(() => {
|
|
1551
|
+
if (!isResolved) {
|
|
1552
|
+
this.debug(
|
|
1553
|
+
`WebSocket connection timeout after ${this.webSocketConnectionTimeout}ms`
|
|
1554
|
+
);
|
|
1555
|
+
webSocket.close();
|
|
1556
|
+
reject(new Error("WebSocket connection timeout"));
|
|
1557
|
+
}
|
|
1558
|
+
}, this.webSocketConnectionTimeout);
|
|
1320
1559
|
webSocket.onopen = () => {
|
|
1560
|
+
isResolved = true;
|
|
1561
|
+
if (timeoutId !== null) {
|
|
1562
|
+
clearTimeout(timeoutId);
|
|
1563
|
+
}
|
|
1564
|
+
this.wsReconnectAttempts = 0;
|
|
1565
|
+
this.wsIntentionalDisconnect = false;
|
|
1321
1566
|
this.debug(`WebSocket connection opened`);
|
|
1322
1567
|
resolve(webSocket);
|
|
1323
1568
|
};
|
|
1324
1569
|
webSocket.onerror = (error) => {
|
|
1570
|
+
isResolved = true;
|
|
1571
|
+
if (timeoutId !== null) {
|
|
1572
|
+
clearTimeout(timeoutId);
|
|
1573
|
+
}
|
|
1325
1574
|
this.debug(`WebSocket connection error:`, error);
|
|
1326
1575
|
reject(error);
|
|
1327
1576
|
};
|
|
1328
1577
|
webSocket.onclose = () => {
|
|
1578
|
+
isResolved = true;
|
|
1579
|
+
if (timeoutId !== null) {
|
|
1580
|
+
clearTimeout(timeoutId);
|
|
1581
|
+
}
|
|
1329
1582
|
this.debug(`WebSocket connection closed`);
|
|
1330
1583
|
this.conn = null;
|
|
1584
|
+
if (!this.wsIntentionalDisconnect && this.webSocketReconnect) {
|
|
1585
|
+
this.attemptReconnect();
|
|
1586
|
+
}
|
|
1587
|
+
};
|
|
1588
|
+
});
|
|
1589
|
+
};
|
|
1590
|
+
// Send a message on the websocket after reconnection, forcing the send even if context appears unchanged
|
|
1591
|
+
// because the server has lost all state and needs the initial context
|
|
1592
|
+
wsSendContextAfterReconnection = (socket, context) => {
|
|
1593
|
+
if (this.isOffline()) {
|
|
1594
|
+
this.debug("wsSendContextAfterReconnection: skipped (offline mode)");
|
|
1595
|
+
this.setIsPending(false);
|
|
1596
|
+
return Promise.resolve();
|
|
1597
|
+
}
|
|
1598
|
+
return new Promise((resolve) => {
|
|
1599
|
+
this.debug(`WebSocket force sending context after reconnection:`, context);
|
|
1600
|
+
this.context = context;
|
|
1601
|
+
const sendMessage = () => {
|
|
1602
|
+
let resolved = false;
|
|
1603
|
+
const messageHandler = (event) => {
|
|
1604
|
+
const message = JSON.parse(event.data);
|
|
1605
|
+
this.debug(`WebSocket message received after reconnection:`, message);
|
|
1606
|
+
if (!(contextString(context) in this.checks)) {
|
|
1607
|
+
this.checks[contextString(context)] = {};
|
|
1608
|
+
}
|
|
1609
|
+
(message.flags ?? []).forEach((flag) => {
|
|
1610
|
+
const flagCheck = CheckFlagReturnFromJSON(flag);
|
|
1611
|
+
const contextStr = contextString(context);
|
|
1612
|
+
if (this.checks[contextStr] === void 0) {
|
|
1613
|
+
this.checks[contextStr] = {};
|
|
1614
|
+
}
|
|
1615
|
+
this.checks[contextStr][flagCheck.flag] = flagCheck;
|
|
1616
|
+
});
|
|
1617
|
+
this.useWebSocket = true;
|
|
1618
|
+
socket.removeEventListener("message", messageHandler);
|
|
1619
|
+
if (!resolved) {
|
|
1620
|
+
resolved = true;
|
|
1621
|
+
resolve(this.setIsPending(false));
|
|
1622
|
+
}
|
|
1623
|
+
};
|
|
1624
|
+
socket.addEventListener("message", messageHandler);
|
|
1625
|
+
const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
|
|
1626
|
+
const messagePayload = {
|
|
1627
|
+
apiKey: this.apiKey,
|
|
1628
|
+
clientVersion,
|
|
1629
|
+
data: context
|
|
1630
|
+
};
|
|
1631
|
+
this.debug(`WebSocket sending forced message after reconnection:`, messagePayload);
|
|
1632
|
+
socket.send(JSON.stringify(messagePayload));
|
|
1331
1633
|
};
|
|
1634
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
1635
|
+
this.debug(`WebSocket already open, sending forced message after reconnection`);
|
|
1636
|
+
sendMessage();
|
|
1637
|
+
} else {
|
|
1638
|
+
socket.addEventListener("open", () => {
|
|
1639
|
+
this.debug(`WebSocket opened, sending forced message after reconnection`);
|
|
1640
|
+
sendMessage();
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1332
1643
|
});
|
|
1333
1644
|
};
|
|
1334
1645
|
// Send a message on the websocket indicating interest in a particular evaluation context
|
|
@@ -1519,7 +1830,7 @@ var notifyFlagValueListener = (listener, value) => {
|
|
|
1519
1830
|
import React, { createContext, useEffect, useMemo, useRef } from "react";
|
|
1520
1831
|
|
|
1521
1832
|
// src/version.ts
|
|
1522
|
-
var version2 = "1.2.
|
|
1833
|
+
var version2 = "1.2.8";
|
|
1523
1834
|
|
|
1524
1835
|
// src/context/schematic.tsx
|
|
1525
1836
|
import { jsx } from "react/jsx-runtime";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schematichq/schematic-react",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.8",
|
|
4
4
|
"main": "dist/schematic-react.cjs.js",
|
|
5
5
|
"module": "dist/schematic-react.esm.js",
|
|
6
6
|
"types": "dist/schematic-react.d.ts",
|
|
@@ -24,40 +24,38 @@
|
|
|
24
24
|
"clean": "rm -rf dist",
|
|
25
25
|
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
|
|
26
26
|
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --fix",
|
|
27
|
-
"test": "
|
|
28
|
-
"test:reactnative": "
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"test:reactnative": "vitest run --config vitest.config.reactnative.ts",
|
|
29
|
+
"test:watch": "vitest",
|
|
29
30
|
"tsc": "npx tsc",
|
|
30
31
|
"prepare": "husky"
|
|
31
32
|
},
|
|
32
33
|
"dependencies": {
|
|
33
|
-
"@schematichq/schematic-js": "^1.2.
|
|
34
|
+
"@schematichq/schematic-js": "^1.2.8"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
|
-
"@eslint/js": "^9.
|
|
37
|
-
"@microsoft/api-extractor": "^7.
|
|
37
|
+
"@eslint/js": "^9.39.1",
|
|
38
|
+
"@microsoft/api-extractor": "^7.55.0",
|
|
38
39
|
"@testing-library/dom": "^10.4.1",
|
|
39
|
-
"@testing-library/jest-dom": "^6.
|
|
40
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
40
41
|
"@testing-library/react": "^16.3.0",
|
|
41
|
-
"@types/
|
|
42
|
-
"@
|
|
43
|
-
"esbuild": "^0.
|
|
44
|
-
"
|
|
45
|
-
"eslint": "^9.24.0",
|
|
42
|
+
"@types/react": "^19.2.6",
|
|
43
|
+
"@vitest/browser": "^4.0.8",
|
|
44
|
+
"esbuild": "^0.27.0",
|
|
45
|
+
"eslint": "^9.39.1",
|
|
46
46
|
"eslint-plugin-import": "^2.32.0",
|
|
47
47
|
"eslint-plugin-react": "^7.37.5",
|
|
48
|
-
"eslint-plugin-react-hooks": "^
|
|
49
|
-
"globals": "^16.
|
|
48
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
49
|
+
"globals": "^16.5.0",
|
|
50
|
+
"happy-dom": "^20.0.10",
|
|
50
51
|
"husky": "^9.1.7",
|
|
51
|
-
"
|
|
52
|
-
"jest-environment-jsdom": "^30.0.0",
|
|
53
|
-
"jest-esbuild": "^0.4.0",
|
|
54
|
-
"jest-fetch-mock": "^3.0.3",
|
|
52
|
+
"jsdom": "^27.2.0",
|
|
55
53
|
"prettier": "^3.6.2",
|
|
56
|
-
"react": "^19.
|
|
57
|
-
"react-dom": "^19.
|
|
58
|
-
"
|
|
59
|
-
"typescript": "^
|
|
60
|
-
"
|
|
54
|
+
"react": "^19.2.0",
|
|
55
|
+
"react-dom": "^19.2.0",
|
|
56
|
+
"typescript": "^5.9.3",
|
|
57
|
+
"typescript-eslint": "^8.47.0",
|
|
58
|
+
"vitest": "^4.0.8"
|
|
61
59
|
},
|
|
62
60
|
"peerDependencies": {
|
|
63
61
|
"react": ">=18"
|