@pineliner/odb-client 1.5.3 → 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.
@@ -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,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"}
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;AA8MnG,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"}
@@ -0,0 +1,30 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import type { Connection } from '../types';
3
+ /**
4
+ * Ambient transaction context — the "smart underneath" that lets an `await`-a-write-inside-a-transaction
5
+ * pattern work on a single-writer engine WITHOUT deadlocking, and without touching business code.
6
+ *
7
+ * The deadlock: a transaction holds the tenant's one writer slot; a nested write (e.g. an event subscriber
8
+ * running during an awaited `eventBus.publish`) that grabs a FRESH connection queues behind the tx on that
9
+ * same slot, while the tx waits for it → cycle. The fix is not to change the subscriber — it's to make the
10
+ * nested write REUSE the transaction's own connection (its pinned stream) so it runs sequentially on the
11
+ * one writer instead of contending for it.
12
+ *
13
+ * `AsyncLocalStorage` propagates through `await`, so any code running inside `transaction(fn)`'s async flow
14
+ * — including subscribers invoked during an awaited publish — can look up the open tx connection and JOIN
15
+ * it. Same technique as Prisma interactive transactions / TypeORM / .NET `TransactionScope` / Spring
16
+ * `@Transactional` propagation. Semantics: a joined write is PART of the transaction (commits/rolls back
17
+ * with it) — which is exactly what "a write issued inside a transaction" should mean. Writes that must be
18
+ * independent stay independent by running AFTER commit (fire-and-forget / post-commit), where the context
19
+ * has already exited and `ambientTx` returns undefined.
20
+ */
21
+ export declare const txContext: AsyncLocalStorage<{
22
+ tenant: string;
23
+ conn: Connection;
24
+ }>;
25
+ /** The open transaction's connection for `tenant`, if the current async flow is inside one — else
26
+ * undefined. Connection accessors (e.g. the app's getConnectionByTenantHash) return this to join the tx. */
27
+ export declare function ambientTx(tenant: string): Connection | undefined;
28
+ /** Run `fn` with `conn` bound as the ambient transaction for `tenant`. Used by transaction() wrappers. */
29
+ export declare function runInTransactionContext<T>(tenant: string, conn: Connection, fn: () => Promise<T>): Promise<T>;
30
+ //# sourceMappingURL=tx-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tx-context.d.ts","sourceRoot":"","sources":["../../../src/database/adapters/tx-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAE1C;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,SAAS;YAAmC,MAAM;UAAQ,UAAU;EAAK,CAAA;AAEtF;6GAC6G;AAC7G,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAGhE;AAED,0GAA0G;AAC1G,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAE7G"}
package/dist/index.cjs CHANGED
@@ -464,18 +464,22 @@ var __webpack_exports__ = {};
464
464
  (()=>{
465
465
  __webpack_require__.r(__webpack_exports__);
466
466
  __webpack_require__.d(__webpack_exports__, {
467
+ txContext: ()=>txContext,
467
468
  gte: ()=>orm.gte,
468
469
  ODBLiteTransaction: ()=>ODBLiteTransaction,
469
470
  updateSet: ()=>src_updateSet,
470
471
  sql: ()=>sql_parser_sql,
471
472
  isNotNull: ()=>orm.isNotNull,
473
+ ambientTx: ()=>ambientTx,
472
474
  LibSQLAdapter: ()=>LibSQLAdapter,
473
475
  ORM: ()=>orm.ORM,
474
476
  join: ()=>src_join,
475
477
  lte: ()=>orm.lte,
476
478
  default: ()=>odblite,
477
- splitSQLStatements: ()=>splitSQLStatements,
479
+ runInTransactionContext: ()=>runInTransactionContext,
478
480
  ne: ()=>orm.ne,
481
+ splitSQLStatements: ()=>splitSQLStatements,
482
+ InteractiveTxConnectionLostError: ()=>InteractiveTxConnectionLostError,
479
483
  gt: ()=>orm.gt,
480
484
  eq: ()=>orm.eq,
481
485
  fragment: ()=>fragment,
@@ -2395,6 +2399,18 @@ var __webpack_exports__ = {};
2395
2399
  this.failPending(new Error('odblite socket closed'));
2396
2400
  }
2397
2401
  }
2402
+ var external_node_async_hooks_ = __webpack_require__("node:async_hooks");
2403
+ const txContext = new external_node_async_hooks_.AsyncLocalStorage();
2404
+ function ambientTx(tenant) {
2405
+ const store = txContext.getStore();
2406
+ return store && store.tenant === tenant ? store.conn : void 0;
2407
+ }
2408
+ function runInTransactionContext(tenant, conn, fn) {
2409
+ return txContext.run({
2410
+ tenant,
2411
+ conn
2412
+ }, fn);
2413
+ }
2398
2414
  function toQueryResult(res) {
2399
2415
  if ('error' === res.type) throw new Error(res.error.message);
2400
2416
  const r = res.response.result;
@@ -2474,6 +2490,8 @@ var __webpack_exports__ = {};
2474
2490
  const s = 'string' == typeof sql ? sql : sql.sql;
2475
2491
  const p = 'string' == typeof sql ? params : sql.args ?? [];
2476
2492
  if (null != this.streamId) return await this.streamExec(s, p);
2493
+ const amb = ambientTx(this.tenant);
2494
+ if (amb && amb !== this) return amb.query(s, p);
2477
2495
  return toQueryResult(await this.one([
2478
2496
  {
2479
2497
  type: 'execute',
@@ -2488,6 +2506,8 @@ var __webpack_exports__ = {};
2488
2506
  const s = 'string' == typeof sql ? sql : sql.sql;
2489
2507
  const p = 'string' == typeof sql ? params : sql.args ?? [];
2490
2508
  if (null != this.streamId) return this.streamExec(s, p);
2509
+ const amb = ambientTx(this.tenant);
2510
+ if (amb && amb !== this) return amb.execute(s, p);
2491
2511
  return toQueryResult(await this.one([
2492
2512
  {
2493
2513
  type: 'execute',
@@ -2519,14 +2539,16 @@ var __webpack_exports__ = {};
2519
2539
  if (null != this.streamId) return fn(this);
2520
2540
  const tx = new OdbliteSocketConnection(this.client, this.tenant, this.client.allocStream());
2521
2541
  tx.beginPending = true;
2522
- try {
2523
- const out = await fn(tx);
2524
- if (!tx.beginPending) await tx.streamExec('COMMIT');
2525
- return out;
2526
- } catch (e) {
2527
- if (!tx.beginPending) await tx.streamExec('ROLLBACK').catch(()=>{});
2528
- throw e;
2529
- }
2542
+ return runInTransactionContext(this.tenant, tx, async ()=>{
2543
+ try {
2544
+ const out = await fn(tx);
2545
+ if (!tx.beginPending) await tx.streamExec('COMMIT');
2546
+ return out;
2547
+ } catch (e) {
2548
+ if (!tx.beginPending) await tx.streamExec('ROLLBACK').catch(()=>{});
2549
+ throw e;
2550
+ }
2551
+ });
2530
2552
  }
2531
2553
  begin(fn) {
2532
2554
  return this.transaction(fn);
@@ -2964,6 +2986,7 @@ exports.BunSQLiteAdapter = __webpack_exports__.BunSQLiteAdapter;
2964
2986
  exports.ConnectionError = __webpack_exports__.ConnectionError;
2965
2987
  exports.DatabaseManager = __webpack_exports__.DatabaseManager;
2966
2988
  exports.HTTPClient = __webpack_exports__.HTTPClient;
2989
+ exports.InteractiveTxConnectionLostError = __webpack_exports__.InteractiveTxConnectionLostError;
2967
2990
  exports.LibSQLAdapter = __webpack_exports__.LibSQLAdapter;
2968
2991
  exports.ODBLiteAdapter = __webpack_exports__.ODBLiteAdapter;
2969
2992
  exports.ODBLiteClient = __webpack_exports__.ODBLiteClient;
@@ -2975,6 +2998,7 @@ exports.QueryError = __webpack_exports__.QueryError;
2975
2998
  exports.SQLParser = __webpack_exports__.SQLParser;
2976
2999
  exports.ServiceClient = __webpack_exports__.ServiceClient;
2977
3000
  exports.SimpleTransaction = __webpack_exports__.SimpleTransaction;
3001
+ exports.ambientTx = __webpack_exports__.ambientTx;
2978
3002
  exports.and = __webpack_exports__.and;
2979
3003
  exports.convertTemplateToQuery = __webpack_exports__.convertTemplateToQuery;
2980
3004
  exports.createORM = __webpack_exports__.createORM;
@@ -2999,6 +3023,7 @@ exports.or = __webpack_exports__.or;
2999
3023
  exports.parseSQL = __webpack_exports__.parseSQL;
3000
3024
  exports.raw = __webpack_exports__.raw;
3001
3025
  exports.rawSQL = __webpack_exports__.rawSQL;
3026
+ exports.runInTransactionContext = __webpack_exports__.runInTransactionContext;
3002
3027
  exports.set = __webpack_exports__.set;
3003
3028
  exports.splitSQLStatements = __webpack_exports__.splitSQLStatements;
3004
3029
  exports.sql = __webpack_exports__.sql;
@@ -3006,6 +3031,7 @@ exports.sqlFragment = __webpack_exports__.sqlFragment;
3006
3031
  exports.sqlJoin = __webpack_exports__.sqlJoin;
3007
3032
  exports.sqlTemplate = __webpack_exports__.sqlTemplate;
3008
3033
  exports.sqlWhere = __webpack_exports__.sqlWhere;
3034
+ exports.txContext = __webpack_exports__.txContext;
3009
3035
  exports.updateSet = __webpack_exports__.updateSet;
3010
3036
  exports.where = __webpack_exports__.where;
3011
3037
  for(var __rspack_i in __webpack_exports__)if (-1 === [
@@ -3013,6 +3039,7 @@ for(var __rspack_i in __webpack_exports__)if (-1 === [
3013
3039
  "ConnectionError",
3014
3040
  "DatabaseManager",
3015
3041
  "HTTPClient",
3042
+ "InteractiveTxConnectionLostError",
3016
3043
  "LibSQLAdapter",
3017
3044
  "ODBLiteAdapter",
3018
3045
  "ODBLiteClient",
@@ -3024,6 +3051,7 @@ for(var __rspack_i in __webpack_exports__)if (-1 === [
3024
3051
  "SQLParser",
3025
3052
  "ServiceClient",
3026
3053
  "SimpleTransaction",
3054
+ "ambientTx",
3027
3055
  "and",
3028
3056
  "convertTemplateToQuery",
3029
3057
  "createORM",
@@ -3048,6 +3076,7 @@ for(var __rspack_i in __webpack_exports__)if (-1 === [
3048
3076
  "parseSQL",
3049
3077
  "raw",
3050
3078
  "rawSQL",
3079
+ "runInTransactionContext",
3051
3080
  "set",
3052
3081
  "splitSQLStatements",
3053
3082
  "sql",
@@ -3055,6 +3084,7 @@ for(var __rspack_i in __webpack_exports__)if (-1 === [
3055
3084
  "sqlJoin",
3056
3085
  "sqlTemplate",
3057
3086
  "sqlWhere",
3087
+ "txContext",
3058
3088
  "updateSet",
3059
3089
  "where"
3060
3090
  ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
package/dist/index.d.ts CHANGED
@@ -9,6 +9,8 @@ export { ORM, Transaction as ORMTransaction, createORM, eq, ne, gt, gte, lt, lte
9
9
  export type { ODBLiteConfig, ODBLiteConnection, QueryResult, Transaction, SQLFragment, PreparedQuery, QueryParameter, PrimitiveType, Row } from './types.ts';
10
10
  export type { ServiceClientConfig, ODBLiteDatabase, ODBLiteNode } from './service/service-client.ts';
11
11
  export type { TenancyMode, BackendType, QueryResult as DatabaseQueryResult, Connection, DatabaseAdapter, PreparedStatement as DatabasePreparedStatement, DatabaseManagerConfig, MigrationConfig, BunSQLiteConfig, LibSQLConfig, ODBLiteConfig as DatabaseODBLiteConfig, TenantInfo, ParsedStatements, SQLParserOptions, SqlQuery, SqlFragment, } from './database/index.ts';
12
+ export { ambientTx, txContext, runInTransactionContext } from './database/adapters/tx-context.ts';
13
+ export { InteractiveTxConnectionLostError } from './database/adapters/socket/client.ts';
12
14
  export { ODBLiteError, ConnectionError, QueryError } from './types.ts';
13
15
  export declare const raw: typeof ODBLiteClient.raw, identifier: typeof ODBLiteClient.identifier, where: typeof ODBLiteClient.where, insertValues: typeof ODBLiteClient.insertValues, updateSet: typeof ODBLiteClient.updateSet, join: typeof ODBLiteClient.join;
14
16
  export { odblite as default } from './core/client.ts';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGjD,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC9E,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAGhE,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAG5D,OAAO,EACL,eAAe,EACf,QAAQ,EACR,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,EACb,cAAc,EAEd,GAAG,IAAI,WAAW,EAClB,GAAG,IAAI,MAAM,EACb,KAAK,EACL,QAAQ,IAAI,WAAW,EACvB,IAAI,IAAI,OAAO,EACf,GAAG,EACH,KAAK,IAAI,QAAQ,EACjB,sBAAsB,GACvB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EACL,GAAG,EACH,WAAW,IAAI,cAAc,EAC7B,SAAS,EACT,EAAE,EACF,EAAE,EACF,EAAE,EACF,GAAG,EACH,EAAE,EACF,GAAG,EACH,IAAI,EACJ,OAAO,EACP,MAAM,EACN,SAAS,EACT,GAAG,EACH,EAAE,EACF,KAAK,cAAc,EACnB,KAAK,gBAAgB,GACtB,MAAM,gBAAgB,CAAC;AAGxB,YAAY,EACV,aAAa,EACb,iBAAiB,EACjB,WAAW,EACX,WAAW,EACX,WAAW,EACX,aAAa,EACb,cAAc,EACd,aAAa,EACb,GAAG,EACJ,MAAM,YAAY,CAAC;AAGpB,YAAY,EACV,mBAAmB,EACnB,eAAe,EACf,WAAW,EACZ,MAAM,6BAA6B,CAAC;AAGrC,YAAY,EACV,WAAW,EACX,WAAW,EACX,WAAW,IAAI,mBAAmB,EAClC,UAAU,EACV,eAAe,EACf,iBAAiB,IAAI,yBAAyB,EAC9C,qBAAqB,EACrB,eAAe,EACf,eAAe,EACf,YAAY,EACZ,aAAa,IAAI,qBAAqB,EACtC,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,QAAQ,EACR,WAAW,GACZ,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EACL,YAAY,EACZ,eAAe,EACf,UAAU,EACX,MAAM,YAAY,CAAC;AAGpB,eAAO,MACL,GAAG,4BACH,UAAU,mCACV,KAAK,8BACL,YAAY,qCACZ,SAAS,kCACT,IAAI,2BACW,CAAC;AAGlB,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGjD,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC9E,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAGhE,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAG5D,OAAO,EACL,eAAe,EACf,QAAQ,EACR,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,EACb,cAAc,EAEd,GAAG,IAAI,WAAW,EAClB,GAAG,IAAI,MAAM,EACb,KAAK,EACL,QAAQ,IAAI,WAAW,EACvB,IAAI,IAAI,OAAO,EACf,GAAG,EACH,KAAK,IAAI,QAAQ,EACjB,sBAAsB,GACvB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EACL,GAAG,EACH,WAAW,IAAI,cAAc,EAC7B,SAAS,EACT,EAAE,EACF,EAAE,EACF,EAAE,EACF,GAAG,EACH,EAAE,EACF,GAAG,EACH,IAAI,EACJ,OAAO,EACP,MAAM,EACN,SAAS,EACT,GAAG,EACH,EAAE,EACF,KAAK,cAAc,EACnB,KAAK,gBAAgB,GACtB,MAAM,gBAAgB,CAAC;AAGxB,YAAY,EACV,aAAa,EACb,iBAAiB,EACjB,WAAW,EACX,WAAW,EACX,WAAW,EACX,aAAa,EACb,cAAc,EACd,aAAa,EACb,GAAG,EACJ,MAAM,YAAY,CAAC;AAGpB,YAAY,EACV,mBAAmB,EACnB,eAAe,EACf,WAAW,EACZ,MAAM,6BAA6B,CAAC;AAGrC,YAAY,EACV,WAAW,EACX,WAAW,EACX,WAAW,IAAI,mBAAmB,EAClC,UAAU,EACV,eAAe,EACf,iBAAiB,IAAI,yBAAyB,EAC9C,qBAAqB,EACrB,eAAe,EACf,eAAe,EACf,YAAY,EACZ,aAAa,IAAI,qBAAqB,EACtC,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,QAAQ,EACR,WAAW,GACZ,MAAM,qBAAqB,CAAC;AAI7B,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,uBAAuB,EAAE,MAAM,mCAAmC,CAAC;AAClG,OAAO,EAAE,gCAAgC,EAAE,MAAM,sCAAsC,CAAC;AAGxF,OAAO,EACL,YAAY,EACZ,eAAe,EACf,UAAU,EACX,MAAM,YAAY,CAAC;AAGpB,eAAO,MACL,GAAG,4BACH,UAAU,mCACV,KAAK,8BACL,YAAY,qCACZ,SAAS,kCACT,IAAI,2BACW,CAAC;AAGlB,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,kBAAkB,CAAC"}
package/dist/index.js CHANGED
@@ -2305,6 +2305,18 @@ class SocketClient {
2305
2305
  this.failPending(new Error('odblite socket closed'));
2306
2306
  }
2307
2307
  }
2308
+ const external_node_async_hooks_ = __webpack_require__("node:async_hooks");
2309
+ const txContext = new external_node_async_hooks_.AsyncLocalStorage();
2310
+ function ambientTx(tenant) {
2311
+ const store = txContext.getStore();
2312
+ return store && store.tenant === tenant ? store.conn : void 0;
2313
+ }
2314
+ function runInTransactionContext(tenant, conn, fn) {
2315
+ return txContext.run({
2316
+ tenant,
2317
+ conn
2318
+ }, fn);
2319
+ }
2308
2320
  function toQueryResult(res) {
2309
2321
  if ('error' === res.type) throw new Error(res.error.message);
2310
2322
  const r = res.response.result;
@@ -2384,6 +2396,8 @@ class OdbliteSocketConnection {
2384
2396
  const s = 'string' == typeof sql ? sql : sql.sql;
2385
2397
  const p = 'string' == typeof sql ? params : sql.args ?? [];
2386
2398
  if (null != this.streamId) return await this.streamExec(s, p);
2399
+ const amb = ambientTx(this.tenant);
2400
+ if (amb && amb !== this) return amb.query(s, p);
2387
2401
  return toQueryResult(await this.one([
2388
2402
  {
2389
2403
  type: 'execute',
@@ -2398,6 +2412,8 @@ class OdbliteSocketConnection {
2398
2412
  const s = 'string' == typeof sql ? sql : sql.sql;
2399
2413
  const p = 'string' == typeof sql ? params : sql.args ?? [];
2400
2414
  if (null != this.streamId) return this.streamExec(s, p);
2415
+ const amb = ambientTx(this.tenant);
2416
+ if (amb && amb !== this) return amb.execute(s, p);
2401
2417
  return toQueryResult(await this.one([
2402
2418
  {
2403
2419
  type: 'execute',
@@ -2429,14 +2445,16 @@ class OdbliteSocketConnection {
2429
2445
  if (null != this.streamId) return fn(this);
2430
2446
  const tx = new OdbliteSocketConnection(this.client, this.tenant, this.client.allocStream());
2431
2447
  tx.beginPending = true;
2432
- try {
2433
- const out = await fn(tx);
2434
- if (!tx.beginPending) await tx.streamExec('COMMIT');
2435
- return out;
2436
- } catch (e) {
2437
- if (!tx.beginPending) await tx.streamExec('ROLLBACK').catch(()=>{});
2438
- throw e;
2439
- }
2448
+ return runInTransactionContext(this.tenant, tx, async ()=>{
2449
+ try {
2450
+ const out = await fn(tx);
2451
+ if (!tx.beginPending) await tx.streamExec('COMMIT');
2452
+ return out;
2453
+ } catch (e) {
2454
+ if (!tx.beginPending) await tx.streamExec('ROLLBACK').catch(()=>{});
2455
+ throw e;
2456
+ }
2457
+ });
2440
2458
  }
2441
2459
  begin(fn) {
2442
2460
  return this.transaction(fn);
@@ -2883,4 +2901,4 @@ var lte = orm.lte;
2883
2901
  var ne = orm.ne;
2884
2902
  var or = orm.or;
2885
2903
  export default odblite;
2886
- export { BunSQLiteAdapter, ConnectionError, DatabaseManager, HTTPClient, LibSQLAdapter, ODBLiteAdapter, ODBLiteClient, ODBLiteError, ODBLiteTransaction, ORM, ServiceClient, SimpleTransaction, Transaction as ORMTransaction, and, convertTemplateToQuery, createORM_0 as createORM, empty, eq, fragment, gt, gte, inArray, isNotNull, isNull, like, lt, lte, ne, odblite, or, parseSQL, set, splitSQLStatements, sql_parser_SQLParser as SQLParser, sql_parser_sql as sql, sql_template_fragment as sqlFragment, sql_template_join as sqlJoin, sql_template_raw as rawSQL, sql_template_sql as sqlTemplate, sql_template_where as sqlWhere, src_identifier as identifier, src_insertValues as insertValues, src_join as join, src_raw as raw, src_updateSet as updateSet, src_where as where, types_QueryError as QueryError };
2904
+ export { BunSQLiteAdapter, ConnectionError, DatabaseManager, HTTPClient, InteractiveTxConnectionLostError, LibSQLAdapter, ODBLiteAdapter, ODBLiteClient, ODBLiteError, ODBLiteTransaction, ORM, ServiceClient, SimpleTransaction, Transaction as ORMTransaction, ambientTx, and, convertTemplateToQuery, createORM_0 as createORM, empty, eq, fragment, gt, gte, inArray, isNotNull, isNull, like, lt, lte, ne, odblite, or, parseSQL, runInTransactionContext, set, splitSQLStatements, sql_parser_SQLParser as SQLParser, sql_parser_sql as sql, sql_template_fragment as sqlFragment, sql_template_join as sqlJoin, sql_template_raw as rawSQL, sql_template_sql as sqlTemplate, sql_template_where as sqlWhere, src_identifier as identifier, src_insertValues as insertValues, src_join as join, src_raw as raw, src_updateSet as updateSet, src_where as where, txContext, types_QueryError as QueryError };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pineliner/odb-client",
3
- "version": "1.5.3",
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",
@@ -1,6 +1,13 @@
1
1
  import type { BackendType, Connection, DatabaseAdapter, PreparedStatement, QueryResult } from '../types'
2
2
  import { ODBLiteAdapter } from './odblite'
3
3
  import { SocketClient, InteractiveTxConnectionLostError, type SocketTarget } from './socket/client'
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
+ }
4
11
  import {
5
12
  toHrana,
6
13
  fromHrana,
@@ -113,6 +120,14 @@ class OdbliteSocketConnection implements Connection {
113
120
  const s = typeof sql === 'string' ? sql : sql.sql
114
121
  const p = typeof sql === 'string' ? params : (sql.args ?? [])
115
122
  if (this.streamId != null) return (await this.streamExec(s, p)) as QueryResult<T>
123
+ const amb = ambientTx(this.tenant) // autocommit conn, but inside a tx for my tenant? → join it (see execute)
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
+ }
116
131
  return toQueryResult(await this.one([{ type: 'execute', stmt: { sql: s, args: p.map(toHrana) } }])) as QueryResult<T>
117
132
  }
118
133
 
@@ -120,6 +135,26 @@ class OdbliteSocketConnection implements Connection {
120
135
  const s = typeof sql === 'string' ? sql : sql.sql
121
136
  const p = typeof sql === 'string' ? params : (sql.args ?? [])
122
137
  if (this.streamId != null) return this.streamExec(s, p) // on the pinned stream — runs immediately, NOT buffered
138
+ // AMBIENT-TX ROUTING (odb-client owns this — the app stays oblivious): this is an autocommit connection
139
+ // (streamId == null), but if the current async flow is INSIDE an open transaction for the SAME tenant
140
+ // (e.g. an event subscriber writing during an awaited publish), route the statement onto that
141
+ // transaction's pinned stream so it JOINS the tx — sequential on the single writer — instead of taking a
142
+ // fresh autocommit write that would deadlock behind the tx on the writer slot. Same tenant key on both
143
+ // sides (this.tenant), so no auth/database-hash translation for a caller to get wrong.
144
+ const amb = ambientTx(this.tenant)
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
+ }
123
158
  return toQueryResult(await this.one([{ type: 'execute', stmt: { sql: s, args: p.map(toHrana) } }]))
124
159
  }
125
160
 
@@ -148,14 +183,19 @@ class OdbliteSocketConnection implements Connection {
148
183
  if (this.streamId != null) return fn(this) // nested → join the already-open stream
149
184
  const tx = new OdbliteSocketConnection(this.client, this.tenant, this.client.allocStream())
150
185
  tx.beginPending = true // BEGIN folds into the first statement's frame (one fewer round-trip)
151
- try {
152
- const out = await fn(tx)
153
- if (!tx.beginPending) await tx.streamExec('COMMIT') // a statement ran BEGIN was sent COMMIT it
154
- return out // else fn issued no statements → empty tx, BEGIN never sent, zero round-trips
155
- } catch (e) {
156
- if (!tx.beginPending) await tx.streamExec('ROLLBACK').catch(() => {}) // best-effort; server also reaps
157
- throw e
158
- }
186
+ // Bind `tx` as the AMBIENT transaction for this tenant while fn runs, so a nested write (e.g. an event
187
+ // subscriber during an awaited publish) that fetches a connection JOINS this stream instead of grabbing
188
+ // a fresh one that would deadlock on the single writer slot. AsyncLocalStorage follows the awaits in fn.
189
+ return runInTransactionContext(this.tenant, tx, async () => {
190
+ try {
191
+ const out = await fn(tx)
192
+ if (!tx.beginPending) await tx.streamExec('COMMIT') // a statement ran → BEGIN was sent → COMMIT it
193
+ return out // else fn issued no statements → empty tx, BEGIN never sent, zero round-trips
194
+ } catch (e) {
195
+ if (!tx.beginPending) await tx.streamExec('ROLLBACK').catch(() => {}) // best-effort; server also reaps
196
+ throw e
197
+ }
198
+ })
159
199
  }
160
200
  begin<T>(fn: (tx: Connection) => Promise<T>): Promise<T> {
161
201
  return this.transaction(fn)
@@ -0,0 +1,34 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks'
2
+ import type { Connection } from '../types'
3
+
4
+ /**
5
+ * Ambient transaction context — the "smart underneath" that lets an `await`-a-write-inside-a-transaction
6
+ * pattern work on a single-writer engine WITHOUT deadlocking, and without touching business code.
7
+ *
8
+ * The deadlock: a transaction holds the tenant's one writer slot; a nested write (e.g. an event subscriber
9
+ * running during an awaited `eventBus.publish`) that grabs a FRESH connection queues behind the tx on that
10
+ * same slot, while the tx waits for it → cycle. The fix is not to change the subscriber — it's to make the
11
+ * nested write REUSE the transaction's own connection (its pinned stream) so it runs sequentially on the
12
+ * one writer instead of contending for it.
13
+ *
14
+ * `AsyncLocalStorage` propagates through `await`, so any code running inside `transaction(fn)`'s async flow
15
+ * — including subscribers invoked during an awaited publish — can look up the open tx connection and JOIN
16
+ * it. Same technique as Prisma interactive transactions / TypeORM / .NET `TransactionScope` / Spring
17
+ * `@Transactional` propagation. Semantics: a joined write is PART of the transaction (commits/rolls back
18
+ * with it) — which is exactly what "a write issued inside a transaction" should mean. Writes that must be
19
+ * independent stay independent by running AFTER commit (fire-and-forget / post-commit), where the context
20
+ * has already exited and `ambientTx` returns undefined.
21
+ */
22
+ export const txContext = new AsyncLocalStorage<{ tenant: string; conn: Connection }>()
23
+
24
+ /** The open transaction's connection for `tenant`, if the current async flow is inside one — else
25
+ * undefined. Connection accessors (e.g. the app's getConnectionByTenantHash) return this to join the tx. */
26
+ export function ambientTx(tenant: string): Connection | undefined {
27
+ const store = txContext.getStore()
28
+ return store && store.tenant === tenant ? store.conn : undefined
29
+ }
30
+
31
+ /** Run `fn` with `conn` bound as the ambient transaction for `tenant`. Used by transaction() wrappers. */
32
+ export function runInTransactionContext<T>(tenant: string, conn: Connection, fn: () => Promise<T>): Promise<T> {
33
+ return txContext.run({ tenant, conn }, fn)
34
+ }
package/src/index.ts CHANGED
@@ -90,6 +90,11 @@ export type {
90
90
  SqlFragment,
91
91
  } from './database/index.ts';
92
92
 
93
+ // Ambient transaction context — connection accessors call ambientTx(tenant) to JOIN an open transaction
94
+ // (so a write issued inside a tx reuses its connection instead of deadlocking on the single writer slot).
95
+ export { ambientTx, txContext, runInTransactionContext } from './database/adapters/tx-context.ts';
96
+ export { InteractiveTxConnectionLostError } from './database/adapters/socket/client.ts';
97
+
93
98
  // Export error classes
94
99
  export {
95
100
  ODBLiteError,