Compare commits

...

14 Commits

Author SHA1 Message Date
Kevin Jahns
34b9343b2e
Merge pull request #702 from kapv89/patch-1
Update README.md to add https://github.com/kapv89/k_yrs_go to persistence providers
2025-03-26 12:57:27 +01:00
kapil verma
d5b5e7a9a1
Update README.md to add https://github.com/kapv89/k_yrs_go to persistence providers
Ref: https://x.com/kevin_jahns/status/1904252641124753641
2025-03-26 01:55:02 +05:30
Kevin Jahns
ad0d915794
Merge pull request #701 from hacklschorsch/patch-2
README: Remove duplicate btw mention
2025-03-18 20:18:35 +01:00
Florian Sesser
2ef9ccd170
README: Remove duplicate btw mention
'btw' was mentioned twice; remove one mention.
2025-03-18 19:23:51 +01:00
Kevin Jahns
3ecfb4e898 add rowsncolumns 2025-03-09 20:57:52 +01:00
Kevin Jahns
35c030d834 improve reject update example 2025-03-06 10:36:18 +01:00
Kevin Jahns
e3739bce8e test example for rejecting updates 2025-03-05 14:15:26 +01:00
Kevin Jahns
afa4c35866 update titanic funding information - closes #696 2025-03-05 14:15:12 +01:00
Kevin Jahns
09fbb62ba9 improve documentation on global UndoManager 2025-03-04 14:52:19 +01:00
Kevin Jahns
78e0527b46 13.6.24 2025-03-04 14:44:19 +01:00
Kevin Jahns
69d4a5c821 [UndoManager] support global undo 2025-03-04 14:42:19 +01:00
Kevin Jahns
cc9a857441 slightly optimize TreeWalker and integration process 2025-02-24 20:30:48 +01:00
Kevin Jahns
4b865764b8
Merge pull request #691 from reknih/add-typst
Add Typst to Yjs users in README
2025-01-17 12:00:53 +01:00
Martin Haug
40725e373b Add Typst to Yjs users in README 2025-01-17 11:58:36 +01:00
8 changed files with 106 additions and 29 deletions

View File

