developing #10

Merged
GHMiranda merged 64 commits from developing into master 2026-01-25 00:37:11 +00:00
3 changed files with 31 additions and 25 deletions
Showing only changes of commit 34815d70ee - Show all commits

View File

@ -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)

View File

@ -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 */

View File

@ -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