diff --git a/.changeset/unlucky-hairs-divide.md b/.changeset/unlucky-hairs-divide.md new file mode 100644 index 00000000..6af8de7d --- /dev/null +++ b/.changeset/unlucky-hairs-divide.md @@ -0,0 +1,8 @@ +--- +"@solid-devtools/frontend": minor +"@solid-devtools/shared": minor +"@solid-devtools/overlay": patch +"@solid-devtools/extension": patch +--- + +Replace bridge.input event hub abstraction with a simple event bus (to be removed later) diff --git a/extension/src/panel.tsx b/extension/src/panel.tsx index 270b5ecf..e2825ff5 100644 --- a/extension/src/panel.tsx +++ b/extension/src/panel.tsx @@ -20,10 +20,10 @@ log(Place_Name.Panel+' loaded.') function App() { const empty_versions: Versions = { - solid: '', - client: '', + solid: '', + client: '', client_expected: '', - extension: '', + extension: '', } const [versions, setVersions] = s.createSignal(empty_versions) @@ -38,9 +38,10 @@ function App() { break default: /* Client -> Devtools */ - if (e.name in devtools.bridge.input) { - devtools.bridge.input.emit(e.name as any, e.details) - } + devtools.bridge.input.emit( + // @ts-expect-error + e + ) } }) diff --git a/packages/frontend/src/controller.tsx b/packages/frontend/src/controller.tsx index ffc14961..9f2426d1 100644 --- a/packages/frontend/src/controller.tsx +++ b/packages/frontend/src/controller.tsx @@ -1,8 +1,9 @@ -import {type Debugger, DebuggerModule, DevtoolsMainView, type NodeID} from '@solid-devtools/debugger/types' import {SECOND} from '@solid-primitives/date' -import {type EventBus, batchEmits, createEventBus, createEventHub} from '@solid-primitives/event-bus' +import {type EventBus, createEventBus, createEventHub} from '@solid-primitives/event-bus' import {debounce} from '@solid-primitives/scheduled' import {defer} from '@solid-primitives/utils' +import {type Debugger, DebuggerModule, DevtoolsMainView, type NodeID} from '@solid-devtools/debugger/types' +import {mutate_remove} from '@solid-devtools/shared/utils' import * as s from 'solid-js' import {App} from './App.tsx' import createInspector from './inspector.tsx' @@ -14,7 +15,16 @@ type ToEventBusChannels> = { [K in keyof T]: EventBus } +export type InputMessage = { + [K in keyof Debugger.OutputChannels]: { + name: K, + details: Debugger.OutputChannels[K], + } +}[keyof Debugger.OutputChannels] +export type InputListener = (e: InputMessage) => void + function createDebuggerBridge() { + const output = createEventHub>($ => ({ ResetState: $(), InspectNode: $(), @@ -26,21 +36,22 @@ function createDebuggerBridge() { ToggleModule: $(), })) - // Listener of the client events (from the debugger) will be called synchronously under `batch` - // to make sure that the state is updated before the effect queue is flushed. - const input = createEventHub>($ => ({ - DebuggerEnabled: batchEmits($()), - ResetPanel: batchEmits($()), - InspectedState: batchEmits($()), - InspectedNodeDetails: batchEmits($()), - StructureUpdates: batchEmits($()), - NodeUpdates: batchEmits($()), - InspectorUpdate: batchEmits($()), - LocatorModeChange: batchEmits($()), - HoveredComponent: batchEmits($()), - InspectedComponent: batchEmits($()), - DgraphUpdate: batchEmits($()), - })) + let input_listeners: InputListener[] = [] + const input = { + listen(listener: InputListener) { + input_listeners.push(listener) + s.onCleanup(() => { + mutate_remove(input_listeners, listener) + }) + }, + emit(e: InputMessage) { + s.batch(() => { + for (let fn of input_listeners) { + fn(e) + } + }) + }, + } return {input, output} } @@ -152,11 +163,9 @@ function createController(bridge: DebuggerBridge, options: DevtoolsOptions) { const locatorEnabled = () => devtoolsLocatorEnabled() || clientLocatorEnabled() // send devtools locator state - s.createEffect( - defer(devtoolsLocatorEnabled, enabled => - bridge.output.ToggleModule.emit({module: DebuggerModule.Locator, enabled}), - ), - ) + s.createEffect(defer(devtoolsLocatorEnabled, enabled => + bridge.output.ToggleModule.emit({module: DebuggerModule.Locator, enabled}), + )) function setClientLocatorState(enabled: boolean) { s.batch(() => { @@ -210,7 +219,6 @@ function createController(bridge: DebuggerBridge, options: DevtoolsOptions) { // Node updates - signals and computations updating // const nodeUpdates = createEventBus() - bridge.input.NodeUpdates.listen(updated => updated.forEach(id => nodeUpdates.emit(id))) // // INSPECTOR @@ -220,23 +228,43 @@ function createController(bridge: DebuggerBridge, options: DevtoolsOptions) { // // Client events // - bridge.input.ResetPanel.listen(() => { - setClientLocatorState(false) - setDevtoolsLocatorState(false) - inspector.setInspectedOwner(null) - }) - - bridge.input.HoveredComponent.listen(({nodeId, state}) => { - setClientHoveredNode(p => (state ? nodeId : p && p === nodeId ? null : p)) + bridge.input.listen(e => { + switch (e.name) { + case 'NodeUpdates': + for (let id of e.details) { + nodeUpdates.emit(id) + } + break + case 'ResetPanel': + setClientLocatorState(false) + setDevtoolsLocatorState(false) + inspector.setInspectedOwner(null) + break + case 'HoveredComponent': + setClientHoveredNode(p => { + return e.details.state + ? e.details.nodeId + : p && p === e.details.nodeId ? null : p + }) + break + case 'InspectedComponent': + inspector.setInspectedOwner(e.details) + setDevtoolsLocatorState(false) + break + case 'LocatorModeChange': + setClientLocatorState(e.details) + break + case 'DebuggerEnabled': + case 'InspectedState': + case 'InspectedNodeDetails': + case 'StructureUpdates': + case 'InspectorUpdate': + case 'DgraphUpdate': + // handled elsewhere for now + break + } }) - bridge.input.InspectedComponent.listen(node => { - inspector.setInspectedOwner(node) - setDevtoolsLocatorState(false) - }) - - bridge.input.LocatorModeChange.listen(setClientLocatorState) - return { locator: { locatorEnabled, diff --git a/packages/frontend/src/dgraph.tsx b/packages/frontend/src/dgraph.tsx index e95bb532..343dfa3c 100644 --- a/packages/frontend/src/dgraph.tsx +++ b/packages/frontend/src/dgraph.tsx @@ -10,7 +10,15 @@ export function createDependencyGraph() { const {bridge, inspector} = useController() const [graph, setGraph] = s.createSignal(null) - bridge.input.DgraphUpdate.listen(setGraph) + + bridge.input.listen(e => { + // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check + switch (e.name) { + case 'DgraphUpdate': + setGraph(e.details) + break + } + }) bridge.output.ToggleModule.emit({module: debug.DebuggerModule.Dgraph, enabled: true}) s.onCleanup(() => diff --git a/packages/frontend/src/inspector.tsx b/packages/frontend/src/inspector.tsx index a1a4a2a5..731e197c 100644 --- a/packages/frontend/src/inspector.tsx +++ b/packages/frontend/src/inspector.tsx @@ -2,8 +2,8 @@ import clsx from 'clsx' import * as s from 'solid-js' import {Entries} from '@solid-primitives/keyed' import {createStaticStore} from '@solid-primitives/static-store' -import {defer} from '@solid-primitives/utils' -import {handleTupleUpdates, createHover, createPingedSignal} from '@solid-devtools/shared/primitives' +import {defer, entries} from '@solid-primitives/utils' +import {createHover, createPingedSignal} from '@solid-devtools/shared/primitives' import {error, splitOnColon} from '@solid-devtools/shared/utils' import * as debug from '@solid-devtools/debugger/types' import * as theme from '@solid-devtools/shared/theme' @@ -210,17 +210,30 @@ export default function createInspector({bridge}: {bridge: DebuggerBridge}) { const storeNodeMap = new decode.StoreNodeMap() - bridge.input.InspectedState.listen(newState => { - s.batch(() => { - const prev = inspected.ownerId - setInspected(newState) - if (newState.ownerId !== prev) setState({...NULL_STATE}) - }) - }) + function getValueItem(value_item_id: debug.ValueItemID): Inspector.ValueItem | undefined { - bridge.input.InspectedNodeDetails.listen(function (raw) { - const id = inspected.ownerId - s.batch(() => { + const [type, id] = splitOnColon(value_item_id) + + // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check + switch (type) { + case debug.ValueItemType.Signal: return state.signals[id] + case debug.ValueItemType.Prop: return state.props?.record[id] + case debug.ValueItemType.Value: return state.value ?? undefined + } + } + + bridge.input.listen(e => { + // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check + switch (e.name) { + case 'InspectedState': { + let prev = inspected.ownerId + setInspected(e.details) + if (e.details.ownerId !== prev) setState({...NULL_STATE}) + break + } + case 'InspectedNodeDetails': { + const raw = e.details + const id = inspected.ownerId // The current inspected node is not the same as the one that sent the details // (replace it with the new one) if (!id || id !== raw.id) setInspectedOwner(raw.id) @@ -236,88 +249,94 @@ export default function createInspector({bridge}: {bridge: DebuggerBridge}) { s.id, s.type, s.name, - decode.decodeValue(s.value, null, storeNodeMap), - ) + decode.decodeValue(s.value, null, storeNodeMap)) return signals }, {} as Inspector.State['signals'], ), value: raw.value ? createValueItem( - debug.ValueItemType.Value, - decode.decodeValue(raw.value, null, storeNodeMap), - ) + debug.ValueItemType.Value, + decode.decodeValue(raw.value, null, storeNodeMap)) : null, props: raw.props ? { - proxy: raw.props.proxy, - record: Object.entries(raw.props.record).reduce( - (record, [key, p]) => { - record[key] = createPropItem( - key, - p.value - ? decode.decodeValue(p.value, null, storeNodeMap) - : {type: debug.ValueType.Unknown}, - p.getter, - ) - return record - }, - {} as Inspector.Props['record'], - ), - } + proxy: raw.props.proxy, + record: Object.entries(raw.props.record).reduce( + (record, [key, p]) => { + record[key] = createPropItem( + key, + p.value + ? decode.decodeValue(p.value, null, storeNodeMap) + : {type: debug.ValueType.Unknown}, + p.getter) + return record + }, + {} as Inspector.Props['record'], + ), + } : null, }) - }) - }) - - function getValueItem(value_item_id: debug.ValueItemID): Inspector.ValueItem | undefined { - const [type, id] = splitOnColon(value_item_id) - - // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check - switch (type) { - case debug.ValueItemType.Signal: return state.signals[id] - case debug.ValueItemType.Prop: return state.props?.record[id] - case debug.ValueItemType.Value: return state.value ?? undefined + break } - } + case 'InspectorUpdate': { + const KIND = 0, DATA = 1 + + for (let update of e.details) { + switch (update[KIND]) { + case 'value': { + let [value_item_id, value] = update[DATA] + + let value_item = getValueItem(value_item_id) + if (value_item == null) { + error(`ValueItem not found for id ${value_item_id}.`) + break + } - bridge.input.InspectorUpdate.listen( - handleTupleUpdates({ - value(update) { - let [value_item_id, value] = update - let value_item = getValueItem(value_item_id) - if (value_item != null) { value_item.setValue(decode.decodeValue(value, value_item.value, storeNodeMap)) + + break } - }, - inspectToggle(update) { - let [value_item_id, value] = update - let value_item = getValueItem(value_item_id) - if (value_item == null) { - error(`ValueItem not found for id ${value_item_id}.`) - return - } + case 'inspectToggle': { + let [value_item_id, value] = update[DATA] + + let value_item = getValueItem(value_item_id) + if (value_item == null) { + error(`ValueItem not found for id ${value_item_id}.`) + break + } + + if (decode.isObjectType(value_item.value)) { + decode.updateCollapsedValue(value_item.value, value, storeNodeMap) + } - if (decode.isObjectType(value_item.value)) { - decode.updateCollapsedValue(value_item.value, value, storeNodeMap) + break } - }, - propKeys(update) { - setState('props', updateProxyProps(update)) - }, - propState(update) { - if (!state.props) return + case 'propKeys': { + setState('props', updateProxyProps(update[DATA])) + break + } + case 'propState': { + if (!state.props) break + + for (const [key, getterState] of entries(update[DATA])) { + state.props.record[key]?.setGetter(getterState) + } - for (const [key, getterState] of Object.entries(update)) { - state.props.record[key]?.setGetter(getterState) + break } - }, - store(update) { - updateStore(update, storeNodeMap) - }, - }), - ) + case 'store': { + updateStore(update[DATA], storeNodeMap) + break + } + } + } + + break + } + } + }) /** * Toggle the inspection of a value item (signal, prop, or owner value) diff --git a/packages/frontend/src/structure.tsx b/packages/frontend/src/structure.tsx index bdf25fda..e7918167 100644 --- a/packages/frontend/src/structure.tsx +++ b/packages/frontend/src/structure.tsx @@ -148,15 +148,19 @@ export function createStructure() { } } - // - // Listen to Client Events - // - bridge.input.ResetPanel.listen(() => { - updateStructure(null) + /* Listen to Client Events */ + bridge.input.listen(e => { + /* eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check */ + switch (e.name) { + case 'ResetPanel': + updateStructure(null) + break + case 'StructureUpdates': + updateStructure(e.details) + break + } }) - bridge.input.StructureUpdates.listen(updateStructure) - // TREE VIEW MODE s.createEffect(defer(mode, bridge.output.TreeViewModeChange.emit)) diff --git a/packages/overlay/src/controller.tsx b/packages/overlay/src/controller.tsx index 51a86cd7..01961aa0 100644 --- a/packages/overlay/src/controller.tsx +++ b/packages/overlay/src/controller.tsx @@ -26,8 +26,7 @@ export function Devtools(props: DevtoolsProps): s.JSX.Element { }) debug.listen(e => { - // TODO this should be fixed in the solid-primitives package (allow for passing events straight through) - separate(e.details, details => bridge.input.emit(e.name, details as never)) + separate(e, bridge.input.emit) }) return diff --git a/packages/shared/src/primitives.ts b/packages/shared/src/primitives.ts index 4826daf3..98c980fd 100644 --- a/packages/shared/src/primitives.ts +++ b/packages/shared/src/primitives.ts @@ -27,6 +27,9 @@ export type WritableDeep = 0 extends 1 & T export const untrackedCallback = (fn: Fn): Fn => ((...a: Parameters) => untrack>(fn.bind(void 0, ...a))) as any +export const batchedCallback = (fn: Fn): Fn => + ((...a: Parameters) => batch>(fn.bind(void 0, ...a))) as any + export const useIsTouch = createSingletonRoot(() => createMediaQuery('(hover: none)')) export const useIsMobile = createSingletonRoot(() => createMediaQuery('(max-width: 640px)')) @@ -179,20 +182,3 @@ export function createPingedSignal( return [isUpdated, ping] } - -export function handleTupleUpdate< - T extends readonly [PropertyKey, any], - O = {readonly [K in T as K[0]]: (value: K[1]) => void}, ->(handlers: O): (update: T) => void { - return update => (handlers as any)[update[0]](update[1]) -} - -export function handleTupleUpdates< - T extends readonly [PropertyKey, any], - O = {readonly [K in T as K[0]]: (value: K[1]) => void}, ->(handlers: O): (updates: T[]) => void { - function runUpdates(updates: T[]): void { - for (const [key, value] of updates) (handlers as any)[key](value) - } - return updates => batch(runUpdates.bind(void 0, updates)) -} diff --git a/packages/shared/src/utils.ts b/packages/shared/src/utils.ts index 8f123c39..f9cff685 100644 --- a/packages/shared/src/utils.ts +++ b/packages/shared/src/utils.ts @@ -155,3 +155,14 @@ export function dedupeArrayById(input: T[]): T[] { } return deduped } + +export function mutate_filter(array: T[], callback: (item: T) => boolean): void { + for (let i = array.length - 1; i >= 0; i--) { + if (!callback(array[i]!)) array.splice(i, 1) + } +} + +export function mutate_remove(array: T[], item: T): void { + const index = array.indexOf(item) + if (index !== -1) array.splice(index, 1) +}