Refactoring Guru: Adapter
kkrpc is a RPC protocol I developed for Kunkun, that’s what kk
stands for.
Also see the blog kkrpc.
Its bidirectional RPC channel that support many environments
- stdio
- deno
- bun
- node
- iframe
- web worker
- browser main thread
- Chrome Extension
- Tauri
- http
- WebSocket
As long as you can setup a connection between 2 environments, kkrpc supports it. e.g.
postMessage
,stdio
(stdin
andstdout
).
So how does all of these environments communicate?
The answer is adapter.
Each environment has an adapter that implements the following interface.
export interface IoInterface {
name: string;
read(): Promise<Buffer | Uint8Array | string | null>; // Reads input
write(data: string): Promise<void>; // Writes output
}
For example, kkrpc provides NodeIo
adapter, DenoIo
adapter, TauriShellStdio
adapter.
Here is an example: TauriShellStdio
import {
Child,
EventEmitter,
type OutputEvents,
} from "@tauri-apps/plugin-shell";
import type { IoInterface } from "../interface";
export class TauriShellStdio implements IoInterface {
name = "tauri-shell-stdio";
constructor(
private readStream: EventEmitter<OutputEvents<string>>, // stdout of child process
private childProcess: Child
) {}
read(): Promise<string | Uint8Array | null> {
return new Promise((resolve, reject) => {
this.readStream.on("data", (chunk) => {
resolve(chunk);
});
});
}
async write(data: string): Promise<void> {
return this.childProcess.write(data + "\n");
}
}
Another example: DenoIo
import { Buffer } from "node:buffer";
import type { IoInterface } from "../interface.ts";
/**
* Stdio implementation for Deno
* Deno doesn't have `process` object, and have a completely different stdio API,
* This implementation wrap Deno's `Deno.stdin` and `Deno.stdout` to follow StdioInterface
*/
export class DenoIo implements IoInterface {
private reader: ReadableStreamDefaultReader<Uint8Array>;
name = "deno-io";
constructor(
private readStream: ReadableStream<Uint8Array> // private writeStream: WritableStream<Uint8Array>
) {
this.reader = this.readStream.getReader();
// const writer = this.writeStream.getWriter()
// const encoder = new TextEncoder()
// writer.write(encoder.encode("hello"))
}
async read(): Promise<Buffer | null> {
const { value, done } = await this.reader.read();
if (done) {
return null; // End of input
}
return Buffer.from(value);
}
write(data: string): Promise<void> {
const encoder = new TextEncoder();
const encodedData = encoder.encode(data + "\n");
Deno.stdout.writeSync(encodedData);
return Promise.resolve();
}
}
To support bidirectional communication the adapter should be able to read from and write to the channel. As long as the environments translate their data to what the channel understands, the other side will understand. Adapters are like human translators, they must be able to listen (read) and speak (write) a language to communicate.
Analogy
Here is an analogy. A Chinese and and French president are having a meeting, there is a Chinese translator who know Chinese and English, and a French translator who know French and English. Then the 2 translators can communicate through English. In this analogy, English language is the bidirectional channel. The 2 translators are the adapters, and the 2 presidents are the 2 environments.
Link to originalgraph TD subgraph Environment1[Environment 1] ChinesePresident[Chinese President] <--> ChineseTranslator[Chinese Translator] end subgraph ChannelLayer[Bidirectional Channel] English[English Language] end subgraph Environment2[Environment 2] FrenchTranslator[French Translator] <--> FrenchPresident[French President] end ChineseTranslator <--> English English <--> FrenchTranslator style ChinesePresident fill:#f9d5e5,stroke:#333 style FrenchPresident fill:#f9d5e5,stroke:#333 style ChineseTranslator fill:#d5e8f9,stroke:#333 style FrenchTranslator fill:#d5e8f9,stroke:#333 style English fill:#d5f9e8,stroke:#333 style ChannelLayer fill:#f5f5f5,stroke-dasharray: 5 5