Chapter 7: Item / AbstractStruct #
In
Chapter 6: StructStore, we opened the warehouse door and saw sorted shelves of structs. Now it is time to examine the bricks themselves. Imagine Alice and Bob are working on a shared text note. Alice types "Hello" and sends it to Bob. Later Bob deletes "Hello". How does Yjs remember that "Hello" existed but is now invisible? The answer is built from tiny, carefully placed building blocks called structs — especially the Item.
What problem does Item / AbstractStruct solve? #
A Yjs document is not just a JavaScript object. If Alice types "Hello", Yjs cannot simply store the string "Hello" in a plain variable. Why? Because Bob might also be typing in the same note, offline. When Bob later changes something, Alice’s document must be able to merge the two versions perfectly.
That means Yjs needs a precise way to store:
- text that was inserted
- formatting that was applied
- data embedded in a shared type
- deletions that should be hidden, but not forgotten
Item is one kind of struct that handles all of this. AbstractStruct is the base class that gives every struct a common shape.
You can think of AbstractStruct as the shape of a brick. Item is a brick that actually carries content. There are also special bricks: GC and Skip. Every brick in the
StructStore knows exactly where it belongs, and together they form the wall of a collaborative document.
Key Concept 1: AbstractStruct is the brick template #
AbstractStruct is the base class for all structs in Yjs. It is not used directly in app code, but it exists in the source at src/structs/AbstractStruct.js.
A struct needs at least two things:
id: where it comes from (client+clock)length: how much space it takes up on that client’s timeline
Here is a simplified version:
// simplified from src/structs/AbstractStruct.js
export class AbstractStruct {
constructor(id, length) {
this.id = id
this.length = length
}
}
Think of AbstractStruct like a blank brick mold. It says: every brick has a position and a size. But it doesn’t say what the brick is made of yet.
Key Concept 2: Item is a content brick #
The most important struct in Yjs is Item. It extends AbstractStruct and adds the details needed to store real content.
An Item has fields that help it find its place in the document:
| Field | What it means |
|---|---|
id | The brick’s address: who created it and where |
content | The actual data: text, map value, array element, formatting, etc. |
left | The item to the left of this item in the current list |
right | The item to the right of this item in the current list |
origin | A reference to an item that was before this item when it was created |
rightOrigin | A reference to an item that was after this item when it was created |
parent | The shared Yjs type that owns this item |
parentSub | The key in a Y.Map, if the parent is a map |
deleted | A tombstone flag: false means visible, true means deleted |
Here is a very simplified Item class:
// very simplified from src/structs/Item.js
export class Item extends AbstractStruct {
constructor(id, parent, parentSub, content) {
super(id, content.getLength())
this.parent = parent
this.parentSub = parentSub
this.content = content
this.deleted = false
}
}
The content has a getLength() method. For text, that is the number of characters. For an array value, it is usually 1. For an embedded object, it is 1.
You don’t create Items yourself. When you call .insert(), .set(), or .push() on a shared type, Yjs creates and places these items for you.
Key Concept 3: Item content can be different types #
An Item is like a brick, but the material inside the brick can change.
Yjs has many content classes defined in src/structs/Item.js, including:
ContentString— plain textContentFormat— formatting like bold or italicContentEmbed— embedded data like an image or videoContentDeleted— a deleted rangeContentJSON— JSON-like valuesContentAny— arbitrary valuesContentDoc— nested Yjs documents
For example, when Alice types "Hello" in a text note, Yjs creates an Item whose content is a ContentString with the string "Hello".
When she formats part of the text as bold, Yjs creates an Item with ContentFormat containing { bold: true }.
This is why Item is so important: it is a single brick shape that can carry almost any kind of collaborative data.
Key Concept 4: GC and Skip are special bricks #
Item is not the only kind of struct. Yjs also has GC and Skip.
GC stands for “garbage collection.” Sometimes Yjs cleans up deleted content. When it does, it may replace deleted items with a GC marker. The marker says:
This clock range existed, but there is no content here anymore.
Skip is another placeholder. It represents an empty range in the clock timeline. Think of it like a spacer brick in a wall: it keeps the wall levels, but it doesn’t carry any meaning.
Both GC and Skip extend AbstractStruct, but they are not Items. They are simpler bricks.
// simplified from src/structs/GC.js
export class GC extends AbstractStruct {
constructor(id, length) {
super(id, length)
}
}
Skip looks similar. You probably won’t create these directly either. Yjs uses them internally to keep the timeline consistent.
Use case: Alice inserts, Bob deletes #
Let’s see how Item and AbstractStruct help solve a real case.
Alice creates a document and inserts "Hello":
const aliceDoc = new Y.Doc()
const note = aliceDoc.getText('note')
note.insert(0, 'Hello')
Inside Alice’s document, Yjs creates an Item with:
content=ContentString("Hello")length= 5deleted= false
Now Alice sends an update to Bob:
const bobDoc = new Y.Doc()
Y.applyUpdate(bobDoc, Y.encodeStateAsUpdate(aliceDoc))
Bob’s document now has the same Item in Bob’s StructStore.
Now Bob deletes the text:
const bobNote = bobDoc.getText('note')
bobNote.delete(0, 5)
console.log(bobNote.toString()) // ''
Bob’s note looks empty. But the Item is still inside Bob’s StructStore. It has simply been marked as deleted:
const helloItem = bobDoc.store.clients.get(aliceDoc.clientID)[0]
console.log(helloItem.deleted) // true
This is the magic of tombstones. The word "Hello" still exists as an Item, but Yjs treats it as invisible.
Later, if Bob sends his changes back to Alice:
Y.applyUpdate(aliceDoc, Y.encodeStateAsUpdate(bobDoc))
console.log(note.toString()) // ''
Alice’s note also becomes empty. Both documents agree: the item is deleted.
Under the hood: what happens when you insert text? #
Let’s walk through what happens inside Yjs when you call:
note.insert(0, 'Hello')
Here is a simple picture:
Step by step:
- You call
insert. - A Transaction opens.
- Yjs creates a new
Item. - The
Itemgets anidlike{ client: aliceDoc.clientID, clock: 0 }. - The
Itemgets itscontentandlength. - The
Itemis integrated into theStructStore. - The transaction finishes and the document can read the new text.
This is why the Item and StructStore work together. The Item is the brick; the StructStore is the shelf where the brick is placed.
A closer look at internal code #
The real Item class in src/structs/Item.js is much larger, but the basic idea is simple.
When an item is integrated, Yjs needs to link it with its neighbors in the shared type. The simplified integration looks like this:
// simplified integration idea from Item.js
integrate(transaction) {
transaction.doc.store.integrate(this)
}
The real integration does more, including setting left and right references. But the main point is: the item is added to the store, and the store keeps it sorted by id.
When Yjs reads an update from another peer, it decodes the update into structs. One of those structs may be an Item. To decode it, Yjs reads the id, the content, and some relationship fields. Then it inserts the Item into the local StructStore.
In global.d.ts, Yjs declares these types for TypeScript:
// global.d.ts (simplified)
declare type AbstractStruct = import('./src/structs/AbstractStruct.js').AbstractStruct
declare type Item = import('./src/structs/Item.js').Item
declare type GC = import('./src/structs/GC.js').GC
declare type Skip = import('./src/structs/Skip.js').Skip
These type declarations don’t change how Yjs works. They just help editors and TypeScript know what kind of objects they are looking at.
Using parentSub for maps #
For a Y.Map, the parentSub field is very helpful.
Suppose you create a profile map:
const doc = new Y.Doc()
const map = doc.getMap('profile')
map.set('name', 'Ada')
Inside Yjs, an Item is created. Its parent is the map, and its parentSub is the key "name":
const item = doc.store.clients.get(doc.clientID)[0]
console.log(item.parentSub) // 'name'
So if a map has multiple keys, Yjs can find all items for "name" by looking at parentSub.
Why deleted items stay behind #
You may wonder: why not just remove a deleted Item from the store?
The answer is collaborative concurrency.
Imagine Bob deletes "Hello" while Alice, who is offline, inserts " world" right after it. If Bob’s document removed the "Hello" item completely, Alice’s new item might not know where it should go when updates are merged.
By keeping the deleted item as a tombstone, Yjs preserves the original wall structure. The deleted brick is still there, but it is painted invisible. New bricks can still find their correct position.
This is why Item has a deleted flag. It is one of the most important fields in Yjs.
Common beginner questions #
1. Do I ever create Item objects myself? #
No. You create shared types like Y.Text, Y.Array, and Y.Map. Yjs creates and manages items for you.
2. Can I inspect items in my document? #
You can, for learning:
const doc = new Y.Doc()
doc.getText('note').insert(0, 'Hello')
const item = doc.store.clients.get(doc.clientID)[0]
console.log(item.content)
But this is internal implementation detail. Don’t depend on it in a real app.
3. Why are deleted items still in memory? #
Because they help Yjs merge concurrent edits correctly. Later, Yjs may garbage-collect them with GC markers, but that is also handled internally.
Conclusion #
You have now met the foundational bricks of Yjs.
AbstractStructis the base class for all structs.Itemis the content-carrying struct.Itemstores text, formatting, array values, map values, embedded data, and more.GCandSkipare special non-content structs.- The
StructStorestores these structs in sorted order. - Deleted items remain as tombstones with
deleted = true.
You don’t need to construct items yourself, but knowing they exist helps you understand how Yjs really works.
Now that we know what the bricks look like, it’s time to learn how each brick gets its unique address.
Next up: ID
Generated by AI Codebase Knowledge Builder