oox 0.3.4 → 0.3.6

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 (42) hide show
  1. package/README.md +13 -0
  2. package/app.js +1 -7
  3. package/bin/cli.js +45 -12
  4. package/bin/configurer.js +6 -9
  5. package/bin/proxy-require.js +1 -23
  6. package/bin/register.js +48 -15
  7. package/bin/starter.js +8 -0
  8. package/config.js +145 -0
  9. package/context.js +58 -0
  10. package/index.js +14 -133
  11. package/keepalive-connection.js +73 -0
  12. package/logger.js +2 -1
  13. package/modules/http/index.js +52 -63
  14. package/modules/http/router.js +9 -4
  15. package/modules/index.js +78 -74
  16. package/modules/module.js +1 -10
  17. package/modules/socketio/adapter.js +24 -13
  18. package/modules/socketio/server.js +37 -16
  19. package/modules/socketio/utils.js +12 -7
  20. package/package.json +13 -6
  21. package/proxy.js +30 -0
  22. package/registry.js +91 -7
  23. package/samples/index.js +1 -1
  24. package/samples/keepalive-connection-sample.js +62 -41
  25. package/types/app.d.ts +18 -24
  26. package/types/bin/configurer.d.ts +3 -1
  27. package/types/bin/register.d.ts +2 -1
  28. package/types/config.d.ts +66 -0
  29. package/types/context.d.ts +30 -0
  30. package/types/index.d.ts +10 -53
  31. package/types/keepalive-connection.d.ts +52 -16
  32. package/types/modules/http/index.d.ts +5 -8
  33. package/types/modules/index.d.ts +37 -22
  34. package/types/modules/module.d.ts +9 -9
  35. package/types/modules/socketio/adapter.d.ts +9 -4
  36. package/types/modules/socketio/server.d.ts +6 -4
  37. package/types/modules/socketio/utils.d.ts +12 -6
  38. package/types/proxy.d.ts +1 -0
  39. package/types/registry.d.ts +28 -3
  40. package/types/samples/index.d.ts +1 -1
  41. package/types/samples/keepalive-connection-sample.d.ts +33 -23
  42. package/utils.js +2 -2
@@ -1,5 +1,15 @@
1
1
  import EventEmitter from 'node:events';
2
+ import { getContext } from './context.js';
3
+ export class KeepAliveConnectionError extends Error {
4
+ connection;
5
+ constructor(message, connection) {
6
+ super(message);
7
+ this.connection = connection;
8
+ }
9
+ }
2
10
  export class KeepAliveConnection extends EventEmitter {
11
+ // 是否为注册服务连接
12
+ isRegistry = false;
3
13
  // 连接是否可用
4
14
  #enabled = false;
5
15
  data;
@@ -49,6 +59,10 @@ export class KeepAliveConnection extends EventEmitter {
49
59
  keepAliveConnections.add(this);
50
60
  this.enabled = true;
51
61
  }
62
+ disconnect() {
63
+ this.enabled = false;
64
+ this.nativeConnection.disconnect();
65
+ }
52
66
  }
53
67
  export class KeepAliveConnectionStore {
54
68
  #map = new Map();
@@ -91,6 +105,65 @@ export class KeepAliveConnectionStore {
91
105
  }
92
106
  }
93
107
  }
108
+ /**
109
+ * 添加长连接
110
+ * @throws {KeepAliveConnectionError} 连接已存在
111
+ * @param connection
112
+ */
113
+ export function addKeepAliveConnection(connection) {
114
+ if (keepAliveConnections.has(connection.data.name, connection.data.id)) {
115
+ connection.disconnect();
116
+ throw new KeepAliveConnectionError('Connection already exists', connection);
117
+ }
118
+ keepAliveConnections.add(connection);
119
+ }
120
+ export function removeKeepAliveConnection(arg1, arg2) {
121
+ if (typeof arg1 === 'string') {
122
+ const connection = keepAliveConnections.get(arg1, arg2);
123
+ if (connection)
124
+ connection.disconnect();
125
+ keepAliveConnections.remove(arg1, arg2);
126
+ enabledKeepAliveConnections.remove(arg1, arg2);
127
+ }
128
+ else {
129
+ arg1.disconnect();
130
+ keepAliveConnections.remove(arg1);
131
+ enabledKeepAliveConnections.remove(arg1);
132
+ }
133
+ }
134
+ /**
135
+ * random connection select for default load balance policy
136
+ * @param name service name
137
+ * @returns selected connection
138
+ */
139
+ let loadBalancePolicy = (name) => {
140
+ const connections = enabledKeepAliveConnections.getConnectionsOfService(name);
141
+ if (!connections || !connections.size)
142
+ return null;
143
+ const arrayConnections = Array.from(connections.values());
144
+ const index = Math.floor(Math.random() * arrayConnections.length);
145
+ return arrayConnections[index];
146
+ };
147
+ export function setLoadBalancePolicy(policy) {
148
+ loadBalancePolicy = policy;
149
+ }
150
+ export async function rpc(arg1, action, params, context) {
151
+ if (!context || !context.traceId) {
152
+ context = getContext();
153
+ }
154
+ let connection = null;
155
+ if (arg1 instanceof KeepAliveConnection) {
156
+ connection = arg1;
157
+ }
158
+ else if ('string' === typeof arg1) {
159
+ connection = loadBalancePolicy(arg1);
160
+ }
161
+ else
162
+ throw new Error(`Unknown rpc arg1<${arg1}>`);
163
+ if (!connection)
164
+ throw new Error(`Connection<${arg1}> not found`);
165
+ return connection.adapter.rpc(connection, action, params, context);
166
+ }
94
167
  export const keepAliveConnectionAdapters = new Map();
