Current section

26 Versions

Jump to

Compare versions

3 files changed
+116 additions
-28 deletions
  @@ -45,6 +45,8 @@ import {toVNode} from "snabbdom";
45 45
46 46 // TODO: test
47 47 export default class Hologram {
48 + static #PAGE_SNAPSHOT_KEY_PREFIX = "hologram_page_snapshot_";
49 +
48 50 // Made public to make tests easier
49 51 static prefetchedPages = new Map();
50 52
  @@ -61,6 +63,9 @@ export default class Hologram {
61 63 Utils: Utils,
62 64 };
63 65
66 + // In-memory cache for page snapshots (fastest access)
67 + static #pageSnapshots = new Map();
68 +
64 69 static #historyId = null;
65 70 static #isInitiated = false;
66 71 static #pageModule = null;
  @@ -284,8 +289,8 @@ export default class Hologram {
284 289 }
285 290
286 291 // Made public to make tests easier
287 - static loadNewPage(pagePath, html) {
288 - $.#savePageSnapshot();
292 + static async loadNewPage(pagePath, html) {
293 + await $.#savePageSnapshot();
289 294 $.#historyId = crypto.randomUUID();
290 295
291 296 window.requestAnimationFrame(() => {
  @@ -346,9 +351,9 @@ export default class Hologram {
346 351 }
347 352
348 353 static run() {
349 - Hologram.#onReady(() => {
354 + Hologram.#onReady(async () => {
350 355 if (!Hologram.#isInitiated) {
351 - Hologram.#init();
356 + await Hologram.#init();
352 357 }
353 358
354 359 try {
  @@ -556,6 +561,44 @@ export default class Hologram {
556 561 }
557 562 }
558 563
564 + static async #getPageSnapshot(historyId) {
565 + const snapshotKey = $.#pageSnapshotKey(historyId);
566 +
567 + // Try in-memory cache first (fastest) - returns deserialized object
568 + if ($.#pageSnapshots.has(snapshotKey)) {
569 + return $.#pageSnapshots.get(snapshotKey);
570 + }
571 +
572 + // Try OPFS second
573 + try {
574 + const root = await navigator.storage.getDirectory();
575 + const fileHandle = await root.getFileHandle(snapshotKey, {create: false});
576 + const file = await fileHandle.getFile();
577 + const serializedSnapshot = await file.text();
578 + const deserializedSnapshot = Deserializer.deserialize(serializedSnapshot);
579 +
580 + // Cache the deserialized snapshot in memory for faster subsequent access
581 + $.#pageSnapshots.set(snapshotKey, deserializedSnapshot);
582 +
583 + return deserializedSnapshot;
584 + } catch {
585 + // Fall back to session storage if OPFS fails
586 + const serializedSnapshot = sessionStorage.getItem(snapshotKey);
587 +
588 + if (serializedSnapshot) {
589 + const deserializedSnapshot =
590 + Deserializer.deserialize(serializedSnapshot);
591 +
592 + // Cache the deserialized snapshot in memory for faster subsequent access
593 + $.#pageSnapshots.set(snapshotKey, deserializedSnapshot);
594 +
595 + return deserializedSnapshot;
596 + }
597 +
598 + return null;
599 + }
600 + }
601 +
559 602 // Deps: [:maps.get/2]
560 603 static #getToParam(operation) {
561 604 return Erlang_Maps["get/2"](
  @@ -565,13 +608,13 @@ export default class Hologram {
565 608 }
566 609
567 610 static async #handlePopstateEvent(event) {
568 - $.#savePageSnapshot();
611 + await $.#savePageSnapshot();
569 612 $.#historyId = event.state;
570 613
571 - const serializedPageSnapshot = sessionStorage.getItem(event.state);
614 + const pageSnapshot = await $.#getPageSnapshot(event.state);
572 615
573 - if (serializedPageSnapshot) {
574 - $.#restorePageSnapshot(serializedPageSnapshot);
616 + if (pageSnapshot) {
617 + $.#restorePageSnapshot(pageSnapshot);
575 618 }
576 619
577 620 if ($.#isPageModuleRegistered(Hologram.#pageModule)) {
  @@ -597,7 +640,7 @@ export default class Hologram {
597 640
598 641 // Executed only once, on the initial page load.
599 642 // Deps: [:maps.get/2]
600 - static #init() {
643 + static async #init() {
601 644 // TODO: consider when implementing boxed error handling
602 645 // window.addEventListener("error", (event) => {
603 646 // if (event.error instanceof HologramBoxedError) {
  @@ -620,7 +663,8 @@ export default class Hologram {
620 663 });
621 664
622 665 window.addEventListener("beforeunload", () => {
623 - Hologram.#savePageSnapshot();
666 + // Force synchronous session storage save since async OPFS may not complete before page termination
667 + Hologram.#savePageSnapshot(true);
624 668 });
625 669
626 670 window.addEventListener("popstate", Hologram.#handlePopstateEvent);
  @@ -635,11 +679,11 @@ export default class Hologram {
635 679 // Check if there's already a history state (e.g., when navigating back from external page)
636 680 if (history.state) {
637 681 $.#historyId = history.state;
638 - const serializedPageSnapshot = sessionStorage.getItem(history.state);
682 + const pageSnapshot = await $.#getPageSnapshot(history.state);
639 683
640 684 // Only restore state for back/forward navigation, not page reloads
641 - if (!$.#isPageReload() && serializedPageSnapshot) {
642 - $.#restorePageSnapshot(serializedPageSnapshot);
685 + if (!$.#isPageReload() && pageSnapshot) {
686 + $.#restorePageSnapshot(pageSnapshot);
643 687 }
644 688 } else {
645 689 $.#historyId = crypto.randomUUID();
  @@ -760,6 +804,10 @@ export default class Hologram {
760 804 }
761 805 }
762 806
807 + static #pageSnapshotKey(historyId) {
808 + return `${$.#PAGE_SNAPSHOT_KEY_PREFIX}${historyId}`;
809 + }
810 +
763 811 // TODO: raise error if there is no head or body
764 812 static #patchPage(html) {
765 813 globalThis.hologram.pageScriptLoaded = false;
  @@ -776,9 +824,9 @@ export default class Hologram {
776 824 $.#registeredPageModules.add(pageModule.value);
777 825 }
778 826
779 - static #restorePageSnapshot(serializedPageSnapshot) {
827 + static #restorePageSnapshot(pageSnapshot) {
780 828 const {componentRegistryEntries, pageModule, pageParams, scrollPosition} =
781 - Deserializer.deserialize(serializedPageSnapshot);
829 + pageSnapshot;
782 830
783 831 ComponentRegistry.populate(componentRegistryEntries);
784 832
  @@ -789,18 +837,58 @@ export default class Hologram {
789 837 $.#shouldLoadMountData = false;
790 838 }
791 839
792 - static #savePageSnapshot() {
793 - const serializedPageSnapshot = Serializer.serialize(
794 - {
795 - componentRegistryEntries: ComponentRegistry.entries,
796 - pageModule: Hologram.#pageModule,
797 - pageParams: Hologram.#pageParams,
798 - scrollPosition: [window.scrollX, window.scrollY],
799 - },
800 - "client",
801 - );
840 + static async #savePageSnapshot(forceSync = false) {
841 + const pageSnapshot = {
842 + componentRegistryEntries: ComponentRegistry.entries,
843 + pageModule: Hologram.#pageModule,
844 + pageParams: Hologram.#pageParams,
845 + scrollPosition: [window.scrollX, window.scrollY],
846 + };
802 847
803 - sessionStorage.setItem($.#historyId, serializedPageSnapshot);
848 + const snapshotKey = $.#pageSnapshotKey($.#historyId);
849 +
850 + // Always save deserialized object to in-memory cache first (fastest)
851 + $.#pageSnapshots.set(snapshotKey, pageSnapshot);
852 +
853 + const serializedPageSnapshot = Serializer.serialize(pageSnapshot, "client");
854 +
855 + // For beforeunload: save synchronously to session storage only
856 + if (forceSync) {
857 + try {
858 + sessionStorage.setItem(snapshotKey, serializedPageSnapshot);
859 + } catch (error) {
860 + console.error(
861 + "Failed to save page snapshot to session storage:",
862 + error,
863 + );
864 + }
865 +
866 + return;
867 + }
868 +
869 + // For normal navigation: OPFS primary, session storage fallback
870 + try {
871 + const root = await navigator.storage.getDirectory();
872 + const fileHandle = await root.getFileHandle(snapshotKey, {create: true});
873 + const writable = await fileHandle.createWritable();
874 + await writable.write(serializedPageSnapshot);
875 + await writable.close();
876 +
877 + // Successfully saved to OPFS, clear session storage fallback if it exists
878 + sessionStorage.removeItem(snapshotKey);
879 + } catch (opfsError) {
880 + console.error("Failed to save page snapshot to OPFS:", opfsError);
881 +
882 + // Fallback to session storage if OPFS fails
883 + try {
884 + sessionStorage.setItem(snapshotKey, serializedPageSnapshot);
885 + } catch (sessionStorageError) {
886 + console.error(
887 + "Failed to save page snapshot to session storage:",
888 + sessionStorageError,
889 + );
890 + }
891 + }
804 892 }
805 893
806 894 static #scheduleQueuedInitActions() {
  @@ -7,7 +7,7 @@
7 7 {<<"Sponsor">>,<<"https://github.com/sponsors/bartblast">>},
8 8 {<<"Website">>,<<"https://hologram.page">>}]}.
9 9 {<<"name">>,<<"hologram">>}.
10 - {<<"version">>,<<"0.6.4">>}.
10 + {<<"version">>,<<"0.6.5">>}.
11 11 {<<"description">>,
12 12 <<"Full stack isomorphic Elixir web framework that can be used on top of Phoenix.">>}.
13 13 {<<"elixir">>,<<"~> 1.0">>}.
  @@ -2,7 +2,7 @@
2 2 defmodule Hologram.MixProject do
3 3 use Mix.Project
4 4
5 - @version "0.6.4"
5 + @version "0.6.5"
6 6
7 7 # Copied from Hologram.Commons.SystemUtils
8 8 @windows_exec_suffixes [".bat", ".cmd", ".exe"]