When one starts out with lifecycle one will likely use the simple start overload:
let thing: Thing
lifecycle.register(
label: "thing",
start: thing.start,
// ...
)
Then one realizes it's best if the shutdown (or start, either) was async.
There's only an func register(label: String, start: @escaping () throws -> Void, shutdown: @escaping () throws -> Void) {
overload, and none for ((Error?) -> Void) -> Void
so one can be left confused, or just miss that erroring the lifecycle is a thing.
One then discovers shutdown: LifecycleHandler.async { (callback: @escaping (Error?) -> Void) -> Void in // callback: Error? -> Void
so:
let thing: Thing
lifecycle.register(
label: "thing",
start: thing.start,
shutdown: .async { callback in thing.shutdown(callback) }
)
which won't compile as well, and neither would shutdown: { callback in thing.shutdown(callback) }
.
The issue is that one has to make BOTH start and shutdown using the explicit .async
/.sync
API:
func register(label: String, start: LifecycleHandler, shutdown: LifecycleHandler) {
mixing the callback style with this is not possible.
We should either:
- provide all overloads for the .sync/.async in all permutations so people can use just plain closures whenever they want (a bit annoying and could yield not so good compiler errors)
- remove the closure overloads completely -- always explicitly say
.sync { ... }
(or async)
I'd vote for the latter, because it helps discoverability -- it's easy to discover there's an async way to init and shutdown, and if we ever need new ones (say, some future type etc), we can easily add those there.