Compare commits

...

53 Commits

Author SHA1 Message Date
Kevin Jahns
e22fed7af3 13.5.17 2021-10-29 21:55:55 +02:00
Kevin Jahns
c91945228f inherid collectionid 2021-10-29 21:53:21 +02:00
Kevin Jahns
3586d91925 fire subdocs event only when something changed 2021-10-29 17:49:30 +02:00
Kevin Jahns
f915ebda1b 13.5.16 2021-10-15 19:18:51 +02:00
Kevin Jahns
a9b92b9099 13.5.15 2021-10-15 19:17:08 +02:00
Kevin Jahns
cbddf6ef90 add warning when Yjs was already imported 2021-10-15 19:10:11 +02:00
Kevin Jahns
491cd422c4 13.5.14 2021-10-14 16:21:02 +02:00
Kevin Jahns
4b88e2aac5 update dependencies 2021-10-14 16:19:24 +02:00
Kevin Jahns
e33c67fc72 bump standard linter 2021-10-14 16:18:50 +02:00
Kevin Jahns
085dda4cbd fix formatting test case #326 2021-10-14 16:09:23 +02:00
Kevin Jahns
f382846874 Merge pull request #326 from raedle/main
[tests] Encode/decode doc with attribute changes
2021-10-14 15:13:32 +02:00
Kevin Jahns
9afc5cf615 Merge pull request #331 from thomaswelter/main
Add support for null values in Y.Map and Y.Array
2021-10-14 15:08:20 +02:00
Kevin Jahns
ca0fb4b15d Merge pull request #332 from yjs/types-as-embeds
Allow types as Y.Text embeds
2021-10-14 15:04:26 +02:00
Kevin Jahns
d369a771a9 Merge pull request #335 from benmerckx/fix/array_map_dts
Remove T from JSDoc template, fixes #334
2021-10-14 15:00:38 +02:00
Kevin Jahns
995fbfa4cc Proper follow redones in nested redos - fixes #317 2021-10-14 14:59:26 +02:00
Kevin Jahns
7486ea7148 Merge pull request #339 from ViktorQvarfordt/patch-1
Fix type annotation of getMap
2021-10-12 22:26:54 +02:00
Viktor Qvarfordt
2c80a955da Fix type annotation of getMap
Make `getMap<T>()` take a generic type parameter just like `getArray<T>()`.
2021-10-10 11:05:18 +02:00
Kevin Jahns
233872493b Merge pull request #338 from nikgraf/patch-2
escape the plus to not interpret it as a list item
2021-10-08 17:53:58 +02:00
Nik Graf
64d164a904 escape the plus to not interpret it as a list item 2021-10-08 17:09:20 +02:00
Kevin Jahns
a08e54c2fc 13.5.13 2021-10-07 11:34:29 +02:00
Kevin Jahns
2b377cd46d export findIndexSS 2021-10-07 11:31:40 +02:00
Ben Merckx
b4b8927550 Remove T from JSDoc template, fixes #334 2021-09-28 12:36:01 +02:00
Kevin Jahns
b2761b50f2 more complex embed test 2021-09-25 11:58:39 +02:00
Kevin Jahns
28a9ce962d import from internals 2021-09-25 11:53:16 +02:00
Kevin Jahns
0ec67170d3 allow types as Y.Text embeds 2021-09-25 11:51:08 +02:00
Kevin Jahns
df9bfbe778 Merge branch 'main' of github.com:yjs/yjs 2021-09-23 22:14:42 +02:00
Kevin Jahns
f1ab417570 mergeUpdates on array with single entry just returns the first entry 2021-09-23 22:14:29 +02:00
Thomas Welter
4922eeac56 Add support for null values in Y.Map and Y.Array 2021-09-21 14:23:58 +02:00
Roman Rädle
57d6c6f831 [tests] Encode/decode doc with attribute changes
Encode a document with a text and decode it into a new document. Then, test if the same change to both documents results in the same text deltas.
2021-08-21 20:18:23 -07:00
Kevin Jahns
371f2b6d55 Merge pull request #318 from dai-shi/patch-1
Add valtio-yjs binding in README
2021-08-15 16:59:57 +02:00
Kevin Jahns
85a7ad148f Merge pull request #323 from vivaxy/bugfix/readme
Update an algorithm example
2021-08-09 15:05:33 +02:00
vivaxy
7ec1b3a19e docs: update an algorithm example 2021-08-09 19:33:45 +08:00
Kevin Jahns
633eb9033c 13.5.12 2021-08-02 16:48:25 +02:00
Kevin Jahns
4707fc46ac fix formatting bug. fixes #319 closes #320 2021-08-02 16:43:25 +02:00
Kevin Jahns
89b4320a8e fix test in #310 2021-08-02 16:20:01 +02:00
Kevin Jahns
0ea0a35521 Merge pull request #310 from SamDuvall/fix-xml-clone
Fix XmlElement.clone and XmlFragment.clone
2021-08-02 16:12:39 +02:00
Daishi Kato
15ea4ee805 Update README.md 2021-07-25 10:08:43 +09:00
Kevin Jahns
744469d363 13.5.11 2021-06-24 17:00:48 +02:00
Kevin Jahns
311dd50f1b array.insert throws length-exceeded event - fixes #314 2021-06-24 16:50:25 +02:00
Kevin Jahns
89c5541ee6 fix merge logic edge-cases with skips 2021-06-24 15:14:49 +02:00
Kevin Jahns
28d8db86f0 Merge branch 'main' of github.com:yjs/yjs into main 2021-06-21 12:05:09 +02:00
Kevin Jahns
0c34216ed0 merge pending structs in v1 format 2021-06-21 12:04:40 +02:00
Kevin Jahns
9aa518bc14 Merge pull request #313 from YousefED/hasAttribute
implement hasAttribute
2021-06-16 02:59:00 +02:00
Kevin Jahns
27b1190a28 Merge pull request #312 from whawker/console-log-removal
remove console.log added in #309
2021-06-16 01:48:22 +02:00
Yousef El-Dardiry
f3d8db491b implement hasAttribute 2021-06-15 16:36:22 +02:00
Will Hawker
e9905602f8 remove console.log debug artifact added in #309 2021-06-11 09:30:54 +01:00
Kevin Jahns
2b8154fa16 Merge pull request #309 from whawker/map-clear
add `clear` to YMap
2021-06-09 16:07:44 +02:00
Sam Duvall
5ddb7eefed Fix YXmlElement.clone and YXmlFragment.clone 2021-06-08 21:30:08 -04:00
Will Hawker
4b35de5ad5 add clear to YMap to remove all elements 2021-06-08 21:09:44 +01:00
Kevin Jahns
097b9e8208 13.5.10 2021-06-07 19:43:44 +02:00
Kevin Jahns
5cac153a17 Fix #308 - stateVector should ignore skips and incomplete content 2021-06-07 19:41:54 +02:00
Kevin Jahns
a7e4724edd 13.5.9 2021-05-31 17:56:10 +02:00
Kevin Jahns
71d8da6513 force that transactions that apply document updates are set as non-local transatctions. Fixes #307 2021-05-31 17:54:24 +02:00
27 changed files with 1603 additions and 1023 deletions

View File

@@ -168,7 +168,7 @@ An implementation of the syncing process is in
## Snapshots
A snapshot can be used to restore an old document state. It is a `state vector`
+ `delete set`. I client can restore an old document state by iterating through
\+ `delete set`. I client can restore an old document state by iterating through
the sequence CRDT and ignoring all Items that have an `id.clock >
stateVector[id.client].clock`. Instead of using `item.deleted` the client will
use the delete set to find out if an item was deleted or not.

View File

