@logux/server 0.13.1 → 0.15.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.
@@ -1,20 +1,10 @@
1
- import type { AnyAction } from '@logux/core'
1
+ import type { Action, AnyAction } from '@logux/core'
2
2
 
3
3
  import type { ServerMeta } from '../base-server/index.js'
4
+ import type { ServerClient } from '../server-client/index.js'
4
5
  import type { Server } from '../server/index.js'
5
6
 
6
- /**
7
- * Action context.
8
- *
9
- * ```js
10
- * server.type('FOO', {
11
- * access (ctx, action, meta) {
12
- * return ctx.isSubprotocol('3.x') ? check3(action) : check4(action)
13
- * }
14
- * })
15
- * ```
16
- */
17
- export class Context<Data extends object = {}, Headers extends object = {}> {
7
+ export class ConnectContext<Headers extends object = unknown> {
18
8
  /**
19
9
  * Unique persistence client ID.
20
10
  *
@@ -24,23 +14,6 @@ export class Context<Data extends object = {}, Headers extends object = {}> {
24
14
  */
25
15
  clientId: string
26
16
 
27
- /**
28
- * Open structure to save some data between different steps of processing.
29
- *
30
- * ```js
31
- * server.type('RENAME', {
32
- * access (ctx, action, meta) {
33
- * ctx.data.user = findUser(ctx.userId)
34
- * return ctx.data.user.hasAccess(action.projectId)
35
- * }
36
- * process (ctx, action, meta) {
37
- * return ctx.data.user.rename(action.projectId, action.name)
38
- * }
39
- * })
40
- * ```
41
- */
42
- data: Data
43
-
44
17
  /**
45
18
  * Client’s headers.
46
19
  *
@@ -53,15 +26,6 @@ export class Context<Data extends object = {}, Headers extends object = {}> {
53
26
  */
54
27
  headers: Headers
55
28
 
56
- /**
57
- * Was action created by Logux server.
58
- *
59
- * ```js
60
- * access: (ctx, action, meta) => ctx.isServer
61
- * ```
62
- */
63
- isServer: boolean
64
-
65
29
  /**
66
30
  * Unique node ID.
67
31
  *
@@ -77,10 +41,9 @@ export class Context<Data extends object = {}, Headers extends object = {}> {
77
41
  server: Server
78
42
 
79
43
  /**
80
- * Action creator application subprotocol version in SemVer format.
81
- * Use {@link Context#isSubprotocol} to check it.
44
+ * Action creator application subprotocol version.
82
45
  */
83
- subprotocol: string
46
+ subprotocol: number
84
47
 
85
48
  /**
86
49
  * User ID taken node ID.
@@ -92,32 +55,27 @@ export class Context<Data extends object = {}, Headers extends object = {}> {
92
55
  * }
93
56
  * ```
94
57
  */
95
- userId: 'server' | string
58
+ userId: string
96
59
 
97
- /**
98
- * @param nodeId Unique node ID.
99
- * @param clientId Unique persistence client ID.
100
- * @param userId User ID taken node ID.
101
- * @param subprotocol Action creator application subprotocol version
102
- * in SemVer format.
103
- * @param server Logux server
104
- */
105
- constructor(server: Server, meta: ServerMeta)
60
+ constructor(server: Server, client: ServerClient)
106
61
 
107
62
  /**
108
- * Check creator subprotocol version. It uses `semver` npm package
109
- * to parse requirements.
63
+ * Wait until the client will confirm all actions, which were sent to it.
64
+ *
65
+ * Use it to send a long history without loading it all into the memory:
66
+ * the client’s speed will limit how fast you read the database.
110
67
  *
111
68
  * ```js
112
- * if (ctx.isSubprotocol('2.x')) {
113
- * useOldAPI()
69
+ * while (await ctx.drain()) {
70
+ * let page = await cursor.next(100)
71
+ * if (!page.length) break
72
+ * ctx.sendBack(page.map(i => i.action))
114
73
  * }
115
74
  * ```
116
75
  *
117
- * @param range npm’s version requirements.
118
- * @returns Is version satisfies requirements.
76
+ * @returns Promise with `false` if the client was disconnected.
119
77
  */
120
- isSubprotocol(range: string): boolean
78
+ drain(): Promise<boolean>
121
79
 
122
80
  /**
123
81
  * Send action back to the client.
@@ -126,13 +84,64 @@ export class Context<Data extends object = {}, Headers extends object = {}> {
126
84
  * ctx.sendBack({ type: 'login/success', token })
127
85
  * ```
128
86
  *
87
+ * An array of actions will be sent in a single message. Use it to send
88
+ * a big history page by page instead of a message per action.
89
+ *
90
+ * ```js
91
+ * ctx.sendBack(page.map(i => i.action))
92
+ * ```
93
+ *
94
+ * Every action in the array can have own meta as `[action, meta]`.
95
+ *
129
96
  * Action will not be processed by server’s callbacks from `Server#type`.
130
97
  *
131
- * @param action The action.
98
+ * @param action The action or the array of actions.
132
99
  * @param meta Action’s meta.
133
100
  * @returns Promise until action was added to the server log.
134
101
  */
135
- sendBack(action: AnyAction, meta?: Partial<ServerMeta>): Promise<void>
102
+ sendBack<TypeAction extends Action = AnyAction>(
103
+ action:
104
+ | (TypeAction | [TypeAction, Partial<ServerMeta>])[]
105
+ | TypeAction,
106
+ meta?: Partial<ServerMeta>
107
+ ): Promise<void>
108
+ }
109
+
110
+ /**
111
+ * Action context.
112
+ * ```
113
+ */
114
+ export class Context<
115
+ Data extends object = unknown,
116
+ Headers extends object = unknown
117
+ > extends ConnectContext<Headers> {
118
+ /**
119
+ * Open structure to save some data between different steps of processing.
120
+ *
121
+ * ```js
122
+ * server.type('RENAME', {
123
+ * access (ctx, action, meta) {
124
+ * ctx.data.user = findUser(ctx.userId)
125
+ * return ctx.data.user.hasAccess(action.projectId)
126
+ * }
127
+ * process (ctx, action, meta) {
128
+ * return ctx.data.user.rename(action.projectId, action.name)
129
+ * }
130
+ * })
131
+ * ```
132
+ */
133
+ data: Data
134
+
135
+ /**
136
+ * Was action created by Logux server.
137
+ *
138
+ * ```js
139
+ * access: (ctx, action, meta) => ctx.isServer
140
+ * ```
141
+ */
142
+ isServer: boolean
143
+
144
+ constructor(server: Server, meta: ServerMeta)
136
145
  }
137
146
 
138
147
  /**
package/context/index.js CHANGED
@@ -1,23 +1,29 @@
1
1
  import { parseId } from '@logux/core'
2
- import semver from 'semver'
3
2
 
4
3
  export class Context {
5
4
  constructor(server, meta) {
6
5
  this.server = server
7
6
  this.data = {}
8
7
 
9
- let parsed = parseId(meta.id)
10
- this.nodeId = parsed.nodeId
11
- this.userId = parsed.userId
12
- this.clientId = parsed.clientId
13
- this.isServer = this.userId === 'server'
14
-
15
- let client = server.clientIds.get(this.clientId)
16
-
17
- if (meta.subprotocol) {
18
- this.subprotocol = meta.subprotocol
19
- } else if (client) {
8
+ let client
9
+ if (meta.node) {
10
+ client = meta
11
+ this.nodeId = client.nodeId
12
+ this.userId = client.userId
13
+ this.clientId = client.clientId
20
14
  this.subprotocol = client.node.remoteSubprotocol
15
+ } else {
16
+ let parsed = parseId(meta.id)
17
+ this.nodeId = parsed.nodeId
18
+ this.userId = parsed.userId
19
+ this.clientId = parsed.clientId
20
+ this.isServer = this.userId === 'server'
21
+ client = server.clientIds.get(this.clientId)
22
+ if (meta.subprotocol) {
23
+ this.subprotocol = meta.subprotocol
24
+ } else if (client) {
25
+ this.subprotocol = client.node.remoteSubprotocol
26
+ }
21
27
  }
22
28
 
23
29
  if (client) {
@@ -27,15 +33,22 @@ export class Context {
27
33
  }
28
34
  }
29
35
 
30
- isSubprotocol(range) {
31
- return semver.satisfies(this.subprotocol, range)
36
+ drain() {
37
+ return this.server.drain(this.clientId)
32
38
  }
33
39
 
34
- sendBack(action, meta = {}) {
35
- return this.server.log.add(action, {
36
- clients: [this.clientId],
37
- status: 'processed',
38
- ...meta
39
- })
40
+ sendBack(actions, meta = {}) {
41
+ let common = { clients: [this.clientId], status: 'processed', ...meta }
42
+ if (Array.isArray(actions)) {
43
+ return this.server.log.add(
44
+ actions.map(item => {
45
+ // Every action can have own meta as `[action, meta]`
46
+ return Array.isArray(item)
47
+ ? [item[0], { ...common, ...item[1] }]
48
+ : [item, { ...common }]
49
+ })
50
+ )
51
+ }
52
+ return this.server.log.add(actions, common)
40
53
  }
41
54
  }
@@ -1,21 +1,14 @@
1
1
  import os from 'node:os'
2
- import { dirname, join, sep } from 'node:path'
3
- import { fileURLToPath } from 'node:url'
4
- import pico from 'picocolors'
5
- import pino from 'pino'
2
+ import { sep } from 'node:path'
6
3
 
7
- const __dirname = dirname(fileURLToPath(import.meta.url))
8
- export const PATH_TO_PRETTIFYING_PINO_TRANSPORT = join(
9
- __dirname,
10
- '../human-formatter/index.js'
11
- )
4
+ import humanFormatter from '../human-formatter/index.js'
5
+ import { BINARY, isBinary } from '../human-formatter/utils.js'
12
6
 
13
7
  const ERROR_CODES = {
14
8
  EACCES: (e, environment) => {
15
9
  let wayToFix = {
16
- development:
17
- 'In dev mode it can be done with sudo:\n' + '$ sudo npm start',
18
- production: '$ su - `<username>`\n' + `$ npm start -p ${e.port}`
10
+ development: 'In dev mode it can be done with sudo:\n$ sudo npm start',
11
+ production: `$ su - \`<username>\`\n$ npm start -p ${e.port}`
19
12
  }
20
13
 
21
14
  return {
@@ -27,13 +20,14 @@ const ERROR_CODES = {
27
20
  }
28
21
  },
29
22
  EADDRINUSE: e => {
23
+ let port = String(e.port)
30
24
  let wayToFix = {
31
- darwin: `$ sudo lsof -i ${e.port}\n` + '$ sudo kill -9 `<processid>`',
25
+ darwin: `$ sudo lsof -i ${e.port}\n$ sudo kill -9 \`<processid>\``,
32
26
  linux:
33
27
  '$ su - root\n' +
34
28
  `# netstat -nlp | grep ${e.port}\n` +
35
29
  'Proto Local Address State PID/Program name\n' +
36
- `tcp 0.0.0.0:${e.port} LISTEN \`777\`/node\n` +
30
+ `tcp 0.0.0.0:${port.padEnd(8)}LISTEN \`777\`/node\n` +
37
31
  '# sudo kill -9 `777`',
38
32
  win32:
39
33
  'Run `cmd.exe` as an administrator\n' +
@@ -49,13 +43,7 @@ const ERROR_CODES = {
49
43
  'or previous version of this server was not killed.\n\n' +
50
44
  (wayToFix[os.platform()] || '')
51
45
  }
52
- },
53
- LOGUX_NO_CONTROL_SECRET: e => ({
54
- msg: e.message,
55
- note:
56
- 'Call `npx nanoid` and set result as `controlSecret` ' +
57
- 'or `LOGUX_CONTROL_SECRET` environment variable'
58
- })
46
+ }
59
47
  }
60
48
 
61
49
  const REPORTERS = {
@@ -134,9 +122,9 @@ const REPORTERS = {
134
122
  let details = {
135
123
  environment: r.environment,
136
124
  loguxServer: r.loguxServer,
125
+ minSubprotocol: r.minSubprotocol,
137
126
  nodeId: r.nodeId,
138
- subprotocol: r.subprotocol,
139
- supports: r.supports
127
+ subprotocol: r.subprotocol
140
128
  }
141
129
 
142
130
  if (r.environment === 'development') {
@@ -155,15 +143,6 @@ const REPORTERS = {
155
143
  details.healthCheck = `${httpProtocol}${r.host}:${r.port}/health`
156
144
  }
157
145
 
158
- if (r.controlSecret) {
159
- details.controlListen = `http://${r.host}:${r.port}/`
160
- details.controlMask = r.controlMask
161
- }
162
-
163
- if (r.backend) {
164
- details.backendSend = r.backend
165
- }
166
-
167
146
  if (r.redis) {
168
147
  details.redis = r.redis
169
148
  }
@@ -191,73 +170,52 @@ const REPORTERS = {
191
170
  msg: 'Wrong channel name'
192
171
  }),
193
172
 
194
- wrongControlIp: () => ({
195
- level: 'warn',
196
- msg: 'IP address of control request do not pass the mask'
197
- }),
198
-
199
- wrongControlSecret: () => ({
200
- level: 'warn',
201
- msg: 'Wrong secret in control request'
202
- }),
203
-
204
173
  zombie: () => ({ level: 'warn', msg: 'Zombie client was disconnected' })
205
174
  }
206
175
 
207
- function createLogger(options) {
208
- if (options.logger === 'human' || options.logger.type === 'human') {
209
- let env = options.env || process.env.NODE_ENV || 'development'
210
- let color =
211
- env !== 'development' ? false : pico.createColors().isColorSupported
212
- let basepath = options.root || process.cwd()
213
- if (basepath.slice(-1) !== sep) basepath += sep
214
-
215
- let logger = pino(
216
- pino.transport({
217
- options: {
218
- basepath,
219
- color,
220
- destination: options.logger.destination
221
- },
222
- target: PATH_TO_PRETTIFYING_PINO_TRANSPORT
223
- })
224
- )
225
-
226
- // NOTE: needed only for tests
227
- logger._basepath = basepath
228
- logger._color = color
229
-
230
- return logger
231
- }
232
- return pino(
233
- {
234
- name: 'logux-server',
235
- timestamp: pino.stdTimeFunctions.isoTime
236
- },
237
- options.logger.stream || pino.destination()
238
- )
239
- }
240
-
241
176
  function cleanFromKeys(obj, regexp, seen) {
242
177
  let result = {}
243
178
  for (let key in obj) {
244
- let v = obj[key]
245
- if (typeof v === 'string') {
246
- result[key] = v.replace(regexp, '[SECRET]')
247
- } else if (typeof v === 'object' && !Array.isArray(v) && v !== null) {
248
- if (seen.includes(v)) {
249
- throw new Error('Circular reference in action')
250
- }
251
- seen.push(v)
252
- result[key] = cleanFromKeys(v, regexp, seen)
253
- seen.pop()
254
- } else {
255
- result[key] = v
256
- }
179
+ result[key] = cleanValue(obj[key], regexp, seen)
257
180
  }
258
181
  return result
259
182
  }
260
183
 
184
+ function cleanValue(value, regexp, seen) {
185
+ if (typeof value === 'string') {
186
+ return value.replace(regexp, '[SECRET]')
187
+ } else if (isBinary(value)) {
188
+ return BINARY
189
+ } else if (typeof value === 'object' && value !== null) {
190
+ if (seen.includes(value)) {
191
+ throw new Error('Circular reference in action')
192
+ }
193
+ seen.push(value)
194
+ let cleaned = Array.isArray(value)
195
+ ? value.map(i => cleanValue(i, regexp, seen))
196
+ : cleanFromKeys(value, regexp, seen)
197
+ seen.pop()
198
+ return cleaned
199
+ } else {
200
+ return value
201
+ }
202
+ }
203
+
204
+ function createRecord(level, details, msg) {
205
+ /* c8 ignore next 4 */
206
+ if (typeof details === 'string') {
207
+ msg = details
208
+ details = {}
209
+ }
210
+ return {
211
+ level,
212
+ time: new Date().toISOString(),
213
+ pid: process.pid,
214
+ ...details,
215
+ msg
216
+ }
217
+ }
218
+
261
219
  export function createReporter(options) {
262
220
  let cleanFromLog = options.cleanFromLog || /Bearer [^\s"]+/g
263
221
  function reporter(type, details) {
@@ -270,12 +228,41 @@ export function createReporter(options) {
270
228
  )
271
229
  }
272
230
 
273
- let customLoggerProvided =
274
- typeof options.logger !== 'string' && 'info' in options.logger
275
- if (customLoggerProvided) {
231
+ if (typeof options.logger !== 'string' && 'info' in options.logger) {
276
232
  reporter.logger = options.logger
277
233
  } else {
278
- reporter.logger = createLogger(options)
234
+ let format
235
+ if (options.logger === 'human' || options.logger.type === 'human') {
236
+ let basepath = options.root || process.cwd()
237
+ if (basepath.slice(-1) !== sep) basepath += sep
238
+ format = humanFormatter({ basepath })
239
+ } else {
240
+ format = record => JSON.stringify(record) + '\n'
241
+ }
242
+ let stream = options.logger?.stream ?? process.stderr
243
+
244
+ reporter.logger = {
245
+ /* c8 ignore next 3 */
246
+ debug(details, msg) {
247
+ stream.write(format(createRecord(20, details, msg)))
248
+ },
249
+ info(details, msg) {
250
+ stream.write(format(createRecord(30, details, msg)))
251
+ },
252
+ warn(details, msg) {
253
+ stream.write(format(createRecord(40, details, msg)))
254
+ },
255
+ error(details, msg) {
256
+ stream.write(format(createRecord(50, details, msg)))
257
+ },
258
+ fatal(details, msg) {
259
+ if (stream.flushSync) {
260
+ stream.flushSync(format(createRecord(60, details, msg)))
261
+ } else {
262
+ stream.write(format(createRecord(60, details, msg)))
263
+ }
264
+ }
265
+ }
279
266
  }
280
267
  return reporter
281
268
  }