@coframe/sdk 1.1.43 → 1.1.45

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 {
@@ -3978,26 +4282,32 @@ function resolvePreview(pageUrl, previewVariantId, isSessionVariant) {
3978
4282
  }
3979
4283
  return { previewVariantId: null, isSessionVariant: false };
3980
4284
  }
4285
+ const isPageHydratedCmd = (cmd) => cmd.pageHydrated === true || cmd.emit === 'pageHydrated';
3981
4286
  async function awaitHydration(config, Coframe, project) {
3982
4287
  var _a, _b;
3983
- if (!config.waitForHydration)
4288
+ if (!shouldSdkWaitForHydration(config))
3984
4289
  return true;
3985
4290
  if (!((_a = project.frames) === null || _a === void 0 ? void 0 : _a.length) && !((_b = project.tags) === null || _b === void 0 ? void 0 : _b.length))
3986
4291
  return true;
3987
- const cmd = await waitForCmd((cmd) => cmd.pageHydrated === true || cmd.emit === 'pageHydrated', Coframe);
4292
+ const cmd = await waitForCmd(isPageHydratedCmd, Coframe);
3988
4293
  if (!cmd) {
3989
4294
  Coframe.logger.warn('Failed to wait for page hydrated command', 'wait_for_cmd_timeout');
3990
4295
  return false;
3991
4296
  }
3992
4297
  return true;
3993
4298
  }
3994
- async function ensureBody(Coframe, cdnProject, timeoutMs, nonce) {
4299
+ async function ensureBody(Coframe, cdnProject, timeoutMs, nonce, revealCoordinator, isCurrent) {
3995
4300
  const bodyPresent = await waitForBody(timeoutMs).catch(() => false);
3996
4301
  if (!bodyPresent) {
3997
4302
  Coframe.logger.error('Timeout waiting for document.body, exiting gracefully', 'body_timeout');
3998
- variantRendered();
4303
+ if (isCurrent()) {
4304
+ variantRendered();
4305
+ revealCoordinator.forceReveal('graceful-exit');
4306
+ }
3999
4307
  return false;
4000
4308
  }
4309
+ if (!isCurrent())
4310
+ return false;
4001
4311
  rebindMetrics(Coframe, Coframe.logger, cdnProject === null || cdnProject === void 0 ? void 0 : cdnProject.metrics, nonce);
4002
4312
  return true;
4003
4313
  }
@@ -4010,7 +4320,13 @@ function setupFlushInterval(Coframe) {
4010
4320
  }
4011
4321
  flushInterval = setInterval(() => flushQueuedEvents(Coframe.projectId), FLUSH_EVENTS_TIME);
4012
4322
  }
