@tokenoftrust/storefront-runner 4.7.3 → 4.7.5

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.
@@ -564,6 +564,7 @@ html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="fulfillment"]
564
564
  html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="reporting"] #reporting,
565
565
  html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="ai-workflows"] #ai-workflows,
566
566
  html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="blog"] #blog,
567
+ html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="messages"] #messages,
567
568
  html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="publish"] #publish,
568
569
  html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="integrations"] #integrations,
569
570
  html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="settings"] #settings {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "4.7.3",
3
+ "version": "4.7.5",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
6
6
  "packageManager": "pnpm@11.9.0",
@@ -5,7 +5,8 @@
5
5
  * the renderer-published boot descriptor global, guarantees ONE lifecycle per document
6
6
  * with a window-level singleton (so a nested layout that re-runs the boot island cannot
7
7
  * double-init), wires same-document navigations to the reserved completed-navigation
8
- * event, disposes on document teardown, and is a guarded no-op off the browser.
8
+ * event, disposes on every `pagehide`, boots a fresh lifecycle when the back-forward
9
+ * cache restores the page, and is a guarded no-op off the browser.
9
10
  *
10
11
  * The entry module for each integration is the same-origin, exact-version asset the
11
12
  * boot descriptor carries (`integration.entry.url`): the default loader dynamically
@@ -440,20 +441,38 @@ function deferFetchCapability(
440
441
  })) as typeof fetch;
441
442
  }
442
443
 
444
+ /** The window property marking that this window's event wiring is installed (once per window). */
445
+ const LIFECYCLE_WIRED_GLOBAL = "__TOT_INTEGRATION_LIFECYCLE_WIRED__";
446
+
443
447
  /**
444
- * Boot the lifecycle for `win` exactly once. A second call returns the same instance
445
- * (nested-layout safety). Returns `null` when there is no boot descriptor to act on.
448
+ * Boot the lifecycle for `win`. While a live lifecycle exists a second call returns it
449
+ * (nested-layout safety). A disposed one — the page was hidden into the back-forward
450
+ * cache — is replaced by a fresh lifecycle. Returns `null` when there is no boot
451
+ * descriptor to act on.
446
452
  */
