import {Injectable} from '@angular/core'; import {environment} from '../../environments/environment'; import {HttpClient} from '@angular/common/http'; import {BehaviorSubject, Observable} from 'rxjs'; import ApiResponse from '../structures/ApiResponse'; import {MenuItem} from './db/MenuItem'; import {DatabaseService} from './db/database.service'; import {ConnectionService} from 'ng-connection-service'; import {Codium} from './db/Codium'; export class ResourceNotAvailableOfflineError extends Error { constructor(msg = 'This resource is not yet available offline on this device.') { super(msg); } } @Injectable({ providedIn: 'root' }) export class ApiService { protected baseEndpoint: string = environment.backendBase; protected statUrl: string = environment.statUrl; protected versionUrl: string = environment.versionUrl; protected offline = false; public readonly offline$: BehaviorSubject = new BehaviorSubject(false); get isOffline() { return this.offline; } constructor( protected http: HttpClient, protected db: DatabaseService, protected connection: ConnectionService, ) { connection.monitor().subscribe(isConnected => { if ( !isConnected ) { this.makeOffline(); } else { this.makeOnline(); // TODO add checks for server. } }); } public makeOffline() { this.offline = true; this.offline$.next(true); } public makeOnline() { this.offline = false; this.offline$.next(false); } public get(endpoint, params = {}): Observable { return this.request(endpoint, params, 'get'); } public post(endpoint, body = {}): Observable { return this.request(endpoint, body, 'post'); } public request(endpoint, params = {}, method: 'get'|'post' = 'get'): Observable { return this._request(this._build_url(endpoint), params, method); } public stat(): Observable { return new Observable(sub => { (async () => { const statKV = await this.db.getKeyValue('host_stat'); // If offline, look up the last stored stat for information if ( this.isOffline ) { if ( typeof statKV !== 'object' ) { throw new Error('No locally stored host stat found.'); } sub.next(new ApiResponse(statKV.data)); sub.complete(); } // Otherwise, fetch the stat and cache it locally this._request(this.statUrl).subscribe(apiResponse => { statKV.data = {status: apiResponse.status, message: apiResponse.message, data: apiResponse.data}; statKV.save().then(() => { sub.next(statKV.data); sub.complete(); }); }); })(); }); } public version(): Promise { return new Promise(async (res, rej) => { const versionKV = await this.db.getKeyValue('app_version'); // If offline, look up the local app version. if ( this.isOffline ) { if ( versionKV ) { return res(versionKV.data); } else { return rej(new Error('No local app version found.')); } } // Otherwise, look up the app version and store it locally this._request(this.versionUrl).subscribe({ next: async result => { const version = result.data.text.trim(); versionKV.data = version; await versionKV.save(); res(version); }, error: rej, }); }); } protected _request(endpoint, params = {}, method: 'get'|'post' = 'get'): Observable { return new Observable(sub => { let data: any = {} if ( method === 'get' ) { data.params = params; } else { data = params; } this.http[method](endpoint, data).subscribe({ next: (response: any) => { sub.next(new ApiResponse(response)); }, error: (err) => { const response = { status: err.status, message: err.message, data: err.error, }; sub.next(new ApiResponse(response)); }, }); }); } public _build_url(endpoint) { if ( !endpoint.startsWith('/') ) { endpoint = `/${endpoint}`; } return `${this.baseEndpoint.endsWith('/') ? this.baseEndpoint.slice(0, -1) : this.baseEndpoint}${endpoint}`; } public getMenuItems(): Promise { return new Promise(async (res, rej) => { await this.db.createSchemata(); // If offline, fetch the menu from the database if ( this.isOffline ) { const items = await this.db.menuItems.toArray(); const nodes = MenuItem.inflateTree(items as MenuItem[]); return res(nodes); } // Download the latest menu items const tree: any[] = await new Promise(res2 => { this.get('/menu/items').subscribe({ next: async result => { const nodes = result.data as any[]; const items = MenuItem.deflateTree(nodes); // Update the locally stored nodes await this.db.menuItems.clear(); await Promise.all(items.map(item => item.save())); res2(nodes); }, error: rej, }); }); res(tree); }); } public getSessionData(): Promise { return new Promise(async (res, rej) => { const sessionKV = await this.db.getKeyValue('session_data'); // If offline, just return the locally cached session data if ( this.isOffline ) { if ( typeof sessionKV.data !== 'object' ) { return rej(new Error('No locally cached session data found.')); } return res(sessionKV.data); } // Otherwise, fetch the session data from the server and cache it locally this.get('/session').subscribe(async result => { sessionKV.data = result.data; await sessionKV.save(); res(sessionKV.data); }); }); } public saveSessionData(data: any): Promise { return new Promise(async (res, rej) => { // Update the local session data const sessionKV = await this.db.getKeyValue('session_data'); sessionKV.data = data; await sessionKV.save(); // If we're not offline, then update the data on the server if ( !this.isOffline ) { await new Promise(res2 => { this.post('/session', data || {}).subscribe({ next: res2, error: rej, }); }); } res(); }); } public deleteCodium(PageId: string, NodeId: string, CodiumId: string): Promise { return new Promise(async (res, rej) => { const existingLocalCodiums = await this.db.codiums.where({ UUID: CodiumId }).toArray(); const existingLocalCodium = existingLocalCodiums.length > 0 ? existingLocalCodiums[0] as Codium : undefined; if ( this.isOffline ) { if ( existingLocalCodium ) { existingLocalCodium.deleted = true; existingLocalCodium.needsServerUpdate = true; await existingLocalCodium.save(); return res(); } else { return rej(new ResourceNotAvailableOfflineError()); } } this.post(`/code/${PageId}/${NodeId}/delete/${CodiumId}`).subscribe({ next: async result => { if ( existingLocalCodium ) { await this.db.codiums.delete(existingLocalCodium.id); } res(); }, error: rej, }); }); } public saveCodium(PageId: string, NodeId: string, CodiumId: string, data: any): Promise { return new Promise(async (res, rej) => { const existingLocalCodiums = await this.db.codiums.where({ UUID: CodiumId }).toArray(); const existingLocalCodium = existingLocalCodiums.length > 0 ? existingLocalCodiums[0] as Codium : undefined; // If we're offline, update or create the local record if ( this.isOffline ) { if ( existingLocalCodium ) { existingLocalCodium.fillFromRecord(data); existingLocalCodium.needsServerUpdate = true; await existingLocalCodium.save(); return res(existingLocalCodium.getSaveRecord()); } else { const newLocalCodium = new Codium( data.Language, NodeId, PageId, data.code, data.UUID || Codium.getUUID(), true, ); await newLocalCodium.save(); return res(newLocalCodium.getSaveRecord()); } } // If we're online, save the data and update our local records this.post(`/code/${PageId}/${NodeId}/set/${CodiumId}`, data).subscribe({ next: async result => { if ( existingLocalCodium ) { existingLocalCodium.fillFromRecord(result.data); existingLocalCodium.needsServerUpdate = false; await existingLocalCodium.save(); return res(result.data); } else { const newLocalCodium = new Codium( result.data.Language, result.data.NodeId, result.data.PageId, result.data.code, result.data.UUID, ); await newLocalCodium.save(); return res(result.data); } }, error: rej, }); }); } public createCodium(PageId: string, NodeId: string): Promise { return new Promise(async (res, rej) => { // If offline, create a new local DB record if ( this.isOffline ) { const newLocalCodium = new Codium( 'javascript', NodeId, PageId, '', Codium.getUUID(), true ); await newLocalCodium.save(); return res(newLocalCodium.getSaveRecord()); } // If online, create a new record on the server and sync it to the local db this.post(`/code/${PageId}/${NodeId}/create`).subscribe({ next: async result => { const newLocalCodium = new Codium( result.data.Language, result.data.NodeId, result.data.PageId, result.data.code, result.data.UUID, ); await newLocalCodium.save(); res(result.data); }, error: rej, }); }); } public getCodium(PageId: string, NodeId: string, CodiumId: string): Promise { return new Promise(async (res, rej) => { const existingLocalCodiums = await this.db.codiums.where({ UUID: CodiumId }).toArray(); const existingLocalCodium = existingLocalCodiums.length > 0 ? existingLocalCodiums[0] as Codium : undefined; // If offline, try to load it from the local DB if ( this.isOffline ) { if ( existingLocalCodium ) { return res(existingLocalCodium.getSaveRecord()); } else { return rej(new ResourceNotAvailableOfflineError()); } } // If online, fetch the codium and store/update it locally this.get(`/code/${PageId}/${NodeId}/get/${CodiumId}`).subscribe({ next: async result => { if ( existingLocalCodium ) { existingLocalCodium.fillFromRecord(result.data); await existingLocalCodium.save(); return res(result.data); } else { const newLocalCodium = new Codium( result.data.Language, result.data.NodeId, result.data.PageId, result.data.code, result.data.UUID ); await newLocalCodium.save(); return res(result.data); } }, error: rej, }); }); } }