@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.
- package/dist/database/adapters/odblite-socket.d.ts.map +1 -1
- package/dist/database/adapters/tx-context.d.ts +30 -0
- package/dist/database/adapters/tx-context.d.ts.map +1 -0
- package/dist/index.cjs +39 -9
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -9
- package/package.json +1 -1
- package/src/database/adapters/odblite-socket.ts +48 -8
- package/src/database/adapters/tx-context.ts +34 -0
- package/src/index.ts +5 -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,EAAkD,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,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
|
-
|
|
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
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
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,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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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,
|