@reddb-io/client 1.26.4 → 1.26.5

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/redwire-core.js +64 -40
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reddb-io/client",
3
- "version": "1.26.4",
3
+ "version": "1.26.5",
4
4
  "description": "Thin remote-only RedDB driver. Pure JavaScript — no install scripts, no native binary. Speaks RedWire/gRPC/HTTP. Embedded URIs (memory://, file://, red:///path) are rejected — use @reddb-io/sdk for those.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -672,28 +672,30 @@ export function decodeFrame(buf) {
672
672
  class FrameReader {
673
673
  constructor(socket) {
674
674
  this.chunks = []
675
+ this.chunkIndex = 0
676
+ this.chunkOffset = 0
675
677
  this.totalLen = 0
678
+ this.frameLength = 0
676
679
  this.waiters = []
677
680
  this.error = null
678
681
  this.eof = false
679
682
  socket.on('data', (chunk) => {
683
+ if (this.error || this.eof) return
680
684
  // chunk is Buffer (Node) or Uint8Array (Bun/Deno)
681
685
  const u8 = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk)
686
+ if (u8.length === 0) return
682
687
  this.chunks.push(u8)
683
688
  this.totalLen += u8.length
684
689
  this.#tryDeliver()
685
690
  })
686
- socket.on('error', (err) => {
687
- this.error = err
688
- this.#flushWaiters()
689
- })
691
+ socket.on('error', (err) => this.#fail(err))
690
692
  socket.on('end', () => {
691
693
  this.eof = true
692
- this.#flushWaiters()
694
+ this.#tryDeliver()
693
695
  })
694
696
  socket.on('close', () => {
695
697
  this.eof = true
696
- this.#flushWaiters()
698
+ this.#tryDeliver()
697
699
  })
698
700
  }
699
701
 
@@ -706,57 +708,79 @@ class FrameReader {
706
708
  }
707
709
 
708
710
  #tryDeliver() {
709
- while (this.waiters.length > 0 && this.totalLen > 0) {
710
- const flat = this.#flatten()
711
+ while (this.waiters.length > 0 && this.totalLen >= FRAME_HEADER_SIZE) {
711
712
  let frame
712
713
  try {
713
- frame = decodeFrame(flat)
714
+ if (this.frameLength === 0) this.frameLength = this.#readLength()
715
+ if (this.totalLen < this.frameLength) break
716
+ frame = decodeFrame(this.#take(this.frameLength))
717
+ this.frameLength = 0
714
718
  } catch (err) {
715
- const w = this.waiters.shift()
716
- w.reject(err)
717
- return
718
- }
719
- if (frame == null) {
720
- // Need more bytes — put the flattened buffer back as a
721
- // single chunk so we don't keep flattening repeatedly.
722
- this.chunks = [flat]
719
+ this.#fail(err)
723
720
  return
724
721
  }
725
- this.chunks = [flat.subarray(frame.consumed)]
726
- this.totalLen = this.chunks[0].length
727
722
  const w = this.waiters.shift()
728
723
  w.resolve(frame)
729
724
  }
730
- if (this.eof && this.waiters.length > 0 && this.totalLen === 0) {
731
- const err = this.error ?? new RedDBError('CONNECTION_CLOSED', 'redwire: connection closed')
732
- while (this.waiters.length > 0) {
733
- this.waiters.shift().reject(err)
734
- }
725
+ if (this.eof && this.waiters.length > 0) {
726
+ this.#fail(new RedDBError('CONNECTION_CLOSED', 'redwire: connection closed'))
735
727
  }
736
728
  }
737
729
 
738
- #flushWaiters() {
739
- if (this.waiters.length === 0) return
740
- if (this.totalLen > 0) {
741
- this.#tryDeliver()
742
- return
730
+ #readLength() {
731
+ let length = 0
732
+ let shift = 0
733
+ for (let index = this.chunkIndex; shift < 32; index++) {
734
+ const chunk = this.chunks[index]
735
+ const start = index === this.chunkIndex ? this.chunkOffset : 0
736
+ for (let offset = start; offset < chunk.length && shift < 32; offset++, shift += 8) {
737
+ length += chunk[offset] * 2 ** shift
738
+ }
743
739
  }
744
- const err = this.error ?? new RedDBError('CONNECTION_CLOSED', 'redwire: connection closed')
740
+ if (length < FRAME_HEADER_SIZE || length > MAX_FRAME_SIZE) {
741
+ throw new RedDBError('FRAME_INVALID_LENGTH', `length=${length}`)
742
+ }
743
+ return length
744
+ }
745
+
746
+ #fail(error) {
747
+ this.error ??= error
748
+ this.chunks = []
749
+ this.chunkIndex = 0
750
+ this.chunkOffset = 0
751
+ this.totalLen = 0
752
+ this.frameLength = 0
745
753
  while (this.waiters.length > 0) {
746
- this.waiters.shift().reject(err)
754
+ this.waiters.shift().reject(this.error)
747
755
  }
748
756
  }
749
757
 
750
- #flatten() {
751
- if (this.chunks.length === 1) return this.chunks[0]
752
- const out = new Uint8Array(this.totalLen)
753
- let off = 0
754
- for (const c of this.chunks) {
755
- out.set(c, off)
756
- off += c.length
758
+ #take(length) {
759
+ const first = this.chunks[this.chunkIndex]
760
+ const contiguous = first.length - this.chunkOffset >= length
761
+ // A plain Uint8Array keeps decodeFrame's payload slice owned, even for Buffer input.
762
+ const bytes = contiguous
763
+ ? new Uint8Array(first.buffer, first.byteOffset + this.chunkOffset, length)
764
+ : new Uint8Array(length)
765
+ let written = 0
766
+ while (written < length) {
767
+ const chunk = this.chunks[this.chunkIndex]
768
+ const count = Math.min(length - written, chunk.length - this.chunkOffset)
769
+ if (!contiguous) bytes.set(chunk.subarray(this.chunkOffset, this.chunkOffset + count), written)
770
+ written += count
771
+ this.chunkOffset += count
772
+ if (this.chunkOffset === chunk.length) {
773
+ this.chunks[this.chunkIndex++] = null
774
+ this.chunkOffset = 0
775
+ }
757
776
  }
758
- this.chunks = [out]
759
- return out
777
+ this.totalLen -= length
778
+ // Release consumed buffers immediately; compact references only after half are consumed.
779
+ if (this.chunkIndex * 2 >= this.chunks.length) {
780
+ this.chunks = this.chunks.slice(this.chunkIndex)
781
+ this.chunkIndex = 0
782
+ }
783
+ return bytes
760
784
  }
761
785
  }
762
786