How to Manually Implement Workspace Push and Pull Operations in Cloudflare Computer

Manually synchronize local SQLite state with remote backends using the Workspace.push() and Workspace.pull() methods from the @cloudflare/computer package, which serialize changes through a FIFO queue and handle transport errors automatically.

The cloudflare/computer repository provides a distributed computing environment where the Workspace class acts as the host-side façade for managing local SQLite storage and remote execution backends. When you need fine-grained control over state synchronization, you can manually trigger workspace push and pull operations instead of relying on automatic command-execution brackets. This guide explains the internal mechanics and provides practical implementations using the actual source code from packages/computer/src/workspace.ts.

Workspace Class Architecture

The Workspace class defined in packages/computer/src/workspace.ts maintains a local SQLite database through a Database instance and coordinates with remote backends configured in WorkspaceOptions.backends. According to the source implementation, synchronization is explicit: you must invoke push() to ship local changes and pull() to fetch remote updates.

Before any data transfer occurs, the private #resolveBackendId(id) method normalizes the optional backend identifier, defaulting to the first backend in the configuration or undefined for filesystem-only workspaces. All operations pass through #serialize, which maintains a per-backend FIFO queue (#mutationTails) to ensure that concurrent pushes to the same backend execute sequentially while operations against different backends proceed in parallel.

The Push and Pull Methods

The public API exposes two primary methods for manual synchronization, both accepting an optional backend identifier and returning specific result types.

Pushing Local Changes

The push(id?) method ships every local mutation since the previous push to the specified backend. As implemented in packages/computer/src/workspace.ts (lines 45-70), the method:

  • Resolves the backend identifier via #resolveBackendId (lines 31-46)
  • Acquires a cached handle through #handleFor(id), which lazily connects and reconciles watermarks on first use (lines 99-130)
  • Wraps the RPC call with #runWithInvalidation to invalidate cached handles on transport errors (lines 84-96)
  • Invokes pushOnce(this.#db, handle.rpc.sync, resolvedId) from the @cloudflare/computer-rpc package (line 61)

The method returns the number of entries shipped as a number.

Pulling Remote Changes

The pull(id?) method fetches and applies remote changes to the local SQLite store. Located at lines 71-74 in packages/computer/src/workspace.ts, this method:

  • Follows the same serialization and handle acquisition pattern as push
  • Calls pullOnce(this.#db, handle.rpc.sync, resolvedId) to perform the actual data transfer (line 40)
  • Returns an ApplyResult object with { applied, skipped } properties indicating how many entries were applied versus skipped

Implementing Manual Synchronization

When working outside the automatic CommandExecutor.exec wrapper—which automatically calls push() before and pull() after command execution—you can manually control synchronization points.

Basic Push and Pull Workflow

The following example demonstrates manual synchronization within a Cloudflare Worker or Durable Object:

import { Workspace } from "@cloudflare/computer";

// Construct workspace with R2 backend
const ws = new Workspace({
  storage: ctx.storage,
  backends: [{ id: "r2", type: "r2", callable: true, connect: /* ... */ }],
});

// Write to local SQLite store
await ws.fs.writeFile("/workspace/hello.txt", "Hello, Cloudflare!");

// Push changes to default backend (R2)
const pushed = await ws.push();
console.log(`Pushed ${pushed} entries to R2`);

// Later, fetch remote updates
const result = await ws.pull();
console.log(`Pulled ${result.applied} entries (skipped ${result.skipped.length})`);

Targeting Specific Backends

When configuring multiple backends, explicitly target them by passing the identifier:

// Push to specific backend
await ws.push("r2");

// Pull from different backend
await ws.pull("cloudflare-worker");

Handling Retry Logic

For resilient synchronization, use the SyncRetryScheduler to handle pending pulls that failed post-command:

const status = await ws.retryPendingSync("r2");

switch (status.status) {
  case "complete":
    console.log(`Recovered ${status.applied} entries`);
    break;
  case "pending":
    console.log(`Retry scheduled, attempt ${status.attempt}`);
    break;
  case "exhausted":
    console.error("Max retry attempts reached");
    break;
}

The retry logic implementation resides around lines 84-124 in the workspace source file.

Low-Level RPC Implementation

The actual wire protocol uses Cap'n Proto RPC as defined in docs/08_capnweb_interface.md. The helper functions pushOnce and pullOnce reside in packages/computer-rpc/driver.ts and implement the low-level synchronization contract. These functions interact with the BackendHandle returned by #handleFor, which manages the RPC interface defined in packages/rpc/src/interface.ts.

The WorkspaceFilesystem implementation in packages/dofs/src/filesystem.ts directly mutates the local SQLite store, making those changes available for the next push() operation.

Summary

  • The Workspace class in packages/computer/src/workspace.ts provides explicit push() and pull() methods for manual synchronization
  • Both methods use private #serialize to maintain per-backend FIFO queues and #runWithInvalidation for transport-error recovery
  • push() returns the number of entries shipped, while pull() returns an ApplyResult with applied and skipped counts
  • Operations default to the first configured backend unless a specific id is provided via #resolveBackendId
  • Low-level RPC calls delegate to pushOnce and pullOnce in the @cloudflare/computer-rpc package using the Cap'n Proto interface defined in docs/08_capnweb_interface.md

Frequently Asked Questions

What is the difference between automatic and manual workspace push and pull operations?

Automatic synchronization occurs within CommandExecutor.exec, which wraps command execution with push() before and pull() after. Manual implementation gives you explicit control over when state diverges and converges, essential for batch operations or specific consistency requirements where you cannot rely on implicit synchronization brackets.

How does the Workspace class handle concurrent push operations?

The #serialize method maintains a per-backend FIFO queue (#mutationTails) that ensures concurrent pushes to the same backend execute sequentially. Pushes to different backends proceed in parallel, maximizing throughput while maintaining consistency per backend as implemented in lines 110-129 of packages/computer/src/workspace.ts.

What happens if a network error occurs during push or pull?

The #runWithInvalidation wrapper (lines 84-96) catches transport-level errors and invalidates the cached BackendHandle. This ensures the next operation triggers #handleFor to establish a fresh connection, automatically recovering from transient network failures without manual intervention.

Can I use workspace push and pull operations without a remote backend?

Yes. If #resolveBackendId returns undefined (no backend configured or filesystem-only workspace), the operations handle local state appropriately. The WorkspaceFilesystem in packages/dofs/src/filesystem.ts manages local SQLite mutations regardless of remote synchronization status, making the workspace functional for local-only scenarios.

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 →