Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@
"vite": "^7.1.2",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4",
"vitest-fetch-mock": "^0.4.5"
"vitest-fetch-mock": "^0.4.5",
"wrangler": "^4.42.0"
},
"packageManager": "yarn@4.2.2",
"engines": {
Expand Down
1 change: 1 addition & 0 deletions packages/cf-worker-example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.wrangler
182 changes: 182 additions & 0 deletions packages/cf-worker-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# Cloudflare Worker with Ocap Kernel Example

This package demonstrates running an Ocap Kernel in a Cloudflare Worker with D1 database persistence and a counter vat using data URI bundles.

## What This Example Shows

This example demonstrates:

1. **Kernel Initialization**: Starting an Ocap Kernel in a Cloudflare Worker environment
2. **D1 Persistence**: Using Cloudflare's D1 database for persistent storage with a write-behind cache
3. **Vat Bundles as Data URIs**: Embedding vat bundles directly in the worker code for fast loading
4. **Vat Launch and Messaging**: Launching a vat and calling methods on it using the kernel API
5. **State Persistence**: Using baggage for vat state that persists across requests and restarts

## How It Works

The worker:

1. Initializes a D1 database backend with write-behind caching
2. Creates a MessageChannel to communicate with the kernel
3. Starts the kernel with the D1 database
4. Launches a counter vat using an embedded data URI bundle
5. Calls methods on the counter vat (`getCount`, `increment`)
6. Uses `kunser()` to deserialize results from the kernel's CapData format
7. Returns the vat's state and demonstrates persistence

## Running the Example

```bash
# From the repo root, first build the CLI tools
yarn build

# Then navigate to this package
cd packages/cf-worker-example

# Build the worker (bundles the vat and embeds it as a data URI)
yarn build

# Start the development server
yarn dev

# In another terminal, test it
curl http://localhost:8788
```

Each request will:
- Launch the counter vat (on first request) or reuse it (on subsequent requests)
- Get the current count from vat state (stored in baggage)
- Increment the counter
- Return the before/after counts, demonstrating persistence

## Response Format

```json
{
"bootstrap": "CFWorkerCounter initialized with count: 0",
"counterRef": "ko3",
"vatCountBefore": 0,
"vatCountAfter": 1,
"requestCount": 5,
"message": "Counter vat launched and incremented!",
"timestamp": "2025-10-04T12:34:56.789Z"
}
```

The `vatCountAfter` will increment with each request, persisting in D1 via baggage. The `requestCount` tracks total requests to the worker.

## Architecture

```
┌─────────────────────────────────────────────────────────┐
│ Cloudflare Worker │
│ │
│ ┌──────────┐ ┌────────────────┐ │
│ │ Fetch │────────▶│ Kernel │ │
│ │ Handler │ │ (Direct API) │ │
│ └──────────┘ └────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Counter Vat │ │
│ │ (Data URI) │ │
│ │ - increment() │ │
│ │ - getCount() │ │
│ └──────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────┐ │
│ │ D1 Database │ │
│ │ (Write-Behind Cache) │ │
│ │ - Kernel state │ │
│ │ - Vat state (baggage) │ │
│ │ - Request counter │ │
│ └────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```

## Key Components

### Counter Vat (`src/counter-vat.js`)
A simple vat that maintains a counter in baggage (persistent storage):
- `bootstrap()` - Called when the vat is first launched
- `increment(amount)` - Increments the counter
- `getCount()` - Returns the current count
- `reset()` - Resets the counter to 0

### Bundle Process
1. **Bundle**: `yarn ocap bundle src/counter-vat.js` creates `counter-vat.bundle`
2. **Embed**: `generate-bundles.mjs` converts the bundle to a base64 data URI
3. **Import**: Worker imports the data URI from `bundles.ts`

### Kernel API Usage

```typescript
import { kunser, makeKernelStore } from '@metamask/ocap-kernel';

// Launch a subcluster with the vat
const bootstrapResult = await kernel.launchSubcluster({
bootstrap: 'counter',
vats: {
counter: {
bundleSpec: counterBundleUri, // Data URI
parameters: { name: 'CFWorkerCounter' }
}
}
});

// Get the bootstrap return value
const message = kunser(bootstrapResult); // "CFWorkerCounter initialized..."

// Get the root object reference
const kernelStore = makeKernelStore(database);
const rootRef = kernelStore.getRootObject('v1'); // First vat is 'v1'

// Call methods on the vat
const countResult = await kernel.queueMessage(rootRef, 'getCount', []);
const count = kunser(countResult); // Deserialize to get actual number

const incrementResult = await kernel.queueMessage(rootRef, 'increment', [1]);
const newCount = kunser(incrementResult); // Get new count value
```

## Key Patterns

### 1. Data URI Bundles
Instead of fetching bundles from HTTP, we embed them directly:
- **Pros**: No network requests, instant loading, self-contained
- **Cons**: Larger bundle size (~950KB base64-encoded)
- **Use case**: Small vats, fast cold starts

### 2. Direct Kernel API
We call kernel methods directly (not through JSON-RPC):
```typescript
await kernel.launchSubcluster(config); // Direct call
await kernel.queueMessage(ref, method, args); // Direct call
```

### 3. Result Deserialization
Always use `kunser()` to extract actual values from CapData:
```typescript
const rawResult = await kernel.queueMessage(...);
const actualValue = kunser(rawResult); // Get the real JavaScript value
```

### 4. Getting Root Object References
Use `makeKernelStore` to look up root object refs by vat ID:
```typescript
const kernelStore = makeKernelStore(database);
const rootRef = kernelStore.getRootObject('v1'); // First vat
```

## Next Steps

To extend this example:

1. **Add more vat methods**: Extend `counter-vat.js` with new functionality
2. **Multiple vats**: Launch multiple vats in the subcluster and have them communicate
3. **HTTP bundles**: Switch from data URIs to R2 or CDN-hosted bundles for larger vats
4. **Request routing**: Use URL parameters to call different vat methods
5. **Web UI**: Create a simple UI that interacts with the vat

See the [kernel-test package](../kernel-test/src/) for more examples of vats with persistence, communication, and complex state management.
28 changes: 28 additions & 0 deletions packages/cf-worker-example/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@ocap/cf-worker-example",
"version": "0.0.0",
"private": true,
"description": "Example Cloudflare Worker using the Ocap Kernel",
"type": "module",
"scripts": {
"bundle:vats": "node ../cli/dist/app.mjs bundle src/counter-vat.js && node scripts/generate-bundles.mjs",
"build": "yarn bundle:vats && ts-bridge --project tsconfig.build.json --no-references --clean",
"dev": "wrangler dev",
"deploy": "wrangler deploy"
},
"dependencies": {
"@metamask/kernel-store": "workspace:^",
"@metamask/kernel-utils": "workspace:^",
"@metamask/logger": "workspace:^",
"@metamask/ocap-kernel": "workspace:^",
"@metamask/streams": "workspace:^",
"@ocap/cf-worker": "workspace:^"
},
"devDependencies": {
"@ocap/cli": "workspace:^",
"@ts-bridge/cli": "^0.6.3",
"@ts-bridge/shims": "^0.1.1",
"typescript": "~5.8.2",
"wrangler": "^4.42.0"
}
}
9 changes: 9 additions & 0 deletions packages/cf-worker-example/scripts/bundle-vats.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/bash
set -e

