@arcblock/event-hub 1.21.3 → 1.22.1
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/lib/rpc.js +121 -0
- package/lib/server.js +81 -16
- package/package.json +4 -4
package/lib/rpc.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
const axon = require('axon');
|
|
3
|
+
const Message = require('amp-message');
|
|
4
|
+
const { randomUUID } = require('crypto');
|
|
5
|
+
|
|
6
|
+
class Client extends axon.Socket {
|
|
7
|
+
/**
|
|
8
|
+
* @param {number} port
|
|
9
|
+
* @param {string} host
|
|
10
|
+
*/
|
|
11
|
+
constructor(port, host = '127.0.0.1') {
|
|
12
|
+
super();
|
|
13
|
+
this.port = port;
|
|
14
|
+
this.host = host;
|
|
15
|
+
this.online = false;
|
|
16
|
+
this.connectPromise = null;
|
|
17
|
+
// reqId -> { resolve, reject, to, errorPrefix? }
|
|
18
|
+
this.inflight = new Map();
|
|
19
|
+
|
|
20
|
+
this.on('connect', () => {
|
|
21
|
+
this.online = true;
|
|
22
|
+
});
|
|
23
|
+
this.on('reconnect attempt', () => {
|
|
24
|
+
this.online = false;
|
|
25
|
+
});
|
|
26
|
+
this.on('disconnect', () => {
|
|
27
|
+
this.online = false;
|
|
28
|
+
});
|
|
29
|
+
this.on('error', () => {
|
|
30
|
+
/* no-op */
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
onmessage() {
|
|
35
|
+
return (buf) => {
|
|
36
|
+
try {
|
|
37
|
+
const msg = new Message(buf);
|
|
38
|
+
console.log('==debug==', msg);
|
|
39
|
+
const [event, data] = msg.args;
|
|
40
|
+
|
|
41
|
+
if (data && typeof data === 'object' && data.reqId) {
|
|
42
|
+
const entry = this.inflight.get(data.reqId);
|
|
43
|
+
if (entry) {
|
|
44
|
+
clearTimeout(entry.to);
|
|
45
|
+
this.inflight.delete(data.reqId);
|
|
46
|
+
if (data.ok === false) {
|
|
47
|
+
entry.reject(
|
|
48
|
+
new Error(entry.errorPrefix ? `${entry.errorPrefix}: ${data.error}` : data.error || 'request failed')
|
|
49
|
+
);
|
|
50
|
+
} else {
|
|
51
|
+
entry.resolve(data.data);
|
|
52
|
+
}
|
|
53
|
+
return; // 已消费
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
this.emit(event, data);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
console.error('[axon] decode error', e);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
sendEvent(event, data) {
|
|
64
|
+
const buf = this.pack([event, data]);
|
|
65
|
+
const s = this.socks[0];
|
|
66
|
+
if (s && s.writable) s.write(buf);
|
|
67
|
+
else throw new Error('axon socket not writable/connected');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async ensureOnline() {
|
|
71
|
+
if (!this.port || !this.host) {
|
|
72
|
+
throw new Error('PORT and HOST must be set via constructor: new Client(port, host)');
|
|
73
|
+
}
|
|
74
|
+
if (!this.connectPromise) {
|
|
75
|
+
this.connectPromise = new Promise((resolve, reject) => {
|
|
76
|
+
this.connect(this.port, this.host, (err) => (err ? reject(err) : resolve(this)));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
await this.connectPromise;
|
|
80
|
+
if (this.online) return this;
|
|
81
|
+
// 等到重连完成
|
|
82
|
+
await new Promise((r) => this.once('connect', r));
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 通用 RPC:发送 event,等待响应(默认只按 reqId 匹配)
|
|
88
|
+
* @param {string} event 例如 'pm2/start'
|
|
89
|
+
* @param {object} payload 发送为 { reqId, payload }
|
|
90
|
+
* @param {object} [opts]
|
|
91
|
+
* @param {number} [opts.timeoutMs=120000]
|
|
92
|
+
* @param {string} [opts.respEvent] // 可选:仅用于调试;匹配靠 reqId,不强依赖此项
|
|
93
|
+
* @param {string} [opts.errorPrefix] // 统一错误前缀
|
|
94
|
+
* @returns {Promise<any>}
|
|
95
|
+
*/
|
|
96
|
+
async rpc(event, payload = {}, { timeoutMs = 120000, respEvent, errorPrefix } = {}) {
|
|
97
|
+
await this.ensureOnline();
|
|
98
|
+
const reqId = randomUUID();
|
|
99
|
+
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const to = setTimeout(() => {
|
|
102
|
+
this.inflight.delete(reqId);
|
|
103
|
+
reject(new Error(`${errorPrefix || 'request'} timeout`));
|
|
104
|
+
}, timeoutMs);
|
|
105
|
+
|
|
106
|
+
this.inflight.set(reqId, { resolve, reject, to, errorPrefix });
|
|
107
|
+
|
|
108
|
+
this.sendEvent(event, { reqId, payload });
|
|
109
|
+
|
|
110
|
+
if (respEvent) {
|
|
111
|
+
const once = (data) => {
|
|
112
|
+
if (!data || data.reqId !== reqId) return;
|
|
113
|
+
this.off(respEvent, once);
|
|
114
|
+
};
|
|
115
|
+
this.on(respEvent, once);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = Client;
|
package/lib/server.js
CHANGED
|
@@ -1,23 +1,82 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
2
|
const axon = require('axon');
|
|
3
3
|
const Message = require('amp-message');
|
|
4
|
-
|
|
5
4
|
const JWT = require('@arcblock/jwt');
|
|
6
|
-
|
|
7
5
|
const { EVENT_AUTH, EVENT_AUTH_FAIL } = require('./constant');
|
|
8
6
|
|
|
9
7
|
const getDid = (jwt) => jwt.iss.replace(/^did:abt:/, '');
|
|
10
8
|
|
|
9
|
+
// 兼容 { reqId, payload } 或直接 payload
|
|
10
|
+
function normalizeReq(input) {
|
|
11
|
+
if (input && typeof input === 'object' && ('payload' in input || 'reqId' in input)) {
|
|
12
|
+
const { reqId, payload } = input;
|
|
13
|
+
return { reqId, payload };
|
|
14
|
+
}
|
|
15
|
+
return { reqId: undefined, payload: input };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 规范响应:默认 { ok: true, data }
|
|
19
|
+
function normalizeRes(result) {
|
|
20
|
+
if (result && typeof result === 'object' && ('ok' in result || 'data' in result || 'error' in result)) {
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
return { ok: true, data: result };
|
|
24
|
+
}
|
|
25
|
+
|
|
11
26
|
class Server extends axon.Socket {
|
|
12
|
-
constructor() {
|
|
27
|
+
constructor(opts = {}) {
|
|
13
28
|
super();
|
|
14
|
-
|
|
15
|
-
|
|
29
|
+
|
|
30
|
+
this.jwt = opts?.jwt || JWT;
|
|
31
|
+
|
|
32
|
+
// 路由表:event -> { fn, authRequired }
|
|
33
|
+
this.handlers = new Map();
|
|
34
|
+
|
|
35
|
+
this.on('message', async (event, data, sock) => {
|
|
36
|
+
const route = this.handlers.get(event);
|
|
37
|
+
|
|
38
|
+
if (route) {
|
|
39
|
+
const { fn, authRequired } = route;
|
|
40
|
+
// 若该路由要求鉴权且当前连接未鉴权,则拒绝
|
|
41
|
+
if (authRequired && !sock.channel) {
|
|
42
|
+
console.error(`unauthenticated socket blocked for event "${event}"`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { reqId, payload } = normalizeReq(data);
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const ctx = { sock, channel: sock.channel, event, server: this };
|
|
50
|
+
const result = await fn(payload, ctx);
|
|
51
|
+
const res = normalizeRes(result);
|
|
52
|
+
const replyEvent = `${event}:res`;
|
|
53
|
+
|
|
54
|
+
// 已鉴权:按 channel 投递;未鉴权:仅回当前 socket
|
|
55
|
+
if (sock.channel) {
|
|
56
|
+
this.send(replyEvent, { reqId, ...res }, sock.channel);
|
|
57
|
+
} else {
|
|
58
|
+
const buf = this.pack([replyEvent, { reqId, ...res }]);
|
|
59
|
+
if (sock.writable) sock.write(buf);
|
|
60
|
+
}
|
|
61
|
+
} catch (err) {
|
|
62
|
+
const replyEvent = `${event}:res`;
|
|
63
|
+
const payloadErr = { reqId, ok: false, error: err?.message || String(err) };
|
|
64
|
+
if (sock.channel) {
|
|
65
|
+
this.send(replyEvent, payloadErr, sock.channel);
|
|
66
|
+
} else {
|
|
67
|
+
const buf = this.pack([replyEvent, payloadErr]);
|
|
68
|
+
if (sock.writable) sock.write(buf);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return; // 路由事件已消费,不再广播
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 非路由事件:保持原有逻辑(必须已鉴权,否则跳过)
|
|
16
75
|
if (!sock.channel) {
|
|
17
76
|
console.error('skip message of unauthenticated socket');
|
|
18
77
|
return;
|
|
19
78
|
}
|
|
20
|
-
this.send(event, data, channel);
|
|
79
|
+
this.send(event, data, sock.channel);
|
|
21
80
|
});
|
|
22
81
|
|
|
23
82
|
this.on('connect', (socket) => {
|
|
@@ -43,14 +102,11 @@ class Server extends axon.Socket {
|
|
|
43
102
|
const [event, data] = msg.args;
|
|
44
103
|
if (event === EVENT_AUTH) {
|
|
45
104
|
try {
|
|
46
|
-
// verify
|
|
47
105
|
const { pk, token } = data;
|
|
48
|
-
if (!
|
|
106
|
+
if (!this.jwt.verify(token, pk)) {
|
|
49
107
|
throw new Error('token verify failed');
|
|
50
108
|
}
|
|
51
|
-
|
|
52
|
-
// set channel as did
|
|
53
|
-
const did = getDid(JWT.decode(token));
|
|
109
|
+
const did = getDid(this.jwt.decode(token));
|
|
54
110
|
sock.channel = did;
|
|
55
111
|
} catch (err) {
|
|
56
112
|
console.error(err);
|
|
@@ -66,16 +122,25 @@ class Server extends axon.Socket {
|
|
|
66
122
|
|
|
67
123
|
send(event, data, channel) {
|
|
68
124
|
const { socks } = this;
|
|
69
|
-
|
|
70
125
|
const buf = this.pack([event, data]);
|
|
71
126
|
|
|
72
|
-
for (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (sock.writable) sock.write(buf);
|
|
127
|
+
for (const sock of socks) {
|
|
128
|
+
if (sock.channel === channel && sock.writable) {
|
|
129
|
+
sock.write(buf);
|
|
76
130
|
}
|
|
77
131
|
}
|
|
132
|
+
return this;
|
|
133
|
+
}
|
|
78
134
|
|
|
135
|
+
/**
|
|
136
|
+
* 注册路由
|
|
137
|
+
* @param {string} event
|
|
138
|
+
* @param {(payload:any, ctx:{sock:any, channel?:string, event:string, server:Server})=>any|Promise<any>} handler
|
|
139
|
+
* @param {{authRequired?: boolean}} [opts] - 默认 true;设为 false 则该事件免鉴权
|
|
140
|
+
*/
|
|
141
|
+
register(event, handler, opts = {}) {
|
|
142
|
+
const authRequired = opts.authRequired !== false; // 默认 true
|
|
143
|
+
this.handlers.set(event, { fn: handler, authRequired });
|
|
79
144
|
return this;
|
|
80
145
|
}
|
|
81
146
|
}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.22.1",
|
|
7
7
|
"description": "A nodejs module for local and remote Inter Process Event",
|
|
8
8
|
"main": "lib/index.js",
|
|
9
9
|
"files": [
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"amp-message": "~0.1.2",
|
|
21
21
|
"axon": "^2.0.3",
|
|
22
|
-
"@arcblock/
|
|
23
|
-
"@
|
|
24
|
-
"@
|
|
22
|
+
"@arcblock/did": "1.22.1",
|
|
23
|
+
"@arcblock/jwt": "1.22.1",
|
|
24
|
+
"@ocap/wallet": "1.22.1"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"jest": "^29.7.0"
|