@ngx-pwa/local-storage 17.0.0 → 19.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +14 -15
  2. package/fesm2022/ngx-pwa-local-storage.mjs +637 -573
  3. package/fesm2022/ngx-pwa-local-storage.mjs.map +1 -1
  4. package/lib/databases/exceptions.d.ts +2 -2
  5. package/lib/databases/indexeddb-database.d.ts +12 -15
  6. package/lib/databases/localstorage-database.d.ts +2 -2
  7. package/lib/databases/memory-database.d.ts +1 -1
  8. package/lib/providers.d.ts +25 -16
  9. package/lib/storage.module.d.ts +6 -4
  10. package/lib/storages/exceptions.d.ts +1 -1
  11. package/lib/storages/storage-map.service.d.ts +100 -46
  12. package/lib/tokens.d.ts +7 -0
  13. package/lib/validation/json-schema.d.ts +10 -8
  14. package/lib/validation/json-validator.d.ts +9 -9
  15. package/package.json +4 -6
  16. package/public-api.d.ts +2 -2
  17. package/schematics/collection.json +1 -1
  18. package/schematics/migration.json +1 -1
  19. package/schematics/ng-add/index.d.ts +1 -1
  20. package/schematics/ng-add/index.js +7 -8
  21. package/schematics/ng-add/index.js.map +1 -1
  22. package/schematics/ng-update/index.d.ts +1 -1
  23. package/schematics/ng-update/index.js +15 -26
  24. package/schematics/ng-update/index.js.map +1 -1
  25. package/schematics/utility/config.js +24 -33
  26. package/schematics/utility/config.js.map +1 -1
  27. package/schematics/utility/module.d.ts +1 -1
  28. package/schematics/utility/module.js +3 -4
  29. package/schematics/utility/module.js.map +1 -1
  30. package/esm2022/lib/databases/exceptions.mjs +0 -28
  31. package/esm2022/lib/databases/indexeddb-database.mjs +0 -306
  32. package/esm2022/lib/databases/local-database.mjs +0 -68
  33. package/esm2022/lib/databases/localstorage-database.mjs +0 -157
  34. package/esm2022/lib/databases/memory-database.mjs +0 -83
  35. package/esm2022/lib/providers.mjs +0 -57
  36. package/esm2022/lib/storage.module.mjs +0 -32
  37. package/esm2022/lib/storages/exceptions.mjs +0 -15
  38. package/esm2022/lib/storages/storage-map.service.mjs +0 -296
  39. package/esm2022/lib/tokens.mjs +0 -59
  40. package/esm2022/lib/validation/json-schema.mjs +0 -5
  41. package/esm2022/lib/validation/json-validator.mjs +0 -246
  42. package/esm2022/ngx-pwa-local-storage.mjs +0 -5
  43. package/esm2022/public-api.mjs +0 -9
@@ -1,7 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { InjectionToken, makeEnvironmentProviders, NgModule, inject, Injectable, PLATFORM_ID } from '@angular/core';
3
- import { ReplaySubject, fromEvent, race, throwError, of, asyncScheduler, Observable, from } from 'rxjs';
4
- import { map, mergeMap, first, takeWhile, tap, observeOn, catchError } from 'rxjs/operators';
3
+ import { of, throwError, Observable, asyncScheduler, from, ReplaySubject, fromEvent, race } from 'rxjs';
4
+ import { observeOn, mergeMap, map, first, takeWhile, tap, catchError } from 'rxjs/operators';
5
5
  import { isPlatformBrowser } from '@angular/common';
6
6
 
7
7
  /**
@@ -18,12 +18,12 @@ class IDBBrokenError extends Error {
18
18
  }
19
19
  }
20
20
  /**
21
- * Exception message when a value can't be serialized for `localStorage`
21
+ * Exception message when a value cannot be serialized for `localStorage`
22
22
  */
23
23
  const SERIALIZATION_ERROR = `The storage is currently localStorage,
24
24
  where data must be serialized, and the provided data can't be serialized.`;
25
25
  /**
26
- * Exception raised when a value can't be serialized for `localStorage`
26
+ * Exception throwned when a value cannot be serialized for `localStorage`
27
27
  */
28
28
  class SerializationError extends Error {
29
29
  constructor() {
@@ -92,26 +92,30 @@ const IDB_NO_WRAP = new InjectionToken("localStorageIDBWrap", {
92
92
 
93
93
  /**
94
94
  * Allows to add a prefix before `localStorage` keys.
95
- * *Use only* for interoperability with other APIs or to avoid collision for multiple apps on the same subdomain.
96
- * **WARNING: do not change this option in an app already deployed in production, as previously stored data would be lost.**
95
+ *
96
+ * *Use only* for interoperability with other APIs or to avoid collision for multiple applications on the same subdomain.
97
+ *
98
+ * **WARNING: do not change this option in an application already deployed in production, as previously stored data would be lost.**
97
99
  *
98
100
  * @example
99
- * bootstrapApplication(AppComponent, {
101
+ * export const appConfig: ApplicationConfig = {
100
102
  * providers: [provideLocalStoragePrefix('custom_')]
101
- * });
103
+ * };
102
104
  */
103
105
  function provideLocalStoragePrefix(prefix) {
104
106
  return makeEnvironmentProviders([{ provide: LS_PREFIX, useValue: prefix }]);
105
107
  }
106
108
  /**
107
109
  * Allows to change the name used for `indexedDB` database.
108
- * *Use only* for interoperability with other APIs or to avoid collision for multiple apps on the same subdomain.
109
- * **WARNING: do not change this option in an app already deployed in production, as previously stored data would be lost.**
110
+ *
111
+ * *Use only* for interoperability with other APIs or to avoid collision for multiple applications on the same subdomain.
112
+ *
113
+ * **WARNING: do not change this option in an application already deployed in production, as previously stored data would be lost.**
110
114
  *
111
115
  * @example
112
- * bootstrapApplication(AppComponent, {
116
+ * export const appConfig: ApplicationConfig = {
113
117
  * providers: [provideIndexedDBDataBaseName('custom')]
114
- * });
118
+ * };
115
119
  */
116
120
  function provideIndexedDBDataBaseName(name) {
117
121
  return makeEnvironmentProviders([{ provide: IDB_DB_NAME, useValue: name }]);
@@ -119,58 +123,69 @@ function provideIndexedDBDataBaseName(name) {
119
123
  /**
120
124
  * Allows to change the database version used for `indexedDB` database.
121
125
  * Must be an unsigned **integer**.
126
+ *
122
127
  * **Use with caution as the creation of the store depends on the version.**
123
- * *Use only* for interoperability with other APIs or to avoid collision for multiple apps on the same subdomain.
124
- * **WARNING: do not change this option in an app already deployed in production, as previously stored data would be lost.**
128
+ *
129
+ * *Use only* for interoperability with other APIs or to avoid collision for multiple applications on the same subdomain.
130
+ *
131
+ * **WARNING: do not change this option in an applicattion already deployed in production, as previously stored data would be lost.**
125
132
  *
126
133
  * @example
127
- * bootstrapApplication(AppComponent, {
134
+ * export const appConfig: ApplicationConfig = {
128
135
  * providers: [provideIndexedDBDataBaseVersion(2)]
129
- * });
136
+ * };
130
137
  */
131
138
  function provideIndexedDBDataBaseVersion(version) {
132
139
  return makeEnvironmentProviders([{ provide: IDB_DB_VERSION, useValue: version }]);
133
140
  }
134
141
  /**
135
142
  * Allows to change the name used for `indexedDB` object store.
143
+ *
136
144
  * *Use only* for interoperability with other APIs.
137
- * **WARNING: do not change this option in an app already deployed in production, as previously stored data would be lost.**
145
+ *
146
+ * **WARNING: do not change this option in an application already deployed in production, as previously stored data would be lost.**
138
147
  *
139
148
  * @example
140
- * bootstrapApplication(AppComponent, {
149
+ * export const appConfig: ApplicationConfig = {
141
150
  * providers: [provideIndexedDBStoreName('custom')]
142
- * });
151
+ * };
143
152
  */
144
153
  function provideIndexedDBStoreName(name) {
145
154
  return makeEnvironmentProviders([{ provide: IDB_STORE_NAME, useValue: name }]);
146
155
  }
147
156
 
148
157
  /**
149
- * This module is only here for backward compatibility, do not add it by yourself
158
+ * This module is only here for backward compatibility, **do not add it by yourself**
159
+ *
160
+ * @ignore
150
161
  */
151
162
  class StorageModule {
152
163
  /**
153
164
  * Only useful to provide options, otherwise it does nothing.
154
- * **Must be used at initialization, ie. in `AppModule`, and must not be loaded again in another module.**
155
165
  *
166
+ * **Must be used at initialization, ie. in `AppModule`, and must not be loaded again in another module.**
156
167
  */
157
168
  static forRoot(config) {
158
169
  return {
159
170
  ngModule: StorageModule,
160
171
  providers: [
161
- config.LSPrefix ? { provide: LS_PREFIX, useValue: config.LSPrefix } : [],
162
- config.IDBDBName ? { provide: IDB_DB_NAME, useValue: config.IDBDBName } : [],
163
- config.IDBStoreName ? { provide: IDB_STORE_NAME, useValue: config.IDBStoreName } : [],
164
- config.IDBDBVersion ? { provide: IDB_DB_VERSION, useValue: config.IDBDBVersion } : [],
172
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
173
+ config.LSPrefix !== undefined ? { provide: LS_PREFIX, useValue: config.LSPrefix } : [],
174
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
175
+ config.IDBDBName !== undefined ? { provide: IDB_DB_NAME, useValue: config.IDBDBName } : [],
176
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
177
+ config.IDBStoreName !== undefined ? { provide: IDB_STORE_NAME, useValue: config.IDBStoreName } : [],
178
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
179
+ config.IDBDBVersion !== undefined ? { provide: IDB_DB_VERSION, useValue: config.IDBDBVersion } : [],
165
180
  (config.IDBNoWrap === false) ? { provide: IDB_NO_WRAP, useValue: config.IDBNoWrap } : [],
166
181
  ],
167
182
  };
168
183
  }
169
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.0", ngImport: i0, type: StorageModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
170
- static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "17.0.0", ngImport: i0, type: StorageModule }); }
171
- static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "17.0.0", ngImport: i0, type: StorageModule }); }
184
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: StorageModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
185
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.0.0", ngImport: i0, type: StorageModule }); }
186
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: StorageModule }); }
172
187
  }
