@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 +1 -1
- package/src/core/index.js +2 -0
- package/src/core/ndjson.js +5 -4
- package/src/core/reddb.js +10 -6
- package/src/core/serialization.js +56 -0
- package/src/grpc.js +17 -5
- package/src/http.js +11 -10
- package/src/redwire-core.js +20 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reddb-io/client",
|
|
3
|
-
"version": "1.23.1-rc.
|
|
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
package/src/core/ndjson.js
CHANGED
|
@@ -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', {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
322
|
-
|
|
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`,
|
package/src/redwire-core.js
CHANGED
|
@@ -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
|
-
|
|
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}`)
|