Skip to content

Conversation

@NathanFlurry
Copy link
Member

No description provided.

@vercel
Copy link

vercel bot commented Nov 13, 2025

Deployment failed with the following error:

Too many requests - try again in 3 minutes (more than 120, code: "api-deployments-flood-pro").

Learn More: https://vercel.com/docs/limits#rate-limits

Copy link
Member Author

NathanFlurry commented Nov 13, 2025

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more


How to use the Graphite Merge Queue

Add the label merge-queue to this PR to add it to the merge queue.

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@claude
Copy link

claude bot commented Nov 13, 2025

PR Review: Add Ability to Destroy Actors

Summary

This PR adds actor destruction functionality to RivetKit, allowing actors to be properly destroyed and resurrected with generation tracking. The implementation is comprehensive and well-tested, but there are several areas that need attention.


Critical Issues

1. Race Condition in Destroy Logic (High Priority)

Location: rivetkit-typescript/packages/cloudflare-workers/src/actor-driver.ts:230-250

The startDestroy method has a potential race condition:

startDestroy(actorId: string): void {
    const handler = this.#globalState.actors.get(doId);
    
    // Actor not loaded, nothing to destroy
    if (!handler || !handler.actor) {
        return;  // ⚠️ Actor exists in storage but isn't loaded - silent failure
    }
    
    // Check if already destroying
    if (handler.destroying) {
        return;
    }
    handler.destroying = true;
    // ...
}

Problem: If an actor exists in Durable Object storage but hasn't been loaded into memory yet, the destroy operation silently fails. This could lead to:

  • Actors that should be destroyed remaining active
  • Inconsistent state between expected and actual actor lifecycle
  • Confusion when destroy() is called but the actor continues functioning

Recommendation: Either:

  1. Load the actor if not present before destroying
  2. Mark as destroyed in storage even if not loaded
  3. Throw an error to indicate the operation wasn't completed

2. Missing Error Handling in Background KV Deletion (Medium Priority)

Location: rivetkit-typescript/packages/cloudflare-workers/src/actor-driver.ts:255-258

doState.ctx.waitUntil(
    env.ACTOR_KV.delete(GLOBAL_KV_KEYS.actorMetadata(actorId)),
);

Problem: The background KV deletion has no error handling. If this fails:

  • The actor will be destroyed locally but metadata remains in KV
  • Future lookups might incorrectly think the actor still exists
  • No logging or notification of the failure

Recommendation: Add error handling and logging:

doState.ctx.waitUntil(
    env.ACTOR_KV.delete(GLOBAL_KV_KEYS.actorMetadata(actorId))
        .catch(err => {
            this.#log.error({
                msg: "failed to delete actor metadata from KV",
                actorId,
                error: stringifyError(err),
            });
        })
);

3. Generation Mismatch Error Handling (Medium Priority)

Location: rivetkit-typescript/packages/cloudflare-workers/src/actor-driver.ts:144-147

if (generation !== expectedGeneration) {
    throw new Error(
        `Actor ${actorId} generation mismatch - expected ${expectedGeneration}, got ${generation}`,
    );
}

Problem: This throws a generic Error instead of a proper ActorError. This means:

  • Client-side error handling won't recognize this as an actor-specific error
  • Error won't have proper group and code for catching
  • Not following the project's error handling patterns

Recommendation: Create a specific error type:

export class ActorGenerationMismatch extends ActorError {
    constructor(actorId: string, expected: number, actual: number) {
        super(
            "actor",
            "generation_mismatch",
            `Actor ${actorId} generation mismatch - expected ${expected}, got ${actual}`,
            { public: true, metadata: { actorId, expected, actual } }
        );
    }
}

Code Quality Issues

4. Inconsistent SQL Schema Comments (Low Priority)

Location: rivetkit-typescript/packages/cloudflare-workers/src/actor-handler-do.ts:92-115

The SQL table creation comments are helpful, but the schema definition could be clearer:

CREATE TABLE IF NOT EXISTS _rivetkit_metadata(
    id INTEGER PRIMARY KEY CHECK (id = 1),  // Not obvious why id=1 constraint
    name TEXT NOT NULL,
    key TEXT NOT NULL,
    destroyed INTEGER DEFAULT 0,
    generation INTEGER DEFAULT 0
);

Recommendation: Add a comment explaining the id=1 constraint ensures single-row table.


5. Potential Memory Leak in Global State (Medium Priority)

Location: rivetkit-typescript/packages/cloudflare-workers/src/actor-driver.ts:30-42

#dos: Map<string, DurableObjectGlobalState> = new Map();
#actors: Map<string, ActorHandler> = new Map();

Problem: The #dos map grows indefinitely with DO IDs. While Cloudflare Workers have memory isolation per request, in long-lived scenarios this could accumulate.

Recommendation: Consider cleanup strategy or document the lifecycle expectations.


6. Type Safety in SQL Queries (Low Priority)

Location: Multiple files using sql.exec()

The SQL queries use type assertions without validation:

const name = result.value[0] as string;
const key = JSON.parse(result.value[1] as string) as ActorKey;
const destroyed = result.value[2] as number;
const generation = result.value[3] as number;

Recommendation: Add runtime validation for critical fields:

invariant(typeof name === 'string', 'name must be string');
invariant(typeof destroyed === 'number', 'destroyed must be number');

Security Concerns

7. Actor Key Storage Security (Low Priority)

Location: rivetkit-typescript/packages/cloudflare-workers/src/actor-handler-do.ts:132-136

Actor keys are stored as JSON strings in SQLite:

key TEXT NOT NULL,
// ...
const key = JSON.parse(result.value[1] as string) as ActorKey;

