@logux/server 0.16.2 → 0.17.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,4 +1,3 @@
1
- import { LoguxNotFoundError } from '@logux/actions'
2
1
  import { Log, MemoryStore, parseId, ServerConnection } from '@logux/core'
3
2
  import { createNanoEvents } from 'nanoevents'
4
3
  import { nanoid } from 'nanoid'
@@ -8,42 +7,34 @@ import { fileURLToPath } from 'node:url'
8
7
  import { WebSocketServer } from 'ws'
9
8
 
10
9
  import { addHttpPages } from '../add-http-pages/index.js'
11
- import { Context } from '../context/index.js'
12
10
  import { createHttpServer } from '../create-http-server/index.js'
11
+ import {
12
+ ActionRunner,
13
+ getRegexType,
14
+ normalizeChannel,
15
+ normalizeType,
16
+ wasNot403
17
+ } from '../processing/index.js'
18
+ import { Publisher } from '../publishing/index.js'
13
19
  import { ServerClient } from '../server-client/index.js'
14
20
  import { createPattern } from '../url-pattern/index.js'
15
21
 
16
- const SKIP_PROCESS = Symbol('skipProcess')
17
22
  const RESEND_META = ['channels', 'users', 'clients', 'nodes']
18
23
 
19
- function addTarget(targets, client, action, meta) {
20
- let entries = targets.get(client)
21
- if (entries) {
22
- entries.push([action, meta])
23
- } else {
24
- targets.set(client, [[action, meta]])
25
- }
26
- }
27
-
28
- function sendTargets(app, targets, settled) {
29
- for (let [client, entries] of targets) {
30
- let sending = client.node.onAdd(entries)
31
- // Only `ServerClient` tracks sending for `Server#drain()`
32
- if (client.track) client.track(sending)
33
- }
34
- if (settled > 0) {
35
- for (let client of app.connected.values()) {
36
- if (!targets.has(client) && client.node.lastAddedCache < settled) {
37
- client.node.lastAddedCache = settled
38
- }
39
- }
40
- }
41
- targets.clear()
42
- }
43
-
44
- function maxAdded(settled, meta) {
45
- if (typeof meta.added === 'undefined' || meta.added < settled) return settled
46
- return meta.added
24
+ // The reports and the debug messages of the failures, which have a reason
25
+ const FAILURES = {
26
+ denied: (action, meta) => [
27
+ { actionId: meta.id },
28
+ `Action "${meta.id}" was denied`
29
+ ],
30
+ unknownType: (action, meta) => [
31
+ { actionId: meta.id, type: action.type },
32
+ `Action with unknown type ${action.type}`
33
+ ],
34
+ wrongChannel: (action, meta) => [
35
+ { actionId: meta.id, channel: action.channel },
36
+ `Wrong channel name ${action.channel}`
37
+ ]
47
38
  }
48
39
 
49
40
  function optionError(msg) {
@@ -53,73 +44,15 @@ function optionError(msg) {
53
44
  throw error
54
45
  }
55
46
 
56
- export async function wasNot403(cb) {
57
- try {
58
- await cb()
59
- return true
60
- } catch (e) {
61
- if (e.name === 'ResponseError' && e.statusCode === 403) {
62
- return false
63
- }
64
- throw e
65
- }
66
- }
67
-
68
- function normalizeTypeCallbacks(name, callbacks) {
69
- if (callbacks && callbacks.accessAndProcess) {
70
- callbacks.access = (ctx, ...args) => {
71
- return wasNot403(async () => {
72
- await callbacks.accessAndProcess(ctx, ...args)
73
- ctx[SKIP_PROCESS] = true
74
- })
75
- }
76
- callbacks.process = async (ctx, ...args) => {
77
- if (!ctx[SKIP_PROCESS]) await callbacks.accessAndProcess(ctx, ...args)
78
- }
79
- }
80
- if (!callbacks || !callbacks.access) {
81
- throw new Error(`${name} must have access callback`)
82
- }
83
- }
84
-
85
- function normalizeChannelCallbacks(pattern, callbacks) {
86
- if (callbacks && callbacks.accessAndLoad) {
87
- callbacks.access = (ctx, ...args) => {
88
- return wasNot403(async () => {
89
- try {
90
- ctx.data.load = await callbacks.accessAndLoad(ctx, ...args)
91
- } catch (e) {
92
- if (e.name === 'LoguxNotFoundError') {
93
- ctx.data.notFound = true
94
- } else if (e.name === 'ResponseError' && e.statusCode === 404) {
95
- ctx.data.notFound = true
96
- } else {
97
- throw e
98
- }
99
- }
100
- })
101
- }
102
- callbacks.load = ctx => {
103
- if (ctx.data.notFound) {
104
- throw new LoguxNotFoundError()
105
- } else {
106
- return ctx.data.load
107
- }
108
- }
109
- }
110
- if (!callbacks || !callbacks.access) {
111
- throw new Error(`Channel ${pattern} must have access callback`)
112
- }
113
- }
114
-
115
- function subscriberFilterId(action) {
116
- return JSON.stringify(action.filter || {})
117
- }
47
+ export { wasNot403 }
118
48
 
119
49
  export class BaseServer {
120
50
  constructor(opts = {}) {
121
51
  this.options = opts
122
52
  this.env = this.options.env || process.env.NODE_ENV || 'development'
53
+ if (typeof this.options.queueTimeout === 'undefined') {
54
+ this.options.queueTimeout = 5 * 60 * 1000
55
+ }
123
56
 
124
57
  if (typeof this.options.subprotocol === 'undefined') {
125
58
  throw optionError('Missed `subprotocol` option in server constructor')
@@ -161,36 +94,20 @@ export class BaseServer {
161
94
  }
162
95
 
163
96
  this.logger = console
164
-
165
- this.contexts = new WeakMap()
166
97
  this.log = log
167
98
 
168
99
  let cleaned = {}
169
100
 
170
101
  this.on('preadd', (action, meta) => {
171
- let isLogux = action.type.slice(0, 6) === 'logux/'
172
- if (!meta.server) {
173
- meta.server = this.nodeId
174
- }
175
- if (!meta.status && !isLogux) {
176
- meta.status = 'waiting'
177
- }
178
- if (meta.id.split(' ')[1] === this.nodeId) {
179
- if (!meta.subprotocol) {
180
- meta.subprotocol = this.options.subprotocol
181
- }
182
- if (
183
- !isLogux &&
184
- !this.types[action.type] &&
185
- !this.getRegexProcessor(action.type)
186
- ) {
187
- meta.status = 'processed'
188
- }
102
+ this.prepareMeta(meta)
103
+ // TODO: Remove the warning in next major
104
+ let old = ['channel', 'client', 'node', 'user'].find(key => key in meta)
105
+ if (old) {
106
+ let message = `Replace meta.${old} with meta.${old}s`
107
+ this.logger.warn({ actionId: meta.id }, message)
108
+ this.debugActionError(meta, message)
189
109
  }
190
- this.replaceResendShortcuts(meta)
191
110
  })
192
- // Reports are per action, `clean` for a reason-less action is emitted
193
- // by the log before `batch` event
194
111
  this.on('add', (action, meta) => {
195
112
  if (meta.reasons.length === 0) {
196
113
  cleaned[meta.id] = true
@@ -199,55 +116,6 @@ export class BaseServer {
199
116
  this.emitter.emit('report', 'add', { action, meta })
200
117
  }
201
118
  })
202
- this.on('batch', entries => {
203
- let start = Date.now()
204
- let ready = []
205
-
206
- for (let [action, meta] of entries) {
207
- if (this.destroying && !this.actionToQueue.has(meta.id)) {
208
- continue
209
- }
210
-
211
- if (action.type === 'logux/subscribe') {
212
- if (meta.server === this.nodeId) {
213
- void this.subscribeAction(action, meta, start)
214
- }
215
- continue
216
- }
217
-
218
- if (action.type === 'logux/unsubscribe') {
219
- if (meta.server === this.nodeId) {
220
- this.unsubscribeAction(action, meta)
221
- }
222
- continue
223
- }
224
-
225
- let processor = this.getProcessor(action.type)
226
- if (processor && processor.resend && meta.status === 'waiting') {
227
- // Resending is asynchronous, so the action can’t wait for others
228
- void this.resendAction(action, meta, processor, start)
229
- continue
230
- }
231
-
232
- if (this.isUseless(action, meta)) {
233
- this.emitter.emit('report', 'useless', { action, meta })
234
- }
235
- ready.push([action, meta, processor])
236
- }
237
-
238
- if (ready.length === 0) return
239
- let runProcessors = () => {
240
- for (let [action, meta, processor] of ready) {
241
- this.runProcessor(action, meta, processor, start)
242
- }
243
- }
244
- // Actions are processed only after they were sent to the clients
245
- void Promise.resolve(this.sendBatch(ready)).then(runProcessors, error => {
246
- // Do not stop action processing on a broken channel filter
247
- runProcessors()
248
- throw error
249
- })
250
- })
251
119
  this.on('clean', (action, meta) => {
252
120
  if (cleaned[meta.id]) {
253
121
  delete cleaned[meta.id]
@@ -309,7 +177,6 @@ export class BaseServer {
309
177
  this.userIds = new Map()
310
178
  this.types = {}
311
179
  this.regexTypes = new Map()
312
- this.processing = 0
313
180
 
314
181
  this.lastClient = 0
315
182
 
@@ -317,101 +184,54 @@ export class BaseServer {
317
184
  this.subscribers = {}
318
185
 
319
186
  this.authAttempts = {}
320
- this.unknownTypes = {}
321
- this.wrongChannels = {}
322
187
 
323
188
  this.timeouts = {}
324
189
  this.lastTimeout = 0
325
190
 
326
- this.typeToQueue = new Map()
327
- this.queues = new Map()
328
- this.actionToQueue = new Map()
191
+ this.publisher = new Publisher(this)
192
+ this.runner = new ActionRunner(this)
329
193
 
330
- this.httpListeners = {}
194
+ this.on('batch', entries => {
195
+ for (let [action, meta] of entries) {
196
+ if (this.isUseless(action, meta)) {
197
+ this.emitter.emit('report', 'useless', { action, meta })
198
+ }
199
+ }
200
+ this.publisher.track(entries).catch(e => {
201
+ this.emitter.emit('error', e)
202
+ })
203
+ })
204
+
205
+ this.httpListeners = []
331
206
  this.httpAllListeners = []
207
+ this.httpNotFoundListener = undefined
332
208
  addHttpPages(this)
333
209
 
334
210
  this.listenNotes = {}
335
211
 
336
- let end = (actionId, queue, queueKey, ...args) => {
337
- this.actionToQueue.delete(actionId)
338
- if (queue.length() === 0) {
339
- this.queues.delete(queueKey)
340
- }
341
- queue.next(...args)
342
- }
343
- let undoRemainingTasks = queue => {
344
- let remainingTasks = queue.getQueue()
345
- if (remainingTasks) {
346
- for (let task of remainingTasks) {
347
- this.undo(task.action, task.meta, 'error')
348
- this.actionToQueue.delete(task.meta.id)
349
- }
350
- }
351
- queue.killAndDrain()
352
- }
353
- this.on('error', (e, action, meta) => {
354
- let queueKey = this.actionToQueue.get(meta?.id)
355
- if (queueKey) {
356
- let queue = this.queues.get(queueKey)
357
- undoRemainingTasks(queue)
358
- end(meta.id, queue, queueKey, e)
359
- }
360
- })
361
- this.on('processed', (action, meta) => {
362
- if (action.type === 'logux/undo') {
363
- let queueKey = this.actionToQueue.get(action.id)
364
- if (queueKey) {
365
- let queue = this.queues.get(queueKey)
366
- undoRemainingTasks(queue)
367
- end(action.id, queue, queueKey, null, meta)
368
- }
369
- } else if (action.type === 'logux/processed') {
370
- let queueKey = this.actionToQueue.get(action.id)
371
- if (queueKey) {
372
- let queue = this.queues.get(queueKey)
373
- end(action.id, queue, queueKey, null, meta)
374
- }
375
- } else if (
376
- action.type !== 'logux/subscribed' &&
377
- action.type !== 'logux/unsubscribed'
378
- ) {
379
- let queueKey = this.actionToQueue.get(meta.id)
380
- if (queueKey) {
381
- let queue = this.queues.get(queueKey)
382
- end(meta.id, queue, queueKey, null, meta)
383
- }
384
- }
385
- })
386
-
387
212
  this.unbind.push(() => {
388
213
  for (let i of this.connected.values()) i.destroy()
389
214
  for (let i in this.timeouts) {
390
215
  clearTimeout(this.timeouts[i])
391
216
  }
392
217
  })
393
- this.unbind.push(() => {
394
- return new Promise(resolve => {
395
- if (this.processing === 0) {
396
- resolve()
397
- } else {
398
- this.on('processed', () => {
399
- if (this.processing === 0) resolve()
400
- })
401
- }
402
- })
403
- })
404
- this.unbind.push(() => {
405
- return Promise.allSettled(
406
- [...this.queues.values()].map(queue => {
407
- return new Promise(resolve => {
408
- queue.drain = resolve
409
- })
218
+ this.unbind.push(async () => {
219
+ await this.runner.waitForTasks()
220
+ await this.publisher.settled()
221
+ // The callbacks, which exceeded `queueTimeout`, can still be running
222
+ if (this.runner.detached.size > 0) {
223
+ this.emitter.emit('report', 'destroyDetached', {
224
+ actions: this.runner.detached.size
410
225
  })
411
- )
226
+ }
412
227
  })
413
228
  }
414
229
 
230
+ handleClient(ws, req) {
231
+ ws.upgradeReq = req
232
+ this.addClient(new ServerConnection(ws))
233
+ }
234
+
415
235
  addClient(connection) {
416
236
  this.lastClient += 1
417
237
  let key = this.lastClient.toString()
@@ -424,50 +244,20 @@ export class BaseServer {
424
244
  this.authenticator = authenticator
425
245
  }
426
246
 
427
- buildUndo(action, meta, reason, extra) {
428
- let undoMeta = { status: 'processed' }
429
-
430
- if (meta.users) undoMeta.users = meta.users.slice(0)
431
- if (meta.nodes) undoMeta.nodes = meta.nodes.slice(0)
432
- if (meta.clients) undoMeta.clients = meta.clients.slice(0)
433
- if (meta.reasons) undoMeta.reasons = meta.reasons.slice(0)
434
- if (meta.channels) undoMeta.channels = meta.channels.slice(0)
435
- if (meta.excludeClients) {
436
- undoMeta.excludeClients = meta.excludeClients.slice(0)
437
- }
438
-
439
- let undoAction = {
440
- ...extra,
441
- action,
442
- id: meta.id,
443
- reason,
444
- type: 'logux/undo'
445
- }
446
- return [undoAction, undoMeta]
447
- }
448
-
449
247
  channel(pattern, callbacks, options = {}) {
450
- normalizeChannelCallbacks(`Channel ${pattern}`, callbacks)
451
- let channel = Object.assign({}, callbacks)
248
+ let channel = normalizeChannel(
249
+ `Channel ${pattern}`,
250
+ callbacks,
251
+ options.queue || 'main'
252
+ )
452
253
  if (typeof pattern === 'string') {
453
254
  channel.pattern = createPattern(pattern)
454
255
  } else {
455
256
  channel.regexp = pattern
456
257
  }
457
-
458
- channel.queueName = options.queue || 'main'
459
258
  this.channels.push(channel)
460
259
  }
461
260
 
462
- createContext(action, meta) {
463
- let context = this.contexts.get(action)
464
- if (!context) {
465
- context = new Context(this, meta)
466
- this.contexts.set(action, context)
467
- }
468
- return context
469
- }
470
-
471
261
  debugActionError(meta, msg) {
472
262
  if (this.env === 'development') {
473
263
  let clientId = parseId(meta.id).clientId
@@ -487,12 +277,6 @@ export class BaseServer {
487
277
  }
488
278
  }
489
279
 
490
- denyAction(action, meta) {
491
- this.emitter.emit('report', 'denied', { actionId: meta.id })
492
- this.undo(action, meta, 'denied')
493
- this.debugActionError(meta, `Action "${meta.id}" was denied`)
494
- }
495
-
496
280
  destroy() {
497
281
  this.destroying = true
498
282
  this.emitter.emit('report', 'destroy')
@@ -505,37 +289,6 @@ export class BaseServer {
505
289
  return client.drain()
506
290
  }
507
291
 
508
- finally(processor, ctx, action, meta) {
509
- this.contexts.delete(action)
510
- if (processor && processor.finally) {
511
- try {
512
- processor.finally(ctx, action, meta)
513
- } catch (err) {
514
- this.emitter.emit('error', err, action, meta)
515
- }
516
- }
517
- }
518
-
519
- getProcessor(type) {
520
- return (
521
- this.types[type] || this.getRegexProcessor(type) || this.otherProcessor
522
- )
523
- }
524
-
525
- getRegexProcessor(type) {
526
- for (let regexp of this.regexTypes.keys()) {
527
- if (type.match(regexp) !== null) {
528
- return this.regexTypes.get(regexp)
529
- }
530
- }
531
- return undefined
532
- }
533
-
534
- handleClient(ws, req) {
535
- ws.upgradeReq = req
536
- this.addClient(new ServerConnection(ws))
537
- }
538
-
539
292
  http(method, url, listener) {
540
293
  if (this.options.disableHttpServer) {
541
294
  throw new Error(
@@ -545,31 +298,26 @@ export class BaseServer {
545
298
  if (!url) {
546
299
  this.httpAllListeners.push(method)
547
300
  } else {
548
- this.httpListeners[`${method} ${url}`] = listener
301
+ let route = { listener, match: createPattern(url), method, url }
302
+ let same = this.httpListeners.findIndex(i => {
303
+ return i.method === method && i.url === url
304
+ })
305
+ if (same === -1) {
306
+ this.httpListeners.push(route)
307
+ } else {
308
+ this.httpListeners[same] = route
309
+ }
549
310
  }
550
311
  }
551
312
 
552
- internalUnknownType(action, meta) {
553
- this.contexts.delete(action)
554
- this.log.changeMeta(meta.id, { status: 'error' })
555
- this.emitter.emit('report', 'unknownType', {
556
- actionId: meta.id,
557
- type: action.type
558
- })
559
- if (parseId(meta.id).userId !== 'server') {
560
- this.undo(action, meta, 'unknownType')
313
+ httpNotFound(listener) {
314
+ if (this.options.disableHttpServer) {
315
+ throw new Error(
316
+ '`server.httpNotFound()` can not be called when ' +
317
+ '`disableHttpServer` enabled'
318
+ )
561
319
  }
562
- this.debugActionError(meta, `Action with unknown type ${action.type}`)
563
- }
564
-
565
- internalWrongChannel(action, meta) {
566
- this.contexts.delete(action)
567
- this.emitter.emit('report', 'wrongChannel', {
568
- actionId: meta.id,
569
- channel: action.channel
570
- })
571
- this.undo(action, meta, 'wrongChannel')
572
- this.debugActionError(meta, `Wrong channel name ${action.channel}`)
320
+ this.httpNotFoundListener = listener
573
321
  }
574
322
 
575
323
  isBruteforce(ip) {
@@ -578,14 +326,10 @@ export class BaseServer {
578
326
  }
579
327
 
580
328
  isUseless(action, meta) {
581
- if (
582
- meta.status !== 'processed' ||
583
- this.types[action.type] ||
584
- this.getRegexProcessor(action.type)
585
- ) {
586
- return false
587
- }
588
- for (let i of ['channels', 'nodes', 'clients', 'users']) {
329
+ if (action.type.startsWith('logux/')) return false
330
+ if (this.otherProcessor) return false
331
+ if (this.types[action.type] || getRegexType(this, action.type)) return false
332
+ for (let i of RESEND_META) {
589
333
  if (Array.isArray(meta[i]) && meta[i].length > 0) return false
590
334
  }
591
335
  return true
@@ -654,17 +398,6 @@ export class BaseServer {
654
398
  })
655
399
  }
656
400
 
657
- markAsProcessed(meta) {
658
- this.log.changeMeta(meta.id, { status: 'processed' })
659
- let data = parseId(meta.id)
660
- if (data.userId !== 'server') {
661
- this.log.add(
662
- { id: meta.id, type: 'logux/processed' },
663
- { clients: [data.clientId], status: 'processed' }
664
- )
665
- }
666
- }
667
-
668
401
  on(event, listener) {
669
402
  if (
670
403
  event === 'preadd' ||
@@ -679,11 +412,10 @@ export class BaseServer {
679
412
  }
680
413
 
681
414
  otherChannel(callbacks) {
682
- normalizeChannelCallbacks('Unknown channel', callbacks)
683
415
  if (this.otherSubscriber) {
684
416
  throw new Error('Callbacks for unknown channel are already defined')
685
417
  }
686
- let channel = Object.assign({}, callbacks)
418
+ let channel = normalizeChannel('Unknown channel', callbacks, 'main')
687
419
  channel.pattern = name => [name]
688
420
  this.otherSubscriber = channel
689
421
  }
@@ -692,75 +424,30 @@ export class BaseServer {
692
424
  if (this.otherProcessor) {
693
425
  throw new Error('Callbacks for unknown types are already defined')
694
426
  }
695
- normalizeTypeCallbacks('Unknown type', callbacks)
696
- this.otherProcessor = callbacks
427
+ this.otherProcessor = normalizeType('Unknown type', callbacks, 'main')
697
428
  }
698
429
 
699
- performUnsubscribe(clientNodeId, action, meta) {
700
- if (action.channel === '__proto__' || clientNodeId === '__proto__') return
701
- if (this.subscribers[action.channel]) {
702
- let subscriber = this.subscribers[action.channel][clientNodeId]
703
- if (subscriber) {
704
- if (subscriber.unsubscribe) {
705
- subscriber.unsubscribe(action, meta)
706
- this.contexts.delete(action)
707
- }
708
- let filterId = subscriberFilterId(action)
709
- delete subscriber.filters[filterId]
710
- if (Object.keys(subscriber.filters).length === 0) {
711
- delete this.subscribers[action.channel][clientNodeId]
712
- }
713
- if (Object.keys(this.subscribers[action.channel]).length === 0) {
714
- delete this.subscribers[action.channel]
715
- }
430
+ // The callbacks need the ID, time, server and subprotocol
431
+ prepareMeta(meta, client) {
432
+ if (typeof meta.id === 'undefined') {
433
+ meta.id = this.log.generateId()
434
+ if (typeof meta.time === 'undefined') meta.time = this.log.lastTime
435
+ } else if (typeof meta.time === 'undefined') {
436
+ meta.time = this.log.now()
437
+ }
438
+ if (typeof meta.reasons === 'undefined') meta.reasons = []
439
+ if (!meta.server) meta.server = this.nodeId
440
+ if (!meta.subprotocol) {
441
+ if (meta.id.split(' ')[1] === this.nodeId) {
442
+ meta.subprotocol = this.options.subprotocol
443
+ } else if (client) {
444
+ meta.subprotocol = client.node.remoteSubprotocol
716
445
  }
717
446
  }
718
- this.emitter.emit('unsubscribed', action, meta, clientNodeId)
719
- this.emitter.emit('report', 'unsubscribed', {
720
- actionId: meta.id,
721
- channel: action.channel
722
- })
723
447
  }
724
448
 
725
449
  process(action, meta = {}) {
726
- return new Promise((resolve, reject) => {
727
- let unbindError = this.on('error', (e, errorAction) => {
728
- if (errorAction === action) {
729
- unbindError()
730
- unbindProcessed()
731
- reject(e)
732
- }
733
- })
734
- let unbindProcessed = this.on('processed', (processed, processedMeta) => {
735
- if (processed === action) {
736
- unbindError()
737
- unbindProcessed()
738
- resolve(processedMeta)
739
- }
740
- })
741
- this.log.add(action, meta)
742
- })
743
- }
744
-
745
- async processAction(processor, action, meta, start) {
746
- let ctx = this.createContext(action, meta)
747
-
748
- let latency
749
- this.processing += 1
750
- try {
751
- await processor.process(ctx, action, meta)
752
- latency = Date.now() - start
753
- this.markAsProcessed(meta)
754
- } catch (e) {
755
- this.log.changeMeta(meta.id, { status: 'error' })
756
- this.undo(action, meta, 'error')
757
- this.emitter.emit('error', e, action, meta)
758
- } finally {
759
- this.finally(processor, ctx, action, meta)
760
- }
761
- if (typeof latency === 'undefined') latency = Date.now() - start
762
- this.processing -= 1
763
- this.emitter.emit('processed', action, meta, latency)
450
+ return this.runner.submit({ action, meta, trusted: true })
764
451
  }
765
452
 
766
453
  async processHttp(req, res) {
@@ -769,24 +456,46 @@ export class BaseServer {
769
456
  urlString = decodeURIComponent(urlString)
770
457
  }
771
458
  let reqUrl = new URL(urlString, 'http://localhost')
772
- let rule = this.httpListeners[req.method + ' ' + reqUrl.pathname]
773
-
774
- if (!rule) {
775
- let processed = false
776
- for (let listener of this.httpAllListeners) {
777
- let result = await listener(req, res)
778
- if (result === true) {
779
- processed = true
780
- break
781
- }
782
- }
783
- if (!processed) {
784
- res.writeHead(404, { 'Content-Type': 'text/plain' })
785
- res.end('Not found\n')
459
+
460
+ // Listeners are checked in the order they were added, like in Express
461
+ for (let route of this.httpListeners) {
462
+ if (route.method === req.method && route.match(reqUrl.pathname)) {
463
+ await route.listener(req, res)
464
+ return
786
465
  }
466
+ }
467
+
468
+ for (let listener of this.httpAllListeners) {
469
+ let result = await listener(req, res)
470
+ if (result === true) return
471
+ }
472
+
473
+ if (this.httpNotFoundListener) {
474
+ await this.httpNotFoundListener(req, res)
787
475
  } else {
788
- await rule(req, res)
476
+ res.writeHead(404, { 'Content-Type': 'text/plain' })
477
+ res.end('Not found\n')
478
+ }
479
+ }
480
+
481
+ // `clientId` differs from the action’s author only for the actions,
482
+ // which were sent with a foreign ID
483
+ publishUndo(action, meta, reason, extra, clientId) {
484
+ let undoMeta = {}
485
+ if (meta.users) undoMeta.users = meta.users.slice(0)
486
+ if (meta.nodes) undoMeta.nodes = meta.nodes.slice(0)
487
+ if (meta.reasons) undoMeta.reasons = meta.reasons.slice(0)
488
+ if (meta.channels) undoMeta.channels = meta.channels.slice(0)
489
+ if (meta.excludeClients) {
490
+ undoMeta.excludeClients = meta.excludeClients.slice(0)
789
491
  }
492
+ undoMeta.clients = (meta.clients || []).concat([
493
+ clientId || parseId(meta.id).clientId
494
+ ])
495
+ return this.log.add(
496
+ { ...extra, action, id: meta.id, reason, type: 'logux/undo' },
497
+ undoMeta
498
+ )
790
499
  }
791
500
 
792
501
  rememberBadAuth(ip) {
@@ -800,172 +509,8 @@ export class BaseServer {
800
509
  }, 3000)
801
510
  }
802
511
 
803
- replaceResendShortcuts(meta) {
804
- if (meta.channel) {
805
- meta.channels = [meta.channel]
806
- delete meta.channel
807
- }
808
- if (meta.user) {
809
- meta.users = [meta.user]
810
- delete meta.user
811
- }
812
- if (meta.client) {
813
- meta.clients = [meta.client]
814
- delete meta.client
815
- }
816
- if (meta.node) {
817
- meta.nodes = [meta.node]
818
- delete meta.node
819
- }
820
- }
821
-
822
- async resendAction(action, meta, processor, start) {
823
- let ctx = this.createContext(action, meta)
824
- let resend
825
- try {
826
- resend = await processor.resend(ctx, action, meta)
827
- } catch (e) {
828
- this.undo(action, meta, 'error')
829
- this.emitter.emit('error', e, action, meta)
830
- this.finally(processor, ctx, action, meta)
831
- return
832
- }
833
- if (resend) {
834
- if (typeof resend === 'string') {
835
- resend = { channels: [resend] }
836
- } else if (Array.isArray(resend)) {
837
- resend = { channels: resend }
838
- } else {
839
- this.replaceResendShortcuts(resend)
840
- }
841
- let diff = {}
842
- for (let i of RESEND_META) {
843
- if (resend[i]) diff[i] = resend[i]
844
- }
845
- await this.log.changeMeta(meta.id, diff)
846
- meta = { ...meta, ...diff }
847
- }
848
-
849
- await this.sendAction(action, meta)
850
- this.runProcessor(action, meta, processor, start)
851
- }
852
-
853
- resolveTargets(action, meta, targets) {
854
- let from = parseId(meta.id).clientId
855
- let ignoreClients = new Set(meta.excludeClients || [])
856
- ignoreClients.add(from)
857
-
858
- if (meta.nodes) {
859
- for (let id of meta.nodes) {
860
- let client = this.nodeIds.get(id)
861
- if (client) {
862
- ignoreClients.add(client.clientId)
863
- addTarget(targets, client, action, meta)
864
- }
865
- }
866
- }
867
-
868
- if (meta.clients) {
869
- for (let id of meta.clients) {
870
- if (this.clientIds.has(id)) {
871
- let client = this.clientIds.get(id)
872
- ignoreClients.add(client.clientId)
873
- addTarget(targets, client, action, meta)
874
- }
875
- }
876
- }
877
-
878
- if (meta.users) {
879
- for (let userId of meta.users) {
880
- let users = this.userIds.get(userId)
881
- if (users) {
882
- for (let client of users) {
883
- if (!ignoreClients.has(client.clientId)) {
884
- ignoreClients.add(client.clientId)
885
- addTarget(targets, client, action, meta)
886
- }
887
- }
888
- }
889
- }
890
- }
891
-
892
- if (!meta.channels) return undefined
893
-
894
- let waiting = []
895
- for (let channel of meta.channels) {
896
- if (this.subscribers[channel]) {
897
- for (let nodeId in this.subscribers[channel]) {
898
- let clientId = parseId(nodeId).clientId
899
- if (!ignoreClients.has(clientId)) {
900
- let subscriber = this.subscribers[channel][nodeId]
901
- if (subscriber) {
902
- let ctx = this.createContext(action, meta)
903
- let client = this.clientIds.get(clientId)
904
- for (let filter of Object.values(subscriber.filters)) {
905
- if (typeof filter === 'function') {
906
- waiting.push(
907
- Promise.resolve(filter(ctx, action, meta)).then(result => {
908
- if (result && client && !ignoreClients.has(clientId)) {
909
- ignoreClients.add(clientId)
910
- addTarget(targets, client, action, meta)
911
- }
912
- })
913
- )
914
- } else if (filter && client) {
915
- ignoreClients.add(clientId)
916
- addTarget(targets, client, action, meta)
917
- }
918
- }
919
- }
920
- }
921
- }
922
- }
923
- }
924
- return waiting.length > 0 ? Promise.all(waiting) : undefined
925
- }
926
-
927
- runProcessor(action, meta, processor, start) {
928
- if (meta.status === 'waiting') {
929
- if (!processor) {
930
- this.internalUnknownType(action, meta)
931
- return
932
- }
933
- if (processor.process) {
934
- void this.processAction(processor, action, meta, start)
935
- } else {
936
- this.emitter.emit('processed', action, meta, 0)
937
- this.finally(processor, this.createContext(action, meta), action, meta)
938
- this.markAsProcessed(meta)
939
- }
940
- } else {
941
- this.emitter.emit('processed', action, meta, 0)
942
- this.finally(processor, this.createContext(action, meta), action, meta)
943
- }
944
- }
945
-
946
- async sendAction(action, meta) {
947
- let targets = new Map()
948
- let waiting = this.resolveTargets(action, meta, targets)
949
- if (waiting) await waiting
950
- sendTargets(this, targets, maxAdded(0, meta))
951
- }
952
-
953
- sendBatch(entries) {
954
- let targets = new Map()
955
- let waiting = []
956
- let settled = 0
957
- for (let [action, meta] of entries) {
958
- settled = maxAdded(settled, meta)
959
- let promise = this.resolveTargets(action, meta, targets)
960
- if (promise) waiting.push(promise)
961
- }
962
- if (waiting.length === 0) {
963
- sendTargets(this, targets, settled)
964
- return undefined
965
- }
966
- return Promise.all(waiting).then(() => {
967
- sendTargets(this, targets, settled)
968
- })
512
+ sendWithoutProcess(action, meta) {
513
+ return this.publisher.send([[action, meta]])
969
514
  }
970
515
 
971
516
  sendOnConnect(loader) {
@@ -981,163 +526,68 @@ export class BaseServer {
981
526
  }, ms)
982
527
  }
983
528
 
984
- subscribe(nodeId, channel) {
529
+ async subscribe(nodeId, channel) {
985
530
  if (channel === '__proto__' || nodeId === '__proto__') return
986
531
  if (!this.subscribers[channel] || !this.subscribers[channel][nodeId]) {
987
532
  if (!this.subscribers[channel]) {
988
533
  this.subscribers[channel] = {}
989
534
  }
990
535
  this.subscribers[channel][nodeId] = { filters: { '{}': true } }
991
- this.log.add({ channel, type: 'logux/subscribed' }, { nodes: [nodeId] })
992
- }
993
- }
994
-
995
- async subscribeAction(action, meta, start) {
996
- if (typeof action.channel !== 'string' || action.channel === '__proto__') {
997
- this.wrongChannel(action, meta)
998
- return
999
- }
1000
-
1001
- let channels = this.channels
1002
- if (this.otherSubscriber) {
1003
- channels = this.channels.concat([this.otherSubscriber])
1004
- }
1005
-
1006
- let match
1007
- for (let channel of channels) {
1008
- if (channel.pattern) {
1009
- match = channel.pattern(action.channel)
1010
- } else {
1011
- match = action.channel.match(channel.regexp)
1012
- }
1013
-
1014
- let subscribed = false
1015
- if (match) {
1016
- let ctx = this.createContext(action, meta)
1017
- if (ctx.nodeId === '__proto__') return
1018
- ctx.params = match
1019
- try {
1020
- let access = await channel.access(ctx, action, meta)
1021
- if (this.wrongChannels[meta.id]) {
1022
- delete this.wrongChannels[meta.id]
1023
- return
1024
- }
1025
- if (!access) {
1026
- this.denyAction(action, meta)
1027
- return
1028
- }
1029
-
1030
- let client = this.clientIds.get(ctx.clientId)
1031
- if (!client) {
1032
- this.emitter.emit('subscriptionCancelled')
1033
- return
1034
- }
1035
-
1036
- let filterId = subscriberFilterId(action)
1037
- let filters = { [filterId]: true }
1038
-
1039
- if (channel.filter) {
1040
- let filter = await channel.filter(ctx, action, meta)
1041
- filters = { [filterId]: filter }
1042
- }
1043
-
1044
- this.emitter.emit('report', 'subscribed', {
1045
- actionId: meta.id,
1046
- channel: action.channel
1047
- })
1048
-
1049
- if (!this.subscribers[action.channel]) {
1050
- this.subscribers[action.channel] = {}
1051
- this.emitter.emit('subscribing', action, meta)
1052
- }
1053
- let subscriber = this.subscribers[action.channel][ctx.nodeId]
1054
- if (subscriber) {
1055
- filters = { ...subscriber.filters, ...filters }
1056
- }
1057
- this.subscribers[action.channel][ctx.nodeId] = {
1058
- filters,
1059
- unsubscribe: channel.unsubscribe
1060
- ? (unsubscribeAction, unsubscribeMeta) =>
1061
- channel.unsubscribe(ctx, unsubscribeAction, unsubscribeMeta)
1062
- : undefined
1063
- }
1064
- subscribed = true
1065
-
1066
- if (channel.load) {
1067
- // Actions from a single `load()` are sent in a single message
1068
- let sendBack = await channel.load(ctx, action, meta)
1069
- if (sendBack) await ctx.sendBack(sendBack)
1070
- }
1071
- this.emitter.emit('subscribed', action, meta, Date.now() - start)
1072
- this.markAsProcessed(meta)
1073
- } catch (e) {
1074
- if (e.name === 'LoguxNotFoundError') {
1075
- this.undo(action, meta, 'notFound')
1076
- } else {
1077
- this.emitter.emit('error', e, action, meta)
1078
- this.undo(action, meta, 'error')
1079
- }
1080
- if (subscribed) {
1081
- this.unsubscribe(action, meta)
1082
- }
1083
- } finally {
1084
- this.finally(channel, ctx, action, meta)
536
+ await this.log.add(
537
+ { channel, type: 'logux/subscribed' },
538
+ {
539
+ nodes: [nodeId]
1085
540
  }
1086
- break
1087
- }
541
+ )
1088
542
  }
1089
-
1090
- if (!match) this.wrongChannel(action, meta)
1091
543
  }
1092
544
 
1093
545
  type(name, callbacks, options = {}) {
1094
- let queue = options.queue || 'main'
1095
- this.typeToQueue.set(name, queue)
1096
-
1097
546
  if (typeof name === 'function') name = name.type
1098
- normalizeTypeCallbacks(`Action type ${name}`, callbacks)
547
+ let handler = normalizeType(
548
+ `Action type ${name}`,
549
+ callbacks,
550
+ options.queue || 'main'
551
+ )
1099
552
 
1100
553
  if (name instanceof RegExp) {
1101
- this.regexTypes.set(name, callbacks)
554
+ this.regexTypes.set(name, handler)
1102
555
  } else {
1103
556
  if (this.types[name]) {
1104
557
  throw new Error(`Action type ${name} was already defined`)
1105
558
  }
1106
- this.types[name] = callbacks
559
+ this.types[name] = handler
1107
560
  }
1108
561
  }
1109
562
 
1110
- undo(action, meta, reason = 'error', extra = {}) {
1111
- let clientId = parseId(meta.id).clientId
1112
- let [undoAction, undoMeta] = this.buildUndo(action, meta, reason, extra)
1113
- undoMeta.clients = (undoMeta.clients || []).concat([clientId])
1114
- return this.log.add(undoAction, undoMeta)
563
+ reportFailure(reason, action, meta) {
564
+ let failure = FAILURES[reason]
565
+ if (!failure) return
566
+ let [details, message] = failure(action, meta)
567
+ this.emitter.emit('report', reason, details)
568
+ this.debugActionError(meta, message)
1115
569
  }
1116
570
 
1117
- unknownType(action, meta) {
1118
- this.internalUnknownType(action, meta)
1119
- this.unknownTypes[meta.id] = true
1120
- }
1121
-
1122
- unsubscribe(action, meta) {
1123
- let clientNodeId = meta.id.split(' ')[1]
1124
- this.performUnsubscribe(clientNodeId, action, meta)
571
+ undo(action, meta, reason = 'error', extra = {}) {
572
+ this.reportFailure(reason, action, meta)
573
+ let task = this.runner.byId.get(meta.id)
574
+ if (task && !task.finished) {
575
+ // The task answers the client with this outcome only once
576
+ task.settle({ extra, reason, type: 'undo' })
577
+ return Promise.resolve()
578
+ }
579
+ return this.publishUndo(action, meta, reason, extra)
1125
580
  }
1126
581
 
1127
- unsubscribeAction(action, meta) {
1128
- if (typeof action.channel !== 'string') {
1129
- this.wrongChannel(action, meta)
1130
- return
582
+ unknownType(action, meta) {
583
+ if (parseId(meta.id).userId === 'server') {
584
+ this.reportFailure('unknownType', action, meta)
585
+ return Promise.resolve()
1131
586
  }
1132
-
1133
- this.unsubscribe(action, meta)
1134
-
1135
- this.markAsProcessed(meta)
1136
- this.contexts.delete(action)
587
+ return this.undo(action, meta, 'unknownType')
1137
588
  }
1138
589
 
1139
590
  wrongChannel(action, meta) {
1140
- this.internalWrongChannel(action, meta)
1141
- this.wrongChannels[meta.id] = true
591
+ return this.undo(action, meta, 'wrongChannel')
1142
592
  }
1143
593
  }