From 7a079b92f246ae8288fdf0fe796abb58d02bc11c Mon Sep 17 00:00:00 2001 From: Hebi Li Date: Mon, 27 Feb 2023 12:50:46 +0800 Subject: [PATCH 1/2] persist arrows to db --- .../20230227022751_refine_edge/migration.sql | 37 ++++++ api/prisma/schema.prisma | 18 +-- api/src/resolver_repo.ts | 62 +++++++++- api/src/typedefs.ts | 8 ++ ui/src/components/Canvas.tsx | 27 ++++- ui/src/components/nodes/FloatingEdge.tsx | 11 +- ui/src/lib/fetch.tsx | 7 ++ ui/src/lib/store/canvasSlice.tsx | 111 ++++++++++++++---- ui/src/lib/store/repoStateSlice.tsx | 6 +- 9 files changed, 252 insertions(+), 35 deletions(-) create mode 100644 api/prisma/migrations/20230227022751_refine_edge/migration.sql diff --git a/api/prisma/migrations/20230227022751_refine_edge/migration.sql b/api/prisma/migrations/20230227022751_refine_edge/migration.sql new file mode 100644 index 00000000..e3b70d80 --- /dev/null +++ b/api/prisma/migrations/20230227022751_refine_edge/migration.sql @@ -0,0 +1,37 @@ +/* + Warnings: + + - The primary key for the `Edge` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to drop the column `fromId` on the `Edge` table. All the data in the column will be lost. + - You are about to drop the column `toId` on the `Edge` table. All the data in the column will be lost. + - You are about to drop the column `podsId` on the `Repo` table. All the data in the column will be lost. + - Added the required column `sourceId` to the `Edge` table without a default value. This is not possible if the table is not empty. + - Added the required column `targetId` to the `Edge` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "Edge" DROP CONSTRAINT "Edge_fromId_fkey"; + +-- DropForeignKey +ALTER TABLE "Edge" DROP CONSTRAINT "Edge_toId_fkey"; + +-- AlterTable +ALTER TABLE "Edge" DROP CONSTRAINT "Edge_pkey", +DROP COLUMN "fromId", +DROP COLUMN "toId", +ADD COLUMN "repoId" TEXT, +ADD COLUMN "sourceId" TEXT NOT NULL, +ADD COLUMN "targetId" TEXT NOT NULL, +ADD CONSTRAINT "Edge_pkey" PRIMARY KEY ("sourceId", "targetId"); + +-- AlterTable +ALTER TABLE "Repo" DROP COLUMN "podsId"; + +-- AddForeignKey +ALTER TABLE "Edge" ADD CONSTRAINT "Edge_sourceId_fkey" FOREIGN KEY ("sourceId") REFERENCES "Pod"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Edge" ADD CONSTRAINT "Edge_targetId_fkey" FOREIGN KEY ("targetId") REFERENCES "Pod"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Edge" ADD CONSTRAINT "Edge_repoId_fkey" FOREIGN KEY ("repoId") REFERENCES "Repo"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index 7973d646..f9476025 100755 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -69,7 +69,7 @@ model Repo { owner User @relation("OWNER", fields: [userId], references: [id]) userId String pods Pod[] @relation("BELONG") - podsId String[] + edges Edge[] public Boolean @default(false) collaborators User[] @relation("COLLABORATOR") createdAt DateTime @default(now()) @@ -88,12 +88,14 @@ enum PodType { } model Edge { - from Pod @relation("FROM", fields: [fromId], references: [id]) - fromId String - to Pod @relation("TO", fields: [toId], references: [id]) - toId String + source Pod @relation("SOURCE", fields: [sourceId], references: [id]) + sourceId String + target Pod @relation("TARGET", fields: [targetId], references: [id]) + targetId String + repo Repo? @relation(fields: [repoId], references: [id]) + repoId String? - @@id([fromId, toId]) + @@id([sourceId, targetId]) } model Pod { @@ -146,6 +148,6 @@ model Pod { repoId String // this is just a place holder. Not useful - to Edge[] @relation("TO") - from Edge[] @relation("FROM") + source Edge[] @relation("SOURCE") + target Edge[] @relation("TARGET") } diff --git a/api/src/resolver_repo.ts b/api/src/resolver_repo.ts index c9a35f85..94cfff46 100644 --- a/api/src/resolver_repo.ts +++ b/api/src/resolver_repo.ts @@ -151,11 +151,69 @@ async function repo(_, { id }, { userId }) { index: "asc", }, }, + edges: true, }, }); if (!repo) throw Error("Repo not found"); await updateUserRepoData({ userId, repoId: id }); - return repo; + return { + ...repo, + edges: repo.edges.map((edge) => ({ + source: edge.sourceId, + target: edge.targetId, + })), + }; +} + +async function addEdge(_, { source, target }, { userId }) { + if (!userId) throw new Error("Not authenticated."); + const sourcePod = await prisma.pod.findFirst({ where: { id: source } }); + const targetPod = await prisma.pod.findFirst({ where: { id: target } }); + if (!sourcePod || !targetPod) throw new Error("Pods not found."); + if (sourcePod.repoId !== targetPod.repoId) + throw new Error("Pods are not in the same repo."); + await ensureRepoEditAccess({ repoId: sourcePod.repoId, userId }); + await prisma.edge.create({ + data: { + source: { + connect: { + id: source, + }, + }, + target: { + connect: { + id: target, + }, + }, + repo: { + connect: { + id: sourcePod.repoId, + }, + }, + }, + }); + return true; +} + +async function deleteEdge(_, { source, target }, { userId }) { + if (!userId) throw new Error("Not authenticated."); + const sourcePod = await prisma.pod.findFirst({ where: { id: source } }); + const targetPod = await prisma.pod.findFirst({ where: { id: target } }); + if (!sourcePod || !targetPod) throw new Error("Pods not found."); + if (sourcePod.repoId !== targetPod.repoId) + throw new Error("Pods are not in the same repo."); + await ensureRepoEditAccess({ repoId: sourcePod.repoId, userId }); + await prisma.edge.deleteMany({ + where: { + source: { + id: source, + }, + target: { + id: target, + }, + }, + }); + return true; } async function createRepo(_, { id, name, isPublic }, { userId }) { @@ -435,6 +493,8 @@ export default { deleteRepo, updatePod, deletePod, + addEdge, + deleteEdge, addCollaborator, updateVisibility, deleteCollaborator, diff --git a/api/src/typedefs.ts b/api/src/typedefs.ts index bf4ddb8f..5b4e6fbc 100644 --- a/api/src/typedefs.ts +++ b/api/src/typedefs.ts @@ -23,6 +23,7 @@ export const typeDefs = gql` id: ID! name: String pods: [Pod] + edges: [Edge] userId: ID! collaborators: [User] public: Boolean @@ -30,6 +31,11 @@ export const typeDefs = gql` updatedAt: String } + type Edge { + source: String! + target: String! + } + type Pod { id: ID! type: String @@ -121,6 +127,8 @@ export const typeDefs = gql` deletePod(id: String!, toDelete: [String]): Boolean addPods(repoId: String!, pods: [PodInput]): Boolean updatePod(id: String!, repoId: String!, input: PodInput): Boolean + addEdge(source: ID!, target: ID!): Boolean + deleteEdge(source: ID!, target: ID!): Boolean clearUser: Boolean clearRepo: Boolean clearPod: Boolean diff --git a/ui/src/components/Canvas.tsx b/ui/src/components/Canvas.tsx index 42fa6c42..1ce025d5 100644 --- a/ui/src/components/Canvas.tsx +++ b/ui/src/components/Canvas.tsx @@ -21,6 +21,7 @@ import ReactFlow, { MarkerType, Node, ReactFlowProvider, + Edge, } from "reactflow"; import "reactflow/dist/style.css"; @@ -153,10 +154,13 @@ function useInitNodes() { const store = useContext(RepoContext)!; const getPod = useStore(store, (state) => state.getPod); const nodesMap = useStore(store, (state) => state.ydoc.getMap("pods")); + const edgesMap = useStore(store, (state) => state.ydoc.getMap("edges")); + const arrows = useStore(store, (state) => state.arrows); const getId2children = useStore(store, (state) => state.getId2children); const provider = useStore(store, (state) => state.provider); const [loading, setLoading] = useState(true); const updateView = useStore(store, (state) => state.updateView); + const updateEdgeView = useStore(store, (state) => state.updateEdgeView); const adjustLevel = useStore(store, (state) => state.adjustLevel); useEffect(() => { const init = () => { @@ -190,8 +194,25 @@ function useInitNodes() { // NOTE we have to trigger an update here, otherwise the nodes are not // rendered. // triggerUpdate(); + // adjust level and update view adjustLevel(); updateView(); + // handling the arrows + // FIXME TODO verify consistency instead of clear. + edgesMap.clear(); + arrows.forEach(({ target, source }) => { + const edge: Edge = { + id: `${source}_${target}`, + source, + sourceHandle: "top", + target, + targetHandle: "top", + }; + edgesMap.set(edge.id, edge); + // This isn't working. I need to set {edges} manually (from edgesMap) + // reactFlowInstance.addEdges(edge); + }); + updateEdgeView(); setLoading(false); }; @@ -419,8 +440,10 @@ function CanvasImpl() { const onNodesChange = useStore(store, (state) => state.onNodesChange(apolloClient) ); - const onEdgesChange = useStore(store, (state) => state.onEdgesChange); - const onConnect = useStore(store, (state) => state.onConnect); + const onEdgesChange = useStore(store, (state) => + state.onEdgesChange(apolloClient) + ); + const onConnect = useStore(store, (state) => state.onConnect(apolloClient)); const moveIntoScope = useStore(store, (state) => state.moveIntoScope); const setDragHighlight = useStore(store, (state) => state.setDragHighlight); const removeDragHighlight = useStore( diff --git a/ui/src/components/nodes/FloatingEdge.tsx b/ui/src/components/nodes/FloatingEdge.tsx index bb4e7a68..d2b900ae 100644 --- a/ui/src/components/nodes/FloatingEdge.tsx +++ b/ui/src/components/nodes/FloatingEdge.tsx @@ -3,7 +3,14 @@ import { useStore, getStraightPath, EdgeProps } from "reactflow"; import { getEdgeParams } from "./utils"; -function FloatingEdge({ id, source, target, markerEnd, style }: EdgeProps) { +function FloatingEdge({ + id, + source, + target, + markerEnd, + style, + selected, +}: EdgeProps) { const sourceNode = useStore( useCallback((store) => store.nodeInternals.get(source), [source]) ); @@ -30,7 +37,7 @@ function FloatingEdge({ id, source, target, markerEnd, style }: EdgeProps) { className="react-flow__edge-path" d={edgePath} markerEnd={markerEnd} - style={style} + style={selected ? { ...style, stroke: "red" } : style} /> ); } diff --git a/ui/src/lib/fetch.tsx b/ui/src/lib/fetch.tsx index 13f18082..4776451e 100644 --- a/ui/src/lib/fetch.tsx +++ b/ui/src/lib/fetch.tsx @@ -23,6 +23,10 @@ export async function doRemoteLoadRepo(client: ApolloClient, id: string) { lastname } public + edges { + source + target + } pods { id type @@ -70,8 +74,10 @@ export async function doRemoteLoadRepo(client: ApolloClient, id: string) { await client.refetchQueries({ include: ["GetRepos", "GetCollabRepos"] }); // We need to do a deep copy here, because apollo client returned immutable objects. let pods = res.data.repo.pods.map((pod) => ({ ...pod })); + let edges = res.data.repo.edges; return { pods, + edges, name: res.data.repo.name, error: null, userId: res.data.repo.userId, @@ -82,6 +88,7 @@ export async function doRemoteLoadRepo(client: ApolloClient, id: string) { console.log(e); return { pods: [], + edges: [], name: "", error: e, userId: null, diff --git a/ui/src/lib/store/canvasSlice.tsx b/ui/src/lib/store/canvasSlice.tsx index 425e6923..b0d522b2 100644 --- a/ui/src/lib/store/canvasSlice.tsx +++ b/ui/src/lib/store/canvasSlice.tsx @@ -1,3 +1,8 @@ +/** + * See this PR for how the Canvas data is maintained: + * https://github.com/codepod-io/codepod/pull/205 + */ + import { createStore, StateCreator, StoreApi } from "zustand"; import { MyState, Pod } from "."; @@ -5,7 +10,7 @@ import { produce } from "immer"; import { useCallback, useEffect, useState, useContext } from "react"; import { useStore } from "zustand"; -import { ApolloClient, useApolloClient } from "@apollo/client"; +import { ApolloClient, useApolloClient, gql } from "@apollo/client"; import { Transaction, YEvent } from "yjs"; import { match, P } from "ts-pattern"; @@ -239,6 +244,7 @@ export interface CanvasSlice { resetSelection: () => boolean; updateView: () => void; + updateEdgeView: () => void; isPaneFocused: boolean; setPaneFocus: () => void; @@ -269,8 +275,8 @@ export interface CanvasSlice { tempUpdateView: ({ x, y }: XYPosition) => void; onNodesChange: (client: ApolloClient) => OnNodesChange; - onEdgesChange: OnEdgesChange; - onConnect: OnConnect; + onEdgesChange: (client: ApolloClient) => OnEdgesChange; + onConnect: (client: ApolloClient) => OnConnect; } export const createCanvasSlice: StateCreator = ( @@ -372,6 +378,10 @@ export const createCanvasSlice: StateCreator = ( } set({ nodes }); }, + updateEdgeView: () => { + const edgesMap = get().ydoc.getMap("edges"); + set({ edges: Array.from(edgesMap.values()).filter((e) => e) }); + }, addNode: (type, position, parent = "ROOT") => { let nodesMap = get().ydoc.getMap("pods"); @@ -736,30 +746,89 @@ export const createCanvasSlice: StateCreator = ( }); get().updateView(); }, - onEdgesChange: (changes: EdgeChange[]) => { + onEdgesChange: (client) => (changes: EdgeChange[]) => { + // TODO sync with remote peer + const edgesMap = get().ydoc.getMap("edges"); + // apply the changes. Especially for the "select" change. set({ edges: applyEdgeChanges(changes, get().edges), }); + // FIXME this will create edge with IDs. But I probably would love to control the IDs to save in DB. + changes.forEach((change) => { + // console.log("=== change", change.type, change); + // TODO update nodesMap to sync with remote peers + switch (change.type) { + case "add": + break; + case "remove": + const edge = edgesMap.get(change.id); + if (!edge) throw new Error("Edge not found"); + remoteDeleteEdge({ + source: edge.source, + target: edge.target, + client, + }); + edgesMap.delete(change.id); + break; + case "reset": + break; + case "select": + break; + default: + throw new Error("NO REACH"); + } + }); }, - onConnect: (connection: Connection) => { - set({ - edges: addEdge( - { - ...connection, - markerEnd: { - type: MarkerType.ArrowClosed, - color: "black", - }, - style: { - stroke: "black", - strokeWidth: 3, - }, - }, - get().edges - ), + onConnect: (client) => (connection: Connection) => { + const edgesMap = get().ydoc.getMap("edges"); + if (!connection.source || !connection.target) return null; + remoteAddEdge({ + source: connection.source, + target: connection.target, + client, }); + const edge = { + id: `${connection.source}_${connection.target}`, + source: connection.source, + sourceHandle: "top", + target: connection.target, + targetHandle: "top", + }; + edgesMap.set(edge.id, edge); + get().updateEdgeView(); }, - setPaneFocus: () => set({ isPaneFocused: true }), setPaneBlur: () => set({ isPaneFocused: false }), }); + +async function remoteAddEdge({ client, source, target }) { + const mutation = gql` + mutation addEdge($source: ID!, $target: ID!) { + addEdge(source: $source, target: $target) + } + `; + await client.mutate({ + mutation, + variables: { + source, + target, + }, + }); + return true; +} + +async function remoteDeleteEdge({ client, source, target }) { + const mutation = gql` + mutation deleteEdge($source: ID!, $target: ID!) { + deleteEdge(source: $source, target: $target) + } + `; + await client.mutate({ + mutation, + variables: { + source, + target, + }, + }); + return true; +} diff --git a/ui/src/lib/store/repoStateSlice.tsx b/ui/src/lib/store/repoStateSlice.tsx index 0fe848b9..99354d0d 100644 --- a/ui/src/lib/store/repoStateSlice.tsx +++ b/ui/src/lib/store/repoStateSlice.tsx @@ -32,6 +32,8 @@ console.log("yjs server url: ", serverURL); export interface RepoStateSlice { pods: Record; + // From source pod id to target pod id. + arrows: { source: string; target: string }[]; id2parent: Record; id2children: Record; setSessionId: (sessionId: string) => void; @@ -81,6 +83,7 @@ export const createRepoStateSlice: StateCreator< RepoStateSlice > = (set, get) => ({ pods: {}, + arrows: [], id2parent: {}, id2children: {}, error: null, @@ -284,7 +287,7 @@ export const createRepoStateSlice: StateCreator< function loadRepo(set, get) { return async (client, id) => { - const { pods, name, error, userId, collaborators, isPublic } = + const { pods, edges, name, error, userId, collaborators, isPublic } = await doRemoteLoadRepo(client, id); set( produce((state: MyState) => { @@ -296,6 +299,7 @@ function loadRepo(set, get) { return; } state.pods = normalize(pods); + state.arrows = edges; state.repoName = name; state.isPublic = isPublic; state.collaborators = collaborators; From 3627d7b5067f916209344c035f202df8be54784c Mon Sep 17 00:00:00 2001 From: Hebi Li Date: Mon, 27 Feb 2023 16:35:03 +0800 Subject: [PATCH 2/2] support yjs sync for add/delete arrows --- ui/src/components/Canvas.tsx | 91 ++++++++++++++++++++++++-------- ui/src/lib/nodes.tsx | 28 +++++++++- ui/src/lib/store/canvasSlice.tsx | 2 +- 3 files changed, 97 insertions(+), 24 deletions(-) diff --git a/ui/src/components/Canvas.tsx b/ui/src/components/Canvas.tsx index 1ce025d5..977687e7 100644 --- a/ui/src/components/Canvas.tsx +++ b/ui/src/components/Canvas.tsx @@ -34,7 +34,7 @@ import { useStore } from "zustand"; import { RepoContext } from "../lib/store"; import { dbtype2nodetype, nodetype2dbtype } from "../lib/utils"; -import { useYjsObserver } from "../lib/nodes"; +import { useEdgesYjsObserver, useYjsObserver } from "../lib/nodes"; import { useApolloClient } from "@apollo/client"; import { CanvasContextMenu } from "./CanvasContextMenu"; @@ -150,6 +150,49 @@ function verifyConsistency(nodes: Node[], nodesMap: YMap) { return true; } +function verifyEdgeConsistency(edges: Edge[], edgesMap: YMap) { + let keys = new Set(edgesMap.keys()); + let edgesMap2 = new Map(); + edges.forEach((edge) => edgesMap2.set(edge.id, edge)); + let keys2 = new Set(edgesMap2.keys()); + if (keys.size !== keys2.size) { + console.error("key sizes are not the same", keys, keys2); + return false; + } + for (let i = 0; i < keys.size; i++) { + if (keys[i] !== keys2[i]) { + console.error("keys are not the same", keys, keys2); + return false; + } + } + // verify the values + for (let key of Array.from(keys)) { + let edge1 = edgesMap.get(key); + let edge2 = edgesMap2.get(key); + if (!edge1) { + console.error("edge1 is undefined"); + return false; + } + if (!edge2) { + console.error("edge2 is undefined"); + return false; + } + if (edge1.id !== edge2.id) { + console.error("edge id are not the same", edge1.id, edge2.id, "key", key); + return false; + } + if (edge1.source !== edge2.source) { + console.error("edge source are not the same", edge1.source, edge2.source); + return false; + } + if (edge1.target !== edge2.target) { + console.error("edge target are not the same", edge1.target, edge2.target); + return false; + } + } + return true; +} + function useInitNodes() { const store = useContext(RepoContext)!; const getPod = useStore(store, (state) => state.getPod); @@ -177,15 +220,7 @@ function useInitNodes() { let nodesMap2 = new Map(); nodes.forEach((node) => nodesMap2.set(node.id, node)); // Not only should we set nodes, but also delete. - nodesMap.forEach((node, key) => { - if (!nodesMap2.has(key)) { - console.error(`Yjs has key ${key} that is not in database.`); - // FIXME CAUTION This will delete the node in the database! Be - // careful! For now, just log errors and do not delete. - // - nodesMap.delete(key); - } - }); + nodesMap.clear(); // add the nodes, so that the nodesMap is consistent with the database. nodes.forEach((node) => { nodesMap.set(node.id, node); @@ -198,20 +233,31 @@ function useInitNodes() { adjustLevel(); updateView(); // handling the arrows - // FIXME TODO verify consistency instead of clear. - edgesMap.clear(); - arrows.forEach(({ target, source }) => { - const edge: Edge = { - id: `${source}_${target}`, + isConsistent = verifyEdgeConsistency( + arrows.map(({ source, target }) => ({ source, - sourceHandle: "top", target, - targetHandle: "top", - }; - edgesMap.set(edge.id, edge); - // This isn't working. I need to set {edges} manually (from edgesMap) - // reactFlowInstance.addEdges(edge); - }); + id: `${source}_${target}`, + })), + edgesMap + ); + if (!isConsistent) { + console.warn("The yjs server is not consistent with the database."); + // delete the old keys + edgesMap.clear(); + arrows.forEach(({ target, source }) => { + const edge: Edge = { + id: `${source}_${target}`, + source, + sourceHandle: "top", + target, + targetHandle: "top", + }; + edgesMap.set(edge.id, edge); + // This isn't working. I need to set {edges} manually (from edgesMap) + // reactFlowInstance.addEdges(edge); + }); + } updateEdgeView(); setLoading(false); }; @@ -412,6 +458,7 @@ function CanvasImplWrap() { const reactFlowWrapper = useRef(null); useYjsObserver(); + useEdgesYjsObserver(); usePaste(reactFlowWrapper); useCut(reactFlowWrapper); diff --git a/ui/src/lib/nodes.tsx b/ui/src/lib/nodes.tsx index 019d7032..6f4d7bdd 100644 --- a/ui/src/lib/nodes.tsx +++ b/ui/src/lib/nodes.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState, useContext } from "react"; -import { applyNodeChanges, Node } from "reactflow"; +import { applyNodeChanges, Edge, Node } from "reactflow"; import { RepoContext } from "./store"; import { nodetype2dbtype } from "./utils"; import { useStore } from "zustand"; @@ -78,3 +78,29 @@ export function useYjsObserver() { }; }, [addPod, deletePod, nodesMap, resetSelection, setPodGeo, updateView]); } + +export function useEdgesYjsObserver() { + const store = useContext(RepoContext); + if (!store) throw new Error("Missing BearContext.Provider in the tree"); + const addPod = useStore(store, (state) => state.addPod); + const deletePod = useStore(store, (state) => state.deletePod); + const setPodGeo = useStore(store, (state) => state.setPodGeo); + const ydoc = useStore(store, (state) => state.ydoc); + const edgesMap = ydoc.getMap("edges"); + const updateEdgeView = useStore(store, (state) => state.updateEdgeView); + const resetSelection = useStore(store, (state) => state.resetSelection); + + useEffect(() => { + const observer = (YMapEvent: YEvent, transaction: Transaction) => { + if (transaction.local) return; + // console.log("== Edge observer", transaction); + updateEdgeView(); + }; + + edgesMap.observe(observer); + + return () => { + edgesMap.unobserve(observer); + }; + }, [addPod, deletePod, edgesMap, resetSelection, setPodGeo, updateEdgeView]); +} diff --git a/ui/src/lib/store/canvasSlice.tsx b/ui/src/lib/store/canvasSlice.tsx index b0d522b2..a2558455 100644 --- a/ui/src/lib/store/canvasSlice.tsx +++ b/ui/src/lib/store/canvasSlice.tsx @@ -755,7 +755,7 @@ export const createCanvasSlice: StateCreator = ( }); // FIXME this will create edge with IDs. But I probably would love to control the IDs to save in DB. changes.forEach((change) => { - // console.log("=== change", change.type, change); + // console.log("=== onEdgeChange", change.type, change); // TODO update nodesMap to sync with remote peers switch (change.type) { case "add":