Add optimistic sync updates in apps with real-time push (like WebSocket etc) for non-blocking operations where only the final value matters and rapid changes coalesce
This skill adds optimisticSyncWithPush for real-time apps that receive server-pushed updates via WebSockets, Firebase, or SSE.
Implements optimistic updates for apps with:
Use optimisticSyncWithPush when:
Use optimisticSync instead if you don't have server-pushed updates.
| Feature | optimisticSync | optimisticSyncWithPush | |---------|----------------|------------------------| | Server pushes | Not supported | Full support | | Multi-device | Single device | Multiple devices | | Follow-up detection | Value comparison | Revision numbers | | Complexity | Lower | Higher |
Your server must:
localRevision and deviceId from clientsserverRevision, localRevision, deviceIdYour state needs to track server revisions:
class ItemState {
final Map<String, Item> items;
final Map<Object, int> revisions; // Track revisions per key
const ItemState({
this.items = const {},
this.revisions = const {},
});
ItemState copyWith({
Map<String, Item>? items,
Map<Object, int>? revisions,
}) => ItemState(
items: items ?? this.items,
revisions: revisions ?? this.revisions,
);
}
import 'package:bloc_superpowers/bloc_superpowers.dart';
class ItemCubit extends Cubit<ItemState> {
ItemCubit() : super(const ItemState());
void toggleLike(String itemId) => optimisticSyncWithPush<bool>(
// Key must match serverPush() key
key: ('toggleLike', itemId),
// Return the value to apply optimistically
valueToApply: () => !state.items[itemId]!.isLiked,
// Apply optimistic value to state
applyOptimisticValueToState: (state, isLiked) => state.copyWith(
items: {
...state.items,
itemId: state.items[itemId]!.copyWith(isLiked: isLiked),
},
),
// Extract current value from state
getValueFromState: (state) => state.items[itemId]!.isLiked,
// Get stored server revision (-1 if unknown)
getServerRevisionFromState: (state) =>
state.revisions[('toggleLike', itemId)] ?? -1,
// Send to server - MUST call informServerRevision()
sendValueToServer: (
isLiked,
localRevision,
deviceId,
informServerRevision,
) async {
final response = await api.setLiked(
itemId: itemId,
isLiked: isLiked,
localRevision: localRevision,
deviceId: deviceId,
);
// CRITICAL: Must call this with server's revision
informServerRevision(response.serverRevision);
return response.isLiked; // Return confirmed value
},
// Apply server response when stable
applyServerResponseToState: (state, serverResponse) {
final confirmedLiked = serverResponse as bool;
return state.copyWith(
items: {
...state.items,
itemId: state.items[itemId]!.copyWith(isLiked: confirmedLiked),
},
);
},
);
}
When your app receives a push from the server (WebSocket message, Firebase update, etc.):
class ItemCubit extends Cubit<ItemState> {
// Called when receiving server push
void handleLikePush(PushData data) => serverPush<bool>(
// Key must match optimisticSyncWithPush key
key: ('toggleLike', data.itemId),
// Push metadata from server
pushMetadata: (
data.serverRevision, // Server's revision number
data.localRevision, // Client's revision (echoed back)
data.deviceId, // Originating device ID
),
// Get stored revision
getServerRevisionFromState: (state) =>
state.revisions[('toggleLike', data.itemId)] ?? -1,
// Apply push and store new revision
applyServerPushToState: (state, serverRevision) => state.copyWith(
items: {
...state.items,
data.itemId: state.items[data.itemId]!.copyWith(isLiked: data.isLiked),
},
revisions: {
...state.revisions,
('toggleLike', data.itemId): serverRevision,
},
),
);
}
| Parameter | Purpose |
|-----------|---------|
| key | Unique identifier (must match serverPush key) |
| valueToApply | Returns optimistic value |
| applyOptimisticValueToState | Applies value to state |
| getValueFromState | Extracts current value |
| getServerRevisionFromState | Returns stored revision (-1 if unknown) |
| sendValueToServer | Sends to server, must call informServerRevision() |
| applyServerResponseToState | Applies server response when stable |
| Parameter | Purpose |
|-----------|---------|
| key | Must match optimisticSyncWithPush key |
| pushMetadata | Tuple: (serverRevision, localRevision, deviceId) |
| getServerRevisionFromState | Returns stored revision |
| applyServerPushToState | Applies push and stores revision |
The sendValueToServer callback MUST call informServerRevision() with the server's revision. Failing to call this throws a StateError.
sendValueToServer: (value, localRevision, deviceId, informServerRevision) async {
final response = await api.update(value, localRevision, deviceId);
// REQUIRED - must call this!
informServerRevision(response.serverRevision);
return response.confirmedValue;
},
Use onFinish to recover from errors:
void toggleLike(String itemId) => optimisticSyncWithPush<bool>(
key: ('toggleLike', itemId),
// ... required params ...
// Called when sync completes (success or failure)
onFinish: (error) async {
if (error != null) {
// Reload from server on error
final item = await api.getItem(itemId);
return state.copyWith(
items: {...state.items, itemId: item},
);
}
return null; // No state change on success
},
);
By default, a random device ID is generated per app run. To persist across sessions:
void main() {
// Set custom device ID generator
optimisticSyncWithPushDeviceId = () => getPersistedDeviceId();
runApp(MyApp());
}
// State
class ItemState {
final Map<String, Item> items;
final Map<Object, int> revisions;
const ItemState({
this.items = const {},
this.revisions = const {},
});
ItemState copyWith({
Map<String, Item>? items,
Map<Object, int>? revisions,
}) => ItemState(
items: items ?? this.items,
revisions: revisions ?? this.revisions,
);
}
// Cubit
class ItemCubit extends Cubit<ItemState> {
final WebSocketService _ws;
ItemCubit(this._ws) : super(const ItemState()) {
// Listen for server pushes
_ws.onLikeUpdate.listen(_handleLikePush);
}
// User action
void toggleLike(String itemId) => optimisticSyncWithPush<bool>(
key: ('like', itemId),
valueToApply: () => !state.items[itemId]!.isLiked,
applyOptimisticValueToState: (state, isLiked) => state.copyWith(
items: {
...state.items,
itemId: state.items[itemId]!.copyWith(isLiked: isLiked),
},
),
getValueFromState: (state) => state.items[itemId]!.isLiked,
getServerRevisionFromState: (state) =>
state.revisions[('like', itemId)] ?? -1,
sendValueToServer: (isLiked, localRev, deviceId, informRev) async {
final resp = await api.setLiked(itemId, isLiked, localRev, deviceId);
informRev(resp.revision); // CRITICAL
return resp.isLiked;
},
applyServerResponseToState: (state, isLiked) => state.copyWith(
items: {
...state.items,
itemId: state.items[itemId]!.copyWith(isLiked: isLiked as bool),
},
),
);
// Server push handler
void _handleLikePush(LikePushData data) => serverPush<bool>(
key: ('like', data.itemId),
pushMetadata: (data.serverRev, data.localRev, data.deviceId),
getServerRevisionFromState: (state) =>
state.revisions[('like', data.itemId)] ?? -1,
applyServerPushToState: (state, serverRev) => state.copyWith(
items: {
...state.items,
data.itemId: state.items[data.itemId]!.copyWith(isLiked: data.isLiked),
},
revisions: {
...state.revisions,
('like', data.itemId): serverRev,
},
),
);
}
Ask the user:
npx skills add marcglasberg/add-optimistic-sync-with-push下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer