@logux/server 0.12.2 → 0.12.4

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/server/index.js CHANGED
@@ -1,80 +1,60 @@
1
+ import glob from 'fast-glob'
1
2
  import { join, relative } from 'path'
2
3
  import pico from 'picocolors'
3
- import glob from 'fast-glob'
4
4
 
5
- import { createReporter } from '../create-reporter/index.js'
6
5
  import { BaseServer } from '../base-server/index.js'
7
- import { loadOptions, oneOf, number } from '../options-loader/index.js'
6
+ import { createReporter } from '../create-reporter/index.js'
7
+ import { loadOptions, number, oneOf } from '../options-loader/index.js'
8
8
 
9
9
  let cliOptionsSpec = {
10
+ envPrefix: 'LOGUX',
11
+ examples: [
12
+ '$0 --port 31337 --host 127.0.0.1',
13
+ 'LOGUX_PORT=1337 LOGUX_HOST=127.0.0.1 $0'
14
+ ],
10
15
  options: {
16
+ backend: {
17
+ description: 'Backend to process actions'
18
+ },
19
+ cert: {
20
+ description: 'Path to SSL certificate'
21
+ },
22
+ controlMask: {
23
+ description: 'CIDR masks of control servers'
24
+ },
25
+ controlSecret: {
26
+ description: 'Secret to control Logux server'
27
+ },
11
28
  host: {
12
29
  alias: 'h',
13
30
  description: 'Host to bind server'
14
31
  },
15
- port: {
16
- alias: 'p',
17
- description: 'Port to bind server',
18
- parse: number
19
- },
20
32
  key: {
21
33
  description: 'Path to SSL key'
22
34
  },
23
- cert: {
24
- description: 'Path to SSL certificate'
25
- },
26
- supports: {
27
- description: 'Range of supported client subprotocols'
28
- },
29
- subprotocol: {
30
- description: 'Server subprotocol'
31
- },
32
35
  logger: {
33
36
  alias: 'l',
34
37
  description: 'Logger type',
35
38
  parse: value => oneOf(['human', 'json'], value)
36
39
  },
37
- backend: {
38
- description: 'Backend to process actions'
39
- },
40
- controlSecret: {
41
- description: 'Secret to control Logux server'
42
- },
43
- controlMask: {
44
- description: 'CIDR masks of control servers'
40
+ port: {
41
+ alias: 'p',
42
+ description: 'Port to bind server',
43
+ parse: number
45
44
  },
46
45
  redis: {
47
46
  description: 'Redis URL for Logux Server Pro scaling'
47
+ },
48
+ subprotocol: {
49
+ description: 'Server subprotocol'
50
+ },
51
+ supports: {
52
+ description: 'Range of supported client subprotocols'
48
53
  }
49
- },
50
- envPrefix: 'LOGUX',
51
- examples: [
52
- '$0 --port 31337 --host 127.0.0.1',
53
- 'LOGUX_PORT=1337 LOGUX_HOST=127.0.0.1 $0'
54
- ]
54
+ }
55
55
  }
56
56
 
57
57
  export class Server extends BaseServer {
58
- static loadOptions(process, defaults) {
59
- let [help, options] = loadOptions(
60
- cliOptionsSpec,
61
- process,
62
- defaults.root ? { path: join(defaults.root, '.env') } : undefined
63
- )
64
- if (help) {
65
- process.stdout.write(help + '\n')
66
- return process.exit(0)
67
- }
68
- try {
69
- return Object.assign(defaults, options)
70
- } catch (e) {
71
- process.stderr.write(
72
- `${pico.bgRed(pico.black(' FATAL '))} ${e.message}\n`
73
- )
74
- return process.exit(1)
75
- }
76
- }
77
-
78
58
  constructor(opts = {}) {
79
59
  if (!opts.logger) {
80
60
  opts.logger = 'human'
@@ -122,11 +102,22 @@ export class Server extends BaseServer {
122
102
  })
123
103
  }
124
104
 
125
- async listen(...args) {
105
+ static loadOptions(process, defaults) {
106
+ let [help, options] = loadOptions(
107
+ cliOptionsSpec,
108
+ process,
109
+ defaults.root ? { path: join(defaults.root, '.env') } : undefined
110
+ )
111
+ if (help) {
112
+ process.stdout.write(help + '\n')
113
+ return process.exit(0)
114
+ }
126
115
  try {
127
- return BaseServer.prototype.listen.apply(this, args)
128
- } catch (err) {
129
- this.emitter.emit('report', 'error', { err })
116
+ return Object.assign(defaults, options)
117
+ } catch (e) {
118
+ process.stderr.write(
119
+ `${pico.bgRed(pico.black(' FATAL '))} ${e.message}\n`
120
+ )
130
121
  return process.exit(1)
131
122
  }
132
123
  }
@@ -135,8 +126,8 @@ export class Server extends BaseServer {
135
126
  files = ['modules/*/index.js', 'modules/*.js', '!**/*.{test,spec}.js']
136
127
  ) {
137
128
  let matches = await glob(files, {
138
- cwd: this.options.root,
139
129
  absolute: true,
130
+ cwd: this.options.root,
140
131
  onlyFiles: true
141
132
  })
142
133
 
@@ -158,4 +149,13 @@ export class Server extends BaseServer {
158
149
  })
159
150
  )
160
151
  }
152
+
153
+ async listen(...args) {
154
+ try {
155
+ return BaseServer.prototype.listen.apply(this, args)
156
+ } catch (err) {
157
+ this.emitter.emit('report', 'error', { err })
158
+ return process.exit(1)
159
+ }
160
+ }
161
161
  }
@@ -1,6 +1,6 @@
1
- import { ServerConnection, ServerNode } from '@logux/core'
1
+ import type { ServerConnection, ServerNode } from '@logux/core'
2
2
 
3
- import { BaseServer } from '../base-server/index.js'
3
+ import type { BaseServer } from '../base-server/index.js'
4
4
 
5
5
  /**
6
6
  * Logux client connected to server.
@@ -10,24 +10,11 @@ import { BaseServer } from '../base-server/index.js'
10
10
  * ```
11
11
  */
