How the `archify preview` Command Watches and Reloads Verified JSON Files During Desktop Authoring
archify preview runs a self-contained live-reload loop that watches a JSON source file for changes, rebuilds a verified HTML artifact when the source stabilizes, and pushes updates to the browser via Server-Sent Events (SSE).
The archify preview command in the tt-a1i/archify repository provides desktop authors with instant visual feedback while editing JSON diagram files. Rather than running manual builds after every change, the tool monitors the source file, coalesces rapid edits, validates each build, and automatically refreshes the browser—only when a verified artifact is ready. This article breaks down the complete watch-and-reload pipeline as implemented in the source code.
Live Preview Architecture Overview
The preview system combines file system watching, digest-based change detection, debounced builds, bounded child processes, and SSE-driven live reload. These components work together to ensure the browser always displays the latest valid diagram without flashing intermediate errors or stale versions.
Key modules in archify/bin/preview.mjs:
runPreview– entry point that initializes the server, resolves paths, and starts the watch loop.observeSource– polls and hashes the source file to detect changes.queueStableBuild– debounces build requests and triggersbeginBuildon stable sources.commitCandidate– verifies build receipts and promotes artifacts to the live view.broadcast– pushes state updates to connected browsers via SSE.
File Watching Strategy: Native Watches Plus Polling
archify preview uses a hybrid watching strategy to avoid missing rapid editor events.
Native fs.watch with Directory Monitoring
The tool attaches a native watcher to the source file's directory:
// From archify/bin/preview.mjs, lines 89-95
watcher = fs.watch(path.dirname(inputPath), (eventType, filename) => {
if (filename === path.basename(inputPath)) {
observeSource();
}
});
Native fs.watch is efficient but unreliable during rapid rename bursts common in editors like VS Code or Vim.
Periodic Poll Backup
To close the reliability gap, a polling timer runs every 800 milliseconds by default:
// From archify/bin/preview.mjs, lines 98-100
pollTimer = setInterval(() => observeSource(), pollMs);
This dual approach ensures changes are detected without excessive CPU usage or missed events.
Digest-Based Change Detection and Debouncing
Every detected change triggers observeSource, which computes a SHA-256 hash of the file contents:
// From archify/bin/preview.mjs, lines 24-28
async function sourceDigest(inputPath) {
const bytes = await fs.promises.readFile(inputPath);
return sha256(bytes); // stable canonical representation
}
The hash—not timestamps—determines whether the source has meaningfully changed.
Debounced Build Queue
When a new hash appears, queueStableBuild schedules a build with a 400ms debounce:
// From archify/bin/preview.mjs, lines 55-58
function queueStableBuild(newHash) {
queuedHash = newHash;
clearTimeout(buildTimer);
buildTimer = setTimeout(() => beginBuild(newHash), debounceMs);
}
If the source stabilizes (same hash after debounce), the build launches. If it changes again, the timer resets with the newer hash.
Bounded Child Process Builds
The beginBuild function spawns archify deliver as a detached child process:
// From archify/bin/preview.mjs, lines 71-82
const child = spawn(process.argv[0], [
binPath, 'deliver', type, inputPath,
'--output', candidatePath,
'--json-receipt'
], { detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
Process Lifecycle Management
The preview enforces strict boundaries on build processes:
- Graceful shutdown:
stopGraceTimerallows the child to complete naturally. - Forced termination:
stopKillTimersendsSIGKILLif the child lingers. - Pending build tracking: If new changes arrive during a build,
pendingBuild = trueschedules a follow-up build immediately after the current child exits.
This prevents zombie processes and ensures the queue never stalls.
Verification, Commit, and State Promotion
Build output goes through cryptographic verification before reaching the browser.
Receipt Parsing and Hash Validation
The child writes a JSON receipt to stdout. parseReceipt extracts the expected artifact hash, and commitCandidate validates:
// From archify/bin/preview.mjs, lines 42-50
const digest = await artifactDigest(candidatePath);
if (digest !== receipt?.artifact?.sha256) {
throw new VerificationError('Artifact hash mismatch');
}
Atomic Promotion to Live View
Verified artifacts are moved from the staging directory (.archify-preview-…) to the output path, stored in artifactBuffer, and marked as verified:
// From archify/bin/preview.mjs, lines 61-68
if (digest !== lastArtifactDigest) {
await fs.promises.rename(candidatePath, outputPath);
artifactBuffer = await fs.promises.readFile(outputPath);
lastArtifactDigest = digest;
revision++;
state = { status: 'verified', revision, updatedAt: Date.now() };
broadcast();
}
If the source changed during the build (supersededBy), the newer hash is re-queued for the next cycle.
Live Reload via Server-Sent Events
The preview server exposes three endpoints that enable real-time browser updates:
| Endpoint | Purpose |
|---|---|
/ |
Serves previewPage with an <iframe> for the artifact. |
/artifact.html |
Returns the current artifactBuffer (latest verified HTML). |
/events |
SSE stream broadcasting state changes. |
Client-Side Reload Logic
The browser connects to /events and listens for state transitions:
// From archify/bin/preview.mjs, lines 28-33 (server) and client-side equivalent
const es = new EventSource('http://127.0.0.1:12345/events');
es.addEventListener('state', ev => {
const s = JSON.parse(ev.data);
if (s.status === 'verified') {
document.getElementById('preview-frame').src =
`/artifact.html?revision=${s.revision}`;
}
});
The revision query parameter busts caches and forces the iframe to load fresh content.
Three UI States
The preview page renders distinct feedback for each state:
- checking – Build in progress; shows spinner with elapsed time.
- verified – Valid artifact displayed; badge shows revision number.
- needs-fix – Build failed; diagnostic panel shows redacted error details.
Startup and Shutdown Sequences
Initialization
runPreview orchestrates startup:
# CLI usage
npx archify preview architecture my-diagram.json
// Programmatic usage
import { runPreview } from 'archify/bin/preview.mjs';
await runPreview({
type: 'architecture',
input: 'my-diagram.json',
// output: 'my-diagram.html',
// debounceMs: 300,
// pollMs: 500
});
Steps include: creating the staging directory, resolving the output path via resolveOutputPath, starting the HTTP server with startPreview, and launching the watch loop.
Graceful Termination
On SIGINT or SIGTERM:
- First signal triggers graceful stop (
stopGraceTimer). - Second signal forces
SIGKILLon the child process. - Server closes, staging directory cleans up, and
preview.closedresolves.
// From archify/bin/preview.mjs, lines 84-104
process.on('SIGINT', () => terminate('SIGINT'));
process.on('SIGTERM', () => terminate('SIGTERM'));
Key Files and Module Responsibilities
| File | Role |
|---|---|
archify/bin/preview.mjs |
Core live-preview engine: watch, debounce, build queue, SSE server, state machine. |
archify/bin/open-artifact.mjs |
Browser launcher (openLoopbackUrl) for the preview URL. |
archify/renderers/shared/output-path.mjs |
Output path resolution and meta.output field handling. |
archify/bin/archify.mjs |
CLI entry point that parses arguments and delegates to runPreview. |
All modules live under archify/bin/ and archify/renderers/ in the repository.
Summary
archify previewcombines nativefs.watch, 800ms polling, and SHA-256 digests to detect stable source changes.- 400ms debouncing ensures rapid editor bursts coalesce into single builds.
- Detached child processes run
archify deliverwith grace-period and kill-timer safeguards. - Cryptographic verification of build receipts guarantees only valid artifacts reach the browser.
- Server-Sent Events push state transitions; the iframe reloads only on verified updates with cache-busting revision parameters.
- Three UI states (checking, verified, needs-fix) give authors clear feedback without distraction.
Frequently Asked Questions
How does archify preview handle rapid successive saves in my editor?
The tool uses debounced digest-based change detection. Rapid saves produce the same or successive file hashes; the 400ms debounce timer resets with each change, and only sources that remain stable for the full debounce period trigger builds. The 800ms polling backup catches any events missed by native fs.watch during editor rename bursts.
What happens if I edit the JSON while a build is running?
The running build continues to completion (or times out). If the source hash changed during the build, supersededBy is set to the newer hash. When the child exits, pendingBuild triggers an immediate new build cycle with the latest hash—no stale artifacts are ever promoted.
Why does the browser only reload on "verified" status, not on every file change?
This design prevents flashing invalid or intermediate states. The preview only pushes updates after commitCandidate validates the artifact hash against the build receipt. Failed builds enter the needs-fix state with diagnostics, keeping the last good diagram visible.
Can I adjust the watch sensitivity or debounce timing?
Yes. The runPreview function accepts debounceMs (default 400) and pollMs (default 800) options. Pass these programmatically or modify defaults in your wrapper. The CLI currently uses fixed defaults; for custom timing, use the programmatic API.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →