velocious 1.0.447 → 1.0.449

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 (69) hide show
  1. package/README.md +1 -1
  2. package/build/configuration-types.js +2 -0
  3. package/build/database/pool/async-tracked-multi-connection.js +123 -8
  4. package/build/database/pool/base.js +14 -1
  5. package/build/database/record/index.js +13 -8
  6. package/build/environment-handlers/node/cli/commands/generate/base-models.js +1 -16
  7. package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +125 -73
  8. package/build/frontend-model-controller.js +9 -0
  9. package/build/frontend-model-resource/base-resource.js +266 -53
  10. package/build/frontend-models/base.js +241 -97
  11. package/build/frontend-models/preloader.js +3 -2
  12. package/build/src/background-jobs/job-record.d.ts +2 -1
  13. package/build/src/background-jobs/job-record.d.ts.map +1 -1
  14. package/build/src/configuration-types.d.ts +10 -0
  15. package/build/src/configuration-types.d.ts.map +1 -1
  16. package/build/src/configuration-types.js +3 -1
  17. package/build/src/database/pool/async-tracked-multi-connection.d.ts +60 -4
  18. package/build/src/database/pool/async-tracked-multi-connection.d.ts.map +1 -1
  19. package/build/src/database/pool/async-tracked-multi-connection.js +113 -9
  20. package/build/src/database/pool/base.d.ts +38 -1
  21. package/build/src/database/pool/base.d.ts.map +1 -1
  22. package/build/src/database/pool/base.js +14 -2
  23. package/build/src/database/query/preloader/belongs-to.d.ts +2 -2
  24. package/build/src/database/query/preloader/belongs-to.d.ts.map +1 -1
  25. package/build/src/database/query/preloader/has-many.d.ts +1 -1
  26. package/build/src/database/query/preloader/has-many.d.ts.map +1 -1
  27. package/build/src/database/query/preloader/has-one.d.ts +2 -2
  28. package/build/src/database/query/preloader/has-one.d.ts.map +1 -1
  29. package/build/src/database/query/preloader.d.ts +1 -1
  30. package/build/src/database/query/preloader.d.ts.map +1 -1
  31. package/build/src/database/record/attachments/handle.d.ts +1 -1
  32. package/build/src/database/record/attachments/handle.d.ts.map +1 -1
  33. package/build/src/database/record/index.d.ts +23 -13
  34. package/build/src/database/record/index.d.ts.map +1 -1
  35. package/build/src/database/record/index.js +14 -9
  36. package/build/src/environment-handlers/node/cli/commands/generate/base-models.d.ts.map +1 -1
  37. package/build/src/environment-handlers/node/cli/commands/generate/base-models.js +2 -15
  38. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +89 -32
  39. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
  40. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +123 -72
  41. package/build/src/frontend-model-controller.d.ts.map +1 -1
  42. package/build/src/frontend-model-controller.js +8 -1
  43. package/build/src/frontend-model-resource/base-resource.d.ts +203 -64
  44. package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
  45. package/build/src/frontend-model-resource/base-resource.js +237 -54
  46. package/build/src/frontend-models/base.d.ts +173 -110
  47. package/build/src/frontend-models/base.d.ts.map +1 -1
  48. package/build/src/frontend-models/base.js +218 -102
  49. package/build/src/frontend-models/preloader.d.ts.map +1 -1
  50. package/build/src/frontend-models/preloader.js +4 -3
  51. package/build/src/testing/browser-frontend-model-event-hook-scenarios.js +2 -2
  52. package/build/src/testing/expect.d.ts +6 -0
  53. package/build/src/testing/expect.d.ts.map +1 -1
  54. package/build/src/testing/expect.js +9 -1
  55. package/build/testing/browser-frontend-model-event-hook-scenarios.js +1 -1
  56. package/build/testing/expect.js +9 -0
  57. package/package.json +1 -1
  58. package/src/configuration-types.js +2 -0
  59. package/src/database/pool/async-tracked-multi-connection.js +123 -8
  60. package/src/database/pool/base.js +14 -1
  61. package/src/database/record/index.js +13 -8
  62. package/src/environment-handlers/node/cli/commands/generate/base-models.js +1 -16
  63. package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +125 -73
  64. package/src/frontend-model-controller.js +9 -0
  65. package/src/frontend-model-resource/base-resource.js +266 -53
  66. package/src/frontend-models/base.js +241 -97
  67. package/src/frontend-models/preloader.js +3 -2
  68. package/src/testing/browser-frontend-model-event-hook-scenarios.js +1 -1
  69. package/src/testing/expect.js +9 -0
