@reddb-io/client-bun 1.30.0 → 1.31.0

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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # @reddb-io/client-bun
2
2
 
3
+ ## 1.31.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies []:
8
+ - @reddb-io/client@1.31.0
9
+
10
+ ## 1.30.1
11
+
12
+ ### Patch Changes
13
+
14
+ - Updated dependencies []:
15
+ - @reddb-io/client@1.30.1
16
+
3
17
  ## 1.30.0
4
18
 
5
19
  ## 1.29.3
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: ['anonymous', 'bearer'],
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 connectWithOptions(addr)
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
- return connectWithOptions(addr, {
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 connectWithOptions(addr: string, tls?: { ca?: string; rejectUnauthorized: boolean }) {
311
- const [host, portStr] = addr.split(':')
312
- const port = parseInt(portStr, 10)
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
- try { await connRef._handshake() }
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.30.0",
3
+ "version": "1.31.0",
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.31.0"
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
  }