Note: My code happens to be in TypeScript, but this question applies to both TypeScript and JavaScript with a few syntactic changes.
I've got a class that subscribes to a few things (such as Autobahn messages). When I'm done with the object, I need to unsubscribe from everything. Generically, my code is something like this:
class Example {
private sub1: Subscription;
private sub2: Subscription;
private sub3: Subscription;
constructor() {
subscribeToThings();
}
async subscribeToThings() {
this.sub1 = await subscribeToThingOne();
this.sub2 = await subscribeToThingTwo();
// *** NOTE 1 ***
this.sub3 = await subscribeToThingThree();
}
cleanupOnDeath () {
this.sub1.unsubscribe();
this.sub2.unsubscribe();
this.sub3.unsubscribe();
}
}
When I no longer need this object, I call cleanupOnDeath
.
However, there is a problem. If the subscriptions (which are asynchronous) take a long time, it's possible that the "cleanupOnDeath()" function will be called before all of sub1
, sub2
, and sub3
are set. For example, the code might be at the line marked *** NOTE 1 ***
when cleanupOnDeath is called.
Of course I could check if each one is undefined or not, before calling unsubscribe
, but that doesn't really solve things because then that third subscription gets processed after cleanupOnDeath
is finished, and lives forever.
Is there a way to implement this code so that cleanupOnDeath()
can be be called in the middle of subscribeToThings()
without making the code incredibly complex?
The simplest solution I have been able to come up with is something like this (replacing the sections of the above code as appropriate):
constructor() {
this.subscribeAndUnsubscribe();
}
async subscribeAndUnsubscribe () {
await this.subscribeToThings();
await cleanupEvent.wait();
this.sub1.unsubscribe();
this.sub2.unsubscribe();
this.sub3.unsubscribe();
}
cleanupOnDeath() {
cleanupEvent.set();
}
Where cleanupEvent
is some kind of synchronization primitive. But I don't think this is optimal, because it also requires all pending subscriptions to be completed before any can be unsubscribed. Ideally, I'd like to be able to abort subscribeToThings()
early, without adding checks after every line of code. For example. it can be done like this:
completed:boolean = false;
async subscribeToThings() {
try {
this.abortIfDone();
this.sub1 = await subscribeToThingOne();
this.abortIfDone();
this.sub2 = await subscribeToThingTwo();
this.abortIfDone();
this.sub3 = await subscribeToThingThree();
} catch (abortedEarly) {
// Something here
}
}
abortIfDone() {
if (this.completed) throw 'Something';
}
cleanupOnDeath() {
this.completed = true;
if (this.sub1) this.sub1.unsubscribe();
if (this.sub1) this.sub2.unsubscribe();
if (this.sub1) this.sub3.unsubscribe();
}
But that is messy and complicated.
I ended up using the following code, based on @ChrisTavares's answer. The difference between this and his answer is that I am no longer awaiting anything or storing the subscriptions themselves. Before, I was awaiting them to get the underlying subscription objects, for the sole purpose of unsubscribing later. Since this code stores the promises and handles unsubscribe on those using then
, there is no need to do that.
class Example {
private sub1Promise: Promise<Subscription>;
private sub2Promise: Promise<Subscription>;
private sub3Promise: Promise<Subscription>;
constructor() {
this.subscribeToThings();
}
subscribeToThings() {
// No longer async!
this.sub1Promise = subscribeToThingOne();
this.sub2Promise = subscribeToThingTwo();
this.sub3Promise = subscribeToThingThree();
}
cleanupOnDeath () {
this.sub1Promise.then(s => s.unsubscribe());
this.sub2Promise.then(s => s.unsubscribe());
this.sub3Promise.then(s => s.unsubscribe());
}
}
There's no standard way to cancel a promise - there's discussions going on in the various spec committees, but it's a hard problem for lots of reasons that don't help here.
I do question if this is a legitimate issue - are subscriptions really slow enough to worry about this issue? I'm not familiar with Autobahn.
But, assuming it is, one thing that could work would be to not await
the stuff immediately, but instead to hold onto the actual promises. Then you can tack a .then
handler to clean stuff up when needed. Something like this:
class Example {
private sub1: Subscription;
private sub2: Subscription;
private sub3: Subscription;
private sub1Promise: Promise<Subscription>;
private sub2Promise: Promise<Subscription>;
private sub3Promise: Promise<Subscription>;
constructor() {
subscribeToThings();
}
async subscribeToThings() {
// *** NOTE - no await here ***
this.sub1Promise = subscribeToThingOne();
this.sub2Promise = subscribeToThingTwo();
this.sub3Promise = subscribeToThingThree();
this.sub1 = await this.sub1Promise;
this.sub2 = await this.sub2Promise;
this.sub3 = await this.sub3Promise;
}
cleanupOnDeath () {
// Unsubscribe each promise. Don't need to check for null,
// they were set in subscribeToThings
this.sub1Promise.then((s) => s.unsubscribe());
this.sub2Promise.then((s) => s.unsubscribe());
this.sub3Promise.then((s) => s.unsubscribe());
}
}