velocious 1.0.709 → 1.0.710

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 (32) hide show
  1. package/README.md +1 -1
  2. package/build/configuration-types.js +17 -1
  3. package/build/configuration.js +74 -2
  4. package/build/database/advisory-lock-errors.js +59 -0
  5. package/build/database/advisory-lock-lock-server-backend.js +557 -0
  6. package/build/database/advisory-lock-runner.js +175 -2
  7. package/build/database/record/index.js +16 -2
  8. package/build/src/configuration-types.d.ts +73 -2
  9. package/build/src/configuration-types.d.ts.map +1 -1
  10. package/build/src/configuration-types.js +17 -2
  11. package/build/src/configuration.d.ts +32 -2
  12. package/build/src/configuration.d.ts.map +1 -1
  13. package/build/src/configuration.js +72 -3
  14. package/build/src/database/advisory-lock-errors.d.ts +54 -0
  15. package/build/src/database/advisory-lock-errors.d.ts.map +1 -0
  16. package/build/src/database/advisory-lock-errors.js +52 -0
  17. package/build/src/database/advisory-lock-lock-server-backend.d.ts +279 -0
  18. package/build/src/database/advisory-lock-lock-server-backend.d.ts.map +1 -0
  19. package/build/src/database/advisory-lock-lock-server-backend.js +474 -0
  20. package/build/src/database/advisory-lock-runner.d.ts +69 -1
  21. package/build/src/database/advisory-lock-runner.d.ts.map +1 -1
  22. package/build/src/database/advisory-lock-runner.js +150 -3
  23. package/build/src/database/record/index.d.ts +2 -2
  24. package/build/src/database/record/index.d.ts.map +1 -1
  25. package/build/src/database/record/index.js +3 -3
  26. package/package.json +1 -1
  27. package/src/configuration-types.js +17 -1
  28. package/src/configuration.js +74 -2
  29. package/src/database/advisory-lock-errors.js +59 -0
  30. package/src/database/advisory-lock-lock-server-backend.js +557 -0
  31. package/src/database/advisory-lock-runner.js +175 -2
  32. package/src/database/record/index.js +16 -2
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "velocious": "build/bin/velocious.js"
4
4
  },
5
5
  "name": "velocious",
6
- "version": "1.0.709",
6
+ "version": "1.0.710",
7
7
  "main": "build/index.js",
8
8
  "types": "build/index.d.ts",
