Compare commits

..

22 Commits

Author SHA1 Message Date
Kevin Jahns
a059fa12e9 13.1.0 2020-06-02 23:52:56 +02:00
Kevin Jahns
0628d8f1c9 fix linting 2020-06-02 23:44:13 +02:00
Kevin Jahns
19e2d51190 Merge branch 'master' of github.com:yjs/yjs 2020-06-02 23:20:54 +02:00
Kevin Jahns
60fab42b3f improve memory allocation ⇒ less "minor gc" cleanups 2020-06-02 23:20:45 +02:00
Kevin Jahns
c9756e5b57 add npm funding url 2020-05-31 23:24:35 +02:00
Kevin Jahns
601d24e930 Add more backers ❤️ 2020-05-30 21:20:59 +02:00
Kevin Jahns
b2c16674f2 Add sponsors to readme ❤️ 2020-05-29 15:19:43 +02:00
Kevin Jahns
13da804b5e use organization funding and issue template file 2020-05-18 23:46:32 +02:00
Kevin Jahns
c5ca7b6f8c Update issue templates 2020-05-18 23:31:10 +02:00
Kevin Jahns
f4b68c0dd4 Merge pull request #200 from Mansehej/yarray-unshift
Implement unshift function in Y-Array
2020-05-18 22:14:13 +02:00
Mansehej
4407f70052 Update ReadMe for y-array unshift 2020-05-19 01:01:23 +05:30
Mansehej
8bb52a485a Implement unshift to y-arrays 2020-05-19 01:01:23 +05:30
Kevin Jahns
9fc18d5ce0 fix lint issues 2020-05-18 18:43:16 +02:00
Kevin Jahns
ada4f400b5 Merge branch 'mohe2015-patch-1' 2020-05-18 18:04:18 +02:00
Kevin Jahns
06048b87ee rework provider combination demo 2020-05-18 18:04:04 +02:00
Kevin Jahns
05dde1db01 Merge branch 'patch-1' of git://github.com/mohe2015/yjs into mohe2015-patch-1 2020-05-18 17:41:20 +02:00
Kevin Jahns
b5b32c5b3c add relm and nimbus as users of Yjs 2020-05-18 17:09:44 +02:00
Kevin Jahns
3f0e2078de Update README.md 2020-05-14 17:01:49 +02:00
Kevin Jahns
21470bb409 Update README.md 2020-05-14 16:59:48 +02:00
Moritz Hedtke
8221db795a Update README.md 2020-04-27 22:39:09 +02:00
Moritz Hedtke
68b4418956 Update README.md 2020-04-27 22:35:37 +02:00
Moritz Hedtke
fa09ebfd82 Add example of combining providers to README.md 2020-04-27 22:31:26 +02:00
22 changed files with 778 additions and 564 deletions

12
.github/FUNDING.yml vendored
View File

