@memberjunction/core 5.15.0 → 5.17.0
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/RegisterForStartup.d.ts +11 -5
- package/dist/generic/RegisterForStartup.d.ts.map +1 -1
- package/dist/generic/RegisterForStartup.js +12 -6
- package/dist/generic/RegisterForStartup.js.map +1 -1
- package/dist/generic/applicationInfo.d.ts.map +1 -1
- package/dist/generic/applicationInfo.js +1 -4
- package/dist/generic/applicationInfo.js.map +1 -1
- package/dist/generic/baseEngine.js +1 -1
- package/dist/generic/baseEngine.js.map +1 -1
- package/dist/generic/baseEntity.js +1 -1
- package/dist/generic/baseEntity.js.map +1 -1
- package/dist/generic/databaseProviderBase.d.ts.map +1 -1
- package/dist/generic/databaseProviderBase.js +6 -6
- package/dist/generic/databaseProviderBase.js.map +1 -1
- package/dist/generic/entityInfo.d.ts +2 -0
- package/dist/generic/entityInfo.d.ts.map +1 -1
- package/dist/generic/entityInfo.js +23 -8
- package/dist/generic/entityInfo.js.map +1 -1
- package/dist/generic/interfaces.d.ts +10 -0
- package/dist/generic/interfaces.d.ts.map +1 -1
- package/dist/generic/interfaces.js.map +1 -1
- package/dist/generic/localCacheManager.js +1 -1
- package/dist/generic/localCacheManager.js.map +1 -1
- package/dist/generic/metadata.d.ts.map +1 -1
- package/dist/generic/metadata.js +19 -4
- package/dist/generic/metadata.js.map +1 -1
- package/dist/generic/providerBase.d.ts +67 -0
- package/dist/generic/providerBase.d.ts.map +1 -1
- package/dist/generic/providerBase.js +312 -11
- package/dist/generic/providerBase.js.map +1 -1
- package/dist/generic/queryInfo.d.ts.map +1 -1
- package/dist/generic/queryInfo.js +10 -2
- package/dist/generic/queryInfo.js.map +1 -1
- package/dist/generic/runQuerySQLFilterImplementations.d.ts +9 -3
- package/dist/generic/runQuerySQLFilterImplementations.d.ts.map +1 -1
- package/dist/generic/runQuerySQLFilterImplementations.js +11 -6
- package/dist/generic/runQuerySQLFilterImplementations.js.map +1 -1
- package/dist/views/runView.js +2 -2
- package/dist/views/runView.js.map +1 -1
- package/dist/views/viewInfo.d.ts.map +1 -1
- package/dist/views/viewInfo.js +1 -3
- package/dist/views/viewInfo.js.map +1 -1
- package/package.json +2 -2
|
@@ -4,7 +4,7 @@ import { AllMetadata } from "./interfaces.js";
|
|
|
4
4
|
import { LocalCacheManager } from "./localCacheManager.js";
|
|
5
5
|
import { ApplicationInfo } from "../generic/applicationInfo.js";
|
|
6
6
|
import { AuditLogTypeInfo, AuthorizationInfo, RoleInfo, RowLevelSecurityFilterInfo, UserInfo } from "./securityInfo.js";
|
|
7
|
-
import { MJGlobal, UUIDsEqual } from "@memberjunction/global";
|
|
7
|
+
import { MJGlobal, NormalizeUUID, UUIDsEqual } from "@memberjunction/global";
|
|
8
8
|
import { TelemetryManager } from "./telemetryManager.js";
|
|
9
9
|
import { LogError, LogStatus, LogStatusEx } from "./logging.js";
|
|
10
10
|
import { QueryCategoryInfo, QueryFieldInfo, QueryInfo, QueryPermissionInfo, QueryEntityInfo, QueryParameterInfo, QueryDependencyInfo, SQLDialectInfo, QuerySQLInfo } from "./queryInfo.js";
|
|
@@ -96,9 +96,19 @@ export const AllMetadataArrays = [
|
|
|
96
96
|
export class ProviderBase {
|
|
97
97
|
constructor() {
|
|
98
98
|
this._localMetadata = new AllMetadata();
|
|
99
|
+
this._entityMapByName = new Map();
|
|
100
|
+
this._entityMapByID = new Map();
|
|
99
101
|
this._entityRecordNameCache = new Map();
|
|
100
102
|
this._refresh = false;
|
|
101
103
|
this._lastRefreshCheckAt = 0;
|
|
104
|
+
/**
|
|
105
|
+
* Pending coalesced requests waiting to be flushed.
|
|
106
|
+
* Each entry tracks the original params, contextUser, and a resolver
|
|
107
|
+
* so the caller's promise resolves with only their slice of results.
|
|
108
|
+
*/
|
|
109
|
+
this._coalesceQueue = [];
|
|
110
|
+
/** Timer handle for the coalesce flush */
|
|
111
|
+
this._coalesceTimer = null;
|
|
102
112
|
/**
|
|
103
113
|
* In-flight + linger cache keyed by a deterministic fingerprint of
|
|
104
114
|
* the RunViewParams batch. While a request is in-flight, concurrent
|
|
@@ -133,6 +143,35 @@ export class ProviderBase {
|
|
|
133
143
|
* Set to 0 to disable auto-caching. Default 250.
|
|
134
144
|
*/
|
|
135
145
|
static { this.ServerAutoCacheMaxRows = 250; }
|
|
146
|
+
// ── Request Coalescing ─────────────────────────────────────────────
|
|
147
|
+
/**
|
|
148
|
+
* When enabled, concurrent RunViews calls arriving within the same
|
|
149
|
+
* microtask (or within CoalesceWindowMs) are merged into a single
|
|
150
|
+
* mega-batch before hitting the network. This dramatically reduces
|
|
151
|
+
* the number of HTTP round-trips during startup when multiple engines
|
|
152
|
+
* independently call RunViews in parallel.
|
|
153
|
+
*
|
|
154
|
+
* Set to 0 to disable coalescing. Default 10ms — enough to capture
|
|
155
|
+
* all engines that fire in the same tick, without adding perceptible delay.
|
|
156
|
+
*/
|
|
157
|
+
static { this.CoalesceWindowMs = 10; }
|
|
158
|
+
// ── Fast Startup Mode ─────────────────────────────────────────────
|
|
159
|
+
/**
|
|
160
|
+
* When enabled, the first round of RunViews calls after startup will
|
|
161
|
+
* trust the local IndexedDB/localStorage cache without server validation.
|
|
162
|
+
* This eliminates the smart cache check round-trips during initial load,
|
|
163
|
+
* making warm page refreshes near-instant.
|
|
164
|
+
*
|
|
165
|
+
* After the first round of engine loads completes, FastStartupMode
|
|
166
|
+
* automatically disables itself so subsequent data access uses normal
|
|
167
|
+
* server validation.
|
|
168
|
+
*
|
|
169
|
+
* Only applies to client-side providers (TrustLocalCacheCompletely=false).
|
|
170
|
+
* Set to false to disable.
|
|
171
|
+
*/
|
|
172
|
+
static { this.FastStartupMode = true; }
|
|
173
|
+
/** Tracks whether fast startup has been consumed (auto-disables after first use) */
|
|
174
|
+
static { this._fastStartupConsumed = false; }
|
|
136
175
|
// ── Request Deduplication + Linger Window ──────────────────────────
|
|
137
176
|
/**
|
|
138
177
|
* How long (ms) a resolved RunViews result stays available for instant
|
|
@@ -335,6 +374,12 @@ export class ProviderBase {
|
|
|
335
374
|
if (this.ShouldBypassDedup(params)) {
|
|
336
375
|
return this.ExecuteRunViewsPipeline(params, contextUser);
|
|
337
376
|
}
|
|
377
|
+
// ── Coalescing: merge concurrent RunViews into one mega-batch ──
|
|
378
|
+
if (ProviderBase.CoalesceWindowMs > 0 && !this.TrustLocalCacheCompletely) {
|
|
379
|
+
// Only coalesce on the client side where cache validation round-trips are expensive.
|
|
380
|
+
// Server-side trusts its cache (no network calls), so coalescing adds no benefit.
|
|
381
|
+
return this.enqueueCoalescedRunViews(params, contextUser);
|
|
382
|
+
}
|
|
338
383
|
const key = this.GenerateDedupKey(params, contextUser);
|
|
339
384
|
const existing = this._inflightViews.get(key);
|
|
340
385
|
// ── Linger hit: resolved result still within the linger window ──
|
|
@@ -433,6 +478,128 @@ export class ProviderBase {
|
|
|
433
478
|
await this.PostRunViews(finalResults, params, preResult, contextUser);
|
|
434
479
|
return finalResults;
|
|
435
480
|
}
|
|
481
|
+
// ── Coalescing implementation ─────────────────────────────────────
|
|
482
|
+
/**
|
|
483
|
+
* Enqueues a RunViews call for coalescing. Multiple calls arriving within
|
|
484
|
+
* CoalesceWindowMs are merged into a single mega-batch, executed once, and
|
|
485
|
+
* each caller receives only their slice of the results.
|
|
486
|
+
*/
|
|
487
|
+
enqueueCoalescedRunViews(params, contextUser) {
|
|
488
|
+
return new Promise((resolve, reject) => {
|
|
489
|
+
this._coalesceQueue.push({
|
|
490
|
+
params,
|
|
491
|
+
contextUser,
|
|
492
|
+
resolve: resolve,
|
|
493
|
+
reject
|
|
494
|
+
});
|
|
495
|
+
// Start the coalesce timer if not already running
|
|
496
|
+
if (!this._coalesceTimer) {
|
|
497
|
+
this._coalesceTimer = setTimeout(() => this.flushCoalesceQueue(), ProviderBase.CoalesceWindowMs);
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Flushes the coalesce queue: merges all pending RunViews params into one
|
|
503
|
+
* mega-batch, executes it, then splits results back to each original caller.
|
|
504
|
+
*/
|
|
505
|
+
async flushCoalesceQueue() {
|
|
506
|
+
this._coalesceTimer = null;
|
|
507
|
+
// Grab and clear the queue atomically
|
|
508
|
+
const queue = this._coalesceQueue.splice(0);
|
|
509
|
+
if (queue.length === 0)
|
|
510
|
+
return;
|
|
511
|
+
// If only one caller, no merging needed
|
|
512
|
+
if (queue.length === 1) {
|
|
513
|
+
const entry = queue[0];
|
|
514
|
+
try {
|
|
515
|
+
const results = await this.RunViewsUncoalesced(entry.params, entry.contextUser);
|
|
516
|
+
entry.resolve(results);
|
|
517
|
+
}
|
|
518
|
+
catch (err) {
|
|
519
|
+
entry.reject(err);
|
|
520
|
+
}
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
// Merge all params into one mega-batch, tracking boundaries
|
|
524
|
+
const allParams = [];
|
|
525
|
+
const boundaries = [];
|
|
526
|
+
// Use the first caller's contextUser (all should be the same on client-side)
|
|
527
|
+
const contextUser = queue[0].contextUser;
|
|
528
|
+
for (const entry of queue) {
|
|
529
|
+
boundaries.push({ start: allParams.length, count: entry.params.length });
|
|
530
|
+
allParams.push(...entry.params);
|
|
531
|
+
}
|
|
532
|
+
const entityNames = allParams.map(p => p.EntityName || p.ViewName || '?').join(', ');
|
|
533
|
+
LogStatusEx({
|
|
534
|
+
message: `⚡ [Coalesce] Merged ${queue.length} RunViews calls (${allParams.length} total entities) into 1 mega-batch: [${entityNames}]`,
|
|
535
|
+
verboseOnly: false
|
|
536
|
+
});
|
|
537
|
+
try {
|
|
538
|
+
// Execute the mega-batch as a single pipeline call
|
|
539
|
+
const allResults = await this.RunViewsUncoalesced(allParams, contextUser);
|
|
540
|
+
// Split results back to each original caller
|
|
541
|
+
for (let i = 0; i < queue.length; i++) {
|
|
542
|
+
const { start, count } = boundaries[i];
|
|
543
|
+
const callerResults = allResults.slice(start, start + count);
|
|
544
|
+
queue[i].resolve(callerResults);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
catch (err) {
|
|
548
|
+
// If the mega-batch fails, reject all callers
|
|
549
|
+
for (const entry of queue) {
|
|
550
|
+
entry.reject(err);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Runs RunViews without coalescing (used internally after coalescing merge).
|
|
556
|
+
* This goes through dedup + linger + the full pipeline.
|
|
557
|
+
*/
|
|
558
|
+
async RunViewsUncoalesced(params, contextUser) {
|
|
559
|
+
const key = this.GenerateDedupKey(params, contextUser);
|
|
560
|
+
const existing = this._inflightViews.get(key);
|
|
561
|
+
// ── Linger hit ──
|
|
562
|
+
if (existing?.resolvedResults && existing.resolvedAt) {
|
|
563
|
+
const age = Date.now() - existing.resolvedAt;
|
|
564
|
+
if (age < ProviderBase.DedupLingerMs) {
|
|
565
|
+
return existing.resolvedResults.map(r => this.ShallowCopyResult(r));
|
|
566
|
+
}
|
|
567
|
+
this._inflightViews.delete(key);
|
|
568
|
+
}
|
|
569
|
+
// ── In-flight hit ──
|
|
570
|
+
if (existing && !existing.resolvedResults) {
|
|
571
|
+
const results = await existing.promise;
|
|
572
|
+
return results.map(r => this.ShallowCopyResult(r));
|
|
573
|
+
}
|
|
574
|
+
// ── Fresh execution ──
|
|
575
|
+
const promise = this.ExecuteRunViewsPipeline(params, contextUser)
|
|
576
|
+
.then(results => {
|
|
577
|
+
const entry = this._inflightViews.get(key);
|
|
578
|
+
if (entry && entry.promise === promise) {
|
|
579
|
+
entry.resolvedResults = results;
|
|
580
|
+
entry.resolvedAt = Date.now();
|
|
581
|
+
if (ProviderBase.DedupLingerMs > 0) {
|
|
582
|
+
setTimeout(() => {
|
|
583
|
+
const current = this._inflightViews.get(key);
|
|
584
|
+
if (current && current.promise === promise) {
|
|
585
|
+
this._inflightViews.delete(key);
|
|
586
|
+
}
|
|
587
|
+
}, ProviderBase.DedupLingerMs);
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
this._inflightViews.delete(key);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return results;
|
|
594
|
+
})
|
|
595
|
+
.catch(err => {
|
|
596
|
+
this._inflightViews.delete(key);
|
|
597
|
+
throw err;
|
|
598
|
+
});
|
|
599
|
+
this._inflightViews.set(key, { promise });
|
|
600
|
+
const results = await promise;
|
|
601
|
+
return results.map(r => this.ShallowCopyResult(r));
|
|
602
|
+
}
|
|
436
603
|
/**
|
|
437
604
|
* Generates a deterministic dedup key for a batch of RunViewParams.
|
|
438
605
|
* Extends the local-cache fingerprint with additional fields that
|
|
@@ -549,7 +716,7 @@ export class ProviderBase {
|
|
|
549
716
|
*/
|
|
550
717
|
async EntityStatusCheck(params, callerName) {
|
|
551
718
|
const entityName = await RunView.GetEntityNameFromRunViewParams(params, this);
|
|
552
|
-
const entity =
|
|
719
|
+
const entity = entityName ? this.EntityByName(entityName) : undefined;
|
|
553
720
|
if (!entity) {
|
|
554
721
|
throw new Error(`Entity ${entityName} not found in metadata`);
|
|
555
722
|
}
|
|
@@ -641,7 +808,7 @@ export class ProviderBase {
|
|
|
641
808
|
// Handle entity_object result type - need all fields
|
|
642
809
|
const entityLookupStart = performance.now();
|
|
643
810
|
if (params.ResultType === 'entity_object') {
|
|
644
|
-
const entity = this.
|
|
811
|
+
const entity = this.EntityByName(params.EntityName);
|
|
645
812
|
if (!entity)
|
|
646
813
|
throw new Error(`Entity ${params.EntityName} not found in metadata`);
|
|
647
814
|
params.Fields = entity.Fields.map(f => f.Name);
|
|
@@ -715,9 +882,52 @@ export class ProviderBase {
|
|
|
715
882
|
// Server-side providers trust the cache completely and fall through to
|
|
716
883
|
// the traditional flow which returns cached data immediately on hit.
|
|
717
884
|
if (!this.TrustLocalCacheCompletely) {
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
885
|
+
// FastStartupMode: on the first engine load after a page refresh, trust
|
|
886
|
+
// the local IndexedDB cache without server validation. This eliminates
|
|
887
|
+
// the RunViewsWithCacheCheck round-trips that dominate warm-load TTI.
|
|
888
|
+
// After the first batch of engine loads, auto-disable so subsequent
|
|
889
|
+
// requests use normal server validation for data freshness.
|
|
890
|
+
const useFastStartup = ProviderBase.FastStartupMode
|
|
891
|
+
&& !ProviderBase._fastStartupConsumed
|
|
892
|
+
&& LocalCacheManager.Instance.IsInitialized
|
|
893
|
+
&& params.some(p => p.CacheLocal);
|
|
894
|
+
if (useFastStartup) {
|
|
895
|
+
// Check if we actually have cached data for ALL params
|
|
896
|
+
let allHaveCachedData = true;
|
|
897
|
+
for (const param of params) {
|
|
898
|
+
if (param.CacheLocal) {
|
|
899
|
+
const fp = LocalCacheManager.Instance.GenerateRunViewFingerprint(param, this.InstanceConnectionString);
|
|
900
|
+
const cached = await LocalCacheManager.Instance.GetRunViewResult(fp);
|
|
901
|
+
if (!cached) {
|
|
902
|
+
allHaveCachedData = false;
|
|
903
|
+
break;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
if (allHaveCachedData) {
|
|
908
|
+
// Mark fast startup as consumed — subsequent loads will validate normally
|
|
909
|
+
ProviderBase._fastStartupConsumed = true;
|
|
910
|
+
const entityNames = params.map(p => p.EntityName || p.ViewName || '?').join(', ');
|
|
911
|
+
LogStatusEx({
|
|
912
|
+
message: `⚡ [Fast-Start] Trusting local cache for ${params.length} views [${entityNames}] — skipping server validation`,
|
|
913
|
+
verboseOnly: false
|
|
914
|
+
});
|
|
915
|
+
// Fall through to the traditional flow below which will return cached data
|
|
916
|
+
}
|
|
917
|
+
else {
|
|
918
|
+
// Not all params have cached data — use normal smart cache check
|
|
919
|
+
ProviderBase._fastStartupConsumed = true; // Still consume the fast-start flag
|
|
920
|
+
const useSmartCacheCheck = params.some(p => p.CacheLocal);
|
|
921
|
+
if (useSmartCacheCheck) {
|
|
922
|
+
return this.prepareSmartCacheCheckParams(params, telemetryEventId, contextUser);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
else if (!useFastStartup) {
|
|
927
|
+
const useSmartCacheCheck = params.some(p => p.CacheLocal);
|
|
928
|
+
if (useSmartCacheCheck && LocalCacheManager.Instance.IsInitialized) {
|
|
929
|
+
return this.prepareSmartCacheCheckParams(params, telemetryEventId, contextUser);
|
|
930
|
+
}
|
|
721
931
|
}
|
|
722
932
|
}
|
|
723
933
|
// Traditional caching flow
|
|
@@ -731,7 +941,7 @@ export class ProviderBase {
|
|
|
731
941
|
await this.EntityStatusCheck(param, 'PreRunViews');
|
|
732
942
|
// Handle entity_object result type - need all fields
|
|
733
943
|
if (param.ResultType === 'entity_object') {
|
|
734
|
-
const entity = this.
|
|
944
|
+
const entity = this.EntityByName(param.EntityName);
|
|
735
945
|
if (!entity) {
|
|
736
946
|
throw new Error(`Entity ${param.EntityName} not found in metadata`);
|
|
737
947
|
}
|
|
@@ -795,7 +1005,7 @@ export class ProviderBase {
|
|
|
795
1005
|
await this.EntityStatusCheck(param, 'PreRunViews');
|
|
796
1006
|
// Handle entity_object result type - need all fields
|
|
797
1007
|
if (param.ResultType === 'entity_object') {
|
|
798
|
-
const entity = this.
|
|
1008
|
+
const entity = this.EntityByName(param.EntityName);
|
|
799
1009
|
if (!entity) {
|
|
800
1010
|
throw new Error(`Entity ${param.EntityName} not found in metadata`);
|
|
801
1011
|
}
|
|
@@ -940,7 +1150,7 @@ export class ProviderBase {
|
|
|
940
1150
|
// Cache is stale but we have differential data - merge with cached data
|
|
941
1151
|
const fingerprint = LocalCacheManager.Instance.GenerateRunViewFingerprint(param, this.InstanceConnectionString);
|
|
942
1152
|
// Get entity info for primary key field name
|
|
943
|
-
const entity = this.
|
|
1153
|
+
const entity = this.EntityByName(param.EntityName);
|
|
944
1154
|
const primaryKeyFieldName = entity?.FirstPrimaryKey?.Name || 'ID';
|
|
945
1155
|
// Apply differential update to cache
|
|
946
1156
|
if (param.CacheLocal && checkResult.differentialData && LocalCacheManager.Instance.IsInitialized) {
|
|
@@ -1379,7 +1589,7 @@ export class ProviderBase {
|
|
|
1379
1589
|
// so that we can get the data to populate the entity object with.
|
|
1380
1590
|
if (params.ResultType === 'entity_object') {
|
|
1381
1591
|
// we need to get the entity definition and then get all the fields for it
|
|
1382
|
-
const entity = this.
|
|
1592
|
+
const entity = this.EntityByName(params.EntityName);
|
|
1383
1593
|
if (!entity)
|
|
1384
1594
|
throw new Error(`Entity ${params.EntityName} not found in metadata`);
|
|
1385
1595
|
params.Fields = entity.Fields.map(f => f.Name); // just override whatever was passed in with all the fields - or if nothing was passed in, we set it. For loading the entity object, we need ALL the fields.
|
|
@@ -1427,7 +1637,7 @@ export class ProviderBase {
|
|
|
1427
1637
|
// so that we can get the data to populate the entity object with.
|
|
1428
1638
|
if (param.ResultType === 'entity_object') {
|
|
1429
1639
|
// we need to get the entity definition and then get all the fields for it
|
|
1430
|
-
const entity = this.
|
|
1640
|
+
const entity = this.EntityByName(param.EntityName);
|
|
1431
1641
|
if (!entity) {
|
|
1432
1642
|
throw new Error(`Entity ${param.EntityName} not found in metadata`);
|
|
1433
1643
|
}
|
|
@@ -1519,6 +1729,22 @@ export class ProviderBase {
|
|
|
1519
1729
|
// Capture the hard-refresh flag before resetting it — when true, we must bypass all
|
|
1520
1730
|
// caching (including LocalCacheManager) so GetDatasetByName hits the actual database.
|
|
1521
1731
|
const hardRefresh = this._refresh;
|
|
1732
|
+
// ── Fast-start from cached metadata ──────────────────────────────────
|
|
1733
|
+
// On warm loads (page refresh), try loading metadata from IndexedDB/localStorage
|
|
1734
|
+
// BEFORE checking with the server. If cached metadata exists, use it immediately
|
|
1735
|
+
// so the app can start initializing (engines, UI) while we validate in the background.
|
|
1736
|
+
// This is a stale-while-revalidate pattern: serve cached data instantly, then
|
|
1737
|
+
// refresh if the server says it's outdated.
|
|
1738
|
+
if (!this.TrustLocalCacheCompletely && !hardRefresh && !this._localMetadata?.AllEntities?.length) {
|
|
1739
|
+
await this.LoadLocalMetadataFromStorage();
|
|
1740
|
+
if (this._localMetadata?.AllEntities?.length) {
|
|
1741
|
+
LogStatusEx({ message: `⚡ [Fast-Start] Loaded ${this._localMetadata.AllEntities.length} entities from local cache — deferring server validation`, verboseOnly: false });
|
|
1742
|
+
// Kick off background validation — if metadata is stale, it will
|
|
1743
|
+
// be atomically swapped when the server response arrives.
|
|
1744
|
+
this.backgroundValidateAndRefresh(providerToUse);
|
|
1745
|
+
return true; // App can proceed immediately with cached metadata
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1522
1748
|
if (hardRefresh || await this.CheckToSeeIfRefreshNeeded(providerToUse)) {
|
|
1523
1749
|
// either a hard refresh flag was set within Refresh(), or LocalMetadata is Obsolete
|
|
1524
1750
|
// first, make sure we reset the flag to false so that if another call to this function happens
|
|
@@ -1546,6 +1772,36 @@ export class ProviderBase {
|
|
|
1546
1772
|
}
|
|
1547
1773
|
return true;
|
|
1548
1774
|
}
|
|
1775
|
+
/**
|
|
1776
|
+
* Background validation for the stale-while-revalidate fast-start pattern.
|
|
1777
|
+
* Checks if local metadata is still current; if stale, fetches fresh metadata
|
|
1778
|
+
* and atomically swaps it in. The app continues operating on cached data
|
|
1779
|
+
* during this process — no blocking.
|
|
1780
|
+
*/
|
|
1781
|
+
async backgroundValidateAndRefresh(providerToUse) {
|
|
1782
|
+
try {
|
|
1783
|
+
const needsRefresh = await this.CheckToSeeIfRefreshNeeded(providerToUse);
|
|
1784
|
+
if (needsRefresh) {
|
|
1785
|
+
LogStatusEx({ message: `⚡ [Fast-Start] Background check: metadata is stale — refreshing...`, verboseOnly: false });
|
|
1786
|
+
const start = Date.now();
|
|
1787
|
+
const res = await this.GetAllMetadata(providerToUse, false);
|
|
1788
|
+
const elapsed = Date.now() - start;
|
|
1789
|
+
if (res) {
|
|
1790
|
+
this.UpdateLocalMetadata(res);
|
|
1791
|
+
this._latestLocalMetadataTimestamps = this._latestRemoteMetadataTimestamps;
|
|
1792
|
+
await this.SaveLocalMetadataToStorage();
|
|
1793
|
+
LogStatusEx({ message: `⚡ [Fast-Start] Background refresh complete (${elapsed}ms) — metadata updated in place`, verboseOnly: false });
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
else {
|
|
1797
|
+
LogStatusEx({ message: `⚡ [Fast-Start] Background check: metadata is current — no refresh needed`, verboseOnly: false });
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
catch (e) {
|
|
1801
|
+
LogError(`[Fast-Start] Background validation failed: ${e}`);
|
|
1802
|
+
// Not critical — app continues with cached metadata
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1549
1805
|
CloneAllMetadata(toClone) {
|
|
1550
1806
|
// we need to create a copy but can't do it the standard way becuase we need object instances
|
|
1551
1807
|
// for various things like EntityInfo
|
|
@@ -1717,6 +1973,26 @@ export class ProviderBase {
|
|
|
1717
1973
|
get Entities() {
|
|
1718
1974
|
return this._localMetadata.AllEntities;
|
|
1719
1975
|
}
|
|
1976
|
+
EntityByName(entityName) {
|
|
1977
|
+
if (!entityName)
|
|
1978
|
+
return undefined;
|
|
1979
|
+
const key = entityName.trim().toLowerCase();
|
|
1980
|
+
if (this._entityMapByName.size > 0) {
|
|
1981
|
+
return this._entityMapByName.get(key);
|
|
1982
|
+
}
|
|
1983
|
+
// Fallback to linear search if maps haven't been built yet
|
|
1984
|
+
return this.Entities.find(e => e.Name.trim().toLowerCase() === key);
|
|
1985
|
+
}
|
|
1986
|
+
EntityByID(entityID) {
|
|
1987
|
+
if (!entityID)
|
|
1988
|
+
return undefined;
|
|
1989
|
+
const key = NormalizeUUID(entityID);
|
|
1990
|
+
if (this._entityMapByID.size > 0) {
|
|
1991
|
+
return this._entityMapByID.get(key);
|
|
1992
|
+
}
|
|
1993
|
+
// Fallback to linear search if maps haven't been built yet
|
|
1994
|
+
return this.Entities.find(e => UUIDsEqual(e.ID, entityID));
|
|
1995
|
+
}
|
|
1720
1996
|
/**
|
|
1721
1997
|
* Gets all application metadata in the system.
|
|
1722
1998
|
* @returns Array of ApplicationInfo objects representing all applications
|
|
@@ -1986,6 +2262,15 @@ export class ProviderBase {
|
|
|
1986
2262
|
async GetAndCacheDatasetByName(datasetName, itemFilters, contextUser, providerToUse) {
|
|
1987
2263
|
// first see if we have anything in cache at all, no reason to check server dates if we dont
|
|
1988
2264
|
if (await this.IsDatasetCached(datasetName, itemFilters)) {
|
|
2265
|
+
// FastStartupMode: on warm loads, trust the cached dataset without server validation.
|
|
2266
|
+
// Same pattern as FastStartupMode for RunViews — serve from IndexedDB immediately.
|
|
2267
|
+
if (ProviderBase.FastStartupMode && !this.TrustLocalCacheCompletely) {
|
|
2268
|
+
LogStatusEx({
|
|
2269
|
+
message: `⚡ [Fast-Start] Serving cached dataset "${datasetName}" from local cache — skipping server validation`,
|
|
2270
|
+
verboseOnly: false
|
|
2271
|
+
});
|
|
2272
|
+
return this.GetCachedDataset(datasetName, itemFilters);
|
|
2273
|
+
}
|
|
1989
2274
|
// compare the local version, if exists to the server version dates
|
|
1990
2275
|
if (await this.IsDatasetCacheUpToDate(datasetName, itemFilters)) {
|
|
1991
2276
|
// we're up to date, all we need to do is get the local cache and return it
|
|
@@ -2265,6 +2550,22 @@ export class ProviderBase {
|
|
|
2265
2550
|
*/
|
|
2266
2551
|
UpdateLocalMetadata(res) {
|
|
2267
2552
|
this._localMetadata = res;
|
|
2553
|
+
this.RebuildEntityMaps();
|
|
2554
|
+
}
|
|
2555
|
+
/**
|
|
2556
|
+
* Rebuilds the O(1) entity lookup Maps from the current AllEntities array.
|
|
2557
|
+
* Called automatically from UpdateLocalMetadata().
|
|
2558
|
+
*/
|
|
2559
|
+
RebuildEntityMaps() {
|
|
2560
|
+
const entities = this._localMetadata?.AllEntities;
|
|
2561
|
+
this._entityMapByName.clear();
|
|
2562
|
+
this._entityMapByID.clear();
|
|
2563
|
+
if (entities) {
|
|
2564
|
+
for (const e of entities) {
|
|
2565
|
+
this._entityMapByName.set(e.Name.trim().toLowerCase(), e);
|
|
2566
|
+
this._entityMapByID.set(NormalizeUUID(e.ID), e);
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2268
2569
|
}
|
|
2269
2570
|
/**
|
|
2270
2571
|
* Returns the filesystem provider for the current environment.
|