Merge pull request #4539 from FinnStutzenstein/localStorageFirefox

Uses storage fallback on incorrect IndexedDB initialization
This commit is contained in:
Emanuel Schütze 2019-03-25 17:30:41 +01:00 committed by GitHub
commit b38ff3fb96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 161 additions and 3 deletions

View File

@ -3,6 +3,7 @@ import { Injectable } from '@angular/core';
import { LocalStorage } from '@ngx-pwa/local-storage'; import { LocalStorage } from '@ngx-pwa/local-storage';
import { OpenSlidesStatusService } from './openslides-status.service'; 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 * 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. * Constructor to create the StorageService. Needs the localStorage service.
* @param localStorage * @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. * Sets the item into the store asynchronously.
@ -24,6 +29,8 @@ export class StorageService {
* @param item * @param item
*/ */
public async set(key: string, item: any): Promise<void> { public async set(key: string, item: any): Promise<void> {
await this.lock.promise;
this.assertNotHistroyMode(); this.assertNotHistroyMode();
if (item === null || item === undefined) { if (item === null || item === undefined) {
await this.remove(key); // You cannot do a setItem with null or 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 * @returns The requested value to the key
*/ */
public async get<T>(key: string): Promise<T> { public async get<T>(key: string): Promise<T> {
await this.lock.promise;
return await this.localStorage.getUnsafeItem<T>(key).toPromise(); return await this.localStorage.getUnsafeItem<T>(key).toPromise();
} }
@ -52,6 +61,8 @@ export class StorageService {
* @param key The key to remove the value from * @param key The key to remove the value from
*/ */
public async remove(key: string): Promise<void> { public async remove(key: string): Promise<void> {
await this.lock.promise;
this.assertNotHistroyMode(); this.assertNotHistroyMode();
if (!(await this.localStorage.removeItem(key).toPromise())) { if (!(await this.localStorage.removeItem(key).toPromise())) {
throw new Error('Could not delete the item.'); throw new Error('Could not delete the item.');
@ -62,6 +73,8 @@ export class StorageService {
* Clear the whole cache * Clear the whole cache
*/ */
public async clear(): Promise<void> { public async clear(): Promise<void> {
await this.lock.promise;
this.assertNotHistroyMode(); this.assertNotHistroyMode();
if (!(await this.localStorage.clear().toPromise())) { if (!(await this.localStorage.clear().toPromise())) {
throw new Error('Could not clear the storage.'); throw new Error('Could not clear the storage.');

View File

@ -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 { CommonModule } from '@angular/common';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { LocalDatabase, LOCAL_STORAGE_PREFIX } from '@ngx-pwa/local-storage';
// Shared Components // Shared Components
import { PromptDialogComponent } from '../shared/components/prompt-dialog/prompt-dialog.component'; import { PromptDialogComponent } from '../shared/components/prompt-dialog/prompt-dialog.component';
import { ChoiceDialogComponent } from '../shared/components/choice-dialog/choice-dialog.component'; import { ChoiceDialogComponent } from '../shared/components/choice-dialog/choice-dialog.component';
import { ProjectionDialogComponent } from 'app/shared/components/projection-dialog/projection-dialog.component'; import { ProjectionDialogComponent } from 'app/shared/components/projection-dialog/projection-dialog.component';
import { OperatorService } from './core-services/operator.service'; import { OperatorService } from './core-services/operator.service';
import { OnAfterAppsLoaded } from './onAfterAppsLoaded'; 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]; export const ServicesToLoadOnAppsLoaded: Type<OnAfterAppsLoaded>[] = [OperatorService];
@ -16,7 +21,15 @@ export const ServicesToLoadOnAppsLoaded: Type<OnAfterAppsLoaded>[] = [OperatorSe
*/ */
@NgModule({ @NgModule({
imports: [CommonModule], 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] entryComponents: [PromptDialogComponent, ChoiceDialogComponent, ProjectionDialogComponent]
}) })
export class CoreModule { export class CoreModule {

View 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();
}
}

View 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();
}
}