@effindomv2/fui-as 0.1.0

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.
Files changed (137) hide show
  1. package/LICENSE.md +7 -0
  2. package/browser/src/common-harness/host-imports.ts +430 -0
  3. package/browser/src/common-harness/interop.ts +39 -0
  4. package/browser/src/common-harness/managed-harness-bitmap-host.ts +92 -0
  5. package/browser/src/common-harness/managed-harness-fetch-host.ts +201 -0
  6. package/browser/src/common-harness/managed-harness-file-host.ts +1101 -0
  7. package/browser/src/common-harness/managed-harness-file-payloads.ts +143 -0
  8. package/browser/src/common-harness/managed-harness-file-types.ts +106 -0
  9. package/browser/src/common-harness/managed-harness-session.ts +15 -0
  10. package/browser/src/common-harness/managed-harness.ts +1323 -0
  11. package/browser/src/common-harness/managed-history.ts +168 -0
  12. package/browser/src/common-harness/persisted-restore-policy.ts +50 -0
  13. package/browser/src/common-harness/persisted-ui-state-controller.ts +309 -0
  14. package/browser/src/common-harness/text-session-bridge.ts +452 -0
  15. package/browser/src/common-harness/types.ts +205 -0
  16. package/browser/src/common-harness/ui-chrome.ts +191 -0
  17. package/browser/src/common-harness/ui-imports.ts +529 -0
  18. package/browser/src/common-harness/wasm-module-cache.ts +47 -0
  19. package/browser/src/common-harness.ts +27 -0
  20. package/browser/src/file-processing-worker.ts +89 -0
  21. package/browser/src/host-events.ts +97 -0
  22. package/browser/src/host-services.ts +203 -0
  23. package/browser/src/index.ts +62 -0
  24. package/browser/src/persisted-ui-state.ts +206 -0
  25. package/browser/src/routed-harness.ts +198 -0
  26. package/browser/src/worker-bootstrap.ts +483 -0
  27. package/browser/src/worker-manager.ts +230 -0
  28. package/browser/src/worker-types.ts +50 -0
  29. package/package.json +89 -0
  30. package/scripts/build-demo-as.sh +91 -0
  31. package/scripts/build.sh +325 -0
  32. package/scripts/generate-host-events.ts +175 -0
  33. package/scripts/generate-host-services.ts +157 -0
  34. package/src/Fui.ts +205 -0
  35. package/src/FuiExports.ts +55 -0
  36. package/src/FuiPrimitives.ts +15 -0
  37. package/src/FuiWorker.ts +3 -0
  38. package/src/FuiWorkerExports.ts +6 -0
  39. package/src/bindings/ui.ts +531 -0
  40. package/src/color.ts +86 -0
  41. package/src/controls/AntiSelectionArea.ts +23 -0
  42. package/src/controls/Button.ts +750 -0
  43. package/src/controls/Checkbox.ts +181 -0
  44. package/src/controls/ContextMenu.ts +885 -0
  45. package/src/controls/ControlTemplateSet.ts +37 -0
  46. package/src/controls/Dialog.ts +355 -0
  47. package/src/controls/Dropdown.ts +856 -0
  48. package/src/controls/Form.ts +110 -0
  49. package/src/controls/NavLink.ts +211 -0
  50. package/src/controls/Popup.ts +129 -0
  51. package/src/controls/ProgressBar.ts +180 -0
  52. package/src/controls/RadioButton.ts +135 -0
  53. package/src/controls/RadioGroup.ts +244 -0
  54. package/src/controls/SelectionArea.ts +75 -0
  55. package/src/controls/Slider.ts +471 -0
  56. package/src/controls/Switch.ts +132 -0
  57. package/src/controls/TextArea.ts +20 -0
  58. package/src/controls/TextInput.ts +7 -0
  59. package/src/controls/index.ts +18 -0
  60. package/src/controls/internal/ButtonPresenter.ts +95 -0
  61. package/src/controls/internal/CheckboxIndicatorPresenter.ts +93 -0
  62. package/src/controls/internal/DropdownChevronPresenter.ts +67 -0
  63. package/src/controls/internal/DropdownFieldPresenter.ts +110 -0
  64. package/src/controls/internal/DropdownOptionRowPresenter.ts +82 -0
  65. package/src/controls/internal/PopupPresenter.ts +198 -0
  66. package/src/controls/internal/PressableIndicatorPresenter.ts +32 -0
  67. package/src/controls/internal/PressableLabeledControl.ts +221 -0
  68. package/src/controls/internal/RadioIndicatorPresenter.ts +73 -0
  69. package/src/controls/internal/SliderPresenter.ts +157 -0
  70. package/src/controls/internal/SwitchIndicatorPresenter.ts +72 -0
  71. package/src/controls/internal/TextInputCore.ts +695 -0
  72. package/src/controls/internal/TextInputPresenter.ts +72 -0
  73. package/src/controls/templating.ts +54 -0
  74. package/src/core/Action.ts +94 -0
  75. package/src/core/Actions.ts +37 -0
  76. package/src/core/Animation.ts +412 -0
  77. package/src/core/Application.ts +328 -0
  78. package/src/core/Assets.ts +264 -0
  79. package/src/core/AttachedProperties.ts +32 -0
  80. package/src/core/Bitmap.ts +70 -0
  81. package/src/core/BoundCallback.ts +104 -0
  82. package/src/core/Callbacks.ts +17 -0
  83. package/src/core/ContextMenuManager.ts +466 -0
  84. package/src/core/DebugApi.ts +30 -0
  85. package/src/core/Disposable.ts +10 -0
  86. package/src/core/DragDropManager.ts +179 -0
  87. package/src/core/DragGesture.ts +184 -0
  88. package/src/core/DynamicAssetIds.ts +24 -0
  89. package/src/core/Errors.ts +48 -0
  90. package/src/core/EventRouter.ts +408 -0
  91. package/src/core/ExternalDropManager.ts +122 -0
  92. package/src/core/Fetch.ts +264 -0
  93. package/src/core/FetchFfi.ts +15 -0
  94. package/src/core/File.ts +1002 -0
  95. package/src/core/FocusAdornerManager.ts +263 -0
  96. package/src/core/FocusVisibility.ts +36 -0
  97. package/src/core/FrameScheduler.ts +28 -0
  98. package/src/core/KeyboardScroll.ts +161 -0
  99. package/src/core/KeyboardScrollTracker.ts +386 -0
  100. package/src/core/Logger.ts +80 -0
  101. package/src/core/Navigation.ts +13 -0
  102. package/src/core/Node.ts +1708 -0
  103. package/src/core/PersistedState.ts +102 -0
  104. package/src/core/PersistedUiState.ts +142 -0
  105. package/src/core/Platform.ts +219 -0
  106. package/src/core/Signal.ts +89 -0
  107. package/src/core/Theme.ts +365 -0
  108. package/src/core/Timers.ts +129 -0
  109. package/src/core/ToolTip.ts +122 -0
  110. package/src/core/ToolTipManager.ts +459 -0
  111. package/src/core/Transitions.ts +34 -0
  112. package/src/core/Typography.ts +204 -0
  113. package/src/core/Worker.ts +196 -0
  114. package/src/core/bind.ts +37 -0
  115. package/src/core/event_exports.ts +596 -0
  116. package/src/core/ffi.ts +728 -0
  117. package/src/host-services/runtime.ts +25 -0
  118. package/src/nodes/FlexBox.ts +789 -0
  119. package/src/nodes/GradientStop.ts +9 -0
  120. package/src/nodes/Grid.ts +183 -0
  121. package/src/nodes/Image.ts +189 -0
  122. package/src/nodes/Portal.ts +14 -0
  123. package/src/nodes/RichText.ts +312 -0
  124. package/src/nodes/ScrollBar.ts +570 -0
  125. package/src/nodes/ScrollBox.ts +415 -0
  126. package/src/nodes/ScrollState.ts +10 -0
  127. package/src/nodes/ScrollView.ts +511 -0
  128. package/src/nodes/Svg.ts +142 -0
  129. package/src/nodes/Text.ts +145 -0
  130. package/src/nodes/TextCore.ts +558 -0
  131. package/src/nodes/VirtualList.ts +431 -0
  132. package/src/nodes/helpers.ts +25 -0
  133. package/src/nodes/index.ts +14 -0
  134. package/src/tsconfig.json +7 -0
  135. package/src/worker/Worker.ts +169 -0
  136. package/src/worker/WorkerJob.ts +65 -0
  137. package/src/worker/ffi.ts +23 -0
