velocious 1.0.559 → 1.0.561
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 +7 -2
- package/build/background-jobs/worker.js +39 -11
- package/build/database/drivers/base.js +9 -2
- package/build/database/drivers/mysql/index.js +11 -2
- package/build/database/drivers/mysql/query.js +198 -22
- package/build/database/query/index.js +16 -1
- package/build/database/query/model-class-query.js +3 -2
- package/build/database/query/query-data.js +75 -7
- package/build/database/query/with-count.js +64 -28
- package/build/database/query-aborted-error.js +24 -0
- package/build/http-server/client/websocket-session.js +134 -28
- package/build/src/background-jobs/worker.d.ts +15 -1
- package/build/src/background-jobs/worker.d.ts.map +1 -1
- package/build/src/background-jobs/worker.js +35 -13
- package/build/src/database/drivers/base.d.ts +6 -1
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +10 -3
- package/build/src/database/drivers/mssql/index.d.ts +6 -0
- package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mysql/index.js +12 -3
- package/build/src/database/drivers/mysql/query.d.ts +13 -3
- package/build/src/database/drivers/mysql/query.d.ts.map +1 -1
- package/build/src/database/drivers/mysql/query.js +172 -23
- package/build/src/database/drivers/pgsql/index.d.ts +6 -0
- package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/index.d.ts +6 -0
- package/build/src/database/drivers/sqlite/index.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/index.native.d.ts +6 -0
- package/build/src/database/drivers/sqlite/index.native.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/index.web.d.ts +6 -0
- package/build/src/database/drivers/sqlite/index.web.d.ts.map +1 -1
- package/build/src/database/query/index.d.ts +13 -1
- package/build/src/database/query/index.d.ts.map +1 -1
- package/build/src/database/query/index.js +15 -3
- package/build/src/database/query/model-class-query.d.ts.map +1 -1
- package/build/src/database/query/model-class-query.js +4 -3
- package/build/src/database/query/query-data.d.ts.map +1 -1
- package/build/src/database/query/query-data.js +69 -8
- package/build/src/database/query/with-count.d.ts.map +1 -1
- package/build/src/database/query/with-count.js +60 -26
- package/build/src/database/query-aborted-error.d.ts +23 -0
- package/build/src/database/query-aborted-error.d.ts.map +1 -0
- package/build/src/database/query-aborted-error.js +23 -0
- package/build/src/http-server/client/websocket-session.d.ts +31 -3
- package/build/src/http-server/client/websocket-session.d.ts.map +1 -1
- package/build/src/http-server/client/websocket-session.js +112 -26
- package/build/src/tenants/tenant-aggregator.d.ts +4 -0
- package/build/src/tenants/tenant-aggregator.d.ts.map +1 -1
- package/build/src/tenants/tenant-aggregator.js +4 -3
- package/build/tenants/tenant-aggregator.js +3 -2
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -1
- package/src/background-jobs/worker.js +39 -11
- package/src/database/drivers/base.js +9 -2
- package/src/database/drivers/mysql/index.js +11 -2
- package/src/database/drivers/mysql/query.js +198 -22
- package/src/database/query/index.js +16 -1
- package/src/database/query/model-class-query.js +3 -2
- package/src/database/query/query-data.js +75 -7
- package/src/database/query/with-count.js +64 -28
- package/src/database/query-aborted-error.js +24 -0
- package/src/http-server/client/websocket-session.js +134 -28
- package/src/tenants/tenant-aggregator.js +3 -2
|
@@ -23,6 +23,11 @@ const WEBSOCKET_PAUSED_QUEUE_CAP = 1000
|
|
|
23
23
|
/** Cap on total bytes buffered for a single fragmented message. */
|
|
24
24
|
const WEBSOCKET_MAX_FRAGMENTED_MESSAGE_BYTES = 16 * 1024 * 1024
|
|
25
25
|
|
|
26
|
+
/** Cap on payload bytes buffered for a single final data frame. */
|
|
27
|
+
const WEBSOCKET_MAX_FINAL_FRAME_BYTES = WEBSOCKET_MAX_FRAGMENTED_MESSAGE_BYTES
|
|
28
|
+
|
|
29
|
+
const WEBSOCKET_MAX_INBOUND_FRAME_BYTES_BIGINT = BigInt(WEBSOCKET_MAX_FINAL_FRAME_BYTES)
|
|
30
|
+
|
|
26
31
|
/** Cap on fragment count for a single fragmented message. */
|
|
27
32
|
const WEBSOCKET_MAX_FRAGMENTED_MESSAGE_FRAGMENTS = 1024
|
|
28
33
|
|
|
@@ -97,7 +102,12 @@ export default class VelociousHttpServerClientWebsocketSession {
|
|
|
97
102
|
* @param {Promise<import("../../configuration-types.js").WebsocketMessageHandler | void>} [args.messageHandlerPromise] - Optional raw message handler promise.
|
|
98
103
|
*/
|
|
99
104
|
constructor({client, configuration, upgradeRequest, messageHandler, messageHandlerPromise}) {
|
|
100
|
-
|
|
105
|
+
/** @type {Buffer[]} */
|
|
106
|
+
this._bufferChunks = []
|
|
107
|
+
this._bufferChunkIndex = 0
|
|
108
|
+
this._bufferChunkOffset = 0
|
|
109
|
+
this._bufferedBytes = 0
|
|
110
|
+
this._bufferedFrameCopyBytes = 0
|
|
101
111
|
this.client = client
|
|
102
112
|
this.configuration = configuration
|
|
103
113
|
this.upgradeRequest = upgradeRequest
|
|
@@ -301,7 +311,10 @@ export default class VelociousHttpServerClientWebsocketSession {
|
|
|
301
311
|
// socket is alive. Mark it here, before `_processBuffer` may return
|
|
302
312
|
// early waiting for the rest of an incomplete frame.
|
|
303
313
|
this._heartbeatAlive = true
|
|
304
|
-
|
|
314
|
+
if (data.length === 0) return
|
|
315
|
+
|
|
316
|
+
this._bufferChunks.push(data)
|
|
317
|
+
this._bufferedBytes += data.length
|
|
305
318
|
this._processBuffer()
|
|
306
319
|
}
|
|
307
320
|
|
|
@@ -588,9 +601,10 @@ export default class VelociousHttpServerClientWebsocketSession {
|
|
|
588
601
|
* @returns {void} - No return value.
|
|
589
602
|
*/
|
|
590
603
|
_processBuffer() {
|
|
591
|
-
while (this.
|
|
592
|
-
const
|
|
593
|
-
const
|
|
604
|
+
while (this._bufferedBytes >= 2) {
|
|
605
|
+
const initialHeader = this._peekBufferedBytes(2)
|
|
606
|
+
const firstByte = initialHeader[0]
|
|
607
|
+
const secondByte = initialHeader[1]
|
|
594
608
|
const isFinal = (firstByte & WEBSOCKET_FINAL_FRAME) === WEBSOCKET_FINAL_FRAME
|
|
595
609
|
const opcode = firstByte & 0x0F
|
|
596
610
|
const isMasked = (secondByte & 0x80) === 0x80
|
|
@@ -598,12 +612,21 @@ export default class VelociousHttpServerClientWebsocketSession {
|
|
|
598
612
|
let offset = 2
|
|
599
613
|
|
|
600
614
|
if (payloadLength === 126) {
|
|
601
|
-
if (this.
|
|
602
|
-
payloadLength = this.
|
|
615
|
+
if (this._bufferedBytes < offset + 2) return
|
|
616
|
+
payloadLength = this._peekBufferedBytes(offset + 2).readUInt16BE(offset)
|
|
603
617
|
offset += 2
|
|
604
618
|
} else if (payloadLength === 127) {
|
|
605
|
-
if (this.
|
|
606
|
-
const bigLength = this.
|
|
619
|
+
if (this._bufferedBytes < offset + 8) return
|
|
620
|
+
const bigLength = this._peekBufferedBytes(offset + 8).readBigUInt64BE(offset)
|
|
621
|
+
|
|
622
|
+
if (bigLength > WEBSOCKET_MAX_INBOUND_FRAME_BYTES_BIGINT) {
|
|
623
|
+
this.logger.warn(() => [
|
|
624
|
+
"Websocket frame exceeded byte cap; closing connection",
|
|
625
|
+
{frameBytes: bigLength.toString(), maxBytes: WEBSOCKET_MAX_FINAL_FRAME_BYTES}
|
|
626
|
+
])
|
|
627
|
+
this._closeForInboundLimit()
|
|
628
|
+
return
|
|
629
|
+
}
|
|
607
630
|
|
|
608
631
|
payloadLength = Number(bigLength)
|
|
609
632
|
offset += 8
|
|
@@ -611,18 +634,20 @@ export default class VelociousHttpServerClientWebsocketSession {
|
|
|
611
634
|
|
|
612
635
|
const maskLength = isMasked ? 4 : 0
|
|
613
636
|
|
|
614
|
-
|
|
637
|
+
const frameLength = offset + maskLength + payloadLength
|
|
638
|
+
|
|
639
|
+
if (this._bufferedBytes < frameLength) return
|
|
640
|
+
|
|
641
|
+
const frame = this._consumeBufferedBytes(frameLength)
|
|
615
642
|
|
|
616
643
|
/** @type {Buffer} */
|
|
617
|
-
let payload =
|
|
644
|
+
let payload = frame.subarray(offset + maskLength, frameLength)
|
|
618
645
|
|
|
619
646
|
if (isMasked) {
|
|
620
|
-
const mask =
|
|
621
|
-
|
|
647
|
+
const mask = frame.subarray(offset, offset + maskLength)
|
|
648
|
+
this._unmaskPayload(payload, mask)
|
|
622
649
|
}
|
|
623
650
|
|
|
624
|
-
this.buffer = this.buffer.slice(offset + maskLength + payloadLength)
|
|
625
|
-
|
|
626
651
|
// Control frames (opcode >= 0x8) must not be fragmented per
|
|
627
652
|
// RFC 6455 and can arrive interleaved with a fragmented data
|
|
628
653
|
// message. Handle them first without touching the fragment
|
|
@@ -726,6 +751,97 @@ export default class VelociousHttpServerClientWebsocketSession {
|
|
|
726
751
|
}
|
|
727
752
|
}
|
|
728
753
|
|
|
754
|
+
/**
|
|
755
|
+
* Copies the leading buffered bytes without consuming them. Header
|
|
756
|
+
* inspection is bounded to the websocket header size.
|
|
757
|
+
* @param {number} byteCount - Number of leading bytes to inspect.
|
|
758
|
+
* @returns {Buffer} - Copied prefix.
|
|
759
|
+
*/
|
|
760
|
+
_peekBufferedBytes(byteCount) {
|
|
761
|
+
const prefix = Buffer.allocUnsafe(byteCount)
|
|
762
|
+
let copiedBytes = 0
|
|
763
|
+
let chunkOffset = this._bufferChunkOffset
|
|
764
|
+
|
|
765
|
+
for (let chunkIndex = this._bufferChunkIndex; chunkIndex < this._bufferChunks.length; chunkIndex += 1) {
|
|
766
|
+
const chunk = this._bufferChunks[chunkIndex]
|
|
767
|
+
const bytesFromChunk = Math.min(chunk.length - chunkOffset, byteCount - copiedBytes)
|
|
768
|
+
|
|
769
|
+
chunk.copy(prefix, copiedBytes, chunkOffset, chunkOffset + bytesFromChunk)
|
|
770
|
+
copiedBytes += bytesFromChunk
|
|
771
|
+
chunkOffset = 0
|
|
772
|
+
if (copiedBytes === byteCount) break
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return prefix
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Consumes a complete frame from the chunk queue with one bounded copy.
|
|
780
|
+
* @param {number} byteCount - Complete frame byte count.
|
|
781
|
+
* @returns {Buffer} - Contiguous frame bytes.
|
|
782
|
+
*/
|
|
783
|
+
_consumeBufferedBytes(byteCount) {
|
|
784
|
+
const result = Buffer.allocUnsafe(byteCount)
|
|
785
|
+
let copiedBytes = 0
|
|
786
|
+
|
|
787
|
+
while (copiedBytes < byteCount) {
|
|
788
|
+
const chunk = this._bufferChunks[this._bufferChunkIndex]
|
|
789
|
+
const bytesFromChunk = Math.min(chunk.length - this._bufferChunkOffset, byteCount - copiedBytes)
|
|
790
|
+
|
|
791
|
+
chunk.copy(
|
|
792
|
+
result,
|
|
793
|
+
copiedBytes,
|
|
794
|
+
this._bufferChunkOffset,
|
|
795
|
+
this._bufferChunkOffset + bytesFromChunk
|
|
796
|
+
)
|
|
797
|
+
copiedBytes += bytesFromChunk
|
|
798
|
+
this._bufferChunkOffset += bytesFromChunk
|
|
799
|
+
|
|
800
|
+
if (this._bufferChunkOffset === chunk.length) {
|
|
801
|
+
this._bufferChunkIndex += 1
|
|
802
|
+
this._bufferChunkOffset = 0
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (this._bufferChunkIndex === this._bufferChunks.length) {
|
|
807
|
+
this._bufferChunks = []
|
|
808
|
+
this._bufferChunkIndex = 0
|
|
809
|
+
} else if (
|
|
810
|
+
this._bufferChunkIndex >= 64 &&
|
|
811
|
+
this._bufferChunkIndex * 2 >= this._bufferChunks.length
|
|
812
|
+
) {
|
|
813
|
+
this._bufferChunks = this._bufferChunks.slice(this._bufferChunkIndex)
|
|
814
|
+
this._bufferChunkIndex = 0
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
this._bufferedBytes -= byteCount
|
|
818
|
+
this._bufferedFrameCopyBytes += byteCount
|
|
819
|
+
|
|
820
|
+
return result
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Drops all incomplete frame chunks.
|
|
825
|
+
* @returns {void}
|
|
826
|
+
*/
|
|
827
|
+
_clearBufferedFrameChunks() {
|
|
828
|
+
this._bufferChunks = []
|
|
829
|
+
this._bufferChunkIndex = 0
|
|
830
|
+
this._bufferChunkOffset = 0
|
|
831
|
+
this._bufferedBytes = 0
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Closes after an inbound buffering limit and releases all parser-owned input.
|
|
836
|
+
* @returns {void}
|
|
837
|
+
*/
|
|
838
|
+
_closeForInboundLimit() {
|
|
839
|
+
this._resetFragmentBuffer()
|
|
840
|
+
this._clearBufferedFrameChunks()
|
|
841
|
+
this.sendGoodbye(this.client)
|
|
842
|
+
this._handleClose()
|
|
843
|
+
}
|
|
844
|
+
|
|
729
845
|
/**
|
|
730
846
|
* Appends a continuation-frame payload to the in-progress
|
|
731
847
|
* fragmented message. Returns true when the fragment was accepted
|
|
@@ -771,10 +887,7 @@ export default class VelociousHttpServerClientWebsocketSession {
|
|
|
771
887
|
}
|
|
772
888
|
])
|
|
773
889
|
|
|
774
|
-
this.
|
|
775
|
-
this.buffer = Buffer.alloc(0)
|
|
776
|
-
this.sendGoodbye(this.client)
|
|
777
|
-
this._handleClose()
|
|
890
|
+
this._closeForInboundLimit()
|
|
778
891
|
|
|
779
892
|
return false
|
|
780
893
|
}
|
|
@@ -1702,19 +1815,12 @@ export default class VelociousHttpServerClientWebsocketSession {
|
|
|
1702
1815
|
* Runs unmask payload.
|
|
1703
1816
|
* @param {Buffer} payload - Payload data.
|
|
1704
1817
|
* @param {Buffer} mask - Mask.
|
|
1705
|
-
* @returns {
|
|
1818
|
+
* @returns {void} - No return value.
|
|
1706
1819
|
*/
|
|
1707
1820
|
_unmaskPayload(payload, mask) {
|
|
1708
|
-
/**
|
|
1709
|
-
* Result.
|
|
1710
|
-
* @type {Buffer} */
|
|
1711
|
-
const result = Buffer.alloc(payload.length)
|
|
1712
|
-
|
|
1713
1821
|
for (let i = 0; i < payload.length; i++) {
|
|
1714
|
-
|
|
1822
|
+
payload[i] ^= mask[i % 4]
|
|
1715
1823
|
}
|
|
1716
|
-
|
|
1717
|
-
return result
|
|
1718
1824
|
}
|
|
1719
1825
|
|
|
1720
1826
|
async _runMessageHandlerOpen() {
|
|
@@ -25,6 +25,7 @@ import Current from "../current.js"
|
|
|
25
25
|
* @property {(tenant: ?) => boolean} [filter] - Optional filter applied to the resolved tenant list.
|
|
26
26
|
* @property {string[]} [keyColumns] - Columns the aggregate is grouped by (for example `["docker_server_id"]`). Empty means a single grand-total row.
|
|
27
27
|
* @property {Record<string, AggregateSpec>} aggregates - Output column name to aggregate. `"SUM"` is shorthand for `{op: "SUM", column: <output name>}`; use `{op: "COUNT", column: "*"}` for `COUNT(*)`.
|
|
28
|
+
* @property {AbortSignal} [signal] - Signal passed to each aggregate database query.
|
|
28
29
|
* @property {(context: SubqueryContext) => string} subquery - Builds one tenant's inner `SELECT`, which must select every `keyColumns` entry plus every aggregate source column.
|
|
29
30
|
* @property {import("../configuration.js").default} [configuration] - Configuration to run against. Defaults to the current configuration.
|
|
30
31
|
*/
|
|
@@ -169,7 +170,7 @@ export default class TenantAggregator {
|
|
|
169
170
|
return await this._withTenant(firstTenant.tenant, async (connections) => {
|
|
170
171
|
const connection = connections[this.options.identifier]
|
|
171
172
|
|
|
172
|
-
return await connection.query(this.buildAggregateSql({connection, entries: group, qualified: true}))
|
|
173
|
+
return await connection.query(this.buildAggregateSql({connection, entries: group, qualified: true}), {signal: this.options.signal})
|
|
173
174
|
})
|
|
174
175
|
}
|
|
175
176
|
|
|
@@ -180,7 +181,7 @@ export default class TenantAggregator {
|
|
|
180
181
|
const tenantRows = await this._withTenant(resolvedTenant.tenant, async (connections) => {
|
|
181
182
|
const connection = connections[this.options.identifier]
|
|
182
183
|
|
|
183
|
-
return await connection.query(this.buildAggregateSql({connection, entries: [resolvedTenant], qualified: false}))
|
|
184
|
+
return await connection.query(this.buildAggregateSql({connection, entries: [resolvedTenant], qualified: false}), {signal: this.options.signal})
|
|
184
185
|
})
|
|
185
186
|
|
|
186
187
|
rows.push(...tenantRows)
|