Analysis: This is acceptable since:

  • Durable Objects have isolated storage per DO
  • Keys are not sensitive credentials (they're identifiers)
  • However, document that keys should not contain sensitive data

Recommendation: Add a comment or documentation noting that actor keys should not contain sensitive information.


Performance Considerations

8. Serial KV Operations (Medium Priority)

Location: rivetkit-typescript/packages/cloudflare-workers/src/actor-driver.ts:191-198, 214-221

async kvBatchPut(actorId: string, entries: [Uint8Array, Uint8Array][]): Promise<void> {
    const sql = this.#getDOCtx(actorId).storage.sql;
    for (const [key, value] of entries) {
        kvPut(sql, key, value);  // Sequential INSERTs
    }
}

Problem: Each kvPut is a separate SQL INSERT. For large batches, this is inefficient.

Recommendation: Use a single SQL transaction with multiple inserts:

sql.exec('BEGIN TRANSACTION');
try {
    for (const [key, value] of entries) {
        kvPut(sql, key, value);
    }
    sql.exec('COMMIT');
} catch (err) {
    sql.exec('ROLLBACK');
    throw err;
}

9. Full Table Scan in kvListPrefix (High Priority)

Location: rivetkit-typescript/packages/cloudflare-workers/src/kv_query.ts:31-47

export function kvListPrefix(sql: SqlStorage, prefix: Uint8Array): [Uint8Array, Uint8Array][] {
    const cursor = sql.exec("SELECT key, value FROM _rivetkit_kv_storage");  // ⚠️ Full table scan!
    const entries: [Uint8Array, Uint8Array][] = [];
    
    for (const row of cursor.raw()) {
        const key = toUint8Array(row[0]);
        const value = toUint8Array(row[1]);
        
        if (hasPrefix(key, prefix)) {  // Filtering in application code
            entries.push([key, value]);
        }
    }
    return entries;
}

Problem: This does a full table scan and filters in memory. For actors with many KV entries, this is very inefficient.

Recommendation: Unfortunately, SQLite doesn't support BLOB prefix matching well. Consider:

  1. Adding an index on key (though BLOB indexing is limited)
  2. Storing a hex-encoded version of keys for efficient prefix queries
  3. Documenting the performance characteristics
  4. If prefix queries are rare, this might be acceptable - but needs documentation

Test Coverage

10. Excellent Test Coverage

The test suite (actor-destroy.ts) is comprehensive:

  • Tests both connected and non-connected scenarios
  • Validates state persistence before/after destroy
  • Confirms lifecycle hooks are called
  • Tests actor resurrection with fresh state
  • Uses proper async waiting patterns

Minor Suggestion: Add test cases for:

  • Destroying an actor that's already being destroyed (idempotency)
  • Destroying an actor mid-action execution
  • Generation mismatch scenarios

Best Practices & Style

11. Good Use of Generation Tracking

The generation tracking pattern is excellent:

  • Prevents stale references
  • Enables safe resurrection
  • Clear separation of DO ID vs Actor ID

Observation: The buildActorId/parseActorId utilities are clean and well-documented.


12. Logging Consistency (Low Priority)

Most logging follows the structured logging pattern correctly:

logger().debug({ msg: "actor creating", key: req.key, generation });

However, some logs could include more context for debugging destroyed actors.


Documentation

13. Missing Documentation (Medium Priority)

Missing:

  1. Comments explaining the generation tracking pattern
  2. Documentation about when actors can/cannot be destroyed
  3. Behavior of destroy during active connections
  4. KV storage schema documentation

Recommendation: Add JSDoc comments to key methods and a README section explaining:

  • Actor lifecycle with destroy
  • Generation tracking
  • Storage cleanup behavior

Breaking Changes

14. Wrangler Configuration Change (Breaking Change)

Location: examples/*/wrangler.json

-"new_classes": ["ActorHandler"]
+"new_sqlite_classes": ["ActorHandler"]

Impact: This is a breaking change for existing deployments. Users will need to update their wrangler.json files.

Recommendation:

  • Document this in migration notes
  • Consider adding a migration guide
  • Mention in PR description

Summary

Strengths:

  • ✅ Comprehensive implementation of actor destruction
  • ✅ Strong test coverage
  • ✅ Good use of generation tracking to prevent stale references
  • ✅ Proper lifecycle hook integration

Must Fix Before Merge:

  1. Race condition in startDestroy when actor not loaded ([SVC-2555] Set up issue templates #1)
  2. Full table scan performance issue in kvListPrefix ([SOC-15] Fix error when running user-delete #9)
  3. Error handling for background KV deletion ([SVC-2479] Send cluster events to PostHog #2)

Should Fix:

  1. Use proper ActorError types ([SVC-2504] Fix 5 GB upload limit for local development from Cloudflare #3)
  2. Add SQL transaction batching for KV operations (Remove fallible uses of as in api services #8)
  3. Document breaking changes (Fix compile error in mm-worker #14)

Nice to Have:

  1. Additional test coverage for edge cases
  2. Documentation for generation tracking
  3. Memory cleanup strategy for global state

Overall, this is a solid implementation. The core functionality is well-designed, but the race condition and performance issues need to be addressed before merging.

Estimated Time to Fix Critical Issues: 2-3 hours

@vercel
Copy link

vercel bot commented Nov 13, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
rivetkit-serverless Ready Ready Preview Comment Nov 13, 2025 3:26am
3 Skipped Deployments
Project Deployment Preview Comments Updated (UTC)
rivet-cloud Ignored Ignored Nov 13, 2025 3:26am
rivet-inspector Ignored Ignored Preview Nov 13, 2025 3:26am
rivet-site Ignored Ignored Preview Nov 13, 2025 3:26am

@vercel vercel bot temporarily deployed to Preview – rivetkit-serverless November 13, 2025 02:27 Inactive
@claude
Copy link

claude bot commented Nov 13, 2025

PR Review: Actor Destroy Functionality

Summary

This PR adds the ability to destroy actors in RivetKit, introducing a generation-based tracking system for Cloudflare Workers actors to handle actor lifecycle management including resurrection. The implementation is comprehensive with good test coverage.

🎯 Strengths

Architecture & Design

  • Generation tracking: Excellent solution using actor IDs formatted as {doId}:{generation} to prevent stale references after actor resurrection
  • Separation of concerns: Clean separation between DO state and actor state using WeakMap for proper garbage collection
  • SQL-based storage: Good migration from native Cloudflare KV to SQL storage for blob key support

Testing

  • Comprehensive test coverage with both connected and non-connected actor scenarios
  • Observer pattern in tests is clean for verifying async destroy behavior
  • Tests verify state cleanup, resurrection, and error cases

🐛 Potential Issues

Critical

  1. Race condition in loadActor (actor-driver.ts:132-161)

    if (actorState?.actorInstance) {
        return actorState.actorInstance;
    }

    If startDestroy is called between this check and returning the instance, you could return an actor that's being destroyed. Consider checking the destroying flag here.

  2. Generation mismatch handling (actor-driver.ts:183-188)

    if (generation !== expectedGeneration) {
        throw new Error(`Actor ${actorId} generation mismatch...`);
    }

    This will throw when loading a resurrected actor if the cached generation is stale. The error handling should be more graceful - perhaps trigger a reload or return a more specific error type.

  3. Memory leak in global state (actor-driver.ts:45-57)
    The #dos Map in CloudflareDurableObjectGlobalState never removes entries. In long-running workers, this could accumulate destroyed actor references. Consider adding cleanup in ActorGlobalState.reset():

    // In reset():
    this.destroying = false;
    // Should also remove from #dos Map somehow

Moderate

  1. SQL injection concerns (actor-kv.ts:34)

    const cursor = sql.exec("SELECT key, value FROM _rivetkit_kv_storage");

    The kvListPrefix function fetches ALL rows then filters in memory. For actors with many KV entries, this is very inefficient and could cause memory issues. Consider using SQL LIKE with proper escaping:

    // Convert prefix to hex and use SQL LIKE
    sql.exec("SELECT key, value FROM _rivetkit_kv_storage WHERE key LIKE ?", prefix + '%')
  2. Error handling in #callOnStopAsync (actor-driver.ts:293-321)
    This method is fire-and-forget with no error handling. If actor.onStop() throws, the destroy might partially complete. Wrap in try-catch and log errors properly:

    try {
        await actor.onStop("destroy");
    } catch (err) {
        logger().error({ msg: "error in onStop during destroy", err });
        // Consider whether to continue with cleanup or abort
    }
  3. Concurrent destroy handling (actor-driver.ts:242-264)
    The check if (actorState.destroying) and immediate return doesn't prevent double-destroy if called concurrently before the flag is set. This is probably fine given the fire-and-forget nature, but worth documenting.

  4. Missing await on KV delete (actor-driver.ts:312-315)

    doState.ctx.waitUntil(
        env.ACTOR_KV.delete(GLOBAL_KV_KEYS.actorMetadata(actorId)),
    );

    This is fire-and-forget which is fine, but if the KV delete fails, the metadata will be stale. Consider logging failures or implementing retry logic.

Minor

  1. Type safety in SQL results (actor-handler-do.ts:139-146)

    const name = result.value[0] as string;
    const key = JSON.parse(result.value[1] as string) as ActorKey;

    No validation that the JSON parse succeeds or that values are the expected types. Add runtime validation:

    if (!name || typeof name !== 'string') {
        throw new Error(`Invalid actor metadata: missing or invalid name`);
    }
  2. Logging improvements

    • Line actor-driver.ts:293 - Consider logging the actorId being destroyed for debugging
    • Line actor-handler-do.ts:252 - "actor is destroyed, cannot load" should include the actorId for correlation
  3. Inconsistent error messages

    • actor-driver.ts:157 uses "is destroyed"
    • actor-handler-do.ts:252 uses "is destroyed, cannot load"
    • Consider standardizing error messages for actor destroy scenarios

⚡ Performance Considerations

  1. SQL table scans (actor-kv.ts:34): As mentioned above, kvListPrefix does a full table scan which will be O(n) with number of KV entries. This could significantly impact actors with large state.

  2. No transaction boundaries: The SQL operations in create() and during destroy aren't wrapped in transactions. If operations fail mid-way, you could have inconsistent state. Consider:

    sql.exec("BEGIN TRANSACTION");
    try {
        // operations
        sql.exec("COMMIT");
    } catch {
        sql.exec("ROLLBACK");
        throw;
    }
  3. waitUntil usage: The background KV delete (actor-driver.ts:314) is good for performance but means metadata cleanup is eventually consistent. Document this behavior.

🔒 Security Considerations

  1. SQL injection: While Cloudflare's SQL API with parameterized queries is generally safe, ensure all user input goes through parameters, not string concatenation. The current code looks good on this front.

  2. Actor resurrection validation: The generation check prevents unauthorized access to destroyed actors - this is good security practice.

📝 Code Quality & Best Practices

Positive

  • Follows CLAUDE.md conventions (structured logging, error handling patterns)
  • Good use of TypeScript for type safety
  • Clear separation between DO lifecycle and actor lifecycle

Suggestions

  1. Add JSDoc comments for public APIs, especially:

    • buildActorId and parseActorId in actor-id.ts (though they do have good comments)
    • ActorGlobalState class and its properties
    • The create() method signature change
  2. Consider extracting constants (actor-handler-do.ts:137):

    const METADATA_QUERY = "SELECT name, key, destroyed, generation FROM _rivetkit_metadata WHERE id = 1";
  3. Wrangler.json change documentation: The change from new_classes to new_sqlite_classes should be noted in commit message or migration guide.

🧪 Test Coverage

Excellent coverage with:

  • Actor destruction without connection
  • Actor destruction with connection
  • State verification post-destroy
  • Actor resurrection verification
  • Observer pattern for async verification

Missing test cases to consider:

  • Concurrent destroy calls
  • Destroy during active request processing
  • Generation mismatch scenarios
  • Actor with very large KV state (performance test)
  • Failed destroy (onStop throws error)

📋 Recommendations

High Priority

  1. Fix the kvListPrefix performance issue with proper SQL LIKE query
  2. Add error handling to #callOnStopAsync
  3. Add race condition check for destroying flag in loadActor
  4. Add memory cleanup for #dos Map in global state

Medium Priority

  1. Add SQL transactions for state consistency
  2. Add runtime validation for SQL query results
  3. Add JSDoc documentation for public APIs

Low Priority

  1. Standardize error messages
  2. Extract SQL query constants
  3. Add additional edge case tests

✅ Verdict

This is a well-designed feature with good architecture and comprehensive testing. The generation-based tracking is a clever solution for handling actor resurrection. The critical issues identified are mostly around edge cases and performance optimization.

Recommendation: Approve with requested changes to address the race condition and performance issues. The memory leak and error handling should be fixed before merge.

Great work overall! 🚀

@NathanFlurry NathanFlurry force-pushed the 11-12-feat_rivet-engine_udb_key_parser branch from 72318fc to cc00181 Compare November 13, 2025 02:47
@NathanFlurry NathanFlurry force-pushed the 11-12-feat_rivetkit_add_ability_to_destroy_actors branch from 892e9cb to 90de47a Compare November 13, 2025 02:47
@claude
Copy link

claude bot commented Nov 13, 2025

PR Review: Add ability to destroy actors

Summary

This PR implements actor destruction functionality for RivetKit, adding the ability to explicitly destroy actors and clean up their state. The implementation includes generation tracking to handle actor resurrection and prevent stale references.

Code Quality & Architecture

Strengths

  1. Generation-based actor IDs: The {doId}:{generation} pattern in actor-id.ts is a clean solution for tracking actor lifecycle and preventing stale references after resurrection
  2. SQL migration to SQLite storage: Moving from Cloudflare's native KV API to SQLite tables provides better flexibility for blob keys (lines in actor-handler-do.ts:16-36)
  3. Comprehensive test coverage: The driver test suite includes tests for both connected and non-connected destroy scenarios with proper async validation
  4. Background cleanup: Using ctx.waitUntil() for KV cleanup prevents blocking the destroy operation

Areas for Improvement

1. Race condition in actor loading (rivetkit-typescript/packages/cloudflare-workers/src/actor-driver.ts:132-160)

// Check if actor is already loaded
let actorState = this.#globalState.getActorState(doState.ctx);
if (actorState?.actorInstance) {
    return actorState.actorInstance;
}

// Create new actor state if it doesn't exist
if (!actorState) {
    actorState = new ActorGlobalState();
    actorState.actorPromise = promiseWithResolvers();
    this.#globalState.setActorState(doState.ctx, actorState);
}

// Another request is already loading this actor, wait for it
if (actorState.actorPromise) {
    await actorState.actorPromise.promise;
    // ...
}

Issue: There's a potential race between checking actorState?.actorInstance and setting actorState.actorPromise. If two requests arrive simultaneously, both could pass the first check and create separate promises.

Recommendation: Initialize the promise immediately when creating actorState:

if (!actorState) {
    actorState = new ActorGlobalState();
    actorState.actorPromise = promiseWithResolvers();
    this.#globalState.setActorState(doState.ctx, actorState);
} else if (actorState.actorPromise) {
    // Already loading
    await actorState.actorPromise.promise;
    return actorState.actorInstance!;
}
// Continue with loading...

2. Generation validation inconsistency (rivetkit-typescript/packages/cloudflare-workers/src/actor-driver.ts:178-183)

// Check if generation matches
if (generation !== expectedGeneration) {
    throw new Error(
        `Actor ${actorId} generation mismatch - expected ${expectedGeneration}, got ${generation}`,
    );
}

Issue: This throws a generic Error instead of a proper ActorError. This is inconsistent with error handling elsewhere and won't be properly caught by client code expecting ActorError types.

Recommendation: Use ActorNotFound or create a new error type:

if (generation !== expectedGeneration) {
    throw new ActorNotFound(actorId);
}

3. Missing error handling in async destroy (rivetkit-typescript/packages/cloudflare-workers/src/actor-driver.ts:295-320)

async #callOnStopAsync(
    actorId: string,
    doId: string,
    actor: CoreAnyActorInstance,
) {
    // Stop
    await actor.onStop("destroy");
    // ... cleanup
}

Issue: There's no try-catch around the destroy logic. If onStop throws or cleanup fails, the actor may be left in an inconsistent state (destroying=true but not actually destroyed).

Recommendation: Add error handling and logging:

async #callOnStopAsync(
    actorId: string,
    doId: string,
    actor: CoreAnyActorInstance,
) {
    try {
        await actor.onStop("destroy");
        
        const doState = this.#globalState.getDOState(doId);
        const sql = doState.ctx.storage.sql;
        sql.exec("UPDATE _rivetkit_metadata SET destroyed = 1 WHERE 1=1");
        sql.exec("DELETE FROM _rivetkit_kv_storage");
        
        await doState.ctx.storage.deleteAlarm();
        
        const env = getCloudflareAmbientEnv();
        doState.ctx.waitUntil(
            env.ACTOR_KV.delete(GLOBAL_KV_KEYS.actorMetadata(actorId)),
        );
        
        const actorHandle = this.#globalState.getActorState(doState.ctx);
        actorHandle?.reset();
    } catch (error) {
        logger().error({ msg: "error during actor destroy", actorId, error });
        // Consider: should we still mark as destroyed? Or roll back?
    }
}

4. SQL injection vulnerability (rivetkit-typescript/packages/cloudflare-workers/src/actor-kv.ts)

While reviewing the diff, I notice SQL execution with blob data. Ensure all SQL queries use parameterized queries, not string concatenation. The implementation appears to use proper parameterization, but verify this throughout.

5. Unclear destroy vs destroyed state (rivetkit-typescript/packages/cloudflare-workers/src/actor-driver.ts:85-89)

/**
 * Indicates if `startDestroy` has been called.
 *
 * This is stored in memory instead of SQLite since the destroy may be cancelled.
 *
 * See the corresponding `destroyed` property in SQLite metadata.
 */
destroying: boolean = false;

Question: The comment mentions "destroy may be cancelled" but there's no cancellation logic in the implementation. Is this intended for future functionality? If not, this comment is misleading.

Performance Considerations

  1. SQL table scans: Queries like UPDATE _rivetkit_metadata SET destroyed = 1 WHERE 1=1 scan all rows. Since there's always exactly 1 row (enforced by CHECK (id = 1)), this is fine, but consider using WHERE id = 1 for clarity.

  2. KV cleanup in background: Good use of waitUntil() to avoid blocking, but there's no retry mechanism if the KV delete fails. Consider adding error handling or using Cloudflare's durable queues for reliability.

  3. Generation tracking overhead: Each actor ID now includes generation which adds parsing overhead. This seems acceptable given the safety benefits.

Security Concerns

  1. Destroyed state verification: The getForId method properly checks the destroying flag (line 264 in manager-driver.ts), preventing access to actors mid-destruction. Good practice.

  2. No authorization on destroy: The destroy action doesn't appear to have any authorization checks beyond what the actor itself implements. Ensure this is documented.

Test Coverage

The test suite in actor-destroy.ts is comprehensive:

  • ✅ Tests both connected and non-connected destroy flows
  • ✅ Validates state cleanup
  • ✅ Verifies actor resurrection with fresh state
  • ✅ Uses proper async waiting with vi.waitFor()

Missing tests:

  • Concurrent destroy requests on the same actor
  • Destroy during active connections
  • Generation mismatch scenarios
  • Failed onStop/cleanup scenarios

Minor Issues

  1. Typo in error class name (rivetkit-typescript/packages/rivetkit/src/actor/errors.ts:331)
export class RequestHandlerNotDfeined extends ActorError {

Should be RequestHandlerNotDefined

  1. Wrangler config migration: The change from new_classes to new_sqlite_classes in wrangler.json is correct for SQLite-backed DOs

  2. pnpm-lock.yaml: Removed unused typedoc dependency - good cleanup

Recommendations Summary

High Priority:

  1. Fix race condition in loadActor
  2. Add error handling in #callOnStopAsync
  3. Use proper ActorError types instead of generic Error

Medium Priority:
4. Add tests for concurrent/edge case scenarios
5. Clarify or remove "destroy may be cancelled" comment
6. Fix typo in RequestHandlerNotDfeined

Low Priority:
7. Use WHERE id = 1 in SQL updates for clarity
8. Consider retry mechanism for KV cleanup

Overall Assessment

This is a well-architected implementation of actor destruction with thoughtful consideration for generation tracking and state cleanup. The core logic is solid, but would benefit from additional error handling and edge case tests. The SQL migration is a good architectural choice for blob key support.

Recommendation: Approve with suggested improvements, particularly around error handling and race conditions.

🤖 Generated with Claude Code

@NathanFlurry NathanFlurry force-pushed the 11-12-feat_rivetkit_add_ability_to_destroy_actors branch from 90de47a to 6cd3c7d Compare November 13, 2025 02:50
@vercel
Copy link

vercel bot commented Nov 13, 2025

Deployment failed with the following error:

Too many requests - try again in 1 minute (more than 120, code: "api-deployments-flood-pro").

Learn More: https://vercel.com/docs/limits#rate-limits

@claude
Copy link

claude bot commented Nov 13, 2025

Pull Request Review: Actor Destruction Feature

I've completed a comprehensive review of this PR that adds actor destruction capabilities to RivetKit. Overall, this is a well-implemented feature with good architectural decisions. Below are my findings organized by category.


✅ Strengths

1. Excellent Generation-Based Architecture

The generation counter system is a clean solution for handling actor resurrection:

  • Actor IDs format: {doId}:{generation} (actor-id.ts:14)
  • Generation increments on resurrection (actor-handler-do.ts:323)
  • Prevents stale references to destroyed actors
  • Proper validation of generation mismatches (actor-driver.ts:188-192)

2. Proper State Management

The separation of concerns between memory and persistent state is well-designed:

  • destroying flag in memory for immediate checks (ActorGlobalState.destroying)
  • destroyed flag in SQLite for persistent state (actor-handler-do.ts:98)
  • WeakMap usage for proper garbage collection (actor-driver.ts:37)

3. Good Test Coverage

The PR includes driver tests with proper fixtures (destroy.ts, driver-tests.test.ts) that validate the destroy functionality.

4. Cloudflare Workers Migration Fix

The wrangler.json fix changing new_classes to new_sqlite_classes is correct for CF Workers with SQLite storage.


🔴 Critical Issues

1. Race Condition in startDestroy (actor-driver.ts:273-294)

Issue: The destroy operation is spawned asynchronously without proper coordination, which can lead to race conditions.

startDestroy(actorId: string): void {
    // ...
    actorState.destroying = true;
    
    // Spawn onStop in background
    this.#callOnStopAsync(actorId, doId, actorState.actorInstance);
}

Problems:

  • If loadActor is called while #callOnStopAsync is executing, it could load the actor after destroying is set but before the SQL destroyed flag is written
  • The destroyed flag update (line 307) happens after onStop completes, creating a window where the actor state is inconsistent
  • No protection against concurrent startDestroy calls on the same actor

Recommendation:

startDestroy(actorId: string): void {
    // ... existing checks ...
    
    // Mark as destroying AND set a promise to track completion
    actorState.destroying = true;
    actorState.destroyPromise = this.#callOnStopAsync(actorId, doId, actorState.actorInstance);
}

// In loadActor, add:
if (actorState?.destroying) {
    throw new Error(`Actor ${actorId} is being destroyed`);
}

2. Incomplete Error Handling in loadActor (actor-driver.ts:132-213)

Issue: The function doesn't check if an actor is being destroyed before attempting to load it.

async loadActor(actorId: string): Promise<AnyActorInstance> {
    // ...
    let actorState = this.#globalState.getActorState(doState.ctx);
    if (actorState?.actorInstance) {
        // ⚠️ Should check actorState.destroying here
        return actorState.actorInstance;
    }
    // ...
}

Recommendation: Add a check immediately after retrieving actorState:

if (actorState?.destroying) {
    throw new ActorDestroying(actorId);
}

3. SQL Injection Vulnerability in kvListPrefix (actor-kv.ts:30-48)

Issue: The function scans ALL rows instead of using a SQL WHERE clause, which is inefficient and potentially dangerous for large datasets.

export function kvListPrefix(
    sql: SqlStorage,
    prefix: Uint8Array,
): [Uint8Array, Uint8Array][] {
    const cursor = sql.exec("SELECT key, value FROM _rivetkit_kv_storage");
    // ⚠️ Loads entire table into memory
    const entries: [Uint8Array, Uint8Array][] = [];
    
    for (const row of cursor.raw()) {
        const key = toUint8Array(row[0]);
        const value = toUint8Array(row[1]);
        
        if (hasPrefix(key, prefix)) {
            entries.push([key, value]);
        }
    }
    
    return entries;
}

Problems:

  • O(n) memory and time complexity
  • No limit on result set size
  • For actors with thousands of KV entries, this will be very slow

Recommendation: Use SQL LIKE with BLOB comparison or add a LIMIT:

export function kvListPrefix(
    sql: SqlStorage,
    prefix: Uint8Array,
    limit: number = 1000,
): [Uint8Array, Uint8Array][] {
    const cursor = sql.exec(
        "SELECT key, value FROM _rivetkit_kv_storage LIMIT ?",
        limit
    );
    // ... rest of implementation
}

⚠️ Major Issues

4. Resurrection Doesn't Clean Old KV Data (actor-handler-do.ts:374-388)

Issue: When resurrecting a destroyed actor, the old KV storage data is not cleared before initializing with new data.

if (created) {
    // Initialize persist data in KV storage
    initializeActorKvStorage(this.ctx.storage.sql, req.input);
    // ⚠️ But DELETE was called in #callOnStopAsync, so this should be OK?
}

Verification Needed: Confirm that DELETE FROM _rivetkit_kv_storage (actor-driver.ts:308) completes before resurrection can occur. If there's any chance of the background delete not completing, old data could mix with new data.

Recommendation: Add defensive deletion:

if (created) {
    // Ensure clean slate for resurrection
    sql.exec("DELETE FROM _rivetkit_kv_storage");
    initializeActorKvStorage(this.ctx.storage.sql, req.input);
}

5. Actor ID Parsing Fragility (actor-id.ts:24-38)

Issue: The parseActorId function uses simple string splitting which could fail if Durable Object IDs contain colons.

export function parseActorId(actorId: string): [string, number] {
    const parts = actorId.split(":");
    if (parts.length !== 2) {
        throw new Error(`Invalid actor ID format: ${actorId}`);
    }
    // ...
}

Recommendation: Use lastIndexOf to split from the right:

export function parseActorId(actorId: string): [string, number] {
    const lastColon = actorId.lastIndexOf(":");
    if (lastColon === -1) {
        throw new Error(`Invalid actor ID format: ${actorId}`);
    }
    
    const doId = actorId.substring(0, lastColon);
    const generationStr = actorId.substring(lastColon + 1);
    const generation = parseInt(generationStr, 10);
    
    if (Number.isNaN(generation)) {
        throw new Error(`Invalid generation number in actor ID: ${actorId}`);
    }
    
    return [doId, generation];
}

6. Missing Cleanup in #callOnStopAsync Error Path (actor-driver.ts:296-322)

Issue: If onStop throws an error, the cleanup operations may not complete.

async #callOnStopAsync(
    actorId: string,
    doId: string,
    actor: CoreAnyActorInstance,
) {
    // Stop
    await actor.onStop("destroy"); // ⚠️ If this throws, cleanup doesn't happen
    
    // Remove state
    const doState = this.#globalState.getDOState(doId);
    const sql = doState.ctx.storage.sql;
    sql.exec("UPDATE _rivetkit_metadata SET destroyed = 1 WHERE 1=1");
    sql.exec("DELETE FROM _rivetkit_kv_storage");
    // ...
}

Recommendation: Add try-finally:

async #callOnStopAsync(
    actorId: string,
    doId: string,
    actor: CoreAnyActorInstance,
) {
    try {
        await actor.onStop("destroy");
    } catch (error) {
        // Log error but continue with cleanup
        console.error(`Error in onStop for actor ${actorId}:`, error);
    } finally {
        // Always perform cleanup
        const doState = this.#globalState.getDOState(doId);
        const sql = doState.ctx.storage.sql;
        sql.exec("UPDATE _rivetkit_metadata SET destroyed = 1 WHERE 1=1");
        sql.exec("DELETE FROM _rivetkit_kv_storage");
        // ... rest of cleanup
    }
}