12
12
  export class ServerClient {
13
- /**
14
- * @param app The server.
15
- * @param connection The Logux connection.
16
- * @param key Client number used as `app.connected` key.
17
- */
18
- constructor(app: BaseServer, connection: ServerConnection, key: number)
19
-
20
13
  /**
21
14
  * Server, which received client.
22
15
  */
23
16
  app: BaseServer
24
17
 
25
- /**
26
- * User ID. It will be filled from client’s node ID.
27
- * It will be undefined before correct authentication.
28
- */
29
- userId?: string
30
-
31
18
  /**
32
19
  * Unique persistence machine ID.
33
20
  * It will be undefined before correct authentication.
@@ -35,28 +22,22 @@ export class ServerClient {
35
22
  clientId?: string
36
23
 
37
24
  /**
38
- * Unique node ID.
39
- * It will be undefined before correct authentication.
40
- */
41
- nodeId?: string
42
-
43
- /**
44
- * Does server process some action from client.
25
+ * The Logux wrapper to WebSocket connection.
45
26
  *
46
27
  * ```js
47
- * console.log('Clients in processing:', clients.map(i => i.processing))
28
+ * console.log(client.connection.ws.upgradeReq.headers)
48
29
  * ```
49
30
  */
50
- processing: boolean
31
+ connection: ServerConnection
51
32
 
52
33
  /**
53
- * The Logux wrapper to WebSocket connection.
34
+ * HTTP headers of WS connection.
54
35
  *
55
36
  * ```js
56
- * console.log(client.connection.ws.upgradeReq.headers)
37
+ * client.httpHeaders['User-Agent']
57
38
  * ```
58
39
  */
59
- connection: ServerConnection
40
+ httpHeaders: { [name: string]: string }
60
41
 
61
42
  /**
62
43
  * Client number used as `app.connected` key.
@@ -70,31 +51,55 @@ export class ServerClient {
70
51
  key: string
71
52
 
72
53
  /**
73
- * Client IP address.
54
+ * Node instance to synchronize logs.
74
55
  *
75
56
  * ```js
76
- * const clientCity = detectLocation(client.remoteAddress)
57
+ * if (client.node.state === 'synchronized')
77
58
  * ```
78
59
  */
79
- remoteAddress: string
60
+ node: ServerNode
80
61
 
81
62
  /**
82
- * HTTP headers of WS connection.
63
+ * Unique node ID.
64
+ * It will be undefined before correct authentication.
65
+ */
66
+ nodeId?: string
67
+
68
+ /**
69
+ * Does server process some action from client.
83
70
  *
84
71
  * ```js
85
- * client.httpHeaders['User-Agent']
72
+ * console.log('Clients in processing:', clients.map(i => i.processing))
86
73
  * ```
87
74
  */
88
- httpHeaders: { [name: string]: string }
75
+ processing: boolean
89
76
 
90
77
  /**
91
- * Node instance to synchronize logs.
78
+ * Client IP address.
92
79
  *
93
80
  * ```js
94
- * if (client.node.state === 'synchronized')
81
+ * const clientCity = detectLocation(client.remoteAddress)
95
82
  * ```
96
83
  */
97
- node: ServerNode
84
+ remoteAddress: string
85
+
86
+ /**
87
+ * User ID. It will be filled from client’s node ID.
88
+ * It will be undefined before correct authentication.
89
+ */
90
+ userId?: string
91
+
92
+ /**
93
+ * @param app The server.
94
+ * @param connection The Logux connection.
95
+ * @param key Client number used as `app.connected` key.
96
+ */
97
+ constructor(app: BaseServer, connection: ServerConnection, key: number)
98
+
99
+ /**
100
+ * Disconnect client.
101
+ */
102
+ destroy(): void
98
103
 
99
104
  /**
100
105
  * Check client subprotocol version. It uses `semver` npm package
@@ -110,9 +115,4 @@ export class ServerClient {
110
115
  * @returns Is version satisfies requirements.
111
116
  */
112
117
  isSubprotocol(range: string): boolean
113
-
114
- /**
115
- * Disconnect client.
116
- */
117
- destroy(): void
118
118
  }
@@ -2,15 +2,15 @@ import { LoguxError, parseId } from '@logux/core'
2
2
  import cookie from 'cookie'
3
3
  import semver from 'semver'
4
4
 
5
- import { FilteredNode } from '../filtered-node/index.js'
6
5
  import { ALLOWED_META } from '../allowed-meta/index.js'
7
6
  import { filterMeta } from '../filter-meta/index.js'
7
+ import { FilteredNode } from '../filtered-node/index.js'
8
8
 
9
9
  function reportDetails(client) {
10
10
  return {
11
11
  connectionId: client.key,
12
- subprotocol: client.node.remoteSubprotocol,
13
- nodeId: client.nodeId
12
+ nodeId: client.nodeId,
13
+ subprotocol: client.node.remoteSubprotocol
14
14
  }
15
15
  }
16
16
 
@@ -32,13 +32,13 @@ export class ServerClient {
32
32
  }
33
33
 
34
34
  this.node = new FilteredNode(this, app.nodeId, app.log, connection, {
35
- subprotocol: app.options.subprotocol,
35
+ auth: this.auth.bind(this),
36
36
  inFilter: this.filter.bind(this),
37
- timeout: app.options.timeout,
38
- outMap: this.outMap.bind(this),
39
37
  inMap: this.inMap.bind(this),
38
+ outMap: this.outMap.bind(this),
40
39
  ping: app.options.ping,
41
- auth: this.auth.bind(this)
40
+ subprotocol: app.options.subprotocol,
41
+ timeout: app.options.timeout
42
42
  })
