@theshelf/notification 0.0.1

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/README.md ADDED
@@ -0,0 +1,68 @@
1
+
2
+ # Notification | The Shelf
3
+
4
+ The notification package provides a universal interaction layer with an actual notification solution.
5
+
6
+ This package is based on a push notification model.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install @theshelf/notification
12
+ ```
13
+
14
+ ## Implementations
15
+
16
+ Currently, there are two implementations:
17
+
18
+ * **Memory** - non-persistent in memory notifications (suited for testing).
19
+ * **WebPush** - web browser based push notifications.
20
+
21
+ ## Configuration
22
+
23
+ The used implementation needs to be configured in the `.env` file with the debug enabled setting.
24
+
25
+ ```env
26
+ NOTIFICATION_IMPLEMENTATION="webpush" # (memory | webpush)
27
+ ```
28
+
29
+ In case of WebPush, additional configuration is required.
30
+
31
+ ```env
32
+ WEBPUSH_VAPID_SUBJECT="..."
33
+ WEBPUSH_VAPID_PUBLIC_KEY="..."
34
+ WEBPUSH_VAPID_PRIVATE_KEY="..."
35
+ ```
36
+
37
+ ## How to use
38
+
39
+ An instance of the configured notification service implementation can be imported for performing notification operations.
40
+
41
+ ```ts
42
+ import notificationService from '@theshelf/notification';
43
+
44
+ // Perform operations with the notificationService instance
45
+ ```
46
+
47
+ ### Operations
48
+
49
+ ```ts
50
+ import notificationService from '@theshelf/notification';
51
+
52
+ // Open connection
53
+ await notificationService.connect();
54
+
55
+ // Close connection
56
+ await notificationService.disconnect();
57
+
58
+ // Subscribe to receive notifications
59
+ await notificationService.subscribe(recipientId);
60
+
61
+ // Unsubscribe from receiving notifications
62
+ // Throws SubscriptionNotFound if subscription not found.
63
+ await notificationService.unsubscribe(recipientId);
64
+
65
+ // Send a notification to a recipient
66
+ // Throws SubscriptionNotFound if subscription not found.
67
+ await notificationService.sendNotification(recipientId, title, body);
68
+ ```
@@ -0,0 +1,9 @@
1
+ export interface NotificationService {
2
+ get connected(): boolean;
3
+ get subscriptions(): Map<string, unknown>;
4
+ connect(): Promise<void>;
5
+ disconnect(): Promise<void>;
6
+ subscribe(recipientId: string, subscription: unknown): Promise<void>;
7
+ unsubscribe(recipientId: string): Promise<void>;
8
+ sendNotification(recipientId: string, title: string, message: string): Promise<void>;
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ import NotificationError from './NotificationError';
2
+ export default class NotConnected extends NotificationError {
3
+ constructor(message?: string);
4
+ }
@@ -0,0 +1,6 @@
1
+ import NotificationError from './NotificationError';
2
+ export default class NotConnected extends NotificationError {
3
+ constructor(message) {
4
+ super(message ?? 'Notification service not connected');
5
+ }
6
+ }
@@ -0,0 +1,2 @@
1
+ export default class NotificationError extends Error {
2
+ }
@@ -0,0 +1,2 @@
1
+ export default class NotificationError extends Error {
2
+ }
@@ -0,0 +1,4 @@
1
+ import NotificationError from './NotificationError';
2
+ export default class SubscriptionNotFound extends NotificationError {
3
+ constructor(recipientId?: string);
4
+ }
@@ -0,0 +1,6 @@
1
+ import NotificationError from './NotificationError';
2
+ export default class SubscriptionNotFound extends NotificationError {
3
+ constructor(recipientId) {
4
+ super(recipientId ? `Subscription not found for: ${recipientId}` : 'Subscription not found');
5
+ }
6
+ }
@@ -0,0 +1,4 @@
1
+ import NotificationError from './NotificationError';
2
+ export default class UnknownImplementation extends NotificationError {
3
+ constructor(name: string);
4
+ }
@@ -0,0 +1,6 @@
1
+ import NotificationError from './NotificationError';
2
+ export default class UnknownImplementation extends NotificationError {
3
+ constructor(name) {
4
+ super(`Unknown notification implementation: ${name}`);
5
+ }
6
+ }
@@ -0,0 +1,3 @@
1
+ import type { NotificationService } from './definitions/interfaces';
2
+ declare const _default: NotificationService;
3
+ export default _default;
@@ -0,0 +1,14 @@
1
+ import UnknownImplementation from './errors/UnknownImplementation';
2
+ import createMemory from './implementations/memory/create';
3
+ import createWebPush from './implementations/webpush/create';
4
+ const implementations = new Map([
5
+ ['memory', createMemory],
6
+ ['webpush', createWebPush],
7
+ ]);
8
+ const DEFAULT_NOTIFICATION_IMPLEMENTATION = 'memory';
9
+ const implementationName = process.env.NOTIFICATION_IMPLEMENTATION ?? DEFAULT_NOTIFICATION_IMPLEMENTATION;
10
+ const creator = implementations.get(implementationName.toLowerCase());
11
+ if (creator === undefined) {
12
+ throw new UnknownImplementation(implementationName);
13
+ }
14
+ export default creator();
@@ -0,0 +1,16 @@
1
+ import type { NotificationService } from '../../definitions/interfaces';
2
+ type Notification = {
3
+ title: string;
4
+ body: string;
5
+ };
6
+ export default class Memory implements NotificationService {
7
+ #private;
8
+ get connected(): boolean;
9
+ get subscriptions(): Map<string, Notification[]>;
10
+ connect(): Promise<void>;
11
+ disconnect(): Promise<void>;
12
+ subscribe(recipientId: string): Promise<void>;
13
+ unsubscribe(recipientId: string): Promise<void>;
14
+ sendNotification(recipientId: string, title: string, body: string): Promise<void>;
15
+ }
16
+ export {};
@@ -0,0 +1,46 @@
1
+ import NotConnected from '../../errors/NotConnected';
2
+ import SubscriptionNotFound from '../../errors/SubscriptionNotFound';
3
+ export default class Memory {
4
+ #subscriptions;
5
+ get connected() {
6
+ return this.#subscriptions !== undefined;
7
+ }
8
+ get subscriptions() {
9
+ return this.#getSubscriptions();
10
+ }
11
+ async connect() {
12
+ this.#subscriptions = new Map();
13
+ }
14
+ async disconnect() {
15
+ this.#subscriptions = undefined;
16
+ }
17
+ async subscribe(recipientId) {
18
+ const subscriptions = this.#getSubscriptions();
19
+ subscriptions.set(recipientId, []);
20
+ }
21
+ async unsubscribe(recipientId) {
22
+ const subscriptions = this.#getSubscriptions();
23
+ if (subscriptions.has(recipientId) === false) {
24
+ throw new SubscriptionNotFound(recipientId);
25
+ }
26
+ subscriptions.delete(recipientId);
27
+ }
28
+ async sendNotification(recipientId, title, body) {
29
+ const subscription = this.#getSubscription(recipientId);
30
+ subscription.push({ title, body });
31
+ }
32
+ #getSubscriptions() {
33
+ if (this.#subscriptions === undefined) {
34
+ throw new NotConnected();
35
+ }
36
+ return this.#subscriptions;
37
+ }
38
+ #getSubscription(recipientId) {
39
+ const subscriptions = this.#getSubscriptions();
40
+ const subscription = subscriptions.get(recipientId);
41
+ if (subscription === undefined) {
42
+ throw new SubscriptionNotFound(recipientId);
43
+ }
44
+ return subscription;
45
+ }
46
+ }
@@ -0,0 +1,2 @@
1
+ import Memory from './Memory';
2
+ export default function create(): Memory;
@@ -0,0 +1,4 @@
1
+ import Memory from './Memory';
2
+ export default function create() {
3
+ return new Memory();
4
+ }
@@ -0,0 +1,19 @@
1
+ import type { PushSubscription } from 'web-push';
2
+ import type { NotificationService } from '../../definitions/interfaces';
3
+ type VapidDetails = {
4
+ subject: string;
5
+ publicKey: string;
6
+ privateKey: string;
7
+ };
8
+ export default class WebPush implements NotificationService {
9
+ #private;
10
+ constructor(configuration: VapidDetails);
11
+ get connected(): boolean;
12
+ get subscriptions(): Map<string, PushSubscription>;
13
+ connect(): Promise<void>;
14
+ disconnect(): Promise<void>;
15
+ subscribe(recipientId: string, subscription: PushSubscription): Promise<void>;
16
+ unsubscribe(recipientId: string): Promise<void>;
17
+ sendNotification(recipientId: string, title: string, body: string): Promise<void>;
18
+ }
19
+ export {};
@@ -0,0 +1,52 @@
1
+ import webpush from 'web-push';
2
+ import NotConnected from '../../errors/NotConnected';
3
+ import SubscriptionNotFound from '../../errors/SubscriptionNotFound';
4
+ export default class WebPush {
5
+ #configuration;
6
+ #subscriptions;
7
+ constructor(configuration) {
8
+ this.#configuration = configuration;
9
+ }
10
+ get connected() {
11
+ return this.#subscriptions !== undefined;
12
+ }
13
+ get subscriptions() {
14
+ return this.#getSubscriptions();
15
+ }
16
+ async connect() {
17
+ this.#subscriptions = new Map();
18
+ webpush.setVapidDetails(this.#configuration.subject, this.#configuration.publicKey, this.#configuration.privateKey);
19
+ }
20
+ async disconnect() {
21
+ this.#subscriptions = undefined;
22
+ }
23
+ async subscribe(recipientId, subscription) {
24
+ const subscriptions = this.#getSubscriptions();
25
+ subscriptions.set(recipientId, subscription);
26
+ }
27
+ async unsubscribe(recipientId) {
28
+ const subscriptions = this.#getSubscriptions();
29
+ if (subscriptions.has(recipientId) === false) {
30
+ throw new SubscriptionNotFound(recipientId);
31
+ }
32
+ subscriptions.delete(recipientId);
33
+ }
34
+ async sendNotification(recipientId, title, body) {
35
+ const subscription = this.#getSubscription(recipientId);
36
+ await webpush.sendNotification(subscription, JSON.stringify({ title, body }));
37
+ }
38
+ #getSubscriptions() {
39
+ if (this.#subscriptions === undefined) {
40
+ throw new NotConnected();
41
+ }
42
+ return this.#subscriptions;
43
+ }
44
+ #getSubscription(recipientId) {
45
+ const subscriptions = this.#getSubscriptions();
46
+ const subscription = subscriptions.get(recipientId);
47
+ if (subscription === undefined) {
48
+ throw new SubscriptionNotFound(recipientId);
49
+ }
50
+ return subscription;
51
+ }
52
+ }
@@ -0,0 +1,2 @@
1
+ import WebPush from './WebPush';
2
+ export default function create(): WebPush;
@@ -0,0 +1,7 @@
1
+ import WebPush from './WebPush';
2
+ export default function create() {
3
+ const subject = process.env.WEBPUSH_SUBJECT ?? 'undefined';
4
+ const publicKey = process.env.WEBPUSH_PUBLIC_KEY ?? 'undefined';
5
+ const privateKey = process.env.WEBPUSH_PRIVATE_KEY ?? 'undefined';
6
+ return new WebPush({ subject, publicKey, privateKey });
7
+ }
@@ -0,0 +1,6 @@
1
+ export type { NotificationService } from './definitions/interfaces';
2
+ export { default as NotConnected } from './errors/NotConnected';
3
+ export { default as NotificationError } from './errors/NotificationError';
4
+ export { default as SubscriptionNotFound } from './errors/SubscriptionNotFound';
5
+ export { default as UnknownImplementation } from './errors/UnknownImplementation';
6
+ export { default } from './implementation';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { default as NotConnected } from './errors/NotConnected';
2
+ export { default as NotificationError } from './errors/NotificationError';
3
+ export { default as SubscriptionNotFound } from './errors/SubscriptionNotFound';
4
+ export { default as UnknownImplementation } from './errors/UnknownImplementation';
5
+ export { default } from './implementation';
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@theshelf/notification",
3
+ "private": false,
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "tsc",
8
+ "clean": "rimraf dist",
9
+ "test": "vitest run",
10
+ "test-coverage": "vitest run --coverage",
11
+ "lint": "eslint",
12
+ "review": "npm run build && npm run lint && npm run test",
13
+ "prepublishOnly": "npm run clean && npm run build"
14
+ },
15
+ "files": [
16
+ "README.md",
17
+ "dist"
18
+ ],
19
+ "types": "dist/index.d.ts",
20
+ "exports": "./dist/index.js",
21
+ "dependencies": {
22
+ "web-push": "3.6.7"
23
+ }
24
+ }