Merge pull request #4539 from FinnStutzenstein/localStorageFirefox
Uses storage fallback on incorrect IndexedDB initialization
This commit is contained in:
commit
b38ff3fb96
@ -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.');
|
||||||
|
@ -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 {
|
||||||
|
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