diff --git a/README.md b/README.md index 510bf0e6..d50e3a0a 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,48 @@ # Async local storage for Angular -Efficient local storage module for Angular apps and Progressive Wep apps (PWA): -- **simplicity**: based on native `localStorage` API, with automatic JSON stringify/parse, -- **perfomance**: internally stored via the asynchronous `IndexedDB` API, -- **Angular-like**: wrapped in RxJS `Observables`, +Efficient client-side storage module for Angular apps and Progressive Wep Apps (PWA): +- **simplicity**: based on native `localStorage` API, +- **perfomance**: internally stored via the asynchronous `indexedDB` API, +- **Angular-like**: wrapped in RxJS `Observable`s, - **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 local storage according to [ngx.tools](https://ngx.tools/#/search?q=local%20storage). +- **reference**: 1st Angular library for client-side storage according to [ngx.tools](https://ngx.tools/#/search?q=local%20storage). ## By the same author - [Angular schematics extension for VS Code](https://marketplace.visualstudio.com/items?itemName=cyrilletuzi.angular-schematics) (GUI for Angular CLI commands) -- Other Angular libraries: [@ngx-pwa/offline](https://github.com/cyrilletuzi/ngx-pwa-offline) and [@ngx-pwa/ngsw-schema](https://github.com/cyrilletuzi/ngsw-schema) +- Other Angular library: [@ngx-pwa/offline](https://github.com/cyrilletuzi/ngx-pwa-offline) - Popular [Angular posts on Medium](https://medium.com/@cyrilletuzi) - Follow updates of this lib on [Twitter](https://twitter.com/cyrilletuzi) - **[Angular onsite trainings](https://formationjavascript.com/formation-angular/)** (based in Paris, so the website is in French, but [my English bio is here](https://www.cyrilletuzi.com/en/web/) and I'm open to travel) ## Why this module? -For now, Angular does not provide a local storage module, and almost every app needs some local storage. +For now, Angular does not provide a client-side storage module, and almost every app needs some client-side storage. There are 2 native JavaScript APIs available: - [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Storage/LocalStorage) -- [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) +- [indexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) The `localStorage` API is simple to use but synchronous, so if you use it too often, your app will soon begin to freeze. -The `IndexedDB` API is asynchronous and efficient, but it's a mess to use: -you'll soon be caught by the callback hell, as it does not support Promises yet. +The `indexedDB` API is asynchronous and efficient, but it's a mess to use: +you'll soon be caught by the callback hell, as it does not support `Promise`s yet. -Mozilla has done a very great job with the [localForage library](http://localforage.github.io/localForage/): +Mozilla has done a very great job with the [`localForage` library](http://localforage.github.io/localForage/): a simple API based on native `localStorage`, -but internally stored via the asynchronous `IndexedDB` for performance. +but internally stored via the asynchronous `indexedDB` for performance. But it's built in ES5 old school way and then it's a mess to include into Angular. -This module is based on the same idea as localForage, but in ES6 -and additionally wrapped into [RxJS Observables](http://reactivex.io/rxjs/) +This module is based on the same idea as `localForage`, but built in ES6+ +and additionally wrapped into [RxJS `Observable`s](http://reactivex.io/rxjs/) to be homogeneous with other Angular modules. ## Getting started -Install the same version as your Angular one via [npm](http://npmjs.com): +Install the right version according to your Angular one via [`npm`](http://npmjs.com): ```bash # For Angular 7 & 8: @@ -52,7 +52,19 @@ npm install @ngx-pwa/local-storage@next npm install @ngx-pwa/local-storage@6 ``` -Now you just have to inject the service where you need it: +### Upgrading + +If you still use the old `angular-async-local-storage` package, or to update to new versions, +see the **[migration guides](./MIGRATION.md).** + +Versions 4 & 5, which are *not* supported anymore, +needed an additional setup step explained in [the old module guide](./docs/OLD_MODULE.md). + +## API + +2 services are available for client-side storage, you just have to inject one of them were you need it. + +### `LocalStorage` ```typescript import { LocalStorage } from '@ngx-pwa/local-storage'; @@ -65,29 +77,71 @@ export class YourService { } ``` -Versions 4 & 5 (only) need an additional setup step explained in [the old module guide](./docs/OLD_MODULE.md). +This service API follows the +[native `localStorage` API](https://developer.mozilla.org/en-US/docs/Web/API/Storage/LocalStorage), +except it's asynchronous via [RxJS `Observable`s](http://reactivex.io/rxjs/): -### Upgrading +```typescript +class LocalStorage { + length: Observable; + getItem(index: string, schema?: JSONSchema): Observable {} + setItem(index: string, value: any): Observable {} + removeItem(index: string): Observable {} + clear(): Observable {} +} +``` -If you still use the old `angular-async-local-storage` package, or to update to new versions, -see the **[migration guides](./MIGRATION.md).** +### `StorageMap` -## API +```typescript +import { StorageMap } from '@ngx-pwa/local-storage'; -The API follows the [native localStorage API](https://developer.mozilla.org/en-US/docs/Web/API/Storage/LocalStorage), -except it's asynchronous via [RxJS Observables](http://reactivex.io/rxjs/). +@Injectable() +export class YourService { + + constructor(private storageMap: StorageMap) {} + +} +``` + +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), +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. +If you are familiar to `Map`, we recommend to use only this service. + +```typescript +class StorageMap { + size: Observable; + get(index: string, schema?: JSONSchema): Observable {} + set(index: string, value: any): Observable {} + delete(index: string): Observable {} + clear(): Observable {} + has(index: string): Observable {} + keys(): Observable {} +} +``` + +## How to + +The following examples will show the 2 services for basic operations, +then stick to the `StorageMap` API. But except for methods which are specific to `StorageMap`, +you can always do the same with the `LocalStorage` API. ### Writing data ```typescript let user: User = { firstName: 'Henri', lastName: 'Bergson' }; +this.storageMap.set('user', user).subscribe(() => {}); +// or this.localStorage.setItem('user', user).subscribe(() => {}); ``` You can store any value, without worrying about serializing. But note that: -- storing `null` or `undefined` can cause issues in some browsers, so the item will be removed instead, -- you should stick to JSON data, ie. primitive types, arrays and literal objects. +- storing `null` or `undefined` makes no sense and can cause issues in some browsers, so the item will be removed instead, +- you should stick to JSON data, ie. primitive types, arrays and *literal* objects. `Map`, `Set`, `Blob` and other special structures can cause issues in some scenarios. See the [serialization guide](./docs/SERIALIZATION.md) for more details. @@ -95,32 +149,45 @@ See the [serialization guide](./docs/SERIALIZATION.md) for more details. To delete one item: ```typescript +this.storageMap.delete('user').subscribe(() => {}); +// or this.localStorage.removeItem('user').subscribe(() => {}); ``` To delete all items: ```typescript +this.storageMap.clear().subscribe(() => {}); +// or this.localStorage.clear().subscribe(() => {}); ``` ### Reading data ```typescript +this.storageMap.get('user').subscribe((user) => { + console.log(user); +}); +// or this.localStorage.getItem('user').subscribe((user) => { - user.firstName; // should be 'Henri' + console.log(user); }); ``` -Not finding an item is not an error, it succeeds but returns `null`. +Not finding an item is not an error, it succeeds but returns: +- `undefined` with `StorageMap` +```typescript +this.storageMap.get('notexisting').subscribe((data) => { + data; // undefined +}); +``` +- `null` with `LocalStorage` ```typescript this.localStorage.getItem('notexisting').subscribe((data) => { data; // null }); ``` -If you tried to store `undefined`, you'll get `null` too, as some storages don't allow `undefined`. - -Note you'll only get *one* value: the `Observable` is here for asynchronicity but is not meant to +Note you'll only get *one* value: the `Observable` is here for asynchrony but is not meant to emit again when the stored data is changed. And it's normal: if app data change, it's the role of your app to keep track of it, not of this lib. See [#16](https://github.com/cyrilletuzi/angular-async-local-storage/issues/16) for more context and [#4](https://github.com/cyrilletuzi/angular-async-local-storage/issues/4) @@ -128,14 +195,13 @@ for an example. ### Checking data -Don't forget it's client-side storage: **always check the data**, as it could have been forged or deleted. +Don't forget it's client-side storage: **always check the data**, as it could have been forged. You can use a [JSON Schema](http://json-schema.org/) to validate the data. ```typescript -this.localStorage.getItem('test', { type: 'string' }) -.subscribe({ - next: (user) => { /* Called if data is valid or `null` */ }, +this.storageMap.get('test', { type: 'string' }).subscribe({ + next: (user) => { /* Called if data is valid or `undefined` or `null` */ }, error: (error) => { /* Called if data is invalid */ }, }); ``` @@ -144,15 +210,16 @@ See the [full validation guide](./docs/VALIDATION.md) to see how to validate all ### Subscription -You *DO NOT* need to unsubscribe: the `Observable` autocompletes (like in the `HttpClient` service). +You *DO NOT* need to unsubscribe: the `Observable` autocompletes (like in the Angular `HttpClient` service). -But **you *DO* need to subscribe**, even if you don't have something specific to do after writing in local storage (because it's how RxJS Observables work). +But **you *DO* need to subscribe**, even if you don't have something specific to do after writing in storage +(because it's how RxJS `Observable`s work). ### Errors As usual, it's better to catch any potential error: ```typescript -this.localStorage.setItem('color', 'red').subscribe({ +this.storageMap.set('color', 'red').subscribe({ next: () => {}, error: (error) => {}, }); @@ -163,8 +230,8 @@ For read operations, you can also manage errors by providing a default value: import { of } from 'rxjs'; import { catchError } from 'rxjs/operators'; -this.localStorage.getItem('color').pipe( - catchError(() => of('red')) +this.storageMap.get('color').pipe( + catchError(() => of('red')), ).subscribe((result) => {}); ``` @@ -177,27 +244,25 @@ Could only happen when in `localStorage` fallback: Should only happen if data was corrupted or modified from outside of the lib: - `.getItem()`: data invalid against your JSON schema (`ValidationError` from this lib) -- any method when in `indexedDB`: database store has been deleted (`DOMException` with name `'NotFoundError'`) +- any method when in `indexedDB`: database store has been deleted (`DOMException` with name `NotFoundError`) Other errors are supposed to be catched or avoided by the lib, so if you were to run into an unlisted error, please file an issue. ### `Map`-like operations -Starting *with version >= 7.4*, in addition to the classic `localStorage`-like API, -this lib also provides some `Map`-like methods for advanced operations: - - `.keys()` method - - `.has(key)` method - - `.size` property +Starting *with version >= 8* of this lib, in addition to the classic `localStorage`-like API, +this lib also provides a `Map`-like API for advanced operations: + - `.keys()` + - `.has(key)` + - `.size` See the [documentation](./docs/MAP_OPERATIONS.md) for more info and some recipes. +For example, it allows to implement a multiple databases scenario. -### Collision - -If you have multiple apps on the same *sub*domain *and* you don't want to share data between them, -see the [prefix guide](./docs/COLLISION.md). +## Support -## Angular support +### Angular support We follow [Angular LTS support](https://angular.io/guide/releases), meaning we support Angular >= 6, until November 2019. @@ -207,22 +272,27 @@ This module supports [AoT pre-compiling](https://angular.io/guide/aot-compiler). This module supports [Universal server-side rendering](https://github.com/angular/universal) via a mock storage. -## Browser support +### Browser support -[All browsers supporting IndexedDB](http://caniuse.com/#feat=indexeddb), ie. **all current browsers** : +[All browsers supporting IndexedDB](https://caniuse.com/#feat=indexeddb), ie. **all current browsers** : Firefox, Chrome, Opera, Safari, Edge, and IE10+. See [the browsers support guide](./docs/BROWSERS_SUPPORT.md) for more details and special cases (like private browsing). -## Interoperability +### Collision + +If you have multiple apps on the same *sub*domain *and* you don't want to share data between them, +see the [prefix guide](./docs/COLLISION.md). + +### Interoperability -For interoperability when mixing this lib with direct usage of native APIs or other libs like `localforage` -(which doesn't make sense in most of cases), +For interoperability when mixing this lib with direct usage of native APIs or other libs like `localForage` +(which doesn't make sense in most cases), see the [interoperability documentation](./docs/INTEROPERABILITY.md). -## Changelog +### Changelog -[Changelog available here](https://github.com/cyrilletuzi/angular-async-local-storage/blob/master/CHANGELOG.md), and [migration guides here](./MIGRATION.md). +[Changelog available here](./CHANGELOG.md), and [migration guides here](./MIGRATION.md). ## License diff --git a/docs/BROWSERS_SUPPORT.md b/docs/BROWSERS_SUPPORT.md index 88abc658..63c1873c 100644 --- a/docs/BROWSERS_SUPPORT.md +++ b/docs/BROWSERS_SUPPORT.md @@ -1,23 +1,24 @@ # Browser support guide -This lib supports [all browsers supporting IndexedDB](http://caniuse.com/#feat=indexeddb), ie. **all current browsers** : +This lib supports [all browsers supporting `indexedDB`](http://caniuse.com/#feat=indexeddb), ie. **all current browsers** : Firefox, Chrome, Opera, Safari, Edge, and IE10+. -Local storage is required only for apps, and given that you won't do an app in older browsers, +Client-side storage is required only for apps, and given that you won't do an app in older browsers, current browsers support is far enough. -Even so, IE9 is supported but use native localStorage as a fallback, +Even so, IE9 is supported but use native `localStorage` as a fallback, so internal operations are synchronous (the public API remains asynchronous-like). -This module is not impacted by IE/Edge missing IndexedDB features. +This module is not impacted by IE/Edge missing `indexedDB` features. It also works in tools based on browser engines (like Electron) but not in non-browser tools (like NativeScript, see [#11](https://github.com/cyrilletuzi/angular-async-local-storage/issues/11)). ## Browsers restrictions -Be aware that local storage is limited in browsers when in private / incognito modes. Most browsers will delete the data when the private browsing session ends. -It's not a real issue as local storage is useful for apps, and apps should not be in private mode. +Be aware that `indexedDB` usage is limited in browsers when in private / incognito modes. +Most browsers will delete the data when the private browsing session ends. +It's not a real issue as client-side storage is only useful for apps, and apps should not be in private mode. In some scenarios, `indexedDB` is not available, so the lib fallbacks to (synchronous) `localStorage`. It happens in: - Firefox private mode (see [#26](https://github.com/cyrilletuzi/angular-async-local-storage/issues/26)) @@ -25,4 +26,7 @@ In some scenarios, `indexedDB` is not available, so the lib fallbacks to (synch - Safari, when in a cross-origin iframe (see [#42](https://github.com/cyrilletuzi/angular-async-local-storage/issues/42)) +If these scenarios are a concern for you, it impacts what you can store. +See the [serialization guide](./SERIALIZATION.md) for full details. + [Back to general documentation](../README.md) diff --git a/docs/COLLISION.md b/docs/COLLISION.md index 144a4c7c..0f9207cf 100644 --- a/docs/COLLISION.md +++ b/docs/COLLISION.md @@ -1,13 +1,16 @@ # Collision guide -`localStorage` and `IndexedDB` are restricted to the same ***sub*domain**, so no risk of collision in most cases. +**All client-side storages (both `indexedDB` and `localStorage`) are restricted to the same *sub*domain**, +so there is no risk of collision in most cases. *Only* if you have multiple apps on the same *sub*domain *and* you don't want to share data between them, -you need to change the config. +you need to add a prefix. ## Version -This is the up to date guide about validation for version >= 8. -The old guide for validation in versions < 8 is available [here](./COLLISION_BEFORE_V8.md). +**This is the up to date guide about collision for version >= 8 of this lib.** + +The old guide about collision in versions < 8 is available [here](./COLLISION_BEFORE_V8.md), +but is not recommended as there was breaking changes in v8. ## Configuration diff --git a/docs/COLLISION_BEFORE_V8.md b/docs/COLLISION_BEFORE_V8.md index 4732dc2a..a6b6063d 100644 --- a/docs/COLLISION_BEFORE_V8.md +++ b/docs/COLLISION_BEFORE_V8.md @@ -6,8 +6,8 @@ you need to add a prefix. ## Version -This is an outdated guide for prefix in versions < 8. -The up to date guide about prefix for version >= 8 is available [here](./COLLISION.md). +**This is an outdated guide about collision in versions < 8 of this lib.** +The up to date guide about collision for versions >= 8 is available [here](./COLLISION.md). ## Configuration diff --git a/docs/INTEROPERABILITY.md b/docs/INTEROPERABILITY.md index 56ea5da4..2128ecd5 100644 --- a/docs/INTEROPERABILITY.md +++ b/docs/INTEROPERABILITY.md @@ -1,17 +1,18 @@ # Interoperability In the vast majority of cases, you'll manage *independent* apps (ie. each having its own data), -and each using only one local storage API (e.g. a native API *or* this lib, but not both at the same time in one app). +and each using only one client-side storage API +(e.g. a native API *or* this lib, but not both at the same time in one app). In some special cases, it could happen: - you share the same data between multiple apps on the same subdomain, -but not all apps are built with the same framework, so each one will use a different local storage API -(e.g. an Angular app using this lib and a non-Angular app using `localForage` lib) +but not all apps are built with the same framework, so each one will use a different client-side storage API +(e.g. an Angular app using this lib and a non-Angular app using the `localForage` lib) - while **not recommended**, you could also mix several APIs inside the same app (e.g. mixing native `indexedDB` *and* this lib). If you're in one of these cases, *please read this guide carefully*, -as there are important things to do and to be aware to achieve interoperability. +as there are important things to do and to be aware of to achieve interoperability. ## Requirements @@ -20,7 +21,7 @@ Interoperability can be achieved: - **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 go would mean to **lose all previously stored data**. + - changing configuration on the fly would mean to **lose all previously stored data**. ## Configuration @@ -30,7 +31,7 @@ Note: ### `indexedDB` database and object store names -When storing in `indexedDb`, names are used for the database and the object store, +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, @@ -58,7 +59,8 @@ export class AppModule {} ### `localStorage` prefix -In some cases, `indexedDB` is not available, and libs fallback to `localStorage`. +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: @@ -106,16 +108,16 @@ or when the version change (but this case doesn't happen in this lib). Then, you need to ensure: - you use the same database `version` as the lib (none or `1`), -- the store is created by: +- 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). ### Limitation with `undefined` Most librairies (like this one and `localforage`) will prevent you to store `undefined`, -by always returning `null` instead of `undefined`, due to technical issues in the native APIs. +due to technical issues in the native APIs. -But if you use the native APIs (`localStorage` and `indexedDb`) directly, +But if you use the native APIs (`localStorage` and `indexedDB`) directly, you could manage to store `undefined`, but it will then throw exceptions in some cases. So **don't store `undefined`**. @@ -123,4 +125,6 @@ So **don't store `undefined`**. ### `indexedDB` keys This lib only allows `string` keys, while `indexedDB` allows some other key types. -So if using this lib `keys()` method, all keys will be converted to `string`. +So if you use this lib `keys()` method, all keys will be converted to a `string`. + +[Back to general documentation](../README.md) diff --git a/docs/MAP_OPERATIONS.md b/docs/MAP_OPERATIONS.md index 2076fa1b..e80fcc00 100644 --- a/docs/MAP_OPERATIONS.md +++ b/docs/MAP_OPERATIONS.md @@ -1,28 +1,46 @@ # `Map`-like operations -Starting with version >= 7.4, in addition to the classic `localStorage`-like API, +Starting with version >= 8 of this lib, in addition to the classic `localStorage`-like API, this lib also provides a partial `Map`-like API for advanced operations. -## `.keys()` method - -An `Observable` returning an array of keys: +To use it: ```typescript -this.localStorage.keys().subscribe((keys) => { +import { StorageMap } from '@ngx-pwa/local-storage'; + +export class AngularComponentOrService { + + constructor(private storageMap: StorageMap) {} + +} +``` - console.log(keys); +## `.keys()` method + +An `Observable` iterating over keys in storage: +```typescript +this.storageMap.keys().subscribe({ + next: (key) => { + console.log(key); + }, + complete: () => { + console.log('Done'); + }, }); ``` -If there is no keys, an empty array is returned. +Note this is an *iterating* `Observable`: +- if there is no key, the `next` callback will *not* be invoked, +- if you need to wait the whole operation to end, be sure to act in the `complete` callback, +as this `Observable` can emit several values and so will invoke the `next` callback several times. ## `.has(key)` method Gives you an `Observable` telling you if a key exists in storage: ```typescript -this.localStorage.has('someindex').subscribe((result) => { +this.storageMap.has('someindex').subscribe((result) => { if (result) { console.log('The key exists :)'); @@ -35,10 +53,10 @@ this.localStorage.has('someindex').subscribe((result) => { ## `.size` property -Number of items stored in local storage. +Number of items stored in storage. ```typescript -this.localStorage.size.subscribe((size) => { +this.storageMap.size.subscribe((size) => { console.log(size); @@ -47,7 +65,8 @@ this.localStorage.size.subscribe((size) => { ## Other methods -`.values()` and `.entries()` have not been implemented on purpose, because it would not be a good idea for performance. +`.values()` and `.entries()` have not been implemented on purpose, +because it has few use cases and it would not be a good idea for performance. But you can easily do your own implementation via `keys()`. ## Recipes @@ -63,30 +82,25 @@ Let's say you stored: You can then delete only app data: ```typescript -import { from } from 'rxjs'; import { filter, mergeMap } from 'rxjs/operators'; -this.localStorageService.keys().pipe( - - /* Transform the array of keys in an Observable iterating over the array. - * Why not iterating on the array directly with a `for... of` or `forEach`? - * Because the next step (removing the item) will be asynchronous, - * and we want to keep track of the process to know when everything is over */ - mergeMap((keys) => from(keys)), +this.storageMap.keys().pipe( /* Keep only keys starting with 'app_' */ filter((key) => key.startsWith('app_')), /* Remove the item for each key */ - mergeMap((key) => localStorageService.removeItem(key)) + mergeMap((key) => this.storageMap.delete(key)) ).subscribe({ complete: () => { /* Note we don't act in the classic success callback as it will be trigerred for each key, - * while we want to act only when all the operations are done */ + * while we want to act only when all the operations are done */ console.log('Done!'); } }); ``` + +[Back to general documentation](../README.md) diff --git a/docs/MIGRATION_TO_V6.md b/docs/MIGRATION_TO_V6.md index bf6e22b4..34fd0e19 100644 --- a/docs/MIGRATION_TO_V6.md +++ b/docs/MIGRATION_TO_V6.md @@ -17,7 +17,7 @@ You can easily and quickly migrate by doing **a global search/replace of**: In most cases, you're probably only using the first one. -## No more NgModule +## No more `NgModule` `LocalStorageModule` is no longer needed and so it is removed. Services are provided directly when injected in Angular >=6. diff --git a/docs/MIGRATION_TO_V7.md b/docs/MIGRATION_TO_V7.md index f45d51bb..a2233331 100644 --- a/docs/MIGRATION_TO_V7.md +++ b/docs/MIGRATION_TO_V7.md @@ -6,8 +6,8 @@ so you understand exactly what you are doing. But relax, ## WARNING -Version 7 of this library tried to take a huge step forward by forcing validation, for security and error management. -Unfortunately, unforeseen issues happened, some very bad as they are beyond our control +Version 7 of this library tried to take a huge step forward by enforcing validation, for security and error management. +Unfortunately, unforeseen issues happened, some very bad as they were beyond our control (like [#64](https://github.com/cyrilletuzi/angular-async-local-storage/issues/64)). Version 8 achieves the goal we tried in v7 the right way. Everything has been cleaned and things are a lot easier for you. diff --git a/docs/MIGRATION_TO_V8.md b/docs/MIGRATION_TO_V8.md index bb55d1c7..46d0096e 100644 --- a/docs/MIGRATION_TO_V8.md +++ b/docs/MIGRATION_TO_V8.md @@ -2,13 +2,28 @@ ## Foreword -Just a word to state the decision to make the following breaking changes was beyond my control. -It follows a regression in TypeScript 3.2 (see [#64](https://github.com/cyrilletuzi/angular-async-local-storage/issues/64)). +Version 8 of this lib is a big update for 2 reasons: -The worst part was the TS team support: the regression would be solved only in TypeScript 3.4. -As a consequence, decision was made to refactor `JSONSchema` to not be dependent on unreliable edgy TypeScript behavior anymore. +1. One was beyond my control: it follows a regression in TypeScript 3.2 (see [#64](https://github.com/cyrilletuzi/angular-async-local-storage/issues/64)). The worst part was the TS team support: +the regression would be solved only in TypeScript 3.4. +As a consequence, decision was made to change the `JSONSchema` interface +to not be dependent on unreliable edgy TypeScript behavior anymore. -The good part: the changes also allowed a lot of simplications for you. :) +2. This lib was born some years ago with Angular 2. What was a little project grew up a lot +and is now downloaded dozens of thousands of times on `npm` each week. +It's now the first Angular library for client-side storage according to [ngx.tools](https://ngx.tools/#/search?q=local%20storage). +It was time to do a full review (and rewrite). + +So yes, there are a lot of changes, but it's for good: +- many things (like validation) have been simplified +- more advanced and some long-awaited features (like watching an item) are now possible +- errors are managed in a better way, fixing many edgy cases +- and more! + +But relax, to ease the migration: +- **there are few breaking changes, so updating to v8 should be easy** +- but there are a lot of deprecations, so preparing for v9 +(were deprecations of v8 will be removed) will take more time. ## Previous migrations @@ -152,6 +167,62 @@ Auto-subscription methods were added for simplicity, but they were risky shortcu So `setItemSubscribe()`, `removeItemSubscribe()` and `clearSubscribe()` are removed: subscribe manually. +## Feature: new `StorageMap` service + +In addition to the `LocalStorage` service, a new `StorageMap` service has been added: + +```typescript +import { StorageMap } from '@ngx-pwa/local-storage'; + +@Injectable() +export class YourService { + + constructor(private storageMap: StorageMap) {} + +} +``` + +This service API follows the +[native `Map` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map), +except it's asynchronous via [RxJS `Observable`s](http://reactivex.io/rxjs/). + +```typescript +class StorageMap { + size: Observable; + get(index: string, schema?: JSONSchema): Observable {} + set(index: string, value: any): Observable {} + delete(index: string): Observable {} + clear(): Observable {} + has(index: string): Observable {} + keys(): Observable {} +} +``` + +It does the same thing as the `LocalStorage` service, but also allows more advanced operations. +If you are familiar to `Map`, we recommend to use only this service. + +So the following examples will use this new service to help you familiarize with it. +But don't worry: the `LocalStorage` service is still there, +and everything can still be done in the same way with the `LocalStorage` service: +- `this.storageMap.get()` is the same as `this.localStorage.getItem()` +- `this.storageMap.set()` is the same as `this.localStorage.setItem()` +- `this.storageMap.delete()` is the same as `this.localStorage.removeItem()` +- `this.storageMap.clear()` is the same as `this.localStorage.clear()` + +Just one difference on the return value when the requested key does not exist: +- `undefined` with `StorageMap` +```typescript +this.storageMap.get('notexisting').subscribe((data) => { + data; // undefined +}); +``` +- `null` with `LocalStorage` +```typescript +this.localStorage.getItem('notexisting').subscribe((data) => { + data; // null +}); +``` + ## The good part: simplication changes The following changes are not required but recommended. @@ -159,7 +230,7 @@ The following changes are not required but recommended. ### Easier API for `getItem()` -`getItem()` API has been simplified: the secong argument is now directly the schema for validation. +`getItem()` / `get()` API has been simplified: the secong argument is now directly the schema for validation. Before v8: ```typescript @@ -168,14 +239,15 @@ this.localStorage.getItem('test', { schema: { type: 'string' } }) Since v8: ```typescript -this.localStorage.getItem('test', { type: 'string' }) +this.storageMap.get('test', { type: 'string' }) ``` -The previous API may be removed in v9. So this change is strongly recommended, but you have time. +Passing the schema via an object is deprecated and will be removed in v9. +So this change is strongly recommended, but you have time. ### Cast now inferred! -The previous change allows that the returned type of `getItem()` is now inferred for basic types (`string`, `number`, `boolean`) +The previous change allows that the returned type of `getItem()` / `get()` is now inferred for basic types (`string`, `number`, `boolean`) and arrays of basic types (`string[]`, `number[]`, `boolean[]`). Before v8: @@ -191,7 +263,7 @@ this.localStorage.getItem('test', { schema: { type: 'string' } }).subscr Since v8: ```typescript -this.localStorage.getItem('test', { type: 'string' }).subscribe((data) => { +this.storageMap.get('test', { type: 'string' }).subscribe((data) => { data; // string :D }); ``` @@ -204,6 +276,42 @@ Version 8 is a fresh new start, where everything has been cleaned. But as the pr **the following features still work in v8 but are deprecated**. They will be removed in v9. So there's no hurry, but as soon as you have some time, do the following changes too. +### `Map`-like operations + +`Map`-like methods introduced in v7.2 has been moved to the new `StorageMap` service: +- `.keys()` +- `.has(key)` +- `.size` + +Before v8: +```typescript +this.localStorage.has('somekey').subscribe((result) => {}); +``` + +Since v8: +```typescript +this.storageMap.has('somekey').subscribe((result) => {}); +``` + +They are still in the `LocalStorage` service but deprecated. +They will be removed from this service in v9. + +### `keys()` is now iterating + +While the deprecated `keys()` in `LocalStorage` service give all the keys at once as an array, +`keys()` in the new `StorageMap` service is now *iterating* over the keys, +which is better for performance and easier to managed advanced operations +(like the multiple databases scenario). + +Follow the updated [`Map` operations guide](./MAP_OPERATIONS.md). + +While *not* recommended, you can get the same behavior as before by doing this: +```typescript +import { toArray } from 'rxjs/operators'; + +this.storageMap.keys().pipe(toArray()).subscribe((keys) => {}); +``` + ### Use the generic `JSONSchema` Now the `JSONSchema` interface has been refactored, just use this one. @@ -247,7 +355,7 @@ import { localStorageProviders } from '@ngx-pwa/local-storage'; export class AppModule {} ``` -**Be very careful while changing this, as an error could mean the loss of all previously stored data.** +**Be very careful while changing this in applications already deployed in production, as an error could mean the loss of all previously stored data.** ## More documentation diff --git a/docs/OLD_MODULE.md b/docs/OLD_MODULE.md index a7c5b6de..82e47a97 100644 --- a/docs/OLD_MODULE.md +++ b/docs/OLD_MODULE.md @@ -1,7 +1,7 @@ # Module required in versions 4 & 5 An additional setup step is required **for *versions 4 & 5 only***: -**include the `LocalStorageModule`** in your app root module (just once, do NOT re-import it in your submodules). +**include the `LocalStorageModule`** in your app root module (just once, do **not** re-import it in your submodules). ```typescript import { LocalStorageModule } from '@ngx-pwa/local-storage'; diff --git a/docs/SERIALIZATION.md b/docs/SERIALIZATION.md index 08318fff..0ea97762 100644 --- a/docs/SERIALIZATION.md +++ b/docs/SERIALIZATION.md @@ -3,8 +3,9 @@ ## JSON serialization In most cases, this library uses `indexedDB` storage, which allows any value type. -But in special cases, like in Firefox / IE private mode, the library will fall back to `localStorage`, -where JSON serialization will happen. +But in special cases (like in Firefox / IE private mode, +see the [browser support guide](./BROWSERS_SUPPORT.md) for details), +the library will fall back to `localStorage`, where JSON serialization will happen. Everything can be serialized (`JSON.stringify()`), but when you unserialize (`JSON.parse()`), you'll only get a JSON, ie. a primitive type, an array or a *literal* object. @@ -17,7 +18,8 @@ So, it's safer to **stick to JSON-compatible values**. ## Validation Also, this library uses JSON schemas for validation, which can only describe JSON-compatible values. -So if you're storing special structures like `Map`, `Set` or `Blob`, you'll have to manage your own validation (which is possible but painful). +So if you're storing special structures like `Map`, `Set` or `Blob`, +you'll have to manage your own validation (which is possible but painful). ## Examples @@ -29,7 +31,7 @@ Here are some examples of the recommended way to store special structures. const someMap = new Map([['hello', 1], ['world', 2]]); /* Writing */ -this.localStorage.setItem('test', Array.from(someMap)).subscribe(); +this.storageMap.set('test', Array.from(someMap)).subscribe(); /* Reading */ const schema: JSONSchema = { @@ -43,7 +45,7 @@ const schema: JSONSchema = { }, }; -this.localStorage.getItem<[string, number][]>('test', schema).pipe( +this.storageMap.get<[string, number][]>('test', schema).pipe( map((data) => new Map(data)), ).subscribe((data) => { data.get('hello'); // 1 @@ -56,7 +58,7 @@ this.localStorage.getItem<[string, number][]>('test', schema).pipe( const someSet = new Set(['hello', 'world']); /* Writing */ -this.localStorage.setItem('test', Array.from(someSet)).subscribe(); +this.storageMap.set('test', Array.from(someSet)).subscribe(); /* Reading */ const schema: JSONSchema = { @@ -64,7 +66,7 @@ const schema: JSONSchema = { items: { type: 'string' }, }; -this.localStorage.getItem('test', schema).pipe( +this.storageMap.get('test', schema).pipe( map((data) => new Set(data)), ).subscribe((data) => { data.has('hello'); // true diff --git a/docs/VALIDATION.md b/docs/VALIDATION.md index 6f80461f..9f7a5b5c 100644 --- a/docs/VALIDATION.md +++ b/docs/VALIDATION.md @@ -2,8 +2,18 @@ ## Version -This is the up to date guide about validation for version >= 8. -The old guide for validation in versions < 8 is available [here](./VALIDATION_BEFORE_V8.md). +**This is the up to date guide about validation for versions >= 8 of this lib.** +The old guide for validation in versions < 8 is available [here](./VALIDATION_BEFORE_V8.md), +but is not recommended as there was breaking changes in v8. + +## Examples + +The examples will use the new `StorageMap` service. +But everything in this guide can be done in the same way with the `LocalStorage` service: +- `this.storageMap.get()` is the same as `this.localStorage.getItem()` +- `this.storageMap.set()` is the same as `this.localStorage.setItem()` +- `this.storageMap.delete()` is the same as `this.localStorage.removeItem()` +- `this.storageMap.clear()` is the same as `this.localStorage.clear()` ## Why validation? @@ -20,7 +30,7 @@ Then, **any data coming from client-side storage should be checked before used** You can see this as an equivalent to the DTD in XML, the Doctype in HTML or interfaces in TypeScript. It can have many uses (it's why you have autocompletion in some JSON files in Visual Studio Code). -**In this lib, JSON schemas are used to validate the data retrieved from local storage.** +**In this lib, JSON schemas are used to validate the data retrieved from client-side storage.** ## How to validate simple data @@ -30,52 +40,52 @@ as you'll see the more complex it is, the more complex is validation too. ### Boolean ```typescript -this.localStorage.getItem('test', { type: 'boolean' }) +this.storageMap.get('test', { type: 'boolean' }) ``` ### Integer ```typescript -this.localStorage.getItem('test', { type: 'integer' }) +this.storageMap.get('test', { type: 'integer' }) ``` ### Number ```typescript -this.localStorage.getItem('test', { type: 'number' }) +this.storageMap.get('test', { type: 'number' }) ``` ### String ```typescript -this.localStorage.getItem('test', { type: 'string' }) +this.storageMap.get('test', { type: 'string' }) ``` ### Arrays ```typescript -this.localStorage.getItem('test', { +this.storageMap.get('test', { type: 'array', items: { type: 'boolean' }, }) ``` ```typescript -this.localStorage.getItem('test', { +this.storageMap.get('test', { type: 'array', items: { type: 'integer' }, }) ``` ```typescript -this.localStorage.getItem('test', { +this.storageMap.get('test', { type: 'array', items: { type: 'number' }, }) ``` ```typescript -this.localStorage.getItem('test', { +this.storageMap.get('test', { type: 'array', items: { type: 'string' }, }) @@ -90,7 +100,7 @@ In special cases, it can be useful to use arrays with values of different types. It's called tuples in TypeScript. For example: `['test', 1]` ```typescript -this.localStorage.getItem('test', { +this.storageMap.get('test', { type: 'array', items: [ { type: 'string' }, @@ -124,7 +134,7 @@ const schema: JSONSchema = { required: ['firstName', 'lastName'] }; -this.localStorage.getItem('test', schema) +this.storageMap.get('test', schema) ``` What's expected for each property is another JSON schema. @@ -136,10 +146,10 @@ see the [serialization guide](./SERIALIZATION.md). ### Why a schema *and* a cast? -You may ask why we have to define a TypeScript cast with `getItem()` *and* a JSON schema with `schema`. +You may ask why we have to define a TypeScript cast with `get()` *and* a JSON schema with `schema`. It's because they happen at different steps: -- a cast (`getItem()`) just says "TypeScript, trust me, I'm telling you it will be a `User`", but it only happens at *compilation* time (it won't be checked at runtime) +- a cast (`get()`) just says "TypeScript, trust me, I'm telling you it will be a `User`", but it only happens at *compilation* time (it won't be checked at runtime) - the JSON schema (`schema`) will be used at *runtime* when getting data in local storage for real. So they each serve a different purpose: @@ -170,7 +180,7 @@ The lib can't check that. For example: ```typescript -this.localStorage.getItem('test', { +this.storageMap.get('test', { type: 'number', maximum: 5 }) @@ -186,7 +196,7 @@ this.localStorage.getItem('test', { For example: ```typescript -this.localStorage.getItem('test', { +this.storageMap.get('test', { type: 'string', maxLength: 10 }) @@ -200,7 +210,7 @@ this.localStorage.getItem('test', { For example: ```typescript -this.localStorage.getItem('test', { +this.storageMap.get('test', { type: 'array', items: { type: 'string' }, maxItems: 5 @@ -232,23 +242,34 @@ const schema: JSONSchema = { } }; -this.localStorage.getItem('test', schema) +this.storageMap.get('test', schema) ``` -## Errors vs. `null` +## Errors vs. `undefined` / `null` If validation fails, it'll go in the error callback: ```typescript -this.localStorage.getItem('existing', { type: 'string' }) +this.storageMap.get('existing', { type: 'string' }) .subscribe({ - next: (result) => { /* Called if data is valid or null */ }, + next: (result) => { /* Called if data is valid or null or undefined */ }, error: (error) => { /* Called if data is invalid */ }, }); ``` -But as usual (like when you do a database request), not finding an item is not an error. It succeeds but returns `null`. +But as usual (like when you do a database request), not finding an item is not an error. +It succeeds but returns: + +- `undefined` with `StorageMap` +```typescript +this.storageMap.get('notExisting', { type: 'string' }) +.subscribe({ + next: (result) => { result; /* undefined */ }, + error: (error) => { /* Not called */ }, +}); +``` +- `null` with `LocalStorage` ```typescript this.localStorage.getItem('notExisting', { type: 'string' }) .subscribe({ diff --git a/docs/VALIDATION_BEFORE_V8.md b/docs/VALIDATION_BEFORE_V8.md index 109b82c3..7f41c7cc 100644 --- a/docs/VALIDATION_BEFORE_V8.md +++ b/docs/VALIDATION_BEFORE_V8.md @@ -2,7 +2,7 @@ ## Version -This is an outdated guide for validation in versions < 8. +**This is an outdated guide for validation in versions < 8 of this lib.** The up to date guide about validation for version >= 8 is available [here](./VALIDATION.md). ## Why validation? diff --git a/projects/demo-e2e/src/app.e2e-spec.ts b/projects/demo-e2e/src/app.e2e-spec.ts index 3a52edf2..01ca9ccd 100644 --- a/projects/demo-e2e/src/app.e2e-spec.ts +++ b/projects/demo-e2e/src/app.e2e-spec.ts @@ -47,14 +47,6 @@ describe('public api', () => { }); - it('size()', async () => { - - const data = Number.parseInt(await $('#size').getText(), 10); - - expect(data).toBeGreaterThan(1); - - }); - it('length', async () => { const data = Number.parseInt(await $('#length').getText(), 10); diff --git a/projects/demo/src/app/app.component.ts b/projects/demo/src/app/app.component.ts index be981b6e..5efca92d 100644 --- a/projects/demo/src/app/app.component.ts +++ b/projects/demo/src/app/app.component.ts @@ -1,7 +1,8 @@ import { Component, OnInit } from '@angular/core'; import { Observable, of } from 'rxjs'; -import { LocalStorage, JSONSchema } from '@ngx-pwa/local-storage'; -import { catchError, mergeMap } from 'rxjs/operators'; +import { catchError, mergeMap, toArray } from 'rxjs/operators'; +import { LocalStorage, StorageMap, JSONSchema } from '@ngx-pwa/local-storage'; + import { DataService } from './data.service'; interface Data { @@ -15,7 +16,6 @@ interface Data {

