
Doh extends the concept of Hot Module Replacement (HMR) into a complete Hot Virtual File System that automatically updates any loadable resource without page reloads. Unlike traditional HMR systems that only handle JavaScript modules, Doh's approach works with all loadable resources including CSS, HTML, JSON, images, and any custom loaders.
This guide covers:
Note: For a higher-level view of how HMR/HVFS fits into the overall architecture with
Doh.Loaded, theLoad System, andData Binding, see the Doh.js Core Architecture Overview.
Getting started with Doh's Hot Virtual File System is incredibly simple:
Enable HMR in your pod.yaml:
browser_pod:
hmr:
enabled: true # This is the only required setting
That's it! When enabled:
Doh.Packages, Doh.PatternModule, etc.) update automatically when files changeAt the core of Doh's HMR capabilities is the Doh.Loaded object, which acts as a virtual file system for your application:
Doh.load() are stored in Doh.Loaded// Resource "mydata.json" is stored in Doh.Loaded["mydata.json"]
const mydata = await Doh.live_load("mydata.json");
console.log(Doh.Loaded["mydata.json"]); // The loaded JSON content
// When the file changes, Doh.Loaded["mydata.json"] is automatically updated
// and all subscribers are notified
Doh's Hot Virtual File System provides:
Doh automatically tracks and hot-reloads several resource types:
| Resource Type | Tracking Method | Update Behavior |
|---|---|---|
| CSS | Automatic for all CSS | Style tags update in-place |
| HTML | Automatic with router | Intelligent DOM diffing |
| Patterns | Automatic with HMR | Live instances hot-patched (methods, properties, MOC) |
| JSON/Data | Via Doh.Loaded | In-memory values update |
| JS Modules | Via module system | Selectively reload affected components |
For any resource type, Doh.live_load provides a reactive connection to the file:
// Load a JSON configuration and update UI when it changes
await Doh.live_load('config/settings.json', function(jsonString) {
const settings = JSON.parse(jsonString);
updateApplicationSettings(settings);
});
Doh.mimic_load takes HMR to the next level by enabling two-way binding with files:
// Create a two-way binding with a JSON configuration
const configMirror = await Doh.mimic_load('config/settings.json', function(jsonString) {
// This callback runs when the server sends an update
const settings = JSON.parse(jsonString);
updateApplicationSettings(settings);
});
// Later, you can modify the file content from the client
// This change will be sent to the server and saved to disk
configMirror.content = JSON.stringify({ theme: 'dark', fontSize: 16 }, null, 2);
The mimic_load function:
content propertycontent propertyThis enables powerful collaborative editing scenarios where multiple clients can edit the same files simultaneously.
For HTML content, Doh.live_html provides intelligent DOM diffing:
// Load HTML template and auto-update with intelligent diffing
await Doh.live_html('templates/dashboard.html');
( !! VERY ALPHA !! )
Similar to mimic_load, but specifically for HTML content with intelligent diffing:
// Create a two-way binding with an HTML template
const templateMirror = await Doh.mimic_html('templates/dashboard.html', function(newHtmlString) {
// This callback runs when the server sends an update
console.log('Template updated from server');
});
// Later, you can modify the HTML content from the client
// This change will be sent to the server and saved to disk
templateMirror.content = '<div class="dashboard updated">New content</div>';
For more control, you can subscribe to specific resource updates:
Doh.Module('my_app', [
'hmr_browser?? hmr'
], function(HMR) {
// Subscribe to updates for a specific resource
HMR.subscribe('data/metrics.json', (updateData) => {
const metrics = JSON.parse(Doh.Loaded['data/metrics.json']);
refreshDashboardMetrics(metrics);
});
});
The power of Doh's Hot Virtual File System comes from its observer pattern:
// Observe changes to any resource in Doh.Loaded
Doh.observe(Doh.Loaded, 'config/theme.json', function(obj, prop, newValue) {
applyThemeChanges(JSON.parse(newValue));
});
// the above can now be replaced with (for simplicity):
Doh.live_load('config/theme.json', function(newValue) {
applyThemeChanges(JSON.parse(newValue));
})
This pattern:
Doh.LoadedYou can temporarily disable and re-enable HMR during runtime:
// Disable HMR temporarily
HMR.pause();
// Perform operations that shouldn't trigger HMR
performBatchUpdates();
// Re-enable HMR
HMR.resume();
You can check if HMR is connected and get the list of active loaders:
// Check if HMR is connected to the server
if (HMR.isConnected()) {
console.log('HMR is connected');
}
// Get a list of all active loaders being watched
const activeLoaders = HMR.getActiveLoaders();
console.log('Active loaders:', activeLoaders);
Create custom loaders that participate in the Hot Virtual File System:
// Custom YAML loader with hot update support
LoaderTypesExtMap['yaml'] = 'yaml';
LoaderTypesExtMap['yml'] = 'yaml';
// define a cache to reduce unwanted reloads
Doh.YAMLIsLoaded = Doh.YAMLIsLoaded || {};
// define the actual loader
Doh.LoaderTypes['yaml'] = async function (loadStatement, from, relpath, loaderType, forceReload = false) {
if (!forceReload && Doh.YAMLIsLoaded[from]) return Doh.YAMLIsLoaded[from];
return Doh.ajaxPromise(from + (forceReload ? '?reload=' + forceReload : '')).then(async response => {
const YAML = await Doh.load('yaml');
Doh.YAMLIsLoaded[from] = YAML.parse(response.data);
return Doh.YAMLIsLoaded[from];
});
};
// Now YAML files are hot-reloadable
await Doh.live_load('config.yaml', function(yamlData) {
applyConfiguration(yamlData);
});
Doh's Hot Virtual File System works through several integrated mechanisms:
Doh.ModuleFile (files that actually loaded via host_load) to determine which files to watch — not the full manifest, just the active dependency treeload array changes (new dependency) or host_load/browser_load adds a module, the watcher extends to cover the new files. When modules are removed, watches contract._hmrReloading flag to prevent duplicate instantiationThis creates a seamless end-to-end system where:
HMR only watches files that are part of the active load graph — the dependency tree rooted at host_load (server) and browser_load (browser). This is fundamentally different from watching the entire project or the full manifest.
At startup, HMR walks Doh.ModuleFile — the set of files that actually executed during boot. These are the files reachable from host_load through the dependency tree. Their associated .doh.yaml package files are also watched.
When the load graph grows, watches extend automatically:
| Trigger | What Happens |
|---|---|
host_load adds a module |
Module's file + its load deps are watched |
host_load removes a module |
Module's file is unwatched |
browser_load adds a module |
Browser subscribes to updates for that module |
A package's load array gains a dep |
The new dependency's file is watched |
| Mini-packager registers new modules | Their load deps are walked and watched |
The same principle applies on both sides. The server watches files reachable from host_load. The browser subscribes to updates for files reachable from browser_load. When either load graph extends (via pod changes or dependency changes), the watches/subscriptions extend reactively.
host_load: [chat, analytics] # Server watches chat + analytics trees
browser_load: [chat_client] # Browser subscribes to chat_client tree
# Edit pod.yaml to add "video_editor" to host_load:
# → Server watches video_editor's file + its dependencies
# → Browser receives pod update, subscribes if browser_load changed
The auto-packager normally only runs at boot, scanning the entire project to build runtime registries (Doh.Packages, Doh.PatternModule, Doh.ModuleFile, etc.) and manifest files. The mini-packager keeps these registries current as files change, enabling always-on servers without restarts.
When a JS or YAML file changes, the mini-packager runs before the module reload:
auto_parser_worker.mjs used at boot) for AST analysisModuleDefinitions, PackageDefinitions, and PatternDefinitions are applied to runtime registriesload arrays are resolved and watched (reactive graph extension)package_manifest.json and patterns_manifest.json are written to diskhmr:manifestsUpdated to re-fetch and sync their local registriesFor YAML package files (.doh.yaml), the process is similar — the file is parsed, old package entries are removed, and new ones are applied.
| Registry | Updated By | Purpose |
|---|---|---|
Doh.Packages |
JS + YAML | Package/module metadata (deps, file paths) |
Doh.PatternModule |
JS | Pattern name → module name mapping |
Doh.PatternFile |
JS | Pattern name → file path |
Doh.PatternsInFile |
JS | File path → pattern names |
Doh.PatternsInModule |
JS | Module name → pattern names |
package_manifest.json |
JS + YAML | Persisted package registry |
patterns_manifest.json |
JS | Persisted pattern registry |
When manifests change, the server emits hmr:manifestsUpdated. The browser re-fetches the changed manifests and uses syncPod (a reference-preserving deep sync) to update its local registries without breaking existing references:
// Subscribe to manifest updates (automatic when HMR is enabled)
Doh.socket.on('hmr:manifestsUpdated', async (data) => {
// Doh.Packages and Doh.PatternModule are synced automatically
});
If a JS file contains Doh.Pod() definitions, the mini-packager automatically triggers a pod recompile when that file changes. This means module-level pod contributions stay in sync without watching every JS file as a pod source.
HMR automatically watches all pod configuration files referenced in Doh.pod.inherited. When a pod file changes, the configuration is recompiled and pushed to connected browsers.
Pod files are extracted from Doh.pod.inherited at startup, filtering out __module_* entries (module pod contributions are handled by the mini-packager) and data: URIs. Typical watched files include:
/doh_js/default.pod.yaml/boot.pod.yaml/pod.yaml/pods/*.pod.yaml/secrets/*.pod.yamlDoh.compile_host_pod() runs — recompiling from all pod sources, writing compiled.pod.yaml and browser_pod.jsonDoh.pod using syncPod, preserving object and array references so that code holding references to Doh.pod sub-objects (and observers watching them) continues to workhost_load is diffed: new entries' files are watched (+ their load deps), removed entries are unwatchedDoh.browser_pod is synced separatelybrowser_pod changed, hmr:podUpdate is emitted to connected clientsDoh.pod and Doh.browser_pod, and subscribe to new browser_load entriesHMR.syncPod(target, source) is a recursive function that makes target structurally identical to source while preserving as many object and array references as possible:
This is critical because Doh code holds references to Doh.pod sub-objects, and observers may be watching specific object identities.
Doh.Module('my_app', [
'hmr_browser?? hmr'
], function(HMR) {
// React to pod configuration changes
HMR.subscribe('pod', (data) => {
console.log('Pod updated at', data.timestamp);
// Doh.pod and Doh.browser_pod are already synced at this point
applyConfigChanges();
});
});
When a module containing Pattern() definitions is hot-reloaded, HMR automatically hot-patches all live instances of changed patterns. No destroy/rebuild cycle — state is preserved, methods are updated, and the DOM stays intact.
Doh.Patterns references for the affected modulePattern() creates new definition objects in Doh.Patternsinst.inherited[patternName] is swapped to the new definitionPhase, Chain, Method, Funnel, etc.) are rebuilt from the updated .inherited chainpattern string changes, the old child is destroyed and a new one is built with the new pattern| Change Type | Behavior |
|---|---|
| Data properties | Updated on instance (preserves runtime overrides) |
| Methods (non-melded) | Replaced on instance |
| Melded methods (Phase, Chain, etc.) | Stack rebuilt from .inherited |
pre_/post_ hooks |
Picked up by parent melder's stack rebuild |
| New MOC entries | Merged onto instance moc, melders created |
| CSS properties | Applied via pattern stylesheet regeneration |
| New properties | Set on instance |
| Sub-object additions | New child built with full phase execution and DOM insertion |
| Sub-object removals | Existing child destroyed, DOM removed, references cleaned up |
| Sub-object pattern changes | Old child destroyed, new child built with new pattern type |
| Sub-object property changes | Patched onto existing child in-place (no rebuild) |
object_phase, html_phase, etc.) don't re-run on the parent instance. If new setup logic is critical, refresh. (Note: newly added sub-objects do run their full phase chain.)tag: The DOM element type can't change without a rebuild.Array to Object.Hot-patching only updates properties whose current value on the instance === the old pattern value. If your code or the user changed a property at runtime, the hot-patch won't overwrite it:
Pattern('MyWidget', 'html', {
count: 0, // Pattern default
label: 'Hello', // Pattern default
});
// At runtime:
widget.count = 42; // Runtime override
// After hot-patch with count: 0 → count: 1:
// widget.count is still 42 (runtime override preserved)
// widget.label updated if it was still 'Hello'
Patterns can opt out of hot-patching by setting hmr: false:
Pattern('PersistentSocket', 'object', {
hmr: false, // This pattern's instances will never be hot-patched
// ...
});
HMR tracks live instances in Doh.HMRInstances (a patternName → Set<instance> registry). Instances are automatically registered after construction and deregistered on destroy(). This registry is only populated when HMR is enabled (Doh._hmrEnabled), so production builds pay no cost.
// Documentation that updates as you edit
await Doh.live_load('docs/api.md', function(markdown) {
document.getElementById('docs').innerHTML = marked(markdown);
Prism.highlightAll();
});
// Shared resource that updates across clients
const sharedWhiteboard = await Doh.mimic_load('shared/whiteboard.json', function(whiteboardData) {
renderCollaborativeWhiteboard(JSON.parse(whiteboardData));
});
// Add a new element to the whiteboard from this client
const currentWhiteboard = JSON.parse(sharedWhiteboard.content);
currentWhiteboard.elements.push({ type: 'circle', x: 100, y: 100, radius: 50 });
sharedWhiteboard.content = JSON.stringify(currentWhiteboard);
// Create a UI for editing configuration
const configMirror = await Doh.mimic_load('config/app.json');
const config = JSON.parse(configMirror.content);
// Create UI elements for editing
const themeSelector = document.getElementById('theme-selector');
themeSelector.value = config.theme;
// Update config when UI changes
themeSelector.addEventListener('change', () => {
const updatedConfig = JSON.parse(configMirror.content);
updatedConfig.theme = themeSelector.value;
configMirror.content = JSON.stringify(updatedConfig, null, 2);
});
Fine-tune the Hot Virtual File System with these pod settings:
browser_pod:
hmr:
enabled: true # Enable HMR system
debug: false # Enable verbose debug logging (default: false)
autocss: true # Enable automatic CSS hot reloading (default: true)
autowatch: true # Auto-watch load graph files at startup (default: true)
debounceTime: 300 # Time in ms to debounce rapid file changes
highlightBeforeApply: false # Highlight DOM changes before applying (default: false)
exclude_modules: [] # Module names to exclude from watching
loaders: [ # Additional loaders to watch explicitly
'ui_components.js',
'dashboard_module',
'templates/header.html'
]
If you encounter issues with the Hot Virtual File System:
HMR.isConnected()HMR.getActiveLoaders() to see what's being watchedHMR.forceReloadPage() to refreshHMR.pause() temporarilyDoh's Hot Virtual File System represents a significant evolution beyond traditional HMR. By extending hot reloading to the entire virtual file system through Doh.Loaded, it creates a development experience where any resource can be updated without losing application state.
The reactive load graph ensures HMR watches only what matters — files in the active dependency tree, not the entire project. As the graph extends (new dependencies, host_load changes), watches extend automatically. The mini-packager keeps registries current, pod file watching recompiles configuration live, and browsers are kept in sync. No restarts needed.
The addition of two-way binding with mimic_load takes this even further, enabling collaborative editing scenarios and powerful tools where clients can modify files directly from the browser.
This approach eliminates the friction between writing code and seeing results, making development more efficient and enjoyable while keeping code clean and maintainable.