@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.
- package/L1_Foundation/L1_Foundation.js +13 -0
- package/L1_Foundation/bootstrap/bootstrap.js +11 -0
- package/L1_Foundation/bootstrap/bootstrap_hooks.js +123 -0
- package/L1_Foundation/bootstrap/bootstrap_index.js +15 -0
- package/L1_Foundation/bootstrap/bootstrap_logger.js +135 -0
- package/L1_Foundation/bootstrap/cdn_loader.js +217 -0
- package/L1_Foundation/bootstrap/module_registry.js +102 -0
- package/L1_Foundation/bootstrap/prism_loader.js +164 -0
- package/L1_Foundation/config/client_config.js +110 -0
- package/L1_Foundation/config/config.js +7 -0
- package/L1_Foundation/connection/connection.js +8 -0
- package/L1_Foundation/connection/websocket_connection.js +122 -0
- package/L1_Foundation/constants/bifrost_constants.js +284 -0
- package/L1_Foundation/constants/constants.js +7 -0
- package/L1_Foundation/logger/logger.js +10 -0
- package/L2_Handling/L2_Handling.js +15 -0
- package/L2_Handling/cache/cache.js +22 -0
- package/L2_Handling/cache/cache_constants.js +69 -0
- package/L2_Handling/cache/orchestration/cache_manager.js +299 -0
- package/L2_Handling/cache/orchestration/cache_orchestrator.js +260 -0
- package/L2_Handling/cache/orchestration/orchestration.js +12 -0
- package/L2_Handling/cache/storage/session_manager.js +289 -0
- package/L2_Handling/cache/storage/storage.js +10 -0
- package/L2_Handling/cache/storage/storage_manager.js +590 -0
- package/L2_Handling/display/composite/composite.js +13 -0
- package/L2_Handling/display/composite/dashboard_renderer.js +221 -0
- package/L2_Handling/display/composite/swiper_renderer.js +564 -0
- package/L2_Handling/display/composite/terminal_renderer.js +922 -0
- package/L2_Handling/display/composite/wizard_conditional_renderer.js +274 -0
- package/L2_Handling/display/display.js +30 -0
- package/L2_Handling/display/feedback/feedback.js +11 -0
- package/L2_Handling/display/feedback/progressbar_renderer.js +418 -0
- package/L2_Handling/display/feedback/spinner_renderer.js +246 -0
- package/L2_Handling/display/inputs/button_renderer.js +634 -0
- package/L2_Handling/display/inputs/form_renderer.js +583 -0
- package/L2_Handling/display/inputs/input_renderer.js +658 -0
- package/L2_Handling/display/inputs/inputs.js +12 -0
- package/L2_Handling/display/navigation/menu_renderer.js +206 -0
- package/L2_Handling/display/navigation/navigation.js +11 -0
- package/L2_Handling/display/navigation/navigation_renderer.js +703 -0
- package/L2_Handling/display/orchestration/orchestration.js +11 -0
- package/L2_Handling/display/orchestration/renderer.js +430 -0
- package/L2_Handling/display/orchestration/zdisplay_orchestrator.js +1759 -0
- package/L2_Handling/display/outputs/alert_renderer.js +161 -0
- package/L2_Handling/display/outputs/audio_renderer.js +94 -0
- package/L2_Handling/display/outputs/card_renderer.js +229 -0
- package/L2_Handling/display/outputs/code_renderer.js +66 -0
- package/L2_Handling/display/outputs/dl_renderer.js +131 -0
- package/L2_Handling/display/outputs/header_renderer.js +162 -0
- package/L2_Handling/display/outputs/icon_renderer.js +107 -0
- package/L2_Handling/display/outputs/image_renderer.js +145 -0
- package/L2_Handling/display/outputs/list_renderer.js +190 -0
- package/L2_Handling/display/outputs/outputs.js +19 -0
- package/L2_Handling/display/outputs/table_renderer.js +765 -0
- package/L2_Handling/display/outputs/text_renderer.js +818 -0
- package/L2_Handling/display/outputs/typography_renderer.js +293 -0
- package/L2_Handling/display/outputs/video_renderer.js +116 -0
- package/L2_Handling/display/primitives/document_structure_primitives.js +319 -0
- package/L2_Handling/display/primitives/form_primitives.js +526 -0
- package/L2_Handling/display/primitives/generic_containers.js +109 -0
- package/L2_Handling/display/primitives/interactive_primitives.js +305 -0
- package/L2_Handling/display/primitives/link_primitives.js +552 -0
- package/L2_Handling/display/primitives/lists_primitives.js +262 -0
- package/L2_Handling/display/primitives/media_primitives.js +383 -0
- package/L2_Handling/display/primitives/primitives.js +19 -0
- package/L2_Handling/display/primitives/semantic_element_primitive.js +226 -0
- package/L2_Handling/display/primitives/table_primitives.js +528 -0
- package/L2_Handling/display/primitives/typography_primitives.js +175 -0
- package/L2_Handling/display/specialized/input_request_renderer.js +467 -0
- package/L2_Handling/display/specialized/specialized.js +10 -0
- package/L2_Handling/hooks/hooks.js +9 -0
- package/L2_Handling/hooks/menu_integration.js +57 -0
- package/L2_Handling/hooks/widget_hook_manager.js +292 -0
- package/L2_Handling/message/message.js +8 -0
- package/L2_Handling/message/message_handler.js +701 -0
- package/L2_Handling/navigation/navigation.js +8 -0
- package/L2_Handling/navigation/navigation_manager.js +403 -0
- package/L2_Handling/zhooks/features/cache_live.js +287 -0
- package/L2_Handling/zhooks/features/crumbs_live.js +292 -0
- package/L2_Handling/zhooks/zhooks_manager.js +65 -0
- package/L2_Handling/zvaf/zvaf.js +8 -0
- package/L2_Handling/zvaf/zvaf_manager.js +334 -0
- package/L3_Abstraction/L3_Abstraction.js +12 -0
- package/L3_Abstraction/orchestrator/container_unwrapper.js +101 -0
- package/L3_Abstraction/orchestrator/group_renderer.js +698 -0
- package/L3_Abstraction/orchestrator/input_event_handler.js +797 -0
- package/L3_Abstraction/orchestrator/metadata_processor.js +249 -0
- package/L3_Abstraction/orchestrator/navbar_builder.js +201 -0
- package/L3_Abstraction/orchestrator/orchestrator.js +13 -0
- package/L3_Abstraction/orchestrator/wizard_gate_handler.js +360 -0
- package/L3_Abstraction/renderer/renderer.js +1 -0
- package/L3_Abstraction/session/session.js +1 -0
- package/L4_Orchestration/L4_Orchestration.js +11 -0
- package/L4_Orchestration/client/client.js +1 -0
- package/L4_Orchestration/facade/facade.js +9 -0
- package/L4_Orchestration/facade/manager_registry.js +118 -0
- package/L4_Orchestration/facade/renderer_registry.js +274 -0
- package/L4_Orchestration/lifecycle/asset_loader.js +255 -0
- package/L4_Orchestration/lifecycle/initializer.js +135 -0
- package/L4_Orchestration/lifecycle/lifecycle.js +8 -0
- package/L4_Orchestration/rendering/facade.js +94 -0
- package/L4_Orchestration/rendering/rendering.js +7 -0
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/bifrost_client.js +204 -0
- package/bifrost_core.js +1686 -0
- package/docs/ARCHITECTURE.md +111 -0
- package/docs/PROTOCOL.md +106 -0
- package/docs/RENDERERS.md +101 -0
- package/docs/SECURITY.md +92 -0
- package/package.json +24 -0
- package/syntax/prism-zconfig.js +41 -0
- package/syntax/prism-zenv.js +69 -0
- package/syntax/prism-zolo-theme.css +288 -0
- package/syntax/prism-zolo.js +380 -0
- package/syntax/prism-zschema.js +38 -0
- package/syntax/prism-zspark.js +25 -0
- package/syntax/prism-zui.js +68 -0
- package/zSys/accessibility/accessibility.js +10 -0
- package/zSys/accessibility/emoji_accessibility.js +173 -0
- package/zSys/dom/block_utils.js +122 -0
- package/zSys/dom/container_utils.js +370 -0
- package/zSys/dom/dom.js +13 -0
- package/zSys/dom/dom_utils.js +328 -0
- package/zSys/dom/encoding_utils.js +117 -0
- package/zSys/dom/style_utils.js +71 -0
- package/zSys/errors/error_display.js +299 -0
- package/zSys/errors/errors.js +10 -0
- package/zSys/theme/color_utils.js +274 -0
- package/zSys/theme/dark_mode_utils.js +272 -0
- package/zSys/theme/size_utils.js +256 -0
- package/zSys/theme/spacing_utils.js +405 -0
- package/zSys/theme/theme.js +14 -0
- package/zSys/theme/zbase.css +1735 -0
- package/zSys/theme/zbase_inject.js +161 -0
- package/zSys/theme/ztheme_utils.js +305 -0
- package/zSys/validation/error_boundary.js +201 -0
- package/zSys/validation/validation.js +11 -0
- package/zSys/validation/validation_utils.js +238 -0
- 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';
|