@reddb-io/client-bun 1.29.3 → 1.30.1
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 +9 -0
- package/README.md +8 -0
- package/index.ts +71 -13
- package/package.json +5 -2
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -83,3 +83,11 @@ SQL double quotes delimit identifiers; single quotes delimit text. For example,
|
|
|
83
83
|
Bind application values as parameters. JSON object/array strings keep double
|
|
84
84
|
quotes. See the [SQL quotation migration guide](../../docs/query/sql-quoting.md)
|
|
85
85
|
before upgrading applications that used double-quoted SQL text literals.
|
|
86
|
+
|
|
87
|
+
## Topology and replica reads
|
|
88
|
+
|
|
89
|
+
Remote `connect` discovers advertised primary/replica endpoints on the driver's supported transports. Generic SQL and writes stay on the primary; transactions and session commands pin their connection.
|
|
90
|
+
|
|
91
|
+
Use `db.queryWithOptions(sql, { consistency: 'local' })` for explicit replica reads. Strong and bounded-staleness options are also available. Ambiguous writes are never replayed automatically.
|
|
92
|
+
|
|
93
|
+
See [the routing contract and transport matrix](../../docs/clients/topology-routing.md) for configuration, consistency and failover behavior.
|
package/index.ts
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
* Uses Bun's native TCP socket and speaks the RedWire v1 frame protocol.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { RoutingClient } from '@reddb-io/client/routing'
|
|
8
|
+
import { decodeTopology } from '@reddb-io/client/topology'
|
|
9
|
+
|
|
10
|
+
export type ReadOptions = { consistency: 'local' | 'strong' | 'bounded_staleness'; maxLagMs?: number }
|
|
11
|
+
|
|
7
12
|
const MAGIC = 0xfe
|
|
8
13
|
const SUPPORTED_VERSION = 0x01
|
|
9
14
|
const FRAME_HEADER_SIZE = 16
|
|
@@ -73,6 +78,7 @@ export class RedDBConnection {
|
|
|
73
78
|
private failure: Error | null = null
|
|
74
79
|
private nextCorr = 1n
|
|
75
80
|
private serverFeatures = 0
|
|
81
|
+
private routing?: RoutingClient
|
|
76
82
|
|
|
77
83
|
constructor(socket: any) {
|
|
78
84
|
this.socket = socket
|
|
@@ -187,11 +193,11 @@ export class RedDBConnection {
|
|
|
187
193
|
})
|
|
188
194
|
}
|
|
189
195
|
|
|
190
|
-
async _handshake() {
|
|
196
|
+
async _handshake(token?: string) {
|
|
191
197
|
this.socket.write(Buffer.from([MAGIC, SUPPORTED_VERSION]))
|
|
192
198
|
const hello = jsonBytes({
|
|
193
199
|
versions: [SUPPORTED_VERSION],
|
|
194
|
-
auth_methods: ['
|
|
200
|
+
auth_methods: token ? ['bearer'] : ['anonymous'],
|
|
195
201
|
features: 0,
|
|
196
202
|
client_name: 'reddb-bun/1.0',
|
|
197
203
|
})
|
|
@@ -201,11 +207,11 @@ export class RedDBConnection {
|
|
|
201
207
|
}
|
|
202
208
|
const parsedAck = jsonOf(ack.payload) ?? {}
|
|
203
209
|
const chosenAuth = parsedAck.auth
|
|
204
|
-
if (chosenAuth !== 'anonymous') {
|
|
210
|
+
if (chosenAuth !== (token ? 'bearer' : 'anonymous')) {
|
|
205
211
|
throw new RedDBError('AUTH_REFUSED', `unsupported auth method: ${chosenAuth}`)
|
|
206
212
|
}
|
|
207
213
|
|
|
208
|
-
const authOk = await this._send(MSG_AUTH_RESPONSE, Buffer.alloc(0))
|
|
214
|
+
const authOk = await this._send(MSG_AUTH_RESPONSE, token ? jsonBytes({ token }) : Buffer.alloc(0))
|
|
209
215
|
if (authOk.type !== MSG_AUTH_OK) {
|
|
210
216
|
throw new RedDBError('PROTOCOL', `expected AuthOk, got ${authOk.type}`)
|
|
211
217
|
}
|
|
@@ -217,10 +223,53 @@ export class RedDBConnection {
|
|
|
217
223
|
return (this.serverFeatures & FEATURE_PARAMS) === FEATURE_PARAMS
|
|
218
224
|
}
|
|
219
225
|
|
|
226
|
+
async queryWithOptions(sql: string, options: ReadOptions, ...params: QueryParam[]): Promise<string> {
|
|
227
|
+
if (!this.routing) throw new RedDBError('CONSISTENCY_UNSUPPORTED', 'use connect() for routed reads')
|
|
228
|
+
return this.routing.queryWithOptions(sql, options, normalizeQueryParams(params))
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
static async routed(address: string, initial: RedDBConnection, tls?: { ca?: string; rejectUnauthorized: boolean }, token?: string) {
|
|
232
|
+
const protocol = tls ? 'reds' : 'red'
|
|
233
|
+
const seed = new URL(`${protocol}://${address}`).href.replace(/\/$/, '')
|
|
234
|
+
const adapt = (raw: RedDBConnection) => ({
|
|
235
|
+
binaryQueryParams: true,
|
|
236
|
+
async call(method: string, params: any = {}) {
|
|
237
|
+
if (method === 'topology') return raw.topologyFor(protocol)
|
|
238
|
+
if (method === 'health') { await raw.query('SELECT 1'); return {} }
|
|
239
|
+
if (method === 'query') return raw.query(params.sql, params.params ?? [])
|
|
240
|
+
if (method === 'bulkInsert') return raw.bulkInsert(params.collection, params.payloads)
|
|
241
|
+
throw new RedDBError('UNSUPPORTED_OPERATION', method)
|
|
242
|
+
},
|
|
243
|
+
async close() { raw.close() },
|
|
244
|
+
})
|
|
245
|
+
const router = new RoutingClient(seed, adapt(initial), async (endpoint: string, signal: AbortSignal) => {
|
|
246
|
+
const uri = new URL(endpoint)
|
|
247
|
+
const pending = connectDirect(`${uri.hostname}:${uri.port}`, tls, token, signal)
|
|
248
|
+
const raw = await pending
|
|
249
|
+
if (signal.aborted) { raw.close(); throw new RedDBError('CLOSED', 'connection canceled') }
|
|
250
|
+
return adapt(raw)
|
|
251
|
+
}, protocol)
|
|
252
|
+
try {
|
|
253
|
+
await router.initialize()
|
|
254
|
+
const facade = new RedDBConnection(null)
|
|
255
|
+
facade.routing = router
|
|
256
|
+
facade.serverFeatures = initial.serverFeatures
|
|
257
|
+
return facade
|
|
258
|
+
} catch (error) { await router.close(); throw error }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private async topologyFor(protocol: string) {
|
|
262
|
+
if (!(this.serverFeatures & 2)) return null
|
|
263
|
+
const response = await this._send(0x32, Buffer.from(protocol))
|
|
264
|
+
if (response.type !== MSG_RESULT) throw new RedDBError('PROTOCOL', 'expected topology Result')
|
|
265
|
+
return decodeTopology(response.payload)
|
|
266
|
+
}
|
|
267
|
+
|
|
220
268
|
async query(sql: string, params: QueryParam[]): Promise<string>
|
|
221
269
|
async query(sql: string, ...params: QueryParam[]): Promise<string>
|
|
222
270
|
async query(sql: string, ...params: unknown[]): Promise<string> {
|
|
223
271
|
const wireParams = normalizeQueryParams(params)
|
|
272
|
+
if (this.routing) return this.routing.call('query', { sql, params: wireParams })
|
|
224
273
|
const hasParams = wireParams.length > 0
|
|
225
274
|
const fullResult = this.supportsParams()
|
|
226
275
|
if (hasParams && !fullResult) {
|
|
@@ -263,6 +312,7 @@ export class RedDBConnection {
|
|
|
263
312
|
}
|
|
264
313
|
|
|
265
314
|
async bulkInsert(collection: string, jsonPayloads: string[]): Promise<number> {
|
|
315
|
+
if (this.routing) return this.routing.call('bulkInsert', { collection, payloads: jsonPayloads })
|
|
266
316
|
const payloads = jsonPayloads.map((p) => JSON.parse(p))
|
|
267
317
|
const resp = await this._send(
|
|
268
318
|
MSG_BULK_INSERT,
|
|
@@ -278,6 +328,7 @@ export class RedDBConnection {
|
|
|
278
328
|
}
|
|
279
329
|
|
|
280
330
|
close() {
|
|
331
|
+
if (this.routing) { void this.routing.close(); return }
|
|
281
332
|
if (this.failure) return
|
|
282
333
|
try {
|
|
283
334
|
this.socket.write(encodeFrame(MSG_BYE, this.nextCorr, Buffer.alloc(0)))
|
|
@@ -288,8 +339,8 @@ export class RedDBConnection {
|
|
|
288
339
|
}
|
|
289
340
|
}
|
|
290
341
|
|
|
291
|
-
export async function connect(addr: string): Promise<RedDBConnection> {
|
|
292
|
-
return
|
|
342
|
+
export async function connect(addr: string, opts: { token?: string } = {}): Promise<RedDBConnection> {
|
|
343
|
+
return RedDBConnection.routed(addr, await connectDirect(addr, undefined, opts.token), undefined, opts.token)
|
|
293
344
|
}
|
|
294
345
|
|
|
295
346
|
/**
|
|
@@ -299,17 +350,20 @@ export async function connect(addr: string): Promise<RedDBConnection> {
|
|
|
299
350
|
*/
|
|
300
351
|
export async function connectTls(
|
|
301
352
|
addr: string,
|
|
302
|
-
opts: { ca?: string; rejectUnauthorized?: boolean } = {},
|
|
353
|
+
opts: { ca?: string; rejectUnauthorized?: boolean; token?: string } = {},
|
|
303
354
|
): Promise<RedDBConnection> {
|
|
304
|
-
|
|
355
|
+
const tls = {
|
|
305
356
|
ca: opts.ca,
|
|
306
357
|
rejectUnauthorized: opts.rejectUnauthorized ?? true,
|
|
307
|
-
}
|
|
358
|
+
}
|
|
359
|
+
return RedDBConnection.routed(addr, await connectDirect(addr, tls, opts.token), tls, opts.token)
|
|
308
360
|
}
|
|
309
361
|
|
|
310
|
-
async function
|
|
311
|
-
|
|
312
|
-
const
|
|
362
|
+
async function connectDirect(addr: string, tls?: { ca?: string; rejectUnauthorized: boolean }, token?: string, signal?: AbortSignal) {
|
|
363
|
+
if (signal?.aborted) throw new RedDBError('CLOSED', 'connection canceled')
|
|
364
|
+
const uri = new URL(`red://${addr}`)
|
|
365
|
+
const host = uri.hostname.replace(/^\[|\]$/g, '')
|
|
366
|
+
const port = Number(uri.port)
|
|
313
367
|
let connRef: RedDBConnection | null = null
|
|
314
368
|
let earlyFailure: Error | null = null
|
|
315
369
|
const fail = (error: Error) => {
|
|
@@ -337,8 +391,12 @@ async function connectWithOptions(addr: string, tls?: { ca?: string; rejectUnaut
|
|
|
337
391
|
|
|
338
392
|
connRef = new RedDBConnection(socket)
|
|
339
393
|
if (earlyFailure) connRef._onError(earlyFailure)
|
|
340
|
-
|
|
394
|
+
const abort = () => connRef?._onError(new RedDBError('CLOSED', 'connection canceled'))
|
|
395
|
+
signal?.addEventListener('abort', abort, { once: true })
|
|
396
|
+
if (signal?.aborted) abort()
|
|
397
|
+
try { await connRef._handshake(token) }
|
|
341
398
|
catch (error) { connRef._onError(error as Error); throw error }
|
|
399
|
+
finally { signal?.removeEventListener('abort', abort) }
|
|
342
400
|
return connRef
|
|
343
401
|
}
|
|
344
402
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reddb-io/client-bun",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.30.1",
|
|
4
4
|
"description": "RedDB wire protocol client for Bun — ultra-fast native TCP",
|
|
5
5
|
"main": "index.ts",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@reddb-io/client": "1.30.1"
|
|
8
|
+
},
|
|
6
9
|
"files": [
|
|
7
10
|
"index.ts",
|
|
8
11
|
"README.md",
|
|
@@ -17,6 +20,6 @@
|
|
|
17
20
|
"wire-protocol"
|
|
18
21
|
],
|
|
19
22
|
"scripts": {
|
|
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"
|
|
23
|
+
"test": "bun test frame-reader.test.ts socket-close.test.ts routing.test.ts && bun run params.test.ts && bun run wire-legacy.test.ts && bun run wire.test.ts"
|
|
21
24
|
}
|
|
22
25
|
}
|