@memberjunction/core 5.44.0 → 5.45.1

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.
Files changed (39) hide show
  1. package/dist/generic/baseEngine.d.ts +146 -10
  2. package/dist/generic/baseEngine.d.ts.map +1 -1
  3. package/dist/generic/baseEngine.js +286 -54
  4. package/dist/generic/baseEngine.js.map +1 -1
  5. package/dist/generic/databaseProviderBase.d.ts.map +1 -1
  6. package/dist/generic/databaseProviderBase.js +16 -0
  7. package/dist/generic/databaseProviderBase.js.map +1 -1
  8. package/dist/generic/entityInfo.d.ts +12 -0
  9. package/dist/generic/entityInfo.d.ts.map +1 -1
  10. package/dist/generic/entityInfo.js +12 -0
  11. package/dist/generic/entityInfo.js.map +1 -1
  12. package/dist/generic/externalDataSourceReadRouter.d.ts +63 -0
  13. package/dist/generic/externalDataSourceReadRouter.d.ts.map +1 -0
  14. package/dist/generic/externalDataSourceReadRouter.js +19 -0
  15. package/dist/generic/externalDataSourceReadRouter.js.map +1 -0
  16. package/dist/generic/externalDataSourceTypes.d.ts +68 -0
  17. package/dist/generic/externalDataSourceTypes.d.ts.map +1 -0
  18. package/dist/generic/externalDataSourceTypes.js +14 -0
  19. package/dist/generic/externalDataSourceTypes.js.map +1 -0
  20. package/dist/generic/localCacheManager.d.ts +1 -1
  21. package/dist/generic/localCacheManager.d.ts.map +1 -1
  22. package/dist/generic/localCacheManager.js +21 -2
  23. package/dist/generic/localCacheManager.js.map +1 -1
  24. package/dist/generic/logging.d.ts.map +1 -1
  25. package/dist/generic/logging.js +4 -1
  26. package/dist/generic/logging.js.map +1 -1
  27. package/dist/generic/providerBase.d.ts +35 -0
  28. package/dist/generic/providerBase.d.ts.map +1 -1
  29. package/dist/generic/providerBase.js +83 -3
  30. package/dist/generic/providerBase.js.map +1 -1
  31. package/dist/generic/securityInfo.d.ts +48 -0
  32. package/dist/generic/securityInfo.d.ts.map +1 -1
  33. package/dist/generic/securityInfo.js +27 -0
  34. package/dist/generic/securityInfo.js.map +1 -1
  35. package/dist/index.d.ts +2 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +2 -0
  38. package/dist/index.js.map +1 -1
  39. package/package.json +3 -3
@@ -5,7 +5,7 @@ import { ComputeRRF } from "./scoring/ReciprocalRankFusion.js";
5
5
  import { LocalCacheManager } from "./localCacheManager.js";
6
6
  import { ApplicationInfo } from "../generic/applicationInfo.js";
7
7
  import { AuditLogTypeInfo, AuthorizationInfo, AuthorizationRoleInfo, RoleInfo, RowLevelSecurityFilterInfo, UserInfo } from "./securityInfo.js";
8
- import { MJGlobal, NormalizeUUID, UUIDsEqual } from "@memberjunction/global";
8
+ import { MJGlobal, MJEventType, NormalizeUUID, UUIDsEqual } from "@memberjunction/global";
9
9
  import { TelemetryManager } from "./telemetryManager.js";
10
10
  import { LogError, LogStatus, LogStatusEx } from "./logging.js";
11
11
  import { QueryCategoryInfo, QueryFieldInfo, QueryInfo, QueryPermissionInfo, QueryEntityInfo, QueryParameterInfo, QueryDependencyInfo, SQLDialectInfo, QuerySQLInfo } from "./queryInfo.js";