@@ -100,6 +100,7 @@ are implemented in separate modules.
| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](https://github.com/yjs/y-codemirror) | [demo](https://demos.yjs.dev/codemirror/codemirror.html) |
| [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](https://github.com/yjs/y-monaco) | [demo](https://demos.yjs.dev/monaco/monaco.html) |
| [Slate](https://github.com/ianstormtaylor/slate) | ✔ | [slate-yjs](https://github.com/bitphinix/slate-yjs) | [demo](https://bitphinix.github.io/slate-yjs-example) |
| [valtio](https://github.com/pmndrs/valtio) | | [valtio-yjs](https://github.com/dai-shi/valtio-yjs) | [demo](https://codesandbox.io/s/valtio-yjs-demo-ox3iy) |
### Providers
@@ -249,36 +250,36 @@ necessary.
<dl>
<b><code>parent:Y.AbstractType|null</code></b>
<dd></dd>
<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|null|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
position 0.
</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|null|Uint8Array|Y.Type&gt;)</code></b>
<dd></dd>
<b><code>unshift(Array&lt;Object|boolean|Array|string|number|Uint8Array|Y.Type&gt;)</code></b>
<b><code>unshift(Array&lt;Object|boolean|Array|string|number|null|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>
<dd></dd>
<b><code>slice(start:number, end:number):Array&lt;Object|boolean|Array|string|number|Uint8Array|Y.Type&gt;</code></b>
<b><code>slice(start:number, end:number):Array&lt;Object|boolean|Array|string|number|null|Uint8Array|Y.Type&gt;</code></b>
<dd>Retrieve a range of content</dd>
<b><code>length:number</code></b>
<dd></dd>
<b>
<code>
forEach(function(value:object|boolean|Array|string|number|Uint8Array|Y.Type,
forEach(function(value:object|boolean|Array|string|number|null|Uint8Array|Y.Type,
index:number, array: Y.Array))
</code>
</b>
<dd></dd>
<b><code>map(function(T, number, YArray):M):Array&lt;M&gt;</code></b>
<dd></dd>
<b><code>toArray():Array&lt;object|boolean|Array|string|number|Uint8Array|Y.Type&gt;</code></b>
<b><code>toArray():Array&lt;object|boolean|Array|string|number|null|Uint8Array|Y.Type&gt;</code></b>
<dd>Copies the content of this YArray to a new Array.</dd>
<b><code>toJSON():Array&lt;Object|boolean|Array|string|number&gt;</code></b>
<b><code>toJSON():Array&lt;Object|boolean|Array|string|number|null&gt;</code></b>
<dd>
Copies the content of this YArray to a new Array. It transforms all child types
to JSON using their <code>toJSON</code> method.
@@ -324,9 +325,9 @@ or any of its children.
<dd></dd>
<b><code>size: number</code></b>
<dd>Total number of key/value pairs.</dd>
<b><code>get(key:string):object|boolean|string|number|Uint8Array|Y.Type</code></b>
<b><code>get(key:string):object|boolean|string|number|null|Uint8Array|Y.Type</code></b>
<dd></dd>
<b><code>set(key:string, value:object|boolean|string|number|Uint8Array|Y.Type)</code></b>
<b><code>set(key:string, value:object|boolean|string|number|null|Uint8Array|Y.Type)</code></b>
<dd></dd>
<b><code>delete(key:string)</code></b>
<dd></dd>
@@ -334,14 +335,16 @@ or any of its children.
<dd></dd>
<b><code>get(index:number)</code></b>
<dd></dd>
<b><code>clear()</code></b>
<dd>Removes all elements from this YMap.</dd>
<b><code>clone():Y.Map</code></b>
<dd>Clone this type into a fresh Yjs type.</dd>
<b><code>toJSON():Object&lt;string, Object|boolean|Array|string|number|Uint8Array&gt;</code></b>
<b><code>toJSON():Object&lt;string, Object|boolean|Array|string|number|null|Uint8Array&gt;</code></b>
<dd>
Copies the <code>[key,value]</code> pairs of this YMap to a new Object.It
transforms all child types to JSON using their <code>toJSON</code> method.
</dd>
<b><code>forEach(function(value:object|boolean|Array|string|number|Uint8Array|Y.Type,
<b><code>forEach(function(value:object|boolean|Array|string|number|null|Uint8Array|Y.Type,
key:string, map: Y.Map))</code></b>
<dd>
Execute the provided function once for every key-value pair.
@@ -986,7 +989,7 @@ order of the structs anymore (e.g. if the parent was deleted).
**Examples:**
1. If a user inserts elements in sequence, the struct will be merged into a
single struct. E.g. `array.insert(0, ['a']), array.insert(0, ['b']);` is
single struct. E.g. `text.insert(0, 'a'), text.insert(1, 'b');` is
first represented as two structs (`[{id: {client, clock: 0}, content: 'a'},
{id: {client, clock: 1}, content: 'b'}`) and then merged into a single
struct: `[{id: {client, clock: 0}, content: 'ab'}]`.

1776
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "yjs",
"version": "13.5.8",
"version": "13.5.17",
"description": "Shared Editing Library",
"main": "./dist/yjs.cjs",
"module": "./dist/yjs.mjs",
@@ -71,19 +71,19 @@
},
"homepage": "https://docs.yjs.dev",
"dependencies": {
"lib0": "^0.2.41"
"lib0": "^0.2.42"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-node-resolve": "^11.2.1",
"concurrently": "^3.6.1",
"http-server": "^0.12.3",
"jsdoc": "^3.6.6",
"jsdoc": "^3.6.7",
"markdownlint-cli": "^0.23.2",
"rollup": "^2.47.0",
"standard": "^14.3.4",
"rollup": "^2.58.0",
"standard": "^16.0.4",
"tui-jsdoc-template": "^1.2.2",
"typescript": "^4.2.4",
"y-protocols": "^1.0.4"
"typescript": "^4.4.4",
"y-protocols": "^1.0.5"
}
}

View File

@@ -1,3 +1,4 @@
/** eslint-env browser */
export {
Doc,
@@ -43,6 +44,7 @@ export {
snapshot,
emptySnapshot,
findRootTypeKey,
findIndexSS,
getItem,
typeListToArraySnapshot,
typeMapGetSnapshot,
@@ -83,3 +85,25 @@ export {
diffUpdate,
diffUpdateV2
} from './internals.js'
const glo = /** @type {any} */ (typeof window !== 'undefined'
? window
: typeof global !== 'undefined' ? global : {})
const importIdentifier = '__ $YJS$ __'
if (glo[importIdentifier] === true) {
/**
* Dear reader of this warning message. Please take this seriously.
*
* If you see this message, please make sure that you only import one version of Yjs. In many cases,
* your package manager installs two versions of Yjs that are used by different packages within your project.
* Another reason for this message is that some parts of your project use the commonjs version of Yjs
* and others use the EcmaScript version of Yjs.
*
* This often leads to issues that are hard to debug. We often need to perform constructor checks,
* e.g. `struct instanceof GC`. If you imported different versions of Yjs, it is impossible for us to
* do the constructor checks anymore - which might break the CRDT algorithm.
*/
console.warn('Yjs was already imported. Importing different versions of Yjs often leads to issues.')
}
glo[importIdentifier] = true

View File

@@ -125,12 +125,13 @@ export const splitItem = (transaction, leftItem, diff) => {
* @param {Transaction} transaction The Yjs instance.
* @param {Item} item
* @param {Set<Item>} redoitems
* @param {Array<Item>} itemsToDelete
*
* @return {Item|null}
*
* @private
*/
export const redoItem = (transaction, item, redoitems) => {
export const redoItem = (transaction, item, redoitems, itemsToDelete) => {
const doc = transaction.doc
const store = doc.store
const ownClientID = doc.clientID
@@ -170,7 +171,7 @@ export const redoItem = (transaction, item, redoitems) => {
// make sure that parent is redone
if (parentItem !== null && parentItem.deleted === true && parentItem.redone === null) {
// try to undo parent if it will be undone anyway
if (!redoitems.has(parentItem) || redoItem(transaction, parentItem, redoitems) === null) {
if (!redoitems.has(parentItem) || redoItem(transaction, parentItem, redoitems, itemsToDelete) === null) {
return null
}
}
@@ -209,6 +210,11 @@ export const redoItem = (transaction, item, redoitems) => {
}
right = right.right
}
// Iterate right while right is in itemsToDelete
// If it is intended to delete right while item is redone, we can expect that item should replace right.
while (left !== null && left.right !== null && left.right !== right && itemsToDelete.findIndex(d => d === /** @type {Item} */ (left).right) >= 0) {
left = left.right
}
}
const nextClock = getState(store, ownClientID)
const nextId = createID(ownClientID, nextClock)

View File

@@ -623,7 +623,7 @@ export const typeListGet = (type, index) => {
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {Item?} referenceItem
* @param {Array<Object<string,any>|Array<any>|boolean|number|string|Uint8Array>} content
* @param {Array<Object<string,any>|Array<any>|boolean|number|null|string|Uint8Array>} content
*
* @private
* @function
@@ -635,7 +635,7 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
const store = doc.store
const right = referenceItem === null ? parent._start : referenceItem.right
/**
* @type {Array<Object|Array<any>|number>}
* @type {Array<Object|Array<any>|number|null>}
*/
let jsonContent = []
const packJsonContent = () => {
@@ -646,49 +646,58 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
}
}
content.forEach(c => {
switch (c.constructor) {
case Number:
case Object:
case Boolean:
case Array:
case String:
jsonContent.push(c)
break
default:
packJsonContent()
switch (c.constructor) {
case Uint8Array:
case ArrayBuffer:
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
case Doc:
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentDoc(/** @type {Doc} */ (c)))
left.integrate(transaction, 0)
break
default:
if (c instanceof AbstractType) {
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentType(c))
if (c === null) {
jsonContent.push(c)
} else {
switch (c.constructor) {
case Number:
case Object:
case Boolean:
case Array:
case String:
jsonContent.push(c)
break
default:
packJsonContent()
switch (c.constructor) {
case Uint8Array:
case ArrayBuffer:
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)
} else {
throw new Error('Unexpected content type in insert operation')
}
}
break
case Doc:
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentDoc(/** @type {Doc} */ (c)))
left.integrate(transaction, 0)
break
default:
if (c instanceof AbstractType) {
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')
}
}
}
}
})
packJsonContent()
}
const lengthExceeded = error.create('Length exceeded!')
/**
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {number} index
* @param {Array<Object<string,any>|Array<any>|number|string|Uint8Array>} content
* @param {Array<Object<string,any>|Array<any>|number|null|string|Uint8Array>} content
*
* @private
* @function
*/
export const typeListInsertGenerics = (transaction, parent, index, content) => {
if (index > parent._length) {
throw lengthExceeded
}
if (index === 0) {
if (parent._searchMarker) {
updateMarkerChanges(parent._searchMarker, index, content.length)
@@ -766,7 +775,7 @@ export const typeListDelete = (transaction, parent, index, length) => {
n = n.right
}
if (length > 0) {
throw error.create('array length exceeded')
throw lengthExceeded
}
if (parent._searchMarker) {
updateMarkerChanges(parent._searchMarker, startIndex, -startLength + length /* in case we remove the above exception */)
@@ -792,7 +801,7 @@ export const typeMapDelete = (transaction, parent, key) => {
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {string} key
* @param {Object|number|Array<any>|string|Uint8Array|AbstractType<any>} value
* @param {Object|number|null|Array<any>|string|Uint8Array|AbstractType<any>} value
*
* @private
* @function
@@ -833,7 +842,7 @@ export const typeMapSet = (transaction, parent, key, value) => {
/**
* @param {AbstractType<any>} parent
* @param {string} key
* @return {Object<string,any>|number|Array<any>|string|Uint8Array|AbstractType<any>|undefined}
* @return {Object<string,any>|number|null|Array<any>|string|Uint8Array|AbstractType<any>|undefined}
*
* @private
* @function
@@ -845,7 +854,7 @@ export const typeMapGet = (parent, key) => {
/**
* @param {AbstractType<any>} parent
* @return {Object<string,Object<string,any>|number|Array<any>|string|Uint8Array|AbstractType<any>|undefined>}
* @return {Object<string,Object<string,any>|number|null|Array<any>|string|Uint8Array|AbstractType<any>|undefined>}
*
* @private
* @function
@@ -880,7 +889,7 @@ export const typeMapHas = (parent, key) => {
* @param {AbstractType<any>} parent
* @param {string} key
* @param {Snapshot} snapshot
* @return {Object<string,any>|number|Array<any>|string|Uint8Array|AbstractType<any>|undefined}
* @return {Object<string,any>|number|null|Array<any>|string|Uint8Array|AbstractType<any>|undefined}
*
* @private
* @function

View File

@@ -215,7 +215,7 @@ export class YArray extends AbstractType {
* Returns an Array with the result of calling a provided function on every
* element of this YArray.
*
* @template T,M
* @template M
* @param {function(T,number,YArray<T>):M} f Function that produces an element of the new Array
* @return {Array<M>} A new array with each element being the result of the
* callback function

View File

@@ -36,11 +36,11 @@ export class YMapEvent extends YEvent {
}
/**
* @template T number|string|Object|Array|Uint8Array
* @template MapType
* A shared Map implementation.
*
* @extends AbstractType<YMapEvent<T>>
* @implements {Iterable<T>}
* @extends AbstractType<YMapEvent<MapType>>
* @implements {Iterable<MapType>}
*/
export class YMap extends AbstractType {
/**
@@ -85,7 +85,7 @@ export class YMap extends AbstractType {
}
/**
* @return {YMap<T>}
* @return {YMap<MapType>}
*/
clone () {
const map = new YMap()
@@ -108,11 +108,11 @@ export class YMap extends AbstractType {
/**
* Transforms this Shared Type to a JSON object.
*
* @return {Object<string,T>}
* @return {Object<string,any>}
*/
toJSON () {
/**
* @type {Object<string,T>}
* @type {Object<string,MapType>}
*/
const map = {}
this._map.forEach((item, key) => {
@@ -163,11 +163,11 @@ export class YMap extends AbstractType {
/**
* 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.
* @param {function(MapType,string,YMap<MapType>):void} f A function to execute on every element of this YArray.
*/
forEach (f) {
/**
* @type {Object<string,T>}
* @type {Object<string,MapType>}
*/
const map = {}
this._map.forEach((item, key) => {
@@ -179,7 +179,7 @@ export class YMap extends AbstractType {
}
/**
* @return {IterableIterator<T>}
* @return {IterableIterator<MapType>}
*/
[Symbol.iterator] () {
return this.entries()
@@ -204,7 +204,7 @@ export class YMap extends AbstractType {
* Adds or updates an element with a specified key and value.
*
* @param {string} key The key of the element to add to this YMap
* @param {T} value The value of the element to add
* @param {MapType} value The value of the element to add
*/
set (key, value) {
if (this.doc !== null) {
@@ -221,7 +221,7 @@ export class YMap extends AbstractType {
* Returns a specified element from this YMap.
*
* @param {string} key
* @return {T|undefined}
* @return {MapType|undefined}
*/
get (key) {
return /** @type {any} */ (typeMapGet(this, key))
@@ -237,6 +237,21 @@ export class YMap extends AbstractType {
return typeMapHas(this, key)
}
/**
* Removes all elements from this YMap.
*/
clear () {
if (this.doc !== null) {
transact(this.doc, transaction => {
this.forEach(function (value, key, map) {
typeMapDelete(transaction, map, key)
})
})
} else {
/** @type {Map<string, any>} */ (this._prelimContent).clear()
}
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
*/

View File

@@ -26,6 +26,7 @@ import {
typeMapGet,
typeMapGetAll,
updateMarkerChanges,
ContentType,
ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction // eslint-disable-line
} from '../internals.js'
@@ -62,17 +63,16 @@ export class ItemTextListPosition {
error.unexpectedCase()
}
switch (this.right.content.constructor) {
case ContentEmbed:
case ContentString:
if (!this.right.deleted) {
this.index += this.right.length
}
break
case ContentFormat:
if (!this.right.deleted) {
updateCurrentAttributes(this.currentAttributes, /** @type {ContentFormat} */ (this.right.content))
}
break
default:
if (!this.right.deleted) {
this.index += this.right.length
}
break
}
this.left = this.right
this.right = this.right.right
@@ -91,8 +91,12 @@ export class ItemTextListPosition {
const findNextPosition = (transaction, pos, count) => {
while (pos.right !== null && count > 0) {
switch (pos.right.content.constructor) {
case ContentEmbed:
case ContentString:
case ContentFormat:
if (!pos.right.deleted) {
updateCurrentAttributes(pos.currentAttributes, /** @type {ContentFormat} */ (pos.right.content))
}
break
default:
if (!pos.right.deleted) {
if (count < pos.right.length) {
// split right
@@ -102,11 +106,6 @@ const findNextPosition = (transaction, pos, count) => {
count -= pos.right.length
}
break
case ContentFormat:
if (!pos.right.deleted) {
updateCurrentAttributes(pos.currentAttributes, /** @type {ContentFormat} */ (pos.right.content))
}
break
}
pos.left = pos.right
pos.right = pos.right.right
@@ -164,12 +163,13 @@ const insertNegatedAttributes = (transaction, parent, currPos, negatedAttributes
}
const doc = transaction.doc
const ownClientId = doc.clientID
let nextFormat = currPos.left
const right = currPos.right
negatedAttributes.forEach((val, key) => {
nextFormat = new Item(createID(ownClientId, getState(doc.store, ownClientId)), nextFormat, nextFormat && nextFormat.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
const left = currPos.left
const right = currPos.right
const nextFormat = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
nextFormat.integrate(transaction, 0)
currPos.right = nextFormat
currPos.forward()
})
}
@@ -244,7 +244,7 @@ const insertAttributes = (transaction, parent, currPos, attributes) => {
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {ItemTextListPosition} currPos
* @param {string|object} text
* @param {string|object|AbstractType<any>} text
* @param {Object<string,any>} attributes
*
* @private
@@ -261,7 +261,7 @@ const insertText = (transaction, parent, currPos, text, attributes) => {
minimizeAttributeChanges(currPos, attributes)
const negatedAttributes = insertAttributes(transaction, parent, currPos, attributes)
// 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)) : (text instanceof AbstractType ? new ContentType(text) : new ContentEmbed(text))
let { left, right, index } = currPos
if (parent._searchMarker) {
updateMarkerChanges(parent._searchMarker, currPos.index, content.getLength())
@@ -307,8 +307,7 @@ const formatText = (transaction, parent, currPos, length, attributes) => {
}
break
}
case ContentEmbed:
case ContentString:
default:
if (length < currPos.right.length) {
getItemCleanStart(transaction, createID(currPos.right.id.client, currPos.right.id.clock + length))
}
@@ -347,7 +346,7 @@ const formatText = (transaction, parent, currPos, length, attributes) => {
* @function
*/
const cleanupFormattingGap = (transaction, start, end, startAttributes, endAttributes) => {
while (end && end.content.constructor !== ContentString && end.content.constructor !== ContentEmbed) {
while (end && (!end.countable || end.deleted)) {
if (!end.deleted && end.content.constructor === ContentFormat) {
updateCurrentAttributes(endAttributes, /** @type {ContentFormat} */ (end.content))
}
@@ -380,12 +379,12 @@ const cleanupFormattingGap = (transaction, start, end, startAttributes, endAttri
*/
const cleanupContextlessFormattingGap = (transaction, item) => {
// iterate until item.right is null or content
while (item && item.right && (item.right.deleted || (item.right.content.constructor !== ContentString && item.right.content.constructor !== ContentEmbed))) {
while (item && item.right && (item.right.deleted || !item.right.countable)) {
item = item.right
}
const attrs = new Set()
// iterate back until a content item is found
while (item && (item.deleted || (item.content.constructor !== ContentString && item.content.constructor !== ContentEmbed))) {
while (item && (item.deleted || !item.countable)) {
if (!item.deleted && item.content.constructor === ContentFormat) {
const key = /** @type {ContentFormat} */ (item.content).key
if (attrs.has(key)) {
@@ -423,8 +422,7 @@ export const cleanupYTextFormatting = type => {
case ContentFormat:
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (end.content))
break
case ContentEmbed:
case ContentString:
default:
res += cleanupFormattingGap(transaction, start, end, startAttributes, currentAttributes)
startAttributes = map.copy(currentAttributes)
start = end
@@ -453,6 +451,7 @@ const deleteText = (transaction, currPos, length) => {
while (length > 0 && currPos.right !== null) {
if (currPos.right.deleted === false) {
switch (currPos.right.content.constructor) {
case ContentType:
case ContentEmbed:
case ContentString:
if (length < currPos.right.length) {
@@ -539,7 +538,7 @@ export class YTextEvent extends YEvent {
get changes () {
if (this._changes === null) {
/**
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Array<{insert?:Array<any>|string, delete?:number, retain?:number}>}}
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Array<{insert?:Array<any>|string|AbstractType<any>|object, delete?:number, retain?:number}>}}
*/
const changes = {
keys: this.keys,
@@ -556,7 +555,7 @@ export class YTextEvent extends YEvent {
* Compute the changes in the delta format.
* A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document.
*
* @type {Array<{insert?:string, delete?:number, retain?:number, attributes?: Object<string,any>}>}
* @type {Array<{insert?:string|object|AbstractType<any>, delete?:number, retain?:number, attributes?: Object<string,any>}>}
*
* @public
*/
@@ -564,7 +563,7 @@ export class YTextEvent extends YEvent {
if (this._delta === null) {
const y = /** @type {Doc} */ (this.target.doc)
/**
* @type {Array<{insert?:string, delete?:number, retain?:number, attributes?: Object<string,any>}>}
* @type {Array<{insert?:string|object|AbstractType<any>, delete?:number, retain?:number, attributes?: Object<string,any>}>}
*/
const delta = []
transact(y, transaction => {
@@ -625,12 +624,13 @@ export class YTextEvent extends YEvent {
}
while (item !== null) {
switch (item.content.constructor) {
case ContentType:
case ContentEmbed:
if (this.adds(item)) {
if (!this.deletes(item)) {
addOp()
action = 'insert'
insert = /** @type {ContentEmbed} */ (item.content).embed
insert = item.content.getContent()[0]
addOp()
}
} else if (this.deletes(item)) {
@@ -1007,13 +1007,14 @@ export class YText extends AbstractType {
str += /** @type {ContentString} */ (n.content).str
break
}
case ContentType:
case ContentEmbed: {
packStr()
/**
* @type {Object<string,any>}
*/
const op = {
insert: /** @type {ContentEmbed} */ (n.content).embed
insert: n.content.getContent()[0]
}
if (currentAttributes.size > 0) {
const attrs = /** @type {Object<string,any>} */ ({})
@@ -1074,16 +1075,13 @@ export class YText extends AbstractType {
* Inserts an embed at a index.
*
* @param {number} index The index to insert the embed at.
* @param {Object} embed The Object that represents the embed.
* @param {Object | AbstractType<any>} embed The Object that represents the embed.
* @param {TextAttributes} attributes Attribute information to apply on the
* embed
*
* @public
*/
insertEmbed (index, embed, attributes = {}) {
if (embed.constructor !== Object) {
throw new Error('Embed must be an Object')
}
const y = this.doc
if (y !== null) {
transact(y, transaction => {

View File

@@ -3,6 +3,7 @@ import {
YXmlFragment,
transact,
typeMapDelete,
typeMapHas,
typeMapSet,
typeMapGet,
typeMapGetAll,
@@ -81,7 +82,7 @@ export class YXmlElement extends YXmlFragment {
el.setAttribute(key, attrs[key])
}
// @ts-ignore
el.insert(0, el.toArray().map(item => item instanceof AbstractType ? item.clone() : item))
el.insert(0, this.toArray().map(item => item instanceof AbstractType ? item.clone() : item))
return el
}
@@ -160,6 +161,18 @@ export class YXmlElement extends YXmlFragment {
return /** @type {any} */ (typeMapGet(this, attributeName))
}
/**
* Returns whether an attribute exists
*
* @param {String} attributeName The attribute name to check for existence.
* @return {boolean} whether the attribute exists.
*
* @public
*/
hasAttribute (attributeName) {
return /** @type {any} */ (typeMapHas(this, attributeName))
}
/**
* Returns all attribute name/value pairs in a JSON Object.
*

View File

@@ -167,7 +167,7 @@ export class YXmlFragment extends AbstractType {
clone () {
const el = new YXmlFragment()
// @ts-ignore
el.insert(0, el.toArray().map(item => item instanceof AbstractType ? item.clone() : item))
el.insert(0, this.toArray().map(item => item instanceof AbstractType ? item.clone() : item))
return el
}

View File

@@ -25,6 +25,7 @@ export const generateNewClientId = random.uint32
* @property {boolean} [DocOpts.gc=true] Disable garbage collection (default: gc=true)
* @property {function(Item):boolean} [DocOpts.gcFilter] Will be called before an Item is garbage collected. Return false to keep the Item.
* @property {string} [DocOpts.guid] Define a globally unique identifier for this document
* @property {string | null} [DocOpts.collectionid] Associate this document with a collection. This only plays a role if your provider has a concept of collection.
* @property {any} [DocOpts.meta] Any kind of meta information you want to associate with this document. If this is a subdocument, remote peers will store the meta information as well.
* @property {boolean} [DocOpts.autoLoad] If a subdocument, automatically load document. If this is a subdocument, remote peers will load the document as well automatically.
*/
@@ -37,12 +38,13 @@ export class Doc extends Observable {
/**
* @param {DocOpts} [opts] configuration
*/
constructor ({ guid = random.uuidv4(), gc = true, gcFilter = () => true, meta = null, autoLoad = false } = {}) {
constructor ({ guid = random.uuidv4(), collectionid = null, gc = true, gcFilter = () => true, meta = null, autoLoad = false } = {}) {
super()
this.gc = gc
this.gcFilter = gcFilter
this.clientID = generateNewClientId()
this.guid = guid
this.collectionid = collectionid
/**
* @type {Map<string, AbstractType<YEvent>>}
*/
@@ -194,8 +196,9 @@ export class Doc extends Observable {
}
/**
* @template T
* @param {string} [name]
* @return {YMap<any>}
* @return {YMap<T>}
*
* @public
*/

View File

@@ -129,9 +129,9 @@ export const snapshot = doc => createSnapshot(createDeleteSetFromStructStore(doc
* @protected
* @function
*/
export const isVisible = (item, snapshot) => snapshot === undefined ? !item.deleted : (
snapshot.sv.has(item.id.client) && (snapshot.sv.get(item.id.client) || 0) > item.id.clock && !isDeleted(snapshot.ds, item.id)
)
export const isVisible = (item, snapshot) => snapshot === undefined
? !item.deleted
: snapshot.sv.has(item.id.client) && (snapshot.sv.get(item.id.client) || 0) > item.id.clock && !isDeleted(snapshot.ds, item.id)
/**
* @param {Transaction} transaction

View File

@@ -350,11 +350,19 @@ const cleanupTransactions = (transactionCleanups, i) => {
doc.emit('updateV2', [encoder.toUint8Array(), transaction.origin, doc, transaction])
}
}
transaction.subdocsAdded.forEach(subdoc => doc.subdocs.add(subdoc))
transaction.subdocsRemoved.forEach(subdoc => doc.subdocs.delete(subdoc))
doc.emit('subdocs', [{ loaded: transaction.subdocsLoaded, added: transaction.subdocsAdded, removed: transaction.subdocsRemoved }])
transaction.subdocsRemoved.forEach(subdoc => subdoc.destroy())
const { subdocsAdded, subdocsLoaded, subdocsRemoved } = transaction
if (subdocsAdded.size > 0 || subdocsRemoved.size > 0 || subdocsLoaded.size > 0) {
subdocsAdded.forEach(subdoc => {
subdoc.clientID = doc.clientID
if (subdoc.collectionid == null) {
subdoc.collectionid = doc.collectionid
}
doc.subdocs.add(subdoc)
})
subdocsRemoved.forEach(subdoc => doc.subdocs.delete(subdoc))
doc.emit('subdocs', [{ loaded: subdocsLoaded, added: subdocsAdded, removed: subdocsRemoved }])
subdocsRemoved.forEach(subdoc => subdoc.destroy())
}
if (transactionCleanups.length <= i + 1) {
doc._transactionCleanups = []

View File

@@ -88,7 +88,7 @@ const popStackItem = (undoManager, stack, eventType) => {
}
})
itemsToRedo.forEach(struct => {
performedChange = redoItem(transaction, struct, itemsToRedo) !== null || performedChange
performedChange = redoItem(transaction, struct, itemsToRedo, itemsToDelete) !== null || performedChange
})
// We want to delete in reverse order so that children are deleted before
// parents, so we have more information available when items are filtered.

View File

@@ -40,7 +40,7 @@ export class YEvent {
*/
this._keys = null
/**
* @type {null | Array<{ insert?: string | Array<any>, retain?: number, delete?: number, attributes?: Object<string, any> }>}
* @type {null | Array<{ insert?: string | Array<any> | object | AbstractType<any>, retain?: number, delete?: number, attributes?: Object<string, any> }>}
*/
this._delta = null
}
@@ -129,7 +129,7 @@ export class YEvent {
}
/**
* @type {Array<{insert?: string | Array<any>, retain?: number, delete?: number, attributes?: Object<string, any>}>}
* @type {Array<{insert?: string | Array<any> | object | AbstractType<any>, retain?: number, delete?: number, attributes?: Object<string, any>}>}
*/
get delta () {
return this.changes.delta

View File

@@ -32,9 +32,11 @@ import {
DSEncoderV2,
DSDecoderV1,
DSEncoderV1,
mergeUpdates,
mergeUpdatesV2,
Skip,
diffUpdateV2,
convertUpdateFormatV2ToV1,
DSDecoderV2, Doc, Transaction, GC, Item, StructStore // eslint-disable-line
} from '../internals.js'
@@ -380,6 +382,8 @@ export const writeStructsFromTransaction = (encoder, transaction) => writeClient
*/
export const readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = new UpdateDecoderV2(decoder)) =>
transact(ydoc, transaction => {
// force that transaction.local is set to non-local
transaction.local = false
let retry = false
const doc = transaction.doc
const store = doc.store
@@ -521,15 +525,16 @@ export const encodeStateAsUpdateV2 = (doc, encodedTargetStateVector = new Uint8A
writeStateAsUpdate(encoder, doc, targetStateVector)
const updates = [encoder.toUint8Array()]
// also add the pending updates (if there are any)
// @todo support diffirent encoders
if (encoder.constructor === UpdateEncoderV2) {
if (doc.store.pendingDs) {
updates.push(doc.store.pendingDs)
}
if (doc.store.pendingStructs) {
updates.push(diffUpdateV2(doc.store.pendingStructs.update, encodedTargetStateVector))
}
if (updates.length > 1) {
if (doc.store.pendingDs) {
updates.push(doc.store.pendingDs)
}
if (doc.store.pendingStructs) {
updates.push(diffUpdateV2(doc.store.pendingStructs.update, encodedTargetStateVector))
}
if (updates.length > 1) {
if (encoder.constructor === UpdateEncoderV1) {
return mergeUpdates(updates.map((update, i) => i === 0 ? update : convertUpdateFormatV2ToV1(update)))
} else if (encoder.constructor === UpdateEncoderV2) {
return mergeUpdatesV2(updates)
}
}

View File

@@ -149,23 +149,27 @@ export const mergeUpdates = updates => mergeUpdatesV2(updates, UpdateDecoderV1,
*/
export const encodeStateVectorFromUpdateV2 = (update, YEncoder = DSEncoderV2, YDecoder = UpdateDecoderV2) => {
const encoder = new YEncoder()
const updateDecoder = new LazyStructReader(new YDecoder(decoding.createDecoder(update)), true)
const updateDecoder = new LazyStructReader(new YDecoder(decoding.createDecoder(update)), false)
let curr = updateDecoder.curr
if (curr !== null) {
let size = 1
let size = 0
let currClient = curr.id.client
let currClock = curr.id.clock
let stopCounting = false
let stopCounting = curr.id.clock !== 0 // must start at 0
let currClock = stopCounting ? 0 : curr.id.clock + curr.length
for (; curr !== null; curr = updateDecoder.next()) {
if (currClient !== curr.id.client) {
size++
// We found a new client
// write what we have to the encoder
encoding.writeVarUint(encoder.restEncoder, currClient)
encoding.writeVarUint(encoder.restEncoder, currClock)
if (currClock !== 0) {
size++
// We found a new client
// write what we have to the encoder
encoding.writeVarUint(encoder.restEncoder, currClient)
encoding.writeVarUint(encoder.restEncoder, currClock)
}
currClient = curr.id.client
stopCounting = false
currClock = 0
stopCounting = curr.id.clock !== 0
}
// we ignore skips
if (curr.constructor === Skip) {
stopCounting = true
}
@@ -174,8 +178,11 @@ export const encodeStateVectorFromUpdateV2 = (update, YEncoder = DSEncoderV2, YD
}
}
// write what we have
encoding.writeVarUint(encoder.restEncoder, currClient)
encoding.writeVarUint(encoder.restEncoder, currClock)
if (currClock !== 0) {
size++
encoding.writeVarUint(encoder.restEncoder, currClient)
encoding.writeVarUint(encoder.restEncoder, currClock)
}
// prepend the size of the state vector
const enc = encoding.createEncoder()
encoding.writeVarUint(enc, size)
@@ -280,6 +287,9 @@ const sliceStruct = (left, diff) => {
* @return {Uint8Array}
*/
export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => {
if (updates.length === 1) {
return updates[0]
}
const updateDecoders = updates.map(update => new YDecoder(decoding.createDecoder(update)))
let lazyStructDecoders = updateDecoders.map(decoder => new LazyStructReader(decoder, true))
@@ -305,9 +315,10 @@ export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = U
if (dec1.curr.id.client === dec2.curr.id.client) {
const clockDiff = dec1.curr.id.clock - dec2.curr.id.clock
if (clockDiff === 0) {
return dec1.curr.constructor === dec2.curr.constructor ? 0 : (
dec1.curr.constructor === Skip ? 1 : -1
)
// @todo remove references to skip since the structDecoders must filter Skips.
return dec1.curr.constructor === dec2.curr.constructor
? 0
: dec1.curr.constructor === Skip ? 1 : -1 // we are filtering skips anyway.
} else {
return clockDiff
}
@@ -326,13 +337,19 @@ export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = U
if (currWrite !== null) {
let curr = /** @type {Item | GC | null} */ (currDecoder.curr)
let iterated = false
// iterate until we find something that we haven't written already
// remember: first the high client-ids are written
while (curr !== null && curr.id.clock + curr.length <= currWrite.struct.id.clock + currWrite.struct.length && curr.id.client >= currWrite.struct.id.client) {
curr = currDecoder.next()
iterated = true
}
if (curr === null || curr.id.client !== firstClient) {
if (
curr === null || // current decoder is empty
curr.id.client !== firstClient || // check whether there is another decoder that has has updates from `firstClient`
(iterated && curr.id.clock > currWrite.struct.id.clock + currWrite.struct.length) // the above while loop was used and we are potentially missing updates
) {
continue
}
@@ -508,3 +525,33 @@ const finishLazyStructWriting = (lazyWriter) => {
encoding.writeUint8Array(restEncoder, partStructs.restEncoder)
}
}
/**
* @param {Uint8Array} update
* @param {typeof UpdateDecoderV2 | typeof UpdateDecoderV1} YDecoder
* @param {typeof UpdateEncoderV2 | typeof UpdateEncoderV1 } YEncoder
*/
export const convertUpdateFormat = (update, YDecoder, YEncoder) => {
const updateDecoder = new YDecoder(decoding.createDecoder(update))
const lazyDecoder = new LazyStructReader(updateDecoder, false)
const updateEncoder = new YEncoder()
const lazyWriter = new LazyStructWriter(updateEncoder)
for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) {
writeStructToLazyStructWriter(lazyWriter, curr, 0)
}
finishLazyStructWriting(lazyWriter)
const ds = readDeleteSet(updateDecoder)
writeDeleteSet(updateEncoder, ds)
return updateEncoder.toUint8Array()
}
/**
* @param {Uint8Array} update
*/
export const convertUpdateFormatV1ToV2 = update => convertUpdateFormat(update, UpdateDecoderV1, UpdateEncoderV2)
/**
* @param {Uint8Array} update
*/
export const convertUpdateFormatV2ToV1 = update => convertUpdateFormat(update, UpdateDecoderV2, UpdateEncoderV1)

View File

@@ -18,6 +18,8 @@ import {
applyUpdate
} from '../src/internals.js'
import * as Y from '../src/index.js'
/**
* @param {t.TestCase} tc
*/
@@ -62,3 +64,45 @@ export const testPermanentUserData = async tc => {
const pd3 = new PermanentUserData(ydoc3)
pd3.setUserMapping(ydoc3, ydoc3.clientID, 'user a')
}
/**
* Reported here: https://github.com/yjs/yjs/issues/308
* @param {t.TestCase} tc
*/
export const testDiffStateVectorOfUpdateIsEmpty = tc => {
const ydoc = new Y.Doc()
/**
* @type {null | Uint8Array}
*/
let sv = /* any */ (null)
ydoc.getText().insert(0, 'a')
ydoc.on('update', update => {
sv = Y.encodeStateVectorFromUpdate(update)
})
// should produce an update with an empty state vector (because previous ops are missing)
ydoc.getText().insert(0, 'a')
t.assert(sv !== null && sv.byteLength === 1 && sv[0] === 0)
}
/**
* Reported here: https://github.com/yjs/yjs/issues/308
* @param {t.TestCase} tc
*/
export const testDiffStateVectorOfUpdateIgnoresSkips = tc => {
const ydoc = new Y.Doc()
/**
* @type {Array<Uint8Array>}
*/
const updates = []
ydoc.on('update', update => {
updates.push(update)
})
ydoc.getText().insert(0, 'a')
ydoc.getText().insert(0, 'b')
ydoc.getText().insert(0, 'c')
const update13 = Y.mergeUpdates([updates[0], updates[2]])
const sv = Y.encodeStateVectorFromUpdate(update13)
const state = Y.decodeStateVector(sv)
t.assert(state.get(ydoc.clientID) === 1)
t.assert(state.size === 1)
}

View File

@@ -324,7 +324,7 @@ export const init = (tc, { users = 5 } = {}, initTestObject) => {
*/
export const compare = users => {
users.forEach(u => u.connect())
while (users[0].tc.flushAllMessages()) {}
while (users[0].tc.flushAllMessages()) {} // eslint-disable-line
// For each document, merge all received document updates with Y.mergeUpdates and create a new document which will be added to the list of "users"
// This ensures that mergeUpdates works correctly
const mergedDocs = users.map(user => {
@@ -362,7 +362,14 @@ export const compare = users => {
t.compare(userMapValues[i], userMapValues[i + 1])
t.compare(userXmlValues[i], userXmlValues[i + 1])
t.compare(userTextValues[i].map(/** @param {any} a */ a => typeof a.insert === 'string' ? a.insert : ' ').join('').length, users[i].getText('text').length)
t.compare(userTextValues[i], userTextValues[i + 1])
t.compare(userTextValues[i], userTextValues[i + 1], '', (constructor, a, b) => {
if (a instanceof Y.AbstractType) {
t.compare(a.toJSON(), b.toJSON())
} else if (a !== b) {
t.fail('Deltas dont match')
}
return true
})
t.compare(Y.getStateVector(users[i].store), Y.getStateVector(users[i + 1].store))
compareDS(Y.createDeleteSetFromStructStore(users[i].store), Y.createDeleteSetFromStructStore(users[i + 1].store))
compareStructStores(users[i].store, users[i + 1].store)

View File

@@ -301,3 +301,58 @@ export const testUndoUntilChangePerformed = tc => {
undoManager.undo()
t.compareStrings(yMap2.get('key'), 'value')
}
/**
* This issue has been reported in https://github.com/yjs/yjs/issues/317
* @param {t.TestCase} tc
*/
export const testUndoNestedUndoIssue = tc => {
const doc = new Y.Doc({ gc: false })
const design = doc.getMap()
const undoManager = new Y.UndoManager(design, { captureTimeout: 0 })
/**
* @type {Y.Map<any>}
*/
const text = new Y.Map()
const blocks1 = new Y.Array()
const blocks1block = new Y.Map()
doc.transact(() => {
blocks1block.set('text', 'Type Something')
blocks1.push([blocks1block])
text.set('blocks', blocks1block)
design.set('text', text)
})
const blocks2 = new Y.Array()
const blocks2block = new Y.Map()
doc.transact(() => {
blocks2block.set('text', 'Something')
blocks2.push([blocks2block])
text.set('blocks', blocks2block)
})
const blocks3 = new Y.Array()
const blocks3block = new Y.Map()
doc.transact(() => {
blocks3block.set('text', 'Something Else')
blocks3.push([blocks3block])
text.set('blocks', blocks3block)
})
t.compare(design.toJSON(), { text: { blocks: { text: 'Something Else' } } })
undoManager.undo()
t.compare(design.toJSON(), { text: { blocks: { text: 'Something' } } })
undoManager.undo()
t.compare(design.toJSON(), { text: { blocks: { text: 'Type Something' } } })
undoManager.undo()
t.compare(design.toJSON(), { })
undoManager.redo()
t.compare(design.toJSON(), { text: { blocks: { text: 'Type Something' } } })
undoManager.redo()
t.compare(design.toJSON(), { text: { blocks: { text: 'Something' } } })
undoManager.redo()
t.compare(design.toJSON(), { text: { blocks: { text: 'Something Else' } } })
}

View File

@@ -166,9 +166,7 @@ const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
const targetSV = Y.encodeStateVectorFromUpdateV2(Y.mergeUpdatesV2(updates.slice(0, j)))
const diffed = enc.diffUpdate(mergedUpdates, targetSV)
const diffedMeta = enc.parseUpdateMeta(diffed)
const decDiffedSV = Y.decodeStateVector(enc.encodeStateVectorFromUpdate(diffed))
t.compare(partMeta, diffedMeta)
t.compare(decDiffedSV, partMeta.to)
{
// We can'd do the following
// - t.compare(diffed, mergedDeletes)
@@ -242,5 +240,49 @@ export const testMergeUpdates2 = tc => {
}
/**
* @todo be able to apply Skip structs to Yjs docs
* @param {t.TestCase} tc
*/
export const testMergePendingUpdates = tc => {
const yDoc = new Y.Doc()
/**
* @type {Array<Uint8Array>}
*/
const serverUpdates = []
yDoc.on('update', (update, origin, c) => {
serverUpdates.splice(serverUpdates.length, 0, update)
})
const yText = yDoc.getText('textBlock')
yText.applyDelta([{ insert: 'r' }])
yText.applyDelta([{ insert: 'o' }])
yText.applyDelta([{ insert: 'n' }])
yText.applyDelta([{ insert: 'e' }])
yText.applyDelta([{ insert: 'n' }])
const yDoc1 = new Y.Doc()
Y.applyUpdate(yDoc1, serverUpdates[0])
const update1 = Y.encodeStateAsUpdate(yDoc1)
const yDoc2 = new Y.Doc()
Y.applyUpdate(yDoc2, update1)
Y.applyUpdate(yDoc2, serverUpdates[1])
const update2 = Y.encodeStateAsUpdate(yDoc2)
const yDoc3 = new Y.Doc()
Y.applyUpdate(yDoc3, update2)
Y.applyUpdate(yDoc3, serverUpdates[3])
const update3 = Y.encodeStateAsUpdate(yDoc3)
const yDoc4 = new Y.Doc()
Y.applyUpdate(yDoc4, update3)
Y.applyUpdate(yDoc4, serverUpdates[2])
const update4 = Y.encodeStateAsUpdate(yDoc4)
const yDoc5 = new Y.Doc()
Y.applyUpdate(yDoc5, update4)
Y.applyUpdate(yDoc5, serverUpdates[4])
// @ts-ignore
const update5 = Y.encodeStateAsUpdate(yDoc5) // eslint-disable-line
const yText5 = yDoc5.getText('textBlock')
t.compareStrings(yText5.toString(), 'nenor')
}

View File

@@ -61,6 +61,49 @@ export const testLengthIssue = tc => {
t.assert(arr.length === arr.toArray().length)
}
/**
* Debugging yjs#314
*
* @param {t.TestCase} tc
*/
export const testLengthIssue2 = tc => {
const doc = new Y.Doc()
const next = doc.getArray()
doc.transact(() => {
next.insert(0, ['group2'])
})
doc.transact(() => {
next.insert(1, ['rectangle3'])
})
doc.transact(() => {
next.delete(0)
next.insert(0, ['rectangle3'])
})
next.delete(1)
doc.transact(() => {
next.insert(1, ['ellipse4'])
})
doc.transact(() => {
next.insert(2, ['ellipse3'])
})
doc.transact(() => {
next.insert(3, ['ellipse2'])
})
doc.transact(() => {
doc.transact(() => {
t.fails(() => {
next.insert(5, ['rectangle2'])
})
next.insert(4, ['rectangle2'])
})
doc.transact(() => {
// this should not throw an error message
next.delete(4)
})
})
console.log(next.toArray())
}
/**
* @param {t.TestCase} tc
*/
@@ -445,6 +488,11 @@ const arrayTransactions = [
map.set('someprop', 43)
map.set('someprop', 44)
},
function insertTypeNull (user, gen) {
const yarray = user.getArray('array')
const pos = prng.int32(gen, 0, yarray.length)
yarray.insert(pos, [null])
},
function _delete (user, gen) {
const yarray = user.getArray('array')
const length = yarray.length
@@ -453,7 +501,7 @@ const arrayTransactions = [
let delLength = prng.int32(gen, 1, math.min(2, length - somePos))
if (prng.bool(gen)) {
const type = yarray.get(somePos)
if (type.length > 0) {
if (type instanceof Y.Array && type.length > 0) {
somePos = prng.int32(gen, 0, type.length - 1)
delLength = prng.int32(gen, 0, math.min(2, type.length - somePos))
type.delete(somePos, delLength)

View File

@@ -42,6 +42,7 @@ export const testBasicMapTests = tc => {
const { testConnector, users, map0, map1, map2 } = init(tc, { users: 3 })
users[2].disconnect()
map0.set('null', null)
map0.set('number', 1)
map0.set('string', 'hello Y')
map0.set('object', { key: { key2: 'value' } })
@@ -54,26 +55,29 @@ export const testBasicMapTests = tc => {
array.insert(0, [0])
array.insert(0, [-1])
t.assert(map0.get('null') === null, 'client 0 computed the change (null)')
t.assert(map0.get('number') === 1, 'client 0 computed the change (number)')
t.assert(map0.get('string') === 'hello Y', 'client 0 computed the change (string)')
t.assert(map0.get('boolean0') === false, 'client 0 computed the change (boolean)')
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')
t.assert(map0.size === 7, 'client 0 map has correct size')
users[2].connect()
testConnector.flushAllMessages()
t.assert(map1.get('null') === null, 'client 1 received the update (null)')
t.assert(map1.get('number') === 1, 'client 1 received the update (number)')
t.assert(map1.get('string') === 'hello Y', 'client 1 received the update (string)')
t.assert(map1.get('boolean0') === false, 'client 1 computed the change (boolean)')
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')
t.assert(map1.size === 7, 'client 1 map has correct size')
// compare disconnected user
t.assert(map2.get('null') === null, 'client 2 received the update (null) - was disconnected')
t.assert(map2.get('number') === 1, 'client 2 received the update (number) - was disconnected')
t.assert(map2.get('string') === 'hello Y', 'client 2 received the update (string) - was disconnected')
t.assert(map2.get('boolean0') === false, 'client 2 computed the change (boolean)')
@@ -189,6 +193,49 @@ export const testGetAndSetAndDeleteOfMapProperty = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testSetAndClearOfMapProperties = tc => {
const { testConnector, users, map0 } = init(tc, { users: 1 })
map0.set('stuff', 'c0')
map0.set('otherstuff', 'c1')
map0.clear()
testConnector.flushAllMessages()
for (const user of users) {
const u = user.getMap('map')
t.assert(u.get('stuff') === undefined)
t.assert(u.get('otherstuff') === undefined)
t.assert(u.size === 0, `map size after clear is ${u.size}, expected 0`)
}
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testSetAndClearOfMapPropertiesWithConflicts = tc => {
const { testConnector, users, map0, map1, map2, map3 } = init(tc, { users: 4 })
map0.set('stuff', 'c0')
map1.set('stuff', 'c1')
map1.set('stuff', 'c2')
map2.set('stuff', 'c3')
testConnector.flushAllMessages()
map0.set('otherstuff', 'c0')
map1.set('otherstuff', 'c1')
map2.set('otherstuff', 'c2')
map3.set('otherstuff', 'c3')
map3.clear()
testConnector.flushAllMessages()
for (const user of users) {
const u = user.getMap('map')
t.assert(u.get('stuff') === undefined)
t.assert(u.get('otherstuff') === undefined)
t.assert(u.size === 0, `map size after clear is ${u.size}, expected 0`)
}
compare(users)
}
/**
* @param {t.TestCase} tc
*/
@@ -335,6 +382,30 @@ export const testThrowsAddAndUpdateAndDeleteEvents = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testThrowsDeleteEventsOnClear = tc => {
const { users, map0 } = init(tc, { users: 2 })
/**
* @type {Object<string,any>}
*/
let event = {}
map0.observe(e => {
event = e // just put it on event, should be thrown synchronously anyway
})
// set values
map0.set('stuff', 4)
map0.set('otherstuff', new Y.Array())
// clear
map0.clear()
compareEvent(event, {
keysChanged: new Set(['stuff', 'otherstuff']),
target: map0
})
compare(users)
}
/**
* @param {t.TestCase} tc
*/

View File

@@ -151,6 +151,29 @@ export const testGetDeltaWithEmbeds = tc => {
}])
}
/**
* @param {t.TestCase} tc
*/
export const testTypesAsEmbed = tc => {
const { text0, text1, testConnector } = init(tc, { users: 2 })
text0.applyDelta([{
insert: new Y.YMap([['key', 'val']])
}])
t.compare(text0.toDelta()[0].insert.toJSON(), { key: 'val' })
let firedEvent = false
text1.observe(event => {
const d = event.delta
t.assert(d.length === 1)
t.compare(d.map(x => /** @type {Y.AbstractType<any>} */ (x.insert).toJSON()), [{ key: 'val' }])
firedEvent = true
})
testConnector.flushAllMessages()
const delta = text1.toDelta()
t.assert(delta.length === 1)
t.compare(delta[0].insert.toJSON(), { key: 'val' })
t.assert(firedEvent, 'fired the event observer containing a Type-Embed')
}
/**
* @param {t.TestCase} tc
*/
@@ -455,6 +478,69 @@ export const testSplitSurrogateCharacter = tc => {
}
}
/**
* Search marker bug https://github.com/yjs/yjs/issues/307
*
* @param {t.TestCase} tc
*/
export const testSearchMarkerBug1 = tc => {
const { users, text0, text1, testConnector } = init(tc, { users: 2 })
users[0].on('update', update => {
users[0].transact(() => {
Y.applyUpdate(users[0], update)
})
})
users[0].on('update', update => {
users[1].transact(() => {
Y.applyUpdate(users[1], update)
})
})
text0.insert(0, 'a_a')
testConnector.flushAllMessages()
text0.insert(2, 's')
testConnector.flushAllMessages()
text1.insert(3, 'd')
testConnector.flushAllMessages()
text0.delete(0, 5)
testConnector.flushAllMessages()
text0.insert(0, 'a_a')
testConnector.flushAllMessages()
text0.insert(2, 's')
testConnector.flushAllMessages()
text1.insert(3, 'd')
testConnector.flushAllMessages()
t.compareStrings(text0.toString(), text1.toString())
t.compareStrings(text0.toString(), 'a_sda')
compare(users)
}
/**
* Reported in https://github.com/yjs/yjs/pull/32
*
* @param {t.TestCase} tc
*/
export const testFormattingBug = async tc => {
const ydoc1 = new Y.Doc()
const ydoc2 = new Y.Doc()
const text1 = ydoc1.getText()
text1.insert(0, '\n\n\n')
text1.format(0, 3, { url: 'http://example.com' })
ydoc1.getText().format(1, 1, { url: 'http://docs.yjs.dev' })
ydoc2.getText().format(1, 1, { url: 'http://docs.yjs.dev' })
Y.applyUpdate(ydoc2, Y.encodeStateAsUpdate(ydoc1))
const text2 = ydoc2.getText()
const expectedResult = [
{ insert: '\n', attributes: { url: 'http://example.com' } },
{ insert: '\n', attributes: { url: 'http://docs.yjs.dev' } },
{ insert: '\n', attributes: { url: 'http://example.com' } }
]
t.compare(text1.toDelta(), expectedResult)
t.compare(text1.toDelta(), text2.toDelta())
console.log(text1.toDelta())
}
// RANDOM TESTS
let charCounter = 0
@@ -590,7 +676,11 @@ const qChanges = [
(y, gen) => { // insert embed
const ytext = y.getText('text')
const insertPos = prng.int32(gen, 0, ytext.length)
ytext.insertEmbed(insertPos, { image: 'https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png' })
if (prng.bool(gen)) {
ytext.insertEmbed(insertPos, { image: 'https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png' })
} else {
ytext.insertEmbed(insertPos, new Y.YMap([[prng.word(gen), prng.word(gen)]]))
}
},
/**
* @param {Y.Doc} y
@@ -637,8 +727,12 @@ const qChanges = [
*/
const checkResult = result => {
for (let i = 1; i < result.testObjects.length; i++) {
const p1 = result.users[i].getText('text').toDelta()
const p2 = result.users[i].getText('text').toDelta()
/**
* @param {any} d
*/
const typeToObject = d => d.insert instanceof Y.AbstractType ? d.insert.toJSON() : d
const p1 = result.users[i].getText('text').toDelta().map(typeToObject)
const p2 = result.users[i].getText('text').toDelta().map(typeToObject)
t.compare(p1, p2)
}
// Uncomment this to find formatting-cleanup issues

View File

@@ -15,6 +15,23 @@ export const testSetProperty = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testHasProperty = tc => {
const { testConnector, users, xml0, xml1 } = init(tc, { users: 2 })
xml0.setAttribute('height', '10')
t.assert(xml0.hasAttribute('height'), 'Simple set+has works')
testConnector.flushAllMessages()
t.assert(xml1.hasAttribute('height'), 'Simple set+has works (remote)')
xml0.removeAttribute('height')
t.assert(!xml0.hasAttribute('height'), 'Simple set+remove+has works')
testConnector.flushAllMessages()
t.assert(!xml1.hasAttribute('height'), 'Simple set+remove+has works (remote)')
compare(users)
}
/**
* @param {t.TestCase} tc
*/
@@ -133,3 +150,36 @@ export const testInsertafter = tc => {
el.insertAfter(deepsecond1, [new Y.XmlText()])
})
}
/**
* @param {t.TestCase} tc
*/
export const testClone = tc => {
const ydoc = new Y.Doc()
const yxml = ydoc.getXmlFragment()
const first = new Y.XmlText('text')
const second = new Y.XmlElement('p')
const third = new Y.XmlElement('p')
yxml.push([first, second, third])
t.compareArrays(yxml.toArray(), [first, second, third])
const cloneYxml = yxml.clone()
ydoc.getArray('copyarr').insert(0, [cloneYxml])
t.assert(cloneYxml.length === 3)
t.compare(cloneYxml.toJSON(), yxml.toJSON())
}
/**
* @param {t.TestCase} tc
*/
export const testFormattingBug = tc => {
const ydoc = new Y.Doc()
const yxml = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText))
const delta = [
{ insert: 'A', attributes: { em: {}, strong: {} } },
{ insert: 'B', attributes: { em: {} } },
{ insert: 'C', attributes: { em: {}, strong: {} } }
]
yxml.applyDelta(delta)
t.compare(yxml.toDelta(), delta)
}