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