@reddb-io/client 1.26.3 → 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.
- package/README.md +8 -0
- package/package.json +1 -1
- package/src/core/reddb.js +1 -1
- package/src/core/serialization.js +13 -3
- package/src/redwire-core.js +68 -40
package/README.md
CHANGED
|
@@ -75,6 +75,14 @@ For `http://` and `https://` connections, `connect()` verifies readiness with
|
|
|
75
75
|
a lightweight `SELECT 1` round-trip. `/health` states such as `degraded` are
|
|
76
76
|
transient during boot and are not fatal as long as queries succeed.
|
|
77
77
|
|
|
78
|
+
RedWire query parameters retain `Float32Array`, `Float64Array`, `Uint8Array`
|
|
79
|
+
and `Buffer` views through normalization, avoiding intermediate number arrays
|
|
80
|
+
and base64 strings. The request owns its encoded bytes before `query()` yields;
|
|
81
|
+
later mutations of caller-owned views do not change the sent request. The
|
|
82
|
+
transport drops input-view references before waiting for the network. HTTP/gRPC
|
|
83
|
+
keep the same JSON-compatible parameter envelopes. See the measured
|
|
84
|
+
[encoding results](../../docs/perf/client-codecs-2026-10-23.md).
|
|
85
|
+
|
|
78
86
|
## Transactions
|
|
79
87
|
|
|
80
88
|
Use `db.transaction()` when a group of writes must commit or roll back together.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reddb-io/client",
|
|
3
|
-
"version": "1.26.
|
|
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",
|
package/src/core/reddb.js
CHANGED
|
@@ -122,7 +122,7 @@ export class RedDB {
|
|
|
122
122
|
|
|
123
123
|
/** Execute a SQL query. Returns `{ statement, affected, columns, rows }`. */
|
|
124
124
|
query(sql, ...params) {
|
|
125
|
-
const wireParams = normalizeQueryParams(params)
|
|
125
|
+
const wireParams = normalizeQueryParams(params, this.client.binaryQueryParams === true)
|
|
126
126
|
if (wireParams == null) {
|
|
127
127
|
return this.client.call('query', { sql }).then(normalizeExactNumbers)
|
|
128
128
|
}
|
|
@@ -126,10 +126,20 @@ export function assertSupportedParam(value) {
|
|
|
126
126
|
)
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
function serializeBinaryParam(value) {
|
|
130
|
+
if (value instanceof Uint8Array || value instanceof Float32Array || value instanceof Float64Array) {
|
|
131
|
+
// Preserve the existing handling of empty, detached and resized views.
|
|
132
|
+
if (value.byteLength === 0) return serializeParam(value)
|
|
133
|
+
return value
|
|
134
|
+
}
|
|
135
|
+
return serializeParam(value)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function normalizeQueryParams(args, binary = false) {
|
|
130
139
|
if (args.length === 0) return null
|
|
131
|
-
|
|
132
|
-
return args.map(
|
|
140
|
+
const serialize = binary ? serializeBinaryParam : serializeParam
|
|
141
|
+
if (args.length === 1 && Array.isArray(args[0])) return args[0].map(serialize)
|
|
142
|
+
return args.map(serialize)
|
|
133
143
|
}
|
|
134
144
|
|
|
135
145
|
export function bytesToBase64(value) {
|
package/src/redwire-core.js
CHANGED
|
@@ -226,6 +226,8 @@ export class RedWireClient {
|
|
|
226
226
|
this.reader = reader
|
|
227
227
|
this.session = session
|
|
228
228
|
this.serverFeatures = serverFeatures >>> 0
|
|
229
|
+
// RedWire consumes typed views directly; JSON transports use envelopes.
|
|
230
|
+
this.binaryQueryParams = true
|
|
229
231
|
this.nextCorr = 1n
|
|
230
232
|
this.nextStream = 1
|
|
231
233
|
this.closed = false
|
|
@@ -357,6 +359,8 @@ export class RedWireClient {
|
|
|
357
359
|
kind = isSelectQuery(sql) ? MessageKind.QueryBinary : MessageKind.Query
|
|
358
360
|
payload = new TextEncoder().encode(sql)
|
|
359
361
|
}
|
|
362
|
+
// The encoded payload owns its bytes; release caller views before suspending.
|
|
363
|
+
params = undefined
|
|
360
364
|
await writeFrame(this.socket, kind, corr, payload)
|
|
361
365
|
const resp = await this.reader.next()
|
|
362
366
|
if (resp.kind === MessageKind.Result) {
|
|
@@ -668,28 +672,30 @@ export function decodeFrame(buf) {
|
|
|
668
672
|
class FrameReader {
|
|
669
673
|
constructor(socket) {
|
|
670
674
|
this.chunks = []
|
|
675
|
+
this.chunkIndex = 0
|
|
676
|
+
this.chunkOffset = 0
|
|
671
677
|
this.totalLen = 0
|
|
678
|
+
this.frameLength = 0
|
|
672
679
|
this.waiters = []
|
|
673
680
|
this.error = null
|
|
674
681
|
this.eof = false
|
|
675
682
|
socket.on('data', (chunk) => {
|
|
683
|
+
if (this.error || this.eof) return
|
|
676
684
|
// chunk is Buffer (Node) or Uint8Array (Bun/Deno)
|
|
677
685
|
const u8 = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk)
|
|
686
|
+
if (u8.length === 0) return
|
|
678
687
|
this.chunks.push(u8)
|
|
679
688
|
this.totalLen += u8.length
|
|
680
689
|
this.#tryDeliver()
|
|
681
690
|
})
|
|
682
|
-
socket.on('error', (err) =>
|
|
683
|
-
this.error = err
|
|
684
|
-
this.#flushWaiters()
|
|
685
|
-
})
|
|
691
|
+
socket.on('error', (err) => this.#fail(err))
|
|
686
692
|
socket.on('end', () => {
|
|
687
693
|
this.eof = true
|
|
688
|
-
this.#
|
|
694
|
+
this.#tryDeliver()
|
|
689
695
|
})
|
|
690
696
|
socket.on('close', () => {
|
|
691
697
|
this.eof = true
|
|
692
|
-
this.#
|
|
698
|
+
this.#tryDeliver()
|
|
693
699
|
})
|
|
694
700
|
}
|
|
695
701
|
|
|
@@ -702,57 +708,79 @@ class FrameReader {
|
|
|
702
708
|
}
|
|
703
709
|
|
|
704
710
|
#tryDeliver() {
|
|
705
|
-
while (this.waiters.length > 0 && this.totalLen
|
|
706
|
-
const flat = this.#flatten()
|
|
711
|
+
while (this.waiters.length > 0 && this.totalLen >= FRAME_HEADER_SIZE) {
|
|
707
712
|
let frame
|
|
708
713
|
try {
|
|
709
|
-
|
|
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
|
|
710
718
|
} catch (err) {
|
|
711
|
-
|
|
712
|
-
w.reject(err)
|
|
713
|
-
return
|
|
714
|
-
}
|
|
715
|
-
if (frame == null) {
|
|
716
|
-
// Need more bytes — put the flattened buffer back as a
|
|
717
|
-
// single chunk so we don't keep flattening repeatedly.
|
|
718
|
-
this.chunks = [flat]
|
|
719
|
+
this.#fail(err)
|
|
719
720
|
return
|
|
720
721
|
}
|
|
721
|
-
this.chunks = [flat.subarray(frame.consumed)]
|
|
722
|
-
this.totalLen = this.chunks[0].length
|
|
723
722
|
const w = this.waiters.shift()
|
|
724
723
|
w.resolve(frame)
|
|
725
724
|
}
|
|
726
|
-
if (this.eof && this.waiters.length > 0
|
|
727
|
-
|
|
728
|
-
while (this.waiters.length > 0) {
|
|
729
|
-
this.waiters.shift().reject(err)
|
|
730
|
-
}
|
|
725
|
+
if (this.eof && this.waiters.length > 0) {
|
|
726
|
+
this.#fail(new RedDBError('CONNECTION_CLOSED', 'redwire: connection closed'))
|
|
731
727
|
}
|
|
732
728
|
}
|
|
733
729
|
|
|
734
|
-
#
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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
|
+
}
|
|
739
739
|
}
|
|
740
|
-
|
|
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
|
|
741
753
|
while (this.waiters.length > 0) {
|
|
742
|
-
this.waiters.shift().reject(
|
|
754
|
+
this.waiters.shift().reject(this.error)
|
|
743
755
|
}
|
|
744
756
|
}
|
|
745
757
|
|
|
746
|
-
#
|
|
747
|
-
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
+
}
|
|
753
776
|
}
|
|
754
|
-
this.
|
|
755
|
-
|
|
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
|
|
756
784
|
}
|
|
757
785
|
}
|
|
758
786
|
|