unitup 0.2.0 → 0.4.0

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.
@@ -0,0 +1,187 @@
1
+ import { DrainManager } from '../core/drain-manager.js';
2
+ import { GenerationManager } from '../core/generation-manager.js';
3
+ import { IPCClient } from '../core/ipc.js';
4
+ import { DeploymentLock } from '../core/lock.js';
5
+ import { ProcessManager } from '../core/process-manager.js';
6
+ import { ReadinessChecker } from '../core/readiness.js';
7
+ import { Router } from '../core/router.js';
8
+ import { readAppMetadata } from '../utils.js';
9
+
10
+ /**
11
+ * RollbackManager provides orchestration for reverting traffic to a previous generation.
12
+ * It strictly composes the Unitup core primitives.
13
+ */
14
+ export class RollbackManager {
15
+ /**
16
+ * @param {object} [options]
17
+ * @param {import('../core/generation-manager.js').GenerationManager} [options.generationManager]
18
+ * @param {import('../core/router.js').Router} [options.router]
19
+ * @param {import('../core/readiness.js').ReadinessChecker} [options.readiness]
20
+ * @param {import('../core/drain-manager.js').DrainManager} [options.drainManager]
21
+ * @param {import('../core/process-manager.js').ProcessManager} [options.processManager]
22
+ * @param {import('../core/lock.js').DeploymentLock} [options.lock]
23
+ */
24
+ constructor(options = {}) {
25
+ this.generationManager = options.generationManager;
26
+ this.router = options.router;
27
+ this.readiness = options.readiness;
28
+ this.drainManager = options.drainManager;
29
+ this.processManager = options.processManager;
30
+ this.lock = options.lock;
31
+ }
32
+
33
+ _ensurePrimitives(serviceName, options = {}) {
34
+ if (!this.processManager) {
35
+ this.processManager = new ProcessManager();
36
+ }
37
+ if (!this.generationManager) {
38
+ this.generationManager = new GenerationManager({
39
+ processManager: this.processManager,
40
+ stateDir: options.stateDir
41
+ });
42
+ }
43
+ if (!this.router) {
44
+ this.router = new Router();
45
+ }
46
+ if (!this.readiness) {
47
+ this.readiness = new ReadinessChecker({ processManager: this.processManager });
48
+ }
49
+ if (!this.drainManager) {
50
+ this.drainManager = new DrainManager({
51
+ router: this.router,
52
+ processManager: this.processManager
53
+ });
54
+ }
55
+ if (!this.lock) {
56
+ this.lock = new DeploymentLock({
57
+ lockDir: options.lockDir,
58
+ processManager: this.processManager
59
+ });
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Rolls back a service to its previous generation.
65
+ *
66
+ * @param {string} serviceName
67
+ * @param {object} [serviceConfig]
68
+ * @param {object} [options]
69
+ * @param {string} [options.readyPath]
70
+ * @param {number} [options.drainTimeout]
71
+ * @param {function(object): void} [options.onProgress]
72
+ * @returns {Promise<object>} Rollback summary
73
+ */
74
+ async rollback(serviceName, serviceConfig = {}, options = {}) {
75
+ if (!serviceName) {
76
+ throw new Error('Service name is required for rollback.');
77
+ }
78
+
79
+ // Check if a supervisor daemon is running for this service via IPC
80
+ const ipcClient = new IPCClient(serviceName, options.ipcDir);
81
+ const isRemote = await ipcClient.isAlive();
82
+ if (isRemote) {
83
+ return ipcClient.send(
84
+ {
85
+ action: 'rollback',
86
+ options: { ...options, ...serviceConfig }
87
+ },
88
+ options.onProgress
89
+ );
90
+ }
91
+
92
+ this._ensurePrimitives(serviceName, options);
93
+
94
+ const config =
95
+ serviceConfig && (serviceConfig.command || serviceConfig.script)
96
+ ? serviceConfig
97
+ : readAppMetadata(serviceName) || serviceConfig || {};
98
+
99
+ await this.lock.acquire(serviceName);
100
+
101
+ try {
102
+ // Check if a canary generation is running: if so, abort canary and preserve active
103
+ const canary = this.generationManager.getCanary(serviceName);
104
+ if (canary) {
105
+ if (options.onProgress) {
106
+ options.onProgress({ state: 'ABORTING_CANARY', canaryGeneration: canary.id });
107
+ }
108
+ this.router.clearCanary(serviceName);
109
+ const drainTimeout = options.drainTimeout || config.deploy?.drainTimeout || 5000;
110
+ await this.drainManager.drain(canary, { timeout: drainTimeout, stopProcess: true });
111
+ this.generationManager.remove(canary, false);
112
+
113
+ const active = this.generationManager.getActive(serviceName);
114
+ return {
115
+ service: serviceName,
116
+ abortedCanary: canary.id,
117
+ activeGeneration: active ? active.id : null,
118
+ status: 'canary_aborted'
119
+ };
120
+ }
121
+
122
+ const current = this.generationManager.getActive(serviceName);
123
+ const previous = this.generationManager.getPrevious(serviceName);
124
+
125
+ if (!previous) {
126
+ throw new Error(`No previous generation available to roll back to for service "${serviceName}".`);
127
+ }
128
+
129
+ if (options.onProgress) {
130
+ options.onProgress({ state: 'CHECKING_PREVIOUS', previous: previous.id });
131
+ }
132
+
133
+ // Check if previous is alive
134
+ if (!this.processManager.isAlive(previous.pid)) {
135
+ throw new Error(
136
+ `Previous generation #${previous.id} (PID ${previous.pid}) is no longer running and cannot be rolled back to.`
137
+ );
138
+ }
139
+
140
+ // Ensure previous generation is still ready
141
+ const isReady = await this.readiness.ensure(previous, {
142
+ path: options.readyPath || config.deploy?.ready
143
+ });
144
+
145
+ if (!isReady) {
146
+ throw new Error(`Previous generation #${previous.id} failed readiness verification.`);
147
+ }
148
+
149
+ if (options.onProgress) {
150
+ options.onProgress({ state: 'SWITCHING', targetGeneration: previous.id });
151
+ }
152
+
153
+ // Switch router to previous generation
154
+ const publicPort = options.publicPort || config.port;
155
+ this.router.switchBackend(serviceName, previous, publicPort);
156
+ this.generationManager.markActive(previous);
157
+
158
+ // Drain and stop the rolled-back current generation
159
+ if (current && current.id !== previous.id) {
160
+ if (options.onProgress) {
161
+ options.onProgress({ state: 'DRAINING_CURRENT', generation: current.id });
162
+ }
163
+ this.generationManager.markDraining(current);
164
+ const drainTimeout = options.drainTimeout || config.deploy?.drainTimeout || 10000;
165
+ await this.drainManager.drain(current, { timeout: drainTimeout, stopProcess: true });
166
+ this.generationManager.remove(current, false);
167
+ }
168
+
169
+ const result = {
170
+ service: serviceName,
171
+ rolledBackFrom: current?.id ?? null,
172
+ activeGeneration: previous.id,
173
+ status: 'rolled_back'
174
+ };
175
+
176
+ if (options.onProgress) {
177
+ options.onProgress({ state: 'COMPLETE', ...result });
178
+ }
179
+
180
+ return result;
181
+ } finally {
182
+ await this.lock.release(serviceName);
183
+ }
184
+ }
185
+ }
186
+
187
+ export const defaultRollbackManager = new RollbackManager();
package/src/index.js CHANGED
@@ -1,3 +1,26 @@
1
+ import {
2
+ DeploymentLock,
3
+ DeploymentLockError,
4
+ DrainManager,
5
+ GenerationManager,
6
+ IPCClient,
7
+ IPCServer,
8
+ PortMismatchError,
9
+ ProcessManager,
10
+ ProcessStartupError,
11
+ ReadinessChecker,
12
+ ReadinessTimeoutError,
13
+ Router,
14
+ Supervisor,
15
+ findFreePort
16
+ } from './core/index.js';
17
+ import {
18
+ DeploymentManager,
19
+ DeploymentState,
20
+ RollbackManager,
21
+ defaultDeploymentManager,
22
+ defaultRollbackManager
23
+ } from './deploy/index.js';
1
24
  import { getDoctorInfo, runDoctor } from './doctor.js';
2
25
  import {
3
26
  ExecutableNotFoundError,
@@ -73,6 +96,7 @@ import {
73
96
  } from './unit.js';
74
97
  import {
75
98
  findProjectConfig,
99
+ formatDuration,
76
100
  formatMemoryBytes,
77
101
  getAppMetadataPath,
78
102
  getAppsDir,
@@ -101,6 +125,10 @@ const uninstall = (name, opts) => defaultManager.uninstall(name, opts);
101
125
  const start = (name, opts) => defaultManager.start(name, opts);
102
126
  const stop = (name, opts) => defaultManager.stop(name, opts);
103
127
  const restart = (name, opts) => defaultManager.restart(name, opts);
128
+ const deploy = (name, opts) => defaultDeploymentManager.deploy(name, {}, opts);
129
+ const promote = (name, opts) => defaultDeploymentManager.promote(name, {}, opts);
130
+ const rollback = (name, opts) => defaultRollbackManager.rollback(name, {}, opts);
131
+ const generations = (name, opts) => new GenerationManager(opts).listGenerations(name);
104
132
  const status = (name, opts) => defaultManager.status(name, opts);
105
133
  const list = (opts) => defaultManager.list(opts);
106
134
  const inspect = (name, opts) => defaultManager.inspect(name, opts);
@@ -116,6 +144,10 @@ export {
116
144
  start,
117
145
  stop,
118
146
  restart,
147
+ deploy,
148
+ promote,
149
+ rollback,
150
+ generations,
119
151
  status,
120
152
  list,
121
153
  inspect,
@@ -125,6 +157,27 @@ export {
125
157
  platform,
126
158
  ServiceManager,
127
159
  defaultManager,
160
+ // Zero-Downtime Deployment & Orchestration
161
+ DeploymentManager,
162
+ DeploymentState,
163
+ defaultDeploymentManager,
164
+ RollbackManager,
165
+ defaultRollbackManager,
166
+ // Core Runtime Primitives
167
+ ProcessManager,
168
+ GenerationManager,
169
+ Router,
170
+ ReadinessChecker,
171
+ ReadinessTimeoutError,
172
+ ProcessStartupError,
173
+ PortMismatchError,
174
+ DrainManager,
175
+ DeploymentLock,
176
+ DeploymentLockError,
177
+ Supervisor,
178
+ IPCServer,
179
+ IPCClient,
180
+ findFreePort,
128
181
  // Platform Adapters & Factory
129
182
  ServiceAdapter,
130
183
  LinuxAdapter,
@@ -179,6 +232,7 @@ export {
179
232
  validateMemorySize,
180
233
  validateDuration,
181
234
  formatMemoryBytes,
235
+ formatDuration,
182
236
  // Runtime detection & resolution
183
237
  detectRuntime,
184
238
  resolveRuntimeConfig,
@@ -13,6 +13,7 @@ import {
13
13
  deleteAppMetadata,
14
14
  formatRelativeTime,
15
15
  getAppsDir,
16
+ getUnitupBinPath,
16
17
  getUnitupDir,
17
18
  readAppMetadata,
18
19
  sanitizeServiceName,
@@ -175,7 +176,9 @@ export class MacOSAdapter extends ServiceAdapter {
175
176
  const safeName = sanitizeServiceName(config.name);
176
177
  const label = this.getLabel(safeName);
177
178
 
178
- const programArguments = [config.command, ...(config.args || [])];
179
+ const programArguments = config.deploy?.zeroDowntime
180
+ ? [process.execPath, getUnitupBinPath(), 'supervisor', safeName]
181
+ : [config.command, ...(config.args || [])];
179
182
 
180
183
  // Ensure log directory exists
181
184
  const stdoutLog = config.logs?.stdout || path.join(getUnitupDir(), 'logs', `${safeName}.log`);
@@ -1,4 +1,4 @@
1
- import { spawn, execFile } from 'node:child_process';
1
+ import { execFile, spawn } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
@@ -14,6 +14,7 @@ import {
14
14
  deleteAppMetadata,
15
15
  formatRelativeTime,
16
16
  getAppsDir,
17
+ getUnitupBinPath,
17
18
  getUnitupDir,
18
19
  readAppMetadata,
19
20
  sanitizeServiceName,
@@ -75,7 +76,9 @@ export class WindowsAdapter extends ServiceAdapter {
75
76
  const safeName = sanitizeServiceName(config.name);
76
77
  const serviceName = this.getServiceName(safeName);
77
78
  const nodeExec = process.execPath;
78
- const binPath = `"${nodeExec}" "${WINDOWS_HOST_PATH}" "${safeName}"`;
79
+ const hostScript = config.deploy?.zeroDowntime ? getUnitupBinPath() : WINDOWS_HOST_PATH;
80
+ const hostArg = config.deploy?.zeroDowntime ? `supervisor "${safeName}"` : `"${safeName}"`;
81
+ const binPath = `"${nodeExec}" "${hostScript}" ${hostArg}`;
79
82
  const startType = config.autostart !== false ? 'auto' : 'demand';
80
83
 
81
84
  return {
@@ -48,6 +48,14 @@ export function mergeConfigWithOptions(opts = {}) {
48
48
  memoryHigh: opts.memoryHigh || projectCfg?.resources?.memoryHigh || projectCfg?.memoryHigh || '',
49
49
  memoryMax: opts.memoryMax || projectCfg?.resources?.memoryMax || projectCfg?.memoryMax || '',
50
50
  memorySwapMax: opts.memorySwapMax || projectCfg?.resources?.memorySwapMax || projectCfg?.memorySwapMax || '',
51
+ port: opts.port !== undefined ? opts.port : projectCfg.port,
52
+ deploy: {
53
+ ...(projectCfg.deploy && typeof projectCfg.deploy === 'object' ? projectCfg.deploy : {}),
54
+ ...(opts.deploy && typeof opts.deploy === 'object' ? opts.deploy : {}),
55
+ ...(opts.zeroDowntime !== undefined ? { zeroDowntime: !!opts.zeroDowntime } : {}),
56
+ ...(opts.ready !== undefined ? { ready: opts.ready } : {}),
57
+ ...(opts.drain !== undefined ? { drain: opts.drain } : {})
58
+ },
51
59
  ...opts,
52
60
  env: { ...(projectCfg.env || {}), ...(opts.env || {}) },
53
61
  cwd: targetCwd
@@ -2,7 +2,13 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { ExecutableNotFoundError, InvalidServiceConfigError } from '../errors.js';
4
4
  import { detectRuntime, resolveRuntimeConfig } from '../runtimes/index.js';
5
- import { getUnitupDir, resolveEffectiveMemoryLimits, resolveWorkingDirectory, sanitizeServiceName } from '../utils.js';
5
+ import {
6
+ getUnitupDir,
7
+ parseDurationMs,
8
+ resolveEffectiveMemoryLimits,
9
+ resolveWorkingDirectory,
10
+ sanitizeServiceName
11
+ } from '../utils.js';
6
12
 
7
13
  /**
8
14
  * Resolves a binary or command name to an absolute executable path.
@@ -187,6 +193,27 @@ export async function normalizeServiceConfig(rawOpts = {}) {
187
193
  // Memory and resource limits
188
194
  const resources = resolveEffectiveMemoryLimits(rawOpts);
189
195
 
196
+ // Port and Deploy normalization
197
+ const port =
198
+ rawOpts.port !== undefined && rawOpts.port !== null && rawOpts.port !== '' ? Number(rawOpts.port) : undefined;
199
+
200
+ const rawDeploy = rawOpts.deploy && typeof rawOpts.deploy === 'object' ? rawOpts.deploy : {};
201
+ const zeroDowntime = !!(rawOpts.deploy === true || rawDeploy.zeroDowntime || rawOpts.zeroDowntime);
202
+ const readyPath =
203
+ typeof rawDeploy.ready === 'string'
204
+ ? rawDeploy.ready
205
+ : typeof rawOpts.ready === 'string'
206
+ ? rawOpts.ready
207
+ : undefined;
208
+ const drainRaw = rawDeploy.drain !== undefined ? rawDeploy.drain : rawOpts.drain;
209
+ const drainTimeout = drainRaw !== undefined ? parseDurationMs(drainRaw) : 10000;
210
+ const deploy = {
211
+ zeroDowntime,
212
+ ready: readyPath,
213
+ drain: drainRaw,
214
+ drainTimeout
215
+ };
216
+
190
217
  return {
191
218
  name,
192
219
  displayName,
@@ -196,6 +223,8 @@ export async function normalizeServiceConfig(rawOpts = {}) {
196
223
  args,
197
224
  script: rawOpts.script ? path.resolve(cwd, rawOpts.script) : undefined,
198
225
  cwd,
226
+ port,
227
+ deploy,
199
228
  env,
200
229
  envFile: rawOpts.envFile ? path.resolve(cwd, rawOpts.envFile) : undefined,
201
230
  autostart,
package/src/unit.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  getServiceNameFromUnit,
10
10
  getTimerFilename,
11
11
  getUnitFilename,
12
+ getUnitupBinPath,
12
13
  resolveAbsolutePath,
13
14
  resolveEffectiveMemoryLimits,
14
15
  resolveWorkingDirectory,
@@ -73,7 +74,10 @@ export function generateUnitContent(opts) {
73
74
  let commandExec = '';
74
75
  let execArgs = [];
75
76
 
76
- if (opts.command) {
77
+ if (opts.deploy?.zeroDowntime) {
78
+ commandExec = process.execPath;
79
+ execArgs = [getUnitupBinPath(), 'supervisor', safeName];
80
+ } else if (opts.command) {
77
81
  commandExec = resolveAbsolutePath(opts.command);
78
82
  execArgs = Array.isArray(opts.args) ? [...opts.args] : [];
79
83
  } else if (opts.script) {
@@ -95,6 +99,10 @@ export function generateUnitContent(opts) {
95
99
  .filter((p, i, self) => p && self.indexOf(p) === i)
96
100
  .join(':');
97
101
 
102
+ const logLevelMax = opts.logLevelMax || 'info';
103
+ const logRateLimitIntervalSec = opts.logRateLimitIntervalSec || '10s';
104
+ const logRateLimitBurst = opts.logRateLimitBurst !== undefined ? opts.logRateLimitBurst : 200;
105
+
98
106
  const serviceLines = [
99
107
  '[Unit]',
100
108
  `Description=unitup service: ${safeName}`,
@@ -108,7 +116,10 @@ export function generateUnitContent(opts) {
108
116
  `Restart=${restartPolicy}`,
109
117
  'RestartSec=3',
110
118
  'StandardOutput=journal',
111
- 'StandardError=journal'
119
+ 'StandardError=journal',
120
+ `LogLevelMax=${logLevelMax}`,
121
+ `LogRateLimitIntervalSec=${logRateLimitIntervalSec}`,
122
+ `LogRateLimitBurst=${logRateLimitBurst}`
112
123
  ];
113
124
 
114
125
  const effectiveLimits = resolveEffectiveMemoryLimits(opts);
@@ -169,6 +180,8 @@ export function writeUnitFile(opts) {
169
180
  script: opts.script,
170
181
  cwd: opts.cwd,
171
182
  node: opts.nodePath,
183
+ port: opts.port,
184
+ deploy: opts.deploy,
172
185
  memoryHigh: opts.memoryHigh || opts.resources?.memoryHigh,
173
186
  memoryMax: opts.memoryMax || opts.resources?.memoryMax,
174
187
  memorySwapMax: opts.memorySwapMax || opts.resources?.memorySwapMax,
@@ -259,6 +272,12 @@ export function parseUnitContent(content) {
259
272
  result.memoryMax = trimmed.slice(10).trim();
260
273
  } else if (trimmed.startsWith('MemorySwapMax=')) {
261
274
  result.memorySwapMax = trimmed.slice(14).trim();
275
+ } else if (trimmed.startsWith('LogLevelMax=')) {
276
+ result.logLevelMax = trimmed.slice(12).trim();
277
+ } else if (trimmed.startsWith('LogRateLimitIntervalSec=')) {
278
+ result.logRateLimitIntervalSec = trimmed.slice(24).trim();
279
+ } else if (trimmed.startsWith('LogRateLimitBurst=')) {
280
+ result.logRateLimitBurst = Number.parseInt(trimmed.slice(18).trim(), 10);
262
281
  }
263
282
  }
264
283
 
@@ -316,6 +335,10 @@ export function generateScheduleServiceContent(opts) {
316
335
  .filter((p, i, self) => p && self.indexOf(p) === i)
317
336
  .join(':');
318
337
 
338
+ const logLevelMax = opts.logLevelMax || 'info';
339
+ const logRateLimitIntervalSec = opts.logRateLimitIntervalSec || '10s';
340
+ const logRateLimitBurst = opts.logRateLimitBurst !== undefined ? opts.logRateLimitBurst : 200;
341
+
319
342
  const serviceLines = [
320
343
  '[Unit]',
321
344
  `Description=unitup scheduled task: ${safeName}`,
@@ -327,7 +350,10 @@ export function generateScheduleServiceContent(opts) {
327
350
  `WorkingDirectory=${cwd}`,
328
351
  `ExecStart=${execStartLine}`,
329
352
  'StandardOutput=journal',
330
- 'StandardError=journal'
353
+ 'StandardError=journal',
354
+ `LogLevelMax=${logLevelMax}`,
355
+ `LogRateLimitIntervalSec=${logRateLimitIntervalSec}`,
356
+ `LogRateLimitBurst=${logRateLimitBurst}`
331
357
  ];
332
358
 
333
359
  const effectiveLimits = resolveEffectiveMemoryLimits(opts);
package/src/utils.js CHANGED
@@ -1,6 +1,16 @@
1
1
  import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ /**
7
+ * Returns the absolute path to the unitup CLI binary.
8
+ * @returns {string}
9
+ */
10
+ export function getUnitupBinPath() {
11
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
12
+ return path.resolve(currentDir, '../bin/unitup.js');
13
+ }
4
14
 
5
15
  /**
6
16
  * Returns the base unitup config directory (~/.config/unitup).
@@ -170,6 +180,8 @@ export function saveAppMetadata(meta) {
170
180
  args,
171
181
  cwd,
172
182
  group: meta.group || 'default',
183
+ ...(meta.port !== undefined ? { port: meta.port } : {}),
184
+ ...(meta.deploy ? { deploy: meta.deploy } : {}),
173
185
  // Optional resources section
174
186
  ...(Object.keys(resources).length > 0 ? { resources } : {}),
175
187
  // Backward compatibility fields
@@ -415,6 +427,32 @@ export function formatRelativeTime(timestamp) {
415
427
  return `${days} day${days === 1 ? '' : 's'} ago`;
416
428
  }
417
429
 
430
+ /**
431
+ * Formats duration in milliseconds into a concise human-readable string (e.g. "23s", "2 mins", "3 hours", "2 days").
432
+ *
433
+ * @param {number} ms
434
+ * @returns {string}
435
+ */
436
+ export function formatDuration(ms) {
437
+ if (typeof ms !== 'number' || isNaN(ms) || ms < 0) {
438
+ return '-';
439
+ }
440
+ const seconds = Math.floor(ms / 1000);
441
+ if (seconds < 60) {
442
+ return `${seconds}s`;
443
+ }
444
+ const minutes = Math.floor(seconds / 60);
445
+ if (minutes < 60) {
446
+ return `${minutes} min${minutes === 1 ? '' : 's'}`;
447
+ }
448
+ const hours = Math.floor(minutes / 60);
449
+ if (hours < 24) {
450
+ return `${hours} hour${hours === 1 ? '' : 's'}`;
451
+ }
452
+ const days = Math.floor(hours / 24);
453
+ return `${days} day${days === 1 ? '' : 's'}`;
454
+ }
455
+
418
456
  /**
419
457
  * Formats data rows into a text table.
420
458
  *
@@ -603,6 +641,48 @@ export function validateDuration(val, paramName = 'Duration') {
603
641
  return str;
604
642
  }
605
643
 
644
+ /**
645
+ * Parses a duration string (e.g. '30s', '500ms', '1m') into milliseconds.
646
+ * @param {string|number} val
647
+ * @param {number} [defaultMs=10000]
648
+ * @returns {number}
649
+ */
650
+ export function parseDurationMs(val, defaultMs = 10000) {
651
+ if (typeof val === 'number') return val;
652
+ if (!val || typeof val !== 'string') return defaultMs;
653
+ const str = val.trim().toLowerCase();
654
+ const match = str.match(/^(\d+(?:\.\d+)?)\s*(ms|msec|s|sec|seconds?|m|min|minutes?|h|hr|hours?|d|day|days?)?$/);
655
+ if (!match) return defaultMs;
656
+ const num = Number.parseFloat(match[1]);
657
+ const unit = match[2] || 's';
658
+ switch (unit) {
659
+ case 'ms':
660
+ case 'msec':
661
+ return num;
662
+ case 's':
663
+ case 'sec':
664
+ case 'second':
665
+ case 'seconds':
666
+ return num * 1000;
667
+ case 'm':
668
+ case 'min':
669
+ case 'minute':
670
+ case 'minutes':
671
+ return num * 60 * 1000;
672
+ case 'h':
673
+ case 'hr':
674
+ case 'hour':
675
+ case 'hours':
676
+ return num * 60 * 60 * 1000;
677
+ case 'd':
678
+ case 'day':
679
+ case 'days':
680
+ return num * 24 * 60 * 60 * 1000;
681
+ default:
682
+ return num * 1000;
683
+ }
684
+ }
685
+
606
686
  /**
607
687
  * Formats a future date/timestamp into relative human readable string (e.g. "in 12 minutes", "tomorrow").
608
688
  * @param {string|number|Date} dateVal
package/SECURITY.md DELETED
@@ -1,21 +0,0 @@
1
- # Security Policy
2
-
3
- ## Supported Versions
4
-
5
- Use this section to tell people about which versions of your project are
6
- currently being supported with security updates.
7
-
8
- | Version | Supported |
9
- | ------- | ------------------ |
10
- | 5.1.x | :white_check_mark: |
11
- | 5.0.x | :x: |
12
- | 4.0.x | :white_check_mark: |
13
- | < 4.0 | :x: |
14
-
15
- ## Reporting a Vulnerability
16
-
17
- Use this section to tell people how to report a vulnerability.
18
-
19
- Tell them where to go, how often they can expect to get an update on a
20
- reported vulnerability, what to expect if the vulnerability is accepted or
21
- declined, etc.
package/biome.json DELETED
@@ -1,44 +0,0 @@
1
- {
2
- "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3
- "vcs": {
4
- "enabled": true,
5
- "clientKind": "git",
6
- "useIgnoreFile": true
7
- },
8
- "files": {
9
- "ignoreUnknown": true,
10
- "ignore": ["node_modules", "dist", "coverage"]
11
- },
12
- "formatter": {
13
- "enabled": true,
14
- "indentStyle": "space",
15
- "indentWidth": 2,
16
- "lineWidth": 120
17
- },
18
- "organizeImports": {
19
- "enabled": true
20
- },
21
- "linter": {
22
- "enabled": true,
23
- "rules": {
24
- "recommended": true,
25
- "style": {
26
- "noUnusedTemplateLiteral": "off"
27
- },
28
- "complexity": {
29
- "noForEach": "off"
30
- },
31
- "suspicious": {
32
- "noEmptyBlockStatements": "off"
33
- }
34
- }
35
- },
36
- "javascript": {
37
- "formatter": {
38
- "quoteStyle": "single",
39
- "jsxQuoteStyle": "double",
40
- "trailingCommas": "none",
41
- "semicolons": "always"
42
- }
43
- }
44
- }