Skip to main content
Reference status

This page predates the current package split. Use the current package guides for checked installation, public imports, and onboarding examples. The detailed examples below have not all been revalidated.

Custom Operations Guide

Operations are the atomic units of document modification. While extensions use operations through transactions, you can also create your own custom operations for domain-specific needs.

What are Operations?​

Operations are the lowest-level actions that modify documents. They're executed within transactions and interact directly with DataStore.

Operation vs Extension:

  • Operation: Atomic document modification (Model package level)
  • Extension: Commands that use operations (Editor level)

When to Create Custom Operations​

Create custom operations when:

  • You need domain-specific document modifications
  • You want reusable operations across multiple extensions
  • You need operations that are more efficient than composing existing ones
  • You're building a library of operations for others to use

Defining Operations​

Operations are defined using defineOperation from @barocss/model/operations.

Basic Operation Definition​

import { defineOperation } from '@barocss/model/operations';
import type { TransactionContext } from '@barocss/model';
import type { INode } from '@barocss/datastore';

// 1. Define Operation Type
export interface SetColorOperation {
type: 'setColor';
payload: {
nodeId: string;
color: string;
};
}

// 2. Define Operation Execution Logic
defineOperation('setColor', async (
operation: SetColorOperation,
context: TransactionContext
) => {
const { nodeId, color } = operation.payload;

// Get node
const node = context.dataStore.getNode(nodeId);
if (!node) {
throw new Error(`Node not found: ${nodeId}`);
}

// Store old value for undo
const oldColor = node.attributes?.color;

// Perform modification
context.dataStore.updateNode(nodeId, {
attributes: {
...node.attributes,
color
}
});

// Return result
return {
ok: true,
data: context.dataStore.getNode(nodeId),
inverse: {
type: 'setColor',
payload: { nodeId, color: oldColor }
}
};
});

Operation Structure​

interface OperationResult {
ok: boolean;
data?: any;
inverse?: Operation; // For undo support
error?: string;
}

Operation execution:

  1. Receives operation and TransactionContext
  2. Accesses DataStore through context
  3. Modifies document
  4. Returns result with optional inverse operation

Registering Operations​

Operations must be imported to be registered:

// packages/model/src/operations/register-operations.ts
import './setColor';
import './toggleHighlight';
// Import other operations...

Important: Operations must be imported before use:

// At app initialization
import '@barocss/model/operations/register-operations';

// Now operations are available
await transaction(editor, [
{ type: 'setColor', payload: { nodeId: 'p1', color: 'red' } }
]).commit();

Defining Operation DSL​

Operation DSL provides helper functions for convenient use in transactions.

DSL Definition​

import { defineOperationDSL } from '@barocss/model/operations';

export const setColor = defineOperationDSL(
(nodeId: string, color: string) => ({
type: 'setColor',
payload: { nodeId, color }
}),
{ atom: false, category: 'content' }
);

Using DSL​

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

const ops = [
...control('p1', [
setColor('p1', 'red')
])
];

await transaction(editor, ops).commit();

DSL benefits:

  • Type-safe function calls
  • Better developer experience
  • Easier to compose operations
  • Clearer intent

Complete Example: Highlight Operation​

Let's create a complete highlight operation with DSL:

Step 1: Define Operation​

// operations/toggleHighlight.ts
import { defineOperation } from '@barocss/model/operations';
import type { TransactionContext } from '@barocss/model';

export interface ToggleHighlightOperation {
type: 'toggleHighlight';
payload: {
nodeId: string;
range: [number, number];
color?: string;
};
}

