Merge pull request #3948 from FinnStutzenstein/changeIdClient
ChangeId for the client
This commit is contained in:
commit
85b91e7101
@ -10,7 +10,7 @@
|
||||
"e2e": "ng e2e",
|
||||
"compodoc": "./node_modules/.bin/compodoc --hideGenerator -p src/tsconfig.app.json -n 'OpenSlides Documentation' -d ../Compodoc -s -w -t -o --port",
|
||||
"extract": "ngx-translate-extract -i ./src -o ./src/assets/i18n/{en,de,fr}.json --clean --sort --format-indentation ' ' --format namespaced-json",
|
||||
"format:fix": "pretty-quick --staged"
|
||||
"prettify": "pretty-quick"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError, tap } from 'rxjs/operators';
|
||||
|
||||
import { OperatorService } from 'app/core/services/operator.service';
|
||||
import { OpenSlidesComponent } from '../../openslides.component';
|
||||
import { environment } from 'environments/environment';
|
||||
import { User } from '../../shared/models/users/user';
|
||||
import { OpenSlidesService } from './openslides.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { HttpService } from './http.service';
|
||||
|
||||
/**
|
||||
* The data returned by a post request to the login route.
|
||||
@ -28,13 +27,16 @@ export class AuthService extends OpenSlidesComponent {
|
||||
* Initializes the httpClient and the {@link OperatorService}.
|
||||
*
|
||||
* Calls `super()` from the parent class.
|
||||
* @param http HttpClient
|
||||
* @param operator who is using OpenSlides
|
||||
* @param http HttpService to send requests to the server
|
||||
* @param operator Who is using OpenSlides
|
||||
* @param OpenSlides The openslides service
|
||||
* @param router To navigate
|
||||
*/
|
||||
public constructor(
|
||||
private http: HttpClient,
|
||||
private http: HttpService,
|
||||
private operator: OperatorService,
|
||||
private OpenSlides: OpenSlidesService
|
||||
private OpenSlides: OpenSlidesService,
|
||||
private router: Router
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@ -48,18 +50,16 @@ export class AuthService extends OpenSlidesComponent {
|
||||
*
|
||||
* @param username
|
||||
* @param password
|
||||
* @returns The login response.
|
||||
*/
|
||||
public login(username: string, password: string): Observable<LoginResponse> {
|
||||
public async login(username: string, password: string): Promise<LoginResponse> {
|
||||
const user = {
|
||||
username: username,
|
||||
password: password
|
||||
};
|
||||
return this.http.post<LoginResponse>(environment.urlPrefix + '/users/login/', user).pipe(
|
||||
tap((response: LoginResponse) => {
|
||||
const response = await this.http.post<LoginResponse>(environment.urlPrefix + '/users/login/', user);
|
||||
this.operator.user = new User(response.user);
|
||||
}),
|
||||
catchError(this.handleError())
|
||||
) as Observable<LoginResponse>;
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -68,10 +68,14 @@ export class AuthService extends OpenSlidesComponent {
|
||||
* Will clear the current {@link OperatorService} and
|
||||
* send a `post`-request to `/apps/users/logout/'`
|
||||
*/
|
||||
public logout(): void {
|
||||
public async logout(): Promise<void> {
|
||||
this.operator.user = null;
|
||||
this.http.post<any>(environment.urlPrefix + '/users/logout/', {}).subscribe(() => {
|
||||
try {
|
||||
await this.http.post(environment.urlPrefix + '/users/logout/', {});
|
||||
} catch (e) {
|
||||
// We do nothing on failures. Reboot OpenSlides anyway.
|
||||
}
|
||||
this.router.navigate(['/']);
|
||||
this.OpenSlides.reboot();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import { WebsocketService } from './websocket.service';
|
||||
|
||||
import { CollectionStringModelMapperService } from './collectionStringModelMapper.service';
|
||||
import { DataStoreService } from './data-store.service';
|
||||
import { BaseModel } from '../../shared/models/base/base-model';
|
||||
|
||||
interface AutoupdateFormat {
|
||||
/**
|
||||
@ -22,9 +23,19 @@ interface AutoupdateFormat {
|
||||
};
|
||||
|
||||
/**
|
||||
* The current change id for this autoupdate
|
||||
* The lower change id bond for this autoupdate
|
||||
*/
|
||||
change_id: number;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -41,14 +52,16 @@ export class AutoupdateService extends OpenSlidesComponent {
|
||||
/**
|
||||
* Constructor to create the AutoupdateService. Calls the constructor of the parent class.
|
||||
* @param websocketService
|
||||
* @param DS
|
||||
* @param modelMapper
|
||||
*/
|
||||
public constructor(
|
||||
websocketService: WebsocketService,
|
||||
private websocketService: WebsocketService,
|
||||
private DS: DataStoreService,
|
||||
private modelMapper: CollectionStringModelMapperService
|
||||
) {
|
||||
super();
|
||||
websocketService.getOberservable<AutoupdateFormat>('autoupdate').subscribe(response => {
|
||||
this.websocketService.getOberservable<AutoupdateFormat>('autoupdate').subscribe(response => {
|
||||
this.storeResponse(response);
|
||||
});
|
||||
}
|
||||
@ -56,37 +69,87 @@ export class AutoupdateService extends OpenSlidesComponent {
|
||||
/**
|
||||
* Handle the answer of incoming data via {@link WebsocketService}.
|
||||
*
|
||||
* Bundles the data per action and collection. THis speeds up the caching in the DataStore.
|
||||
*
|
||||
* Detects the Class of an incomming model, creates a new empty object and assigns
|
||||
* the data to it using the deserialize function.
|
||||
* the data to it using the deserialize function. Also models that are flagged as deleted
|
||||
* will be removed from the data store.
|
||||
*
|
||||
* Saves models in DataStore.
|
||||
* Handles the change ids of all autoupdates.
|
||||
*/
|
||||
public storeResponse(autoupdate: AutoupdateFormat): void {
|
||||
// Delete the removed objects from the DataStore
|
||||
Object.keys(autoupdate.deleted).forEach(collection => {
|
||||
this.DS.remove(collection, autoupdate.deleted[collection], autoupdate.change_id);
|
||||
private async storeResponse(autoupdate: AutoupdateFormat): Promise<void> {
|
||||
console.log('got autoupdate', autoupdate);
|
||||
|
||||
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]));
|
||||
});
|
||||
await this.DS.set(elements, autoupdate.to_change_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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');
|
||||
return; // Ignore autoupdates, that lay full behind our changeid.
|
||||
}
|
||||
|
||||
// Normal autoupdate
|
||||
if (autoupdate.from_change_id <= maxChangeId + 1 && autoupdate.to_change_id > maxChangeId) {
|
||||
// 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.
|
||||
Object.keys(autoupdate.changed).forEach(collection => {
|
||||
for (const collection of Object.keys(autoupdate.changed)) {
|
||||
await this.DS.add(this.mapObjectsToBaseModels(collection, autoupdate.changed[collection]));
|
||||
}
|
||||
|
||||
console.log('new max change id', autoupdate.to_change_id);
|
||||
await this.DS.flushToStorage(autoupdate.to_change_id);
|
||||
} else {
|
||||
// autoupdate fully in the future. we are missing something!
|
||||
this.requestChanges();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates baseModels for each plain object
|
||||
* @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[] {
|
||||
const targetClass = this.modelMapper.getModelConstructor(collection);
|
||||
if (!targetClass) {
|
||||
throw new Error(`Unregistered resource ${collection}`);
|
||||
}
|
||||
this.DS.add(autoupdate.changed[collection].map(model => new targetClass(model)), autoupdate.change_id);
|
||||
});
|
||||
return models.map(model => new targetClass(model));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a WebSocket request to the Server with the maxChangeId of the DataStore.
|
||||
* The server should return an autoupdate with all new data.
|
||||
*
|
||||
* TODO: Wait for changeIds to be implemented on the server.
|
||||
*/
|
||||
public requestChanges(): void {
|
||||
console.log('requesting changed objects');
|
||||
// this.websocketService.send('changeIdRequest', this.DS.maxChangeId);
|
||||
console.log('requesting changed objects with DS max change id', this.DS.maxChangeId + 1);
|
||||
this.websocketService.send('getElements', { change_id: this.DS.maxChangeId + 1 });
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +0,0 @@
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { CacheService } from './cache.service';
|
||||
|
||||
describe('WebsocketService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [CacheService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([CacheService], (service: CacheService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
@ -1,151 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { LocalStorage } from '@ngx-pwa/local-storage';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Container objects for the setQueue.
|
||||
*/
|
||||
interface SetContainer {
|
||||
key: string;
|
||||
item: any;
|
||||
callback: (value: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Container objects for the removeQueue.
|
||||
*/
|
||||
interface RemoveContainer {
|
||||
key: string;
|
||||
callback: (value: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides an async API to an key-value store using ngx-pwa which is internally
|
||||
* using IndexedDB or localStorage as a fallback.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CacheService {
|
||||
/**
|
||||
* The queue of waiting set requests. Just one request (with the same key, which is
|
||||
* an often case) at the time can be handeled. The SetContainer encapsulates the key,
|
||||
* item and callback.
|
||||
*/
|
||||
private setQueue: SetContainer[] = [];
|
||||
|
||||
/**
|
||||
* The queue of waiting remove requests. Same reason for the queue es the @name _setQueue.
|
||||
*/
|
||||
private removeQueue: RemoveContainer[] = [];
|
||||
|
||||
/**
|
||||
* Constructor to create the CacheService. Needs the localStorage service.
|
||||
* @param localStorage
|
||||
*/
|
||||
public constructor(private localStorage: LocalStorage) {}
|
||||
|
||||
/**
|
||||
* Sets the item into the store asynchronously.
|
||||
* @param key
|
||||
* @param item
|
||||
* @param callback An optional callback that is called on success
|
||||
*/
|
||||
public set(key: string, item: any, callback?: (value: boolean) => void): void {
|
||||
if (!callback) {
|
||||
callback = () => {};
|
||||
}
|
||||
|
||||
// Put the set request into the queue
|
||||
const queueObj: SetContainer = {
|
||||
key: key,
|
||||
item: item,
|
||||
callback: callback
|
||||
};
|
||||
this.setQueue.unshift(queueObj);
|
||||
|
||||
// If this is the only object, put it into the cache.
|
||||
if (this.setQueue.length === 1) {
|
||||
this.localStorage.setItem(key, item).subscribe(this._setCallback.bind(this), this._error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* gets called, if a set of the first item in the queue was successful.
|
||||
* @param value success
|
||||
*/
|
||||
private _setCallback(success: boolean): void {
|
||||
// Call the callback and remove the object from the queue
|
||||
this.setQueue[0].callback(success);
|
||||
this.setQueue.pop();
|
||||
// If there are objects left, insert the first one into the cache.
|
||||
if (this.setQueue.length > 0) {
|
||||
const queueObj = this.setQueue[0];
|
||||
this.localStorage.setItem(queueObj.key, queueObj.item).subscribe(this._setCallback.bind(this), this._error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get a value from the store. You need to subscribe to the request to retrieve the value.
|
||||
* @param key The key to get the value from
|
||||
*/
|
||||
public get<T>(key: string): Observable<T> {
|
||||
return this.localStorage.getItem<T>(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the key from the store.
|
||||
* @param key The key to remove the value from
|
||||
* @param callback An optional callback that is called on success
|
||||
*/
|
||||
public remove(key: string, callback?: (value: boolean) => void): void {
|
||||
if (!callback) {
|
||||
callback = () => {};
|
||||
}
|
||||
|
||||
// Put the remove request into the queue
|
||||
const queueObj: RemoveContainer = {
|
||||
key: key,
|
||||
callback: callback
|
||||
};
|
||||
this.removeQueue.unshift(queueObj);
|
||||
|
||||
// If this is the only object, remove it from the cache.
|
||||
if (this.removeQueue.length === 1) {
|
||||
this.localStorage.removeItem(key).subscribe(this._removeCallback.bind(this), this._error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* gets called, if a remove of the first item in the queue was successfull.
|
||||
* @param value success
|
||||
*/
|
||||
private _removeCallback(success: boolean): void {
|
||||
// Call the callback and remove the object from the queue
|
||||
this.removeQueue[0].callback(success);
|
||||
this.removeQueue.pop();
|
||||
// If there are objects left, remove the first one from the cache.
|
||||
if (this.removeQueue.length > 0) {
|
||||
const queueObj = this.removeQueue[0];
|
||||
this.localStorage.removeItem(queueObj.key).subscribe(this._removeCallback.bind(this), this._error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the whole cache
|
||||
* @param callback An optional callback that is called on success
|
||||
*/
|
||||
public clear(callback?: (value: boolean) => void): void {
|
||||
if (!callback) {
|
||||
callback = () => {};
|
||||
}
|
||||
this.localStorage.clear().subscribe(callback, this._error);
|
||||
}
|
||||
|
||||
/**
|
||||
* First error catching function.
|
||||
*/
|
||||
private _error(): void {
|
||||
console.error('caching error', arguments);
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
|
||||
import { BaseModel, ModelConstructor } from '../../shared/models/base/base-model';
|
||||
import { CacheService } from './cache.service';
|
||||
import { StorageService } from './storage.service';
|
||||
import { CollectionStringModelMapperService } from './collectionStringModelMapper.service';
|
||||
|
||||
/**
|
||||
@ -64,7 +64,7 @@ export class DataStoreService {
|
||||
* all cases equal!
|
||||
*/
|
||||
private modelStore: ModelStorage = {};
|
||||
private JsonStore: JsonStorage = {};
|
||||
private jsonStore: JsonStorage = {};
|
||||
|
||||
/**
|
||||
* Observable subject for changed models in the datastore.
|
||||
@ -107,42 +107,51 @@ export class DataStoreService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty constructor for dataStore
|
||||
* @param cacheService use CacheService to cache the DataStore.
|
||||
* @param storageService use StorageService to preserve the DataStore.
|
||||
* @param modelMapper
|
||||
*/
|
||||
public constructor(private cacheService: CacheService, private modelMapper: CollectionStringModelMapperService) {}
|
||||
public constructor(
|
||||
private storageService: StorageService,
|
||||
private modelMapper: CollectionStringModelMapperService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Gets the DataStore from cache and instantiate all models out of the serialized version.
|
||||
* @returns The max change id.
|
||||
*/
|
||||
public initFromCache(): Promise<number> {
|
||||
public async initFromStorage(): Promise<number> {
|
||||
// This promise will be resolved with the maximal change id of the cache.
|
||||
return new Promise<number>(resolve => {
|
||||
this.cacheService.get<JsonStorage>(DataStoreService.cachePrefix + 'DS').subscribe((store: JsonStorage) => {
|
||||
if (store != null) {
|
||||
const store = await this.storageService.get<JsonStorage>(DataStoreService.cachePrefix + 'DS');
|
||||
if (store) {
|
||||
console.log('init from storage:', store);
|
||||
// There is a store. Deserialize it
|
||||
this.JsonStore = store;
|
||||
this.modelStore = this.deserializeJsonStore(this.JsonStore);
|
||||
this.jsonStore = store;
|
||||
this.modelStore = this.deserializeJsonStore(this.jsonStore);
|
||||
// Get the maxChangeId from the cache
|
||||
this.cacheService
|
||||
.get<number>(DataStoreService.cachePrefix + 'maxChangeId')
|
||||
.subscribe((maxChangeId: number) => {
|
||||
if (maxChangeId == null) {
|
||||
let maxChangeId = await this.storageService.get<number>(DataStoreService.cachePrefix + 'maxChangeId');
|
||||
if (!maxChangeId) {
|
||||
maxChangeId = 0;
|
||||
}
|
||||
this._maxChangeId = maxChangeId;
|
||||
resolve(maxChangeId);
|
||||
|
||||
// update observers
|
||||
Object.keys(this.modelStore).forEach(collection => {
|
||||
Object.keys(this.modelStore[collection]).forEach(id => {
|
||||
this.changedSubject.next(this.modelStore[collection][id]);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// No store here, so get all data from the server.
|
||||
resolve(0);
|
||||
this.jsonStore = {};
|
||||
this.modelStore = {};
|
||||
this._maxChangeId = 0;
|
||||
}
|
||||
});
|
||||
});
|
||||
return this.maxChangeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialze the given serializedStorage and returns a Storage.
|
||||
* @param serializedStore The store to deserialize
|
||||
* @returns The serialized storage
|
||||
*/
|
||||
private deserializeJsonStore(serializedStore: JsonStorage): ModelStorage {
|
||||
const storage: ModelStorage = {};
|
||||
@ -161,17 +170,21 @@ export class DataStoreService {
|
||||
|
||||
/**
|
||||
* Clears the complete DataStore and Cache.
|
||||
* @param callback
|
||||
*/
|
||||
public clear(callback?: (value: boolean) => void): void {
|
||||
public async clear(): Promise<void> {
|
||||
console.log('DS clear');
|
||||
this.modelStore = {};
|
||||
this.JsonStore = {};
|
||||
this.jsonStore = {};
|
||||
this._maxChangeId = 0;
|
||||
this.cacheService.remove(DataStoreService.cachePrefix + 'DS', () => {
|
||||
this.cacheService.remove(DataStoreService.cachePrefix + 'maxChangeId', callback);
|
||||
});
|
||||
await this.storageService.remove(DataStoreService.cachePrefix + 'DS');
|
||||
await this.storageService.remove(DataStoreService.cachePrefix + 'maxChangeId');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the collection _string_ based on the model given. If a string is given, it's just returned.
|
||||
* @param collectionType Either a Model constructor or a string.
|
||||
* @returns the collection string
|
||||
*/
|
||||
private getCollectionString<T extends BaseModel<T>>(collectionType: ModelConstructor<T> | string): string {
|
||||
if (typeof collectionType === 'string') {
|
||||
return collectionType;
|
||||
@ -262,62 +275,78 @@ export class DataStoreService {
|
||||
* Add one or multiple models to dataStore.
|
||||
*
|
||||
* @param models BaseModels to add to the store
|
||||
* @param changeId The changeId of this update
|
||||
* @example this.DS.add([new User(1)], changeId)
|
||||
* @example this.DS.add([new User(2), new User(3)], changeId)
|
||||
* @param changeId The changeId of this update. If given, the storage will be flushed to the
|
||||
* cache. Else one can call {@method flushToStorage} to do this manually.
|
||||
* @example this.DS.add([new User(1)])
|
||||
* @example this.DS.add([new User(2), new User(3)])
|
||||
* @example this.DS.add(arrayWithUsers, changeId)
|
||||
*/
|
||||
public add(models: BaseModel[], changeId: number): void {
|
||||
public async add(models: BaseModel[], changeId?: number): Promise<void> {
|
||||
models.forEach(model => {
|
||||
const collectionString = model.collectionString;
|
||||
if (this.modelStore[collectionString] === undefined) {
|
||||
this.modelStore[collectionString] = {};
|
||||
const collection = model.collectionString;
|
||||
if (this.modelStore[collection] === undefined) {
|
||||
this.modelStore[collection] = {};
|
||||
}
|
||||
this.modelStore[collectionString][model.id] = model;
|
||||
this.modelStore[collection][model.id] = model;
|
||||
|
||||
if (this.JsonStore[collectionString] === undefined) {
|
||||
this.JsonStore[collectionString] = {};
|
||||
if (this.jsonStore[collection] === undefined) {
|
||||
this.jsonStore[collection] = {};
|
||||
}
|
||||
this.JsonStore[collectionString][model.id] = JSON.stringify(model);
|
||||
this.jsonStore[collection][model.id] = JSON.stringify(model);
|
||||
this.changedSubject.next(model);
|
||||
});
|
||||
this.storeToCache(changeId);
|
||||
if (changeId) {
|
||||
await this.flushToStorage(changeId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* removes one or multiple models from dataStore.
|
||||
*
|
||||
* @param Type The desired BaseModel type to be read from the datastore
|
||||
* @param collectionString The desired BaseModel type to be removed from the datastore
|
||||
* @param ids A list of IDs of BaseModels to remove from the datastore
|
||||
* @param changeId The changeId of this update
|
||||
* @example this.DS.remove('users/user', [myUser.id, 3, 4], 38213)
|
||||
* @param changeId The changeId of this update. If given, the storage will be flushed to the
|
||||
* cache. Else one can call {@method flushToStorage} to do this manually.
|
||||
* @example this.DS.remove('users/user', [myUser.id, 3, 4])
|
||||
*/
|
||||
public remove(collectionString: string, ids: number[], changeId: number): void {
|
||||
public async remove(collectionString: string, ids: number[], changeId?: number): Promise<void> {
|
||||
ids.forEach(id => {
|
||||
if (this.modelStore[collectionString]) {
|
||||
delete this.modelStore[collectionString][id];
|
||||
}
|
||||
if (this.JsonStore[collectionString]) {
|
||||
delete this.JsonStore[collectionString][id];
|
||||
if (this.jsonStore[collectionString]) {
|
||||
delete this.jsonStore[collectionString][id];
|
||||
}
|
||||
this.deletedSubject.next({
|
||||
collection: collectionString,
|
||||
id: id
|
||||
});
|
||||
});
|
||||
this.storeToCache(changeId);
|
||||
if (changeId) {
|
||||
await this.flushToStorage(changeId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the DataStore and set the given models as the new content.
|
||||
* @param models A list of models to set the DataStore to.
|
||||
* @param newMaxChangeId Optional. If given, the max change id will be updated.
|
||||
*/
|
||||
public async set(models: BaseModel[], newMaxChangeId?: number): Promise<void> {
|
||||
this.modelStore = {};
|
||||
this.jsonStore = {};
|
||||
await this.add(models, newMaxChangeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the cache by inserting the serialized DataStore. Also changes the chageId, if it's larger
|
||||
* @param changeId The changeId from the update. If it's the highest change id seen, it will be set into the cache.
|
||||
*/
|
||||
private storeToCache(changeId: number): void {
|
||||
this.cacheService.set(DataStoreService.cachePrefix + 'DS', this.JsonStore);
|
||||
if (changeId > this._maxChangeId) {
|
||||
public async flushToStorage(changeId: number): Promise<void> {
|
||||
console.log('flush to storage');
|
||||
this._maxChangeId = changeId;
|
||||
this.cacheService.set(DataStoreService.cachePrefix + 'maxChangeId', changeId);
|
||||
}
|
||||
await this.storageService.set(DataStoreService.cachePrefix + 'DS', this.jsonStore);
|
||||
await this.storageService.set(DataStoreService.cachePrefix + 'maxChangeId', changeId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
17
client/src/app/core/services/offline.service.spec.ts
Normal file
17
client/src/app/core/services/offline.service.spec.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { OfflineService } from './offline.service';
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
describe('OfflineService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [OfflineService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([OfflineService], (service: OfflineService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
68
client/src/app/core/services/offline.service.ts
Normal file
68
client/src/app/core/services/offline.service.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { OpenSlidesComponent } from 'app/openslides.component';
|
||||
import { DataStoreService } from './data-store.service';
|
||||
import { WhoAmIResponse } from './operator.service';
|
||||
|
||||
/**
|
||||
* This service handles everything connected with being offline.
|
||||
*
|
||||
* TODO: This is just a stub. Needs to be done in the future; Maybe we cancel this whole concept
|
||||
* of this service. We'll see whats happens here..
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class OfflineService extends OpenSlidesComponent {
|
||||
private _offline = false;
|
||||
|
||||
public get offline(): boolean {
|
||||
return this._offline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor to create the AutoupdateService. Calls the constructor of the parent class.
|
||||
* @param DS
|
||||
*/
|
||||
public constructor(private DS: DataStoreService) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the offline flag. Restores the DataStoreService to the last known configuration.
|
||||
*/
|
||||
public async goOfflineBecauseFailedWhoAmI(): Promise<void> {
|
||||
this._offline = true;
|
||||
console.log('offline because whoami failed.');
|
||||
|
||||
// TODO: Init the DS from cache.
|
||||
await this.DS.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Should be somehow connected to the websocket service.
|
||||
*/
|
||||
public goOfflineBecauseConnectionLost(): void {
|
||||
this._offline = true;
|
||||
console.log('offline because connection lost.');
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Should be somehow connected to the websocket service.
|
||||
*/
|
||||
public goOnline(): void {
|
||||
this._offline = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last cached WhoAmI response.
|
||||
*/
|
||||
public getLastWhoAmI(): WhoAmIResponse {
|
||||
// TODO: use a cached WhoAmI response.
|
||||
return {
|
||||
user_id: null,
|
||||
guest_enabled: false,
|
||||
user: null
|
||||
};
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ import { Router } from '@angular/router';
|
||||
import { OpenSlidesComponent } from 'app/openslides.component';
|
||||
import { WebsocketService } from './websocket.service';
|
||||
import { OperatorService } from './operator.service';
|
||||
import { CacheService } from './cache.service';
|
||||
import { StorageService } from './storage.service';
|
||||
import { AutoupdateService } from './autoupdate.service';
|
||||
import { DataStoreService } from './data-store.service';
|
||||
|
||||
@ -21,15 +21,16 @@ export class OpenSlidesService extends OpenSlidesComponent {
|
||||
public redirectUrl: string;
|
||||
|
||||
/**
|
||||
* Constructor to create the NotifyService. Registers itself to the WebsocketService.
|
||||
* @param cacheService
|
||||
* Constructor to create the OpenSlidesService. Registers itself to the WebsocketService.
|
||||
* @param storageService
|
||||
* @param operator
|
||||
* @param websocketService
|
||||
* @param router
|
||||
* @param autoupdateService
|
||||
* @param DS
|
||||
*/
|
||||
public constructor(
|
||||
private cacheService: CacheService,
|
||||
private storageService: StorageService,
|
||||
private operator: OperatorService,
|
||||
private websocketService: WebsocketService,
|
||||
private router: Router,
|
||||
@ -51,54 +52,51 @@ export class OpenSlidesService extends OpenSlidesComponent {
|
||||
* the bootup-sequence: Do a whoami request and if it was successful, do
|
||||
* {@method afterLoginBootup}. If not, redirect the user to the login page.
|
||||
*/
|
||||
public bootup(): void {
|
||||
public async bootup(): Promise<void> {
|
||||
// start autoupdate if the user is logged in:
|
||||
this.operator.whoAmI().subscribe(resp => {
|
||||
this.operator.guestsEnabled = resp.guest_enabled;
|
||||
if (!resp.user && !resp.guest_enabled) {
|
||||
const response = await this.operator.whoAmI();
|
||||
this.operator.guestsEnabled = response.guest_enabled;
|
||||
if (!response.user && !response.guest_enabled) {
|
||||
this.redirectUrl = location.pathname;
|
||||
// Goto login, if the user isn't login and guests are not allowed
|
||||
this.router.navigate(['/login']);
|
||||
} else {
|
||||
this.afterLoginBootup(resp.user_id);
|
||||
await this.afterLoginBootup(response.user_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* the login bootup-sequence: Check (and maybe clear) the cache und setup the DataStore
|
||||
* and websocket.
|
||||
* and websocket. This "login" also may be the "login" of an anonymous when he is using
|
||||
* OpenSlides as a guest.
|
||||
* @param userId
|
||||
*/
|
||||
public afterLoginBootup(userId: number): void {
|
||||
public async afterLoginBootup(userId: number): Promise<void> {
|
||||
// Else, check, which user was logged in last time
|
||||
this.cacheService.get<number>('lastUserLoggedIn').subscribe((id: number) => {
|
||||
// if the user id changed, reset the cache.
|
||||
if (userId !== id) {
|
||||
this.DS.clear((value: boolean) => {
|
||||
this.setupDataStoreAndWebSocket();
|
||||
});
|
||||
this.cacheService.set('lastUserLoggedIn', userId);
|
||||
} else {
|
||||
this.setupDataStoreAndWebSocket();
|
||||
const lastUserId = await this.storageService.get<number>('lastUserLoggedIn');
|
||||
console.log('user transition:', lastUserId, '->', userId);
|
||||
// if the user changed, reset the cache and save the new user.
|
||||
if (userId !== lastUserId) {
|
||||
await this.DS.clear();
|
||||
await this.storageService.set('lastUserLoggedIn', userId);
|
||||
}
|
||||
});
|
||||
await this.setupDataStoreAndWebSocket();
|
||||
}
|
||||
|
||||
/**
|
||||
* Init DS from cache and after this start the websocket service.
|
||||
*/
|
||||
private setupDataStoreAndWebSocket(): void {
|
||||
this.DS.initFromCache().then((changeId: number) => {
|
||||
this.websocketService.connect(
|
||||
false,
|
||||
changeId
|
||||
);
|
||||
});
|
||||
private async setupDataStoreAndWebSocket(): Promise<void> {
|
||||
let changeId = await this.DS.initFromStorage();
|
||||
console.log('change ID on DS setup', changeId);
|
||||
if (changeId > 0) {
|
||||
changeId += 1;
|
||||
}
|
||||
this.websocketService.connect({ changeId: changeId }); // Request changes after changeId.
|
||||
}
|
||||
|
||||
/**
|
||||
* SHuts down OpenSlides. The websocket is closed and the operator is not set.
|
||||
* Shuts OpenSlides down. The websocket is closed and the operator is not set.
|
||||
*/
|
||||
public shutdown(): void {
|
||||
this.websocketService.close();
|
||||
@ -108,32 +106,31 @@ export class OpenSlidesService extends OpenSlidesComponent {
|
||||
/**
|
||||
* Shutdown and bootup.
|
||||
*/
|
||||
public reboot(): void {
|
||||
public async reboot(): Promise<void> {
|
||||
this.shutdown();
|
||||
this.bootup();
|
||||
await this.bootup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the operator is the same as it was before a reconnect.
|
||||
* Verify that the operator is the same as it was before. Should be alled on a reconnect.
|
||||
*/
|
||||
private checkOperator(): void {
|
||||
this.operator.whoAmI().subscribe(resp => {
|
||||
private async checkOperator(): Promise<void> {
|
||||
const response = await this.operator.whoAmI();
|
||||
// User logged off.
|
||||
if (!resp.user && !resp.guest_enabled) {
|
||||
if (!response.user && !response.guest_enabled) {
|
||||
this.shutdown();
|
||||
this.router.navigate(['/login']);
|
||||
} else {
|
||||
if (
|
||||
(this.operator.user && this.operator.user.id !== resp.user_id) ||
|
||||
(!this.operator.user && resp.user_id)
|
||||
(this.operator.user && this.operator.user.id !== response.user_id) ||
|
||||
(!this.operator.user && response.user_id)
|
||||
) {
|
||||
// user changed
|
||||
this.reboot();
|
||||
await this.reboot();
|
||||
} else {
|
||||
// User is still the same, but check for missed autoupdates.
|
||||
this.autoupdateService.requestChanges();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, BehaviorSubject } from 'rxjs';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { tap, catchError } from 'rxjs/operators';
|
||||
import { OpenSlidesComponent } from 'app/openslides.component';
|
||||
import { Group } from 'app/shared/models/users/group';
|
||||
import { User } from '../../shared/models/users/user';
|
||||
import { environment } from 'environments/environment';
|
||||
import { DataStoreService } from './data-store.service';
|
||||
import { OfflineService } from './offline.service';
|
||||
|
||||
/**
|
||||
* Permissions on the client are just strings. This makes clear, that
|
||||
@ -17,7 +17,7 @@ export type Permission = string;
|
||||
/**
|
||||
* Response format of the WHoAMI request.
|
||||
*/
|
||||
interface WhoAmIResponse {
|
||||
export interface WhoAmIResponse {
|
||||
user_id: number;
|
||||
guest_enabled: boolean;
|
||||
user: User;
|
||||
@ -77,9 +77,14 @@ export class OperatorService extends OpenSlidesComponent {
|
||||
private operatorSubject: BehaviorSubject<User> = new BehaviorSubject<User>(null);
|
||||
|
||||
/**
|
||||
* Sets up an observer for watching changes in the DS. If the operator user or groups are changed,
|
||||
* the operator's permissions are updated.
|
||||
*
|
||||
* @param http HttpClient
|
||||
* @param DS
|
||||
* @param offlineService
|
||||
*/
|
||||
public constructor(private http: HttpClient, private DS: DataStoreService) {
|
||||
public constructor(private http: HttpClient, private DS: DataStoreService, private offlineService: OfflineService) {
|
||||
super();
|
||||
|
||||
this.DS.changeObservable.subscribe(newModel => {
|
||||
@ -101,16 +106,21 @@ export class OperatorService extends OpenSlidesComponent {
|
||||
|
||||
/**
|
||||
* Calls `/apps/users/whoami` to find out the real operator.
|
||||
* @returns The response of the WhoAmI request.
|
||||
*/
|
||||
public whoAmI(): Observable<WhoAmIResponse> {
|
||||
return this.http.get<WhoAmIResponse>(environment.urlPrefix + '/users/whoami/').pipe(
|
||||
tap((response: WhoAmIResponse) => {
|
||||
if (response && response.user_id) {
|
||||
public async whoAmI(): Promise<WhoAmIResponse> {
|
||||
try {
|
||||
const response = await this.http.get<WhoAmIResponse>(environment.urlPrefix + '/users/whoami/').toPromise();
|
||||
if (response && response.user) {
|
||||
this.user = new User(response.user);
|
||||
}
|
||||
}),
|
||||
catchError(this.handleError())
|
||||
) as Observable<WhoAmIResponse>;
|
||||
return response;
|
||||
} catch (e) {
|
||||
// TODO: Implement the offline service. Currently a guest-whoami response is returned and
|
||||
// the DS cleared.
|
||||
this.offlineService.goOfflineBecauseFailedWhoAmI();
|
||||
return this.offlineService.getLastWhoAmI();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
15
client/src/app/core/services/storage.service.spec.ts
Normal file
15
client/src/app/core/services/storage.service.spec.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { StorageService } from './storage.service';
|
||||
|
||||
describe('StorageService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [StorageService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([StorageService], (service: StorageService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
61
client/src/app/core/services/storage.service.ts
Normal file
61
client/src/app/core/services/storage.service.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { LocalStorage } from '@ngx-pwa/local-storage';
|
||||
|
||||
/**
|
||||
* Provides an async API to an key-value store using ngx-pwa which is internally
|
||||
* using IndexedDB or localStorage as a fallback.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class StorageService {
|
||||
/**
|
||||
* Constructor to create the StorageService. Needs the localStorage service.
|
||||
* @param localStorage
|
||||
*/
|
||||
public constructor(private localStorage: LocalStorage) {}
|
||||
|
||||
/**
|
||||
* Sets the item into the store asynchronously.
|
||||
* @param key
|
||||
* @param item
|
||||
*/
|
||||
public async set(key: string, item: any): Promise<void> {
|
||||
if (item === null || item === undefined) {
|
||||
await this.remove(key); // You cannot do a setItem with null or undefined...
|
||||
} else {
|
||||
if (!(await this.localStorage.setItem(key, item).toPromise())) {
|
||||
throw new Error('Could not set the item.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get a value from the store. You need to subscribe to the request to retrieve the value.
|
||||
* @param key The key to get the value from
|
||||
* @returns The requested value to the key
|
||||
*/
|
||||
public async get<T>(key: string): Promise<T> {
|
||||
return await this.localStorage.getItem<T>(key).toPromise();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the key from the store.
|
||||
* @param key The key to remove the value from
|
||||
*/
|
||||
public async remove(key: string): Promise<void> {
|
||||
if (!(await this.localStorage.removeItem(key).toPromise())) {
|
||||
throw new Error('Could not delete the item.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the whole cache
|
||||
*/
|
||||
public async clear(): Promise<void> {
|
||||
console.log('clear storage');
|
||||
if (!(await this.localStorage.clear().toPromise())) {
|
||||
throw new Error('Could not clear the storage.');
|
||||
}
|
||||
}
|
||||
}
|
@ -69,9 +69,17 @@ export class WebsocketService {
|
||||
*/
|
||||
private subjects: { [type: string]: Subject<any> } = {};
|
||||
|
||||
/**
|
||||
* Saves, if the service is in retry mode to get a connection to a previos connection lost.
|
||||
*/
|
||||
private retry = false;
|
||||
|
||||
/**
|
||||
* Constructor that handles the router
|
||||
* @param router the URL Router
|
||||
* @param matSnackBar
|
||||
* @param zone
|
||||
* @param translate
|
||||
*/
|
||||
public constructor(
|
||||
private router: Router,
|
||||
@ -85,34 +93,46 @@ export class WebsocketService {
|
||||
*
|
||||
* Uses NgZone to let all callbacks run in the angular context.
|
||||
*/
|
||||
public connect(retry: boolean = false, changeId?: number): void {
|
||||
public connect(
|
||||
options: {
|
||||
changeId?: number;
|
||||
enableAutoupdates?: boolean;
|
||||
} = {}
|
||||
): void {
|
||||
if (this.websocket) {
|
||||
return;
|
||||
this.close();
|
||||
}
|
||||
|
||||
// set defaults
|
||||
options = Object.assign(options, {
|
||||
enableAutoupdates: true
|
||||
});
|
||||
|
||||
const queryParams: QueryParams = {
|
||||
'change_id': 0,
|
||||
'autoupdate': true,
|
||||
autoupdate: options.enableAutoupdates
|
||||
};
|
||||
// comment-in if changes IDs are supported on server side.
|
||||
/*if (changeId !== undefined) {
|
||||
queryParams.changeId = changeId.toString();
|
||||
}*/
|
||||
|
||||
if (options.changeId !== undefined) {
|
||||
queryParams.change_id = options.changeId;
|
||||
}
|
||||
|
||||
// Create the websocket
|
||||
const socketProtocol = this.getWebSocketProtocol();
|
||||
const socketServer = window.location.hostname + ':' + window.location.port;
|
||||
const socketPath = this.getWebSocketPath(queryParams);
|
||||
console.log('connect to', socketProtocol + socketServer + socketPath);
|
||||
this.websocket = new WebSocket(socketProtocol + socketServer + socketPath);
|
||||
|
||||
// connection established. If this connect attept was a retry,
|
||||
// The error notice will be removed and the reconnectSubject is published.
|
||||
this.websocket.onopen = (event: Event) => {
|
||||
this.zone.run(() => {
|
||||
if (retry) {
|
||||
if (this.retry) {
|
||||
if (this.connectionErrorNotice) {
|
||||
this.connectionErrorNotice.dismiss();
|
||||
this.connectionErrorNotice = null;
|
||||
}
|
||||
this.retry = false;
|
||||
this._reconnectEvent.emit();
|
||||
}
|
||||
this._connectEvent.emit();
|
||||
@ -151,7 +171,8 @@ export class WebsocketService {
|
||||
// A random retry timeout between 2000 and 5000 ms.
|
||||
const timeout = Math.floor(Math.random() * 3000 + 2000);
|
||||
setTimeout(() => {
|
||||
this.connect((retry = true));
|
||||
this.retry = true;
|
||||
this.connect({ enableAutoupdates: true });
|
||||
}, timeout);
|
||||
}
|
||||
});
|
||||
@ -222,7 +243,9 @@ export class WebsocketService {
|
||||
|
||||
const keys: string[] = Object.keys(queryParams);
|
||||
if (keys.length > 0) {
|
||||
path += '?' + keys
|
||||
path +=
|
||||
'?' +
|
||||
keys
|
||||
.map(key => {
|
||||
return key + '=' + queryParams[key].toString();
|
||||
})
|
||||
|
@ -127,26 +127,27 @@ export class LoginMaskComponent extends BaseComponent implements OnInit, OnDestr
|
||||
*
|
||||
* Send username and password to the {@link AuthService}
|
||||
*/
|
||||
public formLogin(): void {
|
||||
public async formLogin(): Promise<void> {
|
||||
this.loginErrorMsg = '';
|
||||
this.inProcess = true;
|
||||
this.authService.login(this.loginForm.value.username, this.loginForm.value.password).subscribe(res => {
|
||||
try {
|
||||
const res = await this.authService.login(this.loginForm.value.username, this.loginForm.value.password);
|
||||
this.inProcess = false;
|
||||
|
||||
if (res instanceof HttpErrorResponse) {
|
||||
this.loginForm.setErrors({
|
||||
notFound: true
|
||||
});
|
||||
this.loginErrorMsg = res.error.detail;
|
||||
} else {
|
||||
this.OpenSlides.afterLoginBootup(res.user_id);
|
||||
let redirect = this.OpenSlides.redirectUrl ? this.OpenSlides.redirectUrl : '/';
|
||||
if (redirect.includes('login')) {
|
||||
redirect = '/';
|
||||
}
|
||||
this.router.navigate([redirect]);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof HttpErrorResponse) {
|
||||
this.loginForm.setErrors({
|
||||
notFound: true
|
||||
});
|
||||
this.loginErrorMsg = e.error.detail;
|
||||
this.inProcess = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user