Merge remote-tracking branch 'origin/main' into improve-types-for-map-public-interface

This commit is contained in:
Mel Bourgeois 2024-02-24 12:11:44 -06:00
commit cd79765f3c
No known key found for this signature in database
GPG Key ID: 290FCF081AEDB3EC
48 changed files with 1346 additions and 627 deletions

View File

@ -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

View File

@ -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

View File

@ -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
View File

@ -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. [![Become a Sponsor](https://img.shields.io/static/v1?label=Become%20a%20Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=d42f2d)](https://github.com/sponsors/dmonad) on Yjs. [![Become a Sponsor](https://img.shields.io/static/v1?label=Become%20a%20Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=d42f2d)](https://github.com/sponsors/dmonad)
## 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. [![Become a Sponsor](https://img.shields.io/static/v1?label=Become%20a%2
Nimbus Web. :star: Nimbus Web. :star:
* [Pluxbox RadioManager](https://getradiomanager.com/) 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. [![Become a Sponsor](https://img.shields.io/static/v1?label=Become%20a%2
* [Slidebeamer](https://slidebeamer.com/) 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

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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

View File

@ -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'

View File

@ -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'

View File

@ -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
} }
/** /**

View File

@ -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)
} }

View File

@ -1,4 +1,3 @@
import { import {
AbstractStruct, AbstractStruct,
addStruct, addStruct,

View File

@ -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

View File

@ -1,4 +1,3 @@
import { import {
AbstractStruct, AbstractStruct,
UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, ID // eslint-disable-line UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, ID // eslint-disable-line

View File

@ -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>>}

View File

@ -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()

View File

@ -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
} }

View File

@ -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))
} }
/** /**

View File

@ -1,4 +1,3 @@
import { import {
YEvent, YEvent,
YXmlText, YXmlElement, YXmlFragment, Transaction // eslint-disable-line YXmlText, YXmlElement, YXmlFragment, Transaction // eslint-disable-line

View File

@ -1,4 +1,3 @@
import { import {
YMap, YMap,
YXmlHookRefID, YXmlHookRefID,

View File

@ -1,4 +1,3 @@
import { import {
YText, YText,
YXmlTextRefID, YXmlTextRefID,

View File

@ -1,4 +1,3 @@
import { Observable } from 'lib0/observable' import { Observable } from 'lib0/observable'
import { import {

View File

@ -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
}

View File

@ -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)
} }

View File

@ -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'

View File

@ -1,4 +1,3 @@
import { import {
YArray, YArray,
YMap, YMap,

View File

@ -1,4 +1,3 @@
import { import {
writeID, writeID,
readID, readID,

View File

@ -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)

View File

@ -1,4 +1,3 @@
import { import {
GC, GC,
splitItem, splitItem,

View File

@ -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)) {

View File

@ -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)
} }
}) })

View File

@ -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'

View File

@ -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()

View File

@ -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
} }

View File

@ -1,4 +1,3 @@
import { AbstractType, Item } from '../internals.js' // eslint-disable-line import { AbstractType, Item } from '../internals.js' // eslint-disable-line
/** /**

View File

@ -1,4 +1,3 @@
import { import {
AbstractType // eslint-disable-line AbstractType // eslint-disable-line
} from '../internals.js' } from '../internals.js'

View File

@ -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'

View File

@ -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.

View File

@ -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'

View File

@ -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'

View File

@ -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]))
}

View File

@ -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

View File

@ -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
*/ */

View File

@ -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

View File

@ -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])
}