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 +21 -0
- package/README.md +209 -0
- package/jsconfig.json +29 -0
- package/package.json +34 -0
- package/src/client.js +1 -0
- package/src/lib.js +810 -0
- package/src/lib.test.js +204 -0
- package/src/server.js +76 -0
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
|
+
}
|
package/src/lib.test.js
ADDED
@@ -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
|
+
}
|