Skip to content

Conversation

reivilibre
Copy link
Contributor

@reivilibre reivilibre commented Jul 9, 2025

Depends-on: #4186, #4306

Rendered

@reivilibre reivilibre force-pushed the rei/msc_ssext_threadsubs branch from c56b003 to 41e8a2c Compare July 9, 2025 11:39
@reivilibre reivilibre changed the title MSC4308: Sliding Sync Extension for Thread Subscriptions MSC4308: Threads Subscriptions extension to Sliding Sync Jul 9, 2025
@reivilibre reivilibre changed the title MSC4308: Threads Subscriptions extension to Sliding Sync MSC4308: Thread Subscriptions extension to Sliding Sync Jul 9, 2025
@reivilibre reivilibre force-pushed the rei/msc_ssext_threadsubs branch from 41e8a2c to 5f22967 Compare July 9, 2025 11:40
@turt2live turt2live added proposal A matrix spec change proposal client-server Client-Server API kind:feature MSC for not-core and not-maintenance stuff needs-implementation This MSC does not have a qualifying implementation for the SCT to review. The MSC cannot enter FCP. labels Jul 9, 2025
```

If two changes occur to the same subscription, only the latter change ever needs
to be sent to the client. \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these on purpose? (applies elsewhere)

Suggested change
to be sent to the client. \
to be sent to the client.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the most sane of the two ways of adding a line break in the more standard variants of Markdown.
(GFM is I think the only notable dialect that doesn't need this)

Comment on lines 75 to 81
// A token that can be used to backpaginate other thread subscription
// changes that occurred since the last sync, but that were not
// included in this response.
//
// Only present when some thread subscriptions have been missed out
// from the response because there are too many of them.
"gap": "OPAQUE_TOKEN"
Copy link
Contributor

@MadLittleMods MadLittleMods Aug 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on prior art, feels like prev_batch/next_batch is the pattern to align with.

Ex.

  • prev_batch from /sync responses are supposed to be used with /messages?from=xxx&to=xxx
  • next_batch from /sync responses are supposed to be used with /sync?since=xxx and /messages?from=xxx&to=xxx
  • pos in Sliding sync responses are supposed to be used with Sliding Sync /sync?since=xxx
  • start/end from /messages responses are supposed to be used with /messages?from=xxx&to=xxx
  • next_batch from /threads responses is supposed to be used with /threads?from=xxx
  • next_token from /notifications responses is supposed to be used with /notifications?from=xxx
  • next_batch from /hierarchy responses is supposed to be used with /hierarchy?from=xxx

Overall, I find our API surface a bit confusing on how we've named fields.

We probably do want to include pagination tokens for both sides of the response (prev/next) so clients can fill in the gap from both directions as desired.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, what's the use case for a next token? i.e., in which cases is one interested in getting a thread subscriptions and the ones after it? I feel that we're not going to need this in the short term, and, if we need it, we could also add it when it's actually justified by a use case. So, as a wish to keep the API as minimal as it can be, I'd be in favor of not adding support for next as well as a direction parameter, unless it's strongly motivated by a real-world use case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also rather not add a 'forward' direction. Maybe we can add the dir=b parameter now and require it to always point backwards.

I am struggling to line up the semantics I want here with the existing API patterns, which is namely tracking and filling a gap (i.e. there are bounds on both sides).

The clients should not really care about the direction of filling, because the time of a subscription is not really important, just the latest state of it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have changed the /thread_subscriptions API to use from=xxx&to=xxx&dir=b, but wondering how we should pass back the pair of tokens from Sliding Sync? I've added a new proposal for that but I'm not sure it's the most satisfying (though couldn't see endpoints that do this already for double-ended bounds)

### Companion endpoint for backpaginating thread subscription changes

```
GET /_matrix/v1/thread_subscriptions
Copy link
Contributor

@MadLittleMods MadLittleMods Aug 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love the bulk endpoint 👍 but is there also a way to get thread_subscriptions for a single room?