@@ -1,12 +0,0 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: dmonad
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -25,6 +25,43 @@ build collaborative or distributed applications ping us at
<yjs@tag1consulting.com>. Otherwise you can find help on our <yjs@tag1consulting.com>. Otherwise you can find help on our
[discussion board](https://discuss.yjs.dev). [discussion board](https://discuss.yjs.dev).
## Sponsors
I'm currently looking for sponsors that allow me to be less dependent on
contracting work. These awesome backers already fund further development of
Yjs:
[![Vincent Waller](https://github.com/vwall.png?size=60)](https://github.com/vwall)
[<img src="https://user-images.githubusercontent.com/5553757/83337333-a7bcb380-a2ba-11ea-837b-e404eb35d318.png"
height="60px" />](https://input.com/)
[![Duane Johnson](https://github.com/canadaduane.png?size=60)](https://github.com/canadaduane)
[![Joe Reeve](https://github.com/ISNIT0.png?size=60)](https://github.com/ISNIT0)
[<img src="https://room.sh/img/icons/android-chrome-192x192.png" height="60px" />](https://room.sh/)
[![JourneyApps](https://github.com/journeyapps.png?size=60)](https://github.com/journeyapps)
[![Adam Brunnmeier](https://github.com/adabru.png?size=60)](https://github.com/adabru)
[![Nathanael Anderson](https://github.com/NathanaelA.png?size=60)](https://github.com/NathanaelA)
Sponsorship also comes with special perks! [![Become a Sponsor](https://img.shields.io/static/v1?label=Become%20a%20Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=d42f2d)](https://github.com/sponsors/dmonad)
## Who is using Yjs
* [Relm](http://www.relm.us/) A collaborative gameworld for teamwork and
community. :star2:
* [Input](https://input.com/) A collaborative note taking app. :star2:
* [Room.sh](https://room.sh/) A meeting application with integrated
collaborative drawing, editing, and coding tools. :star:
* [http://coronavirustechhandbook.com/](https://coronavirustechhandbook.com/)
A collaborative wiki that is edited by thousands of different people to work
on a rapid and sophisticated response to the coronavirus outbreak and
subsequent impacts. :star:
* [Nimbus Note](https://nimbusweb.me/note.php) A note-taking app designed by
Nimbus Web.
* [JoeDocs](https://joedocs.com/) An open collaborative wiki.
* [Pluxbox RadioManager](https://pluxbox.com/) A web-based app to
collaboratively organize radio broadcasts.
* [Cattaz](http://cattaz.io/) A wiki that can run custom applications in the
wiki pages.
## Table of Contents ## Table of Contents
* [Overview](#Overview) * [Overview](#Overview)
@@ -137,6 +174,54 @@ Now you understand how types are defined on a shared document. Next you can jump
to the [demo repository](https://github.com/yjs/yjs-demos) or continue reading to the [demo repository](https://github.com/yjs/yjs-demos) or continue reading
the API docs. the API docs.
### Example: Using and combining providers
Any of the Yjs providers can be combined with each other. So you can sync data
over different network technologies.
In most cases you want to use a network provider (like y-websocket or y-webrtc)
in combination with a persistence provider (y-indexeddb in the browser).
Persistence allows you to load the document faster and to persist data that is
created while offline.
For the sake of this demo we combine two different network providers with a
persistence provider.
```js
import * as Y from 'yjs'
import { WebrtcProvider } from 'y-webrtc'
import { WebsocketProvider } from 'y-websocket'
import { IndexeddbPersistence } from 'y-indexeddb'
const ydoc = new Y.Doc()
// this allows you to instantly get the (cached) documents data
const indexeddbProvider = new IndexeddbPersistence('count-demo', ydoc)
idbP.whenSynced.then(() => {
console.log('loaded data from indexed db')
})
// Sync clients with the y-webrtc provider.
const webrtcProvider = new WebrtcProvider('count-demo', ydoc)
// Sync clients with the y-websocket provider
const websocketProvider = new WebsocketProvider(
'wss://demos.yjs.dev', 'count-demo', ydoc
)
// array of numbers which produce a sum
const yarray = ydoc.getArray('count')
// observe changes of the sum
yarray.observe(event => {
// print updates when the data changes
console.log('new sum: ' + yarray.toArray().reduce((a,b) => a + b))
})
// add 1 to the sum
yarray.push([1]) // => "new sum: 1"
```
## API ## API
```js ```js
@@ -158,11 +243,13 @@ necessary.
<b><code>insert(index:number, content:Array&lt;object|boolean|Array|string|number|Uint8Array|Y.Type&gt;)</code></b> <b><code>insert(index:number, content:Array&lt;object|boolean|Array|string|number|Uint8Array|Y.Type&gt;)</code></b>
<dd> <dd>
Insert content at <var>index</var>. Note that content is an array of elements. 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 I.e. <code>array.insert(0, [1])</code> splices the list and inserts 1 at
position 0. position 0.
</dd> </dd>
<b><code>push(Array&lt;Object|boolean|Array|string|number|Uint8Array|Y.Type&gt;)</code></b> <b><code>push(Array&lt;Object|boolean|Array|string|number|Uint8Array|Y.Type&gt;)</code></b>
<dd></dd> <dd></dd>
<b><code>unshift(Array&lt;Object|boolean|Array|string|number|Uint8Array|Y.Type&gt;)</code></b>
<dd></dd>
<b><code>delete(index:number, length:number)</code></b> <b><code>delete(index:number, length:number)</code></b>
<dd></dd> <dd></dd>
<b><code>get(index:number)</code></b> <b><code>get(index:number)</code></b>

919
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,15 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.0.8", "version": "13.1.0",
"description": "Shared Editing Library", "description": "Shared Editing Library",
"main": "./dist/yjs.cjs", "main": "./dist/yjs.cjs",
"module": "./dist/yjs.mjs", "module": "./dist/yjs.mjs",
"types": "./dist/src/index.d.ts", "types": "./dist/src/index.d.ts",
"sideEffects": false, "sideEffects": false,
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
},
"scripts": { "scripts": {
"test": "npm run dist && node ./dist/tests.cjs --repitition-time 50", "test": "npm run dist && node ./dist/tests.cjs --repitition-time 50",
"test-extensive": "npm run lint && npm run dist && node ./dist/tests.cjs --production --repitition-time 10000", "test-extensive": "npm run lint && npm run dist && node ./dist/tests.cjs --production --repitition-time 10000",
@@ -56,20 +60,20 @@
}, },
"homepage": "https://yjs.dev", "homepage": "https://yjs.dev",
"dependencies": { "dependencies": {
"lib0": "^0.2.26" "lib0": "^0.2.27"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^11.0.1", "@rollup/plugin-commonjs": "^11.1.0",
"@rollup/plugin-node-resolve": "^7.0.0", "@rollup/plugin-node-resolve": "^7.1.3",
"concurrently": "^3.6.1", "concurrently": "^3.6.1",
"http-server": "^0.12.1", "http-server": "^0.12.3",
"jsdoc": "^3.6.3", "jsdoc": "^3.6.4",
"markdownlint-cli": "^0.19.0", "markdownlint-cli": "^0.23.1",
"rollup": "^1.30.0", "rollup": "^1.32.1",
"rollup-cli": "^1.0.9", "rollup-cli": "^1.0.9",
"standard": "^14.0.0", "standard": "^14.3.4",
"tui-jsdoc-template": "^1.2.2", "tui-jsdoc-template": "^1.2.2",
"typescript": "^3.7.5", "typescript": "^3.9.3",
"y-protocols": "^0.2.3" "y-protocols": "^0.2.3"
} }
} }

View File

@@ -12,11 +12,6 @@ export class AbstractStruct {
* @param {number} length * @param {number} length
*/ */
constructor (id, length) { constructor (id, length) {
/**
* The uniqe identifier of this struct.
* @type {ID}
* @readonly
*/
this.id = id this.id = id
this.length = length this.length = length
this.deleted = false this.deleted = false
@@ -55,15 +50,11 @@ export class AbstractStructRef {
* @param {ID} id * @param {ID} id
*/ */
constructor (id) { constructor (id) {
this.id = id
/** /**
* @type {Array<ID>} * @type {Array<ID>}
*/ */
this._missing = [] this._missing = []
/**
* The uniqe identifier of this type.
* @type {ID}
*/
this.id = id
} }
/** /**

View File

@@ -7,7 +7,7 @@ import {
readYXmlFragment, readYXmlFragment,
readYXmlHook, readYXmlHook,
readYXmlText, readYXmlText,
StructStore, Transaction, Item, YEvent, AbstractType // eslint-disable-line ID, StructStore, Transaction, Item, YEvent, AbstractType // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as encoding from 'lib0/encoding.js' // eslint-disable-line import * as encoding from 'lib0/encoding.js' // eslint-disable-line
@@ -115,7 +115,7 @@ export class ContentType {
// We try to merge all deleted items after each transaction, // We try to merge all deleted items after each transaction,
// but we have no knowledge about that this needs to be merged // but we have no knowledge about that this needs to be merged
// since it is not in transaction.ds. Hence we add it to transaction._mergeStructs // since it is not in transaction.ds. Hence we add it to transaction._mergeStructs
transaction._mergeStructs.add(item.id) transaction._mergeStructs.push(item)
} }
item = item.right item = item.right
} }
@@ -124,7 +124,7 @@ export class ContentType {
item.delete(transaction) item.delete(transaction)
} else { } else {
// same as above // same as above
transaction._mergeStructs.add(item.id) transaction._mergeStructs.push(item)
} }
}) })
transaction.changed.delete(this.type) transaction.changed.delete(this.type)

View File

@@ -2,7 +2,6 @@
import { import {
AbstractStructRef, AbstractStructRef,
AbstractStruct, AbstractStruct,
createID,
addStruct, addStruct,
StructStore, Transaction, ID // eslint-disable-line StructStore, Transaction, ID // eslint-disable-line
} from '../internals.js' } from '../internals.js'
@@ -78,8 +77,7 @@ export class GCRef extends AbstractStructRef {
*/ */
toStruct (transaction, store, offset) { toStruct (transaction, store, offset) {
if (offset > 0) { if (offset > 0) {
// @ts-ignore this.id.clock += offset
this.id = createID(this.id.client, this.id.clock + offset)
this.length -= offset this.length -= offset
} }
return new GC( return new GC(

View File

@@ -1,10 +1,9 @@
import { import {
readID, readID,
createID,
writeID, writeID,
GC, GC,
nextID, getState,
AbstractStructRef, AbstractStructRef,
AbstractStruct, AbstractStruct,
replaceStruct, replaceStruct,
@@ -21,6 +20,7 @@ import {
readContentAny, readContentAny,
readContentString, readContentString,
readContentEmbed, readContentEmbed,
createID,
readContentFormat, readContentFormat,
readContentType, readContentType,
addChangedTypeToTransaction, addChangedTypeToTransaction,
@@ -88,12 +88,12 @@ export const keepItem = (item, keep) => {
* @private * @private
*/ */
export const splitItem = (transaction, leftItem, diff) => { export const splitItem = (transaction, leftItem, diff) => {
const id = leftItem.id
// create rightItem // create rightItem
const { client, clock } = leftItem.id
const rightItem = new Item( const rightItem = new Item(
createID(id.client, id.clock + diff), createID(client, clock + diff),
leftItem, leftItem,
createID(id.client, id.clock + diff - 1), createID(client, clock + diff - 1),
leftItem.right, leftItem.right,
leftItem.rightOrigin, leftItem.rightOrigin,
leftItem.parent, leftItem.parent,
@@ -116,7 +116,7 @@ export const splitItem = (transaction, leftItem, diff) => {
rightItem.right.left = rightItem rightItem.right.left = rightItem
} }
// right is more specific. // right is more specific.
transaction._mergeStructs.add(rightItem.id) transaction._mergeStructs.push(rightItem)
// update parent._map // update parent._map
if (rightItem.parentSub !== null && rightItem.right === null) { if (rightItem.parentSub !== null && rightItem.right === null) {
rightItem.parent._map.set(rightItem.parentSub, rightItem) rightItem.parent._map.set(rightItem.parentSub, rightItem)
@@ -137,8 +137,12 @@ export const splitItem = (transaction, leftItem, diff) => {
* @private * @private
*/ */
export const redoItem = (transaction, item, redoitems) => { export const redoItem = (transaction, item, redoitems) => {
if (item.redone !== null) { const doc = transaction.doc
return getItemCleanStart(transaction, item.redone) const store = doc.store
const ownClientID = doc.clientID
const redone = item.redone
if (redone !== null) {
return getItemCleanStart(transaction, redone)
} }
let parentItem = item.parent._item let parentItem = item.parent._item
/** /**
@@ -158,7 +162,7 @@ export const redoItem = (transaction, item, redoitems) => {
left = item left = item
while (left.right !== null) { while (left.right !== null) {
left = left.right left = left.right
if (left.id.client !== transaction.doc.clientID) { if (left.id.client !== ownClientID) {
// It is not possible to redo this item because it conflicts with a // It is not possible to redo this item because it conflicts with a
// change from another client // change from another client
return null return null
@@ -212,15 +216,17 @@ export const redoItem = (transaction, item, redoitems) => {
right = right.right right = right.right
} }
} }
const nextClock = getState(store, ownClientID)
const nextId = createID(ownClientID, nextClock)
const redoneItem = new Item( const redoneItem = new Item(
nextID(transaction), nextId,
left, left === null ? null : left.lastId, left, left && left.lastId,
right, right === null ? null : right.id, right, right && right.id,
parentItem === null ? item.parent : /** @type {ContentType} */ (parentItem.content).type, parentItem === null ? item.parent : /** @type {ContentType} */ (parentItem.content).type,
item.parentSub, item.parentSub,
item.content.copy() item.content.copy()
) )
item.redone = redoneItem.id item.redone = nextId
keepItem(redoneItem, true) keepItem(redoneItem, true)
redoneItem.integrate(transaction) redoneItem.integrate(transaction)
return redoneItem return redoneItem
@@ -294,30 +300,35 @@ export class Item extends AbstractStruct {
* @type {AbstractContent} * @type {AbstractContent}
*/ */
this.content = content this.content = content
this.length = content.getLength()
this.countable = content.isCountable()
/** /**
* If true, do not garbage collect this Item. * If true, do not garbage collect this Item.
*/ */
this.keep = false this.keep = false
} }
get countable () {
return this.content.isCountable()
}
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
*/ */
integrate (transaction) { integrate (transaction) {
const store = transaction.doc.store const store = transaction.doc.store
const id = this.id
const parent = this.parent const parent = this.parent
const parentSub = this.parentSub const parentSub = this.parentSub
const length = this.length const length = this.length
/**
* @type {Item|null}
*/
let left = this.left
/** /**
* @type {Item|null} * @type {Item|null}
*/ */
let o let o
// set o to the first conflicting item // set o to the first conflicting item
if (this.left !== null) { if (left !== null) {
o = this.left.right o = left.right
} else if (parentSub !== null) { } else if (parentSub !== null) {
o = parent._map.get(parentSub) || null o = parent._map.get(parentSub) || null
while (o !== null && o.left !== null) { while (o !== null && o.left !== null) {
@@ -343,14 +354,14 @@ export class Item extends AbstractStruct {
conflictingItems.add(o) conflictingItems.add(o)
if (compareIDs(this.origin, o.origin)) { if (compareIDs(this.origin, o.origin)) {
// case 1 // case 1
if (o.id.client < id.client) { if (o.id.client < this.id.client) {
this.left = o left = o
conflictingItems.clear() conflictingItems.clear()
} }
} else if (o.origin !== null && itemsBeforeOrigin.has(getItem(store, o.origin))) { } else if (o.origin !== null && itemsBeforeOrigin.has(getItem(store, o.origin))) {
// case 2 // case 2
if (o.origin === null || !conflictingItems.has(getItem(store, o.origin))) { if (o.origin === null || !conflictingItems.has(getItem(store, o.origin))) {
this.left = o left = o
conflictingItems.clear() conflictingItems.clear()
} }
} else { } else {
@@ -358,11 +369,12 @@ export class Item extends AbstractStruct {
} }
o = o.right o = o.right
} }
this.left = left
// reconnect left/right + update parent map/start if necessary // reconnect left/right + update parent map/start if necessary
if (this.left !== null) { if (left !== null) {
const right = this.left.right const right = left.right
this.right = right this.right = right
this.left.right = this left.right = this
} else { } else {
let r let r
if (parentSub !== null) { if (parentSub !== null) {
@@ -381,9 +393,9 @@ export class Item extends AbstractStruct {
} else if (parentSub !== null) { } else if (parentSub !== null) {
// set as current parent value if right === null and this is parentSub // set as current parent value if right === null and this is parentSub
parent._map.set(parentSub, this) parent._map.set(parentSub, this)
if (this.left !== null) { if (left !== null) {
// this is the current attribute value of parent. delete right // this is the current attribute value of parent. delete right
this.left.delete(transaction) left.delete(transaction)
} }
} }
// adjust length of parent // adjust length of parent
@@ -522,7 +534,8 @@ export class Item extends AbstractStruct {
} }
if (origin === null && rightOrigin === null) { if (origin === null && rightOrigin === null) {
const parent = this.parent const parent = this.parent
if (parent._item === null) { const parentItem = parent._item
if (parentItem === null) {
// parent type on y._map // parent type on y._map
// find the correct key // find the correct key
const ykey = findRootTypeKey(parent) const ykey = findRootTypeKey(parent)
@@ -530,7 +543,7 @@ export class Item extends AbstractStruct {
encoding.writeVarString(encoder, ykey) encoding.writeVarString(encoder, ykey)
} else { } else {
encoding.writeVarUint(encoder, 0) // write parent id encoding.writeVarUint(encoder, 0) // write parent id
writeID(encoder, parent._item.id) writeID(encoder, parentItem.id)
} }
if (parentSub !== null) { if (parentSub !== null) {
encoding.writeVarString(encoder, parentSub) encoding.writeVarString(encoder, parentSub)
@@ -723,22 +736,18 @@ export class ItemRef extends AbstractStructRef {
*/ */
toStruct (transaction, store, offset) { toStruct (transaction, store, offset) {
if (offset > 0) { if (offset > 0) {
/** this.id.clock += offset
* @type {ID}
*/
const id = this.id
this.id = createID(id.client, id.clock + offset)
this.left = createID(this.id.client, this.id.clock - 1) this.left = createID(this.id.client, this.id.clock - 1)
this.content = this.content.splice(offset) this.content = this.content.splice(offset)
this.length -= offset
} }
const left = this.left === null ? null : getItemCleanEnd(transaction, store, this.left) const left = this.left === null ? null : getItemCleanEnd(transaction, store, this.left)
const right = this.right === null ? null : getItemCleanStart(transaction, this.right) const right = this.right === null ? null : getItemCleanStart(transaction, this.right)
const parentId = this.parent
let parent = null let parent = null
let parentSub = this.parentSub let parentSub = this.parentSub
if (this.parent !== null) { if (parentId !== null) {
const parentItem = getItem(store, this.parent) const parentItem = getItem(store, parentId)
// Edge case: toStruct is called with an offset > 0. In this case left is defined. // 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 // 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 don't have // deleted. This is why we check if left is GC'd. Strictly we don't have
@@ -767,9 +776,9 @@ export class ItemRef extends AbstractStructRef {
: new Item( : new Item(
this.id, this.id,
left, left,
this.left, left && left.lastId,
right, right,
this.right, right && right.id,
parent, parent,
parentSub, parentSub,
this.content this.content

View File

@@ -4,14 +4,14 @@ import {
callEventHandlerListeners, callEventHandlerListeners,
addEventHandlerListener, addEventHandlerListener,
createEventHandler, createEventHandler,
nextID, getState,
isVisible, isVisible,
ContentType, ContentType,
createID,
ContentAny, ContentAny,
ContentBinary, ContentBinary,
createID,
getItemCleanStart, getItemCleanStart,
Doc, Snapshot, Transaction, EventHandler, YEvent, Item, // eslint-disable-line ID, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as map from 'lib0/map.js' import * as map from 'lib0/map.js'
@@ -375,6 +375,9 @@ export const typeListGet = (type, index) => {
*/ */
export const typeListInsertGenericsAfter = (transaction, parent, referenceItem, content) => { export const typeListInsertGenericsAfter = (transaction, parent, referenceItem, content) => {
let left = referenceItem let left = referenceItem
const doc = transaction.doc
const ownClientId = doc.clientID
const store = doc.store
const right = referenceItem === null ? parent._start : referenceItem.right const right = referenceItem === null ? parent._start : referenceItem.right
/** /**
* @type {Array<Object|Array<any>|number>} * @type {Array<Object|Array<any>|number>}
@@ -382,7 +385,7 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
let jsonContent = [] let jsonContent = []
const packJsonContent = () => { const packJsonContent = () => {
if (jsonContent.length > 0) { if (jsonContent.length > 0) {
left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentAny(jsonContent)) left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentAny(jsonContent))
left.integrate(transaction) left.integrate(transaction)
jsonContent = [] jsonContent = []
} }
@@ -401,12 +404,12 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
switch (c.constructor) { switch (c.constructor) {
case Uint8Array: case Uint8Array:
case ArrayBuffer: case ArrayBuffer:
left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentBinary(new Uint8Array(/** @type {Uint8Array} */ (c)))) left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentBinary(new Uint8Array(/** @type {Uint8Array} */ (c))))
left.integrate(transaction) left.integrate(transaction)
break break
default: default:
if (c instanceof AbstractType) { if (c instanceof AbstractType) {
left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentType(c)) left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentType(c))
left.integrate(transaction) left.integrate(transaction)
} else { } else {
throw new Error('Unexpected content type in insert operation') throw new Error('Unexpected content type in insert operation')
@@ -509,6 +512,8 @@ export const typeMapDelete = (transaction, parent, key) => {
*/ */
export const typeMapSet = (transaction, parent, key, value) => { export const typeMapSet = (transaction, parent, key, value) => {
const left = parent._map.get(key) || null const left = parent._map.get(key) || null
const doc = transaction.doc
const ownClientId = doc.clientID
let content let content
if (value == null) { if (value == null) {
content = new ContentAny([value]) content = new ContentAny([value])
@@ -532,7 +537,7 @@ export const typeMapSet = (transaction, parent, key, value) => {
} }
} }
} }
new Item(nextID(transaction), left, left === null ? null : left.lastId, null, null, parent, key, content).integrate(transaction) new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, null, null, parent, key, content).integrate(transaction)
} }
/** /**

View File

@@ -40,7 +40,7 @@ export class YArrayEvent extends YEvent {
* A shared Array implementation. * A shared Array implementation.
* @template T * @template T
* @extends AbstractType<YArrayEvent<T>> * @extends AbstractType<YArrayEvent<T>>
* @implements {IterableIterator<T>} * @implements {Iterable<T>}
*/ */
export class YArray extends AbstractType { export class YArray extends AbstractType {
constructor () { constructor () {
@@ -121,6 +121,15 @@ export class YArray extends AbstractType {
this.insert(this.length, content) this.insert(this.length, content)
} }
/**
* Preppends content to this YArray.
*
* @param {Array<T>} content Array of content to preppend.
*/
unshift (content) {
this.insert(0, content)
}
/** /**
* Deletes elements starting from an index. * Deletes elements starting from an index.
* *

View File

@@ -42,7 +42,7 @@ export class YMapEvent extends YEvent {
* A shared Map implementation. * A shared Map implementation.
* *
* @extends AbstractType<YMapEvent<T>> * @extends AbstractType<YMapEvent<T>>
* @implements {IterableIterator} * @implements {Iterable<T>}
*/ */
export class YMap extends AbstractType { export class YMap extends AbstractType {
constructor () { constructor () {

View File

@@ -6,10 +6,10 @@
import { import {
YEvent, YEvent,
AbstractType, AbstractType,
nextID,
createID,
getItemCleanStart, getItemCleanStart,
getState,
isVisible, isVisible,
createID,
YTextRefID, YTextRefID,
callTypeObservers, callTypeObservers,
transact, transact,
@@ -150,8 +150,10 @@ const insertNegatedAttributes = (transaction, parent, left, right, negatedAttrib
left = right left = right
right = right.right right = right.right
} }
const doc = transaction.doc
const ownClientId = doc.clientID
for (const [key, val] of negatedAttributes) { for (const [key, val] of negatedAttributes) {
left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentFormat(key, val)) left = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
left.integrate(transaction) left.integrate(transaction)
} }
return { left, right } return { left, right }
@@ -215,6 +217,8 @@ const minimizeAttributeChanges = (left, right, currentAttributes, attributes) =>
* @function * @function
**/ **/
const insertAttributes = (transaction, parent, left, right, currentAttributes, attributes) => { const insertAttributes = (transaction, parent, left, right, currentAttributes, attributes) => {
const doc = transaction.doc
const ownClientId = doc.clientID
const negatedAttributes = new Map() const negatedAttributes = new Map()
// insert format-start items // insert format-start items
for (const key in attributes) { for (const key in attributes) {
@@ -223,7 +227,7 @@ const insertAttributes = (transaction, parent, left, right, currentAttributes, a
if (!equalAttrs(currentVal, val)) { if (!equalAttrs(currentVal, val)) {
// save negated attribute (set null if currentVal undefined) // save negated attribute (set null if currentVal undefined)
negatedAttributes.set(key, currentVal) negatedAttributes.set(key, currentVal)
left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentFormat(key, val)) left = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
left.integrate(transaction) left.integrate(transaction)
} }
} }
@@ -249,13 +253,15 @@ const insertText = (transaction, parent, left, right, currentAttributes, text, a
attributes[key] = null attributes[key] = null
} }
} }
const doc = transaction.doc
const ownClientId = doc.clientID
const minPos = minimizeAttributeChanges(left, right, currentAttributes, attributes) const minPos = minimizeAttributeChanges(left, right, currentAttributes, attributes)
const insertPos = insertAttributes(transaction, parent, minPos.left, minPos.right, currentAttributes, attributes) const insertPos = insertAttributes(transaction, parent, minPos.left, minPos.right, currentAttributes, attributes)
left = insertPos.left left = insertPos.left
right = insertPos.right right = insertPos.right
// insert content // insert content
const content = text.constructor === String ? new ContentString(/** @type {string} */ (text)) : new ContentEmbed(text) const content = text.constructor === String ? new ContentString(/** @type {string} */ (text)) : new ContentEmbed(text)
left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, content) left = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, content)
left.integrate(transaction) left.integrate(transaction)
return insertNegatedAttributes(transaction, parent, left, insertPos.right, insertPos.negatedAttributes) return insertNegatedAttributes(transaction, parent, left, insertPos.right, insertPos.negatedAttributes)
} }
@@ -274,6 +280,8 @@ const insertText = (transaction, parent, left, right, currentAttributes, text, a
* @function * @function
*/ */
const formatText = (transaction, parent, left, right, currentAttributes, length, attributes) => { const formatText = (transaction, parent, left, right, currentAttributes, length, attributes) => {
const doc = transaction.doc
const ownClientId = doc.clientID
const minPos = minimizeAttributeChanges(left, right, currentAttributes, attributes) const minPos = minimizeAttributeChanges(left, right, currentAttributes, attributes)
const insertPos = insertAttributes(transaction, parent, minPos.left, minPos.right, currentAttributes, attributes) const insertPos = insertAttributes(transaction, parent, minPos.left, minPos.right, currentAttributes, attributes)
const negatedAttributes = insertPos.negatedAttributes const negatedAttributes = insertPos.negatedAttributes
@@ -318,7 +326,7 @@ const formatText = (transaction, parent, left, right, currentAttributes, length,
for (; length > 0; length--) { for (; length > 0; length--) {
newlines += '\n' newlines += '\n'
} }
left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentString(newlines)) left = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentString(newlines))
left.integrate(transaction) left.integrate(transaction)
} }
return insertNegatedAttributes(transaction, parent, left, right, negatedAttributes) return insertNegatedAttributes(transaction, parent, left, right, negatedAttributes)

View File

@@ -48,7 +48,7 @@ import * as decoding from 'lib0/decoding.js' // eslint-disable-line
* Can be created with {@link YXmlFragment#createTreeWalker} * Can be created with {@link YXmlFragment#createTreeWalker}
* *
* @public * @public
* @implements {IterableIterator} * @implements {Iterable<YXmlElement|YXmlText|YXmlElement|YXmlHook>}
*/ */
export class YXmlTreeWalker { export class YXmlTreeWalker {
/** /**
@@ -81,10 +81,10 @@ export class YXmlTreeWalker {
* @type {Item|null} * @type {Item|null}
*/ */
let n = this._currentNode let n = this._currentNode
let type = /** @type {ContentType} */ (n.content).type let type = /** @type {any} */ (n.content).type
if (n !== null && (!this._firstCall || n.deleted || !this._filter(type))) { // if first call, we check if we can use the first item if (n !== null && (!this._firstCall || n.deleted || !this._filter(type))) { // if first call, we check if we can use the first item
do { do {
type = /** @type {ContentType} */ (n.content).type type = /** @type {any} */ (n.content).type
if (!n.deleted && (type.constructor === YXmlElement || type.constructor === YXmlFragment) && type._start !== null) { if (!n.deleted && (type.constructor === YXmlElement || type.constructor === YXmlFragment) && type._start !== null) {
// walk down in the tree // walk down in the tree
n = type._start n = type._start

View File

@@ -1,11 +1,11 @@
import { import {
findIndexSS, findIndexSS,
createID,
getState, getState,
splitItem, splitItem,
createID,
iterateStructs, iterateStructs,
Item, GC, StructStore, Transaction, ID // eslint-disable-line Item, AbstractStruct, GC, StructStore, Transaction, ID // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as array from 'lib0/array.js' import * as array from 'lib0/array.js'

View File

@@ -1,12 +1,12 @@
import { import {
createID,
writeID, writeID,
readID, readID,
compareIDs, compareIDs,
getState, getState,
findRootTypeKey, findRootTypeKey,
Item, Item,
createID,
ContentType, ContentType,
followRedone, followRedone,
ID, Doc, AbstractType // eslint-disable-line ID, Doc, AbstractType // eslint-disable-line
@@ -107,7 +107,7 @@ export const createRelativePosition = (type, item) => {
if (type._item === null) { if (type._item === null) {
tname = findRootTypeKey(type) tname = findRootTypeKey(type)
} else { } else {
typeid = type._item.id typeid = createID(type._item.id.client, type._item.id.clock)
} }
return new RelativePosition(typeid, tname, item) return new RelativePosition(typeid, tname, item)
} }

View File

@@ -4,13 +4,13 @@ import {
createDeleteSetFromStructStore, createDeleteSetFromStructStore,
getStateVector, getStateVector,
getItemCleanStart, getItemCleanStart,
createID,
iterateDeletedStructs, iterateDeletedStructs,
writeDeleteSet, writeDeleteSet,
writeStateVector, writeStateVector,
readDeleteSet, readDeleteSet,
readStateVector, readStateVector,
createDeleteSet, createDeleteSet,
createID,
getState, getState,
Transaction, Doc, DeleteSet, Item // eslint-disable-line Transaction, Doc, DeleteSet, Item // eslint-disable-line
} from '../internals.js' } from '../internals.js'

View File

@@ -2,7 +2,7 @@
import { import {
GC, GC,
splitItem, splitItem,
GCRef, ItemRef, Transaction, ID, Item // eslint-disable-line AbstractStruct, GCRef, ItemRef, Transaction, ID, Item // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as math from 'lib0/math.js' import * as math from 'lib0/math.js'
@@ -114,7 +114,7 @@ export const addStruct = (store, struct) => {
/** /**
* Perform a binary search on a sorted array * Perform a binary search on a sorted array
* @param {Array<any>} structs * @param {Array<Item|GC>} structs
* @param {number} clock * @param {number} clock
* @return {number} * @return {number}
* *
@@ -163,16 +163,10 @@ export const find = (store, id) => {
/** /**
* Expects that id is actually in store. This function throws or is an infinite loop otherwise. * Expects that id is actually in store. This function throws or is an infinite loop otherwise.
*
* @param {StructStore} store
* @param {ID} id
* @return {Item}
*
* @private * @private
* @function * @function
*/ */
// @ts-ignore export const getItem = /** @type {function(StructStore,ID):Item} */ (find)
export const getItem = (store, id) => find(store, id)
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction

View File

@@ -1,7 +1,6 @@
import { import {
getState, getState,
createID,
writeStructsFromTransaction, writeStructsFromTransaction,
writeDeleteSet, writeDeleteSet,
DeleteSet, DeleteSet,
@@ -11,7 +10,8 @@ import {
callEventHandlerListeners, callEventHandlerListeners,
Item, Item,
generateNewClientId, generateNewClientId,
StructStore, ID, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line createID,
GC, StructStore, ID, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding.js'
@@ -86,9 +86,9 @@ export class Transaction {
*/ */
this.changedParentTypes = new Map() this.changedParentTypes = new Map()
/** /**
* @type {Set<ID>} * @type {Array<AbstractStruct>}
*/ */
this._mergeStructs = new Set() this._mergeStructs = []
/** /**
* @type {any} * @type {any}
*/ */
@@ -170,7 +170,7 @@ const tryToMergeWithLeft = (structs, pos) => {
*/ */
const tryGcDeleteSet = (ds, store, gcFilter) => { const tryGcDeleteSet = (ds, store, gcFilter) => {
for (const [client, deleteItems] of ds.clients) { for (const [client, deleteItems] of ds.clients) {
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client)) const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
for (let di = deleteItems.length - 1; di >= 0; di--) { for (let di = deleteItems.length - 1; di >= 0; di--) {
const deleteItem = deleteItems[di] const deleteItem = deleteItems[di]
const endDeleteItemClock = deleteItem.clock + deleteItem.len const endDeleteItemClock = deleteItem.clock + deleteItem.len
@@ -199,7 +199,7 @@ const tryMergeDeleteSet = (ds, store) => {
// try to merge deleted / gc'd items // try to merge deleted / gc'd items
// merge from right to left for better efficiecy and so we don't miss any merge targets // merge from right to left for better efficiecy and so we don't miss any merge targets
for (const [client, deleteItems] of ds.clients) { for (const [client, deleteItems] of ds.clients) {
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client)) const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
for (let di = deleteItems.length - 1; di >= 0; di--) { for (let di = deleteItems.length - 1; di >= 0; di--) {
const deleteItem = deleteItems[di] const deleteItem = deleteItems[di]
// start with merging the item next to the last deleted item // start with merging the item next to the last deleted item
@@ -235,6 +235,7 @@ const cleanupTransactions = (transactionCleanups, i) => {
const doc = transaction.doc const doc = transaction.doc
const store = doc.store const store = doc.store
const ds = transaction.deleteSet const ds = transaction.deleteSet
const mergeStructs = transaction._mergeStructs
try { try {
sortAndMergeDeleteSet(ds) sortAndMergeDeleteSet(ds)
transaction.afterState = getStateVector(transaction.doc.store) transaction.afterState = getStateVector(transaction.doc.store)
@@ -292,7 +293,7 @@ const cleanupTransactions = (transactionCleanups, i) => {
for (const [client, clock] of transaction.afterState) { for (const [client, clock] of transaction.afterState) {
const beforeClock = transaction.beforeState.get(client) || 0 const beforeClock = transaction.beforeState.get(client) || 0
if (beforeClock !== clock) { if (beforeClock !== clock) {
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client)) const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
// we iterate from right to left so we can safely remove entries // we iterate from right to left so we can safely remove entries
const firstChangePos = math.max(findIndexSS(structs, beforeClock), 1) const firstChangePos = math.max(findIndexSS(structs, beforeClock), 1)
for (let i = structs.length - 1; i >= firstChangePos; i--) { for (let i = structs.length - 1; i >= firstChangePos; i--) {
@@ -303,10 +304,9 @@ const cleanupTransactions = (transactionCleanups, i) => {
// try to merge mergeStructs // try to merge mergeStructs
// @todo: it makes more sense to transform mergeStructs to a DS, sort it, and merge from right to left // @todo: it makes more sense to transform mergeStructs to a DS, sort it, and merge from right to left
// but at the moment DS does not handle duplicates // but at the moment DS does not handle duplicates
for (const mid of transaction._mergeStructs) { for (let i = 0; i < mergeStructs.length; i++) {
const client = mid.client const { client, clock } = mergeStructs[i].id
const clock = mid.clock const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
const replacedStructPos = findIndexSS(structs, clock) const replacedStructPos = findIndexSS(structs, clock)
if (replacedStructPos + 1 < structs.length) { if (replacedStructPos + 1 < structs.length) {
tryToMergeWithLeft(structs, replacedStructPos + 1) tryToMergeWithLeft(structs, replacedStructPos + 1)

View File

@@ -3,14 +3,14 @@ import {
iterateDeletedStructs, iterateDeletedStructs,
keepItem, keepItem,
transact, transact,
createID,
redoItem, redoItem,
iterateStructs, iterateStructs,
isParentOf, isParentOf,
createID,
followRedone, followRedone,
getItemCleanStart, getItemCleanStart,
getState, getState,
Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line ID, Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as time from 'lib0/time.js' import * as time from 'lib0/time.js'

View File

@@ -19,15 +19,15 @@ import {
GCRef, GCRef,
ItemRef, ItemRef,
writeID, writeID,
createID,
readID, readID,
getState, getState,
createID,
getStateVector, getStateVector,
readAndApplyDeleteSet, readAndApplyDeleteSet,
writeDeleteSet, writeDeleteSet,
createDeleteSetFromStructStore, createDeleteSetFromStructStore,
transact, transact,
Doc, Transaction, AbstractStruct, StructStore, ID // eslint-disable-line Doc, Transaction, GC, Item, StructStore, ID // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding.js'
@@ -36,7 +36,7 @@ import * as binary from 'lib0/binary.js'
/** /**
* @param {encoding.Encoder} encoder * @param {encoding.Encoder} encoder
* @param {Array<AbstractStruct>} structs All structs by `client` * @param {Array<GC|Item>} structs All structs by `client`
* @param {number} client * @param {number} client
* @param {number} clock write structs starting with `ID(client,clock)` * @param {number} clock write structs starting with `ID(client,clock)`
* *
@@ -50,35 +50,12 @@ const writeStructs = (encoder, structs, client, clock) => {
writeID(encoder, createID(client, clock)) writeID(encoder, createID(client, clock))
const firstStruct = structs[startNewStructs] const firstStruct = structs[startNewStructs]
// write first struct with an offset // write first struct with an offset
firstStruct.write(encoder, clock - firstStruct.id.clock, 0) firstStruct.write(encoder, clock - firstStruct.id.clock)
for (let i = startNewStructs + 1; i < structs.length; i++) { for (let i = startNewStructs + 1; i < structs.length; i++) {
structs[i].write(encoder, 0, 0) structs[i].write(encoder, 0)
} }
} }
/**
* @param {decoding.Decoder} decoder
* @param {number} numOfStructs
* @param {ID} nextID
* @return {Array<GCRef|ItemRef>}
*
* @private
* @function
*/
const readStructRefs = (decoder, numOfStructs, nextID) => {
/**
* @type {Array<GCRef|ItemRef>}
*/
const refs = []
for (let i = 0; i < numOfStructs; i++) {
const info = decoding.readUint8(decoder)
const ref = (binary.BITS5 & info) === 0 ? new GCRef(decoder, nextID, info) : new ItemRef(decoder, nextID, info)
nextID = createID(nextID.client, nextID.clock + ref.length)
refs.push(ref)
}
return refs
}
/** /**
* @param {encoding.Encoder} encoder * @param {encoding.Encoder} encoder
* @param {StructStore} store * @param {StructStore} store
@@ -111,22 +88,30 @@ export const writeClientsStructs = (encoder, store, _sm) => {
/** /**
* @param {decoding.Decoder} decoder The decoder object to read data from. * @param {decoding.Decoder} decoder The decoder object to read data from.
* @param {Map<number,Array<GCRef|ItemRef>>} clientRefs
* @return {Map<number,Array<GCRef|ItemRef>>} * @return {Map<number,Array<GCRef|ItemRef>>}
* *
* @private * @private
* @function * @function
*/ */
export const readClientsStructRefs = decoder => { export const readClientsStructRefs = (decoder, clientRefs) => {
/**
* @type {Map<number,Array<GCRef|ItemRef>>}
*/
const clientRefs = new Map()
const numOfStateUpdates = decoding.readVarUint(decoder) const numOfStateUpdates = decoding.readVarUint(decoder)
for (let i = 0; i < numOfStateUpdates; i++) { for (let i = 0; i < numOfStateUpdates; i++) {
const numberOfStructs = decoding.readVarUint(decoder) const numberOfStructs = decoding.readVarUint(decoder)
const nextID = readID(decoder) const nextID = readID(decoder)
const refs = readStructRefs(decoder, numberOfStructs, nextID) const nextIdClient = nextID.client
clientRefs.set(nextID.client, refs) let nextIdClock = nextID.clock
/**
* @type {Array<GCRef|ItemRef>}
*/
const refs = []
clientRefs.set(nextIdClient, refs)
for (let i = 0; i < numberOfStructs; i++) {
const info = decoding.readUint8(decoder)
const ref = (binary.BITS5 & info) === 0 ? new GCRef(decoder, createID(nextIdClient, nextIdClock), info) : new ItemRef(decoder, createID(nextIdClient, nextIdClock), info)
refs.push(ref)
nextIdClock += ref.length
}
} }
return clientRefs return clientRefs
} }
@@ -171,16 +156,18 @@ const resumeStructIntegration = (transaction, store) => {
} }
const ref = stack[stack.length - 1] const ref = stack[stack.length - 1]
const m = ref._missing const m = ref._missing
const client = ref.id.client const refID = ref.id
const client = refID.client
const refClock = refID.clock
const localClock = getState(store, client) const localClock = getState(store, client)
const offset = ref.id.clock < localClock ? localClock - ref.id.clock : 0 const offset = refClock < localClock ? localClock - refClock : 0
if (ref.id.clock + offset !== localClock) { if (refClock + offset !== localClock) {
// A previous message from this client is missing // A previous message from this client is missing
// check if there is a pending structRef with a smaller clock and switch them // check if there is a pending structRef with a smaller clock and switch them
const structRefs = clientsStructRefs.get(client) const structRefs = clientsStructRefs.get(client)
if (structRefs !== undefined) { if (structRefs !== undefined) {
const r = structRefs.refs[structRefs.i] const r = structRefs.refs[structRefs.i]
if (r.id.clock < ref.id.clock) { if (r.id.clock < refClock) {
// put ref with smaller clock on stack instead and continue // put ref with smaller clock on stack instead and continue
structRefs.refs[structRefs.i] = ref structRefs.refs[structRefs.i] = ref
stack[stack.length - 1] = r stack[stack.length - 1] = r
@@ -282,7 +269,8 @@ const mergeReadStructsIntoPendingReads = (store, clientsStructsRefs) => {
* @function * @function
*/ */
export const readStructs = (decoder, transaction, store) => { export const readStructs = (decoder, transaction, store) => {
const clientsStructRefs = readClientsStructRefs(decoder) const clientsStructRefs = new Map()
readClientsStructRefs(decoder, clientsStructRefs)
mergeReadStructsIntoPendingReads(store, clientsStructRefs) mergeReadStructsIntoPendingReads(store, clientsStructRefs)
resumeStructIntegration(transaction, store) resumeStructIntegration(transaction, store)
tryResumePendingDeleteReaders(transaction, store) tryResumePendingDeleteReaders(transaction, store)

View File

@@ -330,6 +330,7 @@ export const compareStructStores = (ss1, ss2) => {
s1.constructor !== s2.constructor || s1.constructor !== s2.constructor ||
!Y.compareIDs(s1.id, s2.id) || !Y.compareIDs(s1.id, s2.id) ||
s1.deleted !== s2.deleted || s1.deleted !== s2.deleted ||
// @ts-ignore
s1.length !== s2.length s1.length !== s2.length
) { ) {
t.fail('Structs dont match') t.fail('Structs dont match')

View File

@@ -205,6 +205,23 @@ export const testFormattingRemovedInMidText = tc => {
t.assert(Y.getTypeChildren(text0).length === 3) t.assert(Y.getTypeChildren(text0).length === 3)
} }
/**
* @param {t.TestCase} tc
*
export const testLargeFragmentedDocument = tc => {
const { text0, text1, testConnector } = init(tc, { users: 2 })
// @ts-ignore
text0.doc.transact(() => {
for (let i = 0; i < 1000000; i++) {
text0.insert(0, '0')
}
})
t.measureTime('time to apply', () => {
testConnector.flushAllMessages()
})
}
*/
// RANDOM TESTS // RANDOM TESTS
let charCounter = 0 let charCounter = 0