Skip to content

Commit

Permalink
chore: devtools connect api
Browse files Browse the repository at this point in the history
  • Loading branch information
mikezks committed Dec 18, 2023
1 parent fd95628 commit 5b91817
Show file tree
Hide file tree
Showing 9 changed files with 209 additions and 122 deletions.
2 changes: 1 addition & 1 deletion libs/ngrx-toolkit/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * from './lib/with-devtools';
export * from './lib/redux-devtools';
26 changes: 26 additions & 0 deletions libs/ngrx-toolkit/src/lib/redux-devtools/devtools-connect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { getConnection, getRootState, initDevtools, setUntrackedStore, updateStoreRegistry } from "./devtools-core";
import { Action } from "./model";
import { getStoreSignal } from "./util";


/**
* Devtools Public API: Add Store, Dispatch Action
*/
export function addStoreToReduxDevtools(store: unknown, name: string, live = true): boolean {
if (!initDevtools()) {
return false;
}

!live && setUntrackedStore(name);
const storeSignal = getStoreSignal(store);
updateStoreRegistry((value) => ({
...value,
[name]: storeSignal
}));

return true;
}

export function dispatchActionToReduxDevtools(action: Action): void {
getConnection()?.send(action, getRootState());
}
108 changes: 108 additions & 0 deletions libs/ngrx-toolkit/src/lib/redux-devtools/devtools-core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { isPlatformServer } from '@angular/common';
import { PLATFORM_ID, Signal, effect, inject, signal, untracked } from '@angular/core';
import { ConnectResponse } from './model';


/**
* `storeRegistry` holds
*/


/**
* Local State
*/
let connection: ConnectResponse | undefined;

const storeRegistry = signal<Record<string, Signal<unknown>>>({});
const untrackedStores: Record<string, boolean> = {};
let currentActionNames = new Set<string>();
let synchronizationInitialized = false;

const DEFAULT_ACTION_NAME = 'Store Update';
const DEFAULT_STORE_NAME = 'NgRx Signal Store';

/**
* Devtools Core Getter, Setter
*/
export function addActionName(action: string): void {
currentActionNames.add(action);
}

export function getConnection(): ConnectResponse | undefined {
return connection;
}

export function updateStoreRegistry(
updateFn: (value: Record<string, Signal<unknown>>) => Record<string, Signal<unknown>>
): void {
storeRegistry.update(updateFn);
}

export function setUntrackedStore(name: string) {
untrackedStores[name] = true;
}

export function getRootState(): Record<string, unknown> {
const stores = storeRegistry();
const rootState: Record<string, unknown> = {};
for (const name in stores) {
const store = stores[name];
rootState[name] = untrackedStores[name]
? untracked(() => store())
: store();
}

return rootState;
}

/**
* Devtools Core Internals
*/
function initConnection(): void {
if (!connection) {
connection = window.__REDUX_DEVTOOLS_EXTENSION__.connect({
name: DEFAULT_STORE_NAME,
});
}
}

function initSynchronization(): void {
if (!synchronizationInitialized) {
effect(() => {
if (!connection) {
return;
}

const names = Array.from(currentActionNames);
const type = names.length ? names.join(', ') : DEFAULT_ACTION_NAME;
currentActionNames = new Set<string>();

connection.send({ type }, getRootState());
});

synchronizationInitialized = true;
}
}

function isServerOrDevtoolsMissing(): boolean {
const isServer = isPlatformServer(inject(PLATFORM_ID));
if (isServer || !window.__REDUX_DEVTOOLS_EXTENSION__) {
return true;
}

return false;
}

/**
* Devtools Core API
*/
export function initDevtools(): boolean {
if (isServerOrDevtoolsMissing()) {
return false;
}

initConnection();
initSynchronization();

return true;
}
3 changes: 3 additions & 0 deletions libs/ngrx-toolkit/src/lib/redux-devtools/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

export * from './devtools-connect';
export * from './with-devtools';
27 changes: 27 additions & 0 deletions libs/ngrx-toolkit/src/lib/redux-devtools/model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { patchState as originalPatchState } from '@ngrx/signals';


// eslint-disable-next-line @typescript-eslint/ban-types
export type EmptyFeatureResult = { state: {}; signals: {}; methods: {} };
export type Action = { type: string };

export type ConnectResponse = {
send: (action: Action, state: unknown) => void;
};

export type PatchFn = typeof originalPatchState extends (
arg1: infer First,
...args: infer Rest
) => infer Returner
? (state: First, action: string, ...rest: Rest) => Returner
: never;

declare global {
interface Window {
__REDUX_DEVTOOLS_EXTENSION__: {
connect: (options: { name: string }) => {
send: (action: Action) => void;
};
};
}
}
18 changes: 18 additions & 0 deletions libs/ngrx-toolkit/src/lib/redux-devtools/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Signal } from "@angular/core";


function getValueFromSymbol(obj: unknown, symbol: symbol) {
if (typeof obj === 'object' && obj && symbol in obj) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (obj as { [key: symbol]: any })[symbol];
}
}

export function getStoreSignal(store: unknown): Signal<unknown> {
const [signalStateKey] = Object.getOwnPropertySymbols(store);
if (!signalStateKey) {
throw new Error('Cannot find State Signal');
}

return getValueFromSymbol(store, signalStateKey);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type Flight = {

describe('Devtools', () => {
it('should not fail if no Redux Devtools are available', () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const Flights = signalStore(withEntities<Flight>());
});
it.todo('add a state');
Expand Down
25 changes: 25 additions & 0 deletions libs/ngrx-toolkit/src/lib/redux-devtools/with-devtools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { SignalStoreFeature, patchState as originalPatchState } from '@ngrx/signals';
import { SignalStoreFeatureResult } from '@ngrx/signals/src/signal-store-models';
import { addStoreToReduxDevtools } from './devtools-connect';
import { addActionName } from './devtools-core';
import { EmptyFeatureResult, PatchFn } from './model';


/**
* Devtools Public API: Custom Feature, Patch State
* @param name store's name as it should appear in the DevTools
*/
export function withDevtools<Input extends SignalStoreFeatureResult>(
name: string
): SignalStoreFeature<Input, EmptyFeatureResult> {
return (store) => {
addStoreToReduxDevtools(store, name);

return store;
};
}

export const patchState: PatchFn = (state, action, ...rest) => {
addActionName(action);
return originalPatchState(state, ...rest);
};
121 changes: 0 additions & 121 deletions libs/ngrx-toolkit/src/lib/with-devtools.ts

This file was deleted.

0 comments on commit 5b91817

Please sign in to comment.