Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

---

## [13.0.0](https://github.com/AndrewBarba/apns2/releases/tag/13.0.0)

1. Change the return shape of `send()` to include the notification as well as details from APNS

## [12.2.0](https://github.com/AndrewBarba/apns2/releases/tag/12.2.0)

1. Send http2 ping frames for better support of long lived node processes
Expand Down
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "apns2",
"version": "12.2.0",
"version": "13.0.0",
"description": "Node client for connecting to Apple's Push Notification Service using the new HTTP/2 protocol with JSON web tokens.",
"author": "Andrew Barba <barba@hey.com>",
"license": "MIT",
Expand Down Expand Up @@ -38,7 +38,7 @@
],
"dependencies": {
"fast-jwt": "^6.0.1",
"undici": "^7.9.0"
"undici": "^7.10.0"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
Expand Down
43 changes: 39 additions & 4 deletions src/apns.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { EventEmitter } from "node:events"
import { type PrivateKey, createSigner } from "fast-jwt"
import { type Dispatcher, Pool } from "undici"
import type { IncomingHttpHeaders } from "undici/types/header.js"
import { ApnsError, type ApnsResponseError, Errors } from "./errors.js"
import { undici_getClientHttp2Session, undici_getPoolClients } from "./internals.js"
import { type Notification, Priority } from "./notifications/notification.js"
Expand Down Expand Up @@ -39,6 +40,14 @@ export interface ApnsOptions {
keepAlive?: boolean
}

export interface ApnsSendResponse {
notification: Notification
headers: IncomingHttpHeaders
statusCode: number
apnsId: string
apnsUniqueId?: string // only available in development environment
}

export class ApnsClient extends EventEmitter {
readonly team: string
readonly keyId: string
Expand All @@ -64,6 +73,7 @@ export class ApnsClient extends EventEmitter {
pipelining: this.keepAlive ? 1 : 0,
allowH2: true,
maxConcurrentStreams: 100,
clientTtl: PING_INTERVAL_MS * 3,
})
this._token = null
this._pingInterval = this.keepAlive
Expand All @@ -75,16 +85,20 @@ export class ApnsClient extends EventEmitter {
const promises = notifications.map((notification) =>
this.send(notification).catch((error: ApnsError) => ({ error })),
)
return Promise.all(promises)
return Promise.allSettled(promises)
}

async send(notification: Notification) {
async send(notification: Notification): Promise<ApnsSendResponse> {
const headers: Record<string, string | undefined> = {
authorization: `bearer ${this._getSigningToken()}`,
"apns-push-type": notification.pushType,
"apns-topic": notification.options.topic ?? this.defaultTopic,
}

if (notification.options.id) {
headers["apns-id"] = notification.options.id
}

if (notification.priority !== Priority.immediate) {
headers["apns-priority"] = notification.priority.toString()
}
Expand All @@ -110,7 +124,27 @@ export class ApnsClient extends EventEmitter {
blocking: false,
})

return this._handleServerResponse(res, notification)
await this._handleServerResponse(res, notification)

if (!res.headers["apns-id"]) {
throw new ApnsError({
statusCode: res.statusCode,
notification,
headers: res.headers,
response: {
reason: Errors.unknownError,
timestamp: Date.now(),
},
})
}

return {
notification,
headers: res.headers,
statusCode: res.statusCode,
apnsId: res.headers["apns-id"].toString(),
apnsUniqueId: res.headers["apns-unique-id"]?.toString(),
}
}

async ping() {
Expand Down Expand Up @@ -144,7 +178,7 @@ export class ApnsClient extends EventEmitter {

private async _handleServerResponse(res: Dispatcher.ResponseData, notification: Notification) {
if (res.statusCode === 200) {
return notification
return
}

const responseError = await res.body.json().catch(() => ({
Expand All @@ -155,6 +189,7 @@ export class ApnsClient extends EventEmitter {
const error = new ApnsError({
statusCode: res.statusCode,
notification: notification,
headers: res.headers,
response: responseError as ApnsResponseError,
})

Expand Down
4 changes: 4 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { IncomingHttpHeaders } from "undici/types/header.js"
import type { Notification } from "./notifications/notification.js"

export const Errors = {
Expand Down Expand Up @@ -46,16 +47,19 @@ export class ApnsError extends Error {
readonly statusCode: number
readonly notification: Notification
readonly response: ApnsResponseError
readonly headers: IncomingHttpHeaders

constructor(props: {
statusCode: number
notification: Notification
response: ApnsResponseError
headers: IncomingHttpHeaders
}) {
super(`APNS Error: ${props.statusCode} - ${props.response.reason}`)
this.statusCode = props.statusCode
this.notification = props.notification
this.response = props.response
this.headers = props.headers
}

get reason() {
Expand Down
1 change: 1 addition & 0 deletions src/notifications/constants/push-type.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const PushType = {
alert: "alert",
background: "background",
controls: "controls",
voip: "voip",
complication: "complication",
fileprovider: "fileprovider",
Expand Down
1 change: 1 addition & 0 deletions src/notifications/notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { PushType } from "./constants/push-type.js"
export { PushType, Priority }

export interface NotificationOptions {
id?: string
type?: PushType
alert?: string | { title: string; subtitle?: string; body: string }
badge?: number
Expand Down