@reddb-io/client-bun 1.26.4 → 1.26.6
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/CHANGELOG.md +11 -0
- package/README.md +21 -0
- package/index.ts +108 -20
- package/package.json +7 -2
- package/bench.ts +0 -66
- package/params.test.ts +0 -73
- package/wire-legacy.test.ts +0 -66
- package/wire.test.ts +0 -58
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# @reddb-io/client-bun
|
|
2
2
|
|
|
3
|
+
## 1.26.6
|
|
4
|
+
|
|
5
|
+
## 1.26.5
|
|
6
|
+
|
|
7
|
+
### Patch Changes
|
|
8
|
+
|
|
9
|
+
- [#2478](https://github.com/reddb-io/reddb/pull/2478) [`5d41cee`](https://github.com/reddb-io/reddb/commit/5d41cee8f0d73e40e2620ef62c075b13073fd056) Thanks [@filipeforattini](https://github.com/filipeforattini)! - Accumulate fragmented RedWire input without repeated buffer concatenation and
|
|
10
|
+
copy completed payloads directly into owned result buffers. Release consumed
|
|
11
|
+
receive storage and reject pending/future queries on terminal framing, socket or
|
|
12
|
+
connection-close failures, while preserving per-query engine error recovery.
|
|
13
|
+
|
|
3
14
|
## 1.26.4
|
|
4
15
|
|
|
5
16
|
## 1.26.3
|
package/README.md
CHANGED
|
@@ -28,6 +28,27 @@ Run the shared SDK smoke test and this package's actual TCP-client regression:
|
|
|
28
28
|
REDDB_BINARY_PATH=/absolute/path/to/red bun run test
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
The receive path queues fragments and copies each completed payload into an owned
|
|
32
|
+
buffer. A terminal framing/socket failure or connection close rejects pending and
|
|
33
|
+
future queries. A server query error rejects its own query while the connection
|
|
34
|
+
remains usable. The existing 16 MiB frame limit and query return shapes apply.
|
|
35
|
+
|
|
36
|
+
From this directory in a repository checkout, compare receive processing against
|
|
37
|
+
a baseline checkout using the installed Bun runtime:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
REDDB_BENCH_BASELINE=/path/to/baseline REDDB_BENCH_OUTPUT=bun-receive.json \
|
|
41
|
+
bun run bench-receive.ts
|
|
42
|
+
bun ../js-client/bench/summarize.mjs bun-receive.json
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The benchmark alternates 20 paired samples of at least 20 ms. Scripted sockets
|
|
46
|
+
exercise production query encoding, receive parsing, UTF-8 and Promise delivery.
|
|
47
|
+
Input construction and exact response comparisons are outside timing. Separate
|
|
48
|
+
copy probes count top-level Buffer operations without double counting. Network,
|
|
49
|
+
database execution, the unchanged TCP callback ownership copy and total process
|
|
50
|
+
memory are outside these measurements. Timings never gate CI.
|
|
51
|
+
|
|
31
52
|
<!-- contract-matrix:begin -->
|
|
32
53
|
## Public-surface support
|
|
33
54
|
|
package/index.ts
CHANGED
|
@@ -64,7 +64,13 @@ export class RedDBError extends Error {
|
|
|
64
64
|
export class RedDBConnection {
|
|
65
65
|
private socket: ReturnType<typeof Bun.connect> extends Promise<infer T> ? T : never
|
|
66
66
|
private pending: PendingRequest[] = []
|
|
67
|
-
private
|
|
67
|
+
private chunks: (Buffer | null)[] = []
|
|
68
|
+
private chunkIndex = 0
|
|
69
|
+
private chunkOffset = 0
|
|
70
|
+
private bufferedBytes = 0
|
|
71
|
+
private frameLength = 0
|
|
72
|
+
private frameType = 0
|
|
73
|
+
private failure: Error | null = null
|
|
68
74
|
private nextCorr = 1n
|
|
69
75
|
private serverFeatures = 0
|
|
70
76
|
|
|
@@ -73,39 +79,111 @@ export class RedDBConnection {
|
|
|
73
79
|
}
|
|
74
80
|
|
|
75
81
|
_onData(chunk: Buffer) {
|
|
76
|
-
this.
|
|
82
|
+
if (this.failure || chunk.length === 0) return
|
|
83
|
+
this.chunks.push(chunk)
|
|
84
|
+
this.bufferedBytes += chunk.length
|
|
77
85
|
this._tryResolve()
|
|
78
86
|
}
|
|
79
87
|
|
|
88
|
+
_onClose() {
|
|
89
|
+
this._fail(new RedDBError('CONNECTION_CLOSED', 'redwire: connection closed'))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_onError(error: Error) {
|
|
93
|
+
this._fail(error)
|
|
94
|
+
}
|
|
95
|
+
|
|
80
96
|
private _tryResolve() {
|
|
81
|
-
while (this.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
97
|
+
while (this.bufferedBytes >= FRAME_HEADER_SIZE && this.pending.length > 0) {
|
|
98
|
+
let payload: Buffer
|
|
99
|
+
let msgType: number
|
|
100
|
+
try {
|
|
101
|
+
if (this.frameLength === 0) this._readHeader()
|
|
102
|
+
if (this.bufferedBytes < this.frameLength) break
|
|
103
|
+
msgType = this.frameType
|
|
104
|
+
this._consume(FRAME_HEADER_SIZE)
|
|
105
|
+
payload = Buffer.allocUnsafe(this.frameLength - FRAME_HEADER_SIZE)
|
|
106
|
+
this._consume(payload.length, payload)
|
|
107
|
+
this.frameLength = 0
|
|
108
|
+
} catch (error) {
|
|
109
|
+
this._fail(error as Error)
|
|
86
110
|
return
|
|
87
111
|
}
|
|
88
|
-
if (this.buffer.length < totalLen) break
|
|
89
|
-
|
|
90
|
-
const msgType = this.buffer[4]
|
|
91
|
-
const payload = this.buffer.subarray(FRAME_HEADER_SIZE, totalLen)
|
|
92
|
-
this.buffer = this.buffer.subarray(totalLen)
|
|
93
|
-
|
|
94
112
|
const { resolve, reject } = this.pending.shift()!
|
|
95
113
|
if (msgType === MSG_ERROR || msgType === MSG_AUTH_FAIL) {
|
|
96
114
|
reject(new RedDBError('ENGINE', payload.toString('utf8')))
|
|
97
115
|
} else {
|
|
98
|
-
resolve({ type: msgType, payload
|
|
116
|
+
resolve({ type: msgType, payload })
|
|
99
117
|
}
|
|
100
118
|
}
|
|
101
119
|
}
|
|
102
120
|
|
|
121
|
+
private _readHeader() {
|
|
122
|
+
const first = this.chunks[this.chunkIndex]!
|
|
123
|
+
let header = first
|
|
124
|
+
let offset = this.chunkOffset
|
|
125
|
+
if (first.length - offset < 5) {
|
|
126
|
+
// Only length and kind are needed; the rest of the 16-byte header is discarded later.
|
|
127
|
+
header = Buffer.allocUnsafe(5)
|
|
128
|
+
let written = 0
|
|
129
|
+
for (let index = this.chunkIndex; written < header.length; index++) {
|
|
130
|
+
const chunk = this.chunks[index]!
|
|
131
|
+
const start = index === this.chunkIndex ? this.chunkOffset : 0
|
|
132
|
+
const count = Math.min(header.length - written, chunk.length - start)
|
|
133
|
+
written += chunk.copy(header, written, start, start + count)
|
|
134
|
+
}
|
|
135
|
+
offset = 0
|
|
136
|
+
}
|
|
137
|
+
const length = header.readUInt32LE(offset)
|
|
138
|
+
if (length < FRAME_HEADER_SIZE || length > MAX_FRAME_SIZE) {
|
|
139
|
+
throw new RedDBError('FRAME_INVALID_LENGTH', `length=${length}`)
|
|
140
|
+
}
|
|
141
|
+
this.frameLength = length
|
|
142
|
+
this.frameType = header[offset + 4]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private _consume(length: number, output?: Buffer) {
|
|
146
|
+
if (length === 0) return
|
|
147
|
+
let written = 0
|
|
148
|
+
while (written < length) {
|
|
149
|
+
const chunk = this.chunks[this.chunkIndex]!
|
|
150
|
+
const count = Math.min(length - written, chunk.length - this.chunkOffset)
|
|
151
|
+
if (output) chunk.copy(output, written, this.chunkOffset, this.chunkOffset + count)
|
|
152
|
+
written += count
|
|
153
|
+
this.chunkOffset += count
|
|
154
|
+
if (this.chunkOffset === chunk.length) {
|
|
155
|
+
this.chunks[this.chunkIndex++] = null
|
|
156
|
+
this.chunkOffset = 0
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
this.bufferedBytes -= length
|
|
160
|
+
if (this.chunkIndex * 2 >= this.chunks.length) {
|
|
161
|
+
this.chunks = this.chunks.slice(this.chunkIndex)
|
|
162
|
+
this.chunkIndex = 0
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private _fail(error: Error) {
|
|
167
|
+
if (this.failure) return
|
|
168
|
+
this.failure = error
|
|
169
|
+
this.chunks = []
|
|
170
|
+
this.chunkIndex = 0
|
|
171
|
+
this.chunkOffset = 0
|
|
172
|
+
this.bufferedBytes = 0
|
|
173
|
+
this.frameLength = 0
|
|
174
|
+
while (this.pending.length > 0) this.pending.shift()!.reject(error)
|
|
175
|
+
try { this.socket.end() } catch { /* The peer may already have closed the socket. */ }
|
|
176
|
+
}
|
|
177
|
+
|
|
103
178
|
private _send(msgType: number, payload: Buffer): Promise<{ type: number; payload: Buffer }> {
|
|
179
|
+
if (this.failure) return Promise.reject(this.failure)
|
|
104
180
|
return new Promise((resolve, reject) => {
|
|
105
|
-
this.pending.push({ resolve, reject })
|
|
106
181
|
const corr = this.nextCorr
|
|
107
182
|
this.nextCorr += 1n
|
|
108
|
-
|
|
183
|
+
const request = encodeFrame(msgType, corr, payload)
|
|
184
|
+
this.pending.push({ resolve, reject })
|
|
185
|
+
try { this.socket.write(request) }
|
|
186
|
+
catch (error) { this._fail(error as Error) }
|
|
109
187
|
})
|
|
110
188
|
}
|
|
111
189
|
|
|
@@ -200,11 +278,12 @@ export class RedDBConnection {
|
|
|
200
278
|
}
|
|
201
279
|
|
|
202
280
|
close() {
|
|
281
|
+
if (this.failure) return
|
|
203
282
|
try {
|
|
204
283
|
this.socket.write(encodeFrame(MSG_BYE, this.nextCorr, Buffer.alloc(0)))
|
|
205
284
|
this.nextCorr += 1n
|
|
206
285
|
} finally {
|
|
207
|
-
this.
|
|
286
|
+
this._onClose()
|
|
208
287
|
}
|
|
209
288
|
}
|
|
210
289
|
}
|
|
@@ -232,6 +311,12 @@ async function connectWithOptions(addr: string, tls?: { ca?: string; rejectUnaut
|
|
|
232
311
|
const [host, portStr] = addr.split(':')
|
|
233
312
|
const port = parseInt(portStr, 10)
|
|
234
313
|
let connRef: RedDBConnection | null = null
|
|
314
|
+
let earlyFailure: Error | null = null
|
|
315
|
+
const fail = (error: Error) => {
|
|
316
|
+
if (connRef) connRef._onError(error)
|
|
317
|
+
else earlyFailure ??= error
|
|
318
|
+
}
|
|
319
|
+
const closed = () => fail(new RedDBError('CONNECTION_CLOSED', 'redwire: connection closed'))
|
|
235
320
|
|
|
236
321
|
const socket = await Bun.connect({
|
|
237
322
|
hostname: host,
|
|
@@ -242,15 +327,18 @@ async function connectWithOptions(addr: string, tls?: { ca?: string; rejectUnaut
|
|
|
242
327
|
connRef?._onData(Buffer.from(data))
|
|
243
328
|
},
|
|
244
329
|
error(_socket, error) {
|
|
245
|
-
|
|
330
|
+
fail(error)
|
|
246
331
|
},
|
|
247
|
-
close
|
|
332
|
+
close: closed,
|
|
333
|
+
end: closed,
|
|
248
334
|
open() {},
|
|
249
335
|
},
|
|
250
336
|
})
|
|
251
337
|
|
|
252
338
|
connRef = new RedDBConnection(socket)
|
|
253
|
-
|
|
339
|
+
if (earlyFailure) connRef._onError(earlyFailure)
|
|
340
|
+
try { await connRef._handshake() }
|
|
341
|
+
catch (error) { connRef._onError(error as Error); throw error }
|
|
254
342
|
return connRef
|
|
255
343
|
}
|
|
256
344
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reddb-io/client-bun",
|
|
3
|
-
"version": "1.26.
|
|
3
|
+
"version": "1.26.6",
|
|
4
4
|
"description": "RedDB wire protocol client for Bun — ultra-fast native TCP",
|
|
5
5
|
"main": "index.ts",
|
|
6
|
+
"files": [
|
|
7
|
+
"index.ts",
|
|
8
|
+
"README.md",
|
|
9
|
+
"CHANGELOG.md"
|
|
10
|
+
],
|
|
6
11
|
"license": "MIT",
|
|
7
12
|
"keywords": [
|
|
8
13
|
"reddb",
|
|
@@ -12,6 +17,6 @@
|
|
|
12
17
|
"wire-protocol"
|
|
13
18
|
],
|
|
14
19
|
"scripts": {
|
|
15
|
-
"test": "bun run params.test.ts && bun run wire-legacy.test.ts && bun run wire.test.ts"
|
|
20
|
+
"test": "bun test frame-reader.test.ts socket-close.test.ts && bun run params.test.ts && bun run wire-legacy.test.ts && bun run wire.test.ts"
|
|
16
21
|
}
|
|
17
22
|
}
|
package/bench.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* RedDB Bun Wire Protocol Benchmark
|
|
4
|
-
* Usage: bun run bench.ts [host:port]
|
|
5
|
-
*/
|
|
6
|
-
import { connect } from './index'
|
|
7
|
-
|
|
8
|
-
const ADDR = process.argv[2] || '127.0.0.1:5050'
|
|
9
|
-
const N = 5000
|
|
10
|
-
const POINT_LOOKUPS = 200
|
|
11
|
-
const RANGE_QUERIES = 50
|
|
12
|
-
const FILTER_QUERIES = 50
|
|
13
|
-
|
|
14
|
-
function rng(seed: number) {
|
|
15
|
-
let s = seed
|
|
16
|
-
return () => { s = (s * 1103515245 + 12345) & 0x7fffffff; return s }
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const rand = rng(42)
|
|
20
|
-
const conn = await connect(ADDR)
|
|
21
|
-
console.log(`Connected to ${ADDR}`)
|
|
22
|
-
|
|
23
|
-
// Insert
|
|
24
|
-
const records: string[] = []
|
|
25
|
-
const cities = ['NYC', 'London', 'Tokyo', 'Paris', 'Berlin']
|
|
26
|
-
for (let i = 0; i < N; i++) {
|
|
27
|
-
records.push(JSON.stringify({
|
|
28
|
-
fields: { id: i + 1, name: `User_${i}`, age: 18 + (rand() % 63), city: cities[rand() % 5], score: (rand() % 10000) / 100 }
|
|
29
|
-
}))
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
let t0 = performance.now()
|
|
33
|
-
const count = await conn.bulkInsert('users', records)
|
|
34
|
-
let ms = performance.now() - t0
|
|
35
|
-
console.log(` insert_bulk: ${(N / ms * 1000).toFixed(0).padStart(8)} ops/sec (${ms.toFixed(0)}ms) [${count} rows]`)
|
|
36
|
-
|
|
37
|
-
// Point lookups
|
|
38
|
-
const lids = Array.from({ length: POINT_LOOKUPS }, () => 1 + (rand() % N))
|
|
39
|
-
t0 = performance.now()
|
|
40
|
-
for (const rid of lids) {
|
|
41
|
-
await conn.queryRaw(`SELECT * FROM users WHERE _entity_id = ${rid}`)
|
|
42
|
-
}
|
|
43
|
-
ms = performance.now() - t0
|
|
44
|
-
console.log(` select_point: ${(POINT_LOOKUPS / ms * 1000).toFixed(0).padStart(8)} ops/sec (${ms.toFixed(0)}ms)`)
|
|
45
|
-
|
|
46
|
-
// Range queries
|
|
47
|
-
const rqs = Array.from({ length: RANGE_QUERIES }, () => {
|
|
48
|
-
const l = 18 + (rand() % 53); return [l, l + 10] as const
|
|
49
|
-
})
|
|
50
|
-
t0 = performance.now()
|
|
51
|
-
for (const [l, h] of rqs) {
|
|
52
|
-
await conn.queryRaw(`SELECT * FROM users WHERE age BETWEEN ${l} AND ${h}`)
|
|
53
|
-
}
|
|
54
|
-
ms = performance.now() - t0
|
|
55
|
-
console.log(` select_range: ${(RANGE_QUERIES / ms * 1000).toFixed(0).padStart(8)} ops/sec (${ms.toFixed(0)}ms)`)
|
|
56
|
-
|
|
57
|
-
// Filtered queries
|
|
58
|
-
const fqs = Array.from({ length: FILTER_QUERIES }, () => [cities[rand() % 5], 18 + (rand() % 43)] as const)
|
|
59
|
-
t0 = performance.now()
|
|
60
|
-
for (const [c, a] of fqs) {
|
|
61
|
-
await conn.queryRaw(`SELECT * FROM users WHERE city = '${c}' AND age > ${a}`)
|
|
62
|
-
}
|
|
63
|
-
ms = performance.now() - t0
|
|
64
|
-
console.log(` select_filtered: ${(FILTER_QUERIES / ms * 1000).toFixed(0).padStart(8)} ops/sec (${ms.toFixed(0)}ms)`)
|
|
65
|
-
|
|
66
|
-
conn.close()
|
package/params.test.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
2
|
-
import { dirname, resolve } from 'node:path'
|
|
3
|
-
import { fileURLToPath } from 'node:url'
|
|
4
|
-
|
|
5
|
-
import { connect } from '../js/src/index.js'
|
|
6
|
-
|
|
7
|
-
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
8
|
-
const DEFAULT_BINARY = resolve(HERE, '..', '..', 'target', 'debug', 'red')
|
|
9
|
-
const BINARY = process.env.REDDB_BINARY_PATH || DEFAULT_BINARY
|
|
10
|
-
|
|
11
|
-
function assert(condition: unknown, message: string): asserts condition {
|
|
12
|
-
if (!condition) throw new Error(message)
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function assertEqual<T>(actual: T, expected: T, message: string) {
|
|
16
|
-
if (actual !== expected) {
|
|
17
|
-
throw new Error(
|
|
18
|
-
`${message}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
|
|
19
|
-
)
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (!existsSync(BINARY)) {
|
|
24
|
-
console.log(`SKIP: binary not found at ${BINARY}`)
|
|
25
|
-
process.exit(0)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const db = await connect('memory://', { binary: BINARY })
|
|
29
|
-
try {
|
|
30
|
-
await db.query('CREATE TABLE bun_params (id INTEGER, name TEXT)')
|
|
31
|
-
await db.query('INSERT INTO bun_params (id, name) VALUES ($1, $2)', [1, 'Bun'])
|
|
32
|
-
await db.query('INSERT INTO bun_params (id, name) VALUES ($1, $2)', [2, 'Node'])
|
|
33
|
-
|
|
34
|
-
const selected = await db.query(
|
|
35
|
-
'SELECT * FROM bun_params WHERE id = $1 AND name = $2',
|
|
36
|
-
[1, 'Bun'],
|
|
37
|
-
)
|
|
38
|
-
assert(Array.isArray(selected.rows), 'SELECT rows should be an array')
|
|
39
|
-
assertEqual(selected.rows.length, 1, 'SELECT should match one row')
|
|
40
|
-
assertEqual(selected.rows[0].name, 'Bun', 'SELECT should bind text and int params')
|
|
41
|
-
|
|
42
|
-
await db.query(
|
|
43
|
-
'INSERT INTO bun_embeddings VECTOR (dense, content) VALUES ($1, $2)',
|
|
44
|
-
[new Float32Array([1.0, 0.0]), 'bun vector'],
|
|
45
|
-
)
|
|
46
|
-
await db.query(
|
|
47
|
-
'INSERT INTO bun_embeddings VECTOR (dense, content) VALUES ($1, $2)',
|
|
48
|
-
[new Float32Array([0.0, 1.0]), 'other vector'],
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
const similar = await db.query(
|
|
52
|
-
'SEARCH SIMILAR $1 COLLECTION bun_embeddings LIMIT 1',
|
|
53
|
-
[new Float32Array([1.0, 0.0])],
|
|
54
|
-
)
|
|
55
|
-
assert(Array.isArray(similar.rows), 'SEARCH rows should be an array')
|
|
56
|
-
assertEqual(similar.rows.length, 1, 'SEARCH should match one vector row')
|
|
57
|
-
assertEqual(similar.rows[0].score, 1, 'SEARCH should bind Float32Array vector')
|
|
58
|
-
|
|
59
|
-
const fractional = new Float32Array([0.8, 0.6])
|
|
60
|
-
await db.query('INSERT INTO fractional_vectors VECTOR (dense, content) VALUES ($1, $2)',
|
|
61
|
-
[fractional, 'fractional embedding'])
|
|
62
|
-
const fractionalMatch = await db.query('SEARCH SIMILAR $1 COLLECTION fractional_vectors LIMIT 1',
|
|
63
|
-
[fractional])
|
|
64
|
-
assertEqual(fractionalMatch.rows.length, 1, 'fractional vector should remain searchable')
|
|
65
|
-
assert(Math.abs(fractionalMatch.rows[0].score - 1) < 1e-6, 'identical fractional vectors should match')
|
|
66
|
-
const axisMatch = await db.query('SEARCH SIMILAR $1 COLLECTION fractional_vectors LIMIT 1',
|
|
67
|
-
[new Float32Array([1, 0])])
|
|
68
|
-
assert(Math.abs(axisMatch.rows[0].score - 0.8) < 1e-6, 'fractional coordinates survive binding')
|
|
69
|
-
} finally {
|
|
70
|
-
await db.close()
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
console.log('ok shared SDK parameterized queries run under Bun')
|
package/wire-legacy.test.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { strict as assert } from 'node:assert'
|
|
2
|
-
import { createServer } from 'node:net'
|
|
3
|
-
import { connect, RedDBError } from './index'
|
|
4
|
-
|
|
5
|
-
// The old protocol is intentionally summary-only. A small protocol peer lets
|
|
6
|
-
// us exercise capability negotiation without installing an obsolete engine.
|
|
7
|
-
const queries: number[] = []
|
|
8
|
-
const server = createServer((socket) => {
|
|
9
|
-
socket.setTimeout(5_000, () => socket.destroy())
|
|
10
|
-
let input = Buffer.alloc(0)
|
|
11
|
-
let magicReceived = false
|
|
12
|
-
socket.on('data', (chunk) => {
|
|
13
|
-
input = Buffer.concat([input, chunk])
|
|
14
|
-
if (!magicReceived) {
|
|
15
|
-
if (input.length < 2) return
|
|
16
|
-
assert.deepEqual([...input.subarray(0, 2)], [0xfe, 1])
|
|
17
|
-
input = input.subarray(2)
|
|
18
|
-
magicReceived = true
|
|
19
|
-
}
|
|
20
|
-
while (input.length >= 16) {
|
|
21
|
-
const length = input.readUInt32LE(0)
|
|
22
|
-
assert(length >= 16 && length <= 16 * 1024 * 1024)
|
|
23
|
-
if (input.length < length) return
|
|
24
|
-
const frame = input.subarray(0, length)
|
|
25
|
-
input = input.subarray(length)
|
|
26
|
-
const kind = frame[4]
|
|
27
|
-
if (kind === 0x16) { socket.end(); return }
|
|
28
|
-
let responseKind: number
|
|
29
|
-
let body: object
|
|
30
|
-
if (kind === 0x10) {
|
|
31
|
-
responseKind = 0x11
|
|
32
|
-
body = { auth: 'anonymous', features: 0 }
|
|
33
|
-
} else if (kind === 0x13) {
|
|
34
|
-
responseKind = 0x14
|
|
35
|
-
body = { features: 0 }
|
|
36
|
-
} else {
|
|
37
|
-
queries.push(kind)
|
|
38
|
-
responseKind = 0x02
|
|
39
|
-
body = { ok: true, affected: 0, statement: 'select' }
|
|
40
|
-
}
|
|
41
|
-
const payload = Buffer.from(JSON.stringify(body))
|
|
42
|
-
const response = Buffer.alloc(16 + payload.length)
|
|
43
|
-
response.writeUInt32LE(response.length, 0)
|
|
44
|
-
response[4] = responseKind
|
|
45
|
-
frame.copy(response, 8, 8, 16)
|
|
46
|
-
payload.copy(response, 16)
|
|
47
|
-
socket.write(response)
|
|
48
|
-
}
|
|
49
|
-
})
|
|
50
|
-
})
|
|
51
|
-
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
|
|
52
|
-
const address = server.address()
|
|
53
|
-
assert(address && typeof address === 'object')
|
|
54
|
-
let db: Awaited<ReturnType<typeof connect>> | undefined
|
|
55
|
-
try {
|
|
56
|
-
db = await connect(`127.0.0.1:${address.port}`)
|
|
57
|
-
assert.equal(db.supportsParams(), false)
|
|
58
|
-
assert.deepEqual(await db.queryParsed('SELECT 42'), { ok: true, affected: 0, statement: 'select' })
|
|
59
|
-
await assert.rejects(db.query('SELECT $1', [42]),
|
|
60
|
-
(error) => error instanceof RedDBError && error.code === 'PARAMS_UNSUPPORTED')
|
|
61
|
-
assert.deepEqual(queries, [0x01])
|
|
62
|
-
console.log('ok Bun legacy capability fallback')
|
|
63
|
-
} finally {
|
|
64
|
-
db?.close()
|
|
65
|
-
await new Promise<void>((resolve) => server.close(() => resolve()))
|
|
66
|
-
}
|
package/wire.test.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { strict as assert } from 'node:assert'
|
|
2
|
-
import { mkdtempSync, rmSync } from 'node:fs'
|
|
3
|
-
import { tmpdir } from 'node:os'
|
|
4
|
-
import { join, resolve } from 'node:path'
|
|
5
|
-
import { createServer } from 'node:net'
|
|
6
|
-
import { connect } from './index'
|
|
7
|
-
|
|
8
|
-
// Exercise this package's TCP client against a real server. An absent binary
|
|
9
|
-
// is a test failure: silently skipping hid the unbound-query regression.
|
|
10
|
-
const binary = process.env.REDDB_BINARY_PATH || resolve(import.meta.dir, '../../target/debug/red')
|
|
11
|
-
const reservation = createServer()
|
|
12
|
-
await new Promise<void>((resolve, reject) => {
|
|
13
|
-
reservation.once('error', reject)
|
|
14
|
-
reservation.listen(0, '127.0.0.1', resolve)
|
|
15
|
-
})
|
|
16
|
-
const address = reservation.address()
|
|
17
|
-
assert(address && typeof address === 'object')
|
|
18
|
-
const endpoint = `127.0.0.1:${address.port}`
|
|
19
|
-
await new Promise<void>((resolve) => reservation.close(() => resolve()))
|
|
20
|
-
const directory = mkdtempSync(join(tmpdir(), 'reddb-bun-wire-'))
|
|
21
|
-
const server = Bun.spawn([binary, 'server', '--path', join(directory, 'db.rdb'),
|
|
22
|
-
'--bind', endpoint, '--no-auth', '--no-log-file'], { stdout: 'ignore', stderr: 'inherit' })
|
|
23
|
-
let db: Awaited<ReturnType<typeof connect>> | undefined
|
|
24
|
-
try {
|
|
25
|
-
const deadline = Date.now() + 30_000
|
|
26
|
-
while (Date.now() < deadline) {
|
|
27
|
-
if (server.exitCode !== null) throw new Error(`server exited ${server.exitCode}`)
|
|
28
|
-
try {
|
|
29
|
-
const response = await fetch(`http://${endpoint}/health`, { signal: AbortSignal.timeout(500) })
|
|
30
|
-
if (response.ok) break
|
|
31
|
-
} catch {}
|
|
32
|
-
await Bun.sleep(100)
|
|
33
|
-
}
|
|
34
|
-
db = await connect(endpoint)
|
|
35
|
-
assert(db.supportsParams())
|
|
36
|
-
await db.query('CREATE TABLE wire_rows (id INTEGER, name TEXT)')
|
|
37
|
-
const inserted = await db.queryParsed("INSERT INTO wire_rows (id,name) VALUES (1,'Bun')")
|
|
38
|
-
assert.equal(inserted.affected_rows, 1)
|
|
39
|
-
const unbound = await db.queryParsed('SELECT id,name FROM wire_rows')
|
|
40
|
-
const bound = await db.queryParsed('SELECT id,name FROM wire_rows WHERE id=$1', [1])
|
|
41
|
-
assert.deepEqual(unbound.result.records.map((record: any) => record.values), [{ id: 1, name: 'Bun' }])
|
|
42
|
-
assert.deepEqual(unbound.result.records.map((record: any) => record.values), bound.result.records.map((record: any) => record.values))
|
|
43
|
-
assert.deepEqual((await db.queryParsed('SELECT id,name FROM wire_rows', [])).result.records.map((record: any) => record.values), bound.result.records.map((record: any) => record.values))
|
|
44
|
-
assert.deepEqual((await db.queryParsed('SELECT id FROM wire_rows WHERE id=2')).result.records, [])
|
|
45
|
-
await assert.rejects(db.query('SELECT * FROM absent_wire_table'))
|
|
46
|
-
const cli = Bun.spawnSync([binary, 'query', '--bind', endpoint, '--json',
|
|
47
|
-
"INSERT INTO wire_rows (id,name) VALUES (2,'CLI')"])
|
|
48
|
-
assert.equal(cli.exitCode, 0, cli.stderr.toString())
|
|
49
|
-
const insertedByCli = JSON.parse(cli.stdout.toString()).data
|
|
50
|
-
assert.equal(insertedByCli.affected, 1)
|
|
51
|
-
assert.equal(insertedByCli.statement, 'insert')
|
|
52
|
-
console.log('ok Bun wire bound/unbound result contract')
|
|
53
|
-
} finally {
|
|
54
|
-
db?.close()
|
|
55
|
-
server.kill()
|
|
56
|
-
await server.exited
|
|
57
|
-
rmSync(directory, { recursive: true, force: true })
|
|
58
|
-
}
|