Repository: cloudflare/workerd
Pull request #4116: Experimental implementation of the Web FileSystem api
Author: login: jasnell
Created: 2025-05-08T22:20:47Z | Merged: 2025-05-09T23:05:31Z

Separated out from the larger #4029 PR. This Pr focuses specifically on the Web FileSystem API impl

This will remain behind an experimental compat flag until the impl is complete and there are web platform tests available for it.

As this is behind an experimental compat flag, this can be safely landed before having the appropriate web platform tests landed.



Diff:
diff --git a/samples/webfs/README.md b/samples/webfs/README.md
new file mode 100644
index 00000000000..b3afce1caec
--- /dev/null
+++ b/samples/webfs/README.md
@@ -0,0 +1,28 @@
+# Web File System API Example
+
+To run the example on http://localhost:8080
+
+```sh
+$ ./workerd serve config.capnp
+```
+
+To run using bazel
+
+```sh
+$ bazel run //src/workerd/server:workerd -- serve ~/cloudflare/workerd/samples/helloworld_esm/config.capnp
+```
+
+To create a standalone binary that can be run:
+
+```sh
+$ ./workerd compile config.capnp > helloworld
+
+$ ./helloworld
+```
+
+To test:
+
+```sh
+% curl http://localhost:8080
+Hello World
+```
diff --git a/samples/webfs/config.capnp b/samples/webfs/config.capnp
new file mode 100644
index 00000000000..c50b65d1be8
--- /dev/null
+++ b/samples/webfs/config.capnp
@@ -0,0 +1,35 @@
+# Imports the base schema for workerd configuration files.
+
+# Refer to the comments in /src/workerd/server/workerd.capnp for more details.
+
+using Workerd = import "/workerd/workerd.capnp";
+
+# A constant of type Workerd.Config defines the top-level configuration for an
+# instance of the workerd runtime. A single config file can contain multiple
+# Workerd.Config definitions and must have at least one.
+const helloWorldExample :Workerd.Config = (
+
+  # Every workerd instance consists of a set of named services. A worker, for instance,
+  # is a type of service. Other types of services can include external servers, the
+  # ability to talk to a network, or accessing a disk directory. Here we create a single
+  # worker service. The configuration details for the worker are defined below.
+  services = [ (name = "main", worker = .helloWorld) ],
+
+  # Each configuration defines the sockets on which the server will listen.
+  # Here, we create a single socket that will listen on localhost port 8080, and will
+  # dispatch to the "main" service that we defined above.
+  sockets = [ ( name = "http", address = "*:8080", http = (), service = "main" ) ]
+);
+
+# The definition of the actual helloWorld worker exposed using the "main" service.
+# In this example the worker is implemented as an ESM module (see worker.js).
+# The compatibilityDate is required. For more details on compatibility dates see:
+#   https://developers.cloudflare.com/workers/platform/compatibility-dates/
+
+const helloWorld :Workerd.Worker = (
+  modules = [
+    (name = "worker", esModule = embed "worker.js")
+  ],
+  compatibilityDate = "2025-05-01",
+  compatibilityFlags = ["enable_web_file_system", "experimental"]
+);
diff --git a/samples/webfs/worker.js b/samples/webfs/worker.js
new file mode 100644
index 00000000000..5953ef3f0e8
--- /dev/null
+++ b/samples/webfs/worker.js
@@ -0,0 +1,23 @@
+// Copyright (c) 2017-2023 Cloudflare, Inc.
+// Licensed under the Apache 2.0 license found in the LICENSE file or at:
+//     https://opensource.org/licenses/Apache-2.0
+
+const dir = await navigator.storage.getDirectory();
+const bundle = await dir.getDirectoryHandle("tmp");
+const file = await bundle.getFileHandle("foo", { create: true });
+const handle = await file.createSyncAccessHandle();
+
+const enc = new TextEncoder();
+handle.write(enc.encode("Hello World"));
+handle.write(enc.encode("!!!\n"));
+console.log(handle.getSize());
+handle.close();
+
+const data = await file.getFile();
+console.log(await data.text());
+
+export default {
+  async fetch(req, env) {
+    return new Response("Hello World\n");
+  }
+};
diff --git a/src/workerd/api/BUILD.bazel b/src/workerd/api/BUILD.bazel
index d8249d437da..bb855468654 100644
--- a/src/workerd/api/BUILD.bazel
+++ b/src/workerd/api/BUILD.bazel
@@ -710,3 +710,9 @@ wd_test(
     args = ["--experimental"],
     data = ["tests/disable-importable-env-test.js"],
 )
