js-rpc2 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 yuanliwei
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,209 @@
1
+ # rpc
2
+
3
+ ## example
4
+ ```js
5
+ import { test } from 'node:test'
6
+ import { deepStrictEqual, strictEqual } from 'node:assert'
7
+ import { createRpcClientHttp, createRpcClientWebSocket, sleep, Uint8Array_from } from './lib.js'
8
+ import { createServer } from 'node:http'
9
+ import { WebSocketServer } from 'ws'
10
+ import Koa from 'koa'
11
+ import Router from 'koa-router'
12
+ import { createRpcServerKoaRouter, createRpcServerWebSocket } from './server.js'
13
+
14
+ test('测试RPC调用-WebSocket', async () => {
15
+ // node --test-name-pattern="^测试RPC调用-WebSocket$" src/lib.test.js
16
+
17
+ const extension = {
18
+ hello: async function (/** @type {string} */ name) {
19
+ return `hello ${name}`
20
+ },
21
+ callback: async function (/** @type {string} */ name, /** @type {(data: string) => void} */ update) {
22
+ for (let i = 0; i < 3; i++) {
23
+ update(`progress ${i}`)
24
+ await sleep(30)
25
+ }
26
+ return `hello callback ${name}`
27
+ },
28
+ callback2: async function (/** @type {string} */ name, /** @type {(data: string, data2:Uint8Array) => void} */ update) {
29
+ for (let i = 0; i < 3; i++) {
30
+ update(`progress ${i}`, Uint8Array_from('2345'))
31
+ await sleep(30)
32
+ }
33
+ return `hello callback ${name}`
34
+ },
35
+ buffer: async function (/** @type {Uint8Array} */ buffer) {
36
+ return buffer.slice(3, 8)
37
+ },
38
+ buffer2: async function (/**@type{string}*/string,/** @type {Uint8Array} */ buffer) {
39
+ return ['message:' + string, buffer.slice(3, 8)]
40
+ },
41
+ bigbuffer: async function (/** @type {Uint8Array} */ buffer) {
42
+ return buffer.slice(3)
43
+ },
44
+ array: async function (/** @type {string} */ name,/** @type {Uint8Array} */ buffer) {
45
+ return [123, 'abc', 'hi ' + name, buffer.slice(3, 8)]
46
+ },
47
+ void: async function (/** @type {string} */ name,/** @type {Uint8Array} */ buffer) {
48
+ console.info('call void')
49
+ }
50
+ }
51
+
52
+ await runWithAbortController(async (ac) => {
53
+ let server = createServer()
54
+ ac.signal.addEventListener('abort', () => { server.close() })
55
+ let wss = new WebSocketServer({ server })
56
+ createRpcServerWebSocket({
57
+ path: '/3f1d664e469aa24b54d6bad0d6d869c0',
58
+ wss: wss,
59
+ rpcKey: '11474f3dfbb861700cb6c3864b328311',
60
+ extension: extension,
61
+ })
62
+ server.listen(9000)
63
+ await sleep(100)
64
+
65
+ /** @type{typeof extension} */
66
+ let client = createRpcClientWebSocket({
67
+ url: `ws://127.0.0.1:9000/3f1d664e469aa24b54d6bad0d6d869c0`,
68
+ rpcKey: '11474f3dfbb861700cb6c3864b328311',
69
+ signal: ac.signal,
70
+ })
71
+ await sleep(100)
72
+
73
+ let string = await client.hello('asdfghjkl')
74
+ console.info(string)
75
+ strictEqual(string, 'hello asdfghjkl')
76
+
77
+ let stringcallback = await client.callback('asdfghjkl', (progress) => {
78
+ console.info(`client : ${progress}`)
79
+ })
80
+ strictEqual(stringcallback, 'hello callback asdfghjkl')
81
+
82
+ let callbackCount = 0
83
+ let stringcallback2 = await client.callback2('asdfghjkl', (progress, buffer) => {
84
+ console.info(`client : ${progress}`, buffer)
85
+ callbackCount++
86
+ })
87
+ strictEqual(3, callbackCount)
88
+ strictEqual(stringcallback2, 'hello callback asdfghjkl')
89
+
90
+ let buffer = Uint8Array_from('qwertyuiop')
91
+ let slice = await client.buffer(buffer)
92
+ deepStrictEqual(slice, buffer.slice(3, 8))
93
+
94
+ let slice2 = await client.buffer(new Uint8Array(300000))
95
+ deepStrictEqual(slice2, new Uint8Array(10).slice(3, 8))
96
+
97
+ let array = await client.array('asdfghjkl', buffer)
98
+ deepStrictEqual(array, [123, 'abc', 'hi asdfghjkl', buffer.slice(3, 8)])
99
+
100
+ let retvoid = await client.void('asdfghjkl', buffer)
101
+ strictEqual(retvoid, undefined)
102
+
103
+ let retbuffer2 = await client.buffer2('asdfghjkl', new Uint8Array(300000))
104
+ deepStrictEqual(retbuffer2, ['message:asdfghjkl', new Uint8Array(300).slice(3, 8)])
105
+ })
106
+
107
+ })
108
+
109
+ test('测试RPC调用-KoaRouter', async () => {
110
+ // node --test-name-pattern="^测试RPC调用-KoaRouter$" src/lib.test.js
111
+ const extension = {
112
+ hello: async function (/** @type {string} */ name) {
113
+ return `hello ${name}`
114
+ },
115
+ callback: async function (/** @type {string} */ name, /** @type {(data: string) => void} */ update) {
116
+ for (let i = 0; i < 3; i++) {
117
+ update(`progress ${i}`)
118
+ await sleep(30)
119
+ }
120
+ return `hello callback ${name}`
121
+ },
122
+ callback2: async function (/** @type {string} */ name, /** @type {(data: string, data2:Uint8Array) => void} */ update) {
123
+ for (let i = 0; i < 3; i++) {
124
+ update(`progress ${i}`, Uint8Array_from('2345'))
125
+ await sleep(30)
126
+ }
127
+ return `hello callback ${name}`
128
+ },
129
+ buffer: async function (/** @type {Uint8Array} */ buffer) {
130
+ return buffer.slice(3, 8)
131
+ },
132
+ array: async function (/** @type {string} */ name,/** @type {Uint8Array} */ buffer) {
133
+ return [123, 'abc', 'hi ' + name, buffer.slice(3, 8)]
134
+ },
135
+ void: async function (/** @type {string} */ name,/** @type {Uint8Array} */ buffer) {
136
+ console.info('call void')
137
+ }
138
+ }
139
+
140
+ await runWithAbortController(async (ac) => {
141
+ let server = createServer()
142
+ let app = new Koa()
143
+ let router = new Router()
144
+
145
+ ac.signal.addEventListener('abort', () => { server.close() })
146
+ createRpcServerKoaRouter({
147
+ path: '/3f1d664e469aa24b54d6bad0d6d869c0',
148
+ router: router,
149
+ rpcKey: '11474f3dfbb861700cb6c3864b328311',
150
+ extension: extension,
151
+ })
152
+
153
+ server.addListener('request', app.callback())
154
+ app.use(router.routes())
155
+ app.use(router.allowedMethods())
156
+
157
+ server.listen(9000)
158
+ await sleep(100)
159
+
160
+ /** @type{typeof extension} */
161
+ let client = createRpcClientHttp({
162
+ url: `http://127.0.0.1:9000/3f1d664e469aa24b54d6bad0d6d869c0`,
163
+ rpcKey: '11474f3dfbb861700cb6c3864b328311',
164
+ signal: ac.signal,
165
+ })
166
+ await sleep(100)
167
+
168
+ let string = await client.hello('asdfghjkl')
169
+ console.info(string)
170
+ strictEqual(string, 'hello asdfghjkl')
171
+
172
+ let callbackCount = 0
173
+ let stringcallback = await client.callback('asdfghjkl', (progress) => {
174
+ console.info(`client : ${progress}`)
175
+ callbackCount++
176
+ })
177
+
178
+ strictEqual(3, callbackCount)
179
+ strictEqual(stringcallback, 'hello callback asdfghjkl')
180
+
181
+ let stringcallback2 = await client.callback2('asdfghjkl', (progress, buffer) => {
182
+ console.info(`client : ${progress}`, buffer)
183
+ })
184
+ strictEqual(stringcallback2, 'hello callback asdfghjkl')
185
+
186
+ let buffer = Uint8Array_from('qwertyuiop')
187
+ let slice = await client.buffer(buffer)
188
+ deepStrictEqual(slice, buffer.slice(3, 8))
189
+
190
+ let array = await client.array('asdfghjkl', buffer)
191
+ deepStrictEqual(array, [123, 'abc', 'hi asdfghjkl', buffer.slice(3, 8)])
192
+
193
+ let retvoid = await client.void('asdfghjkl', buffer)
194
+ strictEqual(retvoid, undefined)
195
+ })
196
+ console.info('over!')
197
+ })
198
+
199
+ /**
200
+ * @param {(ac: AbortController) => Promise<void>} func
201
+ */
202
+ export async function runWithAbortController(func) {
203
+ let ac = new AbortController()
204
+ try {
205
+ await func(ac)
206
+ await sleep(1000)
207
+ } finally { ac.abort() }
208
+ }
209
+ ```
package/jsconfig.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ "moduleResolution": "nodenext",
4
+ "target": "ESNext",
5
+ "module": "NodeNext",
6
+ "types": ["node"],
7
+ "isolatedModules": true,
8
+ "resolveJsonModule": true,
9
+ /**
10
+ * To have warnings / errors of the Svelte compiler at the
11
+ * correct position, enable source maps by default.
12
+ */
13
+ "sourceMap": true,
14
+ "esModuleInterop": true,
15
+ "skipLibCheck": true,
16
+ "forceConsistentCasingInFileNames": true,
17
+ "baseUrl": ".",
18
+ /**
19
+ * Typecheck JS in `.svelte` and `.js` files by default.
20
+ * Disable this if you'd like to use dynamic types.
21
+ */
22
+ "checkJs": true
23
+ },
24
+ /**
25
+ * Use global.d.ts instead of compilerOptions.types
26
+ * to avoid limiting type declarations.
27
+ */
28
+ "include": ["src/**/*.d.ts", "src/**/*.js", "bin/**/*.js", "src/**/*.svelte", "*.js"]
29
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "js-rpc2",
3
+ "version": "1.0.0",
4
+ "description": "js web websocket http rpc",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "test": "node --test"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+ssh://git@github.com/yuanliwei/js-rpc.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/yuanliwei/js-rpc/issues"
16
+ },
17
+ "homepage": "https://github.com/yuanliwei/js-rpc#readme",
18
+ "types": "types.d.ts",
19
+ "keywords": [
20
+ "js",
21
+ "web",
22
+ "http",
23
+ "websocket",
24
+ "rpc"
25
+ ],
26
+ "author": "yuanliwei",
27
+ "license": "MIT",
28
+ "devDependencies": {
29
+ "@types/node": "^22.9.4",
30
+ "koa": "^2.15.3",
31
+ "koa-router": "^13.0.1",
32
+ "ws": "^8.18.0"
33
+ }
34
+ }
package/src/client.js ADDED
@@ -0,0 +1 @@
1
+ export { createRpcClientHelper, createRpcClientWebSocket, createRpcClientHttp } from './lib.js'
package/src/lib.js ADDED
@@ -0,0 +1,810 @@
1
+ const JS_RPC_WITH_CRYPTO = true
2
+
3
+ export const sleep = (/** @type {number} */ timeout) => new Promise((resolve) => setTimeout(resolve, timeout))
4
+
5
+
6
+ /**
7
+ * @typedef {{
8
+ * promise: Promise<any>;
9
+ * resolve: (value: any | null) => void;
10
+ * reject: (reason: any | null) => void;
11
+ * }} PromiseResolvers
12
+ */
13
+
14
+ export function Promise_withResolvers() {
15
+ /** @type{(value?:object)=>void} */
16
+ let resolve = null
17
+ /** @type{(reason?:object)=>void} */
18
+ let reject = null
19
+ const promise = new Promise((res, rej) => {
20
+ resolve = res
21
+ reject = rej
22
+ })
23
+ return { promise, resolve, reject }
24
+ }
25
+
26
+ /**
27
+ * @param {Promise<[CryptoKey,Uint8Array]>} key_iv
28
+ * @returns {TransformStream<Uint8Array, Uint8Array>}
29
+ */
30
+ export function createEncodeStream(key_iv) {
31
+ let key = null
32
+ let iv = null
33
+ return new TransformStream({
34
+ async start() {
35
+ [key, iv] = await key_iv
36
+ },
37
+ async transform(chunk, controller) {
38
+ let buffer = await buildBufferData([chunk], key, iv)
39
+ controller.enqueue(buffer)
40
+ }
41
+ })
42
+ }
43
+
44
+ /**
45
+ * @param {Promise<[CryptoKey,Uint8Array]>} key_iv
46
+ * @returns {TransformStream<Uint8Array, Uint8Array>}
47
+ */
48
+ export function createDecodeStream(key_iv) {
49
+ let key = null
50
+ let iv = null
51
+ let last = new Uint8Array(0)
52
+ return new TransformStream({
53
+ async start() {
54
+ [key, iv] = await key_iv
55
+ },
56
+ async transform(chunk, controller) {
57
+ let [queueReceive, remain] = await parseBufferData(Uint8Array_concat([last, chunk]), key, iv)
58
+ last = remain
59
+ for (const o of queueReceive) {
60
+ controller.enqueue(o)
61
+ }
62
+ }
63
+ })
64
+ }
65
+
66
+ const HEADER_CHECK = 0xb1f7705f
67
+
68
+ /**
69
+ * @param {Uint8Array[]} queue
70
+ * @param {CryptoKey} key
71
+ * @param {Uint8Array} iv
72
+ * @returns {Promise<Uint8Array>}
73
+ */
74
+ export async function buildBufferData(queue, key, iv) {
75
+ let buffers = []
76
+ for (const data of queue) {
77
+ let offset = 0
78
+ let header = new Uint8Array(8)
79
+ let headerCheck = HEADER_CHECK
80
+ let buffer = await encrypt(data, key, iv)
81
+ writeUInt32LE(header, buffer.length, offset); offset += 4
82
+ writeUInt32LE(header, headerCheck, offset); offset += 4
83
+ buffers.push(header, buffer)
84
+ }
85
+ return Uint8Array_concat(buffers)
86
+ }
87
+
88
+ /**
89
+ * @param {Uint8Array} buffer
90
+ * @param {CryptoKey} key
91
+ * @param {Uint8Array} iv
92
+ * @returns {Promise<[Uint8Array[],Uint8Array]>}
93
+ */
94
+ export async function parseBufferData(buffer, key, iv) {
95
+ /** @type{Uint8Array[]} */
96
+ let queue = []
97
+ let offset = 0
98
+ let remain = new Uint8Array(0)
99
+ while (offset < buffer.length) {
100
+ if (offset + 8 > buffer.length) {
101
+ remain = buffer.subarray(offset)
102
+ break
103
+ }
104
+ let bufferLength = readUInt32LE(buffer, offset); offset += 4
105
+ let headerCheck = readUInt32LE(buffer, offset); offset += 4
106
+ if (offset + bufferLength > buffer.length) {
107
+ remain = buffer.subarray(offset - 8)
108
+ break
109
+ }
110
+ let check = HEADER_CHECK
111
+ if (check !== headerCheck) {
112
+ remain = new Uint8Array(0)
113
+ console.error('data check error!', bufferLength, check.toString(16), headerCheck.toString(16))
114
+ break
115
+ }
116
+ let data = buffer.subarray(offset, offset + bufferLength); offset += bufferLength
117
+ let buf = await decrypt(data, key, iv)
118
+ queue.push(buf)
119
+ }
120
+ return [queue, remain]
121
+ }
122
+
123
+
124
+ /**
125
+ * @param {Uint8Array} buffer
126
+ * @param {number} offset
127
+ */
128
+ export function readUInt32LE(buffer, offset) {
129
+ if (offset < 0 || offset + 4 > buffer.length) throw new RangeError('Reading out of bounds')
130
+ return ((buffer[offset] & 0xff) |
131
+ ((buffer[offset + 1] & 0xff) << 8) |
132
+ ((buffer[offset + 2] & 0xff) << 16) |
133
+ ((buffer[offset + 3] & 0xff) << 24)) >>> 0 // >>> 0 to convert to unsigned
134
+ }
135
+
136
+ /**
137
+ * @param {Uint8Array} buffer
138
+ * @param {number} value
139
+ * @param {number} offset
140
+ */
141
+ export function writeUInt32LE(buffer, value, offset) {
142
+ if (offset < 0 || offset + 4 > buffer.length) throw new RangeError('Writing out of bounds')
143
+ buffer[offset] = value & 0xff
144
+ buffer[offset + 1] = (value >> 8) & 0xff
145
+ buffer[offset + 2] = (value >> 16) & 0xff
146
+ buffer[offset + 3] = (value >> 24) & 0xff
147
+ }
148
+
149
+ /**
150
+ * @param {Uint8Array[]} buffers
151
+ */
152
+ export function Uint8Array_concat(buffers) {
153
+ const totalLength = buffers.reduce((sum, buffer) => sum + buffer.length, 0)
154
+ const resultBuffer = new Uint8Array(totalLength)
155
+ let offset = 0
156
+ for (const buffer of buffers) {
157
+ resultBuffer.set(buffer, offset)
158
+ offset += buffer.length
159
+ }
160
+ return resultBuffer
161
+ }
162
+
163
+ /**
164
+ * @param {*} array
165
+ * @param {'utf-8'|'hex'|'base64'} [encoding]
166
+ */
167
+ export function Uint8Array_from(array, encoding) {
168
+ if (encoding == 'hex') {
169
+ array = new Uint8Array(array.match(/[\da-f]{2}/gi).map((h) => parseInt(h, 16)))
170
+ }
171
+ if (encoding == 'base64') {
172
+ array = Uint8Array.from(atob(array), (o) => o.codePointAt(0))
173
+ }
174
+ if (encoding == 'utf-8') {
175
+ array = new TextEncoder().encode(array)
176
+ }
177
+ if (typeof array === 'string') {
178
+ array = new TextEncoder().encode(array)
179
+ }
180
+ if (Array.isArray(array) || array instanceof Uint8Array) {
181
+ return new Uint8Array(array)
182
+ }
183
+ throw new TypeError('Argument must be an array or Uint8Array')
184
+ }
185
+
186
+ /**
187
+ * @param {Uint8Array} buffer
188
+ * @param {'utf-8' | 'hex' | 'base64'} [encoding]
189
+ */
190
+ export function Uint8Array_toString(buffer, encoding = 'utf-8') {
191
+ if (encoding == 'hex') {
192
+ return Array.from(buffer).map((b) => b.toString(16).padStart(2, "0")).join('')
193
+ }
194
+ if (encoding == 'base64') {
195
+ return btoa(String.fromCharCode(...buffer))
196
+ }
197
+ // utf-8
198
+ return new TextDecoder().decode(buffer)
199
+ }
200
+
201
+ /**
202
+ * @param {number} number
203
+ */
204
+ function buildBufferNumberUInt32LE(number) {
205
+ let buffer = new Uint8Array(4)
206
+ writeUInt32LE(buffer, number, 0)
207
+ return buffer
208
+ }
209
+
210
+ /**
211
+ * @param {string} string
212
+ */
213
+ function buildBufferSizeString(string) {
214
+ let buffer = new TextEncoder().encode(string)
215
+ return Uint8Array_concat([
216
+ buildBufferNumberUInt32LE(buffer.length),
217
+ buffer,
218
+ ])
219
+ }
220
+
221
+ /**
222
+ * @param {Uint8Array} buffer
223
+ * @param {number} offset
224
+ */
225
+ function readBufferSizeString(buffer, offset) {
226
+ let size = readUInt32LE(buffer, offset)
227
+ let start = offset + 4
228
+ let end = start + size
229
+ let string = new TextDecoder().decode(buffer.slice(start, end))
230
+ return { size: 4 + size, string }
231
+ }
232
+
233
+ export function guid() {
234
+ let buffer = new Uint8Array(16)
235
+ if (globalThis.crypto) {
236
+ crypto.getRandomValues(buffer)
237
+ } else {
238
+ for (let i = 0; i < buffer.length; i++) {
239
+ buffer[i] = Math.floor(Math.random() * 256)
240
+ }
241
+ }
242
+ return Array.from(buffer).map((o) => o.toString(16).padStart(2, '0')).join('')
243
+ }
244
+
245
+ /**
246
+ *
247
+ * @param {string} password
248
+ * @param {number} iterations
249
+ * @returns {Promise<[CryptoKey,Uint8Array]>}
250
+ */
251
+ export async function buildKeyIv(password, iterations) {
252
+ if (!JS_RPC_WITH_CRYPTO) return [null, null]
253
+ if (!password) return [null, null]
254
+ const keyMaterial = await crypto.subtle.importKey(
255
+ "raw",
256
+ new TextEncoder().encode(password),
257
+ "PBKDF2",
258
+ false,
259
+ ["deriveBits", "deriveKey"],
260
+ )
261
+ const salt = await crypto.subtle.digest("SHA-512", new TextEncoder().encode(password))
262
+ const pbkdf2Params = {
263
+ name: "PBKDF2",
264
+ salt,
265
+ iterations: iterations,
266
+ hash: "SHA-256",
267
+ }
268
+ const key = await crypto.subtle.deriveKey(
269
+ pbkdf2Params,
270
+ keyMaterial,
271
+ { name: "AES-GCM", length: 256 },
272
+ true,
273
+ ["encrypt", "decrypt"],
274
+ )
275
+ const iv = await crypto.subtle.deriveBits(
276
+ pbkdf2Params,
277
+ keyMaterial,
278
+ 256,
279
+ )
280
+ return [key, new Uint8Array(iv)]
281
+ }
282
+
283
+ /**
284
+ *
285
+ * @param {Uint8Array} data
286
+ * @param {CryptoKey} key
287
+ * @param {Uint8Array} iv
288
+ * @returns
289
+ */
290
+ export async function encrypt(data, key, iv) {
291
+ if (!JS_RPC_WITH_CRYPTO) return data
292
+ if (!key) return data
293
+ const encryptedData = await crypto.subtle.encrypt(
294
+ { name: 'AES-GCM', iv: iv }, key, data
295
+ )
296
+ return new Uint8Array(encryptedData)
297
+ }
298
+
299
+ /**
300
+ * @param {Uint8Array} data
301
+ * @param {CryptoKey} key
302
+ * @param {Uint8Array} iv
303
+ * @returns
304
+ */
305
+ export async function decrypt(data, key, iv) {
306
+ if (!JS_RPC_WITH_CRYPTO) return data
307
+ if (!key) return data
308
+ const encryptedArray = data
309
+ const decryptedData = await crypto.subtle.decrypt(
310
+ { name: 'AES-GCM', iv: iv }, key, encryptedArray
311
+ )
312
+ return new Uint8Array(decryptedData)
313
+ }
314
+
315
+ /**
316
+ * @param {AbortSignal} signal
317
+ * @param {()=> Promise<void>} callback
318
+ */
319
+ export async function timeWaitRetryLoop(signal, callback) {
320
+ let waitTime = 300
321
+ while (!signal.aborted) {
322
+ let time = performance.now()
323
+ try {
324
+ await callback()
325
+ } catch (error) {
326
+ console.error('timeWaitRetryLoop', error.message)
327
+ }
328
+ if (performance.now() - time > 10_000) {
329
+ waitTime = 300
330
+ }
331
+ await sleep(waitTime)
332
+ waitTime *= 2
333
+ if (waitTime > 60_000) {
334
+ waitTime = 60_000
335
+ }
336
+ }
337
+ }
338
+
339
+ export const RPC_TYPE_CALL = 0xdf68f4cb
340
+ export const RPC_TYPE_RETURN = 0x68b17581
341
+ export const RPC_TYPE_RETURN_ARRAY = 0xceddbf64
342
+ export const RPC_TYPE_CALLBACK = 0x8d65e5cc
343
+ export const RPC_TYPE_ERROR = 0xa07c0f84
344
+
345
+ /**
346
+ * @typedef {RPC_TYPE_CALL
347
+ * |RPC_TYPE_RETURN
348
+ * |RPC_TYPE_RETURN_ARRAY
349
+ * |RPC_TYPE_CALLBACK
350
+ * |RPC_TYPE_ERROR} RPC_TYPES
351
+ */
352
+
353
+ /**
354
+ * @typedef {{
355
+ * id:number;
356
+ * type:RPC_TYPES;
357
+ * promise?:PromiseResolvers;
358
+ * callback?:(...data:object)=>void;
359
+ * }} CALLBACK_ITEM
360
+ */
361
+
362
+ /**
363
+ * @typedef {{type:RPC_DATA_AGR_TYPE;data:object}} RPC_DATA_ARG_ITEM
364
+ * @typedef {{
365
+ * id:number;
366
+ * type:RPC_TYPES;
367
+ * data:{type:RPC_DATA_AGR_TYPE;data:object}[];
368
+ * }} RPC_DATA
369
+ */
370
+
371
+ export const RPC_DATA_AGR_TYPE_OBJECT = 0xa7f68c
372
+ export const RPC_DATA_AGR_TYPE_FUNCTION = 0x7ff45f
373
+ export const RPC_DATA_AGR_TYPE_UINT8ARRAY = 0xedb218
374
+ export const RPC_DATA_AGR_TYPE_UNDEFINED = 0x7f77fe
375
+ export const RPC_DATA_AGR_TYPE_NULL = 0x5794f9
376
+
377
+ /**
378
+ * @typedef {RPC_DATA_AGR_TYPE_OBJECT
379
+ * |RPC_DATA_AGR_TYPE_FUNCTION
380
+ * |RPC_DATA_AGR_TYPE_UINT8ARRAY
381
+ * |RPC_DATA_AGR_TYPE_UNDEFINED
382
+ * |RPC_DATA_AGR_TYPE_NULL} RPC_DATA_AGR_TYPE
383
+ */
384
+
385
+ /**
386
+ * @param {RPC_DATA} box
387
+ */
388
+ export function buildRpcData(box) {
389
+ let buffers = [
390
+ buildBufferNumberUInt32LE(box.id),
391
+ buildBufferNumberUInt32LE(box.type),
392
+ buildBufferNumberUInt32LE(box.data.length),
393
+ ]
394
+ for (const o of box.data) {
395
+ if (o.type == RPC_DATA_AGR_TYPE_UINT8ARRAY) {
396
+ buffers.push(buildBufferNumberUInt32LE(o.type))
397
+ buffers.push(buildBufferNumberUInt32LE(o.data.length))
398
+ buffers.push(o.data)
399
+ }
400
+ if (o.type == RPC_DATA_AGR_TYPE_FUNCTION) {
401
+ buffers.push(buildBufferNumberUInt32LE(o.type))
402
+ buffers.push(buildBufferSizeString(o.data))
403
+ }
404
+ if (o.type == RPC_DATA_AGR_TYPE_OBJECT) {
405
+ buffers.push(buildBufferNumberUInt32LE(o.type))
406
+ buffers.push(buildBufferSizeString(JSON.stringify(o.data)))
407
+ }
408
+ if (o.type == RPC_DATA_AGR_TYPE_UNDEFINED) {
409
+ buffers.push(buildBufferNumberUInt32LE(o.type))
410
+ }
411
+ if (o.type == RPC_DATA_AGR_TYPE_NULL) {
412
+ buffers.push(buildBufferNumberUInt32LE(o.type))
413
+ }
414
+ }
415
+ return Uint8Array_concat(buffers)
416
+ }
417
+
418
+ /**
419
+ * @param {Uint8Array} buffer
420
+ */
421
+ export function parseRpcData(buffer) {
422
+ let offset = 0
423
+ let id = readUInt32LE(buffer, offset)
424
+ offset += 4
425
+ /** @type{*} */
426
+ let type = readUInt32LE(buffer, offset)
427
+ offset += 4
428
+ let dataLength = readUInt32LE(buffer, offset)
429
+ offset += 4
430
+ /** @type{RPC_DATA_ARG_ITEM[]} */
431
+ let args = []
432
+ for (let i = 0; i < dataLength; i++) {
433
+ let type = readUInt32LE(buffer, offset)
434
+ offset += 4
435
+ if (type == RPC_DATA_AGR_TYPE_UINT8ARRAY) {
436
+ let size = readUInt32LE(buffer, offset)
437
+ offset += 4
438
+ let data = buffer.slice(offset, offset + size)
439
+ offset += size
440
+ args.push({ type: type, data })
441
+ }
442
+ if (type == RPC_DATA_AGR_TYPE_FUNCTION) {
443
+ let o = readBufferSizeString(buffer, offset)
444
+ offset += o.size
445
+ args.push({ type: type, data: o.string })
446
+ }
447
+ if (type == RPC_DATA_AGR_TYPE_OBJECT) {
448
+ let o = readBufferSizeString(buffer, offset)
449
+ offset += o.size
450
+ let data = o.string
451
+ args.push({ type: type, data: JSON.parse(data) })
452
+ }
453
+ if (type == RPC_DATA_AGR_TYPE_UNDEFINED) {
454
+ args.push({ type: type, data: undefined })
455
+ }
456
+ if (type == RPC_DATA_AGR_TYPE_NULL) {
457
+ args.push({ type: type, data: null })
458
+ }
459
+ }
460
+ /** @type{RPC_DATA} */
461
+ let box = { id, type, data: args }
462
+ return box
463
+ }
464
+
465
+ /**
466
+ * @param {object[]} items
467
+ */
468
+ export function buildRpcItemData(items) {
469
+ /** @type{RPC_DATA_ARG_ITEM[]} */
470
+ let arr = []
471
+ for (const item of items) {
472
+ /** @type{RPC_DATA_AGR_TYPE} */
473
+ let type = null
474
+ let data = null
475
+ if (item === undefined) {
476
+ type = RPC_DATA_AGR_TYPE_UNDEFINED
477
+ data = item
478
+ } else if (item === null) {
479
+ type = RPC_DATA_AGR_TYPE_NULL
480
+ data = item
481
+ } else if (item instanceof Uint8Array) {
482
+ type = RPC_DATA_AGR_TYPE_UINT8ARRAY
483
+ data = item
484
+ } else if (typeof item == 'function') {
485
+ type = RPC_DATA_AGR_TYPE_FUNCTION
486
+ data = item()
487
+ } else {
488
+ type = RPC_DATA_AGR_TYPE_OBJECT
489
+ data = JSON.stringify(item)
490
+ }
491
+ arr.push({ type, data })
492
+ }
493
+ return arr
494
+ }
495
+
496
+ /**
497
+ * @param {RPC_DATA_ARG_ITEM[]} array
498
+ */
499
+ export function parseRpcItemData(array) {
500
+ /** @type{RPC_DATA_ARG_ITEM[]} */
501
+ let items = []
502
+ for (let i = 0; i < array.length; i++) {
503
+ const o = array[i]
504
+ if (o.type == RPC_DATA_AGR_TYPE_FUNCTION) {
505
+ o.data = o.data
506
+ }
507
+ if (o.type == RPC_DATA_AGR_TYPE_NULL) {
508
+ o.data = null
509
+ }
510
+ if (o.type == RPC_DATA_AGR_TYPE_UNDEFINED) {
511
+ o.data = undefined
512
+ }
513
+ if (o.type == RPC_DATA_AGR_TYPE_UINT8ARRAY) {
514
+ o.data = o.data
515
+ }
516
+ if (o.type == RPC_DATA_AGR_TYPE_OBJECT) {
517
+ o.data = JSON.parse(o.data)
518
+ }
519
+ items.push(o)
520
+ }
521
+ return items
522
+ }
523
+
524
+ /**
525
+ * @param {object} extension
526
+ * @param {WritableStreamDefaultWriter<Uint8Array>} writer
527
+ * @param {Uint8Array} buffer
528
+ */
529
+ export async function rpcRunServerDecodeBuffer(extension, writer, buffer) {
530
+ /** @type{RPC_DATA} */
531
+ let box = null
532
+ let dataId = 0
533
+ try {
534
+ let o = parseRpcData(buffer)
535
+ dataId = o.id
536
+ let items = parseRpcItemData(o.data)
537
+ let fnName = items.at(0).data
538
+ let args = items.slice(1)
539
+ let params = []
540
+ for (let i = 0; i < args.length; i++) {
541
+ const p = args[i]
542
+ if (p.type == RPC_DATA_AGR_TYPE_FUNCTION) {
543
+ const callback = async (/** @type {any[]} */ ...args) => {
544
+ /** @type{RPC_DATA} */
545
+ let box = { id: p.data, type: RPC_TYPE_CALLBACK, data: buildRpcItemData(args), }
546
+ await writer.write(buildRpcData(box))
547
+ }
548
+ params.push(callback)
549
+ } else {
550
+ params.push(p.data)
551
+ }
552
+ }
553
+ let ret = await extension[fnName](...params)
554
+ if (Array.isArray(ret)) {
555
+ box = { id: o.id, type: RPC_TYPE_RETURN_ARRAY, data: buildRpcItemData(ret), }
556
+ } else {
557
+ box = { id: o.id, type: RPC_TYPE_RETURN, data: buildRpcItemData([ret]), }
558
+ }
559
+ } catch (error) {
560
+ console.error(error)
561
+ box = {
562
+ id: dataId,
563
+ type: RPC_TYPE_ERROR,
564
+ data: buildRpcItemData([`Error:${error.message}\n${error.stack}`]),
565
+ }
566
+ }
567
+ await writer.write(buildRpcData(box))
568
+ }
569
+
570
+ /**
571
+ * @param {(fnName:string,args:object[])=>Promise<object>} apiInvoke
572
+ */
573
+ export function createRPCProxy(apiInvoke) {
574
+ const map = new Map()
575
+ const proxy = new Proxy(Object(), {
576
+ get(_target, p) {
577
+ let proxy = map.get(p)
578
+ if (proxy) {
579
+ return proxy
580
+ }
581
+ proxy = new Proxy(Function, {
582
+ async apply(_target, _thisArg, argArray) {
583
+ return await apiInvoke(String(p), argArray)
584
+ }
585
+ })
586
+ map.set(p, proxy)
587
+ return proxy
588
+ }
589
+ })
590
+ return proxy
591
+ }
592
+
593
+ /**
594
+ * @typedef {{
595
+ * writable: WritableStream<Uint8Array>;
596
+ * readable: ReadableStream<Uint8Array>;
597
+ * }} RPC_HELPER_SERVER
598
+ */
599
+
600
+ /**
601
+ * @param {{
602
+ * rpcKey: string;
603
+ * extension: object;
604
+ * }} param
605
+ */
606
+ export function createRpcServerHelper(param) {
607
+ let rpc_key_iv = buildKeyIv(param.rpcKey, 10)
608
+ const encode = createEncodeStream(rpc_key_iv)
609
+ const decode = createDecodeStream(rpc_key_iv)
610
+ let writer = encode.writable.getWriter()
611
+ decode.readable.pipeTo(new WritableStream({
612
+ async write(buffer) {
613
+ await rpcRunServerDecodeBuffer(param.extension, writer, buffer)
614
+ },
615
+ async close() {
616
+ await writer.close()
617
+ }
618
+ }))
619
+
620
+ /** @type{RPC_HELPER_SERVER} */
621
+ let ret = { writable: decode.writable, readable: encode.readable }
622
+ return ret
623
+ }
624
+
625
+ /**
626
+ * @typedef {{
627
+ * writable: WritableStream<Uint8Array>;
628
+ * readable: ReadableStream<Uint8Array>;
629
+ * apiInvoke: (fnName: string, args: object[]) => Promise<object>;
630
+ * }} RPC_HELPER_CLIENT
631
+ */
632
+
633
+ /**
634
+ * @param {{ rpcKey: string; }} param
635
+ */
636
+ export function createRpcClientHelper(param) {
637
+
638
+ let uniqueKeyID = 0
639
+
640
+ /** @type{Map<number,CALLBACK_ITEM>} */
641
+ const callbackFunctionMap = new Map()
642
+
643
+ let rpc_key_iv = buildKeyIv(param.rpcKey, 10)
644
+ let decode = createDecodeStream(rpc_key_iv)
645
+ let encode = createEncodeStream(rpc_key_iv)
646
+ let writer = encode.writable.getWriter()
647
+ decode.readable.pipeTo(new WritableStream({
648
+ async write(buffer) {
649
+ try {
650
+ let data = parseRpcData(buffer)
651
+ let items = parseRpcItemData(data.data)
652
+ if (callbackFunctionMap.has(data.id)) {
653
+ let o = callbackFunctionMap.get(data.id)
654
+ if (data.type == RPC_TYPE_ERROR) {
655
+ o.promise.reject(new Error(items.at(0).data))
656
+ }
657
+ if (data.type == RPC_TYPE_RETURN) {
658
+ callbackFunctionMap.delete(data.id)
659
+ o.promise.resolve(items.at(0).data)
660
+ }
661
+ if (data.type == RPC_TYPE_RETURN_ARRAY) {
662
+ callbackFunctionMap.delete(data.id)
663
+ o.promise.resolve(items.map(o => o.data))
664
+ }
665
+ if (data.type == RPC_TYPE_CALLBACK) {
666
+ o.callback(...items.map(o => o.data))
667
+ }
668
+ }
669
+ } catch (error) {
670
+ console.error('apiInvoke', error)
671
+ callbackFunctionMap.forEach((o) => {
672
+ o.promise?.reject(error)
673
+ })
674
+ callbackFunctionMap.clear()
675
+ }
676
+ }
677
+ }))
678
+
679
+ /**
680
+ * @param {string} fnName
681
+ * @param {object[]} args
682
+ */
683
+ async function apiInvoke(fnName, args) {
684
+ let id = uniqueKeyID++
685
+ let promise = Promise_withResolvers()
686
+ callbackFunctionMap.set(id, { id, type: RPC_TYPE_RETURN, promise })
687
+ const keys = []
688
+ /** @type{object[]} */
689
+ let argArray = []
690
+ argArray.push(fnName)
691
+ for (const arg of args) {
692
+ if (arg instanceof Function) {
693
+ const key = uniqueKeyID++
694
+ keys.push(key)
695
+ callbackFunctionMap.set(key, { id: key, type: RPC_TYPE_CALLBACK, callback: arg })
696
+ argArray.push(() => key)
697
+ } else {
698
+ argArray.push(arg)
699
+ }
700
+ }
701
+ try {
702
+ /** @type{RPC_DATA} */
703
+ let box = { id: id, type: RPC_TYPE_CALL, data: buildRpcItemData(argArray) }
704
+ await writer.write(buildRpcData(box))
705
+ return await promise.promise
706
+ } finally {
707
+ for (const key of keys) {
708
+ callbackFunctionMap.delete(key)
709
+ }
710
+ }
711
+ }
712
+
713
+ /** @type{RPC_HELPER_CLIENT} */
714
+ let ret = { writable: decode.writable, readable: encode.readable, apiInvoke }
715
+ return ret
716
+ }
717
+
718
+ /**
719
+ * @param {{
720
+ * url:string;
721
+ * rpcKey:string;
722
+ * signal:AbortSignal;
723
+ * }} param
724
+ */
725
+ export function createRpcClientWebSocket(param) {
726
+ let helper = createRpcClientHelper({ rpcKey: param.rpcKey })
727
+ let writer = helper.writable.getWriter()
728
+ let signal = Promise_withResolvers()
729
+ /** @type{WritableStreamDefaultWriter<Uint8Array>} */
730
+ let socketWriter = null
731
+ helper.readable.pipeTo(new WritableStream({
732
+ async write(chunk) {
733
+ while (!param.signal.aborted && socketWriter == null) {
734
+ await signal.promise
735
+ }
736
+ if (!param.signal.aborted) {
737
+ await socketWriter.write(chunk)
738
+ }
739
+ }
740
+ }))
741
+ async function createWebSocket() {
742
+ let promise = Promise_withResolvers()
743
+ let ws = new WebSocket(param.url)
744
+ ws.addEventListener('open', () => {
745
+ console.info('createRpcClientWebSocket createWebSocket ws on open')
746
+ socketWriter = new WritableStream({
747
+ async write(chunk) {
748
+ ws.send(chunk)
749
+ }
750
+ }).getWriter()
751
+ ws.addEventListener('message', async (ev) => {
752
+ let buffer = await ev.data.arrayBuffer()
753
+ await writer.write(new Uint8Array(buffer))
754
+ })
755
+ signal.resolve()
756
+ })
757
+ ws.addEventListener('error', (e) => {
758
+ console.error('createRpcClientWebSocket createWebSocket ws error', e)
759
+ promise.resolve()
760
+ })
761
+ ws.addEventListener('close', () => {
762
+ console.error('createRpcClientWebSocket createWebSocket ws close')
763
+ promise.resolve()
764
+ })
765
+ const listenerAC = () => { ws.close() }
766
+ param.signal.addEventListener('abort', listenerAC)
767
+ await promise.promise
768
+ param.signal.removeEventListener('abort', listenerAC)
769
+ socketWriter = null
770
+ signal.resolve()
771
+ signal = Promise_withResolvers()
772
+ }
773
+ timeWaitRetryLoop(param.signal, async () => {
774
+ console.info('createRpcClientWebSocket timeWaitRetryLoop connectWebSocket')
775
+ await createWebSocket()
776
+ })
777
+
778
+ return createRPCProxy(helper.apiInvoke)
779
+ }
780
+
781
+ /**
782
+ * @param {{
783
+ * url:string;
784
+ * rpcKey?:string;
785
+ * signal?:AbortSignal;
786
+ * intercept?:(res:Response)=>void;
787
+ * }} param
788
+ */
789
+ export function createRpcClientHttp(param) {
790
+ let helper = createRpcClientHelper({ rpcKey: param.rpcKey })
791
+ let writer = helper.writable.getWriter()
792
+ helper.readable.pipeTo(new WritableStream({
793
+ async write(chunk) {
794
+ let res = await fetch(param.url, {
795
+ method: 'POST',
796
+ signal: param.signal,
797
+ body: chunk,
798
+ })
799
+ if (param.intercept) {
800
+ param.intercept(res)
801
+ }
802
+ res.body.pipeTo(new WritableStream({
803
+ async write(chunk) {
804
+ await writer.write(chunk)
805
+ }
806
+ })).catch((err) => console.error('createRpcClientHttp fetch error', err.message))
807
+ }
808
+ })).catch((err) => console.error('createRpcClientHttp', err.message))
809
+ return createRPCProxy(helper.apiInvoke)
810
+ }
@@ -0,0 +1,204 @@
1
+ import { test } from 'node:test'
2
+ import { deepStrictEqual, strictEqual } from 'node:assert'
3
+ import { createRpcClientHttp, createRpcClientWebSocket, sleep, Uint8Array_from } from './lib.js'
4
+ import { createServer } from 'node:http'
5
+ import { WebSocketServer } from 'ws'
6
+ import Koa from 'koa'
7
+ import Router from 'koa-router'
8
+ import { createRpcServerKoaRouter, createRpcServerWebSocket } from './server.js'
9
+
10
+ test('测试RPC调用-WebSocket', async () => {
11
+ // node --test-name-pattern="^测试RPC调用-WebSocket$" src/lib.test.js
12
+
13
+ const extension = {
14
+ hello: async function (/** @type {string} */ name) {
15
+ return `hello ${name}`
16
+ },
17
+ callback: async function (/** @type {string} */ name, /** @type {(data: string) => void} */ update) {
18
+ for (let i = 0; i < 3; i++) {
19
+ update(`progress ${i}`)
20
+ await sleep(30)
21
+ }
22
+ return `hello callback ${name}`
23
+ },
24
+ callback2: async function (/** @type {string} */ name, /** @type {(data: string, data2:Uint8Array) => void} */ update) {
25
+ for (let i = 0; i < 3; i++) {
26
+ update(`progress ${i}`, Uint8Array_from('2345'))
27
+ await sleep(30)
28
+ }
29
+ return `hello callback ${name}`
30
+ },
31
+ buffer: async function (/** @type {Uint8Array} */ buffer) {
32
+ return buffer.slice(3, 8)
33
+ },
34
+ buffer2: async function (/**@type{string}*/string,/** @type {Uint8Array} */ buffer) {
35
+ return ['message:' + string, buffer.slice(3, 8)]
36
+ },
37
+ bigbuffer: async function (/** @type {Uint8Array} */ buffer) {
38
+ return buffer.slice(3)
39
+ },
40
+ array: async function (/** @type {string} */ name,/** @type {Uint8Array} */ buffer) {
41
+ return [123, 'abc', 'hi ' + name, buffer.slice(3, 8)]
42
+ },
43
+ void: async function (/** @type {string} */ name,/** @type {Uint8Array} */ buffer) {
44
+ console.info('call void')
45
+ }
46
+ }
47
+
48
+ await runWithAbortController(async (ac) => {
49
+ let server = createServer()
50
+ ac.signal.addEventListener('abort', () => { server.close() })
51
+ let wss = new WebSocketServer({ server })
52
+ createRpcServerWebSocket({
53
+ path: '/3f1d664e469aa24b54d6bad0d6d869c0',
54
+ wss: wss,
55
+ rpcKey: '11474f3dfbb861700cb6c3864b328311',
56
+ extension: extension,
57
+ })
58
+ server.listen(9000)
59
+ await sleep(100)
60
+
61
+ /** @type{typeof extension} */
62
+ let client = createRpcClientWebSocket({
63
+ url: `ws://127.0.0.1:9000/3f1d664e469aa24b54d6bad0d6d869c0`,
64
+ rpcKey: '11474f3dfbb861700cb6c3864b328311',
65
+ signal: ac.signal,
66
+ })
67
+ await sleep(100)
68
+
69
+ let string = await client.hello('asdfghjkl')
70
+ console.info(string)
71
+ strictEqual(string, 'hello asdfghjkl')
72
+
73
+ let stringcallback = await client.callback('asdfghjkl', (progress) => {
74
+ console.info(`client : ${progress}`)
75
+ })
76
+ strictEqual(stringcallback, 'hello callback asdfghjkl')
77
+
78
+ let callbackCount = 0
79
+ let stringcallback2 = await client.callback2('asdfghjkl', (progress, buffer) => {
80
+ console.info(`client : ${progress}`, buffer)
81
+ callbackCount++
82
+ })
83
+ strictEqual(3, callbackCount)
84
+ strictEqual(stringcallback2, 'hello callback asdfghjkl')
85
+
86
+ let buffer = Uint8Array_from('qwertyuiop')
87
+ let slice = await client.buffer(buffer)
88
+ deepStrictEqual(slice, buffer.slice(3, 8))
89
+
90
+ let slice2 = await client.buffer(new Uint8Array(300000))
91
+ deepStrictEqual(slice2, new Uint8Array(10).slice(3, 8))
92
+
93
+ let array = await client.array('asdfghjkl', buffer)
94
+ deepStrictEqual(array, [123, 'abc', 'hi asdfghjkl', buffer.slice(3, 8)])
95
+
96
+ let retvoid = await client.void('asdfghjkl', buffer)
97
+ strictEqual(retvoid, undefined)
98
+
99
+ let retbuffer2 = await client.buffer2('asdfghjkl', new Uint8Array(300000))
100
+ deepStrictEqual(retbuffer2, ['message:asdfghjkl', new Uint8Array(300).slice(3, 8)])
101
+ })
102
+
103
+ })
104
+
105
+ test('测试RPC调用-KoaRouter', async () => {
106
+ // node --test-name-pattern="^测试RPC调用-KoaRouter$" src/lib.test.js
107
+ const extension = {
108
+ hello: async function (/** @type {string} */ name) {
109
+ return `hello ${name}`
110
+ },
111
+ callback: async function (/** @type {string} */ name, /** @type {(data: string) => void} */ update) {
112
+ for (let i = 0; i < 3; i++) {
113
+ update(`progress ${i}`)
114
+ await sleep(30)
115
+ }
116
+ return `hello callback ${name}`
117
+ },
118
+ callback2: async function (/** @type {string} */ name, /** @type {(data: string, data2:Uint8Array) => void} */ update) {
119
+ for (let i = 0; i < 3; i++) {
120
+ update(`progress ${i}`, Uint8Array_from('2345'))
121
+ await sleep(30)
122
+ }
123
+ return `hello callback ${name}`
124
+ },
125
+ buffer: async function (/** @type {Uint8Array} */ buffer) {
126
+ return buffer.slice(3, 8)
127
+ },
128
+ array: async function (/** @type {string} */ name,/** @type {Uint8Array} */ buffer) {
129
+ return [123, 'abc', 'hi ' + name, buffer.slice(3, 8)]
130
+ },
131
+ void: async function (/** @type {string} */ name,/** @type {Uint8Array} */ buffer) {
132
+ console.info('call void')
133
+ }
134
+ }
135
+
136
+ await runWithAbortController(async (ac) => {
137
+ let server = createServer()
138
+ let app = new Koa()
139
+ let router = new Router()
140
+
141
+ ac.signal.addEventListener('abort', () => { server.close() })
142
+ createRpcServerKoaRouter({
143
+ path: '/3f1d664e469aa24b54d6bad0d6d869c0',
144
+ router: router,
145
+ rpcKey: '11474f3dfbb861700cb6c3864b328311',
146
+ extension: extension,
147
+ })
148
+
149
+ server.addListener('request', app.callback())
150
+ app.use(router.routes())
151
+ app.use(router.allowedMethods())
152
+
153
+ server.listen(9000)
154
+ await sleep(100)
155
+
156
+ /** @type{typeof extension} */
157
+ let client = createRpcClientHttp({
158
+ url: `http://127.0.0.1:9000/3f1d664e469aa24b54d6bad0d6d869c0`,
159
+ rpcKey: '11474f3dfbb861700cb6c3864b328311',
160
+ signal: ac.signal,
161
+ })
162
+ await sleep(100)
163
+
164
+ let string = await client.hello('asdfghjkl')
165
+ console.info(string)
166
+ strictEqual(string, 'hello asdfghjkl')
167
+
168
+ let callbackCount = 0
169
+ let stringcallback = await client.callback('asdfghjkl', (progress) => {
170
+ console.info(`client : ${progress}`)
171
+ callbackCount++
172
+ })
173
+
174
+ strictEqual(3, callbackCount)
175
+ strictEqual(stringcallback, 'hello callback asdfghjkl')
176
+
177
+ let stringcallback2 = await client.callback2('asdfghjkl', (progress, buffer) => {
178
+ console.info(`client : ${progress}`, buffer)
179
+ })
180
+ strictEqual(stringcallback2, 'hello callback asdfghjkl')
181
+
182
+ let buffer = Uint8Array_from('qwertyuiop')
183
+ let slice = await client.buffer(buffer)
184
+ deepStrictEqual(slice, buffer.slice(3, 8))
185
+
186
+ let array = await client.array('asdfghjkl', buffer)
187
+ deepStrictEqual(array, [123, 'abc', 'hi asdfghjkl', buffer.slice(3, 8)])
188
+
189
+ let retvoid = await client.void('asdfghjkl', buffer)
190
+ strictEqual(retvoid, undefined)
191
+ })
192
+ console.info('over!')
193
+ })
194
+
195
+ /**
196
+ * @param {(ac: AbortController) => Promise<void>} func
197
+ */
198
+ export async function runWithAbortController(func) {
199
+ let ac = new AbortController()
200
+ try {
201
+ await func(ac)
202
+ await sleep(1000)
203
+ } finally { ac.abort() }
204
+ }
package/src/server.js ADDED
@@ -0,0 +1,76 @@
1
+ import { Readable } from "node:stream"
2
+ import { createRpcServerHelper } from "./lib.js"
3
+
4
+ /**
5
+ * @import {WebSocketServer} from 'ws'
6
+ */
7
+
8
+ export { createRpcServerHelper }
9
+
10
+ /**
11
+ * @import Router from 'koa-router'
12
+ */
13
+
14
+ /**
15
+ * @param {{
16
+ * path: string;
17
+ * wss: WebSocketServer;
18
+ * rpcKey:string;
19
+ * extension: Object;
20
+ * }} param
21
+ */
22
+ export function createRpcServerWebSocket(param) {
23
+ param.wss.on('connection', (ws, request) => {
24
+ let url = request.url
25
+ if (url != param.path) {
26
+ return
27
+ }
28
+ let helper = createRpcServerHelper({ rpcKey: param.rpcKey, extension: param.extension })
29
+ let writer = helper.writable.getWriter()
30
+ helper.readable.pipeTo(new WritableStream({
31
+ async write(chunk) {
32
+ await new Promise((resolve) => {
33
+ ws.send(chunk, resolve)
34
+ })
35
+ }
36
+ }))
37
+ ws.on('message', async (data) => {
38
+ /** @type{*} */
39
+ let buffer = data
40
+ if (writer.desiredSize <= 0) {
41
+ ws.pause()
42
+ }
43
+ await writer.write(buffer)
44
+ ws.resume()
45
+ })
46
+ ws.on('close', () => {
47
+ console.info('createRpcServerWebSocket connection ws close')
48
+ })
49
+ ws.on('error', (error) => {
50
+ console.error('createRpcServerWebSocket connection ws error', error)
51
+ })
52
+ })
53
+ }
54
+
55
+ /**
56
+ *
57
+ * @param {{
58
+ * path: string;
59
+ * router: Router<any, {}>;
60
+ * rpcKey?:string;
61
+ * extension: Object;
62
+ * }} param
63
+ */
64
+ export function createRpcServerKoaRouter(param) {
65
+ param.router.post(param.path, async (ctx) => {
66
+ let helper = createRpcServerHelper({ rpcKey: param.rpcKey, extension: param.extension })
67
+ await Readable.toWeb(ctx.req).pipeTo(helper.writable)
68
+ ctx.status = 200
69
+ ctx.response.set({
70
+ 'Connection': 'keep-alive',
71
+ 'Cache-Control': 'no-cache',
72
+ 'Content-Type': 'application/octet-stream'
73
+ })
74
+ ctx.body = Readable.fromWeb(helper.readable)
75
+ })
76
+ }