@logux/server 0.12.10 → 0.13.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.
@@ -27,9 +27,9 @@ export type WithoutTime<Value extends SyncMapTypes | SyncMapTypes[]> = {
27
27
  [WITH_TIME]: false
28
28
  }
29
29
 
30
- export type SyncMapData<Value extends SyncMapValues> = { id: string } & {
30
+ export type SyncMapData<Value extends SyncMapValues> = {
31
31
  [Key in keyof Value]: WithoutTime<Value[Key]> | WithTime<Value[Key]>
32
- }
32
+ } & { id: string }
33
33
 
34
34
  /**
35
35
  * Add last changed time to value to use in conflict resolution.
@@ -3,7 +3,7 @@
3
3
  *
4
4
  *```js
5
5
  * import { ALLOWED_META } from '@logux/server'
6
- * async function outMap (action, meta) {
6
+ * async function onSend (action, meta) {
7
7
  * const filtered = { }
8
8
  * for (const i in meta) {
9
9
  * if (ALLOWED_META.includes(i)) {
@@ -13,13 +13,33 @@ import type {
13
13
  ServerConnection,
14
14
  TestTime
15
15
  } from '@logux/core'
16
- import type { Server as HTTPServer, IncomingMessage, ServerResponse } from 'http'
17
16
  import type { Unsubscribe } from 'nanoevents'
17
+ import type {
18
+ Server as HTTPServer,
19
+ IncomingMessage,
20
+ ServerResponse
21
+ } from 'node:http'
18
22
  import type { LogFn } from 'pino'
19
23
 
20
24
  import type { ChannelContext, Context } from '../context/index.js'
21
25
  import type { ServerClient } from '../server-client/index.js'
22
26
 
27
+ interface TypeOptions {
28
+ /**
29
+ * Name of the queue that will be used to process actions
30
+ * of the specified type. Default is 'main'
31
+ */
32
+ queue?: string
33
+ }
34
+
35
+ interface ChannelOptions {
36
+ /**
37
+ * Name of the queue that will be used to process channels
38
+ * with the specified name pattern. Default is 'main'
39
+ */
40
+ queue?: string
41
+ }
42
+
23
43
  export interface ServerMeta extends Meta {
24
44
  /**
25
45
  * All nodes subscribed to channel will receive the action.
@@ -446,7 +466,10 @@ type ActionCallbacks<
446
466
  TypeAction extends Action,
447
467
  Data extends object,
448
468
  Headers extends object
449
- > = (
469
+ > = {
470
+ finally?: ActionFinally<TypeAction, Data, Headers>
471
+ resend?: Resender<TypeAction, Data, Headers>
472
+ } & (
450
473
  | {
451
474
  access: Authorizer<TypeAction, Data, Headers>
452
475
  process?: Processor<TypeAction, Data, Headers>
@@ -454,17 +477,18 @@ type ActionCallbacks<
454
477
  | {
455
478
  accessAndProcess: Processor<TypeAction, Data, Headers>
456
479
  }
457
- ) & {
458
- finally?: ActionFinally<TypeAction, Data, Headers>
459
- resend?: Resender<TypeAction, Data, Headers>
460
- }
480
+ )
461
481
 
462
482
  type ChannelCallbacks<
463
483
  SubscribeAction extends Action,
464
484
  Data extends object,
465
485
  ChannelParams extends object | string[],
466
486
  Headers extends object
467
- > = (
487
+ > = {
488
+ filter?: FilterCreator<SubscribeAction, Data, ChannelParams, Headers>
489
+ finally?: ChannelFinally<SubscribeAction, Data, ChannelParams, Headers>
490
+ unsubscribe?: ChannelUnsubscribe<Data, ChannelParams, Headers>
491
+ } & (
468
492
  | {
469
493
  access: ChannelAuthorizer<SubscribeAction, Data, ChannelParams, Headers>
470
494
  load?: ChannelLoader<SubscribeAction, Data, ChannelParams, Headers>
@@ -477,11 +501,7 @@ type ChannelCallbacks<
477
501
  Headers
478
502
  >
479
503
  }
480
- ) & {
481
- filter?: FilterCreator<SubscribeAction, Data, ChannelParams, Headers>
482
- finally?: ChannelFinally<SubscribeAction, Data, ChannelParams, Headers>
483
- unsubscribe?: ChannelUnsubscribe<Data, ChannelParams, Headers>
484
- }
504
+ )
485
505
 
486
506
  interface ActionReporter {
487
507
  action: Readonly<Action>
@@ -505,6 +525,7 @@ interface AuthenticationReporter {
505
525
 
506
526
  interface ReportersArguments {
507
527
  add: ActionReporter
528
+ addClean: ActionReporter
508
529
  authenticated: AuthenticationReporter
509
530
  clean: CleanReporter
510
531
  clientError: {
@@ -803,6 +824,7 @@ export class BaseServer<
803
824
  *
804
825
  * @param pattern Pattern for channel name.
805
826
  * @param callbacks Callback during subscription process.
827
+ * @param options Additional options
806
828
  */
807
829
  channel<
808
830
  ChannelParams extends object = {},
@@ -810,12 +832,14 @@ export class BaseServer<
810
832
  SubscribeAction extends LoguxSubscribeAction = LoguxSubscribeAction
811
833
  >(
812
834
  pattern: string,
813
- callbacks: ChannelCallbacks<SubscribeAction, Data, ChannelParams, Headers>
835
+ callbacks: ChannelCallbacks<SubscribeAction, Data, ChannelParams, Headers>,
836
+ options?: ChannelOptions
814
837
  ): void
815
838
 
816
839
  /**
817
840
  * @param pattern Regular expression for channel name.
818
841
  * @param callbacks Callback during subscription process.
842
+ * @param options Additional options
819
843
  */
820
844
  channel<
821
845
  ChannelParams extends string[] = string[],
@@ -823,7 +847,8 @@ export class BaseServer<
823
847
  SubscribeAction extends LoguxSubscribeAction = LoguxSubscribeAction
824
848
  >(
825
849
  pattern: RegExp,
826
- callbacks: ChannelCallbacks<SubscribeAction, Data, ChannelParams, Headers>
850
+ callbacks: ChannelCallbacks<SubscribeAction, Data, ChannelParams, Headers>,
851
+ options?: ChannelOptions
827
852
  ): void
828
853
 
829
854
  /**
@@ -975,7 +1000,7 @@ export class BaseServer<
975
1000
  * @param listener Client listener.
976
1001
  */
977
1002
  on(
978
- event: 'authenticated',
1003
+ event: 'authenticated' | 'unauthenticated',
979
1004
  listener: (client: ServerClient, latencyMilliseconds: number) => void
980
1005
  ): Unsubscribe
981
1006
 
@@ -1009,7 +1034,8 @@ export class BaseServer<
1009
1034
  event: 'unsubscribed',
1010
1035
  listener: (
1011
1036
  action: LoguxUnsubscribeAction,
1012
- meta: Readonly<ServerMeta>
1037
+ meta: Readonly<ServerMeta>,
1038
+ clientNodeId: string
1013
1039
  ) => void
1014
1040
  ): Unsubscribe
