Skip to main content

File Watching

This document defines the file watching system for Morphir workspaces, enabling automatic recompilation on source changes.

Overview

File watching enables:

  • Incremental builds: Recompile only changed files
  • IDE integration: Real-time error feedback
  • Development workflow: Automatic rebuild on save
  • Hot reload: Update running applications (where supported)

Architecture

┌─────────────────────────────────────────────────────────┐
│ Workspace Daemon │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Watcher │───►│ Debounce │───►│ Compiler │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ ▲ │ │
│ │ FS events │ IR │
│ │ ▼ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ File System │ │ Notifier │ │
│ └─────────────┘ └─────────────┘ │
│ │ │
└──────────────────────────────────────────────┼─────────┘


Clients (IDE, CLI)

Types

WatchEventType

/// Type of file system event
pub type WatchEventType {
/// File or directory created
Created
/// File content modified
Modified
/// File or directory deleted
Deleted
/// File or directory renamed
Renamed
}

WatchEvent

/// A file system change event
pub type WatchEvent {
WatchEvent(
/// Type of event
event_type: WatchEventType,
/// Affected path (relative to workspace root)
path: String,
/// New path (for rename events only)
new_path: Option(String),
/// Project this file belongs to (if determinable)
project: Option(PackagePath),
/// Timestamp of event
timestamp: DateTime,
)
}

WatchState

/// Current state of the file watcher
pub type WatchState {
/// Watcher is not running
Stopped
/// Watcher is initializing
Starting
/// Watcher is active
Running
/// Watcher encountered an error
Error(message: String)
}

Operations

Start Watching

Begins watching the workspace for file changes.

Behavior

  1. Initialize file system watcher
  2. Register watch paths for all projects
  3. Set up debouncing (default: 100ms)
  4. Begin emitting events

Watch Paths

By default, watches:

  • */src/**/*.elm - Elm source files
  • */src/**/*.morphir - Morphir DSL files
  • */morphir.toml - Project configuration
  • morphir.toml - Workspace configuration

WIT Interface

/// Start watching workspace for changes
start-watching: func() -> result<_, workspace-error>;

JSON-RPC

Request:

{
"method": "workspace/watch",
"params": {
"enabled": true
}
}

Response:

{
"result": {
"state": "running",
"watchedPaths": [
"packages/core/src",
"packages/domain/src",
"packages/api/src"
]
}
}

CLI

morphir workspace watch
morphir build --watch

Stop Watching

Stops file system watching.

WIT Interface

/// Stop watching workspace
stop-watching: func() -> result<_, workspace-error>;

JSON-RPC

Request:

{
"method": "workspace/watch",
"params": {
"enabled": false
}
}

Poll Events

Retrieves pending watch events (for polling-based clients).

WIT Interface

/// Poll for watch events (non-blocking)
poll-events: func() -> list<watch-event>;

JSON-RPC

Request:

{
"method": "workspace/pollEvents",
"params": {}
}

Response:

{
"result": [
{
"eventType": "modified",
"path": "packages/domain/src/User.elm",
"project": "my-org/domain",
"timestamp": "2026-01-16T12:34:56Z"
}
]
}

Notifications

workspace/onFileChanged

Push notification sent when files change (for streaming clients).

{
"method": "workspace/onFileChanged",
"params": {
"events": [
{
"eventType": "modified",
"path": "packages/domain/src/User.elm",
"project": "my-org/domain",
"timestamp": "2026-01-16T12:34:56Z"
},
{
"eventType": "created",
"path": "packages/domain/src/Order.elm",
"project": "my-org/domain",
"timestamp": "2026-01-16T12:34:56Z"
}
]
}
}

workspace/onProjectStateChanged

Push notification when a project's state changes due to file events.

{
"method": "workspace/onProjectStateChanged",
"params": {
"project": "my-org/domain",
"previousState": "ready",
"currentState": "stale",
"reason": "Source files modified"
}
}

workspace/onBuildComplete

Push notification when automatic rebuild completes.

{
"method": "workspace/onBuildComplete",
"params": {
"project": "my-org/domain",
"success": true,
"diagnostics": [],
"duration": 1234
}
}

Debouncing

File events are debounced to avoid excessive recompilation:

Events:     ─●─●●──●───●●●──────────────────
Debounce: ─────────────────●──────────────
└── Trigger rebuild
|<── 100ms ──>|

Configuration

# morphir.toml
[watch]
debounce-ms = 100 # Debounce interval
ignore-patterns = [ # Patterns to ignore
"**/node_modules/**",
"**/.git/**",
"**/*.bak"
]
auto-rebuild = true # Automatically rebuild on changes

Event Processing

File Change Flow

1. File saved

2. FS event received

3. Debounce (collect more events)

4. Determine affected project(s)

5. Mark project(s) as 'stale'

6. Emit 'onProjectStateChanged'

7. If auto-rebuild enabled:
│ ├── Recompile affected project(s)
│ └── Emit 'onBuildComplete'

8. Emit 'onFileChanged' (batched events)

Affected Project Detection

/// Determine which project a file belongs to
fn find_project_for_path(
workspace: WorkspaceInfo,
path: String,
) -> Option(PackagePath) {
workspace.projects
|> list.find(fn(p) { string.starts_with(path, p.path) })
|> option.map(fn(p) { p.name })
}

Watch Strategies

Recursive Watch

Watch entire source directories recursively (default):

packages/domain/src/
├── Domain/
│ ├── User.elm ← watched
│ └── Order.elm ← watched
└── Utils.elm ← watched

Glob-Based Watch

Watch specific patterns:

[watch]
patterns = [
"**/*.elm",
"**/*.morphir",
"!**/*_test.elm" # Exclude tests
]

Selective Watch

Watch only specific projects:

[watch]
projects = ["my-org/core", "my-org/domain"] # Only these

Error Handling

ErrorCauseRecovery
WatchErrorFS watcher failedRestart watcher
TooManyFilesWatch limit exceededUse ignore patterns
PermissionDeniedCannot access directoryCheck permissions
PathNotFoundWatched path deletedRe-scan workspace

Platform Considerations

Linux (inotify)

  • Default limit: ~8192 watches
  • Increase with: fs.inotify.max_user_watches

macOS (FSEvents)

  • No practical limit
  • Slightly higher latency

Windows (ReadDirectoryChangesW)

  • Works per-directory
  • May miss rapid changes

Best Practices

  1. Ignore Generated Files: Don't watch .morphir-dist/, node_modules/
  2. Reasonable Debounce: 100-300ms balances responsiveness and efficiency
  3. Batch Events: Process multiple changes together when possible
  4. Graceful Degradation: Fall back to polling if native watching fails
  5. Resource Limits: Monitor memory/CPU usage of watcher