Compare commits

...

55 Commits

Author SHA1 Message Date
Kevin Jahns
0fb55981ba 13.0.0 2020-01-23 21:53:02 +01:00
Kevin Jahns
89378e29ae publish stable Yjs release 🎆 2020-01-23 21:51:26 +01:00
Kevin Jahns
cce35270ec typescript typingis!!! fixes #180 2020-01-23 21:45:56 +01:00
Kevin Jahns
d78180bf97 make opts optional in PermanentUserData 2020-01-23 18:05:12 +01:00
Kevin Jahns
0ab415de3e 13.0.0-108 2020-01-23 05:01:05 +01:00
Kevin Jahns
ff3969caeb dedupe npm 2020-01-23 05:00:11 +01:00
Kevin Jahns
c82cc9f8d6 lint 2020-01-23 04:59:17 +01:00
Kevin Jahns
ef5c71bd8b PermanentUserData fixes 2020-01-23 04:58:02 +01:00
Kevin Jahns
bd6be3d23b 13.0.0-107 2020-01-22 16:45:48 +01:00
Kevin Jahns
0e6deab9c9 type toJSON returns 2020-01-22 16:44:30 +01:00
Kevin Jahns
6cd9e2be32 lint 2020-01-22 16:42:16 +01:00
Kevin Jahns
ac8dab1e88 Merge pull request #179 from garth/text-tojson
basic Y.Text toJSON returns {unformatted:string}
2020-01-22 16:19:01 +01:00
Garth Williams
38ed725c2c basic Y.Text toJSON returns unformatted string
This avoids text nodes in nested structures returning undefined when toJSON is called by a parent.
2020-01-22 13:34:13 +01:00
Kevin Jahns
a210bad25e update keywords 2020-01-19 00:43:23 +01:00
Kevin Jahns
6929a4f0f8 13.0.0-106 2020-01-14 05:16:43 +01:00
Kevin Jahns
52dacfa5f2 update package-lock 2020-01-14 05:15:36 +01:00
Kevin Jahns
27efe86f9c isParentOf 2020-01-14 05:13:51 +01:00
Kevin Jahns
882b9055c7 fix localimports path ending 2020-01-14 02:36:29 +01:00
Kevin Jahns
e089089413 fix debug resolve 2020-01-13 17:03:56 +01:00
Kevin Jahns
197932752e 13.0.0-105 2020-01-13 14:55:05 +01:00
Kevin Jahns
f0b2bdaf34 revert to classic cjs module 2020-01-13 14:54:07 +01:00
Kevin Jahns
b96362c0f1 use correct module script 2020-01-13 07:55:58 +01:00
Kevin Jahns
67f241cd7a 13.0.0-104 2020-01-13 07:48:47 +01:00
Kevin Jahns
c8af0bebf7 fix preversion script 2020-01-13 07:47:43 +01:00
Kevin Jahns
4f35e799a6 update to lib0@.2 2020-01-13 07:41:31 +01:00
Kevin Jahns
eb2a52dd26 update README with podcast links, consulting info, and y-webrtc 2019-12-11 13:26:46 +01:00
Kevin Jahns
189b1068ae 13.0.0-103 2019-12-10 20:52:20 +01:00
Kevin Jahns
7a3b60a5d7 add markdownlint-cli as dep 2019-12-10 20:51:07 +01:00
Kevin Jahns
99f06fc093 bump lib0 for improved encoding performance 2019-12-10 20:46:58 +01:00
Kevin Jahns
22917bca19 fix gc & proper options typings for Y.Doc, fixes #176 2019-12-10 17:51:49 +01:00
Kevin Jahns
7f0e25dcba permanent user store writes updates in separate transaction 2019-12-10 17:18:57 +01:00
Kevin Jahns
d90c9b1cb2 bump lib0 for faster text encoding 2019-12-10 00:26:28 +01:00
Kevin Jahns
c426055f17 spelling 2019-12-10 00:19:02 +01:00
Kevin Jahns
18c9010b63 Merge branch 'master' of github.com:y-js/yjs 2019-11-26 13:02:49 +01:00
Kevin Jahns
c3edac62ef doc typo 2019-11-26 13:02:43 +01:00
Kevin Jahns
755de18fd5 Create Funding.yml 2019-11-07 14:41:50 +01:00
Kevin Jahns
641dc25076 13.0.0-102 2019-10-25 23:47:23 +02:00
Kevin Jahns
1d58ea785f Merge branch 'master' of github.com:yjs/yjs 2019-10-25 23:45:50 +02:00
Kevin Jahns
f53dff5043 delay errors in observe callbacks to throw after cleanup is done 2019-10-25 23:44:09 +02:00
Kevin Jahns
74d1a31f49 Merge pull request #174 from boschDev/master
Fix attrs loop in yXmlText
2019-10-15 17:19:30 +02:00
Roeland Bosch
d1063ab70b Fix attrs loop in yXmlText 2019-10-15 17:07:20 +02:00
Kevin Jahns
f4c919d9ec 13.0.0-101 2019-10-08 18:33:50 +02:00
Kevin Jahns
aeb23dbaa9 follow redone items to prevent some undo-redo issues. Fixes #162 2019-10-08 18:31:56 +02:00
Kevin Jahns
6d4f0c0cdd 13.0.0-100 2019-10-08 17:40:32 +02:00
Kevin Jahns
303138f309 sanitize items before undoing. fixes #165 2019-10-08 17:36:00 +02:00
Kevin Jahns
ad373a3dce Merge pull request #172 from istvank/patch-1
Fixing Y.Map's documentation of forEach
2019-10-05 20:09:53 +02:00
István Koren
2150fa58f2 Fixing Y.Map's documentation of forEach
fixes #171 As always, it's an honor to submit a PR! 🐒 There was also a missing dot in the Y.XmlFragment title.
2019-10-05 15:14:30 +02:00
Kevin Jahns
ece4841b5c update stackItem.meta doc 2019-10-03 22:06:07 +02:00
Kevin Jahns
8103220c05 Merge branch 'master' of github.com:yjs/yjs 2019-09-30 11:10:13 +02:00
Kevin Jahns
66d500f08d YEvent: consider case that item was added & removed in the same transaction 2019-09-30 11:10:03 +02:00
Kevin Jahns
5f8e7c7ba7 Merge pull request #169 from yjs/improve-readme
update quill cursors support
2019-09-23 11:22:51 +02:00
Nik Graf
7b8eee6b25 update quill cursors support 2019-09-23 11:22:24 +02:00
Kevin Jahns
1d5947c602 13.0.0-99 2019-09-23 11:11:45 +02:00
Kevin Jahns
53e4028952 Merge pull request #168 from yjs/fix-absolute-position-calculation
fix absolute position calculation
2019-09-23 11:09:48 +02:00
Nik Graf
b38a8d99e5 fix absolute position calculation 2019-09-23 11:05:50 +02:00
39 changed files with 1655 additions and 2911 deletions

12
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# 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

