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.
@@ -0,0 +1,11 @@
1
+ import { $ } from "bun"
2
+
3
+ await $`deno test -R __deno_tests__`
4
+ const buildOutput = await Bun.build({
5
+ entrypoints: ["__tests__/scripts/node-api.js"],
6
+ outdir: "__tests__/scripts",
7
+ target: "node",
8
+ format: "esm"
9
+ })
10
+
11
+ await $`bun test __tests__ --coverage`
@@ -0,0 +1,38 @@
1
+ import { Buffer } from "node:buffer"
2
+ import type { IoInterface } from "../interface.ts"
3
+
4
+ /**
5
+ * Stdio implementation for Deno
6
+ * Deno doesn't have `process` object, and have a completely different stdio API,
7
+ * This implementation wrap Deno's `Deno.stdin` and `Deno.stdout` to follow StdioInterface
8
+ */
9
+ export class DenoIo implements IoInterface {
10
+ private reader: ReadableStreamDefaultReader<Uint8Array>
11
+ name = "deno-io"
12
+
13
+ constructor(
14
+ private readStream: ReadableStream<Uint8Array>,
15
+ private writeStream: WritableStream<Uint8Array>
16
+ ) {
17
+ this.reader = this.readStream.getReader()
18
+ // const writer = this.writeStream.getWriter()
19
+ // const encoder = new TextEncoder()
20
+
21
+ // writer.write(encoder.encode("hello"))
22
+ }
23
+
24
+ async read(): Promise<Buffer | null> {
25
+ const { value, done } = await this.reader.read()
26
+ if (done) {
27
+ return null // End of input
28
+ }
29
+ return Buffer.from(value)
30
+ }
31
+
32
+ write(data: string): Promise<void> {
33
+ const encoder = new TextEncoder()
34
+ const encodedData = encoder.encode(data + "\n")
35
+ Deno.stdout.writeSync(encodedData)
36
+ return Promise.resolve()
37
+ }
38
+ }
@@ -0,0 +1,115 @@
1
+ import type { DestroyableIoInterface, IoInterface } from "../interface.ts"
2
+
3
+ interface HTTPClientOptions {
4
+ url: string
5
+ headers?: Record<string, string>
6
+ }
7
+
8
+ /**
9
+ * HTTP Client implementation of IoInterface
10
+ */
11
+ export class HTTPClientIO implements IoInterface {
12
+ name = "http-client-io"
13
+ private messageQueue: string[] = []
14
+ private resolveRead: ((value: string | null) => void) | null = null
15
+
16
+ constructor(private options: HTTPClientOptions) {}
17
+
18
+ async read(): Promise<string | null> {
19
+ if (this.messageQueue.length > 0) {
20
+ return this.messageQueue.shift() ?? null
21
+ }
22
+
23
+ return new Promise((resolve) => {
24
+ this.resolveRead = resolve
25
+ })
26
+ }
27
+
28
+ async write(data: string): Promise<void> {
29
+ try {
30
+ const response = await fetch(this.options.url, {
31
+ method: "POST",
32
+ headers: {
33
+ "Content-Type": "application/json",
34
+ ...this.options.headers
35
+ },
36
+ body: data
37
+ })
38
+
39
+ if (!response.ok) {
40
+ throw new Error(`HTTP error! status: ${response.status}`)
41
+ }
42
+
43
+ const responseText = await response.text()
44
+
45
+ if (this.resolveRead) {
46
+ this.resolveRead(responseText)
47
+ this.resolveRead = null
48
+ } else {
49
+ this.messageQueue.push(responseText)
50
+ }
51
+ } catch (error) {
52
+ console.error("HTTP request failed:", error)
53
+ if (this.resolveRead) {
54
+ this.resolveRead(null)
55
+ this.resolveRead = null
56
+ }
57
+ }
58
+ }
59
+ }
60
+
61
+ /**
62
+ * HTTP Server implementation of IoInterface
63
+ */
64
+ export class HTTPServerIO implements IoInterface {
65
+ name = "http-server-io"
66
+ private messageQueue: string[] = []
67
+ private resolveRead: ((value: string | null) => void) | null = null
68
+ private pendingResponses = new Map<string, (response: string) => void>()
69
+
70
+ constructor() {}
71
+
72
+ async read(): Promise<string | null> {
73
+ if (this.messageQueue.length > 0) {
74
+ return this.messageQueue.shift() ?? null
75
+ }
76
+
77
+ return new Promise((resolve) => {
78
+ this.resolveRead = resolve
79
+ })
80
+ }
81
+
82
+ async write(data: string): Promise<void> {
83
+ // Parse the response to get the request ID
84
+ const response = JSON.parse(data)
85
+ const requestId = response.id
86
+
87
+ const resolveResponse = this.pendingResponses.get(requestId)
88
+ if (resolveResponse) {
89
+ resolveResponse(data)
90
+ this.pendingResponses.delete(requestId)
91
+ }
92
+ }
93
+
94
+ async handleRequest(reqData: string): Promise<string> {
95
+ try {
96
+ // Parse the request to get its ID
97
+ const requestData = JSON.parse(reqData)
98
+ const requestId = requestData.id
99
+
100
+ if (this.resolveRead) {
101
+ this.resolveRead(reqData)
102
+ this.resolveRead = null
103
+ } else {
104
+ this.messageQueue.push(reqData)
105
+ }
106
+
107
+ return new Promise((resolve) => {
108
+ this.pendingResponses.set(requestId, resolve)
109
+ })
110
+ } catch (error) {
111
+ console.error("RPC processing error:", error)
112
+ throw new Error("Internal server error")
113
+ }
114
+ }
115
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * This file contains the implementation of the IframeParentIO and IframeChildIO classes.
3
+ * They are used to create a bidirectional communication channel between a parent window and a child iframe.
4
+ */
5
+ import type { DestroyableIoInterface } from "../interface.ts"
6
+
7
+ const DESTROY_SIGNAL = "__DESTROY__"
8
+ const PORT_INIT_SIGNAL = "__PORT_INIT__"
9
+
10
+ /**
11
+ * This design relies on built-in `MessageChannel`, and requires a pairing process to establish the port.
12
+ * The `PORT_INIT_SIGNAL` is designed to be initiated by the child frame, parent window will wait for the signal and establish the port.
13
+ *
14
+ * If `PORT_INIT_SIGNAL` is started by the parent window, there has to be a delay (with `setTimeout`) to wait for the child frame to listen to the signal.
15
+ * Parent window can easily listen to iframe onload event, but there is no way to know when child JS is ready to listen to the message without
16
+ * letting child `postMessage` a signal first.
17
+ *
18
+ * It's much easier to make sure parent window is ready (listening) before iframe is loaded, so `MessageChannel` is designed to be created from iframe's side.
19
+ *
20
+ * It's a good practice to call `destroy()` on either side of the channel to close `MessageChannel` and release resources.
21
+ */
22
+ export class IframeParentIO implements DestroyableIoInterface {
23
+ name = "iframe-parent-io"
24
+ private messageQueue: string[] = []
25
+ private resolveRead: ((value: string | null) => void) | null = null
26
+ private port: MessagePort | null = null
27
+
28
+ /**
29
+ * @example
30
+ * ```ts
31
+ * const io = new IframeParentIO(iframeRef.contentWindow)
32
+ * const rpc = new RPCChannel(io, {
33
+ * add: (a: number, b: number) => Promise.resolve(a + b)
34
+ * })
35
+ * ```
36
+ */
37
+ constructor(private targetWindow: Window) {
38
+ this.port = null as unknown as MessagePort
39
+ window.addEventListener("message", (event: MessageEvent) => {
40
+ if (event.source !== this.targetWindow) return
41
+ if (event.data === PORT_INIT_SIGNAL && event.ports.length > 0) {
42
+ this.port = event.ports[0]
43
+ this.port.onmessage = this.handleMessage
44
+
45
+ while (this.messageQueue.length > 0) {
46
+ const message = this.messageQueue.shift()
47
+ if (message) this.port.postMessage(message)
48
+ }
49
+ }
50
+ })
51
+ }
52
+
53
+ private handleMessage = (event: MessageEvent) => {
54
+ const message = event.data
55
+
56
+ // Handle destroy signal
57
+ if (message === DESTROY_SIGNAL) {
58
+ this.destroy()
59
+ return
60
+ }
61
+
62
+ if (this.resolveRead) {
63
+ // If there's a pending read, resolve it immediately
64
+ this.resolveRead(message)
65
+ this.resolveRead = null
66
+ } else {
67
+ // Otherwise, queue the message
68
+ this.messageQueue.push(message)
69
+ }
70
+ }
71
+
72
+ async read(): Promise<string | null> {
73
+ // If there are queued messages, return the first one
74
+ if (this.messageQueue.length > 0) {
75
+ return this.messageQueue.shift() ?? null
76
+ }
77
+
78
+ // Otherwise, wait for the next message
79
+ return new Promise((resolve) => {
80
+ this.resolveRead = resolve
81
+ })
82
+ }
83
+
84
+ async write(data: string): Promise<void> {
85
+ if (!this.port) {
86
+ this.messageQueue.push(data)
87
+ return
88
+ }
89
+ this.port.postMessage(data)
90
+ }
91
+
92
+ destroy(): void {
93
+ if (this.port) {
94
+ this.port.postMessage(DESTROY_SIGNAL)
95
+ this.port.close()
96
+ }
97
+ }
98
+
99
+ signalDestroy(): void {
100
+ if (this.port) {
101
+ this.port.postMessage(DESTROY_SIGNAL)
102
+ }
103
+ }
104
+ }
105
+
106
+ // Child frame version
107
+ export class IframeChildIO implements DestroyableIoInterface {
108
+ name = "iframe-child-io"
109
+ private messageQueue: string[] = []
110
+ private resolveRead: ((value: string | null) => void) | null = null
111
+ private port: MessagePort | null = null
112
+ private pendingMessages: string[] = []
113
+ private initialized: Promise<void>
114
+ private channel: MessageChannel
115
+
116
+ constructor() {
117
+ this.channel = new MessageChannel()
118
+ this.port = this.channel.port1
119
+ this.port.onmessage = this.handleMessage
120
+
121
+ window.parent.postMessage(PORT_INIT_SIGNAL, "*", [this.channel.port2])
122
+ this.initialized = Promise.resolve()
123
+ }
124
+
125
+ private handleMessage = (event: MessageEvent) => {
126
+ const message = event.data
127
+
128
+ // Handle destroy signal
129
+ if (message === DESTROY_SIGNAL) {
130
+ this.destroy()
131
+ return
132
+ }
133
+
134
+ if (this.resolveRead) {
135
+ this.resolveRead(message)
136
+ this.resolveRead = null
137
+ } else {
138
+ this.messageQueue.push(message)
139
+ }
140
+ }
141
+
142
+ async read(): Promise<string | null> {
143
+ await this.initialized
144
+
145
+ if (this.messageQueue.length > 0) {
146
+ return this.messageQueue.shift() ?? null
147
+ }
148
+
149
+ return new Promise((resolve) => {
150
+ this.resolveRead = resolve
151
+ })
152
+ }
153
+
154
+ async write(data: string): Promise<void> {
155
+ await this.initialized
156
+
157
+ if (this.port) {
158
+ this.port.postMessage(data)
159
+ } else {
160
+ this.pendingMessages.push(data)
161
+ }
162
+ }
163
+
164
+ destroy(): void {
165
+ if (this.port) {
166
+ this.port.postMessage(DESTROY_SIGNAL)
167
+ this.port.close()
168
+ }
169
+ }
170
+
171
+ signalDestroy(): void {
172
+ if (this.port) {
173
+ this.port.postMessage(DESTROY_SIGNAL)
174
+ } else {
175
+ this.pendingMessages.push(DESTROY_SIGNAL)
176
+ }
177
+ }
178
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Node.js implementation of IoInterface
3
+ * Should also work with Bun
4
+ */
5
+ import { type Buffer } from "node:buffer"
6
+ import { Readable, Writable } from "node:stream"
7
+ import { type IoInterface } from "../interface.ts"
8
+
9
+ /**
10
+ * Stdio implementation for Node.js
11
+ * Simply wrap Node.js's `process.stdin` and `process.stdout` to follow StdioInterface
12
+ */
13
+ export class NodeIo implements IoInterface {
14
+ name = "node-io"
15
+ private readStream: Readable
16
+ private writeStream: Writable
17
+ private dataHandler: ((chunk: Buffer) => void) | null = null
18
+ private errorHandler: ((error: Error) => void) | null = null
19
+
20
+ constructor(readStream: Readable, writeStream: Writable) {
21
+ this.readStream = readStream
22
+ this.writeStream = writeStream
23
+
24
+ // Set up persistent listeners
25
+ this.readStream.on("error", (error) => {
26
+ if (this.errorHandler) this.errorHandler(error)
27
+ })
28
+ }
29
+
30
+ async read(): Promise<Buffer | null> {
31
+ return new Promise((resolve, reject) => {
32
+ const onData = (chunk: Buffer) => {
33
+ cleanup()
34
+ resolve(chunk)
35
+ }
36
+
37
+ const onEnd = () => {
38
+ cleanup()
39
+ resolve(null)
40
+ }
41
+
42
+ const onError = (error: Error) => {
43
+ cleanup()
44
+ reject(error)
45
+ }
46
+
47
+ const cleanup = () => {
48
+ this.readStream.removeListener("data", onData)
49
+ this.readStream.removeListener("end", onEnd)
50
+ this.readStream.removeListener("error", onError)
51
+ }
52
+
53
+ this.readStream.once("data", onData)
54
+ this.readStream.once("end", onEnd)
55
+ this.readStream.once("error", onError)
56
+ })
57
+ }
58
+
59
+ async write(data: string): Promise<void> {
60
+ return new Promise((resolve, reject) => {
61
+ this.writeStream.write(data, (err) => {
62
+ if (err) reject(err)
63
+ else {
64
+ resolve()
65
+ }
66
+ })
67
+ })
68
+ }
69
+ }
@@ -0,0 +1,127 @@
1
+ import type { DestroyableIoInterface } from "../interface.ts"
2
+
3
+ const DESTROY_SIGNAL = "__DESTROY__"
4
+
5
+ interface WebSocketClientOptions {
6
+ url: string
7
+ protocols?: string | string[]
8
+ }
9
+
10
+ /**
11
+ * WebSocket Client implementation of IoInterface
12
+ */
13
+ export class WebSocketClientIO implements DestroyableIoInterface {
14
+ name = "websocket-client-io"
15
+ private messageQueue: string[] = []
16
+ private resolveRead: ((value: string | null) => void) | null = null
17
+ private ws: WebSocket
18
+ private connected: Promise<void>
19
+ private connectResolve: (() => void) | null = null
20
+
21
+ constructor(private options: WebSocketClientOptions) {
22
+ this.ws = new WebSocket(options.url, options.protocols)
23
+ this.connected = new Promise((resolve) => {
24
+ this.connectResolve = resolve
25
+ })
26
+
27
+ this.ws.onopen = () => {
28
+ this.connectResolve?.()
29
+ }
30
+
31
+ this.ws.onmessage = (event) => {
32
+ const message = event.data
33
+ if (message === DESTROY_SIGNAL) {
34
+ this.destroy()
35
+ return
36
+ }
37
+
38
+ if (this.resolveRead) {
39
+ this.resolveRead(message)
40
+ this.resolveRead = null
41
+ } else {
42
+ this.messageQueue.push(message)
43
+ }
44
+ }
45
+
46
+ this.ws.onerror = (error) => {
47
+ console.error("WebSocket error:", error)
48
+ }
49
+ }
50
+
51
+ async read(): Promise<string | null> {
52
+ await this.connected
53
+
54
+ if (this.messageQueue.length > 0) {
55
+ return this.messageQueue.shift() ?? null
56
+ }
57
+
58
+ return new Promise((resolve) => {
59
+ this.resolveRead = resolve
60
+ })
61
+ }
62
+
63
+ async write(data: string): Promise<void> {
64
+ await this.connected
65
+ this.ws.send(data)
66
+ }
67
+
68
+ destroy(): void {
69
+ this.ws.close()
70
+ }
71
+
72
+ signalDestroy(): void {
73
+ this.write(DESTROY_SIGNAL)
74
+ }
75
+ }
76
+
77
+ /**
78
+ * WebSocket Server implementation of IoInterface
79
+ */
80
+ export class WebSocketServerIO implements DestroyableIoInterface {
81
+ name = "websocket-server-io"
82
+ private messageQueue: string[] = []
83
+ private resolveRead: ((value: string | null) => void) | null = null
84
+
85
+ constructor(private ws: WebSocket) {
86
+ this.ws.onmessage = (event) => {
87
+ const message = event.data
88
+ if (message === DESTROY_SIGNAL) {
89
+ this.destroy()
90
+ return
91
+ }
92
+
93
+ if (this.resolveRead) {
94
+ this.resolveRead(message)
95
+ this.resolveRead = null
96
+ } else {
97
+ this.messageQueue.push(message)
98
+ }
99
+ }
100
+
101
+ this.ws.onerror = (error) => {
102
+ console.error("WebSocket error:", error)
103
+ }
104
+ }
105
+
106
+ async read(): Promise<string | null> {
107
+ if (this.messageQueue.length > 0) {
108
+ return this.messageQueue.shift() ?? null
109
+ }
110
+
111
+ return new Promise((resolve) => {
112
+ this.resolveRead = resolve
113
+ })
114
+ }
115
+
116
+ async write(data: string): Promise<void> {
117
+ this.ws.send(data)
118
+ }
119
+
120
+ destroy(): void {
121
+ this.ws.close()
122
+ }
123
+
124
+ signalDestroy(): void {
125
+ this.write(DESTROY_SIGNAL)
126
+ }
127
+ }
@@ -0,0 +1,116 @@
1
+ import type { DestroyableIoInterface } from "../interface.ts"
2
+
3
+ const DESTROY_SIGNAL = "__DESTROY__"
4
+
5
+ export class WorkerParentIO implements DestroyableIoInterface {
6
+ name = "worker-parent-io"
7
+ private messageQueue: string[] = []
8
+ private resolveRead: ((value: string | null) => void) | null = null
9
+ private worker: Worker
10
+
11
+ constructor(worker: Worker) {
12
+ this.worker = worker
13
+ this.worker.onmessage = this.handleMessage
14
+ }
15
+
16
+ private handleMessage = (event: MessageEvent) => {
17
+ const message = event.data
18
+
19
+ // Handle destroy signal
20
+ if (message === DESTROY_SIGNAL) {
21
+ this.destroy()
22
+ return
23
+ }
24
+
25
+ if (this.resolveRead) {
26
+ // If there's a pending read, resolve it immediately
27
+ this.resolveRead(message)
28
+ this.resolveRead = null
29
+ } else {
30
+ // Otherwise, queue the message
31
+ this.messageQueue.push(message)
32
+ }
33
+ }
34
+
35
+ read(): Promise<string | null> {
36
+ // If there are queued messages, return the first one
37
+ if (this.messageQueue.length > 0) {
38
+ return Promise.resolve(this.messageQueue.shift() ?? null)
39
+ }
40
+
41
+ // Otherwise, wait for the next message
42
+ return new Promise((resolve) => {
43
+ this.resolveRead = resolve
44
+ })
45
+ }
46
+
47
+ write(data: string): Promise<void> {
48
+ this.worker.postMessage(data)
49
+ return Promise.resolve()
50
+ }
51
+
52
+ destroy(): void {
53
+ this.worker.postMessage(DESTROY_SIGNAL)
54
+ this.worker.terminate()
55
+ }
56
+
57
+ signalDestroy(): void {
58
+ this.worker.postMessage(DESTROY_SIGNAL)
59
+ }
60
+ }
61
+
62
+ // Worker version
63
+ export class WorkerChildIO implements DestroyableIoInterface {
64
+ name = "worker-child-io"
65
+ private messageQueue: string[] = []
66
+ private resolveRead: ((value: string | null) => void) | null = null
67
+
68
+ constructor() {
69
+ // @ts-ignore: lack of types in deno
70
+ self.onmessage = this.handleMessage
71
+ }
72
+
73
+ private handleMessage = (event: MessageEvent) => {
74
+ const message = event.data
75
+
76
+ // Handle destroy signal
77
+ if (message === DESTROY_SIGNAL) {
78
+ this.destroy()
79
+ return
80
+ }
81
+
82
+ if (this.resolveRead) {
83
+ this.resolveRead(message)
84
+ this.resolveRead = null
85
+ } else {
86
+ this.messageQueue.push(message)
87
+ }
88
+ }
89
+
90
+ async read(): Promise<string | null> {
91
+ if (this.messageQueue.length > 0) {
92
+ return this.messageQueue.shift() ?? null
93
+ }
94
+
95
+ return new Promise((resolve) => {
96
+ this.resolveRead = resolve
97
+ })
98
+ }
99
+
100
+ async write(data: string): Promise<void> {
101
+ // @ts-ignore: lack of types in deno
102
+ self.postMessage(data)
103
+ }
104
+
105
+ destroy(): void {
106
+ // @ts-ignore: lack of types in deno
107
+ self.postMessage(DESTROY_SIGNAL)
108
+ // In a worker context, we can use close() to terminate the worker
109
+ self.close()
110
+ }
111
+
112
+ signalDestroy(): void {
113
+ // @ts-ignore: lack of types in deno
114
+ self.postMessage(DESTROY_SIGNAL)
115
+ }
116
+ }