@angular-architects/ngrx-toolkit 20.0.0 → 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.
Files changed (84) 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/src/index.ts +43 -0
  15. package/src/lib/assertions/assertions.ts +9 -0
  16. package/src/lib/devtools/features/with-disabled-name-indicies.ts +31 -0
  17. package/src/lib/devtools/features/with-glitch-tracking.ts +35 -0
  18. package/src/lib/devtools/features/with-mapper.ts +34 -0
  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/src/lib/devtools/provide-devtools-config.ts +32 -0
  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/src/lib/devtools/with-dev-tools-stub.ts +6 -0
  40. package/src/lib/devtools/with-devtools.ts +81 -0
  41. package/src/lib/immutable-state/deep-freeze.ts +43 -0
  42. package/src/lib/immutable-state/is-dev-mode.ts +6 -0
  43. package/src/lib/immutable-state/tests/with-immutable-state.spec.ts +278 -0
  44. package/src/lib/immutable-state/with-immutable-state.ts +150 -0
  45. package/src/lib/shared/prettify.ts +3 -0
  46. package/src/lib/shared/signal-store-models.ts +30 -0
  47. package/src/lib/shared/throw-if-null.ts +7 -0
  48. package/src/lib/storage-sync/features/with-indexed-db.ts +81 -0
  49. package/src/lib/storage-sync/features/with-local-storage.ts +58 -0
  50. package/src/lib/storage-sync/internal/indexeddb.service.ts +124 -0
  51. package/src/lib/storage-sync/internal/local-storage.service.ts +19 -0
  52. package/src/lib/storage-sync/internal/models.ts +62 -0
  53. package/src/lib/storage-sync/internal/session-storage.service.ts +18 -0
  54. package/src/lib/storage-sync/tests/indexeddb.service.spec.ts +99 -0
  55. package/src/lib/storage-sync/tests/with-storage-async.spec.ts +305 -0
  56. package/src/lib/storage-sync/tests/with-storage-sync.spec.ts +273 -0
  57. package/src/lib/storage-sync/with-storage-sync.ts +236 -0
  58. package/src/lib/with-call-state.spec.ts +42 -0
  59. package/src/lib/with-call-state.ts +195 -0
  60. package/src/lib/with-conditional.spec.ts +125 -0
  61. package/src/lib/with-conditional.ts +74 -0
  62. package/src/lib/with-data-service.spec.ts +564 -0
  63. package/src/lib/with-data-service.ts +433 -0
  64. package/src/lib/with-feature-factory.spec.ts +69 -0
  65. package/src/lib/with-feature-factory.ts +56 -0
  66. package/src/lib/with-pagination.spec.ts +135 -0
  67. package/src/lib/with-pagination.ts +373 -0
  68. package/src/lib/with-redux.spec.ts +258 -0
  69. package/src/lib/with-redux.ts +387 -0
  70. package/src/lib/with-reset.spec.ts +112 -0
  71. package/src/lib/with-reset.ts +62 -0
  72. package/src/lib/with-undo-redo.spec.ts +274 -0
  73. package/src/lib/with-undo-redo.ts +200 -0
  74. package/src/test-setup.ts +6 -0
  75. package/tsconfig.json +29 -0
  76. package/tsconfig.lib.json +17 -0
  77. package/tsconfig.lib.prod.json +9 -0
  78. package/tsconfig.spec.json +17 -0
  79. package/fesm2022/angular-architects-ngrx-toolkit-redux-connector.mjs +0 -119
  80. package/fesm2022/angular-architects-ngrx-toolkit-redux-connector.mjs.map +0 -1
  81. package/fesm2022/angular-architects-ngrx-toolkit.mjs +0 -1780
  82. package/fesm2022/angular-architects-ngrx-toolkit.mjs.map +0 -1
  83. package/index.d.ts +0 -938
  84. package/redux-connector/index.d.ts +0 -59
