@logux/server 0.10.8 → 0.12.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.
@@ -0,0 +1,243 @@
1
+ import {
2
+ LoguxSubscribeAction,
3
+ SyncMapChangeAction,
4
+ SyncMapCreateAction,
5
+ SyncMapDeleteAction,
6
+ SyncMapValues,
7
+ SyncMapTypes
8
+ } from '@logux/actions'
9
+
10
+ import { BaseServer, ServerMeta } from '../base-server/index.js'
11
+ import { Context } from '../context/index.js'
12
+
13
+ declare const WITH_TIME: unique symbol
14
+
15
+ export type WithTime<Value extends SyncMapTypes | SyncMapTypes[]> = {
16
+ value: Value
17
+ time: number
18
+ [WITH_TIME]: true
19
+ }
20
+
21
+ export type WithoutTime<Value extends SyncMapTypes | SyncMapTypes[]> = {
22
+ value: Value
23
+ time: undefined
24
+ [WITH_TIME]: false
25
+ }
26
+
27
+ export type SyncMapData<Value extends SyncMapValues> = { id: string } & {
28
+ [Key in keyof Value]: WithTime<Value[Key]> | WithoutTime<Value[Key]>
29
+ }
30
+
31
+ /**
32
+ * Add last changed time to value to use in conflict resolution.
33
+ *
34
+ * If you do not know the time, use {@link NoConflictResolution}.
35
+ *
36
+ * @param value The value.
37
+ * @param time UNIX milliseconds.
38
+ * @returns Wrapper.
39
+ */
40
+ export function ChangedAt<Value extends SyncMapTypes | SyncMapTypes[]>(
41
+ value: Value,
42
+ time: number
43
+ ): WithTime<Value>
44
+
45
+ /**
46
+ * Mark that the value has no last changed date and conflict resolution
47
+ * can’t be applied.
48
+ *
49
+ * @param value The value.
50
+ * @returns Wrapper.
51
+ */
52
+ export function NoConflictResolution<
53
+ Value extends SyncMapTypes | SyncMapTypes[]
54
+ >(
55
+ value: Value
56
+ ): WithTime<Value>
57
+
58
+ interface SyncMapActionFilter<Value extends SyncMapValues> {
59
+ (
60
+ ctx: Context,
61
+ action: SyncMapCreateAction<Value> | SyncMapDeleteAction,
62
+ meta: ServerMeta
63
+ ): Promise<boolean> | boolean
64
+ }
65
+
66
+ interface SyncMapOperations<Value extends SyncMapValues> {
67
+ access(
68
+ ctx: Context,
69
+ id: string,
70
+ action:
71
+ | SyncMapCreateAction
72
+ | SyncMapChangeAction
73
+ | SyncMapDeleteAction
74
+ | LoguxSubscribeAction,
75
+ meta: ServerMeta
76
+ ): Promise<boolean> | boolean
77
+
78
+ load?(
79
+ ctx: Context,
80
+ id: string,
81
+ since: number | undefined,
82
+ action: LoguxSubscribeAction,
83
+ meta: ServerMeta
84
+ ): Promise<SyncMapData<Value>> | SyncMapData<Value> | false | Promise<false>
85
+
86
+ create?(
87
+ ctx: Context,
88
+ id: string,
89
+ fields: Value,
90
+ time: number,
91
+ action: SyncMapCreateAction<Value>,
92
+ meta: ServerMeta
93
+ ): Promise<void | boolean> | void | boolean
94
+
95
+ change?(
96
+ ctx: Context,
97
+ id: string,
98
+ fields: Partial<Value>,
99
+ time: number,
100
+ action: SyncMapChangeAction<Value>,
101
+ meta: ServerMeta
102
+ ): Promise<void | boolean> | void | boolean
103
+
104
+ delete?(
105
+ ctx: Context,
106
+ id: string,
107
+ action: SyncMapDeleteAction,
108
+ meta: ServerMeta
109
+ ): Promise<void | boolean> | void | boolean
110
+ }
111
+
112
+ interface SyncMapFilterOperations<Value extends SyncMapValues> {
113
+ access?(
114
+ ctx: Context,
115
+ filter: Partial<Value> | undefined,
116
+ action: LoguxSubscribeAction,
117
+ meta: ServerMeta
118
+ ): Promise<boolean> | boolean
119
+
120
+ initial(
121
+ ctx: Context,
122
+ filter: Partial<Value> | undefined,
123
+ since: number | undefined,
124
+ action: LoguxSubscribeAction,
125
+ meta: ServerMeta
126
+ ): Promise<SyncMapData<Value>[]> | SyncMapData<Value>[]
127
+
128
+ actions?(
129
+ ctx: Context,
130
+ filter: Partial<Value> | undefined,
131
+ action: LoguxSubscribeAction,
132
+ meta: ServerMeta
133
+ ): Promise<SyncMapActionFilter<Value>> | SyncMapActionFilter<Value> | void
134
+ }
135
+
136
+ /**
137
+ * Add callbacks for client’s `SyncMap`.
138
+ *
139
+ * ```js
140
+ * import { addSyncMap, isFirstTimeOlder, ChangedAt } from '@logux/server'
141
+ * import { LoguxNotFoundError } from '@logux/actions'
142
+ *
143
+ * addSyncMap(server, 'tasks', {
144
+ * async access (ctx, id) {
145
+ * const task = await Task.find(id)
146
+ * return ctx.userId === task.authorId
147
+ * },
148
+ *
149
+ * async load (ctx, id, since) {
150
+ * const task = await Task.find(id)
151
+ * if (!task) throw new LoguxNotFoundError()
152
+ * return {
153
+ * id: task.id,
154
+ * text: ChangedAt(task.text, task.textChanged),
155
+ * finished: ChangedAt(task.finished, task.finishedChanged),
156
+ * }
157
+ * },
158
+ *
159
+ * async create (ctx, id, fields, time) {
160
+ * await Task.create({
161
+ * id,
162
+ * text: fields.text,
163
+ * finished: fields.finished,
164
+ * authorId: ctx.userId,
165
+ * textChanged: time,
166
+ * finishedChanged: time
167
+ * })
168
+ * },
169
+ *
170
+ * async change (ctx, id, fields, time) {
171
+ * const task = await Task.find(id)
172
+ * if ('text' in fields) {
173
+ * if (task.textChanged < time) {
174
+ * await task.update({
175
+ * text: fields.text,
176
+ * textChanged: time
177
+ * })
178
+ * }
179
+ * }
180
+ * if ('finished' in fields) {
181
+ * if (task.finishedChanged < time) {
182
+ * await task.update({
183
+ * finished: fields.finished,
184
+ * finishedChanged: time
185
+ * })
186
+ * }
187
+ * }
188
+ * }
189
+ *
190
+ * async delete (ctx, id) {
191
+ * await Task.delete(id)
192
+ * }
193
+ * })
194
+ * ```
195
+ *
196
+ * @param server Server instance.
197
+ * @param plural Prefix for channel names and action types.
198
+ * @param operations Callbacks.
199
+ */
200
+ export function addSyncMap<Values extends SyncMapValues>(
201
+ server: BaseServer,
202
+ plural: string,
203
+ operations: SyncMapOperations<Values>
204
+ ): void
205
+
206
+ /**
207
+ * Add callbacks for client’s `useFilter`.
208
+ *
209
+ * ```js
210
+ * import { addSyncMapFilter, ChangedAt } from '@logux/server'
211
+ *
212
+ * addSyncMapFilter(server, 'tasks', {
213
+ * access (ctx, filter) {
214
+ * return true
215
+ * },
216
+ *
217
+ * initial (ctx, filter, since) {
218
+ * let tasks = await Tasks.where({ ...filter, authorId: ctx.userId })
219
+ * // You can return only data changed after `since`
220
+ * return tasks.map(task => ({
221
+ * id: task.id,
222
+ * text: ChangedAt(task.text, task.textChanged),
223
+ * finished: ChangedAt(task.finished, task.finishedChanged),
224
+ * }))
225
+ * },
226
+ *
227
+ * actions (filterCtx, filter) {
228
+ * return (actionCtx, action, meta) => {
229
+ * return actionCtx.userId === filterCtx.userId
230
+ * }
231
+ * }
232
+ * })
233
+ * ```
234
+
235
+ * @param server Server instance.
236
+ * @param plural Prefix for channel names and action types.
237
+ * @param operations Callbacks.
238
+ */
239
+ export function addSyncMapFilter<Values extends SyncMapValues>(
240
+ server: BaseServer,
241
+ plural: string,
242
+ operations: SyncMapFilterOperations<Values>
243
+ ): void
@@ -0,0 +1,207 @@
1
+ const WITH_TIME = Symbol('WITH_TIME')
2
+
3
+ export function ChangedAt(value, time) {
4
+ return { value, time, [WITH_TIME]: true }
5
+ }
6
+
7
+ export function NoConflictResolution(value) {
8
+ return { value, [WITH_TIME]: false }
9
+ }
10
+
11
+ async function addFinished(server, ctx, type, action, meta) {
12
+ await server.process(
13
+ { ...action, type },
14
+ { time: meta.time, excludeClients: [ctx.clientId] }
15
+ )
16
+ }
17
+
18
+ function resendFinished(server, plural, type, all = true) {
19
+ if (all) {
20
+ server.type(type, {
21
+ access() {
22
+ return false
23
+ },
24
+ resend(ctx, action) {
25
+ return [plural, `${plural}/${action.id}`]
26
+ }
27
+ })
28
+ } else {
29
+ server.type(type, {
30
+ access() {
31
+ return false
32
+ },
33
+ resend(ctx, action) {
34
+ return [`${plural}/${action.id}`]
35
+ }
36
+ })
37
+ }
38
+ }
39
+
40
+ function buildFilter(filter) {
41
+ return (ctx, action) => {
42
+ if (action.type.endsWith('/created')) {
43
+ for (let key in filter) {
44
+ if (action.fields[key] !== filter[key]) return false
45
+ }
46
+ }
47
+ return true
48
+ }
49
+ }
50
+
51
+ async function sendMap(server, changedType, data, since) {
52
+ let { id, ...other } = data
53
+ let byTime = new Map()
54
+ for (let key in other) {
55
+ if (other[key][WITH_TIME] === true) {
56
+ let time = other[key].time
57
+ if (!byTime.has(time)) byTime.set(time, {})
58
+ byTime.get(time)[key] = other[key].value
59
+ } else if (other[key][WITH_TIME] === false) {
60
+ if (!byTime.has('now')) byTime.set('now', {})
61
+ byTime.get('now')[key] = other[key].value
62
+ } else {
63
+ throw new Error('Wrap value into ChangedAt() or NoConflictResolution()')
64
+ }
65
+ }
66
+ for (let [time, fields] of byTime.entries()) {
67
+ let changedMeta
68
+ if (time !== 'now') {
69
+ changedMeta = { time }
70
+ if (time < since) continue
71
+ }
72
+ await server.process(
73
+ {
74
+ type: changedType,
75
+ id,
76
+ fields
77
+ },
78
+ changedMeta
79
+ )
80
+ }
81
+ }
82
+
83
+ export function addSyncMap(server, plural, operations) {
84
+ let createdType = `${plural}/created`
85
+ let changedType = `${plural}/changed`
86
+ let deletedType = `${plural}/deleted`
87
+ resendFinished(server, plural, createdType)
88
+ resendFinished(server, plural, changedType)
89
+ resendFinished(server, plural, deletedType, false)
90
+
91
+ if (operations.load) {
92
+ server.channel(`${plural}/:id`, {
93
+ access(ctx, action, meta) {
94
+ return operations.access(ctx, ctx.params.id, action, meta)
95
+ },
96
+ async load(ctx, action, meta) {
97
+ if (action.creating) return
98
+ let data = await operations.load(
99
+ ctx,
100
+ ctx.params.id,
101
+ action.since,
102
+ action,
103
+ meta
104
+ )
105
+ if (data !== false) {
106
+ await sendMap(
107
+ server,
108
+ changedType,
109
+ data,
110
+ action.since ? action.since.time : 0
111
+ )
112
+ }
113
+ }
114
+ })
115
+ }
116
+
117
+ if (operations.create) {
118
+ server.type(`${plural}/create`, {
119
+ access(ctx, action, meta) {
120
+ return operations.access(ctx, action.id, action, meta)
121
+ },
122
+ async process(ctx, action, meta) {
123
+ let result = await operations.create(
124
+ ctx,
125
+ action.id,
126
+ action.fields,
127
+ meta.time,
128
+ action,
129
+ meta
130
+ )
131
+ if (result !== false) {
132
+ await addFinished(server, ctx, createdType, action, meta)
133
+ }
134
+ }
135
+ })
136
+ }
137
+
138
+ if (operations.change) {
139
+ server.type(`${plural}/change`, {
140
+ access(ctx, action, meta) {
141
+ return operations.access(ctx, action.id, action, meta)
142
+ },
143
+ async process(ctx, action, meta) {
144
+ let result = await operations.change(
145
+ ctx,
146
+ action.id,
147
+ action.fields,
148
+ meta.time,
149
+ action,
150
+ meta
151
+ )
152
+ if (result !== false) {
153
+ await addFinished(server, ctx, changedType, action, meta)
154
+ }
155
+ }
156
+ })
157
+ }
158
+
159
+ if (operations.delete) {
160
+ server.type(`${plural}/delete`, {
161
+ access(ctx, action, meta) {
162
+ return operations.access(ctx, action.id, action, meta)
163
+ },
164
+ async process(ctx, action, meta) {
165
+ let result = await operations.delete(ctx, action.id, action, meta)
166
+ if (result !== false) {
167
+ await addFinished(server, ctx, deletedType, action, meta)
168
+ }
169
+ }
170
+ })
171
+ }
172
+ }
173
+
174
+ export function addSyncMapFilter(server, plural, operations) {
175
+ let changedType = `${plural}/changed`
176
+
177
+ server.channel(plural, {
178
+ access(ctx, action, meta) {
179
+ return operations.access(ctx, action.filter, action, meta)
180
+ },
181
+ filter(ctx, action, meta) {
182
+ let filter = action.filter ? buildFilter(action.filter) : () => true
183
+ let custom = operations.actions
184
+ ? operations.actions(ctx, action.filter, action, meta)
185
+ : () => true
186
+ return (ctx2, action2, meta2) => {
187
+ return filter(ctx2, action2, meta2) && custom(ctx2, action2, meta2)
188
+ }
189
+ },
190
+ async load(ctx, action, meta) {
191
+ let since = action.since ? action.since.time : 0
192
+ let data = await operations.initial(
193
+ ctx,
194
+ action.filter,
195
+ since,
196
+ action,
197
+ meta
198
+ )
199
+ await Promise.all(
200
+ data.map(async i => {
201
+ await server.subscribe(ctx.nodeId, `${plural}/${i.id}`)
202
+ await sendMap(server, changedType, i, since)
203
+ })
204
+ )
205
+ }
206
+ })
207
+ }
@@ -72,7 +72,7 @@ export interface ServerMeta extends Meta {
72
72
  node?: string
73
73
 
74
74
  /**
75
- * Client IDs, which will not recieve the action.
75
+ * Client IDs, which will not receive the action.
76
76
  */
77
77
  excludeClients?: string[]
78
78
  }
