Real-time collaboration
A3S Office exposes a transport-neutral Yjs collaboration boundary. The host
owns rooms, authentication, authorization, network providers, persistence, and
the Y.Doc; the editor owns the format-specific binding and interaction.
Two browser clients can edit the same artifact in real time, publish a shared participant roster, and project remote cursors, selections, cells, scene objects, pages, or annotations. A native Yrs replica can join that same state through the CLI, MCP, or A3S Code without replacing the complete Office file.
Markdown, Document, Presentation, Spreadsheet, and PDF have browser editor bindings. Native Yrs replicas expose the same Yjs v1 state-vector and update protocol through the CLI and MCP.
For a complete authenticated and persistent service, run the A3S Boot collaboration server. It includes the Rust backend, browser transport adapter, ACL configuration, and integration tests described below as host responsibilities.
Attach a host transport
createOfficeCollaborationTransportBinding is a small reference adapter for a
host-owned room channel. Its envelope binds every message to the protocol
version, artifact ID, artifact kind, namespace, and Yjs client ID. Payloads are
bounded standard Yjs v1 state vectors or updates; incremental updates retain a
validated Office actor/operation origin when one exists.
Every peer sends a fresh state vector after its subscription becomes active and after each reconnect. A peer answers with only the missing update. Repeating the handshake is safe, and inbound updates are not echoed back into the same channel. The host still owns delivery guarantees, offline buffering, authorization, persistence, and room membership. A transport message is not an authorization token.
Drive a native coding agent
Native replicas use Yrs with the same protocol roots and standard Yjs v1
updates. A host can run collab session as a JSONL live peer while another
agent process makes a typed local change:
Locate PDF form values and FreeText contents with collab find first:
With --actor-name, the JSONL session also owns an ephemeral, in-memory Yrs
Awareness peer. Forward outbound-awareness.message to the room's
collaboration.awareness event, pass room Awareness payloads back as
receive-awareness, and pass disconnect notifications as peer-left. A
native agent publishes its current location without touching the durable
replica:
Each valid remote change emits a sorted presence snapshot that uses the same
actor, mode, activity, and format-location schema as the browser participant
roster. Reconnect clears stale remote state and republishes the local agent;
orderly close emits an Awareness tombstone. Presence state and clocks never
enter checkpoints, updates, or operation receipts.
Use ready.clientId for the connection hello and all room envelopes. It is
fresh for each Presence-enabled process; ready.replicaClientId remains the
stable Yrs author ID inside durable updates.
The host forwards session outbound envelopes to its authenticated room and
delivers room envelopes back with stable host delivery IDs. mutate emits a
minimal incremental update through the durable event log, so the running
session publishes changes made by separate CLI or MCP processes. Markdown
offsets are UTF-16 code units, matching browser Yjs; ranges that split a
surrogate pair fail closed. Document replacement edits ProseMirror
Y.XmlText directly, may span formatting runs inside that text node, preserves
the first replaced character's marks, and fails unless expectedMatches
matches current state. Locate that count with collab find /
office_collaboration_find first. Optional occurrence then changes only that
1-based match; omit it to replace every match. Each changed paragraph rotates
its Word textId once.
document-insert-paragraph creates a plain paragraph before or after a stable
paragraph identity in a bounded top-level section, nested list item, table
cell/header, or blockquote path. document-delete-paragraph additionally
requires the current textId and complete text, preserves required container
blocks and list-leading paragraphs, and refuses inline-atom, comment-mark, and
tracked-change cases. Table-contained edits rotate every identified ancestor
row's rowTextId atomically; partial row identities fail before writing. Word
identities are uppercase eight-digit positive 31-bit hexadecimal values.
document-set-page-color/document-clear-page-color and
document-set-track-changes/document-clear-track-changes update their
independent typed option fields. Explicit clear variants keep missing set
fields fail-closed. Locate PDF form values and FreeText (type 3) contents
with collab find / office_collaboration_find first; hits return fieldId
or annotationId/pageIndex/annotationType in collection order. Following
edits stay pdf-set-form-value and pdf-update-annotation—there is no PDF
body replace-text. pdf-set-form-value updates an existing conflict-local
form value leaf or creates the same typed presence/fields/order record used
by browser Yjs. The fully-qualified fieldId is bounded to 512 UTF-16 code
units without trimmed whitespace. PDF source bytes remain host-owned, so the
authenticated viewer/save workflow must still verify that the field exists and
is writable. pdf-create-annotation accepts a complete portable EmbedPDF
FreeText, Highlight, Underline, StrikeOut, or Ink object and writes a
browser-compatible source: created record plus an immutable creation claim.
pdf-update-annotation recursively guards changed JSON leaves, so unrelated
browser/native edits merge while a stale edit to the same leaf fails without
an update. ID, source page, and annotation type remain immutable.
pdf-delete-annotation verifies those identities and writes an irreversible
tombstone. Use --mutation-input <file> for larger or multiline JSON.
pdf-propose-redaction appends bounded, positive source-page rectangles and
pdf-decide-review appends the only final decision for an existing redaction
or page operation. Both are irreversible audit records with canonical creation
claims. The replica manifest supplies proposedBy/actorId; caller JSON cannot
forge them. Native timestamps must use canonical UTC, and conflicting ID reuse,
missing targets, or a second final decision fail before any durable update.
pdf-propose-page-rotation accepts unique page indices and 90, 180, or 270
clockwise degrees; pdf-propose-page-deletion requires a non-empty proper
subset; and pdf-propose-page-reorder requires a complete source-page
permutation. These page-operation proposals use the same append-only claim and
replica-attribution rules and never change or synchronize source bytes.
The equivalent MCP tool is office_collaboration_mutate; coding agents should
poll office_collaboration_events with a persisted cursorSequence for
resumable delivery. Typed canonical content mutations require edit mode.
Document comment mutations accept edit or comment; deletion in comment
is limited to the replica actor's own records. Raw remote updates remain
receivable by every mode so receive-only peers still converge. Document
suggestion creation requires suggest; final suggestion decisions require
edit. Projection schema v3 returns both live proposals and immutable final
decisions through collab read and office_collaboration_read. Do not
construct private ProseMirror/Yjs marks in an agent; use the closed mutation
variants described below. suggest on non-Document formats and comment mode
on non-Document formats remain receive-only.
An authenticated host may attach a typed source origin to a browser Update. Native receipts and events preserve that source actor/operation separately from the host delivery operation ID and re-emit it on live transport. Origin is audit attribution, not proof of authorization; the host must authenticate it before delivery.
Publish ephemeral presence
Pass provider-owned Yjs Awareness to the session, then create one typed presence controller for that local client. The provider remains responsible for synchronizing Awareness; the document transport adapter above deliberately does not persist or relay ephemeral state.
Pass that same host-owned controller beside its exact session to any React, Vue, or Web Component editor. A mismatched artifact, kind, Y.Doc client, actor, namespace, or mode fails closed instead of showing another room's participants.
Editable status bars and preview/PDF toolbars render one responsive, keyboard-accessible participant roster. It distinguishes local and remote humans, agents, and system actors and summarizes activity, mode, and typed format location. The host still owns the controller lifecycle and Awareness transport. Each mounted editor publishes its local location and renders remote locations in its editing surface. Document and Markdown draw text selections and carets, Spreadsheet uses the native Fortune Sheet cell-presence layer, Presentation frames stable object IDs, and PDF shows peers on the current page or annotation. Activating a remote roster row navigates and focuses that location; passive Awareness changes never move the local viewport, selection, or focus.
Locations are format-specific: Document uses ProseMirror anchor/head model
positions; Markdown identifies source UTF-16 offsets or visual
ProseMirror positions (a missing surface remains backward-compatible source
mode); Spreadsheet uses bounded zero-based ranges; Presentation uses one slide
plus stable element IDs; and PDF uses a zero-based page plus an optional
annotation ID. Remote states with the wrong artifact identity, protocol,
actor, mode, or location shape are ignored. Stale locations outside the live
model fail closed. Presence is advisory UI data, not canonical content,
durable audit history, or permission enforcement.
Open a Markdown session
Initialize only after the provider has completed its initial synchronization. For a new room, exactly one server or elected bootstrap owner seeds the artifact. Other clients wait until initialized metadata arrives before mounting the editor.
The example assumes the component renders only after the session has
initialized metadata. initializeOfficeMarkdownCollaboration is idempotent
after a synchronized initialization, but intentionally reports concurrent
independent seeds as an error.
Open a Document session
Document uses a ProseMirror Y.XmlFragment for structured content and
conflict-local maps for document options, comment threads, immutable tracked-
change decisions, and bibliography sources. Initialize after provider sync
just like Markdown:
The fragment includes the structured section layout, comment anchors, and tracked-change marks. Comment thread bodies/replies, final tracked-change decisions, and bibliography sources are stored by stable ID, so independent records can merge without replacing a document-sized JSON value. Append-only creation claims make identical offline retries idempotent and reject conflicting assignments of the same stable ID after synchronization. Unsupported OOXML package parts remain the host's import/export responsibility and must not be projected into Yjs as a full ZIP replacement.
Review a Document in comment mode
Open an already initialized Document with an authenticated actor and
mode: 'comment' when a participant should review without editing canonical
content. Initialization must have been completed by the server or a separate
authorized edit bootstrap session before this reviewer mounts:
The reviewer selects real Document text and uses Add comment. The editor
writes one stable thread to document.comments, appends its ID to
document.comment-order, appends an immutable record claim, and applies the
matching documentComment mark to the exact ProseMirror text range. Replies
append under that thread. Any commenter can resolve or reopen an existing
thread; a comment-mode actor can delete only its own comment or reply. The
thread remains durable and reports detached: true if a later authorized edit
removes every anchor mark.
Comment mode keeps the Document body selectable so review anchors can be created, but normal typing, formatting, page options, bibliography changes, and structural edits fail closed. Its undo/redo history contains only that local actor's review transactions; remotely received comments and replies are never added to the local undo stack.
Native agents use the same records through the projection-v3 read/mutate
contract. Read immediately before deciding so paragraphId, textId, anchor
text, UTF-16 offsets, and the optional state-vector precondition describe the
same current paragraph:
The author on a new native comment or reply must match the display name
authenticated by the room ticket. The replica actor ID is written to
actorId; caller JSON cannot replace it. Identical stable-ID retries are
idempotent, conflicting ID reuse and stale anchors fail without an update, and
comment-mode deletion is ownership restricted. collab read and
office_collaboration_read return projection schema version 3, including
comments, replies, resolution and detached state, live suggestions, immutable
changeDecisions, and every comment or suggestion placement's exact identity,
UTF-16 offsets, and current text.
Suggest changes to a Document
Open an initialized Document with an authenticated actor and mode: 'suggest'
when a participant may propose text changes but must not edit canonical content
or make final review decisions:
The editor forces tracked text proposals on for this surface. Typing creates an
insertion; deleting existing text creates a deletion; replacing text creates a
paired deletion and insertion. Each documentChange mark carries a stable ID,
kind, authenticated actorId, display-name author, and canonical UTC date.
The suggester does not receive canonical formatting, structure, page-option,
comment, accept, or reject controls. Undo and redo may withdraw or restore that
actor's own insertion proposals, but cannot rewrite another actor's proposal or
change the canonical text targeted by a deletion.
An edit participant reviews the same marks in the tracked-changes panel. An
accept or reject operation applies the visible result and appends one immutable
record to document.change-decisions, with its ID in
document.change-decision-order, in the same Yjs transaction. The record keeps
the suggestion ID/kind/text, proposer actor/name/time, final decision, and
decider actor/name/time. A proposal can have only one final decision; identical
offline retries converge, while a different stale decision fails closed. The
editor clears older local history after the decision so undo cannot resurrect a
decided mark while leaving its audit record behind.
The A3S Boot backend does not trust the ticket mode or caller-authored Yjs bytes alone. While holding the durable room lock, it applies the update to a candidate Yrs document and verifies that the canonical Document projection and every non-content root are unchanged. It then admits only new attributed insertion or deletion marks, safe changes to the authenticated actor's existing proposals, or withdrawal of that actor's own proposal. Forged identity, non-canonical timestamps, structure/format/options/comments changes, another actor's proposal change, and unresolved Yjs dependencies are rejected before persistence and broadcast. A replacement follows the same rule as one deletion plus one insertion.
Native agents use the same review model without constructing private Yjs marks.
Projection v3 lists each suggestion's stable ID, kind, actor, author, timestamp,
text, and exact paragraph/text placement. Join an actor-scoped suggest replica,
read that projection immediately before proposing, and create an insertion,
deletion, or atomic replacement with one closed mutation:
Use decision: "reject" to reject. Suggestion creation is allowed only in
suggest; final decisions are allowed only in edit. Exact stable-ID retries
are idempotent. Stale paragraph/text identity, split UTF-16 boundaries,
overlapping proposals, reused suggestion IDs, mismatched reviewed identity or
text, and a conflicting final decision fail the whole operation before a
durable log entry. A successful decision removes the live marks, rotates the
affected paragraph and ancestor table-row text identities, and appends one
immutable browser-compatible changeDecisions record per suggestion.
Synchronize character-formatting revisions
An edit participant with tracked changes enabled can apply bold, italic,
underline, strike, subscript, superscript, font family, font size, text color,
highlight, or Word grid formatting to existing text. The browser writes one
formatting change mark containing a bounded serialized snapshot of the prior
direct marks. The visible content carries the new formatting. Accept removes
only the change mark; reject restores the prior marks without deleting or
inserting text. Either path and its immutable changeKind: "formatting" audit
record commit in one Yjs transaction.
The mark uses the normal document.content Y.XmlFragment; there is no private
side channel. A browser update therefore follows the complete production path:
- the provider sends a bounded standard Yjs v1 update with an authenticated
editroom ticket; - A3S Boot applies it to a candidate Yrs document under the durable room lock;
- the Office validator accepts only the known mark fields, a bounded author, date and ID, and a prior-mark snapshot containing only supported formatting types and scalar attributes;
- the service persists the update before acknowledgement and fan-out; and
- every browser or native replica converges through the next state-vector exchange, including after duplicate or reordered delivery and restart.
The suggest boundary deliberately remains text-only. Its semantic comparison
strips insertion and deletion effects but retains existing formatting-change
marks, so a suggester cannot create, remove, rewrite, or hide one while making a
text proposal. NativeOfficeCollaborationDocumentChangeKind includes
formatting for shared decision projection, but
NativeOfficeCollaborationDocumentSuggestionKind and the current closed
document-suggestion-* mutations still accept only insertion and deletion.
Supported DOCX run-property revisions use the same model: strict or
transitional w:rPrChange imports as a Formatting card and exports back to
native OOXML after synchronization and review. In the Playground choose
体验格式修订, open 审阅, then 查看修订(3); the deterministic
A3S Test scenario proves that rejection preserves the text while removing the
bold and character-revision marks without resolving the paragraph revision.
Synchronize paragraph-formatting revisions
Tracked paragraph commands attach one paragraph-formatting identity and a
canonical prior-property snapshot directly to every affected paragraph or
heading node. The snapshot includes alignment, direction, indentation,
spacing and line rules, pagination controls, contextual spacing, outline
level, tab stops, borders, shading, and collapsed state. A command spanning
multiple paragraphs uses one identity, so a reviewer can resolve the complete
intent atomically. Further formatting while that intent is pending keeps its
original snapshot and ID.
An edit participant accepts by retaining the current node properties and
clearing the revision attributes, or rejects by restoring the complete prior
properties without touching text. The visible decision and immutable
changeKind: "paragraph-formatting" audit record commit in one Yjs transaction
and converge between browser clients. Rust/Yrs projection reads that distinct
decision kind, and a browser-generated fixture remains readable after a
durable service restart.
The suggest boundary remains text-only. Browser admission and the A3S Boot
candidate-state authorizer require every existing paragraph-revision field and
snapshot to remain unchanged, so a suggester cannot create, remove, rewrite,
or hide one while submitting text. Normal attributed insertions and deletions
continue to pass through that same protected document.
Strict or transitional DOCX w:pPrChange uses this model and round-trips as a
Paragraph Formatting card. Malformed, duplicated, namespace-spoofed, or
unsupported property changes remain structural diagnostics. The focused
word-paragraph-formatting-revision.acl A3S Test opens the public Playground,
checks the 段落格式 card, rejects it, and proves that alignment,
indentation, spacing, and line height return while text and the independent
character revision remain.
Synchronize ordered-list numbering revisions
Tracked list-style and starting-number commands attach one numbering identity
and a canonical prior-numbering snapshot to the ordered-list node. The browser
therefore synchronizes one list-range intent rather than one review object per
paragraph. Accept retains the current decimal, letter, or Roman style and start
value; reject restores the complete prior list attributes without touching any
list item. The visible decision and immutable changeKind: "numbering" audit
record commit in one Yjs transaction.
The same standard Yjs v1 update is readable by Rust/Yrs. Native projection
recognizes the Numbering decision kind, persists the browser-generated fixture,
and reconstructs it after restart. Browser admission and the A3S Boot
candidate-state authorizer compare the protected ordered-list attributes, so an
authenticated suggest participant may add attributed text proposals but
cannot create, remove, rewrite, or hide an existing numbering revision.
Strict and transitional DOCX w:numberingChange records use the same model for
unambiguous single-level and bounded multi-level decimal, letter, Roman, and
bullet (nfc 23) lists at the current w:ilvl. Sibling levels in w:original
may carry other ST_NumberFormat values as opaque prior text. Consecutive native
per-item records group only when their identity and old-number sequence agree;
malformed, conflicting, unsupported current-level picture formats, or
namespace-spoofed forms stay on the structural-diagnostic path. The deterministic
word-numbering-revision.acl A3S Test opens the public Playground, checks the
编号格式 card, rejects it, verifies the original Roman numbering and intact
list text, then verifies Undo, accessibility, and clean browser diagnostics.
Synchronize move revisions
Word move revisions are two physical records for one intent. The browser admits
the bounded text-only subset only when w:moveFrom and w:moveTo share a numeric
identity, author, UTC date, and exact text. Each side remains in the canonical
document.content fragment with a move kind and a from or to role; the
review pane presents one card and navigates to the destination side. A decision
therefore resolves the pair atomically: accept removes the source and keeps the
destination, while reject removes the destination and keeps the source. One
immutable changeKind: "move" audit record represents that decision.
The same Yjs update is readable by Rust/Yrs. Native projection accepts the
move decision kind and preserves the paired review record across restart, but
the closed document-suggestion-* mutations remain insertion/deletion-only:
move marks are created and decided by an authenticated edit browser binding.
Stale, mismatched, duplicate, or unpaired sides fail closed before a decision or
audit entry is written. Strict and transitional DOCX export rewrites transient
wrappers to native w:moveFrom/w:moveTo records; rich runs, range markers,
relationship-bound objects, and other unsupported shapes stay structural
diagnostics rather than being silently flattened.
Open a Presentation session
Presentation stores slide/master/layout order separately from ID-keyed records. Each slide and design record owns ID-keyed scene objects; comments also merge by stable ID. Scalar geometry, styles, notes, transitions, charts, and other JSON-compatible object fields update independently instead of replacing one serialized deck:
The binding applies previous -> next differences to shared records. A stale
host snapshot therefore cannot delete a slide or object that arrived remotely
unless the local operation explicitly removed that stable ID. Local undo/redo
tracks only the mounted binding's origin.
Mutate or reorder one Presentation element from Rust, CLI, or MCP
Native clients use the closed presentation-create-element,
presentation-update-element, presentation-move-element, and
presentation-delete-element variants. Every operation names a stable
containerKind (slide, master, or layout) and containerId; callers
never construct internal Yjs roots. Creation accepts one complete element and
can place it after an active stable element:
Creation writes a canonical immutable claim. An identical retry is a no-op; assigning different content to the same ID, reusing a tombstoned ID, or naming a missing insertion anchor fails without a durable update. Update supplies the complete observed element plus the complete desired element:
Only changed top-level fields are written. Unrelated concurrent fields merge;
a stale change to the same field returns
office.collaboration.mutation_match_conflict atomically. Element id and
type are immutable. Reordering uses predecessor IDs rather than array
indexes:
expectedAfterElementId is the predecessor observed by the caller;
afterElementId is the requested predecessor, and null means the first
position in the element-order array. A move already at its destination is an
idempotent no-op. Otherwise a stale observed predecessor, missing or deleted
destination anchor, deleted element, or self-anchor fails without a durable
update. Only the moved element's order entries are removed and reinserted; its
record fields and the surrounding container remain untouched. Delete passes
the exact complete current element as
expectedElement; success removes it from visible order and writes a durable
tombstone, so its ID cannot be reused. The same browser-compatible claims and
tombstones apply to slides, masters, and layouts.
Open a Spreadsheet session
Spreadsheet uses ordered, ID-keyed records for sheets and named ranges plus sparse, recursively field-addressed cells and table records. Formula, style, number-format, hyperlink, note, and independent table-design fields can therefore merge without replacing a dense worksheet or serialized workbook:
Each sheet owns a tables record map and a separate tableOrder array. A
ListObject record is keyed by its stable browser ID and recursively stores its
name/display name, optional OOXML numeric ID, range, ordered column definitions,
supported filters, header/totals flags, built-in style identity, totals labels,
native totals functions, bounded custom totals formulas, and first/last column
plus row/column stripe options. Reordering or editing one table does not
replace its siblings or the surrounding sheet.
Creating a table appends an immutable table creation claim scoped to its
parent sheet. The claim makes an ID collision or later ID reuse fail closed,
including after the record has been deleted. The shared-input validator also
requires one unique non-empty column name per covered worksheet column,
in-range ordered coordinates, at least one body row, unique in-range filter
columns, a valid Light 1–21, Medium 1–28, Dark 1–11, or no-style identity,
non-overlapping tables, and workbook-wide uniqueness against other table and
defined names.
Filter criteria are a closed union rather than arbitrary shared JSON. Value
filters accept at most 10,000 unique entries, each text operand is limited to
32,767 XML-compatible characters, and all filter text in one table is limited
to 1,048,576 UTF-8 bytes. Explicit positive and negative wildcard variants
retain native OOXML *, ?, and ~ expressions inside the same nonrecursive
custom condition contract. Top/bottom counts are 1–500, percentages are 1–100,
dynamic filters use the documented enum, and filters require an enabled header.
Snapshot replacement is translated into record and field patches. For example,
one disconnected client may rename Orders while another enables column
stripes; after standard Yjs updates are exchanged, both clients contain the
new name and the new option. Stale same-field replacement is checked against
the shared record and fails instead of discarding an unrelated remote value.
Focused two-client tests cover initial table creation, independent design
edits, totals-row field updates, update exchange, and exact final convergence.
Native Rust, CLI, MCP, and A3S Code already expose the separate closed
add-spreadsheet-table, set-spreadsheet-table, and typed remove file
contract, including guarded range changes and structured-reference rewrites.
The native collaboration bridge does not yet project those file mutations into
the browser table-record maps. That bridge needs typed optimistic guards and
record-aware transforms; clients must not construct or patch internal Yjs maps
directly.
Sheet activation, cell selection, zoom, calculation caches, and derived chart previews are local view state. They are not persisted in canonical Yjs content and remain stable when a remote workbook update is projected into the editor. Canonical edits and undo/redo use the collaboration binding; one client cannot undo another client's transaction.
Mutate Spreadsheet cells from Rust, CLI, or MCP
Native clients should use the closed spreadsheet-set-cell and
spreadsheet-delete-cell mutations for one coordinate, or
spreadsheet-batch-cells for one bounded multi-cell gesture, instead of
constructing Yjs fields. Rows and columns are zero-based, and sheetId is the
stable sheet identity rather than its display name:
The set mutation recursively compares the observed and next cells with the
current shared cell. It writes only changed leaves, so a concurrent note or
style can merge with a value/formula edit; a stale change to the same leaf
fails before any durable update. Pass expectedCell: null only to create a
coordinate that was observed blank. Delete supplies the exact complete current
cell:
One batch accepts 1 to 4,096 distinct coordinates in the same sheet. A present
nextCell uses the same recursive set/create guard; nextCell: null requires
an exact complete expectedCell and deletes that coordinate:
Every change is preflighted against one shared sheet snapshot. If any guard is invalid or stale, no cell, presence marker, dense row length, or durable event changes. A successful gesture commits all changes in one Yjs transaction.
Dense worksheets retain their matrix dimensions and safely extend row lengths
when needed. Sparse worksheets stay in celldata mode, and the first write to
an empty worksheet also uses the sparse projection. The native boundary checks
Excel coordinate limits, sheet identity, projection consistency, cell JSON
depth and size, unsafe object keys, malformed fields, and orphaned cell state
before writing.
Open a PDF session
PDF collaboration never places the source PDF bytes in Yjs. The bootstrap owner supplies an immutable lowercase SHA-256 digest, exact byte length, and page count, then initializes typed overlay and audit records:
Mount the initialized session with the exact source bytes. The viewer verifies the SHA-256 digest, byte length, and PDFium page count before accepting edits:
Annotations and form values are locally undoable. Annotation deletion is a
durable tombstone. Signature placements, redaction proposals, reviewed page
operations, and final decisions are append-only audit records and are never
put on the undo stack. Signature records refer to a host-owned asset ID and
hash rather than synchronizing private appearance bytes. Rendered bitmaps,
search indexes, current page, zoom, and selection remain local. The viewer
captures source annotations and form values as an immutable baseline, then
projects only shared overlays. Local history is Yjs-owned, so the embedded
viewer cannot undo a remote transaction or create a duplicate undo step.
onSave still persists the complete PDF Blob; collaboration does not
synchronize source bytes or replace the host's save/versioning contract.
The browser projection supports the five annotation tools exposed by the editor: free text, highlight, underline, strikeout, and ink. Stamp, signature, and other annotation kinds that require binary creation context fail closed until the host asset port is implemented. Shared form records must name an existing writable PDF field.
Contract
onChange receives complete snapshots for local and remote shared changes. Use
Yjs updates for durable collaboration persistence; do not turn every snapshot
back into a full-document write.
Vue and Web Components
Vue exposes the same session as the collaboration prop:
Custom elements accept complex values through JavaScript properties:
Do not serialize the session or a Y.Doc into an HTML attribute.