💡 Minor Issues & Suggestions

7. Unclear Comment in loadActor (actor-driver.ts:141-144)

// Check if actor is already loaded
let actorState = this.#globalState.getActorState(doState.ctx);
if (actorState?.actorInstance) {
    // Actor is already loaded, return it
    return actorState.actorInstance;
}

Suggestion: Add comment about generation validation:

// Check if actor is already loaded
let actorState = this.#globalState.getActorState(doState.ctx);
if (actorState?.actorInstance) {
    // Actor is already loaded and generation was validated when it was first loaded
    // Generation is checked in SQL query below for new loads
    return actorState.actorInstance;
}

8. Magic Number in SQL Constraint (actor-handler-do.ts:94-95)

id INTEGER PRIMARY KEY CHECK (id = 1),

Suggestion: Add a comment explaining why id must always be 1:

-- Single row table (id always equals 1) to store actor metadata
id INTEGER PRIMARY KEY CHECK (id = 1),

9. Inconsistent Error Messages

Some errors use the format with actor ID, others don't:

  • actor-driver.ts:173: Actor ${actorId} is not initialized
  • actor-driver.ts:184: Actor ${actorId} is destroyed

Suggestion: Ensure all error messages include the actor ID for debugging.

10. Missing Telemetry/Logging

The startDestroy function doesn't log when it returns early:

startDestroy(actorId: string): void {
    // ...
    if (!actorState?.actorInstance) {
        return; // ⚠️ Should log this
    }
    
    if (actorState.destroying) {
        return; // ⚠️ Should log this
    }
    // ...
}

Suggestion: Add debug logging for observability:

if (!actorState?.actorInstance) {
    logger().debug({ msg: "startDestroy: actor not loaded", actorId });
    return;
}

if (actorState.destroying) {
    logger().debug({ msg: "startDestroy: already destroying", actorId });
    return;
}

📊 Code Quality Assessment

Category Rating Notes
Architecture ⭐⭐⭐⭐⭐ Generation-based resurrection is excellent
Error Handling ⭐⭐⭐ Missing some edge cases
Performance ⭐⭐⭐⭐ Good, but kvListPrefix needs optimization
Security ⭐⭐⭐⭐ Generally safe, watch for SQL performance issues
Test Coverage ⭐⭐⭐⭐ Good test fixtures included
Documentation ⭐⭐⭐ Code is readable but could use more inline comments

🎯 Recommendations Summary

Must Fix Before Merge:

  1. Fix race condition in startDestroy
  2. Add destroying check in loadActor
  3. Optimize kvListPrefix to avoid loading all rows

Should Fix:
4. Add try-finally to #callOnStopAsync
5. Fix parseActorId to handle colons in DO IDs
6. Add defensive deletion on resurrection

