merge chat

This commit is contained in:
Peter Maquiran
2024-09-13 12:27:39 +01:00
471 changed files with 17419 additions and 53900 deletions
@@ -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();
});
});
+37 -1
View File
@@ -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'
+43
View File
@@ -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)
}
}
}
+14
View File
@@ -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>>
}
+17
View File
@@ -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 {}
+128
View File
@@ -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';
}
+17
View File
@@ -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>>
+28
View File
@@ -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
}))
}
}
}
+2
View File
@@ -0,0 +1,2 @@
export * from './adapter';
// export * from './types';
+56
View File
@@ -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
}
View File
+14
View File
@@ -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()
}
}
+207
View File
@@ -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));
}
}
}
+50
View File
@@ -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>
+129
View File
@@ -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();
}
}
+10
View File
@@ -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>;
+105
View File
@@ -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);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
export enum StartRecordingResultError {
NoSpeaker,
NeedPermission,
alreadyRecording
}
export enum StopRecordingResultError {
haventStartYet,
NoValue,
UnknownError
}