Skip to content

FileSystem watch silently falls back to non-recursive fs.watch when WatchBackend layer is provided in wrong order #5913

@schickling

Description

@schickling

Summary

When composing the NodeFileSystem.layer with the ParcelWatcher layer (or any WatchBackend provider), the layer ordering is critical but non-obvious. Providing layers in the wrong order silently falls back to Node.js's built-in fs.watch which is not recursive, instead of using the Parcel watcher.

Reproduction

import { Effect, Layer, Stream } from 'effect'
import { FileSystem } from '@effect/platform'
import { NodeFileSystem } from '@effect/platform-node'
import { layer as ParcelWatcherLayer } from '@effect/platform-node/NodeFileSystem/ParcelWatcher'

const program = Effect.gen(function* () {
  const fs = yield* FileSystem.FileSystem
  const watchStream = fs.watch('/some/directory')
  yield* Stream.runForEach(watchStream, (event) => Effect.log(event))
})

// ❌ WRONG: This silently uses fs.watch (non-recursive) instead of Parcel watcher
await program.pipe(
  Effect.provide(ParcelWatcherLayer),
  Effect.provide(NodeFileSystem.layer),
  Effect.runPromise
)

// ✅ CORRECT: Using Layer.provideMerge ensures WatchBackend is available
const layer = NodeFileSystem.layer.pipe(Layer.provideMerge(ParcelWatcherLayer))
await program.pipe(
  Effect.provide(layer),
  Effect.runPromise
)

Root Cause

The NodeFileSystem.layer uses Effect.serviceOption(FileSystem.WatchBackend) at construction time:

const makeFileSystem = Effect.map(
  Effect.serviceOption(FileSystem.WatchBackend),
  backend => FileSystem.make({
    // ...
    watch(path, options) {
      return watch(backend, path, options) // Falls back to fs.watch if backend is None
    },
  })
)

When using chained Effect.provide calls, the outer layer is constructed first. So:

  • Effect.provide(ParcelWatcherLayer) (inner) - constructed SECOND
  • Effect.provide(NodeFileSystem.layer) (outer) - constructed FIRST, but WatchBackend isn't available yet!

Impact

This is a subtle bug that's hard to diagnose:

  1. Silent fallback: No errors or warnings - code appears to work
  2. Behavioral difference: fs.watch is not recursive by default, so nested file changes are missed
  3. Platform differences: fs.watch has varying behavior across platforms

Suggestions

Option 1: Documentation warning

Add prominent documentation about layer ordering requirements when using WatchBackend.

Option 2: Runtime warning (preferred)

When NodeFileSystem.layer is constructed without a WatchBackend service available, log a warning suggesting the user check their layer composition:

const makeFileSystem = Effect.map(
  Effect.serviceOption(FileSystem.WatchBackend),
  backend => {
    if (Option.isNone(backend)) {
      // Could log a warning here about fallback behavior
    }
    return FileSystem.make({ /* ... */ })
  }
)

Option 3: Lint rule

The Effect LSP could warn when it detects chained Effect.provide calls with layers that have construction-time dependencies on each other.

Note: The Effect LSP already has a multipleEffectProvide warning, but it doesn't specifically catch this dependency ordering issue.

Environment

  • Effect version: 3.19.x
  • @effect/platform-node version: 0.98.x
  • Platform: macOS (darwin arm64)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions