@pineliner/odb-client 1.5.4 → 1.5.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pineliner/odb-client",
3
- "version": "1.5.4",
3
+ "version": "1.5.5",
4
4
  "description": "Isomorphic client for ODB-Lite with postgres.js-like template string SQL support",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -2,6 +2,12 @@ import type { BackendType, Connection, DatabaseAdapter, PreparedStatement, Query
2
2
  import { ODBLiteAdapter } from './odblite'
3
3
  import { SocketClient, InteractiveTxConnectionLostError, type SocketTarget } from './socket/client'
4
4
  import { runInTransactionContext, ambientTx } from './tx-context'
5
+
6
+ // An ambient-tx JOIN whose target transaction already committed/reaped surfaces as "stream not open" — the
7
+ // write is now inherently standalone, so query/execute fall back to autocommit. Matches the server wording.
8
+ function isStreamClosed(e: any): boolean {
9
+ return /stream .*(not open|already ended|never opened)/i.test(String(e?.message ?? ''))
10
+ }
5
11
  import {
6
12
  toHrana,
7
13
  fromHrana,
@@ -115,7 +121,13 @@ class OdbliteSocketConnection implements Connection {
115
121
  const p = typeof sql === 'string' ? params : (sql.args ?? [])
116
122
  if (this.streamId != null) return (await this.streamExec(s, p)) as QueryResult<T>
117
123
  const amb = ambientTx(this.tenant) // autocommit conn, but inside a tx for my tenant? → join it (see execute)
118
- if (amb && amb !== this) return amb.query<T>(s, p)
124
+ if (amb && amb !== this) {
125
+ try {
126
+ return await amb.query<T>(s, p)
127
+ } catch (e: any) {
128
+ if (!isStreamClosed(e)) throw e // tx we tried to join already closed → run standalone (autocommit) below
129
+ }
130
+ }
119
131
  return toQueryResult(await this.one([{ type: 'execute', stmt: { sql: s, args: p.map(toHrana) } }])) as QueryResult<T>
120
132
  }
121
133
 
@@ -130,7 +142,19 @@ class OdbliteSocketConnection implements Connection {
130
142
  // fresh autocommit write that would deadlock behind the tx on the writer slot. Same tenant key on both
131
143
  // sides (this.tenant), so no auth/database-hash translation for a caller to get wrong.
132
144
  const amb = ambientTx(this.tenant)
133
- if (amb && amb !== this) return amb.execute(s, p)
145
+ if (amb && amb !== this) {
146
+ try {
147
+ return await amb.execute(s, p)
148
+ } catch (e: any) {
149
+ // GENERIC ROBUSTNESS: the ambient tx we tried to JOIN has already committed/reaped, so its stream is
150
+ // gone ("stream not open"). The write is inherently post-commit now — fall back to a fresh autocommit
151
+ // connection instead of throwing. This makes a non-awaited publish inside a tx degrade gracefully
152
+ // (decoupled) rather than crash, so callers don't strictly need publishAsync to avoid errors — it
153
+ // stays only as the way to express DETERMINISTIC decoupling. Scoped to the JOIN path: a tx's OWN
154
+ // statements still surface the error (we never silently split a real transaction).
155
+ if (!isStreamClosed(e)) throw e
156
+ }
157
+ }
134
158
  return toQueryResult(await this.one([{ type: 'execute', stmt: { sql: s, args: p.map(toHrana) } }]))
135
159
  }
136
160