@arkstack/notifications 0.18.6 → 0.18.8
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/dist/index.d.ts +372 -138
- package/dist/index.js +249 -15
- package/package.json +7 -7
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,203 @@ import { User as User$1 } from "@arkstack/auth";
|
|
|
8
8
|
import { Attachment } from "nodemailer/lib/mailer/index.js";
|
|
9
9
|
import { PhoneNumber } from "@kanun-hq/plugin-phone";
|
|
10
10
|
import { ArkormCollection, LengthAwarePaginator } from "arkormx";
|
|
11
|
+
//#region src/drivers/realtime/FirebaseRealtimeDriver.d.ts
|
|
12
|
+
/**
|
|
13
|
+
* The outcome of a token multicast: totals plus the tokens FCM rejected as dead.
|
|
14
|
+
*/
|
|
15
|
+
interface FirebaseMulticastResult {
|
|
16
|
+
successCount: number;
|
|
17
|
+
failureCount: number;
|
|
18
|
+
/**
|
|
19
|
+
* Tokens FCM reported as unregistered/invalid — delete these from your store.
|
|
20
|
+
*/
|
|
21
|
+
invalidTokens: string[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Broadcasts notifications over [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging).
|
|
25
|
+
* A single channel maps to an FCM topic; an array of channels is treated as
|
|
26
|
+
* device registration tokens and delivered via a multicast send (chunked to
|
|
27
|
+
* FCM's 500-token limit), returning the tokens that should be pruned.
|
|
28
|
+
*
|
|
29
|
+
* `firebase-admin` is an optional peer dependency, imported lazily so the
|
|
30
|
+
* package installs without it; it is only required when this transport is used.
|
|
31
|
+
* FCM data values must be strings, so the payload is JSON-encoded.
|
|
32
|
+
*/
|
|
33
|
+
declare class FirebaseRealtimeDriver implements RealtimeDriver {
|
|
34
|
+
private options;
|
|
35
|
+
private messagingPromise?;
|
|
36
|
+
constructor(options?: FirebaseTransportConfig);
|
|
37
|
+
private messaging;
|
|
38
|
+
broadcast(channel: string | string[], event: string, payload: RealtimeBroadcastPayload, delivery?: RealtimeDeliveryOptions): Promise<string | FirebaseMulticastResult>;
|
|
39
|
+
/**
|
|
40
|
+
* Translate transport-agnostic delivery hints into FCM's platform blocks.
|
|
41
|
+
*
|
|
42
|
+
* Per-send options win over the transport defaults, and the raw `android` /
|
|
43
|
+
* `apns` / `webpush` escape hatches are merged last so a caller can always
|
|
44
|
+
* override what this derives.
|
|
45
|
+
*
|
|
46
|
+
* `priority` always maps to Android and Web Push. Whether it maps to APNs
|
|
47
|
+
* depends on the message rather than on the platform: Apple requires a
|
|
48
|
+
* background push to be priority 5 or 1, and a data-only message — what this
|
|
49
|
+
* driver sends by default — arrives on iOS as exactly that. So the header is
|
|
50
|
+
* derived only once the message is demonstrably a *visible* push, which a
|
|
51
|
+
* caller declares through the `apns` escape hatch. Guessing otherwise would
|
|
52
|
+
* mean sending APNs something it is documented to reject.
|
|
53
|
+
*
|
|
54
|
+
* @param delivery
|
|
55
|
+
* @returns
|
|
56
|
+
*/
|
|
57
|
+
private overridesFor;
|
|
58
|
+
/**
|
|
59
|
+
* Whether the caller has built a push iOS will actually display.
|
|
60
|
+
*
|
|
61
|
+
* Apple allows `apns-priority: 10` only on a push that shows the user
|
|
62
|
+
* something; a background push must be 5 or 1. A data-only message is a
|
|
63
|
+
* background push, so this is false for the default this driver sends, and
|
|
64
|
+
* true once a caller has declared an alert through the `apns` escape hatch —
|
|
65
|
+
* by push type, or by supplying an alert payload.
|
|
66
|
+
*
|
|
67
|
+
* @param apns
|
|
68
|
+
* @returns
|
|
69
|
+
*/
|
|
70
|
+
private isVisiblePush;
|
|
71
|
+
/**
|
|
72
|
+
* Fold a caller's raw override over the derived settings, dropping the block
|
|
73
|
+
* entirely when neither contributed anything.
|
|
74
|
+
*
|
|
75
|
+
* @param key
|
|
76
|
+
* @param derived
|
|
77
|
+
* @param override
|
|
78
|
+
* @returns
|
|
79
|
+
*/
|
|
80
|
+
private flatBlock;
|
|
81
|
+
/**
|
|
82
|
+
* As `flatBlock`, but for the platforms whose settings live under `headers` —
|
|
83
|
+
* merged key by key, so an override adding one header keeps the rest.
|
|
84
|
+
*
|
|
85
|
+
* @param key
|
|
86
|
+
* @param derived
|
|
87
|
+
* @param override
|
|
88
|
+
* @returns
|
|
89
|
+
*/
|
|
90
|
+
private headerBlock;
|
|
91
|
+
/**
|
|
92
|
+
* Firebase channels do not require server-side authorization.
|
|
93
|
+
*
|
|
94
|
+
* @param _socketId
|
|
95
|
+
* @param _channel
|
|
96
|
+
* @param _data
|
|
97
|
+
*/
|
|
98
|
+
auth(_socketId: string, _channel: string, _data?: unknown): Promise<void>;
|
|
99
|
+
/**
|
|
100
|
+
* Firebase does not require an authorization route.
|
|
101
|
+
*
|
|
102
|
+
* @param _authEndpoint
|
|
103
|
+
* @param _middleware
|
|
104
|
+
* @param _channelPrefix
|
|
105
|
+
* @param _config
|
|
106
|
+
*/
|
|
107
|
+
static registerAuthRoute(_authEndpoint?: string, _middleware?: unknown | unknown[], _channelPrefix?: string, _config?: FirebaseTransportConfig): Promise<void>;
|
|
108
|
+
/**
|
|
109
|
+
* Firebase does not require an authorization route.
|
|
110
|
+
*
|
|
111
|
+
* @param _authEndpoint
|
|
112
|
+
* @param _middleware
|
|
113
|
+
* @param _channelPrefix
|
|
114
|
+
*/
|
|
115
|
+
registerAuthRoute(_authEndpoint?: string, _middleware?: unknown | unknown[], _channelPrefix?: string): Promise<void>;
|
|
116
|
+
/**
|
|
117
|
+
* Send to many device tokens at once, chunked to FCM's 500-token limit, and
|
|
118
|
+
* collect the tokens FCM rejects as dead so the caller can prune them.
|
|
119
|
+
*/
|
|
120
|
+
private multicast;
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/drivers/realtime/PusherRealtimeDriver.d.ts
|
|
124
|
+
/** The slice of the `pusher` server SDK this driver uses. */
|
|
125
|
+
interface PusherClient {
|
|
126
|
+
trigger(channel: string | string[], event: string, data: unknown): Promise<unknown>;
|
|
127
|
+
authorizeChannel(socketId: string, channel: string, data?: {
|
|
128
|
+
user_id: string;
|
|
129
|
+
user_info?: {
|
|
130
|
+
[key: string]: any;
|
|
131
|
+
};
|
|
132
|
+
}): {
|
|
133
|
+
auth: string;
|
|
134
|
+
channel_data?: string;
|
|
135
|
+
shared_secret?: string;
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Broadcasts notifications over [Pusher Channels](https://pusher.com/channels).
|
|
140
|
+
*
|
|
141
|
+
* The `pusher` server SDK is an optional peer dependency, imported lazily so the
|
|
142
|
+
* package installs without it; it is only required when this transport is used.
|
|
143
|
+
*/
|
|
144
|
+
declare class PusherRealtimeDriver implements RealtimeDriver {
|
|
145
|
+
private options;
|
|
146
|
+
private clientPromise?;
|
|
147
|
+
constructor(options?: PusherTransportConfig);
|
|
148
|
+
private client;
|
|
149
|
+
/**
|
|
150
|
+
* `delivery` is accepted for contract parity and ignored: Pusher pushes over a
|
|
151
|
+
* connection the client already holds open, so there is no dozing radio to
|
|
152
|
+
* wake and no per-message priority, TTL or collapse key to set.
|
|
153
|
+
*
|
|
154
|
+
* @param channel
|
|
155
|
+
* @param event
|
|
156
|
+
* @param payload
|
|
157
|
+
* @param _delivery
|
|
158
|
+
* @returns
|
|
159
|
+
*/
|
|
160
|
+
broadcast(channel: string | string[], event: string, payload: RealtimeBroadcastPayload, _delivery?: RealtimeDeliveryOptions): Promise<unknown>;
|
|
161
|
+
/**
|
|
162
|
+
* Authourize a pusher channel
|
|
163
|
+
*
|
|
164
|
+
* @param socketId
|
|
165
|
+
* @param channel
|
|
166
|
+
* @returns
|
|
167
|
+
*/
|
|
168
|
+
auth(socketId: string, channel: string, data?: Parameters<PusherClient['authorizeChannel']>[2]): Promise<ReturnType<PusherClient['authorizeChannel']>>;
|
|
169
|
+
/**
|
|
170
|
+
* Register a realtime autorization route
|
|
171
|
+
*
|
|
172
|
+
* @param authEndpoint
|
|
173
|
+
* @param middleware
|
|
174
|
+
* @param config
|
|
175
|
+
* @param channelPrefix
|
|
176
|
+
*/
|
|
177
|
+
static registerAuthRoute(authEndpoint?: string, middleware?: unknown | unknown[], channelPrefix?: string, config?: PusherTransportConfig): Promise<void>;
|
|
178
|
+
/**
|
|
179
|
+
* Register a realtime autorization route
|
|
180
|
+
*
|
|
181
|
+
* @param authEndpoint
|
|
182
|
+
* @param middleware
|
|
183
|
+
* @param channelPrefix
|
|
184
|
+
*/
|
|
185
|
+
registerAuthRoute(authEndpoint?: string, middleware?: unknown | unknown[], channelPrefix?: string): Promise<void>;
|
|
186
|
+
}
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region src/Contracts/RealtimeDriver.d.ts
|
|
189
|
+
/**
|
|
190
|
+
* A realtime transport (Pusher, Firebase, …) broadcasts a notification payload
|
|
191
|
+
* to a channel/topic that connected clients subscribe to.
|
|
192
|
+
*
|
|
193
|
+
* `channel` may be an array: for Pusher it fans out to multiple channels, and
|
|
194
|
+
* for Firebase it is treated as a list of device registration tokens delivered
|
|
195
|
+
* via a multicast send.
|
|
196
|
+
*
|
|
197
|
+
* `delivery` carries transport-level hints — priority, TTL, collapse key. It is
|
|
198
|
+
* optional so existing implementations stay source-compatible, and transports
|
|
199
|
+
* without a notion of priority (Pusher) are free to ignore it.
|
|
200
|
+
*/
|
|
201
|
+
interface RealtimeDriver {
|
|
202
|
+
broadcast(channel: string | string[], event: string, payload: RealtimeBroadcastPayload, delivery?: RealtimeDeliveryOptions): Promise<unknown>;
|
|
203
|
+
auth(socketId: string, channel: string, data?: unknown): unknown | Promise<unknown>;
|
|
204
|
+
registerAuthRoute(authEndpoint?: string, middleware?: unknown | unknown[], channelPrefix?: string): Promise<void>;
|
|
205
|
+
}
|
|
206
|
+
type RealtimeNotificationDriver<T extends RealtimeDriverName> = T extends 'firebase' ? FirebaseRealtimeDriver : T extends 'pusher' ? PusherRealtimeDriver : RealtimeDriver;
|
|
207
|
+
//#endregion
|
|
11
208
|
//#region src/Contracts/UserNotification.d.ts
|
|
12
209
|
declare abstract class UserNotification extends Model {
|
|
13
210
|
[key: string]: any;
|
|
@@ -92,6 +289,53 @@ type FirebaseTransportConfig = {
|
|
|
92
289
|
private_key?: string;
|
|
93
290
|
app_name?: string;
|
|
94
291
|
admin_sdk_path?: string;
|
|
292
|
+
/** Delivery options applied to every send on this transport. */
|
|
293
|
+
delivery?: RealtimeDeliveryOptions;
|
|
294
|
+
};
|
|
295
|
+
/**
|
|
296
|
+
* Per-send delivery hints for push transports.
|
|
297
|
+
*
|
|
298
|
+
* FCM defaults a data message to normal priority, which Doze and App Standby are
|
|
299
|
+
* free to defer until the next maintenance window — exactly the state a phone is
|
|
300
|
+
* in when something time-critical — an approval someone is waiting on, an alert
|
|
301
|
+
* about to go stale — needs to wake it.
|
|
302
|
+
* `priority: 'high'` is what exempts the message.
|
|
303
|
+
*
|
|
304
|
+
* Only the Firebase transport acts on these; Pusher holds an open connection and
|
|
305
|
+
* has no equivalent, so it ignores them.
|
|
306
|
+
*/
|
|
307
|
+
type RealtimeDeliveryOptions = {
|
|
308
|
+
/**
|
|
309
|
+
* `high` wakes a dozing device (FCM `android.priority`, APNs priority 10).
|
|
310
|
+
* Defaults to `normal`. Budgeted by FCM — reserve it for messages a user is
|
|
311
|
+
* actually waiting on.
|
|
312
|
+
*/
|
|
313
|
+
priority?: 'normal' | 'high';
|
|
314
|
+
/**
|
|
315
|
+
* How long, in **seconds**, the message stays worth delivering. FCM's own
|
|
316
|
+
* default is four weeks; anything time-critical wants far less, so a stale
|
|
317
|
+
* signal expires instead of surfacing long after the moment it described has
|
|
318
|
+
* passed. `0` asks for now-or-never.
|
|
319
|
+
*/
|
|
320
|
+
ttl?: number;
|
|
321
|
+
/**
|
|
322
|
+
* Supersede an undelivered message with the same key rather than stacking a
|
|
323
|
+
* second one — e.g. the id of whatever is being signalled, so a retry replaces
|
|
324
|
+
* the message it repeats.
|
|
325
|
+
*/
|
|
326
|
+
collapseKey?: string;
|
|
327
|
+
/**
|
|
328
|
+
* Merged over the derived `android` block; escape hatch for anything unmapped.
|
|
329
|
+
*/
|
|
330
|
+
android?: Record<string, unknown>;
|
|
331
|
+
/**
|
|
332
|
+
* Merged over the derived `apns` block; escape hatch for anything unmapped.
|
|
333
|
+
*/
|
|
334
|
+
apns?: Record<string, unknown>;
|
|
335
|
+
/**
|
|
336
|
+
* Merged over the derived `webpush` block; escape hatch for anything unmapped.
|
|
337
|
+
*/
|
|
338
|
+
webpush?: Record<string, unknown>;
|
|
95
339
|
};
|
|
96
340
|
type RealtimeDriverOptions<T extends RealtimeDriverName = RealtimeDriverName> = {
|
|
97
341
|
transport?: T;
|
|
@@ -101,6 +345,17 @@ type RealtimeDriverOptions<T extends RealtimeDriverName = RealtimeDriverName> =
|
|
|
101
345
|
event?: string;
|
|
102
346
|
/** Also persist the notification to the database (requires a User recipient). */
|
|
103
347
|
store?: boolean;
|
|
348
|
+
/** Delivery hints (priority, TTL, collapse key) for push transports. */
|
|
349
|
+
delivery?: RealtimeDeliveryOptions;
|
|
350
|
+
/**
|
|
351
|
+
* Build the driver yourself, bypassing the built-ins. Anything satisfying
|
|
352
|
+
* {@link RealtimeDriver} works — a transport this package does not ship (APNs
|
|
353
|
+
* or PushKit, say), or a fake in a test. Supplying this ignores `transport`.
|
|
354
|
+
*
|
|
355
|
+
* Called synchronously; do connection setup lazily inside the driver, as the
|
|
356
|
+
* bundled ones do, so constructing a notification never blocks on a network.
|
|
357
|
+
*/
|
|
358
|
+
driverFactory?: () => RealtimeDriver;
|
|
104
359
|
pusher?: PusherTransportConfig;
|
|
105
360
|
firebase?: FirebaseTransportConfig;
|
|
106
361
|
};
|
|
@@ -113,9 +368,33 @@ type RealtimeNotificationPayload = {
|
|
|
113
368
|
actionText?: string | null;
|
|
114
369
|
actionLink?: string | null;
|
|
115
370
|
meta?: NotificationData | null;
|
|
371
|
+
/**
|
|
372
|
+
* Stable identity for what this notification is *about* — an order, an
|
|
373
|
+
* incident, an approval. A later notification carrying the same tag supersedes
|
|
374
|
+
* this one rather than stacking beside it, so a client can replace what it
|
|
375
|
+
* already showed. Unbounded and client-side; unrelated to the transport's
|
|
376
|
+
* `collapseKey`, which is a queue slot with a much smaller budget.
|
|
377
|
+
*/
|
|
378
|
+
tag?: string | null;
|
|
379
|
+
/**
|
|
380
|
+
* Marks this message as an instruction to *remove* the notification carrying
|
|
381
|
+
* `tag`, rather than as a notification to display. Absent means display.
|
|
382
|
+
*
|
|
383
|
+
* Prefer superseding with replacement content where there is any: a retraction
|
|
384
|
+
* has nothing to show, so it must travel as a silent push, and silent pushes
|
|
385
|
+
* are the ones platforms throttle hardest.
|
|
386
|
+
*/
|
|
387
|
+
retracted?: boolean;
|
|
116
388
|
read_at: string | null;
|
|
117
389
|
created_at: string;
|
|
118
390
|
};
|
|
391
|
+
/**
|
|
392
|
+
* What a transport will carry. Notifications are the common case and what the
|
|
393
|
+
* builder produces, but a channel is just a channel — an application can push its
|
|
394
|
+
* own event shapes over the same one and pick them up with the client's
|
|
395
|
+
* `listen()`, which has always accepted arbitrary events.
|
|
396
|
+
*/
|
|
397
|
+
type RealtimeBroadcastPayload = RealtimeNotificationPayload | Record<string, unknown>;
|
|
119
398
|
/** The result of a realtime broadcast (plus the stored record when `store` is on). */
|
|
120
399
|
type RealtimeBroadcastResult = {
|
|
121
400
|
channel: string | string[];
|
|
@@ -158,13 +437,17 @@ interface NotificationConfig<T = any> {
|
|
|
158
437
|
table: string;
|
|
159
438
|
};
|
|
160
439
|
realtime?: {
|
|
161
|
-
transport
|
|
440
|
+
transport?: RealtimeDriverName;
|
|
162
441
|
/** Prefix for the per-user channel/topic (default `user.`). */
|
|
163
442
|
channel_prefix?: string;
|
|
164
443
|
/** Event name clients subscribe to (default `notification`). */
|
|
165
444
|
event?: string;
|
|
166
445
|
/** Persist broadcasts to the database by default. */
|
|
167
446
|
store?: boolean;
|
|
447
|
+
/** Delivery hints applied to every broadcast unless overridden per-send. */
|
|
448
|
+
delivery?: RealtimeDeliveryOptions;
|
|
449
|
+
/** Build the driver yourself for every realtime notification. */
|
|
450
|
+
driverFactory?: () => RealtimeDriver;
|
|
168
451
|
};
|
|
169
452
|
};
|
|
170
453
|
transports: {
|
|
@@ -295,7 +578,7 @@ declare class DbNotification extends NotificationContract<UserNotification> {
|
|
|
295
578
|
* @param realtime Set to false to disable realtime broadcast
|
|
296
579
|
*/
|
|
297
580
|
broadcast(realtime?: boolean): void;
|
|
298
|
-
create(user: User, payload: DbNotificationPayload): Promise<
|
|
581
|
+
create(user: User, payload: DbNotificationPayload): Promise<UserNotification>;
|
|
299
582
|
/**
|
|
300
583
|
* Send the database notification
|
|
301
584
|
*
|
|
@@ -305,7 +588,7 @@ declare class DbNotification extends NotificationContract<UserNotification> {
|
|
|
305
588
|
* @param data
|
|
306
589
|
* @returns
|
|
307
590
|
*/
|
|
308
|
-
send(message: string, subject?: string, _recipient?: NotificationRecipient, data?: NotificationData): Promise<
|
|
591
|
+
send(message: string, subject?: string, _recipient?: NotificationRecipient, data?: NotificationData): Promise<UserNotification>;
|
|
309
592
|
}
|
|
310
593
|
//#endregion
|
|
311
594
|
//#region src/drivers/MailNotification.d.ts
|
|
@@ -397,136 +680,6 @@ declare class MailNotification extends NotificationContract {
|
|
|
397
680
|
private normalizeRecipient;
|
|
398
681
|
}
|
|
399
682
|
//#endregion
|
|
400
|
-
//#region src/drivers/realtime/FirebaseRealtimeDriver.d.ts
|
|
401
|
-
/**
|
|
402
|
-
* The outcome of a token multicast: totals plus the tokens FCM rejected as dead.
|
|
403
|
-
*/
|
|
404
|
-
interface FirebaseMulticastResult {
|
|
405
|
-
successCount: number;
|
|
406
|
-
failureCount: number;
|
|
407
|
-
/**
|
|
408
|
-
* Tokens FCM reported as unregistered/invalid — delete these from your store.
|
|
409
|
-
*/
|
|
410
|
-
invalidTokens: string[];
|
|
411
|
-
}
|
|
412
|
-
/**
|
|
413
|
-
* Broadcasts notifications over [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging).
|
|
414
|
-
* A single channel maps to an FCM topic; an array of channels is treated as
|
|
415
|
-
* device registration tokens and delivered via a multicast send (chunked to
|
|
416
|
-
* FCM's 500-token limit), returning the tokens that should be pruned.
|
|
417
|
-
*
|
|
418
|
-
* `firebase-admin` is an optional peer dependency, imported lazily so the
|
|
419
|
-
* package installs without it; it is only required when this transport is used.
|
|
420
|
-
* FCM data values must be strings, so the payload is JSON-encoded.
|
|
421
|
-
*/
|
|
422
|
-
declare class FirebaseRealtimeDriver implements RealtimeDriver {
|
|
423
|
-
private options;
|
|
424
|
-
private messagingPromise?;
|
|
425
|
-
constructor(options?: FirebaseTransportConfig);
|
|
426
|
-
private messaging;
|
|
427
|
-
broadcast(channel: string | string[], event: string, payload: RealtimeNotificationPayload): Promise<string | FirebaseMulticastResult>;
|
|
428
|
-
/**
|
|
429
|
-
* Firebase channels do not require server-side authorization.
|
|
430
|
-
*
|
|
431
|
-
* @param _socketId
|
|
432
|
-
* @param _channel
|
|
433
|
-
* @param _data
|
|
434
|
-
*/
|
|
435
|
-
auth(_socketId: string, _channel: string, _data?: unknown): Promise<void>;
|
|
436
|
-
/**
|
|
437
|
-
* Firebase does not require an authorization route.
|
|
438
|
-
*
|
|
439
|
-
* @param _authEndpoint
|
|
440
|
-
* @param _middleware
|
|
441
|
-
* @param _channelPrefix
|
|
442
|
-
* @param _config
|
|
443
|
-
*/
|
|
444
|
-
static registerAuthRoute(_authEndpoint?: string, _middleware?: unknown | unknown[], _channelPrefix?: string, _config?: FirebaseTransportConfig): Promise<void>;
|
|
445
|
-
/**
|
|
446
|
-
* Firebase does not require an authorization route.
|
|
447
|
-
*
|
|
448
|
-
* @param _authEndpoint
|
|
449
|
-
* @param _middleware
|
|
450
|
-
* @param _channelPrefix
|
|
451
|
-
*/
|
|
452
|
-
registerAuthRoute(_authEndpoint?: string, _middleware?: unknown | unknown[], _channelPrefix?: string): Promise<void>;
|
|
453
|
-
/**
|
|
454
|
-
* Send to many device tokens at once, chunked to FCM's 500-token limit, and
|
|
455
|
-
* collect the tokens FCM rejects as dead so the caller can prune them.
|
|
456
|
-
*/
|
|
457
|
-
private multicast;
|
|
458
|
-
}
|
|
459
|
-
//#endregion
|
|
460
|
-
//#region src/drivers/realtime/PusherRealtimeDriver.d.ts
|
|
461
|
-
/** The slice of the `pusher` server SDK this driver uses. */
|
|
462
|
-
interface PusherClient {
|
|
463
|
-
trigger(channel: string | string[], event: string, data: unknown): Promise<unknown>;
|
|
464
|
-
authorizeChannel(socketId: string, channel: string, data?: {
|
|
465
|
-
user_id: string;
|
|
466
|
-
user_info?: {
|
|
467
|
-
[key: string]: any;
|
|
468
|
-
};
|
|
469
|
-
}): {
|
|
470
|
-
auth: string;
|
|
471
|
-
channel_data?: string;
|
|
472
|
-
shared_secret?: string;
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
/**
|
|
476
|
-
* Broadcasts notifications over [Pusher Channels](https://pusher.com/channels).
|
|
477
|
-
*
|
|
478
|
-
* The `pusher` server SDK is an optional peer dependency, imported lazily so the
|
|
479
|
-
* package installs without it; it is only required when this transport is used.
|
|
480
|
-
*/
|
|
481
|
-
declare class PusherRealtimeDriver implements RealtimeDriver {
|
|
482
|
-
private options;
|
|
483
|
-
private clientPromise?;
|
|
484
|
-
constructor(options?: PusherTransportConfig);
|
|
485
|
-
private client;
|
|
486
|
-
broadcast(channel: string | string[], event: string, payload: RealtimeNotificationPayload): Promise<unknown>;
|
|
487
|
-
/**
|
|
488
|
-
* Authourize a pusher channel
|
|
489
|
-
*
|
|
490
|
-
* @param socketId
|
|
491
|
-
* @param channel
|
|
492
|
-
* @returns
|
|
493
|
-
*/
|
|
494
|
-
auth(socketId: string, channel: string, data?: Parameters<PusherClient['authorizeChannel']>[2]): Promise<ReturnType<PusherClient['authorizeChannel']>>;
|
|
495
|
-
/**
|
|
496
|
-
* Register a realtime autorization route
|
|
497
|
-
*
|
|
498
|
-
* @param authEndpoint
|
|
499
|
-
* @param middleware
|
|
500
|
-
* @param config
|
|
501
|
-
* @param channelPrefix
|
|
502
|
-
*/
|
|
503
|
-
static registerAuthRoute(authEndpoint?: string, middleware?: unknown | unknown[], channelPrefix?: string, config?: PusherTransportConfig): Promise<void>;
|
|
504
|
-
/**
|
|
505
|
-
* Register a realtime autorization route
|
|
506
|
-
*
|
|
507
|
-
* @param authEndpoint
|
|
508
|
-
* @param middleware
|
|
509
|
-
* @param channelPrefix
|
|
510
|
-
*/
|
|
511
|
-
registerAuthRoute(authEndpoint?: string, middleware?: unknown | unknown[], channelPrefix?: string): Promise<void>;
|
|
512
|
-
}
|
|
513
|
-
//#endregion
|
|
514
|
-
//#region src/Contracts/RealtimeDriver.d.ts
|
|
515
|
-
/**
|
|
516
|
-
* A realtime transport (Pusher, Firebase, …) broadcasts a notification payload
|
|
517
|
-
* to a channel/topic that connected clients subscribe to.
|
|
518
|
-
*
|
|
519
|
-
* `channel` may be an array: for Pusher it fans out to multiple channels, and
|
|
520
|
-
* for Firebase it is treated as a list of device registration tokens delivered
|
|
521
|
-
* via a multicast send.
|
|
522
|
-
*/
|
|
523
|
-
interface RealtimeDriver {
|
|
524
|
-
broadcast(channel: string | string[], event: string, payload: RealtimeNotificationPayload): Promise<unknown>;
|
|
525
|
-
auth(socketId: string, channel: string, data?: unknown): unknown | Promise<unknown>;
|
|
526
|
-
registerAuthRoute(authEndpoint?: string, middleware?: unknown | unknown[], channelPrefix?: string): Promise<void>;
|
|
527
|
-
}
|
|
528
|
-
type RealtimeNotificationDriver<T extends RealtimeDriverName> = T extends 'firebase' ? FirebaseRealtimeDriver : T extends 'pusher' ? PusherRealtimeDriver : RealtimeDriver;
|
|
529
|
-
//#endregion
|
|
530
683
|
//#region src/drivers/RealtimeNotification.d.ts
|
|
531
684
|
/**
|
|
532
685
|
* Broadcasts a notification to connected clients over a realtime transport
|
|
@@ -535,7 +688,9 @@ type RealtimeNotificationDriver<T extends RealtimeDriverName> = T extends 'fireb
|
|
|
535
688
|
*/
|
|
536
689
|
declare class RealtimeNotification<T extends RealtimeDriverName = RealtimeDriverName> extends NotificationContract<RealtimeBroadcastResult> {
|
|
537
690
|
/**
|
|
538
|
-
* The underlying transport
|
|
691
|
+
* The underlying transport. Assignable, and configurable up front through
|
|
692
|
+
* `driverFactory`, so a transport this package does not ship — or a fake in a
|
|
693
|
+
* test — can stand in for the built-ins.
|
|
539
694
|
*/
|
|
540
695
|
driver: RealtimeNotificationDriver<T>;
|
|
541
696
|
private user?;
|
|
@@ -543,6 +698,9 @@ declare class RealtimeNotification<T extends RealtimeDriverName = RealtimeDriver
|
|
|
543
698
|
private eventName;
|
|
544
699
|
private channelPrefix;
|
|
545
700
|
private shouldStore;
|
|
701
|
+
private tagName?;
|
|
702
|
+
private isRetraction;
|
|
703
|
+
private deliveryOptions;
|
|
546
704
|
private payload;
|
|
547
705
|
constructor(options?: RealtimeDriverOptions<T>);
|
|
548
706
|
from(_: string): this;
|
|
@@ -590,6 +748,82 @@ declare class RealtimeNotification<T extends RealtimeDriverName = RealtimeDriver
|
|
|
590
748
|
type(type?: DbNotificationType | null): this;
|
|
591
749
|
action(text?: string | null, link?: string | null): this;
|
|
592
750
|
meta(meta?: NotificationData | null): this;
|
|
751
|
+
/**
|
|
752
|
+
* How hard the transport should work to deliver this message.
|
|
753
|
+
*
|
|
754
|
+
* FCM sends a data message at normal priority by default, and Doze and App
|
|
755
|
+
* Standby may hold a normal-priority message until the next maintenance
|
|
756
|
+
* window — which is precisely the state a phone is in when something needs to
|
|
757
|
+
* wake it. `high` is what exempts the message.
|
|
758
|
+
*
|
|
759
|
+
* FCM budgets high-priority sends, so reserve it for messages the user is
|
|
760
|
+
* actually waiting on. Pusher ignores this.
|
|
761
|
+
*
|
|
762
|
+
* @param priority
|
|
763
|
+
* @returns
|
|
764
|
+
*/
|
|
765
|
+
priority(priority?: 'normal' | 'high'): this;
|
|
766
|
+
/**
|
|
767
|
+
* How long, in seconds, this message is still worth delivering.
|
|
768
|
+
*
|
|
769
|
+
* FCM keeps a message for four weeks by default. Anything time-critical wants
|
|
770
|
+
* far less — a signal that arrives after the moment has passed is worse than
|
|
771
|
+
* one that never arrives — so pair a short TTL with `priority('high')`. `0`
|
|
772
|
+
* means deliver now or drop it.
|
|
773
|
+
*
|
|
774
|
+
* @param seconds
|
|
775
|
+
* @returns
|
|
776
|
+
*/
|
|
777
|
+
ttl(seconds: number): this;
|
|
778
|
+
/**
|
|
779
|
+
* Supersede an earlier undelivered message instead of stacking beside it.
|
|
780
|
+
* Key it by the thing being signalled — an approval id, say — so a repeat replaces
|
|
781
|
+
* the message it repeats.
|
|
782
|
+
*
|
|
783
|
+
* @param key
|
|
784
|
+
* @returns
|
|
785
|
+
*/
|
|
786
|
+
collapseKey(key: string): this;
|
|
787
|
+
/**
|
|
788
|
+
* Merge raw delivery options, including the per-platform `android` / `apns` /
|
|
789
|
+
* `webpush` escape hatches for anything the helpers above do not cover.
|
|
790
|
+
*
|
|
791
|
+
* @param delivery
|
|
792
|
+
* @returns
|
|
793
|
+
*/
|
|
794
|
+
delivery(delivery: RealtimeDeliveryOptions): this;
|
|
795
|
+
/**
|
|
796
|
+
* Give this notification a stable identity, so a later one can supersede it.
|
|
797
|
+
*
|
|
798
|
+
* Tag by the thing the notification is *about* — `order:42`, `incident:7` —
|
|
799
|
+
* not by the message. A client that already showed a notification with this
|
|
800
|
+
* tag replaces it in place instead of stacking a second one beside it.
|
|
801
|
+
*
|
|
802
|
+
* This is a client-side identity and is deliberately **not** wired to
|
|
803
|
+
* `collapseKey`: a collapse key is a transport queue slot, and FCM keeps only
|
|
804
|
+
* four per device, so auto-deriving one per tag would quietly evict others.
|
|
805
|
+
* When you want both — supersede what is queued *and* what was displayed —
|
|
806
|
+
* set both to the same value.
|
|
807
|
+
*
|
|
808
|
+
* @param tag
|
|
809
|
+
* @returns
|
|
810
|
+
*/
|
|
811
|
+
tag(tag: string): this;
|
|
812
|
+
/**
|
|
813
|
+
* Send an instruction to remove the notification carrying this tag, rather
|
|
814
|
+
* than a notification to display. Nothing is persisted, even under `store()`.
|
|
815
|
+
*
|
|
816
|
+
* Reach for this only when there is genuinely nothing left to say. Where the
|
|
817
|
+
* outcome has its own content — an approval answered elsewhere, a check that
|
|
818
|
+
* went from failing to passing — supersede it by sending that content under
|
|
819
|
+
* the same `tag()`. A replacement travels as an ordinary notification, while a
|
|
820
|
+
* retraction has nothing to display and so must travel silently, which is
|
|
821
|
+
* exactly what Doze, App Standby and iOS throttle hardest.
|
|
822
|
+
*
|
|
823
|
+
* @param retract
|
|
824
|
+
* @returns
|
|
825
|
+
*/
|
|
826
|
+
retract(retract?: boolean): this;
|
|
593
827
|
private resolveChannel;
|
|
594
828
|
/**
|
|
595
829
|
* Send a realtime notification using the default realtime driver
|
|
@@ -600,7 +834,7 @@ declare class RealtimeNotification<T extends RealtimeDriverName = RealtimeDriver
|
|
|
600
834
|
* @param data
|
|
601
835
|
* @returns
|
|
602
836
|
*/
|
|
603
|
-
send(message
|
|
837
|
+
send(message?: string, subject?: string, _recipient?: NotificationRecipient, data?: NotificationData): Promise<RealtimeBroadcastResult>;
|
|
604
838
|
}
|
|
605
839
|
//#endregion
|
|
606
840
|
//#region src/drivers/sms/AfricasTalkingSmsDriver.d.ts
|
|
@@ -724,8 +958,8 @@ declare class UserNotificationCenter {
|
|
|
724
958
|
* @param user
|
|
725
959
|
* @param payload
|
|
726
960
|
*/
|
|
727
|
-
static create(user: User, payload: DbNotificationPayload): Promise<
|
|
728
|
-
static forUser(user: User): Promise<ArkormCollection<
|
|
961
|
+
static create(user: User, payload: DbNotificationPayload): Promise<UserNotification>;
|
|
962
|
+
static forUser(user: User): Promise<ArkormCollection<UserNotification, UserNotification[]>>;
|
|
729
963
|
/**
|
|
730
964
|
* Fetch all the users unread messages
|
|
731
965
|
*
|
|
@@ -765,4 +999,4 @@ declare const configure: <T extends DotPath<NotificationConfig>>(key: T, default
|
|
|
765
999
|
//#region src/utils/template.d.ts
|
|
766
1000
|
declare const interpolate: (value: string, data?: NotificationData) => string;
|
|
767
1001
|
//#endregion
|
|
768
|
-
export { AfricasTalkingSmsDriver, DbNotification, DbNotificationPayload, DbNotificationType, DriverResult, FirebaseMulticastResult, FirebaseRealtimeDriver, FirebaseTransportConfig, MailDriverOptions, MailNotification, MailNotificationOptions, MailRecipient, MailRecipientAddress, MergedTransportConfig, Notification, NotificationChannel, NotificationConfig, NotificationContract, NotificationData, NotificationDriverMap, NotificationRecipient, PusherRealtimeDriver, PusherTransportConfig, RealtimeBroadcastResult, RealtimeDriver, RealtimeDriverName, RealtimeDriverOptions, RealtimeNotification, RealtimeNotificationDriver, RealtimeNotificationPayload, SmsDriverName, SmsDriverOptions, SmsNotification, Transport, TwilioSmsDriver, UserNotification, UserNotificationCenter, configure, interpolate };
|
|
1002
|
+
export { AfricasTalkingSmsDriver, DbNotification, DbNotificationPayload, DbNotificationType, DriverResult, FirebaseMulticastResult, FirebaseRealtimeDriver, FirebaseTransportConfig, MailDriverOptions, MailNotification, MailNotificationOptions, MailRecipient, MailRecipientAddress, MergedTransportConfig, Notification, NotificationChannel, NotificationConfig, NotificationContract, NotificationData, NotificationDriverMap, NotificationRecipient, PusherRealtimeDriver, PusherTransportConfig, RealtimeBroadcastPayload, RealtimeBroadcastResult, RealtimeDeliveryOptions, RealtimeDriver, RealtimeDriverName, RealtimeDriverOptions, RealtimeNotification, RealtimeNotificationDriver, RealtimeNotificationPayload, SmsDriverName, SmsDriverOptions, SmsNotification, Transport, TwilioSmsDriver, UserNotification, UserNotificationCenter, configure, interpolate };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Job } from "@arkstack/jobs";
|
|
2
1
|
import { RequestException, config as config$1, env, getModel } from "@arkstack/common";
|
|
2
|
+
import { Job } from "@arkstack/jobs";
|
|
3
3
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
4
|
import path, { join } from "node:path";
|
|
5
5
|
import nodemailer from "nodemailer";
|
|
@@ -74,7 +74,7 @@ var NotificationContract = class {
|
|
|
74
74
|
//#region src/UserNotificationCenter.ts
|
|
75
75
|
var UserNotificationCenter = class {
|
|
76
76
|
static async getModel() {
|
|
77
|
-
return
|
|
77
|
+
return getModel("UserNotification");
|
|
78
78
|
}
|
|
79
79
|
/**
|
|
80
80
|
* Create a database notification then broadcast it using the configured realtime driver
|
|
@@ -209,7 +209,7 @@ var DbNotification = class extends NotificationContract {
|
|
|
209
209
|
this.realtime = realtime;
|
|
210
210
|
}
|
|
211
211
|
async create(user, payload) {
|
|
212
|
-
|
|
212
|
+
getModel("UserNotification");
|
|
213
213
|
if (this.realtime) return await UserNotificationCenter.send(user, payload);
|
|
214
214
|
return await UserNotificationCenter.create(user, payload);
|
|
215
215
|
}
|
|
@@ -547,20 +547,133 @@ var FirebaseRealtimeDriver = class {
|
|
|
547
547
|
})();
|
|
548
548
|
return this.messagingPromise;
|
|
549
549
|
}
|
|
550
|
-
async broadcast(channel, event, payload) {
|
|
550
|
+
async broadcast(channel, event, payload, delivery) {
|
|
551
551
|
const messaging = await this.messaging();
|
|
552
552
|
const data = {
|
|
553
553
|
event,
|
|
554
554
|
payload: JSON.stringify(payload)
|
|
555
555
|
};
|
|
556
|
-
|
|
556
|
+
const overrides = this.overridesFor(delivery);
|
|
557
|
+
if (Array.isArray(channel)) return await this.multicast(messaging, channel, data, overrides);
|
|
557
558
|
const topic = channel.replace(/[^a-zA-Z0-9-_.~%]/g, "_");
|
|
558
559
|
return await messaging.send({
|
|
559
560
|
topic,
|
|
560
|
-
data
|
|
561
|
+
data,
|
|
562
|
+
...overrides
|
|
561
563
|
});
|
|
562
564
|
}
|
|
563
565
|
/**
|
|
566
|
+
* Translate transport-agnostic delivery hints into FCM's platform blocks.
|
|
567
|
+
*
|
|
568
|
+
* Per-send options win over the transport defaults, and the raw `android` /
|
|
569
|
+
* `apns` / `webpush` escape hatches are merged last so a caller can always
|
|
570
|
+
* override what this derives.
|
|
571
|
+
*
|
|
572
|
+
* `priority` always maps to Android and Web Push. Whether it maps to APNs
|
|
573
|
+
* depends on the message rather than on the platform: Apple requires a
|
|
574
|
+
* background push to be priority 5 or 1, and a data-only message — what this
|
|
575
|
+
* driver sends by default — arrives on iOS as exactly that. So the header is
|
|
576
|
+
* derived only once the message is demonstrably a *visible* push, which a
|
|
577
|
+
* caller declares through the `apns` escape hatch. Guessing otherwise would
|
|
578
|
+
* mean sending APNs something it is documented to reject.
|
|
579
|
+
*
|
|
580
|
+
* @param delivery
|
|
581
|
+
* @returns
|
|
582
|
+
*/
|
|
583
|
+
overridesFor(delivery) {
|
|
584
|
+
const base = this.options.delivery ?? {};
|
|
585
|
+
const merged = {
|
|
586
|
+
...base,
|
|
587
|
+
...delivery,
|
|
588
|
+
android: {
|
|
589
|
+
...base.android,
|
|
590
|
+
...delivery?.android
|
|
591
|
+
},
|
|
592
|
+
apns: {
|
|
593
|
+
...base.apns,
|
|
594
|
+
...delivery?.apns
|
|
595
|
+
},
|
|
596
|
+
webpush: {
|
|
597
|
+
...base.webpush,
|
|
598
|
+
...delivery?.webpush
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
const android = {};
|
|
602
|
+
const apnsHeaders = {};
|
|
603
|
+
const webpushHeaders = {};
|
|
604
|
+
if (merged.priority) {
|
|
605
|
+
android.priority = merged.priority;
|
|
606
|
+
webpushHeaders.Urgency = merged.priority;
|
|
607
|
+
if (this.isVisiblePush(merged.apns)) apnsHeaders["apns-priority"] = merged.priority === "high" ? "10" : "5";
|
|
608
|
+
}
|
|
609
|
+
if (merged.collapseKey) {
|
|
610
|
+
android.collapseKey = merged.collapseKey;
|
|
611
|
+
apnsHeaders["apns-collapse-id"] = merged.collapseKey;
|
|
612
|
+
}
|
|
613
|
+
if (typeof merged.ttl === "number") {
|
|
614
|
+
android.ttl = merged.ttl * 1e3;
|
|
615
|
+
apnsHeaders["apns-expiration"] = merged.ttl === 0 ? "0" : String(Math.floor(Date.now() / 1e3) + merged.ttl);
|
|
616
|
+
webpushHeaders.TTL = String(merged.ttl);
|
|
617
|
+
}
|
|
618
|
+
return {
|
|
619
|
+
...this.flatBlock("android", android, merged.android),
|
|
620
|
+
...this.headerBlock("apns", apnsHeaders, merged.apns),
|
|
621
|
+
...this.headerBlock("webpush", webpushHeaders, merged.webpush)
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Whether the caller has built a push iOS will actually display.
|
|
626
|
+
*
|
|
627
|
+
* Apple allows `apns-priority: 10` only on a push that shows the user
|
|
628
|
+
* something; a background push must be 5 or 1. A data-only message is a
|
|
629
|
+
* background push, so this is false for the default this driver sends, and
|
|
630
|
+
* true once a caller has declared an alert through the `apns` escape hatch —
|
|
631
|
+
* by push type, or by supplying an alert payload.
|
|
632
|
+
*
|
|
633
|
+
* @param apns
|
|
634
|
+
* @returns
|
|
635
|
+
*/
|
|
636
|
+
isVisiblePush(apns) {
|
|
637
|
+
const headers = apns?.headers;
|
|
638
|
+
const aps = (apns?.payload)?.aps;
|
|
639
|
+
return headers?.["apns-push-type"] === "alert" || aps?.alert !== void 0;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Fold a caller's raw override over the derived settings, dropping the block
|
|
643
|
+
* entirely when neither contributed anything.
|
|
644
|
+
*
|
|
645
|
+
* @param key
|
|
646
|
+
* @param derived
|
|
647
|
+
* @param override
|
|
648
|
+
* @returns
|
|
649
|
+
*/
|
|
650
|
+
flatBlock(key, derived, override) {
|
|
651
|
+
const block = {
|
|
652
|
+
...derived,
|
|
653
|
+
...override
|
|
654
|
+
};
|
|
655
|
+
return Object.keys(block).length > 0 ? { [key]: block } : {};
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* As `flatBlock`, but for the platforms whose settings live under `headers` —
|
|
659
|
+
* merged key by key, so an override adding one header keeps the rest.
|
|
660
|
+
*
|
|
661
|
+
* @param key
|
|
662
|
+
* @param derived
|
|
663
|
+
* @param override
|
|
664
|
+
* @returns
|
|
665
|
+
*/
|
|
666
|
+
headerBlock(key, derived, override) {
|
|
667
|
+
const { headers: overrideHeaders, ...rest } = override ?? {};
|
|
668
|
+
const headers = {
|
|
669
|
+
...derived,
|
|
670
|
+
...overrideHeaders
|
|
671
|
+
};
|
|
672
|
+
const block = { ...rest };
|
|
673
|
+
if (Object.keys(headers).length > 0) block.headers = headers;
|
|
674
|
+
return Object.keys(block).length > 0 ? { [key]: block } : {};
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
564
677
|
* Firebase channels do not require server-side authorization.
|
|
565
678
|
*
|
|
566
679
|
* @param _socketId
|
|
@@ -589,7 +702,7 @@ var FirebaseRealtimeDriver = class {
|
|
|
589
702
|
* Send to many device tokens at once, chunked to FCM's 500-token limit, and
|
|
590
703
|
* collect the tokens FCM rejects as dead so the caller can prune them.
|
|
591
704
|
*/
|
|
592
|
-
async multicast(messaging, tokens, data) {
|
|
705
|
+
async multicast(messaging, tokens, data, overrides = {}) {
|
|
593
706
|
const result = {
|
|
594
707
|
successCount: 0,
|
|
595
708
|
failureCount: 0,
|
|
@@ -599,7 +712,8 @@ var FirebaseRealtimeDriver = class {
|
|
|
599
712
|
const batch = tokens.slice(i, i + MULTICAST_LIMIT);
|
|
600
713
|
const response = await messaging.sendEachForMulticast({
|
|
601
714
|
tokens: batch,
|
|
602
|
-
data
|
|
715
|
+
data,
|
|
716
|
+
...overrides
|
|
603
717
|
});
|
|
604
718
|
result.successCount += response.successCount;
|
|
605
719
|
result.failureCount += response.failureCount;
|
|
@@ -640,7 +754,18 @@ var PusherRealtimeDriver = class PusherRealtimeDriver {
|
|
|
640
754
|
})();
|
|
641
755
|
return this.clientPromise;
|
|
642
756
|
}
|
|
643
|
-
|
|
757
|
+
/**
|
|
758
|
+
* `delivery` is accepted for contract parity and ignored: Pusher pushes over a
|
|
759
|
+
* connection the client already holds open, so there is no dozing radio to
|
|
760
|
+
* wake and no per-message priority, TTL or collapse key to set.
|
|
761
|
+
*
|
|
762
|
+
* @param channel
|
|
763
|
+
* @param event
|
|
764
|
+
* @param payload
|
|
765
|
+
* @param _delivery
|
|
766
|
+
* @returns
|
|
767
|
+
*/
|
|
768
|
+
async broadcast(channel, event, payload, _delivery) {
|
|
644
769
|
return await (await this.client()).trigger(channel, event, payload);
|
|
645
770
|
}
|
|
646
771
|
/**
|
|
@@ -704,8 +829,10 @@ var PusherRealtimeDriver = class PusherRealtimeDriver {
|
|
|
704
829
|
* when `store` is enabled, is also persisted so the client can load history.
|
|
705
830
|
*/
|
|
706
831
|
var RealtimeNotification = class extends NotificationContract {
|
|
707
|
-
/**
|
|
708
|
-
* The underlying transport
|
|
832
|
+
/**
|
|
833
|
+
* The underlying transport. Assignable, and configurable up front through
|
|
834
|
+
* `driverFactory`, so a transport this package does not ship — or a fake in a
|
|
835
|
+
* test — can stand in for the built-ins.
|
|
709
836
|
*/
|
|
710
837
|
driver;
|
|
711
838
|
user;
|
|
@@ -713,6 +840,9 @@ var RealtimeNotification = class extends NotificationContract {
|
|
|
713
840
|
eventName;
|
|
714
841
|
channelPrefix;
|
|
715
842
|
shouldStore;
|
|
843
|
+
tagName;
|
|
844
|
+
isRetraction = false;
|
|
845
|
+
deliveryOptions;
|
|
716
846
|
payload = {};
|
|
717
847
|
constructor(options = {}) {
|
|
718
848
|
super();
|
|
@@ -723,7 +853,12 @@ var RealtimeNotification = class extends NotificationContract {
|
|
|
723
853
|
this.eventName = options.event ?? driverConfig?.event ?? "notification";
|
|
724
854
|
this.channelPrefix = driverConfig?.channel_prefix ?? "user.";
|
|
725
855
|
this.shouldStore = options.store ?? driverConfig?.store ?? false;
|
|
726
|
-
this.
|
|
856
|
+
this.deliveryOptions = {
|
|
857
|
+
...driverConfig?.delivery,
|
|
858
|
+
...options.delivery
|
|
859
|
+
};
|
|
860
|
+
const factory = options.driverFactory ?? driverConfig?.driverFactory;
|
|
861
|
+
this.driver = factory ? factory() : transport === "firebase" ? new FirebaseRealtimeDriver({
|
|
727
862
|
...transportConfig,
|
|
728
863
|
...options.firebase
|
|
729
864
|
}) : new PusherRealtimeDriver({
|
|
@@ -811,6 +946,103 @@ var RealtimeNotification = class extends NotificationContract {
|
|
|
811
946
|
this.payload.meta = meta;
|
|
812
947
|
return this;
|
|
813
948
|
}
|
|
949
|
+
/**
|
|
950
|
+
* How hard the transport should work to deliver this message.
|
|
951
|
+
*
|
|
952
|
+
* FCM sends a data message at normal priority by default, and Doze and App
|
|
953
|
+
* Standby may hold a normal-priority message until the next maintenance
|
|
954
|
+
* window — which is precisely the state a phone is in when something needs to
|
|
955
|
+
* wake it. `high` is what exempts the message.
|
|
956
|
+
*
|
|
957
|
+
* FCM budgets high-priority sends, so reserve it for messages the user is
|
|
958
|
+
* actually waiting on. Pusher ignores this.
|
|
959
|
+
*
|
|
960
|
+
* @param priority
|
|
961
|
+
* @returns
|
|
962
|
+
*/
|
|
963
|
+
priority(priority = "high") {
|
|
964
|
+
this.deliveryOptions.priority = priority;
|
|
965
|
+
return this;
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* How long, in seconds, this message is still worth delivering.
|
|
969
|
+
*
|
|
970
|
+
* FCM keeps a message for four weeks by default. Anything time-critical wants
|
|
971
|
+
* far less — a signal that arrives after the moment has passed is worse than
|
|
972
|
+
* one that never arrives — so pair a short TTL with `priority('high')`. `0`
|
|
973
|
+
* means deliver now or drop it.
|
|
974
|
+
*
|
|
975
|
+
* @param seconds
|
|
976
|
+
* @returns
|
|
977
|
+
*/
|
|
978
|
+
ttl(seconds) {
|
|
979
|
+
this.deliveryOptions.ttl = seconds;
|
|
980
|
+
return this;
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Supersede an earlier undelivered message instead of stacking beside it.
|
|
984
|
+
* Key it by the thing being signalled — an approval id, say — so a repeat replaces
|
|
985
|
+
* the message it repeats.
|
|
986
|
+
*
|
|
987
|
+
* @param key
|
|
988
|
+
* @returns
|
|
989
|
+
*/
|
|
990
|
+
collapseKey(key) {
|
|
991
|
+
this.deliveryOptions.collapseKey = key;
|
|
992
|
+
return this;
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Merge raw delivery options, including the per-platform `android` / `apns` /
|
|
996
|
+
* `webpush` escape hatches for anything the helpers above do not cover.
|
|
997
|
+
*
|
|
998
|
+
* @param delivery
|
|
999
|
+
* @returns
|
|
1000
|
+
*/
|
|
1001
|
+
delivery(delivery) {
|
|
1002
|
+
this.deliveryOptions = {
|
|
1003
|
+
...this.deliveryOptions,
|
|
1004
|
+
...delivery
|
|
1005
|
+
};
|
|
1006
|
+
return this;
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Give this notification a stable identity, so a later one can supersede it.
|
|
1010
|
+
*
|
|
1011
|
+
* Tag by the thing the notification is *about* — `order:42`, `incident:7` —
|
|
1012
|
+
* not by the message. A client that already showed a notification with this
|
|
1013
|
+
* tag replaces it in place instead of stacking a second one beside it.
|
|
1014
|
+
*
|
|
1015
|
+
* This is a client-side identity and is deliberately **not** wired to
|
|
1016
|
+
* `collapseKey`: a collapse key is a transport queue slot, and FCM keeps only
|
|
1017
|
+
* four per device, so auto-deriving one per tag would quietly evict others.
|
|
1018
|
+
* When you want both — supersede what is queued *and* what was displayed —
|
|
1019
|
+
* set both to the same value.
|
|
1020
|
+
*
|
|
1021
|
+
* @param tag
|
|
1022
|
+
* @returns
|
|
1023
|
+
*/
|
|
1024
|
+
tag(tag) {
|
|
1025
|
+
this.tagName = tag;
|
|
1026
|
+
return this;
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Send an instruction to remove the notification carrying this tag, rather
|
|
1030
|
+
* than a notification to display. Nothing is persisted, even under `store()`.
|
|
1031
|
+
*
|
|
1032
|
+
* Reach for this only when there is genuinely nothing left to say. Where the
|
|
1033
|
+
* outcome has its own content — an approval answered elsewhere, a check that
|
|
1034
|
+
* went from failing to passing — supersede it by sending that content under
|
|
1035
|
+
* the same `tag()`. A replacement travels as an ordinary notification, while a
|
|
1036
|
+
* retraction has nothing to display and so must travel silently, which is
|
|
1037
|
+
* exactly what Doze, App Standby and iOS throttle hardest.
|
|
1038
|
+
*
|
|
1039
|
+
* @param retract
|
|
1040
|
+
* @returns
|
|
1041
|
+
*/
|
|
1042
|
+
retract(retract = true) {
|
|
1043
|
+
this.isRetraction = retract;
|
|
1044
|
+
return this;
|
|
1045
|
+
}
|
|
814
1046
|
resolveChannel() {
|
|
815
1047
|
if (this.channelName !== void 0) return this.channelName;
|
|
816
1048
|
if (this.user) return `${this.channelPrefix}${this.user.id}`;
|
|
@@ -825,7 +1057,7 @@ var RealtimeNotification = class extends NotificationContract {
|
|
|
825
1057
|
* @param data
|
|
826
1058
|
* @returns
|
|
827
1059
|
*/
|
|
828
|
-
async send(message, subject, _recipient, data) {
|
|
1060
|
+
async send(message = "", subject, _recipient, data) {
|
|
829
1061
|
const channel = this.resolveChannel();
|
|
830
1062
|
const mergedData = this.mergeData(data);
|
|
831
1063
|
const base = {
|
|
@@ -836,7 +1068,7 @@ var RealtimeNotification = class extends NotificationContract {
|
|
|
836
1068
|
actionLink: this.payload.actionLink ?? null,
|
|
837
1069
|
meta: this.payload.meta ?? null
|
|
838
1070
|
};
|
|
839
|
-
const stored = this.shouldStore && this.user ? await UserNotificationCenter.create(this.user, base) : void 0;
|
|
1071
|
+
const stored = this.shouldStore && this.user && !this.isRetraction ? await UserNotificationCenter.create(this.user, base) : void 0;
|
|
840
1072
|
const payload = {
|
|
841
1073
|
id: stored ? String(stored.id) : randomUUID(),
|
|
842
1074
|
type: base.type ?? null,
|
|
@@ -845,10 +1077,12 @@ var RealtimeNotification = class extends NotificationContract {
|
|
|
845
1077
|
actionText: base.actionText ?? null,
|
|
846
1078
|
actionLink: base.actionLink ?? null,
|
|
847
1079
|
meta: base.meta ?? null,
|
|
1080
|
+
...this.tagName !== void 0 ? { tag: this.tagName } : {},
|
|
1081
|
+
...this.isRetraction ? { retracted: true } : {},
|
|
848
1082
|
read_at: stored?.readAt ? new Date(stored.readAt).toISOString() : null,
|
|
849
1083
|
created_at: stored?.createdAt ? new Date(stored.createdAt).toISOString() : (/* @__PURE__ */ new Date()).toISOString()
|
|
850
1084
|
};
|
|
851
|
-
if (channel && channel.length > 0) await this.driver.broadcast(channel, this.eventName, payload);
|
|
1085
|
+
if (channel && channel.length > 0) await this.driver.broadcast(channel, this.eventName, payload, this.deliveryOptions);
|
|
852
1086
|
return {
|
|
853
1087
|
channel,
|
|
854
1088
|
event: this.eventName,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arkstack/notifications",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Framework-agnostic notification module for Arkstack and Nodejs, providing support for multi-channel notification delivery.",
|
|
6
6
|
"homepage": "https://arkstack.toneflix.net/guide/notifications",
|
|
@@ -34,17 +34,17 @@
|
|
|
34
34
|
"africastalking": "^0.8.0",
|
|
35
35
|
"nodemailer": "^9.0.3",
|
|
36
36
|
"twilio": "^6.0.2",
|
|
37
|
-
"@arkstack/common": "^0.18.
|
|
37
|
+
"@arkstack/common": "^0.18.8"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@kanun-hq/plugin-phone": "^0.1.8",
|
|
41
41
|
"firebase-admin": "^13.0.0",
|
|
42
42
|
"pusher": "^5.2.0",
|
|
43
|
-
"@arkstack/
|
|
44
|
-
"@arkstack/
|
|
45
|
-
"@arkstack/driver-
|
|
46
|
-
"@arkstack/
|
|
47
|
-
"@arkstack/jobs": "^0.18.
|
|
43
|
+
"@arkstack/database": "^0.18.8",
|
|
44
|
+
"@arkstack/driver-express": "^0.18.8",
|
|
45
|
+
"@arkstack/driver-h3": "^0.18.8",
|
|
46
|
+
"@arkstack/contract": "^0.18.8",
|
|
47
|
+
"@arkstack/jobs": "^0.18.8"
|
|
48
48
|
},
|
|
49
49
|
"peerDependenciesMeta": {
|
|
50
50
|
"@arkstack/driver-express": {
|