@pineliner/odb-client 1.5.0 → 1.5.2
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.
|
@@ -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;AA+KjE,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"}
|
package/dist/index.cjs
CHANGED
|
@@ -2452,13 +2452,15 @@ var __webpack_exports__ = {};
|
|
|
2452
2452
|
return toQueryResult(res);
|
|
2453
2453
|
}
|
|
2454
2454
|
async query(sql, params = []) {
|
|
2455
|
-
|
|
2455
|
+
const s = 'string' == typeof sql ? sql : sql.sql;
|
|
2456
|
+
const p = 'string' == typeof sql ? params : sql.args ?? [];
|
|
2457
|
+
if (null != this.streamId) return await this.streamExec(s, p);
|
|
2456
2458
|
return toQueryResult(await this.one([
|
|
2457
2459
|
{
|
|
2458
2460
|
type: 'execute',
|
|
2459
2461
|
stmt: {
|
|
2460
|
-
sql,
|
|
2461
|
-
args:
|
|
2462
|
+
sql: s,
|
|
2463
|
+
args: p.map(toHrana)
|
|
2462
2464
|
}
|
|
2463
2465
|
}
|
|
2464
2466
|
]));
|
package/dist/index.js
CHANGED
|
@@ -2362,13 +2362,15 @@ class OdbliteSocketConnection {
|
|
|
2362
2362
|
return toQueryResult(res);
|
|
2363
2363
|
}
|
|
2364
2364
|
async query(sql, params = []) {
|
|
2365
|
-
|
|
2365
|
+
const s = 'string' == typeof sql ? sql : sql.sql;
|
|
2366
|
+
const p = 'string' == typeof sql ? params : sql.args ?? [];
|
|
2367
|
+
if (null != this.streamId) return await this.streamExec(s, p);
|
|
2366
2368
|
return toQueryResult(await this.one([
|
|
2367
2369
|
{
|
|
2368
2370
|
type: 'execute',
|
|
2369
2371
|
stmt: {
|
|
2370
|
-
sql,
|
|
2371
|
-
args:
|
|
2372
|
+
sql: s,
|
|
2373
|
+
args: p.map(toHrana)
|
|
2372
2374
|
}
|
|
2373
2375
|
}
|
|
2374
2376
|
]));
|
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,15 +97,23 @@ 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')
|
|
91
106
|
return toQueryResult(res)
|
|
92
107
|
}
|
|
93
108
|
|
|
94
|
-
async query<T = any>(sql: string, params: any[] = []): Promise<QueryResult<T>> {
|
|
95
|
-
|
|
96
|
-
|
|
109
|
+
async query<T = any>(sql: string | { sql: string; args?: any[] }, params: any[] = []): Promise<QueryResult<T>> {
|
|
110
|
+
// Accept both query(sql, params) and query({ sql, args }) — symmetric with
|
|
111
|
+
// execute() and the other adapters. Without this, object-form calls sent the
|
|
112
|
+
// object itself as the SQL string ("Expected 'query' to be a string, got 'object'").
|
|
113
|
+
const s = typeof sql === 'string' ? sql : sql.sql
|
|
114
|
+
const p = typeof sql === 'string' ? params : (sql.args ?? [])
|
|
115
|
+
if (this.streamId != null) return (await this.streamExec(s, p)) as QueryResult<T>
|
|
116
|
+
return toQueryResult(await this.one([{ type: 'execute', stmt: { sql: s, args: p.map(toHrana) } }])) as QueryResult<T>
|
|
97
117
|
}
|
|
98
118
|
|
|
99
119
|
async execute(sql: string | { sql: string; args?: any[] }, params: any[] = []): Promise<QueryResult> {
|
|
@@ -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
|