@avsbhq/node 1.7.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -701,7 +701,7 @@ function createServerTracker(options) {
701
701
  var import_streaming = require("@avsbhq/utils/streaming");
702
702
 
703
703
  // src/version.ts
704
- var SDK_VERSION = "1.7.0";
704
+ var SDK_VERSION = "1.7.1";
705
705
 
706
706
  // src/UserBoundClient.ts
707
707
  var import_core6 = require("@avsbhq/core");
@@ -844,6 +844,12 @@ var AvsbServer = class {
844
844
  readyPromise = null;
845
845
  pendingEvents = [];
846
846
  pendingPurchases = [];
847
+ /**
848
+ * Exposures produced before the tracker existed. Third member of the
849
+ * pre-ready family alongside `pendingEvents` and `pendingPurchases`; it used
850
+ * to be missing, and exposures were dropped on the floor instead.
851
+ */
852
+ pendingExposures = [];
847
853
  /** ms-epoch of the last liveness heartbeat, for the throttle. */
848
854
  lastHeartbeatAt = 0;
849
855
  /** Keys of one-time warnings, so a request loop cannot flood a log. */
@@ -920,6 +926,7 @@ var AvsbServer = class {
920
926
  if (!keyShape.valid && keyShape.message) {
921
927
  this.logger.error(keyShape.message);
922
928
  }
929
+ if (this.datafile) this.initTracker();
923
930
  }
924
931
  // ---------------------------------------------------------------------------
925
932
  // Readiness
@@ -1029,7 +1036,9 @@ var AvsbServer = class {
1029
1036
  this.logger.warn(
1030
1037
  "AvsB datafile refresh did not apply a new datafile; the SDK keeps serving the one it already has. The request error is logged above."
1031
1038
  );
1039
+ return;
1032
1040
  }
1041
+ if (result === "applied" && !this.ready) this.onDatafileReadable();
1033
1042
  }
1034
1043
  /**
1035
1044
  * Adopt a datafile received out of band: a bootstrap blob from
@@ -1049,6 +1058,7 @@ var AvsbServer = class {
1049
1058
  this.onDatafileReadable();
1050
1059
  return;
1051
1060
  }
1061
+ this.initTracker();
1052
1062
  if (changed) {
1053
1063
  this.emit("configUpdate", {
1054
1064
  publishedAt: datafile.publishedAt,
@@ -1456,7 +1466,11 @@ var AvsbServer = class {
1456
1466
  */
1457
1467
  deliverExposure(event) {
1458
1468
  const stamped = event.eventId ? event : { ...event, eventId: (0, import_node_crypto2.randomUUID)() };
1459
- this.tracker?.queue(stamped);
1469
+ if (this.tracker) {
1470
+ this.tracker.queue(stamped);
1471
+ } else {
1472
+ this.enqueuePendingExposure(stamped);
1473
+ }
1460
1474
  this.emit("exposure", stamped);
1461
1475
  }
1462
1476
  toFlag(result, defaultValue) {
@@ -1579,7 +1593,7 @@ var AvsbServer = class {
1579
1593
  // ---------------------------------------------------------------------------
1580
1594
  initTracker() {
1581
1595
  if (!this.datafile || this.tracker) return;
1582
- this.tracker = createServerTracker({
1596
+ const tracker = createServerTracker({
1583
1597
  sdkKey: this.sdkKey,
1584
1598
  collectEndpoint: this.datafile.collectEndpoint,
1585
1599
  flushInterval: this.eventOptions.flushInterval,
@@ -1588,6 +1602,12 @@ var AvsbServer = class {
1588
1602
  retryAttempts: this.eventOptions.retryAttempts,
1589
1603
  logger: this.logger
1590
1604
  });
1605
+ this.tracker = tracker;
1606
+ if (this.pendingExposures.length > 0) {
1607
+ const queued = this.pendingExposures;
1608
+ this.pendingExposures = [];
1609
+ for (const event of queued) tracker.queue(event);
1610
+ }
1591
1611
  if (this.pendingEvents.length > 0) {
1592
1612
  const queued = this.pendingEvents;
1593
1613
  this.pendingEvents = [];
@@ -1599,6 +1619,16 @@ var AvsbServer = class {
1599
1619
  for (const item of queued) void this.trackPurchase(item.visitorId, item.order);
1600
1620
  }
1601
1621
  }
1622
+ enqueuePendingExposure(event) {
1623
+ if (this.pendingExposures.length >= this.eventOptions.maxQueueSize) {
1624
+ this.pendingExposures.shift();
1625
+ this.warnOnce(
1626
+ "pending-exposures-full",
1627
+ `Pre-ready exposure queue reached ${this.eventOptions.maxQueueSize} events; the oldest are being dropped. This means the datafile never loaded, so nothing has been delivered yet: check the datafile error logged above.`
1628
+ );
1629
+ }
1630
+ this.pendingExposures.push(event);
1631
+ }
1602
1632
  enqueuePending(eventKey, payload) {
1603
1633
  if (this.pendingEvents.length >= this.eventOptions.maxQueueSize) {
1604
1634
  this.pendingEvents.shift();
package/dist/index.d.cts CHANGED
@@ -375,6 +375,12 @@ declare class AvsbServer {
375
375
  private readyPromise;
376
376
  private pendingEvents;
377
377
  private pendingPurchases;
378
+ /**
379
+ * Exposures produced before the tracker existed. Third member of the
380
+ * pre-ready family alongside `pendingEvents` and `pendingPurchases`; it used
381
+ * to be missing, and exposures were dropped on the floor instead.
382
+ */
383
+ private pendingExposures;
378
384
  /** ms-epoch of the last liveness heartbeat, for the throttle. */
379
385
  private lastHeartbeatAt;
380
386
  /** Keys of one-time warnings, so a request loop cannot flood a log. */
@@ -535,6 +541,7 @@ declare class AvsbServer {
535
541
  private findVariation;
536
542
  private fireExposureForStickyAssignment;
537
543
  private initTracker;
544
+ private enqueuePendingExposure;
538
545
  private enqueuePending;
539
546
  /**
540
547
  * Fetch and apply the datafile.
@@ -669,6 +676,6 @@ type WebhookVerifyResult = {
669
676
  */
670
677
  declare function verifyWebhookSignature(payload: string | Buffer, headers: WebhookHeaders, secret: string, options?: VerifyWebhookOptions): WebhookVerifyResult;
671
678
 
672
- declare const SDK_VERSION = "1.7.0";
679
+ declare const SDK_VERSION = "1.7.1";
673
680
 
674
681
  export { AvsbServer, type AvsbServerOptions, DEFAULT_TOLERANCE_SECONDS, type DatafileRecipe, type DatasetGetResult, type DatasetsClient, type EventOptions, type GetAllFlagsOptions, InMemoryStickyBucketService, type PurchaseOrder, type RecProduct, type RecsAttribution, type RecsClient, type RecsGetRequest, type RecsResult, type RedisLike, type RedisStickyBucketOptions, RedisStickyBucketService, SDK_VERSION, type ServerEventMap, type ServerEventName, type ServerGetFlagOptions, type ServerLogLevel, type UserBoundClient, type VerifyWebhookOptions, type WebhookHeaders, type WebhookVerifyFailure, type WebhookVerifyResult, createDecisionLog, defaultLogger, isDevEnvironment, resolveServerLogger, verifyWebhookSignature };
package/dist/index.d.ts CHANGED
@@ -375,6 +375,12 @@ declare class AvsbServer {
375
375
  private readyPromise;
376
376
  private pendingEvents;
377
377
  private pendingPurchases;
378
+ /**
379
+ * Exposures produced before the tracker existed. Third member of the
380
+ * pre-ready family alongside `pendingEvents` and `pendingPurchases`; it used
381
+ * to be missing, and exposures were dropped on the floor instead.
382
+ */
383
+ private pendingExposures;
378
384
  /** ms-epoch of the last liveness heartbeat, for the throttle. */
379
385
  private lastHeartbeatAt;
380
386
  /** Keys of one-time warnings, so a request loop cannot flood a log. */
@@ -535,6 +541,7 @@ declare class AvsbServer {
535
541
  private findVariation;
536
542
  private fireExposureForStickyAssignment;
537
543
  private initTracker;
544
+ private enqueuePendingExposure;
538
545
  private enqueuePending;
539
546
  /**
540
547
  * Fetch and apply the datafile.
@@ -669,6 +676,6 @@ type WebhookVerifyResult = {
669
676
  */
670
677
  declare function verifyWebhookSignature(payload: string | Buffer, headers: WebhookHeaders, secret: string, options?: VerifyWebhookOptions): WebhookVerifyResult;
671
678
 
672
- declare const SDK_VERSION = "1.7.0";
679
+ declare const SDK_VERSION = "1.7.1";
673
680
 
674
681
  export { AvsbServer, type AvsbServerOptions, DEFAULT_TOLERANCE_SECONDS, type DatafileRecipe, type DatasetGetResult, type DatasetsClient, type EventOptions, type GetAllFlagsOptions, InMemoryStickyBucketService, type PurchaseOrder, type RecProduct, type RecsAttribution, type RecsClient, type RecsGetRequest, type RecsResult, type RedisLike, type RedisStickyBucketOptions, RedisStickyBucketService, SDK_VERSION, type ServerEventMap, type ServerEventName, type ServerGetFlagOptions, type ServerLogLevel, type UserBoundClient, type VerifyWebhookOptions, type WebhookHeaders, type WebhookVerifyFailure, type WebhookVerifyResult, createDecisionLog, defaultLogger, isDevEnvironment, resolveServerLogger, verifyWebhookSignature };
package/dist/index.js CHANGED
@@ -667,7 +667,7 @@ import {
667
667
  } from "@avsbhq/utils/streaming";
668
668
 
669
669
  // src/version.ts
670
- var SDK_VERSION = "1.7.0";
670
+ var SDK_VERSION = "1.7.1";
671
671
 
672
672
  // src/UserBoundClient.ts
673
673
  import { bucketingKey } from "@avsbhq/core";
@@ -810,6 +810,12 @@ var AvsbServer = class {
810
810
  readyPromise = null;
811
811
  pendingEvents = [];
812
812
  pendingPurchases = [];
813
+ /**
814
+ * Exposures produced before the tracker existed. Third member of the
815
+ * pre-ready family alongside `pendingEvents` and `pendingPurchases`; it used
816
+ * to be missing, and exposures were dropped on the floor instead.
817
+ */
818
+ pendingExposures = [];
813
819
  /** ms-epoch of the last liveness heartbeat, for the throttle. */
814
820
  lastHeartbeatAt = 0;
815
821
  /** Keys of one-time warnings, so a request loop cannot flood a log. */
@@ -886,6 +892,7 @@ var AvsbServer = class {
886
892
  if (!keyShape.valid && keyShape.message) {
887
893
  this.logger.error(keyShape.message);
888
894
  }
895
+ if (this.datafile) this.initTracker();
889
896
  }
890
897
  // ---------------------------------------------------------------------------
891
898
  // Readiness
@@ -995,7 +1002,9 @@ var AvsbServer = class {
995
1002
  this.logger.warn(
996
1003
  "AvsB datafile refresh did not apply a new datafile; the SDK keeps serving the one it already has. The request error is logged above."
997
1004
  );
1005
+ return;
998
1006
  }
1007
+ if (result === "applied" && !this.ready) this.onDatafileReadable();
999
1008
  }
1000
1009
  /**
1001
1010
  * Adopt a datafile received out of band: a bootstrap blob from
@@ -1015,6 +1024,7 @@ var AvsbServer = class {
1015
1024
  this.onDatafileReadable();
1016
1025
  return;
1017
1026
  }
1027
+ this.initTracker();
1018
1028
  if (changed) {
1019
1029
  this.emit("configUpdate", {
1020
1030
  publishedAt: datafile.publishedAt,
@@ -1422,7 +1432,11 @@ var AvsbServer = class {
1422
1432
  */
1423
1433
  deliverExposure(event) {
1424
1434
  const stamped = event.eventId ? event : { ...event, eventId: randomUUID2() };
1425
- this.tracker?.queue(stamped);
1435
+ if (this.tracker) {
1436
+ this.tracker.queue(stamped);
1437
+ } else {
1438
+ this.enqueuePendingExposure(stamped);
1439
+ }
1426
1440
  this.emit("exposure", stamped);
1427
1441
  }
1428
1442
  toFlag(result, defaultValue) {
@@ -1545,7 +1559,7 @@ var AvsbServer = class {
1545
1559
  // ---------------------------------------------------------------------------
1546
1560
  initTracker() {
1547
1561
  if (!this.datafile || this.tracker) return;
1548
- this.tracker = createServerTracker({
1562
+ const tracker = createServerTracker({
1549
1563
  sdkKey: this.sdkKey,
1550
1564
  collectEndpoint: this.datafile.collectEndpoint,
1551
1565
  flushInterval: this.eventOptions.flushInterval,
@@ -1554,6 +1568,12 @@ var AvsbServer = class {
1554
1568
  retryAttempts: this.eventOptions.retryAttempts,
1555
1569
  logger: this.logger
1556
1570
  });
1571
+ this.tracker = tracker;
1572
+ if (this.pendingExposures.length > 0) {
1573
+ const queued = this.pendingExposures;
1574
+ this.pendingExposures = [];
1575
+ for (const event of queued) tracker.queue(event);
1576
+ }
1557
1577
  if (this.pendingEvents.length > 0) {
1558
1578
  const queued = this.pendingEvents;
1559
1579
  this.pendingEvents = [];
@@ -1565,6 +1585,16 @@ var AvsbServer = class {
1565
1585
  for (const item of queued) void this.trackPurchase(item.visitorId, item.order);
1566
1586
  }
1567
1587
  }
1588
+ enqueuePendingExposure(event) {
1589
+ if (this.pendingExposures.length >= this.eventOptions.maxQueueSize) {
1590
+ this.pendingExposures.shift();
1591
+ this.warnOnce(
1592
+ "pending-exposures-full",
1593
+ `Pre-ready exposure queue reached ${this.eventOptions.maxQueueSize} events; the oldest are being dropped. This means the datafile never loaded, so nothing has been delivered yet: check the datafile error logged above.`
1594
+ );
1595
+ }
1596
+ this.pendingExposures.push(event);
1597
+ }
1568
1598
  enqueuePending(eventKey, payload) {
1569
1599
  if (this.pendingEvents.length >= this.eventOptions.maxQueueSize) {
1570
1600
  this.pendingEvents.shift();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avsbhq/node",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "Node.js server SDK for A vs B feature flags and experiments: per-request contexts, sticky bucketing, decision logs, and batched event delivery.",
5
5
  "keywords": [
6
6
  "avsb",