Skip to content

Commit a0a02f2

Browse files
authored
Implement sendEach and sendEachForMulticast (#2097)
`sendEach` vs `sendAll` 1. `sendEach` sends one HTTP request to V1 Send endpoint for each message in the array. `sendAll` sends only one HTTP request to V1 Batch Send endpoint to send all messages in the array. 2. `sendEach` calls `Promise.allSettled` to wait for all `httpClient.send` calls to complete and construct a `BatchResponse`. An `httpClient.send` call to V1 Send endpoint either completes with a success or throws an error. So if an error is thrown out, the error will be caught in `sendEach` and turned into a `SendResponse` with an error. Therefore, unlike `sendAll`, `sendEach` does not always throw an error for a total failure. It can also return a `BatchResponse` with only errors in it. `sendEachForMulticast` calls `sendEach` under the hood.
1 parent 2725f8c commit a0a02f2

File tree

3 files changed

+651
-27
lines changed

3 files changed

+651
-27
lines changed

src/messaging/messaging-api-request-internal.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,34 @@ export class FirebaseMessagingRequestHandler {
9696
});
9797
}
9898

99+
/**
100+
* Invokes the request handler with the provided request data.
101+
*
102+
* @param host - The host to which to send the request.
103+
* @param path - The path to which to send the request.
104+
* @param requestData - The request data.
105+
* @returns A promise that resolves with the {@link SendResponse}.
106+
*/
107+
public invokeRequestHandlerForSendResponse(host: string, path: string, requestData: object): Promise<SendResponse> {
108+
const request: HttpRequestConfig = {
109+
method: FIREBASE_MESSAGING_HTTP_METHOD,
110+
url: `https://${host}${path}`,
111+
data: requestData,
112+
headers: LEGACY_FIREBASE_MESSAGING_HEADERS,
113+
timeout: FIREBASE_MESSAGING_TIMEOUT,
114+
};
115+
return this.httpClient.send(request).then((response) => {
116+
return this.buildSendResponse(response);
117+
})
118+
.catch((err) => {
119+
if (err instanceof HttpError) {
120+
return this.buildSendResponseFromError(err);
121+
}
122+
// Re-throw the error if it already has the proper format.
123+
throw err;
124+
});
125+
}
126+
99127
/**
100128
* Sends the given array of sub requests as a single batch to FCM, and parses the result into
101129
* a BatchResponse object.
@@ -136,4 +164,11 @@ export class FirebaseMessagingRequestHandler {
136164
}
137165
return result;
138166
}
167+
168+
private buildSendResponseFromError(err: HttpError): SendResponse {
169+
return {
170+
success: false,
171+
error: createFirebaseError(err)
172+
};
173+
}
139174
}

src/messaging/messaging.ts

Lines changed: 86 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
MessagingConditionResponse,
4040
DataMessagePayload,
4141
NotificationMessagePayload,
42+
SendResponse,
4243
} from './messaging-api';
4344

4445
// FCM endpoints
@@ -250,17 +251,16 @@ export class Messaging {
250251
});
251252
}
252253

253-
// TODO: Update the comment based on the implementation
254254
/**
255255
* Sends each message in the given array via Firebase Cloud Messaging.
256256
*
257-
* Unlike {@link Messaging.sendAll}, this method makes a single RPC call for each message in the given array.
257+
* Unlike {@link Messaging.sendAll}, this method makes a single RPC call for each message
258+
* in the given array.
258259
*
259-
* The responses list obtained from the return value
260-
* corresponds to the order of `messages`. An error
261-
* from this method indicates a total failure -- i.e. none of the messages in
262-
* the list could be sent. Partial failures are indicated by a `BatchResponse`
263-
* return value.
260+
* The responses list obtained from the return value corresponds to the order of `messages`.
261+
* An error from this method or a `BatchResponse` with all failures indicates a total failure
262+
* -- i.e. none of the messages in the list could be sent. Partial failures or no failures
263+
* are only indicated by a `BatchResponse` return value.
264264
*
265265
* @param messages - A non-empty array
266266
* containing up to 500 messages.
@@ -270,22 +270,67 @@ export class Messaging {
270270
* send operation.
271271
*/
272272
public sendEach(messages: Message[], dryRun?: boolean): Promise<BatchResponse> {
273-
//TODO: add implementation
274-
console.log(messages, dryRun);
275-
return Promise.resolve({ responses: [], successCount: 0, failureCount: 0 });
273+
if (validator.isArray(messages) && messages.constructor !== Array) {
274+
// In more recent JS specs, an array-like object might have a constructor that is not of
275+
// Array type. Our deepCopy() method doesn't handle them properly. Convert such objects to
276+
// a regular array here before calling deepCopy(). See issue #566 for details.
277+
messages = Array.from(messages);
278+
}
279+
280+
const copy: Message[] = deepCopy(messages);
281+
if (!validator.isNonEmptyArray(copy)) {
282+
throw new FirebaseMessagingError(
283+
MessagingClientErrorCode.INVALID_ARGUMENT, 'messages must be a non-empty array');
284+
}
285+
if (copy.length > FCM_MAX_BATCH_SIZE) {
286+
throw new FirebaseMessagingError(
287+
MessagingClientErrorCode.INVALID_ARGUMENT,
288+
`messages list must not contain more than ${FCM_MAX_BATCH_SIZE} items`);
289+
}
290+
if (typeof dryRun !== 'undefined' && !validator.isBoolean(dryRun)) {
291+
throw new FirebaseMessagingError(
292+
MessagingClientErrorCode.INVALID_ARGUMENT, 'dryRun must be a boolean');
293+
}
294+
295+
return this.getUrlPath()
296+
.then((urlPath) => {
297+
const requests: Promise<SendResponse>[] = copy.map((message) => {
298+
validateMessage(message);
299+
const request: { message: Message; validate_only?: boolean } = { message };
300+
if (dryRun) {
301+
request.validate_only = true;
302+
}
303+
return this.messagingRequestHandler.invokeRequestHandlerForSendResponse(FCM_SEND_HOST, urlPath, request);
304+
});
305+
return Promise.allSettled(requests);
306+
}).then((results) => {
307+
const responses: SendResponse[] = [];
308+
results.forEach(result => {
309+
if (result.status === 'fulfilled') {
310+
responses.push(result.value);
311+
} else { // rejected
312+
responses.push({ success: false, error: result.reason })
313+
}
314+
})
315+
const successCount: number = responses.filter((resp) => resp.success).length;
316+
return {
317+
responses,
318+
successCount,
319+
failureCount: responses.length - successCount,
320+
};
321+
});
276322
}
277323

