Skip to content

Conversation

laevandus
Copy link
Contributor

@laevandus laevandus commented Sep 16, 2025

🔗 Issue Links

Related to: IOS-1090 & IOS-1122

🎯 Goal

Add centralised event handler for state-layer so that we can use it when receiving WS events and when calling changes manually after API calls.

📝 Summary

  • Add StateLayerEventPublisher which converts WS events to custom and simplified events only for state-layer updates
  • StateLayerEventPublisher allows moving event filtering logic to a single handler running on the background thread
  • StateLayerEventPublisher allows converting event data once and forwarding the final data to all the observers
  • Used StateLayerEventPublisher in Activity and added extensive tests to Activity
  • Tidied many of the dummy functions
  • Added support for parallel API request mocking in tests\
  • Add addReaction and deleteReaction to Activity

🛠 Implementation

Centralised handler allows having filtering in a single handler running on a background thread. Before the filtering was in the WS event observer only which causes issues when change handlers were called directly after API calls. Especially problematic with queries with filters where the change might not match the query anymore.

🎨 Showcase

Add relevant screenshots and/or videos/gifs to easily see what this PR changes, if applicable.

Before After
img img

🧪 Manual Testing Notes

☑️ Contributor Checklist

  • I have signed the Stream CLA (required)
  • This change should be manually QAed
  • Changelog is updated with client-facing changes
  • Changelog is updated with new localization keys
  • New code is covered by unit tests
  • Documentation has been updated in the docs-content repo

Use it in Activity and ActivityState
Cover Activity type with tests
@laevandus laevandus requested a review from a team as a code owner September 16, 2025 08:44
@laevandus laevandus added the ✅ Feature An issue or PR related to a feature label Sep 16, 2025
Comment on lines +99 to +105
if flag {
let response = try await apiClient.pinActivity(feedGroupId: feed.group, feedId: feed.id, activityId: activityId)
return response.activity.toModel()
} else {
let response = try await apiClient.unpinActivity(feedGroupId: feed.group, feedId: feed.id, activityId: activityId)
return response.activity.toModel()
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Found a bug while writing tests, unpin was never called

Copy link

1 Warning
⚠️ Big PR
1 Message
📖 There seems to be app changes but CHANGELOG wasn't modified.
Please include an entry if the PR includes user-facing changes.
You can find it at CHANGELOG.md.

Generated by 🚫 Danger

Copy link

github-actions bot commented Sep 16, 2025

Public Interface

 public final class Activity: Sendable  
-   @discardableResult public func addCommentReaction(commentId: String,request: AddCommentReactionRequest)async throws -> FeedsReactionData
+   @discardableResult public func addReaction(request: AddReactionRequest)async throws -> FeedsReactionData
-   @discardableResult public func deleteCommentReaction(commentId: String,type: String)async throws -> FeedsReactionData
+   @discardableResult public func deleteReaction(type: String)async throws -> FeedsReactionData
-   public func pin()async throws 
+   @discardableResult public func addCommentReaction(commentId: String,request: AddCommentReactionRequest)async throws -> FeedsReactionData
-   public func unpin()async throws 
+   @discardableResult public func deleteCommentReaction(commentId: String,type: String)async throws -> FeedsReactionData
-   @discardableResult public func closePoll()async throws -> PollData
+   public func pin()async throws 
-   public func deletePoll(userId: String? = nil)async throws 
+   public func unpin()async throws 
-   @discardableResult public func getPoll(userId: String? = nil)async throws -> PollData
+   @discardableResult public func closePoll()async throws -> PollData
-   @discardableResult public func updatePollPartial(request: UpdatePollPartialRequest)async throws -> PollData
+   public func deletePoll(userId: String? = nil)async throws 
-   @discardableResult public func updatePoll(request: UpdatePollRequest)async throws -> PollData
+   @discardableResult public func getPoll(userId: String? = nil)async throws -> PollData
-   @discardableResult public func createPollOption(request: CreatePollOptionRequest)async throws -> PollOptionData
+   @discardableResult public func updatePollPartial(request: UpdatePollPartialRequest)async throws -> PollData
-   public func deletePollOption(optionId: String,userId: String? = nil)async throws 
+   @discardableResult public func updatePoll(request: UpdatePollRequest)async throws -> PollData
-   @discardableResult public func getPollOption(optionId: String,userId: String?)async throws -> PollOptionData
+   @discardableResult public func createPollOption(request: CreatePollOptionRequest)async throws -> PollOptionData
-   @discardableResult public func updatePollOption(request: UpdatePollOptionRequest)async throws -> PollOptionData
+   public func deletePollOption(optionId: String,userId: String? = nil)async throws 
-   @discardableResult public func castPollVote(request: CastPollVoteRequest)async throws -> PollVoteData?
+   @discardableResult public func getPollOption(optionId: String,userId: String?)async throws -> PollOptionData
-   @discardableResult public func deletePollVote(voteId: String,userId: String? = nil)async throws -> PollVoteData?
+   @discardableResult public func updatePollOption(request: UpdatePollOptionRequest)async throws -> PollOptionData
+   @discardableResult public func castPollVote(request: CastPollVoteRequest)async throws -> PollVoteData?
+   @discardableResult public func deletePollVote(voteId: String,userId: String? = nil)async throws -> PollVoteData?

Comment on lines +24 to +30
await withTaskGroup(of: Void.self) { group in
for handler in handlers {
group.addTask {
await handler(event)
}
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sure handler runs on a background thread which is good when handler wants to do complex filtering, aka local filter matching

@Stream-SDK-Bot
Copy link
Collaborator

Stream-SDK-Bot commented Sep 16, 2025

SDK Size

title develop branch diff status
StreamFeeds 6.16 MB 6.21 MB +51 KB 🟢

Copy link
Contributor

@martinmitrevski martinmitrevski left a comment

Choose a reason for hiding this comment

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

Looks good, left few comments. Also, I think there are still some bugs in the state layer.

private func subscribe(to publisher: StateLayerEventPublisher) {
eventObserver = publisher.subscribe { [weak self, activityId, feed] event in
switch event {
case .activityUpdated(let activityData, let eventFeedId):
Copy link
Contributor

Choose a reason for hiding this comment

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

seems like we still have a bug with activity reactions - e.g. if the initial state is 1, you react, it becomes 3. Remove reaction, it becomes 1.

Copy link
Contributor

Choose a reason for hiding this comment

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

Bookmarks also still don't work well (own issue, maybe backend?)

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 did a quick fix in this PR since the issue comes from the Feed type. Next PR is going to be about Feed, its event handling and adding tests.

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 was able to reproduce this myself as well and now it does not happen.

}

extension StateLayerEventPublisher {
final class ObservationToken: Sendable {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we call this Subscription? That's what we also use for video.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good! Done

final class ObservationToken: Sendable {
private let action: @Sendable () -> Void

init(action: @escaping @Sendable () -> Void) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Also action is a bit misleading here, maybe we should be more explicit here with the name?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renamed it to cancel

@laevandus laevandus enabled auto-merge (squash) September 16, 2025 12:26
@laevandus laevandus merged commit cfbb649 into develop Sep 16, 2025
5 checks passed
@laevandus laevandus deleted the refactor-state-layer-event-handling-in-activity branch September 16, 2025 12:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
✅ Feature An issue or PR related to a feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants