velocious 1.0.599 → 1.0.601

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 (95) hide show
  1. package/README.md +18 -9
  2. package/build/background-jobs/adapter-client.js +45 -0
  3. package/build/background-jobs/adapter.js +139 -0
  4. package/build/background-jobs/job-registry.js +1 -1
  5. package/build/background-jobs/job.js +20 -129
  6. package/build/background-jobs/main.js +71 -28
  7. package/build/background-jobs/perform-job.js +23 -0
  8. package/build/background-jobs/platform-job.js +156 -0
  9. package/build/background-jobs/runtime.js +156 -0
  10. package/build/background-jobs/sql-adapter.js +20 -0
  11. package/build/background-jobs/store.js +3 -1
  12. package/build/background-jobs/types.js +10 -0
  13. package/build/background-jobs/web/controller.js +5 -2
  14. package/build/background-jobs/worker.js +6 -8
  15. package/build/configuration-types.js +7 -2
  16. package/build/configuration.js +224 -45
  17. package/build/environment-handlers/base.js +20 -0
  18. package/build/environment-handlers/node.js +27 -20
  19. package/build/frontend-model-controller.js +16 -20
  20. package/build/jobs/prune-terminal-background-jobs.js +2 -3
  21. package/build/src/background-jobs/adapter-client.d.ts +41 -0
  22. package/build/src/background-jobs/adapter-client.d.ts.map +1 -0
  23. package/build/src/background-jobs/adapter-client.js +39 -0
  24. package/build/src/background-jobs/adapter.d.ts +165 -0
  25. package/build/src/background-jobs/adapter.d.ts.map +1 -0
  26. package/build/src/background-jobs/adapter.js +121 -0
  27. package/build/src/background-jobs/job-registry.d.ts +1 -1
  28. package/build/src/background-jobs/job-registry.d.ts.map +1 -1
  29. package/build/src/background-jobs/job-registry.js +2 -2
  30. package/build/src/background-jobs/job.d.ts +6 -69
  31. package/build/src/background-jobs/job.d.ts.map +1 -1
  32. package/build/src/background-jobs/job.js +17 -116
  33. package/build/src/background-jobs/main.d.ts +13 -3
  34. package/build/src/background-jobs/main.d.ts.map +1 -1
  35. package/build/src/background-jobs/main.js +66 -30
  36. package/build/src/background-jobs/perform-job.d.ts +16 -0
  37. package/build/src/background-jobs/perform-job.d.ts.map +1 -0
  38. package/build/src/background-jobs/perform-job.js +22 -0
  39. package/build/src/background-jobs/platform-job.d.ts +110 -0
  40. package/build/src/background-jobs/platform-job.d.ts.map +1 -0
  41. package/build/src/background-jobs/platform-job.js +138 -0
  42. package/build/src/background-jobs/runtime.d.ts +78 -0
  43. package/build/src/background-jobs/runtime.d.ts.map +1 -0
  44. package/build/src/background-jobs/runtime.js +132 -0
  45. package/build/src/background-jobs/sql-adapter.d.ts +13 -0
  46. package/build/src/background-jobs/sql-adapter.d.ts.map +1 -0
  47. package/build/src/background-jobs/sql-adapter.js +18 -0
  48. package/build/src/background-jobs/store.d.ts +2 -1
  49. package/build/src/background-jobs/store.d.ts.map +1 -1
  50. package/build/src/background-jobs/store.js +4 -2
  51. package/build/src/background-jobs/types.d.ts +41 -0
  52. package/build/src/background-jobs/types.d.ts.map +1 -1
  53. package/build/src/background-jobs/types.js +11 -1
  54. package/build/src/background-jobs/web/controller.d.ts.map +1 -1
  55. package/build/src/background-jobs/web/controller.js +5 -3
  56. package/build/src/background-jobs/worker.d.ts.map +1 -1
  57. package/build/src/background-jobs/worker.js +7 -8
  58. package/build/src/configuration-types.d.ts +20 -4
  59. package/build/src/configuration-types.d.ts.map +1 -1
  60. package/build/src/configuration-types.js +7 -3
  61. package/build/src/configuration.d.ts +53 -4
  62. package/build/src/configuration.d.ts.map +1 -1
  63. package/build/src/configuration.js +209 -44
  64. package/build/src/environment-handlers/base.d.ts +17 -0
  65. package/build/src/environment-handlers/base.d.ts.map +1 -1
  66. package/build/src/environment-handlers/base.js +19 -1
  67. package/build/src/environment-handlers/node.d.ts +18 -0
  68. package/build/src/environment-handlers/node.d.ts.map +1 -1
  69. package/build/src/environment-handlers/node.js +25 -20
  70. package/build/src/frontend-model-controller.d.ts +1 -4
  71. package/build/src/frontend-model-controller.d.ts.map +1 -1
  72. package/build/src/frontend-model-controller.js +16 -20
  73. package/build/src/jobs/prune-terminal-background-jobs.d.ts.map +1 -1
  74. package/build/src/jobs/prune-terminal-background-jobs.js +3 -4
  75. package/build/tsconfig.tsbuildinfo +1 -1
  76. package/package.json +4 -4
  77. package/src/background-jobs/adapter-client.js +45 -0
  78. package/src/background-jobs/adapter.js +139 -0
  79. package/src/background-jobs/job-registry.js +1 -1
  80. package/src/background-jobs/job.js +20 -129
  81. package/src/background-jobs/main.js +71 -28
  82. package/src/background-jobs/perform-job.js +23 -0
  83. package/src/background-jobs/platform-job.js +156 -0
  84. package/src/background-jobs/runtime.js +156 -0
  85. package/src/background-jobs/sql-adapter.js +20 -0
  86. package/src/background-jobs/store.js +3 -1
  87. package/src/background-jobs/types.js +10 -0
  88. package/src/background-jobs/web/controller.js +5 -2
  89. package/src/background-jobs/worker.js +6 -8
  90. package/src/configuration-types.js +7 -2
  91. package/src/configuration.js +224 -45
  92. package/src/environment-handlers/base.js +20 -0
  93. package/src/environment-handlers/node.js +27 -20
  94. package/src/frontend-model-controller.js +16 -20
  95. package/src/jobs/prune-terminal-background-jobs.js +2 -3
