@12-apps/notifications 4.7.1 → 4.8.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/ADOPTING.md +32 -0
- package/dist/{chunk-WHBMPHQE.js → chunk-7234ROOP.js} +8 -4
- package/dist/chunk-7234ROOP.js.map +1 -0
- package/dist/{chunk-XE7HZVMH.js → chunk-BGI4BQ66.js} +1 -1
- package/dist/chunk-BGI4BQ66.js.map +1 -0
- package/dist/{chunk-HHMRCMQU.js → chunk-GGAZOLNN.js} +2 -2
- package/dist/{chunk-CUZW62JS.js → chunk-KU4FVOFA.js} +2 -2
- package/dist/{chunk-MMLV4EZT.js → chunk-M5XTROYM.js} +2 -2
- package/dist/{create-api-notifications-B3u6Kx3x.d.ts → create-api-notifications-WaexrZnn.d.ts} +12 -4
- package/dist/{create-web-notifications-BHCzaU2y.d.ts → create-web-notifications-DrgqtU4Q.d.ts} +2 -2
- package/dist/{generators-B9xt3sRh.d.ts → generators-CH5m8Ucj.d.ts} +1 -1
- package/dist/hono/index.d.ts +5 -5
- package/dist/hono/index.js +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/{jobs-BDZ7aGHV.d.ts → jobs-CcsSA0YR.d.ts} +1 -1
- package/dist/manifest/server.d.ts +6 -6
- package/dist/manifest/server.js +4 -4
- package/dist/manifest/web.d.ts +3 -3
- package/dist/manifest/web.js +1 -1
- package/dist/{preferences-screen-IOW6Y2H2.js → preferences-screen-V3ZBWP7A.js} +2 -2
- package/dist/react/index.d.ts +4 -4
- package/dist/react/index.js +1 -1
- package/dist/server/index.d.ts +7 -7
- package/dist/server/index.js +4 -4
- package/dist/{types-CXLAG3UU.d.ts → types-CYJN1xCM.d.ts} +39 -2
- package/dist/web-push/index.d.ts +2 -2
- package/dist/{web-push-Cs14Wp9u.d.ts → web-push-iER5StBm.d.ts} +1 -1
- package/dist/{wire-6dzyfDE7.d.ts → wire-CmRrB3_s.d.ts} +1 -1
- package/package.json +2 -2
- package/src/index.ts +1 -0
- package/src/server/db.ts +12 -1
- package/src/server/dispatch.ts +4 -0
- package/src/server/router.ts +17 -1
- package/src/types.ts +39 -1
- package/dist/chunk-WHBMPHQE.js.map +0 -1
- package/dist/chunk-XE7HZVMH.js.map +0 -1
- /package/dist/{chunk-HHMRCMQU.js.map → chunk-GGAZOLNN.js.map} +0 -0
- /package/dist/{chunk-CUZW62JS.js.map → chunk-KU4FVOFA.js.map} +0 -0
- /package/dist/{chunk-MMLV4EZT.js.map → chunk-M5XTROYM.js.map} +0 -0
- /package/dist/{preferences-screen-IOW6Y2H2.js.map → preferences-screen-V3ZBWP7A.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -9,11 +9,11 @@ import {
|
|
|
9
9
|
mergeChoices,
|
|
10
10
|
mergeStoredRow,
|
|
11
11
|
normalizePhoneE164
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-GGAZOLNN.js";
|
|
13
13
|
import {
|
|
14
14
|
NOTIFICATION_CHANNELS,
|
|
15
15
|
taxonomyOf
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-BGI4BQ66.js";
|
|
17
17
|
import {
|
|
18
18
|
messagesOf
|
|
19
19
|
} from "./chunk-M2TVBVH2.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { WireJobBlueprint } from '@12-apps/wiring';
|
|
2
|
-
import { A as ApiNotifications } from './create-api-notifications-
|
|
2
|
+
import { A as ApiNotifications } from './create-api-notifications-WaexrZnn.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The two background jobs getting a message out actually needs.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as _12_apps_wiring from '@12-apps/wiring';
|
|
2
2
|
import { WireRequest } from '@12-apps/wiring';
|
|
3
|
-
import { N as NotificationsServerConfig, A as ApiNotifications, b as NotificationsRoute } from '../create-api-notifications-
|
|
4
|
-
import { N as NotificationsJobDeps } from '../jobs-
|
|
5
|
-
import '../generators-
|
|
6
|
-
import '../types-
|
|
7
|
-
import '../wire-
|
|
8
|
-
import '../web-push-
|
|
3
|
+
import { N as NotificationsServerConfig, A as ApiNotifications, b as NotificationsRoute } from '../create-api-notifications-WaexrZnn.js';
|
|
4
|
+
import { N as NotificationsJobDeps } from '../jobs-CcsSA0YR.js';
|
|
5
|
+
import '../generators-CH5m8Ucj.js';
|
|
6
|
+
import '../types-CYJN1xCM.js';
|
|
7
|
+
import '../wire-CmRrB3_s.js';
|
|
8
|
+
import '../web-push-iER5StBm.js';
|
|
9
9
|
|
|
10
10
|
/** One `NotificationsRoute` as the wiring contract reads it. */
|
|
11
11
|
declare function asWireRoute(route: NotificationsRoute): {
|
package/dist/manifest/server.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
NOTIFICATIONS_JOBS
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-KU4FVOFA.js";
|
|
4
4
|
import {
|
|
5
5
|
createApiNotifications
|
|
6
|
-
} from "../chunk-
|
|
7
|
-
import "../chunk-
|
|
8
|
-
import "../chunk-
|
|
6
|
+
} from "../chunk-7234ROOP.js";
|
|
7
|
+
import "../chunk-GGAZOLNN.js";
|
|
8
|
+
import "../chunk-BGI4BQ66.js";
|
|
9
9
|
import "../chunk-M2TVBVH2.js";
|
|
10
10
|
import {
|
|
11
11
|
__name
|
package/dist/manifest/web.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { c as createWebNotifications } from '../create-web-notifications-
|
|
1
|
+
import { c as createWebNotifications } from '../create-web-notifications-DrgqtU4Q.js';
|
|
2
2
|
import 'react';
|
|
3
|
-
import '../wire-
|
|
4
|
-
import '../types-
|
|
3
|
+
import '../wire-CmRrB3_s.js';
|
|
4
|
+
import '../types-CYJN1xCM.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* `@12-apps/notifications/manifest/web` — the web capabilities.
|
package/dist/manifest/web.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "./chunk-CQZMTFPY.js";
|
|
5
5
|
import {
|
|
6
6
|
NOTIFICATION_CHANNELS
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-BGI4BQ66.js";
|
|
8
8
|
import {
|
|
9
9
|
__name
|
|
10
10
|
} from "./chunk-7QVYU63E.js";
|
|
@@ -291,4 +291,4 @@ __name(PreferencesScreen, "PreferencesScreen");
|
|
|
291
291
|
export {
|
|
292
292
|
PreferencesScreen
|
|
293
293
|
};
|
|
294
|
-
//# sourceMappingURL=preferences-screen-
|
|
294
|
+
//# sourceMappingURL=preferences-screen-V3ZBWP7A.js.map
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { N as NotificationsApiClient } from '../create-web-notifications-
|
|
2
|
-
export { B as BADGE_POLL_MS, a as BADGE_RECONCILE_MS, b as BellButtonProps, I as InboxListStatus, d as InboxState, e as InboxStore, f as NotificationsHttpError, g as NotificationsPanelProps, h as NotificationsResult, i as NotificationsSignalHook, j as NotificationsSubscribe, k as NotificationsTransport, l as NotificationsWebConfig, P as PAGE_SIZE, m as PreferencesPayload, n as PreferencesScreenProps, o as PushRegistrationPayload, W as WebNotifications, p as WebPushPlatformHint, q as WebPushSetupConfig, r as createInboxStore, s as createNotificationsApiClient, c as createWebNotifications, t as httpNotificationsTransport, u as useInboxList, v as useInboxState, w as useUnreadCount } from '../create-web-notifications-
|
|
1
|
+
import { N as NotificationsApiClient } from '../create-web-notifications-DrgqtU4Q.js';
|
|
2
|
+
export { B as BADGE_POLL_MS, a as BADGE_RECONCILE_MS, b as BellButtonProps, I as InboxListStatus, d as InboxState, e as InboxStore, f as NotificationsHttpError, g as NotificationsPanelProps, h as NotificationsResult, i as NotificationsSignalHook, j as NotificationsSubscribe, k as NotificationsTransport, l as NotificationsWebConfig, P as PAGE_SIZE, m as PreferencesPayload, n as PreferencesScreenProps, o as PushRegistrationPayload, W as WebNotifications, p as WebPushPlatformHint, q as WebPushSetupConfig, r as createInboxStore, s as createNotificationsApiClient, c as createWebNotifications, t as httpNotificationsTransport, u as useInboxList, v as useInboxState, w as useUnreadCount } from '../create-web-notifications-DrgqtU4Q.js';
|
|
3
3
|
import { JSX } from 'react';
|
|
4
|
-
import { N as NotificationMessages } from '../wire-
|
|
5
|
-
import '../types-
|
|
4
|
+
import { N as NotificationMessages } from '../wire-CmRrB3_s.js';
|
|
5
|
+
import '../types-CYJN1xCM.js';
|
|
6
6
|
|
|
7
7
|
/** Inline SVG bell (no icon-library dependency in this package). */
|
|
8
8
|
|
package/dist/react/index.js
CHANGED
package/dist/server/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { A as ApiNotifications } from '../create-api-notifications-
|
|
2
|
-
export { C as CommittedNotification, E as EMAIL_DRIVERS, c as EmailDriver, d as EmailDriverDeclaration, e as EmailMessage, f as ExtraDrivers, L as ListNotificationsInput, g as NotificationAudienceDirectory, h as NotificationChannelPolicy, i as NotificationCommittedListener, j as NotificationContactDirectory, k as NotificationCreateData, l as NotificationDelegate, m as NotificationDeliveryDelegate, n as NotificationDeliveryRow, o as NotificationDeliveryWhere, p as NotificationDispatchScheduler, q as NotificationInboxStore, r as NotificationPageAfter, s as NotificationPreferenceDelegate, t as NotificationPreferenceRow, u as NotificationPreferenceStore, v as NotificationRouter, w as NotificationWhere, a as NotificationsActor, x as NotificationsApiError, y as NotificationsDb, z as NotificationsDbClient, B as NotificationsDbProvider, D as NotificationsRequest, F as NotificationsResponse, b as NotificationsRoute, N as NotificationsServerConfig, G as NotifyByPermission, H as NotifyOptions, I as NotifyResult, P as PermissionNotificationResult, J as PermissionNotificationSkip, K as PushSubscriptionDelegate, M as PushSubscriptionInput, O as PushSubscriptionRow, Q as PushSubscriptionStore, S as SMS_DRIVERS, R as SmsDriver, T as SmsDriverDeclaration, U as SmsMessage, V as TransportDeclaration, W as TransportRegistry, X as WHATSAPP_DRIVERS, Y as WhatsAppDriver, Z as WhatsAppDriverDeclaration, _ as WhatsAppMessage, $ as createApiNotifications, a0 as createTransportRegistry, a1 as emailTransport, a2 as foldApiError, a3 as formatEmail, a4 as formatSms, a5 as formatWhatsApp, a6 as ok, a7 as smsTransport, a8 as whatsAppTransport } from '../create-api-notifications-
|
|
3
|
-
export { D as DriverDeclarationBase, F as FetchImpl, N as NotificationProviderError, W as WEB_PUSH_DRIVERS, a as WebPushDriverDeclaration, b as WebPushMessage, c as WebPushSender, d as WebPushSubscription, e as WebPushSubscriptionSource, f as absoluteLink, g as formatWebPush, w as webPushTransport } from '../web-push-
|
|
1
|
+
import { A as ApiNotifications } from '../create-api-notifications-WaexrZnn.js';
|
|
2
|
+
export { C as CommittedNotification, E as EMAIL_DRIVERS, c as EmailDriver, d as EmailDriverDeclaration, e as EmailMessage, f as ExtraDrivers, L as ListNotificationsInput, g as NotificationAudienceDirectory, h as NotificationChannelPolicy, i as NotificationCommittedListener, j as NotificationContactDirectory, k as NotificationCreateData, l as NotificationDelegate, m as NotificationDeliveryDelegate, n as NotificationDeliveryRow, o as NotificationDeliveryWhere, p as NotificationDispatchScheduler, q as NotificationInboxStore, r as NotificationPageAfter, s as NotificationPreferenceDelegate, t as NotificationPreferenceRow, u as NotificationPreferenceStore, v as NotificationRouter, w as NotificationWhere, a as NotificationsActor, x as NotificationsApiError, y as NotificationsDb, z as NotificationsDbClient, B as NotificationsDbProvider, D as NotificationsRequest, F as NotificationsResponse, b as NotificationsRoute, N as NotificationsServerConfig, G as NotifyByPermission, H as NotifyOptions, I as NotifyResult, P as PermissionNotificationResult, J as PermissionNotificationSkip, K as PushSubscriptionDelegate, M as PushSubscriptionInput, O as PushSubscriptionRow, Q as PushSubscriptionStore, S as SMS_DRIVERS, R as SmsDriver, T as SmsDriverDeclaration, U as SmsMessage, V as TransportDeclaration, W as TransportRegistry, X as WHATSAPP_DRIVERS, Y as WhatsAppDriver, Z as WhatsAppDriverDeclaration, _ as WhatsAppMessage, $ as createApiNotifications, a0 as createTransportRegistry, a1 as emailTransport, a2 as foldApiError, a3 as formatEmail, a4 as formatSms, a5 as formatWhatsApp, a6 as ok, a7 as smsTransport, a8 as whatsAppTransport } from '../create-api-notifications-WaexrZnn.js';
|
|
3
|
+
export { D as DriverDeclarationBase, F as FetchImpl, N as NotificationProviderError, W as WEB_PUSH_DRIVERS, a as WebPushDriverDeclaration, b as WebPushMessage, c as WebPushSender, d as WebPushSubscription, e as WebPushSubscriptionSource, f as absoluteLink, g as formatWebPush, w as webPushTransport } from '../web-push-iER5StBm.js';
|
|
4
4
|
import { NotifyPort } from '@12-apps/wiring/ports';
|
|
5
|
-
export { a as NOTIFICATIONS_DRAIN_CRON, b as NOTIFICATIONS_DRAIN_LEASE_MS, c as NOTIFICATIONS_JOBS, d as NOTIFICATIONS_SWEEP_QUEUE, N as NotificationsJobDeps } from '../jobs-
|
|
6
|
-
import '../generators-
|
|
7
|
-
import '../types-
|
|
8
|
-
import '../wire-
|
|
5
|
+
export { a as NOTIFICATIONS_DRAIN_CRON, b as NOTIFICATIONS_DRAIN_LEASE_MS, c as NOTIFICATIONS_JOBS, d as NOTIFICATIONS_SWEEP_QUEUE, N as NotificationsJobDeps } from '../jobs-CcsSA0YR.js';
|
|
6
|
+
import '../generators-CH5m8Ucj.js';
|
|
7
|
+
import '../types-CYJN1xCM.js';
|
|
8
|
+
import '../wire-CmRrB3_s.js';
|
|
9
9
|
import '@12-apps/wiring';
|
|
10
10
|
|
|
11
11
|
/**
|
package/dist/server/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
NOTIFICATIONS_JOBS,
|
|
5
5
|
NOTIFICATIONS_SWEEP_QUEUE,
|
|
6
6
|
wireNotifyPort
|
|
7
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-KU4FVOFA.js";
|
|
8
8
|
import {
|
|
9
9
|
EMAIL_DRIVERS,
|
|
10
10
|
NotificationProviderError,
|
|
@@ -25,9 +25,9 @@ import {
|
|
|
25
25
|
smsTransport,
|
|
26
26
|
webPushTransport,
|
|
27
27
|
whatsAppTransport
|
|
28
|
-
} from "../chunk-
|
|
29
|
-
import "../chunk-
|
|
30
|
-
import "../chunk-
|
|
28
|
+
} from "../chunk-7234ROOP.js";
|
|
29
|
+
import "../chunk-GGAZOLNN.js";
|
|
30
|
+
import "../chunk-BGI4BQ66.js";
|
|
31
31
|
import "../chunk-M2TVBVH2.js";
|
|
32
32
|
import "../chunk-7QVYU63E.js";
|
|
33
33
|
export {
|
|
@@ -84,12 +84,44 @@ interface NotificationEvent<TPayload = unknown> {
|
|
|
84
84
|
* generator never touches existing generators, the router, or any transport
|
|
85
85
|
* (open/closed).
|
|
86
86
|
*/
|
|
87
|
+
/**
|
|
88
|
+
* Who the content is being rendered FOR — the reader, at the moment the
|
|
89
|
+
* generator is asked.
|
|
90
|
+
*
|
|
91
|
+
* A notification is stored as rendered TEXT: title and body are columns, so
|
|
92
|
+
* the language is chosen once, when the row is written, and never again. That
|
|
93
|
+
* makes this the only honest place to ask. A generator is registered at BOOT —
|
|
94
|
+
* a host that resolved its words there would pin every future reader to
|
|
95
|
+
* whichever language the process happened to start in, invisibly, because a
|
|
96
|
+
* single-locale host cannot tell the difference.
|
|
97
|
+
*
|
|
98
|
+
* The tag is the RECIPIENT's, never the request's. The person who triggers a
|
|
99
|
+
* notification is routinely not the person who reads it: an invite is sent
|
|
100
|
+
* because an administrator acted and is read by the invitee. Reading
|
|
101
|
+
* `Accept-Language` here would be a bug that only ever surfaces as somebody
|
|
102
|
+
* being told things in a language they do not speak.
|
|
103
|
+
*
|
|
104
|
+
* Absent means "nobody said" — a host with one audience, or one that stores no
|
|
105
|
+
* per-user language, populates nothing and every generator answers with its
|
|
106
|
+
* own default exactly as it did before this existed.
|
|
107
|
+
*/
|
|
108
|
+
interface NotificationGenerateContext {
|
|
109
|
+
readonly locale?: string | null;
|
|
110
|
+
}
|
|
87
111
|
interface NotificationGenerator<TPayload = unknown> {
|
|
88
112
|
/** The event key, dot-namespaced ("order.paid"). One generator per type. */
|
|
89
113
|
type: string;
|
|
90
114
|
/** The preference category the router gates this type's fan-out on. */
|
|
91
115
|
category: NotificationCategory;
|
|
92
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Render this event's content for ONE recipient.
|
|
118
|
+
*
|
|
119
|
+
* `context` is OPTIONAL, and that is what keeps every generator written
|
|
120
|
+
* before it working: a one-parameter `generate` is assignable to this
|
|
121
|
+
* signature unchanged. A host that passes nothing is stating a fact — it has
|
|
122
|
+
* no language for this reader — rather than asserting a default.
|
|
123
|
+
*/
|
|
124
|
+
generate: (payload: TPayload, context?: NotificationGenerateContext) => NotificationContent;
|
|
93
125
|
}
|
|
94
126
|
/**
|
|
95
127
|
* The recipient as a transport sees them: resolved destinations only. Built by
|
|
@@ -100,6 +132,11 @@ interface NotificationGenerator<TPayload = unknown> {
|
|
|
100
132
|
interface TransportRecipient {
|
|
101
133
|
userId: string;
|
|
102
134
|
email: string | null;
|
|
135
|
+
/**
|
|
136
|
+
* The recipient's own language, when the host's contact directory states
|
|
137
|
+
* one. Absent means "nobody said" — see {@link NotificationGenerateContext}.
|
|
138
|
+
*/
|
|
139
|
+
locale?: string | null;
|
|
103
140
|
/** Phone as the host stores it (transports normalize per provider rules). */
|
|
104
141
|
phone: string | null;
|
|
105
142
|
/** How many active browser push subscriptions the user holds. */
|
|
@@ -152,4 +189,4 @@ interface NotificationLogger {
|
|
|
152
189
|
error(message: string, ...meta: unknown[]): void;
|
|
153
190
|
}
|
|
154
191
|
|
|
155
|
-
export { type DeliveryStatus as D, NOTIFICATION_CHANNELS as N, type TransportRecipient as T, type NotificationCategory as a, type NotificationChannel as b, type NotificationContent as c, type NotificationEvent as d, type
|
|
192
|
+
export { type DeliveryStatus as D, NOTIFICATION_CHANNELS as N, type TransportRecipient as T, type NotificationCategory as a, type NotificationChannel as b, type NotificationContent as c, type NotificationEvent as d, type NotificationGenerateContext as e, type NotificationGenerator as f, type NotificationLogger as g, type NotificationRecipient as h, type NotificationTaxonomy as i, type NotificationTransport as j, taxonomyOf as t };
|
package/dist/web-push/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { c as WebPushSender } from '../web-push-
|
|
2
|
-
import '../types-
|
|
1
|
+
import { c as WebPushSender } from '../web-push-iER5StBm.js';
|
|
2
|
+
import '../types-CYJN1xCM.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* `@12-apps/notifications/web-push` — the VAPID sender, behind its own subpath.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { g as NotificationLogger, c as NotificationContent, j as NotificationTransport } from './types-CYJN1xCM.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The DRIVER port — the reason a host adds a vendor with a config entry and no
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12-apps/notifications",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "Plug-and-play notification system (12-15): an always-on in-app inbox, per-user × per-category channel preferences, and email / SMS / WhatsApp / web-push transports behind vendor DRIVERS so a second provider is a config entry. Framework-free core (.), host-mounted backend surface (./server: inbox / preferences / push-subscription endpoints, the channel router with delivery records + retry sweep, the permission fan-out, duck-typed Prisma seam), Hono adapter (./hono), React surface (./react: bell + badge, inbox drawer, preferences screen), VAPID sender (./web-push) and the package-owned Prisma partial + migrations. Standardized adoption contract in ADOPTING.md.",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"devDependencies": {
|
|
81
81
|
"@12-apps/eslint-config": "^1.22.0",
|
|
82
82
|
"@12-apps/typescript-config": "^1.21.0",
|
|
83
|
-
"@12-apps/wiring": "^1.
|
|
83
|
+
"@12-apps/wiring": "^1.16.0",
|
|
84
84
|
"@testing-library/react": "^16.1.0",
|
|
85
85
|
"@types/react": "19.2.2",
|
|
86
86
|
"@types/react-dom": "19.2.2",
|
package/src/index.ts
CHANGED
package/src/server/db.ts
CHANGED
|
@@ -246,7 +246,18 @@ export type NotificationsDbProvider = () => Promise<NotificationsDb>;
|
|
|
246
246
|
* Returning `null` for a user id means "no such recipient", which `notify`
|
|
247
247
|
* treats as a caller bug and throws on — a notification addressed to nobody is
|
|
248
248
|
* never silently dropped.
|
|
249
|
+
*
|
|
250
|
+
* `locale` is the recipient's own language, and it is here rather than on the
|
|
251
|
+
* event because it is a fact about the PERSON, not about what happened
|
|
252
|
+
* The host owns it for the same reason it owns the address: this
|
|
253
|
+
* package has no user table to read it from. Absent — the field omitted, or
|
|
254
|
+
* `null` for a host that stores no language yet — means "nobody said", and
|
|
255
|
+
* every generator answers with its default exactly as before.
|
|
249
256
|
*/
|
|
250
257
|
export interface NotificationContactDirectory {
|
|
251
|
-
getContact(userId: string): Promise<{
|
|
258
|
+
getContact(userId: string): Promise<{
|
|
259
|
+
email: string | null;
|
|
260
|
+
phone: string | null;
|
|
261
|
+
locale?: string | null;
|
|
262
|
+
} | null>;
|
|
252
263
|
}
|
package/src/server/dispatch.ts
CHANGED
|
@@ -99,6 +99,10 @@ export async function loadRecipient(
|
|
|
99
99
|
userId,
|
|
100
100
|
email: contact.email,
|
|
101
101
|
phone: contact.phone,
|
|
102
|
+
// Carried through UNRESOLVED and only where the host supplied one: the
|
|
103
|
+
// absent case has to stay distinguishable from a stated language, because
|
|
104
|
+
// that is what lets a generator apply its own default in one place.
|
|
105
|
+
...(contact.locale === undefined ? {} : { locale: contact.locale }),
|
|
102
106
|
pushSubscriptionCount: await deps.pushSubscriptions.count(userId),
|
|
103
107
|
};
|
|
104
108
|
}
|
package/src/server/router.ts
CHANGED
|
@@ -235,11 +235,27 @@ export function createNotificationRouter(deps: NotificationRouterDeps): Notifica
|
|
|
235
235
|
|
|
236
236
|
async notify(event, options = {}) {
|
|
237
237
|
const generator = deps.generators.resolve(event.type);
|
|
238
|
-
const content = generator.generate(event.payload as never);
|
|
239
238
|
|
|
239
|
+
/*
|
|
240
|
+
The recipient is loaded BEFORE the content is rendered, and the order is
|
|
241
|
+
the whole of the fix.
|
|
242
|
+
|
|
243
|
+
`notify` is already per-person — `event.recipient.userId` is one user,
|
|
244
|
+
and a permission fan-out resolves its audience and calls this once each
|
|
245
|
+
— so the reader was knowable here all along. Rendering first simply
|
|
246
|
+
threw that away: the row was written in whatever language the generator
|
|
247
|
+
had been bound to at boot, for everyone.
|
|
248
|
+
|
|
249
|
+
Loading first also means a notification addressed to nobody now throws
|
|
250
|
+
before any content is built, which is the cheaper order anyway.
|
|
251
|
+
*/
|
|
240
252
|
const recipient = await loadRecipient(deps, event.recipient.userId);
|
|
241
253
|
if (!recipient) throw new UnknownNotificationRecipientError(event.recipient.userId);
|
|
242
254
|
|
|
255
|
+
// Forwarded exactly as the directory stated it, `undefined` included: the
|
|
256
|
+
// generator owns the fallback, in one place a reader can find.
|
|
257
|
+
const content = generator.generate(event.payload as never, { locale: recipient.locale });
|
|
258
|
+
|
|
243
259
|
const channels = await resolveChannels(deps, event, generator.category, recipient);
|
|
244
260
|
const notification = await commit(deps, event, generator.category, content, channels);
|
|
245
261
|
|
package/src/types.ts
CHANGED
|
@@ -91,12 +91,45 @@ export interface NotificationEvent<TPayload = unknown> {
|
|
|
91
91
|
* generator never touches existing generators, the router, or any transport
|
|
92
92
|
* (open/closed).
|
|
93
93
|
*/
|
|
94
|
+
/**
|
|
95
|
+
* Who the content is being rendered FOR — the reader, at the moment the
|
|
96
|
+
* generator is asked.
|
|
97
|
+
*
|
|
98
|
+
* A notification is stored as rendered TEXT: title and body are columns, so
|
|
99
|
+
* the language is chosen once, when the row is written, and never again. That
|
|
100
|
+
* makes this the only honest place to ask. A generator is registered at BOOT —
|
|
101
|
+
* a host that resolved its words there would pin every future reader to
|
|
102
|
+
* whichever language the process happened to start in, invisibly, because a
|
|
103
|
+
* single-locale host cannot tell the difference.
|
|
104
|
+
*
|
|
105
|
+
* The tag is the RECIPIENT's, never the request's. The person who triggers a
|
|
106
|
+
* notification is routinely not the person who reads it: an invite is sent
|
|
107
|
+
* because an administrator acted and is read by the invitee. Reading
|
|
108
|
+
* `Accept-Language` here would be a bug that only ever surfaces as somebody
|
|
109
|
+
* being told things in a language they do not speak.
|
|
110
|
+
*
|
|
111
|
+
* Absent means "nobody said" — a host with one audience, or one that stores no
|
|
112
|
+
* per-user language, populates nothing and every generator answers with its
|
|
113
|
+
* own default exactly as it did before this existed.
|
|
114
|
+
*/
|
|
115
|
+
export interface NotificationGenerateContext {
|
|
116
|
+
readonly locale?: string | null;
|
|
117
|
+
}
|
|
118
|
+
|
|
94
119
|
export interface NotificationGenerator<TPayload = unknown> {
|
|
95
120
|
/** The event key, dot-namespaced ("order.paid"). One generator per type. */
|
|
96
121
|
type: string;
|
|
97
122
|
/** The preference category the router gates this type's fan-out on. */
|
|
98
123
|
category: NotificationCategory;
|
|
99
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Render this event's content for ONE recipient.
|
|
126
|
+
*
|
|
127
|
+
* `context` is OPTIONAL, and that is what keeps every generator written
|
|
128
|
+
* before it working: a one-parameter `generate` is assignable to this
|
|
129
|
+
* signature unchanged. A host that passes nothing is stating a fact — it has
|
|
130
|
+
* no language for this reader — rather than asserting a default.
|
|
131
|
+
*/
|
|
132
|
+
generate: (payload: TPayload, context?: NotificationGenerateContext) => NotificationContent;
|
|
100
133
|
}
|
|
101
134
|
|
|
102
135
|
/**
|
|
@@ -108,6 +141,11 @@ export interface NotificationGenerator<TPayload = unknown> {
|
|
|
108
141
|
export interface TransportRecipient {
|
|
109
142
|
userId: string;
|
|
110
143
|
email: string | null;
|
|
144
|
+
/**
|
|
145
|
+
* The recipient's own language, when the host's contact directory states
|
|
146
|
+
* one. Absent means "nobody said" — see {@link NotificationGenerateContext}.
|
|
147
|
+
*/
|
|
148
|
+
locale?: string | null;
|
|
111
149
|
/** Phone as the host stores it (transports normalize per provider rules). */
|
|
112
150
|
phone: string | null;
|
|
113
151
|
/** How many active browser push subscriptions the user holds. */
|