+
+wd_test(
+    src = "tests/webfs-test.wd-test",
+    args = ["--experimental"],
+    data = ["tests/webfs-test.js"],
+)
diff --git a/src/workerd/api/filesystem.c++ b/src/workerd/api/filesystem.c++
new file mode 100644
index 00000000000..fb2b6e60a79
--- /dev/null
+++ b/src/workerd/api/filesystem.c++
@@ -0,0 +1,449 @@
+#include "filesystem.h"
+
+#include "blob.h"
+
+#include <workerd/api/streams/standard.h>
+
+namespace workerd::api {
+
+// =======================================================================================
+// Implementation of the Web File System API
+
+namespace {
+constexpr bool isValidFileName(kj::StringPtr name) {
+  return name.size() > 0 && name != "."_kj && name != ".."_kj && name.find("/"_kj) == kj::none;
+}
+}  // namespace
+
+jsg::Promise<jsg::Ref<FileSystemDirectoryHandle>> StorageManager::getDirectory(
+    jsg::Lock& js, const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception) {
+  KJ_IF_SOME(vfs, workerd::VirtualFileSystem::tryGetCurrent(js)) {
+    return js.resolvedPromise(
+        js.alloc<FileSystemDirectoryHandle>(jsg::USVString(), vfs.getRoot(js)));
+  }
+
+  auto ex = js.domException(kj::str("SecurityError"), kj::str("No current virtual file system"));
+  return js.rejectedPromise<jsg::Ref<FileSystemDirectoryHandle>>(exception.wrap(js, kj::mv(ex)));
+}
+
+FileSystemDirectoryHandle::FileSystemDirectoryHandle(
+    jsg::USVString name, kj::Rc<workerd::Directory> inner)
+    : FileSystemHandle(kj::mv(name)),
+      inner(kj::mv(inner)) {}
+
+jsg::Promise<bool> FileSystemDirectoryHandle::isSameEntry(
+    jsg::Lock& js, jsg::Ref<FileSystemHandle> other) {
+  KJ_IF_SOME(other, kj::dynamicDowncastIfAvailable<FileSystemDirectoryHandle>(*other)) {
+    return js.resolvedPromise(other.inner.get() == inner.get());
+  }
+  return js.resolvedPromise(false);
+}
+
+jsg::Promise<jsg::Ref<FileSystemFileHandle>> FileSystemDirectoryHandle::getFileHandle(jsg::Lock& js,
+    jsg::USVString name,
+    jsg::Optional<FileSystemGetFileOptions> options,
+    const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception) {
+  JSG_REQUIRE(isValidFileName(name), TypeError, "Invalid file name");
+  kj::Maybe<FsType> createAs;
+  KJ_IF_SOME(opts, options) {
+    if (opts.create) createAs = FsType::FILE;
+  }
+  KJ_IF_SOME(node,
+      inner->tryOpen(js, kj::Path({name}),
+          Directory::OpenOptions{
+            .createAs = createAs,
+          })) {
+    KJ_SWITCH_ONEOF(node) {
+      KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
+        return js.resolvedPromise(js.alloc<FileSystemFileHandle>(kj::mv(name), kj::mv(file)));
+      }
+      KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
+        auto ex =
+            js.domException(kj::str("TypeMismatchError"), kj::str("File name is a directory"));
+        return js.rejectedPromise<jsg::Ref<FileSystemFileHandle>>(exception.wrap(js, kj::mv(ex)));
+      }
+      KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
+        KJ_UNREACHABLE;
+      }
+    }
+    KJ_UNREACHABLE;
+  }
+
+  auto ex = js.domException(kj::str("NotFoundError"), kj::str("File not found"));
+  return js.rejectedPromise<jsg::Ref<FileSystemFileHandle>>(exception.wrap(js, kj::mv(ex)));
+}
+
+jsg::Promise<jsg::Ref<FileSystemDirectoryHandle>> FileSystemDirectoryHandle::getDirectoryHandle(
+    jsg::Lock& js,
+    jsg::USVString name,
+    jsg::Optional<FileSystemGetDirectoryOptions> options,
+    const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception) {
+  JSG_REQUIRE(isValidFileName(name), TypeError, "Invalid file name");
+  kj::Maybe<FsType> createAs;
+  KJ_IF_SOME(opts, options) {
+    if (opts.create) createAs = FsType::FILE;
+  }
+  KJ_IF_SOME(node,
+      inner->tryOpen(js, kj::Path({name}),
+          Directory::OpenOptions{
+            .createAs = createAs,
+          })) {
+    KJ_SWITCH_ONEOF(node) {
+      KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
+        return js.resolvedPromise(js.alloc<FileSystemDirectoryHandle>(kj::mv(name), kj::mv(dir)));
+      }
+      KJ_CASE_ONEOF(dir, kj::Rc<workerd::File>) {
+        auto ex = js.domException(kj::str("TypeMismatchError"), kj::str("File name is a file"));
+        return js.rejectedPromise<jsg::Ref<FileSystemDirectoryHandle>>(
+            exception.wrap(js, kj::mv(ex)));
+      }
+      KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
+        KJ_UNREACHABLE;
+      }
+    }
+    KJ_UNREACHABLE;
+  }
+
+  auto ex = js.domException(kj::str("NotFoundError"), kj::str("File not found"));
+  return js.rejectedPromise<jsg::Ref<FileSystemDirectoryHandle>>(exception.wrap(js, kj::mv(ex)));
+}
+
+jsg::Promise<void> FileSystemDirectoryHandle::removeEntry(jsg::Lock& js,
+    jsg::USVString name,
+    jsg::Optional<FileSystemRemoveOptions> options,
+    const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception) {
+  JSG_REQUIRE(isValidFileName(name), TypeError, "Invalid file name");
+  auto opts = options.orDefault(FileSystemRemoveOptions{});
+
+  if (inner->remove(js, kj::Path({name}),
+          workerd::Directory::RemoveOptions{
+            .recursive = opts.recursive,
+          })) {
+    return js.resolvedPromise();
+  }
+
+  auto ex = js.domException(kj::str("NotFoundError"), kj::str("File not found"));
+  return js.rejectedPromise<void>(exception.wrap(js, kj::mv(ex)));
+}
+
+jsg::Promise<kj::Array<jsg::USVString>> FileSystemDirectoryHandle::resolve(
+    jsg::Lock& js, jsg::Ref<FileSystemHandle> possibleDescendant) {
+  JSG_FAIL_REQUIRE(Error, "Not implemented");
+}
+
+namespace {
+kj::Array<jsg::Ref<FileSystemHandle>> collectEntries(
+    jsg::Lock& js, kj::Rc<workerd::Directory>& inner) {
+  kj::Vector<jsg::Ref<FileSystemHandle>> entries;
+  for (auto& entry: *inner.get()) {
+    KJ_SWITCH_ONEOF(entry.value) {
+      KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
+        entries.add(
+            js.alloc<FileSystemFileHandle>(js.accountedUSVString(entry.key), file.addRef()));
+      }
+      KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
+        entries.add(
+            js.alloc<FileSystemDirectoryHandle>(js.accountedUSVString(entry.key), dir.addRef()));
+      }
+      KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
+        SymbolicLinkRecursionGuardScope guardScope;
+        guardScope.checkSeen(link.get());
+        KJ_IF_SOME(res, link->resolve(js)) {
+          KJ_SWITCH_ONEOF(res) {
+            KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
+              entries.add(
+                  js.alloc<FileSystemFileHandle>(js.accountedUSVString(entry.key), file.addRef()));
+            }
+            KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
+              entries.add(js.alloc<FileSystemDirectoryHandle>(
+                  js.accountedUSVString(entry.key), dir.addRef()));
+            }
+          }
+        }
+      }
+    }
+  }
+  return entries.releaseAsArray();
+}
+}  // namespace
+
+jsg::Ref<FileSystemDirectoryHandle::EntryIterator> FileSystemDirectoryHandle::entries(
+    jsg::Lock& js) {
+  return js.alloc<EntryIterator>(IteratorState(JSG_THIS, collectEntries(js, inner)));
+}
+
+jsg::Ref<FileSystemDirectoryHandle::KeyIterator> FileSystemDirectoryHandle::keys(jsg::Lock& js) {
+  return js.alloc<KeyIterator>(IteratorState(JSG_THIS, collectEntries(js, inner)));
+}
+
+jsg::Ref<FileSystemDirectoryHandle::ValueIterator> FileSystemDirectoryHandle::values(
+    jsg::Lock& js) {
+  return js.alloc<ValueIterator>(IteratorState(JSG_THIS, collectEntries(js, inner)));
+}
+
+void FileSystemDirectoryHandle::forEach(jsg::Lock& js,
+    jsg::Function<void(
+        jsg::USVString, jsg::Ref<FileSystemHandle>, jsg::Ref<FileSystemDirectoryHandle>)> callback,
+    jsg::Optional<jsg::Value> thisArg) {
+  auto receiver = js.v8Undefined();
+  KJ_IF_SOME(arg, thisArg) {
+    auto handle = arg.getHandle(js);
+    if (!handle->IsNullOrUndefined()) {
+      receiver = handle;
+    }
+  }
+  callback.setReceiver(js.v8Ref(receiver));
+
+  for (auto& entry: collectEntries(js, inner)) {
+    callback(js, js.accountedUSVString(entry->getName(js)), entry.addRef(), JSG_THIS);
+  }
+}
+
+FileSystemFileHandle::FileSystemFileHandle(jsg::USVString name, kj::Rc<workerd::File> inner)
+    : FileSystemHandle(kj::mv(name)),
+      inner(kj::mv(inner)) {}
+
+jsg::Promise<bool> FileSystemFileHandle::isSameEntry(
+    jsg::Lock& js, jsg::Ref<FileSystemHandle> other) {
+  KJ_IF_SOME(other, kj::dynamicDowncastIfAvailable<FileSystemFileHandle>(*other)) {
+    return js.resolvedPromise(other.inner.get() == inner.get());
+  }
+  return js.resolvedPromise(false);
+}
+
+jsg::Promise<jsg::Ref<File>> FileSystemFileHandle::getFile(jsg::Lock& js) {
+  // TODO(node-fs): Currently this copies the file data into the new File object.
+  // Alternatively, File/Blob can be modified to allow it to be backed by a
+  // workerd::File such that it does not need to create a separate in-memory
+  // copy of the data. We can make that optimization as a follow-up, however.
+  auto stat = inner->stat(js);
+  return js.resolvedPromise(
+      js.alloc<File>(js, inner->readAllBytes(js), js.accountedUSVString(getName(js)), kj::String(),
+          (stat.lastModified - kj::UNIX_EPOCH) / kj::MILLISECONDS));
+}
+
+jsg::Promise<jsg::Ref<FileSystemWritableFileStream>> FileSystemFileHandle::createWritable(
+    jsg::Lock& js, jsg::Optional<FileSystemCreateWritableOptions> options) {
+
+  // Per the spec, the writable stream we create here is expected to write into
+  // a temporary space until the stream is closed. When closed, the original file
+  // contents are replaced with the new contents. If the stream is aborted or
+  // errored, the temporary file data is discarded.
+
+  auto opts = options.orDefault(FileSystemCreateWritableOptions{});
+
+  // If keepExistingData is true, the temporary file is created with a copy of
+  // the original file data. Otherwise, the temporary file is created empty,
+  // which means that if we create a writable stream and close it without writing
+  // anything, the original file data is lost.
+  bool keepExistingData = opts.keepExistingData.orDefault(false);
+
+  auto sharedState = kj::rc<FileSystemWritableFileStream::State>(
+      inner.addRef(), keepExistingData ? inner->clone(js) : workerd::File::newWritable(js));
+  auto stream =
+      js.alloc<FileSystemWritableFileStream>(newWritableStreamJsController(), sharedState.addRef());
+  stream->getController().setup(js,
+      UnderlyingSink{.type = kj::str("bytes"),
+        .write =
+            [state = sharedState.addRef()](
+                jsg::Lock& js, v8::Local<v8::Value> chunk, auto c) mutable {
+    // TODO(node-fs): The spec allows the write parameter to be a WriteParams struct
+    // as an alternative to a BufferSource. We should implement this before shipping
+    // this feature.
+    return js.tryCatch([&] {
+      auto& inner = JSG_REQUIRE_NONNULL(state->temp, DOMInvalidStateError, "File handle closed");
+      // Make sure what we got can be interpreted as bytes...
+      std::shared_ptr<v8::BackingStore> backing;
+      if (chunk->IsArrayBuffer() || chunk->IsArrayBufferView()) {
+        jsg::BufferSource source(js, chunk);
+        if (source.size() == 0) return js.resolvedPromise();
+        state->position += inner->write(js, state->position, source);
+        return js.resolvedPromise();
+      }
+      return js.rejectedPromise<void>(
+          js.typeError("WritableStream received a value that is not an ArrayBuffer,"
+                       "ArrayBufferView, or string type."));
+    }, [&](jsg::Value exception) { return js.rejectedPromise<void>(kj::mv(exception)); });
+  },
+        .abort =
+            [state = sharedState.addRef()](jsg::Lock& js, auto reason) mutable {
+    // When aborted, we just drop any of the written data on the floor.
+    state->clear();
+    return js.resolvedPromise();
+  },
+        .close =
+            [state = sharedState.addRef()](jsg::Lock& js) mutable {
+    KJ_DEFER(state->clear());
+    return js.tryCatch([&] {
+      KJ_IF_SOME(temp, state->temp) {
+        KJ_IF_SOME(file, state->file) {
+          file->replace(js, kj::mv(temp));
+        }
+      }
+      return js.resolvedPromise();
+    }, [&](jsg::Value exception) { return js.rejectedPromise<void>(kj::mv(exception)); });
+  }},
+      kj::none);
+
+  return js.resolvedPromise(kj::mv(stream));
+}
+
+jsg::Promise<jsg::Ref<FileSystemSyncAccessHandle>> FileSystemFileHandle::createSyncAccessHandle(
+    jsg::Lock& js) {
+  // TODO(node-fs): Per the spec, creating a sync access handle or creating a stream
+  // should be mutually exclusive and should lock the file such that no other sync
+  // handles or streams can be created until the handle/stream is closed. We are not
+  // yet implementing locks on the file and should consider doing so before we ship this.
+  return js.resolvedPromise(js.alloc<FileSystemSyncAccessHandle>(inner.addRef()));
+}
+
+FileSystemSyncAccessHandle::FileSystemSyncAccessHandle(kj::Rc<workerd::File> inner)
+    : inner(kj::mv(inner)) {}
+
+uint32_t FileSystemSyncAccessHandle::read(
+    jsg::Lock& js, jsg::BufferSource buffer, jsg::Optional<FileSystemReadWriteOptions> options) {
+  auto& inner = JSG_REQUIRE_NONNULL(this->inner, DOMInvalidStateError, "File handle closed");
+  auto offset = options.orDefault({}).at.orDefault(position);
+  auto stat = inner->stat(js);
+  if (offset > stat.size && !stat.device) {
+    position = stat.size;
+    return 0;
+  }
+  auto ret = inner->read(js, offset, buffer);
+
+  position += ret;
+  return ret;
+}
+
+uint32_t FileSystemSyncAccessHandle::write(
+    jsg::Lock& js, jsg::BufferSource buffer, jsg::Optional<FileSystemReadWriteOptions> options) {
+  auto& inner = JSG_REQUIRE_NONNULL(this->inner, DOMInvalidStateError, "File handle closed");
+  auto offset = options.orDefault({}).at.orDefault(position);
+  auto stat = inner->stat(js);
+  if (offset > stat.size) {
+    inner->resize(js, offset + buffer.size());
+  }
+  auto ret = inner->write(js, offset, buffer);
+  position = offset + ret;
+  return ret;
+}
+
+void FileSystemSyncAccessHandle::truncate(jsg::Lock& js, uint32_t newSize) {
+  auto& inner = JSG_REQUIRE_NONNULL(this->inner, DOMInvalidStateError, "File handle closed");
+  inner->resize(js, newSize);
+  auto stat = inner->stat(js);
+  if (position > stat.size) {
+    position = stat.size;
+  }
+}
+
+uint32_t FileSystemSyncAccessHandle::getSize(jsg::Lock& js) {
+  auto& inner = JSG_REQUIRE_NONNULL(this->inner, DOMInvalidStateError, "File handle closed");
+  return inner->stat(js).size;
+}
+
+void FileSystemSyncAccessHandle::flush(jsg::Lock& js) {
+  JSG_REQUIRE_NONNULL(this->inner, DOMInvalidStateError, "File handle closed");
+  // Non-op
+}
+
+void FileSystemSyncAccessHandle::close(jsg::Lock& js) {
+  inner = kj::none;
+}
+
+FileSystemWritableFileStream::FileSystemWritableFileStream(
+    kj::Own<WritableStreamController> controller, kj::Rc<State> sharedState)
+    : WritableStream(kj::mv(controller)),
+      sharedState(kj::mv(sharedState)) {}
+
+jsg::Promise<void> FileSystemWritableFileStream::write(
+    jsg::Lock& js, kj::OneOf<jsg::Ref<Blob>, jsg::BufferSource, kj::String, WriteParams> data) {
+  JSG_REQUIRE(!getController().isLockedToWriter(), TypeError,
+      "Cannot write to a stream that is locked to a reader");
+  auto& inner = JSG_REQUIRE_NONNULL(sharedState->temp, DOMInvalidStateError, "File handle closed");
+  auto writer = getWriter(js);
+  KJ_DEFER(writer->releaseLock(js));
+
+  return js.tryCatch([&] {
+    KJ_SWITCH_ONEOF(data) {
+      KJ_CASE_ONEOF(blob, jsg::Ref<Blob>) {
+        sharedState->position += inner->write(js, sharedState->position, blob->getData());
+      }
+      KJ_CASE_ONEOF(buffer, jsg::BufferSource) {
+        sharedState->position += inner->write(js, sharedState->position, buffer);
+      }
+      KJ_CASE_ONEOF(str, kj::String) {
+        sharedState->position += inner->write(js, sharedState->position, str);
+      }
+      KJ_CASE_ONEOF(params, WriteParams) {
+        uint32_t offset = sharedState->position;
+        KJ_IF_SOME(offset, params.position) {
+          auto stat = inner->stat(js);
+          if (offset > stat.size) {
+            inner->resize(js, offset);
+          }
+        }
+
+        if (params.type == "write"_kj) {
+          KJ_IF_SOME(data, params.data) {
+            KJ_SWITCH_ONEOF(data) {
+              KJ_CASE_ONEOF(blob, jsg::Ref<Blob>) {
+                auto ret = inner->write(js, offset, blob->getData());
+                sharedState->position = offset + ret;
+              }
+              KJ_CASE_ONEOF(buffer, jsg::BufferSource) {
+                auto ret = inner->write(js, offset, buffer);
+                sharedState->position = offset + ret;
+              }
+              KJ_CASE_ONEOF(str, kj::String) {
+                auto ret = inner->write(js, offset, str);
+                sharedState->position = offset + ret;
+              }
+            }
+          }
+        } else if (params.type == "seek"_kj) {
+          uint32_t pos = params.position.orDefault(0);
+          sharedState->position += pos;
+          auto stat = inner->stat(js);
+          if (sharedState->position > stat.size) {
+            inner->resize(js, sharedState->position);
+          }
+        } else if (params.type == "truncate"_kj) {
+          uint32_t size = params.size.orDefault(0);
+          inner->resize(js, size);
+          auto stat = inner->stat(js);
+          if (sharedState->position > stat.size) {
+            sharedState->position = stat.size;
+          }
+        } else {
+          return js.rejectedPromise<void>(
+              js.typeError(kj::str("Invalid write type: ", params.type)));
+        }
+      }
+    }
+
+    return js.resolvedPromise();
+  }, [&](jsg::Value exception) { return js.rejectedPromise<void>(kj::mv(exception)); });
+}
+
+jsg::Promise<void> FileSystemWritableFileStream::seek(jsg::Lock& js, uint32_t position) {
+  auto& inner = JSG_REQUIRE_NONNULL(sharedState->temp, DOMInvalidStateError, "File handle closed");
+  auto stat = inner->stat(js);
+  if (position > stat.size) {
+    inner->resize(js, position);
+  }
+  sharedState->position = position;
+  return js.resolvedPromise();
+}
+
+jsg::Promise<void> FileSystemWritableFileStream::truncate(jsg::Lock& js, uint32_t size) {
+  auto& inner = JSG_REQUIRE_NONNULL(sharedState->temp, DOMInvalidStateError, "File handle closed");
+  inner->resize(js, size);
+  auto stat = inner->stat(js);
+  if (sharedState->position > stat.size) {
+    sharedState->position = stat.size;
+  }
+  return js.resolvedPromise();
+}
+
+}  // namespace workerd::api
diff --git a/src/workerd/api/filesystem.h b/src/workerd/api/filesystem.h
new file mode 100644
index 00000000000..a0480cb8f6d
--- /dev/null
+++ b/src/workerd/api/filesystem.h
@@ -0,0 +1,373 @@
+#pragma once
+
+#include <workerd/api/streams/writable.h>
+#include <workerd/io/worker-fs.h>
+#include <workerd/jsg/iterator.h>
+#include <workerd/jsg/jsg.h>
+
+namespace workerd::api {
+
+class Blob;
+class File;
+
+// ======================================================================================
+// An implementation of the WHATWG Web File System API (https://fs.spec.whatwg.org/)
+// All of the classes in this part of the impl are defined by the spec.
+
+class FileSystemSyncAccessHandle;
+class FileSystemWritableFileStream;
+
+class FileSystemHandle: public jsg::Object {
+  // TODO(node-fs): The spec defines FileSystemHandle objects as being
+  // serializable, meaning that they should work with structured cloning.
+  // In workers, this would suggest also that they should work with jsrpc.
+  // We don't yet implement any form of serialization for these objects.
+  // Since each worker has its own file system, it might be a bit weird
+  // as the received file on the other side would not actually be in that
+  // destination worker's file system... that is, it would be an orphaned
+  // file handle. It would still likely be useful tho. As a follow up
+  // step, we will implement serialization/deserialization of these objects.
+ public:
+  FileSystemHandle(jsg::USVString name): name(kj::mv(name)) {}
+  const jsg::USVString& getName(jsg::Lock& js) {
+    return name;
+  }
+
+  virtual kj::StringPtr getKind(jsg::Lock& js) {
+    // Implemented by subclasses.
+    KJ_UNIMPLEMENTED("getKind() not implemented");
+  }
+  virtual jsg::Promise<bool> isSameEntry(jsg::Lock& js, jsg::Ref<FileSystemHandle> other) {
+    // Implemented by subclasses.
+    KJ_UNIMPLEMENTED("isSameEntry() not implemented");
+  }
+
+  JSG_RESOURCE_TYPE(FileSystemHandle) {
+    JSG_READONLY_PROTOTYPE_PROPERTY(kind, getKind);
+    JSG_READONLY_PROTOTYPE_PROPERTY(name, getName);
+    JSG_METHOD(isSameEntry);
+  }
+
+ private:
+  jsg::USVString name;
+};
+
+class FileSystemFileHandle: public FileSystemHandle {
+ public:
+  FileSystemFileHandle(jsg::USVString name, kj::Rc<workerd::File> inner);
+
+  kj::StringPtr getKind(jsg::Lock& js) override {
+    return "file"_kj;
+  }
+
+  jsg::Promise<bool> isSameEntry(jsg::Lock& js, jsg::Ref<FileSystemHandle> other) override;
+
+  struct FileSystemCreateWritableOptions {
+    jsg::Optional<bool> keepExistingData;
+    JSG_STRUCT(keepExistingData);
+  };
+
+  jsg::Promise<jsg::Ref<File>> getFile(jsg::Lock& js);
+
+  jsg::Promise<jsg::Ref<FileSystemWritableFileStream>> createWritable(
+      jsg::Lock& js, jsg::Optional<FileSystemCreateWritableOptions> options);
+
+  jsg::Promise<jsg::Ref<FileSystemSyncAccessHandle>> createSyncAccessHandle(jsg::Lock& js);
+
+  JSG_RESOURCE_TYPE(FileSystemFileHandle) {
+    JSG_INHERIT(FileSystemHandle);
+    JSG_METHOD(getFile);
+    JSG_METHOD(createWritable);
+    JSG_METHOD(createSyncAccessHandle);
+  }
+
+ private:
+  kj::Rc<workerd::File> inner;
+};
+
+class FileSystemDirectoryHandle final: public FileSystemHandle {
+  // TODO(node-fs): Per the spec, FileSystemDirectoryHandler objects are
+  // expected to be async iterables. Here we implement it as a sync iterable.
+  // The effect ends up being largely the same but it's obviously better to
+  // use the asyn iterable API instead. We should implement this before we
+  // ship this feature.
+ public:
+  struct EntryType {
+    jsg::USVString key;
+    jsg::Ref<FileSystemHandle> value;
+    JSG_STRUCT(key, value);
+    EntryType(jsg::USVString key, jsg::Ref<FileSystemHandle> value)
+        : key(kj::mv(key)),
+          value(kj::mv(value)) {}
+  };
+
+ private:
+  using EntryIteratorType = EntryType;
+  using KeyIteratorType = jsg::USVString;
+  using ValueIteratorType = jsg::Ref<FileSystemHandle>;
+
+  struct IteratorState final {
+    jsg::Ref<FileSystemDirectoryHandle> parent;
+    kj::Array<jsg::Ref<FileSystemHandle>> entries;
+    uint index = 0;
+
+    IteratorState(
+        jsg::Ref<FileSystemDirectoryHandle> parent, kj::Array<jsg::Ref<FileSystemHandle>> entries)
+        : parent(kj::mv(parent)),
+          entries(kj::mv(entries)) {}
+
+    void visitForGc(jsg::GcVisitor& visitor) {
+      visitor.visit(parent);
+      visitor.visitAll(entries);
+    }
+
+    JSG_MEMORY_INFO(IteratorState) {
+      tracker.trackField("parent", parent);
+      for (auto& entry: entries) {
+        tracker.trackField("entry", entry);
+      }
+    }
+  };
+
+ public:
+  FileSystemDirectoryHandle(jsg::USVString name, kj::Rc<workerd::Directory> inner);
+
+  kj::StringPtr getKind(jsg::Lock& js) override {
+    return "directory"_kj;
+  }
+
+  jsg::Promise<bool> isSameEntry(jsg::Lock& js, jsg::Ref<FileSystemHandle> other) override;
+
+  struct FileSystemGetFileOptions {
+    bool create = false;
+    JSG_STRUCT(create);
+  };
+
+  struct FileSystemGetDirectoryOptions {
+    bool create = false;
+    JSG_STRUCT(create);
+  };
+
+  struct FileSystemRemoveOptions {
+    bool recursive = false;
+    JSG_STRUCT(recursive);
+  };
+
+  jsg::Promise<jsg::Ref<FileSystemFileHandle>> getFileHandle(jsg::Lock& js,
+      jsg::USVString name,
+      jsg::Optional<FileSystemGetFileOptions> options,
+      const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception);
+
+  jsg::Promise<jsg::Ref<FileSystemDirectoryHandle>> getDirectoryHandle(jsg::Lock& js,
+      jsg::USVString name,
+      jsg::Optional<FileSystemGetDirectoryOptions> options,
+      const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception);
+
+  jsg::Promise<void> removeEntry(jsg::Lock& js,
+      jsg::USVString name,
+      jsg::Optional<FileSystemRemoveOptions> options,
+      const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception);
+
+  // TODO(node-fs): We are not currently implementing the resolve() method
+  // as defined in the spec. This is a bit tricky as it requires us to work
+  // backwards through the parent directories to find the path to the file.
+  // However, our file node's do not currently have a parent pointer, nor do
+  // they know through which path they are accessible through. Support for this
+  // is not critical so we likely won't implement this before we ship the feature
+  // but it is something we should consider doing in the future.
+  jsg::Promise<kj::Array<jsg::USVString>> resolve(
+      jsg::Lock& js, jsg::Ref<FileSystemHandle> possibleDescendant);
+
+  JSG_ASYNC_ITERATOR(EntryIterator,
+      entries,
+      EntryIteratorType,
+      IteratorState,
+      iteratorNext<EntryIteratorType>,
+      iteratorReturn<EntryIteratorType>);
+  JSG_ASYNC_ITERATOR(KeyIterator,
+      keys,
+      KeyIteratorType,
+      IteratorState,
+      iteratorNext<KeyIteratorType>,
+      iteratorReturn<KeyIteratorType>);
+  JSG_ASYNC_ITERATOR(ValueIterator,
+      values,
+      ValueIteratorType,
+      IteratorState,
+      iteratorNext<ValueIteratorType>,
+      iteratorReturn<ValueIteratorType>);
+
+  void forEach(jsg::Lock& js,
+      jsg::Function<void(
+          jsg::USVString, jsg::Ref<FileSystemHandle>, jsg::Ref<FileSystemDirectoryHandle>)>
+          callback,
+      jsg::Optional<jsg::Value> thisArg);
+
+  JSG_RESOURCE_TYPE(FileSystemDirectoryHandle) {
+    JSG_INHERIT(FileSystemHandle);
+    JSG_METHOD(getFileHandle);
+    JSG_METHOD(getDirectoryHandle);
+    JSG_METHOD(removeEntry);
+    JSG_METHOD(resolve);
+    JSG_METHOD(entries);
+    JSG_METHOD(keys);
+    JSG_METHOD(values);
+    JSG_ASYNC_ITERABLE(entries);
+    JSG_METHOD(forEach);
+  }
+
+ private:
+  kj::Rc<workerd::Directory> inner;
+
+  template <typename Type>
+  static jsg::Promise<kj::Maybe<Type>> iteratorNext(jsg::Lock& js, IteratorState& state) {
+    if (state.index >= state.entries.size()) {
+      return js.resolvedPromise<kj::Maybe<Type>>(kj::none);
+    }
+
+    auto& entry = state.entries[state.index++];
+
+    if constexpr (kj::isSameType<Type, EntryIteratorType>()) {
+      return js.resolvedPromise<kj::Maybe<Type>>(
+          EntryType(js.accountedUSVString(entry->getName(js)), entry.addRef()));
+    } else if constexpr (kj::isSameType<Type, KeyIteratorType>()) {
+      return js.resolvedPromise<kj::Maybe<Type>>(js.accountedUSVString(entry->getName(js)));
+    } else if constexpr (kj::isSameType<Type, ValueIteratorType>()) {
+      return js.resolvedPromise<kj::Maybe<Type>>(entry.addRef());
+    } else {
+      static_assert(false, "invalid iterator type");
+    }
+  }
+
+  template <typename Type>
+  static jsg::Promise<void> iteratorReturn(
+      jsg::Lock& js, IteratorState& state, jsg::Optional<Type>& value) {
+    return js.resolvedPromise();
+  }
+};
+
+class FileSystemWritableFileStream: public WritableStream {
+ public:
+  struct State: public kj::Refcounted {
+    // The FileSystemWritableFileStream is a bit transactional in nature.
+    // All writes are done to a temporary file. When the stream is closed
+    // without an error, the original file data is replaced with the data
+    // in the temporary buffer. If the stream is aborted, the temporary file
+    // is deleted and the original contents are left intact.
+    // The temporary file is not visible to the user and is not included in
+    // any directory. It is only used to hold the data until the stream
+    // is closed.
+    kj::Maybe<kj::Rc<workerd::File>> temp;
+
+    // The file we will be writing to.
+    kj::Maybe<kj::Rc<workerd::File>> file;
+
+    // A note on file position and sizes. In the spec, file sizes and positions
+    // are defined in terms of unsigned long long, or 64-bits. We are using
+    // unsigned long (uint32_t) for these instead. This is largely because
+    // the underlying implementation is holding the file in memory and 4 GB
+    // is WAY more than our isolate heap limit. A uint32_t is more than enough
+    // for our purposes.
+    uint32_t position = 0;
+    State(kj::Rc<workerd::File> file, kj::Rc<workerd::File> temp)
+        : temp(kj::mv(temp)),
+          file(kj::mv(file)) {}
+
+    void clear() {
+      temp = kj::none;
+      file = kj::none;
+      position = 0;
+    }
+  };
+
+  FileSystemWritableFileStream(
+      kj::Own<WritableStreamController> controller, kj::Rc<State> sharedState);
+
+  static jsg::Ref<FileSystemWritableFileStream> constructor() = delete;
+
+  struct WriteParams {
+    kj::String type;  // one of: write, seek, truncate
+    jsg::Optional<uint32_t> size;
+    jsg::Optional<uint32_t> position;
+    jsg::Optional<kj::OneOf<jsg::Ref<Blob>, jsg::BufferSource, kj::String>> data;
+    JSG_STRUCT(type, size, position, data);
+  };
+
+  jsg::Promise<void> write(
+      jsg::Lock& js, kj::OneOf<jsg::Ref<Blob>, jsg::BufferSource, kj::String, WriteParams> data);
+  jsg::Promise<void> seek(jsg::Lock& js, uint32_t position);
+  jsg::Promise<void> truncate(jsg::Lock& js, uint32_t size);
+
+  JSG_RESOURCE_TYPE(FileSystemWritableFileStream) {
+    JSG_INHERIT(WritableStream);
+    JSG_METHOD(write);
+    JSG_METHOD(seek);
+    JSG_METHOD(truncate);
+  }
+
+ private:
+  kj::Rc<State> sharedState;
+};
+
+class FileSystemSyncAccessHandle final: public jsg::Object {
+ public:
+  FileSystemSyncAccessHandle(kj::Rc<workerd::File> inner);
+
+  struct FileSystemReadWriteOptions {
+    jsg::Optional<uint32_t> at;
+    JSG_STRUCT(at);
+  };
+
+  uint32_t read(
+      jsg::Lock& js, jsg::BufferSource buffer, jsg::Optional<FileSystemReadWriteOptions> options);
+  uint32_t write(
+      jsg::Lock& js, jsg::BufferSource buffer, jsg::Optional<FileSystemReadWriteOptions> options);
+
+  void truncate(jsg::Lock& js, uint32_t newSize);
+  uint32_t getSize(jsg::Lock& js);
+  void flush(jsg::Lock& js);
+  void close(jsg::Lock& js);
+
+  JSG_RESOURCE_TYPE(FileSystemSyncAccessHandle) {
+    JSG_METHOD(read);
+    JSG_METHOD(write);
+    JSG_METHOD(truncate);
+    JSG_METHOD(getSize);
+    JSG_METHOD(flush);
+    JSG_METHOD(close);
+  }
+
+ private:
+  kj::Maybe<kj::Rc<workerd::File>> inner;
+  uint32_t position = 0;
+};
+
+class StorageManager final: public jsg::Object {
+ public:
+  jsg::Promise<jsg::Ref<FileSystemDirectoryHandle>> getDirectory(
+      jsg::Lock& js, const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception);
+
+  JSG_RESOURCE_TYPE(StorageManager) {
+    JSG_METHOD(getDirectory);
+  }
+};
+
+#define EW_WEB_FILESYSTEM_ISOLATE_TYPE                                                             \
+  workerd::api::FileSystemHandle, workerd::api::FileSystemFileHandle,                              \
+      workerd::api::FileSystemDirectoryHandle, workerd::api::FileSystemWritableFileStream,         \
+      workerd::api::FileSystemSyncAccessHandle, workerd::api::StorageManager,                      \
+      workerd::api::FileSystemFileHandle::FileSystemCreateWritableOptions,                         \
+      workerd::api::FileSystemDirectoryHandle::FileSystemGetFileOptions,                           \
+      workerd::api::FileSystemDirectoryHandle::FileSystemGetDirectoryOptions,                      \
+      workerd::api::FileSystemDirectoryHandle::FileSystemRemoveOptions,                            \
+      workerd::api::FileSystemSyncAccessHandle::FileSystemReadWriteOptions,                        \
+      workerd::api::FileSystemWritableFileStream::WriteParams,                                     \
+      workerd::api::FileSystemDirectoryHandle::EntryType,                                          \
+      workerd::api::FileSystemDirectoryHandle::EntryIterator,                                      \
+      workerd::api::FileSystemDirectoryHandle::KeyIterator,                                        \
+      workerd::api::FileSystemDirectoryHandle::ValueIterator,                                      \
+      workerd::api::FileSystemDirectoryHandle::EntryIterator::Next,                                \
+      workerd::api::FileSystemDirectoryHandle::KeyIterator::Next,                                  \
+      workerd::api::FileSystemDirectoryHandle::ValueIterator::Next
+
+}  // namespace workerd::api
diff --git a/src/workerd/api/global-scope.c++ b/src/workerd/api/global-scope.c++
index 2d6659ef364..74724a9b6b8 100644
--- a/src/workerd/api/global-scope.c++
+++ b/src/workerd/api/global-scope.c++
@@ -910,6 +910,10 @@ double Performance::now() {
   return dateNow();
 }
 
