@zolomedia/bifrost-client 1.7.74

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 (140) hide show
  1. package/L1_Foundation/L1_Foundation.js +13 -0
  2. package/L1_Foundation/bootstrap/bootstrap.js +11 -0
  3. package/L1_Foundation/bootstrap/bootstrap_hooks.js +123 -0
  4. package/L1_Foundation/bootstrap/bootstrap_index.js +15 -0
  5. package/L1_Foundation/bootstrap/bootstrap_logger.js +135 -0
  6. package/L1_Foundation/bootstrap/cdn_loader.js +217 -0
  7. package/L1_Foundation/bootstrap/module_registry.js +102 -0
  8. package/L1_Foundation/bootstrap/prism_loader.js +164 -0
  9. package/L1_Foundation/config/client_config.js +110 -0
  10. package/L1_Foundation/config/config.js +7 -0
  11. package/L1_Foundation/connection/connection.js +8 -0
  12. package/L1_Foundation/connection/websocket_connection.js +122 -0
  13. package/L1_Foundation/constants/bifrost_constants.js +284 -0
  14. package/L1_Foundation/constants/constants.js +7 -0
  15. package/L1_Foundation/logger/logger.js +10 -0
  16. package/L2_Handling/L2_Handling.js +15 -0
  17. package/L2_Handling/cache/cache.js +22 -0
  18. package/L2_Handling/cache/cache_constants.js +69 -0
  19. package/L2_Handling/cache/orchestration/cache_manager.js +299 -0
  20. package/L2_Handling/cache/orchestration/cache_orchestrator.js +260 -0
  21. package/L2_Handling/cache/orchestration/orchestration.js +12 -0
  22. package/L2_Handling/cache/storage/session_manager.js +289 -0
  23. package/L2_Handling/cache/storage/storage.js +10 -0
  24. package/L2_Handling/cache/storage/storage_manager.js +590 -0
  25. package/L2_Handling/display/composite/composite.js +13 -0
  26. package/L2_Handling/display/composite/dashboard_renderer.js +221 -0
  27. package/L2_Handling/display/composite/swiper_renderer.js +564 -0
  28. package/L2_Handling/display/composite/terminal_renderer.js +922 -0
  29. package/L2_Handling/display/composite/wizard_conditional_renderer.js +274 -0
  30. package/L2_Handling/display/display.js +30 -0
  31. package/L2_Handling/display/feedback/feedback.js +11 -0
  32. package/L2_Handling/display/feedback/progressbar_renderer.js +418 -0
  33. package/L2_Handling/display/feedback/spinner_renderer.js +246 -0
  34. package/L2_Handling/display/inputs/button_renderer.js +634 -0
  35. package/L2_Handling/display/inputs/form_renderer.js +583 -0
  36. package/L2_Handling/display/inputs/input_renderer.js +658 -0
  37. package/L2_Handling/display/inputs/inputs.js +12 -0
  38. package/L2_Handling/display/navigation/menu_renderer.js +206 -0
  39. package/L2_Handling/display/navigation/navigation.js +11 -0
  40. package/L2_Handling/display/navigation/navigation_renderer.js +703 -0
  41. package/L2_Handling/display/orchestration/orchestration.js +11 -0
  42. package/L2_Handling/display/orchestration/renderer.js +430 -0
  43. package/L2_Handling/display/orchestration/zdisplay_orchestrator.js +1759 -0
  44. package/L2_Handling/display/outputs/alert_renderer.js +161 -0
  45. package/L2_Handling/display/outputs/audio_renderer.js +94 -0
  46. package/L2_Handling/display/outputs/card_renderer.js +229 -0
  47. package/L2_Handling/display/outputs/code_renderer.js +66 -0
  48. package/L2_Handling/display/outputs/dl_renderer.js +131 -0
  49. package/L2_Handling/display/outputs/header_renderer.js +162 -0
  50. package/L2_Handling/display/outputs/icon_renderer.js +107 -0
  51. package/L2_Handling/display/outputs/image_renderer.js +145 -0
  52. package/L2_Handling/display/outputs/list_renderer.js +190 -0
  53. package/L2_Handling/display/outputs/outputs.js +19 -0
  54. package/L2_Handling/display/outputs/table_renderer.js +765 -0
  55. package/L2_Handling/display/outputs/text_renderer.js +818 -0
  56. package/L2_Handling/display/outputs/typography_renderer.js +293 -0
  57. package/L2_Handling/display/outputs/video_renderer.js +116 -0
  58. package/L2_Handling/display/primitives/document_structure_primitives.js +319 -0
  59. package/L2_Handling/display/primitives/form_primitives.js +526 -0
  60. package/L2_Handling/display/primitives/generic_containers.js +109 -0
  61. package/L2_Handling/display/primitives/interactive_primitives.js +305 -0
  62. package/L2_Handling/display/primitives/link_primitives.js +552 -0
  63. package/L2_Handling/display/primitives/lists_primitives.js +262 -0
  64. package/L2_Handling/display/primitives/media_primitives.js +383 -0
  65. package/L2_Handling/display/primitives/primitives.js +19 -0
  66. package/L2_Handling/display/primitives/semantic_element_primitive.js +226 -0
  67. package/L2_Handling/display/primitives/table_primitives.js +528 -0
  68. package/L2_Handling/display/primitives/typography_primitives.js +175 -0
  69. package/L2_Handling/display/specialized/input_request_renderer.js +467 -0
  70. package/L2_Handling/display/specialized/specialized.js +10 -0
  71. package/L2_Handling/hooks/hooks.js +9 -0
  72. package/L2_Handling/hooks/menu_integration.js +57 -0
  73. package/L2_Handling/hooks/widget_hook_manager.js +292 -0
  74. package/L2_Handling/message/message.js +8 -0
  75. package/L2_Handling/message/message_handler.js +701 -0
  76. package/L2_Handling/navigation/navigation.js +8 -0
  77. package/L2_Handling/navigation/navigation_manager.js +403 -0
  78. package/L2_Handling/zhooks/features/cache_live.js +287 -0
  79. package/L2_Handling/zhooks/features/crumbs_live.js +292 -0
  80. package/L2_Handling/zhooks/zhooks_manager.js +65 -0
  81. package/L2_Handling/zvaf/zvaf.js +8 -0
  82. package/L2_Handling/zvaf/zvaf_manager.js +334 -0
  83. package/L3_Abstraction/L3_Abstraction.js +12 -0
  84. package/L3_Abstraction/orchestrator/container_unwrapper.js +101 -0
  85. package/L3_Abstraction/orchestrator/group_renderer.js +698 -0
  86. package/L3_Abstraction/orchestrator/input_event_handler.js +797 -0
  87. package/L3_Abstraction/orchestrator/metadata_processor.js +249 -0
  88. package/L3_Abstraction/orchestrator/navbar_builder.js +201 -0
  89. package/L3_Abstraction/orchestrator/orchestrator.js +13 -0
  90. package/L3_Abstraction/orchestrator/wizard_gate_handler.js +360 -0
  91. package/L3_Abstraction/renderer/renderer.js +1 -0
  92. package/L3_Abstraction/session/session.js +1 -0
  93. package/L4_Orchestration/L4_Orchestration.js +11 -0
  94. package/L4_Orchestration/client/client.js +1 -0
  95. package/L4_Orchestration/facade/facade.js +9 -0
  96. package/L4_Orchestration/facade/manager_registry.js +118 -0
  97. package/L4_Orchestration/facade/renderer_registry.js +274 -0
  98. package/L4_Orchestration/lifecycle/asset_loader.js +255 -0
  99. package/L4_Orchestration/lifecycle/initializer.js +135 -0
  100. package/L4_Orchestration/lifecycle/lifecycle.js +8 -0
  101. package/L4_Orchestration/rendering/facade.js +94 -0
  102. package/L4_Orchestration/rendering/rendering.js +7 -0
  103. package/LICENSE +21 -0
  104. package/README.md +82 -0
  105. package/bifrost_client.js +204 -0
  106. package/bifrost_core.js +1686 -0
  107. package/docs/ARCHITECTURE.md +111 -0
  108. package/docs/PROTOCOL.md +106 -0
  109. package/docs/RENDERERS.md +101 -0
  110. package/docs/SECURITY.md +92 -0
  111. package/package.json +24 -0
  112. package/syntax/prism-zconfig.js +41 -0
  113. package/syntax/prism-zenv.js +69 -0
  114. package/syntax/prism-zolo-theme.css +288 -0
  115. package/syntax/prism-zolo.js +380 -0
  116. package/syntax/prism-zschema.js +38 -0
  117. package/syntax/prism-zspark.js +25 -0
  118. package/syntax/prism-zui.js +68 -0
  119. package/zSys/accessibility/accessibility.js +10 -0
  120. package/zSys/accessibility/emoji_accessibility.js +173 -0
  121. package/zSys/dom/block_utils.js +122 -0
  122. package/zSys/dom/container_utils.js +370 -0
  123. package/zSys/dom/dom.js +13 -0
  124. package/zSys/dom/dom_utils.js +328 -0
  125. package/zSys/dom/encoding_utils.js +117 -0
  126. package/zSys/dom/style_utils.js +71 -0
  127. package/zSys/errors/error_display.js +299 -0
  128. package/zSys/errors/errors.js +10 -0
  129. package/zSys/theme/color_utils.js +274 -0
  130. package/zSys/theme/dark_mode_utils.js +272 -0
  131. package/zSys/theme/size_utils.js +256 -0
  132. package/zSys/theme/spacing_utils.js +405 -0
  133. package/zSys/theme/theme.js +14 -0
  134. package/zSys/theme/zbase.css +1735 -0
  135. package/zSys/theme/zbase_inject.js +161 -0
  136. package/zSys/theme/ztheme_utils.js +305 -0
  137. package/zSys/validation/error_boundary.js +201 -0
  138. package/zSys/validation/validation.js +11 -0
  139. package/zSys/validation/validation_utils.js +238 -0
  140. package/zSys/zSys.js +14 -0
@@ -0,0 +1,299 @@
1
+ /**
2
+ * CacheManager - Manages offline-first caching, storage, and session
3
+ *
4
+ * Responsibilities:
5
+ * - Initialize StorageManager, SessionManager, TrailStore (CacheOrchestrator)
6
+ * - Register cache-related hooks (onConnectionInfo, onDisconnected, onConnected)
7
+ * - Handle offline/online transitions
8
+ * - Disable/enable forms during offline mode
9
+ * - Dynamic script loading for cache modules
10
+ *
11
+ * Extracted from bifrost_client.js (Phase 3.1)
12
+ */
13
+
14
+ export class CacheManager {
15
+ constructor(client) {
16
+ this.client = client;
17
+ this.logger = client.logger;
18
+ this.hooks = client.hooks;
19
+ this._baseUrl = client._baseUrl;
20
+ }
21
+
22
+ /**
23
+ * Initialize cache system (v1.6.0)
24
+ * Loads StorageManager, SessionManager, and CacheOrchestrator
25
+ */
26
+ async initCacheSystem() {
27
+ try {
28
+ // Check if running in browser
29
+ if (typeof window === 'undefined') {
30
+ this.logger.debug('[Cache] Skipping (not in browser)');
31
+ return;
32
+ }
33
+
34
+ this.logger.debug('[Cache] Loading cache modules');
35
+
36
+ // Dynamically load cache modules (maintains single-import philosophy)
37
+ await this.loadScript(`${this._baseUrl}L2_Handling/cache/storage/storage_manager.js`);
38
+ await this.loadScript(`${this._baseUrl}L2_Handling/cache/storage/session_manager.js`);
39
+ await this.loadScript(`${this._baseUrl}L2_Handling/cache/orchestration/cache_orchestrator.js`);
40
+
41
+ // Verify modules loaded
42
+ if (typeof window.StorageManager === 'undefined' ||
43
+ typeof window.SessionManager === 'undefined' ||
44
+ typeof window.CacheOrchestrator === 'undefined') {
45
+ this.logger.debug('[Cache] Module loading failed, cache disabled');
46
+ return;
47
+ }
48
+
49
+ this.logger.debug('[Cache] Cache modules loaded');
50
+
51
+ // Initialize storage
52
+ this.client.storage = new window.StorageManager('zBifrost', this.logger);
53
+ await this.client.storage.init();
54
+ this.logger.debug('[Cache] Storage initialized');
55
+
56
+ // Initialize session
57
+ this.client.session = new window.SessionManager(this.client.storage, this.logger);
58
+ await this.client.session.init();
59
+ this.logger.debug('[Cache] Session initialized');
60
+
61
+ // Initialize the visited-page trail store (TrailStore, exposed as
62
+ // window.CacheOrchestrator for back-compat). This is the offline-browse
63
+ // engine — the only client cache now that the server is the cache of record.
64
+ this.client.cache = new window.CacheOrchestrator(this.client.storage, this.client.session, this.logger);
65
+ await this.client.cache.init();
66
+ this.logger.debug('[Cache] Trail store initialized');
67
+
68
+ // Clear rendered-HTML cache on every cold page load.
69
+ // The rendered cache stores panel HTML for SPA tab-switching within one
70
+ // page lifetime. Persisting it across page reloads (goto()) causes the
71
+ // dashboard structure (.zDash-container) to be skipped — cached panel
72
+ // content is injected without the onZDash wrapper being re-built.
73
+ try {
74
+ await this.client.cache.clear('rendered');
75
+ this.logger.debug('[Cache] Rendered cache cleared (cold start)');
76
+ } catch(e) {
77
+ this.logger.debug('[Cache] Could not clear rendered cache:', e?.message);
78
+ }
79
+
80
+ // Single summary log
81
+ this.logger.debug('[Cache] Cache system ready (storage, session, trail store)');
82
+
83
+ } catch (error) {
84
+ this.logger.error('[Cache] Initialization error:', error);
85
+ // Non-fatal: cache is optional, BifrostClient will work without it
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Dynamically load a script (v1.6.0)
91
+ * @param {string} src - Script URL
92
+ */
93
+ loadScript(src) {
94
+ return new Promise((resolve, reject) => {
95
+ // Check if already loaded
96
+ if (document.querySelector(`script[src="${src}"]`)) {
97
+ resolve();
98
+ return;
99
+ }
100
+
101
+ // Note: Script loading uses native createElement (not a primitive, as scripts are not visual elements)
102
+ const script = document.createElement('script');
103
+ script.src = src;
104
+ script.onload = () => resolve();
105
+ script.onerror = () => reject(new Error(`Failed to load ${src}`));
106
+ document.head.appendChild(script);
107
+ });
108
+ }
109
+
110
+ /**
111
+ * Register cache-related hooks
112
+ */
113
+ registerCacheHooks() {
114
+ // v1.6.0: Register hook to populate session from connection_info
115
+ this.hooks.register('onConnectionInfo', async (data) => {
116
+ try {
117
+ // Blue-green / scale-to-zero resume: remember our server session id so a
118
+ // reconnect (auto-reconnect lands on a swapped-in instance) can present it
119
+ // and resume the same session instead of re-authenticating from scratch.
120
+ // Per-tab (sessionStorage) — cleared on tab close, survives WS reconnects.
121
+ try {
122
+ const resumeId = data && data.auth && data.auth.bifrost_session
123
+ && data.auth.bifrost_session.full_id;
124
+ if (resumeId) sessionStorage.setItem('zOS_resume_id', resumeId);
125
+ } catch (_) { /* sessionStorage unavailable — resume simply won't engage */ }
126
+
127
+ // 2A: Server-version-gated cache bust
128
+ // server_version is sent on every connect — if it changed since last load,
129
+ // the browser may be serving stale JS modules (304 cache). Reload to flush.
130
+ const incomingVersion = data.server_version;
131
+ if (incomingVersion) {
132
+ const storedVersion = sessionStorage.getItem('zOS_server_version');
133
+ if (storedVersion && storedVersion !== incomingVersion) {
134
+ this.logger.log(`[Cache] Server version changed (${storedVersion} → ${incomingVersion}), reloading to bust JS cache`);
135
+ sessionStorage.setItem('zOS_server_version', incomingVersion);
136
+ window.location.reload();
137
+ return;
138
+ }
139
+ sessionStorage.setItem('zOS_server_version', incomingVersion);
140
+ }
141
+
142
+ if (!this.client.session) {
143
+ this.logger.debug('[Cache] Session not initialized yet, skipping');
144
+ return;
145
+ }
146
+
147
+ const sessionData = data.session;
148
+ if (!sessionData) {
149
+ this.logger.debug('[Cache] No session data in connection_info');
150
+ return;
151
+ }
152
+
153
+ // Get OLD auth state before updating
154
+ const wasAuthenticated = this.client.session.isAuthenticated();
155
+ const oldSessionHash = this.client.session.getHash();
156
+
157
+ // Populate session with backend data
158
+ if (sessionData.authenticated && sessionData.session_hash) {
159
+ await this.client.session.setPublicData({
160
+ username: sessionData.username,
161
+ role: sessionData.role,
162
+ session_hash: sessionData.session_hash,
163
+ app: sessionData.active_app
164
+ });
165
+ this.logger.log(`[Cache] Session populated: ${sessionData.username} (${sessionData.role})`);
166
+ } else {
167
+ this.logger.debug('[Cache] User not authenticated, session remains anonymous');
168
+ }
169
+
170
+ // Get NEW auth state after updating
171
+ const isNowAuthenticated = this.client.session.isAuthenticated();
172
+ const newSessionHash = this.client.session.getHash();
173
+
174
+ // Per-page opt-out wins (SSOT): connection_info.nav_html is the
175
+ // CONNECTION-level (global, page-agnostic) navbar. The rendered page's
176
+ // own zui-config is the authority for THIS page — if it explicitly set
177
+ // zNavBar:false, the connection default must NOT override it. Without
178
+ // this guard, a refresh on a zNavBar:false landing re-injects the global
179
+ // navbar on (re)connect (SPA nav never reconnects, so it only showed on
180
+ // refresh). Mirrors the SPA-nav + server-side explicit-false opt-out.
181
+ const pageZNavBar = this.client.zuiConfig?.zMeta?.zNavBar;
182
+ const pageOptedOut = pageZNavBar === false || pageZNavBar === 'false';
183
+
184
+ // 3A: If server sent nav_html in connection_info, refresh the navbar —
185
+ // unless this page opted out (then hide the chrome and skip).
186
+ const navHtml = data.nav_html || null;
187
+ if (pageOptedOut) {
188
+ if (this.client._zNavBarElement) {
189
+ this.client._zNavBarElement.style.display = 'none';
190
+ this.client._zNavBarElement.innerHTML = '';
191
+ }
192
+ this.logger.log('[NavBar] Page set zNavBar:false — skipping connection_info navbar');
193
+ } else if (navHtml) {
194
+ await this.client._fetchAndPopulateNavBar(navHtml);
195
+ this.logger.log('[NavBar] Navbar refreshed from connection_info nav_html (3A)');
196
+ } else if (wasAuthenticated !== isNowAuthenticated || oldSessionHash !== newSessionHash) {
197
+ // Legacy path: auth change detected — re-fetch from API
198
+ this.logger.log('[NavBar] Auth state changed - fetching fresh navbar from API');
199
+ await this.client._fetchAndPopulateNavBar();
200
+ this.logger.log('[NavBar] Navbar updated after auth change');
201
+ }
202
+ } catch (error) {
203
+ this.logger.error('[Cache] Error populating session:', error);
204
+ }
205
+ });
206
+
207
+ // v1.6.0: Offline-first - Handle disconnect + Badge update (combined hook)
208
+ this.hooks.register('onDisconnected', async (_reason) => {
209
+ try {
210
+ this.logger.debug('[Cache] Connection lost, entering offline mode');
211
+
212
+ // Update badge (v1.6.0: Combined with cache hook to avoid conflicts)
213
+ await this.client._updateBadgeState('disconnected');
214
+
215
+ // Freeze the current page into the trail so Back/forward keep working
216
+ // while the socket is down (offline-browse).
217
+ if (this.client.cache && typeof document !== 'undefined') {
218
+ await this.client._snapshotCurrentPage();
219
+ }
220
+
221
+ // Disable forms (prevent data loss)
222
+ this.disableForms();
223
+
224
+ } catch (error) {
225
+ this.logger.error('[Cache] Error handling disconnect:', error);
226
+ }
227
+ });
228
+
229
+ // v1.6.0: Offline-first - Handle reconnect + Badge update (combined hook)
230
+ this.hooks.register('onConnected', async (_data) => {
231
+ try {
232
+ this.logger.debug('[Cache] Connection restored, exiting offline mode');
233
+
234
+ // Update badge (v1.6.0: Combined with cache hook to avoid conflicts)
235
+ await this.client._updateBadgeState('connected');
236
+
237
+ // Re-enable forms
238
+ this.enableForms();
239
+
240
+ // Offline-browse: if the user requested a never-seen page while down and
241
+ // is sitting on the "you're offline" notice, fulfill it now that the line
242
+ // is back — they never have to retry by hand.
243
+ const pending = this.client._pendingOfflineNav;
244
+ if (pending) {
245
+ this.client._pendingOfflineNav = null;
246
+ this.logger.log(`[Cache] Reconnected — fulfilling pending nav: ${pending}`);
247
+ try {
248
+ // URL was never pushed for the offline notice, so let this nav push it.
249
+ await this.client._navigateToRoute(pending);
250
+ } catch (navErr) {
251
+ this.logger.error('[Cache] Pending nav retry failed:', navErr);
252
+ }
253
+ }
254
+
255
+ } catch (error) {
256
+ this.logger.error('[Cache] Error handling reconnect:', error);
257
+ }
258
+ });
259
+ }
260
+
261
+ /**
262
+ * Disable all forms during offline mode (v1.6.0)
263
+ */
264
+ disableForms() {
265
+ if (typeof document === 'undefined') {
266
+ return;
267
+ }
268
+
269
+ const forms = document.querySelectorAll('form');
270
+ forms.forEach(form => {
271
+ form.setAttribute('data-offline-disabled', 'true');
272
+ const inputs = form.querySelectorAll('input, textarea, select, button');
273
+ inputs.forEach(input => input.disabled = true);
274
+ });
275
+
276
+ this.logger.log('[Offline] [WARN] Forms disabled (offline mode)');
277
+ }
278
+
279
+ /**
280
+ * Re-enable forms after reconnecting (v1.6.0)
281
+ */
282
+ enableForms() {
283
+ if (typeof document === 'undefined') {
284
+ return;
285
+ }
286
+
287
+ const forms = document.querySelectorAll('form[data-offline-disabled]');
288
+ forms.forEach(form => {
289
+ form.removeAttribute('data-offline-disabled');
290
+ const inputs = form.querySelectorAll('input, textarea, select, button');
291
+ inputs.forEach(input => input.disabled = false);
292
+ });
293
+
294
+ this.logger.debug('[Offline] Forms re-enabled');
295
+ }
296
+ }
297
+
298
+ export default CacheManager;
299
+
@@ -0,0 +1,260 @@
1
+ /**
2
+ *
3
+ * TrailStore — the client's visited-trail of rendered pages (offline-browse engine)
4
+ *
5
+ *
6
+ * SSOT: the server (zLoader) is the single cache of record. The browser is a
7
+ * renderer, not a second zLoader. This store holds ONE thing: the rendered
8
+ * output of pages the user has actually visited, keyed by route path, persisted
9
+ * in IndexedDB (via StorageManager). It is the replacement for the bfcache the
10
+ * browser gives a normal MPA for free but cannot give a WebSocket-driven SPA:
11
+ * it lets Back/forward — and navigation while the socket is down — replay a
12
+ * page the user already saw, with no server round-trip.
13
+ *
14
+ * A replayed page is STALE RENDER OUTPUT, never authority:
15
+ * - every entry is stamped with the session_hash and dropped when it changes
16
+ * - a TTL caps how stale a replay can be
17
+ * - the trail is LRU-capped so it can't grow unbounded
18
+ *
19
+ * Static assets (CSS/JS/images/fonts) are NOT stored here — the browser's
20
+ * native HTTP cache (SHA-pinned, immutable) already owns those.
21
+ *
22
+ * Back-compat: exported as both `TrailStore` and `CacheOrchestrator` (the
23
+ * loader and `client.cache` still reference the latter name).
24
+ *
25
+ * @version 2.0.0
26
+ *
27
+ */
28
+
29
+ (function(root, factory) {
30
+ if (typeof define === 'function' && define.amd) {
31
+ define([], factory);
32
+ } else if (typeof module === 'object' && module.exports) {
33
+ module.exports = factory();
34
+ } else {
35
+ const TrailStore = factory();
36
+ root.TrailStore = TrailStore;
37
+ root.CacheOrchestrator = TrailStore; // back-compat alias
38
+ }
39
+ }(typeof self !== 'undefined' ? self : this, () => {
40
+ 'use strict';
41
+
42
+ // SSOT lives in ../cache_constants.js; duplicated here for UMD compatibility.
43
+ const TIER = 'rendered';
44
+ const TRAIL_TTL = 86400000; // 24h
45
+ const TRAIL_LIMIT = 50;
46
+
47
+ class TrailStore {
48
+ /**
49
+ * @param {StorageManager} storage - persistent backing store (IndexedDB)
50
+ * @param {SessionManager} session - identity (for session_hash gating)
51
+ * @param {Object} logger - optional logger
52
+ */
53
+ constructor(storage, session, logger = null) {
54
+ if (!storage) {
55
+ throw new Error('[TrailStore] StorageManager required');
56
+ }
57
+ if (!session) {
58
+ throw new Error('[TrailStore] SessionManager required');
59
+ }
60
+ this.storage = storage;
61
+ this.session = session;
62
+ this.logger = logger || console;
63
+ this.initialized = false;
64
+ this.logger.debug('[TrailStore] Created');
65
+ }
66
+
67
+ async init() {
68
+ if (this.initialized) {
69
+ return true;
70
+ }
71
+ // StorageManager owns IndexedDB init; it may already be initialized by the
72
+ // CacheManager. Calling init() again is a no-op there.
73
+ try {
74
+ if (typeof this.storage.init === 'function') {
75
+ await this.storage.init();
76
+ }
77
+ // Drop trail on identity change (pages are session-scoped).
78
+ if (this.session && typeof this.session.addListener === 'function') {
79
+ this.session.addListener((event) => {
80
+ if (event === 'session_changed' || event === 'session_cleared') {
81
+ this.logger.debug(`[TrailStore] ${event} → clearing trail`);
82
+ this.clear();
83
+ }
84
+ });
85
+ }
86
+ this.initialized = true;
87
+ this.logger.debug('[TrailStore] Initialized (rendered trail only)');
88
+ return true;
89
+ } catch (error) {
90
+ this.logger.debug('[TrailStore] Init failed:', error);
91
+ this.initialized = false;
92
+ return false;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Read a trail entry. Returns the stored value, or null if missing / expired
98
+ * / from a different session.
99
+ * @param {string} key - route path
100
+ */
101
+ async get(key) {
102
+ if (!this.initialized) {
103
+ return null;
104
+ }
105
+ const entry = await this.storage.get(key, TIER);
106
+ if (!entry) {
107
+ return null;
108
+ }
109
+ if (this._isExpired(entry)) {
110
+ await this.storage.remove(key, TIER);
111
+ this.logger.debug(`[TrailStore] Expired: ${key}`);
112
+ return null;
113
+ }
114
+ if (!this._isValidSession(entry)) {
115
+ await this.storage.remove(key, TIER);
116
+ this.logger.debug(`[TrailStore] Session mismatch: ${key}`);
117
+ return null;
118
+ }
119
+ return entry.value;
120
+ }
121
+
122
+ /**
123
+ * Write a rendered page into the trail (LRU-capped, session-stamped).
124
+ * @param {string} key - route path
125
+ * @param {any} value - rendered payload (HTML string / structured snapshot)
126
+ */
127
+ async set(key, value) {
128
+ if (!this.initialized) {
129
+ return false;
130
+ }
131
+ const entry = {
132
+ value: value,
133
+ timestamp: Date.now(),
134
+ session_hash: this.session && typeof this.session.getHash === 'function'
135
+ ? this.session.getHash()
136
+ : null
137
+ };
138
+ const ok = await this.storage.set(key, entry, TIER);
139
+ if (ok) {
140
+ await this._enforceLimit();
141
+ this.logger.debug(`[TrailStore] Stored: ${key}`);
142
+ }
143
+ return ok;
144
+ }
145
+
146
+ /** @param {string} key - route path */
147
+ async has(key) {
148
+ return (await this.get(key)) !== null;
149
+ }
150
+
151
+ /** @param {string} key - route path */
152
+ async remove(key) {
153
+ if (!this.initialized) {
154
+ return false;
155
+ }
156
+ return await this.storage.remove(key, TIER);
157
+ }
158
+
159
+ /**
160
+ * Clear the trail. Accepts an optional tier arg for back-compat with callers
161
+ * that pass 'rendered'; there is only one tier now, so it always clears it.
162
+ */
163
+ async clear() {
164
+ if (!this.initialized) {
165
+ return false;
166
+ }
167
+ await this.storage.clear(TIER);
168
+ this.logger.debug('[TrailStore] Cleared trail');
169
+ return true;
170
+ }
171
+
172
+ /** Back-compat alias — only one tier exists. */
173
+ async clearAll() {
174
+ return this.clear();
175
+ }
176
+
177
+ /** Back-compat alias — trail is already session-scoped. */
178
+ async clearOnSessionChange() {
179
+ return this.clear();
180
+ }
181
+
182
+ /**
183
+ * List trail keys (route paths currently cached). Useful for diagnostics and
184
+ * for the navigator deciding whether a Back target can be replayed offline.
185
+ */
186
+ async keys() {
187
+ if (!this.initialized) {
188
+ return [];
189
+ }
190
+ return await this.storage.keys(TIER);
191
+ }
192
+
193
+ async getStats() {
194
+ if (!this.initialized) {
195
+ return {};
196
+ }
197
+ const keys = await this.keys();
198
+ return { rendered: { size: keys.length, limit: TRAIL_LIMIT } };
199
+ }
200
+
201
+ /**
202
+ * Back-compat shim. The old multi-tier API exposed getTier(); the trail is
203
+ * the only tier now, so callers get this store back.
204
+ */
205
+ getTier() {
206
+ return this.initialized ? this : null;
207
+ }
208
+
209
+ // ── private ──────────────────────────────────────────────────────────────
210
+
211
+ _isExpired(entry) {
212
+ if (!entry || typeof entry.timestamp !== 'number') {
213
+ return true;
214
+ }
215
+ return (Date.now() - entry.timestamp) > TRAIL_TTL;
216
+ }
217
+
218
+ _isValidSession(entry) {
219
+ const current = this.session && typeof this.session.getHash === 'function'
220
+ ? this.session.getHash()
221
+ : null;
222
+ // No current session yet → accept (anonymous browsing).
223
+ if (!current) {
224
+ return true;
225
+ }
226
+ // Entry predates the session system → reject.
227
+ if (!entry.session_hash) {
228
+ return false;
229
+ }
230
+ return entry.session_hash === current;
231
+ }
232
+
233
+ /**
234
+ * Enforce the LRU cap: if the trail exceeds TRAIL_LIMIT, evict the oldest
235
+ * entries by write timestamp. Relies on StorageManager.getAll() exposing the
236
+ * per-entry timestamp.
237
+ */
238
+ async _enforceLimit() {
239
+ try {
240
+ if (typeof this.storage.getAll !== 'function') {
241
+ return;
242
+ }
243
+ const all = await this.storage.getAll(TIER);
244
+ if (!Array.isArray(all) || all.length <= TRAIL_LIMIT) {
245
+ return;
246
+ }
247
+ all.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
248
+ const evictCount = all.length - TRAIL_LIMIT;
249
+ for (let i = 0; i < evictCount; i++) {
250
+ await this.storage.remove(all[i].key, TIER);
251
+ }
252
+ this.logger.debug(`[TrailStore] LRU evicted ${evictCount} entr${evictCount === 1 ? 'y' : 'ies'}`);
253
+ } catch (err) {
254
+ this.logger.debug('[TrailStore] LRU enforce skipped:', err && err.message);
255
+ }
256
+ }
257
+ }
258
+
259
+ return TrailStore;
260
+ }));
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Orchestration Module - Barrel Export
3
+ *
4
+ * Trail coordination (TrailStore / CacheOrchestrator alias) + lifecycle wiring
5
+ * (CacheManager). The HTTP conditional-request manager was removed in the SSOT
6
+ * collapse — static assets are cached by the browser, pages by the server.
7
+ *
8
+ * @module caching/orchestration
9
+ */
10
+
11
+ export { CacheOrchestrator } from './cache_orchestrator.js';
12
+ export { CacheManager } from './cache_manager.js';