package/README.md CHANGED
@@ -22,7 +22,7 @@
22
22
  * EJS-backed mailers with delivery, queueing, and payload rendering support (see [docs/mailers.md](docs/mailers.md))
23
23
  * Trusted reverse proxy handling for `request.remoteAddress()` (see [docs/trusted-proxies.md](docs/trusted-proxies.md))
24
24
  * In-process driver schema metadata caching (see [docs/schema-metadata-cache.md](docs/schema-metadata-cache.md))
25
- * Named database connection checkouts for debugging held connections (see [docs/database-connections.md](docs/database-connections.md))
25
+ * Named database connection checkouts, bounded pool waits, and debugging held connections (see [docs/database-connections.md](docs/database-connections.md))
26
26
  * Optional built-in debug endpoint for inspecting server and database connection state (see [docs/debug-endpoint.md](docs/debug-endpoint.md))
27
27
 
28
28
  # Setup
@@ -47,12 +47,14 @@
47
47
  * @property {number | null} [pool.max] - Maximum number of connections. Set null to disable the cap.
48
48
  * @property {number} [pool.min] - Minimum number of connections.
49
49
  * @property {number} [pool.idleTimeoutMillis] - Idle timeout before releasing a connection.
50
+ * @property {number | null} [pool.checkoutTimeoutMillis] - Timeout while waiting for an available connection after the max connection cap is reached. Set null to wait indefinitely.
50
51
  * @property {string} [server] - SQL server hostname.
51
52
  * @property {string} [user] - SQL username.
52
53
  */
53
54
 
54
55
  /**
55
56
  * @typedef {object} DatabasePoolConfiguration
57
+ * @property {number | null} [checkoutTimeoutMillis] - Timeout while a checkout waits for an available async-tracked connection after the max live connection cap is reached. Set null to wait indefinitely. Default: 10000.
56
58
  * @property {number | null} [idleTimeoutMillis] - Idle timeout before closing a checked-in async-tracked connection. Set null to disable idle reaping. Default: 5000.
57
59
  * @property {number | null} [max] - Maximum live async-tracked connections for this pool. Defaults to 10. Extra checkouts wait until a matching connection is checked in or capacity is freed. Set null to disable the cap.
58
60
  */
