@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.
Files changed (3) hide show
  1. package/lib/rpc.js +121 -0
  2. package/lib/server.js +81 -16
  3. 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
- this.on('message', (event, data, sock) => {
15
- const { channel } = sock;
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 (!JWT.verify(token, pk)) {
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 (let i = 0; i < socks.length; i++) {
73
- const sock = socks[i];
74
- if (sock.channel === channel) {
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.21.3",
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/jwt": "1.21.3",
23
- "@ocap/wallet": "1.21.3",
24
- "@arcblock/did": "1.21.3"
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"