@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.
@@ -0,0 +1,157 @@
1
+ import { parseId } from '@logux/core'
2
+
3
+ import { Context } from '../context/index.js'
4
+
5
+ function addTarget(targets, client, action, meta) {
6
+ let entries = targets.get(client)
7
+ if (entries) {
8
+ entries.push([action, meta])
9
+ } else {
10
+ targets.set(client, [[action, meta]])
11
+ }
12
+ }
13
+
14
+ function maxAdded(settled, meta) {
15
+ if (typeof meta.added === 'undefined' || meta.added < settled) return settled
16
+ return meta.added
17
+ }
18
+
19
+ /**
20
+ * Delivers the actions of the log to the clients.
21
+ *
22
+ * The publisher never runs `access`, `resend` or `process`: it resolves
23
+ * the recipients of the already stored facts and tracks the sending.
24
+ */
25
+ export class Publisher {
26
+ constructor(server) {
27
+ this.server = server
28
+ this.pending = new Set()
29
+ }
30
+
31
+ #resolveTargets(action, meta, targets) {
32
+ let from = parseId(meta.id).clientId
33
+ let ignoreClients = new Set(meta.excludeClients || [])
34
+ ignoreClients.add(from)
35
+
36
+ if (meta.nodes) {
37
+ for (let id of meta.nodes) {
38
+ let client = this.server.nodeIds.get(id)
39
+ if (client) {
40
+ ignoreClients.add(client.clientId)
41
+ addTarget(targets, client, action, meta)
42
+ }
43
+ }
44
+ }
45
+
46
+ if (meta.clients) {
47
+ for (let id of meta.clients) {
48
+ if (this.server.clientIds.has(id)) {
49
+ let client = this.server.clientIds.get(id)
50
+ ignoreClients.add(client.clientId)
51
+ addTarget(targets, client, action, meta)
52
+ }
53
+ }
54
+ }
55
+
56
+ if (meta.users) {
57
+ for (let userId of meta.users) {
58
+ let users = this.server.userIds.get(userId)
59
+ if (users) {
60
+ for (let client of users) {
61
+ if (!ignoreClients.has(client.clientId)) {
62
+ ignoreClients.add(client.clientId)
63
+ addTarget(targets, client, action, meta)
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ if (!meta.channels) return undefined
71
+
72
+ let waiting = []
73
+ for (let channel of meta.channels) {
74
+ if (this.server.subscribers[channel]) {
75
+ for (let nodeId in this.server.subscribers[channel]) {
76
+ let clientId = parseId(nodeId).clientId
77
+ if (!ignoreClients.has(clientId)) {
78
+ let subscriber = this.server.subscribers[channel][nodeId]
79
+ if (subscriber) {
80
+ let ctx = new Context(this.server, meta)
81
+ let client = this.server.clientIds.get(clientId)
82
+ for (let filter of Object.values(subscriber.filters)) {
83
+ if (typeof filter === 'function') {
84
+ waiting.push(
85
+ Promise.resolve(filter(ctx, action, meta)).then(
86
+ result => {
87
+ if (result && client && !ignoreClients.has(clientId)) {
88
+ ignoreClients.add(clientId)
89
+ addTarget(targets, client, action, meta)
90
+ }
91
+ },
92
+ e => {
93
+ // A broken filter is a delivery problem, not a reason
94
+ // to undo the already processed action
95
+ this.server.emitter.emit('error', e, action, meta)
96
+ }
97
+ )
98
+ )
99
+ } else if (filter && client) {
100
+ ignoreClients.add(clientId)
101
+ addTarget(targets, client, action, meta)
102
+ }
103
+ }
104
+ }
105
+ }
106
+ }
107
+ }
108
+ }
109
+ return waiting.length > 0 ? Promise.all(waiting) : undefined
110
+ }
111
+
112
+ async send(entries) {
113
+ let targets = new Map()
114
+ let waiting = []
115
+ let settled = 0
116
+ for (let [action, meta] of entries) {
117
+ settled = maxAdded(settled, meta)
118
+ let promise = this.#resolveTargets(action, meta, targets)
119
+ if (promise) waiting.push(promise)
120
+ }
121
+ if (waiting.length > 0) await Promise.all(waiting)
122
+ this.#sendTargets(targets, settled)
123
+ }
124
+
125
+ // A barrier for the queue and the shutdown: it never rejects,
126
+ // the failures are reported where the sending was started
127
+ async settled() {
128
+ while (this.pending.size > 0) await Promise.all(this.pending)
129
+ }
130
+
131
+ // Returns the original promise, so the rejection is not swallowed
132
+ track(entries) {
133
+ let sending = this.send(entries)
134
+ let done = sending.then(
135
+ () => this.pending.delete(done),
136
+ () => this.pending.delete(done)
137
+ )
138
+ this.pending.add(done)
139
+ return sending
140
+ }
141
+
142
+ #sendTargets(targets, settled) {
143
+ for (let [client, entries] of targets) {
144
+ let sending = client.node.onAdd(entries)
145
+ // Only `ServerClient` tracks sending for `Server#drain()`
146
+ if (client.track) client.track(sending)
147
+ }
148
+ if (settled > 0) {
149
+ for (let client of this.server.connected.values()) {
150
+ if (!targets.has(client) && client.node.lastAddedCache < settled) {
151
+ client.node.lastAddedCache = settled
152
+ }
153
+ }
154
+ }
155
+ targets.clear()
156
+ }
157
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * FIFO queues of the tasks by `(clientId, queueName)`.
3
+ *
4
+ * A task keeps its queue until it is finished or until the deadline
5
+ * answers it: the timeout can not stop the callback, but the queue
6
+ * must move on.
7
+ */
8
+ export class QueueManager {
9
+ constructor(runner) {
10
+ this.queues = new Map()
11
+ this.runner = runner
12
+ }
13
+
14
+ // The tasks, which did not start yet, are answered by the caller
15
+ cancelPending(key) {
16
+ let queue = this.queues.get(key)
17
+ if (!queue) return []
18
+ return queue.pending.splice(0)
19
+ }
20
+
21
+ push(key, task) {
22
+ let queue = this.queues.get(key)
23
+ if (!queue) {
24
+ queue = { chain: Promise.resolve(), pending: [] }
25
+ this.queues.set(key, queue)
26
+ }
27
+ task.queueKey = key
28
+ queue.pending.push(task)
29
+ queue.chain = queue.chain.then(async () => {
30
+ if (queue.pending.shift() !== task) return
31
+ await this.runner.run(task)
32
+ if (queue.pending.length === 0) this.queues.delete(key)
33
+ })
34
+ }
35
+
36
+ get size() {
37
+ return this.queues.size
38
+ }
39
+ }
@@ -82,15 +82,6 @@ export class ServerClient<ClientData extends object = unknown> {
82
82
  */
83
83
  nodeId?: string
84
84
 
85
- /**
86
- * Does server process some action from client.
87
- *
88
- * ```js
89
- * console.log('Clients in processing:', clients.map(i => i.processing))
90
- * ```
91
- */
92
- processing: boolean
93
-
94
85
  /**
95
86
  * Client IP address.
96
87
  *
@@ -1,11 +1,10 @@
1
1
  import { LoguxError, parseId } from '@logux/core'
2
2
  import { parseCookie } from 'cookie'
3
- import fastq from 'fastq'
4
3
 
5
4
  import { ALLOWED_META } from '../allowed-meta/index.js'
6
- import { Context } from '../context/index.js'
7
5
  import { filterMeta } from '../filter-meta/index.js'
8
6
  import { FilteredNode } from '../filtered-node/index.js'
7
+ import { removeMembership } from '../processing/index.js'
9
8
 
10
9
  async function onSend(action, meta) {
11
10
  return [action, filterMeta(meta)]
@@ -21,70 +20,6 @@ function reportDetails(client) {
21
20
  }
22
21
  }
23
22
 
24
- function denyBack(app, clientId, action, meta) {
25
- app.emitter.emit('report', 'denied', { actionId: meta.id })
26
- let [undoAction, undoMeta] = app.buildUndo(action, meta, 'denied')
27
- undoMeta.clients = (undoMeta.clients || []).concat([clientId])
28
- app.log.add(undoAction, undoMeta)
29
- app.debugActionError(meta, `Action "${meta.id}" was denied`)
30
- }
31
-
32
- async function queueWorker(task, next) {
33
- let { action, app, client, clientId, meta, onReceiveResolve, queue } = task
34
- queue.next = next
35
-
36
- if (app.options.queueTimeout) {
37
- let timer = setTimeout(() => {
38
- let error = new Error(
39
- `Action "${meta.id}" was not processed in ${app.options.queueTimeout} ms`
40
- )
41
- app.undo(action, meta, 'error')
42
- app.emitter.emit('error', error, action, meta)
43
- }, app.options.queueTimeout)
44
- if (timer.unref) timer.unref()
45
- app.queueTimers.set(meta.id, timer)
46
- }
47
-
48
- let type = action.type
49
- if (type === 'logux/subscribe' || type === 'logux/unsubscribe') {
50
- return onReceiveResolve([action, meta])
51
- }
52
-
53
- let processor = app.getProcessor(type)
54
- if (!processor) {
55
- app.internalUnknownType(action, meta)
56
- return onReceiveResolve(false)
57
- }
58
-
59
- let ctx = app.createContext(action, meta)
60
- try {
61
- let result = await processor.access(ctx, action, meta)
62
- if (app.unknownTypes[meta.id]) {
63
- delete app.unknownTypes[meta.id]
64
- app.finally(processor, ctx, action, meta)
65
- return false
66
- } else if (!result) {
67
- app.finally(processor, ctx, action, meta)
68
- denyBack(app, clientId, action, meta)
69
- return onReceiveResolve(false)
70
- } else {
71
- // The queue is released by the `processed` event of the action,
72
- // which never comes for the action the log already has: `log.add()`
73
- // returns `false` and no `batch` event fires. The core ignores
74
- // `false`, so the action is added here to see the result
75
- if (client.node.received) client.node.received[meta.id] = true
76
- let added = await app.log.add(action, meta)
77
- if (added === false) await app.resolveDuplicate(action, meta)
78
- return onReceiveResolve(false)
79
- }
80
- } catch (e) {
81
- app.undo(action, meta, 'error')
82
- app.emitter.emit('error', e, action, meta)
83
- app.finally(processor, ctx, action, meta)
84
- return onReceiveResolve(false)
85
- }
86
- }
87
-
88
23
  export class ServerClient {
89
24
  constructor(app, connection, key) {
90
25
  this.app = app
@@ -92,7 +27,6 @@ export class ServerClient {
92
27
  this.clientId = undefined
93
28
  this.nodeId = undefined
94
29
  this.data = {}
95
- this.processing = false
96
30
  this.sending = new Set()
97
31
  this.connection = connection
98
32
  this.key = key.toString()
@@ -111,6 +45,7 @@ export class ServerClient {
111
45
  onReceive: this.onReceive.bind(this),
112
46
  onSend,
113
47
  ping: app.options.ping,
48
+ ready: () => this.waitForReady(),
114
49
  subprotocol: app.options.subprotocol,
115
50
  syncBatch: app.options.syncBatch,
116
51
  timeout: app.options.timeout
@@ -119,20 +54,6 @@ export class ServerClient {
119
54
  this.node.setLocalHeaders({ env: 'development' })
120
55
  }
121
56
 
122
- if (app.connectLoader) {
123
- this.node.syncSinceQuery = async lastSynced => {
124
- let context = new Context(app, this)
125
- let entries = await app.connectLoader(context, lastSynced)
126
- let added = 0
127
- for (let entry of entries) {
128
- // `added` should be taken before `filterMeta()` removes it
129
- if (entry[1].added > added) added = entry[1].added
130
- entry[1] = filterMeta(entry[1])
131
- }
132
- return { added, entries }
133
- }
134
- }
135
-
136
57
  this.node.catch(err => {
137
58
  err.connectionId = this.key
138
59
  this.app.emitter.emit('error', err)
@@ -234,6 +155,7 @@ export class ServerClient {
234
155
  destroy() {
235
156
  this.destroyed = true
236
157
  this.node.destroy()
158
+ this.app.runner.cleanConnection(this)
237
159
  if (this.userId) {
238
160
  let users = this.app.userIds.get(this.userId)
239
161
  if (users) {
@@ -252,7 +174,7 @@ export class ServerClient {
252
174
  let action = { channel, type: 'logux/unsubscribe' }
253
175
  let actionId = this.app.log.generateId()
254
176
  let meta = { id: actionId, reasons: [], time: parseInt(actionId) }
255
- this.app.performUnsubscribe(this.nodeId, action, meta)
177
+ removeMembership(this.app, this.nodeId, action, meta)
256
178
  }
257
179
  }
258
180
  this.app.clientIds.delete(this.clientId)
@@ -282,67 +204,26 @@ export class ServerClient {
282
204
  }
283
205
 
284
206
  onReceive(action, meta) {
285
- if (this.app.actionToQueue.has(meta.id)) {
286
- return Promise.resolve(false)
287
- }
288
-
289
207
  let actionClientId = parseId(meta.id).clientId
290
208
  let wrongUser = !this.clientId || this.clientId !== actionClientId
291
209
  let wrongMeta = Object.keys(meta).some(i => !ALLOWED_META.includes(i))
292
210
  if (wrongUser || wrongMeta) {
293
- denyBack(this.app, this.clientId, action, meta)
211
+ this.app.reportFailure('denied', action, meta)
212
+ void this.app.publishUndo(action, meta, 'denied', {}, this.clientId)
294
213
  return Promise.resolve(false)
295
214
  }
296
215
 
297
- return new Promise(resolve => {
298
- let clientId = parseId(meta.id).clientId
299
- let queueName = ''
300
-
301
- let isChannel =
302
- (action.type === 'logux/subscribe' ||
303
- action.type === 'logux/unsubscribe') &&
304
- action.channel
305
-
306
- if (isChannel) {
307
- for (let channel of this.app.channels) {
308
- let match = channel.regexp
309
- ? action.channel.match(channel.regexp)
310
- : channel.pattern(action.channel)
311
- if (match) {
312
- queueName = channel.queue
313
- break
314
- }
315
- }
316
- } else {
317
- queueName = this.app.typeToQueue.get(action.type)
318
- }
319
-
320
- queueName = queueName || 'main'
321
- let queueKey = `${clientId}/${queueName}`
322
- let queue = this.app.queues.get(queueKey)
323
-
324
- if (!queue) {
325
- queue = fastq(queueWorker, 1)
326
- this.app.queues.set(queueKey, queue)
327
- }
216
+ if (!meta.subprotocol) {
217
+ meta.subprotocol = this.node.remoteSubprotocol
218
+ }
328
219
 
329
- if (!meta.subprotocol) {
330
- meta.subprotocol = this.node.remoteSubprotocol
220
+ return this.app.runner.submit({ action, client: this, meta }).then(
221
+ () => false,
222
+ err => {
223
+ if (err.transportFailure) throw err
224
+ return false
331
225
  }
332
-
333
- this.app.actionToQueue.set(meta.id, queueKey)
334
- queue.push({
335
- action,
336
- app: this.app,
337
- client: this,
338
- clientId,
339
- meta,
340
- onReceiveResolve: result => {
341
- resolve(result)
342
- },
343
- queue
344
- })
345
- })
226
+ )
346
227
  }
347
228
 
348
229
  track(sending) {
@@ -352,4 +233,14 @@ export class ServerClient {
352
233
  this.sending.delete(done)
353
234
  })
354
235
  }
236
+
237
+ async waitForReady() {
238
+ if (!this.node.remoteReady) {
239
+ await new Promise(resolve => {
240
+ this.node.on('ready', resolve)
241
+ })
242
+ }
243
+ await this.app.runner.waitForClient(this)
244
+ await this.drain()
245
+ }
355
246
  }
@@ -1,4 +1,4 @@
1
- const PARAM = /:(\w+)/g
1
+ const PARAM = /:(\w+)|\*/g
2
2
  const SPECIAL = /[.*+?^${}()|[\]\\]/g
3
3
 
4
4
  function escape(text) {
@@ -10,8 +10,14 @@ export function createPattern(pattern) {
10
10
  let source = ''
11
11
  let last = 0
12
12
  for (let param of pattern.matchAll(PARAM)) {
13
- source += escape(pattern.slice(last, param.index)) + '([^/]+)'
14
- names.push(param[1])
13
+ source += escape(pattern.slice(last, param.index))
14
+ if (param[1]) {
15
+ source += '([^/]+)'
16
+ names.push(param[1])
17
+ } else {
18
+ // `*` matches the rest of the URL, including `/`
19
+ source += '.*'
20
+ }
15
21
  last = param.index + param[0].length
16
22
  }
17
23
  let regexp = new RegExp('^' + source + escape(pattern.slice(last)) + '$')