@pineliner/odb-client 1.1.5 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/database/adapters/odblite-socket.d.ts +52 -0
- package/dist/database/adapters/odblite-socket.d.ts.map +1 -0
- package/dist/database/adapters/socket/client.d.ts +28 -0
- package/dist/database/adapters/socket/client.d.ts.map +1 -0
- package/dist/database/adapters/socket/protocol.d.ts +129 -0
- package/dist/database/adapters/socket/protocol.d.ts.map +1 -0
- package/dist/database/manager.d.ts.map +1 -1
- package/dist/database/types.d.ts +6 -1
- package/dist/database/types.d.ts.map +1 -1
- package/dist/index.cjs +425 -1
- package/dist/index.js +425 -1
- package/package.json +1 -1
- package/src/database/adapters/odblite-socket.ts +255 -0
- package/src/database/adapters/socket/client.ts +78 -0
- package/src/database/adapters/socket/protocol.ts +147 -0
- package/src/database/manager.ts +19 -1
- package/src/database/types.ts +8 -1
package/dist/index.cjs
CHANGED
|
@@ -2138,6 +2138,417 @@ var __webpack_exports__ = {};
|
|
|
2138
2138
|
return this.client.httpClient.getNamespaces();
|
|
2139
2139
|
}
|
|
2140
2140
|
}
|
|
2141
|
+
function toHrana(v) {
|
|
2142
|
+
if (null == v) return {
|
|
2143
|
+
type: 'null'
|
|
2144
|
+
};
|
|
2145
|
+
if ('bigint' == typeof v) return {
|
|
2146
|
+
type: 'integer',
|
|
2147
|
+
value: v.toString()
|
|
2148
|
+
};
|
|
2149
|
+
if ('number' == typeof v) return Number.isInteger(v) ? {
|
|
2150
|
+
type: 'integer',
|
|
2151
|
+
value: String(v)
|
|
2152
|
+
} : {
|
|
2153
|
+
type: 'float',
|
|
2154
|
+
value: v
|
|
2155
|
+
};
|
|
2156
|
+
if ('string' == typeof v) return {
|
|
2157
|
+
type: 'text',
|
|
2158
|
+
value: v
|
|
2159
|
+
};
|
|
2160
|
+
if (v instanceof Uint8Array) return {
|
|
2161
|
+
type: 'blob',
|
|
2162
|
+
base64: Buffer.from(v).toString('base64')
|
|
2163
|
+
};
|
|
2164
|
+
if ('boolean' == typeof v) return {
|
|
2165
|
+
type: 'integer',
|
|
2166
|
+
value: v ? '1' : '0'
|
|
2167
|
+
};
|
|
2168
|
+
return {
|
|
2169
|
+
type: 'text',
|
|
2170
|
+
value: String(v)
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
function fromHrana(v) {
|
|
2174
|
+
switch(v.type){
|
|
2175
|
+
case 'null':
|
|
2176
|
+
return null;
|
|
2177
|
+
case 'integer':
|
|
2178
|
+
{
|
|
2179
|
+
const n = Number(v.value);
|
|
2180
|
+
return Number.isSafeInteger(n) ? n : BigInt(v.value);
|
|
2181
|
+
}
|
|
2182
|
+
case 'float':
|
|
2183
|
+
return v.value;
|
|
2184
|
+
case 'text':
|
|
2185
|
+
return v.value;
|
|
2186
|
+
case 'blob':
|
|
2187
|
+
return new Uint8Array(Buffer.from(v.base64, 'base64'));
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
function toBatch(steps) {
|
|
2191
|
+
return {
|
|
2192
|
+
steps: steps.map((s, i)=>({
|
|
2193
|
+
condition: 0 === i ? void 0 : {
|
|
2194
|
+
type: 'ok',
|
|
2195
|
+
step: i - 1
|
|
2196
|
+
},
|
|
2197
|
+
stmt: {
|
|
2198
|
+
sql: s.sql,
|
|
2199
|
+
args: (s.params ?? []).map(toHrana)
|
|
2200
|
+
},
|
|
2201
|
+
expect: s.expect ? {
|
|
2202
|
+
affected: s.expect
|
|
2203
|
+
} : void 0
|
|
2204
|
+
}))
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
class FrameWriter {
|
|
2208
|
+
socket;
|
|
2209
|
+
pending = Buffer.alloc(0);
|
|
2210
|
+
constructor(socket){
|
|
2211
|
+
this.socket = socket;
|
|
2212
|
+
}
|
|
2213
|
+
send(payload) {
|
|
2214
|
+
const header = Buffer.allocUnsafe(4);
|
|
2215
|
+
header.writeUInt32LE(payload.length, 0);
|
|
2216
|
+
const frame = Buffer.concat([
|
|
2217
|
+
header,
|
|
2218
|
+
payload
|
|
2219
|
+
]);
|
|
2220
|
+
this.pending = this.pending.length ? Buffer.concat([
|
|
2221
|
+
this.pending,
|
|
2222
|
+
frame
|
|
2223
|
+
]) : frame;
|
|
2224
|
+
this.flush();
|
|
2225
|
+
}
|
|
2226
|
+
flush() {
|
|
2227
|
+
while(this.pending.length > 0){
|
|
2228
|
+
const n = this.socket.write(this.pending);
|
|
2229
|
+
if (n <= 0) return;
|
|
2230
|
+
this.pending = n < this.pending.length ? this.pending.subarray(n) : Buffer.alloc(0);
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
class FrameReader {
|
|
2235
|
+
buf = Buffer.alloc(0);
|
|
2236
|
+
push(chunk) {
|
|
2237
|
+
this.buf = this.buf.length ? Buffer.concat([
|
|
2238
|
+
this.buf,
|
|
2239
|
+
chunk
|
|
2240
|
+
]) : chunk;
|
|
2241
|
+
const out = [];
|
|
2242
|
+
while(this.buf.length >= 4){
|
|
2243
|
+
const len = this.buf.readUInt32LE(0);
|
|
2244
|
+
if (this.buf.length < 4 + len) break;
|
|
2245
|
+
out.push(this.buf.subarray(4, 4 + len));
|
|
2246
|
+
this.buf = this.buf.subarray(4 + len);
|
|
2247
|
+
}
|
|
2248
|
+
return out;
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
class SocketClient {
|
|
2252
|
+
target;
|
|
2253
|
+
socket;
|
|
2254
|
+
reader = new FrameReader();
|
|
2255
|
+
writer;
|
|
2256
|
+
pending = new Map();
|
|
2257
|
+
nextId = 1;
|
|
2258
|
+
ready;
|
|
2259
|
+
constructor(target){
|
|
2260
|
+
this.target = target;
|
|
2261
|
+
this.ready = this.connect();
|
|
2262
|
+
this.ready.catch(()=>{});
|
|
2263
|
+
}
|
|
2264
|
+
async connect() {
|
|
2265
|
+
const Bun = globalThis.Bun;
|
|
2266
|
+
if (!Bun?.connect) throw new Error('odblite-socket backend requires Bun (Bun.connect)');
|
|
2267
|
+
const opts = this.target.unix ? {
|
|
2268
|
+
unix: this.target.unix
|
|
2269
|
+
} : {
|
|
2270
|
+
hostname: this.target.hostname ?? '127.0.0.1',
|
|
2271
|
+
port: this.target.port ?? 8787
|
|
2272
|
+
};
|
|
2273
|
+
this.socket = await Bun.connect({
|
|
2274
|
+
...opts,
|
|
2275
|
+
socket: {
|
|
2276
|
+
data: (_s, chunk)=>{
|
|
2277
|
+
for (const frame of this.reader.push(chunk)){
|
|
2278
|
+
const msg = JSON.parse(frame.toString('utf8'));
|
|
2279
|
+
const resolve = this.pending.get(msg.id);
|
|
2280
|
+
if (resolve) {
|
|
2281
|
+
this.pending.delete(msg.id);
|
|
2282
|
+
resolve(msg.results);
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
},
|
|
2286
|
+
drain: (_s)=>this.writer.flush(),
|
|
2287
|
+
close: ()=>{
|
|
2288
|
+
this.socket = void 0;
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
});
|
|
2292
|
+
this.writer = new FrameWriter(this.socket);
|
|
2293
|
+
}
|
|
2294
|
+
async pipeline(tenant, requests) {
|
|
2295
|
+
if (!this.socket) {
|
|
2296
|
+
this.ready = this.connect();
|
|
2297
|
+
this.ready.catch(()=>{});
|
|
2298
|
+
}
|
|
2299
|
+
await this.ready;
|
|
2300
|
+
const id = this.nextId++;
|
|
2301
|
+
return new Promise((resolve)=>{
|
|
2302
|
+
this.pending.set(id, resolve);
|
|
2303
|
+
this.writer.send(Buffer.from(JSON.stringify({
|
|
2304
|
+
id,
|
|
2305
|
+
tenant,
|
|
2306
|
+
requests
|
|
2307
|
+
}), 'utf8'));
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2310
|
+
close() {
|
|
2311
|
+
this.socket?.end?.();
|
|
2312
|
+
this.socket = void 0;
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
function toQueryResult(res) {
|
|
2316
|
+
if ('error' === res.type) throw new Error(res.error.message);
|
|
2317
|
+
const r = res.response.result;
|
|
2318
|
+
const rows = r.rows.map((row)=>{
|
|
2319
|
+
const obj = {};
|
|
2320
|
+
r.cols.forEach((c, i)=>{
|
|
2321
|
+
obj[c.name ?? `c${i}`] = fromHrana(row[i] ?? {
|
|
2322
|
+
type: 'null'
|
|
2323
|
+
});
|
|
2324
|
+
});
|
|
2325
|
+
return obj;
|
|
2326
|
+
});
|
|
2327
|
+
return {
|
|
2328
|
+
rows,
|
|
2329
|
+
rowsAffected: r.affected_row_count,
|
|
2330
|
+
lastInsertRowid: r.last_insert_rowid ? Number(r.last_insert_rowid) : void 0
|
|
2331
|
+
};
|
|
2332
|
+
}
|
|
2333
|
+
class OdbliteSocketConnection {
|
|
2334
|
+
client;
|
|
2335
|
+
tenant;
|
|
2336
|
+
databaseName;
|
|
2337
|
+
databaseHash;
|
|
2338
|
+
sql;
|
|
2339
|
+
buffer;
|
|
2340
|
+
constructor(client, tenant, buffer){
|
|
2341
|
+
this.client = client;
|
|
2342
|
+
this.tenant = tenant;
|
|
2343
|
+
this.databaseName = tenant;
|
|
2344
|
+
this.databaseHash = tenant;
|
|
2345
|
+
this.buffer = buffer;
|
|
2346
|
+
const self = this;
|
|
2347
|
+
this.sql = function(strings, ...values) {
|
|
2348
|
+
if (strings && 'object' == typeof strings && 'raw' in strings) {
|
|
2349
|
+
const text = strings.reduce((a, s, i)=>a + s + (i < values.length ? '?' : ''), '');
|
|
2350
|
+
const returns = /^\s*SELECT/i.test(text) || /\bRETURNING\b/i.test(text);
|
|
2351
|
+
return (returns ? self.query(text, values) : self.execute(text, values)).then((r)=>r.rows);
|
|
2352
|
+
}
|
|
2353
|
+
throw new Error('sql() fragment helper not implemented in odblite-socket');
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
async one(requests) {
|
|
2357
|
+
const [res] = await this.client.pipeline(this.tenant, requests);
|
|
2358
|
+
if (!res) throw new Error('odblite-socket: empty pipeline response');
|
|
2359
|
+
return res;
|
|
2360
|
+
}
|
|
2361
|
+
async query(sql, params = []) {
|
|
2362
|
+
return toQueryResult(await this.one([
|
|
2363
|
+
{
|
|
2364
|
+
type: 'execute',
|
|
2365
|
+
stmt: {
|
|
2366
|
+
sql,
|
|
2367
|
+
args: params.map(toHrana)
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
]));
|
|
2371
|
+
}
|
|
2372
|
+
async execute(sql, params = []) {
|
|
2373
|
+
const s = 'string' == typeof sql ? sql : sql.sql;
|
|
2374
|
+
const p = 'string' == typeof sql ? params : sql.args ?? [];
|
|
2375
|
+
if (this.buffer) {
|
|
2376
|
+
this.buffer.push({
|
|
2377
|
+
sql: s,
|
|
2378
|
+
params: p
|
|
2379
|
+
});
|
|
2380
|
+
return {
|
|
2381
|
+
rows: [],
|
|
2382
|
+
rowsAffected: 0
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
return toQueryResult(await this.one([
|
|
2386
|
+
{
|
|
2387
|
+
type: 'execute',
|
|
2388
|
+
stmt: {
|
|
2389
|
+
sql: s,
|
|
2390
|
+
args: p.map(toHrana)
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
]));
|
|
2394
|
+
}
|
|
2395
|
+
async atomic(steps) {
|
|
2396
|
+
const res = await this.one([
|
|
2397
|
+
{
|
|
2398
|
+
type: 'batch',
|
|
2399
|
+
batch: toBatch(steps)
|
|
2400
|
+
}
|
|
2401
|
+
]);
|
|
2402
|
+
if ('error' === res.type) throw new Error(res.error.message);
|
|
2403
|
+
return res.response.result;
|
|
2404
|
+
}
|
|
2405
|
+
prepare(sql) {
|
|
2406
|
+
return {
|
|
2407
|
+
execute: async (params = [])=>this.execute(sql, params),
|
|
2408
|
+
all: async (params = [])=>(await this.query(sql, params)).rows,
|
|
2409
|
+
get: async (params = [])=>(await this.query(sql, params)).rows[0] ?? null
|
|
2410
|
+
};
|
|
2411
|
+
}
|
|
2412
|
+
async transaction(fn) {
|
|
2413
|
+
if (this.buffer) return fn(this);
|
|
2414
|
+
const buffer = [];
|
|
2415
|
+
const out = await fn(new OdbliteSocketConnection(this.client, this.tenant, buffer));
|
|
2416
|
+
if (buffer.length) {
|
|
2417
|
+
const res = await this.atomic(buffer.map((b)=>({
|
|
2418
|
+
sql: b.sql,
|
|
2419
|
+
params: b.params
|
|
2420
|
+
})));
|
|
2421
|
+
const err = res.step_errors?.find((e)=>e);
|
|
2422
|
+
if (err) throw new Error(`transaction batch failed: ${err.message}`);
|
|
2423
|
+
}
|
|
2424
|
+
return out;
|
|
2425
|
+
}
|
|
2426
|
+
begin(fn) {
|
|
2427
|
+
return this.transaction(fn);
|
|
2428
|
+
}
|
|
2429
|
+
savepoint(fn) {
|
|
2430
|
+
return this.transaction(fn);
|
|
2431
|
+
}
|
|
2432
|
+
async ensureNamespaces(namespaces) {
|
|
2433
|
+
return {
|
|
2434
|
+
success: true,
|
|
2435
|
+
databaseName: this.tenant,
|
|
2436
|
+
namespaces
|
|
2437
|
+
};
|
|
2438
|
+
}
|
|
2439
|
+
async getNamespaces() {
|
|
2440
|
+
const ns = (await this.query("SELECT name FROM pragma_database_list WHERE name != 'main'")).rows.map((r)=>r.name);
|
|
2441
|
+
return {
|
|
2442
|
+
success: true,
|
|
2443
|
+
databaseName: this.tenant,
|
|
2444
|
+
exists: true,
|
|
2445
|
+
namespaces: ns,
|
|
2446
|
+
registered: true,
|
|
2447
|
+
registeredNamespaces: ns
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
async close() {}
|
|
2451
|
+
}
|
|
2452
|
+
class OdbliteSocketAdapter {
|
|
2453
|
+
type = 'odblite-socket';
|
|
2454
|
+
client;
|
|
2455
|
+
constructor(config){
|
|
2456
|
+
this.client = new SocketClient(config);
|
|
2457
|
+
}
|
|
2458
|
+
async connect(config) {
|
|
2459
|
+
const tenant = config?.databaseHash ?? config?.databaseName;
|
|
2460
|
+
if (!tenant) throw new Error('odblite-socket: databaseHash (or databaseName) required');
|
|
2461
|
+
return new OdbliteSocketConnection(this.client, String(tenant));
|
|
2462
|
+
}
|
|
2463
|
+
async disconnect() {
|
|
2464
|
+
this.client.close();
|
|
2465
|
+
}
|
|
2466
|
+
async isHealthy() {
|
|
2467
|
+
try {
|
|
2468
|
+
await Promise.race([
|
|
2469
|
+
this.client.pipeline('_health', [
|
|
2470
|
+
{
|
|
2471
|
+
type: 'get_autocommit'
|
|
2472
|
+
}
|
|
2473
|
+
]),
|
|
2474
|
+
new Promise((_, rej)=>setTimeout(()=>rej(new Error('probe timeout')), 1500))
|
|
2475
|
+
]);
|
|
2476
|
+
return true;
|
|
2477
|
+
} catch {
|
|
2478
|
+
return false;
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
const DEFAULT_UNIX_SOCKET = '/tmp/odblite.sock';
|
|
2483
|
+
function parseSocketTarget(s) {
|
|
2484
|
+
const v = (s ?? '').trim();
|
|
2485
|
+
if (!v) return {
|
|
2486
|
+
unix: DEFAULT_UNIX_SOCKET
|
|
2487
|
+
};
|
|
2488
|
+
if (v.includes('/')) return {
|
|
2489
|
+
unix: v
|
|
2490
|
+
};
|
|
2491
|
+
const m = v.match(/^(?:([^:]+):)?(\d+)$/);
|
|
2492
|
+
if (m) return {
|
|
2493
|
+
hostname: m[1] || '127.0.0.1',
|
|
2494
|
+
port: Number(m[2])
|
|
2495
|
+
};
|
|
2496
|
+
return {
|
|
2497
|
+
unix: v
|
|
2498
|
+
};
|
|
2499
|
+
}
|
|
2500
|
+
class OdbliteAutoAdapter {
|
|
2501
|
+
type = 'odblite';
|
|
2502
|
+
chosen;
|
|
2503
|
+
constructor(opts){
|
|
2504
|
+
this.chosen = this.pick(opts);
|
|
2505
|
+
}
|
|
2506
|
+
async pick(opts) {
|
|
2507
|
+
const hasBun = void 0 !== globalThis.Bun && !!globalThis.Bun.connect;
|
|
2508
|
+
if (hasBun) for (const target of this.candidates(opts))try {
|
|
2509
|
+
const socket = new OdbliteSocketAdapter(target);
|
|
2510
|
+
if (await socket.isHealthy()) {
|
|
2511
|
+
console.log(`[odb-client] transport → SOCKET (router_v2 @ ${target.unix ?? `${target.hostname}:${target.port}`})`);
|
|
2512
|
+
return socket;
|
|
2513
|
+
}
|
|
2514
|
+
await socket.disconnect();
|
|
2515
|
+
} catch {}
|
|
2516
|
+
console.log('[odb-client] transport → HTTP (no router_v2 socket reachable)');
|
|
2517
|
+
return new ODBLiteAdapter(opts.http);
|
|
2518
|
+
}
|
|
2519
|
+
candidates(opts) {
|
|
2520
|
+
if (opts.socket?.unix || opts.socket?.port) return [
|
|
2521
|
+
opts.socket
|
|
2522
|
+
];
|
|
2523
|
+
const env = globalThis.process?.env?.ODB_SOCKET;
|
|
2524
|
+
if (env) return [
|
|
2525
|
+
parseSocketTarget(env)
|
|
2526
|
+
];
|
|
2527
|
+
const out = [
|
|
2528
|
+
{
|
|
2529
|
+
unix: DEFAULT_UNIX_SOCKET
|
|
2530
|
+
}
|
|
2531
|
+
];
|
|
2532
|
+
try {
|
|
2533
|
+
const host = new URL(opts.http?.serviceUrl).hostname;
|
|
2534
|
+
const port = Number(globalThis.process?.env?.ODB_SOCKET_PORT ?? 8787);
|
|
2535
|
+
if (host) out.push({
|
|
2536
|
+
hostname: host,
|
|
2537
|
+
port
|
|
2538
|
+
});
|
|
2539
|
+
} catch {}
|
|
2540
|
+
return out;
|
|
2541
|
+
}
|
|
2542
|
+
async connect(config) {
|
|
2543
|
+
return (await this.chosen).connect(config);
|
|
2544
|
+
}
|
|
2545
|
+
async disconnect(tenantId) {
|
|
2546
|
+
return (await this.chosen).disconnect(tenantId);
|
|
2547
|
+
}
|
|
2548
|
+
async isHealthy() {
|
|
2549
|
+
return (await this.chosen).isHealthy();
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2141
2552
|
function parseSQL(sqlContent, options = {}) {
|
|
2142
2553
|
const separatePragma = options.separatePragma ?? true;
|
|
2143
2554
|
const statements = splitStatements(sqlContent);
|
|
@@ -2249,7 +2660,14 @@ var __webpack_exports__ = {};
|
|
|
2249
2660
|
break;
|
|
2250
2661
|
case 'odblite':
|
|
2251
2662
|
if (!config.odblite) throw new Error('odblite config required for odblite backend');
|
|
2252
|
-
this.adapter = new
|
|
2663
|
+
this.adapter = new OdbliteAutoAdapter({
|
|
2664
|
+
socket: config.odbliteSocket,
|
|
2665
|
+
http: config.odblite
|
|
2666
|
+
});
|
|
2667
|
+
break;
|
|
2668
|
+
case 'odblite-socket':
|
|
2669
|
+
if (!config.odbliteSocket) throw new Error('odbliteSocket config required for odblite-socket backend');
|
|
2670
|
+
this.adapter = new OdbliteSocketAdapter(config.odbliteSocket);
|
|
2253
2671
|
break;
|
|
2254
2672
|
default:
|
|
2255
2673
|
throw new Error(`Unknown backend type: ${config.backend}`);
|
|
@@ -2367,6 +2785,12 @@ var __webpack_exports__ = {};
|
|
|
2367
2785
|
databaseName: `${this.config.databasePath}_${name}`,
|
|
2368
2786
|
...this.config.odblite
|
|
2369
2787
|
};
|
|
2788
|
+
case 'odblite-socket':
|
|
2789
|
+
return {
|
|
2790
|
+
databaseName: name,
|
|
2791
|
+
databaseHash: name,
|
|
2792
|
+
...this.config.odbliteSocket
|
|
2793
|
+
};
|
|
2370
2794
|
default:
|
|
2371
2795
|
throw new Error(`Unknown backend: ${this.config.backend}`);
|
|
2372
2796
|
}
|