I feel this should be strongly emphasized in the text accompanying the example. Or better yet, the example should be rewritten so it actually exhibits the consistency part of the eventual consistency.
As it currently stands, the specification and the example seem to conflict with each other. Experience has showed me, times and times again, that this exact kind of appearance of conflict in documentation creates confustion and strongly shakes confidence in using or recommending the feature.
Given how many people have strongly argued this point and how many times, I think the feeling is strongly shared.
Please note that this isn't a concern towards the current (or any) implementation at all, but squarely with the wording of the design and specification. However, when the specification is unclear or confusing, devs tend to fallback to poking and proding the implementation. Which is probably, in part, what's happening here as well.
The expectation of observing a property from a type as an AsyncSequence is that multiple iterations of the same sequence from multiple tasks will emit the same values at the same iteration points.
I would instead expect an exception since sequences are not intended to be used twice. Haven't we discussed this before?
One of the sessions of WWDC 25 mentioned this proposal and I wan't to check my understanding on a specific detail.
I've convinced myself that I should always call Observations.init in the same isolation domain where I would later mutate the target Observable.
@Philippe_Hausler can you kindly clarify if this is the supposed way to use the new APIs. If it is, can you add some notes in the documentation (since such requirements cannot be expressed in the type system)?
That is a decent pattern to follow - not doing so would be considerably harder to enforce any sort of consistency. So it isn't impossible to use in another way, you just end up facing other issues with your @Observable type that might be more tricky to reason about; particularly when it comes to the values that are produced from the sequence and if there might be cases where some events are skipped to the eventual changes that occur; e.g. does it create a larger or finer grained transaction?
The one promise that is made; the isolation for the init will be the same isolation as the closure is invoked.
I know I'm (too!) late to the party, but I had cause to look deeper into this recently and I realized a couple things:
As discussed in the ProgressReporter thread, the current implementation has a shortcoming where if withObservationTracking discovers that the emit closure observes no properties (and will therefore never be run again) the sequence does not finish.
As discussed in the ProgressReporter thread, withObservationTracking inherently creates a window where concurrent changes are not tracked. Observation really wants to be able to guarantee that Observable types are either global-actor-isolated or non-Sendable, which the type system can't represent.
withObservationTracking doesn't provide an option to cancel an established observation. This sequence actually mitigates this somewhat because you can cancel the iterating task, but it's still the case that a long-lived object which many people begin observing, but which never changes, leaks memory through an ever-increasing set of observations and lookups.
I don't see how to avoid the latter two issues without a replacement for withObservationTracking that creates a persistent observation (avoiding windows where concurrent mutations aren't tracked), and allows that observation to be explicitly cancelled (freeing up its resources on the observed objects).
as i've found myself somewhat mentally involved with this feature, i'll endeavor to respond to these:
i raised this in the pitch thread, and it was deemed to be 'pretty much a case of bad input yielding bad results', and providing feedback that this may have occurred was deferred as a possible 'future enhancement'. seems like it is a thing that will (and probably does) happen in practice to me though.
i think a kind-of related question was also covered in the linked posts above, and my reading of the response is that it's the clients' responsibility to ensure there's no 'tearing' potential for any Sendable values. IIUC though, the point you surfaced is a bit of a deeper issue with the current implementation of withObservationTracking, in that the access tracking and tracking installation are perhaps not themselves 'transactional' and may miss changes to tracked values that occur in-between the generation of the access information and the installation of the onChange handler. that seems like a problem.
this was also noted weeks ago on the implementation PR, but the PR has now been merged without that seemingly having been addressed.
I guess this could be fixed by having the access method on ObservationRegistrar pull the under-construction observation out of a task-local & install the tracking for that property synchronously before the access. You'd then also have to hold off calling onChange until after closure returns, and have an explicit check when it does for preexisting concurrent mutations.
Fixing this would also mean that mutations of observed properties by the first closure to withObservationTracking result in calls to onChange, which… might be worthy of a run-time warning?
I've thought a few times that the window between calls to withObservationTracking was a problem too, but it isn't — the requirement is that repeated calls to it ensure that the first closure always eventually sees the "final" value of the observed properties. Mutations that occur in the window between calls will be seen on the next call, concurrency doesn't even interfere there.
That's probably the easiest way to handle it, yes.
Right. You can completely tear down your observation state and rebuild it from scratch during a subsequent call to withObservationTracking, and that's fine; at worst it results in updates being coalesced, which is permitted (and unavoidable).