@absolutejs/absolute 0.20.0-beta.58 → 0.20.0-beta.59

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.
@@ -458,7 +458,11 @@ var createAbsoluteMobilePageRequest = (manifest, path, options = {}) => {
458
458
  headers.set(MOBILE_PAGE_REQUEST_HEADERS.pageId, page.pageId);
459
459
  headers.set(MOBILE_PAGE_REQUEST_HEADERS.protocol, String(ABSOLUTE_MOBILE_PAGE_PROTOCOL_VERSION));
460
460
  headers.set(MOBILE_PAGE_REQUEST_HEADERS.runtime, manifest.runtime);
461
- return new Request(url, { headers, method: "GET" });
461
+ return new Request(url, {
462
+ headers,
463
+ method: "GET",
464
+ ...options.signal ? { signal: options.signal } : {}
465
+ });
462
466
  };
463
467
  var fetchAbsoluteMobilePage = async (manifest, path, options = {}) => {
464
468
  const request = createAbsoluteMobilePageRequest(manifest, path, options);
@@ -1143,9 +1147,268 @@ var installAbsoluteMobileAdaptiveShell = async (devices = defaultDevices) => {
1143
1147
  };
1144
1148
  };
1145
1149
 
1150
+ // src/mobile/navigationLifecycle.ts
1151
+ var isAbortError = (error) => error instanceof DOMException ? error.name === "AbortError" : error instanceof Error && error.name === "AbortError";
1152
+ var cancelledResult = { kind: "cancelled" };
1153
+ var committedResult = { kind: "committed" };
1154
+ var failedResult = (error, phase) => ({ error, kind: "failed", phase });
1155
+ var createAbsoluteMobileNavigationCoordinator = (options) => {
1156
+ let activeLoad;
1157
+ let commitQueue = Promise.resolve();
1158
+ let disposed = false;
1159
+ let generation = 0;
1160
+ let lifecyclePhase = "idle";
1161
+ const navigate = async (request) => {
1162
+ if (disposed)
1163
+ return cancelledResult;
1164
+ const ownGeneration = generation += 1;
1165
+ activeLoad?.abort();
1166
+ const controller = new AbortController;
1167
+ activeLoad = controller;
1168
+ lifecyclePhase = "loading";
1169
+ options.onStart?.(request);
1170
+ let payload;
1171
+ try {
1172
+ payload = await options.load(request, controller.signal);
1173
+ } catch (error) {
1174
+ if (controller.signal.aborted || isAbortError(error) || ownGeneration !== generation || disposed) {
1175
+ if (ownGeneration === generation)
1176
+ lifecyclePhase = "idle";
1177
+ return cancelledResult;
1178
+ }
1179
+ lifecyclePhase = "idle";
1180
+ options.onFailure?.(error, "load", request);
1181
+ return failedResult(error, "load");
1182
+ }
1183
+ if (ownGeneration !== generation || disposed)
1184
+ return cancelledResult;
1185
+ if (activeLoad === controller)
1186
+ activeLoad = undefined;
1187
+ lifecyclePhase = "queued";
1188
+ const execute = async () => {
1189
+ if (ownGeneration !== generation || disposed) {
1190
+ if (ownGeneration === generation)
1191
+ lifecyclePhase = "idle";
1192
+ return cancelledResult;
1193
+ }
1194
+ lifecyclePhase = "committing";
1195
+ try {
1196
+ await options.commit(payload, request);
1197
+ options.onCommit?.(request);
1198
+ } catch (error) {
1199
+ if (ownGeneration === generation) {
1200
+ lifecyclePhase = "idle";
1201
+ options.onFailure?.(error, "commit", request);
1202
+ }
1203
+ return failedResult(error, "commit");
1204
+ }
1205
+ if (ownGeneration !== generation || disposed) {
1206
+ return cancelledResult;
1207
+ }
1208
+ lifecyclePhase = "idle";
1209
+ options.onSuccess?.(request);
1210
+ return committedResult;
1211
+ };
1212
+ const result = commitQueue.then(execute, execute);
1213
+ commitQueue = result.then(() => {
1214
+ return;
1215
+ }, () => {
1216
+ return;
1217
+ });
1218
+ return result;
1219
+ };
1220
+ return {
1221
+ cancelPending: () => {
1222
+ if (disposed || lifecyclePhase !== "loading" && lifecyclePhase !== "queued")
1223
+ return false;
1224
+ generation += 1;
1225
+ activeLoad?.abort();
1226
+ activeLoad = undefined;
1227
+ lifecyclePhase = "idle";
1228
+ return true;
1229
+ },
1230
+ dispose: () => {
1231
+ if (disposed)
1232
+ return;
1233
+ disposed = true;
1234
+ generation += 1;
1235
+ activeLoad?.abort();
1236
+ activeLoad = undefined;
1237
+ lifecyclePhase = "idle";
1238
+ },
1239
+ navigate: (request) => navigate(request),
1240
+ phase: () => lifecyclePhase
1241
+ };
1242
+ };
1243
+
1244
+ // src/mobile/navigationState.ts
1245
+ var CONTROL_SELECTOR = 'input, textarea, select, details, [contenteditable="true"]';
1246
+ var FOCUS_SELECTOR = '[data-absolute-navigation-focus], button, a[href], input, textarea, select, [tabindex]:not([tabindex="-1"])';
1247
+ var SCROLL_SELECTOR = "[data-absolute-app-main], [data-absolute-scroll-restoration]";
1248
+ var SENSITIVE_AUTOCOMPLETE = new Set([
1249
+ "cc-csc",
1250
+ "cc-number",
1251
+ "current-password",
1252
+ "new-password",
1253
+ "one-time-code"
1254
+ ]);
1255
+ var locatorFor = (element, elements) => {
1256
+ if (element.id)
1257
+ return { kind: "id", value: element.id };
1258
+ const name = element.getAttribute("name");
1259
+ if (name) {
1260
+ const matching = elements.filter((candidate) => candidate.getAttribute("name") === name);
1261
+ return { index: matching.indexOf(element), kind: "name", value: name };
1262
+ }
1263
+ return { index: elements.indexOf(element), kind: "index" };
1264
+ };
1265
+ var resolveLocator = (locator, selector) => {
1266
+ if (locator.kind === "id") {
1267
+ return document.getElementById(locator.value) ?? undefined;
1268
+ }
1269
+ if (locator.kind === "name") {
1270
+ return [...document.querySelectorAll(selector)].filter((element) => element.getAttribute("name") === locator.value)[locator.index];
1271
+ }
1272
+ return document.querySelectorAll(selector)[locator.index];
1273
+ };
1274
+ var shouldCaptureControl = (element) => {
1275
+ if (element.closest('[data-absolute-navigation-preserve="off"]'))
1276
+ return false;
1277
+ if (!(element instanceof HTMLInputElement))
1278
+ return true;
1279
+ if (element.type === "file" || element.type === "hidden" || element.type === "password")
1280
+ return false;
1281
+ return !SENSITIVE_AUTOCOMPLETE.has(element.autocomplete.toLowerCase());
1282
+ };
1283
+ var captureControl = (element, locator) => {
1284
+ const snapshot = {
1285
+ locator,
1286
+ tag: element.tagName.toLowerCase()
1287
+ };
1288
+ if (element instanceof HTMLInputElement) {
1289
+ if (element.type === "checkbox" || element.type === "radio") {
1290
+ snapshot.checked = element.checked;
1291
+ } else {
1292
+ snapshot.value = element.value;
1293
+ if (element.selectionStart !== null)
1294
+ snapshot.selectionStart = element.selectionStart;
1295
+ if (element.selectionEnd !== null)
1296
+ snapshot.selectionEnd = element.selectionEnd;
1297
+ }
1298
+ } else if (element instanceof HTMLTextAreaElement) {
1299
+ snapshot.value = element.value;
1300
+ snapshot.selectionStart = element.selectionStart;
1301
+ snapshot.selectionEnd = element.selectionEnd;
1302
+ } else if (element instanceof HTMLSelectElement) {
1303
+ snapshot.values = [...element.selectedOptions].map(({ value }) => value);
1304
+ } else if (element instanceof HTMLDetailsElement) {
1305
+ snapshot.open = element.open;
1306
+ } else if (element.getAttribute("contenteditable") === "true") {
1307
+ snapshot.value = element.textContent ?? "";
1308
+ }
1309
+ return snapshot;
1310
+ };
1311
+ var restoreControl = (snapshot) => {
1312
+ const element = resolveLocator(snapshot.locator, CONTROL_SELECTOR);
1313
+ if (!element || element.tagName.toLowerCase() !== snapshot.tag)
1314
+ return;
1315
+ if (element instanceof HTMLInputElement) {
1316
+ if (snapshot.checked !== undefined)
1317
+ element.checked = snapshot.checked;
1318
+ else if (snapshot.value !== undefined)
1319
+ element.value = snapshot.value;
1320
+ if (snapshot.selectionStart !== undefined && snapshot.selectionEnd !== undefined) {
1321
+ try {
1322
+ element.setSelectionRange(snapshot.selectionStart, snapshot.selectionEnd);
1323
+ } catch {}
1324
+ }
1325
+ } else if (element instanceof HTMLTextAreaElement) {
1326
+ if (snapshot.value !== undefined)
1327
+ element.value = snapshot.value;
1328
+ if (snapshot.selectionStart !== undefined && snapshot.selectionEnd !== undefined)
1329
+ element.setSelectionRange(snapshot.selectionStart, snapshot.selectionEnd);
1330
+ } else if (element instanceof HTMLSelectElement && snapshot.values) {
1331
+ for (const option of element.options) {
1332
+ option.selected = snapshot.values.includes(option.value);
1333
+ }
1334
+ } else if (element instanceof HTMLDetailsElement && snapshot.open !== undefined) {
1335
+ element.open = snapshot.open;
1336
+ } else if (element.getAttribute("contenteditable") === "true" && snapshot.value !== undefined) {
1337
+ element.textContent = snapshot.value;
1338
+ }
1339
+ };
1340
+ var focusElement = (element) => {
1341
+ if (!(element instanceof HTMLElement))
1342
+ return false;
1343
+ if (!element.matches(FOCUS_SELECTOR))
1344
+ element.tabIndex = -1;
1345
+ element.focus({ preventScroll: true });
1346
+ return document.activeElement === element;
1347
+ };
1348
+ var createAbsoluteMobileHistoryEntry = (path, index, entryId = crypto.randomUUID()) => ({
1349
+ absoluteMobile: true,
1350
+ entryId,
1351
+ index,
1352
+ path
1353
+ });
1354
+ var readAbsoluteMobileHistoryEntry = (value) => {
1355
+ if (typeof value !== "object" || value === null)
1356
+ return;
1357
+ if (Reflect.get(value, "absoluteMobile") !== true || typeof Reflect.get(value, "entryId") !== "string" || !Number.isSafeInteger(Reflect.get(value, "index")) || typeof Reflect.get(value, "path") !== "string")
1358
+ return;
1359
+ return {
1360
+ absoluteMobile: true,
1361
+ entryId: Reflect.get(value, "entryId"),
1362
+ index: Reflect.get(value, "index"),
1363
+ path: Reflect.get(value, "path")
1364
+ };
1365
+ };
1366
+ var captureAbsoluteMobileDocumentState = () => {
1367
+ const controls = [
1368
+ ...document.querySelectorAll(CONTROL_SELECTOR)
1369
+ ].filter(shouldCaptureControl);
1370
+ const focusable = [...document.querySelectorAll(FOCUS_SELECTOR)];
1371
+ const scrollable = [...document.querySelectorAll(SCROLL_SELECTOR)];
1372
+ const active = document.activeElement;
1373
+ return {
1374
+ controls: controls.map((element) => captureControl(element, locatorFor(element, controls))),
1375
+ ...active instanceof Element && focusable.includes(active) ? { focus: locatorFor(active, focusable) } : {},
1376
+ scroll: scrollable.map((element) => ({
1377
+ left: element.scrollLeft,
1378
+ locator: locatorFor(element, scrollable),
1379
+ top: element.scrollTop
1380
+ })),
1381
+ window: { x: window.scrollX, y: window.scrollY }
1382
+ };
1383
+ };
1384
+ var restoreAbsoluteMobileDocumentState = (snapshot) => {
1385
+ for (const control of snapshot.controls)
1386
+ restoreControl(control);
1387
+ const focus = snapshot.focus ? resolveLocator(snapshot.focus, FOCUS_SELECTOR) : undefined;
1388
+ focusElement(focus);
1389
+ for (const scroll of snapshot.scroll) {
1390
+ const element = resolveLocator(scroll.locator, SCROLL_SELECTOR);
1391
+ if (element)
1392
+ element.scrollTo(scroll.left, scroll.top);
1393
+ }
1394
+ window.scrollTo(snapshot.window.x, snapshot.window.y);
1395
+ };
1396
+ var resetAbsoluteMobileDocumentState = () => {
1397
+ for (const element of document.querySelectorAll(SCROLL_SELECTOR)) {
1398
+ element.scrollTo(0, 0);
1399
+ }
1400
+ window.scrollTo(0, 0);
1401
+ const autofocus = document.querySelector("[autofocus]");
1402
+ if (autofocus && focusElement(autofocus))
1403
+ return;
1404
+ const target = ["[data-absolute-navigation-focus]", "main h1", "h1", "main"].map((selector) => document.querySelector(selector)).find((element) => element !== null);
1405
+ focusElement(target ?? undefined);
1406
+ };
1407
+
1146
1408
  // src/mobile/shellBootstrap.ts
