Compare commits
No commits in common. "main" and "v13.5.39" have entirely different histories.
12
.github/workflows/node.js.yml
vendored
12
.github/workflows/node.js.yml
vendored
@ -16,16 +16,14 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [16.x, 20.x]
|
node-version: [10.x, 12.x, 14.x]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v1
|
||||||
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 build --if-present
|
||||||
- run: npm run test
|
- run: npm test
|
||||||
env:
|
|
||||||
CI: true
|
|
||||||
|
31
.github/workflows/nodejs.yml
vendored
Normal file
31
.github/workflows/nodejs.yml
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||||
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||||
|
|
||||||
|
name: Node.js CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [10.x, 12.x, 13.x]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run lint
|
||||||
|
- run: npm run test-extensive
|
||||||
|
env:
|
||||||
|
CI: true
|
12
INTERNALS.md
12
INTERNALS.md
@ -26,7 +26,7 @@ article](https://blog.kevinjahns.de/are-crdts-suitable-for-shared-editing/).
|
|||||||
|
|
||||||
Each client is assigned a unique *clientID* property on first insert. This is a
|
Each client is assigned a unique *clientID* property on first insert. This is a
|
||||||
random 53-bit integer (53 bits because that fits in the javascript safe integer
|
random 53-bit integer (53 bits because that fits in the javascript safe integer
|
||||||
range \[JavaScript uses IEEE 754 floats\]).
|
range).
|
||||||
|
|
||||||
## List items
|
## List items
|
||||||
|
|
||||||
@ -60,13 +60,13 @@ characters have either been deleted or all characters are not deleted. The item
|
|||||||
will be split if the run is interrupted for any reason (eg a character in the
|
will be split if the run is interrupted for any reason (eg a character in the
|
||||||
middle of the run is deleted).
|
middle of the run is deleted).
|
||||||
|
|
||||||
When an item is created, it stores a reference to the IDs of the preceding and
|
When an item is created, it stores a reference to the IDs of the preceeding and
|
||||||
succeeding item. These are stored in the item's `origin` and `originRight`
|
succeeding item. These are stored in the item's `origin` and `originRight`
|
||||||
fields, respectively. These are used when peers concurrently insert at the same
|
fields, respectively. These are used when peers concurrently insert at the same
|
||||||
location in a document. Though quite rare in practice, Yjs needs to make sure
|
location in a document. Though quite rare in practice, Yjs needs to make sure
|
||||||
the list items always resolve to the same order on all peers. The actual logic
|
the list items always resolve to the same order on all peers. The actual logic
|
||||||
is relatively simple - its only a couple dozen lines of code and it lives in
|
is relatively simple - its only a couple dozen lines of code and it lives in
|
||||||
the `Item#integrate()` method. The YATA paper has much more detail on this
|
the `Item#integrate()` method. The YATA paper has much more detail on the this
|
||||||
algorithm.
|
algorithm.
|
||||||
|
|
||||||
### Item Storage
|
### Item Storage
|
||||||
@ -88,7 +88,7 @@ When a local insert happens, Yjs needs to map the insert position in the
|
|||||||
document (eg position 1000) to an ID. With just the linked list, this would
|
document (eg position 1000) to an ID. With just the linked list, this would
|
||||||
require a slow O(n) linear scan of the list. But when editing a document, most
|
require a slow O(n) linear scan of the list. But when editing a document, most
|
||||||
inserts are either at the same position as the last insert, or nearby. To
|
inserts are either at the same position as the last insert, or nearby. To
|
||||||
improve performance, Yjs stores a cache of the 80 most recently looked up
|
improve performance, Yjs stores a cache of the 10 most recently looked up
|
||||||
insert positions in the document. This is consulted and updated when a position
|
insert positions in the document. This is consulted and updated when a position
|
||||||
is looked up to improve performance in the average case. The cache is updated
|
is looked up to improve performance in the average case. The cache is updated
|
||||||
using a heuristic that is still changing (currently, it is updated when a new
|
using a heuristic that is still changing (currently, it is updated when a new
|
||||||
@ -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 update that allows clients to sync with each other.
|
an incremental document updates that allows clients to sync with each other.
|
||||||
The update object is a Uint8Array that efficiently encodes `Item` objects and
|
The update object is an 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) 2023
|
Copyright (c) 2014
|
||||||
- Kevin Jahns <kevin.jahns@protonmail.com>.
|
- Kevin Jahns <kevin.jahns@rwth-aachen.de>.
|
||||||
- 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
|
||||||
|
323
README.md
323
README.md
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
> A CRDT framework with a powerful abstraction of shared data
|
> A CRDT framework with a powerful abstraction of shared data
|
||||||
|
|
||||||
Yjs is a [CRDT implementation](#yjs-crdt-algorithm) that exposes its internal
|
Yjs is a [CRDT implementation](#Yjs-CRDT-Algorithm) that exposes its internal
|
||||||
data structure as *shared types*. Shared types are common data types like `Map`
|
data structure as *shared types*. Shared types are common data types like `Map`
|
||||||
or `Array` with superpowers: changes are automatically distributed to other
|
or `Array` with superpowers: changes are automatically distributed to other
|
||||||
peers and merged without merge conflicts.
|
peers and merged without merge conflicts.
|
||||||
@ -32,34 +32,9 @@ 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
|
* [Dynaboard](https://dynaboard.com/) Build web apps collaboratively. :star2:
|
||||||
knowledge base. :star2:
|
|
||||||
* [Huly](https://huly.io/) - Open Source All-in-One Project Management Platform :star2:
|
|
||||||
* [Cargo](https://cargo.site/) Site builder for designers and artists :star2:
|
|
||||||
* [Gitbook](https://gitbook.com) Knowledge management for technical teams :star2:
|
|
||||||
* [Evernote](https://evernote.com) Note-taking app :star2:
|
|
||||||
* [Lessonspace](https://thelessonspace.com) Enterprise platform for virtual
|
|
||||||
classrooms and online training :star2:
|
|
||||||
* [Ellipsus](ellipsus.com) - Collaborative writing app for storytelling etc.
|
|
||||||
Supports versioning, change attribution, and "blame". A solution for the whole
|
|
||||||
publishing process (also selling) :star:
|
|
||||||
* [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
|
||||||
@ -68,78 +43,30 @@ Showcase](https://yjs-diagram.synergy.codes/).
|
|||||||
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:
|
||||||
* [modyfi](https://www.modyfi.com) - Modyfi is the design platform built for
|
|
||||||
multidisciplinary designers. Design, generate, animate, and more — without
|
|
||||||
switching between apps. :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.
|
* [PRSM](https://prsm.uk/) Collaborative mind-mapping and system visualisation. *[(source)](https://github.com/micrology/prsm)*
|
||||||
*[(source)](https://github.com/micrology/prsm)*
|
|
||||||
* [Alldone](https://alldone.app/) A next-gen project management and
|
* [Alldone](https://alldone.app/) A next-gen project management and
|
||||||
collaboration platform.
|
collaboration platform.
|
||||||
* [Living Spec](https://livingspec.com/) A modern way for product teams to collaborate.
|
* [Living Spec](https://livingspec.com/) A modern way for product teams to collaborate.
|
||||||
* [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
|
|
||||||
sharing analyses, documentation, spreadsheets, and dashboards.
|
|
||||||
* [Nosgestesclimat](https://nosgestesclimat.fr/groupe) The french carbon
|
|
||||||
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.
|
|
||||||
* [AWS SageMaker](https://aws.amazon.com/sagemaker/) - Machine Learning Service
|
|
||||||
* [Arkiter](https://www.arkiter.com/) - Live interview software
|
|
||||||
* [Appflowy](https://www.appflowy.io/) - They use Yrs
|
|
||||||
* [Multi.app](https://multi.app) - Multiplayer app sharing: Point, draw and edit
|
|
||||||
in shared apps as if they're on your computer. They are using Yrs.
|
|
||||||
* [AppMaster](https://appmaster.io) A No-Code platform for creating
|
|
||||||
production-ready applications with source code generation.
|
|
||||||
* [Synthesia](https://www.synthesia.io) - Collaborative Video Editor
|
|
||||||
* [thinkdeli](https://thinkdeli.com) - A fast and simple notes app powered by AI
|
|
||||||
* [ourboard](https://github.com/raimohanska/ourboard) - A collaborative whiteboard
|
|
||||||
application
|
|
||||||
* [Ellie.ai](https://ellie.ai) - Data Product Design and Collaboration
|
|
||||||
* [GoPeer](https://gopeer.org/) - Collaborative tutoring
|
|
||||||
* [screen.garden](https://screen.garden) - Collaborative backend for PKM apps.
|
|
||||||
* [NextCloud](https://nextcloud.com/) - Content Collaboration Platform
|
|
||||||
* [keystatic](https://github.com/Thinkmill/keystatic) - git-based CMS
|
|
||||||
* [QDAcity](https://qdacity.com) - Collaborative qualitative data analysis platform
|
|
||||||
* [Kanbert](https://kanbert.com) - Project management software
|
|
||||||
* [Eclipse Theia](https://github.com/eclipse-theia/theia) - A cloud & desktop
|
|
||||||
IDE that runs in the browser.
|
|
||||||
* [ScienHub](https://scienhub.com) - Collaborative LaTeX editor in the browser.
|
|
||||||
* [Open Collaboration Tools](https://www.open-collab.tools/) - Collaborative
|
|
||||||
editing for your IDE or custom editor
|
|
||||||
* [Typst](https://typst.app/) - Compose, edit, and automate technical documents
|
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
* [Overview](#overview)
|
* [Overview](#Overview)
|
||||||
* [Bindings](#bindings)
|
* [Bindings](#Bindings)
|
||||||
* [Providers](#providers)
|
* [Providers](#Providers)
|
||||||
* [Tooling](#tooling)
|
* [Getting Started](#Getting-Started)
|
||||||
* [Ports](#ports)
|
* [API](#API)
|
||||||
* [Getting Started](#getting-started)
|
* [Shared Types](#Shared-Types)
|
||||||
* [API](#api)
|
* [Y.Doc](#YDoc)
|
||||||
* [Shared Types](#shared-types)
|
* [Document Updates](#Document-Updates)
|
||||||
* [Y.Doc](#ydoc)
|
* [Relative Positions](#Relative-Positions)
|
||||||
* [Document Updates](#document-updates)
|
* [Y.UndoManager](#YUndoManager)
|
||||||
* [Relative Positions](#relative-positions)
|
* [Yjs CRDT Algorithm](#Yjs-CRDT-Algorithm)
|
||||||
* [Y.UndoManager](#yundomanager)
|
* [License and Author](#License-and-Author)
|
||||||
* [Yjs CRDT Algorithm](#yjs-crdt-algorithm)
|
|
||||||
* [License and Author](#license-and-author)
|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@ -156,15 +83,9 @@ 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) |
|
|
||||||
| [Lexical](https://lexical.dev/) | ✔ | (native) | [demo](https://lexical.dev/docs/collaboration/react#see-it-in-action) |
|
|
||||||
| [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 | | [react-yjs](https://github.com/nikgraf/react-yjs) | [demo](https://react-yjs-example.vercel.app/) |
|
|
||||||
| 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) |
|
|
||||||
| [PSPDFKit](https://www.nutrient.io/) | | [yjs-pspdfkit](https://github.com/hoangqwe159/yjs-pspdfkit) | [demo](https://github.com/hoangqwe159/yjs-pspdfkit) |
|
|
||||||
| [Rows n'Columns](https://www.rowsncolumns.app/) | ✔ | [@rowsncolumns/y-spreadsheet](https://docs.rowsncolumns.app/collaboration/yjs-collaboration) | |
|
|
||||||
|
|
||||||
### Providers
|
### Providers
|
||||||
|
|
||||||
@ -173,56 +94,26 @@ 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. <a href="https://github.com/yjs/y-redis/"><b>y-redis</b></a>,
|
|
||||||
<b>y-sweet</b>, <b>ypy-websocket</b> and <a href="https://tiptap.dev/docs/hocuspocus/introduction">
|
|
||||||
<b>Hocuspocus</b></a> (see below) are alternative
|
|
||||||
backends to y-websocket.
|
|
||||||
</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
|
||||||
signaling data over signaling servers. Publicly available signaling servers
|
signaling data over signaling servers. Publically available signaling servers
|
||||||
are available. Communication over the signaling servers can be encrypted by
|
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/liveblocks/liveblocks">@liveblocks/yjs </a> 🌟</dt>
|
<dt><a href="https://github.com/yjs/y-websocket">y-websocket</a></dt>
|
||||||
<dd>
|
<dd>
|
||||||
<a href="https://liveblocks.io/document/yjs">Liveblocks Yjs</a> provides a fully
|
A module that contains a simple websocket backend and a websocket client that
|
||||||
hosted WebSocket infrastructure and persisted data store for Yjs
|
connects to that backend. The backend can be extended to persist updates in a
|
||||||
documents. No configuration or maintenance is required. It also features
|
leveldb database.
|
||||||
Yjs webhook events, REST API to read and update Yjs documents, and a
|
|
||||||
browser DevTools extension.
|
|
||||||
</dd>
|
</dd>
|
||||||
<dt><a href="https://github.com/drifting-in-space/y-sweet">y-sweet</a> ⭐</dt>
|
<dt><a href="https://github.com/yjs/y-indexeddb">y-indexeddb</a></dt>
|
||||||
<dd>
|
<dd>
|
||||||
A standalone yjs server with persistence to S3 or filesystem. They offer a
|
Efficiently persists document updates to the browsers indexeddb database.
|
||||||
<a href="https://y-sweet.cloud">cloud service</a> as well.
|
The document is immediately available and only diffs need to be synced through the
|
||||||
</dd>
|
network provider.
|
||||||
<dt><a href="https://github.com/ueberdosis/hocuspocus">Hocuspocus</a> ⭐</dt>
|
|
||||||
<dd>
|
|
||||||
A standalone extensible yjs server with sqlite persistence, webhooks, auth and more.
|
|
||||||
</dd>
|
|
||||||
<dt><a href="https://docs.superviz.com/collaboration/integrations/YJS/overview">@superviz/yjs</a></dt>
|
|
||||||
<dd>
|
|
||||||
SuperViz Yjs Provider comes with a secure, scalable real-time infrastructure
|
|
||||||
for Yjs documents, fully compatible with a set of real-time
|
|
||||||
collaboration components offered by SuperViz. This solution ensures
|
|
||||||
synchronization, offline editing, and real-time updates, enabling
|
|
||||||
multiple users to collaborate effectively within shared workspaces.
|
|
||||||
</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>
|
||||||
@ -246,89 +137,8 @@ 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>
|
|
||||||
<dt><a href="https://tinybase.org/">Tinybase</a></dt>
|
|
||||||
<dd>
|
|
||||||
The reactive data store for local-first apps. They support multiple CRDTs and
|
|
||||||
different network technologies.
|
|
||||||
</dd>
|
|
||||||
<dt><a href="https://codeberg.org/webxdc/y-webxdc">y-webxdc</a></dt>
|
|
||||||
<dd>
|
|
||||||
Provider for sharing data in <a href="https://webxdc.org">webxdc chat apps</a>.
|
|
||||||
</dd>
|
|
||||||
<dt><a href="https://www.secsync.com/">secsync</a></dt>
|
|
||||||
<dd>
|
|
||||||
An architecture to relay end-to-end encrypted CRDTs over a central service.
|
|
||||||
</dd>
|
|
||||||
|
|
||||||
</dl>
|
</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>
|
|
||||||
<dd>
|
|
||||||
Adds persistent storage to a server with MongoDB. Can be used with the
|
|
||||||
y-websocket provider.
|
|
||||||
</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>
|
|
||||||
<dt><a href="https://github.com/malte-j/y-op-sqlite">y-op-sqlite</a></dt>
|
|
||||||
<dd>
|
|
||||||
Persist YJS updates in your React Native app using
|
|
||||||
<a href="https://github.com/OP-Engineering/op-sqlite">op-sqlite</a>
|
|
||||||
, the fastest SQLite library for React Native.
|
|
||||||
</dd>
|
|
||||||
<dt><a href="https://github.com/MaxNoetzold/y-postgresql">y-postgresql</a></dt>
|
|
||||||
<dd>
|
|
||||||
Provides persistent storage for a web server using PostgreSQL and
|
|
||||||
is easily compatible with y-websocket.
|
|
||||||
</dd>
|
|
||||||
<dt><a href="https://github.com/kapv89/k_yrs_go">k_yrs_go</a></dt>
|
|
||||||
<dd>
|
|
||||||
Golang database server for YJS CRDT using Postgres + Redis
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
|
|
||||||
### Tooling
|
|
||||||
|
|
||||||
* [y-sweet debugger](https://docs.jamsocket.com/y-sweet/advanced/debugger)
|
|
||||||
* [liveblocks devtools](https://liveblocks.io/devtools)
|
|
||||||
* [Yjs inspector](https://inspector.yjs.dev)
|
|
||||||
|
|
||||||
### 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
|
|
||||||
* [y_ex](https://github.com/satoren/y_ex) - Elixir bindings
|
|
||||||
* [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:
|
||||||
@ -340,15 +150,12 @@ npm i yjs y-websocket
|
|||||||
Start the y-websocket server:
|
Start the y-websocket server:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
PORT=1234 node ./node_modules/y-websocket/bin/server.cjs
|
PORT=1234 node ./node_modules/y-websocket/bin/server.js
|
||||||
```
|
```
|
||||||
|
|
||||||
### Example: Observe types
|
### Example: Observe types
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import * as Y from 'yjs';
|
|
||||||
|
|
||||||
const doc = new Y.Doc();
|
|
||||||
const yarray = doc.getArray('my-array')
|
const yarray = doc.getArray('my-array')
|
||||||
yarray.observe(event => {
|
yarray.observe(event => {
|
||||||
console.log('yarray was modified')
|
console.log('yarray was modified')
|
||||||
@ -441,11 +248,6 @@ necessary.
|
|||||||
</p>
|
</p>
|
||||||
<pre>const yarray = new Y.Array()</pre>
|
<pre>const yarray = new Y.Array()</pre>
|
||||||
<dl>
|
<dl>
|
||||||
<b><code>
|
|
||||||
Y.Array.from(Array<object|boolean|Array|string|number|null|Uint8Array|Y.Type>):
|
|
||||||
Y.Array
|
|
||||||
</code></b>
|
|
||||||
<dd>An alternative factory function to create a Y.Array based on existing content.</dd>
|
|
||||||
<b><code>parent:Y.AbstractType|null</code></b>
|
<b><code>parent:Y.AbstractType|null</code></b>
|
||||||
<dd></dd>
|
<dd></dd>
|
||||||
<b><code>insert(index:number, content:Array<object|boolean|Array|string|number|null|Uint8Array|Y.Type>)</code></b>
|
<b><code>insert(index:number, content:Array<object|boolean|Array|string|number|null|Uint8Array|Y.Type>)</code></b>
|
||||||
@ -475,11 +277,6 @@ forEach(function(value:object|boolean|Array|string|number|null|Uint8Array|Y.Type
|
|||||||
<dd></dd>
|
<dd></dd>
|
||||||
<b><code>map(function(T, number, YArray):M):Array<M></code></b>
|
<b><code>map(function(T, number, YArray):M):Array<M></code></b>
|
||||||
<dd></dd>
|
<dd></dd>
|
||||||
<b><code>clone(): Y.Array</code></b>
|
|
||||||
<dd>
|
|
||||||
Clone all values into a fresh Y.Array instance. The returned type can be
|
|
||||||
included into the Yjs document.
|
|
||||||
</dd>
|
|
||||||
<b><code>toArray():Array<object|boolean|Array|string|number|null|Uint8Array|Y.Type></code></b>
|
<b><code>toArray():Array<object|boolean|Array|string|number|null|Uint8Array|Y.Type></code></b>
|
||||||
<dd>Copies the content of this YArray to a new Array.</dd>
|
<dd>Copies the content of this YArray to a new Array.</dd>
|
||||||
<b><code>toJSON():Array<Object|boolean|Array|string|number|null></code></b>
|
<b><code>toJSON():Array<Object|boolean|Array|string|number|null></code></b>
|
||||||
@ -536,6 +333,8 @@ or any of its children.
|
|||||||
<dd></dd>
|
<dd></dd>
|
||||||
<b><code>has(key:string):boolean</code></b>
|
<b><code>has(key:string):boolean</code></b>
|
||||||
<dd></dd>
|
<dd></dd>
|
||||||
|
<b><code>get(index:number)</code></b>
|
||||||
|
<dd></dd>
|
||||||
<b><code>clear()</code></b>
|
<b><code>clear()</code></b>
|
||||||
<dd>Removes all elements from this YMap.</dd>
|
<dd>Removes all elements from this YMap.</dd>
|
||||||
<b><code>clone():Y.Map</code></b>
|
<b><code>clone():Y.Map</code></b>
|
||||||
@ -830,10 +629,6 @@ type. Doesn't log types that have not been defined (using
|
|||||||
<dd>Define a shared Y.Array type. Is equivalent to <code>y.get(string, Y.Array)</code>.</dd>
|
<dd>Define a shared Y.Array type. Is equivalent to <code>y.get(string, Y.Array)</code>.</dd>
|
||||||
<b><code>getMap(string):Y.Map</code></b>
|
<b><code>getMap(string):Y.Map</code></b>
|
||||||
<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>
|
|
||||||
<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>
|
||||||
@ -848,8 +643,7 @@ 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. Use `updateV2`
|
peers. You can apply document updates in any order and multiple times.
|
||||||
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>
|
||||||
@ -889,7 +683,7 @@ doc1.getArray('myarray').insert(0, ['Hello doc2, you got this?'])
|
|||||||
doc2.getArray('myarray').get(0) // => 'Hello doc2, you got this?'
|
doc2.getArray('myarray').get(0) // => 'Hello doc2, you got this?'
|
||||||
```
|
```
|
||||||
|
|
||||||
Yjs internally maintains a [state vector](#state-vector) that denotes the next
|
Yjs internally maintains a [state vector](#State-Vector) that denotes the next
|
||||||
expected clock from each client. In a different interpretation it holds the
|
expected clock from each client. In a different interpretation it holds the
|
||||||
number of structs created by each client. When two clients sync, you can either
|
number of structs created by each client. When two clients sync, you can either
|
||||||
exchange the complete document structure or only the differences by sending the
|
exchange the complete document structure or only the differences by sending the
|
||||||
@ -941,43 +735,17 @@ const diff2 = Y.diffUpdate(currentState2, stateVector1)
|
|||||||
|
|
||||||
// sync clients
|
// sync clients
|
||||||
currentState1 = Y.mergeUpdates([currentState1, diff2])
|
currentState1 = Y.mergeUpdates([currentState1, diff2])
|
||||||
currentState2 = Y.mergeUpdates([currentState2, diff1])
|
currentState1 = Y.mergeUpdates([currentState1, diff1])
|
||||||
```
|
|
||||||
|
|
||||||
#### Obfuscating Updates
|
|
||||||
|
|
||||||
If one of your users runs into a weird bug (e.g. the rich-text editor throws
|
|
||||||
error messages), then you don't have to request the full document from your
|
|
||||||
user. Instead, they can obfuscate the document (i.e. replace the content with
|
|
||||||
meaningless generated content) before sending it to you. Note that someone might
|
|
||||||
still deduce the type of content by looking at the general structure of the
|
|
||||||
document. But this is much better than requesting the original document.
|
|
||||||
|
|
||||||
Obfuscated updates contain all the CRDT-related data that is required for
|
|
||||||
merging. So it is safe to merge obfuscated updates.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const ydoc = new Y.Doc()
|
|
||||||
// perform some changes..
|
|
||||||
ydoc.getText().insert(0, 'hello world')
|
|
||||||
const update = Y.encodeStateAsUpdate(ydoc)
|
|
||||||
// the below update contains scrambled data
|
|
||||||
const obfuscatedUpdate = Y.obfuscateUpdate(update)
|
|
||||||
const ydoc2 = new Y.Doc()
|
|
||||||
Y.applyUpdate(ydoc2, obfuscatedUpdate)
|
|
||||||
ydoc2.getText().toString() // => "00000000000"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Using V2 update format
|
#### Using V2 update format
|
||||||
|
|
||||||
Yjs implements two update formats. By default you are using the V1 update format.
|
Yjs implements two update formats. By default you are using the V1 update format.
|
||||||
You can opt-in into the V2 update format which 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`. Also when listening to updates
|
suffix "V2". E.g. `Y.applyUpdate` ⇒ `Y.applyUpdateV2`. We also support conversion
|
||||||
you need to specifically need listen for V2 events e.g. `yDoc.on('updateV2', …)`.
|
functions between both formats: `Y.convertUpdateFormatV1ToV2` & `Y.convertUpdateFormatV2ToV1`.
|
||||||
We also support conversion functions between both formats:
|
|
||||||
`Y.convertUpdateFormatV1ToV2` & `Y.convertUpdateFormatV2ToV1`.
|
|
||||||
|
|
||||||
#### Update API
|
#### Update API
|
||||||
|
|
||||||
@ -1102,7 +870,7 @@ encoding format for document updates. If you prefer JSON encoding, you can
|
|||||||
simply JSON.stringify / JSON.parse the relative position instead.
|
simply JSON.stringify / JSON.parse the relative position instead.
|
||||||
</dd>
|
</dd>
|
||||||
<b><code>Y.decodeRelativePosition(Uint8Array):RelativePosition</code></b>
|
<b><code>Y.decodeRelativePosition(Uint8Array):RelativePosition</code></b>
|
||||||
<dd>Decode a binary-encoded relative position to a RelativePosition object.</dd>
|
<dd>Decode a binary-encoded relative position to a RelativePositon object.</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
### Y.UndoManager
|
### Y.UndoManager
|
||||||
@ -1222,7 +990,7 @@ doc.transact(() => {
|
|||||||
ytext.insert(0, 'abc')
|
ytext.insert(0, 'abc')
|
||||||
}, 41)
|
}, 41)
|
||||||
undoManager.undo()
|
undoManager.undo()
|
||||||
ytext.toString() // => 'abc' (not tracked because 41 is not an instance of
|
ytext.toString() // => '' (not tracked because 41 is not an instance of
|
||||||
// `trackedTransactionorigins`)
|
// `trackedTransactionorigins`)
|
||||||
ytext.delete(0, 3) // revert change
|
ytext.delete(0, 3) // revert change
|
||||||
|
|
||||||
@ -1280,17 +1048,16 @@ More information about the specific implementation is available in
|
|||||||
[INTERNALS.md](./INTERNALS.md) and in
|
[INTERNALS.md](./INTERNALS.md) and in
|
||||||
[this walkthrough of the Yjs codebase](https://youtu.be/0l5XgnQ6rB4).
|
[this walkthrough of the Yjs codebase](https://youtu.be/0l5XgnQ6rB4).
|
||||||
|
|
||||||
CRDTs that are suitable for shared text editing suffer from the fact that they
|
CRDTs that suitable for shared text editing suffer from the fact that they only grow
|
||||||
only grow in size. There are CRDTs that do not grow in size, but they do not
|
in size. There are CRDTs that do not grow in size, but they do not have the
|
||||||
have the characteristics that are beneficial for shared text editing (like
|
characteristics that are benificial for shared text editing (like intention
|
||||||
intention preservation). Yjs implements many improvements to the original
|
preservation). Yjs implements many improvements to the original algorithm that
|
||||||
algorithm that diminish the trade-off that the document only grows in size. We
|
diminish the trade-off that the document only grows in size. We can't garbage
|
||||||
can't garbage collect deleted structs (tombstones) while ensuring a unique
|
collect deleted structs (tombstones) while ensuring a unique order of the
|
||||||
order of the structs. But we can 1. merge preceding structs into a single
|
structs. But we can 1. merge preceeding structs into a single struct to reduce
|
||||||
struct to reduce the amount of meta information, 2. we can delete content from
|
the amount of meta information, 2. we can delete content from the struct if it
|
||||||
the struct if it is deleted, and 3. we can garbage collect tombstones if we
|
is deleted, and 3. we can garbage collect tombstones if we don't care about the
|
||||||
don't care about the order of the structs anymore (e.g. if the parent was
|
order of the structs anymore (e.g. if the parent was deleted).
|
||||||
deleted).
|
|
||||||
|
|
||||||
**Examples:**
|
**Examples:**
|
||||||
|
|
||||||
|
142
funding.json
142
funding.json
@ -1,142 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "v1.0.0",
|
|
||||||
"entity": {
|
|
||||||
"type": "group",
|
|
||||||
"role": "steward",
|
|
||||||
"name": "Kevin Jahns",
|
|
||||||
"email": "kevin.jahns@protonmail.com",
|
|
||||||
"phone": "",
|
|
||||||
"description": "OSS Developer",
|
|
||||||
"webpageUrl": {
|
|
||||||
"url": "https://github.com/yjs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"projects": [
|
|
||||||
{
|
|
||||||
"guid": "yjs",
|
|
||||||
"name": "Yjs",
|
|
||||||
"description": "A library for building collaborative applications. #p2p #local-first #CRDT Funding this project will also enable me to maintain the other Yjs-related technologies.",
|
|
||||||
"webpageUrl": {
|
|
||||||
"url": "https://github.com/yjs/yjs"
|
|
||||||
},
|
|
||||||
"repositoryUrl": {
|
|
||||||
"url": "https://github.com/yjs/yjs"
|
|
||||||
},
|
|
||||||
"licenses": [
|
|
||||||
"spdx:MIT"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"collaboration",
|
|
||||||
"p2p",
|
|
||||||
"CRDT",
|
|
||||||
"rich-text",
|
|
||||||
"real-time"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"guid": "Titanic",
|
|
||||||
"name": "Y/Titanic",
|
|
||||||
"description": "A provider for syncing millions of docs efficiently with other peers. This will become the foundation for building real local-first apps with Yjs.",
|
|
||||||
"webpageUrl": {
|
|
||||||
"url": "https://github.com/yjs/titanic",
|
|
||||||
"wellKnown": "https://github.com/yjs/titanic/blob/main/.well-known/funding-manifest-urls"
|
|
||||||
},
|
|
||||||
"repositoryUrl": {
|
|
||||||
"url": "https://github.com/yjs/titanic",
|
|
||||||
"wellKnown": "https://github.com/yjs/titanic/blob/main/.well-known/funding-manifest-urls"
|
|
||||||
},
|
|
||||||
"licenses": [
|
|
||||||
"spdx:MIT"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"privacy",
|
|
||||||
"collaboration",
|
|
||||||
"p2p",
|
|
||||||
"CRDT",
|
|
||||||
"rich-text",
|
|
||||||
"real-time",
|
|
||||||
"web-development"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"channels": [
|
|
||||||
{
|
|
||||||
"guid": "github-sponsors",
|
|
||||||
"type": "payment-provider",
|
|
||||||
"address": "",
|
|
||||||
"description": "For funding of the Yjs project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"guid": "y-collective",
|
|
||||||
"type": "payment-provider",
|
|
||||||
"address": "https://opencollective.com/y-collective",
|
|
||||||
"description": "For funding the Y-CRDT - the Rust implementation of Yjs and other listed projects."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"plans": [
|
|
||||||
{
|
|
||||||
"guid": "supporter",
|
|
||||||
"status": "active",
|
|
||||||
"name": "Supporter",
|
|
||||||
"description": "",
|
|
||||||
"amount": 0,
|
|
||||||
"currency": "USD",
|
|
||||||
"frequency": "monthly",
|
|
||||||
"channels": [
|
|
||||||
"github-sponsors",
|
|
||||||
"y-collective"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"guid": "titanic-funding",
|
|
||||||
"status": "active",
|
|
||||||
"name": "Titanic Funding",
|
|
||||||
"description": "Fund the next generation of local-first providers.",
|
|
||||||
"amount": 30000,
|
|
||||||
"currency": "USD",
|
|
||||||
"frequency": "one-time",
|
|
||||||
"channels": [
|
|
||||||
"github-sponsors"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"guid": "bronze-sponsor",
|
|
||||||
"status": "active",
|
|
||||||
"name": "Bronze Sponsor",
|
|
||||||
"description": "This is the recommended plan for companies that use Yjs.",
|
|
||||||
"amount": 500,
|
|
||||||
"currency": "USD",
|
|
||||||
"frequency": "monthly",
|
|
||||||
"channels": [
|
|
||||||
"github-sponsors"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"guid": "silver-sponsor",
|
|
||||||
"status": "active",
|
|
||||||
"name": "Silver Sponsor",
|
|
||||||
"description": "This is the recommended plan for large/successfull companies that use Yjs.",
|
|
||||||
"amount": 1000,
|
|
||||||
"currency": "USD",
|
|
||||||
"frequency": "monthly",
|
|
||||||
"channels": [
|
|
||||||
"github-sponsors"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"guid": "gold-sponsor",
|
|
||||||
"status": "active",
|
|
||||||
"name": "Gold Sponsor",
|
|
||||||
"description": "This is the recommended plan for successful companies that build their entire product around Yjs-related technologies.",
|
|
||||||
"amount": 3000,
|
|
||||||
"currency": "USD",
|
|
||||||
"frequency": "monthly",
|
|
||||||
"channels": [
|
|
||||||
"github-sponsors"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"history": null
|
|
||||||
}
|
|
||||||
}
|
|
5228
package-lock.json
generated
5228
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.6.24",
|
"version": "13.5.39",
|
||||||
"description": "Shared Editing Library",
|
"description": "Shared Editing Library",
|
||||||
"main": "./dist/yjs.cjs",
|
"main": "./dist/yjs.cjs",
|
||||||
"module": "./dist/yjs.mjs",
|
"module": "./dist/yjs.mjs",
|
||||||
@ -12,10 +12,9 @@
|
|||||||
"url": "https://github.com/sponsors/dmonad"
|
"url": "https://github.com/sponsors/dmonad"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rm -rf dist docs",
|
"test": "npm run dist && node ./dist/tests.cjs --repetition-time 50",
|
||||||
"test": "npm run dist && NODE_ENV=development node ./dist/tests.cjs --repetition-time 50",
|
|
||||||
"test-extensive": "npm run lint && npm run dist && node ./dist/tests.cjs --production --repetition-time 10000",
|
"test-extensive": "npm run lint && npm run dist && node ./dist/tests.cjs --production --repetition-time 10000",
|
||||||
"dist": "npm run clean && rollup -c && tsc",
|
"dist": "rm -rf dist && rollup -c && tsc",
|
||||||
"watch": "rollup -wc",
|
"watch": "rollup -wc",
|
||||||
"lint": "markdownlint README.md && standard && tsc",
|
"lint": "markdownlint README.md && standard && tsc",
|
||||||
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true",
|
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true",
|
||||||
@ -28,7 +27,6 @@
|
|||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/src/index.d.ts",
|
"types": "./dist/src/index.d.ts",
|
||||||
"module": "./dist/yjs.mjs",
|
|
||||||
"import": "./dist/yjs.mjs",
|
"import": "./dist/yjs.mjs",
|
||||||
"require": "./dist/yjs.cjs"
|
"require": "./dist/yjs.cjs"
|
||||||
},
|
},
|
||||||
@ -76,24 +74,19 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://docs.yjs.dev",
|
"homepage": "https://docs.yjs.dev",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lib0": "^0.2.99"
|
"lib0": "^0.2.49"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^24.0.1",
|
"@rollup/plugin-commonjs": "^17.0.0",
|
||||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
"@rollup/plugin-node-resolve": "^11.2.1",
|
||||||
"@types/node": "^18.15.5",
|
|
||||||
"concurrently": "^3.6.1",
|
"concurrently": "^3.6.1",
|
||||||
"http-server": "^0.12.3",
|
"http-server": "^0.12.3",
|
||||||
"jsdoc": "^3.6.7",
|
"jsdoc": "^3.6.7",
|
||||||
"markdownlint-cli": "^0.41.0",
|
"markdownlint-cli": "^0.23.2",
|
||||||
"rollup": "^3.20.0",
|
"rollup": "^2.60.0",
|
||||||
"standard": "^16.0.4",
|
"standard": "^16.0.4",
|
||||||
"tui-jsdoc-template": "^1.2.2",
|
"tui-jsdoc-template": "^1.2.2",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.4.4",
|
||||||
"y-protocols": "^1.0.5"
|
"y-protocols": "^1.0.5"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"npm": ">=8.0.0",
|
|
||||||
"node": ">=16.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,13 @@ export default [{
|
|||||||
name: 'Y',
|
name: 'Y',
|
||||||
file: 'dist/yjs.cjs',
|
file: 'dist/yjs.cjs',
|
||||||
format: 'cjs',
|
format: 'cjs',
|
||||||
sourcemap: true
|
sourcemap: true,
|
||||||
|
paths: path => {
|
||||||
|
if (/^lib0\//.test(path)) {
|
||||||
|
return `lib0/dist/${path.slice(5)}.cjs`
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
},
|
},
|
||||||
external: id => /^lib0\//.test(id)
|
external: id => /^lib0\//.test(id)
|
||||||
}, {
|
}, {
|
||||||
@ -82,7 +88,7 @@ export default [{
|
|||||||
plugins: [
|
plugins: [
|
||||||
debugResolve,
|
debugResolve,
|
||||||
nodeResolve({
|
nodeResolve({
|
||||||
mainFields: ['browser', 'module', 'main']
|
mainFields: ['module', 'browser', 'main']
|
||||||
}),
|
}),
|
||||||
commonjs()
|
commonjs()
|
||||||
]
|
]
|
||||||
@ -97,10 +103,9 @@ export default [{
|
|||||||
plugins: [
|
plugins: [
|
||||||
debugResolve,
|
debugResolve,
|
||||||
nodeResolve({
|
nodeResolve({
|
||||||
mainFields: ['node', 'module', 'main'],
|
mainFields: ['module', 'main']
|
||||||
exportConditions: ['node', 'module', 'import', 'default']
|
|
||||||
}),
|
}),
|
||||||
commonjs()
|
commonjs()
|
||||||
],
|
],
|
||||||
external: id => /^lib0\//.test(id)
|
external: ['isomorphic.js']
|
||||||
}]
|
}]
|
||||||
|
23
src/index.js
23
src/index.js
@ -18,10 +18,8 @@ export {
|
|||||||
Item,
|
Item,
|
||||||
AbstractStruct,
|
AbstractStruct,
|
||||||
GC,
|
GC,
|
||||||
Skip,
|
|
||||||
ContentBinary,
|
ContentBinary,
|
||||||
ContentDeleted,
|
ContentDeleted,
|
||||||
ContentDoc,
|
|
||||||
ContentEmbed,
|
ContentEmbed,
|
||||||
ContentFormat,
|
ContentFormat,
|
||||||
ContentJSON,
|
ContentJSON,
|
||||||
@ -50,11 +48,8 @@ export {
|
|||||||
findRootTypeKey,
|
findRootTypeKey,
|
||||||
findIndexSS,
|
findIndexSS,
|
||||||
getItem,
|
getItem,
|
||||||
getItemCleanStart,
|
|
||||||
getItemCleanEnd,
|
|
||||||
typeListToArraySnapshot,
|
typeListToArraySnapshot,
|
||||||
typeMapGetSnapshot,
|
typeMapGetSnapshot,
|
||||||
typeMapGetAllSnapshot,
|
|
||||||
createDocFromSnapshot,
|
createDocFromSnapshot,
|
||||||
iterateDeletedStructs,
|
iterateDeletedStructs,
|
||||||
applyUpdate,
|
applyUpdate,
|
||||||
@ -95,20 +90,10 @@ export {
|
|||||||
diffUpdateV2,
|
diffUpdateV2,
|
||||||
convertUpdateFormatV1ToV2,
|
convertUpdateFormatV1ToV2,
|
||||||
convertUpdateFormatV2ToV1,
|
convertUpdateFormatV2ToV1,
|
||||||
obfuscateUpdate,
|
UpdateEncoderV1
|
||||||
obfuscateUpdateV2,
|
|
||||||
UpdateEncoderV1,
|
|
||||||
UpdateEncoderV2,
|
|
||||||
UpdateDecoderV1,
|
|
||||||
UpdateDecoderV2,
|
|
||||||
equalDeleteSets,
|
|
||||||
mergeDeleteSets,
|
|
||||||
snapshotContainsUpdate
|
|
||||||
} from './internals.js'
|
} from './internals.js'
|
||||||
|
|
||||||
const glo = /** @type {any} */ (typeof globalThis !== 'undefined'
|
const glo = /** @type {any} */ (typeof window !== 'undefined'
|
||||||
? globalThis
|
|
||||||
: typeof window !== 'undefined'
|
|
||||||
? window
|
? window
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
: typeof global !== 'undefined' ? global : {})
|
: typeof global !== 'undefined' ? global : {})
|
||||||
@ -127,9 +112,7 @@ if (glo[importIdentifier] === true) {
|
|||||||
* This often leads to issues that are hard to debug. We often need to perform constructor checks,
|
* This often leads to issues that are hard to debug. We often need to perform constructor checks,
|
||||||
* e.g. `struct instanceof GC`. If you imported different versions of Yjs, it is impossible for us to
|
* e.g. `struct instanceof GC`. If you imported different versions of Yjs, it is impossible for us to
|
||||||
* do the constructor checks anymore - which might break the CRDT algorithm.
|
* do the constructor checks anymore - which might break the CRDT algorithm.
|
||||||
*
|
|
||||||
* https://github.com/yjs/yjs/issues/438
|
|
||||||
*/
|
*/
|
||||||
console.error('Yjs was already imported. This breaks constructor checks and will lead to issues! - https://github.com/yjs/yjs/issues/438')
|
console.error('Yjs was already imported. This breaks constructor checks and will lead to isssues!')
|
||||||
}
|
}
|
||||||
glo[importIdentifier] = true
|
glo[importIdentifier] = true
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
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,3 +1,4 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
UpdateEncoderV1, UpdateEncoderV2, ID, Transaction // eslint-disable-line
|
UpdateEncoderV1, UpdateEncoderV2, ID, Transaction // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
@ -26,7 +27,7 @@ export class AbstractStruct {
|
|||||||
* This method is already assuming that `this.id.clock + this.length === this.id.clock`.
|
* This method is already assuming that `this.id.clock + this.length === this.id.clock`.
|
||||||
* Also this method does *not* remove right from StructStore!
|
* Also this method does *not* remove right from StructStore!
|
||||||
* @param {AbstractStruct} right
|
* @param {AbstractStruct} right
|
||||||
* @return {boolean} whether this merged with right
|
* @return {boolean} wether this merged with right
|
||||||
*/
|
*/
|
||||||
mergeWith (right) {
|
mergeWith (right) {
|
||||||
return false
|
return false
|
||||||
|
@ -2,11 +2,6 @@ import {
|
|||||||
UpdateEncoderV1, UpdateEncoderV2, UpdateDecoderV1, UpdateDecoderV2, Transaction, Item, StructStore // eslint-disable-line
|
UpdateEncoderV1, UpdateEncoderV2, UpdateDecoderV1, UpdateDecoderV2, Transaction, Item, StructStore // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as env from 'lib0/environment'
|
|
||||||
import * as object from 'lib0/object'
|
|
||||||
|
|
||||||
const isDevMode = env.getVariable('node_env') === 'development'
|
|
||||||
|
|
||||||
export class ContentAny {
|
export class ContentAny {
|
||||||
/**
|
/**
|
||||||
* @param {Array<any>} arr
|
* @param {Array<any>} arr
|
||||||
@ -16,7 +11,6 @@ export class ContentAny {
|
|||||||
* @type {Array<any>}
|
* @type {Array<any>}
|
||||||
*/
|
*/
|
||||||
this.arr = arr
|
this.arr = arr
|
||||||
isDevMode && object.deepFreeze(arr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
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,3 +1,4 @@
|
|||||||
|
|
||||||
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,3 +1,4 @@
|
|||||||
|
|
||||||
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,5 +1,6 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
YText, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Item, StructStore, Transaction // eslint-disable-line
|
AbstractType, 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'
|
||||||
@ -46,30 +47,28 @@ 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
|
||||||
const p = /** @type {YText} */ (item.parent)
|
/** @type {AbstractType<any>} */ (item.parent)._searchMarker = null
|
||||||
p._searchMarker = null
|
|
||||||
p._hasFormatting = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
readYArray,
|
readYArray,
|
||||||
readYMap,
|
readYMap,
|
||||||
@ -107,7 +108,7 @@ export class ContentType {
|
|||||||
while (item !== null) {
|
while (item !== null) {
|
||||||
if (!item.deleted) {
|
if (!item.deleted) {
|
||||||
item.delete(transaction)
|
item.delete(transaction)
|
||||||
} else if (item.id.clock < (transaction.beforeState.get(item.id.client) || 0)) {
|
} else {
|
||||||
// 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
|
||||||
@ -119,7 +120,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 if (item.id.clock < (transaction.beforeState.get(item.id.client) || 0)) {
|
} else {
|
||||||
// same as above
|
// same as above
|
||||||
transaction._mergeStructs.push(item)
|
transaction._mergeStructs.push(item)
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
AbstractStruct,
|
AbstractStruct,
|
||||||
addStruct,
|
addStruct,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
GC,
|
GC,
|
||||||
getState,
|
getState,
|
||||||
@ -22,12 +23,11 @@ import {
|
|||||||
readContentType,
|
readContentType,
|
||||||
addChangedTypeToTransaction,
|
addChangedTypeToTransaction,
|
||||||
isDeleted,
|
isDeleted,
|
||||||
StackItem, DeleteSet, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction // eslint-disable-line
|
DeleteSet, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as error from 'lib0/error'
|
import * as error from 'lib0/error'
|
||||||
import * as binary from 'lib0/binary'
|
import * as binary from 'lib0/binary'
|
||||||
import * as array from 'lib0/array'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @todo This should return several items
|
* @todo This should return several items
|
||||||
@ -120,12 +120,6 @@ export const splitItem = (transaction, leftItem, diff) => {
|
|||||||
return rightItem
|
return rightItem
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Array<StackItem>} stack
|
|
||||||
* @param {ID} id
|
|
||||||
*/
|
|
||||||
const isDeletedByUndoStack = (stack, id) => array.some(stack, /** @param {StackItem} s */ s => isDeleted(s.deletions, id))
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redoes the effect of this operation.
|
* Redoes the effect of this operation.
|
||||||
*
|
*
|
||||||
@ -134,13 +128,12 @@ const isDeletedByUndoStack = (stack, id) => array.some(stack, /** @param {StackI
|
|||||||
* @param {Set<Item>} redoitems
|
* @param {Set<Item>} redoitems
|
||||||
* @param {DeleteSet} itemsToDelete
|
* @param {DeleteSet} itemsToDelete
|
||||||
* @param {boolean} ignoreRemoteMapChanges
|
* @param {boolean} ignoreRemoteMapChanges
|
||||||
* @param {import('../utils/UndoManager.js').UndoManager} um
|
|
||||||
*
|
*
|
||||||
* @return {Item|null}
|
* @return {Item|null}
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemoteMapChanges, um) => {
|
export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemoteMapChanges) => {
|
||||||
const doc = transaction.doc
|
const doc = transaction.doc
|
||||||
const store = doc.store
|
const store = doc.store
|
||||||
const ownClientID = doc.clientID
|
const ownClientID = doc.clientID
|
||||||
@ -160,7 +153,7 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemo
|
|||||||
// make sure that parent is redone
|
// make sure that parent is redone
|
||||||
if (parentItem !== null && parentItem.deleted === true) {
|
if (parentItem !== null && parentItem.deleted === true) {
|
||||||
// try to undo parent if it will be undone anyway
|
// try to undo parent if it will be undone anyway
|
||||||
if (parentItem.redone === null && (!redoitems.has(parentItem) || redoItem(transaction, parentItem, redoitems, itemsToDelete, ignoreRemoteMapChanges, um) === null)) {
|
if (parentItem.redone === null && (!redoitems.has(parentItem) || redoItem(transaction, parentItem, redoitems, itemsToDelete, ignoreRemoteMapChanges) === null)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
while (parentItem.redone !== null) {
|
while (parentItem.redone !== null) {
|
||||||
@ -210,10 +203,18 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemo
|
|||||||
left = item
|
left = item
|
||||||
// Iterate right while right is in itemsToDelete
|
// Iterate right while right is in itemsToDelete
|
||||||
// If it is intended to delete right while item is redone, we can expect that item should replace right.
|
// If it is intended to delete right while item is redone, we can expect that item should replace right.
|
||||||
while (left !== null && left.right !== null && (left.right.redone || isDeleted(itemsToDelete, left.right.id) || isDeletedByUndoStack(um.undoStack, left.right.id) || isDeletedByUndoStack(um.redoStack, left.right.id))) {
|
while (left !== null && left.right !== null && isDeleted(itemsToDelete, left.right.id)) {
|
||||||
left = left.right
|
left = left.right
|
||||||
|
}
|
||||||
// follow redone
|
// follow redone
|
||||||
while (left.redone) left = getItemCleanStart(transaction, left.redone)
|
// trace redone until parent matches
|
||||||
|
while (left !== null && left.redone !== null) {
|
||||||
|
left = getItemCleanStart(transaction, left.redone)
|
||||||
|
}
|
||||||
|
// check wether we were allowed to follow right (indicating that originally this op was replaced by another item)
|
||||||
|
if (left === null || /** @type {AbstractType<any>} */ (left.parent)._item !== parentItem) {
|
||||||
|
// invalid parent; should never happen
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
if (left && left.right !== null) {
|
if (left && left.right !== null) {
|
||||||
// It is not possible to redo this item because it conflicts with a
|
// It is not possible to redo this item because it conflicts with a
|
||||||
@ -388,12 +389,14 @@ 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
|
||||||
} else if (this.right && this.right.constructor === Item) {
|
}
|
||||||
|
if (this.right && this.right.constructor === Item) {
|
||||||
this.parent = this.right.parent
|
this.parent = this.right.parent
|
||||||
this.parentSub = this.right.parentSub
|
this.parentSub = this.right.parentSub
|
||||||
}
|
}
|
||||||
@ -758,48 +761,48 @@ export class AbstractContent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} _offset
|
* @param {number} offset
|
||||||
* @return {AbstractContent}
|
* @return {AbstractContent}
|
||||||
*/
|
*/
|
||||||
splice (_offset) {
|
splice (offset) {
|
||||||
throw error.methodUnimplemented()
|
throw error.methodUnimplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractContent} _right
|
* @param {AbstractContent} right
|
||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
*/
|
*/
|
||||||
mergeWith (_right) {
|
mergeWith (right) {
|
||||||
throw error.methodUnimplemented()
|
throw error.methodUnimplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Transaction} _transaction
|
* @param {Transaction} transaction
|
||||||
* @param {Item} _item
|
* @param {Item} item
|
||||||
*/
|
*/
|
||||||
integrate (_transaction, _item) {
|
integrate (transaction, item) {
|
||||||
throw error.methodUnimplemented()
|
throw error.methodUnimplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Transaction} _transaction
|
* @param {Transaction} transaction
|
||||||
*/
|
*/
|
||||||
delete (_transaction) {
|
delete (transaction) {
|
||||||
throw error.methodUnimplemented()
|
throw error.methodUnimplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {StructStore} _store
|
* @param {StructStore} store
|
||||||
*/
|
*/
|
||||||
gc (_store) {
|
gc (store) {
|
||||||
throw error.methodUnimplemented()
|
throw error.methodUnimplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} _encoder
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
* @param {number} _offset
|
* @param {number} offset
|
||||||
*/
|
*/
|
||||||
write (_encoder, _offset) {
|
write (encoder, offset) {
|
||||||
throw error.methodUnimplemented()
|
throw error.methodUnimplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
AbstractStruct,
|
AbstractStruct,
|
||||||
UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, ID // eslint-disable-line
|
UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, ID // eslint-disable-line
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
removeEventHandlerListener,
|
removeEventHandlerListener,
|
||||||
callEventHandlerListeners,
|
callEventHandlerListeners,
|
||||||
@ -17,12 +18,6 @@ import * as map from 'lib0/map'
|
|||||||
import * as iterator from 'lib0/iterator'
|
import * as iterator from 'lib0/iterator'
|
||||||
import * as error from 'lib0/error'
|
import * as error from 'lib0/error'
|
||||||
import * as math from 'lib0/math'
|
import * as math from 'lib0/math'
|
||||||
import * as log from 'lib0/logging'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* https://docs.yjs.dev/getting-started/working-with-shared-types#caveats
|
|
||||||
*/
|
|
||||||
export const warnPrematureAccess = () => { log.warn('Invalid access: Add Yjs type to a document before reading data.') }
|
|
||||||
|
|
||||||
const maxSearchMarker = 80
|
const maxSearchMarker = 80
|
||||||
|
|
||||||
@ -155,11 +150,11 @@ export const findMarker = (yarray, index) => {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// if (marker) {
|
// if (marker) {
|
||||||
// if (window.lengths == null) {
|
// if (window.lengthes == null) {
|
||||||
// window.lengths = []
|
// window.lengthes = []
|
||||||
// window.getLengths = () => window.lengths.sort((a, b) => a - b)
|
// window.getLengthes = () => window.lengthes.sort((a, b) => a - b)
|
||||||
// }
|
// }
|
||||||
// window.lengths.push(marker.index - pindex)
|
// window.lengthes.push(marker.index - pindex)
|
||||||
// console.log('distance', marker.index - pindex, 'len', p && p.parent.length)
|
// console.log('distance', marker.index - pindex, 'len', p && p.parent.length)
|
||||||
// }
|
// }
|
||||||
if (marker !== null && math.abs(marker.index - pindex) < /** @type {YText|YArray<any>} */ (p.parent).length / maxSearchMarker) {
|
if (marker !== null && math.abs(marker.index - pindex) < /** @type {YText|YArray<any>} */ (p.parent).length / maxSearchMarker) {
|
||||||
@ -221,7 +216,6 @@ export const updateMarkerChanges = (searchMarker, index, len) => {
|
|||||||
* @return {Array<Item>}
|
* @return {Array<Item>}
|
||||||
*/
|
*/
|
||||||
export const getTypeChildren = t => {
|
export const getTypeChildren = t => {
|
||||||
t.doc ?? warnPrematureAccess()
|
|
||||||
let s = t._start
|
let s = t._start
|
||||||
const arr = []
|
const arr = []
|
||||||
while (s) {
|
while (s) {
|
||||||
@ -323,10 +317,6 @@ export class AbstractType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes a copy of this data type that can be included somewhere else.
|
|
||||||
*
|
|
||||||
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
|
|
||||||
*
|
|
||||||
* @return {AbstractType<EventType>}
|
* @return {AbstractType<EventType>}
|
||||||
*/
|
*/
|
||||||
clone () {
|
clone () {
|
||||||
@ -334,9 +324,9 @@ export class AbstractType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} _encoder
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
*/
|
*/
|
||||||
_write (_encoder) { }
|
_write (encoder) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The first non-deleted item
|
* The first non-deleted item
|
||||||
@ -354,9 +344,9 @@ export class AbstractType {
|
|||||||
* Must be implemented by each type.
|
* Must be implemented by each type.
|
||||||
*
|
*
|
||||||
* @param {Transaction} transaction
|
* @param {Transaction} transaction
|
||||||
* @param {Set<null|string>} _parentSubs Keys changed on this type. `null` if list was modified.
|
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
|
||||||
*/
|
*/
|
||||||
_callObserver (transaction, _parentSubs) {
|
_callObserver (transaction, parentSubs) {
|
||||||
if (!transaction.local && this._searchMarker) {
|
if (!transaction.local && this._searchMarker) {
|
||||||
this._searchMarker.length = 0
|
this._searchMarker.length = 0
|
||||||
}
|
}
|
||||||
@ -415,7 +405,6 @@ export class AbstractType {
|
|||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const typeListSlice = (type, start, end) => {
|
export const typeListSlice = (type, start, end) => {
|
||||||
type.doc ?? warnPrematureAccess()
|
|
||||||
if (start < 0) {
|
if (start < 0) {
|
||||||
start = type._length + start
|
start = type._length + start
|
||||||
}
|
}
|
||||||
@ -451,7 +440,6 @@ export const typeListSlice = (type, start, end) => {
|
|||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const typeListToArray = type => {
|
export const typeListToArray = type => {
|
||||||
type.doc ?? warnPrematureAccess()
|
|
||||||
const cs = []
|
const cs = []
|
||||||
let n = type._start
|
let n = type._start
|
||||||
while (n !== null) {
|
while (n !== null) {
|
||||||
@ -490,7 +478,7 @@ export const typeListToArraySnapshot = (type, snapshot) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a provided function on once on every element of this YArray.
|
* Executes a provided function on once on overy element of this YArray.
|
||||||
*
|
*
|
||||||
* @param {AbstractType<any>} type
|
* @param {AbstractType<any>} type
|
||||||
* @param {function(any,number,any):void} f A function to execute on every element of this YArray.
|
* @param {function(any,number,any):void} f A function to execute on every element of this YArray.
|
||||||
@ -501,7 +489,6 @@ export const typeListToArraySnapshot = (type, snapshot) => {
|
|||||||
export const typeListForEach = (type, f) => {
|
export const typeListForEach = (type, f) => {
|
||||||
let index = 0
|
let index = 0
|
||||||
let n = type._start
|
let n = type._start
|
||||||
type.doc ?? warnPrematureAccess()
|
|
||||||
while (n !== null) {
|
while (n !== null) {
|
||||||
if (n.countable && !n.deleted) {
|
if (n.countable && !n.deleted) {
|
||||||
const c = n.content.getContent()
|
const c = n.content.getContent()
|
||||||
@ -583,7 +570,7 @@ export const typeListCreateIterator = type => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a provided function on once on every element of this YArray.
|
* Executes a provided function on once on overy element of this YArray.
|
||||||
* Operates on a snapshotted state of the document.
|
* Operates on a snapshotted state of the document.
|
||||||
*
|
*
|
||||||
* @param {AbstractType<any>} type
|
* @param {AbstractType<any>} type
|
||||||
@ -616,7 +603,6 @@ export const typeListForEachSnapshot = (type, f, snapshot) => {
|
|||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const typeListGet = (type, index) => {
|
export const typeListGet = (type, index) => {
|
||||||
type.doc ?? warnPrematureAccess()
|
|
||||||
const marker = findMarker(type, index)
|
const marker = findMarker(type, index)
|
||||||
let n = type._start
|
let n = type._start
|
||||||
if (marker !== null) {
|
if (marker !== null) {
|
||||||
@ -697,7 +683,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
|
||||||
@ -710,7 +696,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) {
|
||||||
@ -751,7 +737,7 @@ export const typeListInsertGenerics = (transaction, parent, index, content) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Pushing content is special as we generally want to push after the last item. So we don't have to update
|
* Pushing content is special as we generally want to push after the last item. So we don't have to update
|
||||||
* the search marker.
|
* the serach marker.
|
||||||
*
|
*
|
||||||
* @param {Transaction} transaction
|
* @param {Transaction} transaction
|
||||||
* @param {AbstractType<any>} parent
|
* @param {AbstractType<any>} parent
|
||||||
@ -812,7 +798,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 */)
|
||||||
@ -885,7 +871,6 @@ export const typeMapSet = (transaction, parent, key, value) => {
|
|||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const typeMapGet = (parent, key) => {
|
export const typeMapGet = (parent, key) => {
|
||||||
parent.doc ?? warnPrematureAccess()
|
|
||||||
const val = parent._map.get(key)
|
const val = parent._map.get(key)
|
||||||
return val !== undefined && !val.deleted ? val.content.getContent()[val.length - 1] : undefined
|
return val !== undefined && !val.deleted ? val.content.getContent()[val.length - 1] : undefined
|
||||||
}
|
}
|
||||||
@ -902,7 +887,6 @@ export const typeMapGetAll = (parent) => {
|
|||||||
* @type {Object<string,any>}
|
* @type {Object<string,any>}
|
||||||
*/
|
*/
|
||||||
const res = {}
|
const res = {}
|
||||||
parent.doc ?? warnPrematureAccess()
|
|
||||||
parent._map.forEach((value, key) => {
|
parent._map.forEach((value, key) => {
|
||||||
if (!value.deleted) {
|
if (!value.deleted) {
|
||||||
res[key] = value.content.getContent()[value.length - 1]
|
res[key] = value.content.getContent()[value.length - 1]
|
||||||
@ -920,7 +904,6 @@ export const typeMapGetAll = (parent) => {
|
|||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const typeMapHas = (parent, key) => {
|
export const typeMapHas = (parent, key) => {
|
||||||
parent.doc ?? warnPrematureAccess()
|
|
||||||
const val = parent._map.get(key)
|
const val = parent._map.get(key)
|
||||||
return val !== undefined && !val.deleted
|
return val !== undefined && !val.deleted
|
||||||
}
|
}
|
||||||
@ -943,41 +926,10 @@ export const typeMapGetSnapshot = (parent, key, snapshot) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractType<any>} parent
|
* @param {Map<string,Item>} map
|
||||||
* @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 {AbstractType<any> & { _map: Map<string, Item> }} type
|
|
||||||
* @return {IterableIterator<Array<any>>}
|
* @return {IterableIterator<Array<any>>}
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const createMapIterator = type => {
|
export const createMapIterator = map => iterator.iteratorFilter(map.entries(), /** @param {any} entry */ entry => !entry[1].deleted)
|
||||||
type.doc ?? warnPrematureAccess()
|
|
||||||
return iterator.iteratorFilter(type._map.entries(), /** @param {any} entry */ entry => !entry[1].deleted)
|
|
||||||
}
|
|
||||||
|
@ -16,7 +16,6 @@ import {
|
|||||||
YArrayRefID,
|
YArrayRefID,
|
||||||
callTypeObservers,
|
callTypeObservers,
|
||||||
transact,
|
transact,
|
||||||
warnPrematureAccess,
|
|
||||||
ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
|
ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
import { typeListSlice } from './AbstractType.js'
|
import { typeListSlice } from './AbstractType.js'
|
||||||
@ -26,7 +25,16 @@ import { typeListSlice } from './AbstractType.js'
|
|||||||
* @template T
|
* @template T
|
||||||
* @extends YEvent<YArray<T>>
|
* @extends YEvent<YArray<T>>
|
||||||
*/
|
*/
|
||||||
export class YArrayEvent extends YEvent {}
|
export class YArrayEvent extends YEvent {
|
||||||
|
/**
|
||||||
|
* @param {YArray<T>} yarray The changed type
|
||||||
|
* @param {Transaction} transaction The transaction object
|
||||||
|
*/
|
||||||
|
constructor (yarray, transaction) {
|
||||||
|
super(yarray, transaction)
|
||||||
|
this._transaction = transaction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A shared Array implementation.
|
* A shared Array implementation.
|
||||||
@ -50,14 +58,11 @@ export class YArray extends AbstractType {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a new YArray containing the specified items.
|
* Construct a new YArray containing the specified items.
|
||||||
* @template {Object<string,any>|Array<any>|number|null|string|Uint8Array} T
|
* @template T
|
||||||
* @param {Array<T>} items
|
* @param {Array<T>} items
|
||||||
* @return {YArray<T>}
|
* @return {YArray<T>}
|
||||||
*/
|
*/
|
||||||
static from (items) {
|
static from (items) {
|
||||||
/**
|
|
||||||
* @type {YArray<T>}
|
|
||||||
*/
|
|
||||||
const a = new YArray()
|
const a = new YArray()
|
||||||
a.push(items)
|
a.push(items)
|
||||||
return a
|
return a
|
||||||
@ -79,34 +84,23 @@ export class YArray extends AbstractType {
|
|||||||
this._prelimContent = null
|
this._prelimContent = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {YArray<T>}
|
|
||||||
*/
|
|
||||||
_copy () {
|
_copy () {
|
||||||
return new YArray()
|
return new YArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes a copy of this data type that can be included somewhere else.
|
|
||||||
*
|
|
||||||
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
|
|
||||||
*
|
|
||||||
* @return {YArray<T>}
|
* @return {YArray<T>}
|
||||||
*/
|
*/
|
||||||
clone () {
|
clone () {
|
||||||
/**
|
|
||||||
* @type {YArray<T>}
|
|
||||||
*/
|
|
||||||
const arr = new YArray()
|
const arr = new YArray()
|
||||||
arr.insert(0, this.toArray().map(el =>
|
arr.insert(0, this.toArray().map(el =>
|
||||||
el instanceof AbstractType ? /** @type {typeof el} */ (el.clone()) : el
|
el instanceof AbstractType ? el.clone() : el
|
||||||
))
|
))
|
||||||
return arr
|
return arr
|
||||||
}
|
}
|
||||||
|
|
||||||
get length () {
|
get length () {
|
||||||
this.doc ?? warnPrematureAccess()
|
return this._prelimContent === null ? this._length : this._prelimContent.length
|
||||||
return this._length
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -139,7 +133,7 @@ export class YArray extends AbstractType {
|
|||||||
insert (index, content) {
|
insert (index, content) {
|
||||||
if (this.doc !== null) {
|
if (this.doc !== null) {
|
||||||
transact(this.doc, transaction => {
|
transact(this.doc, transaction => {
|
||||||
typeListInsertGenerics(transaction, this, index, /** @type {any} */ (content))
|
typeListInsertGenerics(transaction, this, index, content)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
/** @type {Array<any>} */ (this._prelimContent).splice(index, 0, ...content)
|
/** @type {Array<any>} */ (this._prelimContent).splice(index, 0, ...content)
|
||||||
@ -156,7 +150,7 @@ export class YArray extends AbstractType {
|
|||||||
push (content) {
|
push (content) {
|
||||||
if (this.doc !== null) {
|
if (this.doc !== null) {
|
||||||
transact(this.doc, transaction => {
|
transact(this.doc, transaction => {
|
||||||
typeListPushGenerics(transaction, this, /** @type {any} */ (content))
|
typeListPushGenerics(transaction, this, content)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
/** @type {Array<any>} */ (this._prelimContent).push(...content)
|
/** @type {Array<any>} */ (this._prelimContent).push(...content)
|
||||||
@ -164,9 +158,9 @@ export class YArray extends AbstractType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepends content to this YArray.
|
* Preppends content to this YArray.
|
||||||
*
|
*
|
||||||
* @param {Array<T>} content Array of content to prepend.
|
* @param {Array<T>} content Array of content to preppend.
|
||||||
*/
|
*/
|
||||||
unshift (content) {
|
unshift (content) {
|
||||||
this.insert(0, content)
|
this.insert(0, content)
|
||||||
@ -208,8 +202,7 @@ export class YArray extends AbstractType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a portion of this YArray into a JavaScript Array selected
|
* Transforms this YArray to a JavaScript Array.
|
||||||
* from start to end (end not included).
|
|
||||||
*
|
*
|
||||||
* @param {number} [start]
|
* @param {number} [start]
|
||||||
* @param {number} [end]
|
* @param {number} [end]
|
||||||
@ -242,7 +235,7 @@ export class YArray extends AbstractType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a provided function once on every element of this YArray.
|
* Executes a provided function on once on overy element of this YArray.
|
||||||
*
|
*
|
||||||
* @param {function(T,number,YArray<T>):void} f A function to execute on every element of this YArray.
|
* @param {function(T,number,YArray<T>):void} f A function to execute on every element of this YArray.
|
||||||
*/
|
*/
|
||||||
@ -266,9 +259,9 @@ export class YArray extends AbstractType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
|
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const readYArray = _decoder => new YArray()
|
export const readYArray = decoder => new YArray()
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @module YMap
|
* @module YMap
|
||||||
*/
|
*/
|
||||||
@ -13,7 +14,6 @@ import {
|
|||||||
YMapRefID,
|
YMapRefID,
|
||||||
callTypeObservers,
|
callTypeObservers,
|
||||||
transact,
|
transact,
|
||||||
warnPrematureAccess,
|
|
||||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
|
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ export class YMapEvent extends YEvent {
|
|||||||
* A shared Map implementation.
|
* A shared Map implementation.
|
||||||
*
|
*
|
||||||
* @extends AbstractType<YMapEvent<MapType>>
|
* @extends AbstractType<YMapEvent<MapType>>
|
||||||
* @implements {Iterable<[string, MapType]>}
|
* @implements {Iterable<MapType>}
|
||||||
*/
|
*/
|
||||||
export class YMap extends AbstractType {
|
export class YMap extends AbstractType {
|
||||||
/**
|
/**
|
||||||
@ -81,27 +81,17 @@ export class YMap extends AbstractType {
|
|||||||
this._prelimContent = null
|
this._prelimContent = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {YMap<MapType>}
|
|
||||||
*/
|
|
||||||
_copy () {
|
_copy () {
|
||||||
return new YMap()
|
return new YMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes a copy of this data type that can be included somewhere else.
|
|
||||||
*
|
|
||||||
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
|
|
||||||
*
|
|
||||||
* @return {YMap<MapType>}
|
* @return {YMap<MapType>}
|
||||||
*/
|
*/
|
||||||
clone () {
|
clone () {
|
||||||
/**
|
|
||||||
* @type {YMap<MapType>}
|
|
||||||
*/
|
|
||||||
const map = new YMap()
|
const map = new YMap()
|
||||||
this.forEach((value, key) => {
|
this.forEach((value, key) => {
|
||||||
map.set(key, value instanceof AbstractType ? /** @type {typeof value} */ (value.clone()) : value)
|
map.set(key, value instanceof AbstractType ? value.clone() : value)
|
||||||
})
|
})
|
||||||
return map
|
return map
|
||||||
}
|
}
|
||||||
@ -122,7 +112,6 @@ export class YMap extends AbstractType {
|
|||||||
* @return {Object<string,any>}
|
* @return {Object<string,any>}
|
||||||
*/
|
*/
|
||||||
toJSON () {
|
toJSON () {
|
||||||
this.doc ?? warnPrematureAccess()
|
|
||||||
/**
|
/**
|
||||||
* @type {Object<string,MapType>}
|
* @type {Object<string,MapType>}
|
||||||
*/
|
*/
|
||||||
@ -142,7 +131,7 @@ export class YMap extends AbstractType {
|
|||||||
* @return {number}
|
* @return {number}
|
||||||
*/
|
*/
|
||||||
get size () {
|
get size () {
|
||||||
return [...createMapIterator(this)].length
|
return [...createMapIterator(this._map)].length
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -151,25 +140,25 @@ export class YMap extends AbstractType {
|
|||||||
* @return {IterableIterator<string>}
|
* @return {IterableIterator<string>}
|
||||||
*/
|
*/
|
||||||
keys () {
|
keys () {
|
||||||
return iterator.iteratorMap(createMapIterator(this), /** @param {any} v */ v => v[0])
|
return iterator.iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => v[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the values for each element in the YMap Type.
|
* Returns the values for each element in the YMap Type.
|
||||||
*
|
*
|
||||||
* @return {IterableIterator<MapType>}
|
* @return {IterableIterator<any>}
|
||||||
*/
|
*/
|
||||||
values () {
|
values () {
|
||||||
return iterator.iteratorMap(createMapIterator(this), /** @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])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an Iterator of [key, value] pairs
|
* Returns an Iterator of [key, value] pairs
|
||||||
*
|
*
|
||||||
* @return {IterableIterator<[string, MapType]>}
|
* @return {IterableIterator<any>}
|
||||||
*/
|
*/
|
||||||
entries () {
|
entries () {
|
||||||
return iterator.iteratorMap(createMapIterator(this), /** @param {any} v */ v => /** @type {any} */ ([v[0], v[1].content.getContent()[v[1].length - 1]]))
|
return iterator.iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => [v[0], v[1].content.getContent()[v[1].length - 1]])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -178,18 +167,22 @@ export class YMap extends AbstractType {
|
|||||||
* @param {function(MapType,string,YMap<MapType>):void} f A function to execute on every element of this YArray.
|
* @param {function(MapType,string,YMap<MapType>):void} f A function to execute on every element of this YArray.
|
||||||
*/
|
*/
|
||||||
forEach (f) {
|
forEach (f) {
|
||||||
this.doc ?? warnPrematureAccess()
|
/**
|
||||||
|
* @type {Object<string,MapType>}
|
||||||
|
*/
|
||||||
|
const map = {}
|
||||||
this._map.forEach((item, key) => {
|
this._map.forEach((item, key) => {
|
||||||
if (!item.deleted) {
|
if (!item.deleted) {
|
||||||
f(item.content.getContent()[item.length - 1], key, this)
|
f(item.content.getContent()[item.length - 1], key, this)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
return map
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an Iterator of [key, value] pairs
|
* Returns an Iterator of [key, value] pairs
|
||||||
*
|
*
|
||||||
* @return {IterableIterator<[string, MapType]>}
|
* @return {IterableIterator<any>}
|
||||||
*/
|
*/
|
||||||
[Symbol.iterator] () {
|
[Symbol.iterator] () {
|
||||||
return this.entries()
|
return this.entries()
|
||||||
@ -212,16 +205,14 @@ export class YMap extends AbstractType {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds or updates an element with a specified key and value.
|
* Adds or updates an element with a specified key and value.
|
||||||
* @template {MapType} VAL
|
|
||||||
*
|
*
|
||||||
* @param {string} key The key of the element to add to this YMap
|
* @param {string} key The key of the element to add to this YMap
|
||||||
* @param {VAL} value The value of the element to add
|
* @param {MapType} value The value of the element to add
|
||||||
* @return {VAL}
|
|
||||||
*/
|
*/
|
||||||
set (key, value) {
|
set (key, value) {
|
||||||
if (this.doc !== null) {
|
if (this.doc !== null) {
|
||||||
transact(this.doc, transaction => {
|
transact(this.doc, transaction => {
|
||||||
typeMapSet(transaction, this, key, /** @type {any} */ (value))
|
typeMapSet(transaction, this, key, value)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
/** @type {Map<string, any>} */ (this._prelimContent).set(key, value)
|
/** @type {Map<string, any>} */ (this._prelimContent).set(key, value)
|
||||||
@ -255,7 +246,7 @@ export class YMap extends AbstractType {
|
|||||||
clear () {
|
clear () {
|
||||||
if (this.doc !== null) {
|
if (this.doc !== null) {
|
||||||
transact(this.doc, transaction => {
|
transact(this.doc, transaction => {
|
||||||
this.forEach(function (_value, key, map) {
|
this.forEach(function (value, key, map) {
|
||||||
typeMapDelete(transaction, map, key)
|
typeMapDelete(transaction, map, key)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -273,9 +264,9 @@ export class YMap extends AbstractType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
|
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const readYMap = _decoder => new YMap()
|
export const readYMap = decoder => new YMap()
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @module YText
|
* @module YText
|
||||||
*/
|
*/
|
||||||
@ -26,7 +27,6 @@ import {
|
|||||||
typeMapGetAll,
|
typeMapGetAll,
|
||||||
updateMarkerChanges,
|
updateMarkerChanges,
|
||||||
ContentType,
|
ContentType,
|
||||||
warnPrematureAccess,
|
|
||||||
ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction // eslint-disable-line
|
ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
@ -118,15 +118,14 @@ 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, useSearchMarker) => {
|
const findPosition = (transaction, parent, index) => {
|
||||||
const currentAttributes = new Map()
|
const currentAttributes = new Map()
|
||||||
const marker = useSearchMarker ? findMarker(parent, index) : null
|
const marker = findMarker(parent, index)
|
||||||
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)
|
||||||
@ -202,7 +201,7 @@ const minimizeAttributeChanges = (currPos, attributes) => {
|
|||||||
while (true) {
|
while (true) {
|
||||||
if (currPos.right === null) {
|
if (currPos.right === null) {
|
||||||
break
|
break
|
||||||
} else if (currPos.right.deleted || (currPos.right.content.constructor === ContentFormat && equalAttrs(attributes[(/** @type {ContentFormat} */ (currPos.right.content)).key] ?? null, /** @type {ContentFormat} */ (currPos.right.content).value))) {
|
} else if (currPos.right.deleted || (currPos.right.content.constructor === ContentFormat && equalAttrs(attributes[(/** @type {ContentFormat} */ (currPos.right.content)).key] || null, /** @type {ContentFormat} */ (currPos.right.content).value))) {
|
||||||
//
|
//
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
@ -228,7 +227,7 @@ const insertAttributes = (transaction, parent, currPos, attributes) => {
|
|||||||
// insert format-start items
|
// insert format-start items
|
||||||
for (const key in attributes) {
|
for (const key in attributes) {
|
||||||
const val = attributes[key]
|
const val = attributes[key]
|
||||||
const currentVal = currPos.currentAttributes.get(key) ?? null
|
const currentVal = currPos.currentAttributes.get(key) || null
|
||||||
if (!equalAttrs(currentVal, val)) {
|
if (!equalAttrs(currentVal, val)) {
|
||||||
// save negated attribute (set null if currentVal undefined)
|
// save negated attribute (set null if currentVal undefined)
|
||||||
negatedAttributes.set(key, currentVal)
|
negatedAttributes.set(key, currentVal)
|
||||||
@ -252,7 +251,7 @@ const insertAttributes = (transaction, parent, currPos, attributes) => {
|
|||||||
* @function
|
* @function
|
||||||
**/
|
**/
|
||||||
const insertText = (transaction, parent, currPos, text, attributes) => {
|
const insertText = (transaction, parent, currPos, text, attributes) => {
|
||||||
currPos.currentAttributes.forEach((_val, key) => {
|
currPos.currentAttributes.forEach((val, key) => {
|
||||||
if (attributes[key] === undefined) {
|
if (attributes[key] === undefined) {
|
||||||
attributes[key] = null
|
attributes[key] = null
|
||||||
}
|
}
|
||||||
@ -364,48 +363,33 @@ const formatText = (transaction, parent, currPos, length, attributes) => {
|
|||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAttributes) => {
|
const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAttributes) => {
|
||||||
/**
|
let end = curr
|
||||||
* @type {Item|null}
|
const endAttributes = map.copy(currAttributes)
|
||||||
*/
|
|
||||||
let end = start
|
|
||||||
/**
|
|
||||||
* @type {Map<string,ContentFormat>}
|
|
||||||
*/
|
|
||||||
const endFormats = map.create()
|
|
||||||
while (end && (!end.countable || end.deleted)) {
|
while (end && (!end.countable || end.deleted)) {
|
||||||
if (!end.deleted && end.content.constructor === ContentFormat) {
|
if (!end.deleted && end.content.constructor === ContentFormat) {
|
||||||
const cf = /** @type {ContentFormat} */ (end.content)
|
updateCurrentAttributes(endAttributes, /** @type {ContentFormat} */ (end.content))
|
||||||
endFormats.set(cf.key, cf)
|
|
||||||
}
|
}
|
||||||
end = end.right
|
end = end.right
|
||||||
}
|
}
|
||||||
let cleanups = 0
|
let cleanups = 0
|
||||||
let reachedCurr = false
|
let reachedEndOfCurr = false
|
||||||
while (start !== end) {
|
while (start !== end) {
|
||||||
if (curr === start) {
|
if (curr === start) {
|
||||||
reachedCurr = true
|
reachedEndOfCurr = true
|
||||||
}
|
}
|
||||||
if (!start.deleted) {
|
if (!start.deleted) {
|
||||||
const content = start.content
|
const content = start.content
|
||||||
switch (content.constructor) {
|
switch (content.constructor) {
|
||||||
case ContentFormat: {
|
case ContentFormat: {
|
||||||
const { key, value } = /** @type {ContentFormat} */ (content)
|
const { key, value } = /** @type {ContentFormat} */ (content)
|
||||||
const startAttrValue = startAttributes.get(key) ?? null
|
if ((endAttributes.get(key) || null) !== value || (startAttributes.get(key) || null) === value) {
|
||||||
if (endFormats.get(key) !== content || startAttrValue === value) {
|
|
||||||
// Either this format is overwritten or it is not necessary because the attribute already existed.
|
// Either this format is overwritten or it is not necessary because the attribute already existed.
|
||||||
start.delete(transaction)
|
start.delete(transaction)
|
||||||
cleanups++
|
cleanups++
|
||||||
if (!reachedCurr && (currAttributes.get(key) ?? null) === value && startAttrValue !== value) {
|
if (!reachedEndOfCurr && (currAttributes.get(key) || null) === value && (startAttributes.get(key) || null) !== value) {
|
||||||
if (startAttrValue === null) {
|
|
||||||
currAttributes.delete(key)
|
currAttributes.delete(key)
|
||||||
} else {
|
|
||||||
currAttributes.set(key, startAttrValue)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (!reachedCurr && !start.deleted) {
|
|
||||||
updateCurrentAttributes(currAttributes, /** @type {ContentFormat} */ (content))
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -477,56 +461,6 @@ export const cleanupYTextFormatting = type => {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This will be called by the transaction 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
|
||||||
@ -568,7 +502,7 @@ const deleteText = (transaction, currPos, length) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The Quill Delta format represents changes on a text document with
|
* The Quill Delta format represents changes on a text document with
|
||||||
* formatting information. For more information visit {@link https://quilljs.com/docs/delta/|Quill Delta}
|
* formatting information. For mor information visit {@link https://quilljs.com/docs/delta/|Quill Delta}
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* {
|
* {
|
||||||
@ -682,16 +616,13 @@ export class YTextEvent extends YEvent {
|
|||||||
/**
|
/**
|
||||||
* @type {any}
|
* @type {any}
|
||||||
*/
|
*/
|
||||||
let op = null
|
let op
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'delete':
|
case 'delete':
|
||||||
if (deleteLen > 0) {
|
|
||||||
op = { delete: deleteLen }
|
op = { delete: deleteLen }
|
||||||
}
|
|
||||||
deleteLen = 0
|
deleteLen = 0
|
||||||
break
|
break
|
||||||
case 'insert':
|
case 'insert':
|
||||||
if (typeof insert === 'object' || insert.length > 0) {
|
|
||||||
op = { insert }
|
op = { insert }
|
||||||
if (currentAttributes.size > 0) {
|
if (currentAttributes.size > 0) {
|
||||||
op.attributes = {}
|
op.attributes = {}
|
||||||
@ -701,20 +632,20 @@ export class YTextEvent extends YEvent {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
insert = ''
|
insert = ''
|
||||||
break
|
break
|
||||||
case 'retain':
|
case 'retain':
|
||||||
if (retain > 0) {
|
|
||||||
op = { retain }
|
op = { retain }
|
||||||
if (!object.isEmpty(attributes)) {
|
if (Object.keys(attributes).length > 0) {
|
||||||
op.attributes = object.assign({}, attributes)
|
op.attributes = {}
|
||||||
|
for (const key in attributes) {
|
||||||
|
op.attributes[key] = attributes[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
retain = 0
|
retain = 0
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if (op) delta.push(op)
|
delta.push(op)
|
||||||
action = null
|
action = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -770,12 +701,12 @@ export class YTextEvent extends YEvent {
|
|||||||
const { key, value } = /** @type {ContentFormat} */ (item.content)
|
const { key, value } = /** @type {ContentFormat} */ (item.content)
|
||||||
if (this.adds(item)) {
|
if (this.adds(item)) {
|
||||||
if (!this.deletes(item)) {
|
if (!this.deletes(item)) {
|
||||||
const curVal = currentAttributes.get(key) ?? null
|
const curVal = currentAttributes.get(key) || null
|
||||||
if (!equalAttrs(curVal, value)) {
|
if (!equalAttrs(curVal, value)) {
|
||||||
if (action === 'retain') {
|
if (action === 'retain') {
|
||||||
addOp()
|
addOp()
|
||||||
}
|
}
|
||||||
if (equalAttrs(value, (oldAttributes.get(key) ?? null))) {
|
if (equalAttrs(value, (oldAttributes.get(key) || null))) {
|
||||||
delete attributes[key]
|
delete attributes[key]
|
||||||
} else {
|
} else {
|
||||||
attributes[key] = value
|
attributes[key] = value
|
||||||
@ -786,7 +717,7 @@ export class YTextEvent extends YEvent {
|
|||||||
}
|
}
|
||||||
} else if (this.deletes(item)) {
|
} else if (this.deletes(item)) {
|
||||||
oldAttributes.set(key, value)
|
oldAttributes.set(key, value)
|
||||||
const curVal = currentAttributes.get(key) ?? null
|
const curVal = currentAttributes.get(key) || null
|
||||||
if (!equalAttrs(curVal, value)) {
|
if (!equalAttrs(curVal, value)) {
|
||||||
if (action === 'retain') {
|
if (action === 'retain') {
|
||||||
addOp()
|
addOp()
|
||||||
@ -860,14 +791,9 @@ export class YText extends AbstractType {
|
|||||||
*/
|
*/
|
||||||
this._pending = string !== undefined ? [() => this.insert(0, string)] : []
|
this._pending = string !== undefined ? [() => this.insert(0, string)] : []
|
||||||
/**
|
/**
|
||||||
* @type {Array<ArraySearchMarker>|null}
|
* @type {Array<ArraySearchMarker>}
|
||||||
*/
|
*/
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -876,7 +802,6 @@ export class YText extends AbstractType {
|
|||||||
* @type {number}
|
* @type {number}
|
||||||
*/
|
*/
|
||||||
get length () {
|
get length () {
|
||||||
this.doc ?? warnPrematureAccess()
|
|
||||||
return this._length
|
return this._length
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -899,10 +824,6 @@ export class YText extends AbstractType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes a copy of this data type that can be included somewhere else.
|
|
||||||
*
|
|
||||||
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
|
|
||||||
*
|
|
||||||
* @return {YText}
|
* @return {YText}
|
||||||
*/
|
*/
|
||||||
clone () {
|
clone () {
|
||||||
@ -920,10 +841,55 @@ 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 && this._hasFormatting) {
|
if (!transaction.local) {
|
||||||
transaction._needFormattingCleanup = true
|
// check if another formatting item was inserted
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -933,7 +899,6 @@ export class YText extends AbstractType {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
toString () {
|
toString () {
|
||||||
this.doc ?? warnPrematureAccess()
|
|
||||||
let str = ''
|
let str = ''
|
||||||
/**
|
/**
|
||||||
* @type {Item|null}
|
* @type {Item|null}
|
||||||
@ -961,8 +926,8 @@ export class YText extends AbstractType {
|
|||||||
/**
|
/**
|
||||||
* Apply a {@link Delta} on this shared YText type.
|
* Apply a {@link Delta} on this shared YText type.
|
||||||
*
|
*
|
||||||
* @param {Array<any>} delta The changes to apply on this element.
|
* @param {any} delta The changes to apply on this element.
|
||||||
* @param {object} opts
|
* @param {object} [opts]
|
||||||
* @param {boolean} [opts.sanitize] Sanitize input delta. Removes ending newlines if set to true.
|
* @param {boolean} [opts.sanitize] Sanitize input delta. Removes ending newlines if set to true.
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
@ -1007,7 +972,6 @@ export class YText extends AbstractType {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
toDelta (snapshot, prevSnapshot, computeYChange) {
|
toDelta (snapshot, prevSnapshot, computeYChange) {
|
||||||
this.doc ?? warnPrematureAccess()
|
|
||||||
/**
|
/**
|
||||||
* @type{Array<any>}
|
* @type{Array<any>}
|
||||||
*/
|
*/
|
||||||
@ -1039,19 +1003,27 @@ export class YText extends AbstractType {
|
|||||||
str = ''
|
str = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const computeDelta = () => {
|
// snapshots are merged again after the transaction, so we need to keep the
|
||||||
|
// transalive until we are done
|
||||||
|
transact(doc, transaction => {
|
||||||
|
if (snapshot) {
|
||||||
|
splitSnapshotAffectedStructs(transaction, snapshot)
|
||||||
|
}
|
||||||
|
if (prevSnapshot) {
|
||||||
|
splitSnapshotAffectedStructs(transaction, prevSnapshot)
|
||||||
|
}
|
||||||
while (n !== null) {
|
while (n !== null) {
|
||||||
if (isVisible(n, snapshot) || (prevSnapshot !== undefined && isVisible(n, prevSnapshot))) {
|
if (isVisible(n, snapshot) || (prevSnapshot !== undefined && isVisible(n, prevSnapshot))) {
|
||||||
switch (n.content.constructor) {
|
switch (n.content.constructor) {
|
||||||
case ContentString: {
|
case ContentString: {
|
||||||
const cur = currentAttributes.get('ychange')
|
const cur = currentAttributes.get('ychange')
|
||||||
if (snapshot !== undefined && !isVisible(n, snapshot)) {
|
if (snapshot !== undefined && !isVisible(n, snapshot)) {
|
||||||
if (cur === undefined || cur.user !== n.id.client || cur.type !== 'removed') {
|
if (cur === undefined || cur.user !== n.id.client || cur.state !== 'removed') {
|
||||||
packStr()
|
packStr()
|
||||||
currentAttributes.set('ychange', computeYChange ? computeYChange('removed', n.id) : { type: 'removed' })
|
currentAttributes.set('ychange', computeYChange ? computeYChange('removed', n.id) : { type: 'removed' })
|
||||||
}
|
}
|
||||||
} else if (prevSnapshot !== undefined && !isVisible(n, prevSnapshot)) {
|
} else if (prevSnapshot !== undefined && !isVisible(n, prevSnapshot)) {
|
||||||
if (cur === undefined || cur.user !== n.id.client || cur.type !== 'added') {
|
if (cur === undefined || cur.user !== n.id.client || cur.state !== 'added') {
|
||||||
packStr()
|
packStr()
|
||||||
currentAttributes.set('ychange', computeYChange ? computeYChange('added', n.id) : { type: 'added' })
|
currentAttributes.set('ychange', computeYChange ? computeYChange('added', n.id) : { type: 'added' })
|
||||||
}
|
}
|
||||||
@ -1092,22 +1064,7 @@ export class YText extends AbstractType {
|
|||||||
n = n.right
|
n = n.right
|
||||||
}
|
}
|
||||||
packStr()
|
packStr()
|
||||||
}
|
}, splitSnapshotAffectedStructs)
|
||||||
if (snapshot || prevSnapshot) {
|
|
||||||
// snapshots are merged again after the transaction, so we need to keep the
|
|
||||||
// transaction alive until we are done
|
|
||||||
transact(doc, transaction => {
|
|
||||||
if (snapshot) {
|
|
||||||
splitSnapshotAffectedStructs(transaction, snapshot)
|
|
||||||
}
|
|
||||||
if (prevSnapshot) {
|
|
||||||
splitSnapshotAffectedStructs(transaction, prevSnapshot)
|
|
||||||
}
|
|
||||||
computeDelta()
|
|
||||||
}, 'cleanup')
|
|
||||||
} else {
|
|
||||||
computeDelta()
|
|
||||||
}
|
|
||||||
return ops
|
return ops
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1128,7 +1085,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, !attributes)
|
const pos = findPosition(transaction, this, index)
|
||||||
if (!attributes) {
|
if (!attributes) {
|
||||||
attributes = {}
|
attributes = {}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -1146,20 +1103,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, !attributes)
|
const pos = findPosition(transaction, this, index)
|
||||||
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1178,7 +1135,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, true), length)
|
deleteText(transaction, findPosition(transaction, this, index), length)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
/** @type {Array<function>} */ (this._pending).push(() => this.delete(index, length))
|
/** @type {Array<function>} */ (this._pending).push(() => this.delete(index, length))
|
||||||
@ -1202,7 +1159,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, false)
|
const pos = findPosition(transaction, this, index)
|
||||||
if (pos.right === null) {
|
if (pos.right === null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1272,11 +1229,12 @@ export class YText extends AbstractType {
|
|||||||
*
|
*
|
||||||
* @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks.
|
* @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks.
|
||||||
*
|
*
|
||||||
|
* @param {Snapshot} [snapshot]
|
||||||
* @return {Object<string, any>} A JSON Object that describes the attributes.
|
* @return {Object<string, any>} A JSON Object that describes the attributes.
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
getAttributes () {
|
getAttributes (snapshot) {
|
||||||
return typeMapGetAll(this)
|
return typeMapGetAll(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1289,10 +1247,10 @@ export class YText extends AbstractType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
|
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||||
* @return {YText}
|
* @return {YText}
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const readYText = _decoder => new YText()
|
export const readYText = decoder => new YText()
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import * as object from 'lib0/object'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
YXmlFragment,
|
YXmlFragment,
|
||||||
@ -8,24 +7,17 @@ import {
|
|||||||
typeMapSet,
|
typeMapSet,
|
||||||
typeMapGet,
|
typeMapGet,
|
||||||
typeMapGetAll,
|
typeMapGetAll,
|
||||||
typeMapGetAllSnapshot,
|
|
||||||
typeListForEach,
|
typeListForEach,
|
||||||
YXmlElementRefID,
|
YXmlElementRefID,
|
||||||
Snapshot, YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item // eslint-disable-line
|
YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Snapshot, Doc, Item // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object|number|null|Array<any>|string|Uint8Array|AbstractType<any>} ValueTypes
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An YXmlElement imitates the behavior of a
|
* An YXmlElement imitates the behavior of a
|
||||||
* https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element
|
* {@link 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
|
||||||
*
|
|
||||||
* @template {{ [key: string]: ValueTypes }} [KV={ [key: string]: string }]
|
|
||||||
*/
|
*/
|
||||||
export class YXmlElement extends YXmlFragment {
|
export class YXmlElement extends YXmlFragment {
|
||||||
constructor (nodeName = 'UNDEFINED') {
|
constructor (nodeName = 'UNDEFINED') {
|
||||||
@ -81,23 +73,14 @@ export class YXmlElement extends YXmlFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes a copy of this data type that can be included somewhere else.
|
* @return {YXmlElement}
|
||||||
*
|
|
||||||
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
|
|
||||||
*
|
|
||||||
* @return {YXmlElement<KV>}
|
|
||||||
*/
|
*/
|
||||||
clone () {
|
clone () {
|
||||||
/**
|
|
||||||
* @type {YXmlElement<KV>}
|
|
||||||
*/
|
|
||||||
const el = new YXmlElement(this.nodeName)
|
const el = new YXmlElement(this.nodeName)
|
||||||
const attrs = this.getAttributes()
|
const attrs = this.getAttributes()
|
||||||
object.forEach(attrs, (value, key) => {
|
for (const key in attrs) {
|
||||||
if (typeof value === 'string') {
|
el.setAttribute(key, attrs[key])
|
||||||
el.setAttribute(key, value)
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
el.insert(0, this.toArray().map(item => item instanceof AbstractType ? item.clone() : item))
|
el.insert(0, this.toArray().map(item => item instanceof AbstractType ? item.clone() : item))
|
||||||
return el
|
return el
|
||||||
@ -133,7 +116,7 @@ export class YXmlElement extends YXmlFragment {
|
|||||||
/**
|
/**
|
||||||
* Removes an attribute from this YXmlElement.
|
* Removes an attribute from this YXmlElement.
|
||||||
*
|
*
|
||||||
* @param {string} attributeName The attribute name that is to be removed.
|
* @param {String} attributeName The attribute name that is to be removed.
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@ -150,10 +133,8 @@ export class YXmlElement extends YXmlFragment {
|
|||||||
/**
|
/**
|
||||||
* Sets or updates an attribute.
|
* Sets or updates an attribute.
|
||||||
*
|
*
|
||||||
* @template {keyof KV & string} KEY
|
* @param {String} attributeName The attribute name that is to be set.
|
||||||
*
|
* @param {String} attributeValue The attribute value that is to be set.
|
||||||
* @param {KEY} attributeName The attribute name that is to be set.
|
|
||||||
* @param {KV[KEY]} attributeValue The attribute value that is to be set.
|
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@ -170,11 +151,9 @@ export class YXmlElement extends YXmlFragment {
|
|||||||
/**
|
/**
|
||||||
* Returns an attribute value that belongs to the attribute name.
|
* Returns an attribute value that belongs to the attribute name.
|
||||||
*
|
*
|
||||||
* @template {keyof KV & string} KEY
|
* @param {String} attributeName The attribute name that identifies the
|
||||||
*
|
|
||||||
* @param {KEY} attributeName The attribute name that identifies the
|
|
||||||
* queried value.
|
* queried value.
|
||||||
* @return {KV[KEY]|undefined} The queried attribute value.
|
* @return {String} The queried attribute value.
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@ -185,7 +164,7 @@ export class YXmlElement extends YXmlFragment {
|
|||||||
/**
|
/**
|
||||||
* Returns whether an attribute exists
|
* Returns whether an attribute exists
|
||||||
*
|
*
|
||||||
* @param {string} attributeName The attribute name to check for existence.
|
* @param {String} attributeName The attribute name to check for existence.
|
||||||
* @return {boolean} whether the attribute exists.
|
* @return {boolean} whether the attribute exists.
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
@ -198,12 +177,12 @@ export class YXmlElement extends YXmlFragment {
|
|||||||
* Returns all attribute name/value pairs in a JSON Object.
|
* Returns all attribute name/value pairs in a JSON Object.
|
||||||
*
|
*
|
||||||
* @param {Snapshot} [snapshot]
|
* @param {Snapshot} [snapshot]
|
||||||
* @return {{ [Key in Extract<keyof KV,string>]?: KV[Key]}} A JSON Object that describes the attributes.
|
* @return {Object<string, any>} A JSON Object that describes the attributes.
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
getAttributes (snapshot) {
|
getAttributes (snapshot) {
|
||||||
return /** @type {any} */ (snapshot ? typeMapGetAllSnapshot(this, snapshot) : typeMapGetAll(this))
|
return typeMapGetAll(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -225,10 +204,7 @@ export class YXmlElement extends YXmlFragment {
|
|||||||
const dom = _document.createElement(this.nodeName)
|
const dom = _document.createElement(this.nodeName)
|
||||||
const attrs = this.getAttributes()
|
const attrs = this.getAttributes()
|
||||||
for (const key in attrs) {
|
for (const key in attrs) {
|
||||||
const value = attrs[key]
|
dom.setAttribute(key, attrs[key])
|
||||||
if (typeof value === 'string') {
|
|
||||||
dom.setAttribute(key, value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
typeListForEach(this, yxml => {
|
typeListForEach(this, yxml => {
|
||||||
dom.appendChild(yxml.toDOM(_document, hooks, binding))
|
dom.appendChild(yxml.toDOM(_document, hooks, binding))
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
YEvent,
|
YEvent,
|
||||||
YXmlText, YXmlElement, YXmlFragment, Transaction // eslint-disable-line
|
YXmlText, YXmlElement, YXmlFragment, Transaction // eslint-disable-line
|
||||||
@ -12,7 +13,7 @@ export class YXmlEvent extends YEvent {
|
|||||||
* @param {YXmlElement|YXmlText|YXmlFragment} target The target on which the event is created.
|
* @param {YXmlElement|YXmlText|YXmlFragment} target The target on which the event is created.
|
||||||
* @param {Set<string|null>} subs The set of changed attributes. `null` is included if the
|
* @param {Set<string|null>} subs The set of changed attributes. `null` is included if the
|
||||||
* child list changed.
|
* child list changed.
|
||||||
* @param {Transaction} transaction The transaction instance with which the
|
* @param {Transaction} transaction The transaction instance with wich the
|
||||||
* change was created.
|
* change was created.
|
||||||
*/
|
*/
|
||||||
constructor (target, subs, transaction) {
|
constructor (target, subs, transaction) {
|
||||||
|
@ -17,12 +17,10 @@ import {
|
|||||||
transact,
|
transact,
|
||||||
typeListGet,
|
typeListGet,
|
||||||
typeListSlice,
|
typeListSlice,
|
||||||
warnPrematureAccess,
|
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, ContentType, Transaction, Item, YXmlText, YXmlHook, Snapshot // eslint-disable-line
|
||||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, ContentType, Transaction, Item, YXmlText, YXmlHook // eslint-disable-line
|
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as error from 'lib0/error'
|
import * as error from 'lib0/error'
|
||||||
import * as array from 'lib0/array'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define the elements to which a set of CSS queries apply.
|
* Define the elements to which a set of CSS queries apply.
|
||||||
@ -67,7 +65,6 @@ export class YXmlTreeWalker {
|
|||||||
*/
|
*/
|
||||||
this._currentNode = /** @type {Item} */ (root._start)
|
this._currentNode = /** @type {Item} */ (root._start)
|
||||||
this._firstCall = true
|
this._firstCall = true
|
||||||
root.doc ?? warnPrematureAccess()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Symbol.iterator] () {
|
[Symbol.iterator] () {
|
||||||
@ -96,12 +93,8 @@ export class YXmlTreeWalker {
|
|||||||
} else {
|
} else {
|
||||||
// walk right or up in the tree
|
// walk right or up in the tree
|
||||||
while (n !== null) {
|
while (n !== null) {
|
||||||
/**
|
if (n.right !== null) {
|
||||||
* @type {Item | null}
|
n = n.right
|
||||||
*/
|
|
||||||
const nxt = n.next
|
|
||||||
if (nxt !== null) {
|
|
||||||
n = nxt
|
|
||||||
break
|
break
|
||||||
} else if (n.parent === this._root) {
|
} else if (n.parent === this._root) {
|
||||||
n = null
|
n = null
|
||||||
@ -169,10 +162,6 @@ export class YXmlFragment extends AbstractType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes a copy of this data type that can be included somewhere else.
|
|
||||||
*
|
|
||||||
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
|
|
||||||
*
|
|
||||||
* @return {YXmlFragment}
|
* @return {YXmlFragment}
|
||||||
*/
|
*/
|
||||||
clone () {
|
clone () {
|
||||||
@ -183,7 +172,6 @@ export class YXmlFragment extends AbstractType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get length () {
|
get length () {
|
||||||
this.doc ?? warnPrematureAccess()
|
|
||||||
return this._prelimContent === null ? this._length : this._prelimContent.length
|
return this._prelimContent === null ? this._length : this._prelimContent.length
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,7 +237,7 @@ export class YXmlFragment extends AbstractType {
|
|||||||
querySelectorAll (query) {
|
querySelectorAll (query) {
|
||||||
query = query.toUpperCase()
|
query = query.toUpperCase()
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return array.from(new YXmlTreeWalker(this, element => element.nodeName && element.nodeName.toUpperCase() === query))
|
return Array.from(new YXmlTreeWalker(this, element => element.nodeName && element.nodeName.toUpperCase() === query))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -387,9 +375,9 @@ export class YXmlFragment extends AbstractType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepends content to this YArray.
|
* Preppends content to this YArray.
|
||||||
*
|
*
|
||||||
* @param {Array<YXmlElement|YXmlText>} content Array of content to prepend.
|
* @param {Array<YXmlElement|YXmlText>} content Array of content to preppend.
|
||||||
*/
|
*/
|
||||||
unshift (content) {
|
unshift (content) {
|
||||||
this.insert(0, content)
|
this.insert(0, content)
|
||||||
@ -406,8 +394,7 @@ export class YXmlFragment extends AbstractType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a portion of this YXmlFragment into a JavaScript Array selected
|
* Transforms this YArray to a JavaScript Array.
|
||||||
* from start to end (end not included).
|
|
||||||
*
|
*
|
||||||
* @param {number} [start]
|
* @param {number} [start]
|
||||||
* @param {number} [end]
|
* @param {number} [end]
|
||||||
@ -418,9 +405,9 @@ export class YXmlFragment extends AbstractType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a provided function on once on every child element.
|
* Executes a provided function on once on overy child element.
|
||||||
*
|
*
|
||||||
* @param {function(YXmlElement|YXmlText,number, typeof self):void} f A function to execute on every element of this YArray.
|
* @param {function(YXmlElement|YXmlText,number, typeof this):void} f A function to execute on every element of this YArray.
|
||||||
*/
|
*/
|
||||||
forEach (f) {
|
forEach (f) {
|
||||||
typeListForEach(this, f)
|
typeListForEach(this, f)
|
||||||
@ -440,10 +427,10 @@ export class YXmlFragment extends AbstractType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
|
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||||
* @return {YXmlFragment}
|
* @return {YXmlFragment}
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const readYXmlFragment = _decoder => new YXmlFragment()
|
export const readYXmlFragment = decoder => new YXmlFragment()
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
YMap,
|
YMap,
|
||||||
YXmlHookRefID,
|
YXmlHookRefID,
|
||||||
@ -29,10 +30,6 @@ export class YXmlHook extends YMap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes a copy of this data type that can be included somewhere else.
|
|
||||||
*
|
|
||||||
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
|
|
||||||
*
|
|
||||||
* @return {YXmlHook}
|
* @return {YXmlHook}
|
||||||
*/
|
*/
|
||||||
clone () {
|
clone () {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
YText,
|
YText,
|
||||||
YXmlTextRefID,
|
YXmlTextRefID,
|
||||||
@ -30,10 +31,6 @@ export class YXmlText extends YText {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes a copy of this data type that can be included somewhere else.
|
|
||||||
*
|
|
||||||
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
|
|
||||||
*
|
|
||||||
* @return {YXmlText}
|
* @return {YXmlText}
|
||||||
*/
|
*/
|
||||||
clone () {
|
clone () {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ObservableV2 } from 'lib0/observable'
|
|
||||||
|
import { Observable } from 'lib0/observable'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Doc // eslint-disable-line
|
Doc // eslint-disable-line
|
||||||
@ -10,9 +11,9 @@ import {
|
|||||||
* @note This interface is experimental and it is not advised to actually inherit this class.
|
* @note This interface is experimental and it is not advised to actually inherit this class.
|
||||||
* It just serves as typing information.
|
* It just serves as typing information.
|
||||||
*
|
*
|
||||||
* @extends {ObservableV2<any>}
|
* @extends {Observable<any>}
|
||||||
*/
|
*/
|
||||||
export class AbstractConnector extends ObservableV2 {
|
export class AbstractConnector extends Observable {
|
||||||
/**
|
/**
|
||||||
* @param {Doc} ydoc
|
* @param {Doc} ydoc
|
||||||
* @param {any} awareness
|
* @param {any} awareness
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
findIndexSS,
|
findIndexSS,
|
||||||
getState,
|
getState,
|
||||||
@ -170,7 +171,7 @@ export const mergeDeleteSets = dss => {
|
|||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const addToDeleteSet = (ds, client, clock, length) => {
|
export const addToDeleteSet = (ds, client, clock, length) => {
|
||||||
map.setIfUndefined(ds.clients, client, () => /** @type {Array<DeleteItem>} */ ([])).push(new DeleteItem(clock, length))
|
map.setIfUndefined(ds.clients, client, () => []).push(new DeleteItem(clock, length))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createDeleteSet = () => new DeleteSet()
|
export const createDeleteSet = () => new DeleteSet()
|
||||||
@ -218,11 +219,7 @@ export const createDeleteSetFromStructStore = ss => {
|
|||||||
*/
|
*/
|
||||||
export const writeDeleteSet = (encoder, ds) => {
|
export const writeDeleteSet = (encoder, ds) => {
|
||||||
encoding.writeVarUint(encoder.restEncoder, ds.clients.size)
|
encoding.writeVarUint(encoder.restEncoder, ds.clients.size)
|
||||||
|
ds.clients.forEach((dsitems, client) => {
|
||||||
// Ensure that the delete set is written in a deterministic order
|
|
||||||
array.from(ds.clients.entries())
|
|
||||||
.sort((a, b) => b[0] - a[0])
|
|
||||||
.forEach(([client, dsitems]) => {
|
|
||||||
encoder.resetDsCurVal()
|
encoder.resetDsCurVal()
|
||||||
encoding.writeVarUint(encoder.restEncoder, client)
|
encoding.writeVarUint(encoder.restEncoder, client)
|
||||||
const len = dsitems.length
|
const len = dsitems.length
|
||||||
@ -250,7 +247,7 @@ export const readDeleteSet = decoder => {
|
|||||||
const client = decoding.readVarUint(decoder.restDecoder)
|
const client = decoding.readVarUint(decoder.restDecoder)
|
||||||
const numberOfDeletes = decoding.readVarUint(decoder.restDecoder)
|
const numberOfDeletes = decoding.readVarUint(decoder.restDecoder)
|
||||||
if (numberOfDeletes > 0) {
|
if (numberOfDeletes > 0) {
|
||||||
const dsField = map.setIfUndefined(ds.clients, client, () => /** @type {Array<DeleteItem>} */ ([]))
|
const dsField = map.setIfUndefined(ds.clients, client, () => [])
|
||||||
for (let i = 0; i < numberOfDeletes; i++) {
|
for (let i = 0; i < numberOfDeletes; i++) {
|
||||||
dsField.push(new DeleteItem(decoder.readDsClock(), decoder.readDsLen()))
|
dsField.push(new DeleteItem(decoder.readDsClock(), decoder.readDsLen()))
|
||||||
}
|
}
|
||||||
@ -327,23 +324,3 @@ 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
|
|
||||||
}
|
|
||||||
|
141
src/utils/Doc.js
141
src/utils/Doc.js
@ -8,13 +8,12 @@ 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
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import { ObservableV2 } from 'lib0/observable'
|
import { Observable } from 'lib0/observable'
|
||||||
import * as random from 'lib0/random'
|
import * as random from 'lib0/random'
|
||||||
import * as map from 'lib0/map'
|
import * as map from 'lib0/map'
|
||||||
import * as array from 'lib0/array'
|
import * as array from 'lib0/array'
|
||||||
@ -33,29 +32,13 @@ export const generateNewClientId = random.uint32
|
|||||||
* @property {boolean} [DocOpts.shouldLoad] Whether the document should be synced by the provider now. This is toggled to true when you call ydoc.load()
|
* @property {boolean} [DocOpts.shouldLoad] Whether the document should be synced by the provider now. This is toggled to true when you call ydoc.load()
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} DocEvents
|
|
||||||
* @property {function(Doc):void} DocEvents.destroy
|
|
||||||
* @property {function(Doc):void} DocEvents.load
|
|
||||||
* @property {function(boolean, Doc):void} DocEvents.sync
|
|
||||||
* @property {function(Uint8Array, any, Doc, Transaction):void} DocEvents.update
|
|
||||||
* @property {function(Uint8Array, any, Doc, Transaction):void} DocEvents.updateV2
|
|
||||||
* @property {function(Doc):void} DocEvents.beforeAllTransactions
|
|
||||||
* @property {function(Transaction, Doc):void} DocEvents.beforeTransaction
|
|
||||||
* @property {function(Transaction, Doc):void} DocEvents.beforeObserverCalls
|
|
||||||
* @property {function(Transaction, Doc):void} DocEvents.afterTransaction
|
|
||||||
* @property {function(Transaction, Doc):void} DocEvents.afterTransactionCleanup
|
|
||||||
* @property {function(Doc, Array<Transaction>):void} DocEvents.afterAllTransactions
|
|
||||||
* @property {function({ loaded: Set<Doc>, added: Set<Doc>, removed: Set<Doc> }, Doc, Transaction):void} DocEvents.subdocs
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Yjs instance handles the state of shared data.
|
* A Yjs instance handles the state of shared data.
|
||||||
* @extends ObservableV2<DocEvents>
|
* @extends Observable<string>
|
||||||
*/
|
*/
|
||||||
export class Doc extends ObservableV2 {
|
export class Doc extends Observable {
|
||||||
/**
|
/**
|
||||||
* @param {DocOpts} opts configuration
|
* @param {DocOpts} [opts] configuration
|
||||||
*/
|
*/
|
||||||
constructor ({ guid = random.uuidv4(), collectionid = null, gc = true, gcFilter = () => true, meta = null, autoLoad = false, shouldLoad = true } = {}) {
|
constructor ({ guid = random.uuidv4(), collectionid = null, gc = true, gcFilter = () => true, meta = null, autoLoad = false, shouldLoad = true } = {}) {
|
||||||
super()
|
super()
|
||||||
@ -89,58 +72,13 @@ export class Doc extends ObservableV2 {
|
|||||||
this.shouldLoad = shouldLoad
|
this.shouldLoad = shouldLoad
|
||||||
this.autoLoad = autoLoad
|
this.autoLoad = autoLoad
|
||||||
this.meta = meta
|
this.meta = meta
|
||||||
/**
|
|
||||||
* This is set to true when the persistence provider loaded the document from the database or when the `sync` event fires.
|
|
||||||
* Note that not all providers implement this feature. Provider authors are encouraged to fire the `load` event when the doc content is loaded from the database.
|
|
||||||
*
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
this.isLoaded = false
|
this.isLoaded = false
|
||||||
/**
|
|
||||||
* This is set to true when the connection provider has successfully synced with a backend.
|
|
||||||
* Note that when using peer-to-peer providers this event may not provide very useful.
|
|
||||||
* Also note that not all providers implement this feature. Provider authors are encouraged to fire
|
|
||||||
* the `sync` event when the doc has been synced (with `true` as a parameter) or if connection is
|
|
||||||
* lost (with false as a parameter).
|
|
||||||
*/
|
|
||||||
this.isSynced = false
|
|
||||||
this.isDestroyed = false
|
|
||||||
/**
|
|
||||||
* Promise that resolves once the document has been loaded from a persistence provider.
|
|
||||||
*/
|
|
||||||
this.whenLoaded = promise.create(resolve => {
|
this.whenLoaded = promise.create(resolve => {
|
||||||
this.on('load', () => {
|
this.on('load', () => {
|
||||||
this.isLoaded = true
|
this.isLoaded = true
|
||||||
resolve(this)
|
resolve(this)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
const provideSyncedPromise = () => promise.create(resolve => {
|
|
||||||
/**
|
|
||||||
* @param {boolean} isSynced
|
|
||||||
*/
|
|
||||||
const eventHandler = (isSynced) => {
|
|
||||||
if (isSynced === undefined || isSynced === true) {
|
|
||||||
this.off('sync', eventHandler)
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.on('sync', eventHandler)
|
|
||||||
})
|
|
||||||
this.on('sync', isSynced => {
|
|
||||||
if (isSynced === false && this.isSynced) {
|
|
||||||
this.whenSynced = provideSyncedPromise()
|
|
||||||
}
|
|
||||||
this.isSynced = isSynced === undefined || isSynced === true
|
|
||||||
if (this.isSynced && !this.isLoaded) {
|
|
||||||
this.emit('load', [this])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
/**
|
|
||||||
* Promise that resolves once the document has been synced with a backend.
|
|
||||||
* This promise is recreated when the connection is lost.
|
|
||||||
* Note the documentation about the `isSynced` property.
|
|
||||||
*/
|
|
||||||
this.whenSynced = provideSyncedPromise()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -165,7 +103,7 @@ export class Doc extends ObservableV2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSubdocGuids () {
|
getSubdocGuids () {
|
||||||
return new Set(array.from(this.subdocs).map(doc => doc.guid))
|
return new Set(Array.from(this.subdocs).map(doc => doc.guid))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -174,45 +112,42 @@ export class Doc extends ObservableV2 {
|
|||||||
* that happened inside of the transaction are sent as one message to the
|
* that happened inside of the transaction are sent as one message to the
|
||||||
* other peers.
|
* other peers.
|
||||||
*
|
*
|
||||||
* @template T
|
* @param {function(Transaction):void} f The function that should be executed as a transaction
|
||||||
* @param {function(Transaction):T} f The function that should be executed as a transaction
|
|
||||||
* @param {any} [origin] Origin of who started the transaction. Will be stored on transaction.origin
|
* @param {any} [origin] Origin of who started the transaction. Will be stored on transaction.origin
|
||||||
* @return T
|
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
transact (f, origin = null) {
|
transact (f, origin = null) {
|
||||||
return transact(this, f, origin)
|
transact(this, f, origin)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a shared data type.
|
* Define a shared data type.
|
||||||
*
|
*
|
||||||
* Multiple calls of `ydoc.get(name, TypeConstructor)` yield the same result
|
* Multiple calls of `y.get(name, TypeConstructor)` yield the same result
|
||||||
* and do not overwrite each other. I.e.
|
* and do not overwrite each other. I.e.
|
||||||
* `ydoc.get(name, Y.Array) === ydoc.get(name, Y.Array)`
|
* `y.define(name, Y.Array) === y.define(name, Y.Array)`
|
||||||
*
|
*
|
||||||
* After this method is called, the type is also available on `ydoc.share.get(name)`.
|
* After this method is called, the type is also available on `y.share.get(name)`.
|
||||||
*
|
*
|
||||||
* *Best Practices:*
|
* *Best Practices:*
|
||||||
* Define all types right after the Y.Doc 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 ydoc = new Y.Doc(..)
|
* const y = new Y(..)
|
||||||
* const appState = {
|
* const appState = {
|
||||||
* document: ydoc.getText('document')
|
* document: y.getText('document')
|
||||||
* comments: ydoc.getArray('comments')
|
* comments: y.getArray('comments')
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @param {Type} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ...
|
* @param {Function} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ...
|
||||||
* @return {InstanceType<Type>} The created type. Constructed with TypeConstructor
|
* @return {AbstractType<any>} The created type. Constructed with TypeConstructor
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
get (name, TypeConstructor = /** @type {any} */ (AbstractType)) {
|
get (name, TypeConstructor = 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()
|
||||||
@ -238,12 +173,12 @@ export class Doc extends ObservableV2 {
|
|||||||
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 /** @type {InstanceType<Type>} */ (t)
|
return 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 {InstanceType<Type>} */ (type)
|
return type
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -254,7 +189,8 @@ export class Doc extends ObservableV2 {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
getArray (name = '') {
|
getArray (name = '') {
|
||||||
return /** @type {YArray<T>} */ (this.get(name, YArray))
|
// @ts-ignore
|
||||||
|
return this.get(name, YArray)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -264,6 +200,7 @@ export class Doc extends ObservableV2 {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
getText (name = '') {
|
getText (name = '') {
|
||||||
|
// @ts-ignore
|
||||||
return this.get(name, YText)
|
return this.get(name, YText)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -275,17 +212,8 @@ export class Doc extends ObservableV2 {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
getMap (name = '') {
|
getMap (name = '') {
|
||||||
return /** @type {YMap<T>} */ (this.get(name, YMap))
|
// @ts-ignore
|
||||||
}
|
return this.get(name, YMap)
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} [name]
|
|
||||||
* @return {YXmlElement}
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
getXmlElement (name = '') {
|
|
||||||
return /** @type {YXmlElement<{[key:string]:string}>} */ (this.get(name, YXmlElement))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -295,6 +223,7 @@ export class Doc extends ObservableV2 {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
getXmlFragment (name = '') {
|
getXmlFragment (name = '') {
|
||||||
|
// @ts-ignore
|
||||||
return this.get(name, YXmlFragment)
|
return this.get(name, YXmlFragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -323,7 +252,6 @@ export class Doc extends ObservableV2 {
|
|||||||
* Emit `destroy` event and unregister all event handlers.
|
* Emit `destroy` event and unregister all event handlers.
|
||||||
*/
|
*/
|
||||||
destroy () {
|
destroy () {
|
||||||
this.isDestroyed = true
|
|
||||||
array.from(this.subdocs).forEach(subdoc => subdoc.destroy())
|
array.from(this.subdocs).forEach(subdoc => subdoc.destroy())
|
||||||
const item = this._item
|
const item = this._item
|
||||||
if (item !== null) {
|
if (item !== null) {
|
||||||
@ -339,9 +267,24 @@ export class Doc extends ObservableV2 {
|
|||||||
transaction.subdocsRemoved.add(this)
|
transaction.subdocsRemoved.add(this)
|
||||||
}, null, true)
|
}, null, true)
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
this.emit('destroyed', [true])
|
||||||
this.emit('destroyed', [true]) // DEPRECATED!
|
|
||||||
this.emit('destroy', [this])
|
this.emit('destroy', [this])
|
||||||
super.destroy()
|
super.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} eventName
|
||||||
|
* @param {function(...any):any} f
|
||||||
|
*/
|
||||||
|
on (eventName, f) {
|
||||||
|
super.on(eventName, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} eventName
|
||||||
|
* @param {function} f
|
||||||
|
*/
|
||||||
|
off (eventName, f) {
|
||||||
|
super.off(eventName, f)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
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,3 +1,4 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
YArray,
|
YArray,
|
||||||
YMap,
|
YMap,
|
||||||
@ -62,7 +63,7 @@ export class PermanentUserData {
|
|||||||
initUser(storeType.get(userDescription), userDescription)
|
initUser(storeType.get(userDescription), userDescription)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
// add initial data
|
// add intial data
|
||||||
storeType.forEach(initUser)
|
storeType.forEach(initUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,7 +71,7 @@ export class PermanentUserData {
|
|||||||
* @param {Doc} doc
|
* @param {Doc} doc
|
||||||
* @param {number} clientid
|
* @param {number} clientid
|
||||||
* @param {string} userDescription
|
* @param {string} userDescription
|
||||||
* @param {Object} conf
|
* @param {Object} [conf]
|
||||||
* @param {function(Transaction, DeleteSet):boolean} [conf.filter]
|
* @param {function(Transaction, DeleteSet):boolean} [conf.filter]
|
||||||
*/
|
*/
|
||||||
setUserMapping (doc, clientid, userDescription, { filter = () => true } = {}) {
|
setUserMapping (doc, clientid, userDescription, { filter = () => true } = {}) {
|
||||||
@ -83,7 +84,7 @@ export class PermanentUserData {
|
|||||||
users.set(userDescription, user)
|
users.set(userDescription, user)
|
||||||
}
|
}
|
||||||
user.get('ids').push([clientid])
|
user.get('ids').push([clientid])
|
||||||
users.observe(_event => {
|
users.observe(event => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const userOverwrite = users.get(userDescription)
|
const userOverwrite = users.get(userDescription)
|
||||||
if (userOverwrite !== user) {
|
if (userOverwrite !== user) {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
writeID,
|
writeID,
|
||||||
readID,
|
readID,
|
||||||
@ -8,8 +9,7 @@ import {
|
|||||||
createID,
|
createID,
|
||||||
ContentType,
|
ContentType,
|
||||||
followRedone,
|
followRedone,
|
||||||
getItem,
|
ID, Doc, AbstractType // eslint-disable-line
|
||||||
StructStore, ID, Doc, AbstractType, // eslint-disable-line
|
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as encoding from 'lib0/encoding'
|
import * as encoding from 'lib0/encoding'
|
||||||
@ -66,7 +66,7 @@ export class RelativePosition {
|
|||||||
* after the meant position.
|
* after the meant position.
|
||||||
* I.e. position 1 in 'ab' is associated to character 'b'.
|
* I.e. position 1 in 'ab' is associated to character 'b'.
|
||||||
*
|
*
|
||||||
* If assoc < 0, then the relative position is associated to the character
|
* If assoc < 0, then the relative position is associated to the caharacter
|
||||||
* before the meant position.
|
* before the meant position.
|
||||||
*
|
*
|
||||||
* @type {number}
|
* @type {number}
|
||||||
@ -102,7 +102,7 @@ export const relativePositionToJSON = rpos => {
|
|||||||
*
|
*
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const createRelativePositionFromJSON = json => new RelativePosition(json.type == null ? null : createID(json.type.client, json.type.clock), json.tname ?? null, json.item == null ? null : createID(json.item.client, json.item.clock), json.assoc == null ? 0 : json.assoc)
|
export const createRelativePositionFromJSON = json => new RelativePosition(json.type == null ? null : createID(json.type.client, json.type.clock), json.tname || null, json.item == null ? null : createID(json.item.client, json.item.clock), json.assoc == null ? 0 : json.assoc)
|
||||||
|
|
||||||
export class AbsolutePosition {
|
export class AbsolutePosition {
|
||||||
/**
|
/**
|
||||||
@ -257,36 +257,13 @@ export const readRelativePosition = decoder => {
|
|||||||
export const decodeRelativePosition = uint8Array => readRelativePosition(decoding.createDecoder(uint8Array))
|
export const decodeRelativePosition = uint8Array => readRelativePosition(decoding.createDecoder(uint8Array))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {StructStore} store
|
|
||||||
* @param {ID} id
|
|
||||||
*/
|
|
||||||
const getItemWithOffset = (store, id) => {
|
|
||||||
const item = getItem(store, id)
|
|
||||||
const diff = id.clock - item.id.clock
|
|
||||||
return {
|
|
||||||
item, diff
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform a relative position to an absolute position.
|
|
||||||
*
|
|
||||||
* If you want to share the relative position with other users, you should set
|
|
||||||
* `followUndoneDeletions` to false to get consistent results across all clients.
|
|
||||||
*
|
|
||||||
* When calculating the absolute position, we try to follow the "undone deletions". This yields
|
|
||||||
* better results for the user who performed undo. However, only the user who performed the undo
|
|
||||||
* will get the better results, the other users don't know which operations recreated a deleted
|
|
||||||
* range of content. There is more information in this ticket: https://github.com/yjs/yjs/issues/638
|
|
||||||
*
|
|
||||||
* @param {RelativePosition} rpos
|
* @param {RelativePosition} rpos
|
||||||
* @param {Doc} doc
|
* @param {Doc} doc
|
||||||
* @param {boolean} followUndoneDeletions - whether to follow undone deletions - see https://github.com/yjs/yjs/issues/638
|
|
||||||
* @return {AbsolutePosition|null}
|
* @return {AbsolutePosition|null}
|
||||||
*
|
*
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const createAbsolutePositionFromRelativePosition = (rpos, doc, followUndoneDeletions = true) => {
|
export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
|
||||||
const store = doc.store
|
const store = doc.store
|
||||||
const rightID = rpos.item
|
const rightID = rpos.item
|
||||||
const typeID = rpos.type
|
const typeID = rpos.type
|
||||||
@ -298,7 +275,7 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc, followUndo
|
|||||||
if (getState(store, rightID.client) <= rightID.clock) {
|
if (getState(store, rightID.client) <= rightID.clock) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const res = followUndoneDeletions ? followRedone(store, rightID) : getItemWithOffset(store, rightID)
|
const res = followRedone(store, rightID)
|
||||||
const right = res.item
|
const right = res.item
|
||||||
if (!(right instanceof Item)) {
|
if (!(right instanceof Item)) {
|
||||||
return null
|
return null
|
||||||
@ -322,7 +299,7 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc, followUndo
|
|||||||
// type does not exist yet
|
// type does not exist yet
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const { item } = followUndoneDeletions ? followRedone(store, typeID) : { item: getItem(store, typeID) }
|
const { item } = followRedone(store, typeID)
|
||||||
if (item instanceof Item && item.content instanceof ContentType) {
|
if (item instanceof Item && item.content instanceof ContentType) {
|
||||||
type = item.content.type
|
type = item.content.type
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
isDeleted,
|
isDeleted,
|
||||||
createDeleteSetFromStructStore,
|
createDeleteSetFromStructStore,
|
||||||
@ -14,10 +15,7 @@ import {
|
|||||||
findIndexSS,
|
findIndexSS,
|
||||||
UpdateEncoderV2,
|
UpdateEncoderV2,
|
||||||
applyUpdateV2,
|
applyUpdateV2,
|
||||||
LazyStructReader,
|
DSEncoderV1, DSEncoderV2, DSDecoderV1, DSDecoderV2, Transaction, Doc, DeleteSet, Item // eslint-disable-line
|
||||||
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'
|
||||||
@ -149,20 +147,12 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @example
|
|
||||||
* const ydoc = new Y.Doc({ gc: false })
|
|
||||||
* ydoc.getText().insert(0, 'world!')
|
|
||||||
* const snapshot = Y.snapshot(ydoc)
|
|
||||||
* ydoc.getText().insert(0, 'hello ')
|
|
||||||
* const restored = Y.createDocFromSnapshot(ydoc, snapshot)
|
|
||||||
* assert(restored.getText().toString() === 'world!')
|
|
||||||
*
|
|
||||||
* @param {Doc} originDoc
|
* @param {Doc} originDoc
|
||||||
* @param {Snapshot} snapshot
|
* @param {Snapshot} snapshot
|
||||||
* @param {Doc} [newDoc] Optionally, you may define the Yjs document that receives the data from originDoc
|
* @param {Doc} [newDoc] Optionally, you may define the Yjs document that receives the data from originDoc
|
||||||
@ -171,7 +161,7 @@ export const splitSnapshotAffectedStructs = (transaction, snapshot) => {
|
|||||||
export const createDocFromSnapshot = (originDoc, snapshot, newDoc = new Doc()) => {
|
export const createDocFromSnapshot = (originDoc, snapshot, newDoc = new Doc()) => {
|
||||||
if (originDoc.gc) {
|
if (originDoc.gc) {
|
||||||
// we should not try to restore a GC-ed document, because some of the restored items might have their content deleted
|
// we should not try to restore a GC-ed document, because some of the restored items might have their content deleted
|
||||||
throw new Error('Garbage-collection must be disabled in `originDoc`!')
|
throw new Error('originDoc must not be garbage collected')
|
||||||
}
|
}
|
||||||
const { sv, ds } = snapshot
|
const { sv, ds } = snapshot
|
||||||
|
|
||||||
@ -209,28 +199,3 @@ 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,3 +1,4 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
GC,
|
GC,
|
||||||
splitItem,
|
splitItem,
|
||||||
@ -66,13 +67,13 @@ export const getState = (store, client) => {
|
|||||||
* @private
|
* @private
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const integrityCheck = store => {
|
export const integretyCheck = store => {
|
||||||
store.clients.forEach(structs => {
|
store.clients.forEach(structs => {
|
||||||
for (let i = 1; i < structs.length; i++) {
|
for (let i = 1; i < structs.length; i++) {
|
||||||
const l = structs[i - 1]
|
const l = structs[i - 1]
|
||||||
const r = structs[i]
|
const r = structs[i]
|
||||||
if (l.id.clock + l.length !== r.id.clock) {
|
if (l.id.clock + l.length !== r.id.clock) {
|
||||||
throw new Error('StructStore failed integrity check')
|
throw new Error('StructStore failed integrety check')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
getState,
|
getState,
|
||||||
writeStructsFromTransaction,
|
writeStructsFromTransaction,
|
||||||
@ -10,7 +11,6 @@ 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'
|
||||||
|
|
||||||
@ -28,8 +28,7 @@ import { callAll } from 'lib0/function'
|
|||||||
* possible. Here is an example to illustrate the advantages of bundling:
|
* possible. Here is an example to illustrate the advantages of bundling:
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const ydoc = new Y.Doc()
|
* const map = y.define('map', YMap)
|
||||||
* const map = ydoc.getMap('map')
|
|
||||||
* // Log content when change is triggered
|
* // Log content when change is triggered
|
||||||
* map.observe(() => {
|
* map.observe(() => {
|
||||||
* console.log('change triggered')
|
* console.log('change triggered')
|
||||||
@ -38,7 +37,7 @@ import { callAll } from 'lib0/function'
|
|||||||
* map.set('a', 0) // => "change triggered"
|
* map.set('a', 0) // => "change triggered"
|
||||||
* map.set('b', 0) // => "change triggered"
|
* map.set('b', 0) // => "change triggered"
|
||||||
* // When put in a transaction, it will trigger the log after the transaction:
|
* // When put in a transaction, it will trigger the log after the transaction:
|
||||||
* ydoc.transact(() => {
|
* y.transact(() => {
|
||||||
* map.set('a', 1)
|
* map.set('a', 1)
|
||||||
* map.set('b', 1)
|
* map.set('b', 1)
|
||||||
* }) // => "change triggered"
|
* }) // => "change triggered"
|
||||||
@ -115,10 +114,6 @@ export class Transaction {
|
|||||||
* @type {Set<Doc>}
|
* @type {Set<Doc>}
|
||||||
*/
|
*/
|
||||||
this.subdocsLoaded = new Set()
|
this.subdocsLoaded = new Set()
|
||||||
/**
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
this._needFormattingCleanup = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,29 +161,18 @@ 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 tryToMergeWithLefts = (structs, pos) => {
|
const tryToMergeWithLeft = (structs, pos) => {
|
||||||
let right = structs[pos]
|
const left = structs[pos - 1]
|
||||||
let left = structs[pos - 1]
|
const right = structs[pos]
|
||||||
let i = pos
|
|
||||||
for (; i > 0; right = left, left = structs[--i - 1]) {
|
|
||||||
if (left.deleted === right.deleted && left.constructor === right.constructor) {
|
if (left.deleted === right.deleted && left.constructor === right.constructor) {
|
||||||
if (left.mergeWith(right)) {
|
if (left.mergeWith(right)) {
|
||||||
|
structs.splice(pos, 1)
|
||||||
if (right instanceof Item && right.parentSub !== null && /** @type {AbstractType<any>} */ (right.parent)._map.get(right.parentSub) === right) {
|
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))
|
/** @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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -225,7 +209,7 @@ const tryGcDeleteSet = (ds, store, gcFilter) => {
|
|||||||
*/
|
*/
|
||||||
const tryMergeDeleteSet = (ds, store) => {
|
const tryMergeDeleteSet = (ds, store) => {
|
||||||
// try to merge deleted / gc'd items
|
// try to merge deleted / gc'd items
|
||||||
// merge from right to left for better efficiency and so we don't miss any merge targets
|
// merge from right to left for better efficiecy and so we don't miss any merge targets
|
||||||
ds.clients.forEach((deleteItems, client) => {
|
ds.clients.forEach((deleteItems, client) => {
|
||||||
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
|
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
|
||||||
for (let di = deleteItems.length - 1; di >= 0; di--) {
|
for (let di = deleteItems.length - 1; di >= 0; di--) {
|
||||||
@ -235,9 +219,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]
|
||||||
) {
|
) {
|
||||||
si -= 1 + tryToMergeWithLefts(structs, si)
|
tryToMergeWithLeft(structs, si)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -267,6 +251,7 @@ const cleanupTransactions = (transactionCleanups, i) => {
|
|||||||
try {
|
try {
|
||||||
sortAndMergeDeleteSet(ds)
|
sortAndMergeDeleteSet(ds)
|
||||||
transaction.afterState = getStateVector(transaction.doc.store)
|
transaction.afterState = getStateVector(transaction.doc.store)
|
||||||
|
doc._transaction = null
|
||||||
doc.emit('beforeObserverCalls', [transaction, doc])
|
doc.emit('beforeObserverCalls', [transaction, doc])
|
||||||
/**
|
/**
|
||||||
* An array of event callbacks.
|
* An array of event callbacks.
|
||||||
@ -286,10 +271,11 @@ 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
|
||||||
@ -297,8 +283,6 @@ const cleanupTransactions = (transactionCleanups, i) => {
|
|||||||
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.
|
// sort events by path length so that top-level events are fired first.
|
||||||
events
|
events
|
||||||
@ -308,12 +292,10 @@ const cleanupTransactions = (transactionCleanups, i) => {
|
|||||||
callEventHandlerListeners(type._dEH, events, transaction)
|
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.
|
||||||
@ -329,25 +311,23 @@ 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;) {
|
for (let i = structs.length - 1; i >= firstChangePos; i--) {
|
||||||
i -= 1 + tryToMergeWithLefts(structs, i)
|
tryToMergeWithLeft(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 = mergeStructs.length - 1; i >= 0; i--) {
|
for (let i = 0; i < mergeStructs.length; 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) {
|
||||||
if (tryToMergeWithLefts(structs, replacedStructPos + 1) > 1) {
|
tryToMergeWithLeft(structs, replacedStructPos + 1)
|
||||||
continue // no need to perform next check, both are already merged
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (replacedStructPos > 0) {
|
if (replacedStructPos > 0) {
|
||||||
tryToMergeWithLefts(structs, replacedStructPos)
|
tryToMergeWithLeft(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)) {
|
||||||
@ -397,21 +377,15 @@ const cleanupTransactions = (transactionCleanups, i) => {
|
|||||||
/**
|
/**
|
||||||
* Implements the functionality of `y.transact(()=>{..})`
|
* Implements the functionality of `y.transact(()=>{..})`
|
||||||
*
|
*
|
||||||
* @template T
|
|
||||||
* @param {Doc} doc
|
* @param {Doc} doc
|
||||||
* @param {function(Transaction):T} f
|
* @param {function(Transaction):void} f
|
||||||
* @param {any} [origin=true]
|
* @param {any} [origin=true]
|
||||||
* @return {T}
|
|
||||||
*
|
*
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const transact = (doc, f, origin = null, local = true) => {
|
export const transact = (doc, f, origin = null, local = true) => {
|
||||||
const transactionCleanups = doc._transactionCleanups
|
const transactionCleanups = doc._transactionCleanups
|
||||||
let initialCall = false
|
let initialCall = false
|
||||||
/**
|
|
||||||
* @type {any}
|
|
||||||
*/
|
|
||||||
let result = null
|
|
||||||
if (doc._transaction === null) {
|
if (doc._transaction === null) {
|
||||||
initialCall = true
|
initialCall = true
|
||||||
doc._transaction = new Transaction(doc, origin, local)
|
doc._transaction = new Transaction(doc, origin, local)
|
||||||
@ -422,12 +396,9 @@ export const transact = (doc, f, origin = null, local = true) => {
|
|||||||
doc.emit('beforeTransaction', [doc._transaction, doc])
|
doc.emit('beforeTransaction', [doc._transaction, doc])
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
result = f(doc._transaction)
|
f(doc._transaction)
|
||||||
} finally {
|
} finally {
|
||||||
if (initialCall) {
|
if (initialCall && transactionCleanups[0] === doc._transaction) {
|
||||||
const finishCleanup = doc._transaction === transactionCleanups[0]
|
|
||||||
doc._transaction = null
|
|
||||||
if (finishCleanup) {
|
|
||||||
// The first transaction ended, now process observer calls.
|
// The first transaction ended, now process observer calls.
|
||||||
// Observer call may create new transactions for which we need to call the observers and do cleanup.
|
// Observer call may create new transactions for which we need to call the observers and do cleanup.
|
||||||
// We don't want to nest these calls, so we execute these calls one after
|
// We don't want to nest these calls, so we execute these calls one after
|
||||||
@ -440,5 +411,3 @@ export const transact = (doc, f, origin = null, local = true) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
@ -10,15 +10,14 @@ import {
|
|||||||
getItemCleanStart,
|
getItemCleanStart,
|
||||||
isDeleted,
|
isDeleted,
|
||||||
addToDeleteSet,
|
addToDeleteSet,
|
||||||
YEvent, Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line
|
Transaction, Doc, Item, GC, DeleteSet, AbstractType, YEvent // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
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 { ObservableV2 } from 'lib0/observable'
|
|
||||||
|
|
||||||
export class StackItem {
|
class StackItem {
|
||||||
/**
|
/**
|
||||||
* @param {DeleteSet} deletions
|
* @param {DeleteSet} deletions
|
||||||
* @param {DeleteSet} insertions
|
* @param {DeleteSet} insertions
|
||||||
@ -39,7 +38,7 @@ export class StackItem {
|
|||||||
*/
|
*/
|
||||||
const clearUndoManagerStackItem = (tr, um, stackItem) => {
|
const clearUndoManagerStackItem = (tr, um, stackItem) => {
|
||||||
iterateDeletedStructs(tr, stackItem.deletions, item => {
|
iterateDeletedStructs(tr, stackItem.deletions, item => {
|
||||||
if (item instanceof Item && um.scope.some(type => type === tr.doc || isParentOf(/** @type {AbstractType<any>} */ (type), item))) {
|
if (item instanceof Item && um.scope.some(type => isParentOf(type, item))) {
|
||||||
keepItem(item, false)
|
keepItem(item, false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -48,10 +47,15 @@ const clearUndoManagerStackItem = (tr, um, stackItem) => {
|
|||||||
/**
|
/**
|
||||||
* @param {UndoManager} undoManager
|
* @param {UndoManager} undoManager
|
||||||
* @param {Array<StackItem>} stack
|
* @param {Array<StackItem>} stack
|
||||||
* @param {'undo'|'redo'} eventType
|
* @param {string} eventType
|
||||||
* @return {StackItem?}
|
* @return {StackItem?}
|
||||||
*/
|
*/
|
||||||
const popStackItem = (undoManager, stack, eventType) => {
|
const popStackItem = (undoManager, stack, eventType) => {
|
||||||
|
/**
|
||||||
|
* Whether a change happened
|
||||||
|
* @type {StackItem?}
|
||||||
|
*/
|
||||||
|
let result = null
|
||||||
/**
|
/**
|
||||||
* Keep a reference to the transaction so we can fire the event with the changedParentTypes
|
* Keep a reference to the transaction so we can fire the event with the changedParentTypes
|
||||||
* @type {any}
|
* @type {any}
|
||||||
@ -60,7 +64,7 @@ const popStackItem = (undoManager, stack, eventType) => {
|
|||||||
const doc = undoManager.doc
|
const doc = undoManager.doc
|
||||||
const scope = undoManager.scope
|
const scope = undoManager.scope
|
||||||
transact(doc, transaction => {
|
transact(doc, transaction => {
|
||||||
while (stack.length > 0 && undoManager.currStackItem === null) {
|
while (stack.length > 0 && result === null) {
|
||||||
const store = doc.store
|
const store = doc.store
|
||||||
const stackItem = /** @type {StackItem} */ (stack.pop())
|
const stackItem = /** @type {StackItem} */ (stack.pop())
|
||||||
/**
|
/**
|
||||||
@ -81,7 +85,7 @@ const popStackItem = (undoManager, stack, eventType) => {
|
|||||||
}
|
}
|
||||||
struct = item
|
struct = item
|
||||||
}
|
}
|
||||||
if (!struct.deleted && scope.some(type => type === transaction.doc || isParentOf(/** @type {AbstractType<any>} */ (type), /** @type {Item} */ (struct)))) {
|
if (!struct.deleted && scope.some(type => isParentOf(type, /** @type {Item} */ (struct)))) {
|
||||||
itemsToDelete.push(struct)
|
itemsToDelete.push(struct)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -89,7 +93,7 @@ const popStackItem = (undoManager, stack, eventType) => {
|
|||||||
iterateDeletedStructs(transaction, stackItem.deletions, struct => {
|
iterateDeletedStructs(transaction, stackItem.deletions, struct => {
|
||||||
if (
|
if (
|
||||||
struct instanceof Item &&
|
struct instanceof Item &&
|
||||||
scope.some(type => type === transaction.doc || isParentOf(/** @type {AbstractType<any>} */ (type), struct)) &&
|
scope.some(type => isParentOf(type, struct)) &&
|
||||||
// Never redo structs in stackItem.insertions because they were created and deleted in the same capture interval.
|
// Never redo structs in stackItem.insertions because they were created and deleted in the same capture interval.
|
||||||
!isDeleted(stackItem.insertions, struct.id)
|
!isDeleted(stackItem.insertions, struct.id)
|
||||||
) {
|
) {
|
||||||
@ -97,7 +101,7 @@ const popStackItem = (undoManager, stack, eventType) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
itemsToRedo.forEach(struct => {
|
itemsToRedo.forEach(struct => {
|
||||||
performedChange = redoItem(transaction, struct, itemsToRedo, stackItem.insertions, undoManager.ignoreRemoteMapChanges, undoManager) !== null || performedChange
|
performedChange = redoItem(transaction, struct, itemsToRedo, stackItem.insertions, undoManager.ignoreRemoteMapChanges) !== null || performedChange
|
||||||
})
|
})
|
||||||
// We want to delete in reverse order so that children are deleted before
|
// We want to delete in reverse order so that children are deleted before
|
||||||
// parents, so we have more information available when items are filtered.
|
// parents, so we have more information available when items are filtered.
|
||||||
@ -108,7 +112,7 @@ const popStackItem = (undoManager, stack, eventType) => {
|
|||||||
performedChange = true
|
performedChange = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
undoManager.currStackItem = performedChange ? stackItem : null
|
result = performedChange ? stackItem : null
|
||||||
}
|
}
|
||||||
transaction.changed.forEach((subProps, type) => {
|
transaction.changed.forEach((subProps, type) => {
|
||||||
// destroy search marker if necessary
|
// destroy search marker if necessary
|
||||||
@ -118,13 +122,11 @@ const popStackItem = (undoManager, stack, eventType) => {
|
|||||||
})
|
})
|
||||||
_tr = transaction
|
_tr = transaction
|
||||||
}, undoManager)
|
}, undoManager)
|
||||||
const res = undoManager.currStackItem
|
if (result != null) {
|
||||||
if (res != null) {
|
|
||||||
const changedParentTypes = _tr.changedParentTypes
|
const changedParentTypes = _tr.changedParentTypes
|
||||||
undoManager.emit('stack-item-popped', [{ stackItem: res, type: eventType, changedParentTypes, origin: undoManager }, undoManager])
|
undoManager.emit('stack-item-popped', [{ stackItem: result, type: eventType, changedParentTypes }, undoManager])
|
||||||
undoManager.currStackItem = null
|
|
||||||
}
|
}
|
||||||
return res
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -132,20 +134,11 @@ const popStackItem = (undoManager, stack, eventType) => {
|
|||||||
* @property {number} [UndoManagerOptions.captureTimeout=500]
|
* @property {number} [UndoManagerOptions.captureTimeout=500]
|
||||||
* @property {function(Transaction):boolean} [UndoManagerOptions.captureTransaction] Do not capture changes of a Transaction if result false.
|
* @property {function(Transaction):boolean} [UndoManagerOptions.captureTransaction] Do not capture changes of a Transaction if result false.
|
||||||
* @property {function(Item):boolean} [UndoManagerOptions.deleteFilter=()=>true] Sometimes
|
* @property {function(Item):boolean} [UndoManagerOptions.deleteFilter=()=>true] Sometimes
|
||||||
* it is necessary to filter what an Undo/Redo operation can delete. If this
|
* it is necessary to filter whan an Undo/Redo operation can delete. If this
|
||||||
* filter returns false, the type/item won't be deleted even it is in the
|
* filter returns false, the type/item won't be deleted even it is in the
|
||||||
* undo/redo scope.
|
* undo/redo scope.
|
||||||
* @property {Set<any>} [UndoManagerOptions.trackedOrigins=new Set([null])]
|
* @property {Set<any>} [UndoManagerOptions.trackedOrigins=new Set([null])]
|
||||||
* @property {boolean} [ignoreRemoteMapChanges] Experimental. By default, the UndoManager will never overwrite remote changes. Enable this property to enable overwriting remote changes on key-value changes (Y.Map, properties on Y.Xml, etc..).
|
* @property {boolean} [ignoreRemoteMapChanges] Experimental. By default, the UndoManager will never overwrite remote changes. Enable this property to enable overwriting remote changes on key-value changes (Y.Map, properties on Y.Xml, etc..).
|
||||||
* @property {Doc} [doc] The document that this UndoManager operates on. Only needed if typeScope is empty.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} StackItemEvent
|
|
||||||
* @property {StackItem} StackItemEvent.stackItem
|
|
||||||
* @property {any} StackItemEvent.origin
|
|
||||||
* @property {'undo'|'redo'} StackItemEvent.type
|
|
||||||
* @property {Map<AbstractType<YEvent<any>>,Array<YEvent<any>>>} StackItemEvent.changedParentTypes
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -155,27 +148,25 @@ const popStackItem = (undoManager, stack, eventType) => {
|
|||||||
* Fires 'stack-item-popped' event when a stack item was popped from either the
|
* Fires 'stack-item-popped' event when a stack item was popped from either the
|
||||||
* undo- or the redo-stack. You may restore the saved stack information from `event.stackItem.meta`.
|
* undo- or the redo-stack. You may restore the saved stack information from `event.stackItem.meta`.
|
||||||
*
|
*
|
||||||
* @extends {ObservableV2<{'stack-item-added':function(StackItemEvent, UndoManager):void, 'stack-item-popped': function(StackItemEvent, UndoManager):void, 'stack-cleared': function({ undoStackCleared: boolean, redoStackCleared: boolean }):void, 'stack-item-updated': function(StackItemEvent, UndoManager):void }>}
|
* @extends {Observable<'stack-item-added'|'stack-item-popped'|'stack-cleared'|'stack-item-updated'>}
|
||||||
*/
|
*/
|
||||||
export class UndoManager extends ObservableV2 {
|
export class UndoManager extends Observable {
|
||||||
/**
|
/**
|
||||||
* @param {Doc|AbstractType<any>|Array<AbstractType<any>>} typeScope Limits the scope of the UndoManager. If this is set to a ydoc instance, all changes on that ydoc will be undone. If set to a specific type, only changes on that type or its children will be undone. Also accepts an array of types.
|
* @param {AbstractType<any>|Array<AbstractType<any>>} typeScope Accepts either a single type, or an array of types
|
||||||
* @param {UndoManagerOptions} options
|
* @param {UndoManagerOptions} options
|
||||||
*/
|
*/
|
||||||
constructor (typeScope, {
|
constructor (typeScope, {
|
||||||
captureTimeout = 500,
|
captureTimeout = 500,
|
||||||
captureTransaction = _tr => true,
|
captureTransaction = tr => true,
|
||||||
deleteFilter = () => true,
|
deleteFilter = () => true,
|
||||||
trackedOrigins = new Set([null]),
|
trackedOrigins = new Set([null]),
|
||||||
ignoreRemoteMapChanges = false,
|
ignoreRemoteMapChanges = false
|
||||||
doc = /** @type {Doc} */ (array.isArray(typeScope) ? typeScope[0].doc : typeScope instanceof Doc ? typeScope : typeScope.doc)
|
|
||||||
} = {}) {
|
} = {}) {
|
||||||
super()
|
super()
|
||||||
/**
|
/**
|
||||||
* @type {Array<AbstractType<any> | Doc>}
|
* @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)
|
||||||
@ -196,15 +187,9 @@ export class UndoManager extends ObservableV2 {
|
|||||||
*/
|
*/
|
||||||
this.undoing = false
|
this.undoing = false
|
||||||
this.redoing = false
|
this.redoing = false
|
||||||
/**
|
this.doc = /** @type {Doc} */ (this.scope[0].doc)
|
||||||
* The currently popped stack item if UndoManager.undoing or UndoManager.redoing
|
|
||||||
*
|
|
||||||
* @type {StackItem|null}
|
|
||||||
*/
|
|
||||||
this.currStackItem = null
|
|
||||||
this.lastChange = 0
|
this.lastChange = 0
|
||||||
this.ignoreRemoteMapChanges = ignoreRemoteMapChanges
|
this.ignoreRemoteMapChanges = ignoreRemoteMapChanges
|
||||||
this.captureTimeout = captureTimeout
|
|
||||||
/**
|
/**
|
||||||
* @param {Transaction} transaction
|
* @param {Transaction} transaction
|
||||||
*/
|
*/
|
||||||
@ -212,7 +197,7 @@ export class UndoManager extends ObservableV2 {
|
|||||||
// Only track certain transactions
|
// Only track certain transactions
|
||||||
if (
|
if (
|
||||||
!this.captureTransaction(transaction) ||
|
!this.captureTransaction(transaction) ||
|
||||||
!this.scope.some(type => transaction.changedParentTypes.has(/** @type {AbstractType<any>} */ (type)) || type === this.doc) ||
|
!this.scope.some(type => transaction.changedParentTypes.has(type)) ||
|
||||||
(!this.trackedOrigins.has(transaction.origin) && (!transaction.origin || !this.trackedOrigins.has(transaction.origin.constructor)))
|
(!this.trackedOrigins.has(transaction.origin) && (!transaction.origin || !this.trackedOrigins.has(transaction.origin.constructor)))
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
@ -236,7 +221,7 @@ export class UndoManager extends ObservableV2 {
|
|||||||
})
|
})
|
||||||
const now = time.getUnixTime()
|
const now = time.getUnixTime()
|
||||||
let didAdd = false
|
let didAdd = false
|
||||||
if (this.lastChange > 0 && now - this.lastChange < this.captureTimeout && stack.length > 0 && !undoing && !redoing) {
|
if (now - this.lastChange < captureTimeout && stack.length > 0 && !undoing && !redoing) {
|
||||||
// append change to last stack op
|
// append change to last stack op
|
||||||
const lastOp = stack[stack.length - 1]
|
const lastOp = stack[stack.length - 1]
|
||||||
lastOp.deletions = mergeDeleteSets([lastOp.deletions, transaction.deleteSet])
|
lastOp.deletions = mergeDeleteSets([lastOp.deletions, transaction.deleteSet])
|
||||||
@ -251,13 +236,10 @@ export class UndoManager extends ObservableV2 {
|
|||||||
}
|
}
|
||||||
// make sure that deleted structs are not gc'd
|
// make sure that deleted structs are not gc'd
|
||||||
iterateDeletedStructs(transaction, transaction.deleteSet, /** @param {Item|GC} item */ item => {
|
iterateDeletedStructs(transaction, transaction.deleteSet, /** @param {Item|GC} item */ item => {
|
||||||
if (item instanceof Item && this.scope.some(type => type === transaction.doc || isParentOf(/** @type {AbstractType<any>} */ (type), item))) {
|
if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) {
|
||||||
keepItem(item, true)
|
keepItem(item, true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
/**
|
|
||||||
* @type {[StackItemEvent, UndoManager]}
|
|
||||||
*/
|
|
||||||
const changeEvent = [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo', changedParentTypes: transaction.changedParentTypes }, this]
|
const changeEvent = [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo', changedParentTypes: transaction.changedParentTypes }, this]
|
||||||
if (didAdd) {
|
if (didAdd) {
|
||||||
this.emit('stack-item-added', changeEvent)
|
this.emit('stack-item-added', changeEvent)
|
||||||
@ -272,17 +254,12 @@ export class UndoManager extends ObservableV2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extend the scope.
|
* @param {Array<AbstractType<any>> | AbstractType<any>} ytypes
|
||||||
*
|
|
||||||
* @param {Array<AbstractType<any> | Doc> | AbstractType<any> | Doc} ytypes
|
|
||||||
*/
|
*/
|
||||||
addToScope (ytypes) {
|
addToScope (ytypes) {
|
||||||
const tmpSet = new Set(this.scope)
|
|
||||||
ytypes = array.isArray(ytypes) ? ytypes : [ytypes]
|
ytypes = array.isArray(ytypes) ? ytypes : [ytypes]
|
||||||
ytypes.forEach(ytype => {
|
ytypes.forEach(ytype => {
|
||||||
if (!tmpSet.has(ytype)) {
|
if (this.scope.every(yt => yt !== ytype)) {
|
||||||
tmpSet.add(ytype)
|
|
||||||
if (ytype instanceof AbstractType ? ytype.doc !== this.doc : ytype !== 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,3 +1,4 @@
|
|||||||
|
|
||||||
import * as error from 'lib0/error'
|
import * as error from 'lib0/error'
|
||||||
import * as encoding from 'lib0/encoding'
|
import * as encoding from 'lib0/encoding'
|
||||||
|
|
||||||
@ -167,7 +168,7 @@ export class UpdateEncoderV2 extends DSEncoderV2 {
|
|||||||
*/
|
*/
|
||||||
this.keyMap = new Map()
|
this.keyMap = new Map()
|
||||||
/**
|
/**
|
||||||
* Refers to the next unique key-identifier to me used.
|
* Refers to the next uniqe key-identifier to me used.
|
||||||
* See writeKey method for more information.
|
* See writeKey method for more information.
|
||||||
*
|
*
|
||||||
* @type {number}
|
* @type {number}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
isDeleted,
|
isDeleted,
|
||||||
Item, AbstractType, Transaction, AbstractStruct // eslint-disable-line
|
Item, AbstractType, Transaction, AbstractStruct // eslint-disable-line
|
||||||
@ -5,9 +6,6 @@ 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
|
||||||
@ -46,10 +44,6 @@ 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -66,7 +60,8 @@ export class YEvent {
|
|||||||
* type === event.target // => true
|
* type === event.target // => true
|
||||||
*/
|
*/
|
||||||
get path () {
|
get path () {
|
||||||
return this._path || (this._path = getPathTo(this.currentTarget, this.target))
|
// @ts-ignore _item is defined because target is integrated
|
||||||
|
return getPathTo(this.currentTarget, this.target)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -86,9 +81,6 @@ 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))
|
||||||
@ -138,11 +130,6 @@ export class YEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a computed property. Note that this can only be safely computed during the
|
|
||||||
* event call. Computing this property after other changes happened might result in
|
|
||||||
* unexpected behavior (incorrect computation of deltas). A safe way to collect changes
|
|
||||||
* is to store the `changes` or the `delta` object. Avoid storing the `transaction` object.
|
|
||||||
*
|
|
||||||
* @type {Array<{insert?: string | Array<any> | object | AbstractType<any>, retain?: number, delete?: number, attributes?: Object<string, any>}>}
|
* @type {Array<{insert?: string | Array<any> | object | AbstractType<any>, retain?: number, delete?: number, attributes?: Object<string, any>}>}
|
||||||
*/
|
*/
|
||||||
get delta () {
|
get delta () {
|
||||||
@ -162,19 +149,11 @@ export class YEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a computed property. Note that this can only be safely computed during the
|
|
||||||
* event call. Computing this property after other changes happened might result in
|
|
||||||
* unexpected behavior (incorrect computation of deltas). A safe way to collect changes
|
|
||||||
* is to store the `changes` or the `delta` object. Avoid storing the `transaction` object.
|
|
||||||
*
|
|
||||||
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Array<{insert?:Array<any>|string, delete?:number, retain?:number}>}}
|
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Array<{insert?:Array<any>|string, delete?:number, retain?:number}>}}
|
||||||
*/
|
*/
|
||||||
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()
|
||||||
@ -264,8 +243,8 @@ const getPathTo = (parent, child) => {
|
|||||||
let i = 0
|
let i = 0
|
||||||
let c = /** @type {AbstractType<any>} */ (child._item.parent)._start
|
let c = /** @type {AbstractType<any>} */ (child._item.parent)._start
|
||||||
while (c !== child._item && c !== null) {
|
while (c !== child._item && c !== null) {
|
||||||
if (!c.deleted && c.countable) {
|
if (!c.deleted) {
|
||||||
i += c.length
|
i++
|
||||||
}
|
}
|
||||||
c = c.right
|
c = c.right
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @module encoding
|
* @module encoding
|
||||||
*/
|
*/
|
||||||
@ -44,7 +45,6 @@ import * as decoding from 'lib0/decoding'
|
|||||||
import * as binary from 'lib0/binary'
|
import * as binary from 'lib0/binary'
|
||||||
import * as map from 'lib0/map'
|
import * as map from 'lib0/map'
|
||||||
import * as math from 'lib0/math'
|
import * as math from 'lib0/math'
|
||||||
import * as array from 'lib0/array'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
@ -87,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)
|
||||||
}
|
}
|
||||||
@ -96,8 +96,9 @@ export const writeClientsStructs = (encoder, store, _sm) => {
|
|||||||
encoding.writeVarUint(encoder.restEncoder, sm.size)
|
encoding.writeVarUint(encoder.restEncoder, sm.size)
|
||||||
// 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]) => {
|
||||||
writeStructs(encoder, /** @type {Array<GC|Item>} */ (store.clients.get(client)), client, clock)
|
// @ts-ignore
|
||||||
|
writeStructs(encoder, store.clients.get(client), client, clock)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,7 +155,7 @@ export const readClientsStructRefs = (decoder, doc) => {
|
|||||||
// @type {string|null}
|
// @type {string|null}
|
||||||
const struct = new Item(
|
const struct = new Item(
|
||||||
createID(client, clock),
|
createID(client, clock),
|
||||||
null, // left
|
null, // leftd
|
||||||
(info & binary.BIT8) === binary.BIT8 ? decoder.readLeftID() : null, // origin
|
(info & binary.BIT8) === binary.BIT8 ? decoder.readLeftID() : null, // origin
|
||||||
null, // right
|
null, // right
|
||||||
(info & binary.BIT7) === binary.BIT7 ? decoder.readRightID() : null, // right origin
|
(info & binary.BIT7) === binary.BIT7 ? decoder.readRightID() : null, // right origin
|
||||||
@ -178,7 +179,7 @@ export const readClientsStructRefs = (decoder, doc) => {
|
|||||||
|
|
||||||
const struct = new Item(
|
const struct = new Item(
|
||||||
createID(client, clock),
|
createID(client, clock),
|
||||||
null, // left
|
null, // leftd
|
||||||
origin, // origin
|
origin, // origin
|
||||||
null, // right
|
null, // right
|
||||||
rightOrigin, // right origin
|
rightOrigin, // right origin
|
||||||
@ -211,7 +212,7 @@ export const readClientsStructRefs = (decoder, doc) => {
|
|||||||
* then we start emptying the stack.
|
* then we start emptying the stack.
|
||||||
*
|
*
|
||||||
* It is not possible to have circles: i.e. struct1 (from client1) depends on struct2 (from client2)
|
* It is not possible to have circles: i.e. struct1 (from client1) depends on struct2 (from client2)
|
||||||
* depends on struct3 (from client1). Therefore the max stack size is equal to `structReaders.length`.
|
* depends on struct3 (from client1). Therefore the max stack size is eqaul to `structReaders.length`.
|
||||||
*
|
*
|
||||||
* This method is implemented in a way so that we can resume computation if this update
|
* This method is implemented in a way so that we can resume computation if this update
|
||||||
* causally depends on another update.
|
* causally depends on another update.
|
||||||
@ -230,7 +231,7 @@ const integrateStructs = (transaction, store, clientsStructRefs) => {
|
|||||||
*/
|
*/
|
||||||
const stack = []
|
const stack = []
|
||||||
// sort them so that we take the higher id first, in case of conflicts the lower id will probably not conflict with the id from the higher user.
|
// sort them so that we take the higher id first, in case of conflicts the lower id will probably not conflict with the id from the higher user.
|
||||||
let clientsStructRefsIds = array.from(clientsStructRefs.keys()).sort((a, b) => a - b)
|
let clientsStructRefsIds = Array.from(clientsStructRefs.keys()).sort((a, b) => a - b)
|
||||||
if (clientsStructRefsIds.length === 0) {
|
if (clientsStructRefsIds.length === 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -250,7 +251,7 @@ const integrateStructs = (transaction, store, clientsStructRefs) => {
|
|||||||
return nextStructsTarget
|
return nextStructsTarget
|
||||||
}
|
}
|
||||||
let curStructsTarget = getNextStructTarget()
|
let curStructsTarget = getNextStructTarget()
|
||||||
if (curStructsTarget === null) {
|
if (curStructsTarget === null && stack.length === 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,14 +280,14 @@ const integrateStructs = (transaction, store, clientsStructRefs) => {
|
|||||||
const addStackToRestSS = () => {
|
const addStackToRestSS = () => {
|
||||||
for (const item of stack) {
|
for (const item of stack) {
|
||||||
const client = item.id.client
|
const client = item.id.client
|
||||||
const inapplicableItems = clientsStructRefs.get(client)
|
const unapplicableItems = clientsStructRefs.get(client)
|
||||||
if (inapplicableItems) {
|
if (unapplicableItems) {
|
||||||
// decrement because we weren't able to apply previous operation
|
// decrement because we weren't able to apply previous operation
|
||||||
inapplicableItems.i--
|
unapplicableItems.i--
|
||||||
restStructs.clients.set(client, inapplicableItems.refs.slice(inapplicableItems.i))
|
restStructs.clients.set(client, unapplicableItems.refs.slice(unapplicableItems.i))
|
||||||
clientsStructRefs.delete(client)
|
clientsStructRefs.delete(client)
|
||||||
inapplicableItems.i = 0
|
unapplicableItems.i = 0
|
||||||
inapplicableItems.refs = []
|
unapplicableItems.refs = []
|
||||||
} else {
|
} else {
|
||||||
// item was the last item on clientsStructRefs and the field was already cleared. Add item to restStructs and continue
|
// item was the last item on clientsStructRefs and the field was already cleared. Add item to restStructs and continue
|
||||||
restStructs.clients.set(client, [item])
|
restStructs.clients.set(client, [item])
|
||||||
@ -370,7 +371,7 @@ export const writeStructsFromTransaction = (encoder, transaction) => writeClient
|
|||||||
/**
|
/**
|
||||||
* Read and apply a document update.
|
* Read and apply a document update.
|
||||||
*
|
*
|
||||||
* This function has the same effect as `applyUpdate` but accepts a decoder.
|
* This function has the same effect as `applyUpdate` but accepts an decoder.
|
||||||
*
|
*
|
||||||
* @param {decoding.Decoder} decoder
|
* @param {decoding.Decoder} decoder
|
||||||
* @param {Doc} ydoc
|
* @param {Doc} ydoc
|
||||||
@ -451,7 +452,7 @@ export const readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = n
|
|||||||
/**
|
/**
|
||||||
* Read and apply a document update.
|
* Read and apply a document update.
|
||||||
*
|
*
|
||||||
* This function has the same effect as `applyUpdate` but accepts a decoder.
|
* This function has the same effect as `applyUpdate` but accepts an decoder.
|
||||||
*
|
*
|
||||||
* @param {decoding.Decoder} decoder
|
* @param {decoding.Decoder} decoder
|
||||||
* @param {Doc} ydoc
|
* @param {Doc} ydoc
|
||||||
@ -600,7 +601,7 @@ export const decodeStateVector = decodedState => readStateVector(new DSDecoderV1
|
|||||||
*/
|
*/
|
||||||
export const writeStateVector = (encoder, sv) => {
|
export const writeStateVector = (encoder, sv) => {
|
||||||
encoding.writeVarUint(encoder.restEncoder, sv.size)
|
encoding.writeVarUint(encoder.restEncoder, sv.size)
|
||||||
array.from(sv.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => {
|
Array.from(sv.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => {
|
||||||
encoding.writeVarUint(encoder.restEncoder, client) // @todo use a special client decoder that is based on mapping
|
encoding.writeVarUint(encoder.restEncoder, client) // @todo use a special client decoder that is based on mapping
|
||||||
encoding.writeVarUint(encoder.restEncoder, clock)
|
encoding.writeVarUint(encoder.restEncoder, clock)
|
||||||
})
|
})
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import { AbstractType, Item } from '../internals.js' // eslint-disable-line
|
import { AbstractType, Item } from '../internals.js' // eslint-disable-line
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
AbstractType // eslint-disable-line
|
AbstractType // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
@ -1,40 +1,20 @@
|
|||||||
|
|
||||||
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'
|
||||||
import * as error from 'lib0/error'
|
|
||||||
import * as f from 'lib0/function'
|
|
||||||
import * as logging from 'lib0/logging'
|
import * as logging from 'lib0/logging'
|
||||||
import * as map from 'lib0/map'
|
|
||||||
import * as math from 'lib0/math'
|
import * as math from 'lib0/math'
|
||||||
import * as string from 'lib0/string'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ContentAny,
|
|
||||||
ContentBinary,
|
|
||||||
ContentDeleted,
|
|
||||||
ContentDoc,
|
|
||||||
ContentEmbed,
|
|
||||||
ContentFormat,
|
|
||||||
ContentJSON,
|
|
||||||
ContentString,
|
|
||||||
ContentType,
|
|
||||||
createID,
|
createID,
|
||||||
decodeStateVector,
|
readItemContent,
|
||||||
|
readDeleteSet,
|
||||||
|
writeDeleteSet,
|
||||||
|
Skip,
|
||||||
|
mergeDeleteSets,
|
||||||
DSEncoderV1,
|
DSEncoderV1,
|
||||||
DSEncoderV2,
|
DSEncoderV2,
|
||||||
GC,
|
decodeStateVector,
|
||||||
Item,
|
Item, GC, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2 // eslint-disable-line
|
||||||
mergeDeleteSets,
|
|
||||||
readDeleteSet,
|
|
||||||
readItemContent,
|
|
||||||
Skip,
|
|
||||||
UpdateDecoderV1,
|
|
||||||
UpdateDecoderV2,
|
|
||||||
UpdateEncoderV1,
|
|
||||||
UpdateEncoderV2,
|
|
||||||
writeDeleteSet,
|
|
||||||
YXmlElement,
|
|
||||||
YXmlHook
|
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -572,17 +552,17 @@ const finishLazyStructWriting = (lazyWriter) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Uint8Array} update
|
* @param {Uint8Array} update
|
||||||
* @param {function(Item|GC|Skip):Item|GC|Skip} blockTransformer
|
|
||||||
* @param {typeof UpdateDecoderV2 | typeof UpdateDecoderV1} YDecoder
|
* @param {typeof UpdateDecoderV2 | typeof UpdateDecoderV1} YDecoder
|
||||||
* @param {typeof UpdateEncoderV2 | typeof UpdateEncoderV1 } YEncoder
|
* @param {typeof UpdateEncoderV2 | typeof UpdateEncoderV1 } YEncoder
|
||||||
*/
|
*/
|
||||||
export const convertUpdateFormat = (update, blockTransformer, YDecoder, YEncoder) => {
|
export const convertUpdateFormat = (update, YDecoder, YEncoder) => {
|
||||||
const updateDecoder = new YDecoder(decoding.createDecoder(update))
|
const updateDecoder = new YDecoder(decoding.createDecoder(update))
|
||||||
const lazyDecoder = new LazyStructReader(updateDecoder, false)
|
const lazyDecoder = new LazyStructReader(updateDecoder, false)
|
||||||
const updateEncoder = new YEncoder()
|
const updateEncoder = new YEncoder()
|
||||||
const lazyWriter = new LazyStructWriter(updateEncoder)
|
const lazyWriter = new LazyStructWriter(updateEncoder)
|
||||||
|
|
||||||
for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) {
|
for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) {
|
||||||
writeStructToLazyStructWriter(lazyWriter, blockTransformer(curr), 0)
|
writeStructToLazyStructWriter(lazyWriter, curr, 0)
|
||||||
}
|
}
|
||||||
finishLazyStructWriting(lazyWriter)
|
finishLazyStructWriting(lazyWriter)
|
||||||
const ds = readDeleteSet(updateDecoder)
|
const ds = readDeleteSet(updateDecoder)
|
||||||
@ -591,132 +571,11 @@ export const convertUpdateFormat = (update, blockTransformer, YDecoder, YEncoder
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} ObfuscatorOptions
|
|
||||||
* @property {boolean} [ObfuscatorOptions.formatting=true]
|
|
||||||
* @property {boolean} [ObfuscatorOptions.subdocs=true]
|
|
||||||
* @property {boolean} [ObfuscatorOptions.yxml=true] Whether to obfuscate nodeName / hookName
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {ObfuscatorOptions} obfuscator
|
|
||||||
*/
|
|
||||||
const createObfuscator = ({ formatting = true, subdocs = true, yxml = true } = {}) => {
|
|
||||||
let i = 0
|
|
||||||
const mapKeyCache = map.create()
|
|
||||||
const nodeNameCache = map.create()
|
|
||||||
const formattingKeyCache = map.create()
|
|
||||||
const formattingValueCache = map.create()
|
|
||||||
formattingValueCache.set(null, null) // end of a formatting range should always be the end of a formatting range
|
|
||||||
/**
|
|
||||||
* @param {Item|GC|Skip} block
|
|
||||||
* @return {Item|GC|Skip}
|
|
||||||
*/
|
|
||||||
return block => {
|
|
||||||
switch (block.constructor) {
|
|
||||||
case GC:
|
|
||||||
case Skip:
|
|
||||||
return block
|
|
||||||
case Item: {
|
|
||||||
const item = /** @type {Item} */ (block)
|
|
||||||
const content = item.content
|
|
||||||
switch (content.constructor) {
|
|
||||||
case ContentDeleted:
|
|
||||||
break
|
|
||||||
case ContentType: {
|
|
||||||
if (yxml) {
|
|
||||||
const type = /** @type {ContentType} */ (content).type
|
|
||||||
if (type instanceof YXmlElement) {
|
|
||||||
type.nodeName = map.setIfUndefined(nodeNameCache, type.nodeName, () => 'node-' + i)
|
|
||||||
}
|
|
||||||
if (type instanceof YXmlHook) {
|
|
||||||
type.hookName = map.setIfUndefined(nodeNameCache, type.hookName, () => 'hook-' + i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case ContentAny: {
|
|
||||||
const c = /** @type {ContentAny} */ (content)
|
|
||||||
c.arr = c.arr.map(() => i)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case ContentBinary: {
|
|
||||||
const c = /** @type {ContentBinary} */ (content)
|
|
||||||
c.content = new Uint8Array([i])
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case ContentDoc: {
|
|
||||||
const c = /** @type {ContentDoc} */ (content)
|
|
||||||
if (subdocs) {
|
|
||||||
c.opts = {}
|
|
||||||
c.doc.guid = i + ''
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case ContentEmbed: {
|
|
||||||
const c = /** @type {ContentEmbed} */ (content)
|
|
||||||
c.embed = {}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case ContentFormat: {
|
|
||||||
const c = /** @type {ContentFormat} */ (content)
|
|
||||||
if (formatting) {
|
|
||||||
c.key = map.setIfUndefined(formattingKeyCache, c.key, () => i + '')
|
|
||||||
c.value = map.setIfUndefined(formattingValueCache, c.value, () => ({ i }))
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case ContentJSON: {
|
|
||||||
const c = /** @type {ContentJSON} */ (content)
|
|
||||||
c.arr = c.arr.map(() => i)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case ContentString: {
|
|
||||||
const c = /** @type {ContentString} */ (content)
|
|
||||||
c.str = string.repeat((i % 10) + '', c.str.length)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
// unknown content type
|
|
||||||
error.unexpectedCase()
|
|
||||||
}
|
|
||||||
if (item.parentSub) {
|
|
||||||
item.parentSub = map.setIfUndefined(mapKeyCache, item.parentSub, () => i + '')
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
return block
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
// unknown block-type
|
|
||||||
error.unexpectedCase()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function obfuscates the content of a Yjs update. This is useful to share
|
|
||||||
* buggy Yjs documents while significantly limiting the possibility that a
|
|
||||||
* developer can on the user. Note that it might still be possible to deduce
|
|
||||||
* some information by analyzing the "structure" of the document or by analyzing
|
|
||||||
* the typing behavior using the CRDT-related metadata that is still kept fully
|
|
||||||
* intact.
|
|
||||||
*
|
|
||||||
* @param {Uint8Array} update
|
* @param {Uint8Array} update
|
||||||
* @param {ObfuscatorOptions} [opts]
|
|
||||||
*/
|
*/
|
||||||
export const obfuscateUpdate = (update, opts) => convertUpdateFormat(update, createObfuscator(opts), UpdateDecoderV1, UpdateEncoderV1)
|
export const convertUpdateFormatV1ToV2 = update => convertUpdateFormat(update, UpdateDecoderV1, UpdateEncoderV2)
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Uint8Array} update
|
|
||||||
* @param {ObfuscatorOptions} [opts]
|
|
||||||
*/
|
|
||||||
export const obfuscateUpdateV2 = (update, opts) => convertUpdateFormat(update, createObfuscator(opts), UpdateDecoderV2, UpdateEncoderV2)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Uint8Array} update
|
* @param {Uint8Array} update
|
||||||
*/
|
*/
|
||||||
export const convertUpdateFormatV1ToV2 = update => convertUpdateFormat(update, f.id, UpdateDecoderV1, UpdateEncoderV2)
|
export const convertUpdateFormatV2ToV1 = update => convertUpdateFormat(update, UpdateDecoderV2, UpdateEncoderV1)
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Uint8Array} update
|
|
||||||
*/
|
|
||||||
export const convertUpdateFormatV2ToV1 = update => convertUpdateFormat(update, f.id, UpdateDecoderV2, UpdateEncoderV1)
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Testing if encoding/decoding compatibility and integration compatibility 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.
|
||||||
*
|
*
|
||||||
* The v1 documents were generated with Yjs v13.2.0 based on the randomisized tests.
|
* The v1 documents were generated with Yjs v13.2.0 based on the randomisized tests.
|
||||||
|
@ -1,55 +1,13 @@
|
|||||||
|
|
||||||
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'
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {t.TestCase} _tc
|
|
||||||
*/
|
|
||||||
export const testAfterTransactionRecursion = _tc => {
|
|
||||||
const ydoc = new Y.Doc()
|
|
||||||
const yxml = ydoc.getXmlFragment('')
|
|
||||||
ydoc.on('afterTransaction', tr => {
|
|
||||||
if (tr.origin === 'test') {
|
|
||||||
yxml.toJSON()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
ydoc.transact(_tr => {
|
|
||||||
for (let i = 0; i < 15000; i++) {
|
|
||||||
yxml.push([new Y.XmlText('a')])
|
|
||||||
}
|
|
||||||
}, 'test')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {t.TestCase} _tc
|
|
||||||
*/
|
|
||||||
export const testOriginInTransaction = _tc => {
|
|
||||||
const doc = new Y.Doc()
|
|
||||||
const ytext = doc.getText()
|
|
||||||
/**
|
|
||||||
* @type {Array<string>}
|
|
||||||
*/
|
|
||||||
const origins = []
|
|
||||||
doc.on('afterTransaction', (tr) => {
|
|
||||||
origins.push(tr.origin)
|
|
||||||
if (origins.length <= 1) {
|
|
||||||
ytext.toDelta(Y.snapshot(doc)) // adding a snapshot forces toDelta to create a cleanup transaction
|
|
||||||
doc.transact(() => {
|
|
||||||
ytext.insert(0, 'a')
|
|
||||||
}, 'nested')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
doc.transact(() => {
|
|
||||||
ytext.insert(0, '0')
|
|
||||||
}, 'first')
|
|
||||||
t.compareArrays(origins, ['first', 'cleanup', 'nested'])
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Client id should be changed when an instance receives updates from another client using the same client id.
|
* Client id should be changed when an instance receives updates from another client using the same client id.
|
||||||
*
|
*
|
||||||
* @param {t.TestCase} _tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testClientIdDuplicateChange = _tc => {
|
export const testClientIdDuplicateChange = tc => {
|
||||||
const doc1 = new Y.Doc()
|
const doc1 = new Y.Doc()
|
||||||
doc1.clientID = 0
|
doc1.clientID = 0
|
||||||
const doc2 = new Y.Doc()
|
const doc2 = new Y.Doc()
|
||||||
@ -61,9 +19,9 @@ export const testClientIdDuplicateChange = _tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} _tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testGetTypeEmptyId = _tc => {
|
export const testGetTypeEmptyId = tc => {
|
||||||
const doc1 = new Y.Doc()
|
const doc1 = new Y.Doc()
|
||||||
doc1.getText('').insert(0, 'h')
|
doc1.getText('').insert(0, 'h')
|
||||||
doc1.getText().insert(1, 'i')
|
doc1.getText().insert(1, 'i')
|
||||||
@ -74,9 +32,9 @@ export const testGetTypeEmptyId = _tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} _tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testToJSON = _tc => {
|
export const testToJSON = tc => {
|
||||||
const doc = new Y.Doc()
|
const doc = new Y.Doc()
|
||||||
t.compare(doc.toJSON(), {}, 'doc.toJSON yields empty object')
|
t.compare(doc.toJSON(), {}, 'doc.toJSON yields empty object')
|
||||||
|
|
||||||
@ -101,9 +59,9 @@ export const testToJSON = _tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} _tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testSubdoc = _tc => {
|
export const testSubdoc = tc => {
|
||||||
const doc = new Y.Doc()
|
const doc = new Y.Doc()
|
||||||
doc.load() // doesn't do anything
|
doc.load() // doesn't do anything
|
||||||
{
|
{
|
||||||
@ -168,9 +126,9 @@ export const testSubdoc = _tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} _tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testSubdocLoadEdgeCases = _tc => {
|
export const testSubdocLoadEdgeCases = tc => {
|
||||||
const ydoc = new Y.Doc()
|
const ydoc = new Y.Doc()
|
||||||
const yarray = ydoc.getArray()
|
const yarray = ydoc.getArray()
|
||||||
const subdoc1 = new Y.Doc()
|
const subdoc1 = new Y.Doc()
|
||||||
@ -215,9 +173,9 @@ export const testSubdocLoadEdgeCases = _tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} _tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testSubdocLoadEdgeCasesAutoload = _tc => {
|
export const testSubdocLoadEdgeCasesAutoload = tc => {
|
||||||
const ydoc = new Y.Doc()
|
const ydoc = new Y.Doc()
|
||||||
const yarray = ydoc.getArray()
|
const yarray = ydoc.getArray()
|
||||||
const subdoc1 = new Y.Doc({ autoLoad: true })
|
const subdoc1 = new Y.Doc({ autoLoad: true })
|
||||||
@ -257,9 +215,9 @@ export const testSubdocLoadEdgeCasesAutoload = _tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} _tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testSubdocsUndo = _tc => {
|
export const testSubdocsUndo = tc => {
|
||||||
const ydoc = new Y.Doc()
|
const ydoc = new Y.Doc()
|
||||||
const elems = ydoc.getXmlFragment()
|
const elems = ydoc.getXmlFragment()
|
||||||
const undoManager = new Y.UndoManager(elems)
|
const undoManager = new Y.UndoManager(elems)
|
||||||
@ -272,9 +230,9 @@ export const testSubdocsUndo = _tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} _tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testLoadDocsEvent = async _tc => {
|
export const testLoadDocs = async tc => {
|
||||||
const ydoc = new Y.Doc()
|
const ydoc = new Y.Doc()
|
||||||
t.assert(ydoc.isLoaded === false)
|
t.assert(ydoc.isLoaded === false)
|
||||||
let loadedEvent = false
|
let loadedEvent = false
|
||||||
@ -286,44 +244,3 @@ export const testLoadDocsEvent = async _tc => {
|
|||||||
t.assert(loadedEvent)
|
t.assert(loadedEvent)
|
||||||
t.assert(ydoc.isLoaded)
|
t.assert(ydoc.isLoaded)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {t.TestCase} _tc
|
|
||||||
*/
|
|
||||||
export const testSyncDocsEvent = async _tc => {
|
|
||||||
const ydoc = new Y.Doc()
|
|
||||||
t.assert(ydoc.isLoaded === false)
|
|
||||||
t.assert(ydoc.isSynced === false)
|
|
||||||
let loadedEvent = false
|
|
||||||
ydoc.once('load', () => {
|
|
||||||
loadedEvent = true
|
|
||||||
})
|
|
||||||
let syncedEvent = false
|
|
||||||
ydoc.once('sync', /** @param {any} isSynced */ (isSynced) => {
|
|
||||||
syncedEvent = true
|
|
||||||
t.assert(isSynced)
|
|
||||||
})
|
|
||||||
ydoc.emit('sync', [true, ydoc])
|
|
||||||
await ydoc.whenLoaded
|
|
||||||
const oldWhenSynced = ydoc.whenSynced
|
|
||||||
await ydoc.whenSynced
|
|
||||||
t.assert(loadedEvent)
|
|
||||||
t.assert(syncedEvent)
|
|
||||||
t.assert(ydoc.isLoaded)
|
|
||||||
t.assert(ydoc.isSynced)
|
|
||||||
let loadedEvent2 = false
|
|
||||||
ydoc.on('load', () => {
|
|
||||||
loadedEvent2 = true
|
|
||||||
})
|
|
||||||
let syncedEvent2 = false
|
|
||||||
ydoc.on('sync', (isSynced) => {
|
|
||||||
syncedEvent2 = true
|
|
||||||
t.assert(isSynced === false)
|
|
||||||
})
|
|
||||||
ydoc.emit('sync', [false, ydoc])
|
|
||||||
t.assert(!loadedEvent2)
|
|
||||||
t.assert(syncedEvent2)
|
|
||||||
t.assert(ydoc.isLoaded)
|
|
||||||
t.assert(!ydoc.isSynced)
|
|
||||||
t.assert(ydoc.whenSynced !== oldWhenSynced)
|
|
||||||
}
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
/* eslint-env node */
|
|
||||||
|
|
||||||
import * as map from './y-map.tests.js'
|
import * as map from './y-map.tests.js'
|
||||||
import * as array from './y-array.tests.js'
|
import * as array from './y-array.tests.js'
|
||||||
@ -15,28 +14,15 @@ import * as relativePositions from './relativePositions.tests.js'
|
|||||||
import { runTests } from 'lib0/testing'
|
import { runTests } from 'lib0/testing'
|
||||||
import { isBrowser, isNode } from 'lib0/environment'
|
import { isBrowser, isNode } from 'lib0/environment'
|
||||||
import * as log from 'lib0/logging'
|
import * as log from 'lib0/logging'
|
||||||
import { environment } from 'lib0'
|
|
||||||
|
|
||||||
if (isBrowser) {
|
if (isBrowser) {
|
||||||
log.createVConsole(document.body)
|
log.createVConsole(document.body)
|
||||||
}
|
}
|
||||||
|
runTests({
|
||||||
/**
|
|
||||||
* @type {any}
|
|
||||||
*/
|
|
||||||
const tests = {
|
|
||||||
doc, map, array, text, xml, encoding, undoredo, compatibility, snapshot, updates, relativePositions
|
doc, map, array, text, xml, encoding, undoredo, compatibility, snapshot, updates, relativePositions
|
||||||
}
|
}).then(success => {
|
||||||
|
|
||||||
const run = async () => {
|
|
||||||
if (environment.isNode) {
|
|
||||||
// tests.nodejs = await import('./node.tests.js')
|
|
||||||
}
|
|
||||||
|
|
||||||
const success = await runTests(tests)
|
|
||||||
/* istanbul ignore next */
|
/* istanbul ignore next */
|
||||||
if (isNode) {
|
if (isNode) {
|
||||||
process.exit(success ? 0 : 1)
|
process.exit(success ? 0 : 1)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
run()
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
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'
|
||||||
|
|
||||||
@ -85,26 +86,6 @@ export const testRelativePositionCase6 = tc => {
|
|||||||
checkRelativePositions(ytext)
|
checkRelativePositions(ytext)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Testing https://github.com/yjs/yjs/issues/657
|
|
||||||
*
|
|
||||||
* @param {t.TestCase} tc
|
|
||||||
*/
|
|
||||||
export const testRelativePositionCase7 = tc => {
|
|
||||||
const docA = new Y.Doc()
|
|
||||||
const textA = docA.getText('text')
|
|
||||||
textA.insert(0, 'abcde')
|
|
||||||
// Create a relative position at index 2 in 'textA'
|
|
||||||
const relativePosition = Y.createRelativePositionFromTypeIndex(textA, 2)
|
|
||||||
// Verify that the absolutes positions on 'docA' are the same
|
|
||||||
const absolutePositionWithFollow =
|
|
||||||
Y.createAbsolutePositionFromRelativePosition(relativePosition, docA, true)
|
|
||||||
const absolutePositionWithoutFollow =
|
|
||||||
Y.createAbsolutePositionFromRelativePosition(relativePosition, docA, false)
|
|
||||||
t.assert(absolutePositionWithFollow?.index === 2)
|
|
||||||
t.assert(absolutePositionWithoutFollow?.index === 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
@ -121,25 +102,3 @@ export const testRelativePositionAssociationDifference = tc => {
|
|||||||
t.assert(posRight != null && posRight.index === 2)
|
t.assert(posRight != null && posRight.index === 2)
|
||||||
t.assert(posLeft != null && posLeft.index === 1)
|
t.assert(posLeft != null && posLeft.index === 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {t.TestCase} tc
|
|
||||||
*/
|
|
||||||
export const testRelativePositionWithUndo = tc => {
|
|
||||||
const ydoc = new Y.Doc()
|
|
||||||
const ytext = ydoc.getText()
|
|
||||||
ytext.insert(0, 'hello world')
|
|
||||||
const rpos = Y.createRelativePositionFromTypeIndex(ytext, 1)
|
|
||||||
const um = new Y.UndoManager(ytext)
|
|
||||||
ytext.delete(0, 6)
|
|
||||||
t.assert(Y.createAbsolutePositionFromRelativePosition(rpos, ydoc)?.index === 0)
|
|
||||||
um.undo()
|
|
||||||
t.assert(Y.createAbsolutePositionFromRelativePosition(rpos, ydoc)?.index === 1)
|
|
||||||
const posWithoutFollow = Y.createAbsolutePositionFromRelativePosition(rpos, ydoc, false)
|
|
||||||
console.log({ posWithoutFollow })
|
|
||||||
t.assert(Y.createAbsolutePositionFromRelativePosition(rpos, ydoc, false)?.index === 6)
|
|
||||||
const ydocClone = new Y.Doc()
|
|
||||||
Y.applyUpdate(ydocClone, Y.encodeStateAsUpdate(ydoc))
|
|
||||||
t.assert(Y.createAbsolutePositionFromRelativePosition(rpos, ydocClone)?.index === 6)
|
|
||||||
t.assert(Y.createAbsolutePositionFromRelativePosition(rpos, ydocClone, false)?.index === 6)
|
|
||||||
}
|
|
||||||
|
@ -3,36 +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 testBasicRestoreSnapshot = tc => {
|
||||||
const ydoc = new Y.Doc({ gc: false })
|
|
||||||
ydoc.getText().insert(0, 'world!')
|
|
||||||
const snapshot = Y.snapshot(ydoc)
|
|
||||||
ydoc.getText().insert(0, 'hello ')
|
|
||||||
const restored = Y.createDocFromSnapshot(ydoc, snapshot)
|
|
||||||
t.assert(restored.getText().toString() === 'world!')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {t.TestCase} _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)
|
||||||
@ -45,9 +18,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)
|
||||||
@ -58,16 +31,16 @@ export const testEmptyRestoreSnapshot = _tc => {
|
|||||||
t.compare(docRestored.getArray().toArray(), [])
|
t.compare(docRestored.getArray().toArray(), [])
|
||||||
t.compare(doc.getArray().toArray(), ['world'])
|
t.compare(doc.getArray().toArray(), ['world'])
|
||||||
|
|
||||||
// now this snapshot reflects the latest state. It should still work.
|
// now this snapshot reflects the latest state. It shoult still work.
|
||||||
const snap2 = Y.snapshot(doc)
|
const snap2 = Y.snapshot(doc)
|
||||||
const docRestored2 = Y.createDocFromSnapshot(doc, snap2)
|
const docRestored2 = Y.createDocFromSnapshot(doc, snap2)
|
||||||
t.compare(docRestored2.getArray().toArray(), ['world'])
|
t.compare(docRestored2.getArray().toArray(), ['world'])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @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)
|
||||||
@ -88,9 +61,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'])
|
||||||
|
|
||||||
@ -104,9 +77,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)
|
||||||
@ -122,9 +95,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)
|
||||||
@ -138,9 +111,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)
|
||||||
@ -196,28 +169,3 @@ 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,3 +1,4 @@
|
|||||||
|
|
||||||
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'
|
||||||
@ -34,7 +35,7 @@ export const encV1 = {
|
|||||||
mergeUpdates: Y.mergeUpdates,
|
mergeUpdates: Y.mergeUpdates,
|
||||||
applyUpdate: Y.applyUpdate,
|
applyUpdate: Y.applyUpdate,
|
||||||
logUpdate: Y.logUpdate,
|
logUpdate: Y.logUpdate,
|
||||||
updateEventName: /** @type {'update'} */ ('update'),
|
updateEventName: 'update',
|
||||||
diffUpdate: Y.diffUpdate
|
diffUpdate: Y.diffUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,7 +44,7 @@ export const encV2 = {
|
|||||||
mergeUpdates: Y.mergeUpdatesV2,
|
mergeUpdates: Y.mergeUpdatesV2,
|
||||||
applyUpdate: Y.applyUpdateV2,
|
applyUpdate: Y.applyUpdateV2,
|
||||||
logUpdate: Y.logUpdateV2,
|
logUpdate: Y.logUpdateV2,
|
||||||
updateEventName: /** @type {'updateV2'} */ ('updateV2'),
|
updateEventName: 'updateV2',
|
||||||
diffUpdate: Y.diffUpdateV2
|
diffUpdate: Y.diffUpdateV2
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,7 +134,7 @@ export class TestYInstance extends Y.Doc {
|
|||||||
* @param {TestYInstance} remoteClient
|
* @param {TestYInstance} remoteClient
|
||||||
*/
|
*/
|
||||||
_receive (message, remoteClient) {
|
_receive (message, remoteClient) {
|
||||||
map.setIfUndefined(this.receiving, remoteClient, () => /** @type {Array<Uint8Array>} */ ([])).push(message)
|
map.setIfUndefined(this.receiving, remoteClient, () => []).push(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -346,7 +347,7 @@ export const compare = users => {
|
|||||||
t.compare(userMapValues[i], userMapValues[i + 1])
|
t.compare(userMapValues[i], userMapValues[i + 1])
|
||||||
t.compare(userXmlValues[i], userXmlValues[i + 1])
|
t.compare(userXmlValues[i], userXmlValues[i + 1])
|
||||||
t.compare(userTextValues[i].map(/** @param {any} a */ a => typeof a.insert === 'string' ? a.insert : ' ').join('').length, users[i].getText('text').length)
|
t.compare(userTextValues[i].map(/** @param {any} a */ a => typeof a.insert === 'string' ? a.insert : ' ').join('').length, users[i].getText('text').length)
|
||||||
t.compare(userTextValues[i], userTextValues[i + 1], '', (_constructor, a, b) => {
|
t.compare(userTextValues[i], userTextValues[i + 1], '', (constructor, a, b) => {
|
||||||
if (a instanceof Y.AbstractType) {
|
if (a instanceof Y.AbstractType) {
|
||||||
t.compare(a.toJSON(), b.toJSON())
|
t.compare(a.toJSON(), b.toJSON())
|
||||||
} else if (a !== b) {
|
} else if (a !== b) {
|
||||||
@ -355,9 +356,8 @@ 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]))
|
||||||
Y.equalDeleteSets(Y.createDeleteSetFromStructStore(users[i].store), Y.createDeleteSetFromStructStore(users[i + 1].store))
|
compareDS(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())
|
||||||
}
|
}
|
||||||
@ -370,8 +370,8 @@ export const compare = users => {
|
|||||||
export const compareItemIDs = (a, b) => a === b || (a !== null && b != null && Y.compareIDs(a.id, b.id))
|
export const compareItemIDs = (a, b) => a === b || (a !== null && b != null && Y.compareIDs(a.id, b.id))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('../src/internals.js').StructStore} ss1
|
* @param {import('../src/internals').StructStore} ss1
|
||||||
* @param {import('../src/internals.js').StructStore} ss2
|
* @param {import('../src/internals').StructStore} ss2
|
||||||
*/
|
*/
|
||||||
export const compareStructStores = (ss1, ss2) => {
|
export const compareStructStores = (ss1, ss2) => {
|
||||||
t.assert(ss1.clients.size === ss2.clients.size)
|
t.assert(ss1.clients.size === ss2.clients.size)
|
||||||
@ -412,6 +412,25 @@ export const compareStructStores = (ss1, ss2) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('../src/internals').DeleteSet} ds1
|
||||||
|
* @param {import('../src/internals').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').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
|
||||||
|
@ -1,61 +1,8 @@
|
|||||||
import { init } from './testHelper.js' // eslint-disable-line
|
import { init, compare, applyRandomTests, Doc } 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
|
|
||||||
*/
|
|
||||||
export const testInfiniteCaptureTimeout = tc => {
|
|
||||||
const { array0 } = init(tc, { users: 3 })
|
|
||||||
const undoManager = new Y.UndoManager(array0, { captureTimeout: Number.MAX_VALUE })
|
|
||||||
array0.push([1, 2, 3])
|
|
||||||
undoManager.stopCapturing()
|
|
||||||
array0.push([4, 5, 6])
|
|
||||||
undoManager.undo()
|
|
||||||
t.compare(array0.toArray(), [1, 2, 3])
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
@ -104,89 +51,9 @@ export const testUndoText = tc => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Test case to fix #241
|
* Test case to fix #241
|
||||||
* @param {t.TestCase} _tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testEmptyTypeScope = _tc => {
|
export const testDoubleUndo = tc => {
|
||||||
const ydoc = new Y.Doc()
|
|
||||||
const um = new Y.UndoManager([], { doc: ydoc })
|
|
||||||
const yarray = ydoc.getArray()
|
|
||||||
um.addToScope(yarray)
|
|
||||||
yarray.insert(0, [1])
|
|
||||||
um.undo()
|
|
||||||
t.assert(yarray.length === 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {t.TestCase} _tc
|
|
||||||
*/
|
|
||||||
export const testRejectUpdateExample = _tc => {
|
|
||||||
const tmpydoc1 = new Y.Doc()
|
|
||||||
tmpydoc1.getArray('restricted').insert(0, [1])
|
|
||||||
tmpydoc1.getArray('public').insert(0, [1])
|
|
||||||
const update1 = Y.encodeStateAsUpdate(tmpydoc1)
|
|
||||||
const tmpydoc2 = new Y.Doc()
|
|
||||||
tmpydoc2.getArray('public').insert(0, [2])
|
|
||||||
const update2 = Y.encodeStateAsUpdate(tmpydoc2)
|
|
||||||
|
|
||||||
const ydoc = new Y.Doc()
|
|
||||||
const restrictedType = ydoc.getArray('restricted')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assume this function handles incoming updates via a communication channel like websockets.
|
|
||||||
* Changes to the `ydoc.getMap('restricted')` type should be rejected.
|
|
||||||
*
|
|
||||||
* - set up undo manager on the restricted types
|
|
||||||
* - cache pending* updates from the Ydoc to avoid certain attacks
|
|
||||||
* - apply received update and check whether the restricted type (or any of its children) has been changed.
|
|
||||||
* - catch errors that might try to circumvent the restrictions
|
|
||||||
* - undo changes on restricted types
|
|
||||||
* - reapply pending* updates
|
|
||||||
*
|
|
||||||
* @param {Uint8Array} update
|
|
||||||
*/
|
|
||||||
const updateHandler = (update) => {
|
|
||||||
// don't handle changes of the local undo manager, which is used to undo invalid changes
|
|
||||||
const um = new Y.UndoManager(restrictedType, { trackedOrigins: new Set(['remote change']) })
|
|
||||||
const beforePendingDs = ydoc.store.pendingDs
|
|
||||||
const beforePendingStructs = ydoc.store.pendingStructs?.update
|
|
||||||
try {
|
|
||||||
Y.applyUpdate(ydoc, update, 'remote change')
|
|
||||||
} finally {
|
|
||||||
while (um.undoStack.length) {
|
|
||||||
um.undo()
|
|
||||||
}
|
|
||||||
um.destroy()
|
|
||||||
ydoc.store.pendingDs = beforePendingDs
|
|
||||||
ydoc.store.pendingStructs = null
|
|
||||||
if (beforePendingStructs) {
|
|
||||||
Y.applyUpdateV2(ydoc, beforePendingStructs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updateHandler(update1)
|
|
||||||
updateHandler(update2)
|
|
||||||
t.assert(restrictedType.length === 0)
|
|
||||||
t.assert(ydoc.getArray('public').length === 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test case to fix #241
|
|
||||||
* @param {t.TestCase} _tc
|
|
||||||
*/
|
|
||||||
export const testGlobalScope = _tc => {
|
|
||||||
const ydoc = new Y.Doc()
|
|
||||||
const um = new Y.UndoManager(ydoc)
|
|
||||||
const yarray = ydoc.getArray()
|
|
||||||
yarray.insert(0, [1])
|
|
||||||
um.undo()
|
|
||||||
t.assert(yarray.length === 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test case to fix #241
|
|
||||||
* @param {t.TestCase} _tc
|
|
||||||
*/
|
|
||||||
export const testDoubleUndo = _tc => {
|
|
||||||
const doc = new Y.Doc()
|
const doc = new Y.Doc()
|
||||||
const text = doc.getText()
|
const text = doc.getText()
|
||||||
text.insert(0, '1221')
|
text.insert(0, '1221')
|
||||||
@ -422,9 +289,9 @@ export const testUndoDeleteFilter = tc => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This issue has been reported in https://discuss.yjs.dev/t/undomanager-with-external-updates/454/6
|
* This issue has been reported in https://discuss.yjs.dev/t/undomanager-with-external-updates/454/6
|
||||||
* @param {t.TestCase} _tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testUndoUntilChangePerformed = _tc => {
|
export const testUndoUntilChangePerformed = tc => {
|
||||||
const doc = new Y.Doc()
|
const doc = new Y.Doc()
|
||||||
const doc2 = new Y.Doc()
|
const doc2 = new Y.Doc()
|
||||||
doc.on('update', update => Y.applyUpdate(doc2, update))
|
doc.on('update', update => Y.applyUpdate(doc2, update))
|
||||||
@ -453,9 +320,9 @@ export const testUndoUntilChangePerformed = _tc => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This issue has been reported in https://github.com/yjs/yjs/issues/317
|
* This issue has been reported in https://github.com/yjs/yjs/issues/317
|
||||||
* @param {t.TestCase} _tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testUndoNestedUndoIssue = _tc => {
|
export const testUndoNestedUndoIssue = tc => {
|
||||||
const doc = new Y.Doc({ gc: false })
|
const doc = new Y.Doc({ gc: false })
|
||||||
const design = doc.getMap()
|
const design = doc.getMap()
|
||||||
const undoManager = new Y.UndoManager(design, { captureTimeout: 0 })
|
const undoManager = new Y.UndoManager(design, { captureTimeout: 0 })
|
||||||
@ -509,9 +376,9 @@ export const testUndoNestedUndoIssue = _tc => {
|
|||||||
/**
|
/**
|
||||||
* This issue has been reported in https://github.com/yjs/yjs/issues/355
|
* This issue has been reported in https://github.com/yjs/yjs/issues/355
|
||||||
*
|
*
|
||||||
* @param {t.TestCase} _tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testConsecutiveRedoBug = _tc => {
|
export const testConsecutiveRedoBug = tc => {
|
||||||
const doc = new Y.Doc()
|
const doc = new Y.Doc()
|
||||||
const yRoot = doc.getMap()
|
const yRoot = doc.getMap()
|
||||||
const undoMgr = new Y.UndoManager(yRoot)
|
const undoMgr = new Y.UndoManager(yRoot)
|
||||||
@ -560,9 +427,9 @@ export const testConsecutiveRedoBug = _tc => {
|
|||||||
/**
|
/**
|
||||||
* This issue has been reported in https://github.com/yjs/yjs/issues/304
|
* This issue has been reported in https://github.com/yjs/yjs/issues/304
|
||||||
*
|
*
|
||||||
* @param {t.TestCase} _tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testUndoXmlBug = _tc => {
|
export const testUndoXmlBug = tc => {
|
||||||
const origin = 'origin'
|
const origin = 'origin'
|
||||||
const doc = new Y.Doc()
|
const doc = new Y.Doc()
|
||||||
const fragment = doc.getXmlFragment('t')
|
const fragment = doc.getXmlFragment('t')
|
||||||
@ -605,9 +472,9 @@ export const testUndoXmlBug = _tc => {
|
|||||||
/**
|
/**
|
||||||
* This issue has been reported in https://github.com/yjs/yjs/issues/343
|
* This issue has been reported in https://github.com/yjs/yjs/issues/343
|
||||||
*
|
*
|
||||||
* @param {t.TestCase} _tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testUndoBlockBug = _tc => {
|
export const testUndoBlockBug = tc => {
|
||||||
const doc = new Y.Doc({ gc: false })
|
const doc = new Y.Doc({ gc: false })
|
||||||
const design = doc.getMap()
|
const design = doc.getMap()
|
||||||
|
|
||||||
@ -665,9 +532,9 @@ export const testUndoBlockBug = _tc => {
|
|||||||
* Undo text formatting delete should not corrupt peer state.
|
* Undo text formatting delete should not corrupt peer state.
|
||||||
*
|
*
|
||||||
* @see https://github.com/yjs/yjs/issues/392
|
* @see https://github.com/yjs/yjs/issues/392
|
||||||
* @param {t.TestCase} _tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testUndoDeleteTextFormat = _tc => {
|
export const testUndoDeleteTextFormat = tc => {
|
||||||
const doc = new Y.Doc()
|
const doc = new Y.Doc()
|
||||||
const text = doc.getText()
|
const text = doc.getText()
|
||||||
text.insert(0, 'Attack ships on fire off the shoulder of Orion.')
|
text.insert(0, 'Attack ships on fire off the shoulder of Orion.')
|
||||||
@ -703,9 +570,9 @@ export const testUndoDeleteTextFormat = _tc => {
|
|||||||
* Undo text formatting delete should not corrupt peer state.
|
* Undo text formatting delete should not corrupt peer state.
|
||||||
*
|
*
|
||||||
* @see https://github.com/yjs/yjs/issues/392
|
* @see https://github.com/yjs/yjs/issues/392
|
||||||
* @param {t.TestCase} _tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testBehaviorOfIgnoreremotemapchangesProperty = _tc => {
|
export const testBehaviorOfIgnoreremotemapchangesProperty = tc => {
|
||||||
const doc = new Y.Doc()
|
const doc = new Y.Doc()
|
||||||
const doc2 = new Y.Doc()
|
const doc2 = new Y.Doc()
|
||||||
doc.on('update', update => Y.applyUpdate(doc2, update, doc))
|
doc.on('update', update => Y.applyUpdate(doc2, update, doc))
|
||||||
@ -721,93 +588,3 @@ export const testBehaviorOfIgnoreremotemapchangesProperty = _tc => {
|
|||||||
t.assert(map1.get('x') === 2)
|
t.assert(map1.get('x') === 2)
|
||||||
t.assert(map2.get('x') === 2)
|
t.assert(map2.get('x') === 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Special deletion case.
|
|
||||||
*
|
|
||||||
* @see https://github.com/yjs/yjs/issues/447
|
|
||||||
* @param {t.TestCase} _tc
|
|
||||||
*/
|
|
||||||
export const testSpecialDeletionCase = _tc => {
|
|
||||||
const origin = 'undoable'
|
|
||||||
const doc = new Y.Doc()
|
|
||||||
const fragment = doc.getXmlFragment()
|
|
||||||
const undoManager = new Y.UndoManager(fragment, { trackedOrigins: new Set([origin]) })
|
|
||||||
doc.transact(() => {
|
|
||||||
const e = new Y.XmlElement('test')
|
|
||||||
e.setAttribute('a', '1')
|
|
||||||
e.setAttribute('b', '2')
|
|
||||||
fragment.insert(0, [e])
|
|
||||||
})
|
|
||||||
t.compareStrings(fragment.toString(), '<test a="1" b="2"></test>')
|
|
||||||
doc.transact(() => {
|
|
||||||
// change attribute "b" and delete test-node
|
|
||||||
const e = fragment.get(0)
|
|
||||||
e.setAttribute('b', '3')
|
|
||||||
fragment.delete(0)
|
|
||||||
}, origin)
|
|
||||||
t.compareStrings(fragment.toString(), '')
|
|
||||||
undoManager.undo()
|
|
||||||
t.compareStrings(fragment.toString(), '<test a="1" b="2"></test>')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deleted entries in a map should be restored on undo.
|
|
||||||
*
|
|
||||||
* @see https://github.com/yjs/yjs/issues/500
|
|
||||||
* @param {t.TestCase} tc
|
|
||||||
*/
|
|
||||||
export const testUndoDeleteInMap = (tc) => {
|
|
||||||
const { map0 } = init(tc, { users: 3 })
|
|
||||||
const undoManager = new Y.UndoManager(map0, { captureTimeout: 0 })
|
|
||||||
map0.set('a', 'a')
|
|
||||||
map0.delete('a')
|
|
||||||
map0.set('a', 'b')
|
|
||||||
map0.delete('a')
|
|
||||||
map0.set('a', 'c')
|
|
||||||
map0.delete('a')
|
|
||||||
map0.set('a', 'd')
|
|
||||||
t.compare(map0.toJSON(), { a: 'd' })
|
|
||||||
undoManager.undo()
|
|
||||||
t.compare(map0.toJSON(), {})
|
|
||||||
undoManager.undo()
|
|
||||||
t.compare(map0.toJSON(), { a: 'c' })
|
|
||||||
undoManager.undo()
|
|
||||||
t.compare(map0.toJSON(), {})
|
|
||||||
undoManager.undo()
|
|
||||||
t.compare(map0.toJSON(), { a: 'b' })
|
|
||||||
undoManager.undo()
|
|
||||||
t.compare(map0.toJSON(), {})
|
|
||||||
undoManager.undo()
|
|
||||||
t.compare(map0.toJSON(), { a: 'a' })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* It should expose the StackItem being processed if undoing
|
|
||||||
*
|
|
||||||
* @param {t.TestCase} _tc
|
|
||||||
*/
|
|
||||||
export const testUndoDoingStackItem = async (_tc) => {
|
|
||||||
const doc = new Y.Doc()
|
|
||||||
const text = doc.getText('text')
|
|
||||||
const undoManager = new Y.UndoManager([text])
|
|
||||||
undoManager.on('stack-item-added', /** @param {any} event */ event => {
|
|
||||||
event.stackItem.meta.set('str', '42')
|
|
||||||
})
|
|
||||||
let metaUndo = /** @type {any} */ (null)
|
|
||||||
let metaRedo = /** @type {any} */ (null)
|
|
||||||
text.observe((event) => {
|
|
||||||
const /** @type {Y.UndoManager} */ origin = event.transaction.origin
|
|
||||||
if (origin === undoManager && origin.undoing) {
|
|
||||||
metaUndo = origin.currStackItem?.meta.get('str')
|
|
||||||
} else if (origin === undoManager && origin.redoing) {
|
|
||||||
metaRedo = origin.currStackItem?.meta.get('str')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
text.insert(0, 'abc')
|
|
||||||
undoManager.undo()
|
|
||||||
undoManager.redo()
|
|
||||||
t.compare(metaUndo, '42', 'currStackItem is accessible while undoing')
|
|
||||||
t.compare(metaRedo, '42', 'currStackItem is accessible while redoing')
|
|
||||||
t.compare(undoManager.currStackItem, null, 'currStackItem is null after observe/transaction')
|
|
||||||
}
|
|
||||||
|
@ -4,7 +4,6 @@ import * as Y from '../src/index.js'
|
|||||||
import { readClientsStructRefs, readDeleteSet, UpdateDecoderV2, UpdateEncoderV2, writeDeleteSet } from '../src/internals.js'
|
import { readClientsStructRefs, readDeleteSet, UpdateDecoderV2, UpdateEncoderV2, writeDeleteSet } from '../src/internals.js'
|
||||||
import * as encoding from 'lib0/encoding'
|
import * as encoding from 'lib0/encoding'
|
||||||
import * as decoding from 'lib0/decoding'
|
import * as decoding from 'lib0/decoding'
|
||||||
import * as object from 'lib0/object'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} Enc
|
* @typedef {Object} Enc
|
||||||
@ -15,7 +14,7 @@ import * as object from 'lib0/object'
|
|||||||
* @property {function(Uint8Array):{from:Map<number,number>,to:Map<number,number>}} Enc.parseUpdateMeta
|
* @property {function(Uint8Array):{from:Map<number,number>,to:Map<number,number>}} Enc.parseUpdateMeta
|
||||||
* @property {function(Y.Doc):Uint8Array} Enc.encodeStateVector
|
* @property {function(Y.Doc):Uint8Array} Enc.encodeStateVector
|
||||||
* @property {function(Uint8Array):Uint8Array} Enc.encodeStateVectorFromUpdate
|
* @property {function(Uint8Array):Uint8Array} Enc.encodeStateVectorFromUpdate
|
||||||
* @property {'update'|'updateV2'} Enc.updateEventName
|
* @property {string} Enc.updateEventName
|
||||||
* @property {string} Enc.description
|
* @property {string} Enc.description
|
||||||
* @property {function(Uint8Array, Uint8Array):Uint8Array} Enc.diffUpdate
|
* @property {function(Uint8Array, Uint8Array):Uint8Array} Enc.diffUpdate
|
||||||
*/
|
*/
|
||||||
@ -139,6 +138,7 @@ export const testKeyEncoding = tc => {
|
|||||||
*/
|
*/
|
||||||
const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
|
const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
|
||||||
const cases = []
|
const cases = []
|
||||||
|
|
||||||
// Case 1: Simple case, simply merge everything
|
// Case 1: Simple case, simply merge everything
|
||||||
cases.push(enc.mergeUpdates(updates))
|
cases.push(enc.mergeUpdates(updates))
|
||||||
|
|
||||||
@ -169,7 +169,7 @@ const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
|
|||||||
// t.info('Target State: ')
|
// t.info('Target State: ')
|
||||||
// enc.logUpdate(targetState)
|
// enc.logUpdate(targetState)
|
||||||
|
|
||||||
cases.forEach((mergedUpdates) => {
|
cases.forEach((mergedUpdates, i) => {
|
||||||
// t.info('State Case $' + i + ':')
|
// t.info('State Case $' + i + ':')
|
||||||
// enc.logUpdate(updates)
|
// enc.logUpdate(updates)
|
||||||
const merged = new Y.Doc({ gc: false })
|
const merged = new Y.Doc({ gc: false })
|
||||||
@ -218,10 +218,10 @@ const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} _tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testMergeUpdates1 = _tc => {
|
export const testMergeUpdates1 = tc => {
|
||||||
encoders.forEach((enc) => {
|
encoders.forEach((enc, i) => {
|
||||||
t.info(`Using encoder: ${enc.description}`)
|
t.info(`Using encoder: ${enc.description}`)
|
||||||
const ydoc = new Y.Doc({ gc: false })
|
const ydoc = new Y.Doc({ gc: false })
|
||||||
const updates = /** @type {Array<Uint8Array>} */ ([])
|
const updates = /** @type {Array<Uint8Array>} */ ([])
|
||||||
@ -299,59 +299,8 @@ export const testMergePendingUpdates = tc => {
|
|||||||
Y.applyUpdate(yDoc5, update4)
|
Y.applyUpdate(yDoc5, update4)
|
||||||
Y.applyUpdate(yDoc5, serverUpdates[4])
|
Y.applyUpdate(yDoc5, serverUpdates[4])
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const _update5 = Y.encodeStateAsUpdate(yDoc5) // eslint-disable-line
|
const update5 = Y.encodeStateAsUpdate(yDoc5) // eslint-disable-line
|
||||||
|
|
||||||
const yText5 = yDoc5.getText('textBlock')
|
const yText5 = yDoc5.getText('textBlock')
|
||||||
t.compareStrings(yText5.toString(), 'nenor')
|
t.compareStrings(yText5.toString(), 'nenor')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {t.TestCase} _tc
|
|
||||||
*/
|
|
||||||
export const testObfuscateUpdates = _tc => {
|
|
||||||
const ydoc = new Y.Doc()
|
|
||||||
const ytext = ydoc.getText('text')
|
|
||||||
const ymap = ydoc.getMap('map')
|
|
||||||
const yarray = ydoc.getArray('array')
|
|
||||||
// test ytext
|
|
||||||
ytext.applyDelta([{ insert: 'text', attributes: { bold: true } }, { insert: { href: 'supersecreturl' } }])
|
|
||||||
// test ymap
|
|
||||||
ymap.set('key', 'secret1')
|
|
||||||
ymap.set('key', 'secret2')
|
|
||||||
// test yarray with subtype & subdoc
|
|
||||||
const subtype = new Y.XmlElement('secretnodename')
|
|
||||||
const subdoc = new Y.Doc({ guid: 'secret' })
|
|
||||||
subtype.setAttribute('attr', 'val')
|
|
||||||
yarray.insert(0, ['teststring', 42, subtype, subdoc])
|
|
||||||
// obfuscate the content and put it into a new document
|
|
||||||
const obfuscatedUpdate = Y.obfuscateUpdate(Y.encodeStateAsUpdate(ydoc))
|
|
||||||
const odoc = new Y.Doc()
|
|
||||||
Y.applyUpdate(odoc, obfuscatedUpdate)
|
|
||||||
const otext = odoc.getText('text')
|
|
||||||
const omap = odoc.getMap('map')
|
|
||||||
const oarray = odoc.getArray('array')
|
|
||||||
// test ytext
|
|
||||||
const delta = otext.toDelta()
|
|
||||||
t.assert(delta.length === 2)
|
|
||||||
t.assert(delta[0].insert !== 'text' && delta[0].insert.length === 4)
|
|
||||||
t.assert(object.length(delta[0].attributes) === 1)
|
|
||||||
t.assert(!object.hasProperty(delta[0].attributes, 'bold'))
|
|
||||||
t.assert(object.length(delta[1]) === 1)
|
|
||||||
t.assert(object.hasProperty(delta[1], 'insert'))
|
|
||||||
// test ymap
|
|
||||||
t.assert(omap.size === 1)
|
|
||||||
t.assert(!omap.has('key'))
|
|
||||||
// test yarray with subtype & subdoc
|
|
||||||
const result = oarray.toArray()
|
|
||||||
t.assert(result.length === 4)
|
|
||||||
t.assert(result[0] !== 'teststring')
|
|
||||||
t.assert(result[1] !== 42)
|
|
||||||
const osubtype = /** @type {Y.XmlElement} */ (result[2])
|
|
||||||
const osubdoc = result[3]
|
|
||||||
// test subtype
|
|
||||||
t.assert(osubtype.nodeName !== subtype.nodeName)
|
|
||||||
t.assert(object.length(osubtype.getAttributes()) === 1)
|
|
||||||
t.assert(osubtype.getAttribute('attr') === undefined)
|
|
||||||
// test subdoc
|
|
||||||
t.assert(osubdoc.guid !== subdoc.guid)
|
|
||||||
}
|
|
||||||
|
@ -4,9 +4,6 @@ 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'
|
||||||
import * as math from 'lib0/math'
|
import * as math from 'lib0/math'
|
||||||
import * as env from 'lib0/environment'
|
|
||||||
|
|
||||||
const isDevMode = env.getVariable('node_env') === 'development'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
@ -20,28 +17,6 @@ export const testBasicUpdate = tc => {
|
|||||||
t.compare(doc2.getArray('array').toArray(), ['hi'])
|
t.compare(doc2.getArray('array').toArray(), ['hi'])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {t.TestCase} tc
|
|
||||||
*/
|
|
||||||
export const testFailsObjectManipulationInDevMode = tc => {
|
|
||||||
if (isDevMode) {
|
|
||||||
t.info('running in dev mode')
|
|
||||||
const doc = new Y.Doc()
|
|
||||||
const a = [1, 2, 3]
|
|
||||||
const b = { o: 1 }
|
|
||||||
doc.getArray('test').insert(0, [a])
|
|
||||||
doc.getMap('map').set('k', b)
|
|
||||||
t.fails(() => {
|
|
||||||
a[0] = 42
|
|
||||||
})
|
|
||||||
t.fails(() => {
|
|
||||||
b.o = 42
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
t.info('not in dev mode')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
@ -57,17 +32,6 @@ export const testSlice = tc => {
|
|||||||
t.compareArrays(arr.slice(0, 2), [0, 1])
|
t.compareArrays(arr.slice(0, 2), [0, 1])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {t.TestCase} tc
|
|
||||||
*/
|
|
||||||
export const testArrayFrom = tc => {
|
|
||||||
const doc1 = new Y.Doc()
|
|
||||||
const db1 = doc1.getMap('root')
|
|
||||||
const nestedArray1 = Y.Array.from([0, 1, 2])
|
|
||||||
db1.set('array', nestedArray1)
|
|
||||||
t.compare(nestedArray1.toArray(), [0, 1, 2])
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debugging yjs#297 - a critical bug connected to the search-marker approach
|
* Debugging yjs#297 - a critical bug connected to the search-marker approach
|
||||||
*
|
*
|
||||||
@ -355,29 +319,6 @@ export const testObserveDeepEventOrder = tc => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Correct index when computing event.path in observeDeep - https://github.com/yjs/yjs/issues/457
|
|
||||||
*
|
|
||||||
* @param {t.TestCase} _tc
|
|
||||||
*/
|
|
||||||
export const testObservedeepIndexes = _tc => {
|
|
||||||
const doc = new Y.Doc()
|
|
||||||
const map = doc.getMap()
|
|
||||||
// Create a field with the array as value
|
|
||||||
map.set('my-array', new Y.Array())
|
|
||||||
// Fill the array with some strings and our Map
|
|
||||||
map.get('my-array').push(['a', 'b', 'c', new Y.Map()])
|
|
||||||
/**
|
|
||||||
* @type {Array<any>}
|
|
||||||
*/
|
|
||||||
let eventPath = []
|
|
||||||
map.observeDeep((events) => { eventPath = events[0].path })
|
|
||||||
// set a value on the map inside of our array
|
|
||||||
map.get('my-array').get(3).set('hello', 'world')
|
|
||||||
console.log(eventPath)
|
|
||||||
t.compare(eventPath, ['my-array', 3])
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
|
@ -8,54 +8,6 @@ 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
|
||||||
*/
|
*/
|
||||||
@ -369,11 +321,11 @@ export const testObserversUsingObservedeep = tc => {
|
|||||||
/**
|
/**
|
||||||
* @type {Array<Array<string|number>>}
|
* @type {Array<Array<string|number>>}
|
||||||
*/
|
*/
|
||||||
const paths = []
|
const pathes = []
|
||||||
let calls = 0
|
let calls = 0
|
||||||
map0.observeDeep(events => {
|
map0.observeDeep(events => {
|
||||||
events.forEach(event => {
|
events.forEach(event => {
|
||||||
paths.push(event.path)
|
pathes.push(event.path)
|
||||||
})
|
})
|
||||||
calls++
|
calls++
|
||||||
})
|
})
|
||||||
@ -381,35 +333,7 @@ export const testObserversUsingObservedeep = tc => {
|
|||||||
map0.get('map').set('array', new Y.Array())
|
map0.get('map').set('array', new Y.Array())
|
||||||
map0.get('map').get('array').insert(0, ['content'])
|
map0.get('map').get('array').insert(0, ['content'])
|
||||||
t.assert(calls === 3)
|
t.assert(calls === 3)
|
||||||
t.compare(paths, [[], ['map'], ['map', 'array']])
|
t.compare(pathes, [[], ['map'], ['map', 'array']])
|
||||||
compare(users)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {t.TestCase} tc
|
|
||||||
*/
|
|
||||||
export const testPathsOfSiblingEvents = tc => {
|
|
||||||
const { users, map0 } = init(tc, { users: 2 })
|
|
||||||
/**
|
|
||||||
* @type {Array<Array<string|number>>}
|
|
||||||
*/
|
|
||||||
const paths = []
|
|
||||||
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 => {
|
|
||||||
paths.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(paths, [['map'], ['map', 'text1']])
|
|
||||||
compare(users)
|
compare(users)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -531,9 +455,9 @@ export const testChangeEvent = tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} _tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testYmapEventExceptionsShouldCompleteTransaction = _tc => {
|
export const testYmapEventExceptionsShouldCompleteTransaction = tc => {
|
||||||
const doc = new Y.Doc()
|
const doc = new Y.Doc()
|
||||||
const map = doc.getMap('map')
|
const map = doc.getMap('map')
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -3,33 +3,6 @@ import * as Y from '../src/index.js'
|
|||||||
|
|
||||||
import * as t from 'lib0/testing'
|
import * as t from 'lib0/testing'
|
||||||
|
|
||||||
export const testCustomTypings = () => {
|
|
||||||
const ydoc = new Y.Doc()
|
|
||||||
const ymap = ydoc.getMap()
|
|
||||||
/**
|
|
||||||
* @type {Y.XmlElement<{ num: number, str: string, [k:string]: object|number|string }>}
|
|
||||||
*/
|
|
||||||
const yxml = ymap.set('yxml', new Y.XmlElement('test'))
|
|
||||||
/**
|
|
||||||
* @type {number|undefined}
|
|
||||||
*/
|
|
||||||
const num = yxml.getAttribute('num')
|
|
||||||
/**
|
|
||||||
* @type {string|undefined}
|
|
||||||
*/
|
|
||||||
const str = yxml.getAttribute('str')
|
|
||||||
/**
|
|
||||||
* @type {object|number|string|undefined}
|
|
||||||
*/
|
|
||||||
const dtrn = yxml.getAttribute('dtrn')
|
|
||||||
const attrs = yxml.getAttributes()
|
|
||||||
/**
|
|
||||||
* @type {object|number|string|undefined}
|
|
||||||
*/
|
|
||||||
const any = attrs.shouldBeAny
|
|
||||||
console.log({ num, str, dtrn, attrs, any })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
@ -119,9 +92,9 @@ export const testTreewalker = tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} _tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testYtextAttributes = _tc => {
|
export const testYtextAttributes = tc => {
|
||||||
const ydoc = new Y.Doc()
|
const ydoc = new Y.Doc()
|
||||||
const ytext = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText))
|
const ytext = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText))
|
||||||
ytext.observe(event => {
|
ytext.observe(event => {
|
||||||
@ -133,9 +106,9 @@ export const testYtextAttributes = _tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} _tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testSiblings = _tc => {
|
export const testSiblings = tc => {
|
||||||
const ydoc = new Y.Doc()
|
const ydoc = new Y.Doc()
|
||||||
const yxml = ydoc.getXmlFragment()
|
const yxml = ydoc.getXmlFragment()
|
||||||
const first = new Y.XmlText()
|
const first = new Y.XmlText()
|
||||||
@ -149,9 +122,9 @@ export const testSiblings = _tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} _tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testInsertafter = _tc => {
|
export const testInsertafter = tc => {
|
||||||
const ydoc = new Y.Doc()
|
const ydoc = new Y.Doc()
|
||||||
const yxml = ydoc.getXmlFragment()
|
const yxml = ydoc.getXmlFragment()
|
||||||
const first = new Y.XmlText()
|
const first = new Y.XmlText()
|
||||||
@ -179,9 +152,9 @@ export const testInsertafter = _tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} _tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testClone = _tc => {
|
export const testClone = tc => {
|
||||||
const ydoc = new Y.Doc()
|
const ydoc = new Y.Doc()
|
||||||
const yxml = ydoc.getXmlFragment()
|
const yxml = ydoc.getXmlFragment()
|
||||||
const first = new Y.XmlText('text')
|
const first = new Y.XmlText('text')
|
||||||
@ -189,6 +162,7 @@ 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)
|
||||||
@ -196,9 +170,9 @@ export const testClone = _tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} _tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testFormattingBug = _tc => {
|
export const testFormattingBug = tc => {
|
||||||
const ydoc = new Y.Doc()
|
const ydoc = new Y.Doc()
|
||||||
const yxml = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText))
|
const yxml = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText))
|
||||||
const delta = [
|
const delta = [
|
||||||
@ -209,15 +183,3 @@ 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])
|
|
||||||
}
|
|
||||||
|
@ -1,21 +1,64 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2021",
|
/* Basic Options */
|
||||||
"lib": ["ES2021", "dom"],
|
"target": "es2018",
|
||||||
"module": "node16",
|
"lib": ["es2018", "dom"], /* Specify library files to be included in the compilation. */
|
||||||
"allowJs": true,
|
"allowJs": true, /* Allow javascript files to be compiled. */
|
||||||
"checkJs": true,
|
"checkJs": true, /* Report errors in .js files. */
|
||||||
"declaration": true,
|
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||||
"declarationMap": true,
|
"declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||||
"outDir": "./dist",
|
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||||
"baseUrl": "./",
|
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||||
|
// "outFile": "./dist/yjs.js", /* Concatenate and emit output to single file. */
|
||||||
|
"outDir": "./dist", /* Redirect output structure to the directory. */
|
||||||
|
"rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||||
|
// "composite": true, /* Enable project compilation */
|
||||||
|
// "removeComments": true, /* Do not emit comments to output. */
|
||||||
|
// "noEmit": true, /* Do not emit outputs. */
|
||||||
|
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||||
|
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||||
|
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||||
|
|
||||||
|
/* Strict Type-Checking Options */
|
||||||
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
|
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||||
"emitDeclarationOnly": true,
|
"emitDeclarationOnly": true,
|
||||||
"strict": true,
|
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||||
"noImplicitAny": true,
|
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||||
"moduleResolution": "nodenext",
|
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||||
|
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||||
|
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||||
|
|
||||||
|
/* Additional Checks */
|
||||||
|
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||||
|
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||||
|
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||||
|
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||||
|
|
||||||
|
/* Module Resolution Options */
|
||||||
|
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||||
|
"baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||||
"paths": {
|
"paths": {
|
||||||
"yjs": ["./src/index.js"]
|
"yjs": ["./src/index.js"]
|
||||||
}
|
}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||||
|
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||||
|
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||||
|
// "types": [], /* Type declaration files to be included in compilation. */
|
||||||
|
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||||
|
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||||
|
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||||
|
|
||||||
|
/* Source Map Options */
|
||||||
|
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||||
|
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||||
|
|
||||||
|
/* Experimental Options */
|
||||||
|
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||||
|
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||||
|
// "maxNodeModuleJsDepth": 0,
|
||||||
|
// "types": ["./src/utils/typedefs.js"]
|
||||||
},
|
},
|
||||||
"include": ["./src/**/*.js", "./tests/**/*.js"]
|
"include": ["./src/**/*.js", "./tests/**/*.js"]
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user