diff --git a/apps/designer/app/canvas/canvas.tsx b/apps/designer/app/canvas/canvas.tsx
index 60e4be6e36..cbfb0d6ea9 100644
--- a/apps/designer/app/canvas/canvas.tsx
+++ b/apps/designer/app/canvas/canvas.tsx
@@ -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 (
<>
-
+
{elements}
>
);
diff --git a/apps/designer/app/canvas/shared/sync/index.ts b/apps/designer/app/canvas/shared/sync/index.ts
deleted file mode 100644
index 375f89800c..0000000000
--- a/apps/designer/app/canvas/shared/sync/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from "./use-sync";
diff --git a/apps/designer/app/canvas/shared/sync/queue.ts b/apps/designer/app/canvas/shared/sync/queue.ts
deleted file mode 100644
index 18e0f4a3ec..0000000000
--- a/apps/designer/app/canvas/shared/sync/queue.ts
+++ /dev/null
@@ -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;
-
-const queue: Array = [];
-
-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();
- });
- }
-};
diff --git a/apps/designer/app/canvas/shared/sync/use-sync.ts b/apps/designer/app/canvas/shared/sync/use-sync.ts
deleted file mode 100644
index 4ee346f2bd..0000000000
--- a/apps/designer/app/canvas/shared/sync/use-sync.ts
+++ /dev/null
@@ -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);
-};
diff --git a/apps/designer/app/designer/designer.tsx b/apps/designer/app/designer/designer.tsx
index ecbb925eec..f4eadedca6 100644
--- a/apps/designer/app/designer/designer.tsx
+++ b/apps/designer/app/designer/designer.tsx
@@ -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);
diff --git a/apps/designer/app/designer/features/topbar/sync-status.tsx b/apps/designer/app/designer/features/topbar/sync-status.tsx
index 1512d9818a..d570539b40 100644
--- a/apps/designer/app/designer/features/topbar/sync-status.tsx
+++ b/apps/designer/app/designer/features/topbar/sync-status.tsx
@@ -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 (
diff --git a/apps/designer/app/designer/shared/nano-states/index.ts b/apps/designer/app/designer/shared/nano-states/index.ts
index b604f3d7a7..418fa14aff 100644
--- a/apps/designer/app/designer/shared/nano-states/index.ts
+++ b/apps/designer/app/designer/shared/nano-states/index.ts
@@ -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();
export const useCanvasRect = () => useValue(canvasRectContainer);
-const syncStatusContainer = atom("idle");
-export const useSyncStatus = () => useValue(syncStatusContainer);
-
const assetsContainer = atom>(
[]
);
diff --git a/apps/designer/app/designer/shared/sync-server.ts b/apps/designer/app/designer/shared/sync-server.ts
new file mode 100644
index 0000000000..f30bfc962e
--- /dev/null
+++ b/apps/designer/app/designer/shared/sync-server.ts
@@ -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("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;
+
+const queue: Array = [];
+
+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]);
+};
diff --git a/apps/designer/app/routes/designer/$projectId.tsx b/apps/designer/app/routes/designer/$projectId.tsx
index 40de33a232..f15279e8ad 100644
--- a/apps/designer/app/routes/designer/$projectId.tsx
+++ b/apps/designer/app/routes/designer/$projectId.tsx
@@ -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),
};
};
diff --git a/apps/designer/app/shared/sync/index.ts b/apps/designer/app/shared/sync/index.ts
index e2f02329d0..9e9c46e6bd 100644
--- a/apps/designer/app/shared/sync/index.ts
+++ b/apps/designer/app/shared/sync/index.ts
@@ -1,2 +1 @@
-export * from "./sync-status";
export * from "./sync-stores";
diff --git a/apps/designer/app/shared/sync/sync-status.ts b/apps/designer/app/shared/sync/sync-status.ts
deleted file mode 100644
index 06c827be78..0000000000
--- a/apps/designer/app/shared/sync/sync-status.ts
+++ /dev/null
@@ -1 +0,0 @@
-export type SyncStatus = "syncing" | "idle";