@jqhtml/core 2.3.10 → 2.3.13
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/dist/component.d.ts +18 -0
- package/dist/component.d.ts.map +1 -1
- package/dist/index.cjs +695 -76
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +695 -77
- package/dist/index.js.map +1 -1
- package/dist/jqhtml-core.esm.js +696 -78
- package/dist/jqhtml-core.esm.js.map +1 -1
- package/dist/local-storage.d.ts +80 -16
- package/dist/local-storage.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/jqhtml-core.esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* JQHTML Core v2.3.
|
|
2
|
+
* JQHTML Core v2.3.13
|
|
3
3
|
* (c) 2025 JQHTML Team
|
|
4
4
|
* Released under the MIT License
|
|
5
5
|
*/
|
|
@@ -1127,6 +1127,15 @@ class Jqhtml_Component {
|
|
|
1127
1127
|
this.__data_frozen = false; // Track if this.data is currently frozen
|
|
1128
1128
|
this.next_reload_force_refresh = null; // State machine for reload()/refresh() debounce precedence
|
|
1129
1129
|
this.__lifecycle_authorized = false; // Flag for lifecycle method protection
|
|
1130
|
+
// Cache mode properties
|
|
1131
|
+
this._cache_key = null; // Cache key for caching
|
|
1132
|
+
// 'html' mode caching
|
|
1133
|
+
this._cached_html = null; // Cached HTML to inject on first render
|
|
1134
|
+
this._used_cached_html = false; // Flag if cached HTML was used (forces re-render after on_load)
|
|
1135
|
+
this._should_cache_html_after_ready = false; // Flag to cache HTML after on_ready lifecycle
|
|
1136
|
+
this._is_dynamic = false; // True if this.data changed during on_load() (used for HTML cache sync)
|
|
1137
|
+
// on_render synchronization (HTML cache mode)
|
|
1138
|
+
this._on_render_complete = false; // True after on_render() has been called post-on_load
|
|
1130
1139
|
this._cid = this._generate_cid();
|
|
1131
1140
|
this._lifecycle_manager = LifecycleManager.get_instance();
|
|
1132
1141
|
// Create or wrap element
|
|
@@ -1357,6 +1366,40 @@ class Jqhtml_Component {
|
|
|
1357
1366
|
`The framework will automatically re-render if this.data changes during on_load().`);
|
|
1358
1367
|
}
|
|
1359
1368
|
this._log_lifecycle('render', 'start');
|
|
1369
|
+
// HTML CACHE MODE - If we have cached HTML, inject it directly and skip template rendering
|
|
1370
|
+
if (this._cached_html !== null) {
|
|
1371
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
1372
|
+
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) injecting cached HTML`, { html_length: this._cached_html.length });
|
|
1373
|
+
}
|
|
1374
|
+
// Inject cached HTML directly
|
|
1375
|
+
this.$[0].innerHTML = this._cached_html;
|
|
1376
|
+
// Mark that we used cached HTML (forces re-render after on_load)
|
|
1377
|
+
this._used_cached_html = true;
|
|
1378
|
+
// Clear cached HTML so next render uses template
|
|
1379
|
+
this._cached_html = null;
|
|
1380
|
+
// Mark first render complete
|
|
1381
|
+
this._did_first_render = true;
|
|
1382
|
+
this._log_lifecycle('render', 'complete (cached HTML)');
|
|
1383
|
+
// NEW ARCHITECTURE: Call on_render() even after cache inject
|
|
1384
|
+
// This ensures consistent behavior - on_render() always runs after DOM update
|
|
1385
|
+
// Note: this.data has defaults from on_create(), not fresh data yet
|
|
1386
|
+
const cacheRenderResult = this._call_lifecycle_sync('on_render');
|
|
1387
|
+
if (cacheRenderResult && typeof cacheRenderResult.then === 'function') {
|
|
1388
|
+
console.warn(`[JQHTML] Component "${this.component_name()}" returned a Promise from on_render(). ` +
|
|
1389
|
+
`on_render() must be synchronous code. Remove 'async' from the function declaration.`);
|
|
1390
|
+
}
|
|
1391
|
+
// Emit lifecycle event
|
|
1392
|
+
this.trigger('render');
|
|
1393
|
+
// Store args/data snapshots for later comparison
|
|
1394
|
+
try {
|
|
1395
|
+
this._args_on_last_render = JSON.parse(JSON.stringify(this.args));
|
|
1396
|
+
}
|
|
1397
|
+
catch (error) {
|
|
1398
|
+
this._args_on_last_render = null;
|
|
1399
|
+
}
|
|
1400
|
+
this._data_on_last_render = JSON.stringify(this.data);
|
|
1401
|
+
return current_render_id;
|
|
1402
|
+
}
|
|
1360
1403
|
// Determine child-finding strategy: If component is off-DOM, children can't register
|
|
1361
1404
|
// via _find_dom_parent() (no parent in DOM to find), so we'll need find() fallback later.
|
|
1362
1405
|
// If attached to DOM, children register normally and we can use the fast _dom_children path.
|
|
@@ -1524,6 +1567,12 @@ class Jqhtml_Component {
|
|
|
1524
1567
|
this._log_lifecycle('render', 'complete');
|
|
1525
1568
|
// Call on_render() with authorization (sync) immediately after render completes
|
|
1526
1569
|
// This ensures event handlers are always re-attached after DOM updates
|
|
1570
|
+
//
|
|
1571
|
+
// NEW ARCHITECTURE: on_render() can now access this.data normally
|
|
1572
|
+
// The HTML cache mode now properly synchronizes - on_render() runs after both:
|
|
1573
|
+
// 1. Cache inject (with on_create() defaults)
|
|
1574
|
+
// 2. Second render (with fresh data from on_load())
|
|
1575
|
+
// Since on_render() always runs with appropriate data, no proxy restriction needed
|
|
1527
1576
|
const renderResult = this._call_lifecycle_sync('on_render');
|
|
1528
1577
|
if (renderResult && typeof renderResult.then === 'function') {
|
|
1529
1578
|
console.warn(`[JQHTML] Component "${this.component_name()}" returned a Promise from on_render(). ` +
|
|
@@ -1544,6 +1593,12 @@ class Jqhtml_Component {
|
|
|
1544
1593
|
}
|
|
1545
1594
|
// Store data snapshot for refresh() comparison
|
|
1546
1595
|
this._data_on_last_render = JSON.stringify(this.data);
|
|
1596
|
+
// HTML CACHE MODE: Mark on_render complete after second render (post-on_load)
|
|
1597
|
+
// This signals to parent components that this component's DOM is fully rendered
|
|
1598
|
+
// with fresh data and ready for HTML snapshot
|
|
1599
|
+
if (this._ready_state >= 2) {
|
|
1600
|
+
this._on_render_complete = true;
|
|
1601
|
+
}
|
|
1547
1602
|
// Return the render ID so callers can check if this render is still current
|
|
1548
1603
|
return current_render_id;
|
|
1549
1604
|
}
|
|
@@ -1617,8 +1672,8 @@ class Jqhtml_Component {
|
|
|
1617
1672
|
`on_create() must be synchronous code. Remove 'async' from the function declaration.`);
|
|
1618
1673
|
await result; // Still wait for it to avoid breaking existing code
|
|
1619
1674
|
}
|
|
1620
|
-
// CACHE
|
|
1621
|
-
// This happens after on_create() but before render, allowing instant first render
|
|
1675
|
+
// CACHE CHECK - Read from cache based on cache mode ('data' or 'html')
|
|
1676
|
+
// This happens after on_create() but before render, allowing instant first render
|
|
1622
1677
|
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
|
|
1623
1678
|
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
|
|
1624
1679
|
// Check if component implements cache_id() for custom cache key
|
|
@@ -1649,21 +1704,48 @@ class Jqhtml_Component {
|
|
|
1649
1704
|
if (window.jqhtml?.debug?.verbose) {
|
|
1650
1705
|
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) has non-serializable args - caching disabled`, { uncacheable_property });
|
|
1651
1706
|
}
|
|
1652
|
-
return
|
|
1653
|
-
}
|
|
1654
|
-
if (window.jqhtml?.debug?.verbose) {
|
|
1655
|
-
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) checking cache in create()`, { cache_key, has_cache_key_set: Jqhtml_Local_Storage.has_cache_key() });
|
|
1656
|
-
}
|
|
1657
|
-
const cached_data = Jqhtml_Local_Storage.get(cache_key);
|
|
1658
|
-
if (cached_data !== null) {
|
|
1659
|
-
this.data = cached_data;
|
|
1660
|
-
if (window.jqhtml?.debug?.verbose) {
|
|
1661
|
-
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) hydrated from cache in create()`, { cache_key, data: this.data });
|
|
1662
|
-
}
|
|
1707
|
+
// Don't return - continue to snapshot this.data for on_load restoration
|
|
1663
1708
|
}
|
|
1664
1709
|
else {
|
|
1710
|
+
// Store cache key for later use
|
|
1711
|
+
this._cache_key = cache_key;
|
|
1712
|
+
// Get cache mode
|
|
1713
|
+
const cache_mode = Jqhtml_Local_Storage.get_cache_mode();
|
|
1665
1714
|
if (window.jqhtml?.debug?.verbose) {
|
|
1666
|
-
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) cache
|
|
1715
|
+
console.log(`[Cache ${cache_mode}] Component ${this._cid} (${this.component_name()}) checking cache in create()`, { cache_key, cache_mode, has_cache_key_set: Jqhtml_Local_Storage.has_cache_key() });
|
|
1716
|
+
}
|
|
1717
|
+
if (cache_mode === 'html') {
|
|
1718
|
+
// HTML cache mode - check for cached HTML to inject on first render
|
|
1719
|
+
const html_cache_key = `${cache_key}::html`;
|
|
1720
|
+
const cached_html = Jqhtml_Local_Storage.get(html_cache_key);
|
|
1721
|
+
if (cached_html !== null && typeof cached_html === 'string') {
|
|
1722
|
+
// Store cached HTML for injection in _render()
|
|
1723
|
+
this._cached_html = cached_html;
|
|
1724
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
1725
|
+
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) found cached HTML`, { cache_key: html_cache_key, html_length: cached_html.length });
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
else {
|
|
1729
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
1730
|
+
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) cache miss`, { cache_key: html_cache_key });
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
else {
|
|
1735
|
+
// Data cache mode (default) - check for cached data to hydrate this.data
|
|
1736
|
+
const cached_data = Jqhtml_Local_Storage.get(cache_key);
|
|
1737
|
+
if (cached_data !== null && typeof cached_data === 'object') {
|
|
1738
|
+
// Hydrate this.data with cached data
|
|
1739
|
+
this.data = cached_data;
|
|
1740
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
1741
|
+
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) hydrated from cache`, { cache_key, data: cached_data });
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
else {
|
|
1745
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
1746
|
+
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) cache miss`, { cache_key });
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1667
1749
|
}
|
|
1668
1750
|
}
|
|
1669
1751
|
// Snapshot this.data after on_create() completes
|
|
@@ -1853,8 +1935,7 @@ class Jqhtml_Component {
|
|
|
1853
1935
|
// Always clear loading flag and complete coordination
|
|
1854
1936
|
this.__loading = false;
|
|
1855
1937
|
complete_coordination();
|
|
1856
|
-
//
|
|
1857
|
-
this.__data_frozen = true;
|
|
1938
|
+
// Note: this.data stays unfrozen until after normalization below
|
|
1858
1939
|
}
|
|
1859
1940
|
// Validate that on_load() only modified this.data
|
|
1860
1941
|
let argsAfterLoad = null;
|
|
@@ -1883,12 +1964,43 @@ class Jqhtml_Component {
|
|
|
1883
1964
|
` ❌ this.${newProperties[0]} = value;\n` +
|
|
1884
1965
|
` ✅ this.data.${newProperties[0]} = value;`);
|
|
1885
1966
|
}
|
|
1886
|
-
//
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1967
|
+
// DATA MODE: Normalize this.data through serialize/deserialize round-trip
|
|
1968
|
+
// This ensures "hot" data (fresh from on_load) behaves identically to "cold" data
|
|
1969
|
+
// (restored from cache). Unregistered class instances become plain objects immediately,
|
|
1970
|
+
// so developers catch missing class registrations during development rather than
|
|
1971
|
+
// only after a page reload when data comes from cache.
|
|
1972
|
+
const cache_mode = Jqhtml_Local_Storage.get_cache_mode();
|
|
1973
|
+
if (cache_mode === 'data') {
|
|
1974
|
+
const normalized = Jqhtml_Local_Storage.normalize_for_cache(this.data);
|
|
1975
|
+
this.data = normalized;
|
|
1890
1976
|
if (window.jqhtml?.debug?.verbose) {
|
|
1891
|
-
console.log(`[Cache] Component ${this._cid} (${this.component_name()})
|
|
1977
|
+
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) normalized this.data after on_load()`, { data: this.data });
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
// Freeze this.data after normalization completes
|
|
1981
|
+
this.__data_frozen = true;
|
|
1982
|
+
// CACHE WRITE - If data changed during on_load(), write to cache based on mode
|
|
1983
|
+
const data_after_load = JSON.stringify(this.data);
|
|
1984
|
+
const data_changed = data_after_load !== data_before_load;
|
|
1985
|
+
// Track if component is "dynamic" (this.data changed during on_load)
|
|
1986
|
+
// Used by HTML cache mode for synchronization - static parents don't block children
|
|
1987
|
+
this._is_dynamic = data_changed && data_after_load !== '{}';
|
|
1988
|
+
if (this._is_dynamic) {
|
|
1989
|
+
if (this._cache_key) {
|
|
1990
|
+
if (cache_mode === 'html') {
|
|
1991
|
+
// HTML cache mode - flag to cache HTML after children ready in _ready()
|
|
1992
|
+
this._should_cache_html_after_ready = true;
|
|
1993
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
1994
|
+
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) will cache HTML after ready`, { cache_key: this._cache_key });
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
else {
|
|
1998
|
+
// Data cache mode (default) - write this.data to cache
|
|
1999
|
+
Jqhtml_Local_Storage.set(this._cache_key, this.data);
|
|
2000
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
2001
|
+
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) cached data after on_load()`, { cache_key: this._cache_key, data: this.data });
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
1892
2004
|
}
|
|
1893
2005
|
}
|
|
1894
2006
|
this._ready_state = 2;
|
|
@@ -1906,6 +2018,34 @@ class Jqhtml_Component {
|
|
|
1906
2018
|
if (this._stopped || this._ready_state >= 4)
|
|
1907
2019
|
return;
|
|
1908
2020
|
this._log_lifecycle('ready', 'start');
|
|
2021
|
+
// HTML CACHE MODE - New synchronization architecture:
|
|
2022
|
+
// 1. Wait for all children to complete on_render (post-on_load)
|
|
2023
|
+
// 2. Take HTML snapshot BEFORE waiting for children ready
|
|
2024
|
+
// 3. This ensures we capture the DOM after on_render but before on_ready manipulations
|
|
2025
|
+
if (this._should_cache_html_after_ready && this._cache_key) {
|
|
2026
|
+
// Wait for all children to complete their on_render
|
|
2027
|
+
await this._wait_for_children_on_render();
|
|
2028
|
+
// Only cache if this component is dynamic (data changed during on_load)
|
|
2029
|
+
// Static parents don't cache - they just let children proceed
|
|
2030
|
+
if (this._is_dynamic) {
|
|
2031
|
+
this._should_cache_html_after_ready = false;
|
|
2032
|
+
// Cache the rendered HTML (async import to avoid circular dependency)
|
|
2033
|
+
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
|
|
2034
|
+
const html = this.$.html();
|
|
2035
|
+
const html_cache_key = `${this._cache_key}::html`;
|
|
2036
|
+
Jqhtml_Local_Storage.set(html_cache_key, html);
|
|
2037
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
2038
|
+
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) cached HTML after children on_render complete`, { cache_key: html_cache_key, html_length: html.length });
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
else {
|
|
2042
|
+
// Static component - just clear the flag, don't cache
|
|
2043
|
+
this._should_cache_html_after_ready = false;
|
|
2044
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
2045
|
+
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) is static (no data change) - skipping HTML cache`);
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
1909
2049
|
// Wait for all children to reach ready state (bottom-up execution)
|
|
1910
2050
|
await this._wait_for_children_ready();
|
|
1911
2051
|
await this._call_lifecycle('on_ready');
|
|
@@ -1986,6 +2126,47 @@ class Jqhtml_Component {
|
|
|
1986
2126
|
// Wait for all children to be ready
|
|
1987
2127
|
await Promise.all(ready_promises);
|
|
1988
2128
|
}
|
|
2129
|
+
/**
|
|
2130
|
+
* Wait for all child components to complete on_render (post-on_load)
|
|
2131
|
+
* Used by HTML cache mode to ensure DOM is fully rendered before taking snapshot
|
|
2132
|
+
*
|
|
2133
|
+
* HTML CACHE ARCHITECTURE:
|
|
2134
|
+
* - Parent waits for all children to complete their on_render after on_load
|
|
2135
|
+
* - This ensures the HTML snapshot captures fully rendered DOM
|
|
2136
|
+
* - Static parents (is_dynamic=false) don't block - they immediately let children proceed
|
|
2137
|
+
*
|
|
2138
|
+
* @private
|
|
2139
|
+
*/
|
|
2140
|
+
async _wait_for_children_on_render() {
|
|
2141
|
+
const children = this._get_dom_children();
|
|
2142
|
+
if (children.length === 0) {
|
|
2143
|
+
return; // No children, nothing to wait for
|
|
2144
|
+
}
|
|
2145
|
+
// Create promises for each child that hasn't completed on_render yet
|
|
2146
|
+
const render_promises = [];
|
|
2147
|
+
for (const child of children) {
|
|
2148
|
+
// If child already completed on_render post-on_load, skip
|
|
2149
|
+
if (child._on_render_complete) {
|
|
2150
|
+
continue;
|
|
2151
|
+
}
|
|
2152
|
+
// Create promise that resolves when child completes on_render
|
|
2153
|
+
const render_promise = new Promise((resolve) => {
|
|
2154
|
+
// Poll for completion (simple approach - could use events for more efficiency)
|
|
2155
|
+
const check = () => {
|
|
2156
|
+
if (child._on_render_complete || child._stopped) {
|
|
2157
|
+
resolve();
|
|
2158
|
+
}
|
|
2159
|
+
else {
|
|
2160
|
+
setTimeout(check, 10);
|
|
2161
|
+
}
|
|
2162
|
+
};
|
|
2163
|
+
check();
|
|
2164
|
+
});
|
|
2165
|
+
render_promises.push(render_promise);
|
|
2166
|
+
}
|
|
2167
|
+
// Wait for all children to complete on_render
|
|
2168
|
+
await Promise.all(render_promises);
|
|
2169
|
+
}
|
|
1989
2170
|
/**
|
|
1990
2171
|
* Reload component - re-fetch data and re-render (debounced)
|
|
1991
2172
|
*
|
|
@@ -2098,19 +2279,38 @@ class Jqhtml_Component {
|
|
|
2098
2279
|
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
|
2099
2280
|
cache_key = result.key;
|
|
2100
2281
|
}
|
|
2101
|
-
//
|
|
2282
|
+
// Check for cached data/html when args changed
|
|
2102
2283
|
if (cache_key !== null) {
|
|
2103
|
-
const
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2284
|
+
const cache_mode = Jqhtml_Local_Storage.get_cache_mode();
|
|
2285
|
+
this._cache_key = cache_key;
|
|
2286
|
+
if (cache_mode === 'html') {
|
|
2287
|
+
// HTML cache mode - check for cached HTML
|
|
2288
|
+
const html_cache_key = `${cache_key}::html`;
|
|
2289
|
+
const cached_html = Jqhtml_Local_Storage.get(html_cache_key);
|
|
2290
|
+
if (cached_html !== null && typeof cached_html === 'string') {
|
|
2291
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
2292
|
+
console.log(`[Cache html] reload() - Component ${this._cid} (${this.component_name()}) found cached HTML (args changed)`, { cache_key: html_cache_key, html_length: cached_html.length });
|
|
2293
|
+
}
|
|
2294
|
+
// Store cached HTML for injection in _render()
|
|
2295
|
+
this._cached_html = cached_html;
|
|
2296
|
+
this.render();
|
|
2297
|
+
rendered_from_cache = true;
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
else {
|
|
2301
|
+
// Data cache mode (default) - check for cached data
|
|
2302
|
+
const cached_data = Jqhtml_Local_Storage.get(cache_key);
|
|
2303
|
+
if (cached_data !== null && typeof cached_data === 'object' && JSON.stringify(cached_data) !== '{}') {
|
|
2304
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
2305
|
+
console.log(`[Cache data] reload() - Component ${this._cid} (${this.component_name()}) hydrated from cache (args changed)`, { cache_key, data: cached_data });
|
|
2306
|
+
}
|
|
2307
|
+
// Hydrate this.data with cached data
|
|
2308
|
+
this.__data_frozen = false;
|
|
2309
|
+
this.data = cached_data;
|
|
2310
|
+
this.__data_frozen = true;
|
|
2311
|
+
this.render();
|
|
2312
|
+
rendered_from_cache = true;
|
|
2107
2313
|
}
|
|
2108
|
-
// Unfreeze to set cached data, then re-freeze
|
|
2109
|
-
this.__data_frozen = false;
|
|
2110
|
-
this.data = cached_data;
|
|
2111
|
-
this.__data_frozen = true;
|
|
2112
|
-
await this.render();
|
|
2113
|
-
rendered_from_cache = true;
|
|
2114
2314
|
}
|
|
2115
2315
|
}
|
|
2116
2316
|
}
|
|
@@ -2130,12 +2330,25 @@ class Jqhtml_Component {
|
|
|
2130
2330
|
// Freeze this.data after on_load() completes
|
|
2131
2331
|
this.__data_frozen = true;
|
|
2132
2332
|
}
|
|
2333
|
+
// Import for cache operations
|
|
2334
|
+
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
|
|
2335
|
+
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
|
|
2336
|
+
// DATA MODE: Normalize this.data through serialize/deserialize round-trip
|
|
2337
|
+
// (Same as in _load() - ensures hot/cold cache parity)
|
|
2338
|
+
const reload_cache_mode = Jqhtml_Local_Storage.get_cache_mode();
|
|
2339
|
+
if (reload_cache_mode === 'data') {
|
|
2340
|
+
this.__data_frozen = false;
|
|
2341
|
+
const normalized = Jqhtml_Local_Storage.normalize_for_cache(this.data);
|
|
2342
|
+
this.data = normalized;
|
|
2343
|
+
this.__data_frozen = true;
|
|
2344
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
2345
|
+
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) normalized this.data after on_load() in reload()`, { data: this.data });
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2133
2348
|
// Check if data changed during on_load() - if so, update cache (but not if empty)
|
|
2134
2349
|
const data_after_load = JSON.stringify(this.data);
|
|
2135
2350
|
const data_changed = data_after_load !== data_before_load;
|
|
2136
2351
|
if (data_changed && data_after_load !== '{}') {
|
|
2137
|
-
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
|
|
2138
|
-
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
|
|
2139
2352
|
// Check if component implements cache_id() for custom cache key
|
|
2140
2353
|
let cache_key = null;
|
|
2141
2354
|
if (typeof this.cache_id === 'function') {
|
|
@@ -2153,11 +2366,22 @@ class Jqhtml_Component {
|
|
|
2153
2366
|
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
|
2154
2367
|
cache_key = result.key;
|
|
2155
2368
|
}
|
|
2156
|
-
//
|
|
2369
|
+
// Write to cache based on mode
|
|
2157
2370
|
if (cache_key !== null) {
|
|
2158
|
-
|
|
2159
|
-
if (
|
|
2160
|
-
|
|
2371
|
+
this._cache_key = cache_key;
|
|
2372
|
+
if (reload_cache_mode === 'html') {
|
|
2373
|
+
// HTML cache mode - flag to cache HTML after children ready in _ready()
|
|
2374
|
+
this._should_cache_html_after_ready = true;
|
|
2375
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
2376
|
+
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) will cache HTML after ready in reload()`, { cache_key });
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
else {
|
|
2380
|
+
// Data cache mode (default) - write this.data to cache
|
|
2381
|
+
Jqhtml_Local_Storage.set(cache_key, this.data);
|
|
2382
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
2383
|
+
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) cached data after on_load() in reload()`, { cache_key, data: this.data });
|
|
2384
|
+
}
|
|
2161
2385
|
}
|
|
2162
2386
|
}
|
|
2163
2387
|
}
|
|
@@ -2283,6 +2507,15 @@ class Jqhtml_Component {
|
|
|
2283
2507
|
* @private
|
|
2284
2508
|
*/
|
|
2285
2509
|
_should_rerender() {
|
|
2510
|
+
// HTML CACHE MODE - If we used cached HTML, always re-render to get live components
|
|
2511
|
+
if (this._used_cached_html) {
|
|
2512
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
2513
|
+
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) forcing re-render after cached HTML`);
|
|
2514
|
+
}
|
|
2515
|
+
// Clear the flag
|
|
2516
|
+
this._used_cached_html = false;
|
|
2517
|
+
return true;
|
|
2518
|
+
}
|
|
2286
2519
|
// Compare current data state with data state before initial render
|
|
2287
2520
|
const currentDataState = JSON.stringify(this.data);
|
|
2288
2521
|
const dataChanged = this._data_before_render !== currentDataState;
|
|
@@ -3510,6 +3743,13 @@ if (typeof window !== 'undefined' && window.jQuery) {
|
|
|
3510
3743
|
* - **Quota management**: Auto-clears storage when full and retries operation
|
|
3511
3744
|
* - **Scope validation**: Clears storage when cache key changes
|
|
3512
3745
|
* - **Developer-friendly keys**: Scoped suffix allows easy inspection in dev tools
|
|
3746
|
+
* - **Class-aware serialization**: ES6 class instances serialize and restore properly
|
|
3747
|
+
*
|
|
3748
|
+
* Class-Aware Serialization:
|
|
3749
|
+
* ES6 class instances can be serialized and deserialized if registered via
|
|
3750
|
+
* register_cache_class(). Classes are wrapped as {__jqhtml_class__: "Name", __jqhtml_props__: {...}}
|
|
3751
|
+
* and restored to proper instances on retrieval. Nested class instances in objects
|
|
3752
|
+
* and arrays are handled recursively.
|
|
3513
3753
|
*
|
|
3514
3754
|
* Scoping Strategy:
|
|
3515
3755
|
* Storage is scoped by a user-provided cache key (typically a session identifier,
|
|
@@ -3531,13 +3771,20 @@ if (typeof window !== 'undefined' && window.jQuery) {
|
|
|
3531
3771
|
* once. This ensures the application continues functioning even when storage is full.
|
|
3532
3772
|
*
|
|
3533
3773
|
* Usage:
|
|
3774
|
+
* // Register classes that need to be cached (call once at startup)
|
|
3775
|
+
* jqhtml.register_cache_class(Contact_Model);
|
|
3776
|
+
* jqhtml.register_cache_class(User_Profile);
|
|
3777
|
+
*
|
|
3534
3778
|
* // Must set cache key first (typically done once on page load)
|
|
3535
3779
|
* Jqhtml_Local_Storage.set_cache_key('user_123');
|
|
3536
3780
|
*
|
|
3537
|
-
* // Then use storage normally
|
|
3538
|
-
* Jqhtml_Local_Storage.set('cached_component', {
|
|
3781
|
+
* // Then use storage normally - ES6 classes serialize automatically
|
|
3782
|
+
* Jqhtml_Local_Storage.set('cached_component', {
|
|
3783
|
+
* contact: new Contact_Model(),
|
|
3784
|
+
* timestamp: Date.now()
|
|
3785
|
+
* });
|
|
3539
3786
|
* const cached = Jqhtml_Local_Storage.get('cached_component');
|
|
3540
|
-
*
|
|
3787
|
+
* // cached.contact instanceof Contact_Model === true
|
|
3541
3788
|
*
|
|
3542
3789
|
* IMPORTANT - Volatile Storage:
|
|
3543
3790
|
* Storage can be cleared at any time due to:
|
|
@@ -3553,14 +3800,290 @@ if (typeof window !== 'undefined' && window.jQuery) {
|
|
|
3553
3800
|
*
|
|
3554
3801
|
* @internal This class is not exposed in the public API
|
|
3555
3802
|
*/
|
|
3803
|
+
// ============================================================================
|
|
3804
|
+
// Class Registry for Serialization
|
|
3805
|
+
// ============================================================================
|
|
3806
|
+
// Registry mapping class names to constructors
|
|
3807
|
+
const class_registry = Object.create(null);
|
|
3808
|
+
// Markers for serialized class instances (unique to avoid collisions with user data)
|
|
3809
|
+
const CLASS_MARKER = '__jqhtml_class__';
|
|
3810
|
+
const PROPS_MARKER = '__jqhtml_props__';
|
|
3811
|
+
/**
|
|
3812
|
+
* Register a class for cache serialization/deserialization.
|
|
3813
|
+
* Must be called before attempting to cache instances of this class.
|
|
3814
|
+
*
|
|
3815
|
+
* @param klass - The class constructor to register
|
|
3816
|
+
* @throws Error if klass is not a named function/class
|
|
3817
|
+
*/
|
|
3818
|
+
function register_cache_class(klass) {
|
|
3819
|
+
if (typeof klass !== 'function' || !klass.name) {
|
|
3820
|
+
throw new Error('[JQHTML Cache] register_cache_class requires a named class constructor');
|
|
3821
|
+
}
|
|
3822
|
+
class_registry[klass.name] = klass;
|
|
3823
|
+
}
|
|
3824
|
+
// ============================================================================
|
|
3825
|
+
// Serialization (Object → String)
|
|
3826
|
+
// ============================================================================
|
|
3827
|
+
/**
|
|
3828
|
+
* Serialize a value to JSON string, handling ES6 class instances recursively.
|
|
3829
|
+
* Returns null if serialization fails for any reason.
|
|
3830
|
+
*
|
|
3831
|
+
* @param value - The value to serialize
|
|
3832
|
+
* @param verbose - Whether to log warnings
|
|
3833
|
+
* @returns Serialized JSON string, or null if serialization failed
|
|
3834
|
+
*/
|
|
3835
|
+
function serialize_value(value, verbose) {
|
|
3836
|
+
try {
|
|
3837
|
+
const seen = new WeakSet();
|
|
3838
|
+
const processed = process_for_serialization(value, verbose, seen);
|
|
3839
|
+
if (processed === undefined) {
|
|
3840
|
+
// Serialization failed - undefined signals failure
|
|
3841
|
+
return null;
|
|
3842
|
+
}
|
|
3843
|
+
return JSON.stringify(processed);
|
|
3844
|
+
}
|
|
3845
|
+
catch (e) {
|
|
3846
|
+
if (verbose) {
|
|
3847
|
+
console.warn('[JQHTML Cache] Serialization failed:', e);
|
|
3848
|
+
}
|
|
3849
|
+
return null;
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
/**
|
|
3853
|
+
* Recursively process a value for serialization.
|
|
3854
|
+
* Returns undefined if serialization should fail (unserializable value encountered).
|
|
3855
|
+
*
|
|
3856
|
+
* @param value - The value to process
|
|
3857
|
+
* @param verbose - Whether to log warnings
|
|
3858
|
+
* @param seen - WeakSet to detect circular references
|
|
3859
|
+
* @returns Processed value ready for JSON.stringify, or undefined if failed
|
|
3860
|
+
*/
|
|
3861
|
+
function process_for_serialization(value, verbose, seen) {
|
|
3862
|
+
// Handle null and undefined
|
|
3863
|
+
if (value === null) {
|
|
3864
|
+
return null;
|
|
3865
|
+
}
|
|
3866
|
+
if (value === undefined) {
|
|
3867
|
+
return undefined; // JSON.stringify will omit this property
|
|
3868
|
+
}
|
|
3869
|
+
// Handle primitives
|
|
3870
|
+
if (typeof value !== 'object') {
|
|
3871
|
+
// string, number, boolean are fine
|
|
3872
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
3873
|
+
return value;
|
|
3874
|
+
}
|
|
3875
|
+
// bigint, symbol, function cannot be serialized
|
|
3876
|
+
if (verbose) {
|
|
3877
|
+
console.warn(`[JQHTML Cache] Cannot serialize ${typeof value} - value will be omitted`);
|
|
3878
|
+
}
|
|
3879
|
+
// Return undefined to omit this value (not fail entire serialization)
|
|
3880
|
+
return undefined;
|
|
3881
|
+
}
|
|
3882
|
+
// Handle circular references
|
|
3883
|
+
if (seen.has(value)) {
|
|
3884
|
+
if (verbose) {
|
|
3885
|
+
console.warn('[JQHTML Cache] Circular reference detected - cannot serialize');
|
|
3886
|
+
}
|
|
3887
|
+
return undefined; // Fail entire serialization
|
|
3888
|
+
}
|
|
3889
|
+
seen.add(value);
|
|
3890
|
+
// Handle arrays
|
|
3891
|
+
if (Array.isArray(value)) {
|
|
3892
|
+
const result = [];
|
|
3893
|
+
for (let i = 0; i < value.length; i++) {
|
|
3894
|
+
const item = value[i];
|
|
3895
|
+
const processed = process_for_serialization(item, verbose, seen);
|
|
3896
|
+
// For arrays, we keep undefined as null to preserve indices
|
|
3897
|
+
result.push(processed === undefined ? null : processed);
|
|
3898
|
+
}
|
|
3899
|
+
return result;
|
|
3900
|
+
}
|
|
3901
|
+
// Handle Date objects specially
|
|
3902
|
+
if (value instanceof Date) {
|
|
3903
|
+
return {
|
|
3904
|
+
[CLASS_MARKER]: 'Date',
|
|
3905
|
+
[PROPS_MARKER]: value.toISOString()
|
|
3906
|
+
};
|
|
3907
|
+
}
|
|
3908
|
+
// Handle Map objects
|
|
3909
|
+
if (value instanceof Map) {
|
|
3910
|
+
const entries = [];
|
|
3911
|
+
for (const [k, v] of value) {
|
|
3912
|
+
const processedKey = process_for_serialization(k, verbose, seen);
|
|
3913
|
+
const processedValue = process_for_serialization(v, verbose, seen);
|
|
3914
|
+
entries.push([processedKey, processedValue]);
|
|
3915
|
+
}
|
|
3916
|
+
return {
|
|
3917
|
+
[CLASS_MARKER]: 'Map',
|
|
3918
|
+
[PROPS_MARKER]: entries
|
|
3919
|
+
};
|
|
3920
|
+
}
|
|
3921
|
+
// Handle Set objects
|
|
3922
|
+
if (value instanceof Set) {
|
|
3923
|
+
const items = [];
|
|
3924
|
+
for (const item of value) {
|
|
3925
|
+
items.push(process_for_serialization(item, verbose, seen));
|
|
3926
|
+
}
|
|
3927
|
+
return {
|
|
3928
|
+
[CLASS_MARKER]: 'Set',
|
|
3929
|
+
[PROPS_MARKER]: items
|
|
3930
|
+
};
|
|
3931
|
+
}
|
|
3932
|
+
// Get the constructor
|
|
3933
|
+
const ctor = value.constructor;
|
|
3934
|
+
// Handle registered class instances
|
|
3935
|
+
if (ctor && ctor.name && class_registry[ctor.name]) {
|
|
3936
|
+
const props = {};
|
|
3937
|
+
// Extract own enumerable properties (bypass any toJSON method)
|
|
3938
|
+
for (const key of Object.keys(value)) {
|
|
3939
|
+
const propValue = value[key];
|
|
3940
|
+
const processed = process_for_serialization(propValue, verbose, seen);
|
|
3941
|
+
// Only include if not undefined (failed serialization)
|
|
3942
|
+
if (processed !== undefined || propValue === undefined) {
|
|
3943
|
+
props[key] = processed;
|
|
3944
|
+
}
|
|
3945
|
+
}
|
|
3946
|
+
return {
|
|
3947
|
+
[CLASS_MARKER]: ctor.name,
|
|
3948
|
+
[PROPS_MARKER]: props
|
|
3949
|
+
};
|
|
3950
|
+
}
|
|
3951
|
+
// Handle plain objects (constructor is Object or no constructor)
|
|
3952
|
+
if (ctor === Object || ctor === undefined || ctor === null) {
|
|
3953
|
+
const result = {};
|
|
3954
|
+
for (const key of Object.keys(value)) {
|
|
3955
|
+
const propValue = value[key];
|
|
3956
|
+
const processed = process_for_serialization(propValue, verbose, seen);
|
|
3957
|
+
// Only include if not undefined (unless original was undefined)
|
|
3958
|
+
if (processed !== undefined || propValue === undefined) {
|
|
3959
|
+
result[key] = processed;
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
3962
|
+
return result;
|
|
3963
|
+
}
|
|
3964
|
+
// Unregistered class instance - convert to plain object for hot/cold parity
|
|
3965
|
+
// This ensures developers catch missing class registrations during development
|
|
3966
|
+
// (methods will be lost, only properties preserved)
|
|
3967
|
+
if (verbose) {
|
|
3968
|
+
console.warn(`[JQHTML Cache] Converting unregistered class "${ctor.name}" to plain object. ` +
|
|
3969
|
+
`Methods will be lost. Call jqhtml.register_cache_class(${ctor.name}) to preserve the class.`);
|
|
3970
|
+
}
|
|
3971
|
+
// Convert to plain object - properties are preserved, prototype methods are lost
|
|
3972
|
+
const result = {};
|
|
3973
|
+
for (const key of Object.keys(value)) {
|
|
3974
|
+
const propValue = value[key];
|
|
3975
|
+
const processed = process_for_serialization(propValue, verbose, seen);
|
|
3976
|
+
// Only include if not undefined (unless original was undefined)
|
|
3977
|
+
if (processed !== undefined || propValue === undefined) {
|
|
3978
|
+
result[key] = processed;
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
return result;
|
|
3982
|
+
}
|
|
3983
|
+
// ============================================================================
|
|
3984
|
+
// Deserialization (String → Object)
|
|
3985
|
+
// ============================================================================
|
|
3986
|
+
/**
|
|
3987
|
+
* Deserialize a JSON string, restoring ES6 class instances.
|
|
3988
|
+
* Returns null if deserialization fails.
|
|
3989
|
+
*
|
|
3990
|
+
* @param str - The JSON string to deserialize
|
|
3991
|
+
* @param verbose - Whether to log warnings
|
|
3992
|
+
* @returns Deserialized value with class instances restored, or null if failed
|
|
3993
|
+
*/
|
|
3994
|
+
function deserialize_value(str, verbose) {
|
|
3995
|
+
try {
|
|
3996
|
+
const parsed = JSON.parse(str);
|
|
3997
|
+
return process_for_deserialization(parsed, verbose);
|
|
3998
|
+
}
|
|
3999
|
+
catch (e) {
|
|
4000
|
+
if (verbose) {
|
|
4001
|
+
console.warn('[JQHTML Cache] Deserialization failed:', e);
|
|
4002
|
+
}
|
|
4003
|
+
return null;
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
/**
|
|
4007
|
+
* Recursively process a parsed value, restoring class instances.
|
|
4008
|
+
*
|
|
4009
|
+
* @param value - The parsed value to process
|
|
4010
|
+
* @param verbose - Whether to log warnings
|
|
4011
|
+
* @returns Processed value with class instances restored
|
|
4012
|
+
*/
|
|
4013
|
+
function process_for_deserialization(value, verbose) {
|
|
4014
|
+
// Handle primitives and null
|
|
4015
|
+
if (value === null || value === undefined || typeof value !== 'object') {
|
|
4016
|
+
return value;
|
|
4017
|
+
}
|
|
4018
|
+
// Handle arrays
|
|
4019
|
+
if (Array.isArray(value)) {
|
|
4020
|
+
return value.map(item => process_for_deserialization(item, verbose));
|
|
4021
|
+
}
|
|
4022
|
+
// Check for class marker
|
|
4023
|
+
if (value[CLASS_MARKER] !== undefined && value[PROPS_MARKER] !== undefined) {
|
|
4024
|
+
const class_name = value[CLASS_MARKER];
|
|
4025
|
+
const props = value[PROPS_MARKER];
|
|
4026
|
+
// Handle built-in types
|
|
4027
|
+
if (class_name === 'Date') {
|
|
4028
|
+
return new Date(props);
|
|
4029
|
+
}
|
|
4030
|
+
if (class_name === 'Map') {
|
|
4031
|
+
const map = new Map();
|
|
4032
|
+
for (const [k, v] of props) {
|
|
4033
|
+
map.set(process_for_deserialization(k, verbose), process_for_deserialization(v, verbose));
|
|
4034
|
+
}
|
|
4035
|
+
return map;
|
|
4036
|
+
}
|
|
4037
|
+
if (class_name === 'Set') {
|
|
4038
|
+
const set = new Set();
|
|
4039
|
+
for (const item of props) {
|
|
4040
|
+
set.add(process_for_deserialization(item, verbose));
|
|
4041
|
+
}
|
|
4042
|
+
return set;
|
|
4043
|
+
}
|
|
4044
|
+
// Look up registered class
|
|
4045
|
+
const klass = class_registry[class_name];
|
|
4046
|
+
if (!klass) {
|
|
4047
|
+
if (verbose) {
|
|
4048
|
+
console.warn(`[JQHTML Cache] Cannot restore class "${class_name}" - not registered. ` +
|
|
4049
|
+
`Data will be returned as plain object. ` +
|
|
4050
|
+
`Call jqhtml.register_cache_class(${class_name}) to restore class instances.`);
|
|
4051
|
+
}
|
|
4052
|
+
// Return as plain object rather than failing completely
|
|
4053
|
+
return process_for_deserialization(props, verbose);
|
|
4054
|
+
}
|
|
4055
|
+
// Create instance without calling constructor and assign properties
|
|
4056
|
+
try {
|
|
4057
|
+
const instance = Object.create(klass.prototype);
|
|
4058
|
+
const processed_props = process_for_deserialization(props, verbose);
|
|
4059
|
+
Object.assign(instance, processed_props);
|
|
4060
|
+
return instance;
|
|
4061
|
+
}
|
|
4062
|
+
catch (e) {
|
|
4063
|
+
if (verbose) {
|
|
4064
|
+
console.warn(`[JQHTML Cache] Failed to restore class "${class_name}":`, e);
|
|
4065
|
+
}
|
|
4066
|
+
// Return null to signal cache should be treated as miss
|
|
4067
|
+
return null;
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
4070
|
+
// Plain object - process recursively
|
|
4071
|
+
const result = {};
|
|
4072
|
+
for (const key of Object.keys(value)) {
|
|
4073
|
+
result[key] = process_for_deserialization(value[key], verbose);
|
|
4074
|
+
}
|
|
4075
|
+
return result;
|
|
4076
|
+
}
|
|
3556
4077
|
class Jqhtml_Local_Storage {
|
|
3557
4078
|
/**
|
|
3558
4079
|
* Set the cache key and initialize storage
|
|
3559
4080
|
* Must be called before any get/set operations
|
|
3560
4081
|
* @param {string} cache_key - Unique identifier for this cache scope
|
|
4082
|
+
* @param {CacheMode} cache_mode - Cache strategy: 'data' (default, recommended) or 'html'
|
|
3561
4083
|
*/
|
|
3562
|
-
static set_cache_key(cache_key) {
|
|
4084
|
+
static set_cache_key(cache_key, cache_mode = 'data') {
|
|
3563
4085
|
this._cache_key = cache_key;
|
|
4086
|
+
this._cache_mode = cache_mode;
|
|
3564
4087
|
this._init();
|
|
3565
4088
|
}
|
|
3566
4089
|
/**
|
|
@@ -3570,6 +4093,13 @@ class Jqhtml_Local_Storage {
|
|
|
3570
4093
|
static has_cache_key() {
|
|
3571
4094
|
return this._cache_key !== null;
|
|
3572
4095
|
}
|
|
4096
|
+
/**
|
|
4097
|
+
* Get the current cache mode
|
|
4098
|
+
* @returns {CacheMode} Current cache mode ('data' or 'html')
|
|
4099
|
+
*/
|
|
4100
|
+
static get_cache_mode() {
|
|
4101
|
+
return this._cache_mode;
|
|
4102
|
+
}
|
|
3573
4103
|
/**
|
|
3574
4104
|
* Initialize storage system and validate scope
|
|
3575
4105
|
* Called automatically after cache key is set
|
|
@@ -3604,6 +4134,13 @@ class Jqhtml_Local_Storage {
|
|
|
3604
4134
|
return false;
|
|
3605
4135
|
}
|
|
3606
4136
|
}
|
|
4137
|
+
/**
|
|
4138
|
+
* Check if verbose mode is enabled
|
|
4139
|
+
* @private
|
|
4140
|
+
*/
|
|
4141
|
+
static _is_verbose() {
|
|
4142
|
+
return window.jqhtml?.debug?.verbose === true;
|
|
4143
|
+
}
|
|
3607
4144
|
/**
|
|
3608
4145
|
* Validate storage scope and clear JQHTML keys if cache key changed
|
|
3609
4146
|
* Only clears keys prefixed with 'jqhtml::' to preserve other libraries' data
|
|
@@ -3682,34 +4219,90 @@ class Jqhtml_Local_Storage {
|
|
|
3682
4219
|
return this._storage_available === true && this._cache_key !== null && this._initialized;
|
|
3683
4220
|
}
|
|
3684
4221
|
/**
|
|
3685
|
-
* Set item in localStorage
|
|
4222
|
+
* Set item in localStorage with class-aware serialization.
|
|
4223
|
+
*
|
|
4224
|
+
* If serialization fails (e.g., unregistered class instances, circular refs),
|
|
4225
|
+
* the existing cache entry is removed and nothing is stored.
|
|
4226
|
+
*
|
|
3686
4227
|
* @param {string} key - Storage key
|
|
3687
|
-
* @param {*} value - Value to store (
|
|
4228
|
+
* @param {*} value - Value to store (primitives, objects, arrays, or registered class instances)
|
|
3688
4229
|
*/
|
|
3689
4230
|
static set(key, value) {
|
|
3690
4231
|
if (!this._is_ready()) {
|
|
3691
4232
|
return;
|
|
3692
4233
|
}
|
|
4234
|
+
const verbose = this._is_verbose();
|
|
4235
|
+
const scoped_key = this._build_key(key);
|
|
4236
|
+
// Serialize with class-aware processing
|
|
4237
|
+
const serialized = serialize_value(value, verbose);
|
|
4238
|
+
if (serialized === null) {
|
|
4239
|
+
// Serialization failed - remove any existing cache entry
|
|
4240
|
+
if (verbose) {
|
|
4241
|
+
console.warn(`[JQHTML Cache] Failed to serialize value for key "${key}". ` +
|
|
4242
|
+
`Removing existing cache entry if present.`);
|
|
4243
|
+
}
|
|
4244
|
+
try {
|
|
4245
|
+
localStorage.removeItem(scoped_key);
|
|
4246
|
+
}
|
|
4247
|
+
catch (e) {
|
|
4248
|
+
// Ignore removal errors
|
|
4249
|
+
}
|
|
4250
|
+
return;
|
|
4251
|
+
}
|
|
3693
4252
|
// Check size before attempting to store (1MB limit)
|
|
3694
|
-
const serialized = JSON.stringify(value);
|
|
3695
4253
|
const size_bytes = new Blob([serialized]).size;
|
|
3696
4254
|
const size_mb = size_bytes / (1024 * 1024);
|
|
3697
4255
|
if (size_mb > 1) {
|
|
3698
|
-
|
|
4256
|
+
if (verbose) {
|
|
4257
|
+
console.warn(`[JQHTML Cache] Skipping set - value too large (${size_mb.toFixed(2)}MB > 1MB limit)`, { key, size_bytes, size_mb });
|
|
4258
|
+
}
|
|
4259
|
+
// Remove existing cache entry since we can't update it
|
|
4260
|
+
try {
|
|
4261
|
+
localStorage.removeItem(scoped_key);
|
|
4262
|
+
}
|
|
4263
|
+
catch (e) {
|
|
4264
|
+
// Ignore removal errors
|
|
4265
|
+
}
|
|
3699
4266
|
return;
|
|
3700
4267
|
}
|
|
3701
|
-
this._set_item(key,
|
|
4268
|
+
this._set_item(key, serialized);
|
|
3702
4269
|
}
|
|
3703
4270
|
/**
|
|
3704
|
-
* Get item from localStorage
|
|
4271
|
+
* Get item from localStorage with class-aware deserialization.
|
|
4272
|
+
*
|
|
4273
|
+
* If deserialization fails, returns null (as if no cache exists).
|
|
4274
|
+
*
|
|
3705
4275
|
* @param {string} key - Storage key
|
|
3706
|
-
* @returns {*|null}
|
|
4276
|
+
* @returns {*|null} Deserialized value with class instances restored, or null if not found/failed
|
|
3707
4277
|
*/
|
|
3708
4278
|
static get(key) {
|
|
3709
4279
|
if (!this._is_ready()) {
|
|
3710
4280
|
return null;
|
|
3711
4281
|
}
|
|
3712
|
-
|
|
4282
|
+
const verbose = this._is_verbose();
|
|
4283
|
+
const scoped_key = this._build_key(key);
|
|
4284
|
+
try {
|
|
4285
|
+
const serialized = localStorage.getItem(scoped_key);
|
|
4286
|
+
if (serialized === null) {
|
|
4287
|
+
return null;
|
|
4288
|
+
}
|
|
4289
|
+
const result = deserialize_value(serialized, verbose);
|
|
4290
|
+
if (result === null) {
|
|
4291
|
+
// Deserialization failed - treat as cache miss
|
|
4292
|
+
if (verbose) {
|
|
4293
|
+
console.warn(`[JQHTML Cache] Failed to deserialize value for key "${key}". ` +
|
|
4294
|
+
`Treating as cache miss.`);
|
|
4295
|
+
}
|
|
4296
|
+
return null;
|
|
4297
|
+
}
|
|
4298
|
+
return result;
|
|
4299
|
+
}
|
|
4300
|
+
catch (e) {
|
|
4301
|
+
if (verbose) {
|
|
4302
|
+
console.warn('[JQHTML Cache] Failed to get item:', e);
|
|
4303
|
+
}
|
|
4304
|
+
return null;
|
|
4305
|
+
}
|
|
3713
4306
|
}
|
|
3714
4307
|
/**
|
|
3715
4308
|
* Remove item from localStorage
|
|
@@ -3721,14 +4314,49 @@ class Jqhtml_Local_Storage {
|
|
|
3721
4314
|
}
|
|
3722
4315
|
this._remove_item(key);
|
|
3723
4316
|
}
|
|
4317
|
+
/**
|
|
4318
|
+
* Perform a serialize/deserialize round-trip on a value.
|
|
4319
|
+
*
|
|
4320
|
+
* This ensures "hot" data (fresh from on_load) behaves identically to "cold" data
|
|
4321
|
+
* (restored from cache). Unregistered class instances will be converted to plain
|
|
4322
|
+
* objects, exactly as they would be if restored from cache.
|
|
4323
|
+
*
|
|
4324
|
+
* Use this to normalize data after on_load() so developers catch missing class
|
|
4325
|
+
* registrations immediately rather than only after a page reload.
|
|
4326
|
+
*
|
|
4327
|
+
* @param {any} value - The value to normalize
|
|
4328
|
+
* @returns {any} The value after serialize/deserialize round-trip, or original if serialization fails
|
|
4329
|
+
*/
|
|
4330
|
+
static normalize_for_cache(value) {
|
|
4331
|
+
const verbose = this._is_verbose();
|
|
4332
|
+
// Serialize to JSON string
|
|
4333
|
+
const serialized = serialize_value(value, verbose);
|
|
4334
|
+
if (serialized === null) {
|
|
4335
|
+
// Serialization failed completely - return original
|
|
4336
|
+
// (This happens with circular references or other fatal issues)
|
|
4337
|
+
if (verbose) {
|
|
4338
|
+
console.warn('[JQHTML Cache] normalize_for_cache: Serialization failed, returning original value');
|
|
4339
|
+
}
|
|
4340
|
+
return value;
|
|
4341
|
+
}
|
|
4342
|
+
// Deserialize back to object
|
|
4343
|
+
const deserialized = deserialize_value(serialized, verbose);
|
|
4344
|
+
if (deserialized === null) {
|
|
4345
|
+
// Deserialization failed - return original
|
|
4346
|
+
if (verbose) {
|
|
4347
|
+
console.warn('[JQHTML Cache] normalize_for_cache: Deserialization failed, returning original value');
|
|
4348
|
+
}
|
|
4349
|
+
return value;
|
|
4350
|
+
}
|
|
4351
|
+
return deserialized;
|
|
4352
|
+
}
|
|
3724
4353
|
/**
|
|
3725
4354
|
* Internal set implementation with scope validation and quota handling
|
|
3726
4355
|
* @param {string} key
|
|
3727
|
-
* @param {*} value - Original value (not used, kept for clarity)
|
|
3728
4356
|
* @param {string} serialized - Pre-serialized JSON string
|
|
3729
4357
|
* @private
|
|
3730
4358
|
*/
|
|
3731
|
-
static _set_item(key,
|
|
4359
|
+
static _set_item(key, serialized) {
|
|
3732
4360
|
// Validate scope before every write
|
|
3733
4361
|
this._validate_scope();
|
|
3734
4362
|
const scoped_key = this._build_key(key);
|
|
@@ -3754,26 +4382,6 @@ class Jqhtml_Local_Storage {
|
|
|
3754
4382
|
}
|
|
3755
4383
|
}
|
|
3756
4384
|
}
|
|
3757
|
-
/**
|
|
3758
|
-
* Internal get implementation
|
|
3759
|
-
* @param {string} key
|
|
3760
|
-
* @returns {*|null}
|
|
3761
|
-
* @private
|
|
3762
|
-
*/
|
|
3763
|
-
static _get_item(key) {
|
|
3764
|
-
const scoped_key = this._build_key(key);
|
|
3765
|
-
try {
|
|
3766
|
-
const serialized = localStorage.getItem(scoped_key);
|
|
3767
|
-
if (serialized === null) {
|
|
3768
|
-
return null;
|
|
3769
|
-
}
|
|
3770
|
-
return JSON.parse(serialized);
|
|
3771
|
-
}
|
|
3772
|
-
catch (e) {
|
|
3773
|
-
console.error('[JQHTML Local Storage] Failed to get item:', e);
|
|
3774
|
-
return null;
|
|
3775
|
-
}
|
|
3776
|
-
}
|
|
3777
4385
|
/**
|
|
3778
4386
|
* Internal remove implementation
|
|
3779
4387
|
* @param {string} key
|
|
@@ -3790,12 +4398,14 @@ class Jqhtml_Local_Storage {
|
|
|
3790
4398
|
}
|
|
3791
4399
|
}
|
|
3792
4400
|
Jqhtml_Local_Storage._cache_key = null;
|
|
4401
|
+
Jqhtml_Local_Storage._cache_mode = 'data';
|
|
3793
4402
|
Jqhtml_Local_Storage._storage_available = null;
|
|
3794
4403
|
Jqhtml_Local_Storage._initialized = false;
|
|
3795
4404
|
|
|
3796
4405
|
var localStorage$1 = /*#__PURE__*/Object.freeze({
|
|
3797
4406
|
__proto__: null,
|
|
3798
|
-
Jqhtml_Local_Storage: Jqhtml_Local_Storage
|
|
4407
|
+
Jqhtml_Local_Storage: Jqhtml_Local_Storage,
|
|
4408
|
+
register_cache_class: register_cache_class
|
|
3799
4409
|
});
|
|
3800
4410
|
|
|
3801
4411
|
/**
|
|
@@ -4058,7 +4668,7 @@ function init(jQuery) {
|
|
|
4058
4668
|
}
|
|
4059
4669
|
}
|
|
4060
4670
|
// Version - will be replaced during build with actual version from package.json
|
|
4061
|
-
const version = '2.3.
|
|
4671
|
+
const version = '2.3.13';
|
|
4062
4672
|
// Default export with all functionality
|
|
4063
4673
|
const jqhtml = {
|
|
4064
4674
|
// Core
|
|
@@ -4142,9 +4752,17 @@ const jqhtml = {
|
|
|
4142
4752
|
return version;
|
|
4143
4753
|
},
|
|
4144
4754
|
// Cache key setter - enables component caching via local storage
|
|
4145
|
-
|
|
4146
|
-
|
|
4755
|
+
// cache_mode: 'data' (default, recommended) or 'html'
|
|
4756
|
+
set_cache_key(cache_key, cache_mode = 'data') {
|
|
4757
|
+
Jqhtml_Local_Storage.set_cache_key(cache_key, cache_mode);
|
|
4758
|
+
},
|
|
4759
|
+
// Get current cache mode
|
|
4760
|
+
get_cache_mode() {
|
|
4761
|
+
return Jqhtml_Local_Storage.get_cache_mode();
|
|
4147
4762
|
},
|
|
4763
|
+
// Register a class for cache serialization/deserialization
|
|
4764
|
+
// Required for ES6 class instances to be stored and restored from localStorage
|
|
4765
|
+
register_cache_class,
|
|
4148
4766
|
// Boot - hydrate server-rendered component placeholders
|
|
4149
4767
|
boot
|
|
4150
4768
|
};
|
|
@@ -4164,5 +4782,5 @@ if (typeof window !== 'undefined' && !window.jqhtml) {
|
|
|
4164
4782
|
}
|
|
4165
4783
|
}
|
|
4166
4784
|
|
|
4167
|
-
export { Jqhtml_Component, LifecycleManager as Jqhtml_LifecycleManager, Jqhtml_Local_Storage, LifecycleManager, Load_Coordinator, applyDebugDelay, boot, create_component, jqhtml as default, devWarn, escape_html, extract_slots, get_component_class, get_component_names, get_registered_templates, get_template, get_template_by_class, handleComponentError, has_component, init, init_jquery_plugin, isSequentialProcessing, list_components, logDataChange, logDispatch, logInstruction, logLifecycle, process_instructions, register, register_component, register_template, render_template, version };
|
|
4785
|
+
export { Jqhtml_Component, LifecycleManager as Jqhtml_LifecycleManager, Jqhtml_Local_Storage, LifecycleManager, Load_Coordinator, applyDebugDelay, boot, create_component, jqhtml as default, devWarn, escape_html, extract_slots, get_component_class, get_component_names, get_registered_templates, get_template, get_template_by_class, handleComponentError, has_component, init, init_jquery_plugin, isSequentialProcessing, list_components, logDataChange, logDispatch, logInstruction, logLifecycle, process_instructions, register, register_cache_class, register_component, register_template, render_template, version };
|
|
4168
4786
|
//# sourceMappingURL=jqhtml-core.esm.js.map
|