From 02949fb40a6716fbe374d0077b144188f4fd8232 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 10 Nov 2025 13:50:31 -0800 Subject: [PATCH] updates to worker --- .github/workflows/deploy-worker.yml | 36 ++++++- DATA_SAFETY_VERIFICATION.md | 145 ++++++++++++++++++++++++++++ worker/worker.ts | 19 ++++ wrangler.toml | 11 ++- 4 files changed, 205 insertions(+), 6 deletions(-) create mode 100644 DATA_SAFETY_VERIFICATION.md diff --git a/.github/workflows/deploy-worker.yml b/.github/workflows/deploy-worker.yml index 2a51a2f..4624c36 100644 --- a/.github/workflows/deploy-worker.yml +++ b/.github/workflows/deploy-worker.yml @@ -3,8 +3,18 @@ name: Deploy Worker on: push: branches: - - main # or 'production' depending on your branch name + - main # Production deployment + - 'automerge/**' # Dev deployment for automerge branches (matches automerge/*, automerge/**/*, etc.) workflow_dispatch: # Allows manual triggering from GitHub UI + inputs: + environment: + description: 'Environment to deploy to' + required: true + default: 'dev' + type: choice + options: + - dev + - production jobs: deploy: @@ -22,7 +32,19 @@ jobs: - name: Install Dependencies run: npm ci - - name: Deploy to Cloudflare Workers + - name: Determine Environment + id: env + run: | + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + echo "environment=${{ github.event.inputs.environment }}" >> $GITHUB_OUTPUT + elif [ "${{ github.ref }}" == "refs/heads/main" ]; then + echo "environment=production" >> $GITHUB_OUTPUT + else + echo "environment=dev" >> $GITHUB_OUTPUT + fi + + - name: Deploy to Cloudflare Workers (Production) + if: steps.env.outputs.environment == 'production' run: | npm install -g wrangler@latest # Uses default wrangler.toml (production config) from root directory @@ -30,3 +52,13 @@ jobs: env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + + - name: Deploy to Cloudflare Workers (Dev) + if: steps.env.outputs.environment == 'dev' + run: | + npm install -g wrangler@latest + # Uses wrangler.dev.toml for dev environment + wrangler deploy --config wrangler.dev.toml + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} diff --git a/DATA_SAFETY_VERIFICATION.md b/DATA_SAFETY_VERIFICATION.md new file mode 100644 index 0000000..4db60ec --- /dev/null +++ b/DATA_SAFETY_VERIFICATION.md @@ -0,0 +1,145 @@ +# Data Safety Verification: TldrawDurableObject → AutomergeDurableObject Migration + +## Overview + +This document verifies that the migration from `TldrawDurableObject` to `AutomergeDurableObject` is safe and will not result in data loss. + +## R2 Bucket Configuration ✅ + +### Production Environment +- **Bucket Binding**: `TLDRAW_BUCKET` +- **Bucket Name**: `jeffemmett-canvas` +- **Storage Path**: `rooms/${roomId}` +- **Configuration**: `wrangler.toml` lines 30-32 + +### Development Environment +- **Bucket Binding**: `TLDRAW_BUCKET` +- **Bucket Name**: `jeffemmett-canvas-preview` +- **Storage Path**: `rooms/${roomId}` +- **Configuration**: `wrangler.toml` lines 72-74 + +## Data Storage Architecture + +### Where Data is Stored + +1. **Document Data (R2 Storage)** ✅ + - **Location**: R2 bucket at path `rooms/${roomId}` + - **Format**: JSON document containing the full board state + - **Persistence**: Permanent storage, independent of Durable Object instances + - **Access**: Both `TldrawDurableObject` and `AutomergeDurableObject` use the same R2 bucket and path + +2. **Room ID (Durable Object Storage)** ⚠️ + - **Location**: Durable Object's internal storage (`ctx.storage`) + - **Purpose**: Cached room ID for the Durable Object instance + - **Recovery**: Can be re-initialized from URL path (`/connect/:roomId`) + +### Data Flow + +``` +┌─────────────────────────────────────────────────────────────┐ +│ R2 Bucket (TLDRAW_BUCKET) │ +│ │ +│ rooms/room-123 ←─── Document Data (PERSISTENT) │ +│ rooms/room-456 ←─── Document Data (PERSISTENT) │ +│ rooms/room-789 ←─── Document Data (PERSISTENT) │ +└─────────────────────────────────────────────────────────────┘ + ▲ ▲ + │ │ + ┌─────────────────┘ └─────────────────┐ + │ │ +┌───────┴────────┐ ┌─────────────┴────────┐ +│ TldrawDurable │ │ AutomergeDurable │ +│ Object │ │ Object │ +│ (DEPRECATED) │ │ (ACTIVE) │ +└────────────────┘ └──────────────────────┘ + │ │ + └─────────────────── Both read/write ─────────────────────┘ + to the same R2 location +``` + +## Migration Safety Guarantees + +### ✅ No Data Loss Risk + +1. **R2 Data is Independent** + - Document data is stored in R2, not in Durable Object storage + - R2 data persists even when Durable Object instances are deleted + - Both classes use the same R2 bucket (`TLDRAW_BUCKET`) and path (`rooms/${roomId}`) + +2. **Stub Class Ensures Compatibility** + - `TldrawDurableObject` extends `AutomergeDurableObject` + - Uses the same R2 bucket and storage path + - Existing instances can access their data during migration + +3. **Room ID Recovery** + - `roomId` is passed in the URL path (`/connect/:roomId`) + - Can be re-initialized if Durable Object storage is lost + - Code handles missing `roomId` by reading from URL (see `AutomergeDurableObject.ts` lines 43-49) + +4. **Automatic Format Conversion** + - `AutomergeDurableObject` handles multiple data formats: + - Automerge Array Format: `[{ state: {...} }, ...]` + - Store Format: `{ store: { "recordId": {...}, ... }, schema: {...} }` + - Old Documents Format: `{ documents: [{ state: {...} }, ...] }` + - Conversion preserves all data, including custom shapes and records + +### Migration Process + +1. **Deployment with Stub** + - `TldrawDurableObject` stub class is exported + - Cloudflare recognizes the class exists + - Existing instances can continue operating + +2. **Delete-Class Migration** + - Migration tag `v2` with `deleted_classes = ["TldrawDurableObject"]` + - Cloudflare will delete Durable Object instances (not R2 data) + - R2 data remains untouched + +3. **Data Access After Migration** + - New `AutomergeDurableObject` instances can access the same R2 data + - Same bucket (`TLDRAW_BUCKET`) and path (`rooms/${roomId}`) + - Automatic format conversion ensures compatibility + +## Verification Checklist + +- [x] R2 bucket binding is correctly configured (`TLDRAW_BUCKET`) +- [x] Both production and dev environments have R2 buckets configured +- [x] `AutomergeDurableObject` uses `env.TLDRAW_BUCKET` +- [x] Storage path is consistent (`rooms/${roomId}`) +- [x] Stub class extends `AutomergeDurableObject` (same R2 access) +- [x] Migration includes `delete-class` for `TldrawDurableObject` +- [x] Code handles missing `roomId` by reading from URL +- [x] Format conversion logic preserves all data types +- [x] Custom shapes and records are preserved during conversion + +## Testing Recommendations + +1. **Before Migration** + - Verify R2 bucket contains expected room data + - List rooms: `wrangler r2 object list TLDRAW_BUCKET --prefix "rooms/"` + - Check a sample room's format + +2. **After Migration** + - Verify rooms are still accessible + - Check that data format is correctly converted + - Verify custom shapes and records are preserved + - Monitor worker logs for conversion statistics + +3. **Data Integrity Checks** + - Shape count matches before/after + - Custom shapes (ObsNote, Holon, etc.) have all properties + - Custom records (obsidian_vault, etc.) are present + - No validation errors in console + +## Conclusion + +✅ **The migration is safe and will not result in data loss.** + +- All document data is stored in R2, which is independent of Durable Object instances +- Both classes use the same R2 bucket and storage path +- The stub class ensures compatibility during migration +- Format conversion logic preserves all data types +- Room IDs can be recovered from URL paths if needed + +The only data that will be lost is the cached `roomId` in Durable Object storage, which can be easily re-initialized from the URL path. + diff --git a/worker/worker.ts b/worker/worker.ts index 451ed20..170ffcb 100644 --- a/worker/worker.ts +++ b/worker/worker.ts @@ -5,6 +5,25 @@ import { Environment } from "./types" // make sure our sync durable objects are made available to cloudflare export { AutomergeDurableObject } from "./AutomergeDurableObject" +// Temporary stub for TldrawDurableObject to allow delete-class migration +// This extends AutomergeDurableObject so existing instances can be handled during migration +// +// DATA SAFETY: All document data is stored in R2 at `rooms/${roomId}`, not in Durable Object storage. +// When TldrawDurableObject instances are deleted, only the Durable Object instances are removed. +// The R2 data remains safe and accessible by AutomergeDurableObject, which uses the same R2 bucket +// (TLDRAW_BUCKET) and storage path. The roomId can be re-initialized from the URL path if needed. +// +// This will be removed after the migration completes +import { AutomergeDurableObject as BaseAutomergeDurableObject } from "./AutomergeDurableObject" + +export class TldrawDurableObject extends BaseAutomergeDurableObject { + constructor(ctx: DurableObjectState, env: Environment) { + // Extends AutomergeDurableObject, so it uses the same R2 bucket (env.TLDRAW_BUCKET) + // and storage path (rooms/${roomId}), ensuring no data loss during migration + super(ctx, env) + } +} + // Lazy load heavy dependencies to avoid startup timeouts let handleUnfurlRequest: any = null diff --git a/wrangler.toml b/wrangler.toml index ee7bcdb..9e140b5 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -23,10 +23,9 @@ bindings = [ tag = "v1" new_classes = ["AutomergeDurableObject"] -# Note: TldrawDurableObject → AutomergeDurableObject migration removed -# The AutomergeDurableObject class is already in use, so we can't rename to it. -# Any remaining TldrawDurableObject instances will be orphaned but won't cause issues. -# If you need to clean them up, you can add a delete-class migration in the future. +[[migrations]] +tag = "v2" +deleted_classes = ["TldrawDurableObject"] [[r2_buckets]] binding = 'TLDRAW_BUCKET' @@ -66,6 +65,10 @@ bindings = [ tag = "v1" new_classes = ["AutomergeDurableObject"] +[[env.dev.migrations]] +tag = "v2" +deleted_classes = ["TldrawDurableObject"] + [[env.dev.r2_buckets]] binding = 'TLDRAW_BUCKET' bucket_name = 'jeffemmett-canvas-preview'