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

> Understand how Karukan's JSON-RPC protocol differentiates sync and async communication using the presence or absence of the id field for requests and notifications.

- Repository: [Hitoshi Togasaki/karukan](https://github.com/togatoga/karukan)
- Tags: internals
- Published: 2026-07-03

---

**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`](https://github.com/togatoga/karukan/blob/main/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`](https://github.com/togatoga/karukan/blob/main/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`:

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

```

### Response Handling

In **[`karukan-im/src/server/mod.rs`](https://github.com/togatoga/karukan/blob/main/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`](https://github.com/togatoga/karukan/blob/main/protocol.rs)** (lines 33–44), includes the consumed flag, UI actions, and timing metadata:

```json
{
  "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:

```json
{
  "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`](https://github.com/togatoga/karukan/blob/main/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`](https://github.com/togatoga/karukan/blob/main/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`](https://github.com/togatoga/karukan/blob/main/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`](https://github.com/togatoga/karukan/blob/main/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`](https://github.com/togatoga/karukan/blob/main/karukan-im/src/server/protocol.rs) while dispatch logic resides in [`karukan-im/src/server/mod.rs`](https://github.com/togatoga/karukan/blob/main/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`](https://github.com/togatoga/karukan/blob/main/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`](https://github.com/togatoga/karukan/blob/main/karukan-im/src/server/mod.rs)**, specifically within the `ImServer::handle_line` and `dispatch` functions.