Skip to content

Commit 83f6b12

Browse files
committed
feat(devtools): add branded Action type and asAction helper
1 parent 83cb10c commit 83f6b12

5 files changed

Lines changed: 46 additions & 9 deletions

File tree

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,17 @@ const dummyConnection: Connection = {
1818

1919
function toDevtoolsAction(actions: (string | Action)[]): Action {
2020
if (!actions.length) {
21-
return { type: 'Store Update' };
21+
return { type: 'Store Update' } as Action;
2222
}
2323

2424
const objects = actions.filter((a): a is Action => typeof a === 'object');
2525
const type = [
2626
...new Set(actions.map((a) => (typeof a === 'string' ? a : a.type))),
2727
].join(', ');
2828

29-
return objects.length ? { ...objects[0], type } : { type };
29+
return objects.length
30+
? ({ ...objects[0], type } as Action)
31+
: ({ type } as Action);
3032
}
3133

3234
/**

libs/ngrx-toolkit/src/lib/devtools/internal/models.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { StateSource } from '@ngrx/signals';
22
import { ReduxDevtoolsConfig } from '../provide-devtools-config';
33
import { DevtoolsInnerOptions } from './devtools-feature';
44

5-
export type Action = { type: string; [key: string]: unknown };
5+
declare const __actionBrand: unique symbol;
6+
export type Action = { type: string; [key: string]: unknown } & {
7+
readonly [__actionBrand]: true;
8+
};
69
export type Connection = {
710
send: (action: Action, state: Record<string, unknown>) => void;
811
};

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { TestBed } from '@angular/core/testing';
22
import { signalStore, withMethods, withState } from '@ngrx/signals';
3-
import { updateState } from '../update-state';
3+
import { asAction, updateState } from '../update-state';
44
import { withDevtools } from '../with-devtools';
55
import { setupExtensions } from './helpers.spec';
66

@@ -45,4 +45,29 @@ describe('updateState', () => {
4545
{ shop: { name: 'i4' } },
4646
);
4747
});
48+
49+
it('should set and send an action object', () => {
50+
const { sendSpy } = setupExtensions();
51+
52+
const Store = signalStore(
53+
{ providedIn: 'root' },
54+
withDevtools('shop'),
55+
withState({ name: 'Car' }),
56+
withMethods((store) => ({
57+
setName(name: string) {
58+
updateState(store, asAction({ type: 'Set Name', name }), { name });
59+
},
60+
})),
61+
);
62+
const store = TestBed.inject(Store);
63+
TestBed.flushEffects();
64+
65+
store.setName('i4');
66+
TestBed.flushEffects();
67+
68+
expect(sendSpy).toHaveBeenLastCalledWith(
69+
{ type: 'Set Name', name: 'i4' },
70+
{ shop: { name: 'i4' } },
71+
);
72+
});
4873
});

libs/ngrx-toolkit/src/lib/devtools/update-state.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
WritableStateSource,
55
} from '@ngrx/signals';
66
import { currentActionNames } from './internal/current-action-names';
7+
import { Action } from './internal/models';
78

89
type PatchFn = typeof originalPatchState extends (
910
arg1: infer First,
@@ -19,6 +20,14 @@ export const patchState: PatchFn = (state, action, ...rest) => {
1920
updateState(state, action, ...rest);
2021
};
2122

23+
/**
24+
* Casts an object with a `type` property to an {@link Action}.
25+
* Use this when you need to pass a structured action object to {@link updateState}.
26+
*/
27+
export function asAction<T extends { type: string }>(value: T): Action {
28+
return value as unknown as Action;
29+
}
30+
2231
/**
2332
* Wrapper of `patchState` for DevTools integration. Next to updating the state,
2433
* it also sends the action to the DevTools.
@@ -28,7 +37,7 @@ export const patchState: PatchFn = (state, action, ...rest) => {
2837
*/
2938
export function updateState<State extends object>(
3039
stateSource: WritableStateSource<State>,
31-
action: string,
40+
action: string | Action,
3241
...updaters: Array<
3342
Partial<NoInfer<State>> | PartialStateUpdater<NoInfer<State>>
3443
>

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
EmptyFeatureResult,
44
getState,
55
PartialStateUpdater,
6-
patchState,
76
SignalStoreFeature,
87
signalStoreFeature,
98
type,
@@ -16,7 +15,7 @@ import {
1615
} from '@ngrx/signals/events';
1716
import { tap } from 'rxjs/operators';
1817
import { GLITCH_TRACKING_FEATURE } from './features/with-glitch-tracking';
19-
import { currentActionNames } from './internal/current-action-names';
18+
import { updateState, asAction } from './update-state';
2019
import { DEVTOOL_FEATURE_NAMES } from './with-devtools';
2120

2221
export function withTrackedReducer<State extends object>(
@@ -40,8 +39,7 @@ export function withTrackedReducer<State extends object>(
4039
const result = caseReducer.reducer(event, state);
4140
const updaters = Array.isArray(result) ? result : [result];
4241

43-
currentActionNames.add(event);
44-
patchState(store, ...updaters);
42+
updateState(store, asAction(event), ...updaters);
4543
}),
4644
),
4745
),

0 commit comments

Comments
 (0)