173
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.0", ngImport: i0, type: StorageModule, decorators: [{
188
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: StorageModule, decorators: [{
174
189
  type: NgModule
175
190
  }] });
176
191
 
@@ -180,7 +195,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.0", ngImpor
180
195
  const VALIDATION_ERROR = `Data stored is not valid against the provided JSON schema.
181
196
  Check your JSON schema, otherwise it means data has been corrupted.`;
182
197
  /**
183
- * Exception raised when a value is not valid against the JSON schema
198
+ * Exception throwned when a value is not valid against the JSON schema
184
199
  */
185
200
  class ValidationError extends Error {
186
201
  constructor() {
@@ -189,536 +204,601 @@ class ValidationError extends Error {
189
204
  }
190
205
  }
191
206
 
192
- class IndexedDBDatabase {
207
+ class LocalStorageDatabase {
193
208
  constructor() {
194
- /**
195
- * `indexedDB` database connection, wrapped in a RxJS `ReplaySubject` to be able to access the connection
196
- * even after the connection success event happened
197
- */
198
- this.database = new ReplaySubject(1);
199
- /**
200
- * Index used when wrapping value. *For backward compatibility only.*
201
- */
202
- this.wrapIndex = "value";
203
- this.dbName = inject(IDB_DB_NAME);
204
- this.storeName = inject(IDB_STORE_NAME);
205
- this.dbVersion = inject(IDB_DB_VERSION);
206
- this.noWrap = inject(IDB_NO_WRAP);
207
- /* Connect to `indexedDB`, with prefix if provided by the user */
208
- this.connect();
209
- }
210
- /**
211
- * Information about `indexedDB` connection. *Only useful for interoperability.*
212
- * @returns `indexedDB` database name, store name and database version
213
- */
214
- get backingStore() {
215
- return {
216
- database: this.dbName,
217
- store: this.storeName,
218
- version: this.dbVersion,
219
- };
209
+ /* Prefix if asked, or no prefix otherwise */
210
+ this.prefix = inject(LS_PREFIX);
220
211
  }
221
212
  /**
222
- * Number of items in our `indexedDB` database and object store
213
+ * Number of items in `localStorage`
223
214
  */
224
215
  get size() {
225
- /* Open a transaction in read-only mode */
226
- return this.transaction("readonly").pipe(mergeMap((transactionData) => {
227
- const { store, events } = transactionData;
228
- /* Request to know the number of items */
229
- const request = store.count();
230
- /* Return the result */
231
- return events.pipe(map(() => request.result));
232
- }),
233
- /* The observable will complete after the first value */
234
- first());
216
+ /* Wrap in a RxJS `Observable` to be consistent with other storages */
217
+ return of(localStorage.length);
235
218
  }
236
219
  /**
237
- * Gets an item value in our `indexedDB` store
220
+ * Gets an item value in `localStorage`
238
221
  * @param key The item's key
239
- * @returns The item's value if the key exists, `undefined` otherwise, wrapped in an RxJS `Observable`
222
+ * @returns The item's value if the key exists, `undefined` otherwise, wrapped in a RxJS `Observable`
240
223
  */
241
224
  get(key) {
242
- /* Open a transaction in read-only mode */
243
- return this.transaction("readonly").pipe(mergeMap((transactionData) => {
244
- const { store, events } = transactionData;
245
- /* Request the value with the key provided by the user */
246
- const request = store.get(key);
247
- /* Listen events and return the result */
248
- return events.pipe(map(() => {
249
- if ((request.result !== undefined) && (request.result !== null)) {
250
- /* Prior to v8, the value was wrapped in an `{ value: ...}` object */
251
- if (!this.noWrap && (typeof request.result === "object") && (this.wrapIndex in request.result) &&
252
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
253
- (request.result[this.wrapIndex] !== undefined) && (request.result[this.wrapIndex] !== null)) {
254
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
255
- return request.result[this.wrapIndex];
256
- }
257
- else {
258
- /* Cast to the wanted type */
259
- return request.result;
260
- }
261
- }
262
- /* Return `undefined` if the value is empty */
263
- return undefined;
264
- }));
265
- }),
266
- /* The observable will complete after the first value */
267
- first());
225
+ /* Get raw data */
226
+ const unparsedData = localStorage.getItem(this.prefixKey(key));
227
+ /* No need to parse if data is `null` or `undefined` */
228
+ if (unparsedData !== null) {
229
+ /* Try to parse */
230
+ try {
231
+ const parsedData = JSON.parse(unparsedData);
232
+ /* Wrap in a RxJS `Observable` to be consistent with other storages */
233
+ return of(parsedData);
234
+ }
235
+ catch (error) {
236
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
237
+ return throwError(() => error);
238
+ }
239
+ }
240
+ return of(undefined);
268
241
  }
269
242
  /**
270
- * Sets an item in our `indexedDB` store
243
+ * Store an item in `localStorage`
271
244
  * @param key The item's key
272
245
  * @param data The item's value
273
- * @returns An RxJS `Observable` to wait the end of the operation
246
+ * @returns A RxJS `Observable` to wait the end of the operation
274
247
  */
275
248
  set(key, data) {
276
- /* Storing `undefined` in `indexedDb` can cause issues in some browsers so removing item instead */
277
- if (data === undefined) {
278
- return this.delete(key);
249
+ let serializedData = null;
250
+ /* Check if data can be serialized */
251
+ const dataPrototype = Object.getPrototypeOf(data);
252
+ if ((typeof data === "object") && (data !== null) && !Array.isArray(data) &&
253
+ !((dataPrototype === Object.prototype) || (dataPrototype === null))) {
254
+ return throwError(() => new SerializationError());
279
255
  }
280
- /* Open a transaction in write mode */
281
- return this.transaction("readwrite").pipe(mergeMap((transactionData) => {
282
- const { store, events } = transactionData;
283
- /* Prior to v8, data was wrapped in a `{ value: ... }` object */
284
- const dataToStore = this.noWrap ? data : { [this.wrapIndex]: data };
285
- /* Add if the item is not existing yet, or update otherwise */
286
- store.put(dataToStore, key);
287
- /* Listen to events and return `undefined` as no value is expected */
288
- return events.pipe(map(() => undefined));
289
- }),
290
- /* The observable will complete after the first value */
291
- first());
256
+ /* Try to stringify (can fail on circular references) */
257
+ try {
258
+ serializedData = JSON.stringify(data);
259
+ }
260
+ catch (error) {
261
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
262
+ return throwError(() => error);
263
+ }
264
+ /* Can fail if storage quota is exceeded */
265
+ try {
266
+ localStorage.setItem(this.prefixKey(key), serializedData);
267
+ }
268
+ catch (error) {
269
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
270
+ return throwError(() => error);
271
+ }
272
+ /* Wrap in a RxJS `Observable` to be consistent with other storages */
273
+ return of(undefined);
292
274
  }
293
275
  /**
294
- * Deletes an item in our `indexedDB` store
276
+ * Deletes an item in `localStorage`
295
277
  * @param key The item's key
296
- * @returns An RxJS `Observable` to wait the end of the operation
278
+ * @returns A RxJS `Observable` to wait the end of the operation
297
279
  */
298
280
  delete(key) {
299
- /* Open a transaction in write mode */
300
- return this.transaction("readwrite").pipe(mergeMap((transactionData) => {
301
- const { store, events } = transactionData;
302
- /* Delete the item in store */
303
- store.delete(key);
304
- /* Listen to events and return `undefined` as no data is expected here */
305
- return events.pipe(map(() => undefined));
306
- }),
307
- /* The observable will complete after the first value */
308
- first());
281
+ localStorage.removeItem(this.prefixKey(key));
282
+ /* Wrap in a RxJS `Observable` to be consistent with other storages */
283
+ return of(undefined);
309
284
  }
310
285
  /**
311
- * Deletes all items from our `indexedDB` objet store
312
- * @returns An RxJS `Observable` to wait the end of the operation
286
+ * Deletes all items in `localStorage`
287
+ * @returns A RxJS `Observable` to wait the end of the operation
313
288
  */
314
289
  clear() {
315
- /* Open a transaction in write mode */
316
- return this.transaction("readwrite").pipe(mergeMap((transactionData) => {
317
- const { store, events } = transactionData;
318
- /* Delete all items in object store */
319
- store.clear();
320
- /* Listen to events and return `undefined` as no data is expected here */
321
- return events.pipe(map(() => undefined));
322
- }),
323
- /* The observable will complete */
324
- first());
290
+ localStorage.clear();
291
+ /* Wrap in a RxJS `Observable` to be consistent with other storages */
292
+ return of(undefined);
325
293
  }
326
294
  /**
327
- * Get all the keys in our `indexedDB` store
328
- * @returns An RxJS `Observable` iterating on each key
295
+ * Get all keys in `localStorage`
296
+ * Note the order of the keys may be inconsistent in Firefox
297
+ * @returns A RxJS `Observable` iterating on keys
329
298
  */
330
299
  keys() {
331
- /* Open a transaction in read-only mode */
332
- return this.transaction("readonly").pipe(
333
- /* `first()` is used as the final operator in other methods to complete the `Observable`
334
- * (as it all starts from a `ReplaySubject` which never ends),
335
- * but as this method is iterating over multiple values, `first()` **must** be used here */
336
- first(), mergeMap((transactionData) => {
337
- const { store } = transactionData;
338
- /* Open a cursor on the store
339
- * Avoid issues like https://github.com/cyrilletuzi/angular-async-local-storage/issues/69 */
340
- const request = store.openKeyCursor();
341
- /* Listen to success event */
342
- const success$ = fromEvent(request, "success").pipe(
343
- /* Stop the `Observable` when the cursor is `null` */
344
- takeWhile(() => (request.result !== null)),
345
- /* This lib only allows string keys, but user could have added other types of keys from outside
346
- * It's OK to cast as the cursor as been tested in the previous operator */
347
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-base-to-string
348
- map(() => request.result.key.toString()),
349
- /* Iterate on the cursor */
350
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
351
- tap(() => { request.result.continue(); }));
352
- /* Listen to error event and if so, throw an error */
353
- const error$ = this.listenError(request);
354
- /* Choose the first event to occur */
355
- return race([success$, error$]);
356
- }));
300
+ /* Create an `Observable` from keys */
301
+ return new Observable((subscriber) => {
302
+ /* Iteretate over all the indexes */
303
+ for (let index = 0; index < localStorage.length; index += 1) {
304
+ /* Cast as we are sure in this case the key is not `null` */
305
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Ensured by the logic
306
+ subscriber.next(this.getUnprefixedKey(index));
307
+ }
308
+ subscriber.complete();
309
+ }).pipe(
310
+ /* Required to work like other databases which are asynchronous */
311
+ observeOn(asyncScheduler));
357
312
  }
358
313
  /**
359
- * Check if a key exists in our `indexedDB` store
360
- * @returns An RxJS `Observable` telling if the key exists or not
314
+ * Check if a key exists in `localStorage`
315
+ * @param key The item's key
316
+ * @returns A RxJS `Observable` telling if the key exists or not
361
317
  */
362
318
  has(key) {
363
- /* Open a transaction in read-only mode */
364
- return this.transaction("readonly").pipe(mergeMap((transactionData) => {
365
- const { store, events } = transactionData;
366
- /* Check if the key exists in the store
367
- * Fixes https://github.com/cyrilletuzi/angular-async-local-storage/issues/69
368
- */
369
- const request = store.getKey(key);
370
- /* Listen to events and return `true` or `false` */
371
- return events.pipe(map(() => (request.result !== undefined) ? true : false));
372
- }),
373
- /* The observable will complete */
374
- first());
319
+ /* Itérate over all indexes in storage */
320
+ for (let index = 0; index < localStorage.length; index += 1) {
321
+ if (key === this.getUnprefixedKey(index)) {
322
+ /* Wrap in a RxJS `Observable` to be consistent with other storages */
323
+ return of(true);
324
+ }
325
+ }
326
+ /* Wrap in a RxJS `Observable` to be consistent with other storages */
327
+ return of(false);
375
328
  }
376
329
  /**
377
- * Connects to `indexedDB` and creates the object store on first time
330
+ * Get an unprefixed key
331
+ * @param index Index of the key
332
+ * @returns The unprefixed key name if exists, `null` otherwise
378
333
  */
379
- connect() {
380
- let request;
381
- /* Connect to `indexedDB`
382
- * Will fail in Safari cross-origin iframes
383
- * @see {@link https://github.com/cyrilletuzi/angular-async-local-storage/issues/42} */
384
- try {
385
- /* Do NOT explicit `window` here, as `indexedDB` could be used from a web worker too */
386
- request = indexedDB.open(this.dbName, this.dbVersion);
334
+ getUnprefixedKey(index) {
335
+ /* Get the key in storage: may have a prefix */
336
+ const prefixedKey = localStorage.key(index);
337
+ if (prefixedKey !== null) {
338
+ /* If no prefix, the key is already good, otherwrite strip the prefix */
339
+ return this.prefix === "" ? prefixedKey : prefixedKey.substring(this.prefix.length);
387
340
  }
388
- catch {
389
- this.database.error(new IDBBrokenError());
390
- return;
391
- }
392
- /* Create store on first connection */
393
- this.createStore(request);
394
- /* Listen to success and error events */
395
- const success$ = fromEvent(request, "success");
396
- const error$ = this.listenError(request);
397
- /* Choose the first to occur */
398
- race([success$, error$])
399
- /* The observable will complete */
400
- .pipe(first())
401
- .subscribe({
402
- next: () => {
403
- /* Register the database connection in the `ReplaySubject` for further access */
404
- this.database.next(request.result);
405
- },
406
- error: () => {
407
- /* Firefox private mode issue: fallback storage if IndexedDb connection is failing
408
- * @see {@link https://bugzilla.mozilla.org/show_bug.cgi?id=781982}
409
- * @see {@link https://github.com/cyrilletuzi/angular-async-local-storage/issues/26} */
410
- this.database.error(new IDBBrokenError());
411
- },
412
- });
413
- }
414
- /**
415
- * Create store on first use of `indexedDB`
416
- * @param request `indexedDB` database opening request
417
- */
418
- createStore(request) {
419
- /* Listen to the event fired on first connection */
420
- fromEvent(request, "upgradeneeded")
421
- /* The observable will complete */
422
- .pipe(first())
423
- .subscribe({
424
- next: () => {
425
- /* Check if the store already exists, to avoid error */
426
- if (!request.result.objectStoreNames.contains(this.storeName)) {
427
- /* Create the object store */
428
- request.result.createObjectStore(this.storeName);
429
- }
430
- }
431
- });
432
- }
433
- /**
434
- * Open an `indexedDB` transaction and get our store
435
- * @param mode `readonly` or `readwrite`
436
- * @returns An `indexedDB` transaction store and events, wrapped in an RxJS `Observable`
437
- */
438
- transaction(mode) {
439
- /* From the `indexedDB` connection, open a transaction and get the store */
440
- return this.database
441
- .pipe(mergeMap((database) => {
442
- let transaction;
443
- try {
444
- transaction = database.transaction([this.storeName], mode);
445
- }
446
- catch (error) {
447
- /* The store could have been deleted from outside */
448
- return throwError(() => error);
449
- }
450
- /* Get the store from the transaction */
451
- const store = transaction.objectStore(this.storeName);
452
- /* Listen transaction `complete` and `error` events */
453
- const events = this.listenTransactionEvents(transaction);
454
- return of({ store, events });
455
- }));
456
- }
457
- /**
458
- * Listen errors on a transaction or request, and throw if trigerred
459
- * @param transactionOrRequest `indexedDb` transaction or request to listen
460
- * @returns An `Observable` listening to errors
461
- */
462
- listenError(transactionOrRequest) {
463
- return fromEvent(transactionOrRequest, "error").pipe(
464
- /* Throw on error to be able to catch errors in RxJS way.
465
- * Here `event.target` must be used, as `transactionOrRequest.error` will be `null`
466
- * if we are on the request and the error is only triggered later by the transaction */
467
- mergeMap((event) => throwError(() => event.target?.error)));
341
+ return null;
468
342
  }
469
343
  /**
470
- * Listen transaction `complete` and `error` events
471
- * @param transaction Transaction to listen
472
- * @returns An `Observable` listening to transaction `complete` and `error` events
344
+ * Add the prefix to a key
345
+ * @param key The key name
346
+ * @returns The prefixed key name
473
347
  */
474
- listenTransactionEvents(transaction) {
475
- /* Listen to the `complete` event */
476
- const complete$ = fromEvent(transaction, "complete");
477
- /* Listen to the `error` event */
478
- const error$ = this.listenError(transaction);
479
- /* Choose the first event to occur */
480
- return race([complete$, error$]);
348
+ prefixKey(key) {
349
+ return `${this.prefix}${key}`;
481
350
  }
482
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.0", ngImport: i0, type: IndexedDBDatabase, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
483
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.0", ngImport: i0, type: IndexedDBDatabase, providedIn: "root" }); }
351
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: LocalStorageDatabase, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
352
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: LocalStorageDatabase, providedIn: "root" }); }
484
353
  }
485
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.0", ngImport: i0, type: IndexedDBDatabase, decorators: [{
354
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: LocalStorageDatabase, decorators: [{
486
355
  type: Injectable,
487
356
  args: [{
488
357
  providedIn: "root"
489
358
  }]
490
359
  }], ctorParameters: () => [] });
491
360
 
492
- class LocalStorageDatabase {
361
+ class MemoryDatabase {
493
362
  constructor() {
494
- /* Prefix if asked, or no prefix otherwise */
495
- this.prefix = inject(LS_PREFIX) || "";
363
+ /**
364
+ * Memory storage
365
+ */
366
+ this.memoryStorage = new Map();
496
367
  }
497
368
  /**
498
- * Number of items in `localStorage`
369
+ * Number of items in memory
499
370
  */
500
371
  get size() {
501
372
  /* Wrap in a RxJS `Observable` to be consistent with other storages */
502
- return of(localStorage.length);
373
+ return of(this.memoryStorage.size);
503
374
  }
504
375
  /**
505
- * Gets an item value in `localStorage`
376
+ * Gets an item value in memory
506
377
  * @param key The item's key
507
378
  * @returns The item's value if the key exists, `undefined` otherwise, wrapped in a RxJS `Observable`
508
379
  */
509
380
  get(key) {
510
- /* Get raw data */
511
- const unparsedData = localStorage.getItem(this.prefixKey(key));
512
- let parsedData;
513
- /* No need to parse if data is `null` or `undefined` */
514
- if (unparsedData !== null) {
515
- /* Try to parse */
516
- try {
517
- parsedData = JSON.parse(unparsedData);
518
- }
519
- catch (error) {
520
- return throwError(() => error);
521
- }
522
- }
523
381
  /* Wrap in a RxJS `Observable` to be consistent with other storages */
524
- return of(parsedData);
382
+ return of(this.memoryStorage.get(key));
525
383
  }
526
384
  /**
527
- * Store an item in `localStorage`
385
+ * Sets an item in memory
528
386
  * @param key The item's key
529
387
  * @param data The item's value
530
388
  * @returns A RxJS `Observable` to wait the end of the operation
531
389
  */
532
390
  set(key, data) {
533
- let serializedData = null;
534
- /* Check if data can be serialized */
535
- const dataPrototype = Object.getPrototypeOf(data);
536
- if ((typeof data === "object") && (data !== null) && !Array.isArray(data) &&
537
- !((dataPrototype === Object.prototype) || (dataPrototype === null))) {
538
- return throwError(() => new SerializationError());
539
- }
540
- /* Try to stringify (can fail on circular references) */
541
- try {
542
- serializedData = JSON.stringify(data);
543
- }
544
- catch (error) {
545
- return throwError(() => error);
546
- }
547
- /* Can fail if storage quota is exceeded */
548
- try {
549
- localStorage.setItem(this.prefixKey(key), serializedData);
550
- }
551
- catch (error) {
552
- return throwError(() => error);
553
- }
391
+ this.memoryStorage.set(key, data);
554
392
  /* Wrap in a RxJS `Observable` to be consistent with other storages */
555
393
  return of(undefined);
556
394
  }
557
395
  /**
558
- * Deletes an item in `localStorage`
396
+ * Deletes an item in memory
559
397
  * @param key The item's key
560
398
  * @returns A RxJS `Observable` to wait the end of the operation
561
399
  */
562
400
  delete(key) {
563
- localStorage.removeItem(this.prefixKey(key));
401
+ this.memoryStorage.delete(key);
564
402
  /* Wrap in a RxJS `Observable` to be consistent with other storages */
565
403
  return of(undefined);
566
404
  }
567
405
  /**
568
- * Deletes all items in `localStorage`
406
+ * Deletes all items in memory
569
407
  * @returns A RxJS `Observable` to wait the end of the operation
570
408
  */
571
409
  clear() {
572
- localStorage.clear();
410
+ this.memoryStorage.clear();
573
411
  /* Wrap in a RxJS `Observable` to be consistent with other storages */
574
412
  return of(undefined);
575
413
  }
576
414
  /**
577
- * Get all keys in `localStorage`
578
- * Note the order of the keys may be inconsistent in Firefox
415
+ * Get all keys in memory
579
416
  * @returns A RxJS `Observable` iterating on keys
580
417
  */
581
418
  keys() {
582
419
  /* Create an `Observable` from keys */
583
- return new Observable((subscriber) => {
584
- /* Iteretate over all the indexes */
585
- for (let index = 0; index < localStorage.length; index += 1) {
586
- /* Cast as we are sure in this case the key is not `null` */
587
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
588
- subscriber.next(this.getUnprefixedKey(index));
589
- }
590
- subscriber.complete();
591
- }).pipe(
592
- /* Required to work like other databases which are asynchronous */
593
- observeOn(asyncScheduler));
420
+ return from(this.memoryStorage.keys());
594
421
  }
595
422
  /**
596
- * Check if a key exists in `localStorage`
597
- * @param key The item's key
598
- * @returns A RxJS `Observable` telling if the key exists or not
423
+ * Check if a key exists in memory
424
+ * @param key Key name
425
+ * @returns a RxJS `Observable` telling if the key exists or not
599
426
  */
600
427
  has(key) {
601
- /* Itérate over all indexes in storage */
602
- for (let index = 0; index < localStorage.length; index += 1) {
603
- if (key === this.getUnprefixedKey(index)) {
604
- /* Wrap in a RxJS `Observable` to be consistent with other storages */
605
- return of(true);
606
- }
607
- }
608
428
  /* Wrap in a RxJS `Observable` to be consistent with other storages */
609
- return of(false);
429
+ return of(this.memoryStorage.has(key));
610
430
  }
611
- /**
612
- * Get an unprefixed key
613
- * @param index Index of the key
614
- * @returns The unprefixed key name if exists, `null` otherwise
431
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: MemoryDatabase, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
432
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: MemoryDatabase, providedIn: "root" }); }
433
+ }
434
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: MemoryDatabase, decorators: [{
435
+ type: Injectable,
436
+ args: [{
437
+ providedIn: "root"
438
+ }]
439
+ }] });
440
+
441
+ /**
442
+ * Factory to create a storage according to browser support
443
+ * @see {@link https://github.com/cyrilletuzi/angular-async-local-storage/blob/main/docs/BROWSERS_SUPPORT.md}
444
+ */
445
+ function localDatabaseFactory() {
446
+ const platformId = inject(PLATFORM_ID);
447
+ /* When storage is fully disabled in browser (via the "Block all cookies" option),
448
+ * just trying to check `indexedDB` or `localStorage` variables causes a security exception.
449
+ * Prevents https://github.com/cyrilletuzi/angular-async-local-storage/issues/118
615
450
  */
616
- getUnprefixedKey(index) {
617
- /* Get the key in storage: may have a prefix */
618
- const prefixedKey = localStorage.key(index);
619
- if (prefixedKey !== null) {
620
- /* If no prefix, the key is already good, otherwrite strip the prefix */
621
- return !this.prefix ? prefixedKey : prefixedKey.substring(this.prefix.length);
451
+ try {
452
+ // Do not explicit `window` here, as the global object is not the same in web workers
453
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- They can be undefined in some browsers scenarios
454
+ if (isPlatformBrowser(platformId) && (indexedDB !== undefined) && (indexedDB !== null) && ("open" in indexedDB)) {
455
+ /* Check:
456
+ * - if we are in a browser context (issue: server-side rendering)
457
+ * - it could exist but be `undefined` or `null`
458
+ * - it could exists but not having a working API
459
+ * Will be the case for:
460
+ * - All other browsers in normal mode
461
+ * - Chromium / Safari / Firefox private mode, but in this case, data will be swiped when the user leaves the app */
462
+ return new IndexedDBDatabase();
463
+ }
464
+ else if (isPlatformBrowser(platformId)
465
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- They can be undefined in some browsers scenarios
466
+ && (localStorage !== undefined) && (localStorage !== null) && ("getItem" in localStorage)) {
467
+ /* Check:
468
+ * - if we are in a browser context (issue: server-side rendering)
469
+ * - if `localStorage` exists (to be sure)
470
+ * - it could exists but not having a working API
471
+ * Will be the case for:
472
+ * - Safari cross-origin iframes, detected later in `IndexedDBDatabase.connect()`
473
+ * @see {@link https://github.com/cyrilletuzi/angular-async-local-storage/issues/42}
474
+ */
475
+ return new LocalStorageDatabase();
622
476
  }
623
- return null;
624
477
  }
625
- /**
626
- * Add the prefix to a key
627
- * @param key The key name
628
- * @returns The prefixed key name
629
- */
630
- prefixKey(key) {
631
- return `${this.prefix}${key}`;
478
+ catch {
479
+ // Nothing to do
632
480
  }
633
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.0", ngImport: i0, type: LocalStorageDatabase, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
634
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.0", ngImport: i0, type: LocalStorageDatabase, providedIn: "root" }); }
481
+ /* Will be the case for:
482
+ * - In browsers if storage has been fully disabled (via the "Block all cookies" option)
483
+ * - Server-side rendering
484
+ * - All other non-browser context
485
+ */
486
+ return new MemoryDatabase();
487
+ }
488
+ class LocalDatabase {
489
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: LocalDatabase, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
490
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: LocalDatabase, providedIn: "root", useFactory: localDatabaseFactory }); }
635
491
  }
636
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.0", ngImport: i0, type: LocalStorageDatabase, decorators: [{
492
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: LocalDatabase, decorators: [{
637
493
  type: Injectable,
638
494
  args: [{
639
- providedIn: "root"
495
+ providedIn: "root",
496
+ useFactory: localDatabaseFactory,
640
497
  }]
641
- }], ctorParameters: () => [] });
498
+ }] });
642
499
 
