ProtectedconstructorReturns the number of fingerprints that have registered change callbacks. Useful for diagnostics and testing.
Returns the current configuration
Returns whether the cache manager has been initialized
StaticInstanceReturns the singleton instance of LocalCacheManager
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).
The cache fingerprint to update
The original RunView parameters (for re-storing the cache)
Rows that have been created or updated since the cache was stored
Record IDs (in CompositeKey concatenated string format) that have been deleted
The name of the primary key field (or first PK field for composite keys)
The new maxUpdatedAt timestamp after applying the delta
OptionalserverRowCount: numberThe 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: IMetadataProviderThe IMetadataProvider that produced these results (for AllowCaching gating in multi-provider scenarios). Falls back to global Metadata.Provider when omitted.
The merged results after applying the differential update, or null if cache not found
Clears all cache entries.
The number of entries cleared
Clears all cache entries of a specific type.
The cache entry type to clear
The number of entries cleared
Clears a cached dataset.
The dataset name
Optional filters applied to the dataset
Prefix for the cache key
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.
The metadata provider to resolve the entity
The entity name to compute the hash for
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.
The cache change event to dispatch
ProtectedextractExtracts 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].
The RunView cache fingerprint
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
OptionalqueryId: stringThe query ID
OptionalqueryName: stringThe query name
Optionalparameters: Record<string, unknown>Optional query parameters
OptionalconnectionPrefix: stringPrefix identifying the connection (e.g., server URL) to differentiate caches across connections
OptionalcategoryPath: stringA 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
The RunView parameters
OptionalconnectionPrefix: stringPrefix identifying the connection (e.g., server URL) to differentiate caches across connections
OptionalrlsWhereClause: stringThe 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.
A unique, human-readable fingerprint string
Returns all cache entries for dashboard display.
Retrieves a cached dataset.
The dataset name
Optional filters applied to the dataset
Prefix for the cache key
The cached dataset or null if not found
Gets the timestamp of a cached dataset.
The dataset name
Optional filters applied to the dataset
Prefix for the cache key
The cache timestamp or null if not found
Returns cache entries filtered by type.
The cache entry type to filter by
Returns the set of cached fingerprints for a given entity name. Useful for diagnostics and testing.
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.
Calculates the cache hit rate as a percentage.
Gets the cache status (fingerprint data) for a RunQuery result. Used for smart cache validation with the server.
The cache fingerprint
The cache status with maxUpdatedAt and rowCount, or null if not found/expired
Retrieves a cached RunQuery result.
The cache fingerprint
The cached results, maxUpdatedAt, rowCount, and queryId, or null if not found
Retrieves a cached RunView result.
Note: rowCount is always derived from results.length, never from persisted data.
The cache fingerprint
The cached results, maxUpdatedAt, rowCount (derived), and aggregateResults, 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.
Cache fingerprints to look up. Duplicates are deduplicated; the returned map has one entry per unique key.
Map from fingerprint to CachedRunViewResult (or null if not cached).
Always returns a map (possibly empty); never throws.
Returns comprehensive cache statistics.
ProtectedHandleHandles 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).
The BaseEntity event payload
ProtectedHandleHandles 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.
ProtectedhasReturns 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.
the fingerprint already split on '|'
ProtectedhasReturns 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.<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.
the fingerprint already split on '|'
ProtectedhasReturns 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)
the fingerprint already split on '|'
ProtectedhasReturns 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)
the fingerprint already split on '|'
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.
The local storage provider to use for persistence
Optionalconfig: Partial<LocalCacheManagerConfig>Optional configuration overrides
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.
The entity name to invalidate
Invalidates all cached RunQuery results for a specific query. Useful when a query's underlying data changes and all related caches should be cleared.
The query name to invalidate
Invalidates a cached RunQuery result.
The cache fingerprint to invalidate
Invalidates a cached RunView result.
The cache fingerprint to invalidate
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.
Checks if a dataset is cached.
The dataset name
Optional filters applied to the dataset
Prefix for the cache key
True if the dataset is cached
ProtectedisReturns 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.
The RunView cache fingerprint
ProtectedisReturns 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:
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).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.
The RunView cache fingerprint
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.
The cache key/fingerprint to watch. For RunView results, use GenerateRunViewFingerprint to build this.
Function invoked with the CacheChangedEvent when the fingerprint's cached data changes on another server.
A function that, when called, removes this specific callback registration.
Removes a single entity from a cached RunView result. Supports composite primary keys via CompositeKey matching.
The cache fingerprint to update
CompositeKey identifying the entity to remove
New maxUpdatedAt timestamp
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.
Resets the hit/miss statistics.
Stores a dataset in the local cache.
The dataset name
Optional filters applied to the dataset
The dataset result to cache
Prefix for the cache key (typically includes connection info)
Stores a RunQuery result in the cache.
The cache fingerprint
The query name for display
The results to cache
The latest update timestamp (for smart cache validation)
OptionalrowCount: numberOptional row count (defaults to results.length if not provided)
OptionalqueryId: stringOptional query ID for reference
OptionalttlMs: numberOptional TTL in milliseconds (for cache expiry tracking)
OptionalwarmedForUserID: stringStores a RunView result in the cache.
Note: rowCount is NOT persisted - it is always derived from results.length when reading to prevent data inconsistency.
The cache fingerprint (from GenerateRunViewFingerprint)
The original RunView parameters
The results to cache
The latest __mj_UpdatedAt from the results
OptionalaggregateResults: AggregateResult[]Optional aggregate results to cache alongside the row data
OptionaltotalRowCount: numberOptional total row count when paging
Optionalprovider: IMetadataProviderThe 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: numberReplaces 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.
The new storage provider to use
The cache fingerprint to update
The entity data as a plain object (use entity.GetAll())
New maxUpdatedAt timestamp (from entity's __mj_UpdatedAt)
true if cache was updated, false if cache not found or update failed
Protected StaticgetReturns 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.
OptionalclassName: string
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:
Usage: