A pack change does not need a JVM restart. Rebuild the .feb, let the host
pick it up, and let connected clients reload themselves onto the new build.
Dev loop
Run the pack builder in watch mode:
pnpm --filter @kolektiv/harbor-pack dev
vite build --watch rewrites samples/harbor/pack/dist/harbor.feb after every
save. A scaffolded pack has the same pnpm dev script. The file is written to
a temp name and renamed into place, so a watcher never reads a partial zip.
Run the host against that file instead of the classpath copy:
./gradlew :samples:harbor:run --args="pack/dist/harbor.feb"
How the swap works
Keel fingerprints file and directory bundles by size, mtime, and their
recursive size/mtime for directories. When watchPacks = true, it polls on
packWatchIntervalMs (default 500), opens a fresh FrontendBundle for a
changed pack, and swaps it into the live bundle registry. Asset URLs, page
modules, and the seed all come from the new bundle; old in-flight requests
finish against the old one. Classpath resource bundles are never watched.
keel {
bundle = FrontendBundle.fromFile(Path.of("pack/dist/harbor.feb"))
watchPacks = true
packWatchIntervalMs = 500
}
Each bundle has a contentHash (SHA-256 over the manifest and sorted entry
metadata). The document and visit responses carry it as X-Keel-Build and as
seed.build, while X-Keel-Version stays the manifest semver. When the
bootstrap router sees a fetched seed whose build differs from the mounted
build, it clears the prefetch cache, aborts the stale seed, and calls
window.location.reload(). No manual cache busting is needed.
Build output is content-addressed: Vite emits chunks/[name]-[hash].js and
assets/[name]-[hash]…, so a new build’s seed points at new URLs and the
immutable asset cache never has to be invalidated. See
Host adapter for the
cache headers a host should send, and
the wire protocol for the
X-Keel-Build header.
Failure modes
A bad manifest, a truncated archive, or an IO error during a reopen keeps the
previous bundle serving. The host logs a warning and retries on the next poll;
when the source becomes valid again, the swap goes through. If the manifest
inside a rebuilt pack changes its id, the registry entry is replaced under
the new id.