Skip to main content

Operations Overview

Operations in Barocss Editor are organized in three layers: DSL, Model, and DataStore. Understanding this hierarchy helps you choose the right operation for your needs.

Operation Hierarchy

Three Layers Explained

1. DSL Layer (User-Friendly)

Purpose: Simple, declarative API for defining operations

Location: packages/model/src/operations-dsl/

Example:

import { insertText, control } from '@barocss/model';

// DSL helper
const op = insertText(5, 'Hello');
// Returns: { type: 'insertText', payload: { pos: 5, text: 'Hello' } }

// With control (injects nodeId)
const ops = control('text-1', [
insertText(5, 'Hello')
]);
// Returns: [{ type: 'insertText', payload: { nodeId: 'text-1', pos: 5, text: 'Hello' } }]

Characteristics:

  • ✅ Simple function calls
  • ✅ Type-safe
  • ✅ Composable with control()
  • ✅ No direct DataStore access

2. Model Layer (Business Logic)

Purpose: Implements operation logic, handles selection mapping, calls DataStore

Location: packages/model/src/operations/

Example:

// Defined with defineOperation
defineOperation('insertText', async (operation, context) => {
const { nodeId, pos, text } = operation.payload;

// 1. Call DataStore operation
const inserted = context.dataStore.range.insertText({
startNodeId: nodeId,
startOffset: pos,
endNodeId: nodeId,
endOffset: pos
}, text);

// 2. Map selection
if (context.selection?.current) {
// Update selection offsets
}

// 3. Return result with inverse
return { ok: true, data: inserted, inverse: {...} };
});

Characteristics:

  • ✅ Business logic implementation
  • ✅ Selection mapping
  • ✅ Transaction context access
  • ✅ Inverse operations for undo
  • ✅ Calls DataStore operations

3. DataStore Layer (Low-Level)

Purpose: Direct data manipulation with overlay and lock support

Location: packages/datastore/src/operations/

Example:

// RangeOperations class
class RangeOperations {
insertText(range: ModelSelection, text: string): string {
const node = this.dataStore.getNode(range.startNodeId);
const newText = node.text.substring(0, range.startOffset) +
text +
node.text.substring(range.startOffset);

// Uses overlay if transaction active
this.dataStore.updateNode(range.startNodeId, { text: newText });
return text;
}
}

Characteristics:

  • ✅ Direct DataStore access
  • ✅ Overlay-aware (COW)
  • ✅ Lock-aware
  • ✅ Schema validation
  • ✅ Operation events

Operation Flow

Complete Example: Insert Text

Detailed Connection Flow

Here's how operations flow through the three layers:

1. DSL Layer → Model Layer

// DSL: insertText helper
export const insertText = defineOperationDSL(
(...args: [number, string] | [string, number, string]) => {
if (args.length === 2) {
const [pos, text] = args;
return { type: 'insertText', payload: { pos, text } };
}
const [nodeId, pos, text] = args;
return { type: 'insertText', payload: { nodeId, pos, text } };
}
);

// control() injects nodeId
control('text-1', [insertText(5, 'Hello')])
// → [{ type: 'insertText', payload: { nodeId: 'text-1', pos: 5, text: 'Hello' } }]

2. Model Layer → DataStore Layer

// Model: defineOperation
defineOperation('insertText', async (operation, context) => {
const { nodeId, pos, text } = operation.payload;

// Calls DataStore operation
const inserted = context.dataStore.range.insertText({
startNodeId: nodeId,
startOffset: pos,
endNodeId: nodeId,
endOffset: pos
}, text);

// Maps selection
if (context.selection?.current) {
// Update selection offsets
}

return { ok: true, data: inserted, inverse: {...} };
});

3. DataStore Layer → Overlay

// DataStore: RangeOperations.insertText
class RangeOperations {
insertText(contentRange: ModelSelection, text: string): string {
const node = this.dataStore.getNode(contentRange.startNodeId);
const newText = node.text.substring(0, range.startOffset) +
text +
node.text.substring(range.startOffset);

// Uses overlay if transaction active
this.dataStore.updateNode(contentRange.startNodeId, { text: newText });
return text;
}
}

// updateNode() is overlay-aware
updateNode(nodeId: string, updates: Partial<INode>): void {
const overlay = this._overlay;
if (overlay && overlay.isActive()) {
// Write to overlay (COW)
overlay.upsertNode(updatedNode, 'update');
} else {
// Write directly to base
this.setNodeInternal(updatedNode);
}
}

Complete Connection Example

// 1. DSL Layer: User writes
const ops = control('text-1', [
insertText(5, 'Hello'),
toggleMark('bold', [0, 10])
]);

// DSL returns:
// [
// { type: 'insertText', payload: { nodeId: 'text-1', pos: 5, text: 'Hello' } },
// { type: 'toggleMark', payload: { nodeId: 'text-1', markType: 'bold', range: [0, 10] } }
// ]