@@ -17,9 +17,11 @@ suited for even large documents.
* Discuss: [https://discuss.yjs.dev](https://discuss.yjs.dev) * Discuss: [https://discuss.yjs.dev](https://discuss.yjs.dev)
* Benchmarks: * Benchmarks:
[https://github.com/dmonad/crdt-benchmarks](https://github.com/dmonad/crdt-benchmarks) [https://github.com/dmonad/crdt-benchmarks](https://github.com/dmonad/crdt-benchmarks)
* Podcast [**"Yjs Deep Dive into real time collaborative editing solutions":**](https://www.tag1consulting.com/blog/deep-dive-real-time-collaborative-editing-solutions-tagteamtalk-001-0)
* Podcast [**"Google Docs-style editing in Gutenberg with the YJS framework":**](https://publishpress.com/blog/yjs/)
:warning: This is the documentation for v13 (still in alpha). For the stable v12 :construction_worker_woman: If you are looking for professional support to build
release checkout the [v12 docs](./README.v12.md) :warning: collaborative or distributed applications ping us at <yjs@tag1consulting.com>.
## Table of Contents ## Table of Contents
@@ -55,7 +57,7 @@ are implemented in separate modules.
| Name | Cursors | Binding | Demo | | Name | Cursors | Binding | Demo |
|---|:-:|---|---| |---|:-:|---|---|
| [ProseMirror](https://prosemirror.net/) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | ✔ | [y-prosemirror](http://github.com/yjs/y-prosemirror) | [demo](https://yjs-demos.now.sh/prosemirror/) | | [ProseMirror](https://prosemirror.net/) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | ✔ | [y-prosemirror](http://github.com/yjs/y-prosemirror) | [demo](https://yjs-demos.now.sh/prosemirror/) |
| [Quill](https://quilljs.com/) | | [y-quill](http://github.com/yjs/y-quill) | [demo](https://yjs-demos.now.sh/quill/) | | [Quill](https://quilljs.com/) | | [y-quill](http://github.com/yjs/y-quill) | [demo](https://yjs-demos.now.sh/quill/) |
| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](http://github.com/yjs/y-codemirror) | [demo](https://yjs-demos.now.sh/codemirror/) | | [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](http://github.com/yjs/y-codemirror) | [demo](https://yjs-demos.now.sh/codemirror/) |
| [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](http://github.com/yjs/y-monaco) | [demo](https://yjs-demos.now.sh/monaco/) | | [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](http://github.com/yjs/y-monaco) | [demo](https://yjs-demos.now.sh/monaco/) |
| [Ace](https://ace.c9.io/) | | [y-ace](http://github.com/yjs/y-ace) | [demo](https://yjs-demos.now.sh/ace/) | | [Ace](https://ace.c9.io/) | | [y-ace](http://github.com/yjs/y-ace) | [demo](https://yjs-demos.now.sh/ace/) |
@@ -70,18 +72,19 @@ manage all that for you and are the perfect starting point for your
collaborative app. collaborative app.
<dl> <dl>
<dt><a href="http://github.com/yjs/y-webrtc">y-webrtc</a></dt>
<dd>
Propagates document updates peer-to-peer using WebRTC. The peers exchange
signaling data over signaling servers. Publically available signaling servers
are available. Communication over the signaling servers can be encrypted by
providing a shared secret, keeping the connection information and the shared
document private.
</dd>
<dt><a href="http://github.com/yjs/y-websocket">y-websocket</a></dt> <dt><a href="http://github.com/yjs/y-websocket">y-websocket</a></dt>
<dd> <dd>
A module that contains a simple websocket backend and a websocket client that A module that contains a simple websocket backend and a websocket client that
connects to that backend. The backend can be extended to persist updates in a connects to that backend. The backend can be extended to persist updates in a
leveldb database. leveldb database.
</dd>
<dt><a href="http://github.com/yjs/y-mesh">y-mesh</a></dt>
<dd>
[WIP] Creates a connected graph of webrtc connections with a high
<a href="https://en.wikipedia.org/wiki/Strength_of_a_graph">strength</a>. It
requires a signalling server that connects a client to the first peer. But after
that the network manages itself. It is well suited for large and small networks.
</dd> </dd>
<dt><a href="http://github.com/yjs/y-dat">y-dat</a></dt> <dt><a href="http://github.com/yjs/y-dat">y-dat</a></dt>
<dd> <dd>
@@ -97,7 +100,7 @@ hypercores and y-dat listens to changes and applies them to the Yjs document.
Install Yjs and a provider with your favorite package manager: Install Yjs and a provider with your favorite package manager:
```sh ```sh
npm i yjs@13.0.0-97 y-websocket@1.0.0-6 npm i yjs y-websocket
``` ```
Start the y-websocket server: Start the y-websocket server:
@@ -235,7 +238,8 @@ or any of its children.
Copies the <code>[key,value]</code> pairs of this YMap to a new Object.It 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. transforms all child types to JSON using their <code>toJSON</code> method.
</dd> </dd>
<b><code>forEach(function(key:string,value:object|boolean|Array|string|number|Uint8Array|Y.Type))</code></b> <b><code>forEach(function(value:object|boolean|Array|string|number|Uint8Array|Y.Type,
key:string, map: Y.Map))</code></b>
<dd> <dd>
Execute the provided function once for every key-value pair. Execute the provided function once for every key-value pair.
</dd> </dd>
@@ -343,7 +347,7 @@ or any of its children.
</details> </details>
<details> <details>
<summary><b>YXmlFragment</b></summary> <summary><b>Y.XmlFragment</b></summary>
<br> <br>
<p> <p>
A container that holds an Array of Y.XmlElements. A container that holds an Array of Y.XmlElements.
@@ -670,7 +674,7 @@ undo- or the redo-stack.
<code> <code>
on('stack-item-popped', { stackItem: { meta: Map&lt;any,any&gt; }, type: 'undo' on('stack-item-popped', { stackItem: { meta: Map&lt;any,any&gt; }, type: 'undo'
| 'redo' }) | 'redo' })
</code> </code>
</b> </b>
<dd> <dd>
Register an event that is called when a <code>StackItem</code> is popped from Register an event that is called when a <code>StackItem</code> is popped from

3647
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,24 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.0.0-98", "version": "13.0.0",
"description": "Shared Editing Library", "description": "Shared Editing Library",
"main": "./dist/yjs.js", "main": "./dist/yjs.cjs",
"module": "./dist/yjs.mjs", "module": "./src/index.js",
"types": "./dist/src/index.d.ts",
"sideEffects": false, "sideEffects": false,
"scripts": { "scripts": {
"test": "npm run dist && PRODUCTION=1 node ./dist/tests.js --repitition-time 50 --production", "test": "npm run dist && node ./dist/tests.cjs --repitition-time 50",
"test-exhaustive": "npm run lint && npm run dist && node ./dist/tests.js --repitition-time 10000", "test-exhaustive": "npm run lint && npm run dist && node ./dist/tests.cjs --production --repitition-time 10000",
"dist": "rm -rf dist && rollup -c", "dist": "rm -rf dist && rollup -c && tsc",
"watch": "rollup -wc", "watch": "rollup -wc",
"lint": "markdownlint README.md && standard && tsc", "lint": "markdownlint README.md && standard && tsc",
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true", "docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true",
"serve-docs": "npm run docs && serve ./docs/", "serve-docs": "npm run docs && http-server ./docs/",
"preversion": "npm run lint && PRODUCTION=1 npm run dist && npm run docs && node ./dist/tests.js --repitition-time 1000", "preversion": "npm run lint && PRODUCTION=1 npm run dist && npm run docs && node ./dist/tests.cjs --repitition-time 1000",
"postversion": "git push && git push --tags", "postversion": "git push && git push --tags",
"debug": "concurrently 'live-server --port=3443 --entry-file=test.html' 'npm run watch'", "debug": "concurrently 'http-server -o test.html' 'npm run watch'",
"trace-deopt": "clear && rollup -c && node --trace-deopt dist/test.js", "trace-deopt": "clear && rollup -c && node --trace-deopt dist/test.cjs",
"trace-opt": "clear && rollup -c && node --trace-opt dist/test.js" "trace-opt": "clear && rollup -c && node --trace-opt dist/test.cjs"
}, },
"files": [ "files": [
"dist/*", "dist/*",
@@ -41,7 +42,12 @@
"url": "https://github.com/yjs/yjs.git" "url": "https://github.com/yjs/yjs.git"
}, },
"keywords": [ "keywords": [
"crdt" "Yjs",
"CRDT",
"offline",
"shared editing",
"concurrency",
"collaboration"
], ],
"author": "Kevin Jahns", "author": "Kevin Jahns",
"email": "kevin.jahns@protonmail.com", "email": "kevin.jahns@protonmail.com",
@@ -51,18 +57,20 @@
}, },
"homepage": "https://yjs.dev", "homepage": "https://yjs.dev",
"dependencies": { "dependencies": {
"lib0": "^0.1.1" "lib0": "^0.2.7"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^11.0.1",
"@rollup/plugin-node-resolve": "^7.0.0",
"concurrently": "^3.6.1", "concurrently": "^3.6.1",
"http-server": "^0.12.1",
"jsdoc": "^3.6.3", "jsdoc": "^3.6.3",
"live-server": "^1.2.1", "markdownlint-cli": "^0.19.0",
"rollup": "^1.20.3", "rollup": "^1.29.1",
"rollup-cli": "^1.0.9", "rollup-cli": "^1.0.9",
"rollup-plugin-node-resolve": "^4.2.4", "standard": "^14.0.0",
"standard": "^11.0.1",
"tui-jsdoc-template": "^1.2.2", "tui-jsdoc-template": "^1.2.2",
"typescript": "^3.6.2", "typescript": "^3.7.5",
"y-protocols": "0.0.6" "y-protocols": "^0.2.0"
} }
} }

View File

@@ -1,4 +1,5 @@
import nodeResolve from 'rollup-plugin-node-resolve' import nodeResolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
const localImports = process.env.LOCALIMPORTS const localImports = process.env.LOCALIMPORTS
@@ -37,23 +38,18 @@ const debugResolve = {
export default [{ export default [{
input: './src/index.js', input: './src/index.js',
output: [{ output: {
name: 'Y', name: 'Y',
file: 'dist/yjs.js', file: 'dist/yjs.cjs',
format: 'cjs', format: 'cjs',
sourcemap: true, sourcemap: true,
paths: path => { paths: path => {
if (/^lib0\//.test(path)) { if (/^lib0\//.test(path)) {
return `lib0/dist/${path.slice(5)}` return `lib0/dist/${path.slice(5, -3)}.cjs`
} }
return path return path
} }
}, { },
name: 'Y',
file: 'dist/yjs.mjs',
format: 'es',
sourcemap: true
}],
external: id => /^lib0\//.test(id) external: id => /^lib0\//.test(id)
}, { }, {
input: './tests/index.js', input: './tests/index.js',
@@ -68,6 +64,24 @@ export default [{
nodeResolve({ nodeResolve({
sourcemap: true, sourcemap: true,
mainFields: ['module', 'browser', 'main'] mainFields: ['module', 'browser', 'main']
}) }),
commonjs()
] ]
}, {
input: './tests/index.js',
output: {
name: 'test',
file: 'dist/tests.cjs',
format: 'cjs',
sourcemap: true
},
plugins: [
debugResolve,
nodeResolve({
sourcemap: true,
mainFields: ['module', 'main']
}),
commonjs()
],
external: ['isomorphic.js']
}] }]

View File

@@ -53,6 +53,7 @@ export {
decodeSnapshot, decodeSnapshot,
encodeSnapshot, encodeSnapshot,
isDeleted, isDeleted,
isParentOf,
equalSnapshots, equalSnapshots,
PermanentUserData // @TODO experimental PermanentUserData // @TODO experimental
} from './internals.js' } from './internals.js'

View File

@@ -24,6 +24,7 @@ export class AbstractStruct {
this.length = length this.length = length
this.deleted = false this.deleted = false
} }
/** /**
* Merge this struct with the item to the right. * Merge this struct with the item to the right.
* This method is already assuming that `this.id.clock + this.length === this.id.clock`. * This method is already assuming that `this.id.clock + this.length === this.id.clock`.
@@ -34,6 +35,7 @@ export class AbstractStruct {
mergeWith (right) { mergeWith (right) {
return false return false
} }
/** /**
* @param {encoding.Encoder} encoder The encoder to write data to. * @param {encoding.Encoder} encoder The encoder to write data to.
* @param {number} offset * @param {number} offset
@@ -43,6 +45,7 @@ export class AbstractStruct {
write (encoder, offset, encodingRef) { write (encoder, offset, encodingRef) {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
*/ */
@@ -69,6 +72,7 @@ export class AbstractStructRef {
*/ */
this.id = id this.id = id
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @return {Array<ID|null>} * @return {Array<ID|null>}
@@ -76,6 +80,7 @@ export class AbstractStructRef {
getMissing (transaction) { getMissing (transaction) {
return this._missing return this._missing
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {StructStore} store * @param {StructStore} store

View File

@@ -18,30 +18,35 @@ export class ContentAny {
*/ */
this.arr = arr this.arr = arr
} }
/** /**
* @return {number} * @return {number}
*/ */
getLength () { getLength () {
return this.arr.length return this.arr.length
} }
/** /**
* @return {Array<any>} * @return {Array<any>}
*/ */
getContent () { getContent () {
return this.arr return this.arr
} }
/** /**
* @return {boolean} * @return {boolean}
*/ */
isCountable () { isCountable () {
return true return true
} }
/** /**
* @return {ContentAny} * @return {ContentAny}
*/ */
copy () { copy () {
return new ContentAny(this.arr) return new ContentAny(this.arr)
} }
/** /**
* @param {number} offset * @param {number} offset
* @return {ContentAny} * @return {ContentAny}
@@ -51,6 +56,7 @@ export class ContentAny {
this.arr = this.arr.slice(0, offset) this.arr = this.arr.slice(0, offset)
return right return right
} }
/** /**
* @param {ContentAny} right * @param {ContentAny} right
* @return {boolean} * @return {boolean}
@@ -59,6 +65,7 @@ export class ContentAny {
this.arr = this.arr.concat(right.arr) this.arr = this.arr.concat(right.arr)
return true return true
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {Item} item * @param {Item} item
@@ -84,6 +91,7 @@ export class ContentAny {
encoding.writeAny(encoder, c) encoding.writeAny(encoder, c)
} }
} }
/** /**
* @return {number} * @return {number}
*/ */

View File

@@ -17,30 +17,35 @@ export class ContentBinary {
constructor (content) { constructor (content) {
this.content = content this.content = content
} }
/** /**
* @return {number} * @return {number}
*/ */
getLength () { getLength () {
return 1 return 1
} }
/** /**
* @return {Array<any>} * @return {Array<any>}
*/ */
getContent () { getContent () {
return [this.content] return [this.content]
} }
/** /**
* @return {boolean} * @return {boolean}
*/ */
isCountable () { isCountable () {
return true return true
} }
/** /**
* @return {ContentBinary} * @return {ContentBinary}
*/ */
copy () { copy () {
return new ContentBinary(this.content) return new ContentBinary(this.content)
} }
/** /**
* @param {number} offset * @param {number} offset
* @return {ContentBinary} * @return {ContentBinary}
@@ -48,6 +53,7 @@ export class ContentBinary {
splice (offset) { splice (offset) {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* @param {ContentBinary} right * @param {ContentBinary} right
* @return {boolean} * @return {boolean}
@@ -55,6 +61,7 @@ export class ContentBinary {
mergeWith (right) { mergeWith (right) {
return false return false
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {Item} item * @param {Item} item
@@ -75,6 +82,7 @@ export class ContentBinary {
write (encoder, offset) { write (encoder, offset) {
encoding.writeVarUint8Array(encoder, this.content) encoding.writeVarUint8Array(encoder, this.content)
} }
/** /**
* @return {number} * @return {number}
*/ */

View File

@@ -17,30 +17,35 @@ export class ContentDeleted {
constructor (len) { constructor (len) {
this.len = len this.len = len
} }
/** /**
* @return {number} * @return {number}
*/ */
getLength () { getLength () {
return this.len return this.len
} }
/** /**
* @return {Array<any>} * @return {Array<any>}
*/ */
getContent () { getContent () {
return [] return []
} }
/** /**
* @return {boolean} * @return {boolean}
*/ */
isCountable () { isCountable () {
return false return false
} }
/** /**
* @return {ContentDeleted} * @return {ContentDeleted}
*/ */
copy () { copy () {
return new ContentDeleted(this.len) return new ContentDeleted(this.len)
} }
/** /**
* @param {number} offset * @param {number} offset
* @return {ContentDeleted} * @return {ContentDeleted}
@@ -50,6 +55,7 @@ export class ContentDeleted {
this.len = offset this.len = offset
return right return right
} }
/** /**
* @param {ContentDeleted} right * @param {ContentDeleted} right
* @return {boolean} * @return {boolean}
@@ -58,6 +64,7 @@ export class ContentDeleted {
this.len += right.len this.len += right.len
return true return true
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {Item} item * @param {Item} item
@@ -66,6 +73,7 @@ export class ContentDeleted {
addToDeleteSet(transaction.deleteSet, item.id, this.len) addToDeleteSet(transaction.deleteSet, item.id, this.len)
item.deleted = true item.deleted = true
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
*/ */
@@ -81,6 +89,7 @@ export class ContentDeleted {
write (encoder, offset) { write (encoder, offset) {
encoding.writeVarUint(encoder, this.len - offset) encoding.writeVarUint(encoder, this.len - offset)
} }
/** /**
* @return {number} * @return {number}
*/ */

View File

@@ -17,30 +17,35 @@ export class ContentEmbed {
constructor (embed) { constructor (embed) {
this.embed = embed this.embed = embed
} }
/** /**
* @return {number} * @return {number}
*/ */
getLength () { getLength () {
return 1 return 1
} }
/** /**
* @return {Array<any>} * @return {Array<any>}
*/ */
getContent () { getContent () {
return [this.embed] return [this.embed]
} }
/** /**
* @return {boolean} * @return {boolean}
*/ */
isCountable () { isCountable () {
return true return true
} }
/** /**
* @return {ContentEmbed} * @return {ContentEmbed}
*/ */
copy () { copy () {
return new ContentEmbed(this.embed) return new ContentEmbed(this.embed)
} }
/** /**
* @param {number} offset * @param {number} offset
* @return {ContentEmbed} * @return {ContentEmbed}
@@ -48,6 +53,7 @@ export class ContentEmbed {
splice (offset) { splice (offset) {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* @param {ContentEmbed} right * @param {ContentEmbed} right
* @return {boolean} * @return {boolean}
@@ -55,6 +61,7 @@ export class ContentEmbed {
mergeWith (right) { mergeWith (right) {
return false return false
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {Item} item * @param {Item} item
@@ -75,6 +82,7 @@ export class ContentEmbed {
write (encoder, offset) { write (encoder, offset) {
encoding.writeVarString(encoder, JSON.stringify(this.embed)) encoding.writeVarString(encoder, JSON.stringify(this.embed))
} }
/** /**
* @return {number} * @return {number}
*/ */

View File

@@ -19,30 +19,35 @@ export class ContentFormat {
this.key = key this.key = key
this.value = value this.value = value
} }
/** /**
* @return {number} * @return {number}
*/ */
getLength () { getLength () {
return 1 return 1
} }
/** /**
* @return {Array<any>} * @return {Array<any>}
*/ */
getContent () { getContent () {
return [] return []
} }
/** /**
* @return {boolean} * @return {boolean}
*/ */
isCountable () { isCountable () {
return false return false
} }
/** /**
* @return {ContentFormat} * @return {ContentFormat}
*/ */
copy () { copy () {
return new ContentFormat(this.key, this.value) return new ContentFormat(this.key, this.value)
} }
/** /**
* @param {number} offset * @param {number} offset
* @return {ContentFormat} * @return {ContentFormat}
@@ -50,6 +55,7 @@ export class ContentFormat {
splice (offset) { splice (offset) {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* @param {ContentFormat} right * @param {ContentFormat} right
* @return {boolean} * @return {boolean}
@@ -57,6 +63,7 @@ export class ContentFormat {
mergeWith (right) { mergeWith (right) {
return false return false
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {Item} item * @param {Item} item
@@ -78,6 +85,7 @@ export class ContentFormat {
encoding.writeVarString(encoder, this.key) encoding.writeVarString(encoder, this.key)
encoding.writeVarString(encoder, JSON.stringify(this.value)) encoding.writeVarString(encoder, JSON.stringify(this.value))
} }
/** /**
* @return {number} * @return {number}
*/ */

View File

@@ -18,30 +18,35 @@ export class ContentJSON {
*/ */
this.arr = arr this.arr = arr
} }
/** /**
* @return {number} * @return {number}
*/ */
getLength () { getLength () {
return this.arr.length return this.arr.length
} }
/** /**
* @return {Array<any>} * @return {Array<any>}
*/ */
getContent () { getContent () {
return this.arr return this.arr
} }
/** /**
* @return {boolean} * @return {boolean}
*/ */
isCountable () { isCountable () {
return true return true
} }
/** /**
* @return {ContentJSON} * @return {ContentJSON}
*/ */
copy () { copy () {
return new ContentJSON(this.arr) return new ContentJSON(this.arr)
} }
/** /**
* @param {number} offset * @param {number} offset
* @return {ContentJSON} * @return {ContentJSON}
@@ -51,6 +56,7 @@ export class ContentJSON {
this.arr = this.arr.slice(0, offset) this.arr = this.arr.slice(0, offset)
return right return right
} }
/** /**
* @param {ContentJSON} right * @param {ContentJSON} right
* @return {boolean} * @return {boolean}
@@ -59,6 +65,7 @@ export class ContentJSON {
this.arr = this.arr.concat(right.arr) this.arr = this.arr.concat(right.arr)
return true return true
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {Item} item * @param {Item} item
@@ -84,6 +91,7 @@ export class ContentJSON {
encoding.writeVarString(encoder, c === undefined ? 'undefined' : JSON.stringify(c)) encoding.writeVarString(encoder, c === undefined ? 'undefined' : JSON.stringify(c))
} }
} }
/** /**
* @return {number} * @return {number}
*/ */

View File

@@ -18,30 +18,35 @@ export class ContentString {
*/ */
this.str = str this.str = str
} }
/** /**
* @return {number} * @return {number}
*/ */
getLength () { getLength () {
return this.str.length return this.str.length
} }
/** /**
* @return {Array<any>} * @return {Array<any>}
*/ */
getContent () { getContent () {
return this.str.split('') return this.str.split('')
} }
/** /**
* @return {boolean} * @return {boolean}
*/ */
isCountable () { isCountable () {
return true return true
} }
/** /**
* @return {ContentString} * @return {ContentString}
*/ */
copy () { copy () {
return new ContentString(this.str) return new ContentString(this.str)
} }
/** /**
* @param {number} offset * @param {number} offset
* @return {ContentString} * @return {ContentString}
@@ -51,6 +56,7 @@ export class ContentString {
this.str = this.str.slice(0, offset) this.str = this.str.slice(0, offset)
return right return right
} }
/** /**
* @param {ContentString} right * @param {ContentString} right
* @return {boolean} * @return {boolean}
@@ -59,6 +65,7 @@ export class ContentString {
this.str += right.str this.str += right.str
return true return true
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {Item} item * @param {Item} item
@@ -79,6 +86,7 @@ export class ContentString {
write (encoder, offset) { write (encoder, offset) {
encoding.writeVarString(encoder, offset === 0 ? this.str : this.str.slice(offset)) encoding.writeVarString(encoder, offset === 0 ? this.str : this.str.slice(offset))
} }
/** /**
* @return {number} * @return {number}
*/ */

View File

@@ -49,30 +49,35 @@ export class ContentType {
*/ */
this.type = type this.type = type
} }
/** /**
* @return {number} * @return {number}
*/ */
getLength () { getLength () {
return 1 return 1
} }
/** /**
* @return {Array<any>} * @return {Array<any>}
*/ */
getContent () { getContent () {
return [this.type] return [this.type]
} }
/** /**
* @return {boolean} * @return {boolean}
*/ */
isCountable () { isCountable () {
return true return true
} }
/** /**
* @return {ContentType} * @return {ContentType}
*/ */
copy () { copy () {
return new ContentType(this.type._copy()) return new ContentType(this.type._copy())
} }
/** /**
* @param {number} offset * @param {number} offset
* @return {ContentType} * @return {ContentType}
@@ -80,6 +85,7 @@ export class ContentType {
splice (offset) { splice (offset) {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* @param {ContentType} right * @param {ContentType} right
* @return {boolean} * @return {boolean}
@@ -87,6 +93,7 @@ export class ContentType {
mergeWith (right) { mergeWith (right) {
return false return false
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {Item} item * @param {Item} item
@@ -94,6 +101,7 @@ export class ContentType {
integrate (transaction, item) { integrate (transaction, item) {
this.type._integrate(transaction.doc, item) this.type._integrate(transaction.doc, item)
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
*/ */
@@ -121,6 +129,7 @@ export class ContentType {
}) })
transaction.changed.delete(this.type) transaction.changed.delete(this.type)
} }
/** /**
* @param {StructStore} store * @param {StructStore} store
*/ */
@@ -139,6 +148,7 @@ export class ContentType {
}) })
this.type._map = new Map() this.type._map = new Map()
} }
/** /**
* @param {encoding.Encoder} encoder * @param {encoding.Encoder} encoder
* @param {number} offset * @param {number} offset
@@ -146,6 +156,7 @@ export class ContentType {
write (encoder, offset) { write (encoder, offset) {
this.type._write(encoder) this.type._write(encoder)
} }
/** /**
* @return {number} * @return {number}
*/ */

