@trillboards/ads-sdk 2.1.1 → 2.3.0

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/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to `@trillboards/ads-sdk` will be documented in this file.
4
4
 
5
+ ## [2.2.0] - 2026-02-24
6
+
7
+ ### Features
8
+ - Heartbeat payloads now include runtime telemetry (`telemetry`, `sdk`, `device`, `network`) for partner-side device profiling.
9
+ - SDK now consumes heartbeat `ad_delivery_profile` responses and prefers direct fallback playback when profile mode is `vast_fallback`.
10
+ - SDK now executes queued heartbeat commands (`refresh_ads`, `restart`) delivered by the partner API polling channel.
11
+
12
+ ### Fixes
13
+ - Programmatic retry/backoff now resets correctly on ad completion and no-fill-like errors to prevent runaway retry escalation.
14
+
5
15
  ## [2.1.1] - 2026-02-15
6
16
 
7
17
  ### Fixes
package/README.md CHANGED
@@ -93,6 +93,14 @@ The `TrillboardsConfig` interface accepts the following options:
93
93
  | `refreshInterval` | `number` | `120000` | Ad refresh interval (ms) |
94
94
  | `cacheSize` | `number` | `10` | Max cached ads |
95
95
 
96
+ ## Heartbeat Telemetry and Delivery Profile
97
+
98
+ Every heartbeat now includes runtime telemetry (`sdk`, `device`, `network`, `ad`) so the partner API can segment legacy devices and return an `ad_delivery_profile`.
99
+
100
+ - `mode: "ima_sdk"` keeps normal IMA playback.
101
+ - `mode: "vast_fallback"` indicates the SDK should prefer direct fallback playback when direct ads are available.
102
+ - Heartbeat command polling now executes queued `refresh_ads` commands immediately.
103
+
96
104
  ## Debug Mode
97
105
 
98
106
  Enable debug logging to see all API requests, state transitions, and cache operations:
@@ -215,7 +223,7 @@ console.log('Tracked:', result.tracked);
215
223
 
216
224
  ## Links
217
225
 