447
453
  export function bootIntegrationLifecycle(
448
454
  win: LifecycleWindow,
449
455
  options: BootLifecycleOptions = {},
450
456
  ): IntegrationLifecycle | null {
451
457
  const existing = win[LIFECYCLE_SINGLETON_GLOBAL];
452
- if (existing instanceof IntegrationLifecycle) return existing;
458
+ if (existing instanceof IntegrationLifecycle && !existing.isDisposed) return existing;
453
459
 
454
460
  const descriptor = readDescriptor(win);
455
461
  if (!descriptor) return null;
456
462
 
463
+ const lifecycle = createDocumentLifecycle(win, descriptor, options);
464
+ win[LIFECYCLE_SINGLETON_GLOBAL] = lifecycle;
465
+ wireWindow(win, options);
466
+ void lifecycle.init();
467
+ return lifecycle;
468
+ }
469
+
470
+ function createDocumentLifecycle(
471
+ win: LifecycleWindow,
472
+ descriptor: IntegrationBootDescriptor,
473
+ options: BootLifecycleOptions,
474
+ ): IntegrationLifecycle {
475
+
457
476
  const hostFetch = options.fetchCapability ??
458
477
  (typeof win.fetch === "function" ? win.fetch.bind(win) : undefined);
459
478
  const schedulePlatformAdapterStart =
@@ -508,7 +527,7 @@ export function bootIntegrationLifecycle(
508
527
  // The managed-analytics bridge over the SAME broker the adapters subscribe to — the
509
528
  // only seam that emits this document's governed page view. The lifecycle owns when it
510
529
  // fires (after every adapter has subscribed, and re-attempted on a consent grant).
511
- const lifecycle = new IntegrationLifecycle({
530
+ return new IntegrationLifecycle({
512
531
  descriptor,
513
532
  loadModule,
514
533
  isEligible,
@@ -536,39 +555,48 @@ export function bootIntegrationLifecycle(
536
555
  pageView: () => documentPageView(win, descriptor, options.now ?? Date.now),
537
556
  },
538
557
  });
539
- win[LIFECYCLE_SINGLETON_GLOBAL] = lifecycle;
558
+ }
540
559
 
541
- wireNavigation(win, lifecycle);
542
- wireTeardown(win, lifecycle);
543
- void lifecycle.init();
544
- return lifecycle;
560
+ /** The window's current live lifecycle, if any. */
561
+ function liveLifecycle(win: LifecycleWindow): IntegrationLifecycle | undefined {
562
+ const current = win[LIFECYCLE_SINGLETON_GLOBAL];
563
+ return current instanceof IntegrationLifecycle && !current.isDisposed ? current : undefined;
545
564
  }
546
565
 
547
566
  /**
548
- * Wire same-document navigations to the reserved completed-navigation event. In a
549
- * traditional MPA a real document load re-inits by construction, so only the two
550
- * navigations that DON'T reload the document — hash changes and history back/forward —
551
- * need a completed-navigation signal, and neither re-initializes integrations.
567
+ * Install this window's event wiring once; every listener acts on the CURRENT lifecycle.
568
+ *
569
+ * Same-document navigations — hash changes and history back/forward — emit the reserved
570
+ * completed-navigation event and never re-initialize integrations; a real document load
571
+ * re-inits by construction.
572
+ *
573
+ * Every `pagehide` disposes the lifecycle, so integrations clean up and abandon any
574
+ * deferred work (a held navigation never resumes after the shopper left). A
575
+ * `pageshow` from the back-forward cache boots a fresh lifecycle, so the restored page
576
+ * has live integrations again.
552
577
  */
553
- function wireNavigation(win: LifecycleWindow, lifecycle: IntegrationLifecycle): void {
578
+ function wireWindow(win: LifecycleWindow, options: BootLifecycleOptions): void {
579
+ if (win[LIFECYCLE_WIRED_GLOBAL] === true) return;
580
+ win[LIFECYCLE_WIRED_GLOBAL] = true;
554
581
  let lastPath = win.location.pathname + win.location.hash;
555
582
  win.addEventListener("hashchange", () => {
556
583
  const to = win.location.pathname + win.location.hash;
557
- lifecycle.notifyNavigationCompleted({ from: lastPath, to, kind: "hash" });
584
+ liveLifecycle(win)?.notifyNavigationCompleted({ from: lastPath, to, kind: "hash" });
558
585
  lastPath = to;
559
586
  });
560
587
  win.addEventListener("popstate", () => {
561
588
  const to = win.location.pathname + win.location.hash;
562
589
  const kind: NavigationKind = "back-forward";
563
- lifecycle.notifyNavigationCompleted({ from: lastPath, to, kind });
590
+ liveLifecycle(win)?.notifyNavigationCompleted({ from: lastPath, to, kind });
564
591
  lastPath = to;
565
592
  });
566
- }
567
-
568
- /** Dispose the lifecycle when the document is torn down, so integrations clean up once. */
569
- function wireTeardown(win: LifecycleWindow, lifecycle: IntegrationLifecycle): void {
570
593
  win.addEventListener("pagehide", () => {
571
- void lifecycle.dispose();
594
+ const current = win[LIFECYCLE_SINGLETON_GLOBAL];
595
+ if (current instanceof IntegrationLifecycle) void current.dispose();
596
+ });
597
+ win.addEventListener("pageshow", (event) => {
598
+ if ((event as { persisted?: unknown } | null)?.persisted !== true) return;
599
+ bootIntegrationLifecycle(win, options);
572
600
  });
573
601
  }
574
602
 
@@ -224,6 +224,11 @@ export class IntegrationLifecycle {
224
224
  return this.state === "initialized";
225
225
  }
226
226
 
227
+ /** True once dispose has started; a disposed lifecycle never runs again. */
228
+ get isDisposed(): boolean {
229
+ return this.isTerminal();
230
+ }
231
+
227
232
  /**
228
233
  * Initialize every integration EXACTLY ONCE per document, in the plan's resolved order.
229
234
  * A second call — including one from a nested layout that re-runs the boot island — is a