rollbridge 0.1.39 → 0.1.41

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/src/config.js CHANGED
@@ -14,7 +14,7 @@ import {pathToFileURL} from "node:url"
14
14
  * @typedef {"proxied" | "companion" | "singleton" | "service"} ProcessPolicy
15
15
  * @typedef {{backoffFactor: number, maxDelayMs: number, maxRestarts: number | undefined, windowMs: number}} RestartConfig
16
16
  * @typedef {{checkIntervalMs: number, limitBytes: number, warnBytes: number}} MemoryConfig
17
- * @typedef {{activateCommand?: string, drainCommand?: string, drainTimeoutMs: number, quietCommand?: string, stopCommand?: string}} LifecycleConfig
17
+ * @typedef {{activateCommand?: string, drainCommand?: string, drainTimeoutMs: number, quietCommand?: string, reactivateCommand?: string, stopCommand?: string}} LifecycleConfig
18
18
  * @typedef {number | "indefinite"} StopTimeoutMs
19
19
  * @typedef {"persistent" | "handoff"} ServiceDeployStrategy
20
20
  * @typedef {{cwd?: string, deployStrategy: ServiceDeployStrategy, env: Record<string, string>, gracefulStopMs: StopTimeoutMs, health?: HealthConfig, id: string, lifecycle: LifecycleConfig, memory?: MemoryConfig, nonBlockingDrain: boolean, outputLines: number, policy: ProcessPolicy, port?: PortRange, replicas: number, restart: RestartConfig, restartDelayMs: number, stopSignal: string, command: string}} ProcessConfig
