Skip to content

Commit e468a07

Browse files
fix: issuses in renameDevtoolsName #213
1 parent c36db3c commit e468a07

File tree

4 files changed

+67
-20
lines changed

4 files changed

+67
-20
lines changed

apps/demo/src/app/devtools/todo-store.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,14 @@ export const TodoStore = signalStore(
3030
},
3131

3232
remove(id: number) {
33-
updateState(store, 'remove todo', removeEntity(id));
33+
updateState(
34+
store,
35+
'remove todo',
36+
removeEntity(id),
37+
({ selectedIds }) => ({
38+
selectedIds: selectedIds.filter((selectedId) => selectedId !== id),
39+
}),
40+
);
3441
},
3542

3643
toggleFinished(id: number): void {

libs/ngrx-toolkit/src/lib/devtools/internal/devtools-syncer.service.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { isPlatformBrowser } from '@angular/common';
22
import { inject, Injectable, OnDestroy, PLATFORM_ID } from '@angular/core';
33
import { StateSource } from '@ngrx/signals';
4-
import { throwIfNull } from '../../shared/throw-if-null';
54
import { REDUX_DEVTOOLS_CONFIG } from '../provide-devtools-config';
65
import { currentActionNames } from './current-action-names';
76
import { DevtoolsInnerOptions } from './devtools-feature';
@@ -150,7 +149,7 @@ Enable automatic indexing via withDevTools('${storeName}', { indexNames: true })
150149
this.#currentState = Object.entries(this.#currentState).reduce(
151150
(newState, [storeName, state]) => {
152151
if (storeName !== name) {
153-
newState[name] = state;
152+
newState[storeName] = state;
154153
}
155154
return newState;
156155
},
@@ -162,23 +161,36 @@ Enable automatic indexing via withDevTools('${storeName}', { indexNames: true })
162161
}
163162
}
164163

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)) {
164+
/**
165+
* Renames a store identified by its internal id. If the store has already
166+
* been removed (e.g. due to component destruction), this is a no-op.
167+
*/
168+
renameStore(id: string, newName: string) {
169+
const storeEntry = this.#stores[id];
170+
if (!storeEntry) {
171+
return;
172+
}
173+
const oldName = storeEntry.name;
174+
175+
if (oldName === newName) {
176+
return;
177+
}
178+
179+
const otherStoreNames = Object.entries(this.#stores)
180+
.filter(([entryId]) => entryId !== id)
181+
.map(([, s]) => s.name);
182+
if (otherStoreNames.includes(newName)) {
171183
throw new Error(
172184
`NgRx Toolkit/DevTools: cannot rename from ${oldName} to ${newName}. ${newName} is already assigned to another SignalStore instance.`,
173185
);
174186
}
175187

176188
this.#stores = Object.entries(this.#stores).reduce(
177-
(newStore, [id, value]) => {
178-
if (value.name === oldName) {
179-
newStore[id] = { ...value, name: newName };
189+
(newStore, [entryId, value]) => {
190+
if (entryId === id) {
191+
newStore[entryId] = { ...value, name: newName };
180192
} else {
181-
newStore[id] = value;
193+
newStore[entryId] = value;
182194
}
183195
return newStore;
184196
},

libs/ngrx-toolkit/src/lib/devtools/tests/naming.spec.ts

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ describe('withDevtools / renaming', () => {
2828
TestBed.inject(Store);
2929
runInInjectionContext(childContext, () => inject(Store));
3030

31-
TestBed.flushEffects();
31+
TestBed.tick();
3232

3333
expect(sendSpy).toHaveBeenLastCalledWith(
3434
{ type: 'Store Update' },
@@ -51,7 +51,7 @@ describe('withDevtools / renaming', () => {
5151
const childContext2 = createEnvironmentInjector([Store], envInjector);
5252

5353
runInInjectionContext(childContext1, () => inject(Store));
54-
TestBed.flushEffects();
54+
TestBed.tick();
5555
childContext1.destroy();
5656

5757
expect(sendSpy.mock.calls).toEqual([
@@ -64,7 +64,7 @@ describe('withDevtools / renaming', () => {
6464
]);
6565

6666
runInInjectionContext(childContext2, () => inject(Store));
67-
TestBed.flushEffects();
67+
TestBed.tick();
6868
expect(sendSpy.mock.calls).toEqual([
6969
[
7070
{ type: 'Store Update' },
@@ -113,7 +113,7 @@ Enable automatic indexing via withDevTools('flights', { indexNames: true }), or
113113
signalStore({ providedIn: 'root' }, withDevtools('flights')),
114114
);
115115

116-
TestBed.flushEffects();
116+
TestBed.tick();
117117
expect(sendSpy.mock.calls).toEqual([
118118
[
119119
{ type: 'Store Update' },
@@ -169,7 +169,7 @@ Enable automatic indexing via withDevTools('flights', { indexNames: true }), or
169169

170170
const store = TestBed.inject(Store);
171171
renameDevtoolsName(store, 'flights');
172-
TestBed.flushEffects();
172+
TestBed.tick();
173173

174174
expect(sendSpy).toHaveBeenCalledWith(
175175
{ type: 'Store Update' },
@@ -192,7 +192,7 @@ Enable automatic indexing via withDevTools('flights', { indexNames: true }), or
192192
);
193193
TestBed.inject(Store1);
194194
const store = TestBed.inject(Store2);
195-
TestBed.flushEffects();
195+
TestBed.tick();
196196

197197
expect(() => renameDevtoolsName(store, 'shop')).toThrow(
198198
'NgRx Toolkit/DevTools: cannot rename from mall to shop. shop is already assigned to another SignalStore instance.',
@@ -212,5 +212,33 @@ Enable automatic indexing via withDevTools('flights', { indexNames: true }), or
212212
"Devtools extensions haven't been added to this store.",
213213
);
214214
});
215+
216+
it('should ignore rename after the store has been destroyed', () => {
217+
const { sendSpy } = setupExtensions();
218+
219+
const Store = signalStore(
220+
withDevtools('flight'),
221+
withState({ name: 'Product', price: 10.5 }),
222+
);
223+
224+
const childContext = createEnvironmentInjector(
225+
[Store],
226+
TestBed.inject(EnvironmentInjector),
227+
);
228+
229+
const store = childContext.get(Store);
230+
TestBed.tick();
231+
232+
expect(sendSpy).toHaveBeenCalledWith(
233+
{ type: 'Store Update' },
234+
{ flight: { name: 'Product', price: 10.5 } },
235+
);
236+
237+
childContext.destroy();
238+
TestBed.tick();
239+
240+
// Previously this could throw; now it is a no-op
241+
expect(() => renameDevtoolsName(store, 'flights')).not.toThrow();
242+
});
215243
});
216244
});

libs/ngrx-toolkit/src/lib/devtools/with-devtools.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function withDevtools(name: string, ...features: DevtoolsFeature[]) {
5151
// TODO: use withProps and symbols
5252
return {
5353
[renameDevtoolsMethodName]: (newName: string) => {
54-
syncer.renameStore(name, newName);
54+
syncer.renameStore(id, newName);
5555
},
5656
[uniqueDevtoolsId]: () => id,
5757
} as Record<string, (newName?: unknown) => unknown>;

0 commit comments

Comments
 (0)