Skip to content

Commit fca03ad

Browse files
committed
Fix infinite loop when references=all for PhysicalInstance and Dataset
- Add visited set tracking to prevent circular reference infinite loops - Detect when an object has already been visited by URN - Mark circular references with _circularRef: true in the response - This fixes the RangeError: Maximum call stack size exceeded error
1 parent 11653ef commit fca03ad

1 file changed

Lines changed: 36 additions & 14 deletions

File tree

mocks/server.js

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ function getResolvedPropertyName(refPropertyName) {
193193
}
194194

195195
// Helper to resolve a single reference
196-
function resolveSingleReference(ref, level, isRecursive, currentDepth = 0) {
196+
function resolveSingleReference(ref, level, isRecursive, currentDepth = 0, visited = new Set()) {
197197
if (!ref || typeof ref !== 'object') return ref;
198198

199199
const refId = extractId(ref);
@@ -211,7 +211,8 @@ function resolveSingleReference(ref, level, isRecursive, currentDepth = 0) {
211211
if (resolvedObj) {
212212
// If recursive, resolve all references in the resolved object
213213
// Pass currentDepth + 1 to continue recursive resolution
214-
return isRecursive ? resolveReferences(resolvedObj, level, currentDepth + 1) : resolvedObj;
214+
// Also pass the visited set to prevent infinite loops
215+
return isRecursive ? resolveReferences(resolvedObj, level, currentDepth + 1, visited) : resolvedObj;
215216
}
216217

217218
return ref;
@@ -220,9 +221,30 @@ function resolveSingleReference(ref, level, isRecursive, currentDepth = 0) {
220221
// Helper to resolve references in an object (truly recursive and generic)
221222
// level: 'none' (default), 'children' (first level only), 'all' (recursive)
222223
// startDepth: starting depth for recursive processing (used internally)
223-
function resolveReferences(obj, level, startDepth = 0) {
224+
// visited: Set of already visited URNs to prevent circular reference infinite loops
225+
function resolveReferences(obj, level, startDepth = 0, visited = new Set()) {
224226
if (!level || level === 'none' || !obj || typeof obj !== 'object') return obj;
225227

228+
// Prevent infinite recursion by tracking visited URNs
229+
const objUrn = obj.urn;
230+
if (objUrn && visited.has(objUrn)) {
231+
// Already visited this object - return reference instead of full object
232+
return {
233+
urn: obj.urn,
234+
id: obj.id,
235+
agencyID: obj.agencyID,
236+
version: obj.version,
237+
typeOfObject: obj.typeOfObject,
238+
_circularRef: true
239+
};
240+
}
241+
242+
// Add current URN to visited set (only for recursive 'all' level)
243+
if (level === 'all' && objUrn) {
244+
visited = new Set(visited);
245+
visited.add(objUrn);
246+
}
247+
226248
const resolved = JSON.parse(JSON.stringify(obj)); // Deep clone
227249
const isRecursive = level === 'all';
228250

@@ -236,12 +258,12 @@ function resolveReferences(obj, level, startDepth = 0) {
236258
}
237259

238260
// Recursively process object properties
239-
function processObject(objToProcess, depth = 0) {
261+
function processObject(objToProcess, depth = 0, visitedSet = visited) {
240262
if (!objToProcess || typeof objToProcess !== 'object') return objToProcess;
241263

242264
// Special handling for arrays
243265
if (Array.isArray(objToProcess)) {
244-
return objToProcess.map(item => processObject(item, depth));
266+
return objToProcess.map(item => processObject(item, depth, visitedSet));
245267
}
246268

247269
const processed = {};
@@ -258,7 +280,7 @@ function resolveReferences(obj, level, startDepth = 0) {
258280
// For 'children' level: resolve only at depth 0
259281
// For 'all' level: resolve at all depths (truly recursive)
260282
if (depth === 0 || isRecursive) {
261-
const resolved = resolveSingleReference(value, level, isRecursive, depth);
283+
const resolved = resolveSingleReference(value, level, isRecursive, depth, visited);
262284
// Replace xxxReference with xxx when resolved
263285
const resolvedKey = getResolvedPropertyName(key);
264286
processed[resolvedKey] = resolved;
@@ -271,7 +293,7 @@ function resolveReferences(obj, level, startDepth = 0) {
271293
else if (key === 'representation' && value?.codeRepresentation?.codeListReference) {
272294
const codeListRef = value.codeRepresentation.codeListReference;
273295
if (depth === 0 || isRecursive) {
274-
const resolved = resolveSingleReference(codeListRef, level, isRecursive, depth);
296+
const resolved = resolveSingleReference(codeListRef, level, isRecursive, depth, visited);
275297
// Create a new codeRepresentation object without codeListReference
276298
const { codeListReference, ...codeRepresentationWithoutRef } = value.codeRepresentation;
277299
processed[key] = {
@@ -303,7 +325,7 @@ function resolveReferences(obj, level, startDepth = 0) {
303325
const categories = loadMock('categories.json');
304326
const category = findById(categories, categoryId);
305327
if (category) {
306-
const resolvedCategory = isRecursive ? resolveReferences(category, level, depth + 1) : category;
328+
const resolvedCategory = isRecursive ? resolveReferences(category, level, depth + 1, visited) : category;
307329
// Exclude categoryReference when resolving to category
308330
const { categoryReference, ...codeWithoutRef } = code;
309331
return {
@@ -313,7 +335,7 @@ function resolveReferences(obj, level, startDepth = 0) {
313335
}
314336
}
315337
}
316-
return isRecursive ? processObject(code, depth + 1) : code;
338+
return isRecursive ? processObject(code, depth + 1, visited) : code;
317339
});
318340
}
319341
// Special handling for scheme children (concepts, variables, codeLists, categories)
@@ -333,7 +355,7 @@ function resolveReferences(obj, level, startDepth = 0) {
333355
// For 'children' level: resolve object but not its internal references
334356
// For 'all' level: resolve recursively with all references
335357
if (isRecursive) {
336-
return resolveReferences(child, level, depth + 1);
358+
return resolveReferences(child, level, depth + 1, visited);
337359
} else {
338360
// For 'children' level, return the full object but don't resolve its internal references
339361
return child;
@@ -342,18 +364,18 @@ function resolveReferences(obj, level, startDepth = 0) {
342364
}
343365
}
344366
// If not an identifier or not found, process as normal
345-
return processObject(identifier, depth + 1);
367+
return processObject(identifier, depth + 1, visited);
346368
});
347369
} else {
348-
processed[key] = value.map(item => processObject(item, depth + 1));
370+
processed[key] = value.map(item => processObject(item, depth + 1, visited));
349371
}
350372
} else {
351-
processed[key] = value.map(item => processObject(item, depth + 1));
373+
processed[key] = value.map(item => processObject(item, depth + 1, visited));
352374
}
353375
}
354376
// Recursively process nested objects
355377
else if (value && typeof value === 'object') {
356-
processed[key] = processObject(value, depth + 1);
378+
processed[key] = processObject(value, depth + 1, visited);
357379
}
358380
// Keep primitive values as-is
359381
else {

0 commit comments

Comments
 (0)