@logux/server 0.13.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,21 +2,38 @@ import { LoguxNotFoundError } from '@logux/actions'
2
2
  import { Log, MemoryStore, parseId, ServerConnection } from '@logux/core'
3
3
  import { createNanoEvents } from 'nanoevents'
4
4
  import { nanoid } from 'nanoid'
5
- import { promises as fs } from 'node:fs'
5
+ import { readFile } from 'node:fs/promises'
6
6
  import { dirname, join } from 'node:path'
7
7
  import { fileURLToPath } from 'node:url'
8
- import UrlPattern from 'url-pattern'
9
8
  import { WebSocketServer } from 'ws'
10
9
 
11
- import { bindBackendProxy } from '../bind-backend-proxy/index.js'
12
- import { bindControlServer } from '../bind-control-server/index.js'
10
+ import { addHttpPages } from '../add-http-pages/index.js'
13
11
  import { Context } from '../context/index.js'
14
12
  import { createHttpServer } from '../create-http-server/index.js'
15
13
  import { ServerClient } from '../server-client/index.js'
14
+ import { createPattern } from '../url-pattern/index.js'
16
15
 
17
16
  const SKIP_PROCESS = Symbol('skipProcess')
18
17
  const RESEND_META = ['channels', 'users', 'clients', 'nodes']
