Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(js): handling the web socket connection and events #5704

Merged
merged 20 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1f0e5b7
feat(js): the base js sdk package scaffolding
LetItRock May 31, 2024
880de43
feat(js): improve the package json exports and tsup config
LetItRock Jun 2, 2024
f37937b
feat(js): lazy session initialization and interface fixes
LetItRock Jun 3, 2024
1102abd
feat(js): renamed the *.spec.ts to *.test.ts
LetItRock Jun 7, 2024
bf034f9
feat(js): js sdk feeds module
LetItRock Jun 6, 2024
4a0308f
feat(js): the base js sdk package scaffolding
LetItRock May 31, 2024
4e74481
feat(js): set the dist file size limits and run the check after the b…
LetItRock Jun 3, 2024
fbea11b
feat(js): lazy session initialization and interface fixes
LetItRock Jun 3, 2024
67c85cb
feat(js): renamed the *.spec.ts to *.test.ts
LetItRock Jun 7, 2024
4167350
feat(js): js sdk feeds module
LetItRock Jun 6, 2024
b9abbae
chore(js): simplified the notification and preference classes
LetItRock Jun 11, 2024
bce4acf
feat(js): lazy session initialization and interface fixes
LetItRock Jun 3, 2024
3a15212
feat(js): the base js sdk package scaffolding
LetItRock May 31, 2024
acb3717
feat(js): js sdk feeds module
LetItRock Jun 6, 2024
26ee19b
feat(js): handling the web socket connection and events
LetItRock Jun 10, 2024
ebc1d38
feat(js): bundle umd with webpack
LetItRock Jun 10, 2024
79c9757
chore(js): removed old spec file
LetItRock Jun 11, 2024
d405bfb
chore(js): fixed the package building on esm and cjs modules
LetItRock Jun 12, 2024
bc2510d
feat(@novu/js): Com 57 add solidjs to novujs (#5729)
BiswaViraj Jun 17, 2024
3329087
feat(js): moved all web socket connection code under ws folder
LetItRock Jun 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/client/src/api/api.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {
import type {
ButtonTypeEnum,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use import type everywhere in the @novu/client so that @novu/shared won't be included in the bundle

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 thought (non-blocking): If we are just using the types, maybe we can move the @novu/shared under the devDependency as well.‏

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't because of the types generated for ESM, they are not "copied" during the build time.

MessageActionStatusEnum,
CustomDataType,
Expand Down Expand Up @@ -222,7 +222,7 @@ export class ApiService {
}

async getPreferences({
level = PreferenceLevelEnum.TEMPLATE,
level,
}: {
level?: `${PreferenceLevelEnum}`;
}): Promise<Array<IUserPreferenceSettings | IUserGlobalPreferenceSettings>> {
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/http-client/http-client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ApiOptions } from '..';
import { CustomDataType } from '@novu/shared';
import type { CustomDataType } from '@novu/shared';

const DEFAULT_API_VERSION = 'v1';
const DEFAULT_BACKEND_URL = 'https://api.novu.co';
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {
import type {
IPreferenceChannels,
NotificationTemplateCustomData,
} from '@novu/shared';
Expand Down
51 changes: 32 additions & 19 deletions packages/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,35 @@
"author": "",
"license": "ISC",
"main": "dist/cjs/index.cjs",
"module": "dist/esm/index.js",
"types": "dist/esm/index.d.ts",
"type": "module",
"module": "dist/esm/index.mjs",
"types": "dist/types/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/esm/index.d.ts",
"default": "./dist/esm/index.js"
},
"require": {
"types": "./dist/cjs/index.d.cts",
"default": "./dist/cjs/index.cjs"
}
"types": "./dist/types/index.d.ts",
"require": "./dist/cjs/index.cjs",
"import": "./dist/esm/index.mjs",
"default": "./dist/esm/index.mjs"
},
"./package.json": "./package.json"
"./ui": {
"types": "./dist/ui/index.d.ts",
"require": "./dist/ui/index.js",
"import": "./dist/ui/index.mjs",
"default": "./dist/ui/index.mjs"
}
},
"files": [
"dist/*",
"dist/esm/*",
"dist/cjs/*"
"dist/cjs",
"dist/esm",
"dist/types",
"dist/ui"
],
"sideEffects": false,
"private": true,
"scripts": {
"start": "pnpm run build -- --watch --sourcemap",
"build": "tsup && pnpm run post:build",
"post:build": "node scripts/size-limit.js",
"build": "tsup && pnpm run build:umd && pnpm run post:build",
"build:umd": "webpack --config webpack.config.cjs",
LetItRock marked this conversation as resolved.
Show resolved Hide resolved
"post:build": "node scripts/size-limit.mjs",
"lint": "eslint --ext .ts,.tsx src",
"test": "jest"
},
Expand All @@ -41,15 +43,26 @@
"@types/node": "^18.11.12",
"bytes-iec": "^3.1.1",
"chalk": "^5.3.0",
"compression-webpack-plugin": "^10.0.0",
"esbuild-plugin-compress": "^1.0.1",
"esbuild-plugin-solid": "^0.6.0",
"jest": "^29.3.1",
"solid-devtools": "^0.29.2",
"terser-webpack-plugin": "^5.3.9",
"tiny-glob": "^0.2.9",
"ts-jest": "^29.0.3",
"ts-loader": "~9.4.0",
"tsup": "^8.0.2",
"typescript": "4.9.5"
"tsup-preset-solid": "^2.2.0",
"typescript": "4.9.5",
"webpack": "^5.74.0",
"webpack-bundle-analyzer": "^4.9.0",
"webpack-cli": "^5.1.4"
},
"dependencies": {
"@novu/client": "workspace:*",
"mitt": "^3.0.1"
"mitt": "^3.0.1",
"socket.io-client": "4.7.2",
"solid-js": "^1.8.11"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,12 @@ const modules = [
{
name: 'UMD minified',
filePath: umdPath,
limit: '90 kb',
limitInBytes: 90_000,
limitInBytes: 70_000,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the current size?

Suggested change
limitInBytes: 70_000,
limitInBytes: 30_000,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2024-06-20 at 16 59 26

},
{
name: 'UMD gzip',
filePath: umdGzipPath,
limit: '25 kb',
limitInBytes: 25_000,
limitInBytes: 20_000,
},
];

Expand Down
11 changes: 9 additions & 2 deletions packages/js/src/base-module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ApiService } from '@novu/client';
import type { ApiService } from '@novu/client';

