lifecycleion 0.0.1 → 0.0.3
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 +139 -22
- package/dist/lib/lifecycle-manager/index.cjs.map +1 -1
- package/dist/lib/lifecycle-manager/index.d.cts +9 -1
- package/dist/lib/lifecycle-manager/index.d.ts +9 -1
- package/dist/lib/lifecycle-manager/index.js +139 -22
- 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,13 @@ 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)();
|
|
1982
1989
|
shutdownMethod = null;
|
|
1983
1990
|
lastShutdownResult = null;
|
|
1984
1991
|
// Signal management
|
|
@@ -2004,6 +2011,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2004
2011
|
haltOnStall: true,
|
|
2005
2012
|
...options.shutdownOptions
|
|
2006
2013
|
};
|
|
2014
|
+
this.attachSignalsBeforeStartup = options.attachSignalsBeforeStartup ?? false;
|
|
2007
2015
|
this.attachSignalsOnStart = options.attachSignalsOnStart ?? false;
|
|
2008
2016
|
this.detachSignalsOnStop = options.detachSignalsOnStop ?? false;
|
|
2009
2017
|
this.onReloadRequested = options.onReloadRequested;
|
|
@@ -2167,6 +2175,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2167
2175
|
this.componentStates.delete(name);
|
|
2168
2176
|
this.componentTimestamps.delete(name);
|
|
2169
2177
|
this.componentErrors.delete(name);
|
|
2178
|
+
this.componentStartAttemptTokens.delete(name);
|
|
2170
2179
|
this.stalledComponents.delete(name);
|
|
2171
2180
|
this.runningComponents.delete(name);
|
|
2172
2181
|
this.updateStartedFlag();
|
|
@@ -2575,6 +2584,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2575
2584
|
this.isStarting = true;
|
|
2576
2585
|
this.shutdownMethod = null;
|
|
2577
2586
|
this.lastShutdownResult = null;
|
|
2587
|
+
const didAutoAttachSignalsForBulkStartup = this.attachSignalsBeforeStartup ? this.autoAttachSignals("bulk startup") : false;
|
|
2578
2588
|
this.logger.info("Starting all components");
|
|
2579
2589
|
const effectiveTimeout = options?.timeoutMS ?? this.startupTimeoutMS;
|
|
2580
2590
|
let hasTimedOut = false;
|
|
@@ -2598,7 +2608,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2598
2608
|
const err = error;
|
|
2599
2609
|
const code = err instanceof DependencyCycleError ? "dependency_cycle" : "unknown_error";
|
|
2600
2610
|
this.logger.error("Failed to resolve startup order", {
|
|
2601
|
-
params: { error: err
|
|
2611
|
+
params: { error: err }
|
|
2602
2612
|
});
|
|
2603
2613
|
return {
|
|
2604
2614
|
success: false,
|
|
@@ -2688,10 +2698,22 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2688
2698
|
startedComponents.push(name);
|
|
2689
2699
|
} else if (result.code === "component_already_running") {
|
|
2690
2700
|
startedComponents.push(name);
|
|
2701
|
+
} else if (result.code === "shutdown_in_progress") {
|
|
2702
|
+
await this.rollbackStartup(startedComponents);
|
|
2703
|
+
return {
|
|
2704
|
+
success: false,
|
|
2705
|
+
startedComponents: [],
|
|
2706
|
+
failedOptionalComponents,
|
|
2707
|
+
skippedDueToDependency: Array.from(skippedDueToDependency),
|
|
2708
|
+
reason: result.reason || "Shutdown triggered during startup",
|
|
2709
|
+
code: "shutdown_in_progress",
|
|
2710
|
+
error: result.error,
|
|
2711
|
+
durationMS: Date.now() - startTime
|
|
2712
|
+
};
|
|
2691
2713
|
} else {
|
|
2692
2714
|
if (component.isOptional()) {
|
|
2693
2715
|
this.logger.entity(name).warn("Optional component failed to start, continuing", {
|
|
2694
|
-
params: { error: result.error
|
|
2716
|
+
params: { error: result.error }
|
|
2695
2717
|
});
|
|
2696
2718
|
this.lifecycleEvents.componentStartFailedOptional(
|
|
2697
2719
|
name,
|
|
@@ -2707,7 +2729,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2707
2729
|
});
|
|
2708
2730
|
} else {
|
|
2709
2731
|
this.logger.entity(name).error("Required component failed to start, rolling back", {
|
|
2710
|
-
params: { error: result.error
|
|
2732
|
+
params: { error: result.error }
|
|
2711
2733
|
});
|
|
2712
2734
|
await this.rollbackStartup(startedComponents);
|
|
2713
2735
|
return {
|
|
@@ -2776,6 +2798,9 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2776
2798
|
if (timeoutHandle) {
|
|
2777
2799
|
clearTimeout(timeoutHandle);
|
|
2778
2800
|
}
|
|
2801
|
+
if (didAutoAttachSignalsForBulkStartup) {
|
|
2802
|
+
this.autoDetachSignalsIfIdle("failed bulk startup");
|
|
2803
|
+
}
|
|
2779
2804
|
this.isStarting = false;
|
|
2780
2805
|
}
|
|
2781
2806
|
}
|
|
@@ -3190,7 +3215,9 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3190
3215
|
} catch (error) {
|
|
3191
3216
|
const durationMS = Date.now() - startTime;
|
|
3192
3217
|
const err = error;
|
|
3193
|
-
this.logger.entity(name).error("Health check failed", {
|
|
3218
|
+
this.logger.entity(name).error("Health check failed", {
|
|
3219
|
+
params: { error: err }
|
|
3220
|
+
});
|
|
3194
3221
|
this.lifecycleEvents.componentHealthCheckFailed(name, err);
|
|
3195
3222
|
return {
|
|
3196
3223
|
name,
|
|
@@ -3337,7 +3364,9 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3337
3364
|
result = component.onMessage(payload, from);
|
|
3338
3365
|
} catch (error) {
|
|
3339
3366
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
3340
|
-
this.logger.entity(componentName).error("Message handler failed", {
|
|
3367
|
+
this.logger.entity(componentName).error("Message handler failed", {
|
|
3368
|
+
params: { error: err, from }
|
|
3369
|
+
});
|
|
3341
3370
|
this.lifecycleEvents.componentMessageFailed(componentName, from, err, {
|
|
3342
3371
|
timedOut: false,
|
|
3343
3372
|
code: "error",
|
|
@@ -3770,7 +3799,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3770
3799
|
startupOrder2 = this.getStartupOrderInternal();
|
|
3771
3800
|
} catch (error) {
|
|
3772
3801
|
this.logger.warn("Failed to compute startup order in error handler", {
|
|
3773
|
-
params: { error
|
|
3802
|
+
params: { error }
|
|
3774
3803
|
});
|
|
3775
3804
|
startupOrder2 = [];
|
|
3776
3805
|
}
|
|
@@ -3839,6 +3868,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3839
3868
|
stoppedAt: null
|
|
3840
3869
|
});
|
|
3841
3870
|
this.componentErrors.set(componentName, null);
|
|
3871
|
+
this.componentStartAttemptTokens.set(componentName, (0, import_ulid2.ulid)());
|
|
3842
3872
|
const isManualPositionRespected = this.isManualPositionRespected({
|
|
3843
3873
|
componentName,
|
|
3844
3874
|
position,
|
|
@@ -3990,6 +4020,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3990
4020
|
};
|
|
3991
4021
|
}
|
|
3992
4022
|
this.isShuttingDown = true;
|
|
4023
|
+
this.shutdownToken = (0, import_ulid2.ulid)();
|
|
3993
4024
|
this.shutdownMethod = method;
|
|
3994
4025
|
const isDuringStartup = this.isStarting;
|
|
3995
4026
|
this.logger.info("Stopping all components", { params: { method } });
|
|
@@ -4005,7 +4036,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4005
4036
|
this.logger.warn(
|
|
4006
4037
|
"Could not resolve shutdown order, using registration order",
|
|
4007
4038
|
{
|
|
4008
|
-
params: { error
|
|
4039
|
+
params: { error }
|
|
4009
4040
|
}
|
|
4010
4041
|
);
|
|
4011
4042
|
shutdownOrder = this.components.map((c) => c.getName()).reverse();
|
|
@@ -4063,7 +4094,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4063
4094
|
stoppedComponents.push(name);
|
|
4064
4095
|
} else {
|
|
4065
4096
|
this.logger.entity(name).error("Component failed to stop, continuing with others", {
|
|
4066
|
-
params: { error: result2.error
|
|
4097
|
+
params: { error: result2.error }
|
|
4067
4098
|
});
|
|
4068
4099
|
const stallInfo = this.stalledComponents.get(name);
|
|
4069
4100
|
if (stallInfo) {
|
|
@@ -4260,6 +4291,10 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4260
4291
|
this.logger.entity(name).info("Starting component");
|
|
4261
4292
|
this.lifecycleEvents.componentStarting(name);
|
|
4262
4293
|
const timeoutMS = component.startupTimeoutMS;
|
|
4294
|
+
const startAttemptToken = (0, import_ulid2.ulid)();
|
|
4295
|
+
this.componentStartAttemptTokens.set(name, startAttemptToken);
|
|
4296
|
+
const shutdownTokenAtStart = this.shutdownToken;
|
|
4297
|
+
const didAutoAttachSignalsForComponentStartup = this.attachSignalsBeforeStartup ? this.autoAttachSignals("component startup") : false;
|
|
4263
4298
|
let timeoutHandle;
|
|
4264
4299
|
try {
|
|
4265
4300
|
const startPromise = component.start();
|
|
@@ -4274,6 +4309,13 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4274
4309
|
params: { error }
|
|
4275
4310
|
});
|
|
4276
4311
|
}
|
|
4312
|
+
} else {
|
|
4313
|
+
this.monitorLateStartupCompletion(
|
|
4314
|
+
name,
|
|
4315
|
+
component,
|
|
4316
|
+
startPromise,
|
|
4317
|
+
startAttemptToken
|
|
4318
|
+
);
|
|
4277
4319
|
}
|
|
4278
4320
|
Promise.resolve(startPromise).catch(() => {
|
|
4279
4321
|
});
|
|
@@ -4289,15 +4331,36 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4289
4331
|
} else {
|
|
4290
4332
|
await startPromise;
|
|
4291
4333
|
}
|
|
4334
|
+
if (this.isShuttingDown || shutdownTokenAtStart !== this.shutdownToken) {
|
|
4335
|
+
this.componentStates.set(name, "running");
|
|
4336
|
+
this.runningComponents.add(name);
|
|
4337
|
+
this.stalledComponents.delete(name);
|
|
4338
|
+
this.updateStartedFlag();
|
|
4339
|
+
const timestamps2 = this.componentTimestamps.get(name) ?? {
|
|
4340
|
+
startedAt: null,
|
|
4341
|
+
stoppedAt: null
|
|
4342
|
+
};
|
|
4343
|
+
timestamps2.startedAt = Date.now();
|
|
4344
|
+
this.componentTimestamps.set(name, timestamps2);
|
|
4345
|
+
this.logger.entity(name).warn(
|
|
4346
|
+
"Component finished starting after shutdown began, stopping immediately"
|
|
4347
|
+
);
|
|
4348
|
+
const stopResult = await this.stopComponentInternal(name);
|
|
4349
|
+
return {
|
|
4350
|
+
success: false,
|
|
4351
|
+
componentName: name,
|
|
4352
|
+
reason: "Shutdown triggered during component startup",
|
|
4353
|
+
code: "shutdown_in_progress",
|
|
4354
|
+
error: stopResult.error,
|
|
4355
|
+
status: this.getComponentStatus(name)
|
|
4356
|
+
};
|
|
4357
|
+
}
|
|
4292
4358
|
this.componentStates.set(name, "running");
|
|
4293
4359
|
this.runningComponents.add(name);
|
|
4294
4360
|
this.stalledComponents.delete(name);
|
|
4295
4361
|
this.updateStartedFlag();
|
|
4296
|
-
if (this.attachSignalsOnStart && this.runningComponents.size === 1
|
|
4297
|
-
this.
|
|
4298
|
-
"Auto-attaching process signals on first component start"
|
|
4299
|
-
);
|
|
4300
|
-
this.attachSignals();
|
|
4362
|
+
if (this.attachSignalsOnStart && this.runningComponents.size === 1) {
|
|
4363
|
+
this.autoAttachSignals("first component start");
|
|
4301
4364
|
}
|
|
4302
4365
|
const timestamps = this.componentTimestamps.get(name) ?? {
|
|
4303
4366
|
startedAt: null,
|
|
@@ -4319,7 +4382,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4319
4382
|
if (err instanceof ComponentStartTimeoutError && err.additionalInfo.componentName === name) {
|
|
4320
4383
|
this.componentStates.set(name, "starting-timed-out");
|
|
4321
4384
|
this.logger.entity(name).error("Component startup timed out", {
|
|
4322
|
-
params: { error: err
|
|
4385
|
+
params: { error: err }
|
|
4323
4386
|
});
|
|
4324
4387
|
this.lifecycleEvents.componentStartTimeout(name, err, {
|
|
4325
4388
|
timeoutMS,
|
|
@@ -4328,7 +4391,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4328
4391
|
} else {
|
|
4329
4392
|
this.componentStates.set(name, "registered");
|
|
4330
4393
|
this.logger.entity(name).error("Component failed to start", {
|
|
4331
|
-
params: { error: err
|
|
4394
|
+
params: { error: err }
|
|
4332
4395
|
});
|
|
4333
4396
|
this.lifecycleEvents.componentStartFailed(name, err, {
|
|
4334
4397
|
reason: err.message
|
|
@@ -4346,6 +4409,9 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4346
4409
|
if (timeoutHandle) {
|
|
4347
4410
|
clearTimeout(timeoutHandle);
|
|
4348
4411
|
}
|
|
4412
|
+
if (didAutoAttachSignalsForComponentStartup) {
|
|
4413
|
+
this.autoDetachSignalsIfIdle("failed component startup");
|
|
4414
|
+
}
|
|
4349
4415
|
}
|
|
4350
4416
|
}
|
|
4351
4417
|
/**
|
|
@@ -4457,7 +4523,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4457
4523
|
this.lifecycleEvents.componentShutdownWarningCompleted(name);
|
|
4458
4524
|
}).catch((error) => {
|
|
4459
4525
|
this.logger.entity(name).warn("Shutdown warning phase failed", {
|
|
4460
|
-
params: { error
|
|
4526
|
+
params: { error }
|
|
4461
4527
|
});
|
|
4462
4528
|
});
|
|
4463
4529
|
}
|
|
@@ -4480,7 +4546,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4480
4546
|
}).catch((error) => {
|
|
4481
4547
|
statuses.set(name, "rejected");
|
|
4482
4548
|
this.logger.entity(name).warn("Shutdown warning phase failed", {
|
|
4483
|
-
params: { error
|
|
4549
|
+
params: { error }
|
|
4484
4550
|
});
|
|
4485
4551
|
})
|
|
4486
4552
|
);
|
|
@@ -4603,7 +4669,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4603
4669
|
};
|
|
4604
4670
|
} else {
|
|
4605
4671
|
this.logger.entity(name).warn("Graceful shutdown threw error", {
|
|
4606
|
-
params: { error: err
|
|
4672
|
+
params: { error: err }
|
|
4607
4673
|
});
|
|
4608
4674
|
return {
|
|
4609
4675
|
success: false,
|
|
@@ -4747,7 +4813,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4747
4813
|
this.lifecycleEvents.componentShutdownForceTimeout(name, timeoutMS);
|
|
4748
4814
|
} else {
|
|
4749
4815
|
this.logger.entity(name).error("Force shutdown failed - stalled", {
|
|
4750
|
-
params: { error: err
|
|
4816
|
+
params: { error: err }
|
|
4751
4817
|
});
|
|
4752
4818
|
}
|
|
4753
4819
|
this.lifecycleEvents.componentStalled(name, stallInfo, {
|
|
@@ -4836,12 +4902,63 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4836
4902
|
const result = await this.stopComponentInternal(name);
|
|
4837
4903
|
if (!result.success) {
|
|
4838
4904
|
this.logger.entity(name).warn("Failed to stop component during rollback, continuing", {
|
|
4839
|
-
params: { error: result.error
|
|
4905
|
+
params: { error: result.error }
|
|
4840
4906
|
});
|
|
4841
4907
|
}
|
|
4842
4908
|
}
|
|
4843
4909
|
this.logger.info("Rollback completed");
|
|
4844
4910
|
}
|
|
4911
|
+
autoAttachSignals(trigger) {
|
|
4912
|
+
if (this.processSignalManager?.getStatus().isAttached) {
|
|
4913
|
+
return false;
|
|
4914
|
+
}
|
|
4915
|
+
this.logger.info(`Auto-attaching process signals on ${trigger}`);
|
|
4916
|
+
this.attachSignals();
|
|
4917
|
+
return true;
|
|
4918
|
+
}
|
|
4919
|
+
autoDetachSignalsIfIdle(trigger) {
|
|
4920
|
+
if (!this.detachSignalsOnStop || this.runningComponents.size > 0 || !this.processSignalManager?.getStatus().isAttached) {
|
|
4921
|
+
return;
|
|
4922
|
+
}
|
|
4923
|
+
this.logger.info(`Auto-detaching process signals after ${trigger}`);
|
|
4924
|
+
this.detachSignals();
|
|
4925
|
+
}
|
|
4926
|
+
monitorLateStartupCompletion(name, component, startPromise, startAttemptToken) {
|
|
4927
|
+
this.logger.entity(name).warn(
|
|
4928
|
+
"Startup timed out without onStartupAborted, stopping component if startup completes later"
|
|
4929
|
+
);
|
|
4930
|
+
Promise.resolve(startPromise).then(async () => {
|
|
4931
|
+
const timeoutState = this.componentStates.get(name);
|
|
4932
|
+
if (this.getComponent(name) !== component || this.componentStartAttemptTokens.get(name) !== startAttemptToken || this.isComponentRunning(name) || timeoutState !== "starting-timed-out" && timeoutState !== "failed") {
|
|
4933
|
+
return;
|
|
4934
|
+
}
|
|
4935
|
+
this.componentStates.set(name, "running");
|
|
4936
|
+
this.runningComponents.add(name);
|
|
4937
|
+
this.stalledComponents.delete(name);
|
|
4938
|
+
this.updateStartedFlag();
|
|
4939
|
+
const timestamps = this.componentTimestamps.get(name) ?? {
|
|
4940
|
+
startedAt: null,
|
|
4941
|
+
stoppedAt: null
|
|
4942
|
+
};
|
|
4943
|
+
timestamps.startedAt = Date.now();
|
|
4944
|
+
this.componentTimestamps.set(name, timestamps);
|
|
4945
|
+
this.logger.entity(name).warn(
|
|
4946
|
+
"Component completed startup after timeout, stopping automatically"
|
|
4947
|
+
);
|
|
4948
|
+
const stopResult = await this.stopComponentInternal(name);
|
|
4949
|
+
if (!stopResult.success) {
|
|
4950
|
+
this.logger.entity(name).warn("Automatic stop after startup timeout failed", {
|
|
4951
|
+
params: {
|
|
4952
|
+
error: stopResult.error,
|
|
4953
|
+
code: stopResult.code
|
|
4954
|
+
}
|
|
4955
|
+
});
|
|
4956
|
+
return;
|
|
4957
|
+
}
|
|
4958
|
+
this.componentStates.set(name, timeoutState);
|
|
4959
|
+
}).catch(() => {
|
|
4960
|
+
});
|
|
4961
|
+
}
|
|
4845
4962
|
/**
|
|
4846
4963
|
* Safe emit wrapper - prevents event handler errors from breaking lifecycle
|
|
4847
4964
|
*/
|
|
@@ -4860,7 +4977,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4860
4977
|
startupOrder = this.getStartupOrderInternal();
|
|
4861
4978
|
} catch (error) {
|
|
4862
4979
|
this.logger.warn("Failed to compute startup order in error handler", {
|
|
4863
|
-
params: { error
|
|
4980
|
+
params: { error }
|
|
4864
4981
|
});
|
|
4865
4982
|
startupOrder = [];
|
|
4866
4983
|
}
|
|
@@ -4883,7 +5000,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4883
5000
|
startupOrder = this.getStartupOrderInternal();
|
|
4884
5001
|
} catch (error) {
|
|
4885
5002
|
this.logger.warn("Failed to compute startup order in error handler", {
|
|
4886
|
-
params: { error
|
|
5003
|
+
params: { error }
|
|
4887
5004
|
});
|
|
4888
5005
|
startupOrder = [];
|
|
4889
5006
|
}
|