Skip to content

Commit 71238fd

Browse files
fix(TraversionGraph): resolve infinite loop in path search
* Replace visited array with minCosts map to prune redundant paths * Remove flawed visitedBorder property from QueueNode * Improve performance by skipping nodes with higher or equal costs * Prevent exponential path explosion during complex conversions like PPTX to PDF Fixes # 605
1 parent 6998584 commit 71238fd

File tree

1 file changed

+22
-14
lines changed

1 file changed

+22
-14
lines changed

src/TraversionGraph.ts

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ interface QueueNode {
55
index: number;
66
cost: number;
77
path: ConvertPathNode[];
8-
visitedBorder: number;
98
};
109
interface CategoryChangeCost {
1110
from: string;
@@ -19,7 +18,6 @@ interface CategoryAdaptiveCost {
1918
cost: number; // Cost to apply when a conversion involves all of the specified categories in sequence.
2019
}
2120

22-
2321
// Parameters for pathfinding algorithm.
2422
const DEPTH_COST: number = 1; // Base cost for each conversion step. Higher values will make the algorithm prefer shorter paths more strongly.
2523
const DEFAULT_CATEGORY_CHANGE_COST : number = 0.6; // Default cost for category changes not specified in CATEGORY_CHANGE_COSTS
@@ -333,30 +331,35 @@ export class TraversionGraph {
333331

334332
public async* searchPath(from: ConvertPathNode, to: ConvertPathNode, simpleMode: boolean) : AsyncGenerator<ConvertPathNode[]> {
335333
// Dijkstra's algorithm
336-
// Priority queue of {index, cost, path}
337334
let queue: PriorityQueue<QueueNode> = new PriorityQueue<QueueNode>(
338335
1000,
339336
(a: QueueNode, b: QueueNode) => a.cost - b.cost
340337
);
341-
let visited = new Array<number>();
338+
339+
let minCosts = new Map<number, number>();
340+
342341
const fromIdentifier = from.format.mime + `(${from.format.format})`;
343342
const toIdentifier = to.format.mime + `(${to.format.format})`;
344343
let fromIndex = this.nodes.findIndex(node => node.identifier === fromIdentifier);
345344
let toIndex = this.nodes.findIndex(node => node.identifier === toIdentifier);
346345
if (fromIndex === -1 || toIndex === -1) return []; // If either format is not in the graph, return empty array
347-
queue.add({index: fromIndex, cost: 0, path: [from], visitedBorder: visited.length });
346+
347+
queue.add({index: fromIndex, cost: 0, path: [from]});
348+
minCosts.set(fromIndex, 0);
349+
348350
console.log(`Starting path search from ${from.format.mime}(${from.handler?.name}) to ${to.format.mime}(${to.handler?.name}) (simple mode: ${simpleMode})`);
349351
let iterations = 0;
350352
let pathsFound = 0;
353+
351354
while (queue.size() > 0) {
352355
iterations++;
353-
// Get the node with the lowest cost
354356
let current = queue.poll()!;
355-
const indexInVisited = visited.indexOf(current.index);
356-
if (indexInVisited >= 0 && indexInVisited < current.visitedBorder) {
357+
358+
if (current.index !== toIndex && current.cost > (minCosts.get(current.index) ?? Infinity)) {
357359
this.dispatchEvent("skipped", current.path);
358360
continue;
359361
}
362+
360363
if (current.index === toIndex) {
361364
// Return the path of handlers and formats to get from the input format to the output format
362365
const logString = `${iterations} with cost ${current.cost.toFixed(3)}: ${current.path.map(p => p.handler.name + "(" + p.format.mime + ")").join(" → ")}`;
@@ -373,21 +376,26 @@ export class TraversionGraph {
373376
}
374377
continue;
375378
}
376-
visited.push(current.index);
379+
377380
this.dispatchEvent("searching", current.path);
378381
this.nodes[current.index].edges.forEach(edgeIndex => {
379382
let edge = this.edges[edgeIndex];
380-
const indexInVisited = visited.indexOf(edge.to.index);
381-
if (indexInVisited >= 0 && indexInVisited < current.visitedBorder) return;
383+
382384
const handler = this.handlers.find(h => h.name === edge.handler);
383385
if (!handler) return; // If the handler for this edge is not found, skip it
384386

385387
let path = current.path.concat({handler: handler, format: edge.to.format});
388+
let newCost = current.cost + edge.cost + this.calculateAdaptiveCost(path);
389+
390+
if (edge.to.index !== toIndex) {
391+
if (newCost >= (minCosts.get(edge.to.index) ?? Infinity)) return;
392+
minCosts.set(edge.to.index, newCost);
393+
}
394+
386395
queue.add({
387396
index: edge.to.index,
388-
cost: current.cost + edge.cost + this.calculateAdaptiveCost(path),
389-
path: path,
390-
visitedBorder: visited.length
397+
cost: newCost,
398+
path: path
391399
});
392400
});
393401
if (iterations % LOG_FREQUENCY === 0) {

0 commit comments

Comments
 (0)