Slackbot
02/25/2026, 8:16 AMshikasd
03/01/2026, 1:00 PMSnapshotStateObserver#clear is called.shikasd
03/02/2026, 11:32 AMZach Klippenstein (he/him) [MOD]
03/02/2026, 6:20 PMChuck Jazdzewski [G]
03/04/2026, 7:19 PMadvance { which is the call that advances the snapshot. Advancing the snapshot is a rather trivial operation that changes ID to the highest snapshot number and ensures that all open snapshots prior to it are explicitly excluded. It does this when a new snapshot is taken to ensure that the child snapshot will see all changes made to the parent at up to the point of the snapshot but not after.
For (2), the code you are looking for is the if statement you quote. The parent doesn't need to advance if changes in the parent are already not visible to the child.
Snapshots is an implement of the MVCC algorithm. An explanation of the underlying principle behind the snapshot system can be found here https://en.wikipedia.org/wiki/Multiversion_concurrency_control. The comments do not explain this algorithm as it is fairly well-known and external sources explain it better than I could. This doesn't cover some of the details around nested transactions (which I call snapshots as snapshots are not durable) but @Zach Klippenstein (he/him) [MOD] explains this better than I could in his talks and blog posts.
For (3), this is to implement `SnapshotMutationPolicy`'s merge method which can be used to implement conflict free data types. By default, any modification to a value is considered conflicting with the same value modified in another snapshot. Through a mutation policy you can define that two snapshot that change the value can be merged. This can result in a third value that includes the changes made by both snapshots. I give an example for this in the documentation regarding the count of a set of physical items where snapshot A and snapshot B can both consume or produce a physical items without conflict. You can learn more about this here (https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type). Any CFRD an be implemented by this mechanism as well as non-replicating conflict free values such a count of resources.
For why (3) needs to advance the snapshot here is because, taking a count as an example, if you decrement the count in both A and B, merging them means adjusting the value by the sum of the differences (e.g. if foo is a mergable count then foo = foo - 2 in A and foo = foo - 3 in B would merge to produce foo = foo - 5. If foo starts at 10 then A sees `8`and B sees 7 but the merged value is 5.) Since neither A or B has seen the new value (5 in the example), a new snapshot ID is required to contain the merged value. Advancing the snapshot allocates this new snapshot.Chuck Jazdzewski [G]
03/05/2026, 5:00 PMChuck Jazdzewski [G]
03/09/2026, 6:29 PM"...even if I accept that the exact IDs are an implementation detail and can be any values the system sees fit for as long as applying a snapshot guarantees the parent has a larger ID. What are the rules?The rules is as I stated them above. The only thing that maters is what the `valid`function returns.
valid is true when a record is potentially visible to the snapshot. This is currently when the record id (the snapshot a record was created in) is less than or equal to its own snapshot and not explicitly made invalid by being in the `invalid`set. readable returns the newest valid record (i.e. with the highest id) .
Snapshots could be implemented by each snapshot having just an `invalid`set and, as new snapshots are added, they are added to every snapshot's invalid set. Using the snapshot id relative value is effectively the same thing. As new snapshots always have ids greater than previous snapshots new snapshots are effectively added to the set of invalid snapshots of older snapshots. The manipulation of these number is just a trick to avoid having to maintain a list of snapshots and updating them whenever a new one is created. What is important is which record is valid and which is not. How this is determined is not visible through public API.
With this in mind, you must ignore the snapshotId. It is an implementation detail that may be useful when debugging but should never be used in a way that changes the behavior of the application as you don't have enough public information to reason about it. The values returned by a state object is the only contract (e.g. what is returned by readable and writable). What the snapshotId is an at any time is an implementation detail.
The general rule for snapshots are that snapshots see only the values the parent snapshot had at the point the snapshot was taken, all changes that have been explicitly made to them, and all changes from nested snapshots applied into them. They are isolated from all other changes. When a changes is applied to a snapshot, all the changes applied are seen as one atomic change. In other words, snapshots are atomic, consistent and isolated. The guarantees are to the snapshot instances, not to what id they may have at any moment.Chuck Jazdzewski [G]
03/10/2026, 4:56 PM300 (or 30). It's a value that was produced by the merge lambda given to MyIntState. This reflects the merge policy of the type which interprets changes to the value as a delta, not the literal values. In this case writing 200 means increase MyIntState by 200 and a write of 100 means increase it by 100. Therefore a merge of these two requests is an increase of the MyIntState instance to 300 (i.e. 100 + 200). If you remove the merge policy you will get a merge conflict as one snapshot writes 100 and the other writes 200. Only one can succeed. A merge policy allows snapshots that would otherwise collide to produce a merged value instead of failing. This merging happens when the snapshot is applied. Also the statement (4) above about `nested1.apply()`in `write2`is misleading, at least from what I can tell. The value of state is 300 after the `nested1.apply()`which is asserted by `kotlin.test.assertEquals(300, state.read())`above. This means it is consistent the 10, 20 case above it.Chuck Jazdzewski [G]
03/11/2026, 3:28 PMreadable and write observers observes calls to writable. The apply observer observes calls to apply of the global snapshot. The changes show up in a apply observer because it was applied to the global snapshot. They do not show up to a write observer as they were not written to by the snapshot that had the write observer. The merged values were "written" in a synthetic snapshot implied by the merge.Chuck Jazdzewski [G]
03/12/2026, 3:25 PMChuck Jazdzewski [G]
03/12/2026, 3:29 PMChuck Jazdzewski [G]
03/18/2026, 1:31 AMwritable. For example, someIntState.value = 10 will call the write observer immediately. Same with read observers, they are called immediately during readable. Read and write observers are ignored during apply.
Like I said above, write observers and apply observers are two entirely different things. They are inconsistent because they are different. You should only use apply observers when observing for changes. Write observers should be almost never be used. For example, they are needed for the global snapshot manager (to know when to schedule an advance of the global snapshot) but are not used for anything else in Compose.
The pattern is to use a read observer to detect which objects an expression reads and an apply observer to detect when the state objects the expression read change. The write observer should not be used for observation. As you point out, write observers are incomplete for this as they only are notified when a write occurs; not when the object changes. Also they will be notified of write even if the write eventually is abandoned and they are called redundantly (multiple time, once for each write). Write observers are both called to little and to much. Apply observers, on the other hand, will see every object that has changed and only the objects that have changed (e.g. not snapshot that are abandoned) and are called once per non-nested snapshot apply (or global advance).
The snapshot id is only relevant to understand how the snapshot system delivers on the promise of consistent, atomic, and isolated changes. It otherwise should be ignored.Chuck Jazdzewski [G]
03/18/2026, 5:11 PMapply. The test just test if it occurred at all, not when. Your test seems to conflate write and apply observers. As I said, they are different and they are called at completely different times. Write is called while in the snapshot (e.g. in enter()). Apply observers are called after the snapshot applies.
> Are you just trying to say that applying to a nested snapshot is equivalent to writing (ⓒ) all diffs to it before merging (ⓓ)?
No. I write observers are not called during apply at all. Nested snapshot do not call any observers when they are applied.
> Or this in Recomposer.composing?
I missed that one! Sorry about that. That is used to detect writes during composition that may invalidate other parts of composition. I am not sure how I missed that... Write observer allow the snapshot to observe itself which is compose does. Composition uses the write observer observers itself (e.g. changes made during composition). The apply observer observe changes outside composition.Chuck Jazdzewski [G]
03/19/2026, 3:00 PMChuck Jazdzewski [G]
03/23/2026, 3:32 PM