Advanced Announcements, part 3
Back to suspending subscriptions.
Another suspension option is #suspendWhile:ifAnyMissed:. It takes a second argument which should always be a no-argument block. It works just like #suspendWith: in that the subscriptions you send this to are suspended and don't deliver anything to their recipients while the block runs. In addition, they keep track of whether there have been any "missed calls". After the "while" block finishes, the second block is evaluated once if there have been any undelivered announcements while the first block ran.
This serves the use case of "I want to bunch up potential multiple updates". For example, the following code will suppress Foo announcements but will then ensure one of those gets announced as a summary if needed:
(anObject subscriptionRegistry subscriptionsFor: Foo) suspendWhle: [...do stuff...] ifAnyMissed: [anObject announce: Foo]
On the recipient side, if we want to suspend response to updates from a certain object but then catch up with a single update, we can also do something like:
(anObject subscriptionRegistry subscriptionsOf: self) suspendWhile: [...] ifAnyMissed: [self update]
Again, overlap between subscrptions suspended by nested blocks is handled correctly, in the sense that nested suspend requests don't affect the outer ones and vice versa. If an outer block suspends a subscription, and then an inner block suspends it again with #suspendWhile:ifAnyMissed:, the missed block will run if needed after the inner block ends, while the subscription will stay suspended until the outer block ends. Conversely, if an outer block runs with #suspendWhile:ifAnyMissed: and the inner block suspends the same subscription outright, and an announcement arrives to that subscription inside the inner block, the summary block will run after the outer block exits.
The third suspending option we need to cover arranges it so that a block of code runs in lieu of each delivery that would have happened otherwise. For example, the following code will count how many actual announcement deliveries would have occurred:
count := 0. (anObject subscriptionRegistry subscriptionsFor: Foo) interceptWith: [count := count + 1] while: [anObject announce: Foo]. ^count
Interceptor block can take arguments, with the same interpretation as in handler blocks established by #when:do:. These open up quite a lot of options of what can be done by the interceptor.
For example, the above code counts deliveries. If there are five subscribers for Foo, and Foo has been announced twice, the count will be 10 for the ten deliveries that would have occurred. If we want to count how many actual announcements were broadcast, regardless of how many objects would have received them, we can do this:
announcements := IdentitySet new. (anObject subscriptionRegistry subscriptionsFor: Foo) interceptWith: [:ann | announcements add: ann] while: [anObject announce: Foo]. ^announcements size
If the interceptor block has two arguments, it receives the announcement and the announcer, again just like in a regular handler. In the context of an interceptor block this probably isn't as useful. Since in order to get the subscriptions to intercept we start with the announcer and its registry, we typically know who the announcer is anyway.
The interceptor block can also take three arguments. In that case, the third argument is the subscription that has just been intercepted. Given that, the interceptor can find out the subscriber of the intercepted delivery. Coming back to our example, to count how many subscribers would have received the announcements we intercepted, we would do this:
subscribers := IdentitySet new. (anObject subscriptionRegistry subscriptionsFor: Foo) interceptWith: [:a :o :s | subscribers add: s subscriber] while: [anObject announce: Foo]. ^subscribers size
Another important option the access to subscription gives us is writing transparent interceptors, those that don't prevent announcements from reaching their subscribers. The following code will silently count how many announcements have been announced, but other than that it will be business as usual and all announcements will safely make it to all of their subscribers:
announcements := IdentitySet new. (anObject subscriptionRegistry subscriptionsFor: Foo) interceptWith: [:announcement :announcer :subscription | announcements add: announcement. subscription deliver: announcement from: announcer] while: [anObject announce: Foo]. ^announcements size
The final "subscription deliver: announcement from: announcer" is what you can use in interceptors to pass the announcement on to the intended recipient, conditionally or unconditionally at the end of the interceptor block.
I mentioned that an interceptor block can take the same arguments a handler block or a handler method can. Indeed, ordinary handler blocks and methods can also take the subscription as their third argument. However, in a regular handler, knowing the subscription that delivers the announcement is not quite as useful. Asking it about its subscriber is pointless when you are the subscriber, just as telling it to deliver the announcement when it is already in the process of being delivered.
One final note about interceptors is their behavior in case of nesting. Interceptors are additive. If you set up an interceptor on a subscription and then set one up in a nested block, both will run when the subscription attempts to deliver an announcement. This is in line with the overall philosophy that nested suspend and intercept requests are independent and don't affect each other's behavior. One notable consequence of this is if both interceptor blocks do "subscription deliver: announcement from: announcer", the subscriber will get the same announcement twice, once from each of the interceptors. Such is the nature of the beast.
To be continued...