import { NovuEventEmitter } from './event-emitter';
import { Session } from './types';
import { ApiServiceSingleton } from './utils/api-service-singleton';

interface CallQueueItem {
Expand All @@ -19,13 +20,15 @@ export class BaseModule {
constructor() {
this._emitter = NovuEventEmitter.getInstance();
this._apiService = ApiServiceSingleton.getInstance();
this._emitter.on('session.initialize.success', () => {
this._emitter.on('session.initialize.success', ({ result }) => {
this.onSessionSuccess(result);
this.#callsQueue.forEach(async ({ fn, resolve }) => {
resolve(await fn());
});
this.#callsQueue = [];
});
this._emitter.on('session.initialize.error', ({ error }) => {
this.onSessionError(error);
this.#sessionError = error;
this.#callsQueue.forEach(({ reject }) => {
reject(error);
Expand All @@ -34,6 +37,10 @@ export class BaseModule {
});
}

protected onSessionSuccess(_: Session): void {}

protected onSessionError(_: unknown): void {}

Comment on lines +40 to +42
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two functions will be overridden by the class that extends this one.

async callWithSession<T>(fn: () => Promise<T>): Promise<T> {
if (this._apiService.isAuthenticated) {
return fn();
Expand Down
2 changes: 1 addition & 1 deletion packages/js/src/event-emitter/novu-event-emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class NovuEventEmitter {
}

off<Key extends EventNames>(eventName: Key, listener: EventHandler<Events[Key]>): void {
this.#mittEmitter.on(eventName, listener);
this.#mittEmitter.off(eventName, listener);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my fault 😅

}

emit<Key extends EventNames>(type: Key, event?: Events[Key]): void {
Expand Down
18 changes: 16 additions & 2 deletions packages/js/src/event-emitter/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type {
import { Preference } from '../preferences/preference';
import { FetchPreferencesArgs, UpdatePreferencesArgs } from '../preferences/types';
import type { InitializeSessionArgs } from '../session';
import type { PaginatedResponse, Session } from '../types';
import { PaginatedResponse, Session, WebSocketEvent } from '../types';

type NovuPendingEvent<A, O = undefined> = {
args: A;
Expand Down Expand Up @@ -94,6 +94,17 @@ type NotificationRemoveEvents = BaseEvents<
>;
type PreferencesFetchEvents = BaseEvents<'preferences.fetch', FetchPreferencesArgs, Preference[]>;
type PreferencesUpdateEvents = BaseEvents<'preferences.update', UpdatePreferencesArgs, Preference>;
type SocketConnectEvents = BaseEvents<'socket.connect', { socketUrl: string }, undefined>;
export type NotificationReceivedEvent = `notifications.${WebSocketEvent.RECEIVED}`;
export type NotificationUnseenEvent = `notifications.${WebSocketEvent.UNSEEN}`;
export type NotificationUnreadEvent = `notifications.${WebSocketEvent.UNREAD}`;
type SocketEvents = {
[key in NotificationReceivedEvent]: { result: Notification };
} & {
[key in NotificationUnseenEvent]: { result: number };
} & {
[key in NotificationUnreadEvent]: { result: number };
};

/**
* Events that are emitted by Novu Event Emitter.
Expand All @@ -119,8 +130,11 @@ export type Events = SessionInitializeEvents &
NotificationMarkActionAsEvents &
NotificationRemoveEvents &
PreferencesFetchEvents &
PreferencesUpdateEvents;
PreferencesUpdateEvents &
SocketConnectEvents &
SocketEvents;

export type EventNames = keyof Events;
export type SocketEventNames = keyof SocketEvents;

export type EventHandler<T = unknown> = (event: T) => void;
2 changes: 1 addition & 1 deletion packages/js/src/feeds/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApiService } from '@novu/client';
import type { ApiService } from '@novu/client';

import type { NovuEventEmitter } from '../event-emitter';
import { NotificationActionStatus, NotificationButton, NotificationStatus, TODO } from '../types';
Expand Down
4 changes: 2 additions & 2 deletions packages/js/src/feeds/notification.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApiService } from '@novu/client';
import type { ApiService } from '@novu/client';

import { EventHandler, EventNames, Events, NovuEventEmitter } from '../event-emitter';
import { Avatar, NotificationActionStatus, NotificationButton, Cta, NotificationStatus, TODO } from '../types';
Expand Down Expand Up @@ -122,6 +122,6 @@ export class Notification implements Pick<NovuEventEmitter, 'on' | 'off'> {
}

off<Key extends EventNames>(eventName: Key, listener: EventHandler<Events[Key]>): void {
this.#emitter.on(eventName, listener);
this.#emitter.off(eventName, listener);
}
}
3 changes: 2 additions & 1 deletion packages/js/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './novu';
export { Novu } from './novu';
export { InboxUI } from './ui';
11 changes: 9 additions & 2 deletions packages/js/src/novu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,22 @@ import { Feeds } from './feeds';
import { Session } from './session';
import { Preferences } from './preferences';
import { ApiServiceSingleton } from './utils/api-service-singleton';
import { Socket } from './ws';

const PRODUCTION_BACKEND_URL = 'https://api.novu.co';

type NovuOptions = {
export type NovuOptions = {
applicationIdentifier: string;
subscriberId: string;
subscriberHash?: string;
backendUrl?: string;
socketUrl?: string;
};

export class Novu implements Pick<NovuEventEmitter, 'on' | 'off'> {
#emitter: NovuEventEmitter;
#session: Session;
#socket: Socket;

public readonly feeds: Feeds;
public readonly preferences: Preferences;
Expand All @@ -32,13 +35,17 @@ export class Novu implements Pick<NovuEventEmitter, 'on' | 'off'> {
this.#session.initialize();
this.feeds = new Feeds();
this.preferences = new Preferences();
this.#socket = new Socket({ socketUrl: options.socketUrl });
}

on<Key extends EventNames>(eventName: Key, listener: EventHandler<Events[Key]>): void {
if (this.#socket.isSocketEvent(eventName)) {
this.#socket.initialize();
}
Comment on lines +42 to +44
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When someone tries to listen to one of the WS events then initialize the WebSocket connection and add listeners on events.

this.#emitter.on(eventName, listener);
}

off<Key extends EventNames>(eventName: Key, listener: EventHandler<Events[Key]>): void {
this.#emitter.on(eventName, listener);
this.#emitter.off(eventName, listener);
}
}
2 changes: 1 addition & 1 deletion packages/js/src/preferences/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApiService } from '@novu/client';
import type { ApiService } from '@novu/client';

import type { NovuEventEmitter } from '../event-emitter';
import type { TODO } from '../types';
Expand Down
2 changes: 1 addition & 1 deletion packages/js/src/preferences/preference.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApiService } from '@novu/client';
import type { ApiService } from '@novu/client';

import { NovuEventEmitter } from '../event-emitter';
import { ChannelPreference, ChannelType, PreferenceLevel, Workflow } from '../types';
Expand Down
2 changes: 1 addition & 1 deletion packages/js/src/session/session.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApiService } from '@novu/client';
import type { ApiService } from '@novu/client';

import { NovuEventEmitter } from '../event-emitter';
import { ApiServiceSingleton } from '../utils/api-service-singleton';
Expand Down
6 changes: 6 additions & 0 deletions packages/js/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ export enum PreferenceOverrideSource {
WORKFLOW_OVERRIDE = 'workflowOverride',
}

export enum WebSocketEvent {
RECEIVED = 'notification_received',
UNREAD = 'unread_count_changed',
UNSEEN = 'unseen_count_changed',
}

export type Session = {
token: string;
profile: {
Expand Down
36 changes: 36 additions & 0 deletions packages/js/src/ui/Inbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { For, createSignal, onMount, type Component } from 'solid-js';
import { Notification } from '../feeds';
import { Novu } from '../novu';
import type { NovuOptions } from '../novu';

const Inbox: Component<{
name: string;
options: NovuOptions;
}> = (props) => {
const [feeds, setFeeds] = createSignal<Notification[]>([]);

onMount(() => {
const novu = new Novu(props.options);

// eslint-disable-next-line promise/always-return
novu.feeds.fetch().then((data) => {
setFeeds(data.data);
});
});

return (
<div>
<header>Hello {props.name} </header>
<For each={feeds()}>
{(feed) => (
<div>
<h2>{feed.body}</h2>
<p>{feed.createdAt}</p>
</div>
)}
</For>
</div>
);
};

export default Inbox;
37 changes: 37 additions & 0 deletions packages/js/src/ui/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { render } from 'solid-js/web';
import Inbox from './Inbox';
import type { NovuOptions } from '../novu';

export class InboxUI {
#dispose: { (): void } | null = null;
#rootElement: HTMLElement;

mount(
el: HTMLElement,
{
name = 'novu',
options,
}: {
name?: string;
options: NovuOptions;
}
): void {
if (this.#dispose !== null) {
return;
}

this.#rootElement = document.createElement('div');
this.#rootElement.setAttribute('id', 'novu-ui');
el.appendChild(this.#rootElement);

const dispose = render(() => <Inbox name={name} options={options} />, this.#rootElement);

this.#dispose = dispose;
}

unmount(): void {
this.#dispose?.();
this.#dispose = null;
this.#rootElement.remove();
}
}
1 change: 1 addition & 0 deletions packages/js/src/umd.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Novu } from './novu';
import { InboxUI } from './ui';

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
Expand Down
Loading
Loading