{{schemaError$ | async}}

{{removeItem$ | async}}

{{clear}}

-

{{size$ | async}}

{{length$ | async}}

{{keys$ | async | json}}

Should not be seen

@@ -33,9 +33,9 @@ export class AppComponent implements OnInit { length$!: Observable; keys$!: Observable; has$!: Observable; - service$!: Observable; + service$!: Observable; - constructor(private localStorage: LocalStorage, private dataService: DataService) {} + constructor(private localStorage: LocalStorage, private storageMap: StorageMap, private dataService: DataService) {} ngOnInit() { @@ -71,22 +71,18 @@ export class AppComponent implements OnInit { mergeMap(() => this.localStorage.getItem('removeItem', { type: 'string' })), ); - this.size$ = this.localStorage.setItem('size1', 'test').pipe( - mergeMap(() => this.localStorage.setItem('size2', 'test')), - mergeMap(() => this.localStorage.size), - ); - this.length$ = this.localStorage.setItem('size1', 'test').pipe( mergeMap(() => this.localStorage.setItem('size2', 'test')), mergeMap(() => this.localStorage.length), ); - this.keys$ = this.localStorage.setItem('keys', 'test').pipe( - mergeMap(() => this.localStorage.keys()), + this.keys$ = this.storageMap.set('keys', 'test').pipe( + mergeMap(() => this.storageMap.keys()), + toArray(), ); this.has$ = this.localStorage.setItem('has', 'test').pipe( - mergeMap(() => this.localStorage.has('has')), + mergeMap(() => this.storageMap.has('has')), ); this.service$ = this.dataService.data$; diff --git a/projects/demo/src/app/data.service.ts b/projects/demo/src/app/data.service.ts index 012da282..b1290758 100644 --- a/projects/demo/src/app/data.service.ts +++ b/projects/demo/src/app/data.service.ts @@ -1,18 +1,18 @@ import { Injectable } from '@angular/core'; -import { LocalStorage } from '@ngx-pwa/local-storage'; import { Observable } from 'rxjs'; import { mergeMap } from 'rxjs/operators'; +import { StorageMap } from '@ngx-pwa/local-storage'; @Injectable({ providedIn: 'root' }) export class DataService { - data$: Observable; + data$: Observable; - constructor(private localStorage: LocalStorage) { - this.data$ = this.localStorage.setItem('serviceTest', 'service').pipe( - mergeMap(() => this.localStorage.getItem('serviceTest', { type: 'string' })), + constructor(private storageMap: StorageMap) { + this.data$ = this.storageMap.set('serviceTest', 'service').pipe( + mergeMap(() => this.storageMap.get('serviceTest', { type: 'string' })), ); } diff --git a/projects/iframe/src/app/app.component.ts b/projects/iframe/src/app/app.component.ts index 807a6441..dd945f9b 100644 --- a/projects/iframe/src/app/app.component.ts +++ b/projects/iframe/src/app/app.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; -import { LocalStorage, JSONSchema } from '@ngx-pwa/local-storage'; +import { StorageMap, JSONSchema } from '@ngx-pwa/local-storage'; import { switchMap } from 'rxjs/operators'; interface Data { @@ -17,9 +17,9 @@ interface Data { export class AppComponent implements OnInit { title = 'LocalStorage'; - data$: Observable | null = null; + data$!: Observable; - constructor(private localStorage: LocalStorage) {} + constructor(private storageMap: StorageMap) {} ngOnInit() { @@ -33,7 +33,9 @@ export class AppComponent implements OnInit { } }; - this.data$ = this.localStorage.setItem(key, { title: this.title }).pipe(switchMap(() => this.localStorage.getItem(key, schema))); + this.data$ = this.storageMap.set(key, { title: this.title }).pipe( + switchMap(() => this.storageMap.get(key, schema)) + ); } diff --git a/projects/localforage/src/app/app.component.ts b/projects/localforage/src/app/app.component.ts index 4b8e3837..5f00b98e 100644 --- a/projects/localforage/src/app/app.component.ts +++ b/projects/localforage/src/app/app.component.ts @@ -1,6 +1,6 @@ import * as localForage from 'localforage'; import { Component, OnInit } from '@angular/core'; -import { LocalStorage } from '@ngx-pwa/local-storage'; +import { StorageMap } from '@ngx-pwa/local-storage'; @Component({ selector: 'app-root', @@ -14,7 +14,7 @@ export class AppComponent implements OnInit { title = 'not ok'; - constructor(private localStorage: LocalStorage) {} + constructor(private storageMap: StorageMap) {} ngOnInit() { @@ -23,7 +23,7 @@ export class AppComponent implements OnInit { localForage.setItem(key, value).then(() => { - this.localStorage.getItem(key, { type: 'string' }).subscribe((result) => { + this.storageMap.get(key, { type: 'string' }).subscribe((result) => { this.title = result || 'not ok'; }); diff --git a/projects/localforage/src/app/lazy/page/page.component.ts b/projects/localforage/src/app/lazy/page/page.component.ts index 26a21ff8..48fc26f1 100644 --- a/projects/localforage/src/app/lazy/page/page.component.ts +++ b/projects/localforage/src/app/lazy/page/page.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { LocalStorage } from '@ngx-pwa/local-storage'; +import { StorageMap } from '@ngx-pwa/local-storage'; @Component({ template: ` @@ -10,11 +10,11 @@ export class PageComponent implements OnInit { text = 'not ok'; - constructor(private localStorage: LocalStorage) {} + constructor(private storageMap: StorageMap) {} ngOnInit() { - this.localStorage.getItem('key', { type: 'string' }).subscribe((result) => { + this.storageMap.get('key', { type: 'string' }).subscribe((result) => { this.text = result || 'not ok'; }); diff --git a/projects/ngx-pwa/local-storage/src/lib/databases/exceptions.ts b/projects/ngx-pwa/local-storage/src/lib/databases/exceptions.ts new file mode 100644 index 00000000..b0cdbc09 --- /dev/null +++ b/projects/ngx-pwa/local-storage/src/lib/databases/exceptions.ts @@ -0,0 +1,11 @@ +/** + * Exception message when `indexedDB` is not working + */ +export const IDB_BROKEN_ERROR = 'indexedDB is not working'; + +/** + * Exception raised when `indexedDB` is not working + */ +export class IDBBrokenError extends Error { + message = IDB_BROKEN_ERROR; +} diff --git a/projects/ngx-pwa/local-storage/src/lib/databases/index.ts b/projects/ngx-pwa/local-storage/src/lib/databases/index.ts new file mode 100644 index 00000000..b2a51451 --- /dev/null +++ b/projects/ngx-pwa/local-storage/src/lib/databases/index.ts @@ -0,0 +1,5 @@ +export { LocalDatabase } from './local-database'; +export { IndexedDBDatabase } from './indexeddb-database'; +export { LocalStorageDatabase } from './localstorage-database'; +export { MemoryDatabase } from './memory-database'; +export { IDB_BROKEN_ERROR, IDBBrokenError } from './exceptions'; 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 f390d701..52cb054b 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 @@ -1,13 +1,13 @@ import { Injectable, Inject } from '@angular/core'; import { Observable, ReplaySubject, fromEvent, of, throwError, race } from 'rxjs'; -import { map, mergeMap, first, tap, filter } from 'rxjs/operators'; +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 } from '../tokens'; -import { IDBBrokenError } from '../exceptions'; @Injectable({ providedIn: 'root' @@ -92,9 +92,9 @@ export class IndexedDBDatabase implements LocalDatabase { /** * Gets an item value in our `indexedDB` store * @param key The item's key - * @returns The item's value if the key exists, `null` otherwise, wrapped in an RxJS `Observable` + * @returns The item's value if the key exists, `undefined` otherwise, wrapped in an RxJS `Observable` */ - getItem(key: string): Observable { + get(key: string): Observable { /* Open a transaction in read-only mode */ return this.transaction('readonly').pipe( @@ -122,8 +122,8 @@ export class IndexedDBDatabase implements LocalDatabase { } - /* Return `null` if the value is `null` or `undefined` */ - return null; + /* Return `undefined` if the value is empty */ + return undefined; }); @@ -140,11 +140,11 @@ export class IndexedDBDatabase implements LocalDatabase { * @param data The item's value * @returns An RxJS `Observable` to wait the end of the operation */ - setItem(key: string, data: any): Observable { + set(key: string, data: any): Observable { - /* Storing `undefined` or `null` in `localStorage` can cause issues in some browsers so removing item instead */ - if ((data === undefined) || (data === null)) { - return this.removeItem(key); + /* Storing `undefined` in `indexedDb` can cause issues in some browsers so removing item instead */ + if (data === undefined) { + return this.delete(key); } /* Open a transaction in write mode */ @@ -175,7 +175,7 @@ export class IndexedDBDatabase implements LocalDatabase { store.put(dataToStore, key); /* Manage success and error events, and map to `true` */ - return this.requestEventsAndMapTo(request2, () => true); + return this.requestEventsAndMapTo(request2, () => undefined); }), ); @@ -191,7 +191,7 @@ export class IndexedDBDatabase implements LocalDatabase { * @param key The item's key * @returns An RxJS `Observable` to wait the end of the operation */ - removeItem(key: string): Observable { + delete(key: string): Observable { /* Open a transaction in write mode */ return this.transaction('readwrite').pipe( @@ -201,11 +201,11 @@ export class IndexedDBDatabase implements LocalDatabase { const request = store.delete(key); /* Manage success and error events, and map to `true` */ - return this.requestEventsAndMapTo(request, () => true); + return this.requestEventsAndMapTo(request, () => undefined); }), /* The observable will complete after the first value */ - first() + first(), ); } @@ -214,7 +214,7 @@ export class IndexedDBDatabase implements LocalDatabase { * Deletes all items from our `indexedDB` objet store * @returns An RxJS `Observable` to wait the end of the operation */ - clear(): Observable { + clear(): Observable { /* Open a transaction in write mode */ return this.transaction('readwrite').pipe( @@ -224,7 +224,7 @@ export class IndexedDBDatabase implements LocalDatabase { const request = store.clear(); /* Manage success and error events, and map to `true` */ - return this.requestEventsAndMapTo(request, () => true); + return this.requestEventsAndMapTo(request, () => undefined); }), /* The observable will complete */ @@ -235,45 +235,43 @@ export class IndexedDBDatabase implements LocalDatabase { /** * Get all the keys in our `indexedDB` store - * @returns An RxJS `Observable` containing all the keys + * @returns An RxJS `Observable` iterating on each key */ - keys(): Observable { + keys(): Observable { /* Open a transaction in read-only mode */ return this.transaction('readonly').pipe( + /* `first()` is used as the final operator in other methods to complete the `Observable` + * (as it all starts from a `ReplaySubject` which never ends), + * but as this method is iterating over multiple values, `first()` **must** be used here */ + first(), mergeMap((store) => { - if ('getAllKeys' in store) { - - /* Request all keys in store */ - const request = store.getAllKeys(); - - /* Manage success and error events, and map to result - * This lib only allows string keys, but user could have added other types of keys from outside */ - return this.requestEventsAndMapTo(request, () => request.result.map((key) => key.toString())) ; - - } else { - - /* `getAllKey()` is better but only available in `indexedDB` v2 (Chrome >= 58, missing in IE/Edge) - * Fixes https://github.com/cyrilletuzi/angular-async-local-storage/issues/69 */ - - /* Open a cursor on the store */ - const request = (store as IDBObjectStore).openCursor(); - - /* Listen to success event */ - const success$ = this.getKeysFromCursor(request); - - /* Listen to error event and if so, throw an error */ - const error$ = this.errorEvent(request); + /* Note: a previous version of the API used `getAllKey()`, + * but it's only available in `indexedDB` v2 (Chrome >= 58, missing in IE/Edge) + * Fixes https://github.com/cyrilletuzi/angular-async-local-storage/issues/69 */ + + /* Open a cursor on the store */ + const request = (store as IDBObjectStore).openCursor(); + + /* Listen to success event */ + const success$ = this.successEvent(request).pipe( + /* Stop the `Observable` when the cursor is `null` */ + takeWhile(() => (request.result !== null)), + /* This lib only allows string keys, but user could have added other types of keys from outside + * It's OK to cast as the cursor as been tested in the previous operator */ + map(() => (request.result as IDBCursorWithValue).key.toString()), + /* Iterate on the cursor */ + tap(() => { (request.result as IDBCursorWithValue).continue(); }), + ); - /* Choose the first event to occur */ - return race([success$, error$]); + /* Listen to error event and if so, throw an error */ + const error$ = this.errorEvent(request); - } + /* Choose the first event to occur */ + return race([success$, error$]); }), - /* The observable will complete */ - first(), ); } @@ -488,38 +486,4 @@ export class IndexedDBDatabase implements LocalDatabase { } - /** - * Get all keys from store from a cursor, for older browsers still in `indexedDB` v1 - * @param request Request containing the cursor - */ - private getKeysFromCursor(request: IDBRequest): Observable { - - /* Keys will be stored here */ - const keys: string[] = []; - - /* Listen to success event */ - return this.successEvent(request).pipe( - /* Map to the result */ - map(() => request.result), - /* Iterate on the cursor */ - tap((cursor) => { - - if (cursor) { - - /* This lib only allows string keys, but user could have added other types of keys from outside */ - keys.push(cursor.key.toString()); - - cursor.continue(); - - } - - }), - /* Wait until the iteration is over */ - filter((cursor) => !cursor), - /* Map to the retrieved keys */ - map(() => keys) - ); - - } - } 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 6f8f955d..f01dd1ac 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 @@ -79,11 +79,11 @@ export abstract class LocalDatabase { abstract readonly size: Observable; - abstract getItem(key: string): Observable; - abstract setItem(key: string, data: any): Observable; - abstract removeItem(key: string): Observable; - abstract clear(): Observable; - abstract keys(): Observable; + abstract get(key: string): Observable; + abstract set(key: string, data: any): Observable; + abstract delete(key: string): Observable; + abstract clear(): Observable; + abstract keys(): Observable; abstract has(key: string): Observable; } 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 4d790469..2443cebf 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 @@ -1,5 +1,6 @@ import { Injectable, Inject } from '@angular/core'; -import { Observable, of, throwError } from 'rxjs'; +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'; @@ -43,14 +44,14 @@ export class LocalStorageDatabase implements LocalDatabase { /** * Gets an item value in `localStorage` * @param key The item's key - * @returns The item's value if the key exists, `null` otherwise, wrapped in a RxJS `Observable` + * @returns The item's value if the key exists, `undefined` otherwise, wrapped in a RxJS `Observable` */ - getItem(key: string): Observable { + get(key: string): Observable { /* Get raw data */ const unparsedData = localStorage.getItem(this.prefixKey(key)); - let parsedData: T | null = null; + let parsedData: T | undefined; /* No need to parse if data is `null` or `undefined` */ if ((unparsedData !== undefined) && (unparsedData !== null)) { @@ -75,12 +76,7 @@ export class LocalStorageDatabase implements LocalDatabase { * @param data The item's value * @returns A RxJS `Observable` to wait the end of the operation */ - setItem(key: string, data: any): Observable { - - /* Storing `undefined` or `null` in `localStorage` can cause issues in some browsers so removing item instead */ - if ((data === undefined) || (data === null)) { - return this.removeItem(key); - } + set(key: string, data: any): Observable { let serializedData: string | null = null; @@ -99,7 +95,7 @@ export class LocalStorageDatabase implements LocalDatabase { } /* Wrap in a RxJS `Observable` to be consistent with other storages */ - return of(true); + return of(undefined); } @@ -108,12 +104,12 @@ export class LocalStorageDatabase implements LocalDatabase { * @param key The item's key * @returns A RxJS `Observable` to wait the end of the operation */ - removeItem(key: string): Observable { + delete(key: string): Observable { localStorage.removeItem(this.prefixKey(key)); /* Wrap in a RxJS `Observable` to be consistent with other storages */ - return of(true); + return of(undefined); } @@ -121,34 +117,39 @@ export class LocalStorageDatabase implements LocalDatabase { * Deletes all items in `localStorage` * @returns A RxJS `Observable` to wait the end of the operation */ - clear(): Observable { + clear(): Observable { localStorage.clear(); /* Wrap in a RxJS `Observable` to be consistent with other storages */ - return of(true); + return of(undefined); } /** * Get all keys in `localStorage` * Note the order of the keys may be inconsistent in Firefox - * @returns A RxJS `Observable` containing the list of keys + * @returns A RxJS `Observable` iterating on keys */ - keys(): Observable { + keys(): Observable { - const keys: string[] = []; + /* Create an `Observable` from keys */ + return new Observable((subscriber) => { - /* Iteretate over all the indexes */ - for (let index = 0; index < localStorage.length; index += 1) { + /* Iteretate over all the indexes */ + for (let index = 0; index < localStorage.length; index += 1) { - /* Cast as we are sure in this case the key is not `null` */ - keys.push(this.getUnprefixedKey(index) as string); + /* Cast as we are sure in this case the key is not `null` */ + subscriber.next(this.getUnprefixedKey(index) as string); - } + } - /* Wrap in a RxJS `Observable` to be consistent with other storages */ - return of(keys); + subscriber.complete(); + + }).pipe( + /* Required to work like other databases which are asynchronous */ + observeOn(asyncScheduler), + ); } 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 188b5cc0..157a74f7 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 @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { Observable, of } from 'rxjs'; +import { Observable, of, from } from 'rxjs'; import { LocalDatabase } from './local-database'; @@ -26,15 +26,14 @@ export class MemoryDatabase implements LocalDatabase { /** * Gets an item value in memory * @param key The item's key - * @returns The item's value if the key exists, `null` otherwise, wrapped in a RxJS `Observable` + * @returns The item's value if the key exists, `undefined` otherwise, wrapped in a RxJS `Observable` */ - getItem(key: string): Observable { + get(key: string): Observable { - const rawData = this.memoryStorage.get(key) as T | null; + const rawData = this.memoryStorage.get(key) as T | undefined; - /* If data is `undefined`, returns `null` instead for the API to be consistent. - * Wrap in a RxJS `Observable` to be consistent with other storages */ - return of((rawData !== undefined) ? rawData : null); + /* Wrap in a RxJS `Observable` to be consistent with other storages */ + return of(rawData); } @@ -44,17 +43,12 @@ export class MemoryDatabase implements LocalDatabase { * @param data The item's value * @returns A RxJS `Observable` to wait the end of the operation */ - setItem(key: string, data: any): Observable { - - /* Storing `undefined` or `null` in `localStorage` is useless, so removing item instead */ - if ((data === undefined) || (data === null)) { - return this.removeItem(key); - } + set(key: string, data: any): Observable { this.memoryStorage.set(key, data); /* Wrap in a RxJS `Observable` to be consistent with other storages */ - return of(true); + return of(undefined); } @@ -63,12 +57,12 @@ export class MemoryDatabase implements LocalDatabase { * @param key The item's key * @returns A RxJS `Observable` to wait the end of the operation */ - removeItem(key: string): Observable { + delete(key: string): Observable { this.memoryStorage.delete(key); /* Wrap in a RxJS `Observable` to be consistent with other storages */ - return of(true); + return of(undefined); } @@ -76,26 +70,23 @@ export class MemoryDatabase implements LocalDatabase { * Deletes all items in memory * @returns A RxJS `Observable` to wait the end of the operation */ - clear(): Observable { + clear(): Observable { this.memoryStorage.clear(); /* Wrap in a RxJS `Observable` to be consistent with other storages */ - return of(true); + return of(undefined); } /** * Get all keys in memory - * @returns List of all keys, wrapped in a RxJS `Observable` + * @returns A RxJS `Observable` iterating on keys */ - keys(): Observable { + keys(): Observable { - /* Transform to a classic array for the API to be consistent */ - const keys = Array.from(this.memoryStorage.keys()); - - /* Wrap in a RxJS `Observable` to be consistent with other storages */ - return of(keys); + /* Create an `Observable` from keys */ + return from(this.memoryStorage.keys()); } diff --git a/projects/ngx-pwa/local-storage/src/lib/get-item-overloads.spec.ts b/projects/ngx-pwa/local-storage/src/lib/storages/get-item-overloads.spec.ts similarity index 97% rename from projects/ngx-pwa/local-storage/src/lib/get-item-overloads.spec.ts rename to projects/ngx-pwa/local-storage/src/lib/storages/get-item-overloads.spec.ts index a9501595..590c4896 100644 --- a/projects/ngx-pwa/local-storage/src/lib/get-item-overloads.spec.ts +++ b/projects/ngx-pwa/local-storage/src/lib/storages/get-item-overloads.spec.ts @@ -1,8 +1,10 @@ -import { LocalStorage } from './lib.service'; -import { MemoryDatabase } from './databases/memory-database'; -import { JSONSchema, JSONSchemaArrayOf, JSONSchemaNumber } from './validation/json-schema'; import { map } from 'rxjs/operators'; +import { LocalStorage } from './local-storage.service'; +import { StorageMap } from './storage-map.service'; +import { MemoryDatabase } from '../databases'; +import { JSONSchema, JSONSchemaArrayOf, JSONSchemaNumber } from '../validation'; + /* For now, `unknown` and `any` cases must be checked manually as any type can be converted to them. */ // TODO: Find a way to automate this: https://github.com/dsherret/conditional-type-checks @@ -13,7 +15,7 @@ describe('getItem() API v8', () => { beforeEach(() => { /* Do compilation tests on memory storage to avoid issues when other storages are not available */ - localStorageService = new LocalStorage(new MemoryDatabase()); + localStorageService = new LocalStorage(new StorageMap(new MemoryDatabase())); }); @@ -358,7 +360,7 @@ describe('getItem() API prior to v8', () => { beforeEach(() => { /* Do compilation tests on memory storage to avoid issues when other storages are not available */ - localStorageService = new LocalStorage(new MemoryDatabase()); + localStorageService = new LocalStorage(new StorageMap(new MemoryDatabase())); }); diff --git a/projects/ngx-pwa/local-storage/src/lib/storages/index.ts b/projects/ngx-pwa/local-storage/src/lib/storages/index.ts new file mode 100644 index 00000000..0dedf3e0 --- /dev/null +++ b/projects/ngx-pwa/local-storage/src/lib/storages/index.ts @@ -0,0 +1,2 @@ +export { StorageMap } from './storage-map.service'; +export { LSGetItemOptions, LocalStorage } from './local-storage.service'; diff --git a/projects/ngx-pwa/local-storage/src/lib/lib.service.spec.ts b/projects/ngx-pwa/local-storage/src/lib/storages/local-storage.service.spec.ts similarity index 53% rename from projects/ngx-pwa/local-storage/src/lib/lib.service.spec.ts rename to projects/ngx-pwa/local-storage/src/lib/storages/local-storage.service.spec.ts index f2553ad8..01eefedc 100755 --- a/projects/ngx-pwa/local-storage/src/lib/lib.service.spec.ts +++ b/projects/ngx-pwa/local-storage/src/lib/storages/local-storage.service.spec.ts @@ -1,15 +1,11 @@ -import { TestBed } from '@angular/core/testing'; import { from } from 'rxjs'; import { mergeMap, filter, tap } from 'rxjs/operators'; -import { LocalStorage } from './lib.service'; -import { IndexedDBDatabase } from './databases/indexeddb-database'; -import { LocalStorageDatabase } from './databases/localstorage-database'; -import { MemoryDatabase } from './databases/memory-database'; -import { JSONSchema } from './validation/json-schema'; -import { VALIDATION_ERROR } from './exceptions'; -import { DEFAULT_IDB_DB_NAME, DEFAULT_IDB_STORE_NAME, DEFAULT_IDB_STORE_NAME_PRIOR_TO_V8 } from './tokens'; -import { clearStorage, closeAndDeleteDatabase } from './testing/cleaning'; +import { LocalStorage } from './local-storage.service'; +import { StorageMap } from './storage-map.service'; +import { IndexedDBDatabase, LocalStorageDatabase, MemoryDatabase } from '../databases'; +import { JSONSchema, VALIDATION_ERROR } from '../validation'; +import { clearStorage, closeAndDeleteDatabase } from '../testing/cleaning'; function tests(description: string, localStorageServiceFactory: () => LocalStorage) { @@ -25,7 +21,8 @@ function tests(description: string, localStorageServiceFactory: () => LocalStora beforeEach((done) => { /* Clear data to avoid tests overlap */ - clearStorage(done, localStorageService); + // tslint:disable-next-line: no-string-literal + clearStorage(done, localStorageService['storageMap']); }); afterAll((done) => { @@ -34,7 +31,8 @@ function tests(description: string, localStorageServiceFactory: () => LocalStora * so the next tests group to will trigger the `indexedDB` `upgradeneeded` event, * as it's where the store is created * - to be able to delete the database, all connections to it must be closed */ - closeAndDeleteDatabase(done, localStorageService); + // tslint:disable-next-line: no-string-literal + closeAndDeleteDatabase(done, localStorageService['storageMap']); }); describe(('setItem() + getItem()'), () => { @@ -297,28 +295,6 @@ function tests(description: string, localStorageServiceFactory: () => LocalStora describe('Map-like API', () => { - it('size', (done) => { - - localStorageService.size.pipe( - tap((length) => { expect(length).toBe(0); }), - mergeMap(() => localStorageService.setItem(key, 'test')), - mergeMap(() => localStorageService.size), - tap((length) => { expect(length).toBe(1); }), - mergeMap(() => localStorageService.setItem('', 'test')), - mergeMap(() => localStorageService.size), - tap((length) => { expect(length).toBe(2); }), - mergeMap(() => localStorageService.removeItem(key)), - mergeMap(() => localStorageService.size), - tap((length) => { expect(length).toBe(1); }), - mergeMap(() => localStorageService.clear()), - mergeMap(() => localStorageService.size), - tap((length) => { expect(length).toBe(0); }), - ).subscribe(() => { - done(); - }); - - }); - it('length', (done) => { localStorageService.length.pipe( @@ -348,6 +324,7 @@ function tests(description: string, localStorageServiceFactory: () => LocalStora localStorageService.setItem(key1, 'test').pipe( mergeMap(() => localStorageService.setItem(key2, 'test')), + // tslint:disable-next-line: deprecation mergeMap(() => localStorageService.keys()), ).subscribe((keys) => { @@ -362,6 +339,7 @@ function tests(description: string, localStorageServiceFactory: () => LocalStora it('getKey() when no items', (done) => { + // tslint:disable-next-line: deprecation localStorageService.keys().subscribe((keys) => { expect(keys.length).toBe(0); @@ -375,6 +353,7 @@ function tests(description: string, localStorageServiceFactory: () => LocalStora it('key() on existing', (done) => { localStorageService.setItem(key, 'test').pipe( + // tslint:disable-next-line: deprecation mergeMap(() => localStorageService.has(key)) ).subscribe((result) => { @@ -388,6 +367,7 @@ function tests(description: string, localStorageServiceFactory: () => LocalStora it('key() on unexisting', (done) => { + // tslint:disable-next-line: deprecation localStorageService.has(`nokey${Date.now()}`).subscribe((result) => { expect(result).toBe(false); @@ -404,6 +384,7 @@ function tests(description: string, localStorageServiceFactory: () => LocalStora mergeMap(() => localStorageService.setItem('user_lastname', 'test')), mergeMap(() => localStorageService.setItem('app_data1', 'test')), mergeMap(() => localStorageService.setItem('app_data2', 'test')), + // tslint:disable-next-line: deprecation mergeMap(() => localStorageService.keys()), /* Now we will have an `Observable` emiting multiple values */ mergeMap((keys) => from(keys)), @@ -413,7 +394,8 @@ function tests(description: string, localStorageServiceFactory: () => LocalStora /* So we need to wait for completion of all actions to check */ complete: () => { - localStorageService.size.subscribe((size) => { + + localStorageService.length.subscribe((size) => { expect(size).toBe(2); @@ -594,20 +576,6 @@ function tests(description: string, localStorageServiceFactory: () => LocalStora }); - it('size', (done) => { - - localStorageService.size.subscribe({ - complete: () => { - - expect().nothing(); - - done(); - - } - }); - - }); - it('length', (done) => { localStorageService.length.subscribe({ @@ -624,6 +592,7 @@ function tests(description: string, localStorageServiceFactory: () => LocalStora it('keys()', (done) => { + // tslint:disable-next-line: deprecation localStorageService.keys().subscribe({ complete: () => { @@ -638,6 +607,7 @@ function tests(description: string, localStorageServiceFactory: () => LocalStora it('has()', (done) => { + // tslint:disable-next-line: deprecation localStorageService.has(key).subscribe({ complete: () => { @@ -652,353 +622,16 @@ function tests(description: string, localStorageServiceFactory: () => LocalStora }); - describe('compatibility', () => { - - it('Promise', (done) => { - - const value = 'test'; - - localStorageService.setItem(key, value).toPromise() - .then(() => localStorageService.getItem(key).toPromise()) - .then((result) => { - expect(result).toBe(value); - done(); - }); - - }); - - it('async / await', async () => { - - const value = 'test'; - - await localStorageService.setItem(key, value).toPromise(); - - const result = await localStorageService.getItem(key).toPromise(); - - expect(result).toBe(value); - - }); - - }); - }); } -tests('memory', () => new LocalStorage(new MemoryDatabase())); - -tests('localStorage', () => new LocalStorage(new LocalStorageDatabase())); - -tests('localStorage with prefix', () => new LocalStorage(new LocalStorageDatabase(`ls`))); - -tests('localStorage with old prefix', () => new LocalStorage(new LocalStorageDatabase(undefined, `old`))); - -tests('indexedDB', () => new LocalStorage(new IndexedDBDatabase())); - -tests('indexedDB with old prefix', () => new LocalStorage(new IndexedDBDatabase(undefined, undefined, `myapp${Date.now()}`))); - -tests( - 'indexedDB with custom database and store names', - () => new LocalStorage(new IndexedDBDatabase(`dbCustom${Date.now()}`, `storeCustom${Date.now()}`)) -); - -describe('specials', () => { - - /* Avoid https://github.com/cyrilletuzi/angular-async-local-storage/issues/57 */ - it('check use of IndexedDb (will be pending in Firefox/IE private mode)', (done) => { - - const index = `test${Date.now()}`; - const value = 'test'; - - const localStorageService = new LocalStorage(new IndexedDBDatabase()); - - localStorageService.setItem(index, value).subscribe(() => { - - try { - - const dbOpen = indexedDB.open(DEFAULT_IDB_DB_NAME); +describe('LocalStoage', () => { - dbOpen.addEventListener('success', () => { - - const store = dbOpen.result.transaction([DEFAULT_IDB_STORE_NAME], 'readonly').objectStore(DEFAULT_IDB_STORE_NAME); - - const request = store.get(index); - - request.addEventListener('success', () => { - - expect(request.result).toEqual(value); - - dbOpen.result.close(); - - closeAndDeleteDatabase(done, localStorageService); - - }); - - request.addEventListener('error', () => { - - dbOpen.result.close(); - - /* This case is not supposed to happen */ - fail(); - - }); + tests('memory', () => new LocalStorage(new StorageMap(new MemoryDatabase()))); - }); - - dbOpen.addEventListener('error', () => { - - /* Cases : Firefox private mode where `indexedDb` exists but fails */ - pending(); - - }); - - } catch { - - /* Cases : IE private mode where `indexedDb` will exist but not its `open()` method */ - pending(); - - } - - }); - - }); - - it('indexedDB default store name (will be pending in Firefox private mode)', (done) => { - - const localStorageService = new LocalStorage(new IndexedDBDatabase()); - - /* Do a request first as a first transaction is needed to set the store name */ - localStorageService.getItem('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(); - - } - - }); - - }); + tests('localStorage', () => new LocalStorage(new StorageMap(new LocalStorageDatabase()))); - 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 LocalStorage(new IndexedDBDatabase(dbName, storeName)); - - /* Do a request first as a first transaction is needed to set the store name */ - localStorageService.getItem('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(storeName); - - closeAndDeleteDatabase(done, localStorageService); - - } else { - - /* Cases: Firefox private mode */ - pending(); - - } - - }); - - }); - - it('indexedDB store prior to v8 (will be pending in Firefox/IE private mode)', (done) => { - - /* Unique name to be sure `indexedDB` `upgradeneeded` event is triggered */ - const dbName = `ngStoreV7${Date.now()}`; - - const index1 = `test1${Date.now()}`; - const value1 = 'test1'; - const index2 = `test2${Date.now()}`; - const value2 = 'test2'; - - try { - - const dbOpen = indexedDB.open(dbName); - - dbOpen.addEventListener('upgradeneeded', () => { - - // tslint:disable-next-line: deprecation - if (!dbOpen.result.objectStoreNames.contains(DEFAULT_IDB_STORE_NAME_PRIOR_TO_V8)) { - - /* Create the object store */ - // tslint:disable-next-line: deprecation - dbOpen.result.createObjectStore(DEFAULT_IDB_STORE_NAME_PRIOR_TO_V8); - - } - - }); - - dbOpen.addEventListener('success', () => { - - const localStorageService = new LocalStorage(new IndexedDBDatabase(dbName)); - - // 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); - - request1.addEventListener('success', () => { - - localStorageService.getItem(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.setItem(index2, value2).subscribe(() => { - - // 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 request2 = store2.get(index2); - - request2.addEventListener('success', () => { - - /* Via direct `indexedDB`, data should be wrapped */ - expect(request2.result).toEqual({ value: value2 }); - - dbOpen.result.close(); - - closeAndDeleteDatabase(done, localStorageService); - - }); - - request2.addEventListener('error', () => { - - dbOpen.result.close(); - - /* This case is not supposed to happen */ - fail(); - - }); - - }); - - }); - - }); - - request1.addEventListener('error', () => { - - dbOpen.result.close(); - - /* This case is not supposed to happen */ - fail(); - - }); - - }); - - dbOpen.addEventListener('error', () => { - - /* Cases : Firefox private mode where `indexedDb` exists but fails */ - pending(); - - }); - - } catch { - - /* Cases : IE private mode where `indexedDb` will exist but not its `open()` method */ - pending(); - - } - - }); - - it('indexedDB old prefix (will be pending in Firefox private mode)', (done) => { - - /* Unique name to be sure `indexedDB` `upgradeneeded` event is triggered */ - const prefix = `myapp${Date.now()}`; - const localStorageService = new LocalStorage(new IndexedDBDatabase(undefined, undefined, prefix)); - - /* Do a request first to allow localStorage fallback if needed */ - localStorageService.getItem('test').subscribe(() => { - - // tslint:disable-next-line: no-string-literal - if (localStorageService['database'] instanceof IndexedDBDatabase) { - - // tslint:disable-next-line: no-string-literal - expect(localStorageService['database']['dbName']).toBe(`${prefix}_${DEFAULT_IDB_DB_NAME}`); - - closeAndDeleteDatabase(done, localStorageService); - - } else { - - /* Cases: Firefox private mode */ - pending(); - - } - - }); - - }); - - it('localStorage prefix', () => { - - const prefix = `ls_`; - - const localStorageService = new LocalStorage(new LocalStorageDatabase(prefix)); - - // tslint:disable-next-line: no-string-literal - expect((localStorageService['database'] as LocalStorageDatabase)['prefix']).toBe(prefix); - - }); - - it('localStorage old prefix', () => { - - const prefix = `old`; - - const localStorageService = new LocalStorage(new LocalStorageDatabase(undefined, prefix)); - - // tslint:disable-next-line: no-string-literal - expect((localStorageService['database'] as LocalStorageDatabase)['prefix']).toBe(`${prefix}_`); - - }); - - it('automatic storage injection', (done) => { - - // TODO: check new API types and backward compitiliby with v7 - // tslint:disable-next-line: deprecation - const localStorageService = TestBed.get(LocalStorage) as LocalStorage; - - const index = 'index'; - const value = `value${Date.now()}`; - - localStorageService.setItem(index, value).pipe( - mergeMap(() => localStorageService.getItem(index)) - ).subscribe((data) => { - - expect(data).toBe(value); - - closeAndDeleteDatabase(done, localStorageService); - - }); - - }); + tests('indexedDB', () => new LocalStorage(new StorageMap(new IndexedDBDatabase()))); }); 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 new file mode 100755 index 00000000..88f4f68b --- /dev/null +++ b/projects/ngx-pwa/local-storage/src/lib/storages/local-storage.service.ts @@ -0,0 +1,159 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { mapTo, toArray, map } from 'rxjs/operators'; + +import { StorageMap } from './storage-map.service'; +import { JSONSchema, JSONSchemaBoolean, JSONSchemaInteger, JSONSchemaNumber, JSONSchemaString, JSONSchemaArrayOf } from '../validation'; + +/** + * @deprecated Will be removed in v9 + */ +export interface LSGetItemOptions { + + /** + * Subset of the JSON Schema standard. + * Types are enforced to validate everything: each value **must** have a `type`. + * @see https://github.com/cyrilletuzi/angular-async-local-storage/blob/master/docs/VALIDATION.md + */ + schema?: JSONSchema | null; + +} + +@Injectable({ + providedIn: 'root' +}) +export class LocalStorage { + + /** + * Number of items in storage + * @deprecated Use `.length`, or use `.size` via the new `StorageMap` service. Will be removed in v9. + */ + get size(): Observable { + + return this.length; + + } + + /** + * Number of items in storage + */ + get length(): Observable { + + return this.storageMap.size; + + } + + /* Use the `StorageMap` service to avoid code duplication */ + constructor(private storageMap: StorageMap) {} + + /** + * Get an item value in storage. + * The signature has many overloads due to validation, **please refer to the documentation.** + * Note you must pass the schema directly as the second argument. + * Passing the schema in an object `{ schema }` is deprecated and only here for backward compatibility: + * it may be removed in v9. + * @see https://github.com/cyrilletuzi/angular-async-local-storage/blob/master/docs/VALIDATION.md + * @param key The item's key + * @returns The item's value if the key exists, `null` otherwise, wrapped in a RxJS `Observable` + */ + getItem(key: string, schema: JSONSchemaString): Observable; + getItem(key: string, schema: JSONSchemaInteger | JSONSchemaNumber): Observable; + getItem(key: string, schema: JSONSchemaBoolean): Observable; + getItem(key: string, schema: JSONSchemaArrayOf): Observable; + getItem(key: string, schema: JSONSchemaArrayOf): Observable; + getItem(key: string, schema: JSONSchemaArrayOf): Observable; + getItem(key: string, schema: JSONSchema | { schema: JSONSchema }): Observable; + getItem(key: string, schema?: null): Observable; + getItem(key: string, schema: JSONSchema | { schema: JSONSchema } | null | undefined = null) { + + if (schema) { + + /* Backward compatibility with version <= 7 */ + const schemaFinal: JSONSchema = ('schema' in schema) ? schema.schema : schema; + + return this.storageMap.get(key, schemaFinal).pipe( + /* Transform `undefined` into `null` to align with `localStorage` API */ + map((value) => (value !== undefined) ? value : null), + ); + + } else { + + return this.storageMap.get(key).pipe( + /* Transform `undefined` into `null` to align with `localStorage` API */ + map((value) => (value !== undefined) ? value : null), + ); + + } + + } + + /** + * Set an item in storage + * @param key The item's key + * @param data The item's value + * @returns A RxJS `Observable` to wait the end of the operation + */ + setItem(key: string, data: any): Observable { + + return this.storageMap.set(key, data).pipe( + /* Transform `undefined` into `true` for backward compatibility with v7 */ + mapTo(true), + ); + + } + + /** + * Delete an item in storage + * @param key The item's key + * @returns A RxJS `Observable` to wait the end of the operation + */ + removeItem(key: string): Observable { + + return this.storageMap.delete(key).pipe( + /* Transform `undefined` into `true` for backward compatibility with v7 */ + mapTo(true), + ); + + } + + /** + * Delete all items in storage + * @returns A RxJS `Observable` to wait the end of the operation + */ + clear(): Observable { + + return this.storageMap.clear().pipe( + /* Transform `undefined` into `true` for backward compatibility with v7 */ + mapTo(true), + ); + + } + + /** + * Get all keys stored in storage + * @returns A list of the keys wrapped in a RxJS `Observable` + * @deprecated Moved to `StorageMap` service. Will be removed in v9. + * Note that while this method was giving you all keys at once in an array, + * the new `keys()` method in `StorageMap` service will *iterate* on each key. + */ + keys(): Observable { + + return this.storageMap.keys().pipe( + /* Backward compatibility with v7: transform iterating `Observable` to a single array value */ + toArray(), + ); + + } + + /** + * Tells if a key exists in storage + * @returns A RxJS `Observable` telling if the key exists + * @deprecated Moved to `StorageMap` service. Will be removed in v9. + */ + has(key: string): Observable { + + return this.storageMap.has(key); + + } + +} 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 new file mode 100644 index 00000000..5c7f1db9 --- /dev/null +++ b/projects/ngx-pwa/local-storage/src/lib/storages/storage-map.service.spec.ts @@ -0,0 +1,924 @@ +import { TestBed } from '@angular/core/testing'; +import { mergeMap, tap, filter } from 'rxjs/operators'; + +import { StorageMap } from './storage-map.service'; +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 { clearStorage, closeAndDeleteDatabase } from '../testing/cleaning'; + +function tests(description: string, localStorageServiceFactory: () => StorageMap) { + + const key = 'test'; + let localStorageService: StorageMap; + + describe(description, () => { + + beforeAll(() => { + /* Via a factory as the class should be instancied only now, not before, otherwise tests could overlap */ + localStorageService = localStorageServiceFactory(); + }); + + beforeEach((done) => { + /* Clear data to avoid tests overlap */ + clearStorage(done, localStorageService); + }); + + afterAll((done) => { + /* Now that `indexedDB` store name can be customized, it's important: + * - to delete the database after each tests group, + * so the next tests group to will trigger the `indexedDB` `upgradeneeded` event, + * as it's where the store is created + * - to be able to delete the database, all connections to it must be closed */ + closeAndDeleteDatabase(done, localStorageService); + }); + + describe(('set() + get()'), () => { + + it('unexisting key', (done) => { + + localStorageService.get(`unknown${Date.now()}`).subscribe((data) => { + + expect(data).toBeUndefined(); + + done(); + + }); + + }); + + it('string', (done) => { + + const value = 'blue'; + + localStorageService.set(key, value).pipe( + mergeMap(() => localStorageService.get(key)) + ).subscribe((result) => { + + expect(result).toBe(value); + + done(); + + }); + + }); + + it('empty string', (done) => { + + const value = ''; + + localStorageService.set(key, value).pipe( + mergeMap(() => localStorageService.get(key)) + ).subscribe((result) => { + + expect(result).toBe(value); + + done(); + + }); + + }); + + it('integer', (done) => { + + const value = 1; + + localStorageService.set(key, value).pipe( + mergeMap(() => localStorageService.get(key)) + ).subscribe((result) => { + + expect(result).toBe(value); + + done(); + + }); + + }); + + it('number', (done) => { + + const value = 1.5; + + localStorageService.set(key, value).pipe( + mergeMap(() => localStorageService.get(key)) + ).subscribe((result) => { + + expect(result).toBe(value); + + done(); + + }); + + }); + + it('zero', (done) => { + + const value = 0; + + localStorageService.set(key, value).pipe( + mergeMap(() => localStorageService.get(key)) + ).subscribe((result) => { + + expect(result).toBe(value); + + done(); + + }); + + }); + + it('true', (done) => { + + const value = true; + + localStorageService.set(key, value).pipe( + mergeMap(() => localStorageService.get(key)) + ).subscribe((result) => { + + expect(result).toBe(value); + + done(); + + }); + + }); + + it('false', (done) => { + + const value = false; + + localStorageService.set(key, value).pipe( + mergeMap(() => localStorageService.get(key)) + ).subscribe((result) => { + + expect(result).toBe(value); + + done(); + + }); + + }); + + it('null', (done) => { + + localStorageService.set(key, 'test').pipe( + mergeMap(() => localStorageService.set(key, null)), + mergeMap(() => localStorageService.get(key)), + ).subscribe((result) => { + + // TODO: see if `null` can be stored in IE/Edge + expect(result).toBeUndefined(); + + done(); + + }); + + }); + + it('undefined', (done) => { + + localStorageService.set(key, 'test').pipe( + mergeMap(() => localStorageService.set(key, undefined)), + mergeMap(() => localStorageService.get(key)), + ).subscribe((result) => { + + expect(result).toBeUndefined(); + + done(); + + }); + + }); + + it('array', (done) => { + + const value = [1, 2, 3]; + + localStorageService.set(key, value).pipe( + mergeMap(() => localStorageService.get(key)) + ).subscribe((result) => { + + expect(result).toEqual(value); + + done(); + + }); + + }); + + it('object', (done) => { + + const value = { name: 'test' }; + + localStorageService.set(key, value).pipe( + mergeMap(() => localStorageService.get(key)) + ).subscribe((result) => { + + expect(result).toEqual(value); + + done(); + + }); + + }); + + it('update', (done) => { + + localStorageService.set(key, 'value').pipe( + mergeMap(() => localStorageService.set(key, 'updated')) + ).subscribe(() => { + + expect().nothing(); + + done(); + + }); + + }); + + it('concurrency', (done) => { + + const value1 = 'test1'; + const value2 = 'test2'; + + expect(() => { + + localStorageService.set(key, value1).subscribe(); + + localStorageService.set(key, value2).pipe( + mergeMap(() => localStorageService.get(key)) + ).subscribe((result) => { + + expect(result).toBe(value2); + + done(); + + }); + + }).not.toThrow(); + + }); + + }); + + describe('delete()', () => { + + it('existing key', (done) => { + + localStorageService.set(key, 'test').pipe( + mergeMap(() => localStorageService.delete(key)), + mergeMap(() => localStorageService.get(key)) + ).subscribe((result) => { + + expect(result).toBeUndefined(); + + done(); + + }); + + }); + + it('unexisting key', (done) => { + + localStorageService.delete(`unexisting${Date.now()}`).subscribe(() => { + + expect().nothing(); + + done(); + + }); + + }); + + }); + + describe('Map-like API', () => { + + it('size', (done) => { + + localStorageService.size.pipe( + tap((length) => { expect(length).toBe(0); }), + mergeMap(() => localStorageService.set(key, 'test')), + mergeMap(() => localStorageService.size), + tap((length) => { expect(length).toBe(1); }), + mergeMap(() => localStorageService.set('', 'test')), + mergeMap(() => localStorageService.size), + tap((length) => { expect(length).toBe(2); }), + mergeMap(() => localStorageService.delete(key)), + mergeMap(() => localStorageService.size), + tap((length) => { expect(length).toBe(1); }), + mergeMap(() => localStorageService.clear()), + mergeMap(() => localStorageService.size), + tap((length) => { expect(length).toBe(0); }), + ).subscribe(() => { + done(); + }); + + }); + + it('keys()', (done) => { + + const key1 = 'index1'; + const key2 = 'index2'; + const keys = [key1, key2]; + + localStorageService.set(key1, 'test').pipe( + mergeMap(() => localStorageService.set(key2, 'test')), + mergeMap(() => localStorageService.keys()), + ).subscribe({ + next: (value) => { + expect(keys).toContain(value); + keys.splice(keys.indexOf(value), 1); + }, + complete: () => { + done(); + }, + error: () => { + done(); + }, + }); + + }); + + it('getKey() when no items', (done) => { + + localStorageService.keys().subscribe({ + next: () => { + fail(); + }, + complete: () => { + expect().nothing(); + done(); + }, + }); + + }); + + it('has() on existing', (done) => { + + localStorageService.set(key, 'test').pipe( + mergeMap(() => localStorageService.has(key)) + ).subscribe((result) => { + + expect(result).toBe(true); + + done(); + + }); + + }); + + it('has() on unexisting', (done) => { + + localStorageService.has(`nokey${Date.now()}`).subscribe((result) => { + + expect(result).toBe(false); + + done(); + + }); + + }); + + it('advanced case: remove only some items', (done) => { + + localStorageService.set('user_firstname', 'test').pipe( + mergeMap(() => localStorageService.set('user_lastname', 'test')), + mergeMap(() => localStorageService.set('app_data1', 'test')), + mergeMap(() => localStorageService.set('app_data2', 'test')), + mergeMap(() => localStorageService.keys()), + filter((currentKey) => currentKey.startsWith('app_')), + mergeMap((currentKey) => localStorageService.delete(currentKey)), + ).subscribe({ + /* So we need to wait for completion of all actions to check */ + complete: () => { + + localStorageService.size.subscribe((size) => { + + expect(size).toBe(2); + + done(); + + }); + + } + }); + + }); + + }); + + describe('JSON schema', () => { + + const schema: JSONSchema = { + type: 'object', + properties: { + expected: { + type: 'string' + } + }, + required: ['expected'] + }; + + it('valid', (done) => { + + const value = { expected: 'value' }; + + localStorageService.set(key, value).pipe( + mergeMap(() => localStorageService.get(key, schema)) + ).subscribe((data) => { + + expect(data).toEqual(value); + + done(); + + }); + + }); + + it('invalid', (done) => { + + localStorageService.set(key, 'test').pipe( + mergeMap(() => localStorageService.get(key, schema)) + ).subscribe({ error: (error) => { + + expect(error.message).toBe(VALIDATION_ERROR); + + done(); + + } }); + + }); + + it('null: no validation', (done) => { + + localStorageService.get<{ expected: string }>(`noassociateddata${Date.now()}`, schema).subscribe(() => { + + expect().nothing(); + + done(); + + }); + + }); + + }); + + /* Avoid https://github.com/cyrilletuzi/angular-async-local-storage/issues/25 + * Avoid https://github.com/cyrilletuzi/angular-async-local-storage/issues/5 */ + describe('complete', () => { + + it('set()', (done) => { + + localStorageService.set('index', 'value').subscribe({ + complete: () => { + + expect().nothing(); + + done(); + + } + }); + + }); + + it('get()', (done) => { + + localStorageService.get(key).subscribe({ + complete: () => { + + expect().nothing(); + + done(); + + } + }); + + }); + + it('delete()', (done) => { + + localStorageService.delete(key).subscribe({ + complete: () => { + + expect().nothing(); + + done(); + } + + }); + + }); + + it('clear()', (done) => { + + localStorageService.clear().subscribe({ + complete: () => { + + expect().nothing(); + + done(); + + } + }); + + }); + + it('size', (done) => { + + localStorageService.size.subscribe({ + complete: () => { + + expect().nothing(); + + done(); + + } + }); + + }); + + it('keys()', (done) => { + + localStorageService.keys().subscribe({ + complete: () => { + + expect().nothing(); + + done(); + + } + }); + + }); + + it('has()', (done) => { + + localStorageService.has(key).subscribe({ + complete: () => { + + expect().nothing(); + + done(); + + } + }); + + }); + + }); + + describe('compatibility', () => { + + it('Promise', (done) => { + + const value = 'test'; + + localStorageService.set(key, value).toPromise() + .then(() => localStorageService.get(key).toPromise()) + .then((result) => { + expect(result).toBe(value); + done(); + }); + + }); + + it('async / await', async () => { + + const value = 'test'; + + await localStorageService.set(key, value).toPromise(); + + const result = await localStorageService.get(key).toPromise(); + + expect(result).toBe(value); + + }); + + }); + + }); + +} + +describe('StorageMap', () => { + + tests('memory', () => new StorageMap(new MemoryDatabase())); + + tests('localStorage', () => new StorageMap(new LocalStorageDatabase())); + + tests('localStorage with prefix', () => new StorageMap(new LocalStorageDatabase(`ls`))); + + tests('localStorage with old prefix', () => new StorageMap(new LocalStorageDatabase(undefined, `old`))); + + tests('indexedDB', () => new StorageMap(new IndexedDBDatabase())); + + tests('indexedDB with old prefix', () => new StorageMap(new IndexedDBDatabase(undefined, undefined, `myapp${Date.now()}`))); + + tests( + 'indexedDB with custom database and store names', + () => new StorageMap(new IndexedDBDatabase(`dbCustom${Date.now()}`, `storeCustom${Date.now()}`)) + ); + + describe('specials', () => { + + /* Avoid https://github.com/cyrilletuzi/angular-async-local-storage/issues/57 */ + it('check use of IndexedDb (will be pending in Firefox/IE private mode)', (done) => { + + const index = `test${Date.now()}`; + const value = 'test'; + + const localStorageService = new StorageMap(new IndexedDBDatabase()); + + localStorageService.set(index, value).subscribe(() => { + + try { + + const dbOpen = indexedDB.open(DEFAULT_IDB_DB_NAME); + + dbOpen.addEventListener('success', () => { + + const store = dbOpen.result.transaction([DEFAULT_IDB_STORE_NAME], 'readonly').objectStore(DEFAULT_IDB_STORE_NAME); + + const request = store.get(index); + + request.addEventListener('success', () => { + + expect(request.result).toEqual(value); + + dbOpen.result.close(); + + closeAndDeleteDatabase(done, localStorageService); + + }); + + request.addEventListener('error', () => { + + dbOpen.result.close(); + + /* This case is not supposed to happen */ + fail(); + + }); + + }); + + dbOpen.addEventListener('error', () => { + + /* Cases : Firefox private mode where `indexedDb` exists but fails */ + pending(); + + }); + + } catch { + + /* Cases : IE private mode where `indexedDb` will exist but not its `open()` method */ + pending(); + + } + + }); + + }); + + 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(() => { + + // tslint:disable-next-line: no-string-literal + if (localStorageService['database'] instanceof IndexedDBDatabase) { + + // tslint:disable-next-line: no-string-literal + expect(localStorageService['database']['storeName']).toBe(storeName); + + closeAndDeleteDatabase(done, localStorageService); + + } else { + + /* Cases: Firefox private mode */ + pending(); + + } + + }); + + }); + + it('indexedDB store prior to v8 (will be pending in Firefox/IE private mode)', (done) => { + + /* Unique name to be sure `indexedDB` `upgradeneeded` event is triggered */ + const dbName = `ngStoreV7${Date.now()}`; + + const index1 = `test1${Date.now()}`; + const value1 = 'test1'; + const index2 = `test2${Date.now()}`; + const value2 = 'test2'; + + try { + + const dbOpen = indexedDB.open(dbName); + + dbOpen.addEventListener('upgradeneeded', () => { + + // tslint:disable-next-line: deprecation + if (!dbOpen.result.objectStoreNames.contains(DEFAULT_IDB_STORE_NAME_PRIOR_TO_V8)) { + + /* Create the object store */ + // tslint:disable-next-line: deprecation + dbOpen.result.createObjectStore(DEFAULT_IDB_STORE_NAME_PRIOR_TO_V8); + + } + + }); + + dbOpen.addEventListener('success', () => { + + const localStorageService = new StorageMap(new IndexedDBDatabase(dbName)); + + // 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); + + request1.addEventListener('success', () => { + + 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(() => { + + // 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 request2 = store2.get(index2); + + request2.addEventListener('success', () => { + + /* Via direct `indexedDB`, data should be wrapped */ + expect(request2.result).toEqual({ value: value2 }); + + dbOpen.result.close(); + + closeAndDeleteDatabase(done, localStorageService); + + }); + + request2.addEventListener('error', () => { + + dbOpen.result.close(); + + /* This case is not supposed to happen */ + fail(); + + }); + + }); + + }); + + }); + + request1.addEventListener('error', () => { + + dbOpen.result.close(); + + /* This case is not supposed to happen */ + fail(); + + }); + + }); + + dbOpen.addEventListener('error', () => { + + /* Cases : Firefox private mode where `indexedDb` exists but fails */ + pending(); + + }); + + } catch { + + /* Cases : IE private mode where `indexedDb` will exist but not its `open()` method */ + pending(); + + } + + }); + + it('indexedDB old prefix (will be pending in Firefox private mode)', (done) => { + + /* Unique name to be sure `indexedDB` `upgradeneeded` event is triggered */ + const prefix = `myapp${Date.now()}`; + const localStorageService = new StorageMap(new IndexedDBDatabase(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) { + + // tslint:disable-next-line: no-string-literal + expect(localStorageService['database']['dbName']).toBe(`${prefix}_${DEFAULT_IDB_DB_NAME}`); + + closeAndDeleteDatabase(done, localStorageService); + + } else { + + /* Cases: Firefox private mode */ + pending(); + + } + + }); + + }); + + it('localStorage prefix', () => { + + const prefix = `ls_`; + + const localStorageService = new StorageMap(new LocalStorageDatabase(prefix)); + + // tslint:disable-next-line: no-string-literal + expect((localStorageService['database'] as LocalStorageDatabase)['prefix']).toBe(prefix); + + }); + + it('localStorage old prefix', () => { + + const prefix = `old`; + + const localStorageService = new StorageMap(new LocalStorageDatabase(undefined, prefix)); + + // tslint:disable-next-line: no-string-literal + expect((localStorageService['database'] as LocalStorageDatabase)['prefix']).toBe(`${prefix}_`); + + }); + + it('automatic storage injection', (done) => { + + // TODO: check new API types and backward compitiliby with v7 + // tslint:disable-next-line: deprecation + const localStorageService = TestBed.get(StorageMap) as StorageMap; + + const index = 'index'; + const value = `value${Date.now()}`; + + localStorageService.set(index, value).pipe( + mergeMap(() => localStorageService.get(index)) + ).subscribe((data) => { + + expect(data).toBe(value); + + closeAndDeleteDatabase(done, localStorageService); + + }); + + }); + + }); + +}); diff --git a/projects/ngx-pwa/local-storage/src/lib/lib.service.ts b/projects/ngx-pwa/local-storage/src/lib/storages/storage-map.service.ts old mode 100755 new mode 100644 similarity index 52% rename from projects/ngx-pwa/local-storage/src/lib/lib.service.ts rename to projects/ngx-pwa/local-storage/src/lib/storages/storage-map.service.ts index fc5588f3..f054fd44 --- a/projects/ngx-pwa/local-storage/src/lib/lib.service.ts +++ b/projects/ngx-pwa/local-storage/src/lib/storages/storage-map.service.ts @@ -2,34 +2,17 @@ import { Injectable, Inject } from '@angular/core'; import { Observable, throwError, of, OperatorFunction } from 'rxjs'; import { mergeMap, catchError } from 'rxjs/operators'; -import { LocalDatabase } from './databases/local-database'; -import { LocalStorageDatabase } from './databases/localstorage-database'; -import { JSONValidator } from './validation/json-validator'; import { JSONSchema, JSONSchemaBoolean, JSONSchemaInteger, - JSONSchemaNumber, JSONSchemaString, JSONSchemaArrayOf -} from './validation/json-schema'; -import { IDB_BROKEN_ERROR, ValidationError } from './exceptions'; -import { LOCAL_STORAGE_PREFIX, LS_PREFIX } from './tokens'; - -/** - * @deprecated Will be removed in v9 - */ -export interface LSGetItemOptions { - - /** - * Subset of the JSON Schema standard. - * Types are enforced to validate everything: each value **must** have a `type`. - * @see https://github.com/cyrilletuzi/angular-async-local-storage/blob/master/docs/VALIDATION.md - */ - schema?: JSONSchema | null; - -} + JSONSchemaNumber, JSONSchemaString, JSONSchemaArrayOf, ValidationError, JSONValidator +} from '../validation'; +import { LocalDatabase, IDB_BROKEN_ERROR, LocalStorageDatabase } from '../databases'; +import { LS_PREFIX, LOCAL_STORAGE_PREFIX } from '../tokens'; @Injectable({ providedIn: 'root' }) -export class LocalStorage { +export class StorageMap { /** * Number of items in storage @@ -40,16 +23,6 @@ export class LocalStorage { } - /** - * Number of items in storage - * Alias of `.size` - */ - get length(): Observable { - - return this.size; - - } - /** * Constructor params are provided by Angular (but can also be passed manually in tests) * @param database Storage to use @@ -58,56 +31,50 @@ export class LocalStorage { * @param oldPrefix Prefix option prior to v8 to avoid collision for multiple apps on the same subdomain or for interoperability */ constructor( - private database: LocalDatabase, - private jsonValidator: JSONValidator = new JSONValidator(), - @Inject(LS_PREFIX) private LSPrefix = '', + protected database: LocalDatabase, + protected jsonValidator: JSONValidator = new JSONValidator(), + @Inject(LS_PREFIX) protected LSPrefix = '', // tslint:disable-next-line: deprecation - @Inject(LOCAL_STORAGE_PREFIX) private oldPrefix = '', + @Inject(LOCAL_STORAGE_PREFIX) protected oldPrefix = '', ) {} /** * Get an item value in storage. * The signature has many overloads due to validation, **please refer to the documentation.** - * Note you must pass the schema directly as the second argument. - * Passing the schema in an object `{ schema }` is deprecated and only here for backward compatibility: - * it may be removed in v9. * @see https://github.com/cyrilletuzi/angular-async-local-storage/blob/master/docs/VALIDATION.md * @param key The item's key - * @returns The item's value if the key exists, `null` otherwise, wrapped in a RxJS `Observable` + * @returns The item's value if the key exists, `undefined` otherwise, wrapped in a RxJS `Observable` */ - getItem(key: string, schema: JSONSchemaString): Observable; - getItem(key: string, schema: JSONSchemaInteger | JSONSchemaNumber): Observable; - getItem(key: string, schema: JSONSchemaBoolean): Observable; - getItem(key: string, schema: JSONSchemaArrayOf): Observable; - getItem(key: string, schema: JSONSchemaArrayOf): Observable; - getItem(key: string, schema: JSONSchemaArrayOf): Observable; - getItem(key: string, schema: JSONSchema | { schema: JSONSchema }): Observable; - getItem(key: string, schema?: null): Observable; - getItem(key: string, schema: JSONSchema | { schema: JSONSchema } | null | undefined = null) { + get(key: string, schema: JSONSchemaString): Observable; + get(key: string, schema: JSONSchemaInteger | JSONSchemaNumber): Observable; + get(key: string, schema: JSONSchemaBoolean): Observable; + get(key: string, schema: JSONSchemaArrayOf): Observable; + get(key: string, schema: JSONSchemaArrayOf): Observable; + get(key: string, schema: JSONSchemaArrayOf): Observable; + get(key: string, schema: JSONSchema): Observable; + get(key: string, schema?: null | undefined): Observable; + get(key: string, schema: JSONSchema | null | undefined = null) { /* Get the data in storage */ - return this.database.getItem(key).pipe( + return this.database.get(key).pipe( /* Check if `indexedDb` is broken */ - this.catchIDBBroken(() => this.database.getItem(key)), + this.catchIDBBroken(() => this.database.get(key)), mergeMap((data) => { - if (data === null) { + /* No need to validate if the data is empty */ + if ((data === undefined) || (data === null)) { - /* No need to validate if the data is `null` */ - return of(null); + return of(undefined); } else if (schema) { - /* Backward compatibility with version <= 7 */ - const schemaFinal: JSONSchema = ('schema' in schema) ? schema.schema : schema; - /* Validate data against a JSON schema if provied */ - if (!this.jsonValidator.validate(data, schemaFinal)) { + if (!this.jsonValidator.validate(data, schema)) { return throwError(new ValidationError()); } /* Data have been checked, so it's OK to cast */ - return of(data as T | null); + return of(data as T | undefined); } @@ -125,11 +92,17 @@ export class LocalStorage { * @param data The item's value * @returns A RxJS `Observable` to wait the end of the operation */ - setItem(key: string, data: any): Observable { + set(key: string, data: any): Observable { + + /* Storing `undefined` or `null` is useless and can cause issues in `indexedDb` in some browsers, + * so removing item instead for all storages to have a consistent API */ + if ((data === undefined) || (data === null)) { + return this.delete(key); + } - return this.database.setItem(key, data) + return this.database.set(key, data) /* Catch if `indexedDb` is broken */ - .pipe(this.catchIDBBroken(() => this.database.setItem(key, data))); + .pipe(this.catchIDBBroken(() => this.database.set(key, data))); } @@ -138,11 +111,11 @@ export class LocalStorage { * @param key The item's key * @returns A RxJS `Observable` to wait the end of the operation */ - removeItem(key: string): Observable { + delete(key: string): Observable { - return this.database.removeItem(key) + return this.database.delete(key) /* Catch if `indexedDb` is broken */ - .pipe(this.catchIDBBroken(() => this.database.removeItem(key))); + .pipe(this.catchIDBBroken(() => this.database.delete(key))); } @@ -150,7 +123,7 @@ export class LocalStorage { * Delete all items in storage * @returns A RxJS `Observable` to wait the end of the operation */ - clear(): Observable { + clear(): Observable { return this.database.clear() /* Catch if `indexedDb` is broken */ @@ -162,7 +135,7 @@ export class LocalStorage { * Get all keys stored in storage * @returns A list of the keys wrapped in a RxJS `Observable` */ - keys(): Observable { + keys(): Observable { return this.database.keys() /* Catch if `indexedDb` is broken */ @@ -186,7 +159,7 @@ export class LocalStorage { * RxJS operator to catch if `indexedDB` is broken * @param operationCallback Callback with the operation to redo */ - private catchIDBBroken(operationCallback: () => Observable): OperatorFunction { + private 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 9fda50de..912fee77 100644 --- a/projects/ngx-pwa/local-storage/src/lib/testing/cleaning.ts +++ b/projects/ngx-pwa/local-storage/src/lib/testing/cleaning.ts @@ -1,20 +1,18 @@ -import { LocalStorage } from '../lib.service'; -import { IndexedDBDatabase } from '../databases/indexeddb-database'; -import { LocalStorageDatabase } from '../databases/localstorage-database'; -import { MemoryDatabase } from '../databases/memory-database'; +import { StorageMap } from '../storages'; +import { IndexedDBDatabase, LocalStorageDatabase, MemoryDatabase } from '../databases'; /** * Helper to clear all data in storage to avoid tests overlap * @param done Jasmine helper to explicit when the operation has ended to avoid tests overlap - * @param localStorageService Service + * @param storageService Service */ -export function clearStorage(done: DoneFn, localStorageService: LocalStorage) { +export function clearStorage(done: DoneFn, storageService: StorageMap) { // tslint:disable-next-line: no-string-literal - if (localStorageService['database'] instanceof IndexedDBDatabase) { + if (storageService['database'] instanceof IndexedDBDatabase) { // tslint:disable-next-line: no-string-literal - const indexedDBService = localStorageService['database']; + const indexedDBService = storageService['database']; try { @@ -78,17 +76,17 @@ export function clearStorage(done: DoneFn, localStorageService: LocalStorage) { } // tslint:disable-next-line: no-string-literal - } else if (localStorageService['database'] instanceof LocalStorageDatabase) { + } else if (storageService['database'] instanceof LocalStorageDatabase) { localStorage.clear(); done(); // tslint:disable-next-line: no-string-literal - } else if (localStorageService['database'] instanceof MemoryDatabase) { + } else if (storageService['database'] instanceof MemoryDatabase) { // tslint:disable-next-line: no-string-literal - localStorageService['database']['memoryStorage'].clear(); + storageService['database']['memoryStorage'].clear(); done(); @@ -107,16 +105,16 @@ export function clearStorage(done: DoneFn, localStorageService: LocalStorage) { * as it's where the store is created * - to be able to delete the database, all connections to it must be closed * @param doneJasmine helper to explicit when the operation has ended to avoid tests overlap - * @param localStorageService Service + * @param storageService Service */ -export function closeAndDeleteDatabase(done: DoneFn, localStorageService: LocalStorage) { +export function closeAndDeleteDatabase(done: DoneFn, storageService: StorageMap) { /* Only `indexedDB` is concerned */ // tslint:disable-next-line: no-string-literal - if (localStorageService['database'] instanceof IndexedDBDatabase) { + if (storageService['database'] instanceof IndexedDBDatabase) { // tslint:disable-next-line: no-string-literal - const indexedDBService = localStorageService['database']; + const indexedDBService = storageService['database']; // tslint:disable-next-line: no-string-literal indexedDBService['database'].subscribe({ diff --git a/projects/ngx-pwa/local-storage/src/lib/interoperability.spec.ts b/projects/ngx-pwa/local-storage/src/lib/testing/interoperability.spec.ts similarity index 87% rename from projects/ngx-pwa/local-storage/src/lib/interoperability.spec.ts rename to projects/ngx-pwa/local-storage/src/lib/testing/interoperability.spec.ts index fb0dfd23..82de1569 100644 --- a/projects/ngx-pwa/local-storage/src/lib/interoperability.spec.ts +++ b/projects/ngx-pwa/local-storage/src/lib/testing/interoperability.spec.ts @@ -1,8 +1,8 @@ -import { clearStorage, closeAndDeleteDatabase } from './testing/cleaning'; -import { LocalStorage } from './lib.service'; -import { IndexedDBDatabase } from './databases/indexeddb-database'; -import { JSONSchema } from './validation/json-schema'; -import { DEFAULT_IDB_STORE_NAME } from './tokens'; +import { clearStorage, closeAndDeleteDatabase } from './cleaning'; +import { StorageMap } from '../storages'; +import { IndexedDBDatabase } from '../databases'; +import { JSONSchema } from '../validation'; +import { DEFAULT_IDB_STORE_NAME } from '../tokens'; const dbName = `interopStore${Date.now()}`; const index = 'test'; @@ -13,7 +13,7 @@ const index = 'test'; * @param done Jasmine helper to explicit when the operation has ended to avoid tests overlap * @param value Value to store */ -function testSetCompatibilityWithNativeAPI(localStorageService: LocalStorage, done: DoneFn, value: any) { +function testSetCompatibilityWithNativeAPI(localStorageService: StorageMap, done: DoneFn, value: any) { try { @@ -40,7 +40,7 @@ function testSetCompatibilityWithNativeAPI(localStorageService: LocalStorage, do request.addEventListener('success', () => { - localStorageService.setItem(index, 'world').subscribe({ + localStorageService.set(index, 'world').subscribe({ next: () => { expect().nothing(); @@ -105,7 +105,7 @@ function testSetCompatibilityWithNativeAPI(localStorageService: LocalStorage, do * @param done Jasmine helper to explicit when the operation has ended to avoid tests overlap * @param value Value to set and get */ -function testGetCompatibilityWithNativeAPI(localStorageService: LocalStorage, done: DoneFn, value: any, schema?: JSONSchema) { +function testGetCompatibilityWithNativeAPI(localStorageService: StorageMap, done: DoneFn, value: any, schema?: JSONSchema) { try { @@ -133,12 +133,12 @@ function testGetCompatibilityWithNativeAPI(localStorageService: LocalStorage, do request.addEventListener('success', () => { // TODO: Investigate schema param not working without the test - const request2 = schema ? localStorageService.getItem(index, schema) : localStorageService.getItem(index); + const request2 = schema ? localStorageService.get(index, schema) : localStorageService.get(index); request2.subscribe((result) => { - /* Transform `undefined` to `null` to align with the lib behavior */ - expect(result).toEqual((value !== undefined) ? value : null); + /* Transform `null` to `undefined` to align with the lib behavior */ + expect(result).toEqual((value !== null) ? (value) : undefined); dbOpen.result.close(); @@ -186,10 +186,10 @@ function testGetCompatibilityWithNativeAPI(localStorageService: LocalStorage, do describe('Interoperability', () => { - let localStorageService: LocalStorage; + let localStorageService: StorageMap; beforeAll(() => { - localStorageService = new LocalStorage(new IndexedDBDatabase(dbName)); + localStorageService = new StorageMap(new IndexedDBDatabase(dbName)); }); beforeEach((done) => { @@ -270,15 +270,14 @@ describe('Interoperability', () => { request.addEventListener('success', () => { - localStorageService.keys().subscribe((keys) => { - - for (const keyItem of keys) { + localStorageService.keys().subscribe({ + next: (keyItem) => { expect(typeof keyItem).toBe('string'); + }, + complete: () => { + dbOpen.result.close(); + done(); } - - dbOpen.result.close(); - done(); - }); }); diff --git a/projects/ngx-pwa/local-storage/src/lib/exceptions.ts b/projects/ngx-pwa/local-storage/src/lib/validation/exceptions.ts similarity index 60% rename from projects/ngx-pwa/local-storage/src/lib/exceptions.ts rename to projects/ngx-pwa/local-storage/src/lib/validation/exceptions.ts index 1d560f37..f1e4630e 100644 --- a/projects/ngx-pwa/local-storage/src/lib/exceptions.ts +++ b/projects/ngx-pwa/local-storage/src/lib/validation/exceptions.ts @@ -1,15 +1,3 @@ -/** - * Exception message when `indexedDB` is not working - */ -export const IDB_BROKEN_ERROR = 'indexedDB is not working'; - -/** - * Exception raised when `indexedDB` is not working - */ -export class IDBBrokenError extends Error { - message = IDB_BROKEN_ERROR; -} - /** * Exception message when a value is not valid against thr JSON schema */ @@ -22,4 +10,3 @@ Check your JSON schema, otherwise it means data has been corrupted.`; export class ValidationError extends Error { message = VALIDATION_ERROR; } - diff --git a/projects/ngx-pwa/local-storage/src/lib/validation/index.ts b/projects/ngx-pwa/local-storage/src/lib/validation/index.ts new file mode 100644 index 00000000..9a25fc59 --- /dev/null +++ b/projects/ngx-pwa/local-storage/src/lib/validation/index.ts @@ -0,0 +1,6 @@ +export { + JSONSchema, JSONSchemaObject, JSONSchemaArray, JSONSchemaArrayOf, + 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/public_api.ts b/projects/ngx-pwa/local-storage/src/public_api.ts index 1856763a..fde70a9a 100644 --- a/projects/ngx-pwa/local-storage/src/public_api.ts +++ b/projects/ngx-pwa/local-storage/src/public_api.ts @@ -6,12 +6,12 @@ export { JSONSchema, JSONSchemaBoolean, JSONSchemaInteger, JSONSchemaNumber, - JSONSchemaNumeric, JSONSchemaString, JSONSchemaArray, JSONSchemaArrayOf, JSONSchemaObject -} from './lib/validation/json-schema'; -export { LocalDatabase } from './lib/databases/local-database'; -export { LocalStorage, LSGetItemOptions } from './lib/lib.service'; + JSONSchemaNumeric, JSONSchemaString, JSONSchemaArray, JSONSchemaArrayOf, JSONSchemaObject, + VALIDATION_ERROR, ValidationError +} from './lib/validation'; +export { LocalDatabase } 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 { VALIDATION_ERROR, ValidationError } from './lib/exceptions';