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