@memberjunction/core 6.2.0-edge.7 → 6.2.0-edge.8
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/JSONType-interfaces/IEntityConfiguration.d.ts +32 -0
- package/dist/generic/JSONType-interfaces/IEntityConfiguration.d.ts.map +1 -1
- package/dist/generic/baseEngine.d.ts +47 -4
- package/dist/generic/baseEngine.d.ts.map +1 -1
- package/dist/generic/baseEngine.js +96 -18
- package/dist/generic/baseEngine.js.map +1 -1
- package/dist/generic/baseEngineRegistry.d.ts +10 -2
- package/dist/generic/baseEngineRegistry.d.ts.map +1 -1
- package/dist/generic/baseEngineRegistry.js +33 -5
- package/dist/generic/baseEngineRegistry.js.map +1 -1
- package/dist/generic/baseEntity.d.ts +60 -4
- package/dist/generic/baseEntity.d.ts.map +1 -1
- package/dist/generic/baseEntity.js +603 -93
- package/dist/generic/baseEntity.js.map +1 -1
- package/dist/generic/databaseProviderBase.d.ts.map +1 -1
- package/dist/generic/databaseProviderBase.js +43 -9
- package/dist/generic/databaseProviderBase.js.map +1 -1
- package/dist/generic/entityInfo.d.ts +19 -1
- package/dist/generic/entityInfo.d.ts.map +1 -1
- package/dist/generic/entityInfo.js +52 -0
- package/dist/generic/entityInfo.js.map +1 -1
- package/dist/generic/localCacheManager.d.ts +21 -0
- package/dist/generic/localCacheManager.d.ts.map +1 -1
- package/dist/generic/localCacheManager.js +107 -1
- package/dist/generic/localCacheManager.js.map +1 -1
- package/dist/generic/logging.d.ts +105 -38
- package/dist/generic/logging.d.ts.map +1 -1
- package/dist/generic/logging.js +196 -5
- package/dist/generic/logging.js.map +1 -1
- package/dist/generic/providerBase.d.ts +43 -1
- package/dist/generic/providerBase.d.ts.map +1 -1
- package/dist/generic/providerBase.js +359 -99
- package/dist/generic/providerBase.js.map +1 -1
- package/dist/generic/sharedLoad.d.ts +42 -0
- package/dist/generic/sharedLoad.d.ts.map +1 -0
- package/dist/generic/sharedLoad.js +74 -0
- package/dist/generic/sharedLoad.js.map +1 -0
- package/dist/generic/telemetryManager.d.ts +31 -0
- package/dist/generic/telemetryManager.d.ts.map +1 -1
- package/dist/generic/telemetryManager.js +85 -9
- package/dist/generic/telemetryManager.js.map +1 -1
- package/dist/generic/transactionGroup.d.ts +19 -0
- package/dist/generic/transactionGroup.d.ts.map +1 -1
- package/dist/generic/transactionGroup.js +55 -7
- package/dist/generic/transactionGroup.js.map +1 -1
- package/dist/generic/util.d.ts +1 -1
- package/dist/generic/util.d.ts.map +1 -1
- package/dist/generic/util.js +2 -2
- package/dist/generic/util.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
|
@@ -238,6 +238,21 @@ export class ProviderBase {
|
|
|
238
238
|
*/
|
|
239
239
|
this._inflightViews = new Map();
|
|
240
240
|
this._lingerInvalidationWired = false;
|
|
241
|
+
/**
|
|
242
|
+
* Per-entity monotonic mutation counter incremented whenever an entity receives a
|
|
243
|
+
* BaseEntity save/delete/remote-invalidate or an explicit {@link InvalidateRunViewDedupCache}
|
|
244
|
+
* call. Used by dataset reads (`GetDatasetByName`) to detect that an entity was mutated while
|
|
245
|
+
* SQL execution or post-processing was in flight and avoid writing stale pre-mutation rows
|
|
246
|
+
* back into `LocalCacheManager`.
|
|
247
|
+
*/
|
|
248
|
+
this._entityMutationVersions = new Map();
|
|
249
|
+
/**
|
|
250
|
+
* Monotonic counter incremented whenever any metadata-member entity is mutated against this
|
|
251
|
+
* provider's backend. Checked by `GetAllMetadata()` before caching `MJ_Metadata` into
|
|
252
|
+
* `DatasetCache` so an in-flight background reload does not overwrite `DatasetCache` with a
|
|
253
|
+
* pre-mutation snapshot after a save/delete has already landed.
|
|
254
|
+
*/
|
|
255
|
+
this._metadataMemberMutationSeq = 0;
|
|
241
256
|
/**
|
|
242
257
|
* Lowercased names of the entities whose rows COMPOSE this provider's metadata — the
|
|
243
258
|
* MJ_Metadata dataset's item entities, recorded each time the dataset is loaded (and restored
|
|
@@ -427,6 +442,23 @@ export class ProviderBase {
|
|
|
427
442
|
if (!entityName) {
|
|
428
443
|
return;
|
|
429
444
|
}
|
|
445
|
+
const cascadeRelatedEntities = [];
|
|
446
|
+
const isaRelatedEntities = [];
|
|
447
|
+
const entityInfo = entityEvent.baseEntity?.EntityInfo;
|
|
448
|
+
if (entityEvent.type === 'delete' && entityInfo?.CascadeDeletes && Array.isArray(entityInfo.RelatedEntities)) {
|
|
449
|
+
for (const rel of entityInfo.RelatedEntities) {
|
|
450
|
+
const relName = rel?.RelatedEntity?.trim().toLowerCase();
|
|
451
|
+
const relType = (rel?.Type ?? 'One To Many').trim().toUpperCase();
|
|
452
|
+
if (relName && relName !== entityName && relType === 'ONE TO MANY' && !cascadeRelatedEntities.includes(relName)) {
|
|
453
|
+
cascadeRelatedEntities.push(relName);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if ((entityEvent.type === 'save' || entityEvent.type === 'delete') && entityInfo) {
|
|
458
|
+
for (const isaLower of ProviderBase.collectISAHierarchyEntityNamesLower(entityInfo, entityName)) {
|
|
459
|
+
isaRelatedEntities.push(isaLower);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
430
462
|
for (const ref of ProviderBase._invalidationTargets) {
|
|
431
463
|
const instance = ref.deref();
|
|
432
464
|
if (!instance) {
|
|
@@ -437,9 +469,50 @@ export class ProviderBase {
|
|
|
437
469
|
}
|
|
438
470
|
instance.invalidateInflightViewsForEntity(entityName);
|
|
439
471
|
instance.handleMetadataMemberEntityEvent(entityName, entityEvent);
|
|
472
|
+
for (const relLower of cascadeRelatedEntities) {
|
|
473
|
+
instance.invalidateInflightViewsForEntity(relLower);
|
|
474
|
+
instance.handleMetadataMemberEntityEvent(relLower, entityEvent);
|
|
475
|
+
}
|
|
476
|
+
for (const isaLower of isaRelatedEntities) {
|
|
477
|
+
instance.invalidateInflightViewsForEntity(isaLower);
|
|
478
|
+
instance.handleMetadataMemberEntityEvent(isaLower, entityEvent);
|
|
479
|
+
}
|
|
440
480
|
}
|
|
441
481
|
});
|
|
442
482
|
}
|
|
483
|
+
/**
|
|
484
|
+
* Collects all lowercased entity names in the same IS-A hierarchy (`ParentEntityInfo` ancestors
|
|
485
|
+
* and `ChildEntities` descendants) as `entityInfo`, excluding `excludeLower`.
|
|
486
|
+
*/
|
|
487
|
+
static collectISAHierarchyEntityNamesLower(entityInfo, excludeLower) {
|
|
488
|
+
if (!entityInfo.IsChildType && !entityInfo.IsParentType) {
|
|
489
|
+
return [];
|
|
490
|
+
}
|
|
491
|
+
let root = entityInfo;
|
|
492
|
+
const seenAncestors = new Set();
|
|
493
|
+
while (root.ParentEntityInfo && !seenAncestors.has(root.ParentEntityInfo)) {
|
|
494
|
+
seenAncestors.add(root);
|
|
495
|
+
root = root.ParentEntityInfo;
|
|
496
|
+
}
|
|
497
|
+
const results = [];
|
|
498
|
+
const visited = new Set();
|
|
499
|
+
const queue = [root];
|
|
500
|
+
while (queue.length > 0) {
|
|
501
|
+
const current = queue.shift();
|
|
502
|
+
const lower = current?.Name?.trim().toLowerCase();
|
|
503
|
+
if (!lower || visited.has(lower)) {
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
visited.add(lower);
|
|
507
|
+
if (lower !== excludeLower) {
|
|
508
|
+
results.push(lower);
|
|
509
|
+
}
|
|
510
|
+
if (Array.isArray(current.ChildEntities) && current.ChildEntities.length > 0) {
|
|
511
|
+
queue.push(...current.ChildEntities);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return results;
|
|
515
|
+
}
|
|
443
516
|
/**
|
|
444
517
|
* Lazily registers this instance (once per provider instance) for write-invalidation
|
|
445
518
|
* of in-flight/lingered RunView entries touching a written entity. Deleting an IN-FLIGHT
|
|
@@ -460,6 +533,17 @@ export class ProviderBase {
|
|
|
460
533
|
this._lingerInvalidationWired = true;
|
|
461
534
|
ProviderBase._invalidationTargets.add(new WeakRef(this));
|
|
462
535
|
}
|
|
536
|
+
/**
|
|
537
|
+
* Returns the current monotonic mutation version for `entityName` (case-insensitive).
|
|
538
|
+
*/
|
|
539
|
+
GetEntityMutationVersion(entityName) {
|
|
540
|
+
this.ensureInflightViewInvalidation();
|
|
541
|
+
const lower = entityName?.trim().toLowerCase();
|
|
542
|
+
if (!lower) {
|
|
543
|
+
return 0;
|
|
544
|
+
}
|
|
545
|
+
return this._entityMutationVersions.get(lower) ?? 0;
|
|
546
|
+
}
|
|
463
547
|
/**
|
|
464
548
|
* Drops this provider's in-flight/lingered RunView dedup entries so the next identical
|
|
465
549
|
* RunView goes to the server instead of replaying a result from the last
|
|
@@ -482,6 +566,9 @@ export class ProviderBase {
|
|
|
482
566
|
const names = (entityNames ?? []).map(n => n?.trim().toLowerCase()).filter((n) => !!n);
|
|
483
567
|
if (names.length === 0) {
|
|
484
568
|
this._inflightViews.clear();
|
|
569
|
+
for (const [k, v] of this._entityMutationVersions) {
|
|
570
|
+
this._entityMutationVersions.set(k, v + 1);
|
|
571
|
+
}
|
|
485
572
|
return;
|
|
486
573
|
}
|
|
487
574
|
for (const n of names) {
|
|
@@ -493,6 +580,7 @@ export class ProviderBase {
|
|
|
493
580
|
* entity (lowercased name). Called on BaseEntity save/delete/remote-invalidate.
|
|
494
581
|
*/
|
|
495
582
|
invalidateInflightViewsForEntity(lowerEntityName) {
|
|
583
|
+
this._entityMutationVersions.set(lowerEntityName, (this._entityMutationVersions.get(lowerEntityName) ?? 0) + 1);
|
|
496
584
|
for (const [key, entry] of this._inflightViews) {
|
|
497
585
|
if (entry.entityNames?.has(lowerEntityName)) {
|
|
498
586
|
this._inflightViews.delete(key);
|
|
@@ -672,6 +760,7 @@ export class ProviderBase {
|
|
|
672
760
|
if (!this.eventTargetsThisProviderBackend(entityEvent)) {
|
|
673
761
|
return;
|
|
674
762
|
}
|
|
763
|
+
this._metadataMemberMutationSeq++;
|
|
675
764
|
this.scheduleMetadataMemberRefresh();
|
|
676
765
|
}
|
|
677
766
|
/**
|
|
@@ -890,6 +979,14 @@ export class ProviderBase {
|
|
|
890
979
|
return true;
|
|
891
980
|
}
|
|
892
981
|
this._metadataMemberRefreshWaits = 0;
|
|
982
|
+
if (this._metadataReloadInFlight) {
|
|
983
|
+
// A metadata reload is already in flight. Do not call RefreshRemoteMetadataTimestamps()
|
|
984
|
+
// concurrently here, because that mutates _latestRemoteMetadataTimestamps out from under
|
|
985
|
+
// the in-flight pass before it runs adoptServerMetadata(). Instead, delegate directly to
|
|
986
|
+
// ReloadMetadata, which sets _metadataReloadQueued = true and re-reads authoritative
|
|
987
|
+
// remote timestamps at the start of the queued follow-up pass.
|
|
988
|
+
return this.ReloadMetadata(true, undefined, true);
|
|
989
|
+
}
|
|
893
990
|
// The writer holds positive evidence, so the timestamps are re-read from the authoritative
|
|
894
991
|
// status query. When they attribute the change entirely to the saved-query family (see
|
|
895
992
|
// PartialRefreshMetadataItemCodes) only that family is reloaded, cache layers bypassed as a
|
|
@@ -1299,6 +1396,7 @@ export class ProviderBase {
|
|
|
1299
1396
|
// allCached ⇒ every param produced a hit and was pushed in order (PreRunViews only
|
|
1300
1397
|
// pushes a null placeholder on the path that clears allCached), so index i of
|
|
1301
1398
|
// cachedResults corresponds to params[i].
|
|
1399
|
+
await this.TransformBatchRunViewResults(params, preResult.cachedResults, undefined, preResult.callerFieldsMap, contextUser);
|
|
1302
1400
|
for (let i = 0; i < preResult.cachedResults.length; i++) {
|
|
1303
1401
|
await this.ApplyPostRunViewHooksToCacheHit(params[i], preResult.cachedResults[i], contextUser);
|
|
1304
1402
|
}
|
|
@@ -2334,22 +2432,24 @@ export class ProviderBase {
|
|
|
2334
2432
|
queryDefinition = queryAuth.definition;
|
|
2335
2433
|
// TTL-enforced, and a miss when the slot was written under another query definition.
|
|
2336
2434
|
const cached = queryFingerprint ? await LocalCacheManager.Instance.GetRunQueryResult(queryFingerprint, queryDefinition) : null;
|
|
2337
|
-
if (
|
|
2338
|
-
const serveFromSlot =
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2435
|
+
if (queryFingerprint) {
|
|
2436
|
+
const serveFromSlot = cached
|
|
2437
|
+
? () => ({
|
|
2438
|
+
QueryID: cached.queryId ?? params.QueryID ?? '',
|
|
2439
|
+
QueryName: params.QueryName ?? '',
|
|
2440
|
+
Success: true,
|
|
2441
|
+
// Transport boundary: `cached.results` is readonly (shared, deep-frozen cache
|
|
2442
|
+
// rows) while the outbound Results is mutable — the runtime freeze is the
|
|
2443
|
+
// enforcement. Same cast as the RunView hit paths above.
|
|
2444
|
+
Results: cached.results,
|
|
2445
|
+
RowCount: cached.results.length,
|
|
2446
|
+
TotalRowCount: cached.rowCount ?? cached.results.length,
|
|
2447
|
+
ExecutionTime: 0,
|
|
2448
|
+
ErrorMessage: '',
|
|
2449
|
+
CacheHit: true,
|
|
2450
|
+
CacheKey: queryFingerprint
|
|
2451
|
+
})
|
|
2452
|
+
: undefined;
|
|
2353
2453
|
const checker = this.RunQueriesWithCacheCheck?.bind(this);
|
|
2354
2454
|
if (!checker || this.TrustLocalCacheCompletely) {
|
|
2355
2455
|
// TTL mode (server providers / no validation transport).
|
|
@@ -2366,69 +2466,71 @@ export class ProviderBase {
|
|
|
2366
2466
|
// authorizes with the proper error message. Query metadata missing from the
|
|
2367
2467
|
// provider cache ⇒ ALSO fall through (never serve rows we cannot authorize);
|
|
2368
2468
|
// that costs one query run, not correctness.
|
|
2369
|
-
if (
|
|
2370
|
-
if (
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2469
|
+
if (cached && serveFromSlot) {
|
|
2470
|
+
if (contextUser) {
|
|
2471
|
+
if (queryAuth.resolvable) {
|
|
2472
|
+
// Metadata can answer — enforce it. `authorized` comes from the
|
|
2473
|
+
// provider's STRONGEST available check (B45): the base enforces
|
|
2474
|
+
// roles-only QueryInfo.UserCanRun; GenericDatabaseProvider's
|
|
2475
|
+
// override enforces the full MJQueryEntityExtended.UserCanRun
|
|
2476
|
+
// (roles + entity CanRead + recursive composition) — the identical
|
|
2477
|
+
// check ValidateQueryForExecution applies on the miss path, so a
|
|
2478
|
+
// hit is never easier to read than a miss.
|
|
2479
|
+
if (queryAuth.authorized) {
|
|
2480
|
+
return serveFromSlot();
|
|
2481
|
+
}
|
|
2482
|
+
LogStatusEx({ message: `RunQuery cache: user '${contextUser.Email}' lacks run permission on '${queryAuth.queryName ?? params.QueryName ?? params.QueryID}' — falling through to authorized execution.`, verboseOnly: true });
|
|
2483
|
+
}
|
|
2484
|
+
else if (cached.warmedForUserID && UUIDsEqual(cached.warmedForUserID, contextUser.ID)) {
|
|
2485
|
+
// Metadata CANNOT answer — runtime-created queries never appear in the
|
|
2486
|
+
// provider's Queries cache (it does not refresh in-process; verified:
|
|
2487
|
+
// a saved Query stays invisible, 21 -> 21). But a slot only exists
|
|
2488
|
+
// because its WARMER executed the fully-authorized miss path — so the
|
|
2489
|
+
// warmer's own permission is already proven. Serve the warmer;
|
|
2490
|
+
// anyone ELSE falls through and pays one authorized execution.
|
|
2491
|
+
//
|
|
2492
|
+
// The first version of this gate failed closed on unresolvable
|
|
2493
|
+
// metadata, which silently disabled TTL caching for every
|
|
2494
|
+
// runtime-created query — caught by Q2/Q5/Q10 going red.
|
|
2379
2495
|
return serveFromSlot();
|
|
2380
2496
|
}
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
// Metadata CANNOT answer — runtime-created queries never appear in the
|
|
2385
|
-
// provider's Queries cache (it does not refresh in-process; verified:
|
|
2386
|
-
// a saved Query stays invisible, 21 -> 21). But a slot only exists
|
|
2387
|
-
// because its WARMER executed the fully-authorized miss path — so the
|
|
2388
|
-
// warmer's own permission is already proven. Serve the warmer;
|
|
2389
|
-
// anyone ELSE falls through and pays one authorized execution.
|
|
2390
|
-
//
|
|
2391
|
-
// The first version of this gate failed closed on unresolvable
|
|
2392
|
-
// metadata, which silently disabled TTL caching for every
|
|
2393
|
-
// runtime-created query — caught by Q2/Q5/Q10 going red.
|
|
2394
|
-
return serveFromSlot();
|
|
2497
|
+
else {
|
|
2498
|
+
LogStatusEx({ message: `RunQuery cache: '${params.QueryID ?? params.QueryName}' not in cached metadata and requester is not the slot's warmer — falling through to authorized execution.`, verboseOnly: true });
|
|
2499
|
+
}
|
|
2395
2500
|
}
|
|
2396
2501
|
else {
|
|
2397
|
-
|
|
2502
|
+
return serveFromSlot();
|
|
2398
2503
|
}
|
|
2399
2504
|
}
|
|
2400
|
-
else {
|
|
2401
|
-
return serveFromSlot();
|
|
2402
|
-
}
|
|
2403
|
-
}
|
|
2404
|
-
// Client smart validation round trip.
|
|
2405
|
-
//
|
|
2406
|
-
// Guarded on `checker` (R4-2): the block above is entered when `!checker ||
|
|
2407
|
-
// TrustLocalCacheCompletely`. If we got here via `!checker` AND the B43 permission
|
|
2408
|
-
// gate DECLINED to serve (denied user / non-warmer), execution used to fall
|
|
2409
|
-
// straight into `await checker(...)` = `await undefined(...)` — a TypeError instead
|
|
2410
|
-
// of the promised "fall through to authorized execution". Every in-tree provider
|
|
2411
|
-
// implements the optional `RunQueriesWithCacheCheck`, so this was latent, but the
|
|
2412
|
-
// pre-B43 code handled `!checker` and this must too: skip the round trip and let
|
|
2413
|
-
// the normal PreRunQuery/InternalRunQuery path below authorize and execute.
|
|
2414
|
-
if (!checker) {
|
|
2415
|
-
// fall through to normal execution below
|
|
2416
2505
|
}
|
|
2417
2506
|
else {
|
|
2507
|
+
// Client smart validation round trip (both cold first-fetch and warm revalidation).
|
|
2508
|
+
// Routing cold CacheLocal requests through RunQueriesWithCacheCheck with no cacheStatus
|
|
2509
|
+
// engages the server's firstFetchWithValidation path so the initial client slot is
|
|
2510
|
+
// stamped with maxUpdatedAt, rowCount, and definitionHash — allowing the very first
|
|
2511
|
+
// subsequent refresh (F5) to validate as 'current' instead of missing.
|
|
2418
2512
|
const response = await checker([{
|
|
2419
2513
|
params,
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2514
|
+
...(cached
|
|
2515
|
+
? {
|
|
2516
|
+
// '' for a slot written without a hash: the server answers `stale` and returns
|
|
2517
|
+
// its hash, so the rewrite below stamps the slot.
|
|
2518
|
+
cacheStatus: {
|
|
2519
|
+
maxUpdatedAt: cached.maxUpdatedAt,
|
|
2520
|
+
rowCount: cached.rowCount,
|
|
2521
|
+
definitionHash: cached.definitionHash ?? ''
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
: {})
|
|
2423
2525
|
}], contextUser);
|
|
2424
2526
|
const check = response.results?.[0];
|
|
2425
2527
|
if (response.success && check) {
|
|
2426
|
-
if (check.status === 'current') {
|
|
2528
|
+
if (check.status === 'current' && serveFromSlot) {
|
|
2427
2529
|
return serveFromSlot();
|
|
2428
2530
|
}
|
|
2429
2531
|
if ((check.status === 'stale' || check.status === 'no_validation') && check.results) {
|
|
2430
2532
|
const freshRows = check.results;
|
|
2431
|
-
// Fire-and-forget slot rewrite — same pattern as the RunView client path
|
|
2533
|
+
// Fire-and-forget slot write/rewrite — same pattern as the RunView client path
|
|
2432
2534
|
LocalCacheManager.Instance.SetRunQueryResult(queryFingerprint, params.QueryName ?? '', freshRows, check.maxUpdatedAt ?? '', check.rowCount, check.queryId, params.CacheLocalTTL, contextUser?.ID, check.definitionHash ? { QueryID: check.queryId, Hash: check.definitionHash } : undefined).catch(e => LogError(`RunQuery cache rewrite failed: ${e}`));
|
|
2433
2535
|
return {
|
|
2434
2536
|
QueryID: check.queryId ?? params.QueryID ?? '',
|
|
@@ -2438,12 +2540,25 @@ export class ProviderBase {
|
|
|
2438
2540
|
RowCount: freshRows.length,
|
|
2439
2541
|
TotalRowCount: check.rowCount ?? freshRows.length,
|
|
2440
2542
|
ExecutionTime: 0,
|
|
2441
|
-
ErrorMessage: ''
|
|
2543
|
+
ErrorMessage: '',
|
|
2544
|
+
CacheKey: queryFingerprint
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2547
|
+
if (check.status === 'error') {
|
|
2548
|
+
return {
|
|
2549
|
+
QueryID: check.queryId ?? params.QueryID ?? '',
|
|
2550
|
+
QueryName: params.QueryName ?? '',
|
|
2551
|
+
Success: false,
|
|
2552
|
+
Results: [],
|
|
2553
|
+
RowCount: 0,
|
|
2554
|
+
TotalRowCount: 0,
|
|
2555
|
+
ExecutionTime: 0,
|
|
2556
|
+
ErrorMessage: check.errorMessage ?? response.errorMessage ?? 'Query execution failed'
|
|
2442
2557
|
};
|
|
2443
2558
|
}
|
|
2444
2559
|
}
|
|
2560
|
+
// validation transport failed — fall through to a normal execution
|
|
2445
2561
|
}
|
|
2446
|
-
// validation transport failed — fall through to a normal execution
|
|
2447
2562
|
}
|
|
2448
2563
|
}
|
|
2449
2564
|
// Pre-processing: telemetry, cache check
|
|
@@ -3378,6 +3493,7 @@ export class ProviderBase {
|
|
|
3378
3493
|
let results = await this.MaterializeCachedRowsForCaller(cached.results, batchEntity, contextUser, Boolean(param.PreserveRestrictedApiCiphertext));
|
|
3379
3494
|
if (projectedCallerFields && param.ResultType !== 'entity_object') {
|
|
3380
3495
|
results = ProjectRowsToFields(results, projectedCallerFields);
|
|
3496
|
+
callerFieldsMap.set(i, projectedCallerFields);
|
|
3381
3497
|
}
|
|
3382
3498
|
// Field-level security on the batch cache-hit path — same reasoning as the
|
|
3383
3499
|
// single-view path in PreRunView: this result is returned without
|
|
@@ -3396,8 +3512,9 @@ export class ProviderBase {
|
|
|
3396
3512
|
// different Aggregates[] order — remap to THIS caller's requested order.
|
|
3397
3513
|
AggregateResults: LocalCacheManager.Instance.ReorderAggregateResultsToRequest(cached.aggregateResults, param.Aggregates)
|
|
3398
3514
|
};
|
|
3399
|
-
//
|
|
3400
|
-
|
|
3515
|
+
// Entity transformation (`ResultType: 'entity_object'`) is deferred to
|
|
3516
|
+
// TransformBatchRunViewResults in runAdmittedViewStages / PostRunViews so
|
|
3517
|
+
// cache hits and DB misses participate together in two-phase sibling IS-A hydration.
|
|
3401
3518
|
this.logDeprecatedAlternateBaseViewIfAny(param, 'PreRunViews');
|
|
3402
3519
|
if (!param.CacheLocal && this.TrustLocalCacheCompletely) {
|
|
3403
3520
|
LogStatusEx({ message: ` ✅ [Server Cache HIT] RunViews "${param.EntityName || param.ViewName || 'unknown'}" — ${cached.results.length} rows served from server cache (no DB query)`, verboseOnly: true });
|
|
@@ -3573,6 +3690,14 @@ export class ProviderBase {
|
|
|
3573
3690
|
// pre-resolved map instead of issuing their own GetRunViewResult calls.
|
|
3574
3691
|
const processingPromises = params.map((param, i) => this.processSingleSmartCacheResult(param, i, serverResultsByViewIndex.get(i), preResolvedCache, contextUser));
|
|
3575
3692
|
const processedResults = await Promise.all(processingPromises);
|
|
3693
|
+
const batchResults = processedResults.map(r => r.result);
|
|
3694
|
+
const alreadyTransformedIndices = new Set();
|
|
3695
|
+
for (let i = 0; i < processedResults.length; i++) {
|
|
3696
|
+
if (processedResults[i].alreadyTransformed) {
|
|
3697
|
+
alreadyTransformedIndices.add(i);
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
await this.TransformBatchRunViewResults(params, batchResults, alreadyTransformedIndices, undefined, contextUser);
|
|
3576
3701
|
// Aggregate telemetry stats
|
|
3577
3702
|
let cacheHits = 0;
|
|
3578
3703
|
let cacheMisses = 0;
|
|
@@ -3590,7 +3715,7 @@ export class ProviderBase {
|
|
|
3590
3715
|
cacheMisses,
|
|
3591
3716
|
batchSize: params.length
|
|
3592
3717
|
});
|
|
3593
|
-
return
|
|
3718
|
+
return batchResults;
|
|
3594
3719
|
}
|
|
3595
3720
|
/**
|
|
3596
3721
|
* Processes a single smart cache check result.
|
|
@@ -3636,8 +3761,6 @@ export class ProviderBase {
|
|
|
3636
3761
|
// different Aggregates[] order — remap to THIS caller's requested order.
|
|
3637
3762
|
AggregateResults: LocalCacheManager.Instance.ReorderAggregateResultsToRequest(cached.aggregateResults, param.Aggregates)
|
|
3638
3763
|
};
|
|
3639
|
-
// Transform to entity objects if needed
|
|
3640
|
-
await this.TransformSimpleObjectToEntityObject(param, cachedResult, contextUser);
|
|
3641
3764
|
return { result: cachedResult, cacheHit: true, cacheMiss: false };
|
|
3642
3765
|
}
|
|
3643
3766
|
else {
|
|
@@ -3688,8 +3811,6 @@ export class ProviderBase {
|
|
|
3688
3811
|
// Include aggregate results - either fresh from server or from merged cache
|
|
3689
3812
|
AggregateResults: checkResult.aggregateResults || merged.aggregateResults
|
|
3690
3813
|
};
|
|
3691
|
-
// Transform to entity objects if needed
|
|
3692
|
-
await this.TransformSimpleObjectToEntityObject(param, mergedResult, contextUser);
|
|
3693
3814
|
return { result: mergedResult, cacheHit: true, cacheMiss: false };
|
|
3694
3815
|
}
|
|
3695
3816
|
}
|
|
@@ -3704,14 +3825,23 @@ export class ProviderBase {
|
|
|
3704
3825
|
// `r.result` straight to the caller, so null became the caller's RESULT.
|
|
3705
3826
|
//
|
|
3706
3827
|
// The only correct remedy is what a cache miss does anyway: fetch in full. The
|
|
3707
|
-
// declined slot was already invalidated by ApplyDifferentialUpdate,
|
|
3708
|
-
//
|
|
3709
|
-
//
|
|
3710
|
-
//
|
|
3828
|
+
// declined slot was already invalidated by ApplyDifferentialUpdate, and because
|
|
3829
|
+
// fallbackParam sets CacheLocal:false + BypassCache:true (so the refetch takes the
|
|
3830
|
+
// plain path and cannot re-enter the smart-cache transport), PostRunViews does not
|
|
3831
|
+
// write to LocalCacheManager — so we repopulate the slot explicitly here.
|
|
3711
3832
|
LogStatusEx({ message: `Differential merge declined for '${param.EntityName}' — refetching in full.`, verboseOnly: true });
|
|
3712
3833
|
const fallbackParam = { ...param, CacheLocal: false, BypassCache: true };
|
|
3713
3834
|
const freshFetch = await this.RunView(fallbackParam, contextUser);
|
|
3714
|
-
|
|
3835
|
+
if (this.runViewCacheEligibleForWrite(param) && freshFetch.Success && LocalCacheManager.Instance.IsInitialized) {
|
|
3836
|
+
const plainRows = (freshFetch.Results || []).map(row => row && typeof row.GetAll === 'function'
|
|
3837
|
+
? row.GetAll()
|
|
3838
|
+
: row);
|
|
3839
|
+
const maxUpdatedAt = checkResult.maxUpdatedAt || this.extractMaxUpdatedAt(plainRows) || '';
|
|
3840
|
+
if (maxUpdatedAt) {
|
|
3841
|
+
LocalCacheManager.Instance.SetRunViewResult(fingerprint, param, plainRows, maxUpdatedAt, freshFetch.AggregateResults ?? checkResult.aggregateResults, freshFetch.TotalRowCount ?? checkResult.rowCount, this).catch(e => LogError(`Failed to update cache after differential fallback: ${e}`));
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3844
|
+
return { result: freshFetch, cacheHit: false, cacheMiss: true, alreadyTransformed: true };
|
|
3715
3845
|
}
|
|
3716
3846
|
else if (checkResult.status === 'stale') {
|
|
3717
3847
|
// Cache is stale - use fresh data and update cache (entity doesn't support differential)
|
|
@@ -3736,8 +3866,6 @@ export class ProviderBase {
|
|
|
3736
3866
|
LocalCacheManager.Instance.SetRunViewResult(fingerprint, param, checkResult.results || [], checkResult.maxUpdatedAt, checkResult.aggregateResults, // Include aggregate results in cache
|
|
3737
3867
|
checkResult.rowCount, this).catch(e => LogError(`Failed to update cache: ${e}`));
|
|
3738
3868
|
}
|
|
3739
|
-
// Transform to entity objects if needed
|
|
3740
|
-
await this.TransformSimpleObjectToEntityObject(param, freshResult, contextUser);
|
|
3741
3869
|
return { result: freshResult, cacheHit: false, cacheMiss: true };
|
|
3742
3870
|
}
|
|
3743
3871
|
else {
|
|
@@ -3986,17 +4114,8 @@ export class ProviderBase {
|
|
|
3986
4114
|
results[i].Results = this.ApplyFieldSecurityProjection(results[i].Results, params[i], contextUser);
|
|
3987
4115
|
results[i].Results = this.ApplyRecordChangeFieldSecurityProjection(results[i].Results, params[i], contextUser);
|
|
3988
4116
|
}
|
|
3989
|
-
// Transform results to entity objects AFTER caching plain objects.
|
|
3990
|
-
|
|
3991
|
-
const transformPromises = [];
|
|
3992
|
-
for (let i = 0; i < results.length; i++) {
|
|
3993
|
-
const cacheInfo = preResult.cacheStatusMap?.get(i);
|
|
3994
|
-
if (cacheInfo?.status === 'hit') {
|
|
3995
|
-
continue;
|
|
3996
|
-
}
|
|
3997
|
-
transformPromises.push(this.TransformSimpleObjectToEntityObject(params[i], results[i], contextUser));
|
|
3998
|
-
}
|
|
3999
|
-
await Promise.all(transformPromises);
|
|
4117
|
+
// Transform results (both cache hits and DB misses) to entity objects AFTER caching plain objects.
|
|
4118
|
+
await this.TransformBatchRunViewResults(params, results, undefined, preResult.callerFieldsMap, contextUser);
|
|
4000
4119
|
// Run registered PostRunView hooks on each result in the batch
|
|
4001
4120
|
for (let i = 0; i < results.length; i++) {
|
|
4002
4121
|
results[i] = await this.RunPostRunViewHooks(params[i], results[i], contextUser);
|
|
@@ -4417,12 +4536,7 @@ export class ProviderBase {
|
|
|
4417
4536
|
*/
|
|
4418
4537
|
async PostProcessRunViews(results, params, contextUser) {
|
|
4419
4538
|
if (params && params.length > 0) {
|
|
4420
|
-
|
|
4421
|
-
for (let i = 0; i < results.length; i++) {
|
|
4422
|
-
promises.push(this.TransformSimpleObjectToEntityObject(params[i], results[i], contextUser));
|
|
4423
|
-
}
|
|
4424
|
-
// await the promises for all transformations
|
|
4425
|
-
await Promise.all(promises);
|
|
4539
|
+
await this.TransformBatchRunViewResults(params, results, undefined, undefined, contextUser);
|
|
4426
4540
|
// End telemetry tracking for batch operation
|
|
4427
4541
|
const eventId = params[0]._telemetryBatchEventId;
|
|
4428
4542
|
if (eventId) {
|
|
@@ -4431,18 +4545,113 @@ export class ProviderBase {
|
|
|
4431
4545
|
}
|
|
4432
4546
|
}
|
|
4433
4547
|
}
|
|
4548
|
+
/**
|
|
4549
|
+
* Two-phase batch entity transformation for `RunViews`:
|
|
4550
|
+
* 1. Transforms all non-IS-A-parent views in parallel first so lookup tables (`SubtypeSelector`)
|
|
4551
|
+
* and IS-A child tables in the same batch are hydrated before parent views run.
|
|
4552
|
+
* 2. Registers all unfiltered full-entity views from this batch on `BaseEntity` for the duration
|
|
4553
|
+
* of Phase 2, then transforms IS-A parent views so they can resolve child records and
|
|
4554
|
+
* `SubtypeSelector` targets directly from memory with 0 extra queries.
|
|
4555
|
+
*/
|
|
4556
|
+
async TransformBatchRunViewResults(params, results, skipTransformIndices, callerFieldsMap, contextUser) {
|
|
4557
|
+
if (!params || params.length === 0) {
|
|
4558
|
+
return;
|
|
4559
|
+
}
|
|
4560
|
+
const nonParentIndices = [];
|
|
4561
|
+
const isaParentIndices = [];
|
|
4562
|
+
for (let i = 0; i < results.length; i++) {
|
|
4563
|
+
if (skipTransformIndices?.has(i)) {
|
|
4564
|
+
continue;
|
|
4565
|
+
}
|
|
4566
|
+
const firstRow = results[i]?.Results?.[0];
|
|
4567
|
+
if (firstRow instanceof BaseEntity) {
|
|
4568
|
+
continue;
|
|
4569
|
+
}
|
|
4570
|
+
const param = params[i];
|
|
4571
|
+
if (!param) {
|
|
4572
|
+
continue;
|
|
4573
|
+
}
|
|
4574
|
+
const entity = param.EntityName ? this.EntityByName(param.EntityName) : undefined;
|
|
4575
|
+
if (param.ResultType === 'entity_object' && entity?.IsParentType && entity.ChildEntities.length > 0) {
|
|
4576
|
+
isaParentIndices.push(i);
|
|
4577
|
+
}
|
|
4578
|
+
else {
|
|
4579
|
+
nonParentIndices.push(i);
|
|
4580
|
+
}
|
|
4581
|
+
}
|
|
4582
|
+
if (nonParentIndices.length > 0) {
|
|
4583
|
+
await Promise.all(nonParentIndices.map(i => this.TransformSimpleObjectToEntityObject(params[i], results[i], contextUser)));
|
|
4584
|
+
}
|
|
4585
|
+
if (isaParentIndices.length === 0) {
|
|
4586
|
+
return;
|
|
4587
|
+
}
|
|
4588
|
+
const batchUnfilteredCache = new Map();
|
|
4589
|
+
for (let i = 0; i < results.length; i++) {
|
|
4590
|
+
const param = params[i];
|
|
4591
|
+
if (param && this.isUnfilteredFullEntityBatchView(param, results[i], callerFieldsMap, i)) {
|
|
4592
|
+
const key = param.EntityName.trim().toLowerCase();
|
|
4593
|
+
if (!batchUnfilteredCache.has(key)) {
|
|
4594
|
+
batchUnfilteredCache.set(key, results[i].Results);
|
|
4595
|
+
}
|
|
4596
|
+
}
|
|
4597
|
+
}
|
|
4598
|
+
const batchCache = batchUnfilteredCache.size > 0 ? batchUnfilteredCache : undefined;
|
|
4599
|
+
await Promise.all(isaParentIndices.map(i => this.TransformSimpleObjectToEntityObject(params[i], results[i], contextUser, batchCache)));
|
|
4600
|
+
}
|
|
4601
|
+
isUnfilteredFullEntityBatchView(param, result, callerFieldsMap, viewIndex) {
|
|
4602
|
+
if (!param.EntityName || !result?.Success || !Array.isArray(result.Results)) {
|
|
4603
|
+
return false;
|
|
4604
|
+
}
|
|
4605
|
+
if (param.ResultType === 'count_only') {
|
|
4606
|
+
return false;
|
|
4607
|
+
}
|
|
4608
|
+
const filter = typeof param.ExtraFilter === 'string' ? param.ExtraFilter.trim() : '';
|
|
4609
|
+
const search = typeof param.UserSearchString === 'string' ? param.UserSearchString.trim() : '';
|
|
4610
|
+
if (filter.length > 0 || search.length > 0) {
|
|
4611
|
+
return false;
|
|
4612
|
+
}
|
|
4613
|
+
if (param.ViewID || param.ViewName || param.ViewEntity) {
|
|
4614
|
+
return false;
|
|
4615
|
+
}
|
|
4616
|
+
const alternateBaseView = param.AlternateBaseView;
|
|
4617
|
+
if (alternateBaseView && alternateBaseView.trim().length > 0) {
|
|
4618
|
+
return false;
|
|
4619
|
+
}
|
|
4620
|
+
if (param.MaxRows || param.StartRow || param.AfterKey) {
|
|
4621
|
+
return false;
|
|
4622
|
+
}
|
|
4623
|
+
if (typeof result.TotalRowCount === 'number' &&
|
|
4624
|
+
result.TotalRowCount > result.Results.length) {
|
|
4625
|
+
return false;
|
|
4626
|
+
}
|
|
4627
|
+
if (param.IgnoreMaxRows !== true) {
|
|
4628
|
+
const entity = this.EntityByName(param.EntityName);
|
|
4629
|
+
const maxRowsCap = entity?.UserViewMaxRows ?? 0;
|
|
4630
|
+
if (maxRowsCap > 0 && result.Results.length >= maxRowsCap) {
|
|
4631
|
+
return false;
|
|
4632
|
+
}
|
|
4633
|
+
}
|
|
4634
|
+
if (viewIndex !== undefined && callerFieldsMap?.has(viewIndex)) {
|
|
4635
|
+
return false;
|
|
4636
|
+
}
|
|
4637
|
+
if (param.Fields && param.Fields.length > 0 && !this.isFullCoverageFieldList(param)) {
|
|
4638
|
+
return false;
|
|
4639
|
+
}
|
|
4640
|
+
return true;
|
|
4641
|
+
}
|
|
4434
4642
|
/**
|
|
4435
4643
|
* Transforms the result set from simple objects to entity objects if needed.
|
|
4436
4644
|
* @param param - The RunViewParams used for the request
|
|
4437
4645
|
* @param result - The RunViewResult returned from the request
|
|
4438
4646
|
* @param contextUser - The user context for permissions
|
|
4647
|
+
* @param batchUnfilteredCache - Optional map of unfiltered full-entity sibling views from the same RunViews call
|
|
4439
4648
|
*/
|
|
4440
|
-
async TransformSimpleObjectToEntityObject(param, result, contextUser) {
|
|
4649
|
+
async TransformSimpleObjectToEntityObject(param, result, contextUser, batchUnfilteredCache) {
|
|
4441
4650
|
// Mutually exclusive with the entity branch below: entity objects get real types from
|
|
4442
4651
|
// BaseEntity's Get/Set conversion, so normalization applies only to non-entity results.
|
|
4443
4652
|
this.NormalizeSimpleRowTypes(param, result);
|
|
4444
4653
|
if (param.ResultType === 'entity_object' && result && result.Success && result.Results?.length > 0) {
|
|
4445
|
-
result.Results = await TransformSimpleObjectToEntityObject(this, param.EntityName, result.Results, contextUser);
|
|
4654
|
+
result.Results = await TransformSimpleObjectToEntityObject(this, param.EntityName, result.Results, contextUser, batchUnfilteredCache);
|
|
4446
4655
|
// Opt-in batched child loading: ONE query per named collection across the whole result
|
|
4447
4656
|
// set, not one per row. Companion eager loading is deliberately kept out of
|
|
4448
4657
|
// LoadFromData() (which is the per-row path above) precisely so that populating children
|
|
@@ -4870,6 +5079,9 @@ export class ProviderBase {
|
|
|
4870
5079
|
LogWarning(`[Metadata Cache] Row count mismatch when adopting server metadata for "${remoteEntry.Type}": ` +
|
|
4871
5080
|
`received ${receivedRowCount} rows but remote status reported ${remoteEntry.RowCount} rows. ` +
|
|
4872
5081
|
`Not advancing local timestamp for "${remoteEntry.Type}" so the next check self-heals.`, 'Metadata');
|
|
5082
|
+
if (LocalCacheManager.Instance.IsInitialized && remoteEntry.Type) {
|
|
5083
|
+
LocalCacheManager.Instance.InvalidateEntityCaches(remoteEntry.Type).catch(() => { });
|
|
5084
|
+
}
|
|
4873
5085
|
const priorEntry = prevLocal.find(m => norm(m.Type) === norm(remoteEntry.Type));
|
|
4874
5086
|
if (priorEntry) {
|
|
4875
5087
|
reconciled.push({
|
|
@@ -4897,6 +5109,7 @@ export class ProviderBase {
|
|
|
4897
5109
|
if (!hasMismatch) {
|
|
4898
5110
|
return remote;
|
|
4899
5111
|
}
|
|
5112
|
+
this.ClearDatasetCache(ProviderBase._mjMetadataDatasetName).catch(() => { });
|
|
4900
5113
|
const remoteRollup = remote.find(r => norm(r.Type) === rollup);
|
|
4901
5114
|
if (remoteRollup) {
|
|
4902
5115
|
const priorRollup = prevLocal.find(m => norm(m.Type) === rollup);
|
|
@@ -5282,6 +5495,40 @@ export class ProviderBase {
|
|
|
5282
5495
|
return f;
|
|
5283
5496
|
}
|
|
5284
5497
|
static { this._mjMetadataDatasetName = 'MJ_Metadata'; }
|
|
5498
|
+
/**
|
|
5499
|
+
* Checks whether any item in a freshly fetched `MJ_Metadata` dataset has a row count that
|
|
5500
|
+
* disagrees with `_latestRemoteMetadataTimestamps` (when remote timestamps are populated).
|
|
5501
|
+
* Used to prevent `GetAllMetadata()` from caching a dataset snapshot into `DatasetCache`
|
|
5502
|
+
* whose row counts are already stale relative to remote status.
|
|
5503
|
+
*/
|
|
5504
|
+
hasDatasetRemoteRowCountMismatch(dataset) {
|
|
5505
|
+
const remote = this._latestRemoteMetadataTimestamps;
|
|
5506
|
+
if (!remote || remote.length === 0 || !Array.isArray(dataset?.Results)) {
|
|
5507
|
+
return false;
|
|
5508
|
+
}
|
|
5509
|
+
const norm = (v) => (v ?? '').trim().toLowerCase();
|
|
5510
|
+
const rollup = norm(ProviderBase._allMetadataTypeName);
|
|
5511
|
+
const nonRollupRemote = remote.filter(r => norm(r.Type) !== rollup);
|
|
5512
|
+
const remoteByID = IndexByUUID(nonRollupRemote.filter(r => Boolean(r.ID)), r => r.ID);
|
|
5513
|
+
const remoteByType = new Map();
|
|
5514
|
+
for (const r of nonRollupRemote) {
|
|
5515
|
+
if (r.Type) {
|
|
5516
|
+
remoteByType.set(norm(r.Type), r);
|
|
5517
|
+
}
|
|
5518
|
+
}
|
|
5519
|
+
for (const item of dataset.Results) {
|
|
5520
|
+
if (!Array.isArray(item.Results)) {
|
|
5521
|
+
continue;
|
|
5522
|
+
}
|
|
5523
|
+
const remoteEntry = (item.EntityID ? remoteByID.Get(item.EntityID) : undefined)
|
|
5524
|
+
?? remoteByType.get(norm(item.EntityName))
|
|
5525
|
+
?? remoteByType.get(norm(item.Code));
|
|
5526
|
+
if (remoteEntry && remoteEntry.RowCount !== undefined && item.Results.length !== remoteEntry.RowCount) {
|
|
5527
|
+
return true;
|
|
5528
|
+
}
|
|
5529
|
+
}
|
|
5530
|
+
return false;
|
|
5531
|
+
}
|
|
5285
5532
|
/**
|
|
5286
5533
|
* Retrieves all metadata from the server and constructs typed instances.
|
|
5287
5534
|
* Uses the MJ_Metadata dataset for efficient bulk loading.
|
|
@@ -5292,6 +5539,7 @@ export class ProviderBase {
|
|
|
5292
5539
|
// we are now using datasets instead of the custom metadata to GraphQL to simplify GraphQL's work as it was very slow preivously
|
|
5293
5540
|
// NOTE: Schema filters (IncludeSchemas/ExcludeSchemas) are for CodeGen only, not for runtime
|
|
5294
5541
|
// metadata loading. We always load all schemas — there are no sys/staging entities in metadata anyway.
|
|
5542
|
+
const mutationSeqAtStart = this._metadataMemberMutationSeq;
|
|
5295
5543
|
// Get the dataset and cache it for anyone else who wants to use it
|
|
5296
5544
|
// When forceRefresh is true (from a hard Refresh() call), bypass LocalCacheManager
|
|
5297
5545
|
const d = await this.GetDatasetByName(ProviderBase._mjMetadataDatasetName, null, this.CurrentUser, providerToUse, forceRefresh);
|
|
@@ -5306,12 +5554,24 @@ export class ProviderBase {
|
|
|
5306
5554
|
this.recordMetadataLoadFailure(new Error(`GetAllMetadata() - the ${ProviderBase._mjMetadataDatasetName} dataset returned no entities; keeping the metadata already loaded`));
|
|
5307
5555
|
}
|
|
5308
5556
|
else if (d && d.Success) {
|
|
5309
|
-
// cache the dataset for anyone who wants to use it
|
|
5310
|
-
await this.CacheDataset(ProviderBase._mjMetadataDatasetName, null, d);
|
|
5311
5557
|
// Record which entities this metadata is built from, so a write to any of them
|
|
5312
5558
|
// schedules a debounced refresh of this provider's metadata (see
|
|
5313
5559
|
// registerMetadataDatasetMembership — membership is the dataset definition, not a list).
|
|
5314
5560
|
this.registerMetadataDatasetMembership(d);
|
|
5561
|
+
// Only cache the dataset in DatasetCache when no metadata-member mutation occurred
|
|
5562
|
+
// while GetDatasetByName was in flight, no follow-up reload is already queued, and
|
|
5563
|
+
// item row counts match _latestRemoteMetadataTimestamps. Otherwise a slow background
|
|
5564
|
+
// GetAllMetadata() that started before a delete can overwrite DatasetCache with
|
|
5565
|
+
// pre-delete rows right after the delete has already completed.
|
|
5566
|
+
const mutatedDuringLoad = this._metadataReloadQueued ||
|
|
5567
|
+
this._metadataMemberMutationSeq !== mutationSeqAtStart ||
|
|
5568
|
+
this.hasDatasetRemoteRowCountMismatch(d);
|
|
5569
|
+
if (!mutatedDuringLoad) {
|
|
5570
|
+
await this.CacheDataset(ProviderBase._mjMetadataDatasetName, null, d);
|
|
5571
|
+
}
|
|
5572
|
+
else {
|
|
5573
|
+
await this.ClearDatasetCache(ProviderBase._mjMetadataDatasetName);
|
|
5574
|
+
}
|
|
5315
5575
|
// got the results, let's build our response in the format we need
|
|
5316
5576
|
const simpleMetadata = {};
|
|
5317
5577
|
for (let r of d.Results) {
|