@pineliner/odb-client 1.5.1 → 1.5.3
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.map +1 -1
- package/dist/database/adapters/socket/client.d.ts +15 -0
- package/dist/database/adapters/socket/client.d.ts.map +1 -1
- package/dist/index.cjs +19 -0
- package/dist/index.js +19 -0
- package/package.json +1 -1
- package/src/database/adapters/odblite-socket.ts +16 -1
- package/src/database/adapters/socket/client.ts +28 -0
|
@@ -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,
|
|
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,EAAkD,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAA;AA8LnG,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"}
|
|
@@ -17,6 +17,15 @@ export interface SocketTarget {
|
|
|
17
17
|
hostname?: string;
|
|
18
18
|
port?: number;
|
|
19
19
|
}
|
|
20
|
+
/** An interactive transaction was pinned to a socket connection that has since been reset (router
|
|
21
|
+
* restart / socket drop). The tx's server-side connection — and its uncommitted writes — are gone, so
|
|
22
|
+
* it CANNOT be resumed on a fresh connection; the caller must retry the whole transaction. Thrown
|
|
23
|
+
* instead of silently reconnecting and sending a continuation frame on the new connection (which the
|
|
24
|
+
* server rejects as "stream N is not open" — a confusing, non-actionable error). */
|
|
25
|
+
export declare class InteractiveTxConnectionLostError extends Error {
|
|
26
|
+
readonly code = "ODB_TX_CONNECTION_LOST";
|
|
27
|
+
constructor();
|
|
28
|
+
}
|
|
20
29
|
export declare class SocketClient {
|
|
21
30
|
private readonly target;
|
|
22
31
|
private socket;
|
|
@@ -25,12 +34,18 @@ export declare class SocketClient {
|
|
|
25
34
|
private readonly pending;
|
|
26
35
|
private nextId;
|
|
27
36
|
private nextStreamId;
|
|
37
|
+
private epoch;
|
|
28
38
|
private ready;
|
|
29
39
|
constructor(target: SocketTarget);
|
|
30
40
|
/** Reject + clear every in-flight request — called on disconnect/error/close so a
|
|
31
41
|
* dropped connection FAILS its requests (the caller retries) instead of hanging. */
|
|
32
42
|
private failPending;
|
|
33
43
|
private connect;
|
|
44
|
+
/** Current connection generation — an interactive tx captures this when it opens (its BEGIN lands). */
|
|
45
|
+
get connectionEpoch(): number;
|
|
46
|
+
/** True only if the live socket is the SAME generation the tx opened on. False after any drop (socket
|
|
47
|
+
* cleared) or reconnect (epoch bumped) — the tx's pinned server connection is gone either way. */
|
|
48
|
+
isConnectionLive(epoch: number | undefined): boolean;
|
|
34
49
|
/** Reconnect with bounded backoff — bridges a router restart so a request landing in the down-window
|
|
35
50
|
* waits it out instead of failing. After the last attempt the error propagates (caller retries / the
|
|
36
51
|
* auto-adapter can fall back to HTTP). Backoff: 50·100·200·400·800·1600ms ≈ 3s total by default. */
|
|
@@ -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;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAqBD,qBAAa,YAAY;
|
|
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;AAqBD;;;;qFAIqF;AACrF,qBAAa,gCAAiC,SAAQ,KAAK;IACzD,QAAQ,CAAC,IAAI,4BAA2B;;CAKzC;AAED,qBAAa,YAAY;IAYX,OAAO,CAAC,QAAQ,CAAC,MAAM;IAXnC,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;IAGxB,OAAO,CAAC,KAAK,CAAI;IACjB,OAAO,CAAC,KAAK,CAAe;gBAEC,MAAM,EAAE,YAAY;IAOjD;yFACqF;IACrF,OAAO,CAAC,WAAW;YAQL,OAAO;IAuDrB,uGAAuG;IACvG,IAAI,eAAe,IAAI,MAAM,CAE5B;IAED;uGACmG;IACnG,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO;IAIpD;;yGAEqG;YACvF,gBAAgB;IAc9B;;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;IA0BnG,KAAK,IAAI,IAAI;CAKd"}
|
package/dist/index.cjs
CHANGED
|
@@ -2258,6 +2258,13 @@ var __webpack_exports__ = {};
|
|
|
2258
2258
|
const RECONNECT_ATTEMPTS = Number(globalThis.process?.env?.ODB_SOCKET_RECONNECT_ATTEMPTS) || 6;
|
|
2259
2259
|
const RECONNECT_BASE_MS = Number(globalThis.process?.env?.ODB_SOCKET_RECONNECT_BASE_MS) || 50;
|
|
2260
2260
|
const sleep = (ms)=>new Promise((r)=>setTimeout(r, ms));
|
|
2261
|
+
class InteractiveTxConnectionLostError extends Error {
|
|
2262
|
+
code = 'ODB_TX_CONNECTION_LOST';
|
|
2263
|
+
constructor(){
|
|
2264
|
+
super('odblite: interactive transaction lost its connection (router restart / socket reset) — retry the transaction');
|
|
2265
|
+
this.name = 'InteractiveTxConnectionLostError';
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2261
2268
|
class SocketClient {
|
|
2262
2269
|
target;
|
|
2263
2270
|
socket;
|
|
@@ -2266,6 +2273,7 @@ var __webpack_exports__ = {};
|
|
|
2266
2273
|
pending = new Map();
|
|
2267
2274
|
nextId = 1;
|
|
2268
2275
|
nextStreamId = 1;
|
|
2276
|
+
epoch = 0;
|
|
2269
2277
|
ready;
|
|
2270
2278
|
constructor(target){
|
|
2271
2279
|
this.target = target;
|
|
@@ -2328,6 +2336,13 @@ var __webpack_exports__ = {};
|
|
|
2328
2336
|
}
|
|
2329
2337
|
});
|
|
2330
2338
|
this.writer = new FrameWriter(this.socket);
|
|
2339
|
+
this.epoch++;
|
|
2340
|
+
}
|
|
2341
|
+
get connectionEpoch() {
|
|
2342
|
+
return this.epoch;
|
|
2343
|
+
}
|
|
2344
|
+
isConnectionLive(epoch) {
|
|
2345
|
+
return !!this.socket && this.epoch === epoch;
|
|
2331
2346
|
}
|
|
2332
2347
|
async connectWithRetry() {
|
|
2333
2348
|
let lastErr;
|
|
@@ -2406,6 +2421,7 @@ var __webpack_exports__ = {};
|
|
|
2406
2421
|
databaseHash;
|
|
2407
2422
|
sql;
|
|
2408
2423
|
beginPending = false;
|
|
2424
|
+
openEpoch;
|
|
2409
2425
|
constructor(client, tenant, streamId){
|
|
2410
2426
|
this.client = client;
|
|
2411
2427
|
this.tenant = tenant;
|
|
@@ -2428,6 +2444,8 @@ var __webpack_exports__ = {};
|
|
|
2428
2444
|
return res;
|
|
2429
2445
|
}
|
|
2430
2446
|
async streamExec(sql, params = []) {
|
|
2447
|
+
const opening = this.beginPending;
|
|
2448
|
+
if (!opening && !this.client.isConnectionLive(this.openEpoch)) throw new InteractiveTxConnectionLostError();
|
|
2431
2449
|
const reqs = [];
|
|
2432
2450
|
if (this.beginPending) {
|
|
2433
2451
|
reqs.push({
|
|
@@ -2446,6 +2464,7 @@ var __webpack_exports__ = {};
|
|
|
2446
2464
|
}
|
|
2447
2465
|
});
|
|
2448
2466
|
const results = await this.client.pipeline(this.tenant, reqs, this.streamId);
|
|
2467
|
+
if (opening) this.openEpoch = this.client.connectionEpoch;
|
|
2449
2468
|
if (reqs.length > 1 && results[0]?.type === 'error') throw new Error(results[0].error.message);
|
|
2450
2469
|
const res = results[results.length - 1];
|
|
2451
2470
|
if (!res) throw new Error('odblite-socket: empty stream response');
|
package/dist/index.js
CHANGED
|
@@ -2168,6 +2168,13 @@ const REQUEST_TIMEOUT_MS = Number(globalThis.process?.env?.ODB_SOCKET_REQUEST_TI
|
|
|
2168
2168
|
const RECONNECT_ATTEMPTS = Number(globalThis.process?.env?.ODB_SOCKET_RECONNECT_ATTEMPTS) || 6;
|
|
2169
2169
|
const RECONNECT_BASE_MS = Number(globalThis.process?.env?.ODB_SOCKET_RECONNECT_BASE_MS) || 50;
|
|
2170
2170
|
const sleep = (ms)=>new Promise((r)=>setTimeout(r, ms));
|
|
2171
|
+
class InteractiveTxConnectionLostError extends Error {
|
|
2172
|
+
code = 'ODB_TX_CONNECTION_LOST';
|
|
2173
|
+
constructor(){
|
|
2174
|
+
super('odblite: interactive transaction lost its connection (router restart / socket reset) — retry the transaction');
|
|
2175
|
+
this.name = 'InteractiveTxConnectionLostError';
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2171
2178
|
class SocketClient {
|
|
2172
2179
|
target;
|
|
2173
2180
|
socket;
|
|
@@ -2176,6 +2183,7 @@ class SocketClient {
|
|
|
2176
2183
|
pending = new Map();
|
|
2177
2184
|
nextId = 1;
|
|
2178
2185
|
nextStreamId = 1;
|
|
2186
|
+
epoch = 0;
|
|
2179
2187
|
ready;
|
|
2180
2188
|
constructor(target){
|
|
2181
2189
|
this.target = target;
|
|
@@ -2238,6 +2246,13 @@ class SocketClient {
|
|
|
2238
2246
|
}
|
|
2239
2247
|
});
|
|
2240
2248
|
this.writer = new FrameWriter(this.socket);
|
|
2249
|
+
this.epoch++;
|
|
2250
|
+
}
|
|
2251
|
+
get connectionEpoch() {
|
|
2252
|
+
return this.epoch;
|
|
2253
|
+
}
|
|
2254
|
+
isConnectionLive(epoch) {
|
|
2255
|
+
return !!this.socket && this.epoch === epoch;
|
|
2241
2256
|
}
|
|
2242
2257
|
async connectWithRetry() {
|
|
2243
2258
|
let lastErr;
|
|
@@ -2316,6 +2331,7 @@ class OdbliteSocketConnection {
|
|
|
2316
2331
|
databaseHash;
|
|
2317
2332
|
sql;
|
|
2318
2333
|
beginPending = false;
|
|
2334
|
+
openEpoch;
|
|
2319
2335
|
constructor(client, tenant, streamId){
|
|
2320
2336
|
this.client = client;
|
|
2321
2337
|
this.tenant = tenant;
|
|
@@ -2338,6 +2354,8 @@ class OdbliteSocketConnection {
|
|
|
2338
2354
|
return res;
|
|
2339
2355
|
}
|
|
2340
2356
|
async streamExec(sql, params = []) {
|
|
2357
|
+
const opening = this.beginPending;
|
|
2358
|
+
if (!opening && !this.client.isConnectionLive(this.openEpoch)) throw new InteractiveTxConnectionLostError();
|
|
2341
2359
|
const reqs = [];
|
|
2342
2360
|
if (this.beginPending) {
|
|
2343
2361
|
reqs.push({
|
|
@@ -2356,6 +2374,7 @@ class OdbliteSocketConnection {
|
|
|
2356
2374
|
}
|
|
2357
2375
|
});
|
|
2358
2376
|
const results = await this.client.pipeline(this.tenant, reqs, this.streamId);
|
|
2377
|
+
if (opening) this.openEpoch = this.client.connectionEpoch;
|
|
2359
2378
|
if (reqs.length > 1 && results[0]?.type === 'error') throw new Error(results[0].error.message);
|
|
2360
2379
|
const res = results[results.length - 1];
|
|
2361
2380
|
if (!res) throw new Error('odblite-socket: empty stream response');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BackendType, Connection, DatabaseAdapter, PreparedStatement, QueryResult } from '../types'
|
|
2
2
|
import { ODBLiteAdapter } from './odblite'
|
|
3
|
-
import { SocketClient, type SocketTarget } from './socket/client'
|
|
3
|
+
import { SocketClient, InteractiveTxConnectionLostError, type SocketTarget } from './socket/client'
|
|
4
4
|
import {
|
|
5
5
|
toHrana,
|
|
6
6
|
fromHrana,
|
|
@@ -47,6 +47,10 @@ class OdbliteSocketConnection implements Connection {
|
|
|
47
47
|
/** A fresh stream tx whose BEGIN hasn't been flushed yet — it's folded into the first statement's
|
|
48
48
|
* frame to save a round-trip (and an empty tx that issues no statements sends nothing at all). */
|
|
49
49
|
private beginPending = false
|
|
50
|
+
/** The socket connection generation this tx's BEGIN landed on. A pinned interactive tx lives on ONE
|
|
51
|
+
* connection; if the socket resets, this stream's server connection (and its uncommitted writes) is
|
|
52
|
+
* gone. Set when the opening frame commits; checked before every continuation frame. */
|
|
53
|
+
private openEpoch?: number
|
|
50
54
|
|
|
51
55
|
constructor(
|
|
52
56
|
private readonly client: SocketClient,
|
|
@@ -78,6 +82,14 @@ class OdbliteSocketConnection implements Connection {
|
|
|
78
82
|
/** Run one statement on this connection's interactive STREAM — the server's pinned connection, so a
|
|
79
83
|
* read sees this tx's own uncommitted writes. Throws on error (incl. a poisoned COMMIT). */
|
|
80
84
|
private async streamExec(sql: string, params: any[] = []): Promise<QueryResult> {
|
|
85
|
+
const opening = this.beginPending
|
|
86
|
+
// Continuation frame (BEGIN already sent): the server pins this tx to ONE connection. If the socket
|
|
87
|
+
// was reset since we opened, that connection — and this tx's uncommitted work — is gone. FAIL LOUD:
|
|
88
|
+
// do NOT fall through to pipeline(), which would reconnect and send this frame on a FRESH connection
|
|
89
|
+
// where the stream was never opened (→ the server's "stream N is not open", a confusing dead end).
|
|
90
|
+
// The opening frame is exempt: a tx that hasn't started yet may legitimately open on a reconnect.
|
|
91
|
+
if (!opening && !this.client.isConnectionLive(this.openEpoch)) throw new InteractiveTxConnectionLostError()
|
|
92
|
+
|
|
81
93
|
const reqs: StreamRequest[] = []
|
|
82
94
|
if (this.beginPending) {
|
|
83
95
|
reqs.push({ type: 'execute', stmt: { sql: 'BEGIN' } }) // fold BEGIN into this first statement's frame
|
|
@@ -85,6 +97,9 @@ class OdbliteSocketConnection implements Connection {
|
|
|
85
97
|
}
|
|
86
98
|
reqs.push({ type: 'execute', stmt: { sql, args: params.map(toHrana) } })
|
|
87
99
|
const results = await this.client.pipeline(this.tenant, reqs, this.streamId)
|
|
100
|
+
// Bind the tx to the connection generation that actually served its BEGIN (post-reconnect if the
|
|
101
|
+
// opening frame waited out a restart). Continuation frames above are validated against this.
|
|
102
|
+
if (opening) this.openEpoch = this.client.connectionEpoch
|
|
88
103
|
if (reqs.length > 1 && results[0]?.type === 'error') throw new Error(results[0].error.message) // the folded BEGIN failed
|
|
89
104
|
const res = results[results.length - 1]
|
|
90
105
|
if (!res) throw new Error('odblite-socket: empty stream response')
|
|
@@ -38,6 +38,19 @@ interface Pending {
|
|
|
38
38
|
timer: ReturnType<typeof setTimeout>
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/** An interactive transaction was pinned to a socket connection that has since been reset (router
|
|
42
|
+
* restart / socket drop). The tx's server-side connection — and its uncommitted writes — are gone, so
|
|
43
|
+
* it CANNOT be resumed on a fresh connection; the caller must retry the whole transaction. Thrown
|
|
44
|
+
* instead of silently reconnecting and sending a continuation frame on the new connection (which the
|
|
45
|
+
* server rejects as "stream N is not open" — a confusing, non-actionable error). */
|
|
46
|
+
export class InteractiveTxConnectionLostError extends Error {
|
|
47
|
+
readonly code = 'ODB_TX_CONNECTION_LOST'
|
|
48
|
+
constructor() {
|
|
49
|
+
super('odblite: interactive transaction lost its connection (router restart / socket reset) — retry the transaction')
|
|
50
|
+
this.name = 'InteractiveTxConnectionLostError'
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
41
54
|
export class SocketClient {
|
|
42
55
|
private socket: any
|
|
43
56
|
private reader = new FrameReader()
|
|
@@ -45,6 +58,9 @@ export class SocketClient {
|
|
|
45
58
|
private readonly pending = new Map<number, Pending>()
|
|
46
59
|
private nextId = 1
|
|
47
60
|
private nextStreamId = 1
|
|
61
|
+
// Monotonic connection generation — bumped on every successful (re)connect. An interactive tx records
|
|
62
|
+
// the epoch it opened on; a mismatch means its pinned server connection is gone (see streamExec).
|
|
63
|
+
private epoch = 0
|
|
48
64
|
private ready: Promise<void>
|
|
49
65
|
|
|
50
66
|
constructor(private readonly target: SocketTarget) {
|
|
@@ -116,6 +132,18 @@ export class SocketClient {
|
|
|
116
132
|
},
|
|
117
133
|
})
|
|
118
134
|
this.writer = new FrameWriter(this.socket)
|
|
135
|
+
this.epoch++ // a new connection generation — any interactive tx from a prior epoch is now dead
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Current connection generation — an interactive tx captures this when it opens (its BEGIN lands). */
|
|
139
|
+
get connectionEpoch(): number {
|
|
140
|
+
return this.epoch
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** True only if the live socket is the SAME generation the tx opened on. False after any drop (socket
|
|
144
|
+
* cleared) or reconnect (epoch bumped) — the tx's pinned server connection is gone either way. */
|
|
145
|
+
isConnectionLive(epoch: number | undefined): boolean {
|
|
146
|
+
return !!this.socket && this.epoch === epoch
|
|
119
147
|
}
|
|
120
148
|
|
|
121
149
|
/** Reconnect with bounded backoff — bridges a router restart so a request landing in the down-window
|