@ -99,7 +99,6 @@ Showcase](https://yjs-diagram.synergy.codes/).
* [AWS SageMaker](https://aws.amazon.com/sagemaker/) Tools for building Machine * [AWS SageMaker](https://aws.amazon.com/sagemaker/) Tools for building Machine
Learning Models Learning Models
* [linear](https://linear.app) Streamline issues, projects, and product roadmaps. * [linear](https://linear.app) Streamline issues, projects, and product roadmaps.
* [btw](https://www.btw.so) - Personal website builder
* [AWS SageMaker](https://aws.amazon.com/sagemaker/) - Machine Learning Service * [AWS SageMaker](https://aws.amazon.com/sagemaker/) - Machine Learning Service
* [Arkiter](https://www.arkiter.com/) - Live interview software * [Arkiter](https://www.arkiter.com/) - Live interview software
* [Appflowy](https://www.appflowy.io/) - They use Yrs * [Appflowy](https://www.appflowy.io/) - They use Yrs
@ -123,6 +122,7 @@ Showcase](https://yjs-diagram.synergy.codes/).
* [ScienHub](https://scienhub.com) - Collaborative LaTeX editor in the browser. * [ScienHub](https://scienhub.com) - Collaborative LaTeX editor in the browser.
* [Open Collaboration Tools](https://www.open-collab.tools/) - Collaborative * [Open Collaboration Tools](https://www.open-collab.tools/) - Collaborative
editing for your IDE or custom editor editing for your IDE or custom editor
* [Typst](https://typst.app/) - Compose, edit, and automate technical documents
## Table of Contents ## Table of Contents
@ -164,6 +164,7 @@ are implemented in separate modules.
| React / Vue / Svelte / MobX | | [SyncedStore](https://syncedstore.org) | [demo](https://syncedstore.org/docs/react) | | React / Vue / Svelte / MobX | | [SyncedStore](https://syncedstore.org) | [demo](https://syncedstore.org/docs/react) |
| [mobx-keystone](https://mobx-keystone.js.org/) | | [mobx-keystone-yjs](https://github.com/xaviergonz/mobx-keystone/tree/master/packages/mobx-keystone-yjs) | [demo](https://mobx-keystone.js.org/examples/yjs-binding) | | [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) | | [PSPDFKit](https://www.nutrient.io/) | | [yjs-pspdfkit](https://github.com/hoangqwe159/yjs-pspdfkit) | [demo](https://github.com/hoangqwe159/yjs-pspdfkit) |
| [Rows n'Columns](https://www.rowsncolumns.app/) | ✔ | [@rowsncolumns/y-spreadsheet](https://docs.rowsncolumns.app/collaboration/yjs-collaboration) | |
### Providers ### Providers
@ -299,6 +300,10 @@ A database and connection provider for Yjs based on Firestore.
Provides persistent storage for a web server using PostgreSQL and Provides persistent storage for a web server using PostgreSQL and
is easily compatible with y-websocket. is easily compatible with y-websocket.
</dd> </dd>
<dt><a href="https://github.com/kapv89/k_yrs_go">k_yrs_go</a></dt>
<dd>
Golang database server for YJS CRDT using Postgres + Redis
</dd>
</dl> </dl>
### Tooling ### Tooling

View File

@ -34,20 +34,19 @@
] ]
}, },
{ {
"guid": "ystream", "guid": "Titanic",
"name": "Y/Stream", "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.", "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": { "webpageUrl": {
"url": "https://github.com/yjs/ystream", "url": "https://github.com/yjs/titanic",
"wellKnown": "https://github.com/yjs/ystream/blob/main/.well-known/funding-manifest-urls" "wellKnown": "https://github.com/yjs/titanic/blob/main/.well-known/funding-manifest-urls"
}, },
"repositoryUrl": { "repositoryUrl": {
"url": "https://github.com/yjs/ystream", "url": "https://github.com/yjs/titanic",
"wellKnown": "https://github.com/yjs/ystream/blob/main/.well-known/funding-manifest-urls" "wellKnown": "https://github.com/yjs/titanic/blob/main/.well-known/funding-manifest-urls"
}, },
"licenses": [ "licenses": [
"spdx:MIT", "spdx:MIT"
"spdx:GPL-3.0"
], ],
"tags": [ "tags": [
"privacy", "privacy",
@ -90,9 +89,9 @@
] ]
}, },
{ {
"guid": "ystream-funding", "guid": "titanic-funding",
"status": "active", "status": "active",
"name": "YStream Funding", "name": "Titanic Funding",
"description": "Fund the next generation of local-first providers.", "description": "Fund the next generation of local-first providers.",
"amount": 30000, "amount": 30000,
"currency": "USD", "currency": "USD",

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.6.23", "version": "13.6.24",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "yjs", "name": "yjs",
"version": "13.6.23", "version": "13.6.24",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"lib0": "^0.2.99" "lib0": "^0.2.99"

View File

@ -1,6 +1,6 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.6.23", "version": "13.6.24",
"description": "Shared Editing Library", "description": "Shared Editing Library",
"main": "./dist/yjs.cjs", "main": "./dist/yjs.cjs",
"module": "./dist/yjs.mjs", "module": "./dist/yjs.mjs",

View File

@ -393,8 +393,7 @@ export class Item extends AbstractStruct {
if (this.left && this.left.constructor === Item) { if (this.left && this.left.constructor === Item) {
this.parent = this.left.parent this.parent = this.left.parent
this.parentSub = this.left.parentSub this.parentSub = this.left.parentSub
} } else if (this.right && this.right.constructor === Item) {
if (this.right && this.right.constructor === Item) {
this.parent = this.right.parent this.parent = this.right.parent
this.parentSub = this.right.parentSub this.parentSub = this.right.parentSub
} }

View File

@ -96,8 +96,12 @@ export class YXmlTreeWalker {
} else { } else {
// walk right or up in the tree // walk right or up in the tree
while (n !== null) { while (n !== null) {
if (n.right !== null) { /**
n = n.right * @type {Item | null}
*/
const nxt = n.next
if (nxt !== null) {
n = nxt
break break
} else if (n.parent === this._root) { } else if (n.parent === this._root) {
n = null n = null

View File

@ -39,7 +39,7 @@ export class StackItem {
*/ */
const clearUndoManagerStackItem = (tr, um, stackItem) => { const clearUndoManagerStackItem = (tr, um, stackItem) => {
iterateDeletedStructs(tr, stackItem.deletions, item => { iterateDeletedStructs(tr, stackItem.deletions, item => {
if (item instanceof Item && um.scope.some(type => isParentOf(type, item))) { if (item instanceof Item && um.scope.some(type => type === tr.doc || isParentOf(/** @type {AbstractType<any>} */ (type), item))) {
keepItem(item, false) keepItem(item, false)
} }
}) })
@ -81,7 +81,7 @@ const popStackItem = (undoManager, stack, eventType) => {
} }
struct = item struct = item
} }
if (!struct.deleted && scope.some(type => isParentOf(type, /** @type {Item} */ (struct)))) { if (!struct.deleted && scope.some(type => type === transaction.doc || isParentOf(/** @type {AbstractType<any>} */ (type), /** @type {Item} */ (struct)))) {
itemsToDelete.push(struct) itemsToDelete.push(struct)
} }
} }
@ -89,7 +89,7 @@ const popStackItem = (undoManager, stack, eventType) => {
iterateDeletedStructs(transaction, stackItem.deletions, struct => { iterateDeletedStructs(transaction, stackItem.deletions, struct => {
if ( if (
struct instanceof Item && struct instanceof Item &&
scope.some(type => isParentOf(type, struct)) && scope.some(type => type === transaction.doc || isParentOf(/** @type {AbstractType<any>} */ (type), struct)) &&
// Never redo structs in stackItem.insertions because they were created and deleted in the same capture interval. // Never redo structs in stackItem.insertions because they were created and deleted in the same capture interval.
!isDeleted(stackItem.insertions, struct.id) !isDeleted(stackItem.insertions, struct.id)
) { ) {
@ -159,7 +159,7 @@ const popStackItem = (undoManager, stack, eventType) => {
*/ */
export class UndoManager extends ObservableV2 { export class UndoManager extends ObservableV2 {
/** /**
* @param {AbstractType<any>|Array<AbstractType<any>>} typeScope Accepts either a single type, or an array of types * @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 {UndoManagerOptions} options * @param {UndoManagerOptions} options
*/ */
constructor (typeScope, { constructor (typeScope, {
@ -168,11 +168,11 @@ export class UndoManager extends ObservableV2 {
deleteFilter = () => true, deleteFilter = () => true,
trackedOrigins = new Set([null]), trackedOrigins = new Set([null]),
ignoreRemoteMapChanges = false, ignoreRemoteMapChanges = false,
doc = /** @type {Doc} */ (array.isArray(typeScope) ? typeScope[0].doc : typeScope.doc) doc = /** @type {Doc} */ (array.isArray(typeScope) ? typeScope[0].doc : typeScope instanceof Doc ? typeScope : typeScope.doc)
} = {}) { } = {}) {
super() super()
/** /**
* @type {Array<AbstractType<any>>} * @type {Array<AbstractType<any> | Doc>}
*/ */
this.scope = [] this.scope = []
this.doc = doc this.doc = doc
@ -212,7 +212,7 @@ export class UndoManager extends ObservableV2 {
// Only track certain transactions // Only track certain transactions
if ( if (
!this.captureTransaction(transaction) || !this.captureTransaction(transaction) ||
!this.scope.some(type => transaction.changedParentTypes.has(type)) || !this.scope.some(type => transaction.changedParentTypes.has(/** @type {AbstractType<any>} */ (type)) || type === this.doc) ||
(!this.trackedOrigins.has(transaction.origin) && (!transaction.origin || !this.trackedOrigins.has(transaction.origin.constructor))) (!this.trackedOrigins.has(transaction.origin) && (!transaction.origin || !this.trackedOrigins.has(transaction.origin.constructor)))
) { ) {
return return
@ -251,7 +251,7 @@ export class UndoManager extends ObservableV2 {
} }
// make sure that deleted structs are not gc'd // make sure that deleted structs are not gc'd
iterateDeletedStructs(transaction, transaction.deleteSet, /** @param {Item|GC} item */ item => { iterateDeletedStructs(transaction, transaction.deleteSet, /** @param {Item|GC} item */ item => {
if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) { if (item instanceof Item && this.scope.some(type => type === transaction.doc || isParentOf(/** @type {AbstractType<any>} */ (type), item))) {
keepItem(item, true) keepItem(item, true)
} }
}) })
@ -272,13 +272,17 @@ export class UndoManager extends ObservableV2 {
} }
/** /**
* @param {Array<AbstractType<any>> | AbstractType<any>} ytypes * Extend the scope.
*
* @param {Array<AbstractType<any> | Doc> | AbstractType<any> | Doc} ytypes
*/ */
addToScope (ytypes) { addToScope (ytypes) {
const tmpSet = new Set(this.scope)
ytypes = array.isArray(ytypes) ? ytypes : [ytypes] ytypes = array.isArray(ytypes) ? ytypes : [ytypes]
ytypes.forEach(ytype => { ytypes.forEach(ytype => {
if (this.scope.every(yt => yt !== ytype)) { if (!tmpSet.has(ytype)) {
if (ytype.doc !== this.doc) logging.warn('[yjs#509] Not same Y.Doc') // use MultiDocUndoManager instead. also see https://github.com/yjs/yjs/issues/509 tmpSet.add(ytype)
if (ytype instanceof AbstractType ? ytype.doc !== this.doc : ytype !== this.doc) logging.warn('[yjs#509] Not same Y.Doc') // use MultiDocUndoManager instead. also see https://github.com/yjs/yjs/issues/509
this.scope.push(ytype) this.scope.push(ytype)
} }
}) })

View File

@ -116,6 +116,72 @@ export const testEmptyTypeScope = _tc => {
t.assert(yarray.length === 0) 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 * Test case to fix #241
* @param {t.TestCase} _tc * @param {t.TestCase} _tc