@@ -8,6 +8,7 @@ const IDLE_CONNECTION_CHECKED_IN_AT = Symbol("velociousIdleConnectionCheckedInAt
8
8
  const CONNECTION_CHECKED_OUT_AT = Symbol("velociousConnectionCheckedOutAt")
9
9
  const DEFAULT_MAX_CONNECTIONS = 10
10
10
  const DEFAULT_IDLE_TIMEOUT_MILLIS = 5000
11
+ const DEFAULT_CHECKOUT_TIMEOUT_MILLIS = 10000
11
12
 
12
13
  /**
13
14
  * PendingCheckout type.
@@ -18,6 +19,9 @@ const DEFAULT_IDLE_TIMEOUT_MILLIS = 5000
18
19
  * @property {string} reuseKey - Database configuration reuse key needed by the checkout.
19
20
  * @property {(connection: import("../drivers/base.js").default) => void} resolve - Resolves with an activated connection.
20
21
  * @property {(error: Error) => void} reject - Rejects when checkout cannot complete.
22
+ * @property {number | null} timeoutAt - Timestamp when the checkout will time out, or null when disabled.
23
+ * @property {number | null} timeoutMillis - Milliseconds to wait before rejecting, or null when disabled.
24
+ * @property {ReturnType<typeof setTimeout> | undefined} timeoutTimer - Timer that rejects the pending checkout.
21
25
  */
22
26
 
23
27
  export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends BasePool {
@@ -281,17 +285,39 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
281
285
 
282
286
  /**
283
287
  * Runs max connections.
284
- * @returns {number | undefined} - Configured max live connections.
288
+ * @returns {number | null} - Configured max live connections.
285
289
  */
286
290
  maxConnections() {
287
291
  const value = this.getConfiguration().pool?.max
288
292
 
289
- if (value === null) return
293
+ if (value === null) return null
290
294
  if (this.validMaxConnections(value)) return value
291
295
 
292
296
  return DEFAULT_MAX_CONNECTIONS
293
297
  }
294
298
 
299
+ /**
300
+ * Runs checkout timeout millis.
301
+ * @returns {number | null} - Pending checkout timeout in milliseconds, or null when disabled.
302
+ */
303
+ checkoutTimeoutMillis() {
304
+ const value = this.getConfiguration().pool?.checkoutTimeoutMillis
305
+
306
+ if (value === null) return null
307
+ if (this.validCheckoutTimeoutMillis(value)) return value
308
+
309
+ return DEFAULT_CHECKOUT_TIMEOUT_MILLIS
310
+ }
311
+
312
+ /**
313
+ * Runs valid checkout timeout millis.
314
+ * @param {?} value - Candidate checkout timeout.
315
+ * @returns {value is number} - Whether the value is a valid timeout.
316
+ */
317
+ validCheckoutTimeoutMillis(value) {
318
+ return typeof value === "number" && Number.isFinite(value) && value >= 0
319
+ }
320
+
295
321
  /**
296
322
  * Runs valid max connections.
297
323
  * @param {?} value - Candidate max connection count.
@@ -322,7 +348,7 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
322
348
  canSpawnConnection() {
323
349
  const maxConnections = this.maxConnections()
324
350
 
325
- return maxConnections === undefined || this.liveConnectionCount() < maxConnections
351
+ return maxConnections === null || this.liveConnectionCount() < maxConnections
326
352
  }
327
353
 
328
354
  /**
@@ -361,7 +387,23 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
361
387
  */
362
388
  async waitForCheckout(databaseConfig, reuseKey, options = {}) {
363
389
  return await new Promise((resolve, reject) => {
364
- this.pendingCheckouts.push({databaseConfig, enqueuedAt: Date.now(), options, reject, resolve, reuseKey})
390
+ const enqueuedAt = Date.now()
391
+ const timeoutMillis = this.checkoutTimeoutMillis()
392
+ /** @type {PendingCheckout} */
393
+ const checkout = {
394
+ databaseConfig,
395
+ enqueuedAt,
396
+ options,
397
+ reject,
398
+ resolve,
399
+ reuseKey,
400
+ timeoutAt: timeoutMillis === null ? null : enqueuedAt + timeoutMillis,
401
+ timeoutMillis,
402
+ timeoutTimer: undefined
403
+ }
404
+
405
+ checkout.timeoutTimer = this.startPendingCheckoutTimeout(checkout)
406
+ this.pendingCheckouts.push(checkout)
365
407
  void this.drainPendingCheckouts().catch((error) => {
366
408
  const checkoutError = error instanceof Error ? error : new Error("Failed to drain pending database connection checkouts.", {cause: error})
367
409
 
@@ -400,17 +442,19 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
400
442
  const checkout = this.pendingCheckouts[0]
401
443
 
402
444
  if (await this.closeIdleConnectionForPendingCheckoutCapacity(checkout)) continue
445
+ if (!this.pendingCheckouts.includes(checkout)) continue
403
446
  if (this.canSpawnConnection()) {
404
- this.pendingCheckouts.shift()
447
+ this.removePendingCheckoutAt(0)
405
448
  await this.spawnAndResolvePendingCheckout(checkout)
406
449
  continue
407
450
  }
408
451
 
409
452
  const reapedConnection = await this.idleConnectionForPendingCheckout(checkout)
410
453
 
454
+ if (!this.pendingCheckouts.includes(checkout)) continue
411
455
  if (!reapedConnection) return
412
456
 
413
- this.pendingCheckouts.shift()
457
+ this.removePendingCheckoutAt(0)
414
458
  await this.resolvePendingCheckout(checkout, reapedConnection)
415
459
  }
416
460
  }
@@ -426,7 +470,7 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
426
470
 
427
471
  if (!connection) continue
428
472
 
429
- this.pendingCheckouts.splice(index, 1)
473
+ this.removePendingCheckoutAt(index)
430
474
  await this.resolvePendingCheckout(checkout, connection)
431
475
 
432
476
  return true
@@ -435,6 +479,71 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
435
479
  return false
436
480
  }
437
481
 
482
+ /**
483
+ * Runs remove pending checkout at.
484
+ * @param {number} index - Pending checkout index.
485
+ * @returns {PendingCheckout} - Removed checkout.
486
+ */
487
+ removePendingCheckoutAt(index) {
488
+ const checkout = this.pendingCheckouts.splice(index, 1)[0]
489
+
490
+ this.clearPendingCheckoutTimeout(checkout)
491
+
492
+ return checkout
493
+ }
494
+
495
+ /**
496
+ * Runs start pending checkout timeout.
497
+ * @param {PendingCheckout} checkout - Pending checkout to time out.
498
+ * @returns {ReturnType<typeof setTimeout> | undefined} - Timer, if timeout is enabled.
499
+ */
500
+ startPendingCheckoutTimeout(checkout) {
501
+ if (checkout.timeoutMillis === null) return undefined
502
+
503
+ const timer = setTimeout(() => {
504
+ this.timeoutPendingCheckout(checkout)
505
+ }, checkout.timeoutMillis)
506
+
507
+ return timer
508
+ }
509
+
510
+ /**
511
+ * Runs timeout pending checkout.
512
+ * @param {PendingCheckout} checkout - Pending checkout to reject.
513
+ * @returns {void}
514
+ */
515
+ timeoutPendingCheckout(checkout) {
516
+ const index = this.pendingCheckouts.indexOf(checkout)
517
+
518
+ if (index === -1) return
519
+
520
+ this.removePendingCheckoutAt(index)
521
+ checkout.reject(this.pendingCheckoutTimeoutError(checkout))
522
+ }
523
+
524
+ /**
525
+ * Runs pending checkout timeout error.
526
+ * @param {PendingCheckout} checkout - Timed-out checkout.
527
+ * @returns {Error} - Timeout error.
528
+ */
529
+ pendingCheckoutTimeoutError(checkout) {
530
+ const checkoutName = checkout.options.name ? ` Checkout name: ${JSON.stringify(checkout.options.name)}.` : ""
531
+
532
+ return new Error(`Timed out after ${checkout.timeoutMillis}ms waiting for database connection checkout from pool "${this.identifier}".${checkoutName}`)
533
+ }
534
+
535
+ /**
536
+ * Runs clear pending checkout timeout.
537
+ * @param {PendingCheckout} checkout - Pending checkout.
538
+ * @returns {void}
539
+ */
540
+ clearPendingCheckoutTimeout(checkout) {
541
+ if (!checkout.timeoutTimer) return
542
+
543
+ clearTimeout(checkout.timeoutTimer)
544
+ checkout.timeoutTimer = undefined
545
+ }
546
+
438
547
  /**
439
548
  * Runs close idle connection for pending checkout capacity.
440
549
  * @param {PendingCheckout} checkout - Checkout waiting for a connection.
@@ -472,6 +581,8 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
472
581
  if (connection) return connection
473
582
 
474
583
  await this.reapIdleConnections()
584
+ if (!this.pendingCheckouts.includes(checkout)) return
585
+
475
586
  connection = this.takeIdleConnectionForReuseKey(checkout.reuseKey, {includeOpenTransactions: false})
476
587
 
477
588
  return connection
@@ -762,14 +873,17 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
762
873
  /**
763
874
  * Runs pending checkout debug snapshots.
764
875
  * @param {number} now - Current timestamp.
765
- * @returns {Array<Record<string, ?>>} - Pending checkout snapshots.
876
+ * @returns {import("./base.js").DatabasePoolPendingCheckoutDebugSnapshot[]} - Pending checkout snapshots.
766
877
  */
767
878
  pendingCheckoutDebugSnapshots(now) {
768
879
  return this.pendingCheckouts.map((checkout, index) => ({
769
880
  checkoutName: checkout.options.name,
770
881
  enqueuedAt: checkout.enqueuedAt,
771
882
  index,
883
+ remainingTimeoutMs: checkout.timeoutAt === null ? null : Math.max(0, checkout.timeoutAt - now),
772
884
  reuseKey: checkout.reuseKey,
885
+ timeoutAt: checkout.timeoutAt,
886
+ timeoutMillis: checkout.timeoutMillis,
773
887
  waitingForMs: Math.max(0, now - checkout.enqueuedAt)
774
888
  }))
775
889
  }
@@ -1131,6 +1245,7 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
1131
1245
  this.pendingCheckouts = []
1132
1246
 
1133
1247
  for (const checkout of pendingCheckouts) {
1248
+ this.clearPendingCheckoutTimeout(checkout)
1134
1249
  checkout.reject(error)
1135
1250
  }
1136
1251
  }
@@ -12,6 +12,19 @@ export const POOL_CONFIGURATION_KEY = Symbol("velociousPoolConfigurationKey")
12
12
  * @property {string} [name] - Human-readable name for the checked-out connection.
13
13
  */
14
14
 
15
+ /**
16
+ * DatabasePoolPendingCheckoutDebugSnapshot type.
17
+ * @typedef {object} DatabasePoolPendingCheckoutDebugSnapshot
18
+ * @property {string | undefined} checkoutName - Human-readable checkout name.
19
+ * @property {number} enqueuedAt - Timestamp when the checkout started waiting.
20
+ * @property {number} index - Pending checkout queue index.
21
+ * @property {number | null} remainingTimeoutMs - Milliseconds before the checkout times out, or null when disabled.
22
+ * @property {string} reuseKey - Database configuration reuse key needed by the checkout.
23
+ * @property {number | null} timeoutAt - Timestamp when the checkout will time out, or null when disabled.
24
+ * @property {number | null} timeoutMillis - Timeout configured for the checkout, or null when disabled.
25
+ * @property {number} waitingForMs - Milliseconds already spent waiting.
26
+ */
27
+
15
28
  /**
16
29
  * DatabasePoolDebugSnapshot type.
17
30
  * @typedef {object} DatabasePoolDebugSnapshot
@@ -21,7 +34,7 @@ export const POOL_CONFIGURATION_KEY = Symbol("velociousPoolConfigurationKey")
21
34
  * @property {number} idleCount - Number of idle connections.
22
35
  * @property {string} identifier - Database identifier.
23
36
  * @property {number} inUseCount - Number of checked-out connections.
24
- * @property {Array<Record<string, ?>>} [pendingCheckouts] - Waiting checkout snapshots.
37
+ * @property {Array<DatabasePoolPendingCheckoutDebugSnapshot>} [pendingCheckouts] - Waiting checkout snapshots.
25
38
  * @property {number} pendingCheckoutCount - Number of queued checkout requests.
26
39
  * @property {string} poolClass - Pool class name.
27
40
  */
@@ -222,6 +222,10 @@ class TenantDatabaseScopeError extends Error {
222
222
  }
223
223
  }
224
224
 
225
+ /**
226
+ * Base database record.
227
+ * @template {Record<string, ?>} [WriteAttributes=Record<string, ?>]
228
+ */
225
229
  class VelociousDatabaseRecord {
226
230
  /**
227
231
  * Narrows the runtime value to the documented type.
@@ -1150,17 +1154,18 @@ class VelociousDatabaseRecord {
1150
1154
 
1151
1155
  /**
1152
1156
  * Runs create.
1153
- * @template {typeof VelociousDatabaseRecord} MC
1154
- * @this {MC}
1155
- * @param {Record<string, ?>} [attributes] - Attributes.
1156
- * @returns {Promise<InstanceType<MC>>} - Resolves with the create.
1157
+ * @template {Record<string, ?>} CreateAttributes
1158
+ * @template {VelociousDatabaseRecord<CreateAttributes>} Model
1159
+ * @this {{new (changes?: CreateAttributes): Model} & typeof VelociousDatabaseRecord}
1160
+ * @param {CreateAttributes} [attributes] - Attributes.
1161
+ * @returns {Promise<Model>} - Resolves with the create.
1157
1162
  */
1158
1163
  static async create(attributes) {
1159
1164
  await this.ensureInitialized()
1160
1165
 
1161
1166
  const record = /**
1162
1167
  * Narrows the runtime value to the documented type.
1163
- @type {InstanceType<MC>} */ (new this(attributes))
1168
+ @type {Model} */ (new this(attributes))
1164
1169
 
1165
1170
  await record.save()
1166
1171
 
@@ -3353,9 +3358,9 @@ class VelociousDatabaseRecord {
3353
3358
 
3354
3359
  /**
3355
3360
  * Runs constructor.
3356
- * @param {Record<string, ?>} changes - Changes.
3361
+ * @param {WriteAttributes} changes - Changes.
3357
3362
  */
3358
- constructor(changes = {}) {
3363
+ constructor(changes = /** @type {WriteAttributes} */ ({})) {
3359
3364
  this.getModelClass()._assertHasBeenInitialized()
3360
3365
  this._attributes = {}
3361
3366
  this._changes = {}
@@ -4256,7 +4261,7 @@ class VelociousDatabaseRecord {
4256
4261
 
4257
4262
  /**
4258
4263
  * Assigns the attributes to the record and saves it.
4259
- * @param {object} attributesToAssign - The attributes to assign to the record.
4264
+ * @param {WriteAttributes} attributesToAssign - The attributes to assign to the record.
4260
4265
  */
4261
4266
  async update(attributesToAssign) {
4262
4267
  if (attributesToAssign) this.assign(attributesToAssign)
@@ -139,24 +139,9 @@ export default class DbGenerateModel extends BaseCommand {
139
139
 
140
140
  const hasManyRelationFilePath = `${velociousPath}/database/record/instance-relationships/has-many.js`
141
141
 
142
+ fileContent += `/** @augments {DatabaseRecord<${writeAttributeTypeName}>} */\n`
142
143
  fileContent += `export default class ${modelNameCamelized}Base extends DatabaseRecord {\n`
143
144
 
144
- fileContent += " /**\n"
145
- fileContent += ` * Creates a ${modelNameCamelized} record.\n`
146
- fileContent += ` * @template {typeof ${modelNameCamelized}Base} T\n`
147
- fileContent += " * @this {T}\n"
148
- fileContent += ` * @param {${writeAttributeTypeName}} [attributes] - Attributes for the new record.\n`
149
- fileContent += " * @returns {Promise<InstanceType<T>>} - Persisted record.\n"
150
- fileContent += " */\n"
151
- fileContent += " static async create(attributes) { return /** @type {Promise<InstanceType<T>>} */ (super.create(attributes)) }\n\n"
152
-
153
- fileContent += " /**\n"
154
- fileContent += ` * Updates this ${modelNameCamelized} record.\n`
155
- fileContent += ` * @param {${writeAttributeTypeName}} attributes - Attributes to assign before saving.\n`
156
- fileContent += " * @returns {Promise<void>} - Resolves when the record is saved.\n"
157
- fileContent += " */\n"
158
- fileContent += " async update(attributes) { return await super.update(attributes) }\n\n"
159
-
160
145
  // --- getModelClass() override (fixes polymorphic typing in JS/JSDoc) ---
161
146
  if (await fileExists(sourceModelFullFilePath)) {
162
147
  // Model file exists (e.g. src/models/ticket.js) → return typeof Ticket