@@ -0,0 +1,111 @@
1
+ import { createEnvironmentInjector, EnvironmentInjector } from '@angular/core';
2
+ import { TestBed } from '@angular/core/testing';
3
+ import { patchState, signalStore, withMethods, withState } from '@ngrx/signals';
4
+ import { renameDevtoolsName } from '../rename-devtools-name';
5
+ import { withDevtools } from '../with-devtools';
6
+ import { setupExtensions } from './helpers.spec';
7
+
8
+ describe('Devtools Basics', () => {
9
+ it('should dispatch update', () => {
10
+ const { sendSpy } = setupExtensions();
11
+ TestBed.inject(
12
+ signalStore(
13
+ { providedIn: 'root' },
14
+ withDevtools('shop'),
15
+ withState({ name: 'Car' }),
16
+ ),
17
+ );
18
+ TestBed.flushEffects();
19
+ expect(sendSpy).toHaveBeenCalledWith(
20
+ { type: 'Store Update' },
21
+ { shop: { name: 'Car' } },
22
+ );
23
+ });
24
+
25
+ it('should add multiple stores as feature stores', () => {
26
+ const { sendSpy } = setupExtensions();
27
+ for (const name of ['category', 'booking']) {
28
+ TestBed.inject(signalStore({ providedIn: 'root' }, withDevtools(name)));
29
+ }
30
+ TestBed.flushEffects();
31
+ expect(sendSpy).toHaveBeenLastCalledWith(
32
+ { type: 'Store Update' },
33
+ {
34
+ category: {},
35
+ booking: {},
36
+ },
37
+ );
38
+ });
39
+
40
+ it('should remove the state once destroyed', () => {
41
+ const { sendSpy } = setupExtensions();
42
+
43
+ const Store = signalStore(withDevtools('flight'));
44
+ const childInjector = createEnvironmentInjector(
45
+ [Store],
46
+ TestBed.inject(EnvironmentInjector),
47
+ );
48
+
49
+ childInjector.get(Store);
50
+ TestBed.flushEffects();
51
+
52
+ expect(sendSpy).toHaveBeenCalledWith(
53
+ { type: 'Store Update' },
54
+ { flight: {} },
55
+ );
56
+
57
+ childInjector.destroy();
58
+ TestBed.flushEffects();
59
+ expect(sendSpy).toHaveBeenCalledWith({ type: 'Store Update' }, {});
60
+ });
61
+
62
+ it('should remove a renamed state once destroyed', () => {
63
+ const { sendSpy } = setupExtensions();
64
+
65
+ const Store = signalStore(withDevtools('flight'));
66
+ const childInjector = createEnvironmentInjector(
67
+ [Store],
68
+ TestBed.inject(EnvironmentInjector),
69
+ );
70
+
71
+ const store = childInjector.get(Store);
72
+ TestBed.flushEffects();
73
+
74
+ expect(sendSpy).toHaveBeenCalledWith(
75
+ { type: 'Store Update' },
76
+ { flight: {} },
77
+ );
78
+
79
+ renameDevtoolsName(store, 'flights');
80
+ childInjector.destroy();
81
+ TestBed.flushEffects();
82
+ expect(sendSpy).toHaveBeenCalledWith({ type: 'Store Update' }, {});
83
+ });
84
+
85
+ it('should group multiple patchState running before the synchronization', () => {
86
+ const { sendSpy } = setupExtensions();
87
+ const store = TestBed.inject(
88
+ signalStore(
89
+ { providedIn: 'root' },
90
+ withDevtools('shop'),
91
+ withState({ name: 'Car', amount: 0 }),
92
+ withMethods((store) => ({
93
+ increment() {
94
+ patchState(store, (value) => ({
95
+ ...value,
96
+ amount: value.amount + 1,
97
+ }));
98
+ },
99
+ })),
100
+ ),
101
+ );
102
+
103
+ store.increment();
104
+ store.increment();
105
+ TestBed.flushEffects();
106
+
107
+ expect(sendSpy.mock.calls).toEqual([
108
+ [{ type: 'Store Update' }, { shop: { name: 'Car', amount: 2 } }],
109
+ ]);
110
+ });
111
+ });
@@ -0,0 +1,37 @@
1
+ import { TestBed } from '@angular/core/testing';
2
+ import { signalStore } from '@ngrx/signals';
3
+ import { withDevtools } from '../with-devtools';
4
+ import { setupExtensions } from './helpers.spec';
5
+
6
+ describe('connect & send', () => {
7
+ it('should connect', () => {
8
+ const Store = signalStore({ providedIn: 'root' }, withDevtools('flight'));
9
+ const { connectSpy } = setupExtensions();
10
+ TestBed.inject(Store);
11
+ expect(connectSpy).toHaveBeenCalledTimes(1);
12
+ });
13
+
14
+ it('should not connect if Redux Devtools are not available', () => {
15
+ const { connectSpy } = setupExtensions(true, false);
16
+ expect(connectSpy).toHaveBeenCalledTimes(0);
17
+ });
18
+
19
+ it('should not throw if it runs on the server', () => {
20
+ setupExtensions(true, false);
21
+ const Store = signalStore({ providedIn: 'root' }, withDevtools('flight'));
22
+ expect(() => TestBed.inject(Store)).not.toThrow();
23
+ });
24
+
25
+ it('should only send when store is initialized', () => {
26
+ const { sendSpy } = setupExtensions();
27
+ expect(sendSpy).toHaveBeenCalledTimes(0);
28
+
29
+ const Store = signalStore({ providedIn: 'root' }, withDevtools('flight'));
30
+ TestBed.flushEffects();
31
+ expect(sendSpy).toHaveBeenCalledTimes(0);
32
+
33
+ TestBed.inject(Store);
34
+ TestBed.flushEffects();
35
+ expect(sendSpy).toHaveBeenCalledTimes(1);
36
+ });
37
+ });
@@ -0,0 +1,43 @@
1
+ import { PLATFORM_ID } from '@angular/core';
2
+ import { TestBed } from '@angular/core/testing';
3
+
4
+ export type Flight = {
5
+ id: number;
6
+ from: string;
7
+ to: string;
8
+ date: Date;
9
+ delayed: boolean;
10
+ };
11
+
12
+ export function setupExtensions(
13
+ isPlatformBrowser = true,
14
+ isExtensionAvailable = true,
15
+ ) {
16
+ const sendSpy = jest.fn();
17
+ const connection = {
18
+ send: sendSpy,
19
+ };
20
+ const connectSpy = jest.fn(() => connection);
21
+
22
+ if (isExtensionAvailable) {
23
+ window.__REDUX_DEVTOOLS_EXTENSION__ = { connect: connectSpy };
24
+ }
25
+
26
+ if (isPlatformBrowser) {
27
+ TestBed.configureTestingModule({
28
+ providers: [
29
+ {
30
+ provide: PLATFORM_ID,
31
+ useValue: isPlatformBrowser ? 'browser' : 'server',
32
+ },
33
+ ],
34
+ });
35
+ }
36
+
37
+ return { sendSpy, connectSpy };
38
+ }
39
+
40
+ it('should initialize', () => {
41
+ const { connectSpy } = setupExtensions();
42
+ expect(connectSpy).not.toHaveBeenCalled();
43
+ });
@@ -0,0 +1,216 @@
1
+ import {
2
+ createEnvironmentInjector,
3
+ EnvironmentInjector,
4
+ inject,
5
+ runInInjectionContext,
6
+ } from '@angular/core';
7
+ import { TestBed } from '@angular/core/testing';
8
+ import { signalStore, withState } from '@ngrx/signals';
9
+ import { withDisabledNameIndices } from '../features/with-disabled-name-indicies';
10
+ import { renameDevtoolsName } from '../rename-devtools-name';
11
+ import { withDevtools } from '../with-devtools';
12
+ import { setupExtensions } from './helpers.spec';
13
+
14
+ describe('withDevtools / renaming', () => {
15
+ it('should automatically index multiple instances', () => {
16
+ const { sendSpy } = setupExtensions();
17
+ const Store = signalStore(
18
+ { providedIn: 'root' },
19
+ withDevtools('flights'),
20
+ withState({ airline: 'Lufthansa' }),
21
+ );
22
+
23
+ const childContext = createEnvironmentInjector(
24
+ [Store],
25
+ TestBed.inject(EnvironmentInjector),
26
+ );
27
+
28
+ TestBed.inject(Store);
29
+ runInInjectionContext(childContext, () => inject(Store));
30
+
31
+ TestBed.flushEffects();
32
+
33
+ expect(sendSpy).toHaveBeenLastCalledWith(
34
+ { type: 'Store Update' },
35
+ {
36
+ flights: { airline: 'Lufthansa' },
37
+ 'flights-1': { airline: 'Lufthansa' },
38
+ },
39
+ );
40
+ });
41
+
42
+ it('not index, if multiple instances do not exist simultaneously', () => {
43
+ const { sendSpy } = setupExtensions();
44
+ const Store = signalStore(
45
+ withDevtools('flights'),
46
+ withState({ airline: 'Lufthansa' }),
47
+ );
48
+
49
+ const envInjector = TestBed.inject(EnvironmentInjector);
50
+ const childContext1 = createEnvironmentInjector([Store], envInjector);
51
+ const childContext2 = createEnvironmentInjector([Store], envInjector);
52
+
53
+ runInInjectionContext(childContext1, () => inject(Store));
54
+ TestBed.flushEffects();
55
+ childContext1.destroy();
56
+
57
+ expect(sendSpy.mock.calls).toEqual([
58
+ [
59
+ { type: 'Store Update' },
60
+ {
61
+ flights: { airline: 'Lufthansa' },
62
+ },
63
+ ],
64
+ ]);
65
+
66
+ runInInjectionContext(childContext2, () => inject(Store));
67
+ TestBed.flushEffects();
68
+ expect(sendSpy.mock.calls).toEqual([
69
+ [
70
+ { type: 'Store Update' },
71
+ {
72
+ flights: { airline: 'Lufthansa' },
73
+ },
74
+ ],
75
+ [
76
+ { type: 'Store Update' },
77
+ {
78
+ flights: { airline: 'Lufthansa' },
79
+ },
80
+ ],
81
+ ]);
82
+ });
83
+
84
+ it('should throw if automatic indexing is disabled', () => {
85
+ setupExtensions();
86
+ const Store = signalStore(
87
+ { providedIn: 'root' },
88
+ withDevtools('flights', withDisabledNameIndices()),
89
+ withState({ airline: 'Lufthansa' }),
90
+ );
91
+
92
+ const childContext = createEnvironmentInjector(
93
+ [Store],
94
+ TestBed.inject(EnvironmentInjector),
95
+ );
96
+
97
+ TestBed.inject(Store);
98
+ expect(() =>
99
+ runInInjectionContext(childContext, () => inject(Store)),
100
+ ).toThrow(
101
+ `An instance of the store flights already exists. \
102
+ Enable automatic indexing via withDevTools('flights', { indexNames: true }), or rename it upon instantiation.`,
103
+ );
104
+ });
105
+
106
+ it('should index for two different stores with same devtools name', () => {
107
+ const { sendSpy } = setupExtensions();
108
+
109
+ TestBed.inject(
110
+ signalStore({ providedIn: 'root' }, withDevtools('flights')),
111
+ );
112
+ TestBed.inject(
113
+ signalStore({ providedIn: 'root' }, withDevtools('flights')),
114
+ );
115
+
116
+ TestBed.flushEffects();
117
+ expect(sendSpy.mock.calls).toEqual([
118
+ [
119
+ { type: 'Store Update' },
120
+ {
121
+ flights: {},
122
+ 'flights-1': {},
123
+ },
124
+ ],
125
+ ]);
126
+ });
127
+
128
+ it('should throw for two different stores when indexing is disabled', () => {
129
+ setupExtensions();
130
+
131
+ TestBed.inject(
132
+ signalStore({ providedIn: 'root' }, withDevtools('flights')),
133
+ );
134
+ expect(() =>
135
+ TestBed.inject(
136
+ signalStore(
137
+ { providedIn: 'root' },
138
+ withDevtools('flights', withDisabledNameIndices()),
139
+ ),
140
+ ),
141
+ ).toThrow();
142
+ });
143
+
144
+ it('should not throw for two different stores if only the first one has indexing disabled', () => {
145
+ setupExtensions();
146
+
147
+ TestBed.inject(
148
+ signalStore(
149
+ { providedIn: 'root' },
150
+ withDevtools('flights', withDisabledNameIndices()),
151
+ ),
152
+ );
153
+ expect(() =>
154
+ TestBed.inject(
155
+ signalStore({ providedIn: 'root' }, withDevtools('flights')),
156
+ ),
157
+ ).not.toThrow();
158
+ });
159
+
160
+ describe('renaming', () => {
161
+ it('should allow to rename the store before first sync', () => {
162
+ const { sendSpy } = setupExtensions();
163
+
164
+ const Store = signalStore(
165
+ { providedIn: 'root' },
166
+ withState({ name: 'Product', price: 10.5 }),
167
+ withDevtools('flight'),
168
+ );
169
+
170
+ const store = TestBed.inject(Store);
171
+ renameDevtoolsName(store, 'flights');
172
+ TestBed.flushEffects();
173
+
174
+ expect(sendSpy).toHaveBeenCalledWith(
175
+ { type: 'Store Update' },
176
+ { flights: { name: 'Product', price: 10.5 } },
177
+ );
178
+ });
179
+
180
+ it('should throw on rename if name already exists', () => {
181
+ setupExtensions();
182
+ const Store1 = signalStore(
183
+ { providedIn: 'root' },
184
+ withState({ name: 'Product', price: 10.5 }),
185
+ withDevtools('shop'),
186
+ );
187
+
188
+ const Store2 = signalStore(
189
+ { providedIn: 'root' },
190
+ withState({ name: 'Product', price: 10.5 }),
191
+ withDevtools('mall'),
192
+ );
193
+ TestBed.inject(Store1);
194
+ const store = TestBed.inject(Store2);
195
+ TestBed.flushEffects();
196
+
197
+ expect(() => renameDevtoolsName(store, 'shop')).toThrow(
198
+ 'NgRx Toolkit/DevTools: cannot rename from mall to shop. shop is already assigned to another SignalStore instance.',
199
+ );
200
+ });
201
+
202
+ it('should throw if applied to a SignalStore without DevTools', () => {
203
+ setupExtensions();
204
+ const Store = signalStore(
205
+ { providedIn: 'root' },
206
+ withState({ name: 'Product', price: 10.5 }),
207
+ );
208
+
209
+ const store = TestBed.inject(Store);
210
+
211
+ expect(() => renameDevtoolsName(store, 'shop')).toThrow(
212
+ "Devtools extensions haven't been added to this store.",
213
+ );
214
+ });
215
+ });
216
+ });
@@ -0,0 +1,25 @@
1
+ import { TestBed } from '@angular/core/testing';
2
+ import { DevtoolsSyncer } from '../internal/devtools-syncer.service';
3
+ import { provideDevtoolsConfig } from '../provide-devtools-config';
4
+ import { setupExtensions } from './helpers.spec';
5
+
6
+ describe('provideDevtoolsConfig', () => {
7
+ it('DevtoolsSyncer should use the default configuration if none is provided', () => {
8
+ const { connectSpy } = setupExtensions();
9
+ TestBed.inject(DevtoolsSyncer);
10
+ expect(connectSpy).toHaveBeenCalledWith({
11
+ name: 'NgRx SignalStore',
12
+ });
13
+ });
14
+
15
+ it('DevtoolsSyncer should use the configuration provided', () => {
16
+ const { connectSpy } = setupExtensions();
17
+ TestBed.configureTestingModule({
18
+ providers: [provideDevtoolsConfig({ name: 'test' })],
19
+ });
20
+ TestBed.inject(DevtoolsSyncer);
21
+ expect(connectSpy).toHaveBeenCalledWith({
22
+ name: 'test',
23
+ });
24
+ });
25
+ });
@@ -0,0 +1,19 @@
1
+ import { computed } from '@angular/core';
2
+ import { patchState, signalStore, withState } from '@ngrx/signals';
3
+ import { withDevtools } from '../with-devtools';
4
+
5
+ it('should compile when signalStore is extended from', () => {
6
+ class CounterStore extends signalStore(
7
+ { protectedState: false },
8
+ withState({ count: 0 }),
9
+ withDevtools('counter-store'),
10
+ ) {
11
+ readonly myReadonlyProp = 42;
12
+
13
+ readonly doubleCount = computed(() => this.count() * 2);
14
+
15
+ increment(): void {
16
+ patchState(this, { count: this.count() + 1 });
17
+ }
18
+ }
19
+ });
@@ -0,0 +1,29 @@
1
+ import { signalStore, withMethods } from '@ngrx/signals';
2
+ import { setAllEntities, withEntities } from '@ngrx/signals/entities';
3
+ import { setLoaded, withCallState } from '../../with-call-state';
4
+ import { updateState } from '../update-state';
5
+
6
+ describe('updateState', () => {
7
+ it('should work with multiple updaters', () => {
8
+ interface Item {
9
+ id: string;
10
+ name: string;
11
+ }
12
+
13
+ signalStore(
14
+ withEntities<Item>(),
15
+ withCallState({ collection: 'items' }),
16
+ withMethods((store) => ({
17
+ loadItems() {
18
+ // This should not cause a type error
19
+ updateState(
20
+ store,
21
+ 'Items loaded successfully',
22
+ setAllEntities([] as Item[]),
23
+ setLoaded('items'),
24
+ );
25
+ },
26
+ })),
27
+ );
28
+ });
29
+ });
@@ -0,0 +1,5 @@
1
+ describe('Devtools', () => {
2
+ it.todo('should group multiple patchStates (glitch-free) in one action');
3
+ it.todo('should allow time-travel (revert state via devtools');
4
+ it.todo('should clear actionNames automatically onDestroy');
5
+ });