I can definitely picture some UI which shows the subscribed threads in a room and it would suck to have to wait for the whole room list to paginate in before you could see the list for the single room you're staring at.

The dumb-simple solution is a filter parameter for this endpoint instead of a separate endpoint.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There isn't, but we expect clients to be needing to cache this information locally anyway, so it'd cover a very tiny surface (i.e. during the time we haven't managed to fully sync yet) on a UI I'm not actually sure anyone would implement (rather than 'list of my subscriptions in this room in time order of the subscription', wouldn't you instead be interested in 'list of most recent threads in this room, filtered by whether I'm subscribed').

So I kinda felt it was not worth adding extra API surface here given I don't have a use case in mind for it. If someone disagrees this could easily be rectified in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking on this, this also means servers need to add indices for this and optimise a bit for it. It's not really for free. And if we don't think of a valid use for it, I guess that's actually harmful

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I come back to my desktop after a week of working on my phone on a holiday, my desktop client is going to need to catch up on the threads I'm subscribed to. If I click into a room and open a threads panel, I will be waiting until my clients catches up on everything whereas it could use the dedicated room endpoint to get everything directly ahead of the entire list on my account.

"filtered by whether I'm subscribed" is the key here. I need to know whether some threads are subscribed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I see what you'd mean, I'd kinda like a client dev who is implementing this to provide some kind of opinion though on what would work for them.

An ugly but maybe-valid workaround in that case could be to backpaginate the threads in general and check the subscriptions one-by-one manually. Given HTTP pipelining and multiplexing, there are probably worse crimes.
I think more effective would be to inline the subscription directly in the APIs where that makes sense. e.g. filtering the threads to only subscribed ones, directly in /threads, or at least returning the state there.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I come back to my desktop after a week of working on my phone on a holiday, my desktop client is going to need to catch up on the threads I'm subscribed to. If I click into a room and open a threads panel, I will be waiting until my clients catches up on everything whereas it could use the dedicated room endpoint to get everything directly ahead of the entire list on my account.

Remember how thread subscriptions are entirely controlled by clients, and never created by the server? If you came back after a holiday and didn't open any client for a week, then you won't have any new subscriptions, by definition. Then, your client would catch up on the threads (not the thread subscriptions), using either automatic back-pagination for all the threads (bleh) or a new threads catchup mechanism (yay), and then only would it recompute new subscriptions for the threads that have been active during your holiday.

If other devices have sync'd and did subscribe you to threads: you'd receive the updates from the sliding sync extension, and then if there were too many of them, you'd use the global endpoints.

Having a room-specific endpoint might likely make the second case above slightly faster (if the other device subscribed to many other threads on many other rooms). But if this endpoint's processing speed is Good Enough™ (viz. not blocked on network/federation), I imagine that it would be sufficient to have only the global feed, for the time being. Does it make sense?

An ugly but maybe-valid workaround in that case could be to backpaginate the threads in general and check the subscriptions one-by-one manually. Given HTTP pipelining and multiplexing, there are probably worse crimes.

For what it's worth, I did experiment with backpaginating all of a room's threads, when implementing a dummy activity center in the SDK, and this was way too slow for a room like MatrixHQ (of course this is the extreme edge case).

I think more effective would be to inline the subscription directly in the APIs where that makes sense. e.g. filtering the threads to only subscribed ones, directly in /threads, or at least returning the state there.

Maybe this could be part of the bundled thread summary, for thread roots, next to the current_user_participated which really thread subscriptions are a generalization of?

Also: note that filtering to only subscribed ones wouldn't give you the information that you may have unsubscribed from another device in the meanwhile.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps the bulk endpoint should work like this:

We could implement a bulk /messages endpoint, where the client would specify multiple rooms and prev_batch tokens.

-- MSC4186

And this also gets around the problem where you can't get to the thread subscriptions that you care about without paginating through the bulk. This way, you can specify the rooms you want to get more thread subscriptions in.

Not sure how fetching thread subscriptions across all rooms would look like yet (many options).

Also being explored in element-hq/synapse#19005 (comment)

GET /_matrix/v1/thread_subscriptions
```

URL parameters:
Copy link
Contributor

@MadLittleMods MadLittleMods Aug 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per my other comment,

We probably do want to include pagination tokens for both sides of the response (prev/next) so clients can fill in the gap from both directions as desired.

This would also require a dir direction parameter. I'm not sure it's necessary to refactor to a from/to pattern like /messages but maybe more obvious if they behave the same.

We should also clarify the token bounds. Synapse reference for other places:

Copy link
Member

@bnjbvr bnjbvr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I like this updated version a lot!

// Returns a limited window of changes to thread subscriptions
// Only the latest changes are returned in this window.
"thread_subscriptions": {
"changed": {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, do you have ideas for other sibling fields at this level? Or is this for symmetry with other response subparts?

(The one idea that comes to mind is that the request/response parts could be a subset of a new threads field, something like:

 "threads": {
    "subscriptions": {
      "!roomId1:example.org": { ... }
    }
  }

)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other than pos which now exists (although might be renamed, c.f. the other thread), the idea was to not block off extensibility; i.e. if we just flattened this struct then we would have no avenue to extend this Sliding Sync extension.

threads -> subscriptions might work, but by the same token, the extension is called threads_subscriptions, so it is weird for the answer to not match the extension name (and I think the Sliding Sync MSC actually says they should match). It would still have the same extensibility problem :/

Comment on lines 128 to 129
- `to` (string, optional): a token used to limit the backpagination \
The token can be acquired from a Sliding Sync response.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, the wording is confusing here as it's talking about the sliding sync response. We might want to precise here again that it's the pos value from the latest request?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried (and maybe failed) to improve with 4d88858

Comment on lines +99 to +101
// Optional. Only present when some thread subscriptions have been
// missed out from the response because there are too many of them.
"prev_batch": "OPAQUE_TOKEN"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a client behavior section defining that ideally the thread subscriptions would all be persisted on the client's side, to avoid having to call into the GET endpoint for individual thread subscriptions when computing notifications locally (notably for e2ee rooms)? The "ideally" in my sentence suggests this could be worded as a "MAY".

Then I guess, to implement this optimization correctly, a client would have to remember if it's synchronized all threads subscriptions yet, with the following assumptions:

  • when a SSS connection has been reset (via a new pos), a client should assume it doesn't know about all the thread subscriptions yet (set an hypothetical boolean known_thread_subscriptions to false)
  • after receiving a response and paginating all the previous unknown thread subscriptions, the client should assume it does know with certainty about all the thread subscriptions (set known_thread_subscriptions to true)
  • when getting a thread subscription: if !known_thread_subscriptions, a GET request should be sent to figure whether a thread has a subscription or not; otherwise, the state synchronized locally can be used.

Comment on lines +184 to +186
The `bump_stamp`s within each thread subscription can be used for determining which
state is latest, for example when a concurrent `/thread_subscriptions` backpagination request
and `/sync` request both return information about the same thread subscription.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For what it's worth, my implementation will likely now store all the bumpstamps in the database (so as to be able to order the latest sub vs unsub, notably). This creates a small discrepancy, when using the endpoints introduced from MSC4306:

  • when we use the PUT or DELETE endpoints, we don't receive a bump_stamp. In this case, it's expected that the bumpstamp will come with the remote echo from the next sync. If we want to implement "local echo" behvaior when using the PUT or DELETE endpoints, then we'd need to store the latest status (subscribed/unsubscribed) in the database, and we wouldn't have a new bumpstamp. I think that in this case, it might be sufficient to not update the bumpstamp.
  • When using the GET endpoint, we don't receive a bumpstamp, but that's fine: it's supposed to be the latest version anyways. Ideally, we'd cache the result in database too, here. And maybe we can perform the same hack as indicated above (keep the previous bumpstamp stored in DB, and only update the subscription status, if it's changed).

TL;DR: adding bump_stamp to the MSC4306 endpoints might be better for API consistency, but not sure it's achieving anything useful, considering the hack described above.

// in the response.
// Defaults to 100.
// Servers may impose a smaller limit than what is requested here.
"limit": 100,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • it'd be nice to add the enabled: boolean here too, for consistency with other extensions (and because the synapse impl requires it, and I was confused as to why my request wasn't taken into account xD)
  • also i see in the impl that limit is required, but I wonder if it should? I didn't put it first, assuming the server would give me a nice default value.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the enabled flag, sorry for forgetting it!

The limit is now opened as a bug in Synapse.

@reivilibre reivilibre removed the request for review from MadLittleMods September 9, 2025 15:30
reivilibre added a commit to element-hq/synapse that referenced this pull request Sep 11, 2025
…to Sliding Sync when MSC4306 and MSC4186 are enabled. (#18695)

Closes: #18436

Implements:
matrix-org/matrix-spec-proposals#4308

Follows: #18674

Adds an extension to Sliding Sync and a companion
endpoint needed for backpaginating missed thread subscription changes,
as described in MSC4308

---------

Signed-off-by: Olivier 'reivilibre <[email protected]>
Co-authored-by: Andrew Morgan <[email protected]>
@reivilibre reivilibre marked this pull request as ready for review September 16, 2025 15:14
netbsd-srcmastr pushed a commit to NetBSD/pkgsrc that referenced this pull request Oct 8, 2025
# Synapse 1.139.0 (2025-09-30)

## Features

- Add experimental support for [MSC4308: Thread Subscriptions extension to Sliding Sync](matrix-org/matrix-spec-proposals#4308) when [MSC4306: Thread Subscriptions](matrix-org/matrix-spec-proposals#4306) and [MSC4186: Simplified Sliding Sync](matrix-org/matrix-spec-proposals#4186) are enabled. ([\#18695](element-hq/synapse#18695))
- Update push rules for experimental [MSC4306: Thread Subscriptions](matrix-org/matrix-spec-proposals#4306) to follow a newer draft. ([\#18846](element-hq/synapse#18846))
- Add `get_media_upload_limits_for_user` and `on_media_upload_limit_exceeded` module API callbacks to the media repository. ([\#18848](element-hq/synapse#18848))
- Support [MSC4169](matrix-org/matrix-spec-proposals#4169) for backwards-compatible redaction sending using the `/send` endpoint. Contributed by @SpiritCroc @ Beeper. ([\#18898](element-hq/synapse#18898))
- Add an in-memory cache to `_get_e2e_cross_signing_signatures_for_devices` to reduce DB load. ([\#18899](element-hq/synapse#18899))
- Update [MSC4190](matrix-org/matrix-spec-proposals#4190) support to return correct errors and allow appservices to reset cross-signing keys without user-interactive authentication. Contributed by @tulir @ Beeper. ([\#18946](element-hq/synapse#18946))

## Deprecations and Removals

- Remove obsolete and experimental `/sync/e2ee` endpoint. ([\#18583](element-hq/synapse#18583))

# Synapse 1.138.0 (2025-09-09)

## Features

- Support for the stable endpoint and scopes of [MSC3861](matrix-org/matrix-spec-proposals#3861) & co. ([\#18549](element-hq/synapse#18549))
itsoyou pushed a commit to famedly/synapse that referenced this pull request Oct 13, 2025
…to Sliding Sync when MSC4306 and MSC4186 are enabled. (#18695)

Closes: #18436

Implements:
matrix-org/matrix-spec-proposals#4308

Follows: #18674

Adds an extension to Sliding Sync and a companion
endpoint needed for backpaginating missed thread subscription changes,
as described in MSC4308

---------

Signed-off-by: Olivier 'reivilibre <[email protected]>
Co-authored-by: Andrew Morgan <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

client-server Client-Server API kind:feature MSC for not-core and not-maintenance stuff needs-implementation This MSC does not have a qualifying implementation for the SCT to review. The MSC cannot enter FCP. proposal A matrix spec change proposal

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants