/**
* Jqhtml_Local_Storage - Scoped local storage for JQHTML component caching
*
* Provides safe, scoped access to localStorage with automatic handling of
* unavailable storage, quota exceeded errors, and scope invalidation.
*
* Key Features:
* - **Manual scoping**: Cache key must be set via set_cache_key() before use
* - **Graceful degradation**: Returns null when storage unavailable or cache key not set
* - **Quota management**: Auto-clears storage when full and retries operation
* - **Scope validation**: Clears storage when cache key changes
* - **Developer-friendly keys**: Scoped suffix allows easy inspection in dev tools
* - **Class-aware serialization**: ES6 class instances serialize and restore properly
*
* Class-Aware Serialization:
* ES6 class instances can be serialized and deserialized if registered via
* register_cache_class(). Classes are wrapped as {__jqhtml_class__: "Name", __jqhtml_props__: {...}}
* and restored to proper instances on retrieval. Nested class instances in objects
* and arrays are handled recursively. A class whose wire form is not its own
* properties (one that defines toJSON(), or keeps state in private fields)
* registers a {to_plain, from_plain} pair and is rebuilt through it. Date, Map and
* Set are pre-registered the same way - there is no second mechanism.
*
* ONE SERIALIZER. Every copy of on_load() data the framework makes goes through
* this file: the localStorage cache (set()/get()), the normalization of every
* on_load() result into this.data (normalize_for_cache()), and the copy a
* deduplicated follower takes of its leader's data (serialize()/deserialize(),
* used by load-coordinator.ts). Anything that survives one of those survives all
* of them, and anything dropped is dropped identically on all of them. A plain
* JSON.stringify anywhere on those paths would consult toJSON() and hand out a
* wire form nothing knows how to restore.
*
* Scoping Strategy:
* Storage is scoped by a user-provided cache key (typically a session identifier,
* user ID, or combination of relevant scope factors). This scope is stored in
* `_jqhtml_cache_key`. If the cache key changes between page loads, all JQHTML
* storage is cleared to prevent stale data from different sessions/users. The check
* runs once, in set_cache_key(), not on every write: entry keys already carry the
* jqhtml version and the cache key, so tabs with different scopes (or different
* jqhtml versions) can write side by side without reading each other's entries.
*
* Key Format:
* Keys are stored as: `jqhtml::::::`
* Example: `jqhtml::User_Profile::{"id":5}::2.3.75::user_123`
* The part after the developer key is exactly the scope marker stored in
* `_jqhtml_cache_key` (`::`), so the version appears once.
* Entries in the older `jqhtml::::` format are never read
* (their key never matches); the scope check or the quota cleanup removes them.
*
* The `jqhtml::` prefix identifies JQHTML keys, allowing safe clearing of only our keys
* without affecting other JavaScript libraries. This enables transparent coexistence
* with third-party libraries that also use browser storage.
*
* Quota Exceeded Handling:
* When storage quota is exceeded during a set operation, only JQHTML keys (prefixed with
* `jqhtml::`) are cleared, preserving other libraries' data. The operation is then retried
* once. This ensures the application continues functioning even when storage is full.
*
* Usage:
* // Register classes that need to be cached (call once at startup)
* jqhtml.register_cache_class(Contact_Model);
* jqhtml.register_cache_class(User_Profile);
*
* // Must set cache key first (typically done once on page load)
* Jqhtml_Local_Storage.set_cache_key('user_123');
*
* // Then use storage normally - ES6 classes serialize automatically
* Jqhtml_Local_Storage.set('cached_component', {
* contact: new Contact_Model(),
* timestamp: Date.now()
* });
* const cached = Jqhtml_Local_Storage.get('cached_component');
* // cached.contact instanceof Contact_Model === true
*
* IMPORTANT - Volatile Storage:
* Storage can be cleared at any time due to:
* - User clearing browser data
* - Private browsing mode restrictions
* - Quota exceeded errors
* - Cache key changes
* - Browser storage unavailable
*
* Therefore, NEVER store critical data. Only use for:
* - Cached component HTML (performance optimization)
* - Transient UI state (convenience, not required)
*
* @internal This class is not exposed in the public API
*
* ---------------------------------------------------------------------------
* REPORTING WHAT WAS DROPPED
*
* `this.data` is always the serialized copy this file produces - in EVERY cache
* mode, including 'none' (see component.ts `_apply_load_result`). Whatever the
* serializer cannot express is therefore not merely missing from the cache, it
* is missing from `this.data`, and an author needs to be told once rather than
* discover it on the next cache hit.
*
* Two small optional parameters carry that:
*
* process_for_serialization(value, verbose, seen, path, collector?)
* `collector(path, kind)` is invoked for every value that is dropped or
* degraded. `path` is a dotted/bracketed path from the root the caller
* named; `kind` is one of Strip_Kind. Paths are only BUILT when a
* collector is present, so the no-collector path costs nothing.
*
* Jqhtml_Local_Storage.normalize_for_cache(value, component_name?)
* With a component name, and only in development mode, each dropped path
* prints exactly ONE console.warn for the life of the page (keyed
* `${component_name}:${path}`, the same once-only shape as
* warn_uncacheable_args in component-cache.ts). Production converts
* silently. Neither ever throws.
* ---------------------------------------------------------------------------
*/
/**
* How a registered class travels through the serializer.
*
* With no options, an instance's own enumerable properties go out and come back
* through Object.create(prototype) + Object.assign - right for a class whose
* state IS its own properties.
*
* A class whose wire form is something else names how to rebuild itself:
*
* to_plain(instance) -> the plain value to store. Default when from_plain is
* given: instance.toJSON() if the class defines one,
* otherwise its own enumerable properties. The result is
* walked by the serializer like any other value, so
* instances nested inside it are handled too.
* from_plain(plain) -> a live instance. Required whenever to_plain is given,
* because the serializer cannot guess how to undo a
* custom wire form.
*/
export interface Cache_Class_Options {
to_plain?: (instance: T) => any;
from_plain?: (plain: any) => T;
}
/**
* Register a class for cache serialization/deserialization.
* Must be called before attempting to cache instances of this class.
*
* @param klass - The class constructor to register
* @param options - How to convert instances to and from their plain form; see
* Cache_Class_Options. Omit for a class whose state is its own
* enumerable properties.
* @throws Error if klass is not a named function/class, or if to_plain is given
* without the from_plain that undoes it
*/
export declare function register_cache_class(klass: new (...args: any[]) => any, options?: Cache_Class_Options): void;
/**
* True when `value` is an instance of a class the cache restores as itself: Date, Map,
* Set or a class passed to register_cache_class().
*/
export declare function is_registered_cache_class_instance(value: any): boolean;
/**
* Check if a class is registered for caching
*/
export declare function is_cache_class_registered(class_name: string): boolean;
/**
* What kind of value the serializer had to drop or degrade. Reported to a
* collector; each kind maps to one piece of advice in strip_advice().
*/
export type Strip_Kind = 'function' | 'promise' | 'dom-node' | 'jquery' | 'component' | 'symbol' | 'bigint' | 'circular' | 'unregistered-class' | 'to-plain-threw';
/** Called once per dropped/degraded value with its path and what it was. */
export type Strip_Collector = (path: string, kind: Strip_Kind, detail?: string) => void;
/**
* Cache mode determines how component data is cached and restored.
*
* - 'data': (Recommended) Caches this.data with class-aware serialization.
* Requires ES6 classes to be registered via register_cache_class().
* On cache hit, this.data is hydrated before first render.
*
* - 'html': Caches rendered DOM HTML after children are ready.
* Does not require class registration. On cache hit, HTML is injected
* directly. Note: this.data is NOT populated during on_render() with
* cached HTML - use on_ready() for any DOM manipulation that depends on data.
*/
export type CacheMode = 'data' | 'html';
export declare class Jqhtml_Local_Storage {
private static _cache_key;
/** Versioned scope identity `::` - compared in _validate_scope() to clear on a core upgrade, and the suffix of every entry key (_build_key()). */
private static _scope_marker;
private static _cache_mode;
private static _storage_available;
private static _initialized;
/**
* Set the cache key and initialize storage
* Must be called before any get/set operations
* @param {string} cache_key - Unique identifier for this cache scope
* @param {CacheMode} cache_mode - Cache strategy: 'data' (default, recommended) or 'html'
*/
static set_cache_key(cache_key: string, cache_mode?: CacheMode): void;
/**
* Check if cache key has been set
* @returns {boolean} True if cache key is configured
*/
static has_cache_key(): boolean;
/**
* Get the current cache mode
* @returns {CacheMode} Current cache mode ('data' or 'html')
*/
static get_cache_mode(): CacheMode;
/**
* Initialize storage system and validate scope
* Called automatically after cache key is set
* @private
*/
private static _init;
/**
* Check if localStorage is available
* @returns {boolean}
* @private
*/
private static _is_storage_available;
/**
* Check if verbose mode is enabled
* @private
*/
private static _is_verbose;
/**
* Validate storage scope and clear JQHTML keys if cache key changed
* Only clears keys prefixed with 'jqhtml::' to preserve other libraries' data
*
* Runs once per set_cache_key(), never per write. The clear here is what bounds
* storage: entries of scopes no longer in use are removed the next time any tab sets
* a different scope. A tab still using such a scope simply re-caches.
* @private
*/
private static _validate_scope;
/**
* Clear only JQHTML keys from storage (keys starting with 'jqhtml::')
* Preserves keys from other libraries
* @private
*/
private static _clear_jqhtml_keys;
/**
* Build scoped key with JQHTML namespace prefix:
* `jqhtml::::::`.
* The suffix is the scope marker, so the version is in the key exactly once.
* @param {string} key - Developer-provided key
* @returns {string}
* @private
*/
private static _build_key;
/**
* Check if storage is ready for use
* @returns {boolean}
* @private
*/
private static _is_ready;
/**
* Set item in localStorage with class-aware serialization.
*
* If serialization fails (e.g., unregistered class instances, circular refs),
* the existing cache entry is removed and nothing is stored.
*
* @param {string} key - Storage key
* @param {*} value - Value to store (primitives, objects, arrays, or registered class instances)
*/
static set(key: string, value: any): void;
/**
* Store text that serialize() (or normalize()) already produced - the write
* set() performs, without serializing a second time. A component's freshly
* loaded data is written this way: the text it was normalized through IS the
* cache entry.
*
* @param {string} key - Storage key
* @param {string|null} serialized - serialize() output; null removes the entry
*/
static set_serialized(key: string, serialized: string | null): void;
/**
* Get item from localStorage with class-aware deserialization.
*
* If deserialization fails, returns null (as if no cache exists).
*
* @param {string} key - Storage key
* @returns {*|null} Deserialized value with class instances restored, or null if not found/failed
*/
static get(key: string): any | null;
/**
* Remove item from localStorage
* @param {string} key - Storage key
*/
static remove(key: string): void;
/**
* Serialize a value to its class-aware TEXT form - the same text set() stores.
*
* This is the copy-by-value primitive the rest of the framework uses whenever
* data has to cross from one component to another (load-coordinator.ts hands a
* leader's data to its followers through it). Plain JSON.stringify is not a
* substitute: it consults toJSON() and produces a wire form that deserialize()
* cannot recognise, so a registered class would come back as a plain object.
*
* @returns The text, or null when the value as a whole cannot be expressed.
*/
static serialize(value: any): string | null;
/**
* Rebuild a value from text produced by serialize(), restoring Date, Map, Set
* and every register_cache_class() class. Returns null when the text is not
* restorable (a registered from_plain threw at the root) - the same signal
* get() gives for a corrupt entry.
*/
static deserialize(text: string): any;
/**
* Perform a serialize/deserialize round-trip on a value.
*
* The result is ALWAYS jqhtml's own copy: nothing of the caller's object graph
* is aliased through, and a value the serializer cannot express is dropped
* rather than smuggled out. That is what makes `this.data` identical whether it
* came from on_load() or from a cache hit - the old "on failure return the
* original" branch broke exactly that promise, and only on the hard cases.
*
* @param {any} value - The value to normalize
* @param {string} [component_name] - Names the component in development warnings.
* Omit to convert silently (the unit tests and non-component callers).
* @returns {any} The round-tripped value. Never the original object.
*/
static normalize_for_cache(value: any, component_name?: string): any;
/**
* normalize_for_cache(), also returning the serialized text the value went
* through. The text IS the value's serialized form - what serialize() of the
* result produces and what the cache stores - so a component keeps it as the
* snapshot of its new this.data (change detection, the cache write, the
* coordinator hand-off) instead of serializing the same data again.
*
* @returns {{value: any, text: string}} The round-tripped value and its text.
*/
static normalize(value: any, component_name?: string): {
value: any;
text: string;
};
/**
* Serialize the on_create() state of this.data for restore_snapshot().
*
* The cache serializer, so Date, Map, Set and register_cache_class() instances
* come back as themselves, plus one thing the cache never stores: a key holding
* `undefined` keeps its key. Functions, DOM nodes and the like are dropped, as
* the JSON copy this replaces dropped them.
*
* @param component_name - Names the component in the development warning for each
* dropped value, as normalize() does. Omit to drop silently.
* @returns The snapshot text, or null when the value cannot be expressed at all.
*/
static snapshot(value: any, component_name?: string): string | null;
/**
* Rebuild a fresh copy of the value snapshot() serialized. Every call returns a
* new object graph, so each on_load() starts from its own copy.
*/
static restore_snapshot(text: string): any;
/**
* Internal set implementation with quota handling.
*
* Does NOT validate the scope: that happens once, in set_cache_key(). Entry keys
* already carry the jqhtml version and the cache key, so tabs with different scopes write disjoint keys and
* coexist. Re-checking the shared `_jqhtml_cache_key` marker here made two such tabs
* clear every jqhtml key on each other's every write.
* @param {string} key
* @param {string} serialized - Pre-serialized JSON string
* @private
*/
private static _set_item;
/**
* Internal remove implementation
* @param {string} key
* @private
*/
private static _remove_item;
}
//# sourceMappingURL=local-storage.d.ts.map