95
168
  export const keepAliveConnections = new KeepAliveConnectionStore();
96
169
  export const enabledKeepAliveConnections = new KeepAliveConnectionStore();
package/logger.js CHANGED
@@ -1,4 +1,5 @@
1
- import { emit, asyncStore } from './app.js';
1
+ import { emit } from './app.js';
2
+ import { asyncStore } from './context.js';
2
3
  export function info(...content) {
3
4
  const context = asyncStore.getStore() || null;
4
5
  emit('log', Date.now(), context, 'info', content);
@@ -1,10 +1,11 @@
1
1
  import * as http from 'node:http';
2
2
  import * as https from 'node:https';
3
- import { httpRequest, parseHTTPBody } from './utils.js';
3
+ import { parseHTTPBody } from './utils.js';
4
4
  import Router from './router.js';
5
5
  import * as oox from '../../index.js';
6
- import Module, { ModuleConfig } from '../module.js';
7
- export class HTTPConfig extends ModuleConfig {
6
+ import { Module } from '../module.js';
7
+ export class HTTPConfig {
8
+ enabled = true;
8
9
  // listen port
9
10
  port = 0;
10
11
  // service path
@@ -80,9 +81,9 @@ export default class HTTPModule extends Module {
80
81
  if (ssl.enabled) {
81
82
  const fs = await import('node:fs');
82
83
  const options = {
83
- key: ssl.key ? fs.readFileSync(ssl.key) : null,
84
- cert: ssl.cert ? fs.readFileSync(ssl.cert) : null,
85
- ca: ssl.ca ? fs.readFileSync(ssl.ca) : null
84
+ key: ssl.key ? fs.readFileSync(ssl.key) : undefined,
85
+ cert: ssl.cert ? fs.readFileSync(ssl.cert) : undefined,
86
+ ca: ssl.ca ? fs.readFileSync(ssl.ca) : undefined
86
87
  };
87
88
  if (!options.key || !options.cert) {
88
89
  throw new Error('HTTPS enabled but missing key or cert');
@@ -101,16 +102,18 @@ export default class HTTPModule extends Module {
101
102
  /**
102
103
  * stop http service
103
104
  */
104
- stop() {
105
- if (this.server && this.server.listening)
106
- return new Promise((resolve, reject) => {
107
- this.server.close(function (error) {
108
- if (error)
109
- reject(error);
110
- else
111
- resolve();
112
- });
105
+ async stop() {
106
+ const { server } = this;
107
+ if (!server || !server.listening)
108
+ return Promise.resolve();
109
+ return new Promise((resolve, reject) => {
110
+ server.close(function (error) {
111
+ if (error)
112
+ reject(error);
113
+ else
114
+ resolve();
113
115
  });
116
+ });
114
117
  }
115
118
  /**
116
119
  * browser cross origin
@@ -120,9 +123,14 @@ export default class HTTPModule extends Module {
120
123
  const origin = this.config.origin;
121
124
  const requestOrigin = request.headers.origin;
122
125
  if (origin && requestOrigin) {
123
- if (origin === '*' ||
124
- origin === requestOrigin ||
125
- (Array.isArray(origin) && origin.includes(requestOrigin))) {
126
+ let allow = false;
127
+ if ('string' === typeof origin) {
128
+ allow = origin === '*' || new RegExp(origin).test(requestOrigin);
129
+ }
130
+ else if (Array.isArray(origin)) {
131
+ allow = origin.some(item => item === '*' || new RegExp(item).test(requestOrigin));
132
+ }
133
+ if (allow) {
126
134
  response.setHeader('Access-Control-Allow-Origin', requestOrigin);
127
135
  response.setHeader('Vary', 'Origin');
128
136
  }
@@ -132,7 +140,7 @@ export default class HTTPModule extends Module {
132
140
  return false;
133
141
  }
134
142
  response.setHeader('Access-Control-Max-Age', 3600);
135
- response.setHeader('Access-Control-Allow-Headers', 'x-caller,content-type');
143
+ response.setHeader('Access-Control-Allow-Headers', 'x-caller,x-token,content-type');
136
144
  response.setHeader('Access-Control-Allow-Methods', '*');
137
145
  }
138
146
  if (request.method === 'OPTIONS') {
@@ -143,9 +151,7 @@ export default class HTTPModule extends Module {
143
151
  return true;
144
152
  }
145
153
  async requestHandler(request, response) {
146
- if (!this.cors(request, response))
147
- return;
148
- const url = new URL(request.url, `http://${request.headers.host || 'localhost'}`);
154
+ const url = new URL(request.url || '', `http://${request.headers.host || 'localhost'}`);
149
155
  const method = request.method || 'GET';
150
156
  // 尝试匹配路由
151
157
  const matched = this.router.match(method, url.pathname);
@@ -170,6 +176,9 @@ export default class HTTPModule extends Module {
170
176
  await this.router.handleRoute(matched.route, matched.params, req, res);
171
177
  return;
172
178
  }
179
+ // 内置cors仅处理RPC请求
180
+ if (!this.cors(request, response))
181
+ return;
173
182
  // 回退到RPC调用
174
183
  if (url.pathname === this.config.path) {
175
184
  await this.call(request, response);
@@ -199,6 +208,24 @@ export default class HTTPModule extends Module {
199
208
  * HTTP-RPC服务器请求监听方法
200
209
  */
201
210
  async call(request, response) {
211
+ // global unique id
212
+ const traceId = String(request.headers['x-trace-id'] || '');
213
+ // service name, required
214
+ const caller = String(request.headers['x-caller'] || 'anonymous');
215
+ // client ip or caller service ip
216
+ const ip = String(request.headers['x-real-ip'] || request.socket.remoteAddress || '');
217
+ // startup client ip
218
+ const sourceIP = String(request.headers['x-source-ip'] || request.socket.remoteAddress || '');
219
+ // check token
220
+ const token = String(request.headers['x-token'] || '');
221
+ const { allow, reason } = oox.checkAllow(ip, caller, token);
222
+ if (!allow)
223
+ return this.respond(request, response, {
224
+ success: false,
225
+ error: {
226
+ message: reason
227
+ }
228
+ });
202
229
  let callArgs = Object.create(null);
203
230
  try {
204
231
  callArgs = await this.getCallArgsFromRequest(request);
@@ -212,14 +239,6 @@ export default class HTTPModule extends Module {
212
239
  }
213
240
  });
214
241
  }
215
- // global unique id
216
- const traceId = String(request.headers['x-trace-id'] || '');
217
- // service name, required
218
- const caller = String(request.headers['x-caller'] || 'anonymous');
219
- // client ip or caller service ip
220
- const ip = String(request.headers['x-ip'] || request.socket.remoteAddress || '');
221
- // startup client ip
222
- const sourceIP = String(request.headers['x-real-ip'] || '');
223
242
  const { action, params = [] } = callArgs;
224
243
  const context = oox.genContext({ traceId, caller, sourceIP, ip, callerId: '' });
225
244
  const format = await oox.call(action, params, context);
@@ -237,15 +256,15 @@ export default class HTTPModule extends Module {
237
256
  try {
238
257
  returnsString = JSON.stringify(returns);
239
258
  }
240
- catch ({ message, stack }) {
259
+ catch (error) {
241
260
  delete returns.body;
242
261
  returns.success = false;
243
262
  returns.error = {
244
- message
263
+ message: error.message
245
264
  };
246
265
  if (oox.config.errorStack) {
247
266
  // 返回错误调用栈信息
248
- returns.error.stack = stack;
267
+ returns.error.stack = error.stack;
249
268
  }
250
269
  returnsString = JSON.stringify(returns);
251
270
  }
@@ -253,34 +272,4 @@ export default class HTTPModule extends Module {
253
272
  response.setHeader('Content-Length', Buffer.byteLength(returnsString));
254
273
  response.end(returnsString);
255
274
  }
256
- /**
257
- * HTTP RPC
258
- */
259
- async rpc(url, action, params, context) {
260
- if (!context || !context.traceId) {
261
- context = oox.getContext();
262
- }
263
- const { traceId, caller, sourceIP } = context;
264
- const headers = {
265
- 'Content-Type': 'application/json',
266
- 'x-trace-id': String(traceId),
267
- };
268
- if (caller)
269
- headers['x-caller'] = String(caller);
270
- if (sourceIP)
271
- headers['x-real-ip'] = sourceIP;
272
- // headers [ 'x-ip' ] = getIPAddress ( 4 ) [ 0 ]
273
- const format = await httpRequest(url, {
274
- headers
275
- }, JSON.stringify({ action, params }));
276
- if ('string' === typeof format)
277
- throw new Error(format);
278
- const { success, error, body } = format;
279
- if (success)
280
- return body;
281
- else if (error)
282
- throw new Error(error.message);
283
- else
284
- throw new Error('[RPC] Unknown Error');
285
- }
286
275
  }
@@ -84,12 +84,17 @@ export default class Router {
84
84
  }
85
85
  // 添加路由
86
86
  addRoute(method, path, handler, middleware = []) {
87
- if (!this.routes.has(method)) {
88
- this.routes.set(method, []);
89
- }
90
87
  // 编译路径为正则表达式,支持路径参数
91
88
  const { regex, paramNames } = this.compilePath(path);
92
- this.routes.get(method).push({
89
+ let routes = [];
90
+ if (this.routes.has(method)) {
91
+ routes = this.routes.get(method) || [];
92
+ }
93
+ else {
94
+ routes = [];
95
+ this.routes.set(method, routes);
96
+ }
97
+ routes.push({
93
98
  path,
94
99
  method,
95
100
  handler,
package/modules/index.js CHANGED
@@ -1,86 +1,90 @@
1
- import Module from './module.js';
2
1
  import HTTP from './http/index.js';
3
2
  import SocketIO from './socketio/index.js';
4
- export default class Modules extends Module {
5
- /**
6
- * the module unique name
7
- */
8
- name = 'oox:modules';
9
- /**
10
- * FIFO queue for modules starting
11
- */
12
- #queue = [];
13
- /**
14
- * all modules map
15
- */
16
- #map = new Map();
17
- /**
18
- * all builtin modules
19
- */
20
- builtins = {
21
- http: new HTTP,
22
- socketio: new SocketIO,
23
- };
24
- constructor() {
25
- super();
26
- this.add(this.builtins.http);
27
- this.add(this.builtins.socketio);
28
- }
29
- add(module) {
30
- if (!module.name || 'string' !== typeof module.name)
31
- throw new Error(`Module<${module.name}> has noname`);
32
- if (this.#map.has(module.name))
33
- throw new Error(`Module<${module.name}> has exists`);
34
- this.#map.set(module.name, module);
35
- this.#queue.push(module);
36
- return this;
37
- }
38
- get(name) {
39
- return this.#map.get(name);
40
- }
41
- async remove(name) {
42
- const module = this.get(name);
43
- if (!module)
44
- return;
45
- await module.stop();
46
- const index = this.#queue.indexOf(module);
47
- this.#queue.splice(index, 1);
48
- this.#map.delete(name);
49
- }
50
- setConfig(config) {
51
- for (const module of this.#queue) {
52
- if (module.name in config) {
53
- const moduleConfig = config[module.name];
54
- if ('boolean' === typeof moduleConfig) {
55
- // eg: http=yes/no
56
- module.setConfig({ enabled: moduleConfig });
57
- }
58
- else {
59
- module.setConfig(moduleConfig);
60
- }
3
+ /**
4
+ * FIFO queue for modules starting
5
+ */
6
+ export const queue = [];
7
+ /**
8
+ * all modules map
9
+ */
10
+ export const map = new Map();
11
+ /**
12
+ * add module to modules queue
13
+ */
14
+ export function add(module) {
15
+ if (!module.name || 'string' !== typeof module.name)
16
+ throw new Error(`Module<${module.name}> has noname`);
17
+ if (map.has(module.name))
18
+ throw new Error(`Module<${module.name}> has exists`);
19
+ map.set(module.name, module);
20
+ queue.push(module);
21
+ }
22
+ /**
23
+ * get module by name
24
+ */
25
+ export function get(name) {
26
+ return map.get(name) || null;
27
+ }
28
+ export async function remove(name) {
29
+ const module = map.get(name);
30
+ if (!module)
31
+ return;
32
+ await module.stop();
33
+ const index = queue.indexOf(module);
34
+ queue.splice(index, 1);
35
+ map.delete(name);
36
+ }
37
+ /**
38
+ * set modules config
39
+ */
40
+ export function setConfig(config) {
41
+ for (const module of queue) {
42
+ if (module.name in config) {
43
+ const moduleConfig = config[module.name];
44
+ if ('boolean' === typeof moduleConfig) {
45
+ // eg: http=yes/no
46
+ module.setConfig({ enabled: moduleConfig });
61
47
  }
62
48
  else {
63
- module.setConfig({ enabled: true });
49
+ module.setConfig(moduleConfig);
64
50
  }
65
- config[module.name] = module.getConfig();
66
51
  }
52
+ else {
53
+ module.setConfig({ enabled: true });
54
+ }
55
+ config[module.name] = module.getConfig();
67
56
  }
68
- async serve() {
69
- try {
70
- for (const module of this.#queue) {
71
- if (module.getConfig().enabled) {
72
- await module.serve();
73
- }
57
+ }
58
+ /**
59
+ * serve modules
60
+ */
61
+ export async function serve() {
62
+ try {
63
+ for (const module of queue) {
64
+ if (module.getConfig().enabled) {
65
+ await module.serve();
74
66
  }
75
67
  }
76
- catch (error) {
77
- await this.stop();
78
- throw error;
79
- }
80
68
  }
81
- async stop() {
82
- for (const module of this.#queue) {
83
- await module.stop();
84
- }
69
+ catch (error) {
70
+ await stop();
71
+ throw error;
72
+ }
73
+ }
74
+ /**
75
+ * stop all modules
76
+ */
77
+ export async function stop() {
78
+ for (const module of queue) {
79
+ await module.stop();
85
80
  }
86
81
  }
82
+ /**
83
+ * all builtin modules
84
+ */
85
+ export const builtins = {
86
+ http: new HTTP,
87
+ socketio: new SocketIO,
88
+ };
89
+ add(builtins.http);
90
+ add(builtins.socketio);
package/modules/module.js CHANGED
@@ -1,11 +1,2 @@
1
- export class ModuleConfig {
2
- enabled = true;
3
- }
4
- export default class Module {
5
- config;
6
- name;
7
- setConfig(config) { }
8
- getConfig() { return this.config; }
9
- async serve() { }
10
- async stop() { }
1
+ export class Module {
11
2
  }
@@ -6,32 +6,43 @@ import { SampleKeepAliveConnectionAdapter } from '../../samples/index.js';
6
6
  export default class SocketIOAdapter extends SampleKeepAliveConnectionAdapter {
7
7
  name = 'socketio';
8
8
  OOXEvent = OOXEvent;
9
- nativeEvent = { CONNECT: 'connect', DISCONNECT: 'disconnect', ERROR: 'connect_error' };
10
- newConnection(url) {
11
- const { id, host, name } = oox.config;
9
+ newConnection(identify) {
10
+ const { id, name } = oox.config;
12
11
  const headers = {
13
- 'x-ip': host,
14
12
  'x-caller': name,
15
13
  'x-caller-id': id,
16
14
  };
17
15
  let mURL;
18
- if ('string' === typeof url) {
19
- mURL = genWebSocketURL(url);
16
+ const connectionData = {
17
+ name: 'anonymous',
18
+ id: randomUUID(),
19
+ adapter: this.name,
20
+ ip: '',
21
+ token: ''
22
+ };
23
+ if ('string' === typeof identify) {
24
+ mURL = genWebSocketURL(identify);
25
+ }
26
+ else if (identify instanceof URL) {
27
+ mURL = identify;
28
+ }
29
+ else if (identify.url) {
30
+ // KeepAliveConnectionData
31
+ Object.assign(connectionData, identify);
32
+ mURL = new URL(identify.url);
33
+ if (identify.token) {
34
+ headers['x-token'] = identify.token;
35
+ }
20
36
  }
21
37
  else {
22
- mURL = url;
38
+ throw new Error('identify must be string, URL, or KeepAliveConnectionData');
23
39
  }
24
40
  const socket = SocketIOClient.io(mURL.origin, {
25
41
  extraHeaders: headers,
26
42
  path: mURL.pathname,
27
43
  autoConnect: false,
28
44
  });
29
- const connection = new oox.KeepAliveConnection(this, socket, {
30
- name: 'anonymous',
31
- id: randomUUID(),
32
- host: mURL.host,
33
- url: mURL
34
- });
45
+ const connection = new oox.KeepAliveConnection(this, socket, connectionData);
35
46
  return connection;
36
47
  }
37
48
  }