+jsg::Ref<StorageManager> Navigator::getStorage(jsg::Lock& js) {
+  return js.alloc<StorageManager>();
+}
+
 bool Navigator::sendBeacon(jsg::Lock& js, kj::String url, jsg::Optional<Body::Initializer> body) {
   if (IoContext::hasCurrent()) {
     auto v8Context = js.v8Context();
diff --git a/src/workerd/api/global-scope.h b/src/workerd/api/global-scope.h
index 4b6b6c2bc50..cd01a2bd6c0 100644
--- a/src/workerd/api/global-scope.h
+++ b/src/workerd/api/global-scope.h
@@ -5,6 +5,7 @@
 #pragma once
 
 #include "basics.h"
+#include "filesystem.h"
 #include "hibernation-event-params.h"
 #include "http.h"
 
@@ -94,6 +95,8 @@ class Navigator: public jsg::Object {
     return builder.finish();
   }
 
+  jsg::Ref<StorageManager> getStorage(jsg::Lock& js);
+
   JSG_RESOURCE_TYPE(Navigator, CompatibilityFlags::Reader reader) {
     JSG_METHOD(sendBeacon);
     JSG_READONLY_INSTANCE_PROPERTY(userAgent, getUserAgent);
@@ -103,6 +106,10 @@ class Navigator: public jsg::Object {
       JSG_READONLY_INSTANCE_PROPERTY(language, getLanguage);
       JSG_READONLY_INSTANCE_PROPERTY(languages, getLanguages);
     }
+
+    if (reader.getWebFileSystem()) {
+      JSG_LAZY_READONLY_INSTANCE_PROPERTY(storage, getStorage);
+    }
   }
 };
 
@@ -675,6 +682,15 @@ class ServiceWorkerGlobalScope: public WorkerGlobalScope {
     JSG_NESTED_TYPE(CountQueuingStrategy);
     JSG_NESTED_TYPE(ErrorEvent);
 
+    if (flags.getWebFileSystem()) {
+      JSG_NESTED_TYPE(FileSystemHandle);
+      JSG_NESTED_TYPE(FileSystemFileHandle);
+      JSG_NESTED_TYPE(FileSystemDirectoryHandle);
+      JSG_NESTED_TYPE(FileSystemWritableFileStream);
+      JSG_NESTED_TYPE(FileSystemSyncAccessHandle);
+      JSG_NESTED_TYPE(StorageManager);
+    }
+
     JSG_NESTED_TYPE(EventSource);
 
     if (flags.getStreamsJavaScriptControllers()) {
diff --git a/src/workerd/api/rtti.c++ b/src/workerd/api/rtti.c++
index b83ffcae4c7..aab7f91bd4c 100644
--- a/src/workerd/api/rtti.c++
+++ b/src/workerd/api/rtti.c++
@@ -12,6 +12,7 @@
 #include <workerd/api/encoding.h>
 #include <workerd/api/events.h>
 #include <workerd/api/eventsource.h>
+#include <workerd/api/filesystem.h>
 #include <workerd/api/global-scope.h>
 #include <workerd/api/html-rewriter.h>
 #include <workerd/api/hyperdrive.h>
@@ -77,7 +78,8 @@
   F("node", EW_NODE_ISOLATE_TYPES)                                                                 \
   F("rtti", EW_RTTI_ISOLATE_TYPES)                                                                 \
   F("eventsource", EW_EVENTSOURCE_ISOLATE_TYPES)                                                   \
-  F("container", EW_CONTAINER_ISOLATE_TYPES)
+  F("container", EW_CONTAINER_ISOLATE_TYPES)                                                       \
+  F("webfs", EW_WEB_FILESYSTEM_ISOLATE_TYPE)
 
 namespace workerd::api {
 
diff --git a/src/workerd/api/streams/readable.c++ b/src/workerd/api/streams/readable.c++
index bd2ce35cc75..a12fa6ea364 100644
--- a/src/workerd/api/streams/readable.c++
+++ b/src/workerd/api/streams/readable.c++
@@ -449,7 +449,7 @@ jsg::Promise<kj::Maybe<jsg::Value>> ReadableStream::nextFunction(
 }
 
 jsg::Promise<void> ReadableStream::returnFunction(
-    jsg::Lock& js, AsyncIteratorState& state, jsg::Optional<jsg::Value> value) {
+    jsg::Lock& js, AsyncIteratorState& state, jsg::Optional<jsg::Value>& value) {
   if (state.reader.get() != nullptr) {
     auto reader = kj::mv(state.reader);
     if (!state.preventCancel) {
diff --git a/src/workerd/api/streams/readable.h b/src/workerd/api/streams/readable.h
index 557f4914c6a..8f227a1354c 100644
--- a/src/workerd/api/streams/readable.h
+++ b/src/workerd/api/streams/readable.h
@@ -203,7 +203,7 @@ class ReadableStream: public jsg::Object {
   static jsg::Promise<void> returnFunction(
       jsg::Lock& js,
       AsyncIteratorState& state,
-      jsg::Optional<jsg::Value> value);
+      jsg::Optional<jsg::Value>& value);
 
 public:
   explicit ReadableStream(IoContext& ioContext,
diff --git a/src/workerd/api/tests/webfs-test.js b/src/workerd/api/tests/webfs-test.js
new file mode 100644
index 00000000000..7eafcc5b8f8
--- /dev/null
+++ b/src/workerd/api/tests/webfs-test.js
@@ -0,0 +1,131 @@
+import {
+  ok,
+  strictEqual,
+  deepStrictEqual,
+  notDeepStrictEqual,
+  rejects,
+  throws,
+} from 'node:assert';
+
+ok(navigator.storage instanceof StorageManager);
+
+const dir = await navigator.storage.getDirectory();
+ok(dir instanceof FileSystemDirectoryHandle);
+strictEqual(dir.name, '');
+
+const bundle = await dir.getDirectoryHandle('bundle');
+const temp = await dir.getDirectoryHandle('tmp');
+
+strictEqual(bundle.name, 'bundle');
+strictEqual(temp.name, 'tmp');
+
+let count = 0;
+for await (const node of dir) {
+  ok(node.value instanceof FileSystemDirectoryHandle);
+  count++;
+}
+strictEqual(count, 3);
+
+const names = await Array.fromAsync(dir.keys());
+deepStrictEqual(names, ['bundle', 'tmp', 'dev']);
+
+export const webfsTest = {
+  async test() {
+    const file = await temp.getFileHandle('foo.txt', { create: true });
+    ok(file instanceof FileSystemFileHandle);
+    const writable = await file.createWritable();
+    ok(writable instanceof FileSystemWritableFileStream);
+    const enc = new TextEncoder();
+    await writable.write(enc.encode('hello world'));
+
+    // The original file contents remain unchanged until the writable is closed.
+    let f = await file.getFile();
+    strictEqual(await f.text(), '');
+    await writable.close();
+
+    // Once closed, however, the file contents are updated.
+    f = await file.getFile();
+    strictEqual(await f.text(), 'hello world');
+
+    const file2 = await temp.getFileHandle('foo.txt');
+    ok(file2 instanceof FileSystemFileHandle);
+    const fileStream = await file2.getFile();
+    ok(fileStream instanceof File);
+    const text = await fileStream.text();
+    strictEqual(text, 'hello world');
+
+    await rejects(temp.getFileHandle('does-not-exist.txt', { create: false }), {
+      message: 'File not found',
+    });
+  },
+};
+
+export const deviceTest = {
+  async test() {
+    // Test /dev/null
+    const dev = await dir.getDirectoryHandle('dev');
+    const devNull = await dev.getFileHandle('null');
+    ok(devNull instanceof FileSystemFileHandle);
+    const sync = await devNull.createSyncAccessHandle();
+    const enc = new TextEncoder();
+    strictEqual(sync.getSize(), 0);
+    sync.write(enc.encode('hello world'));
+    strictEqual(sync.getSize(), 0);
+    strictEqual(sync.read(new Uint8Array(11)), 0);
+
+    // Test /dev/zero
+    const devZero = await dev.getFileHandle('zero');
+    ok(devZero instanceof FileSystemFileHandle);
+    const syncZero = await devZero.createSyncAccessHandle();
+    strictEqual(syncZero.getSize(), 0);
+    syncZero.write(enc.encode('hello world'));
+    strictEqual(syncZero.getSize(), 0);
+    const u8 = new Buffer.from([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]);
+    const zeroes = Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
+    strictEqual(syncZero.read(u8), u8.byteLength);
+    deepStrictEqual(u8, zeroes);
+
+    // Test /dev/full
+    const devFull = await dev.getFileHandle('full');
+    ok(devFull instanceof FileSystemFileHandle);
+    const syncFull = await devFull.createSyncAccessHandle();
+    strictEqual(syncFull.getSize(), 0);
+    throws(() => syncFull.write(enc.encode('hello world')), {
+      message: 'Cannot write to /dev/full',
+    });
+    strictEqual(syncFull.getSize(), 0);
+    const u8Full = new Buffer.from([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]);
+    strictEqual(syncFull.read(u8Full), 11);
+    deepStrictEqual(u8Full, zeroes);
+
+    // Test /dev/random
+    const devRandom = await dev.getFileHandle('random');
+    ok(devRandom instanceof FileSystemFileHandle);
+    const syncRandom = await devRandom.createSyncAccessHandle();
+    strictEqual(syncRandom.getSize(), 0);
+    throws(() => syncRandom.write(enc.encode('hello world')), {
+      message: 'Cannot write to /dev/random',
+    });
+    strictEqual(syncRandom.getSize(), 0);
+    const u8Random = new Buffer.from([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]);
+    const check = new Buffer.from([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]);
+    strictEqual(syncRandom.read(u8Random), 11);
+    notDeepStrictEqual(u8Random, check);
+  },
+};
+
+export const asyncIteratorTest = {
+  async test() {
+    const dir = await navigator.storage.getDirectory();
+
+    let count = 0;
+    for await (const node of dir) {
+      ok(node.value instanceof FileSystemDirectoryHandle);
+      count++;
+    }
+    strictEqual(count, 3);
+
+    const names = await Array.fromAsync(dir.keys());
+    deepStrictEqual(names, ['bundle', 'tmp', 'dev']);
+  },
+};
diff --git a/src/workerd/api/tests/webfs-test.wd-test b/src/workerd/api/tests/webfs-test.wd-test
new file mode 100644
index 00000000000..97ddc5e6230
--- /dev/null
+++ b/src/workerd/api/tests/webfs-test.wd-test
@@ -0,0 +1,19 @@
+using Workerd = import "/workerd/workerd.capnp";
+
+const unitTests :Workerd.Config = (
+  services = [
+    ( name = "webfs-test",
+      worker = (
+        modules = [
+          (name = "worker", esModule = embed "webfs-test.js")
+        ],
+        compatibilityDate = "2025-05-01",
+        compatibilityFlags = [
+          "nodejs_compat",
+          "enable_web_file_system",
+          "experimental",
+        ]
+      )
+    ),
+  ],
+);
diff --git a/src/workerd/io/compatibility-date.capnp b/src/workerd/io/compatibility-date.capnp
index 13221439948..ed2d4f22e07 100644
--- a/src/workerd/io/compatibility-date.capnp
+++ b/src/workerd/io/compatibility-date.capnp
@@ -749,4 +749,10 @@ struct CompatibilityFlags @0x8f8c1b68151b6cef {
       $compatEnableDate("2025-05-19")
       $compatDisableFlag("disable_navigator_language");
   # Enables Navigator.language API.
+
+  webFileSystem @87 :Bool
+      $compatEnableFlag("enable_web_file_system")
+      $experimental;
+  # Enables the experimental Web File System API.
+  # WARNING: This API is still in development and may change or be removed in the future.
 }
diff --git a/src/workerd/io/worker-fs.c++ b/src/workerd/io/worker-fs.c++
index 8e26cfb3b6e..b800ba99270 100644
--- a/src/workerd/io/worker-fs.c++
+++ b/src/workerd/io/worker-fs.c++
@@ -585,13 +585,13 @@ class FileImpl final: public File {
   Stat stat(jsg::Lock& js) override {
     return Stat{
       .type = FsType::FILE,
-      .size = readableView().size(),
+      .size = static_cast<uint32_t>(readableView().size()),
       .lastModified = lastModified,
       .writable = isWritable(),
     };
   }
 
-  size_t read(jsg::Lock& js, size_t offset, kj::ArrayPtr<kj::byte> buffer) const override {
+  uint32_t read(jsg::Lock& js, uint32_t offset, kj::ArrayPtr<kj::byte> buffer) const override {
     auto data = readableView();
     if (offset >= data.size() || buffer.size() == 0) return 0;
     auto src = data.slice(offset);
@@ -605,7 +605,9 @@ class FileImpl final: public File {
   }
 
   // Writes data to the file at the given offset.
-  size_t write(jsg::Lock& js, size_t offset, kj::ArrayPtr<const kj::byte> buffer) override {
+  uint32_t write(jsg::Lock& js, uint32_t offset, kj::ArrayPtr<const kj::byte> buffer) override {
+    static constexpr uint32_t kMax = kj::maxValue;
+    JSG_REQUIRE(buffer.size() <= kMax, Error, "File size exceeds maximum limit");
     auto& owned = writableView();
     size_t end = offset + buffer.size();
     if (end > owned.size()) resize(js, end);
@@ -613,7 +615,7 @@ class FileImpl final: public File {
     return buffer.size();
   }
 
-  void resize(jsg::Lock& js, size_t size) override {
+  void resize(jsg::Lock& js, uint32_t size) override {
     auto& owned = writableView();
     if (size == owned.size()) return;  // Nothing to do.
 
@@ -630,7 +632,7 @@ class FileImpl final: public File {
     ownedOrView = newData.attach(js.getExternalMemoryAdjustment(newData.size()));
   }
 
-  void fill(jsg::Lock& js, kj::byte value, kj::Maybe<size_t> offset) override {
+  void fill(jsg::Lock& js, kj::byte value, kj::Maybe<uint32_t> offset) override {
     auto& owned = JSG_REQUIRE_NONNULL(
         ownedOrView.tryGet<kj::Array<kj::byte>>(), Error, "Cannot modify a read-only file");
     owned.slice(offset.orDefault(0)).fill(value);
@@ -657,6 +659,30 @@ class FileImpl final: public File {
     }
   }
 
+  kj::Rc<File> clone(jsg::Lock&) override {
+    KJ_SWITCH_ONEOF(ownedOrView) {
+      KJ_CASE_ONEOF(owned, kj::Array<kj::byte>) {
+        return kj::rc<FileImpl>(kj::heapArray<kj::byte>(owned));
+      }
+      KJ_CASE_ONEOF(view, kj::ArrayPtr<const kj::byte>) {
+        return kj::rc<FileImpl>(kj::heapArray<kj::byte>(view));
+      }
+    }
+    KJ_UNREACHABLE;
+  }
+
+  void replace(jsg::Lock& js, kj::Rc<File> file) override {
+    JSG_REQUIRE_NONNULL(
+        ownedOrView.tryGet<kj::Array<kj::byte>>(), Error, "Cannot replace a read-only file");
+
+    auto stat = file->stat(js);
+    auto buffer =
+        kj::heapArray<kj::byte>(stat.size).attach(js.getExternalMemoryAdjustment(stat.size));
+    file->read(js, 0, buffer.asPtr());
+    ownedOrView = kj::mv(buffer);
+    lastModified = stat.lastModified;
+  }
+
  private:
   kj::OneOf<kj::Array<kj::byte>, kj::ArrayPtr<const kj::byte>> ownedOrView;
   kj::Date lastModified;
@@ -909,7 +935,7 @@ kj::Rc<Directory> Directory::newWritable() {
   return kj::rc<WritableDirectory>();
 }
 
-kj::Rc<File> File::newWritable(jsg::Lock& js, kj::Maybe<size_t> size) {
+kj::Rc<File> File::newWritable(jsg::Lock& js, kj::Maybe<uint32_t> size) {
   auto actualSize = size.orDefault(0);
   auto data = kj::heapArray<kj::byte>(actualSize);
   if (actualSize > 0) data.asPtr().fill(0);
@@ -1079,7 +1105,7 @@ namespace {
 
 // /dev/null is a special file that discards all data written to it and returns
 // EOF on reads.
-class DevNullFile final: public File {
+class DevNullFile final: public File, public kj::EnableAddRefToThis<DevNullFile> {
  public:
   DevNullFile() = default;
 
@@ -1093,15 +1119,23 @@ class DevNullFile final: public File {
     };
   }
 
+  kj::Rc<File> clone(jsg::Lock&) override {
+    return addRefToThis();
+  }
+
+  void replace(jsg::Lock& js, kj::Rc<File> file) override {
+    // No-op.
+  }
+
   void setLastModified(jsg::Lock& js, kj::Date date = kj::UNIX_EPOCH) override {
     // No-op.
   }
 
-  void fill(jsg::Lock& js, kj::byte value, kj::Maybe<size_t> offset) override {
+  void fill(jsg::Lock& js, kj::byte value, kj::Maybe<uint32_t> offset) override {
     // No-op.
   }
 
-  void resize(jsg::Lock& js, size_t size) override {
+  void resize(jsg::Lock& js, uint32_t size) override {
     // No-op.
   }
 
@@ -1117,18 +1151,18 @@ class DevNullFile final: public File {
     // No-op.
   }
 
-  size_t read(jsg::Lock& js, size_t offset, kj::ArrayPtr<kj::byte> buffer) const override {
+  uint32_t read(jsg::Lock& js, uint32_t offset, kj::ArrayPtr<kj::byte> buffer) const override {
     return 0;
   }
 
-  size_t write(jsg::Lock& js, size_t offset, kj::ArrayPtr<const kj::byte> buffer) override {
+  uint32_t write(jsg::Lock& js, uint32_t offset, kj::ArrayPtr<const kj::byte> buffer) override {
     return buffer.size();
   }
 };
 
 // /dev/zero is a special file that returns zeroes when read from and
 // ignores writes.
-class DevZeroFile final: public File {
+class DevZeroFile final: public File, public kj::EnableAddRefToThis<DevZeroFile> {
  public:
   DevZeroFile() = default;
 
@@ -1142,15 +1176,23 @@ class DevZeroFile final: public File {
     };
   }
 
+  kj::Rc<File> clone(jsg::Lock&) override {
+    return addRefToThis();
+  }
+
+  void replace(jsg::Lock& js, kj::Rc<File> file) override {
+    // No-op.
+  }
+
   void setLastModified(jsg::Lock& js, kj::Date date = kj::UNIX_EPOCH) override {
     // No-op.
   }
 
-  void fill(jsg::Lock& js, kj::byte value, kj::Maybe<size_t> offset) override {
+  void fill(jsg::Lock& js, kj::byte value, kj::Maybe<uint32_t> offset) override {
     // No-op.
   }
 
-  void resize(jsg::Lock& js, size_t size) override {
+  void resize(jsg::Lock& js, uint32_t size) override {
     // No-op.
   }
 
@@ -1166,19 +1208,19 @@ class DevZeroFile final: public File {
     // No-op.
   }
 
-  size_t read(jsg::Lock& js, size_t offset, kj::ArrayPtr<kj::byte> buffer) const override {
+  uint32_t read(jsg::Lock& js, uint32_t offset, kj::ArrayPtr<kj::byte> buffer) const override {
     buffer.fill(0);
     return buffer.size();
   }
 
-  size_t write(jsg::Lock& js, size_t offset, kj::ArrayPtr<const kj::byte> buffer) override {
+  uint32_t write(jsg::Lock& js, uint32_t offset, kj::ArrayPtr<const kj::byte> buffer) override {
     return buffer.size();
   }
 };
 
 // /dev/full is a special file that returns zeroes when read from and
 // returns an error when written to.
-class DevFullFile final: public File {
+class DevFullFile final: public File, public kj::EnableAddRefToThis<DevFullFile> {
  public:
   DevFullFile() = default;
 
@@ -1192,15 +1234,23 @@ class DevFullFile final: public File {
     };
   }
 
+  kj::Rc<File> clone(jsg::Lock&) override {
+    return addRefToThis();
+  }
+
+  void replace(jsg::Lock& js, kj::Rc<File> file) override {
+    JSG_FAIL_REQUIRE(Error, "Cannot replace /dev/full");
+  }
+
   void setLastModified(jsg::Lock& js, kj::Date date = kj::UNIX_EPOCH) override {
     // No-op.
   }
 
-  void fill(jsg::Lock& js, kj::byte value, kj::Maybe<size_t> offset) override {
+  void fill(jsg::Lock& js, kj::byte value, kj::Maybe<uint32_t> offset) override {
     JSG_FAIL_REQUIRE(Error, "Cannot write to /dev/full");
   }
 
-  void resize(jsg::Lock& js, size_t size) override {
+  void resize(jsg::Lock& js, uint32_t size) override {
     JSG_FAIL_REQUIRE(Error, "Cannot write to /dev/full");
   }
 
@@ -1216,17 +1266,17 @@ class DevFullFile final: public File {
     // No-op.
   }
 
-  size_t read(jsg::Lock& js, size_t offset, kj::ArrayPtr<kj::byte> buffer) const override {
+  uint32_t read(jsg::Lock& js, uint32_t offset, kj::ArrayPtr<kj::byte> buffer) const override {
     buffer.fill(0);
     return buffer.size();
   }
 
-  size_t write(jsg::Lock& js, size_t offset, kj::ArrayPtr<const kj::byte> buffer) override {
+  uint32_t write(jsg::Lock& js, uint32_t offset, kj::ArrayPtr<const kj::byte> buffer) override {
     JSG_FAIL_REQUIRE(Error, "Cannot write to /dev/full");
   }
 };
 
-class DevRandomFile final: public File {
+class DevRandomFile final: public File, public kj::EnableAddRefToThis<DevRandomFile> {
  public:
   DevRandomFile() = default;
 
@@ -1240,15 +1290,23 @@ class DevRandomFile final: public File {
     };
   }
 
+  kj::Rc<File> clone(jsg::Lock&) override {
+    return addRefToThis();
+  }
+
+  void replace(jsg::Lock& js, kj::Rc<File> file) override {
+    JSG_FAIL_REQUIRE(Error, "Cannot replace /dev/random");
+  }
+
   void setLastModified(jsg::Lock& js, kj::Date date = kj::UNIX_EPOCH) override {
     JSG_FAIL_REQUIRE(Error, "Cannot write to /dev/random");
   }
 
-  void fill(jsg::Lock& js, kj::byte value, kj::Maybe<size_t> offset) override {
+  void fill(jsg::Lock& js, kj::byte value, kj::Maybe<uint32_t> offset) override {
     JSG_FAIL_REQUIRE(Error, "Cannot write to /dev/random");
   }
 
-  void resize(jsg::Lock& js, size_t size) override {
+  void resize(jsg::Lock& js, uint32_t size) override {
     JSG_FAIL_REQUIRE(Error, "Cannot write to /dev/random");
   }
 
@@ -1264,11 +1322,11 @@ class DevRandomFile final: public File {
     // No-op.
   }
 
-  size_t write(jsg::Lock& js, size_t offset, kj::ArrayPtr<const kj::byte> buffer) override {
+  uint32_t write(jsg::Lock& js, uint32_t offset, kj::ArrayPtr<const kj::byte> buffer) override {
     JSG_FAIL_REQUIRE(Error, "Cannot write to /dev/random");
   }
 
-  size_t read(jsg::Lock& js, size_t offset, kj::ArrayPtr<kj::byte> buffer) const override {
+  uint32_t read(jsg::Lock& js, uint32_t offset, kj::ArrayPtr<kj::byte> buffer) const override {
     // We can only generate random bytes when we have an active IoContext.
     // If there is no IoContext, this will return 0 bytes.
     if (!IoContext::hasCurrent()) return 0;
diff --git a/src/workerd/io/worker-fs.h b/src/workerd/io/worker-fs.h
index 0536a3413d5..acbb6adcf6a 100644
--- a/src/workerd/io/worker-fs.h
+++ b/src/workerd/io/worker-fs.h
@@ -142,8 +142,11 @@ struct Stat final {
   FsType type = FsType::FILE;
 
   // The size of the node in bytes. For directories, this value will
-  // always be 0.
-  size_t size = 0;
+  // always be 0. Note that we are intentionally limiting the size of
+  // files to max(uint32_t) or 4GB. This is well above the maximum
+  // isolate heap limit so in-memory files should generally never
+  // get this large.
+  uint32_t size = 0;
 
   // The last modified time of the node.
   kj::Date lastModified = kj::UNIX_EPOCH;
@@ -182,18 +185,18 @@ class File: public kj::Refcounted {
   jsg::BufferSource readAllBytes(jsg::Lock& js);
 
   // Reads data from the file at the given offset into the given buffer.
-  virtual size_t read(jsg::Lock& js, size_t offset, kj::ArrayPtr<kj::byte> buffer) const = 0;
+  virtual uint32_t read(jsg::Lock& js, uint32_t offset, kj::ArrayPtr<kj::byte> buffer) const = 0;
 
   // Replaces the full contents of the file with the given data.
   // Equivalent to resize(js, data.size()) followed by write(js, 0, data).
-  size_t writeAll(jsg::Lock& js, kj::ArrayPtr<const kj::byte> data) {
+  uint32_t writeAll(jsg::Lock& js, kj::ArrayPtr<const kj::byte> data) {
     resize(js, data.size());
     return write(js, 0, data);
   }
 
   // Replaces the full contents of the file with the given data.
   // Equivalent to resize(js, data.size()) followed by write(js, 0, data).
-  size_t writeAll(jsg::Lock& js, kj::StringPtr data) {
+  uint32_t writeAll(jsg::Lock& js, kj::StringPtr data) {
     return writeAll(js, data.asBytes());
   }
 
@@ -202,22 +205,22 @@ class File: public kj::Refcounted {
   // this will throw an exception. If the offset is greater than the current
   // size of the file, the file may be resized to accommodate the new data or
   // an exception may be thrown (depending on the underlying implementation).
-  virtual size_t write(jsg::Lock& js, size_t offset, kj::ArrayPtr<const kj::byte> data) = 0;
+  virtual uint32_t write(jsg::Lock& js, uint32_t offset, kj::ArrayPtr<const kj::byte> data) = 0;
 
-  size_t write(jsg::Lock& js, size_t offset, kj::StringPtr data) {
+  uint32_t write(jsg::Lock& js, uint32_t offset, kj::StringPtr data) {
     return write(js, offset, data.asBytes());
   }
 
   // Fill the file with the given value from the given offset. This is more
   // efficient that writing the same value as it does not require any allocations.
-  virtual void fill(jsg::Lock& js, kj::byte val, kj::Maybe<size_t> offset = kj::none) = 0;
+  virtual void fill(jsg::Lock& js, kj::byte val, kj::Maybe<uint32_t> offset = kj::none) = 0;
 
   // Resize the file allocation. Note that this is potentially an expensive
   // operation as it requires allocating a new internal buffer and copying
   // the data. If the size is smaller than the current size, the contents of
   // the fill will be truncated. If the size is larger than the current size,
   // the new contents of the file will be filled with zeroes.
-  virtual void resize(jsg::Lock& js, size_t size) = 0;
+  virtual void resize(jsg::Lock& js, uint32_t size) = 0;
 
   // Creates a new readable/writable in-memory file. This file will not be
   // initially included in a directory. To add it to a directory, use the
@@ -231,7 +234,7 @@ class File: public kj::Refcounted {
   // or to resize the file as appropriate to account for the expected writes.
   // This will avoid each individual write causing a reallocation of the
   // internal buffer to accommodate the new data.
-  static kj::Rc<File> newWritable(jsg::Lock& js, kj::Maybe<size_t> size = kj::none);
+  static kj::Rc<File> newWritable(jsg::Lock& js, kj::Maybe<uint32_t> size = kj::none);
 
   // Creates a new readable in-memory file wrapping the given data. The file
   // does not take ownership of the data and the data must remain valid for the
@@ -243,6 +246,13 @@ class File: public kj::Refcounted {
   virtual kj::StringPtr jsgGetMemoryName() const = 0;
   virtual size_t jsgGetMemorySelfSize() const = 0;
   virtual void jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const = 0;
+
+  // Creates a copy of this file.
+  virtual kj::Rc<File> clone(jsg::Lock& js) = 0;
+
+  // Replaces the contents of this file with the given file if possible.
+  // If this file is read-only, an exception will be thrown.
+  virtual void replace(jsg::Lock& js, kj::Rc<File> file) = 0;
 };
 
 // A directory in the virtual file system. If the directory is read-only,
@@ -494,7 +504,7 @@ class VirtualFileSystem {
     // When reading from or writing to the file, if an offset is not
     // explicitly given then the offset will be set to the current
     // position in the file.
-    size_t position = 0;
+    uint32_t position = 0;
   };
 
   // Attempts to open a file descriptor for the given file URL. It's critical
diff --git a/src/workerd/jsg/iterator.h b/src/workerd/jsg/iterator.h
index c3b2481af1e..466a17915ed 100644
--- a/src/workerd/jsg/iterator.h
+++ b/src/workerd/jsg/iterator.h
@@ -847,7 +847,7 @@ template <typename SelfType, typename Type, typename State>
 class AsyncIteratorBase: public Object {
  public:
   using NextSignature = Promise<kj::Maybe<Type>>(Lock&, State&);
-  using ReturnSignature = Promise<void>(Lock&, State&, Optional<Value>);
+  using ReturnSignature = Promise<void>(Lock&, State&, Optional<Type>&);
   using Next = AsyncIteratorImpl::Next<Type>;
   using Finished = AsyncIteratorImpl::Finished;
 
@@ -959,7 +959,7 @@ class AsyncIteratorBase: public Object {
     KJ_UNREACHABLE;
   }
 
-  Promise<Next> returnImpl(Lock& js, Optional<Value> value, ReturnSignature returnFunc) {
+  Promise<Next> returnImpl(Lock& js, Optional<Type> value, ReturnSignature returnFunc) {
     KJ_SWITCH_ONEOF(state) {
       KJ_CASE_ONEOF(finished, Finished) {
         return js.resolvedPromise(Next{.done = true, .value = kj::mv(value)});
@@ -983,9 +983,8 @@ class AsyncIteratorBase: public Object {
               return js.resolvedPromise(Next{.done = true, .value = kj::mv(value)});
             }
             KJ_CASE_ONEOF(inner, InnerState) {
-              return returnFunc(js, inner.state, value.map([&](Value& v) {
-                return v.addRef(js.v8Isolate);
-              })).then(js, [this, self = kj::mv(self), value = kj::mv(value)](Lock& js) mutable {
+              return returnFunc(js, inner.state, value)
+                  .then(js, [this, self = kj::mv(self), value = kj::mv(value)](Lock& js) mutable {
                 state.template init<Finished>();
                 return js.resolvedPromise(Next{.done = true, .value = kj::mv(value)});
               });
@@ -1075,7 +1074,7 @@ class AsyncIteratorBase: public Object {
     inline jsg::Promise<Next> next(jsg::Lock& js) {                                                \
       return nextImpl(js, NextFunc);                                                               \
     }                                                                                              \
-    inline jsg::Promise<Next> return_(jsg::Lock& js, jsg::Optional<jsg::Value> value) {            \
+    inline jsg::Promise<Next> return_(jsg::Lock& js, jsg::Optional<Type> value) {                  \
       return returnImpl(js, kj::mv(value), ReturnFunc);                                            \
     }                                                                                              \
     JSG_RESOURCE_TYPE(Name) {                                                                      \
diff --git a/src/workerd/server/workerd-api.c++ b/src/workerd/server/workerd-api.c++
index 5eec0dd4598..8ebdd73283e 100644
--- a/src/workerd/server/workerd-api.c++
+++ b/src/workerd/server/workerd-api.c++
@@ -16,6 +16,7 @@
 #include <workerd/api/encoding.h>
 #include <workerd/api/events.h>
 #include <workerd/api/eventsource.h>
+#include <workerd/api/filesystem.h>
 #include <workerd/api/global-scope.h>
 #include <workerd/api/html-rewriter.h>
 #include <workerd/api/hyperdrive.h>
@@ -106,6 +107,7 @@ JSG_DECLARE_ISOLATE_TYPE(JsgWorkerdIsolate,
     EW_URL_STANDARD_ISOLATE_TYPES,
     EW_URLPATTERN_ISOLATE_TYPES,
     EW_URLPATTERN_STANDARD_ISOLATE_TYPES,
+    EW_WEB_FILESYSTEM_ISOLATE_TYPE,
     EW_WEBSOCKET_ISOLATE_TYPES,
     EW_SQL_ISOLATE_TYPES,
     EW_NODE_ISOLATE_TYPES,
diff --git a/types/generated-snapshot/experimental/index.d.ts b/types/generated-snapshot/experimental/index.d.ts
index cae899a99cb..1e773801a10 100755
--- a/types/generated-snapshot/experimental/index.d.ts
+++ b/types/generated-snapshot/experimental/index.d.ts
@@ -257,6 +257,12 @@ interface ServiceWorkerGlobalScope extends WorkerGlobalScope {
   ByteLengthQueuingStrategy: typeof ByteLengthQueuingStrategy;
   CountQueuingStrategy: typeof CountQueuingStrategy;
   ErrorEvent: typeof ErrorEvent;
+  FileSystemHandle: typeof FileSystemHandle;
+  FileSystemFileHandle: typeof FileSystemFileHandle;
+  FileSystemDirectoryHandle: typeof FileSystemDirectoryHandle;
+  FileSystemWritableFileStream: typeof FileSystemWritableFileStream;
+  FileSystemSyncAccessHandle: typeof FileSystemSyncAccessHandle;
+  StorageManager: typeof StorageManager;
   EventSource: typeof EventSource;
   ReadableStreamBYOBRequest: typeof ReadableStreamBYOBRequest;
   ReadableStreamDefaultController: typeof ReadableStreamDefaultController;
@@ -469,6 +475,7 @@ declare abstract class Navigator {
   readonly hardwareConcurrency: number;
   readonly language: string;
   readonly languages: string[];
+  readonly storage: StorageManager;
 }
 /**
  * The Workers runtime supports a subset of the Performance API, used to measure timing and performance,
@@ -3099,6 +3106,148 @@ interface ContainerStartupOptions {
   enableInternet: boolean;
   env?: Record<string, string>;
 }
+/**
+ * Available only in secure contexts.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemHandle)
+ */
+declare abstract class FileSystemHandle {
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemHandle/kind) */
+  get kind(): string;
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemHandle/name) */
+  get name(): string;
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemHandle/isSameEntry) */
+  isSameEntry(other: FileSystemHandle): Promise<boolean>;
+}
+/**
+ * Available only in secure contexts.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemFileHandle)
+ */
+declare abstract class FileSystemFileHandle extends FileSystemHandle {
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemFileHandle/getFile) */
+  getFile(): Promise<File>;
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemFileHandle/createWritable) */
+  createWritable(
+    options?: FileSystemFileHandleFileSystemCreateWritableOptions,
+  ): Promise<FileSystemWritableFileStream>;
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemFileHandle/createSyncAccessHandle) */
+  createSyncAccessHandle(): Promise<FileSystemSyncAccessHandle>;
+}
+/**
+ * Available only in secure contexts.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemDirectoryHandle)
+ */
+declare abstract class FileSystemDirectoryHandle extends FileSystemHandle {
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemDirectoryHandle/getFileHandle) */
+  getFileHandle(
+    name: string,
+    options?: FileSystemDirectoryHandleFileSystemGetFileOptions,
+  ): Promise<FileSystemFileHandle>;
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemDirectoryHandle/getDirectoryHandle) */
+  getDirectoryHandle(
+    name: string,
+    options?: FileSystemDirectoryHandleFileSystemGetDirectoryOptions,
+  ): Promise<FileSystemDirectoryHandle>;
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemDirectoryHandle/removeEntry) */
+  removeEntry(
+    name: string,
+    options?: FileSystemDirectoryHandleFileSystemRemoveOptions,
+  ): Promise<void>;
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemDirectoryHandle/resolve) */
+  resolve(possibleDescendant: FileSystemHandle): Promise<string[]>;
+  entries(): AsyncIterableIterator<FileSystemDirectoryHandleEntryType>;
+  keys(): AsyncIterableIterator<string>;
+  values(): AsyncIterableIterator<FileSystemHandle>;
+  forEach(
+    callback: (
+      param0: string,
+      param1: FileSystemHandle,
+      param2: FileSystemDirectoryHandle,
+    ) => void,
+    thisArg?: any,
+  ): void;
+  [Symbol.asyncIterator](): AsyncIterableIterator<FileSystemDirectoryHandleEntryType>;
+}
+/**
+ * Available only in secure contexts.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemWritableFileStream)
+ */
+declare abstract class FileSystemWritableFileStream extends WritableStream {
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemWritableFileStream/write) */
+  write(
+    data:
+      | Blob
+      | (ArrayBuffer | ArrayBufferView)
+      | string
+      | FileSystemWritableFileStreamWriteParams,
+  ): Promise<void>;
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemWritableFileStream/seek) */
+  seek(position: number): Promise<void>;
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemWritableFileStream/truncate) */
+  truncate(size: number): Promise<void>;
+}
+/**
+ * Available only in secure contexts.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemSyncAccessHandle)
+ */
+declare abstract class FileSystemSyncAccessHandle {
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemSyncAccessHandle/read) */
+  read(
+    buffer: ArrayBuffer | ArrayBufferView,
+    options?: FileSystemSyncAccessHandleFileSystemReadWriteOptions,
+  ): number;
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemSyncAccessHandle/write) */
+  write(
+    buffer: ArrayBuffer | ArrayBufferView,
+    options?: FileSystemSyncAccessHandleFileSystemReadWriteOptions,
+  ): number;
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemSyncAccessHandle/truncate) */
+  truncate(newSize: number): void;
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemSyncAccessHandle/getSize) */
+  getSize(): number;
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemSyncAccessHandle/flush) */
+  flush(): void;
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemSyncAccessHandle/close) */
+  close(): void;
+}
+/**
+ * Available only in secure contexts.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/StorageManager)
+ */
+declare abstract class StorageManager {
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/StorageManager/getDirectory) */
+  getDirectory(): Promise<FileSystemDirectoryHandle>;
+}
+interface FileSystemFileHandleFileSystemCreateWritableOptions {
+  keepExistingData?: boolean;
+}
+interface FileSystemDirectoryHandleFileSystemGetFileOptions {
+  create: boolean;
+}
+interface FileSystemDirectoryHandleFileSystemGetDirectoryOptions {
+  create: boolean;
+}
+interface FileSystemDirectoryHandleFileSystemRemoveOptions {
+  recursive: boolean;
+}
+interface FileSystemSyncAccessHandleFileSystemReadWriteOptions {
+  at?: number;
+}
+interface FileSystemWritableFileStreamWriteParams {
+  type: string;
+  size?: number;
+  position?: number;
+  data?: Blob | (ArrayBuffer | ArrayBufferView) | string;
+}
+interface FileSystemDirectoryHandleEntryType {
+  key: string;
+  value: FileSystemHandle;
+}
 type AiImageClassificationInput = {
   image: number[];
 };
diff --git a/types/generated-snapshot/experimental/index.ts b/types/generated-snapshot/experimental/index.ts
index 13526094f6f..31add429a83 100755
--- a/types/generated-snapshot/experimental/index.ts
+++ b/types/generated-snapshot/experimental/index.ts
@@ -257,6 +257,12 @@ export interface ServiceWorkerGlobalScope extends WorkerGlobalScope {
   ByteLengthQueuingStrategy: typeof ByteLengthQueuingStrategy;
   CountQueuingStrategy: typeof CountQueuingStrategy;
   ErrorEvent: typeof ErrorEvent;
+  FileSystemHandle: typeof FileSystemHandle;
+  FileSystemFileHandle: typeof FileSystemFileHandle;
+  FileSystemDirectoryHandle: typeof FileSystemDirectoryHandle;
+  FileSystemWritableFileStream: typeof FileSystemWritableFileStream;
+  FileSystemSyncAccessHandle: typeof FileSystemSyncAccessHandle;
+  StorageManager: typeof StorageManager;
   EventSource: typeof EventSource;
   ReadableStreamBYOBRequest: typeof ReadableStreamBYOBRequest;
   ReadableStreamDefaultController: typeof ReadableStreamDefaultController;
@@ -474,6 +480,7 @@ export declare abstract class Navigator {
   readonly hardwareConcurrency: number;
   readonly language: string;
   readonly languages: string[];
+  readonly storage: StorageManager;
 }
 /**
  * The Workers runtime supports a subset of the Performance API, used to measure timing and performance,
@@ -3111,6 +3118,148 @@ export interface ContainerStartupOptions {
   enableInternet: boolean;
   env?: Record<string, string>;
 }
+/**
+ * Available only in secure contexts.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemHandle)
+ */
+export declare abstract class FileSystemHandle {
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemHandle/kind) */
+  get kind(): string;
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemHandle/name) */
+  get name(): string;
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemHandle/isSameEntry) */
+  isSameEntry(other: FileSystemHandle): Promise<boolean>;
+}
+/**
+ * Available only in secure contexts.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemFileHandle)
+ */
+export declare abstract class FileSystemFileHandle extends FileSystemHandle {
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemFileHandle/getFile) */
+  getFile(): Promise<File>;
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemFileHandle/createWritable) */
+  createWritable(
+    options?: FileSystemFileHandleFileSystemCreateWritableOptions,
+  ): Promise<FileSystemWritableFileStream>;
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemFileHandle/createSyncAccessHandle) */
+  createSyncAccessHandle(): Promise<FileSystemSyncAccessHandle>;
+}
+/**
+ * Available only in secure contexts.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemDirectoryHandle)
+ */
+export declare abstract class FileSystemDirectoryHandle extends FileSystemHandle {
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemDirectoryHandle/getFileHandle) */
+  getFileHandle(
+    name: string,
+    options?: FileSystemDirectoryHandleFileSystemGetFileOptions,
+  ): Promise<FileSystemFileHandle>;
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemDirectoryHandle/getDirectoryHandle) */
+  getDirectoryHandle(
+    name: string,
+    options?: FileSystemDirectoryHandleFileSystemGetDirectoryOptions,
+  ): Promise<FileSystemDirectoryHandle>;
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemDirectoryHandle/removeEntry) */
+  removeEntry(
+    name: string,
+    options?: FileSystemDirectoryHandleFileSystemRemoveOptions,
+  ): Promise<void>;
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemDirectoryHandle/resolve) */
+  resolve(possibleDescendant: FileSystemHandle): Promise<string[]>;
+  entries(): AsyncIterableIterator<FileSystemDirectoryHandleEntryType>;
+  keys(): AsyncIterableIterator<string>;
+  values(): AsyncIterableIterator<FileSystemHandle>;
+  forEach(
+    callback: (
+      param0: string,
+      param1: FileSystemHandle,
+      param2: FileSystemDirectoryHandle,
+    ) => void,
+    thisArg?: any,
+  ): void;
+  [Symbol.asyncIterator](): AsyncIterableIterator<FileSystemDirectoryHandleEntryType>;
+}
+/**
+ * Available only in secure contexts.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemWritableFileStream)
+ */
+export declare abstract class FileSystemWritableFileStream extends WritableStream {
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemWritableFileStream/write) */
+  write(
+    data:
+      | Blob
+      | (ArrayBuffer | ArrayBufferView)
+      | string
+      | FileSystemWritableFileStreamWriteParams,
+  ): Promise<void>;
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemWritableFileStream/seek) */
+  seek(position: number): Promise<void>;
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemWritableFileStream/truncate) */
+  truncate(size: number): Promise<void>;
+}
+/**
+ * Available only in secure contexts.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemSyncAccessHandle)
+ */
+export declare abstract class FileSystemSyncAccessHandle {
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemSyncAccessHandle/read) */
+  read(
+    buffer: ArrayBuffer | ArrayBufferView,
+    options?: FileSystemSyncAccessHandleFileSystemReadWriteOptions,
+  ): number;
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemSyncAccessHandle/write) */
+  write(
+    buffer: ArrayBuffer | ArrayBufferView,
+    options?: FileSystemSyncAccessHandleFileSystemReadWriteOptions,
+  ): number;
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemSyncAccessHandle/truncate) */
+  truncate(newSize: number): void;
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemSyncAccessHandle/getSize) */
+  getSize(): number;
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemSyncAccessHandle/flush) */
+  flush(): void;
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FileSystemSyncAccessHandle/close) */
+  close(): void;
+}
+/**
+ * Available only in secure contexts.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/StorageManager)
+ */
+export declare abstract class StorageManager {
+  /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/StorageManager/getDirectory) */
+  getDirectory(): Promise<FileSystemDirectoryHandle>;
+}
+export interface FileSystemFileHandleFileSystemCreateWritableOptions {
+  keepExistingData?: boolean;
+}
+export interface FileSystemDirectoryHandleFileSystemGetFileOptions {
+  create: boolean;
+}
+export interface FileSystemDirectoryHandleFileSystemGetDirectoryOptions {
+  create: boolean;
+}
+export interface FileSystemDirectoryHandleFileSystemRemoveOptions {
+  recursive: boolean;
+}
+export interface FileSystemSyncAccessHandleFileSystemReadWriteOptions {
+  at?: number;
+}
+export interface FileSystemWritableFileStreamWriteParams {
+  type: string;
+  size?: number;
+  position?: number;
+  data?: Blob | (ArrayBuffer | ArrayBufferView) | string;
+}
+export interface FileSystemDirectoryHandleEntryType {
+  key: string;
+  value: FileSystemHandle;
+}
 export type AiImageClassificationInput = {
   image: number[];
 };


Reviews:
user: login: danlapid
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T00:04:15Z
user: login: jasnell
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T00:06:34Z
user: login: danlapid
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T00:08:27Z
user: login: jasnell
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T00:11:05Z
user: login: danlapid
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T00:28:19Z
user: login: jasnell
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T00:32:44Z
user: login: jasnell
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T00:35:49Z
user: login: jasnell
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T00:37:32Z
user: login: danlapid
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T00:38:17Z
user: login: anonrig
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T01:51:14Z
user: login: danlapid
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T09:38:11Z
user: login: jasnell
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T13:44:51Z
user: login: jasnell
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T14:15:30Z
user: login: anonrig
body: We can now implement URL.createObjectURL as well
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T21:05:11Z

Review comments:
diff_hunk: @@ -0,0 +1,333 @@
+#pragma once
+
+#include <workerd/api/streams/writable.h>
+#include <workerd/io/worker-fs.h>
+#include <workerd/jsg/iterator.h>
+#include <workerd/jsg/jsg.h>
+
+namespace workerd::api {
+
+class Blob;
+class File;
+
+// ======================================================================================
+// An implementation of the WHATWG Web File System API (https://fs.spec.whatwg.org/)
+// All of the classes in this part of the impl are defined by the spec.
+
+class FileSystemSyncAccessHandle;
+class FileSystemWritableFileStream;
+
+class FileSystemHandle: public jsg::Object {
+  // TODO(node-fs): The spec defines FileSystemHandle objects as being
+  // serializable, meaning that they should work with structured cloning.
+  // In workers, this would suggest also that they should work with jsrpc.
+  // We don't yet implement any form of serialization for these objects.
+  // Since each worker has its own file system, it might be a bit weird
+  // as the received file on the other side would not actually be in that
+  // destination worker's file system... that is, it would be an orphaned
+  // file handle. It would still likely be useful tho. As a follow up
+  // step, we will implement serialization/deserialization of these objects.
+ public:
+  FileSystemHandle(jsg::USVString name): name(kj::mv(name)) {}
+  jsg::USVString getName(jsg::Lock& js) {
+    return jsg::USVString(kj::str(name));
path: src/workerd/api/filesystem.h
user: login: danlapid
body: Why do we need the double conversion?
Isn't it enough to just return name?
created_at: 2025-05-09T00:04:15Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,333 @@
+#pragma once
+
+#include <workerd/api/streams/writable.h>
+#include <workerd/io/worker-fs.h>
+#include <workerd/jsg/iterator.h>
+#include <workerd/jsg/jsg.h>
+
+namespace workerd::api {
+
+class Blob;
+class File;
+
+// ======================================================================================
+// An implementation of the WHATWG Web File System API (https://fs.spec.whatwg.org/)
+// All of the classes in this part of the impl are defined by the spec.
+
+class FileSystemSyncAccessHandle;
+class FileSystemWritableFileStream;
+
+class FileSystemHandle: public jsg::Object {
+  // TODO(node-fs): The spec defines FileSystemHandle objects as being
+  // serializable, meaning that they should work with structured cloning.
+  // In workers, this would suggest also that they should work with jsrpc.
+  // We don't yet implement any form of serialization for these objects.
+  // Since each worker has its own file system, it might be a bit weird
+  // as the received file on the other side would not actually be in that
+  // destination worker's file system... that is, it would be an orphaned
+  // file handle. It would still likely be useful tho. As a follow up
+  // step, we will implement serialization/deserialization of these objects.
+ public:
+  FileSystemHandle(jsg::USVString name): name(kj::mv(name)) {}
+  jsg::USVString getName(jsg::Lock& js) {
+    return jsg::USVString(kj::str(name));
path: src/workerd/api/filesystem.h
user: login: jasnell
body: `jsg::USVString` does not have an equivalent to `kj::StringPtr` and the type mapping requires `jsg::USVString` for it to get written correctly. It's an annoyance at this point that I plan to get back around to simplifying but not in this PR.
created_at: 2025-05-09T00:06:34Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,333 @@
+#pragma once
+
+#include <workerd/api/streams/writable.h>
+#include <workerd/io/worker-fs.h>
+#include <workerd/jsg/iterator.h>
+#include <workerd/jsg/jsg.h>
+
+namespace workerd::api {
+
+class Blob;
+class File;
+
+// ======================================================================================
+// An implementation of the WHATWG Web File System API (https://fs.spec.whatwg.org/)
+// All of the classes in this part of the impl are defined by the spec.
+
+class FileSystemSyncAccessHandle;
+class FileSystemWritableFileStream;
+
+class FileSystemHandle: public jsg::Object {
+  // TODO(node-fs): The spec defines FileSystemHandle objects as being
+  // serializable, meaning that they should work with structured cloning.
+  // In workers, this would suggest also that they should work with jsrpc.
+  // We don't yet implement any form of serialization for these objects.
+  // Since each worker has its own file system, it might be a bit weird
+  // as the received file on the other side would not actually be in that
+  // destination worker's file system... that is, it would be an orphaned
+  // file handle. It would still likely be useful tho. As a follow up
+  // step, we will implement serialization/deserialization of these objects.
+ public:
+  FileSystemHandle(jsg::USVString name): name(kj::mv(name)) {}
+  jsg::USVString getName(jsg::Lock& js) {
+    return jsg::USVString(kj::str(name));
+  }
+
+  virtual kj::StringPtr getKind(jsg::Lock& js) {
+    // Implemented by subclasses.
+    KJ_UNIMPLEMENTED("getKind() not implemented");
+  }
+  virtual jsg::Promise<bool> isSameEntry(jsg::Lock& js, jsg::Ref<FileSystemHandle> other) {
+    // Implemented by subclasses.
+    KJ_UNIMPLEMENTED("isSameEntry() not implemented");
+  }
+
+  JSG_RESOURCE_TYPE(FileSystemHandle) {
+    JSG_READONLY_PROTOTYPE_PROPERTY(kind, getKind);
+    JSG_READONLY_PROTOTYPE_PROPERTY(name, getName);
+    JSG_METHOD(isSameEntry);
+  }
+
+ private:
+  jsg::USVString name;
+};
+
+class FileSystemFileHandle: public FileSystemHandle {
+ public:
+  FileSystemFileHandle(jsg::USVString name, kj::Rc<workerd::File> inner);
+
+  kj::StringPtr getKind(jsg::Lock& js) override {
+    return "file"_kj;
+  }
+
+  jsg::Promise<bool> isSameEntry(jsg::Lock& js, jsg::Ref<FileSystemHandle> other) override;
+
+  struct FileSystemCreateWritableOptions {
+    bool keepExistingData = false;
+    JSG_STRUCT(keepExistingData);
+  };
+
+  jsg::Promise<jsg::Ref<File>> getFile(jsg::Lock& js);
+
+  jsg::Promise<jsg::Ref<FileSystemWritableFileStream>> createWritable(
+      jsg::Lock& js, jsg::Optional<FileSystemCreateWritableOptions> options);
+
+  jsg::Promise<jsg::Ref<FileSystemSyncAccessHandle>> createSyncAccessHandle(jsg::Lock& js);
+
+  JSG_RESOURCE_TYPE(FileSystemFileHandle) {
+    JSG_INHERIT(FileSystemHandle);
+    JSG_METHOD(getFile);
+    JSG_METHOD(createWritable);
+    JSG_METHOD(createSyncAccessHandle);
+  }
+
+ private:
+  kj::Rc<workerd::File> inner;
+};
+
+class FileSystemDirectoryHandle final: public FileSystemHandle {
+  // TODO(node-fs): Per the spec, FileSystemDirectoryHandler objects are
+  // expected to be async iterables. Here we implement it as a sync iterable.
+  // The effect ends up being largely the same but it's obviously better to
+  // use the asyn iterable API instead. We should implement this before we
+  // ship this feature.
+ public:
+  struct EntryType {
+    jsg::USVString key;
+    jsg::Ref<FileSystemHandle> value;
+    JSG_STRUCT(key, value);
+    EntryType(jsg::USVString key, jsg::Ref<FileSystemHandle> value)
+        : key(kj::mv(key)),
+          value(kj::mv(value)) {}
+  };
+
+ private:
+  using EntryIteratorType = EntryType;
+  using KeyIteratorType = jsg::USVString;
+  using ValueIteratorType = jsg::Ref<FileSystemHandle>;
+
+  struct IteratorState final {
+    jsg::Ref<FileSystemDirectoryHandle> parent;
+    kj::Array<jsg::Ref<FileSystemHandle>> entries;
+    uint index = 0;
+
+    IteratorState(
+        jsg::Ref<FileSystemDirectoryHandle> parent, kj::Array<jsg::Ref<FileSystemHandle>> entries)
+        : parent(kj::mv(parent)),
+          entries(kj::mv(entries)) {}
+
+    void visitForGc(jsg::GcVisitor& visitor) {
+      visitor.visit(parent);
+      visitor.visitAll(entries);
+    }
+
+    JSG_MEMORY_INFO(IteratorState) {
+      tracker.trackField("parent", parent);
+      for (auto& entry: entries) {
+        tracker.trackField("entry", entry);
+      }
+    }
+  };
+
+ public:
+  FileSystemDirectoryHandle(jsg::USVString name, kj::Rc<workerd::Directory> inner);
+
+  kj::StringPtr getKind(jsg::Lock& js) override {
+    return "directory"_kj;
+  }
+
+  jsg::Promise<bool> isSameEntry(jsg::Lock& js, jsg::Ref<FileSystemHandle> other) override;
+
+  struct FileSystemGetFileOptions {
+    bool create = false;
+    JSG_STRUCT(create);
+  };
+
+  struct FileSystemGetDirectoryOptions {
+    bool create = false;
+    JSG_STRUCT(create);
+  };
+
+  struct FileSystemRemoveOptions {
+    bool recursive = false;
+    JSG_STRUCT(recursive);
+  };
+
+  jsg::Promise<jsg::Ref<FileSystemFileHandle>> getFileHandle(jsg::Lock& js,
+      jsg::USVString name,
+      jsg::Optional<FileSystemGetFileOptions> options,
+      const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception);
+
+  jsg::Promise<jsg::Ref<FileSystemDirectoryHandle>> getDirectoryHandle(jsg::Lock& js,
+      jsg::USVString name,
+      jsg::Optional<FileSystemGetDirectoryOptions> options,
+      const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception);
+
+  jsg::Promise<void> removeEntry(jsg::Lock& js,
+      jsg::USVString name,
+      jsg::Optional<FileSystemRemoveOptions> options,
+      const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception);
+
+  // TODO(node-fs): We are not currently implementing the resolve() method
+  // as defined in the spec. This is a bit tricky as it requires us to work
+  // backwards through the parent directories to find the path to the file.
+  // However, our file node's do not currently have a parent pointer, nor do
+  // they know through which path they are accessible through. Support for this
+  // is not critical so we likely won't implement this before we ship the feature
+  // but it is something we should consider doing in the future.
+  jsg::Promise<kj::Array<jsg::USVString>> resolve(
+      jsg::Lock& js, jsg::Ref<FileSystemHandle> possibleDescendant);
+
+  JSG_ITERATOR(
+      EntryIterator, entries, EntryIteratorType, IteratorState, iteratorNext<EntryIteratorType>);
+  JSG_ITERATOR(KeyIterator, keys, KeyIteratorType, IteratorState, iteratorNext<KeyIteratorType>);
+  JSG_ITERATOR(
+      ValueIterator, values, ValueIteratorType, IteratorState, iteratorNext<ValueIteratorType>);
+
+  void forEach(jsg::Lock& js,
+      jsg::Function<void(
+          jsg::USVString, jsg::Ref<FileSystemHandle>, jsg::Ref<FileSystemDirectoryHandle>)>
+          callback,
+      jsg::Optional<jsg::Value> thisArg);
+
+  JSG_RESOURCE_TYPE(FileSystemDirectoryHandle) {
+    JSG_INHERIT(FileSystemHandle);
+    JSG_METHOD(getFileHandle);
+    JSG_METHOD(getDirectoryHandle);
+    JSG_METHOD(removeEntry);
+    JSG_METHOD(resolve);
+    JSG_METHOD(entries);
+    JSG_METHOD(keys);
+    JSG_METHOD(values);
+    JSG_ITERABLE(entries);
+    JSG_METHOD(forEach);
+  }
+
+ private:
+  kj::Rc<workerd::Directory> inner;
+
+  template <typename Type>
+  static kj::Maybe<Type> iteratorNext(jsg::Lock& js, IteratorState& state) {
+    if (state.index >= state.entries.size()) {
+      return kj::none;
+    }
+
+    auto& entry = state.entries[state.index++];
+
+    if constexpr (kj::isSameType<Type, EntryIteratorType>()) {
+      return EntryType(js.accountedUSVString(entry->getName(js)), entry.addRef());
+    } else if constexpr (kj::isSameType<Type, KeyIteratorType>()) {
+      return js.accountedUSVString(entry->getName(js));
+    } else if constexpr (kj::isSameType<Type, ValueIteratorType>()) {
+      return entry.addRef();
+    } else {
+      KJ_UNREACHABLE;
path: src/workerd/api/filesystem.h
user: login: danlapid
body: this should be a static_assert(false) or similar to denote that it is compile time proven to never be the case.
created_at: 2025-05-09T00:08:26Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,333 @@
+#pragma once
+
+#include <workerd/api/streams/writable.h>
+#include <workerd/io/worker-fs.h>
+#include <workerd/jsg/iterator.h>
+#include <workerd/jsg/jsg.h>
+
+namespace workerd::api {
+
+class Blob;
+class File;
+
+// ======================================================================================
+// An implementation of the WHATWG Web File System API (https://fs.spec.whatwg.org/)
+// All of the classes in this part of the impl are defined by the spec.
+
+class FileSystemSyncAccessHandle;
+class FileSystemWritableFileStream;
+
+class FileSystemHandle: public jsg::Object {
+  // TODO(node-fs): The spec defines FileSystemHandle objects as being
+  // serializable, meaning that they should work with structured cloning.
+  // In workers, this would suggest also that they should work with jsrpc.
+  // We don't yet implement any form of serialization for these objects.
+  // Since each worker has its own file system, it might be a bit weird
+  // as the received file on the other side would not actually be in that
+  // destination worker's file system... that is, it would be an orphaned
+  // file handle. It would still likely be useful tho. As a follow up
+  // step, we will implement serialization/deserialization of these objects.
+ public:
+  FileSystemHandle(jsg::USVString name): name(kj::mv(name)) {}
+  jsg::USVString getName(jsg::Lock& js) {
+    return jsg::USVString(kj::str(name));
path: src/workerd/api/filesystem.h
user: login: jasnell
body: hmm... actually no, I think you're right. I forgot value.h is set up to handle `const jsg::USVString&` returns.
created_at: 2025-05-09T00:11:05Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,333 @@
+#pragma once
+
+#include <workerd/api/streams/writable.h>
+#include <workerd/io/worker-fs.h>
+#include <workerd/jsg/iterator.h>
+#include <workerd/jsg/jsg.h>
+
+namespace workerd::api {
+
+class Blob;
+class File;
+
+// ======================================================================================
+// An implementation of the WHATWG Web File System API (https://fs.spec.whatwg.org/)
+// All of the classes in this part of the impl are defined by the spec.
+
+class FileSystemSyncAccessHandle;
+class FileSystemWritableFileStream;
+
+class FileSystemHandle: public jsg::Object {
+  // TODO(node-fs): The spec defines FileSystemHandle objects as being
+  // serializable, meaning that they should work with structured cloning.
+  // In workers, this would suggest also that they should work with jsrpc.
+  // We don't yet implement any form of serialization for these objects.
+  // Since each worker has its own file system, it might be a bit weird
+  // as the received file on the other side would not actually be in that
+  // destination worker's file system... that is, it would be an orphaned
+  // file handle. It would still likely be useful tho. As a follow up
+  // step, we will implement serialization/deserialization of these objects.
+ public:
+  FileSystemHandle(jsg::USVString name): name(kj::mv(name)) {}
+  const jsg::USVString& getName(jsg::Lock& js) {
+    return name;
+  }
+
+  virtual kj::StringPtr getKind(jsg::Lock& js) {
+    // Implemented by subclasses.
+    KJ_UNIMPLEMENTED("getKind() not implemented");
+  }
+  virtual jsg::Promise<bool> isSameEntry(jsg::Lock& js, jsg::Ref<FileSystemHandle> other) {
+    // Implemented by subclasses.
+    KJ_UNIMPLEMENTED("isSameEntry() not implemented");
+  }
+
+  JSG_RESOURCE_TYPE(FileSystemHandle) {
+    JSG_READONLY_PROTOTYPE_PROPERTY(kind, getKind);
+    JSG_READONLY_PROTOTYPE_PROPERTY(name, getName);
+    JSG_METHOD(isSameEntry);
+  }
+
+ private:
+  jsg::USVString name;
+};
+
+class FileSystemFileHandle: public FileSystemHandle {
+ public:
+  FileSystemFileHandle(jsg::USVString name, kj::Rc<workerd::File> inner);
+
+  kj::StringPtr getKind(jsg::Lock& js) override {
+    return "file"_kj;
+  }
+
+  jsg::Promise<bool> isSameEntry(jsg::Lock& js, jsg::Ref<FileSystemHandle> other) override;
+
+  struct FileSystemCreateWritableOptions {
+    bool keepExistingData = false;
+    JSG_STRUCT(keepExistingData);
+  };
+
+  jsg::Promise<jsg::Ref<File>> getFile(jsg::Lock& js);
+
+  jsg::Promise<jsg::Ref<FileSystemWritableFileStream>> createWritable(
+      jsg::Lock& js, jsg::Optional<FileSystemCreateWritableOptions> options);
+
+  jsg::Promise<jsg::Ref<FileSystemSyncAccessHandle>> createSyncAccessHandle(jsg::Lock& js);
+
+  JSG_RESOURCE_TYPE(FileSystemFileHandle) {
+    JSG_INHERIT(FileSystemHandle);
+    JSG_METHOD(getFile);
+    JSG_METHOD(createWritable);
+    JSG_METHOD(createSyncAccessHandle);
+  }
+
+ private:
+  kj::Rc<workerd::File> inner;
+};
+
+class FileSystemDirectoryHandle final: public FileSystemHandle {
+  // TODO(node-fs): Per the spec, FileSystemDirectoryHandler objects are
+  // expected to be async iterables. Here we implement it as a sync iterable.
+  // The effect ends up being largely the same but it's obviously better to
+  // use the asyn iterable API instead. We should implement this before we
+  // ship this feature.
+ public:
+  struct EntryType {
+    jsg::USVString key;
+    jsg::Ref<FileSystemHandle> value;
+    JSG_STRUCT(key, value);
+    EntryType(jsg::USVString key, jsg::Ref<FileSystemHandle> value)
+        : key(kj::mv(key)),
+          value(kj::mv(value)) {}
+  };
+
+ private:
+  using EntryIteratorType = EntryType;
+  using KeyIteratorType = jsg::USVString;
+  using ValueIteratorType = jsg::Ref<FileSystemHandle>;
+
+  struct IteratorState final {
+    jsg::Ref<FileSystemDirectoryHandle> parent;
+    kj::Array<jsg::Ref<FileSystemHandle>> entries;
+    uint index = 0;
+
+    IteratorState(
+        jsg::Ref<FileSystemDirectoryHandle> parent, kj::Array<jsg::Ref<FileSystemHandle>> entries)
+        : parent(kj::mv(parent)),
+          entries(kj::mv(entries)) {}
+
+    void visitForGc(jsg::GcVisitor& visitor) {
+      visitor.visit(parent);
+      visitor.visitAll(entries);
+    }
+
+    JSG_MEMORY_INFO(IteratorState) {
+      tracker.trackField("parent", parent);
+      for (auto& entry: entries) {
+        tracker.trackField("entry", entry);
+      }
+    }
+  };
+
+ public:
+  FileSystemDirectoryHandle(jsg::USVString name, kj::Rc<workerd::Directory> inner);
+
+  kj::StringPtr getKind(jsg::Lock& js) override {
+    return "directory"_kj;
+  }
+
+  jsg::Promise<bool> isSameEntry(jsg::Lock& js, jsg::Ref<FileSystemHandle> other) override;
+
+  struct FileSystemGetFileOptions {
+    bool create = false;
+    JSG_STRUCT(create);
+  };
+
+  struct FileSystemGetDirectoryOptions {
+    bool create = false;
+    JSG_STRUCT(create);
+  };
+
+  struct FileSystemRemoveOptions {
+    bool recursive = false;
+    JSG_STRUCT(recursive);
+  };
+
+  jsg::Promise<jsg::Ref<FileSystemFileHandle>> getFileHandle(jsg::Lock& js,
+      jsg::USVString name,
+      jsg::Optional<FileSystemGetFileOptions> options,
+      const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception);
+
+  jsg::Promise<jsg::Ref<FileSystemDirectoryHandle>> getDirectoryHandle(jsg::Lock& js,
+      jsg::USVString name,
+      jsg::Optional<FileSystemGetDirectoryOptions> options,
+      const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception);
+
+  jsg::Promise<void> removeEntry(jsg::Lock& js,
+      jsg::USVString name,
+      jsg::Optional<FileSystemRemoveOptions> options,
+      const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception);
+
+  // TODO(node-fs): We are not currently implementing the resolve() method
+  // as defined in the spec. This is a bit tricky as it requires us to work
+  // backwards through the parent directories to find the path to the file.
+  // However, our file node's do not currently have a parent pointer, nor do
+  // they know through which path they are accessible through. Support for this
+  // is not critical so we likely won't implement this before we ship the feature
+  // but it is something we should consider doing in the future.
+  jsg::Promise<kj::Array<jsg::USVString>> resolve(
+      jsg::Lock& js, jsg::Ref<FileSystemHandle> possibleDescendant);
+
+  JSG_ITERATOR(
+      EntryIterator, entries, EntryIteratorType, IteratorState, iteratorNext<EntryIteratorType>);
+  JSG_ITERATOR(KeyIterator, keys, KeyIteratorType, IteratorState, iteratorNext<KeyIteratorType>);
+  JSG_ITERATOR(
+      ValueIterator, values, ValueIteratorType, IteratorState, iteratorNext<ValueIteratorType>);
+
+  void forEach(jsg::Lock& js,
+      jsg::Function<void(
+          jsg::USVString, jsg::Ref<FileSystemHandle>, jsg::Ref<FileSystemDirectoryHandle>)>
+          callback,
+      jsg::Optional<jsg::Value> thisArg);
+
+  JSG_RESOURCE_TYPE(FileSystemDirectoryHandle) {
+    JSG_INHERIT(FileSystemHandle);
+    JSG_METHOD(getFileHandle);
+    JSG_METHOD(getDirectoryHandle);
+    JSG_METHOD(removeEntry);
+    JSG_METHOD(resolve);
+    JSG_METHOD(entries);
+    JSG_METHOD(keys);
+    JSG_METHOD(values);
+    JSG_ITERABLE(entries);
+    JSG_METHOD(forEach);
+  }
+
+ private:
+  kj::Rc<workerd::Directory> inner;
+
+  template <typename Type>
+  static kj::Maybe<Type> iteratorNext(jsg::Lock& js, IteratorState& state) {
+    if (state.index >= state.entries.size()) {
+      return kj::none;
+    }
+
+    auto& entry = state.entries[state.index++];
+
+    if constexpr (kj::isSameType<Type, EntryIteratorType>()) {
+      return EntryType(js.accountedUSVString(entry->getName(js)), entry.addRef());
+    } else if constexpr (kj::isSameType<Type, KeyIteratorType>()) {
+      return js.accountedUSVString(entry->getName(js));
+    } else if constexpr (kj::isSameType<Type, ValueIteratorType>()) {
+      return entry.addRef();
+    } else {
+      static_assert(false, "invalid iterator type");
+    }
+  }
+};
+
+class FileSystemWritableFileStream: public WritableStream {
+  // TODO(node-fs): The spec defines FileSystemWritableFileStream objects such
+  // that any changes to the file are not actually committed until the stream
+  // is closed. We're not yet implementing this. All writes modify the source
+  // file in memory and would be immediately visible to other handles pointing
+  // to the same file. This should be updated before this feature is unflagged
+  // and made stable.
+ public:
+  struct State: public kj::Refcounted {
+    kj::Maybe<kj::Rc<workerd::File>> file;
+    size_t position = 0;
+    State(kj::Rc<workerd::File> file): file(kj::mv(file)) {}
+  };
+
+  FileSystemWritableFileStream(
+      kj::Own<WritableStreamController> controller, kj::Rc<State> sharedState);
+
+  static jsg::Ref<FileSystemWritableFileStream> constructor() = delete;
+
+  struct WriteParams {
+    kj::String type;  // one of: write, seek, truncate
path: src/workerd/api/filesystem.h
user: login: danlapid
body: General nit:
Why not use JsStrings everywhere?
Non blocking of course
created_at: 2025-05-09T00:17:53Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,333 @@
+#pragma once
+
+#include <workerd/api/streams/writable.h>
+#include <workerd/io/worker-fs.h>
+#include <workerd/jsg/iterator.h>
+#include <workerd/jsg/jsg.h>
+
+namespace workerd::api {
+
+class Blob;
+class File;
+
+// ======================================================================================
+// An implementation of the WHATWG Web File System API (https://fs.spec.whatwg.org/)
+// All of the classes in this part of the impl are defined by the spec.
+
+class FileSystemSyncAccessHandle;
+class FileSystemWritableFileStream;
+
+class FileSystemHandle: public jsg::Object {
+  // TODO(node-fs): The spec defines FileSystemHandle objects as being
+  // serializable, meaning that they should work with structured cloning.
+  // In workers, this would suggest also that they should work with jsrpc.
+  // We don't yet implement any form of serialization for these objects.
+  // Since each worker has its own file system, it might be a bit weird
+  // as the received file on the other side would not actually be in that
+  // destination worker's file system... that is, it would be an orphaned
+  // file handle. It would still likely be useful tho. As a follow up
+  // step, we will implement serialization/deserialization of these objects.
+ public:
+  FileSystemHandle(jsg::USVString name): name(kj::mv(name)) {}
+  const jsg::USVString& getName(jsg::Lock& js) {
+    return name;
+  }
+
+  virtual kj::StringPtr getKind(jsg::Lock& js) {
+    // Implemented by subclasses.
+    KJ_UNIMPLEMENTED("getKind() not implemented");
+  }
+  virtual jsg::Promise<bool> isSameEntry(jsg::Lock& js, jsg::Ref<FileSystemHandle> other) {
+    // Implemented by subclasses.
+    KJ_UNIMPLEMENTED("isSameEntry() not implemented");
+  }
+
+  JSG_RESOURCE_TYPE(FileSystemHandle) {
+    JSG_READONLY_PROTOTYPE_PROPERTY(kind, getKind);
+    JSG_READONLY_PROTOTYPE_PROPERTY(name, getName);
+    JSG_METHOD(isSameEntry);
+  }
+
+ private:
+  jsg::USVString name;
+};
+
+class FileSystemFileHandle: public FileSystemHandle {
+ public:
+  FileSystemFileHandle(jsg::USVString name, kj::Rc<workerd::File> inner);
+
+  kj::StringPtr getKind(jsg::Lock& js) override {
+    return "file"_kj;
+  }
+
+  jsg::Promise<bool> isSameEntry(jsg::Lock& js, jsg::Ref<FileSystemHandle> other) override;
+
+  struct FileSystemCreateWritableOptions {
+    bool keepExistingData = false;
+    JSG_STRUCT(keepExistingData);
+  };
+
+  jsg::Promise<jsg::Ref<File>> getFile(jsg::Lock& js);
+
+  jsg::Promise<jsg::Ref<FileSystemWritableFileStream>> createWritable(
+      jsg::Lock& js, jsg::Optional<FileSystemCreateWritableOptions> options);
+
+  jsg::Promise<jsg::Ref<FileSystemSyncAccessHandle>> createSyncAccessHandle(jsg::Lock& js);
+
+  JSG_RESOURCE_TYPE(FileSystemFileHandle) {
+    JSG_INHERIT(FileSystemHandle);
+    JSG_METHOD(getFile);
+    JSG_METHOD(createWritable);
+    JSG_METHOD(createSyncAccessHandle);
+  }
+
+ private:
+  kj::Rc<workerd::File> inner;
+};
+
+class FileSystemDirectoryHandle final: public FileSystemHandle {
+  // TODO(node-fs): Per the spec, FileSystemDirectoryHandler objects are
+  // expected to be async iterables. Here we implement it as a sync iterable.
+  // The effect ends up being largely the same but it's obviously better to
+  // use the asyn iterable API instead. We should implement this before we
+  // ship this feature.
+ public:
+  struct EntryType {
+    jsg::USVString key;
+    jsg::Ref<FileSystemHandle> value;
+    JSG_STRUCT(key, value);
+    EntryType(jsg::USVString key, jsg::Ref<FileSystemHandle> value)
+        : key(kj::mv(key)),
+          value(kj::mv(value)) {}
+  };
+
+ private:
+  using EntryIteratorType = EntryType;
+  using KeyIteratorType = jsg::USVString;
+  using ValueIteratorType = jsg::Ref<FileSystemHandle>;
+
+  struct IteratorState final {
+    jsg::Ref<FileSystemDirectoryHandle> parent;
+    kj::Array<jsg::Ref<FileSystemHandle>> entries;
+    uint index = 0;
+
+    IteratorState(
+        jsg::Ref<FileSystemDirectoryHandle> parent, kj::Array<jsg::Ref<FileSystemHandle>> entries)
+        : parent(kj::mv(parent)),
+          entries(kj::mv(entries)) {}
+
+    void visitForGc(jsg::GcVisitor& visitor) {
+      visitor.visit(parent);
+      visitor.visitAll(entries);
+    }
+
+    JSG_MEMORY_INFO(IteratorState) {
+      tracker.trackField("parent", parent);
+      for (auto& entry: entries) {
+        tracker.trackField("entry", entry);
+      }
+    }
+  };
+
+ public:
+  FileSystemDirectoryHandle(jsg::USVString name, kj::Rc<workerd::Directory> inner);
+
+  kj::StringPtr getKind(jsg::Lock& js) override {
+    return "directory"_kj;
+  }
+
+  jsg::Promise<bool> isSameEntry(jsg::Lock& js, jsg::Ref<FileSystemHandle> other) override;
+
+  struct FileSystemGetFileOptions {
+    bool create = false;
+    JSG_STRUCT(create);
+  };
+
+  struct FileSystemGetDirectoryOptions {
+    bool create = false;
+    JSG_STRUCT(create);
+  };
+
+  struct FileSystemRemoveOptions {
+    bool recursive = false;
+    JSG_STRUCT(recursive);
+  };
+
+  jsg::Promise<jsg::Ref<FileSystemFileHandle>> getFileHandle(jsg::Lock& js,
+      jsg::USVString name,
+      jsg::Optional<FileSystemGetFileOptions> options,
+      const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception);
+
+  jsg::Promise<jsg::Ref<FileSystemDirectoryHandle>> getDirectoryHandle(jsg::Lock& js,
+      jsg::USVString name,
+      jsg::Optional<FileSystemGetDirectoryOptions> options,
+      const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception);
+
+  jsg::Promise<void> removeEntry(jsg::Lock& js,
+      jsg::USVString name,
+      jsg::Optional<FileSystemRemoveOptions> options,
+      const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception);
+
+  // TODO(node-fs): We are not currently implementing the resolve() method
+  // as defined in the spec. This is a bit tricky as it requires us to work
+  // backwards through the parent directories to find the path to the file.
+  // However, our file node's do not currently have a parent pointer, nor do
+  // they know through which path they are accessible through. Support for this
+  // is not critical so we likely won't implement this before we ship the feature
+  // but it is something we should consider doing in the future.
+  jsg::Promise<kj::Array<jsg::USVString>> resolve(
+      jsg::Lock& js, jsg::Ref<FileSystemHandle> possibleDescendant);
+
+  JSG_ITERATOR(
+      EntryIterator, entries, EntryIteratorType, IteratorState, iteratorNext<EntryIteratorType>);
+  JSG_ITERATOR(KeyIterator, keys, KeyIteratorType, IteratorState, iteratorNext<KeyIteratorType>);
+  JSG_ITERATOR(
+      ValueIterator, values, ValueIteratorType, IteratorState, iteratorNext<ValueIteratorType>);
+
+  void forEach(jsg::Lock& js,
+      jsg::Function<void(
+          jsg::USVString, jsg::Ref<FileSystemHandle>, jsg::Ref<FileSystemDirectoryHandle>)>
+          callback,
+      jsg::Optional<jsg::Value> thisArg);
+
+  JSG_RESOURCE_TYPE(FileSystemDirectoryHandle) {
+    JSG_INHERIT(FileSystemHandle);
+    JSG_METHOD(getFileHandle);
+    JSG_METHOD(getDirectoryHandle);
+    JSG_METHOD(removeEntry);
+    JSG_METHOD(resolve);
+    JSG_METHOD(entries);
+    JSG_METHOD(keys);
+    JSG_METHOD(values);
+    JSG_ITERABLE(entries);
+    JSG_METHOD(forEach);
+  }
+
+ private:
+  kj::Rc<workerd::Directory> inner;
+
+  template <typename Type>
+  static kj::Maybe<Type> iteratorNext(jsg::Lock& js, IteratorState& state) {
+    if (state.index >= state.entries.size()) {
+      return kj::none;
+    }
+
+    auto& entry = state.entries[state.index++];
+
+    if constexpr (kj::isSameType<Type, EntryIteratorType>()) {
+      return EntryType(js.accountedUSVString(entry->getName(js)), entry.addRef());
+    } else if constexpr (kj::isSameType<Type, KeyIteratorType>()) {
+      return js.accountedUSVString(entry->getName(js));
+    } else if constexpr (kj::isSameType<Type, ValueIteratorType>()) {
+      return entry.addRef();
+    } else {
+      static_assert(false, "invalid iterator type");
+    }
+  }
+};
+
+class FileSystemWritableFileStream: public WritableStream {
+  // TODO(node-fs): The spec defines FileSystemWritableFileStream objects such
+  // that any changes to the file are not actually committed until the stream
+  // is closed. We're not yet implementing this. All writes modify the source
+  // file in memory and would be immediately visible to other handles pointing
+  // to the same file. This should be updated before this feature is unflagged
+  // and made stable.
+ public:
+  struct State: public kj::Refcounted {
+    kj::Maybe<kj::Rc<workerd::File>> file;
+    size_t position = 0;
+    State(kj::Rc<workerd::File> file): file(kj::mv(file)) {}
+  };
+
+  FileSystemWritableFileStream(
+      kj::Own<WritableStreamController> controller, kj::Rc<State> sharedState);
+
+  static jsg::Ref<FileSystemWritableFileStream> constructor() = delete;
+
+  struct WriteParams {
+    kj::String type;  // one of: write, seek, truncate
+    jsg::Optional<double> size;
path: src/workerd/api/filesystem.h
user: login: danlapid
body: I might be missing something but why is this double?
I think it should be uint64_t or even uint32_t

https://fs.spec.whatwg.org/#api-filesystemwritablefilestream
The SPEC seems to say unsigned long long which is not a double and double to size_t conversions in the implementation are rather ugly
created_at: 2025-05-09T00:26:48Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,333 @@
+#pragma once
+
+#include <workerd/api/streams/writable.h>
+#include <workerd/io/worker-fs.h>
+#include <workerd/jsg/iterator.h>
+#include <workerd/jsg/jsg.h>
+
+namespace workerd::api {
+
+class Blob;
+class File;
+
+// ======================================================================================
+// An implementation of the WHATWG Web File System API (https://fs.spec.whatwg.org/)
+// All of the classes in this part of the impl are defined by the spec.
+
+class FileSystemSyncAccessHandle;
+class FileSystemWritableFileStream;
+
+class FileSystemHandle: public jsg::Object {
+  // TODO(node-fs): The spec defines FileSystemHandle objects as being
+  // serializable, meaning that they should work with structured cloning.
+  // In workers, this would suggest also that they should work with jsrpc.
+  // We don't yet implement any form of serialization for these objects.
+  // Since each worker has its own file system, it might be a bit weird
+  // as the received file on the other side would not actually be in that
+  // destination worker's file system... that is, it would be an orphaned
+  // file handle. It would still likely be useful tho. As a follow up
+  // step, we will implement serialization/deserialization of these objects.
+ public:
+  FileSystemHandle(jsg::USVString name): name(kj::mv(name)) {}
+  const jsg::USVString& getName(jsg::Lock& js) {
+    return name;
+  }
+
+  virtual kj::StringPtr getKind(jsg::Lock& js) {
+    // Implemented by subclasses.
+    KJ_UNIMPLEMENTED("getKind() not implemented");
+  }
+  virtual jsg::Promise<bool> isSameEntry(jsg::Lock& js, jsg::Ref<FileSystemHandle> other) {
+    // Implemented by subclasses.
+    KJ_UNIMPLEMENTED("isSameEntry() not implemented");
+  }
+
+  JSG_RESOURCE_TYPE(FileSystemHandle) {
+    JSG_READONLY_PROTOTYPE_PROPERTY(kind, getKind);
+    JSG_READONLY_PROTOTYPE_PROPERTY(name, getName);
+    JSG_METHOD(isSameEntry);
+  }
+
+ private:
+  jsg::USVString name;
+};
+
+class FileSystemFileHandle: public FileSystemHandle {
+ public:
+  FileSystemFileHandle(jsg::USVString name, kj::Rc<workerd::File> inner);
+
+  kj::StringPtr getKind(jsg::Lock& js) override {
+    return "file"_kj;
+  }
+
+  jsg::Promise<bool> isSameEntry(jsg::Lock& js, jsg::Ref<FileSystemHandle> other) override;
+
+  struct FileSystemCreateWritableOptions {
+    bool keepExistingData = false;
+    JSG_STRUCT(keepExistingData);
+  };
+
+  jsg::Promise<jsg::Ref<File>> getFile(jsg::Lock& js);
+
+  jsg::Promise<jsg::Ref<FileSystemWritableFileStream>> createWritable(
+      jsg::Lock& js, jsg::Optional<FileSystemCreateWritableOptions> options);
+
+  jsg::Promise<jsg::Ref<FileSystemSyncAccessHandle>> createSyncAccessHandle(jsg::Lock& js);
+
+  JSG_RESOURCE_TYPE(FileSystemFileHandle) {
+    JSG_INHERIT(FileSystemHandle);
+    JSG_METHOD(getFile);
+    JSG_METHOD(createWritable);
+    JSG_METHOD(createSyncAccessHandle);
+  }
+
+ private:
+  kj::Rc<workerd::File> inner;
+};
+
+class FileSystemDirectoryHandle final: public FileSystemHandle {
+  // TODO(node-fs): Per the spec, FileSystemDirectoryHandler objects are
+  // expected to be async iterables. Here we implement it as a sync iterable.
+  // The effect ends up being largely the same but it's obviously better to
+  // use the asyn iterable API instead. We should implement this before we
+  // ship this feature.
+ public:
+  struct EntryType {
+    jsg::USVString key;
+    jsg::Ref<FileSystemHandle> value;
+    JSG_STRUCT(key, value);
+    EntryType(jsg::USVString key, jsg::Ref<FileSystemHandle> value)
+        : key(kj::mv(key)),
+          value(kj::mv(value)) {}
+  };
+
+ private:
+  using EntryIteratorType = EntryType;
+  using KeyIteratorType = jsg::USVString;
+  using ValueIteratorType = jsg::Ref<FileSystemHandle>;
+
+  struct IteratorState final {
+    jsg::Ref<FileSystemDirectoryHandle> parent;
+    kj::Array<jsg::Ref<FileSystemHandle>> entries;
+    uint index = 0;
+
+    IteratorState(
+        jsg::Ref<FileSystemDirectoryHandle> parent, kj::Array<jsg::Ref<FileSystemHandle>> entries)
+        : parent(kj::mv(parent)),
+          entries(kj::mv(entries)) {}
+
+    void visitForGc(jsg::GcVisitor& visitor) {
+      visitor.visit(parent);
+      visitor.visitAll(entries);
+    }
+
+    JSG_MEMORY_INFO(IteratorState) {
+      tracker.trackField("parent", parent);
+      for (auto& entry: entries) {
+        tracker.trackField("entry", entry);
+      }
+    }
+  };
+
+ public:
+  FileSystemDirectoryHandle(jsg::USVString name, kj::Rc<workerd::Directory> inner);
+
+  kj::StringPtr getKind(jsg::Lock& js) override {
+    return "directory"_kj;
+  }
+
+  jsg::Promise<bool> isSameEntry(jsg::Lock& js, jsg::Ref<FileSystemHandle> other) override;
+
+  struct FileSystemGetFileOptions {
+    bool create = false;
+    JSG_STRUCT(create);
+  };
+
+  struct FileSystemGetDirectoryOptions {
+    bool create = false;
+    JSG_STRUCT(create);
+  };
+
+  struct FileSystemRemoveOptions {
+    bool recursive = false;
+    JSG_STRUCT(recursive);
+  };
+
+  jsg::Promise<jsg::Ref<FileSystemFileHandle>> getFileHandle(jsg::Lock& js,
+      jsg::USVString name,
+      jsg::Optional<FileSystemGetFileOptions> options,
+      const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception);
+
+  jsg::Promise<jsg::Ref<FileSystemDirectoryHandle>> getDirectoryHandle(jsg::Lock& js,
+      jsg::USVString name,
+      jsg::Optional<FileSystemGetDirectoryOptions> options,
+      const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception);
+
+  jsg::Promise<void> removeEntry(jsg::Lock& js,
+      jsg::USVString name,
+      jsg::Optional<FileSystemRemoveOptions> options,
+      const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception);
+
+  // TODO(node-fs): We are not currently implementing the resolve() method
+  // as defined in the spec. This is a bit tricky as it requires us to work
+  // backwards through the parent directories to find the path to the file.
+  // However, our file node's do not currently have a parent pointer, nor do
+  // they know through which path they are accessible through. Support for this
+  // is not critical so we likely won't implement this before we ship the feature
+  // but it is something we should consider doing in the future.
+  jsg::Promise<kj::Array<jsg::USVString>> resolve(
+      jsg::Lock& js, jsg::Ref<FileSystemHandle> possibleDescendant);
+
+  JSG_ITERATOR(
+      EntryIterator, entries, EntryIteratorType, IteratorState, iteratorNext<EntryIteratorType>);
+  JSG_ITERATOR(KeyIterator, keys, KeyIteratorType, IteratorState, iteratorNext<KeyIteratorType>);
+  JSG_ITERATOR(
+      ValueIterator, values, ValueIteratorType, IteratorState, iteratorNext<ValueIteratorType>);
+
+  void forEach(jsg::Lock& js,
+      jsg::Function<void(
+          jsg::USVString, jsg::Ref<FileSystemHandle>, jsg::Ref<FileSystemDirectoryHandle>)>
+          callback,
+      jsg::Optional<jsg::Value> thisArg);
+
+  JSG_RESOURCE_TYPE(FileSystemDirectoryHandle) {
+    JSG_INHERIT(FileSystemHandle);
+    JSG_METHOD(getFileHandle);
+    JSG_METHOD(getDirectoryHandle);
+    JSG_METHOD(removeEntry);
+    JSG_METHOD(resolve);
+    JSG_METHOD(entries);
+    JSG_METHOD(keys);
+    JSG_METHOD(values);
+    JSG_ITERABLE(entries);
+    JSG_METHOD(forEach);
+  }
+
+ private:
+  kj::Rc<workerd::Directory> inner;
+
+  template <typename Type>
+  static kj::Maybe<Type> iteratorNext(jsg::Lock& js, IteratorState& state) {
+    if (state.index >= state.entries.size()) {
+      return kj::none;
+    }
+
+    auto& entry = state.entries[state.index++];
+
+    if constexpr (kj::isSameType<Type, EntryIteratorType>()) {
+      return EntryType(js.accountedUSVString(entry->getName(js)), entry.addRef());
+    } else if constexpr (kj::isSameType<Type, KeyIteratorType>()) {
+      return js.accountedUSVString(entry->getName(js));
+    } else if constexpr (kj::isSameType<Type, ValueIteratorType>()) {
+      return entry.addRef();
+    } else {
+      static_assert(false, "invalid iterator type");
+    }
+  }
+};
+
+class FileSystemWritableFileStream: public WritableStream {
+  // TODO(node-fs): The spec defines FileSystemWritableFileStream objects such
+  // that any changes to the file are not actually committed until the stream
+  // is closed. We're not yet implementing this. All writes modify the source
+  // file in memory and would be immediately visible to other handles pointing
+  // to the same file. This should be updated before this feature is unflagged
+  // and made stable.
+ public:
+  struct State: public kj::Refcounted {
+    kj::Maybe<kj::Rc<workerd::File>> file;
+    size_t position = 0;
+    State(kj::Rc<workerd::File> file): file(kj::mv(file)) {}
+  };
+
+  FileSystemWritableFileStream(
+      kj::Own<WritableStreamController> controller, kj::Rc<State> sharedState);
+
+  static jsg::Ref<FileSystemWritableFileStream> constructor() = delete;
+
+  struct WriteParams {
+    kj::String type;  // one of: write, seek, truncate
path: src/workerd/api/filesystem.h
user: login: jasnell
body: those still end up needing to be converted to `kj::String` for comparison operations like what we see in `FileSystemWritableFileStream::write` (e.g. `if (params.type == "write"_kj) {` unless we created `JsStrings` to replace the `"write"_kj` part. I'd rather keep this as familiar as possible to our existing convention. If we want to convert more of the `kj::String` uses to `JsString` en masse then we can do so in a separate PR.
created_at: 2025-05-09T00:32:44Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,333 @@
+#pragma once
+
+#include <workerd/api/streams/writable.h>
+#include <workerd/io/worker-fs.h>
+#include <workerd/jsg/iterator.h>
+#include <workerd/jsg/jsg.h>
+
+namespace workerd::api {
+
+class Blob;
+class File;
+
+// ======================================================================================
+// An implementation of the WHATWG Web File System API (https://fs.spec.whatwg.org/)
+// All of the classes in this part of the impl are defined by the spec.
+
+class FileSystemSyncAccessHandle;
+class FileSystemWritableFileStream;
+
+class FileSystemHandle: public jsg::Object {
+  // TODO(node-fs): The spec defines FileSystemHandle objects as being
+  // serializable, meaning that they should work with structured cloning.
+  // In workers, this would suggest also that they should work with jsrpc.
+  // We don't yet implement any form of serialization for these objects.
+  // Since each worker has its own file system, it might be a bit weird
+  // as the received file on the other side would not actually be in that
+  // destination worker's file system... that is, it would be an orphaned
+  // file handle. It would still likely be useful tho. As a follow up
+  // step, we will implement serialization/deserialization of these objects.
+ public:
+  FileSystemHandle(jsg::USVString name): name(kj::mv(name)) {}
+  const jsg::USVString& getName(jsg::Lock& js) {
+    return name;
+  }
+
+  virtual kj::StringPtr getKind(jsg::Lock& js) {
+    // Implemented by subclasses.
+    KJ_UNIMPLEMENTED("getKind() not implemented");
+  }
+  virtual jsg::Promise<bool> isSameEntry(jsg::Lock& js, jsg::Ref<FileSystemHandle> other) {
+    // Implemented by subclasses.
+    KJ_UNIMPLEMENTED("isSameEntry() not implemented");
+  }
+
+  JSG_RESOURCE_TYPE(FileSystemHandle) {
+    JSG_READONLY_PROTOTYPE_PROPERTY(kind, getKind);
+    JSG_READONLY_PROTOTYPE_PROPERTY(name, getName);
+    JSG_METHOD(isSameEntry);
+  }
+
+ private:
+  jsg::USVString name;
+};
+
+class FileSystemFileHandle: public FileSystemHandle {
+ public:
+  FileSystemFileHandle(jsg::USVString name, kj::Rc<workerd::File> inner);
+
+  kj::StringPtr getKind(jsg::Lock& js) override {
+    return "file"_kj;
+  }
+
+  jsg::Promise<bool> isSameEntry(jsg::Lock& js, jsg::Ref<FileSystemHandle> other) override;
+
+  struct FileSystemCreateWritableOptions {
+    bool keepExistingData = false;
+    JSG_STRUCT(keepExistingData);
+  };
+
+  jsg::Promise<jsg::Ref<File>> getFile(jsg::Lock& js);
+
+  jsg::Promise<jsg::Ref<FileSystemWritableFileStream>> createWritable(
+      jsg::Lock& js, jsg::Optional<FileSystemCreateWritableOptions> options);
+
+  jsg::Promise<jsg::Ref<FileSystemSyncAccessHandle>> createSyncAccessHandle(jsg::Lock& js);
+
+  JSG_RESOURCE_TYPE(FileSystemFileHandle) {
+    JSG_INHERIT(FileSystemHandle);
+    JSG_METHOD(getFile);
+    JSG_METHOD(createWritable);
+    JSG_METHOD(createSyncAccessHandle);
+  }
+
+ private:
+  kj::Rc<workerd::File> inner;
+};
+
+class FileSystemDirectoryHandle final: public FileSystemHandle {
+  // TODO(node-fs): Per the spec, FileSystemDirectoryHandler objects are
+  // expected to be async iterables. Here we implement it as a sync iterable.
+  // The effect ends up being largely the same but it's obviously better to
+  // use the asyn iterable API instead. We should implement this before we
+  // ship this feature.
+ public:
+  struct EntryType {
+    jsg::USVString key;
+    jsg::Ref<FileSystemHandle> value;
+    JSG_STRUCT(key, value);
+    EntryType(jsg::USVString key, jsg::Ref<FileSystemHandle> value)
+        : key(kj::mv(key)),
+          value(kj::mv(value)) {}
+  };
+
+ private:
+  using EntryIteratorType = EntryType;
+  using KeyIteratorType = jsg::USVString;
+  using ValueIteratorType = jsg::Ref<FileSystemHandle>;
+
+  struct IteratorState final {
+    jsg::Ref<FileSystemDirectoryHandle> parent;
+    kj::Array<jsg::Ref<FileSystemHandle>> entries;
+    uint index = 0;
+
+    IteratorState(
+        jsg::Ref<FileSystemDirectoryHandle> parent, kj::Array<jsg::Ref<FileSystemHandle>> entries)
+        : parent(kj::mv(parent)),
+          entries(kj::mv(entries)) {}
+
+    void visitForGc(jsg::GcVisitor& visitor) {
+      visitor.visit(parent);
+      visitor.visitAll(entries);
+    }
+
+    JSG_MEMORY_INFO(IteratorState) {
+      tracker.trackField("parent", parent);
+      for (auto& entry: entries) {
+        tracker.trackField("entry", entry);
+      }
+    }
+  };
+
+ public:
+  FileSystemDirectoryHandle(jsg::USVString name, kj::Rc<workerd::Directory> inner);
+
+  kj::StringPtr getKind(jsg::Lock& js) override {
+    return "directory"_kj;
+  }
+
+  jsg::Promise<bool> isSameEntry(jsg::Lock& js, jsg::Ref<FileSystemHandle> other) override;
+
+  struct FileSystemGetFileOptions {
+    bool create = false;
+    JSG_STRUCT(create);
+  };
+
+  struct FileSystemGetDirectoryOptions {
+    bool create = false;
+    JSG_STRUCT(create);
+  };
+
+  struct FileSystemRemoveOptions {
+    bool recursive = false;
+    JSG_STRUCT(recursive);
+  };
+
+  jsg::Promise<jsg::Ref<FileSystemFileHandle>> getFileHandle(jsg::Lock& js,
+      jsg::USVString name,
+      jsg::Optional<FileSystemGetFileOptions> options,
+      const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception);
+
+  jsg::Promise<jsg::Ref<FileSystemDirectoryHandle>> getDirectoryHandle(jsg::Lock& js,
+      jsg::USVString name,
+      jsg::Optional<FileSystemGetDirectoryOptions> options,
+      const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception);
+
+  jsg::Promise<void> removeEntry(jsg::Lock& js,
+      jsg::USVString name,
+      jsg::Optional<FileSystemRemoveOptions> options,
+      const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception);
+
+  // TODO(node-fs): We are not currently implementing the resolve() method
+  // as defined in the spec. This is a bit tricky as it requires us to work
+  // backwards through the parent directories to find the path to the file.
+  // However, our file node's do not currently have a parent pointer, nor do
+  // they know through which path they are accessible through. Support for this
+  // is not critical so we likely won't implement this before we ship the feature
+  // but it is something we should consider doing in the future.
+  jsg::Promise<kj::Array<jsg::USVString>> resolve(
+      jsg::Lock& js, jsg::Ref<FileSystemHandle> possibleDescendant);
+
+  JSG_ITERATOR(
+      EntryIterator, entries, EntryIteratorType, IteratorState, iteratorNext<EntryIteratorType>);
+  JSG_ITERATOR(KeyIterator, keys, KeyIteratorType, IteratorState, iteratorNext<KeyIteratorType>);
+  JSG_ITERATOR(
+      ValueIterator, values, ValueIteratorType, IteratorState, iteratorNext<ValueIteratorType>);
+
+  void forEach(jsg::Lock& js,
+      jsg::Function<void(
+          jsg::USVString, jsg::Ref<FileSystemHandle>, jsg::Ref<FileSystemDirectoryHandle>)>
+          callback,
+      jsg::Optional<jsg::Value> thisArg);
+
+  JSG_RESOURCE_TYPE(FileSystemDirectoryHandle) {
+    JSG_INHERIT(FileSystemHandle);
+    JSG_METHOD(getFileHandle);
+    JSG_METHOD(getDirectoryHandle);
+    JSG_METHOD(removeEntry);
+    JSG_METHOD(resolve);
+    JSG_METHOD(entries);
+    JSG_METHOD(keys);
+    JSG_METHOD(values);
+    JSG_ITERABLE(entries);
+    JSG_METHOD(forEach);
+  }
+
+ private:
+  kj::Rc<workerd::Directory> inner;
+
+  template <typename Type>
+  static kj::Maybe<Type> iteratorNext(jsg::Lock& js, IteratorState& state) {
+    if (state.index >= state.entries.size()) {
+      return kj::none;
+    }
+
+    auto& entry = state.entries[state.index++];
+
+    if constexpr (kj::isSameType<Type, EntryIteratorType>()) {
+      return EntryType(js.accountedUSVString(entry->getName(js)), entry.addRef());
+    } else if constexpr (kj::isSameType<Type, KeyIteratorType>()) {
+      return js.accountedUSVString(entry->getName(js));
+    } else if constexpr (kj::isSameType<Type, ValueIteratorType>()) {
+      return entry.addRef();
+    } else {
+      static_assert(false, "invalid iterator type");
+    }
+  }
+};
+
+class FileSystemWritableFileStream: public WritableStream {
+  // TODO(node-fs): The spec defines FileSystemWritableFileStream objects such
+  // that any changes to the file are not actually committed until the stream
+  // is closed. We're not yet implementing this. All writes modify the source
+  // file in memory and would be immediately visible to other handles pointing
+  // to the same file. This should be updated before this feature is unflagged
+  // and made stable.
+ public:
+  struct State: public kj::Refcounted {
+    kj::Maybe<kj::Rc<workerd::File>> file;
+    size_t position = 0;
+    State(kj::Rc<workerd::File> file): file(kj::mv(file)) {}
+  };
+
+  FileSystemWritableFileStream(
+      kj::Own<WritableStreamController> controller, kj::Rc<State> sharedState);
+
+  static jsg::Ref<FileSystemWritableFileStream> constructor() = delete;
+
+  struct WriteParams {
+    kj::String type;  // one of: write, seek, truncate
+    jsg::Optional<double> size;
path: src/workerd/api/filesystem.h
user: login: jasnell
body: `uin64_t` maps to `bigint` in our type wrapper system (well, on outbound at least). It could be `uing32_t` but the spec says these should be `unsigned long long` and not `unsigned long` which is what `uint32_t` maps to.  If we're going to map to `uint32_t` then we should change the places where we are using `size_t` to using `uint32_t` so that everything is consistent.
created_at: 2025-05-09T00:35:49Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,333 @@
+#pragma once
+
+#include <workerd/api/streams/writable.h>
+#include <workerd/io/worker-fs.h>
+#include <workerd/jsg/iterator.h>
+#include <workerd/jsg/jsg.h>
+
+namespace workerd::api {
+
+class Blob;
+class File;
+
+// ======================================================================================
+// An implementation of the WHATWG Web File System API (https://fs.spec.whatwg.org/)
+// All of the classes in this part of the impl are defined by the spec.
+
+class FileSystemSyncAccessHandle;
+class FileSystemWritableFileStream;
+
+class FileSystemHandle: public jsg::Object {
+  // TODO(node-fs): The spec defines FileSystemHandle objects as being
+  // serializable, meaning that they should work with structured cloning.
+  // In workers, this would suggest also that they should work with jsrpc.
+  // We don't yet implement any form of serialization for these objects.
+  // Since each worker has its own file system, it might be a bit weird
+  // as the received file on the other side would not actually be in that
+  // destination worker's file system... that is, it would be an orphaned
+  // file handle. It would still likely be useful tho. As a follow up
+  // step, we will implement serialization/deserialization of these objects.
+ public:
+  FileSystemHandle(jsg::USVString name): name(kj::mv(name)) {}
+  const jsg::USVString& getName(jsg::Lock& js) {
+    return name;
+  }
+
+  virtual kj::StringPtr getKind(jsg::Lock& js) {
+    // Implemented by subclasses.
+    KJ_UNIMPLEMENTED("getKind() not implemented");
+  }
+  virtual jsg::Promise<bool> isSameEntry(jsg::Lock& js, jsg::Ref<FileSystemHandle> other) {
+    // Implemented by subclasses.
+    KJ_UNIMPLEMENTED("isSameEntry() not implemented");
+  }
+
+  JSG_RESOURCE_TYPE(FileSystemHandle) {
+    JSG_READONLY_PROTOTYPE_PROPERTY(kind, getKind);
+    JSG_READONLY_PROTOTYPE_PROPERTY(name, getName);
+    JSG_METHOD(isSameEntry);
+  }
+
+ private:
+  jsg::USVString name;
+};
+
+class FileSystemFileHandle: public FileSystemHandle {
+ public:
+  FileSystemFileHandle(jsg::USVString name, kj::Rc<workerd::File> inner);
+
+  kj::StringPtr getKind(jsg::Lock& js) override {
+    return "file"_kj;
+  }
+
+  jsg::Promise<bool> isSameEntry(jsg::Lock& js, jsg::Ref<FileSystemHandle> other) override;
+
+  struct FileSystemCreateWritableOptions {
+    bool keepExistingData = false;
+    JSG_STRUCT(keepExistingData);
+  };
+
+  jsg::Promise<jsg::Ref<File>> getFile(jsg::Lock& js);
+
+  jsg::Promise<jsg::Ref<FileSystemWritableFileStream>> createWritable(
+      jsg::Lock& js, jsg::Optional<FileSystemCreateWritableOptions> options);
+
+  jsg::Promise<jsg::Ref<FileSystemSyncAccessHandle>> createSyncAccessHandle(jsg::Lock& js);
+
+  JSG_RESOURCE_TYPE(FileSystemFileHandle) {
+    JSG_INHERIT(FileSystemHandle);
+    JSG_METHOD(getFile);
+    JSG_METHOD(createWritable);
+    JSG_METHOD(createSyncAccessHandle);
+  }
+
+ private:
+  kj::Rc<workerd::File> inner;
+};
+
+class FileSystemDirectoryHandle final: public FileSystemHandle {
+  // TODO(node-fs): Per the spec, FileSystemDirectoryHandler objects are
+  // expected to be async iterables. Here we implement it as a sync iterable.
+  // The effect ends up being largely the same but it's obviously better to
+  // use the asyn iterable API instead. We should implement this before we
+  // ship this feature.
+ public:
+  struct EntryType {
+    jsg::USVString key;
+    jsg::Ref<FileSystemHandle> value;
+    JSG_STRUCT(key, value);
+    EntryType(jsg::USVString key, jsg::Ref<FileSystemHandle> value)
+        : key(kj::mv(key)),
+          value(kj::mv(value)) {}
+  };
+
+ private:
+  using EntryIteratorType = EntryType;
+  using KeyIteratorType = jsg::USVString;
+  using ValueIteratorType = jsg::Ref<FileSystemHandle>;
+
+  struct IteratorState final {
+    jsg::Ref<FileSystemDirectoryHandle> parent;
+    kj::Array<jsg::Ref<FileSystemHandle>> entries;
+    uint index = 0;
+
+    IteratorState(
+        jsg::Ref<FileSystemDirectoryHandle> parent, kj::Array<jsg::Ref<FileSystemHandle>> entries)
+        : parent(kj::mv(parent)),
+          entries(kj::mv(entries)) {}
+
+    void visitForGc(jsg::GcVisitor& visitor) {
+      visitor.visit(parent);
+      visitor.visitAll(entries);
+    }
+
+    JSG_MEMORY_INFO(IteratorState) {
+      tracker.trackField("parent", parent);
+      for (auto& entry: entries) {
+        tracker.trackField("entry", entry);
+      }
+    }
+  };
+
+ public:
+  FileSystemDirectoryHandle(jsg::USVString name, kj::Rc<workerd::Directory> inner);
+
+  kj::StringPtr getKind(jsg::Lock& js) override {
+    return "directory"_kj;
+  }
+
+  jsg::Promise<bool> isSameEntry(jsg::Lock& js, jsg::Ref<FileSystemHandle> other) override;
+
+  struct FileSystemGetFileOptions {
+    bool create = false;
+    JSG_STRUCT(create);
+  };
+
+  struct FileSystemGetDirectoryOptions {
+    bool create = false;
+    JSG_STRUCT(create);
+  };
+
+  struct FileSystemRemoveOptions {
+    bool recursive = false;
+    JSG_STRUCT(recursive);
+  };
+
+  jsg::Promise<jsg::Ref<FileSystemFileHandle>> getFileHandle(jsg::Lock& js,
+      jsg::USVString name,
+      jsg::Optional<FileSystemGetFileOptions> options,
+      const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception);
+
+  jsg::Promise<jsg::Ref<FileSystemDirectoryHandle>> getDirectoryHandle(jsg::Lock& js,
+      jsg::USVString name,
+      jsg::Optional<FileSystemGetDirectoryOptions> options,
+      const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception);
+
+  jsg::Promise<void> removeEntry(jsg::Lock& js,
+      jsg::USVString name,
+      jsg::Optional<FileSystemRemoveOptions> options,
+      const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception);
+
+  // TODO(node-fs): We are not currently implementing the resolve() method
+  // as defined in the spec. This is a bit tricky as it requires us to work
+  // backwards through the parent directories to find the path to the file.
+  // However, our file node's do not currently have a parent pointer, nor do
+  // they know through which path they are accessible through. Support for this
+  // is not critical so we likely won't implement this before we ship the feature
+  // but it is something we should consider doing in the future.
+  jsg::Promise<kj::Array<jsg::USVString>> resolve(
+      jsg::Lock& js, jsg::Ref<FileSystemHandle> possibleDescendant);
+
+  JSG_ITERATOR(
+      EntryIterator, entries, EntryIteratorType, IteratorState, iteratorNext<EntryIteratorType>);
+  JSG_ITERATOR(KeyIterator, keys, KeyIteratorType, IteratorState, iteratorNext<KeyIteratorType>);
+  JSG_ITERATOR(
+      ValueIterator, values, ValueIteratorType, IteratorState, iteratorNext<ValueIteratorType>);
+
+  void forEach(jsg::Lock& js,
+      jsg::Function<void(
+          jsg::USVString, jsg::Ref<FileSystemHandle>, jsg::Ref<FileSystemDirectoryHandle>)>
+          callback,
+      jsg::Optional<jsg::Value> thisArg);
+
+  JSG_RESOURCE_TYPE(FileSystemDirectoryHandle) {
+    JSG_INHERIT(FileSystemHandle);
+    JSG_METHOD(getFileHandle);
+    JSG_METHOD(getDirectoryHandle);
+    JSG_METHOD(removeEntry);
+    JSG_METHOD(resolve);
+    JSG_METHOD(entries);
+    JSG_METHOD(keys);
+    JSG_METHOD(values);
+    JSG_ITERABLE(entries);
+    JSG_METHOD(forEach);
+  }
+
+ private:
+  kj::Rc<workerd::Directory> inner;
+
+  template <typename Type>
+  static kj::Maybe<Type> iteratorNext(jsg::Lock& js, IteratorState& state) {
+    if (state.index >= state.entries.size()) {
+      return kj::none;
+    }
+
+    auto& entry = state.entries[state.index++];
+
+    if constexpr (kj::isSameType<Type, EntryIteratorType>()) {
+      return EntryType(js.accountedUSVString(entry->getName(js)), entry.addRef());
+    } else if constexpr (kj::isSameType<Type, KeyIteratorType>()) {
+      return js.accountedUSVString(entry->getName(js));
+    } else if constexpr (kj::isSameType<Type, ValueIteratorType>()) {
+      return entry.addRef();
+    } else {
+      static_assert(false, "invalid iterator type");
+    }
+  }
+};
+
+class FileSystemWritableFileStream: public WritableStream {
+  // TODO(node-fs): The spec defines FileSystemWritableFileStream objects such
+  // that any changes to the file are not actually committed until the stream
+  // is closed. We're not yet implementing this. All writes modify the source
+  // file in memory and would be immediately visible to other handles pointing
+  // to the same file. This should be updated before this feature is unflagged
+  // and made stable.
+ public:
+  struct State: public kj::Refcounted {
+    kj::Maybe<kj::Rc<workerd::File>> file;
+    size_t position = 0;
+    State(kj::Rc<workerd::File> file): file(kj::mv(file)) {}
+  };
+
+  FileSystemWritableFileStream(
+      kj::Own<WritableStreamController> controller, kj::Rc<State> sharedState);
+
+  static jsg::Ref<FileSystemWritableFileStream> constructor() = delete;
+
+  struct WriteParams {
+    kj::String type;  // one of: write, seek, truncate
+    jsg::Optional<double> size;
path: src/workerd/api/filesystem.h
user: login: jasnell
body: Use of `uint32_t` ought to be fine throughout since that'll easily fit anything that fits without our memory limits unless we decide to support large sparse files at any point (which I wouldn't expect)
created_at: 2025-05-09T00:37:32Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,333 @@
+#pragma once
+
+#include <workerd/api/streams/writable.h>
+#include <workerd/io/worker-fs.h>
+#include <workerd/jsg/iterator.h>
+#include <workerd/jsg/jsg.h>
+
+namespace workerd::api {
+
+class Blob;
+class File;
+
+// ======================================================================================
+// An implementation of the WHATWG Web File System API (https://fs.spec.whatwg.org/)
+// All of the classes in this part of the impl are defined by the spec.
+
+class FileSystemSyncAccessHandle;
+class FileSystemWritableFileStream;
+
+class FileSystemHandle: public jsg::Object {
+  // TODO(node-fs): The spec defines FileSystemHandle objects as being
+  // serializable, meaning that they should work with structured cloning.
+  // In workers, this would suggest also that they should work with jsrpc.
+  // We don't yet implement any form of serialization for these objects.
+  // Since each worker has its own file system, it might be a bit weird
+  // as the received file on the other side would not actually be in that
+  // destination worker's file system... that is, it would be an orphaned
+  // file handle. It would still likely be useful tho. As a follow up
+  // step, we will implement serialization/deserialization of these objects.
+ public:
+  FileSystemHandle(jsg::USVString name): name(kj::mv(name)) {}
+  const jsg::USVString& getName(jsg::Lock& js) {
+    return name;
+  }
+
+  virtual kj::StringPtr getKind(jsg::Lock& js) {
+    // Implemented by subclasses.
+    KJ_UNIMPLEMENTED("getKind() not implemented");
+  }
+  virtual jsg::Promise<bool> isSameEntry(jsg::Lock& js, jsg::Ref<FileSystemHandle> other) {
+    // Implemented by subclasses.
+    KJ_UNIMPLEMENTED("isSameEntry() not implemented");
+  }
+
+  JSG_RESOURCE_TYPE(FileSystemHandle) {
+    JSG_READONLY_PROTOTYPE_PROPERTY(kind, getKind);
+    JSG_READONLY_PROTOTYPE_PROPERTY(name, getName);
+    JSG_METHOD(isSameEntry);
+  }
+
+ private:
+  jsg::USVString name;
+};
+
+class FileSystemFileHandle: public FileSystemHandle {
+ public:
+  FileSystemFileHandle(jsg::USVString name, kj::Rc<workerd::File> inner);
+
+  kj::StringPtr getKind(jsg::Lock& js) override {
+    return "file"_kj;
+  }
+
+  jsg::Promise<bool> isSameEntry(jsg::Lock& js, jsg::Ref<FileSystemHandle> other) override;
+
+  struct FileSystemCreateWritableOptions {
+    bool keepExistingData = false;
+    JSG_STRUCT(keepExistingData);
+  };
+
+  jsg::Promise<jsg::Ref<File>> getFile(jsg::Lock& js);
+
+  jsg::Promise<jsg::Ref<FileSystemWritableFileStream>> createWritable(
+      jsg::Lock& js, jsg::Optional<FileSystemCreateWritableOptions> options);
+
+  jsg::Promise<jsg::Ref<FileSystemSyncAccessHandle>> createSyncAccessHandle(jsg::Lock& js);
+
+  JSG_RESOURCE_TYPE(FileSystemFileHandle) {
+    JSG_INHERIT(FileSystemHandle);
+    JSG_METHOD(getFile);
+    JSG_METHOD(createWritable);
+    JSG_METHOD(createSyncAccessHandle);
+  }
+
+ private:
+  kj::Rc<workerd::File> inner;
+};
+
+class FileSystemDirectoryHandle final: public FileSystemHandle {
+  // TODO(node-fs): Per the spec, FileSystemDirectoryHandler objects are
+  // expected to be async iterables. Here we implement it as a sync iterable.
+  // The effect ends up being largely the same but it's obviously better to
+  // use the asyn iterable API instead. We should implement this before we
+  // ship this feature.
+ public:
+  struct EntryType {
+    jsg::USVString key;
+    jsg::Ref<FileSystemHandle> value;
+    JSG_STRUCT(key, value);
+    EntryType(jsg::USVString key, jsg::Ref<FileSystemHandle> value)
+        : key(kj::mv(key)),
+          value(kj::mv(value)) {}
+  };
+
+ private:
+  using EntryIteratorType = EntryType;
+  using KeyIteratorType = jsg::USVString;
+  using ValueIteratorType = jsg::Ref<FileSystemHandle>;
+
+  struct IteratorState final {
+    jsg::Ref<FileSystemDirectoryHandle> parent;
+    kj::Array<jsg::Ref<FileSystemHandle>> entries;
+    uint index = 0;
+
+    IteratorState(
+        jsg::Ref<FileSystemDirectoryHandle> parent, kj::Array<jsg::Ref<FileSystemHandle>> entries)
+        : parent(kj::mv(parent)),
+          entries(kj::mv(entries)) {}
+
+    void visitForGc(jsg::GcVisitor& visitor) {
+      visitor.visit(parent);
+      visitor.visitAll(entries);
+    }
+
+    JSG_MEMORY_INFO(IteratorState) {
+      tracker.trackField("parent", parent);
+      for (auto& entry: entries) {
+        tracker.trackField("entry", entry);
+      }
+    }
+  };
+
+ public:
+  FileSystemDirectoryHandle(jsg::USVString name, kj::Rc<workerd::Directory> inner);
+
+  kj::StringPtr getKind(jsg::Lock& js) override {
+    return "directory"_kj;
+  }
+
+  jsg::Promise<bool> isSameEntry(jsg::Lock& js, jsg::Ref<FileSystemHandle> other) override;
+
+  struct FileSystemGetFileOptions {
+    bool create = false;
+    JSG_STRUCT(create);
+  };
+
+  struct FileSystemGetDirectoryOptions {
+    bool create = false;
+    JSG_STRUCT(create);
+  };
+
+  struct FileSystemRemoveOptions {
+    bool recursive = false;
+    JSG_STRUCT(recursive);
+  };
+
+  jsg::Promise<jsg::Ref<FileSystemFileHandle>> getFileHandle(jsg::Lock& js,
+      jsg::USVString name,
+      jsg::Optional<FileSystemGetFileOptions> options,
+      const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception);
+
+  jsg::Promise<jsg::Ref<FileSystemDirectoryHandle>> getDirectoryHandle(jsg::Lock& js,
+      jsg::USVString name,
+      jsg::Optional<FileSystemGetDirectoryOptions> options,
+      const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception);
+
+  jsg::Promise<void> removeEntry(jsg::Lock& js,
+      jsg::USVString name,
+      jsg::Optional<FileSystemRemoveOptions> options,
+      const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception);
+
+  // TODO(node-fs): We are not currently implementing the resolve() method
+  // as defined in the spec. This is a bit tricky as it requires us to work
+  // backwards through the parent directories to find the path to the file.
+  // However, our file node's do not currently have a parent pointer, nor do
+  // they know through which path they are accessible through. Support for this
+  // is not critical so we likely won't implement this before we ship the feature
+  // but it is something we should consider doing in the future.
+  jsg::Promise<kj::Array<jsg::USVString>> resolve(
+      jsg::Lock& js, jsg::Ref<FileSystemHandle> possibleDescendant);
+
+  JSG_ITERATOR(
+      EntryIterator, entries, EntryIteratorType, IteratorState, iteratorNext<EntryIteratorType>);
+  JSG_ITERATOR(KeyIterator, keys, KeyIteratorType, IteratorState, iteratorNext<KeyIteratorType>);
+  JSG_ITERATOR(
+      ValueIterator, values, ValueIteratorType, IteratorState, iteratorNext<ValueIteratorType>);
+
+  void forEach(jsg::Lock& js,
+      jsg::Function<void(
+          jsg::USVString, jsg::Ref<FileSystemHandle>, jsg::Ref<FileSystemDirectoryHandle>)>
+          callback,
+      jsg::Optional<jsg::Value> thisArg);
+
+  JSG_RESOURCE_TYPE(FileSystemDirectoryHandle) {
+    JSG_INHERIT(FileSystemHandle);
+    JSG_METHOD(getFileHandle);
+    JSG_METHOD(getDirectoryHandle);
+    JSG_METHOD(removeEntry);
+    JSG_METHOD(resolve);
+    JSG_METHOD(entries);
+    JSG_METHOD(keys);
+    JSG_METHOD(values);
+    JSG_ITERABLE(entries);
+    JSG_METHOD(forEach);
+  }
+
+ private:
+  kj::Rc<workerd::Directory> inner;
+
+  template <typename Type>
+  static kj::Maybe<Type> iteratorNext(jsg::Lock& js, IteratorState& state) {
+    if (state.index >= state.entries.size()) {
+      return kj::none;
+    }
+
+    auto& entry = state.entries[state.index++];
+
+    if constexpr (kj::isSameType<Type, EntryIteratorType>()) {
+      return EntryType(js.accountedUSVString(entry->getName(js)), entry.addRef());
+    } else if constexpr (kj::isSameType<Type, KeyIteratorType>()) {
+      return js.accountedUSVString(entry->getName(js));
+    } else if constexpr (kj::isSameType<Type, ValueIteratorType>()) {
+      return entry.addRef();
+    } else {
+      static_assert(false, "invalid iterator type");
+    }
+  }
+};
+
+class FileSystemWritableFileStream: public WritableStream {
+  // TODO(node-fs): The spec defines FileSystemWritableFileStream objects such
+  // that any changes to the file are not actually committed until the stream
+  // is closed. We're not yet implementing this. All writes modify the source
+  // file in memory and would be immediately visible to other handles pointing
+  // to the same file. This should be updated before this feature is unflagged
+  // and made stable.
+ public:
+  struct State: public kj::Refcounted {
+    kj::Maybe<kj::Rc<workerd::File>> file;
+    size_t position = 0;
+    State(kj::Rc<workerd::File> file): file(kj::mv(file)) {}
+  };
+
+  FileSystemWritableFileStream(
+      kj::Own<WritableStreamController> controller, kj::Rc<State> sharedState);
+
+  static jsg::Ref<FileSystemWritableFileStream> constructor() = delete;
+
+  struct WriteParams {
+    kj::String type;  // one of: write, seek, truncate
+    jsg::Optional<double> size;
path: src/workerd/api/filesystem.h
user: login: danlapid
body: Yeah going with uint32 makes sense to me considering our memory limitations 
Double feels wrong
created_at: 2025-05-09T00:38:17Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,442 @@
+#include "filesystem.h"
+
+#include "blob.h"
+
+#include <workerd/api/streams/standard.h>
+
+namespace workerd::api {
+
+// =======================================================================================
+// Implementation of the Web File System API
+
+namespace {
+bool isValidFileName(kj::StringPtr name) {
path: src/workerd/api/filesystem.c++
user: login: anonrig
body: ```suggestion
constexpr bool isValidFileName(kj::StringPtr name) {
```

created_at: 2025-05-09T01:47:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -749,4 +749,10 @@ struct CompatibilityFlags @0x8f8c1b68151b6cef {
       $compatEnableDate("2025-05-19")
       $compatDisableFlag("disable_navigator_language");
   # Enables Navigator.language API.
+
+  webFileSystem @87 :Bool
+      $compatEnableFlag("enable_web_file_system")
+      $experimental;
+  # Enables the experimental Web File System API.
path: src/workerd/io/compatibility-date.capnp
user: login: anonrig
body: Can you add a link to the spec?
created_at: 2025-05-09T01:49:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,442 @@
+#include "filesystem.h"
+
+#include "blob.h"
+
+#include <workerd/api/streams/standard.h>
+
+namespace workerd::api {
+
+// =======================================================================================
+// Implementation of the Web File System API
+
+namespace {
+bool isValidFileName(kj::StringPtr name) {
+  return name.size() > 0 && name != "."_kj && name != ".."_kj && name.find("/"_kj) == kj::none;
+}
+}  // namespace
+
+jsg::Promise<jsg::Ref<FileSystemDirectoryHandle>> StorageManager::getDirectory(
+    jsg::Lock& js, const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception) {
+  KJ_IF_SOME(vfs, workerd::VirtualFileSystem::tryGetCurrent(js)) {
+    return js.resolvedPromise(
+        js.alloc<FileSystemDirectoryHandle>(jsg::USVString(), vfs.getRoot(js)));
+  }
+
+  auto ex = js.domException(kj::str("SecurityError"), kj::str("No current virtual file system"));
+  return js.rejectedPromise<jsg::Ref<FileSystemDirectoryHandle>>(exception.wrap(js, kj::mv(ex)));
+}
+
+FileSystemDirectoryHandle::FileSystemDirectoryHandle(
+    jsg::USVString name, kj::Rc<workerd::Directory> inner)
+    : FileSystemHandle(kj::mv(name)),
+      inner(kj::mv(inner)) {}
+
+jsg::Promise<bool> FileSystemDirectoryHandle::isSameEntry(
+    jsg::Lock& js, jsg::Ref<FileSystemHandle> other) {
+  KJ_IF_SOME(other, kj::dynamicDowncastIfAvailable<FileSystemDirectoryHandle>(*other)) {
+    return js.resolvedPromise(other.inner.get() == inner.get());
+  }
+  return js.resolvedPromise(false);
+}
+
+jsg::Promise<jsg::Ref<FileSystemFileHandle>> FileSystemDirectoryHandle::getFileHandle(jsg::Lock& js,
+    jsg::USVString name,
+    jsg::Optional<FileSystemGetFileOptions> options,
+    const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception) {
+  JSG_REQUIRE(isValidFileName(name), TypeError, "Invalid file name");
+  kj::Maybe<FsType> createAs;
+  KJ_IF_SOME(opts, options) {
+    if (opts.create) createAs = FsType::FILE;
+  }
+  KJ_IF_SOME(node,
+      inner->tryOpen(js, kj::Path({name}),
path: src/workerd/api/filesystem.c++
user: login: danlapid
body: I regret the original decision to let tryOpen return `kj::Maybe<kj::OneOf<kj::Rc<File>, kj::Rc<Directory>, kj::Rc<SymbolicLink>>>` with the comment 
```
  // If the path identifies a symbolic link, the symbolic link will be resolved
  // and the target, if it exists, will be returned. If the target does not exist,
  // kj::none will be returned.
```
Is there any reason not to amend that to return `kj::Maybe<kj::OneOf<kj::Rc<File>, kj::Rc<Directory>>>` instead and skip all of the 
```
      KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
        KJ_UNREACHABLE;
      }
```
These are counter-intuitive and confusing without real value.
created_at: 2025-05-09T09:38:11Z
author_association: COLLABORATOR
diff_hunk: @@ -749,4 +749,10 @@ struct CompatibilityFlags @0x8f8c1b68151b6cef {
       $compatEnableDate("2025-05-19")
       $compatDisableFlag("disable_navigator_language");
   # Enables Navigator.language API.
+
+  webFileSystem @87 :Bool
+      $compatEnableFlag("enable_web_file_system")
+      $experimental;
+  # Enables the experimental Web File System API.
path: src/workerd/io/compatibility-date.capnp
user: login: jasnell
body: There's a link to the spec in the filesystem.h
created_at: 2025-05-09T13:44:51Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,442 @@
+#include "filesystem.h"
+
+#include "blob.h"
+
+#include <workerd/api/streams/standard.h>
+
+namespace workerd::api {
+
+// =======================================================================================
+// Implementation of the Web File System API
+
+namespace {
+bool isValidFileName(kj::StringPtr name) {
+  return name.size() > 0 && name != "."_kj && name != ".."_kj && name.find("/"_kj) == kj::none;
+}
+}  // namespace
+
+jsg::Promise<jsg::Ref<FileSystemDirectoryHandle>> StorageManager::getDirectory(
+    jsg::Lock& js, const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception) {
+  KJ_IF_SOME(vfs, workerd::VirtualFileSystem::tryGetCurrent(js)) {
+    return js.resolvedPromise(
+        js.alloc<FileSystemDirectoryHandle>(jsg::USVString(), vfs.getRoot(js)));
+  }
+
+  auto ex = js.domException(kj::str("SecurityError"), kj::str("No current virtual file system"));
+  return js.rejectedPromise<jsg::Ref<FileSystemDirectoryHandle>>(exception.wrap(js, kj::mv(ex)));
+}
+
+FileSystemDirectoryHandle::FileSystemDirectoryHandle(
+    jsg::USVString name, kj::Rc<workerd::Directory> inner)
+    : FileSystemHandle(kj::mv(name)),
+      inner(kj::mv(inner)) {}
+
+jsg::Promise<bool> FileSystemDirectoryHandle::isSameEntry(
+    jsg::Lock& js, jsg::Ref<FileSystemHandle> other) {
+  KJ_IF_SOME(other, kj::dynamicDowncastIfAvailable<FileSystemDirectoryHandle>(*other)) {
+    return js.resolvedPromise(other.inner.get() == inner.get());
+  }
+  return js.resolvedPromise(false);
+}
+
+jsg::Promise<jsg::Ref<FileSystemFileHandle>> FileSystemDirectoryHandle::getFileHandle(jsg::Lock& js,
+    jsg::USVString name,
+    jsg::Optional<FileSystemGetFileOptions> options,
+    const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception) {
+  JSG_REQUIRE(isValidFileName(name), TypeError, "Invalid file name");
+  kj::Maybe<FsType> createAs;
+  KJ_IF_SOME(opts, options) {
+    if (opts.create) createAs = FsType::FILE;
+  }
+  KJ_IF_SOME(node,
+      inner->tryOpen(js, kj::Path({name}),
path: src/workerd/api/filesystem.c++
user: login: jasnell
body: These are *currently* `KJ_UNREACHABLE` because we're not doing anything with symlinks yet. Once we fill out the full node:fs implementation these may not remain `KJ_UNREACHABLE`. Once the implementation is done I'll be going back and revisiting these.
created_at: 2025-05-09T14:15:30Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-08T22:29:26Z
body: The generated output of `@cloudflare/workers-types` matches the snapshot in `types/generated-snapshot` :tada:
<!-- Sticky Pull Request Comment -->
author_association: NONE
user: login: anonrig
created_at: 2025-05-09T21:05:47Z
body: @npaun I think we should enable relevant WPT as well
author_association: CONTRIBUTOR
user: login: jasnell
created_at: 2025-05-09T21:14:14Z
body: Making a few more small tweaks before this is fully ready. Namely taking care of a couple of the outstanding todos such as making the dir handle an async iterable rather than a sync iterable. And yeah, once this lands we'll need to look at the web platform tests. It'll be kept experimental until we're sure those are ready to go. We likely won't be able to be 100% compatible so it might take some iteration on the tests to lock down.
author_association: COLLABORATOR