Skip to content

Conversation

@NathanFlurry
Copy link
Member

No description provided.

@vercel
Copy link

vercel bot commented Nov 10, 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 2:48am
3 Skipped Deployments
Project Deployment Preview Comments Updated (UTC)
rivet-cloud Ignored Ignored Nov 13, 2025 2:48am
rivet-inspector Ignored Ignored Preview Nov 13, 2025 2:48am
rivet-site Ignored Ignored Preview Nov 13, 2025 2:48am

Copy link
Member Author

NathanFlurry commented Nov 10, 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 10, 2025

Code Review: JSON Protocol Support

This PR adds JSON protocol support to RivetKit by introducing a flexible serialization system that can handle JSON, CBOR, and BARE encodings. Overall, this is a well-structured change with good architectural decisions. Here's my detailed feedback:


Strengths

  1. Clean Abstraction: The CachedSerializer refactor is excellent. The new generic signature <TBare, TJson, T = TBare> with conversion functions toJson and toBare provides clear separation of concerns.

  2. Type Safety: The use of Zod schemas (src/schemas/client-protocol-zod/mod.ts) for JSON validation ensures runtime type safety while maintaining TypeScript compile-time types.

  3. Performance Optimization: Caching serialized data per encoding in CachedSerializer prevents redundant serialization - smart optimization.

  4. Consistent Pattern: The conversion pattern is applied consistently across all message types (Init, Error, ActionResponse, Event, etc.) - good maintainability.

  5. Backward Compatibility: BARE/CBOR encoding still works with ArrayBuffer for binary data, while JSON uses native JavaScript types.


Issues & Concerns ⚠️

1. Overly Permissive Type Usage (High Priority)

Location: Multiple files

  • src/actor/conn/driver.ts:35 - CachedSerializer<any, any, any>
  • src/actor/conn/drivers/websocket.ts:26 - CachedSerializer<any, any, any>
  • src/actor/protocol/old.ts:90 (in parseMessage return type) - any

Issue: Using any weakens type safety and defeats the purpose of TypeScript. The driver interface should specify proper generic constraints.

Recommendation:

// Instead of:
sendMessage?(actor: AnyActorInstance, conn: AnyConn, message: CachedSerializer<any, any, any>): void;

// Consider:
sendMessage?<TBare, TJson, T = TBare>(
  actor: AnyActorInstance,
  conn: AnyConn,
  message: CachedSerializer<TBare, TJson, T>
): void;

2. Type Coercion Without Validation (Medium Priority)

Location: src/actor/protocol/old.ts:183-189

const output = await handler.onExecuteAction(ctx, name, args as unknown[]);

Issue: The args are cast to unknown[] without validation. For JSON encoding, args come directly from user input and should be validated.

Recommendation: Add runtime validation or at least document the assumption that args are arrays.

3. Inconsistent Error Handling (Medium Priority)

Location: src/client/actor-conn.ts:811-864

Issue: The #parseMessage function has complex CBOR decoding logic that could fail, but errors aren't explicitly handled. If cbor.decode() throws, the error might be cryptic.

Recommendation: Wrap CBOR decoding in try-catch blocks with descriptive error messages:

