Skip to content

Commit 6dd93e8

Browse files
committed
feat(firebase_push_notification_client): improve batch processing and result handling
- Refactor sendNotification and sendBulkNotifications to return PushNotificationResult - Enhance _sendBatch to provide detailed success/failure information - Log invalid tokens at info level for cleanup purposes - Reduce log spam by downgrading some log levels - Simplify error handling and reporting
1 parent e426ab5 commit 6dd93e8

File tree

1 file changed

+67
-66
lines changed

1 file changed

+67
-66
lines changed

lib/src/services/firebase_push_notification_client.dart

Lines changed: 67 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -18,37 +18,39 @@ class FirebasePushNotificationClient implements IPushNotificationClient {
1818
required this.projectId,
1919
required HttpClient httpClient,
2020
required Logger log,
21-
}) : _httpClient = httpClient,
22-
_log = log;
21+
}) : _httpClient = httpClient,
22+
_log = log;
2323

2424
/// The Firebase Project ID for push notifications.
2525
final String projectId;
2626
final HttpClient _httpClient;
2727
final Logger _log;
2828

2929
@override
30-
Future<void> sendNotification({
30+
Future<PushNotificationResult> sendNotification({
3131
required String deviceToken,
3232
required PushNotificationPayload payload,
33-
}) async {
33+
}) {
3434
// For consistency, the single send method now delegates to the bulk
3535
// method with a list containing just one token.
36-
await sendBulkNotifications(
36+
return sendBulkNotifications(
3737
deviceTokens: [deviceToken],
3838
payload: payload,
3939
);
4040
}
4141

4242
@override
43-
Future<void> sendBulkNotifications({
43+
Future<PushNotificationResult> sendBulkNotifications({
4444
required List<String> deviceTokens,
4545
required PushNotificationPayload payload,
4646
}) async {
4747
if (deviceTokens.isEmpty) {
4848
_log.info('No device tokens provided for Firebase bulk send. Aborting.');
49-
return;
49+
return const PushNotificationResult(
50+
sentTokens: [],
51+
failedTokens: [],
52+
);
5053
}
51-
5254
_log.info(
5355
'Sending Firebase bulk notification to ${deviceTokens.length} devices '
5456
'for project "$projectId".',
@@ -57,6 +59,9 @@ class FirebasePushNotificationClient implements IPushNotificationClient {
5759
// The FCM v1 batch API has a limit of 500 messages per request.
5860
// We must chunk the tokens into batches of this size.
5961
const batchSize = 500;
62+
final allSentTokens = <String>[];
63+
final allFailedTokens = <String>[];
64+
6065
for (var i = 0; i < deviceTokens.length; i += batchSize) {
6166
final batch = deviceTokens.sublist(
6267
i,
@@ -66,22 +71,29 @@ class FirebasePushNotificationClient implements IPushNotificationClient {
6671
);
6772

6873
// Send each chunk as a separate batch request.
69-
await _sendBatch(
74+
final batchResult = await _sendBatch(
7075
batchNumber: (i ~/ batchSize) + 1,
7176
totalBatches: (deviceTokens.length / batchSize).ceil(),
7277
deviceTokens: batch,
7378
payload: payload,
7479
);
80+
81+
allSentTokens.addAll(batchResult.sentTokens);
82+
allFailedTokens.addAll(batchResult.failedTokens);
7583
}
84+
85+
return PushNotificationResult(
86+
sentTokens: allSentTokens,
87+
failedTokens: allFailedTokens,
88+
);
7689
}
7790

7891
/// Sends a batch of notifications by dispatching individual requests in
7992
/// parallel.
8093
///
81-
/// This approach is simpler and more robust than using the `batch` endpoint,
82-
/// as it avoids the complexity of constructing a multipart request body and
83-
/// provides clearer error handling for individual message failures.
84-
Future<void> _sendBatch({
94+
/// This method processes the results to distinguish between successful and
95+
/// failed sends, returning a [PushNotificationResult].
96+
Future<PushNotificationResult> _sendBatch({
8597
required int batchNumber,
8698
required int totalBatches,
8799
required List<String> deviceTokens,
@@ -114,63 +126,52 @@ class FirebasePushNotificationClient implements IPushNotificationClient {
114126
return _httpClient.post<void>(url, data: requestBody);
115127
}).toList();
116128

117-
try {
118-
// `eagerError: false` ensures that all futures complete, even if some
119-
// fail. The results list will contain Exception objects for failures.
120-
final results = await Future.wait<dynamic>(
121-
sendFutures,
122-
eagerError: false,
123-
);
129+
// `eagerError: false` ensures that all futures complete, even if some
130+
// fail. The results list will contain Exception objects for failures.
131+
final results = await Future.wait<dynamic>(
132+
sendFutures,
133+
eagerError: false,
134+
);
124135

125-
final failedResults = results.whereType<Exception>().toList();
136+
final sentTokens = <String>[];
137+
final failedTokens = <String>[];
126138

127-
if (failedResults.isEmpty) {
128-
_log.info(
129-
'Successfully sent Firebase batch of ${deviceTokens.length} '
130-
'notifications for project "$projectId".',
131-
);
132-
} else {
133-
_log.warning(
134-
'Batch $batchNumber/$totalBatches: '
135-
'${failedResults.length} of ${deviceTokens.length} Firebase '
136-
'notifications failed to send for project "$projectId".',
137-
);
138-
for (final error in failedResults) {
139-
if (error is HttpException) {
140-
// Downgrade log level for invalid tokens (NotFoundException), which
141-
// is an expected occurrence. Other HTTP errors are still severe.
142-
if (error is NotFoundException) {
143-
_log.info(
144-
'Batch $batchNumber/$totalBatches: Failed to send to an '
145-
'invalid/unregistered token: ${error.message}',
146-
);
147-
} else {
148-
_log.severe(
149-
'Batch $batchNumber/$totalBatches: HTTP error sending '
150-
'Firebase notification: ${error.message}',
151-
error,
152-
);
153-
}
154-
} else {
155-
_log.severe(
156-
'Unexpected error sending Firebase notification.',
157-
error,
158-
);
159-
}
139+
for (var i = 0; i < results.length; i++) {
140+
final result = results[i];
141+
final token = deviceTokens[i];
142+
143+
if (result is Exception) {
144+
failedTokens.add(token);
145+
if (result is NotFoundException) {
146+
// This is an expected failure when a token is unregistered (e.g.,
147+
// app uninstalled). Log it as info for cleanup purposes.
148+
_log.info(
149+
'Batch $batchNumber/$totalBatches: Failed to send to an '
150+
'invalid/unregistered token: ${result.message}',
151+
);
152+
} else if (result is HttpException) {
153+
_log.severe(
154+
'Batch $batchNumber/$totalBatches: HTTP error sending '
155+
'Firebase notification to token "$token": ${result.message}',
156+
result,
157+
);
158+
} else {
159+
_log.severe(
160+
'Unexpected error sending Firebase notification to token "$token".',
161+
result,
162+
);
160163
}
161-
// Throw an exception to indicate that the batch send was not fully successful.
162-
throw OperationFailedException(
163-
'Failed to send ${failedResults.length} Firebase notifications.',
164-
);
164+
} else {
165+
// If there's no exception, the send was successful.
166+
sentTokens.add(token);
165167
}
166-
} catch (e, s) {
167-
_log.severe(
168-
'Unexpected error processing Firebase batch $batchNumber/$totalBatches '
169-
'results.',
170-
e,
171-
s,
172-
);
173-
throw OperationFailedException('Failed to process Firebase batch: $e');
174168
}
169+
_log.info(
170+
'Firebase batch $batchNumber/$totalBatches complete. Success: ${sentTokens.length}, Failed: ${failedTokens.length}.',
171+
);
172+
return PushNotificationResult(
173+
sentTokens: sentTokens,
174+
failedTokens: failedTokens,
175+
);
175176
}
176177
}

0 commit comments

Comments
 (0)