Nice to Have:
7. Add more debug logging
8. Improve inline documentation
9. Add SQL comments for schema


Conclusion

This is a solid implementation of actor destruction with a clever generation-based resurrection system. The main concerns are around race conditions during destroy and performance of the KV list operation. Once the critical issues are addressed, this will be a robust feature.

Great work on the architecture! The generation counter approach is particularly elegant. 👏

@claude
Copy link

claude bot commented Nov 13, 2025

Pull Request Review: Actor Destroy Functionality

Thank you for this comprehensive PR adding actor destroy functionality to RivetKit! This is a well-structured implementation with good test coverage. Below is my detailed review:


✅ Strengths

1. Architecture & Design

  • Generation-based versioning: The actor ID format {doId}:{generation} is an excellent approach for tracking actor lifecycle and preventing stale references after resurrection
  • Proper separation of concerns: New utility files (actor-id.ts, actor-kv.ts, global-kv.ts) cleanly separate functionality
  • Migration to SQLite storage: Moving from native Cloudflare KV to SQLite for actor metadata and KV storage provides better consistency and supports blob keys

2. State Management

  • ActorGlobalState class: Good use of strong references in the DO and weak references in the global state for proper garbage collection
  • destroying flag: Properly tracks in-memory destroy state separate from persisted destroyed flag in SQLite
  • Generation tracking: Prevents stale actor references by incrementing generation on resurrection

