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