@@ -11,11 +11,20 @@
11
11
  * @property {string[]} [databaseIdentifiers] - Database identifiers to include in the connection scope.
12
12
  * @property {string} [name] - Human-readable name for the checked-out database connections.
13
13
  */
14
+ /**
15
+ * One adapter instance and its serialized ready/close lifecycle.
16
+ * @typedef {object} BackgroundJobsAdapterGeneration
17
+ * @property {import("./background-jobs/adapter.js").default} adapter - Adapter owned by this generation.
18
+ * @property {boolean} closing - Whether close has claimed this generation.
19
+ * @property {Promise<void> | undefined} readyPromise - Shared readiness attempt.
20
+ * @property {Promise<void> | undefined} closePromise - Shared close operation.
21
+ */
14
22
 
15
23
  import {digg} from "diggerize"
16
24
  import gettextConfig from "gettext-universal/build/src/config.js"
17
25
  import translate from "gettext-universal/build/src/translate.js"
18
26
  import Ability from "./authorization/ability.js"
27
+ import BackgroundJobsAdapter from "./background-jobs/adapter.js"
19
28
  import DatabaseOperation from "./database/operation.js"
20
29
  import {initializeAuditedModelRelationships} from "./database/record/auditing.js"
21
30
  import EventEmitter from "./utils/event-emitter.js"