1015
1041
 
@@ -1120,10 +1146,12 @@ export class BaseServer<
1120
1146
  /**
1121
1147
  * @param actionCreator Action creator function.
1122
1148
  * @param callbacks Callbacks for action created by creator.
1149
+ * @param options Additional options
1123
1150
  */
1124
1151
  type<Creator extends AbstractActionCreator, Data extends object = {}>(
1125
1152
  actionCreator: Creator,
1126
- callbacks: ActionCallbacks<ReturnType<Creator>, Data, Headers>
1153
+ callbacks: ActionCallbacks<ReturnType<Creator>, Data, Headers>,
1154
+ options?: TypeOptions
1127
1155
  ): void
1128
1156
 
1129
1157
  /**
@@ -1147,10 +1175,12 @@ export class BaseServer<
1147
1175
  *
1148
1176
  * @param name The action’s type or action’s type matching rule as RegExp..
1149
1177
  * @param callbacks Callbacks for actions with this type.
1178
+ * @param options Additional options
1150
1179
  */
1151
1180
  type<TypeAction extends Action = AnyAction, Data extends object = {}>(
1152
1181
  name: RegExp | TypeAction['type'],
1153
- callbacks: ActionCallbacks<TypeAction, Data, Headers>
1182
+ callbacks: ActionCallbacks<TypeAction, Data, Headers>,
1183
+ options?: TypeOptions
1154
1184
  ): void
1155
1185
 
