How Karukan's JSON-RPC Protocol Handles Sync vs Async Communication

Karukan distinguishes synchronous requests from fire-and-forget operations solely by the presence or absence of the id field in the JSON-RPC envelope, where requests with an id expect a matching response while notifications without one execute silently.

The Karukan input method (IM) server communicates with its macOS frontend over stdin/stdout using a line-delimited JSON-RPC 2.0 protocol. When examining how the JSON-RPC protocol in Karukan manages different communication patterns, the implementation reveals a elegant binary split based on the request envelope structure.

The Protocol Distinction Mechanism

According to the Karukan source code, the distinction between blocking and non-blocking operations is enforced at the transport layer. In karukan-im/src/server/protocol.rs, the Request struct defines the JSON-RPC envelope, while the Response struct handles replies. The critical logic resides in karukan-im/src/server/mod.rs, where ImServer::handle_line processes incoming lines.

The protocol follows standard JSON-RPC 2.0 specifications with one simplification: if the id field is present, the server treats the message as a synchronous call requiring a response; if id is omitted, the server processes it as an asynchronous notification and returns nothing.

Synchronous Communication with process_key

Synchronous operations like process_key require the frontend to block until the IM engine returns a result. This pattern ensures the UI receives immediate feedback about whether a keystroke was consumed and what actions to display.

Request Structure

The frontend sends a request containing a unique id:

{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "process_key",
  "params": { 
    "keysym": 0x0061, 
    "modifiers": {}, 
    "is_release": false 
  }
}

Response Handling

In karukan-im/src/server/mod.rs (lines 60–71), handle_line parses the request and passes it to dispatch (lines 85–95). When an id exists, the server constructs a Response::success containing the matching id and a KeyResult payload. The KeyResult struct, defined in protocol.rs (lines 33–44), includes the consumed flag, UI actions, and timing metadata:

{
  "jsonrpc": "2.0",
  "id": 42,
  "result": {
    "consumed": true,
    "actions": [
      {
        "type": "update_preedit",
        "text": "あ",
        "caret": 1,
        "attributes": []
      }
    ],
    "conversion_ms": 5,
    "process_key_ms": 7
  }
}

The response line is written back to stdout, completing the synchronous round-trip.

Fire-and-Forget Notifications

For operations that do not require blocking the UI or returning data, Karukan uses fire-and-forget notifications. These reduce latency by eliminating the response overhead.

The Notification Pattern

When the frontend sends a request like set_surrounding_text without an id field:

{
  "jsonrpc": "2.0",
  "method": "set_surrounding_text",
  "params": { 
    "text": "こんにちは", 
    "cursor_pos": 5 
  }
}

The server still executes the operation through dispatch, but at line 77 in mod.rs, the expression let id = id?; evaluates to None. This causes handle_line to return None, and the server skips sending any JSON response. Errors are logged to stderr, but the client receives no acknowledgment.

Implementation Deep Dive

The dispatch logic in karukan-im/src/server/mod.rs handles both patterns through a single flow:

  1. Parsing: handle_line deserializes the JSON into a Request struct
  2. Routing: dispatch routes the method to the appropriate engine function (e.g., process_key implementation in karukan-im/src/core/engine/mod.rs)
  3. Branching: After execution, the code checks for the id field
    • Sync: Wraps the result in Response::success or Response::failure with the matching id
    • Async: Returns None immediately, suppressing output

This architecture allows the frontend to choose the appropriate communication mode for each operation. Unit tests in karukan-im/src/server/tests.rs verify that notifications result in no response lines, ensuring the protocol behaves correctly for both patterns.

Summary

  • Karukan's JSON-RPC protocol uses stdin/stdout with line-delimited JSON messages
  • Synchronous requests include an id field and receive a matching Response with KeyResult data
  • Fire-and-forget operations omit the id field, causing the server to execute without responding
  • Protocol definitions live in karukan-im/src/server/protocol.rs while dispatch logic resides in karukan-im/src/server/mod.rs
  • Error handling differs by mode: synchronous returns structured errors, async logs to stderr only

Frequently Asked Questions

What determines if a JSON-RPC call is synchronous or asynchronous in Karukan?

The presence of the id field in the request object determines the communication mode. If the request includes an id (such as "id": 42), the server treats it as a synchronous call and returns a response with the same id. If the id field is absent, the server treats it as a fire-and-forget notification and sends no response.

What happens when a fire-and-forget notification fails?

When a notification fails, the server does not send a JSON-RPC error response to the client. Instead, errors are reported only via stderr logs. The client continues execution without blocking, as the protocol guarantees no response will be sent for notifications.

Can any method be called as either synchronous or asynchronous?

Technically, the transport layer allows any method to be invoked with or without an id field. However, methods like process_key are designed to return KeyResult data, so calling them without an id would cause the server to execute the logic but discard the return value. Conversely, methods like set_surrounding_text work well as notifications since they only update state without returning data.

Where is the JSON-RPC protocol implemented in the Karukan repository?

The protocol envelope and data structures are defined in karukan-im/src/server/protocol.rs, including the Request, Response, and KeyResult structs. The request dispatcher and sync/async branching logic are implemented in karukan-im/src/server/mod.rs, specifically within the ImServer::handle_line and dispatch functions.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →