@rtif-sdk/engine
v2.14.0
Published
RTIF editor engine, history, and plugin host
Maintainers
Readme
@rtif-sdk/engine
Editor engine for RTIF (Rich Text Input Format). Manages document state, dispatches operations through a plugin lifecycle, and provides undo/redo history.
Install
npm install @rtif-sdk/engineUsage
Create an engine
import { createEngine } from '@rtif-sdk/engine';
import type { Document } from '@rtif-sdk/core';
const doc: Document = {
version: 1,
blocks: [{ id: 'b1', type: 'text', spans: [{ text: '' }] }],
};
const engine = createEngine(doc);Dispatch operations
engine.dispatch({ type: 'insert_text', offset: 0, text: 'Hello' });
console.log(engine.state.doc.blocks[0].spans[0].text); // 'Hello'Undo / Redo
engine.undo(); // reverts to empty document
engine.redo(); // re-applies 'Hello'Subscribe to changes
const unsubscribe = engine.onChange((state) => {
console.log('Document changed:', state.doc);
});Plugins
import type { Plugin } from '@rtif-sdk/engine';
const myPlugin: Plugin = {
id: 'my-plugin',
init(engine) {
engine.registerCommand('greet', (eng) => {
eng.dispatch({ type: 'insert_text', offset: 0, text: 'Hi! ' });
});
},
};
engine.use(myPlugin);
engine.exec('greet');Plugin Lifecycle
beforeApplyhooks — inspect, modify, or cancel operationsapply()— core applies operations to the documentafterApplyhooks — observe changes, trigger side effectsonChangelisteners — UI re-renders
Plugin errors are caught and isolated — a failing plugin never breaks the editor.
Transactions
Group multiple dispatches into a single undo entry. Useful for multi-step edits that should undo as one unit.
const tx = engine.transaction();
engine.dispatch({ type: 'delete_text', offset: 0, count: 5 });
engine.dispatch({ type: 'insert_text', offset: 0, text: 'Hello' });
tx.commit(); // Single undo groupSnapshot-based undo
For complex replacements where composed inverse ops are fragile, commit with useSnapshot: true to capture the pre-transaction document state and restore it on undo:
const tx = engine.transaction();
// ... many dispatches ...
tx.commit({ useSnapshot: true }); // Undo restores exact pre-transaction stateTransaction API
interface Transaction {
readonly isOpen: boolean;
commit(options?: TransactionCommitOptions): void;
rollback(): void;
}
interface TransactionCommitOptions {
useSnapshot?: boolean;
}Dispatching while a transaction from a different source is already open throws RtifError('TRANSACTION_ACTIVE').
Streaming Plugin
Replace a range of content incrementally (e.g., AI-generated text streaming in). Built on transactions for atomic undo.
import { streamingPlugin, StreamingCommands } from '@rtif-sdk/engine';
const { plugin, startStream } = streamingPlugin();
engine.use(plugin);
const session = startStream(engine, {
startOffset: 0,
endOffset: 10,
onSession(s) { /* streaming started */ },
onCommit() { /* streaming finished */ },
onCancel() { /* streaming cancelled */ },
});
// Append text chunks
session.append({ text: 'Hello ' });
session.append({ text: 'world', marks: { bold: true } });
session.append({ text: '', blockBreak: true }); // New block
// Or replace with fully deserialized blocks
session.replaceContent(deserializedBlocks);
session.commit(); // Finalize (single undo group)
// or session.cancel(); // Roll back everythingStreamingChunk
interface StreamingChunk {
text: string;
marks?: Record<string, unknown>;
blockBreak?: boolean;
blockType?: string;
blockAttrs?: Record<string, unknown>;
}StreamingConfig
interface StreamingConfig {
startOffset: number;
endOffset: number;
onSession?: (session: StreamingSession) => void;
onCommit?: () => void;
onCancel?: () => void;
}Stream Insert (Cursor-Position)
Insert new content at the cursor without requiring a selection. The LLM's output controls all block types, marks, and structure — no boundary corrections are applied.
import { createStreamInsert } from '@rtif-sdk/engine';
// Start session at cursor (startOffset === endOffset — no deletion)
const session = streaming.startStream(engine, {
startOffset: cursor, endOffset: cursor,
});
const insert = createStreamInsert({ session, deserialize });
for await (const token of llm.stream(prompt)) {
insert.pushToken(token);
}
insert.commit();Stream Rewrite (Selection Range)
Replace a selected range with boundary-aware corrections. Preserves the prefix block type when the selection starts mid-block, and splits the suffix when types differ.
import { computeSelectionBoundaries, createStreamRewrite } from '@rtif-sdk/engine';
// 1. Capture boundaries before deletion
const boundaries = computeSelectionBoundaries(doc, start, end);
// 2. Start session (deletes original range)
const session = streaming.startStream(engine, { startOffset: start, endOffset: end });
// 3. Wrap with boundary corrections
const rewrite = createStreamRewrite({ session, engine, deserialize, boundaries });
for await (const token of llm.stream(prompt)) {
rewrite.pushToken(token);
}
rewrite.commit();StreamSession (Generic)
Both StreamInsertSession and StreamRewriteSession extend the shared StreamSession interface. Use it when handling either mode generically:
import type { StreamSession } from '@rtif-sdk/engine';
let stream: StreamSession;
if (hasSelection) {
stream = createStreamRewrite({ ... });
} else {
stream = createStreamInsert({ ... });
}Format Stream Adapter
Bridge a format deserializer (e.g., markdown, HTML) for incremental streaming. Each token push re-deserializes the accumulated text and replaces the streamed content.
import { createStreamingAdapter } from '@rtif-sdk/engine';
import { deserialize } from '@rtif-sdk/format-markdown';
const adapter = createStreamingAdapter(deserialize);
// As tokens arrive from an LLM:
for (const token of tokens) {
const blocks = adapter.push(token); // Re-deserializes accumulated text
session.replaceContent(blocks);
}
session.commit();Additional Exports
| Export | Description |
|--------|-------------|
| blockMovePlugin | Engine plugin that registers block-move operations and keyboard shortcuts. |
| BlockMoveCommands | Command name constants for block move (BlockMoveCommands.MOVE_UP, BlockMoveCommands.MOVE_DOWN). |
| computeMoveBlockOps(doc, blockId, direction) | Returns the composed operation array to move a block up or down without a new core op. |
| computeAttrsDiff(prev, next) | Returns the minimal attrs patch (with null removals) needed to transform prev into next. Used internally by move ops; useful for custom block-mutation commands. |
| streamingPlugin() | Factory returning { plugin, startStream } for streaming content replacement. |
| StreamingCommands | Command name constants (START, CANCEL). |
| createStreamingAdapter(deserialize) | Creates a format-aware streaming adapter for incremental re-deserialization. |
| createStreamInsert({ session, deserialize }) | Cursor-position insert — no boundary corrections. |
| createStreamRewrite({ session, engine, deserialize, boundaries }) | Selection-range rewrite with boundary-aware corrections. |
| computeSelectionBoundaries(doc, start, end) | Compute block-type boundary metadata for rewrite sessions. |
| buildRewriteContext(doc, start, end, serialize) | Build LLM rewrite context (boundaries + serialized selection). |
| buildRewritePrompt({ selected, instruction, ... }) | Optional prompt builder for LLM rewrites. |
| extractRtifSlice(doc, start, end) | Extract a slice of blocks/spans from a document range. |
| deleteSelectionOps(doc, start, end) | Compute delete_text + merge_block ops for a cross-block range. |
License
MIT
