mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-10 14:35:56 +09:00
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:
1 parent
6ca1f7088a
commit
e03dbecb3b
11 files changed
+99
-111
No files matched your search
@@ -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);
|
||||
};
|
||||
@@ -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,2 +1 @@
|
||||
export * from "./sync-status";
|
||||
export * from "./sync-stores";
|
||||
@@ -1 +0,0 @@
|
||||
export type SyncStatus = "syncing" | "idle";
|
||||
Reference in new issue
Block a user