@@ -204,6 +213,9 @@ export default class VelociousConfiguration {
204
213
  * @type {Promise<void> | null} */
205
214
  _closeDatabaseConnectionsPromise = null
206
215
 
216
+ /** @type {BackgroundJobsAdapterGeneration | undefined} */
217
+ _backgroundJobsAdapterGeneration = undefined
218
+
207
219
  /**
208
220
  * Dedicated advisory-lock connections currently holding a lock. These are spawned
209
221
  * outside the pools' tracked sets (so a hold-timeout lock survives pool checkouts),
@@ -224,7 +236,7 @@ export default class VelociousConfiguration {
224
236
  * Runs constructor.
225
237
  * @param {import("./configuration-types.js").ConfigurationArgsType} args - Configuration arguments.
226
238
  */
227
- constructor({abilityResolver, abilityResources, attachments, autoload = true, backgroundJobs, backendProjects, beacon, cookieSecret, cors, database, debug = false, debugEndpoint = false, apiManifest = false, directory, enforceTenantDatabaseScopes = true, environment, environmentHandler, exposeInternalErrorsToClients = false, frontendTenantSqlite, httpServer, initializeModels, initializers, locale, localeFallbacks, locales, logging, mailerBackend, packages, requestTimeoutMs, routeResolverHooks, scheduledBackgroundJobs, secureFrontendModelErrors = false, structureSql, sync, tenantDatabaseProviders, tenantDatabaseResolver, tenantResolver, testing, timeZone, timezoneOffsetMinutes, trustedProxies, websocketChannelResolver, websocketMessageHandlerResolver, ...restArgs}) {
239
+ constructor({abilityResolver, abilityResources, attachments, autoload = true, backgroundJobs, backendProjects, beacon, cookieSecret, cors, database, debug = false, debugEndpoint = false, apiManifest = false, directory, enforceTenantDatabaseScopes = true, environment, environmentHandler, exposeInternalErrorsToClients, frontendTenantSqlite, httpServer, initializeModels, initializers, locale, localeFallbacks, locales, logging, mailerBackend, packages, requestTimeoutMs, routeResolverHooks, scheduledBackgroundJobs, secureFrontendModelErrors, structureSql, sync, tenantDatabaseProviders, tenantDatabaseResolver, tenantResolver, testing, timeZone, timezoneOffsetMinutes, trustedProxies, websocketChannelResolver, websocketMessageHandlerResolver, ...restArgs}) {
228
240
  restArgsError(restArgs)
229
241
 
230
242
  this._abilityResolver = abilityResolver
@@ -268,11 +280,12 @@ export default class VelociousConfiguration {
268
280
  this.debug = debug
269
281
  this._debugEndpoint = this._normalizeDebugEndpoint(debugEndpoint)
270
282
  this._apiManifest = this._normalizeApiManifest(apiManifest)
271
- this._environment = environment || process.env.VELOCIOUS_ENV || process.env.NODE_ENV || "development"
283
+ this._environment = environment || globalThis.process?.env.VELOCIOUS_ENV || globalThis.process?.env.NODE_ENV || "development"
272
284
  this._environmentHandler = environmentHandler
273
285
  this._enforceTenantDatabaseScopes = enforceTenantDatabaseScopes
274
- this._exposeInternalErrorsToClients = exposeInternalErrorsToClients
275
- this._secureFrontendModelErrors = secureFrontendModelErrors
286
+ this._exposeInternalErrorsToClients = exposeInternalErrorsToClients === undefined
287
+ ? secureFrontendModelErrors !== true
288
+ : exposeInternalErrorsToClients
276
289
  this._directory = directory
277
290
  this._initializeModels = initializeModels
278
291
  /** @type {VelociousPackage[]} */
@@ -449,9 +462,10 @@ export default class VelociousConfiguration {
449
462
 
450
463
  /**
451
464
  * Returns whether frontend-model errors expose only explicitly safe messages.
452
- * @returns {boolean} Whether frontend-model errors expose only explicitly safe messages.
465
+ * @deprecated Use `getExposeInternalErrorsToClients()`.
466
+ * @returns {boolean} Whether frontend-model internal error exposure is disabled.
453
467
  */
454
- getSecureFrontendModelErrors() { return this._secureFrontendModelErrors === true }
468
+ getSecureFrontendModelErrors() { return !this.getExposeInternalErrorsToClients() }
455
469
 
456
470
  /**
457
471
  * Runs get debug endpoint.
@@ -1441,22 +1455,23 @@ export default class VelociousConfiguration {
1441
1455
 
1442
1456
  /**
1443
1457
  * Runs get background jobs config.
1444
- * @returns {Required<import("./configuration-types.js").BackgroundJobsConfiguration> & {retention: import("./configuration-types.js").ResolvedBackgroundJobsRetentionConfiguration}} - Background jobs configuration.
1458
+ * @returns {Omit<Required<import("./configuration-types.js").BackgroundJobsConfiguration>, "adapter" | "retention"> & {retention: import("./configuration-types.js").ResolvedBackgroundJobsRetentionConfiguration}} - Background jobs configuration.
1445
1459
  */
1446
1460
  getBackgroundJobsConfig() {
1447
- const envHost = process.env.VELOCIOUS_BACKGROUND_JOBS_HOST
1448
- const envPortRaw = process.env.VELOCIOUS_BACKGROUND_JOBS_PORT
1449
- const envDatabaseIdentifier = process.env.VELOCIOUS_BACKGROUND_JOBS_DATABASE_IDENTIFIER
1450
- const envMaxConcurrentForkedRaw = process.env.VELOCIOUS_BACKGROUND_JOBS_MAX_CONCURRENT_FORKED_JOBS
1451
- const envMaxConcurrentRaw = process.env.VELOCIOUS_BACKGROUND_JOBS_MAX_CONCURRENT_INLINE_JOBS
1452
- const envPooledRunnerCountRaw = process.env.VELOCIOUS_BACKGROUND_JOBS_POOLED_RUNNER_COUNT
1453
- const envPooledRunnerConcurrencyRaw = process.env.VELOCIOUS_BACKGROUND_JOBS_POOLED_RUNNER_CONCURRENCY
1454
- const envPooledRunnerMaxJobsRaw = process.env.VELOCIOUS_BACKGROUND_JOBS_POOLED_RUNNER_MAX_JOBS
1455
- const envPooledRunnerMaxRssBytesRaw = process.env.VELOCIOUS_BACKGROUND_JOBS_POOLED_RUNNER_MAX_RSS_BYTES
1456
- const envPooledRunnerMaxLifetimeMsRaw = process.env.VELOCIOUS_BACKGROUND_JOBS_POOLED_RUNNER_MAX_LIFETIME_MS
1457
- const envDispatchStrategy = process.env.VELOCIOUS_BACKGROUND_JOBS_DISPATCH_STRATEGY
1458
- const envPollIntervalRaw = process.env.VELOCIOUS_BACKGROUND_JOBS_POLL_INTERVAL_MS
1459
- const envJobTimeoutRaw = process.env.VELOCIOUS_BACKGROUND_JOBS_JOB_TIMEOUT_MS
1461
+ const processEnvironment = globalThis.process?.env
1462
+ const envHost = processEnvironment?.VELOCIOUS_BACKGROUND_JOBS_HOST
1463
+ const envPortRaw = processEnvironment?.VELOCIOUS_BACKGROUND_JOBS_PORT
1464
+ const envDatabaseIdentifier = processEnvironment?.VELOCIOUS_BACKGROUND_JOBS_DATABASE_IDENTIFIER
1465
+ const envMaxConcurrentForkedRaw = processEnvironment?.VELOCIOUS_BACKGROUND_JOBS_MAX_CONCURRENT_FORKED_JOBS
1466
+ const envMaxConcurrentRaw = processEnvironment?.VELOCIOUS_BACKGROUND_JOBS_MAX_CONCURRENT_INLINE_JOBS
1467
+ const envPooledRunnerCountRaw = processEnvironment?.VELOCIOUS_BACKGROUND_JOBS_POOLED_RUNNER_COUNT
1468
+ const envPooledRunnerConcurrencyRaw = processEnvironment?.VELOCIOUS_BACKGROUND_JOBS_POOLED_RUNNER_CONCURRENCY
1469
+ const envPooledRunnerMaxJobsRaw = processEnvironment?.VELOCIOUS_BACKGROUND_JOBS_POOLED_RUNNER_MAX_JOBS
1470
+ const envPooledRunnerMaxRssBytesRaw = processEnvironment?.VELOCIOUS_BACKGROUND_JOBS_POOLED_RUNNER_MAX_RSS_BYTES
1471
+ const envPooledRunnerMaxLifetimeMsRaw = processEnvironment?.VELOCIOUS_BACKGROUND_JOBS_POOLED_RUNNER_MAX_LIFETIME_MS
1472
+ const envDispatchStrategy = processEnvironment?.VELOCIOUS_BACKGROUND_JOBS_DISPATCH_STRATEGY
1473
+ const envPollIntervalRaw = processEnvironment?.VELOCIOUS_BACKGROUND_JOBS_POLL_INTERVAL_MS
1474
+ const envJobTimeoutRaw = processEnvironment?.VELOCIOUS_BACKGROUND_JOBS_JOB_TIMEOUT_MS
1460
1475
  const envPort = envPortRaw ? Number(envPortRaw) : undefined
1461
1476
  const envMaxConcurrentForked = envMaxConcurrentForkedRaw ? Number(envMaxConcurrentForkedRaw) : undefined
1462
1477
  const envMaxConcurrent = envMaxConcurrentRaw ? Number(envMaxConcurrentRaw) : undefined
@@ -1468,6 +1483,11 @@ export default class VelociousConfiguration {
1468
1483
  const envPollInterval = envPollIntervalRaw ? Number(envPollIntervalRaw) : undefined
1469
1484
  const envJobTimeout = envJobTimeoutRaw ? Number(envJobTimeoutRaw) : undefined
1470
1485
  const configured = this._backgroundJobs || {}
1486
+ const mode = configured.mode === undefined ? "background" : configured.mode
1487
+
1488
+ if (mode !== "background" && mode !== "inline") {
1489
+ throw new TypeError(`backgroundJobs.mode must be "background" or "inline", got: ${String(mode)}`)
1490
+ }
1471
1491
  const host = configured.host || envHost || "127.0.0.1"
1472
1492
  const port = typeof configured.port === "number"
1473
1493
  ? configured.port
@@ -1522,7 +1542,146 @@ export default class VelociousConfiguration {
1522
1542
  : 60 * 60 * 1000
1523
1543
  }
1524
1544
 
1525
- return {host, port, databaseIdentifier, maxConcurrentForkedJobs, maxConcurrentInlineJobs, pooledRunnerCount, pooledRunnerConcurrency, pooledRunnerMaxJobs, pooledRunnerMaxRssBytes, pooledRunnerMaxLifetimeMs, dispatchStrategy, pollIntervalMs, queues, jobTimeoutMs, retention}
1545
+ return {host, port, databaseIdentifier, maxConcurrentForkedJobs, maxConcurrentInlineJobs, mode, pooledRunnerCount, pooledRunnerConcurrency, pooledRunnerMaxJobs, pooledRunnerMaxRssBytes, pooledRunnerMaxLifetimeMs, dispatchStrategy, pollIntervalMs, queues, jobTimeoutMs, retention}
1546
+ }
1547
+
1548
+ /**
1549
+ * Resolves and memoizes one background-jobs adapter for this configuration lifecycle.
1550
+ * @returns {BackgroundJobsAdapter} - Active adapter.
1551
+ */
1552
+ getBackgroundJobsAdapter() {
1553
+ if (this._backgroundJobsAdapterGeneration) return this._backgroundJobsAdapterGeneration.adapter
1554
+
1555
+ const configuredAdapter = this._backgroundJobs?.adapter
1556
+ const adapter = typeof configuredAdapter === "function"
1557
+ ? configuredAdapter({configuration: this})
1558
+ : (configuredAdapter || this.getEnvironmentHandler().createBackgroundJobsAdapter({configuration: this}))
1559
+
1560
+ if (!(adapter instanceof BackgroundJobsAdapter)) {
1561
+ throw new TypeError("backgroundJobs.adapter must be a BackgroundJobsAdapter instance or a synchronous factory returning one")
1562
+ }
1563
+
1564
+ this._backgroundJobsAdapterGeneration = {
1565
+ adapter,
1566
+ closing: false,
1567
+ closePromise: undefined,
1568
+ readyPromise: undefined
1569
+ }
1570
+ return adapter
1571
+ }
1572
+
1573
+ /**
1574
+ * Atomically acquires the exact ready adapter for the active lifecycle.
1575
+ * A close that claims the generation while readiness is pending wins: this
1576
+ * operation waits for that close, creates the next generation, readies it,
1577
+ * and returns only that live instance.
1578
+ * @returns {Promise<BackgroundJobsAdapter>} - Exact ready adapter generation.
1579
+ */
1580
+ async acquireReadyBackgroundJobsAdapter() {
1581
+ while (true) {
1582
+ const databaseClosePromise = this._closeDatabaseConnectionsPromise
1583
+
1584
+ if (databaseClosePromise) {
1585
+ await databaseClosePromise
1586
+ continue
1587
+ }
1588
+
1589
+ this.getBackgroundJobsAdapter()
1590
+ const generation = this._backgroundJobsAdapterGeneration
1591
+
1592
+ if (!generation) throw new Error("Background jobs adapter generation was not created")
1593
+
1594
+ if (generation.closing) {
1595
+ if (generation.closePromise) await generation.closePromise
1596
+ continue
1597
+ }
1598
+
1599
+ const readyPromise = generation.readyPromise || Promise.resolve().then(async () => {
1600
+ await generation.adapter.ensureReady()
1601
+ })
1602
+
1603
+ generation.readyPromise = readyPromise
1604
+
1605
+ try {
1606
+ await readyPromise
1607
+ } catch (error) {
1608
+ if (generation.readyPromise === readyPromise) generation.readyPromise = undefined
1609
+ throw error
1610
+ }
1611
+
1612
+ if (generation.closing) {
1613
+ if (generation.closePromise) await generation.closePromise
1614
+ continue
1615
+ }
1616
+
1617
+ if (this._backgroundJobsAdapterGeneration !== generation) continue
1618
+
1619
+ return generation.adapter
1620
+ }
1621
+ }
1622
+
1623
+ /**
1624
+ * Readies the active adapter once per lifecycle. A failed attempt remains retryable.
1625
+ * @returns {Promise<void>} - Resolves when ready.
1626
+ */
1627
+ async ensureBackgroundJobsAdapterReady() {
1628
+ await this.acquireReadyBackgroundJobsAdapter()
1629
+ }
1630
+
1631
+ /**
1632
+ * Returns health without resolving persistence in non-durable inline mode.
1633
+ * @returns {Promise<import("./background-jobs/types.js").BackgroundJobsHealth>} - Current health.
1634
+ */
1635
+ async backgroundJobsHealth() {
1636
+ if (this.getBackgroundJobsConfig().mode === "inline") return {ready: true}
1637
+
1638
+ const adapter = await this.acquireReadyBackgroundJobsAdapter()
1639
+
1640
+ return await adapter.health()
1641
+ }
1642
+
1643
+ /**
1644
+ * Closes the resolved adapter once and clears lifecycle caches.
1645
+ * @returns {Promise<void>} - Resolves after close.
1646
+ */
1647
+ async closeBackgroundJobsAdapter() {
1648
+ const generation = this._backgroundJobsAdapterGeneration
1649
+
1650
+ if (!generation) return
1651
+ if (generation.closePromise) return await generation.closePromise
1652
+
1653
+ generation.closing = true
1654
+ const closePromise = (async () => {
1655
+ /** @type {Error[]} */
1656
+ const closeErrors = []
1657
+
1658
+ if (generation.readyPromise) {
1659
+ try {
1660
+ await generation.readyPromise
1661
+ } catch (error) {
1662
+ closeErrors.push(error instanceof Error ? error : new Error(String(error)))
1663
+ }
1664
+ }
1665
+
1666
+ try {
1667
+ await generation.adapter.close()
1668
+ } catch (error) {
1669
+ closeErrors.push(error instanceof Error ? error : new Error(String(error)))
1670
+ }
1671
+
1672
+ if (closeErrors.length === 1) throw closeErrors[0]
1673
+ if (closeErrors.length > 1) throw new AggregateError(closeErrors, "Failed to ready and close the background-jobs adapter")
1674
+ })()
1675
+
1676
+ generation.closePromise = closePromise
1677
+
1678
+ try {
1679
+ await closePromise
1680
+ } finally {
1681
+ if (this._backgroundJobsAdapterGeneration === generation) {
1682
+ this._backgroundJobsAdapterGeneration = undefined
1683
+ }
1684
+ }
1526
1685
  }
1527
1686
 
1528
1687
  /**
@@ -1531,6 +1690,10 @@ export default class VelociousConfiguration {
1531
1690
  * @returns {void}
1532
1691
  */
1533
1692
  setBackgroundJobsConfig(backgroundJobs) {
1693
+ if (this._backgroundJobsAdapterGeneration && backgroundJobs.adapter !== undefined) {
1694
+ throw new Error("Cannot replace backgroundJobs.adapter during an active adapter lifecycle; close it first")
1695
+ }
1696
+
1534
1697
  this._backgroundJobs = Object.assign({}, this._backgroundJobs, backgroundJobs)
1535
1698
  }
1536
1699
 
@@ -2099,8 +2262,8 @@ export default class VelociousConfiguration {
2099
2262
  }
2100
2263
 
2101
2264
  const initializeModelsPromise = (async () => {
2102
- const shouldSkipDummyModelInitialization = process.env.VELOCIOUS_SKIP_DUMMY_MODEL_INITIALIZATION === "1"
2103
- && process.env.VELOCIOUS_BROWSER_TESTS === "true"
2265
+ const shouldSkipDummyModelInitialization = globalThis.process?.env.VELOCIOUS_SKIP_DUMMY_MODEL_INITIALIZATION === "1"
2266
+ && globalThis.process?.env.VELOCIOUS_BROWSER_TESTS === "true"
2104
2267
  && this.getEnvironment() === "test"
2105
2268
 
2106
2269
  if (!shouldSkipDummyModelInitialization) {
@@ -3425,34 +3588,50 @@ export default class VelociousConfiguration {
3425
3588
  const constructors = new Set()
3426
3589
 
3427
3590
  this._closeDatabaseConnectionsPromise = (async () => {
3428
- try {
3429
- // Close dedicated advisory-lock connections first: they are spawned outside the
3430
- // pools' tracked sets, so `pool.closeAll()` would not reach them and a lock held
3431
- // by a runner torn down mid-pass would leak until the DB server's `wait_timeout`.
3432
- // Still close the pools even if this throws, so a stuck lock connection does not
3433
- // leave the rest of the connections open.
3434
- await this._closeAdvisoryLockConnections()
3435
- } finally {
3436
- for (const pool of Object.values(this.databasePools)) {
3437
- if (!pool) continue
3591
+ /** @type {Error[]} */
3592
+ const closeErrors = []
3438
3593
 
3439
- await pool.closeAll()
3594
+ try {
3595
+ await this.closeBackgroundJobsAdapter()
3596
+ } catch (error) {
3597
+ closeErrors.push(error instanceof Error ? error : new Error(String(error)))
3598
+ }
3440
3599
 
3441
- const PoolClass = /** @type {typeof import("./database/pool/base.js").default} */ (pool.constructor)
3442
- constructors.add(PoolClass)
3443
- }
3600
+ try {
3601
+ try {
3602
+ // Close dedicated advisory-lock connections first: they are spawned outside the
3603
+ // pools' tracked sets, so `pool.closeAll()` would not reach them and a lock held
3604
+ // by a runner torn down mid-pass would leak until the DB server's `wait_timeout`.
3605
+ // Still close the pools if this throws, so a stuck lock connection does not
3606
+ // leave the rest of the connections open.
3607
+ await this._closeAdvisoryLockConnections()
3608
+ } finally {
3609
+ for (const pool of Object.values(this.databasePools)) {
3610
+ if (!pool) continue
3611
+
3612
+ await pool.closeAll()
3613
+
3614
+ const PoolClass = /** @type {typeof import("./database/pool/base.js").default} */ (pool.constructor)
3615
+ constructors.add(PoolClass)
3616
+ }
3444
3617
 
3445
- for (const PoolClass of constructors) {
3446
- PoolClass.clearGlobalConnections(this)
3447
- }
3618
+ for (const PoolClass of constructors) {
3619
+ PoolClass.clearGlobalConnections(this)
3620
+ }
3448
3621
 
3449
- this._frontendTenantSqliteLifecycle.reset()
3622
+ this._frontendTenantSqliteLifecycle.reset()
3450
3623
 
3451
- // Allow full re-initialization after connections are closed.
3452
- this._modelInitializationGeneration += 1
3453
- this._modelsInitialized = false
3454
- this._isInitialized = false
3624
+ // Allow full re-initialization after connections are closed.
3625
+ this._modelInitializationGeneration += 1
3626
+ this._modelsInitialized = false
3627
+ this._isInitialized = false
3628
+ }
3629
+ } catch (error) {
3630
+ closeErrors.push(error instanceof Error ? error : new Error(String(error)))
3455
3631
  }
3632
+
3633
+ if (closeErrors.length === 1) throw closeErrors[0]
3634
+ if (closeErrors.length > 1) throw new AggregateError(closeErrors, "Failed to close background-jobs and database resources")
3456
3635
  })()
3457
3636
 
3458
3637
  try {
@@ -1,5 +1,6 @@
1
1
  // @ts-check
2
2
 
3
+ import BackgroundJobsAdapterClient from "../background-jobs/adapter-client.js"
3
4
  import {validateTimeZone} from "../time-zone.js"
4
5
 
5
6
  /**
@@ -575,6 +576,25 @@ export default class VelociousEnvironmentHandlerBase {
575
576
  return
576
577
  }
577
578
 
579
+ /**
580
+ * Creates the environment's default persistence adapter.
581
+ * @abstract
582
+ * @param {{configuration: import("../configuration.js").default}} _args - Adapter options.
583
+ * @returns {import("../background-jobs/adapter.js").default} - Default adapter.
584
+ */
585
+ createBackgroundJobsAdapter(_args) {
586
+ throw new Error("This environment requires an explicit backgroundJobs.adapter")
587
+ }
588
+
589
+ /**
590
+ * Creates the platform-neutral producer path for an explicit adapter.
591
+ * @param {{configuration: import("../configuration.js").default}} args - Client options.
592
+ * @returns {import("../background-jobs/types.js").BackgroundJobsProducer} - Adapter-backed producer.
593
+ */
594
+ backgroundJobsClient(args) {
595
+ return new BackgroundJobsAdapterClient(args)
596
+ }
597
+
578
598
  /**
579
599
  * Runs require command.
580
600
  * @abstract
@@ -2,6 +2,8 @@
2
2
 
3
3
  import "../database/annotations-async-hooks.js"
4
4
  import Base from "./base.js"
5
+ import BackgroundJobsClient from "../background-jobs/client.js"
6
+ import SqlBackgroundJobsAdapter from "../background-jobs/sql-adapter.js"
5
7
  import CliCommandsDestroyMigration from "./node/cli/commands/destroy/migration.js"
6
8
  import CliCommandsInit from "./node/cli/commands/init.js"
7
9
  import CliCommandsGenerateBaseModels from "./node/cli/commands/generate/base-models.js"
@@ -65,6 +67,25 @@ function pathWithinAllowedPrefixes(filePath, allowedPathPrefixes) {
65
67
  }
66
68
 
67
69
  export default class VelociousEnvironmentHandlerNode extends Base{
70
+ /**
71
+ * Creates the built-in SQL persistence adapter.
72
+ * @param {{configuration: import("../configuration.js").default}} args - Adapter options.
73
+ * @returns {SqlBackgroundJobsAdapter} - SQL adapter.
74
+ */
75
+ createBackgroundJobsAdapter({configuration}) {
76
+ return new SqlBackgroundJobsAdapter({configuration})
77
+ }
78
+
79
+ /**
80
+ * Preserves the Node TCP producer and main-process wake-up path. The main
81
+ * owns the configured persistence adapter; Node producers never bypass it.
82
+ * @param {{configuration: import("../configuration.js").default}} args - Client options.
83
+ * @returns {import("../background-jobs/types.js").BackgroundJobsProducer} - Producer client.
84
+ */
85
+ backgroundJobsClient({configuration}) {
86
+ return new BackgroundJobsClient({configuration})
87
+ }
88
+
68
89
  /**
69
90
  * Gives concurrent shared-transaction child jobs independent proxy sessions.
70
91
  * A configured single-connection pool shares mutable transaction state between
@@ -1100,26 +1121,12 @@ export default class VelociousEnvironmentHandlerNode extends Base{
1100
1121
  * @returns {Promise<void>} - Resolves when complete.
1101
1122
  */
1102
1123
  async ensureFrameworkSchema({dbs}) {
1103
- const {default: BackgroundJobsStore} = await import("../background-jobs/store.js")
1104
- const store = new BackgroundJobsStore({configuration: this.getConfiguration()})
1105
- const databaseIdentifier = store.getDatabaseIdentifier() ?? "default"
1106
- const frameworkDb = dbs[databaseIdentifier]
1107
-
1108
- // Only ensure the framework schema when the background-jobs database is actually
1109
- // part of this migrate operation. When it isn't among the migrated set — e.g.
1110
- // `db:tenants:migrate <tenant>`, which migrates only tenant databases — the
1111
- // framework store lives elsewhere (typically the default DB) and was already
1112
- // ensured by the plain `db:migrate` that precedes it. Reaching into it here would
1113
- // open a fresh connection to that shared database once per tenant worker for
1114
- // schema work that is already applied. So skip when the framework DB isn't in
1115
- // this set; the runtime store still creates it lazily if a plain migrate never
1116
- // ran. Queue-cap reconciliation never runs on this path at all — it belongs to
1117
- // main-process startup (`BackgroundJobsStore#reconcileQueueConcurrency`).
1118
- if (!frameworkDb) return
1119
-
1120
- // Reuse the connection db:migrate already holds for this database; opening a
1121
- // nested checkout would deadlock a database whose pool is capped at one connection.
1122
- await store.ensureSchema(frameworkDb)
1124
+ // Migration passes its already checked-out DB into the adapter hook. Do not
1125
+ // run runtime readiness here: SQL readiness would open a nested checkout and
1126
+ // can deadlock a single-connection migration pool.
1127
+ const adapter = this.getConfiguration().getBackgroundJobsAdapter()
1128
+
1129
+ await adapter.ensureFrameworkSchema({dbs})
1123
1130
  }
1124
1131
 
1125
1132
  /**
@@ -175,7 +175,6 @@ const frontendModelJoinedPathsSymbol = Symbol("frontendModelJoinedPaths")
175
175
  const frontendModelGroupedColumnsSymbol = Symbol("frontendModelGroupedColumns")
176
176
  const frontendModelWhereNoMatchSymbol = Symbol("frontendModelWhereNoMatch")
177
177
  const frontendModelClientSafeErrorMessage = "Request failed."
178
- const frontendModelDebugErrorEnvironments = new Set(["development", "test"])
179
178
 
180
179
  /**
181
180
  * Builds a client-safe sync replay validation error.
@@ -277,10 +276,10 @@ function frontendModelVelociousMetadataForError(error) {
277
276
  /**
278
277
  * Runs frontend model client message for error.
279
278
  * @param {unknown} error - Caught error.
280
- * @param {boolean} forwardUnexpectedErrorMessage - Whether unexpected error messages may be exposed.
279
+ * @param {boolean} exposeInternalErrorsToClients - Whether unexpected error messages may be exposed.
281
280
  * @returns {string} - Message safe to return to API clients.
282
281
  */
283
- function frontendModelClientMessageForError(error, forwardUnexpectedErrorMessage) {
282
+ function frontendModelClientMessageForError(error, exposeInternalErrorsToClients) {
284
283
  if (error instanceof RecordNotFoundError) {
285
284
  return "Record not found."
286
285
  }
@@ -301,7 +300,7 @@ function frontendModelClientMessageForError(error, forwardUnexpectedErrorMessage
301
300
  return error.message
302
301
  }
303
302
 
304
- if (forwardUnexpectedErrorMessage && error instanceof Error) return error.message
303
+ if (exposeInternalErrorsToClients && error instanceof Error) return error.message
305
304
 
306
305
  return frontendModelClientSafeErrorMessage
307
306
  }
@@ -310,14 +309,11 @@ function frontendModelClientMessageForError(error, forwardUnexpectedErrorMessage
310
309
  * Runs frontend model debug payload for error.
311
310
  * @param {object} args - Arguments.
312
311
  * @param {import("./configuration.js").default} args.configuration - Current configuration.
313
- * @param {string} args.environment - Current environment.
314
312
  * @param {unknown} args.error - Caught error.
315
- * @returns {import("./configuration-types.js").ClientErrorPayloadReporterPayload} - Optional debug payload for non-production environments.
313
+ * @returns {import("./configuration-types.js").ClientErrorPayloadReporterPayload} - Optional internal error details when client exposure is enabled.
316
314
  */
317
- function frontendModelDebugPayloadForError({configuration, environment, error}) {
318
- const debugAllowed = frontendModelDebugErrorEnvironments.has(environment) || environment !== "production" && configuration.getExposeInternalErrorsToClients()
319
-
320
- if (!debugAllowed) {
315
+ function frontendModelDebugPayloadForError({configuration, error}) {
316
+ if (!configuration.getExposeInternalErrorsToClients()) {
321
317
  return {}
322
318
  }
323
319
 
@@ -3259,10 +3255,9 @@ export default class FrontendModelController extends Controller {
3259
3255
  * Runs frontend model client error payload for error.
3260
3256
  * @param {unknown} error - Caught error.
3261
3257
  * @param {FrontendModelEndpointErrorContext | undefined} [endpointErrorContext] - Frontend-model endpoint error context.
3262
- * @param {{forwardUnexpectedErrorMessage?: boolean}} [options] - Client error rendering options.
3263
3258
  * @returns {Promise<import("./configuration-types.js").ClientErrorPayloadReporterPayload>} - Client payload for the current environment.
3264
3259
  */
3265
- async frontendModelClientErrorPayloadForError(error, endpointErrorContext, {forwardUnexpectedErrorMessage = false} = {}) {
3260
+ async frontendModelClientErrorPayloadForError(error, endpointErrorContext) {
3266
3261
  const velociousMetadata = frontendModelVelociousMetadataForError(error)
3267
3262
  const normalizedError = error instanceof Error ? error : new Error(String(error))
3268
3263
  /** @type {import("./configuration-types.js").ClientErrorPayloadReporterPayload} */
@@ -3312,15 +3307,20 @@ export default class FrontendModelController extends Controller {
3312
3307
  request: this.getRequest()
3313
3308
  })
3314
3309
 
3310
+ if (!this.getConfiguration().getExposeInternalErrorsToClients()) {
3311
+ delete reporterPayload.debugBacktrace
3312
+ delete reporterPayload.debugErrorClass
3313
+ delete reporterPayload.debugErrorMessage
3314
+ }
3315
+
3315
3316
  return {
3316
3317
  ...reporterPayload,
3317
3318
  ...this.frontendModelErrorPayload(frontendModelClientMessageForError(
3318
3319
  error,
3319
- forwardUnexpectedErrorMessage && !this.getConfiguration().getSecureFrontendModelErrors()
3320
+ this.getConfiguration().getExposeInternalErrorsToClients()
3320
3321
  )),
3321
3322
  ...frontendModelDebugPayloadForError({
3322
3323
  configuration: this.getConfiguration(),
3323
- environment: this.getConfiguration().getEnvironment(),
3324
3324
  error
3325
3325
  }),
3326
3326
  ...(velociousMetadata ? {velocious: velociousMetadata} : {}),
@@ -4397,9 +4397,7 @@ export default class FrontendModelController extends Controller {
4397
4397
 
4398
4398
  responses.push({
4399
4399
  requestId,
4400
- response: await this.frontendModelClientErrorPayloadForError(error, errorContext, {
4401
- forwardUnexpectedErrorMessage: !isBuiltInCommand
4402
- })
4400
+ response: await this.frontendModelClientErrorPayloadForError(error, errorContext)
4403
4401
  })
4404
4402
  }
4405
4403
  }
@@ -4623,9 +4621,7 @@ export default class FrontendModelController extends Controller {
4623
4621
  await this.frontendModelLogEndpointError({error, errorContext})
4624
4622
 
4625
4623
  await this.render({
4626
- json: /** @type {Record<string, ReturnType<typeof JSON.parse>>} */ (serializeFrontendModelTransportValue(await this.frontendModelClientErrorPayloadForError(error, errorContext, {
4627
- forwardUnexpectedErrorMessage: true
4628
- }), this.transportSerializationOptions()))
4624
+ json: /** @type {Record<string, ReturnType<typeof JSON.parse>>} */ (serializeFrontendModelTransportValue(await this.frontendModelClientErrorPayloadForError(error, errorContext), this.transportSerializationOptions()))
4629
4625
  })
4630
4626
  }
4631
4627
  }
@@ -1,6 +1,5 @@
1
1
  // @ts-check
2
2
 
3
- import BackgroundJobsStore from "../background-jobs/store.js"
4
3
  import Configuration from "../configuration.js"
5
4
  import VelociousJob from "../background-jobs/job.js"
6
5
 
@@ -59,9 +58,9 @@ export default class PruneTerminalBackgroundJobsJob extends VelociousJob {
59
58
  async perform() {
60
59
  const configuration = Configuration.current()
61
60
  const config = configuration.getBackgroundJobsConfig()
62
- const store = new BackgroundJobsStore({configuration, databaseIdentifier: config.databaseIdentifier})
61
+ const adapter = await configuration.acquireReadyBackgroundJobsAdapter()
63
62
 
64
- await store.pruneTerminalJobs({
63
+ await adapter.pruneTerminalJobs({
65
64
  completedTtlMs: config.retention.completedTtlMs,
66
65
  failedTtlMs: config.retention.failedTtlMs,
67
66
  batchSize: config.retention.batchSize
@@ -0,0 +1,41 @@
1
+ /** Platform-neutral producer client for a configured adapter. */
2
+ export default class BackgroundJobsAdapterClient {
3
+ configuration: import("../configuration.js").default;
4
+ /**
5
+ * Creates an adapter-backed producer.
6
+ * @param {{configuration: import("../configuration.js").default}} args - Client options.
7
+ */
8
+ constructor({ configuration }: {
9
+ configuration: import("../configuration.js").default;
10
+ });
11
+ /**
12
+ * Enqueues a job through the configured adapter.
13
+ * @param {{jobName: string, args: Array<ReturnType<typeof JSON.parse>>, options?: import("./types.js").BackgroundJobOptions}} args - Job request.
14
+ * @returns {Promise<string>} - Job id.
15
+ */
16
+ enqueue(args: {
17
+ jobName: string;
18
+ args: Array<ReturnType<typeof JSON.parse>>;
19
+ options?: import("./types.js").BackgroundJobOptions;
20
+ }): Promise<string>;
21
+ /**
22
+ * Replaces a stable schedule through the configured adapter.
23
+ * @param {{scheduleKey: string, jobName: string, args: Array<ReturnType<typeof JSON.parse>>, options?: import("./types.js").BackgroundJobOptions}} args - Replacement request.
24
+ * @returns {Promise<import("./types.js").BackgroundJobReplacementResult>} - Replacement result.
25
+ */
26
+ replaceScheduled(args: {
27
+ scheduleKey: string;
28
+ jobName: string;
29
+ args: Array<ReturnType<typeof JSON.parse>>;
30
+ options?: import("./types.js").BackgroundJobOptions;
31
+ }): Promise<import("./types.js").BackgroundJobReplacementResult>;
32
+ /**
33
+ * Cancels a stable schedule through the configured adapter.
34
+ * @param {{scheduleKey: string}} args - Cancellation request.
35
+ * @returns {Promise<import("./types.js").BackgroundJobCancellationResult>} - Cancellation result.
36
+ */
37
+ cancelScheduled({ scheduleKey }: {
38
+ scheduleKey: string;
39
+ }): Promise<import("./types.js").BackgroundJobCancellationResult>;
40
+ }
41
+ //# sourceMappingURL=adapter-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter-client.d.ts","sourceRoot":"","sources":["../../../src/background-jobs/adapter-client.js"],"names":[],"mappings":"AAEA,iEAAiE;AACjE,MAAM,CAAC,OAAO,OAAO,2BAA2B;IAMvC,aAAa;IALpB;;;OAGG;IACH,YAAY,EAAC,aAAa,EAAC,EAFhB;QAAC,aAAa,EAAE,OAAO,qBAAqB,EAAE,OAAO,CAAA;KAErC,EAE1B;IAED;;;;OAIG;IACG,OAAO,CAAC,IAAI,EAHP;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,KAAK,CAAC,UAAU,CAAC,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,YAAY,EAAE,oBAAoB,CAAA;KAG1G,GAFL,OAAO,CAAC,MAAM,CAAC,CAM3B;IAED;;;;OAIG;IACG,gBAAgB,CAAC,IAAI,EAHhB;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,KAAK,CAAC,UAAU,CAAC,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,YAAY,EAAE,oBAAoB,CAAA;KAGtH,GAFd,OAAO,CAAC,OAAO,YAAY,EAAE,8BAA8B,CAAC,CAMxE;IAED;;;;OAIG;IACG,eAAe,CAAC,EAAC,WAAW,EAAC,EAHxB;QAAC,WAAW,EAAE,MAAM,CAAA;KAGI,GAFtB,OAAO,CAAC,OAAO,YAAY,EAAE,+BAA+B,CAAC,CAMzE;CACF"}