├── .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 | ![img](https://github.com/yjs/ycs/blob/main/docs/ycs.gif) 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 | 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 | 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 | --------------------------------------------------------------------------------