@memberjunction/core 6.2.0-edge.2 → 6.2.0-edge.3
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/InMemoryLocalStorageProvider.d.ts +5 -0
- package/dist/generic/InMemoryLocalStorageProvider.d.ts.map +1 -1
- package/dist/generic/InMemoryLocalStorageProvider.js +5 -0
- package/dist/generic/InMemoryLocalStorageProvider.js.map +1 -1
- package/dist/generic/JSONType-interfaces/IEntityConfiguration.d.ts +12 -0
- package/dist/generic/JSONType-interfaces/IEntityConfiguration.d.ts.map +1 -1
- package/dist/generic/JSONType-interfaces/IEntityRelationshipConfiguration.d.ts +19 -0
- package/dist/generic/JSONType-interfaces/IEntityRelationshipConfiguration.d.ts.map +1 -1
- package/dist/generic/baseEngine.d.ts +21 -1
- package/dist/generic/baseEngine.d.ts.map +1 -1
- package/dist/generic/baseEngine.js +15 -1
- package/dist/generic/baseEngine.js.map +1 -1
- package/dist/generic/baseEntity.d.ts +91 -7
- package/dist/generic/baseEntity.d.ts.map +1 -1
- package/dist/generic/baseEntity.js +161 -16
- package/dist/generic/baseEntity.js.map +1 -1
- package/dist/generic/databaseProviderBase.d.ts +34 -0
- package/dist/generic/databaseProviderBase.d.ts.map +1 -1
- package/dist/generic/databaseProviderBase.js +88 -20
- package/dist/generic/databaseProviderBase.js.map +1 -1
- package/dist/generic/entityConfiguration.d.ts +20 -0
- package/dist/generic/entityConfiguration.d.ts.map +1 -1
- package/dist/generic/entityConfiguration.js +34 -0
- package/dist/generic/entityConfiguration.js.map +1 -1
- package/dist/generic/entityInfo.d.ts +32 -1
- package/dist/generic/entityInfo.d.ts.map +1 -1
- package/dist/generic/entityInfo.js +44 -2
- package/dist/generic/entityInfo.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 +33 -6
- package/dist/generic/interfaces.d.ts.map +1 -1
- package/dist/generic/interfaces.js.map +1 -1
- package/dist/generic/jsonFieldBinding.d.ts +196 -0
- package/dist/generic/jsonFieldBinding.d.ts.map +1 -0
- package/dist/generic/jsonFieldBinding.js +508 -0
- package/dist/generic/jsonFieldBinding.js.map +1 -0
- package/dist/generic/localCacheManager.d.ts +11 -0
- package/dist/generic/localCacheManager.d.ts.map +1 -1
- package/dist/generic/localCacheManager.js +9 -0
- package/dist/generic/localCacheManager.js.map +1 -1
- package/dist/generic/providerBase.d.ts +68 -32
- package/dist/generic/providerBase.d.ts.map +1 -1
- package/dist/generic/providerBase.js +150 -120
- package/dist/generic/providerBase.js.map +1 -1
- package/dist/generic/runQuery.d.ts +18 -1
- package/dist/generic/runQuery.d.ts.map +1 -1
- package/dist/generic/runQuery.js.map +1 -1
- package/dist/generic/runQuerySQLFilterImplementations.d.ts +7 -1
- package/dist/generic/runQuerySQLFilterImplementations.d.ts.map +1 -1
- package/dist/generic/runQuerySQLFilterImplementations.js +65 -140
- package/dist/generic/runQuerySQLFilterImplementations.js.map +1 -1
- package/dist/generic/telemetryManager.d.ts +11 -0
- package/dist/generic/telemetryManager.d.ts.map +1 -1
- package/dist/generic/telemetryManager.js +40 -0
- package/dist/generic/telemetryManager.js.map +1 -1
- package/dist/generic/util.d.ts +6 -0
- package/dist/generic/util.d.ts.map +1 -1
- package/dist/generic/util.js +20 -0
- package/dist/generic/util.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/views/runView.d.ts +24 -0
- package/dist/views/runView.d.ts.map +1 -1
- package/dist/views/runView.js +4 -0
- package/dist/views/runView.js.map +1 -1
- package/package.json +3 -3
- package/readme.md +29 -2
|
@@ -6,7 +6,7 @@ import { ComputeRRF } from "./scoring/ReciprocalRankFusion.js";
|
|
|
6
6
|
import { LocalCacheManager } from "./localCacheManager.js";
|
|
7
7
|
import { ApplicationInfo } from "../generic/applicationInfo.js";
|
|
8
8
|
import { AuditLogTypeInfo, AuthorizationInfo, AuthorizationRoleInfo, RoleInfo, RowLevelSecurityFilterInfo, UserInfo } from "./securityInfo.js";
|
|
9
|
-
import { MJGlobal, MJEventType, NormalizeUUID, UUIDsEqual,
|
|
9
|
+
import { MJGlobal, MJEventType, NormalizeUUID, UUIDsEqual, EscapeSQLString, ordinalCompare } from "@memberjunction/global";
|
|
10
10
|
import { FindReferencedIdentifiers } from "@memberjunction/sql-dialect";
|
|
11
11
|
import { TelemetryManager } from "./telemetryManager.js";
|
|
12
12
|
import { LogDebug, LogError, LogStatus, LogStatusEx } from "./logging.js";
|
|
@@ -186,10 +186,6 @@ export class ProviderBase {
|
|
|
186
186
|
this._localMetadata = new AllMetadata();
|
|
187
187
|
this._entityMapByName = new Map();
|
|
188
188
|
this._entityMapByID = new Map();
|
|
189
|
-
// Bounded LRU (unlike its siblings above, this cache holds one entry per distinct
|
|
190
|
-
// *record* touched via Load()/Save()/LoadFromData() — not per entity definition — so
|
|
191
|
-
// it can't be reset on metadata refresh; it needs its own eviction policy.
|
|
192
|
-
this._entityRecordNameCache = new MJLruCache({ maxSize: 10000, ttlMs: 60 * 60 * 1000 });
|
|
193
189
|
this._refresh = false;
|
|
194
190
|
/** Single-flight guard for the full metadata reload in {@link Config} — see the comment there. */
|
|
195
191
|
this._metadataReloadInFlight = null;
|
|
@@ -251,6 +247,8 @@ export class ProviderBase {
|
|
|
251
247
|
this._metadataMemberRefreshWaits = 0;
|
|
252
248
|
this._clientFingerprintMemo = new WeakMap();
|
|
253
249
|
this._cachedVisibleExplorerNavigationItems = null;
|
|
250
|
+
/** Guard for {@link logMetadataSnapshotDisabledOnce}. */
|
|
251
|
+
this._metadataSnapshotSkipLogged = false;
|
|
254
252
|
}
|
|
255
253
|
/**
|
|
256
254
|
* Whether this provider can execute a multi-record unit of work atomically, in-process.
|
|
@@ -672,143 +670,63 @@ export class ProviderBase {
|
|
|
672
670
|
return false;
|
|
673
671
|
}
|
|
674
672
|
/**
|
|
675
|
-
*
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
*
|
|
682
|
-
* Use this for synchronous contexts (like template rendering) where you can't await GetEntityRecordName().
|
|
673
|
+
* Returns a record's name for synchronous display code, or `undefined` when it is not at hand.
|
|
674
|
+
*
|
|
675
|
+
* This provider keeps no record names, so without `loadIfNeeded` the answer is always
|
|
676
|
+
* `undefined`. A provider that serves a single user (`GraphQLDataProvider`) overrides the
|
|
677
|
+
* record-name methods with a cache; one shared by several users must not, since a name can be
|
|
678
|
+
* withheld from some of them.
|
|
679
|
+
*
|
|
683
680
|
* @param entityName - The name of the entity
|
|
684
681
|
* @param compositeKey - The primary key value(s) for the record
|
|
685
|
-
* @param loadIfNeeded -
|
|
686
|
-
* @returns The
|
|
682
|
+
* @param loadIfNeeded - Look the name up when it is not cached
|
|
683
|
+
* @returns The display name, or undefined if not cached and not looked up
|
|
687
684
|
*/
|
|
688
685
|
async GetCachedRecordName(entityName, compositeKey, loadIfNeeded) {
|
|
689
|
-
|
|
690
|
-
if (!cachedEntry && loadIfNeeded) {
|
|
691
|
-
cachedEntry = await this.GetEntityRecordName(entityName, compositeKey);
|
|
692
|
-
}
|
|
693
|
-
return cachedEntry;
|
|
686
|
+
return loadIfNeeded ? this.GetEntityRecordName(entityName, compositeKey) : undefined;
|
|
694
687
|
}
|
|
695
688
|
/**
|
|
696
|
-
*
|
|
697
|
-
* @
|
|
698
|
-
* @param compositeKey - The primary key value(s) for the record
|
|
699
|
-
* @returns True if the record name is cached in memory, false otherwise
|
|
689
|
+
* Whether a record's name is cached for synchronous retrieval. Always false here; see
|
|
690
|
+
* {@link GetCachedRecordName}.
|
|
700
691
|
*/
|
|
701
692
|
HasCachedRecordName(entityName, compositeKey) {
|
|
702
|
-
return
|
|
693
|
+
return false;
|
|
703
694
|
}
|
|
704
695
|
/**
|
|
705
|
-
*
|
|
706
|
-
*
|
|
707
|
-
* @param entityName - The name of the entity
|
|
708
|
-
* @param compositeKey - The primary key value(s) for the record
|
|
709
|
-
* @returns The cached display name, or undefined if not in cache
|
|
696
|
+
* A record's cached name, without ever starting a lookup. Always undefined here; see
|
|
697
|
+
* {@link GetCachedRecordName}.
|
|
710
698
|
*/
|
|
711
699
|
GetCachedRecordNameOnlyIfCached(entityName, compositeKey) {
|
|
712
|
-
return
|
|
700
|
+
return undefined;
|
|
713
701
|
}
|
|
714
702
|
/**
|
|
715
|
-
*
|
|
716
|
-
*
|
|
717
|
-
* @param entityName - The name of the entity
|
|
718
|
-
* @param compositeKey - The primary key value(s) for the record
|
|
719
|
-
* @param recordName - The display name to cache
|
|
703
|
+
* Offers a record's name for later synchronous retrieval. `BaseEntity` calls it after every
|
|
704
|
+
* Load(), LoadFromData() and Save(). Ignored here; see {@link GetCachedRecordName}.
|
|
720
705
|
*/
|
|
721
706
|
SetCachedRecordName(entityName, compositeKey, recordName) {
|
|
722
|
-
this
|
|
707
|
+
// Intentionally empty: this provider keeps no record names.
|
|
723
708
|
}
|
|
724
709
|
/**
|
|
725
|
-
* Gets the display name for a single entity record
|
|
710
|
+
* Gets the display name for a single entity record.
|
|
726
711
|
* Uses the entity's IsNameField or falls back to 'Name' field if available.
|
|
727
712
|
* @param entityName - The name of the entity
|
|
728
713
|
* @param compositeKey - The primary key value(s) for the record
|
|
729
|
-
* @param contextUser -
|
|
730
|
-
* @param forceRefresh -
|
|
731
|
-
* @returns The display name of the record or
|
|
714
|
+
* @param contextUser - The acting user; field- and row-level security are applied for them
|
|
715
|
+
* @param forceRefresh - Bypass any cache. This provider has none, so it always looks up.
|
|
716
|
+
* @returns The display name of the record, or an empty string if not found or not readable
|
|
732
717
|
*/
|
|
733
718
|
async GetEntityRecordName(entityName, compositeKey, contextUser, forceRefresh = false) {
|
|
734
|
-
|
|
735
|
-
// Check cache unless forceRefresh
|
|
736
|
-
if (!forceRefresh) {
|
|
737
|
-
const cached = this._entityRecordNameCache.Get(cacheKey);
|
|
738
|
-
if (cached !== undefined) {
|
|
739
|
-
return cached;
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
// Fetch from database via provider-specific implementation
|
|
743
|
-
const name = await this.InternalGetEntityRecordName(entityName, compositeKey, contextUser);
|
|
744
|
-
if (name) {
|
|
745
|
-
this._entityRecordNameCache.Set(cacheKey, name);
|
|
746
|
-
}
|
|
747
|
-
return name;
|
|
719
|
+
return this.InternalGetEntityRecordName(entityName, compositeKey, contextUser);
|
|
748
720
|
}
|
|
749
721
|
/**
|
|
750
|
-
* Gets display names for multiple entity records in a single operation
|
|
751
|
-
* More efficient than multiple GetEntityRecordName calls.
|
|
722
|
+
* Gets display names for multiple entity records in a single operation.
|
|
752
723
|
* @param info - Array of entity/key pairs to lookup
|
|
753
|
-
* @param contextUser -
|
|
754
|
-
* @param forceRefresh -
|
|
724
|
+
* @param contextUser - The acting user; field- and row-level security are applied for them
|
|
725
|
+
* @param forceRefresh - Bypass any cache. This provider has none, so it always looks up.
|
|
755
726
|
* @returns Array of results with names and status for each requested record
|
|
756
727
|
*/
|
|
757
728
|
async GetEntityRecordNames(info, contextUser, forceRefresh = false) {
|
|
758
|
-
|
|
759
|
-
// Check cache for each item, collect uncached items
|
|
760
|
-
const results = [];
|
|
761
|
-
const uncachedInfo = [];
|
|
762
|
-
const uncachedIndexes = [];
|
|
763
|
-
for (let i = 0; i < info.length; i++) {
|
|
764
|
-
const item = info[i];
|
|
765
|
-
const cacheKey = this.getCacheKey(item.EntityName, item.CompositeKey);
|
|
766
|
-
const cached = this._entityRecordNameCache.Get(cacheKey);
|
|
767
|
-
if (cached !== undefined) {
|
|
768
|
-
// Cache hit
|
|
769
|
-
results[i] = {
|
|
770
|
-
EntityName: item.EntityName,
|
|
771
|
-
CompositeKey: item.CompositeKey,
|
|
772
|
-
Status: 'cached',
|
|
773
|
-
Success: true,
|
|
774
|
-
RecordName: cached
|
|
775
|
-
};
|
|
776
|
-
}
|
|
777
|
-
else {
|
|
778
|
-
// Cache miss - need to fetch
|
|
779
|
-
uncachedInfo.push(item);
|
|
780
|
-
uncachedIndexes.push(i);
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
// Fetch uncached items from database
|
|
784
|
-
if (uncachedInfo.length > 0) {
|
|
785
|
-
const uncachedResults = await this.InternalGetEntityRecordNames(uncachedInfo, contextUser);
|
|
786
|
-
// Merge results and update cache
|
|
787
|
-
for (let i = 0; i < uncachedResults.length; i++) {
|
|
788
|
-
const result = uncachedResults[i];
|
|
789
|
-
const originalIndex = uncachedIndexes[i];
|
|
790
|
-
results[originalIndex] = result;
|
|
791
|
-
// Cache successful results
|
|
792
|
-
if (result.Success && result.RecordName) {
|
|
793
|
-
const cacheKey = this.getCacheKey(result.EntityName, result.CompositeKey);
|
|
794
|
-
this._entityRecordNameCache.Set(cacheKey, result.RecordName);
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
return results;
|
|
799
|
-
}
|
|
800
|
-
else {
|
|
801
|
-
// Force refresh - bypass cache entirely
|
|
802
|
-
const results = await this.InternalGetEntityRecordNames(info, contextUser);
|
|
803
|
-
// Update cache with fresh results
|
|
804
|
-
for (const result of results) {
|
|
805
|
-
if (result.Success && result.RecordName) {
|
|
806
|
-
const cacheKey = this.getCacheKey(result.EntityName, result.CompositeKey);
|
|
807
|
-
this._entityRecordNameCache.Set(cacheKey, result.RecordName);
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
return results;
|
|
811
|
-
}
|
|
729
|
+
return this.InternalGetEntityRecordNames(info, contextUser);
|
|
812
730
|
}
|
|
813
731
|
// ========================================================================
|
|
814
732
|
// PUBLIC API METHODS - Orchestrate Pre → Cache → Internal → Post flow
|
|
@@ -2257,8 +2175,44 @@ export class ProviderBase {
|
|
|
2257
2175
|
* they asked for, and field security narrows per request at read time via
|
|
2258
2176
|
* {@link ApplyFieldSecurityProjection}.
|
|
2259
2177
|
*/
|
|
2260
|
-
ComputeRunViewFetchFields(entity) {
|
|
2261
|
-
|
|
2178
|
+
ComputeRunViewFetchFields(entity, params) {
|
|
2179
|
+
const includeBinary = params?.IncludeBinaryFields === true;
|
|
2180
|
+
return entity.Fields.filter(f => includeBinary || !f.IsBinaryFieldType).map(f => f.Name);
|
|
2181
|
+
}
|
|
2182
|
+
/**
|
|
2183
|
+
* True when `fieldNames` names at least one of the entity's binary fields (case-insensitive,
|
|
2184
|
+
* whitespace-trimmed).
|
|
2185
|
+
*
|
|
2186
|
+
* @param entity - The entity being queried.
|
|
2187
|
+
* @param fieldNames - Field names as a caller supplied them, e.g. `RunViewParams.Fields`.
|
|
2188
|
+
*/
|
|
2189
|
+
static NamesAnyBinaryField(entity, fieldNames) {
|
|
2190
|
+
if (!fieldNames || fieldNames.length === 0)
|
|
2191
|
+
return false;
|
|
2192
|
+
const binaryNames = new Set(entity.Fields.filter(f => f.IsBinaryFieldType).map(f => f.Name.toLowerCase()));
|
|
2193
|
+
if (binaryNames.size === 0)
|
|
2194
|
+
return false;
|
|
2195
|
+
return fieldNames.some(name => typeof name === 'string' && binaryNames.has(name.trim().toLowerCase()));
|
|
2196
|
+
}
|
|
2197
|
+
/**
|
|
2198
|
+
* Decides whether a RunView returns the entity's binary fields, and records the decision on
|
|
2199
|
+
* `params.IncludeBinaryFields` so every later step (field widening, cache fingerprint,
|
|
2200
|
+
* transport) sees one answer.
|
|
2201
|
+
*
|
|
2202
|
+
* Binary fields are returned when the caller sets `IncludeBinaryFields`, or names a binary
|
|
2203
|
+
* field in `Fields` — asking for a column by name is asking for it. Otherwise they are left
|
|
2204
|
+
* out, which keeps result sets, engine caches and the RunView caches free of large values
|
|
2205
|
+
* nobody reads. Must run before `Fields` is widened, since widening replaces the caller's list.
|
|
2206
|
+
*
|
|
2207
|
+
* @param params - The view parameters; `IncludeBinaryFields` is set to true when applicable.
|
|
2208
|
+
* @param entity - The entity being queried.
|
|
2209
|
+
* @returns True when binary fields will be returned.
|
|
2210
|
+
*/
|
|
2211
|
+
ResolveIncludeBinaryFields(params, entity) {
|
|
2212
|
+
if (params.IncludeBinaryFields !== true && ProviderBase.NamesAnyBinaryField(entity, params.Fields)) {
|
|
2213
|
+
params.IncludeBinaryFields = true;
|
|
2214
|
+
}
|
|
2215
|
+
return params.IncludeBinaryFields === true;
|
|
2262
2216
|
}
|
|
2263
2217
|
/**
|
|
2264
2218
|
* Pre-processing hook for RunView.
|
|
@@ -2530,8 +2484,10 @@ export class ProviderBase {
|
|
|
2530
2484
|
// field-restricted user RECEIVES is still their allowed set: the server strips denied
|
|
2531
2485
|
// columns on the wire, and the missing keys mark those fields not-loaded.
|
|
2532
2486
|
const widenForEntityObject = params.ResultType === 'entity_object';
|
|
2487
|
+
if (entity)
|
|
2488
|
+
this.ResolveIncludeBinaryFields(params, entity);
|
|
2533
2489
|
if (entity && (willCache || widenForEntityObject)) {
|
|
2534
|
-
params.Fields = this.ComputeRunViewFetchFields(entity);
|
|
2490
|
+
params.Fields = this.ComputeRunViewFetchFields(entity, params);
|
|
2535
2491
|
// Platform contract: explicit Fields always include the primary key(s) —
|
|
2536
2492
|
// project back down to requested ∪ PK, matching the direct SQL path.
|
|
2537
2493
|
if (callerRequestedFields) {
|
|
@@ -2705,8 +2661,10 @@ export class ProviderBase {
|
|
|
2705
2661
|
const batchWillCache = this.runViewCacheEligible(param);
|
|
2706
2662
|
// Same entity_object-always-widens rule as the single-view path above.
|
|
2707
2663
|
const batchWidenForEntityObject = param.ResultType === 'entity_object';
|
|
2664
|
+
if (batchEntity)
|
|
2665
|
+
this.ResolveIncludeBinaryFields(param, batchEntity);
|
|
2708
2666
|
if (batchEntity && (batchWillCache || batchWidenForEntityObject)) {
|
|
2709
|
-
param.Fields = this.ComputeRunViewFetchFields(batchEntity);
|
|
2667
|
+
param.Fields = this.ComputeRunViewFetchFields(batchEntity, param);
|
|
2710
2668
|
// Platform contract: explicit Fields always include the primary key(s)
|
|
2711
2669
|
if (callerFields) {
|
|
2712
2670
|
callerFields = ProviderBase.UnionFieldsWithPrimaryKeys(callerFields, batchEntity);
|
|
@@ -2815,7 +2773,8 @@ export class ProviderBase {
|
|
|
2815
2773
|
// provider FETCHES, not just how the slot is keyed, and I could not verify the
|
|
2816
2774
|
// downstream fetch behavior tonight. The safe fix is to normalize a full-coverage
|
|
2817
2775
|
// field list to `*` in the FINGERPRINT only, which cannot affect fetching.
|
|
2818
|
-
param
|
|
2776
|
+
this.ResolveIncludeBinaryFields(param, entity);
|
|
2777
|
+
param.Fields = this.ComputeRunViewFetchFields(entity, param);
|
|
2819
2778
|
}
|
|
2820
2779
|
// Gate on runViewCacheEligible (NOT raw param.CacheLocal): the smart-cache-check path is a
|
|
2821
2780
|
// second, independent cache transport, and gating it on CacheLocal alone re-admits the exact
|
|
@@ -3642,7 +3601,11 @@ export class ProviderBase {
|
|
|
3642
3601
|
const entity = this.EntityByName(params.EntityName);
|
|
3643
3602
|
if (!entity)
|
|
3644
3603
|
throw new Error(`Entity ${params.EntityName} not found in metadata`);
|
|
3645
|
-
|
|
3604
|
+
// Override whatever was passed in with every field the entity object hydrates from.
|
|
3605
|
+
// Binary fields are included only when requested (ResolveIncludeBinaryFields); a
|
|
3606
|
+
// missing binary field hydrates as not-loaded and is never written back on Save.
|
|
3607
|
+
this.ResolveIncludeBinaryFields(params, entity);
|
|
3608
|
+
params.Fields = this.ComputeRunViewFetchFields(entity, params);
|
|
3646
3609
|
}
|
|
3647
3610
|
}
|
|
3648
3611
|
/**
|
|
@@ -3706,7 +3669,9 @@ export class ProviderBase {
|
|
|
3706
3669
|
if (!entity) {
|
|
3707
3670
|
throw new Error(`Entity ${param.EntityName} not found in metadata`);
|
|
3708
3671
|
}
|
|
3709
|
-
|
|
3672
|
+
// Same rule as PreProcessRunView: every field, binary fields only on request.
|
|
3673
|
+
this.ResolveIncludeBinaryFields(param, entity);
|
|
3674
|
+
param.Fields = this.ComputeRunViewFetchFields(entity, param);
|
|
3710
3675
|
}
|
|
3711
3676
|
}
|
|
3712
3677
|
}
|
|
@@ -5161,6 +5126,12 @@ export class ProviderBase {
|
|
|
5161
5126
|
const ls = this.LocalStorageProvider;
|
|
5162
5127
|
if (!ls)
|
|
5163
5128
|
return;
|
|
5129
|
+
// Symmetrical with the save: an in-process store can only hand back a copy of the
|
|
5130
|
+
// metadata this heap already holds, at the cost of rebuilding every metadata object.
|
|
5131
|
+
if (!this.MetadataSnapshotPersistenceEnabled) {
|
|
5132
|
+
this.logMetadataSnapshotDisabledOnce();
|
|
5133
|
+
return;
|
|
5134
|
+
}
|
|
5164
5135
|
const overallStart = Date.now();
|
|
5165
5136
|
// The metadata snapshot uses three keys (timestamps, format, AllMetadata).
|
|
5166
5137
|
// Pull them in a single batched read — saves two IDB transaction round-trips
|
|
@@ -5251,6 +5222,18 @@ export class ProviderBase {
|
|
|
5251
5222
|
* Used for compressed metadata storage/retrieval.
|
|
5252
5223
|
*/
|
|
5253
5224
|
static base64ToArrayBuffer(base64) {
|
|
5225
|
+
// Node decodes natively; the atob path below allocates an intermediate string and fills the
|
|
5226
|
+
// array a byte at a time.
|
|
5227
|
+
if (typeof Buffer !== 'undefined') {
|
|
5228
|
+
const buf = Buffer.from(base64, 'base64');
|
|
5229
|
+
// Copy into an exact-size buffer: `buf.buffer` is a view into a shared pool, usually
|
|
5230
|
+
// larger than the payload, so returning it directly would carry unrelated bytes and a
|
|
5231
|
+
// wrong byteLength. It is also typed ArrayBufferLike and will not assign to the
|
|
5232
|
+
// declared return type.
|
|
5233
|
+
const exact = new ArrayBuffer(buf.byteLength);
|
|
5234
|
+
new Uint8Array(exact).set(buf);
|
|
5235
|
+
return exact;
|
|
5236
|
+
}
|
|
5254
5237
|
const binaryString = atob(base64);
|
|
5255
5238
|
const bytes = new Uint8Array(binaryString.length);
|
|
5256
5239
|
for (let i = 0; i < binaryString.length; i++) {
|
|
@@ -5263,6 +5246,11 @@ export class ProviderBase {
|
|
|
5263
5246
|
* Used for compressed metadata storage/retrieval.
|
|
5264
5247
|
*/
|
|
5265
5248
|
static arrayBufferToBase64(buffer) {
|
|
5249
|
+
// Node encodes natively. The loop below concatenates one character per byte, building a rope
|
|
5250
|
+
// the size of the payload that then has to be flattened.
|
|
5251
|
+
if (typeof Buffer !== 'undefined') {
|
|
5252
|
+
return Buffer.from(buffer).toString('base64');
|
|
5253
|
+
}
|
|
5266
5254
|
const bytes = new Uint8Array(buffer);
|
|
5267
5255
|
let binary = '';
|
|
5268
5256
|
for (let i = 0; i < bytes.byteLength; i++) {
|
|
@@ -5278,6 +5266,42 @@ export class ProviderBase {
|
|
|
5278
5266
|
get LocalStoragePrefix() {
|
|
5279
5267
|
return "";
|
|
5280
5268
|
}
|
|
5269
|
+
/**
|
|
5270
|
+
* Whether anything could read the metadata snapshot back, and therefore whether saving it is
|
|
5271
|
+
* worth the cost.
|
|
5272
|
+
*
|
|
5273
|
+
* The snapshot lets a cold process start from a cached copy of the metadata instead of querying
|
|
5274
|
+
* for it, which only works if the store outlives the writer. Where it does not, both halves of
|
|
5275
|
+
* the round trip are pure cost: the save serializes, gzips and base64-encodes the entire
|
|
5276
|
+
* metadata graph, and the load parses it and rebuilds every `EntityInfo` and `EntityFieldInfo`
|
|
5277
|
+
* from a copy of objects the heap already holds. On a large tenant that is expensive enough to
|
|
5278
|
+
* exhaust the heap.
|
|
5279
|
+
*
|
|
5280
|
+
* A provider that does not declare {@link ILocalStorageProvider.SupportsCrossProcessPersistence}
|
|
5281
|
+
* is treated as persistent, which fails in the safer direction.
|
|
5282
|
+
*/
|
|
5283
|
+
get MetadataSnapshotPersistenceEnabled() {
|
|
5284
|
+
const ls = this.LocalStorageProvider;
|
|
5285
|
+
if (!ls) {
|
|
5286
|
+
return false;
|
|
5287
|
+
}
|
|
5288
|
+
return ls.SupportsCrossProcessPersistence !== false;
|
|
5289
|
+
}
|
|
5290
|
+
/**
|
|
5291
|
+
* Explains the skip once per process. The refresh path runs every 30 seconds on a busy server,
|
|
5292
|
+
* so logging per call would bury the one line that matters.
|
|
5293
|
+
*/
|
|
5294
|
+
logMetadataSnapshotDisabledOnce() {
|
|
5295
|
+
if (this._metadataSnapshotSkipLogged) {
|
|
5296
|
+
return;
|
|
5297
|
+
}
|
|
5298
|
+
this._metadataSnapshotSkipLogged = true;
|
|
5299
|
+
const name = this.LocalStorageProvider?.constructor?.name ?? 'the local storage provider';
|
|
5300
|
+
LogStatusEx({
|
|
5301
|
+
message: `[Metadata Cache] Snapshot persistence disabled: ${name} is in-process only, so a saved snapshot could never be read back. Skipping the metadata snapshot save and load.`,
|
|
5302
|
+
verboseOnly: false
|
|
5303
|
+
});
|
|
5304
|
+
}
|
|
5281
5305
|
/**
|
|
5282
5306
|
* Saves current metadata to local storage for caching.
|
|
5283
5307
|
* Serializes both timestamps and full metadata collections.
|
|
@@ -5287,6 +5311,12 @@ export class ProviderBase {
|
|
|
5287
5311
|
const ls = this.LocalStorageProvider;
|
|
5288
5312
|
if (!ls)
|
|
5289
5313
|
return;
|
|
5314
|
+
// Nothing could ever read this snapshot back, so the entire serialize/compress/encode
|
|
5315
|
+
// pass buys nothing. See MetadataSnapshotPersistenceEnabled.
|
|
5316
|
+
if (!this.MetadataSnapshotPersistenceEnabled) {
|
|
5317
|
+
this.logMetadataSnapshotDisabledOnce();
|
|
5318
|
+
return;
|
|
5319
|
+
}
|
|
5290
5320
|
const start = Date.now();
|
|
5291
5321
|
// Serialize the AllMetadata object FIRST. If this throws (or the payload write below
|
|
5292
5322
|
// fails), nothing else has been written yet, so the previously stored timestamps still
|