@memberjunction/core 6.2.0-edge.0 → 6.2.0-edge.2
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 +180 -0
- package/dist/generic/JSONType-interfaces/IEntityConfiguration.d.ts.map +1 -1
- package/dist/generic/JSONType-interfaces/IEntityFieldConfiguration.d.ts +28 -0
- package/dist/generic/JSONType-interfaces/IEntityFieldConfiguration.d.ts.map +1 -1
- package/dist/generic/JSONType-interfaces/IEntityRelationshipConfiguration.d.ts +76 -0
- package/dist/generic/JSONType-interfaces/IEntityRelationshipConfiguration.d.ts.map +1 -1
- package/dist/generic/JSONType-interfaces/IEntitySubtypeSelectorConfig.d.ts +14 -0
- package/dist/generic/JSONType-interfaces/IEntitySubtypeSelectorConfig.d.ts.map +1 -1
- package/dist/generic/QueryCache.d.ts +17 -0
- package/dist/generic/QueryCache.d.ts.map +1 -1
- package/dist/generic/QueryCache.js +25 -5
- package/dist/generic/QueryCache.js.map +1 -1
- package/dist/generic/RegisterForStartup.d.ts +5 -5
- package/dist/generic/RegisterForStartup.js +12 -12
- package/dist/generic/applicationInfo.d.ts +4 -2
- package/dist/generic/applicationInfo.d.ts.map +1 -1
- package/dist/generic/applicationInfo.js +13 -9
- package/dist/generic/applicationInfo.js.map +1 -1
- package/dist/generic/baseEngine.d.ts +9 -1
- package/dist/generic/baseEngine.d.ts.map +1 -1
- package/dist/generic/baseEngine.js +13 -3
- package/dist/generic/baseEngine.js.map +1 -1
- package/dist/generic/baseEngineRegistry.d.ts +22 -9
- package/dist/generic/baseEngineRegistry.d.ts.map +1 -1
- package/dist/generic/baseEngineRegistry.js +43 -25
- package/dist/generic/baseEngineRegistry.js.map +1 -1
- package/dist/generic/baseEntity.d.ts +422 -28
- package/dist/generic/baseEntity.d.ts.map +1 -1
- package/dist/generic/baseEntity.js +930 -202
- package/dist/generic/baseEntity.js.map +1 -1
- package/dist/generic/baseInfo.d.ts +12 -0
- package/dist/generic/baseInfo.d.ts.map +1 -1
- package/dist/generic/baseInfo.js +22 -1
- package/dist/generic/baseInfo.js.map +1 -1
- package/dist/generic/column-descriptors.d.ts +25 -5
- package/dist/generic/column-descriptors.d.ts.map +1 -1
- package/dist/generic/column-descriptors.js +45 -5
- package/dist/generic/column-descriptors.js.map +1 -1
- package/dist/generic/compositeKey.d.ts +1 -1
- package/dist/generic/compositeKey.js +2 -2
- package/dist/generic/data-table.js +2 -2
- package/dist/generic/data-table.js.map +1 -1
- package/dist/generic/databaseProviderBase.d.ts +13 -7
- package/dist/generic/databaseProviderBase.d.ts.map +1 -1
- package/dist/generic/databaseProviderBase.js +20 -11
- package/dist/generic/databaseProviderBase.js.map +1 -1
- package/dist/generic/entityConfiguration.d.ts +1 -0
- package/dist/generic/entityConfiguration.d.ts.map +1 -1
- package/dist/generic/entityConfiguration.js.map +1 -1
- package/dist/generic/entityInfo.d.ts +199 -39
- package/dist/generic/entityInfo.d.ts.map +1 -1
- package/dist/generic/entityInfo.js +351 -78
- package/dist/generic/entityInfo.js.map +1 -1
- package/dist/generic/entitySubtypeResolver.d.ts +51 -5
- package/dist/generic/entitySubtypeResolver.d.ts.map +1 -1
- package/dist/generic/entitySubtypeResolver.js +50 -3
- package/dist/generic/entitySubtypeResolver.js.map +1 -1
- package/dist/generic/filters/compositeFilter.d.ts +19 -19
- package/dist/generic/filters/compositeFilter.js +60 -60
- package/dist/generic/graphqlTypeNames.d.ts +6 -0
- package/dist/generic/graphqlTypeNames.d.ts.map +1 -1
- package/dist/generic/graphqlTypeNames.js +18 -6
- package/dist/generic/graphqlTypeNames.js.map +1 -1
- package/dist/generic/interfaces.d.ts +13 -4
- package/dist/generic/interfaces.d.ts.map +1 -1
- package/dist/generic/interfaces.js +3 -3
- package/dist/generic/interfaces.js.map +1 -1
- package/dist/generic/logging.d.ts +2 -0
- package/dist/generic/logging.d.ts.map +1 -1
- package/dist/generic/logging.js +7 -3
- package/dist/generic/logging.js.map +1 -1
- package/dist/generic/metadata.d.ts +1 -1
- package/dist/generic/metadata.js +3 -3
- package/dist/generic/providerBase.d.ts +89 -8
- package/dist/generic/providerBase.d.ts.map +1 -1
- package/dist/generic/providerBase.js +180 -45
- package/dist/generic/providerBase.js.map +1 -1
- package/dist/generic/queryResultEnricher.d.ts +2 -0
- package/dist/generic/queryResultEnricher.d.ts.map +1 -1
- package/dist/generic/queryResultEnricher.js +5 -1
- package/dist/generic/queryResultEnricher.js.map +1 -1
- package/dist/generic/querySQLFilters.d.ts +4 -0
- package/dist/generic/querySQLFilters.d.ts.map +1 -1
- package/dist/generic/querySQLFilters.js +10 -2
- package/dist/generic/querySQLFilters.js.map +1 -1
- package/dist/generic/relatedRecordCollection.d.ts +2 -0
- package/dist/generic/relatedRecordCollection.d.ts.map +1 -1
- package/dist/generic/relatedRecordCollection.js +5 -1
- package/dist/generic/relatedRecordCollection.js.map +1 -1
- package/dist/generic/remoteEventRow.d.ts +55 -0
- package/dist/generic/remoteEventRow.d.ts.map +1 -0
- package/dist/generic/remoteEventRow.js +110 -0
- package/dist/generic/remoteEventRow.js.map +1 -0
- package/dist/generic/remoteOperationDispatch.d.ts +2 -0
- package/dist/generic/remoteOperationDispatch.d.ts.map +1 -1
- package/dist/generic/remoteOperationDispatch.js +5 -1
- package/dist/generic/remoteOperationDispatch.js.map +1 -1
- package/dist/generic/runQuerySQLFilterImplementations.d.ts +8 -0
- package/dist/generic/runQuerySQLFilterImplementations.d.ts.map +1 -1
- package/dist/generic/runQuerySQLFilterImplementations.js +20 -4
- package/dist/generic/runQuerySQLFilterImplementations.js.map +1 -1
- package/dist/generic/schemaHydration.d.ts +8 -0
- package/dist/generic/schemaHydration.d.ts.map +1 -1
- package/dist/generic/schemaHydration.js +21 -5
- package/dist/generic/schemaHydration.js.map +1 -1
- package/dist/generic/securityInfo.d.ts +11 -9
- package/dist/generic/securityInfo.d.ts.map +1 -1
- package/dist/generic/securityInfo.js +31 -27
- package/dist/generic/securityInfo.js.map +1 -1
- package/dist/generic/telemetryManager.d.ts +16 -0
- package/dist/generic/telemetryManager.d.ts.map +1 -1
- package/dist/generic/telemetryManager.js +52 -20
- package/dist/generic/telemetryManager.js.map +1 -1
- package/dist/generic/transactionGroup.d.ts +1 -1
- package/dist/generic/transactionGroup.js +4 -4
- package/dist/generic/transactionGroup.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/dist/views/viewInfo.d.ts +2 -2
- package/dist/views/viewInfo.js +13 -13
- package/dist/views/viewInfo.js.map +1 -1
- package/package.json +3 -3
|
@@ -195,7 +195,20 @@ export class ProviderBase {
|
|
|
195
195
|
this._metadataReloadInFlight = null;
|
|
196
196
|
/** Set when a refresh request arrives while a reload is in flight; the reload loop reruns once. */
|
|
197
197
|
this._metadataReloadQueued = false;
|
|
198
|
+
/** When the last SUCCESSFUL refresh check's status request completed; 0 = never. */
|
|
198
199
|
this._lastRefreshCheckAt = 0;
|
|
200
|
+
/**
|
|
201
|
+
* The check currently running in {@link CheckToSeeIfRefreshNeeded}; null when none is. While
|
|
202
|
+
* set, a non-bypassing caller is answered `false` without a request — the owner of this check
|
|
203
|
+
* acts on its answer. Held as the promise (not a flag) so only its owner clears it.
|
|
204
|
+
*/
|
|
205
|
+
this._refreshCheckInFlight = null;
|
|
206
|
+
/**
|
|
207
|
+
* Set when Config() adopts a snapshot it loaded from the server (which fetched the current user
|
|
208
|
+
* with it); consumed by the next {@link preValidateAndRefresh}, which then has nothing to check.
|
|
209
|
+
*/
|
|
210
|
+
this._configLoadedFromServer = false;
|
|
211
|
+
this._lastMetadataLoadError = null;
|
|
199
212
|
/**
|
|
200
213
|
* Pending coalesced requests waiting to be flushed.
|
|
201
214
|
* Each entry tracks the original params, contextUser, and a resolver
|
|
@@ -227,6 +240,12 @@ export class ProviderBase {
|
|
|
227
240
|
* hardcoded anywhere in this mechanism.
|
|
228
241
|
*/
|
|
229
242
|
this._metadataDatasetEntityNames = null;
|
|
243
|
+
/**
|
|
244
|
+
* True only while {@link RefreshWithinTransaction} is reloading. The metadata dataset is
|
|
245
|
+
* normally read on the pool (#4514); this window joins those reads to the caller's transaction
|
|
246
|
+
* so a push can see the metadata rows it has not committed yet (MJ#4836).
|
|
247
|
+
*/
|
|
248
|
+
this._metadataReadsJoinTransaction = false;
|
|
230
249
|
/** Debounce timer for {@link scheduleMetadataMemberRefresh}. */
|
|
231
250
|
this._metadataMemberRefreshTimer = null;
|
|
232
251
|
this._metadataMemberRefreshWaits = 0;
|
|
@@ -253,9 +272,22 @@ export class ProviderBase {
|
|
|
253
272
|
* Minimum interval (ms) between metadata refresh checks to prevent
|
|
254
273
|
* redundant network calls when Config()/RefreshIfNeeded() fire in
|
|
255
274
|
* quick succession (e.g., multiple engines during startup).
|
|
275
|
+
* The throttle is armed only by a check whose status request succeeded — a check
|
|
276
|
+
* that threw or got no timestamps proved nothing, so the next caller checks again.
|
|
277
|
+
* A caller arriving while a check is in flight is told no refresh is needed, as the
|
|
278
|
+
* in-flight owner acts on the answer (see {@link CheckToSeeIfRefreshNeeded}).
|
|
256
279
|
* Does NOT affect forced Refresh() calls. Default: 30 000 ms.
|
|
257
280
|
*/
|
|
258
281
|
static { this.MinRefreshCheckIntervalMs = 30000; }
|
|
282
|
+
/**
|
|
283
|
+
* Why the most recent metadata download ({@link GetAllMetadata}) failed; null when it succeeded
|
|
284
|
+
* or none has run. GetAllMetadata logs and returns undefined on failure so a background refresh
|
|
285
|
+
* keeps the last good graph; a boot that ends with no metadata reads this to report the cause
|
|
286
|
+
* (e.g. a user with no roles cannot read `MJ: User Roles`) instead of a generic "nothing loaded".
|
|
287
|
+
*/
|
|
288
|
+
get LastMetadataLoadError() {
|
|
289
|
+
return this._lastMetadataLoadError;
|
|
290
|
+
}
|
|
259
291
|
// ── Server-Side Auto-Cache ────────────────────────────────────────
|
|
260
292
|
/**
|
|
261
293
|
* Maximum row count for auto-caching on the server side. When
|
|
@@ -453,6 +485,26 @@ export class ProviderBase {
|
|
|
453
485
|
this._metadataDatasetEntityNames = names;
|
|
454
486
|
this.ensureInflightViewInvalidation();
|
|
455
487
|
}
|
|
488
|
+
/**
|
|
489
|
+
* Whether `entityName` is one of the entities this provider's metadata is built from.
|
|
490
|
+
* False when the membership set has not been loaded yet — a caller must not treat "unknown"
|
|
491
|
+
* as "reload". Names are compared case-insensitively, matching the set recorded by
|
|
492
|
+
* {@link registerMetadataDatasetMembership}.
|
|
493
|
+
*/
|
|
494
|
+
IsMetadataDatasetMember(entityName) {
|
|
495
|
+
const name = entityName?.trim().toLowerCase();
|
|
496
|
+
if (!name || !this._metadataDatasetEntityNames) {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
return this._metadataDatasetEntityNames.has(name);
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* True while {@link RefreshWithinTransaction} is running, so metadata-dataset reads join the
|
|
503
|
+
* ambient transaction instead of the pool.
|
|
504
|
+
*/
|
|
505
|
+
get MetadataReadsJoinTransaction() {
|
|
506
|
+
return this._metadataReadsJoinTransaction;
|
|
507
|
+
}
|
|
456
508
|
/**
|
|
457
509
|
* Static fan-out callback: a BaseEntity save/delete (or a remote-invalidate from another
|
|
458
510
|
* server) touched `lowerEntityName`. If that entity is one of the entities this provider's
|
|
@@ -876,8 +928,8 @@ export class ProviderBase {
|
|
|
876
928
|
// CALLER's objects — reusing params across calls must be safe.
|
|
877
929
|
params = params.map(p => ({ ...p }));
|
|
878
930
|
// Bypass dedup for side-effect calls (SaveViewResults creates DB records)
|
|
879
|
-
if (this.
|
|
880
|
-
return this.
|
|
931
|
+
if (this.shouldBypassDedup(params)) {
|
|
932
|
+
return this.executeRunViewsPipeline(params, contextUser);
|
|
881
933
|
}
|
|
882
934
|
// ── Coalescing: merge concurrent RunViews into one mega-batch ──
|
|
883
935
|
if (ProviderBase.CoalesceWindowMs > 0 && !this.TrustLocalCacheCompletely) {
|
|
@@ -885,7 +937,7 @@ export class ProviderBase {
|
|
|
885
937
|
// Server-side trusts its cache (no network calls), so coalescing adds no benefit.
|
|
886
938
|
return this.enqueueCoalescedRunViews(params, contextUser);
|
|
887
939
|
}
|
|
888
|
-
const key = this.
|
|
940
|
+
const key = this.generateDedupKey(params, contextUser);
|
|
889
941
|
const existing = this._inflightViews.get(key);
|
|
890
942
|
// ── Linger hit: resolved result still within the linger window ──
|
|
891
943
|
if (existing?.resolvedResults && existing.resolvedAt) {
|
|
@@ -896,7 +948,7 @@ export class ProviderBase {
|
|
|
896
948
|
message: `[Dedup] Linger hit for [${entities}] — returning cached result (age ${age}ms, window ${ProviderBase.DedupLingerMs}ms)`,
|
|
897
949
|
verboseOnly: true
|
|
898
950
|
});
|
|
899
|
-
return existing.resolvedResults.map(r => this.
|
|
951
|
+
return existing.resolvedResults.map(r => this.shallowCopyResult(r));
|
|
900
952
|
}
|
|
901
953
|
// Linger expired — fall through to fresh execution
|
|
902
954
|
this._inflightViews.delete(key);
|
|
@@ -909,10 +961,10 @@ export class ProviderBase {
|
|
|
909
961
|
verboseOnly: true
|
|
910
962
|
});
|
|
911
963
|
const results = await existing.promise;
|
|
912
|
-
return results.map(r => this.
|
|
964
|
+
return results.map(r => this.shallowCopyResult(r));
|
|
913
965
|
}
|
|
914
966
|
// ── Fresh execution ──
|
|
915
|
-
const promise = this.
|
|
967
|
+
const promise = this.executeRunViewsPipeline(params, contextUser)
|
|
916
968
|
.then(results => {
|
|
917
969
|
// Stash resolved results for the linger window. Safety cap: under
|
|
918
970
|
// extreme churn (hundreds of distinct keys resolving within one linger
|
|
@@ -948,14 +1000,14 @@ export class ProviderBase {
|
|
|
948
1000
|
this.ensureInflightViewInvalidation();
|
|
949
1001
|
this._inflightViews.set(key, { promise, entityNames: this.collectParamEntityNames(params) });
|
|
950
1002
|
const results = await promise;
|
|
951
|
-
return results.map(r => this.
|
|
1003
|
+
return results.map(r => this.shallowCopyResult(r));
|
|
952
1004
|
}
|
|
953
1005
|
// ── Dedup helpers ──────────────────────────────────────────────────
|
|
954
1006
|
/**
|
|
955
1007
|
* The original RunViews execution pipeline (pre-processing, cache,
|
|
956
1008
|
* internal execution, post-processing).
|
|
957
1009
|
*/
|
|
958
|
-
async
|
|
1010
|
+
async executeRunViewsPipeline(params, contextUser) {
|
|
959
1011
|
// Pre-processing for batch
|
|
960
1012
|
const preResult = await this.PreRunViews(params, contextUser);
|
|
961
1013
|
// Check for smart cache check mode
|
|
@@ -1027,7 +1079,7 @@ export class ProviderBase {
|
|
|
1027
1079
|
if (queue.length === 1) {
|
|
1028
1080
|
const entry = queue[0];
|
|
1029
1081
|
try {
|
|
1030
|
-
const results = await this.
|
|
1082
|
+
const results = await this.runViewsUncoalesced(entry.params, entry.contextUser);
|
|
1031
1083
|
entry.resolve(results);
|
|
1032
1084
|
}
|
|
1033
1085
|
catch (err) {
|
|
@@ -1047,7 +1099,7 @@ export class ProviderBase {
|
|
|
1047
1099
|
for (const entry of queue) {
|
|
1048
1100
|
const entryIndices = [];
|
|
1049
1101
|
for (const param of entry.params) {
|
|
1050
|
-
const key = this.
|
|
1102
|
+
const key = this.generateDedupKey([param], contextUser);
|
|
1051
1103
|
let idx = uniqueKeys.get(key);
|
|
1052
1104
|
if (idx === undefined) {
|
|
1053
1105
|
idx = uniqueParams.length;
|
|
@@ -1076,12 +1128,12 @@ export class ProviderBase {
|
|
|
1076
1128
|
});
|
|
1077
1129
|
try {
|
|
1078
1130
|
// Execute the deduplicated mega-batch as a single pipeline call
|
|
1079
|
-
const uniqueResults = await this.
|
|
1131
|
+
const uniqueResults = await this.runViewsUncoalesced(uniqueParams, contextUser);
|
|
1080
1132
|
// Route deduped results back to each original caller, preserving order.
|
|
1081
1133
|
// ShallowCopyResult gives each caller an independent Results array (rows
|
|
1082
1134
|
// are still shared refs; callers should not mutate rows in place).
|
|
1083
1135
|
for (let i = 0; i < queue.length; i++) {
|
|
1084
|
-
const callerResults = callerIndexMaps[i].map(idx => this.
|
|
1136
|
+
const callerResults = callerIndexMaps[i].map(idx => this.shallowCopyResult(uniqueResults[idx]));
|
|
1085
1137
|
queue[i].resolve(callerResults);
|
|
1086
1138
|
}
|
|
1087
1139
|
TelemetryManager.Instance.EndEvent(eventId);
|
|
@@ -1208,24 +1260,24 @@ export class ProviderBase {
|
|
|
1208
1260
|
const textField = entity.Fields.find(f => f.Type.toLowerCase().includes('varchar') || f.Type.toLowerCase().includes('text'));
|
|
1209
1261
|
return textField?.Name ?? entity.FirstPrimaryKey.Name; // first-pk-ok: display-column fallback for FTS results, not a key construction
|
|
1210
1262
|
}
|
|
1211
|
-
async
|
|
1212
|
-
const key = this.
|
|
1263
|
+
async runViewsUncoalesced(params, contextUser) {
|
|
1264
|
+
const key = this.generateDedupKey(params, contextUser);
|
|
1213
1265
|
const existing = this._inflightViews.get(key);
|
|
1214
1266
|
// ── Linger hit ──
|
|
1215
1267
|
if (existing?.resolvedResults && existing.resolvedAt) {
|
|
1216
1268
|
const age = Date.now() - existing.resolvedAt;
|
|
1217
1269
|
if (age < ProviderBase.DedupLingerMs) {
|
|
1218
|
-
return existing.resolvedResults.map(r => this.
|
|
1270
|
+
return existing.resolvedResults.map(r => this.shallowCopyResult(r));
|
|
1219
1271
|
}
|
|
1220
1272
|
this._inflightViews.delete(key);
|
|
1221
1273
|
}
|
|
1222
1274
|
// ── In-flight hit ──
|
|
1223
1275
|
if (existing && !existing.resolvedResults) {
|
|
1224
1276
|
const results = await existing.promise;
|
|
1225
|
-
return results.map(r => this.
|
|
1277
|
+
return results.map(r => this.shallowCopyResult(r));
|
|
1226
1278
|
}
|
|
1227
1279
|
// ── Fresh execution ──
|
|
1228
|
-
const promise = this.
|
|
1280
|
+
const promise = this.executeRunViewsPipeline(params, contextUser)
|
|
1229
1281
|
.then(results => {
|
|
1230
1282
|
const entry = this._inflightViews.get(key);
|
|
1231
1283
|
if (entry && entry.promise === promise) {
|
|
@@ -1252,7 +1304,7 @@ export class ProviderBase {
|
|
|
1252
1304
|
this.ensureInflightViewInvalidation();
|
|
1253
1305
|
this._inflightViews.set(key, { promise, entityNames: this.collectParamEntityNames(params) });
|
|
1254
1306
|
const results = await promise;
|
|
1255
|
-
return results.map(r => this.
|
|
1307
|
+
return results.map(r => this.shallowCopyResult(r));
|
|
1256
1308
|
}
|
|
1257
1309
|
/**
|
|
1258
1310
|
* Generates a deterministic dedup key for a batch of RunViewParams.
|
|
@@ -1384,11 +1436,11 @@ export class ProviderBase {
|
|
|
1384
1436
|
}
|
|
1385
1437
|
return result;
|
|
1386
1438
|
}
|
|
1387
|
-
|
|
1439
|
+
generateDedupKey(params, contextUser) {
|
|
1388
1440
|
const parts = params.map(p => {
|
|
1389
1441
|
const base = LocalCacheManager.Instance.GenerateRunViewFingerprint(p, this.InstanceConnectionString);
|
|
1390
1442
|
const extras = [
|
|
1391
|
-
ProviderBase.
|
|
1443
|
+
ProviderBase.normalizeFieldsKey(p.Fields),
|
|
1392
1444
|
p.ResultType ?? 'simple',
|
|
1393
1445
|
p.UserSearchString ?? '',
|
|
1394
1446
|
p.ViewID ?? '',
|
|
@@ -1406,7 +1458,7 @@ export class ProviderBase {
|
|
|
1406
1458
|
* semantically identical requests collapse to the same key. Used by both the
|
|
1407
1459
|
* request-dedup key and the client-side cache fingerprint.
|
|
1408
1460
|
*/
|
|
1409
|
-
static
|
|
1461
|
+
static normalizeFieldsKey(fields) {
|
|
1410
1462
|
return fields && fields.length > 0
|
|
1411
1463
|
? fields.map(f => f.trim().toLowerCase()).sort().join(',')
|
|
1412
1464
|
: '*';
|
|
@@ -1460,7 +1512,7 @@ export class ProviderBase {
|
|
|
1460
1512
|
// This touches ONLY how the slot is keyed. param.Fields is left untouched, so what the
|
|
1461
1513
|
// provider FETCHES is unchanged — an earlier attempt cleared param.Fields instead and was
|
|
1462
1514
|
// reverted precisely because fetch behavior could not be verified.
|
|
1463
|
-
const fieldsKey = this.isFullCoverageFieldList(param) ? '*' : ProviderBase.
|
|
1515
|
+
const fieldsKey = this.isFullCoverageFieldList(param) ? '*' : ProviderBase.normalizeFieldsKey(param.Fields);
|
|
1464
1516
|
const fingerprint = `${base}|f:${fieldsKey}`;
|
|
1465
1517
|
this._clientFingerprintMemo.set(param, fingerprint);
|
|
1466
1518
|
return fingerprint;
|
|
@@ -1747,7 +1799,7 @@ export class ProviderBase {
|
|
|
1747
1799
|
* which means the call has a side effect (creating UserViewRun records)
|
|
1748
1800
|
* and must not be deduplicated.
|
|
1749
1801
|
*/
|
|
1750
|
-
|
|
1802
|
+
shouldBypassDedup(params) {
|
|
1751
1803
|
// BypassCache:true MUST bypass the dedup-linger cache as well —
|
|
1752
1804
|
// otherwise the "skip the cache to see DB truth" contract leaks: a
|
|
1753
1805
|
// recent identical RunView (within DedupLingerMs) would return its
|
|
@@ -1764,7 +1816,7 @@ export class ProviderBase {
|
|
|
1764
1816
|
* new array instance (protecting against push/sort/splice by other
|
|
1765
1817
|
* callers) but the individual row objects inside are shared references.
|
|
1766
1818
|
*/
|
|
1767
|
-
|
|
1819
|
+
shallowCopyResult(result) {
|
|
1768
1820
|
return {
|
|
1769
1821
|
...result,
|
|
1770
1822
|
Results: [...result.Results]
|
|
@@ -2330,7 +2382,7 @@ export class ProviderBase {
|
|
|
2330
2382
|
if (denied.size === 0) {
|
|
2331
2383
|
return rows;
|
|
2332
2384
|
}
|
|
2333
|
-
return ProviderBase.
|
|
2385
|
+
return ProviderBase.omitFieldsFromRows(rows, denied);
|
|
2334
2386
|
}
|
|
2335
2387
|
/**
|
|
2336
2388
|
* Row-loop half of {@link ApplyFieldSecurityProjection}, split out so the policy above reads
|
|
@@ -2340,7 +2392,7 @@ export class ProviderBase {
|
|
|
2340
2392
|
*
|
|
2341
2393
|
* @param deniedLowercase field names to omit, already lowercased
|
|
2342
2394
|
*/
|
|
2343
|
-
static
|
|
2395
|
+
static omitFieldsFromRows(rows, deniedLowercase) {
|
|
2344
2396
|
const probe = rows[0];
|
|
2345
2397
|
if (!probe || typeof probe !== 'object') {
|
|
2346
2398
|
return rows;
|
|
@@ -3879,6 +3931,9 @@ export class ProviderBase {
|
|
|
3879
3931
|
*/
|
|
3880
3932
|
async Config(data, providerToUse) {
|
|
3881
3933
|
this._ConfigData = data;
|
|
3934
|
+
// Describes THIS Config only: a flag left by an earlier Refresh() must not make the
|
|
3935
|
+
// pre-validation that follows a Config which loaded nothing skip its check.
|
|
3936
|
+
this._configLoadedFromServer = false;
|
|
3882
3937
|
// Initialize LocalCacheManager early so dataset loading can use the cache.
|
|
3883
3938
|
// Initialize() is idempotent — subsequent calls (e.g. from StartupManager) are no-ops.
|
|
3884
3939
|
if (!LocalCacheManager.Instance.IsInitialized) {
|
|
@@ -3924,7 +3979,12 @@ export class ProviderBase {
|
|
|
3924
3979
|
return true; // App can proceed immediately with cached metadata
|
|
3925
3980
|
}
|
|
3926
3981
|
}
|
|
3927
|
-
|
|
3982
|
+
// An empty graph has nothing to throttle: the throttle exists to stop redundant checks of a
|
|
3983
|
+
// snapshot we already hold. After a status check succeeded but the download that followed
|
|
3984
|
+
// failed, the throttle is armed and a non-bypassing retry would read "current" and load
|
|
3985
|
+
// nothing for the whole window (#4887).
|
|
3986
|
+
const graphIsEmpty = !this._localMetadata?.AllEntities?.length;
|
|
3987
|
+
if (hardRefresh || await this.CheckToSeeIfRefreshNeeded(providerToUse, graphIsEmpty)) {
|
|
3928
3988
|
// either a hard refresh flag was set within Refresh(), or LocalMetadata is Obsolete
|
|
3929
3989
|
// first, make sure we reset the flag to false so that if another call to this function happens
|
|
3930
3990
|
// while we are waiting for the async call to finish, we dont do it again
|
|
@@ -3963,13 +4023,8 @@ export class ProviderBase {
|
|
|
3963
4023
|
const end = new Date().getTime();
|
|
3964
4024
|
LogStatusEx({ message: `GetAllMetadata() took ${end - start} ms`, verboseOnly: true });
|
|
3965
4025
|
if (res) {
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
// Uses UpdateLocalMetadata() to maintain consistency with LoadLocalMetadataFromStorage()
|
|
3969
|
-
// and allow potential subclass overrides for extensibility
|
|
3970
|
-
this.UpdateLocalMetadata(res);
|
|
3971
|
-
this._latestLocalMetadataTimestamps = this._latestRemoteMetadataTimestamps; // update this since we just used server to get all the stuff
|
|
3972
|
-
await this.SaveLocalMetadataToStorage();
|
|
4026
|
+
await this.adoptServerMetadata(res);
|
|
4027
|
+
this._configLoadedFromServer = true;
|
|
3973
4028
|
}
|
|
3974
4029
|
else {
|
|
3975
4030
|
// GetAllMetadata failed - log error but keep existing metadata
|
|
@@ -3989,6 +4044,19 @@ export class ProviderBase {
|
|
|
3989
4044
|
}
|
|
3990
4045
|
return true;
|
|
3991
4046
|
}
|
|
4047
|
+
/**
|
|
4048
|
+
* Adopts a metadata snapshot just loaded from the server: swaps it in, records that the
|
|
4049
|
+
* local copy now matches the server's timestamps, and persists it to local storage.
|
|
4050
|
+
*
|
|
4051
|
+
* The swap goes through UpdateLocalMetadata — a single property assignment, atomic in
|
|
4052
|
+
* JavaScript, so readers see the old snapshot until the new one is complete — which keeps
|
|
4053
|
+
* it consistent with LoadLocalMetadataFromStorage() and lets subclasses override it.
|
|
4054
|
+
*/
|
|
4055
|
+
async adoptServerMetadata(res) {
|
|
4056
|
+
this.UpdateLocalMetadata(res);
|
|
4057
|
+
this._latestLocalMetadataTimestamps = this._latestRemoteMetadataTimestamps; // we just used the server to get all the stuff
|
|
4058
|
+
await this.SaveLocalMetadataToStorage();
|
|
4059
|
+
}
|
|
3992
4060
|
/**
|
|
3993
4061
|
* Background validation for the stale-while-revalidate fast-start pattern.
|
|
3994
4062
|
* Checks if local metadata is still current; if stale, fetches fresh metadata
|
|
@@ -4004,9 +4072,7 @@ export class ProviderBase {
|
|
|
4004
4072
|
const res = await this.GetAllMetadata(providerToUse, false);
|
|
4005
4073
|
const elapsed = Date.now() - start;
|
|
4006
4074
|
if (res) {
|
|
4007
|
-
this.
|
|
4008
|
-
this._latestLocalMetadataTimestamps = this._latestRemoteMetadataTimestamps;
|
|
4009
|
-
await this.SaveLocalMetadataToStorage();
|
|
4075
|
+
await this.adoptServerMetadata(res);
|
|
4010
4076
|
LogStatusEx({ message: `⚡ [Metadata Cache] Background refresh complete (${elapsed}ms) — metadata updated in place`, verboseOnly: false });
|
|
4011
4077
|
}
|
|
4012
4078
|
}
|
|
@@ -4037,9 +4103,22 @@ export class ProviderBase {
|
|
|
4037
4103
|
* on RTT). On the warm-stale path we additionally pay the full metadata fetch but
|
|
4038
4104
|
* avoid serving stale data to the UI in the first place.
|
|
4039
4105
|
*
|
|
4106
|
+
* On a cold boot, where Config() just loaded the graph (and the current user) from the
|
|
4107
|
+
* server, the first call after that load returns immediately — no check, no current-user
|
|
4108
|
+
* re-fetch. Later calls, and calls after a background refresh, check as usual.
|
|
4109
|
+
*
|
|
4040
4110
|
* Caller contract: invoke this before `StartupManager.Startup()`.
|
|
4041
4111
|
*/
|
|
4042
4112
|
async preValidateAndRefresh(providerToUse) {
|
|
4113
|
+
// The snapshot Config just loaded from the server (a cold boot) is current by construction,
|
|
4114
|
+
// and GetAllMetadata fetched CurrentUser with it. Checking again would hit the refresh
|
|
4115
|
+
// throttle, read "current", and re-fetch the same user serially (#4887). One-shot: only the
|
|
4116
|
+
// first pre-validation after that load skips.
|
|
4117
|
+
if (this._configLoadedFromServer) {
|
|
4118
|
+
this._configLoadedFromServer = false;
|
|
4119
|
+
LogStatusEx({ message: `⚡ [Metadata Cache] Pre-validation: metadata was just loaded from the server — skipping`, verboseOnly: true });
|
|
4120
|
+
return;
|
|
4121
|
+
}
|
|
4043
4122
|
try {
|
|
4044
4123
|
const needsRefresh = await this.CheckToSeeIfRefreshNeeded(providerToUse);
|
|
4045
4124
|
if (needsRefresh) {
|
|
@@ -4048,9 +4127,7 @@ export class ProviderBase {
|
|
|
4048
4127
|
const res = await this.GetAllMetadata(providerToUse, false);
|
|
4049
4128
|
const elapsed = Date.now() - start;
|
|
4050
4129
|
if (res) {
|
|
4051
|
-
this.
|
|
4052
|
-
this._latestLocalMetadataTimestamps = this._latestRemoteMetadataTimestamps;
|
|
4053
|
-
await this.SaveLocalMetadataToStorage();
|
|
4130
|
+
await this.adoptServerMetadata(res);
|
|
4054
4131
|
LogStatusEx({ message: `⚡ [Metadata Cache] Pre-validation refresh complete (${elapsed}ms)`, verboseOnly: false });
|
|
4055
4132
|
}
|
|
4056
4133
|
}
|
|
@@ -4213,7 +4290,7 @@ export class ProviderBase {
|
|
|
4213
4290
|
const entitiesItem = d?.Success ? d.Results?.find(r => r.Code === 'Entities') : undefined;
|
|
4214
4291
|
const hasEntities = Array.isArray(entitiesItem?.Results) && entitiesItem.Results.length > 0;
|
|
4215
4292
|
if (d && d.Success && !hasEntities) {
|
|
4216
|
-
|
|
4293
|
+
this.recordMetadataLoadFailure(new Error(`GetAllMetadata() - the ${ProviderBase._mjMetadataDatasetName} dataset returned no entities; keeping the metadata already loaded`));
|
|
4217
4294
|
}
|
|
4218
4295
|
else if (d && d.Success) {
|
|
4219
4296
|
// cache the dataset for anyone who wants to use it
|
|
@@ -4241,16 +4318,23 @@ export class ProviderBase {
|
|
|
4241
4318
|
// build the base return type
|
|
4242
4319
|
const returnMetadata = MetadataFromSimpleObjectWithoutUser(simpleMetadata, this);
|
|
4243
4320
|
returnMetadata.CurrentUser = await this.GetCurrentUser();
|
|
4321
|
+
this._lastMetadataLoadError = null;
|
|
4244
4322
|
return returnMetadata;
|
|
4245
4323
|
}
|
|
4246
4324
|
else {
|
|
4247
|
-
|
|
4325
|
+
this.recordMetadataLoadFailure(new Error('GetAllMetadata() - Error getting metadata from server' + (d ? ': ' + d.Status : '')));
|
|
4248
4326
|
}
|
|
4249
4327
|
}
|
|
4250
4328
|
catch (e) {
|
|
4251
4329
|
LogError(e);
|
|
4330
|
+
this._lastMetadataLoadError = e instanceof Error ? e : new Error(String(e));
|
|
4252
4331
|
}
|
|
4253
4332
|
}
|
|
4333
|
+
/** Logs a failed metadata download and keeps it for {@link LastMetadataLoadError}. */
|
|
4334
|
+
recordMetadataLoadFailure(error) {
|
|
4335
|
+
LogError(error.message);
|
|
4336
|
+
this._lastMetadataLoadError = error;
|
|
4337
|
+
}
|
|
4254
4338
|
/**
|
|
4255
4339
|
* Refreshes the CurrentUser from the server and updates local metadata in place.
|
|
4256
4340
|
* Useful on warm boot or when user roles/permissions change dynamically without
|
|
@@ -4524,6 +4608,24 @@ export class ProviderBase {
|
|
|
4524
4608
|
else
|
|
4525
4609
|
return true; // subclass is telling us not to do any refresh ops right now
|
|
4526
4610
|
}
|
|
4611
|
+
/**
|
|
4612
|
+
* The same hard reload as {@link Refresh}, but the metadata dataset's reads join the ambient
|
|
4613
|
+
* transaction for the duration of the call (MJ#4836).
|
|
4614
|
+
*
|
|
4615
|
+
* A timer-driven refresh must stay on the pool so it cannot land beside COMMIT (#4514).
|
|
4616
|
+
* The caller that owns the transaction — `mj sync push`, between its own writes — is the
|
|
4617
|
+
* one case that has to see its uncommitted metadata rows, and it waits for this call, so
|
|
4618
|
+
* the read cannot race the commit.
|
|
4619
|
+
*/
|
|
4620
|
+
async RefreshWithinTransaction(providerToUse) {
|
|
4621
|
+
this._metadataReadsJoinTransaction = true;
|
|
4622
|
+
try {
|
|
4623
|
+
return await this.Refresh(providerToUse);
|
|
4624
|
+
}
|
|
4625
|
+
finally {
|
|
4626
|
+
this._metadataReadsJoinTransaction = false;
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4527
4629
|
/**
|
|
4528
4630
|
* Checks if local metadata is out of date and needs refreshing.
|
|
4529
4631
|
* Compares local timestamps with server timestamps.
|
|
@@ -4531,12 +4633,28 @@ export class ProviderBase {
|
|
|
4531
4633
|
* throttle. Event-driven callers pass true: they hold positive evidence that a metadata
|
|
4532
4634
|
* member entity was just written, and the throttle otherwise answers "fresh" for any check
|
|
4533
4635
|
* arriving within the window of the previous one — which would silently drop the second of
|
|
4534
|
-
* two permission changes made less than the window apart.
|
|
4636
|
+
* two permission changes made less than the window apart. For the same reason a bypassing
|
|
4637
|
+
* caller always runs its own check, even while another is in flight: that check may have
|
|
4638
|
+
* started before the write.
|
|
4639
|
+
*
|
|
4640
|
+
* The throttle is armed only when the status request succeeded (returned timestamps). A
|
|
4641
|
+
* check that throws or returns no timestamps leaves it unarmed, so a retried boot inside
|
|
4642
|
+
* the window really checks instead of reading "current" and loading nothing (#4887).
|
|
4643
|
+
* A non-bypassing caller arriving while a check is in flight is told no refresh is needed,
|
|
4644
|
+
* as the in-flight owner acts on the answer; that keeps N engines starting together down to
|
|
4645
|
+
* one request. It must not share the owner's answer: a shared `true` would send every caller
|
|
4646
|
+
* into its own full metadata reload (preValidateAndRefresh / backgroundValidateAndRefresh /
|
|
4647
|
+
* RefreshIfNeeded load outside Config's reload single-flight). For the same reason it gets
|
|
4648
|
+
* `false`, not the owner's rejection, when the owner's check throws.
|
|
4535
4649
|
* @returns True if refresh is needed, false otherwise
|
|
4536
4650
|
*/
|
|
4537
4651
|
async CheckToSeeIfRefreshNeeded(providerToUse, bypassMinCheckInterval) {
|
|
4538
4652
|
if (!this.AllowRefresh)
|
|
4539
4653
|
return false;
|
|
4654
|
+
if (!bypassMinCheckInterval && this._refreshCheckInFlight) {
|
|
4655
|
+
LogStatusEx({ message: `[RefreshCheck] Skipped — a check is already in flight; its caller acts on the answer`, verboseOnly: true });
|
|
4656
|
+
return false;
|
|
4657
|
+
}
|
|
4540
4658
|
const now = Date.now();
|
|
4541
4659
|
if (!bypassMinCheckInterval && (now - this._lastRefreshCheckAt) < ProviderBase.MinRefreshCheckIntervalMs) {
|
|
4542
4660
|
LogStatusEx({
|
|
@@ -4545,8 +4663,25 @@ export class ProviderBase {
|
|
|
4545
4663
|
});
|
|
4546
4664
|
return false;
|
|
4547
4665
|
}
|
|
4548
|
-
|
|
4549
|
-
|
|
4666
|
+
const check = this.runRefreshCheck(providerToUse);
|
|
4667
|
+
this._refreshCheckInFlight = check;
|
|
4668
|
+
try {
|
|
4669
|
+
return await check;
|
|
4670
|
+
}
|
|
4671
|
+
finally {
|
|
4672
|
+
// A bypassing caller may have replaced the slot with its own newer check while this
|
|
4673
|
+
// one ran; only the check that still owns the slot clears it.
|
|
4674
|
+
if (this._refreshCheckInFlight === check) {
|
|
4675
|
+
this._refreshCheckInFlight = null;
|
|
4676
|
+
}
|
|
4677
|
+
}
|
|
4678
|
+
}
|
|
4679
|
+
/** One refresh check: fetch remote timestamps, arm the throttle only on success, compare. */
|
|
4680
|
+
async runRefreshCheck(providerToUse) {
|
|
4681
|
+
const gotTimestamps = await this.RefreshRemoteMetadataTimestamps(providerToUse);
|
|
4682
|
+
if (gotTimestamps) {
|
|
4683
|
+
this._lastRefreshCheckAt = Date.now();
|
|
4684
|
+
}
|
|
4550
4685
|
await this.LoadLocalMetadataFromStorage();
|
|
4551
4686
|
return this.LocalMetadataObsolete();
|
|
4552
4687
|
}
|