@memberjunction/core 6.1.5 → 6.1.6
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.
- package/dist/generic/baseEngine.d.ts +21 -0
- package/dist/generic/baseEngine.d.ts.map +1 -1
- package/dist/generic/baseEngine.js +60 -5
- package/dist/generic/baseEngine.js.map +1 -1
- package/dist/generic/baseEntity.d.ts +2 -1
- package/dist/generic/baseEntity.d.ts.map +1 -1
- package/dist/generic/baseEntity.js +3 -4
- package/dist/generic/baseEntity.js.map +1 -1
- package/dist/generic/databaseProviderBase.d.ts +41 -8
- package/dist/generic/databaseProviderBase.d.ts.map +1 -1
- package/dist/generic/databaseProviderBase.js +107 -42
- package/dist/generic/databaseProviderBase.js.map +1 -1
- package/dist/generic/entityRecordNameCache.d.ts +48 -0
- package/dist/generic/entityRecordNameCache.d.ts.map +1 -0
- package/dist/generic/entityRecordNameCache.js +92 -0
- package/dist/generic/entityRecordNameCache.js.map +1 -0
- package/dist/generic/interfaces.d.ts +39 -4
- package/dist/generic/interfaces.d.ts.map +1 -1
- package/dist/generic/interfaces.js.map +1 -1
- package/dist/generic/localCacheManager.d.ts +105 -11
- package/dist/generic/localCacheManager.d.ts.map +1 -1
- package/dist/generic/localCacheManager.js +221 -40
- package/dist/generic/localCacheManager.js.map +1 -1
- package/dist/generic/providerBase.d.ts +18 -22
- package/dist/generic/providerBase.d.ts.map +1 -1
- package/dist/generic/providerBase.js +26 -103
- package/dist/generic/providerBase.js.map +1 -1
- package/dist/generic/queryInfo.d.ts +14 -2
- package/dist/generic/queryInfo.d.ts.map +1 -1
- package/dist/generic/queryInfo.js +18 -5
- package/dist/generic/queryInfo.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -118,6 +118,11 @@ export class LocalCacheManager extends BaseSingleton {
|
|
|
118
118
|
static get Instance() {
|
|
119
119
|
return super.getInstance();
|
|
120
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* How often, at most, a process writes an entity's shared slots while it keeps finding that
|
|
123
|
+
* entity's slots under a schema different from its own. See {@link handleSchemaMismatch}.
|
|
124
|
+
*/
|
|
125
|
+
static { this.SCHEMA_MISMATCH_WRITE_INTERVAL_MS = 60_000; }
|
|
121
126
|
constructor() {
|
|
122
127
|
super();
|
|
123
128
|
this._storageProvider = null;
|
|
@@ -139,6 +144,14 @@ export class LocalCacheManager extends BaseSingleton {
|
|
|
139
144
|
* (e.g., TransactionGroup batch deletes).
|
|
140
145
|
*/
|
|
141
146
|
this._fingerprintLocks = new Map();
|
|
147
|
+
/**
|
|
148
|
+
* Write holds on entities whose shared slots were found under a different schema, keyed by
|
|
149
|
+
* lowercased entity name. While a hold applies, {@link SetRunViewResult} skips or rate-limits
|
|
150
|
+
* writes of that entity's slots. See {@link handleSchemaMismatch} and {@link mayWriteUnderSchemaHold}.
|
|
151
|
+
*/
|
|
152
|
+
this._schemaWriteHolds = new Map();
|
|
153
|
+
/** Schema mismatches already reported, so each entity/hash pair is logged once per process. */
|
|
154
|
+
this._reportedSchemaMismatches = new Set();
|
|
142
155
|
this.REGISTRY_KEY = '__MJ_CACHE_REGISTRY__';
|
|
143
156
|
/**
|
|
144
157
|
* Reverse index from entity name to the set of RunView cache fingerprints
|
|
@@ -540,30 +553,134 @@ export class LocalCacheManager extends BaseSingleton {
|
|
|
540
553
|
return Number.isFinite(startRow) && startRow > 0;
|
|
541
554
|
}
|
|
542
555
|
/**
|
|
543
|
-
*
|
|
544
|
-
* (
|
|
545
|
-
*
|
|
556
|
+
* Compares a cached RunView entry's schema hash against this process's current field list for
|
|
557
|
+
* the entity (a mismatch follows a migration + CodeGen that added, removed or reordered
|
|
558
|
+
* columns). Entries without a hash, and entities this process cannot resolve, never mismatch.
|
|
546
559
|
* @param fingerprint - The cache fingerprint
|
|
547
560
|
* @param data - The cached data to validate
|
|
548
|
-
* @returns
|
|
561
|
+
* @returns The mismatch, or null when the entry may be served
|
|
549
562
|
*/
|
|
550
|
-
|
|
563
|
+
findSchemaMismatch(fingerprint, data) {
|
|
551
564
|
if (!data.schemaHash)
|
|
552
|
-
return
|
|
565
|
+
return null;
|
|
553
566
|
const entityName = this.extractEntityFromFingerprint(fingerprint);
|
|
554
567
|
if (!entityName)
|
|
568
|
+
return null;
|
|
569
|
+
const current = this.computeSchemaSignature(undefined, entityName);
|
|
570
|
+
if (!current || current.Hash === data.schemaHash)
|
|
571
|
+
return null;
|
|
572
|
+
return {
|
|
573
|
+
EntityName: entityName,
|
|
574
|
+
CachedHash: data.schemaHash,
|
|
575
|
+
Current: current,
|
|
576
|
+
EntryIsNewer: LocalCacheManager.isNewerSchemaStamp(data.schemaStamp, current.Stamp),
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* True only when both stamps are present and the cached entry's is strictly newer than this
|
|
581
|
+
* process's. Equal or missing stamps leave the order unknown, which counts as not newer.
|
|
582
|
+
*/
|
|
583
|
+
static isNewerSchemaStamp(entryStamp, currentStamp) {
|
|
584
|
+
if (!entryStamp || !currentStamp) {
|
|
555
585
|
return false;
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
586
|
+
}
|
|
587
|
+
return LocalCacheManager.timestampMs(entryStamp) > LocalCacheManager.timestampMs(currentStamp);
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Decides what a schema mismatch does to the slot. The entry is never served; the question is
|
|
591
|
+
* whether the slot is deleted, which on a shared store also tells every peer to reload.
|
|
592
|
+
*
|
|
593
|
+
* - Process-local store, or a slot-maintenance read: delete it. No other process reads a local
|
|
594
|
+
* store. A maintenance read happens because a row changed, and a slot that cannot take the
|
|
595
|
+
* change in place must go, whichever process wrote it.
|
|
596
|
+
* - Shared store, serving read: leave it, and hold back this process's writes of the entity.
|
|
597
|
+
* Deleting publishes `removed`, every peer engine holding the slot reloads and rewrites it
|
|
598
|
+
* under its own hash, and processes on either side of a migration keep doing that to each
|
|
599
|
+
* other. The caller reads the database instead.
|
|
600
|
+
* - When the slot is known to come from newer metadata, this process is the one out of date.
|
|
601
|
+
* It stops writing the entity's slots until its own metadata changes.
|
|
602
|
+
* - Otherwise it writes the entity's slots at most once per
|
|
603
|
+
* {@link SCHEMA_MISMATCH_WRITE_INTERVAL_MS} while mismatches keep appearing. That still
|
|
604
|
+
* replaces a slot left by older metadata, but a peer running code that deletes mismatched
|
|
605
|
+
* slots (any release before this check stopped deleting them) gets one slot to delete per
|
|
606
|
+
* interval instead of one per read. The hold lapses after an interval with no mismatch.
|
|
607
|
+
*/
|
|
608
|
+
handleSchemaMismatch(fingerprint, mismatch, purpose) {
|
|
609
|
+
const shared = this._storageProvider?.SharedAcrossProcesses === true;
|
|
610
|
+
if (!shared || purpose === 'maintain') {
|
|
611
|
+
this.reportSchemaMismatch(mismatch, 'dropping the slot');
|
|
612
|
+
this.InvalidateRunViewResult(fingerprint).catch(() => { });
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
this.holdSchemaWrites(mismatch);
|
|
616
|
+
if (mismatch.EntryIsNewer) {
|
|
617
|
+
this.reportSchemaMismatch(mismatch, 'another process wrote the slot under newer metadata. This process reads the entity from the database ' +
|
|
618
|
+
'and stops writing its cache slots until its metadata changes. If it started before a migration, restart it.');
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
this.reportSchemaMismatch(mismatch, `reading from the database. While slots under other metadata keep appearing, this process writes the entity's ` +
|
|
622
|
+
`slots at most once every ${LocalCacheManager.SCHEMA_MISMATCH_WRITE_INTERVAL_MS / 1000}s.`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/** Records or extends the write hold for a mismatch seen on a shared store. */
|
|
626
|
+
holdSchemaWrites(mismatch) {
|
|
627
|
+
const key = mismatch.EntityName.trim().toLowerCase();
|
|
628
|
+
const now = Date.now();
|
|
629
|
+
const existing = this._schemaWriteHolds.get(key);
|
|
630
|
+
if (mismatch.EntryIsNewer) {
|
|
631
|
+
this._schemaWriteHolds.set(key, { OwnHash: mismatch.Current.Hash, NextWriteAt: Number.POSITIVE_INFINITY, LastMismatchAt: now });
|
|
632
|
+
}
|
|
633
|
+
else if (existing && existing.OwnHash === mismatch.Current.Hash) {
|
|
634
|
+
existing.LastMismatchAt = now;
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
this._schemaWriteHolds.set(key, {
|
|
638
|
+
OwnHash: mismatch.Current.Hash,
|
|
639
|
+
NextWriteAt: now + LocalCacheManager.SCHEMA_MISMATCH_WRITE_INTERVAL_MS,
|
|
640
|
+
LastMismatchAt: now,
|
|
563
641
|
});
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
/** Logs a schema mismatch once per entity and hash pair per process. */
|
|
645
|
+
reportSchemaMismatch(mismatch, outcome) {
|
|
646
|
+
const key = `${mismatch.EntityName.trim().toLowerCase()}|${mismatch.CachedHash}|${mismatch.Current.Hash}`;
|
|
647
|
+
if (this._reportedSchemaMismatches.has(key)) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
this._reportedSchemaMismatches.add(key);
|
|
651
|
+
LogStatusEx({
|
|
652
|
+
message: `[CACHE-SCHEMA-STALE] Entity "${mismatch.EntityName}" schema differs from a cached slot ` +
|
|
653
|
+
`(cached=${mismatch.CachedHash}, current=${mismatch.Current.Hash}) — ${outcome}`,
|
|
654
|
+
verboseOnly: false
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Whether a slot of `entityName` may be written now, given the entity's write hold (see
|
|
659
|
+
* {@link handleSchemaMismatch}). A permitted write under a rate-limiting hold uses up the
|
|
660
|
+
* interval. The hold is dropped once this process's schema hash changes (a metadata refresh)
|
|
661
|
+
* or, for a rate-limiting hold, once an interval passes with no mismatch.
|
|
662
|
+
*/
|
|
663
|
+
mayWriteUnderSchemaHold(entityName, currentHash) {
|
|
664
|
+
if (this._storageProvider?.SharedAcrossProcesses !== true) {
|
|
564
665
|
return true;
|
|
565
666
|
}
|
|
566
|
-
|
|
667
|
+
const key = entityName.trim().toLowerCase();
|
|
668
|
+
const hold = this._schemaWriteHolds.get(key);
|
|
669
|
+
if (!hold) {
|
|
670
|
+
return true;
|
|
671
|
+
}
|
|
672
|
+
const now = Date.now();
|
|
673
|
+
const interval = LocalCacheManager.SCHEMA_MISMATCH_WRITE_INTERVAL_MS;
|
|
674
|
+
const lapsed = hold.NextWriteAt !== Number.POSITIVE_INFINITY && now - hold.LastMismatchAt > interval;
|
|
675
|
+
if (hold.OwnHash !== currentHash || lapsed) {
|
|
676
|
+
this._schemaWriteHolds.delete(key);
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
679
|
+
if (now < hold.NextWriteAt) {
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
hold.NextWriteAt = now + interval;
|
|
683
|
+
return true;
|
|
567
684
|
}
|
|
568
685
|
/**
|
|
569
686
|
* Adds a fingerprint to the entity→fingerprint reverse index.
|
|
@@ -602,21 +719,26 @@ export class LocalCacheManager extends BaseSingleton {
|
|
|
602
719
|
return this._entityFingerprintIndex.get(entityName) ?? new Set();
|
|
603
720
|
}
|
|
604
721
|
/**
|
|
605
|
-
* Resolves cached fingerprints for an entity,
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
* local index
|
|
722
|
+
* Resolves cached fingerprints for an entity: this process's own index, plus the slots the
|
|
723
|
+
* storage provider lists for the entity.
|
|
724
|
+
*
|
|
725
|
+
* On a provider shared between processes (e.g. Redis) the provider is listed on every call
|
|
726
|
+
* and merged into the local index. The local index only knows slots this process wrote, so
|
|
727
|
+
* answering from it alone would miss slots other processes wrote — including a slot this
|
|
728
|
+
* process only read warm at boot — and a save here would never reach the engines that load
|
|
729
|
+
* from those slots on its peers.
|
|
730
|
+
*
|
|
731
|
+
* On a process-local provider (e.g. IndexedDB) the category is listed only when the local
|
|
732
|
+
* index knows nothing for the entity, which picks up slots persisted by an earlier session.
|
|
610
733
|
*/
|
|
611
734
|
async resolveFingerprintsForEntity(entityName) {
|
|
735
|
+
if (this._storageProvider?.SharedAcrossProcesses === true && this._storageProvider.GetCategoryKeys) {
|
|
736
|
+
return this.resolveFingerprintsFromSharedStore(entityName);
|
|
737
|
+
}
|
|
612
738
|
const local = this._entityFingerprintIndex.get(entityName);
|
|
613
739
|
if (local && local.size > 0)
|
|
614
740
|
return local;
|
|
615
|
-
|
|
616
|
-
return undefined;
|
|
617
|
-
const allKeys = await this._storageProvider.GetCategoryKeys(CacheCategory.RunViewCache);
|
|
618
|
-
const entityPrefix = entityName + '|';
|
|
619
|
-
const remoteFingerprints = allKeys.filter(k => k.startsWith(entityPrefix));
|
|
741
|
+
const remoteFingerprints = await this.listStoredFingerprintsForEntity(entityName);
|
|
620
742
|
if (remoteFingerprints.length > 0) {
|
|
621
743
|
LogStatusVerbose(`LocalCacheManager: found ${remoteFingerprints.length} remote cached fingerprint(s) for "${entityName}" via storage provider`);
|
|
622
744
|
const result = new Set(remoteFingerprints);
|
|
@@ -628,6 +750,26 @@ export class LocalCacheManager extends BaseSingleton {
|
|
|
628
750
|
}
|
|
629
751
|
return undefined;
|
|
630
752
|
}
|
|
753
|
+
/**
|
|
754
|
+
* Merges the slots a shared store lists for the entity into the local index and returns the
|
|
755
|
+
* union. Local entries the store does not list are left in place; invalidation removes them
|
|
756
|
+
* from the index the same way it does for any other slot.
|
|
757
|
+
*/
|
|
758
|
+
async resolveFingerprintsFromSharedStore(entityName) {
|
|
759
|
+
for (const fp of await this.listStoredFingerprintsForEntity(entityName)) {
|
|
760
|
+
this.addToEntityIndex(fp);
|
|
761
|
+
}
|
|
762
|
+
const merged = this._entityFingerprintIndex.get(entityName);
|
|
763
|
+
return merged && merged.size > 0 ? merged : undefined;
|
|
764
|
+
}
|
|
765
|
+
/** Lists the RunView slots the storage provider holds for an entity; empty when it cannot list keys. */
|
|
766
|
+
async listStoredFingerprintsForEntity(entityName) {
|
|
767
|
+
if (!this._storageProvider?.GetCategoryKeys)
|
|
768
|
+
return [];
|
|
769
|
+
const allKeys = await this._storageProvider.GetCategoryKeys(CacheCategory.RunViewCache);
|
|
770
|
+
const entityPrefix = entityName + '|';
|
|
771
|
+
return allKeys.filter(k => k.startsWith(entityPrefix));
|
|
772
|
+
}
|
|
631
773
|
// ========================================================================
|
|
632
774
|
// UNIVERSAL CACHE INVALIDATION (BaseEntity Events)
|
|
633
775
|
// ========================================================================
|
|
@@ -1446,6 +1588,14 @@ export class LocalCacheManager extends BaseSingleton {
|
|
|
1446
1588
|
* @returns The schema hash string, or undefined if the entity can't be resolved
|
|
1447
1589
|
*/
|
|
1448
1590
|
ComputeSchemaHash(provider, entityName) {
|
|
1591
|
+
return this.computeSchemaSignature(provider, entityName)?.Hash;
|
|
1592
|
+
}
|
|
1593
|
+
/**
|
|
1594
|
+
* The entity's schema hash (see {@link ComputeSchemaHash}) together with its schema stamp: the
|
|
1595
|
+
* newest `__mj_UpdatedAt` across the entity row and its field rows, as an ISO string, or
|
|
1596
|
+
* undefined when the metadata carries no timestamps.
|
|
1597
|
+
*/
|
|
1598
|
+
computeSchemaSignature(provider, entityName) {
|
|
1449
1599
|
try {
|
|
1450
1600
|
const md = provider ?? new Metadata();
|
|
1451
1601
|
const entity = md.EntityByName(entityName);
|
|
@@ -1454,12 +1604,27 @@ export class LocalCacheManager extends BaseSingleton {
|
|
|
1454
1604
|
// Use natural sequence order (EntityInfo.Fields is sorted by Sequence).
|
|
1455
1605
|
// This detects field additions, removals, AND reorderings.
|
|
1456
1606
|
const fieldNames = entity.Fields.map(f => f.Name).join('|');
|
|
1457
|
-
|
|
1607
|
+
let newest = LocalCacheManager.timestampMs(entity.__mj_UpdatedAt);
|
|
1608
|
+
for (const field of entity.Fields) {
|
|
1609
|
+
newest = Math.max(newest, LocalCacheManager.timestampMs(field.__mj_UpdatedAt));
|
|
1610
|
+
}
|
|
1611
|
+
return {
|
|
1612
|
+
Hash: this.simpleHash(fieldNames),
|
|
1613
|
+
Stamp: newest > 0 ? new Date(newest).toISOString() : undefined,
|
|
1614
|
+
};
|
|
1458
1615
|
}
|
|
1459
1616
|
catch {
|
|
1460
1617
|
return undefined;
|
|
1461
1618
|
}
|
|
1462
1619
|
}
|
|
1620
|
+
/** Milliseconds since the epoch for a metadata timestamp (a Date or a string), or 0 when unreadable. */
|
|
1621
|
+
static timestampMs(value) {
|
|
1622
|
+
if (value == null) {
|
|
1623
|
+
return 0;
|
|
1624
|
+
}
|
|
1625
|
+
const ms = value instanceof Date ? value.getTime() : Date.parse(value);
|
|
1626
|
+
return Number.isNaN(ms) ? 0 : ms;
|
|
1627
|
+
}
|
|
1463
1628
|
/**
|
|
1464
1629
|
* Deep-freezes an about-to-be-stored cache payload when — and only when — the active
|
|
1465
1630
|
* storage provider hands out live object references
|
|
@@ -1609,9 +1774,16 @@ export class LocalCacheManager extends BaseSingleton {
|
|
|
1609
1774
|
}
|
|
1610
1775
|
// Compute and store schema hash for upgrade detection
|
|
1611
1776
|
if (params.EntityName) {
|
|
1612
|
-
const
|
|
1613
|
-
if (
|
|
1614
|
-
|
|
1777
|
+
const signature = this.computeSchemaSignature(provider, params.EntityName);
|
|
1778
|
+
if (signature) {
|
|
1779
|
+
if (!this.mayWriteUnderSchemaHold(params.EntityName, signature.Hash)) {
|
|
1780
|
+
LogStatusEx({ message: `[CACHE-WRITE-GATE] Skipping cache write for "${params.EntityName}" — its shared slots were found under different metadata`, verboseOnly: true });
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
data.schemaHash = signature.Hash;
|
|
1784
|
+
if (signature.Stamp) {
|
|
1785
|
+
data.schemaStamp = signature.Stamp;
|
|
1786
|
+
}
|
|
1615
1787
|
}
|
|
1616
1788
|
}
|
|
1617
1789
|
// Estimate size by sampling rows (eviction accounting only); the actual stored
|
|
@@ -1626,9 +1798,9 @@ export class LocalCacheManager extends BaseSingleton {
|
|
|
1626
1798
|
LogStatusEx({ message: `[CACHE-WRITE-GATE] Skipping cache write for "${params.EntityName || fingerprint.substring(0, 60)}" — estimated entry size ${sizeBytes} bytes exceeds per-entry cap (${this._config.maxEntryPercentOfCache}% of ${this._config.maxSizeBytes} byte budget)` });
|
|
1627
1799
|
return;
|
|
1628
1800
|
}
|
|
1629
|
-
// The oversized gate above
|
|
1630
|
-
// synchronous — so by here the row set is definitely becoming shared
|
|
1631
|
-
// frozen with no intervening yield point. That ordering is load-bearing: the eviction
|
|
1801
|
+
// The schema gate and the oversized gate above are the ONLY steps that can decline this
|
|
1802
|
+
// write, and they are synchronous — so by here the row set is definitely becoming shared
|
|
1803
|
+
// state, and it is frozen with no intervening yield point. That ordering is load-bearing: the eviction
|
|
1632
1804
|
// steps below are awaited, and callers hold this very array while they run (the
|
|
1633
1805
|
// smart-cache stale leg does not await this method at all). Freezing after them leaves
|
|
1634
1806
|
// a window in which shared rows are handed out still mutable — and `BaseEntity`
|
|
@@ -1685,12 +1857,16 @@ export class LocalCacheManager extends BaseSingleton {
|
|
|
1685
1857
|
* @returns The cached results, maxUpdatedAt, rowCount (derived), and aggregateResults, or null if not found
|
|
1686
1858
|
*/
|
|
1687
1859
|
async GetRunViewResult(fingerprint) {
|
|
1860
|
+
return this.readRunViewSlot(fingerprint, 'serve');
|
|
1861
|
+
}
|
|
1862
|
+
/** {@link GetRunViewResult} for a given purpose; in-place slot maintenance reads with `'maintain'`. */
|
|
1863
|
+
async readRunViewSlot(fingerprint, purpose) {
|
|
1688
1864
|
if (!this._storageProvider || !this._config.enabled)
|
|
1689
1865
|
return null;
|
|
1690
1866
|
try {
|
|
1691
1867
|
// Native object read — IDB structured-clones the result back, no JSON.parse needed.
|
|
1692
1868
|
const parsed = await this._storageProvider.GetItem(fingerprint, CacheCategory.RunViewCache);
|
|
1693
|
-
return this.materializeCachedRunViewResult(fingerprint, parsed);
|
|
1869
|
+
return this.materializeCachedRunViewResult(fingerprint, parsed, purpose);
|
|
1694
1870
|
}
|
|
1695
1871
|
catch (e) {
|
|
1696
1872
|
LogError(`LocalCacheManager.GetRunViewResult failed: ${e}`);
|
|
@@ -1730,7 +1906,7 @@ export class LocalCacheManager extends BaseSingleton {
|
|
|
1730
1906
|
try {
|
|
1731
1907
|
const raw = await this._storageProvider.GetItems(fingerprints, CacheCategory.RunViewCache);
|
|
1732
1908
|
for (const [fp, parsed] of raw) {
|
|
1733
|
-
out.set(fp, this.materializeCachedRunViewResult(fp, parsed));
|
|
1909
|
+
out.set(fp, this.materializeCachedRunViewResult(fp, parsed, 'serve'));
|
|
1734
1910
|
}
|
|
1735
1911
|
return out;
|
|
1736
1912
|
}
|
|
@@ -1753,7 +1929,7 @@ export class LocalCacheManager extends BaseSingleton {
|
|
|
1753
1929
|
* cache entries after schema migrations. If the entity's field list changed
|
|
1754
1930
|
* since the entry was cached, the entry is invalidated and null is returned.
|
|
1755
1931
|
*/
|
|
1756
|
-
materializeCachedRunViewResult(fingerprint, parsed) {
|
|
1932
|
+
materializeCachedRunViewResult(fingerprint, parsed, purpose) {
|
|
1757
1933
|
if (!parsed) {
|
|
1758
1934
|
this._stats.misses++;
|
|
1759
1935
|
return null;
|
|
@@ -1767,8 +1943,9 @@ export class LocalCacheManager extends BaseSingleton {
|
|
|
1767
1943
|
this._stats.misses++;
|
|
1768
1944
|
return null;
|
|
1769
1945
|
}
|
|
1770
|
-
|
|
1771
|
-
|
|
1946
|
+
const schemaMismatch = this.findSchemaMismatch(fingerprint, parsed);
|
|
1947
|
+
if (schemaMismatch) {
|
|
1948
|
+
this.handleSchemaMismatch(fingerprint, schemaMismatch, purpose);
|
|
1772
1949
|
this._stats.misses++;
|
|
1773
1950
|
return null;
|
|
1774
1951
|
}
|
|
@@ -1782,6 +1959,7 @@ export class LocalCacheManager extends BaseSingleton {
|
|
|
1782
1959
|
totalRowCount: parsed.totalRowCount,
|
|
1783
1960
|
// Surfaced so in-place maintenance can carry it forward on rewrite (B38).
|
|
1784
1961
|
schemaHash: parsed.schemaHash,
|
|
1962
|
+
schemaStamp: parsed.schemaStamp,
|
|
1785
1963
|
// Same carry-forward rationale: a maintained scaffolding slot must stay exempt.
|
|
1786
1964
|
providerInternalScaffolding: parsed.providerInternalScaffolding,
|
|
1787
1965
|
};
|
|
@@ -1978,7 +2156,7 @@ export class LocalCacheManager extends BaseSingleton {
|
|
|
1978
2156
|
return false;
|
|
1979
2157
|
return this.withFingerprintLock(fingerprint, async () => {
|
|
1980
2158
|
try {
|
|
1981
|
-
const cached = await this.
|
|
2159
|
+
const cached = await this.readRunViewSlot(fingerprint, 'maintain');
|
|
1982
2160
|
if (!cached) {
|
|
1983
2161
|
LogStatusVerbose(`LocalCacheManager.UpsertSingleEntity: No cached data found for fingerprint "${fingerprint.substring(0, 60)}" — skipping (cache will be populated on next RunView)`);
|
|
1984
2162
|
return false;
|
|
@@ -1999,7 +2177,7 @@ export class LocalCacheManager extends BaseSingleton {
|
|
|
1999
2177
|
// Upsert the entity (add or replace)
|
|
2000
2178
|
resultMap.set(keyStr, entityData);
|
|
2001
2179
|
const updatedResults = Array.from(resultMap.values());
|
|
2002
|
-
return await this.storeCachedResults(fingerprint, updatedResults, newMaxUpdatedAt, { totalRowCount: cached.totalRowCount, rowCount: cached.results.length, schemaHash: cached.schemaHash,
|
|
2180
|
+
return await this.storeCachedResults(fingerprint, updatedResults, newMaxUpdatedAt, { totalRowCount: cached.totalRowCount, rowCount: cached.results.length, schemaHash: cached.schemaHash, schemaStamp: cached.schemaStamp,
|
|
2003
2181
|
providerInternalScaffolding: cached.providerInternalScaffolding });
|
|
2004
2182
|
}
|
|
2005
2183
|
catch (e) {
|
|
@@ -2022,7 +2200,7 @@ export class LocalCacheManager extends BaseSingleton {
|
|
|
2022
2200
|
return false;
|
|
2023
2201
|
return this.withFingerprintLock(fingerprint, async () => {
|
|
2024
2202
|
try {
|
|
2025
|
-
const cached = await this.
|
|
2203
|
+
const cached = await this.readRunViewSlot(fingerprint, 'maintain');
|
|
2026
2204
|
if (!cached) {
|
|
2027
2205
|
return false;
|
|
2028
2206
|
}
|
|
@@ -2042,7 +2220,7 @@ export class LocalCacheManager extends BaseSingleton {
|
|
|
2042
2220
|
}
|
|
2043
2221
|
resultMap.delete(keyStr);
|
|
2044
2222
|
const updatedResults = Array.from(resultMap.values());
|
|
2045
|
-
return await this.storeCachedResults(fingerprint, updatedResults, newMaxUpdatedAt, { totalRowCount: cached.totalRowCount, rowCount: cached.results.length, schemaHash: cached.schemaHash,
|
|
2223
|
+
return await this.storeCachedResults(fingerprint, updatedResults, newMaxUpdatedAt, { totalRowCount: cached.totalRowCount, rowCount: cached.results.length, schemaHash: cached.schemaHash, schemaStamp: cached.schemaStamp,
|
|
2046
2224
|
providerInternalScaffolding: cached.providerInternalScaffolding });
|
|
2047
2225
|
}
|
|
2048
2226
|
catch (e) {
|
|
@@ -2084,6 +2262,9 @@ export class LocalCacheManager extends BaseSingleton {
|
|
|
2084
2262
|
if (prior?.schemaHash) {
|
|
2085
2263
|
data.schemaHash = prior.schemaHash;
|
|
2086
2264
|
}
|
|
2265
|
+
if (prior?.schemaStamp) {
|
|
2266
|
+
data.schemaStamp = prior.schemaStamp;
|
|
2267
|
+
}
|
|
2087
2268
|
// Carry the freeze exemption forward too — a scaffolding slot that gets maintained in
|
|
2088
2269
|
// place must not silently become frozen, or the owner that mutates those rows breaks on
|
|
2089
2270
|
// the next read (for the metadata dataset that means booting with no metadata).
|