Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for timeline streaming via server-sent events #103

Open
bensyverson opened this issue Mar 6, 2023 · 9 comments
Open

Support for timeline streaming via server-sent events #103

bensyverson opened this issue Mar 6, 2023 · 9 comments
Labels
enhancement New feature or request
Milestone

Comments

@bensyverson
Copy link

Hi all, based on the names of some of the API methods, I mistakenly thought TootSDK already supported Mastodon's streaming API:

for await posts in try await client.data.stream(.timeLineHome) {
  ...
}

So I was super confused when I used TootSDK and it did not stream new posts. Digging into the code, it looks like "stream" refers to a Swift AsyncStream. And rather than calling the streaming endpoint for the user's home timeline (GET /api/v1/streaming/use), TootSDK calls the single-use endpoint (GET /api/v1/timelines/home).

For my purposes it's fine to periodically call .refresh(), but at some point it would be amazing to have a real async stream for a long-lived HTTP request or websocket.

Thanks for pulling together this SDK—it's nice!

@kkostov
Copy link
Contributor

kkostov commented Mar 6, 2023

Thanks for reporting this @bensyverson , that's a very cool insight.

Indeed, the streaming consists of facilitating the loading of different timelines. The TootStream is something we want to improve and this is an excellent venue to explore. There are challenges, of course, e.g. server-sent events are Mastodon-specific and not always available for other fediverse projects (or different projects implement them differently), so our solution should continue to allow for alternatives that can take over "under the hood".

@Tunous
Copy link
Contributor

Tunous commented Dec 17, 2023

Hey @kkostov, @davidgarywood I'm planning to use streaming endpoint to observe notifications https://docs.joinmastodon.org/methods/streaming/#notification and was wondering on how to add this to the sdk.

Did you think about this topic previously and have any ideas on how it could be integrated. Should it be added to existing streams? Or something different?

@davidgarywood
Copy link
Contributor

I had planned to come back and integrate the mastodon streaming api under our data streams one, but hadn’t thought too deeply about it as it was in our kinda post 1.0 list. Well, we’re post 1.0 now 😃

I think, I’d love to see a draft PR with a new object, separate from the existing streams, and to then take a step back and see if they can be integrated easily or not.

It’ll either obviously slot in, or it’ll clearly need to be separate - but that shouldn’t be anyone’s concern when first pulling it together !

@kkostov
Copy link
Contributor

kkostov commented Dec 17, 2023

@Tunous are you using the current streams implementation in production?

@Tunous
Copy link
Contributor

Tunous commented Dec 17, 2023

@kkostov No, my app deals with timelines in non-standard way and the current streams implementation didn't work for my needs. I'm using TootClient.getTimeline(_:pageInfo:limit:) directly.

@kkostov
Copy link
Contributor

kkostov commented Dec 17, 2023

Thanks @Tunous, I think Dave's summary is spot on as well. We could try to fit the implementation as an extension to Streams as we have several useful concepts that overlap.

  • timelines is already defined. It also allows us to transparently switch existing timelines to server-sent events (where the arguments match exactly) and offer additional timeline types which correspond to various endpoints that support server-sent events.
  • We'd define new timelines (or reuse existing ones when there is full overlap) that correspond to https://docs.joinmastodon.org/methods/streaming/#streams
  • This can be a totally separate (independent) part of TootDataStream or a new TootEventStream.
  • Using AsyncStream for results - unless we have a better structure to replace this, we can be inspired by this mechanism for server-sent items to be made available to consumers (e.g. a consumer would listen for events of interest e.g. for await events in try await client.data.stream([Timeline.local, Timeline.notifications, ...]).
  • Implementing AsyncStream can be a second step, we can also try to get it to work with good-old callback handlers.

Mastodon offers two ways to subscribe to events, HTTP and WebSockets. I believe WebSockets is the one supported by most flavours, and this may require some research on how to best implement so we can support all target platforms (I'm mostly concerned about Linux and watchOS/tvOS/visionOS). I quickly searched and found for example https://github.com/vapor/websocket-kit or https://github.com/launchdarkly/swift-eventsource (if their license is compatible).

Perhaps it will be worth adding an abstraction on top of that layer so later we can add the second protocol as a fallback.

@kkostov
Copy link
Contributor

kkostov commented Dec 17, 2023

For every message (or event 🤔 - I think it's better to call them events?), I suggest that we stick to a struct which looks something like this:

TootServerEvent<TootServerUpdate> {
  // the timeline which produced this event
  let timeline: Timeline 
  let update: TootServerUpdate
}

Here timeline is something we will need to determine after receiving the message so consumers have an easier time distinguishing where the message is coming from.

for await event in try await client.data.stream([Timeline.local, Timeline.notifications, ...]) {
  switch event.timeline {
    case .local: ...
    case .notifications: ...
    case ...
  }
}

TootServerUpdate is a stream specific struct which describes the fields we're getting. Similar to other models, it should take into account various flavours (but let's start with Mastodon). Field names will follow the same naming guidelines (e.g. status -> post)

How do you feel about this @Tunous @davidgarywood?

@kkostov kkostov added this to the 2.0 milestone Dec 17, 2023
@Tunous
Copy link
Contributor

Tunous commented Dec 18, 2023

@kkostov API like this looks good to me, especially as an async stream.

There is one thing worth considering which could affect the implementation. What would happen if someone activated multiple streams at the same time. For example to handle home timeline events and different one for notifications. These could be called from different parts of the app.

SDK could activate different connections per stream but I believe a better option would be to have one connection per TootClient. The implementation should be able to register multiple streams, deliver events to correct one and disconnect only when all streams finish.

@kkostov
Copy link
Contributor

kkostov commented Dec 18, 2023

@Tunous that's a good question indeed and I totally agree with your suggestion - a single TootClient instance should maintain a single connection per stream/category (https://docs.joinmastodon.org/methods/streaming/#streams).

  • if a consumer requests a given timeline twice, we'd only have 1 client-server connection to that category (while notifying the delegate twice).
  • if the consumer requests several timelines that correspond to several categories, we'd initiate a request for each one which is not already being "watched".
  • TootClient keeps track of currently connected categories and dispatches updates to each corresponding timeline subscription.
  • Having open connections does not prevent TootClient from being disposed.
  • On deinit we will try to gracefully close each connection.
  • Resuming connections is something we will need to investigate. For example, if an app remains backgrounded for a long period of time, there is no guarantee that the server will maintain an open connection (Server-sent events) or open web socket (WebSockets). We may have to offer an api method to be called by the consuming app when we need to "resume/reconnect" (e.g. UIApplication.willEnterForegroundNotification but I would try to avoid having this inside of the library)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

4 participants