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
#runWithInvalidationto invalidate cached handles on transport errors (lines 84-96) - Invokes
pushOnce(this.#db, handle.rpc.sync, resolvedId)from the@cloudflare/computer-rpcpackage (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
ApplyResultobject 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
Workspaceclass inpackages/computer/src/workspace.tsprovides explicitpush()andpull()methods for manual synchronization - Both methods use private
#serializeto maintain per-backend FIFO queues and#runWithInvalidationfor transport-error recovery push()returns the number of entries shipped, whilepull()returns anApplyResultwithappliedandskippedcounts- Operations default to the first configured backend unless a specific
idis provided via#resolveBackendId - Low-level RPC calls delegate to
pushOnceandpullOncein the@cloudflare/computer-rpcpackage using the Cap'n Proto interface defined indocs/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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →