/** * 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