@reddb-io/sdk 1.0.5 → 1.0.8
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/index.d.ts +1 -0
- package/package.json +2 -2
- package/postinstall.js +57 -7
- package/src/index.js +32 -3
- package/src/redwire.js +217 -6
package/index.d.ts
CHANGED
|
@@ -224,6 +224,7 @@ export class RedDB {
|
|
|
224
224
|
readonly vault: (collection?: string) => VaultClient
|
|
225
225
|
|
|
226
226
|
query(sql: string): Promise<QueryResult>
|
|
227
|
+
query(sql: string, params: Array<number | string | null>): Promise<QueryResult>
|
|
227
228
|
insert(collection: string, payload: Record<string, unknown>): Promise<InsertResult>
|
|
228
229
|
bulkInsert(
|
|
229
230
|
collection: string,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reddb-io/sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "Official RedDB driver — talks the native RedWire TCP protocol (mTLS), HTTP, gRPC bridge, or embedded stdio JSON-RPC. Works in Node 18+, Bun and Deno.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -45,6 +45,6 @@
|
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"postinstall": "node postinstall.js",
|
|
48
|
-
"test": "node --test test/cache.test.mjs && node test/smoke.test.mjs"
|
|
48
|
+
"test": "node --test test/cache.test.mjs test/params.test.mjs test/redwire.params.test.mjs && node test/smoke.test.mjs"
|
|
49
49
|
}
|
|
50
50
|
}
|
package/postinstall.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import { createRequire } from 'node:module'
|
|
20
20
|
import { fileURLToPath } from 'node:url'
|
|
21
|
-
import { dirname, join } from 'node:path'
|
|
21
|
+
import { dirname, join, sep } from 'node:path'
|
|
22
22
|
import { existsSync, mkdirSync, writeFileSync, chmodSync } from 'node:fs'
|
|
23
23
|
|
|
24
24
|
import { fetchReleaseAsset } from './src/internal/asset-fetcher/index.js'
|
|
@@ -34,16 +34,66 @@ if (process.env.REDDB_SKIP_POSTINSTALL === '1') {
|
|
|
34
34
|
process.exit(0)
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
// Workspace-local install detection. When this file's resolved path is not
|
|
38
|
+
// inside a `node_modules` tree we're running from a fresh checkout of the
|
|
39
|
+
// reddb monorepo (or a fork), where `package.json` may already carry a
|
|
40
|
+
// version whose GitHub Release has not been built yet — and where the
|
|
41
|
+
// developer typically builds `red` with `cargo build` anyway. Skip the
|
|
42
|
+
// download with a clear pointer; release flows still work because the npm
|
|
43
|
+
// tarball, when installed, lives under `node_modules/`.
|
|
44
|
+
if (!HERE.includes(`${sep}node_modules${sep}`)) {
|
|
45
|
+
process.stdout.write(
|
|
46
|
+
'reddb: skipping postinstall — running from a workspace checkout (no node_modules/).\n' +
|
|
47
|
+
' If you need the binary, either:\n' +
|
|
48
|
+
' - cargo build --release --bin red && export REDDB_BIN="$PWD/target/release/red"\n' +
|
|
49
|
+
' - or: curl -fsSL https://raw.githubusercontent.com/reddb-io/reddb/main/install.sh | bash\n' +
|
|
50
|
+
' Set REDDB_SKIP_POSTINSTALL=1 to silence this message.\n',
|
|
43
51
|
)
|
|
44
52
|
process.exit(0)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
main().catch((err) => {
|
|
56
|
+
process.stderr.write(formatFailure(err))
|
|
57
|
+
process.exit(0)
|
|
45
58
|
})
|
|
46
59
|
|
|
60
|
+
function formatFailure(err) {
|
|
61
|
+
const repo = process.env.REDDB_POSTINSTALL_REPO || DEFAULT_REPO
|
|
62
|
+
if (err && err.code === 'UNSUPPORTED_PLATFORM') {
|
|
63
|
+
return (
|
|
64
|
+
`reddb: no prebuilt red binary for ${process.platform}/${process.arch}.\n` +
|
|
65
|
+
` Options:\n` +
|
|
66
|
+
` - build from source: https://github.com/${repo}#build-from-source\n` +
|
|
67
|
+
` - or set REDDB_BIN=/path/to/red to point at one you compiled.\n`
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
if (err && err.code === 'ASSET_NOT_FOUND') {
|
|
71
|
+
return (
|
|
72
|
+
`reddb: release asset not found at ${err.url}\n` +
|
|
73
|
+
` Common cause: the GitHub Release for this SDK version has not\n` +
|
|
74
|
+
` been published yet (or your platform's binary was not produced\n` +
|
|
75
|
+
` for that release). To unblock without reinstalling:\n` +
|
|
76
|
+
` 1. Install the latest stable red via the official installer\n` +
|
|
77
|
+
` and point the SDK at it:\n` +
|
|
78
|
+
` curl -fsSL https://raw.githubusercontent.com/${repo}/main/install.sh | bash\n` +
|
|
79
|
+
` export REDDB_BIN="$(command -v red)"\n` +
|
|
80
|
+
` 2. Or pull a specific tag explicitly and re-run postinstall:\n` +
|
|
81
|
+
` REDDB_POSTINSTALL_VERSION=v1.0.5 npm rebuild @reddb-io/sdk\n` +
|
|
82
|
+
` 3. Or skip the download entirely and provide the binary yourself:\n` +
|
|
83
|
+
` REDDB_SKIP_POSTINSTALL=1 (re-install), then export REDDB_BIN=…\n` +
|
|
84
|
+
` Releases: https://github.com/${repo}/releases\n`
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
return (
|
|
88
|
+
`reddb: postinstall could not download the binary (${err && err.message}).\n` +
|
|
89
|
+
` The package itself still installs. To use the driver:\n` +
|
|
90
|
+
` - run the installer: curl -fsSL https://raw.githubusercontent.com/${repo}/main/install.sh | bash\n` +
|
|
91
|
+
` then: export REDDB_BIN="$(command -v red)"\n` +
|
|
92
|
+
` - or download manually from https://github.com/${repo}/releases\n` +
|
|
93
|
+
` - or set REDDB_BIN=/path/to/red.\n`
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
47
97
|
async function main() {
|
|
48
98
|
const repo = process.env.REDDB_POSTINSTALL_REPO || DEFAULT_REPO
|
|
49
99
|
const tag = process.env.REDDB_POSTINSTALL_VERSION
|
package/src/index.js
CHANGED
|
@@ -167,6 +167,17 @@ export async function connect(uri, options = {}) {
|
|
|
167
167
|
)
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
// Coerce a JS query parameter to a JSON-serializable shape the server
|
|
171
|
+
// understands. The tracer scope (#355) lifts vector params: `Float32Array`
|
|
172
|
+
// and `Float64Array` round-trip as plain JSON arrays of numbers, which
|
|
173
|
+
// the embedded stdio handler maps to `Value::Vector`.
|
|
174
|
+
function serializeParam(value) {
|
|
175
|
+
if (value instanceof Float32Array || value instanceof Float64Array) {
|
|
176
|
+
return Array.from(value)
|
|
177
|
+
}
|
|
178
|
+
return value
|
|
179
|
+
}
|
|
180
|
+
|
|
170
181
|
function embeddedArgs(parsed) {
|
|
171
182
|
if (parsed.path) return ['rpc', '--stdio', '--path', parsed.path]
|
|
172
183
|
return ['rpc', '--stdio']
|
|
@@ -344,9 +355,27 @@ export class RedDB {
|
|
|
344
355
|
this.vault = (collection = 'red.vault') => new VaultClient(client, collection)
|
|
345
356
|
}
|
|
346
357
|
|
|
347
|
-
/**
|
|
348
|
-
|
|
349
|
-
|
|
358
|
+
/**
|
|
359
|
+
* Execute a SQL query.
|
|
360
|
+
*
|
|
361
|
+
* Two signatures:
|
|
362
|
+
* - `query(sql)` — legacy single-arg form.
|
|
363
|
+
* - `query(sql, params)` — positional `$N` bind values. `params` is
|
|
364
|
+
* an array (JS scalars: number | string | null map to engine
|
|
365
|
+
* int/float / text / null). Indices in the SQL are 1-based
|
|
366
|
+
* (`$1`, `$2`, ...), `params` is 0-based JS-style.
|
|
367
|
+
*
|
|
368
|
+
* Returns `{ statement, affected, columns, rows }`.
|
|
369
|
+
*/
|
|
370
|
+
query(sql, params) {
|
|
371
|
+
if (params === undefined) {
|
|
372
|
+
return this.client.call('query', { sql })
|
|
373
|
+
}
|
|
374
|
+
if (!Array.isArray(params)) {
|
|
375
|
+
throw new TypeError('query: `params` must be an array')
|
|
376
|
+
}
|
|
377
|
+
const wireParams = params.map(serializeParam)
|
|
378
|
+
return this.client.call('query', { sql, params: wireParams })
|
|
350
379
|
}
|
|
351
380
|
|
|
352
381
|
/** Insert one row. Returns `{ affected, id? }`. */
|
package/src/redwire.js
CHANGED
|
@@ -45,6 +45,14 @@ export const MessageKind = Object.freeze({
|
|
|
45
45
|
BulkInsertBinary: 0x06,
|
|
46
46
|
QueryBinary: 0x07,
|
|
47
47
|
BulkInsertPrevalidated: 0x08,
|
|
48
|
+
QueryWithParams: 0x28,
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
export const Features = Object.freeze({ PARAMS: 0x0000_0001 })
|
|
52
|
+
|
|
53
|
+
export const ValueTag = Object.freeze({
|
|
54
|
+
Null: 0x00, Bool: 0x01, Int: 0x02, Float: 0x03, Text: 0x04,
|
|
55
|
+
Bytes: 0x05, Vector: 0x06, Json: 0x07, Timestamp: 0x08, Uuid: 0x09,
|
|
48
56
|
})
|
|
49
57
|
|
|
50
58
|
/**
|
|
@@ -201,8 +209,13 @@ export async function connectRedwire(opts) {
|
|
|
201
209
|
)
|
|
202
210
|
}
|
|
203
211
|
const session = jsonOf(final.payload) ?? {}
|
|
212
|
+
const features = numberOr(session.features, numberOr(ackParsed?.features, 0))
|
|
204
213
|
|
|
205
|
-
return new RedWireClient(socket, reader, session)
|
|
214
|
+
return new RedWireClient(socket, reader, session, features)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function numberOr(v, fallback) {
|
|
218
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : fallback
|
|
206
219
|
}
|
|
207
220
|
|
|
208
221
|
/**
|
|
@@ -211,16 +224,27 @@ export async function connectRedwire(opts) {
|
|
|
211
224
|
* transports so the surface above this is uniform.
|
|
212
225
|
*/
|
|
213
226
|
export class RedWireClient {
|
|
214
|
-
constructor(socket, reader, session) {
|
|
227
|
+
constructor(socket, reader, session, serverFeatures = 0) {
|
|
215
228
|
this.socket = socket
|
|
216
229
|
this.reader = reader
|
|
217
230
|
this.session = session
|
|
231
|
+
this.serverFeatures = serverFeatures >>> 0
|
|
218
232
|
this.nextCorr = 1n
|
|
219
233
|
this.closed = false
|
|
220
234
|
}
|
|
221
235
|
|
|
236
|
+
/** Raw advertised server feature bitmask. */
|
|
237
|
+
features() {
|
|
238
|
+
return this.serverFeatures
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** True when server advertised `FEATURE_PARAMS` (#357). */
|
|
242
|
+
supportsParams() {
|
|
243
|
+
return (this.serverFeatures & Features.PARAMS) === Features.PARAMS
|
|
244
|
+
}
|
|
245
|
+
|
|
222
246
|
async call(method, params = {}) {
|
|
223
|
-
if (method === 'query') return this.#query(params.sql ?? '')
|
|
247
|
+
if (method === 'query') return this.#query(params.sql ?? '', params.params)
|
|
224
248
|
if (method === 'insert') return this.#insert({ collection: params.collection, payload: params.payload })
|
|
225
249
|
if (method === 'bulk_insert') return this.#insert({ collection: params.collection, payloads: params.payloads })
|
|
226
250
|
if (method === 'bulk_insert_binary') {
|
|
@@ -308,10 +332,26 @@ export class RedWireClient {
|
|
|
308
332
|
)
|
|
309
333
|
}
|
|
310
334
|
|
|
311
|
-
async #query(sql) {
|
|
335
|
+
async #query(sql, params) {
|
|
312
336
|
const corr = this.#corr()
|
|
313
|
-
const
|
|
314
|
-
|
|
337
|
+
const hasParams = Array.isArray(params) && params.length > 0
|
|
338
|
+
let kind
|
|
339
|
+
let payload
|
|
340
|
+
if (hasParams) {
|
|
341
|
+
if (!this.supportsParams()) {
|
|
342
|
+
throw new RedDBError(
|
|
343
|
+
'PARAMS_UNSUPPORTED',
|
|
344
|
+
'server did not advertise FEATURE_PARAMS — upgrade the server '
|
|
345
|
+
+ 'to one that supports parameterized queries.',
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
kind = MessageKind.QueryWithParams
|
|
349
|
+
payload = encodeQueryWithParams(sql, params)
|
|
350
|
+
} else {
|
|
351
|
+
kind = MessageKind.Query
|
|
352
|
+
payload = new TextEncoder().encode(sql)
|
|
353
|
+
}
|
|
354
|
+
await writeFrame(this.socket, kind, corr, payload)
|
|
315
355
|
const resp = await this.reader.next()
|
|
316
356
|
if (resp.kind === MessageKind.Result) {
|
|
317
357
|
return jsonOf(resp.payload) ?? {}
|
|
@@ -714,6 +754,177 @@ function writeBinaryCell(buf, view, pos, cell, enc) {
|
|
|
714
754
|
}
|
|
715
755
|
}
|
|
716
756
|
|
|
757
|
+
// ---------------------------------------------------------------------------
|
|
758
|
+
// QueryWithParams payload codec — mirrors `reddb_wire::query_with_params`
|
|
759
|
+
// ---------------------------------------------------------------------------
|
|
760
|
+
|
|
761
|
+
const MAX_VALUE_PAYLOAD_LEN = MAX_FRAME_SIZE
|
|
762
|
+
const MAX_PARAM_COUNT = 65_536
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Encode the `QueryWithParams` payload body.
|
|
766
|
+
* Layout: `[u32 sql_len LE][utf-8 sql][u32 param_count LE][N encoded values]`
|
|
767
|
+
*/
|
|
768
|
+
export function encodeQueryWithParams(sql, params) {
|
|
769
|
+
if (typeof sql !== 'string') throw new TypeError('encodeQueryWithParams: sql must be a string')
|
|
770
|
+
if (!Array.isArray(params)) throw new TypeError('encodeQueryWithParams: params must be an array')
|
|
771
|
+
if (params.length > MAX_PARAM_COUNT) {
|
|
772
|
+
throw new RedDBError('PARAM_COUNT_OVER_LIMIT', `param_count ${params.length} > ${MAX_PARAM_COUNT}`)
|
|
773
|
+
}
|
|
774
|
+
const sqlBytes = new TextEncoder().encode(sql)
|
|
775
|
+
if (sqlBytes.length > MAX_VALUE_PAYLOAD_LEN) {
|
|
776
|
+
throw new RedDBError('PAYLOAD_TOO_LARGE', `sql_len ${sqlBytes.length} > ${MAX_VALUE_PAYLOAD_LEN}`)
|
|
777
|
+
}
|
|
778
|
+
const valueBlobs = params.map(encodeValue)
|
|
779
|
+
let total = 4 + sqlBytes.length + 4
|
|
780
|
+
for (const vb of valueBlobs) total += vb.length
|
|
781
|
+
const buf = new Uint8Array(total)
|
|
782
|
+
const view = new DataView(buf.buffer)
|
|
783
|
+
let pos = 0
|
|
784
|
+
view.setUint32(pos, sqlBytes.length, true); pos += 4
|
|
785
|
+
buf.set(sqlBytes, pos); pos += sqlBytes.length
|
|
786
|
+
view.setUint32(pos, valueBlobs.length, true); pos += 4
|
|
787
|
+
for (const vb of valueBlobs) { buf.set(vb, pos); pos += vb.length }
|
|
788
|
+
return buf
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Encode a single wire `Value`. Mirrors `reddb_wire::value::encode`.
|
|
793
|
+
*
|
|
794
|
+
* Accepts native JS values + the JSON envelopes produced by
|
|
795
|
+
* `serializeParam` so the SDK can pass through a single shape:
|
|
796
|
+
* - `null` / `undefined` → Null
|
|
797
|
+
* - `boolean` → Bool
|
|
798
|
+
* - `bigint` → Int (i64)
|
|
799
|
+
* - `number` integer (safe range) → Int; otherwise → Float
|
|
800
|
+
* - `string` → Text
|
|
801
|
+
* - `Uint8Array` / `Buffer` → Bytes
|
|
802
|
+
* - `Float32Array` / `Array<number>` → Vector (f32)
|
|
803
|
+
* - `{ $bytes: <base64> }` → Bytes
|
|
804
|
+
* - `{ $ts: <unix-seconds> }` → Timestamp
|
|
805
|
+
* - `{ $uuid: <hyphenated> }` → Uuid
|
|
806
|
+
* - other plain object/array → Json (canonical bytes)
|
|
807
|
+
*/
|
|
808
|
+
export function encodeValue(v) {
|
|
809
|
+
if (v === null || v === undefined) return Uint8Array.of(ValueTag.Null)
|
|
810
|
+
if (typeof v === 'boolean') return Uint8Array.of(ValueTag.Bool, v ? 1 : 0)
|
|
811
|
+
if (typeof v === 'bigint') {
|
|
812
|
+
const out = new Uint8Array(1 + 8)
|
|
813
|
+
out[0] = ValueTag.Int
|
|
814
|
+
new DataView(out.buffer).setBigInt64(1, v, true)
|
|
815
|
+
return out
|
|
816
|
+
}
|
|
817
|
+
if (typeof v === 'number') {
|
|
818
|
+
if (Number.isInteger(v) && v >= -(2 ** 53) && v <= 2 ** 53) {
|
|
819
|
+
const out = new Uint8Array(1 + 8)
|
|
820
|
+
out[0] = ValueTag.Int
|
|
821
|
+
new DataView(out.buffer).setBigInt64(1, BigInt(v), true)
|
|
822
|
+
return out
|
|
823
|
+
}
|
|
824
|
+
const out = new Uint8Array(1 + 8)
|
|
825
|
+
out[0] = ValueTag.Float
|
|
826
|
+
new DataView(out.buffer).setFloat64(1, v, true)
|
|
827
|
+
return out
|
|
828
|
+
}
|
|
829
|
+
if (typeof v === 'string') return encodeLenPrefixed(ValueTag.Text, new TextEncoder().encode(v))
|
|
830
|
+
if (v instanceof Uint8Array) return encodeLenPrefixed(ValueTag.Bytes, v)
|
|
831
|
+
if (typeof Buffer !== 'undefined' && v instanceof Buffer) {
|
|
832
|
+
return encodeLenPrefixed(ValueTag.Bytes, new Uint8Array(v.buffer, v.byteOffset, v.byteLength))
|
|
833
|
+
}
|
|
834
|
+
if (v instanceof Float32Array) return encodeVector(v)
|
|
835
|
+
if (v instanceof Float64Array) return encodeVector(Float32Array.from(v))
|
|
836
|
+
if (Array.isArray(v) && v.every((x) => typeof x === 'number')) {
|
|
837
|
+
return encodeVector(Float32Array.from(v))
|
|
838
|
+
}
|
|
839
|
+
if (typeof v === 'object') {
|
|
840
|
+
const keys = Object.keys(v)
|
|
841
|
+
if (keys.length === 1) {
|
|
842
|
+
const k = keys[0]
|
|
843
|
+
if (k === '$bytes' && typeof v.$bytes === 'string') {
|
|
844
|
+
return encodeLenPrefixed(ValueTag.Bytes, base64ToBytes(v.$bytes))
|
|
845
|
+
}
|
|
846
|
+
if (k === '$ts' && typeof v.$ts === 'number' && Number.isFinite(v.$ts)) {
|
|
847
|
+
const out = new Uint8Array(1 + 8)
|
|
848
|
+
out[0] = ValueTag.Timestamp
|
|
849
|
+
new DataView(out.buffer).setBigInt64(1, BigInt(Math.trunc(v.$ts)), true)
|
|
850
|
+
return out
|
|
851
|
+
}
|
|
852
|
+
if (k === '$uuid' && typeof v.$uuid === 'string') {
|
|
853
|
+
const bytes = parseUuidHyphenated(v.$uuid)
|
|
854
|
+
const out = new Uint8Array(1 + 16)
|
|
855
|
+
out[0] = ValueTag.Uuid
|
|
856
|
+
out.set(bytes, 1)
|
|
857
|
+
return out
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
return encodeLenPrefixed(ValueTag.Json, new TextEncoder().encode(canonicalJson(v)))
|
|
861
|
+
}
|
|
862
|
+
throw new RedDBError('UNSUPPORTED_PARAM', `cannot encode param of type ${typeof v}`)
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function encodeLenPrefixed(tag, bytes) {
|
|
866
|
+
if (bytes.length > MAX_VALUE_PAYLOAD_LEN) {
|
|
867
|
+
throw new RedDBError('PAYLOAD_TOO_LARGE', `value len ${bytes.length} > ${MAX_VALUE_PAYLOAD_LEN}`)
|
|
868
|
+
}
|
|
869
|
+
const out = new Uint8Array(1 + 4 + bytes.length)
|
|
870
|
+
out[0] = tag
|
|
871
|
+
new DataView(out.buffer).setUint32(1, bytes.length, true)
|
|
872
|
+
out.set(bytes, 5)
|
|
873
|
+
return out
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function encodeVector(f32) {
|
|
877
|
+
if (f32.length * 4 > MAX_VALUE_PAYLOAD_LEN) {
|
|
878
|
+
throw new RedDBError('PAYLOAD_TOO_LARGE', `vector bytes ${f32.length * 4} > ${MAX_VALUE_PAYLOAD_LEN}`)
|
|
879
|
+
}
|
|
880
|
+
const out = new Uint8Array(1 + 4 + f32.length * 4)
|
|
881
|
+
out[0] = ValueTag.Vector
|
|
882
|
+
const view = new DataView(out.buffer)
|
|
883
|
+
view.setUint32(1, f32.length, true)
|
|
884
|
+
for (let i = 0; i < f32.length; i++) {
|
|
885
|
+
view.setFloat32(5 + i * 4, f32[i], true)
|
|
886
|
+
}
|
|
887
|
+
return out
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function base64ToBytes(s) {
|
|
891
|
+
if (typeof Buffer !== 'undefined') {
|
|
892
|
+
const b = Buffer.from(s, 'base64')
|
|
893
|
+
return new Uint8Array(b.buffer, b.byteOffset, b.byteLength)
|
|
894
|
+
}
|
|
895
|
+
// eslint-disable-next-line no-undef
|
|
896
|
+
const bin = atob(s)
|
|
897
|
+
const out = new Uint8Array(bin.length)
|
|
898
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)
|
|
899
|
+
return out
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function parseUuidHyphenated(s) {
|
|
903
|
+
const hex = s.replace(/-/g, '')
|
|
904
|
+
if (hex.length !== 32 || /[^0-9a-fA-F]/.test(hex)) {
|
|
905
|
+
throw new RedDBError('UUID_INVALID', `bad uuid: ${s}`)
|
|
906
|
+
}
|
|
907
|
+
const out = new Uint8Array(16)
|
|
908
|
+
for (let i = 0; i < 16; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16)
|
|
909
|
+
return out
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/** Stable JSON serialization with sorted keys — matches the server's
|
|
913
|
+
* canonical `crate::json` output so round-tripped Json values compare
|
|
914
|
+
* byte-equal. */
|
|
915
|
+
function canonicalJson(v) {
|
|
916
|
+
if (v === null) return 'null'
|
|
917
|
+
if (typeof v === 'number') return Number.isFinite(v) ? String(v) : 'null'
|
|
918
|
+
if (typeof v === 'string') return JSON.stringify(v)
|
|
919
|
+
if (typeof v === 'boolean') return v ? 'true' : 'false'
|
|
920
|
+
if (Array.isArray(v)) return '[' + v.map(canonicalJson).join(',') + ']'
|
|
921
|
+
if (typeof v === 'object') {
|
|
922
|
+
const keys = Object.keys(v).sort()
|
|
923
|
+
return '{' + keys.map((k) => JSON.stringify(k) + ':' + canonicalJson(v[k])).join(',') + '}'
|
|
924
|
+
}
|
|
925
|
+
return 'null'
|
|
926
|
+
}
|
|
927
|
+
|
|
717
928
|
function jsonReason(bytes) {
|
|
718
929
|
const v = jsonOf(bytes)
|
|
719
930
|
if (v && typeof v === 'object' && typeof v.reason === 'string') {
|