278-
// TODO: Update the comment based on the implementation
279324
/**
280325
* Sends the given multicast message to all the FCM registration tokens
281326
* specified in it.
282327
*
283328
* This method uses the {@link Messaging.sendEach} API under the hood to send the given
284329
* message to all the target recipients. The responses list obtained from the
285330
* return value corresponds to the order of tokens in the `MulticastMessage`.
286-
* An error from this method indicates a total failure -- i.e. the message was
287-
* not sent to any of the tokens in the list. Partial failures are indicated by
288-
* a `BatchResponse` return value.
331+
* An error from this method or a `BatchResponse` with all failures indicates a total failure
332+
* -- i.e. none of the messages in the list could be sent. Partial failures or no failures
333+
* are only indicated by a `BatchResponse` return value.
289334
*
290335
* @param message - A multicast message
291336
* containing up to 500 tokens.
@@ -295,9 +340,33 @@ export class Messaging {
295340
* send operation.
296341
*/
297342
public sendEachForMulticast(message: MulticastMessage, dryRun?: boolean): Promise<BatchResponse> {
298-
//TODO: add implementation
299-
console.log(message, dryRun);
300-
return Promise.resolve({ responses: [], successCount: 0, failureCount: 0 });
343+
const copy: MulticastMessage = deepCopy(message);
344+
if (!validator.isNonNullObject(copy)) {
345+
throw new FirebaseMessagingError(
346+
MessagingClientErrorCode.INVALID_ARGUMENT, 'MulticastMessage must be a non-null object');
347+
}
348+
if (!validator.isNonEmptyArray(copy.tokens)) {
349+
throw new FirebaseMessagingError(
350+
MessagingClientErrorCode.INVALID_ARGUMENT, 'tokens must be a non-empty array');
351+
}
352+
if (copy.tokens.length > FCM_MAX_BATCH_SIZE) {
353+
throw new FirebaseMessagingError(
354+
MessagingClientErrorCode.INVALID_ARGUMENT,
355+
`tokens list must not contain more than ${FCM_MAX_BATCH_SIZE} items`);
356+
}
357+
358+
const messages: Message[] = copy.tokens.map((token) => {
359+
return {
360+
token,
361+
android: copy.android,
362+
apns: copy.apns,
363+
data: copy.data,
364+
notification: copy.notification,
365+
webpush: copy.webpush,
366+
fcmOptions: copy.fcmOptions,
367+
};
368+
});
369+
return this.sendEach(messages, dryRun);
301370
}
302371

303372
/**

0 commit comments

Comments
 (0)