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.
- package/README.md +18 -9
- package/build/background-jobs/adapter-client.js +45 -0
- package/build/background-jobs/adapter.js +139 -0
- package/build/background-jobs/job-registry.js +1 -1
- package/build/background-jobs/job.js +20 -129
- package/build/background-jobs/main.js +71 -28
- package/build/background-jobs/perform-job.js +23 -0
- package/build/background-jobs/platform-job.js +156 -0
- package/build/background-jobs/runtime.js +156 -0
- package/build/background-jobs/sql-adapter.js +20 -0
- package/build/background-jobs/store.js +3 -1
- package/build/background-jobs/types.js +10 -0
- package/build/background-jobs/web/controller.js +5 -2
- package/build/background-jobs/worker.js +6 -8
- package/build/configuration-types.js +7 -2
- package/build/configuration.js +224 -45
- package/build/environment-handlers/base.js +20 -0
- package/build/environment-handlers/node.js +27 -20
- package/build/frontend-model-controller.js +16 -20
- package/build/jobs/prune-terminal-background-jobs.js +2 -3
- package/build/src/background-jobs/adapter-client.d.ts +41 -0
- package/build/src/background-jobs/adapter-client.d.ts.map +1 -0
- package/build/src/background-jobs/adapter-client.js +39 -0
- package/build/src/background-jobs/adapter.d.ts +165 -0
- package/build/src/background-jobs/adapter.d.ts.map +1 -0
- package/build/src/background-jobs/adapter.js +121 -0
- package/build/src/background-jobs/job-registry.d.ts +1 -1
- package/build/src/background-jobs/job-registry.d.ts.map +1 -1
- package/build/src/background-jobs/job-registry.js +2 -2
- package/build/src/background-jobs/job.d.ts +6 -69
- package/build/src/background-jobs/job.d.ts.map +1 -1
- package/build/src/background-jobs/job.js +17 -116
- package/build/src/background-jobs/main.d.ts +13 -3
- package/build/src/background-jobs/main.d.ts.map +1 -1
- package/build/src/background-jobs/main.js +66 -30
- package/build/src/background-jobs/perform-job.d.ts +16 -0
- package/build/src/background-jobs/perform-job.d.ts.map +1 -0
- package/build/src/background-jobs/perform-job.js +22 -0
- package/build/src/background-jobs/platform-job.d.ts +110 -0
- package/build/src/background-jobs/platform-job.d.ts.map +1 -0
- package/build/src/background-jobs/platform-job.js +138 -0
- package/build/src/background-jobs/runtime.d.ts +78 -0
- package/build/src/background-jobs/runtime.d.ts.map +1 -0
- package/build/src/background-jobs/runtime.js +132 -0
- package/build/src/background-jobs/sql-adapter.d.ts +13 -0
- package/build/src/background-jobs/sql-adapter.d.ts.map +1 -0
- package/build/src/background-jobs/sql-adapter.js +18 -0
- package/build/src/background-jobs/store.d.ts +2 -1
- package/build/src/background-jobs/store.d.ts.map +1 -1
- package/build/src/background-jobs/store.js +4 -2
- package/build/src/background-jobs/types.d.ts +41 -0
- package/build/src/background-jobs/types.d.ts.map +1 -1
- package/build/src/background-jobs/types.js +11 -1
- package/build/src/background-jobs/web/controller.d.ts.map +1 -1
- package/build/src/background-jobs/web/controller.js +5 -3
- package/build/src/background-jobs/worker.d.ts.map +1 -1
- package/build/src/background-jobs/worker.js +7 -8
- package/build/src/configuration-types.d.ts +20 -4
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +7 -3
- package/build/src/configuration.d.ts +53 -4
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +209 -44
- package/build/src/environment-handlers/base.d.ts +17 -0
- package/build/src/environment-handlers/base.d.ts.map +1 -1
- package/build/src/environment-handlers/base.js +19 -1
- package/build/src/environment-handlers/node.d.ts +18 -0
- package/build/src/environment-handlers/node.d.ts.map +1 -1
- package/build/src/environment-handlers/node.js +25 -20
- package/build/src/frontend-model-controller.d.ts +1 -4
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +16 -20
- package/build/src/jobs/prune-terminal-background-jobs.d.ts.map +1 -1
- package/build/src/jobs/prune-terminal-background-jobs.js +3 -4
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/background-jobs/adapter-client.js +45 -0
- package/src/background-jobs/adapter.js +139 -0
- package/src/background-jobs/job-registry.js +1 -1
- package/src/background-jobs/job.js +20 -129
- package/src/background-jobs/main.js +71 -28
- package/src/background-jobs/perform-job.js +23 -0
- package/src/background-jobs/platform-job.js +156 -0
- package/src/background-jobs/runtime.js +156 -0
- package/src/background-jobs/sql-adapter.js +20 -0
- package/src/background-jobs/store.js +3 -1
- package/src/background-jobs/types.js +10 -0
- package/src/background-jobs/web/controller.js +5 -2
- package/src/background-jobs/worker.js +6 -8
- package/src/configuration-types.js +7 -2
- package/src/configuration.js +224 -45
- package/src/environment-handlers/base.js +20 -0
- package/src/environment-handlers/node.js +27 -20
- package/src/frontend-model-controller.js +16 -20
- package/src/jobs/prune-terminal-background-jobs.js +2 -3
package/build/configuration.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
* @
|
|
465
|
+
* @deprecated Use `getExposeInternalErrorsToClients()`.
|
|
466
|
+
* @returns {boolean} Whether frontend-model internal error exposure is disabled.
|
|
453
467
|
*/
|
|
454
|
-
getSecureFrontendModelErrors() { return this.
|
|
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
|
|
1448
|
-
const
|
|
1449
|
-
const
|
|
1450
|
-
const
|
|
1451
|
-
const
|
|
1452
|
-
const
|
|
1453
|
-
const
|
|
1454
|
-
const
|
|
1455
|
-
const
|
|
1456
|
-
const
|
|
1457
|
-
const
|
|
1458
|
-
const
|
|
1459
|
-
const
|
|
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
|
|
2103
|
-
&& process
|
|
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
|
-
|
|
3429
|
-
|
|
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
|
-
|
|
3594
|
+
try {
|
|
3595
|
+
await this.closeBackgroundJobsAdapter()
|
|
3596
|
+
} catch (error) {
|
|
3597
|
+
closeErrors.push(error instanceof Error ? error : new Error(String(error)))
|
|
3598
|
+
}
|
|
3440
3599
|
|
|
3441
|
-
|
|
3442
|
-
|
|
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
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3618
|
+
for (const PoolClass of constructors) {
|
|
3619
|
+
PoolClass.clearGlobalConnections(this)
|
|
3620
|
+
}
|
|
3448
3621
|
|
|
3449
|
-
|
|
3622
|
+
this._frontendTenantSqliteLifecycle.reset()
|
|
3450
3623
|
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
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
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
const
|
|
1107
|
-
|
|
1108
|
-
|
|
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}
|
|
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,
|
|
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 (
|
|
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
|
|
313
|
+
* @returns {import("./configuration-types.js").ClientErrorPayloadReporterPayload} - Optional internal error details when client exposure is enabled.
|
|
316
314
|
*/
|
|
317
|
-
function frontendModelDebugPayloadForError({configuration,
|
|
318
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
61
|
+
const adapter = await configuration.acquireReadyBackgroundJobsAdapter()
|
|
63
62
|
|
|
64
|
-
await
|
|
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"}
|