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.
Files changed (64) hide show
  1. package/README.md +7 -2
  2. package/build/background-jobs/worker.js +39 -11
  3. package/build/database/drivers/base.js +9 -2
  4. package/build/database/drivers/mysql/index.js +11 -2
  5. package/build/database/drivers/mysql/query.js +198 -22
  6. package/build/database/query/index.js +16 -1
  7. package/build/database/query/model-class-query.js +3 -2
  8. package/build/database/query/query-data.js +75 -7
  9. package/build/database/query/with-count.js +64 -28
  10. package/build/database/query-aborted-error.js +24 -0
  11. package/build/http-server/client/websocket-session.js +134 -28
  12. package/build/src/background-jobs/worker.d.ts +15 -1
  13. package/build/src/background-jobs/worker.d.ts.map +1 -1
  14. package/build/src/background-jobs/worker.js +35 -13
  15. package/build/src/database/drivers/base.d.ts +6 -1
  16. package/build/src/database/drivers/base.d.ts.map +1 -1
  17. package/build/src/database/drivers/base.js +10 -3
  18. package/build/src/database/drivers/mssql/index.d.ts +6 -0
  19. package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
  20. package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
  21. package/build/src/database/drivers/mysql/index.js +12 -3
  22. package/build/src/database/drivers/mysql/query.d.ts +13 -3
  23. package/build/src/database/drivers/mysql/query.d.ts.map +1 -1
  24. package/build/src/database/drivers/mysql/query.js +172 -23
  25. package/build/src/database/drivers/pgsql/index.d.ts +6 -0
  26. package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
  27. package/build/src/database/drivers/sqlite/index.d.ts +6 -0
  28. package/build/src/database/drivers/sqlite/index.d.ts.map +1 -1
  29. package/build/src/database/drivers/sqlite/index.native.d.ts +6 -0
  30. package/build/src/database/drivers/sqlite/index.native.d.ts.map +1 -1
  31. package/build/src/database/drivers/sqlite/index.web.d.ts +6 -0
  32. package/build/src/database/drivers/sqlite/index.web.d.ts.map +1 -1
  33. package/build/src/database/query/index.d.ts +13 -1
  34. package/build/src/database/query/index.d.ts.map +1 -1
  35. package/build/src/database/query/index.js +15 -3
  36. package/build/src/database/query/model-class-query.d.ts.map +1 -1
  37. package/build/src/database/query/model-class-query.js +4 -3
  38. package/build/src/database/query/query-data.d.ts.map +1 -1
  39. package/build/src/database/query/query-data.js +69 -8
  40. package/build/src/database/query/with-count.d.ts.map +1 -1
  41. package/build/src/database/query/with-count.js +60 -26
  42. package/build/src/database/query-aborted-error.d.ts +23 -0
  43. package/build/src/database/query-aborted-error.d.ts.map +1 -0
  44. package/build/src/database/query-aborted-error.js +23 -0
  45. package/build/src/http-server/client/websocket-session.d.ts +31 -3
  46. package/build/src/http-server/client/websocket-session.d.ts.map +1 -1
  47. package/build/src/http-server/client/websocket-session.js +112 -26
  48. package/build/src/tenants/tenant-aggregator.d.ts +4 -0
  49. package/build/src/tenants/tenant-aggregator.d.ts.map +1 -1
  50. package/build/src/tenants/tenant-aggregator.js +4 -3
  51. package/build/tenants/tenant-aggregator.js +3 -2
  52. package/build/tsconfig.tsbuildinfo +1 -1
  53. package/package.json +3 -1
  54. package/src/background-jobs/worker.js +39 -11
  55. package/src/database/drivers/base.js +9 -2
  56. package/src/database/drivers/mysql/index.js +11 -2
  57. package/src/database/drivers/mysql/query.js +198 -22
  58. package/src/database/query/index.js +16 -1
  59. package/src/database/query/model-class-query.js +3 -2
  60. package/src/database/query/query-data.js +75 -7
  61. package/src/database/query/with-count.js +64 -28
  62. package/src/database/query-aborted-error.js +24 -0
  63. package/src/http-server/client/websocket-session.js +134 -28
  64. 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
- this.buffer = Buffer.alloc(0)
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
- this.buffer = Buffer.concat([this.buffer, data])
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.buffer.length >= 2) {
592
- const firstByte = this.buffer[0]
593
- const secondByte = this.buffer[1]
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.buffer.length < offset + 2) return
602
- payloadLength = this.buffer.readUInt16BE(offset)
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.buffer.length < offset + 8) return
606
- const bigLength = this.buffer.readBigUInt64BE(offset)
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
- if (this.buffer.length < offset + maskLength + payloadLength) return
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 = this.buffer.slice(offset + maskLength, offset + maskLength + payloadLength)
644
+ let payload = frame.subarray(offset + maskLength, frameLength)
618
645
 
619
646
  if (isMasked) {
620
- const mask = this.buffer.slice(offset, offset + maskLength)
621
- payload = this._unmaskPayload(payload, mask)
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._resetFragmentBuffer()
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 {Buffer} - The unmask payload.
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
- result[i] = payload[i] ^ mask[i % 4]
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)