Member Junction
    Preparing search index...

    LocalCacheManager is a singleton that provides a unified caching abstraction for datasets, RunView results, and RunQuery results. It wraps ILocalStorageProvider for actual storage and maintains an internal registry of all cached items.

    Key features:

    • Typed methods for datasets, RunViews, and RunQueries
    • Automatic cache metadata tracking (timestamps, access counts, sizes)
    • Hit/miss statistics for performance monitoring
    • Eviction policies (LRU, LFU, FIFO) for memory management
    • Dashboard-friendly registry queries

    Usage:

    // Initialize during app startup
    await LocalCacheManager.Instance.Initialize(storageProvider);

    // Cache a dataset
    await LocalCacheManager.Instance.SetDataset('MyDataset', filters, dataset, keyPrefix);

    // Retrieve cached data
    const cached = await LocalCacheManager.Instance.GetDataset('MyDataset', filters, keyPrefix);

    Hierarchy (View Summary)

    Index

    Constructors

    Accessors

    • get GlobalKey(): string

      Returns string

    Methods

    • Applies a differential update to a cached RunView result. Merges updated/created rows and removes deleted records from the existing cache.

      This is the core method for differential caching - instead of replacing the entire cache, we efficiently merge only the changes (deltas) with the existing cached data.

      Note: rowCount is always derived from the merged results length, not from a parameter. Note: Aggregates cannot be differentially updated - if provided, they replace the cached aggregates; if not provided, cached aggregates are cleared (they would be stale after a differential update).

      Parameters

      • fingerprint: string

        The cache fingerprint to update

      • params: RunViewParams

        The original RunView parameters (for re-storing the cache)

      • updatedRows: unknown[]

        Rows that have been created or updated since the cache was stored

      • deletedRecordIDs: string[]

        Record IDs (in CompositeKey concatenated string format) that have been deleted

      • primaryKeyFieldName: string

        The name of the primary key field (or first PK field for composite keys)

      • newMaxUpdatedAt: string

        The new maxUpdatedAt timestamp after applying the delta

      • OptionalserverRowCount: number

        The database's authoritative total row count (fresh COUNT(*) over the view) from the smart-cache check. Used as the merged entry's totalRowCount when it exceeds the cached slice size — this keeps paginated / MaxRows-limited slots from undercounting the true total. The visible rowCount is still derived from the merged results length.

      • OptionalaggregateResults: AggregateResult[]

        Optional fresh aggregate results (since aggregates can't be differentially computed)

      • Optionalprovider: IMetadataProvider

        The IMetadataProvider that produced these results (for AllowCaching gating in multi-provider scenarios). Falls back to global Metadata.Provider when omitted.

      Returns Promise<CachedRunViewResult>

      The merged results after applying the differential update, or null if cache not found

    • Computes a hash of an entity's field names in sequence order. Used to detect schema changes (new/removed/reordered columns) that would make cached RunView data structurally stale.

      Parameters

      • provider: IMetadataProvider

        The metadata provider to resolve the entity

      • entityName: string

        The entity name to compute the hash for

      Returns string

      The schema hash string, or undefined if the entity can't be resolved

    • Dispatches a cache change event to all registered callbacks for the affected fingerprint. Called by infrastructure code (e.g., RedisLocalStorageProvider) when another server modifies a cached entry.

      For category_cleared events, dispatches to ALL registered callbacks whose fingerprints belong to the cleared category (matched by the event's CacheKey which contains the category name).

      Errors in individual callbacks are caught and logged via LogError to prevent one bad callback from blocking others.

      Parameters

      Returns void

    • Extracts the entity name from a RunView fingerprint. Fingerprint format: Entity|Filter|OrderBy|MaxRows|StartRow|AggHash|UserSearch[|…] (built in GenerateRunViewFingerprint below — that array is the ground truth). NOTE: ResultType is deliberately NOT a segment; the cache stores plain JSON regardless and transformation happens post-cache. An earlier version of this comment listed it, which would put any new segment-indexing predicate one position off — MaxRows is [3], not [4].

      Parameters

      • fingerprint: string

        The RunView cache fingerprint

      Returns string

      The entity name, or null if the fingerprint is malformed

    • Generates a human-readable cache fingerprint for a RunQuery request.

      Format: QueryName|QueryID|params|connection Example: GetActiveUsers|abc123|{"status":"active"}|localhost

      Parameters

      • OptionalqueryId: string

        The query ID

      • OptionalqueryName: string

        The query name

      • Optionalparameters: Record<string, unknown>

        Optional query parameters

      • OptionalconnectionPrefix: string

        Prefix identifying the connection (e.g., server URL) to differentiate caches across connections

      • OptionalcategoryPath: string

      Returns string

      A unique, human-readable fingerprint string

    • Generates a human-readable cache fingerprint for a RunView request. This fingerprint uniquely identifies the query based on its parameters and connection.

      Format: Entity|Filter|OrderBy|MaxRows|StartRow|AggHash|UserSearch[|appended…][|connection] (the parts array below is the ground truth). NOTE: resultType is NOT a segment — an older version of this comment listed it, which put every index after [2] off by one; that exact off-by-one trap has already bitten a segment-indexing predicate once (see the note on extractEntityFromFingerprint). Example: Users|Active=1|Name ASC|simple|100|0|a1b2c3d4|localhost

      Parameters

      • params: RunViewParams

        The RunView parameters

      • OptionalconnectionPrefix: string

        Prefix identifying the connection (e.g., server URL) to differentiate caches across connections

      • OptionalrlsWhereClause: string

        The per-user Row-Level-Security WHERE clause that the provider will append to this query for the current user. This MUST participate in the fingerprint: an RLS-scoped read produces a different (smaller) result set than an unscoped read of the same entity+filter, so they must never share a cache entry. When empty/undefined (the common case — users with no RLS filter), the fingerprint is byte-for-byte identical to the pre-RLS format so normal cache sharing is preserved and no existing entries are invalidated.

      Returns string

      A unique, human-readable fingerprint string

    • Returns the set of cached fingerprints for a given entity name. Useful for diagnostics and testing.

      Parameters

      • entityName: string

      Returns ReadonlySet<string>

    • The Global Object Store is a place to store global objects that need to be shared across the application. Depending on the execution environment, this could be the window object in a browser, or the global object in a node environment, or something else in other contexts. The key here is that in some cases static variables are not truly shared because it is possible that a given class might have copies of its code in multiple paths in a deployed application. This approach ensures that no matter how many code copies might exist, there is only one instance of the object in question by using the Global Object Store.

      Returns GlobalObjectStore

    • Gets the cache status (fingerprint data) for a RunQuery result. Used for smart cache validation with the server.

      Parameters

      • fingerprint: string

        The cache fingerprint

      Returns Promise<{ maxUpdatedAt: string; rowCount: number }>

      The cache status with maxUpdatedAt and rowCount, or null if not found/expired

    • Retrieves a cached RunQuery result.

      Parameters

      • fingerprint: string

        The cache fingerprint

      Returns Promise<
          {
              maxUpdatedAt: string;
              queryId?: string;
              results: unknown[];
              rowCount: number;
              warmedForUserID?: string;
          },
      >

      The cached results, maxUpdatedAt, rowCount, and queryId, or null if not found

    • Batched retrieval for many cached RunView results in a single underlying IndexedDB transaction (or Redis MGET, or one in-memory pass — depends on provider). N keys, one call.

      Returns a Map keyed by fingerprint. Missing entries map to null. The map preserves the order of the input array's first occurrence of each key.

      Why this exists: the smart-cache-check flow reads N cached entries in two passes — once to build the per-fingerprint cacheStatus payload, then again after the server response to materialize "current" entries. Per-key GetItem calls serialize across IDB transactions; one batched read trades ~N transactions of overhead for a single transaction's commit cost.

      Hits/misses are accounted per fingerprint just like GetRunViewResult.

      Parameters

      • fingerprints: string[]

        Cache fingerprints to look up. Duplicates are deduplicated; the returned map has one entry per unique key.

      Returns Promise<Map<string, CachedRunViewResult>>

      Map from fingerprint to CachedRunViewResult (or null if not cached). Always returns a map (possibly empty); never throws.

    • Handles a BaseEntity event by updating all cached RunView results for the affected entity. For unfiltered caches, updates the record in-place. For filtered caches, invalidates the cache entry (conservative approach since we can't verify filter match without re-querying).

      Parameters

      Returns Promise<void>

    • Handles remote-invalidate events that include recordData (the saved entity as JSON). Updates all cached RunView results for the entity without a server round-trip. For delete events or events without recordData, the cache entries are invalidated so the next RunView call will fetch fresh data from the server.

      Parameters

      Returns Promise<void>

    • Returns true if the slot carries aggregate results (fingerprint segment [5], aggHash).

      The aggregate was computed by the DATABASE over the pre-mutation row set. After an in-place upsert/remove there is no way to recompute it in JS for the general case: COUNT(*) shifts by one, SUM/AVG need the mutated row's contribution to the specific expression, and MAX/MIN may or may not move depending on the value.

      The first attempt at this fix CARRIED the cached aggregate forward — which was worse than the bug it replaced. Verified live: after a save the slot reported rows=7 alongside COUNT(*) = 6. A caller can detect a MISSING aggregate; it cannot detect a stale one, and the read path reports Success: true / cacheStatus: 'hit' either way. Silently wrong beats loudly absent only if you never look.

      So: same treatment as subset slots. The value is not derivable in JS, therefore the slot is dropped and the next read recomputes it against the database.

      Parameters

      • parts: string[]

        the fingerprint already split on '|'

      Returns boolean

    • Returns true if the fingerprint carries any segment BEYOND the 7-part base that narrows the result set — i.e. the slot holds fewer rows than an unfiltered read of the same entity would.

      The base fingerprint is Entity|Filter|OrderBy|MaxRows|StartRow|AggHash|UserSearch, and the original filtered-check inspected ONLY parts[1]. But two later segments narrow the rows WITHOUT touching that segment:

      • vw:<id> — a saved view's WhereClause lives ON THE VIEW, not in params.ExtraFilter, so the filter segment stays _. The slot was therefore classified unfiltered and UPSERTED IN PLACE on save — serving rows the view's own WhereClause excludes. Views are how users are shown a restricted row set, so this reads as a data/permission leak, not merely stale data.
      • rls:<h> — the per-user Row-Level-Security predicate is appended AFTER the filter segment is built. Same misclassification, worse consequence: a save by user A was upserted into user B's RLS-scoped slot, injecting a row B's predicate excludes. That is an RLS bypass.

      Enumerating the narrowing segments would repeat the original mistake: the next segment someone appends is silently treated as maintainable until it causes a leak. So this enumerates only what is provably SAFE and treats everything else as narrowing:

      • imr:1 — IgnoreMaxRows WIDENS the set (it removes a cap), so in-place maintenance remains valid.
      • connection — the <driver>://host:port/ suffix is slot IDENTITY, not a predicate.

      Anything else — present or future — falls through to "narrowing", and the slot is conservatively invalidated on mutation rather than maintained. A new segment can therefore cost a cache refill, but it can never silently serve the wrong rows.

      Parameters

      • parts: string[]

        the fingerprint already split on '|'

      Returns boolean

    • Returns true if the slot was cached under an ORDER BY (fingerprint segment [2]).

      An ordered slot's row SET can be maintained in place, but its ORDER cannot: an upsert appends the new row at map-insertion end and leaves re-sorted rows at their old positions, so the slot silently stops honoring the order the caller asked for — wrong for any "first row of the ordered set" consumer. Re-sorting in JS would require reimplementing SQL ORDER BY semantics (collations, NULL ordering, expression sorts), which is exactly the kind of "derive it in JS" shortcut this file keeps having to walk back.

      DELETE remains maintainable: removing a row preserves the relative order of the rest. This mirrors the filtered-slot asymmetry, and the branch order in processEntityEventForFingerprint (delete is checked before the filtered classification) delivers it without extra wiring. BaseEngine already refuses ordered configs for in-place mutation (canUseImmediateMutation); this closes the same gap in the raw provider cache. (B42)

      Parameters

      • parts: string[]

        the fingerprint already split on '|'

      Returns boolean

    • Returns true if the slot was produced by a user search (fingerprint segment [6]).

      UserSearchString generates LIKE / full-text WHERE clauses, so it narrows rows exactly as ExtraFilter does — but it lives at index [6], INSIDE the 7-segment base, where neither the parts[1] filter check nor hasNarrowingSegment (which starts at index 7) was looking.

      Same bug class as H1/H3, different hiding place: a row-narrowing predicate invisible to the maintainability check. Demonstrated by upserting a non-matching row into a search slot — a search for "annual gala" subsequently served "Totally Unrelated Row". Explorer grid searches are the reachable surface. (N1)

      Parameters

      • parts: string[]

        the fingerprint already split on '|'

      Returns boolean

    • Initialize the cache manager with a storage provider. This should be called during app startup after the storage provider is available.

      This method is safe to call multiple times - subsequent calls will return the same promise as the first caller, ensuring initialization only happens once.

      Parameters

      Returns Promise<void>

      A promise that resolves when initialization is complete

    • Invalidates all cached RunView results for a specific entity. Useful when an entity's data changes and all related caches should be cleared.

      Parameters

      • entityName: string

        The entity name to invalidate

      Returns Promise<void>

    • Invalidates all cached RunQuery results for a specific query. Useful when a query's underlying data changes and all related caches should be cleared.

      Parameters

      • queryName: string

        The query name to invalidate

      Returns Promise<void>

    • Checks whether caching is enabled for a given entity. Returns the entity's AllowCaching metadata flag. This is the single source of truth for cache eligibility — schema-level opt-in is applied at CodeGen time via the newEntityDefaults.AllowCachingBySchema config, which flips this flag when the entity is first inserted into the metadata.

      Parameters

      • entityInfo: { AllowCaching: boolean }

      Returns boolean

    • Returns true if the fingerprint includes a non-trivial filter (not just '_' or empty). Unfiltered fingerprints can safely have records upserted in-place; filtered ones must be invalidated conservatively since the new data may not match the filter.

      Parameters

      • fingerprint: string

        The RunView cache fingerprint

      Returns boolean

    • Returns true if the fingerprint identifies a subset slot — a cache entry whose rows are a TRUNCATION (MaxRows) or an OFFSET WINDOW (StartRow) of the matching set rather than the complete set.

      Subset slots are safe to STORE and SERVE (a cold read of the slot is exactly what the DB would have returned), but they must NEVER be maintained in place by the BaseEntity save/delete event path:

      • Save/upsert appends the saved row to the slot, so a MaxRows: 1 slot grows to 2, 3, 4 … rows — silently violating the caller's own row limit and serving a set that is neither the first-N nor the full set (one arbitrary original row plus every locally saved row).
      • Delete/remove shrinks the slot below the limit, so a MaxRows: 1 slot serves 0 rows while the DB still has 47 matching rows to choose a TOP 1 from.

      Neither can be repaired in JS: deciding whether a newly saved row belongs inside the window, and which row it would displace, requires re-running the query's TOP/OFFSET against the database. So we treat subset slots exactly as filtered slots are treated on save — conservatively INVALIDATE and let the next read repopulate from the DB.

      This is the row-level counterpart to the totalRowCount subset-slot handling: the total is maintained across the delta because the DB total is knowable; the ROWS are not, so the slot is dropped instead.

      Fingerprint format: Entity|Filter|OrderBy|MaxRows|StartRow|AggHash|UserSearch[|…]. Parsing is deliberately conservative — if the segments aren't cleanly numeric (e.g. a filter value containing a literal | shifts the positions), we return false and preserve existing behavior rather than over-invalidating. Such a fingerprint is filtered by definition, and filtered slots are already invalidated on save.

      Parameters

      • fingerprint: string

        The RunView cache fingerprint

      Returns boolean

    • Registers a callback that fires when a specific cache fingerprint is updated by another server instance. Returns an unsubscribe function to remove the callback.

      This is the mechanism that powers the OnDataChanged callback in RunViewParams. Engines, components, and other callers can use this to react to cross-server cache invalidation without polling.

      Parameters

      Returns () => void

      A function that, when called, removes this specific callback registration.

      const fingerprint = cache.GenerateRunViewFingerprint(params, connectionPrefix);
      const unsubscribe = cache.RegisterChangeCallback(fingerprint, (event) => {
      console.log(`Data changed for ${event.CacheKey}`);
      // Reload, re-render, etc.
      });

      // Later, on cleanup:
      unsubscribe();
    • Removes a single entity from a cached RunView result. Supports composite primary keys via CompositeKey matching.

      Parameters

      • fingerprint: string

        The cache fingerprint to update

      • key: CompositeKey

        CompositeKey identifying the entity to remove

      • newMaxUpdatedAt: string

        New maxUpdatedAt timestamp

      Returns Promise<boolean>

      true if cache was updated, false if cache not found or update failed

    • Reorders cached AggregateResults to match the CALLER's requested Aggregates[] order.

      The aggregate fingerprint (see generateAggregateHash) is deliberately order-insensitive — it sorts the aggregates — so two semantically-identical views requested as [A,B] and [B,A] share a single cache slot (cache-efficient). But the RunViewResult.AggregateResults contract is "in same order as input Aggregates array" — PER caller. A slot warmed as [A,B] therefore hands a [B,A] caller its results in the wrong order unless we remap on the way out. This does that remap.

      Matching is by (expression, effective alias) — the same identity that produced the aggHash (a result's alias defaults to its expression when the request omitted one). Fail-safe: returns the input unchanged when there are no aggregates to reorder, the counts differ, or any aggregate can't be matched — so a remap is never able to drop or fabricate a result.

      Parameters

      Returns AggregateResult[]

    • Stores a RunQuery result in the cache.

      Parameters

      • fingerprint: string

        The cache fingerprint

      • queryName: string

        The query name for display

      • results: unknown[]

        The results to cache

      • maxUpdatedAt: string

        The latest update timestamp (for smart cache validation)

      • OptionalrowCount: number

        Optional row count (defaults to results.length if not provided)

      • OptionalqueryId: string

        Optional query ID for reference

      • OptionalttlMs: number

        Optional TTL in milliseconds (for cache expiry tracking)

      • OptionalwarmedForUserID: string

      Returns Promise<void>

    • Stores a RunView result in the cache.

      Note: rowCount is NOT persisted - it is always derived from results.length when reading to prevent data inconsistency.

      Parameters

      • fingerprint: string

        The cache fingerprint (from GenerateRunViewFingerprint)

      • params: RunViewParams

        The original RunView parameters

      • results: unknown[]

        The results to cache

      • maxUpdatedAt: string

        The latest __mj_UpdatedAt from the results

      • OptionalaggregateResults: AggregateResult[]

        Optional aggregate results to cache alongside the row data

      • OptionaltotalRowCount: number

        Optional total row count when paging

      • Optionalprovider: IMetadataProvider

        The IMetadataProvider that produced these results. Required for correct AllowCaching gating in multi-provider scenarios (parallel client connections to multiple servers). Falls back to Metadata.Provider (global default) when omitted, which is fine for single-provider apps but wrong when AllowCaching differs across servers.

      • OptionalttlMs: number

      Returns Promise<void>

    • Replaces the storage provider after initialization. This is needed when the initial provider (e.g., in-memory) needs to be swapped for a persistent provider (e.g., Redis) that becomes available later.

      Migrates the in-memory registry to the new provider and rebuilds the entity→fingerprint reverse index.

      Parameters

      Returns Promise<void>

    • Parameters

      • fingerprint: string

        The cache fingerprint to update

      • entityData: Record<string, unknown>

        The entity data as a plain object (use entity.GetAll())

      • key: CompositeKey
      • newMaxUpdatedAt: string

        New maxUpdatedAt timestamp (from entity's __mj_UpdatedAt)

      Returns Promise<boolean>

      true if cache was updated, false if cache not found or update failed

    • Returns the singleton instance of the class. If the instance does not exist, it is created and stored in the Global Object Store. If className is provided it will be used as part of the key in the Global Object Store, otherwise the actual class name will be used. NOTE: the class name used by default is the lowest level of the object hierarchy, so if you have a class that extends another class, the lowest level class name will be used.

      Type Parameters

      Parameters

      • this: new () => T
      • OptionalclassName: string

      Returns T