Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(devtools): extract tracking logic from DevtoolsSyncer #122

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 45 additions & 9 deletions apps/demo/e2e/devtools.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,50 @@ test('has title', async ({ page }) => {
const devtoolsActions = await page.evaluate(() => window['devtoolsSpy']);

expect(devtoolsActions).toEqual([
{ type: 'add todo' },
{ type: 'select todo 1' },
{ type: 'Store Update' },
{ type: 'select todo 4' },
{ type: 'Store Update' },
{ type: 'select todo 1' },
{ type: 'Store Update' },
{ type: 'select todo 4' },
{ type: 'Store Update' },
{
type: 'add todo',
},
{
type: 'select todo 1',
},
{
type: 'Store Update',
},
{
type: 'Store Update',
},
{
type: 'Store Update',
},
{
type: 'select todo 4',
},
{
type: 'Store Update',
},
{
type: 'Store Update',
},
{
type: 'Store Update',
},
{
type: 'select todo 1',
},
{
type: 'Store Update',
},
{
type: 'Store Update',
},
{
type: 'select todo 4',
},
{
type: 'Store Update',
},
{
type: 'Store Update',
},
]);
});
10 changes: 7 additions & 3 deletions apps/demo/src/app/devtools/todo-detail.component.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Component, effect, inject, input } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { Todo } from './todo-store';
import { signalStore, withState } from '@ngrx/signals';
import { patchState, signalStore, withHooks, withState } from '@ngrx/signals';
import {
renameDevtoolsName,
withDevtools,
withGlitchTracking,
withMapper,
} from '@angular-architects/ngrx-toolkit';

Expand All @@ -29,12 +30,15 @@ const TodoDetailStore = signalStore(

return acc;
}, {} as Record<string, unknown>);
})
}),
withGlitchTracking()
),
withState({
id: 1,
secret: 'do not show in DevTools',
})
active: false,
}),
withHooks((store) => ({ onInit: () => patchState(store, { active: true }) }))
);

