From 81504a766ec823371db30c66fbc6eb678b8be853 Mon Sep 17 00:00:00 2001 From: Hadi Khazaee Asl Date: Sun, 9 Aug 2026 20:43:44 +0330 Subject: [PATCH] Initial Commit on git.saherelm.ir ... --- .editorconfig | 16 + .gitignore | 49 +++ README.md | 11 + angular.json | 45 ++ package.json | 78 ++++ projects/x-framework-push-service/README.md | 11 + .../x-framework-push-service/ng-package.json | 7 + .../x-framework-push-service/package.json | 22 + .../lib/base/x-base-entity.push.service.ts | 385 ++++++++++++++++++ .../src/lib/base/x-base-push.service.ts | 330 +++++++++++++++ .../src/lib/base/x-base-web-rtc.service.ts | 134 ++++++ .../src/lib/base/x-rtc-peer.service.ts | 30 ++ .../config/x-framework-push-service.config.ts | 19 + .../src/lib/models/x-hub-connection.dto.ts | 7 + .../x-push-service-connection-retry-policy.ts | 36 ++ .../policies/x-push-service-token-fetcher.ts | 9 + .../src/lib/services/.gitkeep | 0 .../src/lib/tokens/x-injectable-tokens.ts | 11 + .../typings/x-push-notification.typings.ts | 35 ++ .../lib/x-framework-push-service.module.ts | 11 + .../src/public-api.ts | 31 ++ .../tsconfig.lib.json | 20 + .../tsconfig.lib.prod.json | 10 + tsconfig.json | 40 ++ 24 files changed, 1347 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 README.md create mode 100644 angular.json create mode 100644 package.json create mode 100644 projects/x-framework-push-service/README.md create mode 100644 projects/x-framework-push-service/ng-package.json create mode 100644 projects/x-framework-push-service/package.json create mode 100644 projects/x-framework-push-service/src/lib/base/x-base-entity.push.service.ts create mode 100644 projects/x-framework-push-service/src/lib/base/x-base-push.service.ts create mode 100644 projects/x-framework-push-service/src/lib/base/x-base-web-rtc.service.ts create mode 100644 projects/x-framework-push-service/src/lib/base/x-rtc-peer.service.ts create mode 100644 projects/x-framework-push-service/src/lib/config/x-framework-push-service.config.ts create mode 100644 projects/x-framework-push-service/src/lib/models/x-hub-connection.dto.ts create mode 100644 projects/x-framework-push-service/src/lib/policies/x-push-service-connection-retry-policy.ts create mode 100644 projects/x-framework-push-service/src/lib/policies/x-push-service-token-fetcher.ts create mode 100644 projects/x-framework-push-service/src/lib/services/.gitkeep create mode 100644 projects/x-framework-push-service/src/lib/tokens/x-injectable-tokens.ts create mode 100644 projects/x-framework-push-service/src/lib/typings/x-push-notification.typings.ts create mode 100644 projects/x-framework-push-service/src/lib/x-framework-push-service.module.ts create mode 100644 projects/x-framework-push-service/src/public-api.ts create mode 100644 projects/x-framework-push-service/tsconfig.lib.json create mode 100644 projects/x-framework-push-service/tsconfig.lib.prod.json create mode 100644 tsconfig.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..59d9a3a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a353369 --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +/dist +/tmp +/out-tsc +# Only exists if Bazel was run +/bazel-out + +# dependencies +/node_modules + +# profiling files +chrome-profiler-events*.json + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# misc +/.sass-cache +/connect.lock +/coverage +/libpeerconnection.log +npm-debug.log +yarn-error.log +testem.log +/typings + +# System Files +.DS_Store +Thumbs.db + +# +documentation +package-lock.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..a6d45c0 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# XFrameworkPushService + +this is xFramework's PushService module and conatins all features of XFramework Library for providing Push functionalities. + +## Maintainer + +Hadi Khazaee asl + +[https://www.saherelm.ir](https://www.saherelm.ir) + +[hadi_khazaee_asl@yahoo.com](mailto:hadi_khazaee_asl@yahoo.com) diff --git a/angular.json b/angular.json new file mode 100644 index 0000000..c939b7c --- /dev/null +++ b/angular.json @@ -0,0 +1,45 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "cli": { + "analytics": false + }, + "version": 1, + "newProjectRoot": "projects", + "projects": { + "x-framework-push-service": { + "projectType": "library", + "root": "projects/x-framework-push-service", + "sourceRoot": "projects/x-framework-push-service/src", + "prefix": "lib", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:ng-packagr", + "options": { + "project": "projects/x-framework-push-service/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "projects/x-framework-push-service/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "projects/x-framework-push-service/tsconfig.lib.json" + }, + "ivy": { + "tsConfig": "projects/PROJECT_NAME/tsconfig.lib.ivy.json" + } + }, + "defaultConfiguration": "production" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "projects/x-framework-push-service/src/test.ts", + "tsConfig": "projects/x-framework-push-service/tsconfig.spec.json", + "karmaConfig": "projects/x-framework-push-service/karma.conf.js" + } + } + } + } + }, + "defaultProject": "x-framework-push-service" +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9c33e22 --- /dev/null +++ b/package.json @@ -0,0 +1,78 @@ +{ + "name": "x-framework-push-service-holder", + "version": "0.0.0", + "description": "an isolation layoer of xFrameworkPushServices module", + "keywords": [ + "x-framework", + "x-framework-services" + ], + "homepage": "https://saherelm.ir", + "author": { + "name": "Hadi Khazaee Asl", + "email": "hadi_khazaee_asl@yahoo.com", + "url": "https://saherelm.ir" + }, + "scripts": { + "publishModule": "npm run buildModule && npm run packModule", + "build:ivy": "ng build x-framework-push-service --configuration=ivy", + "serveDocs": "compodoc -p projects/x-framework-push-service/tsconfig.lib.json --theme material -s", + "buildModule": "npm run preBuildModule && ng build x-framework-push-service --configuration production", + "packModule": "cd dist/x-framework-push-service && npm pack && shx mv *.tgz ../../../../pack && cd ../..", + "prepareDependencies": "npm i --save-dev ../../pack/x-framework-core-0.0.1.tgz ../../pack/x-framework-services-0.0.1.tgz", + "prepareDependenciesByLink": "npm link x-framework-core x-framework-services", + "prepareDependenciesFromRegistry": "npm i -D --registry https://npm.farasoohub.ir x-framework-core x-framework-services", + "preBuildModule": "shx true --silent rm -rf dist && shx true --silent rm ../../pack/x-framework-push-service*.tgz && npm run prepareDependencies", + "generateDocs": "compodoc -p projects/x-framework-push-service/tsconfig.lib.json --theme material --hideGenerator --disableSourceCode --disableDomTree --disableTemplateTab --disableStyleTab --disablePrivate --disableLifeCycleHooks --disableRoutesGraph --disableCoverage --customFavicon ../xFrameworkDocuments/Resources/favicon.png --customLogo ../xFrameworkDocuments/Resources/favicon.png" + }, + "private": true, + "dependencies": { + "@angular/animations": "~12.0.4", + "@angular/common": "~12.0.4", + "@angular/compiler": "~12.0.4", + "@angular/core": "~12.0.4", + "@angular/forms": "~12.0.4", + "@angular/platform-browser": "~12.0.4", + "@angular/platform-browser-dynamic": "~12.0.4", + "@angular/router": "~12.0.4", + "rxjs": "~6.6.0", + "tslib": "^2.1.0", + "zone.js": "~0.11.4" + }, + "devDependencies": { + "@angular-devkit/build-angular": "~12.0.4", + "@angular/cdk": "^12.1.2", + "@angular/cli": "~12.0.4", + "@angular/compiler-cli": "~12.0.4", + "@angular/material": "^12.1.2", + "@angular/material-moment-adapter": "^12.1.2", + "@compodoc/compodoc": "^1.1.14", + "@ionic/angular": "^5.6.11", + "@microsoft/signalr": "^7.0.2", + "@microsoft/signalr-protocol-msgpack": "^7.0.2", + "@types/crypto-js": "^4.0.2", + "@types/howler": "^2.2.3", + "@types/jasmine": "~3.6.0", + "@types/node": "^12.11.1", + "@types/uuid": "^8.3.1", + "crypto-js": "^4.0.0", + "howler": "^2.2.3", + "ionicons": "^5.5.2", + "jalali-moment": "^3.3.10", + "jasmine-core": "~3.7.0", + "karma": "~6.3.0", + "karma-chrome-launcher": "~3.1.0", + "karma-coverage": "~2.0.3", + "karma-jasmine": "~4.0.0", + "karma-jasmine-html-reporter": "^1.5.0", + "libphonenumber-js": "^1.9.22", + "md5-typescript": "^1.0.5", + "moment": "^2.29.1", + "ng-packagr": "^12.0.0", + "ngx-device-detector": "^2.1.1", + "shx": "^0.3.3", + "typescript": "~4.2.3", + "uuid": "^8.3.2", + "x-framework-core": "file:../../pack/x-framework-core-0.0.1.tgz", + "x-framework-services": "file:../../pack/x-framework-services-0.0.1.tgz" + } +} diff --git a/projects/x-framework-push-service/README.md b/projects/x-framework-push-service/README.md new file mode 100644 index 0000000..5328e39 --- /dev/null +++ b/projects/x-framework-push-service/README.md @@ -0,0 +1,11 @@ +# XFrameworkPushService + +this is xFramework's PushService module and conatins all features of XFramework Library for providing Push functionalities. + +## Maintainer + +Hadi Khazaee asl + + + +hadi_khazaee_asl@yahoo.com diff --git a/projects/x-framework-push-service/ng-package.json b/projects/x-framework-push-service/ng-package.json new file mode 100644 index 0000000..ef11155 --- /dev/null +++ b/projects/x-framework-push-service/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../dist/x-framework-push-service", + "lib": { + "entryFile": "src/public-api.ts" + } +} \ No newline at end of file diff --git a/projects/x-framework-push-service/package.json b/projects/x-framework-push-service/package.json new file mode 100644 index 0000000..480d577 --- /dev/null +++ b/projects/x-framework-push-service/package.json @@ -0,0 +1,22 @@ +{ + "name": "x-framework-push-service", + "version": "0.0.1", + "description": "access xFrameworkPushServices features ...", + "keywords": [ + "x-framework", + "x-framework-services" + ], + "homepage": "https://saherelm.ir", + "author": { + "name": "Hadi Khazaee Asl", + "email": "hadi_khazaee_asl@yahoo.com", + "url": "https://saherelm.ir" + }, + "peerDependencies": { + "@angular/common": "^12.0.4", + "@angular/core": "^12.0.4" + }, + "dependencies": { + "tslib": "^2.1.0" + } +} \ No newline at end of file diff --git a/projects/x-framework-push-service/src/lib/base/x-base-entity.push.service.ts b/projects/x-framework-push-service/src/lib/base/x-base-entity.push.service.ts new file mode 100644 index 0000000..7980959 --- /dev/null +++ b/projects/x-framework-push-service/src/lib/base/x-base-entity.push.service.ts @@ -0,0 +1,385 @@ +import { Subject } from 'rxjs'; +import { Injectable } from '@angular/core'; +import { XBasePushService } from './x-base-push.service'; +import { hasChild, parseJson, isNullOrUndefined } from 'x-framework-core'; +import { XBaseEntityPushAction } from '../typings/x-push-notification.typings'; + +@Injectable() +export abstract class XBaseEntityPushService extends XBasePushService { + // + //#region Props ... + onAdded$ = new Subject<{ model: TEntity; actorId: string }>(); + onUpdated$ = new Subject<{ model: TEntity; actorId: string }>(); + onDeleted$ = new Subject<{ model: TEntity; actorId: string }>(); + onAddOrUpdated$ = new Subject<{ model: TEntity; actorId: string }>(); + onManyAdded$ = new Subject<{ model: Array; actorId: string }>(); + onManyUpdated$ = new Subject<{ model: Array; actorId: string }>(); + onManyDeleted$ = new Subject<{ model: Array; actorId: string }>(); + //#endregion + + // + //#region Actions ... + /** + * Fire Add Action ... + * + * @param model + * @returns + */ + async add(model: TEntity) { + // + // Validate ... + let isValid = !isNullOrUndefined(model); + if (!isValid) { + return; + } + + // + await this.invoke(XBaseEntityPushAction.Add, JSON.stringify(model)); + } + + /** + * Fire Update Action ... + * + * @param model + * @returns + */ + async update(model: TEntity) { + // + // Validate ... + let isValid = !isNullOrUndefined(model); + if (!isValid) { + return; + } + + // + await this.invoke(XBaseEntityPushAction.Update, JSON.stringify(model)); + } + + /** + * Fire Delete Action ... + * + * @param model + * @returns + */ + async delete(model: TEntity) { + // + // Validate ... + let isValid = !isNullOrUndefined(model); + if (!isValid) { + return; + } + + // + await this.invoke(XBaseEntityPushAction.Delete, JSON.stringify(model)); + } + + /** + * Fire Add Or Update Action ... + * + * @param model + * @returns + */ + async addOrUpdate(model: TEntity) { + // + // Validate ... + let isValid = !isNullOrUndefined(model); + if (!isValid) { + return; + } + + // + await this.invoke(XBaseEntityPushAction.AddOrUpdate, JSON.stringify(model)); + } + + /** + * Fire Add Many Action ... + * + * @param model + * @returns + */ + async addMany(models: Array) { + // + // Validate ... + let isValid = !isNullOrUndefined(models) && hasChild(models); + if (!isValid) { + return; + } + + // + await this.invoke(XBaseEntityPushAction.AddMany, JSON.stringify(models)); + } + + /** + * Fire Update Many Action ... + * + * @param model + * @returns + */ + async updateMany(models: Array) { + // + // Validate ... + let isValid = !isNullOrUndefined(models) && hasChild(models); + if (!isValid) { + return; + } + + // + await this.invoke(XBaseEntityPushAction.UpdateMany, JSON.stringify(models)); + } + + /** + * Fire Delete Many Action ... + * + * @param model + * @returns + */ + async deleteMany(models: Array) { + // + // Validate ... + let isValid = !isNullOrUndefined(models) && hasChild(models); + if (!isValid) { + return; + } + + // + await this.invoke(XBaseEntityPushAction.DeleteMany, JSON.stringify(models)); + } + //#endregion + + // + registerSubjects(): void { + super.registerSubjects(); + + // + this.onAdded((model: TEntity, actorId: string) => { + this.onAdded$.next({ model, actorId }); + }); + + // + this.onUpdated((model: TEntity, actorId: string) => { + this.onUpdated$.next({ model, actorId }); + }); + + // + this.onDeleted((model: TEntity, actorId: string) => { + this.onDeleted$.next({ model, actorId }); + }); + + // + this.onAddOrUpdated((model: TEntity, actorId: string) => { + this.onAddOrUpdated$.next({ model, actorId }); + }); + + // + this.onManyAdded((model: Array, actorId: string) => { + this.onManyAdded$.next({ model, actorId }); + }); + + // + this.onManyUpdated((model: Array, actorId: string) => { + this.onManyUpdated$.next({ model, actorId }); + }); + + // + this.onManyDeleted((model: Array, actorId: string) => { + this.onManyDeleted$.next({ model, actorId }); + }); + } + + // + //#region Event Oners ... + /** + * Register Add Event Listener ... + * + * @param callback + */ + public onAdded(callback: (model: TEntity, actorId: string) => any) { + // + this.on(XBaseEntityPushAction.Add, (modelJson: string, actorId: string) => { + // + const model = parseJson(modelJson); + callback(model, actorId); + }); + } + + /** + * Register Update Event Listener ... + * + * @param callback + */ + public onUpdated(callback: (model: TEntity, actorId: string) => any) { + // + this.on( + XBaseEntityPushAction.Update, + (modelJson: string, actorId: string) => { + // + const model = parseJson(modelJson); + callback(model, actorId); + } + ); + } + + /** + * Register Delete Event Listener ... + * + * @param callback + */ + public onDeleted(callback: (model: TEntity, actorId: string) => any) { + // + this.on( + XBaseEntityPushAction.Delete, + (modelJson: string, actorId: string) => { + // + const model = parseJson(modelJson); + callback(model, actorId); + } + ); + } + + /** + * Register Add Or Update Event Listener ... + * + * @param callback + */ + public onAddOrUpdated(callback: (model: TEntity, actorId: string) => any) { + // + this.on( + XBaseEntityPushAction.AddOrUpdate, + (modelJson: string, actorId: string) => { + // + const model = parseJson(modelJson); + callback(model, actorId); + } + ); + } + + /** + * Register Many Added Event Listener ... + * + * @param callback + */ + public onManyAdded( + callback: (model: Array, actorId: string) => any + ) { + // + this.on( + XBaseEntityPushAction.AddMany, + (modelJson: string, actorId: string) => { + // + const model = parseJson>(modelJson); + callback(model, actorId); + } + ); + } + + /** + * Register Many Updated Event Listener ... + * + * @param callback + */ + public onManyUpdated( + callback: (model: Array, actorId: string) => any + ) { + // + this.on( + XBaseEntityPushAction.UpdateMany, + (modelJson: string, actorId: string) => { + // + const model = parseJson>(modelJson); + callback(model, actorId); + } + ); + } + + /** + * Register Many Deleted Event Listener ... + * + * @param callback + */ + public onManyDeleted( + callback: (model: Array, actorId: string) => any + ) { + // + this.on( + XBaseEntityPushAction.DeleteMany, + (modelJson: string, actorId: string) => { + // + const model = parseJson>(modelJson); + callback(model, actorId); + } + ); + } + //#endregion + + // + //#region Event Offers ... + /** + * UnRegister Add Event Listener ... + * + * @param callback + */ + public offAdded(callback?: (model: TEntity, actorId: string) => any) { + this.off(XBaseEntityPushAction.Add, callback); + } + + /** + * UnRegister Update Event Listener ... + * + * @param callback + */ + public offUpdated(callback?: (model: TEntity, actorId: string) => any) { + this.off(XBaseEntityPushAction.Update, callback); + } + + /** + * UnRegister Delete Event Listener ... + * + * @param callback + */ + public offDeleted(callback?: (model: TEntity, actorId: string) => any) { + this.off(XBaseEntityPushAction.Delete, callback); + } + + /** + * UnRegister Add Or Update Event Listener ... + * + * @param callback + */ + public offAddOrUpdated(callback?: (model: TEntity, actorId: string) => any) { + this.off(XBaseEntityPushAction.AddOrUpdate, callback); + } + + /** + * UnRegister Many Added Event Listener ... + * + * @param callback + */ + public offManyAdded( + callback?: (model: Array, actorId: string) => any + ) { + this.off(XBaseEntityPushAction.AddMany, callback); + } + + /** + * UnRegister Many Updated Event Listener ... + * + * @param callback + */ + public offManyUpdated( + callback?: (model: Array, actorId: string) => any + ) { + this.off(XBaseEntityPushAction.UpdateMany, callback); + } + + /** + * UnRegister Many Deleted Event Listener ... + * + * @param callback + */ + public offManyDeleted( + callback?: (model: Array, actorId: string) => any + ) { + this.off(XBaseEntityPushAction.DeleteMany, callback); + } + //#endregion +} diff --git a/projects/x-framework-push-service/src/lib/base/x-base-push.service.ts b/projects/x-framework-push-service/src/lib/base/x-base-push.service.ts new file mode 100644 index 0000000..086386b --- /dev/null +++ b/projects/x-framework-push-service/src/lib/base/x-base-push.service.ts @@ -0,0 +1,330 @@ +import { + isFunction, + XBaseService, + XExceptionIDs, + throwException, + isNullOrUndefined, + isNullOrEmptyString, +} from 'x-framework-core'; +import { + ITransport, + HubConnection, + MessageHeaders, + HttpTransportType, + HubConnectionState, + HubConnectionBuilder, + IHttpConnectionOptions, +} from '@microsoft/signalr'; +import { Subject } from 'rxjs'; +import { + X_FRAMEWORK_PUSH_SERVICE_CONFIG, + X_FRAMEWORK_PUSH_SERVICE_TOKEN_FETCHER, +} from '../tokens/x-injectable-tokens'; +import { Inject, Injectable, Optional } from '@angular/core'; +import { XBasePushAction } from '../typings/x-push-notification.typings'; +import { MessagePackHubProtocol } from '@microsoft/signalr-protocol-msgpack'; +import { IXPushServiceTokenFetcher } from '../policies/x-push-service-token-fetcher'; +import { XFrameworkPushServiceConfig } from '../config/x-framework-push-service.config'; +import { XPushServiceConnectionRetryPolicy } from '../policies/x-push-service-connection-retry-policy'; + +@Injectable() +export abstract class XBasePushService extends XBaseService { + // + //#region Props ... + // + private CONNECTION_ID: string; + private HUB_CONNECTION: HubConnection; + private HUB_CONNECTION_POLICY: XPushServiceConnectionRetryPolicy; + + // + public readonly connect$ = new Subject(); + public readonly error$ = new Subject(); + public readonly newConnection$ = new Subject(); + public readonly connectionClosed$ = new Subject(); + + /** + * readonly connection id ... + */ + get connectionId() { + // + let result = ''; + + // + if (!isNullOrUndefined(this.HUB_CONNECTION)) { + result = this.HUB_CONNECTION.connectionId; + } + + // + return result; + } + //#endregion + + // + //#region Abstract ... + /** + * retrieve specific hub's name and route ... + */ + abstract hubName(): string; + + /** + * specify the service required access token validator or not ... + */ + abstract requiredAccessToken(): boolean; + + /** + * specify which protocols can used as transport protocol ... + */ + abstract transportType(): HttpTransportType | ITransport; + + /** + * Specified Skip Negotiation ... + */ + public skipNegotiation: boolean = false; + //#endregion + + // + //#region Constructor ... + constructor( + // + @Inject(X_FRAMEWORK_PUSH_SERVICE_CONFIG) + public config: XFrameworkPushServiceConfig, + + // + @Optional() + @Inject(X_FRAMEWORK_PUSH_SERVICE_TOKEN_FETCHER) + public tokenFetcher?: IXPushServiceTokenFetcher + ) { + super(config); + } + //#endregion + + // + //#region Actions ... + /** + * determines connection is stablished ot not ... + * @returns boolean value ... + */ + public isConnected() { + // + const result = + !isNullOrUndefined(this.HUB_CONNECTION) && + this.HUB_CONNECTION.state === HubConnectionState.Connected; + return result; + } + + /** + * retrieve hub route ... + * @returns string represent hub url ... + */ + public getHubRoute() { + // + let result = ''; + + // + // check config exists and + // check server route ... + if ( + isNullOrUndefined(this.config) || + isNullOrEmptyString(this.hubName()) || + isNullOrEmptyString(this.config.hubServerRoute) + ) { + return result; + } + + // + // Configure route ... + result = this.config.hubServerRoute.endsWith('/') + ? `${this.config.hubServerRoute}${ + this.config.hubsBaseRoute + }/${this.hubName()}` + : `${this.config.hubServerRoute}/${ + this.config.hubsBaseRoute + }/${this.hubName()}`; + + // + return result; + } + + /** + * connect to server ... + */ + public async connect() { + // + // Instantiate Hub Connection ... + if (!this.HUB_CONNECTION) { + // + // Prepare Reconnection Policy ... + this.HUB_CONNECTION_POLICY = new XPushServiceConnectionRetryPolicy( + this.config + ); + + // + // retrieve and validate ... + let hubRoute = this.getHubRoute(); + if (isNullOrEmptyString(hubRoute)) { + throwException(XExceptionIDs.InvalidConfiguration); + } + + // + // Normalize Transport Type ... + let transport: HttpTransportType | ITransport = + HttpTransportType.LongPolling; + if (!isNullOrUndefined(this.transportType())) { + transport = this.transportType(); + } + + // + // Add Custom Headers if Required ... + let headers: MessageHeaders = {}; + + // + // Create Connection Options ... + const connectionOptions: IHttpConnectionOptions = { + headers, + transport, + skipNegotiation: this.skipNegotiation, + }; + + // + // Attach Token Fetcher ... + if ( + !!this.requiredAccessToken() && + !isNullOrUndefined(this.tokenFetcher) + ) { + connectionOptions.accessTokenFactory = () => + this.tokenFetcher.getToken(); + } + // + // Create Connection builder ... + const connectionBuilder = new HubConnectionBuilder() + .withUrl(hubRoute, connectionOptions) + .configureLogging(this.config.connectionLogLevel) + .withAutomaticReconnect(this.HUB_CONNECTION_POLICY); + + // + // Handle Message Protocol ... + if (!!this.config.addSupportMessageProtocol) { + connectionBuilder.withHubProtocol(new MessagePackHubProtocol()); + } + + // + // Add LogLevel ... + if (!isNullOrUndefined(this.config.connectionLogLevel)) { + connectionBuilder.configureLogging(this.config.connectionLogLevel); + } + + // + // Create Connection ... + this.HUB_CONNECTION = connectionBuilder.build(); + + // + // Add Default Base Actions Event Handlers ... + this.on(XBasePushAction.NewConnection, (connectionId) => + this.newConnection$.next(connectionId) + ); + this.on(XBasePushAction.ConnectionClosed, (connectionId) => + this.connectionClosed$.next(connectionId) + ); + } + + // + await this.HUB_CONNECTION.start(); + if (!isNullOrEmptyString(this.HUB_CONNECTION.connectionId)) { + // + this.CONNECTION_ID = this.HUB_CONNECTION.connectionId; + this.connect$.next(); + this.registerSubjects(); + } else { + this.CONNECTION_ID = ''; + } + } + + /** + * Disconnect a Connection if Exists ... + */ + public async disconnect() { + // + if ( + this.HUB_CONNECTION && + this.HUB_CONNECTION.state !== HubConnectionState.Disconnected + ) { + // + await this.HUB_CONNECTION.stop(); + this.HUB_CONNECTION = undefined; + } + } + + /** + * Attach an Event Listener to Hub ... + * + * @param event + * @param callback + */ + on(event: string, callback: (...args: any[]) => any) { + // + // Validate ... + if ( + !isFunction(callback) || + isNullOrEmptyString(event) || + isNullOrUndefined(callback) || + isNullOrUndefined(this.HUB_CONNECTION) + ) { + return; + } + + // + this.HUB_CONNECTION.on(event, callback); + } + + /** + * Detach an Event Listener to Hub ... + * + * @param event + * @param callback + */ + off(event: string, callback?: (...args: any[]) => any): void { + // + // Validate ... + if (isNullOrEmptyString(event) || isNullOrUndefined(this.HUB_CONNECTION)) { + return; + } + + // + if (isNullOrUndefined(callback)) { + this.HUB_CONNECTION.off(event); + } else if (!isFunction(callback) || isNullOrUndefined(callback)) { + this.HUB_CONNECTION.off(event, callback); + } + } + + /** + * Invoke Specified Hub Actions ... + * + * @param method + * @param args + * @returns + */ + async invoke(method: string, ...args: any[]) { + // + // Validate Hub Connected ... + if (!this.isConnected() || isNullOrUndefined(this.HUB_CONNECTION)) { + return await Promise.resolve(undefined); + } + + // + return await this.HUB_CONNECTION.invoke(method, ...args); + } + //#endregion + + // + //#region Abstractions ... + /** + * here we have to register all events subjects ... + */ + registerSubjects() {} + //#endregion + + // + //#region Private ... + //#endregion +} diff --git a/projects/x-framework-push-service/src/lib/base/x-base-web-rtc.service.ts b/projects/x-framework-push-service/src/lib/base/x-base-web-rtc.service.ts new file mode 100644 index 0000000..8498f6e --- /dev/null +++ b/projects/x-framework-push-service/src/lib/base/x-base-web-rtc.service.ts @@ -0,0 +1,134 @@ +import { Subject } from 'rxjs'; +import { + X_FRAMEWORK_PUSH_SERVICE_CONFIG, + X_FRAMEWORK_PUSH_SERVICE_TOKEN_FETCHER, +} from '../tokens/x-injectable-tokens'; +import { isString } from 'x-framework-core'; +import { XBasePushService } from './x-base-push.service'; +import { Inject, Injectable, Optional } from '@angular/core'; +import { XWebRTCAction } from '../typings/x-push-notification.typings'; +import { IXPushServiceTokenFetcher } from '../policies/x-push-service-token-fetcher'; +import { XFrameworkPushServiceConfig } from '../config/x-framework-push-service.config'; + +@Injectable() +export abstract class XBaseWebRTCService extends XBasePushService { + // + //#region Props ... + public onOfferRecieved$ = new Subject(); + public onAnswerRecieved$ = new Subject<{ + answer: any; + requesterId: string; + }>(); + public onCandidateRecieved$ = new Subject<{ + candidate: any; + senderId: string; + }>(); + //#endregion + + // + //#region Constructor ... + constructor( + // + @Inject(X_FRAMEWORK_PUSH_SERVICE_CONFIG) + public config: XFrameworkPushServiceConfig, + + // + @Optional() + @Inject(X_FRAMEWORK_PUSH_SERVICE_TOKEN_FETCHER) + public tokenFetcher?: IXPushServiceTokenFetcher + ) { + super(config, tokenFetcher); + } + //#endregion + + // + //#region Actions ... + public async offer(offer: any, to: string) { + // + if (!isString(offer)) { + offer = JSON.stringify(offer); + } + + // + await this.invoke(XWebRTCAction.Offer, offer, to); + } + + public async answer(answer: any) { + // + if (!isString(answer)) { + answer = JSON.stringify(answer); + } + + // + await this.invoke(XWebRTCAction.Answer, answer); + } + + public async candidate(candidate: any, to: string) { + // + if (!isString(candidate)) { + candidate = JSON.stringify(candidate); + } + + // + await this.invoke(XWebRTCAction.Candidate, candidate, to); + } + //#endregion + + /** + * Register All Available + */ + registerSubjects(): void { + super.registerSubjects(); + + // + this.onOfferRecieved((offer: any) => { + this.onOfferRecieved$.next(offer); + }); + + // + this.onAnswerRecieved((answer: any, requesterId: string) => { + this.onAnswerRecieved$.next({ answer, requesterId }); + }); + + // + this.onCandidateRecieved((candidate: any, senderId: string) => { + this.onCandidateRecieved$.next({ candidate, senderId }); + }); + } + + // + //#region Event Handler Attaches ... + public onOfferRecieved(callback: (offer: any) => any) { + this.on(XWebRTCAction.Offer, callback); + } + + public onAnswerRecieved(callback: (answer: any, requesterId: string) => any) { + this.on(XWebRTCAction.Answer, callback); + } + + public onCandidateRecieved( + callback: (candidate: any, senderId: string) => any + ) { + this.on(XWebRTCAction.Candidate, callback); + } + //#endregion + + // + //#region Event Handler Detachers ... + public offOfferRecieved(callback?: (offer: any) => any) { + this.off(XWebRTCAction.Offer, callback); + } + + public offAnswerRecieved( + callback?: (answer: any, requesterId: string) => any + ) { + this.off(XWebRTCAction.Answer, callback); + } + + public offCandidateRecieved( + callback?: (candidate: any, senderId: string) => any + ) { + this.off(XWebRTCAction.Candidate, callback); + } + //#endregion +} diff --git a/projects/x-framework-push-service/src/lib/base/x-rtc-peer.service.ts b/projects/x-framework-push-service/src/lib/base/x-rtc-peer.service.ts new file mode 100644 index 0000000..cf28294 --- /dev/null +++ b/projects/x-framework-push-service/src/lib/base/x-rtc-peer.service.ts @@ -0,0 +1,30 @@ + +import { Inject, Injectable } from '@angular/core'; +import { X_FRAMEWORK_PUSH_SERVICE_CONFIG } from '../tokens/x-injectable-tokens'; +import { XFrameworkPushServiceConfig } from '../config/x-framework-push-service.config'; + +@Injectable({ + providedIn: 'root' +}) +export class XRTCPeerService { + // + //#region Constructor ... + constructor( + @Inject(X_FRAMEWORK_PUSH_SERVICE_CONFIG) + public config: XFrameworkPushServiceConfig + ) {} + //#endregion + + // + //#region Actions ... + public createConnection() { + // + const result = new RTCPeerConnection({ + iceServers: [...this.config.iceServers], + }); + + // + return result; + } + //#endregion +} diff --git a/projects/x-framework-push-service/src/lib/config/x-framework-push-service.config.ts b/projects/x-framework-push-service/src/lib/config/x-framework-push-service.config.ts new file mode 100644 index 0000000..069db8a --- /dev/null +++ b/projects/x-framework-push-service/src/lib/config/x-framework-push-service.config.ts @@ -0,0 +1,19 @@ +import { LogLevel } from '@microsoft/signalr'; +import { XFrameworkCoreConfig } from 'x-framework-core'; +import { XFrameworkServicesConfig } from 'x-framework-services'; + +export type XSharedConfig = XFrameworkCoreConfig & XFrameworkServicesConfig; + +export interface XFrameworkPushServiceConfig extends XSharedConfig { + // + // Push Service Configuration ... + hubsBaseRoute: string; + hubServerRoute: string; + connectionLogLevel?: LogLevel; + pushConnectionMaxRetry: number; + addSupportMessageProtocol?: boolean; + pushConnectionReconnectDelay: number; + + // + iceServers: Array; +} diff --git a/projects/x-framework-push-service/src/lib/models/x-hub-connection.dto.ts b/projects/x-framework-push-service/src/lib/models/x-hub-connection.dto.ts new file mode 100644 index 0000000..7e17c4f --- /dev/null +++ b/projects/x-framework-push-service/src/lib/models/x-hub-connection.dto.ts @@ -0,0 +1,7 @@ +import { XBaseDto } from 'x-framework-core'; + +export interface XHubConnectionDto extends XBaseDto { + userId: string; + username?: string; + connectionId: string; +} diff --git a/projects/x-framework-push-service/src/lib/policies/x-push-service-connection-retry-policy.ts b/projects/x-framework-push-service/src/lib/policies/x-push-service-connection-retry-policy.ts new file mode 100644 index 0000000..12eef0b --- /dev/null +++ b/projects/x-framework-push-service/src/lib/policies/x-push-service-connection-retry-policy.ts @@ -0,0 +1,36 @@ +import { Inject } from '@angular/core'; +import { IRetryPolicy, RetryContext } from '@microsoft/signalr'; +import { XFrameworkPushServiceConfig } from '../config/x-framework-push-service.config'; +import { X_FRAMEWORK_PUSH_SERVICE_CONFIG } from '../tokens/x-injectable-tokens'; + +export class XPushServiceConnectionRetryPolicy implements IRetryPolicy { + // + //#region Props ... + retryReason: Error = null; + previousRetryCount: number = 0; + elapsedMilliseconds: number = 0; + //#endregion + + // + //#region Constructor ... + constructor( + @Inject(X_FRAMEWORK_PUSH_SERVICE_CONFIG) + public config: XFrameworkPushServiceConfig + ) {} + //#endregion + + // + nextRetryDelayInMilliseconds(retryContext: RetryContext): number { + // + this.retryReason = retryContext.retryReason; + this.previousRetryCount = retryContext.previousRetryCount; + this.elapsedMilliseconds = retryContext.elapsedMilliseconds; + + // + if (retryContext.previousRetryCount < this.config.pushConnectionMaxRetry) { + return this.config.pushConnectionReconnectDelay; + } else { + return null; + } + } +} diff --git a/projects/x-framework-push-service/src/lib/policies/x-push-service-token-fetcher.ts b/projects/x-framework-push-service/src/lib/policies/x-push-service-token-fetcher.ts new file mode 100644 index 0000000..e2b60f2 --- /dev/null +++ b/projects/x-framework-push-service/src/lib/policies/x-push-service-token-fetcher.ts @@ -0,0 +1,9 @@ +import { Observable } from 'rxjs'; + +/** + * an interface for accessing tokens and user related infos ... + */ +export interface IXPushServiceTokenFetcher { + token$: Observable; + getToken(): string; +} diff --git a/projects/x-framework-push-service/src/lib/services/.gitkeep b/projects/x-framework-push-service/src/lib/services/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/projects/x-framework-push-service/src/lib/tokens/x-injectable-tokens.ts b/projects/x-framework-push-service/src/lib/tokens/x-injectable-tokens.ts new file mode 100644 index 0000000..3bdba86 --- /dev/null +++ b/projects/x-framework-push-service/src/lib/tokens/x-injectable-tokens.ts @@ -0,0 +1,11 @@ +import { InjectionToken } from '@angular/core'; +import { IXPushServiceTokenFetcher } from '../policies/x-push-service-token-fetcher'; +import { XFrameworkPushServiceConfig } from '../config/x-framework-push-service.config'; + +export const X_FRAMEWORK_PUSH_SERVICE_CONFIG = new InjectionToken( + 'x_config' +); + +export const X_FRAMEWORK_PUSH_SERVICE_TOKEN_FETCHER = new InjectionToken( + 'x_push_token_fetcher' +); diff --git a/projects/x-framework-push-service/src/lib/typings/x-push-notification.typings.ts b/projects/x-framework-push-service/src/lib/typings/x-push-notification.typings.ts new file mode 100644 index 0000000..fcdf742 --- /dev/null +++ b/projects/x-framework-push-service/src/lib/typings/x-push-notification.typings.ts @@ -0,0 +1,35 @@ +/** + * Base Hub Service Actions ... + */ +export enum XBasePushAction { + /// + /// new Connection Join ... + /// + NewConnection = 'NewConnection', + /// + /// an Exists Connection Closed ... + /// + ConnectionClosed = 'ConnectionClosed', +} + +/** + * Base Web RTC Actions ... + */ +export enum XWebRTCAction { + Offer = 'Offer', + Answer = 'Answer', + Candidate = 'Candidate', +} + +/** + * Base Entity Push Service Actions ... + */ +export enum XBaseEntityPushAction { + Add = 'Add', + Update = 'Update', + Delete = 'Delete', + AddMany = 'AddMany', + UpdateMany = 'UpdateMany', + DeleteMany = 'DeleteMany', + AddOrUpdate = 'AddOrUpdate', +} diff --git a/projects/x-framework-push-service/src/lib/x-framework-push-service.module.ts b/projects/x-framework-push-service/src/lib/x-framework-push-service.module.ts new file mode 100644 index 0000000..b0aa78f --- /dev/null +++ b/projects/x-framework-push-service/src/lib/x-framework-push-service.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; + +@NgModule({ + declarations: [ + ], + imports: [ + ], + exports: [ + ] +}) +export class XFrameworkPushServiceModule { } diff --git a/projects/x-framework-push-service/src/public-api.ts b/projects/x-framework-push-service/src/public-api.ts new file mode 100644 index 0000000..7afa8c7 --- /dev/null +++ b/projects/x-framework-push-service/src/public-api.ts @@ -0,0 +1,31 @@ +/* + * Public API Surface of x-framework-push-service + */ + +// +export * from './lib/tokens/x-injectable-tokens'; +export * from './lib/models/x-hub-connection.dto'; +export * from './lib/typings/x-push-notification.typings'; +export * from './lib/policies/x-push-service-token-fetcher'; +export * from './lib/config/x-framework-push-service.config'; +export * from './lib/policies/x-push-service-connection-retry-policy'; + +// +// Base ... +export * from './lib/base/x-rtc-peer.service'; +export * from './lib/base/x-base-push.service'; +export * from './lib/base/x-base-web-rtc.service'; +export * from './lib/base/x-base-entity.push.service'; + +// +// Interfaces ... + +// +// Models ... + +// +// Tools ... + +// +// Module ... +export * from './lib/x-framework-push-service.module'; diff --git a/projects/x-framework-push-service/tsconfig.lib.json b/projects/x-framework-push-service/tsconfig.lib.json new file mode 100644 index 0000000..1407202 --- /dev/null +++ b/projects/x-framework-push-service/tsconfig.lib.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/lib", + "target": "es2015", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [], + "lib": [ + "dom", + "es2018" + ] + }, + "exclude": [ + "src/test.ts", + "**/*.spec.ts" + ] +} diff --git a/projects/x-framework-push-service/tsconfig.lib.prod.json b/projects/x-framework-push-service/tsconfig.lib.prod.json new file mode 100644 index 0000000..06de549 --- /dev/null +++ b/projects/x-framework-push-service/tsconfig.lib.prod.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": { + "compilationMode": "partial" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4ab5487 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,40 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "baseUrl": "./", + "sourceMap": true, + "target": "es2017", + "module": "es2020", + "declaration": false, + "skipLibCheck": true, + "importHelpers": true, + "noImplicitAny": true, + "strictNullChecks": false, + "noImplicitReturns": true, + "outDir": "./dist/out-tsc", + "downlevelIteration": true, + "moduleResolution": "node", + "experimentalDecorators": true, + "noFallthroughCasesInSwitch": true, + "suppressImplicitAnyIndexErrors": true, + "forceConsistentCasingInFileNames": true, + "lib": [ + "es2018", + "dom" + ], + "paths": { + "x-framework-push-service": [ + "dist/x-framework-push-service/x-framework-push-service", + "dist/x-framework-push-service" + ] + } + }, + "angularCompilerOptions": { + "strictTemplates": true, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "enableI18nLegacyMessageIdFormat": false + } +}