shape editing
This commit is contained in:
@@ -105,67 +105,105 @@ class SyncRegistry:
|
||||
|
||||
logger.info(f"apply_client_patch: Current backend seq={entry.seq}")
|
||||
|
||||
if client_base_seq == entry.seq:
|
||||
# No conflict
|
||||
logger.info("apply_client_patch: No conflict - applying patch directly")
|
||||
current_state = entry.model.model_dump(mode="json")
|
||||
logger.info(f"apply_client_patch: Current state before patch: {current_state}")
|
||||
new_state = jsonpatch.apply_patch(current_state, patch)
|
||||
logger.info(f"apply_client_patch: New state after patch: {new_state}")
|
||||
self._update_model(entry.model, new_state)
|
||||
try:
|
||||
if client_base_seq == entry.seq:
|
||||
# No conflict
|
||||
logger.info("apply_client_patch: No conflict - applying patch directly")
|
||||
current_state = entry.model.model_dump(mode="json")
|
||||
logger.info(f"apply_client_patch: Current state before patch: {current_state}")
|
||||
try:
|
||||
new_state = jsonpatch.apply_patch(current_state, patch)
|
||||
logger.info(f"apply_client_patch: New state after patch: {new_state}")
|
||||
self._update_model(entry.model, new_state)
|
||||
|
||||
entry.commit_patch(patch)
|
||||
logger.info(f"apply_client_patch: Patch committed, new seq={entry.seq}")
|
||||
# Don't broadcast back to client - they already have this change
|
||||
# Broadcasting would cause an infinite loop
|
||||
logger.info("apply_client_patch: Not broadcasting back to originating client")
|
||||
|
||||
elif client_base_seq < entry.seq:
|
||||
# Conflict! Frontend wins.
|
||||
# 1. Get backend patches since client_base_seq
|
||||
backend_patches = []
|
||||
for seq, p in entry.history:
|
||||
if seq > client_base_seq:
|
||||
backend_patches.append(p)
|
||||
|
||||
# 2. Apply frontend patch first to the state at client_base_seq
|
||||
# But we only have the current authoritative model.
|
||||
# "Apply the frontend patch first to the model (frontend wins)"
|
||||
# "Re-apply the backend deltas that do not overlap the frontend's changed paths on top"
|
||||
|
||||
# Let's get the state as it was at client_base_seq if possible?
|
||||
# No, history only has patches.
|
||||
|
||||
# Alternative: Apply frontend patch to current model.
|
||||
# Then re-apply backend patches, but discard parts that overlap.
|
||||
|
||||
frontend_paths = {p['path'] for p in patch}
|
||||
|
||||
current_state = entry.model.model_dump(mode="json")
|
||||
# Apply frontend patch
|
||||
new_state = jsonpatch.apply_patch(current_state, patch)
|
||||
|
||||
# Re-apply backend patches that don't overlap
|
||||
for b_patch in backend_patches:
|
||||
filtered_b_patch = [op for op in b_patch if op['path'] not in frontend_paths]
|
||||
if filtered_b_patch:
|
||||
new_state = jsonpatch.apply_patch(new_state, filtered_b_patch)
|
||||
|
||||
self._update_model(entry.model, new_state)
|
||||
|
||||
# Commit the result as a single new patch
|
||||
# We need to compute what changed from last_snapshot to new_state
|
||||
final_patch = jsonpatch.make_patch(entry.last_snapshot, new_state).patch
|
||||
if final_patch:
|
||||
entry.commit_patch(final_patch)
|
||||
# Broadcast resolved state as snapshot to converge
|
||||
if self.websocket:
|
||||
msg = SnapshotMessage(
|
||||
store=entry.store_name,
|
||||
seq=entry.seq,
|
||||
state=entry.model.model_dump(mode="json")
|
||||
)
|
||||
await self.websocket.send_json(msg.model_dump(mode="json"))
|
||||
entry.commit_patch(patch)
|
||||
logger.info(f"apply_client_patch: Patch committed, new seq={entry.seq}")
|
||||
# Don't broadcast back to client - they already have this change
|
||||
# Broadcasting would cause an infinite loop
|
||||
logger.info("apply_client_patch: Not broadcasting back to originating client")
|
||||
except jsonpatch.JsonPatchConflict as e:
|
||||
logger.warning(f"apply_client_patch: Patch conflict on no-conflict path: {e}. Sending snapshot to resync.")
|
||||
# Send snapshot to force resync
|
||||
if self.websocket:
|
||||
msg = SnapshotMessage(
|
||||
store=entry.store_name,
|
||||
seq=entry.seq,
|
||||
state=entry.model.model_dump(mode="json")
|
||||
)
|
||||
await self.websocket.send_json(msg.model_dump(mode="json"))
|
||||
|
||||
elif client_base_seq < entry.seq:
|
||||
# Conflict! Frontend wins.
|
||||
# 1. Get backend patches since client_base_seq
|
||||
backend_patches = []
|
||||
for seq, p in entry.history:
|
||||
if seq > client_base_seq:
|
||||
backend_patches.append(p)
|
||||
|
||||
# 2. Apply frontend patch first to the state at client_base_seq
|
||||
# But we only have the current authoritative model.
|
||||
# "Apply the frontend patch first to the model (frontend wins)"
|
||||
# "Re-apply the backend deltas that do not overlap the frontend's changed paths on top"
|
||||
|
||||
# Let's get the state as it was at client_base_seq if possible?
|
||||
# No, history only has patches.
|
||||
|
||||
# Alternative: Apply frontend patch to current model.
|
||||
# Then re-apply backend patches, but discard parts that overlap.
|
||||
|
||||
frontend_paths = {p['path'] for p in patch}
|
||||
|
||||
current_state = entry.model.model_dump(mode="json")
|
||||
# Apply frontend patch
|
||||
try:
|
||||
new_state = jsonpatch.apply_patch(current_state, patch)
|
||||
except jsonpatch.JsonPatchConflict as e:
|
||||
logger.warning(f"apply_client_patch: Failed to apply client patch during conflict resolution: {e}. Sending snapshot to resync.")
|
||||
# Send snapshot to force resync
|
||||
if self.websocket:
|
||||
msg = SnapshotMessage(
|
||||
store=entry.store_name,
|
||||
seq=entry.seq,
|
||||
state=entry.model.model_dump(mode="json")
|
||||
)
|
||||
await self.websocket.send_json(msg.model_dump(mode="json"))
|
||||
return
|
||||
|
||||
# Re-apply backend patches that don't overlap
|
||||
for b_patch in backend_patches:
|
||||
filtered_b_patch = [op for op in b_patch if op['path'] not in frontend_paths]
|
||||
if filtered_b_patch:
|
||||
try:
|
||||
new_state = jsonpatch.apply_patch(new_state, filtered_b_patch)
|
||||
except jsonpatch.JsonPatchConflict as e:
|
||||
logger.warning(f"apply_client_patch: Failed to apply backend patch during conflict resolution: {e}. Skipping this patch.")
|
||||
continue
|
||||
|
||||
self._update_model(entry.model, new_state)
|
||||
|
||||
# Commit the result as a single new patch
|
||||
# We need to compute what changed from last_snapshot to new_state
|
||||
final_patch = jsonpatch.make_patch(entry.last_snapshot, new_state).patch
|
||||
if final_patch:
|
||||
entry.commit_patch(final_patch)
|
||||
# Broadcast resolved state as snapshot to converge
|
||||
if self.websocket:
|
||||
msg = SnapshotMessage(
|
||||
store=entry.store_name,
|
||||
seq=entry.seq,
|
||||
state=entry.model.model_dump(mode="json")
|
||||
)
|
||||
await self.websocket.send_json(msg.model_dump(mode="json"))
|
||||
except Exception as e:
|
||||
logger.error(f"apply_client_patch: Unexpected error: {e}. Sending snapshot to resync.", exc_info=True)
|
||||
# Send snapshot to force resync
|
||||
if self.websocket:
|
||||
msg = SnapshotMessage(
|
||||
store=entry.store_name,
|
||||
seq=entry.seq,
|
||||
state=entry.model.model_dump(mode="json")
|
||||
)
|
||||
await self.websocket.send_json(msg.model_dump(mode="json"))
|
||||
|
||||
def _update_model(self, model: BaseModel, new_data: Dict[str, Any]):
|
||||
# Update model using model_validate for potentially nested models
|
||||
|
||||
Reference in New Issue
Block a user