643
- class MemoryDatabase {
500
+ class IndexedDBDatabase {
644
501
  constructor() {
645
502
  /**
646
- * Memory storage
503
+ * `indexedDB` database connection, wrapped in a RxJS `ReplaySubject` to be able to access the connection
504
+ * even after the connection success event happened
647
505
  */
648
- this.memoryStorage = new Map();
506
+ this.database = new ReplaySubject(1);
507
+ /**
508
+ * Index used when wrapping value. *For backward compatibility only.*
509
+ */
510
+ this.wrapIndex = "value";
511
+ this.dbName = inject(IDB_DB_NAME);
512
+ this.storeName = inject(IDB_STORE_NAME);
513
+ this.dbVersion = inject(IDB_DB_VERSION);
514
+ this.noWrap = inject(IDB_NO_WRAP);
515
+ /* Connect to `indexedDB`, with prefix if provided by the user */
516
+ this.connect();
517
+ }
518
+ /**
519
+ * Information about `indexedDB` connection. *Only useful for interoperability.*
520
+ * @returns `indexedDB` database name, store name and database version
521
+ */
522
+ get backingStore() {
523
+ return {
524
+ database: this.dbName,
525
+ store: this.storeName,
526
+ version: this.dbVersion,
527
+ };
528
+ }
529
+ /**
530
+ * Number of items in our `indexedDB` database and object store
531
+ */
532
+ get size() {
533
+ /* Open a transaction in read-only mode */
534
+ return this.transaction("readonly").pipe(mergeMap((transactionData) => {
535
+ const { store, events } = transactionData;
536
+ /* Request to know the number of items */
537
+ const request = store.count();
538
+ /* Return the result */
539
+ return events.pipe(map(() => request.result));
540
+ }),
541
+ /* The observable will complete after the first value */
542
+ first());
543
+ }
544
+ /**
545
+ * Gets an item value in our `indexedDB` store
546
+ * @param key The item's key
547
+ * @returns The item's value if the key exists, `undefined` otherwise, wrapped in an RxJS `Observable`
548
+ */
549
+ get(key) {
550
+ /* Open a transaction in read-only mode */
551
+ return this.transaction("readonly").pipe(mergeMap((transactionData) => {
552
+ const { store, events } = transactionData;
553
+ /* Request the value with the key provided by the user */
554
+ const request = store.get(key);
555
+ /* Listen events and return the result */
556
+ return events.pipe(map(() => {
557
+ if ((request.result !== undefined) && (request.result !== null)) {
558
+ /* Prior to v8, the value was wrapped in an `{ value: ...}` object */
559
+ if (!this.noWrap && (typeof request.result === "object") && (this.wrapIndex in request.result) &&
560
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- Required by indexedDb behavior
561
+ (request.result[this.wrapIndex] !== undefined) && (request.result[this.wrapIndex] !== null)) {
562
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- Required by indexedDb behavior
563
+ return request.result[this.wrapIndex];
564
+ }
565
+ else {
566
+ /* Cast to the wanted type */
567
+ return request.result;
568
+ }
569
+ }
570
+ /* Return `undefined` if the value is empty */
571
+ return undefined;
572
+ }));
573
+ }),
574
+ /* The observable will complete after the first value */
575
+ first());
576
+ }
577
+ /**
578
+ * Sets an item in our `indexedDB` store
579
+ * @param key The item's key
580
+ * @param data The item's value
581
+ * @returns An RxJS `Observable` to wait the end of the operation
582
+ */
583
+ set(key, data) {
584
+ /* Storing `undefined` in `indexedDb` can cause issues in some browsers so removing item instead */
585
+ if (data === undefined) {
586
+ return this.delete(key);
587
+ }
588
+ /* Open a transaction in write mode */
589
+ return this.transaction("readwrite").pipe(mergeMap((transactionData) => {
590
+ const { store, events } = transactionData;
591
+ /* Prior to v8, data was wrapped in a `{ value: ... }` object */
592
+ const dataToStore = this.noWrap ? data : { [this.wrapIndex]: data };
593
+ /* Add if the item is not existing yet, or update otherwise */
594
+ store.put(dataToStore, key);
595
+ /* Listen to events and return `undefined` as no value is expected */
596
+ return events.pipe(map(() => undefined));
597
+ }),
598
+ /* The observable will complete after the first value */
599
+ first());
600
+ }
601
+ /**
602
+ * Deletes an item in our `indexedDB` store
603
+ * @param key The item's key
604
+ * @returns An RxJS `Observable` to wait the end of the operation
605
+ */
606
+ delete(key) {
607
+ /* Open a transaction in write mode */
608
+ return this.transaction("readwrite").pipe(mergeMap((transactionData) => {
609
+ const { store, events } = transactionData;
610
+ /* Delete the item in store */
611
+ store.delete(key);
612
+ /* Listen to events and return `undefined` as no data is expected here */
613
+ return events.pipe(map(() => undefined));
614
+ }),
615
+ /* The observable will complete after the first value */
616
+ first());
617
+ }
618
+ /**
619
+ * Deletes all items from our `indexedDB` objet store
620
+ * @returns An RxJS `Observable` to wait the end of the operation
621
+ */
622
+ clear() {
623
+ /* Open a transaction in write mode */
624
+ return this.transaction("readwrite").pipe(mergeMap((transactionData) => {
625
+ const { store, events } = transactionData;
626
+ /* Delete all items in object store */
627
+ store.clear();
628
+ /* Listen to events and return `undefined` as no data is expected here */
629
+ return events.pipe(map(() => undefined));
630
+ }),
631
+ /* The observable will complete */
632
+ first());
649
633
  }
650
634
  /**
651
- * Number of items in memory
635
+ * Get all the keys in our `indexedDB` store
636
+ * @returns An RxJS `Observable` iterating on each key
652
637
  */
653
- get size() {
654
- /* Wrap in a RxJS `Observable` to be consistent with other storages */
655
- return of(this.memoryStorage.size);
638
+ keys() {
639
+ /* Open a transaction in read-only mode */
640
+ return this.transaction("readonly").pipe(
641
+ /* `first()` is used as the final operator in other methods to complete the `Observable`
642
+ * (as it all starts from a `ReplaySubject` which never ends),
643
+ * but as this method is iterating over multiple values, `first()` **must** be used here */
644
+ first(), mergeMap((transactionData) => {
645
+ const { store } = transactionData;
646
+ /* Open a cursor on the store
647
+ * Avoid issues like https://github.com/cyrilletuzi/angular-async-local-storage/issues/69 */
648
+ const request = store.openKeyCursor();
649
+ /* Listen to success event */
650
+ const success$ = fromEvent(request, "success").pipe(
651
+ /* Stop the `Observable` when the cursor is `null` */
652
+ // // eslint-disable-next-line rxjs/no-ignored-takewhile-value -- Required by indexedDb behavior, getting the result from the event does not always work
653
+ takeWhile(() => (request.result !== null)),
654
+ /* This lib only allows string keys, but user could have added other types of keys from outside
655
+ * It's OK to cast as the cursor as been tested in the previous operator */
656
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-base-to-string -- Required by indexedDb behavior, and strings are enforced by the lib
657
+ map(() => request.result.key.toString()),
658
+ /* Iterate on the cursor */
659
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Required by indexedDb behavior
660
+ tap(() => { request.result.continue(); }));
661
+ /* Listen to error event and if so, throw an error */
662
+ const error$ = this.listenError(request);
663
+ /* Choose the first event to occur */
664
+ return race([success$, error$]);
665
+ }));
656
666
  }
657
667
  /**
658
- * Gets an item value in memory
659
- * @param key The item's key
660
- * @returns The item's value if the key exists, `undefined` otherwise, wrapped in a RxJS `Observable`
668
+ * Check if a key exists in our `indexedDB` store
669
+ * @returns An RxJS `Observable` telling if the key exists or not
661
670
  */
662
- get(key) {
663
- /* Wrap in a RxJS `Observable` to be consistent with other storages */
664
- return of(this.memoryStorage.get(key));
671
+ has(key) {
672
+ /* Open a transaction in read-only mode */
673
+ return this.transaction("readonly").pipe(mergeMap((transactionData) => {
674
+ const { store, events } = transactionData;
675
+ /* Check if the key exists in the store
676
+ * Fixes https://github.com/cyrilletuzi/angular-async-local-storage/issues/69
677
+ */
678
+ const request = store.getKey(key);
679
+ /* Listen to events and return `true` or `false` */
680
+ return events.pipe(map(() => (request.result !== undefined) ? true : false));
681
+ }),
682
+ /* The observable will complete */
683
+ first());
665
684
  }
666
685
  /**
667
- * Sets an item in memory
668
- * @param key The item's key
669
- * @param data The item's value
670
- * @returns A RxJS `Observable` to wait the end of the operation
686
+ * Connects to `indexedDB` and creates the object store on first time
671
687
  */
672
- set(key, data) {
673
- this.memoryStorage.set(key, data);
674
- /* Wrap in a RxJS `Observable` to be consistent with other storages */
675
- return of(undefined);
688
+ connect() {
689
+ let request;
690
+ /* Connect to `indexedDB`
691
+ * Will fail in Safari cross-origin iframes
692
+ * @see {@link https://github.com/cyrilletuzi/angular-async-local-storage/issues/42} */
693
+ try {
694
+ /* Do NOT explicit `window` here, as `indexedDB` could be used from a web worker too */
695
+ request = indexedDB.open(this.dbName, this.dbVersion);
696
+ }
697
+ catch {
698
+ this.database.error(new IDBBrokenError());
699
+ return;
700
+ }
701
+ /* Create store on first connection */
702
+ this.createStore(request);
703
+ /* Listen to success and error events */
704
+ const success$ = fromEvent(request, "success");
705
+ const error$ = this.listenError(request);
706
+ /* Choose the first to occur */
707
+ race([success$, error$])
708
+ /* The observable will complete */
709
+ .pipe(first())
710
+ .subscribe({
711
+ next: () => {
712
+ /* Register the database connection in the `ReplaySubject` for further access */
713
+ this.database.next(request.result);
714
+ },
715
+ error: () => {
716
+ /* Keeping this error management for safety, but it should not happen anymore.
717
+ * It was for Firefox private mode issue in Firefox versions < 115
718
+ * @see {@link https://github.com/cyrilletuzi/angular-async-local-storage/issues/26} */
719
+ this.database.error(new IDBBrokenError());
720
+ },
721
+ });
676
722
  }
677
723
  /**
678
- * Deletes an item in memory
679
- * @param key The item's key
680
- * @returns A RxJS `Observable` to wait the end of the operation
724
+ * Create store on first use of `indexedDB`
725
+ * @param request `indexedDB` database opening request
681
726
  */
682
- delete(key) {
683
- this.memoryStorage.delete(key);
684
- /* Wrap in a RxJS `Observable` to be consistent with other storages */
685
- return of(undefined);
727
+ createStore(request) {
728
+ /* Listen to the event fired on first connection */
729
+ fromEvent(request, "upgradeneeded")
730
+ /* The observable will complete */
731
+ .pipe(first())
732
+ .subscribe({
733
+ next: () => {
734
+ /* Check if the store already exists, to avoid error */
735
+ if (!request.result.objectStoreNames.contains(this.storeName)) {
736
+ /* Create the object store */
737
+ request.result.createObjectStore(this.storeName);
738
+ }
739
+ }
740
+ });
686
741
  }
687
742
  /**
688
- * Deletes all items in memory
689
- * @returns A RxJS `Observable` to wait the end of the operation
743
+ * Open an `indexedDB` transaction and get our store
744
+ * @param mode `readonly` or `readwrite`
745
+ * @returns An `indexedDB` transaction store and events, wrapped in an RxJS `Observable`
690
746
  */
691
- clear() {
692
- this.memoryStorage.clear();
693
- /* Wrap in a RxJS `Observable` to be consistent with other storages */
694
- return of(undefined);
747
+ transaction(mode) {
748
+ /* From the `indexedDB` connection, open a transaction and get the store */
749
+ return this.database
750
+ .pipe(mergeMap((database) => {
751
+ let transaction;
752
+ try {
753
+ transaction = database.transaction([this.storeName], mode);
754
+ }
755
+ catch (error) {
756
+ /* The store could have been deleted from outside */
757
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
758
+ return throwError(() => error);
759
+ }
760
+ /* Get the store from the transaction */
761
+ const store = transaction.objectStore(this.storeName);
762
+ /* Listen transaction `complete` and `error` events */
763
+ const events = this.listenTransactionEvents(transaction);
764
+ return of({ store, events });
765
+ }));
695
766
  }
696
767
  /**
697
- * Get all keys in memory
698
- * @returns A RxJS `Observable` iterating on keys
768
+ * Listen errors on a transaction or request, and throw if trigerred
769
+ * @param transactionOrRequest `indexedDb` transaction or request to listen
770
+ * @returns An `Observable` listening to errors
699
771
  */
700
- keys() {
701
- /* Create an `Observable` from keys */
702
- return from(this.memoryStorage.keys());
772
+ listenError(transactionOrRequest) {
773
+ return fromEvent(transactionOrRequest, "error").pipe(
774
+ /* Throw on error to be able to catch errors in RxJS way.
775
+ * Here `event.target` must be used, as `transactionOrRequest.error` will be `null`
776
+ * if we are on the request and the error is only triggered later by the transaction */
777
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
778
+ mergeMap((event) => throwError(() => event.target?.error)));
703
779
  }
704
780
  /**
705
- * Check if a key exists in memory
706
- * @param key Key name
707
- * @returns a RxJS `Observable` telling if the key exists or not
781
+ * Listen transaction `complete` and `error` events
782
+ * @param transaction Transaction to listen
783
+ * @returns An `Observable` listening to transaction `complete` and `error` events
708
784
  */
709
- has(key) {
710
- /* Wrap in a RxJS `Observable` to be consistent with other storages */
711
- return of(this.memoryStorage.has(key));
785
+ listenTransactionEvents(transaction) {
786
+ /* Listen to the `complete` event */
787
+ const complete$ = fromEvent(transaction, "complete");
788
+ /* Listen to the `error` event */
789
+ const error$ = this.listenError(transaction);
790
+ /* Choose the first event to occur */
791
+ return race([complete$, error$]);
712
792
  }
713
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.0", ngImport: i0, type: MemoryDatabase, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
714
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.0", ngImport: i0, type: MemoryDatabase, providedIn: "root" }); }
793
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: IndexedDBDatabase, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
794
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: IndexedDBDatabase, providedIn: "root" }); }
715
795
  }
716
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.0", ngImport: i0, type: MemoryDatabase, decorators: [{
796
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: IndexedDBDatabase, decorators: [{
717
797
  type: Injectable,
718
798
  args: [{
719
799
  providedIn: "root"
720
800
  }]
721
- }] });
801
+ }], ctorParameters: () => [] });
722
802
 
