@logux/server 0.16.3 → 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,68 +44,7 @@ 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 = {}) {
@@ -164,36 +94,20 @@ export class BaseServer {
164
94
  }
165
95
 
166
96
  this.logger = console
167
-
168
- this.contexts = new WeakMap()
169
97
  this.log = log
170
98
 
171
99
  let cleaned = {}
172
100
 
173
101
  this.on('preadd', (action, meta) => {
174
- let isLogux = action.type.slice(0, 6) === 'logux/'
175
- if (!meta.server) {
176
- meta.server = this.nodeId
177
- }
178
- if (!meta.status && !isLogux) {
179
- meta.status = 'waiting'
180
- }
181
- if (meta.id.split(' ')[1] === this.nodeId) {
182
- if (!meta.subprotocol) {
183
- meta.subprotocol = this.options.subprotocol
184
- }
185
- if (
186
- !isLogux &&
187
- !this.types[action.type] &&
188
- !this.getRegexProcessor(action.type)
189
- ) {
190
- meta.status = 'processed'
191
- }
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)
192
109
  }
193
- this.replaceResendShortcuts(meta)
194
110
  })
195
- // Reports are per action, `clean` for a reason-less action is emitted
196
- // by the log before `batch` event
197
111
  this.on('add', (action, meta) => {
198
112
  if (meta.reasons.length === 0) {
199
113
  cleaned[meta.id] = true
@@ -202,55 +116,6 @@ export class BaseServer {
202
116
  this.emitter.emit('report', 'add', { action, meta })
203
117
  }
204
118
  })
205
- this.on('batch', entries => {
206
- let start = Date.now()
207
- let ready = []
208
-
209
- for (let [action, meta] of entries) {
210
- if (this.destroying && !this.actionToQueue.has(meta.id)) {
211
- continue
212
- }
213
-
214
- if (action.type === 'logux/subscribe') {
215
- if (meta.server === this.nodeId) {
216
- void this.subscribeAction(action, meta, start)
217
- }
218
- continue
219
- }
220
-
221
- if (action.type === 'logux/unsubscribe') {
222
- if (meta.server === this.nodeId) {
223
- this.unsubscribeAction(action, meta)
224
- }
225
- continue
226
- }
227
-
228
- let processor = this.getProcessor(action.type)
229
- if (processor && processor.resend && meta.status === 'waiting') {
230
- // Resending is asynchronous, so the action can’t wait for others
231
- void this.resendAction(action, meta, processor, start)
232
- continue
233
- }
234
-
235
- if (this.isUseless(action, meta)) {
236
- this.emitter.emit('report', 'useless', { action, meta })
237
- }
238
- ready.push([action, meta, processor])
239
- }
240
-
241
- if (ready.length === 0) return
242
- let runProcessors = () => {
243
- for (let [action, meta, processor] of ready) {
244
- this.runProcessor(action, meta, processor, start)
245
- }
246
- }
247
- // Actions are processed only after they were sent to the clients
248
- void Promise.resolve(this.sendBatch(ready)).then(runProcessors, error => {
249
- // Do not stop action processing on a broken channel filter
250
- runProcessors()
251
- throw error
252
- })
253
- })
254
119
  this.on('clean', (action, meta) => {
255
120
  if (cleaned[meta.id]) {
256
121
  delete cleaned[meta.id]
@@ -312,7 +177,6 @@ export class BaseServer {
312
177
  this.userIds = new Map()
313
178
  this.types = {}
314
179
  this.regexTypes = new Map()
315
- this.processing = 0
316
180
 
317
181
  this.lastClient = 0
318
182
 
@@ -320,108 +184,54 @@ export class BaseServer {
320
184
  this.subscribers = {}
321
185
 
322
186
  this.authAttempts = {}
323
- this.unknownTypes = {}
324
- this.wrongChannels = {}
325
187
 
326
188
  this.timeouts = {}
327
189
  this.lastTimeout = 0
328
190
 
329
- this.typeToQueue = new Map()
330
- this.queues = new Map()
331
- this.actionToQueue = new Map()
332
- this.queueTimers = new Map()
191
+ this.publisher = new Publisher(this)
192
+ this.runner = new ActionRunner(this)
333
193
 
334
- 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 = []
335
206
  this.httpAllListeners = []
207
+ this.httpNotFoundListener = undefined
336
208
  addHttpPages(this)
337
209
 
338
210
  this.listenNotes = {}
339
211
 
340
- let end = (actionId, queue, queueKey, ...args) => {
341
- clearTimeout(this.queueTimers.get(actionId))
342
- this.queueTimers.delete(actionId)
343
- this.actionToQueue.delete(actionId)
344
- if (queue.length() === 0) {
345
- this.queues.delete(queueKey)
346
- }
347
- queue.next(...args)
348
- }
349
- let undoRemainingTasks = queue => {
350
- let remainingTasks = queue.getQueue()
351
- if (remainingTasks) {
352
- for (let task of remainingTasks) {
353
- this.undo(task.action, task.meta, 'error')
354
- this.actionToQueue.delete(task.meta.id)
355
- // The core waits for it to answer `synced`
356
- task.onReceiveResolve(false)
357
- }
358
- }
359
- queue.killAndDrain()
360
- }
361
- this.releaseQueue = (actionId, error) => {
362
- let queueKey = this.actionToQueue.get(actionId)
363
- if (!queueKey) return
364
- let queue = this.queues.get(queueKey)
365
- if (error) undoRemainingTasks(queue)
366
- end(actionId, queue, queueKey, error ?? null)
367
- }
368
- this.on('error', (e, action, meta) => {
369
- if (meta) this.releaseQueue(meta.id, e)
370
- })
371
- this.on('processed', (action, meta) => {
372
- if (action.type === 'logux/undo') {
373
- let queueKey = this.actionToQueue.get(action.id)
374
- if (queueKey) {
375
- let queue = this.queues.get(queueKey)
376
- undoRemainingTasks(queue)
377
- end(action.id, queue, queueKey, null, meta)
378
- }
379
- } else if (action.type === 'logux/processed') {
380
- let queueKey = this.actionToQueue.get(action.id)
381
- if (queueKey) {
382
- let queue = this.queues.get(queueKey)
383
- end(action.id, queue, queueKey, null, meta)
384
- }
385
- } else if (
386
- action.type !== 'logux/subscribed' &&
387
- action.type !== 'logux/unsubscribed'
388
- ) {
389
- let queueKey = this.actionToQueue.get(meta.id)
390
- if (queueKey) {
391
- let queue = this.queues.get(queueKey)
392
- end(meta.id, queue, queueKey, null, meta)
393
- }
394
- }
395
- })
396
-
397
212
  this.unbind.push(() => {
398
213
  for (let i of this.connected.values()) i.destroy()
399
214
  for (let i in this.timeouts) {
400
215
  clearTimeout(this.timeouts[i])
401
216
  }
402
217
  })
403
- this.unbind.push(() => {
404
- return new Promise(resolve => {
405
- if (this.processing === 0) {
406
- resolve()
407
- } else {
408
- this.on('processed', () => {
409
- if (this.processing === 0) resolve()
410
- })
411
- }
412
- })
413
- })
414
- this.unbind.push(() => {
415
- return Promise.allSettled(
416
- [...this.queues.values()].map(queue => {
417
- return new Promise(resolve => {
418
- queue.drain = resolve
419
- })
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
420
225
  })
421
- )
226
+ }
422
227
  })
423
228
  }
424
229
 
230
+ handleClient(ws, req) {
231
+ ws.upgradeReq = req
232
+ this.addClient(new ServerConnection(ws))
233
+ }
234
+
425
235
  addClient(connection) {
426
236
  this.lastClient += 1
427
237
  let key = this.lastClient.toString()
@@ -434,50 +244,20 @@ export class BaseServer {
434
244
  this.authenticator = authenticator
435
245
  }
436
246
 
437
- buildUndo(action, meta, reason, extra) {
438
- let undoMeta = { status: 'processed' }
439
-
440
- if (meta.users) undoMeta.users = meta.users.slice(0)
441
- if (meta.nodes) undoMeta.nodes = meta.nodes.slice(0)
442
- if (meta.clients) undoMeta.clients = meta.clients.slice(0)
443
- if (meta.reasons) undoMeta.reasons = meta.reasons.slice(0)
444
- if (meta.channels) undoMeta.channels = meta.channels.slice(0)
445
- if (meta.excludeClients) {
446
- undoMeta.excludeClients = meta.excludeClients.slice(0)
447
- }
448
-
449
- let undoAction = {
450
- ...extra,
451
- action,
452
- id: meta.id,
453
- reason,
454
- type: 'logux/undo'
455
- }
456
- return [undoAction, undoMeta]
457
- }
458
-
459
247
  channel(pattern, callbacks, options = {}) {
460
- normalizeChannelCallbacks(`Channel ${pattern}`, callbacks)
461
- let channel = Object.assign({}, callbacks)
248
+ let channel = normalizeChannel(
249
+ `Channel ${pattern}`,
250
+ callbacks,
251
+ options.queue || 'main'
252
+ )
462
253
  if (typeof pattern === 'string') {
463
254
  channel.pattern = createPattern(pattern)
464
255
  } else {
465
256
  channel.regexp = pattern
466
257
  }
467
-
468
- channel.queueName = options.queue || 'main'
469
258
  this.channels.push(channel)
470
259
  }
471
260
 
472
- createContext(action, meta) {
473
- let context = this.contexts.get(action)
474
- if (!context) {
475
- context = new Context(this, meta)
476
- this.contexts.set(action, context)
477
- }
478
- return context
479
- }
480
-
481
261
  debugActionError(meta, msg) {
482
262
  if (this.env === 'development') {
483
263
  let clientId = parseId(meta.id).clientId
@@ -497,12 +277,6 @@ export class BaseServer {
497
277
  }
498
278
  }
499
279
 
500
- denyAction(action, meta) {
501
- this.emitter.emit('report', 'denied', { actionId: meta.id })
502
- this.undo(action, meta, 'denied')
503
- this.debugActionError(meta, `Action "${meta.id}" was denied`)
504
- }
505
-
506
280
  destroy() {
507
281
  this.destroying = true
508
282
  this.emitter.emit('report', 'destroy')
@@ -515,37 +289,6 @@ export class BaseServer {
515
289
  return client.drain()
516
290
  }
517
291
 
518
- finally(processor, ctx, action, meta) {
519
- this.contexts.delete(action)
520
- if (processor && processor.finally) {
521
- try {
522
- processor.finally(ctx, action, meta)
523
- } catch (err) {
524
- this.emitter.emit('error', err, action, meta)
525
- }
526
- }
527
- }
528
-
529
- getProcessor(type) {
530
- return (
531
- this.types[type] || this.getRegexProcessor(type) || this.otherProcessor
532
- )
533
- }
534
-
535
- getRegexProcessor(type) {
536
- for (let regexp of this.regexTypes.keys()) {
537
- if (type.match(regexp) !== null) {
538
- return this.regexTypes.get(regexp)
539
- }
540
- }
541
- return undefined
542
- }
543
-
544
- handleClient(ws, req) {
545
- ws.upgradeReq = req
546
- this.addClient(new ServerConnection(ws))
547
- }
548
-
549
292
  http(method, url, listener) {
550
293
  if (this.options.disableHttpServer) {
551
294
  throw new Error(
@@ -555,31 +298,26 @@ export class BaseServer {
555
298
  if (!url) {
556
299
  this.httpAllListeners.push(method)
557
300
  } else {
558
- 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
+ }
559
310
  }
560
311
  }
561
312
 
562
- internalUnknownType(action, meta) {
563
- this.contexts.delete(action)
564
- this.log.changeMeta(meta.id, { status: 'error' })
565
- this.emitter.emit('report', 'unknownType', {
566
- actionId: meta.id,
567
- type: action.type
568
- })
569
- if (parseId(meta.id).userId !== 'server') {
570
- 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
+ )
571
319
  }
572
- this.debugActionError(meta, `Action with unknown type ${action.type}`)
573
- }
574
-
575
- internalWrongChannel(action, meta) {
576
- this.contexts.delete(action)
577
- this.emitter.emit('report', 'wrongChannel', {
578
- actionId: meta.id,
579
- channel: action.channel
580
- })
581
- this.undo(action, meta, 'wrongChannel')
582
- this.debugActionError(meta, `Wrong channel name ${action.channel}`)
320
+ this.httpNotFoundListener = listener
583
321
  }
584
322
 
585
323
  isBruteforce(ip) {
@@ -588,14 +326,10 @@ export class BaseServer {
588
326
  }
589
327
 
590
328
  isUseless(action, meta) {
591
- if (
592
- meta.status !== 'processed' ||
593
- this.types[action.type] ||
594
- this.getRegexProcessor(action.type)
595
- ) {
596
- return false
597
- }
598
- 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) {
599
333
  if (Array.isArray(meta[i]) && meta[i].length > 0) return false
600
334
  }
601
335
  return true
@@ -664,37 +398,6 @@ export class BaseServer {
664
398
  })
665
399
  }
666
400
 
667
- /**
668
- * The client re-sent the action, which the log already has: the server
669
- * stored it, but the client did not get the answer. Answer again
670
- * and release the queue of the client.
671
- */
672
- async resolveDuplicate(action, meta) {
673
- let [stored, storedMeta] = await this.log.byId(meta.id)
674
- this.emitter.emit('report', 'duplicate', {
675
- actionId: meta.id,
676
- status: storedMeta ? storedMeta.status : undefined
677
- })
678
- if (stored && storedMeta.status === 'processed') {
679
- // The `processed` event of the answer releases the queue
680
- this.markAsProcessed(storedMeta)
681
- } else {
682
- // Still processing or already answered by `logux/undo`
683
- this.releaseQueue(meta.id)
684
- }
685
- }
686
-
687
- markAsProcessed(meta) {
688
- this.log.changeMeta(meta.id, { status: 'processed' })
689
- let data = parseId(meta.id)
690
- if (data.userId !== 'server') {
691
- this.log.add(
692
- { id: meta.id, type: 'logux/processed' },
693
- { clients: [data.clientId], status: 'processed' }
694
- )
695
- }
696
- }
697
-
698
401
  on(event, listener) {
699
402
  if (
700
403
  event === 'preadd' ||
@@ -709,11 +412,10 @@ export class BaseServer {
709
412
  }
710
413
 
711
414
  otherChannel(callbacks) {
712
- normalizeChannelCallbacks('Unknown channel', callbacks)
713
415
  if (this.otherSubscriber) {
714
416
  throw new Error('Callbacks for unknown channel are already defined')
715
417
  }
716
- let channel = Object.assign({}, callbacks)
418
+ let channel = normalizeChannel('Unknown channel', callbacks, 'main')
717
419
  channel.pattern = name => [name]
718
420
  this.otherSubscriber = channel
719
421
  }
@@ -722,75 +424,30 @@ export class BaseServer {
722
424
  if (this.otherProcessor) {
723
425
  throw new Error('Callbacks for unknown types are already defined')
724
426
  }
725
- normalizeTypeCallbacks('Unknown type', callbacks)
726
- this.otherProcessor = callbacks
427
+ this.otherProcessor = normalizeType('Unknown type', callbacks, 'main')
727
428
  }
728
429
 
729
- performUnsubscribe(clientNodeId, action, meta) {
730
- if (action.channel === '__proto__' || clientNodeId === '__proto__') return
731
- if (this.subscribers[action.channel]) {
732
- let subscriber = this.subscribers[action.channel][clientNodeId]
733
- if (subscriber) {
734
- if (subscriber.unsubscribe) {
735
- subscriber.unsubscribe(action, meta)
736
- this.contexts.delete(action)
737
- }
738
- let filterId = subscriberFilterId(action)
739
- delete subscriber.filters[filterId]
740
- if (Object.keys(subscriber.filters).length === 0) {
741
- delete this.subscribers[action.channel][clientNodeId]
742
- }
743
- if (Object.keys(this.subscribers[action.channel]).length === 0) {
744
- delete this.subscribers[action.channel]
745
- }
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
746
445
  }
747
446
  }
748
- this.emitter.emit('unsubscribed', action, meta, clientNodeId)
749
- this.emitter.emit('report', 'unsubscribed', {
750
- actionId: meta.id,
751
- channel: action.channel
752
- })
753
447
  }
754
448
 
755
449
  process(action, meta = {}) {
756
- return new Promise((resolve, reject) => {
757
- let unbindError = this.on('error', (e, errorAction) => {
758
- if (errorAction === action) {
759
- unbindError()
760
- unbindProcessed()
761
- reject(e)
762
- }
763
- })
764
- let unbindProcessed = this.on('processed', (processed, processedMeta) => {
765
- if (processed === action) {
766
- unbindError()
767
- unbindProcessed()
768
- resolve(processedMeta)
769
- }
770
- })
771
- this.log.add(action, meta)
772
- })
773
- }
774
-
775
- async processAction(processor, action, meta, start) {
776
- let ctx = this.createContext(action, meta)
777
-
778
- let latency
779
- this.processing += 1
780
- try {
781
- await processor.process(ctx, action, meta)
782
- latency = Date.now() - start
783
- this.markAsProcessed(meta)
784
- } catch (e) {
785
- this.log.changeMeta(meta.id, { status: 'error' })
786
- this.undo(action, meta, 'error')
787
- this.emitter.emit('error', e, action, meta)
788
- } finally {
789
- this.finally(processor, ctx, action, meta)
790
- }
791
- if (typeof latency === 'undefined') latency = Date.now() - start
792
- this.processing -= 1
793
- this.emitter.emit('processed', action, meta, latency)
450
+ return this.runner.submit({ action, meta, trusted: true })
794
451
  }
795
452
 
796
453
  async processHttp(req, res) {
@@ -799,24 +456,46 @@ export class BaseServer {
799
456
  urlString = decodeURIComponent(urlString)
800
457
  }
801
458
  let reqUrl = new URL(urlString, 'http://localhost')
802
- let rule = this.httpListeners[req.method + ' ' + reqUrl.pathname]
803
-
804
- if (!rule) {
805
- let processed = false
806
- for (let listener of this.httpAllListeners) {
807
- let result = await listener(req, res)
808
- if (result === true) {
809
- processed = true
810
- break
811
- }
812
- }
813
- if (!processed) {
814
- res.writeHead(404, { 'Content-Type': 'text/plain' })
815
- 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
816
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)
817
475
  } else {
818
- 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)
819
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
+ )
820
499
  }
821
500
 
822
501
  rememberBadAuth(ip) {
@@ -830,172 +509,8 @@ export class BaseServer {
830
509
  }, 3000)
831
510
  }
832
511
 
833
- replaceResendShortcuts(meta) {
834
- if (meta.channel) {
835
- meta.channels = [meta.channel]
836
- delete meta.channel
837
- }
838
- if (meta.user) {
839
- meta.users = [meta.user]
840
- delete meta.user
841
- }
842
- if (meta.client) {
843
- meta.clients = [meta.client]
844
- delete meta.client
845
- }
846
- if (meta.node) {
847
- meta.nodes = [meta.node]
848
- delete meta.node
849
- }
850
- }
851
-
852
- async resendAction(action, meta, processor, start) {
853
- let ctx = this.createContext(action, meta)
854
- let resend
855
- try {
856
- resend = await processor.resend(ctx, action, meta)
857
- } catch (e) {
858
- this.undo(action, meta, 'error')
859
- this.emitter.emit('error', e, action, meta)
860
- this.finally(processor, ctx, action, meta)
861
- return
862
- }
863
- if (resend) {
864
- if (typeof resend === 'string') {
865
- resend = { channels: [resend] }
866
- } else if (Array.isArray(resend)) {
867
- resend = { channels: resend }
868
- } else {
869
- this.replaceResendShortcuts(resend)
870
- }
871
- let diff = {}
872
- for (let i of RESEND_META) {
873
- if (resend[i]) diff[i] = resend[i]
874
- }
875
- await this.log.changeMeta(meta.id, diff)
876
- meta = { ...meta, ...diff }
877
- }
878
-
879
- await this.sendAction(action, meta)
880
- this.runProcessor(action, meta, processor, start)
881
- }
882
-
883
- resolveTargets(action, meta, targets) {
884
- let from = parseId(meta.id).clientId
885
- let ignoreClients = new Set(meta.excludeClients || [])
886
- ignoreClients.add(from)
887
-
888
- if (meta.nodes) {
889
- for (let id of meta.nodes) {
890
- let client = this.nodeIds.get(id)
891
- if (client) {
892
- ignoreClients.add(client.clientId)
893
- addTarget(targets, client, action, meta)
894
- }
895
- }
896
- }
897
-
898
- if (meta.clients) {
899
- for (let id of meta.clients) {
900
- if (this.clientIds.has(id)) {
901
- let client = this.clientIds.get(id)
902
- ignoreClients.add(client.clientId)
903
- addTarget(targets, client, action, meta)
904
- }
905
- }
906
- }
907
-
908
- if (meta.users) {
909
- for (let userId of meta.users) {
910
- let users = this.userIds.get(userId)
911
- if (users) {
912
- for (let client of users) {
913
- if (!ignoreClients.has(client.clientId)) {
914
- ignoreClients.add(client.clientId)
915
- addTarget(targets, client, action, meta)
916
- }
917
- }
918
- }
919
- }
920
- }
921
-
922
- if (!meta.channels) return undefined
923
-
924
- let waiting = []
925
- for (let channel of meta.channels) {
926
- if (this.subscribers[channel]) {
927
- for (let nodeId in this.subscribers[channel]) {
928
- let clientId = parseId(nodeId).clientId
929
- if (!ignoreClients.has(clientId)) {
930
- let subscriber = this.subscribers[channel][nodeId]
931
- if (subscriber) {
932
- let ctx = this.createContext(action, meta)
933
- let client = this.clientIds.get(clientId)
934
- for (let filter of Object.values(subscriber.filters)) {
935
- if (typeof filter === 'function') {
936
- waiting.push(
937
- Promise.resolve(filter(ctx, action, meta)).then(result => {
938
- if (result && client && !ignoreClients.has(clientId)) {
939
- ignoreClients.add(clientId)
940
- addTarget(targets, client, action, meta)
941
- }
942
- })
943
- )
944
- } else if (filter && client) {
945
- ignoreClients.add(clientId)
946
- addTarget(targets, client, action, meta)
947
- }
948
- }
949
- }
950
- }
951
- }
952
- }
953
- }
954
- return waiting.length > 0 ? Promise.all(waiting) : undefined
955
- }
956
-
957
- runProcessor(action, meta, processor, start) {
958
- if (meta.status === 'waiting') {
959
- if (!processor) {
960
- this.internalUnknownType(action, meta)
961
- return
962
- }
963
- if (processor.process) {
964
- void this.processAction(processor, action, meta, start)
965
- } else {
966
- this.emitter.emit('processed', action, meta, 0)
967
- this.finally(processor, this.createContext(action, meta), action, meta)
968
- this.markAsProcessed(meta)
969
- }
970
- } else {
971
- this.emitter.emit('processed', action, meta, 0)
972
- this.finally(processor, this.createContext(action, meta), action, meta)
973
- }
974
- }
975
-
976
- async sendAction(action, meta) {
977
- let targets = new Map()
978
- let waiting = this.resolveTargets(action, meta, targets)
979
- if (waiting) await waiting
980
- sendTargets(this, targets, maxAdded(0, meta))
981
- }
982
-
983
- sendBatch(entries) {
984
- let targets = new Map()
985
- let waiting = []
986
- let settled = 0
987
- for (let [action, meta] of entries) {
988
- settled = maxAdded(settled, meta)
989
- let promise = this.resolveTargets(action, meta, targets)
990
- if (promise) waiting.push(promise)
991
- }
992
- if (waiting.length === 0) {
993
- sendTargets(this, targets, settled)
994
- return undefined
995
- }
996
- return Promise.all(waiting).then(() => {
997
- sendTargets(this, targets, settled)
998
- })
512
+ sendWithoutProcess(action, meta) {
513
+ return this.publisher.send([[action, meta]])
999
514
  }
1000
515
 
1001
516
  sendOnConnect(loader) {
@@ -1011,163 +526,68 @@ export class BaseServer {
1011
526
  }, ms)
1012
527
  }
1013
528
 
1014
- subscribe(nodeId, channel) {
529
+ async subscribe(nodeId, channel) {
1015
530
  if (channel === '__proto__' || nodeId === '__proto__') return
1016
531
  if (!this.subscribers[channel] || !this.subscribers[channel][nodeId]) {
1017
532
  if (!this.subscribers[channel]) {
1018
533
  this.subscribers[channel] = {}
1019
534
  }
1020
535
  this.subscribers[channel][nodeId] = { filters: { '{}': true } }
1021
- this.log.add({ channel, type: 'logux/subscribed' }, { nodes: [nodeId] })
1022
- }
1023
- }
1024
-
1025
- async subscribeAction(action, meta, start) {
1026
- if (typeof action.channel !== 'string' || action.channel === '__proto__') {
1027
- this.wrongChannel(action, meta)
1028
- return
1029
- }
1030
-
1031
- let channels = this.channels
1032
- if (this.otherSubscriber) {
1033
- channels = this.channels.concat([this.otherSubscriber])
1034
- }
1035
-
1036
- let match
1037
- for (let channel of channels) {
1038
- if (channel.pattern) {
1039
- match = channel.pattern(action.channel)
1040
- } else {
1041
- match = action.channel.match(channel.regexp)
1042
- }
1043
-
1044
- let subscribed = false
1045
- if (match) {
1046
- let ctx = this.createContext(action, meta)
1047
- if (ctx.nodeId === '__proto__') return
1048
- ctx.params = match
1049
- try {
1050
- let access = await channel.access(ctx, action, meta)
1051
- if (this.wrongChannels[meta.id]) {
1052
- delete this.wrongChannels[meta.id]
1053
- return
1054
- }
1055
- if (!access) {
1056
- this.denyAction(action, meta)
1057
- return
1058
- }
1059
-
1060
- let client = this.clientIds.get(ctx.clientId)
1061
- if (!client) {
1062
- this.emitter.emit('subscriptionCancelled')
1063
- return
1064
- }
1065
-
1066
- let filterId = subscriberFilterId(action)
1067
- let filters = { [filterId]: true }
1068
-
1069
- if (channel.filter) {
1070
- let filter = await channel.filter(ctx, action, meta)
1071
- filters = { [filterId]: filter }
1072
- }
1073
-
1074
- this.emitter.emit('report', 'subscribed', {
1075
- actionId: meta.id,
1076
- channel: action.channel
1077
- })
1078
-
1079
- if (!this.subscribers[action.channel]) {
1080
- this.subscribers[action.channel] = {}
1081
- this.emitter.emit('subscribing', action, meta)
1082
- }
1083
- let subscriber = this.subscribers[action.channel][ctx.nodeId]
1084
- if (subscriber) {
1085
- filters = { ...subscriber.filters, ...filters }
1086
- }
1087
- this.subscribers[action.channel][ctx.nodeId] = {
1088
- filters,
1089
- unsubscribe: channel.unsubscribe
1090
- ? (unsubscribeAction, unsubscribeMeta) =>
1091
- channel.unsubscribe(ctx, unsubscribeAction, unsubscribeMeta)
1092
- : undefined
1093
- }
1094
- subscribed = true
1095
-
1096
- if (channel.load) {
1097
- // Actions from a single `load()` are sent in a single message
1098
- let sendBack = await channel.load(ctx, action, meta)
1099
- if (sendBack) await ctx.sendBack(sendBack)
1100
- }
1101
- this.emitter.emit('subscribed', action, meta, Date.now() - start)
1102
- this.markAsProcessed(meta)
1103
- } catch (e) {
1104
- if (e.name === 'LoguxNotFoundError') {
1105
- this.undo(action, meta, 'notFound')
1106
- } else {
1107
- this.emitter.emit('error', e, action, meta)
1108
- this.undo(action, meta, 'error')
1109
- }
1110
- if (subscribed) {
1111
- this.unsubscribe(action, meta)
1112
- }
1113
- } finally {
1114
- this.finally(channel, ctx, action, meta)
536
+ await this.log.add(
537
+ { channel, type: 'logux/subscribed' },
538
+ {
539
+ nodes: [nodeId]
1115
540
  }
1116
- break
1117
- }
541
+ )
1118
542
  }
1119
-
1120
- if (!match) this.wrongChannel(action, meta)
1121
543
  }
1122
544
 
1123
545
  type(name, callbacks, options = {}) {
1124
- let queue = options.queue || 'main'
1125
- this.typeToQueue.set(name, queue)
1126
-
1127
546
  if (typeof name === 'function') name = name.type
1128
- normalizeTypeCallbacks(`Action type ${name}`, callbacks)
547
+ let handler = normalizeType(
548
+ `Action type ${name}`,
549
+ callbacks,
550
+ options.queue || 'main'
551
+ )
1129
552
 
1130
553
  if (name instanceof RegExp) {
1131
- this.regexTypes.set(name, callbacks)
554
+ this.regexTypes.set(name, handler)
1132
555
  } else {
1133
556
  if (this.types[name]) {
1134
557
  throw new Error(`Action type ${name} was already defined`)
1135
558
  }
1136
- this.types[name] = callbacks
559
+ this.types[name] = handler
1137
560
  }
1138
561
  }
1139
562
 
1140
- undo(action, meta, reason = 'error', extra = {}) {
1141
- let clientId = parseId(meta.id).clientId
1142
- let [undoAction, undoMeta] = this.buildUndo(action, meta, reason, extra)
1143
- undoMeta.clients = (undoMeta.clients || []).concat([clientId])
1144
- return this.log.add(undoAction, undoMeta)
1145
- }
1146
-
1147
- unknownType(action, meta) {
1148
- this.internalUnknownType(action, meta)
1149
- this.unknownTypes[meta.id] = true
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)
1150
569
  }
1151
570
 
1152
- unsubscribe(action, meta) {
1153
- let clientNodeId = meta.id.split(' ')[1]
1154
- 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)
1155
580
  }
1156
581
 
1157
- unsubscribeAction(action, meta) {
1158
- if (typeof action.channel !== 'string') {
1159
- this.wrongChannel(action, meta)
1160
- return
582
+ unknownType(action, meta) {
583
+ if (parseId(meta.id).userId === 'server') {
584
+ this.reportFailure('unknownType', action, meta)
585
+ return Promise.resolve()
1161
586
  }
1162
-
1163
- this.unsubscribe(action, meta)
1164
-
1165
- this.markAsProcessed(meta)
1166
- this.contexts.delete(action)
587
+ return this.undo(action, meta, 'unknownType')
1167
588
  }
1168
589
 
1169
590
  wrongChannel(action, meta) {
1170
- this.internalWrongChannel(action, meta)
1171
- this.wrongChannels[meta.id] = true
591
+ return this.undo(action, meta, 'wrongChannel')
1172
592
  }
1173
593
  }