# Bundle the counter vat
echo "Bundling counter vat..."
yarn ocap bundle src/counter-vat.js

echo "Vat bundling complete!"

50 changes: 50 additions & 0 deletions packages/cf-worker-example/scripts/generate-bundles.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env node
import { readFileSync, writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const projectRoot = join(__dirname, '..');

/**
* Convert a bundle object to a data URI.
*
* @param {object} bundle - The bundle object to convert.
* @returns {string} A data URI containing the bundle.
*/
function bundleToDataUri(bundle) {
const bundleJson = JSON.stringify(bundle);
const base64 = Buffer.from(bundleJson).toString('base64');
return `data:application/json;base64,${base64}`;
}

console.log('Generating bundles.ts from bundle files...');

// Read the counter vat bundle
const counterBundlePath = join(projectRoot, 'src', 'counter-vat.bundle');
const counterBundle = JSON.parse(readFileSync(counterBundlePath, 'utf-8'));
const counterBundleUri = bundleToDataUri(counterBundle);

// Generate the TypeScript file
const tsContent = `/**
* Embedded vat bundles as data URIs.
* This file is auto-generated by scripts/generate-bundles.mjs during the build process.
* DO NOT EDIT MANUALLY.
*/

/**
* Counter vat bundle as a data URI.
* Bundle size: ${JSON.stringify(counterBundle).length} bytes
* Data URI size: ${counterBundleUri.length} bytes
*/
export const counterBundleUri = ${JSON.stringify(counterBundleUri)};
`;

// Write the generated file
const outputPath = join(projectRoot, 'src', 'bundles.ts');
writeFileSync(outputPath, tsContent, 'utf-8');

console.log(`Generated ${outputPath}`);
console.log(` Bundle size: ${JSON.stringify(counterBundle).length} bytes`);
console.log(` Data URI size: ${counterBundleUri.length} bytes`);

12 changes: 12 additions & 0 deletions packages/cf-worker-example/src/bundles.ts

Large diffs are not rendered by default.

71 changes: 71 additions & 0 deletions packages/cf-worker-example/src/counter-vat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { makeDefaultExo } from '@metamask/kernel-utils/exo';

/**
* Build function for a simple counter vat.
*
* @param {object} _vatPowers - Special powers granted to this vat.
* @param {object} parameters - Initialization parameters from the vat's config.
* @param {object} baggage - Root of vat's persistent state.
* @returns {object} The root object for the new vat.
*/
export function buildRootObject(_vatPowers, parameters, baggage) {
const { name = 'Counter' } = parameters;

// Initialize counter in baggage if not present
if (!baggage.has('count')) {
baggage.init('count', 0);
console.log(`${name}: Initialized counter to 0`);
} else {
console.log(`${name}: Counter already exists with value ${baggage.get('count')}`);
}

return makeDefaultExo('root', {
/**
* Bootstrap method called when the vat is first launched.
*
* @returns {string} Bootstrap completion message.
*/
bootstrap() {
const count = baggage.get('count');
console.log(`${name}: bootstrap() - current count: ${count}`);
return `${name} initialized with count: ${count}`;
},

/**
* Increment the counter and return the new value.
*
* @param {number} amount - Amount to increment by (default: 1).
* @returns {number} The new counter value.
*/
increment(amount = 1) {
const oldCount = baggage.get('count');
const newCount = oldCount + amount;
baggage.set('count', newCount);
console.log(`${name}: Incremented from ${oldCount} to ${newCount}`);
return newCount;
},

/**
* Get the current counter value.
*
* @returns {number} The current counter value.
*/
getCount() {
const count = baggage.get('count');
console.log(`${name}: getCount() - current count: ${count}`);
return count;
},

/**
* Reset the counter to zero.
*
* @returns {number} The new counter value (0).
*/
reset() {
baggage.set('count', 0);
console.log(`${name}: Counter reset to 0`);
return 0;
},
});
}

17 changes: 17 additions & 0 deletions packages/cf-worker-example/src/lockdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* global lockdown */
import 'ses';
import '@endo/eventual-send/shim.js';

try {
lockdown({
consoleTaming: 'unsafe',
errorTaming: 'unsafe',
overrideTaming: 'severe',
domainTaming: 'unsafe',
stackFiltering: 'concise',
});
} catch (err) {
// eslint-disable-next-line no-console
console.error('SES lockdown failed (example):', err);
throw err;
}
Loading
Loading