@reddb-io/client 1.23.1-rc.340 → 1.23.1-rc.342

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reddb-io/client",
3
- "version": "1.23.1-rc.340",
3
+ "version": "1.23.1-rc.342",
4
4
  "description": "Thin remote-only RedDB driver. Downloads the `red_client` binary on install. 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/index.js CHANGED
@@ -17,6 +17,8 @@ export { RedDBError } from './errors.js'
17
17
  export { RedDB, Collection } from './reddb.js'
18
18
  export {
19
19
  serializeParam,
20
+ serializeJsonValue,
21
+ normalizeExactNumbers,
20
22
  assertSupportedParam,
21
23
  normalizeQueryParams,
22
24
  bytesToBase64,
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import { RedDBError } from './errors.js'
15
+ import { normalizeExactNumbers } from './serialization.js'
15
16
 
16
17
  /**
17
18
  * Parse an NDJSON line into a typed read-session frame, or `null` for a
@@ -29,10 +30,10 @@ export function classifyNdjsonFrame(line) {
29
30
  throw new RedDBError('STREAM_PROTOCOL', `stream frame is not JSON: ${err.message}`)
30
31
  }
31
32
  if (parsed && typeof parsed === 'object') {
32
- if ('descriptor' in parsed) return { type: 'descriptor', value: parsed.descriptor }
33
- if ('cursor' in parsed) return { type: 'cursor', value: parsed.cursor }
34
- if ('row' in parsed) return { type: 'row', value: parsed.row }
35
- if ('end' in parsed) return { type: 'end', value: parsed.end }
33
+ if ('descriptor' in parsed) return { type: 'descriptor', value: normalizeExactNumbers(parsed.descriptor) }
34
+ if ('cursor' in parsed) return { type: 'cursor', value: normalizeExactNumbers(parsed.cursor) }
35
+ if ('row' in parsed) return { type: 'row', value: normalizeExactNumbers(parsed.row) }
36
+ if ('end' in parsed) return { type: 'end', value: normalizeExactNumbers(parsed.end) }
36
37
  if ('error' in parsed) {
37
38
  const e = parsed.error ?? {}
38
39
  throw new RedDBError(e.code || 'STREAM_ERROR', e.message || 'stream error', e)
package/src/core/reddb.js CHANGED
@@ -19,7 +19,7 @@
19
19
  */
20
20
 
21
21
  import { RedDBError } from './errors.js'
22
- import { normalizeQueryParams } from './serialization.js'
22
+ import { normalizeExactNumbers, normalizeQueryParams, serializeJsonValue } from './serialization.js'
23
23
  import { requireInsertId, requireInsertIds } from './insert-ids.js'
24
24
  import { CacheClient } from '../cache.js'
25
25
  import { KvClient } from '../kv.js'
@@ -124,9 +124,9 @@ export class RedDB {
124
124
  query(sql, ...params) {
125
125
  const wireParams = normalizeQueryParams(params)
126
126
  if (wireParams == null) {
127
- return this.client.call('query', { sql })
127
+ return this.client.call('query', { sql }).then(normalizeExactNumbers)
128
128
  }
129
- return this.client.call('query', { sql, params: wireParams })
129
+ return this.client.call('query', { sql, params: wireParams }).then(normalizeExactNumbers)
130
130
  }
131
131
 
132
132
  /** Execute a SQL statement. Alias for `query`, including parameter binding. */
@@ -136,7 +136,8 @@ export class RedDB {
136
136
 
137
137
  /** Insert one row. Returns `{ affected, rid, id }`; `id` is a legacy alias for `rid`. */
138
138
  async insert(collection, payload) {
139
- let result = await this.client.call('insert', { collection, payload })
139
+ let result = await this.client.call('insert', { collection, payload: serializeJsonValue(payload) })
140
+ result = normalizeExactNumbers(result)
140
141
  if (
141
142
  result &&
142
143
  typeof result === 'object' &&
@@ -150,7 +151,10 @@ export class RedDB {
150
151
 
151
152
  /** Insert many rows in one call. Returns `{ affected, rids, ids }`; `ids` is a legacy alias. */
152
153
  async bulkInsert(collection, payloads) {
153
- const result = await this.client.call('bulk_insert', { collection, payloads })
154
+ const result = await this.client.call('bulk_insert', {
155
+ collection,
156
+ payloads: serializeJsonValue(payloads),
157
+ }).then(normalizeExactNumbers)
154
158
  return requireInsertIds(result, payloads.length)
155
159
  }
156
160
 
@@ -244,7 +248,7 @@ export class RedDB {
244
248
 
245
249
  /** Get an entity by id. Returns `{ entity }` (entity is `null` if not found). */
246
250
  get(collection, id) {
247
- return this.client.call('get', { collection, id: String(id) })
251
+ return this.client.call('get', { collection, id: String(id) }).then(normalizeExactNumbers)
248
252
  }
249
253
 
250
254
  /** Delete an entity by id. Returns `{ affected }`. */
@@ -10,8 +10,15 @@
10
10
 
11
11
  import { RedDBError } from './errors.js'
12
12
 
13
+ const MIN_I64 = -(1n << 63n)
14
+ const MAX_I64 = (1n << 63n) - 1n
15
+ const MAX_U64 = (1n << 64n) - 1n
16
+
13
17
  export function serializeParam(value) {
14
18
  assertSupportedParam(value)
19
+ if (typeof value === 'bigint') {
20
+ return exactIntegerEnvelope(value)
21
+ }
15
22
  if (value instanceof Float32Array || value instanceof Float64Array) {
16
23
  return Array.from(value)
17
24
  }
@@ -31,10 +38,59 @@ export function serializeParam(value) {
31
38
  return value
32
39
  }
33
40
 
41
+ export function serializeJsonValue(value) {
42
+ if (typeof value === 'bigint') {
43
+ return exactIntegerEnvelope(value)
44
+ }
45
+ if (Array.isArray(value)) {
46
+ return value.map(serializeJsonValue)
47
+ }
48
+ if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
49
+ if ('$number' in value || '$decimalText' in value) {
50
+ throw new RedDBError('UNSUPPORTED_EXACT_NUMBER', 'superseded exact-number envelope')
51
+ }
52
+ const out = {}
53
+ for (const [key, item] of Object.entries(value)) out[key] = serializeJsonValue(item)
54
+ return out
55
+ }
56
+ return value
57
+ }
58
+
59
+ export function normalizeExactNumbers(value) {
60
+ if (Array.isArray(value)) return value.map(normalizeExactNumbers)
61
+ if (value && typeof value === 'object') {
62
+ const keys = Object.keys(value)
63
+ if (keys.length === 1) {
64
+ if (typeof value.$int === 'string' || typeof value.$uint === 'string') {
65
+ return BigInt(value.$int ?? value.$uint)
66
+ }
67
+ if (typeof value.$decimal === 'string') return value.$decimal
68
+ if ('$number' in value || '$decimalText' in value) {
69
+ throw new RedDBError('UNSUPPORTED_EXACT_NUMBER', 'superseded exact-number envelope')
70
+ }
71
+ }
72
+ const out = {}
73
+ for (const [key, item] of Object.entries(value)) out[key] = normalizeExactNumbers(item)
74
+ return out
75
+ }
76
+ return value
77
+ }
78
+
79
+ function exactIntegerEnvelope(value) {
80
+ if (value >= MIN_I64 && value <= MAX_I64) {
81
+ return { $int: value.toString() }
82
+ }
83
+ if (value >= 0n && value <= MAX_U64) {
84
+ return { $uint: value.toString() }
85
+ }
86
+ throw new RedDBError('UNSUPPORTED_PARAM', 'integer value is outside i64/u64 range')
87
+ }
88
+
34
89
  export function assertSupportedParam(value) {
35
90
  if (value == null) return
36
91
  if (
37
92
  typeof value === 'boolean'
93
+ || typeof value === 'bigint'
38
94
  || typeof value === 'number'
39
95
  || typeof value === 'string'
40
96
  ) {
package/src/grpc.js CHANGED
@@ -10,6 +10,7 @@ import { connect as connectHttp2 } from 'node:http2'
10
10
  import { Buffer } from 'node:buffer'
11
11
 
12
12
  import { RedDBError } from './protocol.js'
13
+ import { normalizeExactNumbers, serializeJsonValue } from './core/serialization.js'
13
14
 
14
15
  const SERVICE = '/reddb.v1.RedDb'
15
16
 
@@ -154,7 +155,7 @@ function normalizeQueryReply(reply) {
154
155
  throw new RedDBError('QUERY_ERROR', `bad gRPC query JSON: ${err.message}`)
155
156
  }
156
157
  }
157
- const rows = parsed.rows ?? parsed.records ?? []
158
+ const rows = normalizeExactNumbers(parsed.rows ?? parsed.records ?? [])
158
159
  return {
159
160
  ok: reply.ok,
160
161
  statement: parsed.statement ?? reply.statement ?? '',
@@ -171,7 +172,7 @@ function normalizeEntityReply(reply) {
171
172
  affected: reply.ok ? 1 : 0,
172
173
  rid,
173
174
  id: rid,
174
- entity: parseJsonOrNull(reply.entity_json),
175
+ entity: normalizeExactNumbers(parseJsonOrNull(reply.entity_json)),
175
176
  }
176
177
  }
177
178
 
@@ -231,6 +232,13 @@ function encodeQueryValue(value) {
231
232
  return bytesField(7, encodeQueryVector(value))
232
233
  }
233
234
  if (typeof value === 'object') {
235
+ if (typeof value.$int === 'string') return int64Field(3, BigInt(value.$int))
236
+ if ('$uint' in value || '$decimal' in value) {
237
+ throw new RedDBError(
238
+ 'UNSUPPORTED_PARAM',
239
+ 'exact uint and decimal params require a JSON body transport',
240
+ )
241
+ }
234
242
  if (typeof value.$bytes === 'string') return bytesField(6, Buffer.from(value.$bytes, 'base64'))
235
243
  if (typeof value.$float === 'string') return doubleField(4, Number(value.$float))
236
244
  if (value.$ts != null) return int64Field(9, BigInt(value.$ts))
@@ -250,14 +258,14 @@ function encodeQueryVector(values) {
250
258
  function encodeJsonCreateRequest(collection, payload) {
251
259
  return concat([
252
260
  stringField(1, collection ?? ''),
253
- stringField(2, JSON.stringify(payload ?? {})),
261
+ stringField(2, JSON.stringify(serializeJsonValue(payload ?? {}))),
254
262
  ])
255
263
  }
256
264
 
257
265
  function encodeJsonBulkCreateRequest(collection, payloads) {
258
266
  const fields = [stringField(1, collection ?? '')]
259
267
  for (const payload of payloads ?? []) {
260
- fields.push(stringField(2, JSON.stringify(payload ?? {})))
268
+ fields.push(stringField(2, JSON.stringify(serializeJsonValue(payload ?? {}))))
261
269
  }
262
270
  return concat(fields)
263
271
  }
@@ -371,7 +379,11 @@ function uint64Field(no, value) {
371
379
  }
372
380
 
373
381
  function int64Field(no, value) {
374
- return concat([varint((BigInt(no) << 3n) | 0n), varint(BigInt.asUintN(64, BigInt(value)))])
382
+ const raw = BigInt(value)
383
+ if (raw < -(1n << 63n) || raw > (1n << 63n) - 1n) {
384
+ throw new RedDBError('UNSUPPORTED_PARAM', 'integer param is outside i64 range')
385
+ }
386
+ return concat([varint((BigInt(no) << 3n) | 0n), varint(BigInt.asUintN(64, raw))])
375
387
  }
376
388
 
377
389
  function doubleField(no, value) {
package/src/http.js CHANGED
@@ -38,6 +38,7 @@
38
38
 
39
39
  import { RedDBError } from './protocol.js'
40
40
  import { classifyNdjsonFrame, splitLines } from './core/ndjson.js'
41
+ import { normalizeExactNumbers, serializeJsonValue } from './core/serialization.js'
41
42
 
42
43
  export class HttpRpcClient {
43
44
  /**
@@ -165,7 +166,7 @@ export class HttpRpcClient {
165
166
 
166
167
  let opened = false
167
168
  let cols = Array.isArray(columns) && columns.length > 0 ? columns.slice() : null
168
- const encodeLine = (obj) => writer.write(`${JSON.stringify(obj)}\n`)
169
+ const encodeLine = (obj) => writer.write(`${JSON.stringify(serializeJsonValue(obj))}\n`)
169
170
  const ensureOpen = async (row) => {
170
171
  if (opened) return
171
172
  if (!cols) {
@@ -177,7 +178,7 @@ export class HttpRpcClient {
177
178
  'inputStream() needs a non-empty column set — pass { columns } or write at least one object row',
178
179
  )
179
180
  }
180
- await writer.write(`${JSON.stringify({ open: { target, columns: cols } })}\n`)
181
+ await writer.write(`${JSON.stringify({ open: { target, columns: cols } })}\n`)
181
182
  opened = true
182
183
  }
183
184
 
@@ -302,9 +303,9 @@ async function parseResponse(response) {
302
303
  const code = body.error_code || 'RPC_ERROR'
303
304
  throw new RedDBError(code, body.error || 'unknown error', body)
304
305
  }
305
- return body.result ?? body
306
+ return normalizeExactNumbers(body.result ?? body)
306
307
  }
307
- return body
308
+ return normalizeExactNumbers(body)
308
309
  }
309
310
 
310
311
  // ---------------------------------------------------------------------------
@@ -318,18 +319,18 @@ const ROUTES = {
318
319
  url: `${base}/query`,
319
320
  init: {
320
321
  method: 'POST',
321
- body: JSON.stringify(
322
- Array.isArray(params) ? { query: sql, params } : { query: sql },
323
- ),
322
+ body: JSON.stringify(
323
+ Array.isArray(params) ? { query: sql, params } : { query: sql },
324
+ ),
324
325
  },
325
326
  }),
326
327
  insert: (base, { collection, payload }) => ({
327
328
  url: `${base}/collections/${encodeURIComponent(collection)}/rows`,
328
- init: { method: 'POST', body: JSON.stringify(payload) },
329
+ init: { method: 'POST', body: JSON.stringify(serializeJsonValue(payload)) },
329
330
  }),
330
331
  bulk_insert: (base, { collection, payloads }) => ({
331
332
  url: `${base}/collections/${encodeURIComponent(collection)}/bulk/rows`,
332
- init: { method: 'POST', body: JSON.stringify({ rows: payloads }) },
333
+ init: { method: 'POST', body: JSON.stringify(serializeJsonValue({ rows: payloads })) },
333
334
  }),
334
335
  get: (base, { collection, id }) => ({
335
336
  url: `${base}/collections/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`,
@@ -371,7 +372,7 @@ const ROUTES = {
371
372
  }),
372
373
  'cache.put': (base, { namespace, key, value, ttl_ms, tags, policy }) => ({
373
374
  url: `${base}/cache/ns/${encodeURIComponent(namespace)}/${encodeURIComponent(key)}`,
374
- init: { method: 'PUT', body: JSON.stringify({ value, ttl_ms, tags, policy }) },
375
+ init: { method: 'PUT', body: JSON.stringify(serializeJsonValue({ value, ttl_ms, tags, policy })) },
375
376
  }),
376
377
  'cache.exists': (base, { namespace, key }) => ({
377
378
  url: `${base}/cache/ns/${encodeURIComponent(namespace)}/${encodeURIComponent(key)}/exists`,
@@ -65,6 +65,9 @@ export const ValueTag = Object.freeze({
65
65
  Bytes: 0x05, Vector: 0x06, Json: 0x07, Timestamp: 0x08, Uuid: 0x09,
66
66
  })
67
67
 
68
+ const MIN_I64 = -(1n << 63n)
69
+ const MAX_I64 = (1n << 63n) - 1n
70
+
68
71
  /**
69
72
  * Typed value tags for the binary fast path. Identical to the
70
73
  * engine-side `wire::protocol::VAL_*` table.
@@ -1015,10 +1018,7 @@ export function encodeValue(v) {
1015
1018
  if (v === null || v === undefined) return Uint8Array.of(ValueTag.Null)
1016
1019
  if (typeof v === 'boolean') return Uint8Array.of(ValueTag.Bool, v ? 1 : 0)
1017
1020
  if (typeof v === 'bigint') {
1018
- const out = new Uint8Array(1 + 8)
1019
- out[0] = ValueTag.Int
1020
- new DataView(out.buffer).setBigInt64(1, v, true)
1021
- return out
1021
+ return encodeInt(v)
1022
1022
  }
1023
1023
  if (typeof v === 'number') {
1024
1024
  if (Number.isInteger(v) && v >= -(2 ** 53) && v <= 2 ** 53) {
@@ -1046,6 +1046,12 @@ export function encodeValue(v) {
1046
1046
  const keys = Object.keys(v)
1047
1047
  if (keys.length === 1) {
1048
1048
  const k = keys[0]
1049
+ if (k === '$int' && typeof v.$int === 'string') {
1050
+ return encodeInt(BigInt(v.$int))
1051
+ }
1052
+ if (k === '$uint' || k === '$decimal') {
1053
+ throw new RedDBError('UNSUPPORTED_PARAM', `${k} params require a JSON body transport`)
1054
+ }
1049
1055
  if (k === '$bytes' && typeof v.$bytes === 'string') {
1050
1056
  return encodeLenPrefixed(ValueTag.Bytes, base64ToBytes(v.$bytes))
1051
1057
  }
@@ -1072,6 +1078,16 @@ export function encodeValue(v) {
1072
1078
  throw new RedDBError('UNSUPPORTED_PARAM', `cannot encode param of type ${typeof v}`)
1073
1079
  }
1074
1080
 
1081
+ function encodeInt(value) {
1082
+ if (value < MIN_I64 || value > MAX_I64) {
1083
+ throw new RedDBError('UNSUPPORTED_PARAM', 'integer param is outside i64 range')
1084
+ }
1085
+ const out = new Uint8Array(1 + 8)
1086
+ out[0] = ValueTag.Int
1087
+ new DataView(out.buffer).setBigInt64(1, value, true)
1088
+ return out
1089
+ }
1090
+
1075
1091
  function encodeLenPrefixed(tag, bytes) {
1076
1092
  if (bytes.length > MAX_VALUE_PAYLOAD_LEN) {
1077
1093
  throw new RedDBError('PAYLOAD_TOO_LARGE', `value len ${bytes.length} > ${MAX_VALUE_PAYLOAD_LEN}`)