@@ -0,0 +1,168 @@
1
+ import type { ManagedHistoryState } from './types';
2
+
3
+ interface NavigationApiLike {
4
+ readonly canGoBack?: boolean;
5
+ readonly canGoForward?: boolean;
6
+ back?(): void | Promise<void>;
7
+ forward?(): void | Promise<void>;
8
+ }
9
+
10
+ let managedHistoryInitialized = false;
11
+ let managedHistoryEntries: string[] = [];
12
+ let managedHistoryIndex = 0;
13
+
14
+ function normalizeManagedHistoryState(rawState: unknown, currentUrl: URL): ManagedHistoryState {
15
+ const rawRecord = typeof rawState === 'object' && rawState !== null
16
+ ? rawState as { href?: unknown; uiSnapshotId?: unknown }
17
+ : null;
18
+ const href = typeof rawRecord?.href === 'string' && rawRecord.href.length > 0
19
+ ? rawRecord.href
20
+ : currentUrl.href;
21
+ const snapshotId = typeof rawRecord?.uiSnapshotId === 'string' && rawRecord.uiSnapshotId.length > 0
22
+ ? rawRecord.uiSnapshotId
23
+ : undefined;
24
+ return snapshotId === undefined
25
+ ? { href }
26
+ : { href, uiSnapshotId: snapshotId };
27
+ }
28
+
29
+ export function readManagedHistoryState(currentUrl: URL = new URL(window.location.href)): ManagedHistoryState {
30
+ return normalizeManagedHistoryState(window.history.state, currentUrl);
31
+ }
32
+
33
+ function writeManagedHistoryState(state: ManagedHistoryState, mode: 'push' | 'replace'): void {
34
+ if (mode === 'push') {
35
+ window.history.pushState(state, '', state.href);
36
+ return;
37
+ }
38
+ window.history.replaceState(state, '', state.href);
39
+ }
40
+
41
+ export function setCurrentManagedHistorySnapshotId(
42
+ snapshotId: string | undefined,
43
+ currentUrl: URL = new URL(window.location.href),
44
+ ): ManagedHistoryState {
45
+ const state = snapshotId === undefined
46
+ ? { href: currentUrl.href }
47
+ : { href: currentUrl.href, uiSnapshotId: snapshotId };
48
+ writeManagedHistoryState(state, 'replace');
49
+ return state;
50
+ }
51
+
52
+ export function ensureManagedHistoryInitialized(): void {
53
+ const currentUrl = new URL(window.location.href);
54
+ if (managedHistoryInitialized) {
55
+ const normalizedState = readManagedHistoryState(currentUrl);
56
+ writeManagedHistoryState(
57
+ normalizedState.href === currentUrl.href
58
+ ? normalizedState
59
+ : (normalizedState.uiSnapshotId === undefined
60
+ ? { href: currentUrl.href }
61
+ : { href: currentUrl.href, uiSnapshotId: normalizedState.uiSnapshotId }),
62
+ 'replace',
63
+ );
64
+ return;
65
+ }
66
+ managedHistoryEntries = [currentUrl.href];
67
+ managedHistoryIndex = 0;
68
+ managedHistoryInitialized = true;
69
+ const normalizedState = readManagedHistoryState(currentUrl);
70
+ writeManagedHistoryState(
71
+ normalizedState.href === currentUrl.href
72
+ ? normalizedState
73
+ : (normalizedState.uiSnapshotId === undefined
74
+ ? { href: currentUrl.href }
75
+ : { href: currentUrl.href, uiSnapshotId: normalizedState.uiSnapshotId }),
76
+ 'replace',
77
+ );
78
+ }
79
+
80
+ export function pushManagedHistoryEntry(target: URL): void {
81
+ ensureManagedHistoryInitialized();
82
+ managedHistoryEntries = managedHistoryEntries.slice(0, managedHistoryIndex + 1);
83
+ managedHistoryEntries.push(target.href);
84
+ managedHistoryIndex = managedHistoryEntries.length - 1;
85
+ writeManagedHistoryState({ href: target.href }, 'push');
86
+ }
87
+
88
+ export function replaceManagedHistoryEntry(target: URL): void {
89
+ ensureManagedHistoryInitialized();
90
+ managedHistoryEntries[managedHistoryIndex] = target.href;
91
+ writeManagedHistoryState({ href: target.href }, 'replace');
92
+ }
93
+
94
+ export function syncManagedHistoryPop(target: URL): void {
95
+ ensureManagedHistoryInitialized();
96
+ if (managedHistoryIndex > 0 && managedHistoryEntries[managedHistoryIndex - 1] === target.href) {
97
+ managedHistoryIndex -= 1;
98
+ return;
99
+ }
100
+ if (managedHistoryIndex + 1 < managedHistoryEntries.length && managedHistoryEntries[managedHistoryIndex + 1] === target.href) {
101
+ managedHistoryIndex += 1;
102
+ return;
103
+ }
104
+ const existingIndex = managedHistoryEntries.lastIndexOf(target.href);
105
+ if (existingIndex >= 0) {
106
+ managedHistoryIndex = existingIndex;
107
+ return;
108
+ }
109
+ managedHistoryEntries = [target.href];
110
+ managedHistoryIndex = 0;
111
+ }
112
+
113
+ export function canManagedNavigateBack(): boolean {
114
+ ensureManagedHistoryInitialized();
115
+ return managedHistoryIndex > 0;
116
+ }
117
+
118
+ export function canManagedNavigateForward(): boolean {
119
+ ensureManagedHistoryInitialized();
120
+ return managedHistoryIndex + 1 < managedHistoryEntries.length;
121
+ }
122
+
123
+ function getBrowserNavigationApi(): NavigationApiLike | null {
124
+ const windowWithNavigation = window as Window & {
125
+ navigation?: NavigationApiLike;
126
+ };
127
+ return windowWithNavigation.navigation ?? null;
128
+ }
129
+
130
+ export function canBrowserNavigateBack(): boolean {
131
+ const navigationApi = getBrowserNavigationApi();
132
+ if (navigationApi?.canGoBack !== undefined) {
133
+ return navigationApi.canGoBack;
134
+ }
135
+ return canManagedNavigateBack();
136
+ }
137
+
138
+ export function canBrowserNavigateForward(): boolean {
139
+ const navigationApi = getBrowserNavigationApi();
140
+ if (navigationApi?.canGoForward !== undefined) {
141
+ return navigationApi.canGoForward;
142
+ }
143
+ return canManagedNavigateForward();
144
+ }
145
+
146
+ export function navigateBrowserBack(): void {
147
+ if (!canBrowserNavigateBack()) {
148
+ return;
149
+ }
150
+ const navigationApi = getBrowserNavigationApi();
151
+ if (navigationApi?.back !== undefined) {
152
+ void navigationApi.back();
153
+ return;
154
+ }
155
+ window.history.back();
156
+ }
157
+
158
+ export function navigateBrowserForward(): void {
159
+ if (!canBrowserNavigateForward()) {
160
+ return;
161
+ }
162
+ const navigationApi = getBrowserNavigationApi();
163
+ if (navigationApi?.forward !== undefined) {
164
+ void navigationApi.forward();
165
+ return;
166
+ }
167
+ window.history.forward();
168
+ }
@@ -0,0 +1,50 @@
1
+ export type BrowserNavigationType = 'navigate' | 'reload' | 'back_forward' | 'prerender' | 'unknown';
2
+
3
+ interface LegacyPerformanceNavigationLike {
4
+ readonly type?: number;
5
+ }
6
+
7
+ interface LegacyPerformanceLike {
8
+ readonly navigation?: LegacyPerformanceNavigationLike;
9
+ }
10
+
11
+ export function readBrowserNavigationType(
12
+ performanceLike: (Performance & LegacyPerformanceLike) | undefined = globalThis.performance as (Performance & LegacyPerformanceLike) | undefined,
13
+ ): BrowserNavigationType {
14
+ const navigationEntry = performanceLike?.getEntriesByType?.('navigation')?.[0] as PerformanceNavigationTiming | undefined;
15
+ if (navigationEntry !== undefined) {
16
+ switch (navigationEntry.type) {
17
+ case 'navigate':
18
+ case 'reload':
19
+ case 'back_forward':
20
+ case 'prerender':
21
+ return navigationEntry.type;
22
+ default:
23
+ return 'unknown';
24
+ }
25
+ }
26
+
27
+ switch (performanceLike?.navigation?.type) {
28
+ case 0:
29
+ return 'navigate';
30
+ case 1:
31
+ return 'reload';
32
+ case 2:
33
+ return 'back_forward';
34
+ default:
35
+ return 'unknown';
36
+ }
37
+ }
38
+
39
+ export function shouldRestoreInitialHistorySnapshot(
40
+ navigationType: BrowserNavigationType,
41
+ hasHistorySnapshotId: boolean,
42
+ ): boolean {
43
+ if (navigationType === 'back_forward') {
44
+ return true;
45
+ }
46
+ if (navigationType === 'navigate') {
47
+ return hasHistorySnapshotId;
48
+ }
49
+ return false;
50
+ }
@@ -0,0 +1,309 @@
1
+ import {
2
+ createPersistedUiStateStore,
3
+ PERSISTED_SCROLL_ENTRY_KIND,
4
+ PERSISTED_SCROLL_ENTRY_VERSION,
5
+ PERSISTED_UI_STATE_SNAPSHOT_SCHEMA_VERSION,
6
+ type PersistedScrollPayload,
7
+ type PersistedSnapshotEntry,
8
+ type PersistedSnapshotRecord,
9
+ type PersistedUiStateStore,
10
+ } from '../persisted-ui-state';
11
+ import { readBrowserNavigationType, shouldRestoreInitialHistorySnapshot } from './persisted-restore-policy';
12
+ import { readManagedHistoryState, setCurrentManagedHistorySnapshotId } from './managed-history';
13
+
14
+ function createPersistedSnapshotId(): string {
15
+ if (typeof crypto.randomUUID === 'function') {
16
+ return crypto.randomUUID();
17
+ }
18
+ const bytes = new Uint8Array(16);
19
+ crypto.getRandomValues(bytes);
20
+ return Array.from(bytes, (value) => value.toString(16).padStart(2, '0')).join('');
21
+ }
22
+
23
+ function buildPersistedSnapshotRecord(currentUrl: URL): PersistedSnapshotRecord {
24
+ const timestamp = Date.now();
25
+ return {
26
+ snapshotId: createPersistedSnapshotId(),
27
+ appKey: window.location.origin,
28
+ routeHref: currentUrl.href,
29
+ createdAt: timestamp,
30
+ lastAccessedAt: timestamp,
31
+ schemaVersion: PERSISTED_UI_STATE_SNAPSHOT_SCHEMA_VERSION,
32
+ entries: [],
33
+ };
34
+ }
35
+
36
+ export class PersistedUiStateController {
37
+ private readonly persistedUiState: PersistedUiStateStore | null = createPersistedUiStateStore();
38
+ private readonly currentPersistedEntries = new Map<string, PersistedSnapshotEntry>();
39
+ private persistedUiStateWork: Promise<unknown> = Promise.resolve();
40
+
41
+ private persistedEntryKey(kind: string, nodeId: string): string {
42
+ return `${kind}\n${nodeId}`;
43
+ }
44
+
45
+ private reportPersistedUiStateError(context: string, error: unknown): void {
46
+ const message = error instanceof Error ? error.stack ?? error.message : String(error);
47
+ console.error(`[fui_persist] ${context}: ${message}`);
48
+ }
49
+
50
+ private collectGarbage(context: string): void {
51
+ if (this.persistedUiState === null) {
52
+ return;
53
+ }
54
+ void this.persistedUiState.collectGarbage(Date.now()).catch((error: unknown) => {
55
+ this.reportPersistedUiStateError(`collecting garbage after ${context}`, error);
56
+ });
57
+ }
58
+
59
+ private async saveSnapshotForUrl(
60
+ url: URL,
61
+ context: string,
62
+ capture: (() => void) | undefined,
63
+ ): Promise<PersistedSnapshotRecord | null> {
64
+ if (this.persistedUiState === null) {
65
+ return null;
66
+ }
67
+ this.captureCurrentPersistedUiState(context, capture);
68
+ const record: PersistedSnapshotRecord = {
69
+ ...buildPersistedSnapshotRecord(url),
70
+ entries: Array.from(this.currentPersistedEntries.values()),
71
+ };
72
+ try {
73
+ await this.persistedUiState.saveSnapshot(record);
74
+ } catch (error: unknown) {
75
+ this.reportPersistedUiStateError(`saving snapshot while ${context}`, error);
76
+ return null;
77
+ }
78
+ this.collectGarbage(context);
79
+ return record;
80
+ }
81
+
82
+ clearCurrentPersistedEntries(): void {
83
+ this.currentPersistedEntries.clear();
84
+ }
85
+
86
+ hydrateCurrentPersistedEntries(snapshot: PersistedSnapshotRecord | null): void {
87
+ this.clearCurrentPersistedEntries();
88
+ if (snapshot === null) {
89
+ return;
90
+ }
91
+ for (const entry of snapshot.entries) {
92
+ this.currentPersistedEntries.set(this.persistedEntryKey(entry.kind, entry.nodeId), entry);
93
+ }
94
+ }
95
+
96
+ setCurrentPersistedEntry(entry: PersistedSnapshotEntry): void {
97
+ this.currentPersistedEntries.set(this.persistedEntryKey(entry.kind, entry.nodeId), entry);
98
+ }
99
+
100
+ setCurrentPersistedScrollEntry(nodeId: string, x: number, y: number): void {
101
+ this.setCurrentPersistedEntry({
102
+ nodeId,
103
+ kind: PERSISTED_SCROLL_ENTRY_KIND,
104
+ version: PERSISTED_SCROLL_ENTRY_VERSION,
105
+ payload: { x, y } satisfies PersistedScrollPayload,
106
+ });
107
+ }
108
+
109
+ setCurrentPersistedTextEntry(nodeId: string, kind: string, version: number, payload: string): void {
110
+ this.setCurrentPersistedEntry({
111
+ nodeId,
112
+ kind,
113
+ version,
114
+ payload,
115
+ });
116
+ }
117
+
118
+ getCurrentPersistedScrollEntry(nodeId: string): PersistedScrollPayload | null {
119
+ const entry = this.currentPersistedEntries.get(this.persistedEntryKey(PERSISTED_SCROLL_ENTRY_KIND, nodeId));
120
+ if (entry?.kind !== PERSISTED_SCROLL_ENTRY_KIND) {
121
+ return null;
122
+ }
123
+ const payload = entry.payload as { x?: unknown; y?: unknown } | null;
124
+ if (payload === null || typeof payload !== 'object') {
125
+ return null;
126
+ }
127
+ if (typeof payload.x !== 'number' || typeof payload.y !== 'number') {
128
+ return null;
129
+ }
130
+ return {
131
+ x: payload.x,
132
+ y: payload.y,
133
+ };
134
+ }
135
+
136
+ getCurrentPersistedTextEntry(nodeId: string, kind: string): { version: number; payload: string; } | null {
137
+ const entry = this.currentPersistedEntries.get(this.persistedEntryKey(kind, nodeId));
138
+ if (entry === undefined || entry.kind !== kind || typeof entry.payload !== 'string') {
139
+ return null;
140
+ }
141
+ return {
142
+ version: entry.version,
143
+ payload: entry.payload,
144
+ };
145
+ }
146
+
147
+ captureCurrentPersistedUiState(context: string, capture: (() => void) | undefined): void {
148
+ this.clearCurrentPersistedEntries();
149
+ if (capture === undefined) {
150
+ return;
151
+ }
152
+ try {
153
+ capture();
154
+ } catch (error: unknown) {
155
+ this.clearCurrentPersistedEntries();
156
+ this.reportPersistedUiStateError(`capturing persisted state while ${context}`, error);
157
+ }
158
+ }
159
+
160
+ restoreCurrentPersistedUiState(context: string, restore: (() => void) | undefined): void {
161
+ if (restore === undefined) {
162
+ return;
163
+ }
164
+ try {
165
+ restore();
166
+ } catch (error: unknown) {
167
+ this.reportPersistedUiStateError(`restoring persisted state while ${context}`, error);
168
+ }
169
+ }
170
+
171
+ queuePersistedUiStateWork<T>(work: () => Promise<T>): Promise<T> {
172
+ const next = this.persistedUiStateWork.then(work, work);
173
+ this.persistedUiStateWork = next.then(
174
+ () => undefined,
175
+ () => undefined,
176
+ );
177
+ return next;
178
+ }
179
+
180
+ async loadPersistedSnapshotById(snapshotId: string, context: string): Promise<PersistedSnapshotRecord | null> {
181
+ if (this.persistedUiState === null) {
182
+ return null;
183
+ }
184
+ try {
185
+ const snapshot = await this.persistedUiState.loadSnapshot(snapshotId);
186
+ if (snapshot === null) {
187
+ console.error(`[fui_persist] Missing snapshot ${snapshotId} while ${context}.`);
188
+ }
189
+ return snapshot;
190
+ } catch (error: unknown) {
191
+ this.reportPersistedUiStateError(`loading snapshot while ${context}`, error);
192
+ return null;
193
+ }
194
+ }
195
+
196
+ async loadCurrentHistoryEntrySnapshot(context: string): Promise<PersistedSnapshotRecord | null> {
197
+ const state = readManagedHistoryState();
198
+ if (state.uiSnapshotId === undefined) {
199
+ return null;
200
+ }
201
+ return this.loadPersistedSnapshotById(state.uiSnapshotId, context);
202
+ }
203
+
204
+ async loadRouteHeadSnapshot(routeHref: string, context: string): Promise<PersistedSnapshotRecord | null> {
205
+ if (this.persistedUiState === null) {
206
+ return null;
207
+ }
208
+ try {
209
+ const routeHead = await this.persistedUiState.loadRouteHead(window.location.origin, routeHref);
210
+ if (routeHead === null) {
211
+ return null;
212
+ }
213
+ return await this.loadPersistedSnapshotById(routeHead.snapshotId, `${context} via route head`);
214
+ } catch (error: unknown) {
215
+ this.reportPersistedUiStateError(`loading route head while ${context}`, error);
216
+ return null;
217
+ }
218
+ }
219
+
220
+ async loadSelectedPersistedSnapshot(
221
+ context: string,
222
+ routeHref: string = window.location.href,
223
+ ): Promise<PersistedSnapshotRecord | null> {
224
+ const fromHistory = await this.loadCurrentHistoryEntrySnapshot(context);
225
+ if (fromHistory !== null) {
226
+ return fromHistory;
227
+ }
228
+ return this.loadRouteHeadSnapshot(routeHref, context);
229
+ }
230
+
231
+ async loadPopPersistedSnapshot(
232
+ context: string,
233
+ routeHref: string = window.location.href,
234
+ ): Promise<PersistedSnapshotRecord | null> {
235
+ const [fromHistory, fromRouteHead] = await Promise.all([
236
+ this.loadCurrentHistoryEntrySnapshot(context),
237
+ this.loadRouteHeadSnapshot(routeHref, context),
238
+ ]);
239
+ if (fromHistory === null) {
240
+ return fromRouteHead;
241
+ }
242
+ if (fromRouteHead === null) {
243
+ return fromHistory;
244
+ }
245
+ return fromRouteHead.createdAt > fromHistory.createdAt
246
+ ? fromRouteHead
247
+ : fromHistory;
248
+ }
249
+
250
+ async loadInitialPersistedSnapshot(context: string): Promise<PersistedSnapshotRecord | null> {
251
+ const state = readManagedHistoryState();
252
+ const navigationType = readBrowserNavigationType();
253
+ const shouldRestore = shouldRestoreInitialHistorySnapshot(
254
+ navigationType,
255
+ state.uiSnapshotId !== undefined,
256
+ );
257
+ if (!shouldRestore) {
258
+ if (state.uiSnapshotId !== undefined) {
259
+ setCurrentManagedHistorySnapshotId(undefined);
260
+ }
261
+ return null;
262
+ }
263
+
264
+ const fromHistory = await this.loadCurrentHistoryEntrySnapshot(`${context} via ${navigationType}`);
265
+ if (fromHistory !== null) {
266
+ return fromHistory;
267
+ }
268
+ return navigationType === 'back_forward'
269
+ ? this.loadRouteHeadSnapshot(window.location.href, `${context} via ${navigationType}`)
270
+ : null;
271
+ }
272
+
273
+ async saveCurrentHistoryEntrySnapshot(
274
+ context: string,
275
+ capture: (() => void) | undefined,
276
+ ): Promise<string | null> {
277
+ const currentUrl = new URL(window.location.href);
278
+ const record = await this.saveSnapshotForUrl(currentUrl, context, capture);
279
+ if (record === null) {
280
+ return null;
281
+ }
282
+ setCurrentManagedHistorySnapshotId(record.snapshotId, currentUrl);
283
+ return record.snapshotId;
284
+ }
285
+
286
+ async saveRouteHeadSnapshotForHref(
287
+ routeHref: string,
288
+ context: string,
289
+ capture: (() => void) | undefined,
290
+ ): Promise<string | null> {
291
+ const record = await this.saveSnapshotForUrl(new URL(routeHref), context, capture);
292
+ return record?.snapshotId ?? null;
293
+ }
294
+
295
+ async ensureCurrentHistoryEntrySnapshot(
296
+ context: string,
297
+ capture: (() => void) | undefined,
298
+ ): Promise<string | null> {
299
+ const state = readManagedHistoryState();
300
+ if (state.uiSnapshotId === undefined) {
301
+ return this.saveCurrentHistoryEntrySnapshot(context, capture);
302
+ }
303
+ const loaded = await this.loadCurrentHistoryEntrySnapshot(context);
304
+ if (loaded !== null) {
305
+ return state.uiSnapshotId;
306
+ }
307
+ return this.saveCurrentHistoryEntrySnapshot(context, capture);
308
+ }
309
+ }