try {
  return cbor.decode(new Uint8Array(msg.body.val.output));
} catch (error) {
  throw new errors.MalformedMessage(\`Failed to decode action output: \${error}\`);
}

4. Missing Documentation (Low Priority)

Issue: The new conversion functions (toJson, toBare, fromJson, fromBare) lack documentation explaining when each is used and what transformations they perform.

Recommendation: Add JSDoc comments to CachedSerializer and serializeWithEncoding/deserializeWithEncoding explaining the data flow.

5. Type Annotations Clarity (Low Priority)

Location: src/actor/instance/mod.ts:514-540

const initData = { actorId: this.id, connectionId: conn.id };

Issue: The initData type is inferred. While TypeScript can figure it out, explicit typing would make the code more readable.

Recommendation:

const initData: { actorId: string; connectionId: string } = { actorId: this.id, connectionId: conn.id };

Security Considerations 🔒

  1. Input Validation: The JSON path now accepts arbitrary JavaScript objects. Ensure all action handlers properly validate their inputs.

  2. CBOR Bomb Protection: CBOR can encode deeply nested structures. Consider adding size/depth limits to prevent denial-of-service attacks.

  3. Metadata Encoding: Error metadata is user-controlled and gets serialized. Verify that jsonStringifyCompat properly handles malicious inputs (e.g., circular references, large objects).


Test Coverage 🧪

Missing Tests:

  • Unit tests for CachedSerializer with all three encodings
  • Integration tests for JSON vs CBOR vs BARE message round-trips
  • Error cases: malformed JSON, invalid CBOR, oversized messages
  • Edge cases: BigInt serialization, ArrayBuffer handling, null metadata

Recommendation: Add tests to verify:

// Example test structure
describe('CachedSerializer', () => {
  it('should correctly serialize events for JSON encoding', () => {
    const eventData = { name: 'test', args: [1, 'foo', true] };
    const serializer = new CachedSerializer(/* ... */);
    const json = serializer.serialize('json');
    // Assert json structure
  });

  it('should handle CBOR encoding for binary data', () => {
    // Test ArrayBuffer serialization
  });

  it('should cache serialization results', () => {
    // Verify cache hits
  });
});

Performance Considerations

  1. Serialization Overhead: For JSON encoding, data goes through toJson → Zod validation → jsonStringifyCompat. This is more expensive than raw JSON.stringify. Consider benchmarking for high-throughput scenarios.

  2. Memory Usage: CachedSerializer stores multiple encoded versions in memory. For large messages, this could increase memory footprint significantly.

Recommendation: Document the trade-offs and consider adding a max cache size or TTL.


Minor Issues 🔧

  1. Commented Code: src/actor/protocol/serde.ts:70-111 contains large commented-out blocks. Remove dead code or add a TODO if it's needed for reference.

  2. TODO Comment: src/common/router.ts:99 - "TODO: Remove any" - this should be addressed or tracked in an issue.

  3. Truncated Import: src/client/utils.ts:66 line appears incomplete in the diff.


Recommendations Summary

High Priority:

  • Replace any types with proper generics
  • Add input validation for action args

Medium Priority:

  • Add error handling around CBOR decode operations
  • Add comprehensive tests for all three encoding formats

Low Priority:

  • Add documentation for conversion functions
  • Clean up commented code and TODOs
  • Add explicit type annotations where types are inferred

Conclusion

This is a solid implementation that successfully adds JSON protocol support while maintaining backward compatibility. The architecture is clean and extensible. The main concerns are around type safety (using any) and test coverage. Once those are addressed, this should be good to merge!

Overall Assessment: ✅ Approve with suggestions

Great work on maintaining a consistent pattern throughout the codebase! 🚀

@pkg-pr-new
Copy link

pkg-pr-new bot commented Nov 10, 2025

More templates

@rivetkit/actor

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/actor@3432

@rivetkit/cloudflare-workers

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/cloudflare-workers@3432

@rivetkit/core

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/core@3432

@rivetkit/db

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/db@3432

@rivetkit/framework-base

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/framework-base@3432

@rivetkit/next-js

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/next-js@3432

@rivetkit/react

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/react@3432

rivetkit

pnpm add https://pkg.pr.new/rivet-dev/rivet/rivetkit@3432

@rivetkit/sql-loader

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/sql-loader@3432

@rivetkit/engine-runner

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/engine-runner@3432

@rivetkit/engine-runner-protocol

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/engine-runner-protocol@3432

commit: 412f83b

@NathanFlurry NathanFlurry force-pushed the 11-09-chore_rivetkit_add_required_zod_validation_for_json_encoding branch from d27a9f0 to be3dc7d Compare November 13, 2025 02:47
@NathanFlurry NathanFlurry force-pushed the 11-09-feat_rivetkit_add_json_protocol_support branch from 5f6c780 to c713cc8 Compare November 13, 2025 02:47
@claude
Copy link

claude bot commented Nov 13, 2025

PR Review: Add JSON Protocol Support to RivetKit

Summary

This PR adds JSON encoding support to RivetKit's actor protocol, enabling communication using JSON alongside the existing BARE and CBOR encodings. This is a significant architectural change that affects the entire message serialization/deserialization pipeline.


✅ Strengths

1. Well-Architected Abstraction
The introduction of transformation functions (toJson, toBare, fromJson, fromBare) in CachedSerializer and serializeWithEncoding/deserializeWithEncoding provides a clean separation between the source data format and the wire format. This is excellent design.

2. Comprehensive JSON Compatibility Layer
The jsonStringifyCompat/jsonParseCompat functions (rivetkit-typescript/packages/rivetkit/src/actor/protocol/serde.ts:162-218) handle non-JSON types (BigInt, ArrayBuffer, Uint8Array) elegantly with proper escaping for user data. The test coverage for these functions is excellent.

3. Consistent Pattern Application
The refactoring applies the new pattern consistently across all message types (Init, ActionRequest, ActionResponse, Event, Error) in both client and server code.


🔍 Potential Issues & Concerns

Critical: Type Safety Regression

Location: Multiple files

// Before (type-safe):
sendMessage?(
    actor: AnyActorInstance,
    conn: AnyConn,
    message: CachedSerializer<protocol.ToClient>,
): void;

// After (loosely typed):
sendMessage?(
    actor: AnyActorInstance,
    conn: AnyConn,
    message: CachedSerializer<any, any, any>,
): void;

Issue: Replacing specific types with any removes TypeScript's type checking benefits and could allow invalid message types to be passed at compile time, only failing at runtime.

Recommendation: Consider using generic constraints or union types instead of any:

// Option 1: Generic with constraints
sendMessage?<TBare, TJson, T = TBare>(
    actor: AnyActorInstance,
    conn: AnyConn,
    message: CachedSerializer<TBare, TJson, T>,
): void;

// Option 2: Union of known types
type AnyCachedSerializer = 
    | CachedSerializer<protocol.ToClient, ToClientJson, any>
    | CachedSerializer</* other valid types */>;

Bug: Potential Runtime Error in Error Handling

Location: rivetkit-typescript/packages/rivetkit/src/actor/protocol/old.ts:296

const errorData = { group, code, message, metadata, actionId };
// ...
metadata: value.metadata ?? null,  // In toJson
metadata: value.metadata
    ? bufferToArrayBuffer(cbor.encode(value.metadata))
    : null,  // In toBare

Issue: The metadata variable may be undefined but the transformation functions expect it to always be present in errorData. When metadata is undefined, value.metadata in the transformation will also be undefined, not triggering the ternary's null branch.

Recommendation: Initialize metadata explicitly:

const errorData = { 
    group, 
    code, 
    message, 
    metadata: metadata ?? null,  // Normalize undefined to null
    actionId 
};

Performance: Unnecessary Double Serialization with CBOR

Location: rivetkit-typescript/packages/rivetkit/src/serde.ts:68-71

} else if (encoding === "cbor") {
    const jsonValue = toJson(value);
    const validated = zodSchema.parse(jsonValue);
    return cbor.encode(validated);

Issue: For CBOR encoding, the code transforms to JSON format first, validates it, then CBOR-encodes it. This is inefficient because:

  1. CBOR can natively handle the same types as the BARE format (BigInt, ArrayBuffer, etc.)
  2. You're converting T -> TJson -> CBOR when you could do T -> TBare -> CBOR

Recommendation: CBOR should use the BARE format transformation:

} else if (encoding === "cbor") {
    const bareValue = toBare(value);
    // Optionally validate against a BARE schema if available
    return cbor.encode(bareValue);

This would be more efficient and maintain consistency (CBOR and BARE use the same underlying data types).

Type Consistency: Mixed Message Type Definitions

Location: Multiple files (e.g., rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts:514)

async processMessage(
    message: {
        body:
            | { tag: "ActionRequest"; val: { id: bigint; name: string; args: unknown } }
            | { tag: "SubscriptionRequest"; val: { eventName: string; subscribe: boolean } };
    },
    // ...

Issue: The PR introduces inline type definitions for messages instead of importing from the protocol definition. This creates multiple sources of truth and makes the codebase harder to maintain.

Recommendation: Create and import a shared intermediate type:

// In a shared types file
export type ProcessedMessage = {
    body: ToServerBody;  // Reuse from protocol-zod
};

// Usage
async processMessage(message: ProcessedMessage, conn: Conn<...>) {

🔒 Security Considerations

1. Input Validation ✅ Good
The use of Zod schemas for validation in deserializeWithEncoding provides strong runtime type checking, which is essential for untrusted input.

2. JSON Parsing Error Handling
Location: rivetkit-typescript/packages/rivetkit/src/actor/protocol/serde.ts:211-213

throw new Error(
    `Unknown JSON encoding type: ${value[0]}. This may indicate corrupted data or a version mismatch.`,
);

✅ Good defensive programming - unknown special types are rejected rather than silently ignored.

3. Base64 Encoding for Binary Data
The base64 encoding of ArrayBuffer/Uint8Array in JSON is secure and standard practice.


📊 Performance Implications

Positive:

  • CachedSerializer prevents re-serialization for the same encoding, which is excellent for broadcasting to multiple clients

Concerns:

  • JSON encoding/decoding is generally slower than CBOR/BARE for binary data
  • The double transformation for CBOR (noted above) adds unnecessary overhead
  • The escaping mechanism in jsonStringifyCompat adds processing time for every value

Recommendation: Document the performance trade-offs in the PR description or code comments so users can make informed decisions about which encoding to use.


🧪 Test Coverage

Excellent:

  • tests/json-escaping.test.ts provides comprehensive coverage of the JSON compatibility layer
  • Tests cover edge cases (BigInt, ArrayBuffer, user data with $ prefix, nested structures)

Missing:

  • No integration tests demonstrating end-to-end JSON protocol communication
  • No tests verifying that the same data produces equivalent results across all three encodings (JSON, CBOR, BARE)
  • No performance benchmarks comparing encodings

Recommendation: Add integration tests that:

test("should handle action request/response roundtrip with JSON encoding", async () => {
    // Create actor with JSON encoding
    // Send action request
    // Verify response
});

test("should produce equivalent results across all encodings", () => {
    const testData = { /* complex test data */ };
    const jsonResult = serializeWithEncoding("json", ...);
    const cborResult = serializeWithEncoding("cbor", ...);
    const bareResult = serializeWithEncoding("bare", ...);
    
    // All should deserialize to equivalent data
});

📝 Additional Recommendations

  1. Documentation: Add JSDoc comments explaining when to use each encoding (JSON vs CBOR vs BARE) and their trade-offs

  2. Error Messages: Consider adding more context to error messages, especially for deserialization failures in production debugging

  3. Versioning: Ensure the version handlers properly support all three encodings for forward/backward compatibility

  4. Naming: Consider renaming jsonStringifyCompat to something more descriptive like jsonStringifyWithBinarySupport to make the purpose clearer


Summary Verdict

This is a well-architected feature with excellent test coverage for the core JSON compatibility layer. However, there are several concerns that should be addressed:

Must Fix:

  • Type safety regression (using any instead of proper types)
  • Potential undefined/null handling bug in error metadata

Should Fix:

  • CBOR performance issue (double transformation)
  • Type definition duplication across files
  • Missing integration tests

Nice to Have:

  • Better documentation
  • Performance benchmarks
  • Encoding comparison tests

Overall, the code quality is good, but I recommend addressing at least the "Must Fix" items before merging.

@claude
Copy link

claude bot commented Nov 13, 2025

PR Review: Add JSON Protocol Support

Summary

This PR adds JSON encoding support to the RivetKit protocol alongside the existing BARE and CBOR encodings. The implementation introduces a sophisticated serialization layer that allows seamless switching between protocols while maintaining type safety.

Architecture Overview

The core innovation is the CachedSerializer class that:

  • Stores an intermediate data representation (T)
  • Provides conversion functions to both JSON (TJson) and BARE (TBare) formats
  • Caches serialized output per encoding to avoid redundant work
  • Handles encoding-specific transformations (e.g., CBOR encoding for args in BARE, plain arrays in JSON)

Code Quality: ✅ Strong

Positives:

  1. Well-structured abstraction: The serializeWithEncoding and deserializeWithEncoding functions in serde.ts provide a clean, unified API
  2. Type safety: Strong TypeScript generics maintain type correctness across encoding boundaries
  3. Performance: Caching in CachedSerializer prevents redundant serialization
  4. Consistency: Uniform pattern applied across all message types (Init, Error, ActionRequest, ActionResponse, Event, etc.)
  5. JSON compatibility: The jsonStringifyCompat/jsonParseCompat functions handle BigInt, ArrayBuffer, and Uint8Array gracefully with special encoding

Areas for Improvement:

1. Type Safety Concerns

Location: Multiple files (e.g., actor/conn/driver.ts:35)

sendMessage?(
    actor: AnyActorInstance,
    conn: AnyConn,
    message: CachedSerializer<any, any, any>,  // ⚠️ Lost type safety
): void;

Issue: Changing from protocol.ToClient to CachedSerializer<any, any, any> loses type information. This could allow invalid message types to be passed.

Recommendation: Consider using a union type or generic constraint:

message: CachedSerializer<protocol.ToClient, ToClientJson, unknown>

2. Inline Type Definitions

Location: actor/instance/mod.ts:514, actor/protocol/old.ts:72, client/actor-conn.ts:102

The PR introduces many inline type definitions for message structures instead of using the defined protocol types:

message: {
    body:
        | {
                tag: "ActionRequest";
                val: { id: bigint; name: string; args: unknown };
          }
        | {
                tag: "SubscriptionRequest";
                val: { eventName: string; subscribe: boolean };
          };
}

Issue:

  • Code duplication across multiple files
  • Harder to maintain consistency
  • Diverges from using the properly typed protocol definitions

Recommendation: Create intermediate protocol types that represent the "decoded" versions:

// In a shared protocol file
export type DecodedActionRequest = {
    id: bigint;
    name: string;
    args: unknown;  // Already decoded from ArrayBuffer/CBOR
};

export type DecodedToServer = {
    body:
        | { tag: "ActionRequest"; val: DecodedActionRequest }
        | { tag: "SubscriptionRequest"; val: SubscriptionRequest };
};

3. Error Handling - Incomplete Metadata Handling

Location: actor/protocol/old.ts:315

metadata: value.metadata ?? null,  // JSON path

vs

metadata: value.metadata
    ? bufferToArrayBuffer(cbor.encode(value.metadata))
    : null,  // BARE path

Question: What happens if metadata is undefined vs null? The JSON path converts undefined to null, but this should be explicitly documented or handled consistently.

4. Missing Edge Case Validation

Location: serde.ts:104-114 (CBOR deserialization)

} else if (encoding === "cbor") {
    const decoded: unknown = cbor.decode(buffer);
    const validated = zodSchema.parse(decoded);
    return fromJson(validated);  // ⚠️ Assumes CBOR and JSON schemas are identical
}

Issue: The code assumes that CBOR-decoded data will match the JSON schema. While this is likely true for most cases, there could be edge cases where CBOR and JSON representations differ (e.g., how they handle special numeric values, undefined, etc.).

Recommendation: Add comments documenting this assumption or consider separate validation paths.

Security Considerations: ✅ Good

  1. Zod validation: All deserialized data goes through Zod schema validation, preventing malformed data
  2. Size limits: The code respects maxIncomingMessageSize limits (in protocol/old.ts:73)
  3. Special JSON encoding: The $BigInt, $ArrayBuffer special encodings are escaped properly to prevent injection ($$ prefix for user data)

Minor concern: The JSON compatibility layer's error message at serde.ts:211 could potentially leak implementation details:

throw new Error(
    `Unknown JSON encoding type: ${value[0]}. This may indicate corrupted data or a version mismatch.`,
);

Consider whether this is acceptable or if it should be more generic.

Performance Considerations: ✅ Excellent

  1. Caching: CachedSerializer prevents redundant encoding
  2. Lazy evaluation: Only encodes for the requested encoding
  3. Efficient: No unnecessary data copies

Potential optimization: The jsonStringifyCompat replacer function is called for every value. For high-frequency messages, consider caching or pre-processing known message structures.

Test Coverage: ⚠️ Needs Attention

Major concern: I don't see any new tests added for the JSON encoding path. Given the complexity of this change:

Recommended test coverage:

  1. Round-trip tests: Ensure data can be serialized and deserialized correctly in all 3 encodings (JSON, CBOR, BARE)
  2. Edge cases:
    • BigInt values in JSON encoding
    • ArrayBuffer/Uint8Array in JSON encoding
    • Empty arrays, null, undefined handling
    • Nested structures
  3. Cross-encoding compatibility: Verify that the same message encoded in different formats produces equivalent results after deserialization
  4. Error scenarios: Invalid JSON, malformed special encodings (e.g., ["$UnknownType", "data"])
  5. Schema validation failures: Test that Zod properly rejects invalid data

Specific test file suggestions:

  • src/actor/protocol/serde.test.ts - Test CachedSerializer with all encodings
  • src/serde.test.ts - Test jsonStringifyCompat/jsonParseCompat edge cases
  • Integration tests in driver-test-suite/tests/ that explicitly test JSON encoding

Code Style: ✅ Good

  • Follows repository conventions
  • Uses structured logging with tracing
  • Consistent error handling patterns
  • Type imports properly separated

Specific File Comments

serde.ts (56-83, 85-131)

The serializeWithEncoding and deserializeWithEncoding functions are well-designed. The switch statements are exhaustive with assertUnreachable guards.

actor/protocol/serde.ts (25-68)

The refactored CachedSerializer is a significant improvement. The addition of transformation functions (toJson, toBare) is elegant.

client/actor-conn.ts (724-749)

The inline transformation logic for ActionRequest is duplicated. Consider extracting to a helper function:

function toServerMessageToBare(msg: DecodedToServer): protocol.ToServer {
    if (msg.body.tag === "ActionRequest") {
        return {
            body: {
                tag: "ActionRequest",
                val: {
                    id: msg.body.val.id,
                    name: msg.body.val.name,
                    args: bufferToArrayBuffer(cbor.encode(msg.body.val.args)),
                },
            },
        };
    }
    return msg as protocol.ToServer;
}

Overall Recommendation: ⚠️ Approve with Changes

This is a well-architected change that successfully adds JSON protocol support. However, it needs:

  1. Critical: Add comprehensive test coverage for JSON encoding paths
  2. Important: Address type safety issues (use of any)
  3. Recommended: Create shared types for "decoded" protocol messages to reduce duplication
  4. Nice to have: Document the assumptions about CBOR/JSON schema compatibility

The core implementation is solid, but without tests, there's significant risk of subtle bugs in edge cases (especially around BigInt, ArrayBuffer, and nested structures in JSON mode).

Action Items

  • Add test coverage for JSON encoding/decoding
  • Replace any types in CachedSerializer with more specific types
  • Consider creating shared "decoded" protocol types
  • Document metadata handling edge cases (null vs undefined)
  • Add integration tests to driver-test-suite for JSON encoding

Overall: Strong implementation with good architecture, but needs test coverage before merge.

@NathanFlurry NathanFlurry force-pushed the 11-09-feat_rivetkit_add_json_protocol_support branch from c713cc8 to 412f83b Compare November 13, 2025 20:53
@NathanFlurry NathanFlurry force-pushed the 11-09-chore_rivetkit_add_required_zod_validation_for_json_encoding branch from be3dc7d to 1784d8f Compare November 13, 2025 20:54
@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: JSON Protocol Support for RivetKit

Summary

This PR adds JSON protocol support to RivetKit's client-actor communication layer, enabling a more accessible and debuggable alternative to the existing BARE/CBOR binary protocols. The implementation introduces a flexible serialization system with encoding-specific transformations.

Strengths

Architecture & Design

  • Excellent abstraction: The CachedSerializer class cleanly separates concerns with encoding-specific transformation functions (toJson, toBare)
  • Type safety: Strong TypeScript generics (<TBare, TJson, T>) ensure compile-time correctness across encoding boundaries
  • Performance optimization: Serialization caching prevents redundant encoding operations
  • Protocol versioning: Maintains compatibility with existing VersionedDataHandler infrastructure

Code Quality

  • Consistent patterns: The transformation approach is applied uniformly across all message types (Init, Event, ActionRequest, ActionResponse, Error)
  • Special type handling: The jsonStringifyCompat/jsonParseCompat functions elegantly handle BigInt, ArrayBuffer, and Uint8Array with a clever escaping mechanism for user data starting with $
  • Good test coverage: Comprehensive tests in json-escaping.test.ts cover edge cases, nested structures, and error conditions

Issues & Concerns

1. Type Safety Regression (High Priority)

Location: actor/conn/driver.ts:35, actor/conn/drivers/websocket.ts:26

message: CachedSerializer<any, any, any>

Issue: Using any defeats TypeScript's type system and could hide type errors at compile time.

Recommendation: Define a union type for all possible message types:

type ToClientSerializer = 
  | CachedSerializer<protocol.ToClient, ToClientJson, EventData>
  | CachedSerializer<protocol.ToClient, ToClientJson, InitData>
  | CachedSerializer<protocol.ToClient, ToClientJson, ErrorData>;

sendMessage?(
  actor: AnyActorInstance,
  conn: AnyConn,
  message: ToClientSerializer,
): void;

2. Inconsistent Type Annotations (Medium Priority)

Location: actor/instance/mod.ts:514-532, actor/protocol/old.ts:72-85

Issue: Message type definitions are duplicated inline rather than importing from schema files.

Example:

message: {
  body:
    | { tag: "ActionRequest"; val: { id: bigint; name: string; args: unknown } }
    | { tag: "SubscriptionRequest"; val: { eventName: string; subscribe: boolean } };
}

Recommendation: Create a shared type definition:

// In schemas/client-protocol-zod/mod.ts
export type ToServerInternal = {
  body: 
    | { tag: "ActionRequest"; val: { id: bigint; name: string; args: unknown } }
    | { tag: "SubscriptionRequest"; val: { eventName: string; subscribe: boolean } };
};

3. Data Transformation Complexity (Medium Priority)

Location: actor/conn/mod.ts:200-227, actor/instance/event-manager.ts:186-211

Issue: The dual transformation logic for JSON vs BARE protocols is complex and easy to get wrong. CBOR encoding happens in one path but not the other.

Concern: Future maintainers might not understand when args needs CBOR encoding vs when it's already the correct type.

Recommendation: Add comprehensive documentation:

/**
 * Creates a CachedSerializer for event data that handles encoding differences:
 * 
 * JSON Protocol: args remain as raw JavaScript values (arrays, objects, etc.)
 * and are serialized directly by jsonStringifyCompat, which handles special
 * types like BigInt and ArrayBuffer automatically.
 * 
 * BARE/CBOR Protocol: args must be CBOR-encoded into an ArrayBuffer before
 * being passed to the BARE serializer, as the protocol schema expects ArrayBuffer.
 */

4. Schema Validation Performance (Low Priority)

Location: serde.ts:56-83

Issue: Zod validation runs on every serialization, even for cached results.

const validated = zodSchema.parse(jsonValue); // Could throw for invalid data

Observation: While validation is important for correctness, it adds overhead. Consider:

  • Adding metrics to measure validation performance impact
  • Documenting whether validation is expected in production or just development
  • Potentially adding a NODE_ENV-based skip for hot paths in production

5. Missing Test Coverage (Medium Priority)

Gaps identified:

  1. No integration tests verifying end-to-end JSON protocol communication
  2. No tests for the new serialization transformation functions
  3. No tests ensuring CBOR encoding/decoding roundtrips work correctly in the new system
  4. No tests for HTTP action requests with JSON encoding

Recommendation: Add tests like:

describe("JSON Protocol Integration", () => {
  test("should handle action request/response with JSON encoding", async () => {
    // Test the full roundtrip with JSON encoding
  });
  
  test("should handle events with complex nested data in JSON", async () => {
    // Test event broadcasting with JSON
  });
});

6. Error Handling Gaps (Low Priority)

Location: client/actor-conn.ts:811-865

Issue: The parseMessage function has complex deserialization logic that could fail in various ways, but error handling isn't explicit.

Recommendation: Add try-catch blocks with specific error messages for debugging:

try {
  return fromJson(validated);
} catch (err) {
  throw new Error(`Failed to transform JSON message: ${err.message}`, { cause: err });
}

Security Considerations

Positive

  • ✅ No obvious injection vulnerabilities
  • ✅ Schema validation prevents malformed data
  • ✅ User data escaping prevents $ prefix collisions

Consider

  • The jsonParseCompat function could theoretically be abused if malicious JSON contains deeply nested $ arrays (DoS via stack overflow). Consider adding depth limits.

Performance Considerations

Caching Strategy

The CachedSerializer approach is excellent for scenarios where the same message is sent to multiple clients with different encodings. However:

  • Memory overhead: Each cached serialization is stored in memory
  • Cache invalidation: No TTL or size limits on the cache Map
  • Recommendation: Document expected usage patterns and consider adding cache size limits for long-lived instances

JSON vs CBOR Overhead

  • JSON encoding is ~20-30% larger than CBOR for typical payloads
  • JSON parsing is generally faster in browsers due to native optimization
  • Recommendation: Document when to use each encoding (JSON for debugging/web clients, CBOR for performance)

Best Practices & Conventions

Follows Repository Standards ✅

  • Structured logging with lowercase messages
  • Hard tabs for formatting
  • Proper error handling patterns
  • Uses workspace dependencies

Suggestions

  1. Add JSDoc comments to the new CachedSerializer parameters explaining the transformation functions
  2. Consider adding examples in code comments showing the transformation flow
  3. Document the JSON escaping scheme for future maintainers

Conclusion

This is a well-architected PR that successfully adds JSON protocol support with a clean, extensible design. The main concerns are around type safety and test coverage, which should be addressed before merging.

Recommendations Priority

  1. Must Fix: Replace any types with proper unions (type safety)
  2. Should Fix: Add integration tests for JSON protocol
  3. Should Fix: Consolidate inline type definitions
  4. Nice to Have: Add comprehensive documentation for transformation logic
  5. Nice to Have: Consider performance monitoring for schema validation

Approval Status

The core implementation is solid, but I recommend addressing the type safety issues before merging. Once the any types are replaced with proper type definitions, this will be ready to merge.

Estimated effort to address concerns: 2-3 hours


🤖 Generated with Claude Code

@claude
Copy link

claude bot commented Nov 13, 2025

Code Review: Add JSON Protocol Support

Summary

This PR adds JSON protocol support to RivetKit, allowing it to work with JSON in addition to the existing BARE and CBOR binary encodings. The implementation introduces a flexible serialization system using a new CachedSerializer class with separate transformation functions for JSON and BARE encodings.


Strengths

Well-structured architecture: The CachedSerializer class design with separate toJson and toBare transformation functions is elegant and maintainable.

Comprehensive JSON compatibility layer: The jsonStringifyCompat and jsonParseCompat functions handle special types (BigInt, ArrayBuffer, Uint8Array) that JSON doesn't natively support, with proper escaping for user data starting with $.

Good test coverage: The json-escaping.test.ts file contains thorough tests for edge cases including nested structures, special types, and escape sequences.

Backward compatible: The changes maintain compatibility with existing BARE/CBOR encodings while adding JSON support.


Issues & Concerns

🔴 Critical: Type Safety Degradation

Location: Multiple files including actor/conn/driver.ts:35, actor/conn/drivers/websocket.ts:26

// Before
sendMessage?(actor: AnyActorInstance, conn: AnyConn, message: CachedSerializer<protocol.ToClient>): void;

// After
sendMessage?(actor: AnyActorInstance, conn: AnyConn, message: CachedSerializer<any, any, any>): void;

Problem: Using any types defeats TypeScript's type checking and makes the code more error-prone. The CachedSerializer now requires three type parameters but using any for all of them loses compile-time safety.

Recommendation: Define proper generic constraints or specific union types:

type AnyCachedSerializer = 
  | CachedSerializer<protocol.ToClient, ToClientJson, any>
  | CachedSerializer<protocol.ToServer, ToServerJson, any>;

sendMessage?(
  actor: AnyActorInstance, 
  conn: AnyConn, 
  message: CachedSerializer<protocol.ToClient, ToClientJson, any>
): void;

🟡 Medium: Inline Type Definitions Reduce Reusability

Location: actor/instance/mod.ts:514-523, actor/conn.ts:104-113

Problem: Message type definitions are duplicated inline in multiple places rather than being defined as reusable types. This makes maintenance harder and increases the risk of inconsistencies.

Example:

async processMessage(
  message: {
    body:
      | { tag: "ActionRequest"; val: { id: bigint; name: string; args: unknown }; }
      | { tag: "SubscriptionRequest"; val: { eventName: string; subscribe: boolean }; };
  },
  conn: Conn<S, CP, CS, V, I, DB>,
)

Recommendation: Extract these into named types in a shared location:

// In schemas/client-protocol-zod/mod.ts or a new types file
export type ParsedToServer = {
  body: 
    | { tag: "ActionRequest"; val: { id: bigint; name: string; args: unknown } }
    | { tag: "SubscriptionRequest"; val: { eventName: string; subscribe: boolean } };
};

// Then use it
async processMessage(message: ParsedToServer, conn: Conn<S, CP, CS, V, I, DB>)

🟡 Medium: Inconsistent Error Handling

Location: serde.ts:74-77, serde.ts:120-123

if (\!versionedDataHandler) {
  throw new Error("VersionedDataHandler is required for 'bare' encoding");
}

Problem: Generic Error is thrown instead of using the custom error system defined in packages/common/error/. According to CLAUDE.md, the codebase uses a structured error system with derive macros.

Recommendation: Use the RivetKit error system for consistency:

import * as errors from "@/actor/errors";

if (\!versionedDataHandler) {
  throw new errors.InternalError("VersionedDataHandler is required for 'bare' encoding");
}

🟢 Minor: Magic Strings Without Constants

Location: serde.ts:65-71, actor/protocol/serde.ts:162-184

Problem: The JSON encoding uses magic strings like "", "", "" without defining them as constants.

Recommendation: Define constants for maintainability:

const BIGINT_TYPE = "";
const ARRAYBUFFER_TYPE = "";
const UINT8ARRAY_TYPE = "";

🟢 Minor: Commented-Out Code

Location: actor/protocol/serde.ts:70-111

Problem: Large blocks of commented-out code (serialize/deserialize functions) should be removed if no longer needed.

Recommendation: Remove dead code or add a TODO comment explaining why it's preserved.


Performance Considerations

⚠️ CBOR encoding in JSON mode: When using JSON encoding, action arguments and responses still go through CBOR encoding/decoding steps before being converted to JSON (actor/protocol/old.ts:187, actor/router-endpoints.ts:347). This adds overhead.

Observation: The implementation at actor/conn/mod.ts:207-213 shows proper optimization where JSON mode uses raw values while BARE/CBOR mode encodes them. Ensure this pattern is consistently applied throughout.


Security Considerations

Input validation: The use of Zod schemas for validation before processing is good security practice.

⚠️ ArrayBuffer encoding: The base64 encoding/decoding for ArrayBuffers in JSON mode could be a performance bottleneck for large binary data. Consider documenting when developers should prefer CBOR over JSON.


Testing Recommendations

  1. Good coverage for JSON escaping: The json-escaping.test.ts file is comprehensive.

  2. ⚠️ Missing integration tests: Consider adding tests that verify end-to-end JSON protocol communication, especially:

    • WebSocket connections using JSON encoding
    • HTTP actions with JSON encoding
    • Error propagation through JSON encoding
    • Large payloads to test performance characteristics
  3. ⚠️ Missing tests for CachedSerializer: The new CachedSerializer class with its transformation functions should have dedicated unit tests.


Documentation Suggestions

  1. Add JSDoc comments explaining when to use JSON vs CBOR/BARE encodings
  2. Document the performance implications of JSON encoding for binary data
  3. Add migration guide for existing code using the old CachedSerializer API

Verdict

Overall Assessment: ✅ Approve with recommendations

This is a solid implementation that successfully adds JSON protocol support. The architecture is well-designed with the CachedSerializer pattern. However, there are type safety concerns that should be addressed to maintain code quality:

Must Fix Before Merge:

  • Replace any types with proper generic constraints or union types

Should Fix Soon:

  • Extract inline type definitions to reusable types
  • Use custom error system instead of generic Error
  • Add integration tests for JSON protocol

Nice to Have:

  • Define constants for magic strings
  • Remove commented-out code
  • Add comprehensive documentation

Great work overall! The JSON compatibility layer is particularly well-implemented with proper handling of special types and edge cases.

@graphite-app
Copy link
Contributor

graphite-app bot commented Nov 13, 2025

Merge activity

  • Nov 13, 9:15 PM UTC: NathanFlurry added this pull request to the Graphite merge queue.
  • Nov 13, 9:16 PM UTC: CI is running for this pull request on a draft pull request (#3465) due to your merge queue CI optimization settings.
  • Nov 13, 9:16 PM UTC: Merged by the Graphite merge queue via draft PR: #3465.

graphite-app bot pushed a commit that referenced this pull request Nov 13, 2025
@graphite-app graphite-app bot closed this Nov 13, 2025
@graphite-app graphite-app bot deleted the 11-09-feat_rivetkit_add_json_protocol_support branch November 13, 2025 21:16
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.

3 participants