9
9
  "files": [
@@ -1,5 +1,21 @@
1
1
  // @ts-check
2
2
 
3
+ /**
4
+ * Resolved lock-service configuration after environment and Beacon defaults.
5
+ * @typedef {object} LockServiceResolvedConfigType
6
+ * @property {string} [authToken] - Shared secret enforced by the host, when configured.
7
+ * @property {number} connectBackoffMs - Base backoff between connection retries.
8
+ * @property {number} connectRetries - Bounded connection retries before giving up.
9
+ * @property {number} connectTimeoutMs - Handshake timeout.
10
+ * @property {"database" | "none"} fallback - What to do when the service is unreachable.
11
+ * @property {string} host - Lock-service host.
12
+ * @property {number} leaseMs - Lease duration advertised to clients.
13
+ * @property {"beacon" | "in-process" | "off"} mode - Where the authority lives.
14
+ * @property {string} namespace - Prefix applied to every lock name.
15
+ * @property {number} port - Lock-service port.
16
+ * @property {string | null} statePath - Durable mirror for epochs and holders.
17
+ */
18
+
3
19
  /**
4
20
  * @module types
5
21
  */
@@ -866,7 +882,7 @@
866
882
  * behavior; the file backend is disabled by default and only selectable by
867
883
  * explicit configuration.
868
884
  * @typedef {object} AdvisoryLocksConfiguration
869
- * @property {{file?: {directory?: string, enabled?: boolean, leaseMs?: number, renewIntervalMs?: number}}} [backends] - Backend enablement. `file.enabled` defaults to false; enabling it is the deployment's explicit single-host declaration.
885
+ * @property {{file?: {directory?: string, enabled?: boolean, leaseMs?: number, renewIntervalMs?: number}, lockServer?: {authToken?: string, connectTimeoutMs?: number, enabled?: boolean, host?: string, leaseMs?: number, port?: number, reprobeCooldownMs?: number, url?: string}}} [backends] - Backend enablement. `file.enabled` defaults to false; enabling it is the deployment's explicit single-host declaration. `lockServer` defaults to enabled when a host/port/url or the `VELOCIOUS_LOCK_SERVER_HOST`/`_PORT` env pair is configured.
870
886
  * @property {import("./database/advisory-lock-backend-policy.js").AdvisoryLockBackendPolicyEntry[]} [policy] - Per-lock-name minimum/allowed/preferred tier rules (design §5).
871
887
  */
872
888
 
@@ -261,6 +261,12 @@ export default class VelociousConfiguration {
261
261
  * (browser/Expo, no `fs`) cannot create one at all.
262
262
  * @type {import("./database/advisory-lock-file-backend.js").default | undefined} */
263
263
  _advisoryLockFileBackend = undefined
264
+ /**
265
+ * Lazily created lock-server (tier (b)) backend; undefined when the tier is
266
+ * not configured.
267
+ * @type {import("./database/advisory-lock-lock-server-backend.js").default | undefined}
268
+ */
269
+ _advisoryLockLockServerBackend = undefined
264
270
 
265
271
  /**
266
272
  * Most recent advisory-lock acquisition per lock name — the "which backend
@@ -4205,11 +4211,21 @@ export default class VelociousConfiguration {
4205
4211
  * Full backward compatibility: an app that configures nothing gets today's
4206
4212
  * driver-native-only behavior — the file backend is **disabled by default**
4207
4213
  * and only ever selectable by explicit configuration.
4208
- * @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.
4214
+ * @returns {{backends: {file: {directory?: string, enabled: boolean, leaseMs: number, renewIntervalMs: number}, lockServer: {authToken: string, connectTimeoutMs: number, enabled: boolean, leaseMs: number, reprobeCooldownMs: number, url: string}}, policy: import("./database/advisory-lock-backend-policy.js").AdvisoryLockBackendPolicyEntry[]}} - Advisory-lock configuration with defaults applied.
4209
4215
  */
4210
4216
  getAdvisoryLockConfig() {
4211
4217
  const configured = this._advisoryLocks || {}
4212
4218
  const fileConfigured = configured.backends?.file || {}
4219
+ const lockServerConfigured = configured.backends?.lockServer || {}
4220
+ const envHost = process.env.VELOCIOUS_LOCK_SERVER_HOST
4221
+ const envPortRaw = process.env.VELOCIOUS_LOCK_SERVER_PORT
4222
+ const envPort = envPortRaw ? Number(envPortRaw) : undefined
4223
+ const lockServerHost = lockServerConfigured.host || envHost
4224
+ const lockServerPort = typeof lockServerConfigured.port === "number"
4225
+ ? lockServerConfigured.port
4226
+ : (typeof envPort === "number" && Number.isFinite(envPort) ? envPort : 3006)
4227
+ const lockServerUrl = lockServerConfigured.url
4228
+ || `ws://${lockServerHost || "127.0.0.1"}:${lockServerPort}/websocket`
4213
4229
 
4214
4230
  return {
4215
4231
  backends: {
@@ -4218,6 +4234,18 @@ export default class VelociousConfiguration {
4218
4234
  enabled: fileConfigured.enabled === true,
4219
4235
  leaseMs: typeof fileConfigured.leaseMs === "number" ? fileConfigured.leaseMs : 30_000,
4220
4236
  renewIntervalMs: typeof fileConfigured.renewIntervalMs === "number" ? fileConfigured.renewIntervalMs : 10_000
4237
+ },
4238
+ lockServer: {
4239
+ // Enabled only by explicit configuration or an explicit env pair, so
4240
+ // an app that configures nothing keeps today's driver-native behavior.
4241
+ authToken: typeof lockServerConfigured.authToken === "string" ? lockServerConfigured.authToken : "",
4242
+ connectTimeoutMs: typeof lockServerConfigured.connectTimeoutMs === "number" ? lockServerConfigured.connectTimeoutMs : 3000,
4243
+ enabled: typeof lockServerConfigured.enabled === "boolean"
4244
+ ? lockServerConfigured.enabled
4245
+ : Boolean(lockServerConfigured.url || lockServerConfigured.host || envHost || typeof lockServerConfigured.port === "number" || envPortRaw),
4246
+ leaseMs: typeof lockServerConfigured.leaseMs === "number" ? lockServerConfigured.leaseMs : 30_000,
4247
+ reprobeCooldownMs: typeof lockServerConfigured.reprobeCooldownMs === "number" ? lockServerConfigured.reprobeCooldownMs : 5000,
4248
+ url: lockServerUrl
4221
4249
  }
4222
4250
  },
4223
4251
  policy: Array.isArray(configured.policy) ? configured.policy : []
@@ -4267,6 +4295,39 @@ export default class VelociousConfiguration {
4267
4295
  return this._advisoryLockFileBackend
4268
4296
  }
4269
4297
 
4298
+ /**
4299
+ * Returns the lock-server advisory-lock backend (tier (b)) when this
4300
+ * deployment configured one, creating it on first use. Returns `undefined`
4301
+ * when the tier is not configured — callers must never invent one.
4302
+ *
4303
+ * The backend keeps a single socket per process; it holds no database
4304
+ * connection while waiting for or holding a lease.
4305
+ * @returns {Promise<import("./database/advisory-lock-lock-server-backend.js").default | undefined>} - Resolves with the configured backend, or undefined.
4306
+ */
4307
+ async getAdvisoryLockLockServerBackend() {
4308
+ const config = this.getAdvisoryLockConfig()
4309
+
4310
+ if (!config.backends.lockServer.enabled) return undefined
4311
+ if (this._advisoryLockLockServerBackend) return this._advisoryLockLockServerBackend
4312
+
4313
+ const {default: AdvisoryLockLockServerBackend} = await import("./database/advisory-lock-lock-server-backend.js")
4314
+ const backend = new AdvisoryLockLockServerBackend({
4315
+ authToken: config.backends.lockServer.authToken,
4316
+ connectTimeoutMs: config.backends.lockServer.connectTimeoutMs,
4317
+ leaseMs: config.backends.lockServer.leaseMs,
4318
+ onError: (error) => {
4319
+ this.getErrorEvents().emit("framework-error", {context: "advisory-lock-lock-server", error})
4320
+ this.getErrorEvents().emit("all-error", {context: "advisory-lock-lock-server", error, errorType: "framework-error"})
4321
+ },
4322
+ reprobeCooldownMs: config.backends.lockServer.reprobeCooldownMs,
4323
+ url: config.backends.lockServer.url
4324
+ })
4325
+
4326
+ this._advisoryLockLockServerBackend = backend
4327
+
4328
+ return backend
4329
+ }
4330
+
4270
4331
  /**
4271
4332
  * Records which backend tier served one advisory-lock acquisition so
4272
4333
  * `getAdvisoryLockBackendStatus()` can answer "which backend served this
@@ -4287,11 +4348,12 @@ export default class VelociousConfiguration {
4287
4348
  * carries fencing tokens, and the resolved policy (minimum/allowed/preferred
4288
4349
  * tiers) for the name.
4289
4350
  * @param {string} lockName - Lock name.
4290
- * @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.
4351
+ * @returns {{acquisition: {backend: import("./database/advisory-lock-backend-policy.js").AdvisoryLockTier, fencingSupported: boolean, servedAt: string} | null, lockServer: {connected: boolean, lastError: string | null, lastProbeAt: string | null, leasedNames: string[]} | null, policy: import("./database/advisory-lock-backend-policy.js").ResolvedAdvisoryLockBackendPolicy}} - Backend status for the name.
4291
4352
  */
4292
4353
  getAdvisoryLockBackendStatus(lockName) {
4293
4354
  return {
4294
4355
  acquisition: this._advisoryLockAcquisitions.get(lockName) ?? null,
4356
+ lockServer: this._advisoryLockLockServerBackend?.getStatus() ?? null,
4295
4357
  policy: this.resolveAdvisoryLockPolicy(lockName)
4296
4358
  }
4297
4359
  }
@@ -4384,6 +4446,16 @@ export default class VelociousConfiguration {
4384
4446
  closeErrors.push(error instanceof Error ? error : new Error(String(error)))
4385
4447
  }
4386
4448
 
4449
+ try {
4450
+ // Release lock-server leases explicitly first, then drop the socket:
4451
+ // the server reclaims a closed connection anyway, but an explicit
4452
+ // release hands the name to the next waiter without waiting for the
4453
+ // transport teardown.
4454
+ await this._advisoryLockLockServerBackend?.close()
4455
+ } catch (error) {
4456
+ closeErrors.push(error instanceof Error ? error : new Error(String(error)))
4457
+ }
4458
+
4387
4459
  for (const pool of Object.values(this.databasePools)) {
4388
4460
  if (!pool) continue
4389
4461
 
@@ -0,0 +1,59 @@
1
+ // @ts-check
2
+
3
+ import {AdvisoryLockBackendUnavailableError} from "./advisory-lock-backend-policy.js"
4
+
5
+ /**
6
+ * Thrown when a lock-server acquisition could not be attempted because the
7
+ * lock server is unreachable. Distinct from `AdvisoryLockTimeoutError` ("the
8
+ * server was reachable, the holder never released") so a caller can react to
9
+ * an infrastructure outage without guessing.
10
+ */
11
+ export class AdvisoryLockServiceUnavailableError extends Error {
12
+ /**
13
+ * Runs constructor.
14
+ * @param {string} message - Error message.
15
+ * @param {{lockName: string, reason: string}} args - Lock name and failure reason.
16
+ */
17
+ constructor(message, {lockName, reason}) {
18
+ super(message)
19
+ this.name = "AdvisoryLockServiceUnavailableError"
20
+ this.lockName = lockName
21
+ this.reason = reason
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Thrown after a critical section ran on the lock-server tier while its lease
27
+ * was fenced or lost (design §7/§8): the section may have overlapped another
28
+ * holder, so its result must not be trusted as exclusive.
29
+ */
30
+ export class AdvisoryLockLostError extends Error {
31
+ /**
32
+ * Runs constructor.
33
+ * @param {string} message - Error message.
34
+ * @param {{cause?: Error, currentEpoch: number | null, epoch: number | null, lockName: string}} args - Fencing details.
35
+ */
36
+ constructor(message, {cause, currentEpoch, epoch, lockName}) {
37
+ super(message)
38
+ this.name = "AdvisoryLockLostError"
39
+ this.lockName = lockName
40
+ this.epoch = epoch
41
+ this.currentEpoch = currentEpoch
42
+ this.cause = cause
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Builds the error thrown when a lock name's minimum tier cannot be served and
48
+ * no eligible tier is available (design §9, fail-closed).
49
+ * @param {{lockName: string, minTier: import("./advisory-lock-backend-policy.js").AdvisoryLockTier, reasons: string[]}} args - Unavailability details.
50
+ * @returns {AdvisoryLockBackendUnavailableError} - Error to throw.
51
+ */
52
+ export function advisoryLockBackendUnavailable(args) {
53
+ const {lockName, minTier, reasons} = args
54
+
55
+ return new AdvisoryLockBackendUnavailableError(
56
+ `No eligible advisory-lock backend is available for ${JSON.stringify(lockName)} (minimum tier ${JSON.stringify(minTier)}): ${reasons.join("; ")}`,
57
+ {lockName, minTier, reasons}
58
+ )
59
+ }