mirror of
https://code.equilibrium.co.ao/ITO/doneit-web.git
synced 2026-04-20 21:35:50 +00:00
merge chat
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CameraService } from './camera.service';
|
||||
|
||||
describe('CameraService', () => {
|
||||
let service: CameraService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(CameraService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,21 @@ import { err, ok, Result } from 'neverthrow';
|
||||
import { TracingType, XTracerAsync } from 'src/app/services/monitoring/opentelemetry/tracer';
|
||||
import { error } from '../../services/Either/index';
|
||||
|
||||
/**
|
||||
* Parameters for taking a picture.
|
||||
* @typedef {Object} takePictureParams
|
||||
* @property {number} [quality=90] - The quality of the picture, from 0 to 100.
|
||||
* @property {CameraResultType} cameraResultType - The result type of the photo, e.g., Base64, URI.
|
||||
*/
|
||||
export type takePictureParams = {
|
||||
quality?: number;
|
||||
cameraResultType: CameraResultType;
|
||||
};
|
||||
|
||||
/**
|
||||
* Service for handling camera functionality.
|
||||
* This service provides methods to interact with the device's camera.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
@@ -13,7 +28,7 @@ export class CameraService {
|
||||
constructor() { }
|
||||
|
||||
|
||||
async takePicture(data: ITakePictureParams, tracing?: TracingType): Promise<Result<string, any>> {
|
||||
async takePictureAgenda(data: ITakePictureParams, tracing?: TracingType): Promise<Result<string, any>> {
|
||||
try {
|
||||
tracing?.addEvent('take picture')
|
||||
const capturedImage = await Camera.getPhoto({
|
||||
@@ -35,4 +50,25 @@ export class CameraService {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a picture using the device's camera.
|
||||
* @param {takePictureParams} params - The parameters for taking the picture.
|
||||
* @param {number} [params.quality=90] - The quality of the picture, from 0 to 100.
|
||||
* @param {CameraResultType} params.cameraResultType - The result type of the photo.
|
||||
* @returns {Promise<ok<File> | err<any>>} A promise that resolves with an `ok` result containing the file or an `err` result containing the error.
|
||||
*/
|
||||
async takePicture({quality = 90, cameraResultType }: takePictureParams) {
|
||||
try {
|
||||
const file = await Camera.getPhoto({
|
||||
quality: quality,
|
||||
resultType: cameraResultType,
|
||||
source: CameraSource.Camera
|
||||
});
|
||||
|
||||
return ok(file);
|
||||
} catch (e) {
|
||||
return err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { z } from "zod";
|
||||
import { EntityTable } from 'Dexie';
|
||||
import { zodDataUrlSchema } from "src/app/utils/zod";
|
||||
import { MessageAttachmentFileType, MessageAttachmentSource } from "src/app/core/chat/entity/message";
|
||||
|
||||
export const AttachmentTableSchema = z.object({
|
||||
$id: z.number().optional(), // local id
|
||||
$messageId: z.string(),
|
||||
attachmentId: z.string().optional(),
|
||||
file: z.instanceof(Blob).optional(),
|
||||
base64: zodDataUrlSchema.nullable().optional(),
|
||||
//
|
||||
fileType: z.nativeEnum(MessageAttachmentFileType).optional(),
|
||||
source: z.nativeEnum(MessageAttachmentSource).optional(),
|
||||
fileName: z.string().optional(),
|
||||
applicationId: z.number().optional(),
|
||||
docId: z.number().optional(),
|
||||
mimeType: z.string().nullable().optional(),
|
||||
id: z.string().uuid().optional(),
|
||||
description: z.string().optional()
|
||||
})
|
||||
|
||||
export type AttachmentTable = z.infer<typeof AttachmentTableSchema>
|
||||
export type DexieAttachmentsTableSchema = EntityTable<AttachmentTable, '$id'>;
|
||||
export const AttachmentTableColumn = '++$id, id, $messageId, messageId, file'
|
||||
@@ -0,0 +1,11 @@
|
||||
import { EntityTable } from 'Dexie';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const BoldTableSchema = z.object({
|
||||
roomId: z.string(),
|
||||
bold: z.number()
|
||||
})
|
||||
|
||||
export type BoldTable = z.infer<typeof BoldTableSchema>;
|
||||
export type DexieBoldTable = EntityTable<BoldTable, 'roomId'>;
|
||||
export const BoldTableColumn = 'roomId, bold';
|
||||
@@ -0,0 +1,15 @@
|
||||
import { EntityTable } from 'Dexie';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const DistributionTableSchema = z.object({
|
||||
$messageIdMemberId: z.string().optional(),
|
||||
messageId: z.string(),
|
||||
memberId: z.number(),
|
||||
readAt: z.string().nullable(),
|
||||
deliverAt: z.string().nullable(),
|
||||
roomId: z.string(),
|
||||
})
|
||||
|
||||
export type DistributionTable = z.infer<typeof DistributionTableSchema>
|
||||
export type DexieDistributionTable = EntityTable<DistributionTable, '$messageIdMemberId'>;
|
||||
export const DistributionTableColumn = '$messageIdMemberId, messageId, memberId, readAt, deliverAt, roomId'
|
||||
@@ -0,0 +1,19 @@
|
||||
import { z } from "zod";
|
||||
import { EntityTable } from 'Dexie';
|
||||
|
||||
export const MemberTableSchema = z.object({
|
||||
$roomIdUserId: z.string().optional(),
|
||||
id: z.string().optional(), // useless
|
||||
roomId: z.string(),
|
||||
wxUserId: z.number(),
|
||||
wxFullName: z.string(),
|
||||
wxeMail: z.string(),
|
||||
userPhoto: z.string().nullable(),
|
||||
joinAt: z.string(),
|
||||
status: z.string().optional(), // useless
|
||||
isAdmin: z.boolean()
|
||||
})
|
||||
|
||||
export type MemberTable = z.infer<typeof MemberTableSchema>
|
||||
export type DexieMembersTableSchema = EntityTable<MemberTable, '$roomIdUserId'>;
|
||||
export const MemberTableColumn = '$roomIdUserId, userId, id, user, joinAt, roomId, status, wxUserId, isAdmin'
|
||||
@@ -0,0 +1,54 @@
|
||||
import { EntityTable } from 'Dexie';
|
||||
import { MessageAttachmentFileType, MessageAttachmentSource } from 'src/app/core/chat/entity/message';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const MessageTableSchema = z.object({
|
||||
$id: z.string().optional(),
|
||||
$createAt: z.number().optional(),
|
||||
id: z.string().uuid().optional(),
|
||||
roomId: z.string().uuid().optional(),
|
||||
message: z.string().nullable().optional(),
|
||||
requestId: z.string().nullable().optional(),
|
||||
messageType: z.number(),
|
||||
canEdit: z.boolean(),
|
||||
oneShot: z.boolean(),
|
||||
sentAt: z.string().optional(),
|
||||
editedAt: z.string().nullable().optional(),
|
||||
isDeleted: z.boolean().optional(),
|
||||
requireUnlock: z.boolean(),
|
||||
sender: z.object({
|
||||
wxUserId: z.number(),
|
||||
wxFullName: z.string(),
|
||||
wxeMail: z.string(),
|
||||
userPhoto: z.string().nullable().optional(),
|
||||
}),
|
||||
receiverId: z.number().optional(),
|
||||
sending: z.boolean().optional(),
|
||||
reactions: z.object({
|
||||
id: z.string(),
|
||||
reactedAt: z.string(),
|
||||
reaction: z.string(),
|
||||
sender: z.object({}),
|
||||
}).array().optional(),
|
||||
info: z.array(z.object({
|
||||
memberId: z.number(),
|
||||
readAt: z.string().nullable(),
|
||||
deliverAt: z.string().nullable()
|
||||
})).optional(),
|
||||
attachments: z.array(z.object({
|
||||
fileType: z.nativeEnum(MessageAttachmentFileType),
|
||||
source: z.nativeEnum(MessageAttachmentSource),
|
||||
fileName: z.string().optional(),
|
||||
applicationId: z.number().optional(),
|
||||
docId: z.number().optional(),
|
||||
id: z.string().optional(),
|
||||
description: z.string().nullable().optional(),
|
||||
mimeType: z.string().nullable().optional()
|
||||
})).optional(),
|
||||
origin: z.enum(['history', 'local', 'incoming']).optional()
|
||||
})
|
||||
|
||||
export type MessageTable = z.infer<typeof MessageTableSchema>
|
||||
export type DexieMessageTable = EntityTable<MessageTable, '$id'>;
|
||||
export const messageTableColumn = '$id, id, roomId, senderId, message, messageType, canEdit, oneShot, requireUnlock, sending'
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { z } from "zod";
|
||||
import { EntityTable } from 'Dexie';
|
||||
import { RoomType } from "src/app/core/chat/entity/group";
|
||||
import { MessageEntity, MessageEntitySchema } from "src/app/core/chat/entity/message";
|
||||
import { MessageTableSchema } from "./message";
|
||||
|
||||
export const RoomTableSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
roomName: z.string(),
|
||||
createdBy: z.object({
|
||||
wxUserId: z.number(),
|
||||
wxFullName: z.string(),
|
||||
wxeMail: z.string().email(),
|
||||
userPhoto: z.string().nullable().optional()// api check
|
||||
}),
|
||||
createdAt: z.any(),
|
||||
expirationDate: z.any().nullable(),
|
||||
roomType: z.nativeEnum(RoomType),
|
||||
messages: MessageTableSchema.array().optional(),
|
||||
bold: z.number().optional()
|
||||
})
|
||||
|
||||
export type RoomTable = z.infer<typeof RoomTableSchema>
|
||||
export type DexieRoomsTable = EntityTable<RoomTable, 'id'>;
|
||||
export const RoomTableColumn = 'id, createdBy, roomName, roomType, expirationDate, lastMessage'
|
||||
@@ -0,0 +1,13 @@
|
||||
import { z } from "zod";
|
||||
import { EntityTable } from 'Dexie';
|
||||
|
||||
export const TypingTableSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
userId: z.number().optional(),
|
||||
userName: z.string(),
|
||||
roomId: z.string(),
|
||||
})
|
||||
|
||||
export type TypingTable = z.infer<typeof TypingTableSchema>
|
||||
export type DexieTypingsTable = EntityTable<TypingTable, 'id'>;
|
||||
export const TypingTableColumn = 'id, userId, userName, roomId, entryDate'
|
||||
@@ -0,0 +1,12 @@
|
||||
import { EntityTable } from 'Dexie';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const UserPhotoTableSchema = z.object({
|
||||
wxUserId: z.number(),
|
||||
file: z.string(),
|
||||
attachmentId: z.string().nullable()
|
||||
})
|
||||
|
||||
export type UserPhotoTable = z.infer<typeof UserPhotoTableSchema>
|
||||
export type DexieUserPhotoTable = EntityTable<UserPhotoTable, 'wxUserId'>;
|
||||
export const UserPhotoTableColumn = 'wxUserId'
|
||||
@@ -0,0 +1,43 @@
|
||||
|
||||
import { Dexie } from 'Dexie';
|
||||
import { DexieMessageTable, messageTableColumn, MessageTable } from 'src/app/infra/database/dexie/instance/chat/schema/message';
|
||||
import { DexieMembersTableSchema, MemberTableColumn } from 'src/app/infra/database/dexie/instance/chat/schema/members';
|
||||
import { DexieRoomsTable, RoomTableColumn } from 'src/app/infra/database/dexie/instance/chat/schema/room';
|
||||
import { DexieTypingsTable, TypingTableColumn } from 'src/app/infra/database/dexie/instance/chat/schema/typing';
|
||||
import { MessageEntity } from 'src/app/core/chat/entity/message';
|
||||
import { AttachmentTableColumn, DexieAttachmentsTableSchema } from 'src/app/infra/database/dexie/instance/chat/schema/attachment';
|
||||
import { DexieDistributionTable, DistributionTable, DistributionTableColumn } from './instance/chat/schema/destribution';
|
||||
import { BoldTableColumn, DexieBoldTable } from './instance/chat/schema/bold';
|
||||
import { DexieUserPhotoTable, UserPhotoTable, UserPhotoTableColumn } from './instance/chat/schema/user-foto';
|
||||
// import FDBFactory from 'fake-indexeddb/lib/FDBFactory';
|
||||
// import FDBKeyRange from 'fake-indexeddb/lib/FDBKeyRange';
|
||||
|
||||
|
||||
// Database declaration (move this to its own module also)
|
||||
export const chatDatabase = new Dexie('chat-database-v2',{
|
||||
// indexedDB: new FDBFactory,
|
||||
// IDBKeyRange: FDBKeyRange, // Mocking IDBKeyRange
|
||||
}) as Dexie & {
|
||||
message: DexieMessageTable,
|
||||
members: DexieMembersTableSchema,
|
||||
room: DexieRoomsTable,
|
||||
typing: DexieTypingsTable,
|
||||
attachment: DexieAttachmentsTableSchema,
|
||||
distribution: DexieDistributionTable,
|
||||
bold: DexieBoldTable,
|
||||
userPhoto: DexieUserPhotoTable
|
||||
};
|
||||
|
||||
chatDatabase.version(1).stores({
|
||||
message: messageTableColumn,
|
||||
members: MemberTableColumn,
|
||||
room: RoomTableColumn,
|
||||
typing: TypingTableColumn,
|
||||
attachment: AttachmentTableColumn,
|
||||
distribution: DistributionTableColumn,
|
||||
bold:BoldTableColumn,
|
||||
userPhoto: UserPhotoTableColumn
|
||||
});
|
||||
|
||||
chatDatabase.message.mapToClass(MessageEntity)
|
||||
// Apply in-memory storage
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Camera, CameraPhoto, CameraResultType, CameraSource } from '@capacitor/camera';
|
||||
import { err, ok, Result } from 'neverthrow';
|
||||
|
||||
/**
|
||||
* Parameters for picking a picture.
|
||||
* @typedef {Object} PickPictureParams
|
||||
* @property {number} [quality=90] - The quality of the picture, from 0 to 100. Defaults to 90.
|
||||
* @property {CameraResultType} [cameraResultType=CameraResultType.DataUrl] - The result type of the photo. Defaults to `CameraResultType.DataUrl`.
|
||||
*/
|
||||
type PickPictureParams = {
|
||||
quality?: number;
|
||||
cameraResultType?: CameraResultType;
|
||||
};
|
||||
|
||||
/**
|
||||
* Error types for the FilePickerService.
|
||||
*/
|
||||
export interface FilePickerError {
|
||||
type: 'PERMISSION_DENIED' | 'CANCELLED' | 'UNKNOWN';
|
||||
message: string;
|
||||
originalError?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service for handling file picking functionality.
|
||||
* This service provides methods to pick a picture from the device's photo library.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class FilePickerService {
|
||||
|
||||
constructor() { }
|
||||
|
||||
/**
|
||||
* Picks a picture from the device's photo library.
|
||||
* @param {PickPictureParams} params - The parameters for picking the picture.
|
||||
* @param {number} [params.quality=90] - The quality of the picture, from 0 to 100. Defaults to 90.
|
||||
* @param {CameraResultType} [params.cameraResultType=CameraResultType.DataUrl] - The result type of the photo. Defaults to `CameraResultType.DataUrl`.
|
||||
* @returns {Promise<ok<File> | err<any>>} A promise that resolves with an `ok` result containing the file or an `err` result containing the error.
|
||||
*/
|
||||
async getPicture({quality = 90, cameraResultType = CameraResultType.DataUrl }: PickPictureParams): Promise<Result<CameraPhoto, FilePickerError>> {
|
||||
try {
|
||||
const file = await Camera.getPhoto({
|
||||
quality: quality,
|
||||
resultType: cameraResultType,
|
||||
source: CameraSource.Photos
|
||||
})
|
||||
|
||||
return ok(file);
|
||||
} catch (e) {
|
||||
if (e.message.includes('denied')) {
|
||||
return err({
|
||||
type: 'PERMISSION_DENIED',
|
||||
message: 'Permission to access photos was denied.',
|
||||
originalError: e
|
||||
});
|
||||
} else if (e.message.includes('User cancelled photos app')) {
|
||||
return err({
|
||||
type: 'CANCELLED',
|
||||
message: 'User cancelled the photo selection.',
|
||||
originalError: e
|
||||
});
|
||||
} else {
|
||||
return err({
|
||||
type: 'UNKNOWN',
|
||||
message: 'An unknown error occurred while picking the picture.',
|
||||
originalError: e
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { FilePicker, PickFilesResult } from '@capawesome/capacitor-file-picker';
|
||||
import { err, ok, Result } from 'neverthrow';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class FilePickerMobileService {
|
||||
|
||||
constructor() { }
|
||||
|
||||
/**
|
||||
* @example
|
||||
* ```typescript
|
||||
* const types = ['application/pdf', 'application/doc', 'application/docx','application/xls', 'application/xlsx', 'application/ppt','application/pptx', 'application/txt'];
|
||||
* const multiple = false; // Invalid due to commas
|
||||
* const readData = true; // Invalid due to commas
|
||||
* ```
|
||||
*/
|
||||
async getFile({types, multiple, readData}): Promise<Result<PickFilesResult, any>> {
|
||||
try {
|
||||
|
||||
const result = await FilePicker.pickFiles({
|
||||
types: types,
|
||||
multiple: multiple,
|
||||
readData: readData,
|
||||
});
|
||||
|
||||
return ok(result)
|
||||
} catch (e) {
|
||||
return err(e)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
|
||||
import { err, ok, Result } from 'neverthrow';
|
||||
import { FileType } from 'src/app/models/fileType';
|
||||
|
||||
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class FilePickerWebService {
|
||||
|
||||
constructor() { }
|
||||
|
||||
getFileFromDevice(types: typeof FileType[]): Promise<Result<File, any>> {
|
||||
let input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = types.join(', ')
|
||||
|
||||
input.click();
|
||||
|
||||
return new Promise((resolve, reject)=>{
|
||||
input.onchange = async () => {
|
||||
const file = Array.from(input.files)
|
||||
resolve(ok(file[0] as File));
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { File, IWriteOptions } from '@awesome-cordova-plugins/file/ngx';
|
||||
import { err, ok, Result } from 'neverthrow';
|
||||
import { FileOpener } from '@awesome-cordova-plugins/file-opener/ngx';
|
||||
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class FileSystemMobileService {
|
||||
|
||||
constructor(
|
||||
private file: File,
|
||||
private FileOpener: FileOpener,
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Write a new file to the desired location.
|
||||
*
|
||||
* @param {string} path Base FileSystem. Please refer to the iOS and Android filesystem above
|
||||
* @param {string} fileName path relative to base path
|
||||
* @param {string | Blob | ArrayBuffer} text content, blob or ArrayBuffer to write
|
||||
* @param {IWriteOptions} whether to replace/append to an existing file. See IWriteOptions for more information.
|
||||
* @param options
|
||||
* @returns {Promise<any>} Returns a Promise that resolves to updated file entry or rejects with an error.
|
||||
*/
|
||||
async writeFile(path: string, fileName: string, context: string | Blob | ArrayBuffer, options?: IWriteOptions): Promise<Result<any, any>> {
|
||||
try {
|
||||
const result = await this.file.writeFile(path, fileName, context, { replace: true })
|
||||
return ok(result)
|
||||
} catch (e) {
|
||||
console.log('Error writing file', e)
|
||||
return err(e)
|
||||
}
|
||||
}
|
||||
|
||||
async fileOpener(filePath: string, mimetype: string) {
|
||||
try {
|
||||
const result = this.FileOpener.open(filePath, mimetype)
|
||||
return ok(result)
|
||||
} catch (e) {
|
||||
console.error('Error opening file', e)
|
||||
return err(e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
|
||||
import { HttpResult } from './type';
|
||||
import { Result } from 'neverthrow';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
|
||||
export abstract class HttpAdapter {
|
||||
abstract post<T>(url: string, body: any): Promise<Result<HttpResult<T>, HttpErrorResponse>>
|
||||
abstract get<T>(url: string, options?: Object): Promise<Result<HttpResult<T>, HttpErrorResponse>>
|
||||
abstract put<T>(url: string, body: any): Promise<Result<HttpResult<T>, HttpErrorResponse>>
|
||||
abstract patch<T>(url: string, body?: Object): Promise<Result<HttpResult<T>, HttpErrorResponse>>
|
||||
abstract delete<T>(url: string, body?: Object): Promise<Result<HttpResult<T>, HttpErrorResponse>>
|
||||
abstract listen():Observable<Result<HttpResult<any>, HttpErrorResponse>>
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { HttpAdapter } from './adapter';
|
||||
import { HttpService } from './http.service';
|
||||
|
||||
@NgModule({
|
||||
imports: [],
|
||||
declarations: [],
|
||||
schemas: [],
|
||||
providers: [
|
||||
{
|
||||
provide: HttpAdapter,
|
||||
useClass: HttpService, // or MockDataService
|
||||
}
|
||||
],
|
||||
entryComponents: []
|
||||
})
|
||||
export class HttpModule {}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { HttpErrorResponse, HttpHeaders, HttpParams, HttpResponse } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { ok, err, Result } from 'neverthrow';
|
||||
import { HttpResult } from './type';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class HttpService {
|
||||
|
||||
private responseSubject = new BehaviorSubject<Result<HttpResult<any>, HttpErrorResponse>>(null);
|
||||
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
async post<T>(url: string, body: any): Promise<Result<HttpResult<T>, HttpErrorResponse>> {
|
||||
try {
|
||||
const response = await this.http.post<T>(url, body, { observe: 'response' }).toPromise();
|
||||
const data = {
|
||||
data: response.body,
|
||||
status: response.status,
|
||||
headers: response.headers,
|
||||
url: response.url || url,
|
||||
method: '',
|
||||
}
|
||||
this.responseSubject.next(ok(data))
|
||||
return ok(data);
|
||||
} catch (e) {
|
||||
this.responseSubject.next(err(e))
|
||||
return err(e as HttpErrorResponse);
|
||||
}
|
||||
}
|
||||
|
||||
async get<T>(url: string, options = {}): Promise<Result<HttpResult<T>, HttpErrorResponse>> {
|
||||
try {
|
||||
const response = await this.http.get<T>(url, { ...options, observe: 'response' }).toPromise();
|
||||
|
||||
const data = {
|
||||
method: 'GET',
|
||||
data: response.body,
|
||||
status: response.status,
|
||||
headers: response.headers,
|
||||
url: response.url || url
|
||||
}
|
||||
|
||||
this.responseSubject.next(ok(data))
|
||||
return ok(data);
|
||||
} catch (e) {
|
||||
this.responseSubject.next(err(e))
|
||||
return err(e as HttpErrorResponse);
|
||||
}
|
||||
}
|
||||
|
||||
async put<T>(url: string, body: any): Promise<Result<HttpResult<T>, HttpErrorResponse>> {
|
||||
try {
|
||||
const response = await this.http.put<T>(url, body, { observe: 'response' }).toPromise();
|
||||
|
||||
const data = {
|
||||
data: response.body,
|
||||
status: response.status,
|
||||
headers: response.headers,
|
||||
url: response.url || url,
|
||||
method: '',
|
||||
}
|
||||
|
||||
this.responseSubject.next(ok(data))
|
||||
return ok(data);
|
||||
} catch (e) {
|
||||
this.responseSubject.next(err(e))
|
||||
return err(e as HttpErrorResponse);
|
||||
}
|
||||
}
|
||||
|
||||
async patch<T>(url: string, body: any = {}): Promise<Result<HttpResult<T>, HttpErrorResponse>> {
|
||||
try {
|
||||
const response = await this.http.patch<T>(url, body, { observe: 'response' }).toPromise();
|
||||
|
||||
const data = {
|
||||
data: response.body,
|
||||
status: response.status,
|
||||
headers: response.headers,
|
||||
url: response.url || url,
|
||||
method: '',
|
||||
}
|
||||
|
||||
this.responseSubject.next(ok(data))
|
||||
return ok(data);
|
||||
} catch (e) {
|
||||
this.responseSubject.next(err(e))
|
||||
return err(e as HttpErrorResponse);
|
||||
}
|
||||
}
|
||||
|
||||
async delete<T>(url: string, body = {}): Promise<Result<HttpResult<T>, HttpErrorResponse>> {
|
||||
const options = {
|
||||
body: body, // Pass payload as the body of the request
|
||||
observe: 'response' as 'body'
|
||||
};
|
||||
|
||||
try {
|
||||
const response: any = await this.http.delete<T>(url, options).toPromise();
|
||||
|
||||
const data = {
|
||||
data: response?.body,
|
||||
status: response?.status,
|
||||
headers: response?.headers,
|
||||
url: response?.url || url,
|
||||
method: '',
|
||||
}
|
||||
|
||||
this.responseSubject.next(ok(data))
|
||||
return ok(data as any);
|
||||
} catch (e) {
|
||||
this.responseSubject.next(err(e))
|
||||
return err(e as HttpErrorResponse);
|
||||
}
|
||||
}
|
||||
|
||||
listen() {
|
||||
return this.responseSubject.asObservable()
|
||||
}
|
||||
}
|
||||
|
||||
export function isHttpResponse(data: any): data is HttpResponse<any> {
|
||||
return typeof data.status == 'number';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { HttpErrorResponse, HttpHeaders, HttpParams } from '@angular/common/http';
|
||||
import { Result } from 'neverthrow';
|
||||
|
||||
export interface Options {
|
||||
headers?: HttpHeaders
|
||||
params?: HttpParams
|
||||
}
|
||||
|
||||
export interface HttpResult<T> {
|
||||
data: T | null;
|
||||
status: number;
|
||||
headers: HttpHeaders;
|
||||
url: string;
|
||||
method: string
|
||||
}
|
||||
|
||||
export type IHttPReturn<T> = Promise<Result<HttpResult<T>, HttpErrorResponse>>
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Result } from 'neverthrow';
|
||||
import { ZodError} from 'zod';
|
||||
import { IDBError } from './types';
|
||||
import Dexie, { EntityTable, Table } from 'Dexie';
|
||||
|
||||
// Define a type for the Result of repository operations
|
||||
export type RepositoryResult<T, E> = Result<T, IDBError<E>>;
|
||||
export abstract class IDexieRepository<T, R, I = EntityTable<any, any>> {
|
||||
|
||||
abstract createTransaction(callback: (table: I) => Promise<void>): Promise<void>
|
||||
abstract insert(document: T): Promise<RepositoryResult<number, T>>
|
||||
|
||||
abstract insertMany(documents: T[]): Promise<RepositoryResult<number[], T[]>>
|
||||
|
||||
abstract update(id: any, updatedDocument: Partial<T>) : Promise<RepositoryResult<number, T>>
|
||||
|
||||
abstract delete(id: any): Promise<RepositoryResult<void, T>>
|
||||
|
||||
abstract findById(id: any) : Promise<RepositoryResult<R, any>>
|
||||
|
||||
abstract find(filter: Partial<T>): Promise<RepositoryResult<R[], T[]>>
|
||||
|
||||
abstract findOne(filter: Partial<T>): Promise<RepositoryResult<T | undefined, T>>
|
||||
|
||||
abstract findAll(): Promise<RepositoryResult<T[], T>>
|
||||
|
||||
abstract count(filter?: Object): Promise<RepositoryResult<number, T>>
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
import { Result, ok, err } from 'neverthrow';
|
||||
import Dexie, { EntityTable, Table } from 'Dexie';
|
||||
import { ZodError, ZodObject, ZodSchema } from 'zod';
|
||||
import { Logger } from 'src/app/services/logger/main/service';
|
||||
import { IDexieRepository, RepositoryResult } from '../adapter'
|
||||
import { IDBError, IDexieError } from '../types';
|
||||
|
||||
// Define a type for the Result of repository operations
|
||||
|
||||
class DBError<T> extends Error implements IDBError<T> {
|
||||
zodError?: ZodError<T>;
|
||||
parameters: T;
|
||||
error?: IDexieError;
|
||||
|
||||
constructor(data: IDBError<T>) {
|
||||
super(data.message);
|
||||
this.zodError = data.zodError;
|
||||
this.parameters = data.parameters;
|
||||
this.error = data.error;
|
||||
|
||||
Logger.error(data.message, {
|
||||
zodError: this.zodError,
|
||||
parameters: this.parameters
|
||||
})
|
||||
|
||||
// // Manually capture the stack trace if needed
|
||||
// if (Error.captureStackTrace) {
|
||||
// Error.captureStackTrace(this, DBError);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
export class DexieRepository<T, R, I = EntityTable<any, any>> implements IDexieRepository<T, R, I> {
|
||||
private table: EntityTable<any, any>;
|
||||
private ZodSchema: ZodSchema<T>
|
||||
private ZodPartialSchema: ZodSchema<T>
|
||||
private db: Dexie
|
||||
|
||||
constructor(table: EntityTable<any, any>, ZodSchema: ZodSchema, db?:Dexie) {
|
||||
this.table = table as any
|
||||
this.ZodSchema = ZodSchema
|
||||
this.ZodPartialSchema = (ZodSchema as ZodObject<any>).partial() as any;
|
||||
this.db = db
|
||||
}
|
||||
|
||||
// Method to create a transaction and use the callback
|
||||
async createTransaction(callback: (table:I) => Promise<void>): Promise<void> {
|
||||
return this.db.transaction('rw', this.table, async () => {
|
||||
try {
|
||||
// Execute the callback function
|
||||
await callback(this.table as any);
|
||||
} catch (error) {
|
||||
// Transactions are automatically rolled back on error
|
||||
throw error;
|
||||
}
|
||||
}).then(() => {
|
||||
console.log('Transaction completed successfully');
|
||||
}).catch((error) => {
|
||||
console.error('Transaction failed: ' + error);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async insert(document: T): Promise<RepositoryResult<any, T>> {
|
||||
|
||||
const dataValidation = this.ZodSchema.safeParse(document)
|
||||
|
||||
if(dataValidation.success) {
|
||||
try {
|
||||
const id = await this.table.add(dataValidation.data);
|
||||
return ok(id);
|
||||
} catch (_error) {
|
||||
const error: IDexieError = _error
|
||||
return err(new DBError({
|
||||
message: `dexie.js failed to insert into ${this.table.name}, ${error.message}`,
|
||||
parameters: document,
|
||||
error: error
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
return err(new DBError({
|
||||
message: `dexie.js failed to insert into ${this.table.name}, invalid data`,
|
||||
parameters: document,
|
||||
zodError: dataValidation.error
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
async insertMany(documents: T[]): Promise<RepositoryResult<number[], T[]>> {
|
||||
// Validate each document
|
||||
const schema = this.ZodSchema.array()
|
||||
|
||||
const validationResult = schema.safeParse(documents)
|
||||
if(!validationResult.success) {
|
||||
return err(new DBError({
|
||||
message: `dexie.js failed to insert many into ${this.table.name}, invalid data`,
|
||||
parameters: documents,
|
||||
zodError: validationResult.error
|
||||
}))
|
||||
}
|
||||
|
||||
try {
|
||||
const ids = await this.table.bulkAdd(documents as any);
|
||||
return ok(ids);
|
||||
} catch (_error) {
|
||||
const error: IDexieError = _error
|
||||
return err(new DBError({
|
||||
message: `dexie.js failed to insert into many ${this.table.name}, ${error.message}`,
|
||||
parameters: documents,
|
||||
error: error
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
async update(id: any, updatedDocument: Partial<T>) {
|
||||
|
||||
const dataValidation = this.ZodPartialSchema.safeParse(updatedDocument)
|
||||
|
||||
if(dataValidation.success) {
|
||||
try {
|
||||
const updatedCount = await this.table.update(id, dataValidation.data);
|
||||
return ok(updatedCount);
|
||||
} catch (_error) {
|
||||
const error: IDexieError = _error
|
||||
return err(new DBError({
|
||||
message: `dexie.js Failed to update into ${this.table.name}, ${error.message} `,
|
||||
parameters: {
|
||||
...updatedDocument,
|
||||
[this.table.schema.primKey.name]: id
|
||||
} as unknown as T,
|
||||
error: error
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
return err(new DBError({
|
||||
message: `dexie.js failed to update into ${this.table.name}, invalid data`,
|
||||
parameters: {
|
||||
...updatedDocument,
|
||||
[this.table.schema.primKey.name]: id
|
||||
} as unknown as T,
|
||||
zodError: dataValidation.error
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
async updateMany(updatedDocument: Partial<T>[]) {
|
||||
|
||||
const schema = this.ZodSchema.array()
|
||||
const dataValidation = schema.safeParse(updatedDocument)
|
||||
|
||||
if(dataValidation.success) {
|
||||
try {
|
||||
const updatedCount = await this.table.bulkPut(dataValidation.data);
|
||||
return ok(updatedCount);
|
||||
} catch (_error) {
|
||||
const error: IDexieError = _error
|
||||
return err(new DBError({
|
||||
message: `dexie.js Failed to update into ${this.table.name}, ${error.message} `,
|
||||
parameters: document,
|
||||
error: error
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
return err(new DBError({
|
||||
message: `dexie.js failed to update many into ${this.table.name}, invalid data`,
|
||||
parameters: updatedDocument ,
|
||||
zodError: dataValidation.error
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async delete(id: any): Promise<RepositoryResult<void, T>> {
|
||||
try {
|
||||
await this.table.delete(id);
|
||||
return ok(undefined);
|
||||
} catch (_error) {
|
||||
const error: IDexieError = _error
|
||||
return err(new DBError({
|
||||
message: `dexie.js Failed to delete into ${this.table.name}, ${error.message} `,
|
||||
parameters: id,
|
||||
error: error
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: any) {
|
||||
try {
|
||||
const document = await this.table.get(id);
|
||||
return ok(document);
|
||||
} catch (_error) {
|
||||
const error: IDexieError = _error
|
||||
return err(new DBError({
|
||||
message: `dexie.js Failed to delete into ${this.table.name}, ${error.message} `,
|
||||
parameters: id,
|
||||
error: error
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
async find(filter: Partial<T>): Promise<RepositoryResult<R[], T[]>> {
|
||||
try {
|
||||
const documents: any = await this.table.where(filter).toArray();
|
||||
return ok(documents);
|
||||
} catch (_error) {
|
||||
const error: IDexieError = _error;
|
||||
return err(new DBError({
|
||||
message: `dexie.js Failed to find into ${this.table.name}, ${error.message} `,
|
||||
parameters: filter as any,
|
||||
error: error
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
async findOne(filter: Partial<T>): Promise<RepositoryResult<T | undefined, T>> {
|
||||
try {
|
||||
const document = await this.table.where(filter).first();
|
||||
return ok(document);
|
||||
} catch (_error) {
|
||||
const error: IDexieError = _error
|
||||
return err(new DBError({
|
||||
message: `dexie.js Failed to findOne into ${this.table.name}, ${error.message} `,
|
||||
parameters: filter as any,
|
||||
error: error
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
async findAll(): Promise<RepositoryResult<T[], T>> {
|
||||
try {
|
||||
const documents = await this.table.toArray()
|
||||
return ok(documents);
|
||||
} catch (_error) {
|
||||
const error: IDexieError = _error
|
||||
return err(new DBError({
|
||||
message: `dexie.js Failed to findAll into ${this.table.name}, ${error.message} `,
|
||||
parameters: null,
|
||||
error: error
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
async count(filter?: Object): Promise<RepositoryResult<number, T>> {
|
||||
try {
|
||||
const count = filter ? await this.table.where(filter).count() : await this.table.count();
|
||||
return ok(count);
|
||||
} catch (_error) {
|
||||
const error: IDexieError = _error
|
||||
return err(new DBError({
|
||||
message: `dexie.js Failed to count into ${this.table.name}, ${error.message}`,
|
||||
parameters: filter as any,
|
||||
error: error
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './adapter';
|
||||
// export * from './types';
|
||||
@@ -0,0 +1,56 @@
|
||||
// export type UpdatedModel = {
|
||||
// matchedCount: number;
|
||||
// modifiedCount: number;
|
||||
// acknowledged: boolean;
|
||||
// upsertedId: unknown | any;
|
||||
// upsertedCount: number;
|
||||
// };
|
||||
|
||||
import { ZodError } from "zod"
|
||||
|
||||
// export type RemovedModel = {
|
||||
// deletedCount: number;
|
||||
// deleted: boolean;
|
||||
// };
|
||||
|
||||
// export type CreatedModel = {
|
||||
// id: string;
|
||||
// created: boolean;
|
||||
// };
|
||||
|
||||
// export type CreatedOrUpdateModel = {
|
||||
// id: string;
|
||||
// created: boolean;
|
||||
// updated: boolean;
|
||||
// };
|
||||
|
||||
// export enum DatabaseOperationEnum {
|
||||
// EQUAL = 'equal',
|
||||
// NOT_EQUAL = 'not_equal',
|
||||
// NOT_CONTAINS = 'not_contains',
|
||||
// CONTAINS = 'contains'
|
||||
// }
|
||||
|
||||
// export type DatabaseOperationCommand<T> = {
|
||||
// property: keyof T;
|
||||
// value: unknown[];
|
||||
// command: DatabaseOperationEnum;
|
||||
// };
|
||||
|
||||
export type IDBError<T> = {
|
||||
message: string,
|
||||
zodError?: ZodError<T>,
|
||||
parameters?: T,
|
||||
error?: IDexieError
|
||||
}
|
||||
|
||||
export interface IDexieError extends Error {
|
||||
name: string; // The name of the error, e.g., 'NotFoundError', 'ConstraintError'
|
||||
message: string; // The error message
|
||||
stack?: string; // Optional stack trace
|
||||
inner?: Error; // Some Dexie errors have an inner error
|
||||
dbName?: string; // The name of the Dexie database where the error occurred
|
||||
tableName?: string; // The name of the table where the error occurred
|
||||
operation?: string; // The operation being performed (e.g., 'put', 'get', 'delete')
|
||||
key?: any; // The key involved in the operation, if applicable
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Observable, BehaviorSubject } from 'rxjs';
|
||||
import { Result } from 'neverthrow';
|
||||
import { HubConnection } from '@microsoft/signalr';
|
||||
import { ISignalRInput } from './type';
|
||||
|
||||
export abstract class ISignalRService {
|
||||
abstract establishConnection(): Promise<Result<HubConnection, false>>;
|
||||
abstract sendData<T>(input: ISignalRInput): Promise<void>;
|
||||
abstract join(): void;
|
||||
abstract getData<T>(): Observable<{ method: string; data: T }>;
|
||||
abstract getConnectionState(): Observable<boolean>;
|
||||
abstract onReconnect(): Observable<boolean>;
|
||||
abstract newConnection(): void;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable, Subject, timer } from 'rxjs';
|
||||
import { Platform } from '@ionic/angular';
|
||||
import { SignalRConnection, SocketMessage } from './signalR';
|
||||
import { Plugins } from '@capacitor/core';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import { Result } from 'neverthrow';
|
||||
import { HubConnection } from '@microsoft/signalr';
|
||||
import { ISignalRInput } from '../type';
|
||||
|
||||
const { App } = Plugins;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SignalRService {
|
||||
private connection!: SignalRConnection;
|
||||
private connectingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(null);
|
||||
private sendDataSubject: BehaviorSubject<{method: string, data: any}> = new BehaviorSubject<{method: string, data: any}>(null);
|
||||
|
||||
private deadConnectionBackGround: Subject<any>;
|
||||
|
||||
constructor(private platform: Platform) {
|
||||
|
||||
this.deadConnectionBackGround = new Subject()
|
||||
this.deadConnectionBackGround.pipe(
|
||||
switchMap(() => timer(150000)), // 2 minutes 30 seconds
|
||||
).subscribe(() => {
|
||||
console.log('trigger new connections')
|
||||
this.newConnection()
|
||||
})
|
||||
|
||||
try {
|
||||
if (!this.platform.is('desktop')) {
|
||||
App.addListener('appStateChange', ({ isActive }) => {
|
||||
if (isActive) {
|
||||
// The app is in the foreground.
|
||||
// console.log('App is in the foreground');
|
||||
this.deadConnectionBackGround.next()
|
||||
|
||||
}
|
||||
});
|
||||
} else {
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
this.deadConnectionBackGround.next()
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch(error) {}
|
||||
}
|
||||
|
||||
async establishConnection(): Promise<Result<HubConnection, false>> {
|
||||
|
||||
// const connection = new SignalRConnection({url:'https://41e3-41-63-166-54.ngrok-free.app/api/v2/chathub'})
|
||||
const connection = new SignalRConnection({url:'https://gdapi-dev.dyndns.info/stage/api/v2/chathub'})
|
||||
const attempConnection = await connection.establishConnection()
|
||||
|
||||
if(attempConnection.isOk()) {
|
||||
console.log('connect')
|
||||
this.connection?.closeConnection()
|
||||
this.connection = connection
|
||||
|
||||
this.connection.getData().subscribe((data) => {
|
||||
this.sendDataSubject.next(data)
|
||||
this.deadConnectionBackGround.next()
|
||||
})
|
||||
|
||||
this.connection.getConnectionState().subscribe((data) => {
|
||||
this.connectingSubject.next(data)
|
||||
})
|
||||
|
||||
return attempConnection
|
||||
} else {
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(this.establishConnection())
|
||||
}, 2000)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sendData<T>(input: ISignalRInput) {
|
||||
return this.connection.sendData<T>(input)
|
||||
}
|
||||
|
||||
join() {
|
||||
return this.connection.join()
|
||||
}
|
||||
|
||||
getData<T>() {
|
||||
return this.sendDataSubject.asObservable() as BehaviorSubject<{method: string, data: T}>
|
||||
}
|
||||
|
||||
public getConnectionState(): Observable<boolean> {
|
||||
return this.connectingSubject.asObservable();
|
||||
}
|
||||
|
||||
newConnection() {
|
||||
this.establishConnection()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
import * as signalR from '@microsoft/signalr';
|
||||
import { BehaviorSubject, Observable, race, timer } from 'rxjs';
|
||||
import { ok, Result, err } from 'neverthrow';
|
||||
import { SessionStore } from 'src/app/store/session.service';
|
||||
import { filter, first } from 'rxjs/operators';
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { ISignalRInput } from '../type';
|
||||
import { MessageOutPutDataDTO } from 'src/app/core/chat/repository/dto/messageOutputDTO';
|
||||
|
||||
export interface SocketMessage<T> {
|
||||
method: string,
|
||||
data: T
|
||||
}
|
||||
|
||||
export enum EnumSocketError {
|
||||
catch = 1,
|
||||
close
|
||||
}
|
||||
|
||||
export class SignalRConnection {
|
||||
|
||||
private hubConnection: signalR.HubConnection;
|
||||
private connectionStateSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
private disconnectSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
private reconnectSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
private sendLaterSubject: BehaviorSubject<Object> = new BehaviorSubject<Object>(false);
|
||||
private reconnect = true
|
||||
|
||||
private sendDataSubject: BehaviorSubject<{method: string, data: any}> = new BehaviorSubject<{method: string, data: any}>(null);
|
||||
private pendingRequests: Map<string, { resolve: Function; reject: Function }> = new Map();
|
||||
url: string
|
||||
private hasConnectOnce = false
|
||||
|
||||
constructor({url}) {
|
||||
this.url = url
|
||||
}
|
||||
|
||||
establishConnection(): Promise<Result<signalR.HubConnection, false>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
const hubConnection = new signalR.HubConnectionBuilder()
|
||||
.withUrl(this.url)
|
||||
.build();
|
||||
|
||||
this.hubConnection = hubConnection
|
||||
|
||||
hubConnection
|
||||
.start()
|
||||
.then(() => {
|
||||
this.hubConnection = hubConnection
|
||||
this.hasConnectOnce = true
|
||||
console.log('Connection started');
|
||||
this.connectionStateSubject.next(true);
|
||||
this.join()
|
||||
this.addMessageListener()
|
||||
resolve(ok(hubConnection))
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('Error while starting connection: ' + error);
|
||||
if(this.hasConnectOnce) {
|
||||
setTimeout(()=> {
|
||||
resolve(this.attempReconnect());
|
||||
}, 2000)
|
||||
} else {
|
||||
resolve(err(false))
|
||||
}
|
||||
});
|
||||
|
||||
hubConnection.onclose(() => {
|
||||
console.log('Connection closed');
|
||||
this.connectionStateSubject.next(false);
|
||||
this.disconnectSubject.next(true)
|
||||
|
||||
this.pendingRequests.forEach((_, requestId) => {
|
||||
const data = this.pendingRequests.get(requestId);
|
||||
|
||||
if(data) {
|
||||
const { reject } = data
|
||||
reject(err({
|
||||
type: EnumSocketError.close
|
||||
}));
|
||||
this.pendingRequests.delete(requestId);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
if(this.reconnect) {
|
||||
resolve(this.attempReconnect());
|
||||
} else {
|
||||
resolve(err(false))
|
||||
}
|
||||
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
async attempReconnect() {
|
||||
const attempConnection = await this.establishConnection()
|
||||
|
||||
if(attempConnection.isOk()) {
|
||||
this.reconnectSubject.next(true)
|
||||
}
|
||||
|
||||
return attempConnection
|
||||
}
|
||||
|
||||
public join() {
|
||||
if(this.connectionStateSubject.value == true) {
|
||||
this.hubConnection.invoke("Join", SessionStore.user.UserId, SessionStore.user.FullName);
|
||||
//this.hubConnection.invoke("Join", 105, "UserFirefox");
|
||||
} else {
|
||||
this.sendLaterSubject.next({method: 'SendMessage', args:["Join", 312, "Daniel"]})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
sendData<T>(input: ISignalRInput): Promise<Result<T, any>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
if(this.connectionStateSubject.value == true) {
|
||||
try {
|
||||
this.pendingRequests.set(input.data.requestId, { resolve, reject: (data: any) => resolve(data) });
|
||||
|
||||
this.hubConnection.invoke(input.method, input.data)
|
||||
|
||||
this.sendDataSubject.pipe(
|
||||
filter((message) => {
|
||||
return input.data.requestId == message?.data.requestId ||
|
||||
input?.data?.roomName == message?.data.roomName && typeof input?.data?.roomName == 'string'
|
||||
|
||||
}),
|
||||
).subscribe(value => {
|
||||
resolve(ok(value.data as unknown as T))
|
||||
// console.log('Received valid value:', value);
|
||||
});
|
||||
|
||||
} catch(error) {
|
||||
resolve(err({
|
||||
type: EnumSocketError.catch
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
this.sendLaterSubject.next({method: 'SendMessage', args: input})
|
||||
return reject(err(false))
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
private addMessageListener(): void {
|
||||
|
||||
const methods = ['ReceiveMessage', 'TypingMessage', 'AvailableUsers',
|
||||
'ReadAt', 'DeleteMessage', 'UpdateMessage', 'GroupAddedMembers',
|
||||
'GroupDeletedMembers', 'UserAddGroup']
|
||||
|
||||
for(const method of methods) {
|
||||
this.hubConnection.on(method, (message: any) => {
|
||||
this.sendDataSubject.next({
|
||||
method: method,
|
||||
data: message
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public getConnectionState(): Observable<boolean> {
|
||||
return this.connectionStateSubject.asObservable();
|
||||
}
|
||||
|
||||
public getDisconnectTrigger(): Observable<boolean> {
|
||||
return this.disconnectSubject.asObservable();
|
||||
}
|
||||
|
||||
public getData() {
|
||||
return this.sendDataSubject.asObservable()
|
||||
}
|
||||
|
||||
public closeConnection(): void {
|
||||
this.reconnect = false
|
||||
if (this.hubConnection) {
|
||||
this.hubConnection
|
||||
.stop()
|
||||
.then(() => {
|
||||
console.log('Connection closed by user');
|
||||
this.connectionStateSubject.next(false);
|
||||
this.pendingRequests.forEach((_, requestId) => {
|
||||
const data = this.pendingRequests.get(requestId);
|
||||
|
||||
if(data) {
|
||||
const { reject } = data
|
||||
reject(err({
|
||||
type: EnumSocketError.close
|
||||
}));
|
||||
this.pendingRequests.delete(requestId);
|
||||
}
|
||||
|
||||
});
|
||||
})
|
||||
.catch(err => console.log('Error while closing connection: ' + err));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>SignalR Client</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/8.0.0/signalr.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>SignalR Client</h1>
|
||||
<div id="messages"></div>
|
||||
<input id="chatbox">
|
||||
<script>
|
||||
var msgObj = {
|
||||
roomId: "882fcb86-4028-4242-bb47-fca0170dcc65",
|
||||
senderId:105,
|
||||
message:"message enviada",
|
||||
messageType:1,
|
||||
canEdit:true,
|
||||
oneShot:false,
|
||||
};
|
||||
|
||||
const connection = new signalR.HubConnectionBuilder()
|
||||
.withAutomaticReconnect()
|
||||
.withUrl("https://gdapi-dev.dyndns.info/stage/api/v2/chathub")
|
||||
.build();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
connection.start().then(function () {
|
||||
connection.invoke("Join", 105, "UserFirefox");
|
||||
document.getElementById("chatbox").addEventListener("keyup", function (event) {
|
||||
if (event.key === "Enter") {
|
||||
msgObj.Message = chatbox.value;
|
||||
connection.invoke("SendMessage", msgObj);
|
||||
event.target.value = "";
|
||||
}
|
||||
});
|
||||
}).catch(function (err) {
|
||||
return console.error(err.toString());
|
||||
});
|
||||
|
||||
connection.on("ReceiveMessage", function (message) {
|
||||
console.log(message);
|
||||
const messages = document.getElementById("messages");
|
||||
messages.innerHTML += `<p>${message.message}</p>`;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,129 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, Subject, BehaviorSubject } from 'rxjs';
|
||||
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
|
||||
import { catchError, retryWhen, tap, delay } from 'rxjs/operators';
|
||||
import { SessionStore } from 'src/app/store/session.service';
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
|
||||
export interface WebSocketMessage {
|
||||
type: string;
|
||||
payload: any;
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
interface WebSocketError {
|
||||
type: string;
|
||||
error: any;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class WebSocketService {
|
||||
private socket$: WebSocketSubject<WebSocketMessage>;
|
||||
private messageSubject$: Subject<WebSocketMessage>;
|
||||
private connectionStatus$: BehaviorSubject<boolean>;
|
||||
private reconnectAttempts = 0;
|
||||
private readonly maxReconnectAttempts = 5;
|
||||
|
||||
callback: {[key: string]: Function} = {}
|
||||
|
||||
constructor() {
|
||||
this.messageSubject$ = new Subject<WebSocketMessage>();
|
||||
this.connectionStatus$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
// this.connect('https://5-180-182-151.cloud-xip.com:85/ws/')
|
||||
|
||||
// this.messages$.subscribe(({payload, requestId, type}) => {
|
||||
// if(this.callback[requestId]) {
|
||||
// this.callback[requestId]({payload, requestId, type})
|
||||
// delete this.callback[requestId]
|
||||
|
||||
|
||||
// }
|
||||
// })
|
||||
}
|
||||
|
||||
public connect(url: string) {
|
||||
this.socket$ = webSocket<WebSocketMessage>(url);
|
||||
|
||||
this.socket$.pipe(
|
||||
tap({
|
||||
|
||||
error: () => {
|
||||
this.connectionStatus$.next(false);
|
||||
}
|
||||
}),
|
||||
retryWhen(errors => errors.pipe(
|
||||
tap(() => {
|
||||
this.reconnectAttempts++;
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
throw new Error('Max reconnect attempts reached');
|
||||
}
|
||||
}),
|
||||
delay(1000)
|
||||
))
|
||||
).subscribe(
|
||||
(message) => {
|
||||
this.messageSubject$.next(message);
|
||||
|
||||
if (!this.connectionStatus$.getValue()) {
|
||||
this.connectionStatus$.next(true);
|
||||
this.reconnectAttempts = 0;
|
||||
|
||||
// Send a message when the connection is established
|
||||
this.sendMessage(SessionStore.user.UserId as any).subscribe();
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
console.error('WebSocket connection error:', err);
|
||||
},
|
||||
() => {
|
||||
console.log('WebSocket connection closed');
|
||||
this.connectionStatus$.next(false);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public sendMessage(message: WebSocketMessage): Observable<any> {
|
||||
return new Observable<void>(observer => {
|
||||
|
||||
|
||||
|
||||
if(typeof message == 'object') {
|
||||
message.requestId = uuidv4()
|
||||
this.socket$.next(message);
|
||||
|
||||
this.callback[message.requestId] = ({payload, requestId})=> {
|
||||
observer.next(payload as any);
|
||||
observer.complete();
|
||||
}
|
||||
|
||||
} else {
|
||||
this.socket$.next(message);
|
||||
observer.next({} as any);
|
||||
observer.complete();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}).pipe(
|
||||
catchError(err => {
|
||||
console.error('Send message error:', err);
|
||||
return new Observable<never>(observer => {
|
||||
observer.error({ type: 'SEND_ERROR', error: err });
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public get messages$(): Observable<WebSocketMessage> {
|
||||
return this.messageSubject$.asObservable();
|
||||
}
|
||||
|
||||
public get connectionStatus(): Observable<boolean> {
|
||||
return this.connectionStatus$.asObservable();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const SignalRInputSchema = z.object({
|
||||
method: z.string(),
|
||||
data: z.object({
|
||||
requestId: z.string(),
|
||||
}).catchall(z.unknown()), // Allows any additional properties with unknown values
|
||||
})
|
||||
|
||||
export type ISignalRInput = z.infer<typeof SignalRInputSchema>;
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { GenericResponse, RecordingData, VoiceRecorder } from 'capacitor-voice-recorder';
|
||||
import { err, ok, Result } from 'neverthrow';
|
||||
|
||||
export enum StartRecordingResultError {
|
||||
NoSpeaker,
|
||||
NeedPermission,
|
||||
alreadyRecording
|
||||
}
|
||||
|
||||
export enum StopRecordingResultError {
|
||||
haventStartYet,
|
||||
NoValue,
|
||||
UnknownError
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SpeakerService {
|
||||
|
||||
|
||||
recording = false;
|
||||
|
||||
constructor() { }
|
||||
|
||||
|
||||
async startRecording(): Promise<Result<true, StartRecordingResultError>> {
|
||||
// Request audio recording permission
|
||||
await VoiceRecorder.requestAudioRecordingPermission();
|
||||
|
||||
// Check if the device can record audio
|
||||
const canRecord = await VoiceRecorder.canDeviceVoiceRecord();
|
||||
if (!canRecord.value) {
|
||||
return err(StartRecordingResultError.NoSpeaker);
|
||||
}
|
||||
|
||||
// Check if the app has permission to record audio
|
||||
const hasPermission = await VoiceRecorder.requestAudioRecordingPermission();
|
||||
if (!hasPermission.value) {
|
||||
return err(StartRecordingResultError.NeedPermission);
|
||||
}
|
||||
|
||||
// Check if recording is already in progress
|
||||
if (this.recording) {
|
||||
return err(StartRecordingResultError.alreadyRecording)
|
||||
}
|
||||
|
||||
// Start recording
|
||||
this.recording = true;
|
||||
VoiceRecorder.startRecording();
|
||||
return ok(true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Example of a poorly structured startRecording method (commented out).
|
||||
*
|
||||
* - This example demonstrates improper chaining of promises and lack of proper error handling.
|
||||
* - Avoid using this approach for better readability and maintainability.
|
||||
*/
|
||||
// bad code example
|
||||
// async startRecording() {
|
||||
// VoiceRecorder.requestAudioRecordingPermission();
|
||||
// if (await VoiceRecorder.canDeviceVoiceRecord().then((result: GenericResponse) => { return result.value })) {
|
||||
// if (await VoiceRecorder.requestAudioRecordingPermission().then((result: GenericResponse) => { return result.value })) {
|
||||
// //if(await this.hasAudioRecordingPermission()){
|
||||
// if (this.recording) {
|
||||
// return;
|
||||
// }
|
||||
// this.recording = true;
|
||||
// VoiceRecorder.startRecording()
|
||||
// //}
|
||||
// }
|
||||
// else {
|
||||
// return err('need Permission');
|
||||
// }
|
||||
// }
|
||||
// else {
|
||||
// return err('no speaker');
|
||||
// }
|
||||
// }
|
||||
|
||||
async stopRecording(): Promise<Result<RecordingData, StopRecordingResultError>> {
|
||||
if (!this.recording) {
|
||||
return err(StopRecordingResultError.haventStartYet);
|
||||
}
|
||||
|
||||
this.recording = false;
|
||||
|
||||
try {
|
||||
const result = await VoiceRecorder.stopRecording();
|
||||
|
||||
if (result.value && result.value.recordDataBase64) {
|
||||
const recordData = result.value.recordDataBase64;
|
||||
return ok(result);
|
||||
} else {
|
||||
return err(StopRecordingResultError.NoValue);
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle any unexpected errors that might occur during stopRecording
|
||||
return err(StopRecordingResultError.UnknownError);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export enum StartRecordingResultError {
|
||||
NoSpeaker,
|
||||
NeedPermission,
|
||||
alreadyRecording
|
||||
}
|
||||
|
||||
export enum StopRecordingResultError {
|
||||
haventStartYet,
|
||||
NoValue,
|
||||
UnknownError
|
||||
}
|
||||
Reference in New Issue
Block a user