723
803
  class JSONValidator {
724
804
  /**
@@ -766,17 +846,16 @@ class JSONValidator {
766
846
  if ((schema.minLength !== undefined) && (data.length < schema.minLength)) {
767
847
  return false;
768
848
  }
769
- if (schema.pattern) {
770
- let regularExpression = null;
849
+ if (schema.pattern !== undefined) {
771
850
  try {
772
- regularExpression = new RegExp(schema.pattern);
851
+ const regularExpression = new RegExp(schema.pattern);
852
+ if (!regularExpression.test(data)) {
853
+ return false;
854
+ }
773
855
  }
774
856
  catch {
775
857
  // Nothing to do
776
858
  }
777
- if (regularExpression && !regularExpression.test(data)) {
778
- return false;
779
- }
780
859
  }
781
860
  return true;
782
861
  }
@@ -800,7 +879,7 @@ class JSONValidator {
800
879
  return false;
801
880
  }
802
881
  /* Test is done this way to not divide by 0 */
803
- if (schema.multipleOf && !Number.isInteger(data / schema.multipleOf)) {
882
+ if (schema.multipleOf !== undefined && schema.multipleOf !== 0 && Number.isFinite(schema.multipleOf) && !Number.isInteger(data / schema.multipleOf)) {
804
883
  return false;
805
884
  }
806
885
  if ((schema.maximum !== undefined) && (data > schema.maximum)) {
@@ -848,7 +927,7 @@ class JSONValidator {
848
927
  if ((schema.minItems !== undefined) && (data.length < schema.minItems)) {
849
928
  return false;
850
929
  }
851
- if (schema.uniqueItems) {
930
+ if (schema.uniqueItems === true) {
852
931
  /* Create a set to eliminate values with multiple occurences */
853
932
  const dataSet = new Set(data);
854
933
  if (data.length !== dataSet.size) {
@@ -864,6 +943,7 @@ class JSONValidator {
864
943
  for (const value of data) {
865
944
  // TODO: remove when TypeScript 4.1 is available
866
945
  // (currently the narrowed type from `Array.isArray()` is lost on readonly arrays)
946
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
867
947
  if (!this.validate(value, schema.items)) {
868
948
  return false;
869
949
  }
@@ -883,9 +963,8 @@ class JSONValidator {
883
963
  return false;
884
964
  }
885
965
  if (schemas) {
886
- for (let i = 0; i < schemas.length; i += 1) {
887
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
888
- if (!this.validate(data[i], schemas[i])) {
966
+ for (const [index, schema] of schemas.entries()) {
967
+ if (!this.validate(data[index], schema)) {
889
968
  return false;
890
969
  }
891
970
  }
@@ -912,7 +991,7 @@ class JSONValidator {
912
991
  /* Validate required properties */
913
992
  if (schema.required) {
914
993
  for (const requiredProp of schema.required) {
915
- if (!Object.prototype.hasOwnProperty.call(data, requiredProp)) {
994
+ if (!Object.hasOwn(data, requiredProp)) {
916
995
  return false;
917
996
  }
918
997
  }
@@ -920,8 +999,8 @@ class JSONValidator {
920
999
  /* Recursively validate all properties */
921
1000
  for (const property in schema.properties) {
922
1001
  /* Filter to keep only real properties (no internal JS stuff) and check if the data has the property too */
923
- if (Object.prototype.hasOwnProperty.call(schema.properties, property) && Object.prototype.hasOwnProperty.call(data, property)) {
924
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1002
+ if (Object.hasOwn(schema.properties, property) && Object.hasOwn(data, property)) {
1003
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unsafe-type-assertion -- Ensured by the logic
925
1004
  if (!this.validate(data[property], schema.properties[property])) {
926
1005
  return false;
927
1006
  }
@@ -936,7 +1015,7 @@ class JSONValidator {
936
1015
  * @returns If data is valid: `true`, if it is invalid: `false`
937
1016
  */
938
1017
  validateConst(data, schema) {
939
- if (!schema.const) {
1018
+ if (schema.const === undefined) {
940
1019
  return true;
941
1020
  }
942
1021
  return (data === schema.const);
@@ -952,94 +1031,35 @@ class JSONValidator {
952
1031
  return true;
953
1032
  }
954
1033
  /* Cast as the data can be of multiple types, and so TypeScript is lost */
1034
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
955
1035
  return (schema.enum.includes(data));
956
1036
  }
957
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.0", ngImport: i0, type: JSONValidator, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
958
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.0", ngImport: i0, type: JSONValidator, providedIn: "root" }); }
1037
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: JSONValidator, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1038
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: JSONValidator, providedIn: "root" }); }
959
1039
  }
960
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.0", ngImport: i0, type: JSONValidator, decorators: [{
1040
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: JSONValidator, decorators: [{
961
1041
  type: Injectable,
962
1042
  args: [{
963
1043
  providedIn: "root"
964
1044
  }]
965
1045
  }] });
966
1046
 
967
- /**
968
- * Factory to create a storage according to browser support
969
- * @see {@link https://github.com/cyrilletuzi/angular-async-local-storage/blob/main/docs/BROWSERS_SUPPORT.md}
970
- */
971
- function localDatabaseFactory() {
972
- const platformId = inject(PLATFORM_ID);
973
- /* When storage is fully disabled in browser (via the "Block all cookies" option),
974
- * just trying to check `indexedDB` or `localStorage` variables causes a security exception.
975
- * Prevents https://github.com/cyrilletuzi/angular-async-local-storage/issues/118
976
- */
977
- try {
978
- // Do not explicit `window` here, as the global object is not the same in web workers
979
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- They can be undefined in some browsers scenarios
980
- if (isPlatformBrowser(platformId) && (indexedDB !== undefined) && (indexedDB !== null) && ("open" in indexedDB)) {
981
- /* Check:
982
- * - if we are in a browser context (issue: server-side rendering)
983
- * - it could exist but be `undefined` or `null`
984
- * - it could exists but not having a working API
985
- * Will be the case for:
986
- * - All other browsers in normal mode
987
- * - Chromium / Safari private mode, but in this case, data will be swiped when the user leaves the app */
988
- return new IndexedDBDatabase();
989
- }
990
- else if (isPlatformBrowser(platformId)
991
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- They can be undefined in some browsers scenarios
992
- && (localStorage !== undefined) && (localStorage !== null) && ("getItem" in localStorage)) {
993
- /* Check:
994
- * - if we are in a browser context (issue: server-side rendering)
995
- * - if `localStorage` exists (to be sure)
996
- * - it could exists but not having a working API
997
- * Will be the case for:
998
- * - Safari cross-origin iframes, detected later in `IndexedDBDatabase.connect()`
999
- * @see {@link https://github.com/cyrilletuzi/angular-async-local-storage/issues/42}
1000
- * - Firefox private mode, but in this case, data will be swiped when the user leaves the app
1001
- * For Firefox, can only be detected later in `IndexedDBDatabase.connect()`
1002
- * @see {@link https://bugzilla.mozilla.org/show_bug.cgi?id=781982}
1003
- */
1004
- return new LocalStorageDatabase();
1005
- }
1006
- }
1007
- catch {
1008
- // Nothing to do
1009
- }
1010
- /* Will be the case for:
1011
- * - In browsers if storage has been fully disabled (via the "Block all cookies" option)
1012
- * - Server-side rendering
1013
- * - All other non-browser context
1014
- */
1015
- return new MemoryDatabase();
1016
- }
1017
- class LocalDatabase {
1018
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.0", ngImport: i0, type: LocalDatabase, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1019
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.0", ngImport: i0, type: LocalDatabase, providedIn: "root", useFactory: localDatabaseFactory }); }
1020
- }
1021
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.0", ngImport: i0, type: LocalDatabase, decorators: [{
1022
- type: Injectable,
1023
- args: [{
1024
- providedIn: "root",
1025
- useFactory: localDatabaseFactory,
1026
- }]
1027
- }] });
1028
-
1029
1047
  class StorageMap {
1048
+ #database;
1049
+ #jsonValidator;
1050
+ #notifiers = new Map();
1030
1051
  /**
1031
1052
  * Constructor params are provided by Angular (but can also be passed manually in tests)
1032
1053
  * @param database Storage to use
1033
- * @param jsonValidator Validator service
1034
- * @param LSPrefix Prefix for `localStorage` keys to avoid collision for multiple apps on the same subdomain or for interoperability
1035
1054
  */
1036
- constructor(database, jsonValidator = new JSONValidator()) {
1037
- this.database = database;
1038
- this.jsonValidator = jsonValidator;
1039
- this.notifiers = new Map();
1055
+ constructor(database) {
1056
+ this.#database = database;
1057
+ this.#jsonValidator = new JSONValidator();
1040
1058
  }
1041
1059
  /**
1042
- * **Number of items** in storage, wrapped in an `Observable`.
1060
+ * **Number of items** in storage, wrapped in an Observable.
1061
+ *
1062
+ * Note you do *not* need to unsubscribe (it is a self-completing Observable).
1043
1063
  *
1044
1064
  * @example
1045
1065
  * this.storageMap.size.subscribe((size) => {
@@ -1047,14 +1067,17 @@ class StorageMap {
1047
1067
  * });
1048
1068
  */
1049
1069
  get size() {
1050
- return this.database.size
1070
+ return this.#database.size
1051
1071
  /* Catch if `indexedDb` is broken */
1052
- .pipe(this.catchIDBBroken(() => this.database.size));
1072
+ .pipe(this.#catchIDBBroken(() => this.#database.size));
1053
1073
  }
1054
1074
  /**
1055
- * Tells you which storage engine is used. *Only useful for interoperability.*
1075
+ * Tells you which storage engine is used.
1076
+ *
1077
+ * *Only useful for interoperability.*
1078
+ *
1056
1079
  * Note that due to some browsers issues in some special contexts
1057
- * (Firefox private mode and Safari cross-origin iframes),
1080
+ * (like Safari cross-origin iframes),
1058
1081
  * **this information may be wrong at initialization,**
1059
1082
  * as the storage could fallback from `indexedDB` to `localStorage`
1060
1083
  * only after a first read or write operation.
@@ -1066,13 +1089,13 @@ class StorageMap {
1066
1089
  * if (this.storageMap.backingEngine === 'indexedDB') {}
1067
1090
  */
1068
1091
  get backingEngine() {
1069
- if (this.database instanceof IndexedDBDatabase) {
1092
+ if (this.#database instanceof IndexedDBDatabase) {
1070
1093
  return "indexedDB";
1071
1094
  }
1072
- else if (this.database instanceof LocalStorageDatabase) {
1095
+ else if (this.#database instanceof LocalStorageDatabase) {
1073
1096
  return "localStorage";
1074
1097
  }
1075
- else if (this.database instanceof MemoryDatabase) {
1098
+ else if (this.#database instanceof MemoryDatabase) {
1076
1099
  return "memory";
1077
1100
  }
1078
1101
  else {
@@ -1080,10 +1103,12 @@ class StorageMap {
1080
1103
  }
1081
1104
  }
1082
1105
  /**
1083
- * Info about `indexedDB` database. *Only useful for interoperability.*
1106
+ * Information about `indexedDB` database.
1107
+ *
1108
+ * *Only useful for interoperability.*
1109
+ *
1084
1110
  * @returns `indexedDB` database name, store name and database version.
1085
- * **Values will be empty if the storage is not `indexedDB`,**
1086
- * **so it should be used after an engine check**.
1111
+ * **Values will be empty if the storage is not `indexedDB`, so it should be used after an engine check**.
1087
1112
  *
1088
1113
  * @see {@link https://github.com/cyrilletuzi/angular-async-local-storage/blob/main/docs/INTEROPERABILITY.md}
1089
1114
  *
@@ -1093,15 +1118,17 @@ class StorageMap {
1093
1118
  * }
1094
1119
  */
1095
1120
  get backingStore() {
1096
- return (this.database instanceof IndexedDBDatabase) ?
1097
- this.database.backingStore :
1121
+ return (this.#database instanceof IndexedDBDatabase) ?
1122
+ this.#database.backingStore :
1098
1123
  { database: "", store: "", version: 0 };
1099
1124
  }
1100
1125
  /**
1101
- * Info about `localStorage` fallback storage. *Only useful for interoperability.*
1126
+ * Information about `localStorage` fallback storage.
1127
+ *
1128
+ * *Only useful for interoperability.*
1129
+ *
1102
1130
  * @returns `localStorage` prefix.
1103
- * **Values will be empty if the storage is not `localStorage`,**
1104
- * **so it should be used after an engine check**.
1131
+ * **Values will be empty if the storage is not `localStorage`, so it should be used after an engine check**.
1105
1132
  *
1106
1133
  * @see {@link https://github.com/cyrilletuzi/angular-async-local-storage/blob/main/docs/INTEROPERABILITY.md}
1107
1134
  *
@@ -1111,25 +1138,26 @@ class StorageMap {
1111
1138
  * }
1112
1139
  */
1113
1140
  get fallbackBackingStore() {
1114
- return (this.database instanceof LocalStorageDatabase) ?
1115
- { prefix: this.database.prefix } :
1141
+ return (this.#database instanceof LocalStorageDatabase) ?
1142
+ { prefix: this.#database.prefix } :
1116
1143
  { prefix: "" };
1117
1144
  }
1118
1145
  get(key, schema) {
1119
1146
  /* Get the data in storage */
1120
- return this.database.get(key).pipe(
1147
+ return this.#database.get(key).pipe(
1121
1148
  /* Check if `indexedDb` is broken */
1122
- this.catchIDBBroken(() => this.database.get(key)), mergeMap((data) => {
1149
+ this.#catchIDBBroken(() => this.#database.get(key)), mergeMap((data) => {
1123
1150
  /* No need to validate if the data is empty */
1124
1151
  if ((data === undefined) || (data === null)) {
1125
1152
  return of(undefined);
1126
1153
  }
1127
1154
  else if (schema) {
1128
1155
  /* Validate data against a JSON schema if provided */
1129
- if (!this.jsonValidator.validate(data, schema)) {
1156
+ if (!this.#jsonValidator.validate(data, schema)) {
1130
1157
  return throwError(() => new ValidationError());
1131
1158
  }
1132
1159
  /* Data have been checked, so it's OK to cast */
1160
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
1133
1161
  return of(data);
1134
1162
  }
1135
1163
  /* Cast to unknown as the data wasn't checked */
@@ -1137,12 +1165,19 @@ class StorageMap {
1137
1165
  }));
1138
1166
  }
1139
1167
  /**
1140
- * Set an item in storage.
1141
- * Note that setting `null` or `undefined` will remove the item to avoid some browsers issues.
1168
+ * Store an item in storage.
1169
+ *
1170
+ * Note that:
1171
+ * * you *do* need to subscribe, even if you do not have something specific to do after writing in storage, otherwise nothing happens (because it is how RxJS Observables work),
1172
+ * * but you do *not* need to unsubscribe (it is a self-completing Observable),
1173
+ * * setting `null` or `undefined` will remove the item to avoid some browsers issues,
1174
+ * * you should stick to serializable JSON data, meaning primitive types, arrays and literal objects. Date, Map, Set, Blob and other special structures can cause issues in some scenarios.
1175
+ * @see {@link https://github.com/cyrilletuzi/angular-async-local-storage/blob/main/docs/SERIALIZATION.md}
1176
+ *
1142
1177
  * @param key The item's key
1143
1178
  * @param data The item's value
1144
1179
  * @param schema Optional JSON schema to validate the data
1145
- * @returns A RxJS `Observable` to wait the end of the operation
1180
+ * @returns A RxJS Observable to wait the end of the operation
1146
1181
  *
1147
1182
  * @example
1148
1183
  * this.storageMap.set('key', 'value').subscribe(() => {});
@@ -1154,54 +1189,68 @@ class StorageMap {
1154
1189
  return this.delete(key);
1155
1190
  }
1156
1191
  /* Validate data against a JSON schema if provided */
1157
- if (schema && !this.jsonValidator.validate(data, schema)) {
1192
+ if (schema && !this.#jsonValidator.validate(data, schema)) {
1158
1193
  return throwError(() => new ValidationError());
1159
1194
  }
1160
- return this.database.set(key, data).pipe(
1195
+ return this.#database.set(key, data).pipe(
1161
1196
  /* Catch if `indexedDb` is broken */
1162
- this.catchIDBBroken(() => this.database.set(key, data)),
1197
+ this.#catchIDBBroken(() => this.#database.set(key, data)),
1163
1198
  /* Notify watchers (must be last because it should only happen if the operation succeeds) */
1164
- tap(() => { this.notify(key, data); }));
1199
+ tap(() => { this.#notify(key, data); }));
1165
1200
  }
1166
1201
  /**
1167
- * Delete an item in storage
1202
+ * Delete an item in storage.
1203
+ *
1204
+ * Note that:
1205
+ * * you *do* need to subscribe, even if you do not have something specific to do after deleting, otherwise nothing happens (because it is how RxJS Observables work),
1206
+ * * but you do *not* need to unsubscribe (it is a self-completing Observable).
1207
+ *
1168
1208
  * @param key The item's key
1169
- * @returns A RxJS `Observable` to wait the end of the operation
1209
+ * @returns A RxJS Observable to wait the end of the operation
1170
1210
  *
1171
1211
  * @example
1172
1212
  * this.storageMap.delete('key').subscribe(() => {});
1173
1213
  */
1174
1214
  delete(key) {
1175
- return this.database.delete(key).pipe(
1215
+ return this.#database.delete(key).pipe(
1176
1216
  /* Catch if `indexedDb` is broken */
1177
- this.catchIDBBroken(() => this.database.delete(key)),
1217
+ this.#catchIDBBroken(() => this.#database.delete(key)),
1178
1218
  /* Notify watchers (must be last because it should only happen if the operation succeeds) */
1179
- tap(() => { this.notify(key, undefined); }));
1219
+ tap(() => { this.#notify(key, undefined); }));
1180
1220
  }
1181
1221
  /**
1182
- * Delete all items in storage
1183
- * @returns A RxJS `Observable` to wait the end of the operation
1222
+ * Delete all items in storage.
1223
+ *
1224
+ * Note that:
1225
+ * * you *do* need to subscribe, even if you do not have something specific to do after clearing, otherwise nothing happens (because it is how RxJS Observables work),
1226
+ * * but you do *not* need to unsubscribe (it is a self-completing Observable).
1227
+ *
1228
+ * @returns A RxJS Observable to wait the end of the operation
1184
1229
  *
1185
1230
  * @example
1186
1231
  * this.storageMap.clear().subscribe(() => {});
1187
1232
  */
1188
1233
  clear() {
1189
- return this.database.clear().pipe(
1234
+ return this.#database.clear().pipe(
1190
1235
  /* Catch if `indexedDb` is broken */
1191
- this.catchIDBBroken(() => this.database.clear()),
1236
+ this.#catchIDBBroken(() => this.#database.clear()),
1192
1237
  /* Notify watchers (must be last because it should only happen if the operation succeeds) */
1193
1238
  tap(() => {
1194
- for (const key of this.notifiers.keys()) {
1195
- this.notify(key, undefined);
1239
+ for (const key of this.#notifiers.keys()) {
1240
+ this.#notify(key, undefined);
1196
1241
  }
1197
1242
  }));
1198
1243
  }
1199
1244
  /**
1200
- * Get all keys stored in storage. Note **this is an *iterating* `Observable`**:
1245
+ * Get all keys stored in storage.
1246
+ *
1247
+ * Note **this is an *iterating* Observable**:
1201
1248
  * * if there is no key, the `next` callback will not be invoked,
1202
1249
  * * if you need to wait the whole operation to end, be sure to act in the `complete` callback,
1203
- * as this `Observable` can emit several values and so will invoke the `next` callback several times.
1204
- * @returns A list of the keys wrapped in a RxJS `Observable`
1250
+ * as this Observable can emit several values and so will invoke the `next` callback several times,
1251
+ * * you do *not* need to unsubscribe (it is a self-completing Observable).
1252
+ *
1253
+ * @returns A list of the keys wrapped in a RxJS Observable
1205
1254
  *
1206
1255
  * @example
1207
1256
  * this.storageMap.keys().subscribe({
@@ -1210,13 +1259,16 @@ class StorageMap {
1210
1259
  * });
1211
1260
  */
1212
1261
  keys() {
1213
- return this.database.keys()
1262
+ return this.#database.keys()
1214
1263
  /* Catch if `indexedDb` is broken */
1215
- .pipe(this.catchIDBBroken(() => this.database.keys()));
1264
+ .pipe(this.#catchIDBBroken(() => this.#database.keys()));
1216
1265
  }
1217
1266
  /**
1218
- * Tells if a key exists in storage
1219
- * @returns A RxJS `Observable` telling if the key exists
1267
+ * Tells if a key exists in storage.
1268
+ *
1269
+ * Note you do *not* need to unsubscribe (it is a self-completing Observable).
1270
+ *
1271
+ * @returns A RxJS Observable telling if the key exists
1220
1272
  *
1221
1273
  * @example
1222
1274
  * this.storageMap.has('key').subscribe((hasKey) => {
@@ -1224,18 +1276,18 @@ class StorageMap {
1224
1276
  * });
1225
1277
  */
1226
1278
  has(key) {
1227
- return this.database.has(key)
1279
+ return this.#database.has(key)
1228
1280
  /* Catch if `indexedDb` is broken */
1229
- .pipe(this.catchIDBBroken(() => this.database.has(key)));
1281
+ .pipe(this.#catchIDBBroken(() => this.#database.has(key)));
1230
1282
  }
1231
1283
  watch(key, schema) {
1232
1284
  /* Check if there is already a notifier */
1233
- if (!this.notifiers.has(key)) {
1234
- this.notifiers.set(key, new ReplaySubject(1));
1285
+ if (!this.#notifiers.has(key)) {
1286
+ this.#notifiers.set(key, new ReplaySubject(1));
1235
1287
  }
1236
1288
  /* Non-null assertion is required because TypeScript doesn't narrow `.has()` yet */
1237
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1238
- const notifier = this.notifiers.get(key);
1289
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Ensured by the logic
1290
+ const notifier = this.#notifiers.get(key);
1239
1291
  /* Get the current item value */
1240
1292
  (schema ? this.get(key, schema) : this.get(key)).subscribe({
1241
1293
  next: (result) => {
@@ -1245,8 +1297,9 @@ class StorageMap {
1245
1297
  notifier.error(error);
1246
1298
  },
1247
1299
  });
1248
- /* Only the public API of the `Observable` should be returned */
1300
+ /* Only the public API of the Observable should be returned */
1249
1301
  return (schema ?
1302
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
1250
1303
  notifier.asObservable() :
1251
1304
  notifier.asObservable());
1252
1305
  }
@@ -1255,8 +1308,8 @@ class StorageMap {
1255
1308
  * @param key The item's key
1256
1309
  * @param data The new value
1257
1310
  */
1258
- notify(key, value) {
1259
- const notifier = this.notifiers.get(key);
1311
+ #notify(key, value) {
1312
+ const notifier = this.#notifiers.get(key);
1260
1313
  if (notifier) {
1261
1314
  notifier.next(value);
1262
1315
  }
@@ -1265,12 +1318,12 @@ class StorageMap {
1265
1318
  * RxJS operator to catch if `indexedDB` is broken
1266
1319
  * @param operationCallback Callback with the operation to redo
1267
1320
  */
1268
- catchIDBBroken(operationCallback) {
1321
+ #catchIDBBroken(operationCallback) {
1269
1322
  return catchError((error) => {
1270
1323
  /* Check if `indexedDB` is broken based on error message (the specific error class seems to be lost in the process) */
1271
1324
  if ((error !== undefined) && (error !== null)
1272
1325
  && (typeof error === "object") && ("message" in error)
1273
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
1326
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- Required because TypeScript narrowing is not working here
1274
1327
  && (error.message === IDB_BROKEN_ERROR)) {
1275
1328
  /* When storage is fully disabled in browser (via the "Block all cookies" option),
1276
1329
  * just trying to check `localStorage` variable causes a security exception.
@@ -1279,16 +1332,16 @@ class StorageMap {
1279
1332
  try {
1280
1333
  if ("getItem" in localStorage) {
1281
1334
  /* Fallback to `localStorage` if available */
1282
- this.database = new LocalStorageDatabase();
1335
+ this.#database = new LocalStorageDatabase();
1283
1336
  }
1284
1337
  else {
1285
1338
  /* Fallback to memory storage otherwise */
1286
- this.database = new MemoryDatabase();
1339
+ this.#database = new MemoryDatabase();
1287
1340
  }
1288
1341
  }
1289
1342
  catch {
1290
1343
  /* Fallback to memory storage otherwise */
1291
- this.database = new MemoryDatabase();
1344
+ this.#database = new MemoryDatabase();
1292
1345
  }
1293
1346
  /* Redo the operation */
1294
1347
  return operationCallback();
@@ -1300,15 +1353,26 @@ class StorageMap {
1300
1353
  }
1301
1354
  });
1302
1355
  }
1303
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.0", ngImport: i0, type: StorageMap, deps: [{ token: LocalDatabase }, { token: JSONValidator }], target: i0.ɵɵFactoryTarget.Injectable }); }
1304
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.0", ngImport: i0, type: StorageMap, providedIn: "root" }); }
1356
+ /**
1357
+ * THIS METHOD IS FOR INTERNAL PURPOSE ONLY AND MUST NOT BE USED,
1358
+ * IT CAN BE REMOVED AT ANY TIME AND MESSING WITH IT CAN CAUSE ISSUES
1359
+ * @private
1360
+ * @ignore
1361
+ */
1362
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- Silence the not used error, it is used in tests
1363
+ // @ts-ignore
1364
+ ɵinternalGetDatabase() {
1365
+ return this.#database;
1366
+ }
1367
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: StorageMap, deps: [{ token: LocalDatabase }], target: i0.ɵɵFactoryTarget.Injectable }); }
1368
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: StorageMap, providedIn: "root" }); }
1305
1369
  }
1306
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.0", ngImport: i0, type: StorageMap, decorators: [{
1370
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: StorageMap, decorators: [{
1307
1371
  type: Injectable,
1308
1372
  args: [{
1309
1373
  providedIn: "root"
1310
1374
  }]
1311
- }], ctorParameters: () => [{ type: LocalDatabase }, { type: JSONValidator }] });
1375
+ }], ctorParameters: () => [{ type: LocalDatabase }] });
1312
1376
 
1313
1377
  /*
1314
1378
  * Public API Surface of local-storage