Invert some client store dependencies to improve performance and prevent dependency cycles

This commit is contained in:
Andrew Kingston 2022-10-14 18:16:19 +01:00
parent 23eb09ab6a
commit d02fb96e6e
15 changed files with 122 additions and 83 deletions

View File

@ -85,6 +85,10 @@
"icon": "Selection", "icon": "Selection",
"hasChildren": true, "hasChildren": true,
"showSettingsBar": true, "showSettingsBar": true,
"size": {
"width": 400,
"height": 200
},
"styles": [ "styles": [
"padding", "padding",
"size", "size",

View File

@ -21,14 +21,14 @@
devToolsStore, devToolsStore,
componentStore, componentStore,
appStore, appStore,
isDragging, dndIsDragging,
dndComponentPath,
} from "stores" } from "stores"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
import { getActiveConditions, reduceConditionActions } from "utils/conditions" import { getActiveConditions, reduceConditionActions } from "utils/conditions"
import Placeholder from "components/app/Placeholder.svelte" import Placeholder from "components/app/Placeholder.svelte"
import ScreenPlaceholder from "components/app/ScreenPlaceholder.svelte" import ScreenPlaceholder from "components/app/ScreenPlaceholder.svelte"
import ComponentPlaceholder from "components/app/ComponentPlaceholder.svelte" import ComponentPlaceholder from "components/app/ComponentPlaceholder.svelte"
import { DNDPlaceholderID } from "constants"
export let instance = {} export let instance = {}
export let isLayout = false export let isLayout = false
@ -105,7 +105,7 @@
$builderStore.inBuilder && $builderStore.selectedComponentId === id $builderStore.inBuilder && $builderStore.selectedComponentId === id
$: inSelectedPath = $componentStore.selectedComponentPath?.includes(id) $: inSelectedPath = $componentStore.selectedComponentPath?.includes(id)
$: inDragPath = inSelectedPath && $builderStore.editMode $: inDragPath = inSelectedPath && $builderStore.editMode
$: inDndPath = $componentStore.dndPath?.includes(id) $: inDndPath = $dndComponentPath?.includes(id)
// Derive definition properties which can all be optional, so need to be // Derive definition properties which can all be optional, so need to be
// coerced to booleans // coerced to booleans
@ -162,7 +162,7 @@
// nested layers. Only reset this when dragging stops. // nested layers. Only reset this when dragging stops.
let pad = false let pad = false
$: pad = pad || (interactive && hasChildren && inDndPath) $: pad = pad || (interactive && hasChildren && inDndPath)
$: $isDragging, (pad = false) $: $dndIsDragging, (pad = false)
// Update component context // Update component context
$: store.set({ $: store.set({
@ -471,7 +471,6 @@
class:pad class:pad
class:parent={hasChildren} class:parent={hasChildren}
class:block={isBlock} class:block={isBlock}
class:placeholder={id === DNDPlaceholderID}
data-id={id} data-id={id}
data-name={name} data-name={name}
data-icon={icon} data-icon={icon}
@ -502,7 +501,7 @@
display: contents; display: contents;
} }
.component :global(> *) { .component :global(> *) {
transition: padding 260ms ease-in, border 260ms ease-in; transition: padding 260ms ease-out, border 260ms ease-out;
} }
.component.pad :global(> *) { .component.pad :global(> *) {
padding: 12px !important; padding: 12px !important;

View File

@ -4,14 +4,15 @@
import IndicatorSet from "./IndicatorSet.svelte" import IndicatorSet from "./IndicatorSet.svelte"
import { import {
builderStore, builderStore,
componentStore, screenStore,
dndStore, dndStore,
dndParent, dndParent,
isDragging, dndIsDragging,
} from "stores" } from "stores"
import DNDPlaceholderOverlay from "./DNDPlaceholderOverlay.svelte" import DNDPlaceholderOverlay from "./DNDPlaceholderOverlay.svelte"
import { Utils } from "@budibase/frontend-core" import { Utils } from "@budibase/frontend-core"
import { findComponentById } from "utils/components.js" import { findComponentById } from "utils/components.js"
import { DNDPlaceholderID } from "constants"
const ThrottleRate = 130 const ThrottleRate = 130
@ -69,13 +70,13 @@
const id = component.dataset.id const id = component.dataset.id
const parentId = component.dataset.parent const parentId = component.dataset.parent
const parent = findComponentById( const parent = findComponentById(
get(componentStore).currentAsset.props, get(screenStore).activeScreen?.props,
parentId parentId
) )
const index = parent._children.findIndex( const index = parent._children.findIndex(
x => x._id === component.dataset.id x => x._id === component.dataset.id
) )
dndStore.actions.startDragging({ dndStore.actions.startDraggingExistingComponent({
id, id,
bounds: component.children[0].getBoundingClientRect(), bounds: component.children[0].getBoundingClientRect(),
parent: parentId, parent: parentId,
@ -127,7 +128,7 @@
let ephemeralDiv let ephemeralDiv
if (node.children.length === 1) { if (node.children.length === 1) {
ephemeralDiv = document.createElement("div") ephemeralDiv = document.createElement("div")
ephemeralDiv.classList.add("placeholder") ephemeralDiv.dataset.id = DNDPlaceholderID
node.appendChild(ephemeralDiv) node.appendChild(ephemeralDiv)
} }
@ -138,7 +139,7 @@
const child = node.children?.[0] || node const child = node.children?.[0] || node
const bounds = child.getBoundingClientRect() const bounds = child.getBoundingClientRect()
return { return {
placeholder: node.classList.contains("placeholder"), placeholder: node.dataset.id === DNDPlaceholderID,
centerX: bounds.left + bounds.width / 2, centerX: bounds.left + bounds.width / 2,
centerY: bounds.top + bounds.height / 2, centerY: bounds.top + bounds.height / 2,
left: bounds.left, left: bounds.left,
@ -249,7 +250,7 @@
// Convert parent + index into target + mode // Convert parent + index into target + mode
let legacyDropTarget, legacyDropMode let legacyDropTarget, legacyDropMode
const parent = findComponentById( const parent = findComponentById(
get(componentStore).currentAsset?.props, get(screenStore).activeScreen?.props,
drop.parent drop.parent
) )
if (!parent) { if (!parent) {
@ -263,7 +264,7 @@
// Filter out source component and placeholder from consideration // Filter out source component and placeholder from consideration
const children = parent._children?.filter( const children = parent._children?.filter(
x => x._id !== "placeholder" && x._id !== source.id x => x._id !== DNDPlaceholderID && x._id !== source.id
) )
// Use inside if no existing children // Use inside if no existing children
@ -316,6 +317,6 @@
prefix="Inside" prefix="Inside"
/> />
{#if $isDragging} {#if $dndIsDragging}
<DNDPlaceholderOverlay /> <DNDPlaceholderOverlay />
{/if} {/if}

View File

@ -1,7 +1,7 @@
<script> <script>
import { onMount, onDestroy } from "svelte" import { onMount, onDestroy } from "svelte"
import IndicatorSet from "./IndicatorSet.svelte" import IndicatorSet from "./IndicatorSet.svelte"
import { builderStore, isDragging } from "stores" import { builderStore, dndIsDragging } from "stores"
let componentId let componentId
$: zIndex = componentId === $builderStore.selectedComponentId ? 900 : 920 $: zIndex = componentId === $builderStore.selectedComponentId ? 900 : 920
@ -30,7 +30,7 @@
</script> </script>
<IndicatorSet <IndicatorSet
componentId={$isDragging ? null : componentId} componentId={$dndIsDragging ? null : componentId}
color="var(--spectrum-global-color-static-blue-200)" color="var(--spectrum-global-color-static-blue-200)"
transition transition
{zIndex} {zIndex}

View File

@ -1,5 +1,5 @@
<script> <script>
import { builderStore, isDragging } from "stores" import { builderStore, dndIsDragging } from "stores"
import IndicatorSet from "./IndicatorSet.svelte" import IndicatorSet from "./IndicatorSet.svelte"
$: color = $builderStore.editMode $: color = $builderStore.editMode
@ -8,7 +8,7 @@
</script> </script>
<IndicatorSet <IndicatorSet
componentId={$isDragging ? null : $builderStore.selectedComponentId} componentId={$dndIsDragging ? null : $builderStore.selectedComponentId}
{color} {color}
zIndex="910" zIndex="910"
transition transition

View File

@ -3,7 +3,7 @@
import SettingsButton from "./SettingsButton.svelte" import SettingsButton from "./SettingsButton.svelte"
import SettingsColorPicker from "./SettingsColorPicker.svelte" import SettingsColorPicker from "./SettingsColorPicker.svelte"
import SettingsPicker from "./SettingsPicker.svelte" import SettingsPicker from "./SettingsPicker.svelte"
import { builderStore, componentStore, isDragging } from "stores" import { builderStore, componentStore, dndIsDragging } from "stores"
import { domDebounce } from "utils/domDebounce" import { domDebounce } from "utils/domDebounce"
const verticalOffset = 36 const verticalOffset = 36
@ -16,7 +16,7 @@
let measured = false let measured = false
$: definition = $componentStore.selectedComponentDefinition $: definition = $componentStore.selectedComponentDefinition
$: showBar = definition?.showSettingsBar && !$isDragging $: showBar = definition?.showSettingsBar && !$dndIsDragging
$: settings = getBarSettings(definition) $: settings = getBarSettings(definition)
const getBarSettings = definition => { const getBarSettings = definition => {

View File

@ -64,20 +64,11 @@ const loadBudibase = async () => {
} else if (name === "dragging-new-component") { } else if (name === "dragging-new-component") {
const { dragging, component } = payload const { dragging, component } = payload
if (dragging) { if (dragging) {
dndStore.actions.startDragging({ const definition =
id: null, componentStore.actions.getComponentDefinition(component)
parent: null, dndStore.actions.startDraggingNewComponent({ component, definition })
bounds: {
height: 64,
width: 128,
},
index: null,
newComponentType: component,
})
builderStore.actions.setDraggingNewComponent(true)
} else { } else {
dndStore.actions.reset() dndStore.actions.reset()
builderStore.actions.setDraggingNewComponent(false)
} }
} }
} }

View File

@ -16,7 +16,6 @@ const createBuilderStore = () => {
theme: null, theme: null,
customTheme: null, customTheme: null,
previewDevice: "desktop", previewDevice: "desktop",
draggingNewComponent: false,
navigation: null, navigation: null,
hiddenComponentIds: [], hiddenComponentIds: [],
usedPlugins: null, usedPlugins: null,
@ -68,19 +67,12 @@ const createBuilderStore = () => {
}) })
}, },
dropNewComponent: (component, parent, index) => { dropNewComponent: (component, parent, index) => {
console.log("dispatch", component, parent, index)
dispatchEvent("drop-new-component", { dispatchEvent("drop-new-component", {
component, component,
parent, parent,
index, index,
}) })
}, },
setDraggingNewComponent: draggingNewComponent => {
if (draggingNewComponent === get(store).draggingNewComponent) {
return
}
store.update(state => ({ ...state, draggingNewComponent }))
},
setEditMode: enabled => { setEditMode: enabled => {
if (enabled === get(store).editMode) { if (enabled === get(store).editMode) {
return return

View File

@ -4,7 +4,6 @@ import { findComponentById, findComponentPathById } from "../utils/components"
import { devToolsStore } from "./devTools" import { devToolsStore } from "./devTools"
import { screenStore } from "./screens" import { screenStore } from "./screens"
import { builderStore } from "./builder" import { builderStore } from "./builder"
import { dndParent } from "./dnd.js"
import Router from "../components/Router.svelte" import Router from "../components/Router.svelte"
import DNDPlaceholder from "../components/preview/DNDPlaceholder.svelte" import DNDPlaceholder from "../components/preview/DNDPlaceholder.svelte"
import * as AppComponents from "../components/app/index.js" import * as AppComponents from "../components/app/index.js"
@ -20,28 +19,22 @@ const createComponentStore = () => {
}) })
const derivedStore = derived( const derivedStore = derived(
[store, builderStore, devToolsStore, screenStore, dndParent], [store, builderStore, devToolsStore, screenStore],
([$store, $builderState, $devToolsState, $screenState, $dndParent]) => { ([$store, $builderStore, $devToolsStore, $screenStore]) => {
const { inBuilder, selectedComponentId } = $builderStore
// Avoid any of this logic if we aren't in the builder preview // Avoid any of this logic if we aren't in the builder preview
if (!$builderState.inBuilder && !$devToolsState.visible) { if (!inBuilder && !$devToolsStore.visible) {
return {} return {}
} }
// Derive the selected component instance and definition const root = $screenStore.activeScreen?.props
let asset const component = findComponentById(root, selectedComponentId)
const { screen, selectedComponentId } = $builderState
if ($builderState.inBuilder) {
asset = screen
} else {
asset = $screenState.activeScreen
}
const component = findComponentById(asset?.props, selectedComponentId)
const definition = getComponentDefinition(component?._component) const definition = getComponentDefinition(component?._component)
// Derive the selected component path // Derive the selected component path
const selectedPath = const selectedPath =
findComponentPathById(asset?.props, selectedComponentId) || [] findComponentPathById(root, selectedComponentId) || []
const dndPath = findComponentPathById(asset?.props, $dndParent) || []
return { return {
customComponentManifest: $store.customComponentManifest, customComponentManifest: $store.customComponentManifest,
@ -51,9 +44,6 @@ const createComponentStore = () => {
selectedComponentDefinition: definition, selectedComponentDefinition: definition,
selectedComponentPath: selectedPath?.map(component => component._id), selectedComponentPath: selectedPath?.map(component => component._id),
mountedComponentCount: Object.keys($store.mountedComponents).length, mountedComponentCount: Object.keys($store.mountedComponents).length,
currentAsset: asset,
dndPath: dndPath?.map(component => component._id),
dndDepth: dndPath?.length || 0,
} }
} }
) )
@ -101,8 +91,8 @@ const createComponentStore = () => {
} }
const getComponentById = id => { const getComponentById = id => {
const asset = get(derivedStore).currentAsset const root = get(screenStore).activeScreen?.props
return findComponentById(asset?.props, id) return findComponentById(root, id)
} }
const getComponentDefinition = type => { const getComponentDefinition = type => {

View File

@ -0,0 +1,13 @@
import { derived } from "svelte/store"
import { findComponentPathById } from "utils/components.js"
import { dndParent } from "../dnd.js"
import { screenStore } from "../screens.js"
export const dndComponentPath = derived(
[dndParent, screenStore],
([$dndParent, $screenStore]) => {
const root = $screenStore.activeScreen?.props
const path = findComponentPathById(root, $dndParent) || []
return path?.map(component => component._id)
}
)

View File

@ -1,2 +1,5 @@
export { isDragging } from "./isDragging.js" // These derived stores are pulled out from their parent stores to avoid
// dependency loops. By inverting store dependencies and extracting them
// separately we can keep our actual stores lean and performant.
export { currentRole } from "./currentRole.js" export { currentRole } from "./currentRole.js"
export { dndComponentPath } from "./dndComponentPath.js"

View File

@ -1,10 +0,0 @@
import { derived } from "svelte/store"
import { dndStore } from "../dnd"
import { builderStore } from "../builder.js"
// Derive whether we are dragging or not
export const isDragging = derived(
[dndStore, builderStore],
([$dndStore, $builderStore]) =>
$dndStore.source != null || $builderStore.draggingNewComponent
)

View File

@ -2,6 +2,10 @@ import { writable, derived } from "svelte/store"
const createDndStore = () => { const createDndStore = () => {
const initialState = { const initialState = {
// Flags for whether we are inserting a new component or not
isNewComponent: false,
newComponentType: null,
// Info about the dragged component // Info about the dragged component
source: null, source: null,
@ -13,12 +17,32 @@ const createDndStore = () => {
} }
const store = writable(initialState) const store = writable(initialState)
// newComponentType is an optional field to signify we are creating a new const startDraggingExistingComponent = ({ id, parent, bounds, index }) => {
// component rather than moving an existing one
const startDragging = ({ id, parent, bounds, index, newComponentType }) => {
store.set({ store.set({
...initialState, ...initialState,
source: { id, parent, bounds, index, newComponentType }, source: { id, parent, bounds, index },
})
}
const startDraggingNewComponent = ({ type, definition }) => {
if (!type || !definition) {
return
}
// Get size of new component so we can show a properly sized placeholder
const width = definition.size?.width || 128
const height = definition.size?.height || 64
store.set({
...initialState,
isNewComponent: true,
newComponentType: type,
source: {
id: null,
parent: null,
bounds: { height, width },
index: null,
},
}) })
} }
@ -43,7 +67,8 @@ const createDndStore = () => {
return { return {
subscribe: store.subscribe, subscribe: store.subscribe,
actions: { actions: {
startDragging, startDraggingExistingComponent,
startDraggingNewComponent,
updateTarget, updateTarget,
updateDrop, updateDrop,
reset, reset,
@ -52,9 +77,19 @@ const createDndStore = () => {
} }
export const dndStore = createDndStore() export const dndStore = createDndStore()
// The DND store is updated extremely frequently, so we can greatly improve
// performance by deriving any state that needs to be externally observed.
// By doing this and using primitives, we can avoid invalidating other stores
// or components which depend on DND state unless values actually change.
export const dndIsDragging = derived(dndStore, $dndStore => !!$dndStore.source)
export const dndParent = derived(dndStore, $dndStore => $dndStore.drop?.parent) export const dndParent = derived(dndStore, $dndStore => $dndStore.drop?.parent)
export const dndIndex = derived(dndStore, $dndStore => $dndStore.drop?.index) export const dndIndex = derived(dndStore, $dndStore => $dndStore.drop?.index)
export const dndBounds = derived( export const dndBounds = derived(
dndStore, dndStore,
$dndStore => $dndStore.source?.bounds $dndStore => $dndStore.source?.bounds
) )
export const dndIsNewComponent = derived(
dndStore,
$dndStore => $dndStore.isNewComponent
)

View File

@ -15,7 +15,14 @@ export { uploadStore } from "./uploads.js"
export { rowSelectionStore } from "./rowSelection.js" export { rowSelectionStore } from "./rowSelection.js"
export { blockStore } from "./blocks.js" export { blockStore } from "./blocks.js"
export { environmentStore } from "./environment" export { environmentStore } from "./environment"
export { dndStore, dndIndex, dndParent, dndBounds } from "./dnd" export {
dndStore,
dndIndex,
dndParent,
dndBounds,
dndIsNewComponent,
dndIsDragging,
} from "./dnd"
// Context stores are layered and duplicated, so it is not a singleton // Context stores are layered and duplicated, so it is not a singleton
export { createContextStore } from "./context" export { createContextStore } from "./context"

View File

@ -2,7 +2,7 @@ import { derived } from "svelte/store"
import { routeStore } from "./routes" import { routeStore } from "./routes"
import { builderStore } from "./builder" import { builderStore } from "./builder"
import { appStore } from "./app" import { appStore } from "./app"
import { dndIndex, dndParent } from "./dnd.js" import { dndIndex, dndParent, dndIsNewComponent } from "./dnd.js"
import { RoleUtils } from "@budibase/frontend-core" import { RoleUtils } from "@budibase/frontend-core"
import { findComponentById, findComponentParent } from "../utils/components.js" import { findComponentById, findComponentParent } from "../utils/components.js"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
@ -10,8 +10,22 @@ import { DNDPlaceholderID, DNDPlaceholderType } from "constants"
const createScreenStore = () => { const createScreenStore = () => {
const store = derived( const store = derived(
[appStore, routeStore, builderStore, dndParent, dndIndex], [
([$appStore, $routeStore, $builderStore, $dndParent, $dndIndex]) => { appStore,
routeStore,
builderStore,
dndParent,
dndIndex,
dndIsNewComponent,
],
([
$appStore,
$routeStore,
$builderStore,
$dndParent,
$dndIndex,
$dndIsNewComponent,
]) => {
let activeLayout, activeScreen let activeLayout, activeScreen
let screens let screens
@ -50,8 +64,8 @@ const createScreenStore = () => {
if (activeScreen && $dndParent && $dndIndex != null) { if (activeScreen && $dndParent && $dndIndex != null) {
// Remove selected component from tree if we are moving an existing // Remove selected component from tree if we are moving an existing
// component // component
const { selectedComponentId, draggingNewComponent } = $builderStore const { selectedComponentId } = $builderStore
if (!draggingNewComponent) { if (!$dndIsNewComponent) {
let selectedParent = findComponentParent( let selectedParent = findComponentParent(
activeScreen.props, activeScreen.props,
selectedComponentId selectedComponentId