@memberjunction/core 6.2.0-edge.3 → 6.2.0-edge.5

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.
Files changed (99) hide show
  1. package/dist/generic/BaseEntitySaveQueue.d.ts +7 -0
  2. package/dist/generic/BaseEntitySaveQueue.d.ts.map +1 -1
  3. package/dist/generic/BaseEntitySaveQueue.js +22 -4
  4. package/dist/generic/BaseEntitySaveQueue.js.map +1 -1
  5. package/dist/generic/JSONType-interfaces/IEntityConfiguration.d.ts +33 -0
  6. package/dist/generic/JSONType-interfaces/IEntityConfiguration.d.ts.map +1 -1
  7. package/dist/generic/RegisterForStartup.d.ts +44 -0
  8. package/dist/generic/RegisterForStartup.d.ts.map +1 -1
  9. package/dist/generic/RegisterForStartup.js +124 -48
  10. package/dist/generic/RegisterForStartup.js.map +1 -1
  11. package/dist/generic/baseEngine.d.ts +222 -7
  12. package/dist/generic/baseEngine.d.ts.map +1 -1
  13. package/dist/generic/baseEngine.js +555 -37
  14. package/dist/generic/baseEngine.js.map +1 -1
  15. package/dist/generic/baseEngineSweeper.d.ts +35 -0
  16. package/dist/generic/baseEngineSweeper.d.ts.map +1 -0
  17. package/dist/generic/baseEngineSweeper.js +97 -0
  18. package/dist/generic/baseEngineSweeper.js.map +1 -0
  19. package/dist/generic/baseEntity.d.ts +45 -1
  20. package/dist/generic/baseEntity.d.ts.map +1 -1
  21. package/dist/generic/baseEntity.js +308 -187
  22. package/dist/generic/baseEntity.js.map +1 -1
  23. package/dist/generic/baseInfo.d.ts +31 -3
  24. package/dist/generic/baseInfo.d.ts.map +1 -1
  25. package/dist/generic/baseInfo.js +138 -40
  26. package/dist/generic/baseInfo.js.map +1 -1
  27. package/dist/generic/databaseProviderBase.d.ts +57 -19
  28. package/dist/generic/databaseProviderBase.d.ts.map +1 -1
  29. package/dist/generic/databaseProviderBase.js +203 -80
  30. package/dist/generic/databaseProviderBase.js.map +1 -1
  31. package/dist/generic/entityConfiguration.d.ts +8 -2
  32. package/dist/generic/entityConfiguration.d.ts.map +1 -1
  33. package/dist/generic/entityConfiguration.js +9 -0
  34. package/dist/generic/entityConfiguration.js.map +1 -1
  35. package/dist/generic/entityEventBatch.d.ts +124 -0
  36. package/dist/generic/entityEventBatch.d.ts.map +1 -0
  37. package/dist/generic/entityEventBatch.js +194 -0
  38. package/dist/generic/entityEventBatch.js.map +1 -0
  39. package/dist/generic/entityInfo.d.ts +103 -8
  40. package/dist/generic/entityInfo.d.ts.map +1 -1
  41. package/dist/generic/entityInfo.js +376 -60
  42. package/dist/generic/entityInfo.js.map +1 -1
  43. package/dist/generic/infoProviderResolution.d.ts +16 -0
  44. package/dist/generic/infoProviderResolution.d.ts.map +1 -0
  45. package/dist/generic/infoProviderResolution.js +17 -0
  46. package/dist/generic/infoProviderResolution.js.map +1 -0
  47. package/dist/generic/interfaces.d.ts +143 -3
  48. package/dist/generic/interfaces.d.ts.map +1 -1
  49. package/dist/generic/interfaces.js.map +1 -1
  50. package/dist/generic/localCacheManager.d.ts +505 -56
  51. package/dist/generic/localCacheManager.d.ts.map +1 -1
  52. package/dist/generic/localCacheManager.js +1185 -175
  53. package/dist/generic/localCacheManager.js.map +1 -1
  54. package/dist/generic/logging.d.ts +15 -0
  55. package/dist/generic/logging.d.ts.map +1 -1
  56. package/dist/generic/logging.js +17 -0
  57. package/dist/generic/logging.js.map +1 -1
  58. package/dist/generic/metadata.d.ts +1 -1
  59. package/dist/generic/metadata.d.ts.map +1 -1
  60. package/dist/generic/metadata.js.map +1 -1
  61. package/dist/generic/mjapiUrl.d.ts +21 -0
  62. package/dist/generic/mjapiUrl.d.ts.map +1 -0
  63. package/dist/generic/mjapiUrl.js +25 -0
  64. package/dist/generic/mjapiUrl.js.map +1 -0
  65. package/dist/generic/providerBase.d.ts +301 -5
  66. package/dist/generic/providerBase.d.ts.map +1 -1
  67. package/dist/generic/providerBase.js +750 -168
  68. package/dist/generic/providerBase.js.map +1 -1
  69. package/dist/generic/queryInfo.d.ts +10 -9
  70. package/dist/generic/queryInfo.d.ts.map +1 -1
  71. package/dist/generic/queryInfo.js +60 -33
  72. package/dist/generic/queryInfo.js.map +1 -1
  73. package/dist/generic/scoring/ReciprocalRankFusion.d.ts.map +1 -1
  74. package/dist/generic/scoring/ReciprocalRankFusion.js +10 -3
  75. package/dist/generic/scoring/ReciprocalRankFusion.js.map +1 -1
  76. package/dist/generic/securityInfo.d.ts +11 -6
  77. package/dist/generic/securityInfo.d.ts.map +1 -1
  78. package/dist/generic/securityInfo.js +15 -10
  79. package/dist/generic/securityInfo.js.map +1 -1
  80. package/dist/generic/telemetryManager.d.ts.map +1 -1
  81. package/dist/generic/telemetryManager.js +4 -1
  82. package/dist/generic/telemetryManager.js.map +1 -1
  83. package/dist/generic/transactionGroup.d.ts +27 -0
  84. package/dist/generic/transactionGroup.d.ts.map +1 -1
  85. package/dist/generic/transactionGroup.js +58 -5
  86. package/dist/generic/transactionGroup.js.map +1 -1
  87. package/dist/generic/util.d.ts.map +1 -1
  88. package/dist/generic/util.js +3 -0
  89. package/dist/generic/util.js.map +1 -1
  90. package/dist/generic/wellKnownUserSource.d.ts +3 -2
  91. package/dist/generic/wellKnownUserSource.d.ts.map +1 -1
  92. package/dist/generic/wellKnownUserSource.js +18 -9
  93. package/dist/generic/wellKnownUserSource.js.map +1 -1
  94. package/dist/index.d.ts +2 -0
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +2 -0
  97. package/dist/index.js.map +1 -1
  98. package/package.json +3 -3
  99. package/readme.md +29 -4
@@ -3,6 +3,14 @@ import { AggregateResult, DatasetItemFilterType, DatasetResultType, IMetadataPro
3
3
  import { AggregateExpression, RunViewParams } from "../views/runView.js";
4
4
  import { BaseEntityEvent } from "./baseEntity.js";
5
5
  import { CompositeKey } from "./compositeKey.js";
6
+ /**
7
+ * One change for {@link LocalCacheManager.ApplyRowChanges}: `Row` adds or replaces the row with
8
+ * `Key`; `Row: null` removes it.
9
+ */
10
+ export interface CachedRowChange {
11
+ Key: CompositeKey;
12
+ Row: Record<string, unknown> | null;
13
+ }
6
14
  /**
7
15
  * The type of cache entry: dataset, runview, or runquery
8
16
  */
@@ -90,6 +98,14 @@ export interface CachedRunViewData {
90
98
  * Backward-compatible: entries without this field are served normally (no regression).
91
99
  */
92
100
  schemaHash?: string;
101
+ /**
102
+ * The newest `__mj_UpdatedAt` (ISO string) across the entity's metadata row and its field rows,
103
+ * written alongside {@link schemaHash}. When two processes sharing a cache disagree on the hash,
104
+ * this says which of them holds the newer metadata, so the older one stops treating the newer
105
+ * one's slots as stale. Absent on entries from writers that predate it or whose metadata
106
+ * carries no timestamps.
107
+ */
108
+ schemaStamp?: string;
93
109
  /**
94
110
  * Set when the slot was written with {@link CacheWriteOptions.ProviderInternalScaffolding}.
95
111
  * Persisted so that in-place slot maintenance (`storeCachedResults`, driven by BaseEntity
@@ -124,12 +140,50 @@ export interface CachedRunViewResult {
124
140
  * permanently disabling post-migration drift detection for that slot.
125
141
  */
126
142
  schemaHash?: string;
143
+ /** Surfaced for the same carry-forward reason as {@link schemaHash}. See {@link CachedRunViewData.schemaStamp}. */
144
+ schemaStamp?: string;
127
145
  /**
128
146
  * Surfaced from the stored payload so in-place maintenance can carry the freeze exemption
129
147
  * forward. See {@link CachedRunViewData.providerInternalScaffolding}.
130
148
  */
131
149
  providerInternalScaffolding?: boolean;
132
150
  }
151
+ /** A cached RunQuery result, as {@link LocalCacheManager.GetRunQueryResult} returns it. */
152
+ export interface CachedRunQuerySlot {
153
+ /**
154
+ * The cached result rows — shared and deep-frozen under a reference-sharing storage
155
+ * provider, exactly like {@link CachedRunViewData.results}. Do not mutate.
156
+ */
157
+ results: unknown[];
158
+ maxUpdatedAt: string;
159
+ rowCount: number;
160
+ queryId?: string;
161
+ /** User who executed the authorized miss that produced this slot (B43 tie-breaker). */
162
+ warmedForUserID?: string;
163
+ /** The definition hash the slot was written under; undefined for slots written without one. */
164
+ definitionHash?: string;
165
+ }
166
+ /**
167
+ * What a saved query's cached results were computed from. A RunQuery slot stores the hash and the
168
+ * stamp it was written under; a read that expects a different hash is a miss. Computed by the
169
+ * database provider from the query metadata it executes (`QueryEngine`), so every process that
170
+ * runs the query agrees on it once it holds the same definition.
171
+ */
172
+ export interface QueryDefinitionSignature {
173
+ /** The query the signature describes. Its RunQuery slots are indexed and held by this ID. */
174
+ QueryID: string;
175
+ /**
176
+ * Hash of everything that changes the query's results: its SQL for the platform, the columns
177
+ * that change how that SQL executes, its parameter definitions, and the hashes of the queries
178
+ * it composes.
179
+ */
180
+ Hash: string;
181
+ /**
182
+ * The newest `__mj_UpdatedAt` (ISO string) across the rows the hash covers. When two processes
183
+ * sharing a cache disagree on the hash, this says which of them holds the newer definition.
184
+ */
185
+ Stamp?: string;
186
+ }
133
187
  /**
134
188
  * Per-write options for the cache write methods.
135
189
  */
@@ -174,16 +228,29 @@ export interface LocalCacheManagerConfig {
174
228
  */
175
229
  maxPercentOfCachePerEntity: number;
176
230
  /**
177
- * Maximum size of any single cache entry, expressed as a percentage of
178
- * maxSizeBytes. An entry estimated larger than this cap is not cached at
179
- * all — the write is skipped (logged, data still returned to the caller
180
- * uncached). Without this cap, storing an oversized entry is strictly worse
181
- * than not caching it: evictIfNeeded frees max(incoming, 10% of budget), so
182
- * an entry larger than the whole budget evicts EVERY other entry and still
183
- * cannot be retained within budget — a full cache wipe on every store.
184
- * Applies to RunView and RunQuery entries. Default: 25. Set to 0 to disable.
185
- */
186
- maxEntryPercentOfCache: number;
231
+ * Ceiling on the size of any ONE cache entry. An entry estimated larger than the
232
+ * ceiling is not cached at all — the write is skipped (logged, data still returned
233
+ * to the caller uncached). Applies to RunView and RunQuery entries.
234
+ *
235
+ * The ceiling exists because an entry that cannot be RETAINED within the budget is
236
+ * worse than no entry: it evicts other entries to make room and is itself evicted on
237
+ * the next store, so the cache churns and nothing is ever served from it.
238
+ *
239
+ * Three modes:
240
+ * - `'auto'` (default): the ceiling is DERIVED from the budget — everything except a
241
+ * reserve of {@link SINGLE_ENTRY_RESERVE_PERCENT} held back for the rest of the
242
+ * cache. This is deliberately not a small fixed fraction: a fixed fraction sized for
243
+ * a small tenant makes the one entity worth caching (the biggest one) permanently
244
+ * uncacheable, which is the failure this mode exists to avoid. The only entries
245
+ * `'auto'` declines are entries that genuinely cannot be retained — and the decline
246
+ * message says which `maxMemoryMB` would admit them.
247
+ * - a positive number: an explicit percentage of `maxSizeBytes`, for an operator who
248
+ * wants a stricter ceiling than `'auto'`. Clamped to
249
+ * {@link HARD_MAX_ENTRY_PERCENT} — no configuration may admit an entry that leaves
250
+ * the cache less free space than one eviction cycle needs.
251
+ * - `0`: no ceiling at all (the original, unguarded behaviour — not recommended).
252
+ */
253
+ maxEntryPercentOfCache: number | 'auto';
187
254
  /**
188
255
  * Interval in milliseconds for the periodic eviction sweep.
189
256
  * Catches entries that should have been evicted (TTL expired) but weren't
@@ -329,6 +396,25 @@ export declare class LocalCacheManager extends BaseSingleton<LocalCacheManager>
329
396
  * (e.g., TransactionGroup batch deletes).
330
397
  */
331
398
  private _fingerprintLocks;
399
+ /** Logged once when a local TTL is configured against a shared store. See {@link runEvictionSweep}. */
400
+ private _sharedTTLWarned;
401
+ /** Open entity-event batches. See {@link BeginEntityEventBatch}. */
402
+ private _entityEventBatches;
403
+ /** Save/delete events a batch took when they were raised. See {@link WasTakenByEntityEventBatch}. */
404
+ private _eventsTakenByBatch;
405
+ /**
406
+ * Write holds on entities whose shared slots were found under a different schema, keyed by
407
+ * lowercased entity name. While a hold applies, {@link SetRunViewResult} skips or rate-limits
408
+ * writes of that entity's slots. See {@link handleSchemaMismatch} and {@link mayWriteUnderSchemaHold}.
409
+ */
410
+ private _schemaWriteHolds;
411
+ /**
412
+ * How often, at most, a process writes an entity's shared slots while it keeps finding that
413
+ * entity's slots under a schema different from its own. See {@link handleSchemaMismatch}.
414
+ */
415
+ static readonly SCHEMA_MISMATCH_WRITE_INTERVAL_MS = 60000;
416
+ /** Schema mismatches already reported, so each entity/hash pair is logged once per process. */
417
+ private _reportedSchemaMismatches;
332
418
  private readonly REGISTRY_KEY;
333
419
  /**
334
420
  * Reverse index from entity name to the set of RunView cache fingerprints
@@ -341,11 +427,15 @@ export declare class LocalCacheManager extends BaseSingleton<LocalCacheManager>
341
427
  * Initialize the cache manager with a storage provider.
342
428
  * This should be called during app startup after the storage provider is available.
343
429
  *
344
- * This method is safe to call multiple times - subsequent calls will return the same
345
- * promise as the first caller, ensuring initialization only happens once.
430
+ * Safe to call repeatedly: the cache is initialized once, but **a `config` passed by a later
431
+ * caller is still applied**. That matters because a database provider initializes this manager
432
+ * from inside its own `Config()` — before the host has a chance to pass anything — so a host
433
+ * that hands its settings to `StartupManager` would otherwise have them silently dropped, and
434
+ * every `cacheSettings` knob would be inert. The host's explicit settings win
435
+ * over the provider's implicit initialization, whichever runs first.
346
436
  *
347
437
  * @param storageProvider - The local storage provider to use for persistence
348
- * @param config - Optional configuration overrides
438
+ * @param config - Optional configuration overrides, applied even when already initialized
349
439
  * @returns A promise that resolves when initialization is complete
350
440
  */
351
441
  Initialize(storageProvider: ILocalStorageProvider, config?: Partial<LocalCacheManagerConfig>): Promise<void>;
@@ -370,6 +460,17 @@ export declare class LocalCacheManager extends BaseSingleton<LocalCacheManager>
370
460
  * Internal initialization logic - only called once by the first caller
371
461
  */
372
462
  private doInitialize;
463
+ private _initializedListeners;
464
+ /**
465
+ * Runs `listener` once the cache manager is initialized — immediately if it already is.
466
+ * Lets code that ran too early (an engine loaded before startup initialized the cache) finish
467
+ * its cache wiring later instead of never.
468
+ *
469
+ * @param listener - Called once; errors are logged and do not affect other listeners
470
+ */
471
+ WhenInitialized(listener: () => void): void;
472
+ private runInitializedListener;
473
+ private notifyInitialized;
373
474
  /**
374
475
  * Returns whether the cache manager has been initialized
375
476
  */
@@ -379,7 +480,8 @@ export declare class LocalCacheManager extends BaseSingleton<LocalCacheManager>
379
480
  */
380
481
  get Config(): LocalCacheManagerConfig;
381
482
  /**
382
- * Updates the configuration at runtime
483
+ * Updates the configuration at runtime, and re-arms the eviction sweep so a changed interval
484
+ * (or a disabled cache) takes effect immediately instead of at the next restart.
383
485
  */
384
486
  UpdateConfig(config: Partial<LocalCacheManagerConfig>): void;
385
487
  /**
@@ -438,6 +540,23 @@ export declare class LocalCacheManager extends BaseSingleton<LocalCacheManager>
438
540
  * in-place mutation (`canUseImmediateMutation`); this closes the same gap in the raw
439
541
  * provider cache. (B42)
440
542
  *
543
+ * Maintaining the order in place was built and then withdrawn, so the payoff is on record rather
544
+ * than left to be rediscovered. It is possible for a narrow subset — plain column terms over
545
+ * numeric/date/boolean fields with no NULLs present, where JavaScript and SQL provably agree —
546
+ * but the subset is where the value dies, not the implementation. Of 918 RunView param literals
547
+ * in this repo, 289 carry an ORDER BY, 112 are also unfiltered and uncapped (the only ones a slot
548
+ * could maintain), and roughly 26 fall in that type subset, several of which order on a
549
+ * uniqueidentifier and are refused anyway. The survivors are about a dozen configuration and
550
+ * admin lists — API keys, credentials, view types, schema info — saved rarely, so the saving is a
551
+ * handful of small indexed reads per day. High-write entities gain nothing: every ordered read of
552
+ * MJ: AI Agent Runs, MJ: Conversation Details and MJ: AI Prompt Runs is FILTERED, so their slots
553
+ * land in the invalidate branch regardless. Against that, the cost is a comparator that must keep
554
+ * agreeing with SQL collation and NULL placement across dialects forever, whose failure mode is a
555
+ * silently reordered list in a slot other readers treat as authoritative. Widening it to strings —
556
+ * by far the largest bucket, `Name` — means owning collation per dialect and per column.
557
+ *
558
+ * If you are about to attempt this, the measurement above is the argument not to.
559
+ *
441
560
  * @param parts - the fingerprint already split on '|'
442
561
  */
443
562
  protected hasOrderBy(parts: string[]): boolean;
@@ -547,14 +666,82 @@ export declare class LocalCacheManager extends BaseSingleton<LocalCacheManager>
547
666
  */
548
667
  protected isSubsetFingerprint(fingerprint: string): boolean;
549
668
  /**
550
- * Checks whether a cached RunView entry is structurally stale due to a schema change
551
- * (e.g., new columns added via migration + CodeGen). Compares the stored schema hash
552
- * against the current entity field list. If they differ, the entry is invalidated.
669
+ * Compares a cached RunView entry's schema hash against this process's current field list for
670
+ * the entity (a mismatch follows a migration + CodeGen that added, removed or reordered
671
+ * columns). Entries without a hash, and entities this process cannot resolve, never mismatch.
553
672
  * @param fingerprint - The cache fingerprint
554
673
  * @param data - The cached data to validate
555
- * @returns true if the entry is stale and should not be served
556
- */
557
- private isSchemaStaleCacheEntry;
674
+ * @returns The mismatch, or null when the entry may be served
675
+ */
676
+ private findSchemaMismatch;
677
+ /** The mismatch subject for an entity's RunView slots. */
678
+ private static runViewSubject;
679
+ /** The mismatch subject for a query's RunQuery slots. */
680
+ private static runQuerySubject;
681
+ /**
682
+ * True only when both stamps are present and the cached entry's is strictly newer than this
683
+ * process's. Equal or missing stamps leave the order unknown, which counts as not newer.
684
+ */
685
+ private static isNewerSchemaStamp;
686
+ /**
687
+ * Decides what a signature mismatch does to the slot: an entity schema mismatch on a RunView
688
+ * slot, or a query definition mismatch on a RunQuery slot. The entry is never served; the
689
+ * question is whether the slot is deleted, which on a shared store may also tell every peer to
690
+ * reload.
691
+ *
692
+ * - Process-local store, or a slot-maintenance read: delete it (`drop`). No other process reads a
693
+ * local store. A maintenance read happens because a row changed, and a slot that cannot take
694
+ * the change in place must go, whichever process wrote it.
695
+ * - Shared store, serving read: leave it, and hold back this process's writes of the subject's
696
+ * slots. Deleting a RunView slot publishes `removed`, every peer engine holding the slot
697
+ * reloads and rewrites it under its own hash, and processes on either side of a migration keep
698
+ * doing that to each other. The caller reads the database instead.
699
+ * - When the slot is known to come from newer metadata, this process is the one out of date.
700
+ * It stops writing the subject's slots until its own hash changes.
701
+ * - Otherwise it writes the subject's slots at most once per
702
+ * {@link SCHEMA_MISMATCH_WRITE_INTERVAL_MS} while mismatches keep appearing. That still
703
+ * replaces a slot left by older metadata, but a peer running code that deletes mismatched
704
+ * slots (any release before this check stopped deleting them) gets one slot to delete per
705
+ * interval instead of one per read. The hold lapses after an interval with no mismatch.
706
+ * For RunView slots the first replacement waits one interval. For RunQuery slots it is
707
+ * immediate: no release ever deleted a RunQuery slot over its definition and the category
708
+ * is not published, so an immediate replacement cannot feed a loop, and waiting would make
709
+ * every server run an edited query against the database for a full interval.
710
+ */
711
+ private handleSchemaMismatch;
712
+ /** The log text for a mismatch that left the slot in place and took a write hold. */
713
+ private static describeHeldMismatch;
714
+ /** The key of a subject's write hold. RunView and RunQuery holds share one map. */
715
+ private static holdKey;
716
+ /** Records or extends the write hold for a mismatch seen on a shared store. */
717
+ private holdSchemaWrites;
718
+ /** Logs a mismatch once per subject and hash pair per process. */
719
+ private reportSchemaMismatch;
720
+ /**
721
+ * Whether a slot of `subject` may be written now, given its write hold (see
722
+ * {@link handleSchemaMismatch}). A permitted write under a rate-limiting hold uses up the
723
+ * interval. The hold is dropped once this process's hash changes (a metadata refresh, or a
724
+ * query edit reaching this process) or, for a rate-limiting hold, once an interval passes with
725
+ * no mismatch.
726
+ */
727
+ private mayWriteUnderSchemaHold;
728
+ /**
729
+ * The newest `__mj_UpdatedAt` among `rows` as an ISO string, `''` for an empty set, or
730
+ * `undefined` when the rows carry no such column (so the caller can fall back to its own stamp).
731
+ * Mirrors how the RunView fill path stamps a slot.
732
+ */
733
+ static MaxUpdatedAtOfRows(rows: ReadonlyArray<unknown>): string | undefined;
734
+ /**
735
+ * Storage write options for a RunView slot: the entity name as index group, so a shared
736
+ * provider can find the slot from any process, and the time left before the slot's own expiry
737
+ * (external-data-source entities), so a rewrite does not extend it.
738
+ *
739
+ * @param fingerprint - The slot key
740
+ * @param expiresAt - Absolute expiry (ms since epoch), when the slot has one
741
+ */
742
+ private runViewWriteOptions;
743
+ /** Storage write options for any registry entry; only RunView slots are indexed. */
744
+ private writeOptionsForEntry;
558
745
  /**
559
746
  * Adds a fingerprint to the entity→fingerprint reverse index.
560
747
  * Called when a RunView result is cached.
@@ -571,11 +758,14 @@ export declare class LocalCacheManager extends BaseSingleton<LocalCacheManager>
571
758
  */
572
759
  GetFingerprintsForEntity(entityName: string): ReadonlySet<string>;
573
760
  /**
574
- * Resolves cached fingerprints for an entity, checking the local in-memory
575
- * index first and falling back to the shared storage provider (e.g., Redis)
576
- * when the local index is empty. This handles cross-server scenarios where
577
- * Server A cached RunView results and Server B saves a record — Server B's
578
- * local index is empty but Redis still has the stale cached entries.
761
+ * Resolves cached fingerprints for an entity: this process's own index, plus — on a storage
762
+ * provider shared between processes — the slots other processes wrote.
763
+ *
764
+ * A save on server A has to reach slots server B wrote after A booted, because nothing else
765
+ * will: B only hears about A's save through the slots A rewrites. A shared provider therefore
766
+ * answers from its per-entity index ({@link ILocalStorageProvider.GetIndexGroupKeys}) on every
767
+ * call. A provider without that index is process-local or cannot enumerate cheaply, and keeps
768
+ * the older behaviour: the category is listed only when the local index knows nothing.
579
769
  */
580
770
  private resolveFingerprintsForEntity;
581
771
  /**
@@ -593,6 +783,118 @@ export declare class LocalCacheManager extends BaseSingleton<LocalCacheManager>
593
783
  * @param entityEvent - The BaseEntity event payload
594
784
  */
595
785
  protected HandleBaseEntityEvent(entityEvent: BaseEntityEvent): Promise<void>;
786
+ /**
787
+ * Opens a batch for `owner` (normally a provider), or enters the one already open. Until the
788
+ * matching {@link EndEntityEventBatch}, saves and deletes raised by entities that use `owner`
789
+ * as their provider are recorded instead of applied; closing the batch rewrites each affected
790
+ * slot once. `DatabaseProviderBase.BeginEntityTransaction` opens one for every transaction, so
791
+ * callers rarely need this directly — {@link RunInEntityEventBatch} is the wrapper for bulk
792
+ * writers that do not use a transaction.
793
+ *
794
+ * Every call must be paired with exactly one {@link EndEntityEventBatch}.
795
+ */
796
+ BeginEntityEventBatch(owner: object): void;
797
+ /**
798
+ * Replaces a RunView slot wholesale while holding the cross-process lock for it.
799
+ *
800
+ * The event-driven path already reads-modifies-writes under that lock; a writer that replaces
801
+ * the same slot outside it can land between a peer's read and its write, and whichever `SET`
802
+ * lands second wins — the engine sweep's fresh rows lost, or the peer's delta lost, and either
803
+ * way the losing state is published to the fleet.
804
+ */
805
+ ReplaceRunViewResultLocked(fingerprint: string, params: RunViewParams, results: unknown[], maxUpdatedAt: string, totalRowCount: number, provider?: IMetadataProvider): Promise<boolean>;
806
+ /** Milliseconds left on a slot's expiry, or undefined when it has none. @internal */
807
+ private remainingTTLForSlot;
808
+ /**
809
+ * Abandons `owner`'s batch at any depth and invalidates every slot it touched.
810
+ *
811
+ * For an owner that cannot settle its batch normally: an independent provider instance being
812
+ * released with a transaction still open, or a transaction handle reset after a failure. The
813
+ * buffered rows may or may not have reached the database, so the affected slots are invalidated
814
+ * rather than written, and the entities stop counting as pending — a batch left open makes
815
+ * every cached read of its entities miss, for the life of the process.
816
+ *
817
+ * Never throws. Safe when no batch is open.
818
+ */
819
+ AbandonEntityEventBatch(owner: object): Promise<void>;
820
+ /**
821
+ * Leaves one level of `owner`'s batch. Closing the outermost level applies the batch: with
822
+ * every level successful, each affected slot is rewritten once with all its changes; if any
823
+ * level failed (a rollback), the affected slots are invalidated instead, so every server reloads
824
+ * them from the database. Never throws.
825
+ *
826
+ * @param owner - The object passed to {@link BeginEntityEventBatch}
827
+ * @param succeeded - False when the work this level covered was rolled back or failed
828
+ */
829
+ EndEntityEventBatch(owner: object, succeeded: boolean): Promise<void>;
830
+ /**
831
+ * Claims a named lease on the storage provider for `ttlMs`: on a shared store only one process
832
+ * gets it per period; on a private store the caller always does. A lease that cannot be
833
+ * checked is reported as not claimed.
834
+ */
835
+ TryAcquireSharedLease(name: string, ttlMs: number): Promise<boolean>;
836
+ /** One lease attempt: claimed, held by another process, or the store could not be asked. */
837
+ private claimLease;
838
+ /**
839
+ * Extends a lease this process already holds, so long-running work keeps its turn. Falls back
840
+ * to re-claiming when the store has no dedicated renewal, which is a no-op for anyone else's
841
+ * lease because the claim is `SET NX`. Never throws.
842
+ *
843
+ * @returns true when the lease is still (or again) held by this process.
844
+ */
845
+ RenewSharedLease(name: string, ttlMs: number): Promise<boolean>;
846
+ /** Ends a lease claimed with {@link TryAcquireSharedLease}. Never throws. */
847
+ ReleaseSharedLease(name: string): Promise<void>;
848
+ /**
849
+ * Waits until this process claims the lease, polling every `pollMs`, for at most `maxWaitMs`.
850
+ * @returns Whether the lease was claimed, and how long the wait took.
851
+ */
852
+ WaitForSharedLease(name: string, ttlMs: number, maxWaitMs: number, pollMs?: number): Promise<{
853
+ Acquired: boolean;
854
+ WaitedMs: number;
855
+ }>;
856
+ /** True while a batch is open for `owner`. */
857
+ IsBatchingEntityEvents(owner: object | null | undefined): boolean;
858
+ /**
859
+ * True when a batch took `entityEvent` as it was raised, so closing that batch maintains every
860
+ * slot indexed for the entity. A listener that reaches its cache write only after an `await`
861
+ * asks this instead of {@link IsBatchingEntityEvents}: by then the batch may have closed, and
862
+ * a write of its own would rewrite the slot once per event.
863
+ */
864
+ WasTakenByEntityEventBatch(entityEvent: BaseEntityEvent): boolean;
865
+ /**
866
+ * Runs `work` inside an entity-event batch for `owner` and applies the batch when it finishes
867
+ * (as a failure when `work` throws). Use for bulk writes that do not already run in a
868
+ * transaction.
869
+ */
870
+ RunInEntityEventBatch<T>(owner: object, work: () => Promise<T>): Promise<T>;
871
+ /**
872
+ * Records a save/delete in the batch open for the entity's provider.
873
+ * @returns True when the event was taken by a batch (or needs no cache work at all).
874
+ */
875
+ private bufferIfBatching;
876
+ /**
877
+ * True when `fingerprint`'s entity has changes waiting in an open batch. Such a slot is behind
878
+ * what the unit of work has written, so reads treat it as a miss (the database answers,
879
+ * including the uncommitted rows for a reader inside the transaction) and fills are not stored.
880
+ */
881
+ private isPendingInBatch;
882
+ /**
883
+ * The change a save/delete event describes, captured now — the entity object can change (or,
884
+ * after a delete, be reset) before a batch closes. Null when the event needs no cache work.
885
+ */
886
+ private captureEntityChange;
887
+ /** Applies a closed batch; see {@link EndEntityEventBatch}. */
888
+ private applyEntityEventBatch;
889
+ /** Invalidates every entity a batch touched — for a batch that failed or was abandoned. */
890
+ private invalidateBatchEntities;
891
+ /** Applies one entity's buffered changes to every slot indexed for it, each slot once. */
892
+ private applyEntityChanges;
893
+ /**
894
+ * One slot, several changes: the same decisions {@link processEntityEventForFingerprint} makes
895
+ * per event, made once for the whole list.
896
+ */
897
+ private applyEntityChangesToSlot;
596
898
  /**
597
899
  * Handles remote-invalidate events that include recordData (the saved entity as JSON).
598
900
  * Updates all cached RunView results for the entity without a server round-trip.
@@ -637,6 +939,20 @@ export declare class LocalCacheManager extends BaseSingleton<LocalCacheManager>
637
939
  * Decomposed from HandleBaseEntityEvent for clarity and testability.
638
940
  */
639
941
  private processEntityEventForFingerprint;
942
+ /**
943
+ * Handles a save/delete that could not be applied to a slot this process indexed — in practice
944
+ * because the slot expired or was removed by someone else.
945
+ *
946
+ * Rewriting the slot is how peer servers hear about the change: their engines hold the rows in
947
+ * memory and never read the slot again, so a slot that expired while they were running leaves
948
+ * nothing to rewrite and the change would never reach them. Invalidating instead publishes a
949
+ * `removed` event on a shared provider, which makes every peer engine tracking the slot reload
950
+ * it from the database. It also drops the dead fingerprint from this process's index.
951
+ *
952
+ * @param fingerprint - The slot the event was applied to
953
+ * @param maintained - What the in-place upsert/remove reported
954
+ */
955
+ private invalidateIfNotMaintained;
640
956
  /**
641
957
  * Map from cache fingerprint (or category for category_cleared events) to
642
958
  * registered {@link CacheChangedEvent} callbacks. Callbacks are invoked when
@@ -675,9 +991,8 @@ export declare class LocalCacheManager extends BaseSingleton<LocalCacheManager>
675
991
  * fingerprint. Called by infrastructure code (e.g., {@link RedisLocalStorageProvider})
676
992
  * when another server modifies a cached entry.
677
993
  *
678
- * For `category_cleared` events, dispatches to ALL registered callbacks whose
679
- * fingerprints belong to the cleared category (matched by the event's CacheKey
680
- * which contains the category name).
994
+ * For `category_cleared` events on the RunView category, dispatches to ALL registered
995
+ * callbacks (they all watch RunView slots); a clear of any other category reaches none.
681
996
  *
682
997
  * Errors in individual callbacks are caught and logged via {@link LogError}
683
998
  * to prevent one bad callback from blocking others.
@@ -827,6 +1142,14 @@ export declare class LocalCacheManager extends BaseSingleton<LocalCacheManager>
827
1142
  * @returns The schema hash string, or undefined if the entity can't be resolved
828
1143
  */
829
1144
  ComputeSchemaHash(provider: IMetadataProvider | undefined, entityName: string): string | undefined;
1145
+ /**
1146
+ * The entity's schema hash (see {@link ComputeSchemaHash}) together with its schema stamp: the
1147
+ * newest `__mj_UpdatedAt` across the entity row and its field rows, as an ISO string, or
1148
+ * undefined when the metadata carries no timestamps.
1149
+ */
1150
+ private computeSchemaSignature;
1151
+ /** Milliseconds since the epoch for a metadata timestamp (a Date or a string), or 0 when unreadable. */
1152
+ private static timestampMs;
830
1153
  /**
831
1154
  * Deep-freezes an about-to-be-stored cache payload when — and only when — the active
832
1155
  * storage provider hands out live object references
@@ -892,6 +1215,8 @@ export declare class LocalCacheManager extends BaseSingleton<LocalCacheManager>
892
1215
  * @returns The cached results, maxUpdatedAt, rowCount (derived), and aggregateResults, or null if not found
893
1216
  */
894
1217
  GetRunViewResult(fingerprint: string): Promise<CachedRunViewResult | null>;
1218
+ /** {@link GetRunViewResult} without the open-batch check; slot maintenance reads through this. */
1219
+ private readRunViewSlot;
895
1220
  /**
896
1221
  * Batched retrieval for many cached RunView results in a single underlying
897
1222
  * IndexedDB transaction (or Redis MGET, or one in-memory pass — depends on
@@ -920,10 +1245,35 @@ export declare class LocalCacheManager extends BaseSingleton<LocalCacheManager>
920
1245
  * recording the appropriate hit/miss + access-tracking side effects.
921
1246
  *
922
1247
  * Also validates the schema hash (if present) to detect structurally stale
923
- * cache entries after schema migrations. If the entity's field list changed
924
- * since the entry was cached, the entry is invalidated and null is returned.
1248
+ * cache entries after schema migrations. If the entity's field list differs
1249
+ * from the one the entry was cached under, null is returned; whether the slot
1250
+ * is also deleted depends on `purpose` (see {@link handleSchemaMismatch}).
925
1251
  */
926
1252
  private materializeCachedRunViewResult;
1253
+ /**
1254
+ * Records a slot this process READ, so a later save can still find it once it has expired.
1255
+ *
1256
+ * An engine loads its rows once and never reads its slot again — that is the design, not a
1257
+ * defect: the slot is a boot shortcut and a propagation channel, and correctness comes from
1258
+ * events. Giving slots a finite expiry opened a window those events could not cross. When a
1259
+ * slot expires under running replicas, a save finds nothing to rewrite and publishes nothing,
1260
+ * so every peer engine keeps serving what it loaded.
1261
+ *
1262
+ * The fix — invalidate a slot that cannot be maintained, which publishes `removed` — only
1263
+ * fires if the saving process still holds the fingerprint, and only the process that WROTE the
1264
+ * slot did. A server that booted warm read it instead, and a read indexed nothing; behind a
1265
+ * load balancer, which server takes the save is arbitrary. A warm read is the only trace such a
1266
+ * process has of that slot, so it is indexed too.
1267
+ *
1268
+ * Gated on the provider exposing a shared per-entity index, which is exactly the case
1269
+ * {@link resolveFingerprintsForEntity} answers from that index — with no category-scan
1270
+ * fallback. On a process-local store there is nothing to gain (no peers to notify, and every
1271
+ * slot in it was written by this process, so it is already indexed) and something to lose: a
1272
+ * non-empty index SUPPRESSES that fallback scan, which is what finds persisted slots the index
1273
+ * does not know about.
1274
+ * @internal
1275
+ */
1276
+ private indexSlotThisProcessDidNotWrite;
927
1277
  /**
928
1278
  * Invalidates a cached RunView result.
929
1279
  *
@@ -959,6 +1309,14 @@ export declare class LocalCacheManager extends BaseSingleton<LocalCacheManager>
959
1309
  * @returns The merged results after applying the differential update, or null if cache not found
960
1310
  */
961
1311
  ApplyDifferentialUpdate(fingerprint: string, params: RunViewParams, updatedRows: unknown[], deletedRecordIDs: string[], primaryKeyFieldNames: string | string[], newMaxUpdatedAt: string, serverRowCount?: number, aggregateResults?: AggregateResult[], provider?: IMetadataProvider): Promise<CachedRunViewResult | null>;
1312
+ /**
1313
+ * Runs an in-place slot rewrite under {@link withFingerprintLock}. When the lock itself fails
1314
+ * (a shared lock another server held too long), reports the rewrite as not done rather than
1315
+ * throwing, so the caller's fallback — invalidate the slot, making every server reload —
1316
+ * applies.
1317
+ */
1318
+ private maintainSlotLocked;
1319
+ private withFingerprintLock;
962
1320
  /**
963
1321
  * Upserts a single entity in a cached RunView result.
964
1322
  * Used by BaseEngine for immediate cache sync when an entity is saved.
@@ -969,12 +1327,10 @@ export declare class LocalCacheManager extends BaseSingleton<LocalCacheManager>
969
1327
  * (e.g., 3 deletes from a TransactionGroup), each read-modify-write cycle
970
1328
  * must complete before the next one starts for the same fingerprint.
971
1329
  * Different fingerprints run concurrently with no contention.
972
- */
973
- private withFingerprintLock;
974
- /**
1330
+ *
975
1331
  * @param fingerprint - The cache fingerprint to update
976
1332
  * @param entityData - The entity data as a plain object (use entity.GetAll())
977
- * @param primaryKeyFieldName - Name of the primary key field
1333
+ * @param key - The record's primary key
978
1334
  * @param newMaxUpdatedAt - New maxUpdatedAt timestamp (from entity's __mj_UpdatedAt)
979
1335
  * @returns true if cache was updated, false if cache not found or update failed
980
1336
  */
@@ -989,6 +1345,23 @@ export declare class LocalCacheManager extends BaseSingleton<LocalCacheManager>
989
1345
  * @returns true if cache was updated, false if cache not found or update failed
990
1346
  */
991
1347
  RemoveSingleEntity(fingerprint: string, key: CompositeKey, newMaxUpdatedAt: string): Promise<boolean>;
1348
+ /**
1349
+ * Applies several row changes to one cached RunView result in a single read-modify-write:
1350
+ * each change with a `Row` adds or replaces that row, each with `Row: null` removes it, in
1351
+ * order. The slot is written once, and not at all when only removals of absent rows were asked.
1352
+ * All keys must name the same primary-key fields.
1353
+ *
1354
+ * @returns true if the slot now reflects the changes, false if it was not cached or the update failed
1355
+ */
1356
+ ApplyRowChanges(fingerprint: string, changes: ReadonlyArray<CachedRowChange>, newMaxUpdatedAt: string): Promise<boolean>;
1357
+ /**
1358
+ * Rows keyed by a cheap delimiter-joined PK string (no per-row CompositeKey/KeyValuePair
1359
+ * allocation); matching is consistent because {@link cheapKeyFromCompositeKey} uses the same
1360
+ * format. Rows missing a PK value are dropped.
1361
+ */
1362
+ private indexRowsByKey;
1363
+ /** Applies `changes` to `rows` in order. @returns True when anything changed. */
1364
+ private applyChangesToRowMap;
992
1365
  /**
993
1366
  * Stores updated results array back to the cache and updates the registry.
994
1367
  * Shared by UpsertSingleEntity and RemoveSingleEntity to avoid duplication.
@@ -1034,26 +1407,38 @@ export declare class LocalCacheManager extends BaseSingleton<LocalCacheManager>
1034
1407
  * @param rowCount - Optional row count (defaults to results.length if not provided)
1035
1408
  * @param queryId - Optional query ID for reference
1036
1409
  * @param ttlMs - Optional TTL in milliseconds (for cache expiry tracking)
1410
+ * @param warmedForUserID - The user whose authorized execution produced the rows
1411
+ * @param definition - The query definition the rows were computed under. Stored with the slot so
1412
+ * a read under a different definition misses (see {@link GetRunQueryResult}), and used to index
1413
+ * the slot by query ID (see {@link InvalidateRunQueryResultsForQuery}). On a shared store the
1414
+ * write is skipped while the query's slots are under a write hold (see
1415
+ * `handleSchemaMismatch`).
1037
1416
  */
1038
- SetRunQueryResult(fingerprint: string, queryName: string, results: unknown[], maxUpdatedAt: string, rowCount?: number, queryId?: string, ttlMs?: number, warmedForUserID?: string): Promise<void>;
1417
+ SetRunQueryResult(fingerprint: string, queryName: string, results: unknown[], maxUpdatedAt: string, rowCount?: number, queryId?: string, ttlMs?: number, warmedForUserID?: string, definition?: QueryDefinitionSignature): Promise<void>;
1418
+ /**
1419
+ * Store options for a RunQuery slot: its expiry, and the query ID as the index group, so a
1420
+ * shared store can list every slot of one query whichever process wrote it.
1421
+ */
1422
+ private static runQueryWriteOptions;
1039
1423
  /**
1040
1424
  * Retrieves a cached RunQuery result.
1041
1425
  *
1042
1426
  * @param fingerprint - The cache fingerprint
1043
- * @returns The cached results, maxUpdatedAt, rowCount, and queryId, or null if not found
1044
- */
1045
- GetRunQueryResult(fingerprint: string): Promise<{
1046
- /**
1047
- * The cached result rows — shared and deep-frozen under a reference-sharing storage
1048
- * provider, exactly like {@link CachedRunViewData.results}. Do not mutate.
1049
- */
1050
- results: unknown[];
1051
- maxUpdatedAt: string;
1052
- rowCount: number;
1053
- queryId?: string;
1054
- /** User who executed the authorized miss that produced this slot (B43 tie-breaker). */
1055
- warmedForUserID?: string;
1056
- } | null>;
1427
+ * @param expectedDefinition - The query definition the caller would execute on a miss. When
1428
+ * given, a slot written under a different definition (or one that records none) is a miss.
1429
+ * On a process-local store the slot is also deleted; on a shared store it is left in place and
1430
+ * this process's writes of the query's slots are held (see `handleSchemaMismatch`). Omit it
1431
+ * where the caller cannot compute a definition (a client, whose slots the server validates).
1432
+ * @returns The cached results, maxUpdatedAt, rowCount, queryId and definition hash, or null if
1433
+ * not found, expired, or written under another definition
1434
+ */
1435
+ GetRunQueryResult(fingerprint: string, expectedDefinition?: QueryDefinitionSignature): Promise<CachedRunQuerySlot | null>;
1436
+ /**
1437
+ * Compares a RunQuery slot's definition hash with the definition the caller expects. A slot
1438
+ * that records no hash was written before slots carried one, so nothing says which SQL produced
1439
+ * its rows; it counts as a mismatch with an unknown order.
1440
+ */
1441
+ private static findDefinitionMismatch;
1057
1442
  /**
1058
1443
  * Invalidates a cached RunQuery result.
1059
1444
  *
@@ -1064,19 +1449,39 @@ export declare class LocalCacheManager extends BaseSingleton<LocalCacheManager>
1064
1449
  * Invalidates all cached RunQuery results for a specific query.
1065
1450
  * Useful when a query's underlying data changes and all related caches should be cleared.
1066
1451
  *
1452
+ * Matches this process's registry by name only, so it misses slots written by other processes
1453
+ * on a shared store and slots written under a former name.
1454
+ *
1067
1455
  * @param queryName - The query name to invalidate
1456
+ * @deprecated Use {@link InvalidateRunQueryResultsForQuery}, which finds a query's slots by ID
1457
+ * in every process that shares the store.
1068
1458
  */
1069
1459
  InvalidateQueryCaches(queryName: string): Promise<void>;
1460
+ /**
1461
+ * Deletes every cached RunQuery result of one query: the slots this process wrote, and on a
1462
+ * shared store every slot any process wrote with the query's ID (see {@link SetRunQueryResult}).
1463
+ * Slots keyed by the query's name alone, or written before slots were indexed, are not found;
1464
+ * a definition check on read (see {@link GetRunQueryResult}) keeps them from being served.
1465
+ * Never throws.
1466
+ *
1467
+ * @param queryId - The query whose slots to delete
1468
+ * @returns The number of slots deleted
1469
+ */
1470
+ InvalidateRunQueryResultsForQuery(queryId: string): Promise<number>;
1471
+ /** The keys of one query's RunQuery slots: this process's registry, plus the shared index. */
1472
+ private findRunQueryKeysForQuery;
1070
1473
  /**
1071
1474
  * Gets the cache status (fingerprint data) for a RunQuery result.
1072
1475
  * Used for smart cache validation with the server.
1073
1476
  *
1074
1477
  * @param fingerprint - The cache fingerprint
1075
- * @returns The cache status with maxUpdatedAt and rowCount, or null if not found/expired
1478
+ * @returns The cache status with maxUpdatedAt, rowCount and the definition hash the slot was
1479
+ * written under, or null if not found/expired
1076
1480
  */
1077
1481
  GetRunQueryCacheStatus(fingerprint: string): Promise<{
1078
1482
  maxUpdatedAt: string;
1079
1483
  rowCount: number;
1484
+ definitionHash?: string;
1080
1485
  } | null>;
1081
1486
  /**
1082
1487
  * Returns all cache entries for dashboard display.
@@ -1133,6 +1538,14 @@ export declare class LocalCacheManager extends BaseSingleton<LocalCacheManager>
1133
1538
  * Records an access to a cache entry (updates lastAccessedAt and accessCount).
1134
1539
  */
1135
1540
  private recordAccess;
1541
+ /**
1542
+ * True when the registry must stay in this process: the storage is shared with other
1543
+ * processes, so a persisted registry would be one key every server overwrites with its own
1544
+ * view (and publishes to every peer on each write), and a booting server would adopt another
1545
+ * server's accounting. Entries written by other processes are found through the provider's
1546
+ * per-entity index instead ({@link resolveFingerprintsForEntity}).
1547
+ */
1548
+ private registryIsProcessLocalOnly;
1136
1549
  /**
1137
1550
  * Loads the registry from storage.
1138
1551
  */
@@ -1164,16 +1577,50 @@ export declare class LocalCacheManager extends BaseSingleton<LocalCacheManager>
1164
1577
  */
1165
1578
  private estimateResultsSize;
1166
1579
  /**
1167
- * Returns true when a single entry of the given estimated size exceeds the
1168
- * per-entry cap (maxEntryPercentOfCache of maxSizeBytes) and must not be
1169
- * cached. See the config property's doc comment for the full rationale —
1170
- * in short, an entry that large can only be stored by evicting most (or
1171
- * all) of the cache, and it would be evicted again on the next store, so
1172
- * caching it is strictly worse than skipping it.
1580
+ * The largest single entry this cache will store, in bytes, or 0 when no ceiling
1581
+ * applies (`maxEntryPercentOfCache: 0`).
1582
+ *
1583
+ * `'auto'` derives it from the budget — the whole budget less
1584
+ * {@link SINGLE_ENTRY_RESERVE_PERCENT} — so the ceiling grows with the memory the
1585
+ * operator gave the cache instead of being a small fixed fraction that the biggest
1586
+ * entity can never fit under. An explicit percentage is honoured as a stricter
1587
+ * ceiling, clamped to {@link HARD_MAX_ENTRY_PERCENT} so no configuration can admit an
1588
+ * entry that leaves the cache with less free space than one eviction pass needs.
1589
+ */
1590
+ get MaxEntrySizeBytes(): number;
1591
+ /**
1592
+ * The per-entry ceiling as a percentage of the budget, with `'auto'` resolved and an
1593
+ * explicit configuration clamped. 0 or below means "no ceiling".
1594
+ */
1595
+ private get effectiveMaxEntryPercent();
1596
+ /**
1597
+ * Returns true when a single entry of the given estimated size exceeds
1598
+ * {@link MaxEntrySizeBytes} and must not be cached. See the config property's doc
1599
+ * comment for the rationale — in short, an entry that cannot be RETAINED within the
1600
+ * budget would be evicted again on the next store, so caching it is churn. An entry
1601
+ * that fits is cached however large it is: the cases that most need caching are the
1602
+ * large ones.
1173
1603
  */
1174
1604
  private exceedsMaxEntrySize;
1605
+ /**
1606
+ * Renders the decline of an oversized entry as something an operator can act on: what
1607
+ * the ceiling is, and the `cacheSettings.maxMemoryMB` that would admit this entry.
1608
+ * The old message reported raw byte counts and a percentage, which named the symptom
1609
+ * but not the lever — and the entries it declines are exactly the expensive queries
1610
+ * whose absence from the cache costs the most.
1611
+ */
1612
+ private describeOversizedEntry;
1175
1613
  /**
1176
1614
  * Evicts entries if needed to make room for new data.
1615
+ *
1616
+ * Frees the DEFICIT — how far over budget the cache would be with the incoming entry —
1617
+ * rounded up to {@link EVICTION_THRASH_FLOOR_PERCENT} of the budget so a run of stores
1618
+ * near the budget does not evict on every one. It deliberately does not free the
1619
+ * incoming entry's whole size: for a large entry arriving into a mostly empty cache the
1620
+ * deficit is small, and freeing the entry's full size instead emptied the cache to make
1621
+ * room that was already there. That over-eviction is what made a large entry look
1622
+ * inherently destructive, and it is why the per-entry ceiling used to be set low enough
1623
+ * to exclude every large entry.
1177
1624
  */
1178
1625
  private evictIfNeeded;
1179
1626
  /**
@@ -1211,5 +1658,7 @@ export declare class LocalCacheManager extends BaseSingleton<LocalCacheManager>
1211
1658
  * or entries for entities that are over their per-entity cap.
1212
1659
  */
1213
1660
  private runEvictionSweep;
1661
+ /** Drops one entry from the registry and the fingerprint index, leaving the store alone. @internal */
1662
+ private forgetRegistryEntry;
1214
1663
  }
1215
1664
  //# sourceMappingURL=localCacheManager.d.ts.map