@reddb-io/client 1.7.0 → 1.9.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.
@@ -0,0 +1,156 @@
1
+ /**
2
+ * @reddb-io/client — browser entrypoint.
3
+ *
4
+ * The browser counterpart to `./index.js`. It builds `connect()` on the
5
+ * transport-agnostic core (`./core/index.js`), dispatching only the
6
+ * browser-reachable wires:
7
+ *
8
+ * - 'http://host:port' — HTTP JSON-RPC over `fetch`
9
+ * - 'https://host:port' — HTTPS JSON-RPC over `fetch`
10
+ *
11
+ * Streaming is the Web-streams implementation (`./streaming-web.js`), so the
12
+ * full transport-agnostic surface works client-side: query, execute, insert,
13
+ * bulkInsert, transactions, the kv/documents/queue/cache/config/vault clients,
14
+ * the typed query builder, and streaming (`stream()` / `inputStream()`).
15
+ *
16
+ * This module imports **neither** the gRPC transport (`./grpc.js`, which pulls
17
+ * `node:http2`) **nor** the RedWire transport (`./redwire.js`) **nor** the
18
+ * `node:stream` streaming impl (`./streaming.js`). No `node:` built-in enters
19
+ * the browser bundle graph through it. A portability guard test
20
+ * (`test/browser-portability.test.mjs`) is the regression net for that.
21
+ *
22
+ * Schemes that need a raw TCP socket or HTTP/2 — `grpc://`, `grpcs://`,
23
+ * `red://`, `reds://`, and `pg` — are not reachable from a browser sandbox.
24
+ * `connect()` rejects them with an actionable error (see
25
+ * `BROWSER_TRANSPORT_UNSUPPORTED` below) instead of crashing the bundler or
26
+ * runtime. Embedded URIs (`memory://`, `file://`, `red:///path`) are rejected
27
+ * with the same wording as the Node entry.
28
+ */
29
+
30
+ import {
31
+ RedDBError,
32
+ RedDB as CoreRedDB,
33
+ Collection,
34
+ EmbeddedNotSupported,
35
+ EMBEDDED_REJECTION_MESSAGE,
36
+ isEmbeddedUri,
37
+ rejectEmbeddedUri,
38
+ parseUri,
39
+ login,
40
+ mergeAuthFromUri,
41
+ } from './core/index.js'
42
+ import { HttpRpcClient } from './http.js'
43
+ import { createSelectStream, createInputStream } from './streaming-web.js'
44
+
45
+ export { RedDBError, EmbeddedNotSupported, EMBEDDED_REJECTION_MESSAGE, isEmbeddedUri }
46
+ export { RowReadable, RowWritable } from './streaming-web.js'
47
+ export { CacheClient } from './cache.js'
48
+ export { KvClient } from './kv.js'
49
+ export { QueueClient } from './queue.js'
50
+ export { DocumentClient } from './documents.js'
51
+ export { ConfigClient } from './config.js'
52
+ export { VaultClient } from './vault.js'
53
+ export { TypedQueryBuilder } from './db-helpers.js'
54
+ export { parseUri, deriveLoginUrl } from './url.js'
55
+ export { login }
56
+
57
+ // The Web-streams streaming implementation, injected into the core `RedDB` so
58
+ // its `stream()` / `inputStream()` return Web-streams-backed row wrappers. The
59
+ // core itself never statically references Web (or `node:`) streams.
60
+ const WEB_STREAMING = { createSelectStream, createInputStream }
61
+
62
+ /**
63
+ * Shared wording for schemes a browser sandbox cannot reach: they need a raw
64
+ * TCP socket (`red(s)://`, `pg`) or HTTP/2 (`grpc(s)://`), neither of which a
65
+ * browser exposes to JavaScript. The remedy is the same in every case — point
66
+ * the client at an HTTP(S) endpoint or gateway in front of the server.
67
+ */
68
+ function browserTransportError(scheme) {
69
+ return new RedDBError(
70
+ 'BROWSER_TRANSPORT_UNSUPPORTED',
71
+ `'${scheme}' connections are not available in the browser: the `
72
+ + `browser sandbox exposes no raw TCP socket (red://, reds://, pg) or `
73
+ + `HTTP/2 client (grpc://, grpcs://) to JavaScript. Connect to an `
74
+ + `HTTP(S) endpoint instead — e.g. 'http://host:port' / `
75
+ + `'https://host:port' — by running RedDB's HTTP JSON-RPC listener or `
76
+ + `an HTTP gateway in front of the server, then call `
77
+ + `connect('https://…').`,
78
+ )
79
+ }
80
+
81
+ /**
82
+ * Connect to a remote RedDB instance from a browser.
83
+ *
84
+ * @param {string} uri Connection URI. Only `http(s)://` is reachable from a
85
+ * browser; other schemes raise `BROWSER_TRANSPORT_UNSUPPORTED`.
86
+ * @param {object} [options]
87
+ * @param {object} [options.auth] Authentication credentials.
88
+ * @param {string} [options.auth.token] Bearer / API-key token.
89
+ * @param {string} [options.auth.apiKey] Alias for `token`.
90
+ * @param {string} [options.auth.username] Username for password login.
91
+ * @param {string} [options.auth.password] Password for password login.
92
+ * @param {string} [options.auth.loginUrl] Override URL for the password
93
+ * exchange (defaults to deriving `/auth/login` from `uri`).
94
+ * @returns {Promise<RedDB>}
95
+ */
96
+ export async function connect(uri, options = {}) {
97
+ // Reject embedded shapes upfront with the same wording the Node entry and
98
+ // the Rust binary use, before the URL parser would map them to kind=embedded.
99
+ rejectEmbeddedUri(uri)
100
+
101
+ const parsed = parseUri(uri)
102
+
103
+ // Belt-and-braces: if the parser still produced an embedded kind, reject it.
104
+ if (parsed.kind === 'embedded') {
105
+ throw new EmbeddedNotSupported(uri)
106
+ }
107
+
108
+ if (parsed.kind === 'http' || parsed.kind === 'https') {
109
+ const merged = mergeAuthFromUri(parsed, options.auth)
110
+ const baseUrl = `${parsed.kind}://${parsed.host}:${parsed.port}`
111
+ let token = merged.token
112
+ if (!token && merged.username && merged.password) {
113
+ const loginUrl = merged.loginUrl ?? `${baseUrl}/auth/login`
114
+ const session = await login(loginUrl, {
115
+ username: merged.username,
116
+ password: merged.password,
117
+ })
118
+ token = session.token
119
+ }
120
+ const client = new HttpRpcClient({ baseUrl, token })
121
+ await client.call('query', { sql: 'SELECT 1' })
122
+ return new RedDB(client)
123
+ }
124
+
125
+ if (
126
+ parsed.kind === 'grpc'
127
+ || parsed.kind === 'grpcs'
128
+ || parsed.kind === 'red'
129
+ || parsed.kind === 'reds'
130
+ || parsed.kind === 'pg'
131
+ ) {
132
+ throw browserTransportError(parsed.kind)
133
+ }
134
+
135
+ throw new RedDBError(
136
+ 'UNSUPPORTED_KIND',
137
+ `internal: parsed kind '${parsed.kind}' has no browser transport`,
138
+ )
139
+ }
140
+
141
+ /**
142
+ * Browser connection handle. The full request-shaping surface lives in the
143
+ * transport-agnostic core `RedDB`; this subclass exists only to inject the
144
+ * Web-streams streaming implementation so `stream()` / `inputStream()` return
145
+ * Web-streams-backed row wrappers. The public surface — every method, the
146
+ * `kv`/`config`/`vault` factory shapes, the `cache`/`queue`/`documents`
147
+ * clients — is the core's, unchanged.
148
+ */
149
+ export class RedDB extends CoreRedDB {
150
+ /** @param {HttpRpcClient} client */
151
+ constructor(client) {
152
+ super(client, WEB_STREAMING)
153
+ }
154
+ }
155
+
156
+ export { Collection }
package/src/index.js CHANGED
@@ -26,26 +26,26 @@
26
26
  * need an embedded engine, install `@reddb-io/sdk` instead.
