@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.
@@ -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,51 +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, clientId, meta, onReceiveResolve, queue } = task
34
- queue.next = next
35
-
36
- let type = action.type
37
- if (type === 'logux/subscribe' || type === 'logux/unsubscribe') {
38
- return onReceiveResolve([action, meta])
39
- }
40
-
41
- let processor = app.getProcessor(type)
42
- if (!processor) {
43
- app.internalUnknownType(action, meta)
44
- return onReceiveResolve(false)
45
- }
46
-
47
- let ctx = app.createContext(action, meta)
48
- try {
49
- let result = await processor.access(ctx, action, meta)
50
- if (app.unknownTypes[meta.id]) {
51
- delete app.unknownTypes[meta.id]
52
- app.finally(processor, ctx, action, meta)
53
- return false
54
- } else if (!result) {
55
- app.finally(processor, ctx, action, meta)
56
- denyBack(app, clientId, action, meta)
57
- return onReceiveResolve(false)
58
- } else {
59
- return onReceiveResolve([action, meta])
60
- }
61
- } catch (e) {
62
- app.undo(action, meta, 'error')
63
- app.emitter.emit('error', e, action, meta)
64
- app.finally(processor, ctx, action, meta)
65
- return onReceiveResolve(false)
66
- }
67
- }
68
-
69
23
  export class ServerClient {
70
24
  constructor(app, connection, key) {
71
25
  this.app = app
@@ -73,7 +27,6 @@ export class ServerClient {
73
27
  this.clientId = undefined
74
28
  this.nodeId = undefined
75
29
  this.data = {}
76
- this.processing = false
77
30
  this.sending = new Set()
78
31
  this.connection = connection
79
32
  this.key = key.toString()
@@ -92,6 +45,7 @@ export class ServerClient {
92
45
  onReceive: this.onReceive.bind(this),
93
46
  onSend,
94
47
  ping: app.options.ping,
48
+ ready: () => this.waitForReady(),
95
49
  subprotocol: app.options.subprotocol,
96
50
  syncBatch: app.options.syncBatch,
97
51
  timeout: app.options.timeout
@@ -100,20 +54,6 @@ export class ServerClient {
100
54
  this.node.setLocalHeaders({ env: 'development' })
101
55
  }
102
56
 
103
- if (app.connectLoader) {
104
- this.node.syncSinceQuery = async lastSynced => {
105
- let context = new Context(app, this)
106
- let entries = await app.connectLoader(context, lastSynced)
107
- let added = 0
108
- for (let entry of entries) {
109
- // `added` should be taken before `filterMeta()` removes it
110
- if (entry[1].added > added) added = entry[1].added
111
- entry[1] = filterMeta(entry[1])
112
- }
113
- return { added, entries }
114
- }
115
- }
116
-
117
57
  this.node.catch(err => {
118
58
  err.connectionId = this.key
119
59
  this.app.emitter.emit('error', err)
@@ -215,6 +155,7 @@ export class ServerClient {
215
155
  destroy() {
216
156
  this.destroyed = true
217
157
  this.node.destroy()
158
+ this.app.runner.cleanConnection(this)
218
159
  if (this.userId) {
219
160
  let users = this.app.userIds.get(this.userId)
220
161
  if (users) {
@@ -233,7 +174,7 @@ export class ServerClient {
233
174
  let action = { channel, type: 'logux/unsubscribe' }
234
175
  let actionId = this.app.log.generateId()
235
176
  let meta = { id: actionId, reasons: [], time: parseInt(actionId) }
236
- this.app.performUnsubscribe(this.nodeId, action, meta)
177
+ removeMembership(this.app, this.nodeId, action, meta)
237
178
  }
238
179
  }
239
180
  this.app.clientIds.delete(this.clientId)
@@ -263,66 +204,26 @@ export class ServerClient {
263
204
  }
264
205
 
265
206
  onReceive(action, meta) {
266
- if (this.app.actionToQueue.has(meta.id)) {
267
- return Promise.resolve(false)
268
- }
269
-
270
207
  let actionClientId = parseId(meta.id).clientId
271
208
  let wrongUser = !this.clientId || this.clientId !== actionClientId
272
209
  let wrongMeta = Object.keys(meta).some(i => !ALLOWED_META.includes(i))
273
210
  if (wrongUser || wrongMeta) {
274
- denyBack(this.app, this.clientId, action, meta)
211
+ this.app.reportFailure('denied', action, meta)
212
+ void this.app.publishUndo(action, meta, 'denied', {}, this.clientId)
275
213
  return Promise.resolve(false)
276
214
  }
277
215
 
278
- return new Promise(resolve => {
279
- let clientId = parseId(meta.id).clientId
280
- let queueName = ''
281
-
282
- let isChannel =
283
- (action.type === 'logux/subscribe' ||
284
- action.type === 'logux/unsubscribe') &&
285
- action.channel
286
-
287
- if (isChannel) {
288
- for (let channel of this.app.channels) {
289
- let match = channel.regexp
290
- ? action.channel.match(channel.regexp)
291
- : channel.pattern(action.channel)
292
- if (match) {
293
- queueName = channel.queue
294
- break
295
- }
296
- }
297
- } else {
298
- queueName = this.app.typeToQueue.get(action.type)
299
- }
300
-
301
- queueName = queueName || 'main'
302
- let queueKey = `${clientId}/${queueName}`
303
- let queue = this.app.queues.get(queueKey)
304
-
305
- if (!queue) {
306
- queue = fastq(queueWorker, 1)
307
- this.app.queues.set(queueKey, queue)
308
- }
216
+ if (!meta.subprotocol) {
217
+ meta.subprotocol = this.node.remoteSubprotocol
218
+ }
309
219
 
310
- if (!meta.subprotocol) {
311
- 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
312
225
  }
313
-
314
- this.app.actionToQueue.set(meta.id, queueKey)
315
- queue.push({
316
- action,
317
- app: this.app,
318
- clientId,
319
- meta,
320
- onReceiveResolve: result => {
321
- resolve(result)
322
- },
323
- queue
324
- })
325
- })
226
+ )
326
227
  }
327
228
 
328
229
  track(sending) {
@@ -332,4 +233,14 @@ export class ServerClient {
332
233
  this.sending.delete(done)
333
234
  })
334
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
+ }
335
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)) + '$')
package/vite.config.ts DELETED
@@ -1,25 +0,0 @@
1
- import { defineConfig } from 'vitest/config'
2
-
3
- export default defineConfig({
4
- test: {
5
- coverage: {
6
- exclude: [
7
- '**/node_modules/**',
8
- 'server/index.js',
9
- 'test/**',
10
- '**/*.d.ts',
11
- '**/*.test.ts',
12
- '**/errors.ts',
13
- '**/types.ts',
14
- '*.config.*',
15
- 'human-formatter/**'
16
- ],
17
- provider: 'v8',
18
- thresholds: {
19
- lines: 100
20
- }
21
- },
22
- environment: 'node',
23
- exclude: ['**/node_modules/**', 'test/servers/**']
24
- }
25
- })