Skip to content

Commit

Permalink
feat(js): js sdk feeds module (#5688)
Browse files Browse the repository at this point in the history
* feat(js): improve the package json exports and tsup config
* feat(js): lazy session initialization and interface fixes
* feat(js): js sdk feeds module
  • Loading branch information
LetItRock authored Jun 20, 2024
1 parent becc7b0 commit 9088aab
Show file tree
Hide file tree
Showing 28 changed files with 1,271 additions and 1,249 deletions.
1 change: 0 additions & 1 deletion libs/shared/src/entities/messages/action.enum.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export enum ButtonTypeEnum {
PRIMARY = 'primary',
SECONDARY = 'secondary',
CLICKED = 'clicked',
}
45 changes: 36 additions & 9 deletions packages/client/src/api/api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
IPaginatedResponse,
ISessionDto,
INotificationDto,
MarkMessagesAsEnum,
} from '@novu/shared';
import { HttpClient } from '../http-client';
import {
Expand Down Expand Up @@ -43,6 +44,12 @@ export class ApiService {
}
}

private removeNullUndefined(obj) {
return Object.fromEntries(
Object.entries(obj).filter(([_, value]) => value != null)
);
}

setAuthorizationToken(token: string) {
this.httpClient.setAuthorizationToken(token);

Expand All @@ -57,10 +64,10 @@ export class ApiService {

async updateAction(
messageId: string,
executedType: ButtonTypeEnum,
status: MessageActionStatusEnum,
executedType: `${ButtonTypeEnum}`,
status: `${MessageActionStatusEnum}`,
payload?: Record<string, unknown>
): Promise<any> {
): Promise<INotificationDto> {
return await this.httpClient.post(
`/widgets/messages/${messageId}/actions/${executedType}`,
{
Expand All @@ -71,6 +78,9 @@ export class ApiService {
);
}

/**
* @deprecated use markMessagesAs instead
*/
async markMessageAs(
messageId: string | string[],
mark: { seen?: boolean; read?: boolean }
Expand All @@ -86,6 +96,19 @@ export class ApiService {
});
}

async markMessagesAs({
messageId,
markAs,
}: {
messageId: string | string[];
markAs: `${MarkMessagesAsEnum}`;
}): Promise<INotificationDto[]> {
return await this.httpClient.post(`/widgets/messages/mark-as`, {
messageId,
markAs,
});
}

async removeMessage(messageId: string): Promise<any> {
return await this.httpClient.delete(`/widgets/messages/${messageId}`, {});
}
Expand All @@ -104,13 +127,13 @@ export class ApiService {
return await this.httpClient.delete(url);
}

async markAllMessagesAsRead(feedId?: string | string[]): Promise<any> {
async markAllMessagesAsRead(feedId?: string | string[]): Promise<number> {
return await this.httpClient.post(`/widgets/messages/read`, {
feedId,
});
}

async markAllMessagesAsSeen(feedId?: string | string[]): Promise<any> {
async markAllMessagesAsSeen(feedId?: string | string[]): Promise<number> {
return await this.httpClient.post(`/widgets/messages/seen`, {
feedId,
});
Expand Down Expand Up @@ -154,17 +177,21 @@ export class ApiService {
});
}

async getUnseenCount(query: IUnseenCountQuery = {}) {
async getUnseenCount(
query: IUnseenCountQuery = {}
): Promise<{ count: number }> {
return await this.httpClient.get(
'/widgets/notifications/unseen',
query as unknown as CustomDataType
this.removeNullUndefined(query) as unknown as CustomDataType
);
}

async getUnreadCount(query: IUnreadCountQuery = {}) {
async getUnreadCount(
query: IUnreadCountQuery = {}
): Promise<{ count: number }> {
return await this.httpClient.get(
'/widgets/notifications/unread',
query as unknown as CustomDataType
this.removeNullUndefined(query) as unknown as CustomDataType
);
}

Expand Down
23 changes: 11 additions & 12 deletions packages/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,27 @@
"description": "Novu's JavaScript SDK for building custom inbox notification experiences",
"author": "",
"license": "ISC",
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"main": "dist/cjs/index.cjs",
"module": "dist/esm/index.js",
"types": "dist/esm/index.d.ts",
"type": "module",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
"types": "./dist/esm/index.d.ts",
"default": "./dist/esm/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
"types": "./dist/cjs/index.d.cts",
"default": "./dist/cjs/index.cjs"
}
},
"./package.json": "./package.json"
},
"files": [
"dist/*"
"dist/*",
"dist/esm/*",
"dist/cjs/*"
],
"sideEffects": false,
"private": true,
Expand All @@ -35,22 +37,19 @@
"test": "jest"
},
"devDependencies": {
"@size-limit/esbuild": "^11.1.4",
"@size-limit/file": "^11.1.4",
"@types/jest": "^29.2.3",
"@types/node": "^18.11.12",
"bytes-iec": "^3.1.1",
"chalk": "^5.3.0",
"esbuild-plugin-compress": "^1.0.1",
"jest": "^29.3.1",
"size-limit": "^11.1.4",
"tiny-glob": "^0.2.9",
"ts-jest": "^29.0.3",
"tsup": "^8.0.2",
"typescript": "4.9.5"
},
"dependencies": {
"@novu/client": "workspace:*",
"@novu/shared": "workspace:*",
"mitt": "^3.0.1"
}
}
91 changes: 42 additions & 49 deletions packages/js/scripts/size-limit.js
Original file line number Diff line number Diff line change
@@ -1,75 +1,68 @@
import fs from 'fs/promises';
import path from 'path';
import sizeLimit from 'size-limit';
import filePlugin from '@size-limit/file';
import esbuildPlugin from '@size-limit/esbuild';
import bytes from "bytes-iec"
import chalk from "chalk"
import bytes from 'bytes-iec';
import chalk from 'chalk';

const LIMIT = '10 kb';
const LIMIT_IN_BYTES = 10_000;
const baseDir = process.cwd();
const esmPath = path.resolve(baseDir, './dist/index.js');
const cjsPath = path.resolve(baseDir, './dist/index.cjs');
const umdPath = path.resolve(baseDir, './dist/novu.min.js');
const umdGzipPath = path.resolve(baseDir, './dist/novu.min.js.gz');

const formatBytes = (size) => {
return bytes.format(size, { unitSeparator: " " })
}
return bytes.format(size, { unitSeparator: ' ' });
};