27
27
  */
28
28
 
29
- import { RedDBError } from './protocol.js'
30
- import { HttpRpcClient } from './http.js'
31
- import { GrpcRpcClient } from './grpc.js'
32
- import { connectRedwire } from './redwire.js'
33
- import { parseUri, deriveLoginUrl } from './url.js'
34
29
  import {
30
+ RedDBError,
31
+ RedDB as CoreRedDB,
32
+ Collection,
35
33
  EmbeddedNotSupported,
36
34
  EMBEDDED_REJECTION_MESSAGE,
37
35
  isEmbeddedUri,
38
36
  rejectEmbeddedUri,
39
- } from './embedded-rejection.js'
40
- import { CacheClient } from './cache.js'
41
- import { KvClient } from './kv.js'
42
- import { QueueClient } from './queue.js'
43
- import { DocumentClient } from './documents.js'
44
- import { ConfigClient } from './config.js'
45
- import { VaultClient } from './vault.js'
46
- import { TypedQueryBuilder, collectionExists, listCollections } from './db-helpers.js'
37
+ parseUri,
38
+ deriveLoginUrl,
39
+ login,
40
+ mergeAuthFromUri,
41
+ } from './core/index.js'
42
+ import { HttpRpcClient } from './http.js'
43
+ import { GrpcRpcClient } from './grpc.js'
44
+ import { connectRedwire } from './redwire.js'
45
+ import { createSelectStream, createInputStream } from './streaming.js'
47
46
 
