@tstdl/base 0.93.121 → 0.93.123
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/api/server/gateway.js +6 -1
- package/api/types.d.ts +5 -1
- package/notification/api/notification.api.d.ts +6 -0
- package/notification/api/notification.api.js +3 -0
- package/notification/client/notification-client.js +34 -1
- package/notification/server/api/notification.api-controller.d.ts +1 -1
- package/notification/server/api/notification.api-controller.js +15 -1
- package/notification/server/index.d.ts +1 -0
- package/notification/server/index.js +1 -0
- package/notification/server/modules/notification-delivery-worker.module.d.ts +1 -0
- package/notification/server/modules/notification-delivery-worker.module.js +13 -2
- package/notification/server/providers/in-app-channel-provider.js +1 -1
- package/notification/server/services/notification-sse.service.d.ts +14 -2
- package/notification/server/services/notification-sse.service.js +10 -11
- package/notification/server/services/notification.service.d.ts +7 -1
- package/notification/server/services/notification.service.js +44 -14
- package/notification/tests/notification-sse.service.test.js +24 -3
- package/notification/types.d.ts +10 -2
- package/notification/types.js +6 -2
- package/package.json +1 -1
- package/sse/data-stream-source.d.ts +7 -4
- package/sse/data-stream-source.js +7 -4
package/api/server/gateway.js
CHANGED
|
@@ -199,6 +199,11 @@ let ApiGateway = ApiGateway_1 = class ApiGateway {
|
|
|
199
199
|
body,
|
|
200
200
|
request: context.request,
|
|
201
201
|
abortSignal: context.abortSignal,
|
|
202
|
+
serverSentEvents: {
|
|
203
|
+
get lastEventId() {
|
|
204
|
+
return context.request.headers.tryGetSingle('Last-Event-ID');
|
|
205
|
+
},
|
|
206
|
+
},
|
|
202
207
|
tryGetToken: async () => {
|
|
203
208
|
return await requestTokenProvider.tryGetToken(requestContext);
|
|
204
209
|
},
|
|
@@ -238,7 +243,7 @@ let ApiGateway = ApiGateway_1 = class ApiGateway {
|
|
|
238
243
|
const { errorResponse } = logAndGetErrorResponse(this.#logger, this.#supressedErrors, error);
|
|
239
244
|
return errorResponse.error;
|
|
240
245
|
};
|
|
241
|
-
return ({ events: DataStreamSource.fromIterable(value, { errorFormatter }).eventSource });
|
|
246
|
+
return ({ events: DataStreamSource.fromIterable(value, { errorFormatter, ...context.endpoint.definition.dataStream }).eventSource });
|
|
242
247
|
})
|
|
243
248
|
.when(() => (context.endpoint.definition.result == String), (text) => ({ text: text }))
|
|
244
249
|
.otherwise((json) => ({ json }));
|
package/api/types.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { Token } from '../authentication/index.js';
|
|
|
4
4
|
import type { HttpServerRequest, HttpServerResponse } from '../http/server/index.js';
|
|
5
5
|
import type { HttpMethod } from '../http/types.js';
|
|
6
6
|
import type { SchemaOutput, SchemaTestable } from '../schema/index.js';
|
|
7
|
-
import type { DataStream, DataStreamSource } from '../sse/index.js';
|
|
7
|
+
import type { DataStream, DataStreamSource, DataStreamSourceOptions } from '../sse/index.js';
|
|
8
8
|
import type { ServerSentEventsSource } from '../sse/server-sent-events-source.js';
|
|
9
9
|
import type { ServerSentEvents } from '../sse/server-sent-events.js';
|
|
10
10
|
import type { NonUndefinable, OneOrMany, Record, ReturnTypeOrT } from '../types/index.js';
|
|
@@ -66,6 +66,7 @@ export type ApiEndpointDefinition = {
|
|
|
66
66
|
maxBytes?: number;
|
|
67
67
|
description?: string;
|
|
68
68
|
data?: Record;
|
|
69
|
+
dataStream?: DataStreamSourceOptions<any>;
|
|
69
70
|
/**
|
|
70
71
|
* If true, sets browsers fetch to { credentials: 'include' } and enables 'Access-Control-Allow-Credentials' header.
|
|
71
72
|
*
|
|
@@ -107,6 +108,9 @@ export type ApiRequestData<T extends ApiDefinition = ApiDefinition, K extends Ap
|
|
|
107
108
|
body: ApiServerBody<T, K>;
|
|
108
109
|
request: HttpServerRequest;
|
|
109
110
|
abortSignal: AbortSignal;
|
|
111
|
+
serverSentEvents: {
|
|
112
|
+
lastEventId?: string;
|
|
113
|
+
};
|
|
110
114
|
};
|
|
111
115
|
export type ApiRequestContext<T extends ApiDefinition = ApiDefinition, K extends ApiEndpointKeys<T> = ApiEndpointKeys<T>> = ApiRequestData<T, K> & {
|
|
112
116
|
tryGetToken<T extends Token>(): Promise<T | null>;
|
|
@@ -13,6 +13,9 @@ export declare const notificationApiDefinition: {
|
|
|
13
13
|
parse<T>(eventSource: import("../../sse/server-sent-events.js").ServerSentEvents): import("rxjs").Observable<T>;
|
|
14
14
|
};
|
|
15
15
|
credentials: true;
|
|
16
|
+
dataStream: {
|
|
17
|
+
idProvider: (item: NotificationStreamItem) => import("../../types/tagged.js").Tagged<string, "column", import("drizzle-orm").IsPrimaryKey<import("drizzle-orm").HasDefault<import("drizzle-orm/pg-core").PgUUIDBuilderInitial<string>>>> | undefined;
|
|
18
|
+
};
|
|
16
19
|
};
|
|
17
20
|
types: {
|
|
18
21
|
resource: string;
|
|
@@ -116,6 +119,9 @@ declare const _NotificationApiClient: import("../../api/client/index.js").ApiCli
|
|
|
116
119
|
parse<T>(eventSource: import("../../sse/server-sent-events.js").ServerSentEvents): import("rxjs").Observable<T>;
|
|
117
120
|
};
|
|
118
121
|
credentials: true;
|
|
122
|
+
dataStream: {
|
|
123
|
+
idProvider: (item: NotificationStreamItem) => import("../../types/tagged.js").Tagged<string, "column", import("drizzle-orm").IsPrimaryKey<import("drizzle-orm").HasDefault<import("drizzle-orm/pg-core").PgUUIDBuilderInitial<string>>>> | undefined;
|
|
124
|
+
};
|
|
119
125
|
};
|
|
120
126
|
types: {
|
|
121
127
|
resource: string;
|
|
@@ -21,10 +21,25 @@ let NotificationClient = class NotificationClient {
|
|
|
21
21
|
return of({ notifications: [], unreadCount: 0 });
|
|
22
22
|
}
|
|
23
23
|
return merge(defer(() => from(this.api.listInApp({ limit: 20 }))).pipe(map((notifications) => ({ type: 'set-notifications', notifications: notifications }))), defer(() => from(this.api.unreadCount())).pipe(map((unreadCount) => ({ type: 'set-unread-count', unreadCount }))), defer(() => from(this.api.stream())).pipe(switchAll(), forceCast(), switchMap((item) => {
|
|
24
|
-
const actions = [
|
|
24
|
+
const actions = [];
|
|
25
|
+
if (isDefined(item.unreadCount)) {
|
|
26
|
+
actions.push({ type: 'set-unread-count', unreadCount: item.unreadCount });
|
|
27
|
+
}
|
|
25
28
|
if (isDefined(item.notification)) {
|
|
26
29
|
actions.push({ type: 'prepend-notification', notification: item.notification });
|
|
27
30
|
}
|
|
31
|
+
if (isDefined(item.readId)) {
|
|
32
|
+
actions.push({ type: 'mark-read', id: item.readId });
|
|
33
|
+
}
|
|
34
|
+
if (item.readAll == true) {
|
|
35
|
+
actions.push({ type: 'mark-all-read' });
|
|
36
|
+
}
|
|
37
|
+
if (isDefined(item.archiveId)) {
|
|
38
|
+
actions.push({ type: 'archive', id: item.archiveId });
|
|
39
|
+
}
|
|
40
|
+
if (item.archiveAll == true) {
|
|
41
|
+
actions.push({ type: 'archive-all' });
|
|
42
|
+
}
|
|
28
43
|
return from(actions);
|
|
29
44
|
})), this.#pagination$.pipe(concatMap(({ count, after }) => from(this.api.listInApp({ limit: count, after }))), map((notifications) => ({ type: 'append-notifications', notifications: notifications })))).pipe(scan((acc, action) => {
|
|
30
45
|
switch (action.type) {
|
|
@@ -38,6 +53,24 @@ let NotificationClient = class NotificationClient {
|
|
|
38
53
|
case 'append-notifications':
|
|
39
54
|
const filtered = action.notifications.filter((n) => !acc.notifications.some((c) => c.id == n.id));
|
|
40
55
|
return { ...acc, notifications: [...acc.notifications, ...filtered] };
|
|
56
|
+
case 'mark-read':
|
|
57
|
+
return {
|
|
58
|
+
...acc,
|
|
59
|
+
notifications: acc.notifications.map((notification) => (notification.id == action.id) ? { ...notification, readTimestamp: Date.now() } : notification),
|
|
60
|
+
};
|
|
61
|
+
case 'mark-all-read':
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
return {
|
|
64
|
+
...acc,
|
|
65
|
+
notifications: acc.notifications.map((notification) => ({ ...notification, readTimestamp: notification.readTimestamp ?? now })),
|
|
66
|
+
};
|
|
67
|
+
case 'archive':
|
|
68
|
+
return {
|
|
69
|
+
...acc,
|
|
70
|
+
notifications: acc.notifications.filter((notification) => notification.id != action.id),
|
|
71
|
+
};
|
|
72
|
+
case 'archive-all':
|
|
73
|
+
return { ...acc, notifications: [] };
|
|
41
74
|
case 'set-unread-count':
|
|
42
75
|
return { ...acc, unreadCount: action.unreadCount };
|
|
43
76
|
default:
|
|
@@ -7,7 +7,7 @@ export declare class NotificationApiController implements ApiController<Notifica
|
|
|
7
7
|
protected readonly notificationService: NotificationService<Record<string, import("../../index.js").NotificationDefinition<import("../../../types/types.js").ObjectLiteral, import("../../../types/types.js").ObjectLiteral>>>;
|
|
8
8
|
protected readonly notificationTypeService: NotificationTypeService;
|
|
9
9
|
protected readonly sseService: NotificationSseService;
|
|
10
|
-
stream({ abortSignal, getToken }: ApiRequestContext<NotificationApiDefinition, 'stream'>): ApiServerResult<NotificationApiDefinition, 'stream'>;
|
|
10
|
+
stream({ abortSignal, getToken, serverSentEvents: { lastEventId } }: ApiRequestContext<NotificationApiDefinition, 'stream'>): ApiServerResult<NotificationApiDefinition, 'stream'>;
|
|
11
11
|
types(): Promise<Record<string, string>>;
|
|
12
12
|
listInApp({ parameters, getToken }: ApiRequestContext<NotificationApiDefinition, 'listInApp'>): Promise<any>;
|
|
13
13
|
markRead({ parameters, getToken }: ApiRequestContext<NotificationApiDefinition, 'markRead'>): Promise<'ok'>;
|
|
@@ -8,6 +8,7 @@ import { apiController } from '../../../api/server/index.js';
|
|
|
8
8
|
import { inject } from '../../../injector/index.js';
|
|
9
9
|
import { toAsyncIterable } from '../../../rxjs-utils/index.js';
|
|
10
10
|
import { decodeBase64 } from '../../../utils/base64.js';
|
|
11
|
+
import { isDefined } from '../../../utils/type-guards.js';
|
|
11
12
|
import { notificationApiDefinition } from '../../api/notification.api.js';
|
|
12
13
|
import { NotificationSseService } from '../services/notification-sse.service.js';
|
|
13
14
|
import { NotificationTypeService } from '../services/notification-type.service.js';
|
|
@@ -16,12 +17,25 @@ let NotificationApiController = class NotificationApiController {
|
|
|
16
17
|
notificationService = inject(NotificationService);
|
|
17
18
|
notificationTypeService = inject(NotificationTypeService);
|
|
18
19
|
sseService = inject(NotificationSseService);
|
|
19
|
-
async *stream({ abortSignal, getToken }) {
|
|
20
|
+
async *stream({ abortSignal, getToken, serverSentEvents: { lastEventId } }) {
|
|
20
21
|
const token = await getToken();
|
|
21
22
|
const source = this.sseService.register(token.payload.tenant, token.payload.subject);
|
|
22
23
|
const asyncIterable = toAsyncIterable(source);
|
|
23
24
|
abortSignal.addEventListener('abort', () => this.sseService.unregister(token.payload.tenant, token.payload.subject, source));
|
|
24
25
|
try {
|
|
26
|
+
if (isDefined(lastEventId)) {
|
|
27
|
+
const { unreadCount, missedNotifications, readIds, archiveIds } = await this.notificationService.getCatchupData(token.payload.tenant, token.payload.subject, lastEventId);
|
|
28
|
+
yield { unreadCount };
|
|
29
|
+
for (const readId of readIds) {
|
|
30
|
+
yield { readId };
|
|
31
|
+
}
|
|
32
|
+
for (const archiveId of archiveIds) {
|
|
33
|
+
yield { archiveId };
|
|
34
|
+
}
|
|
35
|
+
for (const notification of missedNotifications.reverse()) {
|
|
36
|
+
yield { notification };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
25
39
|
yield* asyncIterable;
|
|
26
40
|
}
|
|
27
41
|
finally {
|
|
@@ -3,6 +3,7 @@ import { afterResolve } from '../../../injector/index.js';
|
|
|
3
3
|
import { Module } from '../../../module/module.js';
|
|
4
4
|
export declare class NotificationDeliveryWorkerModule extends Module {
|
|
5
5
|
#private;
|
|
6
|
+
constructor();
|
|
6
7
|
[afterResolve](): void;
|
|
7
8
|
_run(cancellationSignal: CancellationSignal): Promise<void>;
|
|
8
9
|
}
|
|
@@ -4,23 +4,34 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
7
10
|
import { CancellationSignal } from '../../../cancellation/token.js';
|
|
8
11
|
import { afterResolve, inject, Singleton } from '../../../injector/index.js';
|
|
9
12
|
import { Module } from '../../../module/module.js';
|
|
10
13
|
import { NotificationChannel } from '../../../notification/models/index.js';
|
|
11
|
-
import { InAppChannelProvider } from '../providers/index.js';
|
|
14
|
+
import { EmailChannelProvider, InAppChannelProvider, WebPushChannelProvider } from '../providers/index.js';
|
|
12
15
|
import { NotificationDeliveryWorker } from '../services/index.js';
|
|
13
16
|
let NotificationDeliveryWorkerModule = class NotificationDeliveryWorkerModule extends Module {
|
|
14
17
|
#notificationDeliveryWorker = inject(NotificationDeliveryWorker);
|
|
15
18
|
#inAppChannelProvider = inject(InAppChannelProvider);
|
|
19
|
+
#emailChannelProvider = inject(EmailChannelProvider);
|
|
20
|
+
#webPushChannelProvider = inject(WebPushChannelProvider);
|
|
21
|
+
constructor() {
|
|
22
|
+
super('NotificationDeliveryWorker');
|
|
23
|
+
}
|
|
16
24
|
[afterResolve]() {
|
|
17
25
|
this.#notificationDeliveryWorker.registerProvider(NotificationChannel.InApp, this.#inAppChannelProvider);
|
|
26
|
+
this.#notificationDeliveryWorker.registerProvider(NotificationChannel.Email, this.#emailChannelProvider);
|
|
27
|
+
this.#notificationDeliveryWorker.registerProvider(NotificationChannel.WebPush, this.#webPushChannelProvider);
|
|
18
28
|
}
|
|
19
29
|
async _run(cancellationSignal) {
|
|
20
30
|
await this.#notificationDeliveryWorker.run(cancellationSignal);
|
|
21
31
|
}
|
|
22
32
|
};
|
|
23
33
|
NotificationDeliveryWorkerModule = __decorate([
|
|
24
|
-
Singleton()
|
|
34
|
+
Singleton(),
|
|
35
|
+
__metadata("design:paramtypes", [])
|
|
25
36
|
], NotificationDeliveryWorkerModule);
|
|
26
37
|
export { NotificationDeliveryWorkerModule };
|
|
@@ -24,7 +24,7 @@ let InAppChannelProvider = class InAppChannelProvider extends ChannelProvider {
|
|
|
24
24
|
});
|
|
25
25
|
const inAppNotificationView = toInAppNotificationView(inApp, notification);
|
|
26
26
|
const unreadCount = await this.#notificationService.withOptionalTransaction(tx).unreadCount(notification.tenantId, notification.userId);
|
|
27
|
-
await this.#sseService.
|
|
27
|
+
await this.#sseService.dispatch(notification.tenantId, notification.userId, { notification: inAppNotificationView, unreadCount });
|
|
28
28
|
}
|
|
29
29
|
};
|
|
30
30
|
InAppChannelProvider = __decorate([
|
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import { Subject } from 'rxjs';
|
|
2
2
|
import { afterResolve } from '../../../injector/index.js';
|
|
3
|
+
import type { TypedOmit } from '../../../types/index.js';
|
|
3
4
|
import type { InAppNotificationView } from '../../models/index.js';
|
|
4
5
|
import type { NotificationStreamItem } from '../../types.js';
|
|
6
|
+
type NotificationBusMessage = {
|
|
7
|
+
tenantId: string;
|
|
8
|
+
userId: string;
|
|
9
|
+
notification?: InAppNotificationView;
|
|
10
|
+
readId?: string;
|
|
11
|
+
readAll?: boolean;
|
|
12
|
+
archiveId?: string;
|
|
13
|
+
archiveAll?: boolean;
|
|
14
|
+
unreadCount?: number;
|
|
15
|
+
};
|
|
16
|
+
export type NotificationBusMessageData = TypedOmit<NotificationBusMessage, 'tenantId' | 'userId'>;
|
|
5
17
|
export declare class NotificationSseService {
|
|
6
18
|
#private;
|
|
7
19
|
[afterResolve](): void;
|
|
8
20
|
register(tenantId: string, userId: string): Subject<NotificationStreamItem>;
|
|
9
21
|
unregister(tenantId: string, userId: string, source: Subject<NotificationStreamItem>): void;
|
|
10
|
-
|
|
11
|
-
dispatchUnreadCountUpdate(tenantId: string, userId: string, unreadCount: number): Promise<void>;
|
|
22
|
+
dispatch(tenantId: string, userId: string, data: NotificationBusMessageData): Promise<void>;
|
|
12
23
|
private dispatchToLocal;
|
|
13
24
|
}
|
|
25
|
+
export {};
|
|
@@ -43,19 +43,11 @@ let NotificationSseService = class NotificationSseService {
|
|
|
43
43
|
}
|
|
44
44
|
source.complete();
|
|
45
45
|
}
|
|
46
|
-
async
|
|
47
|
-
await this.#messageBus.publish({
|
|
48
|
-
tenantId: notification.tenantId,
|
|
49
|
-
userId: notification.userId,
|
|
50
|
-
notification,
|
|
51
|
-
unreadCount,
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
async dispatchUnreadCountUpdate(tenantId, userId, unreadCount) {
|
|
46
|
+
async dispatch(tenantId, userId, data) {
|
|
55
47
|
await this.#messageBus.publish({
|
|
56
48
|
tenantId,
|
|
57
49
|
userId,
|
|
58
|
-
|
|
50
|
+
...data,
|
|
59
51
|
});
|
|
60
52
|
}
|
|
61
53
|
dispatchToLocal(message) {
|
|
@@ -63,7 +55,14 @@ let NotificationSseService = class NotificationSseService {
|
|
|
63
55
|
const userSources = tenantMap?.get(message.userId);
|
|
64
56
|
if (userSources != null) {
|
|
65
57
|
for (const source of userSources) {
|
|
66
|
-
source.next({
|
|
58
|
+
source.next({
|
|
59
|
+
notification: message.notification,
|
|
60
|
+
readId: message.readId,
|
|
61
|
+
readAll: message.readAll,
|
|
62
|
+
archiveId: message.archiveId,
|
|
63
|
+
archiveAll: message.archiveAll,
|
|
64
|
+
unreadCount: message.unreadCount,
|
|
65
|
+
});
|
|
67
66
|
}
|
|
68
67
|
}
|
|
69
68
|
}
|
|
@@ -20,8 +20,14 @@ export declare class NotificationService<Definitions extends NotificationDefinit
|
|
|
20
20
|
archive(tenantId: string, userId: string, id: string): Promise<void>;
|
|
21
21
|
archiveAll(tenantId: string, userId: string): Promise<void>;
|
|
22
22
|
unreadCount(tenantId: string, userId: string): Promise<number>;
|
|
23
|
+
getCatchupData(tenantId: string, userId: string, lastNotificationId: string): Promise<{
|
|
24
|
+
unreadCount: number;
|
|
25
|
+
missedNotifications: InAppNotificationView[];
|
|
26
|
+
readIds: string[];
|
|
27
|
+
archiveIds: string[];
|
|
28
|
+
}>;
|
|
29
|
+
getTimestamp(tenantId: string, userId: string, inAppId: string): Promise<number>;
|
|
23
30
|
getPreferences(tenantId: string, userId: string): Promise<NotificationPreference[]>;
|
|
24
31
|
updatePreference(tenantId: string, userId: string, type: string, channel: NotificationChannel, enabled: boolean): Promise<void>;
|
|
25
32
|
registerWebPush(tenantId: string, userId: string, endpoint: string, p256dh: Uint8Array<ArrayBuffer>, auth: Uint8Array<ArrayBuffer>): Promise<void>;
|
|
26
|
-
private dispatchUnreadCountUpdate;
|
|
27
33
|
}
|
|
@@ -6,7 +6,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
6
6
|
};
|
|
7
7
|
var _a;
|
|
8
8
|
var NotificationService_1;
|
|
9
|
-
import { and, desc, eq, isNull, lt, or } from 'drizzle-orm';
|
|
9
|
+
import { and, desc, eq, gt, isNull, lt, or } from 'drizzle-orm';
|
|
10
10
|
import { BadRequestError } from '../../../errors/bad-request.error.js';
|
|
11
11
|
import { inject, Singleton } from '../../../injector/index.js';
|
|
12
12
|
import { Logger } from '../../../logger/logger.js';
|
|
@@ -14,7 +14,7 @@ import { TRANSACTION_TIMESTAMP } from '../../../orm/index.js';
|
|
|
14
14
|
import { injectRepository, Transactional } from '../../../orm/server/index.js';
|
|
15
15
|
import { TaskQueue } from '../../../task-queue/task-queue.js';
|
|
16
16
|
import { tryIgnoreLogAsync } from '../../../utils/try-ignore.js';
|
|
17
|
-
import { isDefined } from '../../../utils/type-guards.js';
|
|
17
|
+
import { isDefined, isNotNull } from '../../../utils/type-guards.js';
|
|
18
18
|
import { InAppNotification, NotificationChannel, NotificationLogEntity, NotificationPreference, NotificationPriority, NotificationStatus, toInAppNotificationView, WebPushSubscription } from '../../models/index.js';
|
|
19
19
|
import { inAppNotification, notificationLog } from '../schemas.js';
|
|
20
20
|
import { NotificationAncillaryService } from './notification-ancillary.service.js';
|
|
@@ -77,19 +77,31 @@ let NotificationService = NotificationService_1 = class NotificationService exte
|
|
|
77
77
|
}
|
|
78
78
|
async markRead(tenantId, userId, id) {
|
|
79
79
|
await this.#inAppNotificationRepository.updateByQuery({ tenantId, id, userId }, { readTimestamp: TRANSACTION_TIMESTAMP });
|
|
80
|
-
await this
|
|
80
|
+
await tryIgnoreLogAsync(this.#logger, async () => {
|
|
81
|
+
const unreadCount = await this.unreadCount(tenantId, userId);
|
|
82
|
+
await this.#sseService.dispatch(tenantId, userId, { readId: id, unreadCount });
|
|
83
|
+
});
|
|
81
84
|
}
|
|
82
85
|
async markAllRead(tenantId, userId) {
|
|
83
|
-
await this.#inAppNotificationRepository.
|
|
84
|
-
await this
|
|
86
|
+
await this.#inAppNotificationRepository.updateManyByQuery({ tenantId, userId, readTimestamp: null }, { readTimestamp: TRANSACTION_TIMESTAMP });
|
|
87
|
+
await tryIgnoreLogAsync(this.#logger, async () => {
|
|
88
|
+
const unreadCount = await this.unreadCount(tenantId, userId);
|
|
89
|
+
await this.#sseService.dispatch(tenantId, userId, { readAll: true, unreadCount });
|
|
90
|
+
});
|
|
85
91
|
}
|
|
86
92
|
async archive(tenantId, userId, id) {
|
|
87
93
|
await this.#inAppNotificationRepository.updateByQuery({ tenantId, id, userId }, { archiveTimestamp: TRANSACTION_TIMESTAMP });
|
|
88
|
-
await this
|
|
94
|
+
await tryIgnoreLogAsync(this.#logger, async () => {
|
|
95
|
+
const unreadCount = await this.unreadCount(tenantId, userId);
|
|
96
|
+
await this.#sseService.dispatch(tenantId, userId, { archiveId: id, unreadCount });
|
|
97
|
+
});
|
|
89
98
|
}
|
|
90
99
|
async archiveAll(tenantId, userId) {
|
|
91
|
-
await this.#inAppNotificationRepository.
|
|
92
|
-
await this
|
|
100
|
+
await this.#inAppNotificationRepository.updateManyByQuery({ tenantId, userId, archiveTimestamp: null }, { archiveTimestamp: TRANSACTION_TIMESTAMP });
|
|
101
|
+
await tryIgnoreLogAsync(this.#logger, async () => {
|
|
102
|
+
const unreadCount = await this.unreadCount(tenantId, userId);
|
|
103
|
+
await this.#sseService.dispatch(tenantId, userId, { archiveAll: true, unreadCount });
|
|
104
|
+
});
|
|
93
105
|
}
|
|
94
106
|
async unreadCount(tenantId, userId) {
|
|
95
107
|
return await this.#inAppNotificationRepository.countByQuery({
|
|
@@ -99,6 +111,30 @@ let NotificationService = NotificationService_1 = class NotificationService exte
|
|
|
99
111
|
archiveTimestamp: null,
|
|
100
112
|
});
|
|
101
113
|
}
|
|
114
|
+
async getCatchupData(tenantId, userId, lastNotificationId) {
|
|
115
|
+
const lastTimestamp = await this.getTimestamp(tenantId, userId, lastNotificationId);
|
|
116
|
+
const [unreadCount, missedNotifications, readAndArchived] = await Promise.all([
|
|
117
|
+
this.unreadCount(tenantId, userId),
|
|
118
|
+
this.listInApp(tenantId, userId, { after: lastNotificationId, limit: 50 }),
|
|
119
|
+
this.#inAppNotificationRepository.session
|
|
120
|
+
.select({
|
|
121
|
+
id: inAppNotification.id,
|
|
122
|
+
readTimestamp: inAppNotification.readTimestamp,
|
|
123
|
+
archiveTimestamp: inAppNotification.archiveTimestamp,
|
|
124
|
+
})
|
|
125
|
+
.from(inAppNotification)
|
|
126
|
+
.where(and(eq(inAppNotification.tenantId, tenantId), eq(inAppNotification.userId, userId), or(gt(inAppNotification.readTimestamp, lastTimestamp), gt(inAppNotification.archiveTimestamp, lastTimestamp))))
|
|
127
|
+
.limit(50),
|
|
128
|
+
]);
|
|
129
|
+
const readIds = readAndArchived.filter((row) => isNotNull(row.readTimestamp) && (row.readTimestamp > lastTimestamp)).map((row) => row.id);
|
|
130
|
+
const archiveIds = readAndArchived.filter((row) => isNotNull(row.archiveTimestamp) && (row.archiveTimestamp > lastTimestamp)).map((row) => row.id);
|
|
131
|
+
return { unreadCount, missedNotifications, readIds, archiveIds };
|
|
132
|
+
}
|
|
133
|
+
async getTimestamp(tenantId, userId, inAppId) {
|
|
134
|
+
const inApp = await this.#inAppNotificationRepository.loadByQuery({ tenantId, userId, id: inAppId });
|
|
135
|
+
const log = await this.#notificationLogRepository.loadByQuery({ tenantId, userId, id: inApp.logId });
|
|
136
|
+
return log.timestamp;
|
|
137
|
+
}
|
|
102
138
|
async getPreferences(tenantId, userId) {
|
|
103
139
|
return await this.#preferenceRepository.loadManyByQuery({ tenantId, userId });
|
|
104
140
|
}
|
|
@@ -132,12 +168,6 @@ let NotificationService = NotificationService_1 = class NotificationService exte
|
|
|
132
168
|
auth,
|
|
133
169
|
});
|
|
134
170
|
}
|
|
135
|
-
async dispatchUnreadCountUpdate(tenantId, userId) {
|
|
136
|
-
await tryIgnoreLogAsync(this.#logger, async () => {
|
|
137
|
-
const unreadCount = await this.unreadCount(tenantId, userId);
|
|
138
|
-
await this.#sseService.dispatchUnreadCountUpdate(tenantId, userId, unreadCount);
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
171
|
};
|
|
142
172
|
NotificationService = NotificationService_1 = __decorate([
|
|
143
173
|
Singleton()
|
|
@@ -13,8 +13,8 @@ describe('NotificationSseService', () => {
|
|
|
13
13
|
expect(source).toBeDefined();
|
|
14
14
|
// We can't easily spy on the LocalMessageBus internals without more complex setup,
|
|
15
15
|
// but we can verify that sending doesn't throw.
|
|
16
|
-
const msg = { tenantId, userId, logId: 'l1' };
|
|
17
|
-
await expect(service.
|
|
16
|
+
const msg = { id: 'l1', tenantId, userId, logId: 'l1' };
|
|
17
|
+
await expect(service.dispatch(tenantId, userId, { notification: msg, unreadCount: 1 })).resolves.not.toThrow();
|
|
18
18
|
});
|
|
19
19
|
});
|
|
20
20
|
test('should dispatch unread count update', async () => {
|
|
@@ -23,7 +23,28 @@ describe('NotificationSseService', () => {
|
|
|
23
23
|
const tenantId = 't1';
|
|
24
24
|
const userId = 'u1';
|
|
25
25
|
await runInInjectionContext(injector, async () => {
|
|
26
|
-
|
|
26
|
+
const source = service.register(tenantId, userId);
|
|
27
|
+
const messages = [];
|
|
28
|
+
source.subscribe((msg) => messages.push(msg));
|
|
29
|
+
await service.dispatch(tenantId, userId, { unreadCount: 5 });
|
|
30
|
+
expect(messages).toHaveLength(1);
|
|
31
|
+
expect(messages[0]).toEqual({ unreadCount: 5 });
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
test('should dispatch mark read and mark all read', async () => {
|
|
35
|
+
const { injector } = await setupIntegrationTest({ modules: { messageBus: true, signals: true } });
|
|
36
|
+
const service = injector.resolve(NotificationSseService);
|
|
37
|
+
const tenantId = 't1';
|
|
38
|
+
const userId = 'u1';
|
|
39
|
+
await runInInjectionContext(injector, async () => {
|
|
40
|
+
const source = service.register(tenantId, userId);
|
|
41
|
+
const messages = [];
|
|
42
|
+
source.subscribe((msg) => messages.push(msg));
|
|
43
|
+
await service.dispatch(tenantId, userId, { readId: 'n1', unreadCount: 2 });
|
|
44
|
+
await service.dispatch(tenantId, userId, { readAll: true, unreadCount: 0 });
|
|
45
|
+
expect(messages).toHaveLength(2);
|
|
46
|
+
expect(messages[0]).toEqual({ readId: 'n1', unreadCount: 2 });
|
|
47
|
+
expect(messages[1]).toEqual({ readAll: true, unreadCount: 0 });
|
|
27
48
|
});
|
|
28
49
|
});
|
|
29
50
|
});
|
package/notification/types.d.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { InAppNotificationView, type NotificationDefinitionMap } from './models/index.js';
|
|
2
2
|
export declare const notificationStreamItemSchema: import("../schema/index.js").ObjectSchema<{
|
|
3
|
-
unreadCount: number;
|
|
4
3
|
notification?: InAppNotificationView<Record<string, import("./models/notification-log.model.js").NotificationDefinition<import("../types/types.js").ObjectLiteral, import("../types/types.js").ObjectLiteral>>> | undefined;
|
|
4
|
+
readId?: string | undefined;
|
|
5
|
+
readAll?: boolean | undefined;
|
|
6
|
+
archiveId?: string | undefined;
|
|
7
|
+
archiveAll?: boolean | undefined;
|
|
8
|
+
unreadCount?: number | undefined;
|
|
5
9
|
}>;
|
|
6
10
|
export type NotificationStreamItem<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap> = {
|
|
7
11
|
notification?: InAppNotificationView<Definitions>;
|
|
8
|
-
|
|
12
|
+
readId?: string;
|
|
13
|
+
readAll?: boolean;
|
|
14
|
+
archiveId?: string;
|
|
15
|
+
archiveAll?: boolean;
|
|
16
|
+
unreadCount?: number;
|
|
9
17
|
};
|
package/notification/types.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { number, object, optional } from '../schema/index.js';
|
|
1
|
+
import { boolean, number, object, optional, string } from '../schema/index.js';
|
|
2
2
|
import { InAppNotificationView } from './models/index.js';
|
|
3
3
|
export const notificationStreamItemSchema = object({
|
|
4
4
|
notification: optional(InAppNotificationView),
|
|
5
|
-
|
|
5
|
+
readId: optional(string()),
|
|
6
|
+
readAll: optional(boolean()),
|
|
7
|
+
archiveId: optional(string()),
|
|
8
|
+
archiveAll: optional(boolean()),
|
|
9
|
+
unreadCount: optional(number()),
|
|
6
10
|
});
|
package/package.json
CHANGED
|
@@ -2,20 +2,23 @@ import type { UndefinableJson } from '../types/types.js';
|
|
|
2
2
|
import type { AnyIterable } from '../utils/any-iterable-iterator.js';
|
|
3
3
|
import { ServerSentEventsSource } from './server-sent-events-source.js';
|
|
4
4
|
export type DataStreamErrorFormatter = (error: unknown) => UndefinableJson;
|
|
5
|
-
export type DataStreamSourceOptions = {
|
|
5
|
+
export type DataStreamSourceOptions<T> = {
|
|
6
6
|
/**
|
|
7
7
|
* Whether to send deltas (the changes) between the last and current data or always the full data.
|
|
8
|
+
* Useful when the target data is a full "state object".
|
|
9
|
+
* For notification-like streams, where only new items are sent, delta should usually be disabled.
|
|
8
10
|
*/
|
|
9
11
|
delta?: boolean;
|
|
10
12
|
errorFormatter?: DataStreamErrorFormatter;
|
|
13
|
+
idProvider?: (data: T) => string | undefined;
|
|
11
14
|
};
|
|
12
15
|
export declare class DataStreamSource<T> {
|
|
13
16
|
#private;
|
|
14
17
|
readonly eventSource: ServerSentEventsSource;
|
|
15
18
|
readonly closed: import("../signals/api.js").Signal<boolean>;
|
|
16
|
-
constructor({ delta, errorFormatter }?: DataStreamSourceOptions);
|
|
17
|
-
static fromIterable<T>(iterable: AnyIterable<T>, options?: DataStreamSourceOptions): DataStreamSource<T>;
|
|
18
|
-
send(data: T): Promise<void>;
|
|
19
|
+
constructor({ delta, errorFormatter, idProvider }?: DataStreamSourceOptions<T>);
|
|
20
|
+
static fromIterable<T>(iterable: AnyIterable<T>, options?: DataStreamSourceOptions<T>): DataStreamSource<T>;
|
|
21
|
+
send(data: T, id?: string): Promise<void>;
|
|
19
22
|
close(): Promise<void>;
|
|
20
23
|
error(error: unknown): Promise<void>;
|
|
21
24
|
}
|
|
@@ -16,12 +16,14 @@ const jsonDiffPatch = createDiffPatch({
|
|
|
16
16
|
export class DataStreamSource {
|
|
17
17
|
#useDelta;
|
|
18
18
|
#errorFormatter;
|
|
19
|
+
#idProvider;
|
|
19
20
|
eventSource = new ServerSentEventsSource();
|
|
20
21
|
closed = this.eventSource.closed;
|
|
21
22
|
#lastData;
|
|
22
|
-
constructor({ delta = true, errorFormatter } = {}) {
|
|
23
|
+
constructor({ delta = true, errorFormatter, idProvider } = {}) {
|
|
23
24
|
this.#useDelta = delta;
|
|
24
25
|
this.#errorFormatter = errorFormatter ?? defaultErrorFormatter;
|
|
26
|
+
this.#idProvider = idProvider;
|
|
25
27
|
}
|
|
26
28
|
static fromIterable(iterable, options) {
|
|
27
29
|
const source = new DataStreamSource(options);
|
|
@@ -43,16 +45,17 @@ export class DataStreamSource {
|
|
|
43
45
|
})();
|
|
44
46
|
return source;
|
|
45
47
|
}
|
|
46
|
-
async send(data) {
|
|
48
|
+
async send(data, id) {
|
|
47
49
|
if (this.eventSource.closed()) {
|
|
48
50
|
throw new Error('Cannot send data to a closed DataStreamSource connection.');
|
|
49
51
|
}
|
|
52
|
+
const eventId = id ?? this.#idProvider?.(data);
|
|
50
53
|
if (this.#useDelta && isDefined(this.#lastData)) {
|
|
51
54
|
const delta = jsonDiffPatch.diff(this.#lastData, data);
|
|
52
|
-
await this.eventSource.sendJson({ name: 'delta', data: delta });
|
|
55
|
+
await this.eventSource.sendJson({ name: 'delta', data: delta, id: eventId });
|
|
53
56
|
}
|
|
54
57
|
else {
|
|
55
|
-
await this.eventSource.sendJson({ name: 'data', data });
|
|
58
|
+
await this.eventSource.sendJson({ name: 'data', data, id: eventId });
|
|
56
59
|
}
|
|
57
60
|
if (this.#useDelta) {
|
|
58
61
|
this.#lastData = data;
|