@jqhtml/core 2.3.50 → 2.3.59
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/README.md +3 -1
- package/dist/boot.d.ts +0 -0
- package/dist/boot.d.ts.map +0 -0
- package/dist/boot.js +130 -0
- package/dist/boot.js.map +1 -0
- package/dist/cache-key-serializer.d.ts +47 -0
- package/dist/cache-key-serializer.d.ts.map +1 -0
- package/dist/cache-key-serializer.js +140 -0
- package/dist/cache-key-serializer.js.map +1 -0
- package/dist/component-cache.d.ts +1 -0
- package/dist/component-cache.d.ts.map +1 -1
- package/dist/component-cache.js +270 -0
- package/dist/component-cache.js.map +1 -0
- package/dist/component-events.d.ts +0 -0
- package/dist/component-events.d.ts.map +1 -1
- package/dist/component-events.js +141 -0
- package/dist/component-events.js.map +1 -0
- package/dist/component-queue.d.ts +0 -0
- package/dist/component-queue.d.ts.map +0 -0
- package/dist/component-queue.js +118 -0
- package/dist/component-queue.js.map +1 -0
- package/dist/component-registry.d.ts +0 -0
- package/dist/component-registry.d.ts.map +0 -0
- package/dist/component-registry.js +298 -0
- package/dist/component-registry.js.map +1 -0
- package/dist/component.d.ts +26 -0
- package/dist/component.d.ts.map +1 -1
- package/dist/component.js +1892 -0
- package/dist/component.js.map +1 -0
- package/dist/config.d.ts +57 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +64 -0
- package/dist/config.js.map +1 -0
- package/dist/data-proxy.d.ts +0 -0
- package/dist/data-proxy.d.ts.map +0 -0
- package/dist/data-proxy.js +223 -0
- package/dist/data-proxy.js.map +1 -0
- package/dist/debug-entry.d.ts +0 -0
- package/dist/debug-entry.d.ts.map +0 -0
- package/dist/debug-entry.js +8 -0
- package/dist/debug-entry.js.map +1 -0
- package/dist/debug.d.ts +0 -0
- package/dist/debug.d.ts.map +0 -0
- package/dist/debug.js +203 -0
- package/dist/debug.js.map +1 -0
- package/dist/index.cjs +520 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +519 -47
- package/dist/index.js.map +1 -1
- package/dist/instruction-processor.d.ts +0 -0
- package/dist/instruction-processor.d.ts.map +1 -1
- package/dist/instruction-processor.js +563 -0
- package/dist/instruction-processor.js.map +1 -0
- package/dist/jqhtml-core.esm.js +520 -48
- package/dist/jqhtml-core.esm.js.map +1 -1
- package/dist/jqhtml-debug.esm.js +0 -0
- package/dist/jqhtml-debug.esm.js.map +0 -0
- package/dist/jquery-plugin.d.ts +0 -0
- package/dist/jquery-plugin.d.ts.map +1 -1
- package/dist/jquery-plugin.js +472 -0
- package/dist/jquery-plugin.js.map +1 -0
- package/dist/lifecycle-manager.d.ts +0 -0
- package/dist/lifecycle-manager.d.ts.map +0 -0
- package/dist/lifecycle-manager.js +201 -0
- package/dist/lifecycle-manager.js.map +1 -0
- package/dist/load-coordinator.d.ts +18 -1
- package/dist/load-coordinator.d.ts.map +1 -1
- package/dist/load-coordinator.js +309 -0
- package/dist/load-coordinator.js.map +1 -0
- package/dist/local-storage.d.ts +2 -0
- package/dist/local-storage.d.ts.map +1 -1
- package/dist/local-storage.js +699 -0
- package/dist/local-storage.js.map +1 -0
- package/dist/preload-data.d.ts +0 -0
- package/dist/preload-data.d.ts.map +0 -0
- package/dist/preload-data.js +136 -0
- package/dist/preload-data.js.map +1 -0
- package/dist/template-renderer.d.ts +0 -0
- package/dist/template-renderer.d.ts.map +0 -0
- package/dist/template-renderer.js +278 -0
- package/dist/template-renderer.js.map +1 -0
- package/dist/viewport.d.ts +36 -0
- package/dist/viewport.d.ts.map +1 -0
- package/dist/viewport.js +69 -0
- package/dist/viewport.js.map +1 -0
- package/package.json +7 -2
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jqhtml_Local_Storage - Scoped local storage for JQHTML component caching
|
|
3
|
+
*
|
|
4
|
+
* Provides safe, scoped access to localStorage with automatic handling of
|
|
5
|
+
* unavailable storage, quota exceeded errors, and scope invalidation.
|
|
6
|
+
*
|
|
7
|
+
* Key Features:
|
|
8
|
+
* - **Manual scoping**: Cache key must be set via set_cache_key() before use
|
|
9
|
+
* - **Graceful degradation**: Returns null when storage unavailable or cache key not set
|
|
10
|
+
* - **Quota management**: Auto-clears storage when full and retries operation
|
|
11
|
+
* - **Scope validation**: Clears storage when cache key changes
|
|
12
|
+
* - **Developer-friendly keys**: Scoped suffix allows easy inspection in dev tools
|
|
13
|
+
* - **Class-aware serialization**: ES6 class instances serialize and restore properly
|
|
14
|
+
*
|
|
15
|
+
* Class-Aware Serialization:
|
|
16
|
+
* ES6 class instances can be serialized and deserialized if registered via
|
|
17
|
+
* register_cache_class(). Classes are wrapped as {__jqhtml_class__: "Name", __jqhtml_props__: {...}}
|
|
18
|
+
* and restored to proper instances on retrieval. Nested class instances in objects
|
|
19
|
+
* and arrays are handled recursively.
|
|
20
|
+
*
|
|
21
|
+
* Scoping Strategy:
|
|
22
|
+
* Storage is scoped by a user-provided cache key (typically a session identifier,
|
|
23
|
+
* user ID, or combination of relevant scope factors). This scope is stored in
|
|
24
|
+
* `_jqhtml_cache_key`. If the cache key changes between page loads, all JQHTML
|
|
25
|
+
* storage is cleared to prevent stale data from different sessions/users.
|
|
26
|
+
*
|
|
27
|
+
* Key Format:
|
|
28
|
+
* Keys are stored as: `jqhtml::developer_key::cache_key`
|
|
29
|
+
* Example: `jqhtml::User_Profile_data::user_123`
|
|
30
|
+
*
|
|
31
|
+
* The `jqhtml::` prefix identifies JQHTML keys, allowing safe clearing of only our keys
|
|
32
|
+
* without affecting other JavaScript libraries. This enables transparent coexistence
|
|
33
|
+
* with third-party libraries that also use browser storage.
|
|
34
|
+
*
|
|
35
|
+
* Quota Exceeded Handling:
|
|
36
|
+
* When storage quota is exceeded during a set operation, only JQHTML keys (prefixed with
|
|
37
|
+
* `jqhtml::`) are cleared, preserving other libraries' data. The operation is then retried
|
|
38
|
+
* once. This ensures the application continues functioning even when storage is full.
|
|
39
|
+
*
|
|
40
|
+
* Usage:
|
|
41
|
+
* // Register classes that need to be cached (call once at startup)
|
|
42
|
+
* jqhtml.register_cache_class(Contact_Model);
|
|
43
|
+
* jqhtml.register_cache_class(User_Profile);
|
|
44
|
+
*
|
|
45
|
+
* // Must set cache key first (typically done once on page load)
|
|
46
|
+
* Jqhtml_Local_Storage.set_cache_key('user_123');
|
|
47
|
+
*
|
|
48
|
+
* // Then use storage normally - ES6 classes serialize automatically
|
|
49
|
+
* Jqhtml_Local_Storage.set('cached_component', {
|
|
50
|
+
* contact: new Contact_Model(),
|
|
51
|
+
* timestamp: Date.now()
|
|
52
|
+
* });
|
|
53
|
+
* const cached = Jqhtml_Local_Storage.get('cached_component');
|
|
54
|
+
* // cached.contact instanceof Contact_Model === true
|
|
55
|
+
*
|
|
56
|
+
* IMPORTANT - Volatile Storage:
|
|
57
|
+
* Storage can be cleared at any time due to:
|
|
58
|
+
* - User clearing browser data
|
|
59
|
+
* - Private browsing mode restrictions
|
|
60
|
+
* - Quota exceeded errors
|
|
61
|
+
* - Cache key changes
|
|
62
|
+
* - Browser storage unavailable
|
|
63
|
+
*
|
|
64
|
+
* Therefore, NEVER store critical data. Only use for:
|
|
65
|
+
* - Cached component HTML (performance optimization)
|
|
66
|
+
* - Transient UI state (convenience, not required)
|
|
67
|
+
*
|
|
68
|
+
* @internal This class is not exposed in the public API
|
|
69
|
+
*/
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// Class Registry for Serialization
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// Registry mapping class names to constructors
|
|
74
|
+
const class_registry = Object.create(null);
|
|
75
|
+
// Markers for serialized class instances (unique to avoid collisions with user data)
|
|
76
|
+
// Replaced at build time by rollup (see rollup.config.js).
|
|
77
|
+
const CORE_VERSION = '__VERSION__';
|
|
78
|
+
const CLASS_MARKER = '__jqhtml_class__';
|
|
79
|
+
const PROPS_MARKER = '__jqhtml_props__';
|
|
80
|
+
/**
|
|
81
|
+
* Register a class for cache serialization/deserialization.
|
|
82
|
+
* Must be called before attempting to cache instances of this class.
|
|
83
|
+
*
|
|
84
|
+
* @param klass - The class constructor to register
|
|
85
|
+
* @throws Error if klass is not a named function/class
|
|
86
|
+
*/
|
|
87
|
+
export function register_cache_class(klass) {
|
|
88
|
+
if (typeof klass !== 'function' || !klass.name) {
|
|
89
|
+
throw new Error('[JQHTML Cache] register_cache_class requires a named class constructor');
|
|
90
|
+
}
|
|
91
|
+
class_registry[klass.name] = klass;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Check if a class is registered for caching
|
|
95
|
+
*/
|
|
96
|
+
export function is_cache_class_registered(class_name) {
|
|
97
|
+
return class_name in class_registry;
|
|
98
|
+
}
|
|
99
|
+
// ============================================================================
|
|
100
|
+
// Serialization (Object → String)
|
|
101
|
+
// ============================================================================
|
|
102
|
+
/**
|
|
103
|
+
* Serialize a value to JSON string, handling ES6 class instances recursively.
|
|
104
|
+
* Returns null if serialization fails for any reason.
|
|
105
|
+
*
|
|
106
|
+
* @param value - The value to serialize
|
|
107
|
+
* @param verbose - Whether to log warnings
|
|
108
|
+
* @returns Serialized JSON string, or null if serialization failed
|
|
109
|
+
*/
|
|
110
|
+
function serialize_value(value, verbose) {
|
|
111
|
+
try {
|
|
112
|
+
const seen = new WeakSet();
|
|
113
|
+
const processed = process_for_serialization(value, verbose, seen);
|
|
114
|
+
if (processed === undefined) {
|
|
115
|
+
// Serialization failed - undefined signals failure
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
return JSON.stringify(processed);
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
if (verbose) {
|
|
122
|
+
console.warn('[JQHTML Cache] Serialization failed:', e);
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Recursively process a value for serialization.
|
|
129
|
+
* Returns undefined if serialization should fail (unserializable value encountered).
|
|
130
|
+
*
|
|
131
|
+
* @param value - The value to process
|
|
132
|
+
* @param verbose - Whether to log warnings
|
|
133
|
+
* @param seen - WeakSet to detect circular references
|
|
134
|
+
* @returns Processed value ready for JSON.stringify, or undefined if failed
|
|
135
|
+
*/
|
|
136
|
+
function process_for_serialization(value, verbose, seen) {
|
|
137
|
+
// Handle null and undefined
|
|
138
|
+
if (value === null) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
if (value === undefined) {
|
|
142
|
+
return undefined; // JSON.stringify will omit this property
|
|
143
|
+
}
|
|
144
|
+
// Handle primitives
|
|
145
|
+
if (typeof value !== 'object') {
|
|
146
|
+
// string, number, boolean are fine
|
|
147
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
// bigint, symbol, function cannot be serialized
|
|
151
|
+
if (verbose) {
|
|
152
|
+
console.warn(`[JQHTML Cache] Cannot serialize ${typeof value} - value will be omitted`);
|
|
153
|
+
}
|
|
154
|
+
// Return undefined to omit this value (not fail entire serialization)
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
// Handle circular references.
|
|
158
|
+
// `seen` tracks only the current ancestor path (the DFS stack): an object is
|
|
159
|
+
// added on the way down and removed on the way back up (see finally below).
|
|
160
|
+
// This flags a genuine cycle (a back-edge to an ancestor) while allowing a
|
|
161
|
+
// shared reference in a DAG to be serialized independently on each visit.
|
|
162
|
+
// JSON round-trip semantics do not preserve identity anyway, so emitting a
|
|
163
|
+
// copy per reference is correct; silently omitting the property is not.
|
|
164
|
+
if (seen.has(value)) {
|
|
165
|
+
if (verbose) {
|
|
166
|
+
console.warn('[JQHTML Cache] Circular reference detected - cannot serialize');
|
|
167
|
+
}
|
|
168
|
+
return undefined; // Fail entire serialization
|
|
169
|
+
}
|
|
170
|
+
seen.add(value);
|
|
171
|
+
try {
|
|
172
|
+
// Handle arrays
|
|
173
|
+
if (Array.isArray(value)) {
|
|
174
|
+
const result = [];
|
|
175
|
+
for (let i = 0; i < value.length; i++) {
|
|
176
|
+
const item = value[i];
|
|
177
|
+
const processed = process_for_serialization(item, verbose, seen);
|
|
178
|
+
// For arrays, we keep undefined as null to preserve indices
|
|
179
|
+
result.push(processed === undefined ? null : processed);
|
|
180
|
+
}
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
// Handle Date objects specially
|
|
184
|
+
if (value instanceof Date) {
|
|
185
|
+
return {
|
|
186
|
+
[CLASS_MARKER]: 'Date',
|
|
187
|
+
[PROPS_MARKER]: value.toISOString()
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
// Handle Map objects
|
|
191
|
+
if (value instanceof Map) {
|
|
192
|
+
const entries = [];
|
|
193
|
+
for (const [k, v] of value) {
|
|
194
|
+
const processedKey = process_for_serialization(k, verbose, seen);
|
|
195
|
+
const processedValue = process_for_serialization(v, verbose, seen);
|
|
196
|
+
entries.push([processedKey, processedValue]);
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
[CLASS_MARKER]: 'Map',
|
|
200
|
+
[PROPS_MARKER]: entries
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
// Handle Set objects
|
|
204
|
+
if (value instanceof Set) {
|
|
205
|
+
const items = [];
|
|
206
|
+
for (const item of value) {
|
|
207
|
+
items.push(process_for_serialization(item, verbose, seen));
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
[CLASS_MARKER]: 'Set',
|
|
211
|
+
[PROPS_MARKER]: items
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
// Get the constructor
|
|
215
|
+
const ctor = value.constructor;
|
|
216
|
+
// Handle registered class instances
|
|
217
|
+
if (ctor && ctor.name && class_registry[ctor.name]) {
|
|
218
|
+
const props = {};
|
|
219
|
+
// Extract own enumerable properties (bypass any toJSON method)
|
|
220
|
+
for (const key of Object.keys(value)) {
|
|
221
|
+
const propValue = value[key];
|
|
222
|
+
const processed = process_for_serialization(propValue, verbose, seen);
|
|
223
|
+
// Only include if not undefined (failed serialization)
|
|
224
|
+
if (processed !== undefined || propValue === undefined) {
|
|
225
|
+
props[key] = processed;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
[CLASS_MARKER]: ctor.name,
|
|
230
|
+
[PROPS_MARKER]: props
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
// Handle plain objects (constructor is Object or no constructor)
|
|
234
|
+
if (ctor === Object || ctor === undefined || ctor === null) {
|
|
235
|
+
const result = {};
|
|
236
|
+
for (const key of Object.keys(value)) {
|
|
237
|
+
const propValue = value[key];
|
|
238
|
+
const processed = process_for_serialization(propValue, verbose, seen);
|
|
239
|
+
// Only include if not undefined (unless original was undefined)
|
|
240
|
+
if (processed !== undefined || propValue === undefined) {
|
|
241
|
+
result[key] = processed;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
// Unregistered class instance - convert to plain object for hot/cold parity
|
|
247
|
+
// This ensures developers catch missing class registrations during development
|
|
248
|
+
// (methods will be lost, only properties preserved)
|
|
249
|
+
if (verbose) {
|
|
250
|
+
console.warn(`[JQHTML Cache] Converting unregistered class "${ctor.name}" to plain object. ` +
|
|
251
|
+
`Methods will be lost. Call jqhtml.register_cache_class(${ctor.name}) to preserve the class.`);
|
|
252
|
+
}
|
|
253
|
+
// Convert to plain object - properties are preserved, prototype methods are lost
|
|
254
|
+
const result = {};
|
|
255
|
+
for (const key of Object.keys(value)) {
|
|
256
|
+
const propValue = value[key];
|
|
257
|
+
const processed = process_for_serialization(propValue, verbose, seen);
|
|
258
|
+
// Only include if not undefined (unless original was undefined)
|
|
259
|
+
if (processed !== undefined || propValue === undefined) {
|
|
260
|
+
result[key] = processed;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
// Pop this object off the ancestor path now that its subtree is done.
|
|
267
|
+
seen.delete(value);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// ============================================================================
|
|
271
|
+
// Deserialization (String → Object)
|
|
272
|
+
// ============================================================================
|
|
273
|
+
/**
|
|
274
|
+
* Deserialize a JSON string, restoring ES6 class instances.
|
|
275
|
+
* Returns null if deserialization fails.
|
|
276
|
+
*
|
|
277
|
+
* @param str - The JSON string to deserialize
|
|
278
|
+
* @param verbose - Whether to log warnings
|
|
279
|
+
* @returns Deserialized value with class instances restored, or null if failed
|
|
280
|
+
*/
|
|
281
|
+
function deserialize_value(str, verbose) {
|
|
282
|
+
try {
|
|
283
|
+
const parsed = JSON.parse(str);
|
|
284
|
+
return process_for_deserialization(parsed, verbose);
|
|
285
|
+
}
|
|
286
|
+
catch (e) {
|
|
287
|
+
if (verbose) {
|
|
288
|
+
console.warn('[JQHTML Cache] Deserialization failed:', e);
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Recursively process a parsed value, restoring class instances.
|
|
295
|
+
*
|
|
296
|
+
* @param value - The parsed value to process
|
|
297
|
+
* @param verbose - Whether to log warnings
|
|
298
|
+
* @returns Processed value with class instances restored
|
|
299
|
+
*/
|
|
300
|
+
function process_for_deserialization(value, verbose) {
|
|
301
|
+
// Handle primitives and null
|
|
302
|
+
if (value === null || value === undefined || typeof value !== 'object') {
|
|
303
|
+
return value;
|
|
304
|
+
}
|
|
305
|
+
// Handle arrays
|
|
306
|
+
if (Array.isArray(value)) {
|
|
307
|
+
return value.map(item => process_for_deserialization(item, verbose));
|
|
308
|
+
}
|
|
309
|
+
// Check for class marker
|
|
310
|
+
if (value[CLASS_MARKER] !== undefined && value[PROPS_MARKER] !== undefined) {
|
|
311
|
+
const class_name = value[CLASS_MARKER];
|
|
312
|
+
const props = value[PROPS_MARKER];
|
|
313
|
+
// Handle built-in types
|
|
314
|
+
if (class_name === 'Date') {
|
|
315
|
+
return new Date(props);
|
|
316
|
+
}
|
|
317
|
+
if (class_name === 'Map') {
|
|
318
|
+
const map = new Map();
|
|
319
|
+
for (const [k, v] of props) {
|
|
320
|
+
map.set(process_for_deserialization(k, verbose), process_for_deserialization(v, verbose));
|
|
321
|
+
}
|
|
322
|
+
return map;
|
|
323
|
+
}
|
|
324
|
+
if (class_name === 'Set') {
|
|
325
|
+
const set = new Set();
|
|
326
|
+
for (const item of props) {
|
|
327
|
+
set.add(process_for_deserialization(item, verbose));
|
|
328
|
+
}
|
|
329
|
+
return set;
|
|
330
|
+
}
|
|
331
|
+
// Look up registered class
|
|
332
|
+
const klass = class_registry[class_name];
|
|
333
|
+
if (!klass) {
|
|
334
|
+
if (verbose) {
|
|
335
|
+
console.warn(`[JQHTML Cache] Cannot restore class "${class_name}" - not registered. ` +
|
|
336
|
+
`Data will be returned as plain object. ` +
|
|
337
|
+
`Call jqhtml.register_cache_class(${class_name}) to restore class instances.`);
|
|
338
|
+
}
|
|
339
|
+
// Return as plain object rather than failing completely
|
|
340
|
+
return process_for_deserialization(props, verbose);
|
|
341
|
+
}
|
|
342
|
+
// Create instance without calling constructor and assign properties
|
|
343
|
+
try {
|
|
344
|
+
const instance = Object.create(klass.prototype);
|
|
345
|
+
const processed_props = process_for_deserialization(props, verbose);
|
|
346
|
+
Object.assign(instance, processed_props);
|
|
347
|
+
return instance;
|
|
348
|
+
}
|
|
349
|
+
catch (e) {
|
|
350
|
+
if (verbose) {
|
|
351
|
+
console.warn(`[JQHTML Cache] Failed to restore class "${class_name}":`, e);
|
|
352
|
+
}
|
|
353
|
+
// Return null to signal cache should be treated as miss
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// Plain object - process recursively
|
|
358
|
+
const result = {};
|
|
359
|
+
for (const key of Object.keys(value)) {
|
|
360
|
+
result[key] = process_for_deserialization(value[key], verbose);
|
|
361
|
+
}
|
|
362
|
+
return result;
|
|
363
|
+
}
|
|
364
|
+
export class Jqhtml_Local_Storage {
|
|
365
|
+
/**
|
|
366
|
+
* Set the cache key and initialize storage
|
|
367
|
+
* Must be called before any get/set operations
|
|
368
|
+
* @param {string} cache_key - Unique identifier for this cache scope
|
|
369
|
+
* @param {CacheMode} cache_mode - Cache strategy: 'data' (default, recommended) or 'html'
|
|
370
|
+
*/
|
|
371
|
+
static set_cache_key(cache_key, cache_mode = 'data') {
|
|
372
|
+
this._cache_key = cache_key;
|
|
373
|
+
// Key SHAPE is a function of the library (see cache-key-serializer.ts), so an
|
|
374
|
+
// upgrade that changes it must invalidate everything persisted under the old shape.
|
|
375
|
+
// Version the SCOPE MARKER rather than the key itself: _validate_scope() already
|
|
376
|
+
// clears every jqhtml key when this value changes, and leaving the per-entry key
|
|
377
|
+
// format alone keeps stored keys readable and avoids repeating the version in each.
|
|
378
|
+
this._scope_marker = `${CORE_VERSION}::${cache_key}`;
|
|
379
|
+
this._cache_mode = cache_mode;
|
|
380
|
+
this._init();
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Check if cache key has been set
|
|
384
|
+
* @returns {boolean} True if cache key is configured
|
|
385
|
+
*/
|
|
386
|
+
static has_cache_key() {
|
|
387
|
+
return this._cache_key !== null;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Get the current cache mode
|
|
391
|
+
* @returns {CacheMode} Current cache mode ('data' or 'html')
|
|
392
|
+
*/
|
|
393
|
+
static get_cache_mode() {
|
|
394
|
+
return this._cache_mode;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Initialize storage system and validate scope
|
|
398
|
+
* Called automatically after cache key is set
|
|
399
|
+
* @private
|
|
400
|
+
*/
|
|
401
|
+
static _init() {
|
|
402
|
+
// Check storage availability
|
|
403
|
+
if (this._storage_available === null) {
|
|
404
|
+
this._storage_available = this._is_storage_available();
|
|
405
|
+
}
|
|
406
|
+
if (!this._storage_available || !this._cache_key) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
// Validate scope
|
|
410
|
+
this._validate_scope();
|
|
411
|
+
this._initialized = true;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Check if localStorage is available
|
|
415
|
+
* @returns {boolean}
|
|
416
|
+
* @private
|
|
417
|
+
*/
|
|
418
|
+
static _is_storage_available() {
|
|
419
|
+
try {
|
|
420
|
+
const storage = window.localStorage;
|
|
421
|
+
const test = '__jqhtml_storage_test__';
|
|
422
|
+
storage.setItem(test, test);
|
|
423
|
+
storage.removeItem(test);
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
catch (e) {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Check if verbose mode is enabled
|
|
432
|
+
* @private
|
|
433
|
+
*/
|
|
434
|
+
static _is_verbose() {
|
|
435
|
+
return window.jqhtml?.debug?.verbose === true;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Validate storage scope and clear JQHTML keys if cache key changed
|
|
439
|
+
* Only clears keys prefixed with 'jqhtml::' to preserve other libraries' data
|
|
440
|
+
* @private
|
|
441
|
+
*/
|
|
442
|
+
static _validate_scope() {
|
|
443
|
+
if (!this._storage_available) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
try {
|
|
447
|
+
const stored_key = localStorage.getItem('_jqhtml_cache_key');
|
|
448
|
+
// If the scope marker (core version + developer key) changed, clear only JQHTML keys
|
|
449
|
+
if (stored_key !== null && stored_key !== this._scope_marker) {
|
|
450
|
+
console.log('[JQHTML Local Storage] Cache scope changed, clearing JQHTML keys:', {
|
|
451
|
+
old_key: stored_key,
|
|
452
|
+
new_key: this._scope_marker,
|
|
453
|
+
});
|
|
454
|
+
this._clear_jqhtml_keys();
|
|
455
|
+
localStorage.setItem('_jqhtml_cache_key', this._scope_marker);
|
|
456
|
+
}
|
|
457
|
+
else if (stored_key === null) {
|
|
458
|
+
// First time JQHTML is using this storage - just set the key, don't clear
|
|
459
|
+
console.log('[JQHTML Local Storage] Initializing cache scope (first use):', {
|
|
460
|
+
new_key: this._scope_marker,
|
|
461
|
+
});
|
|
462
|
+
localStorage.setItem('_jqhtml_cache_key', this._scope_marker);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
catch (e) {
|
|
466
|
+
console.error('[JQHTML Local Storage] Failed to validate scope:', e);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Clear only JQHTML keys from storage (keys starting with 'jqhtml::')
|
|
471
|
+
* Preserves keys from other libraries
|
|
472
|
+
* @private
|
|
473
|
+
*/
|
|
474
|
+
static _clear_jqhtml_keys() {
|
|
475
|
+
if (!this._storage_available) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const keys_to_remove = [];
|
|
479
|
+
// Collect all JQHTML keys
|
|
480
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
481
|
+
const key = localStorage.key(i);
|
|
482
|
+
if (key && key.startsWith('jqhtml::')) {
|
|
483
|
+
keys_to_remove.push(key);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// Remove collected keys
|
|
487
|
+
keys_to_remove.forEach(key => {
|
|
488
|
+
try {
|
|
489
|
+
localStorage.removeItem(key);
|
|
490
|
+
}
|
|
491
|
+
catch (e) {
|
|
492
|
+
console.error('[JQHTML Local Storage] Failed to remove key:', key, e);
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
console.log(`[JQHTML Local Storage] Cleared ${keys_to_remove.length} JQHTML keys`);
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Build scoped key with JQHTML namespace prefix
|
|
499
|
+
* @param {string} key - Developer-provided key
|
|
500
|
+
* @returns {string}
|
|
501
|
+
* @private
|
|
502
|
+
*/
|
|
503
|
+
static _build_key(key) {
|
|
504
|
+
return `jqhtml::${key}::${this._cache_key}`;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Check if storage is ready for use
|
|
508
|
+
* @returns {boolean}
|
|
509
|
+
* @private
|
|
510
|
+
*/
|
|
511
|
+
static _is_ready() {
|
|
512
|
+
return this._storage_available === true && this._cache_key !== null && this._initialized;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Set item in localStorage with class-aware serialization.
|
|
516
|
+
*
|
|
517
|
+
* If serialization fails (e.g., unregistered class instances, circular refs),
|
|
518
|
+
* the existing cache entry is removed and nothing is stored.
|
|
519
|
+
*
|
|
520
|
+
* @param {string} key - Storage key
|
|
521
|
+
* @param {*} value - Value to store (primitives, objects, arrays, or registered class instances)
|
|
522
|
+
*/
|
|
523
|
+
static set(key, value) {
|
|
524
|
+
if (!this._is_ready()) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const verbose = this._is_verbose();
|
|
528
|
+
const scoped_key = this._build_key(key);
|
|
529
|
+
// Serialize with class-aware processing
|
|
530
|
+
const serialized = serialize_value(value, verbose);
|
|
531
|
+
if (serialized === null) {
|
|
532
|
+
// Serialization failed - remove any existing cache entry
|
|
533
|
+
if (verbose) {
|
|
534
|
+
console.warn(`[JQHTML Cache] Failed to serialize value for key "${key}". ` +
|
|
535
|
+
`Removing existing cache entry if present.`);
|
|
536
|
+
}
|
|
537
|
+
try {
|
|
538
|
+
localStorage.removeItem(scoped_key);
|
|
539
|
+
}
|
|
540
|
+
catch (e) {
|
|
541
|
+
// Ignore removal errors
|
|
542
|
+
}
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
// Check size before attempting to store (1MB limit)
|
|
546
|
+
const size_bytes = new Blob([serialized]).size;
|
|
547
|
+
const size_mb = size_bytes / (1024 * 1024);
|
|
548
|
+
if (size_mb > 1) {
|
|
549
|
+
if (verbose) {
|
|
550
|
+
console.warn(`[JQHTML Cache] Skipping set - value too large (${size_mb.toFixed(2)}MB > 1MB limit)`, { key, size_bytes, size_mb });
|
|
551
|
+
}
|
|
552
|
+
// Remove existing cache entry since we can't update it
|
|
553
|
+
try {
|
|
554
|
+
localStorage.removeItem(scoped_key);
|
|
555
|
+
}
|
|
556
|
+
catch (e) {
|
|
557
|
+
// Ignore removal errors
|
|
558
|
+
}
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
this._set_item(key, serialized);
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Get item from localStorage with class-aware deserialization.
|
|
565
|
+
*
|
|
566
|
+
* If deserialization fails, returns null (as if no cache exists).
|
|
567
|
+
*
|
|
568
|
+
* @param {string} key - Storage key
|
|
569
|
+
* @returns {*|null} Deserialized value with class instances restored, or null if not found/failed
|
|
570
|
+
*/
|
|
571
|
+
static get(key) {
|
|
572
|
+
if (!this._is_ready()) {
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
const verbose = this._is_verbose();
|
|
576
|
+
const scoped_key = this._build_key(key);
|
|
577
|
+
try {
|
|
578
|
+
const serialized = localStorage.getItem(scoped_key);
|
|
579
|
+
if (serialized === null) {
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
const result = deserialize_value(serialized, verbose);
|
|
583
|
+
if (result === null) {
|
|
584
|
+
// Deserialization failed - treat as cache miss
|
|
585
|
+
if (verbose) {
|
|
586
|
+
console.warn(`[JQHTML Cache] Failed to deserialize value for key "${key}". ` +
|
|
587
|
+
`Treating as cache miss.`);
|
|
588
|
+
}
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
return result;
|
|
592
|
+
}
|
|
593
|
+
catch (e) {
|
|
594
|
+
if (verbose) {
|
|
595
|
+
console.warn('[JQHTML Cache] Failed to get item:', e);
|
|
596
|
+
}
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Remove item from localStorage
|
|
602
|
+
* @param {string} key - Storage key
|
|
603
|
+
*/
|
|
604
|
+
static remove(key) {
|
|
605
|
+
if (!this._is_ready()) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
this._remove_item(key);
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Perform a serialize/deserialize round-trip on a value.
|
|
612
|
+
*
|
|
613
|
+
* This ensures "hot" data (fresh from on_load) behaves identically to "cold" data
|
|
614
|
+
* (restored from cache). Unregistered class instances will be converted to plain
|
|
615
|
+
* objects, exactly as they would be if restored from cache.
|
|
616
|
+
*
|
|
617
|
+
* Use this to normalize data after on_load() so developers catch missing class
|
|
618
|
+
* registrations immediately rather than only after a page reload.
|
|
619
|
+
*
|
|
620
|
+
* @param {any} value - The value to normalize
|
|
621
|
+
* @returns {any} The value after serialize/deserialize round-trip, or original if serialization fails
|
|
622
|
+
*/
|
|
623
|
+
static normalize_for_cache(value) {
|
|
624
|
+
const verbose = this._is_verbose();
|
|
625
|
+
// Serialize to JSON string
|
|
626
|
+
const serialized = serialize_value(value, verbose);
|
|
627
|
+
if (serialized === null) {
|
|
628
|
+
// Serialization failed completely - return original
|
|
629
|
+
// (This happens with circular references or other fatal issues)
|
|
630
|
+
if (verbose) {
|
|
631
|
+
console.warn('[JQHTML Cache] normalize_for_cache: Serialization failed, returning original value');
|
|
632
|
+
}
|
|
633
|
+
return value;
|
|
634
|
+
}
|
|
635
|
+
// Deserialize back to object
|
|
636
|
+
const deserialized = deserialize_value(serialized, verbose);
|
|
637
|
+
if (deserialized === null) {
|
|
638
|
+
// Deserialization failed - return original
|
|
639
|
+
if (verbose) {
|
|
640
|
+
console.warn('[JQHTML Cache] normalize_for_cache: Deserialization failed, returning original value');
|
|
641
|
+
}
|
|
642
|
+
return value;
|
|
643
|
+
}
|
|
644
|
+
return deserialized;
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Internal set implementation with scope validation and quota handling
|
|
648
|
+
* @param {string} key
|
|
649
|
+
* @param {string} serialized - Pre-serialized JSON string
|
|
650
|
+
* @private
|
|
651
|
+
*/
|
|
652
|
+
static _set_item(key, serialized) {
|
|
653
|
+
// Validate scope before every write
|
|
654
|
+
this._validate_scope();
|
|
655
|
+
const scoped_key = this._build_key(key);
|
|
656
|
+
try {
|
|
657
|
+
localStorage.setItem(scoped_key, serialized);
|
|
658
|
+
}
|
|
659
|
+
catch (e) {
|
|
660
|
+
// Check if quota exceeded
|
|
661
|
+
if (e.name === 'QuotaExceededError' || e.code === 22) {
|
|
662
|
+
console.warn('[JQHTML Local Storage] Quota exceeded, clearing JQHTML keys and retrying');
|
|
663
|
+
// Clear only JQHTML keys and retry once
|
|
664
|
+
this._clear_jqhtml_keys();
|
|
665
|
+
localStorage.setItem('_jqhtml_cache_key', this._cache_key);
|
|
666
|
+
try {
|
|
667
|
+
localStorage.setItem(scoped_key, serialized);
|
|
668
|
+
}
|
|
669
|
+
catch (retry_error) {
|
|
670
|
+
console.error('[JQHTML Local Storage] Failed to set item after clearing JQHTML keys:', retry_error);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
console.error('[JQHTML Local Storage] Failed to set item:', e);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Internal remove implementation
|
|
680
|
+
* @param {string} key
|
|
681
|
+
* @private
|
|
682
|
+
*/
|
|
683
|
+
static _remove_item(key) {
|
|
684
|
+
const scoped_key = this._build_key(key);
|
|
685
|
+
try {
|
|
686
|
+
localStorage.removeItem(scoped_key);
|
|
687
|
+
}
|
|
688
|
+
catch (e) {
|
|
689
|
+
console.error('[JQHTML Local Storage] Failed to remove item:', e);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
Jqhtml_Local_Storage._cache_key = null;
|
|
694
|
+
/** Versioned scope identity - compared in _validate_scope() to clear on a core upgrade. */
|
|
695
|
+
Jqhtml_Local_Storage._scope_marker = null;
|
|
696
|
+
Jqhtml_Local_Storage._cache_mode = 'data';
|
|
697
|
+
Jqhtml_Local_Storage._storage_available = null;
|
|
698
|
+
Jqhtml_Local_Storage._initialized = false;
|
|
699
|
+
//# sourceMappingURL=local-storage.js.map
|