@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.
- package/LICENSE +14 -15
- package/esm2022/lib/databases/exceptions.mjs +3 -3
- package/esm2022/lib/databases/indexeddb-database.mjs +14 -12
- package/esm2022/lib/databases/local-database.mjs +6 -8
- package/esm2022/lib/databases/localstorage-database.mjs +10 -9
- package/esm2022/lib/databases/memory-database.mjs +6 -5
- package/esm2022/lib/providers.mjs +25 -16
- package/esm2022/lib/storage.module.mjs +9 -7
- package/esm2022/lib/storages/exceptions.mjs +2 -2
- package/esm2022/lib/storages/storage-map.service.mjs +117 -73
- package/esm2022/lib/tokens.mjs +1 -1
- package/esm2022/lib/validation/json-schema.mjs +1 -1
- package/esm2022/lib/validation/json-validator.mjs +13 -15
- package/esm2022/ngx-pwa-local-storage.mjs +1 -1
- package/esm2022/public-api.mjs +1 -1
- package/fesm2022/ngx-pwa-local-storage.mjs +616 -565
- package/fesm2022/ngx-pwa-local-storage.mjs.map +1 -1
- package/lib/databases/exceptions.d.ts +2 -2
- package/lib/databases/indexeddb-database.d.ts +12 -15
- package/lib/databases/localstorage-database.d.ts +2 -2
- package/lib/databases/memory-database.d.ts +1 -1
- package/lib/providers.d.ts +25 -16
- package/lib/storage.module.d.ts +6 -4
- package/lib/storages/exceptions.d.ts +1 -1
- package/lib/storages/storage-map.service.d.ts +100 -46
- package/lib/tokens.d.ts +7 -0
- package/lib/validation/json-schema.d.ts +10 -8
- package/lib/validation/json-validator.d.ts +9 -9
- package/package.json +4 -4
- package/public-api.d.ts +2 -2
- package/schematics/collection.json +1 -1
- package/schematics/migration.json +1 -1
- package/schematics/ng-add/index.d.ts +1 -1
- package/schematics/ng-add/index.js +2 -3
- package/schematics/ng-add/index.js.map +1 -1
- package/schematics/ng-update/index.d.ts +1 -1
- package/schematics/ng-update/index.js +1 -2
- package/schematics/ng-update/index.js.map +1 -1
- package/schematics/utility/config.js +2 -1
- package/schematics/utility/config.js.map +1 -1
- package/schematics/utility/module.d.ts +1 -1
- package/schematics/utility/module.js +0 -1
- 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 {
|
|
4
|
-
import {
|
|
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
|
|
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
|
|
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
|
-
*
|
|
96
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
109
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
124
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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: "
|
|
170
|
-
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "
|
|
171
|
-
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "
|
|
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: "
|
|
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
|
|
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
|
|
203
|
+
class LocalStorageDatabase {
|
|
193
204
|
constructor() {
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
209
|
+
* Number of items in `localStorage`
|
|
223
210
|
*/
|
|
224
211
|
get size() {
|
|
225
|
-
/*
|
|
226
|
-
return
|
|
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
|
|
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
|
|
218
|
+
* @returns The item's value if the key exists, `undefined` otherwise, wrapped in a RxJS `Observable`
|
|
240
219
|
*/
|
|
241
220
|
get(key) {
|
|
242
|
-
/*
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
*
|
|
238
|
+
* Store an item in `localStorage`
|
|
271
239
|
* @param key The item's key
|
|
272
240
|
* @param data The item's value
|
|
273
|
-
* @returns
|
|
241
|
+
* @returns A RxJS `Observable` to wait the end of the operation
|
|
274
242
|
*/
|
|
275
243
|
set(key, data) {
|
|
276
|
-
|
|
277
|
-
if
|
|
278
|
-
|
|
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
|
-
/*
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
|
269
|
+
* Deletes an item in `localStorage`
|
|
295
270
|
* @param key The item's key
|
|
296
|
-
* @returns
|
|
271
|
+
* @returns A RxJS `Observable` to wait the end of the operation
|
|
297
272
|
*/
|
|
298
273
|
delete(key) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
|
312
|
-
* @returns
|
|
279
|
+
* Deletes all items in `localStorage`
|
|
280
|
+
* @returns A RxJS `Observable` to wait the end of the operation
|
|
313
281
|
*/
|
|
314
282
|
clear() {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
|
328
|
-
*
|
|
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
|
-
/*
|
|
332
|
-
return
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
|
360
|
-
* @
|
|
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
|
-
/*
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
471
|
-
* @param
|
|
472
|
-
* @returns
|
|
337
|
+
* Add the prefix to a key
|
|
338
|
+
* @param key The key name
|
|
339
|
+
* @returns The prefixed key name
|
|
473
340
|
*/
|
|
474
|
-
|
|
475
|
-
|
|
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: "
|
|
483
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "
|
|
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: "
|
|
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
|
|
354
|
+
class MemoryDatabase {
|
|
493
355
|
constructor() {
|
|
494
|
-
|
|
495
|
-
|
|
356
|
+
/**
|
|
357
|
+
* Memory storage
|
|
358
|
+
*/
|
|
359
|
+
this.memoryStorage = new Map();
|
|
496
360
|
}
|
|
497
361
|
/**
|
|
498
|
-
* Number of items in
|
|
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(
|
|
366
|
+
return of(this.memoryStorage.size);
|
|
503
367
|
}
|
|
504
368
|
/**
|
|
505
|
-
* Gets an item value in
|
|
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(
|
|
375
|
+
return of(this.memoryStorage.get(key));
|
|
525
376
|
}
|
|
526
377
|
/**
|
|
527
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
597
|
-
* @param key
|
|
598
|
-
* @returns
|
|
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(
|
|
422
|
+
return of(this.memoryStorage.has(key));
|
|
610
423
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
if (
|
|
620
|
-
/*
|
|
621
|
-
|
|
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
|
-
|
|
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
|
-
|
|
634
|
-
|
|
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
|
-
|
|
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
|
-
}]
|
|
491
|
+
}] });
|
|
642
492
|
|
|
643
|
-
class
|
|
493
|
+
class IndexedDBDatabase {
|
|
644
494
|
constructor() {
|
|
645
495
|
/**
|
|
646
|
-
*
|
|
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.
|
|
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
|
-
*
|
|
628
|
+
* Get all the keys in our `indexedDB` store
|
|
629
|
+
* @returns An RxJS `Observable` iterating on each key
|
|
652
630
|
*/
|
|
653
|
-
|
|
654
|
-
/*
|
|
655
|
-
return
|
|
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
|
-
*
|
|
659
|
-
* @
|
|
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
|
-
|
|
663
|
-
/*
|
|
664
|
-
return
|
|
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
|
-
*
|
|
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
|
-
|
|
673
|
-
|
|
674
|
-
/*
|
|
675
|
-
|
|
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
|
-
*
|
|
679
|
-
* @param
|
|
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
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
-
*
|
|
689
|
-
* @
|
|
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
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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
|
-
*
|
|
698
|
-
* @
|
|
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
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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
|
-
*
|
|
706
|
-
* @param
|
|
707
|
-
* @returns
|
|
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
|
-
|
|
710
|
-
/*
|
|
711
|
-
|
|
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: "
|
|
714
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "
|
|
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: "
|
|
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 (
|
|
887
|
-
|
|
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.
|
|
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.
|
|
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: "
|
|
958
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "
|
|
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: "
|
|
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
|
|
1037
|
-
this
|
|
1038
|
-
this
|
|
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
|
|
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
|
|
1059
|
+
return this.#database.size
|
|
1051
1060
|
/* Catch if `indexedDb` is broken */
|
|
1052
|
-
.pipe(this
|
|
1061
|
+
.pipe(this.#catchIDBBroken(() => this.#database.size));
|
|
1053
1062
|
}
|
|
1054
1063
|
/**
|
|
1055
|
-
* Tells you which storage engine is used.
|
|
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
|
-
* (
|
|
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
|
|
1081
|
+
if (this.#database instanceof IndexedDBDatabase) {
|
|
1070
1082
|
return "indexedDB";
|
|
1071
1083
|
}
|
|
1072
|
-
else if (this
|
|
1084
|
+
else if (this.#database instanceof LocalStorageDatabase) {
|
|
1073
1085
|
return "localStorage";
|
|
1074
1086
|
}
|
|
1075
|
-
else if (this
|
|
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
|
-
*
|
|
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
|
|
1097
|
-
this
|
|
1110
|
+
return (this.#database instanceof IndexedDBDatabase) ?
|
|
1111
|
+
this.#database.backingStore :
|
|
1098
1112
|
{ database: "", store: "", version: 0 };
|
|
1099
1113
|
}
|
|
1100
1114
|
/**
|
|
1101
|
-
*
|
|
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
|
|
1115
|
-
{ prefix: this
|
|
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
|
|
1136
|
+
return this.#database.get(key).pipe(
|
|
1121
1137
|
/* Check if `indexedDb` is broken */
|
|
1122
|
-
this
|
|
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
|
|
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
|
-
*
|
|
1141
|
-
*
|
|
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
|
|
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
|
|
1180
|
+
if (schema && !this.#jsonValidator.validate(data, schema)) {
|
|
1158
1181
|
return throwError(() => new ValidationError());
|
|
1159
1182
|
}
|
|
1160
|
-
return this
|
|
1183
|
+
return this.#database.set(key, data).pipe(
|
|
1161
1184
|
/* Catch if `indexedDb` is broken */
|
|
1162
|
-
this
|
|
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
|
|
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
|
|
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
|
|
1203
|
+
return this.#database.delete(key).pipe(
|
|
1176
1204
|
/* Catch if `indexedDb` is broken */
|
|
1177
|
-
this
|
|
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
|
|
1207
|
+
tap(() => { this.#notify(key, undefined); }));
|
|
1180
1208
|
}
|
|
1181
1209
|
/**
|
|
1182
|
-
* Delete all items in storage
|
|
1183
|
-
*
|
|
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
|
|
1222
|
+
return this.#database.clear().pipe(
|
|
1190
1223
|
/* Catch if `indexedDb` is broken */
|
|
1191
|
-
this
|
|
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
|
|
1195
|
-
this
|
|
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.
|
|
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
|
|
1204
|
-
*
|
|
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
|
|
1250
|
+
return this.#database.keys()
|
|
1214
1251
|
/* Catch if `indexedDb` is broken */
|
|
1215
|
-
.pipe(this
|
|
1252
|
+
.pipe(this.#catchIDBBroken(() => this.#database.keys()));
|
|
1216
1253
|
}
|
|
1217
1254
|
/**
|
|
1218
|
-
* Tells if a key exists in storage
|
|
1219
|
-
*
|
|
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
|
|
1267
|
+
return this.#database.has(key)
|
|
1228
1268
|
/* Catch if `indexedDb` is broken */
|
|
1229
|
-
.pipe(this
|
|
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
|
|
1234
|
-
this
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1322
|
+
this.#database = new LocalStorageDatabase();
|
|
1283
1323
|
}
|
|
1284
1324
|
else {
|
|
1285
1325
|
/* Fallback to memory storage otherwise */
|
|
1286
|
-
this
|
|
1326
|
+
this.#database = new MemoryDatabase();
|
|
1287
1327
|
}
|
|
1288
1328
|
}
|
|
1289
1329
|
catch {
|
|
1290
1330
|
/* Fallback to memory storage otherwise */
|
|
1291
|
-
this
|
|
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
|
-
|
|
1304
|
-
|
|
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: "
|
|
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 }
|
|
1362
|
+
}], ctorParameters: () => [{ type: LocalDatabase }] });
|
|
1312
1363
|
|
|
1313
1364
|
/*
|
|
1314
1365
|
* Public API Surface of local-storage
|