Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e2710ded9 | ||
|
|
227018f5c7 | ||
|
|
da8bacfc78 | ||
|
|
92bad63145 | ||
|
|
52ff230dd1 | ||
|
|
fe48efe64f | ||
|
|
7e40fc442d | ||
|
|
035e350062 | ||
|
|
bf338d8040 | ||
|
|
658c520b93 | ||
|
|
2576d4efca | ||
|
|
58b754950e | ||
|
|
6b7b3136e0 | ||
|
|
da052bdb0a |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.5.46",
|
"version": "13.5.50",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.5.46",
|
"version": "13.5.50",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lib0": "^0.2.49"
|
"lib0": "^0.2.49"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.5.46",
|
"version": "13.5.50",
|
||||||
"description": "Shared Editing Library",
|
"description": "Shared Editing Library",
|
||||||
"main": "./dist/yjs.cjs",
|
"main": "./dist/yjs.cjs",
|
||||||
"module": "./dist/yjs.mjs",
|
"module": "./dist/yjs.mjs",
|
||||||
|
|||||||
@@ -363,19 +363,26 @@ const formatText = (transaction, parent, currPos, length, attributes) => {
|
|||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAttributes) => {
|
const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAttributes) => {
|
||||||
let end = curr
|
/**
|
||||||
const endAttributes = map.copy(currAttributes)
|
* @type {Item|null}
|
||||||
|
*/
|
||||||
|
let end = start
|
||||||
|
/**
|
||||||
|
* @type {Map<string,ContentFormat>}
|
||||||
|
*/
|
||||||
|
const endFormats = map.create()
|
||||||
while (end && (!end.countable || end.deleted)) {
|
while (end && (!end.countable || end.deleted)) {
|
||||||
if (!end.deleted && end.content.constructor === ContentFormat) {
|
if (!end.deleted && end.content.constructor === ContentFormat) {
|
||||||
updateCurrentAttributes(endAttributes, /** @type {ContentFormat} */ (end.content))
|
const cf = /** @type {ContentFormat} */ (end.content)
|
||||||
|
endFormats.set(cf.key, cf)
|
||||||
}
|
}
|
||||||
end = end.right
|
end = end.right
|
||||||
}
|
}
|
||||||
let cleanups = 0
|
let cleanups = 0
|
||||||
let reachedEndOfCurr = false
|
let reachedCurr = false
|
||||||
while (start !== end) {
|
while (start !== end) {
|
||||||
if (curr === start) {
|
if (curr === start) {
|
||||||
reachedEndOfCurr = true
|
reachedCurr = true
|
||||||
}
|
}
|
||||||
if (!start.deleted) {
|
if (!start.deleted) {
|
||||||
const content = start.content
|
const content = start.content
|
||||||
@@ -383,11 +390,11 @@ const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAtt
|
|||||||
case ContentFormat: {
|
case ContentFormat: {
|
||||||
const { key, value } = /** @type {ContentFormat} */ (content)
|
const { key, value } = /** @type {ContentFormat} */ (content)
|
||||||
const startAttrValue = startAttributes.get(key) || null
|
const startAttrValue = startAttributes.get(key) || null
|
||||||
if ((endAttributes.get(key) || null) !== value || startAttrValue === value) {
|
if (endFormats.get(key) !== content || startAttrValue === value) {
|
||||||
// Either this format is overwritten or it is not necessary because the attribute already existed.
|
// Either this format is overwritten or it is not necessary because the attribute already existed.
|
||||||
start.delete(transaction)
|
start.delete(transaction)
|
||||||
cleanups++
|
cleanups++
|
||||||
if (!reachedEndOfCurr && (currAttributes.get(key) || null) === value && (startAttributes.get(key) || null) !== value) {
|
if (!reachedCurr && (currAttributes.get(key) || null) === value && startAttrValue !== value) {
|
||||||
if (startAttrValue === null) {
|
if (startAttrValue === null) {
|
||||||
currAttributes.delete(key)
|
currAttributes.delete(key)
|
||||||
} else {
|
} else {
|
||||||
@@ -395,6 +402,9 @@ const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAtt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!reachedCurr && !start.deleted) {
|
||||||
|
updateCurrentAttributes(currAttributes, /** @type {ContentFormat} */ (content))
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1008,15 +1018,7 @@ export class YText extends AbstractType {
|
|||||||
str = ''
|
str = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// snapshots are merged again after the transaction, so we need to keep the
|
const computeDelta = () => {
|
||||||
// transalive until we are done
|
|
||||||
transact(doc, transaction => {
|
|
||||||
if (snapshot) {
|
|
||||||
splitSnapshotAffectedStructs(transaction, snapshot)
|
|
||||||
}
|
|
||||||
if (prevSnapshot) {
|
|
||||||
splitSnapshotAffectedStructs(transaction, prevSnapshot)
|
|
||||||
}
|
|
||||||
while (n !== null) {
|
while (n !== null) {
|
||||||
if (isVisible(n, snapshot) || (prevSnapshot !== undefined && isVisible(n, prevSnapshot))) {
|
if (isVisible(n, snapshot) || (prevSnapshot !== undefined && isVisible(n, prevSnapshot))) {
|
||||||
switch (n.content.constructor) {
|
switch (n.content.constructor) {
|
||||||
@@ -1069,7 +1071,22 @@ export class YText extends AbstractType {
|
|||||||
n = n.right
|
n = n.right
|
||||||
}
|
}
|
||||||
packStr()
|
packStr()
|
||||||
}, 'cleanup')
|
}
|
||||||
|
if (snapshot || prevSnapshot) {
|
||||||
|
// snapshots are merged again after the transaction, so we need to keep the
|
||||||
|
// transaction alive until we are done
|
||||||
|
transact(doc, transaction => {
|
||||||
|
if (snapshot) {
|
||||||
|
splitSnapshotAffectedStructs(transaction, snapshot)
|
||||||
|
}
|
||||||
|
if (prevSnapshot) {
|
||||||
|
splitSnapshotAffectedStructs(transaction, prevSnapshot)
|
||||||
|
}
|
||||||
|
computeDelta()
|
||||||
|
}, 'cleanup')
|
||||||
|
} else {
|
||||||
|
computeDelta()
|
||||||
|
}
|
||||||
return ops
|
return ops
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as error from 'lib0/error'
|
import * as error from 'lib0/error'
|
||||||
|
import * as array from 'lib0/array'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define the elements to which a set of CSS queries apply.
|
* Define the elements to which a set of CSS queries apply.
|
||||||
@@ -237,7 +238,7 @@ export class YXmlFragment extends AbstractType {
|
|||||||
querySelectorAll (query) {
|
querySelectorAll (query) {
|
||||||
query = query.toUpperCase()
|
query = query.toUpperCase()
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return Array.from(new YXmlTreeWalker(this, element => element.nodeName && element.nodeName.toUpperCase() === query))
|
return array.from(new YXmlTreeWalker(this, element => element.nodeName && element.nodeName.toUpperCase() === query))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -219,17 +219,21 @@ export const createDeleteSetFromStructStore = ss => {
|
|||||||
*/
|
*/
|
||||||
export const writeDeleteSet = (encoder, ds) => {
|
export const writeDeleteSet = (encoder, ds) => {
|
||||||
encoding.writeVarUint(encoder.restEncoder, ds.clients.size)
|
encoding.writeVarUint(encoder.restEncoder, ds.clients.size)
|
||||||
ds.clients.forEach((dsitems, client) => {
|
|
||||||
encoder.resetDsCurVal()
|
// Ensure that the delete set is written in a deterministic order
|
||||||
encoding.writeVarUint(encoder.restEncoder, client)
|
array.from(ds.clients.entries())
|
||||||
const len = dsitems.length
|
.sort((a, b) => b[0] - a[0])
|
||||||
encoding.writeVarUint(encoder.restEncoder, len)
|
.forEach(([client, dsitems]) => {
|
||||||
for (let i = 0; i < len; i++) {
|
encoder.resetDsCurVal()
|
||||||
const item = dsitems[i]
|
encoding.writeVarUint(encoder.restEncoder, client)
|
||||||
encoder.writeDsClock(item.clock)
|
const len = dsitems.length
|
||||||
encoder.writeDsLen(item.len)
|
encoding.writeVarUint(encoder.restEncoder, len)
|
||||||
}
|
for (let i = 0; i < len; i++) {
|
||||||
})
|
const item = dsitems[i]
|
||||||
|
encoder.writeDsClock(item.clock)
|
||||||
|
encoder.writeDsLen(item.len)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export class Doc extends Observable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSubdocGuids () {
|
getSubdocGuids () {
|
||||||
return new Set(Array.from(this.subdocs).map(doc => doc.guid))
|
return new Set(array.from(this.subdocs).map(doc => doc.guid))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -156,13 +156,15 @@ export class Doc extends Observable {
|
|||||||
* that happened inside of the transaction are sent as one message to the
|
* that happened inside of the transaction are sent as one message to the
|
||||||
* other peers.
|
* other peers.
|
||||||
*
|
*
|
||||||
* @param {function(Transaction):void} f The function that should be executed as a transaction
|
* @template T
|
||||||
|
* @param {function(Transaction):T} f The function that should be executed as a transaction
|
||||||
* @param {any} [origin] Origin of who started the transaction. Will be stored on transaction.origin
|
* @param {any} [origin] Origin of who started the transaction. Will be stored on transaction.origin
|
||||||
|
* @return T
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
transact (f, origin = null) {
|
transact (f, origin = null) {
|
||||||
transact(this, f, origin)
|
return transact(this, f, origin)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -376,15 +376,21 @@ const cleanupTransactions = (transactionCleanups, i) => {
|
|||||||
/**
|
/**
|
||||||
* Implements the functionality of `y.transact(()=>{..})`
|
* Implements the functionality of `y.transact(()=>{..})`
|
||||||
*
|
*
|
||||||
|
* @template T
|
||||||
* @param {Doc} doc
|
* @param {Doc} doc
|
||||||
* @param {function(Transaction):void} f
|
* @param {function(Transaction):T} f
|
||||||
* @param {any} [origin=true]
|
* @param {any} [origin=true]
|
||||||
|
* @return {T}
|
||||||
*
|
*
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const transact = (doc, f, origin = null, local = true) => {
|
export const transact = (doc, f, origin = null, local = true) => {
|
||||||
const transactionCleanups = doc._transactionCleanups
|
const transactionCleanups = doc._transactionCleanups
|
||||||
let initialCall = false
|
let initialCall = false
|
||||||
|
/**
|
||||||
|
* @type {any}
|
||||||
|
*/
|
||||||
|
let result = null
|
||||||
if (doc._transaction === null) {
|
if (doc._transaction === null) {
|
||||||
initialCall = true
|
initialCall = true
|
||||||
doc._transaction = new Transaction(doc, origin, local)
|
doc._transaction = new Transaction(doc, origin, local)
|
||||||
@@ -395,7 +401,7 @@ export const transact = (doc, f, origin = null, local = true) => {
|
|||||||
doc.emit('beforeTransaction', [doc._transaction, doc])
|
doc.emit('beforeTransaction', [doc._transaction, doc])
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
f(doc._transaction)
|
result = f(doc._transaction)
|
||||||
} finally {
|
} finally {
|
||||||
if (initialCall) {
|
if (initialCall) {
|
||||||
const finishCleanup = doc._transaction === transactionCleanups[0]
|
const finishCleanup = doc._transaction === transactionCleanups[0]
|
||||||
@@ -413,4 +419,5 @@ export const transact = (doc, f, origin = null, local = true) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,6 +130,11 @@ export class YEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* This is a computed property. Note that this can only be safely computed during the
|
||||||
|
* event call. Computing this property after other changes happened might result in
|
||||||
|
* unexpected behavior (incorrect computation of deltas). A safe way to collect changes
|
||||||
|
* is to store the `changes` or the `delta` object. Avoid storing the `transaction` object.
|
||||||
|
*
|
||||||
* @type {Array<{insert?: string | Array<any> | object | AbstractType<any>, retain?: number, delete?: number, attributes?: Object<string, any>}>}
|
* @type {Array<{insert?: string | Array<any> | object | AbstractType<any>, retain?: number, delete?: number, attributes?: Object<string, any>}>}
|
||||||
*/
|
*/
|
||||||
get delta () {
|
get delta () {
|
||||||
@@ -149,6 +154,11 @@ export class YEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* This is a computed property. Note that this can only be safely computed during the
|
||||||
|
* event call. Computing this property after other changes happened might result in
|
||||||
|
* unexpected behavior (incorrect computation of deltas). A safe way to collect changes
|
||||||
|
* is to store the `changes` or the `delta` object. Avoid storing the `transaction` object.
|
||||||
|
*
|
||||||
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Array<{insert?:Array<any>|string, delete?:number, retain?:number}>}}
|
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Array<{insert?:Array<any>|string, delete?:number, retain?:number}>}}
|
||||||
*/
|
*/
|
||||||
get changes () {
|
get changes () {
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import * as decoding from 'lib0/decoding'
|
|||||||
import * as binary from 'lib0/binary'
|
import * as binary from 'lib0/binary'
|
||||||
import * as map from 'lib0/map'
|
import * as map from 'lib0/map'
|
||||||
import * as math from 'lib0/math'
|
import * as math from 'lib0/math'
|
||||||
|
import * as array from 'lib0/array'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
@@ -96,7 +97,7 @@ export const writeClientsStructs = (encoder, store, _sm) => {
|
|||||||
encoding.writeVarUint(encoder.restEncoder, sm.size)
|
encoding.writeVarUint(encoder.restEncoder, sm.size)
|
||||||
// Write items with higher client ids first
|
// Write items with higher client ids first
|
||||||
// This heavily improves the conflict algorithm.
|
// This heavily improves the conflict algorithm.
|
||||||
Array.from(sm.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => {
|
array.from(sm.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
writeStructs(encoder, store.clients.get(client), client, clock)
|
writeStructs(encoder, store.clients.get(client), client, clock)
|
||||||
})
|
})
|
||||||
@@ -231,7 +232,7 @@ const integrateStructs = (transaction, store, clientsStructRefs) => {
|
|||||||
*/
|
*/
|
||||||
const stack = []
|
const stack = []
|
||||||
// sort them so that we take the higher id first, in case of conflicts the lower id will probably not conflict with the id from the higher user.
|
// sort them so that we take the higher id first, in case of conflicts the lower id will probably not conflict with the id from the higher user.
|
||||||
let clientsStructRefsIds = Array.from(clientsStructRefs.keys()).sort((a, b) => a - b)
|
let clientsStructRefsIds = array.from(clientsStructRefs.keys()).sort((a, b) => a - b)
|
||||||
if (clientsStructRefsIds.length === 0) {
|
if (clientsStructRefsIds.length === 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -601,7 +602,7 @@ export const decodeStateVector = decodedState => readStateVector(new DSDecoderV1
|
|||||||
*/
|
*/
|
||||||
export const writeStateVector = (encoder, sv) => {
|
export const writeStateVector = (encoder, sv) => {
|
||||||
encoding.writeVarUint(encoder.restEncoder, sv.size)
|
encoding.writeVarUint(encoder.restEncoder, sv.size)
|
||||||
Array.from(sv.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => {
|
array.from(sv.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => {
|
||||||
encoding.writeVarUint(encoder.restEncoder, client) // @todo use a special client decoder that is based on mapping
|
encoding.writeVarUint(encoder.restEncoder, client) // @todo use a special client decoder that is based on mapping
|
||||||
encoding.writeVarUint(encoder.restEncoder, clock)
|
encoding.writeVarUint(encoder.restEncoder, clock)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,6 +2,24 @@
|
|||||||
import * as Y from '../src/index.js'
|
import * as Y from '../src/index.js'
|
||||||
import * as t from 'lib0/testing'
|
import * as t from 'lib0/testing'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} _tc
|
||||||
|
*/
|
||||||
|
export const testAfterTransactionRecursion = _tc => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
const yxml = ydoc.getXmlFragment('')
|
||||||
|
ydoc.on('afterTransaction', tr => {
|
||||||
|
if (tr.origin === 'test') {
|
||||||
|
yxml.toJSON()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
ydoc.transact(_tr => {
|
||||||
|
for (let i = 0; i < 15000; i++) {
|
||||||
|
yxml.push([new Y.XmlText('a')])
|
||||||
|
}
|
||||||
|
}, 'test')
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} _tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
@@ -15,7 +33,7 @@ export const testOriginInTransaction = _tc => {
|
|||||||
doc.on('afterTransaction', (tr) => {
|
doc.on('afterTransaction', (tr) => {
|
||||||
origins.push(tr.origin)
|
origins.push(tr.origin)
|
||||||
if (origins.length <= 1) {
|
if (origins.length <= 1) {
|
||||||
ytext.toDelta()
|
ytext.toDelta(Y.snapshot(doc)) // adding a snapshot forces toDelta to create a cleanup transaction
|
||||||
doc.transact(() => {
|
doc.transact(() => {
|
||||||
ytext.insert(0, 'a')
|
ytext.insert(0, 'a')
|
||||||
}, 'nested')
|
}, 'nested')
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user