@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.
- package/base-server/index.d.ts +65 -39
- package/base-server/index.js +174 -754
- package/context/index.d.ts +13 -1
- package/context/index.js +4 -2
- package/create-reporter/index.js +6 -0
- package/filtered-node/index.js +38 -1
- package/index.d.ts +5 -0
- package/index.js +1 -0
- package/package.json +4 -5
- package/postgres-store/index.d.ts +3 -1
- package/postgres-store/index.js +74 -30
- package/processing/index.js +560 -0
- package/processing-store/index.d.ts +51 -0
- package/processing-store/index.js +44 -0
- package/publishing/index.js +157 -0
- package/queue-manager/index.js +39 -0
- package/server-client/index.d.ts +0 -9
- package/server-client/index.js +25 -134
- package/url-pattern/index.js +9 -3
|
@@ -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
|
+
}
|
package/server-client/index.d.ts
CHANGED
|
@@ -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
|
*
|
package/server-client/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
330
|
-
|
|
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
|
}
|
package/url-pattern/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const PARAM = /:(\w+)
|
|
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
|
-
|
|
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)) + '$')
|