Compare commits
	
		
			No commits in common. "main" and "v13.5.53" have entirely different histories.
		
	
	
		
	
		
							
								
								
									
										8
									
								
								.github/workflows/node.js.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/node.js.yml
									
									
									
									
										vendored
									
									
								
							@ -16,16 +16,16 @@ jobs:
 | 
			
		||||
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        node-version: [16.x, 20.x]
 | 
			
		||||
        node-version: [16.x, 18.x]
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
    - uses: actions/checkout@v4
 | 
			
		||||
    - uses: actions/checkout@v2
 | 
			
		||||
    - name: Use Node.js ${{ matrix.node-version }}
 | 
			
		||||
      uses: actions/setup-node@v3
 | 
			
		||||
      uses: actions/setup-node@v1
 | 
			
		||||
      with:
 | 
			
		||||
        node-version: ${{ matrix.node-version }}
 | 
			
		||||
    - run: npm ci
 | 
			
		||||
    - run: npm run lint
 | 
			
		||||
    - run: npm run test
 | 
			
		||||
    - run: npm run test-extensive
 | 
			
		||||
      env:
 | 
			
		||||
        CI: true
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										10
									
								
								INTERNALS.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								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
 | 
			
		||||
random 53-bit integer (53 bits because that fits in the javascript safe integer
 | 
			
		||||
range \[JavaScript uses IEEE 754 floats\]).
 | 
			
		||||
range).
 | 
			
		||||
 | 
			
		||||
## List items
 | 
			
		||||
 | 
			
		||||
@ -60,7 +60,7 @@ 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
 | 
			
		||||
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`
 | 
			
		||||
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
 | 
			
		||||
@ -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
 | 
			
		||||
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
 | 
			
		||||
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
 | 
			
		||||
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
 | 
			
		||||
@ -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
 | 
			
		||||
  parsed to reconstruct the document. Also every change on the document fires
 | 
			
		||||
an incremental document update that allows clients to sync with each other.
 | 
			
		||||
The update object is a Uint8Array that efficiently encodes `Item` objects and
 | 
			
		||||
an incremental document updates that allows clients to sync with each other.
 | 
			
		||||
The update object is an Uint8Array that efficiently encodes `Item` objects and
 | 
			
		||||
the delete set.
 | 
			
		||||
* `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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								LICENSE
									
									
									
									
									
								
							@ -1,7 +1,7 @@
 | 
			
		||||
The MIT License (MIT)
 | 
			
		||||
 | 
			
		||||
Copyright (c) 2023
 | 
			
		||||
  - Kevin Jahns <kevin.jahns@protonmail.com>.
 | 
			
		||||
Copyright (c) 2014
 | 
			
		||||
  - Kevin Jahns <kevin.jahns@rwth-aachen.de>.
 | 
			
		||||
  - 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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										301
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										301
									
								
								README.md
									
									
									
									
									
								
							@ -3,7 +3,7 @@
 | 
			
		||||
 | 
			
		||||
> 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`
 | 
			
		||||
or `Array` with superpowers: changes are automatically distributed to other
 | 
			
		||||
peers and merged without merge conflicts.
 | 
			
		||||
@ -32,34 +32,13 @@ Otherwise you can find help on our community [discussion board](https://discuss.
 | 
			
		||||
Please contribute to the project financially - especially if your company relies
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
* [AFFiNE](https://affine.pro/) A local-first, privacy-first, open source
 | 
			
		||||
  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:
 | 
			
		||||
  knowledge base. 🏅
 | 
			
		||||
* [Dynaboard](https://dynaboard.com/) Build web apps collaboratively. :star2:
 | 
			
		||||
* [Sana](https://sanalabs.com/) A learning platform with collaborative text
 | 
			
		||||
  editing powered by Yjs.
 | 
			
		||||
* [Relm](https://www.relm.us/) A collaborative gameworld for teamwork and
 | 
			
		||||
  community. :star:
 | 
			
		||||
* [Room.sh](https://room.sh/) A meeting application with integrated
 | 
			
		||||
@ -68,78 +47,34 @@ Showcase](https://yjs-diagram.synergy.codes/).
 | 
			
		||||
  Nimbus Web. :star:
 | 
			
		||||
* [Pluxbox RadioManager](https://getradiomanager.com/) A web-based app to
 | 
			
		||||
  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
 | 
			
		||||
  collaborative notes app.
 | 
			
		||||
* [PRSM](https://prsm.uk/) Collaborative mind-mapping and system visualisation.
 | 
			
		||||
  *[(source)](https://github.com/micrology/prsm)*
 | 
			
		||||
* [PRSM](https://prsm.uk/) Collaborative mind-mapping and system visualisation. *[(source)](https://github.com/micrology/prsm)*
 | 
			
		||||
* [Alldone](https://alldone.app/) A next-gen project management and
 | 
			
		||||
  collaboration platform.
 | 
			
		||||
* [Living Spec](https://livingspec.com/) A modern way for product teams to collaborate.
 | 
			
		||||
* [Slidebeamer](https://slidebeamer.com/) Presentation app.
 | 
			
		||||
* [BlockSurvey](https://blocksurvey.io) End-to-end encryption for your forms/surveys.
 | 
			
		||||
* [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
 | 
			
		||||
 | 
			
		||||
* [Overview](#overview)
 | 
			
		||||
  * [Bindings](#bindings)
 | 
			
		||||
  * [Providers](#providers)
 | 
			
		||||
  * [Tooling](#tooling)
 | 
			
		||||
  * [Ports](#ports)
 | 
			
		||||
* [Getting Started](#getting-started)
 | 
			
		||||
* [API](#api)
 | 
			
		||||
  * [Shared Types](#shared-types)
 | 
			
		||||
  * [Y.Doc](#ydoc)
 | 
			
		||||
  * [Document Updates](#document-updates)
 | 
			
		||||
  * [Relative Positions](#relative-positions)
 | 
			
		||||
  * [Y.UndoManager](#yundomanager)
 | 
			
		||||
* [Yjs CRDT Algorithm](#yjs-crdt-algorithm)
 | 
			
		||||
* [License and Author](#license-and-author)
 | 
			
		||||
* [Overview](#Overview)
 | 
			
		||||
  * [Bindings](#Bindings)
 | 
			
		||||
  * [Providers](#Providers)
 | 
			
		||||
* [Getting Started](#Getting-Started)
 | 
			
		||||
* [API](#API)
 | 
			
		||||
  * [Shared Types](#Shared-Types)
 | 
			
		||||
  * [Y.Doc](#YDoc)
 | 
			
		||||
  * [Document Updates](#Document-Updates)
 | 
			
		||||
  * [Relative Positions](#Relative-Positions)
 | 
			
		||||
  * [Y.UndoManager](#YUndoManager)
 | 
			
		||||
* [Yjs CRDT Algorithm](#Yjs-CRDT-Algorithm)
 | 
			
		||||
* [License and Author](#License-and-Author)
 | 
			
		||||
 | 
			
		||||
## Overview
 | 
			
		||||
 | 
			
		||||
@ -156,15 +91,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) |
 | 
			
		||||
| [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) |
 | 
			
		||||
| [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) |
 | 
			
		||||
| [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) |
 | 
			
		||||
| [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
 | 
			
		||||
 | 
			
		||||
@ -173,56 +102,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
 | 
			
		||||
collaborative app.
 | 
			
		||||
 | 
			
		||||
> This list of providers is incomplete. Please open PRs to add your providers to
 | 
			
		||||
> this list!
 | 
			
		||||
 | 
			
		||||
#### Connection Providers
 | 
			
		||||
 | 
			
		||||
<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>
 | 
			
		||||
  <dd>
 | 
			
		||||
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
 | 
			
		||||
providing a shared secret, keeping the connection information and the shared
 | 
			
		||||
document private.
 | 
			
		||||
  </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>
 | 
			
		||||
<a href="https://liveblocks.io/document/yjs">Liveblocks Yjs</a> provides a fully
 | 
			
		||||
hosted WebSocket infrastructure and persisted data store for Yjs
 | 
			
		||||
documents. No configuration or maintenance is required. It also features
 | 
			
		||||
Yjs webhook events, REST API to read and update Yjs documents, and a
 | 
			
		||||
browser DevTools extension.
 | 
			
		||||
A module that contains a simple websocket backend and a websocket client that
 | 
			
		||||
connects to that backend. The backend can be extended to persist updates in a
 | 
			
		||||
leveldb database.
 | 
			
		||||
  </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>
 | 
			
		||||
A standalone yjs server with persistence to S3 or filesystem. They offer a
 | 
			
		||||
<a href="https://y-sweet.cloud">cloud service</a> as well.
 | 
			
		||||
  </dd>
 | 
			
		||||
  <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.
 | 
			
		||||
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/marcopolo/y-libp2p">y-libp2p</a></dt>
 | 
			
		||||
  <dd>
 | 
			
		||||
@ -245,90 +144,14 @@ Use Matrix as transport and storage of Yjs updates, so you can focus building
 | 
			
		||||
your client app and Matrix can provide powerful features like Authentication,
 | 
			
		||||
Authorization, Federation, hosting (self-hosting or SaaS) and even End-to-End
 | 
			
		||||
Encryption (E2EE).
 | 
			
		||||
  </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>
 | 
			
		||||
 | 
			
		||||
#### 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>
 | 
			
		||||
</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>
 | 
			
		||||
</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
 | 
			
		||||
 | 
			
		||||
Install Yjs and a provider with your favorite package manager:
 | 
			
		||||
@ -340,15 +163,12 @@ npm i yjs y-websocket
 | 
			
		||||
Start the y-websocket server:
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
PORT=1234 node ./node_modules/y-websocket/bin/server.cjs
 | 
			
		||||
PORT=1234 node ./node_modules/y-websocket/bin/server.js
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Example: Observe types
 | 
			
		||||
 | 
			
		||||
```js
 | 
			
		||||
import * as Y from 'yjs';
 | 
			
		||||
 | 
			
		||||
const doc = new Y.Doc();
 | 
			
		||||
const yarray = doc.getArray('my-array')
 | 
			
		||||
