kkrpc 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-test.log +57 -0
- package/README.md +88 -0
- package/__deno_tests__/deno-web-worker.test.ts +31 -0
- package/__tests__/bun.worker.test.ts +24 -0
- package/__tests__/http.test.ts +100 -0
- package/__tests__/scripts/api.ts +38 -0
- package/__tests__/scripts/deno-api.ts +5 -0
- package/__tests__/scripts/node-api.ts +5 -0
- package/__tests__/scripts/worker.ts +10 -0
- package/__tests__/serialization.test.ts +31 -0
- package/__tests__/stdio-rpc.test.ts +90 -0
- package/__tests__/websocket.test.ts +109 -0
- package/browser-mod.ts +7 -0
- package/deno.json +20 -0
- package/mod.ts +9 -0
- package/package.json +25 -0
- package/scripts/prepare.ts +6 -0
- package/scripts/test.ts +11 -0
- package/src/adapters/deno.ts +38 -0
- package/src/adapters/http.ts +115 -0
- package/src/adapters/iframe.ts +178 -0
- package/src/adapters/node.ts +69 -0
- package/src/adapters/websocket.ts +127 -0
- package/src/adapters/worker.ts +116 -0
- package/src/channel.ts +255 -0
- package/src/interface.ts +22 -0
- package/src/serialization.ts +51 -0
- package/src/utils.ts +6 -0
- package/tsconfig.json +28 -0
- package/typedoc.json +12 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
> kkrpc@ test /Users/hacker/Dev/projects/kkrpc/packages/kkrpc
|
|
4
|
+
> bun run scripts/test.ts
|
|
5
|
+
|
|
6
|
+
[0m[32mCheck[0m file:///Users/hacker/Dev/projects/kkrpc/packages/kkrpc/__deno_tests__/deno-web-worker.test.ts
|
|
7
|
+
[0m[38;5;245mrunning 1 test from ./__deno_tests__/deno-web-worker.test.ts[0m
|
|
8
|
+
Call Worker Exposed API ...[0m[32mCheck[0m file:///Users/hacker/Dev/projects/kkrpc/packages/kkrpc/__tests__/scripts/worker.ts
|
|
9
|
+
[0m[32mok[0m [0m[38;5;245m(538ms)[0m
|
|
10
|
+
|
|
11
|
+
[0m[32mok[0m | 1 passed | 0 failed [0m[38;5;245m(541ms)[0m
|
|
12
|
+
|
|
13
|
+
bun test v1.1.34 (5e5e7c60)
|
|
14
|
+
|
|
15
|
+
__tests__/http.test.ts:
|
|
16
|
+
(pass) HTTP RPC > echo service [4.72ms]
|
|
17
|
+
(pass) HTTP RPC > math operations [0.81ms]
|
|
18
|
+
(pass) HTTP RPC > concurrent calls [0.74ms]
|
|
19
|
+
(pass) HTTP RPC > stress test with concurrent calls [220.99ms]
|
|
20
|
+
(pass) HTTP RPC > error handling - invalid endpoint [0.85ms]
|
|
21
|
+
(pass) HTTP RPC > error handling - wrong method [0.30ms]
|
|
22
|
+
|
|
23
|
+
__tests__/websocket.test.ts:
|
|
24
|
+
(pass) WebSocket RPC [32.65ms]
|
|
25
|
+
(pass) WebSocket concurrent connections [2.86ms]
|
|
26
|
+
|
|
27
|
+
__tests__/bun.worker.test.ts:
|
|
28
|
+
(pass) Bun Worker [33.52ms]
|
|
29
|
+
|
|
30
|
+
__tests__/stdio-rpc.test.ts:
|
|
31
|
+
(pass) RPCChannel Test > DenoStdio [483.69ms]
|
|
32
|
+
(pass) RPCChannel Test > NodeStdio [374.62ms]
|
|
33
|
+
|
|
34
|
+
__tests__/serialization.test.ts:
|
|
35
|
+
(pass) Serializer > should serialize and deserialize a message [0.16ms]
|
|
36
|
+
(pass) Serializer > should serialize and deserialize a response [1.69ms]
|
|
37
|
+
---------------------------|---------|---------|-------------------
|
|
38
|
+
File | % Funcs | % Lines | Uncovered Line #s
|
|
39
|
+
---------------------------|---------|---------|-------------------
|
|
40
|
+
All files | 78.63 | 70.89 |
|
|
41
|
+
__tests__/scripts/api.ts | 80.00 | 94.74 |
|
|
42
|
+
mod.ts | 100.00 | 100.00 |
|
|
43
|
+
src/adapters/deno.ts | 0.00 | 20.00 | 13-28,32-35
|
|
44
|
+
src/adapters/http.ts | 100.00 | 100.00 |
|
|
45
|
+
src/adapters/node.ts | 84.62 | 91.43 | 25,42-43
|
|
46
|
+
src/adapters/websocket.ts | 77.27 | 1.61 | 3-124
|
|
47
|
+
src/adapters/worker.ts | 50.00 | 1.77 | 3-113
|
|
48
|
+
src/channel.ts | 73.08 | 88.39 | 42,159,178-184,213-219,251-252
|
|
49
|
+
src/interface.ts | 100.00 | 100.00 |
|
|
50
|
+
src/serialization.ts | 100.00 | 81.82 | 28-29,46-47
|
|
51
|
+
src/utils.ts | 100.00 | 100.00 |
|
|
52
|
+
---------------------------|---------|---------|-------------------
|
|
53
|
+
|
|
54
|
+
13 pass
|
|
55
|
+
0 fail
|
|
56
|
+
14640 expect() calls
|
|
57
|
+
Ran 13 tests across 5 files. [1205.00ms]
|
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# kkrpc
|
|
2
|
+
|
|
3
|
+
> A TypeScript-first RPC library that enables seamless bi-directional communication between processes.
|
|
4
|
+
> Call remote functions as if they were local, with full TypeScript type safety and autocompletion support.
|
|
5
|
+
|
|
6
|
+
## Supported Environments
|
|
7
|
+
|
|
8
|
+
- stdio: RPC over stdio between any combinations of Node.js, Deno, Bun processes
|
|
9
|
+
- web: RPC over `postMessage` API and message channel between browser main thread and web workers, or main thread and iframe
|
|
10
|
+
- Web Worker API (web standard) is also supported in Deno and Bun, the main thread can call functions in worker and vice versa.
|
|
11
|
+
- http: RPC over HTTP like tRPC
|
|
12
|
+
- supports any HTTP server (e.g. hono, bun, nodejs http, express, fastify, deno, etc.)
|
|
13
|
+
- WebSocket: RPC over WebSocket
|
|
14
|
+
|
|
15
|
+
The core of **kkrpc** design is in `RPCChannel` and `IoInterface`.
|
|
16
|
+
|
|
17
|
+
- `RPCChannel` is the bidirectional RPC channel
|
|
18
|
+
- `LocalAPI` is the APIs to be exposed to the other side of the channel
|
|
19
|
+
- `RemoteAPI` is the APIs exposed by the other side of the channel, and callable on the local side
|
|
20
|
+
- `rpc.getAPI()` returns an object that is `RemoteAPI` typed, and is callable on the local side like a normal local function call.
|
|
21
|
+
- `IoInterface` is the interface for implementing the IO for different environments. The implementations are called adapters.
|
|
22
|
+
- For example, for a Node process to communicate with a Deno process, we need `NodeIo` and `DenoIo` adapters which implements `IoInterface`. They share the same stdio pipe (`stdin/stdout`).
|
|
23
|
+
- In web, we have `WorkerChildIO` and `WorkerParentIO` adapters for web worker, `IframeParentIO` and `IframeChildIO` adapters for iframe.
|
|
24
|
+
|
|
25
|
+
> In browser, import from `kkrpc/browser` instead of `kkrpc`, Deno adapter uses node:buffer which doesn't work in browser.
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
interface IoInterface {
|
|
29
|
+
name: string
|
|
30
|
+
read(): Promise<Buffer | Uint8Array | string | null> // Reads input
|
|
31
|
+
write(data: string): Promise<void> // Writes output
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class RPCChannel<
|
|
35
|
+
LocalAPI extends Record<string, any>,
|
|
36
|
+
RemoteAPI extends Record<string, any>,
|
|
37
|
+
Io extends IoInterface = IoInterface
|
|
38
|
+
> {}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Examplesr
|
|
42
|
+
|
|
43
|
+
Below are simple examples.
|
|
44
|
+
|
|
45
|
+
### Stdio Example
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { NodeIo, RPCChannel } from "kkrpc"
|
|
49
|
+
import { apiMethods } from "./api.ts"
|
|
50
|
+
|
|
51
|
+
const stdio = new NodeIo(process.stdin, process.stdout)
|
|
52
|
+
const child = new RPCChannel(stdio, apiMethods)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { spawn } from "child_process"
|
|
57
|
+
|
|
58
|
+
const workerBun = spawn("bun", ["scripts/node-api.ts"])
|
|
59
|
+
const io = new NodeIo(worker.stdout, worker.stdin)
|
|
60
|
+
const parent = new RPCChannel<{}, API>(io, {})
|
|
61
|
+
const api = parent.getAPI()
|
|
62
|
+
|
|
63
|
+
expect(await api.add(1, 2)).toBe(3)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Web Worker Example
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { RPCChannel, WorkerChildIO, type DestroyableIoInterface } from "kkrpc"
|
|
70
|
+
|
|
71
|
+
const worker = new Worker(new URL("./scripts/worker.ts", import.meta.url).href, { type: "module" })
|
|
72
|
+
const io = new WorkerChildIO(worker)
|
|
73
|
+
const rpc = new RPCChannel<API, API, DestroyableIoInterface>(io, apiMethods)
|
|
74
|
+
const api = rpc.getAPI()
|
|
75
|
+
|
|
76
|
+
expect(await api.add(1, 2)).toBe(3)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { RPCChannel, WorkerParentIO, type DestroyableIoInterface } from "kkrpc"
|
|
81
|
+
|
|
82
|
+
const io: DestroyableIoInterface = new WorkerChildIO()
|
|
83
|
+
const rpc = new RPCChannel<API, API, DestroyableIoInterface>(io, apiMethods)
|
|
84
|
+
const api = rpc.getAPI()
|
|
85
|
+
|
|
86
|
+
const sum = await api.add(1, 2)
|
|
87
|
+
expect(sum).toBe(3)
|
|
88
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* If you see error in this file, it's likely Deno extension is not enabled.
|
|
3
|
+
*/
|
|
4
|
+
import { assertEquals } from "jsr:@std/assert"
|
|
5
|
+
import { apiMethods, type API } from "../__tests__/scripts/api.ts"
|
|
6
|
+
import { WorkerParentIO } from "../src/adapters/worker.ts"
|
|
7
|
+
import { RPCChannel } from "../src/channel.ts"
|
|
8
|
+
import type { DestroyableIoInterface } from "../src/interface.ts"
|
|
9
|
+
|
|
10
|
+
const worker = new Worker(new URL("../__tests__/scripts/worker.ts", import.meta.url).href, {
|
|
11
|
+
type: "module"
|
|
12
|
+
})
|
|
13
|
+
const io = new WorkerParentIO(worker)
|
|
14
|
+
const rpc = new RPCChannel<API, API, DestroyableIoInterface>(io, apiMethods)
|
|
15
|
+
const api = rpc.getAPI()
|
|
16
|
+
|
|
17
|
+
Deno.test("Call Worker Exposed API", async () => {
|
|
18
|
+
for (let i = 0; i < 100; i++) {
|
|
19
|
+
const randInt1 = Math.floor(Math.random() * 100)
|
|
20
|
+
const randInt2 = Math.floor(Math.random() * 100)
|
|
21
|
+
const product = await api.math.grade2.multiply(randInt1, randInt2)
|
|
22
|
+
assertEquals(product, randInt1 * randInt2)
|
|
23
|
+
|
|
24
|
+
const sum = await api.math.grade1.add(randInt1, randInt2)
|
|
25
|
+
assertEquals(sum, randInt1 + randInt2)
|
|
26
|
+
api.math.grade1.add(randInt1, randInt2, (sum) => {
|
|
27
|
+
assertEquals(sum, randInt1 + randInt2)
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
io.destroy()
|
|
31
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { NodeIo, RPCChannel, WorkerParentIO, type DestroyableIoInterface } from "../mod.ts"
|
|
3
|
+
import { apiMethods, type API } from "./scripts/api.ts"
|
|
4
|
+
|
|
5
|
+
const worker = new Worker(new URL("./scripts/worker.ts", import.meta.url).href, { type: "module" })
|
|
6
|
+
const io = new WorkerParentIO(worker)
|
|
7
|
+
const rpc = new RPCChannel<API, API, DestroyableIoInterface>(io, apiMethods)
|
|
8
|
+
const api = rpc.getAPI()
|
|
9
|
+
|
|
10
|
+
test("Bun Worker", async () => {
|
|
11
|
+
for (let i = 0; i < 100; i++) {
|
|
12
|
+
const randInt1 = Math.floor(Math.random() * 100)
|
|
13
|
+
const randInt2 = Math.floor(Math.random() * 100)
|
|
14
|
+
const product = await api.math.grade2.multiply(randInt1, randInt2)
|
|
15
|
+
expect(product).toBe(randInt1 * randInt2)
|
|
16
|
+
|
|
17
|
+
const sum = await api.math.grade1.add(randInt1, randInt2)
|
|
18
|
+
expect(sum).toBe(randInt1 + randInt2)
|
|
19
|
+
api.math.grade1.add(randInt1, randInt2, (sum) => {
|
|
20
|
+
expect(sum).toBe(randInt1 + randInt2)
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
io.destroy()
|
|
24
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { type Server } from "bun"
|
|
2
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test"
|
|
3
|
+
import { RPCChannel } from "../mod.ts"
|
|
4
|
+
import { HTTPClientIO, HTTPServerIO } from "../src/adapters/http.ts"
|
|
5
|
+
import { apiMethods, type API } from "./scripts/api.ts"
|
|
6
|
+
|
|
7
|
+
describe("HTTP RPC", () => {
|
|
8
|
+
let server: Server
|
|
9
|
+
let serverIO: HTTPServerIO
|
|
10
|
+
let serverRPC: RPCChannel<API, API>
|
|
11
|
+
let clientIO: HTTPClientIO
|
|
12
|
+
let api: API
|
|
13
|
+
|
|
14
|
+
// Setup before tests
|
|
15
|
+
beforeAll(() => {
|
|
16
|
+
// Create server
|
|
17
|
+
serverIO = new HTTPServerIO()
|
|
18
|
+
serverRPC = new RPCChannel<API, API>(serverIO, apiMethods)
|
|
19
|
+
|
|
20
|
+
server = Bun.serve({
|
|
21
|
+
port: 3000,
|
|
22
|
+
async fetch(req) {
|
|
23
|
+
const url = new URL(req.url)
|
|
24
|
+
if (url.pathname === "/rpc") {
|
|
25
|
+
if (req.method !== "POST") {
|
|
26
|
+
return new Response("Method not allowed", { status: 405 })
|
|
27
|
+
}
|
|
28
|
+
const res = await serverIO.handleRequest(await req.text())
|
|
29
|
+
return new Response(res, { headers: { "Content-Type": "application/json" } })
|
|
30
|
+
}
|
|
31
|
+
return new Response("Not found", { status: 404 })
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// Create client
|
|
36
|
+
clientIO = new HTTPClientIO({
|
|
37
|
+
url: "http://localhost:3000/rpc"
|
|
38
|
+
})
|
|
39
|
+
const clientRPC = new RPCChannel<API, API>(clientIO, apiMethods)
|
|
40
|
+
api = clientRPC.getAPI()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// Cleanup after tests
|
|
44
|
+
afterAll(() => {
|
|
45
|
+
server.stop()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test("echo service", async () => {
|
|
49
|
+
const message = "Hello RPC!"
|
|
50
|
+
const result = await api.echo(message)
|
|
51
|
+
expect(result).toBe(message)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test("math operations", async () => {
|
|
55
|
+
const sum = await api.math.grade1.add(5, 3)
|
|
56
|
+
expect(sum).toBe(8)
|
|
57
|
+
|
|
58
|
+
const product = await api.math.grade2.multiply(4, 6)
|
|
59
|
+
expect(product).toBe(24)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test("concurrent calls", async () => {
|
|
63
|
+
const results = await Promise.all([
|
|
64
|
+
api.math.grade1.add(10, 20),
|
|
65
|
+
api.math.grade2.multiply(10, 20)
|
|
66
|
+
])
|
|
67
|
+
expect(results).toEqual([30, 200])
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test("stress test with concurrent calls", async () => {
|
|
71
|
+
// Run stress test 100 times
|
|
72
|
+
for (let iteration = 0; iteration < 100; iteration++) {
|
|
73
|
+
// Create 50 pairs of random numbers
|
|
74
|
+
const pairs = Array(50)
|
|
75
|
+
.fill(0)
|
|
76
|
+
.map(() => [Math.random(), Math.random()])
|
|
77
|
+
const expectedSums = pairs.map(([a, b]) => a + b)
|
|
78
|
+
|
|
79
|
+
// Make concurrent API calls
|
|
80
|
+
const actualSums = await Promise.all(pairs.map(([a, b]) => api.math.grade1.add(a, b)))
|
|
81
|
+
|
|
82
|
+
// Compare results
|
|
83
|
+
expect(actualSums).toEqual(expectedSums)
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test("error handling - invalid endpoint", async () => {
|
|
88
|
+
const response = await fetch("http://localhost:3000/invalid")
|
|
89
|
+
expect(response.status).toBe(404)
|
|
90
|
+
expect(await response.text()).toBe("Not found")
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test("error handling - wrong method", async () => {
|
|
94
|
+
const response = await fetch("http://localhost:3000/rpc", {
|
|
95
|
+
method: "GET"
|
|
96
|
+
})
|
|
97
|
+
expect(response.status).toBe(405)
|
|
98
|
+
expect(await response.text()).toBe("Method not allowed")
|
|
99
|
+
})
|
|
100
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface API {
|
|
2
|
+
echo(message: string): Promise<string>
|
|
3
|
+
add(a: number, b: number): Promise<number>
|
|
4
|
+
subtract(a: number, b: number): Promise<number>
|
|
5
|
+
addCallback(a: number, b: number, callback: (result: number) => void): void
|
|
6
|
+
math: {
|
|
7
|
+
grade1: {
|
|
8
|
+
add(a: number, b: number, callback?: (result: number) => void): Promise<number>
|
|
9
|
+
}
|
|
10
|
+
grade2: {
|
|
11
|
+
multiply(a: number, b: number, callback?: (result: number) => void): Promise<number>
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Define your API methods
|
|
17
|
+
export const apiMethods: API = {
|
|
18
|
+
echo: async (message: string) => message,
|
|
19
|
+
add: async (a: number, b: number) => a + b,
|
|
20
|
+
subtract: async (a: number, b: number) => a - b,
|
|
21
|
+
addCallback: async (a: number, b: number, callback?: (result: number) => void) => {
|
|
22
|
+
callback?.(a + b)
|
|
23
|
+
},
|
|
24
|
+
math: {
|
|
25
|
+
grade1: {
|
|
26
|
+
add: async (a: number, b: number, callback?: (result: number) => void) => {
|
|
27
|
+
callback?.(a + b)
|
|
28
|
+
return a + b
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
grade2: {
|
|
32
|
+
multiply: async (a: number, b: number, callback?: (result: number) => void) => {
|
|
33
|
+
callback?.(a * b)
|
|
34
|
+
return a * b
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { RPCChannel, WorkerChildIO, type DestroyableIoInterface } from "../../mod.ts"
|
|
2
|
+
import { apiMethods, type API } from "./api.ts"
|
|
3
|
+
|
|
4
|
+
const io: DestroyableIoInterface = new WorkerChildIO()
|
|
5
|
+
const rpc = new RPCChannel<API, API, DestroyableIoInterface>(io, apiMethods)
|
|
6
|
+
const api = rpc.getAPI()
|
|
7
|
+
|
|
8
|
+
// const randInt1 = Math.floor(Math.random() * 100)
|
|
9
|
+
// const randInt2 = Math.floor(Math.random() * 100)
|
|
10
|
+
// api.add(randInt1, randInt2)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import {
|
|
3
|
+
deserializeMessage,
|
|
4
|
+
deserializeResponse,
|
|
5
|
+
serializeMessage,
|
|
6
|
+
serializeResponse,
|
|
7
|
+
type Message
|
|
8
|
+
} from "../src/serialization.ts"
|
|
9
|
+
|
|
10
|
+
describe("Serializer", () => {
|
|
11
|
+
test("should serialize and deserialize a message", async () => {
|
|
12
|
+
const message: Message = {
|
|
13
|
+
id: "1",
|
|
14
|
+
method: "testMethod",
|
|
15
|
+
args: [1, 2, 3],
|
|
16
|
+
type: "request"
|
|
17
|
+
}
|
|
18
|
+
const serialized = serializeMessage(message)
|
|
19
|
+
const deserialized = await deserializeMessage(serialized)
|
|
20
|
+
expect(deserialized).toEqual(message as any)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test("should serialize and deserialize a response", async () => {
|
|
24
|
+
const response = {
|
|
25
|
+
id: 1
|
|
26
|
+
}
|
|
27
|
+
const serializedResponse = serializeResponse(response as any)
|
|
28
|
+
const deserializedResponse = await deserializeResponse(serializedResponse)
|
|
29
|
+
expect(deserializedResponse).toEqual(response as any)
|
|
30
|
+
})
|
|
31
|
+
})
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { spawn } from "child_process"
|
|
2
|
+
import type { ChildProcessWithoutNullStreams } from "node:child_process"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import { sleep } from "bun"
|
|
5
|
+
import { describe, expect, test } from "bun:test"
|
|
6
|
+
import { DenoIo, NodeIo } from "../mod.ts"
|
|
7
|
+
import { RPCChannel } from "../src/channel.ts"
|
|
8
|
+
import { apiMethods, type API } from "./scripts/api.ts"
|
|
9
|
+
|
|
10
|
+
function getProjectRoot(): string {
|
|
11
|
+
const fileUrl = new URL(import.meta.url).pathname
|
|
12
|
+
const folderPath = path.dirname(path.dirname(fileUrl))
|
|
13
|
+
return folderPath
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const projectRoot = getProjectRoot()
|
|
17
|
+
const testsPath = path.join(projectRoot, "__tests__")
|
|
18
|
+
|
|
19
|
+
async function runWorker(worker: ChildProcessWithoutNullStreams) {
|
|
20
|
+
// worker.stderr.pipe(process.stdout);
|
|
21
|
+
|
|
22
|
+
// const stdio = createStdio();
|
|
23
|
+
const io = new NodeIo(worker.stdout, worker.stdin)
|
|
24
|
+
const rpc = new RPCChannel<{}, API>(io, {})
|
|
25
|
+
const api = rpc.getAPI()
|
|
26
|
+
|
|
27
|
+
expect(await api.add(1, 2)).toEqual(3)
|
|
28
|
+
const sum2 = await new Promise((resolve, reject) => {
|
|
29
|
+
api.addCallback(1, 2, (sum) => {
|
|
30
|
+
resolve(sum)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
expect(sum2).toEqual(3)
|
|
35
|
+
expect(await api.subtract(1, 2)).toEqual(-1)
|
|
36
|
+
|
|
37
|
+
// stress test
|
|
38
|
+
for (let i = 0; i < 1000; i++) {
|
|
39
|
+
expect(await api.add(i, i)).toEqual(i + i)
|
|
40
|
+
expect(await api.subtract(i, i)).toEqual(0)
|
|
41
|
+
}
|
|
42
|
+
// stress test with concurrent calls
|
|
43
|
+
await Promise.all(
|
|
44
|
+
Array(5_000)
|
|
45
|
+
.fill(0)
|
|
46
|
+
.map(async (x, idx) => expect(await api.add(idx, idx)).toEqual(idx + idx))
|
|
47
|
+
)
|
|
48
|
+
await Promise.all(
|
|
49
|
+
Array(5_000)
|
|
50
|
+
.fill(0)
|
|
51
|
+
.map(() =>
|
|
52
|
+
api.addCallback(1, 2, (sum) => {
|
|
53
|
+
// expect(sum).toEqual(3);
|
|
54
|
+
})
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
const dummyCallback = (sum: number) => {}
|
|
58
|
+
await Promise.all(
|
|
59
|
+
Array(5_000)
|
|
60
|
+
.fill(0)
|
|
61
|
+
.map(() => api.addCallback(1, 2, dummyCallback))
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
/* -------------------------------------------------------------------------- */
|
|
65
|
+
/* Nested Object API */
|
|
66
|
+
/* -------------------------------------------------------------------------- */
|
|
67
|
+
expect(await api.math.grade1.add(1, 2)).toEqual(3)
|
|
68
|
+
expect(await api.math.grade2.multiply(2, 3)).toEqual(6)
|
|
69
|
+
|
|
70
|
+
/* --------------------- Nested Object API with Callback -------------------- */
|
|
71
|
+
expect(await api.math.grade1.add(1, 2, (result) => expect(result).toEqual(3))).toEqual(3)
|
|
72
|
+
expect(await api.math.grade2.multiply(2, 3, (result) => expect(result).toEqual(6))).toEqual(6)
|
|
73
|
+
|
|
74
|
+
worker.kill()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe("RPCChannel Test", () => {
|
|
78
|
+
test("DenoStdio", async () => {
|
|
79
|
+
const workerDeno = spawn("deno", [path.join(testsPath, "scripts/deno-api.ts")])
|
|
80
|
+
await runWorker(workerDeno)
|
|
81
|
+
})
|
|
82
|
+
test("NodeStdio", async () => {
|
|
83
|
+
const workerBun = spawn("node", [path.join(testsPath, "scripts/node-api.js")])
|
|
84
|
+
await runWorker(workerBun)
|
|
85
|
+
})
|
|
86
|
+
// test("NodeStdio with bun", async () => {
|
|
87
|
+
// const workerBun = spawn("bun", [path.join(testsPath, "scripts/node-api.ts")])
|
|
88
|
+
// await runWorker(workerBun)
|
|
89
|
+
// })
|
|
90
|
+
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { afterAll, beforeAll, expect, test } from "bun:test"
|
|
2
|
+
import { WebSocketServer } from "ws"
|
|
3
|
+
import { RPCChannel } from "../mod.ts"
|
|
4
|
+
import { WebSocketClientIO, WebSocketServerIO } from "../src/adapters/websocket.ts"
|
|
5
|
+
import type { DestroyableIoInterface } from "../src/interface.ts"
|
|
6
|
+
import { apiMethods, type API } from "./scripts/api.ts"
|
|
7
|
+
|
|
8
|
+
const PORT = 3001
|
|
9
|
+
let wss: WebSocketServer
|
|
10
|
+
let serverRPC: RPCChannel<API, API>
|
|
11
|
+
|
|
12
|
+
beforeAll(() => {
|
|
13
|
+
// Create WebSocket server
|
|
14
|
+
wss = new WebSocketServer({ port: PORT })
|
|
15
|
+
|
|
16
|
+
// Handle WebSocket connections
|
|
17
|
+
wss.on("connection", (ws: WebSocket) => {
|
|
18
|
+
const serverIO = new WebSocketServerIO(ws)
|
|
19
|
+
serverRPC = new RPCChannel<API, API>(serverIO, apiMethods)
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
afterAll(() => {
|
|
24
|
+
wss.close()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test("WebSocket RPC", async () => {
|
|
28
|
+
const clientIO = new WebSocketClientIO({
|
|
29
|
+
url: `ws://localhost:${PORT}`
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const clientRPC = new RPCChannel<API, API, DestroyableIoInterface>(clientIO, apiMethods)
|
|
33
|
+
const api = clientRPC.getAPI()
|
|
34
|
+
|
|
35
|
+
// Test individual calls
|
|
36
|
+
const sum = await api.add(5, 3)
|
|
37
|
+
expect(sum).toBe(8)
|
|
38
|
+
|
|
39
|
+
const product = await api.math.grade2.multiply(4, 6)
|
|
40
|
+
expect(product).toBe(24)
|
|
41
|
+
|
|
42
|
+
// Test concurrent calls
|
|
43
|
+
const results = await Promise.all([
|
|
44
|
+
api.add(10, 20),
|
|
45
|
+
api.math.grade2.multiply(10, 20),
|
|
46
|
+
api.add(30, 40),
|
|
47
|
+
api.math.grade2.multiply(30, 40)
|
|
48
|
+
])
|
|
49
|
+
|
|
50
|
+
expect(results).toEqual([30, 200, 70, 1200])
|
|
51
|
+
|
|
52
|
+
// Test multiple random calls
|
|
53
|
+
for (let i = 0; i < 100; i++) {
|
|
54
|
+
const a = Math.floor(Math.random() * 100)
|
|
55
|
+
const b = Math.floor(Math.random() * 100)
|
|
56
|
+
|
|
57
|
+
const sum = await api.add(a, b)
|
|
58
|
+
expect(sum).toBe(a + b)
|
|
59
|
+
|
|
60
|
+
const product = await api.math.grade2.multiply(a, b)
|
|
61
|
+
expect(product).toBe(a * b)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
clientIO.destroy()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// test("WebSocket error handling", async () => {
|
|
68
|
+
// const invalidPort = 54321
|
|
69
|
+
// const clientIO = new WebSocketClientIO({
|
|
70
|
+
// url: `ws://localhost:${invalidPort}`
|
|
71
|
+
// })
|
|
72
|
+
|
|
73
|
+
// const clientRPC = new RPCChannel<{}, API, DestroyableIoInterface>(clientIO, {})
|
|
74
|
+
// const api = clientRPC.getAPI()
|
|
75
|
+
// expect(() => api.add(1, 2)).toThrow()
|
|
76
|
+
// clientIO.destroy()
|
|
77
|
+
// })
|
|
78
|
+
|
|
79
|
+
test("WebSocket concurrent connections", async () => {
|
|
80
|
+
const numClients = 5
|
|
81
|
+
const clients = Array.from({ length: numClients }, () => {
|
|
82
|
+
const clientIO = new WebSocketClientIO({
|
|
83
|
+
url: `ws://localhost:${PORT}`
|
|
84
|
+
})
|
|
85
|
+
return {
|
|
86
|
+
io: clientIO,
|
|
87
|
+
rpc: new RPCChannel<{}, API, DestroyableIoInterface>(clientIO, {})
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
// Test concurrent calls from multiple clients
|
|
93
|
+
const results = await Promise.all(
|
|
94
|
+
clients.flatMap(({ rpc }) => {
|
|
95
|
+
const api = rpc.getAPI()
|
|
96
|
+
return [api.add(10, 20), api.math.grade2.multiply(10, 20)]
|
|
97
|
+
})
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
// Verify results
|
|
101
|
+
for (let i = 0; i < results.length; i += 2) {
|
|
102
|
+
expect(results[i]).toBe(30) // add result
|
|
103
|
+
expect(results[i + 1]).toBe(200) // multiply result
|
|
104
|
+
}
|
|
105
|
+
} finally {
|
|
106
|
+
// Cleanup
|
|
107
|
+
clients.forEach(({ io }) => io.destroy())
|
|
108
|
+
}
|
|
109
|
+
})
|
package/browser-mod.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from "./src/adapters/worker.ts"
|
|
2
|
+
export * from "./src/adapters/node.ts"
|
|
3
|
+
export * from "./src/interface.ts"
|
|
4
|
+
export * from "./src/channel.ts"
|
|
5
|
+
export * from "./src/utils.ts"
|
|
6
|
+
export * from "./src/serialization.ts"
|
|
7
|
+
export * from "./src/adapters/iframe.ts"
|
package/deno.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kunkun/kkrpc",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"tasks": {
|
|
5
|
+
"dev": "deno run --watch main.ts"
|
|
6
|
+
},
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./mod.ts",
|
|
9
|
+
"./browser": "./browser-mod.ts"
|
|
10
|
+
},
|
|
11
|
+
"imports": {
|
|
12
|
+
"@std/assert": "jsr:@std/assert@1"
|
|
13
|
+
},
|
|
14
|
+
"lint": {
|
|
15
|
+
"rules": {
|
|
16
|
+
"exclude": ["no-explicit-any"]
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT"
|
|
20
|
+
}
|
package/mod.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./src/adapters/worker.ts"
|
|
2
|
+
export * from "./src/adapters/node.ts"
|
|
3
|
+
export * from "./src/adapters/websocket.ts"
|
|
4
|
+
export * from "./src/adapters/http.ts"
|
|
5
|
+
export * from "./src/interface.ts"
|
|
6
|
+
export * from "./src/channel.ts"
|
|
7
|
+
export * from "./src/utils.ts"
|
|
8
|
+
export * from "./src/serialization.ts"
|
|
9
|
+
export { DenoIo } from "./src/adapters/deno.ts"
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kkrpc",
|
|
3
|
+
"module": "index.ts",
|
|
4
|
+
"version": "0.0.2",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./mod.ts",
|
|
8
|
+
"./browser": "./browser-mod.ts"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@types/bun": "latest",
|
|
12
|
+
"@types/ws": "^8.5.10",
|
|
13
|
+
"typedoc": "^0.26.11"
|
|
14
|
+
},
|
|
15
|
+
"peerDependencies": {
|
|
16
|
+
"typescript": "^5.0.0"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"ws": "^8.16.0"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "bun run scripts/test.ts",
|
|
23
|
+
"docs": "typedoc --out docs"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { $ } from "bun"
|
|
2
|
+
|
|
3
|
+
// const denoTypes = await $`deno types`.text()
|
|
4
|
+
// // filter out the line with no-default-lib
|
|
5
|
+
// const denoTypesFiltered = denoTypes.split("\n").filter((line) => !line.includes("no-default-lib"))
|
|
6
|
+
// await Bun.write("./deno.d.ts", denoTypesFiltered.join("\n"))
|