@memberjunction/metadata-sync 5.37.0 → 5.39.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/README.md +145 -3
- 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/lib/config-manager.d.ts +13 -5
- package/dist/lib/config-manager.d.ts.map +1 -1
- package/dist/lib/config-manager.js +34 -8
- package/dist/lib/config-manager.js.map +1 -1
- package/dist/lib/sync-engine.d.ts +21 -1
- package/dist/lib/sync-engine.d.ts.map +1 -1
- package/dist/lib/sync-engine.js +96 -8
- package/dist/lib/sync-engine.js.map +1 -1
- package/dist/lib/sync-metadata-engine.d.ts +176 -0
- package/dist/lib/sync-metadata-engine.d.ts.map +1 -0
- package/dist/lib/sync-metadata-engine.js +454 -0
- package/dist/lib/sync-metadata-engine.js.map +1 -0
- package/dist/services/PushService.d.ts +1 -0
- package/dist/services/PushService.d.ts.map +1 -1
- package/dist/services/PushService.js +44 -11
- package/dist/services/PushService.js.map +1 -1
- package/dist/services/ValidationService.d.ts.map +1 -1
- package/dist/services/ValidationService.js +13 -5
- package/dist/services/ValidationService.js.map +1 -1
- package/package.json +12 -12
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Upfront preloading + lookup/file caching for `mj sync push`.
|
|
3
|
+
*
|
|
4
|
+
* `SyncMetadataEngine` extends `BaseEngine` to piggy-back on its batched
|
|
5
|
+
* `RunViews` loader and `BaseEntity` event-bus subscription. The push flow
|
|
6
|
+
* uses it in three ways:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Preload** — `Config()` scans every JSON file under each entity
|
|
9
|
+
* directory once, collects the set of entity names touched (including
|
|
10
|
+
* nested `relatedEntities`), and asks `BaseEngine.Load` to issue one
|
|
11
|
+
* unfiltered `RunView` per entity. We load *all* rows of each touched
|
|
12
|
+
* entity, not just the PKs mentioned in files — for two reasons:
|
|
13
|
+
*
|
|
14
|
+
* (a) `mj sync` is for METADATA. The entities involved (Actions,
|
|
15
|
+
* Prompts, Agents, Templates, etc.) are inherently bounded —
|
|
16
|
+
* hundreds to low tens of thousands of rows per tenant, not
|
|
17
|
+
* millions. Loading everything is faster than computing and
|
|
18
|
+
* shipping a giant `WHERE IN (...)` clause.
|
|
19
|
+
* (b) `@lookup:` resolution often targets records that aren't in
|
|
20
|
+
* the local files (e.g. a category referenced by name). A
|
|
21
|
+
* full preload means those lookups hit the cache too.
|
|
22
|
+
*
|
|
23
|
+
* The loaded `BaseEntity` instances live on dynamic properties named
|
|
24
|
+
* via {@link getPropertyNameForEntity}; lookups during the sync phase
|
|
25
|
+
* consult those arrays instead of round-tripping the DB.
|
|
26
|
+
*
|
|
27
|
+
* 2. **Event-driven mutation** — `BaseEngine` already wires up the global
|
|
28
|
+
* `BaseEntity` event bus. Because our configs carry no Filter/OrderBy,
|
|
29
|
+
* the base class's default `canUseImmediateMutation` path handles
|
|
30
|
+
* save/delete/remote-invalidate events automatically.
|
|
31
|
+
*
|
|
32
|
+
* 3. **Lookup + file caches** — Resolved `@lookup` keys are memoized in
|
|
33
|
+
* {@link lookupCache} and parsed file contents are memoized in
|
|
34
|
+
* {@link fileDataCache}, so validation and sync passes don't re-read
|
|
35
|
+
* or re-parse the same file twice.
|
|
36
|
+
*
|
|
37
|
+
* @module sync-metadata-engine
|
|
38
|
+
*/
|
|
39
|
+
import { BaseEngine } from '@memberjunction/core';
|
|
40
|
+
import { IsUuidSQLType } from '@memberjunction/sql-dialect';
|
|
41
|
+
import fastGlob from 'fast-glob';
|
|
42
|
+
import fs from 'fs-extra';
|
|
43
|
+
import { JsonPreprocessor } from './json-preprocessor.js';
|
|
44
|
+
import { loadEntityConfig } from '../config.js';
|
|
45
|
+
/** Prefix used to namespace the dynamic per-entity cache properties on the engine instance. */
|
|
46
|
+
const CACHED_ENTITY_PROP_PREFIX = 'cached_';
|
|
47
|
+
/** Concurrency cap for parallel file reads during preload. */
|
|
48
|
+
const FILE_READ_CONCURRENCY = 16;
|
|
49
|
+
/**
|
|
50
|
+
* Emit a warning when any single preloaded entity comes back with more rows
|
|
51
|
+
* than this. `mj sync` is for metadata — entities in this benchmark band
|
|
52
|
+
* (Actions, Prompts, Templates, etc.) are bounded by design. Loading 100k+
|
|
53
|
+
* rows of one entity into RAM is a signal that something is being synced
|
|
54
|
+
* that shouldn't be (or the metadata set has grown beyond the tool's intent).
|
|
55
|
+
*/
|
|
56
|
+
const LARGE_ENTITY_WARN_THRESHOLD = 100_000;
|
|
57
|
+
/**
|
|
58
|
+
* Coordinates upfront batch-preloading of `BaseEntity` instances and in-memory
|
|
59
|
+
* caches for `mj sync push`. See module docstring for the full picture.
|
|
60
|
+
*/
|
|
61
|
+
export class SyncMetadataEngine extends BaseEngine {
|
|
62
|
+
constructor() {
|
|
63
|
+
super(...arguments);
|
|
64
|
+
this.entityDirs = [];
|
|
65
|
+
this.preloadedEntityNames = new Set();
|
|
66
|
+
this.lookupCache = new Map();
|
|
67
|
+
this.fileDataCache = new Map();
|
|
68
|
+
/** Reverse index used to scope lookup invalidation on delete: entity name → set of lookup keys touching that entity. */
|
|
69
|
+
this.lookupKeysByEntity = new Map();
|
|
70
|
+
/**
|
|
71
|
+
* O(1) PK lookup index built after preload completes. Keyed by entity
|
|
72
|
+
* name, then by `serializePrimaryKey(...)` of the entity's own PK.
|
|
73
|
+
*
|
|
74
|
+
* Without this, `loadEntity` falls into an `Array.find(... GetAll())`
|
|
75
|
+
* scan over the entire preloaded slot, which is O(N) per record being
|
|
76
|
+
* processed — i.e. O(N×K) overall. For entities like `MJ: Integration
|
|
77
|
+
* Object Fields` where DB-wide row count can be 10×+ the file count,
|
|
78
|
+
* that turns into billions of comparisons (38 min for the integrations
|
|
79
|
+
* dir alone in real-world measurement).
|
|
80
|
+
*
|
|
81
|
+
* Kept in sync by {@link addEntityToCache} / {@link removeEntityFromCache}.
|
|
82
|
+
* `findCachedByPrimaryKey` falls back to an array scan on Map miss so
|
|
83
|
+
* we tolerate drift from `BaseEngine`'s event-driven slot mutations
|
|
84
|
+
* (which mutate the array directly without going through our helpers).
|
|
85
|
+
*/
|
|
86
|
+
this.pkIndexes = new Map();
|
|
87
|
+
/** Non-fatal warnings collected during preload (malformed shapes, oversized entities). Drained by the caller after `Config()`. */
|
|
88
|
+
this.warnings = [];
|
|
89
|
+
}
|
|
90
|
+
initializeEngine(syncEngine) {
|
|
91
|
+
this.syncEngine = syncEngine;
|
|
92
|
+
}
|
|
93
|
+
setEntityDirs(dirs) {
|
|
94
|
+
this.entityDirs = dirs;
|
|
95
|
+
}
|
|
96
|
+
/** Returns and clears the warnings collected during the last preload run. */
|
|
97
|
+
drainWarnings() {
|
|
98
|
+
const out = this.warnings;
|
|
99
|
+
this.warnings = [];
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Property name on this engine instance under which cached `BaseEntity[]`
|
|
104
|
+
* arrays for the given entity are stored. `BaseEngine.applyImmediateMutation`
|
|
105
|
+
* reads/writes these slots directly, so we use a deterministic naming scheme.
|
|
106
|
+
*/
|
|
107
|
+
getPropertyNameForEntity(entityName) {
|
|
108
|
+
return `${CACHED_ENTITY_PROP_PREFIX}${entityName.replace(/[^a-zA-Z0-9]/g, '_')}`;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Build the dynamic configuration list and trigger `BaseEngine.Load` —
|
|
112
|
+
* which fans the per-entity unfiltered views out over a single `RunViews`
|
|
113
|
+
* batch under the hood. Then check each loaded slot for size and emit
|
|
114
|
+
* warnings if anything came back larger than the metadata-scale threshold.
|
|
115
|
+
*/
|
|
116
|
+
async Config(forceRefresh, contextUser, provider) {
|
|
117
|
+
const configs = await this.buildDynamicConfigs();
|
|
118
|
+
const providerToUse = provider || this.ProviderToUse;
|
|
119
|
+
if (configs.length > 0) {
|
|
120
|
+
await this.Load(configs, providerToUse, forceRefresh, contextUser);
|
|
121
|
+
this.buildPKIndexes(configs);
|
|
122
|
+
this.warnOnOversizedSlots(configs);
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Build the per-entity PK → entity Map from each populated slot.
|
|
128
|
+
* Runs once after `BaseEngine.Load` completes. The cost (one walk
|
|
129
|
+
* over the loaded arrays, building Map entries) is trivial compared
|
|
130
|
+
* to the per-record-lookup savings it enables downstream.
|
|
131
|
+
*/
|
|
132
|
+
buildPKIndexes(configs) {
|
|
133
|
+
for (const config of configs) {
|
|
134
|
+
if (!config.PropertyName || !config.EntityName)
|
|
135
|
+
continue;
|
|
136
|
+
const entityInfo = this.syncEngine.getEntityInfo(config.EntityName);
|
|
137
|
+
if (!entityInfo)
|
|
138
|
+
continue;
|
|
139
|
+
const list = this[config.PropertyName];
|
|
140
|
+
if (!Array.isArray(list))
|
|
141
|
+
continue;
|
|
142
|
+
const index = new Map();
|
|
143
|
+
for (const entity of list) {
|
|
144
|
+
try {
|
|
145
|
+
const pkStr = this.serializePrimaryKey(entityInfo, entity.GetAll());
|
|
146
|
+
index.set(pkStr, entity);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// Defensive: a malformed cached entity shouldn't break index
|
|
150
|
+
// construction for the whole entity. The array scan fallback
|
|
151
|
+
// will still find it if needed.
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
this.pkIndexes.set(config.EntityName, index);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* O(1) PK lookup against the preload cache, with an array-scan
|
|
159
|
+
* fallback so we tolerate drift from `BaseEngine` event-driven
|
|
160
|
+
* slot mutations that don't go through our `addEntityToCache` helper.
|
|
161
|
+
*
|
|
162
|
+
* Returns `null` when the entity isn't preloaded or the PK isn't in
|
|
163
|
+
* the cache — callers fall through to a DB load.
|
|
164
|
+
*/
|
|
165
|
+
findCachedByPrimaryKey(entityName, primaryKey) {
|
|
166
|
+
if (!this.isEntityPreloaded(entityName))
|
|
167
|
+
return null;
|
|
168
|
+
const entityInfo = this.syncEngine.getEntityInfo(entityName);
|
|
169
|
+
if (!entityInfo)
|
|
170
|
+
return null;
|
|
171
|
+
const pkStr = this.serializePrimaryKey(entityInfo, primaryKey);
|
|
172
|
+
const index = this.pkIndexes.get(entityName);
|
|
173
|
+
const hit = index?.get(pkStr);
|
|
174
|
+
if (hit)
|
|
175
|
+
return hit;
|
|
176
|
+
// Fallback: someone (BaseEngine event handler) might have spliced
|
|
177
|
+
// a new entity into the array slot without updating the index.
|
|
178
|
+
// Linear scan once, and if we find a match, repair the index so
|
|
179
|
+
// subsequent hits stay O(1).
|
|
180
|
+
const list = this.getCachedEntities(entityName);
|
|
181
|
+
for (const entity of list) {
|
|
182
|
+
try {
|
|
183
|
+
if (this.serializePrimaryKey(entityInfo, entity.GetAll()) === pkStr) {
|
|
184
|
+
index?.set(pkStr, entity);
|
|
185
|
+
return entity;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// skip malformed entries
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
markEntityAsPreloaded(entityName) {
|
|
195
|
+
this.preloadedEntityNames.add(entityName);
|
|
196
|
+
}
|
|
197
|
+
isEntityPreloaded(entityName) {
|
|
198
|
+
return this.preloadedEntityNames.has(entityName);
|
|
199
|
+
}
|
|
200
|
+
getCachedEntities(entityName) {
|
|
201
|
+
const propName = this.getPropertyNameForEntity(entityName);
|
|
202
|
+
const list = this[propName];
|
|
203
|
+
return Array.isArray(list) ? list : [];
|
|
204
|
+
}
|
|
205
|
+
getCachedFile(filePath) {
|
|
206
|
+
return this.fileDataCache.get(filePath);
|
|
207
|
+
}
|
|
208
|
+
cacheFile(filePath, rawData, fileData) {
|
|
209
|
+
this.fileDataCache.set(filePath, { rawData, fileData });
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Invalidate the cached file content for a path. Call this immediately
|
|
213
|
+
* before (or after) any code path that writes back to a metadata JSON
|
|
214
|
+
* file during the sync, so a subsequent reader gets fresh contents.
|
|
215
|
+
*/
|
|
216
|
+
invalidateCachedFile(filePath) {
|
|
217
|
+
this.fileDataCache.delete(filePath);
|
|
218
|
+
}
|
|
219
|
+
addEntityToCache(entityName, entity) {
|
|
220
|
+
const propName = this.getPropertyNameForEntity(entityName);
|
|
221
|
+
const slot = this;
|
|
222
|
+
if (!Array.isArray(slot[propName])) {
|
|
223
|
+
slot[propName] = [];
|
|
224
|
+
}
|
|
225
|
+
const list = slot[propName];
|
|
226
|
+
// `PrimaryKey.Equals` requires the entity to have a populated composite
|
|
227
|
+
// key; if either side is missing one we fall back to reference equality
|
|
228
|
+
// only (the entity might not be saved yet, in which case no dedup is
|
|
229
|
+
// possible anyway).
|
|
230
|
+
const incomingKey = entity.PrimaryKey;
|
|
231
|
+
const arrayIdx = list.findIndex(e => {
|
|
232
|
+
if (e === entity)
|
|
233
|
+
return true;
|
|
234
|
+
const existingKey = e.PrimaryKey;
|
|
235
|
+
if (!incomingKey || !existingKey)
|
|
236
|
+
return false;
|
|
237
|
+
return existingKey.Equals(incomingKey);
|
|
238
|
+
});
|
|
239
|
+
if (arrayIdx >= 0) {
|
|
240
|
+
list[arrayIdx] = entity;
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
list.push(entity);
|
|
244
|
+
}
|
|
245
|
+
// Keep the PK index in sync. Skip silently when serializePrimaryKey
|
|
246
|
+
// throws — typically a half-built entity with no PK populated yet.
|
|
247
|
+
try {
|
|
248
|
+
const entityInfo = this.syncEngine.getEntityInfo(entityName);
|
|
249
|
+
if (entityInfo) {
|
|
250
|
+
const pkStr = this.serializePrimaryKey(entityInfo, entity.GetAll());
|
|
251
|
+
let index = this.pkIndexes.get(entityName);
|
|
252
|
+
if (!index) {
|
|
253
|
+
index = new Map();
|
|
254
|
+
this.pkIndexes.set(entityName, index);
|
|
255
|
+
}
|
|
256
|
+
index.set(pkStr, entity);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch { /* defensive: malformed entity */ }
|
|
260
|
+
}
|
|
261
|
+
removeEntityFromCache(entityName, primaryKey) {
|
|
262
|
+
const propName = this.getPropertyNameForEntity(entityName);
|
|
263
|
+
const list = this[propName];
|
|
264
|
+
const entityInfo = this.syncEngine.getEntityInfo(entityName);
|
|
265
|
+
let pkStr = null;
|
|
266
|
+
if (entityInfo) {
|
|
267
|
+
pkStr = this.serializePrimaryKey(entityInfo, primaryKey);
|
|
268
|
+
if (list) {
|
|
269
|
+
const arrayIdx = list.findIndex(e => this.serializePrimaryKey(entityInfo, e.GetAll()) === pkStr);
|
|
270
|
+
if (arrayIdx >= 0) {
|
|
271
|
+
list.splice(arrayIdx, 1);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (pkStr !== null) {
|
|
276
|
+
this.pkIndexes.get(entityName)?.delete(pkStr);
|
|
277
|
+
}
|
|
278
|
+
this.invalidateLookupsForEntity(entityName);
|
|
279
|
+
}
|
|
280
|
+
getCachedLookup(key) {
|
|
281
|
+
return this.lookupCache.get(key);
|
|
282
|
+
}
|
|
283
|
+
setCachedLookup(key, id, entityName) {
|
|
284
|
+
this.lookupCache.set(key, id);
|
|
285
|
+
if (entityName) {
|
|
286
|
+
const normalized = entityName.toLowerCase();
|
|
287
|
+
let bucket = this.lookupKeysByEntity.get(normalized);
|
|
288
|
+
if (!bucket) {
|
|
289
|
+
bucket = new Set();
|
|
290
|
+
this.lookupKeysByEntity.set(normalized, bucket);
|
|
291
|
+
}
|
|
292
|
+
bucket.add(key);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
clearLookupCache() {
|
|
296
|
+
this.lookupCache.clear();
|
|
297
|
+
this.lookupKeysByEntity.clear();
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Invalidate only the lookup entries that touch a given entity. Avoids
|
|
301
|
+
* the perf cliff of clearing the whole lookup cache on every delete.
|
|
302
|
+
*
|
|
303
|
+
* Note: if `setCachedLookup` was called without an `entityName`, those
|
|
304
|
+
* entries aren't in the reverse index and won't be cleared here. The
|
|
305
|
+
* `resolveLookup` integration always passes the entity name, so this
|
|
306
|
+
* is only a concern for callers that bypass that path.
|
|
307
|
+
*/
|
|
308
|
+
invalidateLookupsForEntity(entityName) {
|
|
309
|
+
const normalized = entityName.toLowerCase();
|
|
310
|
+
const bucket = this.lookupKeysByEntity.get(normalized);
|
|
311
|
+
if (!bucket)
|
|
312
|
+
return;
|
|
313
|
+
for (const key of bucket) {
|
|
314
|
+
this.lookupCache.delete(key);
|
|
315
|
+
}
|
|
316
|
+
this.lookupKeysByEntity.delete(normalized);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Canonical string representation of a record's primary key, used as the
|
|
320
|
+
* key in entity-cache dedup/removal lookups. UUID-typed components are
|
|
321
|
+
* lower-cased to bridge SQL Server (upper) vs PG (lower).
|
|
322
|
+
*/
|
|
323
|
+
serializePrimaryKey(entityInfo, primaryKey) {
|
|
324
|
+
const parts = entityInfo.PrimaryKeys.map((pk) => {
|
|
325
|
+
const val = primaryKey[pk.Name];
|
|
326
|
+
let valStr = val !== undefined && val !== null ? String(val) : '';
|
|
327
|
+
const pkType = pk.Type ?? '';
|
|
328
|
+
const normalizedType = pkType.trim().toLowerCase();
|
|
329
|
+
if (IsUuidSQLType(pkType) || normalizedType === 'uniqueidentifier' || normalizedType === 'uuid') {
|
|
330
|
+
valStr = valStr.toLowerCase().trim();
|
|
331
|
+
}
|
|
332
|
+
return `${pk.Name.toLowerCase()}=${valStr}`;
|
|
333
|
+
});
|
|
334
|
+
parts.sort();
|
|
335
|
+
return parts.join('|');
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Walk every entity referenced by metadata files (including nested
|
|
339
|
+
* `relatedEntities`) and build one unfiltered `BaseEngine` config per
|
|
340
|
+
* entity. Each config triggers a single `SELECT * FROM vw<Entity>`
|
|
341
|
+
* (capped by `IgnoreMaxRows: true` in BaseEngine), pre-populating the
|
|
342
|
+
* per-entity slot the sync path will read from.
|
|
343
|
+
*/
|
|
344
|
+
async buildDynamicConfigs() {
|
|
345
|
+
const entitiesFound = new Set();
|
|
346
|
+
for (const entityDir of this.entityDirs) {
|
|
347
|
+
const entityConfig = await loadEntityConfig(entityDir);
|
|
348
|
+
if (!entityConfig)
|
|
349
|
+
continue;
|
|
350
|
+
const pattern = entityConfig.filePattern || '*.json';
|
|
351
|
+
const files = await fastGlob(pattern, {
|
|
352
|
+
cwd: entityDir,
|
|
353
|
+
absolute: true,
|
|
354
|
+
onlyFiles: true,
|
|
355
|
+
dot: true,
|
|
356
|
+
ignore: ['**/node_modules/**', '**/.mj-*.json']
|
|
357
|
+
});
|
|
358
|
+
await this.processFilesInParallel(files, entityConfig.entity, entitiesFound);
|
|
359
|
+
}
|
|
360
|
+
const configs = [];
|
|
361
|
+
for (const entityName of entitiesFound) {
|
|
362
|
+
const entityInfo = this.syncEngine.getEntityInfo(entityName);
|
|
363
|
+
if (!entityInfo)
|
|
364
|
+
continue;
|
|
365
|
+
this.markEntityAsPreloaded(entityName);
|
|
366
|
+
configs.push({
|
|
367
|
+
PropertyName: this.getPropertyNameForEntity(entityName),
|
|
368
|
+
EntityName: entityName,
|
|
369
|
+
Type: 'entity',
|
|
370
|
+
ResultType: 'entity_object',
|
|
371
|
+
// No Filter — load all rows. Metadata entities are bounded by design;
|
|
372
|
+
// the few thousand-row tables we touch are faster to bulk-load than
|
|
373
|
+
// to fence with a giant WHERE clause. See the module docstring for
|
|
374
|
+
// the full reasoning.
|
|
375
|
+
AutoRefresh: true
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
return configs;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* After `BaseEngine.Load` finishes, walk the configs and check whether any
|
|
382
|
+
* single preloaded entity exceeded the metadata-scale threshold. Emit a
|
|
383
|
+
* warning so the operator knows their sync is loading something it
|
|
384
|
+
* probably shouldn't be.
|
|
385
|
+
*/
|
|
386
|
+
warnOnOversizedSlots(configs) {
|
|
387
|
+
for (const config of configs) {
|
|
388
|
+
if (!config.PropertyName || !config.EntityName)
|
|
389
|
+
continue;
|
|
390
|
+
const list = this[config.PropertyName];
|
|
391
|
+
if (!Array.isArray(list))
|
|
392
|
+
continue;
|
|
393
|
+
if (list.length > LARGE_ENTITY_WARN_THRESHOLD) {
|
|
394
|
+
this.warnings.push(`Preloaded ${list.length.toLocaleString()} rows of '${config.EntityName}'. mj sync is intended for metadata-scale entities (hundreds to low tens of thousands); this is large enough that you may want to confirm this entity belongs in a sync workflow.`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Recursively walk records collecting every entity name they (or their
|
|
400
|
+
* `relatedEntities`) reference. We don't collect primary keys because
|
|
401
|
+
* preload is unfiltered — knowing the entity is enough.
|
|
402
|
+
*
|
|
403
|
+
* Malformed `relatedEntities` shapes (non-array values) produce a warning
|
|
404
|
+
* pointing at the offending file rather than silently being skipped.
|
|
405
|
+
*/
|
|
406
|
+
collectEntitiesRecursive(records, entityName, entitiesFound, filePath) {
|
|
407
|
+
entitiesFound.add(entityName);
|
|
408
|
+
for (const record of records) {
|
|
409
|
+
const deleteRecord = record.deleteRecord;
|
|
410
|
+
if (deleteRecord?.delete === true) {
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
const relatedEntities = record.relatedEntities;
|
|
414
|
+
if (!relatedEntities)
|
|
415
|
+
continue;
|
|
416
|
+
for (const [relatedEntityName, relatedRecords] of Object.entries(relatedEntities)) {
|
|
417
|
+
if (Array.isArray(relatedRecords)) {
|
|
418
|
+
this.collectEntitiesRecursive(relatedRecords, relatedEntityName, entitiesFound, filePath);
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
this.warnings.push(`Skipped preload for relatedEntities['${relatedEntityName}'] in ${filePath}: expected an array, got ${typeof relatedRecords}.`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Process file reads in bounded-parallel batches. `fs.readJson` is I/O-bound;
|
|
428
|
+
* doing this sequentially was the dominant cost on metadata trees with
|
|
429
|
+
* thousands of small files.
|
|
430
|
+
*/
|
|
431
|
+
async processFilesInParallel(files, entityName, entitiesFound) {
|
|
432
|
+
for (let i = 0; i < files.length; i += FILE_READ_CONCURRENCY) {
|
|
433
|
+
const batch = files.slice(i, i + FILE_READ_CONCURRENCY);
|
|
434
|
+
const parsed = await Promise.all(batch.map(filePath => this.readAndPreprocessFile(filePath)));
|
|
435
|
+
for (const { filePath, rawData, fileData } of parsed) {
|
|
436
|
+
this.cacheFile(filePath, rawData, fileData);
|
|
437
|
+
const records = Array.isArray(fileData) ? fileData : [fileData];
|
|
438
|
+
this.collectEntitiesRecursive(records, entityName, entitiesFound, filePath);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
async readAndPreprocessFile(filePath) {
|
|
443
|
+
const rawData = await fs.readJson(filePath);
|
|
444
|
+
let fileData = rawData;
|
|
445
|
+
const jsonString = JSON.stringify(rawData);
|
|
446
|
+
const hasIncludes = jsonString.includes('"@include"') || jsonString.includes('"@include.');
|
|
447
|
+
if (hasIncludes) {
|
|
448
|
+
const preprocessor = new JsonPreprocessor();
|
|
449
|
+
fileData = await preprocessor.processFile(filePath);
|
|
450
|
+
}
|
|
451
|
+
return { filePath, rawData, fileData };
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
//# sourceMappingURL=sync-metadata-engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-metadata-engine.js","sourceRoot":"","sources":["../../src/lib/sync-metadata-engine.ts"],"names":[],"mappings":"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"}
|
|
@@ -48,6 +48,7 @@ export declare class PushService {
|
|
|
48
48
|
private deferredFileWrites;
|
|
49
49
|
private deferredRecords;
|
|
50
50
|
private stateManager;
|
|
51
|
+
private syncMetadataEngine;
|
|
51
52
|
constructor(syncEngine: SyncEngine, contextUser: UserInfo, stateManager?: SyncStateManager);
|
|
52
53
|
/** Set or replace the state manager after construction. */
|
|
53
54
|
setStateManager(stateManager: SyncStateManager): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PushService.d.ts","sourceRoot":"","sources":["../../src/services/PushService.ts"],"names":[],"mappings":"AAGA,OAAO,EAAwB,QAAQ,EAAqB,MAAM,sBAAsB,CAAC;AAGzF,OAAO,EAAE,UAAU,EAA4E,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"PushService.d.ts","sourceRoot":"","sources":["../../src/services/PushService.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,6 +5,7 @@ import { Metadata } from '@memberjunction/core';
|
|
|
5
5
|
import { UUIDsEqual } from '@memberjunction/global';
|
|
6
6
|
import { IsStringSQLType } from '@memberjunction/sql-dialect';
|
|
7
7
|
import { DeferrableLookupError } from '../lib/sync-engine.js';
|
|
8
|
+
import { SyncMetadataEngine } from '../lib/sync-metadata-engine.js';
|
|
8
9
|
import { BatchContextIndex } from '../lib/batch-context-index.js';
|
|
9
10
|
import { loadEntityConfig, loadSyncConfig } from '../config.js';
|
|
10
11
|
import { FileBackupManager } from '../lib/file-backup-manager.js';
|
|
@@ -34,6 +35,10 @@ export class PushService {
|
|
|
34
35
|
this.syncEngine = syncEngine;
|
|
35
36
|
this.contextUser = contextUser;
|
|
36
37
|
this.stateManager = stateManager;
|
|
38
|
+
// Initialize SyncMetadataEngine
|
|
39
|
+
this.syncMetadataEngine = new SyncMetadataEngine();
|
|
40
|
+
this.syncMetadataEngine.initializeEngine(this.syncEngine);
|
|
41
|
+
this.syncEngine.setMetadataEngine(this.syncMetadataEngine);
|
|
37
42
|
}
|
|
38
43
|
/** Set or replace the state manager after construction. */
|
|
39
44
|
setStateManager(stateManager) {
|
|
@@ -152,6 +157,18 @@ export class PushService {
|
|
|
152
157
|
if (entityDirs.length === 0) {
|
|
153
158
|
throw new Error('No entity directories found');
|
|
154
159
|
}
|
|
160
|
+
// Preload entities and cache files.
|
|
161
|
+
// We pass the SyncEngine's provider explicitly rather than reaching for
|
|
162
|
+
// Metadata.Provider — `mj sync` is single-process so they resolve to the
|
|
163
|
+
// same instance today, but routing through SyncEngine keeps the wiring
|
|
164
|
+
// self-consistent and makes future provider plumbing trivial.
|
|
165
|
+
callbacks?.onLog?.('⚡ Preloading metadata and caching files...');
|
|
166
|
+
this.syncMetadataEngine.setEntityDirs(entityDirs);
|
|
167
|
+
await this.syncMetadataEngine.Config(true, this.contextUser, this.syncEngine.getProvider());
|
|
168
|
+
for (const warning of this.syncMetadataEngine.drainWarnings()) {
|
|
169
|
+
callbacks?.onWarn?.(` ⚠️ ${warning}`);
|
|
170
|
+
}
|
|
171
|
+
callbacks?.onLog?.('✓ Preload completed successfully\n');
|
|
155
172
|
if (options.verbose) {
|
|
156
173
|
callbacks?.onLog?.(`Found ${entityDirs.length} entity ${entityDirs.length === 1 ? 'directory' : 'directories'} to process`);
|
|
157
174
|
}
|
|
@@ -390,20 +407,29 @@ export class PushService {
|
|
|
390
407
|
if (!options.dryRun) {
|
|
391
408
|
await fileBackupManager.backupFile(filePath);
|
|
392
409
|
}
|
|
393
|
-
//
|
|
394
|
-
|
|
410
|
+
// Check if file is already cached to avoid double I/O
|
|
411
|
+
let rawFileData;
|
|
412
|
+
let fileData;
|
|
413
|
+
const cachedFile = this.syncMetadataEngine.getCachedFile(filePath);
|
|
414
|
+
if (cachedFile) {
|
|
415
|
+
rawFileData = cachedFile.rawData;
|
|
416
|
+
fileData = cachedFile.fileData;
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
// Fallback (e.g. if not preloaded)
|
|
420
|
+
rawFileData = await fs.readJson(filePath);
|
|
421
|
+
fileData = rawFileData;
|
|
422
|
+
const jsonString = JSON.stringify(rawFileData);
|
|
423
|
+
const hasIncludes = jsonString.includes('"@include"') || jsonString.includes('"@include.');
|
|
424
|
+
if (hasIncludes) {
|
|
425
|
+
const jsonPreprocessor = new JsonPreprocessor();
|
|
426
|
+
fileData = await jsonPreprocessor.processFile(filePath);
|
|
427
|
+
}
|
|
428
|
+
this.syncMetadataEngine.cacheFile(filePath, rawFileData, fileData);
|
|
429
|
+
}
|
|
395
430
|
// Keep unprocessed data to write back (preserves @file: references)
|
|
396
431
|
const unprocessedRecords = Array.isArray(rawFileData) ? rawFileData : [rawFileData];
|
|
397
432
|
const isArray = Array.isArray(rawFileData);
|
|
398
|
-
// Preprocess @include directives before checksumming, so changes to
|
|
399
|
-
// included files are detected by the incremental skip check.
|
|
400
|
-
let fileData = rawFileData;
|
|
401
|
-
const jsonString = JSON.stringify(rawFileData);
|
|
402
|
-
const hasIncludes = jsonString.includes('"@include"') || jsonString.includes('"@include.');
|
|
403
|
-
if (hasIncludes) {
|
|
404
|
-
const jsonPreprocessor = new JsonPreprocessor();
|
|
405
|
-
fileData = await jsonPreprocessor.processFile(filePath);
|
|
406
|
-
}
|
|
407
433
|
// Compute checksum on the RESOLVED content (after @include preprocessing)
|
|
408
434
|
// so that changes to included files are properly detected.
|
|
409
435
|
let cachedChecksum;
|
|
@@ -584,6 +610,9 @@ export class PushService {
|
|
|
584
610
|
else {
|
|
585
611
|
await JsonWriteHelper.writeOrderedRecordData(filePath, unprocessedRecords[0]);
|
|
586
612
|
}
|
|
613
|
+
// Drop the cached snapshot — file on disk no longer matches it,
|
|
614
|
+
// and any later reader within this push must see fresh contents.
|
|
615
|
+
this.syncMetadataEngine.invalidateCachedFile(filePath);
|
|
587
616
|
}
|
|
588
617
|
}
|
|
589
618
|
// Update stored checksum after successful processing (reuse cached value if available)
|
|
@@ -1243,6 +1272,8 @@ export class PushService {
|
|
|
1243
1272
|
}
|
|
1244
1273
|
throw new Error(`Failed to delete ${entityName} record: ${errorMessage}`);
|
|
1245
1274
|
}
|
|
1275
|
+
// Remove from metadata cache
|
|
1276
|
+
this.syncMetadataEngine.removeEntityFromCache(entityName, record.primaryKey);
|
|
1246
1277
|
// Set deletedAt timestamp after successful deletion (only for metadata records)
|
|
1247
1278
|
if (!isDbOnly) {
|
|
1248
1279
|
if (!record.deleteRecord) {
|
|
@@ -1654,6 +1685,8 @@ export class PushService {
|
|
|
1654
1685
|
else {
|
|
1655
1686
|
await JsonWriteHelper.writeOrderedRecordData(deferredWrite.filePath, deferredWrite.records[0]);
|
|
1656
1687
|
}
|
|
1688
|
+
// File contents just changed on disk — drop the stale cache entry.
|
|
1689
|
+
this.syncMetadataEngine.invalidateCachedFile(deferredWrite.filePath);
|
|
1657
1690
|
}
|
|
1658
1691
|
catch (error) {
|
|
1659
1692
|
callbacks?.onWarn?.(` ⚠️ Failed to write ${deferredWrite.filePath}: ${error}`);
|