3. Test Coverage

  • Comprehensive test suite in actor-destroy.ts covering both connected and non-connected scenarios
  • Tests verify state cleanup, observer notifications, and actor resurrection with fresh state
  • Good use of vi.waitFor for async cleanup verification

4. Code Quality

  • Proper error handling with custom ActorDestroying error
  • Good logging throughout for debugging
  • Follows repository conventions (lowercase log messages, structured logging)

🔧 Issues & Concerns

Critical Issues

1. Race Condition in loadActor (actor-driver.ts:131-161)

// Check if actor is already loaded
let actorState = this.#globalState.getActorState(doState.ctx);
if (actorState?.actorInstance) {
    return actorState.actorInstance;
}

// Create new actor state if it doesn't exist
if (!actorState) {
    actorState = new ActorGlobalState();
    actorState.actorPromise = promiseWithResolvers();
    this.#globalState.setActorState(doState.ctx, actorState);
}

// Another request is already loading this actor, wait for it
if (actorState.actorPromise) {
    await actorState.actorPromise.promise;
    // ...
}

Problem: Between checking if (!actorState) and setting actorState.actorPromise, another concurrent request could also create its own ActorGlobalState and set its own promise. Both requests would then proceed to load the actor independently.

Recommendation:

let actorState = this.#globalState.getActorState(doState.ctx);
if (!actorState) {
    actorState = new ActorGlobalState();
    actorState.actorPromise = promiseWithResolvers();
    this.#globalState.setActorState(doState.ctx, actorState);
} else if (actorState.actorInstance) {
    return actorState.actorInstance;
} else if (actorState.actorPromise) {
    await actorState.actorPromise.promise;
    if (!actorState.actorInstance) {
        throw new Error(`Actor failed to load in concurrent request`);
    }
    return actorState.actorInstance;
}
// Now proceed with loading...

