@memberjunction/core 6.2.0-edge.1 → 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/baseEntity.d.ts +201 -11
- package/dist/generic/baseEntity.d.ts.map +1 -1
- package/dist/generic/baseEntity.js +392 -89
- package/dist/generic/baseEntity.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 +18 -9
- 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 +28 -1
- package/dist/generic/entityInfo.d.ts.map +1 -1
- package/dist/generic/entityInfo.js +37 -0
- package/dist/generic/entityInfo.js.map +1 -1
- package/dist/generic/interfaces.d.ts +12 -3
- package/dist/generic/interfaces.d.ts.map +1 -1
- package/dist/generic/interfaces.js.map +1 -1
- package/dist/generic/providerBase.d.ts +82 -1
- package/dist/generic/providerBase.d.ts.map +1 -1
- package/dist/generic/providerBase.js +154 -19
- package/dist/generic/providerBase.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
|
|
@@ -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
|
}
|