48
47
  export { RedDBError, EmbeddedNotSupported, EMBEDDED_REJECTION_MESSAGE, isEmbeddedUri }
48
+ export { splitNdjson, RowReadable, RowWritable } from './streaming.js'
49
49
  export { CacheClient } from './cache.js'
50
50
  export { KvClient } from './kv.js'
51
51
  export { QueueClient } from './queue.js'
@@ -54,9 +54,12 @@ export { ConfigClient } from './config.js'
54
54
  export { VaultClient } from './vault.js'
55
55
  export { TypedQueryBuilder } from './db-helpers.js'
56
56
  export { parseUri, deriveLoginUrl } from './url.js'
57
+ export { login }
57
58
 
58
- const MIN_INSERT_ID_ENGINE_VERSION = '1.0.9'
59
- const NESTED_TX_NOT_SUPPORTED = 'NESTED_TX_NOT_SUPPORTED'
59
+ // The `node:stream`-based streaming implementation, injected into the core
60
+ // `RedDB` so its `stream()` / `inputStream()` return Node streams. The core
61
+ // itself never statically references `node:stream`.
62
+ const NODE_STREAMING = { createSelectStream, createInputStream }
60
63
 
61
64
  /**
62
65
  * Connect to a remote RedDB instance.
@@ -158,89 +161,6 @@ export async function connect(uri, options = {}) {
158
161
  )
159
162
  }
160
163
 
161
- function serializeParam(value) {
162
- assertSupportedParam(value)
163
- if (value instanceof Float32Array || value instanceof Float64Array) {
164
- return Array.from(value)
165
- }
166
- if (value instanceof Date) {
167
- return { $ts: String(BigInt(value.getTime()) * 1_000_000n) }
168
- }
169
- if (value instanceof Uint8Array || (typeof Buffer !== 'undefined' && value instanceof Buffer)) {
170
- return { $bytes: bytesToBase64(value) }
171
- }
172
- if (typeof value === 'number' && !Number.isFinite(value)) {
173
- if (Number.isNaN(value)) return { $float: 'NaN' }
174
- return { $float: value > 0 ? 'Infinity' : '-Infinity' }
175
- }
176
- if (typeof value === 'string' && isUuidString(value)) {
177
- return { $uuid: value }
178
- }
179
- return value
180
- }
181
-
182
- function assertSupportedParam(value) {
183
- if (value == null) return
184
- if (
185
- typeof value === 'boolean'
186
- || typeof value === 'number'
187
- || typeof value === 'string'
188
- ) {
189
- return
190
- }
191
- if (value instanceof Date) {
192
- if (Number.isNaN(value.getTime())) {
193
- throw new RedDBError('UNSUPPORTED_PARAM', 'cannot encode invalid Date query parameter')
194
- }
195
- return
196
- }
197
- if (
198
- value instanceof Uint8Array
199
- || value instanceof Float32Array
200
- || value instanceof Float64Array
201
- || (typeof Buffer !== 'undefined' && value instanceof Buffer)
202
- ) {
203
- return
204
- }
205
- if (Array.isArray(value)) {
206
- if (value.every((item) => typeof item === 'number')) return
207
- throw new RedDBError(
208
- 'UNSUPPORTED_PARAM',
209
- 'array query parameters must contain only numbers',
210
- )
211
- }
212
- if (typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
213
- return
214
- }
215
- throw new RedDBError(
216
- 'UNSUPPORTED_PARAM',
217
- `cannot encode query parameter of type ${typeof value}`,
218
- )
219
- }
220
-
221
- function normalizeQueryParams(args) {
222
- if (args.length === 0) return null
223
- if (args.length === 1 && Array.isArray(args[0])) return args[0].map(serializeParam)
224
- return args.map(serializeParam)
225
- }
226
-
227
- function bytesToBase64(value) {
228
- const bytes = value instanceof Uint8Array
229
- ? value
230
- : new Uint8Array(value.buffer, value.byteOffset, value.byteLength)
231
- if (typeof Buffer !== 'undefined') {
232
- return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString('base64')
233
- }
234
- let text = ''
235
- for (const byte of bytes) text += String.fromCharCode(byte)
236
- // eslint-disable-next-line no-undef
237
- return btoa(text)
238
- }
239
-
240
- function isUuidString(value) {
241
- return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)
242
- }
243
-
244
164
  /**
245
165
  * Resolve TLS options for a redwire(s) connection. Source order:
246
166
  * 1. caller-supplied `options.tls` object.
@@ -270,317 +190,22 @@ function buildTlsOpts(parsed, callerTls) {
270
190
  }
271
191
  }
272
192
 
273
- function mergeAuthFromUri(parsed, optionAuth) {
274
- const out = {
275
- token: parsed.token ?? parsed.apiKey ?? null,
276
- username: parsed.username ?? null,
277
- password: parsed.password ?? null,
278
- loginUrl: parsed.loginUrl ?? null,
279
- }
280
- if (optionAuth == null) return out
281
- if (typeof optionAuth !== 'object') {
282
- throw new TypeError('options.auth must be an object')
283
- }
284
- if (optionAuth.token != null) {
285
- if (typeof optionAuth.token !== 'string' || optionAuth.token.length === 0) {
286
- throw new TypeError('options.auth.token must be a non-empty string')
287
- }
288
- out.token = optionAuth.token
289
- }
290
- if (optionAuth.apiKey != null) {
291
- if (typeof optionAuth.apiKey !== 'string' || optionAuth.apiKey.length === 0) {
292
- throw new TypeError('options.auth.apiKey must be a non-empty string')
293
- }
294
- out.token = optionAuth.apiKey
295
- }
296
- if (optionAuth.username != null) {
297
- if (typeof optionAuth.username !== 'string' || optionAuth.username.length === 0) {
298
- throw new TypeError('options.auth.username must be a non-empty string')
299
- }
300
- out.username = optionAuth.username
301
- }
302
- if (optionAuth.password != null) {
303
- if (typeof optionAuth.password !== 'string' || optionAuth.password.length === 0) {
304
- throw new TypeError('options.auth.password must be a non-empty string')
305
- }
306
- out.password = optionAuth.password
307
- }
308
- if (optionAuth.loginUrl != null) {
309
- out.loginUrl = optionAuth.loginUrl
310
- }
311
- return out
312
- }
313
-
314
193
  /**
315
- * Exchange username + password for a bearer token via the server's
316
- * `POST /auth/login` HTTP endpoint. Same flow used by `connect()` when
317
- * the caller passes `auth: { username, password }`.
194
+ * Node connection handle. The full request-shaping surface lives in the
195
+ * transport-agnostic core `RedDB`; this subclass exists only to inject the
196
+ * `node:stream`-based streaming implementation so `stream()` / `inputStream()`
197
+ * return Node streams. The public surface — every method, the `kv`/`config`/
198
+ * `vault` factory shapes, the `cache`/`queue`/`documents` clients — is the
199
+ * core's, unchanged.
318
200
  *
319
- * @param {string} loginUrl Full URL of the server's auth endpoint.
320
- * @param {{ username: string, password: string }} credentials
321
- * @returns {Promise<{ token: string, username: string, role: string, expires_at: number }>}
201
+ * The `Collection` handle (`db.collection(name)`) is re-exported from the
202
+ * core verbatim.
322
203
  */
323
- export async function login(loginUrl, { username, password }) {
324
- if (typeof loginUrl !== 'string' || !loginUrl.startsWith('http')) {
325
- throw new TypeError("login() requires an http(s):// URL pointing at /auth/login")
326
- }
327
- if (typeof username !== 'string' || username.length === 0) {
328
- throw new TypeError('login() requires a non-empty username')
329
- }
330
- if (typeof password !== 'string' || password.length === 0) {
331
- throw new TypeError('login() requires a non-empty password')
332
- }
333
- const response = await fetch(loginUrl, {
334
- method: 'POST',
335
- headers: { 'content-type': 'application/json' },
336
- body: JSON.stringify({ username, password }),
337
- })
338
- const body = await response.json().catch(() => ({}))
339
- if (!response.ok || body.ok === false) {
340
- const code = body.error_code || `HTTP_${response.status}`
341
- const message = body.error || `auth/login returned ${response.status}`
342
- throw new RedDBError(code, message, body)
343
- }
344
- if (typeof body.token !== 'string') {
345
- throw new RedDBError(
346
- 'AUTH_LOGIN_BAD_RESPONSE',
347
- 'auth/login response missing string token',
348
- body,
349
- )
350
- }
351
- return body
352
- }
353
-
354
- /**
355
- * Connection handle. Methods map 1:1 to JSON-RPC methods on the server.
356
- * Identical surface to `@reddb-io/sdk`'s `RedDB`, minus the local-spawn
357
- * lifecycle.
358
- */
359
- class TransactionHandle {
360
- constructor(db) {
361
- this.db = db
362
- }
363
-
364
- query(sql, ...params) {
365
- return this.db.query(sql, ...params)
366
- }
367
-
368
- execute(sql, ...params) {
369
- return this.db.execute(sql, ...params)
370
- }
371
-
372
- insert(collection, payload) {
373
- return this.db.insert(collection, payload)
374
- }
375
-
376
- bulkInsert(collection, payloads) {
377
- return this.db.bulkInsert(collection, payloads)
378
- }
379
-
380
- async transaction() {
381
- throw nestedTransactionError()
382
- }
383
- }
384
-
385
- export class RedDB {
204
+ export class RedDB extends CoreRedDB {
386
205
  /** @param {HttpRpcClient | import('./redwire.js').RedWireClient} client */
387
206
  constructor(client) {
388
- this.client = client
389
- this.cache = new CacheClient(client)
390
- this.queue = new QueueClient(client)
391
- this.documents = new DocumentClient(this)
392
- const defaultKv = new KvClient(client)
393
- this.kv = Object.assign((collection = 'kv_default') => new KvClient(client, collection), {
394
- put: defaultKv.put.bind(defaultKv),
395
- invalidateTags: defaultKv.invalidateTags.bind(defaultKv),
396
- watch: defaultKv.watch.bind(defaultKv),
397
- watchPrefix: defaultKv.watchPrefix.bind(defaultKv),
398
- })
399
- this.config = (collection = 'red.config') => new ConfigClient(client, collection)
400
- this.vault = (collection = 'red.vault') => new VaultClient(client, collection)
401
- this.inTransaction = false
402
- }
403
-
404
- /** Execute a SQL query. Returns `{ statement, affected, columns, rows }`. */
405
- query(sql, ...params) {
406
- const wireParams = normalizeQueryParams(params)
407
- if (wireParams == null) {
408
- return this.client.call('query', { sql })
409
- }
410
- return this.client.call('query', { sql, params: wireParams })
411
- }
412
-
413
- /** Execute a SQL statement. Alias for `query`, including parameter binding. */
414
- execute(sql, ...params) {
415
- return this.query(sql, ...params)
416
- }
417
-
418
- /** Insert one row. Returns `{ affected, rid, id }`; `id` is a legacy alias for `rid`. */
419
- async insert(collection, payload) {
420
- let result = await this.client.call('insert', { collection, payload })
421
- if (
422
- result &&
423
- typeof result === 'object' &&
424
- !('affected' in result) &&
425
- ('rid' in result || 'id' in result)
426
- ) {
427
- result = { ...result, affected: 1 }
428
- }
429
- return requireInsertId(result, 'insert')
430
- }
431
-
432
- /** Insert many rows in one call. Returns `{ affected, rids, ids }`; `ids` is a legacy alias. */
433
- async bulkInsert(collection, payloads) {
434
- const result = await this.client.call('bulk_insert', { collection, payloads })
435
- return requireInsertIds(result, payloads.length)
436
- }
437
-
438
- async transaction(callback) {
439
- if (this.inTransaction) {
440
- throw nestedTransactionError()
441
- }
442
- if (typeof callback !== 'function') {
443
- throw new TypeError('transaction(callback) requires a function')
444
- }
445
-
446
- this.inTransaction = true
447
- let began = false
448
- try {
449
- await this.query('BEGIN')
450
- began = true
451
- const result = await callback(new TransactionHandle(this))
452
- await this.query('COMMIT')
453
- return result
454
- } catch (err) {
455
- if (began) {
456
- try {
457
- await this.query('ROLLBACK')
458
- } catch (rollbackErr) {
459
- attachRollbackError(err, rollbackErr)
460
- }
461
- }
462
- throw err
463
- } finally {
464
- this.inTransaction = false
465
- }
466
- }
467
-
468
- /** Return true when a collection is visible in the catalog. */
469
- exists(collection) {
470
- return collectionExists(this, collection)
471
- }
472
-
473
- /** List visible collections using SHOW COLLECTIONS. */
474
- list() {
475
- return listCollections(this)
476
- }
477
-
478
- /** Return a caller-typed query builder for a collection. */
479
- from(collection) {
480
- return new TypedQueryBuilder(this, collection)
481
- }
482
-
483
- /** Get an entity by id. Returns `{ entity }` (entity is `null` if not found). */
484
- get(collection, id) {
485
- return this.client.call('get', { collection, id: String(id) })
486
- }
487
-
488
- /** Delete an entity by id. Returns `{ affected }`. */
489
- delete(collection, id) {
490
- return this.client.call('delete', { collection, id: String(id) })
491
- }
492
-
493
- /** Probe the server. Returns `{ ok: true, version }`. */
494
- health() {
495
- return this.client.call('health', {})
496
- }
497
-
498
- /** Server version + protocol version. */
499
- version() {
500
- return this.client.call('version', {})
501
- }
502
-
503
- /** Exchange username + password for a bearer token. */
504
- login(username, password) {
505
- return this.client.call('auth.login', { username, password })
506
- }
507
-
508
- /** Identify the current caller. */
509
- whoami() {
510
- return this.client.call('auth.whoami', {})
207
+ super(client, NODE_STREAMING)
511
208
  }
512
-
513
- /** Change the current caller's password. */
514
- changePassword(currentPassword, newPassword) {
515
- return this.client.call('auth.change_password', {
516
- current_password: currentPassword,
517
- new_password: newPassword,
518
- })
519
- }
520
-
521
- /** Mint a long-lived API key. */
522
- createApiKey({ username, role } = {}) {
523
- return this.client.call('auth.create_api_key', { username, role })
524
- }
525
-
526
- /** Revoke an API key by its public id. */
527
- revokeApiKey(key) {
528
- return this.client.call('auth.revoke_api_key', { key })
529
- }
530
-
531
- /** Close the underlying transport. */
532
- close() {
533
- return this.client.close()
534
- }
535
- }
536
-
537
- function nestedTransactionError() {
538
- return new RedDBError(
539
- NESTED_TX_NOT_SUPPORTED,
540
- `${NESTED_TX_NOT_SUPPORTED}: nested transactions are not supported on one connection`,
541
- )
542
209
  }
543
210
 
544
- function attachRollbackError(err, rollbackErr) {
545
- if (err && typeof err === 'object') {
546
- try {
547
- err.rollbackError = rollbackErr
548
- } catch {
549
- // Preserve the original callback/query error even for frozen errors.
550
- }
551
- }
552
- }
553
-
554
- function requireInsertId(result, method) {
555
- if (!result || typeof result !== 'object' || (result.rid == null && result.id == null)) {
556
- throw new RedDBError(
557
- 'ENGINE_TOO_OLD',
558
- `${method}() requires RedDB engine >= ${MIN_INSERT_ID_ENGINE_VERSION} with insert id support`,
559
- )
560
- }
561
- if (result.rid == null) result.rid = result.id
562
- if (result.id == null) result.id = result.rid
563
- return result
564
- }
565
-
566
- function requireInsertIds(result, expected) {
567
- if (
568
- !result ||
569
- typeof result !== 'object' ||
570
- (!Array.isArray(result.rids) && !Array.isArray(result.ids))
571
- ) {
572
- throw new RedDBError(
573
- 'ENGINE_TOO_OLD',
574
- `bulkInsert() requires RedDB engine >= ${MIN_INSERT_ID_ENGINE_VERSION} with bulk insert id support`,
575
- )
576
- }
577
- if (!Array.isArray(result.rids)) result.rids = result.ids
578
- if (!Array.isArray(result.ids)) result.ids = result.rids
579
- if (result.rids.length !== expected) {
580
- throw new RedDBError(
581
- 'INVALID_RESPONSE',
582
- `bulkInsert() expected ${expected} rids, got ${result.rids.length}`,
583
- )
584
- }
585
- return result
586
- }
211
+ export { Collection }
package/src/protocol.js CHANGED
@@ -8,23 +8,16 @@
8
8
  * Spec: PLAN_DRIVERS.md, "Spec do protocolo stdio".
9
9
  */
10
10
 
11
+ import { RedDBError } from './core/errors.js'
12
+
13
+ // Re-exported from the transport-agnostic core so existing
14
+ // `import { RedDBError } from './protocol.js'` call sites keep working.
15
+ export { RedDBError }
16
+
11
17
  const NEWLINE = 0x0a // '\n'
12
18
  const encoder = new TextEncoder()
13
19
  const decoder = new TextDecoder('utf-8')
14
20
 
15
- /**
16
- * RedDB-shaped error. Drivers in other languages should expose an
17
- * equivalent class with the same `code` field.
18
- */
19
- export class RedDBError extends Error {
20
- constructor(code, message, data) {
21
- super(message)
22
- this.name = 'RedDBError'
23
- this.code = code
24
- this.data = data ?? null
25
- }
26
- }
27
-
28
21
  export class RpcClient {
29
22
  /** @param {import('./spawn.js').RedProcess} child */
30
23
  constructor(child) {
package/src/queue.js CHANGED
@@ -38,6 +38,30 @@ export class QueueClient {
38
38
  sql: `QUEUE PURGE ${queueIdentifier(queue)}`,
39
39
  })
40
40
  }
41
+
42
+ // Live `QUEUE READ … WAIT <ms>` helper (PRD #718 / #725). Same
43
+ // contract as drivers/js: required `waitMs`, timeout returns empty
44
+ // array, cancellation/cap rejection surface as transport errors.
45
+ async readWait(queue, consumer, options = {}) {
46
+ const sql = buildQueueReadWaitSql(queue, consumer, options)
47
+ const result = await this.client.call('query', { sql })
48
+ return queuePayloads(result)
49
+ }
50
+ }
51
+
52
+ function buildQueueReadWaitSql(queue, consumer, options) {
53
+ const { waitMs, group = null, count = null } = options ?? {}
54
+ if (!Number.isInteger(waitMs) || waitMs < 0) {
55
+ throw new RedDBError(
56
+ 'INVALID_QUEUE_WAIT',
57
+ 'queue readWait requires an explicit non-negative integer waitMs (no infinite wait)',
58
+ )
59
+ }
60
+ const q = queueIdentifier(queue)
61
+ const c = queueIdentifier(consumer)
62
+ const g = group != null ? ` GROUP ${queueIdentifier(group)}` : ''
63
+ const n = count != null ? queueCount(count) : ''
64
+ return `QUEUE READ ${q}${g} CONSUMER ${c}${n} WAIT ${waitMs}ms`
41
65
  }
42
66
 
43
67
  function queueIdentifier(value) {