├── Demo ├── ClientApp ├── Client │ ├── src │ │ ├── index.css │ │ ├── vite-env.d.ts │ │ ├── config.ts │ │ ├── main.tsx │ │ ├── hooks │ │ │ ├── useYjs.ts │ │ │ └── useYjsTldrawContext.ts │ │ ├── pages │ │ │ ├── Monaco.tsx │ │ │ ├── ProseMirror.tsx │ │ │ ├── Increment.tsx │ │ │ ├── Chat.tsx │ │ │ └── Tldraw.tsx │ │ ├── components │ │ │ ├── Awareness.tsx │ │ │ ├── YjsMonacoEditor.tsx │ │ │ ├── Increment.tsx │ │ │ ├── YjsTldrawEditor.tsx │ │ │ └── YjsProseMirror.tsx │ │ └── context │ │ │ └── yjsContext.tsx │ ├── .env.sample │ ├── .vite │ │ └── deps_temp_bce6eb42 │ │ │ └── package.json │ ├── .prettierrc.json │ ├── vite.config.ts │ ├── tsconfig.node.json │ ├── .gitignore │ ├── index.html │ ├── tsconfig.json │ ├── .eslintrc.cjs │ ├── README.md │ └── public │ │ └── vite.svg ├── wwwroot │ └── favicon.ico ├── package-lock.json ├── Controllers │ └── CollaborationController.cs ├── Db │ ├── AppDbContext.cs │ └── DbInitializer.cs ├── appsettings.Development.json ├── appsettings.json └── Demo.csproj ├── .gitattributes ├── assets └── logo-dotnet.png ├── Tests ├── YDotNet.Tests.Unit │ ├── Properties.cs │ ├── Infrastructure │ │ └── ClientIdGeneratorTests.cs │ ├── Document │ │ ├── NewTests.cs │ │ ├── IdTests.cs │ │ ├── AutoLoadTests.cs │ │ ├── ShouldLoadTests.cs │ │ ├── GuidTests.cs │ │ ├── CollectionIdTests.cs │ │ └── LoadTests.cs │ ├── Maps │ │ ├── CreateTests.cs │ │ ├── UnobserveTests.cs │ │ ├── RemoveAllTests.cs │ │ └── RemoveTests.cs │ ├── XmlFragments │ │ ├── CreateTests.cs │ │ ├── UnobserveTests.cs │ │ ├── InsertTextTests.cs │ │ └── ChildLengthTests.cs │ ├── Texts │ │ ├── StringTests.cs │ │ ├── UnobserveTests.cs │ │ └── LengthTests.cs │ ├── XmlTexts │ │ ├── CreateTests.cs │ │ └── UnobserveTests.cs │ ├── Arrays │ │ ├── CreateTests.cs │ │ └── UnobserveTests.cs │ ├── Branches │ │ ├── XmlTextObserveDeepTests.cs │ │ ├── TextUnobserveDeepTests.cs │ │ ├── XmlFragmentUnobserveDeepTests.cs │ │ ├── MapUnobserveDeepTests.cs │ │ ├── ArrayUnobserveDeepTests.cs │ │ ├── XmlTextUnobserveDeepTests.cs │ │ └── XmlElementUnobserveDeepTests.cs │ ├── XmlElements │ │ ├── CreateTests.cs │ │ └── UnobserveTests.cs │ ├── Transactions │ │ ├── WriteableTests.cs │ │ ├── SnapshotTests.cs │ │ ├── StateVectorV1Tests.cs │ │ ├── ApplyV1Tests.cs │ │ └── ApplyV2Tests.cs │ ├── UndoManagers │ │ ├── NewTests.cs │ │ ├── StopTests.cs │ │ ├── UnobservePoppedTests.cs │ │ ├── UnobserveAddedTests.cs │ │ ├── CanRedoTests.cs │ │ ├── CanUndoTests.cs │ │ └── RemoveOriginTests.cs │ ├── YDotNet.Tests.Unit.csproj │ └── StickyIndexes │ │ └── AssociationTypeTests.cs └── YDotNet.Tests.Driver │ ├── Abstractions │ └── ITask.cs │ ├── Program.cs │ ├── YDotNet.Tests.Driver.csproj │ └── Tasks │ ├── Docs │ ├── Create.cs │ ├── Clone.cs │ ├── ReadId.cs │ ├── ObserveClear.cs │ ├── ReadAutoLoad.cs │ ├── ReadGuid.cs │ ├── ReadShouldLoad.cs │ ├── ReadCollectionId.cs │ ├── ObserveSubDocs.cs │ ├── ObserveUpdatesV1.cs │ ├── ObserveUpdatesV2.cs │ └── ObserveAfterTransaction.cs │ ├── Texts │ ├── Create.cs │ ├── InsertText.cs │ ├── Observe.cs │ ├── Length.cs │ ├── String.cs │ ├── RemoveText.cs │ └── Chunks.cs │ └── Arrays │ ├── Create.cs │ ├── InsertRange.cs │ ├── Length.cs │ └── Observe.cs ├── YDotNet.Server ├── Properties.cs ├── DocumentContext.cs ├── Internal │ ├── DelegateDisposable.cs │ └── SubscribeToUpdatesV1Once.cs ├── CleanupOptions.cs ├── ConnectedUser.cs ├── Storage │ ├── IDocumentStorage.cs │ └── InMemoryDocumentStorage.cs ├── UpdateResult.cs ├── DocumentManagerOptions.cs ├── IDocumentCallback.cs ├── IDocumentManager.cs └── Events.cs ├── YDotNet ├── Document │ ├── Events │ │ ├── IEventSubscriber.cs │ │ ├── ClearEvent.cs │ │ ├── EventManager.cs │ │ ├── UpdateEvent.cs │ │ ├── EventPublisher.cs │ │ ├── AfterTransactionEvent.cs │ │ └── SubDocsEvent.cs │ ├── State │ │ ├── IdRange.cs │ │ ├── DeleteSet.cs │ │ └── StateVector.cs │ ├── UndoManagers │ │ ├── Events │ │ │ ├── UndoEventKind.cs │ │ │ └── UndoEvent.cs │ │ └── UndoManagerOptions.cs │ ├── Types │ │ ├── Events │ │ │ ├── EventPathSegmentTag.cs │ │ │ ├── EventKeyChangeTag.cs │ │ │ ├── EventDeltaTag.cs │ │ │ ├── EventChangeTag.cs │ │ │ ├── EventChanges.cs │ │ │ ├── EventDeltas.cs │ │ │ ├── EventKeys.cs │ │ │ ├── EventPath.cs │ │ │ ├── EventChange.cs │ │ │ ├── EventPathSegment.cs │ │ │ └── EventBranchTag.cs │ │ ├── Texts │ │ │ ├── TextChunks.cs │ │ │ └── TextChunk.cs │ │ ├── Arrays │ │ │ ├── ArrayEnumerator.cs │ │ │ └── ArrayIterator.cs │ │ └── XmlElements │ │ │ └── Trees │ │ │ └── XmlTreeWalker.cs │ ├── Options │ │ └── DocEncoding.cs │ ├── Cells │ │ ├── JsonArray.cs │ │ └── JsonObject.cs │ ├── Transactions │ │ └── TransactionUpdateResult.cs │ └── StickyIndexes │ │ └── StickyAssociationType.cs ├── Native │ ├── UndoManager │ │ ├── Events │ │ │ ├── UndoEventKindNative.cs │ │ │ └── UndoEventNative.cs │ │ └── UndoManagerOptionsNative.cs │ ├── Types │ │ ├── Events │ │ │ ├── EventDeltaTagNative.cs │ │ │ ├── EventChangeTagNative.cs │ │ │ ├── EventKeyChangeTagNative.cs │ │ │ ├── EventPathSegmentNative.cs │ │ │ ├── EventKeyChangeNative.cs │ │ │ ├── EventChangeNative.cs │ │ │ └── EventDeltaNative.cs │ │ ├── Branches │ │ │ ├── BranchKind.cs │ │ │ ├── BranchIdVariantNative.cs │ │ │ ├── BranchIdNative.cs │ │ │ └── BranchChannel.cs │ │ ├── SubscriptionChannel.cs │ │ ├── StringChannel.cs │ │ ├── PathChannel.cs │ │ ├── BinaryChannel.cs │ │ ├── ChunksChannel.cs │ │ ├── XmlAttributeNative.cs │ │ ├── XmlChannel.cs │ │ ├── EventChannel.cs │ │ ├── Maps │ │ │ └── MapEntryNative.cs │ │ ├── XmlAttributeChannel.cs │ │ └── Texts │ │ │ └── TextChunkNative.cs │ ├── NativeWithHandle.cs │ ├── ChannelSettings.cs │ ├── Document │ │ ├── State │ │ │ ├── IdRangeNative.cs │ │ │ ├── IdRangeSequenceNative.cs │ │ │ ├── StateVectorNative.cs │ │ │ └── DeleteSetNative.cs │ │ ├── Events │ │ │ ├── AfterTransactionEventNative.cs │ │ │ ├── EventBranchNative.cs │ │ │ ├── UpdateEventNative.cs │ │ │ └── SubDocsEventNative.cs │ │ └── DocOptionsNative.cs │ ├── .editorconfig │ └── Cells │ │ ├── Outputs │ │ └── OutputNative.cs │ │ └── Inputs │ │ └── InputNative.cs ├── Infrastructure │ ├── MemoryConstants.cs │ ├── Extensions │ │ └── IntPtrExtensions.cs │ ├── UnmanagedResource.cs │ ├── ThrowHelper.cs │ ├── ClientIdGenerator.cs │ └── TypeCache.cs ├── Protocol │ └── BufferEncoder.cs └── YDotNetException.cs ├── native ├── build.patch ├── YDotNet.Native │ └── YDotNet.Native.csproj ├── YDotNet.Native.Win32 │ └── YDotNet.Native.Win32.csproj ├── YDotNet.Native.MacOS │ └── YDotNet.Native.MacOS.csproj └── YDotNet.Native.Linux │ └── YDotNet.Native.Linux.csproj ├── YDotNet.Server.MongoDB ├── DocumentEntity.cs ├── MongoDocumentStorageOptions.cs ├── ServiceExtensions.cs └── YDotNet.Server.MongoDB.csproj ├── YDotNet.Server.Redis ├── RedisDocumentStorageOptions.cs ├── RedisConnection.cs ├── Internal │ └── LoggerTextWriter.cs ├── RedisOptions.cs ├── ServiceExtensions.cs └── YDotNet.Server.Redis.csproj ├── YDotNet.Server.EntityFramework ├── EFDocumentStorageOptions.cs ├── YDotNetDocument.cs ├── YDotNet.Server.EntityFramework.csproj └── ServiceExtensions.cs ├── .cargo └── config.github ├── YDotNet.Server.WebSockets ├── YDotNetWebSocketOptions.cs ├── YDotNetActionResult.cs ├── ServiceExtensions.cs ├── WebSocketEncoder.cs ├── ClientState.cs └── YDotNet.Server.WebSockets.csproj ├── .gitignore ├── stylecop.json ├── Directory.Build.props ├── .github └── workflows │ └── publish.yml ├── LICENSE └── YDotNet.Extensions └── YDotNet.Extensions.csproj /Demo/ClientApp: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Demo/Client/src/index.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf autocrlf=false 2 | -------------------------------------------------------------------------------- /Demo/Client/.env.sample: -------------------------------------------------------------------------------- 1 | VITE_WS_URL=wss://localhost:5001/collaboration -------------------------------------------------------------------------------- /Demo/Client/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /Demo/Client/.vite/deps_temp_bce6eb42/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /Demo/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-crdt/ydotnet/HEAD/Demo/wwwroot/favicon.ico -------------------------------------------------------------------------------- /assets/logo-dotnet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-crdt/ydotnet/HEAD/assets/logo-dotnet.png -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Properties.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | [assembly: NonParallelizable] 4 | -------------------------------------------------------------------------------- /Demo/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Demo", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /YDotNet.Server/Properties.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("YDotNet.Tests.Server.Unit")] 4 | -------------------------------------------------------------------------------- /Demo/Client/src/config.ts: -------------------------------------------------------------------------------- 1 | const config = { 2 | WS_URL: import.meta.env.VITE_WS_URL || 'ws://localhost:5000', 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Driver/Abstractions/ITask.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Tests.Driver.Abstractions; 2 | 3 | public interface ITask 4 | { 5 | Task Run(); 6 | } 7 | -------------------------------------------------------------------------------- /YDotNet/Document/Events/IEventSubscriber.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Document.Events; 2 | 3 | internal interface IEventSubscriber 4 | { 5 | void Clear(); 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Client/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true, 6 | "arrowParens": "always" 7 | } 8 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Driver/Program.cs: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/new-console-template for more information 2 | 3 | using YDotNet.Tests.Driver.Tasks.Docs; 4 | 5 | new ObserveSubDocs().Run(); 6 | -------------------------------------------------------------------------------- /YDotNet/Native/UndoManager/Events/UndoEventKindNative.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Native.UndoManager.Events; 2 | 3 | internal enum UndoEventKindNative : byte 4 | { 5 | Undo = 0, 6 | Redo = 1, 7 | } 8 | -------------------------------------------------------------------------------- /YDotNet.Server/DocumentContext.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Server; 2 | 3 | public sealed record DocumentContext(string DocumentName, ulong ClientId) 4 | { 5 | public object? Metadata { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /YDotNet/Native/Types/Events/EventDeltaTagNative.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Native.Types.Events; 2 | 3 | internal enum EventDeltaTagNative : sbyte 4 | { 5 | Add = 1, 6 | Remove = 2, 7 | Retain = 3, 8 | } 9 | -------------------------------------------------------------------------------- /YDotNet/Native/Types/Events/EventChangeTagNative.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Native.Types.Events; 2 | 3 | internal enum EventChangeTagNative : sbyte 4 | { 5 | Add = 1, 6 | Remove = 2, 7 | Retain = 3, 8 | } 9 | -------------------------------------------------------------------------------- /Demo/Client/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | -------------------------------------------------------------------------------- /YDotNet/Native/Types/Events/EventKeyChangeTagNative.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Native.Types.Events; 2 | 3 | internal enum EventKeyChangeTagNative : byte 4 | { 5 | Add = 4, 6 | Remove = 5, 7 | Update = 6, 8 | } 9 | -------------------------------------------------------------------------------- /YDotNet/Native/UndoManager/UndoManagerOptionsNative.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Native.UndoManager; 2 | 3 | internal struct UndoManagerOptionsNative 4 | { 5 | internal uint CaptureTimeoutMilliseconds { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /YDotNet/Native/NativeWithHandle.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace YDotNet.Native; 4 | 5 | [StructLayout(LayoutKind.Sequential)] 6 | internal record struct NativeWithHandle(T Value, nint Handle) 7 | where T : struct; 8 | -------------------------------------------------------------------------------- /Demo/Client/src/main.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom/client'; 2 | import App from './App'; 3 | import 'bootstrap/dist/css/bootstrap.min.css'; 4 | import './index.css'; 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render(); 7 | -------------------------------------------------------------------------------- /YDotNet.Server/Internal/DelegateDisposable.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Server.Internal; 2 | 3 | public sealed class DelegateDisposable(Action callback) : IDisposable 4 | { 5 | public void Dispose() 6 | { 7 | callback(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /YDotNet/Infrastructure/MemoryConstants.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace YDotNet.Infrastructure; 4 | 5 | internal static class MemoryConstants 6 | { 7 | internal static readonly int PointerSize = Marshal.SizeOf(); 8 | } 9 | -------------------------------------------------------------------------------- /YDotNet/Native/ChannelSettings.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Native; 2 | 3 | internal static class ChannelSettings 4 | { 5 | // https://learn.microsoft.com/en-us/dotnet/standard/native-interop/native-library-loading 6 | public const string NativeLib = "yrs"; 7 | } 8 | -------------------------------------------------------------------------------- /YDotNet.Server/CleanupOptions.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Server; 2 | 3 | public sealed class CleanupOptions 4 | { 5 | public TimeSpan Interval { get; set; } = TimeSpan.FromMinutes(1); 6 | 7 | public TimeSpan LogWaitTime { get; set; } = TimeSpan.FromMinutes(10); 8 | } 9 | -------------------------------------------------------------------------------- /YDotNet.Server/ConnectedUser.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Server; 2 | 3 | public sealed class ConnectedUser 4 | { 5 | public string? ClientState { get; set; } 6 | 7 | public ulong ClientClock { get; set; } 8 | 9 | public DateTime LastActivity { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /YDotNet/Native/Types/Branches/BranchKind.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Native.Types.Branches; 2 | 3 | internal enum BranchKind 4 | { 5 | Null = 0, 6 | Array = 1, 7 | Map = 2, 8 | Text = 3, 9 | XmlElement = 4, 10 | XmlText = 5, 11 | XmlFragment = 6 12 | } 13 | -------------------------------------------------------------------------------- /Demo/Client/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /native/build.patch: -------------------------------------------------------------------------------- 1 | diff --git a/yffi/Cargo.toml b/yffi/Cargo.toml 2 | index e951bd1..e994e36 100644 3 | --- a/yffi/Cargo.toml 4 | +++ b/yffi/Cargo.toml 5 | @@ -16,5 +16,5 @@ 6 | 7 | [lib] 8 | -crate-type = ["staticlib"] 9 | +crate-type = ["staticlib", "cdylib"] 10 | name = "yrs" 11 | -------------------------------------------------------------------------------- /YDotNet.Server.MongoDB/DocumentEntity.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Server.MongoDB; 2 | 3 | internal sealed class DocumentEntity 4 | { 5 | required public string Id { get; set; } 6 | 7 | required public byte[] Data { get; set; } 8 | 9 | public DateTime? Expiration { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /YDotNet.Server/Storage/IDocumentStorage.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Server.Storage; 2 | 3 | public interface IDocumentStorage 4 | { 5 | ValueTask GetDocAsync(string name, CancellationToken ct = default); 6 | 7 | ValueTask StoreDocAsync(string name, byte[] doc, CancellationToken ct = default); 8 | } 9 | -------------------------------------------------------------------------------- /YDotNet/Native/Document/State/IdRangeNative.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace YDotNet.Native.Document.State; 4 | 5 | [StructLayout(LayoutKind.Sequential)] 6 | internal readonly struct IdRangeNative 7 | { 8 | public uint Start { get; } 9 | 10 | public uint End { get; } 11 | } 12 | -------------------------------------------------------------------------------- /YDotNet.Server/UpdateResult.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document.Transactions; 2 | 3 | namespace YDotNet.Server; 4 | 5 | public sealed class UpdateResult 6 | { 7 | public TransactionUpdateResult TransactionUpdateResult { get; set; } 8 | 9 | public bool IsSkipped { get; set; } 10 | 11 | public byte[]? Diff { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /YDotNet/Native/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # StyleCop rules 4 | 5 | # Documentation rules 6 | dotnet_diagnostic.SA1600.severity = none # Elements should be documented 7 | dotnet_diagnostic.SA1601.severity = none # Partial elements should be documented 8 | dotnet_diagnostic.SA1602.severity = none # Enumeration items should be documented 9 | -------------------------------------------------------------------------------- /YDotNet.Server.Redis/RedisDocumentStorageOptions.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Server.Redis; 2 | 3 | public sealed class RedisDocumentStorageOptions 4 | { 5 | public Func? Expiration { get; set; } 6 | 7 | public int Database { get; set; } 8 | 9 | public string Prefix { get; set; } = "YDotNetDocument_"; 10 | } 11 | -------------------------------------------------------------------------------- /YDotNet.Server.EntityFramework/EFDocumentStorageOptions.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Server.EntityFramework; 2 | 3 | using System; 4 | 5 | public sealed class EFDocumentStorageOptions 6 | { 7 | public Func? Expiration { get; set; } 8 | 9 | public TimeSpan CleanupInterval { get; set; } = TimeSpan.FromMinutes(30); 10 | } 11 | -------------------------------------------------------------------------------- /.cargo/config.github: -------------------------------------------------------------------------------- 1 | [target.aarch64-unknown-linux-gnu] 2 | linker = "aarch64-linux-gnu-gcc" 3 | 4 | [target.aarch64-unknown-linux-musl] 5 | linker = "aarch64-linux-gnu-gcc" 6 | 7 | [target.armv7-unknown-linux-gnueabihf] 8 | linker = "arm-linux-gnueabihf-gcc" 9 | 10 | [target.armv7-unknown-linux-musleabihf] 11 | linker = "arm-linux-musleabihf-gcc" 12 | -------------------------------------------------------------------------------- /YDotNet.Server.MongoDB/MongoDocumentStorageOptions.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Server.MongoDB; 2 | 3 | public sealed class MongoDocumentStorageOptions 4 | { 5 | public Func? Expiration { get; set; } 6 | 7 | public string DatabaseName { get; set; } = "YDotNet"; 8 | 9 | public string CollectionName { get; set; } = "YDotNet"; 10 | } 11 | -------------------------------------------------------------------------------- /YDotNet/Infrastructure/Extensions/IntPtrExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Infrastructure.Extensions; 2 | 3 | internal static class IntPtrExtensions 4 | { 5 | public static nint Checked(this nint input) 6 | { 7 | if (input == nint.Zero) 8 | { 9 | ThrowHelper.Null(); 10 | } 11 | 12 | return input; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /YDotNet/Native/Types/SubscriptionChannel.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace YDotNet.Native.Types; 4 | 5 | internal static class SubscriptionChannel 6 | { 7 | [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yunobserve")] 8 | public static extern void Unobserve(nint subscription); 9 | } 10 | -------------------------------------------------------------------------------- /Demo/Client/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /YDotNet/Native/Types/StringChannel.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace YDotNet.Native.Types; 4 | 5 | internal static class StringChannel 6 | { 7 | [DllImport( 8 | ChannelSettings.NativeLib, 9 | CallingConvention = CallingConvention.Cdecl, 10 | EntryPoint = "ystring_destroy")] 11 | public static extern void Destroy(nint handle); 12 | } 13 | -------------------------------------------------------------------------------- /Demo/Controllers/CollaborationController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using YDotNet.Server.WebSockets; 3 | 4 | namespace Demo.Controllers; 5 | 6 | public class CollaborationController : Controller 7 | { 8 | [HttpGet("/collaboration2/{roomName}")] 9 | public IActionResult Room(string roomName) 10 | { 11 | return new YDotNetActionResult(roomName); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Infrastructure/ClientIdGeneratorTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Infrastructure; 3 | 4 | namespace YDotNet.Tests.Unit.Infrastructure; 5 | 6 | public class ClientIdGeneratorTests 7 | { 8 | [Test] 9 | public void HasCorrectMaxValue() 10 | { 11 | Assert.That(ClientIdGenerator.MaxSafeInteger, Is.EqualTo((2 ^ 53) - 1)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /YDotNet.Server.EntityFramework/YDotNetDocument.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Server.EntityFramework; 2 | 3 | using System; 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | public sealed class YDotNetDocument 7 | { 8 | [Key] 9 | required public string Id { get; set; } 10 | 11 | required public byte[] Data { get; set; } 12 | 13 | public DateTime? Expiration { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /YDotNet/Native/Types/PathChannel.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace YDotNet.Native.Types; 4 | 5 | internal static class PathChannel 6 | { 7 | [DllImport( 8 | ChannelSettings.NativeLib, 9 | CallingConvention = CallingConvention.Cdecl, 10 | EntryPoint = "ypath_destroy")] 11 | public static extern uint Destroy(nint paths, uint length); 12 | } 13 | -------------------------------------------------------------------------------- /YDotNet/Native/Types/BinaryChannel.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace YDotNet.Native.Types; 4 | 5 | internal static class BinaryChannel 6 | { 7 | [DllImport( 8 | ChannelSettings.NativeLib, 9 | CallingConvention = CallingConvention.Cdecl, 10 | EntryPoint = "ybinary_destroy")] 11 | public static extern void Destroy(nint handle, uint length); 12 | } 13 | -------------------------------------------------------------------------------- /YDotNet/Native/Types/ChunksChannel.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace YDotNet.Native.Types; 4 | 5 | internal static class ChunksChannel 6 | { 7 | [DllImport( 8 | ChannelSettings.NativeLib, 9 | CallingConvention = CallingConvention.Cdecl, 10 | EntryPoint = "ychunks_destroy")] 11 | public static extern nint Destroy(nint chunks, uint length); 12 | } 13 | -------------------------------------------------------------------------------- /Demo/Db/AppDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using YDotNet.Server.EntityFramework; 3 | 4 | namespace Demo.Db; 5 | 6 | public class AppDbContext(DbContextOptions options) : DbContext(options) 7 | { 8 | protected override void OnModelCreating(ModelBuilder modelBuilder) 9 | { 10 | modelBuilder.UseYDotNet(); 11 | base.OnModelCreating(modelBuilder); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Document/NewTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | 4 | namespace YDotNet.Tests.Unit.Document; 5 | 6 | public class NewTests 7 | { 8 | [Test] 9 | public void Create() 10 | { 11 | // Arrange and Act 12 | var doc = new Doc(); 13 | 14 | // Assert 15 | Assert.That(doc.Handle, Is.GreaterThan(nint.Zero)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Demo/Client/src/hooks/useYjs.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { YjsContext } from '../context/yjsContext'; 3 | 4 | export function useYjs() { 5 | const yjsContext = React.useContext(YjsContext); 6 | 7 | if (yjsContext === undefined) { 8 | throw new Error( 9 | 'useYjs() should be called with the YjsContext defined.' 10 | ); 11 | } 12 | 13 | return yjsContext; 14 | } 15 | -------------------------------------------------------------------------------- /Demo/Client/src/pages/Monaco.tsx: -------------------------------------------------------------------------------- 1 | import { YjsMonacoEditor } from '../components/YjsMonacoEditor'; 2 | import { YjsContextProvider } from '../context/yjsContext'; 3 | 4 | function MonacoPage() { 5 | return ( 6 | 7 | Monaco 8 | 9 | 10 | 11 | ); 12 | } 13 | 14 | export default MonacoPage; 15 | -------------------------------------------------------------------------------- /YDotNet/Native/Types/Branches/BranchIdVariantNative.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace YDotNet.Native.Types.Branches; 4 | 5 | [StructLayout(LayoutKind.Explicit, Size = 8)] 6 | internal readonly struct BranchIdVariantNative 7 | { 8 | [field: FieldOffset(offset: 0)] 9 | public uint Clock { get; } 10 | 11 | [field: FieldOffset(offset: 0)] 12 | public nint NamePointer { get; } 13 | } 14 | -------------------------------------------------------------------------------- /YDotNet/Native/Cells/Outputs/OutputNative.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace YDotNet.Native.Cells.Outputs; 4 | 5 | [StructLayout(LayoutKind.Explicit, Size = Size)] 6 | internal struct OutputNative 7 | { 8 | internal const int Size = 16; 9 | 10 | [field: FieldOffset(offset: 0)] 11 | public sbyte Tag { get; } 12 | 13 | [field: FieldOffset(offset: 4)] 14 | public uint Length { get; } 15 | } 16 | -------------------------------------------------------------------------------- /YDotNet/Native/Types/Branches/BranchIdNative.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace YDotNet.Native.Types.Branches; 4 | 5 | [StructLayout(LayoutKind.Explicit, Size = 16)] 6 | internal readonly struct BranchIdNative 7 | { 8 | [field: FieldOffset(offset: 0)] 9 | public long ClientIdOrLength { get; } 10 | 11 | [field: FieldOffset(offset: 8)] 12 | public BranchIdVariantNative BranchIdVariant { get; } 13 | } 14 | -------------------------------------------------------------------------------- /Demo/Client/src/pages/ProseMirror.tsx: -------------------------------------------------------------------------------- 1 | import { YjsProseMirror } from '../components/YjsProseMirror'; 2 | import { YjsContextProvider } from '../context/yjsContext'; 3 | 4 | function ProseMirrorPage() { 5 | return ( 6 | 7 | Prose Mirror 8 | 9 | 10 | 11 | ); 12 | } 13 | 14 | export default ProseMirrorPage; 15 | -------------------------------------------------------------------------------- /YDotNet/Document/State/IdRange.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Native.Document.State; 2 | 3 | namespace YDotNet.Document.State; 4 | 5 | /// 6 | /// Represents a single space of clock values, belonging to the same client. 7 | /// 8 | public sealed record IdRange(uint Start, uint End) 9 | { 10 | internal static IdRange Create(IdRangeNative native) 11 | { 12 | return new IdRange(native.Start, native.End); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /YDotNet/Infrastructure/UnmanagedResource.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Infrastructure; 2 | 3 | /// 4 | /// Base class for all unmanaged resources. 5 | /// 6 | public abstract class UnmanagedResource : Resource 7 | { 8 | protected internal UnmanagedResource(nint handle, bool isDisposed = false) 9 | : base(isDisposed) 10 | { 11 | Handle = handle; 12 | } 13 | 14 | internal nint Handle { get; } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Maps/CreateTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | 4 | namespace YDotNet.Tests.Unit.Maps; 5 | 6 | public class MapTests 7 | { 8 | [Test] 9 | public void Create() 10 | { 11 | // Arrange 12 | var doc = new Doc(); 13 | 14 | // Act 15 | var map = doc.Map("map"); 16 | 17 | // Assert 18 | Assert.That(map.Handle, Is.GreaterThan(nint.Zero)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Driver/YDotNet.Tests.Driver.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /YDotNet/Native/Document/Events/AfterTransactionEventNative.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using YDotNet.Native.Document.State; 3 | 4 | namespace YDotNet.Native.Document.Events; 5 | 6 | [StructLayout(LayoutKind.Sequential)] 7 | internal readonly struct AfterTransactionEventNative 8 | { 9 | public StateVectorNative BeforeState { get; } 10 | 11 | public StateVectorNative AfterState { get; } 12 | 13 | public DeleteSetNative DeleteSet { get; } 14 | } 15 | -------------------------------------------------------------------------------- /Demo/Client/src/hooks/useYjsTldrawContext.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { YjsTldrawContext } from '../context/yjsTldrawContext'; 3 | 4 | export function useYjsTldrawContext() { 5 | const context = React.useContext(YjsTldrawContext); 6 | 7 | if (context === undefined) { 8 | throw new Error( 9 | 'useYjsTldrawContext() should be called with the YjsTldrawContext defined.' 10 | ); 11 | } 12 | 13 | return context; 14 | } 15 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Document/IdTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | 4 | namespace YDotNet.Tests.Unit.Document; 5 | 6 | public class IdTests 7 | { 8 | [Test] 9 | public void IsGreaterOrEqualToZeroByDefault() 10 | { 11 | // Arrange 12 | var doc = new Doc(); 13 | 14 | // Act 15 | var id = doc.Id; 16 | 17 | // Assert 18 | Assert.That(id, Is.GreaterThanOrEqualTo(expected: 0)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /YDotNet/Document/UndoManagers/Events/UndoEventKind.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Document.UndoManagers.Events; 2 | 3 | /// 4 | /// Represents the kind of change in an . 5 | /// 6 | public enum UndoEventKind 7 | { 8 | /// 9 | /// Represents an undo operation. 10 | /// 11 | Undo = 0, 12 | 13 | /// 14 | /// Represents a redo operation. 15 | /// 16 | Redo = 1, 17 | } 18 | -------------------------------------------------------------------------------- /YDotNet.Server.WebSockets/YDotNetWebSocketOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | 3 | namespace YDotNet.Server.WebSockets; 4 | 5 | #pragma warning disable MA0048 // File name must match type name 6 | public delegate Task AuthDelegate(HttpContext httpContext, DocumentContext context); 7 | #pragma warning restore MA0048 // File name must match type name 8 | 9 | public sealed class YDotNetWebSocketOptions 10 | { 11 | public AuthDelegate? OnAuthenticateAsync { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /YDotNet/Document/Events/ClearEvent.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Document.Events; 2 | 3 | /// 4 | /// A clear event passed to a callback subscribed to . 5 | /// 6 | public class ClearEvent 7 | { 8 | internal ClearEvent(Doc doc) 9 | { 10 | Doc = doc; 11 | } 12 | 13 | /// 14 | /// Gets the associated with this event. 15 | /// 16 | public Doc Doc { get; } 17 | } 18 | -------------------------------------------------------------------------------- /Demo/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning", 6 | "YDotNet": "Debug" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "Storage": { 11 | "Type": "None", 12 | "MongoDB": { 13 | "ConnectionString": "mongodb://localhost:27017", 14 | "Database": "YDotNet" 15 | }, 16 | "Redis": { 17 | "ConnectionString": "localhost:6379" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /YDotNet/Infrastructure/ThrowHelper.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Infrastructure; 2 | 3 | internal static class ThrowHelper 4 | { 5 | public static void Null() 6 | { 7 | throw new YDotNetException("Operation failed. The yffi library returned null without further details."); 8 | } 9 | 10 | public static void PendingTransaction() 11 | { 12 | throw new YDotNetException("Failed to open a transaction, probably because another transaction is still open."); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /YDotNet/Native/Cells/Inputs/InputNative.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace YDotNet.Native.Cells.Inputs; 4 | 5 | // The size has to be 24 here so that the whole data of the input cell is written/read correctly over the C FFI. 6 | [StructLayout(LayoutKind.Explicit, Size = 24)] 7 | internal readonly struct InputNative 8 | { 9 | [field: FieldOffset(offset: 0)] 10 | public sbyte Tag { get; } 11 | 12 | [field: FieldOffset(offset: 4)] 13 | public uint Length { get; } 14 | } 15 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/XmlFragments/CreateTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | 4 | namespace YDotNet.Tests.Unit.XmlFragments; 5 | 6 | public class CreateTests 7 | { 8 | [Test] 9 | public void Create() 10 | { 11 | // Arrange 12 | var doc = new Doc(); 13 | 14 | // Act 15 | var xmlFragment = doc.XmlFragment("xml-fragment"); 16 | 17 | // Assert 18 | Assert.That(xmlFragment.Handle, Is.GreaterThan(nint.Zero)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Document/AutoLoadTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | 4 | namespace YDotNet.Tests.Unit.Document; 5 | 6 | public class AutoLoadTests 7 | { 8 | [Test] 9 | public void AutoLoad() 10 | { 11 | // Arrange 12 | var doc = new Doc(); 13 | 14 | // Act 15 | var autoLoad = doc.AutoLoad; 16 | 17 | // Assert 18 | Assert.That(autoLoad, Is.False, "The default value for Doc.AutoLoad should be false."); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Demo/Client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | YJS 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Document/ShouldLoadTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | 4 | namespace YDotNet.Tests.Unit.Document; 5 | 6 | public class ShouldLoadTests 7 | { 8 | [Test] 9 | public void ShouldLoad() 10 | { 11 | // Arrange 12 | var doc = new Doc(); 13 | 14 | // Act 15 | var shouldLoad = doc.ShouldLoad; 16 | 17 | // Assert 18 | Assert.That(shouldLoad, Is.True, "The default value for Doc.ShouldLoad should be true."); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /YDotNet.Server/DocumentManagerOptions.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Server; 2 | 3 | public sealed class DocumentManagerOptions 4 | { 5 | public bool AutoCreateDocument { get; set; } = true; 6 | 7 | public TimeSpan StoreDebounce { get; set; } = TimeSpan.FromMilliseconds(1000); 8 | 9 | public TimeSpan MaxWriteTimeInterval { get; set; } = TimeSpan.FromSeconds(5); 10 | 11 | public TimeSpan CacheDuration { get; set; } = TimeSpan.FromSeconds(30); 12 | 13 | public TimeSpan MaxPingTime { get; set; } = TimeSpan.FromMinutes(1); 14 | } 15 | -------------------------------------------------------------------------------- /YDotNet/Native/Document/State/IdRangeSequenceNative.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using YDotNet.Infrastructure; 3 | 4 | namespace YDotNet.Native.Document.State; 5 | 6 | [StructLayout(LayoutKind.Sequential)] 7 | internal readonly struct IdRangeSequenceNative 8 | { 9 | public uint SequenceLength { get; } 10 | 11 | public nint SequenceHandle { get; } 12 | 13 | public IdRangeNative[] Sequence() 14 | { 15 | return MemoryReader.ReadStructs(SequenceHandle, SequenceLength); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /YDotNet/Native/Types/Events/EventPathSegmentNative.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using YDotNet.Infrastructure; 3 | 4 | namespace YDotNet.Native.Types.Events; 5 | 6 | [StructLayout(LayoutKind.Explicit)] 7 | internal readonly struct EventPathSegmentNative 8 | { 9 | [field: FieldOffset(0)] 10 | public byte Tag { get; } 11 | 12 | [field: FieldOffset(8)] 13 | public nint KeyOrIndex { get; } 14 | 15 | public string Key() 16 | { 17 | return MemoryReader.ReadUtf8String(KeyOrIndex); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /YDotNet/Native/Document/Events/EventBranchNative.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using YDotNet.Infrastructure; 3 | using YDotNet.Native.Cells.Outputs; 4 | 5 | namespace YDotNet.Native.Document.Events; 6 | 7 | [StructLayout(LayoutKind.Sequential, Size = Size)] 8 | internal struct EventBranchNative 9 | { 10 | public const int Size = 8 + OutputNative.Size; 11 | 12 | public uint Tag { get; } 13 | 14 | public nint ValueHandle(nint baseHandle) 15 | { 16 | return baseHandle + MemoryConstants.PointerSize; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Demo/Client/src/pages/Increment.tsx: -------------------------------------------------------------------------------- 1 | import { Awareness } from '../components/Awareness'; 2 | import { Increment } from '../components/Increment'; 3 | import { YjsContextProvider } from '../context/yjsContext'; 4 | 5 | function IncrementPage() { 6 | return ( 7 | 8 | Increment 9 | 10 | 11 | 12 | Awareness 13 | 14 | 15 | 16 | ); 17 | } 18 | 19 | export default IncrementPage; 20 | -------------------------------------------------------------------------------- /YDotNet/Native/Document/DocOptionsNative.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace YDotNet.Native.Document; 4 | 5 | [StructLayout(LayoutKind.Sequential)] 6 | internal readonly struct DocOptionsNative 7 | { 8 | public ulong Id { get; init; } 9 | 10 | public nint Guid { get; init; } 11 | 12 | public nint CollectionId { get; init; } 13 | 14 | public byte Encoding { get; init; } 15 | 16 | public byte SkipGc { get; init; } 17 | 18 | public byte AutoLoad { get; init; } 19 | 20 | public byte ShouldLoad { get; init; } 21 | } 22 | -------------------------------------------------------------------------------- /YDotNet.Server.WebSockets/YDotNetActionResult.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace YDotNet.Server.WebSockets; 5 | 6 | public sealed class YDotNetActionResult(string documentName) : IActionResult 7 | { 8 | public async Task ExecuteResultAsync(ActionContext context) 9 | { 10 | var middleware = context.HttpContext.RequestServices.GetRequiredService(); 11 | 12 | await middleware.InvokeAsync(context.HttpContext, documentName).ConfigureAwait(false); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /YDotNet/Native/Types/XmlAttributeNative.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using YDotNet.Infrastructure; 3 | 4 | namespace YDotNet.Native.Types; 5 | 6 | [StructLayout(LayoutKind.Sequential)] 7 | internal readonly struct XmlAttributeNative 8 | { 9 | public nint KeyHandle { get; } 10 | 11 | public nint ValueHandle { get; } 12 | 13 | public string Key() 14 | { 15 | return MemoryReader.ReadUtf8String(KeyHandle); 16 | } 17 | 18 | public string Value() 19 | { 20 | return MemoryReader.ReadUtf8String(ValueHandle); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /YDotNet/Native/Types/Events/EventKeyChangeNative.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using YDotNet.Infrastructure; 3 | 4 | namespace YDotNet.Native.Types.Events; 5 | 6 | [StructLayout(LayoutKind.Sequential)] 7 | internal readonly struct EventKeyChangeNative 8 | { 9 | public nint KeyHandle { get; } 10 | 11 | public EventKeyChangeTagNative TagNative { get; } 12 | 13 | public nint OldValue { get; } 14 | 15 | public nint NewValue { get; } 16 | 17 | public string Key() 18 | { 19 | return MemoryReader.ReadUtf8String(KeyHandle); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /YDotNet.Server/Internal/SubscribeToUpdatesV1Once.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document; 2 | 3 | namespace YDotNet.Server.Internal; 4 | 5 | internal sealed class SubscribeToUpdatesV1Once : IDisposable 6 | { 7 | private readonly IDisposable unsubscribe; 8 | 9 | public SubscribeToUpdatesV1Once(Doc doc) 10 | { 11 | unsubscribe = doc.ObserveUpdatesV1(@event => 12 | { 13 | Update = @event.Update; 14 | }); 15 | } 16 | 17 | public byte[]? Update { get; private set; } 18 | 19 | public void Dispose() 20 | { 21 | unsubscribe.Dispose(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /YDotNet/Document/Types/Events/EventPathSegmentTag.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Document.Types.Events; 2 | 3 | /// 4 | /// Represents the type of data is held by the related instance. 5 | /// 6 | public enum EventPathSegmentTag : sbyte 7 | { 8 | /// 9 | /// The contains a instance. 10 | /// 11 | Key = 1, 12 | 13 | /// 14 | /// The contains an value. 15 | /// 16 | Index = 2, 17 | } 18 | -------------------------------------------------------------------------------- /Demo/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning", 6 | "YDotNet": "Debug" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "Storage": { 11 | "Type": "InMemory", 12 | "MongoDB": { 13 | "ConnectionString": "mongodb://localhost:27017", 14 | "Database": "YDotNet" 15 | }, 16 | "Redis": { 17 | "ConnectionString": "localhost:6379" 18 | }, 19 | "Postgres": { 20 | "ConnectionString": "Host=localhost;Port=5432;Database=ydotnet;Username=postgres;Password=password" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Texts/StringTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | 4 | namespace YDotNet.Tests.Unit.Texts; 5 | 6 | public class StringTests 7 | { 8 | [Test] 9 | public void ReturnsTheFullText() 10 | { 11 | // Arrange 12 | var doc = new Doc(); 13 | var text = doc.Text("name"); 14 | 15 | // Act 16 | var transaction = doc.WriteTransaction(); 17 | text.Insert(transaction, index: 0, "Star. ⭐"); 18 | 19 | // Assert 20 | Assert.That(text.String(transaction), Is.EqualTo("Star. ⭐")); 21 | 22 | transaction.Commit(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /YDotNet/Native/Document/Events/UpdateEventNative.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using YDotNet.Infrastructure; 3 | 4 | namespace YDotNet.Native.Document.Events; 5 | 6 | [StructLayout(LayoutKind.Sequential)] 7 | internal readonly struct UpdateEventNative 8 | { 9 | public uint Length { get; init; } 10 | 11 | public nint Data { get; init; } 12 | 13 | public byte[] Bytes() 14 | { 15 | return MemoryReader.ReadBytes(Data, Length); 16 | } 17 | 18 | public static UpdateEventNative From(uint length, nint data) 19 | { 20 | return new UpdateEventNative { Length = length, Data = data }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /YDotNet/Native/Types/XmlChannel.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace YDotNet.Native.Types; 4 | 5 | internal static class XmlChannel 6 | { 7 | [DllImport( 8 | ChannelSettings.NativeLib, 9 | CallingConvention = CallingConvention.Cdecl, 10 | EntryPoint = "yxml_prev_sibling")] 11 | public static extern nint PreviousSibling(nint handle, nint transaction); 12 | 13 | [DllImport( 14 | ChannelSettings.NativeLib, 15 | CallingConvention = CallingConvention.Cdecl, 16 | EntryPoint = "yxml_next_sibling")] 17 | public static extern nint NextSibling(nint handle, nint transaction); 18 | } 19 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/XmlTexts/CreateTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | 4 | namespace YDotNet.Tests.Unit.XmlTexts; 5 | 6 | public class CreateTests 7 | { 8 | [Test] 9 | public void Create() 10 | { 11 | // Arrange 12 | var doc = new Doc(); 13 | var xmlFragment = doc.XmlFragment("xml-fragment"); 14 | 15 | // Act 16 | var transaction = doc.WriteTransaction(); 17 | var xmlText = xmlFragment.InsertText(transaction, index: 0); 18 | transaction.Commit(); 19 | 20 | // Assert 21 | Assert.That(xmlText.Handle, Is.GreaterThan(nint.Zero)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /YDotNet.Server.Redis/RedisConnection.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Microsoft.Extensions.Options; 3 | using StackExchange.Redis; 4 | using YDotNet.Server.Redis.Internal; 5 | 6 | namespace YDotNet.Server.Redis; 7 | 8 | public sealed class RedisConnection(IOptions options, ILogger logger) : IDisposable 9 | { 10 | public Task Instance { get; } = options.Value.ConnectAsync(new LoggerTextWriter(logger)); 11 | 12 | public void Dispose() 13 | { 14 | if (Instance.IsCompletedSuccessfully) 15 | { 16 | Instance.Result.Close(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /YDotNet/Native/Types/EventChannel.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace YDotNet.Native.Types; 4 | 5 | internal static class EventChannel 6 | { 7 | [DllImport( 8 | ChannelSettings.NativeLib, 9 | CallingConvention = CallingConvention.Cdecl, 10 | EntryPoint = "yevent_keys_destroy")] 11 | public static extern void KeysDestroy(nint eventHandle, uint length); 12 | 13 | [DllImport( 14 | ChannelSettings.NativeLib, 15 | CallingConvention = CallingConvention.Cdecl, 16 | EntryPoint = "yevent_delta_destroy")] 17 | public static extern void DeltaDestroy(nint eventHandle, uint length); 18 | } 19 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Arrays/CreateTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | 4 | namespace YDotNet.Tests.Unit.Arrays; 5 | 6 | public class CreateTests 7 | { 8 | [Test] 9 | public void Create() 10 | { 11 | // Arrange 12 | var doc = new Doc(); 13 | 14 | // Act 15 | var array = doc.Array("array"); 16 | var transaction = doc.ReadTransaction(); 17 | var length = array.Length(transaction); 18 | transaction.Commit(); 19 | 20 | // Assert 21 | Assert.That(array.Handle, Is.GreaterThan(nint.Zero)); 22 | Assert.That(length, Is.EqualTo(expected: 0)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /YDotNet/Document/Events/EventManager.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Document.Events; 2 | 3 | internal sealed class EventManager 4 | { 5 | private readonly HashSet activeSubscribers = new(); 6 | 7 | public void Register(IEventSubscriber eventSubscriber) 8 | { 9 | activeSubscribers.Add(eventSubscriber); 10 | } 11 | 12 | public void Unregister(IEventSubscriber eventSubscriber) 13 | { 14 | activeSubscribers.Remove(eventSubscriber); 15 | } 16 | 17 | public void Clear() 18 | { 19 | foreach (var subscriber in activeSubscribers) 20 | { 21 | subscriber.Clear(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /YDotNet/Native/Types/Maps/MapEntryNative.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using YDotNet.Infrastructure; 3 | using YDotNet.Native.Cells.Outputs; 4 | 5 | namespace YDotNet.Native.Types.Maps; 6 | 7 | [StructLayout(LayoutKind.Sequential, Size = Size)] 8 | internal readonly struct MapEntryNative 9 | { 10 | private const int Size = 8 + OutputNative.Size; 11 | 12 | internal nint KeyHandle { get; } 13 | 14 | public nint ValueHandle(nint baseHandle) 15 | { 16 | return baseHandle + MemoryConstants.PointerSize; 17 | } 18 | 19 | public string Key() 20 | { 21 | return MemoryReader.ReadUtf8String(KeyHandle); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Demo/Client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "node", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "noEmit": true, 14 | "jsx": "react-jsx", 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true 21 | }, 22 | "include": ["src"], 23 | "references": [{ "path": "./tsconfig.node.json" }] 24 | } 25 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Branches/XmlTextObserveDeepTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace YDotNet.Tests.Unit.Branches; 4 | 5 | [TestFixture] 6 | [Ignore("The feature under test does not exist on Yrs yet.")] 7 | public class XmlTextObserveDeepTests 8 | { 9 | [Test] 10 | public void ObserveDeepHasPathWhenAddedTextsAndEmbeds() 11 | { 12 | } 13 | 14 | [Test] 15 | public void ObserveDeepHasPathWhenAddedAttributes() 16 | { 17 | } 18 | 19 | [Test] 20 | public void ObserveDeepHasPathWhenUpdatedAttributes() 21 | { 22 | } 23 | 24 | [Test] 25 | public void ObserveDeepHasPathWhenRemovedAttributes() 26 | { 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /YDotNet/Document/Events/UpdateEvent.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Native.Document.Events; 2 | 3 | namespace YDotNet.Document.Events; 4 | 5 | /// 6 | /// An update event passed to a callback subscribed to or 7 | /// . 8 | /// 9 | public class UpdateEvent 10 | { 11 | internal UpdateEvent(UpdateEventNative native) 12 | { 13 | Update = native.Bytes(); 14 | } 15 | 16 | /// 17 | /// Gets the binary information about all inserted and deleted changes performed within the scope of its transaction. 18 | /// 19 | public byte[] Update { get; } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/XmlElements/CreateTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | 4 | namespace YDotNet.Tests.Unit.XmlElements; 5 | 6 | public class CreateTests 7 | { 8 | [Test] 9 | public void Create() 10 | { 11 | // Arrange 12 | var doc = new Doc(); 13 | var xmlFragment = doc.XmlFragment("xml-fragment"); 14 | 15 | // Act 16 | var transaction = doc.WriteTransaction(); 17 | var xmlElement = xmlFragment.InsertElement(transaction, index: 0, "xml-element"); 18 | transaction.Commit(); 19 | 20 | // Assert 21 | Assert.That(xmlElement.Handle, Is.GreaterThan(nint.Zero)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /YDotNet/Document/Types/Events/EventKeyChangeTag.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Document.Types.Events; 2 | 3 | /// 4 | /// Represents the tags to identify the kind of operation done within the parent instance under a certain key. 5 | /// 6 | public enum EventKeyChangeTag 7 | { 8 | /// 9 | /// Represents that the value under this key was added. 10 | /// 11 | Add, 12 | 13 | /// 14 | /// Represents that the value under this key was removed. 15 | /// 16 | Remove, 17 | 18 | /// 19 | /// Represents that the value under this key was updated. 20 | /// 21 | Update, 22 | } 23 | -------------------------------------------------------------------------------- /YDotNet/Infrastructure/ClientIdGenerator.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Infrastructure; 2 | 3 | /// 4 | /// Helper class to deal with client ids. 5 | /// 6 | public static class ClientIdGenerator 7 | { 8 | /// 9 | /// The maximum safe integer from javascript. 10 | /// 11 | public const ulong MaxSafeInteger = 2 ^ (53 - 1); 12 | 13 | /// 14 | /// Gets a random client id. 15 | /// 16 | /// The random client id. 17 | public static ulong Random() 18 | { 19 | var value = (ulong)System.Random.Shared.Next() & MaxSafeInteger; 20 | 21 | return value; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /YDotNet/Native/UndoManager/Events/UndoEventNative.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using YDotNet.Infrastructure; 3 | 4 | namespace YDotNet.Native.UndoManager.Events; 5 | 6 | [StructLayout(LayoutKind.Sequential)] 7 | internal struct UndoEventNative 8 | { 9 | public UndoEventKindNative KindNative { get; set; } 10 | 11 | public nint OriginHandle { get; set; } 12 | 13 | public uint OriginLength { get; set; } 14 | 15 | public byte[]? Origin() 16 | { 17 | if (OriginHandle == nint.Zero || OriginLength <= 0) 18 | { 19 | return null; 20 | } 21 | 22 | return MemoryReader.ReadBytes(OriginHandle, OriginLength); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /YDotNet/Native/Document/State/StateVectorNative.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using YDotNet.Infrastructure; 3 | 4 | namespace YDotNet.Native.Document.State; 5 | 6 | [StructLayout(LayoutKind.Sequential)] 7 | internal readonly struct StateVectorNative 8 | { 9 | public uint EntriesCount { get; } 10 | 11 | public nint ClientIdsHandle { get; } 12 | 13 | public nint ClocksHandle { get; } 14 | 15 | public ulong[] ClientIds() 16 | { 17 | return MemoryReader.ReadStructs(ClientIdsHandle, EntriesCount); 18 | } 19 | 20 | public uint[] Clocks() 21 | { 22 | return MemoryReader.ReadStructs(ClientIdsHandle, EntriesCount); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.*~ 3 | project.lock.json 4 | .DS_Store 5 | *.pyc 6 | nupkg/ 7 | 8 | # Visual Studio Code 9 | .vscode 10 | 11 | # Rider 12 | .idea 13 | 14 | # User-specific files 15 | *.suo 16 | *.user 17 | *.userosscache 18 | *.sln.docstates 19 | 20 | # Build results 21 | [Dd]ebug/ 22 | [Dd]ebugPublic/ 23 | [Rr]elease/ 24 | [Rr]eleases/ 25 | x64/ 26 | x86/ 27 | build/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Oo]ut/ 32 | [Oo]utput/ 33 | msbuild.log 34 | msbuild.err 35 | msbuild.wrn 36 | 37 | # Visual Studio 2015 38 | .vs/ 39 | 40 | # Project-specific files 41 | *.dll 42 | *.dylib 43 | 44 | # Node 45 | node_modules 46 | 47 | apSettings.Development.json 48 | 49 | launchSettings.json 50 | 51 | .env -------------------------------------------------------------------------------- /YDotNet.Server/Storage/InMemoryDocumentStorage.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | 3 | namespace YDotNet.Server.Storage; 4 | 5 | public sealed class InMemoryDocumentStorage : IDocumentStorage 6 | { 7 | private readonly ConcurrentDictionary docs = new(StringComparer.Ordinal); 8 | 9 | public ValueTask GetDocAsync(string name, CancellationToken ct = default) 10 | { 11 | docs.TryGetValue(name, out var doc); 12 | 13 | return new ValueTask(doc); 14 | } 15 | 16 | public ValueTask StoreDocAsync(string name, byte[] doc, CancellationToken ct = default) 17 | { 18 | docs[name] = doc; 19 | 20 | return default; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /YDotNet/Native/Document/State/DeleteSetNative.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using YDotNet.Infrastructure; 3 | 4 | namespace YDotNet.Native.Document.State; 5 | 6 | [StructLayout(LayoutKind.Sequential)] 7 | internal readonly struct DeleteSetNative 8 | { 9 | public uint EntriesCount { get; } 10 | 11 | public nint ClientIdsHandle { get; } 12 | 13 | public nint RangesHandle { get; } 14 | 15 | public ulong[] Clients() 16 | { 17 | return MemoryReader.ReadStructs(ClientIdsHandle, EntriesCount); 18 | } 19 | 20 | public IdRangeSequenceNative[] Ranges() 21 | { 22 | return MemoryReader.ReadStructs(RangesHandle, EntriesCount); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /YDotNet.Server/IDocumentCallback.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Server; 2 | 3 | public interface IDocumentCallback 4 | { 5 | ValueTask OnInitializedAsync(IDocumentManager manager) 6 | { 7 | return default; 8 | } 9 | 10 | ValueTask OnDocumentLoadedAsync(DocumentLoadEvent @event) 11 | { 12 | return default; 13 | } 14 | 15 | ValueTask OnDocumentChangedAsync(DocumentChangedEvent @event) 16 | { 17 | return default; 18 | } 19 | 20 | ValueTask OnClientDisconnectedAsync(ClientDisconnectedEvent @event) 21 | { 22 | return default; 23 | } 24 | 25 | ValueTask OnAwarenessUpdatedAsync(ClientAwarenessEvent @event) 26 | { 27 | return default; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | // ACTION REQUIRED: This file was automatically added to your project, but it 3 | // will not take effect until additional steps are taken to enable it. See the 4 | // following page for additional information: 5 | // 6 | // https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/EnableConfiguration.md 7 | 8 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 9 | "settings": { 10 | "orderingRules": { 11 | "usingDirectivesPlacement": "outsideNamespace" 12 | }, 13 | "documentationRules": { 14 | "companyName": "PlaceholderCompany" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /YDotNet.Server.Redis/Internal/LoggerTextWriter.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace YDotNet.Server.Redis.Internal; 5 | 6 | internal sealed class LoggerTextWriter(ILogger log) : TextWriter 7 | { 8 | public override Encoding Encoding => Encoding.UTF8; 9 | 10 | public override void Write(char value) 11 | { 12 | } 13 | 14 | public override void WriteLine(string? value) 15 | { 16 | if (log.IsEnabled(LogLevel.Debug)) 17 | { 18 | #pragma warning disable CA2254 // Template should be a static expression 19 | log.LogDebug(new EventId(100, "RedisConnectionLog"), value); 20 | #pragma warning restore CA2254 // Template should be a static expression 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /YDotNet/Native/Types/Events/EventChangeNative.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using YDotNet.Infrastructure; 3 | using YDotNet.Native.Cells.Outputs; 4 | 5 | namespace YDotNet.Native.Types.Events; 6 | 7 | [StructLayout(LayoutKind.Sequential)] 8 | internal readonly struct EventChangeNative 9 | { 10 | public EventChangeTagNative TagNative { get; } 11 | 12 | public uint Length { get; } 13 | 14 | public nint Values { get; } 15 | 16 | public nint[] ValuesHandles 17 | { 18 | get 19 | { 20 | if (Values == nint.Zero) 21 | { 22 | return Array.Empty(); 23 | } 24 | 25 | return MemoryReader.ReadPointers(Values, Length).ToArray(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /native/YDotNet.Native/YDotNet.Native.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Transactions/WriteableTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | 4 | namespace YDotNet.Tests.Unit.Transactions; 5 | 6 | public class WriteableTests 7 | { 8 | [Test] 9 | public void ReadOnly() 10 | { 11 | // Arrange 12 | var doc = new Doc(); 13 | 14 | // Act 15 | var transaction = doc.ReadTransaction(); 16 | 17 | // Assert 18 | Assert.That(transaction.Writeable, Is.False); 19 | } 20 | 21 | [Test] 22 | public void ReadWrite() 23 | { 24 | // Arrange 25 | var doc = new Doc(); 26 | 27 | // Act 28 | var transaction = doc.WriteTransaction(); 29 | 30 | // Assert 31 | Assert.That(transaction.Writeable, Is.True); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /YDotNet/Document/Types/Events/EventDeltaTag.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Document.Types.Events; 2 | 3 | /// 4 | /// Represents the type of change represented by the parent instance. 5 | /// 6 | public enum EventDeltaTag 7 | { 8 | /// 9 | /// Represents the addition of content. 10 | /// 11 | /// 12 | /// In this case, the value of . will not be null. 13 | /// 14 | Add, 15 | 16 | /// 17 | /// Represents the removal of content. 18 | /// 19 | Remove, 20 | 21 | /// 22 | /// Represents the update of content. 23 | /// 24 | Retain, 25 | } 26 | -------------------------------------------------------------------------------- /YDotNet/Document/Types/Events/EventChangeTag.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Document.Types.Events; 2 | 3 | /// 4 | /// Represents the type of change represented by the parent instance. 5 | /// 6 | public enum EventChangeTag 7 | { 8 | /// 9 | /// Represents the addition of content. 10 | /// 11 | /// 12 | /// In this case, the value of . will not be null. 13 | /// 14 | Add, 15 | 16 | /// 17 | /// Represents the removal of content. 18 | /// 19 | Remove, 20 | 21 | /// 22 | /// Represents the update of content. 23 | /// 24 | Retain, 25 | } 26 | -------------------------------------------------------------------------------- /native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Demo/Client/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | 'plugin:prettier/recommended' 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parser: '@typescript-eslint/parser', 12 | plugins: ['react-refresh'], 13 | rules: { 14 | 'prettier/prettier': [ 15 | 'error', 16 | { 17 | endOfLine: 'auto', 18 | }, 19 | ], 20 | 'react-refresh/only-export-components': [ 21 | 'warn', 22 | { allowConstantExport: true }, 23 | ], 24 | '@typescript-eslint/semi': 'error', 25 | '@typescript-eslint/indent': [ 26 | 'warn', 27 | 4 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Demo/Db/DbInitializer.cs: -------------------------------------------------------------------------------- 1 | 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Storage; 5 | 6 | namespace Demo.Db; 7 | 8 | public class DbInitializer(IDbContextFactory dbContextFactory) : IHostedService 9 | { 10 | public async Task StartAsync(CancellationToken cancellationToken) 11 | { 12 | await using var context = await dbContextFactory.CreateDbContextAsync(cancellationToken); 13 | 14 | var creator = (RelationalDatabaseCreator)context.Database.GetService(); 15 | await creator.EnsureCreatedAsync(cancellationToken); 16 | } 17 | 18 | public Task StopAsync(CancellationToken cancellationToken) 19 | { 20 | return Task.CompletedTask; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /YDotNet/Document/Options/DocEncoding.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document.Types.Texts; 2 | using YDotNet.Document.Types.XmlTexts; 3 | 4 | namespace YDotNet.Document.Options; 5 | 6 | /// 7 | /// Determines how string length and offsets are calculated for and . 8 | /// 9 | public enum DocEncoding 10 | { 11 | /// 12 | /// Compute editable strings length and offset using UTF-8 byte count. 13 | /// 14 | Utf8 = 0, 15 | 16 | /// 17 | /// Compute editable strings length and offset using UTF-16 chars count. 18 | /// 19 | Utf16 = 1, 20 | 21 | /// 22 | /// Compute editable strings length and offset using UTF-32 (Unicode) code points number. 23 | /// 24 | Utf32 = 2, 25 | } 26 | -------------------------------------------------------------------------------- /YDotNet/Document/UndoManagers/UndoManagerOptions.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Native.UndoManager; 2 | 3 | namespace YDotNet.Document.UndoManagers; 4 | 5 | /// 6 | /// Represents the set of options used to configure . 7 | /// 8 | public class UndoManagerOptions 9 | { 10 | /// 11 | /// Gets the time interval used to capture snapshots. 12 | /// 13 | /// 14 | /// The updates are grouped together in time-constrained snapshots. 15 | /// 16 | public uint CaptureTimeoutMilliseconds { get; init; } 17 | 18 | internal UndoManagerOptionsNative ToNative() 19 | { 20 | return new UndoManagerOptionsNative 21 | { 22 | CaptureTimeoutMilliseconds = CaptureTimeoutMilliseconds, 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /YDotNet.Server.Redis/RedisOptions.cs: -------------------------------------------------------------------------------- 1 | using StackExchange.Redis; 2 | 3 | namespace YDotNet.Server.Redis; 4 | 5 | public sealed class RedisOptions 6 | { 7 | public ConfigurationOptions? Configuration { get; set; } 8 | 9 | public Func>? ConnectionFactory { get; set; } 10 | 11 | internal async Task ConnectAsync(TextWriter log) 12 | { 13 | if (ConnectionFactory != null) 14 | { 15 | return await ConnectionFactory(log).ConfigureAwait(false); 16 | } 17 | 18 | if (Configuration != null) 19 | { 20 | return await ConnectionMultiplexer.ConnectAsync(Configuration, log).ConfigureAwait(false); 21 | } 22 | 23 | throw new InvalidOperationException("Either configuration or connection factory must be set."); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /YDotNet/Native/Types/XmlAttributeChannel.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace YDotNet.Native.Types; 4 | 5 | internal static class XmlAttributeChannel 6 | { 7 | [DllImport( 8 | ChannelSettings.NativeLib, 9 | CallingConvention = CallingConvention.Cdecl, 10 | EntryPoint = "yxmlattr_iter_next")] 11 | public static extern nint IteratorNext(nint handle); 12 | 13 | [DllImport( 14 | ChannelSettings.NativeLib, 15 | CallingConvention = CallingConvention.Cdecl, 16 | EntryPoint = "yxmlattr_iter_destroy")] 17 | public static extern nint IteratorDestroy(nint handle); 18 | 19 | [DllImport( 20 | ChannelSettings.NativeLib, 21 | CallingConvention = CallingConvention.Cdecl, 22 | EntryPoint = "yxmlattr_destroy")] 23 | public static extern nint Destroy(nint handle); 24 | } 25 | -------------------------------------------------------------------------------- /YDotNet.Server.MongoDB/ServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Server.MongoDB; 2 | 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Hosting; 5 | using YDotNet.Server.Storage; 6 | 7 | public static class ServiceExtensions 8 | { 9 | public static YDotnetRegistration AddMongoStorage(this YDotnetRegistration registration, Action? configure = null) 10 | { 11 | registration.Services.Configure(configure ?? (x => { })); 12 | registration.Services.AddSingleton(); 13 | 14 | registration.Services.AddSingleton( 15 | c => c.GetRequiredService()); 16 | 17 | registration.Services.AddSingleton( 18 | c => c.GetRequiredService()); 19 | 20 | return registration; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Document/GuidTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | using YDotNet.Document.Options; 4 | 5 | namespace YDotNet.Tests.Unit.Document; 6 | 7 | public class GuidTests 8 | { 9 | [Test] 10 | public void RandomGuidByDefault() 11 | { 12 | // Arrange 13 | var doc = new Doc(); 14 | 15 | // Act 16 | var guid = doc.Guid; 17 | 18 | // Assert 19 | Assert.That(guid, Is.Not.EqualTo(string.Empty)); 20 | } 21 | 22 | [Test] 23 | public void SpecialCharacters() 24 | { 25 | // Arrange 26 | var doc = new Doc( 27 | new DocOptions 28 | { 29 | Guid = "shark-🦈" 30 | }); 31 | 32 | // Act 33 | var guid = doc.Guid; 34 | 35 | // Assert 36 | Assert.That(guid, Is.EqualTo("shark-🦈")); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /YDotNet/Protocol/BufferEncoder.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Protocol; 2 | 3 | /// 4 | /// Write to a buffer. 5 | /// 6 | public sealed class BufferEncoder : Encoder 7 | { 8 | private readonly MemoryStream buffer = new(); 9 | 10 | /// 11 | /// Gets the content of the buffer. 12 | /// 13 | /// The content of the buffer. 14 | public byte[] ToArray() 15 | { 16 | return buffer.ToArray(); 17 | } 18 | 19 | /// 20 | protected override ValueTask WriteByteAsync(byte value, CancellationToken ct) 21 | { 22 | buffer.WriteByte(value); 23 | return default; 24 | } 25 | 26 | /// 27 | protected override ValueTask WriteBytesAsync(ArraySegment bytes, CancellationToken ct) 28 | { 29 | buffer.Write(bytes); 30 | return default; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /YDotNet.Server.Redis/ServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Server.Redis; 2 | using YDotNet.Server.Storage; 3 | 4 | namespace Microsoft.Extensions.DependencyInjection; 5 | 6 | public static class ServiceExtensions 7 | { 8 | public static YDotnetRegistration AddRedis(this YDotnetRegistration registration, Action? configure = null) 9 | { 10 | registration.Services.Configure(configure ?? (x => { })); 11 | registration.Services.AddSingleton(); 12 | 13 | return registration; 14 | } 15 | 16 | public static YDotnetRegistration AddRedisStorage(this YDotnetRegistration registration, Action? configure = null) 17 | { 18 | registration.Services.Configure(configure ?? (x => { })); 19 | registration.Services.AddSingleton(); 20 | 21 | return registration; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Demo/Client/src/pages/Chat.tsx: -------------------------------------------------------------------------------- 1 | import { Col, Row } from 'reactstrap'; 2 | import { Awareness } from '../components/Awareness'; 3 | import { Chat } from '../components/Chat'; 4 | import { YjsContextProvider } from '../context/yjsContext'; 5 | 6 | function ChatPage() { 7 | return ( 8 | 9 | 10 | 11 | Chat 12 | 13 | 14 | 15 | 16 | Notifications 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | } 28 | 29 | export default ChatPage; 30 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Document/CollectionIdTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | using YDotNet.Document.Options; 4 | 5 | namespace YDotNet.Tests.Unit.Document; 6 | 7 | public class CollectionIdTests 8 | { 9 | [Test] 10 | public void NullByDefault() 11 | { 12 | // Arrange 13 | var doc = new Doc(); 14 | 15 | // Act 16 | var collectionId = doc.CollectionId; 17 | 18 | // Assert 19 | Assert.That(collectionId, Is.Null); 20 | } 21 | 22 | [Test] 23 | public void SpecialCharacters() 24 | { 25 | // Arrange 26 | var doc = new Doc( 27 | new DocOptions 28 | { 29 | CollectionId = "dragon-🐲" 30 | }); 31 | 32 | // Act 33 | var collectionId = doc.CollectionId; 34 | 35 | // Assert 36 | Assert.That(collectionId, Is.EqualTo("dragon-🐲")); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /YDotNet/Document/State/DeleteSet.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Native.Document.State; 2 | 3 | namespace YDotNet.Document.State; 4 | 5 | /// 6 | /// Represents the deleted changes in a . 7 | /// 8 | public class DeleteSet 9 | { 10 | internal DeleteSet(DeleteSetNative native) 11 | { 12 | var allClients = native.Clients(); 13 | var allRanges = native.Ranges(); 14 | 15 | var ranges = new Dictionary(); 16 | 17 | for (var i = 0; i < native.EntriesCount; i++) 18 | { 19 | ranges.Add(allClients[i], allRanges[i].Sequence().Select(IdRange.Create).ToArray()); 20 | } 21 | 22 | Ranges = ranges; 23 | } 24 | 25 | /// 26 | /// Gets dictionary of unique client identifiers (keys) by their deleted ID ranges (values). 27 | /// 28 | public IReadOnlyDictionary Ranges { get; } 29 | } 30 | -------------------------------------------------------------------------------- /YDotNet/Document/Events/EventPublisher.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Document.Events; 2 | 3 | internal sealed class EventPublisher 4 | { 5 | private readonly HashSet> subscriptions = new(); 6 | 7 | public int Count => subscriptions.Count; 8 | 9 | public void Clear() 10 | { 11 | subscriptions.Clear(); 12 | } 13 | 14 | public void Subscribe(Action handler) 15 | { 16 | subscriptions.Add(handler); 17 | } 18 | 19 | public void Unsubscribe(Action handler) 20 | { 21 | subscriptions.Remove(handler); 22 | } 23 | 24 | public void Publish(TEvent @event) 25 | { 26 | foreach (var subscription in subscriptions) 27 | { 28 | try 29 | { 30 | subscription(@event); 31 | } 32 | catch 33 | { 34 | // Exceptions could have unknown consequences in the Rust part. 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /YDotNet/Native/Types/Events/EventDeltaNative.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using YDotNet.Infrastructure; 3 | using YDotNet.Native.Types.Maps; 4 | 5 | namespace YDotNet.Native.Types.Events; 6 | 7 | [StructLayout(LayoutKind.Sequential)] 8 | internal readonly struct EventDeltaNative 9 | { 10 | public EventDeltaTagNative TagNative { get; } 11 | 12 | public uint Length { get; } 13 | 14 | public nint InsertHandle { get; } 15 | 16 | public uint AttributesLength { get; } 17 | 18 | public nint AttributesHandle { get; } 19 | 20 | public NativeWithHandle[] Attributes 21 | { 22 | get 23 | { 24 | if (AttributesHandle == nint.Zero || AttributesLength == 0) 25 | { 26 | return Array.Empty>(); 27 | } 28 | 29 | return MemoryReader.ReadStructsWithHandles(AttributesHandle, AttributesLength).ToArray(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | lsviana, sebastianstehle, goldsam 4 | MIT 5 | YDotNet provides cross-platform .NET bindings for Yrs (Rust port of Yjs). 6 | true 7 | true 8 | logo-dotnet.png 9 | MIT 10 | https://github.com/LSViana/YDotNet 11 | README.md 12 | true 13 | snupkg 14 | 0.5.0 15 | 16 | 17 | 18 | true 19 | 20 | 21 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Texts/UnobserveTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | 4 | namespace YDotNet.Tests.Unit.Texts; 5 | 6 | public class UnobserveTests 7 | { 8 | [Test] 9 | public void TriggersWhenTextChangedUntilUnobserved() 10 | { 11 | // Arrange 12 | var doc = new Doc(); 13 | var text = doc.Text("value"); 14 | var called = 0; 15 | var subscription = text.Observe(_ => called++); 16 | 17 | // Act 18 | var transaction = doc.WriteTransaction(); 19 | text.Insert(transaction, index: 0, "World"); 20 | transaction.Commit(); 21 | 22 | // Assert 23 | Assert.That(called, Is.EqualTo(expected: 1)); 24 | 25 | // Act 26 | subscription.Dispose(); 27 | 28 | transaction = doc.WriteTransaction(); 29 | text.Insert(transaction, index: 0, "Hello, "); 30 | transaction.Commit(); 31 | 32 | // Assert 33 | Assert.That(called, Is.EqualTo(expected: 1)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /YDotNet/Document/Cells/JsonArray.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using YDotNet.Infrastructure; 3 | using YDotNet.Native.Cells.Outputs; 4 | 5 | namespace YDotNet.Document.Cells; 6 | 7 | /// 8 | /// Represents a JSON array. 9 | /// 10 | public sealed class JsonArray : ReadOnlyCollection 11 | { 12 | internal JsonArray(nint handle, uint length, Doc doc, bool isDeleted) 13 | : base(ReadItems(handle, length, doc, isDeleted)) 14 | { 15 | } 16 | 17 | private static List ReadItems(nint handle, uint length, Doc doc, bool isDeleted) 18 | { 19 | var collectionHandle = OutputChannel.Collection(handle); 20 | var collectionNatives = MemoryReader.ReadPointers(collectionHandle, length); 21 | 22 | var result = new List(); 23 | 24 | foreach (var itemHandle in collectionNatives) 25 | { 26 | result.Add(new Output(itemHandle, doc, isDeleted)); 27 | } 28 | 29 | return result; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /YDotNet/Document/State/StateVector.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Native.Document.State; 2 | 3 | namespace YDotNet.Document.State; 4 | 5 | /// 6 | /// Represents the state of a . 7 | /// 8 | /// 9 | /// It contains the last seen clocks for blocks submitted per any of the clients collaborating on document updates. 10 | /// 11 | public class StateVector 12 | { 13 | internal StateVector(StateVectorNative native) 14 | { 15 | var allClientIds = native.ClientIds(); 16 | var allClocks = native.Clocks(); 17 | 18 | var state = new Dictionary(); 19 | 20 | for (var i = 0; i < native.EntriesCount; i++) 21 | { 22 | state.Add(allClientIds[i], allClocks[i]); 23 | } 24 | 25 | State = state; 26 | } 27 | 28 | /// 29 | /// Gets dictionary of unique client identifiers (keys) by their clocks (values). 30 | /// 31 | public IReadOnlyDictionary State { get; } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Branches/TextUnobserveDeepTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | 4 | namespace YDotNet.Tests.Unit.Branches; 5 | 6 | public class TextUnobserveDeepTests 7 | { 8 | [Test] 9 | public void TriggersWhenChangedUntilUnobserved() 10 | { 11 | // Arrange 12 | var doc = new Doc(); 13 | var text = doc.Text("text"); 14 | var called = 0; 15 | var subscription = text.ObserveDeep(_ => called++); 16 | 17 | // Act 18 | var transaction = doc.WriteTransaction(); 19 | text.Insert(transaction, index: 0, "World"); 20 | transaction.Commit(); 21 | 22 | // Assert 23 | Assert.That(called, Is.EqualTo(expected: 1)); 24 | 25 | // Act 26 | subscription.Dispose(); 27 | 28 | transaction = doc.WriteTransaction(); 29 | text.Insert(transaction, index: 0, "Hello, "); 30 | transaction.Commit(); 31 | 32 | // Assert 33 | Assert.That(called, Is.EqualTo(expected: 1)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /YDotNet.Server.WebSockets/ServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.Extensions.Hosting; 3 | using YDotNet.Server; 4 | using YDotNet.Server.WebSockets; 5 | 6 | namespace Microsoft.Extensions.DependencyInjection; 7 | 8 | public static class ServiceExtensions 9 | { 10 | public static YDotnetRegistration AddWebSockets(this YDotnetRegistration registration, Action? configure = null) 11 | { 12 | registration.Services.Configure(configure ?? (x => { })); 13 | registration.Services.AddSingleton(); 14 | 15 | registration.Services.AddSingleton(x => 16 | x.GetRequiredService()); 17 | 18 | return registration; 19 | } 20 | 21 | public static void UseYDotnetWebSockets(this IApplicationBuilder app) 22 | { 23 | var middleware = app.ApplicationServices.GetRequiredService(); 24 | 25 | app.Run(middleware.InvokeAsync); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /YDotNet.Server.WebSockets/WebSocketEncoder.cs: -------------------------------------------------------------------------------- 1 | using System.Net.WebSockets; 2 | using YDotNet.Protocol; 3 | 4 | namespace YDotNet.Server.WebSockets; 5 | 6 | public sealed class WebSocketEncoder(WebSocket webSocket) : Encoder, IDisposable 7 | { 8 | private readonly byte[] buffer = new byte[1]; 9 | 10 | public void Dispose() 11 | { 12 | } 13 | 14 | public override ValueTask FlushAsync(CancellationToken ct = default) 15 | { 16 | return new ValueTask(webSocket.SendAsync(Array.Empty(), WebSocketMessageType.Binary, true, ct)); 17 | } 18 | 19 | protected override ValueTask WriteByteAsync(byte value, CancellationToken ct) 20 | { 21 | buffer[0] = value; 22 | 23 | return new ValueTask(webSocket.SendAsync(buffer, WebSocketMessageType.Binary, false, ct)); 24 | } 25 | 26 | protected override ValueTask WriteBytesAsync(ArraySegment bytes, CancellationToken ct) 27 | { 28 | return new ValueTask(webSocket.SendAsync(bytes, WebSocketMessageType.Binary, false, ct)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /YDotNet/Document/Types/Events/EventChanges.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using YDotNet.Infrastructure; 3 | using YDotNet.Native.Types; 4 | using YDotNet.Native.Types.Events; 5 | 6 | namespace YDotNet.Document.Types.Events; 7 | 8 | /// 9 | /// Represents a collection of instances. 10 | /// 11 | public class EventChanges : ReadOnlyCollection 12 | { 13 | internal EventChanges(nint handle, uint length, Doc doc) 14 | : base(ReadItems(handle, length, doc)) 15 | { 16 | } 17 | 18 | private static IList ReadItems(nint handle, uint length, Doc doc) 19 | { 20 | var result = new List(); 21 | 22 | foreach (var native in MemoryReader.ReadStructs(handle, length)) 23 | { 24 | result.Add(new EventChange(native, doc)); 25 | } 26 | 27 | // We are done reading and can destroy the resource. 28 | EventChannel.DeltaDestroy(handle, length); 29 | 30 | return result; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /YDotNet/Document/Types/Texts/TextChunks.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using YDotNet.Infrastructure; 3 | using YDotNet.Native.Types; 4 | using YDotNet.Native.Types.Texts; 5 | 6 | namespace YDotNet.Document.Types.Texts; 7 | 8 | /// 9 | /// Represents a collection of instances. 10 | /// 11 | public class TextChunks : ReadOnlyCollection 12 | { 13 | internal TextChunks(nint handle, uint length, Doc doc) 14 | : base(ReadItems(handle, length, doc)) 15 | { 16 | } 17 | 18 | private static IList ReadItems(nint handle, uint length, Doc doc) 19 | { 20 | var result = new List((int)length); 21 | 22 | foreach (var native in MemoryReader.ReadStructsWithHandles(handle, length)) 23 | { 24 | result.Add(new TextChunk(native, doc)); 25 | } 26 | 27 | // We are done reading and can destroy the resource. 28 | ChunksChannel.Destroy(handle, length); 29 | 30 | return result; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/XmlFragments/UnobserveTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | 4 | namespace YDotNet.Tests.Unit.XmlFragments; 5 | 6 | public class UnobserveTests 7 | { 8 | [Test] 9 | public void TriggersWhenXmlElementChangedUntilUnobserved() 10 | { 11 | // Arrange 12 | var doc = new Doc(); 13 | var xmlFragment = doc.XmlFragment("xml-fragment"); 14 | 15 | var called = 0; 16 | var subscription = xmlFragment.Observe(_ => called++); 17 | 18 | // Act 19 | var transaction = doc.WriteTransaction(); 20 | xmlFragment.InsertText(transaction, index: 0); 21 | transaction.Commit(); 22 | 23 | // Assert 24 | Assert.That(called, Is.EqualTo(expected: 1)); 25 | 26 | // Act 27 | subscription.Dispose(); 28 | 29 | transaction = doc.WriteTransaction(); 30 | xmlFragment.InsertText(transaction, index: 1); 31 | transaction.Commit(); 32 | 33 | // Assert 34 | Assert.That(called, Is.EqualTo(expected: 1)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /YDotNet/Document/Types/Events/EventDeltas.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using YDotNet.Infrastructure; 3 | using YDotNet.Native.Types; 4 | using YDotNet.Native.Types.Events; 5 | 6 | namespace YDotNet.Document.Types.Events; 7 | 8 | /// 9 | /// Represents a collection of instances. 10 | /// 11 | public class EventDeltas : ReadOnlyCollection 12 | { 13 | internal EventDeltas(nint handle, uint length, Doc doc) 14 | : base(ReadItems(handle, length, doc)) 15 | { 16 | } 17 | 18 | private static IList ReadItems(nint handle, uint length, Doc doc) 19 | { 20 | var result = new List((int)length); 21 | 22 | foreach (var native in MemoryReader.ReadStructs(handle, length)) 23 | { 24 | result.Add(new EventDelta(native, doc)); 25 | } 26 | 27 | // We are done reading and can destroy the resource. 28 | EventChannel.DeltaDestroy(handle, length); 29 | 30 | return result; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Maps/UnobserveTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | using YDotNet.Document.Cells; 4 | 5 | namespace YDotNet.Tests.Unit.Maps; 6 | 7 | public class UnobserveTests 8 | { 9 | [Test] 10 | public void TriggersWhenMapChangedUntilUnobserved() 11 | { 12 | // Arrange 13 | var doc = new Doc(); 14 | var map = doc.Map("map"); 15 | var called = 0; 16 | var subscription = map.Observe(_ => called++); 17 | 18 | // Act 19 | var transaction = doc.WriteTransaction(); 20 | map.Insert(transaction, "value1", Input.Long(value: 2469L)); 21 | transaction.Commit(); 22 | 23 | // Assert 24 | Assert.That(called, Is.EqualTo(expected: 1)); 25 | 26 | // Act 27 | subscription.Dispose(); 28 | 29 | transaction = doc.WriteTransaction(); 30 | map.Insert(transaction, "value2", Input.Long(value: -420L)); 31 | transaction.Commit(); 32 | 33 | // Assert 34 | Assert.That(called, Is.EqualTo(expected: 1)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /YDotNet/Native/Types/Texts/TextChunkNative.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using YDotNet.Infrastructure; 3 | using YDotNet.Native.Cells.Outputs; 4 | using YDotNet.Native.Types.Maps; 5 | 6 | namespace YDotNet.Native.Types.Texts; 7 | 8 | [StructLayout(LayoutKind.Explicit, Size = Size)] 9 | internal readonly struct TextChunkNative 10 | { 11 | public const int Size = OutputNative.Size + 8 + 8; 12 | 13 | [field: FieldOffset(offset: 0)] 14 | public OutputNative Data { get; } 15 | 16 | [field: FieldOffset(OutputNative.Size)] 17 | public uint AttributesLength { get; } 18 | 19 | [field: FieldOffset(OutputNative.Size + 8)] 20 | public nint AttributesHandle { get; } 21 | 22 | public NativeWithHandle[] Attributes() 23 | { 24 | if (AttributesHandle == nint.Zero || AttributesLength == 0) 25 | { 26 | return Array.Empty>(); 27 | } 28 | 29 | return MemoryReader.ReadStructsWithHandles(AttributesHandle, AttributesLength).ToArray(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Driver/Tasks/Docs/Create.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document; 2 | using YDotNet.Tests.Driver.Abstractions; 3 | 4 | namespace YDotNet.Tests.Driver.Tasks.Docs; 5 | 6 | public class Create : ITask 7 | { 8 | public Task Run() 9 | { 10 | var count = 0; 11 | 12 | // Create many documents 13 | while (count < 1_000_000) 14 | { 15 | // After 1s, stop and show the user the amount of documents 16 | if (count > 0) 17 | { 18 | Console.WriteLine("Status Report"); 19 | Console.WriteLine($"\tDocuments:\t{count}"); 20 | Console.WriteLine(); 21 | } 22 | 23 | if (count % 1_000 == 0) 24 | { 25 | Thread.Sleep(millisecondsTimeout: 15); 26 | } 27 | 28 | // Create many documents 29 | for (var i = 0; i < 100; i++) 30 | { 31 | new Doc().Dispose(); 32 | count++; 33 | } 34 | } 35 | 36 | return Task.CompletedTask; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/UndoManagers/NewTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | using YDotNet.Document.UndoManagers; 4 | 5 | namespace YDotNet.Tests.Unit.UndoManagers; 6 | 7 | public class NewTests 8 | { 9 | [Test] 10 | public void CreateWithOptions() 11 | { 12 | // Arrange 13 | var doc = new Doc(); 14 | var text = doc.Text("text"); 15 | 16 | // Act 17 | var undoManager = new UndoManager( 18 | doc, text, new UndoManagerOptions 19 | { 20 | CaptureTimeoutMilliseconds = 1000 21 | }); 22 | 23 | // Assert 24 | Assert.That(undoManager.Handle, Is.GreaterThan(nint.Zero)); 25 | } 26 | 27 | [Test] 28 | public void CreateWithoutOptions() 29 | { 30 | // Arrange 31 | var doc = new Doc(); 32 | var text = doc.Text("text"); 33 | 34 | // Act 35 | var undoManager = new UndoManager(doc, text); 36 | 37 | // Assert 38 | Assert.That(undoManager.Handle, Is.GreaterThan(nint.Zero)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Branches/XmlFragmentUnobserveDeepTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | 4 | namespace YDotNet.Tests.Unit.Branches; 5 | 6 | public class XmlFragmentUnobserveDeepTests 7 | { 8 | [Test] 9 | public void TriggersWhenChangedUntilUnobserved() 10 | { 11 | // Arrange 12 | var doc = new Doc(); 13 | var xmlFragment = doc.XmlFragment("xml-fragment"); 14 | 15 | var called = 0; 16 | var subscription = xmlFragment.ObserveDeep(_ => called++); 17 | 18 | // Act 19 | var transaction = doc.WriteTransaction(); 20 | xmlFragment.InsertText(transaction, index: 0); 21 | transaction.Commit(); 22 | 23 | // Assert 24 | Assert.That(called, Is.EqualTo(expected: 1)); 25 | 26 | // Act 27 | subscription.Dispose(); 28 | 29 | transaction = doc.WriteTransaction(); 30 | xmlFragment.InsertText(transaction, index: 0); 31 | transaction.Commit(); 32 | 33 | // Assert 34 | Assert.That(called, Is.EqualTo(expected: 1)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /YDotNet.Server/IDocumentManager.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | using YDotNet.Document; 3 | 4 | namespace YDotNet.Server; 5 | 6 | public interface IDocumentManager : IHostedService 7 | { 8 | ValueTask PingAsync(DocumentContext context, ulong clock, string? state = null, CancellationToken ct = default); 9 | 10 | ValueTask DisconnectAsync(DocumentContext context, CancellationToken ct = default); 11 | 12 | ValueTask GetUpdateAsync(DocumentContext context, byte[] stateVector, CancellationToken ct = default); 13 | 14 | ValueTask GetStateVectorAsync(DocumentContext context, CancellationToken ct = default); 15 | 16 | ValueTask> GetAwarenessAsync(DocumentContext context, CancellationToken ct = default); 17 | 18 | ValueTask ApplyUpdateAsync(DocumentContext context, byte[] stateDiff, CancellationToken ct = default); 19 | 20 | ValueTask UpdateDocAsync(DocumentContext context, Action action, CancellationToken ct = default); 21 | 22 | ValueTask CleanupAsync(CancellationToken ct = default); 23 | } 24 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Branches/MapUnobserveDeepTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | using YDotNet.Document.Cells; 4 | 5 | namespace YDotNet.Tests.Unit.Branches; 6 | 7 | public class MapUnobserveDeepTests 8 | { 9 | [Test] 10 | public void TriggersWhenMapChangedUntilUnobserved() 11 | { 12 | // Arrange 13 | var doc = new Doc(); 14 | var map = doc.Map("map"); 15 | var called = 0; 16 | var subscription = map.ObserveDeep(_ => called++); 17 | 18 | // Act 19 | var transaction = doc.WriteTransaction(); 20 | map.Insert(transaction, "value1", Input.Long(value: 2469L)); 21 | transaction.Commit(); 22 | 23 | // Assert 24 | Assert.That(called, Is.EqualTo(expected: 1)); 25 | 26 | // Act 27 | subscription.Dispose(); 28 | 29 | transaction = doc.WriteTransaction(); 30 | map.Insert(transaction, "value2", Input.Long(value: -420L)); 31 | transaction.Commit(); 32 | 33 | // Assert 34 | Assert.That(called, Is.EqualTo(expected: 1)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # Authors: 2 | # 3 | # Sam Gold 4 | # Sebastian Stehle 5 | # Lucas Viana 6 | 7 | name: Publish 8 | 9 | on: 10 | push: 11 | tags: 12 | - 'v*.*.*' 13 | 14 | jobs: 15 | build: 16 | uses: ./.github/workflows/build-binaries.yml 17 | 18 | pack-nuget: 19 | needs: build 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v3 25 | 26 | - name: Download artifacts 27 | uses: actions/download-artifact@v4 28 | with: 29 | path: ./output 30 | 31 | - name: NuGet pack 32 | run: | 33 | dotnet pack -c Release 34 | 35 | - name: NuGet publish 36 | run: | 37 | dotnet nuget push **/*.nupkg --source 'https://api.nuget.org/v3/index.json' --skip-duplicate -k ${{ secrets.nuget }} 38 | 39 | - name: Upload artifacts 40 | uses: actions/upload-artifact@v4 41 | with: 42 | path: | 43 | **/*.nupkg 44 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Maps/RemoveAllTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | using YDotNet.Document.Cells; 4 | using YDotNet.Document.Transactions; 5 | using YDotNet.Document.Types.Maps; 6 | 7 | namespace YDotNet.Tests.Unit.Maps; 8 | 9 | public class RemoveAllTests 10 | { 11 | [Test] 12 | public void RemovesAllValues() 13 | { 14 | // Arrange 15 | var (map, transaction) = ArrangeMap(); 16 | 17 | // Assert 18 | Assert.That(map.Length(transaction), Is.EqualTo(expected: 2)); 19 | 20 | // Act 21 | map.RemoveAll(transaction); 22 | 23 | // Assert 24 | Assert.That(map.Length(transaction), Is.EqualTo(expected: 0)); 25 | } 26 | 27 | private (Map, Transaction) ArrangeMap() 28 | { 29 | var doc = new Doc(); 30 | var map = doc.Map("map"); 31 | var transaction = doc.WriteTransaction(); 32 | 33 | map.Insert(transaction, "value1", Input.Long(value: 2469L)); 34 | map.Insert(transaction, "value2", Input.Long(value: -420L)); 35 | 36 | return (map, transaction); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Driver/Tasks/Docs/Clone.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document; 2 | using YDotNet.Tests.Driver.Abstractions; 3 | 4 | namespace YDotNet.Tests.Driver.Tasks.Docs; 5 | 6 | public class Clone : ITask 7 | { 8 | public Task Run() 9 | { 10 | var count = 0; 11 | var doc = new Doc(); 12 | 13 | // Create many documents 14 | while (count < 1_000_000) 15 | { 16 | // After 1s, stop and show the user the amount of documents 17 | if (count > 0) 18 | { 19 | Console.WriteLine("Status Report"); 20 | Console.WriteLine($"\tDocuments:\t{count}"); 21 | Console.WriteLine(); 22 | } 23 | 24 | if (count % 1_000 == 0) 25 | { 26 | Thread.Sleep(millisecondsTimeout: 15); 27 | } 28 | 29 | // Create many documents 30 | for (var i = 0; i < 100; i++) 31 | { 32 | doc.Clone().Dispose(); 33 | count++; 34 | } 35 | } 36 | 37 | return Task.CompletedTask; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Demo/Client/src/pages/Tldraw.tsx: -------------------------------------------------------------------------------- 1 | import { Col, Container, Row } from 'reactstrap'; 2 | import YjsTldrawEditor from '../components/YjsTldrawEditor'; 3 | import { YjsTldrawContextProvider } from '../context/yjsTldrawContext'; 4 | import { useYjs } from '../hooks/useYjs'; 5 | import { YjsContextProvider } from '../context/yjsContext'; 6 | 7 | function TldrawPage() { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | 15 | function Inner() { 16 | const { roomName } = useYjs(); 17 | 18 | return ( 19 | 20 | 21 | 22 | 23 | Tldraw Editor 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | } 33 | 34 | export default TldrawPage; 35 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Arrays/UnobserveTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | using YDotNet.Document.Cells; 4 | 5 | namespace YDotNet.Tests.Unit.Arrays; 6 | 7 | public class UnobserveTests 8 | { 9 | [Test] 10 | public void TriggersWhenArrayChangedUntilUnobserved() 11 | { 12 | // Arrange 13 | var doc = new Doc(); 14 | var array = doc.Array("array"); 15 | var called = 0; 16 | var subscription = array.Observe(_ => called++); 17 | 18 | // Act 19 | var transaction = doc.WriteTransaction(); 20 | array.InsertRange(transaction, index: 0, new[] { Input.Long(value: 2469L) }); 21 | transaction.Commit(); 22 | 23 | // Assert 24 | Assert.That(called, Is.EqualTo(expected: 1)); 25 | 26 | // Act 27 | subscription.Dispose(); 28 | 29 | transaction = doc.WriteTransaction(); 30 | array.InsertRange(transaction, index: 0, new[] { Input.Long(value: -420L) }); 31 | transaction.Commit(); 32 | 33 | // Assert 34 | Assert.That(called, Is.EqualTo(expected: 1)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /YDotNet/Document/Transactions/TransactionUpdateResult.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Document.Transactions; 2 | 3 | /// 4 | /// Represents the result of applying an update to see through a . 5 | /// 6 | public enum TransactionUpdateResult 7 | { 8 | /// 9 | /// The update operation succeeded. 10 | /// 11 | Ok = 0, 12 | 13 | /// 14 | /// Couldn't read data from input stream. 15 | /// 16 | Io = 1, 17 | 18 | /// 19 | /// Decoded variable integer outside of the expected integer size bounds. 20 | /// 21 | IntegerOutOfBounds = 2, 22 | 23 | /// 24 | /// End of stream found when more data was expected. 25 | /// 26 | UnexpectedValue = 3, 27 | 28 | /// 29 | /// Decoded enum tag value was not among known cases. 30 | /// 31 | InvalidJson = 4, 32 | 33 | /// 34 | /// Failure when trying to decode JSON content. 35 | /// 36 | Other = 5, 37 | } 38 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Document/LoadTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | using YDotNet.Document.Cells; 4 | using YDotNet.Document.Options; 5 | 6 | namespace YDotNet.Tests.Unit.Document; 7 | 8 | public class LoadTests 9 | { 10 | [Test] 11 | public void Load() 12 | { 13 | // Arrange 14 | var doc = new Doc(); 15 | var map = doc.Map("sub-docs"); 16 | var subDoc = new Doc( 17 | new DocOptions 18 | { 19 | ShouldLoad = false 20 | }); 21 | 22 | // Assert 23 | Assert.That(subDoc.ShouldLoad, Is.False); 24 | 25 | // Act 26 | var transaction = doc.WriteTransaction(); 27 | map.Insert(transaction, "sub-doc", Input.Doc(subDoc)); 28 | transaction.Commit(); 29 | 30 | // Assert 31 | Assert.That(subDoc.ShouldLoad, Is.False); 32 | 33 | // Act 34 | transaction = doc.WriteTransaction(); 35 | subDoc.Load(transaction); 36 | transaction.Commit(); 37 | 38 | // Assert 39 | Assert.That(doc.ShouldLoad, Is.True); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Driver/Tasks/Docs/ReadId.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document; 2 | using YDotNet.Tests.Driver.Abstractions; 3 | 4 | namespace YDotNet.Tests.Driver.Tasks.Docs; 5 | 6 | public class ReadId : ITask 7 | { 8 | public Task Run() 9 | { 10 | var count = 0; 11 | var doc = new Doc(); 12 | 13 | // Read many times 14 | while (count < 1_000_000) 15 | { 16 | // After 1s, stop and show the user the amount of documents 17 | if (count > 0) 18 | { 19 | Console.WriteLine("Status Report"); 20 | Console.WriteLine($"\tReads:\t{count}"); 21 | Console.WriteLine(); 22 | } 23 | 24 | if (count % 1_000 == 0) 25 | { 26 | Thread.Sleep(millisecondsTimeout: 15); 27 | } 28 | 29 | // Create many documents 30 | for (var i = 0; i < 100; i++) 31 | { 32 | var _ = doc.Id; 33 | count++; 34 | } 35 | } 36 | 37 | doc.Dispose(); 38 | 39 | return Task.CompletedTask; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /YDotNet/Document/UndoManagers/Events/UndoEvent.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Native.UndoManager.Events; 2 | 3 | namespace YDotNet.Document.UndoManagers.Events; 4 | 5 | /// 6 | /// Represents a redo/undo event from an . 7 | /// 8 | public class UndoEvent 9 | { 10 | internal UndoEvent(UndoEventNative native) 11 | { 12 | Origin = native.Origin(); 13 | 14 | Kind = native.KindNative switch 15 | { 16 | UndoEventKindNative.Undo => UndoEventKind.Undo, 17 | UndoEventKindNative.Redo => UndoEventKind.Redo, 18 | _ => throw new NotSupportedException($"The value \"{native.KindNative}\" for {nameof(UndoEventKindNative)} is not supported.") 19 | }; 20 | } 21 | 22 | /// 23 | /// Gets the kind of the event. 24 | /// 25 | public UndoEventKind Kind { get; } 26 | 27 | /// 28 | /// Gets the origin of the event. 29 | /// 30 | /// 31 | /// The is a binary marker. 32 | /// 33 | public byte[]? Origin { get; } 34 | } 35 | -------------------------------------------------------------------------------- /YDotNet/YDotNetException.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace YDotNet; 4 | 5 | /// 6 | /// Represents an YDotNetException. 7 | /// 8 | public class YDotNetException : Exception 9 | { 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | public YDotNetException() 14 | { 15 | } 16 | 17 | /// 18 | /// Initializes a new instance of the class with error message. 19 | /// 20 | /// The error message. 21 | public YDotNetException(string message) 22 | : base(message) 23 | { 24 | } 25 | 26 | /// 27 | /// Initializes a new instance of the class with error message and inner exception. 28 | /// 29 | /// The error message. 30 | /// The inner exception. 31 | public YDotNetException(string message, Exception inner) 32 | : base(message, inner) 33 | { 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Demo/Demo.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | Always 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/UndoManagers/StopTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | using YDotNet.Document.UndoManagers; 4 | 5 | namespace YDotNet.Tests.Unit.UndoManagers; 6 | 7 | public class StopTests 8 | { 9 | [Test] 10 | public void StopSplitsTheCaptureGroup() 11 | { 12 | // Arrange 13 | var doc = new Doc(); 14 | var text = doc.Text("text"); 15 | var undoManager = new UndoManager(doc, text, new UndoManagerOptions { CaptureTimeoutMilliseconds = 500 }); 16 | 17 | // Act 18 | var transaction = doc.WriteTransaction(); 19 | text.Insert(transaction, index: 0, "Lucas"); 20 | transaction.Commit(); 21 | 22 | undoManager.Stop(); 23 | 24 | transaction = doc.WriteTransaction(); 25 | text.Insert(transaction, index: 5, " Viana"); 26 | transaction.Commit(); 27 | 28 | undoManager.Undo(); 29 | 30 | transaction = doc.ReadTransaction(); 31 | var value = text.String(transaction); 32 | transaction.Commit(); 33 | 34 | // Assert 35 | Assert.That(value, Is.EqualTo("Lucas")); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Branches/ArrayUnobserveDeepTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | using YDotNet.Document.Cells; 4 | 5 | namespace YDotNet.Tests.Unit.Branches; 6 | 7 | public class ArrayUnobserveDeepTests 8 | { 9 | [Test] 10 | public void TriggersWhenArrayChangedUntilUnobserved() 11 | { 12 | // Arrange 13 | var doc = new Doc(); 14 | var array = doc.Array("array"); 15 | var called = 0; 16 | var subscription = array.ObserveDeep(_ => called++); 17 | 18 | // Act 19 | var transaction = doc.WriteTransaction(); 20 | array.InsertRange(transaction, index: 0, new[] { Input.Long(value: 2469L) }); 21 | transaction.Commit(); 22 | 23 | // Assert 24 | Assert.That(called, Is.EqualTo(expected: 1)); 25 | 26 | // Act 27 | subscription.Dispose(); 28 | 29 | transaction = doc.WriteTransaction(); 30 | array.InsertRange(transaction, index: 0, new[] { Input.Long(value: -420L) }); 31 | transaction.Commit(); 32 | 33 | // Assert 34 | Assert.That(called, Is.EqualTo(expected: 1)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /YDotNet/Document/Types/Events/EventKeys.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using YDotNet.Infrastructure; 3 | using YDotNet.Native.Types; 4 | using YDotNet.Native.Types.Events; 5 | 6 | namespace YDotNet.Document.Types.Events; 7 | 8 | /// 9 | /// Represents the keys that changed the shared type that emitted the event related to this instance. 10 | /// 11 | public class EventKeys : ReadOnlyCollection 12 | { 13 | internal EventKeys(nint handle, uint length, Doc doc) 14 | : base(ReadItems(handle, length, doc)) 15 | { 16 | } 17 | 18 | private static IList ReadItems(nint handle, uint length, Doc doc) 19 | { 20 | var result = new List(); 21 | 22 | foreach (var native in MemoryReader.ReadStructsWithHandles(handle, length)) 23 | { 24 | result.Add(new EventKeyChange(native.Value, doc)); 25 | } 26 | 27 | // We are done reading and can destroy the resource. 28 | EventChannel.KeysDestroy(handle, length); 29 | 30 | return result; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /YDotNet/Document/Types/Events/EventPath.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using YDotNet.Infrastructure; 3 | using YDotNet.Native.Types; 4 | using YDotNet.Native.Types.Events; 5 | 6 | namespace YDotNet.Document.Types.Events; 7 | 8 | /// 9 | /// Represents the path from the root type to the shared type that emitted the event related to this 10 | /// instance. 11 | /// 12 | public sealed class EventPath : ReadOnlyCollection 13 | { 14 | internal EventPath(nint handle, uint length) 15 | : base(ReadItems(handle, length)) 16 | { 17 | } 18 | 19 | private static IList ReadItems(nint handle, uint length) 20 | { 21 | var result = new List(); 22 | 23 | foreach (var native in MemoryReader.ReadStructs(handle, length)) 24 | { 25 | result.Add(new EventPathSegment(native)); 26 | } 27 | 28 | // We have read everything, so we can release the memory immediately. 29 | PathChannel.Destroy(handle, length); 30 | 31 | return result; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Lucas Viana 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 | -------------------------------------------------------------------------------- /YDotNet.Server.EntityFramework/YDotNet.Server.EntityFramework.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Driver/Tasks/Texts/Create.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document; 2 | using YDotNet.Tests.Driver.Abstractions; 3 | 4 | namespace YDotNet.Tests.Driver.Tasks.Texts; 5 | 6 | public class Create : ITask 7 | { 8 | public Task Run() 9 | { 10 | var count = 0; 11 | 12 | // Create many documents 13 | while (count < 1_000_000) 14 | { 15 | // After 1s, stop and show the user the amount of documents 16 | if (count > 0) 17 | { 18 | Console.WriteLine("Status Report"); 19 | Console.WriteLine($"\tTexts:\t{count}"); 20 | Console.WriteLine(); 21 | } 22 | 23 | if (count % 1_000 == 0) 24 | { 25 | Thread.Sleep(millisecondsTimeout: 15); 26 | } 27 | 28 | // Create many documents 29 | for (var i = 0; i < 100; i++) 30 | { 31 | var doc = new Doc(); 32 | doc.Text($"sample-{i}"); 33 | doc.Dispose(); 34 | count++; 35 | } 36 | } 37 | 38 | return Task.CompletedTask; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Driver/Tasks/Arrays/Create.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document; 2 | using YDotNet.Tests.Driver.Abstractions; 3 | 4 | namespace YDotNet.Tests.Driver.Tasks.Arrays; 5 | 6 | public class Create : ITask 7 | { 8 | public Task Run() 9 | { 10 | var count = 0; 11 | 12 | // Create many documents 13 | while (count < 1_000_000) 14 | { 15 | // After 1s, stop and show the user the amount of documents 16 | if (count > 0) 17 | { 18 | Console.WriteLine("Status Report"); 19 | Console.WriteLine($"\tArrays:\t{count}"); 20 | Console.WriteLine(); 21 | } 22 | 23 | if (count % 1_000 == 0) 24 | { 25 | Thread.Sleep(millisecondsTimeout: 15); 26 | } 27 | 28 | // Create many documents 29 | for (var i = 0; i < 100; i++) 30 | { 31 | var doc = new Doc(); 32 | doc.Array($"sample-{i}"); 33 | doc.Dispose(); 34 | count++; 35 | } 36 | } 37 | 38 | return Task.CompletedTask; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /YDotNet/Document/Cells/JsonObject.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using YDotNet.Infrastructure; 3 | using YDotNet.Infrastructure.Extensions; 4 | using YDotNet.Native.Cells.Outputs; 5 | using YDotNet.Native.Types.Maps; 6 | 7 | namespace YDotNet.Document.Cells; 8 | 9 | /// 10 | /// Represents a JSON object. 11 | /// 12 | public sealed class JsonObject : ReadOnlyDictionary 13 | { 14 | internal JsonObject(nint handle, uint length, Doc doc, bool isDeleted) 15 | : base(ReadItems(handle, length, doc, isDeleted)) 16 | { 17 | } 18 | 19 | private static Dictionary ReadItems(nint handle, uint length, Doc doc, bool isDeleted) 20 | { 21 | var entriesHandle = OutputChannel.Object(handle).Checked(); 22 | 23 | var result = new Dictionary(StringComparer.Ordinal); 24 | 25 | foreach (var (native, itemHandle) in MemoryReader.ReadStructsWithHandles(entriesHandle, length)) 26 | { 27 | result[native.Key()] = new Output(native.ValueHandle(itemHandle), doc, isDeleted); 28 | } 29 | 30 | return result; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/UndoManagers/UnobservePoppedTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | using YDotNet.Document.UndoManagers; 4 | using YDotNet.Document.UndoManagers.Events; 5 | 6 | namespace YDotNet.Tests.Unit.UndoManagers; 7 | 8 | public class UnobservePoppedTests 9 | { 10 | [Test] 11 | public void TriggersWhenChangesUntilUnobserved() 12 | { 13 | // Arrange 14 | var doc = new Doc(); 15 | var text = doc.Text("text"); 16 | var undoManager = new UndoManager(doc, text, new UndoManagerOptions { CaptureTimeoutMilliseconds = 0 }); 17 | 18 | UndoEvent? undoEvent = null; 19 | var subscription = undoManager.ObservePopped(e => undoEvent = e); 20 | 21 | // Act 22 | var transaction = doc.WriteTransaction(); 23 | text.Insert(transaction, index: 0, "Lucas"); 24 | transaction.Commit(); 25 | undoManager.Undo(); 26 | 27 | // Assert 28 | Assert.That(undoEvent, Is.Not.Null); 29 | 30 | // Act 31 | undoEvent = null; 32 | subscription.Dispose(); 33 | undoManager.Redo(); 34 | 35 | // Assert 36 | Assert.That(undoEvent, Is.Null); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /YDotNet.Server.MongoDB/YDotNet.Server.MongoDB.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | all 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /YDotNet/Document/Events/AfterTransactionEvent.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document.State; 2 | using YDotNet.Native.Document.Events; 3 | 4 | namespace YDotNet.Document.Events; 5 | 6 | /// 7 | /// An after transaction event passed to a callback subscribed to . 8 | /// 9 | public class AfterTransactionEvent 10 | { 11 | internal AfterTransactionEvent(AfterTransactionEventNative native) 12 | { 13 | BeforeState = new StateVector(native.BeforeState); 14 | AfterState = new StateVector(native.AfterState); 15 | DeleteSet = new DeleteSet(native.DeleteSet); 16 | } 17 | 18 | /// 19 | /// Gets descriptor of a document state at the moment of creating the transaction. 20 | /// 21 | public StateVector BeforeState { get; } 22 | 23 | /// 24 | /// Gets descriptor of a document state at the moment of committing the transaction. 25 | /// 26 | public StateVector AfterState { get; } 27 | 28 | /// 29 | /// Gets information about all items deleted within the scope of a transaction. 30 | /// 31 | public DeleteSet DeleteSet { get; } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Driver/Tasks/Docs/ObserveClear.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document; 2 | using YDotNet.Tests.Driver.Abstractions; 3 | 4 | namespace YDotNet.Tests.Driver.Tasks.Docs; 5 | 6 | public class ObserveClear : ITask 7 | { 8 | public Task Run() 9 | { 10 | var count = 0; 11 | 12 | // Create many documents 13 | while (count < 1_000_000) 14 | { 15 | // After 1s, stop and show the user the amount of documents 16 | if (count > 0) 17 | { 18 | Console.WriteLine("Status Report"); 19 | Console.WriteLine($"\tDocs:\t{count}"); 20 | Console.WriteLine(); 21 | } 22 | 23 | if (count % 1_000 == 0) 24 | { 25 | Thread.Sleep(millisecondsTimeout: 15); 26 | } 27 | 28 | // Create many documents 29 | for (var i = 0; i < 100; i++) 30 | { 31 | var doc = new Doc(); 32 | 33 | doc.ObserveClear(_ => { }); 34 | doc.Clear(); 35 | 36 | doc.Dispose(); 37 | 38 | count++; 39 | } 40 | } 41 | 42 | return Task.CompletedTask; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/XmlTexts/UnobserveTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | 4 | namespace YDotNet.Tests.Unit.XmlTexts; 5 | 6 | public class UnobserveTests 7 | { 8 | [Test] 9 | public void TriggersWhenXmlTextChangedUntilUnobserved() 10 | { 11 | // Arrange 12 | var doc = new Doc(); 13 | var xmlFragment = doc.XmlFragment("xml-fragment"); 14 | 15 | var transaction = doc.WriteTransaction(); 16 | var xmlText = xmlFragment.InsertText(transaction, index: 0); 17 | transaction.Commit(); 18 | 19 | var called = 0; 20 | var subscription = xmlText.Observe(_ => called++); 21 | 22 | // Act 23 | transaction = doc.WriteTransaction(); 24 | xmlText.Insert(transaction, index: 0, "Lucas"); 25 | transaction.Commit(); 26 | 27 | // Assert 28 | Assert.That(called, Is.EqualTo(expected: 1)); 29 | 30 | // Act 31 | subscription.Dispose(); 32 | 33 | transaction = doc.WriteTransaction(); 34 | xmlText.Insert(transaction, index: 1, " Viana"); 35 | transaction.Commit(); 36 | 37 | // Assert 38 | Assert.That(called, Is.EqualTo(expected: 1)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /YDotNet.Extensions/YDotNet.Extensions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | all 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | 27 | 28 | all 29 | runtime; build; native; contentfiles; analyzers; buildtransitive 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Transactions/SnapshotTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | 4 | namespace YDotNet.Tests.Unit.Transactions; 5 | 6 | public class SnapshotTests 7 | { 8 | [Test] 9 | public void ReadOnly() 10 | { 11 | // Arrange 12 | var doc = ArrangeDoc(); 13 | 14 | // Act 15 | var snapshot = doc.ReadTransaction().Snapshot(); 16 | 17 | // Assert 18 | Assert.That(snapshot, Is.Not.Null); 19 | Assert.That(snapshot.Length, Is.GreaterThan(expected: 3)); 20 | } 21 | 22 | [Test] 23 | public void ReadWrite() 24 | { 25 | // Arrange 26 | var doc = ArrangeDoc(); 27 | 28 | // Act 29 | var snapshot = doc.WriteTransaction().Snapshot(); 30 | 31 | // Assert 32 | Assert.That(snapshot, Is.Not.Null); 33 | Assert.That(snapshot.Length, Is.GreaterThan(expected: 3)); 34 | } 35 | 36 | private static Doc ArrangeDoc() 37 | { 38 | var doc = new Doc(); 39 | var text = doc.Text("name"); 40 | var transaction = doc.WriteTransaction(); 41 | 42 | text.Insert(transaction, index: 0, "Lucas"); 43 | transaction.Commit(); 44 | 45 | return doc; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/YDotNet.Tests.Unit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | false 7 | YDotNet.Tests.Unit.Program 8 | 9 | 10 | 11 | 1701;1702;8632 12 | 13 | 14 | 15 | 1701;1702;8632 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/XmlElements/UnobserveTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | 4 | namespace YDotNet.Tests.Unit.XmlElements; 5 | 6 | public class UnobserveTests 7 | { 8 | [Test] 9 | public void TriggersWhenXmlElementChangedUntilUnobserved() 10 | { 11 | // Arrange 12 | var doc = new Doc(); 13 | var xmlFragment = doc.XmlFragment("xml-fragment"); 14 | 15 | var transaction = doc.WriteTransaction(); 16 | var xmlElement = xmlFragment.InsertElement(transaction, index: 0, "xml-element"); 17 | transaction.Commit(); 18 | 19 | var called = 0; 20 | var subscription = xmlElement.Observe(_ => called++); 21 | 22 | // Act 23 | transaction = doc.WriteTransaction(); 24 | xmlElement.InsertText(transaction, index: 0); 25 | transaction.Commit(); 26 | 27 | // Assert 28 | Assert.That(called, Is.EqualTo(expected: 1)); 29 | 30 | // Act 31 | subscription.Dispose(); 32 | 33 | transaction = doc.WriteTransaction(); 34 | xmlElement.InsertText(transaction, index: 1); 35 | transaction.Commit(); 36 | 37 | // Assert 38 | Assert.That(called, Is.EqualTo(expected: 1)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Demo/Client/src/components/Awareness.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Input } from 'reactstrap'; 3 | import { useYjs } from '../hooks/useYjs'; 4 | 5 | export const Awareness = () => { 6 | const awareness = useYjs().yjsConnector.awareness; 7 | const [state, setState] = React.useState({}); 8 | 9 | React.useEffect(() => { 10 | const updateUsers = () => { 11 | const allStates: Record = {}; 12 | 13 | awareness.getStates().forEach((value, key) => { 14 | allStates[key.toString()] = value; 15 | }); 16 | 17 | setState(allStates); 18 | }; 19 | 20 | updateUsers(); 21 | awareness.on('change', updateUsers); 22 | awareness.setLocalStateField('user', { 23 | random: Math.random(), 24 | message: 'Hello', 25 | }); 26 | 27 | console.log(`Current CLIENT ID: ${awareness.clientID}`); 28 | 29 | return () => { 30 | awareness.off('change', updateUsers); 31 | }; 32 | }, [awareness]); 33 | 34 | return ( 35 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/UndoManagers/UnobserveAddedTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | using YDotNet.Document.UndoManagers; 4 | using YDotNet.Document.UndoManagers.Events; 5 | 6 | namespace YDotNet.Tests.Unit.UndoManagers; 7 | 8 | public class UnobserveAddedTests 9 | { 10 | [Test] 11 | public void TriggersWhenChangesUntilUnobserved() 12 | { 13 | // Arrange 14 | var doc = new Doc(); 15 | var text = doc.Text("text"); 16 | var undoManager = new UndoManager(doc, text, new UndoManagerOptions { CaptureTimeoutMilliseconds = 0 }); 17 | 18 | UndoEvent? undoEvent = null; 19 | var subscription = undoManager.ObserveAdded(e => undoEvent = e); 20 | 21 | // Act 22 | var transaction = doc.WriteTransaction(); 23 | text.Insert(transaction, index: 0, "Lucas"); 24 | transaction.Commit(); 25 | 26 | // Assert 27 | Assert.That(undoEvent, Is.Not.Null); 28 | 29 | // Act 30 | undoEvent = null; 31 | subscription.Dispose(); 32 | transaction = doc.WriteTransaction(); 33 | text.Insert(transaction, index: 5, " Viana"); 34 | transaction.Commit(); 35 | 36 | // Assert 37 | Assert.That(undoEvent, Is.Null); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /YDotNet/Document/StickyIndexes/StickyAssociationType.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Document.StickyIndexes; 2 | 3 | /// 4 | /// Association type used by a . 5 | /// 6 | /// 7 | /// 8 | /// In general, a refers to a cursor space between two elements (eg. "ab.c" where "abc" 9 | /// is our string and `.` is the placement). 10 | /// 11 | /// 12 | /// In a situation when another client is updating a collection concurrently, a new set of elements may be inserted 13 | /// into that space, expanding it in the result. In such case, the tells us if 14 | /// the should stick to location before or after referenced index. 15 | /// 16 | /// 17 | public enum StickyAssociationType : sbyte 18 | { 19 | /// 20 | /// The corresponding points to space after the referenced element. 21 | /// 22 | After = 0, 23 | 24 | /// 25 | /// The corresponding points to space before the referenced element. 26 | /// 27 | Before = -1, 28 | } 29 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Driver/Tasks/Docs/ReadAutoLoad.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document; 2 | using YDotNet.Document.Options; 3 | using YDotNet.Tests.Driver.Abstractions; 4 | 5 | namespace YDotNet.Tests.Driver.Tasks.Docs; 6 | 7 | public class ReadAutoLoad : ITask 8 | { 9 | public Task Run() 10 | { 11 | var count = 0; 12 | var doc = new Doc( 13 | new DocOptions 14 | { 15 | AutoLoad = true 16 | }); 17 | 18 | // Read many times 19 | while (count < 1_000_000) 20 | { 21 | // After 1s, stop and show the user the amount of documents 22 | if (count > 0) 23 | { 24 | Console.WriteLine("Status Report"); 25 | Console.WriteLine($"\tReads:\t{count}"); 26 | Console.WriteLine(); 27 | } 28 | 29 | if (count % 1_000 == 0) 30 | { 31 | Thread.Sleep(millisecondsTimeout: 15); 32 | } 33 | 34 | // Create many documents 35 | for (var i = 0; i < 100; i++) 36 | { 37 | var _ = doc.AutoLoad; 38 | count++; 39 | } 40 | } 41 | 42 | doc.Dispose(); 43 | 44 | return Task.CompletedTask; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /YDotNet/Document/Events/SubDocsEvent.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Native.Document.Events; 2 | 3 | namespace YDotNet.Document.Events; 4 | 5 | /// 6 | /// Event used to communicate the load requests from the underlying sub-documents. 7 | /// 8 | public class SubDocsEvent 9 | { 10 | internal SubDocsEvent(SubDocsEventNative native, Doc doc) 11 | { 12 | Added = native.Added().Select(h => doc.GetDoc(h, isDeleted: false)).ToList(); 13 | Removed = native.Removed().Select(h => doc.GetDoc(h, isDeleted: true)).ToList(); 14 | Loaded = native.Loaded().Select(h => doc.GetDoc(h, isDeleted: false)).ToList(); 15 | } 16 | 17 | /// 18 | /// Gets the sub-documents that were added to the instance that emitted this event. 19 | /// 20 | public IReadOnlyList Added { get; } 21 | 22 | /// 23 | /// Gets the sub-documents that were removed to the instance that emitted this event. 24 | /// 25 | public IReadOnlyList Removed { get; } 26 | 27 | /// 28 | /// Gets the sub-documents that were loaded to the instance that emitted this event. 29 | /// 30 | public IReadOnlyList Loaded { get; } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Driver/Tasks/Docs/ReadGuid.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document; 2 | using YDotNet.Document.Options; 3 | using YDotNet.Tests.Driver.Abstractions; 4 | 5 | namespace YDotNet.Tests.Driver.Tasks.Docs; 6 | 7 | public class ReadGuid : ITask 8 | { 9 | public Task Run() 10 | { 11 | var count = 0; 12 | var doc = new Doc( 13 | new DocOptions 14 | { 15 | Guid = Guid.NewGuid().ToString() 16 | }); 17 | 18 | // Read many times 19 | while (count < 1_000_000) 20 | { 21 | // After 1s, stop and show the user the amount of documents 22 | if (count > 0) 23 | { 24 | Console.WriteLine("Status Report"); 25 | Console.WriteLine($"\tReads:\t{count}"); 26 | Console.WriteLine(); 27 | } 28 | 29 | if (count % 1_000 == 0) 30 | { 31 | Thread.Sleep(millisecondsTimeout: 15); 32 | } 33 | 34 | // Create many documents 35 | for (var i = 0; i < 100; i++) 36 | { 37 | var _ = doc.Guid; 38 | count++; 39 | } 40 | } 41 | 42 | doc.Dispose(); 43 | 44 | return Task.CompletedTask; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Transactions/StateVectorV1Tests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | 4 | namespace YDotNet.Tests.Unit.Transactions; 5 | 6 | public class StateVectorV1Tests 7 | { 8 | [Test] 9 | public void ReadOnly() 10 | { 11 | // Arrange 12 | var doc = ArrangeDoc(); 13 | 14 | // Act 15 | var stateVector = doc.WriteTransaction().StateVectorV1(); 16 | 17 | // Assert 18 | Assert.That(stateVector, Is.Not.Null); 19 | Assert.That(stateVector.Length, Is.GreaterThanOrEqualTo(expected: 3)); 20 | } 21 | 22 | [Test] 23 | public void ReadWrite() 24 | { 25 | // Arrange 26 | var doc = ArrangeDoc(); 27 | 28 | // Act 29 | var stateVector = doc.WriteTransaction().StateVectorV1(); 30 | 31 | // Assert 32 | Assert.That(stateVector, Is.Not.Null); 33 | Assert.That(stateVector.Length, Is.GreaterThanOrEqualTo(expected: 3)); 34 | } 35 | 36 | private static Doc ArrangeDoc() 37 | { 38 | var doc = new Doc(); 39 | var text = doc.Text("name"); 40 | var transaction = doc.WriteTransaction(); 41 | 42 | text.Insert(transaction, index: 0, "Lucas"); 43 | transaction.Commit(); 44 | 45 | return doc; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /YDotNet.Server/Events.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document; 2 | 3 | #pragma warning disable MA0048 // File name must match type name 4 | #pragma warning disable SA1402 // File may only contain a single type 5 | #pragma warning disable SA1649 // File name should match first type name 6 | 7 | namespace YDotNet.Server; 8 | 9 | public abstract class DocumentEvent 10 | { 11 | required public IDocumentManager Source { get; init; } 12 | 13 | required public DocumentContext Context { get; init; } 14 | } 15 | 16 | public class DocumentChangeEvent : DocumentEvent 17 | { 18 | required public Doc Document { get; init; } 19 | } 20 | 21 | public class DocumentLoadEvent : DocumentEvent 22 | { 23 | required public Doc Document { get; init; } 24 | } 25 | 26 | public sealed class DocumentChangedEvent : DocumentChangeEvent 27 | { 28 | required public byte[] Diff { get; init; } 29 | } 30 | 31 | public sealed class ClientDisconnectedEvent : DocumentEvent 32 | { 33 | required public DisconnectReason Reason { get; init; } 34 | } 35 | 36 | public sealed class ClientAwarenessEvent : DocumentEvent 37 | { 38 | required public string? ClientState { get; set; } 39 | 40 | required public ulong ClientClock { get; set; } 41 | } 42 | 43 | public enum DisconnectReason 44 | { 45 | Disconnect, 46 | Cleanup, 47 | } 48 | -------------------------------------------------------------------------------- /YDotNet.Server.WebSockets/ClientState.cs: -------------------------------------------------------------------------------- 1 | using System.Net.WebSockets; 2 | 3 | namespace YDotNet.Server.WebSockets; 4 | 5 | public sealed class ClientState : IDisposable 6 | { 7 | private readonly SemaphoreSlim slimLock = new(1); 8 | 9 | required public WebSocket WebSocket { get; set; } 10 | 11 | required public WebSocketEncoder Encoder { get; set; } 12 | 13 | required public WebSocketDecoder Decoder { get; set; } 14 | 15 | required public DocumentContext DocumentContext { get; set; } 16 | 17 | required public string DocumentName { get; init; } 18 | 19 | public bool IsSynced { get; set; } 20 | 21 | public Queue PendingUpdates { get; } = new Queue(); 22 | 23 | public async Task WriteLockedAsync(T state, Func action, CancellationToken ct) 24 | { 25 | await slimLock.WaitAsync(ct).ConfigureAwait(false); 26 | try 27 | { 28 | await action(Encoder, state, this, ct).ConfigureAwait(false); 29 | } 30 | finally 31 | { 32 | slimLock.Release(); 33 | } 34 | } 35 | 36 | public void Dispose() 37 | { 38 | WebSocket.Dispose(); 39 | 40 | Encoder.Dispose(); 41 | Decoder.Dispose(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Demo/Client/src/context/yjsContext.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as Y from 'yjs'; 3 | import { WebsocketProvider } from 'y-websocket'; 4 | import config from '../config'; 5 | 6 | export interface IYjsContext { 7 | readonly yjsDocument: Y.Doc; 8 | readonly yjsConnector: WebsocketProvider; 9 | readonly roomName: string; 10 | } 11 | 12 | export interface IOptions extends React.PropsWithChildren { 13 | readonly roomName: string; 14 | } 15 | 16 | export const YjsContextProvider: React.FunctionComponent = ( 17 | props: IOptions 18 | ) => { 19 | const { roomName } = props; 20 | const baseUrl = `${config.WS_URL}/collaboration`; 21 | 22 | const contextProps: IYjsContext = React.useMemo(() => { 23 | const yjsDocument = new Y.Doc(); 24 | const yjsConnector = new WebsocketProvider( 25 | baseUrl, 26 | roomName, 27 | yjsDocument 28 | ); 29 | 30 | return { yjsDocument, yjsConnector, roomName }; 31 | }, [baseUrl, roomName]); 32 | 33 | return ( 34 | 35 | {props.children} 36 | 37 | ); 38 | }; 39 | 40 | export const YjsContext = React.createContext( 41 | undefined 42 | ); 43 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Driver/Tasks/Docs/ReadShouldLoad.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document; 2 | using YDotNet.Document.Options; 3 | using YDotNet.Tests.Driver.Abstractions; 4 | 5 | namespace YDotNet.Tests.Driver.Tasks.Docs; 6 | 7 | public class ReadShouldLoad : ITask 8 | { 9 | public Task Run() 10 | { 11 | var count = 0; 12 | var doc = new Doc( 13 | new DocOptions 14 | { 15 | Guid = Guid.NewGuid().ToString() 16 | }); 17 | 18 | // Read many times 19 | while (count < 1_000_000) 20 | { 21 | // After 1s, stop and show the user the amount of documents 22 | if (count > 0) 23 | { 24 | Console.WriteLine("Status Report"); 25 | Console.WriteLine($"\tReads:\t{count}"); 26 | Console.WriteLine(); 27 | } 28 | 29 | if (count % 1_000 == 0) 30 | { 31 | Thread.Sleep(millisecondsTimeout: 15); 32 | } 33 | 34 | // Create many documents 35 | for (var i = 0; i < 100; i++) 36 | { 37 | var _ = doc.ShouldLoad; 38 | count++; 39 | } 40 | } 41 | 42 | doc.Dispose(); 43 | 44 | return Task.CompletedTask; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Driver/Tasks/Docs/ReadCollectionId.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document; 2 | using YDotNet.Document.Options; 3 | using YDotNet.Tests.Driver.Abstractions; 4 | 5 | namespace YDotNet.Tests.Driver.Tasks.Docs; 6 | 7 | public class ReadCollectionId : ITask 8 | { 9 | public Task Run() 10 | { 11 | var count = 0; 12 | var doc = new Doc( 13 | new DocOptions 14 | { 15 | CollectionId = "sample-collection" 16 | }); 17 | 18 | // Read many times 19 | while (count < 1_000_000) 20 | { 21 | // After 1s, stop and show the user the amount of documents 22 | if (count > 0) 23 | { 24 | Console.WriteLine("Status Report"); 25 | Console.WriteLine($"\tReads:\t{count}"); 26 | Console.WriteLine(); 27 | } 28 | 29 | if (count % 1_000 == 0) 30 | { 31 | Thread.Sleep(millisecondsTimeout: 15); 32 | } 33 | 34 | // Create many documents 35 | for (var i = 0; i < 100; i++) 36 | { 37 | var _ = doc.CollectionId; 38 | count++; 39 | } 40 | } 41 | 42 | doc.Dispose(); 43 | 44 | return Task.CompletedTask; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Branches/XmlTextUnobserveDeepTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | using YDotNet.Document.Cells; 4 | 5 | namespace YDotNet.Tests.Unit.Branches; 6 | 7 | public class XmlTextUnobserveDeepTests 8 | { 9 | [Test] 10 | public void TriggersWhenChangedUntilUnobserved() 11 | { 12 | // Arrange 13 | var doc = new Doc(); 14 | var map = doc.Map("map"); 15 | 16 | var transaction = doc.WriteTransaction(); 17 | map.Insert(transaction, "xml-text", Input.XmlText("xml-text")); 18 | var xmlText = map.Get(transaction, "xml-text").XmlText; 19 | transaction.Commit(); 20 | 21 | var called = 0; 22 | var subscription = xmlText.ObserveDeep(_ => called++); 23 | 24 | // Act 25 | transaction = doc.WriteTransaction(); 26 | xmlText.Insert(transaction, index: 0, "World"); 27 | transaction.Commit(); 28 | 29 | // Assert 30 | Assert.That(called, Is.EqualTo(expected: 1)); 31 | 32 | // Act 33 | subscription.Dispose(); 34 | 35 | transaction = doc.WriteTransaction(); 36 | xmlText.Insert(transaction, index: 0, "Hello, "); 37 | transaction.Commit(); 38 | 39 | // Assert 40 | Assert.That(called, Is.EqualTo(expected: 1)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Branches/XmlElementUnobserveDeepTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | using YDotNet.Document.Cells; 4 | 5 | namespace YDotNet.Tests.Unit.Branches; 6 | 7 | public class XmlElementUnobserveDeepTests 8 | { 9 | [Test] 10 | public void TriggersWhenMapChangedUntilUnobserved() 11 | { 12 | // Arrange 13 | var doc = new Doc(); 14 | var map = doc.Map("map"); 15 | 16 | var transaction = doc.WriteTransaction(); 17 | map.Insert(transaction, "xml-element", Input.XmlElement("xml-element")); 18 | var xmlElement = map.Get(transaction, "xml-element").XmlElement; 19 | transaction.Commit(); 20 | 21 | var called = 0; 22 | var subscription = xmlElement.ObserveDeep(_ => called++); 23 | 24 | // Act 25 | transaction = doc.WriteTransaction(); 26 | xmlElement.InsertText(transaction, index: 0); 27 | transaction.Commit(); 28 | 29 | // Assert 30 | Assert.That(called, Is.EqualTo(expected: 1)); 31 | 32 | // Act 33 | subscription.Dispose(); 34 | 35 | transaction = doc.WriteTransaction(); 36 | xmlElement.InsertText(transaction, index: 0); 37 | transaction.Commit(); 38 | 39 | // Assert 40 | Assert.That(called, Is.EqualTo(expected: 1)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Transactions/ApplyV1Tests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | using YDotNet.Document.Transactions; 4 | 5 | namespace YDotNet.Tests.Unit.Transactions; 6 | 7 | public class ApplyV1Tests 8 | { 9 | [Test] 10 | public void ApplyAndCheckChanges() 11 | { 12 | // Arrange 13 | var senderDoc = ArrangeSenderDoc(); 14 | var receiverDoc = new Doc(); 15 | var receiverText = receiverDoc.Text("name"); 16 | var receiverTransaction = receiverDoc.WriteTransaction(); 17 | var senderTransaction = senderDoc.ReadTransaction(); 18 | 19 | // Act 20 | var stateDiff = senderTransaction.StateDiffV1(receiverTransaction.StateVectorV1()); 21 | var result = receiverTransaction.ApplyV1(stateDiff); 22 | var text = receiverText.String(receiverTransaction); 23 | 24 | // Assert 25 | Assert.That(result, Is.EqualTo(TransactionUpdateResult.Ok)); 26 | Assert.That(text, Is.EqualTo("Lucas")); 27 | } 28 | 29 | private static Doc ArrangeSenderDoc() 30 | { 31 | var doc = new Doc(); 32 | var text = doc.Text("name"); 33 | var transaction = doc.WriteTransaction(); 34 | 35 | text.Insert(transaction, index: 0, "Lucas"); 36 | transaction.Commit(); 37 | 38 | return doc; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Transactions/ApplyV2Tests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | using YDotNet.Document.Transactions; 4 | 5 | namespace YDotNet.Tests.Unit.Transactions; 6 | 7 | public class ApplyV2Tests 8 | { 9 | [Test] 10 | public void ApplyAndCheckChanges() 11 | { 12 | // Arrange 13 | var senderDoc = ArrangeSenderDoc(); 14 | var receiverDoc = new Doc(); 15 | var receiverText = receiverDoc.Text("name"); 16 | var receiverTransaction = receiverDoc.WriteTransaction(); 17 | var senderTransaction = senderDoc.ReadTransaction(); 18 | 19 | // Act 20 | var stateDiff = senderTransaction.StateDiffV2(receiverTransaction.StateVectorV1()); 21 | var result = receiverTransaction.ApplyV2(stateDiff); 22 | var text = receiverText.String(receiverTransaction); 23 | 24 | // Assert 25 | Assert.That(result, Is.EqualTo(TransactionUpdateResult.Ok)); 26 | Assert.That(text, Is.EqualTo("Lucas")); 27 | } 28 | 29 | private static Doc ArrangeSenderDoc() 30 | { 31 | var doc = new Doc(); 32 | var text = doc.Text("name"); 33 | var transaction = doc.WriteTransaction(); 34 | 35 | text.Insert(transaction, index: 0, "Lucas"); 36 | transaction.Commit(); 37 | 38 | return doc; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Demo/Client/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | parserOptions: { 18 | ecmaVersion: 'latest', 19 | sourceType: 'module', 20 | project: ['./tsconfig.json', './tsconfig.node.json'], 21 | tsconfigRootDir: __dirname, 22 | }, 23 | ``` 24 | 25 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` 26 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` 27 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list 28 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Driver/Tasks/Texts/InsertText.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document; 2 | using YDotNet.Tests.Driver.Abstractions; 3 | 4 | namespace YDotNet.Tests.Driver.Tasks.Texts; 5 | 6 | public class InsertText : ITask 7 | { 8 | public Task Run() 9 | { 10 | var count = 0; 11 | 12 | // Create many documents 13 | while (count < 1_000_000) 14 | { 15 | // After 1s, stop and show the user the amount of documents 16 | if (count > 0) 17 | { 18 | Console.WriteLine("Status Report"); 19 | Console.WriteLine($"\tTexts:\t{count}"); 20 | Console.WriteLine(); 21 | } 22 | 23 | if (count % 1_000 == 0) 24 | { 25 | Thread.Sleep(millisecondsTimeout: 15); 26 | } 27 | 28 | // Create many documents 29 | for (var i = 0; i < 100; i++) 30 | { 31 | var doc = new Doc(); 32 | var text = doc.Text($"sample-{i}"); 33 | 34 | var transaction = doc.WriteTransaction(); 35 | text.Insert(transaction, index: 0, "YDotNet"); 36 | transaction.Commit(); 37 | 38 | doc.Dispose(); 39 | 40 | count++; 41 | } 42 | } 43 | 44 | return Task.CompletedTask; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /YDotNet/Document/Types/Texts/TextChunk.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document.Cells; 2 | using YDotNet.Native; 3 | using YDotNet.Native.Types.Texts; 4 | 5 | namespace YDotNet.Document.Types.Texts; 6 | 7 | /// 8 | /// Represents a chunk of text formatted with the same set of attributes. 9 | /// 10 | public class TextChunk 11 | { 12 | internal TextChunk(NativeWithHandle native, Doc doc) 13 | { 14 | // `Handle` is used because the `OutputNative` is located at the head of `TextChunkNative`. 15 | Data = new Output(native.Handle, doc, isDeleted: false); 16 | 17 | Attributes = native.Value.Attributes() 18 | .ToDictionary( 19 | x => x.Value.Key(), 20 | x => new Output(x.Value.ValueHandle(x.Handle), doc, isDeleted: false), 21 | StringComparer.Ordinal); 22 | } 23 | 24 | /// 25 | /// Gets the piece of formatted using the same attributes. 26 | /// 27 | /// 28 | /// It can be a string, embedded object or another shared type. 29 | /// 30 | public Output Data { get; } 31 | 32 | /// 33 | /// Gets the formatting attributes applied to the . 34 | /// 35 | public IReadOnlyDictionary Attributes { get; } 36 | } 37 | -------------------------------------------------------------------------------- /native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /YDotNet/Native/Document/Events/SubDocsEventNative.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using YDotNet.Infrastructure; 3 | 4 | namespace YDotNet.Native.Document.Events; 5 | 6 | [StructLayout(LayoutKind.Sequential)] 7 | internal readonly struct SubDocsEventNative 8 | { 9 | public uint AddedLength { get; } 10 | 11 | public uint RemovedLength { get; } 12 | 13 | public uint LoadedLength { get; } 14 | 15 | public nint AddedHandle { get; } 16 | 17 | public nint RemovedHandle { get; } 18 | 19 | public nint LoadedHandle { get; } 20 | 21 | public nint[] Added() 22 | { 23 | if (AddedHandle == nint.Zero || AddedLength == 0) 24 | { 25 | return Array.Empty(); 26 | } 27 | 28 | return MemoryReader.ReadStructs(AddedHandle, AddedLength); 29 | } 30 | 31 | public nint[] Removed() 32 | { 33 | if (RemovedHandle == nint.Zero || RemovedLength == 0) 34 | { 35 | return Array.Empty(); 36 | } 37 | 38 | return MemoryReader.ReadStructs(RemovedHandle, RemovedLength); 39 | } 40 | 41 | public nint[] Loaded() 42 | { 43 | if (LoadedHandle == nint.Zero || LoadedLength == 0) 44 | { 45 | return Array.Empty(); 46 | } 47 | 48 | return MemoryReader.ReadStructs(LoadedHandle, LoadedLength); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /YDotNet.Server.EntityFramework/ServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Server.EntityFramework; 2 | 3 | using System; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | using YDotNet.Server.Storage; 8 | 9 | public static class ServiceExtensions 10 | { 11 | public static YDotnetRegistration AddEntityFrameworkStorage(this YDotnetRegistration registration, Action? configure = null) 12 | where T : DbContext 13 | { 14 | registration.Services.Configure(configure ?? (x => { })); 15 | registration.Services.AddSingleton>(); 16 | 17 | registration.Services.AddSingleton( 18 | c => c.GetRequiredService>()); 19 | 20 | registration.Services.AddDbContextFactory(); 21 | registration.Services.AddSingleton>(); 22 | 23 | return registration; 24 | } 25 | 26 | public static ModelBuilder UseYDotNet(this ModelBuilder builder) 27 | { 28 | builder.Entity(b => 29 | { 30 | b.ToTable("YDotNetDocument"); 31 | 32 | b.HasKey(x => x.Id); 33 | 34 | b.Property(x => x.Id) 35 | .HasMaxLength(256); 36 | }); 37 | 38 | return builder; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Demo/Client/src/components/YjsMonacoEditor.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import * as React from 'react'; 3 | import * as yMonaco from 'y-monaco'; 4 | import MonacoEditor, { monaco } from 'react-monaco-editor'; 5 | import { useYjs } from '../hooks/useYjs'; 6 | 7 | export const YjsMonacoEditor = () => { 8 | const { yjsDocument, yjsConnector } = useYjs(); 9 | const yText = yjsDocument.getText('monaco'); 10 | 11 | const [, setMonacoEditor] = React.useState(); 12 | const [, setMonacoBinding] = React.useState( 13 | null 14 | ); 15 | 16 | const _onEditorDidMount = React.useCallback( 17 | (editor: monaco.editor.ICodeEditor): void => { 18 | editor.focus(); 19 | editor.setValue(''); 20 | 21 | setMonacoEditor(editor); 22 | setMonacoBinding( 23 | new yMonaco.MonacoBinding( 24 | yText, 25 | editor.getModel()!, 26 | new Set([editor]) as any, 27 | yjsConnector.awareness 28 | ) 29 | ); 30 | }, 31 | [yjsConnector.awareness, yText, setMonacoEditor, setMonacoBinding] 32 | ); 33 | 34 | return ( 35 | 36 | _onEditorDidMount(e)} /> 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /YDotNet/Native/Types/Branches/BranchChannel.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace YDotNet.Native.Types.Branches; 4 | 5 | internal static class BranchChannel 6 | { 7 | public delegate void ObserveCallback(nint state, uint length, nint eventsHandle); 8 | 9 | [DllImport( 10 | ChannelSettings.NativeLib, 11 | CallingConvention = CallingConvention.Cdecl, 12 | EntryPoint = "yobserve_deep")] 13 | public static extern nint ObserveDeep(nint type, nint state, ObserveCallback callback); 14 | 15 | [DllImport( 16 | ChannelSettings.NativeLib, 17 | CallingConvention = CallingConvention.Cdecl, 18 | EntryPoint = "ytype_kind")] 19 | public static extern byte Kind(nint branch); 20 | 21 | [DllImport( 22 | ChannelSettings.NativeLib, 23 | CallingConvention = CallingConvention.Cdecl, 24 | EntryPoint = "ybranch_id")] 25 | public static extern BranchIdNative Id(nint branch); 26 | 27 | [DllImport( 28 | ChannelSettings.NativeLib, 29 | CallingConvention = CallingConvention.Cdecl, 30 | EntryPoint = "ybranch_get")] 31 | public static extern nint Get(nint branchId, nint transaction); 32 | 33 | [DllImport( 34 | ChannelSettings.NativeLib, 35 | CallingConvention = CallingConvention.Cdecl, 36 | EntryPoint = "ybranch_alive")] 37 | public static extern byte Alive(nint branchId); 38 | } 39 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Driver/Tasks/Docs/ObserveSubDocs.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document; 2 | using YDotNet.Tests.Driver.Abstractions; 3 | 4 | namespace YDotNet.Tests.Driver.Tasks.Docs; 5 | 6 | public class ObserveSubDocs : ITask 7 | { 8 | public Task Run() 9 | { 10 | var count = 0; 11 | 12 | // Create many documents 13 | while (count < 1_000_000) 14 | { 15 | // After 1s, stop and show the user the amount of documents 16 | if (count > 0) 17 | { 18 | Console.WriteLine("Status Report"); 19 | Console.WriteLine($"\tDocs:\t{count}"); 20 | Console.WriteLine(); 21 | } 22 | 23 | if (count % 1_000 == 0) 24 | { 25 | Thread.Sleep(millisecondsTimeout: 15); 26 | } 27 | 28 | // Create many documents 29 | for (var i = 0; i < 100; i++) 30 | { 31 | var doc = new Doc(); 32 | var text = doc.Text($"sample-{i}"); 33 | 34 | doc.ObserveSubDocs(_ => { }); 35 | 36 | var transaction = doc.WriteTransaction(); 37 | text.Insert(transaction, index: 0, "YDotNet"); 38 | transaction.Commit(); 39 | 40 | doc.Dispose(); 41 | 42 | count++; 43 | } 44 | } 45 | 46 | return Task.CompletedTask; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Driver/Tasks/Arrays/InsertRange.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document; 2 | using YDotNet.Document.Cells; 3 | using YDotNet.Tests.Driver.Abstractions; 4 | 5 | namespace YDotNet.Tests.Driver.Tasks.Arrays; 6 | 7 | public class InsertRange : ITask 8 | { 9 | public Task Run() 10 | { 11 | var count = 0; 12 | 13 | // Create many documents 14 | while (count < 1_000_000) 15 | { 16 | // After 1s, stop and show the user the amount of documents 17 | if (count > 0) 18 | { 19 | Console.WriteLine("Status Report"); 20 | Console.WriteLine($"\tArrays:\t{count}"); 21 | Console.WriteLine(); 22 | } 23 | 24 | if (count % 1_000 == 0) 25 | { 26 | Thread.Sleep(millisecondsTimeout: 15); 27 | } 28 | 29 | // Create many documents 30 | for (var i = 0; i < 100; i++) 31 | { 32 | var doc = new Doc(); 33 | var array = doc.Array($"sample-{i}"); 34 | 35 | var transaction = doc.WriteTransaction(); 36 | array.InsertRange(transaction, index: 0, new[] { Input.Long(value: 2469L) }); 37 | transaction.Commit(); 38 | 39 | doc.Dispose(); 40 | count++; 41 | } 42 | } 43 | 44 | return Task.CompletedTask; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Driver/Tasks/Docs/ObserveUpdatesV1.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document; 2 | using YDotNet.Tests.Driver.Abstractions; 3 | 4 | namespace YDotNet.Tests.Driver.Tasks.Docs; 5 | 6 | public class ObserveUpdatesV1 : ITask 7 | { 8 | public Task Run() 9 | { 10 | var count = 0; 11 | 12 | // Create many documents 13 | while (count < 1_000_000) 14 | { 15 | // After 1s, stop and show the user the amount of documents 16 | if (count > 0) 17 | { 18 | Console.WriteLine("Status Report"); 19 | Console.WriteLine($"\tDocs:\t{count}"); 20 | Console.WriteLine(); 21 | } 22 | 23 | if (count % 1_000 == 0) 24 | { 25 | Thread.Sleep(millisecondsTimeout: 15); 26 | } 27 | 28 | // Create many documents 29 | for (var i = 0; i < 100; i++) 30 | { 31 | var doc = new Doc(); 32 | var text = doc.Text($"sample-{i}"); 33 | 34 | doc.ObserveUpdatesV1(_ => { }); 35 | 36 | var transaction = doc.WriteTransaction(); 37 | text.Insert(transaction, index: 0, "YDotNet"); 38 | transaction.Commit(); 39 | 40 | doc.Dispose(); 41 | 42 | count++; 43 | } 44 | } 45 | 46 | return Task.CompletedTask; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Driver/Tasks/Docs/ObserveUpdatesV2.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document; 2 | using YDotNet.Tests.Driver.Abstractions; 3 | 4 | namespace YDotNet.Tests.Driver.Tasks.Docs; 5 | 6 | public class ObserveUpdatesV2 : ITask 7 | { 8 | public Task Run() 9 | { 10 | var count = 0; 11 | 12 | // Create many documents 13 | while (count < 1_000_000) 14 | { 15 | // After 1s, stop and show the user the amount of documents 16 | if (count > 0) 17 | { 18 | Console.WriteLine("Status Report"); 19 | Console.WriteLine($"\tDocs:\t{count}"); 20 | Console.WriteLine(); 21 | } 22 | 23 | if (count % 1_000 == 0) 24 | { 25 | Thread.Sleep(millisecondsTimeout: 15); 26 | } 27 | 28 | // Create many documents 29 | for (var i = 0; i < 100; i++) 30 | { 31 | var doc = new Doc(); 32 | var text = doc.Text($"sample-{i}"); 33 | 34 | doc.ObserveUpdatesV2(_ => { }); 35 | 36 | var transaction = doc.WriteTransaction(); 37 | text.Insert(transaction, index: 0, "YDotNet"); 38 | transaction.Commit(); 39 | 40 | doc.Dispose(); 41 | 42 | count++; 43 | } 44 | } 45 | 46 | return Task.CompletedTask; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /YDotNet.Server.Redis/YDotNet.Server.Redis.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | all 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /YDotNet/Document/Types/Arrays/ArrayEnumerator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using YDotNet.Document.Cells; 3 | using YDotNet.Native.Types; 4 | 5 | namespace YDotNet.Document.Types.Arrays; 6 | 7 | /// 8 | /// Represents the iterator to provide instances of or null to 9 | /// . 10 | /// 11 | internal class ArrayEnumerator : IEnumerator 12 | { 13 | private readonly ArrayIterator iterator; 14 | private Output? current; 15 | 16 | internal ArrayEnumerator(ArrayIterator iterator) 17 | { 18 | this.iterator = iterator; 19 | } 20 | 21 | /// 22 | public Output Current => current!; 23 | 24 | /// 25 | object IEnumerator.Current => current!; 26 | 27 | /// 28 | public void Dispose() 29 | { 30 | iterator.Dispose(); 31 | } 32 | 33 | /// 34 | public bool MoveNext() 35 | { 36 | var handle = ArrayChannel.IteratorNext(iterator.Handle); 37 | 38 | if (handle != nint.Zero) 39 | { 40 | current = Output.CreateAndRelease(handle, iterator.Doc); 41 | return true; 42 | } 43 | 44 | current = null!; 45 | 46 | return false; 47 | } 48 | 49 | /// 50 | public void Reset() 51 | { 52 | throw new NotSupportedException(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /YDotNet/Document/Types/Events/EventChange.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document.Cells; 2 | using YDotNet.Native.Types.Events; 3 | 4 | namespace YDotNet.Document.Types.Events; 5 | 6 | /// 7 | /// Represents a single change applied to a shared data type. 8 | /// 9 | public class EventChange 10 | { 11 | internal EventChange(EventChangeNative native, Doc doc) 12 | { 13 | Length = native.Length; 14 | 15 | Tag = native.TagNative switch 16 | { 17 | EventChangeTagNative.Add => EventChangeTag.Add, 18 | EventChangeTagNative.Remove => EventChangeTag.Remove, 19 | EventChangeTagNative.Retain => EventChangeTag.Retain, 20 | _ => throw new NotSupportedException($"The value \"{native.TagNative}\" for {nameof(EventChangeTagNative)} is not supported."), 21 | }; 22 | 23 | Values = native.ValuesHandles.Select(x => new Output(x, doc, false)).ToList(); 24 | } 25 | 26 | /// 27 | /// Gets the type of change represented by this instance. 28 | /// 29 | public EventChangeTag Tag { get; } 30 | 31 | /// 32 | /// Gets the amount of changes represented by this instance. 33 | /// 34 | public uint Length { get; } 35 | 36 | /// 37 | /// Gets the values that were affected by this change. 38 | /// 39 | public IReadOnlyList Values { get; } 40 | } 41 | -------------------------------------------------------------------------------- /YDotNet.Server.WebSockets/YDotNet.Server.WebSockets.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | all 30 | runtime; build; native; contentfiles; analyzers; buildtransitive 31 | 32 | 33 | all 34 | runtime; build; native; contentfiles; analyzers; buildtransitive 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /YDotNet/Document/Types/Events/EventPathSegment.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Native.Types.Events; 2 | 3 | namespace YDotNet.Document.Types.Events; 4 | 5 | /// 6 | /// Represents a segment of the full path represented by . 7 | /// 8 | public class EventPathSegment 9 | { 10 | internal EventPathSegment(EventPathSegmentNative native) 11 | { 12 | Tag = (EventPathSegmentTag)native.Tag; 13 | 14 | switch (Tag) 15 | { 16 | case EventPathSegmentTag.Key: 17 | Key = native.Key(); 18 | break; 19 | 20 | case EventPathSegmentTag.Index: 21 | Index = (uint)native.KeyOrIndex; 22 | break; 23 | } 24 | } 25 | 26 | /// 27 | /// Gets the value that indicates the kind of data held by this instance. 28 | /// 29 | public EventPathSegmentTag Tag { get; } 30 | 31 | /// 32 | /// Gets the key, if is , or null 33 | /// otherwise. 34 | /// 35 | public string? Key { get; } 36 | 37 | /// 38 | /// Gets the index, if is , or 39 | /// null otherwise. 40 | /// 41 | public uint? Index { get; } 42 | } 43 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/XmlFragments/InsertTextTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | 4 | namespace YDotNet.Tests.Unit.XmlFragments; 5 | 6 | public class InsertTextTests 7 | { 8 | [Test] 9 | public void InsertSingleText() 10 | { 11 | // Arrange 12 | var doc = new Doc(); 13 | var xmlFragment = doc.XmlFragment("xml-fragment"); 14 | 15 | // Act 16 | var transaction = doc.WriteTransaction(); 17 | var xmlText = xmlFragment.InsertText(transaction, index: 0); 18 | var childLength = xmlFragment.ChildLength(transaction); 19 | transaction.Commit(); 20 | 21 | // Assert 22 | Assert.That(xmlText.Handle, Is.Not.EqualTo(nint.Zero)); 23 | Assert.That(childLength, Is.EqualTo(expected: 1)); 24 | } 25 | 26 | [Test] 27 | public void InsertMultipleTexts() 28 | { 29 | // Arrange 30 | var doc = new Doc(); 31 | var xmlFragment = doc.XmlFragment("xml-fragment"); 32 | 33 | // Act 34 | var transaction = doc.WriteTransaction(); 35 | xmlFragment.InsertText(transaction, index: 0); 36 | xmlFragment.InsertText(transaction, index: 0); 37 | xmlFragment.InsertText(transaction, index: 0); 38 | var childLength = xmlFragment.ChildLength(transaction); 39 | transaction.Commit(); 40 | 41 | // Assert 42 | Assert.That(childLength, Is.EqualTo(expected: 3)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Driver/Tasks/Arrays/Length.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document; 2 | using YDotNet.Document.Cells; 3 | using YDotNet.Tests.Driver.Abstractions; 4 | 5 | namespace YDotNet.Tests.Driver.Tasks.Arrays; 6 | 7 | public class Length : ITask 8 | { 9 | public Task Run() 10 | { 11 | var count = 0; 12 | 13 | // Create many documents 14 | while (count < 1_000_000) 15 | { 16 | // After 1s, stop and show the user the amount of documents 17 | if (count > 0) 18 | { 19 | Console.WriteLine("Status Report"); 20 | Console.WriteLine($"\tArrays:\t{count}"); 21 | Console.WriteLine(); 22 | } 23 | 24 | if (count % 1_000 == 0) 25 | { 26 | Thread.Sleep(millisecondsTimeout: 15); 27 | } 28 | 29 | // Create many documents 30 | for (var i = 0; i < 100; i++) 31 | { 32 | var doc = new Doc(); 33 | var array = doc.Array($"sample-{i}"); 34 | 35 | var transaction = doc.WriteTransaction(); 36 | array.InsertRange(transaction, index: 0, new[] { Input.Long(value: 2469L) }); 37 | var _ = array.Length; 38 | transaction.Commit(); 39 | 40 | doc.Dispose(); 41 | count++; 42 | } 43 | } 44 | 45 | return Task.CompletedTask; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Demo/Client/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Driver/Tasks/Texts/Observe.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document; 2 | using YDotNet.Tests.Driver.Abstractions; 3 | 4 | namespace YDotNet.Tests.Driver.Tasks.Texts; 5 | 6 | public class Observe : ITask 7 | { 8 | public Task Run() 9 | { 10 | var count = 0; 11 | 12 | // Create many documents 13 | while (count < 1_000_000) 14 | { 15 | // After 1s, stop and show the user the amount of documents 16 | if (count > 0) 17 | { 18 | Console.WriteLine("Status Report"); 19 | Console.WriteLine($"\tTexts:\t{count}"); 20 | Console.WriteLine(); 21 | } 22 | 23 | if (count % 1_000 == 0) 24 | { 25 | Thread.Sleep(millisecondsTimeout: 15); 26 | } 27 | 28 | // Create many documents 29 | for (var i = 0; i < 100; i++) 30 | { 31 | var doc = new Doc(); 32 | var text = doc.Text($"sample-{i}"); 33 | var subscription = text.Observe(_ => { }); 34 | 35 | var transaction = doc.WriteTransaction(); 36 | text.Insert(transaction, index: 0, "YDotNet"); 37 | transaction.Commit(); 38 | 39 | text.Unobserve(subscription); 40 | doc.Dispose(); 41 | 42 | count++; 43 | } 44 | } 45 | 46 | return Task.CompletedTask; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /YDotNet/Document/Types/Arrays/ArrayIterator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using YDotNet.Document.Cells; 3 | using YDotNet.Infrastructure; 4 | using YDotNet.Native.Types; 5 | 6 | namespace YDotNet.Document.Types.Arrays; 7 | 8 | /// 9 | /// Represents an iterator, which can be used to traverse over all elements of an . 10 | /// 11 | /// 12 | /// The iterator can't be reused. If needed, use to accumulate values. 13 | /// 14 | public class ArrayIterator : UnmanagedResource, IEnumerable 15 | { 16 | internal ArrayIterator(nint handle, Doc doc) 17 | : base(handle) 18 | { 19 | Doc = doc; 20 | } 21 | 22 | internal Doc Doc { get; } 23 | 24 | /// 25 | public IEnumerator GetEnumerator() 26 | { 27 | ThrowIfDisposed(); 28 | return new ArrayEnumerator(this); 29 | } 30 | 31 | /// 32 | IEnumerator IEnumerable.GetEnumerator() 33 | { 34 | return GetEnumerator(); 35 | } 36 | 37 | /// 38 | /// Finalizes an instance of the class. 39 | /// 40 | ~ArrayIterator() 41 | { 42 | Dispose(disposing: false); 43 | } 44 | 45 | /// 46 | protected override void DisposeCore(bool disposing) 47 | { 48 | ArrayChannel.IteratorDestroy(Handle); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Demo/Client/src/components/Increment.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Button, Col, Input, Row } from 'reactstrap'; 3 | import { useYjs } from '../hooks/useYjs'; 4 | 5 | export const Increment = () => { 6 | const { yjsDocument } = useYjs(); 7 | const map = yjsDocument.getMap('increment'); 8 | const [state, setState] = React.useState(0); 9 | 10 | React.useEffect(() => { 11 | const handler = () => { 12 | setState((map.get('value') as number) || 0); 13 | }; 14 | 15 | handler(); 16 | map.observeDeep(handler); 17 | 18 | return () => { 19 | map.unobserveDeep(handler); 20 | }; 21 | }, [map]); 22 | 23 | React.useEffect(() => { 24 | yjsDocument.transact(() => { 25 | map.set('value', state); 26 | }); 27 | }, [map, state, yjsDocument]); 28 | 29 | const _increment = () => { 30 | setState((v) => v + 1); 31 | }; 32 | 33 | const _decrement = () => { 34 | setState((v) => v - 1); 35 | }; 36 | 37 | return ( 38 | 39 | 40 | -1 41 | 42 | 43 | 44 | 45 | 46 | +1 47 | 48 | 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/UndoManagers/CanRedoTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | using YDotNet.Document.UndoManagers; 4 | 5 | namespace YDotNet.Tests.Unit.UndoManagers; 6 | 7 | public class CanRedoTests 8 | { 9 | [Test] 10 | public void CanNotRedoInitially() 11 | { 12 | // Arrange 13 | var doc = new Doc(); 14 | var text = doc.Text("text"); 15 | var undoManager = new UndoManager(doc, text, new UndoManagerOptions { CaptureTimeoutMilliseconds = 0 }); 16 | 17 | // Act 18 | var canUndo = undoManager.CanUndo(); 19 | 20 | // Assert 21 | Assert.That(canUndo, Is.False); 22 | } 23 | 24 | [Test] 25 | public void CanRedoAfterUndoingChanges() 26 | { 27 | // Arrange 28 | var doc = new Doc(); 29 | var text = doc.Text("text"); 30 | var undoManager = new UndoManager(doc, text, new UndoManagerOptions { CaptureTimeoutMilliseconds = 0 }); 31 | 32 | // Act 33 | var transaction = doc.WriteTransaction(); 34 | text.Insert(transaction, index: 0, "Lucas"); 35 | transaction.Commit(); 36 | var canRedo = undoManager.CanRedo(); 37 | 38 | // Assert 39 | Assert.That(canRedo, Is.False); 40 | 41 | // Act 42 | var undo = undoManager.Undo(); 43 | canRedo = undoManager.CanRedo(); 44 | 45 | // Assert 46 | Assert.That(undo, Is.True); 47 | Assert.That(canRedo, Is.True); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/UndoManagers/CanUndoTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | using YDotNet.Document.UndoManagers; 4 | 5 | namespace YDotNet.Tests.Unit.UndoManagers; 6 | 7 | public class CanUndoTests 8 | { 9 | [Test] 10 | public void CanNotUndoInitially() 11 | { 12 | // Arrange 13 | var doc = new Doc(); 14 | var text = doc.Text("text"); 15 | var undoManager = new UndoManager(doc, text, new UndoManagerOptions { CaptureTimeoutMilliseconds = 0 }); 16 | 17 | // Act 18 | var canUndo = undoManager.CanUndo(); 19 | 20 | // Assert 21 | Assert.That(canUndo, Is.False); 22 | } 23 | 24 | [Test] 25 | public void CanUndoAfterApplyingChanges() 26 | { 27 | // Arrange 28 | var doc = new Doc(); 29 | var text = doc.Text("text"); 30 | var undoManager = new UndoManager(doc, text, new UndoManagerOptions { CaptureTimeoutMilliseconds = 0 }); 31 | 32 | // Act 33 | var transaction = doc.WriteTransaction(); 34 | text.Insert(transaction, index: 0, "Lucas"); 35 | transaction.Commit(); 36 | var canUndo = undoManager.CanUndo(); 37 | 38 | // Assert 39 | Assert.That(canUndo, Is.True); 40 | 41 | // Act 42 | var undo = undoManager.Undo(); 43 | canUndo = undoManager.CanUndo(); 44 | 45 | // Assert 46 | Assert.That(undo, Is.True); 47 | Assert.That(canUndo, Is.False); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Driver/Tasks/Texts/Length.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document; 2 | using YDotNet.Tests.Driver.Abstractions; 3 | 4 | namespace YDotNet.Tests.Driver.Tasks.Texts; 5 | 6 | public class Length : ITask 7 | { 8 | public Task Run() 9 | { 10 | var count = 0; 11 | 12 | // Create many documents 13 | while (count < 1_000_000) 14 | { 15 | // After 1s, stop and show the user the amount of documents 16 | if (count > 0) 17 | { 18 | Console.WriteLine("Status Report"); 19 | Console.WriteLine($"\tTexts:\t{count}"); 20 | Console.WriteLine(); 21 | } 22 | 23 | if (count % 1_000 == 0) 24 | { 25 | Thread.Sleep(millisecondsTimeout: 15); 26 | } 27 | 28 | // Create many documents 29 | for (var i = 0; i < 100; i++) 30 | { 31 | var doc = new Doc(); 32 | var text = doc.Text($"sample-{i}"); 33 | 34 | var transaction = doc.WriteTransaction(); 35 | text.Insert(transaction, index: 0, "YDotNet"); 36 | transaction.Commit(); 37 | 38 | transaction = doc.ReadTransaction(); 39 | text.Length(transaction); 40 | transaction.Commit(); 41 | 42 | doc.Dispose(); 43 | 44 | count++; 45 | } 46 | } 47 | 48 | return Task.CompletedTask; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Driver/Tasks/Texts/String.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document; 2 | using YDotNet.Tests.Driver.Abstractions; 3 | 4 | namespace YDotNet.Tests.Driver.Tasks.Texts; 5 | 6 | public class String : ITask 7 | { 8 | public Task Run() 9 | { 10 | var count = 0; 11 | 12 | // Create many documents 13 | while (count < 1_000_000) 14 | { 15 | // After 1s, stop and show the user the amount of documents 16 | if (count > 0) 17 | { 18 | Console.WriteLine("Status Report"); 19 | Console.WriteLine($"\tTexts:\t{count}"); 20 | Console.WriteLine(); 21 | } 22 | 23 | if (count % 1_000 == 0) 24 | { 25 | Thread.Sleep(millisecondsTimeout: 15); 26 | } 27 | 28 | // Create many documents 29 | for (var i = 0; i < 100; i++) 30 | { 31 | var doc = new Doc(); 32 | var text = doc.Text($"sample-{i}"); 33 | 34 | var transaction = doc.WriteTransaction(); 35 | text.Insert(transaction, index: 0, "YDotNet"); 36 | transaction.Commit(); 37 | 38 | transaction = doc.ReadTransaction(); 39 | text.String(transaction); 40 | transaction.Commit(); 41 | 42 | doc.Dispose(); 43 | 44 | count++; 45 | } 46 | } 47 | 48 | return Task.CompletedTask; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/StickyIndexes/AssociationTypeTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | using YDotNet.Document.StickyIndexes; 4 | 5 | namespace YDotNet.Tests.Unit.StickyIndexes; 6 | 7 | public class AssociationTypeTests 8 | { 9 | [Test] 10 | public void ReturnsCorrectlyWithBefore() 11 | { 12 | // Arrange 13 | var doc = new Doc(); 14 | var text = doc.Text("text"); 15 | 16 | var transaction = doc.WriteTransaction(); 17 | text.Insert(transaction, index: 0, "Lucas"); 18 | var stickyIndex = text.StickyIndex(transaction, index: 3, StickyAssociationType.Before); 19 | transaction.Commit(); 20 | 21 | // Act 22 | var associationType = stickyIndex.AssociationType; 23 | 24 | // Assert 25 | Assert.That(associationType, Is.EqualTo(StickyAssociationType.Before)); 26 | } 27 | 28 | [Test] 29 | public void ReturnsCorrectlyWithAfter() 30 | { 31 | // Arrange 32 | var doc = new Doc(); 33 | var text = doc.Text("text"); 34 | 35 | var transaction = doc.WriteTransaction(); 36 | text.Insert(transaction, index: 0, "Lucas"); 37 | var stickyIndex = text.StickyIndex(transaction, index: 3, StickyAssociationType.After); 38 | transaction.Commit(); 39 | 40 | // Act 41 | var associationType = stickyIndex.AssociationType; 42 | 43 | // Assert 44 | Assert.That(associationType, Is.EqualTo(StickyAssociationType.After)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Texts/LengthTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | 4 | namespace YDotNet.Tests.Unit.Texts; 5 | 6 | public class LengthTests 7 | { 8 | [Test] 9 | public void LengthIsInitiallyZero() 10 | { 11 | // Arrange 12 | var doc = new Doc(); 13 | var text = doc.Text("name"); 14 | 15 | // Act 16 | var transaction = doc.ReadTransaction(); 17 | var length = text.Length(transaction); 18 | 19 | // Assert 20 | Assert.That(length, Is.EqualTo(expected: 0)); 21 | } 22 | 23 | [Test] 24 | public void LengthIsCorrectForAlphanumericCharacters() 25 | { 26 | // Arrange 27 | var doc = new Doc(); 28 | var text = doc.Text("name"); 29 | 30 | // Act 31 | var transaction = doc.WriteTransaction(); 32 | text.Insert(transaction, index: 0, "Lucas"); 33 | var length = text.Length(transaction); 34 | 35 | // Assert 36 | Assert.That(length, Is.EqualTo(expected: 5)); 37 | } 38 | 39 | [Test] 40 | public void LengthIsCorrectForSpecialCharacters() 41 | { 42 | // Arrange 43 | var doc = new Doc(); 44 | var text = doc.Text("name"); 45 | 46 | // Act 47 | var transaction = doc.WriteTransaction(); 48 | text.Insert(transaction, index: 0, "Earth 🌎☀️🌕⭐"); 49 | var length = text.Length(transaction); 50 | 51 | // Assert 52 | Assert.That(length, Is.EqualTo(expected: 13)); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Driver/Tasks/Docs/ObserveAfterTransaction.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document; 2 | using YDotNet.Tests.Driver.Abstractions; 3 | 4 | namespace YDotNet.Tests.Driver.Tasks.Docs; 5 | 6 | public class ObserveAfterTransaction : ITask 7 | { 8 | public Task Run() 9 | { 10 | var count = 0; 11 | 12 | // Create many documents 13 | while (count < 1_000_000) 14 | { 15 | // After 1s, stop and show the user the amount of documents 16 | if (count > 0) 17 | { 18 | Console.WriteLine("Status Report"); 19 | Console.WriteLine($"\tDocs:\t{count}"); 20 | Console.WriteLine(); 21 | } 22 | 23 | if (count % 1_000 == 0) 24 | { 25 | Thread.Sleep(millisecondsTimeout: 15); 26 | } 27 | 28 | // Create many documents 29 | for (var i = 0; i < 100; i++) 30 | { 31 | var doc = new Doc(); 32 | var text = doc.Text($"sample-{i}"); 33 | var subscription = doc.ObserveAfterTransaction(_ => { }); 34 | 35 | var transaction = doc.WriteTransaction(); 36 | text.Insert(transaction, index: 0, "YDotNet"); 37 | transaction.Commit(); 38 | 39 | doc.UnobserveAfterTransaction(subscription); 40 | doc.Dispose(); 41 | 42 | count++; 43 | } 44 | } 45 | 46 | return Task.CompletedTask; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/UndoManagers/RemoveOriginTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | using YDotNet.Document.UndoManagers; 4 | 5 | namespace YDotNet.Tests.Unit.UndoManagers; 6 | 7 | public class RemoveOriginTests 8 | { 9 | [Test] 10 | public void TracksAssignedOriginUntilRemoval() 11 | { 12 | // Arrange 13 | var doc = new Doc(); 14 | var text = doc.Text("text"); 15 | var undoManager = new UndoManager(doc, text, new UndoManagerOptions { CaptureTimeoutMilliseconds = 0 }); 16 | undoManager.AddOrigin(new byte[] { 0 }); 17 | 18 | var called = 0; 19 | undoManager.ObserveAdded(_ => called++); 20 | 21 | // Act 22 | var transaction = doc.WriteTransaction(new byte[] { 0 }); 23 | text.Insert(transaction, index: 0, "Lucas"); 24 | transaction.Commit(); 25 | 26 | // Assert 27 | Assert.That(called, Is.EqualTo(expected: 1)); 28 | 29 | // Act 30 | undoManager.RemoveOrigin(new byte[] { 0 }); 31 | transaction = doc.WriteTransaction(new byte[] { 0 }); 32 | text.Insert(transaction, index: 5, " Viana"); 33 | transaction.Commit(); 34 | 35 | // Assert 36 | Assert.That(called, Is.EqualTo(expected: 1)); 37 | 38 | // Act 39 | transaction = doc.WriteTransaction(); 40 | text.Insert(transaction, index: 5, " Viana"); 41 | transaction.Commit(); 42 | 43 | // Assert 44 | Assert.That(called, Is.EqualTo(expected: 2)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/Maps/RemoveTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | using YDotNet.Document.Cells; 4 | using YDotNet.Document.Transactions; 5 | using YDotNet.Document.Types.Maps; 6 | 7 | namespace YDotNet.Tests.Unit.Maps; 8 | 9 | public class RemoveTests 10 | { 11 | [Test] 12 | public void ReturnsTrueWhenRemovingValidItems() 13 | { 14 | // Arrange 15 | var (map, transaction) = ArrangeMap(); 16 | 17 | // Act 18 | var result1 = map.Remove(transaction, "value1"); 19 | var result2 = map.Remove(transaction, "value2"); 20 | 21 | // Assert 22 | Assert.That(result1, Is.True); 23 | Assert.That(result2, Is.True); 24 | } 25 | 26 | [Test] 27 | public void ReturnsFalseWhenRemovingInvalidItems() 28 | { 29 | // Arrange 30 | var (map, transaction) = ArrangeMap(); 31 | 32 | // Act 33 | var result1 = map.Remove(transaction, ""); 34 | var result2 = map.Remove(transaction, "xxx"); 35 | 36 | // Assert 37 | Assert.That(result1, Is.False); 38 | Assert.That(result2, Is.False); 39 | } 40 | 41 | private (Map, Transaction) ArrangeMap() 42 | { 43 | var doc = new Doc(); 44 | var map = doc.Map("map"); 45 | var transaction = doc.WriteTransaction(); 46 | 47 | map.Insert(transaction, "value1", Input.Long(value: 2469L)); 48 | map.Insert(transaction, "value2", Input.Long(value: -420L)); 49 | 50 | return (map, transaction); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Unit/XmlFragments/ChildLengthTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using YDotNet.Document; 3 | 4 | namespace YDotNet.Tests.Unit.XmlFragments; 5 | 6 | public class ChildLengthTests 7 | { 8 | [Test] 9 | public void InitialLengthIsZero() 10 | { 11 | // Arrange 12 | var doc = new Doc(); 13 | var xmlFragment = doc.XmlFragment("xml-fragment"); 14 | 15 | // Act 16 | var transaction = doc.ReadTransaction(); 17 | var childLength = xmlFragment.ChildLength(transaction); 18 | transaction.Commit(); 19 | 20 | // Assert 21 | Assert.That(childLength, Is.EqualTo(expected: 0)); 22 | } 23 | 24 | [Test] 25 | public void ChildLengthMatchesAmountOfChildrenAdded() 26 | { 27 | // Arrange 28 | var doc = new Doc(); 29 | var xmlFragment = doc.XmlFragment("xml-fragment"); 30 | 31 | var transaction = doc.WriteTransaction(); 32 | xmlFragment.InsertElement(transaction, index: 0, "xml-element-child-1"); 33 | xmlFragment.InsertElement(transaction, index: 1, "xml-element-child-2"); 34 | xmlFragment.InsertText(transaction, index: 2); 35 | xmlFragment.InsertText(transaction, index: 3); 36 | transaction.Commit(); 37 | 38 | // Act 39 | transaction = doc.ReadTransaction(); 40 | var childLength = xmlFragment.ChildLength(transaction); 41 | transaction.Commit(); 42 | 43 | // Assert 44 | Assert.That(childLength, Is.EqualTo(expected: 4)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Driver/Tasks/Arrays/Observe.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document; 2 | using YDotNet.Document.Cells; 3 | using YDotNet.Tests.Driver.Abstractions; 4 | 5 | namespace YDotNet.Tests.Driver.Tasks.Arrays; 6 | 7 | public class Observe : ITask 8 | { 9 | public Task Run() 10 | { 11 | var count = 0; 12 | 13 | // Create many documents 14 | while (count < 1_000_000) 15 | { 16 | // After 1s, stop and show the user the amount of documents 17 | if (count > 0) 18 | { 19 | Console.WriteLine("Status Report"); 20 | Console.WriteLine($"\tArrays:\t{count}"); 21 | Console.WriteLine(); 22 | } 23 | 24 | if (count % 1_000 == 0) 25 | { 26 | Thread.Sleep(millisecondsTimeout: 15); 27 | } 28 | 29 | // Create many documents 30 | for (var i = 0; i < 100; i++) 31 | { 32 | var doc = new Doc(); 33 | var array = doc.Array($"sample-{i}"); 34 | 35 | var subscription = array.Observe(_ => { }); 36 | 37 | var transaction = doc.WriteTransaction(); 38 | array.InsertRange(transaction, index: 0, new[] { Input.Long(value: 2469L) }); 39 | transaction.Commit(); 40 | 41 | array.Unobserve(subscription); 42 | doc.Dispose(); 43 | count++; 44 | } 45 | } 46 | 47 | return Task.CompletedTask; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Driver/Tasks/Texts/RemoveText.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document; 2 | using YDotNet.Tests.Driver.Abstractions; 3 | 4 | namespace YDotNet.Tests.Driver.Tasks.Texts; 5 | 6 | public class RemoveText : ITask 7 | { 8 | public Task Run() 9 | { 10 | var count = 0; 11 | 12 | // Create many documents 13 | while (count < 1_000_000) 14 | { 15 | // After 1s, stop and show the user the amount of documents 16 | if (count > 0) 17 | { 18 | Console.WriteLine("Status Report"); 19 | Console.WriteLine($"\tTexts:\t{count}"); 20 | Console.WriteLine(); 21 | } 22 | 23 | if (count % 1_000 == 0) 24 | { 25 | Thread.Sleep(millisecondsTimeout: 15); 26 | } 27 | 28 | // Create many documents 29 | for (var i = 0; i < 100; i++) 30 | { 31 | var doc = new Doc(); 32 | var text = doc.Text($"sample-{i}"); 33 | 34 | var transaction = doc.WriteTransaction(); 35 | text.Insert(transaction, index: 0, "YDotNet"); 36 | transaction.Commit(); 37 | 38 | transaction = doc.WriteTransaction(); 39 | text.RemoveRange(transaction, index: 0, length: 7); 40 | transaction.Commit(); 41 | 42 | doc.Dispose(); 43 | 44 | count++; 45 | } 46 | } 47 | 48 | return Task.CompletedTask; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Tests/YDotNet.Tests.Driver/Tasks/Texts/Chunks.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document; 2 | using YDotNet.Tests.Driver.Abstractions; 3 | 4 | namespace YDotNet.Tests.Driver.Tasks.Texts; 5 | 6 | public class Chunks : ITask 7 | { 8 | public Task Run() 9 | { 10 | var count = 0; 11 | 12 | // Create many documents 13 | while (count < 1_000_000) 14 | { 15 | // After 1s, stop and show the user the amount of documents 16 | if (count > 0) 17 | { 18 | Console.WriteLine("Status Report"); 19 | Console.WriteLine($"\tTexts:\t{count}"); 20 | Console.WriteLine(); 21 | } 22 | 23 | if (count % 1_000 == 0) 24 | { 25 | Thread.Sleep(millisecondsTimeout: 15); 26 | } 27 | 28 | // Create many documents 29 | for (var i = 0; i < 100; i++) 30 | { 31 | var doc = new Doc(); 32 | var text = doc.Text($"sample-{i}"); 33 | 34 | var transaction = doc.WriteTransaction(); 35 | text.Insert(transaction, index: 0, "YDotNet"); 36 | transaction.Commit(); 37 | 38 | transaction = doc.ReadTransaction(); 39 | var chunks = text.Chunks(transaction); 40 | transaction.Commit(); 41 | 42 | chunks.Dispose(); 43 | doc.Dispose(); 44 | 45 | count++; 46 | } 47 | } 48 | 49 | return Task.CompletedTask; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalker.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using YDotNet.Document.Cells; 3 | using YDotNet.Document.Types.XmlTexts; 4 | using YDotNet.Infrastructure; 5 | using YDotNet.Native.Types; 6 | 7 | namespace YDotNet.Document.Types.XmlElements.Trees; 8 | 9 | /// 10 | /// Returns an iterator over a nested recursive nodes of an . 11 | /// 12 | /// 13 | /// The traverses values using depth-first and nodes can be either 14 | /// or nodes. 15 | /// 16 | public class XmlTreeWalker : UnmanagedResource, IEnumerable 17 | { 18 | internal XmlTreeWalker(nint handle, Doc doc) 19 | : base(handle) 20 | { 21 | Doc = doc; 22 | } 23 | 24 | /// 25 | /// Finalizes an instance of the class. 26 | /// 27 | ~XmlTreeWalker() 28 | { 29 | Dispose(disposing: false); 30 | } 31 | 32 | internal Doc Doc { get; } 33 | 34 | /// 35 | public IEnumerator GetEnumerator() 36 | { 37 | return new XmlTreeWalkerEnumerator(this); 38 | } 39 | 40 | /// 41 | IEnumerator IEnumerable.GetEnumerator() 42 | { 43 | return GetEnumerator(); 44 | } 45 | 46 | /// 47 | protected override void DisposeCore(bool disposing) 48 | { 49 | XmlElementChannel.TreeWalkerDestroy(Handle); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /YDotNet/Document/Types/Events/EventBranchTag.cs: -------------------------------------------------------------------------------- 1 | using YDotNet.Document.Types.Arrays.Events; 2 | using YDotNet.Document.Types.Maps.Events; 3 | using YDotNet.Document.Types.Texts.Events; 4 | using YDotNet.Document.Types.XmlElements.Events; 5 | using YDotNet.Document.Types.XmlFragments.Events; 6 | using YDotNet.Document.Types.XmlTexts.Events; 7 | using YDotNet.Native.Types.Branches; 8 | 9 | namespace YDotNet.Document.Types.Events; 10 | 11 | /// 12 | /// Represents the type of event that this generic holds. 13 | /// 14 | public enum EventBranchTag : sbyte 15 | { 16 | /// 17 | /// This event holds an instance. 18 | /// 19 | Array = BranchKind.Array, 20 | 21 | /// 22 | /// This event holds an instance. 23 | /// 24 | Map = BranchKind.Map, 25 | 26 | /// 27 | /// This event holds an instance. 28 | /// 29 | Text = BranchKind.Text, 30 | 31 | /// 32 | /// This event holds an instance. 33 | /// 34 | XmlElement = BranchKind.XmlElement, 35 | 36 | /// 37 | /// This event holds an instance. 38 | /// 39 | XmlText = BranchKind.XmlText, 40 | 41 | /// 42 | /// This event holds an instance. 43 | /// 44 | XmlFragment = BranchKind.XmlFragment 45 | } 46 | -------------------------------------------------------------------------------- /Demo/Client/src/components/YjsTldrawEditor.tsx: -------------------------------------------------------------------------------- 1 | import { Tldraw, track, useEditor } from '@tldraw/tldraw'; 2 | import '@tldraw/tldraw/tldraw.css'; 3 | import { useYjsTldrawStore } from '../hooks/useTldrawStore'; 4 | 5 | /* 6 | See tldraw docs for more info 7 | https://tldraw.dev/docs/collaboration 8 | https://github.com/tldraw/tldraw-yjs-example 9 | */ 10 | const NameEditor = track(() => { 11 | const editor = useEditor(); 12 | 13 | const { color, name } = editor.user.getUserPreferences(); 14 | 15 | return ( 16 | 17 | { 21 | editor.user.updateUserPreferences({ 22 | color: e.currentTarget.value, 23 | }); 24 | }} 25 | /> 26 | { 29 | editor.user.updateUserPreferences({ 30 | name: e.currentTarget.value, 31 | }); 32 | }} 33 | /> 34 | 35 | ); 36 | }); 37 | 38 | const YjsTldrawEditor = () => { 39 | const store = useYjsTldrawStore(); 40 | 41 | return ( 42 | 49 | ); 50 | }; 51 | 52 | export default YjsTldrawEditor; 53 | -------------------------------------------------------------------------------- /YDotNet/Infrastructure/TypeCache.cs: -------------------------------------------------------------------------------- 1 | namespace YDotNet.Infrastructure; 2 | 3 | internal class TypeCache 4 | { 5 | private readonly Dictionary> cache = new(); 6 | 7 | public T GetOrAdd(nint handle, Func factory) 8 | where T : UnmanagedResource 9 | { 10 | if (handle == nint.Zero) 11 | { 12 | throw new ArgumentException("Cannot create object for null handle.", nameof(handle)); 13 | } 14 | 15 | Cleanup(); 16 | 17 | if (cache.TryGetValue(handle, out var weakRef) && weakRef.TryGetTarget(out var item)) 18 | { 19 | if (item is not T typed) 20 | { 21 | throw new YDotNetException($"Expected {typeof(T)}, got {item.GetType()}"); 22 | } 23 | 24 | return typed; 25 | } 26 | 27 | var typedItem = factory(handle); 28 | 29 | cache[handle] = new WeakReference(typedItem); 30 | 31 | return typedItem; 32 | } 33 | 34 | private void Cleanup() 35 | { 36 | List? keysToDelete = null; 37 | 38 | foreach (var (key, weakRef) in cache) 39 | { 40 | if (!weakRef.TryGetTarget(out _)) 41 | { 42 | keysToDelete ??= new List(); 43 | keysToDelete.Add(key); 44 | } 45 | } 46 | 47 | if (keysToDelete != null) 48 | { 49 | foreach (var key in keysToDelete) 50 | { 51 | cache.Remove(key); 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Demo/Client/src/components/YjsProseMirror.tsx: -------------------------------------------------------------------------------- 1 | import * as 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 { exampleSetup } from 'prosemirror-example-setup'; 7 | import { 8 | ySyncPlugin, 9 | yCursorPlugin, 10 | yUndoPlugin, 11 | undo, 12 | redo, 13 | } from 'y-prosemirror'; 14 | import { useYjs } from '../hooks/useYjs'; 15 | 16 | export const YjsProseMirror = () => { 17 | const { yjsDocument, yjsConnector } = useYjs(); 18 | const yText = yjsDocument.getXmlFragment('prosemirror'); 19 | const viewHost = React.useRef(null); 20 | const viewRef = React.useRef(null); 21 | 22 | React.useEffect(() => { 23 | const state = EditorState.create({ 24 | schema, 25 | plugins: [ 26 | ySyncPlugin(yText), 27 | yCursorPlugin(yjsConnector.awareness), 28 | yUndoPlugin(), 29 | keymap({ 30 | 'Mod-z': undo, 31 | 'Mod-y': redo, 32 | 'Mod-Shift-z': redo, 33 | }), 34 | ].concat(exampleSetup({ schema })), 35 | }); 36 | 37 | const editor = new EditorView(viewHost.current, { state }); 38 | 39 | viewRef.current = editor; 40 | 41 | return () => { 42 | editor.destroy(); 43 | }; 44 | }, [yText, yjsConnector.awareness]); 45 | 46 | return ; 47 | }; 48 | --------------------------------------------------------------------------------