Sync patches with server in designer (#813)

For security reasons it's important to prevent canvas from having write
access to db.

Here I moved old sync logic to designer folder and got rid of one more
event in pubsub, used atom directly.
This commit is contained in:
Bogdan Chadkin authored and GitHub committed 2023-01-19 13:17:02 +03:00
1 parent 6ca1f7088a
commit e03dbecb3b
11 files changed
+99 -111

No files matched your search

+3 -11
View File
@@ -1,6 +1,6 @@
import { useCallback, useMemo, useState } from "react";
import store from "immerhin";
import type { Build, CanvasData } from "@webstudio-is/project";
import type { CanvasData } from "@webstudio-is/project";
import {
createElementsTree,
useAllUserProps,
@@ -9,7 +9,6 @@ import {
customComponentsMeta,
setParams,
type OnChangeChildren,
type Tree,
} from "@webstudio-is/react-sdk";
import { publish, useSubscribe } from "~/shared/pubsub";
import { registerContainers, useCanvasStore } from "~/shared/sync";
@@ -26,7 +25,6 @@ import {
import { useManageDesignModeStyles, GlobalStyles } from "./shared/styles";
import { useTrackSelectedElement } from "./shared/use-track-selected-element";
import { WrapperComponentDev } from "./features/wrapper-component";
import { useSync } from "./shared/sync";
import {
rootInstanceContainer,
useBreakpoints,
@@ -125,19 +123,13 @@ const useCopyPaste = () => {
});
};
type DesignModeProps = {
treeId: Tree["id"];
buildId: Build["id"];
};
const DesignMode = ({ treeId, buildId }: DesignModeProps) => {
const DesignMode = () => {
useManageDesignModeStyles();
usePublishSelectedInstanceData();
useInsertInstance();
useReparentInstance();
useDeleteInstance();
useTrackSelectedElement();
useSync({ buildId, treeId });
useUpdateSelectedInstance();
useUnselectInstance();
usePublishScrollState();
@@ -195,7 +187,7 @@ export const Canvas = ({ data }: CanvasProps): JSX.Element | null => {
return (
<>
<GlobalStyles assets={assets} />
<DesignMode treeId={data.tree.id} buildId={data.buildId} />
<DesignMode />
{elements}
</>
);
@@ -1 +0,0 @@
export * from "./use-sync";
@@ -1,46 +0,0 @@
import { type SyncStatus } from "~/shared/sync";
import { publish } from "~/shared/pubsub";
declare module "~/shared/pubsub" {
export interface PubsubMap {
syncStatus: SyncStatus;
}
}
// @todo because of limitation with nested models and saving tree as a Json type
// we load that entire Json to manipulate and if a second request happens before the first one has written
// the result into the db, the second request will have outdated tree and will then write the outdated tree back
// to the database
// For now we will just have to queue the changes for all tree mutations.
type Job = () => Promise<unknown>;
const queue: Array<Job> = [];
export const enqueue = (job: Job) => {
queue.push(job);
if (isInProgress === false) {
dequeue();
}
};
let isInProgress = false;
const dequeue = () => {
const job = queue.shift();
if (job) {
isInProgress = true;
publish({
type: "syncStatus",
payload: "syncing",
});
job().finally(() => {
isInProgress = false;
publish({
type: "syncStatus",
payload: "idle",
});
dequeue();
});
}
};
@@ -1,36 +0,0 @@
import { useInterval } from "react-use";
import { sync } from "immerhin";
import { enqueue } from "./queue";
import type { Build } from "@webstudio-is/project";
import { Tree } from "@webstudio-is/react-sdk";
import { restPatchPath } from "~/shared/router-utils";
export const useSync = ({
treeId,
buildId,
}: {
buildId: Build["id"];
treeId: Tree["id"];
}) => {
useInterval(() => {
const entries = sync();
if (entries.length === 0) {
return;
}
// @todo this entire queueing logic needs to be gone, it's a workaround,
// because prisma can't do atomic updates yet with sandbox documents
// and backend fetches and updates big objects, so if we send quickly,
// we end up overwriting things
enqueue(() =>
fetch(restPatchPath(), {
method: "post",
body: JSON.stringify({
transactions: entries,
treeId: treeId,
buildId: buildId,
}),
})
);
}, 1000);
};
+6 -7
View File
@@ -7,6 +7,7 @@ import {
} from "@webstudio-is/project";
import { Box, type CSS, Flex, Grid } from "@webstudio-is/design-system";
import { registerContainers, useDesignerStore } from "~/shared/sync";
import { useSyncServer } from "./shared/sync-server";
import interStyles from "~/shared/font-faces/inter.css";
import { SidebarLeft } from "./features/sidebar-left";
import { Inspector } from "./features/inspector";
@@ -16,7 +17,6 @@ import {
useProject,
useCurrentPageId,
useSelectedInstanceData,
useSyncStatus,
} from "./shared/nano-states";
import { Topbar } from "./features/topbar";
import designerStyles from "./designer.css";
@@ -64,11 +64,6 @@ const useSubscribeHoveredInstanceData = () => {
useSubscribe("hoverInstance", setValue);
};
const useSubscribeSyncStatus = () => {
const [, setValue] = useSyncStatus();
useSubscribe("syncStatus", setValue);
};
const useSetProject = (project: Project) => {
const [, setProject] = useProject();
useEffect(() => {
@@ -286,6 +281,8 @@ export type DesignerProps = {
project: Project;
pages: Pages;
pageId: string;
treeId: string;
buildId: string;
buildOrigin: string;
};
@@ -293,9 +290,10 @@ export const Designer = ({
project,
pages,
pageId,
treeId,
buildId,
buildOrigin,
}: DesignerProps) => {
useSubscribeSyncStatus();
useSubscribeSelectedInstanceData();
useSubscribeHoveredInstanceData();
useSubscribeBreakpoints();
@@ -304,6 +302,7 @@ export const Designer = ({
useSetCurrentPageId(pageId);
const [publish, publishRef] = usePublish();
useDesignerStore(publish);
useSyncServer({ buildId, treeId });
usePublishAssets(publish);
const [isPreviewMode] = useIsPreviewMode();
usePublishShortcuts(publish);
@@ -1,4 +1,6 @@
import { useStore } from "@nanostores/react";
import {
theme,
Flex,
keyframes,
AccessibleIcon,
@@ -6,8 +8,7 @@ import {
Box,
} from "@webstudio-is/design-system";
import { CheckIcon, DotsHorizontalIcon } from "@webstudio-is/icons";
import { useSyncStatus } from "../../shared/nano-states";
import { theme } from "@webstudio-is/design-system";
import { syncStatus } from "~/designer/shared/sync-server";
const iconSize = 15;
@@ -39,7 +40,7 @@ const AnimatedDotsIcon = () => {
};
export const SyncStatus = () => {
const [status] = useSyncStatus();
const status = useStore(syncStatus);
return (
<Flex align="center" justify="center">
<AccessibleIcon label={`Sync status: ${status}`}>
@@ -5,7 +5,6 @@ import {
type SelectedInstanceData,
type HoveredInstanceData,
} from "@webstudio-is/project";
import { type SyncStatus } from "~/shared/sync";
import { type Pages, type Project } from "@webstudio-is/project";
import type { AssetContainer, DeletingAssetContainer } from "../assets";
@@ -42,9 +41,6 @@ export const useCanvasWidth = () => useValue(canvasWidthContainer);
const canvasRectContainer = atom<DOMRect | undefined>();
export const useCanvasRect = () => useValue(canvasRectContainer);
const syncStatusContainer = atom<SyncStatus>("idle");
export const useSyncStatus = () => useValue(syncStatusContainer);
const assetsContainer = atom<Array<AssetContainer | DeletingAssetContainer>>(
[]
);
@@ -0,0 +1,79 @@
import { atom } from "nanostores";
import { sync } from "immerhin";
import type { Build } from "@webstudio-is/project";
import type { Tree } from "@webstudio-is/react-sdk";
import { restPatchPath } from "~/shared/router-utils";
import { useEffect } from "react";
export type SyncStatus = "syncing" | "idle";
export const syncStatus = atom<SyncStatus>("idle");
// @todo because of limitation with nested models and saving tree as a Json type
// we load that entire Json to manipulate and if a second request happens before the first one has written
// the result into the db, the second request will have outdated tree and will then write the outdated tree back
// to the database
// For now we will just have to queue the changes for all tree mutations.
type Job = () => Promise<unknown>;
const queue: Array<Job> = [];
export const enqueue = (job: Job) => {
queue.push(job);
if (isInProgress === false) {
dequeue();
}
};
let isInProgress = false;
const dequeue = () => {
const job = queue.shift();
if (job) {
isInProgress = true;
syncStatus.set("syncing");
job().finally(() => {
isInProgress = false;
syncStatus.set("idle");
dequeue();
});
}
};
export const useSyncServer = ({
treeId,
buildId,
}: {
buildId: Build["id"];
treeId: Tree["id"];
}) => {
useEffect(() => {
const intervalId = setInterval(() => {
// @todo prevent clearing queue in case request is failed
const entries = sync();
if (entries.length === 0) {
return;
}
// @todo this entire queueing logic needs to be gone, it's a workaround,
// because prisma can't do atomic updates yet with sandbox documents
// and backend fetches and updates big objects, so if we send quickly,
// we end up overwriting things
enqueue(() =>
// @todo start next round only when the request is completed
fetch(restPatchPath(), {
method: "post",
body: JSON.stringify({
transactions: entries,
treeId: treeId,
buildId: buildId,
}),
})
);
}, 1000);
return () => {
clearInterval(intervalId);
};
}, [treeId, buildId]);
};
@@ -27,10 +27,16 @@ export const loader = async ({
const devBuild = await db.build.loadByProjectId(project.id, "dev");
const pages = devBuild.pages;
const page =
pages.pages.find((page) => page.id === pageIdParam) ?? pages.homePage;
return {
project,
pages: devBuild.pages,
pages,
pageId: pageIdParam || devBuild.pages.homePage.id,
treeId: page.treeId,
buildId: devBuild.id,
buildOrigin: getBuildOrigin(request),
};
};
-1
View File
@@ -1,2 +1 @@
export * from "./sync-status";
export * from "./sync-stores";
@@ -1 +0,0 @@
export type SyncStatus = "syncing" | "idle";