Help us shape the future of Go! Take the 2026 Go Developer Survey

coedit

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 27, 2026 License: BSD-3-Clause Imports: 11 Imported by: 0

README

coedit

CI Go Reference Coverage

A PDF several people are editing at once.

What is shared is not the file

A file is a settled thing. Merging two people's copies of one means merging two piles of bytes, which cannot be done.

What is shared is the plan: which pages, from which files, in which order, turned and cropped how, with which bookmarks over them. That is a list, a tree and a handful of fields — and a list, a tree and a handful of fields are things a conflict-free replicated data type merges without asking anybody.

So two people reordering the same document keep both reorderings. Two people rotating different pages keep both rotations. Two people rotating the same page settle on one of the two, which is what rotating a page twice means. Nobody waits for a lock and nobody loses work, however many of them there are.

plan := coedit.Open(store)
plan.Files().Put("report.pdf", bytes)
first, _ := plan.Append("report.pdf", 3)
plan.Rotate(first, 90)
plan.AddBookmark(coedit.AtRoot, coedit.BookmarkID{}, "Findings", first)
out, _ := plan.Bytes()

Build hands back an ops.Doc — the same document the command-line tool and the browser build, so what comes out of a shared edit is what comes out of any other.

Where the files live

A file goes in once and is referred to by name from then on, so a plan taking three pages out of a hundred-page report carries the report once. The bytes are cut into pieces on their content with go-deltasync/chunk, so replacing one page of a file sends one piece rather than the file, and two files sharing their bytes share their pieces.

Every piece is stored under the hash of what it holds, and the whole file's hash travels with the list of them: a peer cannot quietly put something else there, and a file whose pieces have not all arrived yet is not a file yet — which is what keeps a half-arrived document from being built.

Where it runs

A plan needs two things from wherever it is kept: read a map, or change one and say what changed. That is the Store interface, and it is two methods long.

Local is a plan held alone — a test, or a participant between one connection and the next. A session over go-crdt/collab is the same two methods, so the same plan rides a central WebSocket or a peer-to-peer data channel without knowing which. It builds for GOOS=js/wasm, so a browser tab runs it.

How it is checked

The point of a shared plan is not that two people can edit it. It is that any number of them can, in any order, on any connection, and end up with the same document. So that is what is tested:

  • 25 editors, 40 rounds each, every one making random appends, inserts, moves, removals, rotations and bookmarks, merged in a random order — and then asked whether they agree. They do, on all 189 pages, and every one of them builds the same file.
  • Two groups of four who cannot reach each other for twenty rounds, and then meet. Nothing either group did is lost.
  • An editor who leaves and comes back with a snapshot: work done while away is not work lost.

That harness found the thing two browser tabs would not have: one person removing a page while another turns it leaves the item behind, because a field written after a removal puts the record back. What it does not put back is which file the page came from — so that is what says whether there is a page there at all.

100% of statements, including everything a hostile replica can put in a plan: a rotation of thirty-seven degrees, a crop box of no area, a page numbered with a word.

The rest of the family

reader reads and writes the format · ops is the verbs and the pdfops command · render turns a page into pixels · pdffont is what a document says about a font · extract reads a page back · coedit is a PDF several people edit at once · app is the browser workbench.

Documentation for all of it: https://go-pdfkit.github.io/docs/

Licence

BSD-3-Clause.

Documentation

Overview

Package coedit is a PDF several people are editing at once.

What is shared is not the file. A file is a settled thing — merging two people's copies of one means merging two piles of bytes, which cannot be done. What is shared is the plan: which pages, from which files, in which order, turned and cropped how, with which bookmarks over them. That is a list, a tree and a handful of fields, and a list, a tree and a handful of fields are things a conflict-free replicated data type merges without asking anybody.

So two people reordering the same document keep both reorderings, two people rotating different pages keep both rotations, and two people rotating the same page settle on one of the two — which is what rotating a page twice means. Nobody waits for a lock and nobody loses work, however many of them there are.

A plan becomes a file through Plan.Build, which hands back an ops.Doc — the same document the command-line tool and the browser build, so what comes out of a shared edit is what comes out of any other.

Index

Constants

This section is empty.

Variables

AtRoot is the top of the bookmark tree.

AtStart is where a page goes when it goes first.

View Source
var ErrNoSuchFile = errors.New("coedit: no such file")

ErrNoSuchFile reports a page naming a file the plan does not carry.

View Source
var ErrNoSuchPage = errors.New("coedit: no such page")

ErrNoSuchPage reports an operation naming a page this replica does not hold — one never added, or one somebody else removed.

Functions

This section is empty.

Types

type Bookmark

type Bookmark struct {
	ID    BookmarkID
	Title string
	// Page is the page it points at, by identity rather than by number, so
	// that a bookmark still points at its page after somebody else has moved
	// it. It is empty for a bookmark that points at nothing.
	Page     PageID
	Children []Bookmark
}

A Bookmark is one entry of the plan's outline.

type BookmarkID

type BookmarkID = structured.TreeID

A BookmarkID names one bookmark.

type Files

type Files interface {
	// Put stores a file under a name, replacing whatever was there.
	Put(name string, data []byte) error
	// Get reads one back.
	Get(name string) ([]byte, bool)
	// Names is every file the plan carries.
	Names() []string
	// Remove drops one. The pages that came from it are left as they are and
	// will not build until it is put back, which is what removing a file a
	// document is made of means.
	Remove(name string) error
}

A Files is where the plan keeps the documents its pages come from.