218
- - [Developer documentation](https://trillboards.com/developers/partner-sdk)
226
+ - [Developer documentation](https://trillboards.com/support/developers/partner-sdk)
219
227
  - [API reference](https://api.trillboards.com/docs/partner)
220
228
 
221
229
  ## License
package/dist/cli.js CHANGED
@@ -28,7 +28,7 @@ var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
28
28
  var path__namespace = /*#__PURE__*/_interopNamespace(path);
29
29
 
30
30
  // src/core/config.ts
31
- var SDK_VERSION = "2.1.1";
31
+ var SDK_VERSION = "2.2.0";
32
32
 
33
33
  // src/cli.ts
34
34
  var BANNER = `
@@ -127,7 +127,7 @@ async function runInit() {
127
127
  console.log(" Next steps:");
128
128
  console.log(" 1. Register more devices: POST /v1/partner/device");
129
129
  console.log(" 2. Connect Stripe for payouts: POST /v1/partner/stripe/connect");
130
- console.log(" 3. Read the docs: https://trillboards.com/developers");
130
+ console.log(" 3. Read the docs: https://trillboards.com/support/developers");
131
131
  console.log("");
132
132
  completed = true;
133
133
  } catch (err) {
package/dist/index.d.mts CHANGED
@@ -59,6 +59,7 @@ interface TrillboardsState {
59
59
  programmaticPlaying: boolean;
60
60
  prefetchedReady: boolean;
61
61
  waterfallMode: WaterfallMode;
62
+ adDeliveryProfileMode: AdDeliveryMode | null;
62
63
  screenId: string | null;
63
64
  deviceId: string | null;
64
65
  }
@@ -113,6 +114,37 @@ interface CircuitBreakerState {
113
114
  consecutiveFailures: number;
114
115
  openUntil: number;
115
116
  }
117
+ type AdDeliveryMode = 'ima_sdk' | 'vast_fallback';
118
+ interface AdDeliveryProfile {
119
+ mode: AdDeliveryMode;
120
+ reason?: string;
121
+ auto_recovery_enabled?: boolean;
122
+ ima_integration?: 'android_native' | 'html5_webview' | 'disabled' | 'unknown';
123
+ android_api_level?: number | null;
124
+ webview_chromium_major?: number | null;
125
+ notes?: string[];
126
+ }
127
+ interface HeartbeatCommand {
128
+ id: string;
129
+ command: string;
130
+ payload?: Record<string, unknown>;
131
+ queued_at?: string;
132
+ }
133
+ interface HeartbeatResponseData {
134
+ status?: 'online' | 'offline' | string;
135
+ last_seen_at?: string;
136
+ ad_delivery_profile?: AdDeliveryProfile;
137
+ commands?: HeartbeatCommand[];
138
+ }
139
+ interface HeartbeatPayload {
140
+ telemetry?: Record<string, unknown>;
141
+ metadata?: Record<string, unknown>;
142
+ sdk?: Record<string, unknown>;
143
+ device?: Record<string, unknown>;
144
+ network?: Record<string, unknown>;
145
+ ad?: Record<string, unknown>;
146
+ [key: string]: unknown;
147
+ }
116
148
  interface FetchAdsResult {
117
149
  ads: AdItem[];
118
150
  settings: Record<string, unknown>;
@@ -260,6 +292,10 @@ declare class TrillboardsAds {
260
292
  private refreshAds;
261
293
  private startRefreshTimer;
262
294
  private startHeartbeatTimer;
295
+ private sendHeartbeatTick;
296
+ private buildHeartbeatPayload;
297
+ private applyAdDeliveryProfile;
298
+ private runHeartbeatCommands;
263
299
  private playNextAd;
264
300
  private playProgrammatic;
265
301
  private playDirect;
@@ -317,7 +353,7 @@ declare class EventEmitter {
317
353
  }
318
354
 
319
355
  /** Single source of truth for the SDK version string. */
320
- declare const SDK_VERSION = "2.1.1";
356
+ declare const SDK_VERSION = "2.2.0";
321
357
  /**
322
358
  * Compile-time constants that mirror the original IIFE CONFIG
323
359
  * object. Every value here acts as a sensible production default
@@ -336,7 +372,7 @@ declare const DEFAULT_CONFIG: {
336
372
  readonly PROGRAMMATIC_MIN_INTERVAL_MS: 5000;
337
373
  readonly PROGRAMMATIC_RETRY_MS: 5000;
338
374
  readonly PROGRAMMATIC_BACKOFF_MAX_MS: number;
339
- readonly VERSION: "2.1.1";
375
+ readonly VERSION: "2.2.0";
340
376
  };
341
377
  /**
342
378
  * Merge a caller-supplied TrillboardsConfig with the defaults,
@@ -392,6 +428,7 @@ interface InternalState {
392
428
  programmaticRetryActive: boolean;
393
429
  programmaticRetryCount: number;
394
430
  programmaticLastError: string | null;
431
+ adDeliveryProfile: AdDeliveryProfile | null;
395
432
  initialized: boolean;
396
433
  screenOrientation: string | null;
397
434
  screenDimensions: ScreenDimensions | null;
@@ -537,6 +574,10 @@ declare class WaterfallEngine {
537
574
  recordFailure(sourceName: string): void;
538
575
  }
539
576
 
577
+ interface HeartbeatResult {
578
+ ok: boolean;
579
+ data: HeartbeatResponseData | null;
580
+ }
540
581
  /**
541
582
  * Low-level HTTP client for the Trillboards Partner API.
542
583
  *
@@ -580,10 +621,9 @@ declare class ApiClient {
580
621
  completed: boolean;
581
622
  }): Promise<boolean>;
582
623
  /**
583
- * Ping the heartbeat endpoint to signal this device is alive.
584
- * Returns `true` on 2xx, `false` on any error.
624
+ * Ping heartbeat endpoint and return delivery profile + queued commands.
585
625
  */
586
- sendHeartbeat(deviceId: string, screenId: string | null): Promise<boolean>;
626
+ sendHeartbeat(deviceId: string, screenId: string | null, payload?: HeartbeatPayload): Promise<HeartbeatResult>;
587
627
  /**
588
628
  * Report a programmatic lifecycle event (VAST fill, timeout,
589
629
  * error, etc.) to the analytics backend.
package/dist/index.d.ts CHANGED
@@ -59,6 +59,7 @@ interface TrillboardsState {
59
59
  programmaticPlaying: boolean;
60
60
  prefetchedReady: boolean;
61
61
  waterfallMode: WaterfallMode;
62
+ adDeliveryProfileMode: AdDeliveryMode | null;
62
63
  screenId: string | null;
63
64
  deviceId: string | null;
64
65
  }
@@ -113,6 +114,37 @@ interface CircuitBreakerState {
113
114
  consecutiveFailures: number;
114
115
  openUntil: number;
115
116
  }
117
+ type AdDeliveryMode = 'ima_sdk' | 'vast_fallback';
118
+ interface AdDeliveryProfile {
119
+ mode: AdDeliveryMode;
120
+ reason?: string;
121
+ auto_recovery_enabled?: boolean;
122
+ ima_integration?: 'android_native' | 'html5_webview' | 'disabled' | 'unknown';
123
+ android_api_level?: number | null;
124
+ webview_chromium_major?: number | null;
125
+ notes?: string[];
126
+ }
127
+ interface HeartbeatCommand {
128
+ id: string;
129
+ command: string;
130
+ payload?: Record<string, unknown>;
131
+ queued_at?: string;
132
+ }
133
+ interface HeartbeatResponseData {
134
+ status?: 'online' | 'offline' | string;
135
+ last_seen_at?: string;
136
+ ad_delivery_profile?: AdDeliveryProfile;
137
+ commands?: HeartbeatCommand[];
138
+ }
139
+ interface HeartbeatPayload {
140
+ telemetry?: Record<string, unknown>;
141
+ metadata?: Record<string, unknown>;
142
+ sdk?: Record<string, unknown>;
143
+ device?: Record<string, unknown>;
144
+ network?: Record<string, unknown>;
145
+ ad?: Record<string, unknown>;
146
+ [key: string]: unknown;
147
+ }
116
148
  interface FetchAdsResult {
117
149
  ads: AdItem[];
118
150
  settings: Record<string, unknown>;
@@ -260,6 +292,10 @@ declare class TrillboardsAds {
260
292
  private refreshAds;
261
293
  private startRefreshTimer;
262
294
  private startHeartbeatTimer;
295
+ private sendHeartbeatTick;
296
+ private buildHeartbeatPayload;
297
+ private applyAdDeliveryProfile;
298
+ private runHeartbeatCommands;
263
299
  private playNextAd;
264
300
  private playProgrammatic;
265
301
  private playDirect;
@@ -317,7 +353,7 @@ declare class EventEmitter {
317
353
  }
318
354
 
319
355
  /** Single source of truth for the SDK version string. */
320
- declare const SDK_VERSION = "2.1.1";
356
+ declare const SDK_VERSION = "2.2.0";
321
357
  /**
322
358
  * Compile-time constants that mirror the original IIFE CONFIG
323
359
  * object. Every value here acts as a sensible production default
@@ -336,7 +372,7 @@ declare const DEFAULT_CONFIG: {
336
372
  readonly PROGRAMMATIC_MIN_INTERVAL_MS: 5000;
337
373
  readonly PROGRAMMATIC_RETRY_MS: 5000;
338
374
  readonly PROGRAMMATIC_BACKOFF_MAX_MS: number;
339
- readonly VERSION: "2.1.1";
375
+ readonly VERSION: "2.2.0";
340
376
  };
341
377
  /**
342
378
  * Merge a caller-supplied TrillboardsConfig with the defaults,
@@ -392,6 +428,7 @@ interface InternalState {
392
428
  programmaticRetryActive: boolean;
393
429
  programmaticRetryCount: number;
394
430
  programmaticLastError: string | null;
431
+ adDeliveryProfile: AdDeliveryProfile | null;
395
432
  initialized: boolean;
396
433
  screenOrientation: string | null;
397
434
  screenDimensions: ScreenDimensions | null;
@@ -537,6 +574,10 @@ declare class WaterfallEngine {
537
574
  recordFailure(sourceName: string): void;
538
575
  }
539
576
 
577
+ interface HeartbeatResult {
578
+ ok: boolean;
579
+ data: HeartbeatResponseData | null;
580
+ }
540
581
  /**
541
582
  * Low-level HTTP client for the Trillboards Partner API.
542
583
  *
@@ -580,10 +621,9 @@ declare class ApiClient {
580
621
  completed: boolean;
581
622
  }): Promise<boolean>;
582
623
  /**
583
- * Ping the heartbeat endpoint to signal this device is alive.
584
- * Returns `true` on 2xx, `false` on any error.
624
+ * Ping heartbeat endpoint and return delivery profile + queued commands.
585
625
  */
586
- sendHeartbeat(deviceId: string, screenId: string | null): Promise<boolean>;
626
+ sendHeartbeat(deviceId: string, screenId: string | null, payload?: HeartbeatPayload): Promise<HeartbeatResult>;
587
627
  /**
588
628
  * Report a programmatic lifecycle event (VAST fill, timeout,
589
629
  * error, etc.) to the analytics backend.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  // src/core/config.ts
4
- var SDK_VERSION = "2.1.1";
4
+ var SDK_VERSION = "2.2.0";
5
5
  var DEFAULT_CONFIG = {
6
6
  API_BASE: "https://api.trillboards.com/v1/partner",
7
7
  CDN_BASE: "https://cdn.trillboards.com",
@@ -60,6 +60,7 @@ var DEFAULT_PUBLIC_STATE = {
60
60
  programmaticPlaying: false,
61
61
  prefetchedReady: false,
62
62
  waterfallMode: "programmatic_only",
63
+ adDeliveryProfileMode: null,
63
64
  screenId: null,
64
65
  deviceId: null
65
66
  };
@@ -87,6 +88,7 @@ function createInitialState() {
87
88
  programmaticRetryActive: false,
88
89
  programmaticRetryCount: 0,
89
90
  programmaticLastError: null,
91
+ adDeliveryProfile: null,
90
92
  initialized: false,
91
93
  screenOrientation: null,
92
94
  screenDimensions: null,
@@ -104,6 +106,7 @@ function getPublicState(internal) {
104
106
  programmaticPlaying: internal.programmaticPlaying,
105
107
  prefetchedReady: internal.prefetchedReady ?? false,
106
108
  waterfallMode: internal.waterfallMode,
109
+ adDeliveryProfileMode: internal.adDeliveryProfile?.mode ?? null,
107
110
  screenId: internal.screenId,
108
111
  deviceId: internal.deviceId
109
112
  };
@@ -364,10 +367,9 @@ var ApiClient = class {
364
367
  }
365
368
  }
366
369
  /**
367
- * Ping the heartbeat endpoint to signal this device is alive.
368
- * Returns `true` on 2xx, `false` on any error.
370
+ * Ping heartbeat endpoint and return delivery profile + queued commands.
369
371
  */
370
- async sendHeartbeat(deviceId, screenId) {
372
+ async sendHeartbeat(deviceId, screenId, payload = {}) {
371
373
  logger.debug("Sending heartbeat", { deviceId });
372
374
  try {
373
375
  const response = await fetch(`${this.apiBase}/device/${deviceId}/heartbeat`, {
@@ -377,13 +379,24 @@ var ApiClient = class {
377
379
  device_id: deviceId,
378
380
  screen_id: screenId,
379
381
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
380
- status: "active"
382
+ status: "active",
383
+ ...payload
381
384
  }),
382
385
  signal: AbortSignal.timeout(5e3)
383
386
  });
384
- return response.ok;
387
+ if (!response.ok) {
388
+ return { ok: false, data: null };
389
+ }
390
+ let data = null;
391
+ try {
392
+ const json = await response.json();
393
+ data = json?.data ?? null;
394
+ } catch {
395
+ data = null;
396
+ }
397
+ return { ok: true, data };
385
398
  } catch {
386
- return false;
399
+ return { ok: false, data: null };
387
400
  }
388
401
  }
389
402
  /**
@@ -936,7 +949,7 @@ var WaterfallEngine = class {
936
949
  };
937
950
 
938
951
  // src/player/ProgrammaticPlayer.ts
939
- var ProgrammaticPlayer = class {
952
+ var _ProgrammaticPlayer = class _ProgrammaticPlayer {
940
953
  constructor(events, timeoutMs = 12e3) {
941
954
  // ── Public state ──────────────────────────────────────────
942
955
  this.vastTagUrl = null;
@@ -1023,8 +1036,11 @@ var ProgrammaticPlayer = class {
1023
1036
  this.telemetry.recordRequest();
1024
1037
  const correlator = Date.now();
1025
1038
  const finalUrl = vastUrl.includes("correlator=") ? vastUrl.replace(/correlator=[^&]*/, `correlator=${correlator}`) : `${vastUrl}${vastUrl.includes("?") ? "&" : "?"}correlator=${correlator}`;
1026
- if (typeof google === "undefined" || !google?.ima) {
1027
- onError("Google IMA SDK not loaded");
1039
+ const imaReady = await this.ensureImaSdk();
1040
+ if (!imaReady) {
1041
+ const msg = "Google IMA SDK not loaded";
1042
+ this.events.emit("programmatic_error", { error: msg, code: void 0 });
1043
+ onError(msg);
1028
1044
  this.telemetry.recordError();
1029
1045
  this.waterfallEngine.recordFailure(sourceName);
1030
1046
  return;
@@ -1041,6 +1057,50 @@ var ProgrammaticPlayer = class {
1041
1057
  onError(err instanceof Error ? err.message : String(err));
1042
1058
  }
1043
1059
  }
1060
+ /**
1061
+ * Ensure Google IMA SDK is available. If already loaded, resolves
1062
+ * immediately. Otherwise injects the `<script>` tag and waits.
1063
+ * Matches the Lite SDK's `loadImaScript()` pattern.
1064
+ */
1065
+ async ensureImaSdk() {
1066
+ if (typeof google !== "undefined" && google?.ima) return true;
1067
+ if (typeof document === "undefined") return false;
1068
+ const existing = document.querySelector(
1069
+ `script[src="${_ProgrammaticPlayer.IMA_SDK_URL}"]`
1070
+ );
1071
+ if (existing) {
1072
+ return new Promise((resolve) => {
1073
+ const start = Date.now();
1074
+ const check = () => {
1075
+ if (typeof google !== "undefined" && google?.ima) {
1076
+ resolve(true);
1077
+ } else if (Date.now() - start > 5e3) {
1078
+ resolve(false);
1079
+ } else {
1080
+ setTimeout(check, 100);
1081
+ }
1082
+ };
1083
+ check();
1084
+ });
1085
+ }
1086
+ if (!_ProgrammaticPlayer.imaLoadPromise) {
1087
+ _ProgrammaticPlayer.imaLoadPromise = new Promise((resolve) => {
1088
+ const script = document.createElement("script");
1089
+ script.src = _ProgrammaticPlayer.IMA_SDK_URL;
1090
+ script.async = true;
1091
+ script.onload = () => {
1092
+ _ProgrammaticPlayer.imaLoadPromise = null;
1093
+ resolve(true);
1094
+ };
1095
+ script.onerror = () => {
1096
+ _ProgrammaticPlayer.imaLoadPromise = null;
1097
+ resolve(false);
1098
+ };
1099
+ document.head.appendChild(script);
1100
+ });
1101
+ }
1102
+ return _ProgrammaticPlayer.imaLoadPromise;
1103
+ }
1044
1104
  // ── IMA request / playback lifecycle ──────────────────────
1045
1105
  async requestAdsViaIMA(vastUrl, onComplete, onError) {
1046
1106
  return new Promise((resolve) => {
@@ -1242,6 +1302,12 @@ var ProgrammaticPlayer = class {
1242
1302
  this.telemetry.reset();
1243
1303
  }
1244
1304
  };
1305
+ // ── IMA SDK loader ──────────────────────────────────────────
1306
+ /** IMA script URL */
1307
+ _ProgrammaticPlayer.IMA_SDK_URL = "https://imasdk.googleapis.com/js/sdkloader/ima3.js";
1308
+ /** Singleton load promise so concurrent calls don't insert multiple scripts. */
1309
+ _ProgrammaticPlayer.imaLoadPromise = null;
1310
+ var ProgrammaticPlayer = _ProgrammaticPlayer;
1245
1311
 
1246
1312
  // src/player/Player.ts
1247
1313
  var MAX_AD_DURATION_SECONDS = 300;
@@ -1676,11 +1742,104 @@ var _TrillboardsAds = class _TrillboardsAds {
1676
1742
  }
1677
1743
  startHeartbeatTimer() {
1678
1744
  if (this.state.heartbeatTimer) clearInterval(this.state.heartbeatTimer);
1745
+ const tick = () => {
1746
+ this.sendHeartbeatTick().catch(() => {
1747
+ });
1748
+ };
1749
+ tick();
1679
1750
  this.state.heartbeatTimer = setInterval(() => {
1680
- this.api.sendHeartbeat(this.config.deviceId, this.state.screenId);
1751
+ tick();
1681
1752
  }, this.config.heartbeatInterval);
1682
1753
  }
1754
+ async sendHeartbeatTick() {
1755
+ const result = await this.api.sendHeartbeat(
1756
+ this.config.deviceId,
1757
+ this.state.screenId,
1758
+ this.buildHeartbeatPayload()
1759
+ );
1760
+ if (!result.ok || !result.data) return;
1761
+ if (result.data.ad_delivery_profile?.mode) {
1762
+ this.applyAdDeliveryProfile(result.data.ad_delivery_profile);
1763
+ }
1764
+ if (Array.isArray(result.data.commands) && result.data.commands.length > 0) {
1765
+ this.runHeartbeatCommands(result.data.commands);
1766
+ }
1767
+ }
1768
+ buildHeartbeatPayload() {
1769
+ const userAgent = typeof navigator !== "undefined" ? navigator.userAgent : "";
1770
+ const platform = typeof navigator !== "undefined" ? navigator.platform : null;
1771
+ const connection = typeof navigator !== "undefined" ? navigator.connection : null;
1772
+ const chromiumMatch = userAgent.match(/(?:Chrome|Chromium)\/(\d+)/i);
1773
+ const chromiumMajor = chromiumMatch ? Number(chromiumMatch[1]) : void 0;
1774
+ const imaSupported = typeof window !== "undefined" ? Boolean(window?.google?.ima) : null;
1775
+ const telemetry = {
1776
+ powerState: typeof document !== "undefined" && document.hidden ? "standby" : "on",
1777
+ agentStatus: typeof document !== "undefined" && document.hidden ? "background" : "foreground",
1778
+ os: platform || void 0,
1779
+ agentVersion: SDK_VERSION,
1780
+ ima_integration: "html5_webview",
1781
+ ima_supported: imaSupported,
1782
+ webview_chromium_major: Number.isFinite(chromiumMajor) ? chromiumMajor : void 0
1783
+ };
1784
+ const payload = {
1785
+ telemetry,
1786
+ sdk: {
1787
+ version: SDK_VERSION,
1788
+ ima_supported: imaSupported,
1789
+ ima_integration: "html5_webview",
1790
+ webview_chromium_major: Number.isFinite(chromiumMajor) ? chromiumMajor : void 0
1791
+ },
1792
+ device: {
1793
+ os: platform || void 0,
1794
+ model: userAgent || void 0
1795
+ }
1796
+ };
1797
+ if (connection) {
1798
+ payload.network = {
1799
+ connectionType: connection.type || void 0,
1800
+ effectiveType: connection.effectiveType || void 0,
1801
+ downlinkMbps: typeof connection.downlink === "number" ? connection.downlink : void 0,
1802
+ bandwidthMbps: typeof connection.downlink === "number" ? connection.downlink : void 0,
1803
+ rtt: typeof connection.rtt === "number" ? connection.rtt : void 0,
1804
+ latencyRtt: typeof connection.rtt === "number" ? connection.rtt : void 0,
1805
+ saveData: typeof connection.saveData === "boolean" ? connection.saveData : void 0
1806
+ };
1807
+ }
1808
+ return payload;
1809
+ }
1810
+ applyAdDeliveryProfile(profile) {
1811
+ if (!profile?.mode) return;
1812
+ const previousMode = this.state.adDeliveryProfile?.mode ?? null;
1813
+ this.state.adDeliveryProfile = profile;
1814
+ if (profile.mode === "vast_fallback" && this.state.programmaticPlaying) {
1815
+ this.programmaticPlayer.stop({ silent: true });
1816
+ this.state.programmaticPlaying = false;
1817
+ }
1818
+ if (previousMode !== profile.mode) {
1819
+ this.emitStateChanged();
1820
+ }
1821
+ }
1822
+ runHeartbeatCommands(commands) {
1823
+ for (const command of commands) {
1824
+ const name = String(command?.command || "").toLowerCase();
1825
+ if (name === "refresh_ads") {
1826
+ this.refresh().catch(() => {
1827
+ });
1828
+ } else if (name === "restart") {
1829
+ if (typeof window !== "undefined" && typeof window.location?.reload === "function") {
1830
+ window.location.reload();
1831
+ return;
1832
+ }
1833
+ this.refresh().catch(() => {
1834
+ });
1835
+ }
1836
+ }
1837
+ }
1683
1838
  playNextAd() {
1839
+ if (this.state.adDeliveryProfile?.mode === "vast_fallback" && this.state.ads.length > 0) {
1840
+ this.playDirect();
1841
+ return;
1842
+ }
1684
1843
  const mode = this.state.waterfallMode;
1685
1844
  if (mode === "programmatic_only" || mode === "programmatic_then_direct") {
1686
1845
  this.playProgrammatic();
@@ -1695,13 +1854,20 @@ var _TrillboardsAds = class _TrillboardsAds {
1695
1854
  this.programmaticPlayer.play(
1696
1855
  () => {
1697
1856
  this.state.programmaticPlaying = false;
1857
+ this.resetProgrammaticBackoff();
1698
1858
  this.emitStateChanged();
1699
1859
  this.scheduleProgrammaticRetry();
1700
1860
  },
1701
1861
  (error) => {
1702
1862
  this.state.programmaticPlaying = false;
1703
1863
  this.state.programmaticLastError = error;
1704
- this.state.programmaticRetryCount++;
1864
+ const normalizedError = String(error || "").toLowerCase();
1865
+ const isNoFillLike = normalizedError.includes("no fill") || normalizedError.includes("no ads vast response") || normalizedError.includes("waterfall_exhausted") || normalizedError === "throttled" || normalizedError === "busy";
1866
+ if (isNoFillLike) {
1867
+ this.state.programmaticRetryCount = 0;
1868
+ } else {
1869
+ this.state.programmaticRetryCount++;
1870
+ }
1705
1871
  this.emitStateChanged();
1706
1872
  if (this.state.waterfallMode === "programmatic_then_direct" && this.state.ads.length > 0) {
1707
1873
  this.playDirect();
@@ -1823,12 +1989,12 @@ var TrillboardsError = class extends Error {
1823
1989
  var TrillboardsAuthenticationError = class extends TrillboardsError {
1824
1990
  constructor(message) {
1825
1991
  super(
1826
- message ?? "Invalid API key. Check your key at https://trillboards.com/developers",
1992
+ message ?? "Invalid API key. Check your key at https://trillboards.com/support/developers",
1827
1993
  {
1828
1994
  type: "authentication_error",
1829
1995
  code: "invalid_api_key",
1830
1996
  statusCode: 401,
1831
- help: "https://trillboards.com/developers"
1997
+ help: "https://trillboards.com/support/developers"
1832
1998
  }
1833
1999
  );
1834
2000
  this.name = "TrillboardsAuthenticationError";