@@ -67,8 +67,12 @@ export async function parseConfigFile(configPath) {
67
67
  */
68
68
  export async function loadConfig(configPath) {
69
69
  const {absolutePath, rawConfig} = await parseConfigFile(configPath)
70
+ const config = normalizeConfig(rawConfig, absolutePath)
71
+ const configDirectory = path.dirname(absolutePath)
70
72
 
71
- return normalizeConfig(rawConfig, absolutePath)
73
+ config.control.path = path.resolve(configDirectory, config.control.path)
74
+ if (config.statePath) config.statePath = path.resolve(configDirectory, config.statePath)
75
+ return config
72
76
  }
73
77
 
74
78
  /**
@@ -342,7 +346,7 @@ function normalizeLifecycle(value, key, issues) {
342
346
  if (value === undefined || value === null) return {drainTimeoutMs: 0}
343
347
 
344
348
  if (!isPlainObject(value)) {
345
- issues.push({fix: `Set ${key} to a mapping with optional activateCommand, quietCommand, drainCommand, stopCommand, and drainTimeoutMs.`, message: `${key} must be an object`})
349
+ issues.push({fix: `Set ${key} to a mapping with optional activateCommand, quietCommand, reactivateCommand, drainCommand, stopCommand, and drainTimeoutMs.`, message: `${key} must be an object`})
346
350
 
347
351
  return {drainTimeoutMs: 0}
348
352
  }
@@ -351,10 +355,11 @@ function normalizeLifecycle(value, key, issues) {
351
355
  /** @type {LifecycleConfig} */
352
356
  const lifecycle = {drainTimeoutMs: nonNegativeOrDefault(drainTimeoutMs, `${key}.drainTimeoutMs`, issues, 0, false)}
353
357
 
354
- if (value.activateCommand !== undefined) lifecycle.activateCommand = normalizeString(value.activateCommand, `${key}.activateCommand`, issues)
355
- if (value.quietCommand !== undefined) lifecycle.quietCommand = normalizeString(value.quietCommand, `${key}.quietCommand`, issues)
356
- if (value.drainCommand !== undefined) lifecycle.drainCommand = normalizeString(value.drainCommand, `${key}.drainCommand`, issues)
357
- if (value.stopCommand !== undefined) lifecycle.stopCommand = normalizeString(value.stopCommand, `${key}.stopCommand`, issues)
358
+ if (value.activateCommand !== undefined) lifecycle.activateCommand = normalizeString(value.activateCommand, `${key}.activateCommand`, issues, {nonEmpty: true})
359
+ if (value.quietCommand !== undefined) lifecycle.quietCommand = normalizeString(value.quietCommand, `${key}.quietCommand`, issues, {nonEmpty: true})
360
+ if (value.reactivateCommand !== undefined) lifecycle.reactivateCommand = normalizeString(value.reactivateCommand, `${key}.reactivateCommand`, issues, {nonEmpty: true})
361
+ if (value.drainCommand !== undefined) lifecycle.drainCommand = normalizeString(value.drainCommand, `${key}.drainCommand`, issues, {nonEmpty: true})
362
+ if (value.stopCommand !== undefined) lifecycle.stopCommand = normalizeString(value.stopCommand, `${key}.stopCommand`, issues, {nonEmpty: true})
358
363
 
359
364
  if (lifecycle.drainCommand !== undefined && lifecycle.drainTimeoutMs <= 0) {
360
365
  issues.push({fix: `Set ${key}.drainTimeoutMs to a positive number to bound ${key}.drainCommand; with 0 the drain step is skipped and the command never runs.`, message: `${key}.drainCommand requires a positive ${key}.drainTimeoutMs`})
@@ -372,6 +377,7 @@ function normalizeLifecycle(value, key, issues) {
372
377
  */
373
378
  function validateActivationLifecycle(processes, ownerRecovery, statePath, issues) {
374
379
  const activated = processes.filter((processConfig) => processConfig.lifecycle.activateCommand !== undefined)
380
+ const reactivated = processes.filter((processConfig) => processConfig.lifecycle.reactivateCommand !== undefined)
375
381
 
376
382
  if (activated.length > 1) {
377
383
  issues.push({fix: "Configure lifecycle.activateCommand on only one release-generation coordinator.", message: "Config may define at most one lifecycle.activateCommand"})
@@ -386,6 +392,23 @@ function validateActivationLifecycle(processes, ownerRecovery, statePath, issues
386
392
  }
387
393
  }
388
394
 
395
+ for (const processConfig of reactivated) {
396
+ if (processConfig.policy !== "companion" || !processConfig.nonBlockingDrain) {
397
+ issues.push({fix: `Set lifecycle.reactivateCommand only on a nonBlockingDrain companion; "${processConfig.id}" is ${processConfig.policy} with nonBlockingDrain: ${processConfig.nonBlockingDrain}.`, message: `Process "${processConfig.id}" can only set lifecycle.reactivateCommand on a nonBlockingDrain companion`})
398
+ }
399
+ if (!processConfig.lifecycle.quietCommand) {
400
+ issues.push({fix: `Add lifecycle.quietCommand to "${processConfig.id}" so worker reactivation has a paired retirement command.`, message: `Process "${processConfig.id}" lifecycle.reactivateCommand requires lifecycle.quietCommand`})
401
+ }
402
+ }
403
+
404
+ if (activated.length > 0) {
405
+ for (const processConfig of processes) {
406
+ if (processConfig.nonBlockingDrain && processConfig.lifecycle.quietCommand && !processConfig.lifecycle.reactivateCommand) {
407
+ issues.push({fix: `Add lifecycle.reactivateCommand to "${processConfig.id}" so failed candidate recovery reverses its external quiet hook.`, message: `Process "${processConfig.id}" lifecycle.quietCommand requires lifecycle.reactivateCommand during generation activation recovery`})
408
+ }
409
+ }
410
+ }
411
+
389
412
  if (activated.length > 0 && (!ownerRecovery || !statePath)) {
390
413
  issues.push({fix: "Configure statePath and ownerRecovery for durable release-generation transition recovery.", message: "lifecycle.activateCommand requires ownerRecovery and statePath"})
391
414
  }
@@ -849,7 +872,7 @@ function normalizePortRange(value, key, issues) {
849
872
  * @param {JsonValue} value - Raw value.
850
873
  * @param {string} key - Config key.
851
874
  * @param {ConfigIssue[]} issues - Issue collector.
852
- * @param {{default?: string}} [options] - Options.
875
+ * @param {{default?: string, nonEmpty?: boolean}} [options] - Options.
853
876
  * @returns {string} Normalized string, or a placeholder when invalid.
854
877
  */
855
878
  function normalizeString(value, key, issues, options = {}) {
@@ -867,6 +890,10 @@ function normalizeString(value, key, issues, options = {}) {
867
890
  return options.default ?? ""
868
891
  }
869
892
 
893
+ if (options.nonEmpty && !value.trim()) {
894
+ issues.push({fix: `Set ${key} to a non-empty command string.`, message: `${key} must not be empty`})
895
+ }
896
+
870
897
  return value
871
898
  }
872
899