name: add-sequential description: Add sequential queue processing to ensure Cubit method calls execute one after another in order
Add Sequential Queue Processing
This skill queues Cubit method calls and processes them one after another in order.
What This Skill Does
Adds the sequential parameter to a mix() call so that:
- Method calls are queued instead of running concurrently
- Each call waits for the previous one to complete
- All calls execute in the order they were made
Instructions
Step 1: Identify the Method
Ask the user which Cubit method needs sequential processing, or identify methods that:
- Must execute in order (messages, transactions)
- Should not run concurrently (database writes)
- Every call matters and must complete
Step 2: Add Sequential
Add sequential: sequential to the mix() call:
import 'package:bloc_superpowers/bloc_superpowers.dart';
class ChatCubit extends Cubit<ChatState> {
ChatCubit() : super(const ChatState());
void sendMessage(String text) => mix(
key: this,
sequential: sequential, // Add this line
() async {
final message = await api.sendMessage(text);
emit(state.copyWith(
messages: [...state.messages, message],
));
},
);
}
Step 3: Understand the Behavior
With sequential:
- First call: Executes immediately
- Second call while first is running: Queued, waits for first to complete
- Third call: Queued behind second
- All calls execute in order, one at a time
// User sends 3 messages rapidly
cubit.sendMessage('Hello'); // Executes immediately
cubit.sendMessage('How'); // Queued (waits for first)
cubit.sendMessage('Are you?'); // Queued (waits for second)
// Result: Messages sent in order: "Hello", "How", "Are you?"
Configuration Options
Default Parameters
sequential(
key: null, // Uses mix key by default
maxQueueSize: null, // Unlimited queue
queueTimeout: null, // No timeout
dropOldest: false, // Drop newest when queue full
)
Limited Queue Size
Prevent memory issues with large queues:
void processItem(Item item) => mix(
key: this,
sequential: sequential(maxQueueSize: 10),
() async {
await api.process(item);
},
);
When queue is full:
- Default: New calls are dropped
- With
dropOldest: true: Oldest queued call is dropped
Queue Timeout
Discard calls that wait too long:
void sendMessage(String text) => mix(
key: this,
sequential: sequential(queueTimeout: 30.sec),
() async {
await api.sendMessage(text);
},
);
Calls waiting longer than 30 seconds are discarded.
Latest Wins (Keep Only Most Recent)
For operations where only the final value matters:
void updateSettings(Settings settings) => mix(
key: this,
sequential: sequential.latestWins,
() async {
await api.saveSettings(settings);
},
);
sequential.latestWins is shorthand for:
sequential(maxQueueSize: 1, dropOldest: true)
Per-Item Queues with Custom Key
Different items can have separate queues:
void sendMessage(String chatId, String text) => mix(
key: this,
sequential: sequential(key: (ChatCubit, chatId)),
() async {
await api.sendMessage(chatId, text);
},
);
With this setup:
- Messages to chat A queue together
- Messages to chat B queue together
- Messages to different chats can run concurrently
Common Patterns
Order Processing
void processOrder(Order order) => mix(
key: this,
sequential: sequential,
() async {
await api.processOrder(order);
emit(state.copyWith(
processedOrders: [...state.processedOrders, order],
));
},
);
Message Sending
void sendMessage(String text) => mix(
key: this,
sequential: sequential(queueTimeout: 30.sec),
retry: retry,
() async {
final message = await api.sendMessage(text);
emit(state.copyWith(messages: [...state.messages, message]));
},
);
Per-Chat Message Queue
void sendMessage(String chatId, String text) => mix(
key: (SendMessage, chatId),
sequential: sequential, // Inherits key from mix
() async {
await api.sendMessage(chatId, text);
},
);
Settings Save (Latest Wins)
void saveSettings(Settings settings) => mix(
key: this,
sequential: sequential.latestWins,
() async {
await api.saveSettings(settings);
emit(settings);
},
);
Accessing Queue Context
Use mix.ctx to access queue information:
void processItem(Item item) => mix.ctx(
key: this,
sequential: sequential,
(ctx) async {
final wasQueued = ctx.sequential!.wasQueued; // Was this call queued?
final index = ctx.sequential!.index; // Position in queue
if (wasQueued) {
print('Processing queued item at position $index');
}
await api.process(item);
},
);
Sequential vs Non-Reentrant
| Feature | Duplicate Calls | Use Case |
|---|---|---|
sequential |
Queued and executed in order | Every call matters |
nonReentrant |
Dropped/ignored | Only first call matters |
Use sequential for:
- Sending messages
- Processing orders
- Database mutations
- Any operation where every call must execute
Use nonReentrant for:
- Loading data
- Refresh operations
- Idempotent actions
Complete Example
class OrderCubit extends Cubit<OrderState> {
OrderCubit() : super(const OrderState());
// Queue all orders for sequential processing
void submitOrder(Order order) => mix(
key: this,
sequential: sequential(
maxQueueSize: 100,
queueTimeout: 60.sec,
),
retry: retry,
() async {
emit(state.copyWith(processingOrder: order));
final result = await api.submitOrder(order);
emit(state.copyWith(
processingOrder: null,
completedOrders: [...state.completedOrders, result],
));
},
);
}
User Preferences
Ask the user:
- Should there be a queue limit? (to prevent memory issues)
- Should calls timeout? (to discard stale requests)
- Latest wins? (only final value matters)
- Per-item queues? (separate queues by ID)