Visitar URL original
fix(workflow): terminate finite port probing without floating-point progress by BlueboxC · Pull Request #1110 · tt-a1i/archify · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified archify.zip
Binary file not shown.
31 changes: 27 additions & 4 deletions archify/renderers/workflow/workflow-compiler.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4525,18 +4525,41 @@ function automaticPortCandidates(edge, node, side, preferred, counterpart) {
}
}
}
const clear = (point) => occupied.every(({ coordinate, clearance }) => Math.abs(coordinate - point[axis]) >= clearance - 0.0001);
if (clear(preferred)) return [preferred];
const blocking = (point) => occupied.filter(({ coordinate, clearance }) => !(Math.abs(coordinate - point[axis]) >= clearance - 0.0001));
if (!blocking(preferred).length) return [preferred];
const candidates = [];
const extent = verticalSide ? node.height : node.width;
const direction = (verticalSide ? counterpart.cy : counterpart.cx) >= center[axis] ? 1 : -1;
for (let offset = 0; offset <= extent / 2 - 16; offset += 12) {
const limit = extent / 2 - 16;
const lastOffset = Math.floor(limit / 12) * 12;
for (let offset = 0; offset <= limit;) {
let nextClearOffset = Infinity;
for (const sign of [direction, -direction]) {
const point = [...center];
point[axis] += sign * offset;
if (clear(point) && !candidates.some((candidate) => candidate[axis] === point[axis])) candidates.push(point);
const obstacles = blocking(point);
if (!obstacles.length && !candidates.some((candidate) => candidate[axis] === point[axis])) candidates.push(point);
let blockedUntil = offset;
for (const { coordinate, clearance } of obstacles) {
// Rounding can reach a clear coordinate before the exact interval end.
const rounding = 2 * Number.EPSILON * Math.abs(coordinate)
+ 2 * Number.EPSILON * Math.abs(center[axis]) + 2 * Number.EPSILON * clearance;
blockedUntil = Math.max(blockedUntil, sign * (coordinate - center[axis]) + clearance - 0.0001 - rounding);
}
nextClearOffset = Math.min(nextClearOffset, blockedUntil);
}
if (candidates.length >= 2) return candidates.slice(0, 2);
let nextOffset = offset + Math.max(12, offset * Number.EPSILON);
const positive = center[axis] + offset, negative = center[axis] - offset;
if (center[axis] + nextOffset === positive && center[axis] - nextOffset === negative) {
// Skip repeated coordinates, not later usable ports. Keep the 12px grid.
const minimumStep = Math.min(Math.abs(positive), Math.abs(negative)) * Number.EPSILON / 2;
nextOffset = Math.max(nextOffset, Math.ceil((offset + minimumStep) / 12) * 12);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
// Earlier offsets remain inside an occupied interval on both sides.
nextOffset = Math.min(Math.max(nextOffset, Math.ceil(nextClearOffset / 12) * 12), lastOffset);
if (!Number.isFinite(nextOffset) || nextOffset <= offset) break;
offset = nextOffset;
}
return candidates.length ? candidates : [preferred];
}
Expand Down
Binary file modified docs/assets/archify-live-proof.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions docs/assets/archify-live-proof.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"fps": 10,
"frameCount": 54,
"durationSeconds": 5.4,
"bytes": 843870,
"sha256": "b1352f9557026653aeeccac19e11d26f5d24eca8ab013e6c7058529211838c9b",
"bytes": 836293,
"sha256": "46aaacb2d93c0d781da4f7de8063ea0a8b08c59430cfe678258a20624ea4859b",
"scenes": [
{
"id": "signal-flow",
Expand All @@ -31,7 +31,7 @@
{
"id": "classic",
"artifact": "docs/gallery/artifacts/cache-miss.sequence.html",
"artifactSha256": "7f68d7303c86988cfbe228202d25fc6da42fb174be47df5f637d57c39fbf5766",
"artifactSha256": "ca273484a235600f96a74351ac3976cdb6bffb1cfb5d0b86f9ced8c21bd1aa80",
"hash": "#route=web~db",
"eyebrow": "SEQUENCE · CLASSIC",
"title": "Cache Miss Request",
Expand Down
102 changes: 102 additions & 0 deletions test/workflow-port-progress.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawn, spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import vm from 'node:vm';

const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');

test('occupied wide-node centers retain distinct clear automatic ports', () => {
// Exercise the actual nested selector with controlled existing routes; the public
// CLI case below separately protects its integration and bounded termination.
const source = fs.readFileSync(path.join(root, 'archify/renderers/workflow/workflow-compiler.mjs'), 'utf8');
const start = source.indexOf('function automaticPortCandidates(');
const end = source.indexOf('\nfunction automaticEndpointRepairs(', start);
assert.ok(start >= 0 && end > start);
const selector = source.slice(start, end);
for (const [coordinate, width, neighbours = [], edgeWidth = 1.4, side = 'bottom'] of [[0, 128], [1.5e17, 128], [-1.5e17, 128],
[5e17, 104], [-5e17, 104], [2 ** 1000, 2 ** 951],
[1e30, 1e16, [1e30 - 2 ** 47, 1e30 + 2 ** 47]],
[5e29, 1e30, [], 1e25], [-5e29, 1e30, [], 1e25],
[5e29, 1e30, [], 1e25, 'right'],
[-72057594037927940, 80, [-72057594037927920]]]) {
const axis = side === 'right' ? 1 : 0;
const center = axis ? [0, coordinate] : [coordinate, 0];
const node = { id: 'wide', width: axis ? 80 : width, height: axis ? width : 80 };
const edge = { from: 'wide', to: 'target', ...(edgeWidth === 1.4 ? {} : { width: edgeWidth }) };
const occupied = [coordinate, ...neighbours];
const context = {
independentAutomaticRoute: () => true, isReadableAutomaticRoute: () => true,
layoutFeedback: {}, anchor: () => [...center],
nodeRoutes: new Map([['wide', new Map(occupied.map((position, index) => [
{ from: 'wide', to: 'previous-' + index }, { points: axis ? [[0, position], [100, position]] : [[position, 0], [position, 100]] },
]))]]),
edge, node, center, side, counterpart: axis ? { cx: 100, cy: coordinate } : { cx: coordinate, cy: 100 },
};
const ports = vm.runInNewContext(`${selector}\nautomaticPortCandidates(edge, node, side, center, counterpart)`, context, { timeout: 1000 });
assert.equal(ports.length, 2, `center ${coordinate} lost available ports`);
assert.ok(ports.every(point => occupied.every(position => Math.abs(point[axis] - position) >= Math.max(16, 3.5 * (edgeWidth + 1.4)) - 0.0001)),
`center ${coordinate} reused the occupied center: ${JSON.stringify(ports)}`);
assert.notEqual(ports[0][axis], ports[1][axis]);
}
});

test('public Workflow validation terminates when port spacing cannot change finite coordinates', async t => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-port-progress-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const input = path.join(directory, 'input.json');
const workflow = {
schema_version: 2, diagram_type: 'workflow',
meta: { title: 'Finite width', output: 'finite-width.html' },
lanes: [{ id: 'lane', label: 'Lane' }],
nodes: [
{ id: 'target', lane: 'lane', col: 1, type: 'backend', label: 'Target', yOffset: -35 },
{ id: 'source', lane: 'lane', col: 0, type: 'backend', label: 'Source', width: 1e30 },
],
edges: [{ id: 'one', from: 'source', to: 'target' }, { id: 'two', from: 'source', to: 'target' }],
};
for (const [width, edgeWidth] of [[1e30], [1e26], [1e25], [1e18], [320],
[1e30, 1e25], [320, 1e25], [1e30, 1e308]]) {
workflow.nodes[1].width = width;
for (const edge of workflow.edges) {
if (edgeWidth === undefined) delete edge.width;
else edge.width = edgeWidth;
}
const authored = JSON.stringify(workflow);
fs.writeFileSync(input, authored);
const child = spawn(process.execPath, [path.join(root, 'archify/bin/archify.mjs'), 'validate', 'workflow', input, '--json'], {
cwd: directory, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, TMPDIR: directory, TMP: directory, TEMP: directory, ARCHIFY_UPDATE_CHECK_DISABLED: '1' },
});
let stdout = '', stderr = '', timedOut = false, exited = false;
const terminate = () => {
if (exited || child.exitCode !== null || !child.pid) return;
if (process.platform === 'win32') {
const result = spawnSync('taskkill', ['/pid', String(child.pid), '/t', '/f'], { encoding: 'utf8', timeout: 5000, windowsHide: true });
if (result.error) throw result.error;
} else {
try { process.kill(-child.pid, 'SIGKILL'); } catch (error) { if (error.code !== 'ESRCH') throw error; }
}
};
const closed = new Promise((resolve, reject) => {
child.once('error', reject);
child.once('close', (code, signal) => { exited = true; resolve({ code, signal }); });
});
child.stdout.on('data', chunk => { stdout += chunk; if (stdout.length > 1024 * 1024) terminate(); });
child.stderr.on('data', chunk => { stderr += chunk; if (stderr.length > 1024 * 1024) terminate(); });
const timer = setTimeout(() => { timedOut = true; terminate(); }, process.platform === 'win32' ? 10000 : 5000);
let result;
try { result = await closed; } finally { clearTimeout(timer); terminate(); }
assert.equal(timedOut, false, `node width ${width}, edge width ${edgeWidth ?? 'default'} exceeded the bounded public CLI deadline`);
assert.ok([0, 1].includes(result.code), stderr);
const receipt = JSON.parse(stdout);
assert.equal(typeof receipt.ok, 'boolean');
if (width === 320 && edgeWidth === undefined) assert.equal(result.code, 0, stdout);
else if (!receipt.ok) assert.ok(Array.isArray(receipt.diagnostics) && receipt.diagnostics.length);
assert.equal(fs.readFileSync(input, 'utf8'), authored);
assert.equal(fs.existsSync(path.join(directory, 'finite-width.html')), false);
}
});
Loading