@memberjunction/core 5.51.3 → 5.51.4
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/InMemoryLocalStorageProvider.d.ts +5 -0
- package/dist/generic/InMemoryLocalStorageProvider.d.ts.map +1 -1
- package/dist/generic/InMemoryLocalStorageProvider.js +5 -0
- package/dist/generic/InMemoryLocalStorageProvider.js.map +1 -1
- package/dist/generic/baseEngine.d.ts +5 -0
- package/dist/generic/baseEngine.d.ts.map +1 -1
- package/dist/generic/baseEngine.js +6 -1
- package/dist/generic/baseEngine.js.map +1 -1
- package/dist/generic/baseEntity.d.ts +35 -0
- package/dist/generic/baseEntity.d.ts.map +1 -1
- package/dist/generic/baseEntity.js +66 -13
- package/dist/generic/baseEntity.js.map +1 -1
- package/dist/generic/databaseProviderBase.d.ts +41 -8
- package/dist/generic/databaseProviderBase.d.ts.map +1 -1
- package/dist/generic/databaseProviderBase.js +67 -30
- package/dist/generic/databaseProviderBase.js.map +1 -1
- package/dist/generic/entityInfo.d.ts +18 -2
- package/dist/generic/entityInfo.d.ts.map +1 -1
- package/dist/generic/entityInfo.js +26 -10
- package/dist/generic/entityInfo.js.map +1 -1
- package/dist/generic/interfaces.d.ts +38 -0
- package/dist/generic/interfaces.d.ts.map +1 -1
- package/dist/generic/interfaces.js.map +1 -1
- package/dist/generic/providerBase.d.ts +66 -5
- package/dist/generic/providerBase.d.ts.map +1 -1
- package/dist/generic/providerBase.js +147 -13
- package/dist/generic/providerBase.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/runQuery.d.ts +18 -1
- package/dist/generic/runQuery.d.ts.map +1 -1
- package/dist/generic/runQuery.js.map +1 -1
- package/dist/generic/runQuerySQLFilterImplementations.d.ts +7 -1
- package/dist/generic/runQuerySQLFilterImplementations.d.ts.map +1 -1
- package/dist/generic/runQuerySQLFilterImplementations.js +64 -139
- package/dist/generic/runQuerySQLFilterImplementations.js.map +1 -1
- package/dist/generic/telemetryManager.d.ts +11 -0
- package/dist/generic/telemetryManager.d.ts.map +1 -1
- package/dist/generic/telemetryManager.js +40 -0
- package/dist/generic/telemetryManager.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
|
@@ -219,6 +219,8 @@ export class ProviderBase {
|
|
|
219
219
|
this._metadataMemberRefreshTimer = null;
|
|
220
220
|
this._clientFingerprintMemo = new WeakMap();
|
|
221
221
|
this._cachedVisibleExplorerNavigationItems = null;
|
|
222
|
+
/** Guard for {@link logMetadataSnapshotDisabledOnce}. */
|
|
223
|
+
this._metadataSnapshotSkipLogged = false;
|
|
222
224
|
}
|
|
223
225
|
// ── Metadata Refresh Check Debounce ────────────────────────────────
|
|
224
226
|
/**
|
|
@@ -271,19 +273,56 @@ export class ProviderBase {
|
|
|
271
273
|
* instead of accumulating result arrays in memory.
|
|
272
274
|
*/
|
|
273
275
|
static { this.MaxLingerEntries = 500; }
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
276
|
+
// ── Write-invalidation fan-out (process-wide, not per-instance) ────
|
|
277
|
+
/**
|
|
278
|
+
* Every live provider instance that has opted into write-invalidation, held via
|
|
279
|
+
* `WeakRef` so registering here never keeps a provider (and everything it
|
|
280
|
+
* references — connection pool, entity metadata, `_inflightViews`) reachable.
|
|
281
|
+
* Short-lived providers (one per GraphQL request, one per durable task-graph
|
|
282
|
+
* task execution — see `MJServer/context.ts` and `TaskGraphProviderFactory`)
|
|
283
|
+
* become eligible for GC as soon as the request/task finishes; dead refs are
|
|
284
|
+
* pruned lazily the next time an entity event fires.
|
|
285
|
+
*/
|
|
286
|
+
static { this._invalidationTargets = new Set(); }
|
|
287
|
+
/**
|
|
288
|
+
* The exact event-bus Observable the fan-out is currently subscribed to — identity, not a
|
|
289
|
+
* boolean "already wired" flag. `MJGlobal.GetEventListener(false)` returns `MJGlobal._events$`,
|
|
290
|
+
* and BOTH `MJGlobal.Reset()` and `resetMJSingletons()` (which drops the singleton out of the
|
|
291
|
+
* global object store entirely) replace that field with a fresh `Subject`. A boolean cannot
|
|
292
|
+
* tell that the bus underneath it changed, so it would leave the fan-out attached to the
|
|
293
|
+
* orphaned Subject for the rest of the process — no error, just linger entries that silently
|
|
294
|
+
* stop being invalidated. Comparing the reference detects the swap exactly.
|
|
295
|
+
*/
|
|
296
|
+
static { this._wiredBus = null; }
|
|
297
|
+
/**
|
|
298
|
+
* The live fan-out subscription, retained so a bus swap can unsubscribe the stale one.
|
|
299
|
+
* Without this, every swap strands a subscriber on the dead Subject — a small instance of
|
|
300
|
+
* the very leak this fan-out exists to prevent.
|
|
301
|
+
*/
|
|
302
|
+
static { this._busSubscription = null; }
|
|
303
|
+
/**
|
|
304
|
+
* Subscribes exactly ONCE PER EVENT BUS (not once per provider instance) to BaseEntity
|
|
305
|
+
* events and fans each one out to every still-live registered provider. This is what keeps
|
|
306
|
+
* the number of `MJGlobal` event-bus subscribers constant regardless of how many short-lived
|
|
307
|
+
* provider instances get created — subscribing per-instance (the original implementation)
|
|
308
|
+
* permanently pinned one subscription per instance on the process-wide `Subject`, which never
|
|
309
|
+
* releases subscribers on its own, so a fresh provider minted on every GraphQL request (or
|
|
310
|
+
* every durable task-graph task) leaked its entire object graph forever.
|
|
311
|
+
*
|
|
312
|
+
* Keyed on the bus's identity rather than a one-shot boolean, because the bus is a
|
|
313
|
+
* replaceable slot and not a process constant. See {@link _wiredBus}.
|
|
280
314
|
*/
|
|
281
|
-
|
|
282
|
-
|
|
315
|
+
static ensureStaticInvalidationSubscription() {
|
|
316
|
+
const bus = MJGlobal.Instance.GetEventListener(false);
|
|
317
|
+
if (ProviderBase._wiredBus === bus) {
|
|
283
318
|
return;
|
|
284
319
|
}
|
|
285
|
-
|
|
286
|
-
|
|
320
|
+
// The bus was replaced (MJGlobal.Reset / singleton clear). Drop the subscriber stranded on
|
|
321
|
+
// the dead Subject, then attach to the live one. Assign _wiredBus BEFORE subscribing so a
|
|
322
|
+
// re-entrant call cannot double-wire.
|
|
323
|
+
ProviderBase._busSubscription?.unsubscribe();
|
|
324
|
+
ProviderBase._wiredBus = bus;
|
|
325
|
+
ProviderBase._busSubscription = bus.subscribe((event) => {
|
|
287
326
|
if (event.event !== MJEventType.ComponentEvent || event.eventCode !== BaseEntity.BaseEventCode) {
|
|
288
327
|
return;
|
|
289
328
|
}
|
|
@@ -295,12 +334,42 @@ export class ProviderBase {
|
|
|
295
334
|
return;
|
|
296
335
|
}
|
|
297
336
|
const entityName = (entityEvent.baseEntity?.EntityInfo?.Name ?? entityEvent.entityName)?.trim().toLowerCase();
|
|
298
|
-
if (entityName) {
|
|
299
|
-
|
|
300
|
-
|
|
337
|
+
if (!entityName) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
for (const ref of ProviderBase._invalidationTargets) {
|
|
341
|
+
const instance = ref.deref();
|
|
342
|
+
if (!instance) {
|
|
343
|
+
// Provider was GC'd since it registered — prune the dead ref instead of
|
|
344
|
+
// letting the registry grow forever with entries that resolve to nothing.
|
|
345
|
+
ProviderBase._invalidationTargets.delete(ref);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
instance.invalidateInflightViewsForEntity(entityName);
|
|
349
|
+
instance.handleMetadataMemberEntityEvent(entityName, entityEvent);
|
|
301
350
|
}
|
|
302
351
|
});
|
|
303
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* Lazily registers this instance (once per provider instance) for write-invalidation
|
|
355
|
+
* of in-flight/lingered RunView entries touching a written entity. Deleting an IN-FLIGHT
|
|
356
|
+
* entry is deliberate: late joiners must not share a query that may have read pre-commit
|
|
357
|
+
* data; the original caller still gets its own result, and the orphaned promise's stash
|
|
358
|
+
* step no-ops safely.
|
|
359
|
+
*/
|
|
360
|
+
ensureInflightViewInvalidation() {
|
|
361
|
+
// Deliberately ABOVE the per-instance guard: the static wiring is re-checked on every call,
|
|
362
|
+
// not just the first. A long-lived provider (Explorer's client provider, MJServer's system
|
|
363
|
+
// provider) that registered before a bus swap would otherwise stay wired to the dead Subject
|
|
364
|
+
// for the rest of the process — short-lived server providers self-heal via the next
|
|
365
|
+
// request's fresh instance, long-lived ones never would.
|
|
366
|
+
ProviderBase.ensureStaticInvalidationSubscription();
|
|
367
|
+
if (this._lingerInvalidationWired) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
this._lingerInvalidationWired = true;
|
|
371
|
+
ProviderBase._invalidationTargets.add(new WeakRef(this));
|
|
372
|
+
}
|
|
304
373
|
/**
|
|
305
374
|
* Drops every in-flight/lingered RunView entry whose params touch the given
|
|
306
375
|
* entity (lowercased name). Called on BaseEntity save/delete/remote-invalidate.
|
|
@@ -4141,6 +4210,12 @@ export class ProviderBase {
|
|
|
4141
4210
|
const ls = this.LocalStorageProvider;
|
|
4142
4211
|
if (!ls)
|
|
4143
4212
|
return;
|
|
4213
|
+
// Symmetrical with the save: an in-process store can only hand back a copy of the
|
|
4214
|
+
// metadata this heap already holds, at the cost of rebuilding every metadata object.
|
|
4215
|
+
if (!this.MetadataSnapshotPersistenceEnabled) {
|
|
4216
|
+
this.logMetadataSnapshotDisabledOnce();
|
|
4217
|
+
return;
|
|
4218
|
+
}
|
|
4144
4219
|
const overallStart = Date.now();
|
|
4145
4220
|
// The metadata snapshot uses three keys (timestamps, format, AllMetadata).
|
|
4146
4221
|
// Pull them in a single batched read — saves two IDB transaction round-trips
|
|
@@ -4231,6 +4306,18 @@ export class ProviderBase {
|
|
|
4231
4306
|
* Used for compressed metadata storage/retrieval.
|
|
4232
4307
|
*/
|
|
4233
4308
|
static base64ToArrayBuffer(base64) {
|
|
4309
|
+
// Node decodes natively; the atob path below allocates an intermediate string and fills the
|
|
4310
|
+
// array a byte at a time.
|
|
4311
|
+
if (typeof Buffer !== 'undefined') {
|
|
4312
|
+
const buf = Buffer.from(base64, 'base64');
|
|
4313
|
+
// Copy into an exact-size buffer: `buf.buffer` is a view into a shared pool, usually
|
|
4314
|
+
// larger than the payload, so returning it directly would carry unrelated bytes and a
|
|
4315
|
+
// wrong byteLength. It is also typed ArrayBufferLike and will not assign to the
|
|
4316
|
+
// declared return type.
|
|
4317
|
+
const exact = new ArrayBuffer(buf.byteLength);
|
|
4318
|
+
new Uint8Array(exact).set(buf);
|
|
4319
|
+
return exact;
|
|
4320
|
+
}
|
|
4234
4321
|
const binaryString = atob(base64);
|
|
4235
4322
|
const bytes = new Uint8Array(binaryString.length);
|
|
4236
4323
|
for (let i = 0; i < binaryString.length; i++) {
|
|
@@ -4243,6 +4330,11 @@ export class ProviderBase {
|
|
|
4243
4330
|
* Used for compressed metadata storage/retrieval.
|
|
4244
4331
|
*/
|
|
4245
4332
|
static arrayBufferToBase64(buffer) {
|
|
4333
|
+
// Node encodes natively. The loop below concatenates one character per byte, building a rope
|
|
4334
|
+
// the size of the payload that then has to be flattened.
|
|
4335
|
+
if (typeof Buffer !== 'undefined') {
|
|
4336
|
+
return Buffer.from(buffer).toString('base64');
|
|
4337
|
+
}
|
|
4246
4338
|
const bytes = new Uint8Array(buffer);
|
|
4247
4339
|
let binary = '';
|
|
4248
4340
|
for (let i = 0; i < bytes.byteLength; i++) {
|
|
@@ -4258,6 +4350,42 @@ export class ProviderBase {
|
|
|
4258
4350
|
get LocalStoragePrefix() {
|
|
4259
4351
|
return "";
|
|
4260
4352
|
}
|
|
4353
|
+
/**
|
|
4354
|
+
* Whether anything could read the metadata snapshot back, and therefore whether saving it is
|
|
4355
|
+
* worth the cost.
|
|
4356
|
+
*
|
|
4357
|
+
* The snapshot lets a cold process start from a cached copy of the metadata instead of querying
|
|
4358
|
+
* for it, which only works if the store outlives the writer. Where it does not, both halves of
|
|
4359
|
+
* the round trip are pure cost: the save serializes, gzips and base64-encodes the entire
|
|
4360
|
+
* metadata graph, and the load parses it and rebuilds every `EntityInfo` and `EntityFieldInfo`
|
|
4361
|
+
* from a copy of objects the heap already holds. On a large tenant that is expensive enough to
|
|
4362
|
+
* exhaust the heap.
|
|
4363
|
+
*
|
|
4364
|
+
* A provider that does not declare {@link ILocalStorageProvider.SupportsCrossProcessPersistence}
|
|
4365
|
+
* is treated as persistent, which fails in the safer direction.
|
|
4366
|
+
*/
|
|
4367
|
+
get MetadataSnapshotPersistenceEnabled() {
|
|
4368
|
+
const ls = this.LocalStorageProvider;
|
|
4369
|
+
if (!ls) {
|
|
4370
|
+
return false;
|
|
4371
|
+
}
|
|
4372
|
+
return ls.SupportsCrossProcessPersistence !== false;
|
|
4373
|
+
}
|
|
4374
|
+
/**
|
|
4375
|
+
* Explains the skip once per process. The refresh path runs every 30 seconds on a busy server,
|
|
4376
|
+
* so logging per call would bury the one line that matters.
|
|
4377
|
+
*/
|
|
4378
|
+
logMetadataSnapshotDisabledOnce() {
|
|
4379
|
+
if (this._metadataSnapshotSkipLogged) {
|
|
4380
|
+
return;
|
|
4381
|
+
}
|
|
4382
|
+
this._metadataSnapshotSkipLogged = true;
|
|
4383
|
+
const name = this.LocalStorageProvider?.constructor?.name ?? 'the local storage provider';
|
|
4384
|
+
LogStatusEx({
|
|
4385
|
+
message: `[Metadata Cache] Snapshot persistence disabled: ${name} is in-process only, so a saved snapshot could never be read back. Skipping the metadata snapshot save and load.`,
|
|
4386
|
+
verboseOnly: false
|
|
4387
|
+
});
|
|
4388
|
+
}
|
|
4261
4389
|
/**
|
|
4262
4390
|
* Saves current metadata to local storage for caching.
|
|
4263
4391
|
* Serializes both timestamps and full metadata collections.
|
|
@@ -4267,6 +4395,12 @@ export class ProviderBase {
|
|
|
4267
4395
|
const ls = this.LocalStorageProvider;
|
|
4268
4396
|
if (!ls)
|
|
4269
4397
|
return;
|
|
4398
|
+
// Nothing could ever read this snapshot back, so the entire serialize/compress/encode
|
|
4399
|
+
// pass buys nothing. See MetadataSnapshotPersistenceEnabled.
|
|
4400
|
+
if (!this.MetadataSnapshotPersistenceEnabled) {
|
|
4401
|
+
this.logMetadataSnapshotDisabledOnce();
|
|
4402
|
+
return;
|
|
4403
|
+
}
|
|
4270
4404
|
const start = Date.now();
|
|
4271
4405
|
// Save timestamps as a JSON string. The metadata snapshot path intentionally uses
|
|
4272
4406
|
// string storage so the compressed (gzip+base64) format below can round-trip cleanly
|