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
|
@@ -697,7 +697,9 @@ interface LifecycleManagerOptions {
|
|
|
697
697
|
shutdownWarningTimeoutMS?: number;
|
|
698
698
|
/** Default message timeout in ms (default: 5000, 0 = disabled) */
|
|
699
699
|
messageTimeoutMS?: number;
|
|
700
|
-
/** Auto-attach signals
|
|
700
|
+
/** Auto-attach signals before startAllComponents()/startComponent() begins work, even if startup later fails (default: false) */
|
|
701
|
+
attachSignalsBeforeStartup?: boolean;
|
|
702
|
+
/** Auto-attach signals when the first component successfully starts (default: false) */
|
|
701
703
|
attachSignalsOnStart?: boolean;
|
|
702
704
|
/** Auto-detach signals when last component stops (default: false) */
|
|
703
705
|
detachSignalsOnStop?: boolean;
|
|
@@ -1003,6 +1005,7 @@ declare class LifecycleManager extends EventEmitterProtected implements Lifecycl
|
|
|
1003
1005
|
private readonly messageTimeoutMS;
|
|
1004
1006
|
private readonly startupTimeoutMS;
|
|
1005
1007
|
private readonly shutdownOptions?;
|
|
1008
|
+
private readonly attachSignalsBeforeStartup;
|
|
1006
1009
|
private readonly attachSignalsOnStart;
|
|
1007
1010
|
private readonly detachSignalsOnStop;
|
|
1008
1011
|
private components;
|
|
@@ -1011,9 +1014,12 @@ declare class LifecycleManager extends EventEmitterProtected implements Lifecycl
|
|
|
1011
1014
|
private stalledComponents;
|
|
1012
1015
|
private componentTimestamps;
|
|
1013
1016
|
private componentErrors;
|
|
1017
|
+
private componentStartAttemptTokens;
|
|
1014
1018
|
private isStarting;
|
|
1015
1019
|
private isStarted;
|
|
1016
1020
|
private isShuttingDown;
|
|
1021
|
+
private shutdownToken;
|
|
1022
|
+
private pendingLoggerExitResolve;
|
|
1017
1023
|
private shutdownMethod;
|
|
1018
1024
|
private lastShutdownResult;
|
|
1019
1025
|
private processSignalManager;
|
|
@@ -1340,6 +1346,10 @@ declare class LifecycleManager extends EventEmitterProtected implements Lifecycl
|
|
|
1340
1346
|
*/
|
|
1341
1347
|
private registerComponentInternal;
|
|
1342
1348
|
private stopAllComponentsInternal;
|
|
1349
|
+
/**
|
|
1350
|
+
* Release a deferred logger.exit() request after shutdown fully settles.
|
|
1351
|
+
*/
|
|
1352
|
+
private finalizePendingLoggerExit;
|
|
1343
1353
|
/**
|
|
1344
1354
|
* Retry shutdown for a stalled component.
|
|
1345
1355
|
* Attempts the force phase directly to avoid re-running a failing stop().
|
|
@@ -1409,6 +1419,9 @@ declare class LifecycleManager extends EventEmitterProtected implements Lifecycl
|
|
|
1409
1419
|
* Used when a required component fails to start during startAllComponents()
|
|
1410
1420
|
*/
|
|
1411
1421
|
private rollbackStartup;
|
|
1422
|
+
private autoAttachSignals;
|
|
1423
|
+
private autoDetachSignalsIfIdle;
|
|
1424
|
+
private monitorLateStartupCompletion;
|
|
1412
1425
|
/**
|
|
1413
1426
|
* Safe emit wrapper - prevents event handler errors from breaking lifecycle
|
|
1414
1427
|
*/
|
|
@@ -697,7 +697,9 @@ interface LifecycleManagerOptions {
|
|
|
697
697
|
shutdownWarningTimeoutMS?: number;
|
|
698
698
|
/** Default message timeout in ms (default: 5000, 0 = disabled) */
|
|
699
699
|
messageTimeoutMS?: number;
|
|
700
|
-
/** Auto-attach signals
|
|
700
|
+
/** Auto-attach signals before startAllComponents()/startComponent() begins work, even if startup later fails (default: false) */
|
|
701
|
+
attachSignalsBeforeStartup?: boolean;
|
|
702
|
+
/** Auto-attach signals when the first component successfully starts (default: false) */
|
|
701
703
|
attachSignalsOnStart?: boolean;
|
|
702
704
|
/** Auto-detach signals when last component stops (default: false) */
|
|
703
705
|
detachSignalsOnStop?: boolean;
|
|
@@ -1003,6 +1005,7 @@ declare class LifecycleManager extends EventEmitterProtected implements Lifecycl
|
|
|
1003
1005
|
private readonly messageTimeoutMS;
|
|
1004
1006
|
private readonly startupTimeoutMS;
|
|
1005
1007
|
private readonly shutdownOptions?;
|
|
1008
|
+
private readonly attachSignalsBeforeStartup;
|
|
1006
1009
|
private readonly attachSignalsOnStart;
|
|
1007
1010
|
private readonly detachSignalsOnStop;
|
|
1008
1011
|
private components;
|
|
@@ -1011,9 +1014,12 @@ declare class LifecycleManager extends EventEmitterProtected implements Lifecycl
|
|
|
1011
1014
|
private stalledComponents;
|
|
1012
1015
|
private componentTimestamps;
|
|
1013
1016
|
private componentErrors;
|
|
1017
|
+
private componentStartAttemptTokens;
|
|
1014
1018
|
private isStarting;
|
|
1015
1019
|
private isStarted;
|
|
1016
1020
|
private isShuttingDown;
|
|
1021
|
+
private shutdownToken;
|
|
1022
|
+
private pendingLoggerExitResolve;
|
|
1017
1023
|
private shutdownMethod;
|
|
1018
1024
|
private lastShutdownResult;
|
|
1019
1025
|
private processSignalManager;
|
|
@@ -1340,6 +1346,10 @@ declare class LifecycleManager extends EventEmitterProtected implements Lifecycl
|
|
|
1340
1346
|
*/
|
|
1341
1347
|
private registerComponentInternal;
|
|
1342
1348
|
private stopAllComponentsInternal;
|
|
1349
|
+
/**
|
|
1350
|
+
* Release a deferred logger.exit() request after shutdown fully settles.
|
|
1351
|
+
*/
|
|
1352
|
+
private finalizePendingLoggerExit;
|
|
1343
1353
|
/**
|
|
1344
1354
|
* Retry shutdown for a stalled component.
|
|
1345
1355
|
* Attempts the force phase directly to avoid re-running a failing stop().
|
|
@@ -1409,6 +1419,9 @@ declare class LifecycleManager extends EventEmitterProtected implements Lifecycl
|
|
|
1409
1419
|
* Used when a required component fails to start during startAllComponents()
|
|
1410
1420
|
*/
|
|
1411
1421
|
private rollbackStartup;
|
|
1422
|
+
private autoAttachSignals;
|
|
1423
|
+
private autoDetachSignalsIfIdle;
|
|
1424
|
+
private monitorLateStartupCompletion;
|
|
1412
1425
|
/**
|
|
1413
1426
|
* Safe emit wrapper - prevents event handler errors from breaking lifecycle
|
|
1414
1427
|
*/
|
|
@@ -850,6 +850,9 @@ var EventEmitterProtected = class {
|
|
|
850
850
|
}
|
|
851
851
|
};
|
|
852
852
|
|
|
853
|
+
// src/lib/lifecycle-manager/lifecycle-manager.ts
|
|
854
|
+
import { ulid as ulid2 } from "ulid";
|
|
855
|
+
|
|
853
856
|
// src/lib/lifecycle-manager/component-lifecycle.ts
|
|
854
857
|
var ComponentLifecycle = class {
|
|
855
858
|
manager;
|
|
@@ -1915,6 +1918,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
1915
1918
|
messageTimeoutMS;
|
|
1916
1919
|
startupTimeoutMS;
|
|
1917
1920
|
shutdownOptions;
|
|
1921
|
+
attachSignalsBeforeStartup;
|
|
1918
1922
|
attachSignalsOnStart;
|
|
1919
1923
|
detachSignalsOnStop;
|
|
1920
1924
|
// Component management
|
|
@@ -1925,10 +1929,15 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
1925
1929
|
// State tracking for individual components
|
|
1926
1930
|
componentTimestamps = /* @__PURE__ */ new Map();
|
|
1927
1931
|
componentErrors = /* @__PURE__ */ new Map();
|
|
1932
|
+
componentStartAttemptTokens = /* @__PURE__ */ new Map();
|
|
1928
1933
|
// State flags
|
|
1929
1934
|
isStarting = false;
|
|
1930
1935
|
isStarted = false;
|
|
1931
1936
|
isShuttingDown = false;
|
|
1937
|
+
// Unique token used to detect shutdowns that happened during async start().
|
|
1938
|
+
shutdownToken = ulid2();
|
|
1939
|
+
// Resolver for the first logger.exit() deferred during an already-running shutdown.
|
|
1940
|
+
pendingLoggerExitResolve = null;
|
|
1932
1941
|
shutdownMethod = null;
|
|
1933
1942
|
lastShutdownResult = null;
|
|
1934
1943
|
// Signal management
|
|
@@ -1954,6 +1963,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
1954
1963
|
haltOnStall: true,
|
|
1955
1964
|
...options.shutdownOptions
|
|
1956
1965
|
};
|
|
1966
|
+
this.attachSignalsBeforeStartup = options.attachSignalsBeforeStartup ?? false;
|
|
1957
1967
|
this.attachSignalsOnStart = options.attachSignalsOnStart ?? false;
|
|
1958
1968
|
this.detachSignalsOnStop = options.detachSignalsOnStop ?? false;
|
|
1959
1969
|
this.onReloadRequested = options.onReloadRequested;
|
|
@@ -2117,6 +2127,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2117
2127
|
this.componentStates.delete(name);
|
|
2118
2128
|
this.componentTimestamps.delete(name);
|
|
2119
2129
|
this.componentErrors.delete(name);
|
|
2130
|
+
this.componentStartAttemptTokens.delete(name);
|
|
2120
2131
|
this.stalledComponents.delete(name);
|
|
2121
2132
|
this.runningComponents.delete(name);
|
|
2122
2133
|
this.updateStartedFlag();
|
|
@@ -2525,6 +2536,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2525
2536
|
this.isStarting = true;
|
|
2526
2537
|
this.shutdownMethod = null;
|
|
2527
2538
|
this.lastShutdownResult = null;
|
|
2539
|
+
const didAutoAttachSignalsForBulkStartup = this.attachSignalsBeforeStartup ? this.autoAttachSignals("bulk startup") : false;
|
|
2528
2540
|
this.logger.info("Starting all components");
|
|
2529
2541
|
const effectiveTimeout = options?.timeoutMS ?? this.startupTimeoutMS;
|
|
2530
2542
|
let hasTimedOut = false;
|
|
@@ -2638,6 +2650,18 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2638
2650
|
startedComponents.push(name);
|
|
2639
2651
|
} else if (result.code === "component_already_running") {
|
|
2640
2652
|
startedComponents.push(name);
|
|
2653
|
+
} else if (result.code === "shutdown_in_progress") {
|
|
2654
|
+
await this.rollbackStartup(startedComponents);
|
|
2655
|
+
return {
|
|
2656
|
+
success: false,
|
|
2657
|
+
startedComponents: [],
|
|
2658
|
+
failedOptionalComponents,
|
|
2659
|
+
skippedDueToDependency: Array.from(skippedDueToDependency),
|
|
2660
|
+
reason: result.reason || "Shutdown triggered during startup",
|
|
2661
|
+
code: "shutdown_in_progress",
|
|
2662
|
+
error: result.error,
|
|
2663
|
+
durationMS: Date.now() - startTime
|
|
2664
|
+
};
|
|
2641
2665
|
} else {
|
|
2642
2666
|
if (component.isOptional()) {
|
|
2643
2667
|
this.logger.entity(name).warn("Optional component failed to start, continuing", {
|
|
@@ -2726,6 +2750,9 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2726
2750
|
if (timeoutHandle) {
|
|
2727
2751
|
clearTimeout(timeoutHandle);
|
|
2728
2752
|
}
|
|
2753
|
+
if (didAutoAttachSignalsForBulkStartup) {
|
|
2754
|
+
this.autoDetachSignalsIfIdle("failed bulk startup");
|
|
2755
|
+
}
|
|
2729
2756
|
this.isStarting = false;
|
|
2730
2757
|
}
|
|
2731
2758
|
}
|
|
@@ -2970,6 +2997,17 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2970
2997
|
this.rootLogger.setBeforeExitCallback(
|
|
2971
2998
|
async (exitCode, isFirstExit) => {
|
|
2972
2999
|
if (this.isShuttingDown) {
|
|
3000
|
+
if (isFirstExit && this.pendingLoggerExitResolve === null) {
|
|
3001
|
+
this.logger.debug(
|
|
3002
|
+
"Logger exit called during shutdown, waiting...",
|
|
3003
|
+
{
|
|
3004
|
+
params: { exitCode }
|
|
3005
|
+
}
|
|
3006
|
+
);
|
|
3007
|
+
return await new Promise((resolve) => {
|
|
3008
|
+
this.pendingLoggerExitResolve = resolve;
|
|
3009
|
+
});
|
|
3010
|
+
}
|
|
2973
3011
|
this.logger.debug("Logger exit called during shutdown, waiting...", {
|
|
2974
3012
|
params: { exitCode }
|
|
2975
3013
|
});
|
|
@@ -3793,6 +3831,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3793
3831
|
stoppedAt: null
|
|
3794
3832
|
});
|
|
3795
3833
|
this.componentErrors.set(componentName, null);
|
|
3834
|
+
this.componentStartAttemptTokens.set(componentName, ulid2());
|
|
3796
3835
|
const isManualPositionRespected = this.isManualPositionRespected({
|
|
3797
3836
|
componentName,
|
|
3798
3837
|
position,
|
|
@@ -3944,6 +3983,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3944
3983
|
};
|
|
3945
3984
|
}
|
|
3946
3985
|
this.isShuttingDown = true;
|
|
3986
|
+
this.shutdownToken = ulid2();
|
|
3947
3987
|
this.shutdownMethod = method;
|
|
3948
3988
|
const isDuringStartup = this.isStarting;
|
|
3949
3989
|
this.logger.info("Stopping all components", { params: { method } });
|
|
@@ -4073,7 +4113,19 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4073
4113
|
}
|
|
4074
4114
|
this.isShuttingDown = false;
|
|
4075
4115
|
this.updateStartedFlag();
|
|
4116
|
+
this.finalizePendingLoggerExit();
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
/**
|
|
4120
|
+
* Release a deferred logger.exit() request after shutdown fully settles.
|
|
4121
|
+
*/
|
|
4122
|
+
finalizePendingLoggerExit() {
|
|
4123
|
+
if (this.pendingLoggerExitResolve === null || this.isShuttingDown) {
|
|
4124
|
+
return;
|
|
4076
4125
|
}
|
|
4126
|
+
const resolve = this.pendingLoggerExitResolve;
|
|
4127
|
+
this.pendingLoggerExitResolve = null;
|
|
4128
|
+
resolve({ action: "proceed" });
|
|
4077
4129
|
}
|
|
4078
4130
|
/**
|
|
4079
4131
|
* Retry shutdown for a stalled component.
|
|
@@ -4214,6 +4266,10 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4214
4266
|
this.logger.entity(name).info("Starting component");
|
|
4215
4267
|
this.lifecycleEvents.componentStarting(name);
|
|
4216
4268
|
const timeoutMS = component.startupTimeoutMS;
|
|
4269
|
+
const startAttemptToken = ulid2();
|
|
4270
|
+
this.componentStartAttemptTokens.set(name, startAttemptToken);
|
|
4271
|
+
const shutdownTokenAtStart = this.shutdownToken;
|
|
4272
|
+
const didAutoAttachSignalsForComponentStartup = this.attachSignalsBeforeStartup ? this.autoAttachSignals("component startup") : false;
|
|
4217
4273
|
let timeoutHandle;
|
|
4218
4274
|
try {
|
|
4219
4275
|
const startPromise = component.start();
|
|
@@ -4228,6 +4284,13 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4228
4284
|
params: { error }
|
|
4229
4285
|
});
|
|
4230
4286
|
}
|
|
4287
|
+
} else {
|
|
4288
|
+
this.monitorLateStartupCompletion(
|
|
4289
|
+
name,
|
|
4290
|
+
component,
|
|
4291
|
+
startPromise,
|
|
4292
|
+
startAttemptToken
|
|
4293
|
+
);
|
|
4231
4294
|
}
|
|
4232
4295
|
Promise.resolve(startPromise).catch(() => {
|
|
4233
4296
|
});
|
|
@@ -4243,15 +4306,36 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4243
4306
|
} else {
|
|
4244
4307
|
await startPromise;
|
|
4245
4308
|
}
|
|
4309
|
+
if (this.isShuttingDown || shutdownTokenAtStart !== this.shutdownToken) {
|
|
4310
|
+
this.componentStates.set(name, "running");
|
|
4311
|
+
this.runningComponents.add(name);
|
|
4312
|
+
this.stalledComponents.delete(name);
|
|
4313
|
+
this.updateStartedFlag();
|
|
4314
|
+
const timestamps2 = this.componentTimestamps.get(name) ?? {
|
|
4315
|
+
startedAt: null,
|
|
4316
|
+
stoppedAt: null
|
|
4317
|
+
};
|
|
4318
|
+
timestamps2.startedAt = Date.now();
|
|
4319
|
+
this.componentTimestamps.set(name, timestamps2);
|
|
4320
|
+
this.logger.entity(name).warn(
|
|
4321
|
+
"Component finished starting after shutdown began, stopping immediately"
|
|
4322
|
+
);
|
|
4323
|
+
const stopResult = await this.stopComponentInternal(name);
|
|
4324
|
+
return {
|
|
4325
|
+
success: false,
|
|
4326
|
+
componentName: name,
|
|
4327
|
+
reason: "Shutdown triggered during component startup",
|
|
4328
|
+
code: "shutdown_in_progress",
|
|
4329
|
+
error: stopResult.error,
|
|
4330
|
+
status: this.getComponentStatus(name)
|
|
4331
|
+
};
|
|
4332
|
+
}
|
|
4246
4333
|
this.componentStates.set(name, "running");
|
|
4247
4334
|
this.runningComponents.add(name);
|
|
4248
4335
|
this.stalledComponents.delete(name);
|
|
4249
4336
|
this.updateStartedFlag();
|
|
4250
|
-
if (this.attachSignalsOnStart && this.runningComponents.size === 1
|
|
4251
|
-
this.
|
|
4252
|
-
"Auto-attaching process signals on first component start"
|
|
4253
|
-
);
|
|
4254
|
-
this.attachSignals();
|
|
4337
|
+
if (this.attachSignalsOnStart && this.runningComponents.size === 1) {
|
|
4338
|
+
this.autoAttachSignals("first component start");
|
|
4255
4339
|
}
|
|
4256
4340
|
const timestamps = this.componentTimestamps.get(name) ?? {
|
|
4257
4341
|
startedAt: null,
|
|
@@ -4300,6 +4384,9 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4300
4384
|
if (timeoutHandle) {
|
|
4301
4385
|
clearTimeout(timeoutHandle);
|
|
4302
4386
|
}
|
|
4387
|
+
if (didAutoAttachSignalsForComponentStartup) {
|
|
4388
|
+
this.autoDetachSignalsIfIdle("failed component startup");
|
|
4389
|
+
}
|
|
4303
4390
|
}
|
|
4304
4391
|
}
|
|
4305
4392
|
/**
|
|
@@ -4796,6 +4883,57 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4796
4883
|
}
|
|
4797
4884
|
this.logger.info("Rollback completed");
|
|
4798
4885
|
}
|
|
4886
|
+
autoAttachSignals(trigger) {
|
|
4887
|
+
if (this.processSignalManager?.getStatus().isAttached) {
|
|
4888
|
+
return false;
|
|
4889
|
+
}
|
|
4890
|
+
this.logger.info(`Auto-attaching process signals on ${trigger}`);
|
|
4891
|
+
this.attachSignals();
|
|
4892
|
+
return true;
|
|
4893
|
+
}
|
|
4894
|
+
autoDetachSignalsIfIdle(trigger) {
|
|
4895
|
+
if (!this.detachSignalsOnStop || this.runningComponents.size > 0 || !this.processSignalManager?.getStatus().isAttached) {
|
|
4896
|
+
return;
|
|
4897
|
+
}
|
|
4898
|
+
this.logger.info(`Auto-detaching process signals after ${trigger}`);
|
|
4899
|
+
this.detachSignals();
|
|
4900
|
+
}
|
|
4901
|
+
monitorLateStartupCompletion(name, component, startPromise, startAttemptToken) {
|
|
4902
|
+
this.logger.entity(name).warn(
|
|
4903
|
+
"Startup timed out without onStartupAborted, stopping component if startup completes later"
|
|
4904
|
+
);
|
|
4905
|
+
Promise.resolve(startPromise).then(async () => {
|
|
4906
|
+
const timeoutState = this.componentStates.get(name);
|
|
4907
|
+
if (this.getComponent(name) !== component || this.componentStartAttemptTokens.get(name) !== startAttemptToken || this.isComponentRunning(name) || timeoutState !== "starting-timed-out" && timeoutState !== "failed") {
|
|
4908
|
+
return;
|
|
4909
|
+
}
|
|
4910
|
+
this.componentStates.set(name, "running");
|
|
4911
|
+
this.runningComponents.add(name);
|
|
4912
|
+
this.stalledComponents.delete(name);
|
|
4913
|
+
this.updateStartedFlag();
|
|
4914
|
+
const timestamps = this.componentTimestamps.get(name) ?? {
|
|
4915
|
+
startedAt: null,
|
|
4916
|
+
stoppedAt: null
|
|
4917
|
+
};
|
|
4918
|
+
timestamps.startedAt = Date.now();
|
|
4919
|
+
this.componentTimestamps.set(name, timestamps);
|
|
4920
|
+
this.logger.entity(name).warn(
|
|
4921
|
+
"Component completed startup after timeout, stopping automatically"
|
|
4922
|
+
);
|
|
4923
|
+
const stopResult = await this.stopComponentInternal(name);
|
|
4924
|
+
if (!stopResult.success) {
|
|
4925
|
+
this.logger.entity(name).warn("Automatic stop after startup timeout failed", {
|
|
4926
|
+
params: {
|
|
4927
|
+
error: stopResult.error,
|
|
4928
|
+
code: stopResult.code
|
|
4929
|
+
}
|
|
4930
|
+
});
|
|
4931
|
+
return;
|
|
4932
|
+
}
|
|
4933
|
+
this.componentStates.set(name, timeoutState);
|
|
4934
|
+
}).catch(() => {
|
|
4935
|
+
});
|
|
4936
|
+
}
|
|
4799
4937
|
/**
|
|
4800
4938
|
* Safe emit wrapper - prevents event handler errors from breaking lifecycle
|
|
4801
4939
|
*/
|