diff --git a/docs/src/content/docs/package/devtools.md b/docs/src/content/docs/package/devtools.md index 0058e511..1453cdf5 100644 --- a/docs/src/content/docs/package/devtools.md +++ b/docs/src/content/docs/package/devtools.md @@ -7,7 +7,7 @@ description: Reatom developer tools for states and actions inspecting -Developer tools for states and actions inspecting +This devtools is NOT coupled to Reatom and may be worked as a general logger for any kind of data. For Reatom users it has some additional features like actions and states cause tracing. But by default it has a huge list of features to log, highlight, filter and inspect any kind of logs. ## Installation @@ -17,29 +17,101 @@ npm install @reatom/devtools **Note** that this package is published with ESM bundle only. -## Usage +## Setup -You typical setup would look like that. +Typical setup in your root file may look like that. ```ts -import { createCtx, connectLogger } from '@reatom/framework' -import { connectDevtools } from '@reatom/devtools' +declare global { + var DEVTOOLS: null | Devtools +} +if (import.meta.env.DEV) { + const { createDevtools } = await import('@reatom/devtools') + globalThis.DEVTOOLS = createDevtools() +} else { + globalThis.DEVTOOLS = null +} +``` -const ctx = createCtx() +It is recommended to put the devtools to the global scope to log and inspect needed data easily during development or debugging. -if (import.meta.env.DEV) { - connectLogger(ctx) - connectDevtools(ctx) +**IMPORTANT NOTE** **for Reatom users** - put your `ctx` as an option to the create function (`createDevtools({ ctx })`). + +## Devtools API + +`createDevtools` returns an object with a few methods: + +- `log(name: string, payload?: any)` - log any kind of data +- `state(name: string, initState: T): DevtoolsState` - log and create a state with a lifetime, which you can update directly later and subscribe to changes from the devtools update form +- `show()` - show the devtools (needed for `initVisibility: false` option) +- `hide()` - hide the devtools the opposite to `show` method :) + +With `declare global { var DEVTOOLS: undefined | Devtools }` you will be able to use this methods like so: + +```ts +function doSome(payload) { + DEVTOOLS?.log('doSome', payload) + + // the rest of the code... } +``` + +You can log states too (only changes will be logged) -// ... +```ts +const [state, setState] = useState() +DEVTOOLS?.state('my-state', state) ``` -`connectDevtools` has an optional second argument where you can specify how to process an atoms names. +With "state" log you can reinit your form from the devtools with the edit tool (see [UI features](#ui-features) below). ```ts -type Options = { - separator?: string | RegExp | ((name: string) => Array) // default is /\.|#/ - privatePrefix?: string // default is '_' -} +const { values, init } = useForm() +React.useEffect(() => DEVTOOLS?.state('my-form', values).subscribe(init), [state]) ``` + +## createDevtools Options + +The create function accepts the following options: + +- `initSize?: number` - initial value of the size of collected logs +- `initVisibility?: boolean` - the predicate to mount the devtools to the DOM, devtools will collect all logs nevertheless of the state, it affects only the UI +- `getColor?: typeof getColor` - the function to get the color of the log, the default function available by an import with the same name +- `separator?: string | RegExp | ((name: string) => Array)` - the separator to split the name of the log for "state view", `.` by default +- `privatePrefix?: string` - the prefix which mark the log as private, `_` by default + +## UI features + +### Logs view + +![Devtools logs view](devtools1.png) + +1. **Filter form** - input RegExp will apply to the name of the log immediately, you can choose the type of the match mechanic: equal, not equal, highlight (color picker), off. By pressing "save" button the filter options will be saved in the filters group below. +2. **Filters group** - the list of saved filters, which you can change, also additional match mechanic "garbage" can be added, which will throw input logs forever, which is useful for better performance of many logs. + > "private" unremovable filter applies to private logs, which defines by `privatePrefix` option. +3. **Values search** allows you to filter the logs by the stringified payload matching. +4. **Actions** allows you to modify the behavior of the devtools: + - "clear lines" clears causes chain of transactional logs + - "clear logs" removes all collected logs + - "size" sets the amount of logs to be collected + - "inline preview" toggles the preview under log name + - "hover preview" toggles the preview by document icon before a log name hover + - "timestamps" toggles the timestamps lines of transactions (you need to restart the page to see the changes) +5. **Log name** row starts with chain highlight button, then payload preview button (could be hovered with the options or clicked to hold it), then log name. +6. **Log payload** row includes the payload of the log, if the relative option is enabled. +7. **Payload actions** (see description below) + +### Payload inspector view + +![Devtools payload inspector view](devtools2.png) + +In the screenshot above we turn off the inline preview, clicked at the chain button of "issues.dataAtom" log, which paint the cause chain to other relative logs, and clicked on the document button of "issues.dataAtom" log to see the payload inspector view. + +1. Name of the log (atom) and its payload (state). +2. List of the changes of this log. +3. **Payload action** buttons: + - **Convert to plain JSON** (useful for [atomized](/recipes/atomization/) state inspection) + - **Log** to the browser console + - **Copy** JSON to the clipboard + - **Open edit form** (will create additional log if will be applied) + - **Close** the inspector diff --git a/docs/src/content/docs/package/devtools1.png b/docs/src/content/docs/package/devtools1.png new file mode 100644 index 00000000..50d17733 Binary files /dev/null and b/docs/src/content/docs/package/devtools1.png differ diff --git a/docs/src/content/docs/package/devtools2.png b/docs/src/content/docs/package/devtools2.png new file mode 100644 index 00000000..307ae124 Binary files /dev/null and b/docs/src/content/docs/package/devtools2.png differ diff --git a/examples/react-search/src/main.tsx b/examples/react-search/src/main.tsx index 7c0644d7..94a5dc58 100644 --- a/examples/react-search/src/main.tsx +++ b/examples/react-search/src/main.tsx @@ -1,13 +1,20 @@ import ReactDOM from 'react-dom/client' -import { createCtx, connectLogger } from '@reatom/framework' -import { connectDevtools } from '@reatom/devtools' +import { createCtx } from '@reatom/framework' +import { type Devtools } from '@reatom/devtools' import { reatomContext } from '@reatom/npm-react' import { App } from './app' const ctx = createCtx() -connectLogger(ctx) -connectDevtools(ctx) +declare global { + var DEVTOOLS: undefined | Devtools +} +if (import.meta.env.DEV) { + const { createDevtools } = await import('@reatom/devtools') + globalThis.DEVTOOLS = createDevtools({ ctx, initVisibility: true }) +} else { + globalThis.DEVTOOLS = undefined +} const root = ReactDOM.createRoot(document.getElementById('root')!) root.render( diff --git a/packages/devtools/README.md b/packages/devtools/README.md index 0aad135b..4aa966c0 100644 --- a/packages/devtools/README.md +++ b/packages/devtools/README.md @@ -1,4 +1,4 @@ -Developer tools for states and actions inspecting +This devtools is NOT coupled to Reatom and may be worked as a general logger for any kind of data. For Reatom users it has some additional features like actions and states cause tracing. But by default it has a huge list of features to log, highlight, filter and inspect any kind of logs. ## Installation @@ -8,29 +8,101 @@ npm install @reatom/devtools **Note** that this package is published with ESM bundle only. -## Usage +## Setup -You typical setup would look like that. +Typical setup in your root file may look like that. ```ts -import { createCtx, connectLogger } from '@reatom/framework' -import { connectDevtools } from '@reatom/devtools' +declare global { + var DEVTOOLS: null | Devtools +} +if (import.meta.env.DEV) { + const { createDevtools } = await import('@reatom/devtools') + globalThis.DEVTOOLS = createDevtools() +} else { + globalThis.DEVTOOLS = null +} +``` -const ctx = createCtx() +It is recommended to put the devtools to the global scope to log and inspect needed data easily during development or debugging. -if (import.meta.env.DEV) { - connectLogger(ctx) - connectDevtools(ctx) +**IMPORTANT NOTE** **for Reatom users** - put your `ctx` as an option to the create function (`createDevtools({ ctx })`). + +## Devtools API + +`createDevtools` returns an object with a few methods: + +- `log(name: string, payload?: any)` - log any kind of data +- `state(name: string, initState: T): DevtoolsState` - log and create a state with a lifetime, which you can update directly later and subscribe to changes from the devtools update form +- `show()` - show the devtools (needed for `initVisibility: false` option) +- `hide()` - hide the devtools the opposite to `show` method :) + +With `declare global { var DEVTOOLS: undefined | Devtools }` you will be able to use this methods like so: + +```ts +function doSome(payload) { + DEVTOOLS?.log('doSome', payload) + + // the rest of the code... } +``` + +You can log states too (only changes will be logged) -// ... +```ts +const [state, setState] = useState() +DEVTOOLS?.state('my-state', state) ``` -`connectDevtools` has an optional second argument where you can specify how to process an atoms names. +With "state" log you can reinit your form from the devtools with the edit tool (see [UI features](#ui-features) below). ```ts -type Options = { - separator?: string | RegExp | ((name: string) => Array) // default is /\.|#/ - privatePrefix?: string // default is '_' -} +const { values, init } = useForm() +React.useEffect(() => DEVTOOLS?.state('my-form', values).subscribe(init), [state]) ``` + +## createDevtools Options + +The create function accepts the following options: + +- `initSize?: number` - initial value of the size of collected logs +- `initVisibility?: boolean` - the predicate to mount the devtools to the DOM, devtools will collect all logs nevertheless of the state, it affects only the UI +- `getColor?: typeof getColor` - the function to get the color of the log, the default function available by an import with the same name +- `separator?: string | RegExp | ((name: string) => Array)` - the separator to split the name of the log for "state view", `.` by default +- `privatePrefix?: string` - the prefix which mark the log as private, `_` by default + +## UI features + +### Logs view + +![Devtools logs view](devtools1.png) + +1. **Filter form** - input RegExp will apply to the name of the log immediately, you can choose the type of the match mechanic: equal, not equal, highlight (color picker), off. By pressing "save" button the filter options will be saved in the filters group below. +2. **Filters group** - the list of saved filters, which you can change, also additional match mechanic "garbage" can be added, which will throw input logs forever, which is useful for better performance of many logs. + > "private" unremovable filter applies to private logs, which defines by `privatePrefix` option. +3. **Values search** allows you to filter the logs by the stringified payload matching. +4. **Actions** allows you to modify the behavior of the devtools: + - "clear lines" clears causes chain of transactional logs + - "clear logs" removes all collected logs + - "size" sets the amount of logs to be collected + - "inline preview" toggles the preview under log name + - "hover preview" toggles the preview by document icon before a log name hover + - "timestamps" toggles the timestamps lines of transactions (you need to restart the page to see the changes) +5. **Log name** row starts with chain highlight button, then payload preview button (could be hovered with the options or clicked to hold it), then log name. +6. **Log payload** row includes the payload of the log, if the relative option is enabled. +7. **Payload actions** (see description below) + +### Payload inspector view + +![Devtools payload inspector view](devtools2.png) + +In the screenshot above we turn off the inline preview, clicked at the chain button of "issues.dataAtom" log, which paint the cause chain to other relative logs, and clicked on the document button of "issues.dataAtom" log to see the payload inspector view. + +1. Name of the log (atom) and its payload (state). +2. List of the changes of this log. +3. **Payload action** buttons: + - **Convert to plain JSON** (useful for [atomized](https://www.reatom.dev/recipes/atomization/) state inspection) + - **Log** to the browser console + - **Copy** JSON to the clipboard + - **Open edit form** (will create additional log if will be applied) + - **Close** the inspector diff --git a/packages/devtools/devtools1.png b/packages/devtools/devtools1.png new file mode 100644 index 00000000..50d17733 Binary files /dev/null and b/packages/devtools/devtools1.png differ diff --git a/packages/devtools/devtools2.png b/packages/devtools/devtools2.png new file mode 100644 index 00000000..307ae124 Binary files /dev/null and b/packages/devtools/devtools2.png differ diff --git a/packages/devtools/package.json b/packages/devtools/package.json index 9ee6d316..f6fc00ec 100644 --- a/packages/devtools/package.json +++ b/packages/devtools/package.json @@ -50,7 +50,9 @@ ], "files": [ "/build", - "/package.json" + "/package.json", + "/devtools1.png", + "/devtools2.png" ], "devDependencies": { "@observablehq/inspector": "^5.0.0", diff --git a/packages/devtools/src/Graph.tsx b/packages/devtools/src/Graph.tsx index b6152d7e..e7ca79d8 100644 --- a/packages/devtools/src/Graph.tsx +++ b/packages/devtools/src/Graph.tsx @@ -24,9 +24,10 @@ type Props = { getColor: typeof getColor width: Atom height: Atom + initSize: number } -export const Graph = ({ clientCtx, getColor, width, height }: Props) => { +export const Graph = ({ clientCtx, getColor, width, height, initSize }: Props) => { const name = '_ReatomDevtools.Graph' const list = reatomLinkedList( @@ -211,7 +212,7 @@ export const Graph = ({ clientCtx, getColor, width, height }: Props) => { const redrawLines = action((ctx) => lines.redraw(ctx, svg), `${name}.redrawLines`) const filters = reatomFilters( - { list: list as unknown as LinkedListAtom, clearLines: lines.clear, redrawLines }, + { list: list as unknown as LinkedListAtom, clearLines: lines.clear, redrawLines, initSize }, `${name}.filters`, ) const valuesSearch = atom((ctx) => { @@ -228,10 +229,12 @@ export const Graph = ({ clientCtx, getColor, width, height }: Props) => { ctx.spy(height) parseAtoms(ctx, filters) await ctx.schedule(() => new Promise((r) => requestAnimationFrame(r))) + // TODO: the second one is required in Firefox + await ctx.schedule(() => new Promise((r) => requestAnimationFrame(r))) return `${listEl.getBoundingClientRect().height}px` }, `${name}.listHeight`).pipe(withDataAtom('0px')).dataAtom - const subscribe = () => + // const subscribe = () => clientCtx.subscribe(async (logs) => { // sort causes and insert only from this transaction const insertTargets = new Set() @@ -322,7 +325,7 @@ export const Graph = ({ clientCtx, getColor, width, height }: Props) => { const listEl = (
    }) => ( @@ -167,17 +166,27 @@ export const reatomFilters = ( list, clearLines, redrawLines, - }: { list: LinkedListAtom; clearLines: Action<[], void>; redrawLines: Action<[], void> }, + initSize, + }: { + list: LinkedListAtom + clearLines: Action<[], void> + redrawLines: Action<[], void> + initSize: number + }, name: string, ) => { const KEY = name + version try { - var snapshot: undefined | FiltersJSON = Filters.parse(JSON.parse(localStorage.getItem(KEY) || initSnapshot)) - } catch {} + const snapshotString = localStorage.getItem(KEY) + const snapshotObject = snapshotString && JSON.parse(snapshotString) + var snapshot: undefined | FiltersJSON = Filters.parse(snapshotObject || { ...initState, size: initSize }) + } catch { + snapshot = { ...initState, size: initSize } + } const filters = reatomZod(Filters, { - initState: snapshot || initState, + initState: snapshot, sync: (ctx) => { redrawLines(ctx) ctx.schedule(() => { diff --git a/packages/devtools/src/Graph/reatomInspector.tsx b/packages/devtools/src/Graph/reatomInspector.tsx index 356e3e9b..4c69b7ce 100644 --- a/packages/devtools/src/Graph/reatomInspector.tsx +++ b/packages/devtools/src/Graph/reatomInspector.tsx @@ -1,4 +1,4 @@ -import { __root, action, atom, AtomCache, Ctx, parseAtoms, withReset } from '@reatom/framework' +import { __root, action, atom, AtomCache, AtomProto, Ctx, parseAtoms, withReset } from '@reatom/framework' import { h, hf, JSX } from '@reatom/jsx' import { ObservableHQ, ObservableHQActionButton } from '../ObservableHQ' import { reatomFilters } from './reatomFilters' @@ -10,6 +10,15 @@ type InspectorState = | { kind: 'open'; patch: AtomCache } | { kind: 'fixed'; patch: AtomCache; element: HTMLElement } +// separate action for naming purpose, CALL ONLY WITH `clientCtx` +export const update = action((ctx, proto: AtomProto, value: string) => { + ctx.get((read, actualize) => { + actualize!(ctx, proto, (patchCtx: Ctx, patch: AtomCache) => { + patch.state = JSON.parse(value) + }) + }) +}, 'update') + export const reatomInspector = ( { clientCtx, filters }: { clientCtx: Ctx; filters: ReturnType }, name: string, @@ -115,16 +124,6 @@ export const reatomInspector = ( }) }, `${name}.close`) - // separate action for naming purpose, CALL ONLY WITH `clientCtx` - const update = action((ctx, value: string) => { - ctx.get((read, actualize) => { - const proto = ctx.get(patch)?.proto! - actualize!(ctx, proto, (patchCtx: Ctx, patch: AtomCache) => { - patch.state = JSON.parse(value) - }) - }) - }, `${name}.update`) - const OPACITY = { hidden: '0', open: '0.8', @@ -221,7 +220,7 @@ export const reatomInspector = ( const proto = ctx.get(patch)?.proto const textarea = e.currentTarget.firstChild if (proto && textarea instanceof HTMLTextAreaElement) { - update(clientCtx, textarea.value) + update(clientCtx, proto, textarea.value) textarea.value = ctx.get(json) } }} diff --git a/packages/devtools/src/Graph/reatomLines.tsx b/packages/devtools/src/Graph/reatomLines.tsx index e04b7fdc..e0acb37a 100644 --- a/packages/devtools/src/Graph/reatomLines.tsx +++ b/packages/devtools/src/Graph/reatomLines.tsx @@ -45,8 +45,7 @@ export const reatomLines = (name: string): Lines => { return cause.cause?.cause && calc(target, cause.cause) } - // @ts-expect-error - if (fromEl?.computedStyleMap().get('display')?.value === 'none') { + if (fromEl && window.getComputedStyle(fromEl).getPropertyValue('display') === 'none') { return cause.cause?.cause && calc(target, cause.cause) } diff --git a/packages/devtools/src/States.tsx b/packages/devtools/src/States.tsx new file mode 100644 index 00000000..bc0b822b --- /dev/null +++ b/packages/devtools/src/States.tsx @@ -0,0 +1,147 @@ +import { atom, type AtomProto, type Ctx, type Rec, Atom, Action } from '@reatom/framework' +import { h, mount, ctx } from '@reatom/jsx' +import { ObservableHQ } from './ObservableHQ' +import { type DevtoolsOptions } from '.' + +export const States = ({ + clientCtx, + viewSwitch, + separator, + snapshot, + privatePrefix, +}: { + clientCtx: Ctx + viewSwitch: Atom + separator: Exclude + snapshot: Atom & { + forceUpdate: Action + } + privatePrefix: Exclude +}) => { + const touched = new WeakSet() + const subscribe = () => { + const clearId = setInterval(() => { + if (Object.keys(ctx.get(snapshot)).length > 0) { + snapshot.forceUpdate(ctx) + clearTimeout(clearId) + } + }, 100) + + return clientCtx.subscribe(async (logs) => { + // await null // needed to prevent `Maximum call stack size exceeded` coz `parseAtoms` + + for (const { proto, state } of logs) { + let name = proto.name! + const path = typeof separator === 'function' ? separator(name) : name.split(separator) + + if (proto.isAction || touched.has(proto) || path.some((key) => key.startsWith(privatePrefix))) { + continue + } + + let thisLogObject = ctx.get(snapshot) + + path.forEach((key, i, { length }) => { + if (i === length - 1) { + name = key + } else { + thisLogObject = thisLogObject[`[${key}]`] ??= {} + } + }) + + let update = (state: any) => { + thisLogObject[name] = state // parseAtoms(ctx, state) + } + + if (name === 'urlAtom') { + update = (state) => { + thisLogObject[name] = state.href + } + } + + update(state) + ;(proto.updateHooks ??= new Set()).add((ctx, { state }) => { + update(state) + }) + + touched.add(proto) + } + }) + } + + const reloadEl = ( + + ) + + const logEl = ( + + ) + + return ( +
    + {atom((ctx) => + ctx.spy(viewSwitch) ? ( +
    + ) : ( +
    + {reloadEl} + {logEl} + +
    + ), + )} +
    + ) +} diff --git a/packages/devtools/src/index.tsx b/packages/devtools/src/index.tsx index 4c2ee8ff..39bc8c9a 100644 --- a/packages/devtools/src/index.tsx +++ b/packages/devtools/src/index.tsx @@ -1,23 +1,56 @@ -import { atom, type AtomProto, type Ctx, type Rec, reatomBoolean, withAssign, action } from '@reatom/framework' +import { + atom, + type Ctx, + type Rec, + reatomBoolean, + withAssign, + action, + createCtx, + Atom, + Action, + AtomMut, + bind, + BooleanAtom, +} from '@reatom/framework' import { h, mount, ctx } from '@reatom/jsx' import { withLocalStorage } from '@reatom/persist-web-storage' -import { ObservableHQ } from './ObservableHQ' import { Graph } from './Graph' import { getColor } from './Graph/utils' +import { update } from './Graph/reatomInspector' +import { States } from './States' export { getColor } +export interface DevtoolsOptions { + separator?: string | RegExp | ((name: string) => Array) + privatePrefix?: string + getColor?: typeof getColor + visible?: BooleanAtom + initSize?: number +} + +export interface DevtoolsState { + (newState: T): void + subscribe(cb: (state: T) => void): () => void +} + +export interface Devtools { + log(name: string, payload?: any): void + state(name: string, initState: T): DevtoolsState + + show(): void + hide(): void +} + export const _connectDevtools = async ( clientCtx: Ctx, { separator = /\.|#/, privatePrefix = '_', getColor: _getColor = getColor, - }: { - separator?: string | RegExp | ((name: string) => Array) - privatePrefix?: string - getColor?: typeof getColor - } = {}, + visible = reatomBoolean(true, `_ReatomDevtools.visible`), + initSize = 1000, + }: DevtoolsOptions = {}, ) => { const name = '_ReatomDevtools' @@ -31,8 +64,8 @@ export const _connectDevtools = async ( const viewSwitch = reatomBoolean(true, `${name}.viewSwitch`).pipe(withLocalStorage(`${name}.viewSwitch`)) const snapshot = atom({}, `${name}.snapshot`).pipe( - withAssign((target) => ({ - forceUpdate: (ctx: Ctx) => target(ctx, (state) => ({ ...state })), + withAssign((target, name) => ({ + forceUpdate: action((ctx) => target(ctx, (state) => ({ ...state })), `${name}.forceUpdate`), })), ) @@ -132,67 +165,6 @@ export const _connectDevtools = async ( ) - const reloadEl = ( - - ) - - const logEl = ( - - ) - const containerEl = (
    {logo} {viewSwitchEl} -
    (ctx.spy(viewSwitch) ? 'none' : 'block'))} - > - {reloadEl} - {logEl} - -
    +
    (ctx.spy(viewSwitch) ? 'block' : 'none'))} > - +
    ) - const touched = new WeakSet() - - clientCtx.subscribe(async (logs) => { - // await null // needed to prevent `Maximum call stack size exceeded` coz `parseAtoms` - - for (const { proto, state } of logs) { - let name = proto.name! - const path = typeof separator === 'function' ? separator(name) : name.split(separator) + if (ctx.get(visible)) mount(document.body, containerEl) - if (proto.isAction || touched.has(proto) || path.some((key) => key.startsWith(privatePrefix))) { - continue - } + visible.onChange((ctx, state) => { + if (state) { + mount(document.body, containerEl) + } else { + containerEl.remove() + } + }) +} - let thisLogObject = ctx.get(snapshot) +/** @deprecated use `createDevtools` instead */ +export const connectDevtools = (...[ctx, options]: Parameters) => { + _connectDevtools(ctx, options) - path.forEach((key, i, { length }) => { - if (i === length - 1) { - name = key - } else { - thisLogObject = thisLogObject[`[${key}]`] ??= {} - } - }) + return (name: string, payload: T): T => { + const logAction = action((ctx, payload: T) => payload, name) + return logAction(ctx, payload) + } +} - let update = (state: any) => { - thisLogObject[name] = state // parseAtoms(ctx, state) - } +export const createDevtools = ({ + ctx: clientCtx = createCtx(), + initVisibility = true, + ...options +}: Omit & { ctx?: Ctx; initVisibility?: boolean } = {}): Devtools => { + const visible = reatomBoolean(initVisibility, '_ReatomDevtools.visible') - if (name === 'urlAtom') { - update = (state) => { - thisLogObject[name] = state.href - } - } + _connectDevtools(clientCtx, { ...options, visible }) - update(state) - ;(proto.updateHooks ??= new Set()).add((ctx, { state }) => { - update(state) - }) + const cache = new Map() - touched.add(proto) + const log: Devtools['log'] = (name: string, payload: any) => { + let target = cache.get(name) as Action | undefined + if (!target) { + cache.set(name, (target = action(name))) } - }) + target(clientCtx, payload) + } - const clearId = setInterval(() => { - if (Object.keys(ctx.get(snapshot)).length > 0) { - snapshot.forceUpdate(ctx) - clearTimeout(clearId) + const state: Devtools['state'] = (name, initState) => { + let target = cache.get(name) as AtomMut | undefined + if (!target) { + cache.set(name, (target = atom(initState, name))) + } else if (ctx.get(target) !== initState) { + target(clientCtx, initState) } - }, 100) - mount(document.body, containerEl) -} + // memoize the reference to the atom + const result = bind(clientCtx, target) as DevtoolsState -export const connectDevtools = (...[ctx, options]: Parameters) => { - _connectDevtools(ctx, options) + const subscribe: DevtoolsState['subscribe'] = (cb) => + target.onChange((ctx, state) => { + if (ctx.cause.cause?.proto === update.__reatom) { + cb(state) + } + }) - return (name: string, payload: T): T => { - const logAction = action((ctx, payload: T) => payload, name) - return logAction(ctx, payload) + result.subscribe ??= subscribe + + return result + } + + return { + log, + state, + show: bind(ctx, visible.setTrue), + hide: bind(ctx, visible.setFalse), } }