2. Async Destroy Without Error Handling (actor-driver.ts:281-320)

async #callOnStopAsync(
    actorId: string,
    doId: string,
    actor: CoreAnyActorInstance,
) {
    // Stop
    await actor.onStop("destroy");
    // ... cleanup ...
}

Problem: This async method spawned in the background has no try-catch. If actor.onStop() throws, the error will be silently swallowed, and subsequent cleanup (SQL updates, KV deletion, state reset) won't execute.

Recommendation: Wrap the entire method body in try-catch with proper logging and ensure cleanup happens even if onStop fails.

3. Potential Memory Leak in Global State (actor-handler-do.ts:65-98)

The #state field in the Durable Object class holds a strong reference to ActorGlobalState. Even after destroy() is called and reset() clears its fields, the DO instance itself may remain in memory if Cloudflare keeps it around.

Recommendation: Consider if reset() is sufficient, or if additional cleanup is needed. Document the expected lifecycle in comments.

High Priority Issues

4. SQL Injection Risk

While the code uses parameterized queries in most places, ensure ALL SQL operations use proper parameter binding. Review kvPut, kvGet, kvDelete, and kvListPrefix to confirm they're not vulnerable to injection if keys contain SQL syntax.

5. Inconsistent Error Handling in getForId (manager-driver.ts:228-273)

const result = await stub.getMetadata();

if (!result) {
    return undefined;
}

if (result.actorId !== actorId) {
    return undefined;
}

if (result.destroying) {
    throw new ActorDestroying(actorId);
}

Problem: If an actor is in the destroying state, this throws an error. However, if the generation doesn't match (which could also indicate a destroyed actor), it returns undefined. This inconsistency may confuse callers.

Recommendation: Document this behavior clearly, or consider consistent error handling.

6. Missing Validation in parseActorId (actor-id.ts:23-37)

Doesn't validate that doId is non-empty or that generation is non-negative.

Recommendation:

if (!doId || generation < 0) {
    throw new Error(`Invalid actor ID format`);
}

Medium Priority Issues

7. Wrangler Config Migration Field

The change from new_classes to new_sqlite_classes in wrangler.json is correct for SQLite DOs, but ensure this is documented in migration notes for users upgrading.

8. Potential Timing Issue in Tests (actor-destroy.ts:34-37)

The test relies on vi.waitFor with default timeout. If the async destroy takes longer than expected, tests may flake. Consider increasing timeout or making it configurable.

9. Missing Type Safety in SQL Operations (actor-handler-do.ts:208-213)

Using array indices and type assertions is error-prone. Consider using named columns or validating data shapes.

Low Priority / Suggestions

10. Typo in Error Class Name (errors.ts:331)

export class RequestHandlerNotDfeined extends ActorError {

Should be RequestHandlerNotDefined (note: "Dfeined" → "Defined").

11. Improve Logging Context

Add more structured fields to logs for better debugging (include doId, name, key in more places).

12. Document Generation Semantics

Add documentation explaining:

  • When generation increments (on destroy + recreate)
  • Why generation 0 is the initial value
  • How generation affects actor resolution

🚀 Performance Considerations

Good

  • ✅ SQL operations are generally efficient
  • ✅ KV operations batched where possible
  • ✅ Proper use of waitUntil for background cleanup

Concerns

  • ⚠️ SQLite table scans: SELECT ... WHERE 1=1 in _rivetkit_metadata is fine since there's only 1 row (enforced by id = 1 constraint), but the constraint CHECK (id = 1) should be clearly documented
  • ⚠️ KV deletion in background: If ACTOR_KV delete fails, there's no retry mechanism. Consider if this is acceptable or if cleanup should be more robust

🔒 Security Considerations

Good

  • ✅ No obvious injection vulnerabilities in SQL (parameterized queries used)
  • ✅ Actor authentication/authorization unchanged
  • ✅ No exposure of internal actor state

Concerns

