├── .gitignore
├── LICENSE
├── README.md
├── Ycs.sln
├── docs
└── ycs.gif
├── samples
└── YcsSample
│ ├── .gitignore
│ ├── ClientApp
│ ├── .gitignore
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── logo192.png
│ │ ├── logo512.png
│ │ ├── manifest.json
│ │ ├── monaco-editor-worker-loader-proxy.js
│ │ └── robots.txt
│ ├── src
│ │ ├── app.tsx
│ │ ├── components
│ │ │ ├── monacoEditor.tsx
│ │ │ └── proseMirror.tsx
│ │ ├── context
│ │ │ └── yjsContext.tsx
│ │ ├── hooks
│ │ │ └── useYjs.ts
│ │ ├── impl
│ │ │ └── yjsSignalrConnector.ts
│ │ ├── index.tsx
│ │ ├── react-app-env.d.ts
│ │ ├── styles
│ │ │ └── index.css
│ │ └── util
│ │ │ ├── encodingUtils.ts
│ │ │ └── reportWebVitals.ts
│ └── tsconfig.json
│ ├── Hubs
│ └── YcsHub.cs
│ ├── Middleware
│ └── YcsHubAccessor.cs
│ ├── Pages
│ ├── Error.cshtml
│ ├── Error.cshtml.cs
│ └── _ViewImports.cshtml
│ ├── Program.cs
│ ├── Properties
│ └── launchSettings.json
│ ├── Startup.cs
│ ├── YcsSample.csproj
│ ├── Yjs
│ └── YcsManager.cs
│ ├── appsettings.Development.json
│ └── appsettings.json
├── src
└── Ycs
│ ├── AssemblyAttributes.cs
│ ├── Protocols
│ └── SyncProtocol.cs
│ ├── Structs
│ ├── AbstractStruct.cs
│ ├── ContentAny.cs
│ ├── ContentBinary.cs
│ ├── ContentDeleted.cs
│ ├── ContentDoc.cs
│ ├── ContentEmbed.cs
│ ├── ContentFormat.cs
│ ├── ContentJson.cs
│ ├── ContentString.cs
│ ├── ContentType.cs
│ ├── GC.cs
│ ├── IContent.cs
│ └── Item.cs
│ ├── Types
│ ├── AbstractType.cs
│ ├── YArray.cs
│ ├── YArrayBase.cs
│ ├── YMap.cs
│ └── YText.cs
│ ├── Utils
│ ├── AbsolutePosition.cs
│ ├── DeleteSet.cs
│ ├── EncodingUtils.cs
│ ├── ID.cs
│ ├── IUpdateDecoder.cs
│ ├── IUpdateEncoder.cs
│ ├── RelativePosition.cs
│ ├── Snapshot.cs
│ ├── StructStore.cs
│ ├── Transaction.cs
│ ├── UndoManager.cs
│ ├── UpdateDecoderV2.cs
│ ├── UpdateEncoderV2.cs
│ ├── YDoc.cs
│ └── YEvent.cs
│ ├── Ycs.csproj
│ └── lib0
│ ├── Decoding
│ ├── AbstractStreamDecoder.cs
│ ├── IDecoder.cs
│ ├── IncUintOptRleDecoder.cs
│ ├── IntDiffDecoder.cs
│ ├── IntDiffOptRleDecoder.cs
│ ├── RleDecoder.cs
│ ├── RleIntDiffDecoder.cs
│ ├── StringDecoder.cs
│ └── UintOptRleDecoder.cs
│ ├── Encoding
│ ├── AbstractStreamEncoder.cs
│ ├── IEncoder.cs
│ ├── IncUintOptRleEncoder.cs
│ ├── IntDiffEncoder.cs
│ ├── IntDiffOptRleEncoder.cs
│ ├── RleEncoder.cs
│ ├── RleIntDiffEncoder.cs
│ ├── StringEncoder.cs
│ └── UintOptRleEncoder.cs
│ ├── NativeEnums.cs
│ ├── StreamDecodingExtensions.cs
│ └── StreamEncodingExtensions.cs
└── tests
├── Ycs.Benchmarks
├── BenchmarkTests.cs
├── Program.cs
└── Ycs.Benchmarks.csproj
└── Ycs.Tests
├── EncodingTests.cs
├── RelativePositionTests.cs
├── RleEncodingTests.cs
├── SnapshotTests.cs
├── TestConnector.cs
├── TestYInstance.cs
├── UndoRedoTests.cs
├── YArrayTests.cs
├── YDocTests.cs
├── YMapTests.cs
├── YTestBase.cs
├── YTextTests.cs
└── Ycs.Tests.csproj
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | node_modules/
4 | .vscode/chrome
5 | .babel-cache-shell
6 | .temp
7 | .vs/
8 | bin/
9 | obj/
10 | TestResults/
11 |
12 | tsconfig.tsbuildinfo
13 | npm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
16 | lerna-debug.log*
17 | pnpm-debug.log
18 |
19 | # PCF-Controls
20 | .suo
21 | out/
22 | obj/
23 |
24 | # ESLint cache
25 | .eslintcache
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Yjs
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Ycs
2 | -------
3 |
4 | A compatible `.Net` implementation of the [Yjs](https://github.com/yjs/yjs) CRDT framework.
5 |
6 | With this, you can host CRDTs in your `.Net` application and synchronize them with the existing `Yjs` models running elsewhere.
7 |
8 | #### Latest tested Yjs version: [13.4.14](https://github.com/yjs/yjs/releases/tag/v13.4.14).
9 |
10 | Supports [Y.Array, Y.Map, Y.Text](https://github.com/yjs/yjs#shared-types), but does not yet support `Y.Xml` types.
11 |
12 | Demo
13 | -------
14 |
15 | Client: [`Yjs`](https://github.com/yjs/yjs), [`MonacoEditor`](https://github.com/microsoft/monaco-editor), [`SignalR`](https://github.com/dotnet/aspnetcore/tree/master/src/SignalR).
16 |
17 | Server: `Ycs`, `SignalR`, [`AspNetCore`](https://github.com/dotnet/aspnetcore).
18 |
19 | 
20 |
--------------------------------------------------------------------------------
/Ycs.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.30611.23
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ycs", "src\Ycs\Ycs.csproj", "{DFF8A62E-F42A-4974-A9E5-7B180C989762}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ycs.Tests", "tests\Ycs.Tests\Ycs.Tests.csproj", "{1D60757D-1CAC-4616-A6AC-52F47D285C29}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YcsSample", "samples\YcsSample\YcsSample.csproj", "{83B32035-B539-4CFC-BA34-E2568BE0918D}"
11 | EndProject
12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ycs.Benchmarks", "tests\Ycs.Benchmarks\Ycs.Benchmarks.csproj", "{DDBA8F8C-A8D3-4C23-A36B-C2DD0BBAB10C}"
13 | EndProject
14 | Global
15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
16 | Debug|Any CPU = Debug|Any CPU
17 | Release|Any CPU = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
20 | {DFF8A62E-F42A-4974-A9E5-7B180C989762}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {DFF8A62E-F42A-4974-A9E5-7B180C989762}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {DFF8A62E-F42A-4974-A9E5-7B180C989762}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {DFF8A62E-F42A-4974-A9E5-7B180C989762}.Release|Any CPU.Build.0 = Release|Any CPU
24 | {1D60757D-1CAC-4616-A6AC-52F47D285C29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
25 | {1D60757D-1CAC-4616-A6AC-52F47D285C29}.Debug|Any CPU.Build.0 = Debug|Any CPU
26 | {1D60757D-1CAC-4616-A6AC-52F47D285C29}.Release|Any CPU.ActiveCfg = Release|Any CPU
27 | {1D60757D-1CAC-4616-A6AC-52F47D285C29}.Release|Any CPU.Build.0 = Release|Any CPU
28 | {83B32035-B539-4CFC-BA34-E2568BE0918D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
29 | {83B32035-B539-4CFC-BA34-E2568BE0918D}.Debug|Any CPU.Build.0 = Debug|Any CPU
30 | {83B32035-B539-4CFC-BA34-E2568BE0918D}.Release|Any CPU.ActiveCfg = Release|Any CPU
31 | {83B32035-B539-4CFC-BA34-E2568BE0918D}.Release|Any CPU.Build.0 = Release|Any CPU
32 | {DDBA8F8C-A8D3-4C23-A36B-C2DD0BBAB10C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33 | {DDBA8F8C-A8D3-4C23-A36B-C2DD0BBAB10C}.Debug|Any CPU.Build.0 = Debug|Any CPU
34 | {DDBA8F8C-A8D3-4C23-A36B-C2DD0BBAB10C}.Release|Any CPU.ActiveCfg = Release|Any CPU
35 | {DDBA8F8C-A8D3-4C23-A36B-C2DD0BBAB10C}.Release|Any CPU.Build.0 = Release|Any CPU
36 | EndGlobalSection
37 | GlobalSection(SolutionProperties) = preSolution
38 | HideSolutionNode = FALSE
39 | EndGlobalSection
40 | GlobalSection(ExtensibilityGlobals) = postSolution
41 | SolutionGuid = {3D7D58AD-F6FB-4263-A6DA-C9D0E655D3C2}
42 | EndGlobalSection
43 | EndGlobal
44 |
--------------------------------------------------------------------------------
/docs/ycs.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yjs/ycs/c5382e5fe073e01b4bd262aabcc223bfafee2508/docs/ycs.gif
--------------------------------------------------------------------------------
/samples/YcsSample/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.userosscache
8 | *.sln.docstates
9 |
10 | # User-specific files (MonoDevelop/Xamarin Studio)
11 | *.userprefs
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | [Rr]eleases/
18 | x64/
19 | x86/
20 | build/
21 | bld/
22 | bin/
23 | Bin/
24 | obj/
25 | Obj/
26 |
27 | # Visual Studio 2015 cache/options directory
28 | .vs/
29 | /wwwroot/dist/
30 |
31 | # MSTest test Results
32 | [Tt]est[Rr]esult*/
33 | [Bb]uild[Ll]og.*
34 |
35 | # NUNIT
36 | *.VisualState.xml
37 | TestResult.xml
38 |
39 | # Build Results of an ATL Project
40 | [Dd]ebugPS/
41 | [Rr]eleasePS/
42 | dlldata.c
43 |
44 | *_i.c
45 | *_p.c
46 | *_i.h
47 | *.ilk
48 | *.meta
49 | *.obj
50 | *.pch
51 | *.pdb
52 | *.pgc
53 | *.pgd
54 | *.rsp
55 | *.sbr
56 | *.tlb
57 | *.tli
58 | *.tlh
59 | *.tmp
60 | *.tmp_proj
61 | *.log
62 | *.vspscc
63 | *.vssscc
64 | .builds
65 | *.pidb
66 | *.svclog
67 | *.scc
68 |
69 | # Chutzpah Test files
70 | _Chutzpah*
71 |
72 | # Visual C++ cache files
73 | ipch/
74 | *.aps
75 | *.ncb
76 | *.opendb
77 | *.opensdf
78 | *.sdf
79 | *.cachefile
80 |
81 | # Visual Studio profiler
82 | *.psess
83 | *.vsp
84 | *.vspx
85 | *.sap
86 |
87 | # TFS 2012 Local Workspace
88 | $tf/
89 |
90 | # Guidance Automation Toolkit
91 | *.gpState
92 |
93 | # ReSharper is a .NET coding add-in
94 | _ReSharper*/
95 | *.[Rr]e[Ss]harper
96 | *.DotSettings.user
97 |
98 | # JustCode is a .NET coding add-in
99 | .JustCode
100 |
101 | # TeamCity is a build add-in
102 | _TeamCity*
103 |
104 | # DotCover is a Code Coverage Tool
105 | *.dotCover
106 |
107 | # NCrunch
108 | _NCrunch_*
109 | .*crunch*.local.xml
110 | nCrunchTemp_*
111 |
112 | # MightyMoose
113 | *.mm.*
114 | AutoTest.Net/
115 |
116 | # Web workbench (sass)
117 | .sass-cache/
118 |
119 | # Installshield output folder
120 | [Ee]xpress/
121 |
122 | # DocProject is a documentation generator add-in
123 | DocProject/buildhelp/
124 | DocProject/Help/*.HxT
125 | DocProject/Help/*.HxC
126 | DocProject/Help/*.hhc
127 | DocProject/Help/*.hhk
128 | DocProject/Help/*.hhp
129 | DocProject/Help/Html2
130 | DocProject/Help/html
131 |
132 | # Click-Once directory
133 | publish/
134 |
135 | # Publish Web Output
136 | *.[Pp]ublish.xml
137 | *.azurePubxml
138 | # TODO: Comment the next line if you want to checkin your web deploy settings
139 | # but database connection strings (with potential passwords) will be unencrypted
140 | *.pubxml
141 | *.publishproj
142 |
143 | # NuGet Packages
144 | *.nupkg
145 | # The packages folder can be ignored because of Package Restore
146 | **/packages/*
147 | # except build/, which is used as an MSBuild target.
148 | !**/packages/build/
149 | # Uncomment if necessary however generally it will be regenerated when needed
150 | #!**/packages/repositories.config
151 |
152 | # Microsoft Azure Build Output
153 | csx/
154 | *.build.csdef
155 |
156 | # Microsoft Azure Emulator
157 | ecf/
158 | rcf/
159 |
160 | # Microsoft Azure ApplicationInsights config file
161 | ApplicationInsights.config
162 |
163 | # Windows Store app package directory
164 | AppPackages/
165 | BundleArtifacts/
166 |
167 | # Visual Studio cache files
168 | # files ending in .cache can be ignored
169 | *.[Cc]ache
170 | # but keep track of directories ending in .cache
171 | !*.[Cc]ache/
172 |
173 | # Others
174 | ClientBin/
175 | ~$*
176 | *~
177 | *.dbmdl
178 | *.dbproj.schemaview
179 | *.pfx
180 | *.publishsettings
181 | orleans.codegen.cs
182 |
183 | /node_modules
184 |
185 | # RIA/Silverlight projects
186 | Generated_Code/
187 |
188 | # Backup & report files from converting an old project file
189 | # to a newer Visual Studio version. Backup files are not needed,
190 | # because we have git ;-)
191 | _UpgradeReport_Files/
192 | Backup*/
193 | UpgradeLog*.XML
194 | UpgradeLog*.htm
195 |
196 | # SQL Server files
197 | *.mdf
198 | *.ldf
199 |
200 | # Business Intelligence projects
201 | *.rdl.data
202 | *.bim.layout
203 | *.bim_*.settings
204 |
205 | # Microsoft Fakes
206 | FakesAssemblies/
207 |
208 | # GhostDoc plugin setting file
209 | *.GhostDoc.xml
210 |
211 | # Node.js Tools for Visual Studio
212 | .ntvs_analysis.dat
213 |
214 | # Visual Studio 6 build log
215 | *.plg
216 |
217 | # Visual Studio 6 workspace options file
218 | *.opt
219 |
220 | # Visual Studio LightSwitch build output
221 | **/*.HTMLClient/GeneratedArtifacts
222 | **/*.DesktopClient/GeneratedArtifacts
223 | **/*.DesktopClient/ModelManifest.xml
224 | **/*.Server/GeneratedArtifacts
225 | **/*.Server/ModelManifest.xml
226 | _Pvt_Extensions
227 |
228 | # Paket dependency manager
229 | .paket/paket.exe
230 |
231 | # FAKE - F# Make
232 | .fake/
233 |
--------------------------------------------------------------------------------
/samples/YcsSample/ClientApp/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/samples/YcsSample/ClientApp/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ycs-sample",
3 | "version": "0.0.1",
4 | "private": true,
5 | "dependencies": {
6 | "@microsoft/signalr": "^5.0.2",
7 | "bootstrap": "^4.6.0",
8 | "lib0": "0.2.35",
9 | "monaco-editor": "0.22.3",
10 | "prosemirror-keymap": "^1.1.4",
11 | "prosemirror-schema-basic": "^1.1.2",
12 | "prosemirror-view": "^1.17.5",
13 | "react": "^17.0.1",
14 | "react-dom": "^17.0.1",
15 | "react-monaco-editor": "^0.42.0",
16 | "react-router-dom": "^5.2.0",
17 | "reactstrap": "^8.9.0",
18 | "uuid": "8.3.1",
19 | "web-vitals": "^1.0.1",
20 | "y-monaco": "^0.1.2",
21 | "y-prosemirror": "^1.0.5",
22 | "y-protocols": "^1.0.3",
23 | "yjs": "13.4.14"
24 | },
25 | "scripts": {
26 | "start": "react-scripts start",
27 | "build": "react-scripts build",
28 | "eject": "react-scripts eject"
29 | },
30 | "eslintConfig": {
31 | "extends": [
32 | "react-app"
33 | ]
34 | },
35 | "browserslist": {
36 | "production": [
37 | ">0.2%",
38 | "not dead",
39 | "not op_mini all"
40 | ],
41 | "development": [
42 | "last 1 chrome version",
43 | "last 1 firefox version",
44 | "last 1 safari version"
45 | ]
46 | },
47 | "devDependencies": {
48 | "@types/react": "^17.0.1",
49 | "@types/react-dom": "^17.0.0",
50 | "@types/react-router": "^5.1.11",
51 | "@types/react-router-dom": "^5.1.7",
52 | "@types/node": "^12.0.0",
53 | "react-scripts": "4.0.2",
54 | "typescript": "^4.1.2"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/samples/YcsSample/ClientApp/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yjs/ycs/c5382e5fe073e01b4bd262aabcc223bfafee2508/samples/YcsSample/ClientApp/public/favicon.ico
--------------------------------------------------------------------------------
/samples/YcsSample/ClientApp/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
24 | Ycs Sample
25 |
26 |
27 | You need to enable JavaScript to run this app.
28 |
29 |
44 |
45 |
46 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/samples/YcsSample/ClientApp/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yjs/ycs/c5382e5fe073e01b4bd262aabcc223bfafee2508/samples/YcsSample/ClientApp/public/logo192.png
--------------------------------------------------------------------------------
/samples/YcsSample/ClientApp/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yjs/ycs/c5382e5fe073e01b4bd262aabcc223bfafee2508/samples/YcsSample/ClientApp/public/logo512.png
--------------------------------------------------------------------------------
/samples/YcsSample/ClientApp/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/samples/YcsSample/ClientApp/public/monaco-editor-worker-loader-proxy.js:
--------------------------------------------------------------------------------
1 | self.MonacoEnvironment = {
2 | baseUrl: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.22.3/min/'
3 | }
4 |
5 | importScripts('https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.22.3/min/vs/base/worker/workerMain.js') // eslint-disable-line
6 |
--------------------------------------------------------------------------------
/samples/YcsSample/ClientApp/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/samples/YcsSample/ClientApp/src/app.tsx:
--------------------------------------------------------------------------------
1 | import { Route } from 'react-router';
2 | import { Navbar, Nav, NavItem, NavLink } from 'reactstrap';
3 | import { Link } from 'react-router-dom';
4 | import { YjsMonacoEditor } from './components/monacoEditor';
5 |
6 | export const App = () => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | Monaco Editor
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/samples/YcsSample/ClientApp/src/components/monacoEditor.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as yMonaco from 'y-monaco';
3 | import MonacoEditor, { monaco } from 'react-monaco-editor';
4 | import { useYjs } from '../hooks/useYjs';
5 |
6 | export const YjsMonacoEditor = () => {
7 | const { yDoc, yjsConnector } = useYjs();
8 | const yText = yDoc.getText('monaco');
9 |
10 | const setMonacoEditor = React.useState()[1];
11 | const setMonacoBinding = React.useState()[1];
12 |
13 | const _onEditorDidMount = React.useCallback(
14 | (editor: monaco.editor.ICodeEditor, monacoParam: typeof monaco): void => {
15 | editor.focus();
16 | editor.setValue('');
17 |
18 | setMonacoEditor(editor);
19 | setMonacoBinding(new yMonaco.MonacoBinding(yText, editor.getModel(), new Set([editor]), yjsConnector.awareness));
20 | },
21 | [yjsConnector.awareness, yText, setMonacoEditor, setMonacoBinding]
22 | );
23 |
24 | return (
25 |
26 | _onEditorDidMount(e, a)}
34 | />
35 |
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/samples/YcsSample/ClientApp/src/components/proseMirror.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { schema } from 'prosemirror-schema-basic';
3 | import { EditorState } from 'prosemirror-state';
4 | import { EditorView } from 'prosemirror-view';
5 | import { keymap } from 'prosemirror-keymap';
6 | import { ySyncPlugin, yCursorPlugin, yUndoPlugin, undo, redo } from 'y-prosemirror';
7 | import { useYjs } from '../hooks/useYjs';
8 |
9 | export const ProseMirror = (props) => {
10 | const { yDoc, yjsConnector } = useYjs();
11 | const yText = yDoc.getText('prosemirror');
12 |
13 | const viewHost = React.useRef(null);
14 | const view = React.useRef(null);
15 |
16 | React.useEffect(() => {
17 | const state = EditorState.create({
18 | schema,
19 | plugins: [
20 | ySyncPlugin(yText),
21 | yCursorPlugin(yjsConnector.awareness),
22 | yUndoPlugin(),
23 | keymap({
24 | 'Mod-z': undo,
25 | 'Mod-y': redo,
26 | 'Mod-Shift-z': redo
27 | })
28 | ]
29 | });
30 |
31 | view.current = new EditorView(viewHost.current, { state });
32 | return () => (view.current as any)?.destroy();
33 | }, []);
34 |
35 | return (
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/samples/YcsSample/ClientApp/src/context/yjsContext.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as Y from 'yjs';
3 | import { HubConnectionBuilder } from '@microsoft/signalr';
4 | import { YjsSignalrConnector } from '../impl/yjsSignalrConnector';
5 |
6 | export interface IYjsContext {
7 | readonly yDoc: Y.Doc;
8 | readonly yjsConnector: YjsSignalrConnector;
9 | }
10 |
11 | export interface IOptions extends React.PropsWithChildren<{}> {
12 | readonly baseUrl: string;
13 | }
14 |
15 | export const YjsContextProvider: React.FunctionComponent = (props: IOptions) => {
16 | const { baseUrl } = props;
17 |
18 | const contextProps: IYjsContext = React.useMemo(() => {
19 | const yDoc = new Y.Doc();
20 |
21 | // Initiate the connection. Doing it here to simplify the example code.
22 | const connection = new HubConnectionBuilder()
23 | .withUrl(baseUrl)
24 | .withAutomaticReconnect()
25 | .build();
26 | const connectedPromise = connection.start();
27 | const yjsConnector = new YjsSignalrConnector(yDoc, connection, connectedPromise);
28 |
29 | return { yDoc, yjsConnector };
30 | }, [baseUrl]);
31 |
32 | return {props.children} ;
33 | };
34 |
35 | export const YjsContext = React.createContext(undefined);
36 |
--------------------------------------------------------------------------------
/samples/YcsSample/ClientApp/src/hooks/useYjs.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as Y from 'yjs';
3 | import { YjsContext } from '../context/yjsContext';
4 | import { YjsSignalrConnector } from '../impl/yjsSignalrConnector';
5 |
6 | export function useYjs(): { yDoc: Y.Doc, yjsConnector: YjsSignalrConnector } {
7 | const yjsContext = React.useContext(YjsContext);
8 | if (yjsContext === undefined) {
9 | throw new Error('useYjs() should be called with the YjsContext defined.');
10 | }
11 |
12 | return {
13 | yDoc: yjsContext.yDoc,
14 | yjsConnector: yjsContext.yjsConnector
15 | };
16 | }
17 |
--------------------------------------------------------------------------------
/samples/YcsSample/ClientApp/src/index.tsx:
--------------------------------------------------------------------------------
1 | import './styles/index.css';
2 | import 'bootstrap/dist/css/bootstrap.min.css';
3 | import React from 'react';
4 | import ReactDOM from 'react-dom';
5 | import { BrowserRouter } from 'react-router-dom';
6 | import { App } from './app';
7 | import { YjsContextProvider } from './context/yjsContext';
8 | import reportWebVitals from './util/reportWebVitals';
9 |
10 | const baseUrl = document.getElementsByTagName('base')[0].getAttribute('href') ?? undefined;
11 | const rootElement = document.getElementById('root');
12 |
13 | ReactDOM.render(
14 |
15 |
16 |
17 |
18 |
19 |
20 | ,
21 | rootElement
22 | );
23 |
24 | // If you want to start measuring performance in your app, pass a function
25 | // to log results (for example: reportWebVitals(console.log))
26 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
27 | reportWebVitals();
28 |
--------------------------------------------------------------------------------
/samples/YcsSample/ClientApp/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/samples/YcsSample/ClientApp/src/styles/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
15 | .yRemoteSelection {
16 | background-color: rgb(250, 129, 0, .5)
17 | }
18 |
19 | .yRemoteSelectionHead {
20 | position: absolute;
21 | border-left: orange solid 2px;
22 | border-top: orange solid 2px;
23 | border-bottom: orange solid 2px;
24 | height: 100%;
25 | box-sizing: border-box;
26 | }
27 |
28 | .yRemoteSelectionHead::after {
29 | position: absolute;
30 | content: ' ';
31 | border: 3px solid orange;
32 | border-radius: 4px;
33 | left: -4px;
34 | top: -5px;
35 | }
36 |
--------------------------------------------------------------------------------
/samples/YcsSample/ClientApp/src/util/encodingUtils.ts:
--------------------------------------------------------------------------------
1 | export function stringToByteArray(str: string): Uint8Array {
2 | const binaryStr = atob(str);
3 | const len = binaryStr.length;
4 | const bytes = new Uint8Array(len);
5 |
6 | for (let i = 0; i < len; i++) {
7 | bytes[i] = binaryStr.charCodeAt(i);
8 | }
9 |
10 | return bytes;
11 | }
12 |
13 | export function byteArrayToString(u8: Uint8Array): string {
14 | let str: string = '';
15 |
16 | u8.forEach(byte => {
17 | str += String.fromCharCode(byte);
18 | });
19 |
20 | return btoa(str);
21 | }
22 |
--------------------------------------------------------------------------------
/samples/YcsSample/ClientApp/src/util/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/samples/YcsSample/ClientApp/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx",
22 | "noImplicitAny": false
23 | },
24 | "include": [
25 | "src"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/samples/YcsSample/Hubs/YcsHub.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text.Json;
3 | using System.Text.Json.Serialization;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.SignalR;
6 | using YcsSample.Yjs;
7 |
8 | namespace YcsSample.Hubs
9 | {
10 | public class YcsHub : Hub
11 | {
12 | private class YjsMessage
13 | {
14 | [JsonPropertyName("clock")]
15 | public long Clock { get; set; }
16 |
17 | [JsonPropertyName("data")]
18 | public string Data { get; set; }
19 |
20 | [JsonPropertyName("inReplyTo")]
21 | public YjsCommandType? InReplyTo { get; set; }
22 | }
23 |
24 | public override async Task OnConnectedAsync()
25 | {
26 | YcsManager.Instance.HandleClientConnected(Context.ConnectionId);
27 | await base.OnConnectedAsync();
28 | }
29 |
30 | public override async Task OnDisconnectedAsync(Exception exception)
31 | {
32 | YcsManager.Instance.HandleClientDisconnected(Context.ConnectionId);
33 | await base.OnDisconnectedAsync(exception);
34 | }
35 |
36 | // SyncStep1
37 | public async Task GetMissing(string data)
38 | {
39 | var yjsMessage = JsonSerializer.Deserialize(data);
40 |
41 | var messageToProcess = new MessageToProcess
42 | {
43 | Command = YjsCommandType.GetMissing,
44 | InReplyTo = yjsMessage.InReplyTo,
45 | Data = yjsMessage.Data
46 | };
47 |
48 | await YcsManager.Instance.EnqueueAndProcessMessagesAsync(Context.ConnectionId, yjsMessage.Clock, messageToProcess, Context.ConnectionAborted);
49 | }
50 |
51 | // SyncStep2
52 | public async Task Update(string data)
53 | {
54 | var yjsMessage = JsonSerializer.Deserialize(data);
55 |
56 | var messageToProcess = new MessageToProcess
57 | {
58 | Command = YjsCommandType.Update,
59 | InReplyTo = yjsMessage.InReplyTo,
60 | Data = yjsMessage.Data
61 | };
62 |
63 | await YcsManager.Instance.EnqueueAndProcessMessagesAsync(Context.ConnectionId, yjsMessage.Clock, messageToProcess, Context.ConnectionAborted);
64 | }
65 |
66 | public async Task QueryAwareness(string data)
67 | {
68 | await Clients.Others.SendAsync(nameof(QueryAwareness), data, Context.ConnectionAborted);
69 | }
70 |
71 | public async Task UpdateAwareness(string data)
72 | {
73 | await Clients.Others.SendAsync(nameof(UpdateAwareness), data, Context.ConnectionAborted);
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/samples/YcsSample/Middleware/YcsHubAccessor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.AspNetCore.Builder;
3 | using Microsoft.AspNetCore.SignalR;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using YcsSample.Hubs;
6 |
7 | namespace YcsSample.Middleware
8 | {
9 | public class YcsHubAccessor
10 | {
11 | private static readonly Lazy _instance = new Lazy(() => new YcsHubAccessor());
12 |
13 | private YcsHubAccessor()
14 | {
15 | // Do nothing.
16 | }
17 |
18 | public static YcsHubAccessor Instance => _instance.Value;
19 |
20 | public IHubContext YcsHub { get; internal set; }
21 | }
22 |
23 | public static class YcsHubAccessorMiddlewareExtensions
24 | {
25 | public static IApplicationBuilder UseYcsHubAccessor(this IApplicationBuilder appBuilder)
26 | {
27 | return appBuilder.Use(async (context, next) =>
28 | {
29 | YcsHubAccessor.Instance.YcsHub = context.RequestServices.GetRequiredService>();
30 |
31 | if (next != null)
32 | {
33 | await next.Invoke();
34 | }
35 | });
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/samples/YcsSample/Pages/Error.cshtml:
--------------------------------------------------------------------------------
1 | @page
2 | @model ErrorModel
3 | @{
4 | ViewData["Title"] = "Error";
5 | }
6 |
7 | Error.
8 | An error occurred while processing your request.
9 |
10 | @if (Model.ShowRequestId)
11 | {
12 |
13 | Request ID: @Model.RequestId
14 |
15 | }
16 |
17 | Development Mode
18 |
19 | Swapping to the Development environment displays detailed information about the error that occurred.
20 |
21 |
22 | The Development environment shouldn't be enabled for deployed applications.
23 | It can result in displaying sensitive information from exceptions to end users.
24 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development
25 | and restarting the app.
26 |
27 |
--------------------------------------------------------------------------------
/samples/YcsSample/Pages/Error.cshtml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 | using Microsoft.AspNetCore.Mvc;
7 | using Microsoft.AspNetCore.Mvc.RazorPages;
8 | using Microsoft.Extensions.Logging;
9 |
10 | namespace YcsSample.Pages
11 | {
12 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
13 | public class ErrorModel : PageModel
14 | {
15 | private readonly ILogger _logger;
16 |
17 | public ErrorModel(ILogger logger)
18 | {
19 | _logger = logger;
20 | }
21 |
22 | public string RequestId { get; set; }
23 |
24 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
25 |
26 | public void OnGet()
27 | {
28 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/samples/YcsSample/Pages/_ViewImports.cshtml:
--------------------------------------------------------------------------------
1 | @using YcsSample
2 | @namespace YcsSample.Pages
3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
4 |
--------------------------------------------------------------------------------
/samples/YcsSample/Program.cs:
--------------------------------------------------------------------------------
1 |
2 |
3 | using Microsoft.AspNetCore.Hosting;
4 | using Microsoft.Extensions.Hosting;
5 |
6 | namespace YcsSample
7 | {
8 | public class Program
9 | {
10 | public static void Main(string[] args)
11 | {
12 | CreateHostBuilder(args).Build().Run();
13 | }
14 |
15 | public static IHostBuilder CreateHostBuilder(string[] args) =>
16 | Host.CreateDefaultBuilder(args)
17 | .ConfigureWebHostDefaults(webBuilder =>
18 | {
19 | webBuilder.UseStartup();
20 | });
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/samples/YcsSample/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true,
5 | "iisExpress": {
6 | "applicationUrl": "http://localhost:2462",
7 | "sslPort": 44366
8 | }
9 | },
10 | "profiles": {
11 | "IIS Express": {
12 | "commandName": "IISExpress",
13 | "launchBrowser": true,
14 | "environmentVariables": {
15 | "ASPNETCORE_ENVIRONMENT": "Development"
16 | }
17 | },
18 | "YcsSample": {
19 | "commandName": "Project",
20 | "launchBrowser": true,
21 | "applicationUrl": "https://localhost:5001;http://localhost:5000",
22 | "environmentVariables": {
23 | "ASPNETCORE_ENVIRONMENT": "Development"
24 | }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/samples/YcsSample/Startup.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.AspNetCore.Hosting;
3 | using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer;
4 | using Microsoft.Extensions.Configuration;
5 | using Microsoft.Extensions.DependencyInjection;
6 | using Microsoft.Extensions.Hosting;
7 | using YcsSample.Hubs;
8 | using YcsSample.Middleware;
9 |
10 | namespace YcsSample
11 | {
12 | public class Startup
13 | {
14 | public Startup(IConfiguration configuration)
15 | {
16 | Configuration = configuration;
17 | }
18 |
19 | public IConfiguration Configuration { get; }
20 |
21 | // This method gets called by the runtime. Use this method to add services to the container.
22 | public void ConfigureServices(IServiceCollection services)
23 | {
24 | services.AddSignalR();
25 | services.AddControllersWithViews();
26 |
27 | // In production, the React files will be served from this directory
28 | services.AddSpaStaticFiles(configuration =>
29 | {
30 | configuration.RootPath = "ClientApp/build";
31 | });
32 | }
33 |
34 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
35 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
36 | {
37 | if (env.IsDevelopment())
38 | {
39 | app.UseDeveloperExceptionPage();
40 | }
41 | else
42 | {
43 | app.UseExceptionHandler("/Error");
44 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
45 | app.UseHsts();
46 | }
47 |
48 | app.UseHttpsRedirection();
49 | app.UseStaticFiles();
50 | app.UseSpaStaticFiles();
51 |
52 | // Allows YcsManager to broadcast YDocument changes made by the current process.
53 | app.UseYcsHubAccessor();
54 |
55 | app.UseRouting();
56 |
57 | app.UseEndpoints(endpoints =>
58 | {
59 | endpoints.MapHub("/hubs/ycs");
60 |
61 | endpoints.MapControllerRoute(
62 | name: "default",
63 | pattern: "{controller}/{action=Index}/{id?}");
64 | });
65 |
66 | app.UseSpa(spa =>
67 | {
68 | spa.Options.SourcePath = "ClientApp";
69 |
70 | if (env.IsDevelopment())
71 | {
72 | spa.UseReactDevelopmentServer(npmScript: "start");
73 | }
74 | });
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/samples/YcsSample/YcsSample.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.1
5 | true
6 | Latest
7 | false
8 | ClientApp\
9 | $(DefaultItemExcludes);$(SpaRoot)node_modules\**
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | %(DistFiles.Identity)
47 | PreserveNewest
48 | true
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/samples/YcsSample/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/samples/YcsSample/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "AllowedHosts": "*"
10 | }
11 |
--------------------------------------------------------------------------------
/src/Ycs/AssemblyAttributes.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 | [assembly: InternalsVisibleTo("Ycs.Benchmarks")]
3 | [assembly: InternalsVisibleTo("Ycs.Tests")]
4 |
--------------------------------------------------------------------------------
/src/Ycs/Protocols/SyncProtocol.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System;
8 | using System.IO;
9 |
10 | namespace Ycs
11 | {
12 | internal static class SyncProtocol
13 | {
14 | public const uint MessageYjsSyncStep1 = 0;
15 | public const uint MessageYjsSyncStep2 = 1;
16 | public const uint MessageYjsUpdate = 2;
17 |
18 | public static void WriteSyncStep1(Stream stream, YDoc doc)
19 | {
20 | stream.WriteVarUint(MessageYjsSyncStep1);
21 | var sv = doc.EncodeStateVectorV2();
22 | stream.WriteVarUint8Array(sv);
23 | }
24 |
25 | public static void WriteSyncStep2(Stream stream, YDoc doc, byte[] encodedStateVector)
26 | {
27 | stream.WriteVarUint(MessageYjsSyncStep2);
28 | var update = doc.EncodeStateAsUpdateV2(encodedStateVector);
29 | stream.WriteVarUint8Array(update);
30 | }
31 |
32 | public static void ReadSyncStep1(Stream reader, Stream writer, YDoc doc)
33 | {
34 | var encodedStateVector = reader.ReadVarUint8Array();
35 | WriteSyncStep2(writer, doc, encodedStateVector);
36 | }
37 |
38 | public static void ReadSyncStep2(Stream stream, YDoc doc, object transactionOrigin)
39 | {
40 | var update = stream.ReadVarUint8Array();
41 | doc.ApplyUpdateV2(update, transactionOrigin);
42 | }
43 |
44 | public static void WriteUpdate(Stream stream, byte[] update)
45 | {
46 | stream.WriteVarUint(MessageYjsUpdate);
47 | stream.WriteVarUint8Array(update);
48 | }
49 |
50 | public static void ReadUpdate(Stream stream, YDoc doc, object transactionOrigin)
51 | {
52 | ReadSyncStep2(stream, doc, transactionOrigin);
53 | }
54 |
55 | public static uint ReadSyncMessage(Stream reader, Stream writer, YDoc doc, object transactionOrigin)
56 | {
57 | var messageType = reader.ReadVarUint();
58 |
59 | switch (messageType)
60 | {
61 | case MessageYjsSyncStep1:
62 | ReadSyncStep1(reader, writer, doc);
63 | break;
64 | case MessageYjsSyncStep2:
65 | ReadSyncStep2(reader, doc, transactionOrigin);
66 | break;
67 | case MessageYjsUpdate:
68 | ReadUpdate(reader, doc, transactionOrigin);
69 | break;
70 | default:
71 | throw new Exception($"Unknown message type: {messageType}");
72 | }
73 |
74 | return messageType;
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Ycs/Structs/AbstractStruct.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System.Diagnostics;
8 |
9 | namespace Ycs
10 | {
11 | public abstract class AbstractStruct
12 | {
13 | protected AbstractStruct(ID id, int length)
14 | {
15 | Debug.Assert(length >= 0);
16 |
17 | Id = id;
18 | Length = length;
19 | }
20 |
21 | public ID Id { get; protected set; }
22 | public int Length { get; protected set; }
23 |
24 | public abstract bool Deleted { get; }
25 |
26 | internal abstract bool MergeWith(AbstractStruct right);
27 | internal abstract void Delete(Transaction transaction);
28 | internal abstract void Integrate(Transaction transaction, int offset);
29 | internal abstract long? GetMissing(Transaction transaction, StructStore store);
30 | internal abstract void Write(IUpdateEncoder encoder, int offset);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Ycs/Structs/ContentAny.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System.Collections;
8 | using System.Collections.Generic;
9 | using System.Diagnostics;
10 | using System.Linq;
11 |
12 | namespace Ycs
13 | {
14 | public class ContentAny : IContentEx
15 | {
16 | internal const int _ref = 8;
17 |
18 | private List _content;
19 |
20 | internal ContentAny(IEnumerable content)
21 | {
22 | _content = new List();
23 | foreach (var v in content)
24 | {
25 | _content.Add(v);
26 | }
27 | }
28 |
29 | internal ContentAny(IEnumerable content)
30 | : this(content.ToList())
31 | {
32 | // Do nothing.
33 | }
34 |
35 | private ContentAny(List content)
36 | {
37 | _content = content;
38 | }
39 |
40 | public bool Countable => true;
41 | public int Length => _content.Count;
42 |
43 | int IContentEx.Ref => _ref;
44 |
45 | public IReadOnlyList GetContent() => _content.AsReadOnly();
46 |
47 | public IContent Copy() => new ContentAny(_content.ToList());
48 |
49 | public IContent Splice(int offset)
50 | {
51 | var right = new ContentAny(_content.GetRange(offset, _content.Count - offset));
52 | _content.RemoveRange(offset, _content.Count - offset);
53 | return right;
54 | }
55 |
56 | public bool MergeWith(IContent right)
57 | {
58 | Debug.Assert(right is ContentAny);
59 | _content.AddRange((right as ContentAny)._content);
60 | return true;
61 | }
62 |
63 | void IContentEx.Integrate(Transaction transaction, Item item)
64 | {
65 | // Do nothing.
66 | }
67 |
68 | void IContentEx.Delete(Transaction transaction)
69 | {
70 | // Do nothing.
71 | }
72 |
73 | void IContentEx.Gc(StructStore store)
74 | {
75 | // Do nothing.
76 | }
77 |
78 | void IContentEx.Write(IUpdateEncoder encoder, int offset)
79 | {
80 | int length = _content.Count;
81 | encoder.WriteLength(length - offset);
82 |
83 | for (int i = offset; i < length; i++)
84 | {
85 | var c = _content[i];
86 | encoder.WriteAny(c);
87 | }
88 | }
89 |
90 | internal static ContentAny Read(IUpdateDecoder decoder)
91 | {
92 | var length = decoder.ReadLength();
93 | var cs = new List(length);
94 |
95 | for (int i = 0; i < length; i++)
96 | {
97 | var c = decoder.ReadAny();
98 | cs.Add(c);
99 | }
100 |
101 | return new ContentAny(cs);
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/Ycs/Structs/ContentBinary.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System;
8 | using System.Collections.Generic;
9 |
10 | namespace Ycs
11 | {
12 | public class ContentBinary : IContentEx
13 | {
14 | internal const int _ref = 3;
15 |
16 | private readonly byte[] _content;
17 |
18 | internal ContentBinary(byte[] data)
19 | {
20 | _content = data;
21 | }
22 |
23 | int IContentEx.Ref => _ref;
24 |
25 | public bool Countable => true;
26 | public int Length => 1;
27 |
28 | public IReadOnlyList GetContent() => new object[] { _content };
29 |
30 | public IContent Copy() => new ContentBinary(_content);
31 |
32 | public IContent Splice(int offset)
33 | {
34 | throw new NotImplementedException();
35 | }
36 |
37 | public bool MergeWith(IContent right)
38 | {
39 | return false;
40 | }
41 |
42 | void IContentEx.Integrate(Transaction transaction, Item item)
43 | {
44 | // Do nothing.
45 | }
46 |
47 | void IContentEx.Delete(Transaction transaction)
48 | {
49 | // Do nothing.
50 | }
51 |
52 | void IContentEx.Gc(StructStore store)
53 | {
54 | // Do nothing.
55 | }
56 |
57 | void IContentEx.Write(IUpdateEncoder encoder, int offset)
58 | {
59 | encoder.WriteBuffer(_content);
60 | }
61 |
62 | internal static ContentBinary Read(IUpdateDecoder decoder)
63 | {
64 | var content = decoder.ReadBuffer();
65 | return new ContentBinary(content);
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/Ycs/Structs/ContentDeleted.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System;
8 | using System.Collections.Generic;
9 | using System.Diagnostics;
10 |
11 | namespace Ycs
12 | {
13 | internal class ContentDeleted : IContentEx
14 | {
15 | internal const int _ref = 1;
16 |
17 | internal ContentDeleted(int length)
18 | {
19 | Length = length;
20 | }
21 |
22 | int IContentEx.Ref => _ref;
23 | public bool Countable => false;
24 |
25 | public int Length { get; private set; }
26 |
27 | public IReadOnlyList GetContent() => throw new NotImplementedException();
28 |
29 | public IContent Copy() => new ContentDeleted(Length);
30 |
31 | public IContent Splice(int offset)
32 | {
33 | var right = new ContentDeleted(Length - offset);
34 | Length = offset;
35 | return right;
36 | }
37 |
38 | public bool MergeWith(IContent right)
39 | {
40 | Debug.Assert(right is ContentDeleted);
41 | Length += right.Length;
42 | return true;
43 | }
44 |
45 | void IContentEx.Integrate(Transaction transaction, Item item)
46 | {
47 | transaction.DeleteSet.Add(item.Id.Client, item.Id.Clock, Length);
48 | item.MarkDeleted();
49 | }
50 |
51 | void IContentEx.Delete(Transaction transaction)
52 | {
53 | // Do nothing.
54 | }
55 |
56 | void IContentEx.Gc(StructStore store)
57 | {
58 | // Do nothing.
59 | }
60 |
61 | void IContentEx.Write(IUpdateEncoder encoder, int offset)
62 | {
63 | encoder.WriteLength(Length - offset);
64 | }
65 |
66 | internal static ContentDeleted Read(IUpdateDecoder decoder)
67 | {
68 | var length = decoder.ReadLength();
69 | return new ContentDeleted(length);
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/Ycs/Structs/ContentDoc.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System;
8 | using System.Collections.Generic;
9 |
10 | namespace Ycs
11 | {
12 | internal class ContentDoc : IContentEx
13 | {
14 | internal const int _ref = 9;
15 |
16 | internal ContentDoc(YDoc doc)
17 | {
18 | if (doc._item != null)
19 | {
20 | throw new Exception("This document was already integrated as a sub-document. You should create a second instance instead with the same guid.");
21 | }
22 |
23 | Doc = doc;
24 | Opts = new YDocOptions();
25 |
26 | if (!doc.Gc)
27 | {
28 | Opts.Gc = false;
29 | }
30 |
31 | if (doc.AutoLoad)
32 | {
33 | Opts.AutoLoad = true;
34 | }
35 |
36 | if (doc.Meta != null)
37 | {
38 | Opts.Meta = doc.Meta;
39 | }
40 | }
41 |
42 | int IContentEx.Ref => _ref;
43 |
44 | public bool Countable => true;
45 | public int Length => 1;
46 |
47 | public YDoc Doc { get; internal set; }
48 | public YDocOptions Opts { get; internal set; } = new YDocOptions();
49 |
50 | public IReadOnlyList GetContent() => new[] { Doc };
51 |
52 | public IContent Copy() => new ContentDoc(Doc);
53 |
54 | public IContent Splice(int offset)
55 | {
56 | throw new NotImplementedException();
57 | }
58 |
59 | public bool MergeWith(IContent right)
60 | {
61 | return false;
62 | }
63 |
64 | void IContentEx.Integrate(Transaction transaction, Item item)
65 | {
66 | // This needs to be reflected in doc.destroy as well.
67 | Doc._item = item;
68 | transaction.SubdocsAdded.Add(Doc);
69 |
70 | if (Doc.ShouldLoad)
71 | {
72 | transaction.SubdocsLoaded.Add(Doc);
73 | }
74 | }
75 |
76 | void IContentEx.Delete(Transaction transaction)
77 | {
78 | if (transaction.SubdocsAdded.Contains(Doc))
79 | {
80 | transaction.SubdocsAdded.Remove(Doc);
81 | }
82 | else
83 | {
84 | transaction.SubdocsRemoved.Add(Doc);
85 | }
86 | }
87 |
88 | void IContentEx.Gc(StructStore store)
89 | {
90 | // Do nothing.
91 | }
92 |
93 | void IContentEx.Write(IUpdateEncoder encoder, int offset)
94 | {
95 | // 32 digits separated by hyphens, no braces.
96 | encoder.WriteString(Doc.Guid);
97 | Opts.Write(encoder, offset);
98 | }
99 |
100 | internal static ContentDoc Read(IUpdateDecoder decoder)
101 | {
102 | var guidStr = decoder.ReadString();
103 |
104 | var opts = YDocOptions.Read(decoder);
105 | opts.Guid = guidStr;
106 |
107 | return new ContentDoc(new YDoc(opts));
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/Ycs/Structs/ContentEmbed.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System;
8 | using System.Collections.Generic;
9 |
10 | namespace Ycs
11 | {
12 | public class ContentEmbed : IContentEx
13 | {
14 | internal const int _ref = 5;
15 |
16 | public readonly object Embed;
17 |
18 | internal ContentEmbed(object embed)
19 | {
20 | Embed = embed;
21 | }
22 |
23 | int IContentEx.Ref => _ref;
24 |
25 | public bool Countable => true;
26 | public int Length => 1;
27 |
28 | public IReadOnlyList GetContent() => new object[] { Embed };
29 |
30 | public IContent Copy() => new ContentEmbed(Embed);
31 |
32 | public IContent Splice(int offset)
33 | {
34 | throw new NotImplementedException();
35 | }
36 |
37 | public bool MergeWith(IContent right)
38 | {
39 | return false;
40 | }
41 |
42 | void IContentEx.Integrate(Transaction transaction, Item item)
43 | {
44 | // Do nothing.
45 | }
46 |
47 | void IContentEx.Delete(Transaction transaction)
48 | {
49 | // Do nothing.
50 | }
51 |
52 | void IContentEx.Gc(StructStore store)
53 | {
54 | // Do nothing.
55 | }
56 |
57 | void IContentEx.Write(IUpdateEncoder encoder, int offset)
58 | {
59 | encoder.WriteJson(Embed);
60 | }
61 |
62 | internal static ContentEmbed Read(IUpdateDecoder decoder)
63 | {
64 | var content = decoder.ReadJson();
65 | return new ContentEmbed(content);
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/Ycs/Structs/ContentFormat.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System;
8 | using System.Collections.Generic;
9 |
10 | namespace Ycs
11 | {
12 | public class ContentFormat : IContentEx
13 | {
14 | internal const int _ref = 6;
15 |
16 | public readonly string Key;
17 | public readonly object Value;
18 |
19 | internal ContentFormat(string key, object value)
20 | {
21 | Key = key;
22 | Value = value;
23 | }
24 |
25 | int IContentEx.Ref => _ref;
26 |
27 | public bool Countable => false;
28 | public int Length => 1;
29 |
30 | public IContent Copy() => new ContentFormat(Key, Value);
31 |
32 | public IReadOnlyList GetContent() => throw new NotImplementedException();
33 |
34 | public IContent Splice(int offset) => throw new NotImplementedException();
35 |
36 | public bool MergeWith(IContent right)
37 | {
38 | return false;
39 | }
40 |
41 | void IContentEx.Integrate(Transaction transaction, Item item)
42 | {
43 | // Search markers are currently unsupported for rich text documents.
44 | (item.Parent as YArrayBase)?.ClearSearchMarkers();
45 | }
46 |
47 | void IContentEx.Delete(Transaction transaction)
48 | {
49 | // Do nothing.
50 | }
51 |
52 | void IContentEx.Gc(StructStore store)
53 | {
54 | // Do nothing.
55 | }
56 |
57 | void IContentEx.Write(IUpdateEncoder encoder, int offset)
58 | {
59 | encoder.WriteKey(Key);
60 | encoder.WriteJson(Value);
61 | }
62 |
63 | internal static ContentFormat Read(IUpdateDecoder decoder)
64 | {
65 | var key = decoder.ReadKey();
66 | var value = decoder.ReadJson();
67 | return new ContentFormat(key, value);
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/Ycs/Structs/ContentJson.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System.Collections.Generic;
8 | using System.Diagnostics;
9 |
10 | namespace Ycs
11 | {
12 | public class ContentJson : IContentEx
13 | {
14 | internal const int _ref = 2;
15 |
16 | private readonly List _content;
17 |
18 | internal ContentJson(IEnumerable data)
19 | {
20 | _content = new List(data);
21 | }
22 |
23 | private ContentJson(List other)
24 | {
25 | _content = other;
26 | }
27 |
28 | int IContentEx.Ref => _ref;
29 |
30 | public bool Countable => true;
31 | public int Length => _content?.Count ?? 0;
32 |
33 | public IReadOnlyList GetContent() => _content.AsReadOnly();
34 |
35 | public IContent Copy() => new ContentJson(_content);
36 |
37 | public IContent Splice(int offset)
38 | {
39 | var right = new ContentJson(_content.GetRange(offset, _content.Count - offset));
40 | _content.RemoveRange(offset, _content.Count - offset);
41 | return right;
42 | }
43 |
44 | public bool MergeWith(IContent right)
45 | {
46 | Debug.Assert(right is ContentJson);
47 | _content.AddRange((right as ContentJson)._content);
48 | return true;
49 | }
50 |
51 | void IContentEx.Integrate(Transaction transaction, Item item)
52 | {
53 | // Do nothing.
54 | }
55 |
56 | void IContentEx.Delete(Transaction transaction)
57 | {
58 | // Do nothing.
59 | }
60 |
61 | void IContentEx.Gc(StructStore store)
62 | {
63 | // Do nothing.
64 | }
65 |
66 | void IContentEx.Write(IUpdateEncoder encoder, int offset)
67 | {
68 | var len = _content.Count;
69 | encoder.WriteLength(len);
70 | for (int i = offset; i < len; i++)
71 | {
72 | var jsonStr = Newtonsoft.Json.JsonConvert.SerializeObject(_content[i]);
73 | encoder.WriteString(jsonStr);
74 | }
75 | }
76 |
77 | internal static ContentJson Read(IUpdateDecoder decoder)
78 | {
79 | var len = decoder.ReadLength();
80 | var content = new List(len);
81 |
82 | for (int i = 0; i < len; i++)
83 | {
84 | var jsonStr = decoder.ReadString();
85 | object jsonObj = string.Equals(jsonStr, "undefined")
86 | ? null
87 | : Newtonsoft.Json.JsonConvert.DeserializeObject(jsonStr);
88 | content.Add(jsonObj);
89 | }
90 |
91 | return new ContentJson(content);
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/Ycs/Structs/ContentString.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System.Collections.Generic;
8 | using System.Diagnostics;
9 | using System.Linq;
10 | using System.Text;
11 |
12 | namespace Ycs
13 | {
14 | public class ContentString : IContentEx
15 | {
16 | internal const int _ref = 4;
17 |
18 | private readonly List _content;
19 |
20 | internal ContentString(string value)
21 | : this(value.Cast().ToList())
22 | {
23 | // Do nothing.
24 | }
25 |
26 | private ContentString(List content)
27 | {
28 | _content = content;
29 | }
30 |
31 | int IContentEx.Ref => _ref;
32 |
33 | public bool Countable => true;
34 | public int Length => _content.Count;
35 |
36 | internal void AppendToBuilder(StringBuilder sb)
37 | {
38 | foreach (var c in _content)
39 | {
40 | sb.Append((char)c);
41 | }
42 | }
43 |
44 | public string GetString()
45 | {
46 | var sb = new StringBuilder();
47 |
48 | foreach (var c in _content)
49 | {
50 | sb.Append((char)c);
51 | }
52 |
53 | return sb.ToString();
54 | }
55 |
56 | public IReadOnlyList GetContent() => _content.AsReadOnly();
57 |
58 | public IContent Copy() => new ContentString(_content.ToList());
59 |
60 | public IContent Splice(int offset)
61 | {
62 | var right = new ContentString(_content.GetRange(offset, _content.Count - offset));
63 | _content.RemoveRange(offset, _content.Count - offset);
64 |
65 | // Prevent encoding invalid documents because of splitting of surrogate pairs.
66 | var firstCharCode = (char)_content[offset - 1];
67 | if (firstCharCode >= 0xD800 && firstCharCode <= 0xDBFF)
68 | {
69 | // Last character of the left split is the start of a surrogate utf16/ucs2 pair.
70 | // We don't support splitting of surrogate pairs because this may lead to invalid documents.
71 | // Replace the invalid character with a unicode replacement character U+FFFD.
72 | _content[offset - 1] = '\uFFFD';
73 |
74 | // Replace right as well.
75 | right._content[0] = '\uFFFD';
76 | }
77 |
78 | return right;
79 | }
80 |
81 | public bool MergeWith(IContent right)
82 | {
83 | Debug.Assert(right is ContentString);
84 | _content.AddRange((right as ContentString)._content);
85 | return true;
86 | }
87 |
88 | void IContentEx.Integrate(Transaction transaction, Item item)
89 | {
90 | // Do nothing.
91 | }
92 |
93 | void IContentEx.Delete(Transaction transaction)
94 | {
95 | // Do nothing.
96 | }
97 |
98 | void IContentEx.Gc(StructStore store)
99 | {
100 | // Do nothing.
101 | }
102 |
103 | void IContentEx.Write(IUpdateEncoder encoder, int offset)
104 | {
105 | var sb = new StringBuilder(_content.Count - offset);
106 | for (int i = offset; i < _content.Count; i++)
107 | {
108 | sb.Append((char)_content[i]);
109 | }
110 |
111 | var str = sb.ToString();
112 | encoder.WriteString(str);
113 | }
114 |
115 | internal static ContentString Read(IUpdateDecoder decoder)
116 | {
117 | return new ContentString(decoder.ReadString());
118 | }
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/Ycs/Structs/ContentType.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System;
8 | using System.Collections.Generic;
9 |
10 | namespace Ycs
11 | {
12 | public class ContentType : IContentEx
13 | {
14 | internal const int _ref = 7;
15 |
16 | internal ContentType(AbstractType type)
17 | {
18 | Type = type;
19 | }
20 |
21 | int IContentEx.Ref => _ref;
22 |
23 | public bool Countable => true;
24 | public int Length => 1;
25 |
26 | public AbstractType Type { get; }
27 |
28 | public IReadOnlyList GetContent() => new object[] { Type };
29 |
30 | public IContent Copy() => new ContentType(Type.InternalCopy());
31 |
32 | public IContent Splice(int offset) => throw new NotImplementedException();
33 |
34 | public bool MergeWith(IContent right) => false;
35 |
36 | void IContentEx.Integrate(Transaction transaction, Item item)
37 | {
38 | Type.Integrate(transaction.Doc, item);
39 | }
40 |
41 | void IContentEx.Delete(Transaction transaction)
42 | {
43 | var item = Type._start;
44 |
45 | while (item != null)
46 | {
47 | if (!item.Deleted)
48 | {
49 | item.Delete(transaction);
50 | }
51 | else
52 | {
53 | // This will be gc'd later and we want to merge it if possible.
54 | // We try to merge all deleted items each transaction,
55 | // but we have no knowledge about that this needs to merged
56 | // since it is not in transaction. Hence we add it to transaction._mergeStructs.
57 | transaction._mergeStructs.Add(item);
58 | }
59 |
60 | item = item.Right as Item;
61 | }
62 |
63 | foreach (var valueItem in Type._map.Values)
64 | {
65 | if (!valueItem.Deleted)
66 | {
67 | valueItem.Delete(transaction);
68 | }
69 | else
70 | {
71 | // Same as above.
72 | transaction._mergeStructs.Add(valueItem);
73 | }
74 | }
75 |
76 | transaction.Changed.Remove(Type);
77 | }
78 |
79 | void IContentEx.Gc(StructStore store)
80 | {
81 | var item = Type._start;
82 | while (item != null)
83 | {
84 | item.Gc(store, parentGCd: true);
85 | item = item.Right as Item;
86 | }
87 |
88 | Type._start = null;
89 |
90 | foreach (var kvp in Type._map)
91 | {
92 | var valueItem = kvp.Value;
93 | while (valueItem != null)
94 | {
95 | valueItem.Gc(store, parentGCd: true);
96 | valueItem = valueItem.Left as Item;
97 | }
98 | }
99 |
100 | Type._map.Clear();
101 | }
102 |
103 | void IContentEx.Write(IUpdateEncoder encoder, int offset)
104 | {
105 | Type.Write(encoder);
106 | }
107 |
108 | internal static ContentType Read(IUpdateDecoder decoder)
109 | {
110 | var typeRef = decoder.ReadTypeRef();
111 | switch (typeRef)
112 | {
113 | case YArray.YArrayRefId:
114 | var arr = YArray.Read(decoder);
115 | return new ContentType(arr);
116 | case YMap.YMapRefId:
117 | var map = YMap.Read(decoder);
118 | return new ContentType(map);
119 | case YText.YTextRefId:
120 | var text = YText.Read(decoder);
121 | return new ContentType(text);
122 | default:
123 | throw new NotImplementedException($"Type {typeRef} not implemented");
124 | }
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/Ycs/Structs/GC.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System.Diagnostics;
8 |
9 | namespace Ycs
10 | {
11 | public class GC : AbstractStruct
12 | {
13 | internal const byte StructGCRefNumber = 0;
14 |
15 | internal GC(ID id, int length)
16 | : base(id, length)
17 | {
18 | // Do nothing.
19 | }
20 |
21 | public override bool Deleted => true;
22 |
23 | internal override bool MergeWith(AbstractStruct right)
24 | {
25 | Debug.Assert(right is GC);
26 | Length += right.Length;
27 | return true;
28 | }
29 |
30 | internal override void Delete(Transaction transaction)
31 | {
32 | // Do nothing.
33 | }
34 |
35 | internal override void Integrate(Transaction transaction, int offset)
36 | {
37 | if (offset > 0)
38 | {
39 | Id = new ID(Id.Client, Id.Clock + offset);
40 | Length -= offset;
41 | }
42 |
43 | transaction.Doc.Store.AddStruct(this);
44 | }
45 |
46 | internal override long? GetMissing(Transaction transaction, StructStore store)
47 | {
48 | return null;
49 | }
50 |
51 | internal override void Write(IUpdateEncoder encoder, int offset)
52 | {
53 | encoder.WriteInfo(StructGCRefNumber);
54 | encoder.WriteLength(Length - offset);
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Ycs/Structs/IContent.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System.Collections.Generic;
8 |
9 | namespace Ycs
10 | {
11 | public interface IContent
12 | {
13 | bool Countable { get; }
14 | int Length { get; }
15 | IReadOnlyList GetContent();
16 | IContent Copy();
17 | IContent Splice(int offset);
18 | bool MergeWith(IContent right);
19 | }
20 |
21 | internal interface IContentEx : IContent
22 | {
23 | int Ref { get; }
24 |
25 | void Integrate(Transaction transaction, Item item);
26 | void Delete(Transaction transaction);
27 | void Gc(StructStore store);
28 | void Write(IUpdateEncoder encoder, int offset);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Ycs/Types/AbstractType.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System;
8 | using System.Collections.Generic;
9 | using System.Linq;
10 |
11 | namespace Ycs
12 | {
13 | public class YEventArgs
14 | {
15 | internal YEventArgs(YEvent evt, Transaction transaction)
16 | {
17 | Event = evt;
18 | Transaction = transaction;
19 | }
20 |
21 | public YEvent Event { get; }
22 | public Transaction Transaction { get; }
23 | }
24 |
25 | public class YDeepEventArgs
26 | {
27 | internal YDeepEventArgs(IList events, Transaction transaction)
28 | {
29 | Events = events;
30 | Transaction = transaction;
31 | }
32 |
33 | public IList Events { get; }
34 | public Transaction Transaction { get; }
35 | }
36 |
37 | public class AbstractType
38 | {
39 | internal Item _item = null;
40 | internal Item _start = null;
41 | internal IDictionary _map = new Dictionary();
42 |
43 | public event EventHandler EventHandler;
44 | public event EventHandler DeepEventHandler;
45 |
46 | public YDoc Doc { get; protected set; }
47 | public AbstractType Parent => _item != null ? _item.Parent as AbstractType : null;
48 |
49 | public virtual int Length { get; internal set; }
50 |
51 | internal virtual void Integrate(YDoc doc, Item item)
52 | {
53 | Doc = doc;
54 | _item = item;
55 | }
56 |
57 | internal virtual AbstractType InternalCopy() { throw new NotImplementedException(); }
58 | internal virtual AbstractType InternalClone() { throw new NotImplementedException(); }
59 |
60 | internal virtual void Write(IUpdateEncoder encoder) { throw new NotImplementedException(); }
61 |
62 | ///
63 | /// Call event listeners with an event. This will also add an event to all parents
64 | /// for observeDeep handlers.
65 | ///
66 | internal virtual void CallTypeObservers(Transaction transaction, YEvent evt)
67 | {
68 | var type = this;
69 |
70 | while (true)
71 | {
72 | if (!transaction.ChangedParentTypes.TryGetValue(type, out var values))
73 | {
74 | values = new List();
75 | transaction.ChangedParentTypes[type] = values;
76 | }
77 |
78 | values.Add(evt);
79 |
80 | if (type._item == null)
81 | {
82 | break;
83 | }
84 |
85 | type = type._item.Parent as AbstractType;
86 | }
87 |
88 | InvokeEventHandlers(evt, transaction);
89 | }
90 |
91 | ///
92 | /// Creates YEvent and calls all type observers.
93 | /// Must be implemented by each type.
94 | ///
95 | internal virtual void CallObserver(Transaction transaction, ISet parentSubs)
96 | {
97 | // Do nothing.
98 | }
99 |
100 | internal Item _First()
101 | {
102 | var n = _start;
103 | while (n != null && n.Deleted)
104 | {
105 | n = n.Right as Item;
106 | }
107 | return n;
108 | }
109 |
110 | internal void InvokeEventHandlers(YEvent evt, Transaction transaction)
111 | {
112 | EventHandler?.Invoke(this, new YEventArgs(evt, transaction));
113 | }
114 |
115 | internal void CallDeepEventHandlerListeners(IList events, Transaction transaction)
116 | {
117 | DeepEventHandler?.Invoke(this, new YDeepEventArgs(events, transaction));
118 | }
119 |
120 | internal string FindRootTypeKey()
121 | {
122 | return Doc.FindRootTypeKey(this);
123 | }
124 |
125 | protected void TypeMapDelete(Transaction transaction, string key)
126 | {
127 | if (_map.TryGetValue(key, out var c))
128 | {
129 | c.Delete(transaction);
130 | }
131 | }
132 |
133 | protected void TypeMapSet(Transaction transaction, string key, object value)
134 | {
135 | if (!_map.TryGetValue(key, out var left))
136 | {
137 | left = null;
138 | }
139 |
140 | var doc = transaction.Doc;
141 | var ownClientId = doc.ClientId;
142 | IContent content;
143 |
144 | if (value == null)
145 | {
146 | content = new ContentAny(new object[] { value });
147 | }
148 | else
149 | {
150 | switch (value)
151 | {
152 | case YDoc d:
153 | content = new ContentDoc(d);
154 | break;
155 | case AbstractType at:
156 | content = new ContentType(at);
157 | break;
158 | case byte[] ba:
159 | content = new ContentBinary(ba);
160 | break;
161 | default:
162 | content = new ContentAny(new[] { value });
163 | break;
164 | }
165 | }
166 |
167 | var newItem = new Item(new ID(ownClientId, doc.Store.GetState(ownClientId)), left, left?.LastId, null, null, this, key, content);
168 | newItem.Integrate(transaction, 0);
169 | }
170 |
171 | protected bool TryTypeMapGet(string key, out object value)
172 | {
173 | if (_map.TryGetValue(key, out var val) && !val.Deleted)
174 | {
175 | value = val.Content.GetContent()[val.Length - 1];
176 | return true;
177 | }
178 |
179 | value = default;
180 | return false;
181 | }
182 |
183 | protected object TypeMapGetSnapshot(string key, Snapshot snapshot)
184 | {
185 | if (!_map.TryGetValue(key, out var v))
186 | {
187 | v = null;
188 | }
189 |
190 | while (v != null && (!snapshot.StateVector.ContainsKey(v.Id.Client) || v.Id.Clock >= snapshot.StateVector[v.Id.Client]))
191 | {
192 | v = v.Left as Item;
193 | }
194 |
195 | return v != null && v.IsVisible(snapshot) ? v.Content.GetContent()[v.Length - 1] : null;
196 | }
197 |
198 | protected IEnumerable> TypeMapEnumerate() => _map.Where(kvp => !kvp.Value.Deleted);
199 |
200 | protected IEnumerable> TypeMapEnumerateValues()
201 | {
202 | foreach (var kvp in TypeMapEnumerate())
203 | {
204 | var key = kvp.Key;
205 | var value = kvp.Value.Content.GetContent()[kvp.Value.Length - 1];
206 | yield return new KeyValuePair(key, value);
207 | }
208 | }
209 | }
210 | }
211 |
--------------------------------------------------------------------------------
/src/Ycs/Types/YArray.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System.Collections.Generic;
8 |
9 | namespace Ycs
10 | {
11 | public class YArrayEvent : YEvent
12 | {
13 | internal YArrayEvent(YArray arr, Transaction transaction)
14 | : base(arr, transaction)
15 | {
16 | // Do nothing.
17 | }
18 | }
19 |
20 | public class YArray : YArrayBase
21 | {
22 | public const byte YArrayRefId = 0;
23 |
24 | private List _prelimContent;
25 |
26 | public YArray()
27 | : this(null)
28 | {
29 | // Do nothing.
30 | }
31 |
32 | public YArray(IEnumerable prelimContent = null)
33 | {
34 | _prelimContent = prelimContent != null ? new List(prelimContent) : new List();
35 | }
36 |
37 | public override int Length => _prelimContent?.Count ?? base.Length;
38 |
39 | public YArray Clone() => InternalClone() as YArray;
40 |
41 | internal override void Integrate(YDoc doc, Item item)
42 | {
43 | base.Integrate(doc, item);
44 | Insert(0, _prelimContent);
45 | _prelimContent = null;
46 | }
47 |
48 | internal override AbstractType InternalCopy()
49 | {
50 | return new YArray();
51 | }
52 |
53 | internal override AbstractType InternalClone()
54 | {
55 | var arr = new YArray();
56 |
57 | foreach (var item in EnumerateList())
58 | {
59 | if (item is AbstractType at)
60 | {
61 | arr.Add(new[] { at.InternalClone() });
62 | }
63 | else
64 | {
65 | arr.Add(new[] { item });
66 | }
67 | }
68 |
69 | return arr;
70 | }
71 |
72 | internal override void Write(IUpdateEncoder encoder)
73 | {
74 | encoder.WriteTypeRef(YArrayRefId);
75 | }
76 |
77 | internal static YArray Read(IUpdateDecoder decoder)
78 | {
79 | return new YArray();
80 | }
81 |
82 | ///
83 | /// Creates YArrayEvent and calls observers.
84 | ///
85 | internal override void CallObserver(Transaction transaction, ISet parentSubs)
86 | {
87 | base.CallObserver(transaction, parentSubs);
88 | CallTypeObservers(transaction, new YArrayEvent(this, transaction));
89 | }
90 |
91 | ///
92 | /// Inserts new content at an index.
93 | ///
94 | public void Insert(int index, ICollection content)
95 | {
96 | if (Doc != null)
97 | {
98 | Doc.Transact((tr) =>
99 | {
100 | InsertGenerics(tr, index, content);
101 | });
102 | }
103 | else
104 | {
105 | _prelimContent.InsertRange(index, content);
106 | }
107 | }
108 |
109 | public void Add(ICollection content)
110 | {
111 | Insert(Length, content);
112 | }
113 |
114 | public void Unshift(ICollection content)
115 | {
116 | Insert(0, content);
117 | }
118 |
119 | public void Delete(int index, int length = 1)
120 | {
121 | if (Doc != null)
122 | {
123 | Doc.Transact((tr) =>
124 | {
125 | Delete(tr, index, length);
126 | });
127 | }
128 | else
129 | {
130 | _prelimContent.RemoveRange(index, length);
131 | }
132 | }
133 |
134 | public IReadOnlyList Slice(int start = 0) => InternalSlice(start, Length);
135 |
136 | public IReadOnlyList Slice(int start, int end) => InternalSlice(start, end);
137 |
138 | public object Get(int index)
139 | {
140 | var marker = FindMarker(index);
141 | var n = _start;
142 |
143 | if (marker != null)
144 | {
145 | n = marker.P;
146 | index -= marker.Index;
147 | }
148 |
149 | for (; n != null; n = n.Right as Item)
150 | {
151 | if (!n.Deleted && n.Countable)
152 | {
153 | if (index < n.Length)
154 | {
155 | return n.Content.GetContent()[index];
156 | }
157 |
158 | index -= n.Length;
159 | }
160 | }
161 |
162 | return default;
163 | }
164 |
165 | public IList ToArray()
166 | {
167 | var cs = new List();
168 | cs.AddRange(EnumerateList());
169 | return cs;
170 | }
171 |
172 | private IEnumerable EnumerateList()
173 | {
174 | var n = _start;
175 |
176 | while (n != null)
177 | {
178 | if (n.Countable && !n.Deleted)
179 | {
180 | var c = n.Content.GetContent();
181 | foreach (var item in c)
182 | {
183 | yield return item;
184 | }
185 | }
186 |
187 | n = n.Right as Item;
188 | }
189 | }
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/src/Ycs/Types/YMap.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System.Collections;
8 | using System.Collections.Generic;
9 | using System.Linq;
10 |
11 | namespace Ycs
12 | {
13 | ///
14 | /// Event that describes changes on a YMap.
15 | ///
16 | public class YMapEvent : YEvent
17 | {
18 | public readonly ISet KeysChanged;
19 |
20 | internal YMapEvent(YMap map, Transaction transaction, ISet subs)
21 | : base(map, transaction)
22 | {
23 | KeysChanged = subs;
24 | }
25 | }
26 |
27 | ///
28 | /// A shared Map implementation.
29 | ///
30 | public class YMap : AbstractType, IEnumerable>
31 | {
32 | internal const int YMapRefId = 1;
33 |
34 | private Dictionary _prelimContent;
35 |
36 | public YMap()
37 | : this(null)
38 | {
39 | // Do nothing.
40 | }
41 |
42 | public YMap(IDictionary entries)
43 | {
44 | _prelimContent = entries != null ? new Dictionary(entries) : new Dictionary();
45 | }
46 |
47 | public int Count => _prelimContent?.Count ?? TypeMapEnumerate().Count();
48 |
49 | public object Get(string key)
50 | {
51 | if (!TryTypeMapGet(key, out var value))
52 | {
53 | throw new KeyNotFoundException();
54 | }
55 |
56 | return value;
57 | }
58 |
59 | public void Set(string key, object value)
60 | {
61 | if (Doc != null)
62 | {
63 | Doc.Transact(tr =>
64 | {
65 | TypeMapSet(tr, key, value);
66 | });
67 | }
68 | else
69 | {
70 | _prelimContent[key] = value;
71 | }
72 | }
73 |
74 | public void Delete(string key)
75 | {
76 | if (Doc != null)
77 | {
78 | Doc.Transact(tr =>
79 | {
80 | TypeMapDelete(tr, key);
81 | });
82 | }
83 | else
84 | {
85 | _prelimContent.Remove(key);
86 | }
87 | }
88 |
89 | public bool ContainsKey(string key)
90 | {
91 | return _map.TryGetValue(key, out var val) && !val.Deleted;
92 | }
93 |
94 | public IEnumerable Keys() => TypeMapEnumerate().Select(kvp => kvp.Key);
95 |
96 | public IEnumerable Values() => TypeMapEnumerate().Select(kvp => kvp.Value.Content.GetContent()[kvp.Value.Length - 1]);
97 |
98 | public YMap Clone() => InternalClone() as YMap;
99 |
100 | internal override AbstractType InternalCopy()
101 | {
102 | return new YMap();
103 | }
104 |
105 | internal override AbstractType InternalClone()
106 | {
107 | var map = new YMap();
108 |
109 | foreach (var kvp in TypeMapEnumerate())
110 | {
111 | // TODO: [alekseyk] Yjs checks for the AbstractType here, but _map can only have 'Item' values. Might be an error?
112 | map.Set(kvp.Key, kvp.Value);
113 | }
114 |
115 | return map;
116 | }
117 |
118 | internal override void Integrate(YDoc doc, Item item)
119 | {
120 | base.Integrate(doc, item);
121 |
122 | foreach (var kvp in _prelimContent)
123 | {
124 | Set(kvp.Key, kvp.Value);
125 | }
126 |
127 | _prelimContent = null;
128 | }
129 |
130 | internal override void CallObserver(Transaction transaction, ISet parentSubs)
131 | {
132 | CallTypeObservers(transaction, new YMapEvent(this, transaction, parentSubs));
133 | }
134 |
135 | internal override void Write(IUpdateEncoder encoder)
136 | {
137 | encoder.WriteTypeRef(YMapRefId);
138 | }
139 |
140 | internal static YMap Read(IUpdateDecoder decoder)
141 | {
142 | return new YMap();
143 | }
144 |
145 | public IEnumerator> GetEnumerator() => TypeMapEnumerateValues().GetEnumerator();
146 |
147 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/src/Ycs/Utils/AbsolutePosition.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System;
8 | using System.Diagnostics;
9 |
10 | namespace Ycs
11 | {
12 | internal class AbsolutePosition
13 | {
14 | public readonly AbstractType Type;
15 | public readonly int Index;
16 | public readonly int Assoc;
17 |
18 | public AbsolutePosition(AbstractType type, int index, int assoc = 0)
19 | {
20 | Type = type;
21 | Index = index;
22 | Assoc = assoc;
23 | }
24 |
25 | public static AbsolutePosition TryCreateFromRelativePosition(RelativePosition rpos, YDoc doc)
26 | {
27 | var store = doc.Store;
28 | var rightId = rpos.Item;
29 | var typeId = rpos.TypeId;
30 | var tName = rpos.TName;
31 | var assoc = rpos.Assoc;
32 | int index = 0;
33 | AbstractType type;
34 |
35 | if (rightId != null)
36 | {
37 | if (store.GetState(rightId.Value.Client) <= rightId.Value.Clock)
38 | {
39 | return null;
40 | }
41 |
42 | var res = store.FollowRedone(rightId.Value);
43 | var right = res.item as Item;
44 | if (right == null)
45 | {
46 | return null;
47 | }
48 |
49 | type = right.Parent as AbstractType;
50 | Debug.Assert(type != null);
51 |
52 | if (type._item == null || !type._item.Deleted)
53 | {
54 | // Adjust position based on the left assotiation, if necessary.
55 | index = (right.Deleted || !right.Countable) ? 0 : (res.diff + (assoc >= 0 ? 0 : 1));
56 | var n = right.Left as Item;
57 | while (n != null)
58 | {
59 | if (!n.Deleted && n.Countable)
60 | {
61 | index += n.Length;
62 | }
63 |
64 | n = n.Left as Item;
65 | }
66 | }
67 | }
68 | else
69 | {
70 | if (tName != null)
71 | {
72 | type = doc.Get(tName);
73 | }
74 | else if (typeId != null)
75 | {
76 | if (store.GetState(typeId.Value.Client) <= typeId.Value.Clock)
77 | {
78 | // Type does not exist yet.
79 | return null;
80 | }
81 |
82 | var item = store.FollowRedone(typeId.Value).item as Item;
83 | if (item != null && item.Content is ContentType)
84 | {
85 | type = (item.Content as ContentType).Type;
86 | }
87 | else
88 | {
89 | // Struct is garbage collected.
90 | return null;
91 | }
92 | }
93 | else
94 | {
95 | throw new Exception();
96 | }
97 |
98 | index = assoc >= 0 ? type.Length : 0;
99 | }
100 |
101 | return new AbsolutePosition(type, index, assoc);
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/Ycs/Utils/ID.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System;
8 | using System.Diagnostics;
9 | using System.IO;
10 |
11 | namespace Ycs
12 | {
13 | public struct ID : IEquatable
14 | {
15 | ///
16 | /// Client id.
17 | ///
18 | public long Client;
19 |
20 | ///
21 | /// Unique per client id, continuous number.
22 | ///
23 | public long Clock;
24 |
25 | public ID(long client, long clock)
26 | {
27 | Debug.Assert(client >= 0, "Client should not be negative, as it causes client encoder to fail");
28 | Debug.Assert(clock >= 0);
29 |
30 | Client = client;
31 | Clock = clock;
32 | }
33 |
34 | public bool Equals(ID other)
35 | {
36 | return Client == other.Client && Clock == other.Clock;
37 | }
38 |
39 | public static bool Equals(ID? a, ID? b)
40 | {
41 | return (a == null && b == null) || (a != null && b != null && a.Value.Equals(b.Value));
42 | }
43 |
44 | public void Write(Stream writer)
45 | {
46 | writer.WriteVarUint((uint)Client);
47 | writer.WriteVarUint((uint)Clock);
48 | }
49 |
50 | public static ID Read(Stream reader)
51 | {
52 | var client = reader.ReadVarUint();
53 | var clock = reader.ReadVarUint();
54 | Debug.Assert(client >= 0 && clock >= 0);
55 | return new ID(client, clock);
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Ycs/Utils/IUpdateDecoder.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System;
8 | using System.IO;
9 |
10 | namespace Ycs
11 | {
12 | internal interface IDSDecoder : IDisposable
13 | {
14 | Stream Reader { get; }
15 |
16 | void ResetDsCurVal();
17 | long ReadDsClock();
18 | long ReadDsLength();
19 | }
20 |
21 | internal interface IUpdateDecoder : IDSDecoder
22 | {
23 | ID ReadLeftId();
24 | ID ReadRightId();
25 | long ReadClient();
26 | byte ReadInfo();
27 | string ReadString();
28 | bool ReadParentInfo();
29 | uint ReadTypeRef();
30 | int ReadLength();
31 | object ReadAny();
32 | byte[] ReadBuffer();
33 | string ReadKey();
34 | object ReadJson();
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Ycs/Utils/IUpdateEncoder.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System;
8 | using System.IO;
9 |
10 | namespace Ycs
11 | {
12 | internal interface IDSEncoder : IDisposable
13 | {
14 | Stream RestWriter { get; }
15 |
16 | byte[] ToArray();
17 |
18 | ///
19 | /// Resets the ds value to 0.
20 | /// The v2 encoder uses this information to reset the initial diff value.
21 | ///
22 | void ResetDsCurVal();
23 |
24 | void WriteDsClock(long clock);
25 | void WriteDsLength(long length);
26 | }
27 |
28 | internal interface IUpdateEncoder : IDSEncoder
29 | {
30 | void WriteLeftId(ID id);
31 | void WriteRightId(ID id);
32 |
33 | ///
34 | /// NOTE: Use 'writeClient' and 'writeClock' instead of writeID if possible.
35 | ///
36 | void WriteClient(long client);
37 |
38 | void WriteInfo(byte info);
39 | void WriteString(string s);
40 | void WriteParentInfo(bool isYKey);
41 | void WriteTypeRef(uint info);
42 |
43 | ///
44 | /// Write len of a struct - well suited for Opt RLE encoder.
45 | ///
46 | void WriteLength(int len);
47 |
48 | void WriteAny(object any);
49 | void WriteBuffer(byte[] buf);
50 | void WriteKey(string key);
51 | void WriteJson(T any);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Ycs/Utils/RelativePosition.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System;
8 | using System.IO;
9 |
10 | namespace Ycs
11 | {
12 | ///
13 | /// A relative position is based on the YUjs model and is not affected by document changes.
14 | /// E.g. if you place a relative position before a certain character, it will always point to this character.
15 | /// If you place a relative position at the end of a type, it will always point to the end of the type.
16 | ///
17 | /// A numberic position is often unsuited for user selections, because it does not change when content is inserted
18 | /// before or after.
19 | ///
20 | /// Insert(0, 'x')('a|bc') = 'xa|bc' Where | is tehre relative position.
21 | ///
22 | /// Only one property must be defined.
23 | ///
24 | internal class RelativePosition : IEquatable
25 | {
26 | public readonly ID? Item;
27 | public readonly ID? TypeId;
28 | public readonly string TName;
29 |
30 | ///
31 | /// A relative position is associated to a specific character.
32 | /// By default, the value is >&eq; 0 , the relative position is associated to the character
33 | /// after the meant position.
34 | /// I.e. position 1 in 'ab' is associated with the character 'b' .
35 | ///
36 | /// If the value is < 0 , then the relative position is associated with the caharacter
37 | /// before the meant position.
38 | ///
39 | public int Assoc;
40 |
41 | public RelativePosition(AbstractType type, ID? item, int assoc = 0)
42 | {
43 | Item = item;
44 | Assoc = assoc;
45 |
46 | if (type._item == null)
47 | {
48 | TName = type.FindRootTypeKey();
49 | }
50 | else
51 | {
52 | TypeId = new ID(type._item.Id.Client, type._item.Id.Clock);
53 | }
54 | }
55 |
56 | /*
57 | public RelativePosition(dynamic json)
58 | {
59 | TypeId = json.type == null ? (ID?)null : new ID((long)json.type.client, (long)json.type.clock);
60 | TName = json.tname ?? null;
61 | Item = json.item == null ? (ID?)null : new ID((long)json.item.client, (long)json.item.clock);
62 | }
63 | */
64 |
65 | private RelativePosition(ID? typeId, string tname, ID? item, int assoc)
66 | {
67 | TypeId = typeId;
68 | TName = tname;
69 | Item = item;
70 | Assoc = assoc;
71 | }
72 |
73 | public bool Equals(RelativePosition other)
74 | {
75 | if (ReferenceEquals(this, other))
76 | {
77 | return true;
78 | }
79 |
80 | return other != null
81 | && string.Equals(TName, other.TName)
82 | && ID.Equals(Item, other.Item)
83 | && ID.Equals(TypeId, other.TypeId)
84 | && Assoc == other.Assoc;
85 | }
86 |
87 | ///
88 | /// Create a relative position based on an absolute position.
89 | ///
90 | public static RelativePosition FromTypeIndex(AbstractType type, int index, int assoc = 0)
91 | {
92 | if (assoc < 0)
93 | {
94 | // Associated with the left character or the beginning of a type, decrement index if possible.
95 | if (index == 0)
96 | {
97 | return new RelativePosition(type, type._item?.Id, assoc);
98 | }
99 |
100 | index--;
101 | }
102 |
103 | var t = type._start;
104 | while (t != null)
105 | {
106 | if (!t.Deleted && t.Countable)
107 | {
108 | if (t.Length > index)
109 | {
110 | // Case 1: found position somewhere in the linked list.
111 | return new RelativePosition(type, new ID(t.Id.Client, t.Id.Clock + index), assoc);
112 | }
113 |
114 | index -= t.Length;
115 | }
116 |
117 | if (t.Right == null && assoc < 0)
118 | {
119 | // Left-associated position, return last available id.
120 | return new RelativePosition(type, t.LastId, assoc);
121 | }
122 |
123 | t = t.Right as Item;
124 | }
125 |
126 | return new RelativePosition(type, type._item?.Id, assoc);
127 | }
128 |
129 | public void Write(Stream writer)
130 | {
131 | if (Item != null)
132 | {
133 | // Case 1: Found position somewhere in the linked list.
134 | writer.WriteVarUint(0);
135 | Item.Value.Write(writer);
136 | }
137 | else if (TName != null)
138 | {
139 | // Case 2: Found position at the end of the list and type is stored in y.share.
140 | writer.WriteVarUint(1);
141 | writer.WriteVarString(TName);
142 | }
143 | else if (TypeId != null)
144 | {
145 | // Case 3: Found position at the end of the list and type is attached to an item.
146 | writer.WriteVarUint(2);
147 | TypeId.Value.Write(writer);
148 | }
149 | else
150 | {
151 | throw new Exception();
152 | }
153 |
154 | writer.WriteVarInt(Assoc, treatZeroAsNegative: false);
155 | }
156 |
157 | public static RelativePosition Read(byte[] encodedPosition)
158 | {
159 | using (var stream = new MemoryStream(encodedPosition))
160 | {
161 | return Read(stream);
162 | }
163 | }
164 |
165 | public static RelativePosition Read(Stream reader)
166 | {
167 | ID? itemId = null;
168 | ID? typeId = null;
169 | string tName = null;
170 |
171 | switch (reader.ReadVarUint())
172 | {
173 | case 0:
174 | // Case 1: Found position somewhere in the linked list.
175 | itemId = ID.Read(reader);
176 | break;
177 | case 1:
178 | // Case 2: Found position at the end of the list and type is stored in y.share.
179 | tName = reader.ReadVarString();
180 | break;
181 | case 2:
182 | // Case 3: Found position at the end of the list and type is attached to an item.
183 | typeId = ID.Read(reader);
184 | break;
185 | default:
186 | throw new Exception();
187 | }
188 |
189 | var assoc = reader.Position < reader.Length ? (int)reader.ReadVarInt().Value : 0;
190 | return new RelativePosition(typeId, tName, itemId, assoc);
191 | }
192 |
193 | public byte[] ToArray()
194 | {
195 | using (var stream = new MemoryStream())
196 | {
197 | Write(stream);
198 | return stream.ToArray();
199 | }
200 | }
201 | }
202 | }
203 |
--------------------------------------------------------------------------------
/src/Ycs/Utils/Snapshot.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System;
8 | using System.Collections.Generic;
9 | using System.IO;
10 | using System.Linq;
11 |
12 | namespace Ycs
13 | {
14 | public sealed class Snapshot : IEquatable
15 | {
16 | internal readonly DeleteSet DeleteSet;
17 | internal readonly IDictionary StateVector;
18 |
19 | internal Snapshot(DeleteSet ds, IDictionary stateMap)
20 | {
21 | DeleteSet = ds;
22 | StateVector = stateMap;
23 | }
24 |
25 | public YDoc RestoreDocument(YDoc originDoc, YDocOptions opts = null)
26 | {
27 | if (originDoc.Gc)
28 | {
29 | // We should try to restore a GC-ed document, because some of the restored items might have their content deleted.
30 | throw new Exception("originDoc must not be garbage collected");
31 | }
32 |
33 | using (var encoder = new UpdateEncoderV2())
34 | {
35 | originDoc.Transact(tr =>
36 | {
37 | int size = StateVector.Count(kvp => kvp.Value /* clock */ > 0);
38 | encoder.RestWriter.WriteVarUint((uint)size);
39 |
40 | // Splitting the structs before writing them to the encoder.
41 | foreach (var kvp in StateVector)
42 | {
43 | var client = kvp.Key;
44 | var clock = kvp.Value;
45 |
46 | if (clock == 0)
47 | {
48 | continue;
49 | }
50 |
51 | if (clock < originDoc.Store.GetState(client))
52 | {
53 | tr.Doc.Store.GetItemCleanStart(tr, new ID(client, clock));
54 | }
55 |
56 | var structs = originDoc.Store.Clients[client];
57 | var lastStructIndex = StructStore.FindIndexSS(structs, clock - 1);
58 |
59 | // Write # encoded structs.
60 | encoder.RestWriter.WriteVarUint((uint)(lastStructIndex + 1));
61 | encoder.WriteClient(client);
62 |
63 | // First clock written is 0.
64 | encoder.RestWriter.WriteVarUint(0);
65 |
66 | for (int i = 0; i <= lastStructIndex; i++)
67 | {
68 | structs[i].Write(encoder, 0);
69 | }
70 | }
71 |
72 | DeleteSet.Write(encoder);
73 | });
74 |
75 | var newDoc = new YDoc(opts ?? originDoc.CloneOptionsWithNewGuid());
76 | newDoc.ApplyUpdateV2(encoder.ToArray(), transactionOrigin: "snapshot");
77 | return newDoc;
78 | }
79 | }
80 |
81 | public bool Equals(Snapshot other)
82 | {
83 | if (other == null)
84 | {
85 | return false;
86 | }
87 |
88 | var ds1 = DeleteSet.Clients;
89 | var ds2 = other.DeleteSet.Clients;
90 | var sv1 = StateVector;
91 | var sv2 = other.StateVector;
92 |
93 | if (sv1.Count != sv2.Count || ds1.Count != ds2.Count)
94 | {
95 | return false;
96 | }
97 |
98 | foreach (var kvp in sv1)
99 | {
100 | if (!sv2.TryGetValue(kvp.Key, out var value) || value != kvp.Value)
101 | {
102 | return false;
103 | }
104 | }
105 |
106 | foreach (var kvp in ds1)
107 | {
108 | var client = kvp.Key;
109 | var dsItems1 = kvp.Value;
110 |
111 | if (!ds2.TryGetValue(client, out var dsItems2))
112 | {
113 | return false;
114 | }
115 |
116 | if (dsItems1.Count != dsItems2.Count)
117 | {
118 | return false;
119 | }
120 |
121 | for (int i = 0; i < dsItems1.Count; i++)
122 | {
123 | var dsItem1 = dsItems1[i];
124 | var dsItem2 = dsItems2[i];
125 | if (dsItem1.Clock != dsItem2.Clock || dsItem1.Length != dsItem2.Length)
126 | {
127 | return false;
128 | }
129 | }
130 | }
131 |
132 | return true;
133 | }
134 |
135 | public byte[] EncodeSnapshotV2()
136 | {
137 | using (var encoder = new DSEncoderV2())
138 | {
139 | DeleteSet.Write(encoder);
140 | EncodingUtils.WriteStateVector(encoder, StateVector);
141 | return encoder.ToArray();
142 | }
143 | }
144 |
145 | public static Snapshot DecodeSnapshot(Stream input)
146 | {
147 | using (var decoder = new DSDecoderV2(input))
148 | {
149 | var ds = DeleteSet.Read(decoder);
150 | var sv = EncodingUtils.ReadStateVector(decoder);
151 | return new Snapshot(ds, sv);
152 | }
153 | }
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/src/Ycs/Utils/UpdateDecoderV2.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System;
8 | using System.Collections.Generic;
9 | using System.Diagnostics;
10 | using System.IO;
11 |
12 | namespace Ycs
13 | {
14 | internal class DSDecoderV2 : IDSDecoder
15 | {
16 | private readonly bool _leaveOpen;
17 | private long _dsCurVal;
18 |
19 | public DSDecoderV2(Stream input, bool leaveOpen = false)
20 | {
21 | _leaveOpen = leaveOpen;
22 | Reader = input;
23 | }
24 |
25 | public Stream Reader { get; private set; }
26 | protected bool Disposed { get; private set; }
27 |
28 | public void Dispose()
29 | {
30 | Dispose(disposing: true);
31 | System.GC.SuppressFinalize(this);
32 | }
33 |
34 | public void ResetDsCurVal()
35 | {
36 | _dsCurVal = 0;
37 | }
38 |
39 | public long ReadDsClock()
40 | {
41 | _dsCurVal += Reader.ReadVarUint();
42 | Debug.Assert(_dsCurVal >= 0);
43 | return _dsCurVal;
44 | }
45 |
46 | public long ReadDsLength()
47 | {
48 | var diff = Reader.ReadVarUint() + 1;
49 | Debug.Assert(diff >= 0);
50 | _dsCurVal += diff;
51 | return diff;
52 | }
53 |
54 | protected virtual void Dispose(bool disposing)
55 | {
56 | if (!Disposed)
57 | {
58 | if (disposing && !_leaveOpen)
59 | {
60 | Reader?.Dispose();
61 | }
62 |
63 | Reader = null;
64 | Disposed = true;
65 | }
66 | }
67 |
68 | [Conditional("DEBUG")]
69 | protected void CheckDisposed()
70 | {
71 | if (Disposed)
72 | {
73 | throw new ObjectDisposedException(GetType().ToString());
74 | }
75 | }
76 | }
77 |
78 | internal sealed class UpdateDecoderV2 : DSDecoderV2, IUpdateDecoder
79 | {
80 | ///
81 | /// List of cached keys. If the keys[id] does not exist, we read a new key from
82 | /// the string encoder and push it to keys.
83 | ///
84 | private List _keys;
85 | private IntDiffOptRleDecoder _keyClockDecoder;
86 | private UintOptRleDecoder _clientDecoder;
87 | private IntDiffOptRleDecoder _leftClockDecoder;
88 | private IntDiffOptRleDecoder _rightClockDecoder;
89 | private RleDecoder _infoDecoder;
90 | private StringDecoder _stringDecoder;
91 | private RleDecoder _parentInfoDecoder;
92 | private UintOptRleDecoder _typeRefDecoder;
93 | private UintOptRleDecoder _lengthDecoder;
94 |
95 | public UpdateDecoderV2(Stream input, bool leaveOpen = false)
96 | : base(input, leaveOpen)
97 | {
98 | _keys = new List();
99 |
100 | // Read feature flag - currently unused.
101 | Reader.ReadByte();
102 |
103 | _keyClockDecoder = new IntDiffOptRleDecoder(Reader.ReadVarUint8ArrayAsStream());
104 | _clientDecoder = new UintOptRleDecoder(Reader.ReadVarUint8ArrayAsStream());
105 | _leftClockDecoder = new IntDiffOptRleDecoder(Reader.ReadVarUint8ArrayAsStream());
106 | _rightClockDecoder = new IntDiffOptRleDecoder(Reader.ReadVarUint8ArrayAsStream());
107 | _infoDecoder = new RleDecoder(Reader.ReadVarUint8ArrayAsStream());
108 | _stringDecoder = new StringDecoder(Reader.ReadVarUint8ArrayAsStream());
109 | _parentInfoDecoder = new RleDecoder(Reader.ReadVarUint8ArrayAsStream());
110 | _typeRefDecoder = new UintOptRleDecoder(Reader.ReadVarUint8ArrayAsStream());
111 | _lengthDecoder = new UintOptRleDecoder(Reader.ReadVarUint8ArrayAsStream());
112 | }
113 |
114 | public ID ReadLeftId()
115 | {
116 | CheckDisposed();
117 | return new ID(_clientDecoder.Read(), _leftClockDecoder.Read());
118 | }
119 |
120 | public ID ReadRightId()
121 | {
122 | CheckDisposed();
123 | return new ID(_clientDecoder.Read(), _rightClockDecoder.Read());
124 | }
125 |
126 | ///
127 | /// Read the next client Id.
128 | ///
129 | public long ReadClient()
130 | {
131 | CheckDisposed();
132 | return _clientDecoder.Read();
133 | }
134 |
135 | public byte ReadInfo()
136 | {
137 | CheckDisposed();
138 | return _infoDecoder.Read();
139 | }
140 |
141 | public string ReadString()
142 | {
143 | CheckDisposed();
144 | return _stringDecoder.Read();
145 | }
146 |
147 | public bool ReadParentInfo()
148 | {
149 | CheckDisposed();
150 | return _parentInfoDecoder.Read() == 1;
151 | }
152 |
153 | public uint ReadTypeRef()
154 | {
155 | CheckDisposed();
156 | return _typeRefDecoder.Read();
157 | }
158 |
159 | public int ReadLength()
160 | {
161 | CheckDisposed();
162 |
163 | var value = (int)_lengthDecoder.Read();
164 | Debug.Assert(value >= 0);
165 | return value;
166 | }
167 |
168 | public object ReadAny()
169 | {
170 | CheckDisposed();
171 | var obj = Reader.ReadAny();
172 | return obj;
173 | }
174 |
175 | public byte[] ReadBuffer()
176 | {
177 | CheckDisposed();
178 | return Reader.ReadVarUint8Array();
179 | }
180 |
181 | public string ReadKey()
182 | {
183 | CheckDisposed();
184 |
185 | var keyClock = (int)_keyClockDecoder.Read();
186 | if (keyClock < _keys.Count)
187 | {
188 | return _keys[keyClock];
189 | }
190 | else
191 | {
192 | var key = _stringDecoder.Read();
193 | _keys.Add(key);
194 | return key;
195 | }
196 | }
197 |
198 | public object ReadJson()
199 | {
200 | CheckDisposed();
201 |
202 | var jsonString = Reader.ReadVarString();
203 | var result = Newtonsoft.Json.JsonConvert.DeserializeObject(jsonString);
204 | return result;
205 | }
206 |
207 | protected override void Dispose(bool disposing)
208 | {
209 | if (!Disposed)
210 | {
211 | if (disposing)
212 | {
213 | _keyClockDecoder?.Dispose();
214 | _clientDecoder?.Dispose();
215 | _leftClockDecoder?.Dispose();
216 | _rightClockDecoder?.Dispose();
217 | _infoDecoder?.Dispose();
218 | _stringDecoder?.Dispose();
219 | _parentInfoDecoder?.Dispose();
220 | _typeRefDecoder?.Dispose();
221 | _lengthDecoder?.Dispose();
222 | }
223 |
224 | _keyClockDecoder = null;
225 | _clientDecoder = null;
226 | _leftClockDecoder = null;
227 | _rightClockDecoder = null;
228 | _infoDecoder = null;
229 | _stringDecoder = null;
230 | _parentInfoDecoder = null;
231 | _typeRefDecoder = null;
232 | _lengthDecoder = null;
233 | }
234 |
235 | base.Dispose(disposing);
236 | }
237 | }
238 | }
239 |
--------------------------------------------------------------------------------
/src/Ycs/Utils/UpdateEncoderV2.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System;
8 | using System.Collections.Generic;
9 | using System.Diagnostics;
10 | using System.IO;
11 |
12 | namespace Ycs
13 | {
14 | internal class DSEncoderV2 : IDSEncoder
15 | {
16 | private long _dsCurVal;
17 |
18 | public DSEncoderV2()
19 | {
20 | _dsCurVal = 0;
21 | RestWriter = new MemoryStream();
22 | }
23 |
24 | public Stream RestWriter { get; private set; }
25 | protected bool Disposed { get; private set; }
26 |
27 | public void Dispose()
28 | {
29 | Dispose(disposing: true);
30 | System.GC.SuppressFinalize(this);
31 | }
32 |
33 | public void ResetDsCurVal()
34 | {
35 | _dsCurVal = 0;
36 | }
37 |
38 | public void WriteDsClock(long clock)
39 | {
40 | var diff = clock - _dsCurVal;
41 | Debug.Assert(diff >= 0);
42 | _dsCurVal = clock;
43 | RestWriter.WriteVarUint((uint)diff);
44 | }
45 |
46 | public void WriteDsLength(long length)
47 | {
48 | if (length <= 0)
49 | {
50 | throw new ArgumentOutOfRangeException();
51 | }
52 |
53 | RestWriter.WriteVarUint((uint)(length - 1));
54 | _dsCurVal += length;
55 | }
56 |
57 | public virtual byte[] ToArray()
58 | {
59 | return ((MemoryStream)RestWriter).ToArray();
60 | }
61 |
62 | protected virtual void Dispose(bool disposing)
63 | {
64 | if (!Disposed)
65 | {
66 | if (disposing)
67 | {
68 | RestWriter.Dispose();
69 | }
70 |
71 | RestWriter = null;
72 | Disposed = true;
73 | }
74 | }
75 | }
76 |
77 | internal sealed class UpdateEncoderV2 : DSEncoderV2, IUpdateEncoder
78 | {
79 | // Refers to the next unique key-identifier to be used.
80 | private int _keyClock;
81 | private IDictionary _keyMap;
82 |
83 | private IntDiffOptRleEncoder _keyClockEncoder;
84 | private UintOptRleEncoder _clientEncoder;
85 | private IntDiffOptRleEncoder _leftClockEncoder;
86 | private IntDiffOptRleEncoder _rightClockEncoder;
87 | private RleEncoder _infoEncoder;
88 | private StringEncoder _stringEncoder;
89 | private RleEncoder _parentInfoEncoder;
90 | private UintOptRleEncoder _typeRefEncoder;
91 | private UintOptRleEncoder _lengthEncoder;
92 |
93 | public UpdateEncoderV2()
94 | {
95 | _keyClock = 0;
96 |
97 | _keyMap = new Dictionary();
98 | _keyClockEncoder = new IntDiffOptRleEncoder();
99 | _clientEncoder = new UintOptRleEncoder();
100 | _leftClockEncoder = new IntDiffOptRleEncoder();
101 | _rightClockEncoder = new IntDiffOptRleEncoder();
102 | _infoEncoder = new RleEncoder();
103 | _stringEncoder = new StringEncoder();
104 | _parentInfoEncoder = new RleEncoder();
105 | _typeRefEncoder = new UintOptRleEncoder();
106 | _lengthEncoder = new UintOptRleEncoder();
107 | }
108 |
109 | public override byte[] ToArray()
110 | {
111 | using (var stream = new MemoryStream())
112 | {
113 | // Read the feature flag that might be used in the future.
114 | stream.WriteByte(0);
115 |
116 | // TODO: [alekseyk] Maybe pass the writer directly instead of using ToArray()?
117 | stream.WriteVarUint8Array(_keyClockEncoder.ToArray());
118 | stream.WriteVarUint8Array(_clientEncoder.ToArray());
119 | stream.WriteVarUint8Array(_leftClockEncoder.ToArray());
120 | stream.WriteVarUint8Array(_rightClockEncoder.ToArray());
121 | stream.WriteVarUint8Array(_infoEncoder.ToArray());
122 | stream.WriteVarUint8Array(_stringEncoder.ToArray());
123 | stream.WriteVarUint8Array(_parentInfoEncoder.ToArray());
124 | stream.WriteVarUint8Array(_typeRefEncoder.ToArray());
125 | stream.WriteVarUint8Array(_lengthEncoder.ToArray());
126 |
127 | // Append the rest of the data from the RestWriter.
128 | // Note it's not the 'WriteVarUint8Array'.
129 | var content = base.ToArray();
130 | stream.Write(content, 0, content.Length);
131 |
132 | return stream.ToArray();
133 | }
134 | }
135 |
136 | public void WriteLeftId(ID id)
137 | {
138 | _clientEncoder.Write((uint)id.Client);
139 | _leftClockEncoder.Write(id.Clock);
140 | }
141 |
142 | public void WriteRightId(ID id)
143 | {
144 | _clientEncoder.Write((uint)id.Client);
145 | _rightClockEncoder.Write(id.Clock);
146 | }
147 |
148 | public void WriteClient(long client)
149 | {
150 | _clientEncoder.Write((uint)client);
151 | }
152 |
153 | public void WriteInfo(byte info)
154 | {
155 | _infoEncoder.Write(info);
156 | }
157 |
158 | public void WriteString(string s)
159 | {
160 | _stringEncoder.Write(s);
161 | }
162 |
163 | public void WriteParentInfo(bool isYKey)
164 | {
165 | _parentInfoEncoder.Write((byte)(isYKey ? 1 : 0));
166 | }
167 |
168 | public void WriteTypeRef(uint info)
169 | {
170 | _typeRefEncoder.Write(info);
171 | }
172 |
173 | public void WriteLength(int len)
174 | {
175 | Debug.Assert(len >= 0);
176 | _lengthEncoder.Write((uint)len);
177 | }
178 |
179 | public void WriteAny(object any)
180 | {
181 | RestWriter.WriteAny(any);
182 | }
183 |
184 | public void WriteBuffer(byte[] data)
185 | {
186 | RestWriter.WriteVarUint8Array(data);
187 | }
188 |
189 | ///
190 | /// Property keys are often reused. For example, in y-prosemirror the key 'bold'
191 | /// might occur very often. For a 3D application, the key 'position' might occur often.
192 | ///
193 | /// We can these keys in a map and refer to them via a unique number.
194 | ///
195 | public void WriteKey(string key)
196 | {
197 | _keyClockEncoder.Write(_keyClock++);
198 |
199 | if (!_keyMap.ContainsKey(key))
200 | {
201 | _stringEncoder.Write(key);
202 | }
203 | }
204 |
205 | public void WriteJson(T any)
206 | {
207 | var jsonString = Newtonsoft.Json.JsonConvert.SerializeObject(any, typeof(T), null);
208 | RestWriter.WriteVarString(jsonString);
209 | }
210 |
211 | protected override void Dispose(bool disposing)
212 | {
213 | if (!Disposed)
214 | {
215 | if (disposing)
216 | {
217 | _keyMap.Clear();
218 | _keyClockEncoder.Dispose();
219 | _clientEncoder.Dispose();
220 | _leftClockEncoder.Dispose();
221 | _rightClockEncoder.Dispose();
222 | _infoEncoder.Dispose();
223 | _stringEncoder.Dispose();
224 | _parentInfoEncoder.Dispose();
225 | _typeRefEncoder.Dispose();
226 | _lengthEncoder.Dispose();
227 | }
228 |
229 | _keyMap = null;
230 | _keyClockEncoder = null;
231 | _clientEncoder = null;
232 | _leftClockEncoder = null;
233 | _rightClockEncoder = null;
234 | _infoEncoder = null;
235 | _stringEncoder = null;
236 | _parentInfoEncoder = null;
237 | _typeRefEncoder = null;
238 | _lengthEncoder = null;
239 | }
240 |
241 | base.Dispose(disposing);
242 | }
243 | }
244 | }
245 |
--------------------------------------------------------------------------------
/src/Ycs/Utils/YEvent.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System.Collections.Generic;
8 | using System.Linq;
9 |
10 | namespace Ycs
11 | {
12 | public class ChangesCollection
13 | {
14 | public ISet- Added;
15 | public ISet
- Deleted;
16 | public IList
Delta;
17 | public IDictionary Keys;
18 | }
19 |
20 | public class Delta
21 | {
22 | public object Insert;
23 | public int? Delete;
24 | public int? Retain;
25 | public IDictionary Attributes;
26 | }
27 |
28 | public enum ChangeAction
29 | {
30 | Add,
31 | Update,
32 | Delete
33 | }
34 |
35 | public class ChangeKey
36 | {
37 | public ChangeAction Action;
38 | public object OldValue;
39 | }
40 |
41 | public class YEvent
42 | {
43 | private ChangesCollection _changes = null;
44 |
45 | internal YEvent(AbstractType target, Transaction transaction)
46 | {
47 | Target = target;
48 | CurrentTarget = target;
49 | Transaction = transaction;
50 | }
51 |
52 | public AbstractType Target { get; set; }
53 | public AbstractType CurrentTarget { get; set; }
54 | public Transaction Transaction { get; set; }
55 |
56 | public IReadOnlyCollection Path => GetPathTo(CurrentTarget, Target);
57 | public ChangesCollection Changes => CollectChanges();
58 |
59 | ///
60 | /// Check if a struct is added by this event.
61 | ///
62 | internal bool Deletes(AbstractStruct str)
63 | {
64 | return Transaction.DeleteSet.IsDeleted(str.Id);
65 | }
66 |
67 | internal bool Adds(AbstractStruct str)
68 | {
69 | return !Transaction.BeforeState.TryGetValue(str.Id.Client, out var clock) || str.Id.Clock >= clock;
70 | }
71 |
72 | private ChangesCollection CollectChanges()
73 | {
74 | if (_changes == null)
75 | {
76 | var target = Target;
77 | var added = new HashSet- ();
78 | var deleted = new HashSet
- ();
79 | var delta = new List
();
80 | var keys = new Dictionary();
81 |
82 | _changes = new ChangesCollection
83 | {
84 | Added = added,
85 | Deleted = deleted,
86 | Delta = delta,
87 | Keys = keys
88 | };
89 |
90 | if (!Transaction.Changed.TryGetValue(Target, out var changed))
91 | {
92 | changed = new HashSet();
93 | Transaction.Changed[Target] = changed;
94 | }
95 |
96 | if (changed.Contains(null))
97 | {
98 | Delta lastOp = null;
99 |
100 | void packOp()
101 | {
102 | if (lastOp != null)
103 | {
104 | delta.Add(lastOp);
105 | }
106 | }
107 |
108 | for (var item = Target._start; item != null; item = item.Right as Item)
109 | {
110 | if (item.Deleted)
111 | {
112 | if (Deletes(item) && !Adds(item))
113 | {
114 | if (lastOp == null || lastOp.Delete == null)
115 | {
116 | packOp();
117 | lastOp = new Delta { Delete = 0 };
118 | }
119 |
120 | lastOp.Delete += item.Length;
121 | deleted.Add(item);
122 | }
123 | else
124 | {
125 | // Do nothing.
126 | }
127 | }
128 | else
129 | {
130 | if (Adds(item))
131 | {
132 | if (lastOp == null || lastOp.Insert == null)
133 | {
134 | packOp();
135 | lastOp = new Delta { Insert = new List(1) };
136 | }
137 |
138 | (lastOp.Insert as List).AddRange(item.Content.GetContent());
139 | added.Add(item);
140 | }
141 | else
142 | {
143 | if (lastOp == null || lastOp.Retain == null)
144 | {
145 | packOp();
146 | lastOp = new Delta { Retain = 0 };
147 | }
148 |
149 | lastOp.Retain += item.Length;
150 | }
151 | }
152 | }
153 |
154 | if (lastOp != null && lastOp.Retain == null)
155 | {
156 | packOp();
157 | }
158 | }
159 |
160 | foreach (var key in changed)
161 | {
162 | if (key != null)
163 | {
164 | ChangeAction action;
165 | object oldValue;
166 | var item = target._map[key];
167 |
168 | if (Adds(item))
169 | {
170 | var prev = item.Left;
171 | while (prev != null && Adds(prev))
172 | {
173 | prev = (prev as Item).Left;
174 | }
175 |
176 | if (Deletes(item))
177 | {
178 | if (prev != null && Deletes(prev))
179 | {
180 | action = ChangeAction.Delete;
181 | oldValue = (prev as Item).Content.GetContent().Last();
182 | }
183 | else
184 | {
185 | break;
186 | }
187 | }
188 | else
189 | {
190 | if (prev != null && Deletes(prev))
191 | {
192 | action = ChangeAction.Update;
193 | oldValue = (prev as Item).Content.GetContent().Last();
194 | }
195 | else
196 | {
197 | action = ChangeAction.Add;
198 | oldValue = null;
199 | }
200 | }
201 | }
202 | else
203 | {
204 | if (Deletes(item))
205 | {
206 | action = ChangeAction.Delete;
207 | oldValue = item.Content.GetContent().Last();
208 | }
209 | else
210 | {
211 | break;
212 | }
213 | }
214 |
215 | keys[key] = new ChangeKey { Action = action, OldValue = oldValue };
216 | }
217 | }
218 | }
219 |
220 | return _changes;
221 | }
222 |
223 | ///
224 | /// Compute the path from this type to the specified target.
225 | ///
226 | private IReadOnlyCollection GetPathTo(AbstractType parent, AbstractType child)
227 | {
228 | var path = new Stack();
229 |
230 | while (child._item != null && child != parent)
231 | {
232 | if (!string.IsNullOrEmpty(child._item.ParentSub))
233 | {
234 | // Parent is map-ish.
235 | path.Push(child._item.ParentSub);
236 | }
237 | else
238 | {
239 | // Parent is array-ish.
240 | int i = 0;
241 | AbstractStruct c = (child._item.Parent as AbstractType)._start;
242 | while (c != child._item && c != null)
243 | {
244 | if (!c.Deleted)
245 | {
246 | i++;
247 | }
248 |
249 | c = (c as Item)?.Right;
250 | }
251 |
252 | path.Push(i);
253 | }
254 |
255 | child = child._item.Parent as AbstractType;
256 | }
257 |
258 | return path;
259 | }
260 | }
261 | }
262 |
--------------------------------------------------------------------------------
/src/Ycs/Ycs.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0;netstandard2.1
5 | Library
6 | Microsoft
7 | 0.0.1
8 | .Net implementation of the Yjs library.
9 | Copyright (c) 2021
10 |
11 | https://github.com/yjs/ycs/
12 | LICENSE
13 | https://github.com/yjs/ycs/
14 | Yjs, Ycs, CRDT, offline, shared editing, concurrency, collaboration
15 | 0.0.1.0
16 | Aleksey Kabanov
17 |
18 |
19 |
20 | true
21 |
22 |
23 |
24 | true
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | True
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/Ycs/lib0/Decoding/AbstractStreamDecoder.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System;
8 | using System.Diagnostics;
9 | using System.IO;
10 |
11 | namespace Ycs
12 | {
13 | ///
14 | internal abstract class AbstractStreamDecoder : IDecoder
15 | {
16 | private readonly bool _leaveOpen;
17 |
18 | protected AbstractStreamDecoder(Stream input, bool leaveOpen = false)
19 | {
20 | Debug.Assert(input != null);
21 |
22 | Stream = input;
23 | _leaveOpen = leaveOpen;
24 | }
25 |
26 | protected Stream Stream { get; private set; }
27 | protected bool Disposed { get; private set; }
28 |
29 | protected bool HasContent => Stream.Position < Stream.Length;
30 |
31 | ///
32 | public abstract T Read();
33 |
34 | public void Dispose()
35 | {
36 | Dispose(disposing: true);
37 | System.GC.SuppressFinalize(this);
38 | }
39 |
40 | protected virtual void Dispose(bool disposing)
41 | {
42 | if (!Disposed)
43 | {
44 | if (disposing && !_leaveOpen)
45 | {
46 | Stream?.Dispose();
47 | }
48 |
49 | Stream = null;
50 | Disposed = true;
51 | }
52 | }
53 |
54 | [Conditional("DEBUG")]
55 | protected void CheckDisposed()
56 | {
57 | if (Disposed)
58 | {
59 | throw new ObjectDisposedException(GetType().ToString());
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/Ycs/lib0/Decoding/IDecoder.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System;
8 |
9 | namespace Ycs
10 | {
11 | ///
12 | internal interface IDecoder : IDisposable
13 | {
14 | ///
15 | /// Reads the next element from the underlying data stream.
16 | ///
17 | T Read();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Ycs/lib0/Decoding/IncUintOptRleDecoder.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System.IO;
8 |
9 | namespace Ycs
10 | {
11 | ///
12 | internal class IncUintOptRleDecoder : AbstractStreamDecoder
13 | {
14 | private uint _state;
15 | private uint _count;
16 |
17 | public IncUintOptRleDecoder(Stream input, bool leaveOpen = false)
18 | : base(input, leaveOpen)
19 | {
20 | // Do nothing.
21 | }
22 |
23 | ///
24 | public override uint Read()
25 | {
26 | CheckDisposed();
27 |
28 | if (_count == 0)
29 | {
30 | var (value, sign) = Stream.ReadVarInt();
31 |
32 | // If the sign is negative, we read the count too; otherwise. count is 1.
33 | bool isNegative = sign < 0;
34 | if (isNegative)
35 | {
36 | _state = (uint)(-value);
37 | _count = Stream.ReadVarUint() + 2;
38 | }
39 | else
40 | {
41 | _state = (uint)value;
42 | _count = 1;
43 | }
44 | }
45 |
46 | _count--;
47 | return _state++;
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Ycs/lib0/Decoding/IntDiffDecoder.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System.IO;
8 |
9 | namespace Ycs
10 | {
11 | ///
12 | internal class IntDiffDecoder : AbstractStreamDecoder
13 | {
14 | private long _state;
15 |
16 | public IntDiffDecoder(Stream input, long start, bool leaveOpen = false)
17 | : base(input, leaveOpen)
18 | {
19 | _state = start;
20 | }
21 |
22 | ///
23 | public override long Read()
24 | {
25 | CheckDisposed();
26 |
27 | _state += Stream.ReadVarInt().Value;
28 | return _state;
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Ycs/lib0/Decoding/IntDiffOptRleDecoder.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System.IO;
8 |
9 | namespace Ycs
10 | {
11 | ///
12 | internal class IntDiffOptRleDecoder : AbstractStreamDecoder
13 | {
14 | private long _state;
15 | private uint _count;
16 | private long _diff;
17 |
18 | public IntDiffOptRleDecoder(Stream input, bool leaveOpen = false)
19 | : base(input, leaveOpen)
20 | {
21 | // Do nothing.
22 | }
23 |
24 | ///
25 | public override long Read()
26 | {
27 | CheckDisposed();
28 |
29 | if (_count == 0)
30 | {
31 | var diff = Stream.ReadVarInt().Value;
32 |
33 | // If the first bit is set, we read more data.
34 | bool hasCount = (diff & Bit.Bit1) > 0;
35 |
36 | if (diff < 0)
37 | {
38 | _diff = -((-diff) >> 1);
39 | }
40 | else
41 | {
42 | _diff = diff >> 1;
43 | }
44 |
45 | _count = hasCount ? Stream.ReadVarUint() + 2 : 1;
46 | }
47 |
48 | _state += _diff;
49 | _count--;
50 | return _state;
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Ycs/lib0/Decoding/RleDecoder.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System.Diagnostics;
8 | using System.IO;
9 |
10 | namespace Ycs
11 | {
12 | ///
13 | internal class RleDecoder : AbstractStreamDecoder
14 | {
15 | private byte _state;
16 | private long _count;
17 |
18 | public RleDecoder(Stream input, bool leaveOpen = false)
19 | : base(input, leaveOpen)
20 | {
21 | // Do nothing.
22 | }
23 |
24 | ///
25 | public override byte Read()
26 | {
27 | CheckDisposed();
28 |
29 | if (_count == 0)
30 | {
31 | _state = Stream._ReadByte();
32 |
33 | if (HasContent)
34 | {
35 | // See encoder implementation for the reason why this is incremented.
36 | _count = Stream.ReadVarUint() + 1;
37 | Debug.Assert(_count > 0);
38 | }
39 | else
40 | {
41 | // Read the current value forever.
42 | _count = -1;
43 | }
44 | }
45 |
46 | _count--;
47 | return _state;
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Ycs/lib0/Decoding/RleIntDiffDecoder.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System.Diagnostics;
8 | using System.IO;
9 |
10 | namespace Ycs
11 | {
12 | ///
13 | internal class RleIntDiffDecoder : AbstractStreamDecoder
14 | {
15 | private long _state;
16 | private long _count;
17 |
18 | public RleIntDiffDecoder(Stream input, long start, bool leaveOpen = false)
19 | : base(input, leaveOpen)
20 | {
21 | _state = start;
22 | }
23 |
24 | ///
25 | public override long Read()
26 | {
27 | CheckDisposed();
28 |
29 | if (_count == 0)
30 | {
31 | _state += Stream.ReadVarInt().Value;
32 |
33 | if (HasContent)
34 | {
35 | // See encoder implementation for the reason why this is incremented.
36 | _count = Stream.ReadVarUint() + 1;
37 | Debug.Assert(_count > 0);
38 | }
39 | else
40 | {
41 | // Read the current value forever.
42 | _count = -1;
43 | }
44 | }
45 |
46 | _count--;
47 | return _state;
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Ycs/lib0/Decoding/StringDecoder.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System;
8 | using System.Diagnostics;
9 | using System.IO;
10 |
11 | namespace Ycs
12 | {
13 | ///
14 | internal class StringDecoder : IDecoder
15 | {
16 | private UintOptRleDecoder _lengthDecoder;
17 | private string _value;
18 | private int _pos;
19 | private bool _disposed;
20 |
21 | public StringDecoder(Stream input, bool leaveOpen = false)
22 | {
23 | Debug.Assert(input != null);
24 |
25 | _value = input.ReadVarString();
26 | _lengthDecoder = new UintOptRleDecoder(input, leaveOpen);
27 | }
28 |
29 | public void Dispose()
30 | {
31 | Dispose(disposing: true);
32 | System.GC.SuppressFinalize(this);
33 | }
34 |
35 | ///
36 | public string Read()
37 | {
38 | CheckDisposed();
39 |
40 | var length = (int)_lengthDecoder.Read();
41 | if (length == 0)
42 | {
43 | return string.Empty;
44 | }
45 |
46 | var result = _value.Substring(_pos, length);
47 | _pos += length;
48 |
49 | // No need to keep the string in memory anymore.
50 | // This also covers the case when nothing but empty strings are left.
51 | if (_pos >= _value.Length)
52 | {
53 | _value = null;
54 | }
55 |
56 | return result;
57 | }
58 |
59 | protected virtual void Dispose(bool disposing)
60 | {
61 | if (!_disposed)
62 | {
63 | if (disposing)
64 | {
65 | _lengthDecoder?.Dispose();
66 | }
67 |
68 | _lengthDecoder = null;
69 | _disposed = true;
70 | }
71 | }
72 |
73 | [Conditional("DEBUG")]
74 | protected void CheckDisposed()
75 | {
76 | if (_disposed)
77 | {
78 | throw new ObjectDisposedException(GetType().ToString());
79 | }
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/Ycs/lib0/Decoding/UintOptRleDecoder.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System.IO;
8 |
9 | namespace Ycs
10 | {
11 | ///
12 | internal class UintOptRleDecoder : AbstractStreamDecoder
13 | {
14 | private uint _state;
15 | private uint _count;
16 |
17 | public UintOptRleDecoder(Stream input, bool leaveOpen = false)
18 | : base(input, leaveOpen)
19 | {
20 | // Do nothing.
21 | }
22 |
23 | public override uint Read()
24 | {
25 | CheckDisposed();
26 |
27 | if (_count == 0)
28 | {
29 | var (value, sign) = Stream.ReadVarInt();
30 |
31 | // If the sign is negative, we read the count too; otherwise, count is 1.
32 | bool isNegative = sign < 0;
33 | if (isNegative)
34 | {
35 | _state = (uint)(-value);
36 | _count = Stream.ReadVarUint() + 2;
37 | }
38 | else
39 | {
40 | _state = (uint)value;
41 | _count = 1;
42 | }
43 | }
44 |
45 | _count--;
46 | return _state;
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Ycs/lib0/Encoding/AbstractStreamEncoder.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System;
8 | using System.Diagnostics;
9 | using System.IO;
10 |
11 | namespace Ycs
12 | {
13 | ///
14 | internal abstract class AbstractStreamEncoder : IEncoder
15 | {
16 | protected AbstractStreamEncoder()
17 | {
18 | Stream = new MemoryStream();
19 | }
20 |
21 | protected MemoryStream Stream { get; private set; }
22 | protected bool Disposed { get; private set; }
23 |
24 | ///
25 | public void Dispose()
26 | {
27 | Dispose(disposing: true);
28 | System.GC.SuppressFinalize(this);
29 | }
30 |
31 | ///
32 | public abstract void Write(T value);
33 |
34 | ///
35 | public virtual byte[] ToArray()
36 | {
37 | Flush();
38 | return Stream.ToArray();
39 | }
40 |
41 | ///
42 | public virtual (byte[] buffer, int length) GetBuffer()
43 | {
44 | Flush();
45 | return (Stream.GetBuffer(), (int)Stream.Length);
46 | }
47 |
48 | protected virtual void Flush()
49 | {
50 | CheckDisposed();
51 | }
52 |
53 | protected virtual void Dispose(bool disposing)
54 | {
55 | if (!Disposed)
56 | {
57 | if (disposing)
58 | {
59 | Stream?.Dispose();
60 | }
61 |
62 | Stream = null;
63 | Disposed = true;
64 | }
65 | }
66 |
67 | [Conditional("DEBUG")]
68 | protected void CheckDisposed()
69 | {
70 | if (Disposed)
71 | {
72 | throw new ObjectDisposedException(GetType().ToString());
73 | }
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/Ycs/lib0/Encoding/IEncoder.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System;
8 |
9 | namespace Ycs
10 | {
11 | ///
12 | internal interface IEncoder : IDisposable
13 | {
14 | void Write(T value);
15 |
16 | ///
17 | /// Returns a copy of the encode contents.
18 | ///
19 | byte[] ToArray();
20 |
21 | ///
22 | /// Returns the current raw buffer of the encoder.
23 | /// This buffer is valid only until the encoder is not disposed.
24 | ///
25 | ///
26 | (byte[] buffer, int length) GetBuffer();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Ycs/lib0/Encoding/IncUintOptRleEncoder.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System.Diagnostics;
8 |
9 | namespace Ycs
10 | {
11 | ///
12 | /// Increasing Uint Optimized RLE Encoder.
13 | ///
14 | /// The RLE encoder counts the number of same occurences of the same value.
15 | /// The counts if the value increases.
16 | ///
17 | /// I.e. [7, 8, 9, 10] will be encoded as [-7, 4] , and [1, 3, 5] will be encoded as [1, 3, 5] .
18 | ///
19 | ///
20 | internal class IncUintOptRleEncoder : AbstractStreamEncoder
21 | {
22 | private uint _state;
23 | private uint _count;
24 |
25 | public IncUintOptRleEncoder()
26 | {
27 | // Do nothing.
28 | }
29 |
30 | ///
31 | public override void Write(uint value)
32 | {
33 | Debug.Assert(value <= int.MaxValue);
34 | CheckDisposed();
35 |
36 | if (_state + _count == value)
37 | {
38 | _count++;
39 | }
40 | else
41 | {
42 | WriteEncodedValue();
43 |
44 | _count = 1;
45 | _state = value;
46 | }
47 | }
48 |
49 | protected override void Flush()
50 | {
51 | WriteEncodedValue();
52 | base.Flush();
53 | }
54 |
55 | private void WriteEncodedValue()
56 | {
57 | if (_count > 0)
58 | {
59 | // Flush counter, unless this is the first value (count = 0).
60 | // Case 1: Just a single value. Set sign to positive.
61 | // Case 2: Write several values. Set sign to negative to indicate that there is a length coming.
62 | if (_count == 1)
63 | {
64 | Stream.WriteVarInt(_state);
65 | }
66 | else
67 | {
68 | // Specify 'treatZeroAsNegative' in case we pass the '-0' value.
69 | Stream.WriteVarInt(-_state, treatZeroAsNegative: _state == 0);
70 |
71 | // Since count is always >1, we can decrement by one. Non-standard encoding.
72 | Stream.WriteVarUint(_count - 2);
73 | }
74 | }
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Ycs/lib0/Encoding/IntDiffEncoder.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | namespace Ycs
8 | {
9 | ///
10 | /// Basic diff encoder using variable length encoding.
11 | /// Encodes the values [3, 1100, 1101, 1050, 0] to [3, 1097, 1, -51, -1050] .
12 | ///
13 | ///
14 | internal class IntDiffEncoder : AbstractStreamEncoder
15 | {
16 | private long _state;
17 |
18 | public IntDiffEncoder(long start)
19 | {
20 | _state = start;
21 | }
22 |
23 | ///
24 | public override void Write(long value)
25 | {
26 | CheckDisposed();
27 |
28 | Stream.WriteVarInt(value - _state);
29 | _state = value;
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Ycs/lib0/Encoding/IntDiffOptRleEncoder.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System.Diagnostics;
8 |
9 | namespace Ycs
10 | {
11 | ///
12 | /// A combination of the and the .
13 | /// The count approach is similar to the , but instead of using
14 | /// the negative bitflag, it encodes in the LSB whether a count is to be read.
15 | ///
16 | /// WARNING: Therefore this encoder only supports 31 bit integers.
17 | ///
18 | /// Encodes [1, 2, 3, 2] as [3, 1, -2] (more specifically [(1 << 1) | 1, (3 << 0) | 0, -((1 << 1) | 0)] ).
19 | ///
20 | /// Internally uses variable length encoding. Contrary to the normal UintVar encoding, the first byte contains:
21 | /// * 1 bit that denotes whether the next value is a count (LSB).
22 | /// * 1 bit that denotes whether this value is negative (MSB - 1).
23 | /// * 1 bit that denotes whether to continue reading the variable length integer (MSB).
24 | ///
25 | /// Therefore, only five bits remain to encode diff ranges.
26 | ///
27 | /// Use this encoder only when appropriate. In most cases, this is probably a bad idea.
28 | ///
29 | ///
30 | internal class IntDiffOptRleEncoder : AbstractStreamEncoder
31 | {
32 | private long _state = 0;
33 | private long _diff = 0;
34 | private uint _count = 0;
35 |
36 | public IntDiffOptRleEncoder()
37 | {
38 | // Do nothing.
39 | }
40 |
41 | ///
42 | public override void Write(long value)
43 | {
44 | Debug.Assert(value <= Bits.Bits30);
45 | CheckDisposed();
46 |
47 | if (_diff == value - _state)
48 | {
49 | _state = value;
50 | _count++;
51 | }
52 | else
53 | {
54 | WriteEncodedValue();
55 |
56 | _count = 1;
57 | _diff = value - _state;
58 | _state = value;
59 | }
60 | }
61 |
62 | protected override void Flush()
63 | {
64 | WriteEncodedValue();
65 | base.Flush();
66 | }
67 |
68 | private void WriteEncodedValue()
69 | {
70 | if (_count > 0)
71 | {
72 | long encodedDiff;
73 | if (_diff < 0)
74 | {
75 | encodedDiff = -(((uint)(-_diff) << 1) | (uint)(_count == 1 ? 0 : 1));
76 | }
77 | else
78 | {
79 | // 31bit making up a diff | whether to write the counter.
80 | encodedDiff = ((uint)_diff << 1) | (uint)(_count == 1 ? 0 : 1);
81 | }
82 |
83 | Stream.WriteVarInt(encodedDiff);
84 |
85 | if (_count > 1)
86 | {
87 | // Since count is always >1, we can decrement by one. Non-standard encoding.
88 | Stream.WriteVarUint(_count - 2);
89 | }
90 | }
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/Ycs/lib0/Encoding/RleEncoder.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | namespace Ycs
8 | {
9 | ///
10 | /// Basic Run Length Encoder - a basic compression implementation.
11 | /// Encodes [1, 1, 1, 7] to [1, 3, 7, 1] (3 times '1', 1 time '7').
12 | ///
13 | /// This encoder might do more harm than good if there are a lot of values that are not repeated.
14 | ///
15 | /// It was originally used for image compression.
16 | ///
17 | ///
18 | internal class RleEncoder : AbstractStreamEncoder
19 | {
20 | private byte? _state = null;
21 | private uint _count = 0;
22 |
23 | public RleEncoder()
24 | {
25 | // Do nothing.
26 | }
27 |
28 | ///
29 | public override void Write(byte value)
30 | {
31 | CheckDisposed();
32 |
33 | if (_state == value)
34 | {
35 | _count++;
36 | }
37 | else
38 | {
39 | if (_count > 0)
40 | {
41 | // Flush counter, unless this is the first value (count = 0).
42 | // Since 'count' is always >0, we can decrement by one. Non-standard encoding.
43 | Stream.WriteVarUint(_count - 1);
44 | }
45 |
46 | Stream.WriteByte(value);
47 |
48 | _count = 1;
49 | _state = value;
50 | }
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Ycs/lib0/Encoding/RleIntDiffEncoder.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | namespace Ycs
8 | {
9 | ///
10 | /// A combination of and .
11 | ///
12 | /// Basically first writes the and then counts duplicate
13 | /// diffs using the .
14 | ///
15 | /// Encodes values [1, 1, 1, 2, 3, 4, 5, 6] as [1, 1, 0, 2, 1, 5] .
16 | ///
17 | ///
18 | internal sealed class RleIntDiffEncoder : AbstractStreamEncoder
19 | {
20 | private long _state;
21 | private uint _count;
22 |
23 | public RleIntDiffEncoder(long start)
24 | {
25 | _state = start;
26 | }
27 |
28 | ///
29 | public override void Write(long value)
30 | {
31 | CheckDisposed();
32 |
33 | if (_state == value && _count > 0)
34 | {
35 | _count++;
36 | }
37 | else
38 | {
39 | if (_count > 0)
40 | {
41 | Stream.WriteVarUint(_count - 1);
42 | }
43 |
44 | Stream.WriteVarInt(value - _state);
45 |
46 | _count = 1;
47 | _state = value;
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Ycs/lib0/Encoding/StringEncoder.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System;
8 | using System.Diagnostics;
9 | using System.IO;
10 | using System.Text;
11 |
12 | namespace Ycs
13 | {
14 | ///
15 | /// Optimized String Encoder.
16 | ///
17 | /// The lengths are encoded using the .
18 | ///
19 | ///
20 | internal class StringEncoder : IEncoder
21 | {
22 | private StringBuilder _sb;
23 | private UintOptRleEncoder _lengthEncoder;
24 | private bool _disposed;
25 |
26 | public StringEncoder()
27 | {
28 | _sb = new StringBuilder();
29 | _lengthEncoder = new UintOptRleEncoder();
30 | }
31 |
32 | ///
33 | public void Dispose()
34 | {
35 | Dispose(disposing: true);
36 | System.GC.SuppressFinalize(this);
37 | }
38 |
39 | ///
40 | public void Write(string value)
41 | {
42 | _sb.Append(value);
43 | _lengthEncoder.Write((uint)value.Length);
44 | }
45 |
46 | public void Write(char[] value, int offset, int count)
47 | {
48 | _sb.Append(value, offset, count);
49 | _lengthEncoder.Write((uint)count);
50 | }
51 |
52 | public byte[] ToArray()
53 | {
54 | using (var stream = new MemoryStream())
55 | {
56 | stream.WriteVarString(_sb.ToString());
57 |
58 | var (buffer, length) = _lengthEncoder.GetBuffer();
59 | stream.Write(buffer, 0, length);
60 |
61 | return stream.ToArray();
62 | }
63 | }
64 |
65 | public (byte[] buffer, int length) GetBuffer()
66 | {
67 | throw new NotSupportedException($"{nameof(StringEncoder)} doesn't use temporary byte buffers");
68 | }
69 |
70 | protected virtual void Dispose(bool disposing)
71 | {
72 | if (!_disposed)
73 | {
74 | if (disposing)
75 | {
76 | _sb.Clear();
77 | _lengthEncoder.Dispose();
78 | }
79 |
80 | _sb = null;
81 | _lengthEncoder = null;
82 | _disposed = true;
83 | }
84 | }
85 |
86 | [Conditional("DEBUG")]
87 | protected void CheckDisposed()
88 | {
89 | if (_disposed)
90 | {
91 | throw new ObjectDisposedException(GetType().ToString());
92 | }
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/Ycs/lib0/Encoding/UintOptRleEncoder.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | namespace Ycs
8 | {
9 | ///
10 | /// Optimized RLE encoder that does not suffer from the mentioned problem of the basic RLE encoder.
11 | /// Internally uses VarInt encoder to write unsigned integers.
12 | /// If the input occurs multiple times, we write it as a negative number. The
13 | /// then understands that it needs to read a count.
14 | ///
15 | ///
16 | internal class UintOptRleEncoder : AbstractStreamEncoder
17 | {
18 | private uint _state;
19 | private uint _count;
20 |
21 | public UintOptRleEncoder()
22 | {
23 | // Do nothing.
24 | }
25 |
26 | ///
27 | public override void Write(uint value)
28 | {
29 | CheckDisposed();
30 |
31 | if (_state == value)
32 | {
33 | _count++;
34 | }
35 | else
36 | {
37 | WriteEncodedValue();
38 |
39 | _count = 1;
40 | _state = value;
41 | }
42 | }
43 |
44 | protected override void Flush()
45 | {
46 | WriteEncodedValue();
47 | base.Flush();
48 | }
49 |
50 | private void WriteEncodedValue()
51 | {
52 | if (_count > 0)
53 | {
54 | // Flush counter, unless this is the first value (count = 0).
55 | // Case 1: Just a single value. Set sign to positive.
56 | // Case 2: Write several values. Set sign to negative to indicate that there is a length coming.
57 | if (_count == 1)
58 | {
59 | Stream.WriteVarInt(_state);
60 | }
61 | else
62 | {
63 | // Specify 'treatZeroAsNegative' in case we pass the '-0'.
64 | Stream.WriteVarInt(-_state, treatZeroAsNegative: _state == 0);
65 |
66 | // Since count is always >1, we can decrement by one. Non-standard encoding.
67 | Stream.WriteVarUint(_count - 2);
68 | }
69 | }
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/Ycs/lib0/NativeEnums.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | namespace Ycs
8 | {
9 | ///
10 | /// N-th bit activated.
11 | ///
12 | internal static class Bit
13 | {
14 | public const uint Bit1 = 1 << 0;
15 | public const uint Bit2 = 1 << 1;
16 | public const uint Bit3 = 1 << 2;
17 | public const uint Bit4 = 1 << 3;
18 | public const uint Bit5 = 1 << 4;
19 | public const uint Bit6 = 1 << 5;
20 | public const uint Bit7 = 1 << 6;
21 | public const uint Bit8 = 1 << 7;
22 | public const uint Bit9 = 1 << 8;
23 | public const uint Bit10 = 1 << 9;
24 | public const uint Bit11 = 1 << 10;
25 | public const uint Bit12 = 1 << 11;
26 | public const uint Bit13 = 1 << 12;
27 | public const uint Bit14 = 1 << 13;
28 | public const uint Bit15 = 1 << 14;
29 | public const uint Bit16 = 1 << 15;
30 | public const uint Bit17 = 1 << 16;
31 | public const uint Bit18 = 1 << 17;
32 | public const uint Bit19 = 1 << 18;
33 | public const uint Bit20 = 1 << 19;
34 | public const uint Bit21 = 1 << 20;
35 | public const uint Bit22 = 1 << 21;
36 | public const uint Bit23 = 1 << 22;
37 | public const uint Bit24 = 1 << 23;
38 | public const uint Bit25 = 1 << 24;
39 | public const uint Bit26 = 1 << 25;
40 | public const uint Bit27 = 1 << 26;
41 | public const uint Bit28 = 1 << 27;
42 | public const uint Bit29 = 1 << 28;
43 | public const uint Bit30 = 1 << 29;
44 | public const uint Bit31 = 1 << 30;
45 | public const int Bit32 = 1 << 31;
46 | }
47 |
48 | ///
49 | /// First N bits activated.
50 | ///
51 | internal static class Bits
52 | {
53 | public const uint Bits0 = (1 << 0) - 1;
54 | public const uint Bits1 = (1 << 1) - 1;
55 | public const uint Bits2 = (1 << 2) - 1;
56 | public const uint Bits3 = (1 << 3) - 1;
57 | public const uint Bits4 = (1 << 4) - 1;
58 | public const uint Bits5 = (1 << 5) - 1;
59 | public const uint Bits6 = (1 << 6) - 1;
60 | public const uint Bits7 = (1 << 7) - 1;
61 | public const uint Bits8 = (1 << 8) - 1;
62 | public const uint Bits9 = (1 << 9) - 1;
63 | public const uint Bits10 = (1 << 10) - 1;
64 | public const uint Bits11 = (1 << 11) - 1;
65 | public const uint Bits12 = (1 << 12) - 1;
66 | public const uint Bits13 = (1 << 13) - 1;
67 | public const uint Bits14 = (1 << 14) - 1;
68 | public const uint Bits15 = (1 << 15) - 1;
69 | public const uint Bits16 = (1 << 16) - 1;
70 | public const uint Bits17 = (1 << 17) - 1;
71 | public const uint Bits18 = (1 << 18) - 1;
72 | public const uint Bits19 = (1 << 19) - 1;
73 | public const uint Bits20 = (1 << 20) - 1;
74 | public const uint Bits21 = (1 << 21) - 1;
75 | public const uint Bits22 = (1 << 22) - 1;
76 | public const uint Bits23 = (1 << 23) - 1;
77 | public const uint Bits24 = (1 << 24) - 1;
78 | public const uint Bits25 = (1 << 25) - 1;
79 | public const uint Bits26 = (1 << 26) - 1;
80 | public const uint Bits27 = (1 << 27) - 1;
81 | public const uint Bits28 = (1 << 28) - 1;
82 | public const uint Bits29 = (1 << 29) - 1;
83 | public const uint Bits30 = (1 << 30) - 1;
84 | public const uint Bits31 = 0x7FFFFFFF;
85 | public const uint Bits32 = 0xFFFFFFFF;
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/tests/Ycs.Benchmarks/BenchmarkTests.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System;
8 | using BenchmarkDotNet.Attributes;
9 |
10 | namespace Ycs.Benchmarks
11 | {
12 | ///
13 | /// Simulate two clients.
14 | /// One client modifies a text object and sends update messages to the other client.
15 | /// We measure the time to perform the task (time), the amount of data exchanged (avgUpdateSize),
16 | /// the size of the encoded document after the task is performed (docSize),
17 | /// the time to parse the encoded document (parseTime),
18 | /// and the memory used to hold the decoded document (memUsed).
19 | ///
20 | public class BenchmarkTests
21 | {
22 | private const int N = 6_000;
23 |
24 | private readonly Random _rand = new Random();
25 |
26 | [Benchmark]
27 | public void B1()
28 | {
29 | var doc1 = new YDoc();
30 | var doc2 = new YDoc();
31 |
32 | doc1.UpdateV2 += (s, e) =>
33 | {
34 | doc2.ApplyUpdateV2(e.data, doc1);
35 | };
36 |
37 | for (int i = 0; i < N; i++)
38 | {
39 | doc1.GetText("text").Insert(i, GetRandomChar(_rand).ToString());
40 | }
41 |
42 | doc1.Destroy();
43 | doc2.Destroy();
44 | }
45 |
46 | private static char GetRandomChar(Random rand) => (char)rand.Next('A', 'Z' + 1);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/tests/Ycs.Benchmarks/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using BenchmarkDotNet.Running;
3 |
4 | namespace Ycs.Benchmarks
5 | {
6 | class Program
7 | {
8 | static void Main(string[] args)
9 | {
10 | BenchmarkRunner.Run();
11 | Console.In.ReadLine();
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/tests/Ycs.Benchmarks/Ycs.Benchmarks.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | netcoreapp3.1
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/tests/Ycs.Tests/EncodingTests.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System;
8 | using System.IO;
9 | using Microsoft.VisualStudio.TestTools.UnitTesting;
10 |
11 | namespace Ycs
12 | {
13 | [TestClass]
14 | public class EncodingTests
15 | {
16 | ///
17 | /// Check if binary encoding is compatible with golang binary encoding.
18 | /// Result: is compatible up to 32 bit: [0, 4294967295] / [0, 0xFFFFFFFF].
19 | ///
20 | [DataTestMethod]
21 | [DataRow(0u, new byte[] { 0 })]
22 | [DataRow(1u, new byte[] { 1 })]
23 | [DataRow(128u, new byte[] { 128, 1 })]
24 | [DataRow(200u, new byte[] { 200, 1 })]
25 | [DataRow(32u, new byte[] { 32 })]
26 | [DataRow(500u, new byte[] { 244, 3 })]
27 | [DataRow(256u, new byte[] { 128, 2 })]
28 | [DataRow(700u, new byte[] { 188, 5 })]
29 | [DataRow(1024u, new byte[] { 128, 8 })]
30 | [DataRow(1025u, new byte[] { 129, 8 })]
31 | [DataRow(4048u, new byte[] { 208, 31 })]
32 | [DataRow(5050u, new byte[] { 186, 39 })]
33 | [DataRow(1_000_000u, new byte[] { 192, 132, 61 })]
34 | [DataRow(34_951_959u, new byte[] { 151, 166, 213, 16 })]
35 | [DataRow(2_147_483_646u, new byte[] { 254, 255, 255, 255, 7 })]
36 | [DataRow(2_147_483_647u, new byte[] { 255, 255, 255, 255, 7 })]
37 | [DataRow(2_147_483_648u, new byte[] { 128, 128, 128, 128, 8 })]
38 | [DataRow(2_147_483_700u, new byte[] { 180, 128, 128, 128, 8 })]
39 | [DataRow(4_294_967_294u, new byte[] { 254, 255, 255, 255, 15 })]
40 | [DataRow(4_294_967_295u, new byte[] { 255, 255, 255, 255, 15 })]
41 | public void TestGolangBinaryEncodingCompatibility(uint value, byte[] expected)
42 | {
43 | using (var stream = new MemoryStream())
44 | {
45 | stream.WriteVarUint(value);
46 |
47 | var actual = stream.ToArray();
48 | CollectionAssert.AreEqual(expected, actual);
49 | }
50 | }
51 |
52 | [TestMethod]
53 | public void TestEncodeMax32BitUint()
54 | {
55 | DoTestEncoding("Max 32bit uint", (w, v) => w.WriteVarUint(v), (r) => r.ReadVarUint(), Bits.Bits32);
56 | }
57 |
58 | [TestMethod]
59 | public void TestVarUintEncoding()
60 | {
61 | DoTestEncoding("VarUint 1 byte", (w, v) => w.WriteVarUint(v), (r) => r.ReadVarUint(), 42);
62 | DoTestEncoding("VarUint 2 bytes", (w, v) => w.WriteVarUint(v), (r) => r.ReadVarUint(), 1 << 9 | 3);
63 | DoTestEncoding("VarUint 3 bytes", (w, v) => w.WriteVarUint(v), (r) => r.ReadVarUint(), 1 << 17 | 1 << 9 | 3);
64 | DoTestEncoding("VarUint 4 bytes", (w, v) => w.WriteVarUint(v), (r) => r.ReadVarUint(), 1 << 25 | 1 << 17 | 1 << 9 | 3);
65 | DoTestEncoding("VarUint of 2839012934", (w, v) => w.WriteVarUint(v), (r) => r.ReadVarUint(), 2_839_012_934);
66 | }
67 |
68 | [TestMethod]
69 | public void TestVarIntEncoding()
70 | {
71 | DoTestEncoding("VarInt 1 byte", (w, v) => w.WriteVarInt(v), (r) => r.ReadVarInt().Value, -42);
72 | DoTestEncoding("VarInt 2 bytes", (w, v) => w.WriteVarInt(v), (r) => r.ReadVarInt().Value, -(1 << 9 | 3));
73 | DoTestEncoding("VarInt 3 bytes", (w, v) => w.WriteVarInt(v), (r) => r.ReadVarInt().Value, -(1 << 17 | 1 << 9 | 3));
74 | DoTestEncoding("VarInt 4 bytes", (w, v) => w.WriteVarInt(v), (r) => r.ReadVarInt().Value, -(1 << 25 | 1 << 17 | 1 << 9 | 3));
75 | DoTestEncoding("VarInt of -691529286", (w, v) => w.WriteVarInt(v), (r) => r.ReadVarInt().Value, -691_529_286);
76 | DoTestEncoding("VarInt of 64 (-0)", (w, v) => w.WriteVarInt(v, treatZeroAsNegative: true), (r) => r.ReadVarInt().Value, 0);
77 | }
78 |
79 | [TestMethod]
80 | public void TestEncodeFloatingPoint()
81 | {
82 | DoTestEncoding("", (w, v) => w.WriteAny(v), (r) => (float)r.ReadAny(), 2.0f);
83 | DoTestEncoding("", (w, v) => w.WriteAny(v), (r) => (double)r.ReadAny(), 2.0);
84 | }
85 |
86 | [TestMethod]
87 | public void TestVarIntEncodingNegativeZero()
88 | {
89 | using (var stream = new MemoryStream())
90 | {
91 | stream.WriteVarInt(0, treatZeroAsNegative: true);
92 |
93 | var actual = stream.ToArray();
94 |
95 | // '-0' should have the 7th bit set, i.e. == 64.
96 | CollectionAssert.AreEqual(new byte[] { 64 }, actual);
97 |
98 | using (var inputStream = new MemoryStream(actual))
99 | {
100 | var v = inputStream.ReadVarInt();
101 | Assert.AreEqual(0, v.Value);
102 | Assert.AreEqual(-1, v.Sign);
103 | }
104 | }
105 | }
106 |
107 | [TestMethod]
108 | public void TestRepeatVarUintEncoding()
109 | {
110 | var rand = new Random();
111 | for (int i = 0; i < 10; i++)
112 | {
113 | var n = rand.Next(0, (1 << 28) - 1);
114 | DoTestEncoding($"VarUint of {n}", (w, v) => w.WriteVarUint(v), (r) => r.ReadVarUint(), (uint)n);
115 | }
116 | }
117 |
118 | [TestMethod]
119 | public void TestRepeatVarIntEncoding()
120 | {
121 | var rand = new Random();
122 | for (int i = 0; i < 10; i++)
123 | {
124 | var n = rand.Next(0, int.MaxValue);
125 | DoTestEncoding($"VarInt of {n}", (w, v) => w.WriteVarInt(v), (r) => r.ReadVarInt().Value, n);
126 | }
127 | }
128 |
129 | [TestMethod]
130 | public void TestStringEncoding()
131 | {
132 | DoTestEncoding(string.Empty, (w, v) => w.WriteVarString(v), (r) => r.ReadVarString(), "hello");
133 | DoTestEncoding(string.Empty, (w, v) => w.WriteVarString(v), (r) => r.ReadVarString(), string.Empty);
134 | DoTestEncoding(string.Empty, (w, v) => w.WriteVarString(v), (r) => r.ReadVarString(), "쾟");
135 | DoTestEncoding(string.Empty, (w, v) => w.WriteVarString(v), (r) => r.ReadVarString(), "龟"); // Surrogate length 3.
136 | DoTestEncoding(string.Empty, (w, v) => w.WriteVarString(v), (r) => r.ReadVarString(), "😝"); // Surrogate length 4.
137 | }
138 |
139 | private void DoTestEncoding(string description, Action write, Func read, T val)
140 | {
141 | byte[] encoded;
142 |
143 | using (var outputStream = new MemoryStream())
144 | {
145 | write(outputStream, val);
146 | encoded = outputStream.ToArray();
147 | }
148 |
149 | using (var inputStream = new MemoryStream(encoded))
150 | {
151 | var decodedValue = read(inputStream);
152 | Assert.AreEqual(val, decodedValue, description);
153 | }
154 | }
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/tests/Ycs.Tests/RelativePositionTests.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using Microsoft.VisualStudio.TestTools.UnitTesting;
8 |
9 | namespace Ycs
10 | {
11 | [TestClass]
12 | public class RelativePositionTests : YTestBase
13 | {
14 | [TestMethod]
15 | public void TestRelativePositionCase1()
16 | {
17 | Init(users: 1);
18 | var yText = Texts[Users[0]];
19 |
20 | yText.Insert(0, "1");
21 | yText.Insert(0, "abc");
22 | yText.Insert(0, "z");
23 | yText.Insert(0, "y");
24 | yText.Insert(0, "x");
25 |
26 | CheckRelativePositions(yText);
27 | }
28 |
29 | [TestMethod]
30 | public void TestRelativePositionCase2()
31 | {
32 | Init(users: 1);
33 | var yText = Texts[Users[0]];
34 |
35 | yText.Insert(0, "abc");
36 |
37 | CheckRelativePositions(yText);
38 | }
39 |
40 | [TestMethod]
41 | public void TestRelativePositionCase3()
42 | {
43 | Init(users: 1);
44 | var yText = Texts[Users[0]];
45 |
46 | yText.Insert(0, "abc");
47 | yText.Insert(0, "1");
48 | yText.Insert(0, "xyz");
49 |
50 | CheckRelativePositions(yText);
51 | }
52 |
53 | [TestMethod]
54 | public void TestRelativePositionCase4()
55 | {
56 | Init(users: 1);
57 | var yText = Texts[Users[0]];
58 |
59 | yText.Insert(0, "1");
60 |
61 | CheckRelativePositions(yText);
62 | }
63 |
64 | [TestMethod]
65 | public void TestRelativePositionCase5()
66 | {
67 | Init(users: 1);
68 | var yText = Texts[Users[0]];
69 |
70 | yText.Insert(0, "2");
71 | yText.Insert(0, "1");
72 |
73 | CheckRelativePositions(yText);
74 | }
75 |
76 | [TestMethod]
77 | public void TestRelativePositionCase6()
78 | {
79 | Init(users: 1);
80 | var yText = Texts[Users[0]];
81 |
82 | CheckRelativePositions(yText);
83 | }
84 |
85 | [TestMethod]
86 | public void TestRelativePositionAssociationDifference()
87 | {
88 | Init(users: 1);
89 | var yText = Texts[Users[0]];
90 |
91 | yText.Insert(0, "2");
92 | yText.Insert(0, "1");
93 |
94 | var rposRight = RelativePosition.FromTypeIndex(yText, 1, 0);
95 | var rposLeft = RelativePosition.FromTypeIndex(yText, 1, -1);
96 |
97 | yText.Insert(1, "x");
98 |
99 | var posRight = AbsolutePosition.TryCreateFromRelativePosition(rposRight, Users[0]);
100 | var posLeft = AbsolutePosition.TryCreateFromRelativePosition(rposLeft, Users[0]);
101 |
102 | Assert.IsNotNull(posRight);
103 | Assert.IsNotNull(posLeft);
104 | Assert.AreEqual(2, posRight.Index);
105 | Assert.AreEqual(1, posLeft.Index);
106 | }
107 |
108 | private void CheckRelativePositions(YText yText)
109 | {
110 | // Test if all positions are encoded and restored correctly.
111 | for (int i = 0; i < yText.Length; i++)
112 | {
113 | // For all types of assotiations.
114 | for (int assoc = -1; assoc < 2; assoc++)
115 | {
116 | var rpos = RelativePosition.FromTypeIndex(yText, i, assoc);
117 | var encodedRpos = rpos.ToArray();
118 | var decodedRpos = RelativePosition.Read(encodedRpos);
119 | var absPos = AbsolutePosition.TryCreateFromRelativePosition(decodedRpos, yText.Doc);
120 |
121 | Assert.AreEqual(i, absPos.Index);
122 | Assert.AreEqual(assoc, absPos.Assoc);
123 | }
124 | }
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/tests/Ycs.Tests/RleEncodingTests.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System;
8 | using System.Collections.Generic;
9 | using System.IO;
10 | using Microsoft.VisualStudio.TestTools.UnitTesting;
11 |
12 | namespace Ycs
13 | {
14 | [TestClass]
15 | public class RleEncodingTests
16 | {
17 | [TestMethod]
18 | public void TestRleEncoder()
19 | {
20 | TestEncoder(new RleEncoder(), s => new RleDecoder(s));
21 | }
22 |
23 | [TestMethod]
24 | public void TestRleIntDiffEncoder()
25 | {
26 | TestEncoder(new RleIntDiffEncoder(0), s => new RleIntDiffDecoder(s, 0));
27 | }
28 |
29 | [TestMethod]
30 | public void TestIntDiffOptRleEncoder()
31 | {
32 | TestEncoder(new IntDiffOptRleEncoder(), s => new IntDiffOptRleDecoder(s));
33 | }
34 |
35 | [TestMethod]
36 | public void TestIntDiffEncoder()
37 | {
38 | TestEncoder(new IntDiffEncoder(0), s => new IntDiffDecoder(s, 0));
39 | TestEncoder(new IntDiffEncoder(42), s => new IntDiffDecoder(s, 42));
40 | }
41 |
42 | [TestMethod]
43 | public void TestStringEncoder()
44 | {
45 | const int n = 100;
46 | var words = new List(n);
47 | var encoder = new StringEncoder();
48 |
49 | for (int i = 0; i < n; i++)
50 | {
51 | var v = Guid.NewGuid().ToString();
52 | words.Add(v);
53 | encoder.Write(v);
54 | }
55 |
56 | var data = encoder.ToArray();
57 | using (var stream = new MemoryStream(data))
58 | {
59 | var decoder = new StringDecoder(stream);
60 |
61 | for (int i = 0; i < words.Count; i++)
62 | {
63 | Assert.AreEqual(words[i], decoder.Read());
64 | }
65 | }
66 | }
67 |
68 | [TestMethod]
69 | public void TestStringEncoderEmptyString()
70 | {
71 | const int n = 10;
72 | var encoder = new StringEncoder();
73 |
74 | for (int i = 0; i < n; i++)
75 | {
76 | encoder.Write(string.Empty);
77 | }
78 |
79 | var data = encoder.ToArray();
80 | using (var stream = new MemoryStream(data))
81 | {
82 | var decoder = new StringDecoder(stream);
83 |
84 | for (int i = 0; i < n; i++)
85 | {
86 | Assert.AreEqual(string.Empty, decoder.Read());
87 | }
88 | }
89 | }
90 |
91 | private void TestEncoder(TEncoder encoder, Func createDecoder, int n = 100)
92 | where TEncoder : IEncoder
93 | where TDecoder : IDecoder
94 | {
95 | for (int i = -n; i < n; i++)
96 | {
97 | encoder.Write(i);
98 |
99 | // Write additional 'i' times.
100 | for (int j = 0; j < i; j++)
101 | {
102 | encoder.Write(i);
103 | }
104 | }
105 |
106 | var data = encoder.ToArray();
107 | using (var stream = new MemoryStream(data))
108 | {
109 | var decoder = createDecoder(stream);
110 |
111 | for (int i = -n; i < n; i++)
112 | {
113 | Assert.AreEqual(i, decoder.Read());
114 |
115 | // Read additional 'i' times.
116 | for (int j = 0; j < i; j++)
117 | {
118 | Assert.AreEqual(i, decoder.Read());
119 | }
120 | }
121 | }
122 | }
123 |
124 | private void TestEncoder(TEncoder encoder, Func createDecoder, byte n = 100)
125 | where TEncoder : IEncoder
126 | where TDecoder : IDecoder
127 | {
128 | for (byte i = 0; i < n; i++)
129 | {
130 | encoder.Write(i);
131 |
132 | // Write additional 'i' times.
133 | for (byte j = 0; j < i; j++)
134 | {
135 | encoder.Write(i);
136 | }
137 | }
138 |
139 | var data = encoder.ToArray();
140 | using (var stream = new MemoryStream(data))
141 | {
142 | var decoder = createDecoder(stream);
143 |
144 | for (byte i = 0; i < n; i++)
145 | {
146 | Assert.AreEqual(i, decoder.Read());
147 |
148 | // Read additional 'i' times.
149 | for (byte j = 0; j < i; j++)
150 | {
151 | Assert.AreEqual(i, decoder.Read());
152 | }
153 | }
154 | }
155 | }
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/tests/Ycs.Tests/SnapshotTests.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System.Collections;
8 | using Microsoft.VisualStudio.TestTools.UnitTesting;
9 |
10 | namespace Ycs
11 | {
12 | [TestClass]
13 | public class SnapshotTests : YTestBase
14 | {
15 | [TestMethod]
16 | public void TestBasicRestoreSnapshot()
17 | {
18 | var doc = new YDoc(new YDocOptions { Gc = false });
19 | doc.GetArray("array").Insert(0, new[] { "hello" });
20 |
21 | var snap = doc.CreateSnapshot();
22 | doc.GetArray("array").Insert(1, new[] { "world" });
23 |
24 | var docRestored = snap.RestoreDocument(doc);
25 |
26 | CollectionAssert.AreEqual(new[] { "hello" }, (ICollection)docRestored.GetArray("array").ToArray());
27 | CollectionAssert.AreEqual(new[] { "hello", "world" }, (ICollection)doc.GetArray("array").ToArray());
28 | }
29 |
30 | [TestMethod]
31 | public void TestEmptyRestoreSnapshot()
32 | {
33 | var doc = new YDoc(new YDocOptions { Gc = false });
34 | var snap = doc.CreateSnapshot();
35 |
36 | snap.StateVector[9999] = 0;
37 | doc.GetArray().Insert(0, new[] { "world" });
38 |
39 | var docRestored = snap.RestoreDocument(doc);
40 | Assert.AreEqual(0, docRestored.GetArray().ToArray().Count);
41 | CollectionAssert.AreEqual(new[] { "world" }, (ICollection)doc.GetArray().ToArray());
42 |
43 | // Now this snapshot reflects the latest state. It should still work.
44 | var snap2 = doc.CreateSnapshot();
45 | var docRestored2 = snap2.RestoreDocument(doc);
46 | CollectionAssert.AreEqual(new[] { "world" }, (ICollection)docRestored2.GetArray().ToArray());
47 | }
48 |
49 | [TestMethod]
50 | public void TestRestoreSnapshotWithSubType()
51 | {
52 | var doc = new YDoc(new YDocOptions { Gc = false });
53 | doc.GetArray("array").Insert(0, new[] { new YMap() });
54 | var subMap = doc.GetArray("array").Get(0) as YMap;
55 | subMap.Set("key1", "value1");
56 |
57 | var snap = doc.CreateSnapshot();
58 | subMap.Set("key2", "value2");
59 | var docRestored = snap.RestoreDocument(doc);
60 |
61 | var restoredSubMap = docRestored.GetArray("array").Get(0) as YMap;
62 | subMap = doc.GetArray("array").Get(0) as YMap;
63 |
64 | Assert.AreEqual(1, restoredSubMap.Count);
65 | Assert.AreEqual("value1", restoredSubMap.Get("key1"));
66 |
67 | Assert.AreEqual(2, subMap.Count);
68 | Assert.AreEqual("value1", subMap.Get("key1"));
69 | Assert.AreEqual("value2", subMap.Get("key2"));
70 | }
71 |
72 | [TestMethod]
73 | public void TestRestoreDeletedItem1()
74 | {
75 | var doc = new YDoc(new YDocOptions { Gc = false });
76 | doc.GetArray("array").Insert(0, new[] { "item1", "item2" });
77 |
78 | var snap = doc.CreateSnapshot();
79 | doc.GetArray("array").Delete(0);
80 | var docRestored = snap.RestoreDocument(doc);
81 |
82 | CollectionAssert.AreEqual(new[] { "item1", "item2" }, (ICollection)docRestored.GetArray("array").ToArray());
83 | CollectionAssert.AreEqual(new[] { "item2" }, (ICollection)doc.GetArray("array").ToArray());
84 | }
85 |
86 | [TestMethod]
87 | public void TestRestoreLeftItem()
88 | {
89 | var doc = new YDoc(new YDocOptions { Gc = false });
90 | doc.GetArray("array").Insert(0, new[] { "item1" });
91 | doc.GetMap("map").Set("test", 1);
92 | doc.GetArray("array").Insert(0, new[] { "item0" });
93 |
94 | var snap = doc.CreateSnapshot();
95 | doc.GetArray("array").Delete(1);
96 | var docRestored = snap.RestoreDocument(doc);
97 |
98 | CollectionAssert.AreEqual(new[] { "item0", "item1" }, (ICollection)docRestored.GetArray("array").ToArray());
99 | CollectionAssert.AreEqual(new[] { "item0" }, (ICollection)doc.GetArray("array").ToArray());
100 | }
101 |
102 | [TestMethod]
103 | public void TestDeletedItemsBase()
104 | {
105 | var doc = new YDoc(new YDocOptions { Gc = false });
106 | doc.GetArray("array").Insert(0, new[] { "item1" });
107 | doc.GetArray("array").Delete(0);
108 |
109 | var snap = doc.CreateSnapshot();
110 | doc.GetArray("array").Insert(0, new[] { "item0" });
111 | var docRestored = snap.RestoreDocument(doc);
112 |
113 | Assert.AreEqual(0, docRestored.GetArray("array").ToArray().Count);
114 | CollectionAssert.AreEqual(new[] { "item0" }, (ICollection)doc.GetArray("array").ToArray());
115 | }
116 |
117 | [TestMethod]
118 | public void TestDeletedItems2()
119 | {
120 | var doc = new YDoc(new YDocOptions { Gc = false });
121 | doc.GetArray("array").Insert(0, new[] { "item1", "item2", "item3" });
122 | doc.GetArray("array").Delete(1);
123 |
124 | var snap = doc.CreateSnapshot();
125 | doc.GetArray("array").Insert(0, new[] { "item0" });
126 | var docRestored = snap.RestoreDocument(doc);
127 |
128 | CollectionAssert.AreEqual(new[] { "item1", "item3" }, (ICollection)docRestored.GetArray("array").ToArray());
129 | CollectionAssert.AreEqual(new[] { "item0", "item1", "item3" }, (ICollection)doc.GetArray("array").ToArray());
130 | }
131 |
132 | [TestMethod]
133 | public void TestDependentChanges()
134 | {
135 | Init(users: 2, new YDocOptions { Gc = false });
136 | var array0 = Arrays[Users[0]];
137 | var array1 = Arrays[Users[1]];
138 |
139 | array0.Insert(0, new[] { "user1item1" });
140 | Connector.SyncAll();
141 | array1.Insert(1, new[] { "user2item1" });
142 | Connector.SyncAll();
143 |
144 | var snap = array0.Doc.CreateSnapshot();
145 |
146 | array0.Insert(2, new[] { "user1item2" });
147 | Connector.SyncAll();
148 | array1.Insert(3, new[] { "user2item2" });
149 | Connector.SyncAll();
150 |
151 | var docRestored0 = snap.RestoreDocument(array0.Doc);
152 | CollectionAssert.AreEqual(new[] { "user1item1", "user2item1" }, (ICollection)docRestored0.GetArray("array").ToArray());
153 |
154 | var docRestored1 = snap.RestoreDocument(array1.Doc);
155 | CollectionAssert.AreEqual(new[] { "user1item1", "user2item1" }, (ICollection)docRestored1.GetArray("array").ToArray());
156 | }
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/tests/Ycs.Tests/TestConnector.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System;
8 | using System.Collections.Generic;
9 | using System.Linq;
10 | using System.IO;
11 | using System.Diagnostics;
12 | using Microsoft.VisualStudio.TestTools.UnitTesting;
13 |
14 | namespace Ycs
15 | {
16 | public class TestConnector
17 | {
18 | public ISet _allConns = new HashSet();
19 | public ISet _onlineConns = new HashSet();
20 | public Random _prng = new Random();
21 |
22 | public TestYInstance CreateY(int clientId, YDocOptions options)
23 | {
24 | return new TestYInstance(this, clientId, options);
25 | }
26 |
27 | public bool FlushNextMessage(TestYInstance sender, TestYInstance receiver)
28 | {
29 | Assert.AreNotEqual(sender, receiver);
30 |
31 | var messages = receiver._receiving[sender];
32 | if (messages.Count == 0)
33 | {
34 | receiver._receiving.Remove(sender);
35 | return false;
36 | }
37 |
38 | var m = messages.Dequeue();
39 | // Debug.WriteLine($"MSG {sender.ClientId} -> {receiver.ClientId}, len {m.Length}:");
40 | // Debug.WriteLine(string.Join(",", m));
41 |
42 | using (var writer = new MemoryStream())
43 | {
44 | using (var reader = new MemoryStream(m))
45 | {
46 | SyncProtocol.ReadSyncMessage(reader, writer, receiver, receiver._tc);
47 | }
48 |
49 | if (writer.Length > 0)
50 | {
51 | // Send reply message.
52 | var replyMessage = writer.ToArray();
53 | // Debug.WriteLine($"REPLY {receiver.ClientId} -> {sender.ClientId}, len {replyMessage.Length}:");
54 | // Debug.WriteLine(string.Join(",", replyMessage));
55 | sender.Receive(replyMessage, receiver);
56 | }
57 | }
58 |
59 | return true;
60 | }
61 |
62 | public bool FlushRandomMessage()
63 | {
64 | var conns = _onlineConns.Where(conn => conn._receiving.Count > 0).ToList();
65 | if (conns.Count > 0)
66 | {
67 | var receiver = conns[_prng.Next(0, conns.Count)];
68 | var keys = receiver._receiving.Keys.ToList();
69 | var sender = keys[_prng.Next(0, keys.Count)];
70 |
71 | if (!FlushNextMessage(sender, receiver))
72 | {
73 | return FlushRandomMessage();
74 | }
75 |
76 | return true;
77 | }
78 |
79 | return false;
80 | }
81 |
82 | public bool FlushAllMessages()
83 | {
84 | var didSomething = false;
85 |
86 | while (FlushRandomMessage())
87 | {
88 | didSomething = true;
89 | }
90 |
91 | return didSomething;
92 | }
93 |
94 | public void ReconnectAll()
95 | {
96 | foreach (var conn in _allConns)
97 | {
98 | conn.Connect();
99 | }
100 | }
101 |
102 | public void DisconnectAll()
103 | {
104 | foreach (var conn in _allConns)
105 | {
106 | conn.Disconnect();
107 | }
108 | }
109 |
110 | public void SyncAll()
111 | {
112 | ReconnectAll();
113 | FlushAllMessages();
114 | }
115 |
116 | public bool DisconnectRandom()
117 | {
118 | if (_onlineConns.Count == 0)
119 | {
120 | return false;
121 | }
122 |
123 | _onlineConns.ToList()[_prng.Next(0, _onlineConns.Count)].Disconnect();
124 | return true;
125 | }
126 |
127 | public bool ReconnectRandom()
128 | {
129 | var reconnectable = new List();
130 | foreach (var conn in _allConns)
131 | {
132 | if (!_onlineConns.Contains(conn))
133 | {
134 | reconnectable.Add(conn);
135 | }
136 | }
137 |
138 | if (reconnectable.Count == 0)
139 | {
140 | return false;
141 | }
142 |
143 | reconnectable[_prng.Next(0, reconnectable.Count)].Connect();
144 | return true;
145 | }
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/tests/Ycs.Tests/TestYInstance.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System.Collections.Generic;
8 | using System.IO;
9 |
10 | namespace Ycs
11 | {
12 | public class TestYInstance : YDoc
13 | {
14 | public TestConnector _tc;
15 | public IDictionary> _receiving = new Dictionary>();
16 |
17 | public TestYInstance(TestConnector connector, int clientId, YDocOptions options)
18 | : base(options)
19 | {
20 | ClientId = clientId;
21 |
22 | _tc = connector;
23 | _tc._allConns.Add(this);
24 |
25 | // Setup observe on local model.
26 | UpdateV2 += (s, e) =>
27 | {
28 | if (e.origin != _tc)
29 | {
30 | using (var stream = new MemoryStream())
31 | {
32 | SyncProtocol.WriteUpdate(stream, e.data);
33 | BroadcastMessage(this, stream.ToArray());
34 | }
35 | }
36 | };
37 |
38 | Connect();
39 | }
40 |
41 | private void BroadcastMessage(TestYInstance sender, byte[] data)
42 | {
43 | if (_tc._onlineConns.Contains(sender))
44 | {
45 | foreach (var conn in _tc._onlineConns)
46 | {
47 | if (sender != conn)
48 | {
49 | conn.Receive(data, sender);
50 | }
51 | }
52 | }
53 | }
54 |
55 | public void Connect()
56 | {
57 | if (!_tc._onlineConns.Contains(this))
58 | {
59 | _tc._onlineConns.Add(this);
60 |
61 | using (var stream = new MemoryStream())
62 | {
63 | SyncProtocol.WriteSyncStep1(stream, this);
64 |
65 | // Publish SyncStep1
66 | BroadcastMessage(this, stream.ToArray());
67 |
68 | foreach (var remoteYInstance in _tc._onlineConns)
69 | {
70 | if (remoteYInstance != this)
71 | {
72 | stream.SetLength(0);
73 | SyncProtocol.WriteSyncStep1(stream, remoteYInstance);
74 |
75 | Receive(stream.ToArray(), remoteYInstance);
76 | }
77 | }
78 | }
79 | }
80 | }
81 |
82 | public void Receive(byte[] data, TestYInstance remoteDoc)
83 | {
84 | if (!_receiving.TryGetValue(remoteDoc, out var messages))
85 | {
86 | messages = new Queue();
87 | _receiving[remoteDoc] = messages;
88 | }
89 |
90 | messages.Enqueue(data);
91 | }
92 |
93 | public void Disconnect()
94 | {
95 | _receiving.Clear();
96 | _tc._onlineConns.Remove(this);
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/tests/Ycs.Tests/YDocTests.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | //
5 | // ------------------------------------------------------------------------------
6 |
7 | using System.Collections.Generic;
8 | using System.Linq;
9 | using Microsoft.VisualStudio.TestTools.UnitTesting;
10 |
11 | namespace Ycs
12 | {
13 | [TestClass]
14 | public class YDocTests : YTestBase
15 | {
16 | [TestMethod]
17 | public void TestClientIdDuplicateChange()
18 | {
19 | var doc1 = new YDoc();
20 | doc1.ClientId = 0;
21 | var doc2 = new YDoc();
22 | doc2.ClientId = 0;
23 | Assert.AreEqual(doc1.ClientId, doc2.ClientId);
24 |
25 | doc1.GetArray("a").Insert(0, new object[] { 1, 2 });
26 | doc2.ApplyUpdateV2(doc1.EncodeStateAsUpdateV2());
27 | Assert.AreNotEqual(doc1.ClientId, doc2.ClientId);
28 | }
29 |
30 | [TestMethod]
31 | public void TestGetTypeEmptyId()
32 | {
33 | var doc1 = new YDoc();
34 | doc1.GetText(string.Empty).Insert(0, "h");
35 | doc1.GetText().Insert(1, "i");
36 |
37 | var doc2 = new YDoc();
38 | doc2.ApplyUpdateV2(doc1.EncodeStateAsUpdateV2());
39 |
40 | Assert.AreEqual("hi", doc2.GetText().ToString());
41 | Assert.AreEqual("hi", doc2.GetText(string.Empty).ToString());
42 | }
43 |
44 | [TestMethod]
45 | public void TestSubdoc()
46 | {
47 | var doc = new YDoc();
48 | doc.Load();
49 |
50 | {
51 | List> events = null;
52 | doc.SubdocsChanged += (s, e) =>
53 | {
54 | events = new List>();
55 | events.Add(new List(e.Added.Select(d => d.Guid)));
56 | events.Add(new List(e.Removed.Select(d => d.Guid)));
57 | events.Add(new List(e.Loaded.Select(d => d.Guid)));
58 | };
59 |
60 | var subdocs = doc.GetMap("mysubdocs");
61 | var docA = new YDoc(new YDocOptions { Guid = "a" });
62 | docA.Load();
63 | subdocs.Set("a", docA);
64 | CollectionAssert.AreEqual(new[] { "a" }, events[0]);
65 | CollectionAssert.AreEqual(new object[] { }, events[1]);
66 | CollectionAssert.AreEqual(new[] { "a" }, events[2]);
67 | events = null;
68 |
69 | (subdocs.Get("a") as YDoc).Load();
70 | Assert.IsNull(events);
71 | events = null;
72 |
73 | (subdocs.Get("a") as YDoc).Destroy();
74 | CollectionAssert.AreEqual(new[] { "a" }, events[0]);
75 | CollectionAssert.AreEqual(new[] { "a" }, events[1]);
76 | CollectionAssert.AreEqual(new object[] { }, events[2]);
77 | events = null;
78 |
79 | (subdocs.Get("a") as YDoc).Load();
80 | CollectionAssert.AreEqual(new object[] { }, events[0]);
81 | CollectionAssert.AreEqual(new object[] { }, events[1]);
82 | CollectionAssert.AreEqual(new[] { "a" }, events[2]);
83 | events = null;
84 |
85 | subdocs.Set("b", new YDoc(new YDocOptions { Guid = "a" }));
86 | CollectionAssert.AreEqual(new[] { "a" }, events[0]);
87 | CollectionAssert.AreEqual(new object[] { }, events[1]);
88 | CollectionAssert.AreEqual(new object[] { }, events[2]);
89 | events = null;
90 |
91 | (subdocs.Get("b") as YDoc).Load();
92 | CollectionAssert.AreEqual(new object[] { }, events[0]);
93 | CollectionAssert.AreEqual(new object[] { }, events[1]);
94 | CollectionAssert.AreEqual(new[] { "a" }, events[2]);
95 | events = null;
96 |
97 | var docC = new YDoc(new YDocOptions { Guid = "c" });
98 | docC.Load();
99 | subdocs.Set("c", docC);
100 | CollectionAssert.AreEqual(new[] { "c" }, events[0]);
101 | CollectionAssert.AreEqual(new object[] { }, events[1]);
102 | CollectionAssert.AreEqual(new[] { "c" }, events[2]);
103 | events = null;
104 |
105 | var guids = doc.GetSubdocGuids().ToList();
106 | guids.Sort();
107 | CollectionAssert.AreEqual(new[] { "a", "c" }, guids);
108 | }
109 |
110 | var doc2 = new YDoc();
111 |
112 | {
113 | Assert.AreEqual(0, doc2.GetSubdocGuids().Count());
114 |
115 | List> events = null;
116 | doc2.SubdocsChanged += (s, e) =>
117 | {
118 | events = new List>();
119 | events.Add(new List(e.Added.Select(d => d.Guid)));
120 | events.Add(new List(e.Removed.Select(d => d.Guid)));
121 | events.Add(new List(e.Loaded.Select(d => d.Guid)));
122 | };
123 |
124 | doc2.ApplyUpdateV2(doc.EncodeStateAsUpdateV2());
125 | CollectionAssert.AreEqual(new[] { "a", "a", "c" }, events[0]);
126 | CollectionAssert.AreEqual(new object[] { }, events[1]);
127 | CollectionAssert.AreEqual(new object[] { }, events[2]);
128 | events = null;
129 |
130 | (doc2.GetMap("mysubdocs").Get("a") as YDoc).Load();
131 | CollectionAssert.AreEqual(new object[] { }, events[0]);
132 | CollectionAssert.AreEqual(new object[] { }, events[1]);
133 | CollectionAssert.AreEqual(new[] { "a" }, events[2]);
134 | events = null;
135 |
136 | var guids = doc2.GetSubdocGuids().ToList();
137 | guids.Sort();
138 | CollectionAssert.AreEqual(new[] { "a", "c" }, guids);
139 |
140 | doc2.GetMap("mysubdocs").Delete("a");
141 | CollectionAssert.AreEqual(new object[] { }, events[0]);
142 | CollectionAssert.AreEqual(new[] { "a" }, events[1]);
143 | CollectionAssert.AreEqual(new object[] { }, events[2]);
144 | events = null;
145 |
146 | guids = doc2.GetSubdocGuids().ToList();
147 | guids.Sort();
148 | CollectionAssert.AreEqual(new[] { "a", "c" }, guids);
149 | }
150 | }
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/tests/Ycs.Tests/Ycs.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp2.0;netcoreapp3.1
5 | false
6 | true
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------