Skip to content

Commit fbbb829

Browse files
authored
Use using for cycle tracker (#1434)
* Reworks the cycle tracker to use the using feature * added IDisposable type * Typescript provides the Disposable type already
1 parent 2e15fc8 commit fbbb829

File tree

1 file changed

+108
-135
lines changed

1 file changed

+108
-135
lines changed

packages/runner/src/traverse.ts

Lines changed: 108 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -92,16 +92,20 @@ export class CycleTracker<K> {
9292
constructor() {
9393
this.partial = new Set<K>();
9494
}
95-
enter(k: K): boolean {
95+
include(k: K, context?: unknown): Disposable | null {
9696
if (this.partial.has(k)) {
97-
console.error("Cycle Detected!");
98-
return false;
97+
console.error(
98+
"Cycle Detected!",
99+
context == null ? JSON.stringify(k) : JSON.stringify(context),
100+
);
101+
return null;
99102
}
100103
this.partial.add(k);
101-
return true;
102-
}
103-
exit(k: K) {
104-
this.partial.delete(k);
104+
return {
105+
[Symbol.dispose]: () => {
106+
this.partial.delete(k);
107+
},
108+
};
105109
}
106110
}
107111

@@ -204,22 +208,17 @@ export abstract class BaseObjectTraverser<K, S> {
204208
if (isPrimitive(doc.value)) {
205209
return doc.value;
206210
} else if (Array.isArray(doc.value)) {
207-
if (tracker.enter(doc.value)) {
208-
try {
209-
return doc.value.map((item, index) =>
210-
this.traverseDAG(
211-
{ ...doc, path: [...doc.path, index.toString()], value: item },
212-
tracker,
213-
schemaTracker,
214-
)
215-
) as Immutable<JSONValue>[];
216-
} finally {
217-
tracker.exit(doc.value);
218-
}
219-
} else {
220-
console.log("Cycle detected", JSON.stringify(doc));
211+
using t = tracker.include(doc.value, doc);
212+
if (t === null) {
221213
return null;
222214
}
215+
return doc.value.map((item, index) =>
216+
this.traverseDAG(
217+
{ ...doc, path: [...doc.path, index.toString()], value: item },
218+
tracker,
219+
schemaTracker,
220+
)
221+
) as Immutable<JSONValue>[];
223222
} else if (isObject(doc.value)) {
224223
// First, see if we need special handling
225224
if (isAnyCellLink(doc.value)) {
@@ -236,31 +235,26 @@ export abstract class BaseObjectTraverser<K, S> {
236235
}
237236
return this.traverseDAG(newDoc, tracker, schemaTracker);
238237
} else {
239-
if (tracker.enter(doc.value)) {
240-
try {
241-
return Object.fromEntries(
242-
Object.entries(doc.value).map((
243-
[k, value],
244-
) => [
245-
k,
246-
this.traverseDAG(
247-
{
248-
...doc,
249-
path: [...doc.path, k],
250-
value: value,
251-
},
252-
tracker,
253-
schemaTracker,
254-
),
255-
]),
256-
) as Immutable<JSONValue>;
257-
} finally {
258-
tracker.exit(doc.value);
259-
}
260-
} else {
261-
console.log("Cycle detected", JSON.stringify(doc));
238+
using t = tracker.include(doc.value, doc);
239+
if (t === null) {
262240
return null;
263241
}
242+
return Object.fromEntries(
243+
Object.entries(doc.value).map((
244+
[k, value],
245+
) => [
246+
k,
247+
this.traverseDAG(
248+
{
249+
...doc,
250+
path: [...doc.path, k],
251+
value: value,
252+
},
253+
tracker,
254+
schemaTracker,
255+
),
256+
]),
257+
) as Immutable<JSONValue>;
264258
}
265259
} else {
266260
console.error("Encountered unexpected object: ", doc.value);
@@ -360,75 +354,69 @@ function followPointer<K, S>(
360354
schemaTracker?: MapSet<string, SchemaPathSelector>,
361355
selector?: SchemaPathSelector,
362356
): [ValueAtPath<K>, SchemaPathSelector | undefined] {
363-
if (!tracker.enter(doc.value!)) {
364-
console.log("Cycle detected", JSON.stringify(doc));
357+
using t = tracker.include(doc.value!, doc);
358+
if (t === null) {
365359
return [{ ...doc, path: [], value: undefined }, selector];
366360
}
367-
try {
368-
const link = parseLink(doc.value)!;
369-
const target = (link.id !== undefined)
370-
? manager.getTarget(link.id)
371-
: doc.doc;
372-
let [targetDoc, targetDocRoot] = [doc.doc, doc.docRoot];
373-
if (selector !== undefined) {
374-
// We'll need to re-root the selector for the target doc
375-
// Remove the portions of doc.path from selector.path, limiting schema if needed
376-
// Also insert the portions of cellTarget.path, so selector is relative to new target doc
377-
// We do this even if the target doc is the same doc, since we want the
378-
// selector path to match.
379-
selector = narrowSchema(doc.path, selector, link.path as string[]);
361+
const link = parseLink(doc.value)!;
362+
const target = (link.id !== undefined) ? manager.getTarget(link.id) : doc.doc;
363+
let [targetDoc, targetDocRoot] = [doc.doc, doc.docRoot];
364+
if (selector !== undefined) {
365+
// We'll need to re-root the selector for the target doc
366+
// Remove the portions of doc.path from selector.path, limiting schema if needed
367+
// Also insert the portions of cellTarget.path, so selector is relative to new target doc
368+
// We do this even if the target doc is the same doc, since we want the
369+
// selector path to match.
370+
selector = narrowSchema(doc.path, selector, link.path as string[]);
371+
}
372+
if (link.id !== undefined) {
373+
// We have a reference to a different cell, so track the dependency
374+
// and update our targetDoc and targetDocRoot
375+
const valueEntry = manager.load(target);
376+
if (valueEntry === null) {
377+
return [{ ...doc, path: [], value: undefined }, selector];
380378
}
381-
if (link.id !== undefined) {
382-
// We have a reference to a different cell, so track the dependency
383-
// and update our targetDoc and targetDocRoot
384-
const valueEntry = manager.load(target);
385-
if (valueEntry === null) {
386-
return [{ ...doc, path: [], value: undefined }, selector];
387-
}
388-
if (schemaTracker !== undefined && selector !== undefined) {
389-
schemaTracker.add(manager.toKey(target), selector);
390-
}
391-
// If the object we're pointing to is a retracted fact, just return undefined.
392-
// We can't do a better match, but we do want to include the result so we watch this doc
393-
if (valueEntry.value === undefined) {
394-
return [
395-
{ doc: target, docRoot: undefined, path: [], value: undefined },
396-
selector,
397-
];
398-
}
399-
// Otherwise, we can continue with the target.
400-
// an assertion fact.is will be an object with a value property, and
401-
// that's what our schema is relative to.
402-
targetDoc = target;
403-
const targetObj = valueEntry.value as Immutable<JSONObject>;
404-
targetDocRoot = targetObj["value"];
405-
// Load any sources (recursively) if they exist and any linked recipes
406-
loadSource(
407-
manager,
408-
valueEntry,
409-
new Set<string>(),
410-
schemaTracker,
411-
);
379+
if (schemaTracker !== undefined && selector !== undefined) {
380+
schemaTracker.add(manager.toKey(target), selector);
412381
}
413-
414-
// We've loaded the linked doc, so walk the path to get to the right part of that doc (or whatever doc that path leads to),
415-
// then the provided path from the arguments.
416-
return getAtPath(
382+
// If the object we're pointing to is a retracted fact, just return undefined.
383+
// We can't do a better match, but we do want to include the result so we watch this doc
384+
if (valueEntry.value === undefined) {
385+
return [
386+
{ doc: target, docRoot: undefined, path: [], value: undefined },
387+
selector,
388+
];
389+
}
390+
// Otherwise, we can continue with the target.
391+
// an assertion fact.is will be an object with a value property, and
392+
// that's what our schema is relative to.
393+
targetDoc = target;
394+
const targetObj = valueEntry.value as Immutable<JSONObject>;
395+
targetDocRoot = targetObj["value"];
396+
// Load any sources (recursively) if they exist and any linked recipes
397+
loadSource(
417398
manager,
418-
{
419-
doc: targetDoc,
420-
docRoot: targetDocRoot,
421-
path: [],
422-
value: targetDocRoot,
423-
},
424-
[...link.path, ...path] as string[],
425-
tracker,
399+
valueEntry,
400+
new Set<string>(),
426401
schemaTracker,
427-
selector,
428402
);
429-
} finally {
430-
tracker.exit(doc.value!);
431403
}
404+
405+
// We've loaded the linked doc, so walk the path to get to the right part of that doc (or whatever doc that path leads to),
406+
// then the provided path from the arguments.
407+
return getAtPath(
408+
manager,
409+
{
410+
doc: targetDoc,
411+
docRoot: targetDocRoot,
412+
path: [],
413+
value: targetDocRoot,
414+
},
415+
[...link.path, ...path] as string[],
416+
tracker,
417+
schemaTracker,
418+
selector,
419+
);
432420
}
433421

434422
// Recursively load the source from the doc ()
@@ -677,19 +665,14 @@ export class SchemaObjectTraverser<K, S> extends BaseObjectTraverser<K, S> {
677665
return this.isValidType(schemaObj, "number") ? doc.value : undefined;
678666
} else if (Array.isArray(doc.value)) {
679667
if (this.isValidType(schemaObj, "array")) {
680-
if (this.tracker.enter(doc.value)) {
681-
try {
682-
return this.traverseArrayWithSchema(doc, {
683-
schema: schemaObj,
684-
rootSchema: schemaContext.rootSchema,
685-
});
686-
} finally {
687-
this.tracker.exit(doc.value);
688-
}
689-
} else {
690-
console.log("Cycle detected", JSON.stringify(doc));
668+
using t = this.tracker.include(doc.value, doc);
669+
if (t === null) {
691670
return null;
692671
}
672+
return this.traverseArrayWithSchema(doc, {
673+
schema: schemaObj,
674+
rootSchema: schemaContext.rootSchema,
675+
});
693676
}
694677
return undefined;
695678
} else if (isObject(doc.value)) {
@@ -701,19 +684,14 @@ export class SchemaObjectTraverser<K, S> extends BaseObjectTraverser<K, S> {
701684
// TODO(@ubik2): it might be technically ok to follow the same pointer more than once, since we might have
702685
// a different schema the second time, which could prevent an infinite cycle, but for now, just reject these.
703686
} else if (this.isValidType(schemaObj, "object")) {
704-
if (this.tracker.enter(doc.value)) {
705-
try {
706-
return this.traverseObjectWithSchema(doc, {
707-
schema: schemaObj,
708-
rootSchema: schemaContext.rootSchema,
709-
});
710-
} finally {
711-
this.tracker.exit(doc.value);
712-
}
713-
} else {
714-
console.log("Cycle detected", JSON.stringify(doc));
687+
using t = this.tracker.include(doc.value, doc);
688+
if (t === null) {
715689
return null;
716690
}
691+
return this.traverseObjectWithSchema(doc, {
692+
schema: schemaObj,
693+
rootSchema: schemaContext.rootSchema,
694+
});
717695
}
718696
}
719697
}
@@ -837,16 +815,11 @@ export class SchemaObjectTraverser<K, S> extends BaseObjectTraverser<K, S> {
837815
// but we may have a pointer cycle of docs, and we've finished resolving
838816
// the pointer now. To avoid descending into a cycle, track entry to the
839817
// doc we were called with (not the one we resolved, which may be a pointer).
840-
if (this.tracker.enter(doc.value!)) {
841-
try {
842-
return this.traverseWithSelector(newDoc, newSelector!);
843-
} finally {
844-
this.tracker.exit(doc.value!);
845-
}
846-
} else {
847-
console.log("Cycle detected", JSON.stringify(doc));
818+
using t = this.tracker.include(doc.value!, doc);
819+
if (t === null) {
848820
return null;
849821
}
822+
return this.traverseWithSelector(newDoc, newSelector!);
850823
}
851824
}
852825

0 commit comments

Comments
 (0)