Home Doh Ref
Dohballs
  • 📁 doh_chat
  • 📁 doh_modules
    • 📦 dataforge
    • 📦 express
    • 📁 sso
    • 📁 user

Hot Module Reload: Hot-reload Everything.

HMR

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:

  • Quick start setup and configuration
  • Core concepts of the Hot Virtual File System
  • Automatic resource tracking and updates
  • Reactive loading patterns with Doh.live_load
  • Two-way binding with Doh.mimic_load
  • Reactive load graph watching
  • Mini-packager for live registry updates
  • Pod file watching for live configuration

Note: For a higher-level view of how HMR/HVFS fits into the overall architecture with Doh.Loaded, the Load System, and Data Binding, see the Doh.js Core Architecture Overview.

Quick Start

Getting started with Doh's Hot Virtual File System is incredibly simple:

  1. Enable HMR in your pod.yaml:

    browser_pod:
      hmr:
        enabled: true   # This is the only required setting
    
  2. That's it! When enabled:

    • Only files in the active load graph are watched (not the entire project)
    • CSS is automatically hot-reloaded by default
    • HTML files served by the router automatically get HMR injection
    • Pattern definitions update live, hot-patching existing instances without page reloads
    • Runtime registries (Doh.Packages, Doh.PatternModule, etc.) update automatically when files change
    • Pod configuration files are watched and recompiled live
    • When the load graph extends (new deps, host_load changes), watches extend reactively
    • All required socket connections are set up automatically
    • Any resource in Doh.Loaded can be hot-updated

Doh.Loaded: A Virtual File System

At the core of Doh's HMR capabilities is the Doh.Loaded object, which acts as a virtual file system for your application:

  • Universal Resource Cache: All resources loaded via Doh.load() are stored in Doh.Loaded
  • Observable Updates: Resources update in-place when files change on disk
  • Auto-Propagating Changes: Updates trigger reactively throughout your application
  • Cross-Origin Compatible: Works with local and remote resources
// 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

Key Features and Benefits

Doh's Hot Virtual File System provides:

  1. Universal Hot Reloading: Any resource type can be hot-reloaded, not just JS modules
  2. Automatic State Preservation: Updates occur without losing application state
  3. Zero-Configuration Approach: Most resources work automatically without extra setup
  4. Reactive Update System: Changes propagate through observer patterns
  5. Intelligent Diffing: HTML updates only change what's needed, preserving DOM state
  6. Cross-Environment Consistency: Same API works in browser and Node.js
  7. Two-Way Binding: Not just receive updates but also send them back to the server
  8. Reactive Load Graph: Only files in the active dependency tree are watched — watches extend and contract as the graph changes
  9. Live Registry Updates: Runtime package and pattern registries stay in sync as files change
  10. Live Pod Configuration: Pod YAML files are watched and recompiled without restart

Automatic Resource Tracking

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

Using the Hot Virtual File System

Doh.live_load: Reactive Resource Loading

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: Two-Way File Binding

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:

  1. Creates a mirror object with a content property
  2. Sets up a subscription for server updates
  3. Observes changes to the content property
  4. Sends updates back to the server when content changes
  5. Prevents update loops by tracking source IDs

This enables powerful collaborative editing scenarios where multiple clients can edit the same files simultaneously.

Doh.live_html: Smart HTML Updates

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');

Doh.mimic_html: Two-Way HTML Binding

( !! 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>';

Subscribing to Resource Changes

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 Doh.Loaded Observer Pattern

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:

  1. Watches for changes to specific keys in Doh.Loaded
  2. Triggers callbacks when values change
  3. Works with any resource type
  4. Enables building reactive UIs without virtual DOM frameworks

Advanced Usage

Pausing and Resuming HMR

You 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();

Checking Connection Status

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);

Custom Loaders with Live Updates

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);
});

How It Works

Doh's Hot Virtual File System works through several integrated mechanisms:

  1. Load Graph Watching: At startup, HMR walks Doh.ModuleFile (files that actually loaded via host_load) to determine which files to watch — not the full manifest, just the active dependency tree
  2. Reactive Graph Extension: When a package's load 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.
  3. Mini-Packager: Changed JS/YAML files are re-parsed to update runtime registries and manifests
  4. Socket Communication: Changes are broadcast to connected clients
  5. Module Reload: Doh modules re-execute with _hmrReloading flag to prevent duplicate instantiation
  6. Pattern Hot-Patching: Changed pattern definitions are diffed and applied to all live instances
  7. Doh.Loaded Updates: The system updates values in Doh.Loaded
  8. Observer Notifications: Observers of Doh.Loaded are notified
  9. Automatic Reapplication: CSS and HTML are reapplied via their respective systems
  10. Two-Way Synchronization: With mimic_load, client changes are sent back to the server
  11. Pod Recompilation: Pod YAML changes trigger recompile with reference-preserving sync

This creates a seamless end-to-end system where:

  • File changes on disk
  • Updates in-memory representation
  • Triggers dependent component updates
  • Without losing application state