// 2. Model Layer: Transaction executes
await transaction(editor, ops).commit();

// For each operation:
// - Model operation receives: { type: 'insertText', payload: {...} }
// - Model operation calls: context.dataStore.range.insertText(...)
// - Model operation maps selection
// - Model operation returns: { ok: true, data: 'Hello', inverse: {...} }

// 3. DataStore Layer: RangeOperations.insertText
// - Reads node from overlay/base
// - Clones to overlay if needed (COW)
// - Applies text insertion
// - Calls updateNode() which writes to overlay
// - Records operation in overlay

// 4. Overlay: All changes collected
// - Operations recorded in opBuffer
// - end() returns operations
// - commit() applies to base in order

Operation Mapping Table

DSL HelperModel OperationDataStore Operation
insertText(pos, text)defineOperation('insertText', ...)RangeOperations.insertText()
deleteTextRange(start, end)defineOperation('deleteTextRange', ...)RangeOperations.deleteText()
create(stype, attrs, content)defineOperation('create', ...)CoreOperations.createNodeWithChildren()
update(updates)defineOperation('update', ...)CoreOperations.updateNode()
addChild(child, pos?)defineOperation('addChild', ...)ContentOperations.addChild()
moveNode(newParentId, pos?)defineOperation('moveNode', ...)ContentOperations.moveNode()
applyMark(markType, range, attrs?)defineOperation('applyMark', ...)MarkOperations.setMarks() or RangeOperations.applyMark()
toggleMark(markType, range, attrs?)defineOperation('toggleMark', ...)MarkOperations.toggleMark()
splitTextNode(splitPos)defineOperation('splitTextNode', ...)SplitMergeOperations.splitTextNode()
mergeTextNodes(rightNodeId)defineOperation('mergeTextNodes', ...)SplitMergeOperations.mergeTextNodes()

When to Use Each Layer

Use DSL Layer When:

  • Writing Commands: Commands should use DSL operations
  • Transaction DSL: transaction(editor, [control(...), ...])
  • Type Safety: Want compile-time type checking
  • Composability: Need to combine multiple operations

Example:

// In Extension command
const result = await transaction(editor, [
...control(selection.startNodeId, [
insertText(0, 'Hello'),
toggleMark('bold', [0, 5])
])
]).commit();

Use Model Layer When:

  • Custom Operations: Creating new operation types
  • Complex Logic: Need transaction context, selection mapping
  • Inverse Operations: Need undo/redo support

Example:

// Define custom operation
defineOperation('customOp', async (operation, context) => {
// Access context.dataStore, context.selection, etc.
// Implement business logic
// Return result with inverse
});

Use DataStore Layer When:

  • Direct Access: Need direct DataStore manipulation
  • Low-Level Control: Need overlay/lock control
  • Performance: Need maximum performance
  • ⚠️ Rare: Usually not needed in extensions

Example:

// Direct DataStore access (rare)
const rangeOps = new RangeOperations(dataStore);
rangeOps.insertText(range, 'Hello');

Operation Categories

Text Operations

  • DSL: insertText, deleteTextRange, replaceText
  • Model: insertText, deleteTextRange, replaceText
  • DataStore: RangeOperations.insertText(), RangeOperations.deleteText()

Node Operations

  • DSL: create, delete, update, transformNode
  • Model: create, delete, update, transformNode
  • DataStore: CoreOperations.setNode(), CoreOperations.deleteNode()

Content Operations

  • DSL: addChild, removeChild, moveNode, reorderChildren
  • Model: addChild, removeChild, moveNode, reorderChildren
  • DataStore: ContentOperations.addChild(), ContentOperations.moveNode()

Mark Operations

  • DSL: applyMark, removeMark, toggleMark, updateMark
  • Model: applyMark, removeMark, toggleMark, updateMark
  • DataStore: MarkOperations.addMark(), MarkOperations.removeMark()

Selection Operations

  • DSL: selectRange, selectNode, clearSelection
  • Model: selectRange, selectNode, clearSelection
  • DataStore: (Selection managed by Editor Core)

Quick Reference

TaskUse ThisExample
Insert textDSL: insertTextcontrol(nodeId, [insertText(5, 'Hello')])
Delete textDSL: deleteTextRangecontrol(nodeId, [deleteTextRange(0, 5)])
Create nodeDSL: createcreate('paragraph', {}, [])
Update nodeDSL: updatecontrol(nodeId, [update({ text: 'New' })])
Add markDSL: applyMarkcontrol(nodeId, [applyMark('bold', [0, 5])])
Move nodeDSL: moveNodemoveNode(nodeId, newParentId, 0)
Custom operationModel: defineOperationSee Custom Operations Guide