lifecycleion 0.0.2 → 0.0.4
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 +1 -1
- package/dist/lib/lifecycle-manager/index.cjs +143 -5
- package/dist/lib/lifecycle-manager/index.cjs.map +1 -1
- package/dist/lib/lifecycle-manager/index.d.cts +14 -1
- package/dist/lib/lifecycle-manager/index.d.ts +14 -1
- package/dist/lib/lifecycle-manager/index.js +143 -5
- package/dist/lib/lifecycle-manager/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -900,6 +900,9 @@ var EventEmitterProtected = class {
|
|
|
900
900
|
}
|
|
901
901
|
};
|
|
902
902
|
|
|
903
|
+
// src/lib/lifecycle-manager/lifecycle-manager.ts
|
|
904
|
+
var import_ulid2 = require("ulid");
|
|
905
|
+
|
|
903
906
|
// src/lib/lifecycle-manager/component-lifecycle.ts
|
|
904
907
|
var ComponentLifecycle = class {
|
|
905
908
|
manager;
|
|
@@ -1965,6 +1968,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
1965
1968
|
messageTimeoutMS;
|
|
1966
1969
|
startupTimeoutMS;
|
|
1967
1970
|
shutdownOptions;
|
|
1971
|
+
attachSignalsBeforeStartup;
|
|
1968
1972
|
attachSignalsOnStart;
|
|
1969
1973
|
detachSignalsOnStop;
|
|
1970
1974
|
// Component management
|
|
@@ -1975,10 +1979,15 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
1975
1979
|
// State tracking for individual components
|
|
1976
1980
|
componentTimestamps = /* @__PURE__ */ new Map();
|
|
1977
1981
|
componentErrors = /* @__PURE__ */ new Map();
|
|
1982
|
+
componentStartAttemptTokens = /* @__PURE__ */ new Map();
|
|
1978
1983
|
// State flags
|
|
1979
1984
|
isStarting = false;
|
|
1980
1985
|
isStarted = false;
|
|
1981
1986
|
isShuttingDown = false;
|
|
1987
|
+
// Unique token used to detect shutdowns that happened during async start().
|
|
1988
|
+
shutdownToken = (0, import_ulid2.ulid)();
|
|
1989
|
+
// Resolver for the first logger.exit() deferred during an already-running shutdown.
|
|
1990
|
+
pendingLoggerExitResolve = null;
|
|
1982
1991
|
shutdownMethod = null;
|
|
1983
1992
|
lastShutdownResult = null;
|
|
1984
1993
|
// Signal management
|
|
@@ -2004,6 +2013,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2004
2013
|
haltOnStall: true,
|
|
2005
2014
|
...options.shutdownOptions
|
|
2006
2015
|
};
|
|
2016
|
+
this.attachSignalsBeforeStartup = options.attachSignalsBeforeStartup ?? false;
|
|
2007
2017
|
this.attachSignalsOnStart = options.attachSignalsOnStart ?? false;
|
|
2008
2018
|
this.detachSignalsOnStop = options.detachSignalsOnStop ?? false;
|
|
2009
2019
|
this.onReloadRequested = options.onReloadRequested;
|
|
@@ -2167,6 +2177,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2167
2177
|
this.componentStates.delete(name);
|
|
2168
2178
|
this.componentTimestamps.delete(name);
|
|
2169
2179
|
this.componentErrors.delete(name);
|
|
2180
|
+
this.componentStartAttemptTokens.delete(name);
|
|
2170
2181
|
this.stalledComponents.delete(name);
|
|
2171
2182
|
this.runningComponents.delete(name);
|
|
2172
2183
|
this.updateStartedFlag();
|
|
@@ -2575,6 +2586,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2575
2586
|
this.isStarting = true;
|
|
2576
2587
|
this.shutdownMethod = null;
|
|
2577
2588
|
this.lastShutdownResult = null;
|
|
2589
|
+
const didAutoAttachSignalsForBulkStartup = this.attachSignalsBeforeStartup ? this.autoAttachSignals("bulk startup") : false;
|
|
2578
2590
|
this.logger.info("Starting all components");
|
|
2579
2591
|
const effectiveTimeout = options?.timeoutMS ?? this.startupTimeoutMS;
|
|
2580
2592
|
let hasTimedOut = false;
|
|
@@ -2688,6 +2700,18 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2688
2700
|
startedComponents.push(name);
|
|
2689
2701
|
} else if (result.code === "component_already_running") {
|
|
2690
2702
|
startedComponents.push(name);
|
|
2703
|
+
} else if (result.code === "shutdown_in_progress") {
|
|
2704
|
+
await this.rollbackStartup(startedComponents);
|
|
2705
|
+
return {
|
|
2706
|
+
success: false,
|
|
2707
|
+
startedComponents: [],
|
|
2708
|
+
failedOptionalComponents,
|
|
2709
|
+
skippedDueToDependency: Array.from(skippedDueToDependency),
|
|
2710
|
+
reason: result.reason || "Shutdown triggered during startup",
|
|
2711
|
+
code: "shutdown_in_progress",
|
|
2712
|
+
error: result.error,
|
|
2713
|
+
durationMS: Date.now() - startTime
|
|
2714
|
+
};
|
|
2691
2715
|
} else {
|
|
2692
2716
|
if (component.isOptional()) {
|
|
2693
2717
|
this.logger.entity(name).warn("Optional component failed to start, continuing", {
|
|
@@ -2776,6 +2800,9 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2776
2800
|
if (timeoutHandle) {
|
|
2777
2801
|
clearTimeout(timeoutHandle);
|
|
2778
2802
|
}
|
|
2803
|
+
if (didAutoAttachSignalsForBulkStartup) {
|
|
2804
|
+
this.autoDetachSignalsIfIdle("failed bulk startup");
|
|
2805
|
+
}
|
|
2779
2806
|
this.isStarting = false;
|
|
2780
2807
|
}
|
|
2781
2808
|
}
|
|
@@ -3020,6 +3047,17 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3020
3047
|
this.rootLogger.setBeforeExitCallback(
|
|
3021
3048
|
async (exitCode, isFirstExit) => {
|
|
3022
3049
|
if (this.isShuttingDown) {
|
|
3050
|
+
if (isFirstExit && this.pendingLoggerExitResolve === null) {
|
|
3051
|
+
this.logger.debug(
|
|
3052
|
+
"Logger exit called during shutdown, waiting...",
|
|
3053
|
+
{
|
|
3054
|
+
params: { exitCode }
|
|
3055
|
+
}
|
|
3056
|
+
);
|
|
3057
|
+
return await new Promise((resolve) => {
|
|
3058
|
+
this.pendingLoggerExitResolve = resolve;
|
|
3059
|
+
});
|
|
3060
|
+
}
|
|
3023
3061
|
this.logger.debug("Logger exit called during shutdown, waiting...", {
|
|
3024
3062
|
params: { exitCode }
|
|
3025
3063
|
});
|
|
@@ -3843,6 +3881,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3843
3881
|
stoppedAt: null
|
|
3844
3882
|
});
|
|
3845
3883
|
this.componentErrors.set(componentName, null);
|
|
3884
|
+
this.componentStartAttemptTokens.set(componentName, (0, import_ulid2.ulid)());
|
|
3846
3885
|
const isManualPositionRespected = this.isManualPositionRespected({
|
|
3847
3886
|
componentName,
|
|
3848
3887
|
position,
|
|
@@ -3994,6 +4033,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3994
4033
|
};
|
|
3995
4034
|
}
|
|
3996
4035
|
this.isShuttingDown = true;
|
|
4036
|
+
this.shutdownToken = (0, import_ulid2.ulid)();
|
|
3997
4037
|
this.shutdownMethod = method;
|
|
3998
4038
|
const isDuringStartup = this.isStarting;
|
|
3999
4039
|
this.logger.info("Stopping all components", { params: { method } });
|
|
@@ -4123,7 +4163,19 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4123
4163
|
}
|
|
4124
4164
|
this.isShuttingDown = false;
|
|
4125
4165
|
this.updateStartedFlag();
|
|
4166
|
+
this.finalizePendingLoggerExit();
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
4169
|
+
/**
|
|
4170
|
+
* Release a deferred logger.exit() request after shutdown fully settles.
|
|
4171
|
+
*/
|
|
4172
|
+
finalizePendingLoggerExit() {
|
|
4173
|
+
if (this.pendingLoggerExitResolve === null || this.isShuttingDown) {
|
|
4174
|
+
return;
|
|
4126
4175
|
}
|
|
4176
|
+
const resolve = this.pendingLoggerExitResolve;
|
|
4177
|
+
this.pendingLoggerExitResolve = null;
|
|
4178
|
+
resolve({ action: "proceed" });
|
|
4127
4179
|
}
|
|
4128
4180
|
/**
|
|
4129
4181
|
* Retry shutdown for a stalled component.
|
|
@@ -4264,6 +4316,10 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4264
4316
|
this.logger.entity(name).info("Starting component");
|
|
4265
4317
|
this.lifecycleEvents.componentStarting(name);
|
|
4266
4318
|
const timeoutMS = component.startupTimeoutMS;
|
|
4319
|
+
const startAttemptToken = (0, import_ulid2.ulid)();
|
|
4320
|
+
this.componentStartAttemptTokens.set(name, startAttemptToken);
|
|
4321
|
+
const shutdownTokenAtStart = this.shutdownToken;
|
|
4322
|
+
const didAutoAttachSignalsForComponentStartup = this.attachSignalsBeforeStartup ? this.autoAttachSignals("component startup") : false;
|
|
4267
4323
|
let timeoutHandle;
|
|
4268
4324
|
try {
|
|
4269
4325
|
const startPromise = component.start();
|
|
@@ -4278,6 +4334,13 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4278
4334
|
params: { error }
|
|
4279
4335
|
});
|
|
4280
4336
|
}
|
|
4337
|
+
} else {
|
|
4338
|
+
this.monitorLateStartupCompletion(
|
|
4339
|
+
name,
|
|
4340
|
+
component,
|
|
4341
|
+
startPromise,
|
|
4342
|
+
startAttemptToken
|
|
4343
|
+
);
|
|
4281
4344
|
}
|
|
4282
4345
|
Promise.resolve(startPromise).catch(() => {
|
|
4283
4346
|
});
|
|
@@ -4293,15 +4356,36 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4293
4356
|
} else {
|
|
4294
4357
|
await startPromise;
|
|
4295
4358
|
}
|
|
4359
|
+
if (this.isShuttingDown || shutdownTokenAtStart !== this.shutdownToken) {
|
|
4360
|
+
this.componentStates.set(name, "running");
|
|
4361
|
+
this.runningComponents.add(name);
|
|
4362
|
+
this.stalledComponents.delete(name);
|
|
4363
|
+
this.updateStartedFlag();
|
|
4364
|
+
const timestamps2 = this.componentTimestamps.get(name) ?? {
|
|
4365
|
+
startedAt: null,
|
|
4366
|
+
stoppedAt: null
|
|
4367
|
+
};
|
|
4368
|
+
timestamps2.startedAt = Date.now();
|
|
4369
|
+
this.componentTimestamps.set(name, timestamps2);
|
|
4370
|
+
this.logger.entity(name).warn(
|
|
4371
|
+
"Component finished starting after shutdown began, stopping immediately"
|
|
4372
|
+
);
|
|
4373
|
+
const stopResult = await this.stopComponentInternal(name);
|
|
4374
|
+
return {
|
|
4375
|
+
success: false,
|
|
4376
|
+
componentName: name,
|
|
4377
|
+
reason: "Shutdown triggered during component startup",
|
|
4378
|
+
code: "shutdown_in_progress",
|
|
4379
|
+
error: stopResult.error,
|
|
4380
|
+
status: this.getComponentStatus(name)
|
|
4381
|
+
};
|
|
4382
|
+
}
|
|
4296
4383
|
this.componentStates.set(name, "running");
|
|
4297
4384
|
this.runningComponents.add(name);
|
|
4298
4385
|
this.stalledComponents.delete(name);
|
|
4299
4386
|
this.updateStartedFlag();
|
|
4300
|
-
if (this.attachSignalsOnStart && this.runningComponents.size === 1
|
|
4301
|
-
this.
|
|
4302
|
-
"Auto-attaching process signals on first component start"
|
|
4303
|
-
);
|
|
4304
|
-
this.attachSignals();
|
|
4387
|
+
if (this.attachSignalsOnStart && this.runningComponents.size === 1) {
|
|
4388
|
+
this.autoAttachSignals("first component start");
|
|
4305
4389
|
}
|
|
4306
4390
|
const timestamps = this.componentTimestamps.get(name) ?? {
|
|
4307
4391
|
startedAt: null,
|
|
@@ -4350,6 +4434,9 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4350
4434
|
if (timeoutHandle) {
|
|
4351
4435
|
clearTimeout(timeoutHandle);
|
|
4352
4436
|
}
|
|
4437
|
+
if (didAutoAttachSignalsForComponentStartup) {
|
|
4438
|
+
this.autoDetachSignalsIfIdle("failed component startup");
|
|
4439
|
+
}
|
|
4353
4440
|
}
|
|
4354
4441
|
}
|
|
4355
4442
|
/**
|
|
@@ -4846,6 +4933,57 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4846
4933
|
}
|
|
4847
4934
|
this.logger.info("Rollback completed");
|
|
4848
4935
|
}
|
|
4936
|
+
autoAttachSignals(trigger) {
|
|
4937
|
+
if (this.processSignalManager?.getStatus().isAttached) {
|
|
4938
|
+
return false;
|
|
4939
|
+
}
|
|
4940
|
+
this.logger.info(`Auto-attaching process signals on ${trigger}`);
|
|
4941
|
+
this.attachSignals();
|
|
4942
|
+
return true;
|
|
4943
|
+
}
|
|
4944
|
+
autoDetachSignalsIfIdle(trigger) {
|
|
4945
|
+
if (!this.detachSignalsOnStop || this.runningComponents.size > 0 || !this.processSignalManager?.getStatus().isAttached) {
|
|
4946
|
+
return;
|
|
4947
|
+
}
|
|
4948
|
+
this.logger.info(`Auto-detaching process signals after ${trigger}`);
|
|
4949
|
+
this.detachSignals();
|
|
4950
|
+
}
|
|
4951
|
+
monitorLateStartupCompletion(name, component, startPromise, startAttemptToken) {
|
|
4952
|
+
this.logger.entity(name).warn(
|
|
4953
|
+
"Startup timed out without onStartupAborted, stopping component if startup completes later"
|
|
4954
|
+
);
|
|
4955
|
+
Promise.resolve(startPromise).then(async () => {
|
|
4956
|
+
const timeoutState = this.componentStates.get(name);
|
|
4957
|
+
if (this.getComponent(name) !== component || this.componentStartAttemptTokens.get(name) !== startAttemptToken || this.isComponentRunning(name) || timeoutState !== "starting-timed-out" && timeoutState !== "failed") {
|
|
4958
|
+
return;
|
|
4959
|
+
}
|
|
4960
|
+
this.componentStates.set(name, "running");
|
|
4961
|
+
this.runningComponents.add(name);
|
|
4962
|
+
this.stalledComponents.delete(name);
|
|
4963
|
+
this.updateStartedFlag();
|
|
4964
|
+
const timestamps = this.componentTimestamps.get(name) ?? {
|
|
4965
|
+
startedAt: null,
|
|
4966
|
+
stoppedAt: null
|
|
4967
|
+
};
|
|
4968
|
+
timestamps.startedAt = Date.now();
|
|
4969
|
+
this.componentTimestamps.set(name, timestamps);
|
|
4970
|
+
this.logger.entity(name).warn(
|
|
4971
|
+
"Component completed startup after timeout, stopping automatically"
|
|
4972
|
+
);
|
|
4973
|
+
const stopResult = await this.stopComponentInternal(name);
|
|
4974
|
+
if (!stopResult.success) {
|
|
4975
|
+
this.logger.entity(name).warn("Automatic stop after startup timeout failed", {
|
|
4976
|
+
params: {
|
|
4977
|
+
error: stopResult.error,
|
|
4978
|
+
code: stopResult.code
|
|
4979
|
+
}
|
|
4980
|
+
});
|
|
4981
|
+
return;
|
|
4982
|
+
}
|
|
4983
|
+
this.componentStates.set(name, timeoutState);
|
|
4984
|
+
}).catch(() => {
|
|
4985
|
+
});
|
|
4986
|
+
}
|
|
4849
4987
|
/**
|
|
4850
4988
|
* Safe emit wrapper - prevents event handler errors from breaking lifecycle
|
|
4851
4989
|
*/
|