View File

@@ -69,6 +69,7 @@ export class GCRef extends AbstractStructRef {
*/ */
this.length = decoding.readVarUint(decoder) this.length = decoding.readVarUint(decoder)
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {StructStore} store * @param {StructStore} store

View File

@@ -35,6 +35,8 @@ import * as set from 'lib0/set.js'
import * as binary from 'lib0/binary.js' import * as binary from 'lib0/binary.js'
/** /**
* @todo This should return several items
*
* @param {StructStore} store * @param {StructStore} store
* @param {ID} id * @param {ID} id
* @return {{item:Item, diff:number}} * @return {{item:Item, diff:number}}
@@ -53,7 +55,7 @@ export const followRedone = (store, id) => {
item = getItem(store, nextID) item = getItem(store, nextID)
diff = nextID.clock - item.id.clock diff = nextID.clock - item.id.clock
nextID = item.redone nextID = item.redone
} while (nextID !== null) } while (nextID !== null && item instanceof Item)
return { return {
item, diff item, diff
} }
@@ -428,6 +430,7 @@ export class Item extends AbstractStruct {
get lastId () { get lastId () {
return createID(this.id.client, this.id.clock + this.length - 1) return createID(this.id.client, this.id.clock + this.length - 1)
} }
/** /**
* Try to merge two items * Try to merge two items
* *
@@ -576,12 +579,14 @@ export class AbstractContent {
getLength () { getLength () {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* @return {Array<any>} * @return {Array<any>}
*/ */
getContent () { getContent () {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* Should return false if this Item is some kind of meta information * Should return false if this Item is some kind of meta information
* (e.g. format information). * (e.g. format information).
@@ -594,12 +599,14 @@ export class AbstractContent {
isCountable () { isCountable () {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* @return {AbstractContent} * @return {AbstractContent}
*/ */
copy () { copy () {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* @param {number} offset * @param {number} offset
* @return {AbstractContent} * @return {AbstractContent}
@@ -607,6 +614,7 @@ export class AbstractContent {
splice (offset) { splice (offset) {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* @param {AbstractContent} right * @param {AbstractContent} right
* @return {boolean} * @return {boolean}
@@ -614,6 +622,7 @@ export class AbstractContent {
mergeWith (right) { mergeWith (right) {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {Item} item * @param {Item} item
@@ -621,18 +630,21 @@ export class AbstractContent {
integrate (transaction, item) { integrate (transaction, item) {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
*/ */
delete (transaction) { delete (transaction) {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* @param {StructStore} store * @param {StructStore} store
*/ */
gc (store) { gc (store) {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* @param {encoding.Encoder} encoder * @param {encoding.Encoder} encoder
* @param {number} offset * @param {number} offset
@@ -640,6 +652,7 @@ export class AbstractContent {
write (encoder, offset) { write (encoder, offset) {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* @return {number} * @return {number}
*/ */
@@ -707,6 +720,7 @@ export class ItemRef extends AbstractStructRef {
this.content = readItemContent(decoder, info) this.content = readItemContent(decoder, info)
this.length = this.content.getLength() this.length = this.content.getLength()
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {StructStore} store * @param {StructStore} store

View File

@@ -30,7 +30,7 @@ import * as encoding from 'lib0/encoding.js' // eslint-disable-line
* @param {EventType} event * @param {EventType} event
*/ */
export const callTypeObservers = (type, transaction, event) => { export const callTypeObservers = (type, transaction, event) => {
callEventHandlerListeners(type._eH, event, transaction) const changedType = type
const changedParentTypes = transaction.changedParentTypes const changedParentTypes = transaction.changedParentTypes
while (true) { while (true) {
// @ts-ignore // @ts-ignore
@@ -40,6 +40,7 @@ export const callTypeObservers = (type, transaction, event) => {
} }
type = type._item.parent type = type._item.parent
} }
callEventHandlerListeners(changedType._eH, event, transaction)
} }
/** /**
@@ -170,7 +171,7 @@ export class AbstractType {
/** /**
* @abstract * @abstract
* @return {Object | Array | number | string} * @return {any}
*/ */
toJSON () {} toJSON () {}
} }
@@ -369,7 +370,7 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
let left = referenceItem let left = referenceItem
const right = referenceItem === null ? parent._start : referenceItem.right const right = referenceItem === null ? parent._start : referenceItem.right
/** /**
* @type {Array<Object|Array|number>} * @type {Array<Object|Array<any>|number>}
*/ */
let jsonContent = [] let jsonContent = []
const packJsonContent = () => { const packJsonContent = () => {
@@ -514,7 +515,7 @@ export const typeMapSet = (transaction, parent, key, value) => {
content = new ContentAny([value]) content = new ContentAny([value])
break break
case Uint8Array: case Uint8Array:
content = new ContentBinary(value) content = new ContentBinary(/** @type {Uint8Array} */ (value))
break break
default: default:
if (value instanceof AbstractType) { if (value instanceof AbstractType) {
@@ -551,7 +552,7 @@ export const typeMapGetAll = (parent) => {
/** /**
* @type {Object<string,any>} * @type {Object<string,any>}
*/ */
let res = {} const res = {}
for (const [key, value] of parent._map) { for (const [key, value] of parent._map) {
if (!value.deleted) { if (!value.deleted) {
res[key] = value.content.getContent()[value.length - 1] res[key] = value.content.getContent()[value.length - 1]

View File

@@ -51,6 +51,7 @@ export class YArray extends AbstractType {
*/ */
this._prelimContent = [] this._prelimContent = []
} }
/** /**
* Integrate this type into the Yjs instance. * Integrate this type into the Yjs instance.
* *
@@ -65,7 +66,7 @@ export class YArray extends AbstractType {
*/ */
_integrate (y, item) { _integrate (y, item) {
super._integrate(y, item) super._integrate(y, item)
this.insert(0, /** @type {Array} */ (this._prelimContent)) this.insert(0, /** @type {Array<any>} */ (this._prelimContent))
this._prelimContent = null this._prelimContent = null
} }
@@ -76,6 +77,7 @@ export class YArray extends AbstractType {
get length () { get length () {
return this._prelimContent === null ? this._length : this._prelimContent.length return this._prelimContent === null ? this._length : this._prelimContent.length
} }
/** /**
* Creates YArrayEvent and calls observers. * Creates YArrayEvent and calls observers.
* *
@@ -110,7 +112,7 @@ export class YArray extends AbstractType {
typeListInsertGenerics(transaction, this, index, content) typeListInsertGenerics(transaction, this, index, content)
}) })
} else { } else {
/** @type {Array} */ (this._prelimContent).splice(index, 0, ...content) /** @type {Array<any>} */ (this._prelimContent).splice(index, 0, ...content)
} }
} }
@@ -135,7 +137,7 @@ export class YArray extends AbstractType {
typeListDelete(transaction, this, index, length) typeListDelete(transaction, this, index, length)
}) })
} else { } else {
/** @type {Array} */ (this._prelimContent).splice(index, length) /** @type {Array<any>} */ (this._prelimContent).splice(index, length)
} }
} }

View File

@@ -53,6 +53,7 @@ export class YMap extends AbstractType {
*/ */
this._prelimContent = new Map() this._prelimContent = new Map()
} }
/** /**
* Integrate this type into the Yjs instance. * Integrate this type into the Yjs instance.
* *
@@ -67,7 +68,7 @@ export class YMap extends AbstractType {
*/ */
_integrate (y, item) { _integrate (y, item) {
super._integrate(y, item) super._integrate(y, item)
for (let [key, value] of /** @type {Map<string, any>} */ (this._prelimContent)) { for (const [key, value] of /** @type {Map<string, any>} */ (this._prelimContent)) {
this.set(key, value) this.set(key, value)
} }
this._prelimContent = null this._prelimContent = null
@@ -99,7 +100,7 @@ export class YMap extends AbstractType {
* @type {Object<string,T>} * @type {Object<string,T>}
*/ */
const map = {} const map = {}
for (let [key, item] of this._map) { for (const [key, item] of this._map) {
if (!item.deleted) { if (!item.deleted) {
const v = item.content.getContent()[item.length - 1] const v = item.content.getContent()[item.length - 1]
map[key] = v instanceof AbstractType ? v.toJSON() : v map[key] = v instanceof AbstractType ? v.toJSON() : v
@@ -145,7 +146,7 @@ export class YMap extends AbstractType {
* @type {Object<string,T>} * @type {Object<string,T>}
*/ */
const map = {} const map = {}
for (let [key, item] of this._map) { for (const [key, item] of this._map) {
if (!item.deleted) { if (!item.deleted) {
f(item.content.getContent()[item.length - 1], key, this) f(item.content.getContent()[item.length - 1], key, this)
} }

View File

@@ -112,10 +112,9 @@ const findNextPosition = (transaction, currentAttributes, left, right, count) =>
* @function * @function
*/ */
const findPosition = (transaction, parent, index) => { const findPosition = (transaction, parent, index) => {
let currentAttributes = new Map() const currentAttributes = new Map()
let left = null const right = parent._start
let right = parent._start return findNextPosition(transaction, currentAttributes, null, right, index)
return findNextPosition(transaction, currentAttributes, left, right, index)
} }
/** /**
@@ -147,7 +146,7 @@ const insertNegatedAttributes = (transaction, parent, left, right, negatedAttrib
left = right left = right
right = right.right right = right.right
} }
for (let [key, val] of negatedAttributes) { for (const [key, val] of negatedAttributes) {
left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentFormat(key, val)) left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentFormat(key, val))
left.integrate(transaction) left.integrate(transaction)
} }
@@ -214,7 +213,7 @@ const minimizeAttributeChanges = (left, right, currentAttributes, attributes) =>
const insertAttributes = (transaction, parent, left, right, currentAttributes, attributes) => { const insertAttributes = (transaction, parent, left, right, currentAttributes, attributes) => {
const negatedAttributes = new Map() const negatedAttributes = new Map()
// insert format-start items // insert format-start items
for (let key in attributes) { for (const key in attributes) {
const val = attributes[key] const val = attributes[key]
const currentVal = currentAttributes.get(key) || null const currentVal = currentAttributes.get(key) || null
if (!equalAttrs(currentVal, val)) { if (!equalAttrs(currentVal, val)) {
@@ -241,7 +240,7 @@ const insertAttributes = (transaction, parent, left, right, currentAttributes, a
* @function * @function
**/ **/
const insertText = (transaction, parent, left, right, currentAttributes, text, attributes) => { const insertText = (transaction, parent, left, right, currentAttributes, text, attributes) => {
for (let [key] of currentAttributes) { for (const [key] of currentAttributes) {
if (attributes[key] === undefined) { if (attributes[key] === undefined) {
attributes[key] = null attributes[key] = null
} }
@@ -281,7 +280,7 @@ const formatText = (transaction, parent, left, right, currentAttributes, length,
while (length > 0 && right !== null) { while (length > 0 && right !== null) {
if (!right.deleted) { if (!right.deleted) {
switch (right.content.constructor) { switch (right.content.constructor) {
case ContentFormat: case ContentFormat: {
const { key, value } = /** @type {ContentFormat} */ (right.content) const { key, value } = /** @type {ContentFormat} */ (right.content)
const attr = attributes[key] const attr = attributes[key]
if (attr !== undefined) { if (attr !== undefined) {
@@ -294,6 +293,7 @@ const formatText = (transaction, parent, left, right, currentAttributes, length,
} }
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (right.content)) updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (right.content))
break break
}
case ContentEmbed: case ContentEmbed:
case ContentString: case ContentString:
if (length < right.length) { if (length < right.length) {
@@ -405,6 +405,7 @@ export class YTextEvent extends YEvent {
*/ */
this._delta = null this._delta = null
} }
/** /**
* Compute the changes in the delta format. * Compute the changes in the delta format.
* A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document. * A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document.
@@ -429,7 +430,10 @@ export class YTextEvent extends YEvent {
/** /**
* @type {Object<string,any>} * @type {Object<string,any>}
*/ */
let attributes = {} // counts added or removed new attributes for retain const attributes = {} // counts added or removed new attributes for retain
/**
* @type {string|object}
*/
let insert = '' let insert = ''
let retain = 0 let retain = 0
let deleteLen = 0 let deleteLen = 0
@@ -448,7 +452,7 @@ export class YTextEvent extends YEvent {
op = { insert } op = { insert }
if (currentAttributes.size > 0) { if (currentAttributes.size > 0) {
op.attributes = {} op.attributes = {}
for (let [key, value] of currentAttributes) { for (const [key, value] of currentAttributes) {
if (value !== null) { if (value !== null) {
op.attributes[key] = value op.attributes[key] = value
} }
@@ -460,7 +464,7 @@ export class YTextEvent extends YEvent {
op = { retain } op = { retain }
if (Object.keys(attributes).length > 0) { if (Object.keys(attributes).length > 0) {
op.attributes = {} op.attributes = {}
for (let key in attributes) { for (const key in attributes) {
op.attributes[key] = attributes[key] op.attributes[key] = attributes[key]
} }
} }
@@ -518,7 +522,7 @@ export class YTextEvent extends YEvent {
retain += item.length retain += item.length
} }
break break
case ContentFormat: case ContentFormat: {
const { key, value } = /** @type {ContentFormat} */ (item.content) const { key, value } = /** @type {ContentFormat} */ (item.content)
if (this.adds(item)) { if (this.adds(item)) {
if (!this.deletes(item)) { if (!this.deletes(item)) {
@@ -570,12 +574,13 @@ export class YTextEvent extends YEvent {
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (item.content)) updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (item.content))
} }
break break
}
} }
item = item.right item = item.right
} }
addOp() addOp()
while (delta.length > 0) { while (delta.length > 0) {
let lastOp = delta[delta.length - 1] const lastOp = delta[delta.length - 1]
if (lastOp.retain !== undefined && lastOp.attributes === undefined) { if (lastOp.retain !== undefined && lastOp.attributes === undefined) {
// retain delta's if they don't assign attributes // retain delta's if they don't assign attributes
delta.pop() delta.pop()
@@ -668,6 +673,16 @@ export class YText extends AbstractType {
return str return str
} }
/**
* Returns the unformatted string representation of this YText type.
*
* @return {string}
* @public
*/
toJSON () {
return this.toString()
}
/** /**
* Apply a {@link Delta} on this shared YText type. * Apply a {@link Delta} on this shared YText type.
* *
@@ -734,7 +749,7 @@ export class YText extends AbstractType {
*/ */
const attributes = {} const attributes = {}
let addAttributes = false let addAttributes = false
for (let [key, value] of currentAttributes) { for (const [key, value] of currentAttributes) {
addAttributes = true addAttributes = true
attributes[key] = value attributes[key] = value
} }
@@ -761,7 +776,7 @@ export class YText extends AbstractType {
while (n !== null) { while (n !== null) {
if (isVisible(n, snapshot) || (prevSnapshot !== undefined && isVisible(n, prevSnapshot))) { if (isVisible(n, snapshot) || (prevSnapshot !== undefined && isVisible(n, prevSnapshot))) {
switch (n.content.constructor) { switch (n.content.constructor) {
case ContentString: case ContentString: {
const cur = currentAttributes.get('ychange') const cur = currentAttributes.get('ychange')
if (snapshot !== undefined && !isVisible(n, snapshot)) { if (snapshot !== undefined && !isVisible(n, snapshot)) {
if (cur === undefined || cur.user !== n.id.client || cur.state !== 'removed') { if (cur === undefined || cur.user !== n.id.client || cur.state !== 'removed') {
@@ -779,6 +794,7 @@ export class YText extends AbstractType {
} }
str += /** @type {ContentString} */ (n.content).str str += /** @type {ContentString} */ (n.content).str
break break
}
case ContentEmbed: case ContentEmbed:
packStr() packStr()
ops.push({ ops.push({
@@ -820,6 +836,7 @@ export class YText extends AbstractType {
const { left, right, currentAttributes } = findPosition(transaction, this, index) const { left, right, currentAttributes } = findPosition(transaction, this, index)
if (!attributes) { if (!attributes) {
attributes = {} attributes = {}
// @ts-ignore
currentAttributes.forEach((v, k) => { attributes[k] = v }) currentAttributes.forEach((v, k) => { attributes[k] = v })
} }
insertText(transaction, this, left, right, currentAttributes, text, attributes) insertText(transaction, this, left, right, currentAttributes, text, attributes)
@@ -891,7 +908,7 @@ export class YText extends AbstractType {
const y = this.doc const y = this.doc
if (y !== null) { if (y !== null) {
transact(y, transaction => { transact(y, transaction => {
let { left, right, currentAttributes } = findPosition(transaction, this, index) const { left, right, currentAttributes } = findPosition(transaction, this, index)
if (right === null) { if (right === null) {
return return
} }

View File

@@ -74,7 +74,7 @@ export class YXmlElement extends YXmlFragment {
const attrs = this.getAttributes() const attrs = this.getAttributes()
const stringBuilder = [] const stringBuilder = []
const keys = [] const keys = []
for (let key in attrs) { for (const key in attrs) {
keys.push(key) keys.push(key)
} }
keys.sort() keys.sort()
@@ -140,7 +140,7 @@ export class YXmlElement extends YXmlFragment {
* Returns all attribute name/value pairs in a JSON Object. * Returns all attribute name/value pairs in a JSON Object.
* *
* @param {Snapshot} [snapshot] * @param {Snapshot} [snapshot]
* @return {Object} A JSON Object that describes the attributes. * @return {Object<string, any>} A JSON Object that describes the attributes.
* *
* @public * @public
*/ */
@@ -165,8 +165,8 @@ export class YXmlElement extends YXmlFragment {
*/ */
toDOM (_document = document, hooks = {}, binding) { toDOM (_document = document, hooks = {}, binding) {
const dom = _document.createElement(this.nodeName) const dom = _document.createElement(this.nodeName)
let attrs = this.getAttributes() const attrs = this.getAttributes()
for (let key in attrs) { for (const key in attrs) {
dom.setAttribute(key, attrs[key]) dom.setAttribute(key, attrs[key])
} }
typeListForEach(this, yxml => { typeListForEach(this, yxml => {

View File

@@ -68,6 +68,7 @@ export class YXmlTreeWalker {
[Symbol.iterator] () { [Symbol.iterator] () {
return this return this
} }
/** /**
* Get the next node. * Get the next node.
* *
@@ -130,6 +131,7 @@ export class YXmlFragment extends AbstractType {
*/ */
this._prelimContent = [] this._prelimContent = []
} }
/** /**
* Integrate this type into the Yjs instance. * Integrate this type into the Yjs instance.
* *
@@ -143,7 +145,7 @@ export class YXmlFragment extends AbstractType {
*/ */
_integrate (y, item) { _integrate (y, item) {
super._integrate(y, item) super._integrate(y, item)
this.insert(0, /** @type {Array} */ (this._prelimContent)) this.insert(0, /** @type {Array<any>} */ (this._prelimContent))
this._prelimContent = null this._prelimContent = null
} }
@@ -240,6 +242,9 @@ export class YXmlFragment extends AbstractType {
return typeListMap(this, xml => xml.toString()).join('') return typeListMap(this, xml => xml.toString()).join('')
} }
/**
* @return {string}
*/
toJSON () { toJSON () {
return this.toString() return this.toString()
} }
@@ -307,6 +312,7 @@ export class YXmlFragment extends AbstractType {
this._prelimContent.splice(index, length) this._prelimContent.splice(index, length)
} }
} }
/** /**
* Transforms this YArray to a JavaScript Array. * Transforms this YArray to a JavaScript Array.
* *
@@ -315,6 +321,7 @@ export class YXmlFragment extends AbstractType {
toArray () { toArray () {
return typeListToArray(this) return typeListToArray(this)
} }
/** /**
* Transform the properties of this type to binary and write it to an * Transform the properties of this type to binary and write it to an
* BinaryEncoder. * BinaryEncoder.

View File

@@ -12,6 +12,7 @@ export class YXmlText extends YText {
_copy () { _copy () {
return new YXmlText() return new YXmlText()
} }
/** /**
* Creates a Dom Element that mirrors this YXmlText. * Creates a Dom Element that mirrors this YXmlText.
* *
@@ -39,9 +40,9 @@ export class YXmlText extends YText {
// @ts-ignore // @ts-ignore
return this.toDelta().map(delta => { return this.toDelta().map(delta => {
const nestedNodes = [] const nestedNodes = []
for (let nodeName in delta.attributes) { for (const nodeName in delta.attributes) {
const attrs = [] const attrs = []
for (let key in delta.attributes[nodeName]) { for (const key in delta.attributes[nodeName]) {
attrs.push({ key, value: delta.attributes[nodeName][key] }) attrs.push({ key, value: delta.attributes[nodeName][key] })
} }
// sort attributes to get a unique order // sort attributes to get a unique order
@@ -56,7 +57,7 @@ export class YXmlText extends YText {
const node = nestedNodes[i] const node = nestedNodes[i]
str += `<${node.nodeName}` str += `<${node.nodeName}`
for (let j = 0; j < node.attrs.length; j++) { for (let j = 0; j < node.attrs.length; j++) {
const attr = node.attrs[i] const attr = node.attrs[j]
str += ` ${attr.key}="${attr.value}"` str += ` ${attr.key}="${attr.value}"`
} }
str += '>' str += '>'
@@ -69,6 +70,9 @@ export class YXmlText extends YText {
}).join('') }).join('')
} }
/**
* @return {string}
*/
toJSON () { toJSON () {
return this.toString() return this.toString()
} }

View File

@@ -23,11 +23,12 @@ import * as map from 'lib0/map.js'
*/ */
export class Doc extends Observable { export class Doc extends Observable {
/** /**
* @param {Object|undefined} conf configuration * @param {Object} conf configuration
* @param {boolean} [conf.gc] Disable garbage collection (default: gc=true)
*/ */
constructor (conf = {}) { constructor ({ gc = true } = {}) {
super() super()
this.gc = conf.gc || true this.gc = gc
this.clientID = random.uint32() this.clientID = random.uint32()
/** /**
* @type {Map<string, AbstractType<YEvent>>} * @type {Map<string, AbstractType<YEvent>>}
@@ -45,6 +46,7 @@ export class Doc extends Observable {
*/ */
this._transactionCleanups = [] this._transactionCleanups = []
} }
/** /**
* Changes that happen inside of a transaction are bundled. This means that * Changes that happen inside of a transaction are bundled. This means that
* the observer fires _after_ the transaction is finished and that all changes * the observer fires _after_ the transaction is finished and that all changes
@@ -59,6 +61,7 @@ export class Doc extends Observable {
transact (f, origin = null) { transact (f, origin = null) {
transact(this, f, origin) transact(this, f, origin)
} }
/** /**
* Define a shared data type. * Define a shared data type.
* *
@@ -117,6 +120,7 @@ export class Doc extends Observable {
} }
return type return type
} }
/** /**
* @template T * @template T
* @param {string} name * @param {string} name
@@ -128,6 +132,7 @@ export class Doc extends Observable {
// @ts-ignore // @ts-ignore
return this.get(name, YArray) return this.get(name, YArray)
} }
/** /**
* @param {string} name * @param {string} name
* @return {YText} * @return {YText}
@@ -138,6 +143,7 @@ export class Doc extends Observable {
// @ts-ignore // @ts-ignore
return this.get(name, YText) return this.get(name, YText)
} }
/** /**
* @param {string} name * @param {string} name
* @return {YMap<any>} * @return {YMap<any>}
@@ -148,6 +154,7 @@ export class Doc extends Observable {
// @ts-ignore // @ts-ignore
return this.get(name, YMap) return this.get(name, YMap)
} }
/** /**
* @param {string} name * @param {string} name
* @return {YXmlFragment} * @return {YXmlFragment}
@@ -158,6 +165,7 @@ export class Doc extends Observable {
// @ts-ignore // @ts-ignore
return this.get(name, YXmlFragment) return this.get(name, YXmlFragment)
} }
/** /**
* Emit `destroy` event and unregister all event handlers. * Emit `destroy` event and unregister all event handlers.
* *
@@ -167,6 +175,7 @@ export class Doc extends Observable {
this.emit('destroyed', [true]) this.emit('destroyed', [true])
super.destroy() super.destroy()
} }
/** /**
* @param {string} eventName * @param {string} eventName
* @param {function} f * @param {function} f
@@ -174,6 +183,7 @@ export class Doc extends Observable {
on (eventName, f) { on (eventName, f) {
super.on(eventName, f) super.on(eventName, f)
} }
/** /**
* @param {string} eventName * @param {string} eventName
* @param {function} f * @param {function} f

View File

@@ -81,7 +81,7 @@ export const readID = decoder =>
*/ */
export const findRootTypeKey = type => { export const findRootTypeKey = type => {
// @ts-ignore _y must be defined, otherwise unexpected case // @ts-ignore _y must be defined, otherwise unexpected case
for (let [key, value] of type.doc.share) { for (const [key, value] of type.doc.share) {
if (value === type) { if (value === type) {
return key return key
} }

View File

@@ -15,15 +15,14 @@ import { mergeDeleteSets, isDeleted } from './DeleteSet.js'
export class PermanentUserData { export class PermanentUserData {
/** /**
* @param {Doc} doc * @param {Doc} doc
* @param {string} key * @param {YMap<any>} [storeType]
*/ */
constructor (doc, key = 'users') { constructor (doc, storeType = doc.getMap('users')) {
const users = doc.getMap(key)
/** /**
* @type {Map<string,DeleteSet>} * @type {Map<string,DeleteSet>}
*/ */
const dss = new Map() const dss = new Map()
this.yusers = users this.yusers = storeType
this.doc = doc this.doc = doc
/** /**
* Maps from clientid to userDescription * Maps from clientid to userDescription
@@ -59,20 +58,23 @@ export class PermanentUserData {
ids.forEach(addClientId) ids.forEach(addClientId)
} }
// observe users // observe users
users.observe(event => { storeType.observe(event => {
event.keysChanged.forEach(userDescription => event.keysChanged.forEach(userDescription =>
initUser(users.get(userDescription), userDescription) initUser(storeType.get(userDescription), userDescription)
) )
}) })
// add intial data // add intial data
users.forEach(initUser) storeType.forEach(initUser)
} }
/** /**
* @param {Doc} doc * @param {Doc} doc
* @param {number} clientid * @param {number} clientid
* @param {string} userDescription * @param {string} userDescription
* @param {Object} [conf]
* @param {function(Transaction, DeleteSet):boolean} [conf.filter]
*/ */
setUserMapping (doc, clientid, userDescription) { setUserMapping (doc, clientid, userDescription, { filter = () => true } = {}) {
const users = this.yusers const users = this.yusers
let user = users.get(userDescription) let user = users.get(userDescription)
if (!user) { if (!user) {
@@ -83,35 +85,40 @@ export class PermanentUserData {
} }
user.get('ids').push([clientid]) user.get('ids').push([clientid])
users.observe(event => { users.observe(event => {
const userOverwrite = users.get(userDescription) setTimeout(() => {
if (userOverwrite !== user) { const userOverwrite = users.get(userDescription)
// user was overwritten, port all data over to the next user object if (userOverwrite !== user) {
// @todo Experiment with Y.Sets here // user was overwritten, port all data over to the next user object
user = userOverwrite // @todo Experiment with Y.Sets here
// @todo iterate over old type user = userOverwrite
this.clients.forEach((_userDescription, clientid) => { // @todo iterate over old type
if (userDescription === _userDescription) { this.clients.forEach((_userDescription, clientid) => {
user.get('ids').push([clientid]) if (userDescription === _userDescription) {
user.get('ids').push([clientid])
}
})
const encoder = encoding.createEncoder()
const ds = this.dss.get(userDescription)
if (ds) {
writeDeleteSet(encoder, ds)
user.get('ds').push([encoding.toUint8Array(encoder)])
} }
})
const encoder = encoding.createEncoder()
const ds = this.dss.get(userDescription)
if (ds) {
writeDeleteSet(encoder, ds)
user.get('ds').push([encoding.toUint8Array(encoder)])
} }
} }, 0)
}) })
doc.on('afterTransaction', /** @param {Transaction} transaction */ transaction => { doc.on('afterTransaction', /** @param {Transaction} transaction */ transaction => {
const yds = user.get('ds') setTimeout(() => {
const ds = transaction.deleteSet const yds = user.get('ds')
if (transaction.local && ds.clients.size > 0) { const ds = transaction.deleteSet
const encoder = encoding.createEncoder() if (transaction.local && ds.clients.size > 0 && filter(transaction, ds)) {
writeDeleteSet(encoder, ds) const encoder = encoding.createEncoder()
yds.push([encoding.toUint8Array(encoder)]) writeDeleteSet(encoder, ds)
} yds.push([encoding.toUint8Array(encoder)])
}
})
}) })
} }
/** /**
* @param {number} clientid * @param {number} clientid
* @return {any} * @return {any}
@@ -119,6 +126,7 @@ export class PermanentUserData {
getUserByClientId (clientid) { getUserByClientId (clientid) {
return this.clients.get(clientid) || null return this.clients.get(clientid) || null
} }
/** /**
* @param {ID} id * @param {ID} id
* @return {string | null} * @return {string | null}

View File

@@ -63,7 +63,7 @@ export class RelativePosition {
} }
/** /**
* @param {Object} json * @param {any} json
* @return {RelativePosition} * @return {RelativePosition}
* *
* @function * @function
@@ -228,7 +228,7 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
return null return null
} }
type = right.parent type = right.parent
if (type._item !== null && !type._item.deleted) { if (type._item === null || !type._item.deleted) {
index = right.deleted || !right.countable ? 0 : res.diff index = right.deleted || !right.countable ? 0 : res.diff
let n = right.left let n = right.left
while (n !== null) { while (n !== null) {

View File

@@ -185,7 +185,7 @@ export const getItem = (store, id) => find(store, id)
*/ */
export const findIndexCleanStart = (transaction, structs, clock) => { export const findIndexCleanStart = (transaction, structs, clock) => {
const index = findIndexSS(structs, clock) const index = findIndexSS(structs, clock)
let struct = structs[index] const struct = structs[index]
if (struct.id.clock < clock && struct instanceof Item) { if (struct.id.clock < clock && struct instanceof Item) {
structs.splice(index + 1, 0, splitItem(transaction, struct, clock - struct.id.clock)) structs.splice(index + 1, 0, splitItem(transaction, struct, clock - struct.id.clock))
return index + 1 return index + 1

View File

@@ -17,6 +17,7 @@ import * as encoding from 'lib0/encoding.js'
import * as map from 'lib0/map.js' import * as map from 'lib0/map.js'
import * as math from 'lib0/math.js' import * as math from 'lib0/math.js'
import * as set from 'lib0/set.js' import * as set from 'lib0/set.js'
import { callAll } from 'lib0/function.js'
/** /**
* A transaction is created for every change on the Yjs model. It is possible * A transaction is created for every change on the Yjs model. It is possible
@@ -72,7 +73,7 @@ export class Transaction {
/** /**
* All types that were directly modified (property added or child * All types that were directly modified (property added or child
* inserted/deleted). New types are not included in this Set. * inserted/deleted). New types are not included in this Set.
* Maps from type to parentSubs (`item._parentSub = null` for YArray) * Maps from type to parentSubs (`item.parentSub = null` for YArray)
* @type {Map<AbstractType<YEvent>,Set<String|null>>} * @type {Map<AbstractType<YEvent>,Set<String|null>>}
*/ */
this.changed = new Map() this.changed = new Map()
@@ -144,6 +145,164 @@ export const addChangedTypeToTransaction = (transaction, type, parentSub) => {
} }
} }
/**
* @param {Array<Transaction>} transactionCleanups
* @param {number} i
*/
const cleanupTransactions = (transactionCleanups, i) => {
if (i < transactionCleanups.length) {
const transaction = transactionCleanups[i]
const doc = transaction.doc
const store = doc.store
const ds = transaction.deleteSet
try {
sortAndMergeDeleteSet(ds)
transaction.afterState = getStateVector(transaction.doc.store)
doc._transaction = null
doc.emit('beforeObserverCalls', [transaction, doc])
/**
* An array of event callbacks.
*
* Each callback is called even if the other ones throw errors.
*
* @type {Array<function():void>}
*/
const fs = []
// observe events on changed types
transaction.changed.forEach((subs, itemtype) =>
fs.push(() => {
if (itemtype._item === null || !itemtype._item.deleted) {
itemtype._callObserver(transaction, subs)
}
})
)
fs.push(() => {
// deep observe events
transaction.changedParentTypes.forEach((events, type) =>
fs.push(() => {
// We need to think about the possibility that the user transforms the
// Y.Doc in the event.
if (type._item === null || !type._item.deleted) {
events = events
.filter(event =>
event.target._item === null || !event.target._item.deleted
)
events
.forEach(event => {
event.currentTarget = type
})
// We don't need to check for events.length
// because we know it has at least one element
callEventHandlerListeners(type._dEH, events, transaction)
}
})
)
fs.push(() => doc.emit('afterTransaction', [transaction, doc]))
})
callAll(fs, [])
} finally {
/**
* @param {Array<AbstractStruct>} structs
* @param {number} pos
*/
const tryToMergeWithLeft = (structs, pos) => {
const left = structs[pos - 1]
const right = 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))
}
}
}
}
// Replace deleted items with ItemDeleted / GC.
// This is where content is actually remove from the Yjs Doc.
if (doc.gc) {
for (const [client, deleteItems] of ds.clients) {
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
for (let di = deleteItems.length - 1; di >= 0; di--) {
const deleteItem = deleteItems[di]
const endDeleteItemClock = deleteItem.clock + deleteItem.len
for (
let si = findIndexSS(structs, deleteItem.clock), struct = structs[si];
si < structs.length && struct.id.clock < endDeleteItemClock;
struct = structs[++si]
) {
const struct = structs[si]
if (deleteItem.clock + deleteItem.len <= struct.id.clock) {
break
}
if (struct instanceof Item && struct.deleted && !struct.keep) {
struct.gc(store, false)
}
}
}
}
}
// 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))
for (let di = deleteItems.length - 1; di >= 0; di--) {
const deleteItem = deleteItems[di]
// start with merging the item next to the last deleted item
const mostRightIndexToCheck = math.min(structs.length - 1, 1 + findIndexSS(structs, deleteItem.clock + deleteItem.len - 1))
for (
let si = mostRightIndexToCheck, struct = structs[si];
si > 0 && struct.id.clock >= deleteItem.clock;
struct = structs[--si]
) {
tryToMergeWithLeft(structs, si)
}
}
}
// on all affected store.clients props, try to merge
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))
// 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--) {
tryToMergeWithLeft(structs, 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))
const replacedStructPos = findIndexSS(structs, clock)
if (replacedStructPos + 1 < structs.length) {
tryToMergeWithLeft(structs, replacedStructPos + 1)
}
if (replacedStructPos > 0) {
tryToMergeWithLeft(structs, replacedStructPos)
}
}
// @todo Merge all the transactions into one and provide send the data as a single update message
doc.emit('afterTransactionCleanup', [transaction, doc])
if (doc._observers.has('update')) {
const updateMessage = computeUpdateMessageFromTransaction(transaction)
if (updateMessage !== null) {
doc.emit('update', [encoding.toUint8Array(updateMessage), transaction.origin, doc])
}
}
if (transactionCleanups.length <= i + 1) {
doc._transactionCleanups = []
} else {
cleanupTransactions(transactionCleanups, i + 1)
}
}
}
}
/** /**
* Implements the functionality of `y.transact(()=>{..})` * Implements the functionality of `y.transact(()=>{..})`
* *
@@ -169,134 +328,13 @@ export const transact = (doc, f, origin = null, local = true) => {
if (initialCall && transactionCleanups[0] === doc._transaction) { if (initialCall && transactionCleanups[0] === doc._transaction) {
// The first transaction ended, now process observer calls. // The first transaction ended, now process observer calls.
// Observer call may create new transactions for which we need to call the observers and do cleanup. // Observer call may create new transactions for which we need to call the observers and do cleanup.
// We don't want to nest these calls, so we execute these calls one after another // We don't want to nest these calls, so we execute these calls one after
for (let i = 0; i < transactionCleanups.length; i++) { // another.
const transaction = transactionCleanups[i] // Also we need to ensure that all cleanups are called, even if the
const store = transaction.doc.store // observes throw errors.
const ds = transaction.deleteSet // This file is full of hacky try {} finally {} blocks to ensure that an
sortAndMergeDeleteSet(ds) // event can throw errors and also that the cleanup is called.
transaction.afterState = getStateVector(transaction.doc.store) cleanupTransactions(transactionCleanups, 0)
doc._transaction = null
doc.emit('beforeObserverCalls', [transaction, doc])
// emit change events on changed types
transaction.changed.forEach((subs, itemtype) => {
if (itemtype._item === null || !itemtype._item.deleted) {
itemtype._callObserver(transaction, subs)
}
})
transaction.changedParentTypes.forEach((events, type) => {
// We need to think about the possibility that the user transforms the
// Y.Doc in the event.
if (type._item === null || !type._item.deleted) {
events = events
.filter(event =>
event.target._item === null || !event.target._item.deleted
)
events
.forEach(event => {
event.currentTarget = type
})
// We don't need to check for events.length
// because we know it has at least one element
callEventHandlerListeners(type._dEH, events, transaction)
}
})
doc.emit('afterTransaction', [transaction, doc])
/**
* @param {Array<AbstractStruct>} structs
* @param {number} pos
*/
const tryToMergeWithLeft = (structs, pos) => {
const left = structs[pos - 1]
const right = 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))
}
}
}
}
// Replace deleted items with ItemDeleted / GC.
// This is where content is actually remove from the Yjs Doc.
if (doc.gc) {
for (const [client, deleteItems] of ds.clients) {
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
for (let di = deleteItems.length - 1; di >= 0; di--) {
const deleteItem = deleteItems[di]
const endDeleteItemClock = deleteItem.clock + deleteItem.len
for (
let si = findIndexSS(structs, deleteItem.clock), struct = structs[si];
si < structs.length && struct.id.clock < endDeleteItemClock;
struct = structs[++si]
) {
const struct = structs[si]
if (deleteItem.clock + deleteItem.len <= struct.id.clock) {
break
}
if (struct instanceof Item && struct.deleted && !struct.keep) {
struct.gc(store, false)
}
}
}
}
}
// 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))
for (let di = deleteItems.length - 1; di >= 0; di--) {
const deleteItem = deleteItems[di]
// start with merging the item next to the last deleted item
const mostRightIndexToCheck = math.min(structs.length - 1, 1 + findIndexSS(structs, deleteItem.clock + deleteItem.len - 1))
for (
let si = mostRightIndexToCheck, struct = structs[si];
si > 0 && struct.id.clock >= deleteItem.clock;
struct = structs[--si]
) {
tryToMergeWithLeft(structs, si)
}
}
}
// on all affected store.clients props, try to merge
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))
// 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--) {
tryToMergeWithLeft(structs, 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))
const replacedStructPos = findIndexSS(structs, clock)
if (replacedStructPos + 1 < structs.length) {
tryToMergeWithLeft(structs, replacedStructPos + 1)
}
if (replacedStructPos > 0) {
tryToMergeWithLeft(structs, replacedStructPos)
}
}
// @todo Merge all the transactions into one and provide send the data as a single update message
doc.emit('afterTransactionCleanup', [transaction, doc])
if (doc._observers.has('update')) {
const updateMessage = computeUpdateMessageFromTransaction(transaction)
if (updateMessage !== null) {
doc.emit('update', [encoding.toUint8Array(updateMessage), transaction.origin, doc])
}
}
}
doc._transactionCleanups = []
} }
} }
} }

View File

@@ -9,6 +9,7 @@ import {
createID, createID,
followRedone, followRedone,
getItemCleanStart, getItemCleanStart,
getState,
Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line
} from '../internals.js' } from '../internals.js'
@@ -49,35 +50,53 @@ const popStackItem = (undoManager, stack, eventType) => {
transact(doc, transaction => { transact(doc, transaction => {
while (stack.length > 0 && result === null) { while (stack.length > 0 && result === null) {
const store = doc.store const store = doc.store
const clientID = doc.clientID
const stackItem = /** @type {StackItem} */ (stack.pop()) const stackItem = /** @type {StackItem} */ (stack.pop())
const stackStartClock = stackItem.start
const stackEndClock = stackItem.start + stackItem.len
const itemsToRedo = new Set() const itemsToRedo = new Set()
// @todo iterateStructs should not need the structs parameter
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(clientID))
let performedChange = false let performedChange = false
if (stackStartClock !== stackEndClock) {
// make sure structs don't overlap with the range of created operations [stackItem.start, stackItem.start + stackItem.end)
getItemCleanStart(transaction, createID(clientID, stackStartClock))
if (stackEndClock < getState(doc.store, clientID)) {
getItemCleanStart(transaction, createID(clientID, stackEndClock))
}
}
iterateDeletedStructs(transaction, stackItem.ds, struct => { iterateDeletedStructs(transaction, stackItem.ds, struct => {
if (struct instanceof Item && scope.some(type => isParentOf(type, struct))) { if (
struct instanceof Item &&
scope.some(type => isParentOf(type, struct)) &&
// Never redo structs in [stackItem.start, stackItem.start + stackItem.end) because they were created and deleted in the same capture interval.
!(struct.id.client === clientID && struct.id.clock >= stackStartClock && struct.id.clock < stackEndClock)
) {
itemsToRedo.add(struct) itemsToRedo.add(struct)
} }
}) })
itemsToRedo.forEach(item => { itemsToRedo.forEach(struct => {
performedChange = redoItem(transaction, item, itemsToRedo) !== null || performedChange performedChange = redoItem(transaction, struct, itemsToRedo) !== null || performedChange
}) })
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(doc.clientID))
/** /**
* @type {Array<Item>} * @type {Array<Item>}
*/ */
const itemsToDelete = [] const itemsToDelete = []
iterateStructs(transaction, structs, stackItem.start, stackItem.len, struct => { iterateStructs(transaction, structs, stackStartClock, stackItem.len, struct => {
if (struct instanceof Item && !struct.deleted && scope.some(type => isParentOf(type, /** @type {Item} */ (struct)))) { if (struct instanceof Item) {
if (struct.redone !== null) { if (struct.redone !== null) {
let { item, diff } = followRedone(store, struct.id) let { item, diff } = followRedone(store, struct.id)
if (diff > 0) { if (diff > 0) {
item = getItemCleanStart(transaction, createID(item.id.client, item.id.clock + diff)) item = getItemCleanStart(transaction, createID(item.id.client, item.id.clock + diff))
} }
if (item.length > stackItem.len) { if (item.length > stackItem.len) {
getItemCleanStart(transaction, createID(item.id.client, item.id.clock + stackItem.len)) getItemCleanStart(transaction, createID(item.id.client, stackEndClock))
} }
struct = item struct = item
} }
itemsToDelete.push(struct) if (!struct.deleted && scope.some(type => isParentOf(type, /** @type {Item} */ (struct)))) {
itemsToDelete.push(struct)
}
} }
}) })
// We want to delete in reverse order so that children are deleted before // We want to delete in reverse order so that children are deleted before
@@ -111,9 +130,9 @@ const popStackItem = (undoManager, stack, eventType) => {
/** /**
* Fires 'stack-item-added' event when a stack item was added to either the undo- or * Fires 'stack-item-added' event when a stack item was added to either the undo- or
* the redo-stack. You may store additional stack information via the * the redo-stack. You may store additional stack information via the
* metadata property on `event.stackItem.metadata` (it is a `Map` of metadata properties). * metadata property on `event.stackItem.meta` (it is a `Map` of metadata properties).
* Fires 'stack-item-popped' event when a stack item was popped from either the * Fires 'stack-item-popped' event when a stack item was popped from either the
* undo- or the redo-stack. You may restore the saved stack information from `event.stackItem.metadata`. * undo- or the redo-stack. You may restore the saved stack information from `event.stackItem.meta`.
* *
* @extends {Observable<'stack-item-added'|'stack-item-popped'>} * @extends {Observable<'stack-item-added'|'stack-item-popped'>}
*/ */

View File

@@ -56,6 +56,8 @@ export class YEvent {
/** /**
* Check if a struct is deleted by this event. * Check if a struct is deleted by this event.
* *
* In contrast to change.deleted, this method also returns true if the struct was added and then deleted.
*
* @param {AbstractStruct} struct * @param {AbstractStruct} struct
* @return {boolean} * @return {boolean}
*/ */
@@ -66,6 +68,8 @@ export class YEvent {
/** /**
* Check if a struct is added by this event. * Check if a struct is added by this event.
* *
* In contrast to change.deleted, this method also returns true if the struct was added and then deleted.
*
* @param {AbstractStruct} struct * @param {AbstractStruct} struct
* @return {boolean} * @return {boolean}
*/ */
@@ -106,7 +110,7 @@ export class YEvent {
} }
for (let item = target._start; item !== null; item = item.right) { for (let item = target._start; item !== null; item = item.right) {
if (item.deleted) { if (item.deleted) {
if (this.deletes(item)) { if (this.deletes(item) && !this.adds(item)) {
if (lastOp === null || lastOp.delete === undefined) { if (lastOp === null || lastOp.delete === undefined) {
packOp() packOp()
lastOp = { delete: 0 } lastOp = { delete: 0 }
@@ -177,7 +181,7 @@ export class YEvent {
}) })
this._changes = changes this._changes = changes
} }
return changes return /** @type {any} */ (changes)
} }
} }

View File

@@ -12,6 +12,7 @@ import * as prng from 'lib0/prng.js'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js' import * as decoding from 'lib0/decoding.js'
import * as syncProtocol from 'y-protocols/sync.js' import * as syncProtocol from 'y-protocols/sync.js'
import * as object from 'lib0/object.js'
export * from '../src/internals.js' export * from '../src/internals.js'
/** /**
@@ -55,6 +56,7 @@ export class TestYInstance extends Doc {
}) })
this.connect() this.connect()
} }
/** /**
* Disconnect from TestConnector. * Disconnect from TestConnector.
*/ */
@@ -62,6 +64,7 @@ export class TestYInstance extends Doc {
this.receiving = new Map() this.receiving = new Map()
this.tc.onlineConns.delete(this) this.tc.onlineConns.delete(this)
} }
/** /**
* Append yourself to the list of known Y instances in testconnector. * Append yourself to the list of known Y instances in testconnector.
* Also initiate sync with all clients. * Also initiate sync with all clients.
@@ -83,6 +86,7 @@ export class TestYInstance extends Doc {
}) })
} }
} }
/** /**
* Receive a message from another client. This message is only appended to the list of receiving messages. * Receive a message from another client. This message is only appended to the list of receiving messages.
* TestConnector decides when this client actually reads this message. * TestConnector decides when this client actually reads this message.
@@ -124,6 +128,7 @@ export class TestConnector {
*/ */
this.prng = gen this.prng = gen
} }
/** /**
* Create a new Y instance and add it to the list of connections * Create a new Y instance and add it to the list of connections
* @param {number} clientID * @param {number} clientID
@@ -131,6 +136,7 @@ export class TestConnector {
createY (clientID) { createY (clientID) {
return new TestYInstance(this, clientID) return new TestYInstance(this, clientID)
} }
/** /**
* Choose random connection and flush a random message from a random sender. * Choose random connection and flush a random message from a random sender.
* *
@@ -162,6 +168,7 @@ export class TestConnector {
} }
return false return false
} }
/** /**
* @return {boolean} True iff this function actually flushed something * @return {boolean} True iff this function actually flushed something
*/ */
@@ -172,16 +179,20 @@ export class TestConnector {
} }
return didSomething return didSomething
} }
reconnectAll () { reconnectAll () {
this.allConns.forEach(conn => conn.connect()) this.allConns.forEach(conn => conn.connect())
} }
disconnectAll () { disconnectAll () {
this.allConns.forEach(conn => conn.disconnect()) this.allConns.forEach(conn => conn.disconnect())
} }
syncAll () { syncAll () {
this.reconnectAll() this.reconnectAll()
this.flushAllMessages() this.flushAllMessages()
} }
/** /**
* @return {boolean} Whether it was possible to disconnect a randon connection. * @return {boolean} Whether it was possible to disconnect a randon connection.
*/ */
@@ -192,6 +203,7 @@ export class TestConnector {
prng.oneOf(this.prng, Array.from(this.onlineConns)).disconnect() prng.oneOf(this.prng, Array.from(this.onlineConns)).disconnect()
return true return true
} }
/** /**
* @return {boolean} Whether it was possible to reconnect a random connection. * @return {boolean} Whether it was possible to reconnect a random connection.
*/ */
@@ -270,12 +282,12 @@ export const compare = users => {
// Test Map iterator // Test Map iterator
const ymapkeys = Array.from(users[0].getMap('map').keys()) const ymapkeys = Array.from(users[0].getMap('map').keys())
t.assert(ymapkeys.length === Object.keys(userMapValues[0]).length) t.assert(ymapkeys.length === Object.keys(userMapValues[0]).length)
ymapkeys.forEach(key => t.assert(userMapValues[0].hasOwnProperty(key))) ymapkeys.forEach(key => t.assert(object.hasProperty(userMapValues[0], key)))
/** /**
* @type {Object<string,any>} * @type {Object<string,any>}
*/ */
const mapRes = {} const mapRes = {}
for (let [k, v] of users[0].getMap('map')) { for (const [k, v] of users[0].getMap('map')) {
mapRes[k] = v instanceof Y.AbstractType ? v.toJSON() : v mapRes[k] = v instanceof Y.AbstractType ? v.toJSON() : v
} }
t.compare(userMapValues[0], mapRes) t.compare(userMapValues[0], mapRes)

View File

@@ -13,6 +13,23 @@ import * as t from 'lib0/testing.js'
export const testUndoText = tc => { export const testUndoText = tc => {
const { testConnector, text0, text1 } = init(tc, { users: 3 }) const { testConnector, text0, text1 } = init(tc, { users: 3 })
const undoManager = new UndoManager(text0) const undoManager = new UndoManager(text0)
// items that are added & deleted in the same transaction won't be undo
text0.insert(0, 'test')
text0.delete(0, 4)
undoManager.undo()
t.assert(text0.toString() === '')
// follow redone items
text0.insert(0, 'a')
undoManager.stopCapturing()
text0.delete(0, 1)
undoManager.stopCapturing()
undoManager.undo()
t.assert(text0.toString() === 'a')
undoManager.undo()
t.assert(text0.toString() === '')
text0.insert(0, 'abc') text0.insert(0, 'abc')
text1.insert(0, 'xyz') text1.insert(0, 'xyz')
testConnector.syncAll() testConnector.syncAll()
@@ -52,11 +69,11 @@ export const testUndoMap = tc => {
const subType = new Y.Map() const subType = new Y.Map()
map0.set('a', subType) map0.set('a', subType)
subType.set('x', 42) subType.set('x', 42)
t.compare(map0.toJSON(), /** @type {any} */ ({ 'a': { x: 42 } })) t.compare(map0.toJSON(), /** @type {any} */ ({ a: { x: 42 } }))
undoManager.undo() undoManager.undo()
t.assert(map0.get('a') === 1) t.assert(map0.get('a') === 1)
undoManager.redo() undoManager.redo()
t.compare(map0.toJSON(), /** @type {any} */ ({ 'a': { x: 42 } })) t.compare(map0.toJSON(), /** @type {any} */ ({ a: { x: 42 } }))
testConnector.syncAll() testConnector.syncAll()
// if content is overwritten by another user, undo operations should be skipped // if content is overwritten by another user, undo operations should be skipped
map1.set('a', 44) map1.set('a', 44)
@@ -65,6 +82,15 @@ export const testUndoMap = tc => {
t.assert(map0.get('a') === 44) t.assert(map0.get('a') === 44)
undoManager.redo() undoManager.redo()
t.assert(map0.get('a') === 44) t.assert(map0.get('a') === 44)
// test setting value multiple times
map0.set('b', 'initial')
undoManager.stopCapturing()
map0.set('b', 'val1')
map0.set('b', 'val2')
undoManager.stopCapturing()
undoManager.undo()
t.assert(map0.get('b') === 'initial')
} }
/** /**

View File

@@ -207,7 +207,7 @@ export const testChangeEvent = tc => {
const newArr = new Y.Array() const newArr = new Y.Array()
array0.insert(0, [newArr, 4, 'dtrn']) array0.insert(0, [newArr, 4, 'dtrn'])
t.assert(changes !== null && changes.added.size === 2 && changes.deleted.size === 0) t.assert(changes !== null && changes.added.size === 2 && changes.deleted.size === 0)
t.compare(changes.delta, [{insert: [newArr, 4, 'dtrn']}]) t.compare(changes.delta, [{ insert: [newArr, 4, 'dtrn'] }])
changes = null changes = null
array0.delete(0, 2) array0.delete(0, 2)
t.assert(changes !== null && changes.added.size === 0 && changes.deleted.size === 2) t.assert(changes !== null && changes.added.size === 0 && changes.deleted.size === 2)
@@ -227,7 +227,7 @@ export const testInsertAndDeleteEventsForTypes2 = tc => {
/** /**
* @type {Array<Object<string,any>>} * @type {Array<Object<string,any>>}
*/ */
let events = [] const events = []
array0.observe(e => { array0.observe(e => {
events.push(e) events.push(e)
}) })
@@ -318,7 +318,7 @@ export const testIteratingArrayContainingTypes = tc => {
arr.push([map]) arr.push([map])
} }
let cnt = 0 let cnt = 0
for (let item of arr) { for (const item of arr) {
t.assert(item.get('value') === cnt++, 'value is correct') t.assert(item.get('value') === cnt++, 'value is correct')
} }
y.destroy() y.destroy()

View File

@@ -66,7 +66,7 @@ export const testGetAndSetOfMapProperty = tc => {
testConnector.flushAllMessages() testConnector.flushAllMessages()
for (let user of users) { for (const user of users) {
const u = user.getMap('map') const u = user.getMap('map')
t.compare(u.get('stuff'), 'stuffy') t.compare(u.get('stuff'), 'stuffy')
t.assert(u.get('undefined') === undefined, 'undefined') t.assert(u.get('undefined') === undefined, 'undefined')
@@ -108,7 +108,7 @@ export const testGetAndSetOfMapPropertySyncs = tc => {
map0.set('stuff', 'stuffy') map0.set('stuff', 'stuffy')
t.compare(map0.get('stuff'), 'stuffy') t.compare(map0.get('stuff'), 'stuffy')
testConnector.flushAllMessages() testConnector.flushAllMessages()
for (let user of users) { for (const user of users) {
var u = user.getMap('map') var u = user.getMap('map')
t.compare(u.get('stuff'), 'stuffy') t.compare(u.get('stuff'), 'stuffy')
} }
@@ -123,7 +123,7 @@ export const testGetAndSetOfMapPropertyWithConflict = tc => {
map0.set('stuff', 'c0') map0.set('stuff', 'c0')
map1.set('stuff', 'c1') map1.set('stuff', 'c1')
testConnector.flushAllMessages() testConnector.flushAllMessages()
for (let user of users) { for (const user of users) {
var u = user.getMap('map') var u = user.getMap('map')
t.compare(u.get('stuff'), 'c1') t.compare(u.get('stuff'), 'c1')
} }
@@ -139,7 +139,7 @@ export const testGetAndSetAndDeleteOfMapProperty = tc => {
map1.set('stuff', 'c1') map1.set('stuff', 'c1')
map1.delete('stuff') map1.delete('stuff')
testConnector.flushAllMessages() testConnector.flushAllMessages()
for (let user of users) { for (const user of users) {
var u = user.getMap('map') var u = user.getMap('map')
t.assert(u.get('stuff') === undefined) t.assert(u.get('stuff') === undefined)
} }
@@ -156,7 +156,7 @@ export const testGetAndSetOfMapPropertyWithThreeConflicts = tc => {
map1.set('stuff', 'c2') map1.set('stuff', 'c2')
map2.set('stuff', 'c3') map2.set('stuff', 'c3')
testConnector.flushAllMessages() testConnector.flushAllMessages()
for (let user of users) { for (const user of users) {
var u = user.getMap('map') var u = user.getMap('map')
t.compare(u.get('stuff'), 'c3') t.compare(u.get('stuff'), 'c3')
} }
@@ -179,7 +179,7 @@ export const testGetAndSetAndDeleteOfMapPropertyWithThreeConflicts = tc => {
map3.set('stuff', 'c3') map3.set('stuff', 'c3')
map3.delete('stuff') map3.delete('stuff')
testConnector.flushAllMessages() testConnector.flushAllMessages()
for (let user of users) { for (const user of users) {
var u = user.getMap('map') var u = user.getMap('map')
t.assert(u.get('stuff') === undefined) t.assert(u.get('stuff') === undefined)
} }
@@ -340,6 +340,56 @@ export const testChangeEvent = tc => {
compare(users) compare(users)
} }
/**
* @param {t.TestCase} tc
*/
export const testYmapEventExceptionsShouldCompleteTransaction = tc => {
const doc = new Y.Doc()
const map = doc.getMap('map')
let updateCalled = false
let throwingObserverCalled = false
let throwingDeepObserverCalled = false
doc.on('update', () => {
updateCalled = true
})
const throwingObserver = () => {
throwingObserverCalled = true
throw new Error('Failure')
}
const throwingDeepObserver = () => {
throwingDeepObserverCalled = true
throw new Error('Failure')
}
map.observe(throwingObserver)
map.observeDeep(throwingDeepObserver)
t.fails(() => {
map.set('y', '2')
})
t.assert(updateCalled)
t.assert(throwingObserverCalled)
t.assert(throwingDeepObserverCalled)
// check if it works again
updateCalled = false
throwingObserverCalled = false
throwingDeepObserverCalled = false
t.fails(() => {
map.set('z', '3')
})
t.assert(updateCalled)
t.assert(throwingObserverCalled)
t.assert(throwingDeepObserverCalled)
t.assert(map.get('z') === '3')
}
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
@@ -380,12 +430,12 @@ export const testYmapEventHasCorrectValueWhenSettingAPrimitiveFromOtherUser = tc
*/ */
const mapTransactions = [ const mapTransactions = [
function set (user, gen) { function set (user, gen) {
let key = prng.oneOf(gen, ['one', 'two']) const key = prng.oneOf(gen, ['one', 'two'])
var value = prng.utf16String(gen) var value = prng.utf16String(gen)
user.getMap('map').set(key, value) user.getMap('map').set(key, value)
}, },
function setType (user, gen) { function setType (user, gen) {
let key = prng.oneOf(gen, ['one', 'two']) const key = prng.oneOf(gen, ['one', 'two'])
var type = prng.oneOf(gen, [new Y.Array(), new Y.Map()]) var type = prng.oneOf(gen, [new Y.Array(), new Y.Map()])
user.getMap('map').set(key, type) user.getMap('map').set(key, type)
if (type instanceof Y.Array) { if (type instanceof Y.Array) {
@@ -395,7 +445,7 @@ const mapTransactions = [
} }
}, },
function _delete (user, gen) { function _delete (user, gen) {
let key = prng.oneOf(gen, ['one', 'two']) const key = prng.oneOf(gen, ['one', 'two'])
user.getMap('map').delete(key) user.getMap('map').delete(key)
} }
] ]

View File

@@ -81,10 +81,10 @@ export const testBasicFormat = tc => {
export const testGetDeltaWithEmbeds = tc => { export const testGetDeltaWithEmbeds = tc => {
const { text0 } = init(tc, { users: 1 }) const { text0 } = init(tc, { users: 1 })
text0.applyDelta([{ text0.applyDelta([{
insert: {linebreak: 's'} insert: { linebreak: 's' }
}]) }])
t.compare(text0.toDelta(), [{ t.compare(text0.toDelta(), [{
insert: {linebreak: 's'} insert: { linebreak: 's' }
}]) }])
} }
@@ -127,7 +127,7 @@ export const testSnapshot = tc => {
delete v.attributes.ychange.user delete v.attributes.ychange.user
} }
}) })
t.compare(state2Diff, [{insert: 'a'}, {insert: 'x', attributes: {ychange: { type: 'added' }}}, {insert: 'b', attributes: {ychange: { type: 'removed' }}}, { insert: 'cd' }]) t.compare(state2Diff, [{ insert: 'a' }, { insert: 'x', attributes: { ychange: { type: 'added' } } }, { insert: 'b', attributes: { ychange: { type: 'removed' } } }, { insert: 'cd' }])
} }
/** /**
@@ -149,3 +149,12 @@ export const testSnapshotDeleteAfter = tc => {
const state1 = text0.toDelta(snapshot1) const state1 = text0.toDelta(snapshot1)
t.compare(state1, [{ insert: 'abcd' }]) t.compare(state1, [{ insert: 'abcd' }])
} }
/**
* @param {t.TestCase} tc
*/
export const testToJson = tc => {
const { text0 } = init(tc, { users: 1 })
text0.insert(0, 'abc', { bold: true })
t.assert(text0.toJSON() === 'abc', 'toJSON returns the unformatted text')
}

View File

@@ -60,13 +60,13 @@ export const testEvents = tc => {
*/ */
export const testTreewalker = tc => { export const testTreewalker = tc => {
const { users, xml0 } = init(tc, { users: 3 }) const { users, xml0 } = init(tc, { users: 3 })
let paragraph1 = new Y.XmlElement('p') const paragraph1 = new Y.XmlElement('p')
let paragraph2 = new Y.XmlElement('p') const paragraph2 = new Y.XmlElement('p')
let text1 = new Y.XmlText('init') const text1 = new Y.XmlText('init')
let text2 = new Y.XmlText('text') const text2 = new Y.XmlText('text')
paragraph1.insert(0, [text1, text2]) paragraph1.insert(0, [text1, text2])
xml0.insert(0, [paragraph1, paragraph2, new Y.XmlElement('img')]) xml0.insert(0, [paragraph1, paragraph2, new Y.XmlElement('img')])
let allParagraphs = xml0.querySelectorAll('p') const allParagraphs = xml0.querySelectorAll('p')
t.assert(allParagraphs.length === 2, 'found exactly two paragraphs') t.assert(allParagraphs.length === 2, 'found exactly two paragraphs')
t.assert(allParagraphs[0] === paragraph1, 'querySelectorAll found paragraph1') t.assert(allParagraphs[0] === paragraph1, 'querySelectorAll found paragraph1')
t.assert(allParagraphs[1] === paragraph2, 'querySelectorAll found paragraph2') t.assert(allParagraphs[1] === paragraph2, 'querySelectorAll found paragraph2')

View File

@@ -6,15 +6,15 @@
"allowJs": true, /* Allow javascript files to be compiled. */ "allowJs": true, /* Allow javascript files to be compiled. */
"checkJs": true, /* Report errors in .js files. */ "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */ "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */ // "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */ // "outFile": "./dist/yjs.js", /* Concatenate and emit output to single file. */
// "outDir": "./build", /* Redirect output structure to the directory. */ "outDir": "./dist", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */ // "composite": true, /* Enable project compilation */
// "removeComments": true, /* Do not emit comments to output. */ // "removeComments": true, /* Do not emit comments to output. */
"noEmit": true, /* Do not emit outputs. */ // "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */ // "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
@@ -22,6 +22,7 @@
/* Strict Type-Checking Options */ /* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */ "strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
"emitDeclarationOnly": true,
// "strictNullChecks": true, /* Enable strict null checks. */ // "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */ // "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
@@ -38,7 +39,10 @@
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"baseUrl": "./", /* Base directory to resolve non-absolute module names. */ "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
"paths": { "paths": {
"yjs": ["./src/index.js"] "yjs": ["./src/index.js"],
"lib0/*": ["node_modules/lib0/*"],
"lib0/set.js": ["node_modules/lib0/set.js"],
"lib0/function.js": ["node_modules/lib0/function.js"]
}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */ // "typeRoots": [], /* List of folders to include type definitions from. */
@@ -59,6 +63,6 @@
"maxNodeModuleJsDepth": 5, "maxNodeModuleJsDepth": 5,
// "types": ["./src/utils/typedefs.js"] // "types": ["./src/utils/typedefs.js"]
}, },
"include": ["./src/**/*", "./tests/**/*"], "include": ["./src/**/*.js", "./tests/**/*.js"],
"exclude": ["../lib0/**/*", "node_modules/**/*", "dist", "dist/**/*.js"] "exclude": ["../lib0/**/*", "node_modules/**/*", "dist", "dist/**/*.js"]
} }