1156
1186
  /**
@@ -1,10 +1,10 @@
1
1
  import { LoguxNotFoundError } from '@logux/actions'
2
2
  import { Log, MemoryStore, parseId, ServerConnection } from '@logux/core'
3
- import { promises as fs } from 'fs'
4
3
  import { createNanoEvents } from 'nanoevents'
5
4
  import { nanoid } from 'nanoid'
6
- import { dirname, join } from 'path'
7
- import { fileURLToPath } from 'url'
5
+ import { promises as fs } from 'node:fs'
6
+ import { dirname, join } from 'node:path'
7
+ import { fileURLToPath } from 'node:url'
8
8
  import UrlPattern from 'url-pattern'
9
9
  import { WebSocketServer } from 'ws'
10
10
 
@@ -185,7 +185,9 @@ export class BaseServer {
185
185
  this.emitter.emit('report', 'add', { action, meta })
186
186
  }
187
187
 
188
- if (this.destroying) return
188
+ if (this.destroying && !this.actionToQueue.has(meta.id)) {
189
+ return
190
+ }
189
191
 
190
192
  if (action.type === 'logux/subscribe') {
191
193
  if (meta.server === this.nodeId) {
@@ -238,7 +240,7 @@ export class BaseServer {
238
240
 
239
241
  if (meta.status === 'waiting') {
240
242
  if (!processor) {
241
- this.internalUnkownType(action, meta)
243
+ this.internalUnknownType(action, meta)
242
244
  return
243
245
  }
244
246
  if (processor.process) {
@@ -333,6 +335,10 @@ export class BaseServer {
333
335
  this.timeouts = {}
334
336
  this.lastTimeout = 0
335
337
 
338
+ this.typeToQueue = new Map()
339
+ this.queues = new Map()
340
+ this.actionToQueue = new Map()
341
+
336
342
  this.controls = {
337
343
  'GET /': {
338
344
  async request() {
@@ -355,24 +361,83 @@ export class BaseServer {
355
361
  this.listenNotes = {}
356
362
  bindBackendProxy(this)
357
363
 
364
+ let end = (actionId, queue, queueKey, ...args) => {
365
+ this.actionToQueue.delete(actionId)
366
+ if (queue.length() === 0) {
367
+ this.queues.delete(queueKey)
368
+ }
369
+ queue.next(...args)
370
+ }
371
+ let undoRemainingTasks = queue => {
372
+ let remainingTasks = queue.getQueue()
373
+ if (remainingTasks) {
374
+ for (let task of remainingTasks) {
375
+ this.undo(task.action, task.meta, 'error')
376
+ this.actionToQueue.delete(task.meta.id)
377
+ }
378
+ }
379
+ queue.killAndDrain()
380
+ }
381
+ this.on('error', (e, action, meta) => {
382
+ let queueKey = this.actionToQueue.get(meta?.id)
383
+ if (queueKey) {
384
+ let queue = this.queues.get(queueKey)
385
+ undoRemainingTasks(queue)
386
+ end(meta.id, queue, queueKey, e)
387
+ }
388
+ })
389
+ this.on('processed', (action, meta) => {
390
+ if (action.type === 'logux/undo') {
391
+ let queueKey = this.actionToQueue.get(action.id)
392
+ if (queueKey) {
393
+ let queue = this.queues.get(queueKey)
394
+ undoRemainingTasks(queue)
395
+ end(action.id, queue, queueKey, null, meta)
396
+ }
397
+ } else if (action.type === 'logux/processed') {
398
+ let queueKey = this.actionToQueue.get(action.id)
399
+ if (queueKey) {
400
+ let queue = this.queues.get(queueKey)
401
+ end(action.id, queue, queueKey, null, meta)
402
+ }
403
+ } else if (
404
+ action.type !== 'logux/subscribed' &&
405
+ action.type !== 'logux/unsubscribed'
406
+ ) {
407
+ let queueKey = this.actionToQueue.get(meta.id)
408
+ if (queueKey) {
409
+ let queue = this.queues.get(queueKey)
410
+ end(meta.id, queue, queueKey, null, meta)
411
+ }
412
+ }
413
+ })
414
+
358
415
  this.unbind.push(() => {
359
416
  for (let i of this.connected.values()) i.destroy()
360
417
  for (let i in this.timeouts) {
361
418
  clearTimeout(this.timeouts[i])
362
419
  }
363
420
  })
364
- this.unbind.push(
365
- () =>
366
- new Promise(resolve => {
367
- if (this.processing === 0) {
368
- resolve()
369
- } else {
370
- this.on('processed', () => {
371
- if (this.processing === 0) resolve()
372
- })
373
- }
421
+ this.unbind.push(() => {
422
+ return new Promise(resolve => {
423
+ if (this.processing === 0) {
424
+ resolve()
425
+ } else {
426
+ this.on('processed', () => {
427
+ if (this.processing === 0) resolve()
428
+ })
429
+ }
430
+ })
431
+ })
432
+ this.unbind.push(() => {
433
+ return Promise.allSettled(
434
+ [...this.queues.values()].map(queue => {
435
+ return new Promise(resolve => {
436
+ queue.drain = resolve
437
+ })
374
438
  })
375
- )
439
+ )
440
+ })
376
441
  }
377
442
 
378
443
  addClient(connection) {
@@ -409,7 +474,7 @@ export class BaseServer {
409
474
  return [undoAction, undoMeta]
410
475
  }
411
476
 
412
- channel(pattern, callbacks) {
477
+ channel(pattern, callbacks, options = {}) {
413
478
  normalizeChannelCallbacks(`Channel ${pattern}`, callbacks)
414
479
  let channel = Object.assign({}, callbacks)
415
480
  if (typeof pattern === 'string') {
@@ -419,6 +484,8 @@ export class BaseServer {
419
484
  } else {
420
485
  channel.regexp = pattern
421
486
  }
487
+
488
+ channel.queueName = options.queue || 'main'
422
489
  this.channels.push(channel)
423
490
  }
424
491
 
@@ -497,7 +564,7 @@ export class BaseServer {
497
564
  this.httpListener = listener
498
565
  }
499
566
 
500
- internalUnkownType(action, meta) {
567
+ internalUnknownType(action, meta) {
501
568
  this.contexts.delete(action)
502
569
  this.log.changeMeta(meta.id, { status: 'error' })
503
570
  this.emitter.emit('report', 'unknownType', {
@@ -657,7 +724,7 @@ export class BaseServer {
657
724
  }
658
725
  }
659
726
  }
660
- this.emitter.emit('unsubscribed', action, meta)
727
+ this.emitter.emit('unsubscribed', action, meta, clientNodeId)
661
728
  this.emitter.emit('report', 'unsubscribed', {
662
729
  actionId: meta.id,
663
730
  channel: action.channel
@@ -785,9 +852,10 @@ export class BaseServer {
785
852
  let ctx = this.createContext(action, meta)
786
853
  let client = this.clientIds.get(clientId)
787
854
  for (let filter of Object.values(subscriber.filters)) {
788
- filter = typeof filter === 'function'
789
- ? await filter(ctx, action, meta)
790
- : filter
855
+ filter =
856
+ typeof filter === 'function'
857
+ ? await filter(ctx, action, meta)
858
+ : filter
791
859
  if (filter && client) {
792
860
  ignoreClients.add(clientId)
793
861
  client.node.onAdd(action, meta)
@@ -924,7 +992,10 @@ export class BaseServer {
924
992
  if (!match) this.wrongChannel(action, meta)
925
993
  }
926
994
 
927
- type(name, callbacks) {
995
+ type(name, callbacks, options = {}) {
996
+ let queue = options.queue || 'main'
997
+ this.typeToQueue.set(name, queue)
998
+
928
999
  if (typeof name === 'function') name = name.type
929
1000
  normalizeTypeCallbacks(`Action type ${name}`, callbacks)
930
1001
 
@@ -946,7 +1017,7 @@ export class BaseServer {
946
1017
  }
947
1018
 
948
1019
  unknownType(action, meta) {
949
- this.internalUnkownType(action, meta)
1020
+ this.internalUnknownType(action, meta)
950
1021
  this.unknownTypes[meta.id] = true
951
1022
  }
952
1023
 
@@ -1,8 +1,8 @@
1
1
  import { LoguxError } from '@logux/core'
2
- import http from 'http'
3
- import https from 'https'
4
2
  import JSONStream from 'JSONStream'
5
3
  import { nanoid } from 'nanoid'
4
+ import http from 'node:http'
5
+ import https from 'node:https'
6
6
 
7
7
  const VERSION = 4
8
8
 
@@ -126,7 +126,7 @@ export function bindBackendProxy(app) {
126
126
  })
127
127
  )
128
128
  }
129
- let accessResolve, accessReject
129
+ let accessReject, accessResolve
130
130
  accessing.set(
131
131
  meta.id,
132
132
  new Promise((resolve, reject) => {
@@ -134,7 +134,7 @@ export function bindBackendProxy(app) {
134
134
  accessReject = reject
135
135
  })
136
136
  )
137
- let processResolve, processReject
137
+ let processReject, processResolve
138
138
  processing.set(
139
139
  meta.id,
140
140
  new Promise((resolve, reject) => {
@@ -1,7 +1,7 @@
1
- import { promises as fs } from 'fs'
2
- import http from 'http'
3
- import https from 'https'
4
- import { isAbsolute, join } from 'path'
1
+ import { promises as fs } from 'node:fs'
2
+ import http from 'node:http'
3
+ import https from 'node:https'
4
+ import { isAbsolute, join } from 'node:path'
5
5
 
6
6
  const PEM_PREAMBLE = '-----BEGIN'
7
7
 
@@ -1,8 +1,8 @@
1
- import os from 'os'
2
- import { dirname, join, sep } from 'path'
1
+ import os from 'node:os'
2
+ import { dirname, join, sep } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
3
4
  import pico from 'picocolors'
4
5
  import pino from 'pino'
5
- import { fileURLToPath } from 'url'
6
6
 
7
7
  const __dirname = dirname(fileURLToPath(import.meta.url))
8
8
  export const PATH_TO_PRETTIFYING_PINO_TRANSPORT = join(
@@ -16,24 +16,12 @@ export class FilteredNode extends ServerNode {
16
16
  delete this.received
17
17
  }
18
18
 
19
- async syncSinceQuery(lastSynced) {
20
- let data = { added: 0, entries: [] }
21
- await this.log.each({ order: 'added' }, (action, meta) => {
22
- if (meta.added <= lastSynced) {
23
- return false
24
- } else {
25
- if (
26
- (has(meta.clients, this.client.clientId) ||
27
- has(meta.nodes, this.client.nodeId) ||
28
- has(meta.users, this.client.userId)) &&
29
- !has(meta.excludeClients, this.client.clientId)
30
- ) {
31
- if (meta.added > data.added) data.added = meta.added
32
- data.entries.push([action, meta])
33
- }
34
- return true
35
- }
36
- })
37
- return data
19
+ syncFilter(action, meta) {
20
+ return (
21
+ (has(meta.clients, this.client.clientId) ||
22
+ has(meta.nodes, this.client.nodeId) ||
23
+ has(meta.users, this.client.userId)) &&
24
+ !has(meta.excludeClients, this.client.clientId)
25
+ )
38
26
  }
39
27
  }
@@ -1,11 +1,10 @@
1
- import { once } from 'events'
2
- import os from 'os'
1
+ import { once } from 'node:events'
2
+ import os from 'node:os'
3
+ import { Transform } from 'node:stream'
3
4
  import pico from 'picocolors'
4
5
  import pino from 'pino'
5
6
  import abstractTransport from 'pino-abstract-transport'
6
- import { Transform } from 'stream'
7
7
  import stripAnsi from 'strip-ansi'
8
- import yyyymmdd from 'yyyy-mm-dd'
9
8
 
10
9
  import { mulberry32, onceXmur3 } from './utils.js'
11
10
 
@@ -39,6 +38,17 @@ const LABELS = {
39
38
 
40
39
  const COLORS = ['red', 'green', 'yellow', 'blue', 'magenta', 'cyan']
41
40
 
41
+ function formatNow() {
42
+ let date = new Date()
43
+ let year = date.getFullYear()
44
+ let month = String(date.getMonth() + 1).padStart(2, '0')
45
+ let day = String(date.getDate()).padStart(2, '0')
46
+ let hour = String(date.getHours()).padStart(2, '0')
47
+ let minutes = String(date.getMinutes()).padStart(2, '0')
48
+ let seconds = String(date.getSeconds()).padStart(2, '0')
49
+ return `${year}-${month}-${day} ${hour}:${minutes}:${seconds}`
50
+ }
51
+
42
52
  function rightPag(str, length) {
43
53
  let add = length - stripAnsi(str).length
44
54
  for (let i = 0; i < add; i++) str += ' '
@@ -47,7 +57,7 @@ function rightPag(str, length) {
47
57
 
48
58
  function label(c, type, color, labelBg, labelText, message) {
49
59
  let pagged = rightPag(c[labelBg](c[labelText](type)), 8)
50
- let time = c.dim(`at ${yyyymmdd.withTime(new Date())}`)
60
+ let time = c.dim(`at ${formatNow()}`)
51
61
  let highlighted = message.replace(/`([^`]+)`/g, c.yellow('$1'))
52
62
  return `${pagged}${c.bold(c[color](highlighted))} ${time}`
53
63
  }
@@ -1,4 +1,4 @@
1
- import { resolve } from 'path'
1
+ import { resolve } from 'node:path'
2
2
  import { describe, expect, it } from 'vitest'
3
3
 
4
4
  import { loadOptions, number, oneOf } from './index.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logux/server",
3
- "version": "0.12.10",
3
+ "version": "0.13.0",
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.org/",
19
19
  "repository": "logux/server",
20
20
  "engines": {
21
- "node": "^14.0.0 || ^16.0.0 || >=18.0.0"
21
+ "node": "^18.0.0 || ^18.0.0 || >=22.0.0"
22
22
  },
23
23
  "type": "module",
24
24
  "types": "./index.d.ts",
@@ -27,24 +27,24 @@
27
27
  "./package.json": "./package.json"
28
28
  },
29
29
  "dependencies": {
30
- "@logux/actions": "^0.3.1",
31
- "@logux/core": "^0.8.5",
30
+ "@logux/actions": "^0.4.0",
31
+ "@logux/core": "^0.9.0",
32
32
  "JSONStream": "^1.3.5",
33
- "cookie": "^0.5.0",
34
- "dotenv": "^16.3.1",
35
- "fast-glob": "^3.3.1",
33
+ "cookie": "^0.6.0",
34
+ "dotenv": "^16.4.5",
35
+ "fast-glob": "^3.3.2",
36
+ "fastq": "^1.17.1",
36
37
  "nanodelay": "^2.0.2",
37
- "nanoevents": "^8.0.0",
38
- "nanoid": "^4.0.2",
38
+ "nanoevents": "^9.0.0",
39
+ "nanoid": "^5.0.7",
39
40
  "netmask": "^2.0.2",
40
41
  "node-fetch": "^3.3.2",
41
- "picocolors": "^1.0.0",
42
- "pino": "8.15.0",
43
- "pino-abstract-transport": "^1.0.0",
44
- "semver": "^7.5.4",
42
+ "picocolors": "^1.0.1",
43
+ "pino": "9.3.2",
44
+ "pino-abstract-transport": "^1.2.0",
45
+ "semver": "^7.6.3",
45
46
  "strip-ansi": "^7.1.0",
46
47
  "url-pattern": "^1.0.3",
47
- "ws": "^8.13.0",
48
- "yyyy-mm-dd": "^1.0.2"
48
+ "ws": "^8.18.0"
49
49
  }
50
50
  }
package/server/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import glob from 'fast-glob'
2
- import { join, relative } from 'path'
2
+ import { join, relative } from 'node:path'
3
3
  import pico from 'picocolors'
4
4
 
5
5
  import { BaseServer } from '../base-server/index.js'
@@ -1,11 +1,16 @@
1
1
  import { LoguxError, parseId } from '@logux/core'
2
2
  import cookie from 'cookie'
3
+ import fastq from 'fastq'
3
4
  import semver from 'semver'
4
5
 
5
6
  import { ALLOWED_META } from '../allowed-meta/index.js'
6
7
  import { filterMeta } from '../filter-meta/index.js'
7
8
  import { FilteredNode } from '../filtered-node/index.js'
8
9
 
10
+ async function onSend(action, meta) {
11
+ return [action, filterMeta(meta)]
12
+ }
13
+
9
14
  function reportDetails(client) {
10
15
  return {
11
16
  connectionId: client.key,
@@ -14,6 +19,51 @@ function reportDetails(client) {
14
19
  }
15
20
  }
16
21
 
22
+ function denyBack(app, clientId, action, meta) {
23
+ app.emitter.emit('report', 'denied', { actionId: meta.id })
24
+ let [undoAction, undoMeta] = app.buildUndo(action, meta, 'denied')
25
+ undoMeta.clients = (undoMeta.clients || []).concat([clientId])
26
+ app.log.add(undoAction, undoMeta)
27
+ app.debugActionError(meta, `Action "${meta.id}" was denied`)
28
+ }
29
+
30
+ async function queueWorker(task, next) {
31
+ let { action, app, clientId, meta, onReceiveResolve, queue } = task
32
+ queue.next = next
33
+
34
+ let type = action.type
35
+ if (type === 'logux/subscribe' || type === 'logux/unsubscribe') {
36
+ return onReceiveResolve([action, meta])
37
+ }
38
+
39
+ let processor = app.getProcessor(type)
40
+ if (!processor) {
41
+ app.internalUnknownType(action, meta)
42
+ return onReceiveResolve(false)
43
+ }
44
+
45
+ let ctx = app.createContext(action, meta)
46
+ try {
47
+ let result = await processor.access(ctx, action, meta)
48
+ if (app.unknownTypes[meta.id]) {
49
+ delete app.unknownTypes[meta.id]
50
+ app.finally(processor, ctx, action, meta)
51
+ return false
52
+ } else if (!result) {
53
+ app.finally(processor, ctx, action, meta)
54
+ denyBack(app, clientId, action, meta)
55
+ return onReceiveResolve(false)
56
+ } else {
57
+ return onReceiveResolve([action, meta])
58
+ }
59
+ } catch (e) {
60
+ app.undo(action, meta, 'error')
61
+ app.emitter.emit('error', e, action, meta)
62
+ app.finally(processor, ctx, action, meta)
63
+ return onReceiveResolve(false)
64
+ }
65
+ }
66
+
17
67
  export class ServerClient {
18
68
  constructor(app, connection, key) {
19
69
  this.app = app
@@ -33,9 +83,8 @@ export class ServerClient {
33
83
 
34
84
  this.node = new FilteredNode(this, app.nodeId, app.log, connection, {
35
85
  auth: this.auth.bind(this),
36
- inFilter: this.filter.bind(this),
37
- inMap: this.inMap.bind(this),
38
- outMap: this.outMap.bind(this),
86
+ onReceive: this.onReceive.bind(this),
87
+ onSend,
39
88
  ping: app.options.ping,
40
89
  subprotocol: app.options.subprotocol,
41
90
  timeout: app.options.timeout
@@ -77,6 +126,7 @@ export class ServerClient {
77
126
  }
78
127
 
79
128
  if (nodeId === 'server' || userId === 'server') {
129
+ this.app.emitter.emit('unauthenticated', this, 0)
80
130
  this.app.emitter.emit('report', 'unauthenticated', reportDetails(this))
81
131
  return false
82
132
  }
@@ -133,20 +183,13 @@ export class ServerClient {
133
183
  this.app.emitter.emit('authenticated', this, Date.now() - start)
134
184
  this.app.emitter.emit('report', 'authenticated', reportDetails(this))
135
185
  } else {
186
+ this.app.emitter.emit('unauthenticated', this, Date.now() - start)
136
187
  this.app.emitter.emit('report', 'unauthenticated', reportDetails(this))
137
188
  this.app.rememberBadAuth(this.remoteAddress)
138
189
  }
139
190
  return result
140
191
  }
141
192
 
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`)
148
- }
149
-
150
193
  destroy() {
151
194
  this.destroyed = true
152
195
  this.node.destroy()
@@ -162,9 +205,6 @@ export class ServerClient {
162
205
  }
163
206
  }
164
207
  if (this.clientId) {
165
- this.app.clientIds.delete(this.clientId)
166
- this.app.nodeIds.delete(this.nodeId)
167
-
168
208
  for (let channel in this.app.subscribers) {
169
209
  let subscriber = this.app.subscribers[channel][this.nodeId]
170
210
  if (subscriber) {
@@ -174,6 +214,8 @@ export class ServerClient {
174
214
  this.app.performUnsubscribe(this.nodeId, action, meta)
175
215
  }
176
216
  }
217
+ this.app.clientIds.delete(this.clientId)
218
+ this.app.nodeIds.delete(this.nodeId)
177
219
  }
178
220
  if (!this.app.destroying) {
179
221
  this.app.emitter.emit('disconnected', this)
@@ -181,61 +223,68 @@ export class ServerClient {
181
223
  this.app.connected.delete(this.key)
182
224
  }
183
225
 
184
- async filter(action, meta) {
185
- let ctx = this.app.createContext(action, meta)
226
+ isSubprotocol(range) {
227
+ return semver.satisfies(this.node.remoteSubprotocol, range)
228
+ }
229
+
230
+ onReceive(action, meta) {
231
+ if (this.app.actionToQueue.has(meta.id)) {
232
+ return Promise.resolve(false)
233
+ }
186
234
 
187
- let wrongUser = !this.clientId || this.clientId !== ctx.clientId
235
+ let actionClientId = parseId(meta.id).clientId
236
+ let wrongUser = !this.clientId || this.clientId !== actionClientId
188
237
  let wrongMeta = Object.keys(meta).some(i => !ALLOWED_META.includes(i))
189
238
  if (wrongUser || wrongMeta) {
190
- this.app.contexts.delete(action)
191
- this.denyBack(action, meta)
192
- return false
239
+ denyBack(this.app, this.clientId, action, meta)
240
+ return Promise.resolve(false)
193
241
  }
194
242
 
195
- let type = action.type
196
- if (type === 'logux/subscribe' || type === 'logux/unsubscribe') {
197
- return true
198
- }
243
+ return new Promise(resolve => {
244
+ let clientId = parseId(meta.id).clientId
245
+ let queueName = ''
199
246
 
200
- let processor = this.app.getProcessor(type)
201
- if (!processor) {
202
- this.app.internalUnkownType(action, meta)
203
- return false
204
- }
247
+ let isChannel =
248
+ (action.type === 'logux/subscribe' ||
249
+ action.type === 'logux/unsubscribe') &&
250
+ action.channel
205
251
 
206
- try {
207
- let result = await processor.access(ctx, action, meta)
208
- if (this.app.unknownTypes[meta.id]) {
209
- delete this.app.unknownTypes[meta.id]
210
- this.app.finally(processor, ctx, action, meta)
211
- return false
212
- } else if (!result) {
213
- this.app.finally(processor, ctx, action, meta)
214
- this.denyBack(action, meta)
215
- return false
252
+ if (isChannel) {
253
+ for (let channel of this.app.channels) {
254
+ let pattern = channel.regexp || channel.pattern.regex
255
+ if (action.channel.match(pattern)) {
256
+ queueName = channel.queue
257
+ break
258
+ }
259
+ }
216
260
  } else {
217
- return true
261
+ queueName = this.app.typeToQueue.get(action.type)
218
262
  }
219
- } catch (e) {
220
- this.app.undo(action, meta, 'error')
221
- this.app.emitter.emit('error', e, action, meta)
222
- this.app.finally(processor, ctx, action, meta)
223
- return false
224
- }
225
- }
226
263
 
227
- async inMap(action, meta) {
228
- if (!meta.subprotocol) {
229
- meta.subprotocol = this.node.remoteSubprotocol
230
- }
231
- return [action, meta]
232
- }
264
+ queueName = queueName || 'main'
265
+ let queueKey = `${clientId}/${queueName}`
266
+ let queue = this.app.queues.get(queueKey)
233
267
 
234
- isSubprotocol(range) {
235
- return semver.satisfies(this.node.remoteSubprotocol, range)
236
- }
268
+ if (!queue) {
269
+ queue = fastq(queueWorker, 1)
270
+ this.app.queues.set(queueKey, queue)
271
+ }
272
+
273
+ if (!meta.subprotocol) {
274
+ meta.subprotocol = this.node.remoteSubprotocol
275
+ }
237
276
 
238
- async outMap(action, meta) {
239
- return [action, filterMeta(meta)]
277
+ this.app.actionToQueue.set(meta.id, queueKey)
278
+ queue.push({
279
+ action,
280
+ app: this.app,
281
+ clientId,
282
+ meta,
283
+ onReceiveResolve: result => {
284
+ resolve(result)
285
+ },
286
+ queue
287
+ })
288
+ })
240
289
  }
241
290
  }
@@ -18,7 +18,7 @@ export class TestClient {
18
18
  this.node = new ClientNode(this.nodeId, this.log, this.pair.left, {
19
19
  ...opts,
20
20
  fixTime: false,
21
- outMap: (action, meta) => {
21
+ onSend(action, meta) {
22
22
  return [action, filterMeta(meta)]
23
23
  }
24
24
  })
package/vite.config.ts CHANGED
@@ -3,9 +3,21 @@ import { defineConfig } from 'vitest/config'
3
3
  export default defineConfig({
4
4
  test: {
5
5
  coverage: {
6
- exclude: ['node_modules', 'server/index.js', '*/*.test.*'],
7
- lines: 100,
8
- provider: 'v8'
6
+ exclude: [
7
+ 'node_modules',
8
+ 'server/index.js',
9
+ 'test/*',
10
+ '**/*.d.ts',
11
+ '**/*.test.ts',
12
+ '*/errors.ts',
13
+ '*/types.ts',
14
+ '*.config.*',
15
+ 'human-formatter'
16
+ ],
17
+ provider: 'v8',
18
+ thresholds: {
19
+ lines: 100
20
+ }
9
21
  },
10
22
  environment: 'node',
11
23
  exclude: ['node_modules', 'test/servers']