@meith/notifications 0.31.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/deliver-push.ts +32 -7
- package/src/kinds.ts +14 -0
- package/src/push.ts +9 -1
- package/src/render.ts +28 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meith/notifications",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
"access": "public"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@meith/core": "0.
|
|
23
|
-
"@meith/i18n": "0.
|
|
24
|
-
"@meith/mail": "0.
|
|
22
|
+
"@meith/core": "0.33.0",
|
|
23
|
+
"@meith/i18n": "0.33.0",
|
|
24
|
+
"@meith/mail": "0.33.0"
|
|
25
25
|
}
|
|
26
26
|
}
|
package/src/deliver-push.ts
CHANGED
|
@@ -7,6 +7,25 @@ import type { NotificationRepository } from './types'
|
|
|
7
7
|
|
|
8
8
|
const BODY_LIMIT = 300
|
|
9
9
|
|
|
10
|
+
export const PUSH_DELIVERY_CONCURRENCY = 4
|
|
11
|
+
|
|
12
|
+
async function mapBounded<T, R>(
|
|
13
|
+
items: readonly T[],
|
|
14
|
+
limit: number,
|
|
15
|
+
run: (item: T) => Promise<R>,
|
|
16
|
+
): Promise<R[]> {
|
|
17
|
+
const results = new Array<R>(items.length)
|
|
18
|
+
let next = 0
|
|
19
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
20
|
+
while (next < items.length) {
|
|
21
|
+
const index = next++
|
|
22
|
+
results[index] = await run(items[index]!)
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
await Promise.all(workers)
|
|
26
|
+
return results
|
|
27
|
+
}
|
|
28
|
+
|
|
10
29
|
export interface PushDeliveryResult {
|
|
11
30
|
readonly outcome: DeliveryOutcome
|
|
12
31
|
readonly sent: number
|
|
@@ -47,6 +66,7 @@ export async function deliverNotificationPush(deps: {
|
|
|
47
66
|
readonly translatorForLocale?: NotificationTranslatorResolver
|
|
48
67
|
readonly now?: () => Date
|
|
49
68
|
readonly send?: typeof sendWebPush
|
|
69
|
+
readonly signal?: AbortSignal
|
|
50
70
|
}): Promise<PushDeliveryResult> {
|
|
51
71
|
const nothing = { sent: 0, pruned: 0, failed: 0 }
|
|
52
72
|
|
|
@@ -73,12 +93,8 @@ export async function deliverNotificationPush(deps: {
|
|
|
73
93
|
badge: await deps.notifications.unreadCount(deliverable.recipient.userId),
|
|
74
94
|
})
|
|
75
95
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
let failed = 0
|
|
79
|
-
|
|
80
|
-
for (const subscription of subscriptions) {
|
|
81
|
-
const result = await send({
|
|
96
|
+
const outcomes = await mapBounded(subscriptions, PUSH_DELIVERY_CONCURRENCY, (subscription) =>
|
|
97
|
+
send({
|
|
82
98
|
subscription: {
|
|
83
99
|
endpoint: subscription.endpoint,
|
|
84
100
|
p256dh: subscription.p256dh,
|
|
@@ -87,7 +103,16 @@ export async function deliverNotificationPush(deps: {
|
|
|
87
103
|
payload,
|
|
88
104
|
vapid: deps.vapid,
|
|
89
105
|
now: now(),
|
|
90
|
-
|
|
106
|
+
...(deps.signal === undefined ? {} : { signal: deps.signal }),
|
|
107
|
+
}),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
let sent = 0
|
|
111
|
+
let pruned = 0
|
|
112
|
+
let failed = 0
|
|
113
|
+
|
|
114
|
+
for (const [index, subscription] of subscriptions.entries()) {
|
|
115
|
+
const result = outcomes[index]!
|
|
91
116
|
|
|
92
117
|
if (result.outcome === 'sent') {
|
|
93
118
|
sent += 1
|
package/src/kinds.ts
CHANGED
|
@@ -70,6 +70,20 @@ export const NOTIFICATION_KINDS = [
|
|
|
70
70
|
pushByDefault: false,
|
|
71
71
|
pushConfigurable: true,
|
|
72
72
|
},
|
|
73
|
+
{
|
|
74
|
+
id: 'board.digest',
|
|
75
|
+
titleKey: 'notification.board.digest.title',
|
|
76
|
+
descriptionKey: 'notification.board.digest.description',
|
|
77
|
+
title: 'A recap of what you missed on the board',
|
|
78
|
+
description:
|
|
79
|
+
'Sent only after you have been away a while, listing the busiest threads since ' +
|
|
80
|
+
'your last visit. Off by default — nobody gets this without asking for it.',
|
|
81
|
+
audience: 'member',
|
|
82
|
+
emailByDefault: false,
|
|
83
|
+
emailConfigurable: true,
|
|
84
|
+
pushByDefault: false,
|
|
85
|
+
pushConfigurable: true,
|
|
86
|
+
},
|
|
73
87
|
{
|
|
74
88
|
id: 'post.mentioned',
|
|
75
89
|
titleKey: 'notification.post.mentioned.title',
|
package/src/push.ts
CHANGED
|
@@ -9,6 +9,8 @@ export const PUSH_RECORD_SIZE = 4096
|
|
|
9
9
|
|
|
10
10
|
export const PUSH_REQUEST_TIMEOUT_MS = 10_000
|
|
11
11
|
|
|
12
|
+
export const PUSH_MAX_RESPONSE_BYTES = 8_192
|
|
13
|
+
|
|
12
14
|
export const PUSH_TTL_SECONDS = 86_400
|
|
13
15
|
|
|
14
16
|
const VAPID_EXPIRY_SECONDS = 12 * 60 * 60
|
|
@@ -199,6 +201,8 @@ const guardedPushFetch: typeof fetch = (async (url: string, init: RequestInit) =
|
|
|
199
201
|
body: init.body as Uint8Array,
|
|
200
202
|
timeoutMs: PUSH_REQUEST_TIMEOUT_MS,
|
|
201
203
|
allowPrivateHosts,
|
|
204
|
+
maxResponseBytes: PUSH_MAX_RESPONSE_BYTES,
|
|
205
|
+
...(init.signal ? { signal: init.signal } : {}),
|
|
202
206
|
})
|
|
203
207
|
return { status } as Response
|
|
204
208
|
}) as unknown as typeof fetch
|
|
@@ -210,6 +214,7 @@ export async function sendWebPush(input: {
|
|
|
210
214
|
readonly ttlSeconds?: number
|
|
211
215
|
readonly now?: Date
|
|
212
216
|
readonly fetchImpl?: typeof fetch
|
|
217
|
+
readonly signal?: AbortSignal
|
|
213
218
|
}): Promise<PushSendResult> {
|
|
214
219
|
const doFetch = input.fetchImpl ?? guardedPushFetch
|
|
215
220
|
|
|
@@ -238,7 +243,10 @@ export async function sendWebPush(input: {
|
|
|
238
243
|
},
|
|
239
244
|
body: bytes(encrypted.body),
|
|
240
245
|
redirect: 'manual',
|
|
241
|
-
signal:
|
|
246
|
+
signal:
|
|
247
|
+
input.signal === undefined
|
|
248
|
+
? AbortSignal.timeout(PUSH_REQUEST_TIMEOUT_MS)
|
|
249
|
+
: AbortSignal.any([input.signal, AbortSignal.timeout(PUSH_REQUEST_TIMEOUT_MS)]),
|
|
242
250
|
})
|
|
243
251
|
|
|
244
252
|
const outcome = pushOutcomeFor(response.status)
|
package/src/render.ts
CHANGED
|
@@ -80,6 +80,11 @@ const DIGEST_CADENCE_KEYS = {
|
|
|
80
80
|
weekly: 'notification.render.digest.weekly',
|
|
81
81
|
} as const
|
|
82
82
|
|
|
83
|
+
const BOARD_DIGEST_CADENCE_KEYS = {
|
|
84
|
+
weekly: 'notification.render.digest.weekly',
|
|
85
|
+
monthly: 'notification.render.boardDigest.monthly',
|
|
86
|
+
} as const
|
|
87
|
+
|
|
83
88
|
const TEMPLATES: Readonly<
|
|
84
89
|
Record<string, (data: NotificationData, t: Translator) => { subject: string; body: string }>
|
|
85
90
|
> = {
|
|
@@ -167,6 +172,29 @@ const TEMPLATES: Readonly<
|
|
|
167
172
|
}
|
|
168
173
|
},
|
|
169
174
|
|
|
175
|
+
'board.digest': (data, t) => {
|
|
176
|
+
const cadence = str(data, 'cadence') === 'monthly' ? 'monthly' : 'weekly'
|
|
177
|
+
const threadCount = num(data, 'threadCount')
|
|
178
|
+
const more = num(data, 'more')
|
|
179
|
+
|
|
180
|
+
const lines = objects(data, 'threads').map((entry) =>
|
|
181
|
+
t.t('notification.render.boardDigest.entry', {
|
|
182
|
+
title: str(entry, 'title', t.t('notification.render.digest.fallbackTitle')),
|
|
183
|
+
forum: str(entry, 'forumTitle', t.t('notification.render.boardDigest.forumFallback')),
|
|
184
|
+
}),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if (more > 0) lines.push(t.t('notification.render.digest.more', { count: more }))
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
subject: t.t('notification.render.boardDigest.subject', {
|
|
191
|
+
cadence: t.t(BOARD_DIGEST_CADENCE_KEYS[cadence]),
|
|
192
|
+
threads: threadCount,
|
|
193
|
+
}),
|
|
194
|
+
body: lines.join('\n'),
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
|
|
170
198
|
'post.mentioned': (data, t) => {
|
|
171
199
|
const by = str(data, 'byUsername', t.t('notification.render.somebody'))
|
|
172
200
|
const title = str(data, 'threadTitle', t.t('notification.render.threadFallback'))
|