Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f2f08ef7e | ||
|
|
39167e6e2a | ||
|
|
5a8519d2c2 | ||
|
|
d039d48b3f | ||
|
|
710ac31af3 | ||
|
|
49f435284f | ||
|
|
ba96f2fe74 | ||
|
|
99bab4a1d8 | ||
|
|
1674d3986d | ||
|
|
dc3e99e6a1 | ||
|
|
fb6664a2bc | ||
|
|
0d7e865531 | ||
|
|
e73eb0bf92 | ||
|
|
d815855450 | ||
|
|
61ba6cdde1 | ||
|
|
cb70d7bad3 | ||
|
|
2001bec8eb |
8
.github/workflows/node.js.yml
vendored
8
.github/workflows/node.js.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [10.x, 12.x, 14.x]
|
node-version: [16.x, 18.x]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
@@ -25,5 +25,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build --if-present
|
- run: npm run lint
|
||||||
- run: npm test
|
- run: npm run test-extensive
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
|||||||
31
.github/workflows/nodejs.yml
vendored
31
.github/workflows/nodejs.yml
vendored
@@ -1,31 +0,0 @@
|
|||||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
|
||||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
|
||||||
|
|
||||||
name: Node.js CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ master ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ master ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [10.x, 12.x, 13.x]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
|
||||||
uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: ${{ matrix.node-version }}
|
|
||||||
- run: npm ci
|
|
||||||
- run: npm run lint
|
|
||||||
- run: npm run test-extensive
|
|
||||||
env:
|
|
||||||
CI: true
|
|
||||||
24
README.md
24
README.md
@@ -753,6 +753,30 @@ currentState1 = Y.mergeUpdates([currentState1, diff2])
|
|||||||
currentState1 = Y.mergeUpdates([currentState1, diff1])
|
currentState1 = Y.mergeUpdates([currentState1, diff1])
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Obfuscating Updates
|
||||||
|
|
||||||
|
If one of your users runs into a weird bug (e.g. the rich-text editor throws
|
||||||
|
error messages), then you don't have to request the full document from your
|
||||||
|
user. Instead, they can obfuscate the document (i.e. replace the content with
|
||||||
|
meaningless generated content) before sending it to you. Note that someone might
|
||||||
|
still deduce the type of content by looking at the general structure of the
|
||||||
|
document. But this is much better than requesting the original document.
|
||||||
|
|
||||||
|
Obfuscated updates contain all the CRDT-related data that is required for
|
||||||
|
merging. So it is safe to merge obfuscated updates.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
// perform some changes..
|
||||||
|
ydoc.getText().insert(0, 'hello world')
|
||||||
|
const update = Y.encodeStateAsUpdate(ydoc)
|
||||||
|
// the below update contains scrambled data
|
||||||
|
const obfuscatedUpdate = Y.obfuscateUpdate(update)
|
||||||
|
const ydoc2 = new Y.Doc()
|
||||||
|
Y.applyUpdate(ydoc2, obfuscatedUpdate)
|
||||||
|
ydoc2.getText().toString() // => "00000000000"
|
||||||
|
```
|
||||||
|
|
||||||
#### Using V2 update format
|
#### Using V2 update format
|
||||||
|
|
||||||
Yjs implements two update formats. By default you are using the V1 update format.
|
Yjs implements two update formats. By default you are using the V1 update format.
|
||||||
|
|||||||
3504
package-lock.json
generated
3504
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.5.50",
|
"version": "13.6.0",
|
||||||
"description": "Shared Editing Library",
|
"description": "Shared Editing Library",
|
||||||
"main": "./dist/yjs.cjs",
|
"main": "./dist/yjs.cjs",
|
||||||
"module": "./dist/yjs.mjs",
|
"module": "./dist/yjs.mjs",
|
||||||
@@ -75,19 +75,24 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://docs.yjs.dev",
|
"homepage": "https://docs.yjs.dev",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lib0": "^0.2.49"
|
"lib0": "^0.2.74"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^17.0.0",
|
"@rollup/plugin-commonjs": "^24.0.1",
|
||||||
"@rollup/plugin-node-resolve": "^11.2.1",
|
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||||
|
"@types/node": "^18.15.5",
|
||||||
"concurrently": "^3.6.1",
|
"concurrently": "^3.6.1",
|
||||||
"typescript": "^4.9.5",
|
|
||||||
"http-server": "^0.12.3",
|
"http-server": "^0.12.3",
|
||||||
"jsdoc": "^3.6.7",
|
"jsdoc": "^3.6.7",
|
||||||
"markdownlint-cli": "^0.23.2",
|
"markdownlint-cli": "^0.23.2",
|
||||||
"rollup": "^2.60.0",
|
"rollup": "^3.20.0",
|
||||||
"standard": "^16.0.4",
|
"standard": "^16.0.4",
|
||||||
"tui-jsdoc-template": "^1.2.2",
|
"tui-jsdoc-template": "^1.2.2",
|
||||||
|
"typescript": "^4.9.5",
|
||||||
"y-protocols": "^1.0.5"
|
"y-protocols": "^1.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"npm": ">=8.0.0",
|
||||||
|
"node": ">=16.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,13 +42,7 @@ export default [{
|
|||||||
name: 'Y',
|
name: 'Y',
|
||||||
file: 'dist/yjs.cjs',
|
file: 'dist/yjs.cjs',
|
||||||
format: 'cjs',
|
format: 'cjs',
|
||||||
sourcemap: true,
|
sourcemap: true
|
||||||
paths: path => {
|
|
||||||
if (/^lib0\//.test(path)) {
|
|
||||||
return `lib0/dist/${path.slice(5)}.cjs`
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
external: id => /^lib0\//.test(id)
|
external: id => /^lib0\//.test(id)
|
||||||
}, {
|
}, {
|
||||||
@@ -88,7 +82,7 @@ export default [{
|
|||||||
plugins: [
|
plugins: [
|
||||||
debugResolve,
|
debugResolve,
|
||||||
nodeResolve({
|
nodeResolve({
|
||||||
mainFields: ['module', 'browser', 'main']
|
mainFields: ['browser', 'module', 'main']
|
||||||
}),
|
}),
|
||||||
commonjs()
|
commonjs()
|
||||||
]
|
]
|
||||||
@@ -103,9 +97,10 @@ export default [{
|
|||||||
plugins: [
|
plugins: [
|
||||||
debugResolve,
|
debugResolve,
|
||||||
nodeResolve({
|
nodeResolve({
|
||||||
mainFields: ['module', 'main']
|
mainFields: ['node', 'module', 'main'],
|
||||||
|
exportConditions: ['node', 'module', 'import', 'default']
|
||||||
}),
|
}),
|
||||||
commonjs()
|
commonjs()
|
||||||
],
|
],
|
||||||
external: ['isomorphic.js']
|
external: id => /^lib0\//.test(id)
|
||||||
}]
|
}]
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ export {
|
|||||||
diffUpdateV2,
|
diffUpdateV2,
|
||||||
convertUpdateFormatV1ToV2,
|
convertUpdateFormatV1ToV2,
|
||||||
convertUpdateFormatV2ToV1,
|
convertUpdateFormatV2ToV1,
|
||||||
|
obfuscateUpdate,
|
||||||
|
obfuscateUpdateV2,
|
||||||
UpdateEncoderV1
|
UpdateEncoderV1
|
||||||
} from './internals.js'
|
} from './internals.js'
|
||||||
|
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ import {
|
|||||||
readContentType,
|
readContentType,
|
||||||
addChangedTypeToTransaction,
|
addChangedTypeToTransaction,
|
||||||
isDeleted,
|
isDeleted,
|
||||||
DeleteSet, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction // eslint-disable-line
|
StackItem, DeleteSet, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as error from 'lib0/error'
|
import * as error from 'lib0/error'
|
||||||
import * as binary from 'lib0/binary'
|
import * as binary from 'lib0/binary'
|
||||||
|
import * as array from 'lib0/array'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @todo This should return several items
|
* @todo This should return several items
|
||||||
@@ -120,6 +121,12 @@ export const splitItem = (transaction, leftItem, diff) => {
|
|||||||
return rightItem
|
return rightItem
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array<StackItem>} stack
|
||||||
|
* @param {ID} id
|
||||||
|
*/
|
||||||
|
const isDeletedByUndoStack = (stack, id) => array.some(stack, /** @param {StackItem} s */ s => isDeleted(s.deletions, id))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redoes the effect of this operation.
|
* Redoes the effect of this operation.
|
||||||
*
|
*
|
||||||
@@ -128,12 +135,13 @@ export const splitItem = (transaction, leftItem, diff) => {
|
|||||||
* @param {Set<Item>} redoitems
|
* @param {Set<Item>} redoitems
|
||||||
* @param {DeleteSet} itemsToDelete
|
* @param {DeleteSet} itemsToDelete
|
||||||
* @param {boolean} ignoreRemoteMapChanges
|
* @param {boolean} ignoreRemoteMapChanges
|
||||||
|
* @param {import('../utils/UndoManager.js').UndoManager} um
|
||||||
*
|
*
|
||||||
* @return {Item|null}
|
* @return {Item|null}
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemoteMapChanges) => {
|
export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemoteMapChanges, um) => {
|
||||||
const doc = transaction.doc
|
const doc = transaction.doc
|
||||||
const store = doc.store
|
const store = doc.store
|
||||||
const ownClientID = doc.clientID
|
const ownClientID = doc.clientID
|
||||||
@@ -153,7 +161,7 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemo
|
|||||||
// make sure that parent is redone
|
// make sure that parent is redone
|
||||||
if (parentItem !== null && parentItem.deleted === true) {
|
if (parentItem !== null && parentItem.deleted === true) {
|
||||||
// try to undo parent if it will be undone anyway
|
// try to undo parent if it will be undone anyway
|
||||||
if (parentItem.redone === null && (!redoitems.has(parentItem) || redoItem(transaction, parentItem, redoitems, itemsToDelete, ignoreRemoteMapChanges) === null)) {
|
if (parentItem.redone === null && (!redoitems.has(parentItem) || redoItem(transaction, parentItem, redoitems, itemsToDelete, ignoreRemoteMapChanges, um) === null)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
while (parentItem.redone !== null) {
|
while (parentItem.redone !== null) {
|
||||||
@@ -203,13 +211,10 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemo
|
|||||||
left = item
|
left = item
|
||||||
// Iterate right while right is in itemsToDelete
|
// Iterate right while right is in itemsToDelete
|
||||||
// If it is intended to delete right while item is redone, we can expect that item should replace right.
|
// If it is intended to delete right while item is redone, we can expect that item should replace right.
|
||||||
while (left !== null && left.right !== null && isDeleted(itemsToDelete, left.right.id)) {
|
while (left !== null && left.right !== null && (left.right.redone || isDeleted(itemsToDelete, left.right.id) || isDeletedByUndoStack(um.undoStack, left.right.id) || isDeletedByUndoStack(um.redoStack, left.right.id))) {
|
||||||
left = left.right
|
left = left.right
|
||||||
}
|
// follow redone
|
||||||
// follow redone
|
while (left.redone) left = getItemCleanStart(transaction, left.redone)
|
||||||
// trace redone until parent matches
|
|
||||||
while (left !== null && left.redone !== null) {
|
|
||||||
left = getItemCleanStart(transaction, left.redone)
|
|
||||||
}
|
}
|
||||||
if (left && left.right !== null) {
|
if (left && left.right !== null) {
|
||||||
// It is not possible to redo this item because it conflicts with a
|
// It is not possible to redo this item because it conflicts with a
|
||||||
@@ -756,48 +761,48 @@ export class AbstractContent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} offset
|
* @param {number} _offset
|
||||||
* @return {AbstractContent}
|
* @return {AbstractContent}
|
||||||
*/
|
*/
|
||||||
splice (offset) {
|
splice (_offset) {
|
||||||
throw error.methodUnimplemented()
|
throw error.methodUnimplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractContent} right
|
* @param {AbstractContent} _right
|
||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
*/
|
*/
|
||||||
mergeWith (right) {
|
mergeWith (_right) {
|
||||||
throw error.methodUnimplemented()
|
throw error.methodUnimplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Transaction} transaction
|
* @param {Transaction} _transaction
|
||||||
* @param {Item} item
|
* @param {Item} _item
|
||||||
*/
|
*/
|
||||||
integrate (transaction, item) {
|
integrate (_transaction, _item) {
|
||||||
throw error.methodUnimplemented()
|
throw error.methodUnimplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Transaction} transaction
|
* @param {Transaction} _transaction
|
||||||
*/
|
*/
|
||||||
delete (transaction) {
|
delete (_transaction) {
|
||||||
throw error.methodUnimplemented()
|
throw error.methodUnimplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {StructStore} store
|
* @param {StructStore} _store
|
||||||
*/
|
*/
|
||||||
gc (store) {
|
gc (_store) {
|
||||||
throw error.methodUnimplemented()
|
throw error.methodUnimplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} _encoder
|
||||||
* @param {number} offset
|
* @param {number} _offset
|
||||||
*/
|
*/
|
||||||
write (encoder, offset) {
|
write (_encoder, _offset) {
|
||||||
throw error.methodUnimplemented()
|
throw error.methodUnimplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ export class YArray extends AbstractType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a provided function on once on overy element of this YArray.
|
* Executes a provided function once on overy element of this YArray.
|
||||||
*
|
*
|
||||||
* @param {function(T,number,YArray<T>):void} f A function to execute on every element of this YArray.
|
* @param {function(T,number,YArray<T>):void} f A function to execute on every element of this YArray.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -631,36 +631,39 @@ export class YTextEvent extends YEvent {
|
|||||||
/**
|
/**
|
||||||
* @type {any}
|
* @type {any}
|
||||||
*/
|
*/
|
||||||
let op
|
let op = null
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'delete':
|
case 'delete':
|
||||||
op = { delete: deleteLen }
|
if (deleteLen > 0) {
|
||||||
|
op = { delete: deleteLen }
|
||||||
|
}
|
||||||
deleteLen = 0
|
deleteLen = 0
|
||||||
break
|
break
|
||||||
case 'insert':
|
case 'insert':
|
||||||
op = { insert }
|
if (typeof insert === 'object' || insert.length > 0) {
|
||||||
if (currentAttributes.size > 0) {
|
op = { insert }
|
||||||
op.attributes = {}
|
if (currentAttributes.size > 0) {
|
||||||
currentAttributes.forEach((value, key) => {
|
op.attributes = {}
|
||||||
if (value !== null) {
|
currentAttributes.forEach((value, key) => {
|
||||||
op.attributes[key] = value
|
if (value !== null) {
|
||||||
}
|
op.attributes[key] = value
|
||||||
})
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
insert = ''
|
insert = ''
|
||||||
break
|
break
|
||||||
case 'retain':
|
case 'retain':
|
||||||
op = { retain }
|
if (retain > 0) {
|
||||||
if (Object.keys(attributes).length > 0) {
|
op = { retain }
|
||||||
op.attributes = {}
|
if (!object.isEmpty(attributes)) {
|
||||||
for (const key in attributes) {
|
op.attributes = object.assign({}, attributes)
|
||||||
op.attributes[key] = attributes[key]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
retain = 0
|
retain = 0
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
delta.push(op)
|
if (op) delta.push(op)
|
||||||
action = null
|
action = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ export const mergeDeleteSets = dss => {
|
|||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const addToDeleteSet = (ds, client, clock, length) => {
|
export const addToDeleteSet = (ds, client, clock, length) => {
|
||||||
map.setIfUndefined(ds.clients, client, () => []).push(new DeleteItem(clock, length))
|
map.setIfUndefined(ds.clients, client, () => /** @type {Array<DeleteItem>} */ ([])).push(new DeleteItem(clock, length))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createDeleteSet = () => new DeleteSet()
|
export const createDeleteSet = () => new DeleteSet()
|
||||||
@@ -251,7 +251,7 @@ export const readDeleteSet = decoder => {
|
|||||||
const client = decoding.readVarUint(decoder.restDecoder)
|
const client = decoding.readVarUint(decoder.restDecoder)
|
||||||
const numberOfDeletes = decoding.readVarUint(decoder.restDecoder)
|
const numberOfDeletes = decoding.readVarUint(decoder.restDecoder)
|
||||||
if (numberOfDeletes > 0) {
|
if (numberOfDeletes > 0) {
|
||||||
const dsField = map.setIfUndefined(ds.clients, client, () => [])
|
const dsField = map.setIfUndefined(ds.clients, client, () => /** @type {Array<DeleteItem>} */ ([]))
|
||||||
for (let i = 0; i < numberOfDeletes; i++) {
|
for (let i = 0; i < numberOfDeletes; i++) {
|
||||||
dsField.push(new DeleteItem(decoder.readDsClock(), decoder.readDsLen()))
|
dsField.push(new DeleteItem(decoder.readDsClock(), decoder.readDsLen()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ import {
|
|||||||
getItemCleanStart,
|
getItemCleanStart,
|
||||||
isDeleted,
|
isDeleted,
|
||||||
addToDeleteSet,
|
addToDeleteSet,
|
||||||
Transaction, Doc, Item, GC, DeleteSet, AbstractType, YEvent // eslint-disable-line
|
Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as time from 'lib0/time'
|
import * as time from 'lib0/time'
|
||||||
import * as array from 'lib0/array'
|
import * as array from 'lib0/array'
|
||||||
import { Observable } from 'lib0/observable'
|
import { Observable } from 'lib0/observable'
|
||||||
|
|
||||||
class StackItem {
|
export class StackItem {
|
||||||
/**
|
/**
|
||||||
* @param {DeleteSet} deletions
|
* @param {DeleteSet} deletions
|
||||||
* @param {DeleteSet} insertions
|
* @param {DeleteSet} insertions
|
||||||
@@ -101,7 +101,7 @@ const popStackItem = (undoManager, stack, eventType) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
itemsToRedo.forEach(struct => {
|
itemsToRedo.forEach(struct => {
|
||||||
performedChange = redoItem(transaction, struct, itemsToRedo, stackItem.insertions, undoManager.ignoreRemoteMapChanges) !== null || performedChange
|
performedChange = redoItem(transaction, struct, itemsToRedo, stackItem.insertions, undoManager.ignoreRemoteMapChanges, undoManager) !== null || performedChange
|
||||||
})
|
})
|
||||||
// We want to delete in reverse order so that children are deleted before
|
// We want to delete in reverse order so that children are deleted before
|
||||||
// parents, so we have more information available when items are filtered.
|
// parents, so we have more information available when items are filtered.
|
||||||
@@ -158,7 +158,7 @@ export class UndoManager extends Observable {
|
|||||||
*/
|
*/
|
||||||
constructor (typeScope, {
|
constructor (typeScope, {
|
||||||
captureTimeout = 500,
|
captureTimeout = 500,
|
||||||
captureTransaction = tr => true,
|
captureTransaction = _tr => true,
|
||||||
deleteFilter = () => true,
|
deleteFilter = () => true,
|
||||||
trackedOrigins = new Set([null]),
|
trackedOrigins = new Set([null]),
|
||||||
ignoreRemoteMapChanges = false,
|
ignoreRemoteMapChanges = false,
|
||||||
|
|||||||
@@ -2,19 +2,40 @@
|
|||||||
import * as binary from 'lib0/binary'
|
import * as binary from 'lib0/binary'
|
||||||
import * as decoding from 'lib0/decoding'
|
import * as decoding from 'lib0/decoding'
|
||||||
import * as encoding from 'lib0/encoding'
|
import * as encoding from 'lib0/encoding'
|
||||||
|
import * as error from 'lib0/error'
|
||||||
|
import * as f from 'lib0/function'
|
||||||
import * as logging from 'lib0/logging'
|
import * as logging from 'lib0/logging'
|
||||||
|
import * as map from 'lib0/map'
|
||||||
import * as math from 'lib0/math'
|
import * as math from 'lib0/math'
|
||||||
|
import * as string from 'lib0/string'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
ContentAny,
|
||||||
|
ContentBinary,
|
||||||
|
ContentDeleted,
|
||||||
|
ContentDoc,
|
||||||
|
ContentEmbed,
|
||||||
|
ContentFormat,
|
||||||
|
ContentJSON,
|
||||||
|
ContentString,
|
||||||
|
ContentType,
|
||||||
createID,
|
createID,
|
||||||
readItemContent,
|
decodeStateVector,
|
||||||
readDeleteSet,
|
|
||||||
writeDeleteSet,
|
|
||||||
Skip,
|
|
||||||
mergeDeleteSets,
|
|
||||||
DSEncoderV1,
|
DSEncoderV1,
|
||||||
DSEncoderV2,
|
DSEncoderV2,
|
||||||
decodeStateVector,
|
GC,
|
||||||
Item, GC, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2 // eslint-disable-line
|
Item,
|
||||||
|
mergeDeleteSets,
|
||||||
|
readDeleteSet,
|
||||||
|
readItemContent,
|
||||||
|
Skip,
|
||||||
|
UpdateDecoderV1,
|
||||||
|
UpdateDecoderV2,
|
||||||
|
UpdateEncoderV1,
|
||||||
|
UpdateEncoderV2,
|
||||||
|
writeDeleteSet,
|
||||||
|
YXmlElement,
|
||||||
|
YXmlHook
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -552,17 +573,17 @@ const finishLazyStructWriting = (lazyWriter) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Uint8Array} update
|
* @param {Uint8Array} update
|
||||||
|
* @param {function(Item|GC|Skip):Item|GC|Skip} blockTransformer
|
||||||
* @param {typeof UpdateDecoderV2 | typeof UpdateDecoderV1} YDecoder
|
* @param {typeof UpdateDecoderV2 | typeof UpdateDecoderV1} YDecoder
|
||||||
* @param {typeof UpdateEncoderV2 | typeof UpdateEncoderV1 } YEncoder
|
* @param {typeof UpdateEncoderV2 | typeof UpdateEncoderV1 } YEncoder
|
||||||
*/
|
*/
|
||||||
export const convertUpdateFormat = (update, YDecoder, YEncoder) => {
|
export const convertUpdateFormat = (update, blockTransformer, YDecoder, YEncoder) => {
|
||||||
const updateDecoder = new YDecoder(decoding.createDecoder(update))
|
const updateDecoder = new YDecoder(decoding.createDecoder(update))
|
||||||
const lazyDecoder = new LazyStructReader(updateDecoder, false)
|
const lazyDecoder = new LazyStructReader(updateDecoder, false)
|
||||||
const updateEncoder = new YEncoder()
|
const updateEncoder = new YEncoder()
|
||||||
const lazyWriter = new LazyStructWriter(updateEncoder)
|
const lazyWriter = new LazyStructWriter(updateEncoder)
|
||||||
|
|
||||||
for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) {
|
for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) {
|
||||||
writeStructToLazyStructWriter(lazyWriter, curr, 0)
|
writeStructToLazyStructWriter(lazyWriter, blockTransformer(curr), 0)
|
||||||
}
|
}
|
||||||
finishLazyStructWriting(lazyWriter)
|
finishLazyStructWriting(lazyWriter)
|
||||||
const ds = readDeleteSet(updateDecoder)
|
const ds = readDeleteSet(updateDecoder)
|
||||||
@@ -571,11 +592,132 @@ export const convertUpdateFormat = (update, YDecoder, YEncoder) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Uint8Array} update
|
* @typedef {Object} ObfuscatorOptions
|
||||||
|
* @property {boolean} [ObfuscatorOptions.formatting=true]
|
||||||
|
* @property {boolean} [ObfuscatorOptions.subdocs=true]
|
||||||
|
* @property {boolean} [ObfuscatorOptions.yxml=true] Whether to obfuscate nodeName / hookName
|
||||||
*/
|
*/
|
||||||
export const convertUpdateFormatV1ToV2 = update => convertUpdateFormat(update, UpdateDecoderV1, UpdateEncoderV2)
|
|
||||||
|
/**
|
||||||
|
* @param {ObfuscatorOptions} obfuscator
|
||||||
|
*/
|
||||||
|
const createObfuscator = ({ formatting = true, subdocs = true, yxml = true } = {}) => {
|
||||||
|
let i = 0
|
||||||
|
const mapKeyCache = map.create()
|
||||||
|
const nodeNameCache = map.create()
|
||||||
|
const formattingKeyCache = map.create()
|
||||||
|
const formattingValueCache = map.create()
|
||||||
|
formattingValueCache.set(null, null) // end of a formatting range should always be the end of a formatting range
|
||||||
|
/**
|
||||||
|
* @param {Item|GC|Skip} block
|
||||||
|
* @return {Item|GC|Skip}
|
||||||
|
*/
|
||||||
|
return block => {
|
||||||
|
switch (block.constructor) {
|
||||||
|
case GC:
|
||||||
|
case Skip:
|
||||||
|
return block
|
||||||
|
case Item: {
|
||||||
|
const item = /** @type {Item} */ (block)
|
||||||
|
const content = item.content
|
||||||
|
switch (content.constructor) {
|
||||||
|
case ContentDeleted:
|
||||||
|
break
|
||||||
|
case ContentType: {
|
||||||
|
if (yxml) {
|
||||||
|
const type = /** @type {ContentType} */ (content).type
|
||||||
|
if (type instanceof YXmlElement) {
|
||||||
|
type.nodeName = map.setIfUndefined(nodeNameCache, type.nodeName, () => 'node-' + i)
|
||||||
|
}
|
||||||
|
if (type instanceof YXmlHook) {
|
||||||
|
type.hookName = map.setIfUndefined(nodeNameCache, type.hookName, () => 'hook-' + i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case ContentAny: {
|
||||||
|
const c = /** @type {ContentAny} */ (content)
|
||||||
|
c.arr = c.arr.map(() => i)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case ContentBinary: {
|
||||||
|
const c = /** @type {ContentBinary} */ (content)
|
||||||
|
c.content = new Uint8Array([i])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case ContentDoc: {
|
||||||
|
const c = /** @type {ContentDoc} */ (content)
|
||||||
|
if (subdocs) {
|
||||||
|
c.opts = {}
|
||||||
|
c.doc.guid = i + ''
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case ContentEmbed: {
|
||||||
|
const c = /** @type {ContentEmbed} */ (content)
|
||||||
|
c.embed = {}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case ContentFormat: {
|
||||||
|
const c = /** @type {ContentFormat} */ (content)
|
||||||
|
if (formatting) {
|
||||||
|
c.key = map.setIfUndefined(formattingKeyCache, c.key, () => i + '')
|
||||||
|
c.value = map.setIfUndefined(formattingValueCache, c.value, () => ({ i }))
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case ContentJSON: {
|
||||||
|
const c = /** @type {ContentJSON} */ (content)
|
||||||
|
c.arr = c.arr.map(() => i)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case ContentString: {
|
||||||
|
const c = /** @type {ContentString} */ (content)
|
||||||
|
c.str = string.repeat((i % 10) + '', c.str.length)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// unknown content type
|
||||||
|
error.unexpectedCase()
|
||||||
|
}
|
||||||
|
if (item.parentSub) {
|
||||||
|
item.parentSub = map.setIfUndefined(mapKeyCache, item.parentSub, () => i + '')
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
return block
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// unknown block-type
|
||||||
|
error.unexpectedCase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function obfuscates the content of a Yjs update. This is useful to share
|
||||||
|
* buggy Yjs documents while significantly limiting the possibility that a
|
||||||
|
* developer can on the user. Note that it might still be possible to deduce
|
||||||
|
* some information by analyzing the "structure" of the document or by analyzing
|
||||||
|
* the typing behavior using the CRDT-related metadata that is still kept fully
|
||||||
|
* intact.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} update
|
||||||
|
* @param {ObfuscatorOptions} [opts]
|
||||||
|
*/
|
||||||
|
export const obfuscateUpdate = (update, opts) => convertUpdateFormat(update, createObfuscator(opts), UpdateDecoderV1, UpdateEncoderV1)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Uint8Array} update
|
||||||
|
* @param {ObfuscatorOptions} [opts]
|
||||||
|
*/
|
||||||
|
export const obfuscateUpdateV2 = (update, opts) => convertUpdateFormat(update, createObfuscator(opts), UpdateDecoderV2, UpdateEncoderV2)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Uint8Array} update
|
* @param {Uint8Array} update
|
||||||
*/
|
*/
|
||||||
export const convertUpdateFormatV2ToV1 = update => convertUpdateFormat(update, UpdateDecoderV2, UpdateEncoderV1)
|
export const convertUpdateFormatV1ToV2 = update => convertUpdateFormat(update, f.id, UpdateDecoderV1, UpdateEncoderV2)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Uint8Array} update
|
||||||
|
*/
|
||||||
|
export const convertUpdateFormatV2ToV1 = update => convertUpdateFormat(update, f.id, UpdateDecoderV2, UpdateEncoderV1)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
|
||||||
import * as map from './y-map.tests.js'
|
import * as map from './y-map.tests.js'
|
||||||
import * as array from './y-array.tests.js'
|
import * as array from './y-array.tests.js'
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ export class TestYInstance extends Y.Doc {
|
|||||||
* @param {TestYInstance} remoteClient
|
* @param {TestYInstance} remoteClient
|
||||||
*/
|
*/
|
||||||
_receive (message, remoteClient) {
|
_receive (message, remoteClient) {
|
||||||
map.setIfUndefined(this.receiving, remoteClient, () => []).push(message)
|
map.setIfUndefined(this.receiving, remoteClient, () => /** @type {Array<Uint8Array>} */ ([])).push(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,7 +347,7 @@ export const compare = users => {
|
|||||||
t.compare(userMapValues[i], userMapValues[i + 1])
|
t.compare(userMapValues[i], userMapValues[i + 1])
|
||||||
t.compare(userXmlValues[i], userXmlValues[i + 1])
|
t.compare(userXmlValues[i], userXmlValues[i + 1])
|
||||||
t.compare(userTextValues[i].map(/** @param {any} a */ a => typeof a.insert === 'string' ? a.insert : ' ').join('').length, users[i].getText('text').length)
|
t.compare(userTextValues[i].map(/** @param {any} a */ a => typeof a.insert === 'string' ? a.insert : ' ').join('').length, users[i].getText('text').length)
|
||||||
t.compare(userTextValues[i], userTextValues[i + 1], '', (constructor, a, b) => {
|
t.compare(userTextValues[i], userTextValues[i + 1], '', (_constructor, a, b) => {
|
||||||
if (a instanceof Y.AbstractType) {
|
if (a instanceof Y.AbstractType) {
|
||||||
t.compare(a.toJSON(), b.toJSON())
|
t.compare(a.toJSON(), b.toJSON())
|
||||||
} else if (a !== b) {
|
} else if (a !== b) {
|
||||||
@@ -370,8 +370,8 @@ export const compare = users => {
|
|||||||
export const compareItemIDs = (a, b) => a === b || (a !== null && b != null && Y.compareIDs(a.id, b.id))
|
export const compareItemIDs = (a, b) => a === b || (a !== null && b != null && Y.compareIDs(a.id, b.id))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('../src/internals').StructStore} ss1
|
* @param {import('../src/internals.js').StructStore} ss1
|
||||||
* @param {import('../src/internals').StructStore} ss2
|
* @param {import('../src/internals.js').StructStore} ss2
|
||||||
*/
|
*/
|
||||||
export const compareStructStores = (ss1, ss2) => {
|
export const compareStructStores = (ss1, ss2) => {
|
||||||
t.assert(ss1.clients.size === ss2.clients.size)
|
t.assert(ss1.clients.size === ss2.clients.size)
|
||||||
@@ -413,13 +413,13 @@ export const compareStructStores = (ss1, ss2) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('../src/internals').DeleteSet} ds1
|
* @param {import('../src/internals.js').DeleteSet} ds1
|
||||||
* @param {import('../src/internals').DeleteSet} ds2
|
* @param {import('../src/internals.js').DeleteSet} ds2
|
||||||
*/
|
*/
|
||||||
export const compareDS = (ds1, ds2) => {
|
export const compareDS = (ds1, ds2) => {
|
||||||
t.assert(ds1.clients.size === ds2.clients.size)
|
t.assert(ds1.clients.size === ds2.clients.size)
|
||||||
ds1.clients.forEach((deleteItems1, client) => {
|
ds1.clients.forEach((deleteItems1, client) => {
|
||||||
const deleteItems2 = /** @type {Array<import('../src/internals').DeleteItem>} */ (ds2.clients.get(client))
|
const deleteItems2 = /** @type {Array<import('../src/internals.js').DeleteItem>} */ (ds2.clients.get(client))
|
||||||
t.assert(deleteItems2 !== undefined && deleteItems1.length === deleteItems2.length)
|
t.assert(deleteItems2 !== undefined && deleteItems1.length === deleteItems2.length)
|
||||||
for (let i = 0; i < deleteItems1.length; i++) {
|
for (let i = 0; i < deleteItems1.length; i++) {
|
||||||
const di1 = deleteItems1[i]
|
const di1 = deleteItems1[i]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint-disable-line
|
import { init } from './testHelper.js' // eslint-disable-line
|
||||||
|
|
||||||
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'
|
||||||
@@ -64,9 +64,9 @@ export const testUndoText = tc => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Test case to fix #241
|
* Test case to fix #241
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testEmptyTypeScope = tc => {
|
export const testEmptyTypeScope = _tc => {
|
||||||
const ydoc = new Y.Doc()
|
const ydoc = new Y.Doc()
|
||||||
const um = new Y.UndoManager([], { doc: ydoc })
|
const um = new Y.UndoManager([], { doc: ydoc })
|
||||||
const yarray = ydoc.getArray()
|
const yarray = ydoc.getArray()
|
||||||
@@ -78,9 +78,9 @@ export const testEmptyTypeScope = tc => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Test case to fix #241
|
* Test case to fix #241
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testDoubleUndo = tc => {
|
export const testDoubleUndo = _tc => {
|
||||||
const doc = new Y.Doc()
|
const doc = new Y.Doc()
|
||||||
const text = doc.getText()
|
const text = doc.getText()
|
||||||
text.insert(0, '1221')
|
text.insert(0, '1221')
|
||||||
@@ -316,9 +316,9 @@ export const testUndoDeleteFilter = tc => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This issue has been reported in https://discuss.yjs.dev/t/undomanager-with-external-updates/454/6
|
* This issue has been reported in https://discuss.yjs.dev/t/undomanager-with-external-updates/454/6
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testUndoUntilChangePerformed = tc => {
|
export const testUndoUntilChangePerformed = _tc => {
|
||||||
const doc = new Y.Doc()
|
const doc = new Y.Doc()
|
||||||
const doc2 = new Y.Doc()
|
const doc2 = new Y.Doc()
|
||||||
doc.on('update', update => Y.applyUpdate(doc2, update))
|
doc.on('update', update => Y.applyUpdate(doc2, update))
|
||||||
@@ -347,9 +347,9 @@ export const testUndoUntilChangePerformed = tc => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This issue has been reported in https://github.com/yjs/yjs/issues/317
|
* This issue has been reported in https://github.com/yjs/yjs/issues/317
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testUndoNestedUndoIssue = tc => {
|
export const testUndoNestedUndoIssue = _tc => {
|
||||||
const doc = new Y.Doc({ gc: false })
|
const doc = new Y.Doc({ gc: false })
|
||||||
const design = doc.getMap()
|
const design = doc.getMap()
|
||||||
const undoManager = new Y.UndoManager(design, { captureTimeout: 0 })
|
const undoManager = new Y.UndoManager(design, { captureTimeout: 0 })
|
||||||
@@ -403,9 +403,9 @@ export const testUndoNestedUndoIssue = tc => {
|
|||||||
/**
|
/**
|
||||||
* This issue has been reported in https://github.com/yjs/yjs/issues/355
|
* This issue has been reported in https://github.com/yjs/yjs/issues/355
|
||||||
*
|
*
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testConsecutiveRedoBug = tc => {
|
export const testConsecutiveRedoBug = _tc => {
|
||||||
const doc = new Y.Doc()
|
const doc = new Y.Doc()
|
||||||
const yRoot = doc.getMap()
|
const yRoot = doc.getMap()
|
||||||
const undoMgr = new Y.UndoManager(yRoot)
|
const undoMgr = new Y.UndoManager(yRoot)
|
||||||
@@ -454,9 +454,9 @@ export const testConsecutiveRedoBug = tc => {
|
|||||||
/**
|
/**
|
||||||
* This issue has been reported in https://github.com/yjs/yjs/issues/304
|
* This issue has been reported in https://github.com/yjs/yjs/issues/304
|
||||||
*
|
*
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testUndoXmlBug = tc => {
|
export const testUndoXmlBug = _tc => {
|
||||||
const origin = 'origin'
|
const origin = 'origin'
|
||||||
const doc = new Y.Doc()
|
const doc = new Y.Doc()
|
||||||
const fragment = doc.getXmlFragment('t')
|
const fragment = doc.getXmlFragment('t')
|
||||||
@@ -499,9 +499,9 @@ export const testUndoXmlBug = tc => {
|
|||||||
/**
|
/**
|
||||||
* This issue has been reported in https://github.com/yjs/yjs/issues/343
|
* This issue has been reported in https://github.com/yjs/yjs/issues/343
|
||||||
*
|
*
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testUndoBlockBug = tc => {
|
export const testUndoBlockBug = _tc => {
|
||||||
const doc = new Y.Doc({ gc: false })
|
const doc = new Y.Doc({ gc: false })
|
||||||
const design = doc.getMap()
|
const design = doc.getMap()
|
||||||
|
|
||||||
@@ -559,9 +559,9 @@ export const testUndoBlockBug = tc => {
|
|||||||
* Undo text formatting delete should not corrupt peer state.
|
* Undo text formatting delete should not corrupt peer state.
|
||||||
*
|
*
|
||||||
* @see https://github.com/yjs/yjs/issues/392
|
* @see https://github.com/yjs/yjs/issues/392
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testUndoDeleteTextFormat = tc => {
|
export const testUndoDeleteTextFormat = _tc => {
|
||||||
const doc = new Y.Doc()
|
const doc = new Y.Doc()
|
||||||
const text = doc.getText()
|
const text = doc.getText()
|
||||||
text.insert(0, 'Attack ships on fire off the shoulder of Orion.')
|
text.insert(0, 'Attack ships on fire off the shoulder of Orion.')
|
||||||
@@ -597,9 +597,9 @@ export const testUndoDeleteTextFormat = tc => {
|
|||||||
* Undo text formatting delete should not corrupt peer state.
|
* Undo text formatting delete should not corrupt peer state.
|
||||||
*
|
*
|
||||||
* @see https://github.com/yjs/yjs/issues/392
|
* @see https://github.com/yjs/yjs/issues/392
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testBehaviorOfIgnoreremotemapchangesProperty = tc => {
|
export const testBehaviorOfIgnoreremotemapchangesProperty = _tc => {
|
||||||
const doc = new Y.Doc()
|
const doc = new Y.Doc()
|
||||||
const doc2 = new Y.Doc()
|
const doc2 = new Y.Doc()
|
||||||
doc.on('update', update => Y.applyUpdate(doc2, update, doc))
|
doc.on('update', update => Y.applyUpdate(doc2, update, doc))
|
||||||
@@ -620,9 +620,9 @@ export const testBehaviorOfIgnoreremotemapchangesProperty = tc => {
|
|||||||
* Special deletion case.
|
* Special deletion case.
|
||||||
*
|
*
|
||||||
* @see https://github.com/yjs/yjs/issues/447
|
* @see https://github.com/yjs/yjs/issues/447
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testSpecialDeletionCase = tc => {
|
export const testSpecialDeletionCase = _tc => {
|
||||||
const origin = 'undoable'
|
const origin = 'undoable'
|
||||||
const doc = new Y.Doc()
|
const doc = new Y.Doc()
|
||||||
const fragment = doc.getXmlFragment()
|
const fragment = doc.getXmlFragment()
|
||||||
@@ -644,3 +644,34 @@ export const testSpecialDeletionCase = tc => {
|
|||||||
undoManager.undo()
|
undoManager.undo()
|
||||||
t.compareStrings(fragment.toString(), '<test a="1" b="2"></test>')
|
t.compareStrings(fragment.toString(), '<test a="1" b="2"></test>')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deleted entries in a map should be restored on undo.
|
||||||
|
*
|
||||||
|
* @see https://github.com/yjs/yjs/issues/500
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testUndoDeleteInMap = (tc) => {
|
||||||
|
const { map0 } = init(tc, { users: 3 })
|
||||||
|
const undoManager = new Y.UndoManager(map0, { captureTimeout: 0 })
|
||||||
|
map0.set('a', 'a')
|
||||||
|
map0.delete('a')
|
||||||
|
map0.set('a', 'b')
|
||||||
|
map0.delete('a')
|
||||||
|
map0.set('a', 'c')
|
||||||
|
map0.delete('a')
|
||||||
|
map0.set('a', 'd')
|
||||||
|
t.compare(map0.toJSON(), { a: 'd' })
|
||||||
|
undoManager.undo()
|
||||||
|
t.compare(map0.toJSON(), {})
|
||||||
|
undoManager.undo()
|
||||||
|
t.compare(map0.toJSON(), { a: 'c' })
|
||||||
|
undoManager.undo()
|
||||||
|
t.compare(map0.toJSON(), {})
|
||||||
|
undoManager.undo()
|
||||||
|
t.compare(map0.toJSON(), { a: 'b' })
|
||||||
|
undoManager.undo()
|
||||||
|
t.compare(map0.toJSON(), {})
|
||||||
|
undoManager.undo()
|
||||||
|
t.compare(map0.toJSON(), { a: 'a' })
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import * as Y from '../src/index.js'
|
|||||||
import { readClientsStructRefs, readDeleteSet, UpdateDecoderV2, UpdateEncoderV2, writeDeleteSet } from '../src/internals.js'
|
import { readClientsStructRefs, readDeleteSet, UpdateDecoderV2, UpdateEncoderV2, writeDeleteSet } from '../src/internals.js'
|
||||||
import * as encoding from 'lib0/encoding'
|
import * as encoding from 'lib0/encoding'
|
||||||
import * as decoding from 'lib0/decoding'
|
import * as decoding from 'lib0/decoding'
|
||||||
|
import * as object from 'lib0/object'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} Enc
|
* @typedef {Object} Enc
|
||||||
@@ -138,7 +139,6 @@ export const testKeyEncoding = tc => {
|
|||||||
*/
|
*/
|
||||||
const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
|
const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
|
||||||
const cases = []
|
const cases = []
|
||||||
|
|
||||||
// Case 1: Simple case, simply merge everything
|
// Case 1: Simple case, simply merge everything
|
||||||
cases.push(enc.mergeUpdates(updates))
|
cases.push(enc.mergeUpdates(updates))
|
||||||
|
|
||||||
@@ -304,3 +304,54 @@ export const testMergePendingUpdates = tc => {
|
|||||||
const yText5 = yDoc5.getText('textBlock')
|
const yText5 = yDoc5.getText('textBlock')
|
||||||
t.compareStrings(yText5.toString(), 'nenor')
|
t.compareStrings(yText5.toString(), 'nenor')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testObfuscateUpdates = tc => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
const ytext = ydoc.getText('text')
|
||||||
|
const ymap = ydoc.getMap('map')
|
||||||
|
const yarray = ydoc.getArray('array')
|
||||||
|
// test ytext
|
||||||
|
ytext.applyDelta([{ insert: 'text', attributes: { bold: true } }, { insert: { href: 'supersecreturl' } }])
|
||||||
|
// test ymap
|
||||||
|
ymap.set('key', 'secret1')
|
||||||
|
ymap.set('key', 'secret2')
|
||||||
|
// test yarray with subtype & subdoc
|
||||||
|
const subtype = new Y.XmlElement('secretnodename')
|
||||||
|
const subdoc = new Y.Doc({ guid: 'secret' })
|
||||||
|
subtype.setAttribute('attr', 'val')
|
||||||
|
yarray.insert(0, ['teststring', 42, subtype, subdoc])
|
||||||
|
// obfuscate the content and put it into a new document
|
||||||
|
const obfuscatedUpdate = Y.obfuscateUpdate(Y.encodeStateAsUpdate(ydoc))
|
||||||
|
const odoc = new Y.Doc()
|
||||||
|
Y.applyUpdate(odoc, obfuscatedUpdate)
|
||||||
|
const otext = odoc.getText('text')
|
||||||
|
const omap = odoc.getMap('map')
|
||||||
|
const oarray = odoc.getArray('array')
|
||||||
|
// test ytext
|
||||||
|
const delta = otext.toDelta()
|
||||||
|
t.assert(delta.length === 2)
|
||||||
|
t.assert(delta[0].insert !== 'text' && delta[0].insert.length === 4)
|
||||||
|
t.assert(object.length(delta[0].attributes) === 1)
|
||||||
|
t.assert(!object.hasProperty(delta[0].attributes, 'bold'))
|
||||||
|
t.assert(object.length(delta[1]) === 1)
|
||||||
|
t.assert(object.hasProperty(delta[1], 'insert'))
|
||||||
|
// test ymap
|
||||||
|
t.assert(omap.size === 1)
|
||||||
|
t.assert(!omap.has('key'))
|
||||||
|
// test yarray with subtype & subdoc
|
||||||
|
const result = oarray.toArray()
|
||||||
|
t.assert(result.length === 4)
|
||||||
|
t.assert(result[0] !== 'teststring')
|
||||||
|
t.assert(result[1] !== 42)
|
||||||
|
const osubtype = /** @type {Y.XmlElement} */ (result[2])
|
||||||
|
const osubdoc = result[3]
|
||||||
|
// test subtype
|
||||||
|
t.assert(osubtype.nodeName !== subtype.nodeName)
|
||||||
|
t.assert(object.length(osubtype.getAttributes()) === 1)
|
||||||
|
t.assert(osubtype.getAttribute('attr') === undefined)
|
||||||
|
// test subdoc
|
||||||
|
t.assert(osubdoc.guid !== subdoc.guid)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,64 +1,21 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
/* Basic Options */
|
"target": "ES2021",
|
||||||
"target": "es2018",
|
"lib": ["ES2021", "dom"],
|
||||||
"lib": ["es2018", "dom"], /* Specify library files to be included in the compilation. */
|
"module": "node16",
|
||||||
"allowJs": true, /* Allow javascript files to be compiled. */
|
"allowJs": true,
|
||||||
"checkJs": true, /* Report errors in .js files. */
|
"checkJs": true,
|
||||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
"declaration": true,
|
||||||
"declaration": true, /* Generates corresponding '.d.ts' file. */
|
"declarationMap": true,
|
||||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
"outDir": "./dist",
|
||||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
"baseUrl": "./",
|
||||||
// "outFile": "./dist/yjs.js", /* Concatenate and emit output to single file. */
|
|
||||||
"outDir": "./dist", /* Redirect output structure to the directory. */
|
|
||||||
"rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
|
||||||
// "composite": true, /* Enable project compilation */
|
|
||||||
// "removeComments": true, /* Do not emit comments to output. */
|
|
||||||
// "noEmit": true, /* Do not emit outputs. */
|
|
||||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
|
||||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
|
||||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
|
||||||
|
|
||||||
/* Strict Type-Checking Options */
|
|
||||||
"strict": true, /* Enable all strict type-checking options. */
|
|
||||||
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
|
||||||
"emitDeclarationOnly": true,
|
"emitDeclarationOnly": true,
|
||||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
"strict": true,
|
||||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
"noImplicitAny": true,
|
||||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
"moduleResolution": "nodenext",
|
||||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
|
||||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
|
||||||
|
|
||||||
/* Additional Checks */
|
|
||||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
|
||||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
|
||||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
|
||||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
|
||||||
|
|
||||||
/* Module Resolution Options */
|
|
||||||
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
|
||||||
"baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"yjs": ["./src/index.js"]
|
"yjs": ["./src/index.js"]
|
||||||
}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
}
|
||||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
|
||||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
|
||||||
// "types": [], /* Type declaration files to be included in compilation. */
|
|
||||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
|
||||||
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
|
||||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
|
||||||
|
|
||||||
/* Source Map Options */
|
|
||||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
|
||||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
|
||||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
|
||||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
|
||||||
|
|
||||||
/* Experimental Options */
|
|
||||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
|
||||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
|
||||||
// "maxNodeModuleJsDepth": 0,
|
|
||||||
// "types": ["./src/utils/typedefs.js"]
|
|
||||||
},
|
},
|
||||||
"include": ["./src/**/*.js", "./tests/**/*.js"]
|
"include": ["./src/**/*.js", "./tests/**/*.js"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user