rework and document api
This commit is contained in:
parent
77687d94e6
commit
8c36f67f0b
239
README.v13.md
239
README.v13.md
@ -1,13 +1,13 @@
|
||||
# 
|
||||
> The shared editing library
|
||||
> A CRDT framework with a powerful abstraction of shared data
|
||||
|
||||
Yjs is a library for automatic conflict resolution on shared state. It implements an [operation-based CRDT](#Yjs-CRDT-Algorithm) and exposes its internal model as shared types. Shared types are common data types like `Map` or `Array` with superpowers: changes are automatically distributed to other peers and merged without merge conflicts.
|
||||
Yjs is a [CRDT implementation](#Yjs-CRDT-Algorithm) that exposes its internal data structure as *shared types*. Shared types are common data types like `Map` or `Array` with superpowers: changes are automatically distributed to other peers and merged without merge conflicts.
|
||||
|
||||
Yjs is **network agnostic** (p2p!), supports many existing **rich text editors**, **offline editing**, **version snapshots**, **shared cursors**, and encodes update messages using **binary protocol encoding**.
|
||||
Yjs is **network agnostic** (p2p!), supports many existing **rich text editors**, **offline editing**, **version snapshots**, **undo/redo** and **shared cursors**. It scales well with an unlimited number of users and is well suited for even large documents.
|
||||
|
||||
* Chat: [https://gitter.im/y-js/yjs](https://gitter.im/y-js/yjs)
|
||||
* Demos: [https://github.com/y-js/yjs-demos](https://github.com/y-js/yjs-demos)
|
||||
* API Docs: [https://yjs.website/](https://yjs.website/)
|
||||
* Benchmarks: [https://github.com/dmonad/crdt-benchmarks](https://github.com/dmonad/crdt-benchmarks)
|
||||
|
||||
# Table of Contents
|
||||
|
||||
@ -16,18 +16,15 @@ Yjs is **network agnostic** (p2p!), supports many existing **rich text editors**
|
||||
* [Providers](#Providers)
|
||||
* [Getting Started](#Getting-Started)
|
||||
* [API](#API)
|
||||
* [Transaction](#Transaction)
|
||||
* [Offline Editing](#Offline-Editing)
|
||||
* [Awareness](#Awareness)
|
||||
* [Working with Yjs](#Working-with-Yjs)
|
||||
* [Shared Types](#Shared-Types)
|
||||
* [Y.Doc](#Y.Doc)
|
||||
* [Document Updates](#Document-Updates)
|
||||
* [Relative Positions](#Relative-Positions)
|
||||
* [Miscellaneous](#Miscellaneous)
|
||||
* [Typescript Declarations](#Typescript-Declarations)
|
||||
* [Binary Protocols](#Binary-Protocols)
|
||||
* [Sync Protocol](#Sync-Protocols)
|
||||
* [Awareness Protocol](#Awareness-Protocols)
|
||||
* [Auth Protocol](#Auth-Protocol)
|
||||
* [Yjs CRDT Algorithm](#Yjs-CRDT-Algorithm)
|
||||
* [Evaluation](#Evaluation)
|
||||
* [Existing shared editing libraries](#Exisisting-Javascript-Products)
|
||||
* [Existing shared editing libraries](#Exisisting-Javascript-Libraries)
|
||||
* [CRDT Algorithms](#CRDT-Algorithms)
|
||||
* [Comparison of CRDT with OT](#Comparing-CRDT-with-OT)
|
||||
* [Comparison of CRDT Algorithms](#Comparing-CRDT-Algorithms)
|
||||
@ -52,7 +49,7 @@ This repository contains a collection of shared types that can be observed for c
|
||||
|
||||
### Providers
|
||||
|
||||
Setting up the communication between clients, managing awareness information, and storing shared data for offline usage is quite a hassle. **Providers** manage all that for you and are a good off-the-shelf solution.
|
||||
Setting up the communication between clients, managing awareness information, and storing shared data for offline usage is quite a hassle. **Providers** manage all that for you and are the perfect starting point for your collaborative app.
|
||||
|
||||
<dl>
|
||||
<dt><a href="http://github.com/y-js/y-websocket">y-websocket</a></dt>
|
||||
@ -60,15 +57,15 @@ Setting up the communication between clients, managing awareness information, an
|
||||
<dt><a href="http://github.com/y-js/y-mesh">y-mesh</a></dt>
|
||||
<dd>[WIP] Creates a connected graph of webrtc connections with a high <a href="https://en.wikipedia.org/wiki/Strength_of_a_graph">strength</a>. It requires a signalling server that connects a client to the first peer. But after that the network manages itself. It is well suited for large and small networks.</dd>
|
||||
<dt><a href="http://github.com/y-js/y-dat">y-dat</a></dt>
|
||||
<dd>[WIP] Writes updates effinciently in the dat network using <a href="https://github.com/kappa-db/multifeed">multifeed</a>. Each client has an append-only log of CRDT local updates (hypercore). Multifeed manages and sync hypercores and y-dat listens to changes and applies them to the Yjs document.</dd>
|
||||
<dd>[WIP] Write document updates effinciently to the dat network using <a href="https://github.com/kappa-db/multifeed">multifeed</a>. Each client has an append-only log of CRDT local updates (hypercore). Multifeed manages and sync hypercores and y-dat listens to changes and applies them to the Yjs document.</dd>
|
||||
</dl>
|
||||
|
||||
## Getting Started
|
||||
|
||||
Install Yjs and a provider with your favorite package manager. In this section we are going to bind a YText to a DOM textarea.
|
||||
Install Yjs and a provider with your favorite package manager.
|
||||
|
||||
```sh
|
||||
npm i yjs@13.0.0-80 y-websocket@1.0.0-1 y-textarea
|
||||
npm i yjs@13.0.0-81 y-websocket@1.0.0-2 y-textarea
|
||||
```
|
||||
|
||||
**Start the y-websocket server**
|
||||
@ -77,7 +74,9 @@ npm i yjs@13.0.0-80 y-websocket@1.0.0-1 y-textarea
|
||||
PORT=1234 node ./node_modules/y-websocket/bin/server.js
|
||||
```
|
||||
|
||||
**Textarea Binding Example**
|
||||
**Example: Textarea Binding**
|
||||
|
||||
This is a complete example on how to create a connection to a [y-websocket](https://github.com/y-js/y-websocket) server instance, sync the shared document to all clients in a *room*, and bind a Y.Text type to a dom textarea. All changes to the textarea are automatically shared with everyone in the same room.
|
||||
|
||||
```js
|
||||
import * as Y from 'yjs'
|
||||
@ -85,15 +84,39 @@ import { WebsocketProvider } from 'y-websocket'
|
||||
import { TextareaBinding } from 'y-textarea'
|
||||
|
||||
const provider = new WebsocketProvider('http://localhost:1234')
|
||||
const sharedDocument = provider.get('my-favourites')
|
||||
const doc = provider.get('roomname')
|
||||
|
||||
// Define a shared type on the document.
|
||||
const ytext = sharedDocument.getText('my resume')
|
||||
const ytext = doc.getText('my resume')
|
||||
|
||||
// bind to a textarea
|
||||
// use data bindings to bind types to editors
|
||||
const binding = new TextareaBinding(ytext, document.querySelector('textarea'))
|
||||
```
|
||||
|
||||
**Example: Observe types**
|
||||
|
||||
```js
|
||||
const yarray = doc.getArray('my-array')
|
||||
yarray.observe(event => {
|
||||
console.log('yarray was modified')
|
||||
})
|
||||
// every time a local or remote client modifies yarray, the observer is called
|
||||
yarray.insert(0, ['val']) // => "yarray was modified"
|
||||
```
|
||||
|
||||
**Example: Nest types**
|
||||
|
||||
Remember, shared types are just plain old data types. The only limitation is that a shared type must exist only once in the shared document.
|
||||
|
||||
```js
|
||||
const ymap = doc.getMap('map')
|
||||
const foodArray = new Y.Array()
|
||||
foodArray.insert(0, ['apple', 'banana'])
|
||||
ymap.set('food', foodArray)
|
||||
ymap.get('food') === foodArray // => true
|
||||
ymap.set('fruit', foodArray) // => Error! foodArray is already defined on the shared document
|
||||
```
|
||||
|
||||
Now you understand how types are defined on a shared document. Next you can jump to the [demo repository](https://github.com/y-js/yjs-demos) or continue reading the API docs.
|
||||
|
||||
## API
|
||||
@ -102,6 +125,8 @@ Now you understand how types are defined on a shared document. Next you can jump
|
||||
import * as Y from 'yjs'
|
||||
```
|
||||
|
||||
### Shared Types
|
||||
|
||||
<details>
|
||||
<summary><b>Y.Array</b></summary>
|
||||
<br>
|
||||
@ -110,11 +135,11 @@ import * as Y from 'yjs'
|
||||
</p>
|
||||
<pre>const yarray = new Y.Array()</pre>
|
||||
<dl>
|
||||
<b><code>insert(index:number, content:Array<object|string|number|Y.Type>)</code></b>
|
||||
<b><code>insert(index:number, content:Array<object|string|number|Uint8Array|Y.Type>)</code></b>
|
||||
<dd>
|
||||
Insert content at <var>index</var>. Note that content is an array of elements. I.e. <code>array.insert(0, [1]</code> splices the list and inserts 1 at position 0.
|
||||
</dd>
|
||||
<b><code>push(Array<Object|Array|string|number|Y.Type>)</code></b>
|
||||
<b><code>push(Array<Object|Array|string|number|Uint8Array|Y.Type>)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>delete(index:number, length:number)</code></b>
|
||||
<dd></dd>
|
||||
@ -124,7 +149,7 @@ import * as Y from 'yjs'
|
||||
<dd></dd>
|
||||
<b><code>map(function(T, number, YArray):M):Array<M></code></b>
|
||||
<dd></dd>
|
||||
<b><code>toArray():Array<Object|Array|string|number|Y.Type></code></b>
|
||||
<b><code>toArray():Array<Object|Array|string|number|Uint8Array|Y.Type></code></b>
|
||||
<dd>Copies the content of this YArray to a new Array.</dd>
|
||||
<b><code>toJSON():Array<Object|Array|string|number></code></b>
|
||||
<dd>Copies the content of this YArray to a new Array. It transforms all child types to JSON using their <code>toJSON</code> method.</dd>
|
||||
@ -159,9 +184,9 @@ import * as Y from 'yjs'
|
||||
</p>
|
||||
<pre><code>const ymap = new Y.Map()</code></pre>
|
||||
<dl>
|
||||
<b><code>get(key:string):object|string|number|Y.Type</code></b>
|
||||
<b><code>get(key:string):object|string|number|Uint8Array|Y.Type</code></b>
|
||||
<dd></dd>
|
||||
<b><code>set(key:string, value:object|string|number|Y.Type)</code></b>
|
||||
<b><code>set(key:string, value:object|string|number|Uint8Array|Y.Type)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>delete(key:string)</code></b>
|
||||
<dd></dd>
|
||||
@ -350,38 +375,148 @@ import * as Y from 'yjs'
|
||||
</dl>
|
||||
</details>
|
||||
|
||||
### Custom Types
|
||||
### Y.Doc
|
||||
|
||||
## Bindings
|
||||
```js
|
||||
const doc = new Y.Doc()
|
||||
```
|
||||
|
||||
## Transaction
|
||||
<dl>
|
||||
<b><code>clientID</code></b>
|
||||
<dd>A unique id that identifies this client. (readonly)</dd>
|
||||
<b><code>transact(function(Transaction):void [, origin:any])</code></b>
|
||||
<dd>Every change on the shared document happens in a transaction. Observer calls and the <code>update</code> event are called after each transaction. You should <i>bundle</i> changes into a single transaction to reduce the amount of event calls. I.e. <code>doc.transact(() => { yarray.insert(..); ymap.set(..) })</code> triggers a single change event. <br>You can specify an optional <code>origin</code> parameter that is stored on <code>transaction.origin</code> and <code>on('update', (update, origin) => ..)</code>.</dd>
|
||||
<b><code>get(string, Y.[TypeClass]):[Type]</code></b>
|
||||
<dd>Define a shared type.</dd>
|
||||
<b><code>getArray(string):Y.Array</code></b>
|
||||
<dd>Define a shared Y.Array type. Is equivalent to <code>y.get(string, Y.Array)</code>.</dd>
|
||||
<b><code>getMap(string):Y.Map</code></b>
|
||||
<dd>Define a shared Y.Map type. Is equivalent to <code>y.get(string, Y.Map)</code>.</dd>
|
||||
<b><code>getXmlFragment(string):Y.XmlFragment</code></b>
|
||||
<dd>Define a shared Y.XmlFragment type. Is equivalent to <code>y.get(string, Y.XmlFragment)</code>.</dd>
|
||||
<b><code>on(string, function)</code></b>
|
||||
<dd>Register an event listener on the shared type</dd>
|
||||
<b><code>off(string, function)</code></b>
|
||||
<dd>Unregister an event listener from the shared type</dd>
|
||||
</dl>
|
||||
|
||||
## Binary Encoding Protocols
|
||||
|
||||
### Sync Protocol
|
||||
#### Y.Doc Events
|
||||
<dl>
|
||||
<b><code>on('update', function(updateMessage:Uint8Array, origin:any, Y.Doc):void)</code></b>
|
||||
<dd>Listen to document updates. Document updates must be transmitted to all other peers. You can apply document updates in any order and multiple times.</dd>
|
||||
<b><code>on('beforeTransaction', function(Y.Transaction, Y.Doc):void)</code></b>
|
||||
<dd>Emitted before each transaction.</dd>
|
||||
<b><code>on('afterTransaction', function(Y.Transaction, Y.Doc):void)</code></b>
|
||||
<dd>Emitted after each transaction.</dd>
|
||||
</dl>
|
||||
|
||||
Sync steps
|
||||
### Document Updates
|
||||
|
||||
### Awareness Protocol
|
||||
Changes on the shared document are encoded into *document updates*. Document updates are *commutative* and *idempotent*. This means that they can be applied in any order and multiple times.
|
||||
|
||||
### Auth Protocol
|
||||
**Example: Listen to update events and apply them on remote client**
|
||||
```js
|
||||
const doc1 = new Y.Doc()
|
||||
const doc2 = new Y.Doc()
|
||||
|
||||
## Offline Editing
|
||||
doc1.on('update', update => {
|
||||
Y.applyUpdate(doc2, update)
|
||||
})
|
||||
|
||||
It is trivial with Yjs to persist the local state to indexeddb, so it is always available when working offline. But there are two non-trivial questions that need to answered when implementing a professional offline editing app:
|
||||
doc2.on('update', update => {
|
||||
Y.applyUpdate(doc1, update)
|
||||
})
|
||||
|
||||
1. How does a client sync down all rooms that were modified while offline?
|
||||
2. How does a client sync up all rooms that were modified while offline?
|
||||
// All changes are also applied to the other document
|
||||
doc1.getArray('myarray').insert(0, ['Hello doc2, you got this?'])
|
||||
doc2.getArray('myarray').get(0) // => 'Hello doc2, you got this?'
|
||||
```
|
||||
|
||||
Assuming 5000 documents are stored on each client for offline usage. How do we sync up/down each of those documents after a client comes online? It would be inefficient to sync each of those rooms separately. The only provider that currently supports syncing many rooms efficiently is Ydb, because its database layer is optimized to sync many rooms with each client.
|
||||
Yjs internally maintains a [State Vector](#State-Vector) that denotes the next expected clock from each client. In a different interpretation it holds the number of structs created by each client. When two clients sync, you can either exchange the complete document structure or only the differences by sending the state vector to compute the differences.
|
||||
|
||||
If you do not care about 1. and 2. you can use `/persistences/indexeddb.js` to mirror the local state to indexeddb.
|
||||
**Example: Sync two clients by exchanging the complete document structure**
|
||||
|
||||
## Working with Yjs
|
||||
```js
|
||||
const state1 = Y.encodeStateAsUpdate(ydoc1)
|
||||
const state2 = Y.encodeStateAsUpdate(ydoc2)
|
||||
Y.applyUpdate(ydoc1, state2)
|
||||
Y.applyUpdate(ydoc2, state1)
|
||||
```
|
||||
|
||||
**Example: Sync two clients by computing the differences**
|
||||
|
||||
This example shows how to sync two clients with the minimal amount of exchanged data by computing only the differences using the state vector of the remote client. Syncing clients using the state vector requires another roundtrip, but can safe a lot of bandwidth.
|
||||
|
||||
```js
|
||||
const stateVector1 = Y.encodeDocumentStateVector(ydoc1)
|
||||
const stateVector2 = Y.encodeDocumentStateVector(ydoc2)
|
||||
const diff1 = Y.encodeStateAsUpdate(ydoc1, stateVector2)
|
||||
const diff2 = Y.encodeStateAsUpdate(ydoc2, stateVector1)
|
||||
Y.applyUpdate(ydoc1, diff2)
|
||||
Y.applyUpdate(ydoc2, diff1)
|
||||
```
|
||||
|
||||
<dl>
|
||||
<b><code>Y.applyUpdate(Y.Doc, update:Uint8Array, [transactionOrigin:any])</code></b>
|
||||
<dd>Apply a document update on the shared document. Optionally you can specify <code>transactionOrigin</code> that will be stored on <code>transaction.origin</code> and <code>ydoc.on('update', (update, origin) => ..)</code>.</dd>
|
||||
<b><code>Y.encodeStateAsUpdate(Y.Doc, [encodedTargetStateVector:Uint8Array]):Uint8Array</code></b>
|
||||
<dd>Encode the document state as a single update message that can be applied on the remote document. Optionally specify the target state vector to only write the differences to the update message.</dd>
|
||||
<b><code>Y.encodeDocumentStateVector(Y.Doc):Uint8Array</code></b>
|
||||
<dd>Computes the state vector and encodes it into an Uint8Array.</dd>
|
||||
</dl>
|
||||
|
||||
### Relative Positions
|
||||
> This API is not stable yet
|
||||
|
||||
This feature is intended for managing selections / cursors. When working with other users that manipulate the shared document, you can't trust that an index position (an integer) will stay at the intended location. A *relative position* is fixated to an element in the shared document and is not affected by remote changes. I.e. given the document `"a|c"`, the relative position is attached to `c`. When a remote user modifies the document by inserting a character before the cursor, the cursor will stay attached to the character `c`. `insert(1, 'x')("a|c") = "ax|c"`. When the *relative position* is set to the end of the document, it will stay attached to the end of the document.
|
||||
|
||||
**Example: Transform to RelativePosition and back**
|
||||
```js
|
||||
const relPos = Y.createRelativePositionFromTypeIndex(ytext, 2)
|
||||
const pos = Y.createAbsolutePositionFromRelativePosition(relPos, doc)
|
||||
pos.type === ytext // => true
|
||||
pos.index === 2 // => true
|
||||
```
|
||||
|
||||
**Example: Send relative position to remote client (json)**
|
||||
```js
|
||||
const relPos = Y.createRelativePositionFromTypeIndex(ytext, 2)
|
||||
const encodedRelPos = JSON.stringify(relPos)
|
||||
// send encodedRelPos to remote client..
|
||||
const parsedRelPos = JSON.parse(encodedRelPos)
|
||||
const pos = Y.createAbsolutePositionFromRelativePosition(parsedRelPos, remoteDoc)
|
||||
pos.type === remoteytext // => true
|
||||
pos.index === 2 // => true
|
||||
```
|
||||
|
||||
**Example: Send relative position to remote client (Uint8Array)**
|
||||
```js
|
||||
const relPos = Y.createRelativePositionFromTypeIndex(ytext, 2)
|
||||
const encodedRelPos = Y.encodeRelativePosition(relPos)
|
||||
// send encodedRelPos to remote client..
|
||||
const parsedRelPos = Y.decodeRelativePosition(encodedRelPos)
|
||||
const pos = Y.createAbsolutePositionFromRelativePosition(parsedRelPos, remoteDoc)
|
||||
pos.type === remoteytext // => true
|
||||
pos.index === 2 // => true
|
||||
```
|
||||
|
||||
<dl>
|
||||
<b><code>Y.createRelativePositionFromTypeIndex(Uint8Array|Y.Type, number)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>Y.createAbsolutePositionFromRelativePosition(RelativePosition, Y.Doc)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>Y.encodeRelativePosition(RelativePosition):Uint8Array</code></b>
|
||||
<dd></dd>
|
||||
<b><code>Y.decodeRelativePosition(Uint8Array):RelativePosition</code></b>
|
||||
<dd></dd>
|
||||
</dl>
|
||||
|
||||
## Miscellaneous
|
||||
|
||||
### Typescript Declarations
|
||||
|
||||
Until [this](https://github.com/Microsoft/TypeScript/issues/7546) is fixed, the only way to get type declarations is by adding Yjs to the list of checked files:
|
||||
Yjs has type descriptions. But until [this ticket](https://github.com/Microsoft/TypeScript/issues/7546) is fixed, this is how you can make use of Yjs type declarations.
|
||||
|
||||
```json
|
||||
{
|
||||
@ -395,10 +530,26 @@ Until [this](https://github.com/Microsoft/TypeScript/issues/7546) is fixed, the
|
||||
|
||||
## Yjs CRDT Algorithm
|
||||
|
||||
*Conflict-free replicated data types* (CRDT) for collaborative editing are an alternative approach to *operational transformation* (OT). A very simple differenciation between the two approaches is that OT attempts to transform index positions to ensure convergence (all clients end up with the same content), while CRDTs use models that usually do not involve index transformations, like linked lists. OT is currently the de-facto standard for shared editing on text. OT approaches that support shared editing without a central source of truth (a central server) require too much bookkeeping to be viable in practice. CRDTs are better suited for distributed systems, provide additional guarantees that the document can be synced with remote clients, and do not require a central source of truth / central source of failure.
|
||||
|
||||
Yjs implements a modified version of the algorithm described in [this paper](https://www.researchgate.net/publication/310212186_Near_Real-Time_Peer-to-Peer_Shared_Editing_on_Extensible_Data_Types). I will eventually publish a paper that describes the new approach. Note: Since operations make up the document structure, we prefer the term *struct* now.
|
||||
|
||||
CRDTs suitable for shared text editing suffer from the fact that they only grow in size. There are CRDTs that do not grow in size, but they do not have the characteristics that are benificial for shared text editing (like intention preservation). Yjs implements many improvements to the original algorithm that diminish the trade-off that the document only grows in size. We can't garbage collect deleted structs (tombstones) while ensuring a unique order of the structs. But we can 1. merge preceeding structs into a single struct to reduce the amount of meta information, 2. we can delete content from the struct if it is deleted, and 3. we can garbage collect tombstones if we don't care about the order of the structs anymore (e.g. if the parent was deleted).
|
||||
|
||||
**Examples:**
|
||||
1. If a user inserts elements in sequence, the struct will be merged into a single struct. E.g. `array.insert(0, ['a']), array.insert(0, ['b']);` is first represented as two structs (`[{id: {client, clock: 0}, content: 'a'}, {id: {client, clock: 1}, content: 'b'}`) and then merged into a single struct: `[{id: {client, clock: 0}, content: 'ab'}]`.
|
||||
2. When a struct that contains content (e.g. `ItemString`) is deleted, the struct will be replaced with an `ItemDeleted` that does not contain content anymore.
|
||||
3. When a type is deleted, all child elements are transformed to `GC` structs. A `GC` struct only denotes the existence of a struct and that it is deleted. `GC` structs can always be merged with other `GC` structs if the id's are adjacent.
|
||||
|
||||
Especially when working on structured content (e.g. shared editing on ProseMirror), these improvements yield very good results when [benchmarking](#Benchmarks) random document edits. In practice they show even better results, because users usually edit text in sequence, resulting in structs that can easily be merged. The benchmarks showt that even in the worst case scenario that a user edits text from right to left, Yjs achieves good performance even for huge documents.
|
||||
|
||||
#### State Vector
|
||||
Yjs has the ability to exchange only the differences when syncing two clients. We use lamport timestamps to identify structs and to track in which order a client created them. Each struct has an `struct.id = { client: number, clock: number}` that uniquely identifies a struct. We define the next expected `clock` by each client as the *state vector*. This data structure is similar to the [version vectors](https://en.wikipedia.org/wiki/Version_vector) data structure. But we use state vectors only to describe the state of the local document, so we can compute the missing struct of the remote client. We do not use it to track causality.
|
||||
|
||||
## License and Author
|
||||
|
||||
Yjs and all related projects are [**MIT licensed**](./LICENSE).
|
||||
|
||||
Yjs is based on the research I did as a student at the [RWTH i5](http://dbis.rwth-aachen.de/). Now I am working on Yjs in my spare time.
|
||||
Yjs is based on my research as a student at the [RWTH i5](http://dbis.rwth-aachen.de/). Now I am working on Yjs in my spare time.
|
||||
|
||||
Support me on [Patreon](https://www.patreon.com/dmonad) to fund this project or hire [me](https://github.com/dmonad) for professional support.
|
||||
Fund this project by donating on [Patreon](https://www.patreon.com/dmonad) or hiring [me](https://github.com/dmonad) for professional support.
|
||||
|
@ -154,6 +154,6 @@ class NoteHistoryPlugin {
|
||||
const encoder = encoding.createEncoder()
|
||||
historyProtocol.writeHistorySnapshot(encoder, y, updatedUserMap)
|
||||
encoding.writeUint32(encoder, Math.floor(Date.now() / 1000))
|
||||
history.push([encoding.toBuffer(encoder)])
|
||||
history.push([encoding.toUint8Array(encoder)])
|
||||
}
|
||||
}
|
||||
|
17
package-lock.json
generated
17
package-lock.json
generated
@ -3036,9 +3036,9 @@
|
||||
}
|
||||
},
|
||||
"lib0": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.0.2.tgz",
|
||||
"integrity": "sha512-7bJgV2emHGRO5kpj66Gdc9SQKVfhDBLx0UIS/aU5P8R0179nRFHKDTYGvLlNloWbeUUARlqk3ndFIO4JbUy7Sw=="
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.0.4.tgz",
|
||||
"integrity": "sha512-osSGIxFM0mUuVAclVOQAio4lq0YYk1xFfj6J+1i3u5az8rXAQKDil2skA19aiiG0sfAdasOtr8Mk+9Mrw10cfQ=="
|
||||
},
|
||||
"live-server": {
|
||||
"version": "1.2.1",
|
||||
@ -5579,10 +5579,13 @@
|
||||
"dev": true
|
||||
},
|
||||
"y-protocols": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-0.0.3.tgz",
|
||||
"integrity": "sha512-b6yUR4KuRN9ehf2b0/YyaxxO2bxk7ti5DDTg5Jm/FEtPf0Vk6s4ez5wIhkV1nA7bloz/NDmVpoCDPFX1zZORhg==",
|
||||
"dev": true
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-0.0.4.tgz",
|
||||
"integrity": "sha512-a7F8t16y6rVPYGvwsbfeN5EauHFBo+SIsYadvaONdp6jCyhcKokDK8u30BQOJCOxwd1FkuAhMs14m6CtJbfRqg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lib0": "0.0.4"
|
||||
}
|
||||
},
|
||||
"yallist": {
|
||||
"version": "2.1.2",
|
||||
|
@ -14,7 +14,7 @@
|
||||
"lint": "standard && tsc",
|
||||
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.v13.md --package ./package.json || true",
|
||||
"serve-docs": "npm run docs && serve ./docs/",
|
||||
"preversion": "PRODUCTION=1 npm run dist && node ./dist/tests.js --repitition-time 1000",
|
||||
"preversion": "PRODUCTION=1 npm run dist && npm run docs && node ./dist/tests.js --repitition-time 1000",
|
||||
"postversion": "git push && git push --tags",
|
||||
"debug": "concurrently 'live-server --port=3443 --entry-file=test.html' 'npm run watch'",
|
||||
"trace-deopt": "clear && rollup -c && node --trace-deopt dist/test.js",
|
||||
@ -54,10 +54,10 @@
|
||||
},
|
||||
"homepage": "http://y-js.org",
|
||||
"dependencies": {
|
||||
"lib0": "0.0.2"
|
||||
"lib0": "0.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"y-protocols": "0.0.3",
|
||||
"y-protocols": "0.0.4",
|
||||
"codemirror": "^5.42.0",
|
||||
"concurrently": "^3.6.1",
|
||||
"jsdoc": "^3.5.5",
|
||||
|
28
src/index.js
28
src/index.js
@ -1,6 +1,6 @@
|
||||
|
||||
export {
|
||||
Y,
|
||||
Doc,
|
||||
Transaction,
|
||||
YArray as Array,
|
||||
YMap as Map,
|
||||
@ -24,25 +24,23 @@ export {
|
||||
ItemString,
|
||||
ItemType,
|
||||
AbstractType,
|
||||
compareCursors,
|
||||
Cursor,
|
||||
createCursorFromTypeOffset,
|
||||
createCursorFromJSON,
|
||||
createAbsolutePositionFromCursor,
|
||||
writeCursor,
|
||||
readCursor,
|
||||
RelativePosition,
|
||||
createRelativePositionFromTypeIndex,
|
||||
createRelativePositionFromJSON,
|
||||
createAbsolutePositionFromRelativePosition,
|
||||
compareRelativePositions,
|
||||
writeRelativePosition,
|
||||
readRelativePosition,
|
||||
ID,
|
||||
createID,
|
||||
compareIDs,
|
||||
getState,
|
||||
getStates,
|
||||
readStatesAsMap,
|
||||
writeStates,
|
||||
writeModel,
|
||||
readModel,
|
||||
Snapshot,
|
||||
findRootTypeKey,
|
||||
typeArrayToArraySnapshot,
|
||||
typeListToArraySnapshot,
|
||||
typeMapGetSnapshot,
|
||||
iterateDeletedStructs
|
||||
iterateDeletedStructs,
|
||||
applyUpdate,
|
||||
encodeStateAsUpdate,
|
||||
encodeDocumentStateVector
|
||||
} from './internals.js'
|
||||
|
@ -2,12 +2,12 @@ export * from './utils/DeleteSet.js'
|
||||
export * from './utils/EventHandler.js'
|
||||
export * from './utils/ID.js'
|
||||
export * from './utils/isParentOf.js'
|
||||
export * from './utils/cursor.js'
|
||||
export * from './utils/RelativePosition.js'
|
||||
export * from './utils/Snapshot.js'
|
||||
export * from './utils/StructStore.js'
|
||||
export * from './utils/Transaction.js'
|
||||
// export * from './utils/UndoManager.js'
|
||||
export * from './utils/Y.js'
|
||||
export * from './utils/Doc.js'
|
||||
export * from './utils/YEvent.js'
|
||||
|
||||
export * from './types/AbstractType.js'
|
||||
|
@ -17,7 +17,7 @@ import {
|
||||
getItemType,
|
||||
getItemCleanEnd,
|
||||
getItemCleanStart,
|
||||
YEvent, StructStore, ID, AbstractType, Y, Transaction // eslint-disable-line
|
||||
YEvent, StructStore, ID, AbstractType, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error.js'
|
||||
@ -153,7 +153,7 @@ export class AbstractItem extends AbstractStruct {
|
||||
* @private
|
||||
*/
|
||||
integrate (transaction) {
|
||||
const store = transaction.y.store
|
||||
const store = transaction.doc.store
|
||||
const id = this.id
|
||||
const parent = this.parent
|
||||
const parentSub = this.parentSub
|
||||
@ -415,23 +415,19 @@ export class AbstractItem extends AbstractStruct {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
gcChildren (transaction, store) { }
|
||||
gcChildren (store) { }
|
||||
|
||||
/**
|
||||
* @todo remove transaction param
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
* @param {boolean} parentGCd
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
gc (transaction, store, parentGCd) {
|
||||
gc (store, parentGCd) {
|
||||
if (!this.deleted) {
|
||||
throw error.unexpectedCase()
|
||||
}
|
||||
@ -619,12 +615,16 @@ export const computeItemParams = (transaction, store, leftid, rightid, parentid,
|
||||
case GC:
|
||||
break
|
||||
default:
|
||||
// Edge case: toStruct is called with an offset > 0. In this case left is defined.
|
||||
// Depending in which order structs arrive, left may be GC'd and the parent not
|
||||
// deleted. This is why we check if left is GC'd. Strictly we probably don't have
|
||||
// to check if right is GC'd, but we will in case we run into future issues
|
||||
if (!parentItem.deleted && (left === null || left.constructor !== GC) && (right === null || right.constructor !== GC)) {
|
||||
parent = parentItem.type
|
||||
}
|
||||
}
|
||||
} else if (parentYKey !== null) {
|
||||
parent = transaction.y.get(parentYKey)
|
||||
parent = transaction.doc.get(parentYKey)
|
||||
} else if (left !== null) {
|
||||
if (left.constructor !== GC) {
|
||||
parent = left.parent
|
||||
|
@ -1,6 +1,6 @@
|
||||
|
||||
import {
|
||||
Y, StructStore, ID, Transaction // eslint-disable-line
|
||||
StructStore, ID, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js' // eslint-disable-line
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
AbstractStruct,
|
||||
createID,
|
||||
addStruct,
|
||||
Y, StructStore, Transaction, ID // eslint-disable-line
|
||||
StructStore, Transaction, ID // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
@ -48,7 +48,7 @@ export class GC extends AbstractStruct {
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
integrate (transaction) {
|
||||
addStruct(transaction.y.store, this)
|
||||
addStruct(transaction.doc.store, this)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
import * as buffer from 'lib0/buffer.js'
|
||||
|
||||
/**
|
||||
* @private
|
||||
@ -27,7 +28,7 @@ export class ItemBinary extends AbstractItem {
|
||||
* @param {ID | null} rightOrigin
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {string | null} parentSub
|
||||
* @param {ArrayBuffer} content
|
||||
* @param {Uint8Array} content
|
||||
*/
|
||||
constructor (id, left, origin, right, rightOrigin, parent, parentSub, content) {
|
||||
super(id, left, origin, right, rightOrigin, parent, parentSub)
|
||||
@ -54,7 +55,7 @@ export class ItemBinary extends AbstractItem {
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
super.write(encoder, offset, structBinaryRefNumber)
|
||||
encoding.writePayload(encoder, this.content)
|
||||
encoding.writeVarUint8Array(encoder, this.content)
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,9 +71,9 @@ export class ItemBinaryRef extends AbstractItemRef {
|
||||
constructor (decoder, id, info) {
|
||||
super(decoder, id, info)
|
||||
/**
|
||||
* @type {ArrayBuffer}
|
||||
* @type {Uint8Array}
|
||||
*/
|
||||
this.content = decoding.readPayload(decoder)
|
||||
this.content = buffer.copyUint8Array(decoding.readVarUint8Array(decoder))
|
||||
}
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
splitItem,
|
||||
addToDeleteSet,
|
||||
mergeItemWith,
|
||||
Y, StructStore, Transaction, ID, AbstractType // eslint-disable-line
|
||||
StructStore, Transaction, ID, AbstractType // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
@ -87,15 +87,14 @@ export class ItemDeleted extends AbstractItem {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
* @param {boolean} parentGCd
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
gc (transaction, store, parentGCd) {
|
||||
gc (store, parentGCd) {
|
||||
if (parentGCd) {
|
||||
super.gc(transaction, store, parentGCd)
|
||||
super.gc(store, parentGCd)
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
@ -67,7 +67,7 @@ export class ItemEmbedRef extends AbstractItemRef {
|
||||
constructor (decoder, id, info) {
|
||||
super(decoder, id, info)
|
||||
/**
|
||||
* @type {ArrayBuffer}
|
||||
* @type {Object}
|
||||
*/
|
||||
this.embed = JSON.parse(decoding.readVarString(decoder))
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
changeItemRefOffset,
|
||||
GC,
|
||||
mergeItemWith,
|
||||
Transaction, StructStore, Y, ID, AbstractType // eslint-disable-line
|
||||
Transaction, StructStore, ID, AbstractType // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
changeItemRefOffset,
|
||||
GC,
|
||||
mergeItemWith,
|
||||
Transaction, StructStore, Y, ID, AbstractType // eslint-disable-line
|
||||
Transaction, StructStore, ID, AbstractType // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
readYXmlFragment,
|
||||
readYXmlHook,
|
||||
readYXmlText,
|
||||
StructStore, Y, GC, Transaction, ID, AbstractType // eslint-disable-line
|
||||
StructStore, GC, Transaction, ID, AbstractType // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js' // eslint-disable-line
|
||||
@ -83,7 +83,7 @@ export class ItemType extends AbstractItem {
|
||||
*/
|
||||
integrate (transaction) {
|
||||
super.integrate(transaction)
|
||||
this.type._integrate(transaction.y, this)
|
||||
this.type._integrate(transaction.doc, this)
|
||||
}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
@ -129,19 +129,18 @@ export class ItemType extends AbstractItem {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
*/
|
||||
gcChildren (transaction, store) {
|
||||
gcChildren (store) {
|
||||
let item = this.type._start
|
||||
while (item !== null) {
|
||||
item.gc(transaction, store, true)
|
||||
item.gc(store, true)
|
||||
item = item.right
|
||||
}
|
||||
this.type._start = null
|
||||
this.type._map.forEach(item => {
|
||||
while (item !== null) {
|
||||
item.gc(transaction, store, true)
|
||||
item.gc(store, true)
|
||||
// @ts-ignore
|
||||
item = item.left
|
||||
}
|
||||
@ -150,13 +149,12 @@ export class ItemType extends AbstractItem {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
* @param {boolean} parentGCd
|
||||
*/
|
||||
gc (transaction, store, parentGCd) {
|
||||
this.gcChildren(transaction, store)
|
||||
super.gc(transaction, store, parentGCd)
|
||||
gc (store, parentGCd) {
|
||||
this.gcChildren(store)
|
||||
super.gc(store, parentGCd)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
ItemBinary,
|
||||
createID,
|
||||
getItemCleanStart,
|
||||
Y, Snapshot, Transaction, EventHandler, YEvent, AbstractItem, // eslint-disable-line
|
||||
Doc, Snapshot, Transaction, EventHandler, YEvent, AbstractItem, // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as map from 'lib0/map.js'
|
||||
@ -64,9 +64,9 @@ export class AbstractType {
|
||||
this._start = null
|
||||
/**
|
||||
* @private
|
||||
* @type {Y|null}
|
||||
* @type {Doc|null}
|
||||
*/
|
||||
this._y = null
|
||||
this.doc = null
|
||||
this._length = 0
|
||||
/**
|
||||
* Event handlers
|
||||
@ -87,12 +87,12 @@ export class AbstractType {
|
||||
* * This type is sent to other client
|
||||
* * Observer functions are fired
|
||||
*
|
||||
* @param {Y} y The Yjs instance
|
||||
* @param {Doc} y The Yjs instance
|
||||
* @param {ItemType|null} item
|
||||
* @private
|
||||
*/
|
||||
_integrate (y, item) {
|
||||
this._y = y
|
||||
this.doc = y
|
||||
this._item = item
|
||||
}
|
||||
|
||||
@ -182,7 +182,7 @@ export class AbstractType {
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeArrayToArray = type => {
|
||||
export const typeListToArray = type => {
|
||||
const cs = []
|
||||
let n = type._start
|
||||
while (n !== null) {
|
||||
@ -205,7 +205,7 @@ export const typeArrayToArray = type => {
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeArrayToArraySnapshot = (type, snapshot) => {
|
||||
export const typeListToArraySnapshot = (type, snapshot) => {
|
||||
const cs = []
|
||||
let n = type._start
|
||||
while (n !== null) {
|
||||
@ -229,7 +229,7 @@ export const typeArrayToArraySnapshot = (type, snapshot) => {
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeArrayForEach = (type, f) => {
|
||||
export const typeListForEach = (type, f) => {
|
||||
let index = 0
|
||||
let n = type._start
|
||||
while (n !== null) {
|
||||
@ -252,12 +252,12 @@ export const typeArrayForEach = (type, f) => {
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeArrayMap = (type, f) => {
|
||||
export const typeListMap = (type, f) => {
|
||||
/**
|
||||
* @type {Array<any>}
|
||||
*/
|
||||
const result = []
|
||||
typeArrayForEach(type, (c, i) => {
|
||||
typeListForEach(type, (c, i) => {
|
||||
result.push(f(c, i, type))
|
||||
})
|
||||
return result
|
||||
@ -270,7 +270,7 @@ export const typeArrayMap = (type, f) => {
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeArrayCreateIterator = type => {
|
||||
export const typeListCreateIterator = type => {
|
||||
let n = type._start
|
||||
/**
|
||||
* @type {Array<any>|null}
|
||||
@ -326,7 +326,7 @@ export const typeArrayCreateIterator = type => {
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeArrayForEachSnapshot = (type, f, snapshot) => {
|
||||
export const typeListForEachSnapshot = (type, f, snapshot) => {
|
||||
let index = 0
|
||||
let n = type._start
|
||||
while (n !== null) {
|
||||
@ -348,7 +348,7 @@ export const typeArrayForEachSnapshot = (type, f, snapshot) => {
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeArrayGet = (type, index) => {
|
||||
export const typeListGet = (type, index) => {
|
||||
for (let n = type._start; n !== null; n = n.right) {
|
||||
if (!n.deleted && n.countable) {
|
||||
if (index < n.length) {
|
||||
@ -363,12 +363,12 @@ export const typeArrayGet = (type, index) => {
|
||||
* @param {Transaction} transaction
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {AbstractItem?} referenceItem
|
||||
* @param {Array<Object<string,any>|Array<any>|number|string|ArrayBuffer>} content
|
||||
* @param {Array<Object<string,any>|Array<any>|number|string|Uint8Array>} content
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeArrayInsertGenericsAfter = (transaction, parent, referenceItem, content) => {
|
||||
export const typeListInsertGenericsAfter = (transaction, parent, referenceItem, content) => {
|
||||
let left = referenceItem
|
||||
const right = referenceItem === null ? parent._start : referenceItem.right
|
||||
/**
|
||||
@ -393,10 +393,9 @@ export const typeArrayInsertGenericsAfter = (transaction, parent, referenceItem,
|
||||
default:
|
||||
packJsonContent()
|
||||
switch (c.constructor) {
|
||||
case Uint8Array:
|
||||
case ArrayBuffer:
|
||||
// @ts-ignore c is definitely an ArrayBuffer
|
||||
left = new ItemBinary(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, c)
|
||||
// @ts-ignore
|
||||
left = new ItemBinary(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new Uint8Array(/** @type {Uint8Array} */ (c)))
|
||||
left.integrate(transaction)
|
||||
break
|
||||
default:
|
||||
@ -416,14 +415,14 @@ export const typeArrayInsertGenericsAfter = (transaction, parent, referenceItem,
|
||||
* @param {Transaction} transaction
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {number} index
|
||||
* @param {Array<Object<string,any>|Array<any>|number|string|ArrayBuffer>} content
|
||||
* @param {Array<Object<string,any>|Array<any>|number|string|Uint8Array>} content
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeArrayInsertGenerics = (transaction, parent, index, content) => {
|
||||
export const typeListInsertGenerics = (transaction, parent, index, content) => {
|
||||
if (index === 0) {
|
||||
return typeArrayInsertGenericsAfter(transaction, parent, null, content)
|
||||
return typeListInsertGenericsAfter(transaction, parent, null, content)
|
||||
}
|
||||
let n = parent._start
|
||||
for (; n !== null; n = n.right) {
|
||||
@ -431,14 +430,14 @@ export const typeArrayInsertGenerics = (transaction, parent, index, content) =>
|
||||
if (index <= n.length) {
|
||||
if (index < n.length) {
|
||||
// insert in-between
|
||||
getItemCleanStart(transaction, transaction.y.store, createID(n.id.client, n.id.clock + index))
|
||||
getItemCleanStart(transaction, transaction.doc.store, createID(n.id.client, n.id.clock + index))
|
||||
}
|
||||
break
|
||||
}
|
||||
index -= n.length
|
||||
}
|
||||
}
|
||||
return typeArrayInsertGenericsAfter(transaction, parent, n, content)
|
||||
return typeListInsertGenericsAfter(transaction, parent, n, content)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -450,14 +449,14 @@ export const typeArrayInsertGenerics = (transaction, parent, index, content) =>
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeArrayDelete = (transaction, parent, index, length) => {
|
||||
export const typeListDelete = (transaction, parent, index, length) => {
|
||||
if (length === 0) { return }
|
||||
let n = parent._start
|
||||
// compute the first item to be deleted
|
||||
for (; n !== null && index > 0; n = n.right) {
|
||||
if (!n.deleted && n.countable) {
|
||||
if (index < n.length) {
|
||||
getItemCleanStart(transaction, transaction.y.store, createID(n.id.client, n.id.clock + index))
|
||||
getItemCleanStart(transaction, transaction.doc.store, createID(n.id.client, n.id.clock + index))
|
||||
}
|
||||
index -= n.length
|
||||
}
|
||||
@ -466,7 +465,7 @@ export const typeArrayDelete = (transaction, parent, index, length) => {
|
||||
while (length > 0 && n !== null) {
|
||||
if (!n.deleted) {
|
||||
if (length < n.length) {
|
||||
getItemCleanStart(transaction, transaction.y.store, createID(n.id.client, n.id.clock + length))
|
||||
getItemCleanStart(transaction, transaction.doc.store, createID(n.id.client, n.id.clock + length))
|
||||
}
|
||||
n.delete(transaction)
|
||||
length -= n.length
|
||||
@ -497,7 +496,7 @@ export const typeMapDelete = (transaction, parent, key) => {
|
||||
* @param {Transaction} transaction
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {string} key
|
||||
* @param {Object|number|Array<any>|string|ArrayBuffer|AbstractType<any>} value
|
||||
* @param {Object|number|Array<any>|string|Uint8Array|AbstractType<any>} value
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
@ -515,7 +514,7 @@ export const typeMapSet = (transaction, parent, key, value) => {
|
||||
case String:
|
||||
new ItemJSON(nextID(transaction), left, left === null ? null : left.lastId, null, null, parent, key, [value]).integrate(transaction)
|
||||
break
|
||||
case ArrayBuffer:
|
||||
case Uint8Array:
|
||||
new ItemBinary(nextID(transaction), left, left === null ? null : left.lastId, null, null, parent, key, value).integrate(transaction)
|
||||
break
|
||||
default:
|
||||
@ -530,7 +529,7 @@ export const typeMapSet = (transaction, parent, key, value) => {
|
||||
/**
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {string} key
|
||||
* @return {Object<string,any>|number|Array<any>|string|ArrayBuffer|AbstractType<any>|undefined}
|
||||
* @return {Object<string,any>|number|Array<any>|string|Uint8Array|AbstractType<any>|undefined}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
@ -542,7 +541,7 @@ export const typeMapGet = (parent, key) => {
|
||||
|
||||
/**
|
||||
* @param {AbstractType<any>} parent
|
||||
* @return {Object<string,Object<string,any>|number|Array<any>|string|ArrayBuffer|AbstractType<any>|undefined>}
|
||||
* @return {Object<string,Object<string,any>|number|Array<any>|string|Uint8Array|AbstractType<any>|undefined>}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
@ -577,7 +576,7 @@ export const typeMapHas = (parent, key) => {
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {string} key
|
||||
* @param {Snapshot} snapshot
|
||||
* @return {Object<string,any>|number|Array<any>|string|ArrayBuffer|AbstractType<any>|undefined}
|
||||
* @return {Object<string,any>|number|Array<any>|string|Uint8Array|AbstractType<any>|undefined}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
|
@ -5,17 +5,17 @@
|
||||
import {
|
||||
YEvent,
|
||||
AbstractType,
|
||||
typeArrayGet,
|
||||
typeArrayToArray,
|
||||
typeArrayForEach,
|
||||
typeArrayCreateIterator,
|
||||
typeArrayInsertGenerics,
|
||||
typeArrayDelete,
|
||||
typeArrayMap,
|
||||
typeListGet,
|
||||
typeListToArray,
|
||||
typeListForEach,
|
||||
typeListCreateIterator,
|
||||
typeListInsertGenerics,
|
||||
typeListDelete,
|
||||
typeListMap,
|
||||
YArrayRefID,
|
||||
callTypeObservers,
|
||||
transact,
|
||||
Y, Transaction, ItemType, // eslint-disable-line
|
||||
Doc, Transaction, ItemType, // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as decoding from 'lib0/decoding.js' // eslint-disable-line
|
||||
@ -58,7 +58,7 @@ export class YArray extends AbstractType {
|
||||
* * This type is sent to other client
|
||||
* * Observer functions are fired
|
||||
*
|
||||
* @param {Y} y The Yjs instance
|
||||
* @param {Doc} y The Yjs instance
|
||||
* @param {ItemType} item
|
||||
*
|
||||
* @private
|
||||
@ -101,9 +101,9 @@ export class YArray extends AbstractType {
|
||||
* @param {Array<T>} content The array of content
|
||||
*/
|
||||
insert (index, content) {
|
||||
if (this._y !== null) {
|
||||
transact(this._y, transaction => {
|
||||
typeArrayInsertGenerics(transaction, this, index, content)
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeListInsertGenerics(transaction, this, index, content)
|
||||
})
|
||||
} else {
|
||||
// @ts-ignore _prelimContent is defined because this is not yet integrated
|
||||
@ -127,9 +127,9 @@ export class YArray extends AbstractType {
|
||||
* @param {number} length The number of elements to remove. Defaults to 1.
|
||||
*/
|
||||
delete (index, length = 1) {
|
||||
if (this._y !== null) {
|
||||
transact(this._y, transaction => {
|
||||
typeArrayDelete(transaction, this, index, length)
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeListDelete(transaction, this, index, length)
|
||||
})
|
||||
} else {
|
||||
// @ts-ignore _prelimContent is defined because this is not yet integrated
|
||||
@ -144,7 +144,7 @@ export class YArray extends AbstractType {
|
||||
* @return {T}
|
||||
*/
|
||||
get (index) {
|
||||
return typeArrayGet(this, index)
|
||||
return typeListGet(this, index)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -153,7 +153,7 @@ export class YArray extends AbstractType {
|
||||
* @return {Array<T>}
|
||||
*/
|
||||
toArray () {
|
||||
return typeArrayToArray(this)
|
||||
return typeListToArray(this)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -176,7 +176,7 @@ export class YArray extends AbstractType {
|
||||
*/
|
||||
map (f) {
|
||||
// @ts-ignore
|
||||
return typeArrayMap(this, f)
|
||||
return typeListMap(this, f)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -185,14 +185,14 @@ export class YArray extends AbstractType {
|
||||
* @param {function(T,number,YArray<T>):void} f A function to execute on every element of this YArray.
|
||||
*/
|
||||
forEach (f) {
|
||||
typeArrayForEach(this, f)
|
||||
typeListForEach(this, f)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {IterableIterator<T>}
|
||||
*/
|
||||
[Symbol.iterator] () {
|
||||
return typeArrayCreateIterator(this)
|
||||
return typeListCreateIterator(this)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
YMapRefID,
|
||||
callTypeObservers,
|
||||
transact,
|
||||
Y, Transaction, ItemType, // eslint-disable-line
|
||||
Doc, Transaction, ItemType, // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
@ -38,7 +38,7 @@ export class YMapEvent extends YEvent {
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T number|string|Object|Array|ArrayBuffer
|
||||
* @template T number|string|Object|Array|Uint8Array
|
||||
* A shared Map implementation.
|
||||
*
|
||||
* @extends AbstractType<YMapEvent<T>>
|
||||
@ -60,7 +60,7 @@ export class YMap extends AbstractType {
|
||||
* * This type is sent to other client
|
||||
* * Observer functions are fired
|
||||
*
|
||||
* @param {Y} y The Yjs instance
|
||||
* @param {Doc} y The Yjs instance
|
||||
* @param {ItemType} item
|
||||
*
|
||||
* @private
|
||||
@ -144,8 +144,8 @@ export class YMap extends AbstractType {
|
||||
* @param {string} key The key of the element to remove.
|
||||
*/
|
||||
delete (key) {
|
||||
if (this._y !== null) {
|
||||
transact(this._y, transaction => {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeMapDelete(transaction, this, key)
|
||||
})
|
||||
} else {
|
||||
@ -161,8 +161,8 @@ export class YMap extends AbstractType {
|
||||
* @param {T} value The value of the element to add
|
||||
*/
|
||||
set (key, value) {
|
||||
if (this._y !== null) {
|
||||
transact(this._y, transaction => {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeMapSet(transaction, this, key, value)
|
||||
})
|
||||
} else {
|
||||
|
@ -16,7 +16,7 @@ import {
|
||||
YTextRefID,
|
||||
callTypeObservers,
|
||||
transact,
|
||||
Y, ItemType, AbstractItem, Snapshot, StructStore, Transaction // eslint-disable-line
|
||||
Doc, ItemType, AbstractItem, Snapshot, StructStore, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as decoding from 'lib0/decoding.js' // eslint-disable-line
|
||||
@ -303,7 +303,7 @@ const formatText = (transaction, parent, left, right, currentAttributes, length,
|
||||
case ItemEmbed:
|
||||
case ItemString:
|
||||
if (length < right.length) {
|
||||
getItemCleanStart(transaction, transaction.y.store, createID(right.id.client, right.id.clock + length))
|
||||
getItemCleanStart(transaction, transaction.doc.store, createID(right.id.client, right.id.clock + length))
|
||||
}
|
||||
length -= right.length
|
||||
break
|
||||
@ -337,7 +337,7 @@ const deleteText = (transaction, left, right, currentAttributes, length) => {
|
||||
case ItemEmbed:
|
||||
case ItemString:
|
||||
if (length < right.length) {
|
||||
getItemCleanStart(transaction, transaction.y.store, createID(right.id.client, right.id.clock + length))
|
||||
getItemCleanStart(transaction, transaction.doc.store, createID(right.id.client, right.id.clock + length))
|
||||
}
|
||||
length -= right.length
|
||||
right.delete(transaction)
|
||||
@ -411,7 +411,7 @@ class YTextEvent extends YEvent {
|
||||
*/
|
||||
get delta () {
|
||||
if (this._delta === null) {
|
||||
const y = this.target._y
|
||||
const y = this.target.doc
|
||||
// @ts-ignore
|
||||
transact(y, transaction => {
|
||||
/**
|
||||
@ -629,7 +629,7 @@ export class YText extends AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Y} y
|
||||
* @param {Doc} y
|
||||
* @param {ItemType} item
|
||||
*
|
||||
* @private
|
||||
@ -686,8 +686,8 @@ export class YText extends AbstractType {
|
||||
* @public
|
||||
*/
|
||||
applyDelta (delta) {
|
||||
if (this._y !== null) {
|
||||
transact(this._y, transaction => {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
/**
|
||||
* @type {ItemListPosition}
|
||||
*/
|
||||
@ -803,7 +803,7 @@ export class YText extends AbstractType {
|
||||
if (text.length <= 0) {
|
||||
return
|
||||
}
|
||||
const y = this._y
|
||||
const y = this.doc
|
||||
if (y !== null) {
|
||||
transact(y, transaction => {
|
||||
const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index)
|
||||
@ -829,7 +829,7 @@ export class YText extends AbstractType {
|
||||
if (embed.constructor !== Object) {
|
||||
throw new Error('Embed must be an Object')
|
||||
}
|
||||
const y = this._y
|
||||
const y = this.doc
|
||||
if (y !== null) {
|
||||
transact(y, transaction => {
|
||||
const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index)
|
||||
@ -853,7 +853,7 @@ export class YText extends AbstractType {
|
||||
if (length === 0) {
|
||||
return
|
||||
}
|
||||
const y = this._y
|
||||
const y = this.doc
|
||||
if (y !== null) {
|
||||
transact(y, transaction => {
|
||||
const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index)
|
||||
@ -876,7 +876,7 @@ export class YText extends AbstractType {
|
||||
* @public
|
||||
*/
|
||||
format (index, length, attributes) {
|
||||
const y = this._y
|
||||
const y = this.doc
|
||||
if (y !== null) {
|
||||
transact(y, transaction => {
|
||||
let { left, right, currentAttributes } = findPosition(transaction, y.store, this, index)
|
||||
|
@ -6,9 +6,9 @@ import {
|
||||
typeMapSet,
|
||||
typeMapGet,
|
||||
typeMapGetAll,
|
||||
typeArrayForEach,
|
||||
typeListForEach,
|
||||
YXmlElementRefID,
|
||||
Snapshot, Y, ItemType // eslint-disable-line
|
||||
Snapshot, Doc, ItemType // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
@ -39,7 +39,7 @@ export class YXmlElement extends YXmlFragment {
|
||||
* * This type is sent to other client
|
||||
* * Observer functions are fired
|
||||
*
|
||||
* @param {Y} y The Yjs instance
|
||||
* @param {Doc} y The Yjs instance
|
||||
* @param {ItemType} item
|
||||
* @private
|
||||
*/
|
||||
@ -100,8 +100,8 @@ export class YXmlElement extends YXmlFragment {
|
||||
* @public
|
||||
*/
|
||||
removeAttribute (attributeName) {
|
||||
if (this._y !== null) {
|
||||
transact(this._y, transaction => {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeMapDelete(transaction, this, attributeName)
|
||||
})
|
||||
} else {
|
||||
@ -119,8 +119,8 @@ export class YXmlElement extends YXmlFragment {
|
||||
* @public
|
||||
*/
|
||||
setAttribute (attributeName, attributeValue) {
|
||||
if (this._y !== null) {
|
||||
transact(this._y, transaction => {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeMapSet(transaction, this, attributeName, attributeValue)
|
||||
})
|
||||
} else {
|
||||
@ -176,7 +176,7 @@ export class YXmlElement extends YXmlFragment {
|
||||
for (let key in attrs) {
|
||||
dom.setAttribute(key, attrs[key])
|
||||
}
|
||||
typeArrayForEach(this, yxml => {
|
||||
typeListForEach(this, yxml => {
|
||||
dom.appendChild(yxml.toDOM(_document, hooks, binding))
|
||||
})
|
||||
if (binding !== undefined) {
|
||||
|
@ -6,11 +6,11 @@ import {
|
||||
YXmlEvent,
|
||||
YXmlElement,
|
||||
AbstractType,
|
||||
typeArrayMap,
|
||||
typeArrayForEach,
|
||||
typeArrayInsertGenerics,
|
||||
typeArrayDelete,
|
||||
typeArrayToArray,
|
||||
typeListMap,
|
||||
typeListForEach,
|
||||
typeListInsertGenerics,
|
||||
typeListDelete,
|
||||
typeListToArray,
|
||||
YXmlFragmentRefID,
|
||||
callTypeObservers,
|
||||
transact,
|
||||
@ -211,7 +211,7 @@ export class YXmlFragment extends AbstractType {
|
||||
* @return {string} The string representation of all children.
|
||||
*/
|
||||
toString () {
|
||||
return typeArrayMap(this, xml => xml.toString()).join('')
|
||||
return typeListMap(this, xml => xml.toString()).join('')
|
||||
}
|
||||
|
||||
toJSON () {
|
||||
@ -238,7 +238,7 @@ export class YXmlFragment extends AbstractType {
|
||||
if (binding !== undefined) {
|
||||
binding._createAssociation(fragment, this)
|
||||
}
|
||||
typeArrayForEach(this, xmlType => {
|
||||
typeListForEach(this, xmlType => {
|
||||
fragment.insertBefore(xmlType.toDOM(_document, hooks, binding), null)
|
||||
})
|
||||
return fragment
|
||||
@ -255,9 +255,9 @@ export class YXmlFragment extends AbstractType {
|
||||
* @param {Array<YXmlElement|YXmlText>} content The array of content
|
||||
*/
|
||||
insert (index, content) {
|
||||
if (this._y !== null) {
|
||||
transact(this._y, transaction => {
|
||||
typeArrayInsertGenerics(transaction, this, index, content)
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeListInsertGenerics(transaction, this, index, content)
|
||||
})
|
||||
} else {
|
||||
// @ts-ignore _prelimContent is defined because this is not yet integrated
|
||||
@ -272,9 +272,9 @@ export class YXmlFragment extends AbstractType {
|
||||
* @param {number} [length=1] The number of elements to remove. Defaults to 1.
|
||||
*/
|
||||
delete (index, length = 1) {
|
||||
if (this._y !== null) {
|
||||
transact(this._y, transaction => {
|
||||
typeArrayDelete(transaction, this, index, length)
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeListDelete(transaction, this, index, length)
|
||||
})
|
||||
} else {
|
||||
// @ts-ignore _prelimContent is defined because this is not yet integrated
|
||||
@ -287,7 +287,7 @@ export class YXmlFragment extends AbstractType {
|
||||
* @return {Array<YXmlElement|YXmlText|YXmlHook>}
|
||||
*/
|
||||
toArray () {
|
||||
return typeArrayToArray(this)
|
||||
return typeListToArray(this)
|
||||
}
|
||||
/**
|
||||
* Transform the properties of this type to binary and write it to an
|
||||
|
@ -48,7 +48,7 @@ export class DeleteSet {
|
||||
/**
|
||||
* Iterate over all structs that were deleted.
|
||||
*
|
||||
* This function expects that the deletes structs are not deleted. Hence, you can
|
||||
* This function expects that the deletes structs are not merged. Hence, you can
|
||||
* probably only use it in type observes and `afterTransaction` events. But not
|
||||
* in `afterTransactionCleanup`.
|
||||
*
|
||||
@ -266,6 +266,6 @@ export const readDeleteSet = (decoder, transaction, store) => {
|
||||
if (unappliedDS.clients.size > 0) {
|
||||
const unappliedDSEncoder = encoding.createEncoder()
|
||||
writeDeleteSet(unappliedDSEncoder, unappliedDS)
|
||||
store.pendingDeleteReaders.push(decoding.createDecoder(encoding.toBuffer(unappliedDSEncoder)))
|
||||
store.pendingDeleteReaders.push(decoding.createDecoder(encoding.toUint8Array(unappliedDSEncoder)))
|
||||
}
|
||||
}
|
||||
|
@ -10,26 +10,23 @@ import {
|
||||
YMap,
|
||||
YXmlFragment,
|
||||
transact,
|
||||
Transaction, YEvent // eslint-disable-line
|
||||
AbstractItem, Transaction, YEvent // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import { Observable } from 'lib0/observable.js'
|
||||
import * as random from 'lib0/random.js'
|
||||
import * as map from 'lib0/map.js'
|
||||
|
||||
// @todo rename to shared document
|
||||
|
||||
/**
|
||||
* A Yjs instance handles the state of shared data.
|
||||
* @extends Observable<string>
|
||||
*/
|
||||
export class Y extends Observable {
|
||||
export class Doc extends Observable {
|
||||
/**
|
||||
* @param {Object|undefined} conf configuration
|
||||
*/
|
||||
constructor (conf = {}) {
|
||||
super()
|
||||
// todo: change to clientId
|
||||
this.clientID = random.uint32()
|
||||
/**
|
||||
* @type {Map<string, AbstractType<YEvent>>}
|
||||
@ -82,7 +79,7 @@ export class Y extends Observable {
|
||||
* }
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {Function} TypeConstructor The constructor of the type definition
|
||||
* @param {Function} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ...
|
||||
* @return {AbstractType<any>} The created type. Constructed with TypeConstructor
|
||||
*
|
||||
* @public
|
||||
@ -97,9 +94,18 @@ export class Y extends Observable {
|
||||
const Constr = type.constructor
|
||||
if (TypeConstructor !== AbstractType && Constr !== TypeConstructor) {
|
||||
if (Constr === AbstractType) {
|
||||
const t = new Constr()
|
||||
// @ts-ignore
|
||||
const t = new TypeConstructor()
|
||||
t._map = type._map
|
||||
type._map.forEach(/** @param {AbstractItem?} n */ n => {
|
||||
for (; n !== null; n = n.left) {
|
||||
n.parent = t
|
||||
}
|
||||
})
|
||||
t._start = type._start
|
||||
for (let n = t._start; n !== null; n = n.right) {
|
||||
n.parent = t
|
||||
}
|
||||
t._length = type._length
|
||||
this.share.set(name, t)
|
||||
t._integrate(this, null)
|
@ -22,16 +22,6 @@ export class ID {
|
||||
*/
|
||||
this.clock = clock
|
||||
}
|
||||
/**
|
||||
* @deprecated
|
||||
* @todo remove and adapt relative position implementation
|
||||
*/
|
||||
toJSON () {
|
||||
return {
|
||||
client: this.client,
|
||||
clock: this.clock
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -91,7 +81,7 @@ export const readID = decoder =>
|
||||
*/
|
||||
export const findRootTypeKey = type => {
|
||||
// @ts-ignore _y must be defined, otherwise unexpected case
|
||||
for (let [key, value] of type._y.share) {
|
||||
for (let [key, value] of type.doc.share) {
|
||||
if (value === type) {
|
||||
return key
|
||||
}
|
||||
|
@ -1,6 +1,3 @@
|
||||
/**
|
||||
* @module Cursors
|
||||
*/
|
||||
|
||||
import {
|
||||
getItem,
|
||||
@ -13,7 +10,7 @@ import {
|
||||
findRootTypeKey,
|
||||
AbstractItem,
|
||||
ItemType,
|
||||
ID, StructStore, Y, AbstractType // eslint-disable-line
|
||||
ID, StructStore, Doc, AbstractType // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
@ -21,29 +18,30 @@ import * as decoding from 'lib0/decoding.js'
|
||||
import * as error from 'lib0/error.js'
|
||||
|
||||
/**
|
||||
* A Cursor is a relative position that is based on the Yjs model. In contrast to an
|
||||
* absolute position (position by index), the Cursor can be
|
||||
* recomputed when remote changes are received. For example:
|
||||
* A relative position is based on the Yjs model and is not affected by document changes.
|
||||
* E.g. If you place a relative position before a certain character, it will always point to this character.
|
||||
* If you place a relative position at the end of a type, it will always point to the end of the type.
|
||||
*
|
||||
* ```Insert(0, 'x')('a|bc') = 'xa|bc'``` Where | is the cursor position.
|
||||
* A numeric position is often unsuited for user selections, because it does not change when content is inserted
|
||||
* before or after.
|
||||
*
|
||||
* A relative cursor position can be obtained with the function
|
||||
* ```Insert(0, 'x')('a|bc') = 'xa|bc'``` Where | is the relative position.
|
||||
*
|
||||
* One of the properties must be defined.
|
||||
*
|
||||
* @example
|
||||
* // Current cursor position is at position 10
|
||||
* const relativePosition = createCursorFromOffset(yText, 10)
|
||||
* const relativePosition = createRelativePositionFromIndex(yText, 10)
|
||||
* // modify yText
|
||||
* yText.insert(0, 'abc')
|
||||
* yText.delete(3, 10)
|
||||
* // Compute the cursor position
|
||||
* const absolutePosition = toAbsolutePosition(y, relativePosition)
|
||||
* const absolutePosition = createAbsolutePositionFromRelativePosition(y, relativePosition)
|
||||
* absolutePosition.type === yText // => true
|
||||
* console.log('cursor location is ' + absolutePosition.offset) // => cursor location is 3
|
||||
* console.log('cursor location is ' + absolutePosition.index) // => cursor location is 3
|
||||
*
|
||||
*/
|
||||
export class Cursor {
|
||||
export class RelativePosition {
|
||||
/**
|
||||
* @param {ID|null} type
|
||||
* @param {string|null} tname
|
||||
@ -63,35 +61,22 @@ export class Cursor {
|
||||
*/
|
||||
this.item = item
|
||||
}
|
||||
toJSON () {
|
||||
const json = {}
|
||||
if (this.type !== null) {
|
||||
json.type = this.type.toJSON()
|
||||
}
|
||||
if (this.tname !== null) {
|
||||
json.tname = this.tname
|
||||
}
|
||||
if (this.item !== null) {
|
||||
json.item = this.item.toJSON()
|
||||
}
|
||||
return json
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} json
|
||||
* @return {Cursor}
|
||||
* @return {RelativePosition}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const createCursorFromJSON = json => new Cursor(json.type == null ? null : createID(json.type.client, json.type.clock), json.tname || null, json.item == null ? null : createID(json.item.client, json.item.clock))
|
||||
export const createRelativePositionFromJSON = json => new RelativePosition(json.type == null ? null : createID(json.type.client, json.type.clock), json.tname || null, json.item == null ? null : createID(json.item.client, json.item.clock))
|
||||
|
||||
export class AbsolutePosition {
|
||||
/**
|
||||
* @param {AbstractType<any>} type
|
||||
* @param {number} offset
|
||||
* @param {number} index
|
||||
*/
|
||||
constructor (type, offset) {
|
||||
constructor (type, index) {
|
||||
/**
|
||||
* @type {AbstractType<any>}
|
||||
*/
|
||||
@ -99,17 +84,17 @@ export class AbsolutePosition {
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
this.offset = offset
|
||||
this.index = index
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractType<any>} type
|
||||
* @param {number} offset
|
||||
* @param {number} index
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const createAbsolutePosition = (type, offset) => new AbsolutePosition(type, offset)
|
||||
export const createAbsolutePosition = (type, index) => new AbsolutePosition(type, index)
|
||||
|
||||
/**
|
||||
* @param {AbstractType<any>} type
|
||||
@ -117,7 +102,7 @@ export const createAbsolutePosition = (type, offset) => new AbsolutePosition(typ
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const createCursor = (type, item) => {
|
||||
export const createRelativePosition = (type, item) => {
|
||||
let typeid = null
|
||||
let tname = null
|
||||
if (type._item === null) {
|
||||
@ -125,40 +110,40 @@ export const createCursor = (type, item) => {
|
||||
} else {
|
||||
typeid = type._item.id
|
||||
}
|
||||
return new Cursor(typeid, tname, item)
|
||||
return new RelativePosition(typeid, tname, item)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a relativePosition based on a absolute position.
|
||||
*
|
||||
* @param {AbstractType<any>} type The base type (e.g. YText or YArray).
|
||||
* @param {number} offset The absolute position.
|
||||
* @return {Cursor}
|
||||
* @param {number} index The absolute position.
|
||||
* @return {RelativePosition}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const createCursorFromTypeOffset = (type, offset) => {
|
||||
export const createRelativePositionFromTypeIndex = (type, index) => {
|
||||
let t = type._start
|
||||
while (t !== null) {
|
||||
if (!t.deleted && t.countable) {
|
||||
if (t.length > offset) {
|
||||
if (t.length > index) {
|
||||
// case 1: found position somewhere in the linked list
|
||||
return createCursor(type, createID(t.id.client, t.id.clock + offset))
|
||||
return createRelativePosition(type, createID(t.id.client, t.id.clock + index))
|
||||
}
|
||||
offset -= t.length
|
||||
index -= t.length
|
||||
}
|
||||
t = t.right
|
||||
}
|
||||
return createCursor(type, null)
|
||||
return createRelativePosition(type, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {Cursor} rpos
|
||||
* @param {RelativePosition} rpos
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const writeCursor = (encoder, rpos) => {
|
||||
export const writeRelativePosition = (encoder, rpos) => {
|
||||
const { type, tname, item } = rpos
|
||||
if (item !== null) {
|
||||
encoding.writeVarUint(encoder, 0)
|
||||
@ -177,15 +162,23 @@ export const writeCursor = (encoder, rpos) => {
|
||||
return encoder
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RelativePosition} rpos
|
||||
* @return {Uint8Array}
|
||||
*/
|
||||
export const encodeRelativePosition = rpos => {
|
||||
const encoder = encoding.createEncoder()
|
||||
writeRelativePosition(encoder, rpos)
|
||||
return encoding.toUint8Array(encoder)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {Y} y
|
||||
* @param {StructStore} store
|
||||
* @return {Cursor|null}
|
||||
* @return {RelativePosition|null}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const readCursor = (decoder, y, store) => {
|
||||
export const readRelativePosition = decoder => {
|
||||
let type = null
|
||||
let tname = null
|
||||
let itemID = null
|
||||
@ -203,23 +196,29 @@ export const readCursor = (decoder, y, store) => {
|
||||
type = readID(decoder)
|
||||
}
|
||||
}
|
||||
return new Cursor(type, tname, itemID)
|
||||
return new RelativePosition(type, tname, itemID)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Cursor} cursor
|
||||
* @param {Y} y
|
||||
* @param {Uint8Array} uint8Array
|
||||
* @return {RelativePosition|null}
|
||||
*/
|
||||
export const decodeRelativePosition = uint8Array => readRelativePosition(decoding.createDecoder(uint8Array))
|
||||
|
||||
/**
|
||||
* @param {RelativePosition} rpos
|
||||
* @param {Doc} doc
|
||||
* @return {AbsolutePosition|null}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const createAbsolutePositionFromCursor = (cursor, y) => {
|
||||
const store = y.store
|
||||
const rightID = cursor.item
|
||||
const typeID = cursor.type
|
||||
const tname = cursor.tname
|
||||
export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
|
||||
const store = doc.store
|
||||
const rightID = rpos.item
|
||||
const typeID = rpos.type
|
||||
const tname = rpos.tname
|
||||
let type = null
|
||||
let offset = 0
|
||||
let index = 0
|
||||
if (rightID !== null) {
|
||||
if (getState(store, rightID.client) <= rightID.clock) {
|
||||
return null
|
||||
@ -228,18 +227,18 @@ export const createAbsolutePositionFromCursor = (cursor, y) => {
|
||||
if (!(right instanceof AbstractItem)) {
|
||||
return null
|
||||
}
|
||||
offset = right.deleted || !right.countable ? 0 : rightID.clock - right.id.clock
|
||||
index = right.deleted || !right.countable ? 0 : rightID.clock - right.id.clock
|
||||
let n = right.left
|
||||
while (n !== null) {
|
||||
if (!n.deleted && n.countable) {
|
||||
offset += n.length
|
||||
index += n.length
|
||||
}
|
||||
n = n.left
|
||||
}
|
||||
type = right.parent
|
||||
} else {
|
||||
if (tname !== null) {
|
||||
type = y.get(tname)
|
||||
type = doc.get(tname)
|
||||
} else if (typeID !== null) {
|
||||
if (getState(store, typeID.client) <= typeID.clock) {
|
||||
// type does not exist yet
|
||||
@ -255,20 +254,20 @@ export const createAbsolutePositionFromCursor = (cursor, y) => {
|
||||
} else {
|
||||
throw error.unexpectedCase()
|
||||
}
|
||||
offset = type._length
|
||||
index = type._length
|
||||
}
|
||||
if (type._item !== null && type._item.deleted) {
|
||||
return null
|
||||
}
|
||||
return createAbsolutePosition(type, offset)
|
||||
return createAbsolutePosition(type, index)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Cursor|null} a
|
||||
* @param {Cursor|null} b
|
||||
* @param {RelativePosition|null} a
|
||||
* @param {RelativePosition|null} b
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const compareCursors = (a, b) => a === b || (
|
||||
export const compareRelativePositions = (a, b) => a === b || (
|
||||
a !== null && b !== null && a.tname === b.tname && compareIDs(a.item, b.item) && compareIDs(a.type, b.type)
|
||||
)
|
@ -6,12 +6,10 @@ import {
|
||||
|
||||
export class Snapshot {
|
||||
/**
|
||||
* @param {DeleteSet} ds delete store
|
||||
* @param {DeleteSet} ds
|
||||
* @param {Map<number,number>} sm state map
|
||||
* @param {Map<number,string>} userMap
|
||||
* @private
|
||||
*/
|
||||
constructor (ds, sm, userMap) {
|
||||
constructor (ds, sm) {
|
||||
/**
|
||||
* @type {DeleteSet}
|
||||
* @private
|
||||
@ -23,14 +21,15 @@ export class Snapshot {
|
||||
* @private
|
||||
*/
|
||||
this.sm = sm
|
||||
/**
|
||||
* @type {Map<number,string>}
|
||||
* @private
|
||||
*/
|
||||
this.userMap = userMap
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DeleteSet} ds
|
||||
* @param {Map<number,number>} sm
|
||||
*/
|
||||
export const createSnapshot = (ds, sm) => new Snapshot(ds, sm)
|
||||
|
||||
/**
|
||||
* @param {AbstractItem} item
|
||||
* @param {Snapshot|undefined} snapshot
|
||||
|
@ -6,8 +6,7 @@ import {
|
||||
|
||||
import * as math from 'lib0/math.js'
|
||||
import * as error from 'lib0/error.js'
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
import * as decoding from 'lib0/decoding.js' // eslint-disable-line
|
||||
|
||||
export class StructStore {
|
||||
constructor () {
|
||||
@ -51,7 +50,7 @@ export class StructStore {
|
||||
* @public
|
||||
* @function
|
||||
*/
|
||||
export const getStates = store => {
|
||||
export const getStateVector = store => {
|
||||
const sm = new Map()
|
||||
store.clients.forEach((structs, client) => {
|
||||
const struct = structs[structs.length - 1]
|
||||
@ -262,42 +261,3 @@ export const replaceStruct = (store, struct, newStruct) => {
|
||||
const structs = store.clients.get(struct.id.client)
|
||||
structs[findIndexSS(structs, struct.id.clock)] = newStruct
|
||||
}
|
||||
|
||||
/**
|
||||
* Read StateMap from Decoder and return as Map
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @return {Map<number,number>}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const readStatesAsMap = decoder => {
|
||||
const ss = new Map()
|
||||
const ssLength = decoding.readVarUint(decoder)
|
||||
for (let i = 0; i < ssLength; i++) {
|
||||
const client = decoding.readVarUint(decoder)
|
||||
const clock = decoding.readVarUint(decoder)
|
||||
ss.set(client, clock)
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
/**
|
||||
* Write StateMap to Encoder
|
||||
*
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {StructStore} store
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const writeStates = (encoder, store) => {
|
||||
encoding.writeVarUint(encoder, store.clients.size)
|
||||
store.clients.forEach((structs, client) => {
|
||||
const id = structs[structs.length - 1].id
|
||||
encoding.writeVarUint(encoder, id.client)
|
||||
encoding.writeVarUint(encoder, id.clock)
|
||||
})
|
||||
return encoder
|
||||
}
|
||||
|
@ -6,11 +6,11 @@ import {
|
||||
writeDeleteSet,
|
||||
DeleteSet,
|
||||
sortAndMergeDeleteSet,
|
||||
getStates,
|
||||
getStateVector,
|
||||
findIndexSS,
|
||||
callEventHandlerListeners,
|
||||
AbstractItem,
|
||||
ID, AbstractType, AbstractStruct, YEvent, Y // eslint-disable-line
|
||||
ID, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
@ -43,15 +43,15 @@ import * as math from 'lib0/math.js'
|
||||
*/
|
||||
export class Transaction {
|
||||
/**
|
||||
* @param {Y} y
|
||||
* @param {Doc} doc
|
||||
* @param {any} origin
|
||||
*/
|
||||
constructor (y, origin) {
|
||||
constructor (doc, origin) {
|
||||
/**
|
||||
* The Yjs instance.
|
||||
* @type {Y}
|
||||
* @type {Doc}
|
||||
*/
|
||||
this.y = y
|
||||
this.doc = doc
|
||||
/**
|
||||
* Describes the set of deleted items by ids
|
||||
* @type {DeleteSet}
|
||||
@ -61,7 +61,7 @@ export class Transaction {
|
||||
* Holds the state before the transaction started.
|
||||
* @type {Map<Number,Number>}
|
||||
*/
|
||||
this.beforeState = getStates(y.store)
|
||||
this.beforeState = getStateVector(doc.store)
|
||||
/**
|
||||
* Holds the state after the transaction.
|
||||
* @type {Map<Number,Number>}
|
||||
@ -80,11 +80,6 @@ export class Transaction {
|
||||
* @type {Map<AbstractType<YEvent>,Array<YEvent>>}
|
||||
*/
|
||||
this.changedParentTypes = new Map()
|
||||
/**
|
||||
* @type {encoding.Encoder|null}
|
||||
* @private
|
||||
*/
|
||||
this._updateMessage = null
|
||||
/**
|
||||
* @type {Set<ID>}
|
||||
* @private
|
||||
@ -95,21 +90,20 @@ export class Transaction {
|
||||
*/
|
||||
this.origin = origin
|
||||
}
|
||||
/**
|
||||
* @type {encoding.Encoder|null}
|
||||
* @public
|
||||
*/
|
||||
get updateMessage () {
|
||||
// only create if content was added in transaction (state or ds changed)
|
||||
if (this._updateMessage === null && (this.deleteSet.clients.size > 0 || map.any(this.afterState, (clock, client) => this.beforeState.get(client) !== clock))) {
|
||||
const encoder = encoding.createEncoder()
|
||||
sortAndMergeDeleteSet(this.deleteSet)
|
||||
writeStructsFromTransaction(encoder, this)
|
||||
writeDeleteSet(encoder, this.deleteSet)
|
||||
this._updateMessage = encoder
|
||||
}
|
||||
return this._updateMessage
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
export const computeUpdateMessageFromTransaction = transaction => {
|
||||
if (transaction.deleteSet.clients.size === 0 && !map.any(transaction.afterState, (clock, client) => transaction.beforeState.get(client) !== clock)) {
|
||||
return null
|
||||
}
|
||||
const encoder = encoding.createEncoder()
|
||||
sortAndMergeDeleteSet(transaction.deleteSet)
|
||||
writeStructsFromTransaction(encoder, transaction)
|
||||
writeDeleteSet(encoder, transaction.deleteSet)
|
||||
return encoder
|
||||
}
|
||||
|
||||
/**
|
||||
@ -119,45 +113,44 @@ export class Transaction {
|
||||
* @function
|
||||
*/
|
||||
export const nextID = transaction => {
|
||||
const y = transaction.y
|
||||
const y = transaction.doc
|
||||
return createID(y.clientID, getState(y.store, y.clientID))
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the functionality of `y.transact(()=>{..})`
|
||||
*
|
||||
* @param {Y} y
|
||||
* @param {Doc} doc
|
||||
* @param {function(Transaction):void} f
|
||||
* @param {any} [origin]
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const transact = (y, f, origin = null) => {
|
||||
const transactionCleanups = y._transactionCleanups
|
||||
export const transact = (doc, f, origin = null) => {
|
||||
const transactionCleanups = doc._transactionCleanups
|
||||
let initialCall = false
|
||||
if (y._transaction === null) {
|
||||
if (doc._transaction === null) {
|
||||
initialCall = true
|
||||
y._transaction = new Transaction(y, origin)
|
||||
transactionCleanups.push(y._transaction)
|
||||
y.emit('beforeTransaction', [y._transaction, y])
|
||||
doc._transaction = new Transaction(doc, origin)
|
||||
transactionCleanups.push(doc._transaction)
|
||||
doc.emit('beforeTransaction', [doc._transaction, doc])
|
||||
}
|
||||
try {
|
||||
f(y._transaction)
|
||||
f(doc._transaction)
|
||||
} finally {
|
||||
// @todo set after state here
|
||||
if (initialCall && transactionCleanups[0] === y._transaction) {
|
||||
if (initialCall && transactionCleanups[0] === doc._transaction) {
|
||||
// The first transaction ended, now process observer calls.
|
||||
// Observer call may create new transactions for which we need to call the observers and do cleanup.
|
||||
// We don't want to nest these calls, so we execute these calls one after another
|
||||
for (let i = 0; i < transactionCleanups.length; i++) {
|
||||
const transaction = transactionCleanups[i]
|
||||
const store = transaction.y.store
|
||||
const store = transaction.doc.store
|
||||
const ds = transaction.deleteSet
|
||||
sortAndMergeDeleteSet(ds)
|
||||
transaction.afterState = getStates(transaction.y.store)
|
||||
y._transaction = null
|
||||
y.emit('beforeObserverCalls', [transaction, y])
|
||||
transaction.afterState = getStateVector(transaction.doc.store)
|
||||
doc._transaction = null
|
||||
doc.emit('beforeObserverCalls', [transaction, doc])
|
||||
// emit change events on changed types
|
||||
transaction.changed.forEach((subs, itemtype) => {
|
||||
itemtype._callObserver(transaction, subs)
|
||||
@ -175,7 +168,7 @@ export const transact = (y, f, origin = null) => {
|
||||
// because we know it has at least one element
|
||||
callEventHandlerListeners(type._dEH, events, transaction)
|
||||
})
|
||||
y.emit('afterTransaction', [transaction, y])
|
||||
doc.emit('afterTransaction', [transaction, doc])
|
||||
/**
|
||||
* @param {Array<AbstractStruct>} structs
|
||||
* @param {number} pos
|
||||
@ -213,7 +206,7 @@ export const transact = (y, f, origin = null) => {
|
||||
break
|
||||
}
|
||||
if (struct.deleted && struct instanceof AbstractItem) {
|
||||
struct.gc(transaction, store, false)
|
||||
struct.gc(store, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -276,10 +269,15 @@ export const transact = (y, f, origin = null) => {
|
||||
}
|
||||
}
|
||||
// @todo Merge all the transactions into one and provide send the data as a single update message
|
||||
// @todo implement a dedicatet event that we can use to send updates to other peer
|
||||
y.emit('afterTransactionCleanup', [transaction, y])
|
||||
doc.emit('afterTransactionCleanup', [transaction, doc])
|
||||
if (doc._observers.has('update')) {
|
||||
const updateMessage = computeUpdateMessageFromTransaction(transaction)
|
||||
if (updateMessage !== null) {
|
||||
doc.emit('update', [encoding.toUint8Array(updateMessage), transaction.origin, doc])
|
||||
}
|
||||
}
|
||||
}
|
||||
y._transactionCleanups = []
|
||||
doc._transactionCleanups = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -116,10 +116,10 @@ export class UndoManager {
|
||||
this._undoing = false
|
||||
this._redoing = false
|
||||
this._lastTransactionWasUndo = false
|
||||
const y = scope._y
|
||||
this.y = y
|
||||
const doc = scope.doc
|
||||
this.y = doc
|
||||
let bindingInfos
|
||||
y.on('beforeTransaction', (y, transaction, remote) => {
|
||||
doc.on('beforeTransaction', (y, transaction, remote) => {
|
||||
if (!remote) {
|
||||
// Store binding information before transaction is executed
|
||||
// By restoring the binding information, we can make sure that the state
|
||||
@ -130,7 +130,7 @@ export class UndoManager {
|
||||
})
|
||||
}
|
||||
})
|
||||
y.on('afterTransaction', (y, transaction, remote) => {
|
||||
doc.on('afterTransaction', (y, transaction, remote) => {
|
||||
if (!remote && transaction.changedParentTypes.has(scope)) {
|
||||
let reverseOperation = new ReverseOperation(y, transaction, bindingInfos)
|
||||
if (!this._undoing) {
|
||||
@ -200,3 +200,4 @@ export class UndoManager {
|
||||
return performedRedo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,11 +17,11 @@ import {
|
||||
createID,
|
||||
readID,
|
||||
getState,
|
||||
getStates,
|
||||
getStateVector,
|
||||
readDeleteSet,
|
||||
writeDeleteSet,
|
||||
createDeleteSetFromStructStore,
|
||||
Transaction, AbstractStruct, AbstractStructRef, StructStore, ID // eslint-disable-line
|
||||
Doc, Transaction, AbstractStruct, AbstractStructRef, StructStore, ID // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
@ -104,7 +104,7 @@ export const writeClientsStructs = (encoder, store, _sm) => {
|
||||
sm.set(client, clock)
|
||||
}
|
||||
})
|
||||
getStates(store).forEach((clock, client) => {
|
||||
getStateVector(store).forEach((clock, client) => {
|
||||
if (!_sm.has(client)) {
|
||||
sm.set(client, 0)
|
||||
}
|
||||
@ -250,7 +250,7 @@ export const tryResumePendingDeleteReaders = (transaction, store) => {
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const writeStructsFromTransaction = (encoder, transaction) => writeClientsStructs(encoder, transaction.y.store, transaction.beforeState)
|
||||
export const writeStructsFromTransaction = (encoder, transaction) => writeClientsStructs(encoder, transaction.doc.store, transaction.beforeState)
|
||||
|
||||
/**
|
||||
* @param {StructStore} store
|
||||
@ -297,25 +297,127 @@ export const readStructs = (decoder, transaction, store) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and apply a document update.
|
||||
*
|
||||
* This function has the same effect as `applyUpdate` but accepts an decoder.
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
* @param {Doc} ydoc
|
||||
* @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))`
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const readModel = (decoder, transaction, store) => {
|
||||
readStructs(decoder, transaction, store)
|
||||
readDeleteSet(decoder, transaction, store)
|
||||
export const readUpdate = (decoder, ydoc, transactionOrigin) =>
|
||||
ydoc.transact(transaction => {
|
||||
readStructs(decoder, transaction, ydoc.store)
|
||||
readDeleteSet(decoder, transaction, ydoc.store)
|
||||
}, transactionOrigin)
|
||||
|
||||
/**
|
||||
* Apply a document update created by, for example, `y.on('update', update => ..)` or `update = encodeStateAsUpdate()`.
|
||||
*
|
||||
* This function has the same effect as `readUpdate` but accepts an Uint8Array instead of a Decoder.
|
||||
*
|
||||
* @param {Doc} ydoc
|
||||
* @param {Uint8Array} update
|
||||
* @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))`
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const applyUpdate = (ydoc, update, transactionOrigin) =>
|
||||
readUpdate(decoding.createDecoder(update), ydoc, transactionOrigin)
|
||||
|
||||
/**
|
||||
* Write all the document as a single update message. If you specify the state of the remote client (`targetStateVector`) it will
|
||||
* only write the operations that are missing.
|
||||
*
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {Doc} doc
|
||||
* @param {Map<number,number>} [targetStateVector] The state of the target that receives the update. Leave empty to write all known structs
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const writeStateAsUpdate = (encoder, doc, targetStateVector = new Map()) => {
|
||||
writeClientsStructs(encoder, doc.store, targetStateVector)
|
||||
writeDeleteSet(encoder, createDeleteSetFromStructStore(doc.store))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {StructStore} store
|
||||
* @param {Map<number,number>} [targetState] The state of the target that receives the update. Leave empty to write all known structs
|
||||
* Write all the document as a single update message that can be applied on the remote document. If you specify the state of the remote client (`targetState`) it will
|
||||
* only write the operations that are missing.
|
||||
*
|
||||
* Use `writeStateAsUpdate` instead if you are working with lib0/encoding.js#Encoder
|
||||
*
|
||||
* @param {Doc} doc
|
||||
* @param {Uint8Array} [encodedTargetStateVector] The state of the target that receives the update. Leave empty to write all known structs
|
||||
* @return {Uint8Array}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const writeModel = (encoder, store, targetState = new Map()) => {
|
||||
writeClientsStructs(encoder, store, targetState)
|
||||
writeDeleteSet(encoder, createDeleteSetFromStructStore(store))
|
||||
export const encodeStateAsUpdate = (doc, encodedTargetStateVector) => {
|
||||
const encoder = encoding.createEncoder()
|
||||
const targetStateVector = encodedTargetStateVector == null ? new Map() : decodeStateVector(encodedTargetStateVector)
|
||||
writeStateAsUpdate(encoder, doc, targetStateVector)
|
||||
return encoding.toUint8Array(encoder)
|
||||
}
|
||||
|
||||
/**
|
||||
* Read state vector from Decoder and return as Map
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @return {Map<number,number>} Maps `client` to the number next expected `clock` from that client.
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const readStateVector = decoder => {
|
||||
const ss = new Map()
|
||||
const ssLength = decoding.readVarUint(decoder)
|
||||
for (let i = 0; i < ssLength; i++) {
|
||||
const client = decoding.readVarUint(decoder)
|
||||
const clock = decoding.readVarUint(decoder)
|
||||
ss.set(client, clock)
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
/**
|
||||
* Read decodedState and return State as Map.
|
||||
*
|
||||
* @param {Uint8Array} decodedState
|
||||
* @return {Map<number,number>} Maps `client` to the number next expected `clock` from that client.
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const decodeStateVector = decodedState => readStateVector(decoding.createDecoder(decodedState))
|
||||
|
||||
/**
|
||||
* Write State Vector to `lib0/encoding.js#Encoder`.
|
||||
*
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {Doc} doc
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const writeDocumentStateVector = (encoder, doc) => {
|
||||
encoding.writeVarUint(encoder, doc.store.clients.size)
|
||||
doc.store.clients.forEach((structs, client) => {
|
||||
const id = structs[structs.length - 1].id
|
||||
encoding.writeVarUint(encoder, id.client)
|
||||
encoding.writeVarUint(encoder, id.clock)
|
||||
})
|
||||
return encoder
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode State as Uint8Array.
|
||||
*
|
||||
* @param {Doc} doc
|
||||
* @return {Uint8Array}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const encodeDocumentStateVector = doc => {
|
||||
const encoder = encoding.createEncoder()
|
||||
writeDocumentStateVector(encoder, doc)
|
||||
return encoding.toUint8Array(encoder)
|
||||
}
|
||||
|
@ -2,9 +2,9 @@ import * as Y from '../src/index.js'
|
||||
|
||||
import {
|
||||
createDeleteSetFromStructStore,
|
||||
getStates,
|
||||
getStateVector,
|
||||
AbstractItem,
|
||||
DeleteSet, StructStore // eslint-disable-line
|
||||
DeleteSet, StructStore, Doc // eslint-disable-line
|
||||
} from '../src/internals.js'
|
||||
|
||||
import * as t from 'lib0/testing.js'
|
||||
@ -13,24 +13,9 @@ import * as encoding from 'lib0/encoding.js'
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
import * as syncProtocol from 'y-protocols/sync.js'
|
||||
|
||||
/**
|
||||
* @param {Y.Transaction} transaction
|
||||
* @param {TestYInstance} y
|
||||
*/
|
||||
const afterTransaction = (transaction, y) => {
|
||||
if (transaction.origin !== y.tc) {
|
||||
const m = transaction.updateMessage
|
||||
if (m !== null) {
|
||||
const encoder = encoding.createEncoder()
|
||||
syncProtocol.writeUpdate(encoder, m)
|
||||
broadcastMessage(y, encoding.toBuffer(encoder))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TestYInstance} y // publish message created by `y` to all other online clients
|
||||
* @param {ArrayBuffer} m
|
||||
* @param {Uint8Array} m
|
||||
*/
|
||||
const broadcastMessage = (y, m) => {
|
||||
if (y.tc.onlineConns.has(y)) {
|
||||
@ -42,7 +27,7 @@ const broadcastMessage = (y, m) => {
|
||||
}
|
||||
}
|
||||
|
||||
export class TestYInstance extends Y.Y {
|
||||
export class TestYInstance extends Doc {
|
||||
/**
|
||||
* @param {TestConnector} testConnector
|
||||
* @param {number} clientID
|
||||
@ -55,12 +40,18 @@ export class TestYInstance extends Y.Y {
|
||||
*/
|
||||
this.tc = testConnector
|
||||
/**
|
||||
* @type {Map<TestYInstance, Array<ArrayBuffer>>}
|
||||
* @type {Map<TestYInstance, Array<Uint8Array>>}
|
||||
*/
|
||||
this.receiving = new Map()
|
||||
testConnector.allConns.add(this)
|
||||
// set up observe on local model
|
||||
this.on('afterTransactionCleanup', afterTransaction)
|
||||
this.on('update', /** @param {Uint8Array} update @param {any} origin */ (update, origin) => {
|
||||
if (origin !== testConnector) {
|
||||
const encoder = encoding.createEncoder()
|
||||
syncProtocol.writeUpdate(encoder, update)
|
||||
broadcastMessage(this, encoding.toUint8Array(encoder))
|
||||
}
|
||||
})
|
||||
this.connect()
|
||||
}
|
||||
/**
|
||||
@ -78,15 +69,15 @@ export class TestYInstance extends Y.Y {
|
||||
if (!this.tc.onlineConns.has(this)) {
|
||||
this.tc.onlineConns.add(this)
|
||||
const encoder = encoding.createEncoder()
|
||||
syncProtocol.writeSyncStep1(encoder, this.store)
|
||||
syncProtocol.writeSyncStep1(encoder, this)
|
||||
// publish SyncStep1
|
||||
broadcastMessage(this, encoding.toBuffer(encoder))
|
||||
broadcastMessage(this, encoding.toUint8Array(encoder))
|
||||
this.tc.onlineConns.forEach(remoteYInstance => {
|
||||
if (remoteYInstance !== this) {
|
||||
// remote instance sends instance to this instance
|
||||
const encoder = encoding.createEncoder()
|
||||
syncProtocol.writeSyncStep1(encoder, remoteYInstance.store)
|
||||
this._receive(encoding.toBuffer(encoder), remoteYInstance)
|
||||
syncProtocol.writeSyncStep1(encoder, remoteYInstance)
|
||||
this._receive(encoding.toUint8Array(encoder), remoteYInstance)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -95,7 +86,7 @@ export class TestYInstance extends Y.Y {
|
||||
* Receive a message from another client. This message is only appended to the list of receiving messages.
|
||||
* TestConnector decides when this client actually reads this message.
|
||||
*
|
||||
* @param {ArrayBuffer} message
|
||||
* @param {Uint8Array} message
|
||||
* @param {TestYInstance} remoteClient
|
||||
*/
|
||||
_receive (message, remoteClient) {
|
||||
@ -164,7 +155,7 @@ export class TestConnector {
|
||||
syncProtocol.readSyncMessage(decoding.createDecoder(m), encoder, receiver, receiver.tc)
|
||||
if (encoding.length(encoder) > 0) {
|
||||
// send reply message
|
||||
sender._receive(encoding.toBuffer(encoder), receiver)
|
||||
sender._receive(encoding.toUint8Array(encoder), receiver)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@ -282,7 +273,7 @@ export const compare = users => {
|
||||
// @ts-ignore
|
||||
t.compare(userTextValues[i].map(a => a.insert).join('').length, users[i].getText('text').length)
|
||||
t.compare(userTextValues[i], userTextValues[i + 1])
|
||||
t.compare(getStates(users[i].store), getStates(users[i + 1].store))
|
||||
t.compare(getStateVector(users[i].store), getStateVector(users[i + 1].store))
|
||||
compareDS(createDeleteSetFromStructStore(users[i].store), createDeleteSetFromStructStore(users[i + 1].store))
|
||||
compareStructStores(users[i].store, users[i + 1].store)
|
||||
}
|
||||
|
@ -263,7 +263,7 @@ export const testEventTargetIsSetCorrectlyOnRemote = tc => {
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testIteratingArrayContainingTypes = tc => {
|
||||
const y = new Y.Y()
|
||||
const y = new Y.Doc()
|
||||
const arr = y.getArray('arr')
|
||||
const numItems = 10
|
||||
for (let i = 0; i < numItems; i++) {
|
||||
|
@ -54,7 +54,7 @@
|
||||
/* Experimental Options */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
"maxNodeModuleJsDepth": 1,
|
||||
"maxNodeModuleJsDepth": 5,
|
||||
// "types": ["./src/utils/typedefs.js"]
|
||||
},
|
||||
"include": ["./src/**/*", "./tests/**/*"],
|
||||
|
Loading…
x
Reference in New Issue
Block a user