Skip to content

Commit 9cadaf5

Browse files
committed
feat(messaging): migrate topic subscription APIs to new REST endpoints via HTTP/2 multiplexing
1 parent 0efb21f commit 9cadaf5

3 files changed

Lines changed: 231 additions & 171 deletions

File tree

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,43 @@ export class FirebaseMessagingRequestHandler {
152152
});
153153
}
154154

155+
/**
156+
* Invokes the HTTP/2 request handler for generic operations (e.g., topic subscriptions).
157+
*
158+
* @param host - The host to which to send the request.
159+
* @param path - The path to which to send the request.
160+
* @param method - The HTTP method to use.
161+
* @param requestData - Optional request data.
162+
* @param http2SessionHandler - The HTTP/2 session handler.
163+
* @returns A promise that resolves with the response data.
164+
*/
165+
public invokeHttp2RequestHandler(
166+
host: string,
167+
path: string,
168+
method: HttpMethod,
169+
requestData: object | undefined,
170+
http2SessionHandler: Http2SessionHandler
171+
): Promise<object> {
172+
const request: Http2RequestConfig = {
173+
method,
174+
url: `https://${host}${path}`,
175+
data: requestData,
176+
headers: FIREBASE_MESSAGING_HEADERS,
177+
timeout: FIREBASE_MESSAGING_TIMEOUT,
178+
http2SessionHandler,
179+
};
180+
return this.http2Client.send(request).then((response) => {
181+
if (!response.isJson()) {
182+
throw new RequestResponseError(response);
183+
}
184+
const errorCode = getErrorCode(response.data);
185+
if (errorCode) {
186+
throw new RequestResponseError(response);
187+
}
188+
return response.data;
189+
});
190+
}
191+
155192
private buildSendResponse(response: RequestResponse): SendResponse {
156193
const result: SendResponse = {
157194
success: response.status === 200,

src/messaging/messaging.ts

Lines changed: 86 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
import * as utils from '../utils';
2424
import * as validator from '../utils/validator';
2525
import { validateMessage } from './messaging-internal';
26+
import { getErrorCode, createFirebaseError } from './messaging-errors-internal';
2627
import { FirebaseMessagingRequestHandler } from './messaging-api-request-internal';
2728

2829
import {
@@ -34,53 +35,14 @@ import {
3435
// Legacy API types
3536
SendResponse,
3637
} from './messaging-api';
37-
import { Http2SessionHandler } from '../utils/api-request';
38+
import { Http2SessionHandler, RequestResponseError } from '../utils/api-request';
3839

3940
// FCM endpoints
4041
const FCM_SEND_HOST = 'fcm.googleapis.com';
41-
const FCM_TOPIC_MANAGEMENT_HOST = 'iid.googleapis.com';
42-
const FCM_TOPIC_MANAGEMENT_ADD_PATH = '/iid/v1:batchAdd';
43-
const FCM_TOPIC_MANAGEMENT_REMOVE_PATH = '/iid/v1:batchRemove';
4442

4543
// Maximum messages that can be included in a batch request.
4644
const FCM_MAX_BATCH_SIZE = 500;
4745

48-
/**
49-
* Maps a raw FCM server response to a `MessagingTopicManagementResponse` object.
50-
*
51-
* @param {object} response The raw FCM server response to map.
52-
*
53-
* @returns {MessagingTopicManagementResponse} The mapped `MessagingTopicManagementResponse` object.
54-
*/
55-
function mapRawResponseToTopicManagementResponse(response: object): MessagingTopicManagementResponse {
56-
// Add the success and failure counts.
57-
const result: MessagingTopicManagementResponse = {
58-
successCount: 0,
59-
failureCount: 0,
60-
errors: [],
61-
};
62-
63-
if ('results' in response) {
64-
(response as any).results.forEach((tokenManagementResult: any, index: number) => {
65-
// Map the FCM server's error strings to actual error objects.
66-
if ('error' in tokenManagementResult) {
67-
result.failureCount += 1;
68-
const newError = FirebaseMessagingError.fromTopicManagementServerError(
69-
tokenManagementResult.error, /* message */ undefined, tokenManagementResult.error,
70-
);
71-
72-
result.errors.push({
73-
index,
74-
error: newError,
75-
});
76-
} else {
77-
result.successCount += 1;
78-
}
79-
});
80-
}
81-
return result;
82-
}
83-
8446

8547
/**
8648
* Messaging service bound to the provided app.
@@ -344,7 +306,7 @@ export class Messaging {
344306
registrationTokenOrTokens,
345307
topic,
346308
'subscribeToTopic',
347-
FCM_TOPIC_MANAGEMENT_ADD_PATH,
309+
'',
348310
);
349311
}
350312

@@ -371,7 +333,7 @@ export class Messaging {
371333
registrationTokenOrTokens,
372334
topic,
373335
'unsubscribeFromTopic',
374-
FCM_TOPIC_MANAGEMENT_REMOVE_PATH,
336+
'',
375337
);
376338
}
377339

@@ -413,7 +375,7 @@ export class Messaging {
413375
registrationTokenOrTokens: string | string[],
414376
topic: string,
415377
methodName: string,
416-
path: string,
378+
_path: string,
417379
): Promise<MessagingTopicManagementResponse> {
418380
this.validateRegistrationTokensType(registrationTokenOrTokens, methodName);
419381
this.validateTopicType(topic, methodName);
@@ -434,17 +396,88 @@ export class Messaging {
434396
registrationTokensArray = [registrationTokenOrTokens as string];
435397
}
436398

437-
const request = {
438-
to: topic,
439-
registration_tokens: registrationTokensArray,
440-
};
399+
return utils.findProjectId(this.app).then((projectId) => {
400+
if (!validator.isNonEmptyString(projectId)) {
401+
throw new FirebaseMessagingError(
402+
MessagingClientErrorCode.INVALID_ARGUMENT,
403+
'Failed to determine project ID for Messaging. Initialize the '
404+
+ 'SDK with service account credentials or set project ID as an app option. '
405+
+ 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.',
406+
);
407+
}
441408

442-
return this.messagingRequestHandler.invokeRequestHandler(
443-
FCM_TOPIC_MANAGEMENT_HOST, path, request,
444-
);
445-
})
446-
.then((response) => {
447-
return mapRawResponseToTopicManagementResponse(response);
409+
const topicName = topic.replace(/^\/topics\//, '');
410+
const isSubscribe = methodName === 'subscribeToTopic';
411+
const httpMethod = isSubscribe ? 'POST' : 'DELETE';
412+
413+
const http2SessionHandler = new Http2SessionHandler('https://fcm.googleapis.com');
414+
415+
let settledPromise: Promise<PromiseSettledResult<any>[]>;
416+
return new Promise<MessagingTopicManagementResponse>((resolve, reject) => {
417+
http2SessionHandler.invoke().catch((error) => {
418+
reject(new FirebaseMessagingSessionError(error, undefined, undefined));
419+
});
420+
421+
const requests = registrationTokensArray.map(async (registrationId) => {
422+
let requestPath = `/v1/projects/${projectId}/registrations/${registrationId}/topicSubscriptions`;
423+
if (isSubscribe) {
424+
requestPath += `?topic_name=${topicName}`;
425+
} else {
426+
requestPath += `/${topicName}?allow_missing=true`;
427+
}
428+
return this.messagingRequestHandler.invokeHttp2RequestHandler(
429+
'fcm.googleapis.com',
430+
requestPath,
431+
httpMethod,
432+
isSubscribe ? {} : undefined,
433+
http2SessionHandler
434+
);
435+
});
436+
437+
settledPromise = Promise.allSettled(requests);
438+
settledPromise.then((results) => {
439+
if (results.length > 0 && results.every((r) => r.status === 'rejected')) {
440+
const firstReason = (results[0] as PromiseRejectedResult).reason;
441+
if (firstReason instanceof RequestResponseError) {
442+
reject(createFirebaseError(firstReason));
443+
} else {
444+
reject(firstReason);
445+
}
446+
return;
447+
}
448+
449+
const response: MessagingTopicManagementResponse = {
450+
successCount: 0,
451+
failureCount: 0,
452+
errors: [],
453+
};
454+
455+
results.forEach((result, index) => {
456+
if (result.status === 'fulfilled') {
457+
response.successCount += 1;
458+
} else {
459+
response.failureCount += 1;
460+
const err = result.reason;
461+
const errorCode = err.response?.isJson() ? getErrorCode(err.response.data) : null;
462+
const errorMessage = err.response?.isJson() ? err.response.data?.error?.message : err.message;
463+
const newError = FirebaseMessagingError.fromTopicManagementServerError(
464+
errorCode || 'UNKNOWN',
465+
errorMessage,
466+
err.response?.isJson() ? err.response.data : undefined
467+
);
468+
response.errors.push({
469+
index,
470+
error: newError,
471+
});
472+
}
473+
});
474+
475+
resolve(response);
476+
});
477+
}).finally(() => {
478+
http2SessionHandler.close();
479+
});
480+
});
448481
});
449482
}
450483

0 commit comments

Comments
 (0)