Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45df311dd7 | ||
|
|
62888b4004 | ||
|
|
76c389dba0 | ||
|
|
78fa98c000 | ||
|
|
e9f9e08450 | ||
|
|
e3c59b0aa7 | ||
|
|
705dce7838 |
@@ -85,6 +85,12 @@ document private.
|
|||||||
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-indexeddb">y-indexeddb</a></dt>
|
||||||
|
<dd>
|
||||||
|
Efficiently persists document updates to the browsers indexeddb database.
|
||||||
|
The document is immediately available and only diffs need to be synced through the
|
||||||
|
network provider.
|
||||||
</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>
|
||||||
|
|||||||
305
README.v12.md
305
README.v12.md
@@ -1,305 +0,0 @@
|
|||||||
|
|
||||||
# 
|
|
||||||
|
|
||||||
Yjs is a framework for offline-first p2p shared editing on structured data like
|
|
||||||
text, richtext, json, or XML. It is fairly easy to get started, as Yjs hides
|
|
||||||
most of the complexity of concurrent editing. For additional information, demos,
|
|
||||||
and tutorials visit [y-js.org](http://y-js.org/).
|
|
||||||
|
|
||||||
:warning: Checkout the [v13 docs](./README.md) for the upcoming release :warning:
|
|
||||||
|
|
||||||
### Extensions
|
|
||||||
Yjs only knows how to resolve conflicts on shared data. You have to choose a ..
|
|
||||||
|
|
||||||
* *Connector* - a communication protocol that propagates changes to the clients
|
|
||||||
* *Database* - a database to store your changes
|
|
||||||
* one or more *Types* - that represent the shared data
|
|
||||||
|
|
||||||
Connectors, Databases, and Types are available as modules that extend Yjs. Here
|
|
||||||
is a list of the modules we know of:
|
|
||||||
|
|
||||||
##### Connectors
|
|
||||||
|
|
||||||
|Name | Description |
|
|
||||||
|----------------|-----------------------------------|
|
|
||||||
|[webrtc](https://github.com/y-js/y-webrtc) | Propagate updates Browser2Browser via WebRTC|
|
|
||||||
|[websockets](https://github.com/y-js/y-websockets-client) | Set up [a central server](https://github.com/y-js/y-websockets-client), and connect to it via websockets |
|
|
||||||
|[xmpp](https://github.com/y-js/y-xmpp) | Propagate updates in a XMPP multi-user-chat room ([XEP-0045](http://xmpp.org/extensions/xep-0045.html))|
|
|
||||||
|[ipfs](https://github.com/ipfs-labs/y-ipfs-connector) | Connector for the [Interplanetary File System](https://ipfs.io/)!|
|
|
||||||
|[test](https://github.com/y-js/y-test) | A Connector for testing purposes. It is designed to simulate delays that happen in worst case scenarios|
|
|
||||||
|
|
||||||
##### Database adapters
|
|
||||||
|
|
||||||
|Name | Description |
|
|
||||||
|----------------|-----------------------------------|
|
|
||||||
|[memory](https://github.com/y-js/y-memory) | In-memory storage. |
|
|
||||||
|[indexeddb](https://github.com/y-js/y-indexeddb) | Offline storage for the browser |
|
|
||||||
|[leveldb](https://github.com/y-js/y-leveldb) | Persistent storage for node apps |
|
|
||||||
|
|
||||||
##### Types
|
|
||||||
|
|
||||||
| Name | Description |
|
|
||||||
|----------|-------------------|
|
|
||||||
|[map](https://github.com/y-js/y-map) | A shared Map implementation. Maps from text to any stringify-able object |
|
|
||||||
|[array](https://github.com/y-js/y-array) | A shared Array implementation |
|
|
||||||
|[xml](https://github.com/y-js/y-xml) | An implementation of the DOM. You can create a two way binding to Browser DOM objects |
|
|
||||||
|[text](https://github.com/y-js/y-text) | Collaborate on text. Supports two way binding to the [Ace Editor](https://ace.c9.io), [CodeMirror](https://codemirror.net/), [Monaco](https://github.com/Microsoft/monaco-editor), textareas, input elements, and HTML elements (e.g. <*h1*>, or <*p*>) |
|
|
||||||
|[richtext](https://github.com/y-js/y-richtext) | Collaborate on rich text. Supports two way binding to the [Quill Rich Text Editor](http://quilljs.com/)|
|
|
||||||
|
|
||||||
##### Other
|
|
||||||
|
|
||||||
| Name | Description |
|
|
||||||
|-----------|-------------------|
|
|
||||||
|[y-element](http://y-js.org/y-element/) | Yjs Polymer Element |
|
|
||||||
|
|
||||||
## Use it!
|
|
||||||
Install Yjs, and its modules with [bower](http://bower.io/), or
|
|
||||||
[npm](https://www.npmjs.org/package/yjs).
|
|
||||||
|
|
||||||
### Bower
|
|
||||||
|
|
||||||
```
|
|
||||||
bower install --save yjs y-array % add all y-* modules you want to use
|
|
||||||
```
|
|
||||||
You only need to include the `y.js` file. Yjs is able to automatically require
|
|
||||||
missing modules.
|
|
||||||
```
|
|
||||||
<script src="./bower_components/yjs/y.js"></script>
|
|
||||||
```
|
|
||||||
|
|
||||||
### CDN
|
|
||||||
|
|
||||||
```
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/yjs@12/dist/y.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/y-array@10/dist/y-array.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/y-websockets-client@8/dist/y-websockets-client.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/y-memory@8/dist/y-memory.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/y-map@10/dist/y-map.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/y-text@9/dist/y-text.js"></script>
|
|
||||||
// ..
|
|
||||||
// do the same for all modules you want to use
|
|
||||||
```
|
|
||||||
|
|
||||||
### Npm
|
|
||||||
|
|
||||||
```
|
|
||||||
npm install --save yjs % add all y-* modules you want to use
|
|
||||||
```
|
|
||||||
|
|
||||||
If you don't include via script tag, you have to explicitly include all modules!
|
|
||||||
(Same goes for other module systems)
|
|
||||||
```
|
|
||||||
var Y = require('yjs')
|
|
||||||
require('y-array')(Y) // add the y-array type to Yjs
|
|
||||||
require('y-websockets-client')(Y)
|
|
||||||
require('y-memory')(Y)
|
|
||||||
require('y-map')(Y)
|
|
||||||
require('y-text')(Y)
|
|
||||||
// ..
|
|
||||||
// do the same for all modules you want to use
|
|
||||||
```
|
|
||||||
|
|
||||||
### ES6 Syntax
|
|
||||||
|
|
||||||
```
|
|
||||||
import Y from 'yjs'
|
|
||||||
import yArray from 'y-array'
|
|
||||||
import yWebsocketsClient from 'y-webrtc'
|
|
||||||
import yMemory from 'y-memory'
|
|
||||||
import yMap from 'y-map'
|
|
||||||
import yText from 'y-text'
|
|
||||||
// ..
|
|
||||||
Y.extend(yArray, yWebsocketsClient, yMemory, yArray, yMap, yText /*, .. */)
|
|
||||||
```
|
|
||||||
|
|
||||||
# Text editing example
|
|
||||||
|
|
||||||
Install dependencies
|
|
||||||
```
|
|
||||||
bower i yjs y-memory y-webrtc y-array y-text
|
|
||||||
```
|
|
||||||
|
|
||||||
Here is a simple example of a shared textarea
|
|
||||||
```HTML
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
<script src="./bower_components/yjs/y.js"></script>
|
|
||||||
<!-- Yjs automatically includes all missing dependencies (browser only) -->
|
|
||||||
<script>
|
|
||||||
Y({
|
|
||||||
db: {
|
|
||||||
name: 'memory' // use memory database adapter.
|
|
||||||
// name: 'indexeddb' // use indexeddb database adapter instead for offline apps
|
|
||||||
},
|
|
||||||
connector: {
|
|
||||||
name: 'webrtc', // use webrtc connector
|
|
||||||
// name: 'websockets-client'
|
|
||||||
// name: 'xmpp'
|
|
||||||
room: 'my-room' // clients connecting to the same room share data
|
|
||||||
},
|
|
||||||
sourceDir: './bower_components', // location of the y-* modules (browser only)
|
|
||||||
share: {
|
|
||||||
textarea: 'Text' // y.share.textarea is of type y-text
|
|
||||||
}
|
|
||||||
}).then(function (y) {
|
|
||||||
// The Yjs instance `y` is available
|
|
||||||
// y.share.* contains the shared types
|
|
||||||
|
|
||||||
// Bind `y.share.textarea` to `<textarea/>`
|
|
||||||
y.share.textarea.bind(document.querySelector('textarea'))
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
<textarea></textarea>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Get Help & Give Help
|
|
||||||
There are some friendly people on [](https://gitter.im/y-js/yjs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) who are eager to help, and answer questions. Please join!
|
|
||||||
|
|
||||||
Report _any_ issues to the
|
|
||||||
[Github issue page](https://github.com/y-js/yjs/issues)! I try to fix them very
|
|
||||||
soon, if possible.
|
|
||||||
|
|
||||||
# API
|
|
||||||
|
|
||||||
### Y(options)
|
|
||||||
* Y.extend(module1, module2, ..)
|
|
||||||
* Add extensions to Y
|
|
||||||
* `Y.extend(require('y-webrtc'))` has the same semantics as
|
|
||||||
`require('y-webrtc')(Y)`
|
|
||||||
* options.db
|
|
||||||
* Will be forwarded to the database adapter. Specify the database adaper on
|
|
||||||
`options.db.name`.
|
|
||||||
* Have a look at the used database adapter repository to see all available
|
|
||||||
options.
|
|
||||||
* options.connector
|
|
||||||
* Will be forwarded to the connector adapter. Specify the connector adaper on
|
|
||||||
`options.connector.name`.
|
|
||||||
* All our connectors implement a `room` property. Clients that specify the
|
|
||||||
same room share the same data.
|
|
||||||
* All of our connectors specify an `url` property that defines the connection
|
|
||||||
endpoint of the used connector.
|
|
||||||
* All of our connectors also have a default connection endpoint that you can
|
|
||||||
use for development.
|
|
||||||
* Set `options.connector.generateUserId = true` in order to genenerate a
|
|
||||||
userid, instead of receiving one from the server. This way the `Y(..)` is
|
|
||||||
immediately going to be resolved, without waiting for any confirmation from
|
|
||||||
the server. Use with caution.
|
|
||||||
* Have a look at the used connector repository to see all available options.
|
|
||||||
* *Only if you know what you are doing:* Set
|
|
||||||
`options.connector.preferUntransformed = true` in order receive the shared
|
|
||||||
data untransformed. This is very efficient as the database content is simply
|
|
||||||
copied to this client. This does only work if this client receives content
|
|
||||||
from only one client.
|
|
||||||
* options.sourceDir (browser only)
|
|
||||||
* Path where all y-* modules are stored
|
|
||||||
* Defaults to `/bower_components`
|
|
||||||
* Not required when running on `nodejs` / `iojs`
|
|
||||||
* When using nodejs you need to manually extend Yjs:
|
|
||||||
```
|
|
||||||
var Y = require('yjs')
|
|
||||||
// you have to require a db, connector, and *all* types you use!
|
|
||||||
require('y-memory')(Y)
|
|
||||||
require('y-webrtc')(Y)
|
|
||||||
require('y-map')(Y)
|
|
||||||
// ..
|
|
||||||
```
|
|
||||||
* options.share
|
|
||||||
* Specify on `options.share[arbitraryName]` types that are shared among all
|
|
||||||
users.
|
|
||||||
* E.g. Specify `options.share[arbitraryName] = 'Array'` to require y-array and
|
|
||||||
create an y-array type on `y.share[arbitraryName]`.
|
|
||||||
* If userA doesn't specify `options.share[arbitraryName]`, it won't be
|
|
||||||
available for userA.
|
|
||||||
* If userB specifies `options.share[arbitraryName]`, it still won't be
|
|
||||||
available for userA. But all the updates are send from userB to userA.
|
|
||||||
* In contrast to y-map, types on `y.share.*` cannot be overwritten or deleted.
|
|
||||||
Instead, they are merged among all users. This feature is only available on
|
|
||||||
`y.share.*`
|
|
||||||
* Weird behavior: It is supported that two users specify different types with
|
|
||||||
the same property name.
|
|
||||||
E.g. userA specifies `options.share.x = 'Array'`, and userB specifies
|
|
||||||
`options.share.x = 'Text'`. But they only share data if they specified the
|
|
||||||
same type with the same property name
|
|
||||||
* options.type (browser only)
|
|
||||||
* Array of modules that Yjs needs to require, before instantiating a shared
|
|
||||||
type.
|
|
||||||
* By default Yjs requires the specified database adapter, the specified
|
|
||||||
connector, and all modules that are used in `options.share.*`
|
|
||||||
* Put all types here that you intend to use, but are not used in y.share.*
|
|
||||||
|
|
||||||
### Instantiated Y object (y)
|
|
||||||
`Y(options)` returns a promise that is fulfilled when..
|
|
||||||
|
|
||||||
* All modules are loaded
|
|
||||||
* The specified database adapter is loaded
|
|
||||||
* The specified connector is loaded
|
|
||||||
* All types are included
|
|
||||||
* The connector is initialized, and a unique user id is set (received from the
|
|
||||||
server)
|
|
||||||
* Note: When using y-indexeddb, a retrieved user id is stored on `localStorage`
|
|
||||||
|
|
||||||
The promise returns an instance of Y. We denote it with a lower case `y`.
|
|
||||||
|
|
||||||
* y.share.*
|
|
||||||
* Instances of the types you specified on options.share.*
|
|
||||||
* y.share.* can only be defined once when you instantiate Y!
|
|
||||||
* y.connector is an instance of Y.AbstractConnector
|
|
||||||
* y.connector.onUserEvent(function (event) {..})
|
|
||||||
* Observe user events (event.action is either 'userLeft' or 'userJoined')
|
|
||||||
* y.connector.whenSynced(listener)
|
|
||||||
* `listener` is executed when y synced with at least one user.
|
|
||||||
* `listener` is not called when no other user is in the same room.
|
|
||||||
* y-websockets-client aways waits to sync with the server
|
|
||||||
* y.connector.disconnect()
|
|
||||||
* Force to disconnect this instance from the other instances
|
|
||||||
* y.connector.connect()
|
|
||||||
* Try to reconnect to the other instances (needs to be supported by the
|
|
||||||
connector)
|
|
||||||
* Not supported by y-xmpp
|
|
||||||
* y.close()
|
|
||||||
* Destroy this object.
|
|
||||||
* Destroys all types (they will throw weird errors if you still use them)
|
|
||||||
* Disconnects from the other instances (via connector)
|
|
||||||
* Returns a promise
|
|
||||||
* y.destroy()
|
|
||||||
* calls y.close()
|
|
||||||
* Removes all data from the database
|
|
||||||
* Returns a promise
|
|
||||||
* y.db.stopGarbageCollector()
|
|
||||||
* Stop the garbage collector. Call y.db.garbageCollect() to continue garbage
|
|
||||||
collection
|
|
||||||
* y.db.gc :: Boolean
|
|
||||||
* Whether gc is turned on
|
|
||||||
* y.db.gcTimeout :: Number (defaults to 50000 ms)
|
|
||||||
* Time interval between two garbage collect cycles
|
|
||||||
* It is required that all instances exchanged all messages after two garbage
|
|
||||||
collect cycles (after 100000 ms per default)
|
|
||||||
* y.db.userId :: String
|
|
||||||
* The used user id for this client. **Never overwrite this**
|
|
||||||
|
|
||||||
### Logging
|
|
||||||
Yjs uses [debug](https://github.com/visionmedia/debug) for logging. The flag
|
|
||||||
`y*` enables logging for all y-* components. You can selectively remove
|
|
||||||
components you are not interested in: E.g. The flag `y*,-y:connector-message`
|
|
||||||
will not log the long `y:connector-message` messages.
|
|
||||||
|
|
||||||
##### Enable logging in Node.js
|
|
||||||
```sh
|
|
||||||
DEBUG=y* node app.js
|
|
||||||
```
|
|
||||||
|
|
||||||
Remove the colors in order to log to a file:
|
|
||||||
```sh
|
|
||||||
DEBUG_COLORS=0 DEBUG=y* node app.js > log
|
|
||||||
```
|
|
||||||
|
|
||||||
##### Enable logging in the browser
|
|
||||||
```js
|
|
||||||
localStorage.debug = 'y*'
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
|
||||||
Yjs is licensed under the [MIT License](./LICENSE).
|
|
||||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.0.0",
|
"version": "13.0.3",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1332,9 +1332,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"isomorphic.js": {
|
"isomorphic.js": {
|
||||||
"version": "0.1.1",
|
"version": "0.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.1.3.tgz",
|
||||||
"integrity": "sha512-Q85LNm6e50saL4EPWa0mWEYNUuV51n623gzPVNC1QiLGLmjONEtfFT3pa04OoUIYB7rzGJBpzO2iNPhV1Ib4hg=="
|
"integrity": "sha512-pabBRLDwYefSsNS+qCazJ97o7P5xDTrNoxSYFTM09JlZTxPrOEPGKekwqUy3/Np4C4PHnVUXHYsZPOix0jELsA=="
|
||||||
},
|
},
|
||||||
"js-tokens": {
|
"js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
@@ -1439,9 +1439,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lib0": {
|
"lib0": {
|
||||||
"version": "0.2.7",
|
"version": "0.2.12",
|
||||||
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.12.tgz",
|
||||||
"integrity": "sha512-25ojhgEondpayWt2mgVTKels6R8viO27AetFgvdOk+WWGVah+q2pXouQ6dGj/gtxNwCDD2ih2B0LbDovNEtCrg==",
|
"integrity": "sha512-ileLyIMsWqcpyFLP6WMIgD6C7rkwrdYjH6CCFvAnqOhnGcyAl8LmLkiryVNmKW0sLRw/gLcB47LGXJDw0tDlkg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"isomorphic.js": "^0.1.1"
|
"isomorphic.js": "^0.1.1"
|
||||||
}
|
}
|
||||||
@@ -2291,9 +2291,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rollup": {
|
"rollup": {
|
||||||
"version": "1.29.1",
|
"version": "1.30.0",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-1.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-1.30.0.tgz",
|
||||||
"integrity": "sha512-dGQ+b9d1FOX/gluiggTAVnTvzQZUEkCi/TwZcax7ujugVRHs0nkYJlV9U4hsifGEMojnO+jvEML2CJQ6qXgbHA==",
|
"integrity": "sha512-ANcmfaSQwpcJtZUTA0ZMNBtFcQ1B4A5FldlNqEK0WdWm9sHSKu93ffa2KV1ux8HA/yKIV/ZARV28m7rNdXJgEw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/estree": "*",
|
"@types/estree": "*",
|
||||||
|
|||||||
11
package.json
11
package.json
@@ -1,21 +1,20 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.0.0",
|
"version": "13.0.3",
|
||||||
"description": "Shared Editing Library",
|
"description": "Shared Editing Library",
|
||||||
"main": "./dist/yjs.cjs",
|
"main": "./dist/yjs.cjs",
|
||||||
"module": "./src/index.js",
|
"module": "./dist/yjs.mjs",
|
||||||
"types": "./dist/src/index.d.ts",
|
"types": "./dist/src/index.d.ts",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npm run dist && node ./dist/tests.cjs --repitition-time 50",
|
"test": "npm run dist && node ./dist/tests.cjs --repitition-time 50",
|
||||||
"test-exhaustive": "npm run lint && npm run dist && node ./dist/tests.cjs --production --repitition-time 10000",
|
"test-extensive": "npm run lint && npm run dist && node ./dist/tests.cjs --production --repitition-time 10000",
|
||||||
"dist": "rm -rf dist && rollup -c && tsc",
|
"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 && http-server ./docs/",
|
"serve-docs": "npm run docs && http-server ./docs/",
|
||||||
"preversion": "npm run lint && PRODUCTION=1 npm run dist && npm run docs && node ./dist/tests.cjs --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",
|
|
||||||
"debug": "concurrently 'http-server -o 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.cjs",
|
"trace-deopt": "clear && rollup -c && node --trace-deopt dist/test.cjs",
|
||||||
"trace-opt": "clear && rollup -c && node --trace-opt dist/test.cjs"
|
"trace-opt": "clear && rollup -c && node --trace-opt dist/test.cjs"
|
||||||
@@ -57,7 +56,7 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://yjs.dev",
|
"homepage": "https://yjs.dev",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lib0": "^0.2.7"
|
"lib0": "^0.2.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^11.0.1",
|
"@rollup/plugin-commonjs": "^11.0.1",
|
||||||
@@ -66,7 +65,7 @@
|
|||||||
"http-server": "^0.12.1",
|
"http-server": "^0.12.1",
|
||||||
"jsdoc": "^3.6.3",
|
"jsdoc": "^3.6.3",
|
||||||
"markdownlint-cli": "^0.19.0",
|
"markdownlint-cli": "^0.19.0",
|
||||||
"rollup": "^1.29.1",
|
"rollup": "^1.30.0",
|
||||||
"rollup-cli": "^1.0.9",
|
"rollup-cli": "^1.0.9",
|
||||||
"standard": "^14.0.0",
|
"standard": "^14.0.0",
|
||||||
"tui-jsdoc-template": "^1.2.2",
|
"tui-jsdoc-template": "^1.2.2",
|
||||||
|
|||||||
@@ -51,6 +51,21 @@ export default [{
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
external: id => /^lib0\//.test(id)
|
external: id => /^lib0\//.test(id)
|
||||||
|
}, {
|
||||||
|
input: './src/index.js',
|
||||||
|
output: {
|
||||||
|
name: 'Y',
|
||||||
|
file: 'dist/yjs.mjs',
|
||||||
|
format: 'esm',
|
||||||
|
sourcemap: true,
|
||||||
|
paths: path => {
|
||||||
|
if (/^lib0\//.test(path)) {
|
||||||
|
return `lib0/dist/${path.slice(5, -3)}.cjs`
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
},
|
||||||
|
external: id => /^lib0\//.test(id)
|
||||||
}, {
|
}, {
|
||||||
input: './tests/index.js',
|
input: './tests/index.js',
|
||||||
output: {
|
output: {
|
||||||
|
|||||||
@@ -55,5 +55,7 @@ export {
|
|||||||
isDeleted,
|
isDeleted,
|
||||||
isParentOf,
|
isParentOf,
|
||||||
equalSnapshots,
|
equalSnapshots,
|
||||||
PermanentUserData // @TODO experimental
|
PermanentUserData, // @TODO experimental
|
||||||
|
tryGc,
|
||||||
|
transact
|
||||||
} from './internals.js'
|
} from './internals.js'
|
||||||
|
|||||||
@@ -68,10 +68,11 @@ export const followRedone = (store, id) => {
|
|||||||
* sending it to other peers
|
* sending it to other peers
|
||||||
*
|
*
|
||||||
* @param {Item|null} item
|
* @param {Item|null} item
|
||||||
|
* @param {boolean} keep
|
||||||
*/
|
*/
|
||||||
export const keepItem = item => {
|
export const keepItem = (item, keep) => {
|
||||||
while (item !== null && !item.keep) {
|
while (item !== null && item.keep !== keep) {
|
||||||
item.keep = true
|
item.keep = keep
|
||||||
item = item.parent._item
|
item = item.parent._item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -220,7 +221,7 @@ export const redoItem = (transaction, item, redoitems) => {
|
|||||||
item.content.copy()
|
item.content.copy()
|
||||||
)
|
)
|
||||||
item.redone = redoneItem.id
|
item.redone = redoneItem.id
|
||||||
keepItem(redoneItem)
|
keepItem(redoneItem, true)
|
||||||
redoneItem.integrate(transaction)
|
redoneItem.integrate(transaction)
|
||||||
return redoneItem
|
return redoneItem
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -617,6 +617,11 @@ export class YText extends AbstractType {
|
|||||||
this._pending = string !== undefined ? [() => this.insert(0, string)] : []
|
this._pending = string !== undefined ? [() => this.insert(0, string)] : []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of characters of this text type.
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
get length () {
|
get length () {
|
||||||
return this._length
|
return this._length
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,10 +25,12 @@ export class Doc extends Observable {
|
|||||||
/**
|
/**
|
||||||
* @param {Object} conf configuration
|
* @param {Object} conf configuration
|
||||||
* @param {boolean} [conf.gc] Disable garbage collection (default: gc=true)
|
* @param {boolean} [conf.gc] Disable garbage collection (default: gc=true)
|
||||||
|
* @param {function(Item):boolean} [conf.gcFilter] Will be called before an Item is garbage collected. Return false to keep the Item.
|
||||||
*/
|
*/
|
||||||
constructor ({ gc = true } = {}) {
|
constructor ({ gc = true, gcFilter = () => true } = {}) {
|
||||||
super()
|
super()
|
||||||
this.gc = gc
|
this.gc = gc
|
||||||
|
this.gcFilter = gcFilter
|
||||||
this.clientID = random.uint32()
|
this.clientID = random.uint32()
|
||||||
/**
|
/**
|
||||||
* @type {Map<string, AbstractType<YEvent>>}
|
* @type {Map<string, AbstractType<YEvent>>}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
findIndexSS,
|
findIndexSS,
|
||||||
callEventHandlerListeners,
|
callEventHandlerListeners,
|
||||||
Item,
|
Item,
|
||||||
ID, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
|
StructStore, ID, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as encoding from 'lib0/encoding.js'
|
import * as encoding from 'lib0/encoding.js'
|
||||||
@@ -145,6 +145,85 @@ export const addChangedTypeToTransaction = (transaction, type, parentSub) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DeleteSet} ds
|
||||||
|
* @param {StructStore} store
|
||||||
|
* @param {function(Item):boolean} gcFilter
|
||||||
|
*/
|
||||||
|
const tryGcDeleteSet = (ds, store, gcFilter) => {
|
||||||
|
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 && gcFilter(struct)) {
|
||||||
|
struct.gc(store, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DeleteSet} ds
|
||||||
|
* @param {StructStore} store
|
||||||
|
*/
|
||||||
|
const tryMergeDeleteSet = (ds, store) => {
|
||||||
|
// try to merge deleted / gc'd items
|
||||||
|
// merge from right to left for better efficiecy and so we don't miss any merge targets
|
||||||
|
for (const [client, deleteItems] of ds.clients) {
|
||||||
|
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DeleteSet} ds
|
||||||
|
* @param {StructStore} store
|
||||||
|
* @param {function(Item):boolean} gcFilter
|
||||||
|
*/
|
||||||
|
export const tryGc = (ds, store, gcFilter) => {
|
||||||
|
tryGcDeleteSet(ds, store, gcFilter)
|
||||||
|
tryMergeDeleteSet(ds, store)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Array<Transaction>} transactionCleanups
|
* @param {Array<Transaction>} transactionCleanups
|
||||||
* @param {number} i
|
* @param {number} i
|
||||||
@@ -201,63 +280,12 @@ const cleanupTransactions = (transactionCleanups, i) => {
|
|||||||
})
|
})
|
||||||
callAll(fs, [])
|
callAll(fs, [])
|
||||||
} finally {
|
} 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.
|
// Replace deleted items with ItemDeleted / GC.
|
||||||
// This is where content is actually remove from the Yjs Doc.
|
// This is where content is actually remove from the Yjs Doc.
|
||||||
if (doc.gc) {
|
if (doc.gc) {
|
||||||
for (const [client, deleteItems] of ds.clients) {
|
tryGcDeleteSet(ds, store, doc.gcFilter)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
tryMergeDeleteSet(ds, store)
|
||||||
|
|
||||||
// on all affected store.clients props, try to merge
|
// on all affected store.clients props, try to merge
|
||||||
for (const [client, clock] of transaction.afterState) {
|
for (const [client, clock] of transaction.afterState) {
|
||||||
@@ -310,7 +338,6 @@ const cleanupTransactions = (transactionCleanups, i) => {
|
|||||||
* @param {function(Transaction):void} f
|
* @param {function(Transaction):void} f
|
||||||
* @param {any} [origin=true]
|
* @param {any} [origin=true]
|
||||||
*
|
*
|
||||||
* @private
|
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const transact = (doc, f, origin = null, local = true) => {
|
export const transact = (doc, f, origin = null, local = true) => {
|
||||||
|
|||||||
@@ -199,13 +199,32 @@ export class UndoManager extends Observable {
|
|||||||
// make sure that deleted structs are not gc'd
|
// make sure that deleted structs are not gc'd
|
||||||
iterateDeletedStructs(transaction, transaction.deleteSet, /** @param {Item|GC} item */ item => {
|
iterateDeletedStructs(transaction, transaction.deleteSet, /** @param {Item|GC} item */ item => {
|
||||||
if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) {
|
if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) {
|
||||||
keepItem(item)
|
keepItem(item, true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.emit('stack-item-added', [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo' }, this])
|
this.emit('stack-item-added', [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo' }, this])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clear () {
|
||||||
|
this.doc.transact(transaction => {
|
||||||
|
/**
|
||||||
|
* @param {StackItem} stackItem
|
||||||
|
*/
|
||||||
|
const clearItem = stackItem => {
|
||||||
|
iterateDeletedStructs(transaction, stackItem.ds, item => {
|
||||||
|
if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) {
|
||||||
|
keepItem(item, false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.undoStack.forEach(clearItem)
|
||||||
|
this.redoStack.forEach(clearItem)
|
||||||
|
})
|
||||||
|
this.undoStack = []
|
||||||
|
this.redoStack = []
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UndoManager merges Undo-StackItem if they are created within time-gap
|
* UndoManager merges Undo-StackItem if they are created within time-gap
|
||||||
* smaller than `options.captureTimeout`. Call `um.stopCapturing()` so that the next
|
* smaller than `options.captureTimeout`. Call `um.stopCapturing()` so that the next
|
||||||
|
|||||||
Reference in New Issue
Block a user