How Files Are Read and Written Through the Inode-Keyed Cache with Buffered Writes in Cloudflare Computer

The inode-keyed cache stores pending writes in memory keyed by file inode, allowing reads to return uncommitted data from buffer.buf while deferring persistent storage updates until the buffer is released.

In the Cloudflare Computer project, the virtual filesystem (VFS) uses an SQLite-backed storage layer where every file and directory is identified by a unique inode. When applications open files for writing, the system creates an inode-keyed cache entry that stages bytes in memory before flushing to disk. This buffer architecture ensures that reads immediately reflect recent writes without requiring expensive synchronous database operations.

How the Inode-Keyed Cache Structures Buffered Data

The buffer implementation centers on a dirty flag that tracks whether uncommitted data exists for a specific inode. When openWriteBufferSync (or the async openWriteBufferForCreateSync) initializes a write session, it registers a buffer structure containing buffer.buf (the raw bytes), buffer.size (the logical file size), and metadata including the target path (dirtyPath) and permissions mode.

Each buffer is retrieved via getWriteBuffer(db, inode), which checks the cache for existing entries before falling back to the persistent vfs_chunks table. This lookup pattern ensures that operations always access the most recent data—whether committed or pending.

Reading from the Write Buffer

Read operations prioritize the inode-keyed cache over persistent storage when the dirty flag is set. In packages/dofs/src/fs/readFile.ts (lines 74‑76), the readRangeSync function first queries the write buffer for the target inode. If a dirty buffer exists, the read returns data directly from buffer.buf limited to buffer.size, bypassing the SQLite vfs_chunks table entirely.

If no dirty buffer exists for the inode, the read falls back to the standard VFS tables. This dual-path approach guarantees consistency: callers always see the latest writes, including those not yet flushed to the database. The statSync and readdirSync implementations in packages/dofs/src/fs/stat.ts (lines 71‑72) apply the same logic, reporting buffer.size from the dirty buffer rather than the persisted node size stored in vfs_nodes.

The Buffered Write Lifecycle

The complete lifecycle of a buffered write spans five distinct phases, each implemented in packages/dofs/src/fs/writeFile.ts:

  1. Opening the Buffer – openWriteBufferSync creates a new buffer structure keyed to the file's inode and registers it in the cache.

  2. Appending Data – writeRangeSync writes incoming bytes into buffer.buf at the specified offset. The function invokes ensureBufferCapacity to expand the underlying array when necessary, then marks the buffer as dirty and updates buffer.size to reflect the new logical end-of-file.

  3. Metadata Reporting – While the buffer remains dirty, calls to statSync return the buffered size rather than querying the vfs_nodes table, ensuring that file explorers and applications see the correct pending size.

  4. Pre-Flush Reading – Any readRangeSync calls during this window serve data directly from buffer.buf, providing immediate read-after-write consistency without database round-trips.

  5. Committing Changes – When the file handle closes or fsync triggers, releaseWriteBufferSync executes the materialization sequence (lines 803‑830). This transaction inserts the buffered bytes into vfs_chunks, updates the inode's size in vfs_nodes, and clears the dirty flag, atomically promoting the cached data to persistent storage.

FUSE Integration and Path Semantics

When the VFS is exposed through the FUSE shim in packages/computerd/src/fuse/driver.ts, the driver respects the inode-keyed cache during path-based operations. The getattr implementation (lines 754‑760) explicitly checks for a dirty buffer via the inode-to-buffer mapping, reporting the buffered size to the operating system before the data commits to SQLite.

This integration preserves write semantics across file operations. The driver's rename test in packages/computerd/src/fuse/driver.test.ts (line 339) verifies that buffered bytes move with the file when its path changes, ensuring that hard links share the same staged data. The test suite in packages/dofs/src/fs/writeBuffer.test.ts (lines 71‑83) confirms that multiple paths referencing the same inode access identical buffered content.

Code Example: Buffered Write and Read

import { openDatabase, getWriteBuffer } from '@cloudflare/computer';

// Initialize database connection
const db = await openDatabase();

// Create a buffered write session for a new file
p.openWriteBufferForCreateSync("/example.txt", { mode: 0o644 });

// Write data that exists only in the inode-keyed cache
p.writeRangeSync("/example.txt", Buffer.from("hello world"), 0);

// Read returns data from the dirty buffer, not SQLite
const data = p.readRangeSync("/example.txt", 0, 11);
console.log(new TextDecoder().decode(data)); // → "hello world"

// Commit to vfs_chunks and update vfs_nodes
p.releaseWriteBufferSync("/example.txt");

// Now stat returns the persisted size
console.log(p.statSync("/example.txt").size); // 11

Summary

  • The inode-keyed cache maintains an in-memory buffer.buf for each dirty inode, enabling fast read-after-write performance without SQLite transactions.
  • Read operations check getWriteBuffer(db, inode) first; if the dirty flag is set, data is served directly from the buffer rather than vfs_chunks.
  • Write operations stage bytes via writeRangeSync, which expands capacity via ensureBufferCapacity and updates buffer.size for accurate stat reporting.
  • Persistence occurs only during releaseWriteBufferSync, which atomically writes to vfs_chunks, updates vfs_nodes, and clears the dirty flag (lines 803‑830 in writeFile.ts).
  • The FUSE driver respects buffered writes in getattr (lines 754‑760) and preserves buffer associations across renames and hard links.

Frequently Asked Questions

What happens when reading a file that has pending buffered writes?

The read operation queries getWriteBuffer(db, inode) to locate any dirty buffer associated with the file's inode. If found, readRangeSync returns bytes directly from buffer.buf up to buffer.size, ensuring the reader sees the most recent uncommitted data. Only when no dirty buffer exists does the system fall back to the persisted vfs_chunks table.

How does the system report file size while writes are buffered?

Calls to statSync and the FUSE driver's getattr check the inode-keyed cache before querying the vfs_nodes table. When a dirty buffer exists, these functions report buffer.size rather than the persisted size, allowing applications to see the logical end-of-file including pending writes.

When are buffered bytes actually written to the database?

Buffered data commits to persistent storage only when releaseWriteBufferSync is invoked—typically when the file descriptor closes or an explicit fsync occurs. This function executes a transaction that inserts chunks into vfs_chunks, updates the inode size in vfs_nodes, and clears the dirty flag, as implemented in packages/dofs/src/fs/writeFile.ts at lines 803‑830.

Yes. Because the cache is keyed by inode rather than path, multiple directory entries referencing the same inode share a single buffer structure. The test suite in packages/dofs/src/fs/writeBuffer.test.ts (lines 71‑83) verifies that writes through one hard link are immediately visible when reading through another link to the same inode.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →