foam3

Live Reload

Save a .js file while the app is open in the browser and the page picks the edit up without a reload. A css: edit restyles what is on screen. A code edit rebuilds that class’s on-screen instances in place. Page scroll position, the open menu and property-bound edits stay.

Source runs only. A jar build never loads it.

For the basics of how a class is defined and rendered, start with U3.md and Build.md. This guide covers what live reload gives you, what it cannot do, and what it costs.


Turn it on

./build.sh -l                # build, run, and watch the .js files under the project root
./build.sh -l -W9090         # same, on another port; combines with every other flag

-l adds the live deployment journal (tools/JavaTooling.js:48). That journal registers two services (deployment/live/services.jrl):

Without -l neither exists, and the browser side stays off: foam.u2.ViewReloader is built only under the dev flag (which only a source run sets) or in a test build (dev&web|web&test in src/foam/u2/pom.js), so a production foam-bin has no such class, and ApplicationController.onClientLoad creates it only when the class exists and the client has sourceChangeDAO.

What you see

One console line per saved file:

[reload] /src/foam/u2/tag/Card.js: 0 class(es) redefined, 1 stylesheet(s) swapped, 0 view(s) rebuilt
[reload] /src/foam/u2/tag/Card.js: 1 class(es) redefined, 0 stylesheet(s) swapped, 9 view(s) rebuilt
[reload] /src/foam/u2/tag/Card.js: 0 class(es) redefined, 0 stylesheet(s) swapped, 0 view(s) rebuilt, 1 unchanged
[reload] /src/foam/lang/Slot.js: 1 class(es) redefined, 0 stylesheet(s) swapped, 0 view(s) rebuilt. Reload the page: foam.lang.Slot is a boot class
Count Meaning
class(es) redefined Classes re-run and re-registered, including every subclass of an edited class. A css-only edit is not counted here: its old class object stays registered and only its style text changes.
stylesheet(s) swapped Classes whose file changed in css: and nothing else. The new text goes onto the class’s existing css axiom and every installed <style> block on the page is re-expanded from its axiom, the same walk a theme change runs.
view(s) rebuilt On-screen instances of the redefined classes that were replaced by a fresh instance.
N unchanged Classes whose model is byte-for-byte what was loaded: a formatter run, or a save of an unchanged buffer. The old class stays registered and nothing is rebuilt.
Reload the page: ... The edit reached the page only partly. Each hint names why.

0 view(s) rebuilt with no hint means no instance of that class is on screen, in the main page or in an open popup.

A save that reports no loaded class came from this file means the browser never loaded that file, so there is nothing to swap.

What a rebuild keeps and what it drops

A rebuilt instance is a fresh create() of the new class, with the old instance’s property values linked in by slot, then swapped into the parent with Element.replaceChild (ViewReloader.replace).

Kept:

Dropped:

The old instance is kept alive off-screen as the relay between the new one and its data. Each save of the same file adds one more relay. Reload the page when a long session gets slow.

When a page reload is still needed

The console hint names each case:

Hint Why
the file defines a foam.SCRIPT A foam.SCRIPT runs once at load. Re-running the file runs it again, and the reloader has no way to undo the first run.
<id> is a boot class foam.lang.* classes are what every other class is built from. Redefining one leaves the rest built on the old one.
N instance(s) render through a SlotNode this.add(someSlot) and this.add(this.slot(...)) wrap the slot in a SlotNode (Element.add_), and SlotNode.update swaps the rendered value into the DOM directly, outside the parent’s childNodes, so the instance has no slot to be swapped into. It is counted and left as is. A dynamic() block is a FunctionNode whose children are ordinary childNodes, so views inside it are rebuilt normally.
N instance(s) are a root themselves The edited class is the application controller’s own, or an open popup’s: the instance has no parent to swap it into. Its children were still walked.
the file holds only refinements The refinement re-ran and patched its target class, so new instances see it, but instances already on screen were not rebuilt: cascade follows extends, not refines, since a refinement of foam.u2.Element would mean rebuilding the whole page.
N class(es) mix this in Same for a mixin: the mixing classes were not rebuilt.
<id> stylesheet count changed Adding the first css: block, or removing the last one, leaves nothing to rewrite in place.

Cases without a hint:

How it works

SourceWatcher (server)          sourceChangeDAO          ViewReloader (browser)
stat every known .js / 500ms -> put SourceChange{id} -> WebSocket put
walk the tree / 30s                                  -> <script src=path?t=...>
                                                     -> css-only: rewrite <style>
                                                     -> else: cascade subclasses,
                                                        refinements, rebuild views

A stat poll instead of java.nio.WatchService because on macOS WatchService polls every registered directory. One-off measurements on a tree of 3,522 directories and 5,725 .js files, each a 60 s sample of an idle server:

60 s window Share of one core
WatchService on every directory 14.2%
Stat of the known files every 500 ms 4.1%
Server idle, -l off / on 0.7% / 5.4%

Only the stat row has a repeatable producer: SourceWatcherBenchmark times one poll tick over the current tree, and one tick’s share of a core at the default interval is (1000 / ops) / 500. The other two rows were read off a process monitor.

Tune the watcher

The poller’s knobs are properties on SourceWatcher, so an app overrides them by redefining the sourceWatcher CSpec in a journal that loads after live:

p({
  "class":"foam.core.boot.CSpec",
  "name":"sourceWatcher",
  "lazy":false,
  "service": {
    "class": "foam.core.fs.SourceWatcher",
    "skipDirs": [ "build", "node_modules", "docs" ],
    "pollInterval": 1000,
    "rescanInterval": 60000
  }
})

A large checkout with extra top-level directories that hold .js files is the usual reason: every file the walk finds is stat’ed on every tick, and every one that changes is pushed to every open tab. Dot-directories are skipped without being listed.

Tests

./build.sh server-tests:WatcherTest              # poll loop, rescan, skipDirs, WatchService path
./build.sh server-tests:SourceWatcherTest        # a changed .js becomes a SourceChange; off without core.webroot
./build.sh client-tests:ViewReloaderTest         # path lookup, css-only detection, cascade, in-place rebuild, popup roots
./build.sh javaBenchmarks:SourceWatcherBenchmark # ticks per second over the current tree