recker 1.0.101 → 1.0.102
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/dist/clients/index.d.ts +1 -0
- package/dist/clients/index.js +1 -0
- package/dist/clients/reddb.d.ts +211 -0
- package/dist/clients/reddb.js +827 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/protocols/index.d.ts +1 -0
- package/dist/protocols/index.js +1 -0
- package/dist/protocols/reddb.d.ts +1 -0
- package/dist/protocols/reddb.js +1 -0
- package/dist/recker.d.ts +2 -0
- package/dist/recker.js +2 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { RedDbClient, createRedDbClient, reddb, type RedDbTransportMode, type RedDbResolvedTransport, type RedDbQueryStats, type RedDbQueryData, type RedDbBatchQueryData, type RedDbScanEntity, type RedDbScanData, type RedDbBulkInsertData, type RedDbCapabilities, type RedDbOperationMetrics, type RedDbOperationEnvelope, type RedDbQueryOptions, type RedDbScanRequest, type RedDbBulkInsertRowsRequest, type RedDbBinaryValue, type RedDbBulkInsertBinaryRequest, type RedDbEnsureCollectionOptions, type RedDbEnsureIndexOptions, type RedDbWarmupIndexOptions, type RedDbStatsOptions, type RedDbWireTlsOptions, type RedDbClientOptions } from './reddb.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { RedDbClient, createRedDbClient, reddb } from './reddb.js';
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { type Client as ReckerClient, type ExtendedClientOptions } from '../core/client.js';
|
|
2
|
+
export type RedDbTransportMode = 'auto' | 'http' | 'grpc' | 'wire';
|
|
3
|
+
export type RedDbResolvedTransport = 'http' | 'wire';
|
|
4
|
+
export interface RedDbQueryStats {
|
|
5
|
+
nodes_scanned?: number;
|
|
6
|
+
edges_scanned?: number;
|
|
7
|
+
rows_scanned?: number;
|
|
8
|
+
exec_time_us?: number;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
export interface RedDbQueryData {
|
|
12
|
+
ok?: boolean;
|
|
13
|
+
query?: string;
|
|
14
|
+
mode?: string;
|
|
15
|
+
capability?: string;
|
|
16
|
+
statement?: string;
|
|
17
|
+
engine?: string;
|
|
18
|
+
record_count?: number;
|
|
19
|
+
affected_rows?: number;
|
|
20
|
+
statement_type?: string;
|
|
21
|
+
result?: {
|
|
22
|
+
columns?: string[];
|
|
23
|
+
records?: Array<Record<string, unknown>>;
|
|
24
|
+
stats?: RedDbQueryStats;
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
};
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
export interface RedDbBatchQueryData {
|
|
30
|
+
results: RedDbQueryData[];
|
|
31
|
+
}
|
|
32
|
+
export interface RedDbScanEntity {
|
|
33
|
+
id: number;
|
|
34
|
+
kind: string;
|
|
35
|
+
collection: string;
|
|
36
|
+
json: string;
|
|
37
|
+
}
|
|
38
|
+
export interface RedDbScanData {
|
|
39
|
+
collection: string;
|
|
40
|
+
total: number;
|
|
41
|
+
next_offset?: number | null;
|
|
42
|
+
items: RedDbScanEntity[];
|
|
43
|
+
[key: string]: unknown;
|
|
44
|
+
}
|
|
45
|
+
export interface RedDbBulkInsertData {
|
|
46
|
+
ok?: boolean;
|
|
47
|
+
count?: number;
|
|
48
|
+
first_id?: number;
|
|
49
|
+
[key: string]: unknown;
|
|
50
|
+
}
|
|
51
|
+
export interface RedDbCapabilities {
|
|
52
|
+
requestedTransport: RedDbTransportMode;
|
|
53
|
+
allowTransportFallback: boolean;
|
|
54
|
+
availableTransports: {
|
|
55
|
+
http: boolean;
|
|
56
|
+
wire: boolean;
|
|
57
|
+
grpc: boolean;
|
|
58
|
+
};
|
|
59
|
+
native: {
|
|
60
|
+
query: boolean;
|
|
61
|
+
batchQuery: boolean;
|
|
62
|
+
scan: boolean;
|
|
63
|
+
bulkInsertRows: boolean;
|
|
64
|
+
bulkInsertBinary: boolean;
|
|
65
|
+
stats: boolean;
|
|
66
|
+
ensureCollection: boolean;
|
|
67
|
+
ensureIndex: boolean;
|
|
68
|
+
warmupIndex: boolean;
|
|
69
|
+
};
|
|
70
|
+
emulated: {
|
|
71
|
+
batchQuery: boolean;
|
|
72
|
+
bulkInsertBinary: boolean;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export interface RedDbOperationMetrics {
|
|
76
|
+
operation: string;
|
|
77
|
+
requestedTransport: RedDbTransportMode;
|
|
78
|
+
transport: RedDbResolvedTransport;
|
|
79
|
+
degradedFromRequestedTransport: boolean;
|
|
80
|
+
emulated: boolean;
|
|
81
|
+
startedAt: number;
|
|
82
|
+
durationMs: number;
|
|
83
|
+
recordCount?: number;
|
|
84
|
+
affectedRows?: number;
|
|
85
|
+
rowsScanned?: number;
|
|
86
|
+
execTimeUs?: number;
|
|
87
|
+
}
|
|
88
|
+
export interface RedDbOperationEnvelope<T> {
|
|
89
|
+
data: T;
|
|
90
|
+
transport: RedDbResolvedTransport;
|
|
91
|
+
requestedTransport: RedDbTransportMode;
|
|
92
|
+
degradedFromRequestedTransport: boolean;
|
|
93
|
+
emulated: boolean;
|
|
94
|
+
metrics: RedDbOperationMetrics;
|
|
95
|
+
}
|
|
96
|
+
export interface RedDbQueryOptions {
|
|
97
|
+
entityTypes?: string[];
|
|
98
|
+
capabilities?: string[];
|
|
99
|
+
transport?: RedDbTransportMode;
|
|
100
|
+
signal?: AbortSignal;
|
|
101
|
+
timeout?: number;
|
|
102
|
+
}
|
|
103
|
+
export interface RedDbScanRequest {
|
|
104
|
+
collection: string;
|
|
105
|
+
offset?: number;
|
|
106
|
+
limit?: number;
|
|
107
|
+
transport?: RedDbTransportMode;
|
|
108
|
+
signal?: AbortSignal;
|
|
109
|
+
timeout?: number;
|
|
110
|
+
}
|
|
111
|
+
export interface RedDbBulkInsertRowsRequest {
|
|
112
|
+
collection: string;
|
|
113
|
+
items: Array<{
|
|
114
|
+
fields: Record<string, unknown>;
|
|
115
|
+
}>;
|
|
116
|
+
transport?: RedDbTransportMode;
|
|
117
|
+
signal?: AbortSignal;
|
|
118
|
+
timeout?: number;
|
|
119
|
+
}
|
|
120
|
+
export type RedDbBinaryValue = string | number | boolean | Buffer | Uint8Array | null;
|
|
121
|
+
export interface RedDbBulkInsertBinaryRequest {
|
|
122
|
+
collection: string;
|
|
123
|
+
fieldNames: string[];
|
|
124
|
+
rows: RedDbBinaryValue[][];
|
|
125
|
+
transport?: RedDbTransportMode;
|
|
126
|
+
signal?: AbortSignal;
|
|
127
|
+
timeout?: number;
|
|
128
|
+
}
|
|
129
|
+
export interface RedDbEnsureCollectionOptions {
|
|
130
|
+
name: string;
|
|
131
|
+
ttl?: string | number;
|
|
132
|
+
ttlMs?: number;
|
|
133
|
+
transport?: RedDbTransportMode;
|
|
134
|
+
signal?: AbortSignal;
|
|
135
|
+
timeout?: number;
|
|
136
|
+
}
|
|
137
|
+
export interface RedDbEnsureIndexOptions {
|
|
138
|
+
name: string;
|
|
139
|
+
collection: string;
|
|
140
|
+
columns: string[];
|
|
141
|
+
unique?: boolean;
|
|
142
|
+
method?: 'BTREE' | 'HASH' | 'BITMAP' | 'RTREE';
|
|
143
|
+
transport?: RedDbTransportMode;
|
|
144
|
+
signal?: AbortSignal;
|
|
145
|
+
timeout?: number;
|
|
146
|
+
}
|
|
147
|
+
export interface RedDbWarmupIndexOptions {
|
|
148
|
+
name: string;
|
|
149
|
+
transport?: RedDbTransportMode;
|
|
150
|
+
signal?: AbortSignal;
|
|
151
|
+
timeout?: number;
|
|
152
|
+
}
|
|
153
|
+
export interface RedDbStatsOptions {
|
|
154
|
+
transport?: RedDbTransportMode;
|
|
155
|
+
signal?: AbortSignal;
|
|
156
|
+
timeout?: number;
|
|
157
|
+
}
|
|
158
|
+
export interface RedDbWireTlsOptions {
|
|
159
|
+
enabled?: boolean;
|
|
160
|
+
rejectUnauthorized?: boolean;
|
|
161
|
+
ca?: string | Buffer;
|
|
162
|
+
servername?: string;
|
|
163
|
+
}
|
|
164
|
+
export interface RedDbClientOptions {
|
|
165
|
+
baseUrl?: string;
|
|
166
|
+
client?: ReckerClient;
|
|
167
|
+
transport?: RedDbTransportMode;
|
|
168
|
+
allowTransportFallback?: boolean;
|
|
169
|
+
authToken?: string;
|
|
170
|
+
writeToken?: string;
|
|
171
|
+
headers?: Record<string, string>;
|
|
172
|
+
timeout?: number;
|
|
173
|
+
http2?: boolean;
|
|
174
|
+
httpClientOptions?: Omit<ExtendedClientOptions, 'baseUrl' | 'headers' | 'timeout' | 'http2'>;
|
|
175
|
+
wireAddress?: string;
|
|
176
|
+
wireTls?: boolean | RedDbWireTlsOptions;
|
|
177
|
+
batchConcurrency?: number;
|
|
178
|
+
}
|
|
179
|
+
export declare class RedDbClient {
|
|
180
|
+
private readonly baseUrl?;
|
|
181
|
+
private readonly httpClient;
|
|
182
|
+
private readonly defaultTransport;
|
|
183
|
+
private readonly allowTransportFallback;
|
|
184
|
+
private readonly timeout;
|
|
185
|
+
private readonly batchConcurrency;
|
|
186
|
+
private readonly wireClient;
|
|
187
|
+
constructor(options?: RedDbClientOptions);
|
|
188
|
+
getCapabilities(): RedDbCapabilities;
|
|
189
|
+
query(query: string, options?: RedDbQueryOptions): Promise<RedDbOperationEnvelope<RedDbQueryData>>;
|
|
190
|
+
explainQuery(query: string, options?: RedDbQueryOptions): Promise<RedDbOperationEnvelope<Record<string, unknown>>>;
|
|
191
|
+
mutateQuery(query: string, options?: RedDbQueryOptions): Promise<RedDbOperationEnvelope<RedDbQueryData>>;
|
|
192
|
+
batchQuery(queries: string[], options?: RedDbQueryOptions): Promise<RedDbOperationEnvelope<RedDbBatchQueryData>>;
|
|
193
|
+
scan(params: RedDbScanRequest): Promise<RedDbOperationEnvelope<RedDbScanData>>;
|
|
194
|
+
bulkInsertRows(params: RedDbBulkInsertRowsRequest): Promise<RedDbOperationEnvelope<RedDbBulkInsertData>>;
|
|
195
|
+
bulkInsertBinary(params: RedDbBulkInsertBinaryRequest): Promise<RedDbOperationEnvelope<RedDbBulkInsertData>>;
|
|
196
|
+
stats(options?: RedDbStatsOptions): Promise<RedDbOperationEnvelope<Record<string, unknown>>>;
|
|
197
|
+
ensureCollection(options: RedDbEnsureCollectionOptions): Promise<RedDbOperationEnvelope<Record<string, unknown>>>;
|
|
198
|
+
ensureIndex(options: RedDbEnsureIndexOptions): Promise<RedDbOperationEnvelope<RedDbQueryData>>;
|
|
199
|
+
warmupIndex(options: RedDbWarmupIndexOptions): Promise<RedDbOperationEnvelope<Record<string, unknown>>>;
|
|
200
|
+
close(): Promise<void>;
|
|
201
|
+
private binaryRowToFields;
|
|
202
|
+
private runHttpQuery;
|
|
203
|
+
private runHttpGet;
|
|
204
|
+
private parseHttpResponse;
|
|
205
|
+
private runWireQuery;
|
|
206
|
+
private runWireBulkInsert;
|
|
207
|
+
private resolveTransport;
|
|
208
|
+
private buildEnvelope;
|
|
209
|
+
}
|
|
210
|
+
export declare function createRedDbClient(options?: RedDbClientOptions): RedDbClient;
|
|
211
|
+
export declare function reddb(options?: RedDbClientOptions): RedDbClient;
|
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks';
|
|
2
|
+
import { connect as connectTcp } from 'node:net';
|
|
3
|
+
import { connect as connectTls } from 'node:tls';
|
|
4
|
+
import { createClient } from '../core/client.js';
|
|
5
|
+
import { AuthenticationError, ConfigurationError, ConnectionError, NotFoundError, ParseError, ProtocolError, UnsupportedError, ValidationError, } from '../core/errors.js';
|
|
6
|
+
const WIRE_MSG_QUERY = 0x01;
|
|
7
|
+
const WIRE_MSG_RESULT = 0x02;
|
|
8
|
+
const WIRE_MSG_ERROR = 0x03;
|
|
9
|
+
const WIRE_MSG_BULK_INSERT = 0x04;
|
|
10
|
+
const WIRE_MSG_BULK_OK = 0x05;
|
|
11
|
+
function isBufferLike(value) {
|
|
12
|
+
return Buffer.isBuffer(value) || value instanceof Uint8Array;
|
|
13
|
+
}
|
|
14
|
+
function parseWireAddress(address) {
|
|
15
|
+
if (!address || !address.trim()) {
|
|
16
|
+
throw new ConfigurationError('wireAddress must be a non-empty host:port string', {
|
|
17
|
+
configKey: 'wireAddress',
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
const trimmed = address.trim();
|
|
21
|
+
if (trimmed.includes('://')) {
|
|
22
|
+
const url = new URL(trimmed);
|
|
23
|
+
const port = Number(url.port);
|
|
24
|
+
if (!url.hostname || !Number.isInteger(port) || port <= 0) {
|
|
25
|
+
throw new ConfigurationError(`Invalid wireAddress: ${address}`, {
|
|
26
|
+
configKey: 'wireAddress',
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return { host: url.hostname, port };
|
|
30
|
+
}
|
|
31
|
+
const ipv6 = trimmed.match(/^\[([^\]]+)\]:(\d+)$/);
|
|
32
|
+
if (ipv6) {
|
|
33
|
+
return {
|
|
34
|
+
host: ipv6[1],
|
|
35
|
+
port: Number(ipv6[2]),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const separator = trimmed.lastIndexOf(':');
|
|
39
|
+
if (separator <= 0 || separator === trimmed.length - 1) {
|
|
40
|
+
throw new ConfigurationError(`Invalid wireAddress: ${address}`, {
|
|
41
|
+
configKey: 'wireAddress',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const host = trimmed.slice(0, separator);
|
|
45
|
+
const port = Number(trimmed.slice(separator + 1));
|
|
46
|
+
if (!host || !Number.isInteger(port) || port <= 0) {
|
|
47
|
+
throw new ConfigurationError(`Invalid wireAddress: ${address}`, {
|
|
48
|
+
configKey: 'wireAddress',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return { host, port };
|
|
52
|
+
}
|
|
53
|
+
function escapeSqlIdentifier(value) {
|
|
54
|
+
if (!value || !value.trim()) {
|
|
55
|
+
throw new ValidationError('SQL identifier cannot be empty', {
|
|
56
|
+
field: 'identifier',
|
|
57
|
+
value,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
61
|
+
}
|
|
62
|
+
function buildQueryStats(data) {
|
|
63
|
+
if (!data || typeof data !== 'object') {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
const queryData = data;
|
|
67
|
+
const stats = queryData.result?.stats;
|
|
68
|
+
return {
|
|
69
|
+
recordCount: typeof queryData.record_count === 'number' ? queryData.record_count : undefined,
|
|
70
|
+
affectedRows: typeof queryData.affected_rows === 'number' ? queryData.affected_rows : undefined,
|
|
71
|
+
rowsScanned: typeof stats?.rows_scanned === 'number' ? stats.rows_scanned : undefined,
|
|
72
|
+
execTimeUs: typeof stats?.exec_time_us === 'number' ? stats.exec_time_us : undefined,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async function mapWithConcurrency(items, concurrency, worker) {
|
|
76
|
+
if (items.length === 0) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
const safeConcurrency = Math.max(1, Math.min(concurrency || 1, items.length));
|
|
80
|
+
const results = new Array(items.length);
|
|
81
|
+
let nextIndex = 0;
|
|
82
|
+
async function run() {
|
|
83
|
+
while (true) {
|
|
84
|
+
const current = nextIndex++;
|
|
85
|
+
if (current >= items.length) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
results[current] = await worker(items[current], current);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
await Promise.all(Array.from({ length: safeConcurrency }, () => run()));
|
|
92
|
+
return results;
|
|
93
|
+
}
|
|
94
|
+
class RedDbWireSocket {
|
|
95
|
+
endpoint;
|
|
96
|
+
tlsOptions;
|
|
97
|
+
timeout;
|
|
98
|
+
socket = null;
|
|
99
|
+
connectPromise = null;
|
|
100
|
+
pending = [];
|
|
101
|
+
buffer = Buffer.alloc(0);
|
|
102
|
+
constructor(address, tlsOptions, timeout) {
|
|
103
|
+
this.endpoint = parseWireAddress(address);
|
|
104
|
+
this.timeout = timeout;
|
|
105
|
+
this.tlsOptions = typeof tlsOptions === 'boolean'
|
|
106
|
+
? { enabled: tlsOptions }
|
|
107
|
+
: { enabled: tlsOptions?.enabled ?? false, ...tlsOptions };
|
|
108
|
+
}
|
|
109
|
+
get available() {
|
|
110
|
+
return this.socket !== null && !this.socket.destroyed;
|
|
111
|
+
}
|
|
112
|
+
async query(sql, timeout) {
|
|
113
|
+
const response = await this.send(WIRE_MSG_QUERY, Buffer.from(sql, 'utf8'), timeout);
|
|
114
|
+
if (response.type !== WIRE_MSG_RESULT) {
|
|
115
|
+
throw new ProtocolError(`Unexpected wire response type: ${response.type}`, {
|
|
116
|
+
protocol: 'reddb',
|
|
117
|
+
code: response.type,
|
|
118
|
+
phase: 'query',
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
return JSON.parse(response.payload.toString('utf8'));
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
throw new ParseError(`Failed to parse RedDB wire query payload: ${error instanceof Error ? error.message : String(error)}`, { format: 'json' });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async bulkInsert(collection, payloads, timeout) {
|
|
129
|
+
const collectionBuffer = Buffer.from(collection, 'utf8');
|
|
130
|
+
const header = Buffer.alloc(2 + collectionBuffer.length + 4);
|
|
131
|
+
header.writeUInt16LE(collectionBuffer.length, 0);
|
|
132
|
+
collectionBuffer.copy(header, 2);
|
|
133
|
+
header.writeUInt32LE(payloads.length, 2 + collectionBuffer.length);
|
|
134
|
+
const parts = [header];
|
|
135
|
+
for (const payload of payloads) {
|
|
136
|
+
const jsonBuffer = Buffer.from(payload, 'utf8');
|
|
137
|
+
const lengthBuffer = Buffer.alloc(4);
|
|
138
|
+
lengthBuffer.writeUInt32LE(jsonBuffer.length, 0);
|
|
139
|
+
parts.push(lengthBuffer, jsonBuffer);
|
|
140
|
+
}
|
|
141
|
+
const response = await this.send(WIRE_MSG_BULK_INSERT, Buffer.concat(parts), timeout);
|
|
142
|
+
if (response.type !== WIRE_MSG_BULK_OK) {
|
|
143
|
+
throw new ProtocolError(`Unexpected wire bulk response type: ${response.type}`, {
|
|
144
|
+
protocol: 'reddb',
|
|
145
|
+
code: response.type,
|
|
146
|
+
phase: 'bulkInsert',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (response.payload.length >= 8) {
|
|
150
|
+
return Number(response.payload.readBigUInt64LE(0));
|
|
151
|
+
}
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
async close() {
|
|
155
|
+
this.destroy();
|
|
156
|
+
}
|
|
157
|
+
async connect() {
|
|
158
|
+
if (this.available) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (this.connectPromise) {
|
|
162
|
+
return this.connectPromise;
|
|
163
|
+
}
|
|
164
|
+
const connectPromise = new Promise((resolve, reject) => {
|
|
165
|
+
const onError = (error) => {
|
|
166
|
+
cleanup();
|
|
167
|
+
this.destroy(error);
|
|
168
|
+
reject(new ConnectionError(error.message, {
|
|
169
|
+
host: this.endpoint.host,
|
|
170
|
+
port: this.endpoint.port,
|
|
171
|
+
code: error.code,
|
|
172
|
+
}));
|
|
173
|
+
};
|
|
174
|
+
const onConnect = () => {
|
|
175
|
+
cleanup();
|
|
176
|
+
if (!socket.destroyed) {
|
|
177
|
+
socket.setNoDelay(true);
|
|
178
|
+
}
|
|
179
|
+
this.socket = socket;
|
|
180
|
+
socket.on('data', (chunk) => this.onData(chunk));
|
|
181
|
+
socket.on('error', (error) => this.destroy(error instanceof Error ? error : new Error(String(error))));
|
|
182
|
+
socket.on('close', () => this.destroy());
|
|
183
|
+
resolve();
|
|
184
|
+
};
|
|
185
|
+
const cleanup = () => {
|
|
186
|
+
socket.removeListener('error', onError);
|
|
187
|
+
socket.removeListener('connect', onConnect);
|
|
188
|
+
};
|
|
189
|
+
const socket = this.tlsOptions.enabled
|
|
190
|
+
? connectTls({
|
|
191
|
+
host: this.endpoint.host,
|
|
192
|
+
port: this.endpoint.port,
|
|
193
|
+
rejectUnauthorized: this.tlsOptions.rejectUnauthorized ?? true,
|
|
194
|
+
ca: this.tlsOptions.ca,
|
|
195
|
+
servername: this.tlsOptions.servername || this.endpoint.host,
|
|
196
|
+
}, onConnect)
|
|
197
|
+
: connectTcp({
|
|
198
|
+
host: this.endpoint.host,
|
|
199
|
+
port: this.endpoint.port,
|
|
200
|
+
}, onConnect);
|
|
201
|
+
socket.setTimeout(this.timeout, () => {
|
|
202
|
+
onError(new Error(`RedDB wire connection to ${this.endpoint.host}:${this.endpoint.port} timed out`));
|
|
203
|
+
});
|
|
204
|
+
socket.once('error', onError);
|
|
205
|
+
}).finally(() => {
|
|
206
|
+
this.connectPromise = null;
|
|
207
|
+
});
|
|
208
|
+
this.connectPromise = connectPromise;
|
|
209
|
+
return connectPromise;
|
|
210
|
+
}
|
|
211
|
+
async send(messageType, payload, timeout) {
|
|
212
|
+
await this.connect();
|
|
213
|
+
if (!this.socket || this.socket.destroyed) {
|
|
214
|
+
throw new ConnectionError('RedDB wire socket is not connected', {
|
|
215
|
+
host: this.endpoint.host,
|
|
216
|
+
port: this.endpoint.port,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return new Promise((resolve, reject) => {
|
|
220
|
+
const frameLength = 1 + payload.length;
|
|
221
|
+
const header = Buffer.alloc(5);
|
|
222
|
+
header.writeUInt32LE(frameLength, 0);
|
|
223
|
+
header[4] = messageType;
|
|
224
|
+
const pending = {
|
|
225
|
+
resolve: (value) => {
|
|
226
|
+
if (pending.timer) {
|
|
227
|
+
clearTimeout(pending.timer);
|
|
228
|
+
}
|
|
229
|
+
resolve(value);
|
|
230
|
+
},
|
|
231
|
+
reject: (error) => {
|
|
232
|
+
if (pending.timer) {
|
|
233
|
+
clearTimeout(pending.timer);
|
|
234
|
+
}
|
|
235
|
+
reject(error);
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
const timeoutMs = timeout ?? this.timeout;
|
|
239
|
+
if (timeoutMs > 0 && Number.isFinite(timeoutMs)) {
|
|
240
|
+
pending.timer = setTimeout(() => {
|
|
241
|
+
const error = new ProtocolError(`RedDB wire request timed out after ${timeoutMs}ms`, {
|
|
242
|
+
protocol: 'reddb',
|
|
243
|
+
phase: 'wire-timeout',
|
|
244
|
+
retriable: true,
|
|
245
|
+
});
|
|
246
|
+
this.destroy(error);
|
|
247
|
+
}, timeoutMs);
|
|
248
|
+
}
|
|
249
|
+
this.pending.push(pending);
|
|
250
|
+
this.socket.write(header);
|
|
251
|
+
this.socket.write(payload);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
onData(chunk) {
|
|
255
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
256
|
+
this.tryResolvePending();
|
|
257
|
+
}
|
|
258
|
+
tryResolvePending() {
|
|
259
|
+
while (this.buffer.length >= 5 && this.pending.length > 0) {
|
|
260
|
+
const totalLength = this.buffer.readUInt32LE(0);
|
|
261
|
+
const frameSize = 4 + totalLength;
|
|
262
|
+
if (this.buffer.length < frameSize) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const type = this.buffer[4];
|
|
266
|
+
const payload = this.buffer.slice(5, frameSize);
|
|
267
|
+
this.buffer = this.buffer.slice(frameSize);
|
|
268
|
+
const current = this.pending.shift();
|
|
269
|
+
if (!current) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (type === WIRE_MSG_ERROR) {
|
|
273
|
+
current.reject(new ProtocolError(payload.toString('utf8'), {
|
|
274
|
+
protocol: 'reddb',
|
|
275
|
+
code: type,
|
|
276
|
+
phase: 'wire-response',
|
|
277
|
+
}));
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
current.resolve({ type, payload });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
destroy(error) {
|
|
285
|
+
if (this.socket) {
|
|
286
|
+
this.socket.destroy();
|
|
287
|
+
this.socket = null;
|
|
288
|
+
}
|
|
289
|
+
this.buffer = Buffer.alloc(0);
|
|
290
|
+
const pending = this.pending.splice(0);
|
|
291
|
+
for (const request of pending) {
|
|
292
|
+
request.reject(error || new ConnectionError('RedDB wire socket closed', {
|
|
293
|
+
host: this.endpoint.host,
|
|
294
|
+
port: this.endpoint.port,
|
|
295
|
+
}));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
export class RedDbClient {
|
|
300
|
+
baseUrl;
|
|
301
|
+
httpClient;
|
|
302
|
+
defaultTransport;
|
|
303
|
+
allowTransportFallback;
|
|
304
|
+
timeout;
|
|
305
|
+
batchConcurrency;
|
|
306
|
+
wireClient;
|
|
307
|
+
constructor(options = {}) {
|
|
308
|
+
this.baseUrl = options.baseUrl;
|
|
309
|
+
this.defaultTransport = options.transport ?? 'auto';
|
|
310
|
+
this.allowTransportFallback = options.allowTransportFallback ?? true;
|
|
311
|
+
this.timeout = options.timeout ?? 30000;
|
|
312
|
+
this.batchConcurrency = Math.max(1, options.batchConcurrency ?? 8);
|
|
313
|
+
const headers = { ...(options.headers || {}) };
|
|
314
|
+
if (options.authToken) {
|
|
315
|
+
headers.Authorization = `Bearer ${options.authToken}`;
|
|
316
|
+
}
|
|
317
|
+
if (options.writeToken) {
|
|
318
|
+
headers['x-write-token'] = options.writeToken;
|
|
319
|
+
}
|
|
320
|
+
if (options.client) {
|
|
321
|
+
this.httpClient = options.client;
|
|
322
|
+
}
|
|
323
|
+
else if (this.baseUrl) {
|
|
324
|
+
this.httpClient = createClient({
|
|
325
|
+
baseUrl: this.baseUrl,
|
|
326
|
+
headers,
|
|
327
|
+
timeout: this.timeout,
|
|
328
|
+
http2: options.http2 ?? true,
|
|
329
|
+
...(options.httpClientOptions || {}),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
this.httpClient = null;
|
|
334
|
+
}
|
|
335
|
+
this.wireClient = options.wireAddress
|
|
336
|
+
? new RedDbWireSocket(options.wireAddress, options.wireTls, this.timeout)
|
|
337
|
+
: null;
|
|
338
|
+
}
|
|
339
|
+
getCapabilities() {
|
|
340
|
+
const hasHttp = this.httpClient !== null;
|
|
341
|
+
const hasWire = this.wireClient !== null;
|
|
342
|
+
return {
|
|
343
|
+
requestedTransport: this.defaultTransport,
|
|
344
|
+
allowTransportFallback: this.allowTransportFallback,
|
|
345
|
+
availableTransports: {
|
|
346
|
+
http: hasHttp,
|
|
347
|
+
wire: hasWire,
|
|
348
|
+
grpc: false,
|
|
349
|
+
},
|
|
350
|
+
native: {
|
|
351
|
+
query: hasHttp || hasWire,
|
|
352
|
+
batchQuery: false,
|
|
353
|
+
scan: hasHttp,
|
|
354
|
+
bulkInsertRows: hasHttp || hasWire,
|
|
355
|
+
bulkInsertBinary: false,
|
|
356
|
+
stats: hasHttp,
|
|
357
|
+
ensureCollection: hasHttp,
|
|
358
|
+
ensureIndex: hasHttp || hasWire,
|
|
359
|
+
warmupIndex: hasHttp,
|
|
360
|
+
},
|
|
361
|
+
emulated: {
|
|
362
|
+
batchQuery: hasHttp || hasWire,
|
|
363
|
+
bulkInsertBinary: hasHttp || hasWire,
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
async query(query, options = {}) {
|
|
368
|
+
if (!query || !query.trim()) {
|
|
369
|
+
throw new ValidationError('Query must be a non-empty string', {
|
|
370
|
+
field: 'query',
|
|
371
|
+
value: query,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
const resolution = this.resolveTransport('query', options.transport);
|
|
375
|
+
const startedAt = Date.now();
|
|
376
|
+
const started = performance.now();
|
|
377
|
+
const data = resolution.transport === 'wire'
|
|
378
|
+
? await this.runWireQuery(query, options)
|
|
379
|
+
: await this.runHttpQuery('/query', {
|
|
380
|
+
query,
|
|
381
|
+
entity_types: options.entityTypes,
|
|
382
|
+
capabilities: options.capabilities,
|
|
383
|
+
}, options);
|
|
384
|
+
return this.buildEnvelope('query', data, resolution, startedAt, started);
|
|
385
|
+
}
|
|
386
|
+
async explainQuery(query, options = {}) {
|
|
387
|
+
if (!query || !query.trim()) {
|
|
388
|
+
throw new ValidationError('Query must be a non-empty string', {
|
|
389
|
+
field: 'query',
|
|
390
|
+
value: query,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
const resolution = this.resolveTransport('explainQuery', options.transport);
|
|
394
|
+
const startedAt = Date.now();
|
|
395
|
+
const started = performance.now();
|
|
396
|
+
const data = resolution.transport === 'wire'
|
|
397
|
+
? await this.runWireQuery(`EXPLAIN ${query}`, options)
|
|
398
|
+
: await this.runHttpQuery('/query/explain', { query }, options);
|
|
399
|
+
return this.buildEnvelope('explainQuery', data, resolution, startedAt, started);
|
|
400
|
+
}
|
|
401
|
+
async mutateQuery(query, options = {}) {
|
|
402
|
+
return this.query(query, options);
|
|
403
|
+
}
|
|
404
|
+
async batchQuery(queries, options = {}) {
|
|
405
|
+
if (!Array.isArray(queries) || queries.length === 0) {
|
|
406
|
+
throw new ValidationError('batchQuery requires a non-empty array of queries', {
|
|
407
|
+
field: 'queries',
|
|
408
|
+
value: queries,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
const resolution = this.resolveTransport('batchQuery', options.transport);
|
|
412
|
+
const startedAt = Date.now();
|
|
413
|
+
const started = performance.now();
|
|
414
|
+
const results = await mapWithConcurrency(queries, this.batchConcurrency, async (query) => {
|
|
415
|
+
if (resolution.transport === 'wire') {
|
|
416
|
+
return this.runWireQuery(query, options);
|
|
417
|
+
}
|
|
418
|
+
return this.runHttpQuery('/query', {
|
|
419
|
+
query,
|
|
420
|
+
entity_types: options.entityTypes,
|
|
421
|
+
capabilities: options.capabilities,
|
|
422
|
+
}, options);
|
|
423
|
+
});
|
|
424
|
+
return this.buildEnvelope('batchQuery', { results }, { ...resolution, emulated: true }, startedAt, started);
|
|
425
|
+
}
|
|
426
|
+
async scan(params) {
|
|
427
|
+
if (!params.collection || !params.collection.trim()) {
|
|
428
|
+
throw new ValidationError('scan requires a collection name', {
|
|
429
|
+
field: 'collection',
|
|
430
|
+
value: params.collection,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
const resolution = this.resolveTransport('scan', params.transport);
|
|
434
|
+
const startedAt = Date.now();
|
|
435
|
+
const started = performance.now();
|
|
436
|
+
const query = new URLSearchParams();
|
|
437
|
+
query.set('offset', String(Math.max(0, params.offset ?? 0)));
|
|
438
|
+
query.set('limit', String(Math.max(1, params.limit ?? 100)));
|
|
439
|
+
const data = await this.runHttpGet(`/collections/${encodeURIComponent(params.collection)}/scan?${query.toString()}`, params);
|
|
440
|
+
return this.buildEnvelope('scan', data, resolution, startedAt, started);
|
|
441
|
+
}
|
|
442
|
+
async bulkInsertRows(params) {
|
|
443
|
+
if (!params.collection || !params.collection.trim()) {
|
|
444
|
+
throw new ValidationError('bulkInsertRows requires a collection name', {
|
|
445
|
+
field: 'collection',
|
|
446
|
+
value: params.collection,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
if (!Array.isArray(params.items) || params.items.length === 0) {
|
|
450
|
+
throw new ValidationError('bulkInsertRows requires a non-empty items array', {
|
|
451
|
+
field: 'items',
|
|
452
|
+
value: params.items,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
const resolution = this.resolveTransport('bulkInsertRows', params.transport);
|
|
456
|
+
const startedAt = Date.now();
|
|
457
|
+
const started = performance.now();
|
|
458
|
+
let data;
|
|
459
|
+
if (resolution.transport === 'wire') {
|
|
460
|
+
const payloads = params.items.map((item) => JSON.stringify(item));
|
|
461
|
+
const count = await this.runWireBulkInsert(params.collection, payloads, params);
|
|
462
|
+
data = { ok: true, count };
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
data = await this.runHttpQuery(`/collections/${encodeURIComponent(params.collection)}/bulk/rows`, { items: params.items }, params);
|
|
466
|
+
}
|
|
467
|
+
return this.buildEnvelope('bulkInsertRows', data, resolution, startedAt, started);
|
|
468
|
+
}
|
|
469
|
+
async bulkInsertBinary(params) {
|
|
470
|
+
if (!params.collection || !params.collection.trim()) {
|
|
471
|
+
throw new ValidationError('bulkInsertBinary requires a collection name', {
|
|
472
|
+
field: 'collection',
|
|
473
|
+
value: params.collection,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
if (!Array.isArray(params.fieldNames) || params.fieldNames.length === 0) {
|
|
477
|
+
throw new ValidationError('bulkInsertBinary requires fieldNames', {
|
|
478
|
+
field: 'fieldNames',
|
|
479
|
+
value: params.fieldNames,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
if (!Array.isArray(params.rows) || params.rows.length === 0) {
|
|
483
|
+
throw new ValidationError('bulkInsertBinary requires rows', {
|
|
484
|
+
field: 'rows',
|
|
485
|
+
value: params.rows,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
const hasTrueBinaryValues = params.rows.some((row) => row.some((value) => isBufferLike(value)));
|
|
489
|
+
if (hasTrueBinaryValues) {
|
|
490
|
+
throw new UnsupportedError('bulkInsertBinary byte values require native gRPC support, which is not implemented yet', { feature: 'reddb.bulkInsertBinary.bytes' });
|
|
491
|
+
}
|
|
492
|
+
const items = params.rows.map((row) => ({
|
|
493
|
+
fields: this.binaryRowToFields(params.fieldNames, row),
|
|
494
|
+
}));
|
|
495
|
+
const envelope = await this.bulkInsertRows({
|
|
496
|
+
collection: params.collection,
|
|
497
|
+
items,
|
|
498
|
+
transport: params.transport,
|
|
499
|
+
signal: params.signal,
|
|
500
|
+
timeout: params.timeout,
|
|
501
|
+
});
|
|
502
|
+
return {
|
|
503
|
+
...envelope,
|
|
504
|
+
emulated: true,
|
|
505
|
+
metrics: {
|
|
506
|
+
...envelope.metrics,
|
|
507
|
+
operation: 'bulkInsertBinary',
|
|
508
|
+
emulated: true,
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
async stats(options = {}) {
|
|
513
|
+
const resolution = this.resolveTransport('stats', options.transport);
|
|
514
|
+
const startedAt = Date.now();
|
|
515
|
+
const started = performance.now();
|
|
516
|
+
const data = await this.runHttpGet('/stats', options);
|
|
517
|
+
return this.buildEnvelope('stats', data, resolution, startedAt, started);
|
|
518
|
+
}
|
|
519
|
+
async ensureCollection(options) {
|
|
520
|
+
if (!options.name || !options.name.trim()) {
|
|
521
|
+
throw new ValidationError('ensureCollection requires a collection name', {
|
|
522
|
+
field: 'name',
|
|
523
|
+
value: options.name,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
const resolution = this.resolveTransport('ensureCollection', options.transport);
|
|
527
|
+
const startedAt = Date.now();
|
|
528
|
+
const started = performance.now();
|
|
529
|
+
const payload = { name: options.name };
|
|
530
|
+
if (options.ttl !== undefined)
|
|
531
|
+
payload.ttl = options.ttl;
|
|
532
|
+
if (options.ttlMs !== undefined)
|
|
533
|
+
payload.ttl_ms = options.ttlMs;
|
|
534
|
+
const data = await this.runHttpQuery('/collections', payload, options);
|
|
535
|
+
return this.buildEnvelope('ensureCollection', data, resolution, startedAt, started);
|
|
536
|
+
}
|
|
537
|
+
async ensureIndex(options) {
|
|
538
|
+
if (!options.name || !options.name.trim()) {
|
|
539
|
+
throw new ValidationError('ensureIndex requires an index name', {
|
|
540
|
+
field: 'name',
|
|
541
|
+
value: options.name,
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
if (!options.collection || !options.collection.trim()) {
|
|
545
|
+
throw new ValidationError('ensureIndex requires a collection name', {
|
|
546
|
+
field: 'collection',
|
|
547
|
+
value: options.collection,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
if (!Array.isArray(options.columns) || options.columns.length === 0) {
|
|
551
|
+
throw new ValidationError('ensureIndex requires at least one column', {
|
|
552
|
+
field: 'columns',
|
|
553
|
+
value: options.columns,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
const unique = options.unique ? 'UNIQUE ' : '';
|
|
557
|
+
const method = options.method ? ` USING ${options.method}` : '';
|
|
558
|
+
const columns = options.columns.map(escapeSqlIdentifier).join(', ');
|
|
559
|
+
const query = `CREATE ${unique}INDEX IF NOT EXISTS ${escapeSqlIdentifier(options.name)} ON ${escapeSqlIdentifier(options.collection)} (${columns})${method}`;
|
|
560
|
+
return this.query(query, options);
|
|
561
|
+
}
|
|
562
|
+
async warmupIndex(options) {
|
|
563
|
+
if (!options.name || !options.name.trim()) {
|
|
564
|
+
throw new ValidationError('warmupIndex requires an index name', {
|
|
565
|
+
field: 'name',
|
|
566
|
+
value: options.name,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
const resolution = this.resolveTransport('warmupIndex', options.transport);
|
|
570
|
+
const startedAt = Date.now();
|
|
571
|
+
const started = performance.now();
|
|
572
|
+
const data = await this.runHttpQuery(`/indexes/${encodeURIComponent(options.name)}/warmup`, {}, options);
|
|
573
|
+
return this.buildEnvelope('warmupIndex', data, resolution, startedAt, started);
|
|
574
|
+
}
|
|
575
|
+
async close() {
|
|
576
|
+
if (this.wireClient) {
|
|
577
|
+
await this.wireClient.close();
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
binaryRowToFields(fieldNames, row) {
|
|
581
|
+
if (row.length !== fieldNames.length) {
|
|
582
|
+
throw new ValidationError(`bulkInsertBinary row length ${row.length} does not match fieldNames length ${fieldNames.length}`, { field: 'rows', value: row });
|
|
583
|
+
}
|
|
584
|
+
const fields = {};
|
|
585
|
+
for (let index = 0; index < fieldNames.length; index++) {
|
|
586
|
+
const fieldName = fieldNames[index];
|
|
587
|
+
const value = row[index];
|
|
588
|
+
if (value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
589
|
+
fields[fieldName] = value;
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
if (isBufferLike(value)) {
|
|
593
|
+
throw new UnsupportedError('bulkInsertBinary emulation cannot safely serialize byte values without native gRPC support', { feature: 'reddb.bulkInsertBinary.bytes' });
|
|
594
|
+
}
|
|
595
|
+
throw new ValidationError(`Unsupported binary row value for field ${fieldName}`, {
|
|
596
|
+
field: fieldName,
|
|
597
|
+
value,
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
return fields;
|
|
601
|
+
}
|
|
602
|
+
async runHttpQuery(path, payload, options) {
|
|
603
|
+
if (!this.httpClient) {
|
|
604
|
+
throw new UnsupportedError('HTTP transport is not configured for this RedDB client', {
|
|
605
|
+
feature: 'reddb.http',
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
const response = await this.httpClient.post(path, {
|
|
609
|
+
json: payload,
|
|
610
|
+
signal: options.signal,
|
|
611
|
+
timeout: options.timeout ?? this.timeout,
|
|
612
|
+
throwHttpErrors: false,
|
|
613
|
+
});
|
|
614
|
+
return this.parseHttpResponse(response, path);
|
|
615
|
+
}
|
|
616
|
+
async runHttpGet(path, options) {
|
|
617
|
+
if (!this.httpClient) {
|
|
618
|
+
throw new UnsupportedError('HTTP transport is not configured for this RedDB client', {
|
|
619
|
+
feature: 'reddb.http',
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
const response = await this.httpClient.get(path, {
|
|
623
|
+
signal: options.signal,
|
|
624
|
+
timeout: options.timeout ?? this.timeout,
|
|
625
|
+
throwHttpErrors: false,
|
|
626
|
+
});
|
|
627
|
+
return this.parseHttpResponse(response, path);
|
|
628
|
+
}
|
|
629
|
+
async parseHttpResponse(response, path) {
|
|
630
|
+
if (response.status === 401) {
|
|
631
|
+
throw new AuthenticationError(`RedDB request unauthorized for ${path}`, {
|
|
632
|
+
authType: 'bearer',
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
if (response.status === 404) {
|
|
636
|
+
throw new NotFoundError(`RedDB resource not found: ${path}`, {
|
|
637
|
+
resource: path,
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
if (!response.ok) {
|
|
641
|
+
const message = await response.text().catch(() => `HTTP ${response.status}`);
|
|
642
|
+
throw new ProtocolError(`RedDB HTTP request failed for ${path}: ${message}`, {
|
|
643
|
+
protocol: 'reddb',
|
|
644
|
+
code: response.status,
|
|
645
|
+
phase: 'http',
|
|
646
|
+
retriable: response.status >= 500,
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
if (response.status === 204) {
|
|
650
|
+
return undefined;
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
return await response.json();
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
throw new ParseError(`Failed to parse RedDB HTTP response for ${path}: ${error instanceof Error ? error.message : String(error)}`, { format: 'json' });
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
async runWireQuery(query, options) {
|
|
660
|
+
if (options.signal) {
|
|
661
|
+
throw new UnsupportedError('AbortSignal is not supported for RedDB wire operations', {
|
|
662
|
+
feature: 'reddb.wire.signal',
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
if (!this.wireClient) {
|
|
666
|
+
throw new UnsupportedError('Wire transport is not configured for this RedDB client', {
|
|
667
|
+
feature: 'reddb.wire',
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
return await this.wireClient.query(query, options.timeout ?? this.timeout);
|
|
671
|
+
}
|
|
672
|
+
async runWireBulkInsert(collection, payloads, options) {
|
|
673
|
+
if (options.signal) {
|
|
674
|
+
throw new UnsupportedError('AbortSignal is not supported for RedDB wire operations', {
|
|
675
|
+
feature: 'reddb.wire.signal',
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
if (!this.wireClient) {
|
|
679
|
+
throw new UnsupportedError('Wire transport is not configured for this RedDB client', {
|
|
680
|
+
feature: 'reddb.wire',
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
return await this.wireClient.bulkInsert(collection, payloads, options.timeout ?? this.timeout);
|
|
684
|
+
}
|
|
685
|
+
resolveTransport(operation, requestedTransport) {
|
|
686
|
+
const requested = requestedTransport ?? this.defaultTransport;
|
|
687
|
+
const hasHttp = this.httpClient !== null;
|
|
688
|
+
const hasWire = this.wireClient !== null;
|
|
689
|
+
const requireHttp = () => {
|
|
690
|
+
if (hasHttp) {
|
|
691
|
+
return {
|
|
692
|
+
transport: 'http',
|
|
693
|
+
requestedTransport: requested,
|
|
694
|
+
degradedFromRequestedTransport: requested !== 'auto' && requested !== 'http',
|
|
695
|
+
emulated: false,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
throw new UnsupportedError(`Operation ${operation} requires the RedDB HTTP API`, {
|
|
699
|
+
feature: `reddb.${operation}.http`,
|
|
700
|
+
});
|
|
701
|
+
};
|
|
702
|
+
const preferWireThenHttp = () => {
|
|
703
|
+
if (hasWire) {
|
|
704
|
+
return {
|
|
705
|
+
transport: 'wire',
|
|
706
|
+
requestedTransport: requested,
|
|
707
|
+
degradedFromRequestedTransport: requested === 'grpc' || requested === 'http',
|
|
708
|
+
emulated: false,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
if (hasHttp) {
|
|
712
|
+
return {
|
|
713
|
+
transport: 'http',
|
|
714
|
+
requestedTransport: requested,
|
|
715
|
+
degradedFromRequestedTransport: requested === 'grpc' || requested === 'wire',
|
|
716
|
+
emulated: false,
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
throw new UnsupportedError(`No RedDB transport is configured for ${operation}`, {
|
|
720
|
+
feature: `reddb.${operation}`,
|
|
721
|
+
});
|
|
722
|
+
};
|
|
723
|
+
const preferHttpThenWire = () => {
|
|
724
|
+
if (hasHttp) {
|
|
725
|
+
return {
|
|
726
|
+
transport: 'http',
|
|
727
|
+
requestedTransport: requested,
|
|
728
|
+
degradedFromRequestedTransport: requested === 'grpc' || requested === 'wire',
|
|
729
|
+
emulated: false,
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
if (hasWire) {
|
|
733
|
+
return {
|
|
734
|
+
transport: 'wire',
|
|
735
|
+
requestedTransport: requested,
|
|
736
|
+
degradedFromRequestedTransport: requested === 'grpc' || requested === 'http',
|
|
737
|
+
emulated: false,
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
throw new UnsupportedError(`No RedDB transport is configured for ${operation}`, {
|
|
741
|
+
feature: `reddb.${operation}`,
|
|
742
|
+
});
|
|
743
|
+
};
|
|
744
|
+
const requiresHttp = new Set(['scan', 'stats', 'ensureCollection', 'warmupIndex']);
|
|
745
|
+
const prefersWire = new Set(['query', 'mutateQuery', 'explainQuery', 'batchQuery', 'bulkInsertRows', 'bulkInsertBinary', 'ensureIndex']);
|
|
746
|
+
if (requested === 'auto') {
|
|
747
|
+
return prefersWire.has(operation) ? preferWireThenHttp() : requireHttp();
|
|
748
|
+
}
|
|
749
|
+
if (requested === 'http') {
|
|
750
|
+
if (requiresHttp.has(operation)) {
|
|
751
|
+
return requireHttp();
|
|
752
|
+
}
|
|
753
|
+
if (hasHttp) {
|
|
754
|
+
return {
|
|
755
|
+
transport: 'http',
|
|
756
|
+
requestedTransport: requested,
|
|
757
|
+
degradedFromRequestedTransport: false,
|
|
758
|
+
emulated: false,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
if (this.allowTransportFallback && hasWire) {
|
|
762
|
+
return {
|
|
763
|
+
transport: 'wire',
|
|
764
|
+
requestedTransport: requested,
|
|
765
|
+
degradedFromRequestedTransport: true,
|
|
766
|
+
emulated: false,
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
throw new UnsupportedError(`HTTP transport is unavailable for ${operation}`, {
|
|
770
|
+
feature: `reddb.${operation}.http`,
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
if (requested === 'wire') {
|
|
774
|
+
if (hasWire && !requiresHttp.has(operation)) {
|
|
775
|
+
return {
|
|
776
|
+
transport: 'wire',
|
|
777
|
+
requestedTransport: requested,
|
|
778
|
+
degradedFromRequestedTransport: false,
|
|
779
|
+
emulated: false,
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
if (this.allowTransportFallback && hasHttp) {
|
|
783
|
+
return {
|
|
784
|
+
transport: 'http',
|
|
785
|
+
requestedTransport: requested,
|
|
786
|
+
degradedFromRequestedTransport: true,
|
|
787
|
+
emulated: false,
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
throw new UnsupportedError(`Wire transport cannot satisfy ${operation}`, {
|
|
791
|
+
feature: `reddb.${operation}.wire`,
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
if (!this.allowTransportFallback) {
|
|
795
|
+
throw new UnsupportedError(`gRPC transport is not implemented for ${operation}`, {
|
|
796
|
+
feature: `reddb.${operation}.grpc`,
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
return requiresHttp.has(operation) ? preferHttpThenWire() : preferWireThenHttp();
|
|
800
|
+
}
|
|
801
|
+
buildEnvelope(operation, data, resolution, startedAt, started) {
|
|
802
|
+
const queryStats = buildQueryStats(data);
|
|
803
|
+
return {
|
|
804
|
+
data,
|
|
805
|
+
transport: resolution.transport,
|
|
806
|
+
requestedTransport: resolution.requestedTransport,
|
|
807
|
+
degradedFromRequestedTransport: resolution.degradedFromRequestedTransport,
|
|
808
|
+
emulated: resolution.emulated,
|
|
809
|
+
metrics: {
|
|
810
|
+
operation,
|
|
811
|
+
requestedTransport: resolution.requestedTransport,
|
|
812
|
+
transport: resolution.transport,
|
|
813
|
+
degradedFromRequestedTransport: resolution.degradedFromRequestedTransport,
|
|
814
|
+
emulated: resolution.emulated,
|
|
815
|
+
startedAt,
|
|
816
|
+
durationMs: Number((performance.now() - started).toFixed(3)),
|
|
817
|
+
...queryStats,
|
|
818
|
+
},
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
export function createRedDbClient(options = {}) {
|
|
823
|
+
return new RedDbClient(options);
|
|
824
|
+
}
|
|
825
|
+
export function reddb(options = {}) {
|
|
826
|
+
return createRedDbClient(options);
|
|
827
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -62,6 +62,7 @@ export * from './plugins/http3.js';
|
|
|
62
62
|
export * from './cache/redis-storage.js';
|
|
63
63
|
export * from './plugins/queue.js';
|
|
64
64
|
export * from './queue/consumer.js';
|
|
65
|
+
export * from './clients/reddb.js';
|
|
65
66
|
export * from './extractors/index.js';
|
|
66
67
|
export * from './video/index.js';
|
|
67
68
|
export * from './events/request-events.js';
|
|
@@ -75,6 +76,7 @@ export * from './utils/http2-metrics.js';
|
|
|
75
76
|
export * from './utils/concurrency.js';
|
|
76
77
|
export * as presets from './presets/index.js';
|
|
77
78
|
export * as testing from './testing/index.js';
|
|
79
|
+
export * as clients from './clients/index.js';
|
|
78
80
|
export * as protocols from './protocols/index.js';
|
|
79
81
|
export * from './raffel/index.js';
|
|
80
82
|
export * from './mcp/client.js';
|
package/dist/index.js
CHANGED
|
@@ -62,6 +62,7 @@ export * from './plugins/http3.js';
|
|
|
62
62
|
export * from './cache/redis-storage.js';
|
|
63
63
|
export * from './plugins/queue.js';
|
|
64
64
|
export * from './queue/consumer.js';
|
|
65
|
+
export * from './clients/reddb.js';
|
|
65
66
|
export * from './extractors/index.js';
|
|
66
67
|
export * from './video/index.js';
|
|
67
68
|
export * from './events/request-events.js';
|
|
@@ -75,6 +76,7 @@ export * from './utils/http2-metrics.js';
|
|
|
75
76
|
export * from './utils/concurrency.js';
|
|
76
77
|
export * as presets from './presets/index.js';
|
|
77
78
|
export * as testing from './testing/index.js';
|
|
79
|
+
export * as clients from './clients/index.js';
|
|
78
80
|
export * as protocols from './protocols/index.js';
|
|
79
81
|
export * from './raffel/index.js';
|
|
80
82
|
export * from './mcp/client.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { FTP, createFTP, ftp, type FTPConfig, type FTPListItem, type FTPTransferProgress, type FTPResponse } from './ftp.js';
|
|
2
2
|
export { SFTP, createSFTP, sftp, type SFTPConfig, type SFTPListItem, type SFTPResponse } from './sftp.js';
|
|
3
3
|
export { Telnet, createTelnet, telnet, type TelnetConfig, type TelnetResponse, type TelnetExecOptions } from './telnet.js';
|
|
4
|
+
export { RedDbClient, createRedDbClient, reddb, type RedDbTransportMode, type RedDbResolvedTransport, type RedDbQueryStats, type RedDbQueryData, type RedDbBatchQueryData, type RedDbScanEntity, type RedDbScanData, type RedDbBulkInsertData, type RedDbCapabilities, type RedDbOperationMetrics, type RedDbOperationEnvelope, type RedDbQueryOptions, type RedDbScanRequest, type RedDbBulkInsertRowsRequest, type RedDbBinaryValue, type RedDbBulkInsertBinaryRequest, type RedDbEnsureCollectionOptions, type RedDbEnsureIndexOptions, type RedDbWarmupIndexOptions, type RedDbStatsOptions, type RedDbWireTlsOptions, type RedDbClientOptions } from '../clients/reddb.js';
|
package/dist/protocols/index.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../clients/reddb.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../clients/reddb.js';
|
package/dist/recker.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { VideoBuilder } from './video/builder.js';
|
|
|
13
13
|
import { getExtractorName, listExtractors } from './extractors/index.js';
|
|
14
14
|
import type { ExtractorResult } from './extractors/base.js';
|
|
15
15
|
import { type GoogleSearchAdvancedOptions, type GoogleSearchResponse } from './search/index.js';
|
|
16
|
+
import { type RedDbClient, type RedDbClientOptions } from './clients/reddb.js';
|
|
16
17
|
export declare function get<T = unknown>(url: string, options?: RequestOptions): RequestPromise<T>;
|
|
17
18
|
export declare function post<T = unknown>(url: string, options?: RequestOptions): RequestPromise<T>;
|
|
18
19
|
export declare function post<T = unknown>(url: string, body?: unknown, options?: RequestOptions): RequestPromise<T>;
|
|
@@ -105,6 +106,7 @@ export declare const recker: {
|
|
|
105
106
|
dnsClient: (options?: DNSClientOptions) => DNSClient;
|
|
106
107
|
whoisClient: typeof createWhois;
|
|
107
108
|
aiClient: (options?: AIClientConfig) => import("./types/ai.js").AIClient;
|
|
109
|
+
reddb: (options?: RedDbClientOptions) => RedDbClient;
|
|
108
110
|
reset: () => void;
|
|
109
111
|
readonly version: string;
|
|
110
112
|
getVersion: typeof getVersion;
|
package/dist/recker.js
CHANGED
|
@@ -10,6 +10,7 @@ import { uploadFile } from './utils/upload.js';
|
|
|
10
10
|
import { createVideoBuilder } from './video/builder.js';
|
|
11
11
|
import { extract, extractors, isSupported, getExtractorName, listExtractors } from './extractors/index.js';
|
|
12
12
|
import { searchGoogleAdvanced } from './search/index.js';
|
|
13
|
+
import { createRedDbClient } from './clients/reddb.js';
|
|
13
14
|
let _defaultClient = null;
|
|
14
15
|
let _defaultDns = null;
|
|
15
16
|
let _defaultAi = null;
|
|
@@ -166,6 +167,7 @@ export const recker = {
|
|
|
166
167
|
dnsClient: (options) => createDNS(options),
|
|
167
168
|
whoisClient: createWhois,
|
|
168
169
|
aiClient: (options) => createAI(options),
|
|
170
|
+
reddb: (options) => createRedDbClient(options),
|
|
169
171
|
reset: () => {
|
|
170
172
|
_defaultClient = null;
|
|
171
173
|
_defaultDns = null;
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "recker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.102",
|
|
4
4
|
"description": "Multi-Protocol SDK for the AI Era - HTTP, WebSocket, DNS, FTP, SFTP, Telnet, HLS unified with AI providers and MCP tools",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|