velocious 1.0.562 → 1.0.563
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/README.md +3 -1
- package/build/configuration-types.js +1 -0
- package/build/configuration.js +44 -3
- package/build/http-server/client/websocket-session.js +3 -3
- package/build/http-server/client-delivery-queue.js +168 -0
- package/build/http-server/server-client.js +9 -0
- package/build/http-server/websocket-events-host.js +42 -12
- package/build/http-server/worker-handler/in-process.js +53 -14
- package/build/http-server/worker-handler/index.js +88 -18
- package/build/http-server/worker-handler/worker-thread.js +2 -2
- package/build/src/configuration-types.d.ts +8 -0
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +2 -1
- package/build/src/configuration.d.ts +34 -1
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +40 -4
- package/build/src/http-server/client/websocket-session.js +4 -4
- package/build/src/http-server/client-delivery-queue.d.ts +121 -0
- package/build/src/http-server/client-delivery-queue.d.ts.map +1 -0
- package/build/src/http-server/client-delivery-queue.js +152 -0
- package/build/src/http-server/server-client.d.ts +6 -0
- package/build/src/http-server/server-client.d.ts.map +1 -1
- package/build/src/http-server/server-client.js +10 -1
- package/build/src/http-server/websocket-events-host.d.ts +15 -4
- package/build/src/http-server/websocket-events-host.d.ts.map +1 -1
- package/build/src/http-server/websocket-events-host.js +39 -13
- package/build/src/http-server/worker-handler/in-process.d.ts +19 -1
- package/build/src/http-server/worker-handler/in-process.d.ts.map +1 -1
- package/build/src/http-server/worker-handler/in-process.js +50 -14
- package/build/src/http-server/worker-handler/index.d.ts +36 -4
- package/build/src/http-server/worker-handler/index.d.ts.map +1 -1
- package/build/src/http-server/worker-handler/index.js +82 -18
- package/build/src/http-server/worker-handler/worker-thread.js +3 -3
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/src/configuration-types.js +1 -0
- package/src/configuration.js +44 -3
- package/src/http-server/client/websocket-session.js +3 -3
- package/src/http-server/client-delivery-queue.js +168 -0
- package/src/http-server/server-client.js +9 -0
- package/src/http-server/websocket-events-host.js +42 -12
- package/src/http-server/worker-handler/in-process.js +53 -14
- package/src/http-server/worker-handler/index.js +88 -18
- package/src/http-server/worker-handler/worker-thread.js +2 -2
package/README.md
CHANGED
|
@@ -1568,7 +1568,9 @@ database: {
|
|
|
1568
1568
|
|
|
1569
1569
|
Velocious includes a lightweight websocket entry point for API-style calls and server-side events.
|
|
1570
1570
|
|
|
1571
|
-
Inbound frames remain ordered when TCP splits a frame across reads. Velocious limits a single final client data frame and a reassembled fragmented message to 16 MiB; larger payloads close the connection.
|
|
1571
|
+
Inbound frames remain ordered when TCP splits a frame across reads. Velocious limits a single final client data frame and a reassembled fragmented message to 16 MiB; larger payloads close the connection.
|
|
1572
|
+
|
|
1573
|
+
Server-to-client WebSocket delivery is bounded independently per client. The defaults retain at most 256 queued/in-flight completed frames or 16 MiB of serialized frame output; the HTTP 101 upgrade response and ordinary HTTP/file output remain outside that budget, while exact FIFO is preserved. A slow client that exceeds either limit is reported through the framework error events and deterministically closed without affecting other clients. V2 channel broadcast delivery and persistence remain isolated to the originating configuration when several applications share a process. Configure positive safe integers with `httpServer.websocketOutboundQueue.maxPendingFrames` and `maxPendingBytes`. See [WebSocket connections](docs/websocket-connections.md) for configuration and wire-protocol details.
|
|
1572
1574
|
|
|
1573
1575
|
## Connect and call a controller
|
|
1574
1576
|
|
|
@@ -255,6 +255,7 @@
|
|
|
255
255
|
* @property {boolean} [inProcess] - Run HTTP handlers in the main thread instead of worker threads.
|
|
256
256
|
* @property {number} [maxWorkers] - Backward-compatible alias for workers.
|
|
257
257
|
* @property {number} [port] - Port to bind the HTTP server to.
|
|
258
|
+
* @property {{maxPendingBytes?: number, maxPendingFrames?: number}} [websocketOutboundQueue] - Per-client retained outbound WebSocket frame limits.
|
|
258
259
|
* @property {number} [workers] - Worker handlers to start for the HTTP server.
|
|
259
260
|
*/
|
|
260
261
|
|
package/build/configuration.js
CHANGED
|
@@ -114,6 +114,25 @@ function resolveBeaconUnreachableReportMs(value) {
|
|
|
114
114
|
return 30_000
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
const DEFAULT_WEBSOCKET_OUTBOUND_MAX_PENDING_BYTES = 16 * 1024 * 1024
|
|
118
|
+
const DEFAULT_WEBSOCKET_OUTBOUND_MAX_PENDING_FRAMES = 256
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Validates a positive safe integer configuration value.
|
|
122
|
+
* @param {?} value - Configured positive safe integer.
|
|
123
|
+
* @param {string} name - Configuration key.
|
|
124
|
+
* @param {number} defaultValue - Default value.
|
|
125
|
+
* @returns {number} - Validated configured or default value.
|
|
126
|
+
*/
|
|
127
|
+
function positiveSafeInteger(value, name, defaultValue) {
|
|
128
|
+
if (value === undefined) return defaultValue
|
|
129
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
|
|
130
|
+
throw new TypeError(`${name} must be a positive safe integer`)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return value
|
|
134
|
+
}
|
|
135
|
+
|
|
117
136
|
export default class VelociousConfiguration {
|
|
118
137
|
/**
|
|
119
138
|
* Close database connections promise.
|
|
@@ -209,7 +228,15 @@ export default class VelociousConfiguration {
|
|
|
209
228
|
* @type {Promise<void> | undefined}
|
|
210
229
|
*/
|
|
211
230
|
this._initializePromise = undefined
|
|
212
|
-
|
|
231
|
+
const websocketOutboundQueue = httpServer?.websocketOutboundQueue
|
|
232
|
+
|
|
233
|
+
this.httpServer = {
|
|
234
|
+
...(httpServer || {}),
|
|
235
|
+
websocketOutboundQueue: {
|
|
236
|
+
maxPendingBytes: positiveSafeInteger(websocketOutboundQueue?.maxPendingBytes, "httpServer.websocketOutboundQueue.maxPendingBytes", DEFAULT_WEBSOCKET_OUTBOUND_MAX_PENDING_BYTES),
|
|
237
|
+
maxPendingFrames: positiveSafeInteger(websocketOutboundQueue?.maxPendingFrames, "httpServer.websocketOutboundQueue.maxPendingFrames", DEFAULT_WEBSOCKET_OUTBOUND_MAX_PENDING_FRAMES)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
213
240
|
/**
|
|
214
241
|
* Stores the http server instance value.
|
|
215
242
|
* @type {{getDebugSnapshot: () => Promise<Record<string, ?>>} | undefined} */
|
|
@@ -1693,7 +1720,8 @@ export default class VelociousConfiguration {
|
|
|
1693
1720
|
websocketEvents.broadcastV2({
|
|
1694
1721
|
channel: message.channel,
|
|
1695
1722
|
broadcastParams: message.broadcastParams,
|
|
1696
|
-
body: message.body
|
|
1723
|
+
body: message.body,
|
|
1724
|
+
configuration: this
|
|
1697
1725
|
})
|
|
1698
1726
|
return
|
|
1699
1727
|
}
|
|
@@ -2342,6 +2370,19 @@ export default class VelociousConfiguration {
|
|
|
2342
2370
|
*/
|
|
2343
2371
|
getWebsocketSessionHeartbeatSeconds() { return this._websocketSessionHeartbeatSeconds }
|
|
2344
2372
|
|
|
2373
|
+
/**
|
|
2374
|
+
* Gets per-client WebSocket outbound queue limits.
|
|
2375
|
+
* @returns {{maxBytes: number, maxFrames: number}} - Per-client outbound queue high-water marks.
|
|
2376
|
+
*/
|
|
2377
|
+
getWebsocketOutboundQueueLimits() {
|
|
2378
|
+
const queue = this.httpServer.websocketOutboundQueue
|
|
2379
|
+
|
|
2380
|
+
return {
|
|
2381
|
+
maxBytes: queue.maxPendingBytes,
|
|
2382
|
+
maxFrames: queue.maxPendingFrames
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2345
2386
|
/**
|
|
2346
2387
|
* Registers a wrapper invoked around every WS-borne request /
|
|
2347
2388
|
* connection message / channel dispatch. The wrapper receives the
|
|
@@ -2537,7 +2578,7 @@ export default class VelociousConfiguration {
|
|
|
2537
2578
|
const websocketEvents = this._websocketEvents
|
|
2538
2579
|
|
|
2539
2580
|
if (websocketEvents && typeof websocketEvents.broadcastV2 === "function") {
|
|
2540
|
-
websocketEvents.broadcastV2({channel: name, broadcastParams, body})
|
|
2581
|
+
websocketEvents.broadcastV2({channel: name, broadcastParams, body, configuration: this})
|
|
2541
2582
|
return
|
|
2542
2583
|
}
|
|
2543
2584
|
|
|
@@ -423,7 +423,7 @@ export default class VelociousHttpServerClientWebsocketSession {
|
|
|
423
423
|
sendGoodbye(client) {
|
|
424
424
|
const frame = Buffer.from([WEBSOCKET_FINAL_FRAME | WEBSOCKET_OPCODE_CLOSE, 0x00])
|
|
425
425
|
|
|
426
|
-
client.events.emit("output", frame)
|
|
426
|
+
client.events.emit("output", frame, {websocketFrame: true})
|
|
427
427
|
}
|
|
428
428
|
|
|
429
429
|
/**
|
|
@@ -967,7 +967,7 @@ export default class VelociousHttpServerClientWebsocketSession {
|
|
|
967
967
|
header[0] = WEBSOCKET_FINAL_FRAME | opcode
|
|
968
968
|
header[1] = payload.length
|
|
969
969
|
|
|
970
|
-
this.client.events.emit("output", Buffer.concat([header, payload]))
|
|
970
|
+
this.client.events.emit("output", Buffer.concat([header, payload]), {websocketFrame: true})
|
|
971
971
|
}
|
|
972
972
|
|
|
973
973
|
/**
|
|
@@ -1012,7 +1012,7 @@ export default class VelociousHttpServerClientWebsocketSession {
|
|
|
1012
1012
|
|
|
1013
1013
|
header[0] = WEBSOCKET_FINAL_FRAME | WEBSOCKET_OPCODE_TEXT
|
|
1014
1014
|
|
|
1015
|
-
this.client.events.emit("output", Buffer.concat([header, payload]))
|
|
1015
|
+
this.client.events.emit("output", Buffer.concat([header, payload]), {websocketFrame: true})
|
|
1016
1016
|
}
|
|
1017
1017
|
|
|
1018
1018
|
/**
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
export class ClientDeliveryQueueOverflowError extends Error {
|
|
4
|
+
/**
|
|
5
|
+
* Builds an outbound queue overflow error.
|
|
6
|
+
* @param {object} args - Overflow details.
|
|
7
|
+
* @param {number} args.clientCount - Client identifier.
|
|
8
|
+
* @param {number} args.maxBytes - Configured byte high-water mark.
|
|
9
|
+
* @param {number} args.maxFrames - Configured frame high-water mark.
|
|
10
|
+
* @param {number} args.pendingBytes - Bytes retained before rejecting the frame.
|
|
11
|
+
* @param {number} args.pendingFrames - Frames retained before rejecting the frame.
|
|
12
|
+
* @param {number} args.rejectedBytes - Rejected frame size.
|
|
13
|
+
*/
|
|
14
|
+
constructor({clientCount, maxBytes, maxFrames, pendingBytes, pendingFrames, rejectedBytes}) {
|
|
15
|
+
super(`WebSocket client ${clientCount} exceeded its outbound queue limit (${pendingFrames}/${maxFrames} frames, ${pendingBytes}/${maxBytes} bytes; rejected ${rejectedBytes} bytes)`)
|
|
16
|
+
this.name = "ClientDeliveryQueueOverflowError"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {object} DeliveryTask
|
|
22
|
+
* @property {number} byteLength - Retained complete-buffer bytes.
|
|
23
|
+
* @property {boolean} countedFrame - Whether this task is an outbound frame.
|
|
24
|
+
* @property {() => Promise<void>} delivery - Delivery operation.
|
|
25
|
+
* @property {(error?: Error) => void} settle - Settles the enqueue promise.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export default class ClientDeliveryQueue {
|
|
29
|
+
/**
|
|
30
|
+
* Builds a per-client delivery queue.
|
|
31
|
+
* @param {object} args - Queue options.
|
|
32
|
+
* @param {number} args.clientCount - Client identifier.
|
|
33
|
+
* @param {number} args.maxBytes - Byte high-water mark.
|
|
34
|
+
* @param {number} args.maxFrames - Frame high-water mark.
|
|
35
|
+
* @param {(error: ClientDeliveryQueueOverflowError) => void} args.onOverflow - Overflow handler.
|
|
36
|
+
*/
|
|
37
|
+
constructor({clientCount, maxBytes, maxFrames, onOverflow}) {
|
|
38
|
+
this.clientCount = clientCount
|
|
39
|
+
this.maxBytes = maxBytes
|
|
40
|
+
this.maxFrames = maxFrames
|
|
41
|
+
this.onOverflow = onOverflow
|
|
42
|
+
/** @type {DeliveryTask[]} */
|
|
43
|
+
this.tasks = []
|
|
44
|
+
/** @type {DeliveryTask | undefined} */
|
|
45
|
+
this.activeTask = undefined
|
|
46
|
+
this.pendingBytes = 0
|
|
47
|
+
this.pendingFrames = 0
|
|
48
|
+
this.destroyed = false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Enqueues one complete output buffer.
|
|
53
|
+
* @param {object} args - Delivery details.
|
|
54
|
+
* @param {number} args.byteLength - Exact buffer byte length.
|
|
55
|
+
* @param {() => Promise<void>} args.delivery - Delivery operation.
|
|
56
|
+
* @returns {Promise<void>} - Settles after delivery or teardown.
|
|
57
|
+
*/
|
|
58
|
+
enqueueFrame({byteLength, delivery}) {
|
|
59
|
+
if (this.destroyed) return Promise.resolve()
|
|
60
|
+
|
|
61
|
+
if (this.pendingFrames + 1 > this.maxFrames || this.pendingBytes + byteLength > this.maxBytes) {
|
|
62
|
+
const error = new ClientDeliveryQueueOverflowError({
|
|
63
|
+
clientCount: this.clientCount,
|
|
64
|
+
maxBytes: this.maxBytes,
|
|
65
|
+
maxFrames: this.maxFrames,
|
|
66
|
+
pendingBytes: this.pendingBytes,
|
|
67
|
+
pendingFrames: this.pendingFrames,
|
|
68
|
+
rejectedBytes: byteLength
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
this.onOverflow(error)
|
|
72
|
+
return Promise.reject(error)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.pendingFrames += 1
|
|
76
|
+
this.pendingBytes += byteLength
|
|
77
|
+
return this._enqueue({byteLength, countedFrame: true, delivery})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Enqueues an ordering-only operation that retains no complete output frame.
|
|
82
|
+
* @param {() => Promise<void>} delivery - Delivery operation.
|
|
83
|
+
* @returns {Promise<void>} - Settles after delivery or teardown.
|
|
84
|
+
*/
|
|
85
|
+
enqueueControl(delivery) {
|
|
86
|
+
if (this.destroyed) return Promise.resolve()
|
|
87
|
+
|
|
88
|
+
return this._enqueue({byteLength: 0, countedFrame: false, delivery})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Releases queued and active accounting during explicit client teardown.
|
|
93
|
+
* @returns {void}
|
|
94
|
+
*/
|
|
95
|
+
destroy() {
|
|
96
|
+
if (this.destroyed) return
|
|
97
|
+
|
|
98
|
+
this.destroyed = true
|
|
99
|
+
const tasks = this.activeTask ? [this.activeTask, ...this.tasks] : this.tasks
|
|
100
|
+
|
|
101
|
+
this.activeTask = undefined
|
|
102
|
+
this.tasks = []
|
|
103
|
+
this.pendingBytes = 0
|
|
104
|
+
this.pendingFrames = 0
|
|
105
|
+
|
|
106
|
+
for (const task of tasks) task.settle()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Gets current retained-buffer accounting.
|
|
111
|
+
* @returns {{pendingBytes: number, pendingFrames: number}} - Current retained-buffer accounting.
|
|
112
|
+
*/
|
|
113
|
+
snapshot() {
|
|
114
|
+
return {pendingBytes: this.pendingBytes, pendingFrames: this.pendingFrames}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Enqueues a delivery task.
|
|
119
|
+
* @param {Omit<DeliveryTask, "settle">} task - Task to enqueue.
|
|
120
|
+
* @returns {Promise<void>} - Task completion.
|
|
121
|
+
*/
|
|
122
|
+
_enqueue(task) {
|
|
123
|
+
const promise = new Promise((resolve, reject) => {
|
|
124
|
+
this.tasks.push({
|
|
125
|
+
...task,
|
|
126
|
+
settle: (error) => error ? reject(error) : resolve(undefined)
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
this._drain()
|
|
131
|
+
return promise
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Starts the next task when idle.
|
|
136
|
+
* @returns {void} - No return value.
|
|
137
|
+
*/
|
|
138
|
+
_drain() {
|
|
139
|
+
if (this.destroyed || this.activeTask) return
|
|
140
|
+
|
|
141
|
+
const task = this.tasks.shift()
|
|
142
|
+
if (!task) return
|
|
143
|
+
|
|
144
|
+
this.activeTask = task
|
|
145
|
+
void task.delivery().then(
|
|
146
|
+
() => this._finish(task),
|
|
147
|
+
(error) => this._finish(task, error)
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Finishes the active delivery task.
|
|
153
|
+
* @param {DeliveryTask} task - Completed task.
|
|
154
|
+
* @param {Error} [error] - Delivery error.
|
|
155
|
+
* @returns {void}
|
|
156
|
+
*/
|
|
157
|
+
_finish(task, error) {
|
|
158
|
+
if (this.destroyed || this.activeTask !== task) return
|
|
159
|
+
|
|
160
|
+
this.activeTask = undefined
|
|
161
|
+
if (task.countedFrame) {
|
|
162
|
+
this.pendingBytes -= task.byteLength
|
|
163
|
+
this.pendingFrames -= 1
|
|
164
|
+
}
|
|
165
|
+
task.settle(error)
|
|
166
|
+
this._drain()
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -76,6 +76,15 @@ export default class ServerClient {
|
|
|
76
76
|
})
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Immediately destroys the socket and all transport-owned write buffers.
|
|
81
|
+
* @param {Error} error - Destruction reason.
|
|
82
|
+
* @returns {void}
|
|
83
|
+
*/
|
|
84
|
+
destroy(error) {
|
|
85
|
+
if (!this.socket.destroyed) this.socket.destroy(error)
|
|
86
|
+
}
|
|
87
|
+
|
|
79
88
|
/**
|
|
80
89
|
* On socket data.
|
|
81
90
|
* @param {Buffer} chunk - Chunk.
|
|
@@ -8,6 +8,10 @@ export class VelociousHttpServerWebsocketEventsHost {
|
|
|
8
8
|
* Narrows the runtime value to the documented type.
|
|
9
9
|
* @type {Set<import("./worker-handler/index.js").default>} */
|
|
10
10
|
this.handlers = new Set()
|
|
11
|
+
/**
|
|
12
|
+
* Broadcast handlers grouped by the configuration that owns them.
|
|
13
|
+
* @type {Map<import("../configuration.js").default, Set<import("./worker-handler/index.js").default>>} */
|
|
14
|
+
this.broadcastHandlersByConfiguration = new Map()
|
|
11
15
|
this.publishQueue = Promise.resolve()
|
|
12
16
|
}
|
|
13
17
|
|
|
@@ -30,8 +34,23 @@ export class VelociousHttpServerWebsocketEventsHost {
|
|
|
30
34
|
*/
|
|
31
35
|
register(handler) {
|
|
32
36
|
this.handlers.add(handler)
|
|
37
|
+
let configurationHandlers = this.broadcastHandlersByConfiguration.get(handler.configuration)
|
|
38
|
+
|
|
39
|
+
if (!configurationHandlers) {
|
|
40
|
+
configurationHandlers = new Set()
|
|
41
|
+
this.broadcastHandlersByConfiguration.set(handler.configuration, configurationHandlers)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
configurationHandlers.add(handler)
|
|
45
|
+
|
|
46
|
+
return () => {
|
|
47
|
+
this.handlers.delete(handler)
|
|
48
|
+
configurationHandlers.delete(handler)
|
|
33
49
|
|
|
34
|
-
|
|
50
|
+
if (configurationHandlers.size === 0) {
|
|
51
|
+
this.broadcastHandlersByConfiguration.delete(handler.configuration)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
35
54
|
}
|
|
36
55
|
|
|
37
56
|
/**
|
|
@@ -69,17 +88,24 @@ export class VelociousHttpServerWebsocketEventsHost {
|
|
|
69
88
|
* @param {string} args.channel - Channel name.
|
|
70
89
|
* @param {Record<string, ?>} args.broadcastParams - Routing filter params.
|
|
71
90
|
* @param {?} args.body - Message body.
|
|
91
|
+
* @param {import("../configuration.js").default} args.configuration - Originating configuration.
|
|
72
92
|
* @returns {void}
|
|
73
93
|
*/
|
|
74
|
-
broadcastV2({body, broadcastParams, channel}) {
|
|
94
|
+
broadcastV2({body, broadcastParams, channel, configuration}) {
|
|
75
95
|
// Chain onto publishQueue so persistence completes before
|
|
76
96
|
// the next broadcast — without this, a subscriber that connects
|
|
77
97
|
// immediately after a broadcast could miss the just-persisted
|
|
78
98
|
// event when replaying from lastEventId on a slow DB.
|
|
79
99
|
this._queuePublish(async () => {
|
|
80
|
-
const persistedEvent = await this._persistV2EventIfNeeded({body, channel})
|
|
100
|
+
const persistedEvent = await this._persistV2EventIfNeeded({body, channel, configuration})
|
|
101
|
+
const dispatchedTargets = new Set()
|
|
81
102
|
|
|
82
|
-
for (const handler of this.
|
|
103
|
+
for (const handler of this.broadcastHandlersByConfiguration.get(configuration) || []) {
|
|
104
|
+
const dispatchKey = handler.websocketV2BroadcastDispatchKey()
|
|
105
|
+
|
|
106
|
+
if (dispatchedTargets.has(dispatchKey)) continue
|
|
107
|
+
|
|
108
|
+
dispatchedTargets.add(dispatchKey)
|
|
83
109
|
handler.dispatchWebsocketV2Broadcast({
|
|
84
110
|
body,
|
|
85
111
|
broadcastParams,
|
|
@@ -88,18 +114,19 @@ export class VelociousHttpServerWebsocketEventsHost {
|
|
|
88
114
|
createdAt: persistedEvent?.createdAt
|
|
89
115
|
})
|
|
90
116
|
}
|
|
91
|
-
}, "Failed to persist/broadcast V2 event")
|
|
117
|
+
}, "Failed to persist/broadcast V2 event", configuration)
|
|
92
118
|
}
|
|
93
119
|
|
|
94
120
|
/**
|
|
95
121
|
* Runs queue publish.
|
|
96
122
|
* @param {() => Promise<void>} callback - Publish work to run in order.
|
|
97
123
|
* @param {string} errorMessage - Message logged when publish work fails.
|
|
124
|
+
* @param {import("../configuration.js").default} [originatingConfiguration] - Configuration whose context owns the work.
|
|
98
125
|
* @returns {void}
|
|
99
126
|
*/
|
|
100
|
-
_queuePublish(callback, errorMessage) {
|
|
127
|
+
_queuePublish(callback, errorMessage, originatingConfiguration) {
|
|
101
128
|
const handler = this.handlers.values().next().value
|
|
102
|
-
const configuration = handler?.configuration
|
|
129
|
+
const configuration = originatingConfiguration || handler?.configuration
|
|
103
130
|
|
|
104
131
|
this.publishQueue = this.publishQueue
|
|
105
132
|
.then(async () => {
|
|
@@ -120,10 +147,11 @@ export class VelociousHttpServerWebsocketEventsHost {
|
|
|
120
147
|
* @param {object} args - Options.
|
|
121
148
|
* @param {?} args.body - Event body.
|
|
122
149
|
* @param {string} args.channel - Channel name.
|
|
150
|
+
* @param {import("../configuration.js").default} args.configuration - Originating configuration.
|
|
123
151
|
* @returns {Promise<{createdAt: string, id: string} | null>} - Persisted event metadata when storage is enabled.
|
|
124
152
|
*/
|
|
125
|
-
async _persistV2EventIfNeeded({body, channel}) {
|
|
126
|
-
return await this._persistChannelEventIfNeeded({channel, payload: body})
|
|
153
|
+
async _persistV2EventIfNeeded({body, channel, configuration}) {
|
|
154
|
+
return await this._persistChannelEventIfNeeded({channel, payload: body, configuration})
|
|
127
155
|
}
|
|
128
156
|
|
|
129
157
|
/**
|
|
@@ -142,14 +170,16 @@ export class VelociousHttpServerWebsocketEventsHost {
|
|
|
142
170
|
* @param {object} args - Options object.
|
|
143
171
|
* @param {string} args.channel - Channel name.
|
|
144
172
|
* @param {?} args.payload - Payload data.
|
|
173
|
+
* @param {import("../configuration.js").default} [args.configuration] - Configuration owning the event store.
|
|
145
174
|
* @returns {Promise<{createdAt: string, id: string} | null>} - Persisted event metadata.
|
|
146
175
|
*/
|
|
147
|
-
async _persistChannelEventIfNeeded({channel, payload}) {
|
|
176
|
+
async _persistChannelEventIfNeeded({channel, payload, configuration}) {
|
|
148
177
|
const handler = this.handlers.values().next().value
|
|
178
|
+
const eventConfiguration = configuration || handler?.configuration
|
|
149
179
|
|
|
150
|
-
if (!
|
|
180
|
+
if (!eventConfiguration) return null
|
|
151
181
|
|
|
152
|
-
const websocketEventLogStore = websocketEventLogStoreForConfiguration(
|
|
182
|
+
const websocketEventLogStore = websocketEventLogStoreForConfiguration(eventConfiguration)
|
|
153
183
|
const shouldPersist = await websocketEventLogStore.shouldPersistChannel(channel)
|
|
154
184
|
|
|
155
185
|
if (!shouldPersist) return null
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import Client from "../client/index.js"
|
|
4
|
+
import ClientDeliveryQueue from "../client-delivery-queue.js"
|
|
4
5
|
import dispatchChannelSubscribers from "./channel-subscriber-dispatch.js"
|
|
5
6
|
import Logger from "../../logger.js"
|
|
6
7
|
import websocketEventsHost from "../websocket-events-host.js"
|
|
@@ -23,7 +24,7 @@ export default class VelociousHttpServerInProcessHandler {
|
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Narrows the runtime value to the documented type.
|
|
26
|
-
* @type {Record<number, {httpClient: Client, serverClient: import("../server-client.js").default}>} */
|
|
27
|
+
* @type {Record<number, {deliveryQueue: ClientDeliveryQueue, httpClient: Client, serverClient: import("../server-client.js").default}>} */
|
|
27
28
|
this.clients = {}
|
|
28
29
|
|
|
29
30
|
/** @type {Set<Promise<void>>} */
|
|
@@ -56,25 +57,36 @@ export default class VelociousHttpServerInProcessHandler {
|
|
|
56
57
|
remoteAddress: serverClient.remoteAddress
|
|
57
58
|
})
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
const {maxBytes, maxFrames} = this.configuration.getWebsocketOutboundQueueLimits()
|
|
61
|
+
const deliveryQueue = new ClientDeliveryQueue({
|
|
62
|
+
clientCount,
|
|
63
|
+
maxBytes,
|
|
64
|
+
maxFrames,
|
|
65
|
+
onOverflow: (error) => {
|
|
66
|
+
deliveryQueue.destroy()
|
|
67
|
+
serverClient.destroy(error)
|
|
68
|
+
this._reportOutboundQueueOverflow({clientCount, error})
|
|
69
|
+
}
|
|
70
|
+
})
|
|
67
71
|
|
|
68
|
-
httpClient.events.on("output", (output) => {
|
|
72
|
+
httpClient.events.on("output", (output, {websocketFrame = false} = {}) => {
|
|
69
73
|
if (output !== null && output !== undefined) {
|
|
70
|
-
|
|
74
|
+
const delivery = () => serverClient.send(output)
|
|
75
|
+
const queued = websocketFrame
|
|
76
|
+
? deliveryQueue.enqueueFrame({
|
|
77
|
+
byteLength: typeof output === "string" ? Buffer.byteLength(output) : output.byteLength,
|
|
78
|
+
delivery
|
|
79
|
+
})
|
|
80
|
+
: deliveryQueue.enqueueControl(delivery)
|
|
81
|
+
|
|
82
|
+
void queued.catch((error) => {
|
|
71
83
|
this.logger.error(() => ["Failed to deliver client output", {clientCount}, error])
|
|
72
84
|
})
|
|
73
85
|
}
|
|
74
86
|
})
|
|
75
87
|
|
|
76
88
|
httpClient.events.on("file", ({filePath, sendBody, settle}) => {
|
|
77
|
-
void
|
|
89
|
+
void deliveryQueue.enqueueControl(async () => {
|
|
78
90
|
await settle(await serverClient.sendFile(filePath, sendBody))
|
|
79
91
|
}).catch((error) => {
|
|
80
92
|
this.logger.error(() => ["Failed to deliver file response", {clientCount, filePath}, error])
|
|
@@ -83,11 +95,12 @@ export default class VelociousHttpServerInProcessHandler {
|
|
|
83
95
|
})
|
|
84
96
|
|
|
85
97
|
httpClient.events.on("close", () => {
|
|
86
|
-
void
|
|
98
|
+
void deliveryQueue.enqueueControl(() => serverClient.end())
|
|
87
99
|
.finally(() => delete this.clients[clientCount])
|
|
88
100
|
})
|
|
89
101
|
|
|
90
102
|
serverClient.events.on("close", () => {
|
|
103
|
+
deliveryQueue.destroy()
|
|
91
104
|
const cleanup = httpClient.abortPendingFileResponses()
|
|
92
105
|
.catch((error) => {
|
|
93
106
|
this.logger.warn("Failed to abort file responses after client close", error)
|
|
@@ -100,7 +113,7 @@ export default class VelociousHttpServerInProcessHandler {
|
|
|
100
113
|
this.pendingClientCloseCleanups.add(cleanup)
|
|
101
114
|
})
|
|
102
115
|
|
|
103
|
-
this.clients[clientCount] = {httpClient, serverClient}
|
|
116
|
+
this.clients[clientCount] = {deliveryQueue, httpClient, serverClient}
|
|
104
117
|
|
|
105
118
|
// Create a message-port shim so ServerClient.onSocketData can route data
|
|
106
119
|
// to the in-process HTTP Client without needing a real worker thread.
|
|
@@ -118,6 +131,24 @@ export default class VelociousHttpServerInProcessHandler {
|
|
|
118
131
|
serverClient.listen()
|
|
119
132
|
}
|
|
120
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Reports a per-client outbound queue overflow.
|
|
136
|
+
* @param {object} args - Overflow details.
|
|
137
|
+
* @param {number} args.clientCount - Affected client.
|
|
138
|
+
* @param {Error} args.error - Overflow error.
|
|
139
|
+
* @returns {void}
|
|
140
|
+
*/
|
|
141
|
+
_reportOutboundQueueOverflow({clientCount, error}) {
|
|
142
|
+
const errorPayload = {
|
|
143
|
+
context: {clientCount, websocketOutboundQueueOverflow: true, workerCount: this.workerCount},
|
|
144
|
+
error
|
|
145
|
+
}
|
|
146
|
+
const errorEvents = this.configuration.getErrorEvents()
|
|
147
|
+
|
|
148
|
+
errorEvents.emit("framework-error", errorPayload)
|
|
149
|
+
errorEvents.emit("all-error", {...errorPayload, errorType: "framework-error"})
|
|
150
|
+
}
|
|
151
|
+
|
|
121
152
|
/**
|
|
122
153
|
* Runs stop.
|
|
123
154
|
* @returns {Promise<void>} */
|
|
@@ -158,6 +189,14 @@ export default class VelociousHttpServerInProcessHandler {
|
|
|
158
189
|
return this.configuration._broadcastToChannelLocal(channel, broadcastParams, body, {eventId})
|
|
159
190
|
}
|
|
160
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Gets the configuration-wide V2 broadcast target shared by in-process handlers.
|
|
194
|
+
* @returns {import("../../configuration.js").default} - Shared configuration target.
|
|
195
|
+
*/
|
|
196
|
+
websocketV2BroadcastDispatchKey() {
|
|
197
|
+
return this.configuration
|
|
198
|
+
}
|
|
199
|
+
|
|
161
200
|
/**
|
|
162
201
|
* Runs dispatch websocket event.
|
|
163
202
|
* @param {object} args - Options object.
|