{"id":13394134,"url":"https://github.com/yjs/yjs","last_synced_at":"2026-02-27T02:13:10.656Z","repository":{"id":19160989,"uuid":"22392639","full_name":"yjs/yjs","owner":"yjs","description":"Shared data types for building collaborative software","archived":false,"fork":false,"pushed_at":"2026-02-25T15:27:29.000Z","size":73442,"stargazers_count":21287,"open_issues_count":127,"forks_count":744,"subscribers_count":122,"default_branch":"main","last_synced_at":"2026-02-25T18:59:16.742Z","etag":null,"topics":["collaboration","collaborative-editing","crdt","decentralized","offline-first","p2p","peer-to-peer","realtime","shared-editing","yjs"],"latest_commit_sha":null,"homepage":"https://docs.yjs.dev","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yjs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":"funding.json","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"dmonad","patreon":null,"open_collective":"y-collective","ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"custom":null}},"created_at":"2014-07-29T19:29:45.000Z","updated_at":"2026-02-25T17:12:16.000Z","dependencies_parsed_at":"2025-12-17T22:05:24.631Z","dependency_job_id":null,"html_url":"https://github.com/yjs/yjs","commit_stats":{"total_commits":1678,"total_committers":112,"mean_commits":"14.982142857142858","dds":0.4743742550655542,"last_synced_commit":"8dc1296a0bd15e70c573160593c2617e0d772f01"},"previous_names":["y-js/yjs","dadamonad/yatta","rwth-acis/yjs"],"tags_count":348,"template":false,"template_full_name":null,"purl":"pkg:github/yjs/yjs","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yjs%2Fyjs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yjs%2Fyjs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yjs%2Fyjs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yjs%2Fyjs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yjs","download_url":"https://codeload.github.com/yjs/yjs/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yjs%2Fyjs/sbom","scorecard":{"id":489902,"data":{"date":"2025-07-07","repo":{"name":"github.com/yjs/yjs","commit":"4120d7216117cac0c870e9c250ff84d500b6c42e"},"scorecard":{"version":"v5.2.1-18-gbb9c347d","commit":"bb9c347dff6349d986baab6578a46d68a5524c62"},"score":3.9,"checks":[{"name":"Maintained","score":10,"reason":"30 commit(s) and 8 issue activity found in the last 90 days -- score normalized to 10","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/bb9c347dff6349d986baab6578a46d68a5524c62/docs/checks.md#maintained"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/bb9c347dff6349d986baab6578a46d68a5524c62/docs/checks.md#dangerous-workflow"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/bb9c347dff6349d986baab6578a46d68a5524c62/docs/checks.md#packaging"}},{"name":"Pinned-Dependencies","score":3,"reason":"dependency not pinned by hash detected -- score normalized to 3","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/node.js.yml:22: update your workflow using https://app.stepsecurity.io/secureworkflow/yjs/yjs/node.js.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/node.js.yml:24: update your workflow using https://app.stepsecurity.io/secureworkflow/yjs/yjs/node.js.yml/main?enable=pin","Info:   0 out of   2 GitHub-owned GitHubAction dependencies pinned","Info:   1 out of   1 npmCommand dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/bb9c347dff6349d986baab6578a46d68a5524c62/docs/checks.md#pinned-dependencies"}},{"name":"Code-Review","score":2,"reason":"Found 6/23 approved changesets -- score normalized to 2","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/bb9c347dff6349d986baab6578a46d68a5524c62/docs/checks.md#code-review"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/bb9c347dff6349d986baab6578a46d68a5524c62/docs/checks.md#binary-artifacts"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/node.js.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/bb9c347dff6349d986baab6578a46d68a5524c62/docs/checks.md#token-permissions"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/bb9c347dff6349d986baab6578a46d68a5524c62/docs/checks.md#cii-best-practices"}},{"name":"License","score":9,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Warn: project license file does not contain an FSF or OSI license."],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/bb9c347dff6349d986baab6578a46d68a5524c62/docs/checks.md#license"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/bb9c347dff6349d986baab6578a46d68a5524c62/docs/checks.md#fuzzing"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/bb9c347dff6349d986baab6578a46d68a5524c62/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'main'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/bb9c347dff6349d986baab6578a46d68a5524c62/docs/checks.md#branch-protection"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/bb9c347dff6349d986baab6578a46d68a5524c62/docs/checks.md#security-policy"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 13 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/bb9c347dff6349d986baab6578a46d68a5524c62/docs/checks.md#sast"}},{"name":"Vulnerabilities","score":1,"reason":"9 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275","Warn: Project is vulnerable to: GHSA-jc84-3g44-wf2q","Warn: Project is vulnerable to: GHSA-cxjh-pqwp-8mfp","Warn: Project is vulnerable to: GHSA-p6mc-m468-83gw","Warn: Project is vulnerable to: GHSA-rp65-9cf3-cjxr","Warn: Project is vulnerable to: GHSA-gcx4-mw62-g8wm","Warn: Project is vulnerable to: GHSA-pqhp-25j4-6hq9","Warn: Project is vulnerable to: GHSA-mxhp-79qh-mcx6"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/bb9c347dff6349d986baab6578a46d68a5524c62/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-19T18:48:58.066Z","repository_id":19160989,"created_at":"2025-08-19T18:48:58.067Z","updated_at":"2025-08-19T18:48:58.067Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29882992,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-26T23:51:21.483Z","status":"online","status_checked_at":"2026-02-27T02:00:06.759Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["collaboration","collaborative-editing","crdt","decentralized","offline-first","p2p","peer-to-peer","realtime","shared-editing","yjs"],"created_at":"2024-07-30T17:01:10.067Z","updated_at":"2026-02-27T02:13:10.612Z","avatar_url":"https://github.com/yjs.png","language":"JavaScript","readme":"\n# ![Yjs](https://yjs.dev/images/logo/yjs-120x120.png)\n\n\u003e A CRDT framework with a powerful abstraction of shared data\n\nYjs is a [CRDT implementation](#yjs-crdt-algorithm) that exposes its internal\ndata structure as *shared types*. Shared types are common data types like `Map`\nor `Array` with superpowers: changes are automatically distributed to other\npeers and merged without merge conflicts.\n\nYjs is **network agnostic** (p2p!), supports many existing **rich text\neditors**, **offline editing**, **version snapshots**, **undo/redo** and\n**shared cursors**. It scales well with an unlimited number of users and is well\nsuited for even large documents.\n\n* Demos: [https://github.com/yjs/yjs-demos](https://github.com/yjs/yjs-demos)\n* Discuss: [https://discuss.yjs.dev](https://discuss.yjs.dev)\n* Chat: [Gitter](https://gitter.im/Yjs/community) | [Discord](https://discord.gg/T3nqMT6qbM)\n* Benchmark Yjs vs. Automerge:\n  [https://github.com/dmonad/crdt-benchmarks](https://github.com/dmonad/crdt-benchmarks)\n* Podcast [**\"Yjs Deep Dive into real time collaborative editing solutions\":**](https://www.tag1consulting.com/blog/deep-dive-real-time-collaborative-editing-solutions-tagteamtalk-001-0)\n* Podcast [**\"Google Docs-style editing in Gutenberg with the YJS framework\":**](https://publishpress.com/blog/yjs/)\n\n:construction_worker_woman: If you are looking for professional support, please\nconsider supporting this project via a \"support contract\" on\n[GitHub Sponsors](https://github.com/sponsors/dmonad). I will attend your issues\nquicker and we can discuss questions and problems in regular video conferences.\nOtherwise you can find help on our community [discussion board](https://discuss.yjs.dev).\n\n## Sponsorship\n\nPlease contribute to the project financially - especially if your company relies\non Yjs. [![Become a Sponsor](https://img.shields.io/static/v1?label=Become%20a%20Sponsor\u0026message=%E2%9D%A4\u0026logo=GitHub\u0026style=flat\u0026color=d42f2d)](https://github.com/sponsors/dmonad)\n\n## Professional Support\n\n* [Support Contract with the Maintainer](https://github.com/sponsors/dmonad) -\nBy contributing financially to the open-source Yjs project, you can receive\nprofessional support directly from the author. This includes the opportunity for\nweekly video calls to discuss your specific challenges.\n* [Synergy Codes](https://synergycodes.com/yjs-services/) - Specializing in\nconsulting and developing real-time collaborative editing solutions for visual\napps, Synergy Codes focuses on interactive diagrams, complex graphs, charts, and\nvarious data visualization types. Their expertise empowers developers to build\nengaging and interactive visual experiences leveraging the power of Yjs. See\ntheir work in action at [Visual Collaboration\nShowcase](https://yjs-diagram.synergy.codes/).\n\n## Who is using Yjs\n\n* [AFFiNE](https://affine.pro/) A local-first, privacy-first, open source\n  knowledge base. :star2:\n* [Huly](https://huly.io/) - Open Source All-in-One Project Management Platform :star2:\n* [Cargo](https://cargo.site/) Site builder for designers and artists :star2:\n* [Gitbook](https://gitbook.com) Knowledge management for technical teams :star2:\n* [Evernote](https://evernote.com) Note-taking app :star2:\n* [Lessonspace](https://thelessonspace.com) Enterprise platform for virtual\n  classrooms and online training :star2:\n* [Ellipsus](ellipsus.com) - Collaborative writing app for storytelling etc.\n  Supports versioning, change attribution, and \"blame\". A solution for the whole\n  publishing process (also selling) :star:\n* [Dynaboard](https://dynaboard.com/) Build web apps collaboratively. :star:\n* [Relm](https://www.relm.us/) A collaborative gameworld for teamwork and\n  community. :star:\n* [Room.sh](https://room.sh/) A meeting application with integrated\n  collaborative drawing, editing, and coding tools. :star:\n* [Nimbus Note](https://nimbusweb.me/note.php) A note-taking app designed by\n  Nimbus Web. :star:\n* [Pluxbox RadioManager](https://getradiomanager.com/) A web-based app to\n  collaboratively organize radio broadcasts. :star:\n* [modyfi](https://www.modyfi.com) - Modyfi is the design platform built for\n  multidisciplinary designers. Design, generate, animate, and more — without\n  switching between apps. :star:\n* [Sana](https://sanalabs.com/) A learning platform with collaborative text\n  editing powered by Yjs.\n* [Serenity Notes](https://www.serenity.re/en/notes) End-to-end encrypted\n  collaborative notes app.\n* [PRSM](https://prsm.uk/) Collaborative mind-mapping and system visualisation.\n  *[(source)](https://github.com/micrology/prsm)*\n* [Alldone](https://alldone.app/) A next-gen project management and\n  collaboration platform.\n* [Living Spec](https://livingspec.com/) A modern way for product teams to collaborate.\n* [Slidebeamer](https://slidebeamer.com/) Presentation app.\n* [BlockSurvey](https://blocksurvey.io) End-to-end encryption for your forms/surveys.\n* [Skiff](https://skiff.org/) Private, decentralized workspace.\n* [JupyterLab](https://jupyter.org/) Collaborative computational Notebooks\n* [JupyterCad](https://jupytercad.readthedocs.io/en/latest/) Extension to\n  JupyterLab that enables collaborative editing of 3d FreeCAD Models.\n* [JupyterGIS](https://github.com/geojupyter/jupytergis) Collaborative GIS\n  (Geographic Information System) editor in Jupyter\n* [Hyperquery](https://hyperquery.ai/) A collaborative data workspace for\n  sharing analyses, documentation, spreadsheets, and dashboards.\n* [Nosgestesclimat](https://nosgestesclimat.fr/groupe) The french carbon\n  footprint calculator has a group P2P mode based on yjs\n* [oorja.io](https://oorja.io) Online meeting spaces extensible with\n  collaborative apps, end-to-end encrypted.\n* [LegendKeeper](https://legendkeeper.com) Collaborative campaign planner and\n  worldbuilding app for tabletop RPGs.\n* [IllumiDesk](https://illumidesk.com/) Build courses and content with A.I.\n* [btw](https://www.btw.so) Open-source Medium alternative\n* [AWS SageMaker](https://aws.amazon.com/sagemaker/) Tools for building Machine\n  Learning Models\n* [linear](https://linear.app) Streamline issues, projects, and product roadmaps.\n* [Arkiter](https://www.arkiter.com/) - Live interview software\n* [Appflowy](https://www.appflowy.io/) - They use Yrs\n* [Multi.app](https://multi.app) - Multiplayer app sharing: Point, draw and edit\n  in shared apps as if they're on your computer. They are using Yrs.\n* [AppMaster](https://appmaster.io) A No-Code platform for creating\n  production-ready applications with source code generation.\n* [Synthesia](https://www.synthesia.io) - Collaborative Video Editor\n* [thinkdeli](https://thinkdeli.com) - A fast and simple notes app powered by AI\n* [ourboard](https://github.com/raimohanska/ourboard) - A collaborative whiteboard\n  application\n* [Ellie.ai](https://ellie.ai) - Data Product Design and Collaboration\n* [GoPeer](https://gopeer.org/) - Collaborative tutoring\n* [screen.garden](https://screen.garden) - Collaborative backend for PKM apps.\n* [NextCloud](https://nextcloud.com/) - Content Collaboration Platform\n* [keystatic](https://github.com/Thinkmill/keystatic) - git-based CMS\n* [QDAcity](https://qdacity.com) - Collaborative qualitative data analysis platform\n* [Kanbert](https://kanbert.com) - Project management software\n* [Eclipse Theia](https://github.com/eclipse-theia/theia) - A cloud \u0026 desktop\n  IDE that runs in the browser.\n* [ScienHub](https://scienhub.com) - Collaborative LaTeX editor in the browser.\n* [Open Collaboration Tools](https://www.open-collab.tools/) - Collaborative\nediting for your IDE or custom editor\n* [Typst](https://typst.app/) - Compose, edit, and automate technical documents\n* [Kedyou](https://kedyou.com/) - Digital workspaces for tutoring\n* [Lightpage](https://lightpage.com/) - Personal living notebook\n* [reearth-flow](https://github.com/reearth/reearth-flow) -\n  Collaboratively calculate and convert various data\n* [ProtonMail | Proton Docs](https://proton.me/drive/docs) - E2E encrypted\n  collaborative documents in Proton Drive.\n* [Theneo](https://www.theneo.io/) - AI-powered API docs with live team collaboration.\n\n## Table of Contents\n\n* [Overview](#overview)\n  * [Bindings](#bindings)\n  * [Providers](#providers)\n  * [Tooling](#tooling)\n  * [Ports](#ports)\n* [Getting Started](#getting-started)\n* [API](#api)\n  * [Shared Types](#shared-types)\n  * [Y.Doc](#ydoc)\n  * [Document Updates](#document-updates)\n  * [Relative Positions](#relative-positions)\n  * [Y.UndoManager](#yundomanager)\n* [Yjs CRDT Algorithm](#yjs-crdt-algorithm)\n* [License and Author](#license-and-author)\n\n## Overview\n\nThis repository contains a collection of shared types that can be observed for\nchanges and manipulated concurrently. Network functionality and two-way-bindings\nare implemented in separate modules.\n\n### Bindings\n\n| Name | Cursors | Binding |  Demo |\n|---|:-:|---|---|\n| [ProseMirror](https://prosemirror.net/) \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; | ✔ | [y-prosemirror](https://github.com/yjs/y-prosemirror) | [demo](https://demos.yjs.dev/prosemirror/prosemirror.html) |\n| [Quill](https://quilljs.com/) | ✔ | [y-quill](https://github.com/yjs/y-quill) | [demo](https://demos.yjs.dev/quill/quill.html) |\n| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](https://github.com/yjs/y-codemirror) | [demo](https://demos.yjs.dev/codemirror/codemirror.html) |\n| [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](https://github.com/yjs/y-monaco) | [demo](https://demos.yjs.dev/monaco/monaco.html) |\n| [Ace](https://ace.c9.io/) | ✔ | [y-ace](https://github.com/bajrangCoder/y-ace) | |\n| [Slate](https://github.com/ianstormtaylor/slate) | ✔ | [slate-yjs](https://github.com/bitphinix/slate-yjs) | [demo](https://bitphinix.github.io/slate-yjs-example) |\n| [BlockSuite](https://github.com/toeverything/blocksuite) | ✔ | (native) | [demo](https://blocksuite-toeverything.vercel.app/?init) |\n| [Lexical](https://lexical.dev/) | ✔ | (native) | [demo](https://lexical.dev/docs/collaboration/react#see-it-in-action) |\n| [BlockNote](https://www.blocknotejs.org/docs/collaboration/real-time-collaboration) | ✔ | [y-prosemirror](https://github.com/yjs/y-prosemirror) | [demo](https://www.blocknotejs.org/docs/collaboration/real-time-collaboration) |\n| [Tiptap](https://tiptap.dev/) | ✔ | [y-prosemirror](https://github.com/yjs/y-prosemirror) | [demo](https://template.tiptap.dev/preview/templates/simple) |\n| [Milkdown](https://github.com/Milkdown/milkdown) | ✔ | [y-prosemirror](https://github.com/yjs/y-prosemirror) | [demo](https://milkdown.dev/playground) |\n| [Superdoc](https://superdoc.dev/) | ✔ | (native) | [demo](https://superdoc.dev/) |\n| [valtio](https://github.com/pmndrs/valtio) |  | [valtio-yjs](https://github.com/dai-shi/valtio-yjs) | [demo](https://codesandbox.io/s/valtio-yjs-demo-ox3iy) |\n| [immer](https://github.com/immerjs/immer) |  | [immer-yjs](https://github.com/sep2/immer-yjs) | [demo](https://codesandbox.io/s/immer-yjs-demo-6e0znb) |\n| React | | [react-yjs](https://github.com/nikgraf/react-yjs) | [demo](https://react-yjs-example.vercel.app/) |\n| React / Vue / Svelte / MobX | | [SyncedStore](https://syncedstore.org) | [demo](https://syncedstore.org/docs/react) |\n| [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) |\n| [PSPDFKit](https://www.nutrient.io/) |  | [yjs-pspdfkit](https://github.com/hoangqwe159/yjs-pspdfkit) | [demo](https://github.com/hoangqwe159/yjs-pspdfkit) |\n| [Rows n'Columns](https://www.rowsncolumns.app/) | ✔ | [@rowsncolumns/y-spreadsheet](https://docs.rowsncolumns.app/collaboration/yjs-collaboration) | |\n\n### Utilities\n\nTools that extend the core functionality of Yjs.\n\n\u003cdl\u003e\n  \u003cdt\u003e\u003ca href=\"https://github.com/yjs/y-utility\"\u003ey-utility\u003c/a\u003e\u003c/dt\u003e\n  \u003cdd\u003e\nLibrary with \u003ccode\u003eYMultiDocUndoManager\u003c/code\u003e (undo/redo across Yjs docs) and\n\u003ccode\u003eYKeyValue\u003c/code\u003e (optimized key-value store).\n  \u003c/dd\u003e\n  \u003cdt\u003e\n\u003ca href=\"https://github.com/Tulip-Writer/yjs-orderedtree\"\u003e yjs-orderedtree \u003c/a\u003e 🌳\n  \u003c/dt\u003e\n  \u003cdd\u003e\nClass for ordered trees via Y.Map. Handles \u003ccode\u003einsert\u003c/code\u003e,\n\u003ccode\u003edelete\u003c/code\u003e, and \u003ccode\u003emove\u003c/code\u003e operations for folder-like\nhierarchies.  \n  \u003c/dd\u003e\n\n\u003c/dl\u003e\n\n### Providers\n\nSetting up the communication between clients, managing awareness information,\nand storing shared data for offline usage is quite a hassle. **Providers**\nmanage all that for you and are the perfect starting point for your\ncollaborative app.\n\n\u003e This list of providers is incomplete. Please open PRs to add your providers to\n\u003e this list!\n\n#### Connection Providers\n\n\u003cdl\u003e\n  \u003cdt\u003e\u003ca href=\"https://github.com/yjs/y-websocket\"\u003ey-websocket\u003c/a\u003e\u003c/dt\u003e\n  \u003cdd\u003e\nA module that contains a simple websocket backend and a websocket client that\nconnects to that backend. \u003ca href=\"https://github.com/yjs/y-redis/\"\u003e\u003cb\u003ey-redis\u003c/b\u003e\u003c/a\u003e,\n\u003cb\u003ey-sweet\u003c/b\u003e, \u003cb\u003eypy-websocket\u003c/b\u003e, \u003cb\u003eyrs-warp\u003c/b\u003e and \u003ca href=\"https://tiptap.dev/docs/hocuspocus/introduction\"\u003e\n\u003cb\u003eHocuspocus\u003c/b\u003e\u003c/a\u003e (see below) are alternative\nbackends to y-websocket.\n  \u003c/dd\u003e\n  \u003cdt\u003e\u003ca href=\"https://github.com/yjs/y-webrtc\"\u003ey-webrtc\u003c/a\u003e\u003c/dt\u003e\n  \u003cdd\u003e\nPropagates document updates peer-to-peer using WebRTC. The peers exchange\nsignaling data over signaling servers. Publicly available signaling servers\nare available. Communication over the signaling servers can be encrypted by\nproviding a shared secret, keeping the connection information and the shared\ndocument private.\n  \u003c/dd\u003e\n  \u003cdt\u003e\u003ca href=\"https://github.com/liveblocks/liveblocks\"\u003e@liveblocks/yjs \u003c/a\u003e 🌟\u003c/dt\u003e\n  \u003cdd\u003e\n\u003ca href=\"https://liveblocks.io/document/yjs\"\u003eLiveblocks Yjs\u003c/a\u003e provides a fully\nhosted WebSocket infrastructure and persisted data store for Yjs\ndocuments. No configuration or maintenance is required. It also features\nYjs webhook events, REST API to read and update Yjs documents, and a\nbrowser DevTools extension.\n  \u003c/dd\u003e\n  \u003cdt\u003e\u003ca href=\"https://github.com/ueberdosis/hocuspocus\"\u003eHocuspocus\u003c/a\u003e ⭐\u003c/dt\u003e\n  \u003cdd\u003e\nA standalone extensible yjs server with sqlite persistence, webhooks, auth and more.\n  \u003c/dd\u003e\n  \u003cdt\u003e\u003ca href=\"https://github.com/nperez0111/teleportal\"\u003eteleportal\u003c/a\u003e\u003c/dt\u003e\n  \u003cdd\u003e\nBuild your own Y.js sync server: any storage, any JS runtime, any transport.\n  \u003c/dd\u003e\n  \u003cdt\u003e\u003ca href=\"https://github.com/drifting-in-space/y-sweet\"\u003ey-sweet\u003c/a\u003e\u003c/dt\u003e\n  \u003cdd\u003e\nA standalone yjs server with persistence to S3 or filesystem. They offer a\n\u003ca href=\"https://y-sweet.cloud\"\u003ecloud service\u003c/a\u003e as well.\n  \u003c/dd\u003e\n  \u003cdt\u003e\u003ca href=\"https://docs.superviz.com/collaboration/integrations/YJS/overview\"\u003e@superviz/yjs\u003c/a\u003e\u003c/dt\u003e\n  \u003cdd\u003e\n    SuperViz Yjs Provider comes with a secure, scalable real-time infrastructure\n    for Yjs documents, fully compatible with a set of real-time\n    collaboration components offered by SuperViz. This solution ensures\n    synchronization, offline editing, and real-time updates, enabling\n    multiple users to collaborate effectively within shared workspaces.\n  \u003c/dd\u003e\n  \u003cdt\u003e\u003ca href=\"https://docs.partykit.io/reference/y-partykit-api/\"\u003ePartyKit\u003c/a\u003e\u003c/dt\u003e\n  \u003cdd\u003e\nCloud service for building multiplayer apps.\n  \u003c/dd\u003e\n  \u003c/dd\u003e\n    \u003cdt\u003e\u003ca href=\"https://github.com/pluv-io/pluv\"\u003e@pluv/crdt-yjs\u003c/a\u003e\u003c/dt\u003e\n  \u003cdd\u003e\nUse \u003ca href=\"https://pluv.io/docs/storage/using-yjs\"\u003epluv.io\u003c/a\u003e as a\nfull-featured backend for Yjs. pluv.io can either be be used on its\nfully-managed WebSocket infrastructure, or self-hosted on Cloudflare Workers\nand Node.js runtimes. Offers a typesafe API with authentication, webhooks,\nrooms, and more.\n  \u003cdt\u003e\u003ca href=\"https://github.com/marcopolo/y-libp2p\"\u003ey-libp2p\u003c/a\u003e\u003c/dt\u003e\n  \u003cdd\u003e\nUses \u003ca href=\"https://libp2p.io/\"\u003elibp2p\u003c/a\u003e to propagate updates via\n\u003ca href=\"https://github.com/libp2p/specs/tree/master/pubsub/gossipsub\"\u003eGossipSub\u003c/a\u003e.\nAlso includes a peer-sync mechanism to catch up on missed updates.\n  \u003c/dd\u003e\n  \u003cdt\u003e\u003ca href=\"https://github.com/yjs/y-dat\"\u003ey-dat\u003c/a\u003e\u003c/dt\u003e\n  \u003cdd\u003e\n[WIP] Write document updates efficiently to the dat network using\n\u003ca href=\"https://github.com/kappa-db/multifeed\"\u003emultifeed\u003c/a\u003e. Each client has\nan append-only log of CRDT local updates (hypercore). Multifeed manages and sync\nhypercores and y-dat listens to changes and applies them to the Yjs document.\n\u003c/dd\u003e\n  \u003cdt\u003e\u003ca href=\"https://github.com/yousefED/matrix-crdt\"\u003eMatrix-CRDT\u003c/a\u003e\u003c/dt\u003e\n  \u003cdd\u003e\nUse \u003ca href=\"https://www.matrix.org\"\u003eMatrix\u003c/a\u003e as an off-the-shelf backend for\nYjs by using the \u003ca href=\"https://github.com/yousefED/matrix-crdt\"\u003eMatrixProvider\u003c/a\u003e.\nUse Matrix as transport and storage of Yjs updates, so you can focus building\nyour client app and Matrix can provide powerful features like Authentication,\nAuthorization, Federation, hosting (self-hosting or SaaS) and even End-to-End\nEncryption (E2EE).\n  \u003c/dd\u003e\n  \u003cdt\u003e\u003ca href=\"https://github.com/y-crdt/yrb-actioncable\"\u003eyrb-actioncable\u003c/a\u003e\u003c/dt\u003e\n  \u003cdd\u003e\nAn ActionCable companion for Yjs clients. There is a fitting\n\u003ca href=\"https://github.com/y-crdt/yrb-redis\"\u003eredis extension\u003c/a\u003e as well.\n  \u003c/dd\u003e\n  \u003cdt\u003e\u003ca href=\"https://github.com/y-crdt/ypy-websocket\"\u003eypy-websocket\u003c/a\u003e\u003c/dt\u003e\n  \u003cdd\u003e\nWebsocket backend, written in Python.\n  \u003c/dd\u003e\n  \u003cdt\u003e\u003ca href=\"https://tinybase.org/\"\u003eTinybase\u003c/a\u003e\u003c/dt\u003e\n  \u003cdd\u003e\nThe reactive data store for local-first apps. They support multiple CRDTs and\n    different network technologies.\n  \u003c/dd\u003e\n  \u003cdt\u003e\u003ca href=\"https://codeberg.org/webxdc/y-webxdc\"\u003ey-webxdc\u003c/a\u003e\u003c/dt\u003e\n  \u003cdd\u003e\nProvider for sharing data in \u003ca href=\"https://webxdc.org\"\u003ewebxdc chat apps\u003c/a\u003e.\n  \u003c/dd\u003e\n  \u003cdt\u003e\u003ca href=\"https://www.secsync.com/\"\u003esecsync\u003c/a\u003e\u003c/dt\u003e\n  \u003cdd\u003e\nAn architecture to relay end-to-end encrypted CRDTs over a central service.\n  \u003c/dd\u003e\n  \u003cdt\u003e\u003ca href=\"https://www.npmjs.com/package/@electric-sql/y-electric\"\u003ey-electric\u003c/a\u003e\u003c/dt\u003e\n  \u003cdd\u003e\n    Sync Yjs over \u003ca href=\"https://electric-sql.com/\"\u003eElectricSQL\u003c/a\u003e.\n  \u003c/dd\u003e\n  \u003cdt\u003e\u003ca href=\"https://github.com/TimoWilhelm/yjs-cf-ws-provider\"\u003eyjs-cf-ws-provider\u003c/a\u003e\u003c/dt\u003e\n  \u003cdd\u003e\n    Cloudflare provider for Yjs based on durable objects.\n  \u003c/dd\u003e\n  \u003cdt\u003e\u003ca href=\"https://github.com/yousefED/nostr-crdt\"\u003enostr-crdt\u003c/a\u003e\u003c/dt\u003e\n  \u003cdd\u003e\n    Sync Yjs over \u003ca href=\"https://github.com/nostr-protocol/\"\u003enostr\u003c/a\u003e.\n  \u003c/dd\u003e\n\u003c/dl\u003e\n\n#### Persistence Providers\n\n\u003cdl\u003e\n  \u003cdt\u003e\u003ca href=\"https://github.com/yjs/y-indexeddb\"\u003ey-indexeddb\u003c/a\u003e\u003c/dt\u003e\n  \u003cdd\u003e\nEfficiently persists document updates to the browsers indexeddb database.\nThe document is immediately available and only diffs need to be synced through the\nnetwork provider.\n  \u003c/dd\u003e\n  \u003cdt\u003e\u003ca href=\"https://github.com/MaxNoetzold/y-mongodb-provider\"\u003ey-mongodb-provider\u003c/a\u003e\u003c/dt\u003e\n  \u003cdd\u003e\nAdds persistent storage to a server with MongoDB. Can be used with the\ny-websocket provider.\n  \u003c/dd\u003e\n  \u003cdt\u003e\u003ca href=\"https://github.com/podraven/y-fire\"\u003ey-fire\u003c/a\u003e\u003c/dt\u003e\n  \u003cdd\u003e\nA database and connection provider for Yjs based on Firestore.\n  \u003c/dd\u003e\n  \u003cdt\u003e\u003ca href=\"https://github.com/malte-j/y-op-sqlite\"\u003ey-op-sqlite\u003c/a\u003e\u003c/dt\u003e\n  \u003cdd\u003e\n  Persist YJS updates in your React Native app using\n   \u003ca href=\"https://github.com/OP-Engineering/op-sqlite\"\u003eop-sqlite\u003c/a\u003e\n  , the fastest SQLite library for React Native.  \n  \u003c/dd\u003e\n  \u003cdt\u003e\u003ca href=\"https://github.com/MaxNoetzold/y-postgresql\"\u003ey-postgresql\u003c/a\u003e\u003c/dt\u003e\n  \u003cdd\u003e\n  Provides persistent storage for a web server using PostgreSQL and\n  is easily compatible with y-websocket.  \n  \u003c/dd\u003e\n  \u003cdt\u003e\u003ca href=\"https://github.com/kapv89/k_yrs_go\"\u003ek_yrs_go\u003c/a\u003e\u003c/dt\u003e\n  \u003cdd\u003e\n  Golang database server for YJS CRDT using Postgres + Redis\n  \u003c/dd\u003e\n  \u003cdt\u003e\u003ca href=\"https://github.com/malte-j/y-op-sqlite\"\u003ey-op-sqlite\u003c/a\u003e\u003c/dt\u003e\n  \u003cdd\u003e\n Yjs persistence provider for op-sqlite\n  \u003c/dd\u003e\n\n\u003c/dl\u003e\n\n### Tooling\n\n* [y-sweet debugger](https://y-sweet.cloud/advanced/debugger)\n* [liveblocks devtools](https://liveblocks.io/devtools)\n* [Yjs inspector](https://inspector.yjs.dev)\n\n### Ports\n\nThere are several Yjs-compatible ports to other programming languages.\n\n* [y-octo](https://github.com/toeverything/y-octo) - Rust implementation by\n[AFFiNE](https://affine.pro)\n* [y-crdt](https://github.com/y-crdt/y-crdt) - Rust implementation with multiple\nlanguage bindings to other languages\n  * [yrs](https://github.com/y-crdt/y-crdt/tree/main/yrs) - Rust interface\n  * [ypy](https://github.com/y-crdt/ypy) - Python binding\n  * [yrb](https://github.com/y-crdt/yrb) - Ruby binding\n  * [yswift](https://github.com/y-crdt/yswift) - Swift binding\n  * [yffi](https://github.com/y-crdt/y-crdt/tree/main/yffi) - C-FFI\n  * [ywasm](https://github.com/y-crdt/y-crdt/tree/main/ywasm) - WASM binding\n  * [y_ex](https://github.com/satoren/y_ex) - Elixir bindings\n* [ycs](https://github.com/yjs/ycs) - .Net compatible C# implementation.\n\n## Getting Started\n\nInstall Yjs and a provider with your favorite package manager:\n\n```sh\nnpm i yjs y-websocket\n```\n\nStart the y-websocket server:\n\n```sh\nPORT=1234 node ./node_modules/y-websocket/bin/server.cjs\n```\n\n### Example: Observe types\n\n```js\nimport * as Y from 'yjs';\n\nconst doc = new Y.Doc();\nconst yarray = doc.getArray('my-array')\nyarray.observe(event =\u003e {\n  console.log('yarray was modified')\n})\n// every time a local or remote client modifies yarray, the observer is called\nyarray.insert(0, ['val']) // =\u003e \"yarray was modified\"\n```\n\n### Example: Nest types\n\nRemember, shared types are just plain old data types. The only limitation is\nthat a shared type must exist only once in the shared document.\n\n```js\nconst ymap = doc.getMap('map')\nconst foodArray = new Y.Array()\nfoodArray.insert(0, ['apple', 'banana'])\nymap.set('food', foodArray)\nymap.get('food') === foodArray // =\u003e true\nymap.set('fruit', foodArray) // =\u003e Error! foodArray is already defined\n```\n\nNow you understand how types are defined on a shared document. Next you can jump\nto the [demo repository](https://github.com/yjs/yjs-demos) or continue reading\nthe API docs.\n\n### Example: Using and combining providers\n\nAny of the Yjs providers can be combined with each other. So you can sync data\nover different network technologies.\n\nIn most cases you want to use a network provider (like y-websocket or y-webrtc)\nin combination with a persistence provider (y-indexeddb in the browser).\nPersistence allows you to load the document faster and to persist data that is\ncreated while offline.\n\nFor the sake of this demo we combine two different network providers with a\npersistence provider.\n\n```js\nimport * as Y from 'yjs'\nimport { WebrtcProvider } from 'y-webrtc'\nimport { WebsocketProvider } from 'y-websocket'\nimport { IndexeddbPersistence } from 'y-indexeddb'\n\nconst ydoc = new Y.Doc()\n\n// this allows you to instantly get the (cached) documents data\nconst indexeddbProvider = new IndexeddbPersistence('count-demo', ydoc)\nindexeddbProvider.whenSynced.then(() =\u003e {\n  console.log('loaded data from indexed db')\n})\n\n// Sync clients with the y-webrtc provider.\nconst webrtcProvider = new WebrtcProvider('count-demo', ydoc)\n\n// Sync clients with the y-websocket provider\nconst websocketProvider = new WebsocketProvider(\n  'wss://demos.yjs.dev', 'count-demo', ydoc\n)\n\n// array of numbers which produce a sum\nconst yarray = ydoc.getArray('count')\n\n// observe changes of the sum\nyarray.observe(event =\u003e {\n  // print updates when the data changes\n  console.log('new sum: ' + yarray.toArray().reduce((a,b) =\u003e a + b))\n})\n\n// add 1 to the sum\nyarray.push([1]) // =\u003e \"new sum: 1\"\n```\n\n## API\n\n```js\nimport * as Y from 'yjs'\n```\n\n### Shared Types\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cb\u003eY.Array\u003c/b\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n  \u003cp\u003e\nA shareable Array-like type that supports efficient insert/delete of elements\nat any position. Internally it uses a linked list of Arrays that is split when\nnecessary.\n  \u003c/p\u003e\n  \u003cpre\u003econst yarray = new Y.Array()\u003c/pre\u003e\n  \u003cdl\u003e\n    \u003cb\u003e\u003ccode\u003e\nY.Array.from(Array\u0026lt;object|boolean|Array|string|number|null|Uint8Array|Y.Type\u0026gt;):\nY.Array\n    \u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003eAn alternative factory function to create a Y.Array based on existing content.\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eparent:Y.AbstractType|null\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003einsert(index:number, content:Array\u0026lt;object|boolean|Array|string|number|null|Uint8Array|Y.Type\u0026gt;)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\nInsert content at \u003cvar\u003eindex\u003c/var\u003e. Note that content is an array of elements.\nI.e. \u003ccode\u003earray.insert(0, [1])\u003c/code\u003e splices the list and inserts 1 at\nposition 0.\n    \u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003epush(Array\u0026lt;Object|boolean|Array|string|number|null|Uint8Array|Y.Type\u0026gt;)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eunshift(Array\u0026lt;Object|boolean|Array|string|number|null|Uint8Array|Y.Type\u0026gt;)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003edelete(index:number, length:number)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eget(index:number)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eslice(start:number, end:number):Array\u0026lt;Object|boolean|Array|string|number|null|Uint8Array|Y.Type\u0026gt;\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003eRetrieve a range of content\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003elength:number\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\n      \u003ccode\u003e\nforEach(function(value:object|boolean|Array|string|number|null|Uint8Array|Y.Type,\n index:number, array: Y.Array))\n      \u003c/code\u003e\n    \u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003emap(function(T, number, YArray):M):Array\u0026lt;M\u0026gt;\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eclone(): Y.Array\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\nClone all values into a fresh Y.Array instance. The returned type can be\nincluded into the Yjs document.\n    \u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003etoArray():Array\u0026lt;object|boolean|Array|string|number|null|Uint8Array|Y.Type\u0026gt;\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003eCopies the content of this YArray to a new Array.\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003etoJSON():Array\u0026lt;Object|boolean|Array|string|number|null\u0026gt;\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\nCopies the content of this YArray to a new Array. It transforms all child types\nto JSON using their \u003ccode\u003etoJSON\u003c/code\u003e method.\n    \u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003e[Symbol.Iterator]\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\n      Returns an YArray Iterator that contains the values for each index in the array.\n      \u003cpre\u003efor (let value of yarray) { .. }\u003c/pre\u003e\n    \u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eobserve(function(YArrayEvent, Transaction):void)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\nAdds an event listener to this type that will be called synchronously every time\nthis type is modified. In the case this type is modified in the event listener,\nthe event listener will be called again after the current event listener returns.\n    \u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eunobserve(function(YArrayEvent, Transaction):void)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\n      Removes an \u003ccode\u003eobserve\u003c/code\u003e event listener from this type.\n    \u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eobserveDeep(function(Array\u0026lt;YEvent\u0026gt;, Transaction):void)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\nAdds an event listener to this type that will be called synchronously every time\nthis type or any of its children is modified. In the case this type is modified\nin the event listener, the event listener will be called again after the current\nevent listener returns. The event listener receives all Events created by itself\nor any of its children.\n    \u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eunobserveDeep(function(Array\u0026lt;YEvent\u0026gt;, Transaction):void)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\n      Removes an \u003ccode\u003eobserveDeep\u003c/code\u003e event listener from this type.\n    \u003c/dd\u003e\n  \u003c/dl\u003e\n\u003c/details\u003e\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cb\u003eY.Map\u003c/b\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n  \u003cp\u003e\n    A shareable Map type.\n  \u003c/p\u003e\n  \u003cpre\u003e\u003ccode\u003econst ymap = new Y.Map()\u003c/code\u003e\u003c/pre\u003e\n  \u003cdl\u003e\n    \u003cb\u003e\u003ccode\u003eparent:Y.AbstractType|null\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003esize: number\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003eTotal number of key/value pairs.\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eget(key:string):object|boolean|string|number|null|Uint8Array|Y.Type\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eset(key:string, value:object|boolean|string|number|null|Uint8Array|Y.Type)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003edelete(key:string)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003ehas(key:string):boolean\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eclear()\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003eRemoves all elements from this YMap.\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eclone():Y.Map\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003eClone this type into a fresh Yjs type.\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003etoJSON():Object\u0026lt;string, Object|boolean|Array|string|number|null|Uint8Array\u0026gt;\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\nCopies the \u003ccode\u003e[key,value]\u003c/code\u003e pairs of this YMap to a new Object.It\ntransforms all child types to JSON using their \u003ccode\u003etoJSON\u003c/code\u003e method.\n    \u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eforEach(function(value:object|boolean|Array|string|number|null|Uint8Array|Y.Type,\n key:string, map: Y.Map))\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\n      Execute the provided function once for every key-value pair.\n    \u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003e[Symbol.Iterator]\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\n      Returns an Iterator of \u003ccode\u003e[key, value]\u003c/code\u003e pairs.\n      \u003cpre\u003efor (let [key, value] of ymap) { .. }\u003c/pre\u003e\n    \u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eentries()\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\n      Returns an Iterator of \u003ccode\u003e[key, value]\u003c/code\u003e pairs.\n    \u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003evalues()\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\n      Returns an Iterator of all values.\n    \u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003ekeys()\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\n      Returns an Iterator of all keys.\n    \u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eobserve(function(YMapEvent, Transaction):void)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\nAdds an event listener to this type that will be called synchronously every time\nthis type is modified. In the case this type is modified in the event listener,\nthe event listener will be called again after the current event listener returns.\n    \u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eunobserve(function(YMapEvent, Transaction):void)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\n      Removes an \u003ccode\u003eobserve\u003c/code\u003e event listener from this type.\n    \u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eobserveDeep(function(Array\u0026lt;YEvent\u0026gt;, Transaction):void)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\nAdds an event listener to this type that will be called synchronously every time\nthis type or any of its children is modified. In the case this type is modified\nin the event listener, the event listener will be called again after the current\nevent listener returns. The event listener receives all Events created by itself\nor any of its children.\n    \u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eunobserveDeep(function(Array\u0026lt;YEvent\u0026gt;, Transaction):void)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\n      Removes an \u003ccode\u003eobserveDeep\u003c/code\u003e event listener from this type.\n    \u003c/dd\u003e\n  \u003c/dl\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cb\u003eY.Text\u003c/b\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n  \u003cp\u003e\nA shareable type that is optimized for shared editing on text. It allows to\nassign properties to ranges in the text. This makes it possible to implement\nrich-text bindings to this type.\n  \u003c/p\u003e\n  \u003cp\u003e\nThis type can also be transformed to the\n\u003ca href=\"https://quilljs.com/docs/delta\"\u003edelta format\u003c/a\u003e. Similarly the\nYTextEvents compute changes as deltas.\n  \u003c/p\u003e\n  \u003cpre\u003econst ytext = new Y.Text()\u003c/pre\u003e\n  \u003cdl\u003e\n    \u003cb\u003e\u003ccode\u003eparent:Y.AbstractType|null\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003einsert(index:number, content:string, [formattingAttributes:Object\u0026lt;string,string\u0026gt;])\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\n      Insert a string at \u003cvar\u003eindex\u003c/var\u003e and assign formatting attributes to it.\n      \u003cpre\u003eytext.insert(0, 'bold text', { bold: true })\u003c/pre\u003e\n    \u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003edelete(index:number, length:number)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eformat(index:number, length:number, formattingAttributes:Object\u0026lt;string,string\u0026gt;)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003eAssign formatting attributes to a range in the text\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eapplyDelta(delta: Delta, opts:Object\u0026lt;string,any\u0026gt;)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\n        See \u003ca href=\"https://quilljs.com/docs/delta/\"\u003eQuill Delta\u003c/a\u003e\n        Can set options for preventing remove ending newLines, default is true.\n        \u003cpre\u003eytext.applyDelta(delta, { sanitize: false })\u003c/pre\u003e\n    \u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003elength:number\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003etoString():string\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003eTransforms this type, without formatting options, into a string.\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003etoJSON():string\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003eSee \u003ccode\u003etoString\u003c/code\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003etoDelta():Delta\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\nTransforms this type to a \u003ca href=\"https://quilljs.com/docs/delta/\"\u003eQuill Delta\u003c/a\u003e\n    \u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eobserve(function(YTextEvent, Transaction):void)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\nAdds an event listener to this type that will be called synchronously every time\nthis type is modified. In the case this type is modified in the event listener,\nthe event listener will be called again after the current event listener returns.\n    \u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eunobserve(function(YTextEvent, Transaction):void)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\n      Removes an \u003ccode\u003eobserve\u003c/code\u003e event listener from this type.\n    \u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eobserveDeep(function(Array\u0026lt;YEvent\u0026gt;, Transaction):void)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\nAdds an event listener to this type that will be called synchronously every time\nthis type or any of its children is modified. In the case this type is modified\nin the event listener, the event listener will be called again after the current\nevent listener returns. The event listener receives all Events created by itself\nor any of its children.\n    \u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eunobserveDeep(function(Array\u0026lt;YEvent\u0026gt;, Transaction):void)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\n      Removes an \u003ccode\u003eobserveDeep\u003c/code\u003e event listener from this type.\n    \u003c/dd\u003e\n  \u003c/dl\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cb\u003eY.XmlFragment\u003c/b\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n  \u003cp\u003e\n    A container that holds an Array of Y.XmlElements.\n  \u003c/p\u003e\n  \u003cpre\u003e\u003ccode\u003econst yxml = new Y.XmlFragment()\u003c/code\u003e\u003c/pre\u003e\n  \u003cdl\u003e\n    \u003cb\u003e\u003ccode\u003eparent:Y.AbstractType|null\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003efirstChild:Y.XmlElement|Y.XmlText|null\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003einsert(index:number, content:Array\u0026lt;Y.XmlElement|Y.XmlText\u0026gt;)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003edelete(index:number, length:number)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eget(index:number)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eslice(start:number, end:number):Array\u0026lt;Y.XmlElement|Y.XmlText\u0026gt;\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003eRetrieve a range of content\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003elength:number\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eclone():Y.XmlFragment\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003eClone this type into a fresh Yjs type.\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003etoArray():Array\u0026lt;Y.XmlElement|Y.XmlText\u0026gt;\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003eCopies the children to a new Array.\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003etoString():string\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003eGet the XML serialization of all descendants.\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003etoJSON():string\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003eSee \u003ccode\u003etoString\u003c/code\u003e.\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003ecreateTreeWalker(filter: function(AbstractType\u0026lt;any\u0026gt;):boolean):Iterable\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003eCreate an Iterable that walks through the children.\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eobserve(function(YXmlEvent, Transaction):void)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\nAdds an event listener to this type that will be called synchronously every time\nthis type is modified. In the case this type is modified in the event listener,\nthe event listener will be called again after the current event listener returns.\n    \u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eunobserve(function(YXmlEvent, Transaction):void)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\n      Removes an \u003ccode\u003eobserve\u003c/code\u003e event listener from this type.\n    \u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eobserveDeep(function(Array\u0026lt;YEvent\u0026gt;, Transaction):void)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\nAdds an event listener to this type that will be called synchronously every time\nthis type or any of its children is modified. In the case this type is modified\nin the event listener, the event listener will be called again after the current\nevent listener returns. The event listener receives all Events created by itself\nor any of its children.\n    \u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eunobserveDeep(function(Array\u0026lt;YEvent\u0026gt;, Transaction):void)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\n      Removes an \u003ccode\u003eobserveDeep\u003c/code\u003e event listener from this type.\n    \u003c/dd\u003e\n  \u003c/dl\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cb\u003eY.XmlElement\u003c/b\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n  \u003cp\u003e\nA shareable type that represents an XML Element. It has a \u003ccode\u003enodeName\u003c/code\u003e,\nattributes, and a list of children. But it makes no effort to validate its\ncontent and be actually XML compliant.\n  \u003c/p\u003e\n  \u003cpre\u003e\u003ccode\u003econst yxml = new Y.XmlElement()\u003c/code\u003e\u003c/pre\u003e\n  \u003cdl\u003e\n    \u003cb\u003e\u003ccode\u003eparent:Y.AbstractType|null\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003efirstChild:Y.XmlElement|Y.XmlText|null\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003enextSibling:Y.XmlElement|Y.XmlText|null\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eprevSibling:Y.XmlElement|Y.XmlText|null\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003einsert(index:number, content:Array\u0026lt;Y.XmlElement|Y.XmlText\u0026gt;)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003edelete(index:number, length:number)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eget(index:number)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003elength:number\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003esetAttribute(attributeName:string, attributeValue:string)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eremoveAttribute(attributeName:string)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003egetAttribute(attributeName:string):string\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003egetAttributes():Object\u0026lt;string,string\u0026gt;\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eget(i:number):Y.XmlElement|Y.XmlText\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003eRetrieve the i-th element.\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eslice(start:number, end:number):Array\u0026lt;Y.XmlElement|Y.XmlText\u0026gt;\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003eRetrieve a range of content\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eclone():Y.XmlElement\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003eClone this type into a fresh Yjs type.\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003etoArray():Array\u0026lt;Y.XmlElement|Y.XmlText\u0026gt;\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003eCopies the children to a new Array.\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003etoString():string\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003eGet the XML serialization of all descendants.\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003etoJSON():string\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003eSee \u003ccode\u003etoString\u003c/code\u003e.\u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eobserve(function(YXmlEvent, Transaction):void)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\nAdds an event listener to this type that will be called synchronously every\ntime this type is modified. In the case this type is modified in the event\nlistener, the event listener will be called again after the current event\nlistener returns.\n    \u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eunobserve(function(YXmlEvent, Transaction):void)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\n      Removes an \u003ccode\u003eobserve\u003c/code\u003e event listener from this type.\n    \u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eobserveDeep(function(Array\u0026lt;YEvent\u0026gt;, Transaction):void)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\nAdds an event listener to this type that will be called synchronously every time\nthis type or any of its children is modified. In the case this type is modified\nin the event listener, the event listener will be called again after the current\nevent listener returns. The event listener receives all Events created by itself\nor any of its children.\n    \u003c/dd\u003e\n    \u003cb\u003e\u003ccode\u003eunobserveDeep(function(Array\u0026lt;YEvent\u0026gt;, Transaction):void)\u003c/code\u003e\u003c/b\u003e\n    \u003cdd\u003e\n      Removes an \u003ccode\u003eobserveDeep\u003c/code\u003e event listener from this type.\n    \u003c/dd\u003e\n  \u003c/dl\u003e\n\u003c/details\u003e\n\n### Y.Doc\n\n```js\nconst doc = new Y.Doc()\n```\n\n\u003cdl\u003e\n  \u003cb\u003e\u003ccode\u003eclientID\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003eA unique id that identifies this client. (readonly)\u003c/dd\u003e\n  \u003cb\u003e\u003ccode\u003egc\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003e\nWhether garbage collection is enabled on this doc instance. Set `doc.gc = false`\nin order to disable gc and be able to restore old content. See https://github.com/yjs/yjs#yjs-crdt-algorithm\nfor more information about gc in Yjs.\n  \u003c/dd\u003e\n  \u003cb\u003e\u003ccode\u003etransact(function(Transaction):void [, origin:any])\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003e\nEvery change on the shared document happens in a transaction. Observer calls and\nthe \u003ccode\u003eupdate\u003c/code\u003e event are called after each transaction. You should\n\u003ci\u003ebundle\u003c/i\u003e changes into a single transaction to reduce the amount of event\ncalls. I.e. \u003ccode\u003edoc.transact(() =\u003e { yarray.insert(..); ymap.set(..) })\u003c/code\u003e\ntriggers a single change event. \u003cbr\u003eYou can specify an optional \u003ccode\u003eorigin\u003c/code\u003e\nparameter that is stored on \u003ccode\u003etransaction.origin\u003c/code\u003e and\n\u003ccode\u003eon('update', (update, origin) =\u003e ..)\u003c/code\u003e.\n  \u003c/dd\u003e\n  \u003cb\u003e\u003ccode\u003etoJSON():any\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003e\nDeprecated: It is recommended to call toJSON directly on the shared types.\nConverts the entire document into a js object, recursively traversing each yjs\ntype. Doesn't log types that have not been defined (using\n\u003ccode\u003eydoc.getType(..)\u003c/code\u003e).\n  \u003c/dd\u003e\n  \u003cb\u003e\u003ccode\u003eget(string, Y.[TypeClass]):[Type]\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003eDefine a shared type.\u003c/dd\u003e\n  \u003cb\u003e\u003ccode\u003egetArray(string):Y.Array\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003eDefine a shared Y.Array type. Is equivalent to \u003ccode\u003ey.get(string, Y.Array)\u003c/code\u003e.\u003c/dd\u003e\n  \u003cb\u003e\u003ccode\u003egetMap(string):Y.Map\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003eDefine a shared Y.Map type. Is equivalent to \u003ccode\u003ey.get(string, Y.Map)\u003c/code\u003e.\u003c/dd\u003e\n  \u003cb\u003e\u003ccode\u003egetText(string):Y.Text\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003eDefine a shared Y.Text type. Is equivalent to \u003ccode\u003ey.get(string, Y.Text)\u003c/code\u003e.\u003c/dd\u003e\n  \u003cb\u003e\u003ccode\u003egetXmlElement(string, string):Y.XmlElement\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003eDefine a shared Y.XmlElement type. Is equivalent to \u003ccode\u003ey.get(string, Y.XmlElement)\u003c/code\u003e.\u003c/dd\u003e\n  \u003cb\u003e\u003ccode\u003egetXmlFragment(string):Y.XmlFragment\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003eDefine a shared Y.XmlFragment type. Is equivalent to \u003ccode\u003ey.get(string, Y.XmlFragment)\u003c/code\u003e.\u003c/dd\u003e\n  \u003cb\u003e\u003ccode\u003eon(string, function)\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003eRegister an event listener on the shared type\u003c/dd\u003e\n  \u003cb\u003e\u003ccode\u003eoff(string, function)\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003eUnregister an event listener from the shared type\u003c/dd\u003e\n\u003c/dl\u003e\n\n#### Y.Doc Events\n\n\u003cdl\u003e\n  \u003cb\u003e\u003ccode\u003eon('update', function(updateMessage:Uint8Array, origin:any, Y.Doc):void)\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003e\nListen to document updates. Document updates must be transmitted to all other\npeers. You can apply document updates in any order and multiple times. Use `updateV2`\nto receive V2 events.\n  \u003c/dd\u003e\n  \u003cb\u003e\u003ccode\u003eon('beforeTransaction', function(Y.Transaction, Y.Doc):void)\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003eEmitted before each transaction.\u003c/dd\u003e\n  \u003cb\u003e\u003ccode\u003eon('afterTransaction', function(Y.Transaction, Y.Doc):void)\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003eEmitted after each transaction.\u003c/dd\u003e\n  \u003cb\u003e\u003ccode\u003eon('beforeAllTransactions', function(Y.Doc):void)\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003e\nTransactions can be nested (e.g. when an event within a transaction calls another\ntransaction). Emitted before the first transaction.\n  \u003c/dd\u003e\n  \u003cb\u003e\u003ccode\u003eon('afterAllTransactions', function(Y.Doc, Array\u0026lt;Y.Transaction\u0026gt;):void)\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003eEmitted after the last transaction is cleaned up.\u003c/dd\u003e\n\u003c/dl\u003e\n\n### Document Updates\n\nChanges on the shared document are encoded into *document updates*. Document\nupdates are *commutative* and *idempotent*. This means that they can be applied\nin any order and multiple times.\n\n#### Example: Listen to update events and apply them on remote client\n\n```js\nconst doc1 = new Y.Doc()\nconst doc2 = new Y.Doc()\n\ndoc1.on('update', update =\u003e {\n  Y.applyUpdate(doc2, update)\n})\n\ndoc2.on('update', update =\u003e {\n  Y.applyUpdate(doc1, update)\n})\n\n// All changes are also applied to the other document\ndoc1.getArray('myarray').insert(0, ['Hello doc2, you got this?'])\ndoc2.getArray('myarray').get(0) // =\u003e 'Hello doc2, you got this?'\n```\n\nYjs internally maintains a [state vector](#state-vector) that denotes the next\nexpected clock from each client. In a different interpretation it holds the\nnumber of structs created by each client. When two clients sync, you can either\nexchange the complete document structure or only the differences by sending the\nstate vector to compute the differences.\n\n#### Example: Sync two clients by exchanging the complete document structure\n\n```js\nconst state1 = Y.encodeStateAsUpdate(ydoc1)\nconst state2 = Y.encodeStateAsUpdate(ydoc2)\nY.applyUpdate(ydoc1, state2)\nY.applyUpdate(ydoc2, state1)\n```\n\n#### Example: Sync two clients by computing the differences\n\nThis example shows how to sync two clients with the minimal amount of exchanged\ndata by computing only the differences using the state vector of the remote\nclient. Syncing clients using the state vector requires another roundtrip, but\ncan save a lot of bandwidth.\n\n```js\nconst stateVector1 = Y.encodeStateVector(ydoc1)\nconst stateVector2 = Y.encodeStateVector(ydoc2)\nconst diff1 = Y.encodeStateAsUpdate(ydoc1, stateVector2)\nconst diff2 = Y.encodeStateAsUpdate(ydoc2, stateVector1)\nY.applyUpdate(ydoc1, diff2)\nY.applyUpdate(ydoc2, diff1)\n```\n\n#### Example: Syncing clients without loading the Y.Doc\n\nIt is possible to sync clients and compute delta updates without loading the Yjs\ndocument to memory. Yjs exposes an API to compute the differences directly on the\nbinary document updates.\n\n```js\n// encode the current state as a binary buffer\nlet currentState1 = Y.encodeStateAsUpdate(ydoc1)\nlet currentState2 = Y.encodeStateAsUpdate(ydoc2)\n// now we can continue syncing clients using state vectors without using the Y.Doc\nydoc1.destroy()\nydoc2.destroy()\n\nconst stateVector1 = Y.encodeStateVectorFromUpdate(currentState1)\nconst stateVector2 = Y.encodeStateVectorFromUpdate(currentState2)\nconst diff1 = Y.diffUpdate(currentState1, stateVector2)\nconst diff2 = Y.diffUpdate(currentState2, stateVector1)\n\n// sync clients\ncurrentState1 = Y.mergeUpdates([currentState1, diff2])\ncurrentState2 = Y.mergeUpdates([currentState2, diff1])\n```\n\n#### Obfuscating Updates\n\nIf one of your users runs into a weird bug (e.g. the rich-text editor throws\nerror messages), then you don't have to request the full document from your\nuser. Instead, they can obfuscate the document (i.e. replace the content with\nmeaningless generated content) before sending it to you. Note that someone might\nstill deduce the type of content by looking at the general structure of the\ndocument. But this is much better than requesting the original document.\n\nObfuscated updates contain all the CRDT-related data that is required for\nmerging. So it is safe to merge obfuscated updates.\n\n```javascript\nconst ydoc = new Y.Doc()\n// perform some changes..\nydoc.getText().insert(0, 'hello world')\nconst update = Y.encodeStateAsUpdate(ydoc)\n// the below update contains scrambled data\nconst obfuscatedUpdate = Y.obfuscateUpdate(update)\nconst ydoc2 = new Y.Doc()\nY.applyUpdate(ydoc2, obfuscatedUpdate)\nydoc2.getText().toString() // =\u003e \"00000000000\"\n```\n\n#### Using V2 update format\n\nYjs implements two update formats. By default you are using the V1 update format.\nYou can opt-in into the V2 update format which provides much better compression.\nIt is not yet used by all providers. However, you can already use it if\nyou are building your own provider. All below functions are available with the\nsuffix \"V2\". E.g. `Y.applyUpdate` ⇒ `Y.applyUpdateV2`. Also when listening to updates\nyou need to specifically need listen for V2 events e.g. `yDoc.on('updateV2', …)`.\nWe also support conversion functions between both formats:\n`Y.convertUpdateFormatV1ToV2` \u0026 `Y.convertUpdateFormatV2ToV1`.\n\n#### Update API\n\n\u003cdl\u003e\n  \u003cb\u003e\u003ccode\u003eY.applyUpdate(Y.Doc, update:Uint8Array, [transactionOrigin:any])\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003e\nApply a document update on the shared document. Optionally you can specify\n\u003ccode\u003etransactionOrigin\u003c/code\u003e that will be stored on\n\u003ccode\u003etransaction.origin\u003c/code\u003e\nand \u003ccode\u003eydoc.on('update', (update, origin) =\u003e ..)\u003c/code\u003e.\n  \u003c/dd\u003e\n  \u003cb\u003e\u003ccode\u003eY.encodeStateAsUpdate(Y.Doc, [encodedTargetStateVector:Uint8Array]):Uint8Array\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003e\nEncode the document state as a single update message that can be applied on the\nremote document. Optionally specify the target state vector to only write the\ndifferences to the update message.\n  \u003c/dd\u003e\n  \u003cb\u003e\u003ccode\u003eY.encodeStateVector(Y.Doc):Uint8Array\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003eComputes the state vector and encodes it into an Uint8Array.\u003c/dd\u003e\n  \u003cb\u003e\u003ccode\u003eY.mergeUpdates(Array\u0026lt;Uint8Array\u0026gt;)\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003e\nMerge several document updates into a single document update while removing\nduplicate information. The merged document update is always smaller than\nthe separate updates because of the compressed encoding.\n  \u003c/dd\u003e\n  \u003cb\u003e\u003ccode\u003eY.encodeStateVectorFromUpdate(Uint8Array): Uint8Array\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003e\nComputes the state vector from a document update and encodes it into an Uint8Array.\n  \u003c/dd\u003e\n  \u003cb\u003e\u003ccode\u003eY.diffUpdate(update: Uint8Array, stateVector: Uint8Array): Uint8Array\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003e\nEncode the missing differences to another update message. This function works\nsimilarly to \u003ccode\u003eY.encodeStateAsUpdate(ydoc, stateVector)\u003c/code\u003e but works\non updates instead.\n  \u003c/dd\u003e\n  \u003cb\u003e\u003ccode\u003econvertUpdateFormatV1ToV2\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003e\nConvert V1 update format to the V2 update format.\n  \u003c/dd\u003e\n  \u003cb\u003e\u003ccode\u003econvertUpdateFormatV2ToV1\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003e\nConvert V2 update format to the V1 update format.\n  \u003c/dd\u003e\n\u003c/dl\u003e\n\n### Relative Positions\n\nWhen working with collaborative documents, we often need to work with positions.\nPositions may represent cursor locations, selection ranges, or even assign a\ncomment to a range of text. Normal index-positions (expressed as integers) are\nnot convenient to use because the index-range is invalidated as soon as a remote\nchange manipulates the document. Relative positions give you a powerful API to\nexpress positions.\n\nA relative position is fixated to an element in the shared document and is not\naffected by remote changes. I.e. given the document `\"a|c\"`, the relative\nposition is attached to `c`. When a remote user modifies the document by\ninserting a character before the cursor, the cursor will stay attached to the\ncharacter `c`. `insert(1, 'x')(\"a|c\") = \"ax|c\"`. When the relative position is\nset to the end of the document, it will stay attached to the end of the\ndocument.\n\n#### Example: Transform to RelativePosition and back\n\n```js\nconst relPos = Y.createRelativePositionFromTypeIndex(ytext, 2)\nconst pos = Y.createAbsolutePositionFromRelativePosition(relPos, doc)\npos.type === ytext // =\u003e true\npos.index === 2 // =\u003e true\n```\n\n#### Example: Send relative position to remote client (json)\n\n```js\nconst relPos = Y.createRelativePositionFromTypeIndex(ytext, 2)\nconst encodedRelPos = JSON.stringify(relPos)\n// send encodedRelPos to remote client..\nconst parsedRelPos = JSON.parse(encodedRelPos)\nconst pos = Y.createAbsolutePositionFromRelativePosition(parsedRelPos, remoteDoc)\npos.type === remoteytext // =\u003e true\npos.index === 2 // =\u003e true\n```\n\n#### Example: Send relative position to remote client (Uint8Array)\n\n```js\nconst relPos = Y.createRelativePositionFromTypeIndex(ytext, 2)\nconst encodedRelPos = Y.encodeRelativePosition(relPos)\n// send encodedRelPos to remote client..\nconst parsedRelPos = Y.decodeRelativePosition(encodedRelPos)\nconst pos = Y.createAbsolutePositionFromRelativePosition(parsedRelPos, remoteDoc)\npos.type === remoteytext // =\u003e true\npos.index === 2 // =\u003e true\n```\n\n\u003cdl\u003e\n  \u003cb\u003e\u003ccode\u003e\nY.createRelativePositionFromTypeIndex(type:Uint8Array|Y.Type, index: number\n[, assoc=0])\n  \u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003e\nCreate a relative position fixated to the i-th element in any sequence-like\nshared type (if \u003ccode\u003eassoc \u003e= 0\u003c/code\u003e). By default, the position associates\nwith the character that comes after the specified index position. If\n\u003ccode\u003eassoc \u003c 0\u003c/code\u003e, then the relative position associates with the character\nbefore the specified index position.\n  \u003c/dd\u003e\n  \u003cb\u003e\u003ccode\u003e\nY.createAbsolutePositionFromRelativePosition(RelativePosition, Y.Doc):\n{ type: Y.AbstractType, index: number, assoc: number } | null\n  \u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003e\nCreate an absolute position from a relative position. If the relative position\ncannot be referenced, or the type is deleted, then the result is null.\n  \u003c/dd\u003e\n  \u003cb\u003e\u003ccode\u003e\nY.encodeRelativePosition(RelativePosition):Uint8Array\n  \u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003e\nEncode a relative position to an Uint8Array. Binary data is the preferred\nencoding format for document updates. If you prefer JSON encoding, you can\nsimply JSON.stringify / JSON.parse the relative position instead.\n  \u003c/dd\u003e\n  \u003cb\u003e\u003ccode\u003eY.decodeRelativePosition(Uint8Array):RelativePosition\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003eDecode a binary-encoded relative position to a RelativePosition object.\u003c/dd\u003e\n\u003c/dl\u003e\n\n### Y.UndoManager\n\nYjs ships with an Undo/Redo manager for selective undo/redo of changes on a\nYjs type. The changes can be optionally scoped to transaction origins.\n\n```js\nconst ytext = doc.getText('text')\nconst undoManager = new Y.UndoManager(ytext)\n\nytext.insert(0, 'abc')\nundoManager.undo()\nytext.toString() // =\u003e ''\nundoManager.redo()\nytext.toString() // =\u003e 'abc'\n```\n\n\u003cdl\u003e\n  \u003cb\u003e\u003ccode\u003econstructor(scope:Y.AbstractType|Array\u0026lt;Y.AbstractType\u0026gt;\n  [, {captureTimeout:number,trackedOrigins:Set\u0026lt;any\u0026gt;,deleteFilter:function(item):boolean}])\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003eAccepts either single type as scope or an array of types.\u003c/dd\u003e\n  \u003cb\u003e\u003ccode\u003eundo()\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003e\u003c/dd\u003e\n  \u003cb\u003e\u003ccode\u003eredo()\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003e\u003c/dd\u003e\n  \u003cb\u003e\u003ccode\u003estopCapturing()\u003c/code\u003e\u003c/b\u003e\n  \u003cdd\u003e\u003c/dd\u003e\n  \u003cb\u003e\n    \u003ccode\u003e\non('stack-item-added', { stackItem: { meta: Map\u0026lt;any,any\u0026gt; }, type: 'undo'\n| 'redo' })\n    \u003c/code\u003e\n  \u003c/b\u003e\n  \u003cdd\u003e\nRegister an event that is called when a \u003ccode\u003eStackItem\u003c/code\u003e is added to the\nundo- or the redo-stack.\n  \u003c/dd\u003e\n  \u003cb\u003e\n    \u003ccode\u003e\non('stack-item-updated', { stackItem: { meta: Map\u0026lt;any,any\u0026gt; }, type: 'undo'\n| 'redo' })\n    \u003c/code\u003e\n  \u003c/b\u003e\n  \u003cdd\u003e\nRegister an event that is called when an existing \u003ccode\u003eStackItem\u003c/code\u003e is updated.\nThis happens when two changes happen within a \"captureInterval\".\n  \u003c/dd\u003e\n  \u003cb\u003e\n    \u003ccode\u003e\non('stack-item-popped', { stackItem: { meta: Map\u0026lt;any,any\u0026gt; }, type: 'undo'\n| 'redo' })\n    \u003c/code\u003e\n  \u003c/b\u003e\n  \u003cdd\u003e\nRegister an event that is called when a \u003ccode\u003eStackItem\u003c/code\u003e is popped from\nthe undo- or the redo-stack.\n  \u003c/dd\u003e\n  \u003cb\u003e\n    \u003ccode\u003e\non('stack-cleared', { undoStackCleared: boolean, redoStackCleared: boolean })\n    \u003c/code\u003e\n  \u003c/b\u003e\n  \u003cdd\u003e\nRegister an event that is called when the undo- and/or the redo-stack is cleared.\n  \u003c/dd\u003e\n\u003c/dl\u003e\n\n#### Example: Stop Capturing\n\nUndoManager merges Undo-StackItems if they are created within time-gap\nsmaller than `options.captureTimeout`. Call `um.stopCapturing()` so that the next\nStackItem won't be merged.\n\n```js\n// without stopCapturing\nytext.insert(0, 'a')\nytext.insert(1, 'b')\nundoManager.undo()\nytext.toString() // =\u003e '' (note that 'ab' was removed)\n// with stopCapturing\nytext.insert(0, 'a')\nundoManager.stopCapturing()\nytext.insert(0, 'b')\nundoManager.undo()\nytext.toString() // =\u003e 'a' (note that only 'b' was removed)\n```\n\n#### Example: Specify tracked origins\n\nEvery change on the shared document has an origin. If no origin was specified,\nit defaults to `null`. By specifying `trackedOrigins` you can\nselectively specify which changes should be tracked by `UndoManager`. The\nUndoManager instance is always added to `trackedOrigins`.\n\n```js\nclass CustomBinding {}\n\nconst ytext = doc.getText('text')\nconst undoManager = new Y.UndoManager(ytext, {\n  trackedOrigins: new Set([42, CustomBinding])\n})\n\nytext.insert(0, 'abc')\nundoManager.undo()\nytext.toString() // =\u003e 'abc' (does not track because origin `null` and not part\n                 //           of `trackedTransactionOrigins`)\nytext.delete(0, 3) // revert change\n\ndoc.transact(() =\u003e {\n  ytext.insert(0, 'abc')\n}, 42)\nundoManager.undo()\nytext.toString() // =\u003e '' (tracked because origin is an instance of `trackedTransactionorigins`)\n\ndoc.transact(() =\u003e {\n  ytext.insert(0, 'abc')\n}, 41)\nundoManager.undo()\nytext.toString() // =\u003e 'abc' (not tracked because 41 is not an instance of\n                 //        `trackedTransactionorigins`)\nytext.delete(0, 3) // revert change\n\ndoc.transact(() =\u003e {\n  ytext.insert(0, 'abc')\n}, new CustomBinding())\nundoManager.undo()\nytext.toString() // =\u003e '' (tracked because origin is a `CustomBinding` and\n                 //        `CustomBinding` is in `trackedTransactionorigins`)\n```\n\n#### Example: Add additional information to the StackItems\n\nWhen undoing or redoing a previous action, it is often expected to restore\nadditional meta information like the cursor location or the view on the\ndocument. You can assign meta-information to Undo-/Redo-StackItems.\n\n```js\nconst ytext = doc.getText('text')\nconst undoManager = new Y.UndoManager(ytext, {\n  trackedOrigins: new Set([42, CustomBinding])\n})\n\nundoManager.on('stack-item-added', event =\u003e {\n  // save the current cursor location on the stack-item\n  event.stackItem.meta.set('cursor-location', getRelativeCursorLocation())\n})\n\nundoManager.on('stack-item-popped', event =\u003e {\n  // restore the current cursor location on the stack-item\n  restoreCursorLocation(event.stackItem.meta.get('cursor-location'))\n})\n```\n\n## Yjs CRDT Algorithm\n\n*Conflict-free replicated data types* (CRDT) for collaborative editing are an\nalternative approach to *operational transformation* (OT). A very simple\ndifferentiation between the two approaches is that OT attempts to transform\nindex positions to ensure convergence (all clients end up with the same\ncontent), while CRDTs use mathematical models that usually do not involve index\ntransformations, like linked lists. OT is currently the de-facto standard for\nshared editing on text. OT approaches that support shared editing without a\ncentral source of truth (a central server) require too much bookkeeping to be\nviable in practice. CRDTs are better suited for distributed systems, provide\nadditional guarantees that the document can be synced with remote clients, and\ndo not require a central source of truth.\n\nYjs implements a modified version of the algorithm described in [this\npaper](https://www.researchgate.net/publication/310212186_Near_Real-Time_Peer-to-Peer_Shared_Editing_on_Extensible_Data_Types).\nThis [article](https://blog.kevinjahns.de/are-crdts-suitable-for-shared-editing/)\nexplains a simple optimization on the CRDT model and\ngives more insight about the performance characteristics in Yjs.\nMore information about the specific implementation is available in\n[INTERNALS.md](./INTERNALS.md) and in\n[this walkthrough of the Yjs codebase](https://youtu.be/0l5XgnQ6rB4).\n\nCRDTs that are suitable for shared text editing suffer from the fact that they\nonly grow in size. There are CRDTs that do not grow in size, but they do not\nhave the characteristics that are beneficial for shared text editing (like\nintention preservation). Yjs implements many improvements to the original\nalgorithm that diminish the trade-off that the document only grows in size. We\ncan't garbage collect deleted structs (tombstones) while ensuring a unique\norder of the structs. But we can 1. merge preceding structs into a single\nstruct to reduce the amount of meta information, 2. we can delete content from\nthe struct if it is deleted, and 3. we can garbage collect tombstones if we\ndon't care about the order of the structs anymore (e.g. if the parent was\ndeleted).\n\n**Examples:**\n\n1. If a user inserts elements in sequence, the struct will be merged into a\n   single struct. E.g. `text.insert(0, 'a'), text.insert(1, 'b');` is\n   first represented as two structs (`[{id: {client, clock: 0}, content: 'a'},\n   {id: {client, clock: 1}, content: 'b'}`) and then merged into a single\n   struct: `[{id: {client, clock: 0}, content: 'ab'}]`.\n2. When a struct that contains content (e.g. `ItemString`) is deleted, the\n   struct will be replaced with an `ItemDeleted` that does not contain content\n   anymore.\n3. When a type is deleted, all child elements are transformed to `GC` structs. A\n   `GC` struct only denotes the existence of a struct and that it is deleted.\n   `GC` structs can always be merged with other `GC` structs if the id's are\n   adjacent.\n\nEspecially when working on structured content (e.g. shared editing on\nProseMirror), these improvements yield very good results when\n[benchmarking](https://github.com/dmonad/crdt-benchmarks) random document edits.\nIn practice they show even better results, because users usually edit text in\nsequence, resulting in structs that can easily be merged. The benchmarks show\nthat even in the worst case scenario that a user edits text from right to left,\nYjs achieves good performance even for huge documents.\n\n### State Vector\n\nYjs has the ability to exchange only the differences when syncing two clients.\nWe use lamport timestamps to identify structs and to track in which order a\nclient created them. Each struct has an `struct.id = { client: number, clock:\nnumber}` that uniquely identifies a struct. We define the next expected `clock`\nby each client as the *state vector*. This data structure is similar to the\n[version vectors](https://en.wikipedia.org/wiki/Version_vector) data structure.\nBut we use state vectors only to describe the state of the local document, so we\ncan compute the missing struct of the remote client. We do not use it to track\ncausality.\n\n### Formal Proof\n\n[lean-yjs](https://github.com/iasakura/lean-yjs) provides a formal verification\nof the YATA CRDT algorithm that Yjs implements, using the Lean theorem prover to\nmathematically prove correctness properties. While the CRDT algorithm itself is\ncorrect (currently proven for preservation and commutativity), the project\nreveals that [the pseudocode in the original YATA paper contains\nerrors](https://discuss.yjs.dev/t/lean-yjs-formally-proving-the-yjs-conflict-resolution-algorithms/3875/2).\n\n## License and Author\n\nYjs and all related projects are [**MIT licensed**](./LICENSE).\n\nYjs is based on my research as a student at the [RWTH\ni5](http://dbis.rwth-aachen.de/). Now I am working on Yjs in my spare time.\n\nFund this project by donating on [GitHub Sponsors](https://github.com/sponsors/dmonad)\nor hiring [me](https://github.com/dmonad) as a contractor for your collaborative\napp.\n","funding_links":["https://github.com/sponsors/dmonad","https://opencollective.com/y-collective"],"categories":["JavaScript","Uncategorized","Applications","Collaborative Documents","Project","对打造产品有帮助","p2p","[🎓 research](https://github.com/stars/ketsapiwiq/lists/research)","Packages","其他_大数据","Editor","前端常见效果库","OT协同","Server"],"sub_categories":["Uncategorized","Collaborative Editor (e.g. Google Docs)","Single Purpose Bots","Data","资源传输下载","代码编辑器","运行器e2e测试","JavaScript"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyjs%2Fyjs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyjs%2Fyjs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyjs%2Fyjs/lists"}