expo-server-sdk
Version:
Server-side library for working with Expo using Node.js
319 lines • 12.9 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Expo = void 0;
/**
* expo-server-sdk
*
* Use this if you are running Node on your server backend when you are working with Expo
* Application Services
* https://expo.dev
*/
const node_fetch_1 = __importStar(require("node-fetch"));
const node_assert_1 = __importDefault(require("node:assert"));
const node_zlib_1 = require("node:zlib");
const promise_limit_1 = __importDefault(require("promise-limit"));
const promise_retry_1 = __importDefault(require("promise-retry"));
const ExpoClientValues_1 = require("./ExpoClientValues");
class Expo {
static pushNotificationChunkSizeLimit = ExpoClientValues_1.pushNotificationChunkLimit;
static pushNotificationReceiptChunkSizeLimit = ExpoClientValues_1.pushNotificationReceiptChunkLimit;
httpAgent;
limitConcurrentRequests;
accessToken;
useFcmV1;
retryMinTimeout;
constructor(options = {}) {
this.httpAgent = options.httpAgent;
this.limitConcurrentRequests = (0, promise_limit_1.default)(options.maxConcurrentRequests ?? ExpoClientValues_1.defaultConcurrentRequestLimit);
this.retryMinTimeout = options.retryMinTimeout ?? ExpoClientValues_1.requestRetryMinTimeout;
this.accessToken = options.accessToken;
this.useFcmV1 = options.useFcmV1;
}
/**
* Returns `true` if the token is an Expo push token
*/
static isExpoPushToken(token) {
return (typeof token === 'string' &&
(((token.startsWith('ExponentPushToken[') || token.startsWith('ExpoPushToken[')) &&
token.endsWith(']')) ||
/^[a-z\d]{8}-[a-z\d]{4}-[a-z\d]{4}-[a-z\d]{4}-[a-z\d]{12}$/i.test(token)));
}
/**
* Sends the given messages to their recipients via push notifications and returns an array of
* push tickets. Each ticket corresponds to the message at its respective index (the nth receipt
* is for the nth message) and contains a receipt ID. Later, after Expo attempts to deliver the
* messages to the underlying push notification services, the receipts with those IDs will be
* available for a period of time (approximately a day).
*
* There is a limit on the number of push notifications you can send at once. Use
* `chunkPushNotifications` to divide an array of push notification messages into appropriately
* sized chunks.
*/
async sendPushNotificationsAsync(messages) {
const url = new URL(ExpoClientValues_1.sendApiUrl);
// Only append the useFcmV1 option if the option is set to false
if (this.useFcmV1 === false) {
url.searchParams.append('useFcmV1', String(this.useFcmV1));
}
const actualMessagesCount = Expo._getActualMessageCount(messages);
const data = await this.limitConcurrentRequests(async () => {
return await (0, promise_retry_1.default)(async (retry) => {
try {
return await this.requestAsync(url.toString(), {
httpMethod: 'post',
body: messages,
shouldCompress(body) {
return body.length > 1024;
},
});
}
catch (e) {
// if Expo servers rate limit, retry with exponential backoff
if (e.statusCode === 429) {
return retry(e);
}
throw e;
}
}, {
retries: 2,
factor: 2,
minTimeout: this.retryMinTimeout,
});
});
if (!Array.isArray(data) || data.length !== actualMessagesCount) {
const apiError = new Error(`Expected Expo to respond with ${actualMessagesCount} ${actualMessagesCount === 1 ? 'ticket' : 'tickets'} but got ${data.length}`);
apiError['data'] = data;
throw apiError;
}
return data;
}
async getPushNotificationReceiptsAsync(receiptIds) {
const data = await this.requestAsync(ExpoClientValues_1.getReceiptsApiUrl, {
httpMethod: 'post',
body: { ids: receiptIds },
shouldCompress(body) {
return body.length > 1024;
},
});
if (!data || typeof data !== 'object' || Array.isArray(data)) {
const apiError = new Error(`Expected Expo to respond with a map from receipt IDs to receipts but received data of another type`);
apiError['data'] = data;
throw apiError;
}
return data;
}
chunkPushNotifications(messages) {
const chunks = [];
let chunk = [];
let chunkMessagesCount = 0;
for (const message of messages) {
if (Array.isArray(message.to)) {
let partialTo = [];
for (const recipient of message.to) {
partialTo.push(recipient);
chunkMessagesCount++;
if (chunkMessagesCount >= ExpoClientValues_1.pushNotificationChunkLimit) {
// Cap this chunk here if it already exceeds PUSH_NOTIFICATION_CHUNK_LIMIT.
// Then create a new chunk to continue on the remaining recipients for this message.
chunk.push({ ...message, to: partialTo });
chunks.push(chunk);
chunk = [];
chunkMessagesCount = 0;
partialTo = [];
}
}
if (partialTo.length) {
// Add remaining `partialTo` to the chunk.
chunk.push({ ...message, to: partialTo });
}
}
else {
chunk.push(message);
chunkMessagesCount++;
}
if (chunkMessagesCount >= ExpoClientValues_1.pushNotificationChunkLimit) {
// Cap this chunk if it exceeds PUSH_NOTIFICATION_CHUNK_LIMIT.
// Then create a new chunk to continue on the remaining messages.
chunks.push(chunk);
chunk = [];
chunkMessagesCount = 0;
}
}
if (chunkMessagesCount) {
// Add the remaining chunk to the chunks.
chunks.push(chunk);
}
return chunks;
}
chunkPushNotificationReceiptIds(receiptIds) {
return this.chunkItems(receiptIds, ExpoClientValues_1.pushNotificationReceiptChunkLimit);
}
chunkItems(items, chunkSize) {
const chunks = [];
let chunk = [];
for (const item of items) {
chunk.push(item);
if (chunk.length >= chunkSize) {
chunks.push(chunk);
chunk = [];
}
}
if (chunk.length) {
chunks.push(chunk);
}
return chunks;
}
async requestAsync(url, options) {
let requestBody;
const sdkVersion = require('../package.json').version;
const requestHeaders = new node_fetch_1.Headers({
Accept: 'application/json',
'Accept-Encoding': 'gzip, deflate',
'User-Agent': `expo-server-sdk-node/${sdkVersion}`,
});
if (this.accessToken) {
requestHeaders.set('Authorization', `Bearer ${this.accessToken}`);
}
if (options.body != null) {
const json = JSON.stringify(options.body);
(0, node_assert_1.default)(json != null, `JSON request body must not be null`);
if (options.shouldCompress(json)) {
requestBody = (0, node_zlib_1.gzipSync)(Buffer.from(json));
requestHeaders.set('Content-Encoding', 'gzip');
}
else {
requestBody = json;
}
requestHeaders.set('Content-Type', 'application/json');
}
const response = await (0, node_fetch_1.default)(url, {
method: options.httpMethod,
body: requestBody,
headers: requestHeaders,
agent: this.httpAgent,
});
if (response.status !== 200) {
const apiError = await this.parseErrorResponseAsync(response);
throw apiError;
}
const textBody = await response.text();
// We expect the API response body to be JSON
let result;
try {
result = JSON.parse(textBody);
}
catch {
const apiError = await this.getTextResponseErrorAsync(response, textBody);
throw apiError;
}
if (result.errors) {
const apiError = this.getErrorFromResult(response, result);
throw apiError;
}
return result.data;
}
async parseErrorResponseAsync(response) {
const textBody = await response.text();
let result;
try {
result = JSON.parse(textBody);
}
catch {
return await this.getTextResponseErrorAsync(response, textBody);
}
if (!result.errors || !Array.isArray(result.errors) || !result.errors.length) {
const apiError = await this.getTextResponseErrorAsync(response, textBody);
apiError['errorData'] = result;
return apiError;
}
return this.getErrorFromResult(response, result);
}
async getTextResponseErrorAsync(response, text) {
const apiError = new Error(`Expo responded with an error with status code ${response.status}: ` + text);
apiError['statusCode'] = response.status;
apiError['errorText'] = text;
return apiError;
}
/**
* Returns an error for the first API error in the result, with an optional `others` field that
* contains any other errors.
*/
getErrorFromResult(response, result) {
const noErrorsMessage = `Expected at least one error from Expo`;
(0, node_assert_1.default)(result.errors, noErrorsMessage);
const [errorData, ...otherErrorData] = result.errors;
node_assert_1.default.ok(errorData, noErrorsMessage);
const error = this.getErrorFromResultError(errorData);
if (otherErrorData.length) {
error['others'] = otherErrorData.map((data) => this.getErrorFromResultError(data));
}
error['statusCode'] = response.status;
return error;
}
/**
* Returns an error for a single API error
*/
getErrorFromResultError(errorData) {
const error = new Error(errorData.message);
error['code'] = errorData.code;
if (errorData.details != null) {
error['details'] = errorData.details;
}
if (errorData.stack != null) {
error['serverStack'] = errorData.stack;
}
return error;
}
static _getActualMessageCount(messages) {
return messages.reduce((total, message) => {
if (Array.isArray(message.to)) {
total += message.to.length;
}
else {
total++;
}
return total;
}, 0);
}
}
exports.Expo = Expo;
exports.default = Expo;
class ExtensibleError extends Error {
}
//# sourceMappingURL=ExpoClient.js.map