defineOperation('toggleHighlight', async (
operation: ToggleHighlightOperation,
context: TransactionContext
) => {
const { nodeId, range, color = 'yellow' } = operation.payload;
const node = context.dataStore.getNode(nodeId);

if (!node || typeof node.text !== 'string') {
throw new Error(`Invalid node for toggleHighlight: ${nodeId}`);
}

const marks = node.marks || [];
const [start, end] = range;

// Check if highlight mark exists
const existingMark = marks.find(
m => m.type === 'highlight' &&
m.range[0] === start &&
m.range[1] === end &&
m.attrs?.color === color
);

if (existingMark) {
// Remove mark
const newMarks = marks.filter(m => m !== existingMark);
context.dataStore.updateNode(nodeId, { marks: newMarks });

return {
ok: true,
data: context.dataStore.getNode(nodeId),
inverse: {
type: 'toggleHighlight',
payload: { nodeId, range, color }
}
};
} else {
// Add mark
const newMark = {
type: 'highlight',
range: [start, end],
attrs: { color }
};
context.dataStore.updateNode(nodeId, {
marks: [...marks, newMark]
});

return {
ok: true,
data: context.dataStore.getNode(nodeId),
inverse: {
type: 'toggleHighlight',
payload: { nodeId, range, color }
}
};
}
});

Step 2: Define DSL​

// operations-dsl/toggleHighlight.ts
import { defineOperationDSL } from '@barocss/model/operations';

export const toggleHighlight = defineOperationDSL(
(range: [number, number], color?: string) => ({
type: 'toggleHighlight',
payload: { range, color }
}),
{ atom: false, category: 'format' }
);

Step 3: Register Operation​

// operations/register-operations.ts
import './toggleHighlight';
// operations-dsl/index.ts
export * from './toggleHighlight';

Step 4: Use in Extension​

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

// In extension command
const result = await transaction(editor, [
...control(selection.startNodeId, [
toggleHighlight([startOffset, endOffset], 'yellow')
])
]).commit();

Operation Best Practices​

1. Always Validate Inputs​

defineOperation('myOperation', async (operation, context) => {
const { nodeId } = operation.payload;

// Validate node exists
const node = context.dataStore.getNode(nodeId);
if (!node) {
throw new Error(`Node not found: ${nodeId}`);
}

// Validate node type
if (node.stype !== 'expected-type') {
throw new Error(`Invalid node type: ${node.stype}`);
}

// Proceed with operation
// ...
});

2. Provide Inverse Operations​

return {
ok: true,
data: updatedNode,
inverse: {
type: 'myOperation',
payload: { /* old values */ }
}
};

Inverse operations enable:

  • Undo/redo support
  • Transaction rollback
  • History management

3. Use TransactionContext​

defineOperation('myOperation', async (operation, context) => {
// Access DataStore
const node = context.dataStore.getNode(nodeId);

// Access Schema (if needed)
const schema = context.schema;

// Access Editor (if needed)
const editor = context.editor;

// Perform operation
// ...
});

4. Handle Errors Gracefully​

defineOperation('myOperation', async (operation, context) => {
try {
// Operation logic
return { ok: true, data: result };
} catch (error) {
return {
ok: false,
error: error.message
};
}
});

5. Keep Operations Atomic​

// ✅ Good: Single responsibility
defineOperation('setColor', async (operation, context) => {
// Only sets color
});

// ❌ Bad: Multiple responsibilities
defineOperation('setColorAndSize', async (operation, context) => {
// Sets both color and size - should be two operations
});

Operation Categories​

Operations can be categorized for better organization:

defineOperationDSL(
(/* params */) => ({ type: 'myOperation', payload: {} }),
{
atom: false, // Can be split into smaller operations
category: 'content' | 'format' | 'structure' | 'custom'
}
);

Categories:

  • content: Text and content modifications
  • format: Formatting (marks, styles)
  • structure: Node structure changes
  • custom: Domain-specific operations

Testing Operations​

Test operations in isolation:

import { describe, it, expect } from 'vitest';
import { DataStore } from '@barocss/datastore';
import { TransactionManager } from '@barocss/model';
import './operations/register-operations';

describe('setColor operation', () => {
it('should set node color', async () => {
const dataStore = new DataStore(undefined, schema);
const tm = new TransactionManager(dataStore);

// Create node
dataStore.createNode({
sid: 'p1',
stype: 'paragraph',
content: []
});

// Execute operation
const transaction = tm.createBuilder('test')
.addOperation({
type: 'setColor',
payload: { nodeId: 'p1', color: 'red' }
});

const result = await transaction.commit();
expect(result.success).toBe(true);

// Verify
const node = dataStore.getNode('p1');
expect(node.attributes?.color).toBe('red');
});
});