19
18
 
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(targets) {
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
+ targets.clear()
35
+ }
36
+
20
37
  function optionError(msg) {
21
38
  let error = new Error(msg)
22
39
  error.logux = true
@@ -24,17 +41,6 @@ function optionError(msg) {
24
41
  throw error
25
42
  }
26
43
 
27
- let helloCache
28
- async function readHello() {
29
- if (!helloCache) {
30
- let hello = await fs.readFile(
31
- join(fileURLToPath(import.meta.url), '..', 'hello.html')
32
- )
33
- helloCache = hello.toString()
34
- }
35
- return helloCache
36
- }
37
-
38
44
  export async function wasNot403(cb) {
39
45
  try {
40
46
  await cb()
@@ -103,17 +109,11 @@ export class BaseServer {
103
109
  this.options = opts
104
110
  this.env = this.options.env || process.env.NODE_ENV || 'development'
105
111
 
106
- if (
107
- typeof this.options.subprotocol === 'undefined' &&
108
- typeof this.options.backend === 'undefined'
109
- ) {
112
+ if (typeof this.options.subprotocol === 'undefined') {
110
113
  throw optionError('Missed `subprotocol` option in server constructor')
111
114
  }
112
- if (
113
- typeof this.options.supports === 'undefined' &&
114
- typeof this.options.backend === 'undefined'
115
- ) {
116
- throw optionError('Missed `supports` option in server constructor')
115
+ if (typeof this.options.minSubprotocol === 'undefined') {
116
+ throw optionError('Missed `minSubprotocol` option in server constructor')
117
117
  }
118
118
 
119
119
  if (this.options.key && !this.options.cert) {
@@ -128,14 +128,16 @@ export class BaseServer {
128
128
  if (!this.options.host) this.options.host = '127.0.0.1'
129
129
  }
130
130
 
131
- this.nodeId = `server:${this.options.id || nanoid(8)}`
131
+ this.nodeId = `server:${this.options.id || nanoid(6)}`
132
132
 
133
133
  if (this.options.fileUrl) {
134
134
  this.options.root = dirname(fileURLToPath(this.options.fileUrl))
135
135
  }
136
136
 
137
137
  this.options.root = this.options.root || process.cwd()
138
- this.options.controlMask = this.options.controlMask || '127.0.0.1/8'
138
+ if (typeof this.options.port === 'string') {
139
+ this.options.port = parseInt(this.options.port, 10)
140
+ }
139
141
 
140
142
  let store = this.options.store || new MemoryStore()
141
143
 
@@ -167,7 +169,6 @@ export class BaseServer {
167
169
  }
168
170
  if (
169
171
  !isLogux &&
170
- !this.options.backend &&
171
172
  !this.types[action.type] &&
172
173
  !this.getRegexProcessor(action.type)
173
174
  ) {
@@ -176,89 +177,64 @@ export class BaseServer {
176
177
  }
177
178
  this.replaceResendShortcuts(meta)
178
179
  })
179
- this.on('add', async (action, meta) => {
180
- let start = Date.now()
180
+ // Reports are per action, `clean` for a reason-less action is emitted
181
+ // by the log before `batch` event
182
+ this.on('add', (action, meta) => {
181
183
  if (meta.reasons.length === 0) {
182
184
  cleaned[meta.id] = true
183
185
  this.emitter.emit('report', 'addClean', { action, meta })
184
186
  } else {
185
187
  this.emitter.emit('report', 'add', { action, meta })
186
188
  }
189
+ })
190
+ this.on('batch', entries => {
191
+ let start = Date.now()
192
+ let ready = []
187
193
 
188
- if (this.destroying && !this.actionToQueue.has(meta.id)) {
189
- return
190
- }
191
-
192
- if (action.type === 'logux/subscribe') {
193
- if (meta.server === this.nodeId) {
194
- this.subscribeAction(action, meta, start)
194
+ for (let [action, meta] of entries) {
195
+ if (this.destroying && !this.actionToQueue.has(meta.id)) {
196
+ continue
195
197
  }
196
- return
197
- }
198
198
 
199
- if (action.type === 'logux/unsubscribe') {
200
- if (meta.server === this.nodeId) {
201
- this.unsubscribeAction(action, meta)
199
+ if (action.type === 'logux/subscribe') {
200
+ if (meta.server === this.nodeId) {
201
+ void this.subscribeAction(action, meta, start)
202
+ }
203
+ continue
202
204
  }
203
- return
204
- }
205
205
 
206
- let processor = this.getProcessor(action.type)
207
- if (processor && processor.resend && meta.status === 'waiting') {
208
- let ctx = this.createContext(action, meta)
209
- let resend
210
- try {
211
- resend = await processor.resend(ctx, action, meta)
212
- } catch (e) {
213
- this.undo(action, meta, 'error')
214
- this.emitter.emit('error', e, action, meta)
215
- this.finally(processor, ctx, action, meta)
216
- return
217
- }
218
- if (resend) {
219
- if (typeof resend === 'string') {
220
- resend = { channels: [resend] }
221
- } else if (Array.isArray(resend)) {
222
- resend = { channels: resend }
223
- } else {
224
- this.replaceResendShortcuts(resend)
206
+ if (action.type === 'logux/unsubscribe') {
207
+ if (meta.server === this.nodeId) {
208
+ this.unsubscribeAction(action, meta)
225
209
  }
226
- let diff = {}
227
- for (let i of RESEND_META) {
228
- if (resend[i]) diff[i] = resend[i]
229
- }
230
- await this.log.changeMeta(meta.id, diff)
231
- meta = { ...meta, ...diff }
210
+ continue
232
211
  }
233
- }
234
-
235
- if (this.isUseless(action, meta)) {
236
- this.emitter.emit('report', 'useless', { action, meta })
237
- }
238
212
 
239
- await this.sendAction(action, meta)
213
+ let processor = this.getProcessor(action.type)
214
+ if (processor && processor.resend && meta.status === 'waiting') {
215
+ // Resending is asynchronous, so the action can’t wait for others
216
+ void this.resendAction(action, meta, processor, start)
217
+ continue
218
+ }
240
219
 
241
- if (meta.status === 'waiting') {
242
- if (!processor) {
243
- this.internalUnknownType(action, meta)
244
- return
220
+ if (this.isUseless(action, meta)) {
221
+ this.emitter.emit('report', 'useless', { action, meta })
245
222
  }
246
- if (processor.process) {
247
- this.processAction(processor, action, meta, start)
248
- } else {
249
- this.emitter.emit('processed', action, meta, 0)
250
- this.finally(
251
- processor,
252
- this.createContext(action, meta),
253
- action,
254
- meta
255
- )
256
- this.markAsProcessed(meta)
223
+ ready.push([action, meta, processor])
224
+ }
225
+
226
+ if (ready.length === 0) return
227
+ let runProcessors = () => {
228
+ for (let [action, meta, processor] of ready) {
229
+ this.runProcessor(action, meta, processor, start)
257
230
  }
258
- } else {
259
- this.emitter.emit('processed', action, meta, 0)
260
- this.finally(processor, this.createContext(action, meta), action, meta)
261
231
  }
232
+ // Actions are processed only after they were sent to the clients
233
+ void Promise.resolve(this.sendBatch(ready)).then(runProcessors, error => {
234
+ // Do not stop action processing on a broken channel filter
235
+ runProcessors()
236
+ throw error
237
+ })
262
238
  })
263
239
  this.on('clean', (action, meta) => {
264
240
  if (cleaned[meta.id]) {
@@ -339,27 +315,11 @@ export class BaseServer {
339
315
  this.queues = new Map()
340
316
  this.actionToQueue = new Map()
341
317
 
342
- this.controls = {
343
- 'GET /': {
344
- async request() {
345
- return {
346
- body: await readHello(),
347
- headers: { 'Content-Type': 'text/html' }
348
- }
349
- },
350
- safe: true
351
- },
352
- 'GET /health': {
353
- request: () => ({
354
- body: 'Logux Server: OK\n',
355
- headers: { 'Content-Type': 'text/plain' }
356
- }),
357
- safe: true
358
- }
359
- }
318
+ this.httpListeners = {}
319
+ this.httpAllListeners = []
320
+ addHttpPages(this)
360
321
 
361
322
  this.listenNotes = {}
362
- bindBackendProxy(this)
363
323
 
364
324
  let end = (actionId, queue, queueKey, ...args) => {
365
325
  this.actionToQueue.delete(actionId)
@@ -478,9 +438,7 @@ export class BaseServer {
478
438
  normalizeChannelCallbacks(`Channel ${pattern}`, callbacks)
479
439
  let channel = Object.assign({}, callbacks)
480
440
  if (typeof pattern === 'string') {
481
- channel.pattern = new UrlPattern(pattern, {
482
- segmentValueCharset: '^/'
483
- })
441
+ channel.pattern = createPattern(pattern)
484
442
  } else {
485
443
  channel.regexp = pattern
486
444
  }
@@ -529,6 +487,12 @@ export class BaseServer {
529
487
  return Promise.all(this.unbind.map(i => i()))
530
488
  }
531
489
 
490
+ drain(clientId) {
491
+ let client = this.clientIds.get(clientId)
492
+ if (!client) return Promise.resolve(false)
493
+ return client.drain()
494
+ }
495
+
532
496
  finally(processor, ctx, action, meta) {
533
497
  this.contexts.delete(action)
534
498
  if (processor && processor.finally) {
@@ -555,13 +519,22 @@ export class BaseServer {
555
519
  return undefined
556
520
  }
557
521
 
558
- http(listener) {
522
+ handleClient(ws, req) {
523
+ ws.upgradeReq = req
524
+ this.addClient(new ServerConnection(ws))
525
+ }
526
+
527
+ http(method, url, listener) {
559
528
  if (this.options.disableHttpServer) {
560
529
  throw new Error(
561
530
  '`server.http()` can not be called when `disableHttpServer` enabled'
562
531
  )
563
532
  }
564
- this.httpListener = listener
533
+ if (!url) {
534
+ this.httpAllListeners.push(method)
535
+ } else {
536
+ this.httpListeners[`${method} ${url}`] = listener
537
+ }
565
538
  }
566
539
 
567
540
  internalUnknownType(action, meta) {
@@ -618,50 +591,54 @@ export class BaseServer {
618
591
  this.httpServer.listen(this.options.port, this.options.host, resolve)
619
592
  })
620
593
  }
621
- bindControlServer(this, this.httpListener)
622
594
 
623
- this.unbind.push(
624
- () =>
625
- new Promise(resolve => {
626
- this.ws.on('close', resolve)
627
- this.ws.close()
628
- })
629
- )
630
- if (this.httpServer) {
631
- this.unbind.push(
632
- () =>
633
- new Promise(resolve => {
634
- this.httpServer.on('close', resolve)
635
- this.httpServer.close()
636
- })
637
- )
595
+ let processing = 0
596
+ let waiting
597
+ this.unbind.push(() => {
598
+ return new Promise(resolve => {
599
+ let end = () => {
600
+ this.ws.close(resolve)
601
+ this.httpServer.close()
602
+ }
603
+ if (processing === 0) {
604
+ end()
605
+ } else {
606
+ waiting = end
607
+ }
608
+ })
609
+ })
610
+
611
+ if (!this.options.disableHttpServer) {
612
+ this.httpServer.on('request', async (req, res) => {
613
+ if (this.destroying) {
614
+ res.writeHead(503, { 'Content-Type': 'text/plain' })
615
+ res.end('The server is shutting down\n')
616
+ return
617
+ }
618
+ processing += 1
619
+ await this.processHttp(req, res)
620
+ processing -= 1
621
+ if (processing === 0 && waiting) waiting()
622
+ })
638
623
  }
639
624
 
640
625
  let pkg = JSON.parse(
641
- await fs.readFile(
642
- join(fileURLToPath(import.meta.url), '..', '..', 'package.json')
643
- )
626
+ await readFile(join(import.meta.dirname, '..', 'package.json'))
644
627
  )
645
628
 
646
- this.ws.on('connection', (ws, req) => {
647
- ws.upgradeReq = req
648
- this.addClient(new ServerConnection(ws))
649
- })
629
+ this.ws.on('connection', (ws, req) => this.handleClient(ws, req))
650
630
  this.emitter.emit('report', 'listen', {
651
- backend: this.options.backend,
652
631
  cert: !!this.options.cert,
653
- controlMask: this.options.controlMask,
654
- controlSecret: this.options.controlSecret,
655
632
  environment: this.env,
656
633
  host: this.options.host,
657
634
  loguxServer: pkg.version,
635
+ minSubprotocol: this.options.minSubprotocol,
658
636
  nodeId: this.nodeId,
659
637
  notes: this.listenNotes,
660
638
  port: this.options.port,
661
639
  redis: this.options.redis,
662
640
  server: !!this.options.server,
663
- subprotocol: this.options.subprotocol,
664
- supports: this.options.supports
641
+ subprotocol: this.options.subprotocol
665
642
  })
666
643
  }
667
644
 
@@ -677,7 +654,12 @@ export class BaseServer {
677
654
  }
678
655
 
679
656
  on(event, listener) {
680
- if (event === 'preadd' || event === 'add' || event === 'clean') {
657
+ if (
658
+ event === 'preadd' ||
659
+ event === 'add' ||
660
+ event === 'batch' ||
661
+ event === 'clean'
662
+ ) {
681
663
  return this.log.emitter.on(event, listener)
682
664
  } else {
683
665
  return this.emitter.on(event, listener)
@@ -690,11 +672,7 @@ export class BaseServer {
690
672
  throw new Error('Callbacks for unknown channel are already defined')
691
673
  }
692
674
  let channel = Object.assign({}, callbacks)
693
- channel.pattern = {
694
- match(name) {
695
- return [name]
696
- }
697
- }
675
+ channel.pattern = name => [name]
698
676
  this.otherSubscriber = channel
699
677
  }
700
678
 
@@ -707,6 +685,7 @@ export class BaseServer {
707
685
  }
708
686
 
709
687
  performUnsubscribe(clientNodeId, action, meta) {
688
+ if (action.channel === '__proto__' || clientNodeId === '__proto__') return
710
689
  if (this.subscribers[action.channel]) {
711
690
  let subscriber = this.subscribers[action.channel][clientNodeId]
712
691
  if (subscriber) {
@@ -772,6 +751,32 @@ export class BaseServer {
772
751
  this.emitter.emit('processed', action, meta, latency)
773
752
  }
774
753
 
754
+ async processHttp(req, res) {
755
+ let urlString = req.url
756
+ if (/^\/\w+%3F/.test(urlString)) {
757
+ urlString = decodeURIComponent(urlString)
758
+ }
759
+ let reqUrl = new URL(urlString, 'http://localhost')
760
+ let rule = this.httpListeners[req.method + ' ' + reqUrl.pathname]
761
+
762
+ if (!rule) {
763
+ let processed = false
764
+ for (let listener of this.httpAllListeners) {
765
+ let result = await listener(req, res)
766
+ if (result === true) {
767
+ processed = true
768
+ break
769
+ }
770
+ }
771
+ if (!processed) {
772
+ res.writeHead(404, { 'Content-Type': 'text/plain' })
773
+ res.end('Not found\n')
774
+ }
775
+ } else {
776
+ await rule(req, res)
777
+ }
778
+ }
779
+
775
780
  rememberBadAuth(ip) {
776
781
  this.authAttempts[ip] = (this.authAttempts[ip] || 0) + 1
777
782
  this.setTimeout(() => {
@@ -802,7 +807,42 @@ export class BaseServer {
802
807
  }
803
808
  }
804
809
 
805
- async sendAction(action, meta) {
810
+ async resendAction(action, meta, processor, start) {
811
+ let ctx = this.createContext(action, meta)
812
+ let resend
813
+ try {
814
+ resend = await processor.resend(ctx, action, meta)
815
+ } catch (e) {
816
+ this.undo(action, meta, 'error')
817
+ this.emitter.emit('error', e, action, meta)
818
+ this.finally(processor, ctx, action, meta)
819
+ return
820
+ }
821
+ if (resend) {
822
+ if (typeof resend === 'string') {
823
+ resend = { channels: [resend] }
824
+ } else if (Array.isArray(resend)) {
825
+ resend = { channels: resend }
826
+ } else {
827
+ this.replaceResendShortcuts(resend)
828
+ }
829
+ let diff = {}
830
+ for (let i of RESEND_META) {
831
+ if (resend[i]) diff[i] = resend[i]
832
+ }
833
+ await this.log.changeMeta(meta.id, diff)
834
+ meta = { ...meta, ...diff }
835
+ }
836
+
837
+ if (this.isUseless(action, meta)) {
838
+ this.emitter.emit('report', 'useless', { action, meta })
839
+ }
840
+
841
+ await this.sendAction(action, meta)
842
+ this.runProcessor(action, meta, processor, start)
843
+ }
844
+
845
+ resolveTargets(action, meta, targets) {
806
846
  let from = parseId(meta.id).clientId
807
847
  let ignoreClients = new Set(meta.excludeClients || [])
808
848
  ignoreClients.add(from)
@@ -812,7 +852,7 @@ export class BaseServer {
812
852
  let client = this.nodeIds.get(id)
813
853
  if (client) {
814
854
  ignoreClients.add(client.clientId)
815
- client.node.onAdd(action, meta)
855
+ addTarget(targets, client, action, meta)
816
856
  }
817
857
  }
818
858
  }
@@ -822,7 +862,7 @@ export class BaseServer {
822
862
  if (this.clientIds.has(id)) {
823
863
  let client = this.clientIds.get(id)
824
864
  ignoreClients.add(client.clientId)
825
- client.node.onAdd(action, meta)
865
+ addTarget(targets, client, action, meta)
826
866
  }
827
867
  }
828
868
  }
@@ -834,32 +874,38 @@ export class BaseServer {
834
874
  for (let client of users) {
835
875
  if (!ignoreClients.has(client.clientId)) {
836
876
  ignoreClients.add(client.clientId)
837
- client.node.onAdd(action, meta)
877
+ addTarget(targets, client, action, meta)
838
878
  }
839
879
  }
840
880
  }
841
881
  }
842
882
  }
843
883
 
844
- if (meta.channels) {
845
- for (let channel of meta.channels) {
846
- if (this.subscribers[channel]) {
847
- for (let nodeId in this.subscribers[channel]) {
848
- let clientId = parseId(nodeId).clientId
849
- if (!ignoreClients.has(clientId)) {
850
- let subscriber = this.subscribers[channel][nodeId]
851
- if (subscriber) {
852
- let ctx = this.createContext(action, meta)
853
- let client = this.clientIds.get(clientId)
854
- for (let filter of Object.values(subscriber.filters)) {
855
- filter =
856
- typeof filter === 'function'
857
- ? await filter(ctx, action, meta)
858
- : filter
859
- if (filter && client) {
860
- ignoreClients.add(clientId)
861
- client.node.onAdd(action, meta)
862
- }
884
+ if (!meta.channels) return undefined
885
+
886
+ let waiting = []
887
+ for (let channel of meta.channels) {
888
+ if (this.subscribers[channel]) {
889
+ for (let nodeId in this.subscribers[channel]) {
890
+ let clientId = parseId(nodeId).clientId
891
+ if (!ignoreClients.has(clientId)) {
892
+ let subscriber = this.subscribers[channel][nodeId]
893
+ if (subscriber) {
894
+ let ctx = this.createContext(action, meta)
895
+ let client = this.clientIds.get(clientId)
896
+ for (let filter of Object.values(subscriber.filters)) {
897
+ if (typeof filter === 'function') {
898
+ waiting.push(
899
+ Promise.resolve(filter(ctx, action, meta)).then(result => {
900
+ if (result && client && !ignoreClients.has(clientId)) {
901
+ ignoreClients.add(clientId)
902
+ addTarget(targets, client, action, meta)
903
+ }
904
+ })
905
+ )
906
+ } else if (filter && client) {
907
+ ignoreClients.add(clientId)
908
+ addTarget(targets, client, action, meta)
863
909
  }
864
910
  }
865
911
  }
@@ -867,6 +913,53 @@ export class BaseServer {
867
913
  }
868
914
  }
869
915
  }
916
+ return waiting.length > 0 ? Promise.all(waiting) : undefined
917
+ }
918
+
919
+ runProcessor(action, meta, processor, start) {
920
+ if (meta.status === 'waiting') {
921
+ if (!processor) {
922
+ this.internalUnknownType(action, meta)
923
+ return
924
+ }
925
+ if (processor.process) {
926
+ void this.processAction(processor, action, meta, start)
927
+ } else {
928
+ this.emitter.emit('processed', action, meta, 0)
929
+ this.finally(processor, this.createContext(action, meta), action, meta)
930
+ this.markAsProcessed(meta)
931
+ }
932
+ } else {
933
+ this.emitter.emit('processed', action, meta, 0)
934
+ this.finally(processor, this.createContext(action, meta), action, meta)
935
+ }
936
+ }
937
+
938
+ async sendAction(action, meta) {
939
+ let targets = new Map()
940
+ let waiting = this.resolveTargets(action, meta, targets)
941
+ if (waiting) await waiting
942
+ sendTargets(targets)
943
+ }
944
+
945
+ sendBatch(entries) {
946
+ let targets = new Map()
947
+ let waiting = []
948
+ for (let [action, meta] of entries) {
949
+ let promise = this.resolveTargets(action, meta, targets)
950
+ if (promise) waiting.push(promise)
951
+ }
952
+ if (waiting.length === 0) {
953
+ sendTargets(targets)
954
+ return undefined
955
+ }
956
+ return Promise.all(waiting).then(() => {
957
+ sendTargets(targets)
958
+ })
959
+ }
960
+
961
+ sendOnConnect(loader) {
962
+ this.connectLoader = loader
870
963
  }
871
964
 
872
965
  setTimeout(callback, ms) {
@@ -879,6 +972,7 @@ export class BaseServer {
879
972
  }
880
973
 
881
974
  subscribe(nodeId, channel) {
975
+ if (channel === '__proto__' || nodeId === '__proto__') return
882
976
  if (!this.subscribers[channel] || !this.subscribers[channel][nodeId]) {
883
977
  if (!this.subscribers[channel]) {
884
978
  this.subscribers[channel] = {}
@@ -889,7 +983,7 @@ export class BaseServer {
889
983
  }
890
984
 
891
985
  async subscribeAction(action, meta, start) {
892
- if (typeof action.channel !== 'string') {
986
+ if (typeof action.channel !== 'string' || action.channel === '__proto__') {
893
987
  this.wrongChannel(action, meta)
894
988
  return
895
989
  }
@@ -902,7 +996,7 @@ export class BaseServer {
902
996
  let match
903
997
  for (let channel of channels) {
904
998
  if (channel.pattern) {
905
- match = channel.pattern.match(action.channel)
999
+ match = channel.pattern(action.channel)
906
1000
  } else {
907
1001
  match = action.channel.match(channel.regexp)
908
1002
  }
@@ -910,6 +1004,7 @@ export class BaseServer {
910
1004
  let subscribed = false
911
1005
  if (match) {
912
1006
  let ctx = this.createContext(action, meta)
1007
+ if (ctx.nodeId === '__proto__') return
913
1008
  ctx.params = match
914
1009
  try {
915
1010
  let access = await channel.access(ctx, action, meta)
@@ -959,16 +1054,9 @@ export class BaseServer {
959
1054
  subscribed = true
960
1055
 
961
1056
  if (channel.load) {
1057
+ // Actions from a single `load()` are sent in a single message
962
1058
  let sendBack = await channel.load(ctx, action, meta)
963
- if (Array.isArray(sendBack)) {
964
- await Promise.all(
965
- sendBack.map(i => {
966
- return Array.isArray(i) ? ctx.sendBack(...i) : ctx.sendBack(i)
967
- })
968
- )
969
- } else if (sendBack) {
970
- await ctx.sendBack(sendBack)
971
- }
1059
+ if (sendBack) await ctx.sendBack(sendBack)
972
1060
  }
973
1061
  this.emitter.emit('subscribed', action, meta, Date.now() - start)
974
1062
  this.markAsProcessed(meta)