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
package/scripts/test.ts
ADDED
|
@@ -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
|
+
}
|