@angular-architects/ngrx-toolkit 20.0.1 → 20.0.2
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/eslint.config.cjs +43 -0
- package/jest.config.ts +22 -0
- package/ng-package.json +7 -0
- package/package.json +4 -21
- package/project.json +37 -0
- package/redux-connector/docs/README.md +131 -0
- package/redux-connector/index.ts +6 -0
- package/redux-connector/ng-package.json +5 -0
- package/redux-connector/src/lib/create-redux.ts +102 -0
- package/redux-connector/src/lib/model.ts +89 -0
- package/redux-connector/src/lib/rxjs-interop/redux-method.ts +66 -0
- package/redux-connector/src/lib/signal-redux-store.ts +59 -0
- package/redux-connector/src/lib/util.ts +22 -0
- package/src/index.ts +43 -0
- package/src/lib/assertions/assertions.ts +9 -0
- package/src/lib/devtools/features/with-disabled-name-indicies.ts +31 -0
- package/src/lib/devtools/features/with-glitch-tracking.ts +35 -0
- package/src/lib/devtools/features/with-mapper.ts +34 -0
- package/src/lib/devtools/internal/current-action-names.ts +1 -0
- package/src/lib/devtools/internal/default-tracker.ts +60 -0
- package/src/lib/devtools/internal/devtools-feature.ts +37 -0
- package/src/lib/devtools/internal/devtools-syncer.service.ts +202 -0
- package/src/lib/devtools/internal/glitch-tracker.service.ts +61 -0
- package/src/lib/devtools/internal/models.ts +29 -0
- package/src/lib/devtools/provide-devtools-config.ts +32 -0
- package/src/lib/devtools/rename-devtools-name.ts +21 -0
- package/src/lib/devtools/tests/action-name.spec.ts +48 -0
- package/src/lib/devtools/tests/basic.spec.ts +111 -0
- package/src/lib/devtools/tests/connecting.spec.ts +37 -0
- package/src/lib/devtools/tests/helpers.spec.ts +43 -0
- package/src/lib/devtools/tests/naming.spec.ts +216 -0
- package/src/lib/devtools/tests/provide-devtools-config.spec.ts +25 -0
- package/src/lib/devtools/tests/types.spec.ts +19 -0
- package/src/lib/devtools/tests/update-state.spec.ts +29 -0
- package/src/lib/devtools/tests/with-devtools.spec.ts +5 -0
- package/src/lib/devtools/tests/with-glitch-tracking.spec.ts +272 -0
- package/src/lib/devtools/tests/with-mapper.spec.ts +69 -0
- package/src/lib/devtools/update-state.ts +38 -0
- package/src/lib/devtools/with-dev-tools-stub.ts +6 -0
- package/src/lib/devtools/with-devtools.ts +81 -0
- package/src/lib/immutable-state/deep-freeze.ts +43 -0
- package/src/lib/immutable-state/is-dev-mode.ts +6 -0
- package/src/lib/immutable-state/tests/with-immutable-state.spec.ts +278 -0
- package/src/lib/immutable-state/with-immutable-state.ts +150 -0
- package/src/lib/shared/prettify.ts +3 -0
- package/src/lib/shared/signal-store-models.ts +30 -0
- package/src/lib/shared/throw-if-null.ts +7 -0
- package/src/lib/storage-sync/features/with-indexed-db.ts +81 -0
- package/src/lib/storage-sync/features/with-local-storage.ts +58 -0
- package/src/lib/storage-sync/internal/indexeddb.service.ts +124 -0
- package/src/lib/storage-sync/internal/local-storage.service.ts +19 -0
- package/src/lib/storage-sync/internal/models.ts +62 -0
- package/src/lib/storage-sync/internal/session-storage.service.ts +18 -0
- package/src/lib/storage-sync/tests/indexeddb.service.spec.ts +99 -0
- package/src/lib/storage-sync/tests/with-storage-async.spec.ts +305 -0
- package/src/lib/storage-sync/tests/with-storage-sync.spec.ts +273 -0
- package/src/lib/storage-sync/with-storage-sync.ts +236 -0
- package/src/lib/with-call-state.spec.ts +42 -0
- package/src/lib/with-call-state.ts +195 -0
- package/src/lib/with-conditional.spec.ts +125 -0
- package/src/lib/with-conditional.ts +74 -0
- package/src/lib/with-data-service.spec.ts +564 -0
- package/src/lib/with-data-service.ts +433 -0
- package/src/lib/with-feature-factory.spec.ts +69 -0
- package/src/lib/with-feature-factory.ts +56 -0
- package/src/lib/with-pagination.spec.ts +135 -0
- package/src/lib/with-pagination.ts +373 -0
- package/src/lib/with-redux.spec.ts +258 -0
- package/src/lib/with-redux.ts +387 -0
- package/src/lib/with-reset.spec.ts +112 -0
- package/src/lib/with-reset.ts +62 -0
- package/src/lib/with-undo-redo.spec.ts +274 -0
- package/src/lib/with-undo-redo.ts +200 -0
- package/src/test-setup.ts +6 -0
- package/tsconfig.json +29 -0
- package/tsconfig.lib.json +17 -0
- package/tsconfig.lib.prod.json +9 -0
- package/tsconfig.spec.json +17 -0
- package/fesm2022/angular-architects-ngrx-toolkit-redux-connector.mjs +0 -119
- package/fesm2022/angular-architects-ngrx-toolkit-redux-connector.mjs.map +0 -1
- package/fesm2022/angular-architects-ngrx-toolkit.mjs +0 -1789
- package/fesm2022/angular-architects-ngrx-toolkit.mjs.map +0 -1
- package/index.d.ts +0 -949
- package/redux-connector/index.d.ts +0 -59
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
export const keyPath = 'ngrxToolkitKeyPath';
|
|
4
|
+
|
|
5
|
+
export const dbName = 'ngrxToolkitDb';
|
|
6
|
+
|
|
7
|
+
export const storeName = 'ngrxToolkitStore';
|
|
8
|
+
|
|
9
|
+
export const VERSION: number = 1 as const;
|
|
10
|
+
|
|
11
|
+
@Injectable({ providedIn: 'root' })
|
|
12
|
+
export class IndexedDBService {
|
|
13
|
+
/**
|
|
14
|
+
* write to indexedDB
|
|
15
|
+
* @param key
|
|
16
|
+
* @param data
|
|
17
|
+
*/
|
|
18
|
+
async setItem(key: string, data: string): Promise<void> {
|
|
19
|
+
const db = await this.openDB();
|
|
20
|
+
|
|
21
|
+
const tx = db.transaction(storeName, 'readwrite');
|
|
22
|
+
|
|
23
|
+
const store = tx.objectStore(storeName);
|
|
24
|
+
|
|
25
|
+
store.put({
|
|
26
|
+
[keyPath]: key,
|
|
27
|
+
value: data,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
tx.oncomplete = (): void => {
|
|
32
|
+
db.close();
|
|
33
|
+
resolve();
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
tx.onerror = (): void => {
|
|
37
|
+
db.close();
|
|
38
|
+
reject();
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* read from indexedDB
|
|
45
|
+
* @param key
|
|
46
|
+
*/
|
|
47
|
+
async getItem(key: string): Promise<string | null> {
|
|
48
|
+
const db = await this.openDB();
|
|
49
|
+
|
|
50
|
+
const tx = db.transaction(storeName, 'readonly');
|
|
51
|
+
|
|
52
|
+
const store = tx.objectStore(storeName);
|
|
53
|
+
|
|
54
|
+
const request = store.get(key);
|
|
55
|
+
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
request.onsuccess = (): void => {
|
|
58
|
+
db.close();
|
|
59
|
+
// localStorage(sessionStorage) returns null if the key does not exist
|
|
60
|
+
// Similarly, indexedDB should return null
|
|
61
|
+
if (request.result === undefined) {
|
|
62
|
+
resolve(null);
|
|
63
|
+
}
|
|
64
|
+
resolve(request.result?.['value']);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
request.onerror = (): void => {
|
|
68
|
+
db.close();
|
|
69
|
+
reject();
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* delete indexedDB
|
|
76
|
+
* @param key
|
|
77
|
+
*/
|
|
78
|
+
async clear(key: string): Promise<void> {
|
|
79
|
+
const db = await this.openDB();
|
|
80
|
+
|
|
81
|
+
const tx = db.transaction(storeName, 'readwrite');
|
|
82
|
+
|
|
83
|
+
const store = tx.objectStore(storeName);
|
|
84
|
+
|
|
85
|
+
const request = store.delete(key);
|
|
86
|
+
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
request.onsuccess = (): void => {
|
|
89
|
+
db.close();
|
|
90
|
+
resolve();
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
request.onerror = (): void => {
|
|
94
|
+
db.close();
|
|
95
|
+
reject();
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* open indexedDB
|
|
102
|
+
*/
|
|
103
|
+
private async openDB(): Promise<IDBDatabase> {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const request = indexedDB.open(dbName, VERSION);
|
|
106
|
+
|
|
107
|
+
request.onupgradeneeded = () => {
|
|
108
|
+
const db = request.result;
|
|
109
|
+
|
|
110
|
+
if (!db.objectStoreNames.contains(storeName)) {
|
|
111
|
+
db.createObjectStore(storeName, { keyPath });
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
request.onsuccess = (): void => {
|
|
116
|
+
resolve(request.result);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
request.onerror = (): void => {
|
|
120
|
+
reject(request.error);
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import {} from './models';
|
|
3
|
+
|
|
4
|
+
@Injectable({
|
|
5
|
+
providedIn: 'root',
|
|
6
|
+
})
|
|
7
|
+
export class LocalStorageService {
|
|
8
|
+
getItem(key: string): string | null {
|
|
9
|
+
return localStorage.getItem(key);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
setItem(key: string, data: string): void {
|
|
13
|
+
return localStorage.setItem(key, data);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
clear(key: string): void {
|
|
17
|
+
return localStorage.removeItem(key);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Signal, WritableSignal } from '@angular/core';
|
|
2
|
+
import { EmptyFeatureResult, WritableStateSource } from '@ngrx/signals';
|
|
3
|
+
import { SyncConfig } from '../with-storage-sync';
|
|
4
|
+
|
|
5
|
+
export type SyncMethods = {
|
|
6
|
+
clearStorage(): void;
|
|
7
|
+
readFromStorage(): void;
|
|
8
|
+
writeToStorage(): void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type SyncFeatureResult = EmptyFeatureResult & {
|
|
12
|
+
methods: SyncMethods;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type SyncStoreForFactory<State extends object> =
|
|
16
|
+
WritableStateSource<State>;
|
|
17
|
+
|
|
18
|
+
export type SyncStorageStrategy<State extends object> = ((
|
|
19
|
+
config: Required<SyncConfig<State>>,
|
|
20
|
+
store: SyncStoreForFactory<State>,
|
|
21
|
+
useStubs: boolean,
|
|
22
|
+
) => SyncMethods) & { type: 'sync' };
|
|
23
|
+
|
|
24
|
+
export type AsyncMethods = {
|
|
25
|
+
clearStorage(): Promise<void>;
|
|
26
|
+
readFromStorage(): Promise<void>;
|
|
27
|
+
writeToStorage(): Promise<void>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* AsyncFeatureResult is used as the public interface that users interact with
|
|
32
|
+
* when calling `withIndexedDB`. It intentionally omits the internal SYNC_STATUS
|
|
33
|
+
* property to avoid TypeScript error TS4058 (return type of public method
|
|
34
|
+
* includes private type).
|
|
35
|
+
*
|
|
36
|
+
* For internal implementation, we use AsyncStoreForFactory which includes
|
|
37
|
+
* the SYNC_STATUS property needed for state management.
|
|
38
|
+
*/
|
|
39
|
+
export const SYNC_STATUS = Symbol('SYNC_STATUS');
|
|
40
|
+
export type SyncStatus = 'idle' | 'syncing' | 'synced';
|
|
41
|
+
|
|
42
|
+
// Keeping it internal avoids TS4058 error
|
|
43
|
+
export type InternalAsyncProps = AsyncFeatureResult['props'] & {
|
|
44
|
+
[SYNC_STATUS]: WritableSignal<SyncStatus>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type AsyncFeatureResult = EmptyFeatureResult & {
|
|
48
|
+
methods: AsyncMethods;
|
|
49
|
+
props: {
|
|
50
|
+
isSynced: Signal<boolean>;
|
|
51
|
+
whenSynced: () => Promise<void>;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type AsyncStoreForFactory<State extends object> =
|
|
56
|
+
WritableStateSource<State> & InternalAsyncProps;
|
|
57
|
+
|
|
58
|
+
export type AsyncStorageStrategy<State extends object> = ((
|
|
59
|
+
config: Required<SyncConfig<State>>,
|
|
60
|
+
store: AsyncStoreForFactory<State>,
|
|
61
|
+
useStubs: boolean,
|
|
62
|
+
) => AsyncMethods) & { type: 'async' };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Injectable({
|
|
4
|
+
providedIn: 'root',
|
|
5
|
+
})
|
|
6
|
+
export class SessionStorageService {
|
|
7
|
+
getItem(key: string): string | null {
|
|
8
|
+
return sessionStorage.getItem(key);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
setItem(key: string, data: string): void {
|
|
12
|
+
return sessionStorage.setItem(key, data);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
clear(key: string): void {
|
|
16
|
+
return sessionStorage.removeItem(key);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import 'fake-indexeddb/auto';
|
|
2
|
+
import { IndexedDBService } from '../internal/indexeddb.service';
|
|
3
|
+
|
|
4
|
+
describe('IndexedDBService', () => {
|
|
5
|
+
const sampleData = JSON.stringify({
|
|
6
|
+
foo: 'bar',
|
|
7
|
+
users: [
|
|
8
|
+
{ name: 'John', age: 30, isAdmin: true },
|
|
9
|
+
{ name: 'Jane', age: 25, isAdmin: false },
|
|
10
|
+
],
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
let indexedDBService: IndexedDBService;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
indexedDBService = new IndexedDBService();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('It should be possible to write data using write() and then read the data using read()', async (): Promise<void> => {
|
|
20
|
+
const key = 'users';
|
|
21
|
+
|
|
22
|
+
const expectedData = sampleData;
|
|
23
|
+
|
|
24
|
+
await indexedDBService.setItem(key, sampleData);
|
|
25
|
+
|
|
26
|
+
const receivedData = await indexedDBService.getItem(key);
|
|
27
|
+
|
|
28
|
+
expect(receivedData).toEqual(expectedData);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('It should be possible to delete data using clear()', async (): Promise<void> => {
|
|
32
|
+
const key = 'sample';
|
|
33
|
+
|
|
34
|
+
await indexedDBService.setItem(key, sampleData);
|
|
35
|
+
|
|
36
|
+
await indexedDBService.clear(key);
|
|
37
|
+
|
|
38
|
+
const receivedData = await indexedDBService.getItem(key);
|
|
39
|
+
|
|
40
|
+
expect(receivedData).toEqual(null);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('When there is no data, read() should return null', async (): Promise<void> => {
|
|
44
|
+
const key = 'nullData';
|
|
45
|
+
|
|
46
|
+
const receivedData = await indexedDBService.getItem(key);
|
|
47
|
+
|
|
48
|
+
expect(receivedData).toEqual(null);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('write() should handle null data', async (): Promise<void> => {
|
|
52
|
+
const key = 'nullData';
|
|
53
|
+
|
|
54
|
+
await indexedDBService.setItem(key, JSON.stringify(null));
|
|
55
|
+
|
|
56
|
+
const receivedData = await indexedDBService.getItem(key);
|
|
57
|
+
|
|
58
|
+
expect(receivedData).toEqual('null');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('write() should handle empty object data', async (): Promise<void> => {
|
|
62
|
+
const key = 'emptyData';
|
|
63
|
+
|
|
64
|
+
const emptyData = JSON.stringify({});
|
|
65
|
+
const expectedData = emptyData;
|
|
66
|
+
|
|
67
|
+
await indexedDBService.setItem(key, emptyData);
|
|
68
|
+
|
|
69
|
+
const receivedData = await indexedDBService.getItem(key);
|
|
70
|
+
|
|
71
|
+
expect(receivedData).toEqual(expectedData);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('write() should handle large data objects', async (): Promise<void> => {
|
|
75
|
+
const key = 'largeData';
|
|
76
|
+
|
|
77
|
+
const largeData = JSON.stringify({ foo: 'a'.repeat(100000) });
|
|
78
|
+
const expectedData = largeData;
|
|
79
|
+
|
|
80
|
+
await indexedDBService.setItem(key, largeData);
|
|
81
|
+
|
|
82
|
+
const receivedData = await indexedDBService.getItem(key);
|
|
83
|
+
|
|
84
|
+
expect(receivedData).toEqual(expectedData);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('write() should handle special characters in data', async (): Promise<void> => {
|
|
88
|
+
const key = 'specialCharData';
|
|
89
|
+
|
|
90
|
+
const specialCharData = JSON.stringify({ foo: 'bar!@#$%^&*()_+{}:"<>?' });
|
|
91
|
+
const expectedData = specialCharData;
|
|
92
|
+
|
|
93
|
+
await indexedDBService.setItem(key, specialCharData);
|
|
94
|
+
|
|
95
|
+
const receivedData = await indexedDBService.getItem(key);
|
|
96
|
+
|
|
97
|
+
expect(receivedData).toEqual(expectedData);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
import { getState, patchState, signalStore, withState } from '@ngrx/signals';
|
|
3
|
+
import 'fake-indexeddb/auto';
|
|
4
|
+
import { withIndexedDB } from '../features/with-indexed-db';
|
|
5
|
+
import { IndexedDBService } from '../internal/indexeddb.service';
|
|
6
|
+
import { withStorageSync } from '../with-storage-sync';
|
|
7
|
+
|
|
8
|
+
interface StateObject {
|
|
9
|
+
foo: string;
|
|
10
|
+
age: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const initialState: StateObject = {
|
|
14
|
+
foo: 'bar',
|
|
15
|
+
age: 18,
|
|
16
|
+
};
|
|
17
|
+
const key = 'FooBar';
|
|
18
|
+
|
|
19
|
+
const waitForSyncStable = async (store: {
|
|
20
|
+
whenSynced?: () => Promise<void>;
|
|
21
|
+
}) => {
|
|
22
|
+
if (store.whenSynced) {
|
|
23
|
+
await store.whenSynced();
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
describe('withStorageSync (async storage)', () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
// make sure to start with a clean storage
|
|
30
|
+
globalThis.indexedDB = new IDBFactory();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('adds methods for storage access to the store', () => {
|
|
34
|
+
TestBed.runInInjectionContext(() => {
|
|
35
|
+
const Store = signalStore(withStorageSync({ key }, withIndexedDB()));
|
|
36
|
+
const store = new Store();
|
|
37
|
+
|
|
38
|
+
expect(Object.keys(store)).toEqual([
|
|
39
|
+
'isSynced',
|
|
40
|
+
'whenSynced',
|
|
41
|
+
'clearStorage',
|
|
42
|
+
'readFromStorage',
|
|
43
|
+
'writeToStorage',
|
|
44
|
+
]);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('offers manual sync using provided methods', async () => {
|
|
49
|
+
TestBed.runInInjectionContext(async () => {
|
|
50
|
+
// prefill storage
|
|
51
|
+
const indexedDBService = TestBed.inject(IndexedDBService);
|
|
52
|
+
await indexedDBService.setItem(
|
|
53
|
+
key,
|
|
54
|
+
JSON.stringify({
|
|
55
|
+
foo: 'baz',
|
|
56
|
+
age: 99,
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const Store = signalStore(
|
|
61
|
+
{ protectedState: false },
|
|
62
|
+
withState(initialState),
|
|
63
|
+
withStorageSync({ key, autoSync: false }, withIndexedDB()),
|
|
64
|
+
);
|
|
65
|
+
const store = TestBed.inject(Store);
|
|
66
|
+
await waitForSyncStable(store);
|
|
67
|
+
|
|
68
|
+
expect(getState(store)).toEqual({});
|
|
69
|
+
|
|
70
|
+
await store.readFromStorage();
|
|
71
|
+
|
|
72
|
+
expect(getState(store)).toEqual({
|
|
73
|
+
foo: 'baz',
|
|
74
|
+
age: 99,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
patchState(store, { ...initialState });
|
|
78
|
+
await waitForSyncStable(store);
|
|
79
|
+
|
|
80
|
+
expect(await indexedDBService.getItem(key)).toEqual({
|
|
81
|
+
foo: 'baz',
|
|
82
|
+
age: 99,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await store.writeToStorage();
|
|
86
|
+
expect(await indexedDBService.getItem(key)).toEqual({
|
|
87
|
+
...initialState,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await store.clearStorage();
|
|
91
|
+
expect(await indexedDBService.getItem(key)).toEqual(null);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('autoSync', () => {
|
|
96
|
+
it('inits from storage and write to storage on changes when set to `true`', async () => {
|
|
97
|
+
const indexedDBService = TestBed.inject(IndexedDBService);
|
|
98
|
+
// prefill storage
|
|
99
|
+
await indexedDBService.setItem(
|
|
100
|
+
key,
|
|
101
|
+
JSON.stringify({
|
|
102
|
+
foo: 'baz',
|
|
103
|
+
age: 99,
|
|
104
|
+
} as StateObject),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const Store = signalStore(
|
|
108
|
+
{ providedIn: 'root', protectedState: false },
|
|
109
|
+
withState(initialState),
|
|
110
|
+
withStorageSync(key, withIndexedDB()),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const store = TestBed.inject(Store);
|
|
114
|
+
await waitForSyncStable(store);
|
|
115
|
+
expect(getState(store)).toEqual({
|
|
116
|
+
foo: 'baz',
|
|
117
|
+
age: 99,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
patchState(store, { ...initialState });
|
|
121
|
+
await waitForSyncStable(store);
|
|
122
|
+
|
|
123
|
+
expect(getState(store)).toEqual({
|
|
124
|
+
...initialState,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(await indexedDBService.getItem(key)).toEqual(
|
|
128
|
+
JSON.stringify(initialState),
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('does not init from storage and does write to storage on changes when set to `false`', async () => {
|
|
133
|
+
const indexedDBService = TestBed.inject(IndexedDBService);
|
|
134
|
+
await indexedDBService.setItem(
|
|
135
|
+
key,
|
|
136
|
+
JSON.stringify({
|
|
137
|
+
foo: 'baz',
|
|
138
|
+
age: 99,
|
|
139
|
+
}),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const Store = signalStore(
|
|
143
|
+
{ providedIn: 'root', protectedState: false },
|
|
144
|
+
withStorageSync({ key, autoSync: false }, withIndexedDB()),
|
|
145
|
+
);
|
|
146
|
+
const store = TestBed.inject(Store);
|
|
147
|
+
expect(store.isSynced()).toBe(false);
|
|
148
|
+
expect(getState(store)).toEqual({});
|
|
149
|
+
|
|
150
|
+
patchState(store, { ...initialState });
|
|
151
|
+
expect(store.isSynced()).toBe(false);
|
|
152
|
+
|
|
153
|
+
const storeItem = JSON.parse(
|
|
154
|
+
(await indexedDBService.getItem(key)) || '{}',
|
|
155
|
+
);
|
|
156
|
+
expect(storeItem).toEqual({
|
|
157
|
+
foo: 'baz',
|
|
158
|
+
age: 99,
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('select', () => {
|
|
164
|
+
it('syncs the whole state by default', async () => {
|
|
165
|
+
const indexedDBService = TestBed.inject(IndexedDBService);
|
|
166
|
+
const Store = signalStore(
|
|
167
|
+
{ providedIn: 'root', protectedState: false },
|
|
168
|
+
withState(initialState),
|
|
169
|
+
withStorageSync(key, withIndexedDB()),
|
|
170
|
+
);
|
|
171
|
+
const store = TestBed.inject(Store);
|
|
172
|
+
await waitForSyncStable(store);
|
|
173
|
+
|
|
174
|
+
patchState(store, { foo: 'baz', age: 25 });
|
|
175
|
+
await waitForSyncStable(store);
|
|
176
|
+
|
|
177
|
+
expect(await indexedDBService.getItem(key)).toEqual(
|
|
178
|
+
JSON.stringify({ foo: 'baz', age: 25 }),
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('syncs selected slices when specified', async () => {
|
|
183
|
+
const indexedDBService = TestBed.inject(IndexedDBService);
|
|
184
|
+
const Store = signalStore(
|
|
185
|
+
{ providedIn: 'root', protectedState: false },
|
|
186
|
+
withState(initialState),
|
|
187
|
+
withStorageSync(
|
|
188
|
+
{ key, select: ({ foo }) => ({ foo }) },
|
|
189
|
+
withIndexedDB(),
|
|
190
|
+
),
|
|
191
|
+
);
|
|
192
|
+
const store = TestBed.inject(Store);
|
|
193
|
+
await waitForSyncStable(store);
|
|
194
|
+
|
|
195
|
+
patchState(store, { foo: 'baz' });
|
|
196
|
+
await waitForSyncStable(store);
|
|
197
|
+
|
|
198
|
+
const storeItem = JSON.parse(
|
|
199
|
+
(await indexedDBService.getItem(key)) || '{}',
|
|
200
|
+
);
|
|
201
|
+
expect(storeItem).toEqual({
|
|
202
|
+
foo: 'baz',
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('parse/stringify', () => {
|
|
208
|
+
it('uses custom parsing/stringification when specified', async () => {
|
|
209
|
+
const indexedDBService = TestBed.inject(IndexedDBService);
|
|
210
|
+
const parse = (stateString: string) => {
|
|
211
|
+
const [foo, age] = stateString.split('_');
|
|
212
|
+
return {
|
|
213
|
+
foo,
|
|
214
|
+
age: +age,
|
|
215
|
+
};
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const Store = signalStore(
|
|
219
|
+
{ providedIn: 'root', protectedState: false },
|
|
220
|
+
withState(initialState),
|
|
221
|
+
withStorageSync(
|
|
222
|
+
{
|
|
223
|
+
key,
|
|
224
|
+
parse,
|
|
225
|
+
stringify: (state) => `${state.foo}_${state.age}`,
|
|
226
|
+
},
|
|
227
|
+
withIndexedDB(),
|
|
228
|
+
),
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const store = TestBed.inject(Store);
|
|
232
|
+
await waitForSyncStable(store);
|
|
233
|
+
patchState(store, { foo: 'baz' });
|
|
234
|
+
await waitForSyncStable(store);
|
|
235
|
+
|
|
236
|
+
const storeItem = parse((await indexedDBService.getItem(key)) || '');
|
|
237
|
+
expect(storeItem).toEqual({
|
|
238
|
+
...initialState,
|
|
239
|
+
foo: 'baz',
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe('withStorageSync', () => {
|
|
245
|
+
let warnings = [] as string[];
|
|
246
|
+
|
|
247
|
+
jest.spyOn(console, 'warn').mockImplementation((...messages: string[]) => {
|
|
248
|
+
warnings.push(...messages);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
beforeEach(() => {
|
|
252
|
+
warnings = [];
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('logs when writing happens before state is synchronized', async () => {
|
|
256
|
+
const Store = signalStore(
|
|
257
|
+
{ providedIn: 'root', protectedState: false },
|
|
258
|
+
withState({ name: 'Delta', age: 52 }),
|
|
259
|
+
withStorageSync('flights', withIndexedDB()),
|
|
260
|
+
);
|
|
261
|
+
const store = TestBed.inject(Store);
|
|
262
|
+
await waitForSyncStable(store);
|
|
263
|
+
|
|
264
|
+
expect(warnings).toEqual([]);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('warns when reading happens during a write', async () => {
|
|
268
|
+
const Store = signalStore(
|
|
269
|
+
{ providedIn: 'root', protectedState: false },
|
|
270
|
+
withState({ name: 'Delta', age: 52 }),
|
|
271
|
+
withStorageSync('flights', withIndexedDB()),
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
const store = TestBed.inject(Store);
|
|
275
|
+
await waitForSyncStable(store);
|
|
276
|
+
patchState(store, { name: 'Lufthansa', age: 27 });
|
|
277
|
+
store.readFromStorage();
|
|
278
|
+
|
|
279
|
+
expect(warnings).toEqual([
|
|
280
|
+
'Reading to Store (flights) happened during an ongoing synchronization process.',
|
|
281
|
+
'Please ensure that the store is not in syncing state via `store.whenSynced()`.',
|
|
282
|
+
'Alternatively, you can disable the autoSync by passing `autoSync: false` in the config.',
|
|
283
|
+
]);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('warns when writing happens during a read', async () => {
|
|
287
|
+
const Store = signalStore(
|
|
288
|
+
{ providedIn: 'root', protectedState: false },
|
|
289
|
+
withState({ name: 'Delta', age: 52 }),
|
|
290
|
+
withStorageSync('flights', withIndexedDB()),
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const store = TestBed.inject(Store);
|
|
294
|
+
await waitForSyncStable(store);
|
|
295
|
+
|
|
296
|
+
store.readFromStorage();
|
|
297
|
+
patchState(store, { name: 'Lufthansa', age: 27 });
|
|
298
|
+
expect(warnings).toEqual([
|
|
299
|
+
'Writing to Store (flights) happened during an ongoing synchronization process.',
|
|
300
|
+
'Please ensure that the store is not in syncing state via `store.whenSynced()`.',
|
|
301
|
+
'Alternatively, you can disable the autoSync by passing `autoSync: false` in the config.',
|
|
302
|
+
]);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
});
|