@coframe/sdk 1.1.44 → 1.1.46

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.
@@ -35,6 +35,22 @@ if (!Array.prototype.includes) {
35
35
  enumerable: false,
36
36
  });
37
37
  }
38
+ if (!String.prototype.includes) {
39
+ Object.defineProperty(String.prototype, 'includes', {
40
+ value: function (search, position) {
41
+ if (this == null) {
42
+ throw new TypeError('String.prototype.includes called on null or undefined');
43
+ }
44
+ if (search instanceof RegExp) {
45
+ throw new TypeError('First argument to String.prototype.includes must not be a regular expression');
46
+ }
47
+ return String(this).indexOf(search, position) !== -1;
48
+ },
49
+ writable: true,
50
+ configurable: true,
51
+ enumerable: false,
52
+ });
53
+ }
38
54
  if (!Array.prototype.flatMap) {
39
55
  Object.defineProperty(Array.prototype, 'flatMap', {
40
56
  value: function (callback, thisArg) {
@@ -1126,7 +1142,7 @@ function warnEventsNotSent() {
1126
1142
  }
1127
1143
  }
1128
1144
 
1129
- const SDK_VERSION = '2.9.0';
1145
+ const SDK_VERSION = '2.10.0';
1130
1146
  const queuedEvents = [];
1131
1147
  const queuedMonitoringEvents = [];
1132
1148
  const queuedSessions = [];
@@ -1474,6 +1490,7 @@ const applyUpdateModification = (element, args, logger, logMetadata = {}) => {
1474
1490
  return true;
1475
1491
  };
1476
1492
  const executeJs = (jsCode, args = {}, opts = {}) => {
1493
+ var _a, _b, _c;
1477
1494
  const key = `__cf_${Math.random().toString(36).slice(2, 12)}`;
1478
1495
  const w = window;
1479
1496
  w.Coframe = w.Coframe || {};
@@ -1482,11 +1499,15 @@ const executeJs = (jsCode, args = {}, opts = {}) => {
1482
1499
  if (opts.elementRef) {
1483
1500
  w.Coframe.vars[elKey] = opts.elementRef;
1484
1501
  }
1502
+ const refKeys = Object.keys((_a = opts.refs) !== null && _a !== void 0 ? _a : {});
1503
+ for (const refName of refKeys) {
1504
+ w.Coframe.vars[`${key}_${refName}`] = (_b = opts.refs) === null || _b === void 0 ? void 0 : _b[refName];
1505
+ }
1485
1506
  const invocation = opts.isExpression
1486
1507
  ? `(\n${jsCode}\n)(__args)`
1487
1508
  : opts.elementRef
1488
- ? `(function(el){\n${jsCode}\n})(__args.el)`
1489
- : `(function(){\n${jsCode}\n})()`;
1509
+ ? `(function(variantSession){return (function(el){\n${jsCode}\n})(__args.el)})(__args.variantSession)`
1510
+ : `(function(variantSession){return (function(){\n${jsCode}\n})()})(__args.variantSession)`;
1490
1511
  const ranKey = `${key}_ran`;
1491
1512
  const script = document.createElement('script');
1492
1513
  script.id = `coframe-script-${key}`;
@@ -1497,31 +1518,46 @@ const executeJs = (jsCode, args = {}, opts = {}) => {
1497
1518
  try {
1498
1519
  var __args = ${JSON.stringify(args)};
1499
1520
  ${opts.elementRef ? `__args.el = window.Coframe.vars[${JSON.stringify(elKey)}];` : ''}
1521
+ ${refKeys
1522
+ .map((refName) => `__args[${JSON.stringify(refName)}] = window.Coframe.vars[${JSON.stringify(`${key}_${refName}`)}];`)
1523
+ .join('\n ')}
1500
1524
  window.Coframe.vars[${JSON.stringify(key)}] = ${invocation};
1501
1525
  } catch (e) {
1502
- window.Coframe.vars[${JSON.stringify(`${key}_err`)}] = (e && (e.stack || e.message)) ? String(e.stack || e.message) : (String(e) || 'Unknown error');
1526
+ var __err = 'Unknown error';
1527
+ try { __err = (e && (e.stack || e.message)) ? String(e.stack || e.message) : (String(e) || __err); } catch (_) {}
1528
+ window.Coframe.vars[${JSON.stringify(`${key}_err`)}] = __err;
1503
1529
  }
1504
1530
  window.Coframe.vars[${JSON.stringify(ranKey)}] = true;
1505
1531
  })();
1506
1532
  `;
1507
- document.head.appendChild(script);
1508
- document.head.removeChild(script);
1509
- const value = w.Coframe.vars[key];
1510
- const err = w.Coframe.vars[`${key}_err`];
1511
- const ran = w.Coframe.vars[ranKey];
1512
- delete w.Coframe.vars[key];
1513
- delete w.Coframe.vars[`${key}_err`];
1514
- delete w.Coframe.vars[ranKey];
1515
- if (opts.elementRef)
1516
- delete w.Coframe.vars[elKey];
1533
+ let value;
1534
+ let err;
1535
+ let ran;
1536
+ try {
1537
+ document.head.appendChild(script);
1538
+ value = w.Coframe.vars[key];
1539
+ err = w.Coframe.vars[`${key}_err`];
1540
+ ran = w.Coframe.vars[ranKey];
1541
+ }
1542
+ finally {
1543
+ (_c = script.parentNode) === null || _c === void 0 ? void 0 : _c.removeChild(script);
1544
+ delete w.Coframe.vars[key];
1545
+ delete w.Coframe.vars[`${key}_err`];
1546
+ delete w.Coframe.vars[ranKey];
1547
+ if (opts.elementRef)
1548
+ delete w.Coframe.vars[elKey];
1549
+ for (const refName of refKeys) {
1550
+ delete w.Coframe.vars[`${key}_${refName}`];
1551
+ }
1552
+ }
1517
1553
  if (err !== undefined)
1518
- throw new Error(err);
1554
+ throw new Error(String(err));
1519
1555
  if (!ran) {
1520
1556
  throw new SyntaxError('Failed to parse JavaScript (likely a syntax error in variant code)');
1521
1557
  }
1522
1558
  return value;
1523
1559
  };
1524
- const applyJavascriptModification = (element, args, logger, nonce, logMetadata = {}) => {
1560
+ const applyJavascriptModification = (element, args, logger, nonce, logMetadata = {}, refs) => {
1525
1561
  var _a, _b;
1526
1562
  const javascript = args.js;
1527
1563
  try {
@@ -1537,6 +1573,7 @@ const applyJavascriptModification = (element, args, logger, nonce, logMetadata =
1537
1573
  executeJs(javascript, {}, {
1538
1574
  nonce,
1539
1575
  elementRef: isPageElement(element) ? undefined : element,
1576
+ refs,
1540
1577
  });
1541
1578
  return true;
1542
1579
  }
@@ -1544,9 +1581,7 @@ const applyJavascriptModification = (element, args, logger, nonce, logMetadata =
1544
1581
  if (logger === null || logger === void 0 ? void 0 : logger.error) {
1545
1582
  logger.error('Error executing JavaScript:', 'error_execute_js', {
1546
1583
  ...logMetadata,
1547
- error: error instanceof Error
1548
- ? error.stack || error.message
1549
- : String(error),
1584
+ error: error instanceof Error ? error.stack || error.message : String(error),
1550
1585
  javascript,
1551
1586
  element: (_b = (_a = element.outerHTML) === null || _a === void 0 ? void 0 : _a.slice(0, 200)) !== null && _b !== void 0 ? _b : element.tagName,
1552
1587
  });
@@ -1836,7 +1871,7 @@ function findLastCmd(predicate, queueName = QueueName.CFQ) {
1836
1871
  }
1837
1872
  return undefined;
1838
1873
  }
1839
- function waitForCmd(predicate, coframe, timeoutMs = 3600000, queueName = QueueName.CFQ) {
1874
+ function waitForCmd(predicate, coframe, timeoutMs = 3600000, queueName = QueueName.CFQ, reportTimeout = true) {
1840
1875
  return new Promise((resolve) => {
1841
1876
  const existing = findLastCmd(predicate, queueName);
1842
1877
  if (existing) {
@@ -1851,7 +1886,7 @@ function waitForCmd(predicate, coframe, timeoutMs = 3600000, queueName = QueueNa
1851
1886
  const timer = setTimeout(() => {
1852
1887
  stop();
1853
1888
  const error = new Error(`Timeout waiting for command after ${timeoutMs}ms for:\n${predicate.toString()}`);
1854
- if ((coframe === null || coframe === void 0 ? void 0 : coframe.projectId) && (coframe === null || coframe === void 0 ? void 0 : coframe.sessionId)) {
1889
+ if ((coframe === null || coframe === void 0 ? void 0 : coframe.projectId) && (coframe === null || coframe === void 0 ? void 0 : coframe.sessionId) && reportTimeout) {
1855
1890
  reportException({
1856
1891
  err: error,
1857
1892
  orgId: coframe.orgId || '',
@@ -1876,6 +1911,42 @@ function subscribeToCmd(predicate, callback, queueName = QueueName.CFQ) {
1876
1911
  return __addListener(predicate, callback, queueName);
1877
1912
  }
1878
1913
 
1914
+ function describeError(error) {
1915
+ if (error && (typeof error === 'object' || typeof error === 'function')) {
1916
+ const stack = readString(() => error.stack);
1917
+ const message = readString(() => error.message);
1918
+ const header = readString(() => String(error));
1919
+ const detail = withHeaderLine(stack, header, message);
1920
+ if (detail)
1921
+ return detail;
1922
+ }
1923
+ try {
1924
+ return String(error);
1925
+ }
1926
+ catch (_a) {
1927
+ return 'Unprintable error';
1928
+ }
1929
+ }
1930
+ function readString(read) {
1931
+ try {
1932
+ const value = read();
1933
+ return typeof value === 'string' ? value : '';
1934
+ }
1935
+ catch (_a) {
1936
+ return '';
1937
+ }
1938
+ }
1939
+ function withHeaderLine(stack, header, message) {
1940
+ if (!stack)
1941
+ return header || message;
1942
+ if (!header) {
1943
+ return message && stack.indexOf(message) === -1
1944
+ ? `${message}\n${stack}`
1945
+ : stack;
1946
+ }
1947
+ return stack.indexOf(header) === 0 ? stack : `${header}\n${stack}`;
1948
+ }
1949
+
1879
1950
  function querySelector(config) {
1880
1951
  const result = findElementsRecursive(config);
1881
1952
  if (result !== null && result.length === 0) {
@@ -1980,7 +2051,11 @@ function variantRendered() {
1980
2051
  emit: 'variantRendered',
1981
2052
  });
1982
2053
  }
1983
- const applyVariantChanges = (variantElements, currentUrl, logger, nonce) => {
2054
+ function hasJavascriptModification(variantElements) {
2055
+ const modifications = variantElements.variant.modifications || {};
2056
+ return Object.keys(modifications).some((elementId) => (modifications[elementId] || []).some((modification) => modification.type === 'execute_js'));
2057
+ }
2058
+ const applyVariantChanges = (variantElements, currentUrl, logger, nonce, lifecycleContext) => {
1984
2059
  var _a;
1985
2060
  const { variant, elements } = variantElements;
1986
2061
  const variantMetadata = {
@@ -1988,12 +2063,18 @@ const applyVariantChanges = (variantElements, currentUrl, logger, nonce) => {
1988
2063
  experiment_id: variant.experiment_id,
1989
2064
  };
1990
2065
  setApplyVariantChangesStarted(true);
2066
+ const session = lifecycleContext && hasJavascriptModification(variantElements)
2067
+ ? lifecycleContext.runtime.createVariantSession({
2068
+ batchId: lifecycleContext.batchId,
2069
+ variantId: variant.id,
2070
+ })
2071
+ : null;
1991
2072
  if (!variant.modifications ||
1992
2073
  Object.keys(variant.modifications).length === 0) {
1993
2074
  variantRendered();
1994
2075
  return true;
1995
2076
  }
1996
- for (const [elementId, _] of Object.entries(variant.modifications)) {
2077
+ for (const elementId of Object.keys(variant.modifications)) {
1997
2078
  const elementSelectors = elements[elementId];
1998
2079
  let selectedElement = null;
1999
2080
  for (const selector of elementSelectors || []) {
@@ -2008,23 +2089,36 @@ const applyVariantChanges = (variantElements, currentUrl, logger, nonce) => {
2008
2089
  variant_id: variant.id,
2009
2090
  experiment_id: variant.experiment_id,
2010
2091
  });
2092
+ if (session &&
2093
+ (variant.modifications[elementId] || []).some((modification) => modification.type === 'execute_js')) {
2094
+ session.facade.error(new Error(`Element not found for JavaScript modification: ${elementId}`));
2095
+ }
2011
2096
  continue;
2012
2097
  }
2013
- const edited = editElement(selectedElement, variant.modifications, elementId, logger, nonce, variantMetadata);
2014
- if (edited && selectedElement instanceof HTMLElement) {
2015
- selectedElement.setAttribute('coframe-exp-id', variant.id);
2016
- }
2017
- else {
2098
+ const edited = editElement(selectedElement, variant.modifications, elementId, logger, nonce, variantMetadata, session === null || session === void 0 ? void 0 : session.facade);
2099
+ if (!edited) {
2018
2100
  logger.error('element not edited', 'element_not_edited', {
2019
2101
  ...variantMetadata,
2020
- selectedElement,
2102
+ selectedElement: describeElement(selectedElement),
2021
2103
  elementSelectors,
2022
2104
  });
2023
2105
  }
2106
+ else if (selectedElement instanceof HTMLElement) {
2107
+ selectedElement.setAttribute('coframe-exp-id', variant.id);
2108
+ }
2024
2109
  }
2025
2110
  return true;
2026
2111
  };
2027
- const editElement = (element, modifications, elementId, logger, nonce, logMetadata = {}) => {
2112
+ function describeElement(element) {
2113
+ if (!element || typeof element.getAttribute !== 'function') {
2114
+ return String(element);
2115
+ }
2116
+ const target = element;
2117
+ const id = target.getAttribute('id');
2118
+ const className = target.getAttribute('class');
2119
+ return `<${String(target.tagName).toLowerCase()}${id ? ` id=${JSON.stringify(id)}` : ''}${className ? ` class=${JSON.stringify(className)}` : ''}>`.slice(0, 200);
2120
+ }
2121
+ const editElement = (element, modifications, elementId, logger, nonce, logMetadata = {}, variantSession) => {
2028
2122
  if (!modifications[elementId]) {
2029
2123
  logger.warn('no modifications for element', 'no_modifications_for_element', {
2030
2124
  ...logMetadata,
@@ -2036,7 +2130,11 @@ const editElement = (element, modifications, elementId, logger, nonce, logMetada
2036
2130
  for (const content of modifications[elementId]) {
2037
2131
  switch (content.type) {
2038
2132
  case 'execute_js':
2039
- applyJavascriptModification(element, content, logger, nonce, logMetadata);
2133
+ if (!applyJavascriptModification(element, content, logger, nonce, logMetadata, variantSession ? { variantSession } : undefined) &&
2134
+ variantSession) {
2135
+ variantSession.error(new Error('JavaScript variant modification did not run'));
2136
+ return false;
2137
+ }
2040
2138
  break;
2041
2139
  case 'update':
2042
2140
  applyUpdateModification(element, content, logger, logMetadata);
@@ -2093,13 +2191,16 @@ function revealPage() {
2093
2191
  document.documentElement.style.setProperty('--cf-show', '1');
2094
2192
  document.documentElement.style.setProperty('--cf-visibility', 'visible');
2095
2193
  }
2096
- async function replaceVariants(variants, orgId, logger, nonce) {
2194
+ async function replaceVariants(variants, orgId, logger, nonce, runtime) {
2097
2195
  const failedReplacements = [];
2098
2196
  let currentVariant;
2197
+ const batch = runtime === null || runtime === void 0 ? void 0 : runtime.beginBatch({
2198
+ expectedSessionCount: variants.filter(hasJavascriptModification).length,
2199
+ });
2099
2200
  try {
2100
2201
  for (const v of variants) {
2101
2202
  currentVariant = v.variant;
2102
- if (!applyVariantChanges(v, getCurrentPageUrl(), logger, nonce)) {
2203
+ if (!applyVariantChanges(v, getCurrentPageUrl(), logger, nonce, batch && runtime ? { runtime, batchId: batch.batchId } : undefined)) {
2103
2204
  failedReplacements.push({
2104
2205
  message: 'Original element not found',
2105
2206
  metadata: {
@@ -2113,10 +2214,11 @@ async function replaceVariants(variants, orgId, logger, nonce) {
2113
2214
  }
2114
2215
  }
2115
2216
  catch (error) {
2116
- const errorMessage = error instanceof Error ? error.stack || error.message : String(error);
2217
+ const errorMessage = describeError(error);
2218
+ const description = errorMessage.split('\n')[0];
2117
2219
  failedReplacements.push({
2118
2220
  metadata: {
2119
- description: error instanceof Error ? error.toString() : String(error),
2221
+ description,
2120
2222
  org_id: orgId,
2121
2223
  variant_id: currentVariant === null || currentVariant === void 0 ? void 0 : currentVariant.id,
2122
2224
  experiment_id: currentVariant === null || currentVariant === void 0 ? void 0 : currentVariant.experiment_id,
@@ -2124,6 +2226,10 @@ async function replaceVariants(variants, orgId, logger, nonce) {
2124
2226
  message: errorMessage,
2125
2227
  });
2126
2228
  }
2229
+ finally {
2230
+ if (batch && runtime)
2231
+ runtime.closeBatch(batch.batchId);
2232
+ }
2127
2233
  for (const event of failedReplacements) {
2128
2234
  logger.error(event.message, 'failed_replacement', event.metadata);
2129
2235
  }
@@ -2499,7 +2605,8 @@ function urlConditionMatches(conditionUrl, currentUrl, baseUrlMatch, logger) {
2499
2605
  }
2500
2606
  }
2501
2607
  function urlsMatch(conditionUrl, currentUrl, baseUrlMatch) {
2502
- if (currentUrl.includes('localhost') || currentUrl.includes('0.0.0.0')) {
2608
+ if (currentUrl.indexOf('localhost') !== -1 ||
2609
+ currentUrl.indexOf('0.0.0.0') !== -1) {
2503
2610
  const conditionUrlObj = new URL(conditionUrl);
2504
2611
  const currentUrlObj = new URL(currentUrl);
2505
2612
  const normalizePath = (path) => path.endsWith('/') ? path.slice(0, -1) : path;
@@ -3318,8 +3425,10 @@ function rebindMetrics(coframe, logger, metrics, nonce) {
3318
3425
  }
3319
3426
 
3320
3427
  let lastUrl = null;
3321
- function observeUrlChanges(baseUrlMatch, onUrlChange) {
3428
+ function observeUrlChanges(baseUrlMatch, onUrlChange, isActive = () => true) {
3322
3429
  const tryTrack = () => {
3430
+ if (!isActive())
3431
+ return;
3323
3432
  const currentUrl = getCurrentPageUrl();
3324
3433
  const previousUrl = lastUrl;
3325
3434
  if (previousUrl && !urlsMatch(currentUrl, previousUrl, baseUrlMatch)) {
@@ -3343,6 +3452,200 @@ function observeUrlChanges(baseUrlMatch, onUrlChange) {
3343
3452
  patchHistoryMethod('replaceState');
3344
3453
  }
3345
3454
 
3455
+ function isVariantRuntimeLifecycleEnabled(config) {
3456
+ return config.deferHydrationWaitToVariant === true;
3457
+ }
3458
+ function shouldSdkWaitForHydration(config) {
3459
+ return (Boolean(config.waitForHydration) &&
3460
+ !isVariantRuntimeLifecycleEnabled(config));
3461
+ }
3462
+
3463
+ function createRevealCoordinator(revealPage, reportError = () => { }) {
3464
+ const notify = (listener, reason) => {
3465
+ try {
3466
+ listener(reason);
3467
+ }
3468
+ catch (error) {
3469
+ try {
3470
+ reportError(error);
3471
+ }
3472
+ catch (_a) {
3473
+ }
3474
+ }
3475
+ };
3476
+ let pageRevealed = false;
3477
+ let activeBlockerId = null;
3478
+ let revealedReason = null;
3479
+ const pendingReasons = [];
3480
+ const revealedListeners = new Set();
3481
+ const reveal = (reason) => {
3482
+ if (pageRevealed)
3483
+ return;
3484
+ pageRevealed = true;
3485
+ activeBlockerId = null;
3486
+ pendingReasons.length = 0;
3487
+ revealedReason = reason;
3488
+ let revealError = null;
3489
+ let revealFailed = false;
3490
+ try {
3491
+ revealPage();
3492
+ }
3493
+ catch (error) {
3494
+ revealFailed = true;
3495
+ revealError = error;
3496
+ }
3497
+ for (const listener of Array.from(revealedListeners)) {
3498
+ notify(listener, reason);
3499
+ }
3500
+ if (revealFailed)
3501
+ throw revealError;
3502
+ };
3503
+ return {
3504
+ requestReveal(reason) {
3505
+ if (pageRevealed)
3506
+ return;
3507
+ if (activeBlockerId) {
3508
+ if (!pendingReasons.includes(reason))
3509
+ pendingReasons.push(reason);
3510
+ return;
3511
+ }
3512
+ reveal(reason);
3513
+ },
3514
+ forceReveal(reason) {
3515
+ reveal(reason);
3516
+ },
3517
+ block(batchId) {
3518
+ if (pageRevealed)
3519
+ return false;
3520
+ activeBlockerId = batchId;
3521
+ return true;
3522
+ },
3523
+ release(batchId, reason) {
3524
+ if (pageRevealed || activeBlockerId !== batchId)
3525
+ return;
3526
+ activeBlockerId = null;
3527
+ reveal(reason);
3528
+ },
3529
+ unblock(batchId) {
3530
+ if (activeBlockerId === batchId)
3531
+ activeBlockerId = null;
3532
+ },
3533
+ flushPending() {
3534
+ if (pageRevealed || activeBlockerId || pendingReasons.length === 0)
3535
+ return;
3536
+ reveal(pendingReasons[0]);
3537
+ },
3538
+ onRevealed(handler) {
3539
+ revealedListeners.add(handler);
3540
+ return () => revealedListeners.delete(handler);
3541
+ },
3542
+ getSnapshot() {
3543
+ return {
3544
+ pageRevealed,
3545
+ activeBlockerId,
3546
+ pendingReasons: [...pendingReasons],
3547
+ revealedReason,
3548
+ };
3549
+ },
3550
+ };
3551
+ }
3552
+ const logRevealListenerError = (logger) => (error) => logger().error('Page revealed listener failed', 'page_reveal_error', {
3553
+ error: describeError(error),
3554
+ });
3555
+
3556
+ const createNavigationController = () => {
3557
+ if (typeof AbortController !== 'undefined') {
3558
+ try {
3559
+ const controller = new AbortController();
3560
+ return {
3561
+ signal: controller.signal,
3562
+ abort: () => controller.abort(),
3563
+ native: true,
3564
+ };
3565
+ }
3566
+ catch (_a) {
3567
+ }
3568
+ }
3569
+ const listeners = [];
3570
+ const signal = {
3571
+ aborted: false,
3572
+ reason: undefined,
3573
+ onabort: null,
3574
+ addEventListener: (type, listener) => {
3575
+ if (type === 'abort')
3576
+ listeners.push(listener);
3577
+ },
3578
+ removeEventListener: (type, listener) => {
3579
+ if (type !== 'abort')
3580
+ return;
3581
+ const index = listeners.indexOf(listener);
3582
+ if (index !== -1)
3583
+ listeners.splice(index, 1);
3584
+ },
3585
+ };
3586
+ return {
3587
+ signal: signal,
3588
+ abort: () => {
3589
+ if (signal.aborted)
3590
+ return;
3591
+ signal.aborted = true;
3592
+ signal.reason = new Error('The navigation was aborted');
3593
+ const event = { type: 'abort', target: signal };
3594
+ const call = (listener) => {
3595
+ try {
3596
+ listener.call(signal, event);
3597
+ }
3598
+ catch (error) {
3599
+ setTimeout(() => {
3600
+ throw error;
3601
+ }, 0);
3602
+ }
3603
+ };
3604
+ if (typeof signal.onabort === 'function')
3605
+ call(signal.onabort);
3606
+ for (const listener of listeners.slice())
3607
+ call(listener);
3608
+ },
3609
+ native: false,
3610
+ };
3611
+ };
3612
+ const waitForFrame = (capMs, hiddenCapMs) => new Promise((resolve) => {
3613
+ let timer;
3614
+ let done = false;
3615
+ const finish = () => {
3616
+ if (done)
3617
+ return;
3618
+ done = true;
3619
+ if (timer !== undefined)
3620
+ clearTimeout(timer);
3621
+ cancelAnimationFrame(frame);
3622
+ document.removeEventListener('visibilitychange', onVisibilityChange);
3623
+ resolve();
3624
+ };
3625
+ const startTimer = (ms) => {
3626
+ if (timer === undefined)
3627
+ timer = setTimeout(finish, ms);
3628
+ };
3629
+ const onVisibilityChange = () => {
3630
+ if (document.hidden)
3631
+ startTimer(hiddenCapMs);
3632
+ else if (timer !== undefined) {
3633
+ clearTimeout(timer);
3634
+ timer = undefined;
3635
+ }
3636
+ };
3637
+ const frame = requestAnimationFrame(finish);
3638
+ if (capMs !== undefined)
3639
+ startTimer(capMs);
3640
+ else {
3641
+ document.addEventListener('visibilitychange', onVisibilityChange);
3642
+ onVisibilityChange();
3643
+ }
3644
+ });
3645
+ const HANDOVER_FRAME_CAP_MS = 50;
3646
+ const NAVIGATION_FRAME_CAP_MS = 100;
3647
+ const shouldSettleRoute = (aborted, threw, result) => !aborted && (threw || result !== 'skipped');
3648
+
3346
3649
  function mergeClientHeaders(client, server) {
3347
3650
  if (server)
3348
3651
  return { ...client, ...server };
@@ -3354,7 +3657,7 @@ function getQueuedPersonalizationHeaders() {
3354
3657
  const nested = (_c = (_b = findLastCmd((c) => !!c.projectData)) === null || _b === void 0 ? void 0 : _b.projectData) === null || _c === void 0 ? void 0 : _c.personalizationHeaders;
3355
3658
  return mergeClientHeaders(topLevel, nested);
3356
3659
  }
3357
- async function resolveProject(Coframe, previewVariantId, isSessionVariant, config) {
3660
+ async function resolveProject(Coframe, previewVariantId, isSessionVariant, config, isCurrent = () => true) {
3358
3661
  var _a, _b;
3359
3662
  const logger = Coframe.logger;
3360
3663
  const projectData = (_a = findLastCmd((c) => !!c.projectData)) === null || _a === void 0 ? void 0 : _a.projectData;
@@ -3370,7 +3673,8 @@ async function resolveProject(Coframe, previewVariantId, isSessionVariant, confi
3370
3673
  logger.error('No Coframe Project Config', 'no_project_config', {
3371
3674
  previewVariantId,
3372
3675
  });
3373
- variantRendered();
3676
+ if (isCurrent())
3677
+ variantRendered();
3374
3678
  return null;
3375
3679
  }
3376
3680
  return {
@@ -3888,17 +4192,39 @@ function setupListeners(Coframe, executionStartTime) {
3888
4192
  });
3889
4193
  };
3890
4194
  document.addEventListener('click', handleClick, true);
3891
- const handleEventFlush = debounce((eventType) => {
4195
+ let scheduledEventFlush = null;
4196
+ const flushEvent = (eventType) => {
3892
4197
  queueLeaveEvent(Coframe, eventType, eventType === 'tab_show' ? undefined : leaveFields());
3893
4198
  flushQueuedEvents(Coframe.projectId);
3894
4199
  notifyEventsFlushed();
3895
- }, 1);
3896
- window.addEventListener('beforeunload', () => handleEventFlush('before_unload'));
3897
- window.addEventListener('pagehide', () => handleEventFlush('page_hide'));
4200
+ };
4201
+ const scheduleEventFlush = (eventType) => {
4202
+ if (scheduledEventFlush !== null) {
4203
+ clearTimeout(scheduledEventFlush);
4204
+ }
4205
+ scheduledEventFlush = setTimeout(() => {
4206
+ scheduledEventFlush = null;
4207
+ flushEvent(eventType);
4208
+ }, 1);
4209
+ };
4210
+ const flushEventImmediately = (eventType) => {
4211
+ if (scheduledEventFlush !== null) {
4212
+ clearTimeout(scheduledEventFlush);
4213
+ scheduledEventFlush = null;
4214
+ }
4215
+ flushEvent(eventType);
4216
+ };
4217
+ window.addEventListener('beforeunload', () => scheduleEventFlush('before_unload'));
4218
+ window.addEventListener('pagehide', () => flushEventImmediately('page_hide'));
3898
4219
  window.addEventListener('visibilitychange', () => {
3899
4220
  const isPageHidden = document.hidden;
3900
4221
  const eventType = isPageHidden ? 'tab_hide' : 'tab_show';
3901
- handleEventFlush(eventType);
4222
+ if (isPageHidden) {
4223
+ flushEventImmediately(eventType);
4224
+ }
4225
+ else {
4226
+ scheduleEventFlush(eventType);
4227
+ }
3902
4228
  });
3903
4229
  }
3904
4230
  function queueLeaveEvent(Coframe, eventType, fields) {
@@ -3978,26 +4304,32 @@ function resolvePreview(pageUrl, previewVariantId, isSessionVariant) {
3978
4304
  }
3979
4305
  return { previewVariantId: null, isSessionVariant: false };
3980
4306
  }
4307
+ const isPageHydratedCmd = (cmd) => cmd.pageHydrated === true || cmd.emit === 'pageHydrated';
3981
4308
  async function awaitHydration(config, Coframe, project) {
3982
4309
  var _a, _b;
3983
- if (!config.waitForHydration)
4310
+ if (!shouldSdkWaitForHydration(config))
3984
4311
  return true;
3985
4312
  if (!((_a = project.frames) === null || _a === void 0 ? void 0 : _a.length) && !((_b = project.tags) === null || _b === void 0 ? void 0 : _b.length))
3986
4313
  return true;
3987
- const cmd = await waitForCmd((cmd) => cmd.pageHydrated === true || cmd.emit === 'pageHydrated', Coframe);
4314
+ const cmd = await waitForCmd(isPageHydratedCmd, Coframe);
3988
4315
  if (!cmd) {
3989
4316
  Coframe.logger.warn('Failed to wait for page hydrated command', 'wait_for_cmd_timeout');
3990
4317
  return false;
3991
4318
  }
3992
4319
  return true;
3993
4320
  }
3994
- async function ensureBody(Coframe, cdnProject, timeoutMs, nonce) {
4321
+ async function ensureBody(Coframe, cdnProject, timeoutMs, nonce, revealCoordinator, isCurrent) {
3995
4322
  const bodyPresent = await waitForBody(timeoutMs).catch(() => false);
3996
4323
  if (!bodyPresent) {
3997
4324
  Coframe.logger.error('Timeout waiting for document.body, exiting gracefully', 'body_timeout');
3998
- variantRendered();
4325
+ if (isCurrent()) {
4326
+ variantRendered();
4327
+ revealCoordinator.forceReveal('graceful-exit');
4328
+ }
3999
4329
  return false;
4000
4330
  }
4331
+ if (!isCurrent())
4332
+ return false;
4001
4333
  rebindMetrics(Coframe, Coframe.logger, cdnProject === null || cdnProject === void 0 ? void 0 : cdnProject.metrics, nonce);
4002
4334
  return true;
4003
4335
  }
@@ -4010,7 +4342,13 @@ function setupFlushInterval(Coframe) {
4010
4342
  }
4011
4343
  flushInterval = setInterval(() => flushQueuedEvents(Coframe.projectId), FLUSH_EVENTS_TIME);
4012
4344
  }
4013
- function setupSPANavigation(Coframe, project, config, previewVariantId, isSessionVariant, applyVariants) {
4345
+ const hasNoVariants = (Coframe) => !Coframe.variantsAndElements || Coframe.variantsAndElements.length === 0;
4346
+ const logNavigationError = (Coframe, message, error) => {
4347
+ Coframe.logger.error(message, 'spa_navigation_error', {
4348
+ error: describeError(error),
4349
+ });
4350
+ };
4351
+ function setupSPANavigation(Coframe, project, config, previewVariantId, isSessionVariant, runtime, applier, firstViewDeadline, onFirstViewDone) {
4014
4352
  var _a;
4015
4353
  if (!((_a = project === null || project === void 0 ? void 0 : project.settings) === null || _a === void 0 ? void 0 : _a.mutation_observer_active))
4016
4354
  return;
@@ -4035,16 +4373,20 @@ function setupSPANavigation(Coframe, project, config, previewVariantId, isSessio
4035
4373
  const observer = new MutationObserver(mutationCallback);
4036
4374
  observer.observe(document.body, { childList: true, subtree: true });
4037
4375
  let navigationAbortController = null;
4038
- observeUrlChanges(false, async (newPageUrl, _oldPageUrl) => {
4376
+ const onUrlChange = async (newPageUrl, _oldPageUrl, options = {}) => {
4039
4377
  var _a;
4040
- sendPageLeave(Coframe);
4378
+ const firstView = !applier.hasCommitted();
4379
+ if (!firstView)
4380
+ sendPageLeave(Coframe);
4041
4381
  if (navigationAbortController)
4042
4382
  navigationAbortController.abort();
4043
- navigationAbortController = new AbortController();
4044
- const { signal } = navigationAbortController;
4045
- await new Promise((resolve) => requestAnimationFrame(resolve));
4046
- if (signal.aborted)
4047
- return;
4383
+ runtime === null || runtime === void 0 ? void 0 : runtime.changeRoute(newPageUrl);
4384
+ const controller = createNavigationController();
4385
+ navigationAbortController = controller;
4386
+ const { signal } = controller;
4387
+ await waitForFrame(options.routeAlreadyRendered ? HANDOVER_FRAME_CAP_MS : undefined, NAVIGATION_FRAME_CAP_MS);
4388
+ if (signal.aborted || (runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed()))
4389
+ return 'skipped';
4048
4390
  infiniteLoopProtection = 0;
4049
4391
  let pageProject = project;
4050
4392
  let pagePersonalizationHeaders = Coframe.personalizationHeaders;
@@ -4060,9 +4402,9 @@ function setupSPANavigation(Coframe, project, config, previewVariantId, isSessio
4060
4402
  else {
4061
4403
  try {
4062
4404
  const preview = resolvePreview(newPageUrl, previewVariantId, isSessionVariant);
4063
- const result = await fetchProject(config.projectUrl, Coframe.projectId, newPageUrl, signal, config.userContext, preview.previewVariantId || undefined, undefined, (_a = config.referrer) !== null && _a !== void 0 ? _a : getBrowserReferrer());
4064
- if (signal.aborted)
4065
- return;
4405
+ const result = await fetchProject(config.projectUrl, Coframe.projectId, newPageUrl, controller.native ? signal : undefined, config.userContext, preview.previewVariantId || undefined, undefined, (_a = config.referrer) !== null && _a !== void 0 ? _a : getBrowserReferrer());
4406
+ if (signal.aborted || (runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed()))
4407
+ return 'skipped';
4066
4408
  if (result) {
4067
4409
  pageProject = result.project;
4068
4410
  pagePersonalizationHeaders = {
@@ -4075,10 +4417,37 @@ function setupSPANavigation(Coframe, project, config, previewVariantId, isSessio
4075
4417
  catch (_b) {
4076
4418
  }
4077
4419
  }
4078
- if (signal.aborted)
4079
- return;
4080
- await applyVariants(newPageUrl, pageProject, pagePersonalizationHeaders, signal);
4081
- });
4420
+ if (signal.aborted || (runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed()))
4421
+ return 'skipped';
4422
+ let result = 'skipped';
4423
+ let threw = false;
4424
+ try {
4425
+ result = await applier.apply(newPageUrl, pageProject, pagePersonalizationHeaders, signal, firstView ? firstViewDeadline : undefined);
4426
+ }
4427
+ catch (error) {
4428
+ threw = true;
4429
+ if (firstView && !signal.aborted && !(runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed())) {
4430
+ onFirstViewDone === null || onFirstViewDone === void 0 ? void 0 : onFirstViewDone(pageProject);
4431
+ }
4432
+ throw error;
4433
+ }
4434
+ finally {
4435
+ if (shouldSettleRoute(signal.aborted, threw, result)) {
4436
+ runtime === null || runtime === void 0 ? void 0 : runtime.settleRoute();
4437
+ }
4438
+ }
4439
+ if (firstView && result === 'applied' && !(runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed())) {
4440
+ if (hasNoVariants(Coframe))
4441
+ variantRendered();
4442
+ onFirstViewDone === null || onFirstViewDone === void 0 ? void 0 : onFirstViewDone(pageProject);
4443
+ }
4444
+ return result;
4445
+ };
4446
+ observeUrlChanges(false, (newPageUrl, oldPageUrl) => onUrlChange(newPageUrl).catch((error) => {
4447
+ logNavigationError(Coframe, 'SPA navigation failed', error);
4448
+ return 'skipped';
4449
+ }), () => !(runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed()));
4450
+ return onUrlChange;
4082
4451
  }
4083
4452
  function readLaneClaims() {
4084
4453
  const cookie = getCookie(LANE_COOKIE_KEY);
@@ -4108,17 +4477,14 @@ function persistLaneClaims(claims, logger) {
4108
4477
  setCookie(LANE_COOKIE_KEY, encodeURIComponent(serialized), logger);
4109
4478
  }
4110
4479
  }
4111
- function createApplyVariants(Coframe, config, previewVariantId, isSessionVariant, executionStartTime, analytics, laneStore) {
4480
+ function createApplyVariants(Coframe, config, previewVariantId, isSessionVariant, executionStartTime, analytics, laneStore, runtime) {
4112
4481
  let isFirstApply = true;
4113
4482
  let applicationId = 0;
4114
- return async (pageUrl, pageProject, personalizationHeaders, signal, deadline) => {
4115
- var _a;
4483
+ const apply = async (pageUrl, pageProject, personalizationHeaders, signal, deadline) => {
4484
+ var _a, _b;
4116
4485
  const currentApplication = ++applicationId;
4117
4486
  if (signal === null || signal === void 0 ? void 0 : signal.aborted)
4118
- return;
4119
- const sessionId = isFirstApply ? Coframe.sessionId : generateUUID();
4120
- const sessionContext = buildSessionContext(isFirstApply);
4121
- isFirstApply = false;
4487
+ return 'skipped';
4122
4488
  const preview = resolvePreview(pageUrl, previewVariantId, isSessionVariant);
4123
4489
  const chooseCmabVariants = createCmabVariantBatchChooser({
4124
4490
  projectId: Coframe.projectId,
@@ -4132,8 +4498,22 @@ function createApplyVariants(Coframe, config, previewVariantId, isSessionVariant
4132
4498
  const { variantsAndElements, experimentsInfo, hasActivatedFrames, laneClaims, } = await getVariantsAndElements(personalizationHeaders, pageProject, preview.previewVariantId, Coframe.userToken, pageUrl, getBrowserLanguages(), getBrowserPlatform(), Coframe.logger, config.nonce, laneStore.claims, preview.isSessionVariant, chooseCmabVariants);
4133
4499
  if ((signal === null || signal === void 0 ? void 0 : signal.aborted) ||
4134
4500
  currentApplication !== applicationId ||
4501
+ (runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed()) ||
4135
4502
  (signal && !urlsMatch(pageUrl, getCurrentPageUrl())))
4136
- return;
4503
+ return 'skipped';
4504
+ if (!signal &&
4505
+ runtime &&
4506
+ ((_b = pageProject === null || pageProject === void 0 ? void 0 : pageProject.settings) === null || _b === void 0 ? void 0 : _b.mutation_observer_active) &&
4507
+ !urlsMatch(pageUrl, getCurrentPageUrl(), true))
4508
+ return 'stale-route';
4509
+ if (!signal && runtime) {
4510
+ const urlAtCommit = getCurrentPageUrl();
4511
+ if (urlsMatch(pageUrl, urlAtCommit, true))
4512
+ runtime.updateUrl(urlAtCommit);
4513
+ }
4514
+ const sessionId = isFirstApply ? Coframe.sessionId : generateUUID();
4515
+ const sessionContext = buildSessionContext(isFirstApply);
4516
+ isFirstApply = false;
4137
4517
  Coframe.sessionId = sessionId;
4138
4518
  Coframe.personalizationHeaders = personalizationHeaders;
4139
4519
  setIsPreviewVariant(!!preview.previewVariantId);
@@ -4151,7 +4531,9 @@ function createApplyVariants(Coframe, config, previewVariantId, isSessionVariant
4151
4531
  variantRendered();
4152
4532
  }
4153
4533
  else {
4154
- replaceVariants(variantsAndElements, Coframe.orgId, Coframe.logger, config.nonce);
4534
+ await replaceVariants(variantsAndElements, Coframe.orgId, Coframe.logger, config.nonce, runtime);
4535
+ if (runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed())
4536
+ return 'skipped';
4155
4537
  }
4156
4538
  const cmabDecisions = chooseCmabVariants.getDecisions(experimentsInfo);
4157
4539
  startSession(Coframe, isCookiesEnabled, executionStartTime, personalizationHeaders, {
@@ -4160,29 +4542,77 @@ function createApplyVariants(Coframe, config, previewVariantId, isSessionVariant
4160
4542
  }, pageUrl);
4161
4543
  analytics.resetBroadcasted();
4162
4544
  analytics.sendAnalytics();
4545
+ return 'applied';
4163
4546
  };
4547
+ return { apply, hasCommitted: () => !isFirstApply };
4164
4548
  }
4165
- async function modifyPage(Coframe, configResult, projectResult, analytics) {
4549
+ async function modifyPage(Coframe, configResult, projectResult, analytics, lifecycle) {
4550
+ var _a;
4166
4551
  const { config, timeoutMs, previewVariantId, isSessionVariant, integrationStartTime, executionStartTime, } = configResult;
4167
4552
  const { project: cdnProject, personalizationHeaders } = projectResult;
4553
+ const revealCoordinator = (_a = lifecycle === null || lifecycle === void 0 ? void 0 : lifecycle.revealCoordinator) !== null && _a !== void 0 ? _a : createRevealCoordinator(revealPage, logRevealListenerError(() => Coframe.logger));
4554
+ const runtime = lifecycle === null || lifecycle === void 0 ? void 0 : lifecycle.runtime;
4555
+ if (runtime && config.waitForHydration) {
4556
+ if (findLastCmd(isPageHydratedCmd)) {
4557
+ runtime.publishHydrated();
4558
+ }
4559
+ else {
4560
+ let stopHydration = () => { };
4561
+ stopHydration = subscribeToCmd(isPageHydratedCmd, () => {
4562
+ stopHydration();
4563
+ runtime.publishHydrated();
4564
+ });
4565
+ }
4566
+ }
4168
4567
  if (!(await awaitHydration(config, Coframe, cdnProject)))
4169
4568
  return;
4569
+ if (runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed())
4570
+ return;
4170
4571
  if (cdnProject.tags) {
4171
4572
  executeTags(cdnProject.tags, Coframe.logger, config.nonce);
4172
4573
  Coframe.status.tags = cdnProject.tags;
4173
4574
  }
4174
- if (!(await ensureBody(Coframe, cdnProject, timeoutMs, config.nonce)))
4575
+ if (!(await ensureBody(Coframe, cdnProject, timeoutMs, config.nonce, revealCoordinator, () => !(runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed()))))
4175
4576
  return;
4176
4577
  const laneStore = { claims: readLaneClaims() };
4177
- const applyVariants = createApplyVariants(Coframe, config, previewVariantId, isSessionVariant, executionStartTime, analytics, laneStore);
4178
- await applyVariants(getCurrentPageUrl(), cdnProject, personalizationHeaders, undefined, (integrationStartTime || executionStartTime).getTime() + timeoutMs);
4179
- if (!Coframe.variantsAndElements ||
4180
- Coframe.variantsAndElements.length === 0) {
4181
- revealPage();
4578
+ const applier = createApplyVariants(Coframe, config, previewVariantId, isSessionVariant, executionStartTime, analytics, laneStore, runtime);
4579
+ const initialUrl = getCurrentPageUrl();
4580
+ if (runtime) {
4581
+ const runtimeUrl = runtime.getState().currentUrl;
4582
+ if (!urlsMatch(runtimeUrl, initialUrl, true))
4583
+ runtime.changeRoute(initialUrl);
4584
+ }
4585
+ const firstDeadline = (integrationStartTime || executionStartTime).getTime() + timeoutMs;
4586
+ const firstResult = await applier.apply(initialUrl, cdnProject, personalizationHeaders, undefined, firstDeadline);
4587
+ if (runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed())
4588
+ return;
4589
+ const routeChangedDuringInit = firstResult === 'stale-route';
4590
+ if (!routeChangedDuringInit && hasNoVariants(Coframe)) {
4591
+ revealCoordinator.requestReveal('no-variants');
4182
4592
  variantRendered();
4183
4593
  }
4184
- reportInitTiming(Coframe, cdnProject, integrationStartTime, executionStartTime);
4185
- setupSPANavigation(Coframe, cdnProject, config, previewVariantId, isSessionVariant, applyVariants);
4594
+ if (!routeChangedDuringInit)
4595
+ revealCoordinator.flushPending();
4596
+ let timingReported = false;
4597
+ const reportTiming = (appliedProject = cdnProject) => {
4598
+ if (timingReported)
4599
+ return;
4600
+ timingReported = true;
4601
+ reportInitTiming(Coframe, appliedProject, integrationStartTime, executionStartTime);
4602
+ };
4603
+ if (!routeChangedDuringInit)
4604
+ reportTiming();
4605
+ const onUrlChange = setupSPANavigation(Coframe, cdnProject, config, previewVariantId, isSessionVariant, runtime, applier, firstDeadline, routeChangedDuringInit ? reportTiming : undefined);
4606
+ const urlNow = getCurrentPageUrl();
4607
+ if (runtime &&
4608
+ onUrlChange &&
4609
+ (routeChangedDuringInit || !urlsMatch(initialUrl, urlNow, true))) {
4610
+ onUrlChange(urlNow, initialUrl, {
4611
+ routeAlreadyRendered: routeChangedDuringInit,
4612
+ }).catch((error) => {
4613
+ logNavigationError(Coframe, 'Applying the route reached during init failed', error);
4614
+ });
4615
+ }
4186
4616
  setupFlushInterval(Coframe);
4187
4617
  }
4188
4618
 
@@ -4510,11 +4940,12 @@ async function resolveFeatureFlag(Coframe, timeoutMs) {
4510
4940
  return undefined;
4511
4941
  return getFeatureFlagValue(cmd);
4512
4942
  }
4513
- async function resolveConfig(Coframe, revealPage) {
4943
+ async function resolveConfig(Coframe, revealActions) {
4514
4944
  const logger = Coframe.logger;
4515
4945
  const configCmd = await waitForCmd((cmd) => !!cmd.config, Coframe);
4516
4946
  if (!configCmd) {
4517
4947
  logger.error('Failed to wait for config cmd', 'wait_for_cmd_timeout');
4948
+ revealActions.forceReveal('config-timeout');
4518
4949
  return null;
4519
4950
  }
4520
4951
  const config = configCmd.config;
@@ -4530,7 +4961,7 @@ async function resolveConfig(Coframe, revealPage) {
4530
4961
  configureReplayScriptUrl(config.replayScriptUrl);
4531
4962
  const gracefulExit = (msg) => {
4532
4963
  console.warn(`[Coframe] ${msg}`);
4533
- revealPage();
4964
+ revealActions.forceReveal('graceful-exit');
4534
4965
  return null;
4535
4966
  };
4536
4967
  if (config.waitForFeatureFlag) {
@@ -4543,8 +4974,9 @@ async function resolveConfig(Coframe, revealPage) {
4543
4974
  }
4544
4975
  }
4545
4976
  const projectId = config === null || config === void 0 ? void 0 : config.projectId;
4977
+ const lifecycleMode = isVariantRuntimeLifecycleEnabled(config);
4546
4978
  const warnMissingVariant = () => {
4547
- if (applyVariantChangesStarted) {
4979
+ if (applyVariantChangesStarted && !lifecycleMode) {
4548
4980
  logger.warn('Failed to wait for variant rendered command', 'missing_variant_rendered');
4549
4981
  }
4550
4982
  };
@@ -4557,14 +4989,15 @@ async function resolveConfig(Coframe, revealPage) {
4557
4989
  }
4558
4990
  logger.warn('Variant not rendered', 'variant_not_rendered', metadata);
4559
4991
  };
4560
- waitForCmd((cmd) => cmd.variantRendered === true || cmd.emit === 'variantRendered', Coframe, timeoutMs)
4561
- .then((cmd) => {
4562
- if (!cmd) {
4563
- warnMissingVariant();
4564
- }
4565
- })
4566
- .finally(() => {
4567
- revealPage();
4992
+ waitForCmd((cmd) => cmd.variantRendered === true || cmd.emit === 'variantRendered', Coframe, timeoutMs, QueueName.CFQ, !lifecycleMode).then((cmd) => {
4993
+ if (cmd) {
4994
+ revealActions.requestReveal('legacy-variant-rendered');
4995
+ return;
4996
+ }
4997
+ warnMissingVariant();
4998
+ if (revealActions.getSnapshot().pageRevealed)
4999
+ return;
5000
+ revealActions.forceReveal('config-timeout');
4568
5001
  });
4569
5002
  const loggedReasons = [];
4570
5003
  listenForCmd((cmd) => cmd.emit === 'variantNotRendered', (cmd) => {
@@ -4578,6 +5011,7 @@ async function resolveConfig(Coframe, revealPage) {
4578
5011
  description: 'No Coframe Page ID',
4579
5012
  });
4580
5013
  variantRendered();
5014
+ revealActions.forceReveal('graceful-exit');
4581
5015
  return null;
4582
5016
  }
4583
5017
  Coframe.projectId = projectId;
@@ -4749,6 +5183,824 @@ function injectOptions(options) {
4749
5183
  }
4750
5184
  }
4751
5185
 
5186
+ const TERMINAL_STATUSES = new Set([
5187
+ 'completed',
5188
+ 'error',
5189
+ 'cleaned',
5190
+ ]);
5191
+ const DEFAULT_FADE_DURATION_MS = 150;
5192
+ const MAX_INGEST_PER_SESSION = 10;
5193
+ const TRANSITION_LONGHANDS = [
5194
+ 'transition-property',
5195
+ 'transition-duration',
5196
+ 'transition-timing-function',
5197
+ 'transition-delay',
5198
+ ];
5199
+ const fadeTargets = new Map();
5200
+ const FADE_SAFETY_MS = 5000;
5201
+ function cancelFadeAnimation(fadeTarget) {
5202
+ if (fadeTarget.animationFrame !== null) {
5203
+ cancelAnimationFrame(fadeTarget.animationFrame);
5204
+ }
5205
+ if (fadeTarget.timer)
5206
+ clearTimeout(fadeTarget.timer);
5207
+ fadeTarget.animationFrame = null;
5208
+ fadeTarget.timer = null;
5209
+ }
5210
+ function finishFade(fadeTarget) {
5211
+ if (fadeTargets.get(fadeTarget.target) !== fadeTarget)
5212
+ return;
5213
+ fadeTargets.delete(fadeTarget.target);
5214
+ cancelFadeAnimation(fadeTarget);
5215
+ fadeTarget.target.removeEventListener('transitionend', onFadeTransitionEnd);
5216
+ for (const record of Array.from(fadeTarget.records))
5217
+ record.stop();
5218
+ fadeTarget.records.clear();
5219
+ const { target } = fadeTarget;
5220
+ const style = target.style;
5221
+ style.setProperty('transition', 'none', 'important');
5222
+ style.setProperty('opacity', ...fadeTarget.opacity);
5223
+ void getComputedStyle(target).opacity;
5224
+ style.removeProperty('transition');
5225
+ if (fadeTarget.transition[0]) {
5226
+ style.setProperty('transition', ...fadeTarget.transition);
5227
+ }
5228
+ for (const [name, value, priority] of fadeTarget.longhands) {
5229
+ if (value)
5230
+ style.setProperty(name, value, priority);
5231
+ }
5232
+ }
5233
+ function onFadeTransitionEnd(event) {
5234
+ const fadeTarget = fadeTargets.get(event.target);
5235
+ if ((fadeTarget === null || fadeTarget === void 0 ? void 0 : fadeTarget.timer) && event.propertyName === 'opacity') {
5236
+ finishFade(fadeTarget);
5237
+ }
5238
+ }
5239
+ function maybeStartFade(fadeTarget) {
5240
+ if (fadeTarget.animationFrame !== null || fadeTarget.timer)
5241
+ return;
5242
+ let duration = 0;
5243
+ for (const record of Array.from(fadeTarget.records)) {
5244
+ if (!record.ready)
5245
+ return;
5246
+ duration = Math.max(duration, record.duration);
5247
+ }
5248
+ const { target } = fadeTarget;
5249
+ void getComputedStyle(target).opacity;
5250
+ fadeTarget.animationFrame = requestAnimationFrame(() => {
5251
+ fadeTarget.animationFrame = null;
5252
+ if (fadeTargets.get(target) !== fadeTarget)
5253
+ return;
5254
+ target.style.setProperty('transition', `opacity ${duration}ms ease`, 'important');
5255
+ target.style.setProperty('opacity', '1', 'important');
5256
+ fadeTarget.timer = setTimeout(() => finishFade(fadeTarget), duration + 100);
5257
+ });
5258
+ }
5259
+ function removeFadeRecord(record) {
5260
+ const { fadeTarget } = record;
5261
+ if (!fadeTarget.records.delete(record))
5262
+ return;
5263
+ record.stop();
5264
+ if (fadeTarget.records.size === 0)
5265
+ finishFade(fadeTarget);
5266
+ else
5267
+ maybeStartFade(fadeTarget);
5268
+ }
5269
+ function createVariantSession(options) {
5270
+ const listenerStops = new Set();
5271
+ let ingestCount = 0;
5272
+ let ingestLimitReported = false;
5273
+ let metadataDropReported = false;
5274
+ const fades = new Map();
5275
+ const fadeReleases = new Set();
5276
+ const state = {
5277
+ sessionId: options.sessionId,
5278
+ variantId: options.variantId,
5279
+ batchId: options.batchId,
5280
+ routeVersion: options.routeVersion,
5281
+ active: true,
5282
+ status: 'created',
5283
+ ownsPageAntiflicker: false,
5284
+ pageAntiflickerRemoved: false,
5285
+ };
5286
+ const isMutable = () => state.active && !TERMINAL_STATUSES.has(state.status);
5287
+ const notify = () => options.onStateChange(internal);
5288
+ const releaseVisuals = () => {
5289
+ var _a;
5290
+ for (const record of Array.from(fades.values()))
5291
+ removeFadeRecord(record);
5292
+ (_a = options.releaseVisuals) === null || _a === void 0 ? void 0 : _a.call(options, internal);
5293
+ };
5294
+ const fadeIn = (target, durationMs = DEFAULT_FADE_DURATION_MS) => {
5295
+ if (fades.has(target))
5296
+ return;
5297
+ const duration = typeof durationMs === 'number' && durationMs - durationMs === 0
5298
+ ? Math.max(0, durationMs)
5299
+ : DEFAULT_FADE_DURATION_MS;
5300
+ let fadeTarget = fadeTargets.get(target);
5301
+ if (!fadeTarget) {
5302
+ const style = target.style;
5303
+ fadeTarget = {
5304
+ target,
5305
+ opacity: [
5306
+ style.getPropertyValue('opacity'),
5307
+ style.getPropertyPriority('opacity'),
5308
+ ],
5309
+ transition: [
5310
+ style.getPropertyValue('transition'),
5311
+ style.getPropertyPriority('transition'),
5312
+ ],
5313
+ longhands: TRANSITION_LONGHANDS.map((name) => [
5314
+ name,
5315
+ style.getPropertyValue(name),
5316
+ style.getPropertyPriority(name),
5317
+ ]),
5318
+ records: new Set(),
5319
+ animationFrame: null,
5320
+ timer: null,
5321
+ };
5322
+ fadeTargets.set(target, fadeTarget);
5323
+ target.addEventListener('transitionend', onFadeTransitionEnd);
5324
+ }
5325
+ cancelFadeAnimation(fadeTarget);
5326
+ target.style.setProperty('transition', 'none', 'important');
5327
+ target.style.setProperty('opacity', '0', 'important');
5328
+ let pageRevealed = false;
5329
+ let released = false;
5330
+ let stopPageListener = () => { };
5331
+ let safetyTimer = null;
5332
+ const update = () => {
5333
+ record.ready = pageRevealed && released;
5334
+ if (record.ready)
5335
+ maybeStartFade(record.fadeTarget);
5336
+ };
5337
+ const release = () => {
5338
+ released = true;
5339
+ if (safetyTimer)
5340
+ clearTimeout(safetyTimer);
5341
+ safetyTimer = null;
5342
+ update();
5343
+ };
5344
+ const record = {
5345
+ fadeTarget,
5346
+ duration,
5347
+ ready: false,
5348
+ stop: () => {
5349
+ stopPageListener();
5350
+ if (safetyTimer)
5351
+ clearTimeout(safetyTimer);
5352
+ safetyTimer = null;
5353
+ fadeReleases.delete(release);
5354
+ if (fades.get(target) === record)
5355
+ fades.delete(target);
5356
+ },
5357
+ };
5358
+ fadeTarget.records.add(record);
5359
+ fades.set(target, record);
5360
+ fadeReleases.add(release);
5361
+ safetyTimer = setTimeout(release, FADE_SAFETY_MS);
5362
+ stopPageListener = subscribe('pageRevealed', () => {
5363
+ pageRevealed = true;
5364
+ update();
5365
+ }, true);
5366
+ if (options.getPageState().pageRevealed) {
5367
+ pageRevealed = true;
5368
+ update();
5369
+ }
5370
+ };
5371
+ const subscribe = (event, handler, once) => {
5372
+ if (!state.active)
5373
+ return () => { };
5374
+ const stopSubscription = options.subscribePage(event, handler, once);
5375
+ if (!state.active) {
5376
+ stopSubscription();
5377
+ return () => { };
5378
+ }
5379
+ const stop = () => {
5380
+ listenerStops.delete(stop);
5381
+ stopSubscription();
5382
+ };
5383
+ listenerStops.add(stop);
5384
+ return stop;
5385
+ };
5386
+ const facade = {
5387
+ getPageState: () => ({ ...options.getPageState() }),
5388
+ getState: () => ({ ...state }),
5389
+ getVariantId: () => state.variantId,
5390
+ onPage: (event, handler) => subscribe(event, handler, false),
5391
+ oncePage: (event, handler) => subscribe(event, handler, true),
5392
+ ready() {
5393
+ if (!isMutable() || state.status !== 'created')
5394
+ return;
5395
+ state.status = 'ready';
5396
+ notify();
5397
+ },
5398
+ takePageAntiflickerOwnership() {
5399
+ if (!isMutable() || state.ownsPageAntiflicker)
5400
+ return;
5401
+ state.ownsPageAntiflicker = true;
5402
+ state.status = 'rendering';
5403
+ notify();
5404
+ },
5405
+ fadeIn(target, durationMs) {
5406
+ if (!isMutable() || !target)
5407
+ return;
5408
+ if (options.fadeIn)
5409
+ options.fadeIn(internal, target, durationMs);
5410
+ else
5411
+ fadeIn(target, durationMs);
5412
+ },
5413
+ ingestApplied(payload) {
5414
+ var _a, _b, _c;
5415
+ if (!isMutable())
5416
+ return;
5417
+ if (ingestCount >= MAX_INGEST_PER_SESSION) {
5418
+ if (!ingestLimitReported) {
5419
+ ingestLimitReported = true;
5420
+ (_a = options.onIngestLimit) === null || _a === void 0 ? void 0 : _a.call(options, internal);
5421
+ }
5422
+ return;
5423
+ }
5424
+ ingestCount += 1;
5425
+ const metadataDropped = (_b = options.ingestApplied) === null || _b === void 0 ? void 0 : _b.call(options, internal, payload);
5426
+ if (metadataDropped && !metadataDropReported) {
5427
+ metadataDropReported = true;
5428
+ (_c = options.onMetadataDropped) === null || _c === void 0 ? void 0 : _c.call(options, internal);
5429
+ }
5430
+ },
5431
+ complete() {
5432
+ if (!isMutable())
5433
+ return;
5434
+ state.status = 'completed';
5435
+ for (const release of Array.from(fadeReleases))
5436
+ release();
5437
+ fadeReleases.clear();
5438
+ options.emitLegacyVariantRendered();
5439
+ notify();
5440
+ },
5441
+ error(error) {
5442
+ var _a;
5443
+ if (!isMutable())
5444
+ return;
5445
+ state.status = 'error';
5446
+ releaseVisuals();
5447
+ (_a = options.onError) === null || _a === void 0 ? void 0 : _a.call(options, internal, error);
5448
+ options.emitLegacyVariantRendered();
5449
+ notify();
5450
+ },
5451
+ cleanup() {
5452
+ if (state.status === 'cleaned')
5453
+ return;
5454
+ state.active = false;
5455
+ state.status = 'cleaned';
5456
+ for (const stop of Array.from(listenerStops))
5457
+ stop();
5458
+ releaseVisuals();
5459
+ notify();
5460
+ },
5461
+ };
5462
+ const internal = {
5463
+ facade: Object.freeze(facade),
5464
+ getState: () => ({ ...state }),
5465
+ isSettledForReveal: () => state.status === 'ready' ||
5466
+ state.status === 'rendering' ||
5467
+ TERMINAL_STATUSES.has(state.status),
5468
+ markPageRevealed() {
5469
+ if (state.pageAntiflickerRemoved)
5470
+ return;
5471
+ state.pageAntiflickerRemoved = true;
5472
+ },
5473
+ invalidate() {
5474
+ facade.cleanup();
5475
+ },
5476
+ };
5477
+ return internal;
5478
+ }
5479
+
5480
+ const DEFAULT_BATCH_TIMEOUT_MS = 1000;
5481
+ function createPageRuntime(options) {
5482
+ var _a, _b, _c;
5483
+ const createId = (_a = options.createId) !== null && _a !== void 0 ? _a : generateUUID;
5484
+ const batchTimeoutMs = (_b = options.batchTimeoutMs) !== null && _b !== void 0 ? _b : DEFAULT_BATCH_TIMEOUT_MS;
5485
+ const revealDeadline = Date.now() + batchTimeoutMs;
5486
+ const timeUntilDeadline = () => Math.max(0, revealDeadline - Date.now());
5487
+ const listeners = new Map();
5488
+ const state = {
5489
+ sdkReady: false,
5490
+ hydrated: (_c = options.initialHydrated) !== null && _c !== void 0 ? _c : false,
5491
+ routeVersion: 0,
5492
+ currentUrl: options.currentUrl,
5493
+ pageRevealed: options.revealCoordinator.getSnapshot().pageRevealed,
5494
+ };
5495
+ let activeBatch = null;
5496
+ let handoffBlockerId = null;
5497
+ let handoffTimeout = null;
5498
+ let destroyed = false;
5499
+ const clearHandoff = () => {
5500
+ handoffBlockerId = null;
5501
+ if (handoffTimeout)
5502
+ clearTimeout(handoffTimeout);
5503
+ handoffTimeout = null;
5504
+ };
5505
+ const callListener = (event, handler, payload) => {
5506
+ var _a;
5507
+ try {
5508
+ handler(payload);
5509
+ }
5510
+ catch (error) {
5511
+ (_a = options.error) === null || _a === void 0 ? void 0 : _a.call(options, 'Variant runtime listener failed', {
5512
+ event,
5513
+ error: describeError(error),
5514
+ });
5515
+ }
5516
+ };
5517
+ const emit = (event, payload) => {
5518
+ const handlers = listeners.get(event);
5519
+ if (!handlers)
5520
+ return;
5521
+ for (const handler of Array.from(handlers)) {
5522
+ if (handlers.has(handler))
5523
+ callListener(event, handler, payload);
5524
+ }
5525
+ };
5526
+ const on = (event, handler) => {
5527
+ var _a;
5528
+ const handlers = (_a = listeners.get(event)) !== null && _a !== void 0 ? _a : new Set();
5529
+ const entry = (payload) => handler(payload);
5530
+ handlers.add(entry);
5531
+ listeners.set(event, handlers);
5532
+ return () => {
5533
+ handlers.delete(entry);
5534
+ if (handlers.size === 0 && listeners.get(event) === handlers) {
5535
+ listeners.delete(event);
5536
+ }
5537
+ };
5538
+ };
5539
+ const stickySatisfied = (event) => {
5540
+ if (event === 'sdkReady')
5541
+ return state.sdkReady;
5542
+ if (event === 'hydrated')
5543
+ return state.hydrated;
5544
+ if (event === 'pageRevealed')
5545
+ return state.pageRevealed;
5546
+ return false;
5547
+ };
5548
+ const stickyPayload = (event) => event === 'pageRevealed'
5549
+ ? {
5550
+ ...state,
5551
+ reason: options.revealCoordinator.getSnapshot().revealedReason,
5552
+ }
5553
+ : { ...state };
5554
+ const once = (event, handler) => {
5555
+ if (stickySatisfied(event)) {
5556
+ callListener(event, handler, stickyPayload(event));
5557
+ return () => { };
5558
+ }
5559
+ let stop = () => { };
5560
+ stop = on(event, (payload) => {
5561
+ stop();
5562
+ handler(payload);
5563
+ });
5564
+ return stop;
5565
+ };
5566
+ const clearBatchTimeout = (batch) => {
5567
+ if (!batch.timeout)
5568
+ return;
5569
+ clearTimeout(batch.timeout);
5570
+ batch.timeout = null;
5571
+ };
5572
+ const revealFromRuntime = (batchId, reveal) => {
5573
+ var _a;
5574
+ try {
5575
+ reveal();
5576
+ }
5577
+ catch (error) {
5578
+ (_a = options.error) === null || _a === void 0 ? void 0 : _a.call(options, 'Variant runtime page reveal failed', {
5579
+ error: describeError(error),
5580
+ ...(batchId ? { batchId } : {}),
5581
+ routeVersion: state.routeVersion,
5582
+ });
5583
+ }
5584
+ };
5585
+ const forceRevealOnTimeout = (batchId) => revealFromRuntime(batchId, () => options.revealCoordinator.forceReveal('batch-timeout'));
5586
+ const releaseBatch = (batch) => {
5587
+ if (batch.released)
5588
+ return;
5589
+ batch.released = true;
5590
+ clearBatchTimeout(batch);
5591
+ revealFromRuntime(batch.batchId, () => options.revealCoordinator.release(batch.batchId, 'batch-settled'));
5592
+ };
5593
+ const warnForSession = (session, message) => {
5594
+ var _a;
5595
+ const sessionState = session.getState();
5596
+ (_a = options.warn) === null || _a === void 0 ? void 0 : _a.call(options, message, {
5597
+ variantId: sessionState.variantId,
5598
+ batchId: sessionState.batchId,
5599
+ });
5600
+ };
5601
+ const maybeReleaseBatch = (batch) => {
5602
+ if (activeBatch !== batch ||
5603
+ batch.released ||
5604
+ !batch.closed ||
5605
+ batch.sessions.size < batch.expectedSessionCount)
5606
+ return;
5607
+ if (Array.from(batch.sessions.values()).every((session) => session.isSettledForReveal())) {
5608
+ releaseBatch(batch);
5609
+ }
5610
+ };
5611
+ const warnUnsettled = (batch, reason) => {
5612
+ var _a;
5613
+ (_a = options.warn) === null || _a === void 0 ? void 0 : _a.call(options, 'Variant runtime batch timed out', {
5614
+ batchId: batch.batchId,
5615
+ routeVersion: batch.routeVersion,
5616
+ reason,
5617
+ expectedSessionCount: batch.expectedSessionCount,
5618
+ createdSessionCount: batch.sessions.size,
5619
+ unsettledVariantIds: Array.from(batch.sessions.values())
5620
+ .filter((session) => !session.isSettledForReveal())
5621
+ .map((session) => session.getState().variantId),
5622
+ });
5623
+ };
5624
+ const invalidateBatch = (batchId) => {
5625
+ if (!activeBatch || activeBatch.batchId !== batchId)
5626
+ return;
5627
+ const batch = activeBatch;
5628
+ activeBatch = null;
5629
+ clearBatchTimeout(batch);
5630
+ for (const session of batch.sessions.values())
5631
+ session.invalidate();
5632
+ };
5633
+ const coordinatorStop = options.revealCoordinator.onRevealed((reason) => {
5634
+ if (state.pageRevealed)
5635
+ return;
5636
+ if (activeBatch && !activeBatch.released && reason !== 'batch-settled') {
5637
+ warnUnsettled(activeBatch, reason);
5638
+ }
5639
+ state.pageRevealed = true;
5640
+ clearHandoff();
5641
+ if (activeBatch) {
5642
+ activeBatch.released = true;
5643
+ clearBatchTimeout(activeBatch);
5644
+ for (const session of activeBatch.sessions.values()) {
5645
+ session.markPageRevealed();
5646
+ }
5647
+ }
5648
+ emit('pageRevealed', { ...state, reason });
5649
+ });
5650
+ const runtime = {
5651
+ getState: () => ({ ...state }),
5652
+ on,
5653
+ once,
5654
+ publishSdkReady() {
5655
+ if (state.sdkReady)
5656
+ return;
5657
+ state.sdkReady = true;
5658
+ emit('sdkReady', { ...state });
5659
+ },
5660
+ publishHydrated() {
5661
+ if (state.hydrated)
5662
+ return;
5663
+ state.hydrated = true;
5664
+ emit('hydrated', { ...state });
5665
+ },
5666
+ settleRoute() {
5667
+ if (destroyed)
5668
+ return;
5669
+ if (handoffBlockerId) {
5670
+ const batchId = handoffBlockerId;
5671
+ clearHandoff();
5672
+ revealFromRuntime(batchId, () => options.revealCoordinator.release(batchId, 'no-variants'));
5673
+ return;
5674
+ }
5675
+ if (!state.pageRevealed && !activeBatch) {
5676
+ revealFromRuntime(null, () => options.revealCoordinator.requestReveal('no-variants'));
5677
+ }
5678
+ },
5679
+ updateUrl(currentUrl) {
5680
+ if (!destroyed)
5681
+ state.currentUrl = currentUrl;
5682
+ },
5683
+ changeRoute(currentUrl) {
5684
+ if (destroyed)
5685
+ return;
5686
+ if (activeBatch) {
5687
+ const { batchId } = activeBatch;
5688
+ invalidateBatch(batchId);
5689
+ if (!state.pageRevealed) {
5690
+ handoffBlockerId = batchId;
5691
+ const abandonedRouteVersion = state.routeVersion;
5692
+ if (!handoffTimeout) {
5693
+ handoffTimeout = setTimeout(() => {
5694
+ var _a;
5695
+ if (!handoffBlockerId)
5696
+ return;
5697
+ const handedOverBatchId = handoffBlockerId;
5698
+ (_a = options.warn) === null || _a === void 0 ? void 0 : _a.call(options, 'Variant runtime route handoff timed out', {
5699
+ routeVersion: abandonedRouteVersion,
5700
+ });
5701
+ clearHandoff();
5702
+ forceRevealOnTimeout(handedOverBatchId);
5703
+ }, timeUntilDeadline());
5704
+ }
5705
+ }
5706
+ }
5707
+ state.routeVersion += 1;
5708
+ state.currentUrl = currentUrl;
5709
+ },
5710
+ beginBatch(input) {
5711
+ if (destroyed || input.expectedSessionCount === 0)
5712
+ return null;
5713
+ if (activeBatch)
5714
+ invalidateBatch(activeBatch.batchId);
5715
+ clearHandoff();
5716
+ const batch = {
5717
+ batchId: createId(),
5718
+ routeVersion: state.routeVersion,
5719
+ expectedSessionCount: input.expectedSessionCount,
5720
+ closed: false,
5721
+ released: state.pageRevealed,
5722
+ sessions: new Map(),
5723
+ timeout: null,
5724
+ };
5725
+ activeBatch = batch;
5726
+ if (!state.pageRevealed &&
5727
+ options.revealCoordinator.block(batch.batchId)) {
5728
+ batch.timeout = setTimeout(() => {
5729
+ if (activeBatch !== batch || batch.released)
5730
+ return;
5731
+ warnUnsettled(batch, 'batch-timeout');
5732
+ batch.released = true;
5733
+ forceRevealOnTimeout(batch.batchId);
5734
+ }, timeUntilDeadline());
5735
+ }
5736
+ return { batchId: batch.batchId, routeVersion: batch.routeVersion };
5737
+ },
5738
+ createVariantSession(input) {
5739
+ var _a;
5740
+ const batch = activeBatch;
5741
+ if (!batch ||
5742
+ batch.batchId !== input.batchId ||
5743
+ batch.routeVersion !== state.routeVersion ||
5744
+ (batch.released && !state.pageRevealed))
5745
+ return null;
5746
+ const session = createVariantSession({
5747
+ sessionId: createId(),
5748
+ variantId: input.variantId,
5749
+ batchId: batch.batchId,
5750
+ routeVersion: batch.routeVersion,
5751
+ getPageState: () => ({ ...state }),
5752
+ subscribePage: (event, handler, onlyOnce) => {
5753
+ if (onlyOnce)
5754
+ return once(event, handler);
5755
+ if (stickySatisfied(event)) {
5756
+ callListener(event, handler, stickyPayload(event));
5757
+ }
5758
+ return on(event, handler);
5759
+ },
5760
+ onStateChange: () => maybeReleaseBatch(batch),
5761
+ emitLegacyVariantRendered: (_a = options.emitLegacyVariantRendered) !== null && _a !== void 0 ? _a : (() => { }),
5762
+ ingestApplied: (currentSession, payload) => {
5763
+ var _a, _b;
5764
+ return (_b = (_a = options.onVariantApplied) === null || _a === void 0 ? void 0 : _a.call(options, { ...state }, currentSession.getState(), payload)) !== null && _b !== void 0 ? _b : false;
5765
+ },
5766
+ onIngestLimit: (currentSession) => warnForSession(currentSession, 'Variant ingestApplied limit reached'),
5767
+ onMetadataDropped: (currentSession) => warnForSession(currentSession, 'Variant ingestApplied metadata dropped'),
5768
+ onError: (currentSession, error) => {
5769
+ var _a;
5770
+ const sessionState = currentSession.getState();
5771
+ (_a = options.error) === null || _a === void 0 ? void 0 : _a.call(options, 'Variant runtime session failed', {
5772
+ error: describeError(error),
5773
+ variantId: sessionState.variantId,
5774
+ sessionId: sessionState.sessionId,
5775
+ batchId: sessionState.batchId,
5776
+ routeVersion: sessionState.routeVersion,
5777
+ currentUrl: state.currentUrl,
5778
+ });
5779
+ },
5780
+ });
5781
+ if (state.pageRevealed)
5782
+ session.markPageRevealed();
5783
+ batch.sessions.set(session.getState().sessionId, session);
5784
+ maybeReleaseBatch(batch);
5785
+ return session;
5786
+ },
5787
+ closeBatch(batchId) {
5788
+ if (!activeBatch || activeBatch.batchId !== batchId)
5789
+ return;
5790
+ activeBatch.closed = true;
5791
+ maybeReleaseBatch(activeBatch);
5792
+ },
5793
+ invalidateBatch,
5794
+ invalidateAllSessions() {
5795
+ if (activeBatch)
5796
+ invalidateBatch(activeBatch.batchId);
5797
+ },
5798
+ getDebugSnapshot() {
5799
+ return {
5800
+ page: { ...state },
5801
+ activeBatch: activeBatch
5802
+ ? {
5803
+ batchId: activeBatch.batchId,
5804
+ routeVersion: activeBatch.routeVersion,
5805
+ expectedSessionCount: activeBatch.expectedSessionCount,
5806
+ closed: activeBatch.closed,
5807
+ released: activeBatch.released,
5808
+ sessionStates: Array.from(activeBatch.sessions.values()).map((session) => session.getState()),
5809
+ }
5810
+ : null,
5811
+ };
5812
+ },
5813
+ isDestroyed: () => destroyed,
5814
+ destroy() {
5815
+ destroyed = true;
5816
+ const blockerIds = [handoffBlockerId, activeBatch === null || activeBatch === void 0 ? void 0 : activeBatch.batchId];
5817
+ if (activeBatch)
5818
+ invalidateBatch(activeBatch.batchId);
5819
+ clearHandoff();
5820
+ for (const blockerId of blockerIds) {
5821
+ if (blockerId)
5822
+ options.revealCoordinator.unblock(blockerId);
5823
+ }
5824
+ coordinatorStop();
5825
+ listeners.clear();
5826
+ },
5827
+ };
5828
+ return runtime;
5829
+ }
5830
+
5831
+ const MAX_TARGET_SELECTORS = 20;
5832
+ const MAX_SELECTOR_LENGTH = 500;
5833
+ const MAX_METADATA_JSON_BYTES = 4096;
5834
+ const MAX_METADATA_VISITS = 20000;
5835
+ const MAX_SELECTOR_ENTRIES_SCANNED = 200;
5836
+ function utf8ByteLength(value) {
5837
+ let bytes = 0;
5838
+ for (let i = 0; i < value.length; i++) {
5839
+ const code = value.charCodeAt(i);
5840
+ if (code < 0x80)
5841
+ bytes += 1;
5842
+ else if (code < 0x800)
5843
+ bytes += 2;
5844
+ else if (code >= 0xd800 && code <= 0xdbff) {
5845
+ const next = value.charCodeAt(i + 1);
5846
+ if (next >= 0xdc00 && next <= 0xdfff) {
5847
+ bytes += 4;
5848
+ i++;
5849
+ }
5850
+ else {
5851
+ bytes += 3;
5852
+ }
5853
+ }
5854
+ else
5855
+ bytes += 3;
5856
+ }
5857
+ return bytes;
5858
+ }
5859
+ function isBoxedString(value) {
5860
+ if (!value || typeof value !== 'object')
5861
+ return false;
5862
+ try {
5863
+ String.prototype.valueOf.call(value);
5864
+ return true;
5865
+ }
5866
+ catch (_a) {
5867
+ return false;
5868
+ }
5869
+ }
5870
+ function sanitizeMetadata(payload) {
5871
+ try {
5872
+ const rawMetadata = payload.metadata;
5873
+ if (rawMetadata === undefined || rawMetadata === null)
5874
+ return {};
5875
+ if (typeof rawMetadata !== 'object' || Array.isArray(rawMetadata)) {
5876
+ return null;
5877
+ }
5878
+ let minimumLength = 0;
5879
+ let visits = 0;
5880
+ const json = JSON.stringify(rawMetadata, function (key, value) {
5881
+ visits += 1;
5882
+ if (visits > MAX_METADATA_VISITS) {
5883
+ throw new Error('metadata too large');
5884
+ }
5885
+ const isObjectProperty = visits > 1 && !Array.isArray(this);
5886
+ if (isObjectProperty &&
5887
+ (value === undefined ||
5888
+ typeof value === 'function' ||
5889
+ typeof value === 'symbol')) {
5890
+ return value;
5891
+ }
5892
+ if (isObjectProperty)
5893
+ minimumLength += key.length + 3;
5894
+ const written = isBoxedString(value) ? String(value) : value;
5895
+ minimumLength += typeof written === 'string' ? written.length + 2 : 1;
5896
+ if (minimumLength > MAX_METADATA_JSON_BYTES) {
5897
+ throw new Error('metadata too large');
5898
+ }
5899
+ return written;
5900
+ });
5901
+ if (!json ||
5902
+ json.length > MAX_METADATA_JSON_BYTES ||
5903
+ utf8ByteLength(json) > MAX_METADATA_JSON_BYTES) {
5904
+ return null;
5905
+ }
5906
+ const copy = JSON.parse(json);
5907
+ return copy && typeof copy === 'object' && !Array.isArray(copy)
5908
+ ? copy
5909
+ : null;
5910
+ }
5911
+ catch (_a) {
5912
+ return null;
5913
+ }
5914
+ }
5915
+ function sanitizeSelectors(payload) {
5916
+ try {
5917
+ const selectors = payload.targetSelectors;
5918
+ if (!Array.isArray(selectors))
5919
+ return [];
5920
+ const kept = [];
5921
+ const scanned = Math.min(selectors.length, MAX_SELECTOR_ENTRIES_SCANNED);
5922
+ for (let i = 0; i < scanned; i++) {
5923
+ let selector;
5924
+ try {
5925
+ selector = selectors[i];
5926
+ }
5927
+ catch (_a) {
5928
+ continue;
5929
+ }
5930
+ if (typeof selector === 'string' &&
5931
+ selector.length > 0 &&
5932
+ selector.length <= MAX_SELECTOR_LENGTH) {
5933
+ kept.push(selector);
5934
+ if (kept.length === MAX_TARGET_SELECTORS)
5935
+ break;
5936
+ }
5937
+ }
5938
+ return kept;
5939
+ }
5940
+ catch (_b) {
5941
+ return [];
5942
+ }
5943
+ }
5944
+ function sanitizePayload(payloadInput) {
5945
+ if (!payloadInput ||
5946
+ typeof payloadInput !== 'object' ||
5947
+ Array.isArray(payloadInput)) {
5948
+ const given = payloadInput !== undefined && payloadInput !== null;
5949
+ return { metadata: {}, metadataDropped: given, targetSelectors: [] };
5950
+ }
5951
+ const payload = payloadInput;
5952
+ const metadata = sanitizeMetadata(payload);
5953
+ return {
5954
+ metadata: metadata || {},
5955
+ metadataDropped: metadata === null,
5956
+ targetSelectors: sanitizeSelectors(payload),
5957
+ };
5958
+ }
5959
+ function ingestVariantApplied(coframe, pageState, sessionState, payloadInput) {
5960
+ const { metadata, metadataDropped, targetSelectors } = sanitizePayload(payloadInput);
5961
+ const personalizationHeaders = coframe.personalizationHeaders;
5962
+ const event = {
5963
+ type: 'variant_applied',
5964
+ project_id: coframe.projectId || '',
5965
+ session_id: coframe.sessionId || '',
5966
+ time: new Date(),
5967
+ url: getCurrentPageUrl(),
5968
+ user_token: coframe.userToken || '',
5969
+ org_id: coframe.orgId || '',
5970
+ variant_id: sessionState.variantId,
5971
+ content: {
5972
+ metadata: {
5973
+ batch_id: sessionState.batchId,
5974
+ route_version: pageState.routeVersion,
5975
+ current_url: pageState.currentUrl,
5976
+ ...(personalizationHeaders
5977
+ ? { personalization_headers: { ...personalizationHeaders } }
5978
+ : {}),
5979
+ ...(targetSelectors.length
5980
+ ? { target_selectors: targetSelectors }
5981
+ : {}),
5982
+ ...(Object.keys(metadata).length ? { custom_data: metadata } : {}),
5983
+ },
5984
+ },
5985
+ };
5986
+ try {
5987
+ document.dispatchEvent(new CustomEvent('coframe:variant-applied', {
5988
+ detail: {
5989
+ ...event,
5990
+ time: new Date(event.time.getTime()),
5991
+ content: {
5992
+ metadata: JSON.parse(JSON.stringify(event.content.metadata)),
5993
+ },
5994
+ },
5995
+ }));
5996
+ }
5997
+ catch (_a) {
5998
+ }
5999
+ if (coframe.projectId)
6000
+ queueEventForBatchedSending(coframe, event);
6001
+ return metadataDropped;
6002
+ }
6003
+
4752
6004
  function push(cmd) {
4753
6005
  if (typeof window === 'undefined')
4754
6006
  return;
@@ -4796,34 +6048,81 @@ const Coframe = {
4796
6048
  },
4797
6049
  };
4798
6050
  Coframe.logger = createLogger(() => Coframe);
4799
- async function init(options) {
6051
+ const revealCoordinator = createRevealCoordinator(revealPage, logRevealListenerError(() => Coframe.logger));
6052
+ let activeRuntime = null;
6053
+ function clearActiveRuntime() {
6054
+ activeRuntime === null || activeRuntime === void 0 ? void 0 : activeRuntime.destroy();
6055
+ activeRuntime = null;
6056
+ delete Coframe.runtime;
6057
+ }
6058
+ function installRuntime(config) {
6059
+ const batchTimeoutMs = config.timeoutMs
6060
+ ? Math.min(config.timeoutMs, TIMEOUT_MAX_MS)
6061
+ : undefined;
6062
+ clearActiveRuntime();
6063
+ if (!isVariantRuntimeLifecycleEnabled(config))
6064
+ return undefined;
6065
+ const runtime = createPageRuntime({
6066
+ revealCoordinator,
6067
+ currentUrl: getCurrentPageUrl(),
6068
+ initialHydrated: !config.waitForHydration,
6069
+ batchTimeoutMs,
6070
+ warn: (message, metadata) => Coframe.logger.warn(message, 'variant_runtime_warning', metadata),
6071
+ error: (message, metadata) => Coframe.logger.error(message, 'variant_runtime_error', metadata),
6072
+ emitLegacyVariantRendered: variantRendered,
6073
+ onVariantApplied: (pageState, sessionState, payload) => ingestVariantApplied(Coframe, pageState, sessionState, payload),
6074
+ });
6075
+ activeRuntime = runtime;
6076
+ Coframe.runtime = Object.freeze({
6077
+ getSnapshot: () => runtime.getDebugSnapshot(),
6078
+ });
6079
+ runtime.publishSdkReady();
6080
+ return runtime;
6081
+ }
6082
+ async function initialize(options, context = {}) {
4800
6083
  if (typeof window !== 'undefined' &&
4801
6084
  /cf_disable/.test(window.location.search)) {
4802
6085
  console.warn('[Coframe] Disabled via URL parameter');
4803
- revealPage();
6086
+ revealCoordinator.forceReveal('graceful-exit');
4804
6087
  return;
4805
6088
  }
4806
6089
  if (options)
4807
6090
  injectOptions(options);
4808
6091
  const gracefulExit = (msg) => {
4809
6092
  console.warn(`[Coframe] ${msg}`);
4810
- revealPage();
6093
+ revealCoordinator.forceReveal('graceful-exit');
4811
6094
  return;
4812
6095
  };
4813
- const configResult = await resolveConfig(Coframe, revealPage);
6096
+ const configResult = await resolveConfig(Coframe, revealCoordinator);
4814
6097
  if (!configResult)
4815
6098
  return;
6099
+ const runtime = installRuntime(configResult.config);
6100
+ context.runtime = runtime;
4816
6101
  loadLauncherIfEnabled(configResult.config.enableDebug, configResult.config.nonce);
4817
- const projectResult = await resolveProject(Coframe, configResult.previewVariantId, configResult.isSessionVariant, configResult.config);
4818
- if (!projectResult)
6102
+ const projectResult = await resolveProject(Coframe, configResult.previewVariantId, configResult.isSessionVariant, configResult.config, () => !(runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed()));
6103
+ if (runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed())
6104
+ return;
6105
+ if (!projectResult) {
6106
+ revealCoordinator.forceReveal('graceful-exit');
4819
6107
  return;
6108
+ }
4820
6109
  Coframe.orgId = projectResult.project.org_id || '';
4821
6110
  Coframe.personalizationHeaders = projectResult.personalizationHeaders;
4822
- const identity = await resolveIdentity(configResult.config, Coframe, configResult.timeoutMs, gracefulExit, configResult.previewVariantId);
6111
+ const identity = await resolveIdentity(configResult.config, Coframe, configResult.timeoutMs, (msg) => {
6112
+ if (!(runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed()))
6113
+ gracefulExit(msg);
6114
+ }, configResult.previewVariantId);
4823
6115
  if (!identity)
4824
6116
  return;
6117
+ if (runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed())
6118
+ return;
4825
6119
  const analytics = setupTracking(Coframe, projectResult, configResult.previewVariantId, configResult.executionStartTime, configResult.config.nonce, configResult.config.waitForCookiesEnabled, identity.onConsentChange);
4826
- await modifyPage(Coframe, configResult, projectResult, analytics);
6120
+ await modifyPage(Coframe, configResult, projectResult, analytics, {
6121
+ revealCoordinator,
6122
+ runtime,
6123
+ });
6124
+ if (runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed())
6125
+ return;
4827
6126
  try {
4828
6127
  startReplayIfEnabled(Coframe, projectResult.project, configResult.config.nonce);
4829
6128
  }
@@ -4831,6 +6130,19 @@ async function init(options) {
4831
6130
  console.warn('[Coframe] Failed to start session replay', error);
4832
6131
  }
4833
6132
  }
6133
+ async function init(options) {
6134
+ var _a;
6135
+ const context = {};
6136
+ try {
6137
+ await initialize(options, context);
6138
+ }
6139
+ catch (error) {
6140
+ if (!((_a = context.runtime) === null || _a === void 0 ? void 0 : _a.isDestroyed())) {
6141
+ revealCoordinator.forceReveal('init-error');
6142
+ }
6143
+ throw error;
6144
+ }
6145
+ }
4834
6146
  Coframe.debug = {
4835
6147
  getSelector,
4836
6148
  getElement,