@angular-architects/ngrx-toolkit 19.2.3 → 19.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/eslint.config.cjs +43 -0
  2. package/jest.config.ts +22 -0
  3. package/ng-package.json +7 -0
  4. package/package.json +4 -21
  5. package/project.json +37 -0
  6. package/redux-connector/docs/README.md +131 -0
  7. package/redux-connector/index.ts +6 -0
  8. package/redux-connector/ng-package.json +5 -0
  9. package/redux-connector/src/lib/create-redux.ts +102 -0
  10. package/redux-connector/src/lib/model.ts +89 -0
  11. package/redux-connector/src/lib/rxjs-interop/redux-method.ts +66 -0
  12. package/redux-connector/src/lib/signal-redux-store.ts +59 -0
  13. package/redux-connector/src/lib/util.ts +22 -0
  14. package/{index.d.ts → src/index.ts} +36 -4
  15. package/src/lib/assertions/assertions.ts +9 -0
  16. package/{lib/devtools/features/with-disabled-name-indicies.d.ts → src/lib/devtools/features/with-disabled-name-indicies.ts} +5 -1
  17. package/{lib/devtools/features/with-glitch-tracking.d.ts → src/lib/devtools/features/with-glitch-tracking.ts} +6 -1
  18. package/{lib/devtools/features/with-mapper.d.ts → src/lib/devtools/features/with-mapper.ts} +7 -1
  19. package/src/lib/devtools/internal/current-action-names.ts +1 -0
  20. package/src/lib/devtools/internal/default-tracker.ts +60 -0
  21. package/src/lib/devtools/internal/devtools-feature.ts +37 -0
  22. package/src/lib/devtools/internal/devtools-syncer.service.ts +202 -0
  23. package/src/lib/devtools/internal/glitch-tracker.service.ts +61 -0
  24. package/src/lib/devtools/internal/models.ts +29 -0
  25. package/{lib/devtools/provide-devtools-config.d.ts → src/lib/devtools/provide-devtools-config.ts} +16 -4
  26. package/src/lib/devtools/rename-devtools-name.ts +21 -0
  27. package/src/lib/devtools/tests/action-name.spec.ts +48 -0
  28. package/src/lib/devtools/tests/basic.spec.ts +111 -0
  29. package/src/lib/devtools/tests/connecting.spec.ts +37 -0
  30. package/src/lib/devtools/tests/helpers.spec.ts +43 -0
  31. package/src/lib/devtools/tests/naming.spec.ts +216 -0
  32. package/src/lib/devtools/tests/provide-devtools-config.spec.ts +25 -0
  33. package/src/lib/devtools/tests/types.spec.ts +19 -0
  34. package/src/lib/devtools/tests/update-state.spec.ts +29 -0
  35. package/src/lib/devtools/tests/with-devtools.spec.ts +5 -0
  36. package/src/lib/devtools/tests/with-glitch-tracking.spec.ts +272 -0
  37. package/src/lib/devtools/tests/with-mapper.spec.ts +69 -0
  38. package/src/lib/devtools/update-state.ts +38 -0
  39. package/{lib/devtools/with-dev-tools-stub.d.ts → src/lib/devtools/with-dev-tools-stub.ts} +2 -1
  40. package/src/lib/devtools/with-devtools.ts +81 -0
  41. package/src/lib/flattening-operator.ts +42 -0
  42. package/src/lib/immutable-state/deep-freeze.ts +43 -0
  43. package/src/lib/immutable-state/is-dev-mode.ts +6 -0
  44. package/src/lib/immutable-state/tests/with-immutable-state.spec.ts +260 -0
  45. package/src/lib/immutable-state/with-immutable-state.ts +115 -0
  46. package/src/lib/mutation/http-mutation.spec.ts +473 -0
  47. package/src/lib/mutation/http-mutation.ts +172 -0
  48. package/src/lib/mutation/mutation.ts +26 -0
  49. package/src/lib/mutation/rx-mutation.spec.ts +594 -0
  50. package/src/lib/mutation/rx-mutation.ts +208 -0
  51. package/src/lib/shared/prettify.ts +3 -0
  52. package/{lib/shared/signal-store-models.d.ts → src/lib/shared/signal-store-models.ts} +8 -4
  53. package/src/lib/shared/throw-if-null.ts +7 -0
  54. package/src/lib/storage-sync/features/with-indexed-db.ts +81 -0
  55. package/src/lib/storage-sync/features/with-local-storage.ts +58 -0
  56. package/src/lib/storage-sync/internal/indexeddb.service.ts +124 -0
  57. package/src/lib/storage-sync/internal/local-storage.service.ts +19 -0
  58. package/src/lib/storage-sync/internal/models.ts +62 -0
  59. package/src/lib/storage-sync/internal/session-storage.service.ts +18 -0
  60. package/src/lib/storage-sync/tests/indexeddb.service.spec.ts +99 -0
  61. package/src/lib/storage-sync/tests/with-storage-async.spec.ts +308 -0
  62. package/src/lib/storage-sync/tests/with-storage-sync.spec.ts +268 -0
  63. package/src/lib/storage-sync/with-storage-sync.ts +233 -0
  64. package/src/lib/with-call-state.spec.ts +42 -0
  65. package/src/lib/with-call-state.ts +195 -0
  66. package/src/lib/with-conditional.spec.ts +125 -0
  67. package/{lib/with-conditional.d.ts → src/lib/with-conditional.ts} +31 -7
  68. package/src/lib/with-data-service.spec.ts +564 -0
  69. package/src/lib/with-data-service.ts +433 -0
  70. package/src/lib/with-feature-factory.spec.ts +69 -0
  71. package/{lib/with-feature-factory.d.ts → src/lib/with-feature-factory.ts} +32 -4
  72. package/src/lib/with-mutations.spec.ts +537 -0
  73. package/src/lib/with-mutations.ts +146 -0
  74. package/src/lib/with-pagination.spec.ts +90 -0
  75. package/src/lib/with-pagination.ts +353 -0
  76. package/src/lib/with-redux.spec.ts +258 -0
  77. package/src/lib/with-redux.ts +387 -0
  78. package/src/lib/with-reset.spec.ts +112 -0
  79. package/src/lib/with-reset.ts +62 -0
  80. package/src/lib/with-undo-redo.spec.ts +287 -0
  81. package/src/lib/with-undo-redo.ts +199 -0
  82. package/src/test-setup.ts +8 -0
  83. package/tsconfig.json +29 -0
  84. package/tsconfig.lib.json +17 -0
  85. package/tsconfig.lib.prod.json +9 -0
  86. package/tsconfig.spec.json +17 -0
  87. package/fesm2022/angular-architects-ngrx-toolkit-redux-connector.mjs +0 -119
  88. package/fesm2022/angular-architects-ngrx-toolkit-redux-connector.mjs.map +0 -1
  89. package/fesm2022/angular-architects-ngrx-toolkit.mjs +0 -1787
  90. package/fesm2022/angular-architects-ngrx-toolkit.mjs.map +0 -1
  91. package/lib/assertions/assertions.d.ts +0 -2
  92. package/lib/devtools/internal/current-action-names.d.ts +0 -1
  93. package/lib/devtools/internal/default-tracker.d.ts +0 -13
  94. package/lib/devtools/internal/devtools-feature.d.ts +0 -24
  95. package/lib/devtools/internal/devtools-syncer.service.d.ts +0 -35
  96. package/lib/devtools/internal/glitch-tracker.service.d.ts +0 -18
  97. package/lib/devtools/internal/models.d.ts +0 -24
  98. package/lib/devtools/rename-devtools-name.d.ts +0 -7
  99. package/lib/devtools/update-state.d.ts +0 -15
  100. package/lib/devtools/with-devtools.d.ts +0 -24
  101. package/lib/immutable-state/deep-freeze.d.ts +0 -11
  102. package/lib/immutable-state/is-dev-mode.d.ts +0 -1
  103. package/lib/immutable-state/with-immutable-state.d.ts +0 -60
  104. package/lib/shared/throw-if-null.d.ts +0 -1
  105. package/lib/storage-sync/features/with-indexed-db.d.ts +0 -2
  106. package/lib/storage-sync/features/with-local-storage.d.ts +0 -3
  107. package/lib/storage-sync/internal/indexeddb.service.d.ts +0 -29
  108. package/lib/storage-sync/internal/local-storage.service.d.ts +0 -8
  109. package/lib/storage-sync/internal/models.d.ts +0 -45
  110. package/lib/storage-sync/internal/session-storage.service.d.ts +0 -8
  111. package/lib/storage-sync/with-storage-sync.d.ts +0 -45
  112. package/lib/with-call-state.d.ts +0 -58
  113. package/lib/with-data-service.d.ts +0 -109
  114. package/lib/with-pagination.d.ts +0 -98
  115. package/lib/with-redux.d.ts +0 -147
  116. package/lib/with-reset.d.ts +0 -29
  117. package/lib/with-undo-redo.d.ts +0 -31
  118. package/redux-connector/index.d.ts +0 -2
  119. package/redux-connector/src/lib/create-redux.d.ts +0 -13
  120. package/redux-connector/src/lib/model.d.ts +0 -40
  121. package/redux-connector/src/lib/rxjs-interop/redux-method.d.ts +0 -14
  122. package/redux-connector/src/lib/signal-redux-store.d.ts +0 -11
  123. package/redux-connector/src/lib/util.d.ts +0 -5
@@ -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,308 @@
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
+ withStorageSync({ key, autoSync: false }, withIndexedDB()),
63
+ );
64
+ const store = TestBed.inject(Store);
65
+ await waitForSyncStable(store);
66
+
67
+ expect(getState(store)).toEqual({});
68
+
69
+ await store.readFromStorage();
70
+
71
+ expect(getState(store)).toEqual({
72
+ foo: 'baz',
73
+ age: 99,
74
+ });
75
+
76
+ patchState(store, { ...initialState });
77
+ await waitForSyncStable(store);
78
+
79
+ expect(await indexedDBService.getItem(key)).toEqual({
80
+ foo: 'baz',
81
+ age: 99,
82
+ });
83
+
84
+ await store.writeToStorage();
85
+ expect(await indexedDBService.getItem(key)).toEqual({
86
+ ...initialState,
87
+ });
88
+
89
+ await store.clearStorage();
90
+ expect(await indexedDBService.getItem(key)).toEqual(null);
91
+ });
92
+ });
93
+
94
+ describe('autoSync', () => {
95
+ it('inits from storage and write to storage on changes when set to `true`', async () => {
96
+ const indexedDBService = TestBed.inject(IndexedDBService);
97
+ // prefill storage
98
+ await indexedDBService.setItem(
99
+ key,
100
+ JSON.stringify({
101
+ foo: 'baz',
102
+ age: 99,
103
+ } as StateObject),
104
+ );
105
+
106
+ const Store = signalStore(
107
+ { providedIn: 'root', protectedState: false },
108
+ withStorageSync(key, withIndexedDB()),
109
+ );
110
+
111
+ const store = TestBed.inject(Store);
112
+ await waitForSyncStable(store);
113
+ expect(getState(store)).toEqual({
114
+ foo: 'baz',
115
+ age: 99,
116
+ });
117
+
118
+ patchState(store, { ...initialState });
119
+ await waitForSyncStable(store);
120
+
121
+ expect(getState(store)).toEqual({
122
+ ...initialState,
123
+ });
124
+
125
+ expect(await indexedDBService.getItem(key)).toEqual(
126
+ JSON.stringify(initialState),
127
+ );
128
+ });
129
+
130
+ it('does not init from storage and does write to storage on changes when set to `false`', async () => {
131
+ const indexedDBService = TestBed.inject(IndexedDBService);
132
+ await indexedDBService.setItem(
133
+ key,
134
+ JSON.stringify({
135
+ foo: 'baz',
136
+ age: 99,
137
+ }),
138
+ );
139
+
140
+ const Store = signalStore(
141
+ { providedIn: 'root', protectedState: false },
142
+ withStorageSync({ key, autoSync: false }, withIndexedDB()),
143
+ );
144
+ const store = TestBed.inject(Store);
145
+ expect(store.isSynced()).toBe(false);
146
+ expect(getState(store)).toEqual({});
147
+
148
+ patchState(store, { ...initialState });
149
+ expect(store.isSynced()).toBe(false);
150
+
151
+ const storeItem = JSON.parse(
152
+ (await indexedDBService.getItem(key)) || '{}',
153
+ );
154
+ expect(storeItem).toEqual({
155
+ foo: 'baz',
156
+ age: 99,
157
+ });
158
+ });
159
+ });
160
+
161
+ describe('select', () => {
162
+ it('syncs the whole state by default', async () => {
163
+ const indexedDBService = TestBed.inject(IndexedDBService);
164
+ const Store = signalStore(
165
+ { providedIn: 'root', protectedState: false },
166
+ withStorageSync(key, withIndexedDB()),
167
+ );
168
+ const store = TestBed.inject(Store);
169
+ await waitForSyncStable(store);
170
+
171
+ patchState(store, { ...initialState });
172
+ await waitForSyncStable(store);
173
+
174
+ expect(await indexedDBService.getItem(key)).toEqual(
175
+ JSON.stringify(initialState),
176
+ );
177
+ });
178
+
179
+ it('syncs selected slices when specified', async () => {
180
+ const indexedDBService = TestBed.inject(IndexedDBService);
181
+ const Store = signalStore(
182
+ { providedIn: 'root', protectedState: false },
183
+ withState(initialState),
184
+ withStorageSync(
185
+ { key, select: ({ foo }) => ({ foo }) },
186
+ withIndexedDB(),
187
+ ),
188
+ );
189
+ const store = TestBed.inject(Store);
190
+ await waitForSyncStable(store);
191
+
192
+ patchState(store, { foo: 'baz' });
193
+ await waitForSyncStable(store);
194
+
195
+ const storeItem = JSON.parse(
196
+ (await indexedDBService.getItem(key)) || '{}',
197
+ );
198
+ expect(storeItem).toEqual({
199
+ foo: 'baz',
200
+ });
201
+ });
202
+ });
203
+
204
+ describe('parse/stringify', () => {
205
+ it('uses custom parsing/stringification when specified', async () => {
206
+ const indexedDBService = TestBed.inject(IndexedDBService);
207
+ const parse = (stateString: string) => {
208
+ const [foo, age] = stateString.split('_');
209
+ return {
210
+ foo,
211
+ age: +age,
212
+ };
213
+ };
214
+
215
+ const Store = signalStore(
216
+ { providedIn: 'root', protectedState: false },
217
+ withState(initialState),
218
+ withStorageSync(
219
+ {
220
+ key,
221
+ parse,
222
+ stringify: (state) => `${state.foo}_${state.age}`,
223
+ },
224
+ withIndexedDB(),
225
+ ),
226
+ );
227
+
228
+ const store = TestBed.inject(Store);
229
+ await waitForSyncStable(store);
230
+ patchState(store, { foo: 'baz' });
231
+ await waitForSyncStable(store);
232
+
233
+ const storeItem = parse((await indexedDBService.getItem(key)) || '');
234
+ expect(storeItem).toEqual({
235
+ ...initialState,
236
+ foo: 'baz',
237
+ });
238
+ });
239
+ });
240
+
241
+ describe('withStorageSync', () => {
242
+ let warnings = [] as string[];
243
+
244
+ jest.spyOn(console, 'warn').mockImplementation((...messages: string[]) => {
245
+ warnings.push(...messages);
246
+ });
247
+
248
+ beforeEach(() => {
249
+ warnings = [];
250
+ });
251
+
252
+ it('logs when writing happens before state is synchronized', async () => {
253
+ const Store = signalStore(
254
+ { providedIn: 'root', protectedState: false },
255
+ withState({ name: 'Delta', age: 52 }),
256
+ withStorageSync('flights', withIndexedDB()),
257
+ );
258
+ const store = TestBed.inject(Store);
259
+
260
+ expect(warnings).toEqual([]);
261
+ patchState(store, { name: 'Lufthansa', age: 27 });
262
+ expect(warnings).toEqual([
263
+ 'Writing to Store (flights) happened before the state was initially read from storage.',
264
+ 'Please ensure that the store is not in syncing state via `store.whenSynced()` before writing to the state.',
265
+ 'Alternatively, you can disable autoSync by passing `autoSync: false` in the config.',
266
+ ]);
267
+ });
268
+
269
+ it('warns when reading happens during a write', async () => {
270
+ console.log('starting test');
271
+ const Store = signalStore(
272
+ { providedIn: 'root', protectedState: false },
273
+ withState({ name: 'Delta', age: 52 }),
274
+ withStorageSync('flights', withIndexedDB()),
275
+ );
276
+
277
+ const store = TestBed.inject(Store);
278
+ await waitForSyncStable(store);
279
+ patchState(store, { name: 'Lufthansa', age: 27 });
280
+ store.readFromStorage();
281
+
282
+ expect(warnings).toEqual([
283
+ 'Reading to Store (flights) happened during an ongoing synchronization process.',
284
+ 'Please ensure that the store is not in syncing state via `store.whenSynced()`.',
285
+ 'Alternatively, you can disable the autoSync by passing `autoSync: false` in the config.',
286
+ ]);
287
+ });
288
+
289
+ it('warns when writing happens during a read', async () => {
290
+ const Store = signalStore(
291
+ { providedIn: 'root', protectedState: false },
292
+ withState({ name: 'Delta', age: 52 }),
293
+ withStorageSync('flights', withIndexedDB()),
294
+ );
295
+
296
+ const store = TestBed.inject(Store);
297
+ await waitForSyncStable(store);
298
+
299
+ store.readFromStorage();
300
+ patchState(store, { name: 'Lufthansa', age: 27 });
301
+ expect(warnings).toEqual([
302
+ 'Writing to Store (flights) happened during an ongoing synchronization process.',
303
+ 'Please ensure that the store is not in syncing state via `store.whenSynced()`.',
304
+ 'Alternatively, you can disable the autoSync by passing `autoSync: false` in the config.',
305
+ ]);
306
+ });
307
+ });
308
+ });
@@ -0,0 +1,268 @@
1
+ import { TestBed } from '@angular/core/testing';
2
+ import { getState, patchState, signalStore, withState } from '@ngrx/signals';
3
+ import { withLocalStorage } from '../features/with-local-storage';
4
+ import { withStorageSync } from '../with-storage-sync';
5
+
6
+ interface StateObject {
7
+ foo: string;
8
+ age: number;
9
+ }
10
+
11
+ const initialState: StateObject = {
12
+ foo: 'bar',
13
+ age: 18,
14
+ };
15
+ const key = 'FooBar';
16
+
17
+ describe('withStorageSync (sync storage)', () => {
18
+ beforeEach(() => {
19
+ // make sure to start with a clean storage
20
+ localStorage.removeItem(key);
21
+ });
22
+
23
+ it('adds methods for storage access to the store', () => {
24
+ TestBed.runInInjectionContext(() => {
25
+ const Store = signalStore(withStorageSync({ key }));
26
+ const store = new Store();
27
+
28
+ expect(Object.keys(store)).toEqual([
29
+ 'clearStorage',
30
+ 'readFromStorage',
31
+ 'writeToStorage',
32
+ ]);
33
+ });
34
+ });
35
+
36
+ it('offers manual sync using provided methods', () => {
37
+ TestBed.runInInjectionContext(() => {
38
+ // prefill storage
39
+ localStorage.setItem(
40
+ key,
41
+ JSON.stringify({
42
+ foo: 'baz',
43
+ age: 99,
44
+ } as StateObject),
45
+ );
46
+
47
+ const Store = signalStore(
48
+ { protectedState: false },
49
+ withStorageSync({ key, autoSync: false }),
50
+ );
51
+ const store = new Store();
52
+ expect(getState(store)).toEqual({});
53
+
54
+ store.readFromStorage();
55
+ expect(getState(store)).toEqual({
56
+ foo: 'baz',
57
+ age: 99,
58
+ });
59
+
60
+ patchState(store, { ...initialState });
61
+ TestBed.flushEffects();
62
+
63
+ let storeItem = JSON.parse(localStorage.getItem(key) || '{}');
64
+ expect(storeItem).toEqual({
65
+ foo: 'baz',
66
+ age: 99,
67
+ });
68
+
69
+ store.writeToStorage();
70
+ storeItem = JSON.parse(localStorage.getItem(key) || '{}');
71
+ expect(storeItem).toEqual({
72
+ ...initialState,
73
+ });
74
+
75
+ store.clearStorage();
76
+ storeItem = localStorage.getItem(key);
77
+ expect(storeItem).toEqual(null);
78
+ });
79
+ });
80
+
81
+ describe('autoSync', () => {
82
+ it('inits from storage and write to storage on changes when set to `true`', () => {
83
+ TestBed.runInInjectionContext(() => {
84
+ // prefill storage
85
+ localStorage.setItem(
86
+ key,
87
+ JSON.stringify({
88
+ foo: 'baz',
89
+ age: 99,
90
+ } as StateObject),
91
+ );
92
+
93
+ const Store = signalStore(
94
+ { protectedState: false },
95
+ withStorageSync(key),
96
+ );
97
+ const store = new Store();
98
+ expect(getState(store)).toEqual({
99
+ foo: 'baz',
100
+ age: 99,
101
+ });
102
+
103
+ patchState(store, { ...initialState });
104
+ TestBed.flushEffects();
105
+
106
+ expect(getState(store)).toEqual({
107
+ ...initialState,
108
+ });
109
+ const storeItem = JSON.parse(localStorage.getItem(key) || '{}');
110
+ expect(storeItem).toEqual({
111
+ ...initialState,
112
+ });
113
+ });
114
+ });
115
+
116
+ it('does not init from storage and does write to storage on changes when set to `false`', () => {
117
+ TestBed.runInInjectionContext(() => {
118
+ // prefill storage
119
+ localStorage.setItem(
120
+ key,
121
+ JSON.stringify({
122
+ foo: 'baz',
123
+ age: 99,
124
+ } as StateObject),
125
+ );
126
+
127
+ const Store = signalStore(
128
+ { protectedState: false },
129
+ withStorageSync({ key, autoSync: false }),
130
+ );
131
+ const store = new Store();
132
+ expect(getState(store)).toEqual({});
133
+
134
+ patchState(store, { ...initialState });
135
+ const storeItem = JSON.parse(localStorage.getItem(key) || '{}');
136
+ expect(storeItem).toEqual({
137
+ foo: 'baz',
138
+ age: 99,
139
+ });
140
+ });
141
+ });
142
+ });
143
+
144
+ describe('select', () => {
145
+ it('syncs the whole state by default', () => {
146
+ TestBed.runInInjectionContext(() => {
147
+ const Store = signalStore(
148
+ { protectedState: false },
149
+ withStorageSync(key),
150
+ );
151
+ const store = new Store();
152
+
153
+ patchState(store, { ...initialState });
154
+ TestBed.flushEffects();
155
+
156
+ const storeItem = JSON.parse(localStorage.getItem(key) || '{}');
157
+ expect(storeItem).toEqual({
158
+ ...initialState,
159
+ });
160
+ });
161
+ });
162
+
163
+ it('syncs selected slices when specified', () => {
164
+ TestBed.runInInjectionContext(() => {
165
+ const Store = signalStore(
166
+ { protectedState: false },
167
+ withState(initialState),
168
+ withStorageSync({ key, select: ({ foo }) => ({ foo }) }),
169
+ );
170
+ const store = new Store();
171
+
172
+ patchState(store, { foo: 'baz' });
173
+ TestBed.flushEffects();
174
+
175
+ const storeItem = JSON.parse(localStorage.getItem(key) || '{}');
176
+ expect(storeItem).toEqual({
177
+ foo: 'baz',
178
+ });
179
+ });
180
+ });
181
+ });
182
+
183
+ describe('parse/stringify', () => {
184
+ it('uses custom parsing/stringification when specified', () => {
185
+ const parse = (stateString: string) => {
186
+ const [foo, age] = stateString.split('_');
187
+ return {
188
+ foo,
189
+ age: +age,
190
+ };
191
+ };
192
+
193
+ TestBed.runInInjectionContext(() => {
194
+ const Store = signalStore(
195
+ { protectedState: false },
196
+ withState(initialState),
197
+ withStorageSync({
198
+ key,
199
+ parse,
200
+ stringify: (state) => `${state.foo}_${state.age}`,
201
+ }),
202
+ );
203
+ const store = new Store();
204
+
205
+ patchState(store, { foo: 'baz' });
206
+ TestBed.flushEffects();
207
+
208
+ const storeItem = parse(localStorage.getItem(key) || '');
209
+ expect(storeItem).toEqual({
210
+ ...initialState,
211
+ foo: 'baz',
212
+ });
213
+ });
214
+ });
215
+ });
216
+
217
+ describe('storage factory', () => {
218
+ it('should throw an error when both config and storage strategy are provided', () => {
219
+ const signalStoreFactory = () =>
220
+ signalStore(
221
+ withStorageSync(
222
+ { key: 'foo', storage: () => localStorage },
223
+ withLocalStorage(),
224
+ ),
225
+ );
226
+
227
+ expect(signalStoreFactory).toThrow(
228
+ 'You can either pass a storage strategy or a config with storage, but not both.',
229
+ );
230
+ });
231
+
232
+ it('uses specified storage', () => {
233
+ TestBed.runInInjectionContext(() => {
234
+ // prefill storage
235
+ sessionStorage.setItem(
236
+ key,
237
+ JSON.stringify({
238
+ foo: 'baz',
239
+ age: 99,
240
+ } as StateObject),
241
+ );
242
+
243
+ const Store = signalStore(
244
+ { protectedState: false },
245
+ withStorageSync({ key, storage: () => sessionStorage }),
246
+ );
247
+ const store = new Store();
248
+ expect(getState(store)).toEqual({
249
+ foo: 'baz',
250
+ age: 99,
251
+ });
252
+
253
+ patchState(store, { ...initialState });
254
+ TestBed.flushEffects();
255
+
256
+ expect(getState(store)).toEqual({
257
+ ...initialState,
258
+ });
259
+ const storeItem = JSON.parse(sessionStorage.getItem(key) || '{}');
260
+ expect(storeItem).toEqual({
261
+ ...initialState,
262
+ });
263
+
264
+ store.clearStorage();
265
+ });
266
+ });
267
+ });
268
+ });