@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.
- 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 -1780
- package/fesm2022/angular-architects-ngrx-toolkit.mjs.map +0 -1
- package/index.d.ts +0 -938
- package/redux-connector/index.d.ts +0 -59
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createEnvironmentInjector,
|
|
3
|
+
EnvironmentInjector,
|
|
4
|
+
inject,
|
|
5
|
+
runInInjectionContext,
|
|
6
|
+
} from '@angular/core';
|
|
7
|
+
import { TestBed } from '@angular/core/testing';
|
|
8
|
+
import { patchState, signalStore, withMethods, withState } from '@ngrx/signals';
|
|
9
|
+
import { withGlitchTracking } from '../features/with-glitch-tracking';
|
|
10
|
+
import { renameDevtoolsName } from '../rename-devtools-name';
|
|
11
|
+
import { withDevtools } from '../with-devtools';
|
|
12
|
+
import { setupExtensions } from './helpers.spec';
|
|
13
|
+
|
|
14
|
+
describe('withGlitchTracking', () => {
|
|
15
|
+
it('should sync immediately upon instantiation', () => {
|
|
16
|
+
const { sendSpy } = setupExtensions();
|
|
17
|
+
|
|
18
|
+
const Store = signalStore(
|
|
19
|
+
{ providedIn: 'root' },
|
|
20
|
+
withDevtools('counter', withGlitchTracking()),
|
|
21
|
+
withState({ count: 0 }),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
expect(sendSpy).not.toHaveBeenCalled();
|
|
25
|
+
TestBed.inject(Store);
|
|
26
|
+
|
|
27
|
+
expect(sendSpy).toHaveBeenCalledWith(
|
|
28
|
+
{ type: 'Store Update' },
|
|
29
|
+
{ counter: { count: 0 } },
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should sync synchronous state changes', () => {
|
|
34
|
+
const { sendSpy } = setupExtensions();
|
|
35
|
+
|
|
36
|
+
const Store = signalStore(
|
|
37
|
+
{ providedIn: 'root' },
|
|
38
|
+
withState({ count: 0 }),
|
|
39
|
+
withDevtools('counter', withGlitchTracking()),
|
|
40
|
+
withMethods((store) => ({
|
|
41
|
+
increase: () =>
|
|
42
|
+
patchState(store, (value) => ({ count: value.count + 1 })),
|
|
43
|
+
})),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const store = TestBed.inject(Store);
|
|
47
|
+
|
|
48
|
+
store.increase();
|
|
49
|
+
store.increase();
|
|
50
|
+
store.increase();
|
|
51
|
+
TestBed.flushEffects();
|
|
52
|
+
|
|
53
|
+
expect(sendSpy.mock.calls).toEqual([
|
|
54
|
+
[{ type: 'Store Update' }, { counter: { count: 0 } }],
|
|
55
|
+
[{ type: 'Store Update' }, { counter: { count: 1 } }],
|
|
56
|
+
[{ type: 'Store Update' }, { counter: { count: 2 } }],
|
|
57
|
+
[{ type: 'Store Update' }, { counter: { count: 3 } }],
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should support a mixed approach', () => {
|
|
62
|
+
const { sendSpy } = setupExtensions();
|
|
63
|
+
|
|
64
|
+
const GlitchFreeStore = signalStore(
|
|
65
|
+
{ providedIn: 'root' },
|
|
66
|
+
withState({ count: 0 }),
|
|
67
|
+
withDevtools('glitch-free counter'),
|
|
68
|
+
withMethods((store) => ({
|
|
69
|
+
increase: () =>
|
|
70
|
+
patchState(store, (value) => ({ count: value.count + 1 })),
|
|
71
|
+
})),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const GlitchStore = signalStore(
|
|
75
|
+
{ providedIn: 'root' },
|
|
76
|
+
withState({ count: 0 }),
|
|
77
|
+
withDevtools('glitch counter', withGlitchTracking()),
|
|
78
|
+
withMethods((store) => ({
|
|
79
|
+
increase: () =>
|
|
80
|
+
patchState(store, (value) => ({ count: value.count + 1 })),
|
|
81
|
+
})),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const glitchFreeStore = TestBed.inject(GlitchFreeStore);
|
|
85
|
+
const glitchStore = TestBed.inject(GlitchStore);
|
|
86
|
+
|
|
87
|
+
TestBed.flushEffects();
|
|
88
|
+
for (let i = 0; i < 2; i++) {
|
|
89
|
+
glitchFreeStore.increase();
|
|
90
|
+
glitchStore.increase();
|
|
91
|
+
}
|
|
92
|
+
TestBed.flushEffects();
|
|
93
|
+
|
|
94
|
+
expect(sendSpy.mock.calls).toEqual([
|
|
95
|
+
[{ type: 'Store Update' }, { 'glitch counter': { count: 0 } }],
|
|
96
|
+
[
|
|
97
|
+
{ type: 'Store Update' },
|
|
98
|
+
{ 'glitch-free counter': { count: 0 }, 'glitch counter': { count: 0 } },
|
|
99
|
+
],
|
|
100
|
+
[
|
|
101
|
+
{ type: 'Store Update' },
|
|
102
|
+
{ 'glitch-free counter': { count: 0 }, 'glitch counter': { count: 1 } },
|
|
103
|
+
],
|
|
104
|
+
[
|
|
105
|
+
{ type: 'Store Update' },
|
|
106
|
+
{ 'glitch-free counter': { count: 0 }, 'glitch counter': { count: 2 } },
|
|
107
|
+
],
|
|
108
|
+
[
|
|
109
|
+
{ type: 'Store Update' },
|
|
110
|
+
{ 'glitch-free counter': { count: 2 }, 'glitch counter': { count: 2 } },
|
|
111
|
+
],
|
|
112
|
+
]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('two glitch stores should sync per change', () => {
|
|
116
|
+
const { sendSpy } = setupExtensions();
|
|
117
|
+
|
|
118
|
+
const GlitchStore1 = signalStore(
|
|
119
|
+
{ providedIn: 'root' },
|
|
120
|
+
withState({ count: 0 }),
|
|
121
|
+
withDevtools('glitch counter 1', withGlitchTracking()),
|
|
122
|
+
withMethods((store) => ({
|
|
123
|
+
increase: () =>
|
|
124
|
+
patchState(store, (value) => ({ count: value.count + 1 })),
|
|
125
|
+
})),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const GlitchStore2 = signalStore(
|
|
129
|
+
{ providedIn: 'root' },
|
|
130
|
+
withState({ count: 0 }),
|
|
131
|
+
withDevtools('glitch counter 2', withGlitchTracking()),
|
|
132
|
+
withMethods((store) => ({
|
|
133
|
+
increase: () =>
|
|
134
|
+
patchState(store, (value) => ({ count: value.count + 1 })),
|
|
135
|
+
})),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const glitchStore1 = TestBed.inject(GlitchStore1);
|
|
139
|
+
const glitchStore2 = TestBed.inject(GlitchStore2);
|
|
140
|
+
|
|
141
|
+
for (let i = 0; i < 2; i++) {
|
|
142
|
+
glitchStore1.increase();
|
|
143
|
+
glitchStore2.increase();
|
|
144
|
+
}
|
|
145
|
+
TestBed.flushEffects();
|
|
146
|
+
|
|
147
|
+
expect(sendSpy.mock.calls).toEqual([
|
|
148
|
+
[{ type: 'Store Update' }, { 'glitch counter 1': { count: 0 } }],
|
|
149
|
+
[
|
|
150
|
+
{ type: 'Store Update' },
|
|
151
|
+
{ 'glitch counter 1': { count: 0 }, 'glitch counter 2': { count: 0 } },
|
|
152
|
+
],
|
|
153
|
+
[
|
|
154
|
+
{ type: 'Store Update' },
|
|
155
|
+
{ 'glitch counter 1': { count: 1 }, 'glitch counter 2': { count: 0 } },
|
|
156
|
+
],
|
|
157
|
+
[
|
|
158
|
+
{ type: 'Store Update' },
|
|
159
|
+
{ 'glitch counter 1': { count: 1 }, 'glitch counter 2': { count: 1 } },
|
|
160
|
+
],
|
|
161
|
+
[
|
|
162
|
+
{ type: 'Store Update' },
|
|
163
|
+
{ 'glitch counter 1': { count: 2 }, 'glitch counter 2': { count: 1 } },
|
|
164
|
+
],
|
|
165
|
+
[
|
|
166
|
+
{ type: 'Store Update' },
|
|
167
|
+
{ 'glitch counter 1': { count: 2 }, 'glitch counter 2': { count: 2 } },
|
|
168
|
+
],
|
|
169
|
+
]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should not sync glitch-free if glitched is renamed', () => {
|
|
173
|
+
const { sendSpy } = setupExtensions();
|
|
174
|
+
|
|
175
|
+
const GlitchFreeStore = signalStore(
|
|
176
|
+
{ providedIn: 'root' },
|
|
177
|
+
withState({ name: 'Product', price: 10.5 }),
|
|
178
|
+
withDevtools('flight1'),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const GlitchStore = signalStore(
|
|
182
|
+
{ providedIn: 'root' },
|
|
183
|
+
withState({ name: 'Product', price: 10.5 }),
|
|
184
|
+
withDevtools('flight2', withGlitchTracking()),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
TestBed.inject(GlitchFreeStore);
|
|
188
|
+
const glitchStore = TestBed.inject(GlitchStore);
|
|
189
|
+
|
|
190
|
+
TestBed.flushEffects();
|
|
191
|
+
|
|
192
|
+
expect(sendSpy.mock.calls).toEqual([
|
|
193
|
+
[{ type: 'Store Update' }, { flight2: { name: 'Product', price: 10.5 } }],
|
|
194
|
+
[
|
|
195
|
+
{ type: 'Store Update' },
|
|
196
|
+
{
|
|
197
|
+
flight1: { name: 'Product', price: 10.5 },
|
|
198
|
+
flight2: { name: 'Product', price: 10.5 },
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
]);
|
|
202
|
+
sendSpy.mockClear();
|
|
203
|
+
|
|
204
|
+
renameDevtoolsName(glitchStore, 'flights2');
|
|
205
|
+
TestBed.flushEffects();
|
|
206
|
+
|
|
207
|
+
expect(sendSpy.mock.calls).toEqual([
|
|
208
|
+
[
|
|
209
|
+
{ type: 'Store Update' },
|
|
210
|
+
{
|
|
211
|
+
flight1: { name: 'Product', price: 10.5 },
|
|
212
|
+
flights2: { name: 'Product', price: 10.5 },
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
]);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should not sync glitch tracker if glitch-free store is renamed', () => {
|
|
219
|
+
const { sendSpy } = setupExtensions();
|
|
220
|
+
|
|
221
|
+
const GlitchFreeStore = signalStore(
|
|
222
|
+
{ providedIn: 'root' },
|
|
223
|
+
withState({ name: 'Product', price: 10.5 }),
|
|
224
|
+
withDevtools('flight1'),
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
const GlitchStore = signalStore(
|
|
228
|
+
{ providedIn: 'root' },
|
|
229
|
+
withState({ name: 'Product', price: 10.5 }),
|
|
230
|
+
withDevtools('glitched Flights', withGlitchTracking()),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const glitchFreeStore = TestBed.inject(GlitchFreeStore);
|
|
234
|
+
TestBed.inject(GlitchStore);
|
|
235
|
+
TestBed.flushEffects();
|
|
236
|
+
|
|
237
|
+
sendSpy.mockClear();
|
|
238
|
+
renameDevtoolsName(glitchFreeStore, 'glitch-free Flights');
|
|
239
|
+
expect(sendSpy).not.toHaveBeenCalled();
|
|
240
|
+
TestBed.flushEffects();
|
|
241
|
+
|
|
242
|
+
expect(sendSpy.mock.calls).toEqual([
|
|
243
|
+
[
|
|
244
|
+
{ type: 'Store Update' },
|
|
245
|
+
{
|
|
246
|
+
'glitch-free Flights': { name: 'Product', price: 10.5 },
|
|
247
|
+
'glitched Flights': { name: 'Product', price: 10.5 },
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
]);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should destroy watcher if store is destroyed', () => {
|
|
254
|
+
const { sendSpy } = setupExtensions();
|
|
255
|
+
|
|
256
|
+
const GlitchStore = signalStore(
|
|
257
|
+
withState({ name: 'Product', price: 10.5 }),
|
|
258
|
+
withDevtools('Glitched Store', withGlitchTracking()),
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const childContext = createEnvironmentInjector(
|
|
262
|
+
[GlitchStore],
|
|
263
|
+
TestBed.inject(EnvironmentInjector),
|
|
264
|
+
);
|
|
265
|
+
runInInjectionContext(childContext, () => inject(GlitchStore));
|
|
266
|
+
|
|
267
|
+
expect(sendSpy).toHaveBeenCalled();
|
|
268
|
+
sendSpy.mockClear();
|
|
269
|
+
childContext.destroy();
|
|
270
|
+
expect(sendSpy).toHaveBeenCalledWith({ type: 'Store Update' }, {});
|
|
271
|
+
});
|
|
272
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
import { signalStore, withState } from '@ngrx/signals';
|
|
3
|
+
import { withMapper } from '../features/with-mapper';
|
|
4
|
+
import { withDevtools } from '../with-devtools';
|
|
5
|
+
import { setupExtensions } from './helpers.spec';
|
|
6
|
+
|
|
7
|
+
function domRemover(state: Record<string, unknown>) {
|
|
8
|
+
return Object.keys(state).reduce((acc, key) => {
|
|
9
|
+
const value = state[key];
|
|
10
|
+
|
|
11
|
+
if (value instanceof HTMLElement) {
|
|
12
|
+
return acc;
|
|
13
|
+
} else {
|
|
14
|
+
return { ...acc, [key]: value };
|
|
15
|
+
}
|
|
16
|
+
}, {});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('with-mapper', () => {
|
|
20
|
+
it('should remove DOM Nodes', () => {
|
|
21
|
+
const { sendSpy } = setupExtensions();
|
|
22
|
+
|
|
23
|
+
const Store = signalStore(
|
|
24
|
+
{ providedIn: 'root' },
|
|
25
|
+
withState({
|
|
26
|
+
name: 'Car',
|
|
27
|
+
carElement: document.createElement('div'),
|
|
28
|
+
}),
|
|
29
|
+
withDevtools('shop', withMapper(domRemover)),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
TestBed.inject(Store);
|
|
33
|
+
TestBed.flushEffects();
|
|
34
|
+
expect(sendSpy).toHaveBeenCalledWith(
|
|
35
|
+
{ type: 'Store Update' },
|
|
36
|
+
{ shop: { name: 'Car' } },
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should every property ending with *Key', () => {
|
|
41
|
+
const { sendSpy } = setupExtensions();
|
|
42
|
+
const Store = signalStore(
|
|
43
|
+
{ providedIn: 'root' },
|
|
44
|
+
withState({
|
|
45
|
+
name: 'Car',
|
|
46
|
+
unlockKey: '1234',
|
|
47
|
+
}),
|
|
48
|
+
withDevtools(
|
|
49
|
+
'shop',
|
|
50
|
+
withMapper((state: Record<string, unknown>) =>
|
|
51
|
+
Object.keys(state).reduce((acc, key) => {
|
|
52
|
+
if (key.endsWith('Key')) {
|
|
53
|
+
return acc;
|
|
54
|
+
} else {
|
|
55
|
+
return { ...acc, [key]: state[key] };
|
|
56
|
+
}
|
|
57
|
+
}, {}),
|
|
58
|
+
),
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
TestBed.inject(Store);
|
|
63
|
+
TestBed.flushEffects();
|
|
64
|
+
expect(sendSpy).toHaveBeenCalledWith(
|
|
65
|
+
{ type: 'Store Update' },
|
|
66
|
+
{ shop: { name: 'Car' } },
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
patchState as originalPatchState,
|
|
3
|
+
PartialStateUpdater,
|
|
4
|
+
WritableStateSource,
|
|
5
|
+
} from '@ngrx/signals';
|
|
6
|
+
import { currentActionNames } from './internal/current-action-names';
|
|
7
|
+
|
|
8
|
+
type PatchFn = typeof originalPatchState extends (
|
|
9
|
+
arg1: infer First,
|
|
10
|
+
...args: infer Rest
|
|
11
|
+
) => infer Returner
|
|
12
|
+
? (state: First, action: string, ...rest: Rest) => Returner
|
|
13
|
+
: never;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @deprecated Has been renamed to `updateState`
|
|
17
|
+
*/
|
|
18
|
+
export const patchState: PatchFn = (state, action, ...rest) => {
|
|
19
|
+
updateState(state, action, ...rest);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Wrapper of `patchState` for DevTools integration. Next to updating the state,
|
|
24
|
+
* it also sends the action to the DevTools.
|
|
25
|
+
* @param stateSource state of Signal Store
|
|
26
|
+
* @param action name of action how it will show in DevTools
|
|
27
|
+
* @param updaters updater functions or objects
|
|
28
|
+
*/
|
|
29
|
+
export function updateState<State extends object>(
|
|
30
|
+
stateSource: WritableStateSource<State>,
|
|
31
|
+
action: string,
|
|
32
|
+
...updaters: Array<
|
|
33
|
+
Partial<NoInfer<State>> | PartialStateUpdater<NoInfer<State>>
|
|
34
|
+
>
|
|
35
|
+
): void {
|
|
36
|
+
currentActionNames.add(action);
|
|
37
|
+
return originalPatchState(stateSource, ...updaters);
|
|
38
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { inject, InjectionToken } from '@angular/core';
|
|
2
|
+
import {
|
|
3
|
+
EmptyFeatureResult,
|
|
4
|
+
SignalStoreFeature,
|
|
5
|
+
signalStoreFeature,
|
|
6
|
+
withHooks,
|
|
7
|
+
withMethods,
|
|
8
|
+
} from '@ngrx/signals';
|
|
9
|
+
import { DefaultTracker } from './internal/default-tracker';
|
|
10
|
+
import {
|
|
11
|
+
DevtoolsFeature,
|
|
12
|
+
DevtoolsInnerOptions,
|
|
13
|
+
} from './internal/devtools-feature';
|
|
14
|
+
import { DevtoolsSyncer } from './internal/devtools-syncer.service';
|
|
15
|
+
import { ReduxDevtoolsExtension } from './internal/models';
|
|
16
|
+
|
|
17
|
+
declare global {
|
|
18
|
+
interface Window {
|
|
19
|
+
__REDUX_DEVTOOLS_EXTENSION__: ReduxDevtoolsExtension | undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const renameDevtoolsMethodName = '___renameDevtoolsName';
|
|
24
|
+
export const uniqueDevtoolsId = '___uniqueDevtoolsId';
|
|
25
|
+
|
|
26
|
+
const EXISTING_NAMES = new InjectionToken(
|
|
27
|
+
'Array contain existing names for the signal stores',
|
|
28
|
+
{ factory: () => [] as string[], providedIn: 'root' },
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Adds this store as a feature state to the Redux DevTools.
|
|
33
|
+
*
|
|
34
|
+
* By default, the action name is 'Store Update'. You can
|
|
35
|
+
* change that via the {@link updateState} method, which has as second
|
|
36
|
+
* parameter the action name.
|
|
37
|
+
*
|
|
38
|
+
* The standalone function {@link renameDevtoolsName} can rename
|
|
39
|
+
* the store name.
|
|
40
|
+
*
|
|
41
|
+
* @param name name of the store as it should appear in the DevTools
|
|
42
|
+
* @param features features to extend or modify the behavior of the Devtools
|
|
43
|
+
*/
|
|
44
|
+
export function withDevtools(name: string, ...features: DevtoolsFeature[]) {
|
|
45
|
+
return signalStoreFeature(
|
|
46
|
+
withMethods(() => {
|
|
47
|
+
const syncer = inject(DevtoolsSyncer);
|
|
48
|
+
|
|
49
|
+
const id = syncer.getNextId();
|
|
50
|
+
|
|
51
|
+
// TODO: use withProps and symbols
|
|
52
|
+
return {
|
|
53
|
+
[renameDevtoolsMethodName]: (newName: string) => {
|
|
54
|
+
syncer.renameStore(name, newName);
|
|
55
|
+
},
|
|
56
|
+
[uniqueDevtoolsId]: () => id,
|
|
57
|
+
} as Record<string, (newName?: unknown) => unknown>;
|
|
58
|
+
}),
|
|
59
|
+
withHooks((store) => {
|
|
60
|
+
const syncer = inject(DevtoolsSyncer);
|
|
61
|
+
const id = String(store[uniqueDevtoolsId]());
|
|
62
|
+
return {
|
|
63
|
+
onInit() {
|
|
64
|
+
const id = String(store[uniqueDevtoolsId]());
|
|
65
|
+
const finalOptions: DevtoolsInnerOptions = {
|
|
66
|
+
indexNames: !features.some((f) => f.indexNames === false),
|
|
67
|
+
map: features.find((f) => f.map)?.map ?? ((state) => state),
|
|
68
|
+
tracker: inject(
|
|
69
|
+
features.find((f) => f.tracker)?.tracker || DefaultTracker,
|
|
70
|
+
),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
syncer.addStore(id, name, store, finalOptions);
|
|
74
|
+
},
|
|
75
|
+
onDestroy() {
|
|
76
|
+
syncer.removeStore(id);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}),
|
|
80
|
+
) as SignalStoreFeature<EmptyFeatureResult, EmptyFeatureResult>;
|
|
81
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep freezes a state object along its properties with primitive values
|
|
3
|
+
* on the first level.
|
|
4
|
+
*
|
|
5
|
+
* The reason for this is that the final state is a merge of all
|
|
6
|
+
* root properties of all states, i.e. `withState`,....
|
|
7
|
+
*
|
|
8
|
+
* Since the root object will not be part of the state (shadow clone),
|
|
9
|
+
* we are not freezing it.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export function deepFreeze<T extends Record<string | symbol, unknown>>(
|
|
13
|
+
target: T,
|
|
14
|
+
// if empty all properties will be frozen
|
|
15
|
+
propertyNamesToBeFrozen: (string | symbol)[],
|
|
16
|
+
// also means that we are on the first level
|
|
17
|
+
isRoot = true,
|
|
18
|
+
): void {
|
|
19
|
+
const runPropertyNameCheck = propertyNamesToBeFrozen.length > 0;
|
|
20
|
+
for (const key of Reflect.ownKeys(target)) {
|
|
21
|
+
if (runPropertyNameCheck && !propertyNamesToBeFrozen.includes(key)) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const propValue = target[key];
|
|
26
|
+
if (isRecordLike(propValue) && !Object.isFrozen(propValue)) {
|
|
27
|
+
Object.freeze(propValue);
|
|
28
|
+
deepFreeze(propValue, [], false);
|
|
29
|
+
} else if (isRoot) {
|
|
30
|
+
Object.defineProperty(target, key, {
|
|
31
|
+
value: propValue,
|
|
32
|
+
writable: false,
|
|
33
|
+
configurable: false,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isRecordLike(
|
|
40
|
+
target: unknown,
|
|
41
|
+
): target is Record<string | symbol, unknown> {
|
|
42
|
+
return typeof target === 'object' && target !== null;
|
|
43
|
+
}
|