@Component({
Expand Down
31 changes: 31 additions & 0 deletions docs/docs/with-devtools.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,37 @@ export class TodoDetailComponent {
}
```

## `withGlitchTracking()`

It tracks all state changes of the State, including intermediary updates
that are typically suppressed by Angular's glitch-free mechanism.

This feature is especially useful for debugging.

Example:

```typescript
const Store = signalStore(
{ providedIn: 'root' },
withState({ count: 0 }),
withDevtools('counter', withGlitchTracking()),
withMethods((store) => ({
increase: () => patchState(store, (value) => ({ count: value.count + 1 })),
}))
);

// would show up in the DevTools with value 0
const store = inject(Store);

store.increase(); // would show up in the DevTools with value 1
store.increase(); // would show up in the DevTools with value 2
store.increase(); // would show up in the DevTools with value 3
```

Without `withGlitchTracking`, the DevTools would only show the final value of 3.

It is also possible to mix. So one store could have `withGlitchTracking` and another one not.

## `withDisabledNameIndices()`

`withDevtools` foresees the possibility to add features which extend or modify it. At the moment, `withDisabledNameIndices` is the only feature available. It disables the automatic indexing of the store names in the Devtools.
Expand Down
5 changes: 3 additions & 2 deletions libs/ngrx-toolkit/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
export { withDevToolsStub } from './lib/devtools/with-dev-tools-stub';
export { withDevtools } from './lib/devtools/with-devtools';
export { withDisabledNameIndices } from './lib/devtools/with-disabled-name-indicies';
export { withMapper } from './lib/devtools/with-mapper';
export { withDisabledNameIndices } from './lib/devtools/features/with-disabled-name-indicies';
export { withMapper } from './lib/devtools/features/with-mapper';
export { withGlitchTracking } from './lib/devtools/features/with-glitch-tracking';
export { patchState, updateState } from './lib/devtools/update-state';
export { renameDevtoolsName } from './lib/devtools/rename-devtools-name';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createDevtoolsFeature } from './devtools-feature';
import { createDevtoolsFeature } from '../internal/devtools-feature';

/**
* If multiple instances of the same SignalStore class
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { createDevtoolsFeature } from '../internal/devtools-feature';
import { GlitchTrackerService } from '../internal/glitch-tracker.service';

/**
* It tracks all state changes of the State, including intermediary updates
* that are typically suppressed by Angular's glitch-free mechanism.
*
* This feature is especially useful for debugging.
*
* Example:
*
* <pre>
* const Store = signalStore(
* { providedIn: 'root' },
* withState({ count: 0 }),
* withDevtools('counter', withGlitchTracking()),
* withMethods((store) => ({
* increase: () =>
* patchState(store, (value) => ({ count: value.count + 1 })),
* }))
* );
*
* // would show up in the DevTools with value 0
* const store = inject(Store);
*
* store.increase(); // would show up in the DevTools with value 1
* store.increase(); // would show up in the DevTools with value 2
* store.increase(); // would show up in the DevTools with value 3
* </pre>
*
* Without `withGlitchTracking`, the DevTools would only show the final value of 3.
*/
export function withGlitchTracking() {
return createDevtoolsFeature({ tracker: GlitchTrackerService });
}
34 changes: 34 additions & 0 deletions libs/ngrx-toolkit/src/lib/devtools/features/with-mapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { createDevtoolsFeature, Mapper } from '../internal/devtools-feature';

/**
* Allows you to define a function to map the state.
*
* It is needed for huge states, that slows down the Devtools and where
* you don't need to see the whole state or other reasons.
*
* Example:
*
* <pre>
* const initialState = {
* id: 1,
* email: 'john.list@host.com',
* name: 'John List',
* enteredPassword: ''
* }
*
* const Store = signalStore(
* withState(initialState),
* withDevtools(
* 'user',
* withMapper(state => ({state, { enteredPassword: '***' }}))
* )
* )
* </pre>
*
* @param map function which maps the state
*/
export function withMapper<State extends object>(
map: (state: State) => Record<string, unknown>
) {
return createDevtoolsFeature({ map: map as Mapper });
}
57 changes: 57 additions & 0 deletions libs/ngrx-toolkit/src/lib/devtools/internal/default-tracker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { effect, Injectable, signal } from '@angular/core';
import { getState, StateSource } from '@ngrx/signals';
import { Tracker, TrackerStores } from './models';

@Injectable({ providedIn: 'root' })
export class DefaultTracker implements Tracker {
readonly #stores = signal<TrackerStores>({});

get stores(): TrackerStores {
return this.#stores();
}

#trackCallback: undefined | ((changedState: Record<string, object>) => void);

#trackingEffect = effect(() => {
if (this.#trackCallback === undefined) {
throw new Error('no callback function defined');
}
const stores = this.#stores();

const fullState = Object.entries(stores).reduce((acc, [id, store]) => {
return { ...acc, [id]: getState(store) };
}, {} as Record<string, object>);

this.#trackCallback(fullState);
});

track(id: string, store: StateSource<object>): void {
this.#stores.update((value) => ({
...value,
[id]: store,
}));
}

onChange(callback: (changedState: Record<string, object>) => void): void {
this.#trackCallback = callback;
}

removeStore(id: string) {
this.#stores.update((stores) =>
Object.entries(stores).reduce((newStore, [storeId, state]) => {
if (storeId !== id) {
newStore[storeId] = state;
}
return newStore;
}, {} as TrackerStores)
);
}

notifyRenamedStore(id: string): void {
if (this.#stores()[id]) {
this.#stores.update((stores) => {
return { ...stores };
});
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { Tracker } from './models';

export const DEVTOOLS_FEATURE = Symbol('DEVTOOLS_FEATURE');

export type Mapper = (state: object) => object;

export type DevtoolsOptions = {
indexNames: boolean; // defines if names should be indexed.
map: Mapper; // defines a mapper for the state.
indexNames?: boolean; // defines if names should be indexed.
map?: Mapper; // defines a mapper for the state.
tracker?: new () => Tracker; // defines a tracker for the state
};

export type DevtoolsInnerOptions = {
indexNames: boolean;
map: Mapper;
tracker: Tracker;
};

/**
Expand All @@ -19,7 +28,7 @@ export type DevtoolsFeature = {
} & Partial<DevtoolsOptions>;

export function createDevtoolsFeature(
options: Partial<DevtoolsOptions>
options: DevtoolsOptions
): DevtoolsFeature {
return {
[DEVTOOLS_FEATURE]: true,
Expand Down
Loading