UNPKG

expo-server-sdk

Version:

Server-side library for working with Expo using Node.js

319 lines 12.9 kB
"use strict"; 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