A file is put in once and referred to by name from then on, so a plan that takes three pages out of a hundred-page report carries the report once. The bytes are cut into pieces on their content rather than every so many bytes, which is what makes replacing one page of a file send one piece rather than the file.

type Local

type Local struct {
	// contains filtered or unexported fields
}

Local is a plan held on its own, with nobody else editing it: the shape a test takes, and what a participant edits between one connection and the next.

Its snapshot is what a session is joined with, so work done alone is not work lost.

func LoadLocal

func LoadLocal(site crdt.SiteID, snapshot []byte) (*Local, error)

LoadLocal rebuilds a plan from a snapshot, to be edited as site.

func NewLocal

func NewLocal(site crdt.SiteID) *Local

NewLocal returns an empty plan this site edits alone.

func (*Local) Composite

func (l *Local) Composite() *crdt.Composite

Composite is the replica underneath, for a caller merging two plans by hand.

func (*Local) Edit

func (l *Local) Edit(part string, fn func(*crdt.Map) ([]crdt.MapOp, error)) error

Edit runs fn against the named map part. There is nobody to publish to, so the operations it produced are already everywhere they are going.

func (*Local) Merge

func (l *Local) Merge(other *Local) error

Merge takes everything another replica has that this one lacks. It is what two people who have been editing apart do when they meet, and it is the same merge a session does — there is only one.

func (*Local) Read

func (l *Local) Read(part string, fn func(*crdt.Map)) error

Read runs fn against the named map part.

func (*Local) Snapshot

func (l *Local) Snapshot() []byte

Snapshot is the whole plan, to be reloaded or to join a session with.

type Page

type Page struct {
	ID     PageID
	Source string
	Number int
	Rotate int
	Crop   []float64
}

A Page is one page of the plan, as it now stands.

type PageID

type PageID = structured.ItemID

A PageID names one page of the plan. It survives being moved, so two people moving the same page mean the same page.

type Plan

type Plan struct {
	// contains filtered or unexported fields
}

A Plan is a PDF being edited: the pages in their order, the bookmarks over them, and the files they come from.

func Open

func Open(store Store) *Plan

Open reads a store as a plan.

func (*Plan) AddBookmark

func (p *Plan) AddBookmark(parent, after BookmarkID, title string, page PageID) (BookmarkID, error)

AddBookmark puts an entry under a parent, after a sibling — AtRoot for the top and the zero identity for first.

func (*Plan) Append

func (p *Plan) Append(file string, number int) (PageID, error)

Append puts a page of a file at the end of the plan.

func (*Plan) Build

func (p *Plan) Build() (*ops.Doc, error)

Build turns the plan into a document ready to be written.

Every page named by the plan is taken from the file it names, in the order the plan puts them, turned and cropped as the plan says. The bookmarks are written over the pages they point at; one pointing at a page that is no longer there is left out rather than pointed at the wrong one.

A plan naming a file this replica has not received yet cannot be built, and says which file it is waiting for. That is not a failure — it is what a participant sees for the moment between somebody adding a file and its pieces arriving.

func (*Plan) Bytes

func (p *Plan) Bytes() ([]byte, error)

Bytes builds the plan and writes it out.

func (*Plan) Crop

func (p *Plan) Crop(page PageID, box []float64) error

Crop sets what of a page is visible, as the four numbers a PDF rectangle is. A box of nothing puts back whatever the source page said.

func (*Plan) Files

func (p *Plan) Files() Files

Files is where the documents this plan's pages come from are kept.

func (*Plan) InsertAfter

func (p *Plan) InsertAfter(after PageID, file string, number int) (PageID, error)

InsertAfter puts a page of a file after another page, or first for AtStart.

func (*Plan) Len

func (p *Plan) Len() int

Len is how many pages the plan holds.

func (*Plan) Move

func (p *Plan) Move(page, after PageID) error

Move puts a page after another one, or first for AtStart. Two people moving the same page settle on one of the two moves, and two people moving different pages keep both.

func (*Plan) MoveBookmark

func (p *Plan) MoveBookmark(node, parent, after BookmarkID) error

MoveBookmark puts an entry under another parent, after another sibling.

func (*Plan) Outline

func (p *Plan) Outline() []Bookmark

Outline is the bookmark tree as it now stands.

func (*Plan) Pages

func (p *Plan) Pages() []Page

Pages is every page of the plan, in order.

An item that names no file is not a page. That is not a nicety: one person removing a page while another turns it leaves the item behind with the turn on it and nothing else, because a field written after a removal puts the record back. What it does not put back is which file the page came from, so that is what says whether there is a page there at all.

func (*Plan) Remove

func (p *Plan) Remove(page PageID) error

Remove drops a page.

func (*Plan) RemoveBookmark

func (p *Plan) RemoveBookmark(node BookmarkID) error

RemoveBookmark drops an entry and everything under it.

func (*Plan) Rotate

func (p *Plan) Rotate(page PageID, degrees int) error

Rotate turns a page, in quarters clockwise. It is the rotation the page will have, not one added to what it has: two people asking for the same rotation agree, and two asking for different ones settle on one.

type Store

type Store interface {
	// Read runs fn against the named map part.
	Read(part string, fn func(*crdt.Map)) error
	// Edit runs fn against the named map part and publishes what it produced.
	Edit(part string, fn func(*crdt.Map) ([]crdt.MapOp, error)) error
}

A Store is where a plan keeps its parts: a composite held on its own, or one shared with everybody else over a session.

The two methods are the whole of what a plan needs, and they are the shape a session already has: read the map, or change it and say what changed.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL