@pineliner/odb-client 1.2.0 → 1.4.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 +14 -1
- package/dist/database/adapters/odblite-socket.d.ts.map +1 -1
- package/dist/database/adapters/socket/client.d.ts +14 -1
- package/dist/database/adapters/socket/client.d.ts.map +1 -1
- package/dist/database/adapters/socket/protocol.d.ts +3 -0
- package/dist/database/adapters/socket/protocol.d.ts.map +1 -1
- package/dist/index.cjs +145 -48
- package/dist/index.js +145 -48
- package/package.json +1 -1
- package/src/database/adapters/odblite-socket.ts +118 -38
- package/src/database/adapters/socket/client.ts +66 -10
- package/src/database/adapters/socket/protocol.ts +10 -0
|
@@ -10,6 +10,10 @@ export declare class OdbliteSocketAdapter implements DatabaseAdapter {
|
|
|
10
10
|
disconnect(): Promise<void>;
|
|
11
11
|
isHealthy(): Promise<boolean>;
|
|
12
12
|
}
|
|
13
|
+
/** Socket wire-protocol version this client speaks. The connect-time handshake (isHealthy → get_version)
|
|
14
|
+
* requires the server to match; on mismatch the auto-adapter falls back to HTTP. Keep in lockstep with
|
|
15
|
+
* router_v2's SOCKET_PROTOCOL. */
|
|
16
|
+
export declare const CLIENT_SOCKET_PROTOCOL = 2;
|
|
13
17
|
/** Default same-host unix socket (free — v1 only uses /tmp/odblite-internal.sock). */
|
|
14
18
|
export declare const DEFAULT_UNIX_SOCKET = "/tmp/odblite.sock";
|
|
15
19
|
/**
|
|
@@ -36,12 +40,21 @@ export declare function parseSocketTarget(s?: string): SocketTarget;
|
|
|
36
40
|
*/
|
|
37
41
|
export declare class OdbliteAutoAdapter implements DatabaseAdapter {
|
|
38
42
|
readonly type: BackendType;
|
|
39
|
-
private readonly
|
|
43
|
+
private readonly opts;
|
|
44
|
+
private chosen;
|
|
45
|
+
private onHttp;
|
|
46
|
+
private nextRetryAt;
|
|
40
47
|
constructor(opts: {
|
|
41
48
|
socket?: OdbliteSocketConfig;
|
|
42
49
|
http: any;
|
|
43
50
|
});
|
|
44
51
|
private pick;
|
|
52
|
+
/** Probe the cascade once; return a live socket adapter or null. */
|
|
53
|
+
private trySocket;
|
|
54
|
+
/** When we fell back to HTTP, re-probe the socket on a cooldown so a socket that
|
|
55
|
+
* comes up LATER (server started after the app) is picked up without a restart —
|
|
56
|
+
* subsequent connections then use it. */
|
|
57
|
+
private maybeUpgrade;
|
|
45
58
|
/** Ordered socket targets to probe. Explicit override → a single target; else the
|
|
46
59
|
* zero-config cascade unix (same host) → TCP (host derived from ODB_LITE_URL). */
|
|
47
60
|
private candidates;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"odblite-socket.d.ts","sourceRoot":"","sources":["../../../src/database/adapters/odblite-socket.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,eAAe,EAAkC,MAAM,UAAU,CAAA;AAExG,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"odblite-socket.d.ts","sourceRoot":"","sources":["../../../src/database/adapters/odblite-socket.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,eAAe,EAAkC,MAAM,UAAU,CAAA;AAExG,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAA;AA0KjE,MAAM,WAAW,mBAAoB,SAAQ,YAAY;CAAG;AAE5D,qBAAa,oBAAqB,YAAW,eAAe;IAC1D,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAmB;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,MAAM,EAAE,mBAAmB;IAIjC,OAAO,CAAC,MAAM,EAAE,GAAG,GAAG,OAAO,CAAC,UAAU,CAAC;IAMzC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3B,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;CAmBpC;AAED;;mCAEmC;AACnC,eAAO,MAAM,sBAAsB,IAAI,CAAA;AAEvC,sFAAsF;AACtF,eAAO,MAAM,mBAAmB,sBAAsB,CAAA;AAEtD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,CAAC,EAAE,MAAM,GAAG,YAAY,CAO1D;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,kBAAmB,YAAW,eAAe;IACxD,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA6C;IAClE,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,WAAW,CAAI;gBAEX,IAAI,EAAE;QAAE,MAAM,CAAC,EAAE,mBAAmB,CAAC;QAAC,IAAI,EAAE,GAAG,CAAA;KAAE;YAK/C,IAAI;IAWlB,oEAAoE;YACtD,SAAS;IAgBvB;;8CAE0C;YAC5B,YAAY;IAY1B;uFACmF;IACnF,OAAO,CAAC,UAAU;IAgBZ,OAAO,CAAC,MAAM,EAAE,GAAG,GAAG,OAAO,CAAC,UAAU,CAAC;IAIzC,UAAU,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAG5C,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;CAGpC"}
|
|
@@ -6,6 +6,11 @@ import { type StreamRequest, type StreamResult } from './protocol';
|
|
|
6
6
|
*
|
|
7
7
|
* Bun-only (uses `Bun.connect`). app-router runs on Bun; the HTTP `odblite`
|
|
8
8
|
* backend remains the universal fallback for non-Bun environments.
|
|
9
|
+
*
|
|
10
|
+
* Liveness invariant: an in-flight request must ALWAYS terminate. If the
|
|
11
|
+
* connection drops (close/error) or a reply never arrives, the pending promise
|
|
12
|
+
* is REJECTED — never left hanging. A hung pending request would wedge the
|
|
13
|
+
* caller's transaction, hold its write lane, and cascade into a 30s timeout.
|
|
9
14
|
*/
|
|
10
15
|
export interface SocketTarget {
|
|
11
16
|
unix?: string;
|
|
@@ -19,10 +24,18 @@ export declare class SocketClient {
|
|
|
19
24
|
private writer;
|
|
20
25
|
private readonly pending;
|
|
21
26
|
private nextId;
|
|
27
|
+
private nextStreamId;
|
|
22
28
|
private ready;
|
|
23
29
|
constructor(target: SocketTarget);
|
|
30
|
+
/** Reject + clear every in-flight request — called on disconnect/error/close so a
|
|
31
|
+
* dropped connection FAILS its requests (the caller retries) instead of hanging. */
|
|
32
|
+
private failPending;
|
|
24
33
|
private connect;
|
|
25
|
-
|
|
34
|
+
/** Allocate a fresh stream id for an interactive transaction (unique per connection). The server
|
|
35
|
+
* pins one SQLite connection per stream id, so all of a tx's statements run on it (read-your-own-
|
|
36
|
+
* writes). The id is carried in every frame of that tx via pipeline()'s `stream` arg. */
|
|
37
|
+
allocStream(): number;
|
|
38
|
+
pipeline(tenant: string, requests: StreamRequest[], stream?: number): Promise<StreamResult[]>;
|
|
26
39
|
close(): void;
|
|
27
40
|
}
|
|
28
41
|
//# sourceMappingURL=client.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../../src/database/adapters/socket/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,YAAY,CAAA;AAE5F
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../../src/database/adapters/socket/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,YAAY,CAAA;AAE5F;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAaD,qBAAa,YAAY;IASX,OAAO,CAAC,QAAQ,CAAC,MAAM;IARnC,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IACrD,OAAO,CAAC,MAAM,CAAI;IAClB,OAAO,CAAC,YAAY,CAAI;IACxB,OAAO,CAAC,KAAK,CAAe;gBAEC,MAAM,EAAE,YAAY;IAOjD;yFACqF;IACrF,OAAO,CAAC,WAAW;YAQL,OAAO;IAuCrB;;8FAE0F;IAC1F,WAAW,IAAI,MAAM;IAIf,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAwBnG,KAAK,IAAI,IAAI;CAKd"}
|
|
@@ -95,6 +95,8 @@ export type StreamRequest = {
|
|
|
95
95
|
sql: string;
|
|
96
96
|
} | {
|
|
97
97
|
type: 'get_autocommit';
|
|
98
|
+
} | {
|
|
99
|
+
type: 'get_version';
|
|
98
100
|
} | {
|
|
99
101
|
type: 'close';
|
|
100
102
|
};
|
|
@@ -111,6 +113,7 @@ export interface AtomicStep {
|
|
|
111
113
|
expect?: 'one' | 'atLeastOne' | number;
|
|
112
114
|
}
|
|
113
115
|
export declare function toBatch(steps: AtomicStep[]): Batch;
|
|
116
|
+
export declare function mightReturnRows(sql: string): boolean;
|
|
114
117
|
interface Writable {
|
|
115
118
|
write(data: Uint8Array): number;
|
|
116
119
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../../../src/database/adapters/socket/protocol.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../../../src/database/adapters/socket/protocol.ts"],"names":[],"mappings":"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"}
|
package/dist/index.cjs
CHANGED
|
@@ -2204,6 +2204,10 @@ var __webpack_exports__ = {};
|
|
|
2204
2204
|
}))
|
|
2205
2205
|
};
|
|
2206
2206
|
}
|
|
2207
|
+
const READ_ONLY_RE = /^\s*(?:SELECT|EXPLAIN|VALUES)\b/i;
|
|
2208
|
+
function mightReturnRows(sql) {
|
|
2209
|
+
return READ_ONLY_RE.test(sql) || /\bRETURNING\b/i.test(sql);
|
|
2210
|
+
}
|
|
2207
2211
|
class FrameWriter {
|
|
2208
2212
|
socket;
|
|
2209
2213
|
pending = Buffer.alloc(0);
|
|
@@ -2248,6 +2252,7 @@ var __webpack_exports__ = {};
|
|
|
2248
2252
|
return out;
|
|
2249
2253
|
}
|
|
2250
2254
|
}
|
|
2255
|
+
const REQUEST_TIMEOUT_MS = Number(globalThis.process?.env?.ODB_SOCKET_REQUEST_TIMEOUT_MS) || 30000;
|
|
2251
2256
|
class SocketClient {
|
|
2252
2257
|
target;
|
|
2253
2258
|
socket;
|
|
@@ -2255,12 +2260,20 @@ var __webpack_exports__ = {};
|
|
|
2255
2260
|
writer;
|
|
2256
2261
|
pending = new Map();
|
|
2257
2262
|
nextId = 1;
|
|
2263
|
+
nextStreamId = 1;
|
|
2258
2264
|
ready;
|
|
2259
2265
|
constructor(target){
|
|
2260
2266
|
this.target = target;
|
|
2261
2267
|
this.ready = this.connect();
|
|
2262
2268
|
this.ready.catch(()=>{});
|
|
2263
2269
|
}
|
|
2270
|
+
failPending(err) {
|
|
2271
|
+
for (const p of this.pending.values()){
|
|
2272
|
+
clearTimeout(p.timer);
|
|
2273
|
+
p.reject(err);
|
|
2274
|
+
}
|
|
2275
|
+
this.pending.clear();
|
|
2276
|
+
}
|
|
2264
2277
|
async connect() {
|
|
2265
2278
|
const Bun = globalThis.Bun;
|
|
2266
2279
|
if (!Bun?.connect) throw new Error('odblite-socket backend requires Bun (Bun.connect)');
|
|
@@ -2276,40 +2289,67 @@ var __webpack_exports__ = {};
|
|
|
2276
2289
|
data: (_s, chunk)=>{
|
|
2277
2290
|
for (const frame of this.reader.push(chunk)){
|
|
2278
2291
|
const msg = JSON.parse(frame.toString('utf8'));
|
|
2279
|
-
const
|
|
2280
|
-
if (
|
|
2292
|
+
const p = this.pending.get(msg.id);
|
|
2293
|
+
if (p) {
|
|
2281
2294
|
this.pending.delete(msg.id);
|
|
2282
|
-
|
|
2295
|
+
clearTimeout(p.timer);
|
|
2296
|
+
p.resolve(msg.results);
|
|
2283
2297
|
}
|
|
2284
2298
|
}
|
|
2285
2299
|
},
|
|
2286
2300
|
drain: (_s)=>this.writer.flush(),
|
|
2287
2301
|
close: ()=>{
|
|
2288
2302
|
this.socket = void 0;
|
|
2303
|
+
this.reader = new FrameReader();
|
|
2304
|
+
this.failPending(new Error('odblite socket disconnected — request not completed'));
|
|
2305
|
+
},
|
|
2306
|
+
error: (_s, err)=>{
|
|
2307
|
+
this.socket = void 0;
|
|
2308
|
+
this.reader = new FrameReader();
|
|
2309
|
+
this.failPending(err instanceof Error ? err : new Error(`odblite socket error: ${err}`));
|
|
2289
2310
|
}
|
|
2290
2311
|
}
|
|
2291
2312
|
});
|
|
2292
2313
|
this.writer = new FrameWriter(this.socket);
|
|
2293
2314
|
}
|
|
2294
|
-
|
|
2315
|
+
allocStream() {
|
|
2316
|
+
return this.nextStreamId++;
|
|
2317
|
+
}
|
|
2318
|
+
async pipeline(tenant, requests, stream) {
|
|
2319
|
+
await this.ready;
|
|
2295
2320
|
if (!this.socket) {
|
|
2296
2321
|
this.ready = this.connect();
|
|
2297
2322
|
this.ready.catch(()=>{});
|
|
2323
|
+
await this.ready;
|
|
2298
2324
|
}
|
|
2299
|
-
await this.ready;
|
|
2300
2325
|
const id = this.nextId++;
|
|
2301
|
-
return new Promise((resolve)=>{
|
|
2302
|
-
|
|
2303
|
-
|
|
2326
|
+
return new Promise((resolve, reject)=>{
|
|
2327
|
+
const timer = setTimeout(()=>{
|
|
2328
|
+
if (this.pending.delete(id)) reject(new Error(`odblite socket request ${id} timed out after ${REQUEST_TIMEOUT_MS}ms`));
|
|
2329
|
+
}, REQUEST_TIMEOUT_MS);
|
|
2330
|
+
timer.unref?.();
|
|
2331
|
+
this.pending.set(id, {
|
|
2332
|
+
resolve,
|
|
2333
|
+
reject,
|
|
2334
|
+
timer
|
|
2335
|
+
});
|
|
2336
|
+
const payload = null != stream ? {
|
|
2337
|
+
id,
|
|
2338
|
+
tenant,
|
|
2339
|
+
stream,
|
|
2340
|
+
requests
|
|
2341
|
+
} : {
|
|
2304
2342
|
id,
|
|
2305
2343
|
tenant,
|
|
2306
2344
|
requests
|
|
2307
|
-
}
|
|
2345
|
+
};
|
|
2346
|
+
this.writer.send(Buffer.from(JSON.stringify(payload), 'utf8'));
|
|
2308
2347
|
});
|
|
2309
2348
|
}
|
|
2310
2349
|
close() {
|
|
2311
2350
|
this.socket?.end?.();
|
|
2312
2351
|
this.socket = void 0;
|
|
2352
|
+
this.failPending(new Error('odblite socket closed'));
|
|
2313
2353
|
}
|
|
2314
2354
|
}
|
|
2315
2355
|
function toQueryResult(res) {
|
|
@@ -2333,21 +2373,22 @@ var __webpack_exports__ = {};
|
|
|
2333
2373
|
class OdbliteSocketConnection {
|
|
2334
2374
|
client;
|
|
2335
2375
|
tenant;
|
|
2376
|
+
streamId;
|
|
2336
2377
|
databaseName;
|
|
2337
2378
|
databaseHash;
|
|
2338
2379
|
sql;
|
|
2339
|
-
|
|
2340
|
-
constructor(client, tenant,
|
|
2380
|
+
beginPending = false;
|
|
2381
|
+
constructor(client, tenant, streamId){
|
|
2341
2382
|
this.client = client;
|
|
2342
2383
|
this.tenant = tenant;
|
|
2384
|
+
this.streamId = streamId;
|
|
2343
2385
|
this.databaseName = tenant;
|
|
2344
2386
|
this.databaseHash = tenant;
|
|
2345
|
-
this.buffer = buffer;
|
|
2346
2387
|
const self = this;
|
|
2347
2388
|
this.sql = function(strings, ...values) {
|
|
2348
2389
|
if (strings && 'object' == typeof strings && 'raw' in strings) {
|
|
2349
2390
|
const text = strings.reduce((a, s, i)=>a + s + (i < values.length ? '?' : ''), '');
|
|
2350
|
-
const returns =
|
|
2391
|
+
const returns = mightReturnRows(text);
|
|
2351
2392
|
return (returns ? self.query(text, values) : self.execute(text, values)).then((r)=>r.rows);
|
|
2352
2393
|
}
|
|
2353
2394
|
throw new Error('sql() fragment helper not implemented in odblite-socket');
|
|
@@ -2358,7 +2399,32 @@ var __webpack_exports__ = {};
|
|
|
2358
2399
|
if (!res) throw new Error('odblite-socket: empty pipeline response');
|
|
2359
2400
|
return res;
|
|
2360
2401
|
}
|
|
2402
|
+
async streamExec(sql, params = []) {
|
|
2403
|
+
const reqs = [];
|
|
2404
|
+
if (this.beginPending) {
|
|
2405
|
+
reqs.push({
|
|
2406
|
+
type: 'execute',
|
|
2407
|
+
stmt: {
|
|
2408
|
+
sql: 'BEGIN'
|
|
2409
|
+
}
|
|
2410
|
+
});
|
|
2411
|
+
this.beginPending = false;
|
|
2412
|
+
}
|
|
2413
|
+
reqs.push({
|
|
2414
|
+
type: 'execute',
|
|
2415
|
+
stmt: {
|
|
2416
|
+
sql,
|
|
2417
|
+
args: params.map(toHrana)
|
|
2418
|
+
}
|
|
2419
|
+
});
|
|
2420
|
+
const results = await this.client.pipeline(this.tenant, reqs, this.streamId);
|
|
2421
|
+
if (reqs.length > 1 && results[0]?.type === 'error') throw new Error(results[0].error.message);
|
|
2422
|
+
const res = results[results.length - 1];
|
|
2423
|
+
if (!res) throw new Error('odblite-socket: empty stream response');
|
|
2424
|
+
return toQueryResult(res);
|
|
2425
|
+
}
|
|
2361
2426
|
async query(sql, params = []) {
|
|
2427
|
+
if (null != this.streamId) return await this.streamExec(sql, params);
|
|
2362
2428
|
return toQueryResult(await this.one([
|
|
2363
2429
|
{
|
|
2364
2430
|
type: 'execute',
|
|
@@ -2372,16 +2438,7 @@ var __webpack_exports__ = {};
|
|
|
2372
2438
|
async execute(sql, params = []) {
|
|
2373
2439
|
const s = 'string' == typeof sql ? sql : sql.sql;
|
|
2374
2440
|
const p = 'string' == typeof sql ? params : sql.args ?? [];
|
|
2375
|
-
if (this.
|
|
2376
|
-
this.buffer.push({
|
|
2377
|
-
sql: s,
|
|
2378
|
-
params: p
|
|
2379
|
-
});
|
|
2380
|
-
return {
|
|
2381
|
-
rows: [],
|
|
2382
|
-
rowsAffected: 0
|
|
2383
|
-
};
|
|
2384
|
-
}
|
|
2441
|
+
if (null != this.streamId) return this.streamExec(s, p);
|
|
2385
2442
|
return toQueryResult(await this.one([
|
|
2386
2443
|
{
|
|
2387
2444
|
type: 'execute',
|
|
@@ -2410,24 +2467,33 @@ var __webpack_exports__ = {};
|
|
|
2410
2467
|
};
|
|
2411
2468
|
}
|
|
2412
2469
|
async transaction(fn) {
|
|
2413
|
-
if (this.
|
|
2414
|
-
const
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
const
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2470
|
+
if (null != this.streamId) return fn(this);
|
|
2471
|
+
const tx = new OdbliteSocketConnection(this.client, this.tenant, this.client.allocStream());
|
|
2472
|
+
tx.beginPending = true;
|
|
2473
|
+
try {
|
|
2474
|
+
const out = await fn(tx);
|
|
2475
|
+
if (!tx.beginPending) await tx.streamExec('COMMIT');
|
|
2476
|
+
return out;
|
|
2477
|
+
} catch (e) {
|
|
2478
|
+
if (!tx.beginPending) await tx.streamExec('ROLLBACK').catch(()=>{});
|
|
2479
|
+
throw e;
|
|
2423
2480
|
}
|
|
2424
|
-
return out;
|
|
2425
2481
|
}
|
|
2426
2482
|
begin(fn) {
|
|
2427
2483
|
return this.transaction(fn);
|
|
2428
2484
|
}
|
|
2429
|
-
savepoint(fn) {
|
|
2430
|
-
return this.transaction(fn);
|
|
2485
|
+
async savepoint(fn) {
|
|
2486
|
+
if (null == this.streamId) return this.transaction(fn);
|
|
2487
|
+
const name = `sp_${this.client.allocStream()}`;
|
|
2488
|
+
await this.streamExec(`SAVEPOINT ${name}`);
|
|
2489
|
+
try {
|
|
2490
|
+
const out = await fn(this);
|
|
2491
|
+
await this.streamExec(`RELEASE SAVEPOINT ${name}`);
|
|
2492
|
+
return out;
|
|
2493
|
+
} catch (e) {
|
|
2494
|
+
await this.streamExec(`ROLLBACK TO SAVEPOINT ${name}`).catch(()=>{});
|
|
2495
|
+
throw e;
|
|
2496
|
+
}
|
|
2431
2497
|
}
|
|
2432
2498
|
async ensureNamespaces(namespaces) {
|
|
2433
2499
|
return {
|
|
@@ -2465,20 +2531,26 @@ var __webpack_exports__ = {};
|
|
|
2465
2531
|
}
|
|
2466
2532
|
async isHealthy() {
|
|
2467
2533
|
try {
|
|
2468
|
-
await Promise.race([
|
|
2534
|
+
const res = await Promise.race([
|
|
2469
2535
|
this.client.pipeline('_health', [
|
|
2470
2536
|
{
|
|
2471
|
-
type: '
|
|
2537
|
+
type: 'get_version'
|
|
2472
2538
|
}
|
|
2473
2539
|
]),
|
|
2474
2540
|
new Promise((_, rej)=>setTimeout(()=>rej(new Error('probe timeout')), 1500))
|
|
2475
2541
|
]);
|
|
2542
|
+
const proto = res?.[0]?.response?.protocol;
|
|
2543
|
+
if (proto !== CLIENT_SOCKET_PROTOCOL) {
|
|
2544
|
+
console.warn(`[odb-client] socket protocol mismatch (server=${proto ?? 'none'}, client=${CLIENT_SOCKET_PROTOCOL}) → using HTTP`);
|
|
2545
|
+
return false;
|
|
2546
|
+
}
|
|
2476
2547
|
return true;
|
|
2477
2548
|
} catch {
|
|
2478
2549
|
return false;
|
|
2479
2550
|
}
|
|
2480
2551
|
}
|
|
2481
2552
|
}
|
|
2553
|
+
const CLIENT_SOCKET_PROTOCOL = 2;
|
|
2482
2554
|
const DEFAULT_UNIX_SOCKET = '/tmp/odblite.sock';
|
|
2483
2555
|
function parseSocketTarget(s) {
|
|
2484
2556
|
const v = (s ?? '').trim();
|
|
@@ -2499,13 +2571,26 @@ var __webpack_exports__ = {};
|
|
|
2499
2571
|
}
|
|
2500
2572
|
class OdbliteAutoAdapter {
|
|
2501
2573
|
type = 'odblite';
|
|
2574
|
+
opts;
|
|
2502
2575
|
chosen;
|
|
2576
|
+
onHttp = false;
|
|
2577
|
+
nextRetryAt = 0;
|
|
2503
2578
|
constructor(opts){
|
|
2504
|
-
this.
|
|
2579
|
+
this.opts = opts;
|
|
2580
|
+
this.chosen = this.pick();
|
|
2505
2581
|
}
|
|
2506
|
-
async pick(
|
|
2582
|
+
async pick() {
|
|
2507
2583
|
const hasBun = void 0 !== globalThis.Bun && !!globalThis.Bun.connect;
|
|
2508
|
-
if (hasBun)
|
|
2584
|
+
if (hasBun) {
|
|
2585
|
+
const socket = await this.trySocket();
|
|
2586
|
+
if (socket) return socket;
|
|
2587
|
+
}
|
|
2588
|
+
this.onHttp = true;
|
|
2589
|
+
console.log('[odb-client] transport → HTTP (no router_v2 socket reachable)');
|
|
2590
|
+
return new ODBLiteAdapter(this.opts.http);
|
|
2591
|
+
}
|
|
2592
|
+
async trySocket() {
|
|
2593
|
+
for (const target of this.candidates())try {
|
|
2509
2594
|
const socket = new OdbliteSocketAdapter(target);
|
|
2510
2595
|
if (await socket.isHealthy()) {
|
|
2511
2596
|
console.log(`[odb-client] transport → SOCKET (router_v2 @ ${target.unix ?? `${target.hostname}:${target.port}`})`);
|
|
@@ -2513,12 +2598,23 @@ var __webpack_exports__ = {};
|
|
|
2513
2598
|
}
|
|
2514
2599
|
await socket.disconnect();
|
|
2515
2600
|
} catch {}
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2601
|
+
return null;
|
|
2602
|
+
}
|
|
2603
|
+
async maybeUpgrade() {
|
|
2604
|
+
if (!this.onHttp) return;
|
|
2605
|
+
const now = Date.now();
|
|
2606
|
+
if (now < this.nextRetryAt) return;
|
|
2607
|
+
this.nextRetryAt = now + 15000;
|
|
2608
|
+
const socket = await this.trySocket();
|
|
2609
|
+
if (socket) {
|
|
2610
|
+
this.chosen = Promise.resolve(socket);
|
|
2611
|
+
this.onHttp = false;
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
candidates() {
|
|
2615
|
+
const { socket, http } = this.opts;
|
|
2616
|
+
if (socket?.unix || socket?.port) return [
|
|
2617
|
+
socket
|
|
2522
2618
|
];
|
|
2523
2619
|
const env = globalThis.process?.env?.ODB_SOCKET;
|
|
2524
2620
|
if (env) return [
|
|
@@ -2530,7 +2626,7 @@ var __webpack_exports__ = {};
|
|
|
2530
2626
|
}
|
|
2531
2627
|
];
|
|
2532
2628
|
try {
|
|
2533
|
-
const host = new URL(
|
|
2629
|
+
const host = new URL(http?.serviceUrl).hostname;
|
|
2534
2630
|
const port = Number(globalThis.process?.env?.ODB_SOCKET_PORT ?? 8787);
|
|
2535
2631
|
if (host) out.push({
|
|
2536
2632
|
hostname: host,
|
|
@@ -2540,6 +2636,7 @@ var __webpack_exports__ = {};
|
|
|
2540
2636
|
return out;
|
|
2541
2637
|
}
|
|
2542
2638
|
async connect(config) {
|
|
2639
|
+
await this.maybeUpgrade();
|
|
2543
2640
|
return (await this.chosen).connect(config);
|
|
2544
2641
|
}
|
|
2545
2642
|
async disconnect(tenantId) {
|
package/dist/index.js
CHANGED
|
@@ -2114,6 +2114,10 @@ function toBatch(steps) {
|
|
|
2114
2114
|
}))
|
|
2115
2115
|
};
|
|
2116
2116
|
}
|
|
2117
|
+
const READ_ONLY_RE = /^\s*(?:SELECT|EXPLAIN|VALUES)\b/i;
|
|
2118
|
+
function mightReturnRows(sql) {
|
|
2119
|
+
return READ_ONLY_RE.test(sql) || /\bRETURNING\b/i.test(sql);
|
|
2120
|
+
}
|
|
2117
2121
|
class FrameWriter {
|
|
2118
2122
|
socket;
|
|
2119
2123
|
pending = Buffer.alloc(0);
|
|
@@ -2158,6 +2162,7 @@ class FrameReader {
|
|
|
2158
2162
|
return out;
|
|
2159
2163
|
}
|
|
2160
2164
|
}
|
|
2165
|
+
const REQUEST_TIMEOUT_MS = Number(globalThis.process?.env?.ODB_SOCKET_REQUEST_TIMEOUT_MS) || 30000;
|
|
2161
2166
|
class SocketClient {
|
|
2162
2167
|
target;
|
|
2163
2168
|
socket;
|
|
@@ -2165,12 +2170,20 @@ class SocketClient {
|
|
|
2165
2170
|
writer;
|
|
2166
2171
|
pending = new Map();
|
|
2167
2172
|
nextId = 1;
|
|
2173
|
+
nextStreamId = 1;
|
|
2168
2174
|
ready;
|
|
2169
2175
|
constructor(target){
|
|
2170
2176
|
this.target = target;
|
|
2171
2177
|
this.ready = this.connect();
|
|
2172
2178
|
this.ready.catch(()=>{});
|
|
2173
2179
|
}
|
|
2180
|
+
failPending(err) {
|
|
2181
|
+
for (const p of this.pending.values()){
|
|
2182
|
+
clearTimeout(p.timer);
|
|
2183
|
+
p.reject(err);
|
|
2184
|
+
}
|
|
2185
|
+
this.pending.clear();
|
|
2186
|
+
}
|
|
2174
2187
|
async connect() {
|
|
2175
2188
|
const Bun = globalThis.Bun;
|
|
2176
2189
|
if (!Bun?.connect) throw new Error('odblite-socket backend requires Bun (Bun.connect)');
|
|
@@ -2186,40 +2199,67 @@ class SocketClient {
|
|
|
2186
2199
|
data: (_s, chunk)=>{
|
|
2187
2200
|
for (const frame of this.reader.push(chunk)){
|
|
2188
2201
|
const msg = JSON.parse(frame.toString('utf8'));
|
|
2189
|
-
const
|
|
2190
|
-
if (
|
|
2202
|
+
const p = this.pending.get(msg.id);
|
|
2203
|
+
if (p) {
|
|
2191
2204
|
this.pending.delete(msg.id);
|
|
2192
|
-
|
|
2205
|
+
clearTimeout(p.timer);
|
|
2206
|
+
p.resolve(msg.results);
|
|
2193
2207
|
}
|
|
2194
2208
|
}
|
|
2195
2209
|
},
|
|
2196
2210
|
drain: (_s)=>this.writer.flush(),
|
|
2197
2211
|
close: ()=>{
|
|
2198
2212
|
this.socket = void 0;
|
|
2213
|
+
this.reader = new FrameReader();
|
|
2214
|
+
this.failPending(new Error('odblite socket disconnected — request not completed'));
|
|
2215
|
+
},
|
|
2216
|
+
error: (_s, err)=>{
|
|
2217
|
+
this.socket = void 0;
|
|
2218
|
+
this.reader = new FrameReader();
|
|
2219
|
+
this.failPending(err instanceof Error ? err : new Error(`odblite socket error: ${err}`));
|
|
2199
2220
|
}
|
|
2200
2221
|
}
|
|
2201
2222
|
});
|
|
2202
2223
|
this.writer = new FrameWriter(this.socket);
|
|
2203
2224
|
}
|
|
2204
|
-
|
|
2225
|
+
allocStream() {
|
|
2226
|
+
return this.nextStreamId++;
|
|
2227
|
+
}
|
|
2228
|
+
async pipeline(tenant, requests, stream) {
|
|
2229
|
+
await this.ready;
|
|
2205
2230
|
if (!this.socket) {
|
|
2206
2231
|
this.ready = this.connect();
|
|
2207
2232
|
this.ready.catch(()=>{});
|
|
2233
|
+
await this.ready;
|
|
2208
2234
|
}
|
|
2209
|
-
await this.ready;
|
|
2210
2235
|
const id = this.nextId++;
|
|
2211
|
-
return new Promise((resolve)=>{
|
|
2212
|
-
|
|
2213
|
-
|
|
2236
|
+
return new Promise((resolve, reject)=>{
|
|
2237
|
+
const timer = setTimeout(()=>{
|
|
2238
|
+
if (this.pending.delete(id)) reject(new Error(`odblite socket request ${id} timed out after ${REQUEST_TIMEOUT_MS}ms`));
|
|
2239
|
+
}, REQUEST_TIMEOUT_MS);
|
|
2240
|
+
timer.unref?.();
|
|
2241
|
+
this.pending.set(id, {
|
|
2242
|
+
resolve,
|
|
2243
|
+
reject,
|
|
2244
|
+
timer
|
|
2245
|
+
});
|
|
2246
|
+
const payload = null != stream ? {
|
|
2247
|
+
id,
|
|
2248
|
+
tenant,
|
|
2249
|
+
stream,
|
|
2250
|
+
requests
|
|
2251
|
+
} : {
|
|
2214
2252
|
id,
|
|
2215
2253
|
tenant,
|
|
2216
2254
|
requests
|
|
2217
|
-
}
|
|
2255
|
+
};
|
|
2256
|
+
this.writer.send(Buffer.from(JSON.stringify(payload), 'utf8'));
|
|
2218
2257
|
});
|
|
2219
2258
|
}
|
|
2220
2259
|
close() {
|
|
2221
2260
|
this.socket?.end?.();
|
|
2222
2261
|
this.socket = void 0;
|
|
2262
|
+
this.failPending(new Error('odblite socket closed'));
|
|
2223
2263
|
}
|
|
2224
2264
|
}
|
|
2225
2265
|
function toQueryResult(res) {
|
|
@@ -2243,21 +2283,22 @@ function toQueryResult(res) {
|
|
|
2243
2283
|
class OdbliteSocketConnection {
|
|
2244
2284
|
client;
|
|
2245
2285
|
tenant;
|
|
2286
|
+
streamId;
|
|
2246
2287
|
databaseName;
|
|
2247
2288
|
databaseHash;
|
|
2248
2289
|
sql;
|
|
2249
|
-
|
|
2250
|
-
constructor(client, tenant,
|
|
2290
|
+
beginPending = false;
|
|
2291
|
+
constructor(client, tenant, streamId){
|
|
2251
2292
|
this.client = client;
|
|
2252
2293
|
this.tenant = tenant;
|
|
2294
|
+
this.streamId = streamId;
|
|
2253
2295
|
this.databaseName = tenant;
|
|
2254
2296
|
this.databaseHash = tenant;
|
|
2255
|
-
this.buffer = buffer;
|
|
2256
2297
|
const self = this;
|
|
2257
2298
|
this.sql = function(strings, ...values) {
|
|
2258
2299
|
if (strings && 'object' == typeof strings && 'raw' in strings) {
|
|
2259
2300
|
const text = strings.reduce((a, s, i)=>a + s + (i < values.length ? '?' : ''), '');
|
|
2260
|
-
const returns =
|
|
2301
|
+
const returns = mightReturnRows(text);
|
|
2261
2302
|
return (returns ? self.query(text, values) : self.execute(text, values)).then((r)=>r.rows);
|
|
2262
2303
|
}
|
|
2263
2304
|
throw new Error('sql() fragment helper not implemented in odblite-socket');
|
|
@@ -2268,7 +2309,32 @@ class OdbliteSocketConnection {
|
|
|
2268
2309
|
if (!res) throw new Error('odblite-socket: empty pipeline response');
|
|
2269
2310
|
return res;
|
|
2270
2311
|
}
|
|
2312
|
+
async streamExec(sql, params = []) {
|
|
2313
|
+
const reqs = [];
|
|
2314
|
+
if (this.beginPending) {
|
|
2315
|
+
reqs.push({
|
|
2316
|
+
type: 'execute',
|
|
2317
|
+
stmt: {
|
|
2318
|
+
sql: 'BEGIN'
|
|
2319
|
+
}
|
|
2320
|
+
});
|
|
2321
|
+
this.beginPending = false;
|
|
2322
|
+
}
|
|
2323
|
+
reqs.push({
|
|
2324
|
+
type: 'execute',
|
|
2325
|
+
stmt: {
|
|
2326
|
+
sql,
|
|
2327
|
+
args: params.map(toHrana)
|
|
2328
|
+
}
|
|
2329
|
+
});
|
|
2330
|
+
const results = await this.client.pipeline(this.tenant, reqs, this.streamId);
|
|
2331
|
+
if (reqs.length > 1 && results[0]?.type === 'error') throw new Error(results[0].error.message);
|
|
2332
|
+
const res = results[results.length - 1];
|
|
2333
|
+
if (!res) throw new Error('odblite-socket: empty stream response');
|
|
2334
|
+
return toQueryResult(res);
|
|
2335
|
+
}
|
|
2271
2336
|
async query(sql, params = []) {
|
|
2337
|
+
if (null != this.streamId) return await this.streamExec(sql, params);
|
|
2272
2338
|
return toQueryResult(await this.one([
|
|
2273
2339
|
{
|
|
2274
2340
|
type: 'execute',
|
|
@@ -2282,16 +2348,7 @@ class OdbliteSocketConnection {
|
|
|
2282
2348
|
async execute(sql, params = []) {
|
|
2283
2349
|
const s = 'string' == typeof sql ? sql : sql.sql;
|
|
2284
2350
|
const p = 'string' == typeof sql ? params : sql.args ?? [];
|
|
2285
|
-
if (this.
|
|
2286
|
-
this.buffer.push({
|
|
2287
|
-
sql: s,
|
|
2288
|
-
params: p
|
|
2289
|
-
});
|
|
2290
|
-
return {
|
|
2291
|
-
rows: [],
|
|
2292
|
-
rowsAffected: 0
|
|
2293
|
-
};
|
|
2294
|
-
}
|
|
2351
|
+
if (null != this.streamId) return this.streamExec(s, p);
|
|
2295
2352
|
return toQueryResult(await this.one([
|
|
2296
2353
|
{
|
|
2297
2354
|
type: 'execute',
|
|
@@ -2320,24 +2377,33 @@ class OdbliteSocketConnection {
|
|
|
2320
2377
|
};
|
|
2321
2378
|
}
|
|
2322
2379
|
async transaction(fn) {
|
|
2323
|
-
if (this.
|
|
2324
|
-
const
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
const
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2380
|
+
if (null != this.streamId) return fn(this);
|
|
2381
|
+
const tx = new OdbliteSocketConnection(this.client, this.tenant, this.client.allocStream());
|
|
2382
|
+
tx.beginPending = true;
|
|
2383
|
+
try {
|
|
2384
|
+
const out = await fn(tx);
|
|
2385
|
+
if (!tx.beginPending) await tx.streamExec('COMMIT');
|
|
2386
|
+
return out;
|
|
2387
|
+
} catch (e) {
|
|
2388
|
+
if (!tx.beginPending) await tx.streamExec('ROLLBACK').catch(()=>{});
|
|
2389
|
+
throw e;
|
|
2333
2390
|
}
|
|
2334
|
-
return out;
|
|
2335
2391
|
}
|
|
2336
2392
|
begin(fn) {
|
|
2337
2393
|
return this.transaction(fn);
|
|
2338
2394
|
}
|
|
2339
|
-
savepoint(fn) {
|
|
2340
|
-
return this.transaction(fn);
|
|
2395
|
+
async savepoint(fn) {
|
|
2396
|
+
if (null == this.streamId) return this.transaction(fn);
|
|
2397
|
+
const name = `sp_${this.client.allocStream()}`;
|
|
2398
|
+
await this.streamExec(`SAVEPOINT ${name}`);
|
|
2399
|
+
try {
|
|
2400
|
+
const out = await fn(this);
|
|
2401
|
+
await this.streamExec(`RELEASE SAVEPOINT ${name}`);
|
|
2402
|
+
return out;
|
|
2403
|
+
} catch (e) {
|
|
2404
|
+
await this.streamExec(`ROLLBACK TO SAVEPOINT ${name}`).catch(()=>{});
|
|
2405
|
+
throw e;
|
|
2406
|
+
}
|
|
2341
2407
|
}
|
|
2342
2408
|
async ensureNamespaces(namespaces) {
|
|
2343
2409
|
return {
|
|
@@ -2375,20 +2441,26 @@ class OdbliteSocketAdapter {
|
|
|
2375
2441
|
}
|
|
2376
2442
|
async isHealthy() {
|
|
2377
2443
|
try {
|
|
2378
|
-
await Promise.race([
|
|
2444
|
+
const res = await Promise.race([
|
|
2379
2445
|
this.client.pipeline('_health', [
|
|
2380
2446
|
{
|
|
2381
|
-
type: '
|
|
2447
|
+
type: 'get_version'
|
|
2382
2448
|
}
|
|
2383
2449
|
]),
|
|
2384
2450
|
new Promise((_, rej)=>setTimeout(()=>rej(new Error('probe timeout')), 1500))
|
|
2385
2451
|
]);
|
|
2452
|
+
const proto = res?.[0]?.response?.protocol;
|
|
2453
|
+
if (proto !== CLIENT_SOCKET_PROTOCOL) {
|
|
2454
|
+
console.warn(`[odb-client] socket protocol mismatch (server=${proto ?? 'none'}, client=${CLIENT_SOCKET_PROTOCOL}) → using HTTP`);
|
|
2455
|
+
return false;
|
|
2456
|
+
}
|
|
2386
2457
|
return true;
|
|
2387
2458
|
} catch {
|
|
2388
2459
|
return false;
|
|
2389
2460
|
}
|
|
2390
2461
|
}
|
|
2391
2462
|
}
|
|
2463
|
+
const CLIENT_SOCKET_PROTOCOL = 2;
|
|
2392
2464
|
const DEFAULT_UNIX_SOCKET = '/tmp/odblite.sock';
|
|
2393
2465
|
function parseSocketTarget(s) {
|
|
2394
2466
|
const v = (s ?? '').trim();
|
|
@@ -2409,13 +2481,26 @@ function parseSocketTarget(s) {
|
|
|
2409
2481
|
}
|
|
2410
2482
|
class OdbliteAutoAdapter {
|
|
2411
2483
|
type = 'odblite';
|
|
2484
|
+
opts;
|
|
2412
2485
|
chosen;
|
|
2486
|
+
onHttp = false;
|
|
2487
|
+
nextRetryAt = 0;
|
|
2413
2488
|
constructor(opts){
|
|
2414
|
-
this.
|
|
2489
|
+
this.opts = opts;
|
|
2490
|
+
this.chosen = this.pick();
|
|
2415
2491
|
}
|
|
2416
|
-
async pick(
|
|
2492
|
+
async pick() {
|
|
2417
2493
|
const hasBun = void 0 !== globalThis.Bun && !!globalThis.Bun.connect;
|
|
2418
|
-
if (hasBun)
|
|
2494
|
+
if (hasBun) {
|
|
2495
|
+
const socket = await this.trySocket();
|
|
2496
|
+
if (socket) return socket;
|
|
2497
|
+
}
|
|
2498
|
+
this.onHttp = true;
|
|
2499
|
+
console.log('[odb-client] transport → HTTP (no router_v2 socket reachable)');
|
|
2500
|
+
return new ODBLiteAdapter(this.opts.http);
|
|
2501
|
+
}
|
|
2502
|
+
async trySocket() {
|
|
2503
|
+
for (const target of this.candidates())try {
|
|
2419
2504
|
const socket = new OdbliteSocketAdapter(target);
|
|
2420
2505
|
if (await socket.isHealthy()) {
|
|
2421
2506
|
console.log(`[odb-client] transport → SOCKET (router_v2 @ ${target.unix ?? `${target.hostname}:${target.port}`})`);
|
|
@@ -2423,12 +2508,23 @@ class OdbliteAutoAdapter {
|
|
|
2423
2508
|
}
|
|
2424
2509
|
await socket.disconnect();
|
|
2425
2510
|
} catch {}
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2511
|
+
return null;
|
|
2512
|
+
}
|
|
2513
|
+
async maybeUpgrade() {
|
|
2514
|
+
if (!this.onHttp) return;
|
|
2515
|
+
const now = Date.now();
|
|
2516
|
+
if (now < this.nextRetryAt) return;
|
|
2517
|
+
this.nextRetryAt = now + 15000;
|
|
2518
|
+
const socket = await this.trySocket();
|
|
2519
|
+
if (socket) {
|
|
2520
|
+
this.chosen = Promise.resolve(socket);
|
|
2521
|
+
this.onHttp = false;
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
candidates() {
|
|
2525
|
+
const { socket, http } = this.opts;
|
|
2526
|
+
if (socket?.unix || socket?.port) return [
|
|
2527
|
+
socket
|
|
2432
2528
|
];
|
|
2433
2529
|
const env = globalThis.process?.env?.ODB_SOCKET;
|
|
2434
2530
|
if (env) return [
|
|
@@ -2440,7 +2536,7 @@ class OdbliteAutoAdapter {
|
|
|
2440
2536
|
}
|
|
2441
2537
|
];
|
|
2442
2538
|
try {
|
|
2443
|
-
const host = new URL(
|
|
2539
|
+
const host = new URL(http?.serviceUrl).hostname;
|
|
2444
2540
|
const port = Number(globalThis.process?.env?.ODB_SOCKET_PORT ?? 8787);
|
|
2445
2541
|
if (host) out.push({
|
|
2446
2542
|
hostname: host,
|
|
@@ -2450,6 +2546,7 @@ class OdbliteAutoAdapter {
|
|
|
2450
2546
|
return out;
|
|
2451
2547
|
}
|
|
2452
2548
|
async connect(config) {
|
|
2549
|
+
await this.maybeUpgrade();
|
|
2453
2550
|
return (await this.chosen).connect(config);
|
|
2454
2551
|
}
|
|
2455
2552
|
async disconnect(tenantId) {
|
package/package.json
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
toHrana,
|
|
6
6
|
fromHrana,
|
|
7
7
|
toBatch,
|
|
8
|
+
mightReturnRows,
|
|
8
9
|
type AtomicStep,
|
|
9
10
|
type BatchResult,
|
|
10
11
|
type StmtResult,
|
|
@@ -43,21 +44,24 @@ class OdbliteSocketConnection implements Connection {
|
|
|
43
44
|
databaseName?: string
|
|
44
45
|
databaseHash?: string
|
|
45
46
|
public sql: any
|
|
46
|
-
|
|
47
|
+
/** A fresh stream tx whose BEGIN hasn't been flushed yet — it's folded into the first statement's
|
|
48
|
+
* frame to save a round-trip (and an empty tx that issues no statements sends nothing at all). */
|
|
49
|
+
private beginPending = false
|
|
47
50
|
|
|
48
51
|
constructor(
|
|
49
52
|
private readonly client: SocketClient,
|
|
50
53
|
private readonly tenant: string,
|
|
51
|
-
|
|
54
|
+
/** Set → this connection is scoped to an interactive transaction STREAM: every query/execute runs
|
|
55
|
+
* on the server's PINNED connection (read-your-own-writes), not autocommit. */
|
|
56
|
+
private readonly streamId?: number,
|
|
52
57
|
) {
|
|
53
58
|
this.databaseName = tenant
|
|
54
59
|
this.databaseHash = tenant
|
|
55
|
-
this.buffer = buffer
|
|
56
60
|
const self = this
|
|
57
61
|
this.sql = function (strings: any, ...values: any[]): any {
|
|
58
62
|
if (strings && typeof strings === 'object' && 'raw' in strings) {
|
|
59
63
|
const text = (strings as string[]).reduce((a, s, i) => a + s + (i < values.length ? '?' : ''), '')
|
|
60
|
-
const returns =
|
|
64
|
+
const returns = mightReturnRows(text)
|
|
61
65
|
return (returns ? self.query(text, values) : self.execute(text, values)).then((r: any) => r.rows)
|
|
62
66
|
}
|
|
63
67
|
throw new Error('sql() fragment helper not implemented in odblite-socket')
|
|
@@ -71,17 +75,31 @@ class OdbliteSocketConnection implements Connection {
|
|
|
71
75
|
return res
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
/** Run one statement on this connection's interactive STREAM — the server's pinned connection, so a
|
|
79
|
+
* read sees this tx's own uncommitted writes. Throws on error (incl. a poisoned COMMIT). */
|
|
80
|
+
private async streamExec(sql: string, params: any[] = []): Promise<QueryResult> {
|
|
81
|
+
const reqs: StreamRequest[] = []
|
|
82
|
+
if (this.beginPending) {
|
|
83
|
+
reqs.push({ type: 'execute', stmt: { sql: 'BEGIN' } }) // fold BEGIN into this first statement's frame
|
|
84
|
+
this.beginPending = false
|
|
85
|
+
}
|
|
86
|
+
reqs.push({ type: 'execute', stmt: { sql, args: params.map(toHrana) } })
|
|
87
|
+
const results = await this.client.pipeline(this.tenant, reqs, this.streamId)
|
|
88
|
+
if (reqs.length > 1 && results[0]?.type === 'error') throw new Error(results[0].error.message) // the folded BEGIN failed
|
|
89
|
+
const res = results[results.length - 1]
|
|
90
|
+
if (!res) throw new Error('odblite-socket: empty stream response')
|
|
91
|
+
return toQueryResult(res)
|
|
92
|
+
}
|
|
93
|
+
|
|
74
94
|
async query<T = any>(sql: string, params: any[] = []): Promise<QueryResult<T>> {
|
|
95
|
+
if (this.streamId != null) return (await this.streamExec(sql, params)) as QueryResult<T>
|
|
75
96
|
return toQueryResult(await this.one([{ type: 'execute', stmt: { sql, args: params.map(toHrana) } }])) as QueryResult<T>
|
|
76
97
|
}
|
|
77
98
|
|
|
78
99
|
async execute(sql: string | { sql: string; args?: any[] }, params: any[] = []): Promise<QueryResult> {
|
|
79
100
|
const s = typeof sql === 'string' ? sql : sql.sql
|
|
80
101
|
const p = typeof sql === 'string' ? params : (sql.args ?? [])
|
|
81
|
-
if (this.
|
|
82
|
-
this.buffer.push({ sql: s, params: p }) // deferred → flushed as one batch on commit
|
|
83
|
-
return { rows: [], rowsAffected: 0 }
|
|
84
|
-
}
|
|
102
|
+
if (this.streamId != null) return this.streamExec(s, p) // on the pinned stream — runs immediately, NOT buffered
|
|
85
103
|
return toQueryResult(await this.one([{ type: 'execute', stmt: { sql: s, args: p.map(toHrana) } }]))
|
|
86
104
|
}
|
|
87
105
|
|
|
@@ -100,22 +118,42 @@ class OdbliteSocketConnection implements Connection {
|
|
|
100
118
|
}
|
|
101
119
|
}
|
|
102
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Interactive transaction over a STREAM (one pinned server connection): BEGIN → fn's reads+writes →
|
|
123
|
+
* COMMIT, all on the same connection. Reads see their own writes, and the engine enforces atomicity
|
|
124
|
+
* (a failed statement poisons the tx → a later COMMIT is forced to ROLLBACK). Replaces the old
|
|
125
|
+
* buffered model, which couldn't read its own writes and only flushed at commit.
|
|
126
|
+
*/
|
|
103
127
|
async transaction<T>(fn: (tx: Connection) => Promise<T>): Promise<T> {
|
|
104
|
-
if (this.
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
128
|
+
if (this.streamId != null) return fn(this) // nested → join the already-open stream
|
|
129
|
+
const tx = new OdbliteSocketConnection(this.client, this.tenant, this.client.allocStream())
|
|
130
|
+
tx.beginPending = true // BEGIN folds into the first statement's frame (one fewer round-trip)
|
|
131
|
+
try {
|
|
132
|
+
const out = await fn(tx)
|
|
133
|
+
if (!tx.beginPending) await tx.streamExec('COMMIT') // a statement ran → BEGIN was sent → COMMIT it
|
|
134
|
+
return out // else fn issued no statements → empty tx, BEGIN never sent, zero round-trips
|
|
135
|
+
} catch (e) {
|
|
136
|
+
if (!tx.beginPending) await tx.streamExec('ROLLBACK').catch(() => {}) // best-effort; server also reaps
|
|
137
|
+
throw e
|
|
111
138
|
}
|
|
112
|
-
return out
|
|
113
139
|
}
|
|
114
140
|
begin<T>(fn: (tx: Connection) => Promise<T>): Promise<T> {
|
|
115
141
|
return this.transaction(fn)
|
|
116
142
|
}
|
|
117
|
-
|
|
118
|
-
|
|
143
|
+
/** Nested partial-rollback scope. Inside a stream → a real SAVEPOINT (a ROLLBACK TO SAVEPOINT clears
|
|
144
|
+
* the engine's poison); at top level → a full transaction. */
|
|
145
|
+
async savepoint<T>(fn: (tx: Connection) => Promise<T>): Promise<T> {
|
|
146
|
+
if (this.streamId == null) return this.transaction(fn)
|
|
147
|
+
const name = `sp_${this.client.allocStream()}`
|
|
148
|
+
await this.streamExec(`SAVEPOINT ${name}`)
|
|
149
|
+
try {
|
|
150
|
+
const out = await fn(this)
|
|
151
|
+
await this.streamExec(`RELEASE SAVEPOINT ${name}`)
|
|
152
|
+
return out
|
|
153
|
+
} catch (e) {
|
|
154
|
+
await this.streamExec(`ROLLBACK TO SAVEPOINT ${name}`).catch(() => {})
|
|
155
|
+
throw e
|
|
156
|
+
}
|
|
119
157
|
}
|
|
120
158
|
|
|
121
159
|
// router_v2 auto-ATTACHes a tenant's namespaces on open → these are no-ops.
|
|
@@ -154,10 +192,18 @@ export class OdbliteSocketAdapter implements DatabaseAdapter {
|
|
|
154
192
|
|
|
155
193
|
async isHealthy(): Promise<boolean> {
|
|
156
194
|
try {
|
|
157
|
-
await Promise.race([
|
|
158
|
-
this.client.pipeline('_health', [{ type: '
|
|
195
|
+
const res: any = await Promise.race([
|
|
196
|
+
this.client.pipeline('_health', [{ type: 'get_version' }]),
|
|
159
197
|
new Promise((_, rej) => setTimeout(() => rej(new Error('probe timeout')), 1500)),
|
|
160
198
|
])
|
|
199
|
+
// Connect-time HANDSHAKE: adopt the socket only if the server speaks our wire protocol. A different
|
|
200
|
+
// generation — or a server too old to answer get_version — falls back to HTTP instead of letting a
|
|
201
|
+
// mismatched client mis-speak the binary port (the 1.2.0 ↔ rebuilt-server skew).
|
|
202
|
+
const proto = res?.[0]?.response?.protocol
|
|
203
|
+
if (proto !== CLIENT_SOCKET_PROTOCOL) {
|
|
204
|
+
console.warn(`[odb-client] socket protocol mismatch (server=${proto ?? 'none'}, client=${CLIENT_SOCKET_PROTOCOL}) → using HTTP`)
|
|
205
|
+
return false
|
|
206
|
+
}
|
|
161
207
|
return true
|
|
162
208
|
} catch {
|
|
163
209
|
return false
|
|
@@ -165,6 +211,11 @@ export class OdbliteSocketAdapter implements DatabaseAdapter {
|
|
|
165
211
|
}
|
|
166
212
|
}
|
|
167
213
|
|
|
214
|
+
/** Socket wire-protocol version this client speaks. The connect-time handshake (isHealthy → get_version)
|
|
215
|
+
* requires the server to match; on mismatch the auto-adapter falls back to HTTP. Keep in lockstep with
|
|
216
|
+
* router_v2's SOCKET_PROTOCOL. */
|
|
217
|
+
export const CLIENT_SOCKET_PROTOCOL = 2
|
|
218
|
+
|
|
168
219
|
/** Default same-host unix socket (free — v1 only uses /tmp/odblite-internal.sock). */
|
|
169
220
|
export const DEFAULT_UNIX_SOCKET = '/tmp/odblite.sock'
|
|
170
221
|
|
|
@@ -200,41 +251,69 @@ export function parseSocketTarget(s?: string): SocketTarget {
|
|
|
200
251
|
*/
|
|
201
252
|
export class OdbliteAutoAdapter implements DatabaseAdapter {
|
|
202
253
|
readonly type: BackendType = 'odblite'
|
|
203
|
-
private readonly
|
|
254
|
+
private readonly opts: { socket?: OdbliteSocketConfig; http: any }
|
|
255
|
+
private chosen: Promise<DatabaseAdapter>
|
|
256
|
+
private onHttp = false
|
|
257
|
+
private nextRetryAt = 0
|
|
204
258
|
|
|
205
259
|
constructor(opts: { socket?: OdbliteSocketConfig; http: any }) {
|
|
206
|
-
this.
|
|
260
|
+
this.opts = opts
|
|
261
|
+
this.chosen = this.pick()
|
|
207
262
|
}
|
|
208
263
|
|
|
209
|
-
private async pick(
|
|
264
|
+
private async pick(): Promise<DatabaseAdapter> {
|
|
210
265
|
const hasBun = typeof (globalThis as any).Bun !== 'undefined' && !!(globalThis as any).Bun.connect
|
|
211
266
|
if (hasBun) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
267
|
+
const socket = await this.trySocket()
|
|
268
|
+
if (socket) return socket
|
|
269
|
+
}
|
|
270
|
+
this.onHttp = true
|
|
271
|
+
console.log('[odb-client] transport → HTTP (no router_v2 socket reachable)')
|
|
272
|
+
return new ODBLiteAdapter(this.opts.http)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Probe the cascade once; return a live socket adapter or null. */
|
|
276
|
+
private async trySocket(): Promise<DatabaseAdapter | null> {
|
|
277
|
+
for (const target of this.candidates()) {
|
|
278
|
+
try {
|
|
279
|
+
const socket = new OdbliteSocketAdapter(target)
|
|
280
|
+
if (await socket.isHealthy()) {
|
|
281
|
+
console.log(`[odb-client] transport → SOCKET (router_v2 @ ${target.unix ?? `${target.hostname}:${target.port}`})`)
|
|
282
|
+
return socket
|
|
222
283
|
}
|
|
284
|
+
await socket.disconnect()
|
|
285
|
+
} catch {
|
|
286
|
+
/* try the next candidate */
|
|
223
287
|
}
|
|
224
288
|
}
|
|
225
|
-
|
|
226
|
-
|
|
289
|
+
return null
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** When we fell back to HTTP, re-probe the socket on a cooldown so a socket that
|
|
293
|
+
* comes up LATER (server started after the app) is picked up without a restart —
|
|
294
|
+
* subsequent connections then use it. */
|
|
295
|
+
private async maybeUpgrade(): Promise<void> {
|
|
296
|
+
if (!this.onHttp) return
|
|
297
|
+
const now = Date.now()
|
|
298
|
+
if (now < this.nextRetryAt) return
|
|
299
|
+
this.nextRetryAt = now + 15_000
|
|
300
|
+
const socket = await this.trySocket()
|
|
301
|
+
if (socket) {
|
|
302
|
+
this.chosen = Promise.resolve(socket)
|
|
303
|
+
this.onHttp = false
|
|
304
|
+
}
|
|
227
305
|
}
|
|
228
306
|
|
|
229
307
|
/** Ordered socket targets to probe. Explicit override → a single target; else the
|
|
230
308
|
* zero-config cascade unix (same host) → TCP (host derived from ODB_LITE_URL). */
|
|
231
|
-
private candidates(
|
|
232
|
-
|
|
309
|
+
private candidates(): SocketTarget[] {
|
|
310
|
+
const { socket, http } = this.opts
|
|
311
|
+
if (socket?.unix || socket?.port) return [socket]
|
|
233
312
|
const env = (globalThis as any).process?.env?.ODB_SOCKET
|
|
234
313
|
if (env) return [parseSocketTarget(env)]
|
|
235
314
|
const out: SocketTarget[] = [{ unix: DEFAULT_UNIX_SOCKET }]
|
|
236
315
|
try {
|
|
237
|
-
const host = new URL(
|
|
316
|
+
const host = new URL(http?.serviceUrl).hostname // e.g. odb-lite from http://odb-lite:8671
|
|
238
317
|
const port = Number((globalThis as any).process?.env?.ODB_SOCKET_PORT ?? 8787)
|
|
239
318
|
if (host) out.push({ hostname: host, port })
|
|
240
319
|
} catch {
|
|
@@ -244,6 +323,7 @@ export class OdbliteAutoAdapter implements DatabaseAdapter {
|
|
|
244
323
|
}
|
|
245
324
|
|
|
246
325
|
async connect(config: any): Promise<Connection> {
|
|
326
|
+
await this.maybeUpgrade()
|
|
247
327
|
return (await this.chosen).connect(config)
|
|
248
328
|
}
|
|
249
329
|
async disconnect(tenantId?: string): Promise<void> {
|
|
@@ -7,6 +7,11 @@ import { FrameReader, FrameWriter, type StreamRequest, type StreamResult } from
|
|
|
7
7
|
*
|
|
8
8
|
* Bun-only (uses `Bun.connect`). app-router runs on Bun; the HTTP `odblite`
|
|
9
9
|
* backend remains the universal fallback for non-Bun environments.
|
|
10
|
+
*
|
|
11
|
+
* Liveness invariant: an in-flight request must ALWAYS terminate. If the
|
|
12
|
+
* connection drops (close/error) or a reply never arrives, the pending promise
|
|
13
|
+
* is REJECTED — never left hanging. A hung pending request would wedge the
|
|
14
|
+
* caller's transaction, hold its write lane, and cascade into a 30s timeout.
|
|
10
15
|
*/
|
|
11
16
|
export interface SocketTarget {
|
|
12
17
|
unix?: string
|
|
@@ -14,12 +19,24 @@ export interface SocketTarget {
|
|
|
14
19
|
port?: number
|
|
15
20
|
}
|
|
16
21
|
|
|
22
|
+
// Backstop: a request that gets neither a reply nor a disconnect (server wedged on
|
|
23
|
+
// it) is rejected after this long, so the caller fails fast + can retry rather than
|
|
24
|
+
// hang forever. Generous by default; tune via ODB_SOCKET_REQUEST_TIMEOUT_MS.
|
|
25
|
+
const REQUEST_TIMEOUT_MS = Number((globalThis as any).process?.env?.ODB_SOCKET_REQUEST_TIMEOUT_MS) || 30000
|
|
26
|
+
|
|
27
|
+
interface Pending {
|
|
28
|
+
resolve: (r: StreamResult[]) => void
|
|
29
|
+
reject: (e: any) => void
|
|
30
|
+
timer: ReturnType<typeof setTimeout>
|
|
31
|
+
}
|
|
32
|
+
|
|
17
33
|
export class SocketClient {
|
|
18
34
|
private socket: any
|
|
19
35
|
private reader = new FrameReader()
|
|
20
36
|
private writer!: FrameWriter
|
|
21
|
-
private readonly pending = new Map<number,
|
|
37
|
+
private readonly pending = new Map<number, Pending>()
|
|
22
38
|
private nextId = 1
|
|
39
|
+
private nextStreamId = 1
|
|
23
40
|
private ready: Promise<void>
|
|
24
41
|
|
|
25
42
|
constructor(private readonly target: SocketTarget) {
|
|
@@ -29,6 +46,16 @@ export class SocketClient {
|
|
|
29
46
|
void this.ready.catch(() => {})
|
|
30
47
|
}
|
|
31
48
|
|
|
49
|
+
/** Reject + clear every in-flight request — called on disconnect/error/close so a
|
|
50
|
+
* dropped connection FAILS its requests (the caller retries) instead of hanging. */
|
|
51
|
+
private failPending(err: Error): void {
|
|
52
|
+
for (const p of this.pending.values()) {
|
|
53
|
+
clearTimeout(p.timer)
|
|
54
|
+
p.reject(err)
|
|
55
|
+
}
|
|
56
|
+
this.pending.clear()
|
|
57
|
+
}
|
|
58
|
+
|
|
32
59
|
private async connect(): Promise<void> {
|
|
33
60
|
const Bun = (globalThis as any).Bun
|
|
34
61
|
if (!Bun?.connect) throw new Error('odblite-socket backend requires Bun (Bun.connect)')
|
|
@@ -41,38 +68,67 @@ export class SocketClient {
|
|
|
41
68
|
data: (_s: any, chunk: Buffer) => {
|
|
42
69
|
for (const frame of this.reader.push(chunk)) {
|
|
43
70
|
const msg = JSON.parse(frame.toString('utf8')) as { id: number; results: StreamResult[] }
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
71
|
+
const p = this.pending.get(msg.id)
|
|
72
|
+
if (p) {
|
|
46
73
|
this.pending.delete(msg.id)
|
|
47
|
-
|
|
74
|
+
clearTimeout(p.timer)
|
|
75
|
+
p.resolve(msg.results)
|
|
48
76
|
}
|
|
49
77
|
}
|
|
50
78
|
},
|
|
51
79
|
drain: (_s: any) => this.writer.flush(),
|
|
52
80
|
close: () => {
|
|
53
|
-
// reconnect lazily on next pipeline()
|
|
81
|
+
// Connection gone → reconnect lazily on next pipeline(). A half-read frame
|
|
82
|
+
// in the buffer is invalid now, and EVERY in-flight request must fail (not
|
|
83
|
+
// hang) so its caller's transaction unwinds + releases the write lane.
|
|
54
84
|
this.socket = undefined
|
|
85
|
+
this.reader = new FrameReader()
|
|
86
|
+
this.failPending(new Error('odblite socket disconnected — request not completed'))
|
|
87
|
+
},
|
|
88
|
+
error: (_s: any, err: any) => {
|
|
89
|
+
this.socket = undefined
|
|
90
|
+
this.reader = new FrameReader()
|
|
91
|
+
this.failPending(err instanceof Error ? err : new Error(`odblite socket error: ${err}`))
|
|
55
92
|
},
|
|
56
93
|
},
|
|
57
94
|
})
|
|
58
95
|
this.writer = new FrameWriter(this.socket)
|
|
59
96
|
}
|
|
60
97
|
|
|
61
|
-
|
|
98
|
+
/** Allocate a fresh stream id for an interactive transaction (unique per connection). The server
|
|
99
|
+
* pins one SQLite connection per stream id, so all of a tx's statements run on it (read-your-own-
|
|
100
|
+
* writes). The id is carried in every frame of that tx via pipeline()'s `stream` arg. */
|
|
101
|
+
allocStream(): number {
|
|
102
|
+
return this.nextStreamId++
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async pipeline(tenant: string, requests: StreamRequest[], stream?: number): Promise<StreamResult[]> {
|
|
106
|
+
// Await the in-flight connect FIRST — only reconnect if it's actually gone
|
|
107
|
+
// (closed/failed). Checking `!this.socket` before awaiting would start a
|
|
108
|
+
// second connect while the first is still resolving (a leaked connection).
|
|
109
|
+
await this.ready
|
|
62
110
|
if (!this.socket) {
|
|
63
111
|
this.ready = this.connect()
|
|
64
112
|
void this.ready.catch(() => {})
|
|
113
|
+
await this.ready
|
|
65
114
|
}
|
|
66
|
-
await this.ready
|
|
67
115
|
const id = this.nextId++
|
|
68
|
-
return new Promise<StreamResult[]>((resolve) => {
|
|
69
|
-
|
|
70
|
-
|
|
116
|
+
return new Promise<StreamResult[]>((resolve, reject) => {
|
|
117
|
+
const timer = setTimeout(() => {
|
|
118
|
+
// No reply AND no disconnect (server wedged) → reject so we never hang forever.
|
|
119
|
+
if (this.pending.delete(id)) reject(new Error(`odblite socket request ${id} timed out after ${REQUEST_TIMEOUT_MS}ms`))
|
|
120
|
+
}, REQUEST_TIMEOUT_MS)
|
|
121
|
+
;(timer as any).unref?.()
|
|
122
|
+
this.pending.set(id, { resolve, reject, timer })
|
|
123
|
+
// `stream` present → the server routes this frame to that interactive tx's pinned connection.
|
|
124
|
+
const payload = stream != null ? { id, tenant, stream, requests } : { id, tenant, requests }
|
|
125
|
+
this.writer.send(Buffer.from(JSON.stringify(payload), 'utf8'))
|
|
71
126
|
})
|
|
72
127
|
}
|
|
73
128
|
|
|
74
129
|
close(): void {
|
|
75
130
|
this.socket?.end?.()
|
|
76
131
|
this.socket = undefined
|
|
132
|
+
this.failPending(new Error('odblite socket closed'))
|
|
77
133
|
}
|
|
78
134
|
}
|
|
@@ -88,6 +88,7 @@ export type StreamRequest =
|
|
|
88
88
|
| { type: 'batch'; batch: Batch }
|
|
89
89
|
| { type: 'sequence'; sql: string }
|
|
90
90
|
| { type: 'get_autocommit' }
|
|
91
|
+
| { type: 'get_version' } // connect-time wire-protocol handshake
|
|
91
92
|
| { type: 'close' }
|
|
92
93
|
export type StreamResult =
|
|
93
94
|
| { type: 'ok'; response: any }
|
|
@@ -109,6 +110,15 @@ export function toBatch(steps: AtomicStep[]): Batch {
|
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
// ── SQL classification (client-side heuristic) ────────────────────────────────
|
|
114
|
+
// Mirrors router_v2/engine/sql-kind.ts (kept in lockstep, like the wire types).
|
|
115
|
+
// The SERVER is authoritative (it checks the prepared statement's column count);
|
|
116
|
+
// this only picks the `sql``` convenience tag's shape before the round-trip.
|
|
117
|
+
const READ_ONLY_RE = /^\s*(?:SELECT|EXPLAIN|VALUES)\b/i
|
|
118
|
+
export function mightReturnRows(sql: string): boolean {
|
|
119
|
+
return READ_ONLY_RE.test(sql) || /\bRETURNING\b/i.test(sql)
|
|
120
|
+
}
|
|
121
|
+
|
|
112
122
|
// ── length-prefixed framing with backpressure-safe writes ─────────────────────
|
|
113
123
|
interface Writable {
|
|
114
124
|
write(data: Uint8Array): number
|