Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
34b9343b2e | ||
|
d5b5e7a9a1 | ||
|
ad0d915794 | ||
|
2ef9ccd170 | ||
|
3ecfb4e898 | ||
|
35c030d834 | ||
|
e3739bce8e | ||
|
afa4c35866 | ||
|
09fbb62ba9 | ||
|
78e0527b46 | ||
|
69d4a5c821 | ||
|
cc9a857441 | ||
|
4b865764b8 | ||
|
40725e373b |
@ -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
|
||||||
|
19
funding.json
19
funding.json
@ -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
4
package-lock.json
generated
@ -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"
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user