@trillboards/ads-sdk 2.1.0 → 2.2.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.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/core/config.ts
2
- var SDK_VERSION = "2.1.0";
2
+ var SDK_VERSION = "2.2.0";
3
3
  var DEFAULT_CONFIG = {
4
4
  API_BASE: "https://api.trillboards.com/v1/partner",
5
5
  CDN_BASE: "https://cdn.trillboards.com",
@@ -58,6 +58,7 @@ var DEFAULT_PUBLIC_STATE = {
58
58
  programmaticPlaying: false,
59
59
  prefetchedReady: false,
60
60
  waterfallMode: "programmatic_only",
61
+ adDeliveryProfileMode: null,
61
62
  screenId: null,
62
63
  deviceId: null
63
64
  };
@@ -85,6 +86,7 @@ function createInitialState() {
85
86
  programmaticRetryActive: false,
86
87
  programmaticRetryCount: 0,
87
88
  programmaticLastError: null,
89
+ adDeliveryProfile: null,
88
90
  initialized: false,
89
91
  screenOrientation: null,
90
92
  screenDimensions: null,
@@ -102,6 +104,7 @@ function getPublicState(internal) {
102
104
  programmaticPlaying: internal.programmaticPlaying,
103
105
  prefetchedReady: internal.prefetchedReady ?? false,
104
106
  waterfallMode: internal.waterfallMode,
107
+ adDeliveryProfileMode: internal.adDeliveryProfile?.mode ?? null,
105
108
  screenId: internal.screenId,
106
109
  deviceId: internal.deviceId
107
110
  };
@@ -362,10 +365,9 @@ var ApiClient = class {
362
365
  }
363
366
  }
364
367
  /**
365
- * Ping the heartbeat endpoint to signal this device is alive.
366
- * Returns `true` on 2xx, `false` on any error.
368
+ * Ping heartbeat endpoint and return delivery profile + queued commands.
367
369
  */
368
- async sendHeartbeat(deviceId, screenId) {
370
+ async sendHeartbeat(deviceId, screenId, payload = {}) {
369
371
  logger.debug("Sending heartbeat", { deviceId });
370
372
  try {
371
373
  const response = await fetch(`${this.apiBase}/device/${deviceId}/heartbeat`, {
@@ -375,13 +377,24 @@ var ApiClient = class {
375
377
  device_id: deviceId,
376
378
  screen_id: screenId,
377
379
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
378
- status: "active"
380
+ status: "active",
381
+ ...payload
379
382
  }),
380
383
  signal: AbortSignal.timeout(5e3)
381
384
  });
382
- return response.ok;
385
+ if (!response.ok) {
386
+ return { ok: false, data: null };
387
+ }
388
+ let data = null;
389
+ try {
390
+ const json = await response.json();
391
+ data = json?.data ?? null;
392
+ } catch {
393
+ data = null;
394
+ }
395
+ return { ok: true, data };
383
396
  } catch {
384
- return false;
397
+ return { ok: false, data: null };
385
398
  }
386
399
  }
387
400
  /**
@@ -1398,6 +1411,9 @@ var _TrillboardsAds = class _TrillboardsAds {
1398
1411
  this.offlineHandler = null;
1399
1412
  this.visibilityHandler = null;
1400
1413
  this.config = resolveConfig(config);
1414
+ if (this.config.debug) {
1415
+ setLogLevel("debug");
1416
+ }
1401
1417
  this.state = createInitialState();
1402
1418
  this.events = new EventEmitter();
1403
1419
  this.api = new ApiClient(this.config.apiBase);
@@ -1671,11 +1687,104 @@ var _TrillboardsAds = class _TrillboardsAds {
1671
1687
  }
1672
1688
  startHeartbeatTimer() {
1673
1689
  if (this.state.heartbeatTimer) clearInterval(this.state.heartbeatTimer);
1690
+ const tick = () => {
1691
+ this.sendHeartbeatTick().catch(() => {
1692
+ });
1693
+ };
1694
+ tick();
1674
1695
  this.state.heartbeatTimer = setInterval(() => {
1675
- this.api.sendHeartbeat(this.config.deviceId, this.state.screenId);
1696
+ tick();
1676
1697
  }, this.config.heartbeatInterval);
1677
1698
  }
1699
+ async sendHeartbeatTick() {
1700
+ const result = await this.api.sendHeartbeat(
1701
+ this.config.deviceId,
1702
+ this.state.screenId,
1703
+ this.buildHeartbeatPayload()
1704
+ );
1705
+ if (!result.ok || !result.data) return;
1706
+ if (result.data.ad_delivery_profile?.mode) {
1707
+ this.applyAdDeliveryProfile(result.data.ad_delivery_profile);
1708
+ }
1709
+ if (Array.isArray(result.data.commands) && result.data.commands.length > 0) {
1710
+ this.runHeartbeatCommands(result.data.commands);
1711
+ }
1712
+ }
1713
+ buildHeartbeatPayload() {
1714
+ const userAgent = typeof navigator !== "undefined" ? navigator.userAgent : "";
1715
+ const platform = typeof navigator !== "undefined" ? navigator.platform : null;
1716
+ const connection = typeof navigator !== "undefined" ? navigator.connection : null;
1717
+ const chromiumMatch = userAgent.match(/(?:Chrome|Chromium)\/(\d+)/i);
1718
+ const chromiumMajor = chromiumMatch ? Number(chromiumMatch[1]) : void 0;
1719
+ const imaSupported = typeof window !== "undefined" ? Boolean(window?.google?.ima) : null;
1720
+ const telemetry = {
1721
+ powerState: typeof document !== "undefined" && document.hidden ? "standby" : "on",
1722
+ agentStatus: typeof document !== "undefined" && document.hidden ? "background" : "foreground",
1723
+ os: platform || void 0,
1724
+ agentVersion: SDK_VERSION,
1725
+ ima_integration: "html5_webview",
1726
+ ima_supported: imaSupported,
1727
+ webview_chromium_major: Number.isFinite(chromiumMajor) ? chromiumMajor : void 0
1728
+ };
1729
+ const payload = {
1730
+ telemetry,
1731
+ sdk: {
1732
+ version: SDK_VERSION,
1733
+ ima_supported: imaSupported,
1734
+ ima_integration: "html5_webview",
1735
+ webview_chromium_major: Number.isFinite(chromiumMajor) ? chromiumMajor : void 0
1736
+ },
1737
+ device: {
1738
+ os: platform || void 0,
1739
+ model: userAgent || void 0
1740
+ }
1741
+ };
1742
+ if (connection) {
1743
+ payload.network = {
1744
+ connectionType: connection.type || void 0,
1745
+ effectiveType: connection.effectiveType || void 0,
1746
+ downlinkMbps: typeof connection.downlink === "number" ? connection.downlink : void 0,
1747
+ bandwidthMbps: typeof connection.downlink === "number" ? connection.downlink : void 0,
1748
+ rtt: typeof connection.rtt === "number" ? connection.rtt : void 0,
1749
+ latencyRtt: typeof connection.rtt === "number" ? connection.rtt : void 0,
1750
+ saveData: typeof connection.saveData === "boolean" ? connection.saveData : void 0
1751
+ };
1752
+ }
1753
+ return payload;
1754
+ }
1755
+ applyAdDeliveryProfile(profile) {
1756
+ if (!profile?.mode) return;
1757
+ const previousMode = this.state.adDeliveryProfile?.mode ?? null;
1758
+ this.state.adDeliveryProfile = profile;
1759
+ if (profile.mode === "vast_fallback" && this.state.programmaticPlaying) {
1760
+ this.programmaticPlayer.stop({ silent: true });
1761
+ this.state.programmaticPlaying = false;
1762
+ }
1763
+ if (previousMode !== profile.mode) {
1764
+ this.emitStateChanged();
1765
+ }
1766
+ }
1767
+ runHeartbeatCommands(commands) {
1768
+ for (const command of commands) {
1769
+ const name = String(command?.command || "").toLowerCase();
1770
+ if (name === "refresh_ads") {
1771
+ this.refresh().catch(() => {
1772
+ });
1773
+ } else if (name === "restart") {
1774
+ if (typeof window !== "undefined" && typeof window.location?.reload === "function") {
1775
+ window.location.reload();
1776
+ return;
1777
+ }
1778
+ this.refresh().catch(() => {
1779
+ });
1780
+ }
1781
+ }
1782
+ }
1678
1783
  playNextAd() {
1784
+ if (this.state.adDeliveryProfile?.mode === "vast_fallback" && this.state.ads.length > 0) {
1785
+ this.playDirect();
1786
+ return;
1787
+ }
1679
1788
  const mode = this.state.waterfallMode;
1680
1789
  if (mode === "programmatic_only" || mode === "programmatic_then_direct") {
1681
1790
  this.playProgrammatic();
@@ -1690,13 +1799,20 @@ var _TrillboardsAds = class _TrillboardsAds {
1690
1799
  this.programmaticPlayer.play(
1691
1800
  () => {
1692
1801
  this.state.programmaticPlaying = false;
1802
+ this.resetProgrammaticBackoff();
1693
1803
  this.emitStateChanged();
1694
1804
  this.scheduleProgrammaticRetry();
1695
1805
  },
1696
1806
  (error) => {
1697
1807
  this.state.programmaticPlaying = false;
1698
1808
  this.state.programmaticLastError = error;
1699
- this.state.programmaticRetryCount++;
1809
+ const normalizedError = String(error || "").toLowerCase();
1810
+ const isNoFillLike = normalizedError.includes("no fill") || normalizedError.includes("no ads vast response") || normalizedError.includes("waterfall_exhausted") || normalizedError === "throttled" || normalizedError === "busy";
1811
+ if (isNoFillLike) {
1812
+ this.state.programmaticRetryCount = 0;
1813
+ } else {
1814
+ this.state.programmaticRetryCount++;
1815
+ }
1700
1816
  this.emitStateChanged();
1701
1817
  if (this.state.waterfallMode === "programmatic_then_direct" && this.state.ads.length > 0) {
1702
1818
  this.playDirect();
@@ -1806,6 +1922,7 @@ var TrillboardsAds = _TrillboardsAds;
1806
1922
  var TrillboardsError = class extends Error {
1807
1923
  constructor(message, options = {}) {
1808
1924
  super(message);
1925
+ Object.setPrototypeOf(this, new.target.prototype);
1809
1926
  this.name = "TrillboardsError";
1810
1927
  this.type = options.type ?? "api_error";
1811
1928
  this.code = options.code ?? "unknown_error";
@@ -1817,12 +1934,12 @@ var TrillboardsError = class extends Error {
1817
1934
  var TrillboardsAuthenticationError = class extends TrillboardsError {
1818
1935
  constructor(message) {
1819
1936
  super(
1820
- message ?? "Invalid API key. Check your key at https://trillboards.com/developers",
1937
+ message ?? "Invalid API key. Check your key at https://trillboards.com/support/developers",
1821
1938
  {
1822
1939
  type: "authentication_error",
1823
1940
  code: "invalid_api_key",
1824
1941
  statusCode: 401,
1825
- help: "https://trillboards.com/developers"
1942
+ help: "https://trillboards.com/support/developers"
1826
1943
  }
1827
1944
  );
1828
1945
  this.name = "TrillboardsAuthenticationError";