Compare commits
125 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b792902f17 | ||
|
|
83b7c6839e | ||
|
|
65c4d40a87 | ||
|
|
942c8a267b | ||
|
|
eda085936a | ||
|
|
12be6c006a | ||
|
|
5d862477cd | ||
|
|
c398448152 | ||
|
|
2fbba13246 | ||
|
|
885a740470 | ||
|
|
aedd4c8bf3 | ||
|
|
9563612126 | ||
|
|
ce098d0ac2 | ||
|
|
08801dd406 | ||
|
|
3741f43a11 | ||
|
|
00ef472d68 | ||
|
|
719858201a | ||
|
|
5db1eed181 | ||
|
|
2e9a648d08 | ||
|
|
83712cb1a6 | ||
|
|
30b56d5ae9 | ||
|
|
61eeaef226 | ||
|
|
adaa95ebb8 | ||
|
|
1f2f08ef7e | ||
|
|
39167e6e2a | ||
|
|
5a8519d2c2 | ||
|
|
d039d48b3f | ||
|
|
710ac31af3 | ||
|
|
49f435284f | ||
|
|
ba96f2fe74 | ||
|
|
99bab4a1d8 | ||
|
|
1674d3986d | ||
|
|
dc3e99e6a1 | ||
|
|
fb6664a2bc | ||
|
|
0d7e865531 | ||
|
|
e73eb0bf92 | ||
|
|
d815855450 | ||
|
|
61ba6cdde1 | ||
|
|
cb70d7bad3 | ||
|
|
2001bec8eb | ||
|
|
2e2710ded9 | ||
|
|
227018f5c7 | ||
|
|
da8bacfc78 | ||
|
|
92bad63145 | ||
|
|
52ff230dd1 | ||
|
|
fe48efe64f | ||
|
|
7e40fc442d | ||
|
|
035e350062 | ||
|
|
bf338d8040 | ||
|
|
658c520b93 | ||
|
|
2576d4efca | ||
|
|
58b754950e | ||
|
|
ea7ad07f34 | ||
|
|
1c999b250e | ||
|
|
e9189365ee | ||
|
|
e0a2f11db3 | ||
|
|
7445a9ce5f | ||
|
|
7f6c12a541 | ||
|
|
370d0c138d | ||
|
|
d29de75f85 | ||
|
|
f215866429 | ||
|
|
093b41ccc4 | ||
|
|
ab60cd1ff8 | ||
|
|
1130abe05b | ||
|
|
31b4ab8d0c | ||
|
|
ab978b2003 | ||
|
|
afc6728c9e | ||
|
|
0ef5bd42fe | ||
|
|
3ece681758 | ||
|
|
cac9407185 | ||
|
|
7ea8ffebae | ||
|
|
d7751c16fd | ||
|
|
a64c51ec06 | ||
|
|
7405057037 | ||
|
|
6208b82872 | ||
|
|
12a9134b09 | ||
|
|
7395229086 | ||
|
|
8fb73edd97 | ||
|
|
f1ad5686c1 | ||
|
|
ed9236bdc7 | ||
|
|
5405fd2d7c | ||
|
|
12667f6b66 | ||
|
|
3d7ef7e28b | ||
|
|
56267e0a7d | ||
|
|
da71f6fa45 | ||
|
|
588788fbef | ||
|
|
fb9df6efe2 | ||
|
|
a69ecb0287 | ||
|
|
923fc6e06e | ||
|
|
0fdfd93e4b | ||
|
|
e0e5f8d2ea | ||
|
|
daf034cf75 | ||
|
|
2157ebb4d0 | ||
|
|
97ef4ae1e0 | ||
|
|
df2d59e2fb | ||
|
|
7a61c90261 | ||
|
|
6fa8778fc7 | ||
|
|
6b7b3136e0 | ||
|
|
da052bdb0a | ||
|
|
1bc9308566 | ||
|
|
a5e0448a92 | ||
|
|
c0c2b3347b | ||
|
|
6258ba1ce9 | ||
|
|
5a7ee74f68 | ||
|
|
29fb4a0aab | ||
|
|
8937494bdd | ||
|
|
4504196d5c | ||
|
|
0c8d29bfff | ||
|
|
43384e4148 | ||
|
|
a2b62b0a58 | ||
|
|
6febf51b1a | ||
|
|
5a4816a1b2 | ||
|
|
4ad8af9a80 | ||
|
|
fc25136b25 | ||
|
|
ece1fe5426 | ||
|
|
40196ae0a3 | ||
|
|
bdefe0526d | ||
|
|
dbbb86adc7 | ||
|
|
1c9c97ffe6 | ||
|
|
14c14de21e | ||
|
|
71fad52854 | ||
|
|
3935ba1faa | ||
|
|
4aacb487d2 | ||
|
|
5f56baa23e | ||
|
|
8d809ebacb |
@@ -1,7 +0,0 @@
|
||||
version: 2.1
|
||||
orbs:
|
||||
node: circleci/node@3.0.0
|
||||
workflows:
|
||||
node-tests:
|
||||
jobs:
|
||||
- node/test
|
||||
8
.github/workflows/node.js.yml
vendored
8
.github/workflows/node.js.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10.x, 12.x, 14.x]
|
||||
node-version: [16.x, 18.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -25,5 +25,7 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm ci
|
||||
- run: npm run build --if-present
|
||||
- run: npm test
|
||||
- run: npm run lint
|
||||
- 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
|
||||
@@ -66,7 +66,7 @@ fields, respectively. These are used when peers concurrently insert at the same
|
||||
location in a document. Though quite rare in practice, Yjs needs to make sure
|
||||
the list items always resolve to the same order on all peers. The actual logic
|
||||
is relatively simple - its only a couple dozen lines of code and it lives in
|
||||
the `Item#integrate()` method. The YATA paper has much more detail on the this
|
||||
the `Item#integrate()` method. The YATA paper has much more detail on this
|
||||
algorithm.
|
||||
|
||||
### Item Storage
|
||||
|
||||
133
README.md
133
README.md
@@ -15,27 +15,32 @@ suited for even large documents.
|
||||
|
||||
* Demos: [https://github.com/yjs/yjs-demos](https://github.com/yjs/yjs-demos)
|
||||
* Discuss: [https://discuss.yjs.dev](https://discuss.yjs.dev)
|
||||
* Chat: [Gitter](https://gitter.im/Yjs/community) | [Discord](https://discord.gg/T3nqMT6qbM)
|
||||
* Benchmark Yjs vs. Automerge:
|
||||
[https://github.com/dmonad/crdt-benchmarks](https://github.com/dmonad/crdt-benchmarks)
|
||||
* Podcast [**"Yjs Deep Dive into real time collaborative editing solutions":**](https://www.tag1consulting.com/blog/deep-dive-real-time-collaborative-editing-solutions-tagteamtalk-001-0)
|
||||
* Podcast [**"Google Docs-style editing in Gutenberg with the YJS framework":**](https://publishpress.com/blog/yjs/)
|
||||
|
||||
:construction_worker_woman: If you are looking for professional (paid) support to
|
||||
build collaborative or distributed applications ping us at
|
||||
<yjs@tag1consulting.com>. Otherwise you can find help on our
|
||||
[discussion board](https://discuss.yjs.dev).
|
||||
:construction_worker_woman: If you are looking for professional support, please
|
||||
consider supporting this project via a "support contract" on
|
||||
[GitHub Sponsors](https://github.com/sponsors/dmonad). I will attend your issues
|
||||
quicker and we can discuss questions and problems in regular video conferences.
|
||||
Otherwise you can find help on our community [discussion board](https://discuss.yjs.dev).
|
||||
|
||||
## Sponsorship
|
||||
|
||||
Please contribute to the project financially if your company relies on Yjs.
|
||||
Support the project and receive more attention on your tickets.
|
||||
[](https://github.com/sponsors/dmonad)
|
||||
Please contribute to the project financially - especially if your company relies
|
||||
on Yjs. [](https://github.com/sponsors/dmonad)
|
||||
|
||||
## Who is using Yjs
|
||||
|
||||
* [AFFiNE](https://affine.pro/) A local-first, privacy-first, open source
|
||||
knowledge base. 🏅
|
||||
* [Dynaboard](https://dynaboard.com/) Build web apps collaboratively. :star2:
|
||||
* [Sana](https://sanalabs.com/) A learning platform with collaborative text
|
||||
editing powered by Yjs.
|
||||
* [Relm](https://www.relm.us/) A collaborative gameworld for teamwork and
|
||||
community. :star2:
|
||||
community. :star:
|
||||
* [Room.sh](https://room.sh/) A meeting application with integrated
|
||||
collaborative drawing, editing, and coding tools. :star:
|
||||
* [Nimbus Note](https://nimbusweb.me/note.php) A note-taking app designed by
|
||||
@@ -48,7 +53,13 @@ Support the project and receive more attention on your tickets.
|
||||
* [Alldone](https://alldone.app/) A next-gen project management and
|
||||
collaboration platform.
|
||||
* [Living Spec](https://livingspec.com/) A modern way for product teams to collaborate.
|
||||
* [Slidebeamer](https://slidebeamer.com/) Presentation app.
|
||||
* [BlockSurvey](https://blocksurvey.io) End-to-end encryption for your forms/surveys.
|
||||
* [Skiff](https://skiff.org/) Private, decentralized workspace.
|
||||
* [Hyperquery](https://hyperquery.ai/) A collaborative data workspace for
|
||||
sharing analyses, documentation, spreadsheets, and dashboards.
|
||||
* [Nosgestesclimat](https://nosgestesclimat.fr/groupe) The french carbon
|
||||
footprint calculator has a group P2P mode based on yjs
|
||||
|
||||
## Table of Contents
|
||||
|
||||
@@ -80,6 +91,7 @@ are implemented in separate modules.
|
||||
| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](https://github.com/yjs/y-codemirror) | [demo](https://demos.yjs.dev/codemirror/codemirror.html) |
|
||||
| [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](https://github.com/yjs/y-monaco) | [demo](https://demos.yjs.dev/monaco/monaco.html) |
|
||||
| [Slate](https://github.com/ianstormtaylor/slate) | ✔ | [slate-yjs](https://github.com/bitphinix/slate-yjs) | [demo](https://bitphinix.github.io/slate-yjs-example) |
|
||||
| [BlockSuite](https://github.com/toeverything/blocksuite) | ✔ | (native) | [demo](https://blocksuite-toeverything.vercel.app/?init) |
|
||||
| [valtio](https://github.com/pmndrs/valtio) | | [valtio-yjs](https://github.com/dai-shi/valtio-yjs) | [demo](https://codesandbox.io/s/valtio-yjs-demo-ox3iy) |
|
||||
| [immer](https://github.com/immerjs/immer) | | [immer-yjs](https://github.com/sep2/immer-yjs) | [demo](https://codesandbox.io/s/immer-yjs-demo-6e0znb) |
|
||||
| React / Vue / Svelte / MobX | | [SyncedStore](https://syncedstore.org) | [demo](https://syncedstore.org/docs/react) |
|
||||
@@ -133,6 +145,11 @@ Use Matrix as transport and storage of Yjs updates, so you can focus building
|
||||
your client app and Matrix can provide powerful features like Authentication,
|
||||
Authorization, Federation, hosting (self-hosting or SaaS) and even End-to-End
|
||||
Encryption (E2EE).
|
||||
</dd>
|
||||
<dt><a href="https://github.com/MaxNoetzold/y-mongodb-provider">y-mongodb-provider</a></dt>
|
||||
<dd>
|
||||
Adds persistent storage to a server with MongoDB. Can be used with the
|
||||
y-websocket provider.
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
@@ -153,6 +170,9 @@ PORT=1234 node ./node_modules/y-websocket/bin/server.js
|
||||
### Example: Observe types
|
||||
|
||||
```js
|
||||
import * as Y from 'yjs';
|
||||
|
||||
const doc = new Y.Doc();
|
||||
const yarray = doc.getArray('my-array')
|
||||
yarray.observe(event => {
|
||||
console.log('yarray was modified')
|
||||
@@ -626,6 +646,8 @@ type. Doesn't log types that have not been defined (using
|
||||
<dd>Define a shared Y.Array type. Is equivalent to <code>y.get(string, Y.Array)</code>.</dd>
|
||||
<b><code>getMap(string):Y.Map</code></b>
|
||||
<dd>Define a shared Y.Map type. Is equivalent to <code>y.get(string, Y.Map)</code>.</dd>
|
||||
<b><code>getText(string):Y.Text</code></b>
|
||||
<dd>Define a shared Y.Text type. Is equivalent to <code>y.get(string, Y.Text)</code>.</dd>
|
||||
<b><code>getXmlFragment(string):Y.XmlFragment</code></b>
|
||||
<dd>Define a shared Y.XmlFragment type. Is equivalent to <code>y.get(string, Y.XmlFragment)</code>.</dd>
|
||||
<b><code>on(string, function)</code></b>
|
||||
@@ -735,6 +757,30 @@ currentState1 = Y.mergeUpdates([currentState1, diff2])
|
||||
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
|
||||
|
||||
Yjs implements two update formats. By default you are using the V1 update format.
|
||||
@@ -790,6 +836,21 @@ Convert V2 update format to the V1 update format.
|
||||
|
||||
### Relative Positions
|
||||
|
||||
When working with collaborative documents, we often need to work with positions.
|
||||
Positions may represent cursor locations, selection ranges, or even assign a
|
||||
comment to a range of text. Normal index-positions (expressed as integers) are
|
||||
not convenient to use because the index-range is invalidated as soon as a remote
|
||||
change manipulates the document. Relative positions give you a powerful API to
|
||||
express positions.
|
||||
|
||||
A relative position is fixated to an element in the shared document and is not
|
||||
affected by remote changes. I.e. given the document `"a|c"`, the relative
|
||||
position is attached to `c`. When a remote user modifies the document by
|
||||
inserting a character before the cursor, the cursor will stay attached to the
|
||||
character `c`. `insert(1, 'x')("a|c") = "ax|c"`. When the relative position is
|
||||
set to the end of the document, it will stay attached to the end of the
|
||||
document.
|
||||
|
||||
#### Example: Transform to RelativePosition and back
|
||||
|
||||
```js
|
||||
@@ -824,14 +885,35 @@ pos.index === 2 // => true
|
||||
```
|
||||
|
||||
<dl>
|
||||
<b><code>Y.createRelativePositionFromTypeIndex(Uint8Array|Y.Type, number)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>Y.createAbsolutePositionFromRelativePosition(RelativePosition, Y.Doc)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>Y.encodeRelativePosition(RelativePosition):Uint8Array</code></b>
|
||||
<dd></dd>
|
||||
<b><code>
|
||||
Y.createRelativePositionFromTypeIndex(type:Uint8Array|Y.Type, index: number
|
||||
[, assoc=0])
|
||||
</code></b>
|
||||
<dd>
|
||||
Create a relative position fixated to the i-th element in any sequence-like
|
||||
shared type (if <code>assoc >= 0</code>). By default, the position associates
|
||||
with the character that comes after the specified index position. If
|
||||
<code>assoc < 0</code>, then the relative position associates with the character
|
||||
before the specified index position.
|
||||
</dd>
|
||||
<b><code>
|
||||
Y.createAbsolutePositionFromRelativePosition(RelativePosition, Y.Doc):
|
||||
{ type: Y.AbstractType, index: number, assoc: number } | null
|
||||
</code></b>
|
||||
<dd>
|
||||
Create an absolute position from a relative position. If the relative position
|
||||
cannot be referenced, or the type is deleted, then the result is null.
|
||||
</dd>
|
||||
<b><code>
|
||||
Y.encodeRelativePosition(RelativePosition):Uint8Array
|
||||
</code></b>
|
||||
<dd>
|
||||
Encode a relative position to an Uint8Array. Binary data is the preferred
|
||||
encoding format for document updates. If you prefer JSON encoding, you can
|
||||
simply JSON.stringify / JSON.parse the relative position instead.
|
||||
</dd>
|
||||
<b><code>Y.decodeRelativePosition(Uint8Array):RelativePosition</code></b>
|
||||
<dd></dd>
|
||||
<dd>Decode a binary-encoded relative position to a RelativePositon object.</dd>
|
||||
</dl>
|
||||
|
||||
### Y.UndoManager
|
||||
@@ -1009,16 +1091,17 @@ More information about the specific implementation is available in
|
||||
[INTERNALS.md](./INTERNALS.md) and in
|
||||
[this walkthrough of the Yjs codebase](https://youtu.be/0l5XgnQ6rB4).
|
||||
|
||||
CRDTs that suitable for shared text editing suffer from the fact that they only grow
|
||||
in size. There are CRDTs that do not grow in size, but they do not have the
|
||||
characteristics that are benificial for shared text editing (like intention
|
||||
preservation). Yjs implements many improvements to the original algorithm that
|
||||
diminish the trade-off that the document only grows in size. We can't garbage
|
||||
collect deleted structs (tombstones) while ensuring a unique order of the
|
||||
structs. But we can 1. merge preceeding structs into a single struct to reduce
|
||||
the amount of meta information, 2. we can delete content from the struct if it
|
||||
is deleted, and 3. we can garbage collect tombstones if we don't care about the
|
||||
order of the structs anymore (e.g. if the parent was deleted).
|
||||
CRDTs that are suitable for shared text editing suffer from the fact that they
|
||||
only grow in size. There are CRDTs that do not grow in size, but they do not
|
||||
have the characteristics that are benificial for shared text editing (like
|
||||
intention preservation). Yjs implements many improvements to the original
|
||||
algorithm that diminish the trade-off that the document only grows in size. We
|
||||
can't garbage collect deleted structs (tombstones) while ensuring a unique
|
||||
order of the structs. But we can 1. merge preceeding structs into a single
|
||||
struct to reduce the amount of meta information, 2. we can delete content from
|
||||
the struct if it is deleted, and 3. we can garbage collect tombstones if we
|
||||
don't care about the order of the structs anymore (e.g. if the parent was
|
||||
deleted).
|
||||
|
||||
**Examples:**
|
||||
|
||||
|
||||
4105
package-lock.json
generated
4105
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "13.5.34",
|
||||
"version": "13.6.5",
|
||||
"description": "Shared Editing Library",
|
||||
"main": "./dist/yjs.cjs",
|
||||
"module": "./dist/yjs.mjs",
|
||||
@@ -26,6 +26,8 @@
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/src/index.d.ts",
|
||||
"module": "./dist/yjs.mjs",
|
||||
"import": "./dist/yjs.mjs",
|
||||
"require": "./dist/yjs.cjs"
|
||||
},
|
||||
@@ -73,19 +75,24 @@
|
||||
},
|
||||
"homepage": "https://docs.yjs.dev",
|
||||
"dependencies": {
|
||||
"lib0": "^0.2.49"
|
||||
"lib0": "^0.2.74"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^17.0.0",
|
||||
"@rollup/plugin-node-resolve": "^11.2.1",
|
||||
"@rollup/plugin-commonjs": "^24.0.1",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"@types/node": "^18.15.5",
|
||||
"concurrently": "^3.6.1",
|
||||
"http-server": "^0.12.3",
|
||||
"jsdoc": "^3.6.7",
|
||||
"markdownlint-cli": "^0.23.2",
|
||||
"rollup": "^2.60.0",
|
||||
"rollup": "^3.20.0",
|
||||
"standard": "^16.0.4",
|
||||
"tui-jsdoc-template": "^1.2.2",
|
||||
"typescript": "^4.4.4",
|
||||
"typescript": "^4.9.5",
|
||||
"y-protocols": "^1.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=8.0.0",
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,13 +42,7 @@ export default [{
|
||||
name: 'Y',
|
||||
file: 'dist/yjs.cjs',
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
paths: path => {
|
||||
if (/^lib0\//.test(path)) {
|
||||
return `lib0/dist/${path.slice(5)}.cjs`
|
||||
}
|
||||
return path
|
||||
}
|
||||
sourcemap: true
|
||||
},
|
||||
external: id => /^lib0\//.test(id)
|
||||
}, {
|
||||
@@ -88,7 +82,7 @@ export default [{
|
||||
plugins: [
|
||||
debugResolve,
|
||||
nodeResolve({
|
||||
mainFields: ['module', 'browser', 'main']
|
||||
mainFields: ['browser', 'module', 'main']
|
||||
}),
|
||||
commonjs()
|
||||
]
|
||||
@@ -103,9 +97,10 @@ export default [{
|
||||
plugins: [
|
||||
debugResolve,
|
||||
nodeResolve({
|
||||
mainFields: ['module', 'main']
|
||||
mainFields: ['node', 'module', 'main'],
|
||||
exportConditions: ['node', 'module', 'import', 'default']
|
||||
}),
|
||||
commonjs()
|
||||
],
|
||||
external: ['isomorphic.js']
|
||||
external: id => /^lib0\//.test(id)
|
||||
}]
|
||||
|
||||
25
src/index.js
25
src/index.js
@@ -89,21 +89,28 @@ export {
|
||||
diffUpdate,
|
||||
diffUpdateV2,
|
||||
convertUpdateFormatV1ToV2,
|
||||
convertUpdateFormatV2ToV1
|
||||
convertUpdateFormatV2ToV1,
|
||||
obfuscateUpdate,
|
||||
obfuscateUpdateV2,
|
||||
UpdateEncoderV1,
|
||||
equalDeleteSets,
|
||||
snapshotContainsUpdate
|
||||
} from './internals.js'
|
||||
|
||||
const glo = /** @type {any} */ (typeof window !== 'undefined'
|
||||
? window
|
||||
// @ts-ignore
|
||||
: typeof global !== 'undefined' ? global : {})
|
||||
const glo = /** @type {any} */ (typeof globalThis !== 'undefined'
|
||||
? globalThis
|
||||
: typeof window !== 'undefined'
|
||||
? window
|
||||
// @ts-ignore
|
||||
: typeof global !== 'undefined' ? global : {})
|
||||
|
||||
const importIdentifier = '__ $YJS$ __'
|
||||
|
||||
if (glo[importIdentifier] === true) {
|
||||
/**
|
||||
* Dear reader of this warning message. Please take this seriously.
|
||||
* Dear reader of this message. Please take this seriously.
|
||||
*
|
||||
* If you see this message, please make sure that you only import one version of Yjs. In many cases,
|
||||
* If you see this message, make sure that you only import one version of Yjs. In many cases,
|
||||
* your package manager installs two versions of Yjs that are used by different packages within your project.
|
||||
* Another reason for this message is that some parts of your project use the commonjs version of Yjs
|
||||
* and others use the EcmaScript version of Yjs.
|
||||
@@ -111,7 +118,9 @@ if (glo[importIdentifier] === true) {
|
||||
* This often leads to issues that are hard to debug. We often need to perform constructor checks,
|
||||
* e.g. `struct instanceof GC`. If you imported different versions of Yjs, it is impossible for us to
|
||||
* do the constructor checks anymore - which might break the CRDT algorithm.
|
||||
*
|
||||
* https://github.com/yjs/yjs/issues/438
|
||||
*/
|
||||
console.warn('Yjs was already imported. Importing different versions of Yjs often leads to issues.')
|
||||
console.error('Yjs was already imported. This breaks constructor checks and will lead to issues! - https://github.com/yjs/yjs/issues/438')
|
||||
}
|
||||
glo[importIdentifier] = true
|
||||
|
||||
@@ -23,11 +23,12 @@ import {
|
||||
readContentType,
|
||||
addChangedTypeToTransaction,
|
||||
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'
|
||||
|
||||
import * as error from 'lib0/error'
|
||||
import * as binary from 'lib0/binary'
|
||||
import * as array from 'lib0/array'
|
||||
|
||||
/**
|
||||
* @todo This should return several items
|
||||
@@ -120,6 +121,12 @@ export const splitItem = (transaction, leftItem, diff) => {
|
||||
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.
|
||||
*
|
||||
@@ -128,12 +135,13 @@ export const splitItem = (transaction, leftItem, diff) => {
|
||||
* @param {Set<Item>} redoitems
|
||||
* @param {DeleteSet} itemsToDelete
|
||||
* @param {boolean} ignoreRemoteMapChanges
|
||||
* @param {import('../utils/UndoManager.js').UndoManager} um
|
||||
*
|
||||
* @return {Item|null}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemoteMapChanges) => {
|
||||
export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemoteMapChanges, um) => {
|
||||
const doc = transaction.doc
|
||||
const store = doc.store
|
||||
const ownClientID = doc.clientID
|
||||
@@ -153,7 +161,7 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemo
|
||||
// make sure that parent is redone
|
||||
if (parentItem !== null && parentItem.deleted === true) {
|
||||
// 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
|
||||
}
|
||||
while (parentItem.redone !== null) {
|
||||
@@ -203,18 +211,10 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemo
|
||||
left = item
|
||||
// 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.
|
||||
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
|
||||
}
|
||||
// follow redone
|
||||
// trace redone until parent matches
|
||||
while (left !== null && left.redone !== null) {
|
||||
left = getItemCleanStart(transaction, left.redone)
|
||||
}
|
||||
// check wether we were allowed to follow right (indicating that originally this op was replaced by another item)
|
||||
if (left === null || /** @type {AbstractType<any>} */ (left.parent)._item !== parentItem) {
|
||||
// invalid parent; should never happen
|
||||
return null
|
||||
// follow redone
|
||||
while (left.redone) left = getItemCleanStart(transaction, left.redone)
|
||||
}
|
||||
if (left && left.right !== null) {
|
||||
// It is not possible to redo this item because it conflicts with a
|
||||
@@ -761,48 +761,48 @@ export class AbstractContent {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @param {number} _offset
|
||||
* @return {AbstractContent}
|
||||
*/
|
||||
splice (offset) {
|
||||
splice (_offset) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractContent} right
|
||||
* @param {AbstractContent} _right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
mergeWith (_right) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} item
|
||||
* @param {Transaction} _transaction
|
||||
* @param {Item} _item
|
||||
*/
|
||||
integrate (transaction, item) {
|
||||
integrate (_transaction, _item) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Transaction} _transaction
|
||||
*/
|
||||
delete (transaction) {
|
||||
delete (_transaction) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {StructStore} store
|
||||
* @param {StructStore} _store
|
||||
*/
|
||||
gc (store) {
|
||||
gc (_store) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {number} offset
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} _encoder
|
||||
* @param {number} _offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
write (_encoder, _offset) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
|
||||
@@ -324,9 +324,9 @@ export class AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} _encoder
|
||||
*/
|
||||
_write (encoder) { }
|
||||
_write (_encoder) { }
|
||||
|
||||
/**
|
||||
* The first non-deleted item
|
||||
@@ -344,9 +344,9 @@ export class AbstractType {
|
||||
* Must be implemented by each type.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
|
||||
* @param {Set<null|string>} _parentSubs Keys changed on this type. `null` if list was modified.
|
||||
*/
|
||||
_callObserver (transaction, parentSubs) {
|
||||
_callObserver (transaction, _parentSubs) {
|
||||
if (!transaction.local && this._searchMarker) {
|
||||
this._searchMarker.length = 0
|
||||
}
|
||||
|
||||
@@ -58,11 +58,14 @@ export class YArray extends AbstractType {
|
||||
|
||||
/**
|
||||
* Construct a new YArray containing the specified items.
|
||||
* @template T
|
||||
* @template {Object<string,any>|Array<any>|number|null|string|Uint8Array} T
|
||||
* @param {Array<T>} items
|
||||
* @return {YArray<T>}
|
||||
*/
|
||||
static from (items) {
|
||||
/**
|
||||
* @type {YArray<T>}
|
||||
*/
|
||||
const a = new YArray()
|
||||
a.push(items)
|
||||
return a
|
||||
@@ -84,6 +87,9 @@ export class YArray extends AbstractType {
|
||||
this._prelimContent = null
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {YArray<T>}
|
||||
*/
|
||||
_copy () {
|
||||
return new YArray()
|
||||
}
|
||||
@@ -92,9 +98,12 @@ export class YArray extends AbstractType {
|
||||
* @return {YArray<T>}
|
||||
*/
|
||||
clone () {
|
||||
/**
|
||||
* @type {YArray<T>}
|
||||
*/
|
||||
const arr = new YArray()
|
||||
arr.insert(0, this.toArray().map(el =>
|
||||
el instanceof AbstractType ? el.clone() : el
|
||||
el instanceof AbstractType ? /** @type {typeof el} */ (el.clone()) : el
|
||||
))
|
||||
return arr
|
||||
}
|
||||
@@ -133,7 +142,7 @@ export class YArray extends AbstractType {
|
||||
insert (index, content) {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeListInsertGenerics(transaction, this, index, content)
|
||||
typeListInsertGenerics(transaction, this, index, /** @type {any} */ (content))
|
||||
})
|
||||
} else {
|
||||
/** @type {Array<any>} */ (this._prelimContent).splice(index, 0, ...content)
|
||||
@@ -150,7 +159,7 @@ export class YArray extends AbstractType {
|
||||
push (content) {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeListPushGenerics(transaction, this, content)
|
||||
typeListPushGenerics(transaction, this, /** @type {any} */ (content))
|
||||
})
|
||||
} else {
|
||||
/** @type {Array<any>} */ (this._prelimContent).push(...content)
|
||||
@@ -235,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.
|
||||
*/
|
||||
@@ -259,9 +268,9 @@ export class YArray extends AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const readYArray = decoder => new YArray()
|
||||
export const readYArray = _decoder => new YArray()
|
||||
|
||||
@@ -81,6 +81,9 @@ export class YMap extends AbstractType {
|
||||
this._prelimContent = null
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {YMap<MapType>}
|
||||
*/
|
||||
_copy () {
|
||||
return new YMap()
|
||||
}
|
||||
@@ -89,9 +92,12 @@ export class YMap extends AbstractType {
|
||||
* @return {YMap<MapType>}
|
||||
*/
|
||||
clone () {
|
||||
/**
|
||||
* @type {YMap<MapType>}
|
||||
*/
|
||||
const map = new YMap()
|
||||
this.forEach((value, key) => {
|
||||
map.set(key, value instanceof AbstractType ? value.clone() : value)
|
||||
map.set(key, value instanceof AbstractType ? /** @type {typeof value} */ (value.clone()) : value)
|
||||
})
|
||||
return map
|
||||
}
|
||||
@@ -167,20 +173,17 @@ export class YMap extends AbstractType {
|
||||
* @param {function(MapType,string,YMap<MapType>):void} f A function to execute on every element of this YArray.
|
||||
*/
|
||||
forEach (f) {
|
||||
/**
|
||||
* @type {Object<string,MapType>}
|
||||
*/
|
||||
const map = {}
|
||||
this._map.forEach((item, key) => {
|
||||
if (!item.deleted) {
|
||||
f(item.content.getContent()[item.length - 1], key, this)
|
||||
}
|
||||
})
|
||||
return map
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {IterableIterator<MapType>}
|
||||
* Returns an Iterator of [key, value] pairs
|
||||
*
|
||||
* @return {IterableIterator<any>}
|
||||
*/
|
||||
[Symbol.iterator] () {
|
||||
return this.entries()
|
||||
@@ -203,14 +206,16 @@ export class YMap extends AbstractType {
|
||||
|
||||
/**
|
||||
* Adds or updates an element with a specified key and value.
|
||||
* @template {MapType} VAL
|
||||
*
|
||||
* @param {string} key The key of the element to add to this YMap
|
||||
* @param {MapType} value The value of the element to add
|
||||
* @param {VAL} value The value of the element to add
|
||||
* @return {VAL}
|
||||
*/
|
||||
set (key, value) {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeMapSet(transaction, this, key, value)
|
||||
typeMapSet(transaction, this, key, /** @type {any} */ (value))
|
||||
})
|
||||
} else {
|
||||
/** @type {Map<string, any>} */ (this._prelimContent).set(key, value)
|
||||
@@ -244,7 +249,7 @@ export class YMap extends AbstractType {
|
||||
clear () {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
this.forEach(function (value, key, map) {
|
||||
this.forEach(function (_value, key, map) {
|
||||
typeMapDelete(transaction, map, key)
|
||||
})
|
||||
})
|
||||
@@ -262,9 +267,9 @@ export class YMap extends AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const readYMap = decoder => new YMap()
|
||||
export const readYMap = _decoder => new YMap()
|
||||
|
||||
@@ -251,7 +251,7 @@ const insertAttributes = (transaction, parent, currPos, attributes) => {
|
||||
* @function
|
||||
**/
|
||||
const insertText = (transaction, parent, currPos, text, attributes) => {
|
||||
currPos.currentAttributes.forEach((val, key) => {
|
||||
currPos.currentAttributes.forEach((_val, key) => {
|
||||
if (attributes[key] === undefined) {
|
||||
attributes[key] = null
|
||||
}
|
||||
@@ -292,7 +292,16 @@ const formatText = (transaction, parent, currPos, length, attributes) => {
|
||||
// iterate until first non-format or null is found
|
||||
// delete all formats with attributes[format.key] != null
|
||||
// also check the attributes after the first non-format as we do not want to insert redundant negated attributes there
|
||||
while (currPos.right !== null && (length > 0 || currPos.right.content.constructor === ContentFormat)) {
|
||||
// eslint-disable-next-line no-labels
|
||||
iterationLoop: while (
|
||||
currPos.right !== null &&
|
||||
(length > 0 ||
|
||||
(
|
||||
negatedAttributes.size > 0 &&
|
||||
(currPos.right.deleted || currPos.right.content.constructor === ContentFormat)
|
||||
)
|
||||
)
|
||||
) {
|
||||
if (!currPos.right.deleted) {
|
||||
switch (currPos.right.content.constructor) {
|
||||
case ContentFormat: {
|
||||
@@ -302,9 +311,16 @@ const formatText = (transaction, parent, currPos, length, attributes) => {
|
||||
if (equalAttrs(attr, value)) {
|
||||
negatedAttributes.delete(key)
|
||||
} else {
|
||||
if (length === 0) {
|
||||
// no need to further extend negatedAttributes
|
||||
// eslint-disable-next-line no-labels
|
||||
break iterationLoop
|
||||
}
|
||||
negatedAttributes.set(key, value)
|
||||
}
|
||||
currPos.right.delete(transaction)
|
||||
} else {
|
||||
currPos.currentAttributes.set(key, value)
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -347,33 +363,48 @@ const formatText = (transaction, parent, currPos, length, attributes) => {
|
||||
* @function
|
||||
*/
|
||||
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)) {
|
||||
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
|
||||
}
|
||||
let cleanups = 0
|
||||
let reachedEndOfCurr = false
|
||||
let reachedCurr = false
|
||||
while (start !== end) {
|
||||
if (curr === start) {
|
||||
reachedEndOfCurr = true
|
||||
reachedCurr = true
|
||||
}
|
||||
if (!start.deleted) {
|
||||
const content = start.content
|
||||
switch (content.constructor) {
|
||||
case ContentFormat: {
|
||||
const { key, value } = /** @type {ContentFormat} */ (content)
|
||||
if ((endAttributes.get(key) || null) !== value || (startAttributes.get(key) || null) === value) {
|
||||
const startAttrValue = startAttributes.get(key) || null
|
||||
if (endFormats.get(key) !== content || startAttrValue === value) {
|
||||
// Either this format is overwritten or it is not necessary because the attribute already existed.
|
||||
start.delete(transaction)
|
||||
cleanups++
|
||||
if (!reachedEndOfCurr && (currAttributes.get(key) || null) === value && (startAttributes.get(key) || null) !== value) {
|
||||
currAttributes.delete(key)
|
||||
if (!reachedCurr && (currAttributes.get(key) || null) === value && startAttrValue !== value) {
|
||||
if (startAttrValue === null) {
|
||||
currAttributes.delete(key)
|
||||
} else {
|
||||
currAttributes.set(key, startAttrValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!reachedCurr && !start.deleted) {
|
||||
updateCurrentAttributes(currAttributes, /** @type {ContentFormat} */ (content))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -445,6 +476,56 @@ export const cleanupYTextFormatting = type => {
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* This will be called by the transction once the event handlers are called to potentially cleanup
|
||||
* formatting attributes.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
export const cleanupYTextAfterTransaction = transaction => {
|
||||
/**
|
||||
* @type {Set<YText>}
|
||||
*/
|
||||
const needFullCleanup = new Set()
|
||||
// check if another formatting item was inserted
|
||||
const doc = transaction.doc
|
||||
for (const [client, afterClock] of transaction.afterState.entries()) {
|
||||
const clock = transaction.beforeState.get(client) || 0
|
||||
if (afterClock === clock) {
|
||||
continue
|
||||
}
|
||||
iterateStructs(transaction, /** @type {Array<Item|GC>} */ (doc.store.clients.get(client)), clock, afterClock, item => {
|
||||
if (
|
||||
!item.deleted && /** @type {Item} */ (item).content.constructor === ContentFormat && item.constructor !== GC
|
||||
) {
|
||||
needFullCleanup.add(/** @type {any} */ (item).parent)
|
||||
}
|
||||
})
|
||||
}
|
||||
// cleanup in a new transaction
|
||||
transact(doc, (t) => {
|
||||
iterateDeletedStructs(transaction, transaction.deleteSet, item => {
|
||||
if (item instanceof GC || needFullCleanup.has(/** @type {YText} */ (item.parent))) {
|
||||
return
|
||||
}
|
||||
const parent = /** @type {YText} */ (item.parent)
|
||||
if (item.content.constructor === ContentFormat) {
|
||||
needFullCleanup.add(parent)
|
||||
} else {
|
||||
// If no formatting attribute was inserted or deleted, we can make due with contextless
|
||||
// formatting cleanups.
|
||||
// Contextless: it is not necessary to compute currentAttributes for the affected position.
|
||||
cleanupContextlessFormattingGap(t, item)
|
||||
}
|
||||
})
|
||||
// If a formatting item was inserted, we simply clean the whole type.
|
||||
// We need to compute currentAttributes for the current position anyway.
|
||||
for (const yText of needFullCleanup) {
|
||||
cleanupYTextFormatting(yText)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {ItemTextListPosition} currPos
|
||||
@@ -600,36 +681,39 @@ export class YTextEvent extends YEvent {
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
let op
|
||||
let op = null
|
||||
switch (action) {
|
||||
case 'delete':
|
||||
op = { delete: deleteLen }
|
||||
if (deleteLen > 0) {
|
||||
op = { delete: deleteLen }
|
||||
}
|
||||
deleteLen = 0
|
||||
break
|
||||
case 'insert':
|
||||
op = { insert }
|
||||
if (currentAttributes.size > 0) {
|
||||
op.attributes = {}
|
||||
currentAttributes.forEach((value, key) => {
|
||||
if (value !== null) {
|
||||
op.attributes[key] = value
|
||||
}
|
||||
})
|
||||
if (typeof insert === 'object' || insert.length > 0) {
|
||||
op = { insert }
|
||||
if (currentAttributes.size > 0) {
|
||||
op.attributes = {}
|
||||
currentAttributes.forEach((value, key) => {
|
||||
if (value !== null) {
|
||||
op.attributes[key] = value
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
insert = ''
|
||||
break
|
||||
case 'retain':
|
||||
op = { retain }
|
||||
if (Object.keys(attributes).length > 0) {
|
||||
op.attributes = {}
|
||||
for (const key in attributes) {
|
||||
op.attributes[key] = attributes[key]
|
||||
if (retain > 0) {
|
||||
op = { retain }
|
||||
if (!object.isEmpty(attributes)) {
|
||||
op.attributes = object.assign({}, attributes)
|
||||
}
|
||||
}
|
||||
retain = 0
|
||||
break
|
||||
}
|
||||
delta.push(op)
|
||||
if (op) delta.push(op)
|
||||
action = null
|
||||
}
|
||||
}
|
||||
@@ -825,55 +909,10 @@ export class YText extends AbstractType {
|
||||
_callObserver (transaction, parentSubs) {
|
||||
super._callObserver(transaction, parentSubs)
|
||||
const event = new YTextEvent(this, transaction, parentSubs)
|
||||
const doc = transaction.doc
|
||||
callTypeObservers(this, transaction, event)
|
||||
// If a remote change happened, we try to cleanup potential formatting duplicates.
|
||||
if (!transaction.local) {
|
||||
// check if another formatting item was inserted
|
||||
let foundFormattingItem = false
|
||||
for (const [client, afterClock] of transaction.afterState.entries()) {
|
||||
const clock = transaction.beforeState.get(client) || 0
|
||||
if (afterClock === clock) {
|
||||
continue
|
||||
}
|
||||
iterateStructs(transaction, /** @type {Array<Item|GC>} */ (doc.store.clients.get(client)), clock, afterClock, item => {
|
||||
if (!item.deleted && /** @type {Item} */ (item).content.constructor === ContentFormat) {
|
||||
foundFormattingItem = true
|
||||
}
|
||||
})
|
||||
if (foundFormattingItem) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!foundFormattingItem) {
|
||||
iterateDeletedStructs(transaction, transaction.deleteSet, item => {
|
||||
if (item instanceof GC || foundFormattingItem) {
|
||||
return
|
||||
}
|
||||
if (item.parent === this && item.content.constructor === ContentFormat) {
|
||||
foundFormattingItem = true
|
||||
}
|
||||
})
|
||||
}
|
||||
transact(doc, (t) => {
|
||||
if (foundFormattingItem) {
|
||||
// If a formatting item was inserted, we simply clean the whole type.
|
||||
// We need to compute currentAttributes for the current position anyway.
|
||||
cleanupYTextFormatting(this)
|
||||
} else {
|
||||
// If no formatting attribute was inserted, we can make due with contextless
|
||||
// formatting cleanups.
|
||||
// Contextless: it is not necessary to compute currentAttributes for the affected position.
|
||||
iterateDeletedStructs(t, t.deleteSet, item => {
|
||||
if (item instanceof GC) {
|
||||
return
|
||||
}
|
||||
if (item.parent === this) {
|
||||
cleanupContextlessFormattingGap(t, item)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
transaction._needFormattingCleanup = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -911,7 +950,7 @@ export class YText extends AbstractType {
|
||||
* Apply a {@link Delta} on this shared YText type.
|
||||
*
|
||||
* @param {any} delta The changes to apply on this element.
|
||||
* @param {object} [opts]
|
||||
* @param {object} opts
|
||||
* @param {boolean} [opts.sanitize] Sanitize input delta. Removes ending newlines if set to true.
|
||||
*
|
||||
*
|
||||
@@ -987,27 +1026,19 @@ export class YText extends AbstractType {
|
||||
str = ''
|
||||
}
|
||||
}
|
||||
// snapshots are merged again after the transaction, so we need to keep the
|
||||
// transalive until we are done
|
||||
transact(doc, transaction => {
|
||||
if (snapshot) {
|
||||
splitSnapshotAffectedStructs(transaction, snapshot)
|
||||
}
|
||||
if (prevSnapshot) {
|
||||
splitSnapshotAffectedStructs(transaction, prevSnapshot)
|
||||
}
|
||||
const computeDelta = () => {
|
||||
while (n !== null) {
|
||||
if (isVisible(n, snapshot) || (prevSnapshot !== undefined && isVisible(n, prevSnapshot))) {
|
||||
switch (n.content.constructor) {
|
||||
case ContentString: {
|
||||
const cur = currentAttributes.get('ychange')
|
||||
if (snapshot !== undefined && !isVisible(n, snapshot)) {
|
||||
if (cur === undefined || cur.user !== n.id.client || cur.state !== 'removed') {
|
||||
if (cur === undefined || cur.user !== n.id.client || cur.type !== 'removed') {
|
||||
packStr()
|
||||
currentAttributes.set('ychange', computeYChange ? computeYChange('removed', n.id) : { type: 'removed' })
|
||||
}
|
||||
} else if (prevSnapshot !== undefined && !isVisible(n, prevSnapshot)) {
|
||||
if (cur === undefined || cur.user !== n.id.client || cur.state !== 'added') {
|
||||
if (cur === undefined || cur.user !== n.id.client || cur.type !== 'added') {
|
||||
packStr()
|
||||
currentAttributes.set('ychange', computeYChange ? computeYChange('added', n.id) : { type: 'added' })
|
||||
}
|
||||
@@ -1048,7 +1079,22 @@ export class YText extends AbstractType {
|
||||
n = n.right
|
||||
}
|
||||
packStr()
|
||||
}, splitSnapshotAffectedStructs)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1213,12 +1259,11 @@ export class YText extends AbstractType {
|
||||
*
|
||||
* @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks.
|
||||
*
|
||||
* @param {Snapshot} [snapshot]
|
||||
* @return {Object<string, any>} A JSON Object that describes the attributes.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
getAttributes (snapshot) {
|
||||
getAttributes () {
|
||||
return typeMapGetAll(this)
|
||||
}
|
||||
|
||||
@@ -1231,10 +1276,10 @@ export class YText extends AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
|
||||
* @return {YText}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const readYText = decoder => new YText()
|
||||
export const readYText = _decoder => new YText()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as object from 'lib0/object'
|
||||
|
||||
import {
|
||||
YXmlFragment,
|
||||
@@ -9,15 +10,21 @@ import {
|
||||
typeMapGetAll,
|
||||
typeListForEach,
|
||||
YXmlElementRefID,
|
||||
YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Snapshot, Doc, Item // eslint-disable-line
|
||||
YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
/**
|
||||
* @typedef {Object|number|null|Array<any>|string|Uint8Array|AbstractType<any>} ValueTypes
|
||||
*/
|
||||
|
||||
/**
|
||||
* An YXmlElement imitates the behavior of a
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}.
|
||||
*
|
||||
* * An YXmlElement has attributes (key value pairs)
|
||||
* * An YXmlElement has childElements that must inherit from YXmlElement
|
||||
*
|
||||
* @template {{ [key: string]: ValueTypes }} [KV={ [key: string]: string }]
|
||||
*/
|
||||
export class YXmlElement extends YXmlFragment {
|
||||
constructor (nodeName = 'UNDEFINED') {
|
||||
@@ -73,14 +80,19 @@ export class YXmlElement extends YXmlFragment {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {YXmlElement}
|
||||
* @return {YXmlElement<KV>}
|
||||
*/
|
||||
clone () {
|
||||
/**
|
||||
* @type {YXmlElement<KV>}
|
||||
*/
|
||||
const el = new YXmlElement(this.nodeName)
|
||||
const attrs = this.getAttributes()
|
||||
for (const key in attrs) {
|
||||
el.setAttribute(key, attrs[key])
|
||||
}
|
||||
object.forEach(attrs, (value, key) => {
|
||||
if (typeof value === 'string') {
|
||||
el.setAttribute(key, value)
|
||||
}
|
||||
})
|
||||
// @ts-ignore
|
||||
el.insert(0, this.toArray().map(item => item instanceof AbstractType ? item.clone() : item))
|
||||
return el
|
||||
@@ -116,7 +128,7 @@ export class YXmlElement extends YXmlFragment {
|
||||
/**
|
||||
* Removes an attribute from this YXmlElement.
|
||||
*
|
||||
* @param {String} attributeName The attribute name that is to be removed.
|
||||
* @param {string} attributeName The attribute name that is to be removed.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
@@ -133,8 +145,10 @@ export class YXmlElement extends YXmlFragment {
|
||||
/**
|
||||
* Sets or updates an attribute.
|
||||
*
|
||||
* @param {String} attributeName The attribute name that is to be set.
|
||||
* @param {String} attributeValue The attribute value that is to be set.
|
||||
* @template {keyof KV & string} KEY
|
||||
*
|
||||
* @param {KEY} attributeName The attribute name that is to be set.
|
||||
* @param {KV[KEY]} attributeValue The attribute value that is to be set.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
@@ -151,9 +165,11 @@ export class YXmlElement extends YXmlFragment {
|
||||
/**
|
||||
* Returns an attribute value that belongs to the attribute name.
|
||||
*
|
||||
* @param {String} attributeName The attribute name that identifies the
|
||||
* @template {keyof KV & string} KEY
|
||||
*
|
||||
* @param {KEY} attributeName The attribute name that identifies the
|
||||
* queried value.
|
||||
* @return {String} The queried attribute value.
|
||||
* @return {KV[KEY]|undefined} The queried attribute value.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
@@ -164,7 +180,7 @@ export class YXmlElement extends YXmlFragment {
|
||||
/**
|
||||
* Returns whether an attribute exists
|
||||
*
|
||||
* @param {String} attributeName The attribute name to check for existence.
|
||||
* @param {string} attributeName The attribute name to check for existence.
|
||||
* @return {boolean} whether the attribute exists.
|
||||
*
|
||||
* @public
|
||||
@@ -176,13 +192,12 @@ export class YXmlElement extends YXmlFragment {
|
||||
/**
|
||||
* Returns all attribute name/value pairs in a JSON Object.
|
||||
*
|
||||
* @param {Snapshot} [snapshot]
|
||||
* @return {Object<string, any>} A JSON Object that describes the attributes.
|
||||
* @return {{ [Key in Extract<keyof KV,string>]?: KV[Key]}} A JSON Object that describes the attributes.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
getAttributes (snapshot) {
|
||||
return typeMapGetAll(this)
|
||||
getAttributes () {
|
||||
return /** @type {any} */ (typeMapGetAll(this))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -204,7 +219,10 @@ export class YXmlElement extends YXmlFragment {
|
||||
const dom = _document.createElement(this.nodeName)
|
||||
const attrs = this.getAttributes()
|
||||
for (const key in attrs) {
|
||||
dom.setAttribute(key, attrs[key])
|
||||
const value = attrs[key]
|
||||
if (typeof value === 'string') {
|
||||
dom.setAttribute(key, value)
|
||||
}
|
||||
}
|
||||
typeListForEach(this, yxml => {
|
||||
dom.appendChild(yxml.toDOM(_document, hooks, binding))
|
||||
|
||||
@@ -17,10 +17,11 @@ import {
|
||||
transact,
|
||||
typeListGet,
|
||||
typeListSlice,
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, ContentType, Transaction, Item, YXmlText, YXmlHook, Snapshot // eslint-disable-line
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, ContentType, Transaction, Item, YXmlText, YXmlHook // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error'
|
||||
import * as array from 'lib0/array'
|
||||
|
||||
/**
|
||||
* Define the elements to which a set of CSS queries apply.
|
||||
@@ -237,7 +238,7 @@ export class YXmlFragment extends AbstractType {
|
||||
querySelectorAll (query) {
|
||||
query = query.toUpperCase()
|
||||
// @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))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -404,6 +405,15 @@ export class YXmlFragment extends AbstractType {
|
||||
return typeListSlice(this, start, end)
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a provided function on once on overy child element.
|
||||
*
|
||||
* @param {function(YXmlElement|YXmlText,number, typeof self):void} f A function to execute on every element of this YArray.
|
||||
*/
|
||||
forEach (f) {
|
||||
typeListForEach(this, f)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the properties of this type to binary and write it to an
|
||||
* BinaryEncoder.
|
||||
@@ -418,10 +428,10 @@ export class YXmlFragment extends AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
|
||||
* @return {YXmlFragment}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const readYXmlFragment = decoder => new YXmlFragment()
|
||||
export const readYXmlFragment = _decoder => new YXmlFragment()
|
||||
|
||||
@@ -171,7 +171,7 @@ export const mergeDeleteSets = dss => {
|
||||
* @function
|
||||
*/
|
||||
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()
|
||||
@@ -219,17 +219,21 @@ export const createDeleteSetFromStructStore = ss => {
|
||||
*/
|
||||
export const writeDeleteSet = (encoder, ds) => {
|
||||
encoding.writeVarUint(encoder.restEncoder, ds.clients.size)
|
||||
ds.clients.forEach((dsitems, client) => {
|
||||
encoder.resetDsCurVal()
|
||||
encoding.writeVarUint(encoder.restEncoder, client)
|
||||
const len = dsitems.length
|
||||
encoding.writeVarUint(encoder.restEncoder, len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
const item = dsitems[i]
|
||||
encoder.writeDsClock(item.clock)
|
||||
encoder.writeDsLen(item.len)
|
||||
}
|
||||
})
|
||||
|
||||
// Ensure that the delete set is written in a deterministic order
|
||||
array.from(ds.clients.entries())
|
||||
.sort((a, b) => b[0] - a[0])
|
||||
.forEach(([client, dsitems]) => {
|
||||
encoder.resetDsCurVal()
|
||||
encoding.writeVarUint(encoder.restEncoder, client)
|
||||
const len = dsitems.length
|
||||
encoding.writeVarUint(encoder.restEncoder, len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
const item = dsitems[i]
|
||||
encoder.writeDsClock(item.clock)
|
||||
encoder.writeDsLen(item.len)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -247,7 +251,7 @@ export const readDeleteSet = decoder => {
|
||||
const client = decoding.readVarUint(decoder.restDecoder)
|
||||
const numberOfDeletes = decoding.readVarUint(decoder.restDecoder)
|
||||
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++) {
|
||||
dsField.push(new DeleteItem(decoder.readDsClock(), decoder.readDsLen()))
|
||||
}
|
||||
@@ -324,3 +328,23 @@ export const readAndApplyDeleteSet = (decoder, transaction, store) => {
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DeleteSet} ds1
|
||||
* @param {DeleteSet} ds2
|
||||
*/
|
||||
export const equalDeleteSets = (ds1, ds2) => {
|
||||
if (ds1.clients.size !== ds2.clients.size) return false
|
||||
for (const [client, deleteItems1] of ds1.clients.entries()) {
|
||||
const deleteItems2 = /** @type {Array<import('../internals.js').DeleteItem>} */ (ds2.clients.get(client))
|
||||
if (deleteItems2 === undefined || deleteItems1.length !== deleteItems2.length) return false
|
||||
for (let i = 0; i < deleteItems1.length; i++) {
|
||||
const di1 = deleteItems1[i]
|
||||
const di2 = deleteItems2[i]
|
||||
if (di1.clock !== di2.clock || di1.len !== di2.len) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export const generateNewClientId = random.uint32
|
||||
*/
|
||||
export class Doc extends Observable {
|
||||
/**
|
||||
* @param {DocOpts} [opts] configuration
|
||||
* @param {DocOpts} opts configuration
|
||||
*/
|
||||
constructor ({ guid = random.uuidv4(), collectionid = null, gc = true, gcFilter = () => true, meta = null, autoLoad = false, shouldLoad = true } = {}) {
|
||||
super()
|
||||
@@ -72,13 +72,57 @@ export class Doc extends Observable {
|
||||
this.shouldLoad = shouldLoad
|
||||
this.autoLoad = autoLoad
|
||||
this.meta = meta
|
||||
/**
|
||||
* This is set to true when the persistence provider loaded the document from the database or when the `sync` event fires.
|
||||
* Note that not all providers implement this feature. Provider authors are encouraged to fire the `load` event when the doc content is loaded from the database.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.isLoaded = false
|
||||
/**
|
||||
* This is set to true when the connection provider has successfully synced with a backend.
|
||||
* Note that when using peer-to-peer providers this event may not provide very useful.
|
||||
* Also note that not all providers implement this feature. Provider authors are encouraged to fire
|
||||
* the `sync` event when the doc has been synced (with `true` as a parameter) or if connection is
|
||||
* lost (with false as a parameter).
|
||||
*/
|
||||
this.isSynced = false
|
||||
/**
|
||||
* Promise that resolves once the document has been loaded from a presistence provider.
|
||||
*/
|
||||
this.whenLoaded = promise.create(resolve => {
|
||||
this.on('load', () => {
|
||||
this.isLoaded = true
|
||||
resolve(this)
|
||||
})
|
||||
})
|
||||
const provideSyncedPromise = () => promise.create(resolve => {
|
||||
/**
|
||||
* @param {boolean} isSynced
|
||||
*/
|
||||
const eventHandler = (isSynced) => {
|
||||
if (isSynced === undefined || isSynced === true) {
|
||||
this.off('sync', eventHandler)
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
this.on('sync', eventHandler)
|
||||
})
|
||||
this.on('sync', isSynced => {
|
||||
if (isSynced === false && this.isSynced) {
|
||||
this.whenSynced = provideSyncedPromise()
|
||||
}
|
||||
this.isSynced = isSynced === undefined || isSynced === true
|
||||
if (!this.isLoaded) {
|
||||
this.emit('load', [])
|
||||
}
|
||||
})
|
||||
/**
|
||||
* Promise that resolves once the document has been synced with a backend.
|
||||
* This promise is recreated when the connection is lost.
|
||||
* Note the documentation about the `isSynced` property.
|
||||
*/
|
||||
this.whenSynced = provideSyncedPromise()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,7 +147,7 @@ export class Doc extends Observable {
|
||||
}
|
||||
|
||||
getSubdocGuids () {
|
||||
return new Set(Array.from(this.subdocs).map(doc => doc.guid))
|
||||
return new Set(array.from(this.subdocs).map(doc => doc.guid))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,13 +156,15 @@ export class Doc extends Observable {
|
||||
* that happened inside of the transaction are sent as one message to the
|
||||
* 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
|
||||
* @return T
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
transact (f, origin = null) {
|
||||
transact(this, f, origin)
|
||||
return transact(this, f, origin)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -71,7 +71,7 @@ export class PermanentUserData {
|
||||
* @param {Doc} doc
|
||||
* @param {number} clientid
|
||||
* @param {string} userDescription
|
||||
* @param {Object} [conf]
|
||||
* @param {Object} conf
|
||||
* @param {function(Transaction, DeleteSet):boolean} [conf.filter]
|
||||
*/
|
||||
setUserMapping (doc, clientid, userDescription, { filter = () => true } = {}) {
|
||||
@@ -84,7 +84,7 @@ export class PermanentUserData {
|
||||
users.set(userDescription, user)
|
||||
}
|
||||
user.get('ids').push([clientid])
|
||||
users.observe(event => {
|
||||
users.observe(_event => {
|
||||
setTimeout(() => {
|
||||
const userOverwrite = users.get(userDescription)
|
||||
if (userOverwrite !== user) {
|
||||
|
||||
@@ -15,7 +15,10 @@ import {
|
||||
findIndexSS,
|
||||
UpdateEncoderV2,
|
||||
applyUpdateV2,
|
||||
DSEncoderV1, DSEncoderV2, DSDecoderV1, DSDecoderV2, Transaction, Doc, DeleteSet, Item // eslint-disable-line
|
||||
LazyStructReader,
|
||||
equalDeleteSets,
|
||||
UpdateDecoderV1, UpdateDecoderV2, DSEncoderV1, DSEncoderV2, DSDecoderV1, DSDecoderV2, Transaction, Doc, DeleteSet, Item, // eslint-disable-line
|
||||
mergeDeleteSets
|
||||
} from '../internals.js'
|
||||
|
||||
import * as map from 'lib0/map'
|
||||
@@ -147,12 +150,20 @@ export const splitSnapshotAffectedStructs = (transaction, snapshot) => {
|
||||
getItemCleanStart(transaction, createID(client, clock))
|
||||
}
|
||||
})
|
||||
iterateDeletedStructs(transaction, snapshot.ds, item => {})
|
||||
iterateDeletedStructs(transaction, snapshot.ds, _item => {})
|
||||
meta.add(snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @example
|
||||
* const ydoc = new Y.Doc({ gc: false })
|
||||
* ydoc.getText().insert(0, 'world!')
|
||||
* const snapshot = Y.snapshot(ydoc)
|
||||
* ydoc.getText().insert(0, 'hello ')
|
||||
* const restored = Y.createDocFromSnapshot(ydoc, snapshot)
|
||||
* assert(restored.getText().toString() === 'world!')
|
||||
*
|
||||
* @param {Doc} originDoc
|
||||
* @param {Snapshot} snapshot
|
||||
* @param {Doc} [newDoc] Optionally, you may define the Yjs document that receives the data from originDoc
|
||||
@@ -161,7 +172,7 @@ export const splitSnapshotAffectedStructs = (transaction, snapshot) => {
|
||||
export const createDocFromSnapshot = (originDoc, snapshot, newDoc = new Doc()) => {
|
||||
if (originDoc.gc) {
|
||||
// we should not try to restore a GC-ed document, because some of the restored items might have their content deleted
|
||||
throw new Error('originDoc must not be garbage collected')
|
||||
throw new Error('Garbage-collection must be disabled in `originDoc`!')
|
||||
}
|
||||
const { sv, ds } = snapshot
|
||||
|
||||
@@ -199,3 +210,28 @@ export const createDocFromSnapshot = (originDoc, snapshot, newDoc = new Doc()) =
|
||||
applyUpdateV2(newDoc, encoder.toUint8Array(), 'snapshot')
|
||||
return newDoc
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Snapshot} snapshot
|
||||
* @param {Uint8Array} update
|
||||
* @param {typeof UpdateDecoderV2 | typeof UpdateDecoderV1} [YDecoder]
|
||||
*/
|
||||
export const snapshotContainsUpdateV2 = (snapshot, update, YDecoder = UpdateDecoderV2) => {
|
||||
const structs = []
|
||||
const updateDecoder = new YDecoder(decoding.createDecoder(update))
|
||||
const lazyDecoder = new LazyStructReader(updateDecoder, false)
|
||||
for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) {
|
||||
structs.push(curr)
|
||||
if ((snapshot.sv.get(curr.id.client) || 0) < curr.id.clock + curr.length) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
const mergedDS = mergeDeleteSets([snapshot.ds, readDeleteSet(updateDecoder)])
|
||||
return equalDeleteSets(snapshot.ds, mergedDS)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Snapshot} snapshot
|
||||
* @param {Uint8Array} update
|
||||
*/
|
||||
export const snapshotContainsUpdate = (snapshot, update) => snapshotContainsUpdateV2(snapshot, update, UpdateDecoderV1)
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Item,
|
||||
generateNewClientId,
|
||||
createID,
|
||||
cleanupYTextAfterTransaction,
|
||||
UpdateEncoderV1, UpdateEncoderV2, GC, StructStore, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
@@ -114,6 +115,10 @@ export class Transaction {
|
||||
* @type {Set<Doc>}
|
||||
*/
|
||||
this.subdocsLoaded = new Set()
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
this._needFormattingCleanup = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,7 +256,6 @@ const cleanupTransactions = (transactionCleanups, i) => {
|
||||
try {
|
||||
sortAndMergeDeleteSet(ds)
|
||||
transaction.afterState = getStateVector(transaction.doc.store)
|
||||
doc._transaction = null
|
||||
doc.emit('beforeObserverCalls', [transaction, doc])
|
||||
/**
|
||||
* An array of event callbacks.
|
||||
@@ -271,31 +275,34 @@ const cleanupTransactions = (transactionCleanups, i) => {
|
||||
)
|
||||
fs.push(() => {
|
||||
// deep observe events
|
||||
transaction.changedParentTypes.forEach((events, type) =>
|
||||
fs.push(() => {
|
||||
// We need to think about the possibility that the user transforms the
|
||||
// Y.Doc in the event.
|
||||
if (type._item === null || !type._item.deleted) {
|
||||
events = events
|
||||
.filter(event =>
|
||||
event.target._item === null || !event.target._item.deleted
|
||||
)
|
||||
events
|
||||
.forEach(event => {
|
||||
event.currentTarget = type
|
||||
})
|
||||
// sort events by path length so that top-level events are fired first.
|
||||
events
|
||||
.sort((event1, event2) => event1.path.length - event2.path.length)
|
||||
// We don't need to check for events.length
|
||||
// because we know it has at least one element
|
||||
callEventHandlerListeners(type._dEH, events, transaction)
|
||||
}
|
||||
})
|
||||
)
|
||||
fs.push(() => doc.emit('afterTransaction', [transaction, doc]))
|
||||
transaction.changedParentTypes.forEach((events, type) => {
|
||||
// We need to think about the possibility that the user transforms the
|
||||
// Y.Doc in the event.
|
||||
if (type._dEH.l.length > 0 && (type._item === null || !type._item.deleted)) {
|
||||
events = events
|
||||
.filter(event =>
|
||||
event.target._item === null || !event.target._item.deleted
|
||||
)
|
||||
events
|
||||
.forEach(event => {
|
||||
event.currentTarget = type
|
||||
// path is relative to the current target
|
||||
event._path = null
|
||||
})
|
||||
// sort events by path length so that top-level events are fired first.
|
||||
events
|
||||
.sort((event1, event2) => event1.path.length - event2.path.length)
|
||||
// We don't need to check for events.length
|
||||
// because we know it has at least one element
|
||||
callEventHandlerListeners(type._dEH, events, transaction)
|
||||
}
|
||||
})
|
||||
})
|
||||
fs.push(() => doc.emit('afterTransaction', [transaction, doc]))
|
||||
callAll(fs, [])
|
||||
if (transaction._needFormattingCleanup) {
|
||||
cleanupYTextAfterTransaction(transaction)
|
||||
}
|
||||
} finally {
|
||||
// Replace deleted items with ItemDeleted / GC.
|
||||
// This is where content is actually remove from the Yjs Doc.
|
||||
@@ -377,15 +384,21 @@ const cleanupTransactions = (transactionCleanups, i) => {
|
||||
/**
|
||||
* Implements the functionality of `y.transact(()=>{..})`
|
||||
*
|
||||
* @template T
|
||||
* @param {Doc} doc
|
||||
* @param {function(Transaction):void} f
|
||||
* @param {function(Transaction):T} f
|
||||
* @param {any} [origin=true]
|
||||
* @return {T}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const transact = (doc, f, origin = null, local = true) => {
|
||||
const transactionCleanups = doc._transactionCleanups
|
||||
let initialCall = false
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
let result = null
|
||||
if (doc._transaction === null) {
|
||||
initialCall = true
|
||||
doc._transaction = new Transaction(doc, origin, local)
|
||||
@@ -396,18 +409,23 @@ export const transact = (doc, f, origin = null, local = true) => {
|
||||
doc.emit('beforeTransaction', [doc._transaction, doc])
|
||||
}
|
||||
try {
|
||||
f(doc._transaction)
|
||||
result = f(doc._transaction)
|
||||
} finally {
|
||||
if (initialCall && transactionCleanups[0] === doc._transaction) {
|
||||
// The first transaction ended, now process observer calls.
|
||||
// Observer call may create new transactions for which we need to call the observers and do cleanup.
|
||||
// We don't want to nest these calls, so we execute these calls one after
|
||||
// another.
|
||||
// Also we need to ensure that all cleanups are called, even if the
|
||||
// observes throw errors.
|
||||
// This file is full of hacky try {} finally {} blocks to ensure that an
|
||||
// event can throw errors and also that the cleanup is called.
|
||||
cleanupTransactions(transactionCleanups, 0)
|
||||
if (initialCall) {
|
||||
const finishCleanup = doc._transaction === transactionCleanups[0]
|
||||
doc._transaction = null
|
||||
if (finishCleanup) {
|
||||
// The first transaction ended, now process observer calls.
|
||||
// Observer call may create new transactions for which we need to call the observers and do cleanup.
|
||||
// We don't want to nest these calls, so we execute these calls one after
|
||||
// another.
|
||||
// Also we need to ensure that all cleanups are called, even if the
|
||||
// observes throw errors.
|
||||
// This file is full of hacky try {} finally {} blocks to ensure that an
|
||||
// event can throw errors and also that the cleanup is called.
|
||||
cleanupTransactions(transactionCleanups, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -10,14 +10,14 @@ import {
|
||||
getItemCleanStart,
|
||||
isDeleted,
|
||||
addToDeleteSet,
|
||||
Transaction, Doc, Item, GC, DeleteSet, AbstractType, YEvent // eslint-disable-line
|
||||
Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as time from 'lib0/time'
|
||||
import * as array from 'lib0/array'
|
||||
import { Observable } from 'lib0/observable'
|
||||
|
||||
class StackItem {
|
||||
export class StackItem {
|
||||
/**
|
||||
* @param {DeleteSet} deletions
|
||||
* @param {DeleteSet} insertions
|
||||
@@ -101,7 +101,7 @@ const popStackItem = (undoManager, stack, eventType) => {
|
||||
}
|
||||
})
|
||||
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
|
||||
// parents, so we have more information available when items are filtered.
|
||||
@@ -132,12 +132,14 @@ const popStackItem = (undoManager, stack, eventType) => {
|
||||
/**
|
||||
* @typedef {Object} UndoManagerOptions
|
||||
* @property {number} [UndoManagerOptions.captureTimeout=500]
|
||||
* @property {function(Transaction):boolean} [UndoManagerOptions.captureTransaction] Do not capture changes of a Transaction if result false.
|
||||
* @property {function(Item):boolean} [UndoManagerOptions.deleteFilter=()=>true] Sometimes
|
||||
* it is necessary to filter whan an Undo/Redo operation can delete. If this
|
||||
* it is necessary to filter what an Undo/Redo operation can delete. If this
|
||||
* filter returns false, the type/item won't be deleted even it is in the
|
||||
* undo/redo scope.
|
||||
* @property {Set<any>} [UndoManagerOptions.trackedOrigins=new Set([null])]
|
||||
* @property {boolean} [ignoreRemoteMapChanges] Experimental. By default, the UndoManager will never overwrite remote changes. Enable this property to enable overwriting remote changes on key-value changes (Y.Map, properties on Y.Xml, etc..).
|
||||
* @property {Doc} [doc] The document that this UndoManager operates on. Only needed if typeScope is empty.
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -154,7 +156,14 @@ export class UndoManager extends Observable {
|
||||
* @param {AbstractType<any>|Array<AbstractType<any>>} typeScope Accepts either a single type, or an array of types
|
||||
* @param {UndoManagerOptions} options
|
||||
*/
|
||||
constructor (typeScope, { captureTimeout = 500, deleteFilter = () => true, trackedOrigins = new Set([null]), ignoreRemoteMapChanges = false } = {}) {
|
||||
constructor (typeScope, {
|
||||
captureTimeout = 500,
|
||||
captureTransaction = _tr => true,
|
||||
deleteFilter = () => true,
|
||||
trackedOrigins = new Set([null]),
|
||||
ignoreRemoteMapChanges = false,
|
||||
doc = /** @type {Doc} */ (array.isArray(typeScope) ? typeScope[0].doc : typeScope.doc)
|
||||
} = {}) {
|
||||
super()
|
||||
/**
|
||||
* @type {Array<AbstractType<any>>}
|
||||
@@ -164,6 +173,7 @@ export class UndoManager extends Observable {
|
||||
this.deleteFilter = deleteFilter
|
||||
trackedOrigins.add(this)
|
||||
this.trackedOrigins = trackedOrigins
|
||||
this.captureTransaction = captureTransaction
|
||||
/**
|
||||
* @type {Array<StackItem>}
|
||||
*/
|
||||
@@ -179,15 +189,20 @@ export class UndoManager extends Observable {
|
||||
*/
|
||||
this.undoing = false
|
||||
this.redoing = false
|
||||
this.doc = /** @type {Doc} */ (this.scope[0].doc)
|
||||
this.doc = doc
|
||||
this.lastChange = 0
|
||||
this.ignoreRemoteMapChanges = ignoreRemoteMapChanges
|
||||
this.captureTimeout = captureTimeout
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
this.afterTransactionHandler = transaction => {
|
||||
// Only track certain transactions
|
||||
if (!this.scope.some(type => transaction.changedParentTypes.has(type)) || (!this.trackedOrigins.has(transaction.origin) && (!transaction.origin || !this.trackedOrigins.has(transaction.origin.constructor)))) {
|
||||
if (
|
||||
!this.captureTransaction(transaction) ||
|
||||
!this.scope.some(type => transaction.changedParentTypes.has(type)) ||
|
||||
(!this.trackedOrigins.has(transaction.origin) && (!transaction.origin || !this.trackedOrigins.has(transaction.origin.constructor)))
|
||||
) {
|
||||
return
|
||||
}
|
||||
const undoing = this.undoing
|
||||
@@ -209,7 +224,7 @@ export class UndoManager extends Observable {
|
||||
})
|
||||
const now = time.getUnixTime()
|
||||
let didAdd = false
|
||||
if (now - this.lastChange < captureTimeout && stack.length > 0 && !undoing && !redoing) {
|
||||
if (this.lastChange > 0 && now - this.lastChange < this.captureTimeout && stack.length > 0 && !undoing && !redoing) {
|
||||
// append change to last stack op
|
||||
const lastOp = stack[stack.length - 1]
|
||||
lastOp.deletions = mergeDeleteSets([lastOp.deletions, transaction.deleteSet])
|
||||
|
||||
@@ -44,6 +44,10 @@ export class YEvent {
|
||||
* @type {null | Array<{ insert?: string | Array<any> | object | AbstractType<any>, retain?: number, delete?: number, attributes?: Object<string, any> }>}
|
||||
*/
|
||||
this._delta = null
|
||||
/**
|
||||
* @type {Array<string|number>|null}
|
||||
*/
|
||||
this._path = null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,8 +64,7 @@ export class YEvent {
|
||||
* type === event.target // => true
|
||||
*/
|
||||
get path () {
|
||||
// @ts-ignore _item is defined because target is integrated
|
||||
return getPathTo(this.currentTarget, this.target)
|
||||
return this._path || (this._path = getPathTo(this.currentTarget, this.target))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,6 +133,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>}>}
|
||||
*/
|
||||
get delta () {
|
||||
@@ -149,6 +157,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}>}}
|
||||
*/
|
||||
get changes () {
|
||||
|
||||
@@ -45,6 +45,7 @@ import * as decoding from 'lib0/decoding'
|
||||
import * as binary from 'lib0/binary'
|
||||
import * as map from 'lib0/map'
|
||||
import * as math from 'lib0/math'
|
||||
import * as array from 'lib0/array'
|
||||
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
@@ -87,7 +88,7 @@ export const writeClientsStructs = (encoder, store, _sm) => {
|
||||
sm.set(client, clock)
|
||||
}
|
||||
})
|
||||
getStateVector(store).forEach((clock, client) => {
|
||||
getStateVector(store).forEach((_clock, client) => {
|
||||
if (!_sm.has(client)) {
|
||||
sm.set(client, 0)
|
||||
}
|
||||
@@ -96,9 +97,8 @@ export const writeClientsStructs = (encoder, store, _sm) => {
|
||||
encoding.writeVarUint(encoder.restEncoder, sm.size)
|
||||
// Write items with higher client ids first
|
||||
// This heavily improves the conflict algorithm.
|
||||
Array.from(sm.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => {
|
||||
// @ts-ignore
|
||||
writeStructs(encoder, store.clients.get(client), client, clock)
|
||||
array.from(sm.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => {
|
||||
writeStructs(encoder, /** @type {Array<GC|Item>} */ (store.clients.get(client)), client, clock)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ const integrateStructs = (transaction, store, clientsStructRefs) => {
|
||||
*/
|
||||
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.
|
||||
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) {
|
||||
return null
|
||||
}
|
||||
@@ -601,7 +601,7 @@ export const decodeStateVector = decodedState => readStateVector(new DSDecoderV1
|
||||
*/
|
||||
export const writeStateVector = (encoder, sv) => {
|
||||
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, clock)
|
||||
})
|
||||
|
||||
@@ -2,19 +2,40 @@
|
||||
import * as binary from 'lib0/binary'
|
||||
import * as decoding from 'lib0/decoding'
|
||||
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 map from 'lib0/map'
|
||||
import * as math from 'lib0/math'
|
||||
import * as string from 'lib0/string'
|
||||
|
||||
import {
|
||||
ContentAny,
|
||||
ContentBinary,
|
||||
ContentDeleted,
|
||||
ContentDoc,
|
||||
ContentEmbed,
|
||||
ContentFormat,
|
||||
ContentJSON,
|
||||
ContentString,
|
||||
ContentType,
|
||||
createID,
|
||||
readItemContent,
|
||||
readDeleteSet,
|
||||
writeDeleteSet,
|
||||
Skip,
|
||||
mergeDeleteSets,
|
||||
decodeStateVector,
|
||||
DSEncoderV1,
|
||||
DSEncoderV2,
|
||||
decodeStateVector,
|
||||
Item, GC, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2 // eslint-disable-line
|
||||
GC,
|
||||
Item,
|
||||
mergeDeleteSets,
|
||||
readDeleteSet,
|
||||
readItemContent,
|
||||
Skip,
|
||||
UpdateDecoderV1,
|
||||
UpdateDecoderV2,
|
||||
UpdateEncoderV1,
|
||||
UpdateEncoderV2,
|
||||
writeDeleteSet,
|
||||
YXmlElement,
|
||||
YXmlHook
|
||||
} from '../internals.js'
|
||||
|
||||
/**
|
||||
@@ -552,17 +573,17 @@ const finishLazyStructWriting = (lazyWriter) => {
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} update
|
||||
* @param {function(Item|GC|Skip):Item|GC|Skip} blockTransformer
|
||||
* @param {typeof UpdateDecoderV2 | typeof UpdateDecoderV1} YDecoder
|
||||
* @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 lazyDecoder = new LazyStructReader(updateDecoder, false)
|
||||
const updateEncoder = new YEncoder()
|
||||
const lazyWriter = new LazyStructWriter(updateEncoder)
|
||||
|
||||
for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) {
|
||||
writeStructToLazyStructWriter(lazyWriter, curr, 0)
|
||||
writeStructToLazyStructWriter(lazyWriter, blockTransformer(curr), 0)
|
||||
}
|
||||
finishLazyStructWriting(lazyWriter)
|
||||
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
|
||||
*/
|
||||
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)
|
||||
|
||||
@@ -2,12 +2,55 @@
|
||||
import * as Y from '../src/index.js'
|
||||
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
|
||||
*/
|
||||
export const testOriginInTransaction = _tc => {
|
||||
const doc = new Y.Doc()
|
||||
const ytext = doc.getText()
|
||||
/**
|
||||
* @type {Array<string>}
|
||||
*/
|
||||
const origins = []
|
||||
doc.on('afterTransaction', (tr) => {
|
||||
origins.push(tr.origin)
|
||||
if (origins.length <= 1) {
|
||||
ytext.toDelta(Y.snapshot(doc)) // adding a snapshot forces toDelta to create a cleanup transaction
|
||||
doc.transact(() => {
|
||||
ytext.insert(0, 'a')
|
||||
}, 'nested')
|
||||
}
|
||||
})
|
||||
doc.transact(() => {
|
||||
ytext.insert(0, '0')
|
||||
}, 'first')
|
||||
t.compareArrays(origins, ['first', 'cleanup', 'nested'])
|
||||
}
|
||||
|
||||
/**
|
||||
* Client id should be changed when an instance receives updates from another client using the same client id.
|
||||
*
|
||||
* @param {t.TestCase} tc
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testClientIdDuplicateChange = tc => {
|
||||
export const testClientIdDuplicateChange = _tc => {
|
||||
const doc1 = new Y.Doc()
|
||||
doc1.clientID = 0
|
||||
const doc2 = new Y.Doc()
|
||||
@@ -19,9 +62,9 @@ export const testClientIdDuplicateChange = tc => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testGetTypeEmptyId = tc => {
|
||||
export const testGetTypeEmptyId = _tc => {
|
||||
const doc1 = new Y.Doc()
|
||||
doc1.getText('').insert(0, 'h')
|
||||
doc1.getText().insert(1, 'i')
|
||||
@@ -32,9 +75,9 @@ export const testGetTypeEmptyId = tc => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testToJSON = tc => {
|
||||
export const testToJSON = _tc => {
|
||||
const doc = new Y.Doc()
|
||||
t.compare(doc.toJSON(), {}, 'doc.toJSON yields empty object')
|
||||
|
||||
@@ -59,9 +102,9 @@ export const testToJSON = tc => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testSubdoc = tc => {
|
||||
export const testSubdoc = _tc => {
|
||||
const doc = new Y.Doc()
|
||||
doc.load() // doesn't do anything
|
||||
{
|
||||
@@ -126,9 +169,9 @@ export const testSubdoc = tc => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testSubdocLoadEdgeCases = tc => {
|
||||
export const testSubdocLoadEdgeCases = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const yarray = ydoc.getArray()
|
||||
const subdoc1 = new Y.Doc()
|
||||
@@ -173,9 +216,9 @@ export const testSubdocLoadEdgeCases = tc => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testSubdocLoadEdgeCasesAutoload = tc => {
|
||||
export const testSubdocLoadEdgeCasesAutoload = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const yarray = ydoc.getArray()
|
||||
const subdoc1 = new Y.Doc({ autoLoad: true })
|
||||
@@ -215,9 +258,9 @@ export const testSubdocLoadEdgeCasesAutoload = tc => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testSubdocsUndo = tc => {
|
||||
export const testSubdocsUndo = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const elems = ydoc.getXmlFragment()
|
||||
const undoManager = new Y.UndoManager(elems)
|
||||
@@ -230,9 +273,9 @@ export const testSubdocsUndo = tc => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testLoadDocs = async tc => {
|
||||
export const testLoadDocsEvent = async _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
t.assert(ydoc.isLoaded === false)
|
||||
let loadedEvent = false
|
||||
@@ -244,3 +287,44 @@ export const testLoadDocs = async tc => {
|
||||
t.assert(loadedEvent)
|
||||
t.assert(ydoc.isLoaded)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testSyncDocsEvent = async _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
t.assert(ydoc.isLoaded === false)
|
||||
t.assert(ydoc.isSynced === false)
|
||||
let loadedEvent = false
|
||||
ydoc.once('load', () => {
|
||||
loadedEvent = true
|
||||
})
|
||||
let syncedEvent = false
|
||||
ydoc.once('sync', /** @param {any} isSynced */ (isSynced) => {
|
||||
syncedEvent = true
|
||||
t.assert(isSynced)
|
||||
})
|
||||
ydoc.emit('sync', [true, ydoc])
|
||||
await ydoc.whenLoaded
|
||||
const oldWhenSynced = ydoc.whenSynced
|
||||
await ydoc.whenSynced
|
||||
t.assert(loadedEvent)
|
||||
t.assert(syncedEvent)
|
||||
t.assert(ydoc.isLoaded)
|
||||
t.assert(ydoc.isSynced)
|
||||
let loadedEvent2 = false
|
||||
ydoc.on('load', () => {
|
||||
loadedEvent2 = true
|
||||
})
|
||||
let syncedEvent2 = false
|
||||
ydoc.on('sync', (isSynced) => {
|
||||
syncedEvent2 = true
|
||||
t.assert(isSynced === false)
|
||||
})
|
||||
ydoc.emit('sync', [false, ydoc])
|
||||
t.assert(!loadedEvent2)
|
||||
t.assert(syncedEvent2)
|
||||
t.assert(ydoc.isLoaded)
|
||||
t.assert(!ydoc.isSynced)
|
||||
t.assert(ydoc.whenSynced !== oldWhenSynced)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env node */
|
||||
|
||||
import * as map from './y-map.tests.js'
|
||||
import * as array from './y-array.tests.js'
|
||||
|
||||
@@ -3,9 +3,21 @@ import * as t from 'lib0/testing'
|
||||
import { init } from './testHelper.js'
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testBasicRestoreSnapshot = tc => {
|
||||
export const testBasic = _tc => {
|
||||
const ydoc = new Y.Doc({ gc: false })
|
||||
ydoc.getText().insert(0, 'world!')
|
||||
const snapshot = Y.snapshot(ydoc)
|
||||
ydoc.getText().insert(0, 'hello ')
|
||||
const restored = Y.createDocFromSnapshot(ydoc, snapshot)
|
||||
t.assert(restored.getText().toString() === 'world!')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testBasicRestoreSnapshot = _tc => {
|
||||
const doc = new Y.Doc({ gc: false })
|
||||
doc.getArray('array').insert(0, ['hello'])
|
||||
const snap = Y.snapshot(doc)
|
||||
@@ -18,9 +30,9 @@ export const testBasicRestoreSnapshot = tc => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testEmptyRestoreSnapshot = tc => {
|
||||
export const testEmptyRestoreSnapshot = _tc => {
|
||||
const doc = new Y.Doc({ gc: false })
|
||||
const snap = Y.snapshot(doc)
|
||||
snap.sv.set(9999, 0)
|
||||
@@ -38,9 +50,9 @@ export const testEmptyRestoreSnapshot = tc => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testRestoreSnapshotWithSubType = tc => {
|
||||
export const testRestoreSnapshotWithSubType = _tc => {
|
||||
const doc = new Y.Doc({ gc: false })
|
||||
doc.getArray('array').insert(0, [new Y.Map()])
|
||||
const subMap = doc.getArray('array').get(0)
|
||||
@@ -61,9 +73,9 @@ export const testRestoreSnapshotWithSubType = tc => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testRestoreDeletedItem1 = tc => {
|
||||
export const testRestoreDeletedItem1 = _tc => {
|
||||
const doc = new Y.Doc({ gc: false })
|
||||
doc.getArray('array').insert(0, ['item1', 'item2'])
|
||||
|
||||
@@ -77,9 +89,9 @@ export const testRestoreDeletedItem1 = tc => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testRestoreLeftItem = tc => {
|
||||
export const testRestoreLeftItem = _tc => {
|
||||
const doc = new Y.Doc({ gc: false })
|
||||
doc.getArray('array').insert(0, ['item1'])
|
||||
doc.getMap('map').set('test', 1)
|
||||
@@ -95,9 +107,9 @@ export const testRestoreLeftItem = tc => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testDeletedItemsBase = tc => {
|
||||
export const testDeletedItemsBase = _tc => {
|
||||
const doc = new Y.Doc({ gc: false })
|
||||
doc.getArray('array').insert(0, ['item1'])
|
||||
doc.getArray('array').delete(0)
|
||||
@@ -111,9 +123,9 @@ export const testDeletedItemsBase = tc => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testDeletedItems2 = tc => {
|
||||
export const testDeletedItems2 = _tc => {
|
||||
const doc = new Y.Doc({ gc: false })
|
||||
doc.getArray('array').insert(0, ['item1', 'item2', 'item3'])
|
||||
doc.getArray('array').delete(1)
|
||||
@@ -169,3 +181,28 @@ export const testDependentChanges = tc => {
|
||||
const docRestored1 = Y.createDocFromSnapshot(array1.doc, snap)
|
||||
t.compare(docRestored1.getArray('array').toArray(), ['user1item1', 'user2item1'])
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testContainsUpdate = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
/**
|
||||
* @type {Array<Uint8Array>}
|
||||
*/
|
||||
const updates = []
|
||||
ydoc.on('update', update => {
|
||||
updates.push(update)
|
||||
})
|
||||
const yarr = ydoc.getArray()
|
||||
const snapshot1 = Y.snapshot(ydoc)
|
||||
yarr.insert(0, [1])
|
||||
const snapshot2 = Y.snapshot(ydoc)
|
||||
yarr.delete(0, 1)
|
||||
const snapshotFinal = Y.snapshot(ydoc)
|
||||
t.assert(!Y.snapshotContainsUpdate(snapshot1, updates[0]))
|
||||
t.assert(!Y.snapshotContainsUpdate(snapshot2, updates[1]))
|
||||
t.assert(Y.snapshotContainsUpdate(snapshot2, updates[0]))
|
||||
t.assert(Y.snapshotContainsUpdate(snapshotFinal, updates[0]))
|
||||
t.assert(Y.snapshotContainsUpdate(snapshotFinal, updates[1]))
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as encoding from 'lib0/encoding'
|
||||
import * as decoding from 'lib0/decoding'
|
||||
import * as syncProtocol from 'y-protocols/sync'
|
||||
import * as object from 'lib0/object'
|
||||
import * as map from 'lib0/map'
|
||||
import * as Y from '../src/index.js'
|
||||
export * from '../src/index.js'
|
||||
|
||||
@@ -89,8 +90,8 @@ export class TestYInstance extends Y.Doc {
|
||||
const encoder = encoding.createEncoder()
|
||||
syncProtocol.writeUpdate(encoder, update)
|
||||
broadcastMessage(this, encoding.toUint8Array(encoder))
|
||||
this.updates.push(update)
|
||||
}
|
||||
this.updates.push(update)
|
||||
})
|
||||
this.connect()
|
||||
}
|
||||
@@ -133,12 +134,7 @@ export class TestYInstance extends Y.Doc {
|
||||
* @param {TestYInstance} remoteClient
|
||||
*/
|
||||
_receive (message, remoteClient) {
|
||||
let messages = this.receiving.get(remoteClient)
|
||||
if (messages === undefined) {
|
||||
messages = []
|
||||
this.receiving.set(remoteClient, messages)
|
||||
}
|
||||
messages.push(message)
|
||||
map.setIfUndefined(this.receiving, remoteClient, () => /** @type {Array<Uint8Array>} */ ([])).push(message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,17 +198,6 @@ export class TestConnector {
|
||||
// send reply message
|
||||
sender._receive(encoding.toUint8Array(encoder), receiver)
|
||||
}
|
||||
{
|
||||
// If update message, add the received message to the list of received messages
|
||||
const decoder = decoding.createDecoder(m)
|
||||
const messageType = decoding.readVarUint(decoder)
|
||||
switch (messageType) {
|
||||
case syncProtocol.messageYjsUpdate:
|
||||
case syncProtocol.messageYjsSyncStep2:
|
||||
receiver.updates.push(decoding.readVarUint8Array(decoder))
|
||||
break
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -362,7 +347,7 @@ export const compare = users => {
|
||||
t.compare(userMapValues[i], userMapValues[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], userTextValues[i + 1], '', (constructor, a, b) => {
|
||||
t.compare(userTextValues[i], userTextValues[i + 1], '', (_constructor, a, b) => {
|
||||
if (a instanceof Y.AbstractType) {
|
||||
t.compare(a.toJSON(), b.toJSON())
|
||||
} else if (a !== b) {
|
||||
@@ -371,8 +356,9 @@ export const compare = users => {
|
||||
return true
|
||||
})
|
||||
t.compare(Y.encodeStateVector(users[i]), Y.encodeStateVector(users[i + 1]))
|
||||
compareDS(Y.createDeleteSetFromStructStore(users[i].store), Y.createDeleteSetFromStructStore(users[i + 1].store))
|
||||
Y.equalDeleteSets(Y.createDeleteSetFromStructStore(users[i].store), Y.createDeleteSetFromStructStore(users[i + 1].store))
|
||||
compareStructStores(users[i].store, users[i + 1].store)
|
||||
t.compare(Y.encodeSnapshot(Y.snapshot(users[i])), Y.encodeSnapshot(Y.snapshot(users[i + 1])))
|
||||
}
|
||||
users.map(u => u.destroy())
|
||||
}
|
||||
@@ -385,8 +371,8 @@ export const compare = users => {
|
||||
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').StructStore} ss2
|
||||
* @param {import('../src/internals.js').StructStore} ss1
|
||||
* @param {import('../src/internals.js').StructStore} ss2
|
||||
*/
|
||||
export const compareStructStores = (ss1, ss2) => {
|
||||
t.assert(ss1.clients.size === ss2.clients.size)
|
||||
@@ -427,25 +413,6 @@ export const compareStructStores = (ss1, ss2) => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('../src/internals').DeleteSet} ds1
|
||||
* @param {import('../src/internals').DeleteSet} ds2
|
||||
*/
|
||||
export const compareDS = (ds1, ds2) => {
|
||||
t.assert(ds1.clients.size === ds2.clients.size)
|
||||
ds1.clients.forEach((deleteItems1, client) => {
|
||||
const deleteItems2 = /** @type {Array<import('../src/internals').DeleteItem>} */ (ds2.clients.get(client))
|
||||
t.assert(deleteItems2 !== undefined && deleteItems1.length === deleteItems2.length)
|
||||
for (let i = 0; i < deleteItems1.length; i++) {
|
||||
const di1 = deleteItems1[i]
|
||||
const di2 = deleteItems2[i]
|
||||
if (di1.clock !== di2.clock || di1.len !== di2.len) {
|
||||
t.fail('DeleteSets dont match')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @callback InitTestObjectCallback
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
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 t from 'lib0/testing'
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testInfiniteCaptureTimeout = tc => {
|
||||
const { array0 } = init(tc, { users: 3 })
|
||||
const undoManager = new Y.UndoManager(array0, { captureTimeout: Number.MAX_VALUE })
|
||||
array0.push([1, 2, 3])
|
||||
undoManager.stopCapturing()
|
||||
array0.push([4, 5, 6])
|
||||
undoManager.undo()
|
||||
t.compare(array0.toArray(), [1, 2, 3])
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
@@ -51,9 +64,23 @@ export const testUndoText = tc => {
|
||||
|
||||
/**
|
||||
* Test case to fix #241
|
||||
* @param {t.TestCase} tc
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testDoubleUndo = tc => {
|
||||
export const testEmptyTypeScope = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const um = new Y.UndoManager([], { doc: ydoc })
|
||||
const yarray = ydoc.getArray()
|
||||
um.addToScope(yarray)
|
||||
yarray.insert(0, [1])
|
||||
um.undo()
|
||||
t.assert(yarray.length === 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case to fix #241
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testDoubleUndo = _tc => {
|
||||
const doc = new Y.Doc()
|
||||
const text = doc.getText()
|
||||
text.insert(0, '1221')
|
||||
@@ -289,9 +316,9 @@ export const testUndoDeleteFilter = tc => {
|
||||
|
||||
/**
|
||||
* 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 doc2 = new Y.Doc()
|
||||
doc.on('update', update => Y.applyUpdate(doc2, update))
|
||||
@@ -320,9 +347,9 @@ export const testUndoUntilChangePerformed = tc => {
|
||||
|
||||
/**
|
||||
* 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 design = doc.getMap()
|
||||
const undoManager = new Y.UndoManager(design, { captureTimeout: 0 })
|
||||
@@ -376,9 +403,9 @@ export const testUndoNestedUndoIssue = tc => {
|
||||
/**
|
||||
* 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 yRoot = doc.getMap()
|
||||
const undoMgr = new Y.UndoManager(yRoot)
|
||||
@@ -427,9 +454,9 @@ export const testConsecutiveRedoBug = tc => {
|
||||
/**
|
||||
* 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 doc = new Y.Doc()
|
||||
const fragment = doc.getXmlFragment('t')
|
||||
@@ -472,9 +499,9 @@ export const testUndoXmlBug = tc => {
|
||||
/**
|
||||
* 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 design = doc.getMap()
|
||||
|
||||
@@ -532,9 +559,9 @@ export const testUndoBlockBug = tc => {
|
||||
* Undo text formatting delete should not corrupt peer state.
|
||||
*
|
||||
* @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 text = doc.getText()
|
||||
text.insert(0, 'Attack ships on fire off the shoulder of Orion.')
|
||||
@@ -570,9 +597,9 @@ export const testUndoDeleteTextFormat = tc => {
|
||||
* Undo text formatting delete should not corrupt peer state.
|
||||
*
|
||||
* @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 doc2 = new Y.Doc()
|
||||
doc.on('update', update => Y.applyUpdate(doc2, update, doc))
|
||||
@@ -588,3 +615,63 @@ export const testBehaviorOfIgnoreremotemapchangesProperty = tc => {
|
||||
t.assert(map1.get('x') === 2)
|
||||
t.assert(map2.get('x') === 2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Special deletion case.
|
||||
*
|
||||
* @see https://github.com/yjs/yjs/issues/447
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testSpecialDeletionCase = _tc => {
|
||||
const origin = 'undoable'
|
||||
const doc = new Y.Doc()
|
||||
const fragment = doc.getXmlFragment()
|
||||
const undoManager = new Y.UndoManager(fragment, { trackedOrigins: new Set([origin]) })
|
||||
doc.transact(() => {
|
||||
const e = new Y.XmlElement('test')
|
||||
e.setAttribute('a', '1')
|
||||
e.setAttribute('b', '2')
|
||||
fragment.insert(0, [e])
|
||||
})
|
||||
t.compareStrings(fragment.toString(), '<test a="1" b="2"></test>')
|
||||
doc.transact(() => {
|
||||
// change attribute "b" and delete test-node
|
||||
const e = fragment.get(0)
|
||||
e.setAttribute('b', '3')
|
||||
fragment.delete(0)
|
||||
}, origin)
|
||||
t.compareStrings(fragment.toString(), '')
|
||||
undoManager.undo()
|
||||
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 * as encoding from 'lib0/encoding'
|
||||
import * as decoding from 'lib0/decoding'
|
||||
import * as object from 'lib0/object'
|
||||
|
||||
/**
|
||||
* @typedef {Object} Enc
|
||||
@@ -138,7 +139,6 @@ export const testKeyEncoding = tc => {
|
||||
*/
|
||||
const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
|
||||
const cases = []
|
||||
|
||||
// Case 1: Simple case, simply merge everything
|
||||
cases.push(enc.mergeUpdates(updates))
|
||||
|
||||
@@ -304,3 +304,54 @@ export const testMergePendingUpdates = tc => {
|
||||
const yText5 = yDoc5.getText('textBlock')
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,17 @@ export const testSlice = tc => {
|
||||
t.compareArrays(arr.slice(0, 2), [0, 1])
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testArrayFrom = tc => {
|
||||
const doc1 = new Y.Doc()
|
||||
const db1 = doc1.getMap('root')
|
||||
const nestedArray1 = Y.Array.from([0, 1, 2])
|
||||
db1.set('array', nestedArray1)
|
||||
t.compare(nestedArray1.toArray(), [0, 1, 2])
|
||||
}
|
||||
|
||||
/**
|
||||
* Debugging yjs#297 - a critical bug connected to the search-marker approach
|
||||
*
|
||||
|
||||
@@ -337,6 +337,34 @@ export const testObserversUsingObservedeep = tc => {
|
||||
compare(users)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testPathsOfSiblingEvents = tc => {
|
||||
const { users, map0 } = init(tc, { users: 2 })
|
||||
/**
|
||||
* @type {Array<Array<string|number>>}
|
||||
*/
|
||||
const pathes = []
|
||||
let calls = 0
|
||||
const doc = users[0]
|
||||
map0.set('map', new Y.Map())
|
||||
map0.get('map').set('text1', new Y.Text('initial'))
|
||||
map0.observeDeep(events => {
|
||||
events.forEach(event => {
|
||||
pathes.push(event.path)
|
||||
})
|
||||
calls++
|
||||
})
|
||||
doc.transact(() => {
|
||||
map0.get('map').get('text1').insert(0, 'post-')
|
||||
map0.get('map').set('text2', new Y.Text('new'))
|
||||
})
|
||||
t.assert(calls === 1)
|
||||
t.compare(pathes, [['map'], ['map', 'text1']])
|
||||
compare(users)
|
||||
}
|
||||
|
||||
// TODO: Test events in Y.Map
|
||||
/**
|
||||
* @param {Object<string,any>} is
|
||||
@@ -455,9 +483,9 @@ export const testChangeEvent = tc => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testYmapEventExceptionsShouldCompleteTransaction = tc => {
|
||||
export const testYmapEventExceptionsShouldCompleteTransaction = _tc => {
|
||||
const doc = new Y.Doc()
|
||||
const map = doc.getMap('map')
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,33 @@ import * as Y from '../src/index.js'
|
||||
|
||||
import * as t from 'lib0/testing'
|
||||
|
||||
export const testCustomTypings = () => {
|
||||
const ydoc = new Y.Doc()
|
||||
const ymap = ydoc.getMap()
|
||||
/**
|
||||
* @type {Y.XmlElement<{ num: number, str: string, [k:string]: object|number|string }>}
|
||||
*/
|
||||
const yxml = ymap.set('yxml', new Y.XmlElement('test'))
|
||||
/**
|
||||
* @type {number|undefined}
|
||||
*/
|
||||
const num = yxml.getAttribute('num')
|
||||
/**
|
||||
* @type {string|undefined}
|
||||
*/
|
||||
const str = yxml.getAttribute('str')
|
||||
/**
|
||||
* @type {object|number|string|undefined}
|
||||
*/
|
||||
const dtrn = yxml.getAttribute('dtrn')
|
||||
const attrs = yxml.getAttributes()
|
||||
/**
|
||||
* @type {object|number|string|undefined}
|
||||
*/
|
||||
const any = attrs.shouldBeAny
|
||||
console.log({ num, str, dtrn, attrs, any })
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
@@ -92,9 +119,9 @@ export const testTreewalker = tc => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testYtextAttributes = tc => {
|
||||
export const testYtextAttributes = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const ytext = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText))
|
||||
ytext.observe(event => {
|
||||
@@ -106,9 +133,9 @@ export const testYtextAttributes = tc => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testSiblings = tc => {
|
||||
export const testSiblings = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const yxml = ydoc.getXmlFragment()
|
||||
const first = new Y.XmlText()
|
||||
@@ -122,9 +149,9 @@ export const testSiblings = tc => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testInsertafter = tc => {
|
||||
export const testInsertafter = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const yxml = ydoc.getXmlFragment()
|
||||
const first = new Y.XmlText()
|
||||
@@ -152,9 +179,9 @@ export const testInsertafter = tc => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testClone = tc => {
|
||||
export const testClone = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const yxml = ydoc.getXmlFragment()
|
||||
const first = new Y.XmlText('text')
|
||||
@@ -170,9 +197,9 @@ export const testClone = tc => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testFormattingBug = tc => {
|
||||
export const testFormattingBug = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const yxml = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText))
|
||||
const delta = [
|
||||
|
||||
@@ -1,64 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Basic Options */
|
||||
"target": "es2018",
|
||||
"lib": ["es2018", "dom"], /* Specify library files to be included in the compilation. */
|
||||
"allowJs": true, /* Allow javascript files to be compiled. */
|
||||
"checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||
"declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "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. */
|
||||
"target": "ES2021",
|
||||
"lib": ["ES2021", "dom"],
|
||||
"module": "node16",
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"emitDeclarationOnly": true,
|
||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||
// "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. */
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "nodenext",
|
||||
"paths": {
|
||||
"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"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user