etherpad-lite/doc/api/changeset_library.md
John McLear 01d0b08a4e
docs: migrate useful wiki content into the manual (#7990) (#7994)
* docs: migrate useful wiki content into the VitePress manual (#7990)

The GitHub wiki is being retired; documentation should ship with the
software. This migrates the still-accurate, non-duplicate wiki pages into
the published VitePress site (doc/**/*.md + the sidebar in
doc/.vitepress/config.mts) so they are versioned, searchable and portable:

- deployment.md: reverse-proxy configs (Nginx/Apache/Caddy/Traefik/
  HAProxy) with the WebSocket-upgrade rules, subdirectory hosting via
  X-Proxy-Path, native HTTPS via the ssl block, a systemd unit, and the
  Istio manifest (with the Redis-adapter multi-replica caveat).
- accessibility.md: editor keyboard shortcuts (verified against
  ace2_inner.ts / broadcast_slider.ts / pad_editbar.ts), toolbar
  navigation, NVDA notes.
- faq.md: install methods, URL-path reference, listing/deleting pads
  (API-first), backup/restore, and history pruning.
- development.md: source-tree tour, the pad<->format conversion pipeline,
  the internal DB API, and the Fontello toolbar-icon workflow.
- database.md: the key/value schema plus connecting MySQL/PostgreSQL/Redis
  backends and a pgloader MySQL->PostgreSQL migration (database docs were
  previously absent from the VitePress site).

Every page was checked against the current source before inclusion:
corrected the apt instructions to the live signed repo (stable/main,
signed-by key), dropped the unpublished snap, fixed the Redis dbSettings
(flat host/port/password or url, not the obsolete client_options),
dropped charset from the PostgreSQL example, and removed a phantom
getEtherpad API reference. The VitePress site builds cleanly
(pnpm run docs:build) with the dead-link checker enabled.

Closes #7990

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: add verified hands-on changeset/atext walkthrough (#7990)

Migrate the practical Changeset-library tutorial from the wiki into
changeset_library.md, rewritten against the current API: unpack(),
deserializeOps() (replacing the deprecated opIterator) and
new AttributePool() (replacing the removed AttributePoolFactory). Every
example output was produced by running the code against the current
Changeset.ts / AttributePool.ts, not copied from the wiki. Also fixes a
stale ether/etherpad-lite source link.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 09:52:33 +01:00

133 lines
4.4 KiB
Markdown
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.

# Changeset Library
The [changeset
library](https://github.com/ether/etherpad/blob/develop/src/static/js/Changeset.ts)
provides tools to create, read, and apply changesets.
## Changeset
```javascript
const Changeset = require('ep_etherpad-lite/static/js/Changeset');
```
A changeset describes the difference between two revisions of a document. When a
user edits a pad, the browser generates and sends a changeset to the server,
which relays it to the other users and saves a copy (so that every past revision
is accessible).
A transmitted changeset looks like this:
```
'Z:z>1|2=m=b*0|1+1$\n'
```
### Reading a changeset
`unpack()` splits a changeset string into its parts:
```javascript
const unpacked = Changeset.unpack('Z:z>1|2=m=b*0|1+1$\n');
// { oldLen: 35, newLen: 36, ops: '|2=m=b*0|1+1', charBank: '\n' }
```
`oldLen` is the document length before the change and `newLen` the length after.
`ops` is the list of operations, and `charBank` holds the characters inserted by
those operations.
Iterate the operations with `deserializeOps()`, which yields one `Op` at a time:
```javascript
for (const op of Changeset.deserializeOps(unpacked.ops)) {
console.log(op);
}
// Op { opcode: '=', chars: 22, lines: 2, attribs: '' }
// Op { opcode: '=', chars: 11, lines: 0, attribs: '' }
// Op { opcode: '+', chars: 1, lines: 1, attribs: '*0' }
```
There are three kinds of operation, each applied starting from the current
position in the text:
- `=` keeps text (it may still change the text's attributes, e.g. make it bold).
- `-` removes text.
- `+` inserts text (taking the characters from the changeset's `charBank`).
`opcode` is the operation type; `chars` and `lines` are how much text it covers;
and `attribs` are the attributes applied, written as `*` references into the
pad's attribute pool. In the example above the final op inserts one character
(the newline from `charBank`) carrying attribute `*0`.
## Attribute Pool
```javascript
const AttributePool = require('ep_etherpad-lite/static/js/AttributePool');
```
Changesets do not include any attribute keyvalue pairs. Instead, they use
numeric identifiers that reference attributes kept in an [attribute
pool](https://github.com/ether/etherpad/blob/develop/src/static/js/AttributePool.ts).
This attribute interning reduces the transmission overhead of attributes that
are used many times.
There is one attribute pool per pad, and it includes every current and
historical attribute used in the pad.
A pool can be serialized to and from a plain object with `toJsonable()` and
`fromJsonable()`:
```javascript
const pool = new AttributePool();
pool.fromJsonable({
numToAttrib: {
0: ['author', 'a.kVnWeomPADAT2pn9'],
1: ['bold', 'true'],
2: ['italic', 'true'],
},
nextNum: 3,
});
pool.getAttrib(1); // [ 'bold', 'true' ]
pool.getAttribKey(1); // 'bold'
pool.getAttribValue(1); // 'true'
```
Each attribute is a `[key, value]` pair — `['bold', 'true']`, or
`['author', '<authorId>']`. A character can carry several attributes (bold *and*
italic), but only one value per key (so it cannot belong to two authors).
## Attributed text (atext)
A pad's content is stored as *attributed text* (`atext`): the plain text plus an
attribute string describing which attributes apply to each span.
```javascript
const atext = {
text: 'bold text\nitalic text\nnormal text\n\n',
attribs: '*0*1+9*0|1+1*0*1*2+b|1+1*0+b|2+2',
};
```
The attribute string is a sequence of `+` operations — the same encoding used by
changesets — which you can read with `deserializeOps()`:
```javascript
for (const op of Changeset.deserializeOps(atext.attribs)) {
console.log(op);
}
// Op { opcode: '+', chars: 9, lines: 0, attribs: '*0*1' }
// Op { opcode: '+', chars: 1, lines: 1, attribs: '*0' }
// Op { opcode: '+', chars: 11, lines: 0, attribs: '*0*1*2' }
// Op { opcode: '+', chars: 1, lines: 1, attribs: '' }
// Op { opcode: '+', chars: 11, lines: 0, attribs: '*0' }
// Op { opcode: '+', chars: 2, lines: 2, attribs: '' }
```
Read against the pool above, the first nine characters (`bold text`) carry
attributes `*0*1` (author + bold), the following newline carries `*0`, and so on.
## Further Reading
Detailed information about the changesets & Easysync protocol:
* [Easysync Protocol](https://github.com/ether/etherpad/blob/develop/doc/easysync/easysync-notes.pdf)
* [Etherpad and EasySync Technical Manual](https://github.com/ether/etherpad/blob/develop/doc/easysync/easysync-full-description.pdf)