Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactor: Extract services and split composables for better organization
- Created SpatialIndexManager to handle QuadTree operations separately
- Added LayoutAdapter interface for CRDT abstraction (Yjs, mock implementations)
- Split GraphNodeManager into focused composables:
  - useNodeWidgets: Widget state and callback management
  - useNodeChangeDetection: RAF-based geometry change detection
  - useNodeState: Node visibility and reactive state management
- Extracted constants for magic numbers and configuration values
- Updated layout store to use SpatialIndexManager and constants

This improves code organization, testability, and makes it easier to swap
CRDT implementations or mock services for testing.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
  • Loading branch information
christian-byrne and claude committed Aug 13, 2025
commit b09419c4d5e97ea991e8f1935a8d8c214bd44369
82 changes: 82 additions & 0 deletions src/adapters/layoutAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* Layout Adapter Interface
*
* Abstracts the underlying CRDT implementation to allow for different
* backends (Yjs, Automerge, etc.) and easier testing.
*/
import type { LayoutOperation } from '@/types/layoutOperations'
import type { NodeId, NodeLayout } from '@/types/layoutTypes'

/**
* Change event emitted by the adapter
*/
export interface AdapterChange {
/** Type of change */
type: 'set' | 'delete' | 'clear'
/** Affected node IDs */
nodeIds: NodeId[]
/** Actor who made the change */
actor?: string
}

/**
* Layout adapter interface for CRDT abstraction
*/
export interface LayoutAdapter {
/**
* Set a node's layout data
*/
setNode(nodeId: NodeId, layout: NodeLayout): void

/**
* Get a node's layout data
*/
getNode(nodeId: NodeId): NodeLayout | null

/**
* Delete a node
*/
deleteNode(nodeId: NodeId): void

/**
* Get all nodes
*/
getAllNodes(): Map<NodeId, NodeLayout>

/**
* Clear all nodes
*/
clear(): void

/**
* Add an operation to the log
*/
addOperation(operation: LayoutOperation): void

/**
* Get operations since a timestamp
*/
getOperationsSince(timestamp: number): LayoutOperation[]

/**
* Get operations by a specific actor
*/
getOperationsByActor(actor: string): LayoutOperation[]

/**
* Subscribe to changes
*/
subscribe(callback: (change: AdapterChange) => void): () => void

/**
* Transaction support for atomic updates
*/
transaction(fn: () => void, actor?: string): void

/**
* Network sync methods (for future use)
*/
getStateVector(): Uint8Array
getStateAsUpdate(): Uint8Array
applyUpdate(update: Uint8Array): void
}
137 changes: 137 additions & 0 deletions src/adapters/mockLayoutAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
* Mock Layout Adapter
*
* Simple in-memory implementation for testing without CRDT overhead.
*/
import type { LayoutOperation } from '@/types/layoutOperations'
import type { NodeId, NodeLayout } from '@/types/layoutTypes'

import type { AdapterChange, LayoutAdapter } from './layoutAdapter'

/**
* Mock implementation for testing
*/
export class MockLayoutAdapter implements LayoutAdapter {
private nodes = new Map<NodeId, NodeLayout>()
private operations: LayoutOperation[] = []
private changeCallbacks = new Set<(change: AdapterChange) => void>()
private currentActor?: string

setNode(nodeId: NodeId, layout: NodeLayout): void {
this.nodes.set(nodeId, { ...layout })
this.notifyChange({
type: 'set',
nodeIds: [nodeId],
actor: this.currentActor
})
}

getNode(nodeId: NodeId): NodeLayout | null {
const layout = this.nodes.get(nodeId)
return layout ? { ...layout } : null
}

deleteNode(nodeId: NodeId): void {
const existed = this.nodes.delete(nodeId)
if (existed) {
this.notifyChange({
type: 'delete',
nodeIds: [nodeId],
actor: this.currentActor
})
}
}

getAllNodes(): Map<NodeId, NodeLayout> {
// Return a copy to prevent external mutations
const copy = new Map<NodeId, NodeLayout>()
for (const [id, layout] of this.nodes) {
copy.set(id, { ...layout })
}
return copy
}

clear(): void {
const nodeIds = Array.from(this.nodes.keys())
this.nodes.clear()
this.operations = []

if (nodeIds.length > 0) {
this.notifyChange({
type: 'clear',
nodeIds,
actor: this.currentActor
})
}
}

addOperation(operation: LayoutOperation): void {
this.operations.push({ ...operation })
}

getOperationsSince(timestamp: number): LayoutOperation[] {
return this.operations
.filter((op) => op.timestamp > timestamp)
.map((op) => ({ ...op }))
}

getOperationsByActor(actor: string): LayoutOperation[] {
return this.operations
.filter((op) => op.actor === actor)
.map((op) => ({ ...op }))
}

subscribe(callback: (change: AdapterChange) => void): () => void {
this.changeCallbacks.add(callback)
return () => this.changeCallbacks.delete(callback)
}

transaction(fn: () => void, actor?: string): void {
const previousActor = this.currentActor
this.currentActor = actor
try {
fn()
} finally {
this.currentActor = previousActor
}
}

// Mock network sync methods
getStateVector(): Uint8Array {
return new Uint8Array([1, 2, 3]) // Mock data
}

getStateAsUpdate(): Uint8Array {
// Simple serialization for testing
const json = JSON.stringify({
nodes: Array.from(this.nodes.entries()),
operations: this.operations
})
return new TextEncoder().encode(json)
}

applyUpdate(update: Uint8Array): void {
// Simple deserialization for testing
const json = new TextDecoder().decode(update)
const data = JSON.parse(json) as {
nodes: Array<[NodeId, NodeLayout]>
operations: LayoutOperation[]
}

this.nodes.clear()
for (const [id, layout] of data.nodes) {
this.nodes.set(id, layout)
}
this.operations = data.operations
}

private notifyChange(change: AdapterChange): void {
this.changeCallbacks.forEach((callback) => {
try {
callback(change)
} catch (error) {
console.error('Error in mock adapter change callback:', error)
}
})
}
}
Loading