Merge remote-tracking branch 'origin/main' into improve-types-for-map-public-interface
This commit is contained in:
commit
cd79765f3c
8
.github/workflows/node.js.yml
vendored
8
.github/workflows/node.js.yml
vendored
@ -16,16 +16,16 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [16.x, 18.x]
|
node-version: [16.x, 20.x]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run lint
|
- run: npm run lint
|
||||||
- run: npm run test-extensive
|
- run: npm run test
|
||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
|
@ -149,8 +149,8 @@ concepts that can be used to create a custom network protocol:
|
|||||||
|
|
||||||
* `update`: The Yjs document can be encoded to an *update* object that can be
|
* `update`: The Yjs document can be encoded to an *update* object that can be
|
||||||
parsed to reconstruct the document. Also every change on the document fires
|
parsed to reconstruct the document. Also every change on the document fires
|
||||||
an incremental document updates that allows clients to sync with each other.
|
an incremental document update that allows clients to sync with each other.
|
||||||
The update object is an Uint8Array that efficiently encodes `Item` objects and
|
The update object is a Uint8Array that efficiently encodes `Item` objects and
|
||||||
the delete set.
|
the delete set.
|
||||||
* `state vector`: A state vector defines the known state of each user (a set of
|
* `state vector`: A state vector defines the known state of each user (a set of
|
||||||
tuples `(client, clock)`). This object is also efficiently encoded as a
|
tuples `(client, clock)`). This object is also efficiently encoded as a
|
||||||
|
4
LICENSE
4
LICENSE
@ -1,7 +1,7 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2014
|
Copyright (c) 2023
|
||||||
- Kevin Jahns <kevin.jahns@rwth-aachen.de>.
|
- Kevin Jahns <kevin.jahns@protonmail.com>.
|
||||||
- Chair of Computer Science 5 (Databases & Information Systems), RWTH Aachen University, Germany
|
- Chair of Computer Science 5 (Databases & Information Systems), RWTH Aachen University, Germany
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
137
README.md
137
README.md
@ -32,13 +32,30 @@ Otherwise you can find help on our community [discussion board](https://discuss.
|
|||||||
Please contribute to the project financially - especially if your company relies
|
Please contribute to the project financially - especially if your company relies
|
||||||
on Yjs. [](https://github.com/sponsors/dmonad)
|
on Yjs. [](https://github.com/sponsors/dmonad)
|
||||||
|
|
||||||
|
## Professional Support
|
||||||
|
|
||||||
|
* [Support Contract with the Maintainer](https://github.com/sponsors/dmonad) -
|
||||||
|
By contributing financially to the open-source Yjs project, you can receive
|
||||||
|
professional support directly from the author. This includes the opportunity for
|
||||||
|
weekly video calls to discuss your specific challenges.
|
||||||
|
* [Synergy Codes](https://synergycodes.com/yjs-services/) - Specializing in
|
||||||
|
consulting and developing real-time collaborative editing solutions for visual
|
||||||
|
apps, Synergy Codes focuses on interactive diagrams, complex graphs, charts, and
|
||||||
|
various data visualization types. Their expertise empowers developers to build
|
||||||
|
engaging and interactive visual experiences leveraging the power of Yjs. See
|
||||||
|
their work in action at [Visual Collaboration
|
||||||
|
Showcase](https://yjs-diagram.synergy.codes/).
|
||||||
|
|
||||||
## Who is using Yjs
|
## Who is using Yjs
|
||||||
|
|
||||||
* [AFFiNE](https://affine.pro/) A local-first, privacy-first, open source
|
* [AFFiNE](https://affine.pro/) A local-first, privacy-first, open source
|
||||||
knowledge base. 🏅
|
knowledge base. 🏅
|
||||||
* [Dynaboard](https://dynaboard.com/) Build web apps collaboratively. :star2:
|
* [Cargo](https://cargo.site/) Site builder for designers and artists :star2:
|
||||||
* [Sana](https://sanalabs.com/) A learning platform with collaborative text
|
* [Gitbook](https://gitbook.com) Knowledge management for technical teams :star2:
|
||||||
editing powered by Yjs.
|
* [Evernote](https://evernote.com) Note-taking app :star2:
|
||||||
|
* [Lessonspace](https://thelessonspace.com) Enterprise platform for virtual
|
||||||
|
classrooms and online training :star2:
|
||||||
|
* [Dynaboard](https://dynaboard.com/) Build web apps collaboratively. :star:
|
||||||
* [Relm](https://www.relm.us/) A collaborative gameworld for teamwork and
|
* [Relm](https://www.relm.us/) A collaborative gameworld for teamwork and
|
||||||
community. :star:
|
community. :star:
|
||||||
* [Room.sh](https://room.sh/) A meeting application with integrated
|
* [Room.sh](https://room.sh/) A meeting application with integrated
|
||||||
@ -47,6 +64,8 @@ on Yjs. [ A web-based app to
|
* [Pluxbox RadioManager](https://getradiomanager.com/) A web-based app to
|
||||||
collaboratively organize radio broadcasts. :star:
|
collaboratively organize radio broadcasts. :star:
|
||||||
|
* [Sana](https://sanalabs.com/) A learning platform with collaborative text
|
||||||
|
editing powered by Yjs.
|
||||||
* [Serenity Notes](https://www.serenity.re/en/notes) End-to-end encrypted
|
* [Serenity Notes](https://www.serenity.re/en/notes) End-to-end encrypted
|
||||||
collaborative notes app.
|
collaborative notes app.
|
||||||
* [PRSM](https://prsm.uk/) Collaborative mind-mapping and system visualisation. *[(source)](https://github.com/micrology/prsm)*
|
* [PRSM](https://prsm.uk/) Collaborative mind-mapping and system visualisation. *[(source)](https://github.com/micrology/prsm)*
|
||||||
@ -56,16 +75,29 @@ on Yjs. [ Presentation app.
|
* [Slidebeamer](https://slidebeamer.com/) Presentation app.
|
||||||
* [BlockSurvey](https://blocksurvey.io) End-to-end encryption for your forms/surveys.
|
* [BlockSurvey](https://blocksurvey.io) End-to-end encryption for your forms/surveys.
|
||||||
* [Skiff](https://skiff.org/) Private, decentralized workspace.
|
* [Skiff](https://skiff.org/) Private, decentralized workspace.
|
||||||
|
* [JupyterLab](https://jupyter.org/) Collaborative computational Notebooks
|
||||||
|
* [JupyterCad](https://jupytercad.readthedocs.io/en/latest/) Extension to
|
||||||
|
JupyterLab that enables collaborative editing of 3d FreeCAD Models.
|
||||||
* [Hyperquery](https://hyperquery.ai/) A collaborative data workspace for
|
* [Hyperquery](https://hyperquery.ai/) A collaborative data workspace for
|
||||||
sharing analyses, documentation, spreadsheets, and dashboards.
|
sharing analyses, documentation, spreadsheets, and dashboards.
|
||||||
* [Nosgestesclimat](https://nosgestesclimat.fr/groupe) The french carbon
|
* [Nosgestesclimat](https://nosgestesclimat.fr/groupe) The french carbon
|
||||||
footprint calculator has a group P2P mode based on yjs
|
footprint calculator has a group P2P mode based on yjs
|
||||||
|
* [oorja.io](https://oorja.io) Online meeting spaces extensible with
|
||||||
|
collaborative apps, end-to-end encrypted.
|
||||||
|
* [LegendKeeper](https://legendkeeper.com) Collaborative campaign planner and
|
||||||
|
worldbuilding app for tabletop RPGs.
|
||||||
|
* [IllumiDesk](https://illumidesk.com/) Build courses and content with A.I.
|
||||||
|
* [btw](https://www.btw.so) Open-source Medium alternative
|
||||||
|
* [AWS SageMaker](https://aws.amazon.com/sagemaker/) Tools for building Machine
|
||||||
|
Learning Models
|
||||||
|
* [linear](https://linear.app) Streamline issues, projects, and product roadmaps.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
* [Overview](#Overview)
|
* [Overview](#Overview)
|
||||||
* [Bindings](#Bindings)
|
* [Bindings](#Bindings)
|
||||||
* [Providers](#Providers)
|
* [Providers](#Providers)
|
||||||
|
* [Ports](#Ports)
|
||||||
* [Getting Started](#Getting-Started)
|
* [Getting Started](#Getting-Started)
|
||||||
* [API](#API)
|
* [API](#API)
|
||||||
* [Shared Types](#Shared-Types)
|
* [Shared Types](#Shared-Types)
|
||||||
@ -91,9 +123,11 @@ are implemented in separate modules.
|
|||||||
| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](https://github.com/yjs/y-codemirror) | [demo](https://demos.yjs.dev/codemirror/codemirror.html) |
|
| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](https://github.com/yjs/y-codemirror) | [demo](https://demos.yjs.dev/codemirror/codemirror.html) |
|
||||||
| [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](https://github.com/yjs/y-monaco) | [demo](https://demos.yjs.dev/monaco/monaco.html) |
|
| [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](https://github.com/yjs/y-monaco) | [demo](https://demos.yjs.dev/monaco/monaco.html) |
|
||||||
| [Slate](https://github.com/ianstormtaylor/slate) | ✔ | [slate-yjs](https://github.com/bitphinix/slate-yjs) | [demo](https://bitphinix.github.io/slate-yjs-example) |
|
| [Slate](https://github.com/ianstormtaylor/slate) | ✔ | [slate-yjs](https://github.com/bitphinix/slate-yjs) | [demo](https://bitphinix.github.io/slate-yjs-example) |
|
||||||
|
| [BlockSuite](https://github.com/toeverything/blocksuite) | ✔ | (native) | [demo](https://blocksuite-toeverything.vercel.app/?init) |
|
||||||
| [valtio](https://github.com/pmndrs/valtio) | | [valtio-yjs](https://github.com/dai-shi/valtio-yjs) | [demo](https://codesandbox.io/s/valtio-yjs-demo-ox3iy) |
|
| [valtio](https://github.com/pmndrs/valtio) | | [valtio-yjs](https://github.com/dai-shi/valtio-yjs) | [demo](https://codesandbox.io/s/valtio-yjs-demo-ox3iy) |
|
||||||
| [immer](https://github.com/immerjs/immer) | | [immer-yjs](https://github.com/sep2/immer-yjs) | [demo](https://codesandbox.io/s/immer-yjs-demo-6e0znb) |
|
| [immer](https://github.com/immerjs/immer) | | [immer-yjs](https://github.com/sep2/immer-yjs) | [demo](https://codesandbox.io/s/immer-yjs-demo-6e0znb) |
|
||||||
| React / Vue / Svelte / MobX | | [SyncedStore](https://syncedstore.org) | [demo](https://syncedstore.org/docs/react) |
|
| React / Vue / Svelte / MobX | | [SyncedStore](https://syncedstore.org) | [demo](https://syncedstore.org/docs/react) |
|
||||||
|
| [mobx-keystone](https://mobx-keystone.js.org/) | | [mobx-keystone-yjs](https://github.com/xaviergonz/mobx-keystone/tree/master/packages/mobx-keystone-yjs) | [demo](https://mobx-keystone.js.org/examples/yjs-binding) |
|
||||||
|
|
||||||
### Providers
|
### Providers
|
||||||
|
|
||||||
@ -102,7 +136,19 @@ and storing shared data for offline usage is quite a hassle. **Providers**
|
|||||||
manage all that for you and are the perfect starting point for your
|
manage all that for you and are the perfect starting point for your
|
||||||
collaborative app.
|
collaborative app.
|
||||||
|
|
||||||
|
> This list of providers is incomplete. Please open PRs to add your providers to
|
||||||
|
> this list!
|
||||||
|
|
||||||
|
#### Connection Providers
|
||||||
|
|
||||||
<dl>
|
<dl>
|
||||||
|
<dt><a href="https://github.com/yjs/y-websocket">y-websocket</a></dt>
|
||||||
|
<dd>
|
||||||
|
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
|
||||||
|
leveldb database. <b>y-sweet</b> and <b>ypy-websocket</b> (see below) are
|
||||||
|
compatible to the y-wesocket protocol.
|
||||||
|
</dd>
|
||||||
<dt><a href="https://github.com/yjs/y-webrtc">y-webrtc</a></dt>
|
<dt><a href="https://github.com/yjs/y-webrtc">y-webrtc</a></dt>
|
||||||
<dd>
|
<dd>
|
||||||
Propagates document updates peer-to-peer using WebRTC. The peers exchange
|
Propagates document updates peer-to-peer using WebRTC. The peers exchange
|
||||||
@ -111,17 +157,22 @@ are available. Communication over the signaling servers can be encrypted by
|
|||||||
providing a shared secret, keeping the connection information and the shared
|
providing a shared secret, keeping the connection information and the shared
|
||||||
document private.
|
document private.
|
||||||
</dd>
|
</dd>
|
||||||
<dt><a href="https://github.com/yjs/y-websocket">y-websocket</a></dt>
|
<dt><a href="https://github.com/liveblocks/liveblocks">@liveblocks/yjs</a></dt>
|
||||||
<dd>
|
<dd>
|
||||||
A module that contains a simple websocket backend and a websocket client that
|
<a href="https://liveblocks.io/document/yjs">Liveblocks Yjs</a> provides a fully
|
||||||
connects to that backend. The backend can be extended to persist updates in a
|
hosted WebSocket infrastructure and persisted data store for Yjs
|
||||||
leveldb database.
|
documents. No configuration or maintenance is required. It also features
|
||||||
|
Yjs webhook events, REST API to read and update Yjs documents, and a
|
||||||
|
browser DevTools extension.
|
||||||
</dd>
|
</dd>
|
||||||
<dt><a href="https://github.com/yjs/y-indexeddb">y-indexeddb</a></dt>
|
<dt><a href="https://github.com/drifting-in-space/y-sweet">y-sweet</a></dt>
|
||||||
<dd>
|
<dd>
|
||||||
Efficiently persists document updates to the browsers indexeddb database.
|
A standalone yjs server with persistence to S3 or filesystem. They offer a
|
||||||
The document is immediately available and only diffs need to be synced through the
|
<a href="https://y-sweet.cloud">cloud service</a> as well.
|
||||||
network provider.
|
</dd>
|
||||||
|
<dt><a href="https://docs.partykit.io/reference/y-partykit-api/">PartyKit</a></dt>
|
||||||
|
<dd>
|
||||||
|
Cloud service for building multiplayer apps.
|
||||||
</dd>
|
</dd>
|
||||||
<dt><a href="https://github.com/marcopolo/y-libp2p">y-libp2p</a></dt>
|
<dt><a href="https://github.com/marcopolo/y-libp2p">y-libp2p</a></dt>
|
||||||
<dd>
|
<dd>
|
||||||
@ -144,14 +195,59 @@ Use Matrix as transport and storage of Yjs updates, so you can focus building
|
|||||||
your client app and Matrix can provide powerful features like Authentication,
|
your client app and Matrix can provide powerful features like Authentication,
|
||||||
Authorization, Federation, hosting (self-hosting or SaaS) and even End-to-End
|
Authorization, Federation, hosting (self-hosting or SaaS) and even End-to-End
|
||||||
Encryption (E2EE).
|
Encryption (E2EE).
|
||||||
</dd>
|
</dd>
|
||||||
|
<dt><a href="https://github.com/y-crdt/yrb-actioncable">yrb-actioncable</a></dt>
|
||||||
|
<dd>
|
||||||
|
An ActionCable companion for Yjs clients. There is a fitting
|
||||||
|
<a href="https://github.com/y-crdt/yrb-redis">redis extension</a> as well.
|
||||||
|
</dd>
|
||||||
|
<dt><a href="https://github.com/y-crdt/ypy-websocket">ypy-websocket</a></dt>
|
||||||
|
<dd>
|
||||||
|
Websocket backend, written in Python.
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
#### Persistence Providers
|
||||||
|
|
||||||
|
<dl>
|
||||||
|
<dt><a href="https://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>
|
||||||
<dt><a href="https://github.com/MaxNoetzold/y-mongodb-provider">y-mongodb-provider</a></dt>
|
<dt><a href="https://github.com/MaxNoetzold/y-mongodb-provider">y-mongodb-provider</a></dt>
|
||||||
<dd>
|
<dd>
|
||||||
Adds persistent storage to a server with MongoDB. Can be used with the
|
Adds persistent storage to a server with MongoDB. Can be used with the
|
||||||
y-websocket provider.
|
y-websocket provider.
|
||||||
</dd>
|
</dd>
|
||||||
|
<dt><a href="https://github.com/toeverything/AFFiNE/tree/master/packages/y-indexeddb">
|
||||||
|
@toeverything/y-indexeddb</a></dt>
|
||||||
|
<dd>
|
||||||
|
Like y-indexeddb, but with sub-documents support and fully TypeScript.
|
||||||
|
</dd>
|
||||||
|
<dt><a href="https://github.com/podraven/y-fire">y-fire</a></dt>
|
||||||
|
<dd>
|
||||||
|
A database and connection provider for Yjs based on Firestore.
|
||||||
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
|
# Ports
|
||||||
|
|
||||||
|
There are several Yjs-compatible ports to other programming languages.
|
||||||
|
|
||||||
|
* [y-octo](https://github.com/toeverything/y-octo) - Rust implementation by
|
||||||
|
[AFFiNE](https://affine.pro)
|
||||||
|
* [y-crdt](https://github.com/y-crdt/y-crdt) - Rust implementation with multiple
|
||||||
|
language bindings to other languages
|
||||||
|
* [yrs](https://github.com/y-crdt/y-crdt/tree/main/yrs) - Rust interface
|
||||||
|
* [ypy](https://github.com/y-crdt/ypy) - Python binding
|
||||||
|
* [yrb](https://github.com/y-crdt/yrb) - Ruby binding
|
||||||
|
* [yswift](https://github.com/y-crdt/yswift) - Swift binding
|
||||||
|
* [yffi](https://github.com/y-crdt/y-crdt/tree/main/yffi) - C-FFI
|
||||||
|
* [ywasm](https://github.com/y-crdt/y-crdt/tree/main/ywasm) - WASM binding
|
||||||
|
* [ycs](https://github.com/yjs/ycs) - .Net compatible C# implementation.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
Install Yjs and a provider with your favorite package manager:
|
Install Yjs and a provider with your favorite package manager:
|
||||||
@ -647,6 +743,8 @@ type. Doesn't log types that have not been defined (using
|
|||||||
<dd>Define a shared Y.Map type. Is equivalent to <code>y.get(string, Y.Map)</code>.</dd>
|
<dd>Define a shared Y.Map type. Is equivalent to <code>y.get(string, Y.Map)</code>.</dd>
|
||||||
<b><code>getText(string):Y.Text</code></b>
|
<b><code>getText(string):Y.Text</code></b>
|
||||||
<dd>Define a shared Y.Text type. Is equivalent to <code>y.get(string, Y.Text)</code>.</dd>
|
<dd>Define a shared Y.Text type. Is equivalent to <code>y.get(string, Y.Text)</code>.</dd>
|
||||||
|
<b><code>getXmlElement(string, string):Y.XmlElement</code></b>
|
||||||
|
<dd>Define a shared Y.XmlElement type. Is equivalent to <code>y.get(string, Y.XmlElement)</code>.</dd>
|
||||||
<b><code>getXmlFragment(string):Y.XmlFragment</code></b>
|
<b><code>getXmlFragment(string):Y.XmlFragment</code></b>
|
||||||
<dd>Define a shared Y.XmlFragment type. Is equivalent to <code>y.get(string, Y.XmlFragment)</code>.</dd>
|
<dd>Define a shared Y.XmlFragment type. Is equivalent to <code>y.get(string, Y.XmlFragment)</code>.</dd>
|
||||||
<b><code>on(string, function)</code></b>
|
<b><code>on(string, function)</code></b>
|
||||||
@ -661,7 +759,8 @@ type. Doesn't log types that have not been defined (using
|
|||||||
<b><code>on('update', function(updateMessage:Uint8Array, origin:any, Y.Doc):void)</code></b>
|
<b><code>on('update', function(updateMessage:Uint8Array, origin:any, Y.Doc):void)</code></b>
|
||||||
<dd>
|
<dd>
|
||||||
Listen to document updates. Document updates must be transmitted to all other
|
Listen to document updates. Document updates must be transmitted to all other
|
||||||
peers. You can apply document updates in any order and multiple times.
|
peers. You can apply document updates in any order and multiple times. Use `updateV2`
|
||||||
|
to receive V2 events.
|
||||||
</dd>
|
</dd>
|
||||||
<b><code>on('beforeTransaction', function(Y.Transaction, Y.Doc):void)</code></b>
|
<b><code>on('beforeTransaction', function(Y.Transaction, Y.Doc):void)</code></b>
|
||||||
<dd>Emitted before each transaction.</dd>
|
<dd>Emitted before each transaction.</dd>
|
||||||
@ -753,7 +852,7 @@ const diff2 = Y.diffUpdate(currentState2, stateVector1)
|
|||||||
|
|
||||||
// sync clients
|
// sync clients
|
||||||
currentState1 = Y.mergeUpdates([currentState1, diff2])
|
currentState1 = Y.mergeUpdates([currentState1, diff2])
|
||||||
currentState1 = Y.mergeUpdates([currentState1, diff1])
|
currentState2 = Y.mergeUpdates([currentState2, diff1])
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Obfuscating Updates
|
#### Obfuscating Updates
|
||||||
@ -786,8 +885,10 @@ Yjs implements two update formats. By default you are using the V1 update format
|
|||||||
You can opt-in into the V2 update format wich provides much better compression.
|
You can opt-in into the V2 update format wich provides much better compression.
|
||||||
It is not yet used by all providers. However, you can already use it if
|
It is not yet used by all providers. However, you can already use it if
|
||||||
you are building your own provider. All below functions are available with the
|
you are building your own provider. All below functions are available with the
|
||||||
suffix "V2". E.g. `Y.applyUpdate` ⇒ `Y.applyUpdateV2`. We also support conversion
|
suffix "V2". E.g. `Y.applyUpdate` ⇒ `Y.applyUpdateV2`. Also when listening to updates
|
||||||
functions between both formats: `Y.convertUpdateFormatV1ToV2` & `Y.convertUpdateFormatV2ToV1`.
|
you need to specifically need listen for V2 events e.g. `yDoc.on('updateV2', …)`.
|
||||||
|
We also support conversion functions between both formats:
|
||||||
|
`Y.convertUpdateFormatV1ToV2` & `Y.convertUpdateFormatV2ToV1`.
|
||||||
|
|
||||||
#### Update API
|
#### Update API
|
||||||
|
|
||||||
@ -1032,7 +1133,7 @@ doc.transact(() => {
|
|||||||
ytext.insert(0, 'abc')
|
ytext.insert(0, 'abc')
|
||||||
}, 41)
|
}, 41)
|
||||||
undoManager.undo()
|
undoManager.undo()
|
||||||
ytext.toString() // => '' (not tracked because 41 is not an instance of
|
ytext.toString() // => 'abc' (not tracked because 41 is not an instance of
|
||||||
// `trackedTransactionorigins`)
|
// `trackedTransactionorigins`)
|
||||||
ytext.delete(0, 3) // revert change
|
ytext.delete(0, 3) // revert change
|
||||||
|
|
||||||
|
1133
package-lock.json
generated
1133
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.6.1",
|
"version": "13.6.12",
|
||||||
"description": "Shared Editing Library",
|
"description": "Shared Editing Library",
|
||||||
"main": "./dist/yjs.cjs",
|
"main": "./dist/yjs.cjs",
|
||||||
"module": "./dist/yjs.mjs",
|
"module": "./dist/yjs.mjs",
|
||||||
@ -75,7 +75,7 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://docs.yjs.dev",
|
"homepage": "https://docs.yjs.dev",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lib0": "^0.2.74"
|
"lib0": "^0.2.86"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^24.0.1",
|
"@rollup/plugin-commonjs": "^24.0.1",
|
||||||
|
10
src/index.js
10
src/index.js
@ -18,8 +18,10 @@ export {
|
|||||||
Item,
|
Item,
|
||||||
AbstractStruct,
|
AbstractStruct,
|
||||||
GC,
|
GC,
|
||||||
|
Skip,
|
||||||
ContentBinary,
|
ContentBinary,
|
||||||
ContentDeleted,
|
ContentDeleted,
|
||||||
|
ContentDoc,
|
||||||
ContentEmbed,
|
ContentEmbed,
|
||||||
ContentFormat,
|
ContentFormat,
|
||||||
ContentJSON,
|
ContentJSON,
|
||||||
@ -50,6 +52,7 @@ export {
|
|||||||
getItem,
|
getItem,
|
||||||
typeListToArraySnapshot,
|
typeListToArraySnapshot,
|
||||||
typeMapGetSnapshot,
|
typeMapGetSnapshot,
|
||||||
|
typeMapGetAllSnapshot,
|
||||||
createDocFromSnapshot,
|
createDocFromSnapshot,
|
||||||
iterateDeletedStructs,
|
iterateDeletedStructs,
|
||||||
applyUpdate,
|
applyUpdate,
|
||||||
@ -92,7 +95,12 @@ export {
|
|||||||
convertUpdateFormatV2ToV1,
|
convertUpdateFormatV2ToV1,
|
||||||
obfuscateUpdate,
|
obfuscateUpdate,
|
||||||
obfuscateUpdateV2,
|
obfuscateUpdateV2,
|
||||||
UpdateEncoderV1
|
UpdateEncoderV1,
|
||||||
|
UpdateEncoderV2,
|
||||||
|
UpdateDecoderV1,
|
||||||
|
UpdateDecoderV2,
|
||||||
|
equalDeleteSets,
|
||||||
|
snapshotContainsUpdate
|
||||||
} from './internals.js'
|
} from './internals.js'
|
||||||
|
|
||||||
const glo = /** @type {any} */ (typeof globalThis !== 'undefined'
|
const glo = /** @type {any} */ (typeof globalThis !== 'undefined'
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
export * from './utils/AbstractConnector.js'
|
export * from './utils/AbstractConnector.js'
|
||||||
export * from './utils/DeleteSet.js'
|
export * from './utils/DeleteSet.js'
|
||||||
export * from './utils/Doc.js'
|
export * from './utils/Doc.js'
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
UpdateEncoderV1, UpdateEncoderV2, ID, Transaction // eslint-disable-line
|
UpdateEncoderV1, UpdateEncoderV2, ID, Transaction // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
addToDeleteSet,
|
addToDeleteSet,
|
||||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line
|
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
Doc, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item // eslint-disable-line
|
Doc, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line
|
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Item, StructStore, Transaction // eslint-disable-line
|
YText, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Item, StructStore, Transaction // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as error from 'lib0/error'
|
import * as error from 'lib0/error'
|
||||||
@ -47,28 +46,30 @@ export class ContentFormat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} offset
|
* @param {number} _offset
|
||||||
* @return {ContentFormat}
|
* @return {ContentFormat}
|
||||||
*/
|
*/
|
||||||
splice (offset) {
|
splice (_offset) {
|
||||||
throw error.methodUnimplemented()
|
throw error.methodUnimplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {ContentFormat} right
|
* @param {ContentFormat} _right
|
||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
*/
|
*/
|
||||||
mergeWith (right) {
|
mergeWith (_right) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Transaction} transaction
|
* @param {Transaction} _transaction
|
||||||
* @param {Item} item
|
* @param {Item} item
|
||||||
*/
|
*/
|
||||||
integrate (transaction, item) {
|
integrate (_transaction, item) {
|
||||||
// @todo searchmarker are currently unsupported for rich text documents
|
// @todo searchmarker are currently unsupported for rich text documents
|
||||||
/** @type {AbstractType<any>} */ (item.parent)._searchMarker = null
|
const p = /** @type {YText} */ (item.parent)
|
||||||
|
p._searchMarker = null
|
||||||
|
p._hasFormatting = true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
readYArray,
|
readYArray,
|
||||||
readYMap,
|
readYMap,
|
||||||
@ -108,7 +107,7 @@ export class ContentType {
|
|||||||
while (item !== null) {
|
while (item !== null) {
|
||||||
if (!item.deleted) {
|
if (!item.deleted) {
|
||||||
item.delete(transaction)
|
item.delete(transaction)
|
||||||
} else {
|
} else if (item.id.clock < (transaction.beforeState.get(item.id.client) || 0)) {
|
||||||
// This will be gc'd later and we want to merge it if possible
|
// This will be gc'd later and we want to merge it if possible
|
||||||
// We try to merge all deleted items after each transaction,
|
// We try to merge all deleted items after each transaction,
|
||||||
// but we have no knowledge about that this needs to be merged
|
// but we have no knowledge about that this needs to be merged
|
||||||
@ -120,7 +119,7 @@ export class ContentType {
|
|||||||
this.type._map.forEach(item => {
|
this.type._map.forEach(item => {
|
||||||
if (!item.deleted) {
|
if (!item.deleted) {
|
||||||
item.delete(transaction)
|
item.delete(transaction)
|
||||||
} else {
|
} else if (item.id.clock < (transaction.beforeState.get(item.id.client) || 0)) {
|
||||||
// same as above
|
// same as above
|
||||||
transaction._mergeStructs.push(item)
|
transaction._mergeStructs.push(item)
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
AbstractStruct,
|
AbstractStruct,
|
||||||
addStruct,
|
addStruct,
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
GC,
|
GC,
|
||||||
getState,
|
getState,
|
||||||
@ -389,9 +388,8 @@ export class Item extends AbstractStruct {
|
|||||||
}
|
}
|
||||||
if ((this.left && this.left.constructor === GC) || (this.right && this.right.constructor === GC)) {
|
if ((this.left && this.left.constructor === GC) || (this.right && this.right.constructor === GC)) {
|
||||||
this.parent = null
|
this.parent = null
|
||||||
}
|
} else if (!this.parent) {
|
||||||
// only set parent if this shouldn't be garbage collected
|
// only set parent if this shouldn't be garbage collected
|
||||||
if (!this.parent) {
|
|
||||||
if (this.left && this.left.constructor === Item) {
|
if (this.left && this.left.constructor === Item) {
|
||||||
this.parent = this.left.parent
|
this.parent = this.left.parent
|
||||||
this.parentSub = this.left.parentSub
|
this.parentSub = this.left.parentSub
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
AbstractStruct,
|
AbstractStruct,
|
||||||
UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, ID // eslint-disable-line
|
UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, ID // eslint-disable-line
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
removeEventHandlerListener,
|
removeEventHandlerListener,
|
||||||
callEventHandlerListeners,
|
callEventHandlerListeners,
|
||||||
@ -683,7 +682,7 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
|
|||||||
packJsonContent()
|
packJsonContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
const lengthExceeded = error.create('Length exceeded!')
|
const lengthExceeded = () => error.create('Length exceeded!')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Transaction} transaction
|
* @param {Transaction} transaction
|
||||||
@ -696,7 +695,7 @@ const lengthExceeded = error.create('Length exceeded!')
|
|||||||
*/
|
*/
|
||||||
export const typeListInsertGenerics = (transaction, parent, index, content) => {
|
export const typeListInsertGenerics = (transaction, parent, index, content) => {
|
||||||
if (index > parent._length) {
|
if (index > parent._length) {
|
||||||
throw lengthExceeded
|
throw lengthExceeded()
|
||||||
}
|
}
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
if (parent._searchMarker) {
|
if (parent._searchMarker) {
|
||||||
@ -798,7 +797,7 @@ export const typeListDelete = (transaction, parent, index, length) => {
|
|||||||
n = n.right
|
n = n.right
|
||||||
}
|
}
|
||||||
if (length > 0) {
|
if (length > 0) {
|
||||||
throw lengthExceeded
|
throw lengthExceeded()
|
||||||
}
|
}
|
||||||
if (parent._searchMarker) {
|
if (parent._searchMarker) {
|
||||||
updateMarkerChanges(parent._searchMarker, startIndex, -startLength + length /* in case we remove the above exception */)
|
updateMarkerChanges(parent._searchMarker, startIndex, -startLength + length /* in case we remove the above exception */)
|
||||||
@ -925,6 +924,34 @@ export const typeMapGetSnapshot = (parent, key, snapshot) => {
|
|||||||
return v !== null && isVisible(v, snapshot) ? v.content.getContent()[v.length - 1] : undefined
|
return v !== null && isVisible(v, snapshot) ? v.content.getContent()[v.length - 1] : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {AbstractType<any>} parent
|
||||||
|
* @param {Snapshot} snapshot
|
||||||
|
* @return {Object<string,Object<string,any>|number|null|Array<any>|string|Uint8Array|AbstractType<any>|undefined>}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const typeMapGetAllSnapshot = (parent, snapshot) => {
|
||||||
|
/**
|
||||||
|
* @type {Object<string,any>}
|
||||||
|
*/
|
||||||
|
const res = {}
|
||||||
|
parent._map.forEach((value, key) => {
|
||||||
|
/**
|
||||||
|
* @type {Item|null}
|
||||||
|
*/
|
||||||
|
let v = value
|
||||||
|
while (v !== null && (!snapshot.sv.has(v.id.client) || v.id.clock >= (snapshot.sv.get(v.id.client) || 0))) {
|
||||||
|
v = v.left
|
||||||
|
}
|
||||||
|
if (v !== null && isVisible(v, snapshot)) {
|
||||||
|
res[key] = v.content.getContent()[v.length - 1]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Map<string,Item>} map
|
* @param {Map<string,Item>} map
|
||||||
* @return {IterableIterator<Array<any>>}
|
* @return {IterableIterator<Array<any>>}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @module YMap
|
* @module YMap
|
||||||
*/
|
*/
|
||||||
@ -66,7 +65,7 @@ export class YMapEvent extends YEvent {
|
|||||||
* A shared Map implementation.
|
* A shared Map implementation.
|
||||||
*
|
*
|
||||||
* @extends AbstractType<YMapEvent<MapType>>
|
* @extends AbstractType<YMapEvent<MapType>>
|
||||||
* @implements {Iterable<MapType>}
|
* @implements {Iterable<[string, MapType]>}
|
||||||
*/
|
*/
|
||||||
export class YMap extends AbstractType {
|
export class YMap extends AbstractType {
|
||||||
/**
|
/**
|
||||||
@ -177,7 +176,7 @@ export class YMap extends AbstractType {
|
|||||||
/**
|
/**
|
||||||
* Returns the values for each element in the YMap Type.
|
* Returns the values for each element in the YMap Type.
|
||||||
*
|
*
|
||||||
* @return {IterableIterator<any>}
|
* @return {IterableIterator<MapType>}
|
||||||
*/
|
*/
|
||||||
values () {
|
values () {
|
||||||
return iterator.iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => v[1].content.getContent()[v[1].length - 1])
|
return iterator.iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => v[1].content.getContent()[v[1].length - 1])
|
||||||
@ -186,10 +185,10 @@ export class YMap extends AbstractType {
|
|||||||
/**
|
/**
|
||||||
* Returns an Iterator of [key, value] pairs
|
* Returns an Iterator of [key, value] pairs
|
||||||
*
|
*
|
||||||
* @return {IterableIterator<any>}
|
* @return {IterableIterator<[string, MapType]>}
|
||||||
*/
|
*/
|
||||||
entries () {
|
entries () {
|
||||||
return iterator.iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => [v[0], v[1].content.getContent()[v[1].length - 1]])
|
return iterator.iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => /** @type {any} */ ([v[0], v[1].content.getContent()[v[1].length - 1]]))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -208,7 +207,7 @@ export class YMap extends AbstractType {
|
|||||||
/**
|
/**
|
||||||
* Returns an Iterator of [key, value] pairs
|
* Returns an Iterator of [key, value] pairs
|
||||||
*
|
*
|
||||||
* @return {IterableIterator<any>}
|
* @return {IterableIterator<[string, MapType]>}
|
||||||
*/
|
*/
|
||||||
[Symbol.iterator] () {
|
[Symbol.iterator] () {
|
||||||
return this.entries()
|
return this.entries()
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @module YText
|
* @module YText
|
||||||
*/
|
*/
|
||||||
@ -118,14 +117,15 @@ const findNextPosition = (transaction, pos, count) => {
|
|||||||
* @param {Transaction} transaction
|
* @param {Transaction} transaction
|
||||||
* @param {AbstractType<any>} parent
|
* @param {AbstractType<any>} parent
|
||||||
* @param {number} index
|
* @param {number} index
|
||||||
|
* @param {boolean} useSearchMarker
|
||||||
* @return {ItemTextListPosition}
|
* @return {ItemTextListPosition}
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
const findPosition = (transaction, parent, index) => {
|
const findPosition = (transaction, parent, index, useSearchMarker) => {
|
||||||
const currentAttributes = new Map()
|
const currentAttributes = new Map()
|
||||||
const marker = findMarker(parent, index)
|
const marker = useSearchMarker ? findMarker(parent, index) : null
|
||||||
if (marker) {
|
if (marker) {
|
||||||
const pos = new ItemTextListPosition(marker.p.left, marker.p, marker.index, currentAttributes)
|
const pos = new ItemTextListPosition(marker.p.left, marker.p, marker.index, currentAttributes)
|
||||||
return findNextPosition(transaction, pos, index - marker.index)
|
return findNextPosition(transaction, pos, index - marker.index)
|
||||||
@ -476,6 +476,56 @@ export const cleanupYTextFormatting = type => {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will be called by the transction once the event handlers are called to potentially cleanup
|
||||||
|
* formatting attributes.
|
||||||
|
*
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
*/
|
||||||
|
export const cleanupYTextAfterTransaction = transaction => {
|
||||||
|
/**
|
||||||
|
* @type {Set<YText>}
|
||||||
|
*/
|
||||||
|
const needFullCleanup = new Set()
|
||||||
|
// check if another formatting item was inserted
|
||||||
|
const doc = transaction.doc
|
||||||
|
for (const [client, afterClock] of transaction.afterState.entries()) {
|
||||||
|
const clock = transaction.beforeState.get(client) || 0
|
||||||
|
if (afterClock === clock) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
iterateStructs(transaction, /** @type {Array<Item|GC>} */ (doc.store.clients.get(client)), clock, afterClock, item => {
|
||||||
|
if (
|
||||||
|
!item.deleted && /** @type {Item} */ (item).content.constructor === ContentFormat && item.constructor !== GC
|
||||||
|
) {
|
||||||
|
needFullCleanup.add(/** @type {any} */ (item).parent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// cleanup in a new transaction
|
||||||
|
transact(doc, (t) => {
|
||||||
|
iterateDeletedStructs(transaction, transaction.deleteSet, item => {
|
||||||
|
if (item instanceof GC || !(/** @type {YText} */ (item.parent)._hasFormatting) || needFullCleanup.has(/** @type {YText} */ (item.parent))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const parent = /** @type {YText} */ (item.parent)
|
||||||
|
if (item.content.constructor === ContentFormat) {
|
||||||
|
needFullCleanup.add(parent)
|
||||||
|
} else {
|
||||||
|
// If no formatting attribute was inserted or deleted, we can make due with contextless
|
||||||
|
// formatting cleanups.
|
||||||
|
// Contextless: it is not necessary to compute currentAttributes for the affected position.
|
||||||
|
cleanupContextlessFormattingGap(t, item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// If a formatting item was inserted, we simply clean the whole type.
|
||||||
|
// We need to compute currentAttributes for the current position anyway.
|
||||||
|
for (const yText of needFullCleanup) {
|
||||||
|
cleanupYTextFormatting(yText)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Transaction} transaction
|
* @param {Transaction} transaction
|
||||||
* @param {ItemTextListPosition} currPos
|
* @param {ItemTextListPosition} currPos
|
||||||
@ -809,9 +859,14 @@ export class YText extends AbstractType {
|
|||||||
*/
|
*/
|
||||||
this._pending = string !== undefined ? [() => this.insert(0, string)] : []
|
this._pending = string !== undefined ? [() => this.insert(0, string)] : []
|
||||||
/**
|
/**
|
||||||
* @type {Array<ArraySearchMarker>}
|
* @type {Array<ArraySearchMarker>|null}
|
||||||
*/
|
*/
|
||||||
this._searchMarker = []
|
this._searchMarker = []
|
||||||
|
/**
|
||||||
|
* Whether this YText contains formatting attributes.
|
||||||
|
* This flag is updated when a formatting item is integrated (see ContentFormat.integrate)
|
||||||
|
*/
|
||||||
|
this._hasFormatting = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -859,55 +914,10 @@ export class YText extends AbstractType {
|
|||||||
_callObserver (transaction, parentSubs) {
|
_callObserver (transaction, parentSubs) {
|
||||||
super._callObserver(transaction, parentSubs)
|
super._callObserver(transaction, parentSubs)
|
||||||
const event = new YTextEvent(this, transaction, parentSubs)
|
const event = new YTextEvent(this, transaction, parentSubs)
|
||||||
const doc = transaction.doc
|
|
||||||
callTypeObservers(this, transaction, event)
|
callTypeObservers(this, transaction, event)
|
||||||
// If a remote change happened, we try to cleanup potential formatting duplicates.
|
// If a remote change happened, we try to cleanup potential formatting duplicates.
|
||||||
if (!transaction.local) {
|
if (!transaction.local && this._hasFormatting) {
|
||||||
// check if another formatting item was inserted
|
transaction._needFormattingCleanup = true
|
||||||
let foundFormattingItem = false
|
|
||||||
for (const [client, afterClock] of transaction.afterState.entries()) {
|
|
||||||
const clock = transaction.beforeState.get(client) || 0
|
|
||||||
if (afterClock === clock) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
iterateStructs(transaction, /** @type {Array<Item|GC>} */ (doc.store.clients.get(client)), clock, afterClock, item => {
|
|
||||||
if (!item.deleted && /** @type {Item} */ (item).content.constructor === ContentFormat) {
|
|
||||||
foundFormattingItem = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (foundFormattingItem) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!foundFormattingItem) {
|
|
||||||
iterateDeletedStructs(transaction, transaction.deleteSet, item => {
|
|
||||||
if (item instanceof GC || foundFormattingItem) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (item.parent === this && item.content.constructor === ContentFormat) {
|
|
||||||
foundFormattingItem = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
transact(doc, (t) => {
|
|
||||||
if (foundFormattingItem) {
|
|
||||||
// If a formatting item was inserted, we simply clean the whole type.
|
|
||||||
// We need to compute currentAttributes for the current position anyway.
|
|
||||||
cleanupYTextFormatting(this)
|
|
||||||
} else {
|
|
||||||
// If no formatting attribute was inserted, we can make due with contextless
|
|
||||||
// formatting cleanups.
|
|
||||||
// Contextless: it is not necessary to compute currentAttributes for the affected position.
|
|
||||||
iterateDeletedStructs(t, t.deleteSet, item => {
|
|
||||||
if (item instanceof GC) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (item.parent === this) {
|
|
||||||
cleanupContextlessFormattingGap(t, item)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1110,7 +1120,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 => {
|
||||||
const pos = findPosition(transaction, this, index)
|
const pos = findPosition(transaction, this, index, !attributes)
|
||||||
if (!attributes) {
|
if (!attributes) {
|
||||||
attributes = {}
|
attributes = {}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -1128,20 +1138,20 @@ export class YText extends AbstractType {
|
|||||||
*
|
*
|
||||||
* @param {number} index The index to insert the embed at.
|
* @param {number} index The index to insert the embed at.
|
||||||
* @param {Object | AbstractType<any>} embed The Object that represents the embed.
|
* @param {Object | AbstractType<any>} embed The Object that represents the embed.
|
||||||
* @param {TextAttributes} attributes Attribute information to apply on the
|
* @param {TextAttributes} [attributes] Attribute information to apply on the
|
||||||
* embed
|
* embed
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
insertEmbed (index, embed, attributes = {}) {
|
insertEmbed (index, embed, attributes) {
|
||||||
const y = this.doc
|
const y = this.doc
|
||||||
if (y !== null) {
|
if (y !== null) {
|
||||||
transact(y, transaction => {
|
transact(y, transaction => {
|
||||||
const pos = findPosition(transaction, this, index)
|
const pos = findPosition(transaction, this, index, !attributes)
|
||||||
insertText(transaction, this, pos, embed, attributes)
|
insertText(transaction, this, pos, embed, attributes || {})
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
/** @type {Array<function>} */ (this._pending).push(() => this.insertEmbed(index, embed, attributes))
|
/** @type {Array<function>} */ (this._pending).push(() => this.insertEmbed(index, embed, attributes || {}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1160,7 +1170,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 => {
|
||||||
deleteText(transaction, findPosition(transaction, this, index), length)
|
deleteText(transaction, findPosition(transaction, this, index, true), length)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
/** @type {Array<function>} */ (this._pending).push(() => this.delete(index, length))
|
/** @type {Array<function>} */ (this._pending).push(() => this.delete(index, length))
|
||||||
@ -1184,7 +1194,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 => {
|
||||||
const pos = findPosition(transaction, this, index)
|
const pos = findPosition(transaction, this, index, false)
|
||||||
if (pos.right === null) {
|
if (pos.right === null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -8,9 +8,10 @@ import {
|
|||||||
typeMapSet,
|
typeMapSet,
|
||||||
typeMapGet,
|
typeMapGet,
|
||||||
typeMapGetAll,
|
typeMapGetAll,
|
||||||
|
typeMapGetAllSnapshot,
|
||||||
typeListForEach,
|
typeListForEach,
|
||||||
YXmlElementRefID,
|
YXmlElementRefID,
|
||||||
YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item // eslint-disable-line
|
Snapshot, YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -19,7 +20,7 @@ import {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* An YXmlElement imitates the behavior of a
|
* An YXmlElement imitates the behavior of a
|
||||||
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}.
|
* https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element
|
||||||
*
|
*
|
||||||
* * An YXmlElement has attributes (key value pairs)
|
* * An YXmlElement has attributes (key value pairs)
|
||||||
* * An YXmlElement has childElements that must inherit from YXmlElement
|
* * An YXmlElement has childElements that must inherit from YXmlElement
|
||||||
@ -192,12 +193,13 @@ 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]
|
||||||
* @return {{ [Key in Extract<keyof KV,string>]?: KV[Key]}} A JSON Object that describes the attributes.
|
* @return {{ [Key in Extract<keyof KV,string>]?: KV[Key]}} A JSON Object that describes the attributes.
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
getAttributes () {
|
getAttributes (snapshot) {
|
||||||
return /** @type {any} */ (typeMapGetAll(this))
|
return /** @type {any} */ (snapshot ? typeMapGetAllSnapshot(this, snapshot) : typeMapGetAll(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
YEvent,
|
YEvent,
|
||||||
YXmlText, YXmlElement, YXmlFragment, Transaction // eslint-disable-line
|
YXmlText, YXmlElement, YXmlFragment, Transaction // eslint-disable-line
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
YMap,
|
YMap,
|
||||||
YXmlHookRefID,
|
YXmlHookRefID,
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
YText,
|
YText,
|
||||||
YXmlTextRefID,
|
YXmlTextRefID,
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { Observable } from 'lib0/observable'
|
import { Observable } from 'lib0/observable'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
findIndexSS,
|
findIndexSS,
|
||||||
getState,
|
getState,
|
||||||
@ -328,3 +327,23 @@ export const readAndApplyDeleteSet = (decoder, transaction, store) => {
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DeleteSet} ds1
|
||||||
|
* @param {DeleteSet} ds2
|
||||||
|
*/
|
||||||
|
export const equalDeleteSets = (ds1, ds2) => {
|
||||||
|
if (ds1.clients.size !== ds2.clients.size) return false
|
||||||
|
for (const [client, deleteItems1] of ds1.clients.entries()) {
|
||||||
|
const deleteItems2 = /** @type {Array<import('../internals.js').DeleteItem>} */ (ds2.clients.get(client))
|
||||||
|
if (deleteItems2 === undefined || deleteItems1.length !== deleteItems2.length) return false
|
||||||
|
for (let i = 0; i < deleteItems1.length; i++) {
|
||||||
|
const di1 = deleteItems1[i]
|
||||||
|
const di2 = deleteItems2[i]
|
||||||
|
if (di1.clock !== di2.clock || di1.len !== di2.len) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
YArray,
|
YArray,
|
||||||
YText,
|
YText,
|
||||||
YMap,
|
YMap,
|
||||||
|
YXmlElement,
|
||||||
YXmlFragment,
|
YXmlFragment,
|
||||||
transact,
|
transact,
|
||||||
ContentDoc, Item, Transaction, YEvent // eslint-disable-line
|
ContentDoc, Item, Transaction, YEvent // eslint-disable-line
|
||||||
@ -113,7 +114,7 @@ export class Doc extends Observable {
|
|||||||
this.whenSynced = provideSyncedPromise()
|
this.whenSynced = provideSyncedPromise()
|
||||||
}
|
}
|
||||||
this.isSynced = isSynced === undefined || isSynced === true
|
this.isSynced = isSynced === undefined || isSynced === true
|
||||||
if (!this.isLoaded) {
|
if (this.isSynced && !this.isLoaded) {
|
||||||
this.emit('load', [])
|
this.emit('load', [])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -180,6 +181,7 @@ export class Doc extends Observable {
|
|||||||
* Define all types right after the Yjs instance is created and store them in a separate object.
|
* Define all types right after the Yjs instance is created and store them in a separate object.
|
||||||
* Also use the typed methods `getText(name)`, `getArray(name)`, ..
|
* Also use the typed methods `getText(name)`, `getArray(name)`, ..
|
||||||
*
|
*
|
||||||
|
* @template {typeof AbstractType<any>} Type
|
||||||
* @example
|
* @example
|
||||||
* const y = new Y(..)
|
* const y = new Y(..)
|
||||||
* const appState = {
|
* const appState = {
|
||||||
@ -188,12 +190,12 @@ export class Doc extends Observable {
|
|||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @param {Function} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ...
|
* @param {Type} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ...
|
||||||
* @return {AbstractType<any>} The created type. Constructed with TypeConstructor
|
* @return {InstanceType<Type>} The created type. Constructed with TypeConstructor
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
get (name, TypeConstructor = AbstractType) {
|
get (name, TypeConstructor = /** @type {any} */ (AbstractType)) {
|
||||||
const type = map.setIfUndefined(this.share, name, () => {
|
const type = map.setIfUndefined(this.share, name, () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const t = new TypeConstructor()
|
const t = new TypeConstructor()
|
||||||
@ -219,12 +221,12 @@ export class Doc extends Observable {
|
|||||||
t._length = type._length
|
t._length = type._length
|
||||||
this.share.set(name, t)
|
this.share.set(name, t)
|
||||||
t._integrate(this, null)
|
t._integrate(this, null)
|
||||||
return t
|
return /** @type {InstanceType<Type>} */ (t)
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Type with the name ${name} has already been defined with a different constructor`)
|
throw new Error(`Type with the name ${name} has already been defined with a different constructor`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return type
|
return /** @type {InstanceType<Type>} */ (type)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -235,8 +237,7 @@ export class Doc extends Observable {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
getArray (name = '') {
|
getArray (name = '') {
|
||||||
// @ts-ignore
|
return /** @type {YArray<T>} */ (this.get(name, YArray))
|
||||||
return this.get(name, YArray)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -246,7 +247,6 @@ export class Doc extends Observable {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
getText (name = '') {
|
getText (name = '') {
|
||||||
// @ts-ignore
|
|
||||||
return this.get(name, YText)
|
return this.get(name, YText)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,8 +258,17 @@ export class Doc extends Observable {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
getMap (name = '') {
|
getMap (name = '') {
|
||||||
// @ts-ignore
|
return /** @type {YMap<T>} */ (this.get(name, YMap))
|
||||||
return this.get(name, YMap)
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} [name]
|
||||||
|
* @return {YXmlElement}
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
getXmlElement (name = '') {
|
||||||
|
return /** @type {YXmlElement<{[key:string]:string}>} */ (this.get(name, YXmlElement))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -269,7 +278,6 @@ export class Doc extends Observable {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
getXmlFragment (name = '') {
|
getXmlFragment (name = '') {
|
||||||
// @ts-ignore
|
|
||||||
return this.get(name, YXmlFragment)
|
return this.get(name, YXmlFragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { AbstractType } from '../internals.js' // eslint-disable-line
|
import { AbstractType } from '../internals.js' // eslint-disable-line
|
||||||
|
|
||||||
import * as decoding from 'lib0/decoding'
|
import * as decoding from 'lib0/decoding'
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
YArray,
|
YArray,
|
||||||
YMap,
|
YMap,
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
writeID,
|
writeID,
|
||||||
readID,
|
readID,
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
isDeleted,
|
isDeleted,
|
||||||
createDeleteSetFromStructStore,
|
createDeleteSetFromStructStore,
|
||||||
@ -15,7 +14,10 @@ import {
|
|||||||
findIndexSS,
|
findIndexSS,
|
||||||
UpdateEncoderV2,
|
UpdateEncoderV2,
|
||||||
applyUpdateV2,
|
applyUpdateV2,
|
||||||
DSEncoderV1, DSEncoderV2, DSDecoderV1, DSDecoderV2, Transaction, Doc, DeleteSet, Item // eslint-disable-line
|
LazyStructReader,
|
||||||
|
equalDeleteSets,
|
||||||
|
UpdateDecoderV1, UpdateDecoderV2, DSEncoderV1, DSEncoderV2, DSDecoderV1, DSDecoderV2, Transaction, Doc, DeleteSet, Item, // eslint-disable-line
|
||||||
|
mergeDeleteSets
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as map from 'lib0/map'
|
import * as map from 'lib0/map'
|
||||||
@ -147,7 +149,7 @@ export const splitSnapshotAffectedStructs = (transaction, snapshot) => {
|
|||||||
getItemCleanStart(transaction, createID(client, clock))
|
getItemCleanStart(transaction, createID(client, clock))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
iterateDeletedStructs(transaction, snapshot.ds, item => {})
|
iterateDeletedStructs(transaction, snapshot.ds, _item => {})
|
||||||
meta.add(snapshot)
|
meta.add(snapshot)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -207,3 +209,28 @@ export const createDocFromSnapshot = (originDoc, snapshot, newDoc = new Doc()) =
|
|||||||
applyUpdateV2(newDoc, encoder.toUint8Array(), 'snapshot')
|
applyUpdateV2(newDoc, encoder.toUint8Array(), 'snapshot')
|
||||||
return newDoc
|
return newDoc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Snapshot} snapshot
|
||||||
|
* @param {Uint8Array} update
|
||||||
|
* @param {typeof UpdateDecoderV2 | typeof UpdateDecoderV1} [YDecoder]
|
||||||
|
*/
|
||||||
|
export const snapshotContainsUpdateV2 = (snapshot, update, YDecoder = UpdateDecoderV2) => {
|
||||||
|
const structs = []
|
||||||
|
const updateDecoder = new YDecoder(decoding.createDecoder(update))
|
||||||
|
const lazyDecoder = new LazyStructReader(updateDecoder, false)
|
||||||
|
for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) {
|
||||||
|
structs.push(curr)
|
||||||
|
if ((snapshot.sv.get(curr.id.client) || 0) < curr.id.clock + curr.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const mergedDS = mergeDeleteSets([snapshot.ds, readDeleteSet(updateDecoder)])
|
||||||
|
return equalDeleteSets(snapshot.ds, mergedDS)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Snapshot} snapshot
|
||||||
|
* @param {Uint8Array} update
|
||||||
|
*/
|
||||||
|
export const snapshotContainsUpdate = (snapshot, update) => snapshotContainsUpdateV2(snapshot, update, UpdateDecoderV1)
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
GC,
|
GC,
|
||||||
splitItem,
|
splitItem,
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
getState,
|
getState,
|
||||||
writeStructsFromTransaction,
|
writeStructsFromTransaction,
|
||||||
@ -11,6 +10,7 @@ import {
|
|||||||
Item,
|
Item,
|
||||||
generateNewClientId,
|
generateNewClientId,
|
||||||
createID,
|
createID,
|
||||||
|
cleanupYTextAfterTransaction,
|
||||||
UpdateEncoderV1, UpdateEncoderV2, GC, StructStore, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
|
UpdateEncoderV1, UpdateEncoderV2, GC, StructStore, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
@ -114,6 +114,10 @@ export class Transaction {
|
|||||||
* @type {Set<Doc>}
|
* @type {Set<Doc>}
|
||||||
*/
|
*/
|
||||||
this.subdocsLoaded = new Set()
|
this.subdocsLoaded = new Set()
|
||||||
|
/**
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
this._needFormattingCleanup = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,18 +165,29 @@ export const addChangedTypeToTransaction = (transaction, type, parentSub) => {
|
|||||||
/**
|
/**
|
||||||
* @param {Array<AbstractStruct>} structs
|
* @param {Array<AbstractStruct>} structs
|
||||||
* @param {number} pos
|
* @param {number} pos
|
||||||
|
* @return {number} # of merged structs
|
||||||
*/
|
*/
|
||||||
const tryToMergeWithLeft = (structs, pos) => {
|
const tryToMergeWithLefts = (structs, pos) => {
|
||||||
const left = structs[pos - 1]
|
let right = structs[pos]
|
||||||
const right = structs[pos]
|
let left = structs[pos - 1]
|
||||||
if (left.deleted === right.deleted && left.constructor === right.constructor) {
|
let i = pos
|
||||||
if (left.mergeWith(right)) {
|
for (; i > 0; right = left, left = structs[--i - 1]) {
|
||||||
structs.splice(pos, 1)
|
if (left.deleted === right.deleted && left.constructor === right.constructor) {
|
||||||
if (right instanceof Item && right.parentSub !== null && /** @type {AbstractType<any>} */ (right.parent)._map.get(right.parentSub) === right) {
|
if (left.mergeWith(right)) {
|
||||||
/** @type {AbstractType<any>} */ (right.parent)._map.set(right.parentSub, /** @type {Item} */ (left))
|
if (right instanceof Item && right.parentSub !== null && /** @type {AbstractType<any>} */ (right.parent)._map.get(right.parentSub) === right) {
|
||||||
|
/** @type {AbstractType<any>} */ (right.parent)._map.set(right.parentSub, /** @type {Item} */ (left))
|
||||||
|
}
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
const merged = pos - i
|
||||||
|
if (merged) {
|
||||||
|
// remove all merged structs from the array
|
||||||
|
structs.splice(pos + 1 - merged, merged)
|
||||||
|
}
|
||||||
|
return merged
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -219,9 +234,9 @@ const tryMergeDeleteSet = (ds, store) => {
|
|||||||
for (
|
for (
|
||||||
let si = mostRightIndexToCheck, struct = structs[si];
|
let si = mostRightIndexToCheck, struct = structs[si];
|
||||||
si > 0 && struct.id.clock >= deleteItem.clock;
|
si > 0 && struct.id.clock >= deleteItem.clock;
|
||||||
struct = structs[--si]
|
struct = structs[si]
|
||||||
) {
|
) {
|
||||||
tryToMergeWithLeft(structs, si)
|
si -= 1 + tryToMergeWithLefts(structs, si)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -270,31 +285,34 @@ const cleanupTransactions = (transactionCleanups, i) => {
|
|||||||
)
|
)
|
||||||
fs.push(() => {
|
fs.push(() => {
|
||||||
// deep observe events
|
// deep observe events
|
||||||
transaction.changedParentTypes.forEach((events, type) =>
|
transaction.changedParentTypes.forEach((events, type) => {
|
||||||
fs.push(() => {
|
// We need to think about the possibility that the user transforms the
|
||||||
// We need to think about the possibility that the user transforms the
|
// Y.Doc in the event.
|
||||||
// Y.Doc in the event.
|
if (type._dEH.l.length > 0 && (type._item === null || !type._item.deleted)) {
|
||||||
if (type._item === null || !type._item.deleted) {
|
events = events
|
||||||
events = events
|
.filter(event =>
|
||||||
.filter(event =>
|
event.target._item === null || !event.target._item.deleted
|
||||||
event.target._item === null || !event.target._item.deleted
|
)
|
||||||
)
|
events
|
||||||
events
|
.forEach(event => {
|
||||||
.forEach(event => {
|
event.currentTarget = type
|
||||||
event.currentTarget = type
|
// path is relative to the current target
|
||||||
})
|
event._path = null
|
||||||
// sort events by path length so that top-level events are fired first.
|
})
|
||||||
events
|
// sort events by path length so that top-level events are fired first.
|
||||||
.sort((event1, event2) => event1.path.length - event2.path.length)
|
events
|
||||||
// We don't need to check for events.length
|
.sort((event1, event2) => event1.path.length - event2.path.length)
|
||||||
// because we know it has at least one element
|
// We don't need to check for events.length
|
||||||
callEventHandlerListeners(type._dEH, events, transaction)
|
// because we know it has at least one element
|
||||||
}
|
callEventHandlerListeners(type._dEH, events, transaction)
|
||||||
})
|
}
|
||||||
)
|
})
|
||||||
fs.push(() => doc.emit('afterTransaction', [transaction, doc]))
|
|
||||||
})
|
})
|
||||||
|
fs.push(() => doc.emit('afterTransaction', [transaction, doc]))
|
||||||
callAll(fs, [])
|
callAll(fs, [])
|
||||||
|
if (transaction._needFormattingCleanup) {
|
||||||
|
cleanupYTextAfterTransaction(transaction)
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// 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.
|
||||||
@ -310,23 +328,25 @@ const cleanupTransactions = (transactionCleanups, i) => {
|
|||||||
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
|
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
|
||||||
// we iterate from right to left so we can safely remove entries
|
// we iterate from right to left so we can safely remove entries
|
||||||
const firstChangePos = math.max(findIndexSS(structs, beforeClock), 1)
|
const firstChangePos = math.max(findIndexSS(structs, beforeClock), 1)
|
||||||
for (let i = structs.length - 1; i >= firstChangePos; i--) {
|
for (let i = structs.length - 1; i >= firstChangePos;) {
|
||||||
tryToMergeWithLeft(structs, i)
|
i -= 1 + tryToMergeWithLefts(structs, i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// try to merge mergeStructs
|
// try to merge mergeStructs
|
||||||
// @todo: it makes more sense to transform mergeStructs to a DS, sort it, and merge from right to left
|
// @todo: it makes more sense to transform mergeStructs to a DS, sort it, and merge from right to left
|
||||||
// but at the moment DS does not handle duplicates
|
// but at the moment DS does not handle duplicates
|
||||||
for (let i = 0; i < mergeStructs.length; i++) {
|
for (let i = mergeStructs.length - 1; i >= 0; i--) {
|
||||||
const { client, clock } = mergeStructs[i].id
|
const { client, clock } = mergeStructs[i].id
|
||||||
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
|
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
|
||||||
const replacedStructPos = findIndexSS(structs, clock)
|
const replacedStructPos = findIndexSS(structs, clock)
|
||||||
if (replacedStructPos + 1 < structs.length) {
|
if (replacedStructPos + 1 < structs.length) {
|
||||||
tryToMergeWithLeft(structs, replacedStructPos + 1)
|
if (tryToMergeWithLefts(structs, replacedStructPos + 1) > 1) {
|
||||||
|
continue // no need to perform next check, both are already merged
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (replacedStructPos > 0) {
|
if (replacedStructPos > 0) {
|
||||||
tryToMergeWithLeft(structs, replacedStructPos)
|
tryToMergeWithLefts(structs, replacedStructPos)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!transaction.local && transaction.afterState.get(doc.clientID) !== transaction.beforeState.get(doc.clientID)) {
|
if (!transaction.local && transaction.afterState.get(doc.clientID) !== transaction.beforeState.get(doc.clientID)) {
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
|
|
||||||
import * as time from 'lib0/time'
|
import * as time from 'lib0/time'
|
||||||
import * as array from 'lib0/array'
|
import * as array from 'lib0/array'
|
||||||
|
import * as logging from 'lib0/logging'
|
||||||
import { Observable } from 'lib0/observable'
|
import { Observable } from 'lib0/observable'
|
||||||
|
|
||||||
export class StackItem {
|
export class StackItem {
|
||||||
@ -169,6 +170,7 @@ export class UndoManager extends Observable {
|
|||||||
* @type {Array<AbstractType<any>>}
|
* @type {Array<AbstractType<any>>}
|
||||||
*/
|
*/
|
||||||
this.scope = []
|
this.scope = []
|
||||||
|
this.doc = doc
|
||||||
this.addToScope(typeScope)
|
this.addToScope(typeScope)
|
||||||
this.deleteFilter = deleteFilter
|
this.deleteFilter = deleteFilter
|
||||||
trackedOrigins.add(this)
|
trackedOrigins.add(this)
|
||||||
@ -189,7 +191,6 @@ export class UndoManager extends Observable {
|
|||||||
*/
|
*/
|
||||||
this.undoing = false
|
this.undoing = false
|
||||||
this.redoing = false
|
this.redoing = false
|
||||||
this.doc = doc
|
|
||||||
this.lastChange = 0
|
this.lastChange = 0
|
||||||
this.ignoreRemoteMapChanges = ignoreRemoteMapChanges
|
this.ignoreRemoteMapChanges = ignoreRemoteMapChanges
|
||||||
this.captureTimeout = captureTimeout
|
this.captureTimeout = captureTimeout
|
||||||
@ -263,6 +264,7 @@ export class UndoManager extends Observable {
|
|||||||
ytypes = array.isArray(ytypes) ? ytypes : [ytypes]
|
ytypes = array.isArray(ytypes) ? ytypes : [ytypes]
|
||||||
ytypes.forEach(ytype => {
|
ytypes.forEach(ytype => {
|
||||||
if (this.scope.every(yt => yt !== ytype)) {
|
if (this.scope.every(yt => yt !== ytype)) {
|
||||||
|
if (ytype.doc !== this.doc) logging.warn('[yjs#509] Not same Y.Doc') // use MultiDocUndoManager instead. also see https://github.com/yjs/yjs/issues/509
|
||||||
this.scope.push(ytype)
|
this.scope.push(ytype)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import * as error from 'lib0/error'
|
import * as error from 'lib0/error'
|
||||||
import * as encoding from 'lib0/encoding'
|
import * as encoding from 'lib0/encoding'
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
isDeleted,
|
isDeleted,
|
||||||
Item, AbstractType, Transaction, AbstractStruct // eslint-disable-line
|
Item, AbstractType, Transaction, AbstractStruct // eslint-disable-line
|
||||||
@ -6,6 +5,9 @@ import {
|
|||||||
|
|
||||||
import * as set from 'lib0/set'
|
import * as set from 'lib0/set'
|
||||||
import * as array from 'lib0/array'
|
import * as array from 'lib0/array'
|
||||||
|
import * as error from 'lib0/error'
|
||||||
|
|
||||||
|
const errorComputeChanges = 'You must not compute changes after the event-handler fired.'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @template {AbstractType<any>} T
|
* @template {AbstractType<any>} T
|
||||||
@ -44,6 +46,10 @@ export class YEvent {
|
|||||||
* @type {null | Array<{ insert?: string | Array<any> | object | AbstractType<any>, retain?: number, delete?: number, attributes?: Object<string, any> }>}
|
* @type {null | Array<{ insert?: string | Array<any> | object | AbstractType<any>, retain?: number, delete?: number, attributes?: Object<string, any> }>}
|
||||||
*/
|
*/
|
||||||
this._delta = null
|
this._delta = null
|
||||||
|
/**
|
||||||
|
* @type {Array<string|number>|null}
|
||||||
|
*/
|
||||||
|
this._path = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -60,8 +66,7 @@ export class YEvent {
|
|||||||
* type === event.target // => true
|
* type === event.target // => true
|
||||||
*/
|
*/
|
||||||
get path () {
|
get path () {
|
||||||
// @ts-ignore _item is defined because target is integrated
|
return this._path || (this._path = getPathTo(this.currentTarget, this.target))
|
||||||
return getPathTo(this.currentTarget, this.target)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -81,6 +86,9 @@ export class YEvent {
|
|||||||
*/
|
*/
|
||||||
get keys () {
|
get keys () {
|
||||||
if (this._keys === null) {
|
if (this._keys === null) {
|
||||||
|
if (this.transaction.doc._transactionCleanups.length === 0) {
|
||||||
|
throw error.create(errorComputeChanges)
|
||||||
|
}
|
||||||
const keys = new Map()
|
const keys = new Map()
|
||||||
const target = this.target
|
const target = this.target
|
||||||
const changed = /** @type Set<string|null> */ (this.transaction.changed.get(target))
|
const changed = /** @type Set<string|null> */ (this.transaction.changed.get(target))
|
||||||
@ -164,6 +172,9 @@ export class YEvent {
|
|||||||
get changes () {
|
get changes () {
|
||||||
let changes = this._changes
|
let changes = this._changes
|
||||||
if (changes === null) {
|
if (changes === null) {
|
||||||
|
if (this.transaction.doc._transactionCleanups.length === 0) {
|
||||||
|
throw error.create(errorComputeChanges)
|
||||||
|
}
|
||||||
const target = this.target
|
const target = this.target
|
||||||
const added = set.create()
|
const added = set.create()
|
||||||
const deleted = set.create()
|
const deleted = set.create()
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @module encoding
|
* @module encoding
|
||||||
*/
|
*/
|
||||||
@ -88,7 +87,7 @@ export const writeClientsStructs = (encoder, store, _sm) => {
|
|||||||
sm.set(client, clock)
|
sm.set(client, clock)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
getStateVector(store).forEach((clock, client) => {
|
getStateVector(store).forEach((_clock, client) => {
|
||||||
if (!_sm.has(client)) {
|
if (!_sm.has(client)) {
|
||||||
sm.set(client, 0)
|
sm.set(client, 0)
|
||||||
}
|
}
|
||||||
@ -98,8 +97,7 @@ export const writeClientsStructs = (encoder, store, _sm) => {
|
|||||||
// Write items with higher client ids first
|
// Write items with higher client ids first
|
||||||
// This heavily improves the conflict algorithm.
|
// This heavily improves the conflict algorithm.
|
||||||
array.from(sm.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => {
|
array.from(sm.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => {
|
||||||
// @ts-ignore
|
writeStructs(encoder, /** @type {Array<GC|Item>} */ (store.clients.get(client)), client, clock)
|
||||||
writeStructs(encoder, store.clients.get(client), client, clock)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,7 +250,7 @@ const integrateStructs = (transaction, store, clientsStructRefs) => {
|
|||||||
return nextStructsTarget
|
return nextStructsTarget
|
||||||
}
|
}
|
||||||
let curStructsTarget = getNextStructTarget()
|
let curStructsTarget = getNextStructTarget()
|
||||||
if (curStructsTarget === null && stack.length === 0) {
|
if (curStructsTarget === null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { AbstractType, Item } from '../internals.js' // eslint-disable-line
|
import { AbstractType, Item } from '../internals.js' // eslint-disable-line
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
AbstractType // eslint-disable-line
|
AbstractType // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import * as binary from 'lib0/binary'
|
import * as binary from 'lib0/binary'
|
||||||
import * as decoding from 'lib0/decoding'
|
import * as decoding from 'lib0/decoding'
|
||||||
import * as encoding from 'lib0/encoding'
|
import * as encoding from 'lib0/encoding'
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Testing if encoding/decoding compatibility and integration compatiblity is given.
|
* Testing if encoding/decoding compatibility and integration compatiblity is given.
|
||||||
* We expect that the document always looks the same, even if we upgrade the integration algorithm, or add additional encoding approaches.
|
* We expect that the document always looks the same, even if we upgrade the integration algorithm, or add additional encoding approaches.
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import * as Y from '../src/index.js'
|
import * as Y from '../src/index.js'
|
||||||
import * as t from 'lib0/testing'
|
import * as t from 'lib0/testing'
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import * as Y from '../src/index.js'
|
import * as Y from '../src/index.js'
|
||||||
import * as t from 'lib0/testing'
|
import * as t from 'lib0/testing'
|
||||||
|
|
||||||
|
@ -3,9 +3,9 @@ import * as t from 'lib0/testing'
|
|||||||
import { init } from './testHelper.js'
|
import { init } from './testHelper.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testBasic = tc => {
|
export const testBasic = _tc => {
|
||||||
const ydoc = new Y.Doc({ gc: false })
|
const ydoc = new Y.Doc({ gc: false })
|
||||||
ydoc.getText().insert(0, 'world!')
|
ydoc.getText().insert(0, 'world!')
|
||||||
const snapshot = Y.snapshot(ydoc)
|
const snapshot = Y.snapshot(ydoc)
|
||||||
@ -15,9 +15,24 @@ export const testBasic = tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testBasicRestoreSnapshot = tc => {
|
export const testBasicXmlAttributes = _tc => {
|
||||||
|
const ydoc = new Y.Doc({ gc: false })
|
||||||
|
const yxml = ydoc.getMap().set('el', new Y.XmlElement('div'))
|
||||||
|
const snapshot1 = Y.snapshot(ydoc)
|
||||||
|
yxml.setAttribute('a', '1')
|
||||||
|
const snapshot2 = Y.snapshot(ydoc)
|
||||||
|
yxml.setAttribute('a', '2')
|
||||||
|
t.compare(yxml.getAttributes(), { a: '2' })
|
||||||
|
t.compare(yxml.getAttributes(snapshot2), { a: '1' })
|
||||||
|
t.compare(yxml.getAttributes(snapshot1), {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} _tc
|
||||||
|
*/
|
||||||
|
export const testBasicRestoreSnapshot = _tc => {
|
||||||
const doc = new Y.Doc({ gc: false })
|
const doc = new Y.Doc({ gc: false })
|
||||||
doc.getArray('array').insert(0, ['hello'])
|
doc.getArray('array').insert(0, ['hello'])
|
||||||
const snap = Y.snapshot(doc)
|
const snap = Y.snapshot(doc)
|
||||||
@ -30,9 +45,9 @@ export const testBasicRestoreSnapshot = tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testEmptyRestoreSnapshot = tc => {
|
export const testEmptyRestoreSnapshot = _tc => {
|
||||||
const doc = new Y.Doc({ gc: false })
|
const doc = new Y.Doc({ gc: false })
|
||||||
const snap = Y.snapshot(doc)
|
const snap = Y.snapshot(doc)
|
||||||
snap.sv.set(9999, 0)
|
snap.sv.set(9999, 0)
|
||||||
@ -50,9 +65,9 @@ export const testEmptyRestoreSnapshot = tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testRestoreSnapshotWithSubType = tc => {
|
export const testRestoreSnapshotWithSubType = _tc => {
|
||||||
const doc = new Y.Doc({ gc: false })
|
const doc = new Y.Doc({ gc: false })
|
||||||
doc.getArray('array').insert(0, [new Y.Map()])
|
doc.getArray('array').insert(0, [new Y.Map()])
|
||||||
const subMap = doc.getArray('array').get(0)
|
const subMap = doc.getArray('array').get(0)
|
||||||
@ -73,9 +88,9 @@ export const testRestoreSnapshotWithSubType = tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testRestoreDeletedItem1 = tc => {
|
export const testRestoreDeletedItem1 = _tc => {
|
||||||
const doc = new Y.Doc({ gc: false })
|
const doc = new Y.Doc({ gc: false })
|
||||||
doc.getArray('array').insert(0, ['item1', 'item2'])
|
doc.getArray('array').insert(0, ['item1', 'item2'])
|
||||||
|
|
||||||
@ -89,9 +104,9 @@ export const testRestoreDeletedItem1 = tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testRestoreLeftItem = tc => {
|
export const testRestoreLeftItem = _tc => {
|
||||||
const doc = new Y.Doc({ gc: false })
|
const doc = new Y.Doc({ gc: false })
|
||||||
doc.getArray('array').insert(0, ['item1'])
|
doc.getArray('array').insert(0, ['item1'])
|
||||||
doc.getMap('map').set('test', 1)
|
doc.getMap('map').set('test', 1)
|
||||||
@ -107,9 +122,9 @@ export const testRestoreLeftItem = tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testDeletedItemsBase = tc => {
|
export const testDeletedItemsBase = _tc => {
|
||||||
const doc = new Y.Doc({ gc: false })
|
const doc = new Y.Doc({ gc: false })
|
||||||
doc.getArray('array').insert(0, ['item1'])
|
doc.getArray('array').insert(0, ['item1'])
|
||||||
doc.getArray('array').delete(0)
|
doc.getArray('array').delete(0)
|
||||||
@ -123,9 +138,9 @@ export const testDeletedItemsBase = tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testDeletedItems2 = tc => {
|
export const testDeletedItems2 = _tc => {
|
||||||
const doc = new Y.Doc({ gc: false })
|
const doc = new Y.Doc({ gc: false })
|
||||||
doc.getArray('array').insert(0, ['item1', 'item2', 'item3'])
|
doc.getArray('array').insert(0, ['item1', 'item2', 'item3'])
|
||||||
doc.getArray('array').delete(1)
|
doc.getArray('array').delete(1)
|
||||||
@ -181,3 +196,28 @@ export const testDependentChanges = tc => {
|
|||||||
const docRestored1 = Y.createDocFromSnapshot(array1.doc, snap)
|
const docRestored1 = Y.createDocFromSnapshot(array1.doc, snap)
|
||||||
t.compare(docRestored1.getArray('array').toArray(), ['user1item1', 'user2item1'])
|
t.compare(docRestored1.getArray('array').toArray(), ['user1item1', 'user2item1'])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} _tc
|
||||||
|
*/
|
||||||
|
export const testContainsUpdate = _tc => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
/**
|
||||||
|
* @type {Array<Uint8Array>}
|
||||||
|
*/
|
||||||
|
const updates = []
|
||||||
|
ydoc.on('update', update => {
|
||||||
|
updates.push(update)
|
||||||
|
})
|
||||||
|
const yarr = ydoc.getArray()
|
||||||
|
const snapshot1 = Y.snapshot(ydoc)
|
||||||
|
yarr.insert(0, [1])
|
||||||
|
const snapshot2 = Y.snapshot(ydoc)
|
||||||
|
yarr.delete(0, 1)
|
||||||
|
const snapshotFinal = Y.snapshot(ydoc)
|
||||||
|
t.assert(!Y.snapshotContainsUpdate(snapshot1, updates[0]))
|
||||||
|
t.assert(!Y.snapshotContainsUpdate(snapshot2, updates[1]))
|
||||||
|
t.assert(Y.snapshotContainsUpdate(snapshot2, updates[0]))
|
||||||
|
t.assert(Y.snapshotContainsUpdate(snapshotFinal, updates[0]))
|
||||||
|
t.assert(Y.snapshotContainsUpdate(snapshotFinal, updates[1]))
|
||||||
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import * as t from 'lib0/testing'
|
import * as t from 'lib0/testing'
|
||||||
import * as prng from 'lib0/prng'
|
import * as prng from 'lib0/prng'
|
||||||
import * as encoding from 'lib0/encoding'
|
import * as encoding from 'lib0/encoding'
|
||||||
@ -356,8 +355,9 @@ export const compare = users => {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
t.compare(Y.encodeStateVector(users[i]), Y.encodeStateVector(users[i + 1]))
|
t.compare(Y.encodeStateVector(users[i]), Y.encodeStateVector(users[i + 1]))
|
||||||
compareDS(Y.createDeleteSetFromStructStore(users[i].store), Y.createDeleteSetFromStructStore(users[i + 1].store))
|
Y.equalDeleteSets(Y.createDeleteSetFromStructStore(users[i].store), Y.createDeleteSetFromStructStore(users[i + 1].store))
|
||||||
compareStructStores(users[i].store, users[i + 1].store)
|
compareStructStores(users[i].store, users[i + 1].store)
|
||||||
|
t.compare(Y.encodeSnapshot(Y.snapshot(users[i])), Y.encodeSnapshot(Y.snapshot(users[i + 1])))
|
||||||
}
|
}
|
||||||
users.map(u => u.destroy())
|
users.map(u => u.destroy())
|
||||||
}
|
}
|
||||||
@ -412,25 +412,6 @@ export const compareStructStores = (ss1, ss2) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('../src/internals.js').DeleteSet} ds1
|
|
||||||
* @param {import('../src/internals.js').DeleteSet} ds2
|
|
||||||
*/
|
|
||||||
export const compareDS = (ds1, ds2) => {
|
|
||||||
t.assert(ds1.clients.size === ds2.clients.size)
|
|
||||||
ds1.clients.forEach((deleteItems1, client) => {
|
|
||||||
const deleteItems2 = /** @type {Array<import('../src/internals.js').DeleteItem>} */ (ds2.clients.get(client))
|
|
||||||
t.assert(deleteItems2 !== undefined && deleteItems1.length === deleteItems2.length)
|
|
||||||
for (let i = 0; i < deleteItems1.length; i++) {
|
|
||||||
const di1 = deleteItems1[i]
|
|
||||||
const di2 = deleteItems2[i]
|
|
||||||
if (di1.clock !== di2.clock || di1.len !== di2.len) {
|
|
||||||
t.fail('DeleteSets dont match')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @template T
|
* @template T
|
||||||
* @callback InitTestObjectCallback
|
* @callback InitTestObjectCallback
|
||||||
|
@ -3,6 +3,46 @@ import { init } from './testHelper.js' // eslint-disable-line
|
|||||||
import * as Y from '../src/index.js'
|
import * as Y from '../src/index.js'
|
||||||
import * as t from 'lib0/testing'
|
import * as t from 'lib0/testing'
|
||||||
|
|
||||||
|
export const testInconsistentFormat = () => {
|
||||||
|
/**
|
||||||
|
* @param {Y.Doc} ydoc
|
||||||
|
*/
|
||||||
|
const testYjsMerge = ydoc => {
|
||||||
|
const content = /** @type {Y.XmlText} */ (ydoc.get('text', Y.XmlText))
|
||||||
|
content.format(0, 6, { bold: null })
|
||||||
|
content.format(6, 4, { type: 'text' })
|
||||||
|
t.compare(content.toDelta(), [
|
||||||
|
{
|
||||||
|
attributes: { type: 'text' },
|
||||||
|
insert: 'Merge Test'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributes: { type: 'text', italic: true },
|
||||||
|
insert: ' After'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
const initializeYDoc = () => {
|
||||||
|
const yDoc = new Y.Doc({ gc: false })
|
||||||
|
|
||||||
|
const content = /** @type {Y.XmlText} */ (yDoc.get('text', Y.XmlText))
|
||||||
|
content.insert(0, ' After', { type: 'text', italic: true })
|
||||||
|
content.insert(0, 'Test', { type: 'text' })
|
||||||
|
content.insert(0, 'Merge ', { type: 'text', bold: true })
|
||||||
|
return yDoc
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const yDoc = initializeYDoc()
|
||||||
|
testYjsMerge(yDoc)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const initialYDoc = initializeYDoc()
|
||||||
|
const yDoc = new Y.Doc({ gc: false })
|
||||||
|
Y.applyUpdate(yDoc, Y.encodeStateAsUpdate(initialYDoc))
|
||||||
|
testYjsMerge(yDoc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
|
@ -8,6 +8,54 @@ import * as Y from '../src/index.js'
|
|||||||
import * as t from 'lib0/testing'
|
import * as t from 'lib0/testing'
|
||||||
import * as prng from 'lib0/prng'
|
import * as prng from 'lib0/prng'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} _tc
|
||||||
|
*/
|
||||||
|
export const testIterators = _tc => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
/**
|
||||||
|
* @type {Y.Map<number>}
|
||||||
|
*/
|
||||||
|
const ymap = ydoc.getMap()
|
||||||
|
// we are only checking if the type assumptions are correct
|
||||||
|
/**
|
||||||
|
* @type {Array<number>}
|
||||||
|
*/
|
||||||
|
const vals = Array.from(ymap.values())
|
||||||
|
/**
|
||||||
|
* @type {Array<[string,number]>}
|
||||||
|
*/
|
||||||
|
const entries = Array.from(ymap.entries())
|
||||||
|
/**
|
||||||
|
* @type {Array<string>}
|
||||||
|
*/
|
||||||
|
const keys = Array.from(ymap.keys())
|
||||||
|
console.log(vals, entries, keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computing event changes after transaction should result in an error. See yjs#539
|
||||||
|
*
|
||||||
|
* @param {t.TestCase} _tc
|
||||||
|
*/
|
||||||
|
export const testMapEventError = _tc => {
|
||||||
|
const doc = new Y.Doc()
|
||||||
|
const ymap = doc.getMap()
|
||||||
|
/**
|
||||||
|
* @type {any}
|
||||||
|
*/
|
||||||
|
let event = null
|
||||||
|
ymap.observe((e) => {
|
||||||
|
event = e
|
||||||
|
})
|
||||||
|
t.fails(() => {
|
||||||
|
t.info(event.keys)
|
||||||
|
})
|
||||||
|
t.fails(() => {
|
||||||
|
t.info(event.keys)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
@ -339,6 +387,34 @@ export const testObserversUsingObservedeep = tc => {
|
|||||||
compare(users)
|
compare(users)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testPathsOfSiblingEvents = tc => {
|
||||||
|
const { users, map0 } = init(tc, { users: 2 })
|
||||||
|
/**
|
||||||
|
* @type {Array<Array<string|number>>}
|
||||||
|
*/
|
||||||
|
const pathes = []
|
||||||
|
let calls = 0
|
||||||
|
const doc = users[0]
|
||||||
|
map0.set('map', new Y.Map())
|
||||||
|
map0.get('map').set('text1', new Y.Text('initial'))
|
||||||
|
map0.observeDeep(events => {
|
||||||
|
events.forEach(event => {
|
||||||
|
pathes.push(event.path)
|
||||||
|
})
|
||||||
|
calls++
|
||||||
|
})
|
||||||
|
doc.transact(() => {
|
||||||
|
map0.get('map').get('text1').insert(0, 'post-')
|
||||||
|
map0.get('map').set('text2', new Y.Text('new'))
|
||||||
|
})
|
||||||
|
t.assert(calls === 1)
|
||||||
|
t.compare(pathes, [['map'], ['map', 'text1']])
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Test events in Y.Map
|
// TODO: Test events in Y.Map
|
||||||
/**
|
/**
|
||||||
* @param {Object<string,any>} is
|
* @param {Object<string,any>} is
|
||||||
|
@ -189,7 +189,6 @@ export const testClone = _tc => {
|
|||||||
const third = new Y.XmlElement('p')
|
const third = new Y.XmlElement('p')
|
||||||
yxml.push([first, second, third])
|
yxml.push([first, second, third])
|
||||||
t.compareArrays(yxml.toArray(), [first, second, third])
|
t.compareArrays(yxml.toArray(), [first, second, third])
|
||||||
|
|
||||||
const cloneYxml = yxml.clone()
|
const cloneYxml = yxml.clone()
|
||||||
ydoc.getArray('copyarr').insert(0, [cloneYxml])
|
ydoc.getArray('copyarr').insert(0, [cloneYxml])
|
||||||
t.assert(cloneYxml.length === 3)
|
t.assert(cloneYxml.length === 3)
|
||||||
@ -210,3 +209,15 @@ export const testFormattingBug = _tc => {
|
|||||||
yxml.applyDelta(delta)
|
yxml.applyDelta(delta)
|
||||||
t.compare(yxml.toDelta(), delta)
|
t.compare(yxml.toDelta(), delta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} _tc
|
||||||
|
*/
|
||||||
|
export const testElement = _tc => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
const yxmlel = ydoc.getXmlElement()
|
||||||
|
const text1 = new Y.XmlText('text1')
|
||||||
|
const text2 = new Y.XmlText('text2')
|
||||||
|
yxmlel.insert(0, [text1, text2])
|
||||||
|
t.compareArrays(yxmlel.toArray(), [text1, text2])
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user