4013
- function setupSPANavigation(Coframe, project, config, previewVariantId, isSessionVariant, applyVariants) {
4323
+ const hasNoVariants = (Coframe) => !Coframe.variantsAndElements || Coframe.variantsAndElements.length === 0;
4324
+ const logNavigationError = (Coframe, message, error) => {
4325
+ Coframe.logger.error(message, 'spa_navigation_error', {
4326
+ error: describeError(error),
4327
+ });
4328
+ };
4329
+ function setupSPANavigation(Coframe, project, config, previewVariantId, isSessionVariant, runtime, applier, firstViewDeadline, onFirstViewDone) {
4014
4330
  var _a;
4015
4331
  if (!((_a = project === null || project === void 0 ? void 0 : project.settings) === null || _a === void 0 ? void 0 : _a.mutation_observer_active))
4016
4332
  return;
@@ -4035,16 +4351,20 @@ function setupSPANavigation(Coframe, project, config, previewVariantId, isSessio
4035
4351
  const observer = new MutationObserver(mutationCallback);
4036
4352
  observer.observe(document.body, { childList: true, subtree: true });
4037
4353
  let navigationAbortController = null;
4038
- observeUrlChanges(false, async (newPageUrl, _oldPageUrl) => {
4354
+ const onUrlChange = async (newPageUrl, _oldPageUrl, options = {}) => {
4039
4355
  var _a;
4040
- sendPageLeave(Coframe);
4356
+ const firstView = !applier.hasCommitted();
4357
+ if (!firstView)
4358
+ sendPageLeave(Coframe);
4041
4359
  if (navigationAbortController)
4042
4360
  navigationAbortController.abort();
4043
- navigationAbortController = new AbortController();
4044
- const { signal } = navigationAbortController;
4045
- await new Promise((resolve) => requestAnimationFrame(resolve));
4046
- if (signal.aborted)
4047
- return;
4361
+ runtime === null || runtime === void 0 ? void 0 : runtime.changeRoute(newPageUrl);
4362
+ const controller = createNavigationController();
4363
+ navigationAbortController = controller;
4364
+ const { signal } = controller;
4365
+ await waitForFrame(options.routeAlreadyRendered ? HANDOVER_FRAME_CAP_MS : undefined, NAVIGATION_FRAME_CAP_MS);
4366
+ if (signal.aborted || (runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed()))
4367
+ return 'skipped';
4048
4368
  infiniteLoopProtection = 0;
4049
4369
  let pageProject = project;
4050
4370
  let pagePersonalizationHeaders = Coframe.personalizationHeaders;
@@ -4060,9 +4380,9 @@ function setupSPANavigation(Coframe, project, config, previewVariantId, isSessio
4060
4380
  else {
4061
4381
  try {
4062
4382
  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;
4383
+ 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());
4384
+ if (signal.aborted || (runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed()))
4385
+ return 'skipped';
4066
4386
  if (result) {
4067
4387
  pageProject = result.project;
4068
4388
  pagePersonalizationHeaders = {
@@ -4075,10 +4395,37 @@ function setupSPANavigation(Coframe, project, config, previewVariantId, isSessio
4075
4395
  catch (_b) {
4076
4396
  }
4077
4397
  }
4078
- if (signal.aborted)
4079
- return;
4080
- await applyVariants(newPageUrl, pageProject, pagePersonalizationHeaders, signal);
4081
- });
4398
+ if (signal.aborted || (runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed()))
4399
+ return 'skipped';
4400
+ let result = 'skipped';
4401
+ let threw = false;
4402
+ try {
4403
+ result = await applier.apply(newPageUrl, pageProject, pagePersonalizationHeaders, signal, firstView ? firstViewDeadline : undefined);
4404
+ }
4405
+ catch (error) {
4406
+ threw = true;
4407
+ if (firstView && !signal.aborted && !(runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed())) {
4408
+ onFirstViewDone === null || onFirstViewDone === void 0 ? void 0 : onFirstViewDone(pageProject);
4409
+ }
4410
+ throw error;
4411
+ }
4412
+ finally {
4413
+ if (shouldSettleRoute(signal.aborted, threw, result)) {
4414
+ runtime === null || runtime === void 0 ? void 0 : runtime.settleRoute();
4415
+ }
4416
+ }
4417
+ if (firstView && result === 'applied' && !(runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed())) {
4418
+ if (hasNoVariants(Coframe))
4419
+ variantRendered();
4420
+ onFirstViewDone === null || onFirstViewDone === void 0 ? void 0 : onFirstViewDone(pageProject);
4421
+ }
4422
+ return result;
4423
+ };
4424
+ observeUrlChanges(false, (newPageUrl, oldPageUrl) => onUrlChange(newPageUrl).catch((error) => {
4425
+ logNavigationError(Coframe, 'SPA navigation failed', error);
4426
+ return 'skipped';
4427
+ }), () => !(runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed()));
4428
+ return onUrlChange;
4082
4429
  }
4083
4430
  function readLaneClaims() {
4084
4431
  const cookie = getCookie(LANE_COOKIE_KEY);
@@ -4108,17 +4455,14 @@ function persistLaneClaims(claims, logger) {
4108
4455
  setCookie(LANE_COOKIE_KEY, encodeURIComponent(serialized), logger);
4109
4456
  }
4110
4457
  }
4111
- function createApplyVariants(Coframe, config, previewVariantId, isSessionVariant, executionStartTime, analytics, laneStore) {
4458
+ function createApplyVariants(Coframe, config, previewVariantId, isSessionVariant, executionStartTime, analytics, laneStore, runtime) {
4112
4459
  let isFirstApply = true;
4113
4460
  let applicationId = 0;
4114
- return async (pageUrl, pageProject, personalizationHeaders, signal, deadline) => {
4115
- var _a;
4461
+ const apply = async (pageUrl, pageProject, personalizationHeaders, signal, deadline) => {
4462
+ var _a, _b;
4116
4463
  const currentApplication = ++applicationId;
4117
4464
  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;
4465
+ return 'skipped';
4122
4466
  const preview = resolvePreview(pageUrl, previewVariantId, isSessionVariant);
4123
4467
  const chooseCmabVariants = createCmabVariantBatchChooser({
4124
4468
  projectId: Coframe.projectId,
@@ -4132,8 +4476,22 @@ function createApplyVariants(Coframe, config, previewVariantId, isSessionVariant
4132
4476
  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
4477
  if ((signal === null || signal === void 0 ? void 0 : signal.aborted) ||
4134
4478
  currentApplication !== applicationId ||
4479
+ (runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed()) ||
4135
4480
  (signal && !urlsMatch(pageUrl, getCurrentPageUrl())))
4136
- return;
4481
+ return 'skipped';
4482
+ if (!signal &&
4483
+ runtime &&
4484
+ ((_b = pageProject === null || pageProject === void 0 ? void 0 : pageProject.settings) === null || _b === void 0 ? void 0 : _b.mutation_observer_active) &&
4485
+ !urlsMatch(pageUrl, getCurrentPageUrl(), true))
4486
+ return 'stale-route';
4487
+ if (!signal && runtime) {
4488
+ const urlAtCommit = getCurrentPageUrl();
4489
+ if (urlsMatch(pageUrl, urlAtCommit, true))
4490
+ runtime.updateUrl(urlAtCommit);
4491
+ }
4492
+ const sessionId = isFirstApply ? Coframe.sessionId : generateUUID();
4493
+ const sessionContext = buildSessionContext(isFirstApply);
4494
+ isFirstApply = false;
4137
4495
  Coframe.sessionId = sessionId;
4138
4496
  Coframe.personalizationHeaders = personalizationHeaders;
4139
4497
  setIsPreviewVariant(!!preview.previewVariantId);
@@ -4151,7 +4509,9 @@ function createApplyVariants(Coframe, config, previewVariantId, isSessionVariant
4151
4509
  variantRendered();
4152
4510
  }
4153
4511
  else {
4154
- replaceVariants(variantsAndElements, Coframe.orgId, Coframe.logger, config.nonce);
4512
+ await replaceVariants(variantsAndElements, Coframe.orgId, Coframe.logger, config.nonce, runtime);
4513
+ if (runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed())
4514
+ return 'skipped';
4155
4515
  }
4156
4516
  const cmabDecisions = chooseCmabVariants.getDecisions(experimentsInfo);
4157
4517
  startSession(Coframe, isCookiesEnabled, executionStartTime, personalizationHeaders, {
@@ -4160,29 +4520,77 @@ function createApplyVariants(Coframe, config, previewVariantId, isSessionVariant
4160
4520
  }, pageUrl);
4161
4521
  analytics.resetBroadcasted();
4162
4522
  analytics.sendAnalytics();
4523
+ return 'applied';
4163
4524
  };
4525
+ return { apply, hasCommitted: () => !isFirstApply };
4164
4526
  }
4165
- async function modifyPage(Coframe, configResult, projectResult, analytics) {
4527
+ async function modifyPage(Coframe, configResult, projectResult, analytics, lifecycle) {
4528
+ var _a;
4166
4529
  const { config, timeoutMs, previewVariantId, isSessionVariant, integrationStartTime, executionStartTime, } = configResult;
4167
4530
  const { project: cdnProject, personalizationHeaders } = projectResult;
4531
+ const revealCoordinator = (_a = lifecycle === null || lifecycle === void 0 ? void 0 : lifecycle.revealCoordinator) !== null && _a !== void 0 ? _a : createRevealCoordinator(revealPage, logRevealListenerError(() => Coframe.logger));
4532
+ const runtime = lifecycle === null || lifecycle === void 0 ? void 0 : lifecycle.runtime;
4533
+ if (runtime && config.waitForHydration) {
4534
+ if (findLastCmd(isPageHydratedCmd)) {
4535
+ runtime.publishHydrated();
4536
+ }
4537
+ else {
4538
+ let stopHydration = () => { };
4539
+ stopHydration = subscribeToCmd(isPageHydratedCmd, () => {
4540
+ stopHydration();
4541
+ runtime.publishHydrated();
4542
+ });
4543
+ }
4544
+ }
4168
4545
  if (!(await awaitHydration(config, Coframe, cdnProject)))
4169
4546
  return;
4547
+ if (runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed())
4548
+ return;
4170
4549
  if (cdnProject.tags) {
4171
4550
  executeTags(cdnProject.tags, Coframe.logger, config.nonce);
4172
4551
  Coframe.status.tags = cdnProject.tags;
4173
4552
  }
4174
- if (!(await ensureBody(Coframe, cdnProject, timeoutMs, config.nonce)))
4553
+ if (!(await ensureBody(Coframe, cdnProject, timeoutMs, config.nonce, revealCoordinator, () => !(runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed()))))
4175
4554
  return;
4176
4555
  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();
4556
+ const applier = createApplyVariants(Coframe, config, previewVariantId, isSessionVariant, executionStartTime, analytics, laneStore, runtime);
4557
+ const initialUrl = getCurrentPageUrl();
4558
+ if (runtime) {
4559
+ const runtimeUrl = runtime.getState().currentUrl;
4560
+ if (!urlsMatch(runtimeUrl, initialUrl, true))
4561
+ runtime.changeRoute(initialUrl);
4562
+ }
4563
+ const firstDeadline = (integrationStartTime || executionStartTime).getTime() + timeoutMs;
4564
+ const firstResult = await applier.apply(initialUrl, cdnProject, personalizationHeaders, undefined, firstDeadline);
4565
+ if (runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed())
4566
+ return;
4567
+ const routeChangedDuringInit = firstResult === 'stale-route';
4568
+ if (!routeChangedDuringInit && hasNoVariants(Coframe)) {
4569
+ revealCoordinator.requestReveal('no-variants');
4182
4570
  variantRendered();
4183
4571
  }
4184
- reportInitTiming(Coframe, cdnProject, integrationStartTime, executionStartTime);
4185
- setupSPANavigation(Coframe, cdnProject, config, previewVariantId, isSessionVariant, applyVariants);
4572
+ if (!routeChangedDuringInit)
4573
+ revealCoordinator.flushPending();
4574
+ let timingReported = false;
4575
+ const reportTiming = (appliedProject = cdnProject) => {
4576
+ if (timingReported)
4577
+ return;
4578
+ timingReported = true;
4579
+ reportInitTiming(Coframe, appliedProject, integrationStartTime, executionStartTime);
4580
+ };
4581
+ if (!routeChangedDuringInit)
4582
+ reportTiming();
4583
+ const onUrlChange = setupSPANavigation(Coframe, cdnProject, config, previewVariantId, isSessionVariant, runtime, applier, firstDeadline, routeChangedDuringInit ? reportTiming : undefined);
4584
+ const urlNow = getCurrentPageUrl();
4585
+ if (runtime &&
4586
+ onUrlChange &&
4587
+ (routeChangedDuringInit || !urlsMatch(initialUrl, urlNow, true))) {
4588
+ onUrlChange(urlNow, initialUrl, {
4589
+ routeAlreadyRendered: routeChangedDuringInit,
4590
+ }).catch((error) => {
4591
+ logNavigationError(Coframe, 'Applying the route reached during init failed', error);
4592
+ });
4593
+ }
4186
4594
  setupFlushInterval(Coframe);
4187
4595
  }
4188
4596
 
@@ -4420,6 +4828,12 @@ function startReplayIfEnabled(coframe, project, nonce) {
4420
4828
  var _a;
4421
4829
  const consent = readReplayConsent(settings);
4422
4830
  if (consent.state === 'allowed') {
4831
+ try {
4832
+ queueMonitoringEvent(coframe, createMonitoringPayload(coframe, 'replay_consent_allowed', {
4833
+ source: consent.source,
4834
+ }));
4835
+ }
4836
+ catch (_b) { }
4423
4837
  insertReplayScript(coframe, settings, consent.source, nonce);
4424
4838
  }
4425
4839
  else if (consent.state === 'denied') {
@@ -4441,7 +4855,7 @@ function startReplayIfEnabled(coframe, project, nonce) {
4441
4855
  loadFeatures.call(window.Shopify, [{ name: 'consent-tracking-api', version: '0.1' }], () => { });
4442
4856
  }
4443
4857
  }
4444
- catch (_b) { }
4858
+ catch (_c) { }
4445
4859
  }
4446
4860
  waited += CONSENT_POLL_MS;
4447
4861
  window.setTimeout(decide, CONSENT_POLL_MS);
@@ -4504,11 +4918,12 @@ async function resolveFeatureFlag(Coframe, timeoutMs) {
4504
4918
  return undefined;
4505
4919
  return getFeatureFlagValue(cmd);
4506
4920
  }
4507
- async function resolveConfig(Coframe, revealPage) {
4921
+ async function resolveConfig(Coframe, revealActions) {
4508
4922
  const logger = Coframe.logger;
4509
4923
  const configCmd = await waitForCmd((cmd) => !!cmd.config, Coframe);
4510
4924
  if (!configCmd) {
4511
4925
  logger.error('Failed to wait for config cmd', 'wait_for_cmd_timeout');
4926
+ revealActions.forceReveal('config-timeout');
4512
4927
  return null;
4513
4928
  }
4514
4929
  const config = configCmd.config;
@@ -4524,7 +4939,7 @@ async function resolveConfig(Coframe, revealPage) {
4524
4939
  configureReplayScriptUrl(config.replayScriptUrl);
4525
4940
  const gracefulExit = (msg) => {
4526
4941
  console.warn(`[Coframe] ${msg}`);
4527
- revealPage();
4942
+ revealActions.forceReveal('graceful-exit');
4528
4943
  return null;
4529
4944
  };
4530
4945
  if (config.waitForFeatureFlag) {
@@ -4537,8 +4952,9 @@ async function resolveConfig(Coframe, revealPage) {
4537
4952
  }
4538
4953
  }
4539
4954
  const projectId = config === null || config === void 0 ? void 0 : config.projectId;
4955
+ const lifecycleMode = isVariantRuntimeLifecycleEnabled(config);
4540
4956
  const warnMissingVariant = () => {
4541
- if (applyVariantChangesStarted) {
4957
+ if (applyVariantChangesStarted && !lifecycleMode) {
4542
4958
  logger.warn('Failed to wait for variant rendered command', 'missing_variant_rendered');
4543
4959
  }
4544
4960
  };
@@ -4551,14 +4967,15 @@ async function resolveConfig(Coframe, revealPage) {
4551
4967
  }
4552
4968
  logger.warn('Variant not rendered', 'variant_not_rendered', metadata);
4553
4969
  };
4554
- waitForCmd((cmd) => cmd.variantRendered === true || cmd.emit === 'variantRendered', Coframe, timeoutMs)
4555
- .then((cmd) => {
4556
- if (!cmd) {
4557
- warnMissingVariant();
4558
- }
4559
- })
4560
- .finally(() => {
4561
- revealPage();
4970
+ waitForCmd((cmd) => cmd.variantRendered === true || cmd.emit === 'variantRendered', Coframe, timeoutMs, QueueName.CFQ, !lifecycleMode).then((cmd) => {
4971
+ if (cmd) {
4972
+ revealActions.requestReveal('legacy-variant-rendered');
4973
+ return;
4974
+ }
4975
+ warnMissingVariant();
4976
+ if (revealActions.getSnapshot().pageRevealed)
4977
+ return;
4978
+ revealActions.forceReveal('config-timeout');
4562
4979
  });
4563
4980
  const loggedReasons = [];
4564
4981
  listenForCmd((cmd) => cmd.emit === 'variantNotRendered', (cmd) => {
@@ -4572,6 +4989,7 @@ async function resolveConfig(Coframe, revealPage) {
4572
4989
  description: 'No Coframe Page ID',
4573
4990
  });
4574
4991
  variantRendered();
4992
+ revealActions.forceReveal('graceful-exit');
4575
4993
  return null;
4576
4994
  }
4577
4995
  Coframe.projectId = projectId;
@@ -4743,6 +5161,824 @@ function injectOptions(options) {
4743
5161
  }
4744
5162
  }
4745
5163
 
5164
+ const TERMINAL_STATUSES = new Set([
5165
+ 'completed',
5166
+ 'error',
5167
+ 'cleaned',
5168
+ ]);
5169
+ const DEFAULT_FADE_DURATION_MS = 150;
5170
+ const MAX_INGEST_PER_SESSION = 10;
5171
+ const TRANSITION_LONGHANDS = [
5172
+ 'transition-property',
5173
+ 'transition-duration',
5174
+ 'transition-timing-function',
5175
+ 'transition-delay',
5176
+ ];
5177
+ const fadeTargets = new Map();
5178
+ const FADE_SAFETY_MS = 5000;
5179
+ function cancelFadeAnimation(fadeTarget) {
5180
+ if (fadeTarget.animationFrame !== null) {
5181
+ cancelAnimationFrame(fadeTarget.animationFrame);
5182
+ }
5183
+ if (fadeTarget.timer)
5184
+ clearTimeout(fadeTarget.timer);
5185
+ fadeTarget.animationFrame = null;
5186
+ fadeTarget.timer = null;
5187
+ }
5188
+ function finishFade(fadeTarget) {
5189
+ if (fadeTargets.get(fadeTarget.target) !== fadeTarget)
5190
+ return;
5191
+ fadeTargets.delete(fadeTarget.target);
5192
+ cancelFadeAnimation(fadeTarget);
5193
+ fadeTarget.target.removeEventListener('transitionend', onFadeTransitionEnd);
5194
+ for (const record of Array.from(fadeTarget.records))
5195
+ record.stop();
5196
+ fadeTarget.records.clear();
5197
+ const { target } = fadeTarget;
5198
+ const style = target.style;
5199
+ style.setProperty('transition', 'none', 'important');
5200
+ style.setProperty('opacity', ...fadeTarget.opacity);
5201
+ void getComputedStyle(target).opacity;
5202
+ style.removeProperty('transition');
5203
+ if (fadeTarget.transition[0]) {
5204
+ style.setProperty('transition', ...fadeTarget.transition);
5205
+ }
5206
+ for (const [name, value, priority] of fadeTarget.longhands) {
5207
+ if (value)
5208
+ style.setProperty(name, value, priority);
5209
+ }
5210
+ }
5211
+ function onFadeTransitionEnd(event) {
5212
+ const fadeTarget = fadeTargets.get(event.target);
5213
+ if ((fadeTarget === null || fadeTarget === void 0 ? void 0 : fadeTarget.timer) && event.propertyName === 'opacity') {
5214
+ finishFade(fadeTarget);
5215
+ }
5216
+ }
5217
+ function maybeStartFade(fadeTarget) {
5218
+ if (fadeTarget.animationFrame !== null || fadeTarget.timer)
5219
+ return;
5220
+ let duration = 0;
5221
+ for (const record of Array.from(fadeTarget.records)) {
5222
+ if (!record.ready)
5223
+ return;
5224
+ duration = Math.max(duration, record.duration);
5225
+ }
5226
+ const { target } = fadeTarget;
5227
+ void getComputedStyle(target).opacity;
5228
+ fadeTarget.animationFrame = requestAnimationFrame(() => {
5229
+ fadeTarget.animationFrame = null;
5230
+ if (fadeTargets.get(target) !== fadeTarget)
5231
+ return;
5232
+ target.style.setProperty('transition', `opacity ${duration}ms ease`, 'important');
5233
+ target.style.setProperty('opacity', '1', 'important');
5234
+ fadeTarget.timer = setTimeout(() => finishFade(fadeTarget), duration + 100);
5235
+ });
5236
+ }
5237
+ function removeFadeRecord(record) {
5238
+ const { fadeTarget } = record;
5239
+ if (!fadeTarget.records.delete(record))
5240
+ return;
5241
+ record.stop();
5242
+ if (fadeTarget.records.size === 0)
5243
+ finishFade(fadeTarget);
5244
+ else
5245
+ maybeStartFade(fadeTarget);
5246
+ }
5247
+ function createVariantSession(options) {
5248
+ const listenerStops = new Set();
5249
+ let ingestCount = 0;
5250
+ let ingestLimitReported = false;
5251
+ let metadataDropReported = false;
5252
+ const fades = new Map();
5253
+ const fadeReleases = new Set();
5254
+ const state = {
5255
+ sessionId: options.sessionId,
5256
+ variantId: options.variantId,
5257
+ batchId: options.batchId,
5258
+ routeVersion: options.routeVersion,
5259
+ active: true,
5260
+ status: 'created',
5261
+ ownsPageAntiflicker: false,
5262
+ pageAntiflickerRemoved: false,
5263
+ };
5264
+ const isMutable = () => state.active && !TERMINAL_STATUSES.has(state.status);
5265
+ const notify = () => options.onStateChange(internal);
5266
+ const releaseVisuals = () => {
5267
+ var _a;
5268
+ for (const record of Array.from(fades.values()))
5269
+ removeFadeRecord(record);
5270
+ (_a = options.releaseVisuals) === null || _a === void 0 ? void 0 : _a.call(options, internal);
5271
+ };
5272
+ const fadeIn = (target, durationMs = DEFAULT_FADE_DURATION_MS) => {
5273
+ if (fades.has(target))
5274
+ return;
5275
+ const duration = typeof durationMs === 'number' && durationMs - durationMs === 0
5276
+ ? Math.max(0, durationMs)
5277
+ : DEFAULT_FADE_DURATION_MS;
5278
+ let fadeTarget = fadeTargets.get(target);
5279
+ if (!fadeTarget) {
5280
+ const style = target.style;
5281
+ fadeTarget = {
5282
+ target,
5283
+ opacity: [
5284
+ style.getPropertyValue('opacity'),
5285
+ style.getPropertyPriority('opacity'),
5286
+ ],
5287
+ transition: [
5288
+ style.getPropertyValue('transition'),
5289
+ style.getPropertyPriority('transition'),
5290
+ ],
5291
+ longhands: TRANSITION_LONGHANDS.map((name) => [
5292
+ name,
5293
+ style.getPropertyValue(name),
5294
+ style.getPropertyPriority(name),
5295
+ ]),
5296
+ records: new Set(),
5297
+ animationFrame: null,
5298
+ timer: null,
5299
+ };
5300
+ fadeTargets.set(target, fadeTarget);
5301
+ target.addEventListener('transitionend', onFadeTransitionEnd);
5302
+ }
5303
+ cancelFadeAnimation(fadeTarget);
5304
+ target.style.setProperty('transition', 'none', 'important');
5305
+ target.style.setProperty('opacity', '0', 'important');
5306
+ let pageRevealed = false;
5307
+ let released = false;
5308
+ let stopPageListener = () => { };
5309
+ let safetyTimer = null;
5310
+ const update = () => {
5311
+ record.ready = pageRevealed && released;
5312
+ if (record.ready)
5313
+ maybeStartFade(record.fadeTarget);
5314
+ };
5315
+ const release = () => {
5316
+ released = true;
5317
+ if (safetyTimer)
5318
+ clearTimeout(safetyTimer);
5319
+ safetyTimer = null;
5320
+ update();
5321
+ };
5322
+ const record = {
5323
+ fadeTarget,
5324
+ duration,
5325
+ ready: false,
5326
+ stop: () => {
5327
+ stopPageListener();
5328
+ if (safetyTimer)
5329
+ clearTimeout(safetyTimer);
5330
+ safetyTimer = null;
5331
+ fadeReleases.delete(release);
5332
+ if (fades.get(target) === record)
5333
+ fades.delete(target);
5334
+ },
5335
+ };
5336
+ fadeTarget.records.add(record);
5337
+ fades.set(target, record);
5338
+ fadeReleases.add(release);
5339
+ safetyTimer = setTimeout(release, FADE_SAFETY_MS);
5340
+ stopPageListener = subscribe('pageRevealed', () => {
5341
+ pageRevealed = true;
5342
+ update();
5343
+ }, true);
5344
+ if (options.getPageState().pageRevealed) {
5345
+ pageRevealed = true;
5346
+ update();
5347
+ }
5348
+ };
5349
+ const subscribe = (event, handler, once) => {
5350
+ if (!state.active)
5351
+ return () => { };
5352
+ const stopSubscription = options.subscribePage(event, handler, once);
5353
+ if (!state.active) {
5354
+ stopSubscription();
5355
+ return () => { };
5356
+ }
5357
+ const stop = () => {
5358
+ listenerStops.delete(stop);
5359
+ stopSubscription();
5360
+ };
5361
+ listenerStops.add(stop);
5362
+ return stop;
5363
+ };
5364
+ const facade = {
5365
+ getPageState: () => ({ ...options.getPageState() }),
5366
+ getState: () => ({ ...state }),
5367
+ getVariantId: () => state.variantId,
5368
+ onPage: (event, handler) => subscribe(event, handler, false),
5369
+ oncePage: (event, handler) => subscribe(event, handler, true),
5370
+ ready() {
5371
+ if (!isMutable() || state.status !== 'created')
5372
+ return;
5373
+ state.status = 'ready';
5374
+ notify();
5375
+ },
5376
+ takePageAntiflickerOwnership() {
5377
+ if (!isMutable() || state.ownsPageAntiflicker)
5378
+ return;
5379
+ state.ownsPageAntiflicker = true;
5380
+ state.status = 'rendering';
5381
+ notify();
5382
+ },
5383
+ fadeIn(target, durationMs) {
5384
+ if (!isMutable() || !target)
5385
+ return;
5386
+ if (options.fadeIn)
5387
+ options.fadeIn(internal, target, durationMs);
5388
+ else
5389
+ fadeIn(target, durationMs);
5390
+ },
5391
+ ingestApplied(payload) {
5392
+ var _a, _b, _c;
5393
+ if (!isMutable())
5394
+ return;
5395
+ if (ingestCount >= MAX_INGEST_PER_SESSION) {
5396
+ if (!ingestLimitReported) {
5397
+ ingestLimitReported = true;
5398
+ (_a = options.onIngestLimit) === null || _a === void 0 ? void 0 : _a.call(options, internal);
5399
+ }
5400
+ return;
5401
+ }
5402
+ ingestCount += 1;
5403
+ const metadataDropped = (_b = options.ingestApplied) === null || _b === void 0 ? void 0 : _b.call(options, internal, payload);
5404
+ if (metadataDropped && !metadataDropReported) {
5405
+ metadataDropReported = true;
5406
+ (_c = options.onMetadataDropped) === null || _c === void 0 ? void 0 : _c.call(options, internal);
5407
+ }
5408
+ },
5409
+ complete() {
5410
+ if (!isMutable())
5411
+ return;
5412
+ state.status = 'completed';
5413
+ for (const release of Array.from(fadeReleases))
5414
+ release();
5415
+ fadeReleases.clear();
5416
+ options.emitLegacyVariantRendered();
5417
+ notify();
5418
+ },
5419
+ error(error) {
5420
+ var _a;
5421
+ if (!isMutable())
5422
+ return;
5423
+ state.status = 'error';
5424
+ releaseVisuals();
5425
+ (_a = options.onError) === null || _a === void 0 ? void 0 : _a.call(options, internal, error);
5426
+ options.emitLegacyVariantRendered();
5427
+ notify();
5428
+ },
5429
+ cleanup() {
5430
+ if (state.status === 'cleaned')
5431
+ return;
5432
+ state.active = false;
5433
+ state.status = 'cleaned';
5434
+ for (const stop of Array.from(listenerStops))
5435
+ stop();
5436
+ releaseVisuals();
5437
+ notify();
5438
+ },
5439
+ };
5440
+ const internal = {
5441
+ facade: Object.freeze(facade),
5442
+ getState: () => ({ ...state }),
5443
+ isSettledForReveal: () => state.status === 'ready' ||
5444
+ state.status === 'rendering' ||
5445
+ TERMINAL_STATUSES.has(state.status),
5446
+ markPageRevealed() {
5447
+ if (state.pageAntiflickerRemoved)
5448
+ return;
5449
+ state.pageAntiflickerRemoved = true;
5450
+ },
5451
+ invalidate() {
5452
+ facade.cleanup();
5453
+ },
5454
+ };
5455
+ return internal;
5456
+ }
5457
+
5458
+ const DEFAULT_BATCH_TIMEOUT_MS = 1000;
5459
+ function createPageRuntime(options) {
5460
+ var _a, _b, _c;
5461
+ const createId = (_a = options.createId) !== null && _a !== void 0 ? _a : generateUUID;
5462
+ const batchTimeoutMs = (_b = options.batchTimeoutMs) !== null && _b !== void 0 ? _b : DEFAULT_BATCH_TIMEOUT_MS;
5463
+ const revealDeadline = Date.now() + batchTimeoutMs;
5464
+ const timeUntilDeadline = () => Math.max(0, revealDeadline - Date.now());
5465
+ const listeners = new Map();
5466
+ const state = {
5467
+ sdkReady: false,
5468
+ hydrated: (_c = options.initialHydrated) !== null && _c !== void 0 ? _c : false,
5469
+ routeVersion: 0,
5470
+ currentUrl: options.currentUrl,
5471
+ pageRevealed: options.revealCoordinator.getSnapshot().pageRevealed,
5472
+ };
5473
+ let activeBatch = null;
5474
+ let handoffBlockerId = null;
5475
+ let handoffTimeout = null;
5476
+ let destroyed = false;
5477
+ const clearHandoff = () => {
5478
+ handoffBlockerId = null;
5479
+ if (handoffTimeout)
5480
+ clearTimeout(handoffTimeout);
5481
+ handoffTimeout = null;
5482
+ };
5483
+ const callListener = (event, handler, payload) => {
5484
+ var _a;
5485
+ try {
5486
+ handler(payload);
5487
+ }
5488
+ catch (error) {
5489
+ (_a = options.error) === null || _a === void 0 ? void 0 : _a.call(options, 'Variant runtime listener failed', {
5490
+ event,
5491
+ error: describeError(error),
5492
+ });
5493
+ }
5494
+ };
5495
+ const emit = (event, payload) => {
5496
+ const handlers = listeners.get(event);
5497
+ if (!handlers)
5498
+ return;
5499
+ for (const handler of Array.from(handlers)) {
5500
+ if (handlers.has(handler))
5501
+ callListener(event, handler, payload);
5502
+ }
5503
+ };
5504
+ const on = (event, handler) => {
5505
+ var _a;
5506
+ const handlers = (_a = listeners.get(event)) !== null && _a !== void 0 ? _a : new Set();
5507
+ const entry = (payload) => handler(payload);
5508
+ handlers.add(entry);
5509
+ listeners.set(event, handlers);
5510
+ return () => {
5511
+ handlers.delete(entry);
5512
+ if (handlers.size === 0 && listeners.get(event) === handlers) {
5513
+ listeners.delete(event);
5514
+ }
5515
+ };
5516
+ };
5517
+ const stickySatisfied = (event) => {
5518
+ if (event === 'sdkReady')
5519
+ return state.sdkReady;
5520
+ if (event === 'hydrated')
5521
+ return state.hydrated;
5522
+ if (event === 'pageRevealed')
5523
+ return state.pageRevealed;
5524
+ return false;
5525
+ };
5526
+ const stickyPayload = (event) => event === 'pageRevealed'
5527
+ ? {
5528
+ ...state,
5529
+ reason: options.revealCoordinator.getSnapshot().revealedReason,
5530
+ }
5531
+ : { ...state };
5532
+ const once = (event, handler) => {
5533
+ if (stickySatisfied(event)) {
5534
+ callListener(event, handler, stickyPayload(event));
5535
+ return () => { };
5536
+ }
5537
+ let stop = () => { };
5538
+ stop = on(event, (payload) => {
5539
+ stop();
5540
+ handler(payload);
5541
+ });
5542
+ return stop;
5543
+ };
5544
+ const clearBatchTimeout = (batch) => {
5545
+ if (!batch.timeout)
5546
+ return;
5547
+ clearTimeout(batch.timeout);
5548
+ batch.timeout = null;
5549
+ };
5550
+ const revealFromRuntime = (batchId, reveal) => {
5551
+ var _a;
5552
+ try {
5553
+ reveal();
5554
+ }
5555
+ catch (error) {
5556
+ (_a = options.error) === null || _a === void 0 ? void 0 : _a.call(options, 'Variant runtime page reveal failed', {
5557
+ error: describeError(error),
5558
+ ...(batchId ? { batchId } : {}),
5559
+ routeVersion: state.routeVersion,
5560
+ });
5561
+ }
5562
+ };
5563
+ const forceRevealOnTimeout = (batchId) => revealFromRuntime(batchId, () => options.revealCoordinator.forceReveal('batch-timeout'));
5564
+ const releaseBatch = (batch) => {
5565
+ if (batch.released)
5566
+ return;
5567
+ batch.released = true;
5568
+ clearBatchTimeout(batch);
5569
+ revealFromRuntime(batch.batchId, () => options.revealCoordinator.release(batch.batchId, 'batch-settled'));
5570
+ };
5571
+ const warnForSession = (session, message) => {
5572
+ var _a;
5573
+ const sessionState = session.getState();
5574
+ (_a = options.warn) === null || _a === void 0 ? void 0 : _a.call(options, message, {
5575
+ variantId: sessionState.variantId,
5576
+ batchId: sessionState.batchId,
5577
+ });
5578
+ };
5579
+ const maybeReleaseBatch = (batch) => {
5580
+ if (activeBatch !== batch ||
5581
+ batch.released ||
5582
+ !batch.closed ||
5583
+ batch.sessions.size < batch.expectedSessionCount)
5584
+ return;
5585
+ if (Array.from(batch.sessions.values()).every((session) => session.isSettledForReveal())) {
5586
+ releaseBatch(batch);
5587
+ }
5588
+ };
5589
+ const warnUnsettled = (batch, reason) => {
5590
+ var _a;
5591
+ (_a = options.warn) === null || _a === void 0 ? void 0 : _a.call(options, 'Variant runtime batch timed out', {
5592
+ batchId: batch.batchId,
5593
+ routeVersion: batch.routeVersion,
5594
+ reason,
5595
+ expectedSessionCount: batch.expectedSessionCount,
5596
+ createdSessionCount: batch.sessions.size,
5597
+ unsettledVariantIds: Array.from(batch.sessions.values())
5598
+ .filter((session) => !session.isSettledForReveal())
5599
+ .map((session) => session.getState().variantId),
5600
+ });
5601
+ };
5602
+ const invalidateBatch = (batchId) => {
5603
+ if (!activeBatch || activeBatch.batchId !== batchId)
5604
+ return;
5605
+ const batch = activeBatch;
5606
+ activeBatch = null;
5607
+ clearBatchTimeout(batch);
5608
+ for (const session of batch.sessions.values())
5609
+ session.invalidate();
5610
+ };
5611
+ const coordinatorStop = options.revealCoordinator.onRevealed((reason) => {
5612
+ if (state.pageRevealed)
5613
+ return;
5614
+ if (activeBatch && !activeBatch.released && reason !== 'batch-settled') {
5615
+ warnUnsettled(activeBatch, reason);
5616
+ }
5617
+ state.pageRevealed = true;
5618
+ clearHandoff();
5619
+ if (activeBatch) {
5620
+ activeBatch.released = true;
5621
+ clearBatchTimeout(activeBatch);
5622
+ for (const session of activeBatch.sessions.values()) {
5623
+ session.markPageRevealed();
5624
+ }
5625
+ }
5626
+ emit('pageRevealed', { ...state, reason });
5627
+ });
5628
+ const runtime = {
5629
+ getState: () => ({ ...state }),
5630
+ on,
5631
+ once,
5632
+ publishSdkReady() {
5633
+ if (state.sdkReady)
5634
+ return;
5635
+ state.sdkReady = true;
5636
+ emit('sdkReady', { ...state });
5637
+ },
5638
+ publishHydrated() {
5639
+ if (state.hydrated)
5640
+ return;
5641
+ state.hydrated = true;
5642
+ emit('hydrated', { ...state });
5643
+ },
5644
+ settleRoute() {
5645
+ if (destroyed)
5646
+ return;
5647
+ if (handoffBlockerId) {
5648
+ const batchId = handoffBlockerId;
5649
+ clearHandoff();
5650
+ revealFromRuntime(batchId, () => options.revealCoordinator.release(batchId, 'no-variants'));
5651
+ return;
5652
+ }
5653
+ if (!state.pageRevealed && !activeBatch) {
5654
+ revealFromRuntime(null, () => options.revealCoordinator.requestReveal('no-variants'));
5655
+ }
5656
+ },
5657
+ updateUrl(currentUrl) {
5658
+ if (!destroyed)
5659
+ state.currentUrl = currentUrl;
5660
+ },
5661
+ changeRoute(currentUrl) {
5662
+ if (destroyed)
5663
+ return;
5664
+ if (activeBatch) {
5665
+ const { batchId } = activeBatch;
5666
+ invalidateBatch(batchId);
5667
+ if (!state.pageRevealed) {
5668
+ handoffBlockerId = batchId;
5669
+ const abandonedRouteVersion = state.routeVersion;
5670
+ if (!handoffTimeout) {
5671
+ handoffTimeout = setTimeout(() => {
5672
+ var _a;
5673
+ if (!handoffBlockerId)
5674
+ return;
5675
+ const handedOverBatchId = handoffBlockerId;
5676
+ (_a = options.warn) === null || _a === void 0 ? void 0 : _a.call(options, 'Variant runtime route handoff timed out', {
5677
+ routeVersion: abandonedRouteVersion,
5678
+ });
5679
+ clearHandoff();
5680
+ forceRevealOnTimeout(handedOverBatchId);
5681
+ }, timeUntilDeadline());
5682
+ }
5683
+ }
5684
+ }
5685
+ state.routeVersion += 1;
5686
+ state.currentUrl = currentUrl;
5687
+ },
5688
+ beginBatch(input) {
5689
+ if (destroyed || input.expectedSessionCount === 0)
5690
+ return null;
5691
+ if (activeBatch)
5692
+ invalidateBatch(activeBatch.batchId);
5693
+ clearHandoff();
5694
+ const batch = {
5695
+ batchId: createId(),
5696
+ routeVersion: state.routeVersion,
5697
+ expectedSessionCount: input.expectedSessionCount,
5698
+ closed: false,
5699
+ released: state.pageRevealed,
5700
+ sessions: new Map(),
5701
+ timeout: null,
5702
+ };
5703
+ activeBatch = batch;
5704
+ if (!state.pageRevealed &&
5705
+ options.revealCoordinator.block(batch.batchId)) {
5706
+ batch.timeout = setTimeout(() => {
5707
+ if (activeBatch !== batch || batch.released)
5708
+ return;
5709
+ warnUnsettled(batch, 'batch-timeout');
5710
+ batch.released = true;
5711
+ forceRevealOnTimeout(batch.batchId);
5712
+ }, timeUntilDeadline());
5713
+ }
5714
+ return { batchId: batch.batchId, routeVersion: batch.routeVersion };
5715
+ },
5716
+ createVariantSession(input) {
5717
+ var _a;
5718
+ const batch = activeBatch;
5719
+ if (!batch ||
5720
+ batch.batchId !== input.batchId ||
5721
+ batch.routeVersion !== state.routeVersion ||
5722
+ (batch.released && !state.pageRevealed))
5723
+ return null;
5724
+ const session = createVariantSession({
5725
+ sessionId: createId(),
5726
+ variantId: input.variantId,
5727
+ batchId: batch.batchId,
5728
+ routeVersion: batch.routeVersion,
5729
+ getPageState: () => ({ ...state }),
5730
+ subscribePage: (event, handler, onlyOnce) => {
5731
+ if (onlyOnce)
5732
+ return once(event, handler);
5733
+ if (stickySatisfied(event)) {
5734
+ callListener(event, handler, stickyPayload(event));
5735
+ }
5736
+ return on(event, handler);
5737
+ },
5738
+ onStateChange: () => maybeReleaseBatch(batch),
5739
+ emitLegacyVariantRendered: (_a = options.emitLegacyVariantRendered) !== null && _a !== void 0 ? _a : (() => { }),
5740
+ ingestApplied: (currentSession, payload) => {
5741
+ var _a, _b;
5742
+ return (_b = (_a = options.onVariantApplied) === null || _a === void 0 ? void 0 : _a.call(options, { ...state }, currentSession.getState(), payload)) !== null && _b !== void 0 ? _b : false;
5743
+ },
5744
+ onIngestLimit: (currentSession) => warnForSession(currentSession, 'Variant ingestApplied limit reached'),
5745
+ onMetadataDropped: (currentSession) => warnForSession(currentSession, 'Variant ingestApplied metadata dropped'),
5746
+ onError: (currentSession, error) => {
5747
+ var _a;
5748
+ const sessionState = currentSession.getState();
5749
+ (_a = options.error) === null || _a === void 0 ? void 0 : _a.call(options, 'Variant runtime session failed', {
5750
+ error: describeError(error),
5751
+ variantId: sessionState.variantId,
5752
+ sessionId: sessionState.sessionId,
5753
+ batchId: sessionState.batchId,
5754
+ routeVersion: sessionState.routeVersion,
5755
+ currentUrl: state.currentUrl,
5756
+ });
5757
+ },
5758
+ });
5759
+ if (state.pageRevealed)
5760
+ session.markPageRevealed();
5761
+ batch.sessions.set(session.getState().sessionId, session);
5762
+ maybeReleaseBatch(batch);
5763
+ return session;
5764
+ },
5765
+ closeBatch(batchId) {
5766
+ if (!activeBatch || activeBatch.batchId !== batchId)
5767
+ return;
5768
+ activeBatch.closed = true;
5769
+ maybeReleaseBatch(activeBatch);
5770
+ },
5771
+ invalidateBatch,
5772
+ invalidateAllSessions() {
5773
+ if (activeBatch)
5774
+ invalidateBatch(activeBatch.batchId);
5775
+ },
5776
+ getDebugSnapshot() {
5777
+ return {
5778
+ page: { ...state },
5779
+ activeBatch: activeBatch
5780
+ ? {
5781
+ batchId: activeBatch.batchId,
5782
+ routeVersion: activeBatch.routeVersion,
5783
+ expectedSessionCount: activeBatch.expectedSessionCount,
5784
+ closed: activeBatch.closed,
5785
+ released: activeBatch.released,
5786
+ sessionStates: Array.from(activeBatch.sessions.values()).map((session) => session.getState()),
5787
+ }
5788
+ : null,
5789
+ };
5790
+ },
5791
+ isDestroyed: () => destroyed,
5792
+ destroy() {
5793
+ destroyed = true;
5794
+ const blockerIds = [handoffBlockerId, activeBatch === null || activeBatch === void 0 ? void 0 : activeBatch.batchId];
5795
+ if (activeBatch)
5796
+ invalidateBatch(activeBatch.batchId);
5797
+ clearHandoff();
5798
+ for (const blockerId of blockerIds) {
5799
+ if (blockerId)
5800
+ options.revealCoordinator.unblock(blockerId);
5801
+ }
5802
+ coordinatorStop();
5803
+ listeners.clear();
5804
+ },
5805
+ };
5806
+ return runtime;
5807
+ }
5808
+
5809
+ const MAX_TARGET_SELECTORS = 20;
5810
+ const MAX_SELECTOR_LENGTH = 500;
5811
+ const MAX_METADATA_JSON_BYTES = 4096;
5812
+ const MAX_METADATA_VISITS = 20000;
5813
+ const MAX_SELECTOR_ENTRIES_SCANNED = 200;
5814
+ function utf8ByteLength(value) {
5815
+ let bytes = 0;
5816
+ for (let i = 0; i < value.length; i++) {
5817
+ const code = value.charCodeAt(i);
5818
+ if (code < 0x80)
5819
+ bytes += 1;
5820
+ else if (code < 0x800)
5821
+ bytes += 2;
5822
+ else if (code >= 0xd800 && code <= 0xdbff) {
5823
+ const next = value.charCodeAt(i + 1);
5824
+ if (next >= 0xdc00 && next <= 0xdfff) {
5825
+ bytes += 4;
5826
+ i++;
5827
+ }
5828
+ else {
5829
+ bytes += 3;
5830
+ }
5831
+ }
5832
+ else
5833
+ bytes += 3;
5834
+ }
5835
+ return bytes;
5836
+ }
5837
+ function isBoxedString(value) {
5838
+ if (!value || typeof value !== 'object')
5839
+ return false;
5840
+ try {
5841
+ String.prototype.valueOf.call(value);
5842
+ return true;
5843
+ }
5844
+ catch (_a) {
5845
+ return false;
5846
+ }
5847
+ }
5848
+ function sanitizeMetadata(payload) {
5849
+ try {
5850
+ const rawMetadata = payload.metadata;
5851
+ if (rawMetadata === undefined || rawMetadata === null)
5852
+ return {};
5853
+ if (typeof rawMetadata !== 'object' || Array.isArray(rawMetadata)) {
5854
+ return null;
5855
+ }
5856
+ let minimumLength = 0;
5857
+ let visits = 0;
5858
+ const json = JSON.stringify(rawMetadata, function (key, value) {
5859
+ visits += 1;
5860
+ if (visits > MAX_METADATA_VISITS) {
5861
+ throw new Error('metadata too large');
5862
+ }
5863
+ const isObjectProperty = visits > 1 && !Array.isArray(this);
5864
+ if (isObjectProperty &&
5865
+ (value === undefined ||
5866
+ typeof value === 'function' ||
5867
+ typeof value === 'symbol')) {
5868
+ return value;
5869
+ }
5870
+ if (isObjectProperty)
5871
+ minimumLength += key.length + 3;
5872
+ const written = isBoxedString(value) ? String(value) : value;
5873
+ minimumLength += typeof written === 'string' ? written.length + 2 : 1;
5874
+ if (minimumLength > MAX_METADATA_JSON_BYTES) {
5875
+ throw new Error('metadata too large');
5876
+ }
5877
+ return written;
5878
+ });
5879
+ if (!json ||
5880
+ json.length > MAX_METADATA_JSON_BYTES ||
5881
+ utf8ByteLength(json) > MAX_METADATA_JSON_BYTES) {
5882
+ return null;
5883
+ }
5884
+ const copy = JSON.parse(json);
5885
+ return copy && typeof copy === 'object' && !Array.isArray(copy)
5886
+ ? copy
5887
+ : null;
5888
+ }
5889
+ catch (_a) {
5890
+ return null;
5891
+ }
5892
+ }
5893
+ function sanitizeSelectors(payload) {
5894
+ try {
5895
+ const selectors = payload.targetSelectors;
5896
+ if (!Array.isArray(selectors))
5897
+ return [];
5898
+ const kept = [];
5899
+ const scanned = Math.min(selectors.length, MAX_SELECTOR_ENTRIES_SCANNED);
5900
+ for (let i = 0; i < scanned; i++) {
5901
+ let selector;
5902
+ try {
5903
+ selector = selectors[i];
5904
+ }
5905
+ catch (_a) {
5906
+ continue;
5907
+ }
5908
+ if (typeof selector === 'string' &&
5909
+ selector.length > 0 &&
5910
+ selector.length <= MAX_SELECTOR_LENGTH) {
5911
+ kept.push(selector);
5912
+ if (kept.length === MAX_TARGET_SELECTORS)
5913
+ break;
5914
+ }
5915
+ }
5916
+ return kept;
5917
+ }
5918
+ catch (_b) {
5919
+ return [];
5920
+ }
5921
+ }
5922
+ function sanitizePayload(payloadInput) {
5923
+ if (!payloadInput ||
5924
+ typeof payloadInput !== 'object' ||
5925
+ Array.isArray(payloadInput)) {
5926
+ const given = payloadInput !== undefined && payloadInput !== null;
5927
+ return { metadata: {}, metadataDropped: given, targetSelectors: [] };
5928
+ }
5929
+ const payload = payloadInput;
5930
+ const metadata = sanitizeMetadata(payload);
5931
+ return {
5932
+ metadata: metadata || {},
5933
+ metadataDropped: metadata === null,
5934
+ targetSelectors: sanitizeSelectors(payload),
5935
+ };
5936
+ }
5937
+ function ingestVariantApplied(coframe, pageState, sessionState, payloadInput) {
5938
+ const { metadata, metadataDropped, targetSelectors } = sanitizePayload(payloadInput);
5939
+ const personalizationHeaders = coframe.personalizationHeaders;
5940
+ const event = {
5941
+ type: 'variant_applied',
5942
+ project_id: coframe.projectId || '',
5943
+ session_id: coframe.sessionId || '',
5944
+ time: new Date(),
5945
+ url: getCurrentPageUrl(),
5946
+ user_token: coframe.userToken || '',
5947
+ org_id: coframe.orgId || '',
5948
+ variant_id: sessionState.variantId,
5949
+ content: {
5950
+ metadata: {
5951
+ batch_id: sessionState.batchId,
5952
+ route_version: pageState.routeVersion,
5953
+ current_url: pageState.currentUrl,
5954
+ ...(personalizationHeaders
5955
+ ? { personalization_headers: { ...personalizationHeaders } }
5956
+ : {}),
5957
+ ...(targetSelectors.length
5958
+ ? { target_selectors: targetSelectors }
5959
+ : {}),
5960
+ ...(Object.keys(metadata).length ? { custom_data: metadata } : {}),
5961
+ },
5962
+ },
5963
+ };
5964
+ try {
5965
+ document.dispatchEvent(new CustomEvent('coframe:variant-applied', {
5966
+ detail: {
5967
+ ...event,
5968
+ time: new Date(event.time.getTime()),
5969
+ content: {
5970
+ metadata: JSON.parse(JSON.stringify(event.content.metadata)),
5971
+ },
5972
+ },
5973
+ }));
5974
+ }
5975
+ catch (_a) {
5976
+ }
5977
+ if (coframe.projectId)
5978
+ queueEventForBatchedSending(coframe, event);
5979
+ return metadataDropped;
5980
+ }
5981
+
4746
5982
  function push(cmd) {
4747
5983
  if (typeof window === 'undefined')
4748
5984
  return;
@@ -4790,34 +6026,81 @@ const Coframe = {
4790
6026
  },
4791
6027
  };
4792
6028
  Coframe.logger = createLogger(() => Coframe);
4793
- async function init(options) {
6029
+ const revealCoordinator = createRevealCoordinator(revealPage, logRevealListenerError(() => Coframe.logger));
6030
+ let activeRuntime = null;
6031
+ function clearActiveRuntime() {
6032
+ activeRuntime === null || activeRuntime === void 0 ? void 0 : activeRuntime.destroy();
6033
+ activeRuntime = null;
6034
+ delete Coframe.runtime;
6035
+ }
6036
+ function installRuntime(config) {
6037
+ const batchTimeoutMs = config.timeoutMs
6038
+ ? Math.min(config.timeoutMs, TIMEOUT_MAX_MS)
6039
+ : undefined;
6040
+ clearActiveRuntime();
6041
+ if (!isVariantRuntimeLifecycleEnabled(config))
6042
+ return undefined;
6043
+ const runtime = createPageRuntime({
6044
+ revealCoordinator,
6045
+ currentUrl: getCurrentPageUrl(),
6046
+ initialHydrated: !config.waitForHydration,
6047
+ batchTimeoutMs,
6048
+ warn: (message, metadata) => Coframe.logger.warn(message, 'variant_runtime_warning', metadata),
6049
+ error: (message, metadata) => Coframe.logger.error(message, 'variant_runtime_error', metadata),
6050
+ emitLegacyVariantRendered: variantRendered,
6051
+ onVariantApplied: (pageState, sessionState, payload) => ingestVariantApplied(Coframe, pageState, sessionState, payload),
6052
+ });
6053
+ activeRuntime = runtime;
6054
+ Coframe.runtime = Object.freeze({
6055
+ getSnapshot: () => runtime.getDebugSnapshot(),
6056
+ });
6057
+ runtime.publishSdkReady();
6058
+ return runtime;
6059
+ }
6060
+ async function initialize(options, context = {}) {
4794
6061
  if (typeof window !== 'undefined' &&
4795
6062
  /cf_disable/.test(window.location.search)) {
4796
6063
  console.warn('[Coframe] Disabled via URL parameter');
4797
- revealPage();
6064
+ revealCoordinator.forceReveal('graceful-exit');
4798
6065
  return;
4799
6066
  }
4800
6067
  if (options)
4801
6068
  injectOptions(options);
4802
6069
  const gracefulExit = (msg) => {
4803
6070
  console.warn(`[Coframe] ${msg}`);
4804
- revealPage();
6071
+ revealCoordinator.forceReveal('graceful-exit');
4805
6072
  return;
4806
6073
  };
4807
- const configResult = await resolveConfig(Coframe, revealPage);
6074
+ const configResult = await resolveConfig(Coframe, revealCoordinator);
4808
6075
  if (!configResult)
4809
6076
  return;
6077
+ const runtime = installRuntime(configResult.config);
6078
+ context.runtime = runtime;
4810
6079
  loadLauncherIfEnabled(configResult.config.enableDebug, configResult.config.nonce);
4811
- const projectResult = await resolveProject(Coframe, configResult.previewVariantId, configResult.isSessionVariant, configResult.config);
4812
- if (!projectResult)
6080
+ const projectResult = await resolveProject(Coframe, configResult.previewVariantId, configResult.isSessionVariant, configResult.config, () => !(runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed()));
6081
+ if (runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed())
4813
6082
  return;
6083
+ if (!projectResult) {
6084
+ revealCoordinator.forceReveal('graceful-exit');
6085
+ return;
6086
+ }
4814
6087
  Coframe.orgId = projectResult.project.org_id || '';
4815
6088
  Coframe.personalizationHeaders = projectResult.personalizationHeaders;
4816
- const identity = await resolveIdentity(configResult.config, Coframe, configResult.timeoutMs, gracefulExit, configResult.previewVariantId);
6089
+ const identity = await resolveIdentity(configResult.config, Coframe, configResult.timeoutMs, (msg) => {
6090
+ if (!(runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed()))
6091
+ gracefulExit(msg);
6092
+ }, configResult.previewVariantId);
4817
6093
  if (!identity)
4818
6094
  return;
6095
+ if (runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed())
6096
+ return;
4819
6097
  const analytics = setupTracking(Coframe, projectResult, configResult.previewVariantId, configResult.executionStartTime, configResult.config.nonce, configResult.config.waitForCookiesEnabled, identity.onConsentChange);
4820
- await modifyPage(Coframe, configResult, projectResult, analytics);
6098
+ await modifyPage(Coframe, configResult, projectResult, analytics, {
6099
+ revealCoordinator,
6100
+ runtime,
6101
+ });
6102
+ if (runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed())
6103
+ return;
4821
6104
  try {
4822
6105
  startReplayIfEnabled(Coframe, projectResult.project, configResult.config.nonce);
4823
6106
  }
@@ -4825,6 +6108,19 @@ async function init(options) {
4825
6108
  console.warn('[Coframe] Failed to start session replay', error);
4826
6109
  }
4827
6110
  }
6111
+ async function init(options) {
6112
+ var _a;
6113
+ const context = {};
6114
+ try {
6115
+ await initialize(options, context);
6116
+ }
6117
+ catch (error) {
6118
+ if (!((_a = context.runtime) === null || _a === void 0 ? void 0 : _a.isDestroyed())) {
6119
+ revealCoordinator.forceReveal('init-error');
6120
+ }
6121
+ throw error;
6122
+ }
6123
+ }
4828
6124
  Coframe.debug = {
4829
6125
  getSelector,
4830
6126
  getElement,