@@ -206,6 +206,14 @@ export interface BaseServerOptions {
206
206
  * By default it cleans `Bearer [^\s"]+`.
207
207
  */
208
208
  cleanFromLog?: RegExp
209
+
210
+ /**
211
+ * Disable health check endpoint, control HTTP API, {@link Server#http}.
212
+ *
213
+ * The server will process only WebSocket connection and ignore all other
214
+ * HTTP request (so they can be processed by other HTTP server).
215
+ */
216
+ disableHttpServer?: boolean
209
217
  }
210
218
 
211
219
  export interface AuthenticatorOptions<Headers extends object> {
@@ -750,8 +758,8 @@ export class BaseServer<
750
758
  * and node ID. It should return a Promise with `true` or `false`.
751
759
  *
752
760
  * ```js
753
- * server.auth(async ({ userId, token }) => {
754
- * const user = await findUserByToken(token)
761
+ * server.auth(async ({ userId, cookie }) => {
762
+ * const user = await findUserByToken(cookie.token)
755
763
  * return !!user && userId === user.id
756
764
  * })
757
765
  * ```
@@ -153,6 +153,8 @@ export class BaseServer {
153
153
  this.contexts = new WeakMap()
154
154
  this.log = log
155
155
 
156
+ let cleaned = {}
157
+
156
158
  this.on('preadd', (action, meta) => {
157
159
  let isLogux = action.type.slice(0, 6) === 'logux/'
158
160
  if (!meta.server) {
@@ -178,7 +180,12 @@ export class BaseServer {
178
180
  })
179
181
  this.on('add', async (action, meta) => {
180
182
  let start = Date.now()
181
- this.emitter.emit('report', 'add', { action, meta })
183
+ if (meta.reasons.length === 0) {
184
+ cleaned[meta.id] = true
185
+ this.emitter.emit('report', 'addClean', { action, meta })
186
+ } else {
187
+ this.emitter.emit('report', 'add', { action, meta })
188
+ }
182
189
 
183
190
  if (this.destroying) return
184
191
 
@@ -254,6 +261,10 @@ export class BaseServer {
254
261
  }
255
262
  })
256
263
  this.on('clean', (action, meta) => {
264
+ if (cleaned[meta.id]) {
265
+ delete cleaned[meta.id]
266
+ return
267
+ }
257
268
  this.emitter.emit('report', 'clean', { actionId: meta.id })
258
269
  })
259
270
 
@@ -371,6 +382,11 @@ export class BaseServer {
371
382
  }
372
383
 
373
384
  http(listener) {
385
+ if (this.options.disableHttpServer) {
386
+ throw new Error(
387
+ '`server.http()` can not be called when `disableHttpServer` enabled'
388
+ )
389
+ }
374
390
  this.httpListener = listener
375
391
  }
376
392
 
@@ -658,7 +674,6 @@ export class BaseServer {
658
674
  try {
659
675
  await processor.process(ctx, action, meta)
660
676
  latency = Date.now() - start
661
- this.emitter.emit('report', 'processed', { actionId: meta.id, latency })
662
677
  this.markAsProcessed(meta)
663
678
  } catch (e) {
664
679
  this.log.changeMeta(meta.id, { status: 'error' })
@@ -37,7 +37,7 @@ function send(backend, command, events) {
37
37
  },
38
38
  res => {
39
39
  if (res.statusCode < 200 || res.statusCode > 299) {
40
- events.error(new Error(`Backend responsed with ${res.statusCode} code`))
40
+ events.error(new Error(`Backend responded with ${res.statusCode} code`))
41
41
  } else {
42
42
  let data = false
43
43
  res
@@ -17,6 +17,17 @@ function isValidBody(body) {
17
17
  }
18
18
 
19
19
  export function bindControlServer(app, custom) {
20
+ if (app.options.disableHttpServer) {
21
+ if (app.options.controlSecret) {
22
+ let err = new Error(
23
+ '`controlSecret` can be set together with `disableHttpServer` option'
24
+ )
25
+ err.logux = true
26
+ throw err
27
+ }
28
+ return
29
+ }
30
+
20
31
  let masks = app.options.controlMask.split(/,\s*/).map(i => ip.cidrSubnet(i))
21
32
  app.httpServer.on('request', async (req, res) => {
22
33
  let urlString = req.url
@@ -1,7 +1,14 @@
1
1
  import pino from 'pino'
2
2
  import os from 'os'
3
+ import { dirname, join, sep } from 'path'
4
+ import pico from 'picocolors'
5
+ import { fileURLToPath } from 'url'
3
6
 
4
- import { humanFormatter } from '../human-formatter/index.js'
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
+ )
5
12
 
6
13
  const ERROR_CODES = {
7
14
  EADDRINUSE: e => {
@@ -105,9 +112,9 @@ const REPORTERS = {
105
112
 
106
113
  add: () => ({ msg: 'Action was added' }),
107
114
 
108
- clean: () => ({ msg: 'Action was cleaned' }),
115
+ addClean: () => ({ msg: 'Action was added and cleaned' }),
109
116
 
110
- processed: () => ({ msg: 'Action was processed' }),
117
+ clean: () => ({ msg: 'Action was cleaned' }),
111
118
 
112
119
  subscribed: () => ({ msg: 'Client was subscribed' }),
113
120
 
@@ -198,27 +205,36 @@ const REPORTERS = {
198
205
  }
199
206
 
200
207
  function createLogger(options) {
201
- let stream = options.logger.stream || pino.destination()
202
- let prettifier = {}
203
208
  if (options.logger === 'human' || options.logger.type === 'human') {
204
209
  let env = options.env || process.env.NODE_ENV || 'development'
205
- let color = env !== 'development' ? false : undefined
206
- prettifier = {
207
- prettyPrint: {
208
- suppressFlushSyncWarning: true,
209
- basepath: options.root,
210
- color
211
- },
212
- prettifier: humanFormatter
213
- }
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
+ target: PATH_TO_PRETTIFYING_PINO_TRANSPORT,
218
+ options: {
219
+ basepath,
220
+ color,
221
+ destination: options.logger.destination
222
+ }
223
+ })
224
+ )
225
+
226
+ // NOTE: needed only for tests
227
+ logger._basepath = basepath
228
+ logger._color = color
229
+
230
+ return logger
214
231
  }
215
232
  return pino(
216
233
  {
217
234
  name: 'logux-server',
218
- timestamp: pino.stdTimeFunctions.isoTime,
219
- ...prettifier
235
+ timestamp: pino.stdTimeFunctions.isoTime
220
236
  },
221
- stream
237
+ options.logger.stream || pino.destination()
222
238
  )
223
239
  }
224
240
 
@@ -1,16 +1,19 @@
1
1
  import stripAnsi from 'strip-ansi'
2
2
  import yyyymmdd from 'yyyy-mm-dd'
3
3
  import pico from 'picocolors'
4
- import path from 'path'
5
4
  import os from 'os'
5
+ import abstractTransport from 'pino-abstract-transport'
6
+ import { Transform } from 'stream'
7
+ import pino from 'pino'
8
+ import { once } from 'events'
9
+
10
+ import { mulberry32, onceXmur3 } from './utils.js'
6
11
 
7
12
  const INDENT = ' '
8
13
  const PADDING = ' '
9
14
  const SEPARATOR = os.EOL + os.EOL
10
15
  const NEXT_LINE = os.EOL === '\n' ? '\r\v' : os.EOL
11
16
 
12
- const LATENCY_UNIT = ' ms'
13
-
14
17
  const PARAMS_BLACKLIST = {
15
18
  v: true,
16
19
  msg: true,
@@ -34,6 +37,8 @@ const LABELS = {
34
37
  60: (c, str) => label(c, ' FATAL ', 'red', 'bgRed', 'white', str)
35
38
  }
36
39
 
40
+ const COLORS = ['red', 'green', 'yellow', 'blue', 'magenta', 'cyan']
41
+
37
42
  function rightPag(str, length) {
38
43
  let add = length - stripAnsi(str).length
39
44
  for (let i = 0; i < add; i++) str += ' '
@@ -56,15 +61,54 @@ function formatName(key) {
56
61
  .replace(/^\w/, char => char.toUpperCase())
57
62
  }
58
63
 
64
+ function shuffledColors(str) {
65
+ let index = -1
66
+ let result = Array.from(COLORS)
67
+ let lastIndex = result.length - 1
68
+ let seed = onceXmur3(str)
69
+ let randomFn = mulberry32(seed)
70
+
71
+ while (++index < COLORS.length) {
72
+ let randIndex = index + Math.floor(randomFn() * (lastIndex - index + 1))
73
+ let value = result[randIndex]
74
+
75
+ result[randIndex] = result[index]
76
+ result[index] = value
77
+ }
78
+ return result
79
+ }
80
+
81
+ function splitAndColorize(c, partLength, str) {
82
+ let strBuilder = []
83
+ let colors = shuffledColors(str)
84
+
85
+ for (
86
+ let start = 0, end = partLength, n = 0, color = colors[n];
87
+ start < str.length;
88
+ start += partLength,
89
+ end += partLength,
90
+ n = n + 1,
91
+ color = colors[n % colors.length]
92
+ ) {
93
+ let strToColorize = str.slice(start, end)
94
+ if (strToColorize.length === 1) {
95
+ color = colors[Math.abs(n - 1) % colors.length]
96
+ }
97
+ strBuilder.push(c[color](strToColorize))
98
+ }
99
+
100
+ return strBuilder.join('')
101
+ }
102
+
59
103
  function formatNodeId(c, nodeId) {
60
104
  let pos = nodeId.lastIndexOf(':')
61
- let id, random
62
105
  if (pos === -1) {
63
106
  return nodeId
64
107
  } else {
65
- id = nodeId.slice(0, pos)
66
- random = nodeId.slice(pos)
67
- return c.bold(id) + random
108
+ let s = nodeId.split(':')
109
+ let id = c.bold(s[0])
110
+ let random = splitAndColorize(c, 3, s[1])
111
+ return `${id}:${random}`
68
112
  }
69
113
  }
70
114
 
@@ -92,7 +136,13 @@ function formatArray(c, array) {
92
136
 
93
137
  function formatActionId(c, id) {
94
138
  let p = id.split(' ')
95
- return `${c.bold(p[0])} ${formatNodeId(c, p[1])} ${c.bold(p[2])}`
139
+ if (p.length === 1) {
140
+ return p
141
+ }
142
+ return `${c.bold(splitAndColorize(c, 4, p[0]))} ${formatNodeId(
143
+ c,
144
+ p[1]
145
+ )} ${c.bold(p[2])}`
96
146
  }
97
147
 
98
148
  function formatParams(c, params, parent) {
@@ -108,8 +158,13 @@ function formatParams(c, params, parent) {
108
158
 
109
159
  let start = PADDING + rightPag(`${name}: `, maxName + 2)
110
160
 
111
- if (name === 'Node ID') {
161
+ if (name === 'Node ID' || (parent === 'Meta' && name === 'server')) {
112
162
  return start + formatNodeId(c, value)
163
+ } else if (
164
+ parent === 'Meta' &&
165
+ (name === 'clients' || name === 'excludeClients')
166
+ ) {
167
+ return `${start}[${value.map(v => `"${formatNodeId(c, v)}"`).join()}]`
113
168
  } else if (name === 'Action ID' || (parent === 'Meta' && name === 'id')) {
114
169
  return start + formatActionId(c, value)
115
170
  } else if (Array.isArray(value)) {
@@ -124,8 +179,6 @@ function formatParams(c, params, parent) {
124
179
  .split(NEXT_LINE)
125
180
  .join(NEXT_LINE + INDENT)
126
181
  )
127
- } else if (name === 'Latency' && !parent) {
128
- return start + c.bold(value) + LATENCY_UNIT
129
182
  } else if (typeof value === 'string' && parent) {
130
183
  return start + '"' + c.bold(value) + '"'
131
184
  } else {
@@ -169,14 +222,11 @@ function prettyStackTrace(c, stack, basepath) {
169
222
  .join(NEXT_LINE)
170
223
  }
171
224
 
172
- export function humanFormatter(options) {
225
+ function humanFormatter(options) {
173
226
  let c = pico.createColors(options.color)
174
- this.color = c.isColorSupported
175
- let basepath = options.basepath || process.cwd()
176
- if (basepath.slice(-1) !== path.sep) basepath += path.sep
177
- this.basepath = basepath
227
+ let basepath = options.basepath
178
228
 
179
- return function (record) {
229
+ return function format(record) {
180
230
  let message = [LABELS[record.level](c, record.msg)]
181
231
  let params = Object.keys(record)
182
232
  .filter(key => !PARAMS_BLACKLIST[key])
@@ -213,3 +263,32 @@ export function humanFormatter(options) {
213
263
  return message.filter(i => i !== '').join(NEXT_LINE) + SEPARATOR
214
264
  }
215
265
  }
266
+
267
+ export default async function (options) {
268
+ let format = humanFormatter(options)
269
+ let destination = pino.destination({
270
+ dest: options.destination || 1,
271
+ sync: options.sync || false
272
+ })
273
+ await once(destination, 'ready')
274
+ let transform = new Transform({
275
+ autoDestroy: true,
276
+ objectMode: true,
277
+ transform(chunk, encoding, callback) {
278
+ callback(null, format(chunk))
279
+ }
280
+ })
281
+
282
+ return abstractTransport(
283
+ source => {
284
+ source.pipe(transform)
285
+ transform.pipe(destination)
286
+ },
287
+ {
288
+ close(err, cb) {
289
+ transform.end()
290
+ transform.on('close', cb.bind(null, err))
291
+ }
292
+ }
293
+ )
294
+ }
@@ -0,0 +1,20 @@
1
+ export function onceXmur3(str) {
2
+ let h = 1779033703 ^ str.length
3
+ for (let i = 0; i < str.length; i++) {
4
+ h = Math.imul(h ^ str.charCodeAt(i), 3432918353)
5
+ h = (h << 13) | (h >>> 19)
6
+ }
7
+ h = Math.imul(h ^ (h >>> 16), 2246822507)
8
+ h = Math.imul(h ^ (h >>> 13), 3266489909)
9
+ return (h ^ (h >>> 16)) >>> 0
10
+ }
11
+
12
+ export function mulberry32(a) {
13
+ return function () {
14
+ a |= 0
15
+ a = (a + 0x6d2b79f5) | 0
16
+ let t = Math.imul(a ^ (a >>> 15), 1 | a)
17
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
18
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296
19
+ }
20
+ }
package/index.d.ts CHANGED
@@ -17,6 +17,15 @@ export {
17
17
  put,
18
18
  del
19
19
  } from './request/index.js'
20
+ export {
21
+ NoConflictResolution,
22
+ addSyncMapFilter,
23
+ WithoutTime,
24
+ SyncMapData,
25
+ addSyncMap,
26
+ ChangedAt,
27
+ WithTime
28
+ } from './add-sync-map/index.js'
20
29
  export { TestServer, TestServerOptions } from './test-server/index.js'
21
30
  export { TestClient, LoguxActionError } from './test-client/index.js'
22
31
  export { Context, ChannelContext } from './context/index.js'
package/index.js CHANGED
@@ -7,6 +7,12 @@ export {
7
7
  put,
8
8
  del
9
9
  } from './request/index.js'
10
+ export {
11
+ NoConflictResolution,
12
+ addSyncMapFilter,
13
+ addSyncMap,
14
+ ChangedAt
15
+ } from './add-sync-map/index.js'
10
16
  export { BaseServer, wasNot403 } from './base-server/index.js'
11
17
  export { ALLOWED_META } from './allowed-meta/index.js'
12
18
  export { TestServer } from './test-server/index.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logux/server",
3
- "version": "0.10.8",
3
+ "version": "0.12.1",
4
4
  "description": "Build own Logux server or make proxy between WebSocket and HTTP backend on any language",
5
5
  "keywords": [
6
6
  "logux",
@@ -18,7 +18,7 @@
18
18
  "homepage": "https://logux.io/",
19
19
  "repository": "logux/server",
20
20
  "engines": {
21
- "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
21
+ "node": "^14.0.0 || ^16.0.0 || >=18.0.0"
22
22
  },
23
23
  "type": "module",
24
24
  "types": "./index.d.ts",
@@ -27,23 +27,24 @@
27
27
  "./package.json": "./package.json"
28
28
  },
29
29
  "dependencies": {
30
- "@logux/actions": "^0.2.1",
31
- "@logux/core": "^0.7.2",
30
+ "@logux/actions": "^0.3.0",
31
+ "@logux/core": "^0.8.0",
32
32
  "JSONStream": "^1.3.5",
33
- "cookie": "^0.4.1",
34
- "dotenv": "^10.0.0",
35
- "fast-glob": "^3.2.7",
36
- "ip": "^1.1.5",
33
+ "cookie": "^0.5.0",
34
+ "dotenv": "^16.0.2",
35
+ "fast-glob": "^3.2.11",
36
+ "ip": "^1.1.8",
37
37
  "nanodelay": "^2.0.2",
38
- "nanoevents": "^6.0.2",
39
- "nanoid": "^3.1.30",
40
- "node-fetch": "^2.6.1",
38
+ "nanoevents": "^7.0.1",
39
+ "nanoid": "^4.0.0",
40
+ "node-fetch": "^2.6.7",
41
41
  "picocolors": "^1.0.0",
42
- "pino": "^6.13.3",
43
- "semver": "^7.3.5",
42
+ "pino": "8.4.2",
43
+ "pino-abstract-transport": "^1.0.0",
44
+ "semver": "^7.3.7",
44
45
  "strip-ansi": "^7.0.1",
45
46
  "url-pattern": "^1.0.3",
46
- "ws": "^8.3.0",
47
+ "ws": "^8.8.1",
47
48
  "yyyy-mm-dd": "^1.0.2"
48
49
  }
49
50
  }
@@ -151,7 +151,7 @@ export class TestClient {
151
151
  * @param test Function, where do you expect action will be received
152
152
  * @returns Promise with all received actions
153
153
  */
154
- collect(test: () => Promise<void>): Promise<Action[]>
154
+ collect(test: () => Promise<any>): Promise<Action[]>
155
155
 
156
156
  /**
157
157
  * Send action to the sever and collect all response actions.
@@ -180,11 +180,13 @@ export class TestClient {
180
180
  *
181
181
  * @param channel Channel name or `logux/subscribe` action.
182
182
  * @param filter Optional filter for subscription.
183
+ * @param since Optional time from last data.
183
184
  * @returns Promise with all actions from the server.
184
185
  */
185
186
  subscribe(
186
187
  channel: string | LoguxSubscribeAction,
187
- filter?: object
188
+ filter?: object,
189
+ since?: { id: string; time: number }
188
190
  ): Promise<Action[]>
189
191
 
190
192
  /**
@@ -216,5 +218,5 @@ export class TestClient {
216
218
  * @param test Function, where do you expect action will be received
217
219
  * @returns Promise with all received actions
218
220
  */
219
- received(test: () => Promise<void> | void): Promise<Action[]>
221
+ received(test: () => Promise<any> | any): Promise<Action[]>
220
222
  }
@@ -133,7 +133,7 @@ export class TestClient {
133
133
  })
134
134
  }
135
135
 
136
- async subscribe(channel, filter) {
136
+ async subscribe(channel, filter, since) {
137
137
  let action = channel
138
138
  if (typeof channel === 'string') {
139
139
  action = { type: 'logux/subscribe', channel }
@@ -141,6 +141,9 @@ export class TestClient {
141
141
  if (filter) {
142
142
  action.filter = filter
143
143
  }
144
+ if (since) {
145
+ action.since = since
146
+ }
144
147
  let actions = await this.process(action)
145
148
  return actions.filter(i => i.type !== 'logux/processed')
146
149
  }
@@ -126,4 +126,16 @@ export class TestServer<
126
126
  * @param test Callback with subscripting or action sending.
127
127
  */
128
128
  expectUndo(reason: string, test: () => any | Promise<any>): Promise<void>
129
+
130
+ /**
131
+ * Call callback and throw an error if there was no error during
132
+ * server processing.
133
+ *
134
+ * @param text RegExp or string of error message.
135
+ * @param test Callback with subscripting or action sending.
136
+ */
137
+ expectError(
138
+ text: string | RegExp,
139
+ test: () => any | Promise<any>
140
+ ): Promise<void>
129
141
  }
@@ -74,4 +74,18 @@ export class TestServer extends BaseServer {
74
74
  }
75
75
  }
76
76
  }
77
+
78
+ async expectError(text, test) {
79
+ try {
80
+ await test()
81
+ throw new Error('Actions passed without error')
82
+ } catch (e) {
83
+ if (
84
+ (typeof text === 'string' && e.message !== text) ||
85
+ (text instanceof RegExp && !text.test(e.message))
86
+ ) {
87
+ throw e
88
+ }
89
+ }
90
+ }
77
91
  }