Files
web-cad/frontend/src/crdt/YjsDocument.ts
T

207 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* YjsDocument Manages a Y.Doc with shared maps for CAD elements, layers, and blocks.
* Provides helpers to sync local state with the CRDT document.
*/
import * as Y from 'yjs';
import type { CADElement, CADLayer, BlockDefinition } from '../types/cad.types';
import type { ElementGroup } from '../tools/modification/GroupTool';
import type { BackgroundConfig } from '../services/backgroundService';
import type { LayoutDefinition } from '../types/cad.types';
export interface YjsDocumentData {
elements: Y.Map<CADElement>;
layers: Y.Map<CADLayer>;
blocks: Y.Map<BlockDefinition>;
meta: Y.Map<unknown>;
groups: Y.Map<ElementGroup>;
bgConfig: Y.Map<BackgroundConfig>;
layouts: Y.Map<LayoutDefinition>; // Task G1
}
export class YjsDocument {
readonly doc: Y.Doc;
readonly data: YjsDocumentData;
constructor() {
this.doc = new Y.Doc();
this.data = {
elements: this.doc.getMap<CADElement>('elements'),
layers: this.doc.getMap<CADLayer>('layers'),
blocks: this.doc.getMap<BlockDefinition>('blocks'),
meta: this.doc.getMap<unknown>('meta'),
groups: this.doc.getMap<ElementGroup>('groups'),
bgConfig: this.doc.getMap<BackgroundConfig>('bgConfig'),
layouts: this.doc.getMap<LayoutDefinition>('layouts'),
};
}
/** Get all elements as a plain array */
getElements(): CADElement[] {
return Array.from(this.data.elements.values());
}
/** Get all layers as a plain array */
getLayers(): CADLayer[] {
return Array.from(this.data.layers.values());
}
/** Get all blocks as a plain array */
getBlocks(): BlockDefinition[] {
return Array.from(this.data.blocks.values());
}
/** Get all groups as a plain array */
getGroups(): ElementGroup[] {
return Array.from(this.data.groups.values());
}
/** Get all background configs as a plain array */
getBgConfigs(): BackgroundConfig[] {
return Array.from(this.data.bgConfig.values());
}
/** Get all layouts as a plain array (Task G1) */
getLayouts(): LayoutDefinition[] {
return Array.from(this.data.layouts.values());
}
/** Add or update a single element */
setElement(el: CADElement): void {
this.doc.transact(() => {
this.data.elements.set(el.id, el);
});
}
/** Remove an element by id */
deleteElement(id: string): void {
this.data.elements.delete(id);
}
/** Add or update a layer */
setLayer(layer: CADLayer): void {
this.data.layers.set(layer.id, layer);
}
/** Remove a layer by id */
deleteLayer(id: string): void {
this.data.layers.delete(id);
}
/** Add or update a block definition */
setBlock(block: BlockDefinition): void {
this.data.blocks.set(block.id, block);
}
/** Remove a block by id */
deleteBlock(id: string): void {
this.data.blocks.delete(id);
}
/** Add or update a group */
setGroup(group: ElementGroup): void {
this.doc.transact(() => {
this.data.groups.set(group.id, group);
});
}
/** Remove a group by id */
deleteGroup(id: string): void {
this.data.groups.delete(id);
}
/** Add or update a background config */
setBgConfig(id: string, config: BackgroundConfig): void {
this.doc.transact(() => {
this.data.bgConfig.set(id, config);
});
}
/** Remove a background config by id */
deleteBgConfig(id: string): void {
this.data.bgConfig.delete(id);
}
/** Bulk-load local state into the Y.Doc (replaces all content) */
loadFromState(state: {
elements: CADElement[];
layers: CADLayer[];
blocks: BlockDefinition[];
groups?: ElementGroup[];
bgConfigs?: BackgroundConfig[];
}): void {
this.doc.transact(() => {
// Issue #7: Only clear and replace a map if the incoming array is non-empty.
// Clearing maps when data is empty wipes CRDT data from other connected clients.
if (state.elements.length > 0) {
this.data.elements.clear();
for (const el of state.elements) {
this.data.elements.set(el.id, el);
}
}
if (state.layers.length > 0) {
this.data.layers.clear();
for (const layer of state.layers) {
this.data.layers.set(layer.id, layer);
}
}
if (state.blocks.length > 0) {
this.data.blocks.clear();
for (const block of state.blocks) {
this.data.blocks.set(block.id, block);
}
}
if (state.groups && state.groups.length > 0) {
this.data.groups.clear();
for (const group of state.groups) {
this.data.groups.set(group.id, group);
}
}
if (state.bgConfigs && state.bgConfigs.length > 0) {
this.data.bgConfig.clear();
for (const cfg of state.bgConfigs) {
// Use 'default' as the key since there is typically one background config
this.data.bgConfig.set(cfg.name || 'default', cfg);
}
}
});
}
/** Export current state as a plain object */
toState(): {
elements: CADElement[];
layers: CADLayer[];
blocks: BlockDefinition[];
groups: ElementGroup[];
bgConfigs: BackgroundConfig[];
} {
return {
elements: this.getElements(),
layers: this.getLayers(),
blocks: this.getBlocks(),
groups: this.getGroups(),
bgConfigs: this.getBgConfigs(),
};
}
/** Encode full document state as update binary */
encodeState(): Uint8Array {
return Y.encodeStateAsUpdate(this.doc);
}
/** Apply a remote update binary */
applyUpdate(update: Uint8Array): void {
Y.applyUpdate(this.doc, update);
}
/** Register a callback for document changes */
onChange(callback: () => void): () => void {
this.doc.on('update', callback);
return () => this.doc.off('update', callback);
}
/** Destroy the document */
destroy(): void {
this.doc.destroy();
}
}