@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,31 @@
|
|
|
1
|
+
import { createDevtoolsFeature } from '../internal/devtools-feature';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* If multiple instances of the same SignalStore class
|
|
5
|
+
* exist, their devtool names are indexed.
|
|
6
|
+
*
|
|
7
|
+
* For example:
|
|
8
|
+
*
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const Store = signalStore(
|
|
11
|
+
* withDevtools('flights')
|
|
12
|
+
* )
|
|
13
|
+
*
|
|
14
|
+
* const store1 = new Store(); // will show up as 'flights'
|
|
15
|
+
* const store2 = new Store(); // will show up as 'flights-1'
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* With adding `withDisabledNameIndices` to the store:
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const Store = signalStore(
|
|
21
|
+
* withDevtools('flights', withDisabledNameIndices())
|
|
22
|
+
* )
|
|
23
|
+
*
|
|
24
|
+
* const store1 = new Store(); // will show up as 'flights'
|
|
25
|
+
* const store2 = new Store(); //💥 throws an error
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
*/
|
|
29
|
+
export function withDisabledNameIndices() {
|
|
30
|
+
return createDevtoolsFeature({ indexNames: false });
|
|
31
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createDevtoolsFeature } from '../internal/devtools-feature';
|
|
2
|
+
import { GlitchTrackerService } from '../internal/glitch-tracker.service';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* It tracks all state changes of the State, including intermediary updates
|
|
6
|
+
* that are typically suppressed by Angular's glitch-free mechanism.
|
|
7
|
+
*
|
|
8
|
+
* This feature is especially useful for debugging.
|
|
9
|
+
*
|
|
10
|
+
* Example:
|
|
11
|
+
*
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const Store = signalStore(
|
|
14
|
+
* { providedIn: 'root' },
|
|
15
|
+
* withState({ count: 0 }),
|
|
16
|
+
* withDevtools('counter', withGlitchTracking()),
|
|
17
|
+
* withMethods((store) => ({
|
|
18
|
+
* increase: () =>
|
|
19
|
+
* patchState(store, (value) => ({ count: value.count + 1 })),
|
|
20
|
+
* }))
|
|
21
|
+
* );
|
|
22
|
+
*
|
|
23
|
+
* // would show up in the DevTools with value 0
|
|
24
|
+
* const store = inject(Store);
|
|
25
|
+
*
|
|
26
|
+
* store.increase(); // would show up in the DevTools with value 1
|
|
27
|
+
* store.increase(); // would show up in the DevTools with value 2
|
|
28
|
+
* store.increase(); // would show up in the DevTools with value 3
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* Without `withGlitchTracking`, the DevTools would only show the final value of 3.
|
|
32
|
+
*/
|
|
33
|
+
export function withGlitchTracking() {
|
|
34
|
+
return createDevtoolsFeature({ tracker: GlitchTrackerService });
|
|
35
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createDevtoolsFeature, Mapper } from '../internal/devtools-feature';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Allows you to define a function to map the state.
|
|
5
|
+
*
|
|
6
|
+
* It is needed for huge states, that slows down the Devtools and where
|
|
7
|
+
* you don't need to see the whole state or other reasons.
|
|
8
|
+
*
|
|
9
|
+
* Example:
|
|
10
|
+
*
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const initialState = {
|
|
13
|
+
* id: 1,
|
|
14
|
+
* email: 'john.list@host.com',
|
|
15
|
+
* name: 'John List',
|
|
16
|
+
* enteredPassword: ''
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* const Store = signalStore(
|
|
20
|
+
* withState(initialState),
|
|
21
|
+
* withDevtools(
|
|
22
|
+
* 'user',
|
|
23
|
+
* withMapper(state => ({...state, enteredPassword: '***' }))
|
|
24
|
+
* )
|
|
25
|
+
* )
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @param map function which maps the state
|
|
29
|
+
*/
|
|
30
|
+
export function withMapper<State extends object>(
|
|
31
|
+
map: (state: State) => Record<string, unknown>,
|
|
32
|
+
) {
|
|
33
|
+
return createDevtoolsFeature({ map: map as Mapper });
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const currentActionNames = new Set<string>();
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { effect, Injectable, signal } from '@angular/core';
|
|
2
|
+
import { getState, StateSource } from '@ngrx/signals';
|
|
3
|
+
import { Tracker, TrackerStores } from './models';
|
|
4
|
+
|
|
5
|
+
@Injectable({ providedIn: 'root' })
|
|
6
|
+
export class DefaultTracker implements Tracker {
|
|
7
|
+
readonly #stores = signal<TrackerStores>({});
|
|
8
|
+
|
|
9
|
+
get stores(): TrackerStores {
|
|
10
|
+
return this.#stores();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
#trackCallback: undefined | ((changedState: Record<string, object>) => void);
|
|
14
|
+
|
|
15
|
+
#trackingEffect = effect(() => {
|
|
16
|
+
if (this.#trackCallback === undefined) {
|
|
17
|
+
throw new Error('no callback function defined');
|
|
18
|
+
}
|
|
19
|
+
const stores = this.#stores();
|
|
20
|
+
|
|
21
|
+
const fullState = Object.entries(stores).reduce(
|
|
22
|
+
(acc, [id, store]) => {
|
|
23
|
+
return { ...acc, [id]: getState(store) };
|
|
24
|
+
},
|
|
25
|
+
{} as Record<string, object>,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
this.#trackCallback(fullState);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
track(id: string, store: StateSource<object>): void {
|
|
32
|
+
this.#stores.update((value) => ({
|
|
33
|
+
...value,
|
|
34
|
+
[id]: store,
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
onChange(callback: (changedState: Record<string, object>) => void): void {
|
|
39
|
+
this.#trackCallback = callback;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
removeStore(id: string) {
|
|
43
|
+
this.#stores.update((stores) =>
|
|
44
|
+
Object.entries(stores).reduce((newStore, [storeId, state]) => {
|
|
45
|
+
if (storeId !== id) {
|
|
46
|
+
newStore[storeId] = state;
|
|
47
|
+
}
|
|
48
|
+
return newStore;
|
|
49
|
+
}, {} as TrackerStores),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
notifyRenamedStore(id: string): void {
|
|
54
|
+
if (this.#stores()[id]) {
|
|
55
|
+
this.#stores.update((stores) => {
|
|
56
|
+
return { ...stores };
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Tracker } from './models';
|
|
2
|
+
|
|
3
|
+
export const DEVTOOLS_FEATURE = Symbol('DEVTOOLS_FEATURE');
|
|
4
|
+
|
|
5
|
+
export type Mapper = (state: object) => object;
|
|
6
|
+
|
|
7
|
+
export type DevtoolsOptions = {
|
|
8
|
+
indexNames?: boolean; // defines if names should be indexed.
|
|
9
|
+
map?: Mapper; // defines a mapper for the state.
|
|
10
|
+
tracker?: new () => Tracker; // defines a tracker for the state
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type DevtoolsInnerOptions = {
|
|
14
|
+
indexNames: boolean;
|
|
15
|
+
map: Mapper;
|
|
16
|
+
tracker: Tracker;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A DevtoolsFeature adds or modifies the behavior of the
|
|
21
|
+
* devtools extension.
|
|
22
|
+
*
|
|
23
|
+
* We use them (function calls) instead of a config object,
|
|
24
|
+
* because of tree-shaking.
|
|
25
|
+
*/
|
|
26
|
+
export type DevtoolsFeature = {
|
|
27
|
+
[DEVTOOLS_FEATURE]: true;
|
|
28
|
+
} & Partial<DevtoolsOptions>;
|
|
29
|
+
|
|
30
|
+
export function createDevtoolsFeature(
|
|
31
|
+
options: DevtoolsOptions,
|
|
32
|
+
): DevtoolsFeature {
|
|
33
|
+
return {
|
|
34
|
+
[DEVTOOLS_FEATURE]: true,
|
|
35
|
+
...options,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { isPlatformBrowser } from '@angular/common';
|
|
2
|
+
import { inject, Injectable, OnDestroy, PLATFORM_ID } from '@angular/core';
|
|
3
|
+
import { StateSource } from '@ngrx/signals';
|
|
4
|
+
import { throwIfNull } from '../../shared/throw-if-null';
|
|
5
|
+
import { REDUX_DEVTOOLS_CONFIG } from '../provide-devtools-config';
|
|
6
|
+
import { currentActionNames } from './current-action-names';
|
|
7
|
+
import { DevtoolsInnerOptions } from './devtools-feature';
|
|
8
|
+
import { Connection, StoreRegistry, Tracker } from './models';
|
|
9
|
+
|
|
10
|
+
const dummyConnection: Connection = {
|
|
11
|
+
send: () => void true,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A service provided by the root injector is
|
|
16
|
+
* required because the synchronization runs
|
|
17
|
+
* globally.
|
|
18
|
+
*
|
|
19
|
+
* The SignalStore could be provided in a component.
|
|
20
|
+
* If the effect starts in the injection
|
|
21
|
+
* context of the SignalStore, the complete sync
|
|
22
|
+
* process would shut down once the component gets
|
|
23
|
+
* destroyed.
|
|
24
|
+
*/
|
|
25
|
+
@Injectable({ providedIn: 'root' })
|
|
26
|
+
export class DevtoolsSyncer implements OnDestroy {
|
|
27
|
+
/**
|
|
28
|
+
* Stores all SignalStores that are connected to the
|
|
29
|
+
* DevTools along their options, names and id.
|
|
30
|
+
*/
|
|
31
|
+
#stores: StoreRegistry = {};
|
|
32
|
+
readonly #isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
33
|
+
readonly #trackers = [] as Tracker[];
|
|
34
|
+
readonly #devtoolsConfig = {
|
|
35
|
+
name: 'NgRx SignalStore',
|
|
36
|
+
...inject(REDUX_DEVTOOLS_CONFIG, { optional: true }),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Maintains the current states of all stores to avoid conflicts
|
|
41
|
+
* between glitch-free and glitched trackers when used simultaneously.
|
|
42
|
+
*
|
|
43
|
+
* The challenge lies in ensuring that glitched trackers do not
|
|
44
|
+
* interfere with the synchronization process of glitch-free trackers.
|
|
45
|
+
* Specifically, glitched trackers could cause the synchronization to
|
|
46
|
+
* read the current state of stores managed by glitch-free trackers.
|
|
47
|
+
*
|
|
48
|
+
* Therefore, the synchronization process doesn't read the state from
|
|
49
|
+
* each store, but relies on #currentState.
|
|
50
|
+
*
|
|
51
|
+
* Please note, that here the key is the name and not the id.
|
|
52
|
+
*/
|
|
53
|
+
#currentState: Record<string, object> = {};
|
|
54
|
+
#currentId = 1;
|
|
55
|
+
|
|
56
|
+
readonly #connection: Connection = this.#isBrowser
|
|
57
|
+
? window.__REDUX_DEVTOOLS_EXTENSION__
|
|
58
|
+
? window.__REDUX_DEVTOOLS_EXTENSION__.connect(this.#devtoolsConfig)
|
|
59
|
+
: dummyConnection
|
|
60
|
+
: dummyConnection;
|
|
61
|
+
|
|
62
|
+
constructor() {
|
|
63
|
+
if (!this.#isBrowser) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
ngOnDestroy(): void {
|
|
69
|
+
currentActionNames.clear();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
syncToDevTools(changedStatePerId: Record<string, object>) {
|
|
73
|
+
const mappedChangedStatePerName = Object.entries(changedStatePerId).reduce(
|
|
74
|
+
(acc, [id, store]) => {
|
|
75
|
+
const { options, name } = this.#stores[id];
|
|
76
|
+
acc[name] = options.map(store);
|
|
77
|
+
return acc;
|
|
78
|
+
},
|
|
79
|
+
{} as Record<string, object>,
|
|
80
|
+
);
|
|
81
|
+
this.#currentState = {
|
|
82
|
+
...this.#currentState,
|
|
83
|
+
...mappedChangedStatePerName,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const names = Array.from(currentActionNames);
|
|
87
|
+
const type = names.length ? names.join(', ') : 'Store Update';
|
|
88
|
+
currentActionNames.clear();
|
|
89
|
+
|
|
90
|
+
this.#connection.send({ type }, this.#currentState);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
getNextId() {
|
|
94
|
+
return String(this.#currentId++);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Consumer provides the id. That is because we can only start
|
|
99
|
+
* tracking the store in the init hook.
|
|
100
|
+
* Unfortunately, methods for renaming having the final id
|
|
101
|
+
* need to be defined already before.
|
|
102
|
+
* That's why `withDevtools` requests first the id and
|
|
103
|
+
* then registers itself later.
|
|
104
|
+
*/
|
|
105
|
+
addStore(
|
|
106
|
+
id: string,
|
|
107
|
+
name: string,
|
|
108
|
+
store: StateSource<object>,
|
|
109
|
+
options: DevtoolsInnerOptions,
|
|
110
|
+
) {
|
|
111
|
+
let storeName = name;
|
|
112
|
+
const names = Object.values(this.#stores).map((store) => store.name);
|
|
113
|
+
|
|
114
|
+
if (names.includes(storeName)) {
|
|
115
|
+
// const { options } = throwIfNull(
|
|
116
|
+
// Object.values(this.#stores).find((store) => store.name === storeName)
|
|
117
|
+
// );
|
|
118
|
+
if (!options.indexNames) {
|
|
119
|
+
throw new Error(`An instance of the store ${storeName} already exists. \
|
|
120
|
+
Enable automatic indexing via withDevTools('${storeName}', { indexNames: true }), or rename it upon instantiation.`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (let i = 1; names.includes(storeName); i++) {
|
|
125
|
+
storeName = `${name}-${i}`;
|
|
126
|
+
}
|
|
127
|
+
this.#stores[id] = { name: storeName, options };
|
|
128
|
+
|
|
129
|
+
const tracker = options.tracker;
|
|
130
|
+
if (!this.#trackers.includes(tracker)) {
|
|
131
|
+
this.#trackers.push(tracker);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
tracker.onChange((changedState) => this.syncToDevTools(changedState));
|
|
135
|
+
tracker.track(id, store);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
removeStore(id: string) {
|
|
139
|
+
const name = this.#stores[id].name;
|
|
140
|
+
this.#stores = Object.entries(this.#stores).reduce(
|
|
141
|
+
(newStore, [storeId, value]) => {
|
|
142
|
+
if (storeId !== id) {
|
|
143
|
+
newStore[storeId] = value;
|
|
144
|
+
}
|
|
145
|
+
return newStore;
|
|
146
|
+
},
|
|
147
|
+
{} as StoreRegistry,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
this.#currentState = Object.entries(this.#currentState).reduce(
|
|
151
|
+
(newState, [storeName, state]) => {
|
|
152
|
+
if (storeName !== name) {
|
|
153
|
+
newState[name] = state;
|
|
154
|
+
}
|
|
155
|
+
return newState;
|
|
156
|
+
},
|
|
157
|
+
{} as Record<string, object>,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
for (const tracker of this.#trackers) {
|
|
161
|
+
tracker.removeStore(id);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
renameStore(oldName: string, newName: string) {
|
|
166
|
+
const storeNames = Object.values(this.#stores).map((store) => store.name);
|
|
167
|
+
const id = throwIfNull(
|
|
168
|
+
Object.keys(this.#stores).find((id) => this.#stores[id].name === oldName),
|
|
169
|
+
);
|
|
170
|
+
if (storeNames.includes(newName)) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`NgRx Toolkit/DevTools: cannot rename from ${oldName} to ${newName}. ${newName} is already assigned to another SignalStore instance.`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
this.#stores = Object.entries(this.#stores).reduce(
|
|
177
|
+
(newStore, [id, value]) => {
|
|
178
|
+
if (value.name === oldName) {
|
|
179
|
+
newStore[id] = { ...value, name: newName };
|
|
180
|
+
} else {
|
|
181
|
+
newStore[id] = value;
|
|
182
|
+
}
|
|
183
|
+
return newStore;
|
|
184
|
+
},
|
|
185
|
+
{} as StoreRegistry,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// we don't rename in #currentState but wait for tracker to notify
|
|
189
|
+
// us with a changed state that contains that name.
|
|
190
|
+
this.#currentState = Object.entries(this.#currentState).reduce(
|
|
191
|
+
(newState, [storeName, state]) => {
|
|
192
|
+
if (storeName !== oldName) {
|
|
193
|
+
newState[storeName] = state;
|
|
194
|
+
}
|
|
195
|
+
return newState;
|
|
196
|
+
},
|
|
197
|
+
{} as Record<string, object>,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
this.#trackers.forEach((tracker) => tracker.notifyRenamedStore(id));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import { getState, StateSource, watchState } from '@ngrx/signals';
|
|
3
|
+
import { throwIfNull } from '../../shared/throw-if-null';
|
|
4
|
+
import { Tracker, TrackerStores } from './models';
|
|
5
|
+
|
|
6
|
+
type Stores = Record<
|
|
7
|
+
string,
|
|
8
|
+
{ destroyWatcher: () => void; store: StateSource<object> }
|
|
9
|
+
>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Internal Service used by {@link withGlitchTracking}. It does not rely
|
|
13
|
+
* on `effect` as {@link DefaultTracker} does but uses the NgRx function
|
|
14
|
+
* `watchState` to track all state changes.
|
|
15
|
+
*/
|
|
16
|
+
@Injectable({ providedIn: 'root' })
|
|
17
|
+
export class GlitchTrackerService implements Tracker {
|
|
18
|
+
#stores: Stores = {};
|
|
19
|
+
#callback: ((changedState: Record<string, object>) => void) | undefined;
|
|
20
|
+
|
|
21
|
+
get stores() {
|
|
22
|
+
return Object.entries(this.#stores).reduce((acc, [id, { store }]) => {
|
|
23
|
+
acc[id] = store;
|
|
24
|
+
return acc;
|
|
25
|
+
}, {} as TrackerStores);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
onChange(callback: (changedState: Record<string, object>) => void): void {
|
|
29
|
+
this.#callback = callback;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
removeStore(id: string): void {
|
|
33
|
+
this.#stores = Object.entries(this.#stores).reduce(
|
|
34
|
+
(newStore, [storeId, value]) => {
|
|
35
|
+
if (storeId !== id) {
|
|
36
|
+
newStore[storeId] = value;
|
|
37
|
+
} else {
|
|
38
|
+
value.destroyWatcher();
|
|
39
|
+
}
|
|
40
|
+
return newStore;
|
|
41
|
+
},
|
|
42
|
+
{} as Stores,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
throwIfNull(this.#callback)({});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
track(id: string, store: StateSource<object>): void {
|
|
49
|
+
const watcher = watchState(store, (state) => {
|
|
50
|
+
throwIfNull(this.#callback)({ [id]: state });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
this.#stores[id] = { destroyWatcher: watcher.destroy, store };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
notifyRenamedStore(id: string): void {
|
|
57
|
+
if (Object.keys(this.#stores).includes(id) && this.#callback) {
|
|
58
|
+
this.#callback({ [id]: getState(this.#stores[id].store) });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { StateSource } from '@ngrx/signals';
|
|
2
|
+
import { ReduxDevtoolsConfig } from '../provide-devtools-config';
|
|
3
|
+
import { DevtoolsInnerOptions } from './devtools-feature';
|
|
4
|
+
|
|
5
|
+
export type Action = { type: string };
|
|
6
|
+
export type Connection = {
|
|
7
|
+
send: (action: Action, state: Record<string, unknown>) => void;
|
|
8
|
+
};
|
|
9
|
+
export type ReduxDevtoolsExtension = {
|
|
10
|
+
connect: (options: ReduxDevtoolsConfig) => Connection;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type StoreRegistry = Record<
|
|
14
|
+
string,
|
|
15
|
+
{
|
|
16
|
+
options: DevtoolsInnerOptions;
|
|
17
|
+
name: string;
|
|
18
|
+
}
|
|
19
|
+
>;
|
|
20
|
+
|
|
21
|
+
export type Tracker = {
|
|
22
|
+
track(id: string, store: StateSource<object>): void;
|
|
23
|
+
onChange(callback: (changedState: Record<string, object>) => void): void;
|
|
24
|
+
notifyRenamedStore(id: string): void;
|
|
25
|
+
removeStore(id: string): void;
|
|
26
|
+
get stores(): TrackerStores;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type TrackerStores = Record<string, StateSource<object>>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { InjectionToken, ValueProvider } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Provides the configuration options for connecting to the Redux DevTools Extension.
|
|
5
|
+
*/
|
|
6
|
+
export function provideDevtoolsConfig(
|
|
7
|
+
config: ReduxDevtoolsConfig,
|
|
8
|
+
): ValueProvider {
|
|
9
|
+
return {
|
|
10
|
+
provide: REDUX_DEVTOOLS_CONFIG,
|
|
11
|
+
useValue: config,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Injection token for the configuration options for connecting to the Redux DevTools Extension.
|
|
17
|
+
*/
|
|
18
|
+
export const REDUX_DEVTOOLS_CONFIG = new InjectionToken<ReduxDevtoolsConfig>(
|
|
19
|
+
'ReduxDevtoolsConfig',
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Options for connecting to the Redux DevTools Extension.
|
|
24
|
+
* @example
|
|
25
|
+
* const devToolsOptions: ReduxDevtoolsConfig = {
|
|
26
|
+
* name: 'My App',
|
|
27
|
+
* };
|
|
28
|
+
*/
|
|
29
|
+
export type ReduxDevtoolsConfig = {
|
|
30
|
+
/** Optional name for the devtools instance. If empty, "NgRx SignalStore" will be used. */
|
|
31
|
+
name?: string;
|
|
32
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { StateSource } from '@ngrx/signals';
|
|
2
|
+
import { renameDevtoolsMethodName } from './with-devtools';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Renames the name of a store how it appears in the Devtools.
|
|
6
|
+
* @param store instance of the SignalStore
|
|
7
|
+
* @param newName new name for the Devtools
|
|
8
|
+
*/
|
|
9
|
+
export function renameDevtoolsName<State extends object>(
|
|
10
|
+
store: StateSource<State>,
|
|
11
|
+
newName: string,
|
|
12
|
+
): void {
|
|
13
|
+
const renameMethod = (store as Record<string, (newName: string) => void>)[
|
|
14
|
+
renameDevtoolsMethodName
|
|
15
|
+
];
|
|
16
|
+
if (!renameMethod) {
|
|
17
|
+
throw new Error("Devtools extensions haven't been added to this store.");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
renameMethod(newName);
|
|
21
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
import { signalStore, withMethods, withState } from '@ngrx/signals';
|
|
3
|
+
import { updateState } from '../update-state';
|
|
4
|
+
import { withDevtools } from '../with-devtools';
|
|
5
|
+
import { setupExtensions } from './helpers.spec';
|
|
6
|
+
|
|
7
|
+
describe('updateState', () => {
|
|
8
|
+
it('should show the name of the action', () => {
|
|
9
|
+
const { sendSpy } = setupExtensions();
|
|
10
|
+
TestBed.inject(
|
|
11
|
+
signalStore(
|
|
12
|
+
{ providedIn: 'root' },
|
|
13
|
+
withDevtools('shop'),
|
|
14
|
+
withState({ name: 'Car' }),
|
|
15
|
+
),
|
|
16
|
+
);
|
|
17
|
+
TestBed.flushEffects();
|
|
18
|
+
expect(sendSpy).toHaveBeenCalledWith(
|
|
19
|
+
{ type: 'Store Update' },
|
|
20
|
+
{ shop: { name: 'Car' } },
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should set the action name', () => {
|
|
25
|
+
const { sendSpy } = setupExtensions();
|
|
26
|
+
|
|
27
|
+
const Store = signalStore(
|
|
28
|
+
{ providedIn: 'root' },
|
|
29
|
+
withDevtools('shop'),
|
|
30
|
+
withState({ name: 'Car' }),
|
|
31
|
+
withMethods((store) => ({
|
|
32
|
+
setName(name: string) {
|
|
33
|
+
updateState(store, 'Set Name', { name });
|
|
34
|
+
},
|
|
35
|
+
})),
|
|
36
|
+
);
|
|
37
|
+
const store = TestBed.inject(Store);
|
|
38
|
+
TestBed.flushEffects();
|
|
39
|
+
|
|
40
|
+
store.setName('i4');
|
|
41
|
+
TestBed.flushEffects();
|
|
42
|
+
|
|
43
|
+
expect(sendSpy).toHaveBeenLastCalledWith(
|
|
44
|
+
{ type: 'Set Name' },
|
|
45
|
+
{ shop: { name: 'i4' } },
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
});
|