43
43
  if (this.app.env === 'development') {
44
44
  this.node.setLocalHeaders({ env: 'development' })
@@ -61,44 +61,6 @@ export class ServerClient {
61
61
  this.app.emitter.emit('connected', this)
62
62
  }
63
63
 
64
- isSubprotocol(range) {
65
- return semver.satisfies(this.node.remoteSubprotocol, range)
66
- }
67
-
68
- destroy() {
69
- this.destroyed = true
70
- this.node.destroy()
71
- if (this.userId) {
72
- let users = this.app.userIds.get(this.userId)
73
- if (users) {
74
- users = users.filter(i => i !== this)
75
- if (users.length === 0) {
76
- this.app.userIds.delete(this.userId)
77
- } else {
78
- this.app.userIds.set(this.userId, users)
79
- }
80
- }
81
- }
82
- if (this.clientId) {
83
- this.app.clientIds.delete(this.clientId)
84
- this.app.nodeIds.delete(this.nodeId)
85
-
86
- for (let channel in this.app.subscribers) {
87
- let subscriber = this.app.subscribers[channel][this.nodeId]
88
- if (subscriber) {
89
- let action = { type: 'logux/unsubscribe', channel }
90
- let actionId = this.app.log.generateId()
91
- let meta = { id: actionId, time: parseInt(actionId), reasons: [] }
92
- this.app.performUnsubscribe(this.nodeId, action, meta)
93
- }
94
- }
95
- }
96
- if (!this.app.destroying) {
97
- this.app.emitter.emit('disconnected', this)
98
- }
99
- this.app.connected.delete(this.key)
100
- }
101
-
102
64
  async auth(nodeId, token) {
103
65
  this.nodeId = nodeId
104
66
  let { clientId, userId } = parseId(nodeId)
@@ -129,11 +91,11 @@ export class ServerClient {
129
91
  let result
130
92
  try {
131
93
  result = await this.app.authenticator({
132
- headers: this.node.remoteHeaders,
133
- cookie: cookie.parse(headers.cookie || ''),
134
- userId: this.userId,
135
94
  client: this,
136
- token
95
+ cookie: cookie.parse(headers.cookie || ''),
96
+ headers: this.node.remoteHeaders,
97
+ token,
98
+ userId: this.userId
137
99
  })
138
100
  } catch (e) {
139
101
  if (e.name === 'LoguxError') {
@@ -177,15 +139,46 @@ export class ServerClient {
177
139
  return result
178
140
  }
179
141
 
180
- async outMap(action, meta) {
181
- return [action, filterMeta(meta)]
142
+ denyBack(action, meta) {
143
+ this.app.emitter.emit('report', 'denied', { actionId: meta.id })
144
+ let [undoAction, undoMeta] = this.app.buildUndo(action, meta, 'denied')
145
+ undoMeta.clients = (undoMeta.clients || []).concat([this.clientId])
146
+ this.app.log.add(undoAction, undoMeta)
147
+ this.app.debugActionError(meta, `Action "${meta.id}" was denied`)
182
148
  }
183
149
 
184
- async inMap(action, meta) {
185
- if (!meta.subprotocol) {
186
- meta.subprotocol = this.node.remoteSubprotocol
150
+ destroy() {
151
+ this.destroyed = true
152
+ this.node.destroy()
153
+ if (this.userId) {
154
+ let users = this.app.userIds.get(this.userId)
155
+ if (users) {
156
+ users = users.filter(i => i !== this)
157
+ if (users.length === 0) {
158
+ this.app.userIds.delete(this.userId)
159
+ } else {
160
+ this.app.userIds.set(this.userId, users)
161
+ }
162
+ }
187
163
  }
188
- return [action, meta]
164
+ if (this.clientId) {
165
+ this.app.clientIds.delete(this.clientId)
166
+ this.app.nodeIds.delete(this.nodeId)
167
+
168
+ for (let channel in this.app.subscribers) {
169
+ let subscriber = this.app.subscribers[channel][this.nodeId]
170
+ if (subscriber) {
171
+ let action = { channel, type: 'logux/unsubscribe' }
172
+ let actionId = this.app.log.generateId()
173
+ let meta = { id: actionId, reasons: [], time: parseInt(actionId) }
174
+ this.app.performUnsubscribe(this.nodeId, action, meta)
175
+ }
176
+ }
177
+ }
178
+ if (!this.app.destroying) {
179
+ this.app.emitter.emit('disconnected', this)
180
+ }
181
+ this.app.connected.delete(this.key)
189
182
  }
190
183
 
191
184
  async filter(action, meta) {
@@ -231,11 +224,18 @@ export class ServerClient {
231
224
  }
232
225
  }
233
226
 
234
- denyBack(action, meta) {
235
- this.app.emitter.emit('report', 'denied', { actionId: meta.id })
236
- let [undoAction, undoMeta] = this.app.buildUndo(action, meta, 'denied')
237
- undoMeta.clients = (undoMeta.clients || []).concat([this.clientId])
238
- this.app.log.add(undoAction, undoMeta)
239
- this.app.debugActionError(meta, `Action "${meta.id}" was denied`)
227
+ async inMap(action, meta) {
228
+ if (!meta.subprotocol) {
229
+ meta.subprotocol = this.node.remoteSubprotocol
230
+ }
231
+ return [action, meta]
232
+ }
233
+
234
+ isSubprotocol(range) {
235
+ return semver.satisfies(this.node.remoteSubprotocol, range)
236
+ }
237
+
238
+ async outMap(action, meta) {
239
+ return [action, filterMeta(meta)]
240
240
  }
241
241
  }
@@ -1,18 +1,18 @@
1
- import { LoguxSubscribeAction, LoguxUnsubscribeAction } from '@logux/actions'
2
- import { Action, AnyAction, TestLog, TestPair } from '@logux/core'
1
+ import type { LoguxSubscribeAction, LoguxUnsubscribeAction } from '@logux/actions'
2
+ import type { Action, AnyAction, TestLog, TestPair } from '@logux/core'
3
3
 
4
- import { ServerMeta } from '../base-server/index.js'
5
- import { TestServer } from '../test-server/index.js'
4
+ import type { ServerMeta } from '../base-server/index.js'
5
+ import type { TestServer } from '../test-server/index.js'
6
6
 
7
7
  export class LoguxActionError extends Error {
8
8
  action: Action
9
9
  }
10
10
 
11
11
  export interface TestClientOptions {
12
+ cookie?: object
13
+ headers?: object
12
14
  httpHeaders?: { [key: string]: string }
13
15
  subprotocol?: string
14
- headers?: object
15
- cookie?: object
16
16
  token?: string
17
17
  }
18
18
 
@@ -59,11 +59,14 @@ export interface TestClientOptions {
59
59
  */
60
60
  export class TestClient {
61
61
  /**
62
- * @param server Test server.
63
- * @param userId User ID.
64
- * @param opts Other options.
62
+ * Client’s ID.
63
+ *
64
+ * ```js
65
+ * let client = new TestClient(server, '10')
66
+ * client.clientId //=> '10:1'
67
+ * ```
65
68
  */
66
- constructor(server: TestServer, userId: string, opts?: TestClientOptions)
69
+ clientId: string
67
70
 
68
71
  /**
69
72
  * Client’s log with extra methods to check actions inside.
@@ -85,14 +88,13 @@ export class TestClient {
85
88
  nodeId: string
86
89
 
87
90
  /**
88
- * Client’s ID.
91
+ * Connection channel between client and server to track sent messages.
89
92
  *
90
93
  * ```js
91
- * let client = new TestClient(server, '10')
92
- * client.clientId //=> '10:1'
94
+ * console.log(client.pair.leftSent)
93
95
  * ```
94
96
  */
95
- clientId: string
97
+ pair: TestPair
96
98
 
97
99
  /**
98
100
  * User ID.
@@ -105,13 +107,27 @@ export class TestClient {
105
107
  userId: string
106
108
 
107
109
  /**
108
- * Connection channel between client and server to track sent messages.
110
+ * @param server Test server.
111
+ * @param userId User ID.
112
+ * @param opts Other options.
113
+ */
114
+ constructor(server: TestServer, userId: string, opts?: TestClientOptions)
115
+
116
+ /**
117
+ * Collect actions added by server and other clients during the `test` call.
109
118
  *
110
119
  * ```js
111
- * console.log(client.pair.leftSent)
120
+ * let answers = await client.collect(async () => {
121
+ * client.log.add({ type: 'pay' })
122
+ * await delay(10)
123
+ * })
124
+ * expect(actions).toEqual([{ type: 'paid' }])
112
125
  * ```
126
+ *
127
+ * @param test Function, where do you expect action will be received
128
+ * @returns Promise with all received actions
113
129
  */
114
- pair: TestPair
130
+ collect(test: () => Promise<any>): Promise<Action[]>
115
131
 
116
132
  /**
117
133
  * Connect to test server.
@@ -137,22 +153,6 @@ export class TestClient {
137
153
  */
138
154
  disconnect(): Promise<void>
139
155
 
140
- /**
141
- * Collect actions added by server and other clients during the `test` call.
142
- *
143
- * ```js
144
- * let answers = await client.collect(async () => {
145
- * client.log.add({ type: 'pay' })
146
- * await delay(10)
147
- * })
148
- * expect(actions).toEqual([{ type: 'paid' }])
149
- * ```
150
- *
151
- * @param test Function, where do you expect action will be received
152
- * @returns Promise with all received actions
153
- */
154
- collect(test: () => Promise<any>): Promise<Action[]>
155
-
156
156
  /**
157
157
  * Send action to the sever and collect all response actions.
158
158
  *
@@ -168,6 +168,21 @@ export class TestClient {
168
168
  */
169
169
  process(action: AnyAction, meta?: Partial<ServerMeta>): Promise<Action[]>
170
170
 
171
+ /**
172
+ * Collect actions received from server during the `test` call.
173
+ *
174
+ * ```js
175
+ * let answers = await client1.received(async () => {
176
+ * await client2.process({ type: 'resend' })
177
+ * })
178
+ * expect(actions).toEqual([{ type: 'local' }])
179
+ * ```
180
+ *
181
+ * @param test Function, where do you expect action will be received
182
+ * @returns Promise with all received actions
183
+ */
184
+ received(test: () => any | Promise<any>): Promise<Action[]>
185
+
171
186
  /**
172
187
  * Subscribe to the channel and collect all actions dunring the subscription.
173
188
  *
@@ -184,7 +199,7 @@ export class TestClient {
184
199
  * @returns Promise with all actions from the server.
185
200
  */
186
201
  subscribe(
187
- channel: string | LoguxSubscribeAction,
202
+ channel: LoguxSubscribeAction | string,
188
203
  filter?: object,
189
204
  since?: { id: string; time: number }
190
205
  ): Promise<Action[]>
@@ -201,22 +216,7 @@ export class TestClient {
201
216
  * @returns Promise until server will remove client from subscribers.
202
217
  */
203
218
  unsubscribe(
204
- channel: string | LoguxUnsubscribeAction,
219
+ channel: LoguxUnsubscribeAction | string,
205
220
  filter?: object
206
221
  ): Promise<Action[]>
207
-
208
- /**
209
- * Collect actions received from server during the `test` call.
210
- *
211
- * ```js
212
- * let answers = await client1.received(async () => {
213
- * await client2.process({ type: 'resend' })
214
- * })
215
- * expect(actions).toEqual([{ type: 'local' }])
216
- * ```
217
- *
218
- * @param test Function, where do you expect action will be received
219
- * @returns Promise with all received actions
220
- */
221
- received(test: () => Promise<any> | any): Promise<Action[]>
222
222
  }