Skip to content

Subscription Context with Convertors

Convertor Subscription Pattern

Subscription context flows through SubscriptionRequestParams and Ferry’s updateCacheHandlerContext.

Subscriptions often need context for filtering:

  • Chat message subscriptions need current chat ID and user ID
  • Notification subscriptions need the current event/conference ID
  • Presence subscriptions need the current room or channel

Create a serializable context class:

class ChatMessageSubscriptionContext {
final String chatId;
final String currentUserId;
const ChatMessageSubscriptionContext({
required this.chatId,
required this.currentUserId,
});
Map<String, dynamic> toJson() => {
'chatId': chatId,
'currentUserId': currentUserId,
};
factory ChatMessageSubscriptionContext.fromJson(Map<String, dynamic> json) {
return ChatMessageSubscriptionContext(
chatId: json['chatId'] as String,
currentUserId: json['currentUserId'] as String,
);
}
}
@riverpod
Convertor<GSubscribeToChatMessagesReq,
SubscriptionRequestParams<ChatMessageSubscriptionContext>>
chatMessageSubscriptionConvertor(Ref ref) {
return Convertor((params) {
return GSubscribeToChatMessagesReq((builder) {
builder
..requestId = 'chat_message_subscription_${params.context?.chatId}'
..updateCacheHandlerKey = 'messageSubscriptionCacheHandler'
..updateCacheHandlerContext = params.context?.toJson();
});
});
}
@riverpod
StreamConvertor<GSubscribeToChatMessagesData_messageSubscription,
SubscriptionRequestParams<ChatMessageSubscriptionContext>>
chatMessageSubscriptionDatasource(Ref ref) {
return GraphQLRequestExecutor<GSubscribeToChatMessagesData,
SubscriptionRequestParams<ChatMessageSubscriptionContext>,
GSubscribeToChatMessagesVars>(
gqlClient: ref.watch(gqlClientProvider),
convertor: ref.watch(chatMessageSubscriptionConvertorProvider),
)
.then(GraphQLStreamConvertor())
.map((data) => data.messageSubscription);
}
Stream<ChatMessageModel> subscribeToMessages({
required String chatId,
required String currentUserId,
}) {
final context = ChatMessageSubscriptionContext(
chatId: chatId,
currentUserId: currentUserId,
);
final params = SubscriptionRequestParams(context: context);
return subscriptionDatasource
.thenMap(messageModelConvertor)
.execute(params);
}

The CacheHandlerSpecs can access subscription context for intelligent cache updates:

CacheHandlerSpecs.merge(
cacheHandlerKey: 'messageSubscriptionCacheHandler',
mapToCachedRequest: Convertor((subscriptionRequest) {
// Use the subscription context to map to the correct list query
final context = subscriptionRequest.updateCacheHandlerContext;
if (context == null) return null;
final subContext = ChatMessageSubscriptionContext.fromJson(context);
return GQueryChatMessagesReq((b) => b
..requestId = 'chat_messages_${subContext.chatId}'
..vars.G_filter = GmessageFilters((f) => f
..systemParentId = subContext.chatId).toBuilder());
}),
mapResponse: Convertor((data) => data.messageSubscription),
mergeCachedData: Convertor(((oldData, newMessage)) {
final items = oldData.message!.messageItems!.toList();
items.insert(0, newMessage); // Prepend new message
return oldData.rebuild((b) =>
b..message.messageItems.replace(items));
}),
);
  • Subscription contexts must be JSON-serializable (Ferry passes them as Map<String, dynamic>)
  • Use unique requestId per subscription instance (include context identifiers)
  • updateCacheHandlerKey links the subscription to its CacheHandlerSpecs
  • Context filtering happens in the CacheHandlerSpecs, not in the subscription stream