@holo-js/notifications 0.1.3
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/chunk-INGM2JYY.mjs +126 -0
- package/dist/contracts.d.ts +212 -0
- package/dist/contracts.mjs +16 -0
- package/dist/index.d.ts +226 -0
- package/dist/index.mjs +842 -0
- package/package.json +47 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAnonymousNotificationTarget,
|
|
3
|
+
defineNotification,
|
|
4
|
+
isNotificationDefinition,
|
|
5
|
+
normalizeNotificationDefinition,
|
|
6
|
+
notificationsInternals
|
|
7
|
+
} from "./chunk-INGM2JYY.mjs";
|
|
8
|
+
|
|
9
|
+
// src/runtime.ts
|
|
10
|
+
import { randomUUID } from "crypto";
|
|
11
|
+
import { holoNotificationsDefaults } from "@holo-js/config";
|
|
12
|
+
|
|
13
|
+
// src/registry.ts
|
|
14
|
+
function normalizeChannelName(value) {
|
|
15
|
+
const normalized = value.trim();
|
|
16
|
+
if (!normalized) {
|
|
17
|
+
throw new Error("[@holo-js/notifications] Notification channel names must be non-empty strings.");
|
|
18
|
+
}
|
|
19
|
+
return normalized;
|
|
20
|
+
}
|
|
21
|
+
function getRegistry() {
|
|
22
|
+
const runtime = globalThis;
|
|
23
|
+
runtime.__holoNotificationChannelRegistry__ ??= /* @__PURE__ */ new Map();
|
|
24
|
+
return runtime.__holoNotificationChannelRegistry__;
|
|
25
|
+
}
|
|
26
|
+
function registerNotificationChannel(name, channel, options = {}) {
|
|
27
|
+
const normalizedName = normalizeChannelName(name);
|
|
28
|
+
if (typeof channel?.send !== "function") {
|
|
29
|
+
throw new Error(`[@holo-js/notifications] Notification channel "${normalizedName}" must define send().`);
|
|
30
|
+
}
|
|
31
|
+
const registry = getRegistry();
|
|
32
|
+
if (registry.has(normalizedName) && options.replaceExisting !== true) {
|
|
33
|
+
throw new Error(`[@holo-js/notifications] Notification channel "${normalizedName}" is already registered.`);
|
|
34
|
+
}
|
|
35
|
+
registry.set(normalizedName, Object.freeze({
|
|
36
|
+
name: normalizedName,
|
|
37
|
+
channel
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
function getRegisteredNotificationChannel(name) {
|
|
41
|
+
return getRegistry().get(normalizeChannelName(name));
|
|
42
|
+
}
|
|
43
|
+
function listRegisteredNotificationChannels() {
|
|
44
|
+
return Object.freeze([...getRegistry().values()]);
|
|
45
|
+
}
|
|
46
|
+
function resetNotificationChannelRegistry() {
|
|
47
|
+
getRegistry().clear();
|
|
48
|
+
}
|
|
49
|
+
var notificationRegistryInternals = {
|
|
50
|
+
getRegistry,
|
|
51
|
+
normalizeChannelName
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// src/runtime.ts
|
|
55
|
+
var HOLO_NOTIFICATIONS_DELIVER_JOB = "holo.notifications.deliver";
|
|
56
|
+
function normalizeOptionalString(value, label) {
|
|
57
|
+
const normalized = value.trim();
|
|
58
|
+
if (!normalized) {
|
|
59
|
+
throw new Error(`[@holo-js/notifications] ${label} must be a non-empty string.`);
|
|
60
|
+
}
|
|
61
|
+
return normalized;
|
|
62
|
+
}
|
|
63
|
+
function normalizeDelayValue(value, label) {
|
|
64
|
+
if (typeof value === "number") {
|
|
65
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
66
|
+
throw new Error(`[@holo-js/notifications] ${label} must be a finite number greater than or equal to 0.`);
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
|
|
71
|
+
throw new Error(`[@holo-js/notifications] ${label} dates must be valid Date instances.`);
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
function isObject(value) {
|
|
76
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
77
|
+
}
|
|
78
|
+
function isAnonymousTarget(value) {
|
|
79
|
+
return isObject(value) && value.anonymous === true && isObject(value.routes);
|
|
80
|
+
}
|
|
81
|
+
function getRuntimeBindings() {
|
|
82
|
+
return getRuntimeState().bindings ?? {};
|
|
83
|
+
}
|
|
84
|
+
function getRuntimeState() {
|
|
85
|
+
const runtime = globalThis;
|
|
86
|
+
runtime.__holoNotificationsRuntime__ ??= {};
|
|
87
|
+
return runtime.__holoNotificationsRuntime__;
|
|
88
|
+
}
|
|
89
|
+
function getDispatchHandler() {
|
|
90
|
+
const bindings = getRuntimeBindings();
|
|
91
|
+
return bindings.dispatch ?? dispatchNotifications;
|
|
92
|
+
}
|
|
93
|
+
function dynamicImport(specifier) {
|
|
94
|
+
return import(specifier);
|
|
95
|
+
}
|
|
96
|
+
async function loadQueueModule() {
|
|
97
|
+
const override = getRuntimeState().loadQueueModule;
|
|
98
|
+
if (override) {
|
|
99
|
+
try {
|
|
100
|
+
return await override();
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ERR_MODULE_NOT_FOUND") {
|
|
103
|
+
throw new Error("[@holo-js/notifications] Queued or delayed notifications require @holo-js/queue to be installed.");
|
|
104
|
+
}
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
return await dynamicImport("@holo-js/queue");
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ERR_MODULE_NOT_FOUND") {
|
|
112
|
+
throw new Error("[@holo-js/notifications] Queued or delayed notifications require @holo-js/queue to be installed.");
|
|
113
|
+
}
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function loadDbModule() {
|
|
118
|
+
const override = getRuntimeState().loadDbModule;
|
|
119
|
+
if (override) {
|
|
120
|
+
try {
|
|
121
|
+
return await override();
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ERR_MODULE_NOT_FOUND") {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
return await dynamicImport("@holo-js/db");
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ERR_MODULE_NOT_FOUND") {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function createNotificationContext(anonymous) {
|
|
139
|
+
return Object.freeze({ anonymous });
|
|
140
|
+
}
|
|
141
|
+
function createBuildContext(channel, anonymous) {
|
|
142
|
+
return Object.freeze({
|
|
143
|
+
channel,
|
|
144
|
+
anonymous
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function normalizeEmailRouteFromValue(value) {
|
|
148
|
+
if (typeof value === "string") {
|
|
149
|
+
const email = value.trim();
|
|
150
|
+
if (!email) {
|
|
151
|
+
throw new Error("[@holo-js/notifications] Email routes must be non-empty strings.");
|
|
152
|
+
}
|
|
153
|
+
return email;
|
|
154
|
+
}
|
|
155
|
+
if (!isObject(value) || typeof value.email !== "string" || !value.email.trim()) {
|
|
156
|
+
throw new Error("[@holo-js/notifications] Email routes must be a string or an object with a non-empty email.");
|
|
157
|
+
}
|
|
158
|
+
return Object.freeze({
|
|
159
|
+
email: value.email.trim(),
|
|
160
|
+
...typeof value.name === "string" && value.name.trim() ? { name: value.name.trim() } : {}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
function resolveEmailRouteFromNotifiable(notifiable) {
|
|
164
|
+
if (!isObject(notifiable) || typeof notifiable.email !== "string" || !notifiable.email.trim()) {
|
|
165
|
+
throw new Error("[@holo-js/notifications] Email notifications require a notifiable with a non-empty email.");
|
|
166
|
+
}
|
|
167
|
+
return Object.freeze({
|
|
168
|
+
email: notifiable.email.trim(),
|
|
169
|
+
...typeof notifiable.name === "string" && notifiable.name.trim() ? { name: notifiable.name.trim() } : {}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
function normalizeDatabaseRouteFromValue(value) {
|
|
173
|
+
if (!isObject(value) || typeof value.id !== "string" && typeof value.id !== "number" || typeof value.type !== "string" || !value.type.trim()) {
|
|
174
|
+
throw new Error("[@holo-js/notifications] Database routes must include a string or numeric id and a non-empty type.");
|
|
175
|
+
}
|
|
176
|
+
return Object.freeze({
|
|
177
|
+
id: value.id,
|
|
178
|
+
type: value.type.trim()
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function resolveDatabaseRouteFromNotifiable(notifiable) {
|
|
182
|
+
if (!isObject(notifiable) || typeof notifiable.id !== "string" && typeof notifiable.id !== "number") {
|
|
183
|
+
throw new Error("[@holo-js/notifications] Database notifications require a notifiable with a string or numeric id.");
|
|
184
|
+
}
|
|
185
|
+
const explicitType = typeof notifiable.type === "string" && notifiable.type.trim() ? notifiable.type.trim() : void 0;
|
|
186
|
+
if (explicitType) {
|
|
187
|
+
return Object.freeze({
|
|
188
|
+
id: notifiable.id,
|
|
189
|
+
type: explicitType
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
const constructorName = isObject(notifiable) && "constructor" in notifiable && typeof notifiable.constructor === "function" && typeof notifiable.constructor.name === "string" ? notifiable.constructor.name.trim() : "";
|
|
193
|
+
if (!constructorName || constructorName === "Object") {
|
|
194
|
+
throw new Error(
|
|
195
|
+
"[@holo-js/notifications] Database notifications require a notifiable.type or a non-plain-object constructor name."
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
return Object.freeze({
|
|
199
|
+
id: notifiable.id,
|
|
200
|
+
type: constructorName
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
function normalizeBroadcastRouteFromValue(value) {
|
|
204
|
+
if (typeof value === "string") {
|
|
205
|
+
const channel = value.trim();
|
|
206
|
+
if (!channel) {
|
|
207
|
+
throw new Error("[@holo-js/notifications] Broadcast routes must be non-empty strings.");
|
|
208
|
+
}
|
|
209
|
+
return channel;
|
|
210
|
+
}
|
|
211
|
+
if (Array.isArray(value)) {
|
|
212
|
+
const channels = value.map((entry, index) => {
|
|
213
|
+
if (typeof entry !== "string" || !entry.trim()) {
|
|
214
|
+
throw new Error(`[@holo-js/notifications] Broadcast route entry at index ${index} must be a non-empty string.`);
|
|
215
|
+
}
|
|
216
|
+
return entry.trim();
|
|
217
|
+
});
|
|
218
|
+
if (channels.length === 0) {
|
|
219
|
+
throw new Error("[@holo-js/notifications] Broadcast routes must include at least one channel.");
|
|
220
|
+
}
|
|
221
|
+
return Object.freeze(channels);
|
|
222
|
+
}
|
|
223
|
+
if (!isObject(value) || !Array.isArray(value.channels)) {
|
|
224
|
+
throw new Error("[@holo-js/notifications] Broadcast routes must be a string, string array, or object with channels.");
|
|
225
|
+
}
|
|
226
|
+
return Object.freeze({
|
|
227
|
+
channels: normalizeBroadcastRouteFromValue(value.channels)
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
function resolveBroadcastRouteFromNotifiable(notifiable) {
|
|
231
|
+
if (!isObject(notifiable)) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
"[@holo-js/notifications] Broadcast notifications require an anonymous route or a routeNotificationForBroadcast() method."
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
if (typeof notifiable.routeNotificationForBroadcast === "function") {
|
|
237
|
+
return normalizeBroadcastRouteFromValue(notifiable.routeNotificationForBroadcast());
|
|
238
|
+
}
|
|
239
|
+
if (typeof notifiable.broadcastChannels === "function") {
|
|
240
|
+
return normalizeBroadcastRouteFromValue(notifiable.broadcastChannels());
|
|
241
|
+
}
|
|
242
|
+
if ("broadcastChannels" in notifiable) {
|
|
243
|
+
return normalizeBroadcastRouteFromValue(notifiable.broadcastChannels);
|
|
244
|
+
}
|
|
245
|
+
throw new Error(
|
|
246
|
+
"[@holo-js/notifications] Broadcast notifications require an anonymous route or a routeNotificationForBroadcast() method."
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
function normalizeNotificationRecord(route, payload, notificationType) {
|
|
250
|
+
const now = /* @__PURE__ */ new Date();
|
|
251
|
+
return Object.freeze({
|
|
252
|
+
id: randomUUID(),
|
|
253
|
+
type: notificationType,
|
|
254
|
+
notifiableType: route.type,
|
|
255
|
+
notifiableId: route.id,
|
|
256
|
+
data: payload.data,
|
|
257
|
+
readAt: null,
|
|
258
|
+
createdAt: now,
|
|
259
|
+
updatedAt: now
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
function requireMailer(bindings) {
|
|
263
|
+
if (!bindings.mailer) {
|
|
264
|
+
throw new Error("[@holo-js/notifications] Email notifications require a configured mailer runtime.");
|
|
265
|
+
}
|
|
266
|
+
return bindings.mailer;
|
|
267
|
+
}
|
|
268
|
+
function requireBroadcaster(bindings) {
|
|
269
|
+
if (!bindings.broadcaster) {
|
|
270
|
+
throw new Error("[@holo-js/notifications] Broadcast notifications require a configured broadcaster runtime.");
|
|
271
|
+
}
|
|
272
|
+
return bindings.broadcaster;
|
|
273
|
+
}
|
|
274
|
+
function requireStore(bindings) {
|
|
275
|
+
if (!bindings.store) {
|
|
276
|
+
throw new Error("[@holo-js/notifications] Database notifications require a configured notification store runtime.");
|
|
277
|
+
}
|
|
278
|
+
return bindings.store;
|
|
279
|
+
}
|
|
280
|
+
function normalizeNotificationRecordIds(ids) {
|
|
281
|
+
const normalized = ids.map((value, index) => {
|
|
282
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
283
|
+
throw new Error(`[@holo-js/notifications] Notification id at index ${index} must be a non-empty string.`);
|
|
284
|
+
}
|
|
285
|
+
return value.trim();
|
|
286
|
+
});
|
|
287
|
+
return Object.freeze([...new Set(normalized)]);
|
|
288
|
+
}
|
|
289
|
+
var builtInChannels = Object.freeze({
|
|
290
|
+
email: Object.freeze({
|
|
291
|
+
resolveRoute: resolveEmailRouteFromNotifiable,
|
|
292
|
+
async send(input) {
|
|
293
|
+
await requireMailer(getRuntimeBindings()).send(
|
|
294
|
+
input.payload,
|
|
295
|
+
input
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}),
|
|
299
|
+
database: Object.freeze({
|
|
300
|
+
resolveRoute: resolveDatabaseRouteFromNotifiable,
|
|
301
|
+
async send(input) {
|
|
302
|
+
const route = input.route;
|
|
303
|
+
if (!route) {
|
|
304
|
+
throw new Error("[@holo-js/notifications] Database notifications require a resolved route.");
|
|
305
|
+
}
|
|
306
|
+
await requireStore(getRuntimeBindings()).create(
|
|
307
|
+
normalizeNotificationRecord(
|
|
308
|
+
route,
|
|
309
|
+
input.payload,
|
|
310
|
+
input.notificationType
|
|
311
|
+
)
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
}),
|
|
315
|
+
broadcast: Object.freeze({
|
|
316
|
+
resolveRoute: resolveBroadcastRouteFromNotifiable,
|
|
317
|
+
async send(input) {
|
|
318
|
+
await requireBroadcaster(getRuntimeBindings()).send(
|
|
319
|
+
input.payload,
|
|
320
|
+
input
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
})
|
|
324
|
+
});
|
|
325
|
+
function getNotificationChannel(name) {
|
|
326
|
+
if (name in builtInChannels) {
|
|
327
|
+
return builtInChannels[name];
|
|
328
|
+
}
|
|
329
|
+
return getRegisteredNotificationChannel(name)?.channel;
|
|
330
|
+
}
|
|
331
|
+
function resolveTargets(target) {
|
|
332
|
+
if (target.kind === "anonymous") {
|
|
333
|
+
const anonymous = target.value;
|
|
334
|
+
if (!isAnonymousTarget(anonymous)) {
|
|
335
|
+
throw new Error("[@holo-js/notifications] Anonymous notification targets must be created through notifyUsing().");
|
|
336
|
+
}
|
|
337
|
+
return Object.freeze([Object.freeze({
|
|
338
|
+
index: 0,
|
|
339
|
+
anonymous: true,
|
|
340
|
+
notifiable: anonymous,
|
|
341
|
+
routes: anonymous.routes
|
|
342
|
+
})]);
|
|
343
|
+
}
|
|
344
|
+
if (target.kind === "many") {
|
|
345
|
+
if (!Array.isArray(target.value)) {
|
|
346
|
+
throw new Error("[@holo-js/notifications] Multi-target notification dispatch requires an array target.");
|
|
347
|
+
}
|
|
348
|
+
return Object.freeze(target.value.map((notifiable, index) => Object.freeze({
|
|
349
|
+
index,
|
|
350
|
+
anonymous: false,
|
|
351
|
+
notifiable
|
|
352
|
+
})));
|
|
353
|
+
}
|
|
354
|
+
return Object.freeze([Object.freeze({
|
|
355
|
+
index: 0,
|
|
356
|
+
anonymous: false,
|
|
357
|
+
notifiable: target.value
|
|
358
|
+
})]);
|
|
359
|
+
}
|
|
360
|
+
function resolveChannels(notification, target) {
|
|
361
|
+
const channels = notification.via(target.notifiable, createNotificationContext(target.anonymous));
|
|
362
|
+
if (!Array.isArray(channels)) {
|
|
363
|
+
throw new Error("[@holo-js/notifications] Notification via() must return an array of channel names.");
|
|
364
|
+
}
|
|
365
|
+
return Object.freeze(channels.map((channel, index) => {
|
|
366
|
+
if (typeof channel !== "string") {
|
|
367
|
+
throw new Error(`[@holo-js/notifications] Notification channel at index ${index} must be a string.`);
|
|
368
|
+
}
|
|
369
|
+
const normalized = normalizeOptionalString(channel, "Notification channel");
|
|
370
|
+
if (!getNotificationChannel(normalized)) {
|
|
371
|
+
throw new Error(`[@holo-js/notifications] Notification channel "${normalized}" is not registered.`);
|
|
372
|
+
}
|
|
373
|
+
return normalized;
|
|
374
|
+
}));
|
|
375
|
+
}
|
|
376
|
+
function resolvePayload(notification, channel, target) {
|
|
377
|
+
const factory = notification.build[channel];
|
|
378
|
+
if (typeof factory !== "function") {
|
|
379
|
+
throw new Error(
|
|
380
|
+
`[@holo-js/notifications] Notification channel "${channel}" is listed in via() but has no build.${channel}() payload factory.`
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
return factory(target.notifiable, createBuildContext(channel, target.anonymous));
|
|
384
|
+
}
|
|
385
|
+
function resolveNotificationQueueOptions(notification, target, channel) {
|
|
386
|
+
if (typeof notification.queue === "function") {
|
|
387
|
+
return notification.queue(
|
|
388
|
+
target.notifiable,
|
|
389
|
+
channel,
|
|
390
|
+
createNotificationContext(target.anonymous)
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
return notification.queue ?? false;
|
|
394
|
+
}
|
|
395
|
+
function resolveNotificationDelay(notification, target, channel) {
|
|
396
|
+
if (typeof notification.delay === "function") {
|
|
397
|
+
return notification.delay(
|
|
398
|
+
target.notifiable,
|
|
399
|
+
channel,
|
|
400
|
+
createNotificationContext(target.anonymous)
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
if (typeof notification.delay === "undefined") {
|
|
404
|
+
return void 0;
|
|
405
|
+
}
|
|
406
|
+
if (typeof notification.delay === "number" || notification.delay instanceof Date) {
|
|
407
|
+
return notification.delay;
|
|
408
|
+
}
|
|
409
|
+
return notification.delay[channel];
|
|
410
|
+
}
|
|
411
|
+
function resolveRoute(channel, target) {
|
|
412
|
+
if (target.anonymous) {
|
|
413
|
+
if (!(channel in (target.routes ?? {}))) {
|
|
414
|
+
throw new Error(`[@holo-js/notifications] Anonymous notifications must define a route for channel "${channel}".`);
|
|
415
|
+
}
|
|
416
|
+
const route = target.routes?.[channel];
|
|
417
|
+
if (channel === "email") {
|
|
418
|
+
return normalizeEmailRouteFromValue(route);
|
|
419
|
+
}
|
|
420
|
+
if (channel === "database") {
|
|
421
|
+
return normalizeDatabaseRouteFromValue(route);
|
|
422
|
+
}
|
|
423
|
+
if (channel === "broadcast") {
|
|
424
|
+
return normalizeBroadcastRouteFromValue(route);
|
|
425
|
+
}
|
|
426
|
+
return route;
|
|
427
|
+
}
|
|
428
|
+
const registered = getNotificationChannel(channel);
|
|
429
|
+
if (!registered) {
|
|
430
|
+
throw new Error(`[@holo-js/notifications] Notification channel "${channel}" is not registered.`);
|
|
431
|
+
}
|
|
432
|
+
if ("resolveRoute" in registered && typeof registered.resolveRoute === "function") {
|
|
433
|
+
return registered.resolveRoute(target.notifiable);
|
|
434
|
+
}
|
|
435
|
+
if (isObject(registered) && typeof registered.validateRoute === "function") {
|
|
436
|
+
const routedNotifiable = isObject(target.notifiable) && typeof target.notifiable.routeNotificationFor === "function" ? target.notifiable.routeNotificationFor(channel) : void 0;
|
|
437
|
+
if (typeof routedNotifiable === "undefined") {
|
|
438
|
+
return void 0;
|
|
439
|
+
}
|
|
440
|
+
return registered.validateRoute(routedNotifiable);
|
|
441
|
+
}
|
|
442
|
+
if (isObject(target.notifiable) && typeof target.notifiable.routeNotificationFor === "function") {
|
|
443
|
+
return target.notifiable.routeNotificationFor(channel);
|
|
444
|
+
}
|
|
445
|
+
return void 0;
|
|
446
|
+
}
|
|
447
|
+
function resolveChannelSendContext(notification, channel, target) {
|
|
448
|
+
const payload = resolvePayload(notification, channel, target);
|
|
449
|
+
return Object.freeze({
|
|
450
|
+
channel,
|
|
451
|
+
anonymous: target.anonymous,
|
|
452
|
+
notifiable: target.notifiable,
|
|
453
|
+
route: resolveRoute(channel, target),
|
|
454
|
+
notificationType: notification.type,
|
|
455
|
+
payload,
|
|
456
|
+
targetIndex: target.index
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
function resolveChannelDispatchPlan(notification, target, channel, options) {
|
|
460
|
+
const notificationQueue = resolveNotificationQueueOptions(notification, target, channel);
|
|
461
|
+
const notificationQueueOptions = notificationQueue && notificationQueue !== true ? notificationQueue : void 0;
|
|
462
|
+
const config = getRuntimeBindings().config ?? holoNotificationsDefaults;
|
|
463
|
+
const resolvedDelay = options.delayByChannel?.[channel] ?? options.delay ?? resolveNotificationDelay(notification, target, channel);
|
|
464
|
+
const resolvedConnection = options.connection ?? notificationQueueOptions?.connection ?? config.queue.connection;
|
|
465
|
+
const resolvedQueue = options.queue ?? notificationQueueOptions?.queue ?? config.queue.queue;
|
|
466
|
+
const afterCommit = options.afterCommit ?? notificationQueueOptions?.afterCommit ?? config.queue.afterCommit;
|
|
467
|
+
const queued = notificationQueue === true || !!notificationQueueOptions || typeof resolvedDelay !== "undefined" || typeof resolvedConnection !== "undefined" || typeof resolvedQueue !== "undefined";
|
|
468
|
+
return Object.freeze({
|
|
469
|
+
channel,
|
|
470
|
+
queued,
|
|
471
|
+
connection: queued ? resolvedConnection : void 0,
|
|
472
|
+
queue: queued ? resolvedQueue : void 0,
|
|
473
|
+
delay: queued ? resolvedDelay : void 0,
|
|
474
|
+
afterCommit
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
async function deliverResolvedNotificationChannel(context) {
|
|
478
|
+
const definition = getNotificationChannel(context.channel);
|
|
479
|
+
if (!definition) {
|
|
480
|
+
throw new Error(`[@holo-js/notifications] Notification channel "${context.channel}" is not registered.`);
|
|
481
|
+
}
|
|
482
|
+
const routeValidated = isObject(definition) && typeof definition.validateRoute === "function" && typeof context.route !== "undefined" ? definition.validateRoute(context.route) : context.route;
|
|
483
|
+
const runtimeContext = typeof routeValidated === "undefined" ? context : Object.freeze({
|
|
484
|
+
...context,
|
|
485
|
+
route: routeValidated
|
|
486
|
+
});
|
|
487
|
+
return await definition.send(runtimeContext);
|
|
488
|
+
}
|
|
489
|
+
function createQueuedDeliveryPayload(context) {
|
|
490
|
+
return Object.freeze({
|
|
491
|
+
channel: context.channel,
|
|
492
|
+
anonymous: context.anonymous,
|
|
493
|
+
notifiable: context.notifiable,
|
|
494
|
+
...typeof context.route === "undefined" ? {} : { route: context.route },
|
|
495
|
+
...typeof context.notificationType === "undefined" ? {} : { notificationType: context.notificationType },
|
|
496
|
+
payload: context.payload,
|
|
497
|
+
targetIndex: context.targetIndex
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
async function runQueuedNotificationDelivery(payload) {
|
|
501
|
+
return await deliverResolvedNotificationChannel(Object.freeze({
|
|
502
|
+
channel: payload.channel,
|
|
503
|
+
anonymous: payload.anonymous,
|
|
504
|
+
notifiable: payload.notifiable,
|
|
505
|
+
route: payload.route,
|
|
506
|
+
notificationType: payload.notificationType,
|
|
507
|
+
payload: payload.payload,
|
|
508
|
+
targetIndex: payload.targetIndex
|
|
509
|
+
}));
|
|
510
|
+
}
|
|
511
|
+
async function ensureNotificationsQueueJobRegistered(queueModule) {
|
|
512
|
+
const resolvedQueueModule = queueModule ?? await loadQueueModule();
|
|
513
|
+
if (resolvedQueueModule.getRegisteredQueueJob(HOLO_NOTIFICATIONS_DELIVER_JOB)) {
|
|
514
|
+
return resolvedQueueModule;
|
|
515
|
+
}
|
|
516
|
+
resolvedQueueModule.registerQueueJob(
|
|
517
|
+
resolvedQueueModule.defineJob({
|
|
518
|
+
async handle(payload) {
|
|
519
|
+
return await runQueuedNotificationDelivery(payload);
|
|
520
|
+
}
|
|
521
|
+
}),
|
|
522
|
+
{ name: HOLO_NOTIFICATIONS_DELIVER_JOB }
|
|
523
|
+
);
|
|
524
|
+
return resolvedQueueModule;
|
|
525
|
+
}
|
|
526
|
+
async function dispatchQueuedNotificationChannel(context, plan) {
|
|
527
|
+
const queueModule = await ensureNotificationsQueueJobRegistered();
|
|
528
|
+
let pending = queueModule.dispatch(
|
|
529
|
+
HOLO_NOTIFICATIONS_DELIVER_JOB,
|
|
530
|
+
createQueuedDeliveryPayload(context)
|
|
531
|
+
);
|
|
532
|
+
if (typeof plan.connection !== "undefined") {
|
|
533
|
+
pending = pending.onConnection(plan.connection);
|
|
534
|
+
}
|
|
535
|
+
if (typeof plan.queue !== "undefined") {
|
|
536
|
+
pending = pending.onQueue(plan.queue);
|
|
537
|
+
}
|
|
538
|
+
if (typeof plan.delay !== "undefined") {
|
|
539
|
+
pending = pending.delay(plan.delay);
|
|
540
|
+
}
|
|
541
|
+
await pending.dispatch();
|
|
542
|
+
}
|
|
543
|
+
async function deferDispatchUntilCommit(input, targets, notification) {
|
|
544
|
+
const dbModule = await loadDbModule();
|
|
545
|
+
const active = dbModule?.connectionAsyncContext.getActive()?.connection;
|
|
546
|
+
if (!active || active.getScope().kind === "root") {
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
const channels = [];
|
|
550
|
+
for (const target of targets) {
|
|
551
|
+
for (const channel of resolveChannels(notification, target)) {
|
|
552
|
+
const plan = resolveChannelDispatchPlan(notification, target, channel, input.options);
|
|
553
|
+
channels.push(Object.freeze({
|
|
554
|
+
channel,
|
|
555
|
+
targetIndex: target.index,
|
|
556
|
+
queued: plan.queued,
|
|
557
|
+
deferred: true,
|
|
558
|
+
success: true
|
|
559
|
+
}));
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
active.afterCommit(async () => {
|
|
563
|
+
await dispatchNotifications(input, { allowAfterCommitDeferral: false });
|
|
564
|
+
});
|
|
565
|
+
return Object.freeze({
|
|
566
|
+
totalTargets: targets.length,
|
|
567
|
+
channels: Object.freeze(channels),
|
|
568
|
+
deferred: true
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
function shouldDeferDispatchAfterCommit(notification, targets, options) {
|
|
572
|
+
if (options.afterCommit) {
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
return targets.some((target) => resolveChannels(notification, target).some((channel) => {
|
|
576
|
+
return resolveChannelDispatchPlan(notification, target, channel, options).afterCommit;
|
|
577
|
+
}));
|
|
578
|
+
}
|
|
579
|
+
async function dispatchNotifications(input, execution = {}) {
|
|
580
|
+
const notification = normalizeNotificationDefinition(input.notification);
|
|
581
|
+
const targets = resolveTargets(input.target);
|
|
582
|
+
if (execution.allowAfterCommitDeferral !== false && shouldDeferDispatchAfterCommit(notification, targets, input.options)) {
|
|
583
|
+
const deferredResult = await deferDispatchUntilCommit(input, targets, notification);
|
|
584
|
+
if (deferredResult) {
|
|
585
|
+
return deferredResult;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
const results = [];
|
|
589
|
+
for (const target of targets) {
|
|
590
|
+
const channels = resolveChannels(notification, target);
|
|
591
|
+
for (const channel of channels) {
|
|
592
|
+
try {
|
|
593
|
+
const context = resolveChannelSendContext(notification, channel, target);
|
|
594
|
+
const plan = resolveChannelDispatchPlan(notification, target, channel, input.options);
|
|
595
|
+
const result = plan.queued ? await dispatchQueuedNotificationChannel(context, plan) : await deliverResolvedNotificationChannel(context);
|
|
596
|
+
results.push(Object.freeze({
|
|
597
|
+
channel,
|
|
598
|
+
targetIndex: target.index,
|
|
599
|
+
queued: plan.queued,
|
|
600
|
+
success: true,
|
|
601
|
+
...typeof result === "undefined" ? {} : { result }
|
|
602
|
+
}));
|
|
603
|
+
} catch (error) {
|
|
604
|
+
results.push(Object.freeze({
|
|
605
|
+
channel,
|
|
606
|
+
targetIndex: target.index,
|
|
607
|
+
queued: false,
|
|
608
|
+
success: false,
|
|
609
|
+
error
|
|
610
|
+
}));
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return Object.freeze({
|
|
615
|
+
totalTargets: targets.length,
|
|
616
|
+
channels: Object.freeze(results)
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
var PendingDispatch = class {
|
|
620
|
+
constructor(target, notification, options = {}) {
|
|
621
|
+
this.target = target;
|
|
622
|
+
this.notification = notification;
|
|
623
|
+
this.options = options;
|
|
624
|
+
}
|
|
625
|
+
#promise;
|
|
626
|
+
onConnection(name) {
|
|
627
|
+
this.options.connection = normalizeOptionalString(name, "Notification queue connection");
|
|
628
|
+
return this;
|
|
629
|
+
}
|
|
630
|
+
onQueue(name) {
|
|
631
|
+
this.options.queue = normalizeOptionalString(name, "Notification queue name");
|
|
632
|
+
return this;
|
|
633
|
+
}
|
|
634
|
+
delay(value) {
|
|
635
|
+
this.options.delay = normalizeDelayValue(value, "Notification delay");
|
|
636
|
+
return this;
|
|
637
|
+
}
|
|
638
|
+
delayFor(channel, value) {
|
|
639
|
+
const normalizedChannel = normalizeOptionalString(channel, "Notification channel");
|
|
640
|
+
this.options.delayByChannel ??= {};
|
|
641
|
+
this.options.delayByChannel[normalizedChannel] = normalizeDelayValue(value, `Notification delay for channel "${normalizedChannel}"`);
|
|
642
|
+
return this;
|
|
643
|
+
}
|
|
644
|
+
afterCommit() {
|
|
645
|
+
this.options.afterCommit = true;
|
|
646
|
+
return this;
|
|
647
|
+
}
|
|
648
|
+
then(onfulfilled, onrejected) {
|
|
649
|
+
return this.#execute().then(onfulfilled, onrejected);
|
|
650
|
+
}
|
|
651
|
+
catch(onrejected) {
|
|
652
|
+
return this.#execute().catch(onrejected);
|
|
653
|
+
}
|
|
654
|
+
finally(onfinally) {
|
|
655
|
+
return this.#execute().finally(onfinally ?? void 0);
|
|
656
|
+
}
|
|
657
|
+
#execute() {
|
|
658
|
+
if (!this.#promise) {
|
|
659
|
+
try {
|
|
660
|
+
this.#promise = getDispatchHandler()({
|
|
661
|
+
target: typeof this.target === "function" ? this.target() : this.target,
|
|
662
|
+
notification: this.notification,
|
|
663
|
+
options: Object.freeze({
|
|
664
|
+
connection: this.options.connection,
|
|
665
|
+
queue: this.options.queue,
|
|
666
|
+
delay: this.options.delay,
|
|
667
|
+
delayByChannel: this.options.delayByChannel ? Object.freeze({ ...this.options.delayByChannel }) : void 0,
|
|
668
|
+
afterCommit: this.options.afterCommit
|
|
669
|
+
})
|
|
670
|
+
});
|
|
671
|
+
} catch (error) {
|
|
672
|
+
this.#promise = Promise.reject(error);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return this.#promise;
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
var AnonymousNotificationBuilder = class _AnonymousNotificationBuilder {
|
|
679
|
+
target;
|
|
680
|
+
constructor(routes = {}) {
|
|
681
|
+
this.target = createAnonymousNotificationTarget(routes);
|
|
682
|
+
}
|
|
683
|
+
channel(channel, route) {
|
|
684
|
+
const normalizedChannel = normalizeOptionalString(channel, "Notification channel");
|
|
685
|
+
return new _AnonymousNotificationBuilder({
|
|
686
|
+
...this.target.routes,
|
|
687
|
+
[normalizedChannel]: route
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- must accept any NotificationDefinition variant
|
|
691
|
+
notify(notification) {
|
|
692
|
+
return new PendingDispatch({
|
|
693
|
+
kind: "anonymous",
|
|
694
|
+
value: this.target
|
|
695
|
+
}, notification);
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
function configureNotificationsRuntime(bindings) {
|
|
699
|
+
getRuntimeState().bindings = bindings;
|
|
700
|
+
}
|
|
701
|
+
function getNotificationsRuntimeBindings() {
|
|
702
|
+
return getRuntimeBindings();
|
|
703
|
+
}
|
|
704
|
+
function resetNotificationsRuntime() {
|
|
705
|
+
const state = getRuntimeState();
|
|
706
|
+
state.bindings = void 0;
|
|
707
|
+
state.loadQueueModule = void 0;
|
|
708
|
+
state.loadDbModule = void 0;
|
|
709
|
+
}
|
|
710
|
+
function notify(notifiable, notification) {
|
|
711
|
+
return new PendingDispatch({
|
|
712
|
+
kind: "notifiable",
|
|
713
|
+
value: notifiable
|
|
714
|
+
}, notification);
|
|
715
|
+
}
|
|
716
|
+
function notifyMany(notifiables, notification) {
|
|
717
|
+
return new PendingDispatch(() => ({
|
|
718
|
+
kind: "many",
|
|
719
|
+
value: Object.freeze([...notifiables])
|
|
720
|
+
}), notification);
|
|
721
|
+
}
|
|
722
|
+
function notifyUsing() {
|
|
723
|
+
return new AnonymousNotificationBuilder();
|
|
724
|
+
}
|
|
725
|
+
async function listNotifications(notifiable) {
|
|
726
|
+
return requireStore(getRuntimeBindings()).list(resolveDatabaseRouteFromNotifiable(notifiable));
|
|
727
|
+
}
|
|
728
|
+
async function unreadNotifications(notifiable) {
|
|
729
|
+
return requireStore(getRuntimeBindings()).unread(resolveDatabaseRouteFromNotifiable(notifiable));
|
|
730
|
+
}
|
|
731
|
+
async function markNotificationsAsRead(ids) {
|
|
732
|
+
return requireStore(getRuntimeBindings()).markAsRead(normalizeNotificationRecordIds(ids));
|
|
733
|
+
}
|
|
734
|
+
async function markNotificationsAsUnread(ids) {
|
|
735
|
+
return requireStore(getRuntimeBindings()).markAsUnread(normalizeNotificationRecordIds(ids));
|
|
736
|
+
}
|
|
737
|
+
async function deleteNotifications(ids) {
|
|
738
|
+
return requireStore(getRuntimeBindings()).delete(normalizeNotificationRecordIds(ids));
|
|
739
|
+
}
|
|
740
|
+
function getNotificationsRuntime() {
|
|
741
|
+
return Object.freeze({
|
|
742
|
+
notify,
|
|
743
|
+
notifyMany,
|
|
744
|
+
notifyUsing,
|
|
745
|
+
listNotifications,
|
|
746
|
+
unreadNotifications,
|
|
747
|
+
markNotificationsAsRead,
|
|
748
|
+
markNotificationsAsUnread,
|
|
749
|
+
deleteNotifications
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
var notificationsRuntimeInternals = {
|
|
753
|
+
HOLO_NOTIFICATIONS_DELIVER_JOB,
|
|
754
|
+
AnonymousNotificationBuilder,
|
|
755
|
+
PendingDispatch,
|
|
756
|
+
builtInChannels,
|
|
757
|
+
createBuildContext,
|
|
758
|
+
createNotificationContext,
|
|
759
|
+
createQueuedDeliveryPayload,
|
|
760
|
+
deferDispatchUntilCommit,
|
|
761
|
+
deliverResolvedNotificationChannel,
|
|
762
|
+
dispatchNotifications,
|
|
763
|
+
dispatchQueuedNotificationChannel,
|
|
764
|
+
ensureNotificationsQueueJobRegistered,
|
|
765
|
+
getDispatchHandler,
|
|
766
|
+
getRuntimeBindings,
|
|
767
|
+
getRuntimeState,
|
|
768
|
+
getNotificationChannel,
|
|
769
|
+
isAnonymousTarget,
|
|
770
|
+
isObject,
|
|
771
|
+
loadDbModule,
|
|
772
|
+
loadQueueModule,
|
|
773
|
+
normalizeBroadcastRouteFromValue,
|
|
774
|
+
normalizeDatabaseRouteFromValue,
|
|
775
|
+
normalizeDelayValue,
|
|
776
|
+
normalizeEmailRouteFromValue,
|
|
777
|
+
normalizeNotificationRecord,
|
|
778
|
+
normalizeNotificationRecordIds,
|
|
779
|
+
normalizeOptionalString,
|
|
780
|
+
resolveBroadcastRouteFromNotifiable,
|
|
781
|
+
resolveChannelDispatchPlan,
|
|
782
|
+
resolveChannels,
|
|
783
|
+
resolveDatabaseRouteFromNotifiable,
|
|
784
|
+
resolveEmailRouteFromNotifiable,
|
|
785
|
+
resolveNotificationDelay,
|
|
786
|
+
resolveNotificationQueueOptions,
|
|
787
|
+
resolvePayload,
|
|
788
|
+
resolveRoute,
|
|
789
|
+
runQueuedNotificationDelivery,
|
|
790
|
+
resolveTargets,
|
|
791
|
+
setDbModuleLoader(loader) {
|
|
792
|
+
getRuntimeState().loadDbModule = loader;
|
|
793
|
+
},
|
|
794
|
+
setQueueModuleLoader(loader) {
|
|
795
|
+
getRuntimeState().loadQueueModule = loader;
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
// src/index.ts
|
|
800
|
+
import {
|
|
801
|
+
defineNotificationsConfig as defineBaseNotificationsConfig
|
|
802
|
+
} from "@holo-js/config";
|
|
803
|
+
function defineNotificationsConfig(config) {
|
|
804
|
+
return defineBaseNotificationsConfig(config);
|
|
805
|
+
}
|
|
806
|
+
var notifications = Object.freeze({
|
|
807
|
+
deleteNotifications,
|
|
808
|
+
listNotifications,
|
|
809
|
+
markNotificationsAsRead,
|
|
810
|
+
markNotificationsAsUnread,
|
|
811
|
+
notify,
|
|
812
|
+
notifyMany,
|
|
813
|
+
notifyUsing,
|
|
814
|
+
unreadNotifications
|
|
815
|
+
});
|
|
816
|
+
var src_default = notifications;
|
|
817
|
+
export {
|
|
818
|
+
configureNotificationsRuntime,
|
|
819
|
+
src_default as default,
|
|
820
|
+
defineNotification,
|
|
821
|
+
defineNotificationsConfig,
|
|
822
|
+
deleteNotifications,
|
|
823
|
+
getNotificationsRuntime,
|
|
824
|
+
getNotificationsRuntimeBindings,
|
|
825
|
+
getRegisteredNotificationChannel,
|
|
826
|
+
isNotificationDefinition,
|
|
827
|
+
listNotifications,
|
|
828
|
+
listRegisteredNotificationChannels,
|
|
829
|
+
markNotificationsAsRead,
|
|
830
|
+
markNotificationsAsUnread,
|
|
831
|
+
normalizeNotificationDefinition,
|
|
832
|
+
notificationRegistryInternals,
|
|
833
|
+
notificationsInternals,
|
|
834
|
+
notificationsRuntimeInternals,
|
|
835
|
+
notify,
|
|
836
|
+
notifyMany,
|
|
837
|
+
notifyUsing,
|
|
838
|
+
registerNotificationChannel,
|
|
839
|
+
resetNotificationChannelRegistry,
|
|
840
|
+
resetNotificationsRuntime,
|
|
841
|
+
unreadNotifications
|
|
842
|
+
};
|