  • ⚠️ Ambient environment access: getCloudflareAmbientEnv() assumes environment is set. Ensure proper initialization and error handling if env is missing

📋 Recommendations Summary

Must Fix

  1. Add proper error handling to #callOnStopAsync
  2. Fix race condition in loadActor
  3. Add validation to parseActorId for doId and generation

Should Fix

  1. Document destroy lifecycle and generation semantics
  2. Clarify getForId error handling consistency
  3. Fix typo in RequestHandlerNotDfeined

Nice to Have

  1. Improve SQL type safety
  2. Add more logging context
  3. Document wrangler.json migration

🎯 Overall Assessment

This is a strong PR with thoughtful design and good test coverage. The generation-based actor versioning is particularly well done. The main concerns are around race conditions and error handling in async operations. Once those are addressed, this will be a solid foundation for actor lifecycle management.

Recommendation: Request changes to address critical issues #1 and #2, then approve after those are fixed.

Great work! 🚀

@claude
Copy link

claude bot commented Nov 13, 2025

Code Review: Actor Destroy Feature

Great work on implementing the actor destroy functionality! This is a solid implementation with good test coverage.

Strengths

  1. Comprehensive Test Coverage - The test suite covers both connected and non-connected scenarios, including state verification after resurrection
  2. Generation Tracking - Uses a generation counter to handle actor resurrection elegantly
  3. KV Storage Migration - Moving to SQL-based storage is the right choice for supporting blob keys
  4. Proper Lifecycle Handling - The onDestroy lifecycle hook is properly implemented
  5. Error Handling - New ActorDestroying error properly signals when an actor is being destroyed

Potential Issues

1. Race Condition in Destroy Cleanup (High Priority)
Location: rivetkit-typescript/packages/cloudflare-workers/src/actor-driver.ts:295-321

The callOnStopAsync method performs cleanup asynchronously but doesn't prevent concurrent access during cleanup. If a request arrives after startDestroy is called but before onStop completes, the actor might still be loaded and used. The destroying flag is checked in getForId but loadActor doesn't check this flag.

Recommendation: Add a check in loadActor to prevent loading actors that are marked as destroying.

2. KV Prefix List Performance Issue (Medium Priority)
Location: rivetkit-typescript/packages/cloudflare-workers/src/actor-kv.ts:30-48

The kvListPrefix function performs a full table scan and checks prefixes in memory. This will be slow for actors with many KV entries. Consider using SQLite BLOB comparisons for more efficient prefix matching.

3. Missing Invariant Check in Actor ID Parsing (Low Priority)
Location: rivetkit-typescript/packages/cloudflare-workers/src/actor-id.ts:23-37

The parseActorId function doesn't use the project's error system. Use the invariant helper or a custom error for consistency.

Security

  • SQL queries use parameterized queries consistently - prevents SQL injection
  • Actor metadata cleanup properly handles both SQL storage and KV metadata

Performance

  • Background cleanup using ctx.waitUntil() is efficient
  • Generation increment prevents integer overflow

Test Coverage

Comprehensive test suite includes destroy scenarios with/without connections, state persistence, and resurrection. Suggestion: Add a test for the race condition scenario.

Summary

This is a well-implemented feature with good architecture. Main concerns:

  1. High Priority: Race condition in loadActor when actor is destroying
  2. Medium Priority: KV prefix scan performance
  3. Low Priority: Error consistency

Recommendation: Address the high-priority race condition before merging. Other items can be addressed in follow-up PRs if preferred.

@claude
Copy link

claude bot commented Nov 13, 2025

PR Review: Actor Destroy Feature

Overview

This PR adds the ability to destroy actors in RivetKit, implementing generation-based actor lifecycle management. The implementation is comprehensive and well-structured.

Strengths

Architecture & Design

✅ Generation-based resurrection: The generation counter approach is excellent for preventing stale references
✅ Clean separation of concerns: New utility modules are well-organized
✅ State management: ActorGlobalState properly tracks initialization and loaded state
✅ Proper async handling: Destroy operation correctly spawns onStop in background
✅ Migration to SQL storage: Right approach for handling binary key/value pairs
✅ Error handling: New ActorDestroying error properly communicates actor state
✅ Test coverage: Comprehensive tests for both connected and non-connected scenarios

Critical Issue: Race Condition in startDestroy

Location: actor-driver.ts:273-294

If startDestroy is called before an actor is loaded, it returns early without marking anything. A subsequent loadActor call could then load the actor after destroy was requested.

Scenario:

  1. Client calls destroy() on unloaded actor → startDestroy returns early
  2. Another request triggers loadActor
  3. Actor is now loaded and running, but should be destroyed

Recommendation: Store a pending destroy flag that loadActor checks, or mark the actor as destroyed in SQL before attempting to load it.

Medium Issues

  1. Incomplete Cleanup Ordering (actor-driver.ts:301-322): ACTOR_KV deletion is fire-and-forget via waitUntil. If worker terminates early, KV entry remains. Consider documenting this eventual consistency behavior.

  2. Missing Generation Validation (actor-handler-do.ts:400-420): fetch/alarm methods don't validate that the generation in the request matches current generation. Stale client requests may succeed incorrectly.

  3. SQL Table Creation Without Version Migration (actor-handler-do.ts:85-99): No migration path if schema needs to change in future. Consider adding schema version tracking.

Low Priority Issues

  1. kvListPrefix Performance (actor-kv.ts:30-48): Full table scan for prefix queries. Consider optimizing or documenting performance characteristics.
  2. Minor code quality: Error messages could be more descriptive, redundant WHERE clauses

Testing Suggestions

Consider adding tests for:

  • Concurrent destroy + loadActor race condition
  • Actor resurrection (destroy + recreate with same key)
  • Generation mismatch error scenarios

Documentation

⚠️ PR description is empty. Consider adding feature overview, examples, and any breaking changes.

Summary

This is a well-designed feature with solid code quality. The generation-based approach is excellent. However, the race condition in startDestroy should be addressed before merging.

Recommendations

Before merge:

  1. Fix the race condition in startDestroy
  2. Document the eventual consistency behavior of ACTOR_KV cleanup
  3. Add PR description

Post-merge:

  1. Add resurrection test cases
  2. Optimize kvListPrefix or document performance
  3. Consider schema versioning strategy

Overall: Approve with requested changes 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants