diff --git a/CHANGELOG.md b/CHANGELOG.md index 812dda27..a9fbd110 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,7 +49,7 @@ It was time to do a full review and refactoring, which results in: - `.has()`, `.keys()` and `.size` are deprecated in `LocalStorage`. They will be removed in v9. They moved to the new `StorageMap` service. - `JSONSchemaNumeric` deprecated (will be removed in v9) - `LSGetItemsOptions` deprecated (not necessary anymore, will be removed in v9) -- `LOCAL_STORAGE_PREFIX` and `prefix` option of `localStorageProviders()` deprecated (will be removed in v9)) +- `LOCAL_STORAGE_PREFIX` and `localStorageProviders()` deprecated (will be removed in v9). Moved to `StorageModule.forRoot()` - `setItemSubscribe()`, `removeItemSubscribe()` and `clearSubscribe()` deprecated (will be removed in v9) ### Reduced public API diff --git a/README.md b/README.md index a2aefb2f..7d317b77 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ Efficient client-side storage module for Angular apps and Progressive Wep Apps ( - **security**: validate data with a JSON Schema, - **compatibility**: works around some browsers issues, - **documentation**: API fully explained, and a changelog! -- **maintenance**: the lib follows Angular LTS and anticipates the next Angular version, - **reference**: 1st Angular library for client-side storage according to [ngx.tools](https://ngx.tools/#/search?q=local%20storage). ## By the same author @@ -52,6 +51,28 @@ npm install @ngx-pwa/local-storage@next npm install @ngx-pwa/local-storage@6 ``` +*Since version 8*, this second step is: +- not required for the lib to work, +- **strongly recommended for all new applications**, as it allows interoperability +and is future-proof, as it should become the default in a future version, +- **prohibited in applications already using this lib and already deployed in production**, +as it would break with previously stored data. + +```ts +import { StorageModule } from '@ngx-pwa/local-storage'; + +@NgModule({ + imports: [ + StorageModule.forRoot({ + IDBNoWrap: true, + }) + ] +}) +export class AppModule {} +``` + +**Must be done at initialization, ie. in `AppModule`, and must not be loaded again in another module.** + ### Upgrading If you still use the old `angular-async-local-storage` package, or to update to new versions, @@ -105,7 +126,8 @@ export class YourService { ``` New *since version 8* of this lib, this service API follows the -[native `Map` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map), +[native `Map` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) +and the new upcoming standard [kv-storage API](https://github.com/WICG/kv-storage), except it's asynchronous via [RxJS `Observable`s](http://reactivex.io/rxjs/). It does the same thing as the `LocalStorage` service, but also allows more advanced operations. diff --git a/docs/COLLISION.md b/docs/COLLISION.md index 0f9207cf..e82114d0 100644 --- a/docs/COLLISION.md +++ b/docs/COLLISION.md @@ -17,11 +17,11 @@ but is not recommended as there was breaking changes in v8. For example: ```typescript -import { localStorageProviders } from '@ngx-pwa/local-storage'; +import { StorageModule } from '@ngx-pwa/local-storage'; @NgModule({ - providers: [ - localStorageProviders({ + imports: [ + StorageModule.forRoot({ IDBDBName: 'myAppStorage', // custom database name when in `indexedDB` LSPrefix: 'myapp_', // prefix when in `localStorage` fallback }) diff --git a/docs/INTEROPERABILITY.md b/docs/INTEROPERABILITY.md index 2128ecd5..818f4902 100644 --- a/docs/INTEROPERABILITY.md +++ b/docs/INTEROPERABILITY.md @@ -19,36 +19,42 @@ as there are important things to do and to be aware of to achieve interoperabili Interoperability can be achieved: - **since v8 of this lib**, - **only for apps that haven't been deployed in production yet**, -as v8 changed the storage system to achieve interoperability: - - it won't work on data stored with this lib before v8, as it still uses the old storage system for backward compatibility, - - changing configuration on the fly would mean to **lose all previously stored data**. +as v8 uses the following opt-in option to allow interoperability: +changing configuration on the fly would mean to **lose all previously stored data**. -## Configuration +```ts +import { StorageModule } from '@ngx-pwa/local-storage'; + +@NgModule({ + imports: [ + StorageModule.forRoot({ + IDBNoWrap: true, + }) + ] +}) +export class AppModule {} +``` Note: - it is an initialization step, so as mentioned in the examples below, **it must be done in `AppModule`**, - **never change these options in an app already deployed in production, as all previously stored data would be lost**. +## Configuration + ### `indexedDB` database and object store names When storing in `indexedDB`, names are used for the database and the object store, so you will need that all APIs use the same names. -Option 1: keep the config of this lib and change the names in the other APIs, -by using the default values exported by the lib: - -```typescript -import { DEFAULT_IDB_DB_NAME, DEFAULT_IDB_STORE_NAME } from '@ngx-pwa/local-storage'; -``` - -Option 2: change this lib config, according to your other APIs: +- Option 1 (recommended): change this lib config, according to your other APIs: -```typescript -import { localStorageProviders } from '@ngx-pwa/local-storage'; +```ts +import { StorageModule } from '@ngx-pwa/local-storage'; @NgModule({ - providers: [ - localStorageProviders({ + imports: [ + StorageModule.forRoot({ + IDBNoWrap: true, IDBDBName: 'customDataBaseName', IDBStoreName: 'customStoreName', }) @@ -57,19 +63,36 @@ import { localStorageProviders } from '@ngx-pwa/local-storage'; export class AppModule {} ``` +- Option 2: keep the config of this lib and change the options in the other APIs, +by using the values exported by the lib: + +```ts +if (this.storageMap.backingEngine === 'indexedDB') { + const { database, store, version } = this.storageMap.backingStore; +} +``` + +This second option can be difficult to manage due to some browsers issues in some special contexts +(Firefox private mode and Safari cross-origin iframes), +as **the information may be wrong at initialization,** +as the storage could fallback from `indexedDB` to `localStorage` +only after a first read or write operation. + ### `localStorage` prefix In some cases (see the [browser support guide](./BROWSERS_SUPPORT)), `indexedDB` is not available, and libs fallback to `localStorage`. Some libs prefixes `localStorage` keys. This lib doesn't by default, -but you can add a prefix: +but you can add a prefix. + +- Option 1 (recommended): ```typescript -import { localStorageProviders } from '@ngx-pwa/local-storage'; +import { StorageModule } from '@ngx-pwa/local-storage'; @NgModule({ - providers: [ - localStorageProviders({ + imports: [ + StorageModule.forRoot({ LSPrefix: 'myapp_', }) ] @@ -77,16 +100,25 @@ import { localStorageProviders } from '@ngx-pwa/local-storage'; export class AppModule {} ``` +- Option 2: + +```ts +if (this.storageMap.backingEngine === 'localStorage') { + const { prefix } = this.storageMap.fallbackBackingStore; +} +``` + ### Example with `localforage` Interoperability with `localforage` lib can be achieved with this config: ```typescript -import { localStorageProviders } from '@ngx-pwa/local-storage'; +import { StorageModule } from '@ngx-pwa/local-storage'; @NgModule({ - providers: [ - localStorageProviders({ + imports: [ + StorageModule.forRoot({ + IDBNoWrap: true, LSPrefix: 'localforage/', IDBDBName: 'localforage', IDBStoreName: 'keyvaluepairs', @@ -96,6 +128,28 @@ import { localStorageProviders } from '@ngx-pwa/local-storage'; export class AppModule {} ``` +### Example with native `indexedDB` + +Interoperability with native `indexedDB` can be achieved that way: + +```ts +if (this.storageMap.backingEngine === 'indexedDB') { + + const { database, store, version } = this.storageMap.backingStore; + + const dbRequest = indexedDB.open(database, version); + + dbRequest.addEventListener('success', () => { + + const store = dbRequest.result.transaction([store], 'readonly').objectStore(store); + + const request = store.get(index); + + }); + +} +``` + ## Warnings ### `indexedDB` store @@ -107,7 +161,7 @@ or when the version change (but this case doesn't happen in this lib). **If this step is missing, then all `indexedDB` operations in the lib will fail as the store will be missing.** Then, you need to ensure: -- you use the same database `version` as the lib (none or `1`), +- you use the same database `version` as the lib (default to `1`), - the store is created: - by letting this lib to be initialized first (beware of concurrency issues), - or if another API is going first, it needs to take care of the creation of the store (with the same name). diff --git a/docs/MIGRATION_TO_V8.md b/docs/MIGRATION_TO_V8.md index 91d2e2c3..f588c2f0 100644 --- a/docs/MIGRATION_TO_V8.md +++ b/docs/MIGRATION_TO_V8.md @@ -44,19 +44,6 @@ npm install @ngx-pwa/local-storage@next 2. Start your project: problems will be seen at compilation. Or you could search for `getItem` as most breaking changes are about its options. -## New `indexedDB` store - -To allow interoperability, the internal `indexedDB` storing system has changed. -It is not a breaking change as the lib do it in a backward-compatible way: -- when `indexedDB` storage is empty (new app users or data swiped), the new storage is used, -- when `indexedDB` old storage is here, the lib stays on this one. - -So it should not concern you, but as it is very sensitive change, we recommend -**to test previously stored data is not lost before deploying in production**. - -It's internal stuff, but it also means there is a transition phase where some of the users of your app -will be on the new storage, and others will be on the old one. - ## The bad part: breaking changes **The following changes may require action from you**. @@ -343,11 +330,11 @@ export class AppModule {} Since v8: ```typescript -import { localStorageProviders } from '@ngx-pwa/local-storage'; +import { StorageModule } from '@ngx-pwa/local-storage'; @NgModule({ - providers: [ - localStorageProviders({ + imports: [ + StorageModule.forRoot({ LSPrefix: 'myapp_', // Note the underscore IDBDBName: 'myapp_ngStorage', }), diff --git a/projects/demo/src/app/app.module.ts b/projects/demo/src/app/app.module.ts index f6571635..a9dd9e3d 100644 --- a/projects/demo/src/app/app.module.ts +++ b/projects/demo/src/app/app.module.ts @@ -1,16 +1,17 @@ import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; +import { StorageModule } from '@ngx-pwa/local-storage'; import { AppComponent } from './app.component'; @NgModule({ - declarations: [ - AppComponent - ], + declarations: [AppComponent], imports: [ - BrowserModule + BrowserModule, + StorageModule.forRoot({ + IDBNoWrap: true, + }), ], - providers: [], bootstrap: [AppComponent] }) export class AppModule { } diff --git a/projects/ivy/src/app/app.module.ts b/projects/ivy/src/app/app.module.ts index f6571635..1b39ec6c 100644 --- a/projects/ivy/src/app/app.module.ts +++ b/projects/ivy/src/app/app.module.ts @@ -3,10 +3,9 @@ import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; +// TODO: reintroduce `StorageModule.forRoot()` @NgModule({ - declarations: [ - AppComponent - ], + declarations: [AppComponent], imports: [ BrowserModule ], diff --git a/projects/localforage/src/app/app.module.ts b/projects/localforage/src/app/app.module.ts index 961084c1..d7a5e2d2 100644 --- a/projects/localforage/src/app/app.module.ts +++ b/projects/localforage/src/app/app.module.ts @@ -1,6 +1,6 @@ import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; -import { localStorageProviders } from '@ngx-pwa/local-storage'; +import { StorageModule } from '@ngx-pwa/local-storage'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; @@ -14,13 +14,12 @@ import { HomeComponent } from './home/home.component'; imports: [ BrowserModule, AppRoutingModule, - ], - providers: [ - localStorageProviders({ + StorageModule.forRoot({ LSPrefix: 'localforage/', + IDBNoWrap: true, IDBDBName: 'localforage', IDBStoreName: 'keyvaluepairs' - }) + }), ], bootstrap: [AppComponent] }) diff --git a/projects/ngx-pwa/local-storage/src/lib/databases/indexeddb-database.ts b/projects/ngx-pwa/local-storage/src/lib/databases/indexeddb-database.ts index 7760a7d3..b717e6b9 100644 --- a/projects/ngx-pwa/local-storage/src/lib/databases/indexeddb-database.ts +++ b/projects/ngx-pwa/local-storage/src/lib/databases/indexeddb-database.ts @@ -5,8 +5,8 @@ import { map, mergeMap, first, takeWhile, tap } from 'rxjs/operators'; import { LocalDatabase } from './local-database'; import { IDBBrokenError } from './exceptions'; import { - IDB_DB_NAME, IDB_STORE_NAME, DEFAULT_IDB_STORE_NAME, DEFAULT_IDB_STORE_NAME_PRIOR_TO_V8, - LOCAL_STORAGE_PREFIX, DEFAULT_IDB_DB_NAME + IDB_DB_NAME, IDB_STORE_NAME, DEFAULT_IDB_STORE_NAME, IDB_DB_VERSION, LOCAL_STORAGE_PREFIX, + DEFAULT_IDB_DB_NAME, DEFAULT_IDB_DB_VERSION, IDB_NO_WRAP, DEFAULT_IDB_NO_WRAP } from '../tokens'; @Injectable({ @@ -17,60 +17,47 @@ export class IndexedDBDatabase implements LocalDatabase { /** * `indexedDB` database name */ - private readonly dbName: string; + protected readonly dbName: string; /** * `indexedDB` object store name */ - private storeName: string | null = null; + protected readonly storeName: string; /** - * `indexedDB` data path name for local storage (where items' value will be stored) + * `indexedDB` database version. Must be an unsigned **integer** */ - private readonly dataPath = 'value'; + protected readonly dbVersion: number; /** * `indexedDB` database connection, wrapped in a RxJS `ReplaySubject` to be able to access the connection * even after the connection success event happened */ - private database: ReplaySubject; + protected readonly database = new ReplaySubject(1); /** - * Flag to remember if we are using the new or old object store + * Flag to not wrap `indexedDB` values for interoperability or to wrap for backward compatibility. */ - private isStorePriorToV8 = false; + protected readonly noWrap: boolean; /** - * Number of items in our `indexedDB` database and object store + * Index used when wrapping value. *For backward compatibility only.* */ - get size(): Observable { - - /* Open a transaction in read-only mode */ - return this.transaction('readonly').pipe( - mergeMap((store) => { - - /* Request to know the number of items */ - const request = store.count(); - - /* Manage success and error events, and get the result */ - return this.requestEventsAndMapTo(request, () => request.result); - - }), - /* The observable will complete after the first value */ - first(), - ); - - } + protected readonly wrapIndex = 'value'; /** * Constructor params are provided by Angular (but can also be passed manually in tests) * @param dbName `indexedDB` database name * @param storeName `indexedDB` store name - * @param oldPrefix Prefix to avoid collision for multiple apps on the same subdomain + * @param dbVersion `indexedDB` database version + * @param noWrap `indexedDB` database version + * @param oldPrefix Pre-v8 backward compatible prefix */ constructor( @Inject(IDB_DB_NAME) dbName = DEFAULT_IDB_DB_NAME, - @Inject(IDB_STORE_NAME) storeName: string | null = null, + @Inject(IDB_STORE_NAME) storeName = DEFAULT_IDB_STORE_NAME, + @Inject(IDB_DB_VERSION) dbVersion = DEFAULT_IDB_DB_VERSION, + @Inject(IDB_NO_WRAP) noWrap = DEFAULT_IDB_NO_WRAP, // tslint:disable-next-line: deprecation @Inject(LOCAL_STORAGE_PREFIX) oldPrefix = '', ) { @@ -78,17 +65,51 @@ export class IndexedDBDatabase implements LocalDatabase { /* Initialize `indexedDB` database name, with prefix if provided by the user */ this.dbName = oldPrefix ? `${oldPrefix}_${dbName}` : dbName; - /* Initialize `indexedDB` store name */ this.storeName = storeName; - - /* Creating the RxJS ReplaySubject */ - this.database = new ReplaySubject(1); + this.dbVersion = dbVersion; + this.noWrap = noWrap; /* Connect to `indexedDB`, with prefix if provided by the user */ this.connect(); } + /** + * Information about `indexedDB` connection. *Only useful for interoperability.* + * @returns `indexedDB` database name, store name and database version + */ + get backingStore(): { database: string, store: string, version: number } { + + return { + database: this.dbName, + store: this.storeName, + version: this.dbVersion, + }; + + } + + /** + * Number of items in our `indexedDB` database and object store + */ + get size(): Observable { + + /* Open a transaction in read-only mode */ + return this.transaction('readonly').pipe( + mergeMap((store) => { + + /* Request to know the number of items */ + const request = store.count(); + + /* Manage success and error events, and get the result */ + return this.requestEventsAndMapTo(request, () => request.result); + + }), + /* The observable will complete after the first value */ + first(), + ); + + } + /** * Gets an item value in our `indexedDB` store * @param key The item's key @@ -108,15 +129,16 @@ export class IndexedDBDatabase implements LocalDatabase { if ((request.result !== undefined) && (request.result !== null)) { - if (!this.isStorePriorToV8) { + /* Prior to v8, the value was wrapped in an `{ value: ...}` object */ + if (!this.noWrap && (typeof request.result === 'object') && (this.wrapIndex in request.result) && + (request.result[this.wrapIndex] !== undefined) && (request.result[this.wrapIndex] !== null)) { - /* Cast to the wanted type */ - return request.result as T; + return (request.result[this.wrapIndex] as T); - } else if ((request.result[this.dataPath] !== undefined) && (request.result[this.dataPath] !== null)) { + } else { - /* Prior to v8, the value was wrapped in an `{ value: ...}` object */ - return (request.result[this.dataPath] as T); + /* Cast to the wanted type */ + return request.result as T; } @@ -156,26 +178,26 @@ export class IndexedDBDatabase implements LocalDatabase { * In older browsers, the value is checked instead, but it could lead to an exception * if `undefined` was stored outside of this lib (e.g. directly with the native `indexedDB` API). */ - const request1 = this.getKeyRequest(store, key); + const requestGet = this.getKeyRequest(store, key); /* Manage success and error events, and get the request result */ - return this.requestEventsAndMapTo(request1, () => request1.result).pipe( + return this.requestEventsAndMapTo(requestGet, () => requestGet.result).pipe( mergeMap((existingEntry) => { /* It is very important the second request is done from the same transaction/store as the previous one, * otherwise it could lead to concurrency failures * Avoid https://github.com/cyrilletuzi/angular-async-local-storage/issues/47 */ - /* Prior to v8, data was wrapped in a `{ value: ... }` object */ - const dataToStore = !this.isStorePriorToV8 ? data : { [this.dataPath]: data }; + /* Prior to v8, data was wrapped in a `{ value: ... }` object */ + const dataToStore = this.noWrap ? data : { [this.wrapIndex]: data }; /* Add if the item is not existing yet, or update otherwise */ - const request2 = (existingEntry === undefined) ? + const requestSet = (existingEntry === undefined) ? store.add(dataToStore, key) : store.put(dataToStore, key); /* Manage success and error events, and map to `true` */ - return this.requestEventsAndMapTo(request2, () => undefined); + return this.requestEventsAndMapTo(requestSet, () => undefined); }), ); @@ -300,7 +322,7 @@ export class IndexedDBDatabase implements LocalDatabase { /** * Connects to `indexedDB` and creates the object store on first time */ - private connect(): void { + protected connect(): void { let request: IDBOpenDBRequest; @@ -310,7 +332,7 @@ export class IndexedDBDatabase implements LocalDatabase { try { /* Do NOT explicit `window` here, as `indexedDB` could be used from a web worker too */ - request = indexedDB.open(this.dbName); + request = indexedDB.open(this.dbName, this.dbVersion); } catch { @@ -346,7 +368,7 @@ export class IndexedDBDatabase implements LocalDatabase { * Create store on first use of `indexedDB` * @param request `indexedDB` database opening request */ - private createStore(request: IDBOpenDBRequest): void { + protected createStore(request: IDBOpenDBRequest): void { /* Listen to the event fired on first connection */ fromEvent(request, 'upgradeneeded') @@ -354,16 +376,15 @@ export class IndexedDBDatabase implements LocalDatabase { .pipe(first()) .subscribe({ next: () => { - /* Use custom store name if requested, otherwise use the default */ - const storeName = this.storeName || DEFAULT_IDB_STORE_NAME; /* Check if the store already exists, to avoid error */ - if (!request.result.objectStoreNames.contains(storeName)) { + if (!request.result.objectStoreNames.contains(this.storeName)) { + /* Create the object store */ - request.result.createObjectStore(storeName); + request.result.createObjectStore(this.storeName); + } - this.storeName = storeName; } }); @@ -374,7 +395,7 @@ export class IndexedDBDatabase implements LocalDatabase { * @param mode `readonly` or `readwrite` * @returns An `indexedDB` store, wrapped in an RxJS `Observable` */ - private transaction(mode: IDBTransactionMode): Observable { + protected transaction(mode: IDBTransactionMode): Observable { /* From the `indexedDB` connection, open a transaction and get the store */ return this.database @@ -384,33 +405,7 @@ export class IndexedDBDatabase implements LocalDatabase { try { - /* If the store name has already been set or detected, use it */ - if (this.storeName) { - - store = database.transaction([this.storeName], mode).objectStore(this.storeName); - - } else { - - try { - - /* Otherwise try with the default store name for version >= 8 */ - store = database.transaction([DEFAULT_IDB_STORE_NAME], mode).objectStore(DEFAULT_IDB_STORE_NAME); - this.storeName = DEFAULT_IDB_STORE_NAME; - - } catch { - - // TODO: test with previous versions of the lib to check no data is lost - // TODO: explicit option to keep old behavior? - /* Or try with the default store name for version < 8 */ - // tslint:disable-next-line: deprecation - store = database.transaction([DEFAULT_IDB_STORE_NAME_PRIOR_TO_V8], mode).objectStore(DEFAULT_IDB_STORE_NAME_PRIOR_TO_V8); - // tslint:disable-next-line: deprecation - this.storeName = DEFAULT_IDB_STORE_NAME_PRIOR_TO_V8; - this.isStorePriorToV8 = true; - - } - - } + store = database.transaction([this.storeName], mode).objectStore(this.storeName); } catch (error) { @@ -430,7 +425,7 @@ export class IndexedDBDatabase implements LocalDatabase { * @param request Request to listen * @returns An RxJS `Observable` listening to the success event */ - private successEvent(request: IDBRequest): Observable { + protected successEvent(request: IDBRequest): Observable { return fromEvent(request, 'success'); @@ -441,7 +436,7 @@ export class IndexedDBDatabase implements LocalDatabase { * @param request Request to listen * @returns An RxJS `Observable` listening to the error event and if so, throwing an error */ - private errorEvent(request: IDBRequest): Observable { + protected errorEvent(request: IDBRequest): Observable { return fromEvent(request, 'error').pipe(mergeMap(() => throwError(request.error as DOMException))); @@ -453,7 +448,7 @@ export class IndexedDBDatabase implements LocalDatabase { * @param mapCallback Callback returning the wanted value * @returns An RxJS `Observable` listening to request events and mapping to the wanted value */ - private requestEventsAndMapTo(request: IDBRequest, mapCallback: () => T): Observable { + protected requestEventsAndMapTo(request: IDBRequest, mapCallback: () => T): Observable { /* Listen to the success event and map to the wanted value * `mapTo()` must not be used here as it would eval `request.result` too soon */ @@ -473,7 +468,7 @@ export class IndexedDBDatabase implements LocalDatabase { * @param key Key to check * @returns An `indexedDB` request */ - private getKeyRequest(store: IDBObjectStore, key: string): IDBRequest { + protected getKeyRequest(store: IDBObjectStore, key: string): IDBRequest { /* `getKey()` is better but only available in `indexedDB` v2 (Chrome >= 58, missing in IE/Edge). * In older browsers, the value is checked instead, but it could lead to an exception diff --git a/projects/ngx-pwa/local-storage/src/lib/databases/local-database.ts b/projects/ngx-pwa/local-storage/src/lib/databases/local-database.ts index ae5e3726..50e45cf5 100644 --- a/projects/ngx-pwa/local-storage/src/lib/databases/local-database.ts +++ b/projects/ngx-pwa/local-storage/src/lib/databases/local-database.ts @@ -5,7 +5,7 @@ import { Observable } from 'rxjs'; import { IndexedDBDatabase } from './indexeddb-database'; import { LocalStorageDatabase } from './localstorage-database'; import { MemoryDatabase } from './memory-database'; -import { IDB_STORE_NAME, IDB_DB_NAME, LOCAL_STORAGE_PREFIX, LS_PREFIX } from '../tokens'; +import { IDB_STORE_NAME, IDB_DB_NAME, LOCAL_STORAGE_PREFIX, LS_PREFIX, IDB_DB_VERSION, IDB_NO_WRAP } from '../tokens'; /** * Factory to create a storage according to browser support @@ -17,7 +17,8 @@ import { IDB_STORE_NAME, IDB_DB_NAME, LOCAL_STORAGE_PREFIX, LS_PREFIX } from '.. * @see https://github.com/cyrilletuzi/angular-async-local-storage/blob/master/docs/BROWSERS_SUPPORT.md */ export function localDatabaseFactory( - platformId: string, LSPrefix: string, IDBDBName: string, IDBstoreName: string, oldPrefix: string): LocalDatabase { + platformId: string, LSPrefix: string, IDBDBName: string, IDBStoreName: string, + IDBDBVersion: number, IDBNoWrap: boolean, oldPrefix: string): LocalDatabase { // Do not explicit `window` here, as the global object is not the same in web workers if (isPlatformBrowser(platformId) && (indexedDB !== undefined) && (indexedDB !== null) && ('open' in indexedDB)) { @@ -30,7 +31,7 @@ export function localDatabaseFactory( * Will be the case for: * - IE10+ and all other browsers in normal mode * - Chromium / Safari private mode, but in this case, data will be swiped when the user leaves the app */ - return new IndexedDBDatabase(IDBDBName, IDBstoreName, oldPrefix); + return new IndexedDBDatabase(IDBDBName, IDBStoreName, IDBDBVersion, IDBNoWrap, oldPrefix); } else if (isPlatformBrowser(platformId) && (localStorage !== undefined) && (localStorage !== null) && ('getItem' in localStorage)) { @@ -69,6 +70,8 @@ export function localDatabaseFactory( LS_PREFIX, IDB_DB_NAME, IDB_STORE_NAME, + IDB_DB_VERSION, + IDB_NO_WRAP, // tslint:disable-next-line: deprecation LOCAL_STORAGE_PREFIX, ] diff --git a/projects/ngx-pwa/local-storage/src/lib/databases/localstorage-database.ts b/projects/ngx-pwa/local-storage/src/lib/databases/localstorage-database.ts index 2e7b1ffb..b6eca672 100644 --- a/projects/ngx-pwa/local-storage/src/lib/databases/localstorage-database.ts +++ b/projects/ngx-pwa/local-storage/src/lib/databases/localstorage-database.ts @@ -3,8 +3,8 @@ import { Observable, of, throwError, asyncScheduler } from 'rxjs'; import { observeOn } from 'rxjs/operators'; import { LocalDatabase } from './local-database'; -import { LOCAL_STORAGE_PREFIX, LS_PREFIX } from '../tokens'; import { SerializationError } from './exceptions'; +import { LOCAL_STORAGE_PREFIX, LS_PREFIX } from '../tokens'; @Injectable({ providedIn: 'root' @@ -14,31 +14,31 @@ export class LocalStorageDatabase implements LocalDatabase { /** * Optional user prefix to avoid collision for multiple apps on the same subdomain */ - private readonly prefix: string; - - /** - * Number of items in `localStorage` - */ - get size(): Observable { - - /* Wrap in a RxJS `Observable` to be consistent with other storages */ - return of(localStorage.length); - - } + readonly prefix: string; /** * Constructor params are provided by Angular (but can also be passed manually in tests) + * @param prefix Prefix option to avoid collision for multiple apps on the same subdomain or for interoperability * @param oldPrefix Prefix option prior to v8 to avoid collision for multiple apps on the same subdomain or for interoperability - * @param newPrefix Prefix option to avoid collision for multiple apps on the same subdomain or for interoperability */ constructor( - @Inject(LS_PREFIX) newPrefix = '', + @Inject(LS_PREFIX) prefix = '', // tslint:disable-next-line: deprecation @Inject(LOCAL_STORAGE_PREFIX) oldPrefix = '', ) { /* Priority for the new prefix option, otherwise old prefix with separator, or no prefix */ - this.prefix = newPrefix || (oldPrefix ? `${oldPrefix}_` : ''); + this.prefix = prefix || (oldPrefix ? `${oldPrefix}_` : ''); + + } + + /** + * Number of items in `localStorage` + */ + get size(): Observable { + + /* Wrap in a RxJS `Observable` to be consistent with other storages */ + return of(localStorage.length); } @@ -190,7 +190,7 @@ export class LocalStorageDatabase implements LocalDatabase { * @param index Index of the key * @returns The unprefixed key name if exists, `null` otherwise */ - private getUnprefixedKey(index: number): string | null { + protected getUnprefixedKey(index: number): string | null { /* Get the key in storage: may have a prefix */ const prefixedKey = localStorage.key(index); @@ -211,7 +211,7 @@ export class LocalStorageDatabase implements LocalDatabase { * @param key The key name * @returns The prefixed key name */ - private prefixKey(key: string): string { + protected prefixKey(key: string): string { return `${this.prefix}${key}`; diff --git a/projects/ngx-pwa/local-storage/src/lib/databases/memory-database.ts b/projects/ngx-pwa/local-storage/src/lib/databases/memory-database.ts index 157a74f7..a890e9fe 100644 --- a/projects/ngx-pwa/local-storage/src/lib/databases/memory-database.ts +++ b/projects/ngx-pwa/local-storage/src/lib/databases/memory-database.ts @@ -11,7 +11,7 @@ export class MemoryDatabase implements LocalDatabase { /** * Memory storage */ - private memoryStorage = new Map(); + protected memoryStorage = new Map(); /** * Number of items in memory diff --git a/projects/ngx-pwa/local-storage/src/lib/storage.module.ts b/projects/ngx-pwa/local-storage/src/lib/storage.module.ts new file mode 100644 index 00000000..c18e4636 --- /dev/null +++ b/projects/ngx-pwa/local-storage/src/lib/storage.module.ts @@ -0,0 +1,28 @@ +import { NgModule, ModuleWithProviders } from '@angular/core'; + +import { LocalStorageProvidersConfig, LS_PREFIX, IDB_DB_NAME, IDB_STORE_NAME, IDB_DB_VERSION, IDB_NO_WRAP } from './tokens'; + +/** + * This module does not contain anything, it's only useful to provide options via `.forRoot()`. + */ +@NgModule() +export class StorageModule { + + /** + * Only useful to provide options, otherwise it does nothing. + * **Must be used at initialization, ie. in `AppModule`, and must not be loaded again in another module.** + */ + static forRoot(config: LocalStorageProvidersConfig): ModuleWithProviders { + return { + ngModule: StorageModule, + providers: [ + config.LSPrefix ? { provide: LS_PREFIX, useValue: config.LSPrefix } : [], + config.IDBDBName ? { provide: IDB_DB_NAME, useValue: config.IDBDBName } : [], + config.IDBStoreName ? { provide: IDB_STORE_NAME, useValue: config.IDBStoreName } : [], + config.IDBDBVersion ? { provide: IDB_DB_VERSION, useValue: config.IDBDBVersion } : [], + config.IDBNoWrap ? { provide: IDB_NO_WRAP, useValue: config.IDBNoWrap } : [], + ], + }; + } + +} diff --git a/projects/ngx-pwa/local-storage/src/lib/validation/exceptions.ts b/projects/ngx-pwa/local-storage/src/lib/storages/exceptions.ts similarity index 100% rename from projects/ngx-pwa/local-storage/src/lib/validation/exceptions.ts rename to projects/ngx-pwa/local-storage/src/lib/storages/exceptions.ts diff --git a/projects/ngx-pwa/local-storage/src/lib/storages/index.ts b/projects/ngx-pwa/local-storage/src/lib/storages/index.ts index 0dedf3e0..36204bcd 100644 --- a/projects/ngx-pwa/local-storage/src/lib/storages/index.ts +++ b/projects/ngx-pwa/local-storage/src/lib/storages/index.ts @@ -1,2 +1,3 @@ export { StorageMap } from './storage-map.service'; export { LSGetItemOptions, LocalStorage } from './local-storage.service'; +export { VALIDATION_ERROR, ValidationError } from './exceptions'; diff --git a/projects/ngx-pwa/local-storage/src/lib/storages/local-storage.service.spec.ts b/projects/ngx-pwa/local-storage/src/lib/storages/local-storage.service.spec.ts index 01eefedc..736018e3 100755 --- a/projects/ngx-pwa/local-storage/src/lib/storages/local-storage.service.spec.ts +++ b/projects/ngx-pwa/local-storage/src/lib/storages/local-storage.service.spec.ts @@ -3,8 +3,9 @@ import { mergeMap, filter, tap } from 'rxjs/operators'; import { LocalStorage } from './local-storage.service'; import { StorageMap } from './storage-map.service'; +import { VALIDATION_ERROR } from './exceptions'; import { IndexedDBDatabase, LocalStorageDatabase, MemoryDatabase } from '../databases'; -import { JSONSchema, VALIDATION_ERROR } from '../validation'; +import { JSONSchema } from '../validation'; import { clearStorage, closeAndDeleteDatabase } from '../testing/cleaning'; function tests(description: string, localStorageServiceFactory: () => LocalStorage) { diff --git a/projects/ngx-pwa/local-storage/src/lib/storages/local-storage.service.ts b/projects/ngx-pwa/local-storage/src/lib/storages/local-storage.service.ts index a6eb9b9b..68a621e6 100755 --- a/projects/ngx-pwa/local-storage/src/lib/storages/local-storage.service.ts +++ b/projects/ngx-pwa/local-storage/src/lib/storages/local-storage.service.ts @@ -44,7 +44,7 @@ export class LocalStorage { } /* Use the `StorageMap` service to avoid code duplication */ - constructor(private storageMap: StorageMap) {} + constructor(protected storageMap: StorageMap) {} /** * Get an item value in storage. diff --git a/projects/ngx-pwa/local-storage/src/lib/storages/storage-map.service.spec.ts b/projects/ngx-pwa/local-storage/src/lib/storages/storage-map.service.spec.ts index e32d812d..64089b44 100644 --- a/projects/ngx-pwa/local-storage/src/lib/storages/storage-map.service.spec.ts +++ b/projects/ngx-pwa/local-storage/src/lib/storages/storage-map.service.spec.ts @@ -2,9 +2,10 @@ import { TestBed } from '@angular/core/testing'; import { mergeMap, tap, filter } from 'rxjs/operators'; import { StorageMap } from './storage-map.service'; +import { VALIDATION_ERROR } from './exceptions'; import { IndexedDBDatabase, LocalStorageDatabase, MemoryDatabase } from '../databases'; -import { JSONSchema, VALIDATION_ERROR } from '../validation'; -import { DEFAULT_IDB_DB_NAME, DEFAULT_IDB_STORE_NAME, DEFAULT_IDB_STORE_NAME_PRIOR_TO_V8 } from '../tokens'; +import { JSONSchema } from '../validation'; +import { DEFAULT_IDB_DB_NAME, DEFAULT_IDB_STORE_NAME, DEFAULT_IDB_DB_VERSION } from '../tokens'; import { clearStorage, closeAndDeleteDatabase } from '../testing/cleaning'; function tests(description: string, localStorageServiceFactory: () => StorageMap) { @@ -226,8 +227,7 @@ function tests(description: string, localStorageServiceFactory: () => StorageMap const value = new Blob(); - // tslint:disable-next-line: no-string-literal - const observer = (localStorageService['database'] instanceof LocalStorageDatabase) ? + const observer = (localStorageService.backingEngine === 'localStorage') ? { next: () => {}, error: () => { @@ -652,7 +652,12 @@ describe('StorageMap', () => { tests('indexedDB', () => new StorageMap(new IndexedDBDatabase())); - tests('indexedDB with old prefix', () => new StorageMap(new IndexedDBDatabase(undefined, undefined, `myapp${Date.now()}`))); + tests('indexedDB with no wrap', () => new StorageMap(new IndexedDBDatabase())); + + tests('indexedDB with custom options', () => new StorageMap(new IndexedDBDatabase('customDbTest', 'storeTest', 2))); + + tests('indexedDB with old prefix', () => + new StorageMap(new IndexedDBDatabase(undefined, undefined, undefined, undefined, `myapp${Date.now()}`))); tests( 'indexedDB with custom database and store names', @@ -673,7 +678,7 @@ describe('StorageMap', () => { try { - const dbOpen = indexedDB.open(DEFAULT_IDB_DB_NAME); + const dbOpen = indexedDB.open(DEFAULT_IDB_DB_NAME, DEFAULT_IDB_DB_VERSION); dbOpen.addEventListener('success', () => { @@ -683,7 +688,7 @@ describe('StorageMap', () => { request.addEventListener('success', () => { - expect(request.result).toEqual(value); + expect(request.result).toEqual({ value }); dbOpen.result.close(); @@ -720,170 +725,123 @@ describe('StorageMap', () => { }); - it('indexedDB default store name (will be pending in Firefox private mode)', (done) => { - - const localStorageService = new StorageMap(new IndexedDBDatabase()); - - /* Do a request first as a first transaction is needed to set the store name */ - localStorageService.get('test').subscribe(() => { - - // tslint:disable-next-line: no-string-literal - if (localStorageService['database'] instanceof IndexedDBDatabase) { - - // tslint:disable-next-line: no-string-literal - expect(localStorageService['database']['storeName']).toBe(DEFAULT_IDB_STORE_NAME); - - closeAndDeleteDatabase(done, localStorageService); - - } else { - - /* Cases: Firefox private mode */ - pending(); - - } - - }); - - }); - - it('indexedDB custom store name (will be pending in Firefox private mode)', (done) => { - - /* Unique names to be sure `indexedDB` `upgradeneeded` event is triggered */ - const dbName = `dbCustom${Date.now()}`; - const storeName = `storeCustom${Date.now()}`; - - const localStorageService = new StorageMap(new IndexedDBDatabase(dbName, storeName)); - - /* Do a request first as a first transaction is needed to set the store name */ - localStorageService.get('test').subscribe(() => { + /* Avoid https://github.com/cyrilletuzi/angular-async-local-storage/issues/57 */ + it('IndexedDb with noWrap (will be pending in Firefox/IE private mode)', (done) => { - // tslint:disable-next-line: no-string-literal - if (localStorageService['database'] instanceof IndexedDBDatabase) { + const index = `nowrap${Date.now()}`; + const value = 'test'; - // tslint:disable-next-line: no-string-literal - expect(localStorageService['database']['storeName']).toBe(storeName); + const localStorageService = new StorageMap(new IndexedDBDatabase(undefined, undefined, undefined, true)); - closeAndDeleteDatabase(done, localStorageService); + localStorageService.set(index, value).subscribe(() => { - } else { + try { - /* Cases: Firefox private mode */ - pending(); + const dbOpen = indexedDB.open(DEFAULT_IDB_DB_NAME, DEFAULT_IDB_DB_VERSION); - } + dbOpen.addEventListener('success', () => { - }); + const store = dbOpen.result.transaction([DEFAULT_IDB_STORE_NAME], 'readonly').objectStore(DEFAULT_IDB_STORE_NAME); - }); + const request = store.get(index); - it('indexedDB store prior to v8 (will be pending in Firefox/IE private mode)', (done) => { + request.addEventListener('success', () => { - /* Unique name to be sure `indexedDB` `upgradeneeded` event is triggered */ - const dbName = `ngStoreV7${Date.now()}`; + expect(request.result).toEqual(value); - const index1 = `test1${Date.now()}`; - const value1 = 'test1'; - const index2 = `test2${Date.now()}`; - const value2 = 'test2'; + dbOpen.result.close(); - try { + closeAndDeleteDatabase(done, localStorageService); - const dbOpen = indexedDB.open(dbName); + }); - dbOpen.addEventListener('upgradeneeded', () => { + request.addEventListener('error', () => { - // tslint:disable-next-line: deprecation - if (!dbOpen.result.objectStoreNames.contains(DEFAULT_IDB_STORE_NAME_PRIOR_TO_V8)) { + dbOpen.result.close(); - /* Create the object store */ - // tslint:disable-next-line: deprecation - dbOpen.result.createObjectStore(DEFAULT_IDB_STORE_NAME_PRIOR_TO_V8); + /* This case is not supposed to happen */ + fail(); - } + }); - }); + }); - dbOpen.addEventListener('success', () => { + dbOpen.addEventListener('error', () => { - const localStorageService = new StorageMap(new IndexedDBDatabase(dbName)); + /* Cases : Firefox private mode where `indexedDb` exists but fails */ + pending(); - // tslint:disable-next-line: deprecation - const store1 = dbOpen.result.transaction([DEFAULT_IDB_STORE_NAME_PRIOR_TO_V8], 'readwrite') - // tslint:disable-next-line: deprecation - .objectStore(DEFAULT_IDB_STORE_NAME_PRIOR_TO_V8); + }); - const request1 = store1.add({ value: value1 }, index1); + } catch { - request1.addEventListener('success', () => { + /* Cases : IE private mode where `indexedDb` will exist but not its `open()` method */ + pending(); - localStorageService.get(index1).subscribe((result) => { + } - /* Check detection of old store has gone well */ - // tslint:disable-next-line: deprecation no-string-literal - expect((localStorageService['database'] as IndexedDBDatabase)['storeName']).toBe(DEFAULT_IDB_STORE_NAME_PRIOR_TO_V8); + }); - /* Via the lib, data should be unwrapped */ - expect(result).toBe(value1); + }); - localStorageService.set(index2, value2).subscribe(() => { + it('indexedDB default options (will be pending in Firefox private mode)', (done) => { - // tslint:disable-next-line: deprecation - const store2 = dbOpen.result.transaction([DEFAULT_IDB_STORE_NAME_PRIOR_TO_V8], 'readonly') - // tslint:disable-next-line: deprecation - .objectStore(DEFAULT_IDB_STORE_NAME_PRIOR_TO_V8); + const localStorageService = new StorageMap(new IndexedDBDatabase()); - const request2 = store2.get(index2); + /* Do a request first as a first transaction is needed to set the store name */ + localStorageService.get('test').subscribe(() => { - request2.addEventListener('success', () => { + if (localStorageService.backingEngine === 'indexedDB') { - /* Via direct `indexedDB`, data should be wrapped */ - expect(request2.result).toEqual({ value: value2 }); + const { database, store, version } = localStorageService.backingStore; - dbOpen.result.close(); + expect(database).toBe(DEFAULT_IDB_DB_NAME); + expect(store).toBe(DEFAULT_IDB_STORE_NAME); + expect(version).toBe(DEFAULT_IDB_DB_VERSION); - closeAndDeleteDatabase(done, localStorageService); + closeAndDeleteDatabase(done, localStorageService); - }); + } else { - request2.addEventListener('error', () => { + /* Cases: Firefox private mode */ + pending(); - dbOpen.result.close(); + } - /* This case is not supposed to happen */ - fail(); + }); - }); + }); - }); + it('indexedDB custom options (will be pending in Firefox private mode)', (done) => { - }); + /* Unique names to be sure `indexedDB` `upgradeneeded` event is triggered */ + const dbName = `dbCustom${Date.now()}`; + const storeName = `storeCustom${Date.now()}`; + const dbVersion = 2; - }); + const localStorageService = new StorageMap(new IndexedDBDatabase(dbName, storeName, dbVersion)); - request1.addEventListener('error', () => { + /* Do a request first as a first transaction is needed to set the store name */ + localStorageService.get('test').subscribe(() => { - dbOpen.result.close(); + if (localStorageService.backingEngine === 'indexedDB') { - /* This case is not supposed to happen */ - fail(); + const { database, store, version } = localStorageService.backingStore; - }); + expect(database).toBe(dbName); + expect(store).toBe(storeName); + expect(version).toBe(dbVersion); - }); + closeAndDeleteDatabase(done, localStorageService); - dbOpen.addEventListener('error', () => { + } else { - /* Cases : Firefox private mode where `indexedDb` exists but fails */ + /* Cases: Firefox private mode */ pending(); - }); - - } catch { - - /* Cases : IE private mode where `indexedDb` will exist but not its `open()` method */ - pending(); + } - } + }); }); @@ -891,16 +849,14 @@ describe('StorageMap', () => { /* Unique name to be sure `indexedDB` `upgradeneeded` event is triggered */ const prefix = `myapp${Date.now()}`; - const localStorageService = new StorageMap(new IndexedDBDatabase(undefined, undefined, prefix)); + const localStorageService = new StorageMap(new IndexedDBDatabase(undefined, undefined, undefined, undefined, prefix)); /* Do a request first to allow localStorage fallback if needed */ localStorageService.get('test').subscribe(() => { - // tslint:disable-next-line: no-string-literal - if (localStorageService['database'] instanceof IndexedDBDatabase) { + if (localStorageService.backingEngine === 'indexedDB') { - // tslint:disable-next-line: no-string-literal - expect(localStorageService['database']['dbName']).toBe(`${prefix}_${DEFAULT_IDB_DB_NAME}`); + expect(localStorageService.backingStore.database).toBe(`${prefix}_${DEFAULT_IDB_DB_NAME}`); closeAndDeleteDatabase(done, localStorageService); @@ -922,7 +878,7 @@ describe('StorageMap', () => { const localStorageService = new StorageMap(new LocalStorageDatabase(prefix)); // tslint:disable-next-line: no-string-literal - expect((localStorageService['database'] as LocalStorageDatabase)['prefix']).toBe(prefix); + expect(localStorageService.fallbackBackingStore.prefix).toBe(prefix); }); @@ -933,7 +889,7 @@ describe('StorageMap', () => { const localStorageService = new StorageMap(new LocalStorageDatabase(undefined, prefix)); // tslint:disable-next-line: no-string-literal - expect((localStorageService['database'] as LocalStorageDatabase)['prefix']).toBe(`${prefix}_`); + expect(localStorageService.fallbackBackingStore.prefix).toBe(`${prefix}_`); }); diff --git a/projects/ngx-pwa/local-storage/src/lib/storages/storage-map.service.ts b/projects/ngx-pwa/local-storage/src/lib/storages/storage-map.service.ts index 1fa55355..fed32904 100644 --- a/projects/ngx-pwa/local-storage/src/lib/storages/storage-map.service.ts +++ b/projects/ngx-pwa/local-storage/src/lib/storages/storage-map.service.ts @@ -2,11 +2,12 @@ import { Injectable, Inject } from '@angular/core'; import { Observable, throwError, of, OperatorFunction } from 'rxjs'; import { mergeMap, catchError } from 'rxjs/operators'; +import { ValidationError } from './exceptions'; import { JSONSchema, JSONSchemaBoolean, JSONSchemaInteger, - JSONSchemaNumber, JSONSchemaString, JSONSchemaArrayOf, ValidationError, JSONValidator + JSONSchemaNumber, JSONSchemaString, JSONSchemaArrayOf, JSONValidator } from '../validation'; -import { LocalDatabase, IDB_BROKEN_ERROR, LocalStorageDatabase } from '../databases'; +import { LocalDatabase, IDB_BROKEN_ERROR, LocalStorageDatabase, IndexedDBDatabase, MemoryDatabase } from '../databases'; import { LS_PREFIX, LOCAL_STORAGE_PREFIX } from '../tokens'; @Injectable({ @@ -14,15 +15,6 @@ import { LS_PREFIX, LOCAL_STORAGE_PREFIX } from '../tokens'; }) export class StorageMap { - /** - * Number of items in storage - */ - get size(): Observable { - - return this.database.size; - - } - /** * Constructor params are provided by Angular (but can also be passed manually in tests) * @param database Storage to use @@ -38,6 +30,84 @@ export class StorageMap { @Inject(LOCAL_STORAGE_PREFIX) protected oldPrefix = '', ) {} + /** + * Number of items in storage. + */ + get size(): Observable { + + return this.database.size; + + } + + /** + * Tells you which storage engine is used. *Only useful for interoperability.* + * Note that due to some browsers issues in some special contexts + * (Firefox private mode and Safari cross-origin iframes), + * **this information may be wrong at initialization,** + * as the storage could fallback from `indexedDB` to `localStorage` + * only after a first read or write operation. + * @returns Storage engine used + */ + get backingEngine(): 'indexedDB' | 'localStorage' | 'memory' | 'unknown' { + + if (this.database instanceof IndexedDBDatabase) { + + return 'indexedDB'; + + } else if (this.database instanceof LocalStorageDatabase) { + + return 'localStorage'; + + } else if (this.database instanceof MemoryDatabase) { + + return 'memory'; + + } else { + + return 'unknown'; + + } + + } + + /** + * Info about `indexedDB` database. *Only useful for interoperability.* + * @returns `indexedDB` database name, store name and database version. + * **Values will be empty if the storage is not `indexedDB`,** + * **so it should be used after an engine check**: + * ```ts + * if (this.storageMap.backingEngine === 'indexedDB') { + * const { database, store, version } = this.storageMap.backingStore; + * } + * ``` + */ + get backingStore(): { database: string, store: string, version: number } { + + return (this.database instanceof IndexedDBDatabase) ? + this.database.backingStore : + { database: '', store: '', version: 0 }; + + } + + /** + * Info about `localStorage` fallback storage. *Only useful for interoperability.* + * @returns `localStorage` prefix. + * **Values will be empty if the storage is not `localStorage`,** + * **so it should be used after an engine check**: + * ```ts + * if (this.storageMap.backingEngine === 'localStorage') { + * const { prefix } = this.storageMap.fallbackBackingStore; + * } + * ``` + */ + get fallbackBackingStore(): { prefix: string } { + + return (this.database instanceof LocalStorageDatabase) ? + { prefix: this.database.prefix } : + { prefix: '' }; + + } + /** * Get an item value in storage. * The signature has many overloads due to validation, **please refer to the documentation.** @@ -166,7 +236,7 @@ export class StorageMap { * RxJS operator to catch if `indexedDB` is broken * @param operationCallback Callback with the operation to redo */ - private catchIDBBroken(operationCallback: () => Observable): OperatorFunction { + protected catchIDBBroken(operationCallback: () => Observable): OperatorFunction { return catchError((error) => { diff --git a/projects/ngx-pwa/local-storage/src/lib/testing/cleaning.ts b/projects/ngx-pwa/local-storage/src/lib/testing/cleaning.ts index 912fee77..efd02e0a 100644 --- a/projects/ngx-pwa/local-storage/src/lib/testing/cleaning.ts +++ b/projects/ngx-pwa/local-storage/src/lib/testing/cleaning.ts @@ -1,5 +1,5 @@ import { StorageMap } from '../storages'; -import { IndexedDBDatabase, LocalStorageDatabase, MemoryDatabase } from '../databases'; +import { IndexedDBDatabase, MemoryDatabase } from '../databases'; /** * Helper to clear all data in storage to avoid tests overlap @@ -8,21 +8,18 @@ import { IndexedDBDatabase, LocalStorageDatabase, MemoryDatabase } from '../data */ export function clearStorage(done: DoneFn, storageService: StorageMap) { - // tslint:disable-next-line: no-string-literal - if (storageService['database'] instanceof IndexedDBDatabase) { + if (storageService.backingEngine === 'indexedDB') { // tslint:disable-next-line: no-string-literal - const indexedDBService = storageService['database']; + const indexedDBService = storageService['database'] as IndexedDBDatabase; try { - // tslint:disable-next-line: no-string-literal - const dbOpen = indexedDB.open(indexedDBService['dbName']); + const dbOpen = indexedDB.open(indexedDBService.backingStore.database); dbOpen.addEventListener('success', () => { - // tslint:disable-next-line: no-string-literal - const storeName = indexedDBService['storeName']; + const storeName = indexedDBService.backingStore.store; /* May be `null` if no requests were made */ if (storeName) { @@ -75,18 +72,16 @@ export function clearStorage(done: DoneFn, storageService: StorageMap) { } - // tslint:disable-next-line: no-string-literal - } else if (storageService['database'] instanceof LocalStorageDatabase) { + } else if (storageService.backingEngine === 'localStorage') { localStorage.clear(); done(); - // tslint:disable-next-line: no-string-literal - } else if (storageService['database'] instanceof MemoryDatabase) { + } else if (storageService.backingEngine === 'memory') { // tslint:disable-next-line: no-string-literal - storageService['database']['memoryStorage'].clear(); + (storageService['database'] as MemoryDatabase)['memoryStorage'].clear(); done(); @@ -110,11 +105,10 @@ export function clearStorage(done: DoneFn, storageService: StorageMap) { export function closeAndDeleteDatabase(done: DoneFn, storageService: StorageMap) { /* Only `indexedDB` is concerned */ - // tslint:disable-next-line: no-string-literal - if (storageService['database'] instanceof IndexedDBDatabase) { + if (storageService.backingEngine === 'indexedDB') { // tslint:disable-next-line: no-string-literal - const indexedDBService = storageService['database']; + const indexedDBService = storageService['database'] as IndexedDBDatabase; // tslint:disable-next-line: no-string-literal indexedDBService['database'].subscribe({ @@ -124,8 +118,7 @@ export function closeAndDeleteDatabase(done: DoneFn, storageService: StorageMap) database.close(); /* Delete database */ - // tslint:disable-next-line: no-string-literal - const deletingDb = indexedDB.deleteDatabase(indexedDBService['dbName']); + const deletingDb = indexedDB.deleteDatabase(indexedDBService.backingStore.database); /* Use an arrow function for done, otherwise it causes an issue in IE */ deletingDb.addEventListener('success', () => { done(); }); diff --git a/projects/ngx-pwa/local-storage/src/lib/tokens.ts b/projects/ngx-pwa/local-storage/src/lib/tokens.ts index ecdfbce3..3abb75df 100644 --- a/projects/ngx-pwa/local-storage/src/lib/tokens.ts +++ b/projects/ngx-pwa/local-storage/src/lib/tokens.ts @@ -1,8 +1,40 @@ import { InjectionToken, Provider } from '@angular/core'; /** - * Token to provide a prefix to avoid collision when multiple apps on the same subdomain. - * @deprecated Use options of `localStorageProviders()` instead. Will be removed in v9. + * Token to provide a prefix to avoid collision when multiple apps on the same *sub*domain. + * @deprecated **Will be removed in v9**. Set options with `StorageModule.forRoot()` instead: + * + * Before v8: + * ```ts + * import { localStorageProviders, LOCAL_STORAGE_PREFIX } from '@ngx-pwa/local-storage'; + * + * @NgModule({ + * providers: [ + * { provide: LOCAL_STORAGE_PREFIX, useValue: 'myapp' }, + * ] + * }) + * export class AppModule {} + * ``` + * + * Since v8: + * ```ts + * import { StorageModule } from '@ngx-pwa/local-storage'; + * + * @NgModule({ + * imports: [ + * StorageModule.forRoot({ + * LSPrefix: 'myapp_', // Note the underscore + * IDBDBName: 'myapp_ngStorage', + * }), + * ] + * }) + * export class AppModule {} + * ``` + * + * **Be very careful while changing this in applications already deployed in production,** + * **as an error would mean the loss of all previously stored data.** + * **SO PLEASE TEST BEFORE PUSHING IN PRODUCTION.** + * */ export const LOCAL_STORAGE_PREFIX = new InjectionToken('localStoragePrefix', { providedIn: 'root', @@ -31,23 +63,52 @@ export const IDB_DB_NAME = new InjectionToken('localStorageIDBDBName', { }); /** - * Default name used for `indexedDB` object store. + * Default version used for `indexedDB` database. + */ +export const DEFAULT_IDB_DB_VERSION = 1; + +/** + * Token to provide `indexedDB` database version. + * Must be an unsigned **integer**. */ -export const DEFAULT_IDB_STORE_NAME = 'storage'; +export const IDB_DB_VERSION = new InjectionToken('localStorageIDBDBVersion', { + providedIn: 'root', + factory: () => DEFAULT_IDB_DB_VERSION +}); /** - * Default name used for `indexedDB` object store prior to v8. - * @deprecated **For backward compatibility only.** May be removed in future versions. + * Default name used for `indexedDB` object store. */ -export const DEFAULT_IDB_STORE_NAME_PRIOR_TO_V8 = 'localStorage'; +export const DEFAULT_IDB_STORE_NAME = 'localStorage'; /** * Token to provide `indexedDB` store name. * For backward compatibility, the default can't be set now, `IndexedDBDatabase` will do it at runtime. */ -export const IDB_STORE_NAME = new InjectionToken('localStorageIDBStoreName', { +export const IDB_STORE_NAME = new InjectionToken('localStorageIDBStoreName', { providedIn: 'root', - factory: () => null + factory: () => DEFAULT_IDB_STORE_NAME +}); + +/** + * Default value for interoperability with native `indexedDB` and other storage libs, + * by changing how values are stored in `indexedDB` database. + * Currently defaults to `false` for backward compatiblity in existing applications + * (**DO NOT CHANGE IT IN PRODUCTION**, as it would break with existing data), + * but **should be `false` in all new applications, as it may become the default in a future version**. + */ +export const DEFAULT_IDB_NO_WRAP = false; + +/** + * Token to allow interoperability with native `indexedDB` and other storage libs, + * by changing how values are stored in `indexedDB` database. + * Currently defaults to `false` for backward compatiblity in existing applications + * (**DO NOT CHANGE IT IN PRODUCTION**, as it would break with existing data), + * but **should be `true` in all new applications, as it may become the default in a future version**. + */ +export const IDB_NO_WRAP = new InjectionToken('localStorageIDBWrap', { + providedIn: 'root', + factory: () => DEFAULT_IDB_NO_WRAP }); export interface LocalStorageProvidersConfig { @@ -80,21 +141,69 @@ export interface LocalStorageProvidersConfig { */ IDBStoreName?: string; + /** + * Allows to change the database version used for `indexedDB` database. + * Must be an unsigned **integer**. + * **Use with caution as the creation of the store depends on the version.** + * *Use only* for interoperability with other APIs or to avoid collision for multiple apps on the same subdomain. + * **WARNING: do not change this option in an app already deployed in production, as previously stored data would be lost.** + */ + IDBDBVersion?: number; + + /** + * Allows interoperability with native `indexedDB` and other storage libs, + * by changing how values are stored in `indexedDB` database. + * Currently defaults to `false` for backward compatiblity in existing applications, + * **DO NOT CHANGE IT IN PRODUCTION**, as it would break with existing data. + * but **should be `true` in all new applications, as it may become the default in a future version**. + */ + IDBNoWrap?: boolean; + } /** * Helper function to provide options. **Must be used at initialization, ie. in `AppModule`.** * @param config Options. * @returns A list of providers for the lib options. + * @deprecated **Will be removed in v9.** Set options via `StorageModule.forRoot()` instead: + * + * Before v8: + * ```ts + * import { localStorageProviders, LOCAL_STORAGE_PREFIX } from '@ngx-pwa/local-storage'; + * + * @NgModule({ + * providers: [ + * localStorageProviders({ prefix: 'myapp' }), + * ] + * }) + * export class AppModule {} + * ``` + * + * Since v8: + * ```ts + * import { StorageModule } from '@ngx-pwa/local-storage'; + * + * @NgModule({ + * imports: [ + * StorageModule.forRoot({ + * LSPrefix: 'myapp_', // Note the underscore + * IDBDBName: 'myapp_ngStorage', + * }), + * ] + * }) + * export class AppModule {} + * ``` + * + * **Be very careful while changing this in applications already deployed in production,** + * **as an error would mean the loss of all previously stored data.** + * **SO PLEASE TEST BEFORE PUSHING IN PRODUCTION.** + * */ export function localStorageProviders(config: LocalStorageProvidersConfig): Provider[] { return [ // tslint:disable-next-line: deprecation config.prefix ? { provide: LOCAL_STORAGE_PREFIX, useValue: config.prefix } : [], - config.LSPrefix ? { provide: LS_PREFIX, useValue: config.LSPrefix } : [], - config.IDBDBName ? { provide: IDB_DB_NAME, useValue: config.IDBDBName } : [], - config.IDBStoreName ? { provide: IDB_STORE_NAME, useValue: config.IDBStoreName } : [], ]; } diff --git a/projects/ngx-pwa/local-storage/src/lib/validation/index.ts b/projects/ngx-pwa/local-storage/src/lib/validation/index.ts index 9a25fc59..e467dca8 100644 --- a/projects/ngx-pwa/local-storage/src/lib/validation/index.ts +++ b/projects/ngx-pwa/local-storage/src/lib/validation/index.ts @@ -3,4 +3,3 @@ export { JSONSchemaBoolean, JSONSchemaInteger, JSONSchemaNumber, JSONSchemaNumeric, JSONSchemaString } from './json-schema'; export { JSONValidator } from './json-validator'; -export { VALIDATION_ERROR, ValidationError } from './exceptions'; diff --git a/projects/ngx-pwa/local-storage/src/lib/validation/json-validator.ts b/projects/ngx-pwa/local-storage/src/lib/validation/json-validator.ts index a9e79515..41d75130 100644 --- a/projects/ngx-pwa/local-storage/src/lib/validation/json-validator.ts +++ b/projects/ngx-pwa/local-storage/src/lib/validation/json-validator.ts @@ -44,7 +44,7 @@ export class JSONValidator { * @param schema Schema describing the string * @returns If data is valid: `true`, if it is invalid: `false` */ - private validateString(data: any, schema: JSONSchemaString): boolean { + protected validateString(data: any, schema: JSONSchemaString): boolean { if (typeof data !== 'string') { return false; @@ -90,7 +90,7 @@ export class JSONValidator { * @param schema Schema describing the number or integer * @returns If data is valid: `true`, if it is invalid: `false` */ - private validateNumber(data: any, schema: JSONSchemaNumber | JSONSchemaInteger): boolean { + protected validateNumber(data: any, schema: JSONSchemaNumber | JSONSchemaInteger): boolean { if (typeof data !== 'number') { return false; @@ -141,7 +141,7 @@ export class JSONValidator { * @param schema Schema describing the boolean * @returns If data is valid: `true`, if it is invalid: `false` */ - private validateBoolean(data: any, schema: JSONSchemaBoolean): boolean { + protected validateBoolean(data: any, schema: JSONSchemaBoolean): boolean { if (typeof data !== 'boolean') { return false; @@ -161,7 +161,7 @@ export class JSONValidator { * @param schema Schema describing the array * @returns If data is valid: `true`, if it is invalid: `false` */ - private validateArray(data: any[], schema: JSONSchemaArray): boolean { + protected validateArray(data: any[], schema: JSONSchemaArray): boolean { if (!Array.isArray(data)) { return false; @@ -212,7 +212,7 @@ export class JSONValidator { * @param schemas Schemas describing the tuple * @returns If data is valid: `true`, if it is invalid: `false` */ - private validateTuple(data: any[], schemas: JSONSchema[]): boolean { + protected validateTuple(data: any[], schemas: JSONSchema[]): boolean { /* Tuples have a fixed length */ if (data.length !== schemas.length) { @@ -239,7 +239,7 @@ export class JSONValidator { * @param schema JSON schema describing the object * @returns If data is valid: `true`, if it is invalid: `false` */ - private validateObject(data: { [k: string]: any; }, schema: JSONSchemaObject): boolean { + protected validateObject(data: { [k: string]: any; }, schema: JSONSchemaObject): boolean { /* Check the type and if not `null` as `null` also have the type `object` in old browsers */ if ((data === null) || (typeof data !== 'object')) { @@ -290,7 +290,7 @@ export class JSONValidator { * @param schema JSON schema describing the constant * @returns If data is valid: `true`, if it is invalid: `false` */ - private validateConst(data: any, schema: JSONSchemaBoolean | JSONSchemaInteger | JSONSchemaNumber | JSONSchemaString): boolean { + protected validateConst(data: any, schema: JSONSchemaBoolean | JSONSchemaInteger | JSONSchemaNumber | JSONSchemaString): boolean { if (!schema.const) { return true; @@ -306,7 +306,7 @@ export class JSONValidator { * @param schema JSON schema describing the enum * @returns If data is valid: `true`, if it is invalid: `false` */ - private validateEnum(data: any, schema: JSONSchemaInteger | JSONSchemaNumber | JSONSchemaString): boolean { + protected validateEnum(data: any, schema: JSONSchemaInteger | JSONSchemaNumber | JSONSchemaString): boolean { if (!schema.enum) { return true; diff --git a/projects/ngx-pwa/local-storage/src/public_api.ts b/projects/ngx-pwa/local-storage/src/public_api.ts index 89127467..d88c601d 100644 --- a/projects/ngx-pwa/local-storage/src/public_api.ts +++ b/projects/ngx-pwa/local-storage/src/public_api.ts @@ -7,12 +7,9 @@ export { JSONSchema, JSONSchemaBoolean, JSONSchemaInteger, JSONSchemaNumber, - JSONSchemaNumeric, JSONSchemaString, JSONSchemaArray, JSONSchemaArrayOf, JSONSchemaObject, - VALIDATION_ERROR, ValidationError + JSONSchemaNumeric, JSONSchemaString, JSONSchemaArray, JSONSchemaArrayOf, JSONSchemaObject } from './lib/validation'; export { LocalDatabase, SERIALIZATION_ERROR, SerializationError } from './lib/databases'; -export { LocalStorage, StorageMap, LSGetItemOptions } from './lib/storages'; -export { - localStorageProviders, LocalStorageProvidersConfig, - DEFAULT_IDB_DB_NAME, DEFAULT_IDB_STORE_NAME, LOCAL_STORAGE_PREFIX -} from './lib/tokens'; +export { LocalStorage, StorageMap, LSGetItemOptions, ValidationError, VALIDATION_ERROR } from './lib/storages'; +export { localStorageProviders, LocalStorageProvidersConfig, LOCAL_STORAGE_PREFIX } from './lib/tokens'; +export { StorageModule } from './lib/storage.module';