@@ -196,8 +196,16 @@ export class ProviderBase {
196
196
  * the RunViewParams batch. While a request is in-flight, concurrent
197
197
  * identical calls share the same promise. After resolution the entry
198
198
  * lingers so that near-sequential identical calls return immediately.
199
+ *
200
+ * Entries record the (lowercased) entity names their params touch so that
201
+ * a BaseEntity save/delete/remote-invalidate can drop them — a lingered
202
+ * result is a cache, and without write-invalidation a caller re-running
203
+ * the identical view within the linger window after a save would receive
204
+ * PRE-save rows (observed live: an engine's debounced post-save refreshes
205
+ * for multiple sequential saves — only the first save's data ever arrived).
199
206
  */
200
207
  this._inflightViews = new Map();
208
+ this._lingerInvalidationWired = false;
201
209
  this._clientFingerprintMemo = new WeakMap();
202
210
  this._cachedVisibleExplorerNavigationItems = null;
203
211
  }
@@ -252,6 +260,62 @@ export class ProviderBase {
252
260
  * instead of accumulating result arrays in memory.
253
261
  */
254
262
  static { this.MaxLingerEntries = 500; }
263
+ /**
264
+ * Lazily subscribes (once per provider instance) to BaseEntity events so
265
+ * in-flight/lingered RunView entries touching a written entity are dropped.
266
+ * Deleting an IN-FLIGHT entry is deliberate: late joiners must not share a
267
+ * query that may have read pre-commit data; the original caller still gets
268
+ * its own result, and the orphaned promise's stash step no-ops safely.
269
+ */
270
+ ensureInflightViewInvalidation() {
271
+ if (this._lingerInvalidationWired) {
272
+ return;
273
+ }
274
+ this._lingerInvalidationWired = true;
275
+ MJGlobal.Instance.GetEventListener(false).subscribe((event) => {
276
+ if (event.event !== MJEventType.ComponentEvent || event.eventCode !== BaseEntity.BaseEventCode) {
277
+ return;
278
+ }
279
+ const entityEvent = event.args;
280
+ if (!entityEvent) {
281
+ return;
282
+ }
283
+ if (entityEvent.type !== 'save' && entityEvent.type !== 'delete' && entityEvent.type !== 'remote-invalidate') {
284
+ return;
285
+ }
286
+ const entityName = (entityEvent.baseEntity?.EntityInfo?.Name ?? entityEvent.entityName)?.trim().toLowerCase();
287
+ if (entityName) {
288
+ this.invalidateInflightViewsForEntity(entityName);
289
+ }
290
+ });
291
+ }
292
+ /**
293
+ * Drops every in-flight/lingered RunView entry whose params touch the given
294
+ * entity (lowercased name). Called on BaseEntity save/delete/remote-invalidate.
295
+ */
296
+ invalidateInflightViewsForEntity(lowerEntityName) {
297
+ for (const [key, entry] of this._inflightViews) {
298
+ if (entry.entityNames?.has(lowerEntityName)) {
299
+ this._inflightViews.delete(key);
300
+ }
301
+ }
302
+ }
303
+ /**
304
+ * Collects the lowercased entity names a RunViewParams batch touches, for
305
+ * write-invalidation of dedup/linger entries. Params addressed by ViewID /
306
+ * ViewName only (no EntityName) are not tracked — they keep today's
307
+ * linger behavior rather than pay a metadata resolution here.
308
+ */
309
+ collectParamEntityNames(params) {
310
+ let names;
311
+ for (const p of params) {
312
+ const n = p.EntityName?.trim().toLowerCase();
313
+ if (n) {
314
+ (names ??= new Set()).add(n);
315
+ }
316
+ }
317
+ return names;
318
+ }
255
319
  /******** ABSTRACT SECTION ****************************************************************** */
256
320
  /**
257
321
  * When true, cached RunView/RunQuery results are returned immediately on a
@@ -566,7 +630,8 @@ export class ProviderBase {
566
630
  }
567
631
  throw err;
568
632
  });
569
- this._inflightViews.set(key, { promise });
633
+ this.ensureInflightViewInvalidation();
634
+ this._inflightViews.set(key, { promise, entityNames: this.collectParamEntityNames(params) });
570
635
  const results = await promise;
571
636
  return results.map(r => this.ShallowCopyResult(r));
572
637
  }
@@ -860,7 +925,8 @@ export class ProviderBase {
860
925
  this._inflightViews.delete(key);
861
926
  throw err;
862
927
  });
863
- this._inflightViews.set(key, { promise });
928
+ this.ensureInflightViewInvalidation();
929
+ this._inflightViews.set(key, { promise, entityNames: this.collectParamEntityNames(params) });
864
930
  const results = await promise;
865
931
  return results.map(r => this.ShallowCopyResult(r));
866
932
  }
@@ -1270,6 +1336,15 @@ export class ProviderBase {
1270
1336
  * @param contextUser - Optional user context for permissions (required server-side)
1271
1337
  * @returns The query results
1272
1338
  */
1339
+ /**
1340
+ * Whether a saved query is bound to an external data source. The base returns false;
1341
+ * providers that support external data sources override this (consulting query metadata)
1342
+ * so the outer RunQuery CacheLocal layer can defer to InternalRunQuery's own external
1343
+ * TTL caching. Synchronous + non-throwing: resolves from cached metadata only.
1344
+ */
1345
+ IsExternalQuery(_params) {
1346
+ return false;
1347
+ }
1273
1348
  async RunQuery(params, contextUser) {
1274
1349
  // Shallow-clone for symmetry with RunView — pipeline must never mutate caller objects
1275
1350
  params = { ...params };
@@ -1286,6 +1361,11 @@ export class ProviderBase {
1286
1361
  const queryCacheEngaged = params.CacheLocal === true
1287
1362
  && !params.SQL
1288
1363
  && (!!params.QueryID || !!params.QueryName)
1364
+ // External-data-source queries own their own TTL caching inside InternalRunQuery
1365
+ // (runExternalQueryWithCache, keyed to the source's DefaultCacheTTLSeconds). This
1366
+ // outer CacheLocal layer would otherwise write a SECOND, divergent slot with no/foreign
1367
+ // TTL (CacheLocalTTL) — a stale-forever hazard since external data can't be event-invalidated.
1368
+ && !this.IsExternalQuery(params)
1289
1369
  && LocalCacheManager.Instance.IsInitialized;
1290
1370
  let queryFingerprint;
1291
1371
  if (queryCacheEngaged) {