Current section
26 Versions
Jump to
Current section
26 Versions
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"] |