const checks = [
{
name: 'ESM',
path: esmPath,
limit: LIMIT,
files: [esmPath],
sizeLimit: LIMIT_IN_BYTES,
},
const modules = [
{
name: 'CJS',
path: cjsPath,
limit: LIMIT,
files: [cjsPath],
sizeLimit: LIMIT_IN_BYTES,
name: 'UMD minified',
filePath: umdPath,
limit: '10 kb',
limitInBytes: 20_000,
},
{
name: 'UMD',
path: umdPath,
limit: LIMIT,
files: [umdPath],
sizeLimit: LIMIT_IN_BYTES,
name: 'UMD gzip',
filePath: umdGzipPath,
limit: '10 kb',
limitInBytes: 10_000,
},
];

const config = {
cwd: process.cwd(),
checks,
const checkFiles = async () => {
const result = [];
for (const module of modules) {
const { name, filePath, limitInBytes } = module;
const stats = await fs.stat(filePath);
const passed = stats.size <= limitInBytes;
result.push({ name, passed, size: formatBytes(stats.size), limit: formatBytes(limitInBytes) });
}

return result;
};

const calculateSizes = async () => {
console.log(chalk.gray("Checking the build dist files..."));

const results = await sizeLimit([filePlugin, esbuildPlugin], config);
if (config.failed) {
console.log(chalk.bold.red("\nThe build has reached the dist files size limits! 🚨\n"));
console.log(chalk.gray('🚧 Checking the build dist files...\n'));

results.filter((_, index) => {
const check = checks[index]
const { passed } = check
const checks = await checkFiles();
const anyFailed = checks.some((check) => !check.passed);

return !passed;
}).forEach((result, index) => {
const check = checks[index]
const { size } = result
const { name } = check
checks.forEach((check) => {
const { name, passed, size, limit } = check;

if (!passed) {
console.log(chalk.yellow(`The ${name} file has failed the size limit.`));
console.log(chalk.yellow(`Current size is "${formatBytes(size)}" and the limit is "${check.limit}".\n`));
})
console.log(chalk.yellow(`Current size is "${size}" and the limit is "${limit}".\n`));
} else {
console.log(chalk.green(`The ${name} file has passed the size limit.`));
console.log(chalk.green(`Current size is "${size}" and the limit is "${limit}".\n`));
}
});

if (anyFailed) {
console.log(chalk.bold.red('\nThe build has reached the dist files size limits! 🚨\n'));

process.exit(1);
} else {
console.log(chalk.green("All good! 🙌"));
console.log(chalk.green('All good! 🙌'));
}
}
};

calculateSizes();
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ApiService } from 'client/dist/cjs';
import { NovuEventEmitter } from '../event-emitter';
import { ApiService } from '@novu/client';

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

interface CallQueueItem {
fn: () => Promise<unknown>;
Expand All @@ -14,9 +16,9 @@ export class BaseModule {
#callsQueue: CallQueueItem[] = [];
#sessionError: unknown;

constructor(emitter: NovuEventEmitter, apiService: ApiService) {
this._emitter = emitter;
this._apiService = apiService;
constructor() {
this._emitter = NovuEventEmitter.getInstance();
this._apiService = ApiServiceSingleton.getInstance();
this._emitter.on('session.initialize.success', () => {
this.#callsQueue.forEach(async ({ fn, resolve }) => {
resolve(await fn());
Expand Down
25 changes: 18 additions & 7 deletions packages/js/src/event-emitter/novu-event-emitter.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
import mitt, { Emitter } from 'mitt';

import { Events, EventHandler, EventNames } from './types';
import { EventHandler, Events, EventNames } from './types';

type SingletonOptions = { recreate: true };

export class NovuEventEmitter {
private emitter: Emitter<Events>;
static #instance: NovuEventEmitter;
#mittEmitter: Emitter<Events>;

static getInstance(options?: SingletonOptions): NovuEventEmitter {
if (options?.recreate) {
NovuEventEmitter.#instance = new NovuEventEmitter();
}

return NovuEventEmitter.#instance;
}

constructor() {
this.emitter = mitt();
private constructor() {
this.#mittEmitter = mitt();
}

on<Key extends EventNames>(eventName: Key, listener: EventHandler<Events[Key]>): void {
this.emitter.on(eventName, listener);
this.#mittEmitter.on(eventName, listener);
}

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

emit<Key extends EventNames>(type: Key, event?: Events[Key]): void {
this.emitter.emit(type, event);
this.#mittEmitter.emit(type, event as Events[Key]);
}
}
Loading

1 comment on commit 9088aab

@github-actions
Copy link

Choose a reason for hiding this comment

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

Please sign in to comment.