Uses storage fallback on incorrect IndexedDB initialization
This commit is contained in:
parent
0532a23219
commit
b267722901
@ -3,6 +3,7 @@ import { Injectable } from '@angular/core';
|
||||
import { LocalStorage } from '@ngx-pwa/local-storage';
|
||||
|
||||
import { OpenSlidesStatusService } from './openslides-status.service';
|
||||
import { StoragelockService } from '../local-storage/storagelock.service';
|
||||
|
||||
/**
|
||||
* Provides an async API to an key-value store using ngx-pwa which is internally
|
||||
@ -16,7 +17,11 @@ export class StorageService {
|
||||
* Constructor to create the StorageService. Needs the localStorage service.
|
||||
* @param localStorage
|
||||
*/
|
||||
public constructor(private localStorage: LocalStorage, private OSStatus: OpenSlidesStatusService) {}
|
||||
public constructor(
|
||||
private localStorage: LocalStorage,
|
||||
private OSStatus: OpenSlidesStatusService,
|
||||
private lock: StoragelockService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Sets the item into the store asynchronously.
|
||||
@ -24,6 +29,8 @@ export class StorageService {
|
||||
* @param item
|
||||
*/
|
||||
public async set(key: string, item: any): Promise<void> {
|
||||
await this.lock.promise;
|
||||
|
||||
this.assertNotHistroyMode();
|
||||
if (item === null || item === undefined) {
|
||||
await this.remove(key); // You cannot do a setItem with null or undefined...
|
||||
@ -44,6 +51,8 @@ export class StorageService {
|
||||
* @returns The requested value to the key
|
||||
*/
|
||||
public async get<T>(key: string): Promise<T> {
|
||||
await this.lock.promise;
|
||||
|
||||
return await this.localStorage.getUnsafeItem<T>(key).toPromise();
|
||||
}
|
||||
|
||||
@ -52,6 +61,8 @@ export class StorageService {
|
||||
* @param key The key to remove the value from
|
||||
*/
|
||||
public async remove(key: string): Promise<void> {
|
||||
await this.lock.promise;
|
||||
|
||||
this.assertNotHistroyMode();
|
||||
if (!(await this.localStorage.removeItem(key).toPromise())) {
|
||||
throw new Error('Could not delete the item.');
|
||||
@ -62,6 +73,8 @@ export class StorageService {
|
||||
* Clear the whole cache
|
||||
*/
|
||||
public async clear(): Promise<void> {
|
||||
await this.lock.promise;
|
||||
|
||||
this.assertNotHistroyMode();
|
||||
if (!(await this.localStorage.clear().toPromise())) {
|
||||
throw new Error('Could not clear the storage.');
|
||||
|
@ -1,13 +1,18 @@
|
||||
import { NgModule, Optional, SkipSelf, Type } from '@angular/core';
|
||||
import { NgModule, Optional, SkipSelf, Type, PLATFORM_ID } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
|
||||
import { LocalDatabase, LOCAL_STORAGE_PREFIX } from '@ngx-pwa/local-storage';
|
||||
|
||||
// Shared Components
|
||||
import { PromptDialogComponent } from '../shared/components/prompt-dialog/prompt-dialog.component';
|
||||
import { ChoiceDialogComponent } from '../shared/components/choice-dialog/choice-dialog.component';
|
||||
import { ProjectionDialogComponent } from 'app/shared/components/projection-dialog/projection-dialog.component';
|
||||
|
||||
import { OperatorService } from './core-services/operator.service';
|
||||
import { OnAfterAppsLoaded } from './onAfterAppsLoaded';
|
||||
import { StoragelockService } from './local-storage/storagelock.service';
|
||||
import { customLocalDatabaseFactory } from './local-storage/custom-indexeddb-database';
|
||||
|
||||
export const ServicesToLoadOnAppsLoaded: Type<OnAfterAppsLoaded>[] = [OperatorService];
|
||||
|
||||
@ -16,7 +21,15 @@ export const ServicesToLoadOnAppsLoaded: Type<OnAfterAppsLoaded>[] = [OperatorSe
|
||||
*/
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
providers: [Title],
|
||||
providers: [
|
||||
Title,
|
||||
// Use our own localStorage wrapper.
|
||||
{
|
||||
provide: LocalDatabase,
|
||||
useFactory: customLocalDatabaseFactory,
|
||||
deps: [PLATFORM_ID, StoragelockService, [new Optional(), LOCAL_STORAGE_PREFIX]]
|
||||
}
|
||||
],
|
||||
entryComponents: [PromptDialogComponent, ChoiceDialogComponent, ProjectionDialogComponent]
|
||||
})
|
||||
export class CoreModule {
|
||||
|
108
client/src/app/core/local-storage/custom-indexeddb-database.ts
Normal file
108
client/src/app/core/local-storage/custom-indexeddb-database.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import { Optional, Inject, Injectable } from '@angular/core';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
|
||||
import {
|
||||
IndexedDBDatabase,
|
||||
LOCAL_STORAGE_PREFIX,
|
||||
LocalStorageDatabase,
|
||||
MockLocalDatabase,
|
||||
LocalDatabase
|
||||
} from '@ngx-pwa/local-storage';
|
||||
import { fromEvent, race, Observable } from 'rxjs';
|
||||
import { first } from 'rxjs/operators';
|
||||
|
||||
import { StoragelockService } from './storagelock.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CustomIndexedDBDatabase extends IndexedDBDatabase {
|
||||
public constructor(
|
||||
private storageLock: StoragelockService,
|
||||
@Optional() @Inject(LOCAL_STORAGE_PREFIX) protected prefix: string | null = null
|
||||
) {
|
||||
super(prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to IndexedDB and creates the object store on first time
|
||||
*/
|
||||
protected connect(prefix: string | null = null): void {
|
||||
let request: IDBOpenDBRequest;
|
||||
|
||||
// Connecting to IndexedDB
|
||||
try {
|
||||
request = indexedDB.open(this.dbName);
|
||||
} catch (error) {
|
||||
// Fallback storage if IndexedDb connection is failing
|
||||
this.setFallback(prefix);
|
||||
return;
|
||||
}
|
||||
|
||||
// Listening the event fired on first connection, creating the object store for local storage
|
||||
(fromEvent(request, 'upgradeneeded') as Observable<Event>).pipe(first()).subscribe(event => {
|
||||
// Getting the database connection
|
||||
const database = (event.target as IDBRequest).result as IDBDatabase;
|
||||
|
||||
// Checking if the object store already exists, to avoid error
|
||||
if (!database.objectStoreNames.contains(this.objectStoreName)) {
|
||||
// Creating the object store for local storage
|
||||
database.createObjectStore(this.objectStoreName);
|
||||
}
|
||||
});
|
||||
|
||||
// Listening the success event and converting to an RxJS Observable
|
||||
const success = fromEvent(request, 'success') as Observable<Event>;
|
||||
|
||||
// Merging success and errors events
|
||||
(race(success, this.toErrorObservable(request, `connection`)) as Observable<Event>).pipe(first()).subscribe(
|
||||
event => {
|
||||
const db = (event.target as IDBRequest).result as IDBDatabase;
|
||||
|
||||
// CUSTOM: If the indexedDB initialization fails, because 'upgradeneeded' didn't fired
|
||||
// the fallback will be used.
|
||||
if (!db.objectStoreNames.contains(this.objectStoreName)) {
|
||||
this.setFallback(prefix);
|
||||
} else {
|
||||
// Storing the database connection for further access
|
||||
this.database.next(db);
|
||||
this.storageLock.OK();
|
||||
}
|
||||
},
|
||||
() => {
|
||||
// Fallback storage if IndexedDb connection is failing
|
||||
this.setFallback(prefix);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// CUSTOM: If the fallback is used, unlock the storage service
|
||||
public setFallback(prefix: string): void {
|
||||
console.log('uses localStorage as IndexedDB fallback!');
|
||||
super.setFallback(prefix);
|
||||
this.storageLock.OK();
|
||||
}
|
||||
}
|
||||
|
||||
export function customLocalDatabaseFactory(
|
||||
platformId: Object,
|
||||
storagelock: StoragelockService,
|
||||
prefix: string | null
|
||||
): LocalDatabase {
|
||||
if (isPlatformBrowser(platformId) && 'indexedDB' in window && indexedDB !== undefined && indexedDB !== null) {
|
||||
// Try with IndexedDB in modern browsers
|
||||
// CUSTOM: Use our own IndexedDB implementation
|
||||
return new CustomIndexedDBDatabase(storagelock, prefix);
|
||||
} else if (
|
||||
isPlatformBrowser(platformId) &&
|
||||
'localStorage' in window &&
|
||||
localStorage !== undefined &&
|
||||
localStorage !== null
|
||||
) {
|
||||
// Try with localStorage in old browsers (IE9)
|
||||
return new LocalStorageDatabase(prefix);
|
||||
} else {
|
||||
// Fake database for server-side rendering (Universal)
|
||||
return new MockLocalDatabase();
|
||||
}
|
||||
}
|
24
client/src/app/core/local-storage/storagelock.service.ts
Normal file
24
client/src/app/core/local-storage/storagelock.service.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Provides a storage lock for waiting to database to be initialized.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class StoragelockService {
|
||||
private lock: Promise<void>;
|
||||
private resolve: () => void;
|
||||
|
||||
public get promise(): Promise<void> {
|
||||
return this.lock;
|
||||
}
|
||||
|
||||
public constructor() {
|
||||
this.lock = new Promise<void>(resolve => (this.resolve = resolve));
|
||||
}
|
||||
|
||||
public OK(): void {
|
||||
this.resolve();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user