yarray.observe(event => {
 | 
			
		||||
  console.log('yarray was modified')
 | 
			
		||||
@ -441,11 +261,6 @@ necessary.
 | 
			
		||||
  </p>
 | 
			
		||||
  <pre>const yarray = new Y.Array()</pre>
 | 
			
		||||
  <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>
 | 
			
		||||
    <dd></dd>
 | 
			
		||||
    <b><code>insert(index:number, content:Array<object|boolean|Array|string|number|null|Uint8Array|Y.Type>)</code></b>
 | 
			
		||||
@ -475,11 +290,6 @@ forEach(function(value:object|boolean|Array|string|number|null|Uint8Array|Y.Type
 | 
			
		||||
    <dd></dd>
 | 
			
		||||
    <b><code>map(function(T, number, YArray):M):Array<M></code></b>
 | 
			
		||||
    <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>
 | 
			
		||||
    <dd>Copies the content of this YArray to a new Array.</dd>
 | 
			
		||||
    <b><code>toJSON():Array<Object|boolean|Array|string|number|null></code></b>
 | 
			
		||||
@ -536,6 +346,8 @@ or any of its children.
 | 
			
		||||
    <dd></dd>
 | 
			
		||||
    <b><code>has(key:string):boolean</code></b>
 | 
			
		||||
    <dd></dd>
 | 
			
		||||
    <b><code>get(index:number)</code></b>
 | 
			
		||||
    <dd></dd>
 | 
			
		||||
    <b><code>clear()</code></b>
 | 
			
		||||
    <dd>Removes all elements from this YMap.</dd>
 | 
			
		||||
    <b><code>clone():Y.Map</code></b>
 | 
			
		||||
@ -832,8 +644,6 @@ type. Doesn't log types that have not been defined (using
 | 
			
		||||
  <dd>Define a shared Y.Map type. Is equivalent to <code>y.get(string, Y.Map)</code>.</dd>
 | 
			
		||||
  <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>
 | 
			
		||||
  <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>
 | 
			
		||||
@ -848,8 +658,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>
 | 
			
		||||
  <dd>
 | 
			
		||||
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`
 | 
			
		||||
to receive V2 events.
 | 
			
		||||
peers. You can apply document updates in any order and multiple times.
 | 
			
		||||
  </dd>
 | 
			
		||||
  <b><code>on('beforeTransaction', function(Y.Transaction, Y.Doc):void)</code></b>
 | 
			
		||||
  <dd>Emitted before each transaction.</dd>
 | 
			
		||||
@ -889,7 +698,7 @@ doc1.getArray('myarray').insert(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
 | 
			
		||||
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
 | 
			
		||||
@ -941,43 +750,17 @@ const diff2 = Y.diffUpdate(currentState2, stateVector1)
 | 
			
		||||
 | 
			
		||||
// sync clients
 | 
			
		||||
currentState1 = Y.mergeUpdates([currentState1, diff2])
 | 
			
		||||
currentState2 = Y.mergeUpdates([currentState2, 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"
 | 
			
		||||
currentState1 = Y.mergeUpdates([currentState1, diff1])
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### Using V2 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
 | 
			
		||||
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
 | 
			
		||||
you need to specifically need listen for V2 events e.g. `yDoc.on('updateV2', …)`.
 | 
			
		||||
We also support conversion functions between both formats:
 | 
			
		||||
`Y.convertUpdateFormatV1ToV2` & `Y.convertUpdateFormatV2ToV1`.
 | 
			
		||||
suffix "V2". E.g. `Y.applyUpdate` ⇒ `Y.applyUpdateV2`. We also support conversion
 | 
			
		||||
functions between both formats: `Y.convertUpdateFormatV1ToV2` & `Y.convertUpdateFormatV2ToV1`.
 | 
			
		||||
 | 
			
		||||
#### Update API
 | 
			
		||||
 | 
			
		||||
@ -1102,7 +885,7 @@ encoding format for document updates. If you prefer JSON encoding, you can
 | 
			
		||||
simply JSON.stringify / JSON.parse the relative position instead.
 | 
			
		||||
  </dd>
 | 
			
		||||
  <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>
 | 
			
		||||
 | 
			
		||||
### Y.UndoManager
 | 
			
		||||
@ -1222,7 +1005,7 @@ doc.transact(() => {
 | 
			
		||||
  ytext.insert(0, 'abc')
 | 
			
		||||
}, 41)
 | 
			
		||||
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`)
 | 
			
		||||
ytext.delete(0, 3) // revert change
 | 
			
		||||
 | 
			
		||||
@ -1282,11 +1065,11 @@ More information about the specific implementation is available in
 | 
			
		||||
 | 
			
		||||
CRDTs that are suitable for shared text editing suffer from the fact that they
 | 
			
		||||
only grow in size. There are CRDTs that do not grow in size, but they do not
 | 
			
		||||
have the characteristics that are beneficial for shared text editing (like
 | 
			
		||||
have the characteristics that are benificial for shared text editing (like
 | 
			
		||||
intention preservation). Yjs implements many improvements to the original
 | 
			
		||||
algorithm that diminish the trade-off that the document only grows in size. We
 | 
			
		||||
can't garbage collect deleted structs (tombstones) while ensuring a unique
 | 
			
		||||
order of the structs. But we can 1. merge preceding structs into a single
 | 
			
		||||
order of the structs. But we can 1. merge preceeding structs into a single
 | 
			
		||||
struct to reduce the amount of meta information, 2. we can delete content from
 | 
			
		||||
the struct if it is deleted, and 3. we can garbage collect tombstones if we
 | 
			
		||||
don't care about the order of the structs anymore (e.g. if the parent was
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1744
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1744
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										11
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								package.json
									
									
									
									
									
								
							@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "yjs",
 | 
			
		||||
  "version": "13.6.24",
 | 
			
		||||
  "version": "13.5.53",
 | 
			
		||||
  "description": "Shared Editing Library",
 | 
			
		||||
  "main": "./dist/yjs.cjs",
 | 
			
		||||
  "module": "./dist/yjs.mjs",
 | 
			
		||||
@ -12,10 +12,9 @@
 | 
			
		||||
    "url": "https://github.com/sponsors/dmonad"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "clean": "rm -rf dist docs",
 | 
			
		||||
    "test": "npm run dist && NODE_ENV=development node ./dist/tests.cjs --repetition-time 50",
 | 
			
		||||
    "test": "npm run dist && node ./dist/tests.cjs --repetition-time 50",
 | 
			
		||||
    "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",
 | 
			
		||||
    "lint": "markdownlint README.md && standard && tsc",
 | 
			
		||||
    "docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true",
 | 
			
		||||
@ -76,7 +75,7 @@
 | 
			
		||||
  },
 | 
			
		||||
  "homepage": "https://docs.yjs.dev",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "lib0": "^0.2.99"
 | 
			
		||||
    "lib0": "^0.2.72"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@rollup/plugin-commonjs": "^24.0.1",
 | 
			
		||||
@ -85,7 +84,7 @@
 | 
			
		||||
    "concurrently": "^3.6.1",
 | 
			
		||||
    "http-server": "^0.12.3",
 | 
			
		||||
    "jsdoc": "^3.6.7",
 | 
			
		||||
    "markdownlint-cli": "^0.41.0",
 | 
			
		||||
    "markdownlint-cli": "^0.23.2",
 | 
			
		||||
    "rollup": "^3.20.0",
 | 
			
		||||
    "standard": "^16.0.4",
 | 
			
		||||
    "tui-jsdoc-template": "^1.2.2",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										15
									
								
								src/index.js
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								src/index.js
									
									
									
									
									
								
							@ -18,10 +18,8 @@ export {
 | 
			
		||||
  Item,
 | 
			
		||||
  AbstractStruct,
 | 
			
		||||
  GC,
 | 
			
		||||
  Skip,
 | 
			
		||||
  ContentBinary,
 | 
			
		||||
  ContentDeleted,
 | 
			
		||||
  ContentDoc,
 | 
			
		||||
  ContentEmbed,
 | 
			
		||||
  ContentFormat,
 | 
			
		||||
  ContentJSON,
 | 
			
		||||
@ -50,11 +48,8 @@ export {
 | 
			
		||||
  findRootTypeKey,
 | 
			
		||||
  findIndexSS,
 | 
			
		||||
  getItem,
 | 
			
		||||
  getItemCleanStart,
 | 
			
		||||
  getItemCleanEnd,
 | 
			
		||||
  typeListToArraySnapshot,
 | 
			
		||||
  typeMapGetSnapshot,
 | 
			
		||||
  typeMapGetAllSnapshot,
 | 
			
		||||
  createDocFromSnapshot,
 | 
			
		||||
  iterateDeletedStructs,
 | 
			
		||||
  applyUpdate,
 | 
			
		||||
@ -95,15 +90,7 @@ export {
 | 
			
		||||
  diffUpdateV2,
 | 
			
		||||
  convertUpdateFormatV1ToV2,
 | 
			
		||||
  convertUpdateFormatV2ToV1,
 | 
			
		||||
  obfuscateUpdate,
 | 
			
		||||
  obfuscateUpdateV2,
 | 
			
		||||
  UpdateEncoderV1,
 | 
			
		||||
  UpdateEncoderV2,
 | 
			
		||||
  UpdateDecoderV1,
 | 
			
		||||
  UpdateDecoderV2,
 | 
			
		||||
  equalDeleteSets,
 | 
			
		||||
  mergeDeleteSets,
 | 
			
		||||
  snapshotContainsUpdate
 | 
			
		||||
  UpdateEncoderV1
 | 
			
		||||
} from './internals.js'
 | 
			
		||||
 | 
			
		||||
const glo = /** @type {any} */ (typeof globalThis !== 'undefined'
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
 | 
			
		||||
export * from './utils/AbstractConnector.js'
 | 
			
		||||
export * from './utils/DeleteSet.js'
 | 
			
		||||
export * from './utils/Doc.js'
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  UpdateEncoderV1, UpdateEncoderV2, ID, Transaction // eslint-disable-line
 | 
			
		||||
} from '../internals.js'
 | 
			
		||||
@ -26,7 +27,7 @@ export class AbstractStruct {
 | 
			
		||||
   * This method is already assuming that `this.id.clock + this.length === this.id.clock`.
 | 
			
		||||
   * Also this method does *not* remove right from StructStore!
 | 
			
		||||
   * @param {AbstractStruct} right
 | 
			
		||||
   * @return {boolean} whether this merged with right
 | 
			
		||||
   * @return {boolean} wether this merged with right
 | 
			
		||||
   */
 | 
			
		||||
  mergeWith (right) {
 | 
			
		||||
    return false
 | 
			
		||||
 | 
			
		||||
@ -2,11 +2,6 @@ import {
 | 
			
		||||
  UpdateEncoderV1, UpdateEncoderV2, UpdateDecoderV1, UpdateDecoderV2, Transaction, Item, StructStore // eslint-disable-line
 | 
			
		||||
} 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 {
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {Array<any>} arr
 | 
			
		||||
@ -16,7 +11,6 @@ export class ContentAny {
 | 
			
		||||
     * @type {Array<any>}
 | 
			
		||||
     */
 | 
			
		||||
    this.arr = arr
 | 
			
		||||
    isDevMode && object.deepFreeze(arr)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  addToDeleteSet,
 | 
			
		||||
  UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  Doc, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item // eslint-disable-line
 | 
			
		||||
} from '../internals.js'
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line
 | 
			
		||||
} from '../internals.js'
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
 | 
			
		||||
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'
 | 
			
		||||
 | 
			
		||||
import * as error from 'lib0/error'
 | 
			
		||||
@ -46,30 +47,28 @@ export class ContentFormat {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {number} _offset
 | 
			
		||||
   * @param {number} offset
 | 
			
		||||
   * @return {ContentFormat}
 | 
			
		||||
   */
 | 
			
		||||
  splice (_offset) {
 | 
			
		||||
  splice (offset) {
 | 
			
		||||
    throw error.methodUnimplemented()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {ContentFormat} _right
 | 
			
		||||
   * @param {ContentFormat} right
 | 
			
		||||
   * @return {boolean}
 | 
			
		||||
   */
 | 
			
		||||
  mergeWith (_right) {
 | 
			
		||||
  mergeWith (right) {
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {Transaction} _transaction
 | 
			
		||||
   * @param {Transaction} transaction
 | 
			
		||||
   * @param {Item} item
 | 
			
		||||
   */
 | 
			
		||||
  integrate (_transaction, item) {
 | 
			
		||||
  integrate (transaction, item) {
 | 
			
		||||
    // @todo searchmarker are currently unsupported for rich text documents
 | 
			
		||||
    const p = /** @type {YText} */ (item.parent)
 | 
			
		||||
    p._searchMarker = null
 | 
			
		||||
    p._hasFormatting = true
 | 
			
		||||
    /** @type {AbstractType<any>} */ (item.parent)._searchMarker = null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  readYArray,
 | 
			
		||||
  readYMap,
 | 
			
		||||
@ -107,7 +108,7 @@ export class ContentType {
 | 
			
		||||
    while (item !== null) {
 | 
			
		||||
      if (!item.deleted) {
 | 
			
		||||
        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
 | 
			
		||||
        // We try to merge all deleted items after each transaction,
 | 
			
		||||
        // but we have no knowledge about that this needs to be merged
 | 
			
		||||
@ -119,7 +120,7 @@ export class ContentType {
 | 
			
		||||
    this.type._map.forEach(item => {
 | 
			
		||||
      if (!item.deleted) {
 | 
			
		||||
        item.delete(transaction)
 | 
			
		||||
      } else if (item.id.clock < (transaction.beforeState.get(item.id.client) || 0)) {
 | 
			
		||||
      } else {
 | 
			
		||||
        // same as above
 | 
			
		||||
        transaction._mergeStructs.push(item)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  AbstractStruct,
 | 
			
		||||
  addStruct,
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  GC,
 | 
			
		||||
  getState,
 | 
			
		||||
@ -388,12 +389,14 @@ export class Item extends AbstractStruct {
 | 
			
		||||
    }
 | 
			
		||||
    if ((this.left && this.left.constructor === GC) || (this.right && this.right.constructor === GC)) {
 | 
			
		||||
      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) {
 | 
			
		||||
        this.parent = this.left.parent
 | 
			
		||||
        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.parentSub = this.right.parentSub
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  AbstractStruct,
 | 
			
		||||
  UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, ID // eslint-disable-line
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  removeEventHandlerListener,
 | 
			
		||||
  callEventHandlerListeners,
 | 
			
		||||
@ -17,12 +18,6 @@ import * as map from 'lib0/map'
 | 
			
		||||
import * as iterator from 'lib0/iterator'
 | 
			
		||||
import * as error from 'lib0/error'
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
@ -155,11 +150,11 @@ export const findMarker = (yarray, index) => {
 | 
			
		||||
  //   }
 | 
			
		||||
  // }
 | 
			
		||||
  // if (marker) {
 | 
			
		||||
  //   if (window.lengths == null) {
 | 
			
		||||
  //     window.lengths = []
 | 
			
		||||
  //     window.getLengths = () => window.lengths.sort((a, b) => a - b)
 | 
			
		||||
  //   if (window.lengthes == null) {
 | 
			
		||||
  //     window.lengthes = []
 | 
			
		||||
  //     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)
 | 
			
		||||
  // }
 | 
			
		||||
  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>}
 | 
			
		||||
 */
 | 
			
		||||
export const getTypeChildren = t => {
 | 
			
		||||
  t.doc ?? warnPrematureAccess()
 | 
			
		||||
  let s = t._start
 | 
			
		||||
  const arr = []
 | 
			
		||||
  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>}
 | 
			
		||||
   */
 | 
			
		||||
  clone () {
 | 
			
		||||
@ -415,7 +405,6 @@ export class AbstractType {
 | 
			
		||||
 * @function
 | 
			
		||||
 */
 | 
			
		||||
export const typeListSlice = (type, start, end) => {
 | 
			
		||||
  type.doc ?? warnPrematureAccess()
 | 
			
		||||
  if (start < 0) {
 | 
			
		||||
    start = type._length + start
 | 
			
		||||
  }
 | 
			
		||||
@ -451,7 +440,6 @@ export const typeListSlice = (type, start, end) => {
 | 
			
		||||
 * @function
 | 
			
		||||
 */
 | 
			
		||||
export const typeListToArray = type => {
 | 
			
		||||
  type.doc ?? warnPrematureAccess()
 | 
			
		||||
  const cs = []
 | 
			
		||||
  let n = type._start
 | 
			
		||||
  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 {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) => {
 | 
			
		||||
  let index = 0
 | 
			
		||||
  let n = type._start
 | 
			
		||||
  type.doc ?? warnPrematureAccess()
 | 
			
		||||
  while (n !== null) {
 | 
			
		||||
    if (n.countable && !n.deleted) {
 | 
			
		||||
      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.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {AbstractType<any>} type
 | 
			
		||||
@ -616,7 +603,6 @@ export const typeListForEachSnapshot = (type, f, snapshot) => {
 | 
			
		||||
 * @function
 | 
			
		||||
 */
 | 
			
		||||
export const typeListGet = (type, index) => {
 | 
			
		||||
  type.doc ?? warnPrematureAccess()
 | 
			
		||||
  const marker = findMarker(type, index)
 | 
			
		||||
  let n = type._start
 | 
			
		||||
  if (marker !== null) {
 | 
			
		||||
@ -697,7 +683,7 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
 | 
			
		||||
  packJsonContent()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const lengthExceeded = () => error.create('Length exceeded!')
 | 
			
		||||
const lengthExceeded = error.create('Length exceeded!')
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {Transaction} transaction
 | 
			
		||||
@ -710,7 +696,7 @@ const lengthExceeded = () => error.create('Length exceeded!')
 | 
			
		||||
 */
 | 
			
		||||
export const typeListInsertGenerics = (transaction, parent, index, content) => {
 | 
			
		||||
  if (index > parent._length) {
 | 
			
		||||
    throw lengthExceeded()
 | 
			
		||||
    throw lengthExceeded
 | 
			
		||||
  }
 | 
			
		||||
  if (index === 0) {
 | 
			
		||||
    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
 | 
			
		||||
 * the search marker.
 | 
			
		||||
 * the serach marker.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {Transaction} transaction
 | 
			
		||||
 * @param {AbstractType<any>} parent
 | 
			
		||||
@ -812,7 +798,7 @@ export const typeListDelete = (transaction, parent, index, length) => {
 | 
			
		||||
    n = n.right
 | 
			
		||||
  }
 | 
			
		||||
  if (length > 0) {
 | 
			
		||||
    throw lengthExceeded()
 | 
			
		||||
    throw lengthExceeded
 | 
			
		||||
  }
 | 
			
		||||
  if (parent._searchMarker) {
 | 
			
		||||
    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
 | 
			
		||||
 */
 | 
			
		||||
export const typeMapGet = (parent, key) => {
 | 
			
		||||
  parent.doc ?? warnPrematureAccess()
 | 
			
		||||
  const val = parent._map.get(key)
 | 
			
		||||
  return val !== undefined && !val.deleted ? val.content.getContent()[val.length - 1] : undefined
 | 
			
		||||
}
 | 
			
		||||
@ -902,7 +887,6 @@ export const typeMapGetAll = (parent) => {
 | 
			
		||||
   * @type {Object<string,any>}
 | 
			
		||||
   */
 | 
			
		||||
  const res = {}
 | 
			
		||||
  parent.doc ?? warnPrematureAccess()
 | 
			
		||||
  parent._map.forEach((value, key) => {
 | 
			
		||||
    if (!value.deleted) {
 | 
			
		||||
      res[key] = value.content.getContent()[value.length - 1]
 | 
			
		||||
@ -920,7 +904,6 @@ export const typeMapGetAll = (parent) => {
 | 
			
		||||
 * @function
 | 
			
		||||
 */
 | 
			
		||||
export const typeMapHas = (parent, key) => {
 | 
			
		||||
  parent.doc ?? warnPrematureAccess()
 | 
			
		||||
  const val = parent._map.get(key)
 | 
			
		||||
  return val !== undefined && !val.deleted
 | 
			
		||||
}
 | 
			
		||||
@ -943,41 +926,10 @@ export const typeMapGetSnapshot = (parent, key, snapshot) => {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {AbstractType<any>} parent
 | 
			
		||||
 * @param {Snapshot} snapshot
 | 
			
		||||
 * @return {Object<string,Object<string,any>|number|null|Array<any>|string|Uint8Array|AbstractType<any>|undefined>}
 | 
			
		||||
 *
 | 
			
		||||
 * @private
 | 
			
		||||
 * @function
 | 
			
		||||
 */
 | 
			
		||||
export const typeMapGetAllSnapshot = (parent, snapshot) => {
 | 
			
		||||
  /**
 | 
			
		||||
   * @type {Object<string,any>}
 | 
			
		||||
   */
 | 
			
		||||
  const res = {}
 | 
			
		||||
  parent._map.forEach((value, key) => {
 | 
			
		||||
    /**
 | 
			
		||||
     * @type {Item|null}
 | 
			
		||||
     */
 | 
			
		||||
    let v = value
 | 
			
		||||
    while (v !== null && (!snapshot.sv.has(v.id.client) || v.id.clock >= (snapshot.sv.get(v.id.client) || 0))) {
 | 
			
		||||
      v = v.left
 | 
			
		||||
    }
 | 
			
		||||
    if (v !== null && isVisible(v, snapshot)) {
 | 
			
		||||
      res[key] = v.content.getContent()[v.length - 1]
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
  return res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {AbstractType<any> & { _map: Map<string, Item> }} type
 | 
			
		||||
 * @param {Map<string,Item>} map
 | 
			
		||||
 * @return {IterableIterator<Array<any>>}
 | 
			
		||||
 *
 | 
			
		||||
 * @private
 | 
			
		||||
 * @function
 | 
			
		||||
 */
 | 
			
		||||
export const createMapIterator = type => {
 | 
			
		||||
  type.doc ?? warnPrematureAccess()
 | 
			
		||||
  return iterator.iteratorFilter(type._map.entries(), /** @param {any} entry */ entry => !entry[1].deleted)
 | 
			
		||||
}
 | 
			
		||||
export const createMapIterator = map => iterator.iteratorFilter(map.entries(), /** @param {any} entry */ entry => !entry[1].deleted)
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,6 @@ import {
 | 
			
		||||
  YArrayRefID,
 | 
			
		||||
  callTypeObservers,
 | 
			
		||||
  transact,
 | 
			
		||||
  warnPrematureAccess,
 | 
			
		||||
  ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
 | 
			
		||||
} from '../internals.js'
 | 
			
		||||
import { typeListSlice } from './AbstractType.js'
 | 
			
		||||
@ -26,7 +25,16 @@ import { typeListSlice } from './AbstractType.js'
 | 
			
		||||
 * @template 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.
 | 
			
		||||
@ -87,10 +95,6 @@ export class YArray 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 {YArray<T>}
 | 
			
		||||
   */
 | 
			
		||||
  clone () {
 | 
			
		||||
@ -105,8 +109,7 @@ export class YArray extends AbstractType {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get length () {
 | 
			
		||||
    this.doc ?? warnPrematureAccess()
 | 
			
		||||
    return this._length
 | 
			
		||||
    return this._prelimContent === null ? this._length : this._prelimContent.length
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -164,9 +167,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) {
 | 
			
		||||
    this.insert(0, content)
 | 
			
		||||
@ -208,8 +211,7 @@ export class YArray extends AbstractType {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns a portion of this YArray into a JavaScript Array selected
 | 
			
		||||
   * from start to end (end not included).
 | 
			
		||||
   * Transforms this YArray to a JavaScript Array.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {number} [start]
 | 
			
		||||
   * @param {number} [end]
 | 
			
		||||
@ -242,7 +244,7 @@ export class YArray extends AbstractType {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Executes a provided function once on every element of this YArray.
 | 
			
		||||
   * Executes a provided function 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.
 | 
			
		||||
   */
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @module YMap
 | 
			
		||||
 */
 | 
			
		||||
@ -13,7 +14,6 @@ import {
 | 
			
		||||
  YMapRefID,
 | 
			
		||||
  callTypeObservers,
 | 
			
		||||
  transact,
 | 
			
		||||
  warnPrematureAccess,
 | 
			
		||||
  UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
 | 
			
		||||
} from '../internals.js'
 | 
			
		||||
 | 
			
		||||
@ -41,7 +41,7 @@ export class YMapEvent extends YEvent {
 | 
			
		||||
 * A shared Map implementation.
 | 
			
		||||
 *
 | 
			
		||||
 * @extends AbstractType<YMapEvent<MapType>>
 | 
			
		||||
 * @implements {Iterable<[string, MapType]>}
 | 
			
		||||
 * @implements {Iterable<MapType>}
 | 
			
		||||
 */
 | 
			
		||||
export class YMap extends AbstractType {
 | 
			
		||||
  /**
 | 
			
		||||
@ -89,10 +89,6 @@ export class YMap 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 {YMap<MapType>}
 | 
			
		||||
   */
 | 
			
		||||
  clone () {
 | 
			
		||||
@ -122,7 +118,6 @@ export class YMap extends AbstractType {
 | 
			
		||||
   * @return {Object<string,any>}
 | 
			
		||||
   */
 | 
			
		||||
  toJSON () {
 | 
			
		||||
    this.doc ?? warnPrematureAccess()
 | 
			
		||||
    /**
 | 
			
		||||
     * @type {Object<string,MapType>}
 | 
			
		||||
     */
 | 
			
		||||
@ -142,7 +137,7 @@ export class YMap extends AbstractType {
 | 
			
		||||
   * @return {number}
 | 
			
		||||
   */
 | 
			
		||||
  get size () {
 | 
			
		||||
    return [...createMapIterator(this)].length
 | 
			
		||||
    return [...createMapIterator(this._map)].length
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -151,25 +146,25 @@ export class YMap extends AbstractType {
 | 
			
		||||
   * @return {IterableIterator<string>}
 | 
			
		||||
   */
 | 
			
		||||
  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.
 | 
			
		||||
   *
 | 
			
		||||
   * @return {IterableIterator<MapType>}
 | 
			
		||||
   * @return {IterableIterator<any>}
 | 
			
		||||
   */
 | 
			
		||||
  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
 | 
			
		||||
   *
 | 
			
		||||
   * @return {IterableIterator<[string, MapType]>}
 | 
			
		||||
   * @return {IterableIterator<any>}
 | 
			
		||||
   */
 | 
			
		||||
  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,7 +173,6 @@ export class YMap extends AbstractType {
 | 
			
		||||
   * @param {function(MapType,string,YMap<MapType>):void} f A function to execute on every element of this YArray.
 | 
			
		||||
   */
 | 
			
		||||
  forEach (f) {
 | 
			
		||||
    this.doc ?? warnPrematureAccess()
 | 
			
		||||
    this._map.forEach((item, key) => {
 | 
			
		||||
      if (!item.deleted) {
 | 
			
		||||
        f(item.content.getContent()[item.length - 1], key, this)
 | 
			
		||||
@ -189,7 +183,7 @@ export class YMap extends AbstractType {
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns an Iterator of [key, value] pairs
 | 
			
		||||
   *
 | 
			
		||||
   * @return {IterableIterator<[string, MapType]>}
 | 
			
		||||
   * @return {IterableIterator<any>}
 | 
			
		||||
   */
 | 
			
		||||
  [Symbol.iterator] () {
 | 
			
		||||
    return this.entries()
 | 
			
		||||
@ -212,11 +206,9 @@ export class YMap extends AbstractType {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 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 {VAL} value The value of the element to add
 | 
			
		||||
   * @return {VAL}
 | 
			
		||||
   * @param {MapType} value The value of the element to add
 | 
			
		||||
   */
 | 
			
		||||
  set (key, value) {
 | 
			
		||||
    if (this.doc !== null) {
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @module YText
 | 
			
		||||
 */
 | 
			
		||||
@ -26,7 +27,6 @@ import {
 | 
			
		||||
  typeMapGetAll,
 | 
			
		||||
  updateMarkerChanges,
 | 
			
		||||
  ContentType,
 | 
			
		||||
  warnPrematureAccess,
 | 
			
		||||
  ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction // eslint-disable-line
 | 
			
		||||
} from '../internals.js'
 | 
			
		||||
 | 
			
		||||
@ -118,15 +118,14 @@ const findNextPosition = (transaction, pos, count) => {
 | 
			
		||||
 * @param {Transaction} transaction
 | 
			
		||||
 * @param {AbstractType<any>} parent
 | 
			
		||||
 * @param {number} index
 | 
			
		||||
 * @param {boolean} useSearchMarker
 | 
			
		||||
 * @return {ItemTextListPosition}
 | 
			
		||||
 *
 | 
			
		||||
 * @private
 | 
			
		||||
 * @function
 | 
			
		||||
 */
 | 
			
		||||
const findPosition = (transaction, parent, index, useSearchMarker) => {
 | 
			
		||||
const findPosition = (transaction, parent, index) => {
 | 
			
		||||
  const currentAttributes = new Map()
 | 
			
		||||
  const marker = useSearchMarker ? findMarker(parent, index) : null
 | 
			
		||||
  const marker = findMarker(parent, index)
 | 
			
		||||
  if (marker) {
 | 
			
		||||
    const pos = new ItemTextListPosition(marker.p.left, marker.p, marker.index, currentAttributes)
 | 
			
		||||
    return findNextPosition(transaction, pos, index - marker.index)
 | 
			
		||||
@ -202,7 +201,7 @@ const minimizeAttributeChanges = (currPos, attributes) => {
 | 
			
		||||
  while (true) {
 | 
			
		||||
    if (currPos.right === null) {
 | 
			
		||||
      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 {
 | 
			
		||||
      break
 | 
			
		||||
@ -228,7 +227,7 @@ const insertAttributes = (transaction, parent, currPos, attributes) => {
 | 
			
		||||
  // insert format-start items
 | 
			
		||||
  for (const key in attributes) {
 | 
			
		||||
    const val = attributes[key]
 | 
			
		||||
    const currentVal = currPos.currentAttributes.get(key) ?? null
 | 
			
		||||
    const currentVal = currPos.currentAttributes.get(key) || null
 | 
			
		||||
    if (!equalAttrs(currentVal, val)) {
 | 
			
		||||
      // save negated attribute (set null if currentVal undefined)
 | 
			
		||||
      negatedAttributes.set(key, currentVal)
 | 
			
		||||
@ -390,12 +389,12 @@ const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAtt
 | 
			
		||||
      switch (content.constructor) {
 | 
			
		||||
        case ContentFormat: {
 | 
			
		||||
          const { key, value } = /** @type {ContentFormat} */ (content)
 | 
			
		||||
          const startAttrValue = startAttributes.get(key) ?? null
 | 
			
		||||
          const startAttrValue = startAttributes.get(key) || null
 | 
			
		||||
          if (endFormats.get(key) !== content || startAttrValue === value) {
 | 
			
		||||
            // Either this format is overwritten or it is not necessary because the attribute already existed.
 | 
			
		||||
            start.delete(transaction)
 | 
			
		||||
            cleanups++
 | 
			
		||||
            if (!reachedCurr && (currAttributes.get(key) ?? null) === value && startAttrValue !== value) {
 | 
			
		||||
            if (!reachedCurr && (currAttributes.get(key) || null) === value && startAttrValue !== value) {
 | 
			
		||||
              if (startAttrValue === null) {
 | 
			
		||||
                currAttributes.delete(key)
 | 
			
		||||
              } else {
 | 
			
		||||
@ -477,56 +476,6 @@ export const cleanupYTextFormatting = type => {
 | 
			
		||||
  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 {ItemTextListPosition} currPos
 | 
			
		||||
@ -568,7 +517,7 @@ const deleteText = (transaction, currPos, length) => {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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
 | 
			
		||||
 *   {
 | 
			
		||||
@ -770,12 +719,12 @@ export class YTextEvent extends YEvent {
 | 
			
		||||
              const { key, value } = /** @type {ContentFormat} */ (item.content)
 | 
			
		||||
              if (this.adds(item)) {
 | 
			
		||||
                if (!this.deletes(item)) {
 | 
			
		||||
                  const curVal = currentAttributes.get(key) ?? null
 | 
			
		||||
                  const curVal = currentAttributes.get(key) || null
 | 
			
		||||
                  if (!equalAttrs(curVal, value)) {
 | 
			
		||||
                    if (action === 'retain') {
 | 
			
		||||
                      addOp()
 | 
			
		||||
                    }
 | 
			
		||||
                    if (equalAttrs(value, (oldAttributes.get(key) ?? null))) {
 | 
			
		||||
                    if (equalAttrs(value, (oldAttributes.get(key) || null))) {
 | 
			
		||||
                      delete attributes[key]
 | 
			
		||||
                    } else {
 | 
			
		||||
                      attributes[key] = value
 | 
			
		||||
@ -786,7 +735,7 @@ export class YTextEvent extends YEvent {
 | 
			
		||||
                }
 | 
			
		||||
              } else if (this.deletes(item)) {
 | 
			
		||||
                oldAttributes.set(key, value)
 | 
			
		||||
                const curVal = currentAttributes.get(key) ?? null
 | 
			
		||||
                const curVal = currentAttributes.get(key) || null
 | 
			
		||||
                if (!equalAttrs(curVal, value)) {
 | 
			
		||||
                  if (action === 'retain') {
 | 
			
		||||
                    addOp()
 | 
			
		||||
@ -860,14 +809,9 @@ export class YText extends AbstractType {
 | 
			
		||||
     */
 | 
			
		||||
    this._pending = string !== undefined ? [() => this.insert(0, string)] : []
 | 
			
		||||
    /**
 | 
			
		||||
     * @type {Array<ArraySearchMarker>|null}
 | 
			
		||||
     * @type {Array<ArraySearchMarker>}
 | 
			
		||||
     */
 | 
			
		||||
    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 +820,6 @@ export class YText extends AbstractType {
 | 
			
		||||
   * @type {number}
 | 
			
		||||
   */
 | 
			
		||||
  get length () {
 | 
			
		||||
    this.doc ?? warnPrematureAccess()
 | 
			
		||||
    return this._length
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -899,10 +842,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}
 | 
			
		||||
   */
 | 
			
		||||
  clone () {
 | 
			
		||||
@ -920,10 +859,55 @@ export class YText extends AbstractType {
 | 
			
		||||
  _callObserver (transaction, parentSubs) {
 | 
			
		||||
    super._callObserver(transaction, parentSubs)
 | 
			
		||||
    const event = new YTextEvent(this, transaction, parentSubs)
 | 
			
		||||
    const doc = transaction.doc
 | 
			
		||||
    callTypeObservers(this, transaction, event)
 | 
			
		||||
    // If a remote change happened, we try to cleanup potential formatting duplicates.
 | 
			
		||||
    if (!transaction.local && this._hasFormatting) {
 | 
			
		||||
      transaction._needFormattingCleanup = true
 | 
			
		||||
    if (!transaction.local) {
 | 
			
		||||
      // 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 +917,6 @@ export class YText extends AbstractType {
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  toString () {
 | 
			
		||||
    this.doc ?? warnPrematureAccess()
 | 
			
		||||
    let str = ''
 | 
			
		||||
    /**
 | 
			
		||||
     * @type {Item|null}
 | 
			
		||||
@ -961,7 +944,7 @@ export class YText extends AbstractType {
 | 
			
		||||
  /**
 | 
			
		||||
   * 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 {boolean} [opts.sanitize] Sanitize input delta. Removes ending newlines if set to true.
 | 
			
		||||
   *
 | 
			
		||||
@ -1007,7 +990,6 @@ export class YText extends AbstractType {
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  toDelta (snapshot, prevSnapshot, computeYChange) {
 | 
			
		||||
    this.doc ?? warnPrematureAccess()
 | 
			
		||||
    /**
 | 
			
		||||
     * @type{Array<any>}
 | 
			
		||||
     */
 | 
			
		||||
@ -1128,7 +1110,7 @@ export class YText extends AbstractType {
 | 
			
		||||
    const y = this.doc
 | 
			
		||||
    if (y !== null) {
 | 
			
		||||
      transact(y, transaction => {
 | 
			
		||||
        const pos = findPosition(transaction, this, index, !attributes)
 | 
			
		||||
        const pos = findPosition(transaction, this, index)
 | 
			
		||||
        if (!attributes) {
 | 
			
		||||
          attributes = {}
 | 
			
		||||
          // @ts-ignore
 | 
			
		||||
@ -1146,20 +1128,20 @@ export class YText extends AbstractType {
 | 
			
		||||
   *
 | 
			
		||||
   * @param {number} index The index to insert the embed at.
 | 
			
		||||
   * @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
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  insertEmbed (index, embed, attributes) {
 | 
			
		||||
  insertEmbed (index, embed, attributes = {}) {
 | 
			
		||||
    const y = this.doc
 | 
			
		||||
    if (y !== null) {
 | 
			
		||||
      transact(y, transaction => {
 | 
			
		||||
        const pos = findPosition(transaction, this, index, !attributes)
 | 
			
		||||
        insertText(transaction, this, pos, embed, attributes || {})
 | 
			
		||||
        const pos = findPosition(transaction, this, index)
 | 
			
		||||
        insertText(transaction, this, pos, embed, attributes)
 | 
			
		||||
      })
 | 
			
		||||
    } 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 +1160,7 @@ export class YText extends AbstractType {
 | 
			
		||||
    const y = this.doc
 | 
			
		||||
    if (y !== null) {
 | 
			
		||||
      transact(y, transaction => {
 | 
			
		||||
        deleteText(transaction, findPosition(transaction, this, index, true), length)
 | 
			
		||||
        deleteText(transaction, findPosition(transaction, this, index), length)
 | 
			
		||||
      })
 | 
			
		||||
    } else {
 | 
			
		||||
      /** @type {Array<function>} */ (this._pending).push(() => this.delete(index, length))
 | 
			
		||||
@ -1202,7 +1184,7 @@ export class YText extends AbstractType {
 | 
			
		||||
    const y = this.doc
 | 
			
		||||
    if (y !== null) {
 | 
			
		||||
      transact(y, transaction => {
 | 
			
		||||
        const pos = findPosition(transaction, this, index, false)
 | 
			
		||||
        const pos = findPosition(transaction, this, index)
 | 
			
		||||
        if (pos.right === null) {
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,3 @@
 | 
			
		||||
import * as object from 'lib0/object'
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  YXmlFragment,
 | 
			
		||||
@ -8,24 +7,17 @@ import {
 | 
			
		||||
  typeMapSet,
 | 
			
		||||
  typeMapGet,
 | 
			
		||||
  typeMapGetAll,
 | 
			
		||||
  typeMapGetAllSnapshot,
 | 
			
		||||
  typeListForEach,
 | 
			
		||||
  YXmlElementRefID,
 | 
			
		||||
  Snapshot, YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item // eslint-disable-line
 | 
			
		||||
  YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item // eslint-disable-line
 | 
			
		||||
} from '../internals.js'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @typedef {Object|number|null|Array<any>|string|Uint8Array|AbstractType<any>} ValueTypes
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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 childElements that must inherit from YXmlElement
 | 
			
		||||
 *
 | 
			
		||||
 * @template {{ [key: string]: ValueTypes }} [KV={ [key: string]: string }]
 | 
			
		||||
 */
 | 
			
		||||
export class YXmlElement extends YXmlFragment {
 | 
			
		||||
  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.
 | 
			
		||||
   *
 | 
			
		||||
   * Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
 | 
			
		||||
   *
 | 
			
		||||
   * @return {YXmlElement<KV>}
 | 
			
		||||
   * @return {YXmlElement}
 | 
			
		||||
   */
 | 
			
		||||
  clone () {
 | 
			
		||||
    /**
 | 
			
		||||
     * @type {YXmlElement<KV>}
 | 
			
		||||
     */
 | 
			
		||||
    const el = new YXmlElement(this.nodeName)
 | 
			
		||||
    const attrs = this.getAttributes()
 | 
			
		||||
    object.forEach(attrs, (value, key) => {
 | 
			
		||||
      if (typeof value === 'string') {
 | 
			
		||||
        el.setAttribute(key, value)
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    for (const key in attrs) {
 | 
			
		||||
      el.setAttribute(key, attrs[key])
 | 
			
		||||
    }
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    el.insert(0, this.toArray().map(item => item instanceof AbstractType ? item.clone() : item))
 | 
			
		||||
    return el
 | 
			
		||||
@ -133,7 +116,7 @@ export class YXmlElement extends YXmlFragment {
 | 
			
		||||
  /**
 | 
			
		||||
   * 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
 | 
			
		||||
   */
 | 
			
		||||
@ -150,10 +133,8 @@ export class YXmlElement extends YXmlFragment {
 | 
			
		||||
  /**
 | 
			
		||||
   * Sets or updates an attribute.
 | 
			
		||||
   *
 | 
			
		||||
   * @template {keyof KV & string} KEY
 | 
			
		||||
   *
 | 
			
		||||
   * @param {KEY} attributeName The attribute name that is to be set.
 | 
			
		||||
   * @param {KV[KEY]} attributeValue The attribute value that is to be set.
 | 
			
		||||
   * @param {String} attributeName The attribute name that is to be set.
 | 
			
		||||
   * @param {String} attributeValue The attribute value that is to be set.
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
@ -170,11 +151,9 @@ export class YXmlElement extends YXmlFragment {
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns an attribute value that belongs to the attribute name.
 | 
			
		||||
   *
 | 
			
		||||
   * @template {keyof KV & string} KEY
 | 
			
		||||
   *
 | 
			
		||||
   * @param {KEY} attributeName The attribute name that identifies the
 | 
			
		||||
   * @param {String} attributeName The attribute name that identifies the
 | 
			
		||||
   *                               queried value.
 | 
			
		||||
   * @return {KV[KEY]|undefined} The queried attribute value.
 | 
			
		||||
   * @return {String} The queried attribute value.
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
@ -185,7 +164,7 @@ export class YXmlElement extends YXmlFragment {
 | 
			
		||||
  /**
 | 
			
		||||
   * 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.
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
@ -197,13 +176,12 @@ export class YXmlElement extends YXmlFragment {
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns all attribute name/value pairs in a JSON Object.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Snapshot} [snapshot]
 | 
			
		||||
   * @return {{ [Key in Extract<keyof KV,string>]?: KV[Key]}} A JSON Object that describes the attributes.
 | 
			
		||||
   * @return {Object<string, any>} A JSON Object that describes the attributes.
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  getAttributes (snapshot) {
 | 
			
		||||
    return /** @type {any} */ (snapshot ? typeMapGetAllSnapshot(this, snapshot) : typeMapGetAll(this))
 | 
			
		||||
  getAttributes () {
 | 
			
		||||
    return typeMapGetAll(this)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -225,10 +203,7 @@ export class YXmlElement extends YXmlFragment {
 | 
			
		||||
    const dom = _document.createElement(this.nodeName)
 | 
			
		||||
    const attrs = this.getAttributes()
 | 
			
		||||
    for (const key in attrs) {
 | 
			
		||||
      const value = attrs[key]
 | 
			
		||||
      if (typeof value === 'string') {
 | 
			
		||||
        dom.setAttribute(key, value)
 | 
			
		||||
      }
 | 
			
		||||
      dom.setAttribute(key, attrs[key])
 | 
			
		||||
    }
 | 
			
		||||
    typeListForEach(this, yxml => {
 | 
			
		||||
      dom.appendChild(yxml.toDOM(_document, hooks, binding))
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  YEvent,
 | 
			
		||||
  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 {Set<string|null>} subs The set of changed attributes. `null` is included if the
 | 
			
		||||
   *                   child list changed.
 | 
			
		||||
   * @param {Transaction} transaction The transaction instance with which the
 | 
			
		||||
   * @param {Transaction} transaction The transaction instance with wich the
 | 
			
		||||
   *                                  change was created.
 | 
			
		||||
   */
 | 
			
		||||
  constructor (target, subs, transaction) {
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,6 @@ import {
 | 
			
		||||
  transact,
 | 
			
		||||
  typeListGet,
 | 
			
		||||
  typeListSlice,
 | 
			
		||||
  warnPrematureAccess,
 | 
			
		||||
  UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, ContentType, Transaction, Item, YXmlText, YXmlHook // eslint-disable-line
 | 
			
		||||
} from '../internals.js'
 | 
			
		||||
 | 
			
		||||
@ -67,7 +66,6 @@ export class YXmlTreeWalker {
 | 
			
		||||
     */
 | 
			
		||||
    this._currentNode = /** @type {Item} */ (root._start)
 | 
			
		||||
    this._firstCall = true
 | 
			
		||||
    root.doc ?? warnPrematureAccess()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  [Symbol.iterator] () {
 | 
			
		||||
@ -96,12 +94,8 @@ export class YXmlTreeWalker {
 | 
			
		||||
        } else {
 | 
			
		||||
          // walk right or up in the tree
 | 
			
		||||
          while (n !== null) {
 | 
			
		||||
            /**
 | 
			
		||||
             * @type {Item | null}
 | 
			
		||||
             */
 | 
			
		||||
            const nxt = n.next
 | 
			
		||||
            if (nxt !== null) {
 | 
			
		||||
              n = nxt
 | 
			
		||||
            if (n.right !== null) {
 | 
			
		||||
              n = n.right
 | 
			
		||||
              break
 | 
			
		||||
            } else if (n.parent === this._root) {
 | 
			
		||||
              n = null
 | 
			
		||||
@ -169,10 +163,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}
 | 
			
		||||
   */
 | 
			
		||||
  clone () {
 | 
			
		||||
@ -183,7 +173,6 @@ export class YXmlFragment extends AbstractType {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get length () {
 | 
			
		||||
    this.doc ?? warnPrematureAccess()
 | 
			
		||||
    return this._prelimContent === null ? this._length : this._prelimContent.length
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -387,9 +376,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) {
 | 
			
		||||
    this.insert(0, content)
 | 
			
		||||
@ -406,8 +395,7 @@ export class YXmlFragment extends AbstractType {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns a portion of this YXmlFragment into a JavaScript Array selected
 | 
			
		||||
   * from start to end (end not included).
 | 
			
		||||
   * Transforms this YArray to a JavaScript Array.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {number} [start]
 | 
			
		||||
   * @param {number} [end]
 | 
			
		||||
@ -418,7 +406,7 @@ 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.
 | 
			
		||||
   */
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  YMap,
 | 
			
		||||
  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}
 | 
			
		||||
   */
 | 
			
		||||
  clone () {
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  YText,
 | 
			
		||||
  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}
 | 
			
		||||
   */
 | 
			
		||||
  clone () {
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
import { ObservableV2 } from 'lib0/observable'
 | 
			
		||||
 | 
			
		||||
import { Observable } from 'lib0/observable'
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  Doc // eslint-disable-line
 | 
			
		||||
@ -10,9 +11,9 @@ import {
 | 
			
		||||
 * @note This interface is experimental and it is not advised to actually inherit this class.
 | 
			
		||||
 *       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 {any} awareness
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  findIndexSS,
 | 
			
		||||
  getState,
 | 
			
		||||
@ -327,23 +328,3 @@ export const readAndApplyDeleteSet = (decoder, transaction, store) => {
 | 
			
		||||
  }
 | 
			
		||||
  return null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {DeleteSet} ds1
 | 
			
		||||
 * @param {DeleteSet} ds2
 | 
			
		||||
 */
 | 
			
		||||
export const equalDeleteSets = (ds1, ds2) => {
 | 
			
		||||
  if (ds1.clients.size !== ds2.clients.size) return false
 | 
			
		||||
  for (const [client, deleteItems1] of ds1.clients.entries()) {
 | 
			
		||||
    const deleteItems2 = /** @type {Array<import('../internals.js').DeleteItem>} */ (ds2.clients.get(client))
 | 
			
		||||
    if (deleteItems2 === undefined || deleteItems1.length !== deleteItems2.length) return false
 | 
			
		||||
    for (let i = 0; i < deleteItems1.length; i++) {
 | 
			
		||||
      const di1 = deleteItems1[i]
 | 
			
		||||
      const di2 = deleteItems2[i]
 | 
			
		||||
      if (di1.clock !== di2.clock || di1.len !== di2.len) {
 | 
			
		||||
        return false
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -8,13 +8,12 @@ import {
 | 
			
		||||
  YArray,
 | 
			
		||||
  YText,
 | 
			
		||||
  YMap,
 | 
			
		||||
  YXmlElement,
 | 
			
		||||
  YXmlFragment,
 | 
			
		||||
  transact,
 | 
			
		||||
  ContentDoc, Item, Transaction, YEvent // eslint-disable-line
 | 
			
		||||
} from '../internals.js'
 | 
			
		||||
 | 
			
		||||
import { ObservableV2 } from 'lib0/observable'
 | 
			
		||||
import { Observable } from 'lib0/observable'
 | 
			
		||||
import * as random from 'lib0/random'
 | 
			
		||||
import * as map from 'lib0/map'
 | 
			
		||||
import * as array from 'lib0/array'
 | 
			
		||||
@ -33,27 +32,11 @@ 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()
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @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.
 | 
			
		||||
 * @extends ObservableV2<DocEvents>
 | 
			
		||||
 * @extends Observable<string>
 | 
			
		||||
 */
 | 
			
		||||
export class Doc extends ObservableV2 {
 | 
			
		||||
export class Doc extends Observable {
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {DocOpts} opts configuration
 | 
			
		||||
   */
 | 
			
		||||
@ -104,9 +87,8 @@ export class Doc extends ObservableV2 {
 | 
			
		||||
     * 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.
 | 
			
		||||
     * Promise that resolves once the document has been loaded from a presistence provider.
 | 
			
		||||
     */
 | 
			
		||||
    this.whenLoaded = promise.create(resolve => {
 | 
			
		||||
      this.on('load', () => {
 | 
			
		||||
@ -131,8 +113,8 @@ export class Doc extends ObservableV2 {
 | 
			
		||||
        this.whenSynced = provideSyncedPromise()
 | 
			
		||||
      }
 | 
			
		||||
      this.isSynced = isSynced === undefined || isSynced === true
 | 
			
		||||
      if (this.isSynced && !this.isLoaded) {
 | 
			
		||||
        this.emit('load', [this])
 | 
			
		||||
      if (!this.isLoaded) {
 | 
			
		||||
        this.emit('load', [])
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    /**
 | 
			
		||||
@ -188,31 +170,30 @@ export class Doc extends ObservableV2 {
 | 
			
		||||
  /**
 | 
			
		||||
   * 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.
 | 
			
		||||
   * `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:*
 | 
			
		||||
   * 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)`, ..
 | 
			
		||||
   *
 | 
			
		||||
   * @template {typeof AbstractType<any>} Type
 | 
			
		||||
   * @example
 | 
			
		||||
   *   const ydoc = new Y.Doc(..)
 | 
			
		||||
   *   const y = new Y(..)
 | 
			
		||||
   *   const appState = {
 | 
			
		||||
   *     document: ydoc.getText('document')
 | 
			
		||||
   *     comments: ydoc.getArray('comments')
 | 
			
		||||
   *     document: y.getText('document')
 | 
			
		||||
   *     comments: y.getArray('comments')
 | 
			
		||||
   *   }
 | 
			
		||||
   *
 | 
			
		||||
   * @param {string} name
 | 
			
		||||
   * @param {Type} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ...
 | 
			
		||||
   * @return {InstanceType<Type>} The created type. Constructed with TypeConstructor
 | 
			
		||||
   * @param {Function} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ...
 | 
			
		||||
   * @return {AbstractType<any>} The created type. Constructed with TypeConstructor
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  get (name, TypeConstructor = /** @type {any} */ (AbstractType)) {
 | 
			
		||||
  get (name, TypeConstructor = AbstractType) {
 | 
			
		||||
    const type = map.setIfUndefined(this.share, name, () => {
 | 
			
		||||
      // @ts-ignore
 | 
			
		||||
      const t = new TypeConstructor()
 | 
			
		||||
@ -238,12 +219,12 @@ export class Doc extends ObservableV2 {
 | 
			
		||||
        t._length = type._length
 | 
			
		||||
        this.share.set(name, t)
 | 
			
		||||
        t._integrate(this, null)
 | 
			
		||||
        return /** @type {InstanceType<Type>} */ (t)
 | 
			
		||||
        return t
 | 
			
		||||
      } else {
 | 
			
		||||
        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 +235,8 @@ export class Doc extends ObservableV2 {
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  getArray (name = '') {
 | 
			
		||||
    return /** @type {YArray<T>} */ (this.get(name, YArray))
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    return this.get(name, YArray)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -264,6 +246,7 @@ export class Doc extends ObservableV2 {
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  getText (name = '') {
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    return this.get(name, YText)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -275,17 +258,8 @@ export class Doc extends ObservableV2 {
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  getMap (name = '') {
 | 
			
		||||
    return /** @type {YMap<T>} */ (this.get(name, YMap))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {string} [name]
 | 
			
		||||
   * @return {YXmlElement}
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  getXmlElement (name = '') {
 | 
			
		||||
    return /** @type {YXmlElement<{[key:string]:string}>} */ (this.get(name, YXmlElement))
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    return this.get(name, YMap)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -295,6 +269,7 @@ export class Doc extends ObservableV2 {
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  getXmlFragment (name = '') {
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    return this.get(name, YXmlFragment)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -323,7 +298,6 @@ export class Doc extends ObservableV2 {
 | 
			
		||||
   * Emit `destroy` event and unregister all event handlers.
 | 
			
		||||
   */
 | 
			
		||||
  destroy () {
 | 
			
		||||
    this.isDestroyed = true
 | 
			
		||||
    array.from(this.subdocs).forEach(subdoc => subdoc.destroy())
 | 
			
		||||
    const item = this._item
 | 
			
		||||
    if (item !== null) {
 | 
			
		||||
@ -339,9 +313,24 @@ export class Doc extends ObservableV2 {
 | 
			
		||||
        transaction.subdocsRemoved.add(this)
 | 
			
		||||
      }, null, true)
 | 
			
		||||
    }
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    this.emit('destroyed', [true]) // DEPRECATED!
 | 
			
		||||
    this.emit('destroyed', [true])
 | 
			
		||||
    this.emit('destroy', [this])
 | 
			
		||||
    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 * as decoding from 'lib0/decoding'
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  YArray,
 | 
			
		||||
  YMap,
 | 
			
		||||
@ -62,7 +63,7 @@ export class PermanentUserData {
 | 
			
		||||
        initUser(storeType.get(userDescription), userDescription)
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
    // add initial data
 | 
			
		||||
    // add intial data
 | 
			
		||||
    storeType.forEach(initUser)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  writeID,
 | 
			
		||||
  readID,
 | 
			
		||||
@ -8,8 +9,7 @@ import {
 | 
			
		||||
  createID,
 | 
			
		||||
  ContentType,
 | 
			
		||||
  followRedone,
 | 
			
		||||
  getItem,
 | 
			
		||||
  StructStore, ID, Doc, AbstractType, // eslint-disable-line
 | 
			
		||||
  ID, Doc, AbstractType // eslint-disable-line
 | 
			
		||||
} from '../internals.js'
 | 
			
		||||
 | 
			
		||||
import * as encoding from 'lib0/encoding'
 | 
			
		||||
@ -66,7 +66,7 @@ export class RelativePosition {
 | 
			
		||||
     * after the meant position.
 | 
			
		||||
     * 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.
 | 
			
		||||
     *
 | 
			
		||||
     * @type {number}
 | 
			
		||||
@ -102,7 +102,7 @@ export const relativePositionToJSON = rpos => {
 | 
			
		||||
 *
 | 
			
		||||
 * @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 {
 | 
			
		||||
  /**
 | 
			
		||||
@ -257,36 +257,13 @@ export const readRelativePosition = decoder => {
 | 
			
		||||
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 {Doc} doc
 | 
			
		||||
 * @param {boolean} followUndoneDeletions - whether to follow undone deletions - see https://github.com/yjs/yjs/issues/638
 | 
			
		||||
 * @return {AbsolutePosition|null}
 | 
			
		||||
 *
 | 
			
		||||
 * @function
 | 
			
		||||
 */
 | 
			
		||||
export const createAbsolutePositionFromRelativePosition = (rpos, doc, followUndoneDeletions = true) => {
 | 
			
		||||
export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
 | 
			
		||||
  const store = doc.store
 | 
			
		||||
  const rightID = rpos.item
 | 
			
		||||
  const typeID = rpos.type
 | 
			
		||||
@ -298,7 +275,7 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc, followUndo
 | 
			
		||||
    if (getState(store, rightID.client) <= rightID.clock) {
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
    const res = followUndoneDeletions ? followRedone(store, rightID) : getItemWithOffset(store, rightID)
 | 
			
		||||
    const res = followRedone(store, rightID)
 | 
			
		||||
    const right = res.item
 | 
			
		||||
    if (!(right instanceof Item)) {
 | 
			
		||||
      return null
 | 
			
		||||
@ -322,7 +299,7 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc, followUndo
 | 
			
		||||
        // type does not exist yet
 | 
			
		||||
        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) {
 | 
			
		||||
        type = item.content.type
 | 
			
		||||
      } else {
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  isDeleted,
 | 
			
		||||
  createDeleteSetFromStructStore,
 | 
			
		||||
@ -14,10 +15,7 @@ import {
 | 
			
		||||
  findIndexSS,
 | 
			
		||||
  UpdateEncoderV2,
 | 
			
		||||
  applyUpdateV2,
 | 
			
		||||
  LazyStructReader,
 | 
			
		||||
  equalDeleteSets,
 | 
			
		||||
  UpdateDecoderV1, UpdateDecoderV2, DSEncoderV1, DSEncoderV2, DSDecoderV1, DSDecoderV2, Transaction, Doc, DeleteSet, Item, // eslint-disable-line
 | 
			
		||||
  mergeDeleteSets
 | 
			
		||||
  DSEncoderV1, DSEncoderV2, DSDecoderV1, DSDecoderV2, Transaction, Doc, DeleteSet, Item // eslint-disable-line
 | 
			
		||||
} from '../internals.js'
 | 
			
		||||
 | 
			
		||||
import * as map from 'lib0/map'
 | 
			
		||||
@ -149,20 +147,12 @@ export const splitSnapshotAffectedStructs = (transaction, snapshot) => {
 | 
			
		||||
        getItemCleanStart(transaction, createID(client, clock))
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    iterateDeletedStructs(transaction, snapshot.ds, _item => {})
 | 
			
		||||
    iterateDeletedStructs(transaction, snapshot.ds, item => {})
 | 
			
		||||
    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 {Snapshot} snapshot
 | 
			
		||||
 * @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()) => {
 | 
			
		||||
  if (originDoc.gc) {
 | 
			
		||||
    // 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
 | 
			
		||||
 | 
			
		||||
@ -209,28 +199,3 @@ export const createDocFromSnapshot = (originDoc, snapshot, newDoc = new Doc()) =
 | 
			
		||||
  applyUpdateV2(newDoc, encoder.toUint8Array(), 'snapshot')
 | 
			
		||||
  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 {
 | 
			
		||||
  GC,
 | 
			
		||||
  splitItem,
 | 
			
		||||
@ -66,13 +67,13 @@ export const getState = (store, client) => {
 | 
			
		||||
 * @private
 | 
			
		||||
 * @function
 | 
			
		||||
 */
 | 
			
		||||
export const integrityCheck = store => {
 | 
			
		||||
export const integretyCheck = store => {
 | 
			
		||||
  store.clients.forEach(structs => {
 | 
			
		||||
    for (let i = 1; i < structs.length; i++) {
 | 
			
		||||
      const l = structs[i - 1]
 | 
			
		||||
      const r = structs[i]
 | 
			
		||||
      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 {
 | 
			
		||||
  getState,
 | 
			
		||||
  writeStructsFromTransaction,
 | 
			
		||||
@ -10,7 +11,6 @@ import {
 | 
			
		||||
  Item,
 | 
			
		||||
  generateNewClientId,
 | 
			
		||||
  createID,
 | 
			
		||||
  cleanupYTextAfterTransaction,
 | 
			
		||||
  UpdateEncoderV1, UpdateEncoderV2, GC, StructStore, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
 | 
			
		||||
} from '../internals.js'
 | 
			
		||||
 | 
			
		||||
@ -28,8 +28,7 @@ import { callAll } from 'lib0/function'
 | 
			
		||||
 * possible. Here is an example to illustrate the advantages of bundling:
 | 
			
		||||
 *
 | 
			
		||||
 * @example
 | 
			
		||||
 * const ydoc = new Y.Doc()
 | 
			
		||||
 * const map = ydoc.getMap('map')
 | 
			
		||||
 * const map = y.define('map', YMap)
 | 
			
		||||
 * // Log content when change is triggered
 | 
			
		||||
 * map.observe(() => {
 | 
			
		||||
 *   console.log('change triggered')
 | 
			
		||||
@ -38,7 +37,7 @@ import { callAll } from 'lib0/function'
 | 
			
		||||
 * map.set('a', 0) // => "change triggered"
 | 
			
		||||
 * map.set('b', 0) // => "change triggered"
 | 
			
		||||
 * // When put in a transaction, it will trigger the log after the transaction:
 | 
			
		||||
 * ydoc.transact(() => {
 | 
			
		||||
 * y.transact(() => {
 | 
			
		||||
 *   map.set('a', 1)
 | 
			
		||||
 *   map.set('b', 1)
 | 
			
		||||
 * }) // => "change triggered"
 | 
			
		||||
@ -115,10 +114,6 @@ export class Transaction {
 | 
			
		||||
     * @type {Set<Doc>}
 | 
			
		||||
     */
 | 
			
		||||
    this.subdocsLoaded = new Set()
 | 
			
		||||
    /**
 | 
			
		||||
     * @type {boolean}
 | 
			
		||||
     */
 | 
			
		||||
    this._needFormattingCleanup = false
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -166,29 +161,18 @@ export const addChangedTypeToTransaction = (transaction, type, parentSub) => {
 | 
			
		||||
/**
 | 
			
		||||
 * @param {Array<AbstractStruct>} structs
 | 
			
		||||
 * @param {number} pos
 | 
			
		||||
 * @return {number} # of merged structs
 | 
			
		||||
 */
 | 
			
		||||
const tryToMergeWithLefts = (structs, pos) => {
 | 
			
		||||
  let right = structs[pos]
 | 
			
		||||
  let left = structs[pos - 1]
 | 
			
		||||
  let i = pos
 | 
			
		||||
  for (; i > 0; right = left, left = structs[--i - 1]) {
 | 
			
		||||
    if (left.deleted === right.deleted && left.constructor === right.constructor) {
 | 
			
		||||
      if (left.mergeWith(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))
 | 
			
		||||
        }
 | 
			
		||||
        continue
 | 
			
		||||
const tryToMergeWithLeft = (structs, pos) => {
 | 
			
		||||
  const left = structs[pos - 1]
 | 
			
		||||
  const right = structs[pos]
 | 
			
		||||
  if (left.deleted === right.deleted && left.constructor === right.constructor) {
 | 
			
		||||
    if (left.mergeWith(right)) {
 | 
			
		||||
      structs.splice(pos, 1)
 | 
			
		||||
      if (right instanceof Item && right.parentSub !== null && /** @type {AbstractType<any>} */ (right.parent)._map.get(right.parentSub) === right) {
 | 
			
		||||
        /** @type {AbstractType<any>} */ (right.parent)._map.set(right.parentSub, /** @type {Item} */ (left))
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    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) => {
 | 
			
		||||
  // 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) => {
 | 
			
		||||
    const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
 | 
			
		||||
    for (let di = deleteItems.length - 1; di >= 0; di--) {
 | 
			
		||||
@ -235,9 +219,9 @@ const tryMergeDeleteSet = (ds, store) => {
 | 
			
		||||
      for (
 | 
			
		||||
        let si = mostRightIndexToCheck, struct = structs[si];
 | 
			
		||||
        si > 0 && struct.id.clock >= deleteItem.clock;
 | 
			
		||||
        struct = structs[si]
 | 
			
		||||
        struct = structs[--si]
 | 
			
		||||
      ) {
 | 
			
		||||
        si -= 1 + tryToMergeWithLefts(structs, si)
 | 
			
		||||
        tryToMergeWithLeft(structs, si)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
@ -286,34 +270,31 @@ const cleanupTransactions = (transactionCleanups, i) => {
 | 
			
		||||
      )
 | 
			
		||||
      fs.push(() => {
 | 
			
		||||
        // deep observe events
 | 
			
		||||
        transaction.changedParentTypes.forEach((events, type) => {
 | 
			
		||||
          // We need to think about the possibility that the user transforms the
 | 
			
		||||
          // Y.Doc in the event.
 | 
			
		||||
          if (type._dEH.l.length > 0 && (type._item === null || !type._item.deleted)) {
 | 
			
		||||
            events = events
 | 
			
		||||
              .filter(event =>
 | 
			
		||||
                event.target._item === null || !event.target._item.deleted
 | 
			
		||||
              )
 | 
			
		||||
            events
 | 
			
		||||
              .forEach(event => {
 | 
			
		||||
                event.currentTarget = type
 | 
			
		||||
                // path is relative to the current target
 | 
			
		||||
                event._path = null
 | 
			
		||||
              })
 | 
			
		||||
            // sort events by path length so that top-level events are fired first.
 | 
			
		||||
            events
 | 
			
		||||
              .sort((event1, event2) => event1.path.length - event2.path.length)
 | 
			
		||||
            // We don't need to check for events.length
 | 
			
		||||
            // because we know it has at least one element
 | 
			
		||||
            callEventHandlerListeners(type._dEH, events, transaction)
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        transaction.changedParentTypes.forEach((events, type) =>
 | 
			
		||||
          fs.push(() => {
 | 
			
		||||
            // We need to think about the possibility that the user transforms the
 | 
			
		||||
            // Y.Doc in the event.
 | 
			
		||||
            if (type._item === null || !type._item.deleted) {
 | 
			
		||||
              events = events
 | 
			
		||||
                .filter(event =>
 | 
			
		||||
                  event.target._item === null || !event.target._item.deleted
 | 
			
		||||
                )
 | 
			
		||||
              events
 | 
			
		||||
                .forEach(event => {
 | 
			
		||||
                  event.currentTarget = type
 | 
			
		||||
                })
 | 
			
		||||
              // sort events by path length so that top-level events are fired first.
 | 
			
		||||
              events
 | 
			
		||||
                .sort((event1, event2) => event1.path.length - event2.path.length)
 | 
			
		||||
              // We don't need to check for events.length
 | 
			
		||||
              // because we know it has at least one element
 | 
			
		||||
              callEventHandlerListeners(type._dEH, events, transaction)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
        fs.push(() => doc.emit('afterTransaction', [transaction, doc]))
 | 
			
		||||
      })
 | 
			
		||||
      fs.push(() => doc.emit('afterTransaction', [transaction, doc]))
 | 
			
		||||
      callAll(fs, [])
 | 
			
		||||
      if (transaction._needFormattingCleanup) {
 | 
			
		||||
        cleanupYTextAfterTransaction(transaction)
 | 
			
		||||
      }
 | 
			
		||||
    } finally {
 | 
			
		||||
      // Replace deleted items with ItemDeleted / GC.
 | 
			
		||||
      // This is where content is actually remove from the Yjs Doc.
 | 
			
		||||
@ -329,25 +310,23 @@ const cleanupTransactions = (transactionCleanups, i) => {
 | 
			
		||||
          const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
 | 
			
		||||
          // we iterate from right to left so we can safely remove entries
 | 
			
		||||
          const firstChangePos = math.max(findIndexSS(structs, beforeClock), 1)
 | 
			
		||||
          for (let i = structs.length - 1; i >= firstChangePos;) {
 | 
			
		||||
            i -= 1 + tryToMergeWithLefts(structs, i)
 | 
			
		||||
          for (let i = structs.length - 1; i >= firstChangePos; i--) {
 | 
			
		||||
            tryToMergeWithLeft(structs, i)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      // try to merge mergeStructs
 | 
			
		||||
      // @todo: it makes more sense to transform mergeStructs to a DS, sort it, and merge from right to left
 | 
			
		||||
      //        but at the moment DS does not handle duplicates
 | 
			
		||||
      for (let i = mergeStructs.length - 1; i >= 0; i--) {
 | 
			
		||||
      for (let i = 0; i < mergeStructs.length; i++) {
 | 
			
		||||
        const { client, clock } = mergeStructs[i].id
 | 
			
		||||
        const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
 | 
			
		||||
        const replacedStructPos = findIndexSS(structs, clock)
 | 
			
		||||
        if (replacedStructPos + 1 < structs.length) {
 | 
			
		||||
          if (tryToMergeWithLefts(structs, replacedStructPos + 1) > 1) {
 | 
			
		||||
            continue // no need to perform next check, both are already merged
 | 
			
		||||
          }
 | 
			
		||||
          tryToMergeWithLeft(structs, replacedStructPos + 1)
 | 
			
		||||
        }
 | 
			
		||||
        if (replacedStructPos > 0) {
 | 
			
		||||
          tryToMergeWithLefts(structs, replacedStructPos)
 | 
			
		||||
          tryToMergeWithLeft(structs, replacedStructPos)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      if (!transaction.local && transaction.afterState.get(doc.clientID) !== transaction.beforeState.get(doc.clientID)) {
 | 
			
		||||
 | 
			
		||||
@ -10,13 +10,12 @@ import {
 | 
			
		||||
  getItemCleanStart,
 | 
			
		||||
  isDeleted,
 | 
			
		||||
  addToDeleteSet,
 | 
			
		||||
  YEvent, Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line
 | 
			
		||||
  Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line
 | 
			
		||||
} from '../internals.js'
 | 
			
		||||
 | 
			
		||||
import * as time from 'lib0/time'
 | 
			
		||||
import * as array from 'lib0/array'
 | 
			
		||||
import * as logging from 'lib0/logging'
 | 
			
		||||
import { ObservableV2 } from 'lib0/observable'
 | 
			
		||||
import { Observable } from 'lib0/observable'
 | 
			
		||||
 | 
			
		||||
export class StackItem {
 | 
			
		||||
  /**
 | 
			
		||||
@ -39,7 +38,7 @@ export class StackItem {
 | 
			
		||||
 */
 | 
			
		||||
const clearUndoManagerStackItem = (tr, um, stackItem) => {
 | 
			
		||||
  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)
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
@ -48,10 +47,15 @@ const clearUndoManagerStackItem = (tr, um, stackItem) => {
 | 
			
		||||
/**
 | 
			
		||||
 * @param {UndoManager} undoManager
 | 
			
		||||
 * @param {Array<StackItem>} stack
 | 
			
		||||
 * @param {'undo'|'redo'} eventType
 | 
			
		||||
 * @param {string} eventType
 | 
			
		||||
 * @return {StackItem?}
 | 
			
		||||
 */
 | 
			
		||||
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
 | 
			
		||||
   * @type {any}
 | 
			
		||||
@ -60,7 +64,7 @@ const popStackItem = (undoManager, stack, eventType) => {
 | 
			
		||||
  const doc = undoManager.doc
 | 
			
		||||
  const scope = undoManager.scope
 | 
			
		||||
  transact(doc, transaction => {
 | 
			
		||||
    while (stack.length > 0 && undoManager.currStackItem === null) {
 | 
			
		||||
    while (stack.length > 0 && result === null) {
 | 
			
		||||
      const store = doc.store
 | 
			
		||||
      const stackItem = /** @type {StackItem} */ (stack.pop())
 | 
			
		||||
      /**
 | 
			
		||||
@ -81,7 +85,7 @@ const popStackItem = (undoManager, stack, eventType) => {
 | 
			
		||||
            }
 | 
			
		||||
            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)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
@ -89,7 +93,7 @@ const popStackItem = (undoManager, stack, eventType) => {
 | 
			
		||||
      iterateDeletedStructs(transaction, stackItem.deletions, struct => {
 | 
			
		||||
        if (
 | 
			
		||||
          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.
 | 
			
		||||
          !isDeleted(stackItem.insertions, struct.id)
 | 
			
		||||
        ) {
 | 
			
		||||
@ -108,7 +112,7 @@ const popStackItem = (undoManager, stack, eventType) => {
 | 
			
		||||
          performedChange = true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      undoManager.currStackItem = performedChange ? stackItem : null
 | 
			
		||||
      result = performedChange ? stackItem : null
 | 
			
		||||
    }
 | 
			
		||||
    transaction.changed.forEach((subProps, type) => {
 | 
			
		||||
      // destroy search marker if necessary
 | 
			
		||||
@ -118,13 +122,11 @@ const popStackItem = (undoManager, stack, eventType) => {
 | 
			
		||||
    })
 | 
			
		||||
    _tr = transaction
 | 
			
		||||
  }, undoManager)
 | 
			
		||||
  const res = undoManager.currStackItem
 | 
			
		||||
  if (res != null) {
 | 
			
		||||
  if (result != null) {
 | 
			
		||||
    const changedParentTypes = _tr.changedParentTypes
 | 
			
		||||
    undoManager.emit('stack-item-popped', [{ stackItem: res, type: eventType, changedParentTypes, origin: undoManager }, undoManager])
 | 
			
		||||
    undoManager.currStackItem = null
 | 
			
		||||
    undoManager.emit('stack-item-popped', [{ stackItem: result, type: eventType, changedParentTypes }, undoManager])
 | 
			
		||||
  }
 | 
			
		||||
  return res
 | 
			
		||||
  return result
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -140,14 +142,6 @@ const popStackItem = (undoManager, stack, eventType) => {
 | 
			
		||||
 * @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
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Fires 'stack-item-added' event when a stack item was added to either the undo- or
 | 
			
		||||
 * the redo-stack. You may store additional stack information via the
 | 
			
		||||
@ -155,11 +149,11 @@ const popStackItem = (undoManager, stack, eventType) => {
 | 
			
		||||
 * 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`.
 | 
			
		||||
 *
 | 
			
		||||
 * @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
 | 
			
		||||
   */
 | 
			
		||||
  constructor (typeScope, {
 | 
			
		||||
@ -168,14 +162,13 @@ export class UndoManager extends ObservableV2 {
 | 
			
		||||
    deleteFilter = () => true,
 | 
			
		||||
    trackedOrigins = new Set([null]),
 | 
			
		||||
    ignoreRemoteMapChanges = false,
 | 
			
		||||
    doc = /** @type {Doc} */ (array.isArray(typeScope) ? typeScope[0].doc : typeScope instanceof Doc ? typeScope : typeScope.doc)
 | 
			
		||||
    doc = /** @type {Doc} */ (array.isArray(typeScope) ? typeScope[0].doc : typeScope.doc)
 | 
			
		||||
  } = {}) {
 | 
			
		||||
    super()
 | 
			
		||||
    /**
 | 
			
		||||
     * @type {Array<AbstractType<any> | Doc>}
 | 
			
		||||
     * @type {Array<AbstractType<any>>}
 | 
			
		||||
     */
 | 
			
		||||
    this.scope = []
 | 
			
		||||
    this.doc = doc
 | 
			
		||||
    this.addToScope(typeScope)
 | 
			
		||||
    this.deleteFilter = deleteFilter
 | 
			
		||||
    trackedOrigins.add(this)
 | 
			
		||||
@ -196,12 +189,7 @@ export class UndoManager extends ObservableV2 {
 | 
			
		||||
     */
 | 
			
		||||
    this.undoing = false
 | 
			
		||||
    this.redoing = false
 | 
			
		||||
    /**
 | 
			
		||||
     * The currently popped stack item if UndoManager.undoing or UndoManager.redoing
 | 
			
		||||
     *
 | 
			
		||||
     * @type {StackItem|null}
 | 
			
		||||
     */
 | 
			
		||||
    this.currStackItem = null
 | 
			
		||||
    this.doc = doc
 | 
			
		||||
    this.lastChange = 0
 | 
			
		||||
    this.ignoreRemoteMapChanges = ignoreRemoteMapChanges
 | 
			
		||||
    this.captureTimeout = captureTimeout
 | 
			
		||||
@ -212,7 +200,7 @@ export class UndoManager extends ObservableV2 {
 | 
			
		||||
      // Only track certain transactions
 | 
			
		||||
      if (
 | 
			
		||||
        !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)))
 | 
			
		||||
      ) {
 | 
			
		||||
        return
 | 
			
		||||
@ -251,13 +239,10 @@ export class UndoManager extends ObservableV2 {
 | 
			
		||||
      }
 | 
			
		||||
      // make sure that deleted structs are not gc'd
 | 
			
		||||
      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)
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      /**
 | 
			
		||||
       * @type {[StackItemEvent, UndoManager]}
 | 
			
		||||
       */
 | 
			
		||||
      const changeEvent = [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo', changedParentTypes: transaction.changedParentTypes }, this]
 | 
			
		||||
      if (didAdd) {
 | 
			
		||||
        this.emit('stack-item-added', changeEvent)
 | 
			
		||||
@ -272,17 +257,12 @@ export class UndoManager extends ObservableV2 {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Extend the scope.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Array<AbstractType<any> | Doc> | AbstractType<any> | Doc} ytypes
 | 
			
		||||
   * @param {Array<AbstractType<any>> | AbstractType<any>} ytypes
 | 
			
		||||
   */
 | 
			
		||||
  addToScope (ytypes) {
 | 
			
		||||
    const tmpSet = new Set(this.scope)
 | 
			
		||||
    ytypes = array.isArray(ytypes) ? ytypes : [ytypes]
 | 
			
		||||
    ytypes.forEach(ytype => {
 | 
			
		||||
      if (!tmpSet.has(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
 | 
			
		||||
      if (this.scope.every(yt => yt !== ytype)) {
 | 
			
		||||
        this.scope.push(ytype)
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
 | 
			
		||||
import * as error from 'lib0/error'
 | 
			
		||||
import * as encoding from 'lib0/encoding'
 | 
			
		||||
 | 
			
		||||
@ -167,7 +168,7 @@ export class UpdateEncoderV2 extends DSEncoderV2 {
 | 
			
		||||
     */
 | 
			
		||||
    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.
 | 
			
		||||
     *
 | 
			
		||||
     * @type {number}
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  isDeleted,
 | 
			
		||||
  Item, AbstractType, Transaction, AbstractStruct // eslint-disable-line
 | 
			
		||||
@ -5,9 +6,6 @@ import {
 | 
			
		||||
 | 
			
		||||
import * as set from 'lib0/set'
 | 
			
		||||
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
 | 
			
		||||
@ -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> }>}
 | 
			
		||||
     */
 | 
			
		||||
    this._delta = null
 | 
			
		||||
    /**
 | 
			
		||||
     * @type {Array<string|number>|null}
 | 
			
		||||
     */
 | 
			
		||||
    this._path = null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -66,7 +60,8 @@ export class YEvent {
 | 
			
		||||
   *   type === event.target // => true
 | 
			
		||||
   */
 | 
			
		||||
  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 () {
 | 
			
		||||
    if (this._keys === null) {
 | 
			
		||||
      if (this.transaction.doc._transactionCleanups.length === 0) {
 | 
			
		||||
        throw error.create(errorComputeChanges)
 | 
			
		||||
      }
 | 
			
		||||
      const keys = new Map()
 | 
			
		||||
      const target = this.target
 | 
			
		||||
      const changed = /** @type Set<string|null> */ (this.transaction.changed.get(target))
 | 
			
		||||
@ -172,9 +164,6 @@ export class YEvent {
 | 
			
		||||
  get changes () {
 | 
			
		||||
    let changes = this._changes
 | 
			
		||||
    if (changes === null) {
 | 
			
		||||
      if (this.transaction.doc._transactionCleanups.length === 0) {
 | 
			
		||||
        throw error.create(errorComputeChanges)
 | 
			
		||||
      }
 | 
			
		||||
      const target = this.target
 | 
			
		||||
      const added = set.create()
 | 
			
		||||
      const deleted = set.create()
 | 
			
		||||
@ -264,8 +253,8 @@ const getPathTo = (parent, child) => {
 | 
			
		||||
      let i = 0
 | 
			
		||||
      let c = /** @type {AbstractType<any>} */ (child._item.parent)._start
 | 
			
		||||
      while (c !== child._item && c !== null) {
 | 
			
		||||
        if (!c.deleted && c.countable) {
 | 
			
		||||
          i += c.length
 | 
			
		||||
        if (!c.deleted) {
 | 
			
		||||
          i++
 | 
			
		||||
        }
 | 
			
		||||
        c = c.right
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @module encoding
 | 
			
		||||
 */
 | 
			
		||||
@ -87,7 +88,7 @@ export const writeClientsStructs = (encoder, store, _sm) => {
 | 
			
		||||
      sm.set(client, clock)
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
  getStateVector(store).forEach((_clock, client) => {
 | 
			
		||||
  getStateVector(store).forEach((clock, client) => {
 | 
			
		||||
    if (!_sm.has(client)) {
 | 
			
		||||
      sm.set(client, 0)
 | 
			
		||||
    }
 | 
			
		||||
@ -97,7 +98,8 @@ export const writeClientsStructs = (encoder, store, _sm) => {
 | 
			
		||||
  // Write items with higher client ids first
 | 
			
		||||
  // This heavily improves the conflict algorithm.
 | 
			
		||||
  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 +156,7 @@ export const readClientsStructRefs = (decoder, doc) => {
 | 
			
		||||
          // @type {string|null}
 | 
			
		||||
          const struct = new Item(
 | 
			
		||||
            createID(client, clock),
 | 
			
		||||
            null, // left
 | 
			
		||||
            null, // leftd
 | 
			
		||||
            (info & binary.BIT8) === binary.BIT8 ? decoder.readLeftID() : null, // origin
 | 
			
		||||
            null, // right
 | 
			
		||||
            (info & binary.BIT7) === binary.BIT7 ? decoder.readRightID() : null, // right origin
 | 
			
		||||
@ -178,7 +180,7 @@ export const readClientsStructRefs = (decoder, doc) => {
 | 
			
		||||
 | 
			
		||||
          const struct = new Item(
 | 
			
		||||
            createID(client, clock),
 | 
			
		||||
            null, // left
 | 
			
		||||
            null, // leftd
 | 
			
		||||
            origin, // origin
 | 
			
		||||
            null, // right
 | 
			
		||||
            rightOrigin, // right origin
 | 
			
		||||
@ -211,7 +213,7 @@ export const readClientsStructRefs = (decoder, doc) => {
 | 
			
		||||
 * then we start emptying the stack.
 | 
			
		||||
 *
 | 
			
		||||
 * 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
 | 
			
		||||
 * causally depends on another update.
 | 
			
		||||
@ -250,7 +252,7 @@ const integrateStructs = (transaction, store, clientsStructRefs) => {
 | 
			
		||||
    return nextStructsTarget
 | 
			
		||||
  }
 | 
			
		||||
  let curStructsTarget = getNextStructTarget()
 | 
			
		||||
  if (curStructsTarget === null) {
 | 
			
		||||
  if (curStructsTarget === null && stack.length === 0) {
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -279,14 +281,14 @@ const integrateStructs = (transaction, store, clientsStructRefs) => {
 | 
			
		||||
  const addStackToRestSS = () => {
 | 
			
		||||
    for (const item of stack) {
 | 
			
		||||
      const client = item.id.client
 | 
			
		||||
      const inapplicableItems = clientsStructRefs.get(client)
 | 
			
		||||
      if (inapplicableItems) {
 | 
			
		||||
      const unapplicableItems = clientsStructRefs.get(client)
 | 
			
		||||
      if (unapplicableItems) {
 | 
			
		||||
        // decrement because we weren't able to apply previous operation
 | 
			
		||||
        inapplicableItems.i--
 | 
			
		||||
        restStructs.clients.set(client, inapplicableItems.refs.slice(inapplicableItems.i))
 | 
			
		||||
        unapplicableItems.i--
 | 
			
		||||
        restStructs.clients.set(client, unapplicableItems.refs.slice(unapplicableItems.i))
 | 
			
		||||
        clientsStructRefs.delete(client)
 | 
			
		||||
        inapplicableItems.i = 0
 | 
			
		||||
        inapplicableItems.refs = []
 | 
			
		||||
        unapplicableItems.i = 0
 | 
			
		||||
        unapplicableItems.refs = []
 | 
			
		||||
      } else {
 | 
			
		||||
        // item was the last item on clientsStructRefs and the field was already cleared. Add item to restStructs and continue
 | 
			
		||||
        restStructs.clients.set(client, [item])
 | 
			
		||||
@ -370,7 +372,7 @@ export const writeStructsFromTransaction = (encoder, transaction) => writeClient
 | 
			
		||||
/**
 | 
			
		||||
 * 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 {Doc} ydoc
 | 
			
		||||
@ -451,7 +453,7 @@ export const readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = n
 | 
			
		||||
/**
 | 
			
		||||
 * 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 {Doc} ydoc
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
 | 
			
		||||
import { AbstractType, Item } from '../internals.js' // eslint-disable-line
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  AbstractType // eslint-disable-line
 | 
			
		||||
} from '../internals.js'
 | 
			
		||||
 | 
			
		||||
@ -1,40 +1,20 @@
 | 
			
		||||
 | 
			
		||||
import * as binary from 'lib0/binary'
 | 
			
		||||
import * as decoding from 'lib0/decoding'
 | 
			
		||||
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 map from 'lib0/map'
 | 
			
		||||
import * as math from 'lib0/math'
 | 
			
		||||
import * as string from 'lib0/string'
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  ContentAny,
 | 
			
		||||
  ContentBinary,
 | 
			
		||||
  ContentDeleted,
 | 
			
		||||
  ContentDoc,
 | 
			
		||||
  ContentEmbed,
 | 
			
		||||
  ContentFormat,
 | 
			
		||||
  ContentJSON,
 | 
			
		||||
  ContentString,
 | 
			
		||||
  ContentType,
 | 
			
		||||
  createID,
 | 
			
		||||
  decodeStateVector,
 | 
			
		||||
  readItemContent,
 | 
			
		||||
  readDeleteSet,
 | 
			
		||||
  writeDeleteSet,
 | 
			
		||||
  Skip,
 | 
			
		||||
  mergeDeleteSets,
 | 
			
		||||
  DSEncoderV1,
 | 
			
		||||
  DSEncoderV2,
 | 
			
		||||
  GC,
 | 
			
		||||
  Item,
 | 
			
		||||
  mergeDeleteSets,
 | 
			
		||||
  readDeleteSet,
 | 
			
		||||
  readItemContent,
 | 
			
		||||
  Skip,
 | 
			
		||||
  UpdateDecoderV1,
 | 
			
		||||
  UpdateDecoderV2,
 | 
			
		||||
  UpdateEncoderV1,
 | 
			
		||||
  UpdateEncoderV2,
 | 
			
		||||
  writeDeleteSet,
 | 
			
		||||
  YXmlElement,
 | 
			
		||||
  YXmlHook
 | 
			
		||||
  decodeStateVector,
 | 
			
		||||
  Item, GC, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2 // eslint-disable-line
 | 
			
		||||
} from '../internals.js'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -572,17 +552,17 @@ const finishLazyStructWriting = (lazyWriter) => {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {Uint8Array} update
 | 
			
		||||
 * @param {function(Item|GC|Skip):Item|GC|Skip} blockTransformer
 | 
			
		||||
 * @param {typeof UpdateDecoderV2 | typeof UpdateDecoderV1} YDecoder
 | 
			
		||||
 * @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 lazyDecoder = new LazyStructReader(updateDecoder, false)
 | 
			
		||||
  const updateEncoder = new YEncoder()
 | 
			
		||||
  const lazyWriter = new LazyStructWriter(updateEncoder)
 | 
			
		||||
 | 
			
		||||
  for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) {
 | 
			
		||||
    writeStructToLazyStructWriter(lazyWriter, blockTransformer(curr), 0)
 | 
			
		||||
    writeStructToLazyStructWriter(lazyWriter, curr, 0)
 | 
			
		||||
  }
 | 
			
		||||
  finishLazyStructWriting(lazyWriter)
 | 
			
		||||
  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 {ObfuscatorOptions} [opts]
 | 
			
		||||
 */
 | 
			
		||||
export const obfuscateUpdate = (update, opts) => convertUpdateFormat(update, createObfuscator(opts), UpdateDecoderV1, UpdateEncoderV1)
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {Uint8Array} update
 | 
			
		||||
 * @param {ObfuscatorOptions} [opts]
 | 
			
		||||
 */
 | 
			
		||||
export const obfuscateUpdateV2 = (update, opts) => convertUpdateFormat(update, createObfuscator(opts), UpdateDecoderV2, UpdateEncoderV2)
 | 
			
		||||
export const convertUpdateFormatV1ToV2 = update => convertUpdateFormat(update, UpdateDecoderV1, UpdateEncoderV2)
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {Uint8Array} update
 | 
			
		||||
 */
 | 
			
		||||
export const convertUpdateFormatV1ToV2 = update => convertUpdateFormat(update, f.id, UpdateDecoderV1, UpdateEncoderV2)
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {Uint8Array} update
 | 
			
		||||
 */
 | 
			
		||||
export const convertUpdateFormatV2ToV1 = update => convertUpdateFormat(update, f.id, UpdateDecoderV2, UpdateEncoderV1)
 | 
			
		||||
export const convertUpdateFormatV2ToV1 = update => convertUpdateFormat(update, 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.
 | 
			
		||||
 *
 | 
			
		||||
 * The v1 documents were generated with Yjs v13.2.0 based on the randomisized tests.
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
 | 
			
		||||
import * as Y from '../src/index.js'
 | 
			
		||||
import * as t from 'lib0/testing'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -15,28 +15,15 @@ import * as relativePositions from './relativePositions.tests.js'
 | 
			
		||||
import { runTests } from 'lib0/testing'
 | 
			
		||||
import { isBrowser, isNode } from 'lib0/environment'
 | 
			
		||||
import * as log from 'lib0/logging'
 | 
			
		||||
import { environment } from 'lib0'
 | 
			
		||||
 | 
			
		||||
if (isBrowser) {
 | 
			
		||||
  log.createVConsole(document.body)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @type {any}
 | 
			
		||||
 */
 | 
			
		||||
const tests = {
 | 
			
		||||
runTests({
 | 
			
		||||
  doc, map, array, text, xml, encoding, undoredo, compatibility, snapshot, updates, relativePositions
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const run = async () => {
 | 
			
		||||
  if (environment.isNode) {
 | 
			
		||||
    // tests.nodejs = await import('./node.tests.js')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const success = await runTests(tests)
 | 
			
		||||
}).then(success => {
 | 
			
		||||
  /* istanbul ignore next */
 | 
			
		||||
  if (isNode) {
 | 
			
		||||
    process.exit(success ? 0 : 1)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
run()
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
 | 
			
		||||
import * as Y from '../src/index.js'
 | 
			
		||||
import * as t from 'lib0/testing'
 | 
			
		||||
 | 
			
		||||
@ -85,26 +86,6 @@ export const testRelativePositionCase6 = tc => {
 | 
			
		||||
  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
 | 
			
		||||
 */
 | 
			
		||||
@ -121,25 +102,3 @@ export const testRelativePositionAssociationDifference = tc => {
 | 
			
		||||
  t.assert(posRight != null && posRight.index === 2)
 | 
			
		||||
  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'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {t.TestCase} _tc
 | 
			
		||||
 * @param {t.TestCase} tc
 | 
			
		||||
 */
 | 
			
		||||
export const testBasic = _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 => {
 | 
			
		||||
export const testBasicRestoreSnapshot = tc => {
 | 
			
		||||
  const doc = new Y.Doc({ gc: false })
 | 
			
		||||
  doc.getArray('array').insert(0, ['hello'])
 | 
			
		||||
  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 snap = Y.snapshot(doc)
 | 
			
		||||
  snap.sv.set(9999, 0)
 | 
			
		||||
@ -58,16 +31,16 @@ export const testEmptyRestoreSnapshot = _tc => {
 | 
			
		||||
  t.compare(docRestored.getArray().toArray(), [])
 | 
			
		||||
  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 docRestored2 = Y.createDocFromSnapshot(doc, snap2)
 | 
			
		||||
  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 })
 | 
			
		||||
  doc.getArray('array').insert(0, [new Y.Map()])
 | 
			
		||||
  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 })
 | 
			
		||||
  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 })
 | 
			
		||||
  doc.getArray('array').insert(0, ['item1'])
 | 
			
		||||
  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 })
 | 
			
		||||
  doc.getArray('array').insert(0, ['item1'])
 | 
			
		||||
  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 })
 | 
			
		||||
  doc.getArray('array').insert(0, ['item1', 'item2', 'item3'])
 | 
			
		||||
  doc.getArray('array').delete(1)
 | 
			
		||||
@ -196,28 +169,3 @@ export const testDependentChanges = tc => {
 | 
			
		||||
  const docRestored1 = Y.createDocFromSnapshot(array1.doc, snap)
 | 
			
		||||
  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 prng from 'lib0/prng'
 | 
			
		||||
import * as encoding from 'lib0/encoding'
 | 
			
		||||
@ -34,7 +35,7 @@ export const encV1 = {
 | 
			
		||||
  mergeUpdates: Y.mergeUpdates,
 | 
			
		||||
  applyUpdate: Y.applyUpdate,
 | 
			
		||||
  logUpdate: Y.logUpdate,
 | 
			
		||||
  updateEventName: /** @type {'update'} */ ('update'),
 | 
			
		||||
  updateEventName: 'update',
 | 
			
		||||
  diffUpdate: Y.diffUpdate
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -43,7 +44,7 @@ export const encV2 = {
 | 
			
		||||
  mergeUpdates: Y.mergeUpdatesV2,
 | 
			
		||||
  applyUpdate: Y.applyUpdateV2,
 | 
			
		||||
  logUpdate: Y.logUpdateV2,
 | 
			
		||||
  updateEventName: /** @type {'updateV2'} */ ('updateV2'),
 | 
			
		||||
  updateEventName: 'updateV2',
 | 
			
		||||
  diffUpdate: Y.diffUpdateV2
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -355,9 +356,8 @@ export const compare = users => {
 | 
			
		||||
      return true
 | 
			
		||||
    })
 | 
			
		||||
    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)
 | 
			
		||||
    t.compare(Y.encodeSnapshot(Y.snapshot(users[i])), Y.encodeSnapshot(Y.snapshot(users[i + 1])))
 | 
			
		||||
  }
 | 
			
		||||
  users.map(u => u.destroy())
 | 
			
		||||
}
 | 
			
		||||
@ -412,6 +412,25 @@ export const compareStructStores = (ss1, ss2) => {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {import('../src/internals.js').DeleteSet} ds1
 | 
			
		||||
 * @param {import('../src/internals.js').DeleteSet} ds2
 | 
			
		||||
 */
 | 
			
		||||
export const compareDS = (ds1, ds2) => {
 | 
			
		||||
  t.assert(ds1.clients.size === ds2.clients.size)
 | 
			
		||||
  ds1.clients.forEach((deleteItems1, client) => {
 | 
			
		||||
    const deleteItems2 = /** @type {Array<import('../src/internals.js').DeleteItem>} */ (ds2.clients.get(client))
 | 
			
		||||
    t.assert(deleteItems2 !== undefined && deleteItems1.length === deleteItems2.length)
 | 
			
		||||
    for (let i = 0; i < deleteItems1.length; i++) {
 | 
			
		||||
      const di1 = deleteItems1[i]
 | 
			
		||||
      const di2 = deleteItems2[i]
 | 
			
		||||
      if (di1.clock !== di2.clock || di1.len !== di2.len) {
 | 
			
		||||
        t.fail('DeleteSets dont match')
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @template T
 | 
			
		||||
 * @callback InitTestObjectCallback
 | 
			
		||||
 | 
			
		||||
@ -3,46 +3,6 @@ import { init } from './testHelper.js' // eslint-disable-line
 | 
			
		||||
import * as Y from '../src/index.js'
 | 
			
		||||
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
 | 
			
		||||
 */
 | 
			
		||||
@ -116,72 +76,6 @@ export const testEmptyTypeScope = _tc => {
 | 
			
		||||
  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
 | 
			
		||||
@ -781,33 +675,3 @@ export const testUndoDeleteInMap = (tc) => {
 | 
			
		||||
  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 * as encoding from 'lib0/encoding'
 | 
			
		||||
import * as decoding from 'lib0/decoding'
 | 
			
		||||
import * as object from 'lib0/object'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @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(Y.Doc):Uint8Array} Enc.encodeStateVector
 | 
			
		||||
 * @property {function(Uint8Array):Uint8Array} Enc.encodeStateVectorFromUpdate
 | 
			
		||||
 * @property {'update'|'updateV2'} Enc.updateEventName
 | 
			
		||||
 * @property {string} Enc.updateEventName
 | 
			
		||||
 * @property {string} Enc.description
 | 
			
		||||
 * @property {function(Uint8Array, Uint8Array):Uint8Array} Enc.diffUpdate
 | 
			
		||||
 */
 | 
			
		||||
@ -139,6 +138,7 @@ export const testKeyEncoding = tc => {
 | 
			
		||||
 */
 | 
			
		||||
const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
 | 
			
		||||
  const cases = []
 | 
			
		||||
 | 
			
		||||
  // Case 1: Simple case, simply merge everything
 | 
			
		||||
  cases.push(enc.mergeUpdates(updates))
 | 
			
		||||
 | 
			
		||||
@ -169,7 +169,7 @@ const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
 | 
			
		||||
  // t.info('Target State: ')
 | 
			
		||||
  // enc.logUpdate(targetState)
 | 
			
		||||
 | 
			
		||||
  cases.forEach((mergedUpdates) => {
 | 
			
		||||
  cases.forEach((mergedUpdates, i) => {
 | 
			
		||||
    // t.info('State Case $' + i + ':')
 | 
			
		||||
    // enc.logUpdate(updates)
 | 
			
		||||
    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 => {
 | 
			
		||||
  encoders.forEach((enc) => {
 | 
			
		||||
export const testMergeUpdates1 = tc => {
 | 
			
		||||
  encoders.forEach((enc, i) => {
 | 
			
		||||
    t.info(`Using encoder: ${enc.description}`)
 | 
			
		||||
    const ydoc = new Y.Doc({ gc: false })
 | 
			
		||||
    const updates = /** @type {Array<Uint8Array>} */ ([])
 | 
			
		||||
@ -299,59 +299,8 @@ export const testMergePendingUpdates = tc => {
 | 
			
		||||
  Y.applyUpdate(yDoc5, update4)
 | 
			
		||||
  Y.applyUpdate(yDoc5, serverUpdates[4])
 | 
			
		||||
  // @ts-ignore
 | 
			
		||||
  const _update5 = Y.encodeStateAsUpdate(yDoc5) // eslint-disable-line
 | 
			
		||||
  const update5 = Y.encodeStateAsUpdate(yDoc5) // eslint-disable-line
 | 
			
		||||
 | 
			
		||||
  const yText5 = yDoc5.getText('textBlock')
 | 
			
		||||
  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 prng from 'lib0/prng'
 | 
			
		||||
import * as math from 'lib0/math'
 | 
			
		||||
import * as env from 'lib0/environment'
 | 
			
		||||
 | 
			
		||||
const isDevMode = env.getVariable('node_env') === 'development'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {t.TestCase} tc
 | 
			
		||||
@ -20,28 +17,6 @@ export const testBasicUpdate = tc => {
 | 
			
		||||
  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
 | 
			
		||||
 */
 | 
			
		||||
@ -355,29 +330,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
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
@ -8,54 +8,6 @@ import * as Y from '../src/index.js'
 | 
			
		||||
import * as t from 'lib0/testing'
 | 
			
		||||
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
 | 
			
		||||
 */
 | 
			
		||||
@ -369,11 +321,11 @@ export const testObserversUsingObservedeep = tc => {
 | 
			
		||||
  /**
 | 
			
		||||
   * @type {Array<Array<string|number>>}
 | 
			
		||||
   */
 | 
			
		||||
  const paths = []
 | 
			
		||||
  const pathes = []
 | 
			
		||||
  let calls = 0
 | 
			
		||||
  map0.observeDeep(events => {
 | 
			
		||||
    events.forEach(event => {
 | 
			
		||||
      paths.push(event.path)
 | 
			
		||||
      pathes.push(event.path)
 | 
			
		||||
    })
 | 
			
		||||
    calls++
 | 
			
		||||
  })
 | 
			
		||||
@ -381,35 +333,7 @@ export const testObserversUsingObservedeep = tc => {
 | 
			
		||||
  map0.get('map').set('array', new Y.Array())
 | 
			
		||||
  map0.get('map').get('array').insert(0, ['content'])
 | 
			
		||||
  t.assert(calls === 3)
 | 
			
		||||
  t.compare(paths, [[], ['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']])
 | 
			
		||||
  t.compare(pathes, [[], ['map'], ['map', 'array']])
 | 
			
		||||
  compare(users)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -376,7 +376,7 @@ export const testDeltaBug = _tc => {
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      insert: '\n',
 | 
			
		||||
      // This attributes has only list and no table-cell-line
 | 
			
		||||
      // This attibutes has only list and no table-cell-line
 | 
			
		||||
      attributes: {
 | 
			
		||||
        list: {
 | 
			
		||||
          rowspan: '1',
 | 
			
		||||
@ -1746,27 +1746,6 @@ export const testBasicFormat = tc => {
 | 
			
		||||
  compare(users)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {t.TestCase} tc
 | 
			
		||||
 */
 | 
			
		||||
export const testFalsyFormats = tc => {
 | 
			
		||||
  const { users, text0 } = init(tc, { users: 2 })
 | 
			
		||||
  let delta
 | 
			
		||||
  text0.observe(event => {
 | 
			
		||||
    delta = event.delta
 | 
			
		||||
  })
 | 
			
		||||
  text0.insert(0, 'abcde', { falsy: false })
 | 
			
		||||
  t.compare(text0.toDelta(), [{ insert: 'abcde', attributes: { falsy: false } }])
 | 
			
		||||
  t.compare(delta, [{ insert: 'abcde', attributes: { falsy: false } }])
 | 
			
		||||
  text0.format(1, 3, { falsy: true })
 | 
			
		||||
  t.compare(text0.toDelta(), [{ insert: 'a', attributes: { falsy: false } }, { insert: 'bcd', attributes: { falsy: true } }, { insert: 'e', attributes: { falsy: false } }])
 | 
			
		||||
  t.compare(delta, [{ retain: 1 }, { retain: 3, attributes: { falsy: true } }])
 | 
			
		||||
  text0.format(2, 1, { falsy: false })
 | 
			
		||||
  t.compare(text0.toDelta(), [{ insert: 'a', attributes: { falsy: false } }, { insert: 'b', attributes: { falsy: true } }, { insert: 'c', attributes: { falsy: false } }, { insert: 'd', attributes: { falsy: true } }, { insert: 'e', attributes: { falsy: false } }])
 | 
			
		||||
  t.compare(delta, [{ retain: 2 }, { retain: 1, attributes: { falsy: false } }])
 | 
			
		||||
  compare(users)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {t.TestCase} _tc
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
@ -3,33 +3,6 @@ import * as Y from '../src/index.js'
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 */
 | 
			
		||||
@ -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 ytext = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText))
 | 
			
		||||
  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 yxml = ydoc.getXmlFragment()
 | 
			
		||||
  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 yxml = ydoc.getXmlFragment()
 | 
			
		||||
  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 yxml = ydoc.getXmlFragment()
 | 
			
		||||
  const first = new Y.XmlText('text')
 | 
			
		||||
@ -189,6 +162,7 @@ export const testClone = _tc => {
 | 
			
		||||
  const third = new Y.XmlElement('p')
 | 
			
		||||
  yxml.push([first, second, third])
 | 
			
		||||
  t.compareArrays(yxml.toArray(), [first, second, third])
 | 
			
		||||
 | 
			
		||||
  const cloneYxml = yxml.clone()
 | 
			
		||||
  ydoc.getArray('copyarr').insert(0, [cloneYxml])
 | 
			
		||||
  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 yxml = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText))
 | 
			
		||||
  const delta = [
 | 
			
		||||
@ -209,15 +183,3 @@ export const testFormattingBug = _tc => {
 | 
			
		||||
  yxml.applyDelta(delta)
 | 
			
		||||
  t.compare(yxml.toDelta(), delta)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {t.TestCase} _tc
 | 
			
		||||
 */
 | 
			
		||||
export const testElement = _tc => {
 | 
			
		||||
  const ydoc = new Y.Doc()
 | 
			
		||||
  const yxmlel = ydoc.getXmlElement()
 | 
			
		||||
  const text1 = new Y.XmlText('text1')
 | 
			
		||||
  const text2 = new Y.XmlText('text2')
 | 
			
		||||
  yxmlel.insert(0, [text1, text2])
 | 
			
		||||
  t.compareArrays(yxmlel.toArray(), [text1, text2])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user