1147
1409
  var MANIFEST_PATH = "./absolute-mobile-manifest.json";
1148
1410
  var STATUS_ID = "absolute-mobile-status";
1411
+ var NAVIGATION_ERROR_ID = "absolute-mobile-navigation-error";
1149
1412
  var initialNavigationPath = (entry) => {
1150
1413
  const expoPath = new URLSearchParams(location.search).get("absolutePath");
1151
1414
  if (expoPath?.startsWith("/") && !expoPath.startsWith("//"))
@@ -1157,33 +1420,19 @@ var currentNavigationPath = () => {
1157
1420
  const expoPath = new URLSearchParams(location.search).get("absolutePath");
1158
1421
  return expoPath?.startsWith("/") && !expoPath.startsWith("//") ? expoPath : `${location.pathname}${location.search}${location.hash}`;
1159
1422
  };
1160
- var pushNavigationHistory = (path) => {
1423
+ var historyUrl = (path) => {
1161
1424
  if (!Reflect.get(globalThis, "__absoluteExpoBridge")) {
1162
- history.pushState({ absoluteMobile: true }, "", path);
1163
- return;
1425
+ return path;
1164
1426
  }
1165
1427
  const url = new URL(location.href);
1166
1428
  url.search = "";
1167
1429
  url.hash = "";
1168
1430
  url.searchParams.set("absolutePath", path);
1169
- history.pushState({ absoluteMobile: true }, "", url.href);
1170
- const bridge = Reflect.get(globalThis, "__absoluteExpoBridge");
1171
- if (typeof bridge === "object" && bridge !== null && typeof Reflect.get(bridge, "setPath") === "function") {
1172
- Reflect.get(bridge, "setPath").call(bridge, path);
1173
- }
1431
+ return url.href;
1174
1432
  };
1175
- var replaceNavigationHistory = (path) => {
1176
- const state = { absoluteMobile: true };
1177
- if (!Reflect.get(globalThis, "__absoluteExpoBridge")) {
1178
- history.replaceState(state, "", path);
1179
- return;
1180
- }
1181
- const url = new URL(location.href);
1182
- url.search = "";
1183
- url.hash = "";
1184
- url.searchParams.set("absolutePath", path);
1185
- history.replaceState(state, "", url.href);
1186
- notifyExpoNavigationPath(path);
1433
+ var writeNavigationHistory = (entry, mode) => {
1434
+ history[mode === "push" ? "pushState" : "replaceState"](entry, "", historyUrl(entry.path));
1435
+ notifyExpoNavigationPath(entry.path);
1187
1436
  };
1188
1437
  var notifyExpoNavigationPath = (path) => {
1189
1438
  const bridge = Reflect.get(globalThis, "__absoluteExpoBridge");
@@ -1222,6 +1471,34 @@ var renderStatus = (message, kind = "loading") => {
1222
1471
  status.textContent = message;
1223
1472
  adaptiveShell?.refreshDocument();
1224
1473
  };
1474
+ var clearNavigationFailure = () => {
1475
+ document.getElementById(NAVIGATION_ERROR_ID)?.remove();
1476
+ };
1477
+ var renderNavigationFailure = (message, retry) => {
1478
+ clearNavigationFailure();
1479
+ const error = document.createElement("aside");
1480
+ error.id = NAVIGATION_ERROR_ID;
1481
+ error.dataset.absoluteMobileNavigationError = "";
1482
+ error.setAttribute("role", "alert");
1483
+ error.style.cssText = "position:fixed;z-index:2147483646;inset:auto max(1rem,var(--absolute-safe-area-inset-right,0px)) max(1rem,var(--absolute-safe-area-inset-bottom,0px)) max(1rem,var(--absolute-safe-area-inset-left,0px));display:flex;align-items:center;justify-content:space-between;gap:1rem;box-sizing:border-box;padding:.75rem 1rem;border:1px solid color-mix(in srgb,CanvasText 24%,transparent);border-radius:.75rem;background:Canvas;color:CanvasText;box-shadow:0 .5rem 2rem color-mix(in srgb,CanvasText 20%,transparent)";
1484
+ const text = document.createElement("span");
1485
+ text.textContent = message;
1486
+ const button = document.createElement("button");
1487
+ button.type = "button";
1488
+ button.textContent = "Retry";
1489
+ button.addEventListener("click", retry, { once: true });
1490
+ error.append(text, button);
1491
+ document.body.append(error);
1492
+ };
1493
+ var markNavigationPending = (pending) => {
1494
+ if (pending) {
1495
+ document.body.dataset.absoluteMobileNavigationPending = "";
1496
+ document.body.setAttribute("aria-busy", "true");
1497
+ } else {
1498
+ delete document.body.dataset.absoluteMobileNavigationPending;
1499
+ document.body.removeAttribute("aria-busy");
1500
+ }
1501
+ };
1225
1502
  var renderPageTarget = () => {
1226
1503
  document.title = "AbsoluteJS";
1227
1504
  document.head.innerHTML = '<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">';
@@ -1247,13 +1524,16 @@ var installLocalPageStyle = (manifest, pageId, contract) => {
1247
1524
  link.dataset.absoluteMobilePageStyle = page.bundleHash;
1248
1525
  document.head.appendChild(link);
1249
1526
  };
1250
- var navigate = async (manifest, path, historyMode, fetchImpl) => {
1251
- const hadActivePage = document.body.dataset.absoluteMobilePageActive !== undefined;
1527
+ var waitForDocumentPaint = () => new Promise((resolve) => {
1528
+ if (typeof requestAnimationFrame !== "function") {
1529
+ setTimeout(resolve, 0);
1530
+ return;
1531
+ }
1532
+ requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
1533
+ });
1534
+ var commitNavigation = async (manifest, envelope, hadActivePage) => {
1252
1535
  if (!hadActivePage)
1253
1536
  renderStatus("Loading…");
1254
- const envelope = await fetchAbsoluteMobilePage(manifest, path, {
1255
- ...fetchImpl ? { fetch: fetchImpl } : {}
1256
- });
1257
1537
  let activation;
1258
1538
  const commit = async () => {
1259
1539
  await disposeAbsoluteMobilePage();
@@ -1285,14 +1565,12 @@ var navigate = async (manifest, path, historyMode, fetchImpl) => {
1285
1565
  throw new TypeError("The mobile page did not produce an activation.");
1286
1566
  if (activation.kind === "upgrade-required") {
1287
1567
  renderStatus("This app version must be updated to continue.", "update");
1288
- return;
1568
+ return false;
1289
1569
  }
1570
+ await waitForDocumentPaint();
1290
1571
  document.body.dataset.absoluteMobilePageActive = "";
1291
1572
  adaptiveShell?.refreshDocument();
1292
- if (historyMode === "push")
1293
- pushNavigationHistory(path);
1294
- else if (historyMode === "replace")
1295
- replaceNavigationHistory(path);
1573
+ return true;
1296
1574
  };
1297
1575
  var openExternalLink = (anchor, event) => {
1298
1576
  if (Reflect.get(globalThis, "__absoluteExpoBridge"))
@@ -1305,7 +1583,7 @@ var openExternalLink = (anchor, event) => {
1305
1583
  Browser.open({ url: url.href }).catch(() => location.assign(url.href));
1306
1584
  } catch {}
1307
1585
  };
1308
- var installAnchorNavigation = (manifest, onNavigate) => {
1586
+ var installAnchorNavigation = (manifest, onNavigate, onBack) => {
1309
1587
  const handleClick = (event) => {
1310
1588
  if (!(event.target instanceof Element))
1311
1589
  return;
@@ -1315,7 +1593,7 @@ var installAnchorNavigation = (manifest, onNavigate) => {
1315
1593
  const intent = readAbsoluteMobileLinkIntent(anchor);
1316
1594
  if (intent.kind === "back") {
1317
1595
  event.preventDefault();
1318
- if (!uiPrimitives?.requestBack())
1596
+ if (!uiPrimitives?.requestBack() && !onBack())
1319
1597
  history.back();
1320
1598
  return;
1321
1599
  }
@@ -1355,21 +1633,6 @@ var installDeepLinks = async (manifest, onNavigate, authRedirectUri) => {
1355
1633
  } catch {}
1356
1634
  return listener;
1357
1635
  };
1358
- var navigateWithFailureState = async (manifest, path, historyMode, direction, from, reinstallBrowserNavigation, fetchImpl) => {
1359
- let completed = false;
1360
- try {
1361
- document.documentElement.dataset.absoluteNavigationDirection = direction;
1362
- await navigate(manifest, path, historyMode, fetchImpl);
1363
- completed = true;
1364
- } catch (error) {
1365
- console.error("[Absolute Mobile] Navigation failed:", error);
1366
- renderStatus(document.documentElement.dataset.absoluteNetwork === "offline" ? "You are offline. Reconnect to load this page." : "Unable to load this page. Check your connection and retry.", "error");
1367
- } finally {
1368
- reinstallBrowserNavigation();
1369
- }
1370
- if (completed)
1371
- uiPrimitives?.navigate({ direction, from, to: path });
1372
- };
1373
1636
  var startAbsoluteMobileShell = async (options = {}) => {
1374
1637
  notifyCapacitorSystemBarsDomReady();
1375
1638
  await adaptiveShell?.dispose();
@@ -1378,6 +1641,11 @@ var startAbsoluteMobileShell = async (options = {}) => {
1378
1641
  uiPrimitives = installAbsoluteMobileUiPrimitives();
1379
1642
  const manifest = await readManifest();
1380
1643
  let activePath = initialNavigationPath(manifest.entry);
1644
+ let activeEntry = readAbsoluteMobileHistoryEntry(history.state) ?? createAbsoluteMobileHistoryEntry(activePath, 0);
1645
+ if (activeEntry.path !== activePath) {
1646
+ activeEntry = createAbsoluteMobileHistoryEntry(activePath, 0);
1647
+ }
1648
+ writeNavigationHistory(activeEntry, "replace");
1381
1649
  const auth = manifest.auth && options.createAuth ? await options.createAuth(manifest.auth, {
1382
1650
  beforeSignOut: options.beforeSignOut
1383
1651
  }) : undefined;
@@ -1387,15 +1655,104 @@ var startAbsoluteMobileShell = async (options = {}) => {
1387
1655
  options.connectPush?.(auth);
1388
1656
  if (auth && manifest.sync?.socketTickets)
1389
1657
  options.installSync?.(auth, manifest.sync);
1658
+ const snapshots = new Map;
1659
+ const targetEntries = new WeakMap;
1660
+ let suppressNextPop = false;
1661
+ let hasActivePage = false;
1390
1662
  let removeAnchorNavigation = () => {
1391
1663
  return;
1392
1664
  };
1393
- const handlePopState = () => {
1665
+ const coordinator = createAbsoluteMobileNavigationCoordinator({
1666
+ commit: async (envelope, request) => {
1667
+ if (hasActivePage) {
1668
+ snapshots.set(activeEntry.entryId, captureAbsoluteMobileDocumentState());
1669
+ }
1670
+ document.documentElement.dataset.absoluteNavigationDirection = request.direction;
1671
+ try {
1672
+ const previouslyActive = hasActivePage;
1673
+ hasActivePage = false;
1674
+ hasActivePage = await commitNavigation(manifest, envelope, previouslyActive);
1675
+ } finally {
1676
+ reinstallBrowserNavigation();
1677
+ }
1678
+ },
1679
+ load: (request, signal) => fetchAbsoluteMobilePage(manifest, request.path, {
1680
+ fetch: applicationFetch,
1681
+ signal
1682
+ }),
1683
+ onFailure: (error, phase, request) => {
1684
+ console.error("[Absolute Mobile] Navigation failed:", error);
1685
+ markNavigationPending(false);
1686
+ const targetEntry = targetEntries.get(request);
1687
+ if (targetEntry && targetEntry.index !== activeEntry.index) {
1688
+ suppressNextPop = true;
1689
+ history.go(activeEntry.index - targetEntry.index);
1690
+ }
1691
+ const message = document.documentElement.dataset.absoluteNetwork === "offline" ? "You are offline. Reconnect to load this page." : "Unable to load this page. Check your connection and retry.";
1692
+ if (phase === "load" && hasActivePage) {
1693
+ renderNavigationFailure(message, () => {
1694
+ if (targetEntry) {
1695
+ history.go(targetEntry.index - activeEntry.index);
1696
+ } else
1697
+ coordinator.navigate(request);
1698
+ });
1699
+ } else
1700
+ renderStatus(message, "error");
1701
+ },
1702
+ onStart: () => {
1703
+ clearNavigationFailure();
1704
+ markNavigationPending(true);
1705
+ },
1706
+ onSuccess: (request) => {
1707
+ let nextEntry = targetEntries.get(request);
1708
+ if (request.historyMode === "push") {
1709
+ nextEntry = createAbsoluteMobileHistoryEntry(request.path, activeEntry.index + 1);
1710
+ writeNavigationHistory(nextEntry, "push");
1711
+ } else if (request.historyMode === "replace") {
1712
+ nextEntry = createAbsoluteMobileHistoryEntry(request.path, activeEntry.index);
1713
+ writeNavigationHistory(nextEntry, "replace");
1714
+ }
1715
+ nextEntry ??= activeEntry;
1716
+ activeEntry = nextEntry;
1717
+ activePath = request.path;
1718
+ markNavigationPending(false);
1719
+ clearNavigationFailure();
1720
+ const snapshot = snapshots.get(activeEntry.entryId);
1721
+ if (snapshot)
1722
+ restoreAbsoluteMobileDocumentState(snapshot);
1723
+ else if (request.historyMode !== "none")
1724
+ resetAbsoluteMobileDocumentState();
1725
+ uiPrimitives?.navigate({
1726
+ direction: request.direction,
1727
+ from: request.from,
1728
+ to: request.path
1729
+ });
1730
+ }
1731
+ });
1732
+ const cancelPendingNavigation = () => {
1733
+ if (!coordinator.cancelPending())
1734
+ return false;
1735
+ markNavigationPending(false);
1736
+ clearNavigationFailure();
1737
+ return true;
1738
+ };
1739
+ const handlePopState = (event) => {
1394
1740
  const path = currentNavigationPath();
1395
- const from = activePath;
1396
- activePath = path;
1741
+ if (suppressNextPop) {
1742
+ suppressNextPop = false;
1743
+ notifyExpoNavigationPath(activePath);
1744
+ return;
1745
+ }
1746
+ const targetEntry = readAbsoluteMobileHistoryEntry(event.state) ?? createAbsoluteMobileHistoryEntry(path, activeEntry.index - 1);
1397
1747
  notifyExpoNavigationPath(path);
1398
- navigateWithFailureState(manifest, path, "none", "back", from, reinstallBrowserNavigation, applicationFetch);
1748
+ const request = {
1749
+ direction: targetEntry.index > activeEntry.index ? "forward" : "back",
1750
+ from: activePath,
1751
+ historyMode: "none",
1752
+ path
1753
+ };
1754
+ targetEntries.set(request, targetEntry);
1755
+ coordinator.navigate(request);
1399
1756
  };
1400
1757
  const reinstallBrowserNavigation = () => {
1401
1758
  removeAnchorNavigation();
@@ -1403,20 +1760,33 @@ var startAbsoluteMobileShell = async (options = {}) => {
1403
1760
  uiPrimitives?.dispose();
1404
1761
  uiPrimitives = installAbsoluteMobileUiPrimitives();
1405
1762
  uiPrimitives.refreshDocument(activePath);
1406
- removeAnchorNavigation = installAnchorNavigation(manifest, onNavigate);
1763
+ removeAnchorNavigation = installAnchorNavigation(manifest, onNavigate, cancelPendingNavigation);
1407
1764
  addEventListener("popstate", handlePopState);
1408
1765
  };
1409
1766
  const onNavigate = (path, direction = "forward", replace = false) => {
1410
- const from = activePath;
1411
- activePath = path;
1412
- navigateWithFailureState(manifest, path, replace ? "replace" : "push", direction, from, reinstallBrowserNavigation, applicationFetch);
1767
+ if (path === activePath && !replace && hasActivePage)
1768
+ return;
1769
+ coordinator.navigate({
1770
+ direction,
1771
+ from: activePath,
1772
+ historyMode: replace ? "replace" : "push",
1773
+ path
1774
+ });
1413
1775
  };
1414
- await navigateWithFailureState(manifest, activePath, "none", "replace", activePath, reinstallBrowserNavigation, applicationFetch);
1776
+ reinstallBrowserNavigation();
1777
+ await coordinator.navigate({
1778
+ direction: "replace",
1779
+ from: activePath,
1780
+ historyMode: "none",
1781
+ path: activePath
1782
+ });
1415
1783
  await installDeepLinks(manifest, onNavigate, auth?.redirectUri);
1416
1784
  try {
1417
1785
  await App.addListener("backButton", ({ canGoBack }) => {
1418
1786
  if (uiPrimitives?.requestBack())
1419
1787
  return;
1788
+ if (cancelPendingNavigation())
1789
+ return;
1420
1790
  if (canGoBack)
1421
1791
  history.back();
1422
1792
  else
@@ -107,17 +107,28 @@ var installAbsoluteMobileShellSync = (auth, config, options = {}) => {
107
107
  const reportSchemaState = options.reportSchemaState ?? reportShellSchemaState;
108
108
  const configureBackground = options.configureBackground ?? configureCapacitorBackgroundSync;
109
109
  const clearBackground = options.clearBackground ?? (() => AbsoluteBackgroundSync.clear());
110
+ let schemaReady = Promise.resolve(true);
110
111
  if (store?.getSchemaStatus) {
111
112
  reportSchemaState({ state: "preparing" });
112
- store.getSchemaStatus().then(reportSchemaState).catch((error) => reportSchemaState(schemaFailureState(error)));
113
+ schemaReady = store.getSchemaStatus().then((state) => {
114
+ reportSchemaState(state);
115
+ return true;
116
+ }).catch((error) => {
117
+ reportSchemaState(schemaFailureState(error));
118
+ return false;
119
+ });
113
120
  }
114
121
  if (namespace && config) {
115
- configureBackground({
116
- clientId: auth.clientId,
117
- endpoint: config.background.endpoint,
118
- intervalMinutes: config.background.intervalMinutes,
119
- issuer: auth.issuer,
120
- namespace
122
+ schemaReady.then((ready) => {
123
+ if (!ready)
124
+ return;
125
+ return configureBackground({
126
+ clientId: auth.clientId,
127
+ endpoint: config.background.endpoint,
128
+ intervalMinutes: config.background.intervalMinutes,
129
+ issuer: auth.issuer,
130
+ namespace
131
+ });
121
132
  }).catch((error) => console.error("[Absolute Mobile] Background Sync configuration failed:", error));
122
133
  } else {
123
134
  clearBackground().catch(() => {
@@ -0,0 +1,36 @@
1
+ export type AbsoluteMobileNavigationHistoryMode = 'none' | 'push' | 'replace';
2
+ export type AbsoluteMobileNavigationRequest = {
3
+ direction: 'back' | 'forward' | 'replace';
4
+ from: string;
5
+ historyMode: AbsoluteMobileNavigationHistoryMode;
6
+ path: string;
7
+ };
8
+ export type AbsoluteMobileNavigationFailurePhase = 'commit' | 'load';
9
+ export type AbsoluteMobileNavigationResult = {
10
+ kind: 'cancelled';
11
+ } | {
12
+ kind: 'committed';
13
+ } | {
14
+ kind: 'failed';
15
+ error: unknown;
16
+ phase: AbsoluteMobileNavigationFailurePhase;
17
+ };
18
+ export type AbsoluteMobileNavigationCoordinatorOptions<Payload> = {
19
+ commit: (payload: Payload, request: AbsoluteMobileNavigationRequest) => Promise<void>;
20
+ load: (request: AbsoluteMobileNavigationRequest, signal: AbortSignal) => Promise<Payload>;
21
+ onCommit?: (request: AbsoluteMobileNavigationRequest) => void;
22
+ onFailure?: (error: unknown, phase: AbsoluteMobileNavigationFailurePhase, request: AbsoluteMobileNavigationRequest) => void;
23
+ onStart?: (request: AbsoluteMobileNavigationRequest) => void;
24
+ onSuccess?: (request: AbsoluteMobileNavigationRequest) => void;
25
+ };
26
+ export type AbsoluteMobileNavigationCoordinator = {
27
+ cancelPending(): boolean;
28
+ dispose(): void;
29
+ navigate(request: AbsoluteMobileNavigationRequest): Promise<AbsoluteMobileNavigationResult>;
30
+ phase(): 'committing' | 'idle' | 'loading' | 'queued';
31
+ };
32
+ /**
33
+ * Coordinates native-shell route loads. Loads may overlap, but only the latest
34
+ * completed load may enter the serialized document commit boundary.
35
+ */
36
+ export declare const createAbsoluteMobileNavigationCoordinator: <Payload>(options: AbsoluteMobileNavigationCoordinatorOptions<Payload>) => AbsoluteMobileNavigationCoordinator;