npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rtif-sdk/engine

v3.3.0

Published

RTIF editor engine: dispatch, commands, history, features.

Readme

@rtif-sdk/engine

The stateful RTIF editor engine: one synchronous dispatch path, namespaced feature commands, a single inverse-based history, and pending marks. Depends on @rtif-sdk/core only — no DOM.

npm install @rtif-sdk/engine @rtif-sdk/core

30 seconds

import { createEngine, insertTextCommand, toggleMarkCommand } from '@rtif-sdk/engine';
import type { Feature } from '@rtif-sdk/engine';

const bold: Feature = {
  name: 'bold',
  marks: { bold: {} },
  commands: { toggle: toggleMarkCommand('bold') },
};
const text: Feature = { name: 'text', commands: { insert: insertTextCommand() } };

const engine = createEngine({ features: [bold, text] });
engine.dispatch([{ type: 'insert_text', at: 0, text: 'Hello' }]);
engine.setSelection({ anchor: 0, focus: 5 });
engine.exec('bold.toggle');         // commands are `${feature.name}.${key}`
engine.isActive('bold.toggle');     // true
engine.undo();

createEngine({ doc?, features?, historyLimit? }) — the default document is one empty paragraph; historyLimit defaults to 1000 entries.

dispatch and transactions

dispatch(ops, opts?) is synchronous and atomic: validate → for each op invert then apply → normalize touched blocks → map the selection → run onTransaction hooks → push history → notify onState subscribers once.

DispatchOptions: origin ('user' default, 'history', 'remote', or custom), historyGroup (coalescing key), selectionAfter (explicit post-transaction selection; otherwise the selection is mapped through each op).

Subscribers get the committed Transaction: ops, inverses (in apply-ready order — the reverse of ops), stateBefore/stateAfter, origin, historyGroup. setSelection notifies with tx = null and no history entry.

Commands

interface Command {
  run(ctx: CommandContext, params?: unknown): void;
  canRun?(ctx: CommandQueryContext): boolean;   // default true
  isActive?(ctx: CommandQueryContext): boolean; // default false
}

// The ONE object query hooks receive — destructure what you need
// (`({ state, history }) => …`); the signatures never grow positional params.
interface CommandQueryContext {
  readonly state: EngineState;
  readonly pendingMarks: Marks | null;
  readonly schema: Schema;
  readonly history: HistoryCapability; // canUndo() / canRedo()
}

engine.exec(name, params?) runs a command; canExec(name) and isActive(name) drive toolbars. Shipped factories:

| Factory | Does | |---|---| | toggleMarkCommand(mark, value?) | Range: add/remove everywhere. Collapsed: pending override. | | setMarkCommand(mark) | exec('link.set', { href }); null params removes. | | setBlockTypeCommand(type, attrs?) | Sets every block intersecting the selection. | | toggleBlockTypeCommand(type, attrs?) | Same, but reverts to paragraph when already active. | | insertTextCommand() | exec(name, { text }); carries typing marks; coalesces as 'typing'. | | deleteSelectionCommand() | One atomic replace_range. | | deleteDirectionCommand('backward' \| 'forward') | Code-point delete, boundary merges, atomic neighbors. | | splitBlockCommand(generateId?) | Enter: replaces selection + splits, in one transaction. | | undoCommand() / redoCommand() | Call ctx.undo() / ctx.redo(). |

Undo/redo and history groups

One history mechanism: a stack of { ops, inverses, selectionBefore, selectionAfter, group }. Adjacent entries with the same non-undefined historyGroup coalesce — insertTextCommand dispatches collapsed-cursor inserts with { historyGroup: 'typing' }, so a typed word undoes as one step. undo()/redo() replay with origin: 'history' (no new history entry) and are safe no-ops on empty stacks.

pendingMarks (bold-then-type)

Toggle bold at a collapsed cursor and no operation is dispatched — there is no text to format yet. Instead the engine records a pending override:

engine.exec('bold.toggle');                  // pendingMarks → { bold: true }
engine.exec('text.insert', { text: 'hi' }); // 'hi' is inserted bold; pending consumed

pendingMarks is Marks | null; the value false means "explicitly off" (toggling bold off inside bold text). Clearing is an engine policy, not a UI subscriber's job: any document-changing transaction consumes them, any collapsed-cursor move clears them, and marks whose MarkSpec is exclusive (links) also clear at their trailing edge — for every input path, always. isActive sees pendingMarks on its query context, so toolbars reflect the override.

Features and onTransaction

interface Feature {
  name: string;
  marks?: Record<string, MarkSpec>;
  blocks?: Record<string, BlockSpec>;
  commands?: Record<string, Command>;     // registered as `${name}.${key}`
  keymap?: Record<string, KeyBinding>;    // used by web; validated here
  onTransaction?(tx: Transaction, state: EngineState): readonly Operation[] | void;
}

// 'Mod-b' → 'bold.toggle', or a command plus bound params:
// 'Mod-Alt-2' → { command: 'heading.toggle', params: { level: 2 } }
type KeyBinding = string | { command: string; params?: unknown };

onTransaction hooks run in feature order after each transaction's ops apply. Returned ops join the same transaction (applied, inverted, re-offered to all hooks — bounded to 10 passes, then EngineError('HOOK_LIMIT')), so input rules and renumbering undo together with the edit that triggered them. Hooks run for every origin, including 'history' replays — check tx.origin to avoid fighting undo. A hook that throws fails the whole dispatch: the transaction is discarded and the error rethrown — no silent skip-and-continue corruption.

The reentrancy rule

Calling dispatch (or exec, undo, setSelection) from inside an onState subscriber or onTransaction hook throws EngineError('REENTRANT_DISPATCH'). There is no deferred queue and no reconciliation flag — the contract is explicit. Need follow-up work from a subscriber? Defer it yourself:

engine.onState((state) => {
  if (needsFixup(state)) queueMicrotask(() => engine.dispatch(fixupOps));
});

Engine errors are EngineError with code: 'REENTRANT_DISPATCH' | 'UNKNOWN_COMMAND' | 'INVALID_FEATURE' | 'HOOK_LIMIT'. Document/operation errors are RtifError from @rtif-sdk/core. mergeSchemas(features) (also used by web) merges feature specs into one Schema and throws on conflicts.