velocious 1.0.698 → 1.0.700
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 +2 -2
- package/build/configuration-types.js +23 -0
- package/build/configuration.js +136 -1
- package/build/database/advisory-lock-backend-policy.js +204 -0
- package/build/database/advisory-lock-file-backend.js +348 -0
- package/build/database/advisory-lock-runner.js +239 -16
- package/build/database/drivers/base.js +318 -15
- package/build/database/drivers/mssql/index.js +1 -1
- package/build/database/drivers/mysql/index.js +4 -2
- package/build/database/drivers/pgsql/index.js +4 -2
- package/build/database/drivers/sqlite/base.js +13 -129
- package/build/database/drivers/sqlite/index.js +41 -62
- package/build/database/migrator.js +120 -50
- package/build/database/record/attachments/store.js +84 -1
- package/build/environment-handlers/base.js +13 -0
- package/build/environment-handlers/node.js +11 -0
- package/build/http-server/server-lock.js +21 -115
- package/build/src/configuration-types.d.ts +68 -0
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +21 -1
- package/build/src/configuration.d.ts +100 -1
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +124 -2
- package/build/src/database/advisory-lock-backend-policy.d.ts +145 -0
- package/build/src/database/advisory-lock-backend-policy.d.ts.map +1 -0
- package/build/src/database/advisory-lock-backend-policy.js +184 -0
- package/build/src/database/advisory-lock-file-backend.d.ts +194 -0
- package/build/src/database/advisory-lock-file-backend.d.ts.map +1 -0
- package/build/src/database/advisory-lock-file-backend.js +313 -0
- package/build/src/database/advisory-lock-runner.d.ts +116 -15
- package/build/src/database/advisory-lock-runner.d.ts.map +1 -1
- package/build/src/database/advisory-lock-runner.js +190 -17
- package/build/src/database/drivers/base.d.ts +125 -4
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +282 -17
- package/build/src/database/drivers/mssql/index.d.ts +1 -1
- package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mssql/index.js +2 -2
- package/build/src/database/drivers/mysql/index.d.ts +4 -2
- package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mysql/index.js +5 -3
- package/build/src/database/drivers/pgsql/index.d.ts +4 -2
- package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
- package/build/src/database/drivers/pgsql/index.js +5 -3
- package/build/src/database/drivers/sqlite/base.d.ts +11 -49
- package/build/src/database/drivers/sqlite/base.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/base.js +14 -114
- package/build/src/database/drivers/sqlite/index.d.ts +28 -19
- package/build/src/database/drivers/sqlite/index.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/index.js +41 -59
- package/build/src/database/migrator.d.ts.map +1 -1
- package/build/src/database/migrator.js +110 -56
- package/build/src/database/record/attachments/store.d.ts +0 -3
- package/build/src/database/record/attachments/store.d.ts.map +1 -1
- package/build/src/database/record/attachments/store.js +71 -2
- package/build/src/environment-handlers/base.d.ts +14 -0
- package/build/src/environment-handlers/base.d.ts.map +1 -1
- package/build/src/environment-handlers/base.js +13 -1
- package/build/src/environment-handlers/node.d.ts +11 -0
- package/build/src/environment-handlers/node.d.ts.map +1 -1
- package/build/src/environment-handlers/node.js +11 -1
- package/build/src/http-server/server-lock.d.ts +13 -37
- package/build/src/http-server/server-lock.d.ts.map +1 -1
- package/build/src/http-server/server-lock.js +22 -109
- package/build/src/testing/test-runner.d.ts +74 -0
- package/build/src/testing/test-runner.d.ts.map +1 -1
- package/build/src/testing/test-runner.js +140 -1
- package/build/src/testing/test-tenant-database-pool.d.ts +183 -0
- package/build/src/testing/test-tenant-database-pool.d.ts.map +1 -0
- package/build/src/testing/test-tenant-database-pool.js +388 -0
- package/build/src/testing/velocious-attempt-executor.d.ts.map +1 -1
- package/build/src/testing/velocious-attempt-executor.js +43 -1
- package/build/testing/test-runner.js +152 -0
- package/build/testing/test-tenant-database-pool.js +438 -0
- package/build/testing/velocious-attempt-executor.js +44 -0
- package/package.json +1 -1
- package/src/configuration-types.js +23 -0
- package/src/configuration.js +136 -1
- package/src/database/advisory-lock-backend-policy.js +204 -0
- package/src/database/advisory-lock-file-backend.js +348 -0
- package/src/database/advisory-lock-runner.js +239 -16
- package/src/database/drivers/base.js +318 -15
- package/src/database/drivers/mssql/index.js +1 -1
- package/src/database/drivers/mysql/index.js +4 -2
- package/src/database/drivers/pgsql/index.js +4 -2
- package/src/database/drivers/sqlite/base.js +13 -129
- package/src/database/drivers/sqlite/index.js +41 -62
- package/src/database/migrator.js +120 -50
- package/src/database/record/attachments/store.js +84 -1
- package/src/environment-handlers/base.js +13 -0
- package/src/environment-handlers/node.js +11 -0
- package/src/http-server/server-lock.js +21 -115
- package/src/testing/test-runner.js +152 -0
- package/src/testing/test-tenant-database-pool.js +438 -0
- package/src/testing/velocious-attempt-executor.js +44 -0
package/src/configuration.js
CHANGED
|
@@ -45,6 +45,7 @@ import VelociousPackage from "./packages/velocious-package.js"
|
|
|
45
45
|
import FrontendTenantSqliteLifecycle from "./tenants/frontend-tenant-sqlite-lifecycle.js"
|
|
46
46
|
import { resolveGenerationId, resolveInitialGenerationState, resolveLifecycleSocketPath } from "./background-jobs/generation-identity.js"
|
|
47
47
|
import { runShutdownSteps } from "./utils/shutdown-lifecycle.js"
|
|
48
|
+
import { resolveAdvisoryLockBackendPolicy } from "./database/advisory-lock-backend-policy.js"
|
|
48
49
|
|
|
49
50
|
export { CurrentConfigurationNotSetError }
|
|
50
51
|
|
|
@@ -245,6 +246,20 @@ export default class VelociousConfiguration {
|
|
|
245
246
|
* @type {Set<import("./database/drivers/base.js").default>} */
|
|
246
247
|
_advisoryLockConnections = new Set()
|
|
247
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Lazily created file-tier advisory-lock backend (STC-213). Stays `undefined`
|
|
251
|
+
* unless `advisoryLocks.backends.file.enabled: true` is explicitly configured —
|
|
252
|
+
* the file tier is never auto-enabled, and a non-Node environment handler
|
|
253
|
+
* (browser/Expo, no `fs`) cannot create one at all.
|
|
254
|
+
* @type {import("./database/advisory-lock-file-backend.js").default | undefined} */
|
|
255
|
+
_advisoryLockFileBackend = undefined
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Most recent advisory-lock acquisition per lock name — the "which backend
|
|
259
|
+
* served this lock" surface (design §3).
|
|
260
|
+
* @type {Map<string, {backend: import("./database/advisory-lock-backend-policy.js").AdvisoryLockTier, fencingSupported: boolean, servedAt: string}>} */
|
|
261
|
+
_advisoryLockAcquisitions = new Map()
|
|
262
|
+
|
|
248
263
|
/** @type {Map<string, number>} */
|
|
249
264
|
_schemaCacheGenerationsByReuseKey = new Map()
|
|
250
265
|
|
|
@@ -260,11 +275,12 @@ export default class VelociousConfiguration {
|
|
|
260
275
|
* Runs constructor.
|
|
261
276
|
* @param {import("./configuration-types.js").ConfigurationArgsType} args - Configuration arguments.
|
|
262
277
|
*/
|
|
263
|
-
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, mcp = false, packages, requestTimeoutMs, routeResolverHooks, scheduledBackgroundJobs, secureFrontendModelErrors, structureSql, sync, tenantDatabaseProviders, tenantDatabaseResolver, tenantResolver, testing, timeZone, timezoneOffsetMinutes, trustedProxies, websocketChannelResolver, websocketMessageHandlerResolver, ...restArgs}) {
|
|
278
|
+
constructor({abilityResolver, abilityResources, advisoryLocks, 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, mcp = false, packages, requestTimeoutMs, routeResolverHooks, scheduledBackgroundJobs, secureFrontendModelErrors, structureSql, sync, tenantDatabaseProviders, tenantDatabaseResolver, tenantResolver, testTenantPools, testing, timeZone, timezoneOffsetMinutes, trustedProxies, websocketChannelResolver, websocketMessageHandlerResolver, ...restArgs}) {
|
|
264
279
|
restArgsError(restArgs)
|
|
265
280
|
|
|
266
281
|
this._abilityResolver = abilityResolver
|
|
267
282
|
this._abilityResources = abilityResources || []
|
|
283
|
+
this._advisoryLocks = advisoryLocks
|
|
268
284
|
this._autoload = autoload
|
|
269
285
|
this._backgroundJobs = backgroundJobs
|
|
270
286
|
this._beacon = beacon
|
|
@@ -399,6 +415,7 @@ export default class VelociousConfiguration {
|
|
|
399
415
|
this._tenantDatabaseProviders = tenantDatabaseProviders || {}
|
|
400
416
|
this._tenantDatabaseResolver = tenantDatabaseResolver
|
|
401
417
|
this._tenantResolver = tenantResolver
|
|
418
|
+
this._testTenantPools = testTenantPools
|
|
402
419
|
this._websocketEvents = undefined
|
|
403
420
|
/**
|
|
404
421
|
* Stores the websocket channel subscribers value.
|
|
@@ -1445,6 +1462,12 @@ export default class VelociousConfiguration {
|
|
|
1445
1462
|
*/
|
|
1446
1463
|
getTenantDatabaseResolver() { return this._tenantDatabaseResolver }
|
|
1447
1464
|
|
|
1465
|
+
/**
|
|
1466
|
+
* Runs get test tenant pool configurations.
|
|
1467
|
+
* @returns {import("./configuration-types.js").TestTenantPoolsConfiguration | undefined} - Opt-in test tenant database pools keyed by database identifier.
|
|
1468
|
+
*/
|
|
1469
|
+
getTestTenantPools() { return this._testTenantPools }
|
|
1470
|
+
|
|
1448
1471
|
/**
|
|
1449
1472
|
* Runs get enforce tenant database scopes.
|
|
1450
1473
|
* @returns {boolean} - Whether tenant-switched models require a resolved tenant database identifier.
|
|
@@ -1514,6 +1537,13 @@ export default class VelociousConfiguration {
|
|
|
1514
1537
|
*/
|
|
1515
1538
|
setTenantDatabaseResolver(resolver) { this._tenantDatabaseResolver = resolver }
|
|
1516
1539
|
|
|
1540
|
+
/**
|
|
1541
|
+
* Runs set test tenant pool configurations.
|
|
1542
|
+
* @param {import("./configuration-types.js").TestTenantPoolsConfiguration | undefined} pools - Opt-in test tenant database pools.
|
|
1543
|
+
* @returns {void} - No return value.
|
|
1544
|
+
*/
|
|
1545
|
+
setTestTenantPools(pools) { this._testTenantPools = pools }
|
|
1546
|
+
|
|
1517
1547
|
/**
|
|
1518
1548
|
* Runs set enforce tenant database scopes.
|
|
1519
1549
|
* @param {boolean} newValue - Whether tenant-switched models require a resolved tenant database identifier.
|
|
@@ -4062,6 +4092,102 @@ export default class VelociousConfiguration {
|
|
|
4062
4092
|
})
|
|
4063
4093
|
}
|
|
4064
4094
|
|
|
4095
|
+
/**
|
|
4096
|
+
* Resolves the `advisoryLocks` configuration block (STC-210 design §10).
|
|
4097
|
+
* Full backward compatibility: an app that configures nothing gets today's
|
|
4098
|
+
* driver-native-only behavior — the file backend is **disabled by default**
|
|
4099
|
+
* and only ever selectable by explicit configuration.
|
|
4100
|
+
* @returns {{backends: {file: {directory?: string, enabled: boolean, leaseMs: number, renewIntervalMs: number}}, policy: import("./database/advisory-lock-backend-policy.js").AdvisoryLockBackendPolicyEntry[]}} - Advisory-lock configuration with defaults applied.
|
|
4101
|
+
*/
|
|
4102
|
+
getAdvisoryLockConfig() {
|
|
4103
|
+
const configured = this._advisoryLocks || {}
|
|
4104
|
+
const fileConfigured = configured.backends?.file || {}
|
|
4105
|
+
|
|
4106
|
+
return {
|
|
4107
|
+
backends: {
|
|
4108
|
+
file: {
|
|
4109
|
+
directory: fileConfigured.directory,
|
|
4110
|
+
enabled: fileConfigured.enabled === true,
|
|
4111
|
+
leaseMs: typeof fileConfigured.leaseMs === "number" ? fileConfigured.leaseMs : 30_000,
|
|
4112
|
+
renewIntervalMs: typeof fileConfigured.renewIntervalMs === "number" ? fileConfigured.renewIntervalMs : 10_000
|
|
4113
|
+
}
|
|
4114
|
+
},
|
|
4115
|
+
policy: Array.isArray(configured.policy) ? configured.policy : []
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
4118
|
+
|
|
4119
|
+
/**
|
|
4120
|
+
* Runs get advisory lock policy.
|
|
4121
|
+
* @returns {import("./database/advisory-lock-backend-policy.js").AdvisoryLockBackendPolicyEntry[]} - Per-lock-name backend policy entries.
|
|
4122
|
+
*/
|
|
4123
|
+
getAdvisoryLockPolicy() {
|
|
4124
|
+
return this.getAdvisoryLockConfig().policy
|
|
4125
|
+
}
|
|
4126
|
+
|
|
4127
|
+
/**
|
|
4128
|
+
* Resolves the effective backend policy for one lock name (design §5).
|
|
4129
|
+
* @param {string} lockName - Concrete lock name.
|
|
4130
|
+
* @returns {import("./database/advisory-lock-backend-policy.js").ResolvedAdvisoryLockBackendPolicy} - Resolved policy.
|
|
4131
|
+
*/
|
|
4132
|
+
resolveAdvisoryLockPolicy(lockName) {
|
|
4133
|
+
return resolveAdvisoryLockBackendPolicy(lockName, this.getAdvisoryLockPolicy())
|
|
4134
|
+
}
|
|
4135
|
+
|
|
4136
|
+
/**
|
|
4137
|
+
* Returns the file-tier advisory-lock backend when this deployment has
|
|
4138
|
+
* explicitly enabled it (`advisoryLocks.backends.file.enabled: true`),
|
|
4139
|
+
* creating it on first use. Returns `undefined` when the tier is disabled
|
|
4140
|
+
* (the default) — callers must never fall back to it implicitly. Creation
|
|
4141
|
+
* goes through the environment handler so browser/Expo configurations
|
|
4142
|
+
* without `fs` fail loudly instead of half-working.
|
|
4143
|
+
* @returns {Promise<import("./database/advisory-lock-file-backend.js").default | undefined>} - Resolves with the enabled file backend, or undefined.
|
|
4144
|
+
*/
|
|
4145
|
+
async getAdvisoryLockFileBackend() {
|
|
4146
|
+
const config = this.getAdvisoryLockConfig()
|
|
4147
|
+
|
|
4148
|
+
if (!config.backends.file.enabled) return undefined
|
|
4149
|
+
if (this._advisoryLockFileBackend) return this._advisoryLockFileBackend
|
|
4150
|
+
|
|
4151
|
+
const directory = config.backends.file.directory || `${this.getDirectory()}/tmp/advisory-locks`
|
|
4152
|
+
|
|
4153
|
+
this._advisoryLockFileBackend = await this.getEnvironmentHandler().createAdvisoryLockFileBackend({
|
|
4154
|
+
directory,
|
|
4155
|
+
leaseMs: config.backends.file.leaseMs,
|
|
4156
|
+
renewIntervalMs: config.backends.file.renewIntervalMs
|
|
4157
|
+
})
|
|
4158
|
+
|
|
4159
|
+
return this._advisoryLockFileBackend
|
|
4160
|
+
}
|
|
4161
|
+
|
|
4162
|
+
/**
|
|
4163
|
+
* Records which backend tier served one advisory-lock acquisition so
|
|
4164
|
+
* `getAdvisoryLockBackendStatus()` can answer "which backend served this
|
|
4165
|
+
* lock" (design §3).
|
|
4166
|
+
* @param {object} args - Acquisition record.
|
|
4167
|
+
* @param {string} args.lockName - Lock name.
|
|
4168
|
+
* @param {import("./database/advisory-lock-backend-policy.js").AdvisoryLockTier} args.backend - Tier that served the acquisition.
|
|
4169
|
+
* @param {boolean} args.fencingSupported - Whether the serving tier carries fencing tokens.
|
|
4170
|
+
* @returns {void}
|
|
4171
|
+
*/
|
|
4172
|
+
recordAdvisoryLockAcquisition({lockName, backend, fencingSupported}) {
|
|
4173
|
+
this._advisoryLockAcquisitions.set(lockName, {backend, fencingSupported, servedAt: new Date().toISOString()})
|
|
4174
|
+
}
|
|
4175
|
+
|
|
4176
|
+
/**
|
|
4177
|
+
* The "which backend served this lock" diagnostics surface (design §3):
|
|
4178
|
+
* per lock name, the tier of the most recent acquisition, whether that tier
|
|
4179
|
+
* carries fencing tokens, and the resolved policy (minimum/allowed/preferred
|
|
4180
|
+
* tiers) for the name.
|
|
4181
|
+
* @param {string} lockName - Lock name.
|
|
4182
|
+
* @returns {{acquisition: {backend: import("./database/advisory-lock-backend-policy.js").AdvisoryLockTier, fencingSupported: boolean, servedAt: string} | null, policy: import("./database/advisory-lock-backend-policy.js").ResolvedAdvisoryLockBackendPolicy}} - Backend status for the name.
|
|
4183
|
+
*/
|
|
4184
|
+
getAdvisoryLockBackendStatus(lockName) {
|
|
4185
|
+
return {
|
|
4186
|
+
acquisition: this._advisoryLockAcquisitions.get(lockName) ?? null,
|
|
4187
|
+
policy: this.resolveAdvisoryLockPolicy(lockName)
|
|
4188
|
+
}
|
|
4189
|
+
}
|
|
4190
|
+
|
|
4065
4191
|
/**
|
|
4066
4192
|
* Registers a dedicated connection that currently holds an advisory lock, so a
|
|
4067
4193
|
* shutdown can close it and release the lock. See `_advisoryLockConnections`.
|
|
@@ -4141,6 +4267,15 @@ export default class VelociousConfiguration {
|
|
|
4141
4267
|
// leave the rest of the connections open.
|
|
4142
4268
|
await this._closeAdvisoryLockConnections()
|
|
4143
4269
|
} finally {
|
|
4270
|
+
try {
|
|
4271
|
+
// Release file-tier advisory-lock holds explicitly (STC-213): the
|
|
4272
|
+
// lease would make them reclaimable anyway, but a clean shutdown
|
|
4273
|
+
// should not leave lock directories behind for the staleness path.
|
|
4274
|
+
await this._advisoryLockFileBackend?.releaseAllHeld()
|
|
4275
|
+
} catch (error) {
|
|
4276
|
+
closeErrors.push(error instanceof Error ? error : new Error(String(error)))
|
|
4277
|
+
}
|
|
4278
|
+
|
|
4144
4279
|
for (const pool of Object.values(this.databasePools)) {
|
|
4145
4280
|
if (!pool) continue
|
|
4146
4281
|
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Advisory-lock backend tiers (STC-210 design, `docs/advisory-lock-backends.md` §1/§9).
|
|
5
|
+
* `process-local` and `file` are host-local; `driver-native` and `lock-server` are
|
|
6
|
+
* cross-host. Tier strength order (strongest first):
|
|
7
|
+
* `lock-server` ≡ `driver-native` > `file` > `process-local`.
|
|
8
|
+
* @typedef {"process-local" | "file" | "driver-native" | "lock-server"} AdvisoryLockTier
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* One per-lock-name backend policy entry as declared in configuration (§5).
|
|
12
|
+
* @typedef {object} AdvisoryLockBackendPolicyEntry
|
|
13
|
+
* @property {string} name - Lock name or `*`-suffixed glob pattern (e.g. `background_jobs:*`).
|
|
14
|
+
* @property {AdvisoryLockTier} [minTier] - Weakest tier this name may ever be served by. Defaults to `driver-native` (default-deny).
|
|
15
|
+
* @property {AdvisoryLockTier[]} [allowedTiers] - Explicit whitelist of servable tiers. Defaults to every tier at or above `minTier`.
|
|
16
|
+
* @property {AdvisoryLockTier} [preferredTier] - Tier used when available; falling below it triggers the fallback report (§3). Defaults to `driver-native`.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Policy resolved for one concrete lock name, with tier gaps already closed:
|
|
20
|
+
* `allowedTiers` always contains every tier at or above `minTier`, so a
|
|
21
|
+
* cross-host-minimum name can never be served by `file` or `process-local`.
|
|
22
|
+
* @typedef {object} ResolvedAdvisoryLockBackendPolicy
|
|
23
|
+
* @property {AdvisoryLockTier} minTier - Weakest tier this name may be served by.
|
|
24
|
+
* @property {AdvisoryLockTier[]} allowedTiers - Tiers that may serve this name (minimum-closed).
|
|
25
|
+
* @property {AdvisoryLockTier} preferredTier - Tier preferred when available.
|
|
26
|
+
* @property {AdvisoryLockBackendPolicyEntry | null} entry - The matched configuration entry, or null for the default policy.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Thrown when an acquisition would have to be served by a tier below the
|
|
31
|
+
* lock name's declared minimum tier. Fail-closed refusal (§9): the name is
|
|
32
|
+
* never served by the weaker tier, and the refusal is reported on the
|
|
33
|
+
* framework-error channel as `advisory-lock-min-tier-refused`.
|
|
34
|
+
*/
|
|
35
|
+
export class AdvisoryLockMinTierRefusedError extends Error {
|
|
36
|
+
/**
|
|
37
|
+
* Runs constructor.
|
|
38
|
+
* @param {string} message - Error message.
|
|
39
|
+
* @param {{lockName: string, requestedBackend: AdvisoryLockTier, minTier: AdvisoryLockTier}} args - Refusal details.
|
|
40
|
+
*/
|
|
41
|
+
constructor(message, {lockName, requestedBackend, minTier}) {
|
|
42
|
+
super(message)
|
|
43
|
+
this.name = "AdvisoryLockMinTierRefusedError"
|
|
44
|
+
this.lockName = lockName
|
|
45
|
+
this.requestedBackend = requestedBackend
|
|
46
|
+
this.minTier = minTier
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Thrown when no enabled backend that satisfies the lock name's minimum tier
|
|
52
|
+
* is available (§9). Distinct from `AdvisoryLockTimeoutError` (a backend
|
|
53
|
+
* served the wait but the holder never released).
|
|
54
|
+
*/
|
|
55
|
+
export class AdvisoryLockBackendUnavailableError extends Error {
|
|
56
|
+
/**
|
|
57
|
+
* Runs constructor.
|
|
58
|
+
* @param {string} message - Error message.
|
|
59
|
+
* @param {{lockName: string, minTier: AdvisoryLockTier, reasons: string[]}} args - Unavailability details.
|
|
60
|
+
*/
|
|
61
|
+
constructor(message, {lockName, minTier, reasons}) {
|
|
62
|
+
super(message)
|
|
63
|
+
this.name = "AdvisoryLockBackendUnavailableError"
|
|
64
|
+
this.lockName = lockName
|
|
65
|
+
this.minTier = minTier
|
|
66
|
+
this.reasons = reasons
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Tier strength ranks (§9): higher rank is stronger. `lock-server` and
|
|
72
|
+
* `driver-native` are both cross-host and rank equal.
|
|
73
|
+
* @type {Record<AdvisoryLockTier, number>} */
|
|
74
|
+
const TIER_RANKS = {
|
|
75
|
+
"driver-native": 2,
|
|
76
|
+
file: 1,
|
|
77
|
+
"lock-server": 3,
|
|
78
|
+
"process-local": 0
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** All tiers ordered strongest-first. */
|
|
82
|
+
const TIERS_STRONGEST_FIRST = /** @type {AdvisoryLockTier[]} */ (["lock-server", "driver-native", "file", "process-local"])
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Runs tier rank.
|
|
86
|
+
* @param {AdvisoryLockTier} tier - Tier to rank.
|
|
87
|
+
* @returns {number} - Strength rank (higher is stronger).
|
|
88
|
+
*/
|
|
89
|
+
export function advisoryLockTierRank(tier) {
|
|
90
|
+
const rank = TIER_RANKS[tier]
|
|
91
|
+
|
|
92
|
+
if (rank === undefined) throw new Error(`Unknown advisory-lock tier ${JSON.stringify(String(tier))}`)
|
|
93
|
+
|
|
94
|
+
return rank
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Runs tier is at least as strong as.
|
|
99
|
+
* @param {AdvisoryLockTier} tier - Candidate serving tier.
|
|
100
|
+
* @param {AdvisoryLockTier} minimum - Required minimum tier.
|
|
101
|
+
* @returns {boolean} - Whether `tier` satisfies the minimum.
|
|
102
|
+
*/
|
|
103
|
+
export function advisoryLockTierSatisfies(tier, minimum) {
|
|
104
|
+
return advisoryLockTierRank(tier) >= advisoryLockTierRank(minimum)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Resolves the effective backend policy for one concrete lock name:
|
|
109
|
+
* longest-prefix glob match over the configured entries (exact names win),
|
|
110
|
+
* default-deny for undeclared names (`minTier: driver-native`,
|
|
111
|
+
* `preferredTier: driver-native`), and `allowedTiers` closed upward so a
|
|
112
|
+
* cross-host minimum can never be served by a host-local tier.
|
|
113
|
+
* @param {string} lockName - Concrete lock name.
|
|
114
|
+
* @param {AdvisoryLockBackendPolicyEntry[] | undefined} policy - Configured policy entries.
|
|
115
|
+
* @returns {ResolvedAdvisoryLockBackendPolicy} - Resolved policy.
|
|
116
|
+
*/
|
|
117
|
+
export function resolveAdvisoryLockBackendPolicy(lockName, policy) {
|
|
118
|
+
const entry = matchAdvisoryLockPolicyEntry(lockName, policy)
|
|
119
|
+
const minTier = entry?.minTier ?? "driver-native"
|
|
120
|
+
const preferredTier = entry?.preferredTier ?? "driver-native"
|
|
121
|
+
|
|
122
|
+
// §9: eligibility filters to tier strength >= minTier even when a weaker
|
|
123
|
+
// tier was declared in `allowedTiers` — a cross-host minimum can never be
|
|
124
|
+
// served by `file` or `process-local`. A `preferredTier` below the minimum
|
|
125
|
+
// stays visible here so the runner can refuse it loudly before contacting
|
|
126
|
+
// the weaker backend.
|
|
127
|
+
const declaredAllowed = entry?.allowedTiers
|
|
128
|
+
const allowedTiers = TIERS_STRONGEST_FIRST.filter((tier) => {
|
|
129
|
+
if (!advisoryLockTierSatisfies(tier, minTier)) return false
|
|
130
|
+
|
|
131
|
+
if (!Array.isArray(declaredAllowed)) return true
|
|
132
|
+
|
|
133
|
+
return declaredAllowed.includes(tier)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
return {allowedTiers, entry: entry ?? null, minTier, preferredTier}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Matches one policy entry for a lock name: exact `name` matches win;
|
|
141
|
+
* otherwise the longest `*`-suffixed prefix pattern wins.
|
|
142
|
+
* @param {string} lockName - Concrete lock name.
|
|
143
|
+
* @param {AdvisoryLockBackendPolicyEntry[] | undefined} policy - Configured policy entries.
|
|
144
|
+
* @returns {AdvisoryLockBackendPolicyEntry | null} - Matched entry or null.
|
|
145
|
+
*/
|
|
146
|
+
function matchAdvisoryLockPolicyEntry(lockName, policy) {
|
|
147
|
+
if (!Array.isArray(policy)) return null
|
|
148
|
+
|
|
149
|
+
let bestPrefixMatch = null
|
|
150
|
+
let bestPrefixLength = -1
|
|
151
|
+
|
|
152
|
+
for (const entry of policy) {
|
|
153
|
+
if (typeof entry?.name !== "string") continue
|
|
154
|
+
|
|
155
|
+
if (entry.name === lockName) return entry
|
|
156
|
+
|
|
157
|
+
if (entry.name.endsWith("*")) {
|
|
158
|
+
const prefix = entry.name.slice(0, -1)
|
|
159
|
+
|
|
160
|
+
if (lockName.startsWith(prefix) && prefix.length > bestPrefixLength) {
|
|
161
|
+
bestPrefixMatch = entry
|
|
162
|
+
bestPrefixLength = prefix.length
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return bestPrefixMatch
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Refuses a downgrade below the lock name's minimum tier before any contact
|
|
172
|
+
* with the weaker backend (§9), reporting the refusal on the framework-error
|
|
173
|
+
* channel so a silent downgrade is impossible to hide.
|
|
174
|
+
* @param {object} args - Refusal args.
|
|
175
|
+
* @param {import("../configuration.js").default} args.configuration - Configuration owning the error channel.
|
|
176
|
+
* @param {string} args.lockName - Lock name.
|
|
177
|
+
* @param {AdvisoryLockTier} args.requestedBackend - Tier the acquisition would fall to.
|
|
178
|
+
* @param {AdvisoryLockTier} args.minTier - Declared minimum tier.
|
|
179
|
+
* @returns {never} - Always throws.
|
|
180
|
+
*/
|
|
181
|
+
export function refuseAdvisoryLockTierBelowMinimum({configuration, lockName, requestedBackend, minTier}) {
|
|
182
|
+
const error = new AdvisoryLockMinTierRefusedError(
|
|
183
|
+
`Advisory lock ${JSON.stringify(lockName)} requires minimum tier ${JSON.stringify(minTier)}; the ${JSON.stringify(requestedBackend)} tier is refused. Enable a cross-host backend or declare minTier: "file" for this lock name (single-host deployments only).`,
|
|
184
|
+
{lockName, minTier, requestedBackend}
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
configuration.getErrorEvents().emit("framework-error", {
|
|
188
|
+
context: "advisory-lock-min-tier-refused",
|
|
189
|
+
error,
|
|
190
|
+
lockName,
|
|
191
|
+
minTier,
|
|
192
|
+
requestedBackend
|
|
193
|
+
})
|
|
194
|
+
configuration.getErrorEvents().emit("all-error", {
|
|
195
|
+
context: "advisory-lock-min-tier-refused",
|
|
196
|
+
error,
|
|
197
|
+
errorType: "framework-error",
|
|
198
|
+
lockName,
|
|
199
|
+
minTier,
|
|
200
|
+
requestedBackend
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
throw error
|
|
204
|
+
}
|