litedbmodel 2.2.9 → 2.2.11

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.
Files changed (120) hide show
  1. package/README.md +8 -5
  2. package/dist/DBHandler.d.ts +2 -28
  3. package/dist/DBHandler.d.ts.map +1 -1
  4. package/dist/DBHandler.js.map +1 -1
  5. package/dist/DBModel.d.ts +14 -12
  6. package/dist/DBModel.d.ts.map +1 -1
  7. package/dist/DBModel.js +71 -72
  8. package/dist/DBModel.js.map +1 -1
  9. package/dist/LazyRelation.d.ts +1 -0
  10. package/dist/LazyRelation.d.ts.map +1 -1
  11. package/dist/LazyRelation.js +17 -14
  12. package/dist/LazyRelation.js.map +1 -1
  13. package/dist/Middleware.d.ts +19 -17
  14. package/dist/Middleware.d.ts.map +1 -1
  15. package/dist/Middleware.js +30 -33
  16. package/dist/Middleware.js.map +1 -1
  17. package/dist/drivers/mysql.d.ts.map +1 -1
  18. package/dist/drivers/mysql.js +1 -0
  19. package/dist/drivers/mysql.js.map +1 -1
  20. package/dist/drivers/postgres.d.ts.map +1 -1
  21. package/dist/drivers/postgres.js +42 -22
  22. package/dist/drivers/postgres.js.map +1 -1
  23. package/dist/drivers/types.d.ts +2 -26
  24. package/dist/drivers/types.d.ts.map +1 -1
  25. package/dist/drivers/types.js.map +1 -1
  26. package/dist/index.cjs +224 -163
  27. package/dist/scp/connection-routing.d.ts +50 -18
  28. package/dist/scp/connection-routing.d.ts.map +1 -1
  29. package/dist/scp/connection-routing.js +55 -24
  30. package/dist/scp/connection-routing.js.map +1 -1
  31. package/dist/scp/dbmodel-runtime.d.ts +3 -0
  32. package/dist/scp/dbmodel-runtime.d.ts.map +1 -1
  33. package/dist/scp/dbmodel-runtime.js +2 -0
  34. package/dist/scp/dbmodel-runtime.js.map +1 -1
  35. package/dist/scp/exec-context.d.ts.map +1 -1
  36. package/dist/scp/exec-context.js +54 -46
  37. package/dist/scp/exec-context.js.map +1 -1
  38. package/dist/scp/index.cjs +99 -48
  39. package/dist/scp/index.mjs +96 -47
  40. package/dist/scp/makesql/pool-executor.d.ts +4 -1
  41. package/dist/scp/makesql/pool-executor.d.ts.map +1 -1
  42. package/dist/scp/makesql/pool-executor.js +21 -3
  43. package/dist/scp/makesql/pool-executor.js.map +1 -1
  44. package/dist/types.d.ts +9 -7
  45. package/dist/types.d.ts.map +1 -1
  46. package/dist/types.js.map +1 -1
  47. package/docs/BENCHMARK.md +1 -1
  48. package/docs/COMPARISON.md +2 -2
  49. package/docs/api/README.md +1 -1
  50. package/docs/api/classes/Conditions.md +1 -1
  51. package/docs/api/classes/DBHandler.md +16 -16
  52. package/docs/api/classes/DBModel.md +94 -93
  53. package/docs/api/classes/LimitExceededError.md +8 -8
  54. package/docs/api/classes/Middleware.md +35 -31
  55. package/docs/api/classes/SqlRaw.md +1 -1
  56. package/docs/api/classes/SqlRef.md +1 -1
  57. package/docs/api/classes/Values.md +1 -1
  58. package/docs/api/classes/WriteInReadOnlyContextError.md +5 -5
  59. package/docs/api/classes/WriteOutsideTransactionError.md +5 -5
  60. package/docs/api/functions/belongsTo.md +1 -1
  61. package/docs/api/functions/closeAllPools.md +2 -2
  62. package/docs/api/functions/condsToRecord.md +1 -1
  63. package/docs/api/functions/createMiddleware.md +4 -4
  64. package/docs/api/functions/createPostgresDriver.md +2 -2
  65. package/docs/api/functions/createSqliteDriver.md +1 -1
  66. package/docs/api/functions/dbCast.md +1 -1
  67. package/docs/api/functions/dbCastIn.md +1 -1
  68. package/docs/api/functions/dbDynamic.md +1 -1
  69. package/docs/api/functions/dbFalse.md +1 -1
  70. package/docs/api/functions/dbImmediate.md +1 -1
  71. package/docs/api/functions/dbIn.md +1 -1
  72. package/docs/api/functions/dbNotNull.md +1 -1
  73. package/docs/api/functions/dbNow.md +1 -1
  74. package/docs/api/functions/dbNull.md +1 -1
  75. package/docs/api/functions/dbRaw.md +1 -1
  76. package/docs/api/functions/dbTrue.md +1 -1
  77. package/docs/api/functions/dbUuid.md +1 -1
  78. package/docs/api/functions/dbUuidIn.md +1 -1
  79. package/docs/api/functions/formatLocalDate.md +1 -1
  80. package/docs/api/functions/formatUTCDate.md +1 -1
  81. package/docs/api/functions/getDBConfig.md +2 -2
  82. package/docs/api/functions/getDBHandler.md +2 -2
  83. package/docs/api/functions/getTransactionConnection.md +2 -2
  84. package/docs/api/functions/getTransactionContext.md +2 -2
  85. package/docs/api/functions/hasMany.md +1 -1
  86. package/docs/api/functions/hasOne.md +1 -1
  87. package/docs/api/functions/initDBHandler.md +2 -2
  88. package/docs/api/functions/isAnySqlFragment.md +1 -1
  89. package/docs/api/functions/isConnectionError.md +1 -1
  90. package/docs/api/functions/isSqlCondition.md +1 -1
  91. package/docs/api/functions/isSqlFragment.md +1 -1
  92. package/docs/api/functions/isSqlRaw.md +1 -1
  93. package/docs/api/functions/isSqlRef.md +1 -1
  94. package/docs/api/functions/isSqlTypedFragment.md +1 -1
  95. package/docs/api/functions/model.md +1 -1
  96. package/docs/api/functions/parentRef.md +1 -1
  97. package/docs/api/functions/sql.md +1 -1
  98. package/docs/api/globals.md +5 -5
  99. package/docs/api/interfaces/Column.md +1 -1
  100. package/docs/api/interfaces/ColumnDecorator.md +1 -1
  101. package/docs/api/interfaces/ColumnOptions.md +1 -1
  102. package/docs/api/interfaces/CreatedMiddlewareClass.md +7 -7
  103. package/docs/api/interfaces/DBConfig.md +29 -14
  104. package/docs/api/interfaces/DBConfigOptions.md +8 -8
  105. package/docs/api/interfaces/DeleteOptions.md +4 -4
  106. package/docs/api/interfaces/InsertOptions.md +8 -8
  107. package/docs/api/interfaces/LimitConfig.md +4 -4
  108. package/docs/api/interfaces/MiddlewareConfig.md +15 -15
  109. package/docs/api/interfaces/ModelClassDecorator.md +1 -1
  110. package/docs/api/interfaces/ModelOptions.md +1 -1
  111. package/docs/api/interfaces/PkeyResult.md +1 -1
  112. package/docs/api/interfaces/RelationDecorator.md +1 -1
  113. package/docs/api/interfaces/SelectOptions.md +17 -17
  114. package/docs/api/interfaces/SqlCondition.md +1 -1
  115. package/docs/api/interfaces/SqlFragment.md +1 -1
  116. package/docs/api/interfaces/SqlTypedFragment.md +1 -1
  117. package/docs/api/interfaces/TransactionOptions.md +7 -7
  118. package/docs/api/interfaces/UpdateManyOptions.md +4 -4
  119. package/docs/api/interfaces/UpdateOptions.md +4 -4
  120. package/package.json +2 -2
package/dist/index.cjs CHANGED
@@ -3092,11 +3092,27 @@ function mysqlConnection(conn) {
3092
3092
  }
3093
3093
  };
3094
3094
  }
3095
- function mysqlConnectionPool(pool) {
3095
+ function mysqlConnectionPool(pool, acquireTimeoutMillis) {
3096
3096
  const handles = /* @__PURE__ */ new WeakMap();
3097
3097
  return {
3098
3098
  async acquire() {
3099
- const raw = await pool.getConnection();
3099
+ const pending = pool.getConnection();
3100
+ const raw = acquireTimeoutMillis === void 0 ? await pending : await new Promise((resolve, reject) => {
3101
+ const timer = setTimeout(() => {
3102
+ pending.then((late) => late.release(), () => void 0);
3103
+ reject(new Error(`timeout exceeded when trying to get a connection (${acquireTimeoutMillis} ms)`));
3104
+ }, acquireTimeoutMillis);
3105
+ pending.then(
3106
+ (c) => {
3107
+ clearTimeout(timer);
3108
+ resolve(c);
3109
+ },
3110
+ (e) => {
3111
+ clearTimeout(timer);
3112
+ reject(e);
3113
+ }
3114
+ );
3115
+ });
3100
3116
  const conn = mysqlConnection(raw);
3101
3117
  handles.set(conn, raw);
3102
3118
  return conn;
@@ -3121,6 +3137,9 @@ function pgPoolFactory(pg) {
3121
3137
  // SIZING (construction knob): the config is the sole source of the cap.
3122
3138
  max: config.maxPool,
3123
3139
  min: config.minPool,
3140
+ // pg-pool bounds BOTH a new connection and the wait for a free one with this one limit.
3141
+ connectionTimeoutMillis: config.connectTimeoutMillis,
3142
+ idleTimeoutMillis: config.idleTimeoutMillis,
3124
3143
  // keepAlive (construction knob): TCP keepalive for serverless/long-idle connections.
3125
3144
  keepAlive: config.keepAlive,
3126
3145
  ...config.keepAlive ? { keepAliveInitialDelayMillis: config.keepAliveInitialDelayMillis } : {}
@@ -3138,12 +3157,15 @@ function mysqlPoolFactory(mysql2) {
3138
3157
  ...config.password !== void 0 ? { password: config.password } : {},
3139
3158
  // SIZING (construction knob): connectionLimit = maxPool (the sole source of the cap).
3140
3159
  connectionLimit: config.maxPool,
3160
+ connectTimeout: config.connectTimeoutMillis,
3161
+ idleTimeout: config.idleTimeoutMillis,
3141
3162
  // keepAlive (construction knob).
3142
3163
  enableKeepAlive: config.keepAlive,
3143
3164
  ...config.keepAlive ? { keepAliveInitialDelay: config.keepAliveInitialDelayMillis } : {},
3144
3165
  ...mysqlDeboxPoolOptions
3145
3166
  });
3146
- return { pool: mysqlConnectionPool(pool), close: () => pool.end() };
3167
+ const acquireTimeout = config.connectTimeoutMillis > 0 ? config.connectTimeoutMillis : void 0;
3168
+ return { pool: mysqlConnectionPool(pool, acquireTimeout), close: () => pool.end() };
3147
3169
  };
3148
3170
  }
3149
3171
 
@@ -3179,6 +3201,7 @@ function getPool(config) {
3179
3201
  password: config.password,
3180
3202
  max: config.max || 10,
3181
3203
  connectionTimeoutMillis: (config.timeout || 30) * 1e3,
3204
+ idleTimeoutMillis: config.idleTimeoutMillis ?? 1e4,
3182
3205
  query_timeout: (config.queryTimeout || 30) * 1e3,
3183
3206
  keepAlive: config.keepAlive,
3184
3207
  keepAliveInitialDelayMillis: config.keepAliveInitialDelayMillis
@@ -3209,33 +3232,44 @@ function convertPlaceholders(sql2) {
3209
3232
  let index = 0;
3210
3233
  return sql2.replace(/\?/g, () => `$${++index}`);
3211
3234
  }
3235
+ var MAX_NAMED_STATEMENTS = 1e3;
3236
+ var MAX_UNNAMED_QUERIES = 1e3;
3212
3237
  var preparedStmtCounter = 0;
3213
3238
  var queryCache = /* @__PURE__ */ new Map();
3239
+ var unnamedQueries = /* @__PURE__ */ new Map();
3214
3240
  function resolveQuery(sql2) {
3215
3241
  let cached = queryCache.get(sql2);
3216
- if (!cached) {
3217
- const convertedSql = convertPlaceholders(sql2);
3218
- let isMulti = false;
3219
- let end = convertedSql.length - 1;
3220
- while (end >= 0 && (convertedSql[end] === " " || convertedSql[end] === "\n" || convertedSql[end] === " " || convertedSql[end] === "\r")) end--;
3221
- if (end >= 0 && convertedSql[end] === ";") end--;
3222
- for (let i = 0; i <= end; i++) {
3223
- if (convertedSql[i] === ";") {
3224
- isMulti = true;
3225
- break;
3226
- }
3227
- }
3228
- cached = {
3229
- convertedSql,
3230
- isMulti,
3231
- name: `ldb_${++preparedStmtCounter}`
3232
- };
3233
- queryCache.set(sql2, cached);
3242
+ if (cached) return cached;
3243
+ cached = unnamedQueries.get(sql2);
3244
+ if (cached) {
3245
+ if (preparedStmtCounter < MAX_NAMED_STATEMENTS) {
3246
+ unnamedQueries.delete(sql2);
3247
+ cached.name = `ldb_${++preparedStmtCounter}`;
3248
+ queryCache.set(sql2, cached);
3249
+ }
3250
+ return cached;
3251
+ }
3252
+ const convertedSql = convertPlaceholders(sql2);
3253
+ let isMulti = false;
3254
+ let end = convertedSql.length - 1;
3255
+ while (end >= 0 && (convertedSql[end] === " " || convertedSql[end] === "\n" || convertedSql[end] === " " || convertedSql[end] === "\r")) end--;
3256
+ if (end >= 0 && convertedSql[end] === ";") end--;
3257
+ for (let i = 0; i <= end; i++) {
3258
+ if (convertedSql[i] === ";") {
3259
+ isMulti = true;
3260
+ break;
3261
+ }
3262
+ }
3263
+ cached = { convertedSql, isMulti };
3264
+ if (unnamedQueries.size >= MAX_UNNAMED_QUERIES) {
3265
+ unnamedQueries.delete(unnamedQueries.keys().next().value);
3234
3266
  }
3267
+ unnamedQueries.set(sql2, cached);
3235
3268
  return cached;
3236
3269
  }
3237
3270
  function clearQueryCache() {
3238
3271
  queryCache.clear();
3272
+ unnamedQueries.clear();
3239
3273
  preparedStmtCounter = 0;
3240
3274
  }
3241
3275
  var PostgresConnection = class {
@@ -3581,6 +3615,7 @@ function getPool2(config) {
3581
3615
  waitForConnections: true,
3582
3616
  connectionLimit: config.max || 10,
3583
3617
  connectTimeout: (config.timeout || 30) * 1e3,
3618
+ idleTimeout: config.idleTimeoutMillis ?? 1e4,
3584
3619
  // Read de-box (issue #9): make BIGINT arrive as an exact string (else mysql2 rounds it to a JS
3585
3620
  // number past 2^53) and the DATE/TIMESTAMP/DATETIME family arrive as its native textual string
3586
3621
  // (else a TZ-shifted JS Date) — the coercible forms `materializeCell` (int64/date) expects. Same
@@ -4277,6 +4312,8 @@ function resolveConnectionConfig(config = {}) {
4277
4312
  keepAliveInitialDelayMillis: config.keepAliveInitialDelayMillis ?? 1e4,
4278
4313
  minPool: config.minPool ?? 0,
4279
4314
  maxPool: config.maxPool ?? 10,
4315
+ connectTimeoutMillis: config.connectTimeoutMillis ?? 3e4,
4316
+ idleTimeoutMillis: config.idleTimeoutMillis ?? 1e4,
4280
4317
  ...config.searchPath !== void 0 ? { searchPath: config.searchPath } : {},
4281
4318
  ...config.charset !== void 0 ? { charset: config.charset } : {}
4282
4319
  };
@@ -4380,14 +4417,15 @@ var writerScopeStore = new import_node_async_hooks2.AsyncLocalStorage();
4380
4417
  function inWriterScope() {
4381
4418
  return writerScopeStore.getStore() === true;
4382
4419
  }
4420
+ function latestStamp(cell) {
4421
+ let latest = null;
4422
+ for (let c = cell; c; c = c.prev) {
4423
+ if (c.at !== null && (latest === null || c.at > latest)) latest = c.at;
4424
+ }
4425
+ return latest;
4426
+ }
4383
4427
  var WriterStickyClock = class {
4384
- /**
4385
- * `null` until the first `mark()` — absence, NOT a value sentinel. The injectable clocks legitimately
4386
- * return 0 (a monotonic clock right after process start; rust's `SystemClock` does), so encoding
4387
- * "never marked" as the value 0 would mis-classify a `mark()` at clock t=0 as unmarked and leak the
4388
- * read-your-writes read to the reader replica. Absence must be distinct from the value 0.
4389
- */
4390
- lastWriteAt = null;
4428
+ chain = new import_node_async_hooks2.AsyncLocalStorage();
4391
4429
  enabled;
4392
4430
  stickyDurationMs;
4393
4431
  /** Injectable clock (tests advance it); defaults to `Date.now`. */
@@ -4397,18 +4435,37 @@ var WriterStickyClock = class {
4397
4435
  this.stickyDurationMs = opts.writerStickyDuration ?? 5e3;
4398
4436
  this.now = opts.now ?? Date.now;
4399
4437
  }
4400
- /** Record that a write/commit just happened (the tx runtime calls this on success). */
4438
+ /**
4439
+ * Start tracking a transaction in the current chain: enters a fresh cell that carries the chain's
4440
+ * latest stamp. Must run synchronously at the start of the transaction, before its first `await`.
4441
+ */
4442
+ begin() {
4443
+ const current = this.chain.getStore();
4444
+ let open = current ?? null;
4445
+ while (open !== null && !open.open) open = open.prev;
4446
+ const cell = { at: latestStamp(current), open: true, prev: open };
4447
+ this.chain.enterWith(cell);
4448
+ return cell;
4449
+ }
4450
+ /** The transaction that began `cell` is over (committed or not). */
4451
+ end(cell) {
4452
+ cell.open = false;
4453
+ }
4454
+ /** Record that the current chain just committed. */
4401
4455
  mark() {
4402
- if (this.enabled) this.lastWriteAt = this.now();
4456
+ if (!this.enabled) return;
4457
+ let cell = this.chain.getStore();
4458
+ if (cell === void 0) {
4459
+ cell = { at: null, open: false, prev: null };
4460
+ this.chain.enterWith(cell);
4461
+ }
4462
+ cell.at = this.now();
4403
4463
  }
4404
- /** Is a read currently sticky-to-writer (within `writerStickyDuration` of the last write)? */
4464
+ /** Is a read in the current chain sticky-to-writer (within `writerStickyDuration` of its last commit)? */
4405
4465
  isSticky() {
4406
- if (!this.enabled || this.lastWriteAt === null) return false;
4407
- return this.now() - this.lastWriteAt < this.stickyDurationMs;
4408
- }
4409
- /** Reset the clock (e.g. between tests / on `closeAllPools`). */
4410
- reset() {
4411
- this.lastWriteAt = null;
4466
+ if (!this.enabled) return false;
4467
+ const at = latestStamp(this.chain.getStore());
4468
+ return at !== null && this.now() - at < this.stickyDurationMs;
4412
4469
  }
4413
4470
  };
4414
4471
  function resolvePool(intent, routing) {
@@ -4693,38 +4750,43 @@ async function withTransactionAsync(ctx, fn, opts = {}, dialect = "postgres", is
4693
4750
  const resolved = resolveTxOptions(opts);
4694
4751
  const begins = beginStatements(dialect, resolved.isolation);
4695
4752
  const pool = ctx.connectionPoolFor({ write: true, ...connection !== void 0 ? { db: connection } : {} });
4696
- let attempt = 0;
4697
- for (; ; ) {
4698
- attempt++;
4699
- const conn = await pool.acquire();
4700
- const txCtx = ctx.withConnection(conn, true);
4701
- let poisoned = false;
4702
- const attemptResult = await runWithPinnedAsyncConnection(conn, connection ?? null, async () => {
4703
- for (const begin of begins) await runAsync(txCtx, begin, []);
4704
- try {
4705
- const r = await runInTransactionScope(() => fn(txCtx));
4706
- await runAsync(txCtx, resolved.rollbackOnly ? "ROLLBACK" : "COMMIT", []);
4707
- return { ok: true, value: r };
4708
- } catch (error2) {
4753
+ const stickyCell = ctx.stickyClock.begin();
4754
+ try {
4755
+ let attempt = 0;
4756
+ for (; ; ) {
4757
+ attempt++;
4758
+ const conn = await pool.acquire();
4759
+ const txCtx = ctx.withConnection(conn, true);
4760
+ let poisoned = false;
4761
+ const attemptResult = await runWithPinnedAsyncConnection(conn, connection ?? null, async () => {
4762
+ for (const begin of begins) await runAsync(txCtx, begin, []);
4709
4763
  try {
4710
- await runAsync(txCtx, "ROLLBACK", []);
4711
- } catch {
4712
- poisoned = true;
4764
+ const r = await runInTransactionScope(() => fn(txCtx));
4765
+ await runAsync(txCtx, resolved.rollbackOnly ? "ROLLBACK" : "COMMIT", []);
4766
+ return { ok: true, value: r };
4767
+ } catch (error2) {
4768
+ try {
4769
+ await runAsync(txCtx, "ROLLBACK", []);
4770
+ } catch {
4771
+ poisoned = true;
4772
+ }
4773
+ return { ok: false, error: error2 };
4713
4774
  }
4714
- return { ok: false, error: error2 };
4775
+ });
4776
+ await pool.release(conn, poisoned || !attemptResult.ok);
4777
+ if (attemptResult.ok) {
4778
+ if (!resolved.rollbackOnly && resolved.useWriterAfterTransaction) ctx.stickyClock.mark();
4779
+ return attemptResult.value;
4715
4780
  }
4716
- });
4717
- await pool.release(conn, poisoned || !attemptResult.ok);
4718
- if (attemptResult.ok) {
4719
- if (!resolved.rollbackOnly && resolved.useWriterAfterTransaction) ctx.stickyClock.mark();
4720
- return attemptResult.value;
4721
- }
4722
- const { error } = attemptResult;
4723
- if (resolved.retryOnError && attempt < resolved.retryLimit && isRetryableTxError(error, isConnectionError2)) {
4724
- await sleep(resolved.retryDuration * Math.pow(2, attempt - 1));
4725
- continue;
4781
+ const { error } = attemptResult;
4782
+ if (resolved.retryOnError && attempt < resolved.retryLimit && isRetryableTxError(error, isConnectionError2)) {
4783
+ await sleep(resolved.retryDuration * Math.pow(2, attempt - 1));
4784
+ continue;
4785
+ }
4786
+ throw error;
4726
4787
  }
4727
- throw error;
4788
+ } finally {
4789
+ ctx.stickyClock.end(stickyCell);
4728
4790
  }
4729
4791
  }
4730
4792
  function transaction(ctx, fn, options = {}, dialect = "postgres", isConnectionError2 = defaultIsConnectionError, connection) {
@@ -4857,11 +4919,13 @@ function compareCore(op, a, b) {
4857
4919
  if (typeof a === "bigint" && typeof b === "bigint")
4858
4920
  c = a < b ? -1 : a > b ? 1 : 0;
4859
4921
  else if (typeof a === "number" && typeof b === "number")
4860
- c = a < b ? -1 : a > b ? 1 : 0;
4922
+ c = a < b ? -1 : a > b ? 1 : a === b ? 0 : null;
4861
4923
  else if (typeof a === "string" && typeof b === "string")
4862
4924
  c = cmpCodePoints(a, b);
4863
4925
  else
4864
4926
  fail("TYPE_MISMATCH", `${op}: \u540C\u4E00\u578B\u306E int/float/string \u306E\u307F\uFF08got ${typeName(a)}\xD7${typeName(b)}\uFF09`);
4927
+ if (c === null)
4928
+ return false;
4865
4929
  switch (op) {
4866
4930
  case "lt":
4867
4931
  return c < 0;
@@ -7203,6 +7267,8 @@ function toConnectionConfig(config) {
7203
7267
  ...config.user !== void 0 ? { user: config.user } : {},
7204
7268
  ...config.password !== void 0 ? { password: config.password } : {},
7205
7269
  ...config.max !== void 0 ? { maxPool: config.max } : {},
7270
+ ...config.timeout !== void 0 ? { connectTimeoutMillis: config.timeout * 1e3 } : {},
7271
+ ...config.idleTimeoutMillis !== void 0 ? { idleTimeoutMillis: config.idleTimeoutMillis } : {},
7206
7272
  ...config.keepAlive !== void 0 ? { keepAlive: config.keepAlive } : {},
7207
7273
  ...config.keepAliveInitialDelayMillis !== void 0 ? { keepAliveInitialDelayMillis: config.keepAliveInitialDelayMillis } : {}
7208
7274
  };
@@ -7480,52 +7546,46 @@ async function closeAllPools2() {
7480
7546
 
7481
7547
  // src/Middleware.ts
7482
7548
  var import_async_hooks = require("async_hooks");
7483
- var Middleware = class {
7484
- /** AsyncLocalStorage for per-request instances */
7485
- static _storage;
7486
- /** Get or create storage for this class */
7487
- static getStorage() {
7488
- if (!this.hasOwnProperty("_storage") || !this._storage) {
7489
- this._storage = new import_async_hooks.AsyncLocalStorage();
7490
- }
7491
- return this._storage;
7492
- }
7549
+ var requestScope = new import_async_hooks.AsyncLocalStorage();
7550
+ var Middleware = class _Middleware {
7493
7551
  /**
7494
- * Get current request's instance.
7495
- * Creates a new instance on first access within a request.
7552
+ * Get the current request's instance of this middleware. Inside a {@link Middleware.run} scope the
7553
+ * instance is created on first access and shared by the rest of the scope; outside any scope each
7554
+ * call gets a new instance that lives only for that call.
7496
7555
  */
7497
7556
  static getCurrentContext() {
7498
- const storage = this.getStorage();
7499
- let instance = storage.getStore();
7557
+ const scope = requestScope.getStore();
7558
+ let instance = scope?.get(this);
7500
7559
  if (!instance) {
7501
7560
  instance = new this();
7502
- storage.enterWith(instance);
7561
+ instance.init?.();
7562
+ scope?.set(this, instance);
7503
7563
  }
7504
7564
  return instance;
7505
7565
  }
7506
7566
  /**
7507
- * Run a function with a fresh middleware context.
7508
- * Useful for explicit context boundaries (e.g., in tests).
7567
+ * Run `fn` as one request: the middleware instances `fn` uses (directly or through DBModel
7568
+ * operations) are created inside the scope and are not shared with any other request. Call it at
7569
+ * the request entry. `Middleware.run` gives every middleware a fresh instance; `X.run` gives `X` a
7570
+ * fresh instance and keeps the enclosing scope's instances of the others.
7509
7571
  */
7510
7572
  static run(fn) {
7511
- const storage = this.getStorage();
7512
- const instance = new this();
7513
- return storage.run(instance, fn);
7573
+ const enclosing = requestScope.getStore();
7574
+ const scope = new Map(this === _Middleware || enclosing === void 0 ? [] : enclosing);
7575
+ scope.delete(this);
7576
+ return requestScope.run(scope, fn);
7514
7577
  }
7515
7578
  /**
7516
- * Check if currently in a context
7579
+ * Check if the current request scope holds an instance of this middleware
7517
7580
  */
7518
7581
  static hasContext() {
7519
- return this.getStorage().getStore() !== void 0;
7582
+ return requestScope.getStore()?.has(this) ?? false;
7520
7583
  }
7521
7584
  /**
7522
- * Clear current context (for testing)
7585
+ * Drop this middleware's instance from the current request scope (for testing)
7523
7586
  */
7524
7587
  static clearContext() {
7525
- const storage = this.getStorage();
7526
- if (storage.getStore()) {
7527
- storage.enterWith(void 0);
7528
- }
7588
+ requestScope.getStore()?.delete(this);
7529
7589
  }
7530
7590
  };
7531
7591
  function createMiddleware(config) {
@@ -7586,6 +7646,7 @@ function inferPgArrayType2(values, sqlCast) {
7586
7646
  var LazyRelationContext = class {
7587
7647
  sourceClass;
7588
7648
  records;
7649
+ /** Batch loads by cache key — held from the start, so concurrent first accesses share one load. */
7589
7650
  batchCache = /* @__PURE__ */ new Map();
7590
7651
  constructor(sourceClass, records) {
7591
7652
  this.sourceClass = sourceClass;
@@ -7599,10 +7660,16 @@ var LazyRelationContext = class {
7599
7660
  */
7600
7661
  async getRelation(record, relationType, config) {
7601
7662
  const cacheKey = this.getCacheKey(relationType, config);
7602
- if (!this.batchCache.has(cacheKey)) {
7603
- await this.batchLoadRelation(relationType, config);
7663
+ let pending = this.batchCache.get(cacheKey);
7664
+ if (!pending) {
7665
+ const load = this.batchLoadRelation(relationType, config);
7666
+ pending = load;
7667
+ this.batchCache.set(cacheKey, load);
7668
+ load.catch(() => {
7669
+ if (this.batchCache.get(cacheKey) === load) this.batchCache.delete(cacheKey);
7670
+ });
7604
7671
  }
7605
- const relationMap = this.batchCache.get(cacheKey);
7672
+ const relationMap = await pending;
7606
7673
  const isCompositeKey = config.targetKeys && config.targetKeys.length > 0 || config.sourceKeys && config.sourceKeys.length > 0;
7607
7674
  let mapKey;
7608
7675
  if (isCompositeKey) {
@@ -7669,9 +7736,7 @@ var LazyRelationContext = class {
7669
7736
  }
7670
7737
  }
7671
7738
  if (sourceKeyValues.length === 0 && compositeKeyValues.length === 0) {
7672
- const cacheKey2 = this.getCacheKey(relationType, config);
7673
- this.batchCache.set(cacheKey2, /* @__PURE__ */ new Map());
7674
- return;
7739
+ return /* @__PURE__ */ new Map();
7675
7740
  }
7676
7741
  const driverType = TargetClass.getDriverType();
7677
7742
  let results;
@@ -7682,9 +7747,7 @@ var LazyRelationContext = class {
7682
7747
  const sourceKeys = config.sourceKeys || [config.sourceKey || "id"];
7683
7748
  const uniqueTuples = this.getUniqueCompositeKeyTuples(compositeKeyValues, sourceKeys);
7684
7749
  if (uniqueTuples.length === 0) {
7685
- const cacheKey2 = this.getCacheKey(relationType, config);
7686
- this.batchCache.set(cacheKey2, /* @__PURE__ */ new Map());
7687
- return;
7750
+ return /* @__PURE__ */ new Map();
7688
7751
  }
7689
7752
  if (needsLimitedLoading) {
7690
7753
  if (driverType === "postgres") {
@@ -7733,8 +7796,7 @@ var LazyRelationContext = class {
7733
7796
  relationMap.set(keyValue, result);
7734
7797
  }
7735
7798
  }
7736
- const cacheKey = this.getCacheKey(relationType, config);
7737
- this.batchCache.set(cacheKey, relationMap);
7799
+ return relationMap;
7738
7800
  }
7739
7801
  /**
7740
7802
  * PostgreSQL optimized: Single key batch load using = ANY($1::type[])
@@ -8153,8 +8215,8 @@ var DBModel = class _DBModel {
8153
8215
  useWriterAfterTransaction: true,
8154
8216
  writerStickyDuration: 5e3
8155
8217
  };
8156
- /** Last transaction completion time (for writer sticky) */
8157
- static _lastTransactionTime = 0;
8218
+ /** Writer-sticky clock of the v1 execution path (the SCP runtime carries its own in its routing). */
8219
+ static _writerSticky = new WriterStickyClock();
8158
8220
  /**
8159
8221
  * The Phase A-E SCP runtime context (built lazily from `_dbConfig` for the live pg/mysql dialects).
8160
8222
  * `null` when not yet built; `false` when the config is sqlite / uninitialized (⇒ v1 in-proc path,
@@ -8192,6 +8254,8 @@ var DBModel = class _DBModel {
8192
8254
  ...config.user !== void 0 ? { user: config.user } : {},
8193
8255
  ...config.password !== void 0 ? { password: config.password } : {},
8194
8256
  ...config.max !== void 0 ? { max: config.max } : {},
8257
+ ...config.timeout !== void 0 ? { timeout: config.timeout } : {},
8258
+ ...config.idleTimeoutMillis !== void 0 ? { idleTimeoutMillis: config.idleTimeoutMillis } : {},
8195
8259
  ...config.keepAlive !== void 0 ? { keepAlive: config.keepAlive } : {},
8196
8260
  ...config.keepAliveInitialDelayMillis !== void 0 ? { keepAliveInitialDelayMillis: config.keepAliveInitialDelayMillis } : {},
8197
8261
  driver
@@ -8306,6 +8370,10 @@ var DBModel = class _DBModel {
8306
8370
  ...this._configOptions,
8307
8371
  ...options
8308
8372
  };
8373
+ this._writerSticky = new WriterStickyClock({
8374
+ useWriterAfterTransaction: this._configOptions.useWriterAfterTransaction,
8375
+ writerStickyDuration: this._configOptions.writerStickyDuration
8376
+ });
8309
8377
  this._resetScpRuntime();
8310
8378
  }
8311
8379
  /**
@@ -8378,12 +8446,9 @@ var DBModel = class _DBModel {
8378
8446
  }
8379
8447
  }
8380
8448
  /**
8381
- * Get a DBHandler instance for this model.
8382
- * Connection priority:
8383
- * 1. Transaction connection (if in transaction)
8384
- * 2. Writer context (if in withWriter())
8385
- * 3. Writer sticky (if within writerStickyDuration after transaction)
8386
- * 4. Default reader connection
8449
+ * Get a DBHandler instance for this model: the transaction's handler inside a transaction, else the
8450
+ * default handler (whose reads `execute()` sends to the writer inside `withWriter()` or while
8451
+ * writer-sticky).
8387
8452
  */
8388
8453
  static getHandler() {
8389
8454
  const txContext = this._getTransactionContext().getStore();
@@ -8393,24 +8458,14 @@ var DBModel = class _DBModel {
8393
8458
  }
8394
8459
  return txContext.handler;
8395
8460
  }
8396
- const handler = getDBHandler();
8397
- if (this._getWriterContext().getStore()) {
8398
- return handler;
8399
- }
8400
- if (this._shouldUseWriterSticky()) {
8401
- return handler;
8402
- }
8403
- return handler;
8461
+ return getDBHandler();
8404
8462
  }
8405
8463
  /**
8406
- * Check if we should use writer due to sticky after transaction.
8464
+ * Check if this async chain should read from the writer because it committed a transaction within
8465
+ * `writerStickyDuration` (read-your-writes for the request that wrote).
8407
8466
  */
8408
8467
  static _shouldUseWriterSticky() {
8409
- if (!this._configOptions.useWriterAfterTransaction) {
8410
- return false;
8411
- }
8412
- const stickyDuration = this._configOptions.writerStickyDuration ?? 5e3;
8413
- return Date.now() - this._lastTransactionTime < stickyDuration;
8468
+ return this._writerSticky.isSticky();
8414
8469
  }
8415
8470
  /**
8416
8471
  * Get transaction context storage.
@@ -8465,8 +8520,11 @@ var DBModel = class _DBModel {
8465
8520
  *
8466
8521
  * DBModel.use(LoggerMiddleware);
8467
8522
  *
8468
- * // After queries
8469
- * console.log(LoggerMiddleware.getCurrentContext().getLogs());
8523
+ * // One request
8524
+ * await Middleware.run(async () => {
8525
+ * await handleRequest();
8526
+ * console.log(LoggerMiddleware.getCurrentContext().getLogs());
8527
+ * });
8470
8528
  * ```
8471
8529
  */
8472
8530
  static use(MiddlewareClass) {
@@ -8515,7 +8573,7 @@ var DBModel = class _DBModel {
8515
8573
  * // Register
8516
8574
  * DBModel.use(TenantMiddleware);
8517
8575
  *
8518
- * // Set tenant for current request (type-safe)
8576
+ * // Set tenant for the current request (inside Middleware.run at the request entry, type-safe)
8519
8577
  * TenantMiddleware.getCurrentContext().tenantId = 123;
8520
8578
  * ```
8521
8579
  */
@@ -10316,8 +10374,7 @@ var DBModel = class _DBModel {
10316
10374
  }
10317
10375
  const rt = this._getScpRuntime();
10318
10376
  if (rt !== null && this._scpUsable()) {
10319
- const useWriterAfterTx = options.useWriterAfterTransaction ?? this._configOptions.useWriterAfterTransaction ?? true;
10320
- const result = await transaction(
10377
+ return transaction(
10321
10378
  rt.ctx,
10322
10379
  () => {
10323
10380
  const sentinel = {
@@ -10329,8 +10386,6 @@ var DBModel = class _DBModel {
10329
10386
  options,
10330
10387
  rt.dialect
10331
10388
  );
10332
- if (useWriterAfterTx && !(options.rollbackOnly ?? false)) this._lastTransactionTime = Date.now();
10333
- return result;
10334
10389
  }
10335
10390
  const handler = this.getHandler();
10336
10391
  const retryOnError = options.retryOnError ?? true;
@@ -10338,38 +10393,41 @@ var DBModel = class _DBModel {
10338
10393
  const retryDuration = options.retryDuration ?? 200;
10339
10394
  const rollbackOnly = options.rollbackOnly ?? false;
10340
10395
  const useWriterAfterTransaction = options.useWriterAfterTransaction ?? this._configOptions.useWriterAfterTransaction ?? true;
10341
- let attempt = 0;
10342
- while (true) {
10343
- attempt++;
10344
- const connection = await handler.getConnection();
10345
- let destroyConnection = false;
10346
- try {
10347
- await connection.query("BEGIN");
10348
- const context = { connection };
10349
- const result = await txContext.run(context, func);
10350
- if (rollbackOnly) {
10351
- await connection.query("ROLLBACK");
10352
- } else {
10353
- await connection.query("COMMIT");
10354
- if (useWriterAfterTransaction) {
10355
- this._lastTransactionTime = Date.now();
10356
- }
10357
- }
10358
- return result;
10359
- } catch (error) {
10360
- destroyConnection = true;
10396
+ const stickyCell = this._writerSticky.begin();
10397
+ try {
10398
+ let attempt = 0;
10399
+ while (true) {
10400
+ attempt++;
10401
+ const connection = await handler.getConnection();
10402
+ let destroyConnection = false;
10361
10403
  try {
10362
- await connection.query("ROLLBACK");
10363
- } catch {
10364
- }
10365
- if (retryOnError && attempt < retryLimit && this.isRetryableError(error)) {
10366
- await this.sleep(retryDuration * Math.pow(2, attempt - 1));
10367
- continue;
10404
+ await connection.query("BEGIN");
10405
+ const context = { connection };
10406
+ const result = await txContext.run(context, func);
10407
+ if (rollbackOnly) {
10408
+ await connection.query("ROLLBACK");
10409
+ } else {
10410
+ await connection.query("COMMIT");
10411
+ if (useWriterAfterTransaction) this._writerSticky.mark();
10412
+ }
10413
+ return result;
10414
+ } catch (error) {
10415
+ destroyConnection = true;
10416
+ try {
10417
+ await connection.query("ROLLBACK");
10418
+ } catch {
10419
+ }
10420
+ if (retryOnError && attempt < retryLimit && this.isRetryableError(error)) {
10421
+ await this.sleep(retryDuration * Math.pow(2, attempt - 1));
10422
+ continue;
10423
+ }
10424
+ throw error;
10425
+ } finally {
10426
+ connection.release(destroyConnection || void 0);
10368
10427
  }
10369
- throw error;
10370
- } finally {
10371
- connection.release(destroyConnection || void 0);
10372
10428
  }
10429
+ } finally {
10430
+ this._writerSticky.end(stickyCell);
10373
10431
  }
10374
10432
  }
10375
10433
  /**
@@ -10510,7 +10568,10 @@ var DBModel = class _DBModel {
10510
10568
  writerStickyDuration: options?.writerStickyDuration ?? 5e3,
10511
10569
  ...options
10512
10570
  };
10513
- static _lastTransactionTime = 0;
10571
+ static _writerSticky = new WriterStickyClock({
10572
+ useWriterAfterTransaction: options?.useWriterAfterTransaction ?? true,
10573
+ writerStickyDuration: options?.writerStickyDuration ?? 5e3
10574
+ });
10514
10575
  // Phase F-2 (#105): this base class gets its OWN SCP runtime slot (built lazily from ITS config),
10515
10576
  // isolated from DBModel's default-config runtime and from other base classes — otherwise a
10516
10577
  // sqlite/mysql base would inherit DBModel's shared pg runtime and route its SQL to the wrong DB.