@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.
- package/base-server/index.d.ts +72 -38
- package/base-server/index.js +177 -727
- package/context/index.d.ts +13 -1
- package/context/index.js +4 -2
- package/create-reporter/index.js +8 -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 -114
- package/url-pattern/index.js +9 -3
- package/vite.config.ts +0 -25
|
@@ -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,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
|
|
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
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
311
|
-
|
|
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
|
}
|
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)) + '$')
|
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
|
-
})
|