Reactive Load Graph Watching

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.

What Gets Watched

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.

Graph Extension

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

Server and Browser: Same Load Engine

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

Mini-Packager: Live Registry Updates

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.

How It Works

When a JS or YAML file changes, the mini-packager runs before the module reload:

  1. JS files are sent to a persistent parser worker (the same auto_parser_worker.mjs used at boot) for AST analysis
  2. Old registry entries for the file are cleaned out
  3. New ModuleDefinitions, PackageDefinitions, and PatternDefinitions are applied to runtime registries
  4. New dependencies in updated packages' load arrays are resolved and watched (reactive graph extension)
  5. Updated package_manifest.json and patterns_manifest.json are written to disk
  6. Connected browsers are notified via hmr:manifestsUpdated to re-fetch and sync their local registries

For YAML package files (.doh.yaml), the process is similar — the file is parsed, old package entries are removed, and new ones are applied.

What Gets Updated

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

Browser Manifest Sync

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
});

Module Pod Detection

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.

Pod File Watching: Live Configuration

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.

Watched Files

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.yaml

How It Works

  1. A pod file changes on disk
  2. Doh.compile_host_pod() runs — recompiling from all pod sources, writing compiled.pod.yaml and browser_pod.json
  3. The fresh pod is deep-synced onto the existing Doh.pod using syncPod, preserving object and array references so that code holding references to Doh.pod sub-objects (and observers watching them) continues to work
  4. host_load is diffed: new entries' files are watched (+ their load deps), removed entries are unwatched
  5. Doh.browser_pod is synced separately
  6. If browser_pod changed, hmr:podUpdate is emitted to connected clients
  7. Browsers sync the update onto their local Doh.pod and Doh.browser_pod, and subscribe to new browser_load entries

Reference-Preserving Sync

HMR.syncPod(target, source) is a recursive function that makes target structurally identical to source while preserving as many object and array references as possible:

  • Objects: If both target and source have an object at the same key, recurse (preserving the target object reference)
  • Arrays: If both have arrays, the target array is cleared and repopulated (preserving the array reference)
  • Deletions: Keys in target that don't exist in source are deleted
  • Primitives: Assigned directly

This is critical because Doh code holds references to Doh.pod sub-objects, and observers may be watching specific object identities.

Subscribing to Pod Changes

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();
  });
});

Pattern Instance Hot-Patching

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.

How It Works

  1. Before reloading, HMR snapshots old Doh.Patterns references for the affected module
  2. The module re-executes, Pattern() creates new definition objects in Doh.Patterns
  3. HMR compares old vs new references — if they differ, the pattern changed
  4. For each live instance of a changed pattern:
    • inst.inherited[patternName] is swapped to the new definition
    • New MOC entries are merged onto the instance's moc
    • Changed data properties are applied (only if the instance hasn't modified them at runtime)
    • All melded method stacks (Phase, Chain, Method, Funnel, etc.) are rebuilt from the updated .inherited chain
    • Sub-object additions: New sub-objects in the definition are built as live children (full phase execution, DOM insertion with correct ordering)
    • Sub-object removals: Sub-objects removed from the definition are destroyed (DOM removal, cascade cleanup, HMR deregistration)
    • Sub-object pattern type changes: If a sub-object's pattern string changes, the old child is destroyed and a new one is built with the new pattern
    • Sub-object property patches: If the same sub-object exists in both old and new definitions, changed properties are patched onto the existing child without rebuilding
  5. CSS changes apply automatically through the pattern stylesheet system

What Updates Automatically

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)

What Requires a Page Refresh

  • Phase re-execution: Lifecycle phases (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.)
  • Changing tag: The DOM element type can't change without a rebuild.
  • MOC type changes: Changing a property's MOC from e.g. Array to Object.
  • Removed properties: Non-sub-object properties deleted from a pattern definition remain on existing instances.

Runtime State Preservation

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'

Opting Out

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
  // ...
});

Instance Tracking

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.

Real-World Examples

Live Documentation Browser

// Documentation that updates as you edit
await Doh.live_load('docs/api.md', function(markdown) {
  document.getElementById('docs').innerHTML = marked(markdown);
  Prism.highlightAll();
});

Real-Time Collaborative Editing

// 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);

Live Configuration Editor

// 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);
});

Configuration Options

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'
    ]

Troubleshooting

If you encounter issues with the Hot Virtual File System:

  1. Check Connection Status: Look for the visual indicator in the bottom right corner or use HMR.isConnected()
  2. Verify File Paths: Ensure paths match exactly what's in Doh.Loaded
  3. Check Subscriptions: Use HMR.getActiveLoaders() to see what's being watched
  4. Force Reload: As a last resort, use HMR.forceReloadPage() to refresh
  5. Pause and Resume: If updates are causing issues, try using HMR.pause() temporarily

Conclusion

Doh'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.

Last updated: 2/10/2026