191 lines
7.3 KiB
TypeScript
191 lines
7.3 KiB
TypeScript
import { Injectable } from '@angular/core';
|
|
|
|
import { BaseModel } from '../../shared/models/base/base-model';
|
|
import { CollectionStringMapperService } from './collection-string-mapper.service';
|
|
import { DataStoreService, DataStoreUpdateManagerService } from './data-store.service';
|
|
import { WEBSOCKET_ERROR_CODES, WebsocketService } from './websocket.service';
|
|
|
|
interface AutoupdateFormat {
|
|
/**
|
|
* All changed (and created) items as their full/restricted data grouped by their collection.
|
|
*/
|
|
changed: {
|
|
[collectionString: string]: object[];
|
|
};
|
|
|
|
/**
|
|
* All deleted items (by id) grouped by their collection.
|
|
*/
|
|
deleted: {
|
|
[collectionString: string]: number[];
|
|
};
|
|
|
|
/**
|
|
* The lower change id bond for this autoupdate
|
|
*/
|
|
from_change_id: number;
|
|
|
|
/**
|
|
* The upper change id bound for this autoupdate
|
|
*/
|
|
to_change_id: number;
|
|
|
|
/**
|
|
* Flag, if this autoupdate contains all data. If so, the DS needs to be resetted.
|
|
*/
|
|
all_data: boolean;
|
|
}
|
|
|
|
/**
|
|
* Handles the initial update and automatic updates using the {@link WebsocketService}
|
|
* Incoming objects, usually BaseModels, will be saved in the dataStore (`this.DS`)
|
|
* This service usually creates all models
|
|
*/
|
|
@Injectable({
|
|
providedIn: 'root'
|
|
})
|
|
export class AutoupdateService {
|
|
/**
|
|
* Constructor to create the AutoupdateService. Calls the constructor of the parent class.
|
|
* @param websocketService
|
|
* @param DS
|
|
* @param modelMapper
|
|
*/
|
|
public constructor(
|
|
private websocketService: WebsocketService,
|
|
private DS: DataStoreService,
|
|
private modelMapper: CollectionStringMapperService,
|
|
private DSUpdateManager: DataStoreUpdateManagerService
|
|
) {
|
|
this.websocketService.getOberservable<AutoupdateFormat>('autoupdate').subscribe(response => {
|
|
this.storeResponse(response);
|
|
});
|
|
|
|
// Check for too high change id-errors. If this happens, reset the DS and get fresh data.
|
|
this.websocketService.errorResponseObservable.subscribe(error => {
|
|
if (error.code === WEBSOCKET_ERROR_CODES.CHANGE_ID_TOO_HIGH) {
|
|
this.doFullUpdate();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle the answer of incoming data via {@link WebsocketService}.
|
|
*
|
|
* Detects the Class of an incomming model, creates a new empty object and assigns
|
|
* the data to it using the deserialize function. Also models that are flagged as deleted
|
|
* will be removed from the data store.
|
|
*
|
|
* Handles the change ids of all autoupdates.
|
|
*/
|
|
private async storeResponse(autoupdate: AutoupdateFormat): Promise<void> {
|
|
if (autoupdate.all_data) {
|
|
await this.storeAllData(autoupdate);
|
|
} else {
|
|
await this.storePartialAutoupdate(autoupdate);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stores all data from the autoupdate. This means, that the DS is resettet and filled with just the
|
|
* given data from the autoupdate.
|
|
* @param autoupdate The autoupdate
|
|
*/
|
|
private async storeAllData(autoupdate: AutoupdateFormat): Promise<void> {
|
|
let elements: BaseModel[] = [];
|
|
Object.keys(autoupdate.changed).forEach(collection => {
|
|
elements = elements.concat(this.mapObjectsToBaseModels(collection, autoupdate.changed[collection]));
|
|
});
|
|
|
|
const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this.DS);
|
|
await this.DS.set(elements, autoupdate.to_change_id);
|
|
this.DSUpdateManager.commit(updateSlot, autoupdate.to_change_id, true);
|
|
}
|
|
|
|
/**
|
|
* handles a normal autoupdate that is not a full update (all_data=false).
|
|
* @param autoupdate The autoupdate
|
|
*/
|
|
private async storePartialAutoupdate(autoupdate: AutoupdateFormat): Promise<void> {
|
|
const maxChangeId = this.DS.maxChangeId;
|
|
|
|
if (autoupdate.from_change_id <= maxChangeId && autoupdate.to_change_id <= maxChangeId) {
|
|
console.log(`Ignore. Clients change id: ${maxChangeId}`);
|
|
return; // Ignore autoupdates, that lay full behind our changeid.
|
|
}
|
|
|
|
// Normal autoupdate
|
|
if (autoupdate.from_change_id <= maxChangeId + 1 && autoupdate.to_change_id > maxChangeId) {
|
|
const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this.DS);
|
|
|
|
// Delete the removed objects from the DataStore
|
|
for (const collection of Object.keys(autoupdate.deleted)) {
|
|
await this.DS.remove(collection, autoupdate.deleted[collection]);
|
|
}
|
|
|
|
// Add the objects to the DataStore.
|
|
for (const collection of Object.keys(autoupdate.changed)) {
|
|
await this.DS.add(this.mapObjectsToBaseModels(collection, autoupdate.changed[collection]));
|
|
}
|
|
|
|
await this.DS.flushToStorage(autoupdate.to_change_id);
|
|
|
|
this.DSUpdateManager.commit(updateSlot, autoupdate.to_change_id);
|
|
} else {
|
|
// autoupdate fully in the future. we are missing something!
|
|
this.requestChanges();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates baseModels for each plain object. If the collection is not registered,
|
|
* A console error will be issued and an empty list returned.
|
|
*
|
|
* @param collection The collection all models have to be from.
|
|
* @param models All models that should be mapped to BaseModels
|
|
* @returns A list of basemodels constructed from the given models.
|
|
*/
|
|
private mapObjectsToBaseModels(collection: string, models: object[]): BaseModel[] {
|
|
if (this.modelMapper.isCollectionRegistered(collection)) {
|
|
const targetClass = this.modelMapper.getModelConstructor(collection);
|
|
return models.map(model => new targetClass(model));
|
|
} else {
|
|
console.error(`Unregistered collection "${collection}". Ignore it.`);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sends a WebSocket request to the Server with the maxChangeId of the DataStore.
|
|
* The server should return an autoupdate with all new data.
|
|
*/
|
|
public requestChanges(): void {
|
|
const changeId = this.DS.maxChangeId === 0 ? 0 : this.DS.maxChangeId + 1;
|
|
console.log(`requesting changed objects with DS max change id ${changeId}`);
|
|
this.websocketService.send('getElements', { change_id: changeId });
|
|
}
|
|
|
|
/**
|
|
* Does a full update: Requests all data from the server and sets the DS to the fresh data.
|
|
*/
|
|
public async doFullUpdate(): Promise<void> {
|
|
const oldChangeId = this.DS.maxChangeId;
|
|
const response = await this.websocketService.sendAndGetResponse<{}, AutoupdateFormat>('getElements', {});
|
|
|
|
const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this.DS);
|
|
let allModels: BaseModel[] = [];
|
|
for (const collection of Object.keys(response.changed)) {
|
|
if (this.modelMapper.isCollectionRegistered(collection)) {
|
|
allModels = allModels.concat(this.mapObjectsToBaseModels(collection, response.changed[collection]));
|
|
} else {
|
|
console.error(`Unregistered collection "${collection}". Ignore it.`);
|
|
}
|
|
}
|
|
|
|
await this.DS.set(allModels, response.to_change_id);
|
|
this.DSUpdateManager.commit(updateSlot, response.to_change_id, true);
|
|
|
|
console.log(`Full update done from ${oldChangeId} to ${response.to_change_id}`);
|
|
}
|
|
}
|