Compare commits

..

41 Commits

Author SHA1 Message Date
Kevin Jahns
7fb63de8fc 13.2.0 2020-06-09 01:04:00 +02:00
Kevin Jahns
c4d80d133d Merge branch 'master' of github.com:yjs/yjs 2020-06-09 00:54:59 +02:00
Kevin Jahns
cebe96c001 Merge pull request #209 from relm-us/ymap-size
Add 'size' getter to Y.Map
2020-06-09 00:54:52 +02:00
Kevin Jahns
4d2369ce21 Merge branch 'master' of github.com:yjs/yjs 2020-06-09 00:53:38 +02:00
Kevin Jahns
5293ab4df1 Improve memory usage by omitting the ItemRef step and directly applying the Item 2020-06-09 00:53:05 +02:00
Duane Johnson
e53c01c6c5 Add 'size' getter to Y.Map 2020-06-07 07:44:37 -06:00
Kevin Jahns
03faa27787 Merge pull request #208 from relm-us/ymap-iterable-constructor
Add optional iterable param to Y.Map(), matching Map()
2020-06-07 12:34:08 +02:00
Duane Johnson
868dd5f0a5 Add optional iterable param to Y.Map(), matching Map() 2020-06-06 21:32:24 -06:00
Kevin Jahns
fa58ce53cd Update Sponsors ❤️ 2020-06-07 01:56:16 +02:00
Kevin Jahns
0a0098fdfb reuse item position references in Y.Text 2020-06-05 00:27:36 +02:00
Kevin Jahns
a5a48d07f6 13.1.1 2020-06-04 18:15:58 +02:00
Kevin Jahns
7b16d5c92d implement pivoting in struct search 2020-06-04 18:14:41 +02:00
Kevin Jahns
ee147c14f1 Merge branch 'master' of github.com:yjs/yjs 2020-06-04 17:07:27 +02:00
Kevin Jahns
e86d5ba25b fix ref offset issue 2020-06-04 17:07:17 +02:00
Kevin Jahns
149ca6f636 Merge pull request #205 from Kisama/ytext-newline-option
Add sanitize option
2020-06-03 19:22:29 +02:00
Cole
e4223760b0 - rollback shorter url to original and ignore max length check for specific line
- add opts sanitize for applyDelata in YText
- apply applyDelata document about YText
2020-06-03 11:18:09 +09:00
Cole
9d3dd4e082 Add setter form permit empty paragraph at the end of the content when applyDelta. 2020-06-03 11:15:03 +09:00
Cole
5a4ff33bf4 Merge branch 'master' of github.com:yjs/yjs 2020-06-03 11:12:38 +09:00
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
Cole
469404c6e1 move quill relate newline remove logic to y-quill 2020-06-01 19:17:54 +09: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
25 changed files with 1163 additions and 919 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,44 @@ build collaborative or distributed applications ping us at
<yjs@tag1consulting.com>. Otherwise you can find help on our
[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)
[![Gremloon](https://github.com/gremloon.png?size=60)](https://github.com/gremloon)
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
* [Overview](#Overview)
@@ -137,6 +175,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
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
```js
@@ -158,11 +244,13 @@ necessary.
<b><code>insert(index:number, content:Array&lt;object|boolean|Array|string|number|Uint8Array|Y.Type&gt;)</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
I.e. <code>array.insert(0, [1])</code> splices the list and inserts 1 at
position 0.
</dd>
<b><code>push(Array&lt;Object|boolean|Array|string|number|Uint8Array|Y.Type&gt;)</code></b>
<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>
<dd></dd>
<b><code>get(index:number)</code></b>
@@ -308,8 +396,12 @@ YTextEvents compute changes as deltas.
<dd></dd>
<b><code>format(index:number, length:number, formattingAttributes:Object&lt;string,string&gt;)</code></b>
<dd>Assign formatting attributes to a range in the text</dd>
<b><code>applyDelta(delta)</code></b>
<dd>See <a href="https://quilljs.com/docs/delta/">Quill Delta</a></dd>
<b><code>applyDelta(delta, opts:Object&lt;string,any&gt;)</code></b>
<dd>
See <a href="https://quilljs.com/docs/delta/">Quill Delta</a>
Can set options for preventing remove ending newLines, default is true.
<pre>ytext.applyDelta(delta, { sanitize: false })</pre>
</dd>
<b><code>length:number</code></b>
<dd></dd>
<b><code>toString():string</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",
"version": "13.0.8",
"version": "13.2.0",
"description": "Shared Editing Library",
"main": "./dist/yjs.cjs",
"module": "./dist/yjs.mjs",
"types": "./dist/src/index.d.ts",
"sideEffects": false,
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
},
"scripts": {
"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",
@@ -56,20 +60,20 @@
},
"homepage": "https://yjs.dev",
"dependencies": {
"lib0": "^0.2.26"
"lib0": "^0.2.27"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^11.0.1",
"@rollup/plugin-node-resolve": "^7.0.0",
"@rollup/plugin-commonjs": "^11.1.0",
"@rollup/plugin-node-resolve": "^7.1.3",
"concurrently": "^3.6.1",
"http-server": "^0.12.1",
"jsdoc": "^3.6.3",
"markdownlint-cli": "^0.19.0",
"rollup": "^1.30.0",
"http-server": "^0.12.3",
"jsdoc": "^3.6.4",
"markdownlint-cli": "^0.23.1",
"rollup": "^1.32.1",
"rollup-cli": "^1.0.9",
"standard": "^14.0.0",
"standard": "^14.3.4",
"tui-jsdoc-template": "^1.2.2",
"typescript": "^3.7.5",
"typescript": "^3.9.3",
"y-protocols": "^0.2.3"
}
}

View File

@@ -12,11 +12,6 @@ export class AbstractStruct {
* @param {number} length
*/
constructor (id, length) {
/**
* The uniqe identifier of this struct.
* @type {ID}
* @readonly
*/
this.id = id
this.length = length
this.deleted = false
@@ -44,43 +39,9 @@ export class AbstractStruct {
/**
* @param {Transaction} transaction
*/
integrate (transaction) {
throw error.methodUnimplemented()
}
}
export class AbstractStructRef {
/**
* @param {ID} id
*/
constructor (id) {
/**
* @type {Array<ID>}
*/
this._missing = []
/**
* The uniqe identifier of this type.
* @type {ID}
*/
this.id = id
}
/**
* @param {Transaction} transaction
* @return {Array<ID|null>}
*/
getMissing (transaction) {
return this._missing
}
/**
* @param {Transaction} transaction
* @param {StructStore} store
* @param {number} offset
* @return {AbstractStruct}
*/
toStruct (transaction, store, offset) {
integrate (transaction, offset) {
throw error.methodUnimplemented()
}
}

View File

@@ -7,7 +7,7 @@ import {
readYXmlFragment,
readYXmlHook,
readYXmlText,
StructStore, Transaction, Item, YEvent, AbstractType // eslint-disable-line
ID, StructStore, Transaction, Item, YEvent, AbstractType // eslint-disable-line
} from '../internals.js'
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,
// 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
transaction._mergeStructs.add(item.id)
transaction._mergeStructs.push(item)
}
item = item.right
}
@@ -124,7 +124,7 @@ export class ContentType {
item.delete(transaction)
} else {
// same as above
transaction._mergeStructs.add(item.id)
transaction._mergeStructs.push(item)
}
})
transaction.changed.delete(this.type)

View File

@@ -1,13 +1,10 @@
import {
AbstractStructRef,
AbstractStruct,
createID,
addStruct,
StructStore, Transaction, ID // eslint-disable-line
} from '../internals.js'
import * as decoding from 'lib0/decoding.js'
import * as encoding from 'lib0/encoding.js'
export const structGCRefNumber = 0
@@ -38,8 +35,13 @@ export class GC extends AbstractStruct {
/**
* @param {Transaction} transaction
* @param {number} offset
*/
integrate (transaction) {
integrate (transaction, offset) {
if (offset > 0) {
this.id.clock += offset
this.length -= offset
}
addStruct(transaction.doc.store, this)
}
@@ -51,40 +53,13 @@ export class GC extends AbstractStruct {
encoding.writeUint8(encoder, structGCRefNumber)
encoding.writeVarUint(encoder, this.length - offset)
}
}
/**
* @private
*/
export class GCRef extends AbstractStructRef {
/**
* @param {decoding.Decoder} decoder
* @param {ID} id
* @param {number} info
*/
constructor (decoder, id, info) {
super(id)
/**
* @type {number}
*/
this.length = decoding.readVarUint(decoder)
}
/**
* @param {Transaction} transaction
* @param {StructStore} store
* @param {number} offset
* @return {GC}
* @return {null | ID}
*/
toStruct (transaction, store, offset) {
if (offset > 0) {
// @ts-ignore
this.id = createID(this.id.client, this.id.clock + offset)
this.length -= offset
}
return new GC(
this.id,
this.length
)
getMissing (transaction, store) {
return null
}
}

View File

@@ -1,11 +1,9 @@
import {
readID,
createID,
writeID,
GC,
nextID,
AbstractStructRef,
getState,
AbstractStruct,
replaceStruct,
addStruct,
@@ -21,10 +19,11 @@ import {
readContentAny,
readContentString,
readContentEmbed,
createID,
readContentFormat,
readContentType,
addChangedTypeToTransaction,
ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction // eslint-disable-line
Doc, ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction // eslint-disable-line
} from '../internals.js'
import * as error from 'lib0/error.js'
@@ -73,7 +72,7 @@ export const followRedone = (store, id) => {
export const keepItem = (item, keep) => {
while (item !== null && item.keep !== keep) {
item.keep = keep
item = item.parent._item
item = /** @type {AbstractType<any>} */ (item.parent)._item
}
}
@@ -88,12 +87,12 @@ export const keepItem = (item, keep) => {
* @private
*/
export const splitItem = (transaction, leftItem, diff) => {
const id = leftItem.id
// create rightItem
const { client, clock } = leftItem.id
const rightItem = new Item(
createID(id.client, id.clock + diff),
createID(client, clock + diff),
leftItem,
createID(id.client, id.clock + diff - 1),
createID(client, clock + diff - 1),
leftItem.right,
leftItem.rightOrigin,
leftItem.parent,
@@ -116,10 +115,10 @@ export const splitItem = (transaction, leftItem, diff) => {
rightItem.right.left = rightItem
}
// right is more specific.
transaction._mergeStructs.add(rightItem.id)
transaction._mergeStructs.push(rightItem)
// update parent._map
if (rightItem.parentSub !== null && rightItem.right === null) {
rightItem.parent._map.set(rightItem.parentSub, rightItem)
/** @type {AbstractType<any>} */ (rightItem.parent)._map.set(rightItem.parentSub, rightItem)
}
leftItem.length = diff
return rightItem
@@ -137,10 +136,14 @@ export const splitItem = (transaction, leftItem, diff) => {
* @private
*/
export const redoItem = (transaction, item, redoitems) => {
if (item.redone !== null) {
return getItemCleanStart(transaction, item.redone)
const doc = transaction.doc
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 = /** @type {AbstractType<any>} */ (item.parent)._item
/**
* @type {Item|null}
*/
@@ -158,14 +161,14 @@ export const redoItem = (transaction, item, redoitems) => {
left = item
while (left.right !== null) {
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
// change from another client
return null
}
}
if (left.right !== null) {
left = /** @type {Item} */ (item.parent._map.get(item.parentSub))
left = /** @type {Item} */ (/** @type {AbstractType<any>} */ (item.parent)._map.get(item.parentSub))
}
right = null
}
@@ -187,10 +190,10 @@ export const redoItem = (transaction, item, redoitems) => {
*/
let leftTrace = left
// trace redone until parent matches
while (leftTrace !== null && leftTrace.parent._item !== parentItem) {
while (leftTrace !== null && /** @type {AbstractType<any>} */ (leftTrace.parent)._item !== parentItem) {
leftTrace = leftTrace.redone === null ? null : getItemCleanStart(transaction, leftTrace.redone)
}
if (leftTrace !== null && leftTrace.parent._item === parentItem) {
if (leftTrace !== null && /** @type {AbstractType<any>} */ (leftTrace.parent)._item === parentItem) {
left = leftTrace
break
}
@@ -202,27 +205,29 @@ export const redoItem = (transaction, item, redoitems) => {
*/
let rightTrace = right
// trace redone until parent matches
while (rightTrace !== null && rightTrace.parent._item !== parentItem) {
while (rightTrace !== null && /** @type {AbstractType<any>} */ (rightTrace.parent)._item !== parentItem) {
rightTrace = rightTrace.redone === null ? null : getItemCleanStart(transaction, rightTrace.redone)
}
if (rightTrace !== null && rightTrace.parent._item === parentItem) {
if (rightTrace !== null && /** @type {AbstractType<any>} */ (rightTrace.parent)._item === parentItem) {
right = rightTrace
break
}
right = right.right
}
}
const nextClock = getState(store, ownClientID)
const nextId = createID(ownClientID, nextClock)
const redoneItem = new Item(
nextID(transaction),
left, left === null ? null : left.lastId,
right, right === null ? null : right.id,
nextId,
left, left && left.lastId,
right, right && right.id,
parentItem === null ? item.parent : /** @type {ContentType} */ (parentItem.content).type,
item.parentSub,
item.content.copy()
)
item.redone = redoneItem.id
item.redone = nextId
keepItem(redoneItem, true)
redoneItem.integrate(transaction)
redoneItem.integrate(transaction, 0)
return redoneItem
}
@@ -236,7 +241,7 @@ export class Item extends AbstractStruct {
* @param {ID | null} origin
* @param {Item | null} right
* @param {ID | null} rightOrigin
* @param {AbstractType<any>} parent
* @param {AbstractType<any>|ID|null} parent Is a type if integrated, is null if it is possible to copy parent from left or right, is ID before integration to search for it.
* @param {string | null} parentSub
* @param {AbstractContent} content
*/
@@ -245,7 +250,6 @@ export class Item extends AbstractStruct {
/**
* The item that was originally to the left of this item.
* @type {ID | null}
* @readonly
*/
this.origin = origin
/**
@@ -260,14 +264,11 @@ export class Item extends AbstractStruct {
this.right = right
/**
* The item that was originally to the right of this item.
* @readonly
* @type {ID | null}
*/
this.rightOrigin = rightOrigin
/**
* The parent type.
* @type {AbstractType<any>}
* @readonly
* @type {AbstractType<any>|ID|null}
*/
this.parent = parent
/**
@@ -276,7 +277,6 @@ export class Item extends AbstractStruct {
* to insert this item. If `parentSub = null` type._start is the list in
* which to insert to. Otherwise it is `parent._map`.
* @type {String | null}
* @readonly
*/
this.parentSub = parentSub
/**
@@ -294,109 +294,189 @@ export class Item extends AbstractStruct {
* @type {AbstractContent}
*/
this.content = content
this.length = content.getLength()
this.countable = content.isCountable()
/**
* If true, do not garbage collect this Item.
*/
this.keep = false
}
get countable () {
return this.content.isCountable()
}
/**
* Return missing ids, or define missing items and return null.
*
* @param {Transaction} transaction
* @param {StructStore} store
* @return {null | ID}
*/
getMissing (transaction, store) {
const origin = this.origin
const rightOrigin = this.rightOrigin
const parent = /** @type {ID} */ (this.parent)
if (origin && origin.clock >= getState(store, origin.client)) {
return this.origin
}
if (rightOrigin && rightOrigin.clock >= getState(store, rightOrigin.client)) {
return this.rightOrigin
}
if (parent && parent.constructor === ID && parent.clock >= getState(store, parent.client)) {
return parent
}
// We have all missing ids, now find the items
if (origin) {
this.left = getItemCleanEnd(transaction, store, origin)
this.origin = this.left.lastId
}
if (rightOrigin) {
this.right = getItemCleanStart(transaction, rightOrigin)
this.rightOrigin = this.right.id
}
if (parent && parent.constructor === ID) {
if (parent.clock < getState(store, parent.client)) {
const parentItem = getItem(store, parent)
if (parentItem.constructor === GC) {
this.parent = null
} else {
this.parent = /** @type {ContentType} */ (parentItem.content).type
}
} else {
return parent
}
}
// only set item if this shouldn't be garbage collected
if (!this.parent) {
if (this.left && this.left.constructor === Item) {
this.parent = this.left.parent
this.parentSub = this.left.parentSub
}
if (this.right && this.right.constructor === Item) {
this.parent = this.right.parent
this.parentSub = this.right.parentSub
}
}
return null
}
/**
* @param {Transaction} transaction
* @param {number} offset
*/
integrate (transaction) {
integrate (transaction, offset) {
const store = transaction.doc.store
const id = this.id
const parent = this.parent
if (offset > 0) {
this.id.clock += offset
this.left = getItemCleanEnd(transaction, store, createID(this.id.client, this.id.clock - 1))
this.origin = this.left.lastId
this.content = this.content.splice(offset)
this.length -= offset
}
const parentSub = this.parentSub
const length = this.length
/**
* @type {Item|null}
*/
let o
// set o to the first conflicting item
if (this.left !== null) {
o = this.left.right
} else if (parentSub !== null) {
o = parent._map.get(parentSub) || null
while (o !== null && o.left !== null) {
o = o.left
}
} else {
o = parent._start
}
// TODO: use something like DeleteSet here (a tree implementation would be best)
/**
* @type {Set<Item>}
*/
const conflictingItems = new Set()
/**
* @type {Set<Item>}
*/
const itemsBeforeOrigin = new Set()
// Let c in conflictingItems, b in itemsBeforeOrigin
// ***{origin}bbbb{this}{c,b}{c,b}{o}***
// Note that conflictingItems is a subset of itemsBeforeOrigin
while (o !== null && o !== this.right) {
itemsBeforeOrigin.add(o)
conflictingItems.add(o)
if (compareIDs(this.origin, o.origin)) {
// case 1
if (o.id.client < id.client) {
this.left = o
conflictingItems.clear()
}
} else if (o.origin !== null && itemsBeforeOrigin.has(getItem(store, o.origin))) {
// case 2
if (o.origin === null || !conflictingItems.has(getItem(store, o.origin))) {
this.left = o
conflictingItems.clear()
const parent = /** @type {AbstractType<any>|null} */ (this.parent)
if (parent) {
/**
* @type {Item|null}
*/
let left = this.left
/**
* @type {Item|null}
*/
let o
// set o to the first conflicting item
if (left !== null) {
o = left.right
} else if (parentSub !== null) {
o = parent._map.get(parentSub) || null
while (o !== null && o.left !== null) {
o = o.left
}
} else {
break
o = parent._start
}
o = o.right
}
// reconnect left/right + update parent map/start if necessary
if (this.left !== null) {
const right = this.left.right
this.right = right
this.left.right = this
} else {
let r
if (parentSub !== null) {
r = parent._map.get(parentSub) || null
while (r !== null && r.left !== null) {
r = r.left
// TODO: use something like DeleteSet here (a tree implementation would be best)
// @todo use global set definitions
/**
* @type {Set<Item>}
*/
const conflictingItems = new Set()
/**
* @type {Set<Item>}
*/
const itemsBeforeOrigin = new Set()
// Let c in conflictingItems, b in itemsBeforeOrigin
// ***{origin}bbbb{this}{c,b}{c,b}{o}***
// Note that conflictingItems is a subset of itemsBeforeOrigin
while (o !== null && o !== this.right) {
itemsBeforeOrigin.add(o)
conflictingItems.add(o)
if (compareIDs(this.origin, o.origin)) {
// case 1
if (o.id.client < this.id.client) {
left = o
conflictingItems.clear()
}
} else if (o.origin !== null && itemsBeforeOrigin.has(getItem(store, o.origin))) {
// case 2
if (o.origin === null || !conflictingItems.has(getItem(store, o.origin))) {
left = o
conflictingItems.clear()
}
} else {
break
}
o = o.right
}
this.left = left
// reconnect left/right + update parent map/start if necessary
if (left !== null) {
const right = left.right
this.right = right
left.right = this
} else {
r = parent._start
parent._start = this
let r
if (parentSub !== null) {
r = parent._map.get(parentSub) || null
while (r !== null && r.left !== null) {
r = r.left
}
} else {
r = parent._start
parent._start = this
}
this.right = r
}
this.right = r
}
if (this.right !== null) {
this.right.left = this
} else if (parentSub !== null) {
// set as current parent value if right === null and this is parentSub
parent._map.set(parentSub, this)
if (this.left !== null) {
// this is the current attribute value of parent. delete right
this.left.delete(transaction)
if (this.right !== null) {
this.right.left = this
} else if (parentSub !== null) {
// set as current parent value if right === null and this is parentSub
parent._map.set(parentSub, this)
if (left !== null) {
// this is the current attribute value of parent. delete right
left.delete(transaction)
}
}
}
// adjust length of parent
if (parentSub === null && this.countable && !this.deleted) {
parent._length += length
}
addStruct(store, this)
this.content.integrate(transaction, this)
// add parent to transaction.changed
addChangedTypeToTransaction(transaction, parent, parentSub)
if ((parent._item !== null && parent._item.deleted) || (this.right !== null && parentSub !== null)) {
// delete if parent is deleted or if this is not the current attribute value of parent
this.delete(transaction)
// adjust length of parent
if (parentSub === null && this.countable && !this.deleted) {
parent._length += length
}
addStruct(store, this)
this.content.integrate(transaction, this)
// add parent to transaction.changed
addChangedTypeToTransaction(transaction, parent, parentSub)
if ((parent._item !== null && parent._item.deleted) || (this.right !== null && parentSub !== null)) {
// delete if parent is deleted or if this is not the current attribute value of parent
this.delete(transaction)
}
} else {
// parent is not defined. Integrate GC struct instead
new GC(this.id, this.length).integrate(transaction, 0)
}
}
@@ -426,7 +506,8 @@ export class Item extends AbstractStruct {
* Computes the last content address of this Item.
*/
get lastId () {
return createID(this.id.client, this.id.clock + this.length - 1)
// allocating ids is pretty costly because of the amount of ids created, so we try to reuse whenever possible
return this.length === 1 ? this.id : createID(this.id.client, this.id.clock + this.length - 1)
}
/**
@@ -468,7 +549,7 @@ export class Item extends AbstractStruct {
*/
delete (transaction) {
if (!this.deleted) {
const parent = this.parent
const parent = /** @type {AbstractType<any>} */ (this.parent)
// adjust the length of parent
if (this.countable && this.parentSub === null) {
parent._length -= this.length
@@ -521,8 +602,9 @@ export class Item extends AbstractStruct {
writeID(encoder, rightOrigin)
}
if (origin === null && rightOrigin === null) {
const parent = this.parent
if (parent._item === null) {
const parent = /** @type {AbstractType<any>} */ (this.parent)
const parentItem = parent._item
if (parentItem === null) {
// parent type on y._map
// find the correct key
const ykey = findRootTypeKey(parent)
@@ -530,7 +612,7 @@ export class Item extends AbstractStruct {
encoding.writeVarString(encoder, ykey)
} else {
encoding.writeVarUint(encoder, 0) // write parent id
writeID(encoder, parent._item.id)
writeID(encoder, parentItem.id)
}
if (parentSub !== null) {
encoding.writeVarString(encoder, parentSub)
@@ -656,123 +738,49 @@ export class AbstractContent {
}
/**
* @private
* @param {decoding.Decoder} decoder
* @param {ID} id
* @param {number} info
* @param {Doc} doc
*/
export class ItemRef extends AbstractStructRef {
export const readItem = (decoder, id, info, doc) => {
/**
* @param {decoding.Decoder} decoder
* @param {ID} id
* @param {number} info
* The item that was originally to the left of this item.
* @type {ID | null}
*/
constructor (decoder, id, info) {
super(id)
/**
* The item that was originally to the left of this item.
* @type {ID | null}
*/
this.left = (info & binary.BIT8) === binary.BIT8 ? readID(decoder) : null
/**
* The item that was originally to the right of this item.
* @type {ID | null}
*/
this.right = (info & binary.BIT7) === binary.BIT7 ? readID(decoder) : null
const canCopyParentInfo = (info & (binary.BIT7 | binary.BIT8)) === 0
const hasParentYKey = canCopyParentInfo ? decoding.readVarUint(decoder) === 1 : false
/**
* If parent = null and neither left nor right are defined, then we know that `parent` is child of `y`
* and we read the next string as parentYKey.
* It indicates how we store/retrieve parent from `y.share`
* @type {string|null}
*/
this.parentYKey = canCopyParentInfo && hasParentYKey ? decoding.readVarString(decoder) : null
/**
* The parent type.
* @type {ID | null}
*/
this.parent = canCopyParentInfo && !hasParentYKey ? readID(decoder) : null
/**
* If the parent refers to this item with some kind of key (e.g. YMap, the
* key is specified here. The key is then used to refer to the list in which
* to insert this item. If `parentSub = null` type._start is the list in
* which to insert to. Otherwise it is `parent._map`.
* @type {String | null}
*/
this.parentSub = canCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoding.readVarString(decoder) : null
const missing = this._missing
if (this.left !== null) {
missing.push(this.left)
}
if (this.right !== null) {
missing.push(this.right)
}
if (this.parent !== null) {
missing.push(this.parent)
}
/**
* @type {AbstractContent}
*/
this.content = readItemContent(decoder, info)
this.length = this.content.getLength()
}
const origin = (info & binary.BIT8) === binary.BIT8 ? readID(decoder) : null
/**
* The item that was originally to the right of this item.
* @type {ID | null}
*/
const rightOrigin = (info & binary.BIT7) === binary.BIT7 ? readID(decoder) : null
const canCopyParentInfo = (info & (binary.BIT7 | binary.BIT8)) === 0
const hasParentYKey = canCopyParentInfo ? decoding.readVarUint(decoder) === 1 : false
/**
* If parent = null and neither left nor right are defined, then we know that `parent` is child of `y`
* and we read the next string as parentYKey.
* It indicates how we store/retrieve parent from `y.share`
* @type {string|null}
*/
const parentYKey = canCopyParentInfo && hasParentYKey ? decoding.readVarString(decoder) : null
/**
* The parent type.
* @type {ID | AbstractType<any> | null}
*/
const parent = canCopyParentInfo && !hasParentYKey ? readID(decoder) : (parentYKey ? doc.get(parentYKey) : null)
/**
* If the parent refers to this item with some kind of key (e.g. YMap, the
* key is specified here. The key is then used to refer to the list in which
* to insert this item. If `parentSub = null` type._start is the list in
* which to insert to. Otherwise it is `parent._map`.
* @type {String | null}
*/
const parentSub = canCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoding.readVarString(decoder) : null
/**
* @param {Transaction} transaction
* @param {StructStore} store
* @param {number} offset
* @return {Item|GC}
* @type {AbstractContent}
*/
toStruct (transaction, store, offset) {
if (offset > 0) {
/**
* @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.content = this.content.splice(offset)
this.length -= offset
}
const content = readItemContent(decoder, info)
const left = this.left === null ? null : getItemCleanEnd(transaction, store, this.left)
const right = this.right === null ? null : getItemCleanStart(transaction, this.right)
let parent = null
let parentSub = this.parentSub
if (this.parent !== null) {
const parentItem = getItem(store, this.parent)
// 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 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 = /** @type {ContentType} */ (parentItem.content).type
}
} else if (this.parentYKey !== null) {
parent = transaction.doc.get(this.parentYKey)
} else if (left !== null) {
if (left.constructor !== GC) {
parent = left.parent
parentSub = left.parentSub
}
} else if (right !== null) {
if (right.constructor !== GC) {
parent = right.parent
parentSub = right.parentSub
}
} else {
throw error.unexpectedCase()
}
return parent === null
? new GC(this.id, this.length)
: new Item(
this.id,
left,
this.left,
right,
this.right,
parent,
parentSub,
this.content
)
}
return new Item(id, null, origin, null, rightOrigin, parent, parentSub, content)
}

View File

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

View File

@@ -40,7 +40,7 @@ export class YArrayEvent extends YEvent {
* A shared Array implementation.
* @template T
* @extends AbstractType<YArrayEvent<T>>
* @implements {IterableIterator<T>}
* @implements {Iterable<T>}
*/
export class YArray extends AbstractType {
constructor () {
@@ -121,6 +121,15 @@ export class YArray extends AbstractType {
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.
*

View File

@@ -42,16 +42,26 @@ export class YMapEvent extends YEvent {
* A shared Map implementation.
*
* @extends AbstractType<YMapEvent<T>>
* @implements {IterableIterator}
* @implements {Iterable<T>}
*/
export class YMap extends AbstractType {
constructor () {
/**
*
* @param {Iterable<readonly [string, any]>=} entries - an optional iterable to initialize the YMap
*/
constructor (entries) {
super()
/**
* @type {Map<string,any>?}
* @private
*/
this._prelimContent = new Map()
this._prelimContent = null
if (entries === undefined) {
this._prelimContent = new Map()
} else {
this._prelimContent = new Map(entries)
}
}
/**
@@ -105,6 +115,15 @@ export class YMap extends AbstractType {
return map
}
/**
* Returns the size of the YMap (count of key/value pairs)
*
* @return {number}
*/
get size () {
return [...createMapIterator(this._map)].length
}
/**
* Returns the keys for each element in the YMap Type.
*
@@ -133,7 +152,7 @@ export class YMap extends AbstractType {
}
/**
* Executes a provided function on once on overy key-value pair.
* Executes a provided function on once on every key-value pair.
*
* @param {function(T,string,YMap<T>):void} f A function to execute on every element of this YArray.
*/

View File

@@ -6,10 +6,10 @@
import {
YEvent,
AbstractType,
nextID,
createID,
getItemCleanStart,
getState,
isVisible,
createID,
YTextRefID,
callTypeObservers,
transact,
@@ -126,15 +126,14 @@ const findPosition = (transaction, parent, index) => {
*
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {Item|null} left
* @param {Item|null} right
* @param {ItemListPosition} currPos
* @param {Map<string,any>} negatedAttributes
* @return {ItemListPosition}
*
* @private
* @function
*/
const insertNegatedAttributes = (transaction, parent, left, right, negatedAttributes) => {
const insertNegatedAttributes = (transaction, parent, currPos, negatedAttributes) => {
let { left, right } = currPos
// check if we really need to remove attributes
while (
right !== null && (
@@ -150,11 +149,14 @@ const insertNegatedAttributes = (transaction, parent, left, right, negatedAttrib
left = right
right = right.right
}
const doc = transaction.doc
const ownClientId = doc.clientID
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.integrate(transaction)
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, 0)
}
return { left, right }
currPos.left = left
currPos.right = right
}
/**
@@ -174,17 +176,16 @@ const updateCurrentAttributes = (currentAttributes, format) => {
}
/**
* @param {Item|null} left
* @param {Item|null} right
* @param {ItemListPosition} currPos
* @param {Map<string,any>} currentAttributes
* @param {Object<string,any>} attributes
* @return {ItemListPosition}
*
* @private
* @function
*/
const minimizeAttributeChanges = (left, right, currentAttributes, attributes) => {
const minimizeAttributeChanges = (currPos, currentAttributes, attributes) => {
// go right while attributes[right.key] === right.value (or right is deleted)
let { left, right } = currPos
while (true) {
if (right === null) {
break
@@ -199,22 +200,24 @@ const minimizeAttributeChanges = (left, right, currentAttributes, attributes) =>
left = right
right = right.right
}
return new ItemListPosition(left, right)
currPos.left = left
currPos.right = right
}
/**
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {Item|null} left
* @param {Item|null} right
* @param {ItemListPosition} currPos
* @param {Map<string,any>} currentAttributes
* @param {Object<string,any>} attributes
* @return {ItemInsertionResult}
* @return {Map<string,any>}
*
* @private
* @function
**/
const insertAttributes = (transaction, parent, left, right, currentAttributes, attributes) => {
const insertAttributes = (transaction, parent, currPos, currentAttributes, attributes) => {
const doc = transaction.doc
const ownClientId = doc.clientID
const negatedAttributes = new Map()
// insert format-start items
for (const key in attributes) {
@@ -223,62 +226,60 @@ const insertAttributes = (transaction, parent, left, right, currentAttributes, a
if (!equalAttrs(currentVal, val)) {
// save negated attribute (set null if currentVal undefined)
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.integrate(transaction)
const { left, right } = currPos
currPos.left = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
currPos.left.integrate(transaction, 0)
}
}
return new ItemInsertionResult(left, right, negatedAttributes)
return negatedAttributes
}
/**
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {Item|null} left
* @param {Item|null} right
* @param {ItemListPosition} currPos
* @param {Map<string,any>} currentAttributes
* @param {string|object} text
* @param {Object<string,any>} attributes
* @return {ItemListPosition}
*
* @private
* @function
**/
const insertText = (transaction, parent, left, right, currentAttributes, text, attributes) => {
const insertText = (transaction, parent, currPos, currentAttributes, text, attributes) => {
for (const [key] of currentAttributes) {
if (attributes[key] === undefined) {
attributes[key] = null
}
}
const minPos = minimizeAttributeChanges(left, right, currentAttributes, attributes)
const insertPos = insertAttributes(transaction, parent, minPos.left, minPos.right, currentAttributes, attributes)
left = insertPos.left
right = insertPos.right
const doc = transaction.doc
const ownClientId = doc.clientID
minimizeAttributeChanges(currPos, currentAttributes, attributes)
const negatedAttributes = insertAttributes(transaction, parent, currPos, currentAttributes, attributes)
// insert content
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.integrate(transaction)
return insertNegatedAttributes(transaction, parent, left, insertPos.right, insertPos.negatedAttributes)
const { left, right } = currPos
currPos.left = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, content)
currPos.left.integrate(transaction, 0)
return insertNegatedAttributes(transaction, parent, currPos, negatedAttributes)
}
/**
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {Item|null} left
* @param {Item|null} right
* @param {ItemListPosition} currPos
* @param {Map<string,any>} currentAttributes
* @param {number} length
* @param {Object<string,any>} attributes
* @return {ItemListPosition}
*
* @private
* @function
*/
const formatText = (transaction, parent, left, right, currentAttributes, length, attributes) => {
const minPos = minimizeAttributeChanges(left, right, currentAttributes, attributes)
const insertPos = insertAttributes(transaction, parent, minPos.left, minPos.right, currentAttributes, attributes)
const negatedAttributes = insertPos.negatedAttributes
left = insertPos.left
right = insertPos.right
const formatText = (transaction, parent, currPos, currentAttributes, length, attributes) => {
const doc = transaction.doc
const ownClientId = doc.clientID
minimizeAttributeChanges(currPos, currentAttributes, attributes)
const negatedAttributes = insertAttributes(transaction, parent, currPos, currentAttributes, attributes)
let { left, right } = currPos
// iterate until first non-format or null is found
// delete all formats with attributes[format.key] != null
while (length > 0 && right !== null) {
@@ -318,10 +319,12 @@ const formatText = (transaction, parent, left, right, currentAttributes, length,
for (; length > 0; length--) {
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.integrate(transaction)
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, 0)
}
return insertNegatedAttributes(transaction, parent, left, right, negatedAttributes)
currPos.left = left
currPos.right = right
insertNegatedAttributes(transaction, parent, currPos, negatedAttributes)
}
/**
@@ -430,8 +433,7 @@ export const cleanupYTextFormatting = type => {
/**
* @param {Transaction} transaction
* @param {Item|null} left
* @param {Item|null} right
* @param {ItemListPosition} currPos
* @param {Map<string,any>} currentAttributes
* @param {number} length
* @return {ItemListPosition}
@@ -439,9 +441,10 @@ export const cleanupYTextFormatting = type => {
* @private
* @function
*/
const deleteText = (transaction, left, right, currentAttributes, length) => {
const deleteText = (transaction, currPos, currentAttributes, length) => {
const startAttrs = map.copy(currentAttributes)
const start = right
const start = currPos.right
let { left, right } = currPos
while (length > 0 && right !== null) {
if (right.deleted === false) {
switch (right.content.constructor) {
@@ -464,7 +467,9 @@ const deleteText = (transaction, left, right, currentAttributes, length) => {
if (start) {
cleanupFormattingGap(transaction, start, right, startAttrs, map.copy(currentAttributes))
}
return { left, right }
currPos.left = left
currPos.right = right
return currPos
}
/**
@@ -840,16 +845,19 @@ export class YText extends AbstractType {
* Apply a {@link Delta} on this shared YText type.
*
* @param {any} delta The changes to apply on this element.
* @param {object} [opts]
* @param {boolean} [opts.sanitize] Sanitize input delta. Removes ending newlines if set to true.
*
*
* @public
*/
applyDelta (delta) {
applyDelta (delta, { sanitize = true } = {}) {
if (this.doc !== null) {
transact(this.doc, transaction => {
/**
* @type {ItemListPosition}
*/
let pos = new ItemListPosition(null, this._start)
const currPos = new ItemListPosition(null, this._start)
const currentAttributes = new Map()
for (let i = 0; i < delta.length; i++) {
const op = delta[i]
@@ -859,14 +867,14 @@ export class YText extends AbstractType {
// there is a newline at the end of the content.
// If we omit this step, clients will see a different number of
// paragraphs, but nothing bad will happen.
const ins = (typeof op.insert === 'string' && i === delta.length - 1 && pos.right === null && op.insert.slice(-1) === '\n') ? op.insert.slice(0, -1) : op.insert
const ins = (!sanitize && typeof op.insert === 'string' && i === delta.length - 1 && currPos.right === null && op.insert.slice(-1) === '\n') ? op.insert.slice(0, -1) : op.insert
if (typeof ins !== 'string' || ins.length > 0) {
pos = insertText(transaction, this, pos.left, pos.right, currentAttributes, ins, op.attributes || {})
insertText(transaction, this, currPos, currentAttributes, ins, op.attributes || {})
}
} else if (op.retain !== undefined) {
pos = formatText(transaction, this, pos.left, pos.right, currentAttributes, op.retain, op.attributes || {})
formatText(transaction, this, currPos, currentAttributes, op.retain, op.attributes || {})
} else if (op.delete !== undefined) {
pos = deleteText(transaction, pos.left, pos.right, currentAttributes, op.delete)
deleteText(transaction, currPos, currentAttributes, op.delete)
}
}
})
@@ -1004,7 +1012,7 @@ export class YText extends AbstractType {
// @ts-ignore
currentAttributes.forEach((v, k) => { attributes[k] = v })
}
insertText(transaction, this, left, right, currentAttributes, text, attributes)
insertText(transaction, this, new ItemListPosition(left, right), currentAttributes, text, attributes)
})
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.insert(index, text, attributes))
@@ -1029,7 +1037,7 @@ export class YText extends AbstractType {
if (y !== null) {
transact(y, transaction => {
const { left, right, currentAttributes } = findPosition(transaction, this, index)
insertText(transaction, this, left, right, currentAttributes, embed, attributes)
insertText(transaction, this, new ItemListPosition(left, right), currentAttributes, embed, attributes)
})
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.insertEmbed(index, embed, attributes))
@@ -1052,7 +1060,7 @@ export class YText extends AbstractType {
if (y !== null) {
transact(y, transaction => {
const { left, right, currentAttributes } = findPosition(transaction, this, index)
deleteText(transaction, left, right, currentAttributes, length)
deleteText(transaction, new ItemListPosition(left, right), currentAttributes, length)
})
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.delete(index, length))
@@ -1080,7 +1088,7 @@ export class YText extends AbstractType {
if (right === null) {
return
}
formatText(transaction, this, left, right, currentAttributes, length, attributes)
formatText(transaction, this, new ItemListPosition(left, right), currentAttributes, length, attributes)
})
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.format(index, length, attributes))

View File

@@ -48,7 +48,7 @@ import * as decoding from 'lib0/decoding.js' // eslint-disable-line
* Can be created with {@link YXmlFragment#createTreeWalker}
*
* @public
* @implements {IterableIterator}
* @implements {Iterable<YXmlElement|YXmlText|YXmlElement|YXmlHook>}
*/
export class YXmlTreeWalker {
/**
@@ -81,10 +81,10 @@ export class YXmlTreeWalker {
* @type {Item|null}
*/
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
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) {
// walk down in the tree
n = type._start
@@ -97,7 +97,7 @@ export class YXmlTreeWalker {
} else if (n.parent === this._root) {
n = null
} else {
n = n.parent._item
n = /** @type {AbstractType<any>} */ (n.parent)._item
}
}
}

View File

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

View File

@@ -1,12 +1,12 @@
import {
createID,
writeID,
readID,
compareIDs,
getState,
findRootTypeKey,
Item,
createID,
ContentType,
followRedone,
ID, Doc, AbstractType // eslint-disable-line
@@ -107,7 +107,7 @@ export const createRelativePosition = (type, item) => {
if (type._item === null) {
tname = findRootTypeKey(type)
} else {
typeid = type._item.id
typeid = createID(type._item.id.client, type._item.id.clock)
}
return new RelativePosition(typeid, tname, item)
}
@@ -227,7 +227,7 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
if (!(right instanceof Item)) {
return null
}
type = right.parent
type = /** @type {AbstractType<any>} */ (right.parent)
if (type._item === null || !type._item.deleted) {
index = right.deleted || !right.countable ? 0 : res.diff
let n = right.left

View File

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

View File

@@ -2,7 +2,7 @@
import {
GC,
splitItem,
GCRef, ItemRef, Transaction, ID, Item // eslint-disable-line
AbstractStruct, Transaction, ID, Item // eslint-disable-line
} from '../internals.js'
import * as math from 'lib0/math.js'
@@ -21,13 +21,13 @@ export class StructStore {
* We could shift the array of refs instead, but shift is incredible
* slow in Chrome for arrays with more than 100k elements
* @see tryResumePendingStructRefs
* @type {Map<number,{i:number,refs:Array<GCRef|ItemRef>}>}
* @type {Map<number,{i:number,refs:Array<GC|Item>}>}
*/
this.pendingClientsStructRefs = new Map()
/**
* Stack of pending structs waiting for struct dependencies
* Maximum length of stack is structReaders.size
* @type {Array<GCRef|ItemRef>}
* @type {Array<GC|Item>}
*/
this.pendingStack = []
/**
@@ -114,7 +114,7 @@ export const addStruct = (store, struct) => {
/**
* Perform a binary search on a sorted array
* @param {Array<any>} structs
* @param {Array<Item|GC>} structs
* @param {number} clock
* @return {number}
*
@@ -124,10 +124,18 @@ export const addStruct = (store, struct) => {
export const findIndexSS = (structs, clock) => {
let left = 0
let right = structs.length - 1
let mid = structs[right]
let midclock = mid.id.clock
if (mid.id.clock === clock) {
return right
}
// @todo does it even make sense to pivot the search?
// If a good split misses, it might actually increase the time to find the correct item.
// Currently, the only advantage is that search with pivoting might find the item on the first try.
let midindex = math.floor((clock / (midclock + mid.length - 1)) * right) // pivoting the search
while (left <= right) {
const midindex = math.floor((left + right) / 2)
const mid = structs[midindex]
const midclock = mid.id.clock
mid = structs[midindex]
midclock = mid.id.clock
if (midclock <= clock) {
if (clock < midclock + mid.length) {
return midindex
@@ -136,6 +144,7 @@ export const findIndexSS = (structs, clock) => {
} else {
right = midindex - 1
}
midindex = math.floor((left + right) / 2)
}
// Always check state before looking for a struct in StructStore
// Therefore the case of not finding a struct is unexpected
@@ -163,16 +172,10 @@ export const find = (store, id) => {
/**
* 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
* @function
*/
// @ts-ignore
export const getItem = (store, id) => find(store, id)
export const getItem = /** @type {function(StructStore,ID):Item} */ (find)
/**
* @param {Transaction} transaction

View File

@@ -1,7 +1,6 @@
import {
getState,
createID,
writeStructsFromTransaction,
writeDeleteSet,
DeleteSet,
@@ -11,7 +10,8 @@ import {
callEventHandlerListeners,
Item,
generateNewClientId,
StructStore, ID, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
createID,
GC, StructStore, ID, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
} from '../internals.js'
import * as encoding from 'lib0/encoding.js'
@@ -86,9 +86,9 @@ export class Transaction {
*/
this.changedParentTypes = new Map()
/**
* @type {Set<ID>}
* @type {Array<AbstractStruct>}
*/
this._mergeStructs = new Set()
this._mergeStructs = []
/**
* @type {any}
*/
@@ -156,8 +156,8 @@ const tryToMergeWithLeft = (structs, pos) => {
if (left.deleted === right.deleted && left.constructor === right.constructor) {
if (left.mergeWith(right)) {
structs.splice(pos, 1)
if (right instanceof Item && right.parentSub !== null && right.parent._map.get(right.parentSub) === right) {
right.parent._map.set(right.parentSub, /** @type {Item} */ (left))
if (right instanceof Item && right.parentSub !== null && /** @type {AbstractType<any>} */ (right.parent)._map.get(right.parentSub) === right) {
/** @type {AbstractType<any>} */ (right.parent)._map.set(right.parentSub, /** @type {Item} */ (left))
}
}
}
@@ -170,7 +170,7 @@ const tryToMergeWithLeft = (structs, pos) => {
*/
const tryGcDeleteSet = (ds, store, gcFilter) => {
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--) {
const deleteItem = deleteItems[di]
const endDeleteItemClock = deleteItem.clock + deleteItem.len
@@ -199,7 +199,7 @@ const tryMergeDeleteSet = (ds, store) => {
// try to merge deleted / gc'd items
// merge from right to left for better efficiecy and so we don't miss any merge targets
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--) {
const deleteItem = deleteItems[di]
// start with merging the item next to the last deleted item
@@ -235,6 +235,7 @@ const cleanupTransactions = (transactionCleanups, i) => {
const doc = transaction.doc
const store = doc.store
const ds = transaction.deleteSet
const mergeStructs = transaction._mergeStructs
try {
sortAndMergeDeleteSet(ds)
transaction.afterState = getStateVector(transaction.doc.store)
@@ -292,7 +293,7 @@ const cleanupTransactions = (transactionCleanups, i) => {
for (const [client, clock] of transaction.afterState) {
const beforeClock = transaction.beforeState.get(client) || 0
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
const firstChangePos = math.max(findIndexSS(structs, beforeClock), 1)
for (let i = structs.length - 1; i >= firstChangePos; i--) {
@@ -303,10 +304,9 @@ const cleanupTransactions = (transactionCleanups, i) => {
// try to merge mergeStructs
// @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
for (const mid of transaction._mergeStructs) {
const client = mid.client
const clock = mid.clock
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
for (let i = 0; i < mergeStructs.length; i++) {
const { client, clock } = mergeStructs[i].id
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
const replacedStructPos = findIndexSS(structs, clock)
if (replacedStructPos + 1 < structs.length) {
tryToMergeWithLeft(structs, replacedStructPos + 1)

View File

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

View File

@@ -211,7 +211,7 @@ const getPathTo = (parent, child) => {
} else {
// parent is array-ish
let i = 0
let c = child._item.parent._start
let c = /** @type {AbstractType<any>} */ (child._item.parent)._start
while (c !== child._item && c !== null) {
if (!c.deleted) {
i++
@@ -220,7 +220,7 @@ const getPathTo = (parent, child) => {
}
path.unshift(i)
}
child = child._item.parent
child = /** @type {AbstractType<any>} */ (child._item.parent)
}
return path
}

View File

@@ -16,18 +16,17 @@
import {
findIndexSS,
GCRef,
ItemRef,
writeID,
createID,
readID,
getState,
createID,
getStateVector,
readAndApplyDeleteSet,
writeDeleteSet,
createDeleteSetFromStructStore,
transact,
Doc, Transaction, AbstractStruct, StructStore, ID // eslint-disable-line
readItem,
Doc, Transaction, GC, Item, StructStore, ID // eslint-disable-line
} from '../internals.js'
import * as encoding from 'lib0/encoding.js'
@@ -36,7 +35,7 @@ import * as binary from 'lib0/binary.js'
/**
* @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} clock write structs starting with `ID(client,clock)`
*
@@ -50,35 +49,12 @@ const writeStructs = (encoder, structs, client, clock) => {
writeID(encoder, createID(client, clock))
const firstStruct = structs[startNewStructs]
// 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++) {
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 {StructStore} store
@@ -103,7 +79,9 @@ export const writeClientsStructs = (encoder, store, _sm) => {
})
// write # states that were updated
encoding.writeVarUint(encoder, sm.size)
sm.forEach((clock, client) => {
// Write items with higher client ids first
// This heavily improves the conflict algorithm.
Array.from(sm.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => {
// @ts-ignore
writeStructs(encoder, store.clients.get(client), client, clock)
})
@@ -111,22 +89,32 @@ export const writeClientsStructs = (encoder, store, _sm) => {
/**
* @param {decoding.Decoder} decoder The decoder object to read data from.
* @return {Map<number,Array<GCRef|ItemRef>>}
* @param {Map<number,Array<GC|Item>>} clientRefs
* @param {Doc} doc
* @return {Map<number,Array<GC|Item>>}
*
* @private
* @function
*/
export const readClientsStructRefs = decoder => {
/**
* @type {Map<number,Array<GCRef|ItemRef>>}
*/
const clientRefs = new Map()
export const readClientsStructRefs = (decoder, clientRefs, doc) => {
const numOfStateUpdates = decoding.readVarUint(decoder)
for (let i = 0; i < numOfStateUpdates; i++) {
const numberOfStructs = decoding.readVarUint(decoder)
const nextID = readID(decoder)
const refs = readStructRefs(decoder, numberOfStructs, nextID)
clientRefs.set(nextID.client, refs)
const nextIdClient = nextID.client
let nextIdClock = nextID.clock
/**
* @type {Array<GC|Item>}
*/
const refs = []
clientRefs.set(nextIdClient, refs)
for (let i = 0; i < numberOfStructs; i++) {
const info = decoding.readUint8(decoder)
const id = createID(nextIdClient, nextIdClock)
const struct = (binary.BITS5 & info) === 0 ? new GC(id, decoding.readVarUint(decoder)) : readItem(decoder, id, info, doc)
refs.push(struct)
nextIdClock += struct.length
}
}
return clientRefs
}
@@ -170,17 +158,19 @@ const resumeStructIntegration = (transaction, store) => {
}
}
const ref = stack[stack.length - 1]
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 offset = ref.id.clock < localClock ? localClock - ref.id.clock : 0
if (ref.id.clock + offset !== localClock) {
const offset = refClock < localClock ? localClock - refClock : 0
const missing = ref.getMissing(transaction, store)
if (refClock + offset !== localClock) {
// A previous message from this client is missing
// check if there is a pending structRef with a smaller clock and switch them
const structRefs = clientsStructRefs.get(client)
if (structRefs !== undefined) {
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
structRefs.refs[structRefs.i] = ref
stack[stack.length - 1] = r
@@ -193,27 +183,21 @@ const resumeStructIntegration = (transaction, store) => {
// wait until missing struct is available
return
}
while (m.length > 0) {
const missing = m[m.length - 1]
if (getState(store, missing.client) <= missing.clock) {
const client = missing.client
// get the struct reader that has the missing struct
const structRefs = clientsStructRefs.get(client)
if (structRefs === undefined) {
// This update message causally depends on another update message.
return
}
stack.push(structRefs.refs[structRefs.i++])
if (structRefs.i === structRefs.refs.length) {
clientsStructRefs.delete(client)
}
break
if (missing) {
const client = missing.client
// get the struct reader that has the missing struct
const structRefs = clientsStructRefs.get(client)
if (structRefs === undefined) {
// This update message causally depends on another update message.
return
}
ref._missing.pop()
}
if (m.length === 0) {
stack.push(structRefs.refs[structRefs.i++])
if (structRefs.i === structRefs.refs.length) {
clientsStructRefs.delete(client)
}
} else {
if (offset < ref.length) {
ref.toStruct(transaction, store, offset).integrate(transaction)
ref.integrate(transaction, offset)
}
stack.pop()
}
@@ -246,7 +230,7 @@ export const writeStructsFromTransaction = (encoder, transaction) => writeClient
/**
* @param {StructStore} store
* @param {Map<number, Array<GCRef|ItemRef>>} clientsStructsRefs
* @param {Map<number, Array<GC|Item>>} clientsStructsRefs
*
* @private
* @function
@@ -282,7 +266,8 @@ const mergeReadStructsIntoPendingReads = (store, clientsStructsRefs) => {
* @function
*/
export const readStructs = (decoder, transaction, store) => {
const clientsStructRefs = readClientsStructRefs(decoder)
const clientsStructRefs = new Map()
readClientsStructRefs(decoder, clientsStructRefs, transaction.doc)
mergeReadStructsIntoPendingReads(store, clientsStructRefs)
resumeStructIntegration(transaction, store)
tryResumePendingDeleteReaders(transaction, store)

View File

@@ -16,7 +16,7 @@ export const isParentOf = (parent, child) => {
if (child.parent === parent) {
return true
}
child = child.parent._item
child = /** @type {AbstractType<any>} */ (child.parent)._item
}
return false
}

View File

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

View File

@@ -8,6 +8,33 @@ import * as Y from '../src/index.js'
import * as t from 'lib0/testing.js'
import * as prng from 'lib0/prng.js'
/**
* @param {t.TestCase} tc
*/
export const testMapHavingIterableAsConstructorParamTests = tc => {
const { map0 } = init(tc, { users: 1 })
const m1 = new Y.Map(Object.entries({ number: 1, string: 'hello' }))
map0.set('m1', m1)
t.assert(m1.get('number') === 1)
t.assert(m1.get('string') === 'hello')
const m2 = new Y.Map([
['object', { x: 1 }],
['boolean', true]
])
map0.set('m2', m2)
t.assert(m2.get('object').x === 1)
t.assert(m2.get('boolean') === true)
const m3 = new Y.Map([...m1, ...m2])
map0.set('m3', m3)
t.assert(m3.get('number') === 1)
t.assert(m3.get('string') === 'hello')
t.assert(m3.get('object').x === 1)
t.assert(m3.get('boolean') === true)
}
/**
* @param {t.TestCase} tc
*/
@@ -33,6 +60,7 @@ export const testBasicMapTests = tc => {
t.assert(map0.get('boolean1') === true, 'client 0 computed the change (boolean)')
t.compare(map0.get('object'), { key: { key2: 'value' } }, 'client 0 computed the change (object)')
t.assert(map0.get('y-map').get('y-array').get(0) === -1, 'client 0 computed the change (type)')
t.assert(map0.size === 6, 'client 0 map has correct size')
users[2].connect()
testConnector.flushAllMessages()
@@ -43,6 +71,7 @@ export const testBasicMapTests = tc => {
t.assert(map1.get('boolean1') === true, 'client 1 computed the change (boolean)')
t.compare(map1.get('object'), { key: { key2: 'value' } }, 'client 1 received the update (object)')
t.assert(map1.get('y-map').get('y-array').get(0) === -1, 'client 1 received the update (type)')
t.assert(map1.size === 6, 'client 1 map has correct size')
// compare disconnected user
t.assert(map2.get('number') === 1, 'client 2 received the update (number) - was disconnected')
@@ -130,6 +159,20 @@ export const testGetAndSetOfMapPropertyWithConflict = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testSizeAndDeleteOfMapProperty = tc => {
const { map0 } = init(tc, { users: 1 })
map0.set('stuff', 'c0')
map0.set('otherstuff', 'c1')
t.assert(map0.size === 2, `map size is ${map0.size} expected 2`)
map0.delete('stuff')
t.assert(map0.size === 1, `map size after delete is ${map0.size}, expected 1`)
map0.delete('otherstuff')
t.assert(map0.size === 0, `map size after delete is ${map0.size}, expected 0`)
}
/**
* @param {t.TestCase} tc
*/
@@ -454,7 +497,7 @@ const mapTransactions = [
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYmapTests10 = tc => {
applyRandomTests(tc, mapTransactions, 10)
applyRandomTests(tc, mapTransactions, 3)
}
/**

View File

@@ -205,6 +205,34 @@ export const testFormattingRemovedInMidText = tc => {
t.assert(Y.getTypeChildren(text0).length === 3)
}
/**
* @param {t.TestCase} tc
*/
export const testLargeFragmentedDocument = tc => {
const itemsToInsert = 1000000
let update = /** @type {any} */ (null)
;(() => {
const doc1 = new Y.Doc()
const text0 = doc1.getText('txt')
t.measureTime(`time to insert ${itemsToInsert}`, () => {
doc1.transact(() => {
for (let i = 0; i < itemsToInsert; i++) {
text0.insert(0, '0')
}
})
})
t.measureTime('time to encode', () => {
update = Y.encodeStateAsUpdate(doc1)
})
})()
;(() => {
const doc2 = new Y.Doc()
t.measureTime('time to apply', () => {
Y.applyUpdate(doc2, update)
})
})()
}
// RANDOM TESTS
let charCounter = 0