developing #10
|
|
@ -527,6 +527,12 @@ function FlowchartEditorInner({ projectId, projectName, userId, userDisplayName,
|
||||||
|
|
||||||
const [characters, setCharacters] = useState<Character[]>(migratedData.characters)
|
const [characters, setCharacters] = useState<Character[]>(migratedData.characters)
|
||||||
const [variables, setVariables] = useState<Variable[]>(migratedData.variables)
|
const [variables, setVariables] = useState<Variable[]>(migratedData.variables)
|
||||||
|
|
||||||
|
// Refs to always have the latest characters/variables for the CRDT persist callback
|
||||||
|
const charactersRef = useRef(characters)
|
||||||
|
const variablesRef = useRef(variables)
|
||||||
|
charactersRef.current = characters
|
||||||
|
variablesRef.current = variables
|
||||||
const [showSettings, setShowSettings] = useState(false)
|
const [showSettings, setShowSettings] = useState(false)
|
||||||
const [showShare, setShowShare] = useState(false)
|
const [showShare, setShowShare] = useState(false)
|
||||||
const [showHistory, setShowHistory] = useState(false)
|
const [showHistory, setShowHistory] = useState(false)
|
||||||
|
|
@ -540,7 +546,6 @@ function FlowchartEditorInner({ projectId, projectName, userId, userDisplayName,
|
||||||
const realtimeRef = useRef<RealtimeConnection | null>(null)
|
const realtimeRef = useRef<RealtimeConnection | null>(null)
|
||||||
const crdtRef = useRef<CRDTManager | null>(null)
|
const crdtRef = useRef<CRDTManager | null>(null)
|
||||||
const auditRef = useRef<AuditTrailRecorder | null>(null)
|
const auditRef = useRef<AuditTrailRecorder | null>(null)
|
||||||
const isRemoteUpdateRef = useRef(false)
|
|
||||||
const cursorThrottleRef = useRef<number>(0)
|
const cursorThrottleRef = useRef<number>(0)
|
||||||
const [collaborationNotifications, setCollaborationNotifications] = useState<CollaborationNotification[]>([])
|
const [collaborationNotifications, setCollaborationNotifications] = useState<CollaborationNotification[]>([])
|
||||||
const [nodeLocks, setNodeLocks] = useState<Map<string, NodeLockInfo>>(new Map())
|
const [nodeLocks, setNodeLocks] = useState<Map<string, NodeLockInfo>>(new Map())
|
||||||
|
|
@ -554,14 +559,10 @@ function FlowchartEditorInner({ projectId, projectName, userId, userDisplayName,
|
||||||
|
|
||||||
const crdtManager = new CRDTManager({
|
const crdtManager = new CRDTManager({
|
||||||
onNodesChange: (crdtNodes: FlowchartNode[]) => {
|
onNodesChange: (crdtNodes: FlowchartNode[]) => {
|
||||||
isRemoteUpdateRef.current = true
|
|
||||||
setNodes(toReactFlowNodes(crdtNodes))
|
setNodes(toReactFlowNodes(crdtNodes))
|
||||||
isRemoteUpdateRef.current = false
|
|
||||||
},
|
},
|
||||||
onEdgesChange: (crdtEdges: FlowchartEdge[]) => {
|
onEdgesChange: (crdtEdges: FlowchartEdge[]) => {
|
||||||
isRemoteUpdateRef.current = true
|
|
||||||
setEdges(toReactFlowEdges(crdtEdges))
|
setEdges(toReactFlowEdges(crdtEdges))
|
||||||
isRemoteUpdateRef.current = false
|
|
||||||
},
|
},
|
||||||
onPersist: async (persistNodes: FlowchartNode[], persistEdges: FlowchartEdge[]) => {
|
onPersist: async (persistNodes: FlowchartNode[], persistEdges: FlowchartEdge[]) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -571,8 +572,8 @@ function FlowchartEditorInner({ projectId, projectName, userId, userDisplayName,
|
||||||
flowchart_data: {
|
flowchart_data: {
|
||||||
nodes: persistNodes,
|
nodes: persistNodes,
|
||||||
edges: persistEdges,
|
edges: persistEdges,
|
||||||
characters,
|
characters: charactersRef.current,
|
||||||
variables,
|
variables: variablesRef.current,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.eq('id', projectId)
|
.eq('id', projectId)
|
||||||
|
|
@ -633,6 +634,9 @@ function FlowchartEditorInner({ projectId, projectName, userId, userDisplayName,
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
onCRDTUpdate: (update: number[]) => {
|
||||||
|
crdtManager.applyRemoteUpdate(update)
|
||||||
|
},
|
||||||
onChannelSubscribed: (channel) => {
|
onChannelSubscribed: (channel) => {
|
||||||
crdtManager.connectChannel(channel)
|
crdtManager.connectChannel(channel)
|
||||||
},
|
},
|
||||||
|
|
@ -678,7 +682,6 @@ function FlowchartEditorInner({ projectId, projectName, userId, userDisplayName,
|
||||||
}, [edges])
|
}, [edges])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isRemoteUpdateRef.current) return
|
|
||||||
crdtRef.current?.updateNodes(nodesForCRDT)
|
crdtRef.current?.updateNodes(nodesForCRDT)
|
||||||
if (!isRevertingRef.current) {
|
if (!isRevertingRef.current) {
|
||||||
auditRef.current?.recordNodeChanges(nodesForCRDT)
|
auditRef.current?.recordNodeChanges(nodesForCRDT)
|
||||||
|
|
@ -686,7 +689,6 @@ function FlowchartEditorInner({ projectId, projectName, userId, userDisplayName,
|
||||||
}, [nodesForCRDT])
|
}, [nodesForCRDT])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isRemoteUpdateRef.current) return
|
|
||||||
crdtRef.current?.updateEdges(edgesForCRDT)
|
crdtRef.current?.updateEdges(edgesForCRDT)
|
||||||
if (!isRevertingRef.current) {
|
if (!isRevertingRef.current) {
|
||||||
auditRef.current?.recordEdgeChanges(edgesForCRDT)
|
auditRef.current?.recordEdgeChanges(edgesForCRDT)
|
||||||
|
|
|
||||||
|
|
@ -57,25 +57,23 @@ export class CRDTManager {
|
||||||
}, 'init')
|
}, 'init')
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Connect to a Supabase Realtime channel for syncing updates */
|
/** Connect to a Supabase Realtime channel for outbound broadcasts */
|
||||||
connectChannel(channel: RealtimeChannel): void {
|
connectChannel(channel: RealtimeChannel): void {
|
||||||
this.channel = channel
|
this.channel = channel
|
||||||
|
}
|
||||||
|
|
||||||
// Listen for broadcast updates from other clients
|
/** Apply a remote CRDT update received via broadcast */
|
||||||
channel.on('broadcast', { event: BROADCAST_EVENT }, (payload) => {
|
applyRemoteUpdate(updateData: number[]): void {
|
||||||
if (this.isDestroyed) return
|
if (this.isDestroyed) return
|
||||||
const data = payload.payload as { update?: number[] } | undefined
|
const update = new Uint8Array(updateData)
|
||||||
if (data?.update) {
|
this.isApplyingRemote = true
|
||||||
const update = new Uint8Array(data.update)
|
Y.applyUpdate(this.doc, update, 'remote')
|
||||||
this.isApplyingRemote = true
|
this.isApplyingRemote = false
|
||||||
Y.applyUpdate(this.doc, update, 'remote')
|
// Notify React state of remote changes
|
||||||
this.isApplyingRemote = false
|
this.notifyNodesChange()
|
||||||
// Notify React state of remote changes
|
this.notifyEdgesChange()
|
||||||
this.notifyNodesChange()
|
// Note: we do NOT schedulePersist here. Only the originating client
|
||||||
this.notifyEdgesChange()
|
// persists its own changes to avoid write races and stale data overwrites.
|
||||||
this.schedulePersist()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Apply local node changes to the Yjs document */
|
/** Apply local node changes to the Yjs document */
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ type RealtimeCallbacks = {
|
||||||
onChannelSubscribed?: (channel: RealtimeChannel) => void
|
onChannelSubscribed?: (channel: RealtimeChannel) => void
|
||||||
onCursorUpdate?: (cursor: RemoteCursor) => void
|
onCursorUpdate?: (cursor: RemoteCursor) => void
|
||||||
onNodeLockUpdate?: (lock: NodeLock | null, userId: string) => void
|
onNodeLockUpdate?: (lock: NodeLock | null, userId: string) => void
|
||||||
|
onCRDTUpdate?: (update: number[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const HEARTBEAT_INTERVAL_MS = 30_000
|
const HEARTBEAT_INTERVAL_MS = 30_000
|
||||||
|
|
@ -131,6 +132,11 @@ export class RealtimeConnection {
|
||||||
this.callbacks.onNodeLockUpdate?.(null, payload.userId)
|
this.callbacks.onNodeLockUpdate?.(null, payload.userId)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.on('broadcast', { event: 'yjs-update' }, ({ payload }) => {
|
||||||
|
if (payload?.update) {
|
||||||
|
this.callbacks.onCRDTUpdate?.(payload.update)
|
||||||
|
}
|
||||||
|
})
|
||||||
.subscribe(async (status) => {
|
.subscribe(async (status) => {
|
||||||
if (this.isDestroyed) return
|
if (this.isDestroyed) return
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue