From 977dd60853dbab4348b9846d5617892ada64dd32 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 1 Oct 2023 17:34:51 +0200 Subject: [PATCH 001/186] Server project. --- YDotNet.Server/DefaultDocumentManager.cs | 62 ++++++++++++++++++++ YDotNet.Server/DocumentManagerOptions.cs | 6 ++ YDotNet.Server/IDocumentManager.cs | 9 +++ YDotNet.Server/ServiceExtensions.cs | 7 +++ YDotNet.Server/Storage/IDocumentStorage.cs | 12 ++++ YDotNet.Server/Storage/InMemoryDocStorage.cs | 25 ++++++++ YDotNet.Server/UpdateResult.cs | 12 ++++ YDotNet.Server/YDotNet.Server.csproj | 9 +++ 8 files changed, 142 insertions(+) create mode 100644 YDotNet.Server/DefaultDocumentManager.cs create mode 100644 YDotNet.Server/DocumentManagerOptions.cs create mode 100644 YDotNet.Server/IDocumentManager.cs create mode 100644 YDotNet.Server/ServiceExtensions.cs create mode 100644 YDotNet.Server/Storage/IDocumentStorage.cs create mode 100644 YDotNet.Server/Storage/InMemoryDocStorage.cs create mode 100644 YDotNet.Server/UpdateResult.cs create mode 100644 YDotNet.Server/YDotNet.Server.csproj diff --git a/YDotNet.Server/DefaultDocumentManager.cs b/YDotNet.Server/DefaultDocumentManager.cs new file mode 100644 index 00000000..04135e45 --- /dev/null +++ b/YDotNet.Server/DefaultDocumentManager.cs @@ -0,0 +1,62 @@ +using Microsoft.Extensions.Options; +using YDotNet.Document; +using YDotNet.Server.Storage; + +namespace YDotNet.Server; + +public sealed class DefaultDocumentManager : IDocumentManager +{ + private readonly IDocumentStorage documentStorage; + private readonly DocumentManagerOptions options; + + public DefaultDocumentManager(IDocumentStorage documentStorage, + IOptions options) + { + this.documentStorage = documentStorage; + this.options = options.Value; + } + + public ValueTask GetDocAsync(string name, + CancellationToken ct = default) + { + throw new NotImplementedException(); + } + + public async ValueTask ApplyUpdateAsync(string name, byte[] stateDiff, object metadata, + CancellationToken ct) + { + var doc = await documentStorage.GetDocAsync(name, ct); + + if (doc == null) + { + if (options.AutoCreateDocument) + { + doc = new Doc(); + } + else + { + return new UpdateResult + { + IsSkipped = true + }; + } + } + + var result = new UpdateResult(); + + using (var transaction = doc.WriteTransaction()) + { + if (transaction == null) + { + throw new InvalidOperationException("Transaction cannot be created."); + } + + result.TransactionUpdateResult = transaction.ApplyV2(stateDiff); + transaction.Commit(); + + result.Update = stateDiff; + } + + return result; + } +} diff --git a/YDotNet.Server/DocumentManagerOptions.cs b/YDotNet.Server/DocumentManagerOptions.cs new file mode 100644 index 00000000..7efca8cb --- /dev/null +++ b/YDotNet.Server/DocumentManagerOptions.cs @@ -0,0 +1,6 @@ +namespace YDotNet.Server; + +public sealed class DocumentManagerOptions +{ + public bool AutoCreateDocument { get; set; } = true; +} diff --git a/YDotNet.Server/IDocumentManager.cs b/YDotNet.Server/IDocumentManager.cs new file mode 100644 index 00000000..fb4dffca --- /dev/null +++ b/YDotNet.Server/IDocumentManager.cs @@ -0,0 +1,9 @@ +using YDotNet.Document; + +namespace YDotNet.Server; + +public interface IDocumentManager +{ + ValueTask GetDocAsync(string name, + CancellationToken ct = default); +} diff --git a/YDotNet.Server/ServiceExtensions.cs b/YDotNet.Server/ServiceExtensions.cs new file mode 100644 index 00000000..54b4d41e --- /dev/null +++ b/YDotNet.Server/ServiceExtensions.cs @@ -0,0 +1,7 @@ +namespace YDotNet.Server +{ + public class Class1 + { + + } +} \ No newline at end of file diff --git a/YDotNet.Server/Storage/IDocumentStorage.cs b/YDotNet.Server/Storage/IDocumentStorage.cs new file mode 100644 index 00000000..678ced46 --- /dev/null +++ b/YDotNet.Server/Storage/IDocumentStorage.cs @@ -0,0 +1,12 @@ +using YDotNet.Document; + +namespace YDotNet.Server.Storage; + +public interface IDocumentStorage +{ + ValueTask GetDocAsync(string name, + CancellationToken ct = default); + + ValueTask StoreDocAsync(string name, Doc doc, + CancellationToken ct = default); +} diff --git a/YDotNet.Server/Storage/InMemoryDocStorage.cs b/YDotNet.Server/Storage/InMemoryDocStorage.cs new file mode 100644 index 00000000..7f90c053 --- /dev/null +++ b/YDotNet.Server/Storage/InMemoryDocStorage.cs @@ -0,0 +1,25 @@ +using System.Collections.Concurrent; +using YDotNet.Document; + +namespace YDotNet.Server.Storage; + +public sealed class InMemoryDocStorage : IDocumentStorage +{ + private readonly ConcurrentDictionary docs = new(); + + public ValueTask GetDocAsync(string name, + CancellationToken ct = default) + { + docs.TryGetValue(name, out var doc); + + return new ValueTask(doc); + } + + public ValueTask StoreDocAsync(string name, Doc doc, + CancellationToken ct = default) + { + docs[name] = doc; + + return default; + } +} diff --git a/YDotNet.Server/UpdateResult.cs b/YDotNet.Server/UpdateResult.cs new file mode 100644 index 00000000..258e1bef --- /dev/null +++ b/YDotNet.Server/UpdateResult.cs @@ -0,0 +1,12 @@ +using YDotNet.Document.Transactions; + +namespace YDotNet.Server; + +public sealed class UpdateResult +{ + public TransactionUpdateResult TransactionUpdateResult { get; set; } + + public bool IsSkipped { get; set; } + + public byte[]? Update { get; set; } +} diff --git a/YDotNet.Server/YDotNet.Server.csproj b/YDotNet.Server/YDotNet.Server.csproj new file mode 100644 index 00000000..cfadb03d --- /dev/null +++ b/YDotNet.Server/YDotNet.Server.csproj @@ -0,0 +1,9 @@ + + + + net7.0 + enable + enable + + + From c7ccc4644c99d7b76ab05d227605614f92fc7313 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 1 Oct 2023 18:49:16 +0200 Subject: [PATCH 002/186] Cache documents. --- YDotNet.Server/DefaultDocumentManager.cs | 78 ++++++---- YDotNet.Server/DocumentContext.cs | 141 +++++++++++++++++++ YDotNet.Server/DocumentContextCache.cs | 53 +++++++ YDotNet.Server/DocumentManagerOptions.cs | 6 + YDotNet.Server/IDocumentManager.cs | 2 - YDotNet.Server/ServiceExtensions.cs | 6 +- YDotNet.Server/Storage/IDocumentStorage.cs | 4 +- YDotNet.Server/Storage/InMemoryDocStorage.cs | 8 +- YDotNet.Server/YDotNet.Server.csproj | 14 ++ YDotNet.sln | 22 ++- 10 files changed, 290 insertions(+), 44 deletions(-) create mode 100644 YDotNet.Server/DocumentContext.cs create mode 100644 YDotNet.Server/DocumentContextCache.cs diff --git a/YDotNet.Server/DefaultDocumentManager.cs b/YDotNet.Server/DefaultDocumentManager.cs index 04135e45..29dbc850 100644 --- a/YDotNet.Server/DefaultDocumentManager.cs +++ b/YDotNet.Server/DefaultDocumentManager.cs @@ -1,62 +1,80 @@ using Microsoft.Extensions.Options; using YDotNet.Document; +using YDotNet.Document.Transactions; using YDotNet.Server.Storage; namespace YDotNet.Server; public sealed class DefaultDocumentManager : IDocumentManager { - private readonly IDocumentStorage documentStorage; - private readonly DocumentManagerOptions options; + private DocumentContextCache contexts; public DefaultDocumentManager(IDocumentStorage documentStorage, IOptions options) { - this.documentStorage = documentStorage; - this.options = options.Value; + contexts = new DocumentContextCache(documentStorage, options.Value); } - public ValueTask GetDocAsync(string name, - CancellationToken ct = default) + public async ValueTask GetMissingChanges(string name, byte[] stateVector) { - throw new NotImplementedException(); + var context = contexts.GetContext(name); + + return await context.ApplyUpdateReturnAsync(doc => + { + using (var transaction = doc.ReadTransaction()) + { + if (transaction == null) + { + throw new InvalidOperationException("Transaction cannot be created."); + } + + return transaction.StateDiffV2(stateVector); + } + }); } - public async ValueTask ApplyUpdateAsync(string name, byte[] stateDiff, object metadata, - CancellationToken ct) + public async ValueTask ApplyUpdateAsync(string name, byte[] stateDiff, object metadata) { - var doc = await documentStorage.GetDocAsync(name, ct); + var context = contexts.GetContext(name); - if (doc == null) + return await context.ApplyUpdateReturnAsync(doc => { - if (options.AutoCreateDocument) + var result = new UpdateResult { - doc = new Doc(); - } - else + Update = stateDiff + }; + + using (var transaction = doc.WriteTransaction()) { - return new UpdateResult + if (transaction == null) { - IsSkipped = true - }; + throw new InvalidOperationException("Transaction cannot be created."); + } + + result.TransactionUpdateResult = transaction.ApplyV2(stateDiff); } - } - var result = new UpdateResult(); + return result; + }); + } + + public async ValueTask UpdateDocAsync(string name, Action action) + { + var context = contexts.GetContext(name); - using (var transaction = doc.WriteTransaction()) + await context.ApplyUpdateReturnAsync(doc => { - if (transaction == null) + using (var transaction = doc.WriteTransaction()) { - throw new InvalidOperationException("Transaction cannot be created."); - } - - result.TransactionUpdateResult = transaction.ApplyV2(stateDiff); - transaction.Commit(); + if (transaction == null) + { + throw new InvalidOperationException("Transaction cannot be created."); + } - result.Update = stateDiff; - } + action(doc, transaction); + } - return result; + return true; + }); } } diff --git a/YDotNet.Server/DocumentContext.cs b/YDotNet.Server/DocumentContext.cs new file mode 100644 index 00000000..d943ab00 --- /dev/null +++ b/YDotNet.Server/DocumentContext.cs @@ -0,0 +1,141 @@ +using YDotNet.Document; +using YDotNet.Server.Storage; + +namespace YDotNet.Server; + +internal sealed class DocumentContext : IAsyncDisposable +{ + private readonly IDocumentStorage documentStorage; + private readonly DocumentManagerOptions options; + private readonly string documentName; + private readonly Task loadingTask; + private readonly SemaphoreSlim slimLock = new SemaphoreSlim(1); + private DateTime lastWrite; + private Timer? writeTimer; + private Task? writeTask; + private Doc? doc; + + public DocumentContext(string documentName, IDocumentStorage documentStorage, DocumentManagerOptions options) + { + this.documentName = documentName; + this.documentStorage = documentStorage; + this.options = options; + + loadingTask = LoadInternalAsync(); + } + + public async ValueTask DisposeAsync() + { + if (writeTask != null) + { + await writeTask; + return; + } + + if (writeTimer != null && doc != null) + { + await WriteAsync(doc); + } + } + + private async Task LoadInternalAsync() + { + var doc = await LoadCoreAsync(); + + doc.ObserveUpdatesV2(e => + { + var now = DateTime.UtcNow; + + if (lastWrite == default) + { + lastWrite = now; + } + + if (writeTask != null) + { + var timeSinceLastWrite = now - lastWrite; + if (timeSinceLastWrite > options.MaxWriteTimeInterval) + { + Write(doc); + } + else + { + writeTimer?.Dispose(); + writeTimer = new Timer(_ => Write(doc), null, (int)options.DelayWriting.TotalMilliseconds, 0); + } + } + }); + + this.doc = doc; + + return doc; + } + + private async Task LoadCoreAsync() + { + var documentData = await documentStorage.GetDocAsync(documentName); + + if (documentData != null) + { + var doc = new Doc(); + + using (var transaction = doc.WriteTransaction()) + { + if (transaction == null) + { + throw new InvalidOperationException("Transaction cannot be acquired."); + } + + transaction.ApplyV2(documentData); + } + + return doc; + } + + if (options.AutoCreateDocument) + { + return new Doc(); + } + else + { + throw new InvalidOperationException("Document does not exist yet."); + } + } + + private void Write(Doc doc) + { + writeTask = WriteAsync(doc).ContinueWith(_ => writeTask = null); + } + + private async Task WriteAsync(Doc doc) + { + using (var transaction = doc.ReadTransaction()) + { + if (transaction == null) + { + throw new InvalidOperationException("Transaction cannot be acquired."); + } + + var state = transaction!.Snapshot(); + + await documentStorage.StoreDocAsync(documentName, state); + } + + lastWrite = DateTime.Now; + } + + public async Task ApplyUpdateReturnAsync(Func action) + { + var document = await loadingTask; + + slimLock.Wait(); + try + { + return action(document); + } + finally + { + slimLock.Release(); + } + } +} diff --git a/YDotNet.Server/DocumentContextCache.cs b/YDotNet.Server/DocumentContextCache.cs new file mode 100644 index 00000000..ddb86576 --- /dev/null +++ b/YDotNet.Server/DocumentContextCache.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using YDotNet.Server.Storage; + +namespace YDotNet.Server; + +internal sealed class DocumentContextCache : IAsyncDisposable +{ + private readonly IDocumentStorage documentStorage; + private readonly DocumentManagerOptions options; + private readonly MemoryCache memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); + private readonly List livingContexts = new List(); + private readonly SemaphoreSlim slimLock = new SemaphoreSlim(1); + + public DocumentContextCache(IDocumentStorage documentStorage, DocumentManagerOptions options) + { + this.documentStorage = documentStorage; + this.options = options; + } + + public async ValueTask DisposeAsync() + { + foreach (var documentContext in livingContexts) + { + await documentContext.DisposeAsync(); + } + } + + public DocumentContext GetContext(string documentName) + { + slimLock.Wait(); + try + { + return memoryCache.GetOrCreate(documentName, entry => + { + var context = new DocumentContext(documentName, documentStorage, options); + + entry.SlidingExpiration = options.CacheDuration; + entry.RegisterPostEvictionCallback((_, _, _, _) => + { + livingContexts.Remove(context); + }); + + livingContexts.Add(context); + return context; + })!; + } + finally + { + slimLock.Release(); + } + } +} diff --git a/YDotNet.Server/DocumentManagerOptions.cs b/YDotNet.Server/DocumentManagerOptions.cs index 7efca8cb..9d655b36 100644 --- a/YDotNet.Server/DocumentManagerOptions.cs +++ b/YDotNet.Server/DocumentManagerOptions.cs @@ -3,4 +3,10 @@ namespace YDotNet.Server; public sealed class DocumentManagerOptions { public bool AutoCreateDocument { get; set; } = true; + + public TimeSpan DelayWriting { get; set; } = TimeSpan.FromMilliseconds(100); + + public TimeSpan MaxWriteTimeInterval { get; set; } = TimeSpan.FromSeconds(5); + + public TimeSpan CacheDuration { get; set; } = TimeSpan.FromMinutes(5); } diff --git a/YDotNet.Server/IDocumentManager.cs b/YDotNet.Server/IDocumentManager.cs index fb4dffca..34355459 100644 --- a/YDotNet.Server/IDocumentManager.cs +++ b/YDotNet.Server/IDocumentManager.cs @@ -4,6 +4,4 @@ namespace YDotNet.Server; public interface IDocumentManager { - ValueTask GetDocAsync(string name, - CancellationToken ct = default); } diff --git a/YDotNet.Server/ServiceExtensions.cs b/YDotNet.Server/ServiceExtensions.cs index 54b4d41e..f744c75a 100644 --- a/YDotNet.Server/ServiceExtensions.cs +++ b/YDotNet.Server/ServiceExtensions.cs @@ -1,7 +1,7 @@ -namespace YDotNet.Server +namespace YDotNet.Server { - public class Class1 + public class ServiceExtensions { } -} \ No newline at end of file +} diff --git a/YDotNet.Server/Storage/IDocumentStorage.cs b/YDotNet.Server/Storage/IDocumentStorage.cs index 678ced46..d77900a7 100644 --- a/YDotNet.Server/Storage/IDocumentStorage.cs +++ b/YDotNet.Server/Storage/IDocumentStorage.cs @@ -4,9 +4,9 @@ namespace YDotNet.Server.Storage; public interface IDocumentStorage { - ValueTask GetDocAsync(string name, + ValueTask GetDocAsync(string name, CancellationToken ct = default); - ValueTask StoreDocAsync(string name, Doc doc, + ValueTask StoreDocAsync(string name, byte[] doc, CancellationToken ct = default); } diff --git a/YDotNet.Server/Storage/InMemoryDocStorage.cs b/YDotNet.Server/Storage/InMemoryDocStorage.cs index 7f90c053..71c71443 100644 --- a/YDotNet.Server/Storage/InMemoryDocStorage.cs +++ b/YDotNet.Server/Storage/InMemoryDocStorage.cs @@ -5,17 +5,17 @@ namespace YDotNet.Server.Storage; public sealed class InMemoryDocStorage : IDocumentStorage { - private readonly ConcurrentDictionary docs = new(); + private readonly ConcurrentDictionary docs = new(); - public ValueTask GetDocAsync(string name, + public ValueTask GetDocAsync(string name, CancellationToken ct = default) { docs.TryGetValue(name, out var doc); - return new ValueTask(doc); + return new ValueTask(doc); } - public ValueTask StoreDocAsync(string name, Doc doc, + public ValueTask StoreDocAsync(string name, byte[] doc, CancellationToken ct = default) { docs[name] = doc; diff --git a/YDotNet.Server/YDotNet.Server.csproj b/YDotNet.Server/YDotNet.Server.csproj index cfadb03d..5cf7f923 100644 --- a/YDotNet.Server/YDotNet.Server.csproj +++ b/YDotNet.Server/YDotNet.Server.csproj @@ -6,4 +6,18 @@ enable + + + + + + + + + + + + + + diff --git a/YDotNet.sln b/YDotNet.sln index 0de44951..2a111d18 100644 --- a/YDotNet.sln +++ b/YDotNet.sln @@ -1,9 +1,15 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YDotNet", "YDotNet\YDotNet.csproj", "{52763429-AB26-4415-9C7B-F17012FE3FDA}" + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33103.184 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YDotNet", "YDotNet\YDotNet.csproj", "{52763429-AB26-4415-9C7B-F17012FE3FDA}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{03858045-3849-41E2-ACE9-F10AAA4CCBCA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YDotNet.Tests.Unit", "Tests\YDotNet.Tests.Unit\YDotNet.Tests.Unit.csproj", "{F85C9334-87AF-4FFC-A301-3A9F57ECD4AB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YDotNet.Tests.Unit", "Tests\YDotNet.Tests.Unit\YDotNet.Tests.Unit.csproj", "{F85C9334-87AF-4FFC-A301-3A9F57ECD4AB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YDotNet.Server", "YDotNet.Server\YDotNet.Server.csproj", "{F24C69F2-E08A-4C93-A173-3F24FD4A0E7A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -19,8 +25,18 @@ Global {F85C9334-87AF-4FFC-A301-3A9F57ECD4AB}.Debug|Any CPU.Build.0 = Debug|Any CPU {F85C9334-87AF-4FFC-A301-3A9F57ECD4AB}.Release|Any CPU.ActiveCfg = Debug|Any CPU {F85C9334-87AF-4FFC-A301-3A9F57ECD4AB}.Release|Any CPU.Build.0 = Debug|Any CPU + {F24C69F2-E08A-4C93-A173-3F24FD4A0E7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F24C69F2-E08A-4C93-A173-3F24FD4A0E7A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F24C69F2-E08A-4C93-A173-3F24FD4A0E7A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F24C69F2-E08A-4C93-A173-3F24FD4A0E7A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {F85C9334-87AF-4FFC-A301-3A9F57ECD4AB} = {03858045-3849-41E2-ACE9-F10AAA4CCBCA} EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F7865083-8AF3-4562-88F2-95FD43368B57} + EndGlobalSection EndGlobal From 6a1ef6b03c1dc589510299090068c36e6396b93b Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 1 Oct 2023 20:10:47 +0200 Subject: [PATCH 003/186] Just some progress. --- YDotNet.Server/DocumentContext.cs | 94 ++++++++++--------------- YDotNet.Server/DocumentContextCache.cs | 56 ++++++++++++--- YDotNet.Server/Utils/DelayedWriter.cs | 97 ++++++++++++++++++++++++++ YDotNet.Server/YDotNet.Server.csproj | 1 + 4 files changed, 183 insertions(+), 65 deletions(-) create mode 100644 YDotNet.Server/Utils/DelayedWriter.cs diff --git a/YDotNet.Server/DocumentContext.cs b/YDotNet.Server/DocumentContext.cs index d943ab00..a58864f5 100644 --- a/YDotNet.Server/DocumentContext.cs +++ b/YDotNet.Server/DocumentContext.cs @@ -1,18 +1,17 @@ using YDotNet.Document; using YDotNet.Server.Storage; +using YDotNet.Server.Utils; namespace YDotNet.Server; -internal sealed class DocumentContext : IAsyncDisposable +internal sealed class DocumentContext { private readonly IDocumentStorage documentStorage; private readonly DocumentManagerOptions options; private readonly string documentName; private readonly Task loadingTask; private readonly SemaphoreSlim slimLock = new SemaphoreSlim(1); - private DateTime lastWrite; - private Timer? writeTimer; - private Task? writeTask; + private readonly DelayedWriter writer; private Doc? doc; public DocumentContext(string documentName, IDocumentStorage documentStorage, DocumentManagerOptions options) @@ -21,53 +20,25 @@ public DocumentContext(string documentName, IDocumentStorage documentStorage, Do this.documentStorage = documentStorage; this.options = options; + writer = new DelayedWriter(options.DelayWriting, options.MaxWriteTimeInterval, WriteAsync); + loadingTask = LoadInternalAsync(); } - public async ValueTask DisposeAsync() + public Task FlushAsync() { - if (writeTask != null) - { - await writeTask; - return; - } - - if (writeTimer != null && doc != null) - { - await WriteAsync(doc); - } + return writer.FlushAsync(); } private async Task LoadInternalAsync() { - var doc = await LoadCoreAsync(); + doc = await LoadCoreAsync(); doc.ObserveUpdatesV2(e => { - var now = DateTime.UtcNow; - - if (lastWrite == default) - { - lastWrite = now; - } - - if (writeTask != null) - { - var timeSinceLastWrite = now - lastWrite; - if (timeSinceLastWrite > options.MaxWriteTimeInterval) - { - Write(doc); - } - else - { - writeTimer?.Dispose(); - writeTimer = new Timer(_ => Write(doc), null, (int)options.DelayWriting.TotalMilliseconds, 0); - } - } + writer.Ping(); }); - this.doc = doc; - return doc; } @@ -102,40 +73,49 @@ private async Task LoadCoreAsync() } } - private void Write(Doc doc) + public async Task ApplyUpdateReturnAsync(Func action) { - writeTask = WriteAsync(doc).ContinueWith(_ => writeTask = null); - } + var document = await loadingTask; - private async Task WriteAsync(Doc doc) - { - using (var transaction = doc.ReadTransaction()) + slimLock.Wait(); + try { - if (transaction == null) - { - throw new InvalidOperationException("Transaction cannot be acquired."); - } - - var state = transaction!.Snapshot(); - - await documentStorage.StoreDocAsync(documentName, state); + return action(document); + } + finally + { + slimLock.Release(); } - - lastWrite = DateTime.Now; } - public async Task ApplyUpdateReturnAsync(Func action) + private async Task WriteAsync() { - var document = await loadingTask; + var doc = this.doc; + + if (doc == null) + { + return; + } + + byte[] snapshot; slimLock.Wait(); try { - return action(document); + using var transaction = doc.ReadTransaction(); + + if (transaction == null) + { + throw new InvalidOperationException("Transaction cannot be acquired."); + } + + snapshot = transaction!.Snapshot(); } finally { slimLock.Release(); } + + await documentStorage.StoreDocAsync(documentName, snapshot); } } diff --git a/YDotNet.Server/DocumentContextCache.cs b/YDotNet.Server/DocumentContextCache.cs index ddb86576..d3052d1d 100644 --- a/YDotNet.Server/DocumentContextCache.cs +++ b/YDotNet.Server/DocumentContextCache.cs @@ -9,7 +9,7 @@ internal sealed class DocumentContextCache : IAsyncDisposable private readonly IDocumentStorage documentStorage; private readonly DocumentManagerOptions options; private readonly MemoryCache memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); - private readonly List livingContexts = new List(); + private readonly Dictionary livingContexts = new Dictionary(); private readonly SemaphoreSlim slimLock = new SemaphoreSlim(1); public DocumentContextCache(IDocumentStorage documentStorage, DocumentManagerOptions options) @@ -20,28 +20,52 @@ public DocumentContextCache(IDocumentStorage documentStorage, DocumentManagerOpt public async ValueTask DisposeAsync() { - foreach (var documentContext in livingContexts) + await slimLock.WaitAsync(); + try + { + foreach (var (_, context) in livingContexts) + { + await context.FlushAsync(); + } + } + finally { - await documentContext.DisposeAsync(); + slimLock.Release(); } } - public DocumentContext GetContext(string documentName) + public void RemoveEvictedItems() { + memoryCache.Remove(true); + } + + public DocumentContext GetContext(string name) + { + // The memory cache does nto guarantees that the callback is called in parallel. Therefore we need the lock here. slimLock.Wait(); try { - return memoryCache.GetOrCreate(documentName, entry => + return memoryCache.GetOrCreate(name, entry => { - var context = new DocumentContext(documentName, documentStorage, options); + // Check if there are any pending flushes. If the flush is still running we reuse the context. + if (livingContexts.TryGetValue(name, out var context)) + { + livingContexts.Remove(name); + } + else + { + context = new DocumentContext(name, documentStorage, options); + } + // For each access we extend the lifetime of the cache entry. entry.SlidingExpiration = options.CacheDuration; entry.RegisterPostEvictionCallback((_, _, _, _) => { - livingContexts.Remove(context); + // There is no background thread for eviction. It is just done from + _ = CleanupAsync(name, context); }); - livingContexts.Add(context); + livingContexts.Add(name, context); return context; })!; } @@ -50,4 +74,20 @@ public DocumentContext GetContext(string documentName) slimLock.Release(); } } + + private async Task CleanupAsync(string name, DocumentContext context) + { + // Flush all pending changes to the storage and then remove the context from the list of living entries. + await context.FlushAsync(); + + slimLock.Wait(); + try + { + livingContexts.Remove(name); + } + finally + { + slimLock.Release(); + } + } } diff --git a/YDotNet.Server/Utils/DelayedWriter.cs b/YDotNet.Server/Utils/DelayedWriter.cs new file mode 100644 index 00000000..cfd11dd3 --- /dev/null +++ b/YDotNet.Server/Utils/DelayedWriter.cs @@ -0,0 +1,97 @@ +namespace YDotNet.Server.Utils; + +internal sealed class DelayedWriter +{ + private readonly TimeSpan delay; + private readonly TimeSpan delayMax; + private readonly Func action; + private int pendingWrites = 0; + private DateTime lastWrite; + private Timer? writeTimer; + private Task? writeTask; + + public Func Clock = () => DateTime.UtcNow; + + public DelayedWriter(TimeSpan delay, TimeSpan delayMax, Func action) + { + this.delay = delay; + this.delayMax = delayMax; + this.action = action; + } + + public async Task FlushAsync() + { + writeTimer?.Dispose(); + + if (writeTask != null) + { + await writeTask; + } + + if (pendingWrites > 0) + { + Write(); + } + + if (writeTask != null) + { + await writeTask; + } + } + + public void Ping() + { + var now = Clock(); + + Interlocked.Increment(ref pendingWrites); + + if (lastWrite == default) + { + lastWrite = now; + } + + var timeSinceLastPing = now - lastWrite; + if (timeSinceLastPing > delayMax) + { + Write(); + } + else + { + writeTimer?.Dispose(); + writeTimer = new Timer(_ => Write(), null, (int)delay.TotalMilliseconds, 0); + } + } + + private void Write() + { + _ = WriteAsync(); + } + + private async Task WriteAsync() + { + var now = Clock(); + + if (writeTask != null) + { + return; + } + + var localPendingWrites = pendingWrites; + + var task = action(); + try + { + writeTask = task; + await task; + } + finally + { + lastWrite = Clock(); + +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + Interlocked.CompareExchange(ref writeTask, null, task); + Interlocked.Add(ref pendingWrites, -localPendingWrites); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + } + } +} diff --git a/YDotNet.Server/YDotNet.Server.csproj b/YDotNet.Server/YDotNet.Server.csproj index 5cf7f923..15559217 100644 --- a/YDotNet.Server/YDotNet.Server.csproj +++ b/YDotNet.Server/YDotNet.Server.csproj @@ -15,6 +15,7 @@ + From c939fe865371fd56acc551947523044745ec7fb4 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 1 Oct 2023 22:33:42 +0200 Subject: [PATCH 004/186] Some improvements. --- YDotNet.Server/ConnectedUser.cs | 8 + YDotNet.Server/DefaultDocumentManager.cs | 165 ++++++++++++++++-- YDotNet.Server/DocumentManagerOptions.cs | 2 + YDotNet.Server/DocumentRequestContext.cs | 10 ++ YDotNet.Server/Events.cs | 40 +++++ YDotNet.Server/IDocumentCallback.cs | 39 +++++ YDotNet.Server/IDocumentManager.cs | 24 +++ YDotNet.Server/Internal/CallbackManager.cs | 56 ++++++ YDotNet.Server/Internal/ConnectedUsers.cs | 117 +++++++++++++ .../{Utils => Internal}/DelayedWriter.cs | 2 +- .../DocumentContainer.cs} | 17 +- .../DocumentContainerCache.cs} | 34 ++-- YDotNet.Server/UpdateResult.cs | 2 +- YDotNet.Server/YDotNet.Server.csproj | 1 - 14 files changed, 474 insertions(+), 43 deletions(-) create mode 100644 YDotNet.Server/ConnectedUser.cs create mode 100644 YDotNet.Server/DocumentRequestContext.cs create mode 100644 YDotNet.Server/Events.cs create mode 100644 YDotNet.Server/IDocumentCallback.cs create mode 100644 YDotNet.Server/Internal/CallbackManager.cs create mode 100644 YDotNet.Server/Internal/ConnectedUsers.cs rename YDotNet.Server/{Utils => Internal}/DelayedWriter.cs (98%) rename YDotNet.Server/{DocumentContext.cs => Internal/DocumentContainer.cs} (86%) rename YDotNet.Server/{DocumentContextCache.cs => Internal/DocumentContainerCache.cs} (62%) diff --git a/YDotNet.Server/ConnectedUser.cs b/YDotNet.Server/ConnectedUser.cs new file mode 100644 index 00000000..46f43549 --- /dev/null +++ b/YDotNet.Server/ConnectedUser.cs @@ -0,0 +1,8 @@ +namespace YDotNet.Server; + +public sealed class ConnectedUser +{ + public Dictionary State { get; set; } = new Dictionary(); + + public DateTime LastActivity { get; set; } +} diff --git a/YDotNet.Server/DefaultDocumentManager.cs b/YDotNet.Server/DefaultDocumentManager.cs index 29dbc850..24cc29f7 100644 --- a/YDotNet.Server/DefaultDocumentManager.cs +++ b/YDotNet.Server/DefaultDocumentManager.cs @@ -1,25 +1,36 @@ +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using YDotNet.Document; using YDotNet.Document.Transactions; +using YDotNet.Server.Internal; using YDotNet.Server.Storage; +#pragma warning disable IDE0063 // Use simple 'using' statement + namespace YDotNet.Server; public sealed class DefaultDocumentManager : IDocumentManager { - private DocumentContextCache contexts; + private readonly ConnectedUsers users = new(); + private readonly DocumentManagerOptions options; + private readonly DocumentContainerCache containers; + private readonly CallbackManager callbacks; - public DefaultDocumentManager(IDocumentStorage documentStorage, - IOptions options) + public DefaultDocumentManager(IDocumentStorage documentStorage, IEnumerable callbacks, + IOptions options, ILogger logger) { - contexts = new DocumentContextCache(documentStorage, options.Value); + this.options = options.Value; + this.callbacks = new CallbackManager(callbacks, logger); + + containers = new DocumentContainerCache(documentStorage, options.Value); } - public async ValueTask GetMissingChanges(string name, byte[] stateVector) + public async ValueTask GetMissingChangesAsync(DocumentContext context, byte[] stateVector, + CancellationToken ct = default) { - var context = contexts.GetContext(name); + var container = containers.GetContext(context.DocumentName); - return await context.ApplyUpdateReturnAsync(doc => + return await container.ApplyUpdateReturnAsync(doc => { using (var transaction = doc.ReadTransaction()) { @@ -30,18 +41,19 @@ public async ValueTask GetMissingChanges(string name, byte[] stateVector return transaction.StateDiffV2(stateVector); } - }); + }, null); } - public async ValueTask ApplyUpdateAsync(string name, byte[] stateDiff, object metadata) + public async ValueTask ApplyUpdateAsync(DocumentContext context, byte[] stateDiff, + CancellationToken ct = default) { - var context = contexts.GetContext(name); + var container = containers.GetContext(context.DocumentName); - return await context.ApplyUpdateReturnAsync(doc => + var (result, doc) = await container.ApplyUpdateReturnAsync(doc => { var result = new UpdateResult { - Update = stateDiff + Diff = stateDiff }; using (var transaction = doc.WriteTransaction()) @@ -54,16 +66,44 @@ public async ValueTask ApplyUpdateAsync(string name, byte[] stateD result.TransactionUpdateResult = transaction.ApplyV2(stateDiff); } - return result; + return (result, doc); + }, async doc => + { + await callbacks.OnDocumentChangingAsync(new DocumentChangeEvent + { + DocumentContext = context, + Document = doc, + DocumentManager = this, + }); }); + + if (result.Diff != null) + { + await callbacks.OnDocumentChangedAsync(new DocumentChangedEvent + { + Diff = result.Diff, + Document = doc, + DocumentContext = context, + DocumentManager = this, + }); + } + + return result; } - public async ValueTask UpdateDocAsync(string name, Action action) + public async ValueTask UpdateDocAsync(DocumentContext context, Action action, + CancellationToken ct = default) { - var context = contexts.GetContext(name); + var container = containers.GetContext(context.DocumentName); - await context.ApplyUpdateReturnAsync(doc => + var (diff, doc) = await container.ApplyUpdateReturnAsync(doc => { + byte[]? diff = null; + var subscription = doc.ObserveUpdatesV1(@event => + { + diff = @event.Update; + }); + using (var transaction = doc.WriteTransaction()) { if (transaction == null) @@ -74,7 +114,98 @@ await context.ApplyUpdateReturnAsync(doc => action(doc, transaction); } - return true; + doc.UnobserveUpdatesV2(subscription); + return (diff, doc); + }, async doc => + { + await callbacks.OnDocumentChangingAsync(new DocumentChangeEvent + { + Document = doc, + DocumentContext = context, + DocumentManager = this, + }); }); + + if (diff != null) + { + await callbacks.OnDocumentChangedAsync(new DocumentChangedEvent + { + Diff = diff, + Document = doc, + DocumentContext = context, + DocumentManager = this, + }); + } + } + + public async ValueTask PingAsync(DocumentContext context, + CancellationToken ct = default) + { + if (users.Add(context.DocumentName, context.ClientId)) + { + await callbacks.OnClientConnectedAsync(new ClientConnectedEvent + { + DocumentContext = context, + DocumentManager = this, + }); + } + } + + public async ValueTask UpdateAwarenessAsync(DocumentContext context, string key, object value, + CancellationToken ct = default) + { + var user = users.SetAwareness(context.DocumentName, context.ClientId, key, value); + + if (user != null) + { + await callbacks.OnAwarenessUpdatedAsync(new ClientAwarenessEvent + { + DocumentContext = context, + DocumentManager = this, + LocalState = null!, + }); + } + } + + public async ValueTask DisconnectAsync(DocumentContext context, + CancellationToken ct = default) + { + if (users.Remove(context.DocumentName, context.ClientId)) + { + await callbacks.OnClientDisconnectedAsync(new[] + { + new ClientDisconnectedEvent + { + DocumentContext = context, + DocumentManager = this, + } + }); + } + } + + public async ValueTask CleanupAsync( + CancellationToken ct = default) + { + var removedUsers = users.Cleanup(options.MaxPingTime).ToList(); + + if (removedUsers.Count > 0) + { + await callbacks.OnClientDisconnectedAsync(removedUsers.Select(x => + { + return new ClientDisconnectedEvent + { + DocumentContext = new DocumentContext { ClientId = x.ClientId, DocumentName = x.DocumentName }, + DocumentManager = this, + }; + }).ToArray()); + } + + containers.RemoveEvictedItems(); + } + + public ValueTask> GetAwarenessAsync(string roomName, + CancellationToken ct = default) + { + return new ValueTask>(users.GetUsers(roomName)); } } diff --git a/YDotNet.Server/DocumentManagerOptions.cs b/YDotNet.Server/DocumentManagerOptions.cs index 9d655b36..2de359b8 100644 --- a/YDotNet.Server/DocumentManagerOptions.cs +++ b/YDotNet.Server/DocumentManagerOptions.cs @@ -9,4 +9,6 @@ public sealed class DocumentManagerOptions public TimeSpan MaxWriteTimeInterval { get; set; } = TimeSpan.FromSeconds(5); public TimeSpan CacheDuration { get; set; } = TimeSpan.FromMinutes(5); + + public TimeSpan MaxPingTime { get; set; } = TimeSpan.FromMinutes(1); } diff --git a/YDotNet.Server/DocumentRequestContext.cs b/YDotNet.Server/DocumentRequestContext.cs new file mode 100644 index 00000000..1418e476 --- /dev/null +++ b/YDotNet.Server/DocumentRequestContext.cs @@ -0,0 +1,10 @@ +namespace YDotNet.Server; + +public sealed class DocumentContext +{ + required public string DocumentName { get; init; } + + required public long ClientId { get; init; } + + public object? Metadata { get; init; } +} diff --git a/YDotNet.Server/Events.cs b/YDotNet.Server/Events.cs new file mode 100644 index 00000000..ca8ac619 --- /dev/null +++ b/YDotNet.Server/Events.cs @@ -0,0 +1,40 @@ +using YDotNet.Document; + +namespace YDotNet.Server; + +public abstract class DocumentEvent +{ + required public IDocumentManager DocumentManager { get; init; } + + required public DocumentContext DocumentContext { get; init; } +} + +public class DocumentChangeEvent : DocumentEvent +{ + required public Doc Document { get; init; } +} + +public sealed class DocumentChangedEvent : DocumentChangeEvent +{ + required public byte[] Diff { get; init; } +} + +public sealed class DocumentStoreEvent : DocumentEvent +{ + required public Doc Document { get; init; } + + required public byte[] StateVector { get; init; } +} + +public sealed class ClientConnectedEvent : DocumentEvent +{ +} + +public sealed class ClientDisconnectedEvent : DocumentEvent +{ +} + +public sealed class ClientAwarenessEvent : DocumentEvent +{ + required public Dictionary LocalState { get; set; } +} diff --git a/YDotNet.Server/IDocumentCallback.cs b/YDotNet.Server/IDocumentCallback.cs new file mode 100644 index 00000000..8d779d0d --- /dev/null +++ b/YDotNet.Server/IDocumentCallback.cs @@ -0,0 +1,39 @@ +namespace YDotNet.Server; + +public interface IDocumentCallback +{ + ValueTask OnDocumentChangingAsync(DocumentChangeEvent @event) + { + return default; + } + + ValueTask OnDocumentChangedAsync(DocumentChangedEvent @event) + { + return default; + } + + ValueTask OnDocumentStoringAsync(DocumentStoreEvent @event) + { + return default; + } + + ValueTask OnDocumentStoredAsync(DocumentStoreEvent @event) + { + return default; + } + + ValueTask OnClientConnectedAsync(ClientConnectedEvent @event) + { + return default; + } + + ValueTask OnClientDisconnectedAsync(ClientDisconnectedEvent[] events) + { + return default; + } + + ValueTask OnAwarenessUpdatedAsync(ClientAwarenessEvent @event) + { + return default; + } +} diff --git a/YDotNet.Server/IDocumentManager.cs b/YDotNet.Server/IDocumentManager.cs index 34355459..93008519 100644 --- a/YDotNet.Server/IDocumentManager.cs +++ b/YDotNet.Server/IDocumentManager.cs @@ -1,7 +1,31 @@ using YDotNet.Document; +using YDotNet.Document.Transactions; namespace YDotNet.Server; public interface IDocumentManager { + ValueTask PingAsync(DocumentContext context, + CancellationToken ct = default); + + ValueTask DisconnectAsync(DocumentContext context, + CancellationToken ct = default); + + ValueTask UpdateAwarenessAsync(DocumentContext context, string key, object value, + CancellationToken ct = default); + + ValueTask GetMissingChangesAsync(DocumentContext context, byte[] stateVector, + CancellationToken ct = default); + + ValueTask> GetAwarenessAsync(string roomName, + CancellationToken ct = default); + + ValueTask ApplyUpdateAsync(DocumentContext context, byte[] stateDiff, + CancellationToken ct = default); + + ValueTask UpdateDocAsync(DocumentContext context, Action action, + CancellationToken ct = default); + + ValueTask CleanupAsync( + CancellationToken ct = default); } diff --git a/YDotNet.Server/Internal/CallbackManager.cs b/YDotNet.Server/Internal/CallbackManager.cs new file mode 100644 index 00000000..eab68041 --- /dev/null +++ b/YDotNet.Server/Internal/CallbackManager.cs @@ -0,0 +1,56 @@ +using Microsoft.Extensions.Logging; + +namespace YDotNet.Server.Internal; + +public sealed class CallbackManager : IDocumentCallback +{ + private readonly List callbacks; + private readonly ILogger logger; + + public CallbackManager(IEnumerable callbacks, ILogger logger) + { + this.callbacks = callbacks.ToList(); + + this.logger = logger; + } + + public ValueTask OnDocumentChangingAsync(DocumentChangeEvent @event) + { + return InvokeCallbackAsync(@event, (c, e) => c.OnDocumentChangingAsync(e)); + } + + public ValueTask OnDocumentChangedAsync(DocumentChangedEvent @event) + { + return InvokeCallbackAsync(@event, (c, e) => c.OnDocumentChangedAsync(e)); + } + + public ValueTask OnClientConnectedAsync(ClientConnectedEvent @event) + { + return InvokeCallbackAsync(@event, (c, e) => c.OnClientConnectedAsync(e)); + } + + public ValueTask OnClientDisconnectedAsync(ClientDisconnectedEvent[] events) + { + return InvokeCallbackAsync(@events, (c, e) => c.OnClientDisconnectedAsync(e)); + } + + public ValueTask OnAwarenessUpdatedAsync(ClientAwarenessEvent @event) + { + return InvokeCallbackAsync(@event, (c, e) => c.OnAwarenessUpdatedAsync(e)); + } + + private async ValueTask InvokeCallbackAsync(T @event, Func action) + { + foreach (var callback in callbacks) + { + try + { + await action(callback, @event); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to invoke callback for {callback} and {event}.", callback, @event); + } + } + } +} diff --git a/YDotNet.Server/Internal/ConnectedUsers.cs b/YDotNet.Server/Internal/ConnectedUsers.cs new file mode 100644 index 00000000..8904b62c --- /dev/null +++ b/YDotNet.Server/Internal/ConnectedUsers.cs @@ -0,0 +1,117 @@ +using System.Collections.Concurrent; + +namespace YDotNet.Server.Internal; + +public sealed class ConnectedUsers +{ + private readonly ConcurrentDictionary> users = new(); + + public Func Clock = () => DateTime.UtcNow; + + public IReadOnlyDictionary GetUsers(string documentName) + { + var documentUsers = users.GetOrAdd(documentName, _ => new Dictionary()); + + lock (documentUsers) + { + return documentUsers.ToDictionary( + x => x.Key, + x => new ConnectedUser + { + State = new Dictionary(x.Value.State) + }); + } + } + + public bool Add(string documentName, long clientId) + { + var documentUsers = users.GetOrAdd(documentName, _ => new Dictionary()); + + lock (documentUsers) + { + if (documentUsers.ContainsKey(clientId)) + { + return false; + } + + documentUsers.Add(clientId, new ConnectedUser + { + LastActivity = Clock(), + }); + + return true; + } + } + + public ConnectedUser SetAwareness(string documentName, long clientId, string key, object value) + { + var documentUsers = users.GetOrAdd(documentName, _ => new Dictionary()); + + ConnectedUser user; + + lock (documentUsers) + { + if (!documentUsers.TryGetValue(clientId, out user!)) + { + user = new ConnectedUser + { + LastActivity = Clock(), + }; + + documentUsers.Add(clientId, user); + } + } + + lock (user) + { + user.State[key] = value; + } + + return user; + } + + public bool Remove(string documentName, long clientId) + { + if (!users.TryGetValue(documentName, out var documentUsers)) + { + return false; + } + + lock (documentUsers) + { + return documentUsers.Remove(clientId); + } + } + + public IEnumerable<(long ClientId, string DocumentName)> Cleanup(TimeSpan maxAge) + { + var olderThan = Clock() - maxAge; + + foreach (var (documentName, users) in users) + { + List? usersToRemove = null; + + lock (users) + { + foreach (var (clientId, user) in users) + { + if (user.LastActivity < olderThan) + { + usersToRemove ??= new List(); + usersToRemove.Add(clientId); + + yield return (clientId, documentName); + } + } + } + + if (usersToRemove != null) + { + foreach (var user in usersToRemove) + { + users.Remove(user); + } + } + } + } +} diff --git a/YDotNet.Server/Utils/DelayedWriter.cs b/YDotNet.Server/Internal/DelayedWriter.cs similarity index 98% rename from YDotNet.Server/Utils/DelayedWriter.cs rename to YDotNet.Server/Internal/DelayedWriter.cs index cfd11dd3..02081a54 100644 --- a/YDotNet.Server/Utils/DelayedWriter.cs +++ b/YDotNet.Server/Internal/DelayedWriter.cs @@ -1,4 +1,4 @@ -namespace YDotNet.Server.Utils; +namespace YDotNet.Server.Internal; internal sealed class DelayedWriter { diff --git a/YDotNet.Server/DocumentContext.cs b/YDotNet.Server/Internal/DocumentContainer.cs similarity index 86% rename from YDotNet.Server/DocumentContext.cs rename to YDotNet.Server/Internal/DocumentContainer.cs index a58864f5..d6e3b048 100644 --- a/YDotNet.Server/DocumentContext.cs +++ b/YDotNet.Server/Internal/DocumentContainer.cs @@ -1,20 +1,20 @@ using YDotNet.Document; using YDotNet.Server.Storage; -using YDotNet.Server.Utils; +using YDotNet.Server.Internal; -namespace YDotNet.Server; +namespace YDotNet.Server.Internal; -internal sealed class DocumentContext +internal sealed class DocumentContainer { private readonly IDocumentStorage documentStorage; private readonly DocumentManagerOptions options; private readonly string documentName; private readonly Task loadingTask; - private readonly SemaphoreSlim slimLock = new SemaphoreSlim(1); + private readonly SemaphoreSlim slimLock = new(1); private readonly DelayedWriter writer; private Doc? doc; - public DocumentContext(string documentName, IDocumentStorage documentStorage, DocumentManagerOptions options) + public DocumentContainer(string documentName, IDocumentStorage documentStorage, DocumentManagerOptions options) { this.documentName = documentName; this.documentStorage = documentStorage; @@ -73,10 +73,15 @@ private async Task LoadCoreAsync() } } - public async Task ApplyUpdateReturnAsync(Func action) + public async Task ApplyUpdateReturnAsync(Func action, Func? beforeAction) { var document = await loadingTask; + if (beforeAction != null) + { + await beforeAction(document); + } + slimLock.Wait(); try { diff --git a/YDotNet.Server/DocumentContextCache.cs b/YDotNet.Server/Internal/DocumentContainerCache.cs similarity index 62% rename from YDotNet.Server/DocumentContextCache.cs rename to YDotNet.Server/Internal/DocumentContainerCache.cs index d3052d1d..46e0665b 100644 --- a/YDotNet.Server/DocumentContextCache.cs +++ b/YDotNet.Server/Internal/DocumentContainerCache.cs @@ -2,17 +2,17 @@ using Microsoft.Extensions.Options; using YDotNet.Server.Storage; -namespace YDotNet.Server; +namespace YDotNet.Server.Internal; -internal sealed class DocumentContextCache : IAsyncDisposable +internal sealed class DocumentContainerCache : IAsyncDisposable { private readonly IDocumentStorage documentStorage; private readonly DocumentManagerOptions options; - private readonly MemoryCache memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); - private readonly Dictionary livingContexts = new Dictionary(); - private readonly SemaphoreSlim slimLock = new SemaphoreSlim(1); + private readonly MemoryCache memoryCache = new(Options.Create(new MemoryCacheOptions())); + private readonly Dictionary livingContainers = new(); + private readonly SemaphoreSlim slimLock = new(1); - public DocumentContextCache(IDocumentStorage documentStorage, DocumentManagerOptions options) + public DocumentContainerCache(IDocumentStorage documentStorage, DocumentManagerOptions options) { this.documentStorage = documentStorage; this.options = options; @@ -23,9 +23,9 @@ public async ValueTask DisposeAsync() await slimLock.WaitAsync(); try { - foreach (var (_, context) in livingContexts) + foreach (var (_, container) in livingContainers) { - await context.FlushAsync(); + await container.FlushAsync(); } } finally @@ -39,7 +39,7 @@ public void RemoveEvictedItems() memoryCache.Remove(true); } - public DocumentContext GetContext(string name) + public DocumentContainer GetContext(string name) { // The memory cache does nto guarantees that the callback is called in parallel. Therefore we need the lock here. slimLock.Wait(); @@ -48,13 +48,13 @@ public DocumentContext GetContext(string name) return memoryCache.GetOrCreate(name, entry => { // Check if there are any pending flushes. If the flush is still running we reuse the context. - if (livingContexts.TryGetValue(name, out var context)) + if (livingContainers.TryGetValue(name, out var container)) { - livingContexts.Remove(name); + livingContainers.Remove(name); } else { - context = new DocumentContext(name, documentStorage, options); + container = new DocumentContainer(name, documentStorage, options); } // For each access we extend the lifetime of the cache entry. @@ -62,11 +62,11 @@ public DocumentContext GetContext(string name) entry.RegisterPostEvictionCallback((_, _, _, _) => { // There is no background thread for eviction. It is just done from - _ = CleanupAsync(name, context); + _ = CleanupAsync(name, container); }); - livingContexts.Add(name, context); - return context; + livingContainers.Add(name, container); + return container; })!; } finally @@ -75,7 +75,7 @@ public DocumentContext GetContext(string name) } } - private async Task CleanupAsync(string name, DocumentContext context) + private async Task CleanupAsync(string name, DocumentContainer context) { // Flush all pending changes to the storage and then remove the context from the list of living entries. await context.FlushAsync(); @@ -83,7 +83,7 @@ private async Task CleanupAsync(string name, DocumentContext context) slimLock.Wait(); try { - livingContexts.Remove(name); + livingContainers.Remove(name); } finally { diff --git a/YDotNet.Server/UpdateResult.cs b/YDotNet.Server/UpdateResult.cs index 258e1bef..bde4f585 100644 --- a/YDotNet.Server/UpdateResult.cs +++ b/YDotNet.Server/UpdateResult.cs @@ -8,5 +8,5 @@ public sealed class UpdateResult public bool IsSkipped { get; set; } - public byte[]? Update { get; set; } + public byte[]? Diff { get; set; } } diff --git a/YDotNet.Server/YDotNet.Server.csproj b/YDotNet.Server/YDotNet.Server.csproj index 15559217..5cf7f923 100644 --- a/YDotNet.Server/YDotNet.Server.csproj +++ b/YDotNet.Server/YDotNet.Server.csproj @@ -15,7 +15,6 @@ - From 6504795086e0b21561811a0f23f0b2c28697aae5 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 2 Oct 2023 10:42:24 +0200 Subject: [PATCH 005/186] Test --- .../Internal/LoggerTextWriter.cs | 30 +++ YDotNet.Server.Redis/Messages.cs | 45 ++++ YDotNet.Server.Redis/RedisCallback.cs | 170 +++++++++++++++ .../RedisClusteringOptions.cs | 27 +++ YDotNet.Server.Redis/ServiceExtensions.cs | 13 ++ .../YDotNet.Server.Redis.csproj | 18 ++ YDotNet.Server.WebSockets/ClientState.cs | 44 ++++ .../EncoderExtensions.cs | 38 ++++ YDotNet.Server.WebSockets/MessageTypes.cs | 17 ++ YDotNet.Server.WebSockets/WebSocketDecoder.cs | 71 +++++++ YDotNet.Server.WebSockets/WebSocketEncoder.cs | 39 ++++ .../YDotNet.Server.WebSockets.csproj | 18 ++ .../YDotNetSocketMiddleware.cs | 197 ++++++++++++++++++ YDotNet.Server/ConnectedUser.cs | 4 +- YDotNet.Server/DefaultDocumentManager.cs | 41 ++-- YDotNet.Server/DocumentRequestContext.cs | 6 +- YDotNet.Server/Events.cs | 15 +- YDotNet.Server/IDocumentCallback.cs | 16 +- YDotNet.Server/IDocumentManager.cs | 7 +- YDotNet.Server/Internal/CallbackManager.cs | 11 +- YDotNet.Server/Internal/ConnectedUsers.cs | 54 ++--- YDotNet.Server/ServiceExtensions.cs | 14 +- YDotNet.Server/Storage/InMemoryDocStorage.cs | 2 +- YDotNet.Server/YDotNet.Server.csproj | 1 + YDotNet.sln | 14 +- YDotNet/Protocol/Decoder.cs | 81 +++++++ YDotNet/Protocol/Encoder.cs | 73 +++++++ YDotNet/stylecop.json | 14 ++ 28 files changed, 978 insertions(+), 102 deletions(-) create mode 100644 YDotNet.Server.Redis/Internal/LoggerTextWriter.cs create mode 100644 YDotNet.Server.Redis/Messages.cs create mode 100644 YDotNet.Server.Redis/RedisCallback.cs create mode 100644 YDotNet.Server.Redis/RedisClusteringOptions.cs create mode 100644 YDotNet.Server.Redis/ServiceExtensions.cs create mode 100644 YDotNet.Server.Redis/YDotNet.Server.Redis.csproj create mode 100644 YDotNet.Server.WebSockets/ClientState.cs create mode 100644 YDotNet.Server.WebSockets/EncoderExtensions.cs create mode 100644 YDotNet.Server.WebSockets/MessageTypes.cs create mode 100644 YDotNet.Server.WebSockets/WebSocketDecoder.cs create mode 100644 YDotNet.Server.WebSockets/WebSocketEncoder.cs create mode 100644 YDotNet.Server.WebSockets/YDotNet.Server.WebSockets.csproj create mode 100644 YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs create mode 100644 YDotNet/Protocol/Decoder.cs create mode 100644 YDotNet/Protocol/Encoder.cs create mode 100644 YDotNet/stylecop.json diff --git a/YDotNet.Server.Redis/Internal/LoggerTextWriter.cs b/YDotNet.Server.Redis/Internal/LoggerTextWriter.cs new file mode 100644 index 00000000..8e0d2e84 --- /dev/null +++ b/YDotNet.Server.Redis/Internal/LoggerTextWriter.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.Logging; +using System.Text; + +namespace YDotNet.Server.Redis.Internal; + +internal sealed class LoggerTextWriter : TextWriter +{ + private readonly ILogger log; + + public LoggerTextWriter(ILogger log) + { + this.log = log; + } + + public override Encoding Encoding => Encoding.UTF8; + + public override void Write(char value) + { + } + + public override void WriteLine(string? value) + { + if (log.IsEnabled(LogLevel.Debug)) + { +#pragma warning disable CA2254 // Template should be a static expression + log.LogDebug(new EventId(100, "RedisConnectionLog"), value); +#pragma warning restore CA2254 // Template should be a static expression + } + } +} diff --git a/YDotNet.Server.Redis/Messages.cs b/YDotNet.Server.Redis/Messages.cs new file mode 100644 index 00000000..ac4898eb --- /dev/null +++ b/YDotNet.Server.Redis/Messages.cs @@ -0,0 +1,45 @@ +using System.Text.Json.Serialization; + +namespace YDotNet.Server.Redis; + +public sealed class Message +{ + [JsonPropertyName("s")] + required public Guid SenderId { get; init; } + + [JsonPropertyName("c")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public PingMessage? Pinged { get; set; } + + [JsonPropertyName("d")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public PingMessage[]? ClientDisconnected { get; set; } + + [JsonPropertyName("u")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public DocumentChangeMessage? DocumentChanged { get; set; } +} + +public abstract class ClientMessage +{ + [JsonPropertyName("d")] + required public string DocumentName { get; init; } + + [JsonPropertyName("c")] + required public long ClientId { get; init; } +} + +public sealed class DocumentChangeMessage : ClientMessage +{ + [JsonPropertyName("u")] + required public byte[] DocumentDiff { get; init; } +} + +public sealed class PingMessage : ClientMessage +{ + [JsonPropertyName("c")] + required public long ClientClock { get; init; } + + [JsonPropertyName("s")] + required public string? ClientState { get; init; } +} diff --git a/YDotNet.Server.Redis/RedisCallback.cs b/YDotNet.Server.Redis/RedisCallback.cs new file mode 100644 index 00000000..6f88c80b --- /dev/null +++ b/YDotNet.Server.Redis/RedisCallback.cs @@ -0,0 +1,170 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using StackExchange.Redis; +using System.Text.Json; +using YDotNet.Server.Redis.Internal; + +namespace YDotNet.Server.Redis; + +public sealed class RedisCallback : IDocumentCallback, IHostedService +{ + private readonly Guid senderId = Guid.NewGuid(); + private readonly RedisClusteringOptions options; + private readonly ILogger logger; + private ISubscriber? subscriber; + private IDocumentManager? documentManager; + + public RedisCallback(IOptions options, ILogger logger) + { + this.options = options.Value; + this.logger = logger; + } + + public async Task StartAsync( + CancellationToken cancellationToken) + { + var connection = await options.ConnectAsync(new LoggerTextWriter(logger)); + + // Is only needed for topics, but it has only minor costs. + subscriber = connection.GetSubscriber(); + subscriber.Subscribe(options.Channel, async (_, value) => + { + await HandleMessage(value); + }); + } + + public ValueTask OnInitializedAsync( + IDocumentManager manager) + { + documentManager = manager; + return default; + } + + public Task StopAsync( + CancellationToken cancellationToken) + { + subscriber?.UnsubscribeAll(); + return Task.CompletedTask; + } + + private async Task HandleMessage(RedisValue value) + { + if (documentManager == null) + { + return; + } + + var message = JsonSerializer.Deserialize(value.ToString()); + + if (message == null || message.SenderId == senderId) + { + return; + } + + if (message.DocumentChanged is DocumentChangeMessage changed) + { + await documentManager.ApplyUpdateAsync(new DocumentContext + { + ClientId = changed.ClientId, + DocumentName = changed.DocumentName, + Metadata = senderId + }, changed.DocumentDiff); + } + + if (message.Pinged is PingMessage pinged) + { + await documentManager.PingAsync(new DocumentContext + { + ClientId = pinged.ClientId, + DocumentName = pinged.DocumentName, + Metadata = senderId + }, pinged.ClientClock, pinged.ClientState); + } + + if (message.ClientDisconnected is PingMessage[] disconnected) + { + foreach (var client in disconnected) + { + await documentManager.DisconnectAsync(new DocumentContext + { + ClientId = client.ClientId, + DocumentName = client.DocumentName, + Metadata = senderId + }); + } + } + } + + public async ValueTask OnAwarenessUpdatedAsync(ClientAwarenessEvent @event) + { + if (subscriber == null) + { + return; + } + + var message = new Message + { + SenderId = senderId, + Pinged = new PingMessage + { + ClientId = @event.DocumentContext.ClientId, + ClientClock = @event.ClientClock, + ClientState = @event.ClientState, + DocumentName = @event.DocumentContext.DocumentName, + }, + }; + + var json = JsonSerializer.Serialize(message); + + await subscriber.PublishAsync(options.Channel, json); + } + + public async ValueTask OnClientDisconnectedAsync(ClientDisconnectedEvent[] events) + { + if (subscriber == null) + { + return; + } + + var message = new Message + { + SenderId = senderId, + ClientDisconnected = events.Select(x => new PingMessage + { + ClientId = x.DocumentContext.ClientId, + ClientState = null, + ClientClock = 0, + DocumentName = x.DocumentContext.DocumentName, + }).ToArray() + }; + + var json = JsonSerializer.Serialize(message); + + await subscriber.PublishAsync(options.Channel, json); + } + + public async ValueTask OnDocumentChangedAsync(DocumentChangedEvent @event) + { + if (subscriber == null) + { + return; + } + + var message = new Message + { + SenderId = senderId, + DocumentChanged = new DocumentChangeMessage + { + ClientId = @event.DocumentContext.ClientId, + DocumentName = @event.DocumentContext.DocumentName, + DocumentDiff = @event.Diff + }, + }; + + var json = JsonSerializer.Serialize(message); + + await subscriber.PublishAsync(options.Channel, json); + + } +} diff --git a/YDotNet.Server.Redis/RedisClusteringOptions.cs b/YDotNet.Server.Redis/RedisClusteringOptions.cs new file mode 100644 index 00000000..c6fb44b0 --- /dev/null +++ b/YDotNet.Server.Redis/RedisClusteringOptions.cs @@ -0,0 +1,27 @@ +using StackExchange.Redis; + +namespace YDotNet.Server.Redis; + +public sealed class RedisClusteringOptions +{ + public RedisChannel Channel { get; set; } = RedisChannel.Literal("YDotNet"); + + public ConfigurationOptions? Configuration { get; set; } + + public Func>? ConnectionFactory { get; set; } + + internal async Task ConnectAsync(TextWriter log) + { + if (ConnectionFactory != null) + { + return await ConnectionFactory(log); + } + + if (Configuration != null) + { + return await ConnectionMultiplexer.ConnectAsync(Configuration, log); + } + + throw new InvalidOperationException("Either configuration or connection factory must be set."); + } +} diff --git a/YDotNet.Server.Redis/ServiceExtensions.cs b/YDotNet.Server.Redis/ServiceExtensions.cs new file mode 100644 index 00000000..c2fe6d10 --- /dev/null +++ b/YDotNet.Server.Redis/ServiceExtensions.cs @@ -0,0 +1,13 @@ +using YDotNet.Server; +using YDotNet.Server.Redis; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class ServiceExtensions +{ + public static void AddYDotNetRedisClustering(IServiceCollection services, Action configure) + { + services.Configure(configure); + services.AddSingleton(); + } +} diff --git a/YDotNet.Server.Redis/YDotNet.Server.Redis.csproj b/YDotNet.Server.Redis/YDotNet.Server.Redis.csproj new file mode 100644 index 00000000..36a8c68a --- /dev/null +++ b/YDotNet.Server.Redis/YDotNet.Server.Redis.csproj @@ -0,0 +1,18 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + diff --git a/YDotNet.Server.WebSockets/ClientState.cs b/YDotNet.Server.WebSockets/ClientState.cs new file mode 100644 index 00000000..b3ff586f --- /dev/null +++ b/YDotNet.Server.WebSockets/ClientState.cs @@ -0,0 +1,44 @@ +using System.Collections.Concurrent; +using System.Net.WebSockets; + +namespace YDotNet.Server.WebSockets; + +public sealed class ClientState : IDisposable +{ + private readonly SemaphoreSlim slimLock = new SemaphoreSlim(1); + + required public WebSocket WebSocket { get; set; } + + required public WebSocketEncoder Encoder { get; set; } + + required public WebSocketDecoder Decoder { get; set; } + + required public DocumentContext DocumentContext { get; set; } + + required public string DocumentName { get; init; } + + public bool IsSynced { get; set; } + + public ConcurrentQueue PendingUpdates { get; } = new ConcurrentQueue(); + + public async Task WriteLockedAsync(T state, Func action, CancellationToken ct) + { + await slimLock.WaitAsync(ct); + try + { + await action(Encoder, state, this, ct); + } + finally + { + slimLock.Release(); + } + } + + public void Dispose() + { + WebSocket.Dispose(); + + Encoder.Dispose(); + Decoder.Dispose(); + } +} diff --git a/YDotNet.Server.WebSockets/EncoderExtensions.cs b/YDotNet.Server.WebSockets/EncoderExtensions.cs new file mode 100644 index 00000000..80d4ce79 --- /dev/null +++ b/YDotNet.Server.WebSockets/EncoderExtensions.cs @@ -0,0 +1,38 @@ +using YDotNet.Protocol; + +namespace YDotNet.Server.WebSockets; + +public static class EncoderExtensions +{ + public static async Task WriteSyncStep1Async(this Encoder encoder, byte[] stateVector, + CancellationToken ct) + { + await encoder.WriteVarUintAsync(MessageTypes.TypeSync, ct); + await encoder.WriteVarUintAsync(MessageTypes.SyncStep1, ct); + await encoder.WriteVarUint8Array(stateVector, ct); + } + + public static async Task WriteSyncStep2Async(this Encoder encoder, byte[] update, + CancellationToken ct) + { + await encoder.WriteVarUintAsync(MessageTypes.TypeSync, ct); + await encoder.WriteVarUintAsync(MessageTypes.SyncStep2, ct); + await encoder.WriteVarUint8Array(update, ct); + } + + public static async Task WriteSyncUpdateAsync(this Encoder encoder, byte[] update, + CancellationToken ct) + { + await encoder.WriteVarUintAsync(MessageTypes.TypeSync, ct); + await encoder.WriteVarUintAsync(MessageTypes.SyncUpdate, ct); + await encoder.WriteVarUint8Array(update, ct); + } + + public static async Task WriteAwarenessAsync(this Encoder encoder, long clientId, long clock, string? state, + CancellationToken ct) + { + await encoder.WriteVarUintAsync((int)clientId, ct); + await encoder.WriteVarUintAsync((int)clock, ct); + await encoder.WriteVarStringAsync(state ?? string.Empty, ct); + } +} diff --git a/YDotNet.Server.WebSockets/MessageTypes.cs b/YDotNet.Server.WebSockets/MessageTypes.cs new file mode 100644 index 00000000..09c98f52 --- /dev/null +++ b/YDotNet.Server.WebSockets/MessageTypes.cs @@ -0,0 +1,17 @@ +using Microsoft.VisualBasic; + +namespace YDotNet.Server.WebSockets; + +public static class MessageTypes +{ + // Read: https://github.com/yjs/y-websocket/blob/master/src/y-websocket.js#L19 + public const int TypeSync = 0; + public const int TypeAwareness = 1; + public const int TypeAuth = 2; + public const int TypeQueryAwareness = 3; + + // Read: https://github.com/yjs/y-protocols/blob/master/sync.js + public const int SyncStep1 = 1; + public const int SyncStep2 = 2; + public const int SyncUpdate = 2; +} diff --git a/YDotNet.Server.WebSockets/WebSocketDecoder.cs b/YDotNet.Server.WebSockets/WebSocketDecoder.cs new file mode 100644 index 00000000..8f9ebd72 --- /dev/null +++ b/YDotNet.Server.WebSockets/WebSocketDecoder.cs @@ -0,0 +1,71 @@ +using System.Buffers; +using System.Net.WebSockets; +using YDotNet.Protocol; + +namespace YDotNet.Server.WebSockets; + +public sealed class WebSocketDecoder : Decoder, IDisposable +{ + private readonly WebSocket webSocket; + private readonly byte[] buffer = ArrayPool.Shared.Rent(1024 * 4); + private int bufferLength; + private int bufferIndex; + + public WebSocketDecoder(WebSocket webSocket) + { + this.webSocket = webSocket; + } + + public void Dispose() + { + ArrayPool.Shared.Return(buffer); + } + + protected override async ValueTask ReadByteAsync( + CancellationToken ct) + { + EnsureSocketIsOpen(); + + await ReadIfEndOfBufferReachedAsync(ct); + + return buffer[bufferIndex++]; + } + + protected override async ValueTask ReadBytesAsync(Memory memory, + CancellationToken ct) + { + EnsureSocketIsOpen(); + + while (memory.Length > 0) + { + await ReadIfEndOfBufferReachedAsync(ct); + + var bytesLeft = bufferLength - bufferIndex; + var bytesToCopy = Math.Min(memory.Length, bytesLeft); + + buffer.AsMemory(bufferIndex, bytesToCopy).CopyTo(memory); + memory = memory.Slice(bytesToCopy); + + bufferIndex += bytesToCopy; + } + } + + private async Task ReadIfEndOfBufferReachedAsync(CancellationToken ct) + { + if (bufferIndex == bufferLength) + { + var received = await webSocket.ReceiveAsync(buffer, ct); + + bufferLength = received.Count; + bufferIndex = 0; + } + } + + private void EnsureSocketIsOpen() + { + if (webSocket.State != WebSocketState.Open) + { + throw new InvalidOperationException("Socket is already closed."); + } + } +} diff --git a/YDotNet.Server.WebSockets/WebSocketEncoder.cs b/YDotNet.Server.WebSockets/WebSocketEncoder.cs new file mode 100644 index 00000000..f7f2e5a1 --- /dev/null +++ b/YDotNet.Server.WebSockets/WebSocketEncoder.cs @@ -0,0 +1,39 @@ +using System.Net.WebSockets; +using YDotNet.Protocol; + +namespace YDotNet.Server.WebSockets; + +public sealed class WebSocketEncoder : Encoder, IDisposable +{ + private readonly byte[] buffer = new byte[1]; + private readonly WebSocket webSocket; + + public WebSocketEncoder(WebSocket webSocket) + { + this.webSocket = webSocket; + } + + public void Dispose() + { + } + + protected override ValueTask WriteByteAsync(byte value, + CancellationToken ct) + { + buffer[0] = value; + + return new ValueTask(webSocket.SendAsync(buffer, WebSocketMessageType.Binary, false, ct)); + } + + protected override ValueTask WriteBytesAsync(ArraySegment bytes, + CancellationToken ct) + { + return new ValueTask(webSocket.SendAsync(bytes, WebSocketMessageType.Binary, false, ct)); + } + + public override ValueTask EndMessageAsync( + CancellationToken ct = default) + { + return new ValueTask(webSocket.SendAsync(Array.Empty(), WebSocketMessageType.Binary, true, ct)); + } +} diff --git a/YDotNet.Server.WebSockets/YDotNet.Server.WebSockets.csproj b/YDotNet.Server.WebSockets/YDotNet.Server.WebSockets.csproj new file mode 100644 index 00000000..91a13e8a --- /dev/null +++ b/YDotNet.Server.WebSockets/YDotNet.Server.WebSockets.csproj @@ -0,0 +1,18 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + diff --git a/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs b/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs new file mode 100644 index 00000000..0bdd1e42 --- /dev/null +++ b/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs @@ -0,0 +1,197 @@ +using Microsoft.AspNetCore.Http; +using System.Collections.Concurrent; + +namespace YDotNet.Server.WebSockets; + +public sealed class YDotNetSocketMiddleware : IDocumentCallback +{ + private readonly ConcurrentDictionary> statesPerDocumentName = new ConcurrentDictionary>(); + private IDocumentManager? documentManager; + + public ValueTask OnInitializedAsync(IDocumentManager manager) + { + documentManager = manager; + return default; + } + + public async ValueTask OnAwarenessUpdatedAsync(ClientAwarenessEvent @event) + { + var documentStates = GetOtherClients(@event.DocumentContext.DocumentName, @event.DocumentContext.ClientId); + + foreach (var state in documentStates) + { + await state.WriteLockedAsync(@event, async (encoder, @event, _, ct) => + { + await encoder.WriteVarUintAsync(MessageTypes.TypeAwareness, ct); + await encoder.WriteVarUintAsync(1, ct); + await encoder.WriteAwarenessAsync(@event.DocumentContext.ClientId, @event.ClientClock, @event.ClientState, ct); + }, default); + } + } + + public async ValueTask OnDocumentChangedAsync(DocumentChangedEvent @event) + { + var documentStates = GetOtherClients(@event.DocumentContext.DocumentName, @event.DocumentContext.ClientId); + + foreach (var state in documentStates) + { + await state.WriteLockedAsync(@event.Diff, async (encoder, diff, _, ct) => + { + if (state.IsSynced) + { + await encoder.WriteSyncUpdateAsync(diff, ct); + } + else + { + state.PendingUpdates.Enqueue(@event.Diff); + } + }, default); + } + } + + public async Task InvokeAsync(HttpContext httpContext) + { + if (!httpContext.WebSockets.IsWebSocketRequest || documentManager == null) + { + httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + return; + } + + var documentName = httpContext.Request.Path; + var documentStates = statesPerDocumentName.GetOrAdd(documentName, _ => new List()); + + var webSocket = await httpContext.WebSockets.AcceptWebSocketAsync(); + + using var state = new ClientState + { + Decoder = new WebSocketDecoder(webSocket), + DocumentContext = new DocumentContext { ClientId = 0, DocumentName = documentName }, + DocumentName = documentName, + Encoder = new WebSocketEncoder(webSocket), + WebSocket = webSocket + }; + + // Usually we should have relatively few clients per document, therefore we use a simple lock. + lock (documentStates) + { + documentStates.Add(state); + } + + try + { + var messageType = await state.Decoder.ReadVarUintAsync(httpContext.RequestAborted); + + switch (messageType) + { + case MessageTypes.TypeSync: + await HandleSyncAsync(state, httpContext.RequestAborted); + break; + + case MessageTypes.TypeAwareness: + await HandleAwarenessAsync(state, httpContext.RequestAborted); + break; + + default: + throw new InvalidOperationException("Protocol error."); + } + } + finally + { + // Usually we should have relatively few clients per document, therefore we use a simple lock. + lock (documentStates) + { + documentStates.Remove(state); + } + + if (state.DocumentContext.ClientId != 0) + { + await documentManager.DisconnectAsync(state.DocumentContext, default); + } + } + } + + private async Task HandleSyncAsync(ClientState state, + CancellationToken ct) + { + var syncType = await state.Decoder.ReadVarUintAsync(ct); + + switch (syncType) + { + case MessageTypes.SyncStep1: + var stateVector = await state.Decoder.ReadVarUint8ArrayAsync(ct); + + var (update, serverState) = await documentManager!.GetMissingChangesAsync(state.DocumentContext, stateVector, ct); + + await state.WriteLockedAsync((update, serverState), async (encoder, context, state, ct) => + { + state.IsSynced = false; + + var (update, serverState) = context; + + await encoder.WriteSyncStep2Async(update, ct); + await encoder.WriteSyncStep1Async(serverState, ct); + + while (state.PendingUpdates.TryDequeue(out var diff)) + { + await encoder.WriteSyncUpdateAsync(diff, ct); + } + + var users = await documentManager.GetAwarenessAsync(state.DocumentName, ct); + + if (users.Count != 0) + { + await encoder.WriteVarUintAsync(MessageTypes.TypeAwareness, ct); + await encoder.WriteVarUintAsync(users.Count, ct); + + foreach (var (clientId, user) in await documentManager.GetAwarenessAsync(state.DocumentName, ct)) + { + await encoder.WriteAwarenessAsync(clientId, user.ClientClock, user.ClientState, ct); + } + } + + state.IsSynced = true; + }, ct); + + break; + + case MessageTypes.SyncUpdate: + var stateDiff = await state.Decoder.ReadVarUint8ArrayAsync(ct); + + await documentManager!.ApplyUpdateAsync(state.DocumentContext, stateDiff, ct); + break; + + default: + throw new InvalidOperationException("Protocol error."); + } + } + + private async Task HandleAwarenessAsync(ClientState state, + CancellationToken ct) + { + var clientCount = await state.Decoder.ReadVarUintAsync(ct); + + if (clientCount != 1) + { + throw new InvalidOperationException($"Protocol error. Expected client count to be 1, got {clientCount}."); + } + + var clientId = await state.Decoder.ReadVarUintAsync(ct); + var clientClock = await state.Decoder.ReadVarUintAsync(ct); + var clientState = await state.Decoder.ReadVarStringAsync(ct); + + state.DocumentContext.ClientId = clientId; + + await documentManager!.PingAsync(state.DocumentContext, clientClock, clientState, ct); + } + + private List GetOtherClients(string documentName, long clientId) + { + var documentStates = statesPerDocumentName.GetOrAdd(documentName, _ => new List()); + + // Usually we should have relatively few clients per document, therefore we use a simple lock. + lock (documentStates) + { + return documentStates.Where(x => x.DocumentContext.ClientId != clientId).ToList(); + } + } +} diff --git a/YDotNet.Server/ConnectedUser.cs b/YDotNet.Server/ConnectedUser.cs index 46f43549..6ddd2de6 100644 --- a/YDotNet.Server/ConnectedUser.cs +++ b/YDotNet.Server/ConnectedUser.cs @@ -2,7 +2,9 @@ namespace YDotNet.Server; public sealed class ConnectedUser { - public Dictionary State { get; set; } = new Dictionary(); + required public string? ClientState { get; set; } + + required public long ClientClock { get; set; } public DateTime LastActivity { get; set; } } diff --git a/YDotNet.Server/DefaultDocumentManager.cs b/YDotNet.Server/DefaultDocumentManager.cs index 24cc29f7..4300e85a 100644 --- a/YDotNet.Server/DefaultDocumentManager.cs +++ b/YDotNet.Server/DefaultDocumentManager.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using YDotNet.Document; @@ -9,7 +10,7 @@ namespace YDotNet.Server; -public sealed class DefaultDocumentManager : IDocumentManager +public sealed class DefaultDocumentManager : IDocumentManager, IHostedService { private readonly ConnectedUsers users = new(); private readonly DocumentManagerOptions options; @@ -25,7 +26,19 @@ public DefaultDocumentManager(IDocumentStorage documentStorage, IEnumerable GetMissingChangesAsync(DocumentContext context, byte[] stateVector, + public async Task StartAsync( + CancellationToken cancellationToken) + { + await callbacks.OnInitializedAsync(this); + } + + public async Task StopAsync( + CancellationToken cancellationToken) + { + await containers.DisposeAsync(); + } + + public async ValueTask<(byte[] Update, byte[] StateVector)> GetMissingChangesAsync(DocumentContext context, byte[] stateVector, CancellationToken ct = default) { var container = containers.GetContext(context.DocumentName); @@ -39,7 +52,7 @@ public async ValueTask GetMissingChangesAsync(DocumentContext context, b throw new InvalidOperationException("Transaction cannot be created."); } - return transaction.StateDiffV2(stateVector); + return (transaction.StateDiffV2(stateVector), transaction.StateVectorV1()); } }, null); } @@ -138,31 +151,17 @@ await callbacks.OnDocumentChangedAsync(new DocumentChangedEvent } } - public async ValueTask PingAsync(DocumentContext context, + public async ValueTask PingAsync(DocumentContext context, long clock, string? state = null, CancellationToken ct = default) { - if (users.Add(context.DocumentName, context.ClientId)) - { - await callbacks.OnClientConnectedAsync(new ClientConnectedEvent - { - DocumentContext = context, - DocumentManager = this, - }); - } - } - - public async ValueTask UpdateAwarenessAsync(DocumentContext context, string key, object value, - CancellationToken ct = default) - { - var user = users.SetAwareness(context.DocumentName, context.ClientId, key, value); - - if (user != null) + if (users.AddOrUpdate(context.DocumentName, context.ClientId, clock, state, out var newState)) { await callbacks.OnAwarenessUpdatedAsync(new ClientAwarenessEvent { DocumentContext = context, DocumentManager = this, - LocalState = null!, + ClientClock = clock, + ClientState = newState }); } } diff --git a/YDotNet.Server/DocumentRequestContext.cs b/YDotNet.Server/DocumentRequestContext.cs index 1418e476..270eb065 100644 --- a/YDotNet.Server/DocumentRequestContext.cs +++ b/YDotNet.Server/DocumentRequestContext.cs @@ -2,9 +2,9 @@ namespace YDotNet.Server; public sealed class DocumentContext { - required public string DocumentName { get; init; } + required public string DocumentName { get; set; } - required public long ClientId { get; init; } + required public long ClientId { get; set; } - public object? Metadata { get; init; } + public object? Metadata { get; set; } } diff --git a/YDotNet.Server/Events.cs b/YDotNet.Server/Events.cs index ca8ac619..f9b77b24 100644 --- a/YDotNet.Server/Events.cs +++ b/YDotNet.Server/Events.cs @@ -19,22 +19,13 @@ public sealed class DocumentChangedEvent : DocumentChangeEvent required public byte[] Diff { get; init; } } -public sealed class DocumentStoreEvent : DocumentEvent -{ - required public Doc Document { get; init; } - - required public byte[] StateVector { get; init; } -} - -public sealed class ClientConnectedEvent : DocumentEvent -{ -} - public sealed class ClientDisconnectedEvent : DocumentEvent { } public sealed class ClientAwarenessEvent : DocumentEvent { - required public Dictionary LocalState { get; set; } + required public string? ClientState { get; set; } + + required public long ClientClock { get; set; } } diff --git a/YDotNet.Server/IDocumentCallback.cs b/YDotNet.Server/IDocumentCallback.cs index 8d779d0d..67a317bd 100644 --- a/YDotNet.Server/IDocumentCallback.cs +++ b/YDotNet.Server/IDocumentCallback.cs @@ -2,27 +2,17 @@ namespace YDotNet.Server; public interface IDocumentCallback { - ValueTask OnDocumentChangingAsync(DocumentChangeEvent @event) - { - return default; - } - - ValueTask OnDocumentChangedAsync(DocumentChangedEvent @event) + ValueTask OnInitializedAsync(IDocumentManager manager) { return default; } - ValueTask OnDocumentStoringAsync(DocumentStoreEvent @event) - { - return default; - } - - ValueTask OnDocumentStoredAsync(DocumentStoreEvent @event) + ValueTask OnDocumentChangingAsync(DocumentChangeEvent @event) { return default; } - ValueTask OnClientConnectedAsync(ClientConnectedEvent @event) + ValueTask OnDocumentChangedAsync(DocumentChangedEvent @event) { return default; } diff --git a/YDotNet.Server/IDocumentManager.cs b/YDotNet.Server/IDocumentManager.cs index 93008519..393b1207 100644 --- a/YDotNet.Server/IDocumentManager.cs +++ b/YDotNet.Server/IDocumentManager.cs @@ -5,16 +5,13 @@ namespace YDotNet.Server; public interface IDocumentManager { - ValueTask PingAsync(DocumentContext context, + ValueTask PingAsync(DocumentContext context, long clock, string? clientState = null, CancellationToken ct = default); ValueTask DisconnectAsync(DocumentContext context, CancellationToken ct = default); - ValueTask UpdateAwarenessAsync(DocumentContext context, string key, object value, - CancellationToken ct = default); - - ValueTask GetMissingChangesAsync(DocumentContext context, byte[] stateVector, + ValueTask<(byte[] Update, byte[] StateVector)> GetMissingChangesAsync(DocumentContext context, byte[] stateVector, CancellationToken ct = default); ValueTask> GetAwarenessAsync(string roomName, diff --git a/YDotNet.Server/Internal/CallbackManager.cs b/YDotNet.Server/Internal/CallbackManager.cs index eab68041..8637fd4d 100644 --- a/YDotNet.Server/Internal/CallbackManager.cs +++ b/YDotNet.Server/Internal/CallbackManager.cs @@ -10,10 +10,14 @@ public sealed class CallbackManager : IDocumentCallback public CallbackManager(IEnumerable callbacks, ILogger logger) { this.callbacks = callbacks.ToList(); - this.logger = logger; } + public ValueTask OnInitializedAsync(IDocumentManager manager) + { + return InvokeCallbackAsync(manager, (c, m) => c.OnInitializedAsync(m)); + } + public ValueTask OnDocumentChangingAsync(DocumentChangeEvent @event) { return InvokeCallbackAsync(@event, (c, e) => c.OnDocumentChangingAsync(e)); @@ -24,11 +28,6 @@ public ValueTask OnDocumentChangedAsync(DocumentChangedEvent @event) return InvokeCallbackAsync(@event, (c, e) => c.OnDocumentChangedAsync(e)); } - public ValueTask OnClientConnectedAsync(ClientConnectedEvent @event) - { - return InvokeCallbackAsync(@event, (c, e) => c.OnClientConnectedAsync(e)); - } - public ValueTask OnClientDisconnectedAsync(ClientDisconnectedEvent[] events) { return InvokeCallbackAsync(@events, (c, e) => c.OnClientDisconnectedAsync(e)); diff --git a/YDotNet.Server/Internal/ConnectedUsers.cs b/YDotNet.Server/Internal/ConnectedUsers.cs index 8904b62c..da50117b 100644 --- a/YDotNet.Server/Internal/ConnectedUsers.cs +++ b/YDotNet.Server/Internal/ConnectedUsers.cs @@ -14,62 +14,44 @@ public IReadOnlyDictionary GetUsers(string documentName) lock (documentUsers) { - return documentUsers.ToDictionary( - x => x.Key, - x => new ConnectedUser - { - State = new Dictionary(x.Value.State) - }); + return new Dictionary(documentUsers); } } - public bool Add(string documentName, long clientId) + public bool AddOrUpdate(string documentName, long clientId, long clock, string? state, out string? existingState) { var documentUsers = users.GetOrAdd(documentName, _ => new Dictionary()); lock (documentUsers) { - if (documentUsers.ContainsKey(clientId)) + if (documentUsers.TryGetValue(clientId, out var user)) { - return false; + var isChanged = false; + + if (clock > user.ClientClock) + { + user.ClientClock = clock; + user.ClientState = state; + } + + existingState = user.ClientState; + + user.LastActivity = Clock(); + return isChanged; } documentUsers.Add(clientId, new ConnectedUser { + ClientClock = clock, + ClientState = state, LastActivity = Clock(), }); + existingState = state; return true; } } - public ConnectedUser SetAwareness(string documentName, long clientId, string key, object value) - { - var documentUsers = users.GetOrAdd(documentName, _ => new Dictionary()); - - ConnectedUser user; - - lock (documentUsers) - { - if (!documentUsers.TryGetValue(clientId, out user!)) - { - user = new ConnectedUser - { - LastActivity = Clock(), - }; - - documentUsers.Add(clientId, user); - } - } - - lock (user) - { - user.State[key] = value; - } - - return user; - } - public bool Remove(string documentName, long clientId) { if (!users.TryGetValue(documentName, out var documentUsers)) diff --git a/YDotNet.Server/ServiceExtensions.cs b/YDotNet.Server/ServiceExtensions.cs index f744c75a..6f92c30d 100644 --- a/YDotNet.Server/ServiceExtensions.cs +++ b/YDotNet.Server/ServiceExtensions.cs @@ -1,7 +1,15 @@ -namespace YDotNet.Server +using Microsoft.Extensions.DependencyInjection.Extensions; +using YDotNet.Server; +using YDotNet.Server.Storage; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class ServiceExtensions { - public class ServiceExtensions + public static void AddYDotNet(IServiceCollection services) { - + services.AddOptions(); + services.TryAddSingleton(); + services.TryAddSingleton(); } } diff --git a/YDotNet.Server/Storage/InMemoryDocStorage.cs b/YDotNet.Server/Storage/InMemoryDocStorage.cs index 71c71443..e0e6d26f 100644 --- a/YDotNet.Server/Storage/InMemoryDocStorage.cs +++ b/YDotNet.Server/Storage/InMemoryDocStorage.cs @@ -3,7 +3,7 @@ namespace YDotNet.Server.Storage; -public sealed class InMemoryDocStorage : IDocumentStorage +public sealed class InMemoryDocumentStorage : IDocumentStorage { private readonly ConcurrentDictionary docs = new(); diff --git a/YDotNet.Server/YDotNet.Server.csproj b/YDotNet.Server/YDotNet.Server.csproj index 5cf7f923..6c1752bc 100644 --- a/YDotNet.Server/YDotNet.Server.csproj +++ b/YDotNet.Server/YDotNet.Server.csproj @@ -17,6 +17,7 @@ + diff --git a/YDotNet.sln b/YDotNet.sln index 2a111d18..9af8a27c 100644 --- a/YDotNet.sln +++ b/YDotNet.sln @@ -9,7 +9,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{03858045 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YDotNet.Tests.Unit", "Tests\YDotNet.Tests.Unit\YDotNet.Tests.Unit.csproj", "{F85C9334-87AF-4FFC-A301-3A9F57ECD4AB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YDotNet.Server", "YDotNet.Server\YDotNet.Server.csproj", "{F24C69F2-E08A-4C93-A173-3F24FD4A0E7A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YDotNet.Server", "YDotNet.Server\YDotNet.Server.csproj", "{F24C69F2-E08A-4C93-A173-3F24FD4A0E7A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YDotNet.Server.Redis", "YDotNet.Server.Redis\YDotNet.Server.Redis.csproj", "{99705343-FB53-47F5-8601-FC89BA565F94}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YDotNet.Server.WebSockets", "YDotNet.Server.WebSockets\YDotNet.Server.WebSockets.csproj", "{273EF3C8-55E5-46EC-A985-ED677F41133D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -29,6 +33,14 @@ Global {F24C69F2-E08A-4C93-A173-3F24FD4A0E7A}.Debug|Any CPU.Build.0 = Debug|Any CPU {F24C69F2-E08A-4C93-A173-3F24FD4A0E7A}.Release|Any CPU.ActiveCfg = Release|Any CPU {F24C69F2-E08A-4C93-A173-3F24FD4A0E7A}.Release|Any CPU.Build.0 = Release|Any CPU + {99705343-FB53-47F5-8601-FC89BA565F94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {99705343-FB53-47F5-8601-FC89BA565F94}.Debug|Any CPU.Build.0 = Debug|Any CPU + {99705343-FB53-47F5-8601-FC89BA565F94}.Release|Any CPU.ActiveCfg = Release|Any CPU + {99705343-FB53-47F5-8601-FC89BA565F94}.Release|Any CPU.Build.0 = Release|Any CPU + {273EF3C8-55E5-46EC-A985-ED677F41133D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {273EF3C8-55E5-46EC-A985-ED677F41133D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {273EF3C8-55E5-46EC-A985-ED677F41133D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {273EF3C8-55E5-46EC-A985-ED677F41133D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/YDotNet/Protocol/Decoder.cs b/YDotNet/Protocol/Decoder.cs new file mode 100644 index 00000000..c69b96a5 --- /dev/null +++ b/YDotNet/Protocol/Decoder.cs @@ -0,0 +1,81 @@ +using System; +using System.Buffers; +using System.Text; + +namespace YDotNet.Protocol; + +public abstract class Decoder +{ + private const int BITS7 = 1 << 7; + private const int BITS8 = 1 << 8; + private readonly byte[] stringBuffer = new byte[128]; + + public async ValueTask ReadVarUintAsync( + CancellationToken ct = default) + { + int num = 0, multiplicator = 1; + + while (true) + { + var value = await ReadByteAsync(ct); + + num += (value & BITS7) * multiplicator; + + if (num < BITS8) + { + return num; + } + + multiplicator *= 128; + } + + throw new IndexOutOfRangeException(); + } + + public async ValueTask ReadVarUint8ArrayAsync( + CancellationToken ct) + { + var arrayLength = await ReadVarUintAsync(ct); + var arrayBuffer = new byte[arrayLength]; + + await ReadBytesAsync(arrayBuffer, ct); + + return arrayBuffer; + } + + public async ValueTask ReadVarStringAsync( + CancellationToken ct) + { + var length = (int)await ReadVarUintAsync(ct); + if (length > stringBuffer.Length) + { + var buffer = ArrayPool.Shared.Rent(length); + try + { + return await ReadCoreAsync(length, buffer, ct); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + else + { + return await ReadCoreAsync(length, stringBuffer, ct); + } + + async ValueTask ReadCoreAsync(int length, byte[] buffer, CancellationToken ct) + { + var slicedBuffer = buffer.AsMemory(0, length); + await ReadBytesAsync(slicedBuffer, ct); + + return Encoding.UTF8.GetString(slicedBuffer.Span); + } + } + + protected abstract ValueTask ReadByteAsync( + CancellationToken ct); + + protected abstract ValueTask ReadBytesAsync(Memory bytes, + CancellationToken ct); +} diff --git a/YDotNet/Protocol/Encoder.cs b/YDotNet/Protocol/Encoder.cs new file mode 100644 index 00000000..ebf81f80 --- /dev/null +++ b/YDotNet/Protocol/Encoder.cs @@ -0,0 +1,73 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace YDotNet.Protocol; + +public abstract class Encoder +{ + private const int BITS7 = 1 << 7; + private const int BITS8 = 1 << 8; + private readonly byte[] stringBuffer = new byte[128]; + + public async ValueTask WriteVarUintAsync(long value, + CancellationToken ct = default) + { + while (value > BITS7) + { + await WriteByteAsync((byte)(BITS8 | (BITS7 & value)), ct); + value >>= 7; + } + + await WriteByteAsync((byte)(BITS7 & value), ct); + } + + public async ValueTask WriteVarUint8Array(byte[] value, + CancellationToken ct = default) + { + await WriteVarUintAsync(value.Length, ct); + await WriteBytesAsync(value, ct); + } + + public async ValueTask WriteVarStringAsync(string value, + CancellationToken ct = default) + { + var length = Encoding.UTF8.GetByteCount(value); + if (length > stringBuffer.Length) + { + var buffer = ArrayPool.Shared.Rent(length); + try + { + await WriteCoreAsync(value, buffer, ct); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + else + { + await WriteCoreAsync(value, stringBuffer, ct); + } + + async Task WriteCoreAsync(string value, byte[] buffer, CancellationToken ct) + { + var length = Encoding.UTF8.GetBytes(value, buffer); + + await WriteBytesAsync(new ArraySegment(buffer, 0, length), ct); + } + } + + public abstract ValueTask EndMessageAsync( + CancellationToken ct = default); + + protected abstract ValueTask WriteByteAsync(byte value, + CancellationToken ct); + + protected abstract ValueTask WriteBytesAsync(ArraySegment bytes, + CancellationToken ct); +} diff --git a/YDotNet/stylecop.json b/YDotNet/stylecop.json new file mode 100644 index 00000000..42fb1f8e --- /dev/null +++ b/YDotNet/stylecop.json @@ -0,0 +1,14 @@ +{ + // ACTION REQUIRED: This file was automatically added to your project, but it + // will not take effect until additional steps are taken to enable it. See the + // following page for additional information: + // + // https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/EnableConfiguration.md + + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "PlaceholderCompany" + } + } +} From 1afecfd1006eb471de9794cfe293f59b12ba2d85 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 2 Oct 2023 10:52:04 +0200 Subject: [PATCH 006/186] Service extensions. --- YDotNet.Server.Redis/ServiceExtensions.cs | 13 ++++++++---- .../ServiceExtensions.cs | 20 +++++++++++++++++++ .../YDotNetActionResult.cs | 14 +++++++++++++ YDotNet.Server/DefaultDocumentManager.cs | 4 ++-- YDotNet.Server/IDocumentManager.cs | 5 +++-- YDotNet.Server/ServiceExtensions.cs | 16 ++++++++++++++- 6 files changed, 63 insertions(+), 9 deletions(-) create mode 100644 YDotNet.Server.WebSockets/ServiceExtensions.cs create mode 100644 YDotNet.Server.WebSockets/YDotNetActionResult.cs diff --git a/YDotNet.Server.Redis/ServiceExtensions.cs b/YDotNet.Server.Redis/ServiceExtensions.cs index c2fe6d10..15cc4377 100644 --- a/YDotNet.Server.Redis/ServiceExtensions.cs +++ b/YDotNet.Server.Redis/ServiceExtensions.cs @@ -1,13 +1,18 @@ -using YDotNet.Server; +using Microsoft.Extensions.Hosting; using YDotNet.Server.Redis; namespace Microsoft.Extensions.DependencyInjection; public static class ServiceExtensions { - public static void AddYDotNetRedisClustering(IServiceCollection services, Action configure) + public static YDotnetRegistration AddRedisClustering(this YDotnetRegistration registration, Action configure) { - services.Configure(configure); - services.AddSingleton(); + registration.Services.Configure(configure); + registration.Services.AddSingleton(); + + registration.Services.AddSingleton(x => + x.GetRequiredService()); + + return registration; } } diff --git a/YDotNet.Server.WebSockets/ServiceExtensions.cs b/YDotNet.Server.WebSockets/ServiceExtensions.cs new file mode 100644 index 00000000..97d18414 --- /dev/null +++ b/YDotNet.Server.WebSockets/ServiceExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Builder; +using YDotNet.Server.WebSockets; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class ServiceExtensions +{ + public static YDotnetRegistration AddWebSockets(this YDotnetRegistration registration) + { + registration.Services.AddSingleton(); + return registration; + } + + public static void UseYDotnetWebSockets(this IApplicationBuilder app) + { + var middleware = app.ApplicationServices.GetRequiredService(); + + app.Run(middleware.InvokeAsync); + } +} diff --git a/YDotNet.Server.WebSockets/YDotNetActionResult.cs b/YDotNet.Server.WebSockets/YDotNetActionResult.cs new file mode 100644 index 00000000..b91bdf69 --- /dev/null +++ b/YDotNet.Server.WebSockets/YDotNetActionResult.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; + +namespace YDotNet.Server.WebSockets; + +public sealed class YDotNetActionResult : IActionResult +{ + public async Task ExecuteResultAsync(ActionContext context) + { + var middleware = context.HttpContext.RequestServices.GetRequiredService(); + + await middleware.InvokeAsync(context.HttpContext); + } +} diff --git a/YDotNet.Server/DefaultDocumentManager.cs b/YDotNet.Server/DefaultDocumentManager.cs index 4300e85a..b258d765 100644 --- a/YDotNet.Server/DefaultDocumentManager.cs +++ b/YDotNet.Server/DefaultDocumentManager.cs @@ -10,7 +10,7 @@ namespace YDotNet.Server; -public sealed class DefaultDocumentManager : IDocumentManager, IHostedService +public sealed class DefaultDocumentManager : IDocumentManager { private readonly ConnectedUsers users = new(); private readonly DocumentManagerOptions options; @@ -161,7 +161,7 @@ await callbacks.OnAwarenessUpdatedAsync(new ClientAwarenessEvent DocumentContext = context, DocumentManager = this, ClientClock = clock, - ClientState = newState + ClientState = newState }); } } diff --git a/YDotNet.Server/IDocumentManager.cs b/YDotNet.Server/IDocumentManager.cs index 393b1207..f6c51923 100644 --- a/YDotNet.Server/IDocumentManager.cs +++ b/YDotNet.Server/IDocumentManager.cs @@ -1,11 +1,12 @@ +using Microsoft.Extensions.Hosting; using YDotNet.Document; using YDotNet.Document.Transactions; namespace YDotNet.Server; -public interface IDocumentManager +public interface IDocumentManager : IHostedService { - ValueTask PingAsync(DocumentContext context, long clock, string? clientState = null, + ValueTask PingAsync(DocumentContext context, long clock, string? state = null, CancellationToken ct = default); ValueTask DisconnectAsync(DocumentContext context, diff --git a/YDotNet.Server/ServiceExtensions.cs b/YDotNet.Server/ServiceExtensions.cs index 6f92c30d..6362f7c9 100644 --- a/YDotNet.Server/ServiceExtensions.cs +++ b/YDotNet.Server/ServiceExtensions.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; using YDotNet.Server; using YDotNet.Server.Storage; @@ -6,10 +7,23 @@ namespace Microsoft.Extensions.DependencyInjection; public static class ServiceExtensions { - public static void AddYDotNet(IServiceCollection services) + public static YDotnetRegistration AddYDotNet(IServiceCollection services) { services.AddOptions(); services.TryAddSingleton(); services.TryAddSingleton(); + + services.AddSingleton(x => + x.GetRequiredService()); + + return new YDotnetRegistration + { + Services = services + }; } } + +public sealed class YDotnetRegistration +{ + required public IServiceCollection Services { get; init; } +} From db32c1827b343427da50af0f89d5a82ffb81d42c Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 2 Oct 2023 10:52:33 +0200 Subject: [PATCH 007/186] Rename file. --- .../Internal/{CallbackManager.cs => CallbackInvoker.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename YDotNet.Server/Internal/{CallbackManager.cs => CallbackInvoker.cs} (100%) diff --git a/YDotNet.Server/Internal/CallbackManager.cs b/YDotNet.Server/Internal/CallbackInvoker.cs similarity index 100% rename from YDotNet.Server/Internal/CallbackManager.cs rename to YDotNet.Server/Internal/CallbackInvoker.cs From 24906be252d3a9ed1e49f5a47d1799b5077434e4 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 2 Oct 2023 15:06:52 +0200 Subject: [PATCH 008/186] Temp. --- .gitignore | 7 + Demo/Client/.eslintrc.cjs | 18 + Demo/Client/.gitignore | 24 + Demo/Client/README.md | 27 + Demo/Client/index.html | 13 + Demo/Client/package-lock.json | 4087 +++++++++++++++++ Demo/Client/package.json | 41 + Demo/Client/public/vite.svg | 1 + Demo/Client/src/App.css | 42 + Demo/Client/src/App.tsx | 14 + Demo/Client/src/assets/react.svg | 1 + .../Client/src/components/YjsMonacoEditor.tsx | 31 + Demo/Client/src/components/YjsProseMirror.tsx | 40 + Demo/Client/src/context/yjsContext.tsx | 27 + Demo/Client/src/hooks/useYjs.ts | 12 + Demo/Client/src/index.css | 0 Demo/Client/src/main.tsx | 12 + Demo/Client/src/vite-env.d.ts | 1 + Demo/Client/tsconfig.json | 25 + Demo/Client/tsconfig.node.json | 10 + Demo/Client/vite.config.ts | 7 + Demo/Demo.csproj | 19 + Demo/Program.cs | 26 + Demo/appsettings.Development.json | 9 + Demo/appsettings.json | 9 + Demo/package-lock.json | 6 + Demo/wwwroot/favicon.ico | Bin 0 -> 5430 bytes YDotNet.Server.WebSockets/ClientState.cs | 3 +- .../EncoderExtensions.cs | 18 +- YDotNet.Server.WebSockets/MessageTypes.cs | 4 +- .../ServiceExtensions.cs | 6 + YDotNet.Server.WebSockets/WebSocketDecoder.cs | 17 +- YDotNet.Server.WebSockets/WebSocketEncoder.cs | 6 +- .../YDotNetSocketMiddleware.cs | 82 +- YDotNet.Server/DefaultDocumentManager.cs | 16 +- YDotNet.Server/Internal/CallbackInvoker.cs | 4 +- YDotNet.Server/ServiceExtensions.cs | 2 +- YDotNet.sln | 9 + YDotNet/Protocol/Decoder.cs | 53 +- YDotNet/Protocol/Encoder.cs | 22 +- 40 files changed, 4681 insertions(+), 70 deletions(-) create mode 100644 Demo/Client/.eslintrc.cjs create mode 100644 Demo/Client/.gitignore create mode 100644 Demo/Client/README.md create mode 100644 Demo/Client/index.html create mode 100644 Demo/Client/package-lock.json create mode 100644 Demo/Client/package.json create mode 100644 Demo/Client/public/vite.svg create mode 100644 Demo/Client/src/App.css create mode 100644 Demo/Client/src/App.tsx create mode 100644 Demo/Client/src/assets/react.svg create mode 100644 Demo/Client/src/components/YjsMonacoEditor.tsx create mode 100644 Demo/Client/src/components/YjsProseMirror.tsx create mode 100644 Demo/Client/src/context/yjsContext.tsx create mode 100644 Demo/Client/src/hooks/useYjs.ts create mode 100644 Demo/Client/src/index.css create mode 100644 Demo/Client/src/main.tsx create mode 100644 Demo/Client/src/vite-env.d.ts create mode 100644 Demo/Client/tsconfig.json create mode 100644 Demo/Client/tsconfig.node.json create mode 100644 Demo/Client/vite.config.ts create mode 100644 Demo/Demo.csproj create mode 100644 Demo/Program.cs create mode 100644 Demo/appsettings.Development.json create mode 100644 Demo/appsettings.json create mode 100644 Demo/package-lock.json create mode 100644 Demo/wwwroot/favicon.ico diff --git a/.gitignore b/.gitignore index 77a71251..19a2a345 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,10 @@ msbuild.wrn # Project-specific files *.dll *.dylib + +# Node +node_modules + +apSettings.Development.json + +launchSettings.json \ No newline at end of file diff --git a/Demo/Client/.eslintrc.cjs b/Demo/Client/.eslintrc.cjs new file mode 100644 index 00000000..d6c95379 --- /dev/null +++ b/Demo/Client/.eslintrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/Demo/Client/.gitignore b/Demo/Client/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/Demo/Client/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/Demo/Client/README.md b/Demo/Client/README.md new file mode 100644 index 00000000..1ebe379f --- /dev/null +++ b/Demo/Client/README.md @@ -0,0 +1,27 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@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 +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json'], + tsconfigRootDir: __dirname, + }, +``` + +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- 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 diff --git a/Demo/Client/index.html b/Demo/Client/index.html new file mode 100644 index 00000000..e4b78eae --- /dev/null +++ b/Demo/Client/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/Demo/Client/package-lock.json b/Demo/Client/package-lock.json new file mode 100644 index 00000000..08b4d7fc --- /dev/null +++ b/Demo/Client/package-lock.json @@ -0,0 +1,4087 @@ +{ + "name": "client", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "client", + "version": "0.0.0", + "dependencies": { + "bootstrap": "^4.6.0", + "lib0": "0.2.35", + "monaco-editor": "0.22.3", + "prosemirror-keymap": "^1.1.4", + "prosemirror-schema-basic": "^1.1.2", + "prosemirror-view": "^1.17.5", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-monaco-editor": "^0.42.0", + "reactstrap": "^8.9.0", + "y-monaco": "^0.1.2", + "y-prosemirror": "^1.0.5", + "y-protocols": "^1.0.3", + "y-websocket": "^1.5.0", + "yjs": "13.5.6" + }, + "devDependencies": { + "@types/react": "^18.2.15", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "@vitejs/plugin-react": "^4.0.3", + "eslint": "^8.45.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.3", + "typescript": "^5.0.2", + "vite": "^4.4.5" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.20.tgz", + "integrity": "sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.0.tgz", + "integrity": "sha512-97z/ju/Jy1rZmDxybphrBuI+jtJjFVoz7Mr9yUQVVVi+DNZE333uFQeMOqcCIy1x3WYBIbWftUSLmbNXNT7qFQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.0", + "@babel/helpers": "^7.23.0", + "@babel/parser": "^7.23.0", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.0", + "@babel/types": "^7.23.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz", + "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", + "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.23.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.1.tgz", + "integrity": "sha512-chNpneuK18yW5Oxsr+t553UZzzAs3aZnFm4bxhebsNTeshrC95yA7l5yl7GBAG+JG1rF0F7zzD2EixK9mWSDoA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.0", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.22.5.tgz", + "integrity": "sha512-nTh2ogNUtxbiSbxaT4Ds6aXnXEipHweN9YRgOX/oNXdf0cCrGn/+2LozFa3lnPV5D90MkjhgckCPBrsoSc1a7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.22.5.tgz", + "integrity": "sha512-yIiRO6yobeEIaI0RTbIr8iAK9FcBHLtZq0S89ZPjDLQXBA4xvghaKqI0etp/tF3htTM0sazJKKLz9oEiGRtu7w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.23.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz", + "integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.0.tgz", + "integrity": "sha512-t/QaEvyIoIkwzpiZ7aoSKK8kObQYeF7T2v+dazAYCb8SXtp58zEVkWW7zAnju8FNKNdr4ScAOEDmMItbyOmEYw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.1.tgz", + "integrity": "sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.22.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", + "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", + "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", + "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@hypnosphi/create-react-context": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@hypnosphi/create-react-context/-/create-react-context-0.3.1.tgz", + "integrity": "sha512-V1klUed202XahrWJLLOT3EXNeCpFHCcJntdFGI15ntCwau+jfT386w7OFTMaCqOgXUH1fa0w/I1oZs+i/Rfr0A==", + "dependencies": { + "gud": "^1.0.0", + "warning": "^4.0.3" + }, + "peerDependencies": { + "prop-types": "^15.0.0", + "react": ">=0.14.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", + "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.2.tgz", + "integrity": "sha512-pNpr1T1xLUc2l3xJKuPtsEky3ybxN3m4fJkknfIpTCTfIZCDW57oAg+EfCgIIp2rvCe0Wn++/FfodDS4YXxBwA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.5.tgz", + "integrity": "sha512-h9yIuWbJKdOPLJTbmSpPzkF67e659PbQDba7ifWm5BJ8xTv+sDmS7rFmywkWOvXedGTivCdeGSIIX8WLcRTz8w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.2.tgz", + "integrity": "sha512-/AVzPICMhMOMYoSx9MoKpGDKdBRsIXMNByh1PXSZoa+v6ZoLa8xxtsT/uLQ/NJm0XVAWl/BvId4MlDeXJaeIZQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.2.tgz", + "integrity": "sha512-ojlGK1Hsfce93J0+kn3H5R73elidKUaZonirN33GSmgTUMpzI/MIFfSpF3haANe3G1bEBS9/9/QEqwTzwqFsKw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", + "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.8", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.8.tgz", + "integrity": "sha512-kMpQpfZKSCBqltAJwskgePRaYRFukDkm1oItcAbC3gNELR20XIBcN9VRgg4+m8DKsTfkWeA4m4Imp4DDuWy7FQ==" + }, + "node_modules/@types/react": { + "version": "18.2.24", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.24.tgz", + "integrity": "sha512-Ee0Jt4sbJxMu1iDcetZEIKQr99J1Zfb6D4F3qfUWoR1JpInkY1Wdg4WwCyBjL257D0+jGqSl1twBjV8iCaC0Aw==", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.8.tgz", + "integrity": "sha512-bAIvO5lN/U8sPGvs1Xm61rlRHHaq5rp5N3kp9C+NJ/Q41P8iqjkXSu0+/qu8POsjH9pNWb0OYabFez7taP7omw==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.4.tgz", + "integrity": "sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ==" + }, + "node_modules/@types/semver": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.3.tgz", + "integrity": "sha512-vntq452UHNltxsaaN+L9WyuMch8bMd9CqJ3zhzTPXXidwbf5mqqKCVXEuvRZUqLJSTLeWE65lQwyXsRGnXkCTA==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.7.3", + "@typescript-eslint/type-utils": "6.7.3", + "@typescript-eslint/utils": "6.7.3", + "@typescript-eslint/visitor-keys": "6.7.3", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.3.tgz", + "integrity": "sha512-TlutE+iep2o7R8Lf+yoer3zU6/0EAUc8QIBB3GYBc1KGz4c4TRm83xwXUZVPlZ6YCLss4r77jbu6j3sendJoiQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.7.3", + "@typescript-eslint/types": "6.7.3", + "@typescript-eslint/typescript-estree": "6.7.3", + "@typescript-eslint/visitor-keys": "6.7.3", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.7.3.tgz", + "integrity": "sha512-wOlo0QnEou9cHO2TdkJmzF7DFGvAKEnB82PuPNHpT8ZKKaZu6Bm63ugOTn9fXNJtvuDPanBc78lGUGGytJoVzQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.7.3", + "@typescript-eslint/visitor-keys": "6.7.3" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.7.3.tgz", + "integrity": "sha512-Fc68K0aTDrKIBvLnKTZ5Pf3MXK495YErrbHb1R6aTpfK5OdSFj0rVN7ib6Tx6ePrZ2gsjLqr0s98NG7l96KSQw==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "6.7.3", + "@typescript-eslint/utils": "6.7.3", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.7.3.tgz", + "integrity": "sha512-4g+de6roB2NFcfkZb439tigpAMnvEIg3rIjWQ+EM7IBaYt/CdJt6em9BJ4h4UpdgaBWdmx2iWsafHTrqmgIPNw==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.3.tgz", + "integrity": "sha512-YLQ3tJoS4VxLFYHTw21oe1/vIZPRqAO91z6Uv0Ss2BKm/Ag7/RVQBcXTGcXhgJMdA4U+HrKuY5gWlJlvoaKZ5g==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.7.3", + "@typescript-eslint/visitor-keys": "6.7.3", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.7.3.tgz", + "integrity": "sha512-vzLkVder21GpWRrmSR9JxGZ5+ibIUSudXlW52qeKpzUEQhRSmyZiVDDj3crAth7+5tmN1ulvgKaCU2f/bPRCzg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.7.3", + "@typescript-eslint/types": "6.7.3", + "@typescript-eslint/typescript-estree": "6.7.3", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.3.tgz", + "integrity": "sha512-HEVXkU9IB+nk9o63CeICMHxFWbHWr3E1mpilIQBe9+7L/lH97rleFLVtYsfnWB+JVMaiFnEaxvknvmIzX+CqVg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.7.3", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.1.0.tgz", + "integrity": "sha512-rM0SqazU9iqPUraQ2JlIvReeaxOoRj6n+PzB1C0cBzIbd8qP336nC39/R9yPi3wVcah7E7j/kdU1uCUqMEU4OQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.22.20", + "@babel/plugin-transform-react-jsx-self": "^7.22.5", + "@babel/plugin-transform-react-jsx-source": "^7.22.5", + "@types/babel__core": "^7.20.2", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0" + } + }, + "node_modules/abstract-leveldown": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz", + "integrity": "sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "immediate": "^3.2.3", + "level-concat-iterator": "~2.0.0", + "level-supports": "~1.0.0", + "xtend": "~4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "optional": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/bootstrap": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz", + "integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "peerDependencies": { + "jquery": "1.9.1 - 3", + "popper.js": "^1.16.1" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001542", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001542.tgz", + "integrity": "sha512-UrtAXVcj1mvPBFQ4sKd38daP8dEcXXr5sQe6QNNinaPd0iA/cxg9/l3VrSdL73jgw5sKyuQ6jNgiKO12W3SsVA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/classnames": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", + "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "dependencies": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deferred-leveldown": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-5.3.0.tgz", + "integrity": "sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw==", + "optional": true, + "dependencies": { + "abstract-leveldown": "~6.2.1", + "inherits": "^2.0.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/define-data-property": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz", + "integrity": "sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-helpers": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", + "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.537", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.537.tgz", + "integrity": "sha512-W1+g9qs9hviII0HAwOdehGYkr+zt7KKdmCcJcjH0mYg6oL8+ioT3Skjmt7BLoAQqXhjf40AXd+HlR4oAWMlXjA==", + "dev": true + }, + "node_modules/encoding-down": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/encoding-down/-/encoding-down-6.3.0.tgz", + "integrity": "sha512-QKrV0iKR6MZVJV08QY0wp1e7vF6QbhnbQhb07bwpEyuz4uZiZgPlEGdkCROuFkUwdxlFaiPIhjyarH1ee/3vhw==", + "optional": true, + "dependencies": { + "abstract-leveldown": "^6.2.1", + "inherits": "^2.0.3", + "level-codec": "^9.0.0", + "level-errors": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", + "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.50.0", + "@humanwhocodes/config-array": "^0.11.11", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.3.tgz", + "integrity": "sha512-Hh0wv8bUNY877+sI0BlCUlsS0TYYQqvzEwJsJJPM2WF4RnTStSnSR3zdJYa2nPOJgg3UghXi54lVyMSmpCalzA==", + "dev": true, + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.22.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", + "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", + "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", + "dev": true, + "dependencies": { + "flatted": "^3.2.7", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/gud": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz", + "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==" + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/immediate": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.3.0.tgz", + "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==", + "optional": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "devOptional": true + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isomorphic.js": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.1.5.tgz", + "integrity": "sha512-MkX5lLQApx/8IAIU31PKvpAZosnu2Jqcj1rM8TzxyA4CR96tv3SgMKQNTCxL58G7696Q57zd7ubHV/hTg+5fNA==", + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "peer": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", + "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/level": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/level/-/level-6.0.1.tgz", + "integrity": "sha512-psRSqJZCsC/irNhfHzrVZbmPYXDcEYhA5TVNwr+V92jF44rbf86hqGp8fiT702FyiArScYIlPSBTDUASCVNSpw==", + "optional": true, + "dependencies": { + "level-js": "^5.0.0", + "level-packager": "^5.1.0", + "leveldown": "^5.4.0" + }, + "engines": { + "node": ">=8.6.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/level" + } + }, + "node_modules/level-codec": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-9.0.2.tgz", + "integrity": "sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ==", + "optional": true, + "dependencies": { + "buffer": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-concat-iterator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/level-concat-iterator/-/level-concat-iterator-2.0.1.tgz", + "integrity": "sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/level-errors/-/level-errors-2.0.1.tgz", + "integrity": "sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw==", + "optional": true, + "dependencies": { + "errno": "~0.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-iterator-stream": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/level-iterator-stream/-/level-iterator-stream-4.0.2.tgz", + "integrity": "sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q==", + "optional": true, + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^3.4.0", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-js": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/level-js/-/level-js-5.0.2.tgz", + "integrity": "sha512-SnBIDo2pdO5VXh02ZmtAyPP6/+6YTJg2ibLtl9C34pWvmtMEmRTWpra+qO/hifkUtBTOtfx6S9vLDjBsBK4gRg==", + "optional": true, + "dependencies": { + "abstract-leveldown": "~6.2.3", + "buffer": "^5.5.0", + "inherits": "^2.0.3", + "ltgt": "^2.1.2" + } + }, + "node_modules/level-packager": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/level-packager/-/level-packager-5.1.1.tgz", + "integrity": "sha512-HMwMaQPlTC1IlcwT3+swhqf/NUO+ZhXVz6TY1zZIIZlIR0YSn8GtAAWmIvKjNY16ZkEg/JcpAuQskxsXqC0yOQ==", + "optional": true, + "dependencies": { + "encoding-down": "^6.3.0", + "levelup": "^4.3.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-supports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/level-supports/-/level-supports-1.0.1.tgz", + "integrity": "sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==", + "optional": true, + "dependencies": { + "xtend": "^4.0.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/leveldown": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/leveldown/-/leveldown-5.6.0.tgz", + "integrity": "sha512-iB8O/7Db9lPaITU1aA2txU/cBEXAt4vWwKQRrrWuS6XDgbP4QZGj9BL2aNbwb002atoQ/lIotJkfyzz+ygQnUQ==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "abstract-leveldown": "~6.2.1", + "napi-macros": "~2.0.0", + "node-gyp-build": "~4.1.0" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/levelup": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/levelup/-/levelup-4.4.0.tgz", + "integrity": "sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ==", + "optional": true, + "dependencies": { + "deferred-leveldown": "~5.3.0", + "level-errors": "~2.0.0", + "level-iterator-stream": "~4.0.0", + "level-supports": "~1.0.0", + "xtend": "~4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lib0": { + "version": "0.2.35", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.35.tgz", + "integrity": "sha512-drVD3EscB3TIxiFzceuZg7oF5Z6I8a0KX+7FowNcAXOEsTej/hlHB+ElJ8Pa/Ge73Gy3fklSJtPxpNd2PajdWg==", + "dependencies": { + "isomorphic.js": "^0.1.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ltgt": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ltgt/-/ltgt-2.2.1.tgz", + "integrity": "sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==", + "optional": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/monaco-editor": { + "version": "0.22.3", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.22.3.tgz", + "integrity": "sha512-RM559z2CJbczZ3k2b+ouacMINkAYWwRit4/vs0g2X/lkYefDiu0k2GmgWjAuiIpQi+AqASPOKvXNmYc8KUSvVQ==" + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-macros": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.0.0.tgz", + "integrity": "sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==", + "optional": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-gyp-build": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.1.1.tgz", + "integrity": "sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ==", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==" + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", + "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.2.tgz", + "integrity": "sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==", + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/prosemirror-model": { + "version": "1.19.3", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.19.3.tgz", + "integrity": "sha512-tgSnwN7BS7/UM0sSARcW+IQryx2vODKX4MI7xpqY2X+iaepJdKBPc7I4aACIsDV/LTaTjt12Z56MhDr9LsyuZQ==", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-schema-basic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.2.tgz", + "integrity": "sha512-/dT4JFEGyO7QnNTe9UaKUhjDXbTNkiWTq/N4VpKaF79bBjSExVV2NXmJpcM7z/gD7mbqNjxbmWW5nf1iNSSGnw==", + "dependencies": { + "prosemirror-model": "^1.19.0" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.3.tgz", + "integrity": "sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.8.0.tgz", + "integrity": "sha512-BaSBsIMv52F1BVVMvOmp1yzD3u65uC3HTzCBQV1WDPqJRQ2LuHKcyfn0jwqodo8sR9vVzMzZyI+Dal5W9E6a9A==", + "dependencies": { + "prosemirror-model": "^1.0.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.32.0.tgz", + "integrity": "sha512-HwW7IWgca6ehiW2PA48H/8yl0TakA0Ms5LgN5Krc97oar7GfjIKE/NocUsLe74Jq4mwyWKUNoBljE8WkXKZwng==", + "dependencies": { + "prosemirror-model": "^1.16.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "optional": true + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "node_modules/react-monaco-editor": { + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/react-monaco-editor/-/react-monaco-editor-0.42.0.tgz", + "integrity": "sha512-LMgiY6XU1To7tjPoMg497eST/Od01QOaUYr/tUUan4GRVBBVi5v9K7MvxMjg1FhViOgeNyD8XemsBb1cLzMxBA==", + "dependencies": { + "monaco-editor": "*", + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@types/react": "^17.x", + "react": "^17.x" + } + }, + "node_modules/react-refresh": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", + "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-transition-group": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-3.0.0.tgz", + "integrity": "sha512-A9ojB/LWECbFj58SNfjK1X9aaAU+1olLS0DFSikvrr2KfMaiBELemHDa5dKNvcTk2t3gUtDL/PJpFrBKDfMpLg==", + "dependencies": { + "dom-helpers": "^3.4.0", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/reactstrap": { + "version": "8.10.1", + "resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-8.10.1.tgz", + "integrity": "sha512-StjLADa/12yMNjafrSs+UD7sZAGtKpLO9fZp++2Dj0IzJinqY7eQhXlM3nFf0q40YsIcLvQdFc9pKF8PF4f0Qg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "classnames": "^2.2.3", + "prop-types": "^15.5.8", + "react-popper": "^1.3.6", + "react-transition-group": "^3.0.0" + }, + "peerDependencies": { + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + } + }, + "node_modules/reactstrap/node_modules/react-popper": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.11.tgz", + "integrity": "sha512-VSA/bS+pSndSF2fiasHK/PTEEAyOpX60+H5EPAjoArr8JGm+oihu4UbrqcEBpQibJxBVCpYyjAX7abJ+7DoYVg==", + "dependencies": { + "@babel/runtime": "^7.1.2", + "@hypnosphi/create-react-context": "^0.3.1", + "deep-equal": "^1.1.1", + "popper.js": "^1.14.4", + "prop-types": "^15.6.1", + "typed-styles": "^0.0.7", + "warning": "^4.0.2" + }, + "peerDependencies": { + "react": "0.14.x || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "engines": { + "node": ">=16.13.0" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-styles": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.7.tgz", + "integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==" + }, + "node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "optional": true + }, + "node_modules/vite": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", + "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", + "dev": true, + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" + }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", + "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", + "optional": true, + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "optional": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y-leveldb": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/y-leveldb/-/y-leveldb-0.1.2.tgz", + "integrity": "sha512-6ulEn5AXfXJYi89rXPEg2mMHAyyw8+ZfeMMdOtBbV8FJpQ1NOrcgi6DTAcXof0dap84NjHPT2+9d0rb6cFsjEg==", + "optional": true, + "dependencies": { + "level": "^6.0.1", + "lib0": "^0.2.31" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "yjs": "^13.0.0" + } + }, + "node_modules/y-monaco": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/y-monaco/-/y-monaco-0.1.4.tgz", + "integrity": "sha512-RzAvjKJ2yt3VjlKK55U8STJyd1gz5/Qm6pjt7SAVb+k+g+KaSeTuSBGt+lURN+dzzSNs73ZkIAerW3JM0mzBSw==", + "dependencies": { + "lib0": "^0.2.43" + }, + "peerDependencies": { + "monaco-editor": ">=0.20.0", + "yjs": "^13.3.1" + } + }, + "node_modules/y-monaco/node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/y-monaco/node_modules/lib0": { + "version": "0.2.86", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.86.tgz", + "integrity": "sha512-kxigQTM4Q7NwJkEgdqQvU21qiR37twcqqLmh+/SbiGbRLfPlLVbHyY9sWp7PwXh0Xus9ELDSjsUOwcrdt5yZ4w==", + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/y-prosemirror": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/y-prosemirror/-/y-prosemirror-1.2.1.tgz", + "integrity": "sha512-czMBfB1eL2awqmOSxQM8cS/fsUOGE6fjvyPLInrh4crPxFiw67wDpwIW+EGBYKRa04sYbS0ScGj7ZgvWuDrmBQ==", + "dependencies": { + "lib0": "^0.2.42" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "prosemirror-model": "^1.7.1", + "prosemirror-state": "^1.2.3", + "prosemirror-view": "^1.9.10", + "y-protocols": "^1.0.1", + "yjs": "^13.5.38" + } + }, + "node_modules/y-prosemirror/node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/y-prosemirror/node_modules/lib0": { + "version": "0.2.86", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.86.tgz", + "integrity": "sha512-kxigQTM4Q7NwJkEgdqQvU21qiR37twcqqLmh+/SbiGbRLfPlLVbHyY9sWp7PwXh0Xus9ELDSjsUOwcrdt5yZ4w==", + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/y-protocols": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz", + "integrity": "sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==", + "dependencies": { + "lib0": "^0.2.85" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "yjs": "^13.0.0" + } + }, + "node_modules/y-protocols/node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/y-protocols/node_modules/lib0": { + "version": "0.2.86", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.86.tgz", + "integrity": "sha512-kxigQTM4Q7NwJkEgdqQvU21qiR37twcqqLmh+/SbiGbRLfPlLVbHyY9sWp7PwXh0Xus9ELDSjsUOwcrdt5yZ4w==", + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/y-websocket": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/y-websocket/-/y-websocket-1.5.0.tgz", + "integrity": "sha512-A8AO6XtnQlYwWFytWdkDCeXg4l8ghRTIw5h2YUgUYDmEC9ugWGIwYNW80yadhSFAF7CvuWTEkQNEpevnH6EiZw==", + "dependencies": { + "lib0": "^0.2.52", + "lodash.debounce": "^4.0.8", + "y-protocols": "^1.0.5" + }, + "bin": { + "y-websocket": "bin/server.js", + "y-websocket-server": "bin/server.js" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "optionalDependencies": { + "ws": "^6.2.1", + "y-leveldb": "^0.1.0" + }, + "peerDependencies": { + "yjs": "^13.5.6" + } + }, + "node_modules/y-websocket/node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/y-websocket/node_modules/lib0": { + "version": "0.2.86", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.86.tgz", + "integrity": "sha512-kxigQTM4Q7NwJkEgdqQvU21qiR37twcqqLmh+/SbiGbRLfPlLVbHyY9sWp7PwXh0Xus9ELDSjsUOwcrdt5yZ4w==", + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yjs": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.5.6.tgz", + "integrity": "sha512-0ebPpLB/zizJbWaFUDRarWbXiXYD0OMDOCa8ZqkVVWQzeIoMRbmbNwB3suZ9YwD0bV4Su9RLn8M/bvGzIwX3hA==", + "hasInstallScript": true, + "dependencies": { + "lib0": "^0.2.41" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/yjs/node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/yjs/node_modules/lib0": { + "version": "0.2.86", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.86.tgz", + "integrity": "sha512-kxigQTM4Q7NwJkEgdqQvU21qiR37twcqqLmh+/SbiGbRLfPlLVbHyY9sWp7PwXh0Xus9ELDSjsUOwcrdt5yZ4w==", + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/Demo/Client/package.json b/Demo/Client/package.json new file mode 100644 index 00000000..dca72034 --- /dev/null +++ b/Demo/Client/package.json @@ -0,0 +1,41 @@ +{ + "name": "client", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "bootstrap": "^4.6.0", + "lib0": "0.2.35", + "monaco-editor": "0.22.3", + "prosemirror-keymap": "^1.1.4", + "prosemirror-schema-basic": "^1.1.2", + "prosemirror-view": "^1.17.5", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-monaco-editor": "^0.42.0", + "reactstrap": "^8.9.0", + "y-monaco": "^0.1.2", + "y-prosemirror": "^1.0.5", + "y-protocols": "^1.0.3", + "y-websocket": "^1.5.0", + "yjs": "13.5.6" + }, + "devDependencies": { + "@types/react": "^18.2.15", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "@vitejs/plugin-react": "^4.0.3", + "eslint": "^8.45.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.3", + "typescript": "^5.0.2", + "vite": "^4.4.5" + } +} diff --git a/Demo/Client/public/vite.svg b/Demo/Client/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/Demo/Client/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Demo/Client/src/App.css b/Demo/Client/src/App.css new file mode 100644 index 00000000..b9d355df --- /dev/null +++ b/Demo/Client/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/Demo/Client/src/App.tsx b/Demo/Client/src/App.tsx new file mode 100644 index 00000000..ce69c622 --- /dev/null +++ b/Demo/Client/src/App.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { YjsMonacoEditor } from './components/YjsMonacoEditor' + +function App() { + return ( + <> +
+ +
+ + ) +} + +export default App diff --git a/Demo/Client/src/assets/react.svg b/Demo/Client/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/Demo/Client/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Demo/Client/src/components/YjsMonacoEditor.tsx b/Demo/Client/src/components/YjsMonacoEditor.tsx new file mode 100644 index 00000000..34f9aa6d --- /dev/null +++ b/Demo/Client/src/components/YjsMonacoEditor.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import * as yMonaco from 'y-monaco'; +import MonacoEditor, { monaco } from 'react-monaco-editor'; +import { useYjs } from '../hooks/useYjs'; + +export const YjsMonacoEditor = () => { + const { yjsDocument, yjsConnector } = useYjs(); + const yText = yjsDocument.getText('monaco'); + + const setMonacoEditor = React.useState()[1]; + const setMonacoBinding = React.useState()[1]; + + const _onEditorDidMount = React.useCallback((editor: monaco.editor.ICodeEditor): void => { + editor.focus(); + editor.setValue(''); + + setMonacoEditor(editor); + setMonacoBinding(new yMonaco.MonacoBinding(yText, editor.getModel(), new Set([editor]), yjsConnector.awareness)); + }, [yjsConnector.awareness, yText, setMonacoEditor, setMonacoBinding]); + + return ( +
+ _onEditorDidMount(e)} + /> +
+ ); +}; diff --git a/Demo/Client/src/components/YjsProseMirror.tsx b/Demo/Client/src/components/YjsProseMirror.tsx new file mode 100644 index 00000000..b5b1c23a --- /dev/null +++ b/Demo/Client/src/components/YjsProseMirror.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { schema } from 'prosemirror-schema-basic'; +import { EditorState } from 'prosemirror-state'; +import { EditorView } from 'prosemirror-view'; +import { keymap } from 'prosemirror-keymap'; +import { ySyncPlugin, yCursorPlugin, yUndoPlugin, undo, redo } from 'y-prosemirror'; +import { useYjs } from '../hooks/useYjs'; + +export const YjsProseMirror = () => { + const { yjsDocument, yjsConnector } = useYjs(); + const yText = yjsDocument.getText('prosemirror'); + const viewHost = React.useRef(null); + const viewRef = React.useRef(null); + + React.useEffect(() => { + const state = EditorState.create({ + schema, + plugins: [ + ySyncPlugin(yText), + yCursorPlugin(yjsConnector.awareness), + yUndoPlugin(), + keymap({ + 'Mod-z': undo, + 'Mod-y': redo, + 'Mod-Shift-z': redo + }) + ] + }); + + viewRef.current = new EditorView(viewHost.current, { state }); + + return () => { + (viewRef.current as any)?.destroy(); + }; + }, [yText, yjsConnector.awareness]); + + return ( +
+ ); +}; diff --git a/Demo/Client/src/context/yjsContext.tsx b/Demo/Client/src/context/yjsContext.tsx new file mode 100644 index 00000000..661e252d --- /dev/null +++ b/Demo/Client/src/context/yjsContext.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import * as Y from 'yjs'; +import { WebsocketProvider } from 'y-websocket'; + +export interface IYjsContext { + readonly yjsDocument: Y.Doc; + readonly yjsConnector: WebsocketProvider; +} + +export interface IOptions extends React.PropsWithChildren<{}> { + readonly baseUrl: string; +} + +export const YjsContextProvider: React.FunctionComponent = (props: IOptions) => { + const { baseUrl } = props; + + const contextProps: IYjsContext = React.useMemo(() => { + const yjsDocument = new Y.Doc(); + const yjsConnector = new WebsocketProvider(baseUrl, 'test', yjsDocument); + + return { yjsDocument, yjsConnector }; + }, [baseUrl]); + + return {props.children}; +}; + +export const YjsContext = React.createContext(undefined); diff --git a/Demo/Client/src/hooks/useYjs.ts b/Demo/Client/src/hooks/useYjs.ts new file mode 100644 index 00000000..8bfbb4f8 --- /dev/null +++ b/Demo/Client/src/hooks/useYjs.ts @@ -0,0 +1,12 @@ +import * as React from 'react'; +import { YjsContext } from '../context/yjsContext'; + +export function useYjs() { + const yjsContext = React.useContext(YjsContext); + + if (yjsContext === undefined) { + throw new Error('useYjs() should be called with the YjsContext defined.'); + } + + return yjsContext; +} diff --git a/Demo/Client/src/index.css b/Demo/Client/src/index.css new file mode 100644 index 00000000..e69de29b diff --git a/Demo/Client/src/main.tsx b/Demo/Client/src/main.tsx new file mode 100644 index 00000000..194a6250 --- /dev/null +++ b/Demo/Client/src/main.tsx @@ -0,0 +1,12 @@ +import * as React from 'react' +import * as ReactDOM from 'react-dom/client' +import { YjsContextProvider } from './context/yjsContext' +import App from './App'; +import 'bootstrap/dist/css/bootstrap.min.css'; +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/Demo/Client/src/vite-env.d.ts b/Demo/Client/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/Demo/Client/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/Demo/Client/tsconfig.json b/Demo/Client/tsconfig.json new file mode 100644 index 00000000..a7fc6fbf --- /dev/null +++ b/Demo/Client/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/Demo/Client/tsconfig.node.json b/Demo/Client/tsconfig.node.json new file mode 100644 index 00000000..42872c59 --- /dev/null +++ b/Demo/Client/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/Demo/Client/vite.config.ts b/Demo/Client/vite.config.ts new file mode 100644 index 00000000..5a33944a --- /dev/null +++ b/Demo/Client/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/Demo/Demo.csproj b/Demo/Demo.csproj new file mode 100644 index 00000000..0a87d6d1 --- /dev/null +++ b/Demo/Demo.csproj @@ -0,0 +1,19 @@ + + + + net7.0 + enable + enable + + + + + + + + + Always + + + + diff --git a/Demo/Program.cs b/Demo/Program.cs new file mode 100644 index 00000000..1a830c17 --- /dev/null +++ b/Demo/Program.cs @@ -0,0 +1,26 @@ +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + builder.Services.AddRazorPages(); + builder.Services.AddYDotNet() + .AddWebSockets(); + + var app = builder.Build(); + + app.UseStaticFiles(); + app.UseWebSockets(); + app.UseRouting(); + app.Map("/collaboration", builder => + { + builder.UseYDotnetWebSockets(); + }); + + app.Run(); + } + } +} diff --git a/Demo/appsettings.Development.json b/Demo/appsettings.Development.json new file mode 100644 index 00000000..770d3e93 --- /dev/null +++ b/Demo/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Demo/appsettings.json b/Demo/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/Demo/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Demo/package-lock.json b/Demo/package-lock.json new file mode 100644 index 00000000..a953ca90 --- /dev/null +++ b/Demo/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "Demo", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/Demo/wwwroot/favicon.ico b/Demo/wwwroot/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..63e859b476eff5055e0e557aaa151ca8223fbeef GIT binary patch literal 5430 zcmc&&Yj2xp8Fqnv;>&(QB_ve7>^E#o2mu=cO~A%R>DU-_hfbSRv1t;m7zJ_AMrntN zy0+^f&8be>q&YYzH%(88lQ?#KwiCzaCO*ZEo%j&v;<}&Lj_stKTKK>#U3nin@AF>w zb3ONSAFR{u(S1d?cdw53y}Gt1b-Hirbh;;bm(Rcbnoc*%@jiaXM|4jU^1WO~`TYZ~ zC-~jh9~b-f?fX`DmwvcguQzn*uV}c^Vd&~?H|RUs4Epv~gTAfR(B0lT&?RWQOtduM z^1vUD9{HQsW!{a9|0crA34m7Z6lpG^}f6f?={zD+ zXAzk^i^aKN_}s2$eX81wjSMONE#WVdzf|MT)Ap*}Vsn!XbvsI#6o&ij{87^d%$|A{ z=F{KB%)g%@z76yBzbb7seW**Ju8r4e*Z3PWNX3_tTDgzZatz7)Q6ytwB%@&@A|XT; zecM`Snxx5po$C)%yCP!KEtos~eOS)@2=kX-RIm)4glMCoagTEFxrBeSX%Euz734Fk z%7)x(k~T!@Hbg_37NSQL!vlTBXoURSzt~I**Zw`&F24fH*&kx=%nvZv|49SC*daD( zIw<~%#=lk8{2-l(BcIjy^Q$Q&m#KlWL9?UG{b8@qhlD z;umc+6p%|NsAT~0@DgV4-NKgQuWPWrmPIK&&XhV&n%`{l zOl^bbWYjQNuVXTXESO)@|iUKVmErPUDfz2Wh`4dF@OFiaCW|d`3paV^@|r^8T_ZxM)Z+$p5qx# z#K=z@%;aBPO=C4JNNGqVv6@UGolIz;KZsAro``Rz8X%vq_gpi^qEV&evgHb_=Y9-l z`)imdx0UC>GWZYj)3+3aKh?zVb}=@%oNzg7a8%kfVl)SV-Amp1Okw&+hEZ3|v(k8vRjXW9?ih`&FFM zV$~{j3IzhtcXk?Mu_!12;=+I7XK-IR2>Yd%VB^?oI9c^E&Chb&&je$NV0P-R;ujkP z;cbLCCPEF6|22NDj=S`F^2e~XwT1ZnRX8ra0#DaFa9-X|8(xNW_+JhD75WnSd7cxo z2>I_J5{c|WPfrgl7E2R)^c}F7ry()Z>$Jhk9CzZxiPKL#_0%`&{MX>P_%b~Dx0D^S z7xP1(DQ!d_Icpk!RN3I1w@~|O1ru#CO==h#9M~S4Chx*@?=EKUPGBv$tmU+7Zs_al z`!jR?6T&Z7(%uVq>#yLu`abWk!FBlnY{RFNHlj~6zh*;@u}+}viRKsD`IIxN#R-X3 z@vxu#EA_m}I503U(8Qmx^}u;)KfGP`O9E1H1Q|xeeksX8jC%@!{YT1)!lWgO=+Y3*jr=iSxvOW1}^HSy=y){tOMQJ@an>sOl4FYniE z;GOxd7AqxZNbYFNqobpv&HVO$c-w!Y*6r;$2oJ~h(a#(Bp<-)dg*mNigX~9rPqcHv z^;c*|Md?tD)$y?6FO$DWl$jUGV`F1G_^E&E>sY*YnA~ruv3=z9F8&&~Xpm<<75?N3 z>x~`I&M9q)O1=zWZHN9hZWx>RQ}zLP+iL57Q)%&_^$Sme^^G7;e-P~CR?kqU#Io#( z(nH1Wn*Ig)|M>WLGrxoU?FZrS`4GO&w;+39A3f8w{{Q7eg|$+dIlNFPAe+tN=FOYU z{A&Fg|H73+w1IK(W=j*L>JQgz$g0 z7JpKXLHIh}#$wm|N`s}o-@|L_`>*(gTQ~)wr3Eap7g%PVNisKw82im;Gdv#85x#s+ zoqqtnwu4ycd>cOQgRh-=aEJbnvVK`}ja%+FZx}&ehtX)n(9nVfe4{mn0bgijUbNr7Tf5X^$*{qh2%`?--%+sbSrjE^;1e3>% zqa%jdY16{Y)a1hSy*mr0JGU05Z%=qlx5vGvTjSpTt6k%nR06q}1DU`SQh_ZAeJ}A@`hL~xvv05U?0%=spP`R>dk?cOWM9^KNb7B?xjex>OZo%JMQQ1Q zB|q@}8RiP@DWn-(fB;phPaIOP2Yp)XN3-Fsn)S3w($4&+p8f5W_f%gac}QvmkHfCj$2=!t`boCvQ zCW;&Dto=f8v##}dy^wg3VNaBy&kCe3N;1|@n@pUaMPT?(aJ9b*(gJ28$}(2qFt$H~u5z94xcIQkcOI++)*exzbrk?WOOOf*|%k5#KV zL=&ky3)Eirv$wbRJ2F2s_ILQY--D~~7>^f}W|Aw^e7inXr#WLI{@h`0|jHud2Y~cI~Yn{r_kU^Vo{1gja PendingUpdates { get; } = new ConcurrentQueue(); + public Queue PendingUpdates { get; } = new Queue(); public async Task WriteLockedAsync(T state, Func action, CancellationToken ct) { diff --git a/YDotNet.Server.WebSockets/EncoderExtensions.cs b/YDotNet.Server.WebSockets/EncoderExtensions.cs index 80d4ce79..8e7c81dc 100644 --- a/YDotNet.Server.WebSockets/EncoderExtensions.cs +++ b/YDotNet.Server.WebSockets/EncoderExtensions.cs @@ -4,35 +4,39 @@ namespace YDotNet.Server.WebSockets; public static class EncoderExtensions { - public static async Task WriteSyncStep1Async(this Encoder encoder, byte[] stateVector, + public static async Task WriteSyncStep1Async(this WebSocketEncoder encoder, byte[] stateVector, CancellationToken ct) { await encoder.WriteVarUintAsync(MessageTypes.TypeSync, ct); await encoder.WriteVarUintAsync(MessageTypes.SyncStep1, ct); - await encoder.WriteVarUint8Array(stateVector, ct); + await encoder.WriteBytesAsync(stateVector, ct); + await encoder.EndMessageAsync(ct); } - public static async Task WriteSyncStep2Async(this Encoder encoder, byte[] update, + public static async Task WriteSyncStep2Async(this WebSocketEncoder encoder, byte[] update, CancellationToken ct) { await encoder.WriteVarUintAsync(MessageTypes.TypeSync, ct); await encoder.WriteVarUintAsync(MessageTypes.SyncStep2, ct); - await encoder.WriteVarUint8Array(update, ct); + await encoder.WriteBytesAsync(update, ct); + await encoder.EndMessageAsync(ct); } - public static async Task WriteSyncUpdateAsync(this Encoder encoder, byte[] update, + public static async Task WriteSyncUpdateAsync(this WebSocketEncoder encoder, byte[] update, CancellationToken ct) { await encoder.WriteVarUintAsync(MessageTypes.TypeSync, ct); await encoder.WriteVarUintAsync(MessageTypes.SyncUpdate, ct); - await encoder.WriteVarUint8Array(update, ct); + await encoder.WriteBytesAsync(update, ct); + await encoder.EndMessageAsync(ct); } - public static async Task WriteAwarenessAsync(this Encoder encoder, long clientId, long clock, string? state, + public static async Task WriteAwarenessAsync(this WebSocketEncoder encoder, long clientId, long clock, string? state, CancellationToken ct) { await encoder.WriteVarUintAsync((int)clientId, ct); await encoder.WriteVarUintAsync((int)clock, ct); await encoder.WriteVarStringAsync(state ?? string.Empty, ct); + await encoder.EndMessageAsync(ct); } } diff --git a/YDotNet.Server.WebSockets/MessageTypes.cs b/YDotNet.Server.WebSockets/MessageTypes.cs index 09c98f52..e976239e 100644 --- a/YDotNet.Server.WebSockets/MessageTypes.cs +++ b/YDotNet.Server.WebSockets/MessageTypes.cs @@ -11,7 +11,7 @@ public static class MessageTypes public const int TypeQueryAwareness = 3; // Read: https://github.com/yjs/y-protocols/blob/master/sync.js - public const int SyncStep1 = 1; - public const int SyncStep2 = 2; + public const int SyncStep1 = 0; + public const int SyncStep2 = 1; public const int SyncUpdate = 2; } diff --git a/YDotNet.Server.WebSockets/ServiceExtensions.cs b/YDotNet.Server.WebSockets/ServiceExtensions.cs index 97d18414..a63d55ab 100644 --- a/YDotNet.Server.WebSockets/ServiceExtensions.cs +++ b/YDotNet.Server.WebSockets/ServiceExtensions.cs @@ -1,4 +1,6 @@ using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Hosting; +using YDotNet.Server; using YDotNet.Server.WebSockets; namespace Microsoft.Extensions.DependencyInjection; @@ -8,6 +10,10 @@ public static class ServiceExtensions public static YDotnetRegistration AddWebSockets(this YDotnetRegistration registration) { registration.Services.AddSingleton(); + + registration.Services.AddSingleton(x => + x.GetRequiredService()); + return registration; } diff --git a/YDotNet.Server.WebSockets/WebSocketDecoder.cs b/YDotNet.Server.WebSockets/WebSocketDecoder.cs index 8f9ebd72..1e3213f2 100644 --- a/YDotNet.Server.WebSockets/WebSocketDecoder.cs +++ b/YDotNet.Server.WebSockets/WebSocketDecoder.cs @@ -10,6 +10,13 @@ public sealed class WebSocketDecoder : Decoder, IDisposable private readonly byte[] buffer = ArrayPool.Shared.Rent(1024 * 4); private int bufferLength; private int bufferIndex; + private long previousBuffers; + + public override long Position => previousBuffers + bufferIndex; + + public bool CanRead { get; set; } = true; + + public bool HasMore => bufferIndex < bufferLength; public WebSocketDecoder(WebSocket webSocket) { @@ -56,6 +63,14 @@ private async Task ReadIfEndOfBufferReachedAsync(CancellationToken ct) { var received = await webSocket.ReceiveAsync(buffer, ct); + if (received.CloseStatus != null) + { + CanRead = false; + throw new InvalidOperationException("Socket is already closed."); + } + + previousBuffers += bufferLength; + bufferLength = received.Count; bufferIndex = 0; } @@ -63,7 +78,7 @@ private async Task ReadIfEndOfBufferReachedAsync(CancellationToken ct) private void EnsureSocketIsOpen() { - if (webSocket.State != WebSocketState.Open) + if (webSocket.State != WebSocketState.Open && CanRead) { throw new InvalidOperationException("Socket is already closed."); } diff --git a/YDotNet.Server.WebSockets/WebSocketEncoder.cs b/YDotNet.Server.WebSockets/WebSocketEncoder.cs index f7f2e5a1..1e171de7 100644 --- a/YDotNet.Server.WebSockets/WebSocketEncoder.cs +++ b/YDotNet.Server.WebSockets/WebSocketEncoder.cs @@ -17,7 +17,7 @@ public void Dispose() { } - protected override ValueTask WriteByteAsync(byte value, + public override ValueTask WriteByteAsync(byte value, CancellationToken ct) { buffer[0] = value; @@ -25,13 +25,13 @@ protected override ValueTask WriteByteAsync(byte value, return new ValueTask(webSocket.SendAsync(buffer, WebSocketMessageType.Binary, false, ct)); } - protected override ValueTask WriteBytesAsync(ArraySegment bytes, + public override ValueTask WriteBytesAsync(ArraySegment bytes, CancellationToken ct) { return new ValueTask(webSocket.SendAsync(bytes, WebSocketMessageType.Binary, false, ct)); } - public override ValueTask EndMessageAsync( + public ValueTask EndMessageAsync( CancellationToken ct = default) { return new ValueTask(webSocket.SendAsync(Array.Empty(), WebSocketMessageType.Binary, true, ct)); diff --git a/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs b/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs index 0bdd1e42..bee1d782 100644 --- a/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs +++ b/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs @@ -79,20 +79,23 @@ public async Task InvokeAsync(HttpContext httpContext) try { - var messageType = await state.Decoder.ReadVarUintAsync(httpContext.RequestAborted); - - switch (messageType) + while (state.Decoder.CanRead) { - case MessageTypes.TypeSync: - await HandleSyncAsync(state, httpContext.RequestAborted); - break; + var messageType = await state.Decoder.ReadVarUintAsync(httpContext.RequestAborted); - case MessageTypes.TypeAwareness: - await HandleAwarenessAsync(state, httpContext.RequestAborted); - break; + switch (messageType) + { + case MessageTypes.TypeSync: + await HandleSyncAsync(state, httpContext.RequestAborted); + break; - default: - throw new InvalidOperationException("Protocol error."); + case MessageTypes.TypeAwareness: + await HandleAwarenessAsync(state, httpContext.RequestAborted); + break; + + default: + throw new InvalidOperationException("Protocol error."); + } } } finally @@ -113,27 +116,31 @@ public async Task InvokeAsync(HttpContext httpContext) private async Task HandleSyncAsync(ClientState state, CancellationToken ct) { - var syncType = await state.Decoder.ReadVarUintAsync(ct); - - switch (syncType) + await state.WriteLockedAsync(true, async (encoder, context, state, ct) => { - case MessageTypes.SyncStep1: - var stateVector = await state.Decoder.ReadVarUint8ArrayAsync(ct); + if (!state.Decoder.HasMore) + { + return; + } - var (update, serverState) = await documentManager!.GetMissingChangesAsync(state.DocumentContext, stateVector, ct); + var syncType = await state.Decoder.ReadVarUintAsync(ct); - await state.WriteLockedAsync((update, serverState), async (encoder, context, state, ct) => - { - state.IsSynced = false; + switch (syncType) + { + case MessageTypes.SyncStep1: + var stateVector = await state.Decoder.ReadVarUint8ArrayAsync(ct); - var (update, serverState) = context; + var (update, serverState) = await documentManager!.GetMissingChangesAsync(state.DocumentContext, stateVector, ct); + + // We mark the sync state as false again to handle multiple sync steps. + state.IsSynced = false; await encoder.WriteSyncStep2Async(update, ct); await encoder.WriteSyncStep1Async(serverState, ct); - while (state.PendingUpdates.TryDequeue(out var diff)) + while (state.PendingUpdates.TryDequeue(out var pendingDiff)) { - await encoder.WriteSyncUpdateAsync(diff, ct); + await encoder.WriteSyncUpdateAsync(pendingDiff, ct); } var users = await documentManager.GetAwarenessAsync(state.DocumentName, ct); @@ -149,25 +156,36 @@ await state.WriteLockedAsync((update, serverState), async (encoder, context, sta } } + // Sync state has been completed, therefore the client will receive normal updates now. state.IsSynced = true; - }, ct); + break; - break; + case MessageTypes.SyncStep2: + case MessageTypes.SyncUpdate: + var diff = await state.Decoder.ReadVarUint8ArrayAsync(ct); - case MessageTypes.SyncUpdate: - var stateDiff = await state.Decoder.ReadVarUint8ArrayAsync(ct); + try + { + await documentManager!.ApplyUpdateAsync(state.DocumentContext, diff, ct); + } + catch + { - await documentManager!.ApplyUpdateAsync(state.DocumentContext, stateDiff, ct); - break; + } + break; - default: - throw new InvalidOperationException("Protocol error."); - } + default: + throw new InvalidOperationException("Protocol error."); + } + }, ct); } private async Task HandleAwarenessAsync(ClientState state, CancellationToken ct) { + // This is the length of the awareness message (for whatever reason). + await state.Decoder.ReadVarUintAsync(ct); + var clientCount = await state.Decoder.ReadVarUintAsync(ct); if (clientCount != 1) diff --git a/YDotNet.Server/DefaultDocumentManager.cs b/YDotNet.Server/DefaultDocumentManager.cs index b258d765..54b6cff0 100644 --- a/YDotNet.Server/DefaultDocumentManager.cs +++ b/YDotNet.Server/DefaultDocumentManager.cs @@ -15,13 +15,13 @@ public sealed class DefaultDocumentManager : IDocumentManager private readonly ConnectedUsers users = new(); private readonly DocumentManagerOptions options; private readonly DocumentContainerCache containers; - private readonly CallbackManager callbacks; + private readonly CallbackInvoker callbacks; public DefaultDocumentManager(IDocumentStorage documentStorage, IEnumerable callbacks, IOptions options, ILogger logger) { this.options = options.Value; - this.callbacks = new CallbackManager(callbacks, logger); + this.callbacks = new CallbackInvoker(callbacks, logger); containers = new DocumentContainerCache(documentStorage, options.Value); } @@ -52,7 +52,17 @@ public async Task StopAsync( throw new InvalidOperationException("Transaction cannot be created."); } - return (transaction.StateDiffV2(stateVector), transaction.StateVectorV1()); + byte[] update; + if (stateVector.Length == 0) + { + update = stateVector; + } + else + { + update = transaction.StateDiffV2(stateVector); + } + + return (update, transaction.StateVectorV1()); } }, null); } diff --git a/YDotNet.Server/Internal/CallbackInvoker.cs b/YDotNet.Server/Internal/CallbackInvoker.cs index 8637fd4d..5151a358 100644 --- a/YDotNet.Server/Internal/CallbackInvoker.cs +++ b/YDotNet.Server/Internal/CallbackInvoker.cs @@ -2,12 +2,12 @@ namespace YDotNet.Server.Internal; -public sealed class CallbackManager : IDocumentCallback +public sealed class CallbackInvoker : IDocumentCallback { private readonly List callbacks; private readonly ILogger logger; - public CallbackManager(IEnumerable callbacks, ILogger logger) + public CallbackInvoker(IEnumerable callbacks, ILogger logger) { this.callbacks = callbacks.ToList(); this.logger = logger; diff --git a/YDotNet.Server/ServiceExtensions.cs b/YDotNet.Server/ServiceExtensions.cs index 6362f7c9..857095d7 100644 --- a/YDotNet.Server/ServiceExtensions.cs +++ b/YDotNet.Server/ServiceExtensions.cs @@ -7,7 +7,7 @@ namespace Microsoft.Extensions.DependencyInjection; public static class ServiceExtensions { - public static YDotnetRegistration AddYDotNet(IServiceCollection services) + public static YDotnetRegistration AddYDotNet(this IServiceCollection services) { services.AddOptions(); services.TryAddSingleton(); diff --git a/YDotNet.sln b/YDotNet.sln index 9af8a27c..37e0d8d0 100644 --- a/YDotNet.sln +++ b/YDotNet.sln @@ -15,6 +15,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YDotNet.Server.Redis", "YDo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YDotNet.Server.WebSockets", "YDotNet.Server.WebSockets\YDotNet.Server.WebSockets.csproj", "{273EF3C8-55E5-46EC-A985-ED677F41133D}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Demo", "Demo", "{12A368ED-DC07-4A33-85AB-6330C11476ED}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demo", "Demo\Demo.csproj", "{13D76453-95FC-441D-9AC7-E41848C882C4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -41,12 +45,17 @@ Global {273EF3C8-55E5-46EC-A985-ED677F41133D}.Debug|Any CPU.Build.0 = Debug|Any CPU {273EF3C8-55E5-46EC-A985-ED677F41133D}.Release|Any CPU.ActiveCfg = Release|Any CPU {273EF3C8-55E5-46EC-A985-ED677F41133D}.Release|Any CPU.Build.0 = Release|Any CPU + {13D76453-95FC-441D-9AC7-E41848C882C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {13D76453-95FC-441D-9AC7-E41848C882C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {13D76453-95FC-441D-9AC7-E41848C882C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {13D76453-95FC-441D-9AC7-E41848C882C4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {F85C9334-87AF-4FFC-A301-3A9F57ECD4AB} = {03858045-3849-41E2-ACE9-F10AAA4CCBCA} + {13D76453-95FC-441D-9AC7-E41848C882C4} = {12A368ED-DC07-4A33-85AB-6330C11476ED} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F7865083-8AF3-4562-88F2-95FD43368B57} diff --git a/YDotNet/Protocol/Decoder.cs b/YDotNet/Protocol/Decoder.cs index c69b96a5..5cd4ff6a 100644 --- a/YDotNet/Protocol/Decoder.cs +++ b/YDotNet/Protocol/Decoder.cs @@ -6,27 +6,27 @@ namespace YDotNet.Protocol; public abstract class Decoder { - private const int BITS7 = 1 << 7; - private const int BITS8 = 1 << 8; private readonly byte[] stringBuffer = new byte[128]; + public abstract long Position { get; } + public async ValueTask ReadVarUintAsync( CancellationToken ct = default) { - int num = 0, multiplicator = 1; + int value = 0, shift = 0; while (true) { - var value = await ReadByteAsync(ct); + var lower7bits = await ReadByteAsync(ct); - num += (value & BITS7) * multiplicator; + value |= (lower7bits & 0x7f) << shift; - if (num < BITS8) + if ((lower7bits & 128) == 0) { - return num; + return value; } - multiplicator *= 128; + shift += 7; } throw new IndexOutOfRangeException(); @@ -43,6 +43,43 @@ public async ValueTask ReadVarUint8ArrayAsync( return arrayBuffer; } + public async ValueTask ReadVarUint8ArrayWithLengthAsync( + CancellationToken ct) + { + var positionBeforeLength = Position; + + // Reserve enough place for the array that we have just read and the total array. + var arrayLength = await ReadVarUintAsync(ct); + var arrayPrefix = (int)(Position - positionBeforeLength); + var arrayBuffer = new byte[arrayLength + arrayPrefix]; + + // We cannot read backwards, therefore we have to write the number to the array again. + WriteWithVariableEncoding(arrayLength, arrayBuffer); + + await ReadBytesAsync(arrayBuffer.AsMemory().Slice(arrayPrefix), ct); + + static void WriteWithVariableEncoding(long value, byte[] target) + { + var index = 0; + do + { + byte lower7bits = (byte)(value & 0x7f); + + value >>= 7; + + if (value > 0) + { + lower7bits |= 128; + } + + target[index++] = lower7bits; + } + while (value > 0); + } + + return arrayBuffer; + } + public async ValueTask ReadVarStringAsync( CancellationToken ct) { diff --git a/YDotNet/Protocol/Encoder.cs b/YDotNet/Protocol/Encoder.cs index ebf81f80..08e53f47 100644 --- a/YDotNet/Protocol/Encoder.cs +++ b/YDotNet/Protocol/Encoder.cs @@ -17,13 +17,20 @@ public abstract class Encoder public async ValueTask WriteVarUintAsync(long value, CancellationToken ct = default) { - while (value > BITS7) + do { - await WriteByteAsync((byte)(BITS8 | (BITS7 & value)), ct); + byte lower7bits = (byte)(value & 0x7f); + value >>= 7; - } - await WriteByteAsync((byte)(BITS7 & value), ct); + if (value > 0) + { + lower7bits |= 128; + } + + await WriteByteAsync(lower7bits, ct); + } + while (value > 0); } public async ValueTask WriteVarUint8Array(byte[] value, @@ -62,12 +69,9 @@ async Task WriteCoreAsync(string value, byte[] buffer, CancellationToken ct) } } - public abstract ValueTask EndMessageAsync( - CancellationToken ct = default); - - protected abstract ValueTask WriteByteAsync(byte value, + public abstract ValueTask WriteByteAsync(byte value, CancellationToken ct); - protected abstract ValueTask WriteBytesAsync(ArraySegment bytes, + public abstract ValueTask WriteBytesAsync(ArraySegment bytes, CancellationToken ct); } From 7f937d292ab3fd8e164a44b5542b18da8109f9df Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 2 Oct 2023 15:45:41 +0200 Subject: [PATCH 009/186] Deadlock test --- Demo/Client/src/App.css | 42 ------------- Demo/Client/src/App.tsx | 21 ++++++- .../Client/src/components/YjsMonacoEditor.tsx | 7 ++- Demo/Client/src/components/YjsProseMirror.tsx | 12 ++-- Demo/Client/src/context/yjsContext.tsx | 2 +- Demo/Client/tsconfig.json | 3 +- .../EncoderExtensions.cs | 6 +- YDotNet.Server.WebSockets/WebSocketDecoder.cs | 5 -- YDotNet.Server.WebSockets/WebSocketEncoder.cs | 4 +- .../YDotNetSocketMiddleware.cs | 61 +++++++++---------- YDotNet.Server/DefaultDocumentManager.cs | 2 +- YDotNet/Protocol/Decoder.cs | 39 ------------ YDotNet/Protocol/Encoder.cs | 4 +- 13 files changed, 69 insertions(+), 139 deletions(-) diff --git a/Demo/Client/src/App.css b/Demo/Client/src/App.css index b9d355df..e69de29b 100644 --- a/Demo/Client/src/App.css +++ b/Demo/Client/src/App.css @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/Demo/Client/src/App.tsx b/Demo/Client/src/App.tsx index ce69c622..e9bdb4d1 100644 --- a/Demo/Client/src/App.tsx +++ b/Demo/Client/src/App.tsx @@ -1,12 +1,27 @@ import * as React from 'react'; +import { Col, Container, Row } from 'reactstrap'; import { YjsMonacoEditor } from './components/YjsMonacoEditor' +import { YjsProseMirror } from './components/YjsProseMirror'; function App() { return ( <> -
- -
+ + + +

Monaco Editor

+ + + +
+ + +

Prose Mirror

+ + + +
+
) } diff --git a/Demo/Client/src/components/YjsMonacoEditor.tsx b/Demo/Client/src/components/YjsMonacoEditor.tsx index 34f9aa6d..60f7986a 100644 --- a/Demo/Client/src/components/YjsMonacoEditor.tsx +++ b/Demo/Client/src/components/YjsMonacoEditor.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import * as React from 'react'; import * as yMonaco from 'y-monaco'; import MonacoEditor, { monaco } from 'react-monaco-editor'; @@ -7,15 +8,15 @@ export const YjsMonacoEditor = () => { const { yjsDocument, yjsConnector } = useYjs(); const yText = yjsDocument.getText('monaco'); - const setMonacoEditor = React.useState()[1]; - const setMonacoBinding = React.useState()[1]; + const [, setMonacoEditor] = React.useState(); + const [, setMonacoBinding] = React.useState(null); const _onEditorDidMount = React.useCallback((editor: monaco.editor.ICodeEditor): void => { editor.focus(); editor.setValue(''); setMonacoEditor(editor); - setMonacoBinding(new yMonaco.MonacoBinding(yText, editor.getModel(), new Set([editor]), yjsConnector.awareness)); + setMonacoBinding(new yMonaco.MonacoBinding(yText, editor.getModel()!, new Set([editor]) as any, yjsConnector.awareness)); }, [yjsConnector.awareness, yText, setMonacoEditor, setMonacoBinding]); return ( diff --git a/Demo/Client/src/components/YjsProseMirror.tsx b/Demo/Client/src/components/YjsProseMirror.tsx index b5b1c23a..5efd4be7 100644 --- a/Demo/Client/src/components/YjsProseMirror.tsx +++ b/Demo/Client/src/components/YjsProseMirror.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import { schema } from 'prosemirror-schema-basic'; import { EditorState } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; @@ -8,9 +8,9 @@ import { useYjs } from '../hooks/useYjs'; export const YjsProseMirror = () => { const { yjsDocument, yjsConnector } = useYjs(); - const yText = yjsDocument.getText('prosemirror'); + const yText = yjsDocument.getXmlFragment('prosemirror'); const viewHost = React.useRef(null); - const viewRef = React.useRef(null); + const viewRef = React.useRef(null); React.useEffect(() => { const state = EditorState.create({ @@ -27,10 +27,12 @@ export const YjsProseMirror = () => { ] }); - viewRef.current = new EditorView(viewHost.current, { state }); + const editor = new EditorView(viewHost.current, { state }); + + viewRef.current = editor; return () => { - (viewRef.current as any)?.destroy(); + editor.destroy(); }; }, [yText, yjsConnector.awareness]); diff --git a/Demo/Client/src/context/yjsContext.tsx b/Demo/Client/src/context/yjsContext.tsx index 661e252d..66b22144 100644 --- a/Demo/Client/src/context/yjsContext.tsx +++ b/Demo/Client/src/context/yjsContext.tsx @@ -7,7 +7,7 @@ export interface IYjsContext { readonly yjsConnector: WebsocketProvider; } -export interface IOptions extends React.PropsWithChildren<{}> { +export interface IOptions extends React.PropsWithChildren { readonly baseUrl: string; } diff --git a/Demo/Client/tsconfig.json b/Demo/Client/tsconfig.json index a7fc6fbf..7ca5e5ec 100644 --- a/Demo/Client/tsconfig.json +++ b/Demo/Client/tsconfig.json @@ -7,8 +7,7 @@ "skipLibCheck": true, /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, + "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, diff --git a/YDotNet.Server.WebSockets/EncoderExtensions.cs b/YDotNet.Server.WebSockets/EncoderExtensions.cs index 8e7c81dc..cd75f08c 100644 --- a/YDotNet.Server.WebSockets/EncoderExtensions.cs +++ b/YDotNet.Server.WebSockets/EncoderExtensions.cs @@ -9,7 +9,7 @@ public static async Task WriteSyncStep1Async(this WebSocketEncoder encoder, byte { await encoder.WriteVarUintAsync(MessageTypes.TypeSync, ct); await encoder.WriteVarUintAsync(MessageTypes.SyncStep1, ct); - await encoder.WriteBytesAsync(stateVector, ct); + await encoder.WriteVarUint8Array(stateVector, ct); await encoder.EndMessageAsync(ct); } @@ -18,7 +18,7 @@ public static async Task WriteSyncStep2Async(this WebSocketEncoder encoder, byte { await encoder.WriteVarUintAsync(MessageTypes.TypeSync, ct); await encoder.WriteVarUintAsync(MessageTypes.SyncStep2, ct); - await encoder.WriteBytesAsync(update, ct); + await encoder.WriteVarUint8Array(update, ct); await encoder.EndMessageAsync(ct); } @@ -27,7 +27,7 @@ public static async Task WriteSyncUpdateAsync(this WebSocketEncoder encoder, byt { await encoder.WriteVarUintAsync(MessageTypes.TypeSync, ct); await encoder.WriteVarUintAsync(MessageTypes.SyncUpdate, ct); - await encoder.WriteBytesAsync(update, ct); + await encoder.WriteVarUint8Array(update, ct); await encoder.EndMessageAsync(ct); } diff --git a/YDotNet.Server.WebSockets/WebSocketDecoder.cs b/YDotNet.Server.WebSockets/WebSocketDecoder.cs index 1e3213f2..19f1f2a4 100644 --- a/YDotNet.Server.WebSockets/WebSocketDecoder.cs +++ b/YDotNet.Server.WebSockets/WebSocketDecoder.cs @@ -10,9 +10,6 @@ public sealed class WebSocketDecoder : Decoder, IDisposable private readonly byte[] buffer = ArrayPool.Shared.Rent(1024 * 4); private int bufferLength; private int bufferIndex; - private long previousBuffers; - - public override long Position => previousBuffers + bufferIndex; public bool CanRead { get; set; } = true; @@ -69,8 +66,6 @@ private async Task ReadIfEndOfBufferReachedAsync(CancellationToken ct) throw new InvalidOperationException("Socket is already closed."); } - previousBuffers += bufferLength; - bufferLength = received.Count; bufferIndex = 0; } diff --git a/YDotNet.Server.WebSockets/WebSocketEncoder.cs b/YDotNet.Server.WebSockets/WebSocketEncoder.cs index 1e171de7..215a145d 100644 --- a/YDotNet.Server.WebSockets/WebSocketEncoder.cs +++ b/YDotNet.Server.WebSockets/WebSocketEncoder.cs @@ -17,7 +17,7 @@ public void Dispose() { } - public override ValueTask WriteByteAsync(byte value, + protected override ValueTask WriteByteAsync(byte value, CancellationToken ct) { buffer[0] = value; @@ -25,7 +25,7 @@ public override ValueTask WriteByteAsync(byte value, return new ValueTask(webSocket.SendAsync(buffer, WebSocketMessageType.Binary, false, ct)); } - public override ValueTask WriteBytesAsync(ArraySegment bytes, + protected override ValueTask WriteBytesAsync(ArraySegment bytes, CancellationToken ct) { return new ValueTask(webSocket.SendAsync(bytes, WebSocketMessageType.Binary, false, ct)); diff --git a/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs b/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs index bee1d782..22dca018 100644 --- a/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs +++ b/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs @@ -16,37 +16,43 @@ public ValueTask OnInitializedAsync(IDocumentManager manager) public async ValueTask OnAwarenessUpdatedAsync(ClientAwarenessEvent @event) { - var documentStates = GetOtherClients(@event.DocumentContext.DocumentName, @event.DocumentContext.ClientId); - - foreach (var state in documentStates) + await Task.Run(async () => { - await state.WriteLockedAsync(@event, async (encoder, @event, _, ct) => + var documentStates = GetOtherClients(@event.DocumentContext.DocumentName, @event.DocumentContext.ClientId); + + foreach (var state in documentStates) { - await encoder.WriteVarUintAsync(MessageTypes.TypeAwareness, ct); - await encoder.WriteVarUintAsync(1, ct); - await encoder.WriteAwarenessAsync(@event.DocumentContext.ClientId, @event.ClientClock, @event.ClientState, ct); - }, default); - } + await state.WriteLockedAsync(@event, async (encoder, @event, _, ct) => + { + await encoder.WriteVarUintAsync(MessageTypes.TypeAwareness, ct); + await encoder.WriteVarUintAsync(1, ct); + await encoder.WriteAwarenessAsync(@event.DocumentContext.ClientId, @event.ClientClock, @event.ClientState, ct); + }, default); + } + }); } public async ValueTask OnDocumentChangedAsync(DocumentChangedEvent @event) { - var documentStates = GetOtherClients(@event.DocumentContext.DocumentName, @event.DocumentContext.ClientId); - - foreach (var state in documentStates) + await Task.Run(async () => { - await state.WriteLockedAsync(@event.Diff, async (encoder, diff, _, ct) => + var documentStates = GetOtherClients(@event.DocumentContext.DocumentName, @event.DocumentContext.ClientId); + + foreach (var state in documentStates) { - if (state.IsSynced) - { - await encoder.WriteSyncUpdateAsync(diff, ct); - } - else + await state.WriteLockedAsync(@event.Diff, async (encoder, diff, _, ct) => { - state.PendingUpdates.Enqueue(@event.Diff); - } - }, default); - } + if (state.IsSynced) + { + await encoder.WriteSyncUpdateAsync(diff, ct); + } + else + { + state.PendingUpdates.Enqueue(@event.Diff); + } + }, default); + } + }); } public async Task InvokeAsync(HttpContext httpContext) @@ -163,15 +169,8 @@ await state.WriteLockedAsync(true, async (encoder, context, state, ct) => case MessageTypes.SyncStep2: case MessageTypes.SyncUpdate: var diff = await state.Decoder.ReadVarUint8ArrayAsync(ct); - - try - { - await documentManager!.ApplyUpdateAsync(state.DocumentContext, diff, ct); - } - catch - { - - } + + await documentManager!.ApplyUpdateAsync(state.DocumentContext, diff, ct); break; default: diff --git a/YDotNet.Server/DefaultDocumentManager.cs b/YDotNet.Server/DefaultDocumentManager.cs index 54b6cff0..77284833 100644 --- a/YDotNet.Server/DefaultDocumentManager.cs +++ b/YDotNet.Server/DefaultDocumentManager.cs @@ -86,7 +86,7 @@ public async ValueTask ApplyUpdateAsync(DocumentContext context, b throw new InvalidOperationException("Transaction cannot be created."); } - result.TransactionUpdateResult = transaction.ApplyV2(stateDiff); + result.TransactionUpdateResult = transaction.ApplyV1(stateDiff); } return (result, doc); diff --git a/YDotNet/Protocol/Decoder.cs b/YDotNet/Protocol/Decoder.cs index 5cd4ff6a..30b05b40 100644 --- a/YDotNet/Protocol/Decoder.cs +++ b/YDotNet/Protocol/Decoder.cs @@ -8,8 +8,6 @@ public abstract class Decoder { private readonly byte[] stringBuffer = new byte[128]; - public abstract long Position { get; } - public async ValueTask ReadVarUintAsync( CancellationToken ct = default) { @@ -43,43 +41,6 @@ public async ValueTask ReadVarUint8ArrayAsync( return arrayBuffer; } - public async ValueTask ReadVarUint8ArrayWithLengthAsync( - CancellationToken ct) - { - var positionBeforeLength = Position; - - // Reserve enough place for the array that we have just read and the total array. - var arrayLength = await ReadVarUintAsync(ct); - var arrayPrefix = (int)(Position - positionBeforeLength); - var arrayBuffer = new byte[arrayLength + arrayPrefix]; - - // We cannot read backwards, therefore we have to write the number to the array again. - WriteWithVariableEncoding(arrayLength, arrayBuffer); - - await ReadBytesAsync(arrayBuffer.AsMemory().Slice(arrayPrefix), ct); - - static void WriteWithVariableEncoding(long value, byte[] target) - { - var index = 0; - do - { - byte lower7bits = (byte)(value & 0x7f); - - value >>= 7; - - if (value > 0) - { - lower7bits |= 128; - } - - target[index++] = lower7bits; - } - while (value > 0); - } - - return arrayBuffer; - } - public async ValueTask ReadVarStringAsync( CancellationToken ct) { diff --git a/YDotNet/Protocol/Encoder.cs b/YDotNet/Protocol/Encoder.cs index 08e53f47..4e45c362 100644 --- a/YDotNet/Protocol/Encoder.cs +++ b/YDotNet/Protocol/Encoder.cs @@ -69,9 +69,9 @@ async Task WriteCoreAsync(string value, byte[] buffer, CancellationToken ct) } } - public abstract ValueTask WriteByteAsync(byte value, + protected abstract ValueTask WriteByteAsync(byte value, CancellationToken ct); - public abstract ValueTask WriteBytesAsync(ArraySegment bytes, + protected abstract ValueTask WriteBytesAsync(ArraySegment bytes, CancellationToken ct); } From 381990bead1fe92b32e7232481289b5c47268d40 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 2 Oct 2023 16:09:17 +0200 Subject: [PATCH 010/186] Styling. --- Demo/Client/index.html | 4 +- Demo/Client/package-lock.json | 99 +++++++++++++++++++ Demo/Client/package.json | 1 + Demo/Client/src/App.css | 69 +++++++++++++ Demo/Client/src/App.tsx | 1 + .../Client/src/components/YjsMonacoEditor.tsx | 5 +- Demo/Client/src/components/YjsProseMirror.tsx | 3 +- .../YDotNetSocketMiddleware.cs | 12 ++- 8 files changed, 184 insertions(+), 10 deletions(-) diff --git a/Demo/Client/index.html b/Demo/Client/index.html index e4b78eae..42c3c56a 100644 --- a/Demo/Client/index.html +++ b/Demo/Client/index.html @@ -4,7 +4,9 @@ - Vite + React + TS + YJS + +
diff --git a/Demo/Client/package-lock.json b/Demo/Client/package-lock.json index 08b4d7fc..e27f9ffe 100644 --- a/Demo/Client/package-lock.json +++ b/Demo/Client/package-lock.json @@ -11,6 +11,7 @@ "bootstrap": "^4.6.0", "lib0": "0.2.35", "monaco-editor": "0.22.3", + "prosemirror-example-setup": "^1.2.2", "prosemirror-keymap": "^1.1.4", "prosemirror-schema-basic": "^1.1.2", "prosemirror-view": "^1.17.5", @@ -1562,6 +1563,11 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -3130,6 +3136,73 @@ "react-is": "^16.13.1" } }, + "node_modules/prosemirror-commands": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.5.2.tgz", + "integrity": "sha512-hgLcPaakxH8tu6YvVAaILV2tXYsW3rAdDR8WNkeKGcgeMVQg3/TMhPdVoh7iAmfgVjZGtcOSjKiQaoeKjzd2mQ==", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.1.tgz", + "integrity": "sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw==", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-example-setup": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/prosemirror-example-setup/-/prosemirror-example-setup-1.2.2.tgz", + "integrity": "sha512-pHJc656IgYm249RNp0eQaWNmnyWGk6OrzysWfYI4+NwE14HQ7YNYOlRBLErUS6uCAHIYJLNXf0/XCmf1OCtNbQ==", + "dependencies": { + "prosemirror-commands": "^1.0.0", + "prosemirror-dropcursor": "^1.0.0", + "prosemirror-gapcursor": "^1.0.0", + "prosemirror-history": "^1.0.0", + "prosemirror-inputrules": "^1.0.0", + "prosemirror-keymap": "^1.0.0", + "prosemirror-menu": "^1.0.0", + "prosemirror-schema-list": "^1.0.0", + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz", + "integrity": "sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==", + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.3.2.tgz", + "integrity": "sha512-/zm0XoU/N/+u7i5zepjmZAEnpvjDtzoPWW6VmKptcAnPadN/SStsBjMImdCEbb3seiNTpveziPTIrXQbHLtU1g==", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.2.1.tgz", + "integrity": "sha512-3LrWJX1+ULRh5SZvbIQlwZafOXqp1XuV21MGBu/i5xsztd+9VD15x6OtN6mdqSFI7/8Y77gYUbQ6vwwJ4mr6QQ==", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, "node_modules/prosemirror-keymap": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.2.tgz", @@ -3139,6 +3212,17 @@ "w3c-keyname": "^2.2.0" } }, + "node_modules/prosemirror-menu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.4.tgz", + "integrity": "sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==", + "dependencies": { + "crelt": "^1.0.0", + "prosemirror-commands": "^1.0.0", + "prosemirror-history": "^1.0.0", + "prosemirror-state": "^1.0.0" + } + }, "node_modules/prosemirror-model": { "version": "1.19.3", "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.19.3.tgz", @@ -3155,6 +3239,16 @@ "prosemirror-model": "^1.19.0" } }, + "node_modules/prosemirror-schema-list": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.3.0.tgz", + "integrity": "sha512-Hz/7gM4skaaYfRPNgr421CU4GSwotmEwBVvJh5ltGiffUJwm7C8GfN/Bc6DR1EKEp5pDKhODmdXXyi9uIsZl5A==", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" + } + }, "node_modules/prosemirror-state": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.3.tgz", @@ -3406,6 +3500,11 @@ "fsevents": "~2.3.2" } }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", diff --git a/Demo/Client/package.json b/Demo/Client/package.json index dca72034..16c8545a 100644 --- a/Demo/Client/package.json +++ b/Demo/Client/package.json @@ -13,6 +13,7 @@ "bootstrap": "^4.6.0", "lib0": "0.2.35", "monaco-editor": "0.22.3", + "prosemirror-example-setup": "^1.2.2", "prosemirror-keymap": "^1.1.4", "prosemirror-schema-basic": "^1.1.2", "prosemirror-view": "^1.17.5", diff --git a/Demo/Client/src/App.css b/Demo/Client/src/App.css index e69de29b..69adefb7 100644 --- a/Demo/Client/src/App.css +++ b/Demo/Client/src/App.css @@ -0,0 +1,69 @@ +/* this is a rough fix for the first cursor position when the first paragraph is empty */ +.ProseMirror>.ProseMirror-yjs-cursor:first-child { + margin-top: 16px; +} + +.ProseMirror p:first-child, +.ProseMirror h1:first-child, +.ProseMirror h2:first-child, +.ProseMirror h3:first-child, +.ProseMirror h4:first-child, +.ProseMirror h5:first-child, +.ProseMirror h6:first-child { + margin-top: 16px +} + +/* This gives the remote user caret. The colors are automatically overwritten*/ +.ProseMirror-yjs-cursor { + border-color: orange; + border-left: 1px solid black; + border-right: 1px solid black; + margin-left: -1px; + margin-right: -1px; + pointer-events: none; + position: relative; + word-break: normal; +} + +/* This renders the username above the caret */ +.ProseMirror-yjs-cursor>div { + background-color: rgb(250, 129, 0); + color: white; + font-size: 13px; + font-style: normal; + font-weight: normal; + left: -1px; + line-height: normal; + padding-left: 4px; + padding-right: 4px; + padding-bottom: 2px; + padding-top: 2px; + position: absolute; + pointer-events: none; + top: -1.05em; + user-select: none; + white-space: nowrap; +} + +.react-monaco-editor-container { + height: 300px !important; + border: 1px solid #ddd; + border-radius: 2px; + box-sizing: content-box; +} + +.ProseMirror-menubar-wrapper { + height: 300px !important; + border: 1px solid silver; + border-radius: 2px; +} + +.ProseMirror-icon { + line-height: inherit; +} + +.ProseMirror-menubar { + display: flex; + flex-direction: row; + align-items: center; +} \ No newline at end of file diff --git a/Demo/Client/src/App.tsx b/Demo/Client/src/App.tsx index e9bdb4d1..bed4a4c1 100644 --- a/Demo/Client/src/App.tsx +++ b/Demo/Client/src/App.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { Col, Container, Row } from 'reactstrap'; import { YjsMonacoEditor } from './components/YjsMonacoEditor' import { YjsProseMirror } from './components/YjsProseMirror'; +import './App.css'; function App() { return ( diff --git a/Demo/Client/src/components/YjsMonacoEditor.tsx b/Demo/Client/src/components/YjsMonacoEditor.tsx index 60f7986a..1c8fac12 100644 --- a/Demo/Client/src/components/YjsMonacoEditor.tsx +++ b/Demo/Client/src/components/YjsMonacoEditor.tsx @@ -21,10 +21,7 @@ export const YjsMonacoEditor = () => { return (
- _onEditorDidMount(e)} />
diff --git a/Demo/Client/src/components/YjsProseMirror.tsx b/Demo/Client/src/components/YjsProseMirror.tsx index 5efd4be7..9a68b43e 100644 --- a/Demo/Client/src/components/YjsProseMirror.tsx +++ b/Demo/Client/src/components/YjsProseMirror.tsx @@ -3,6 +3,7 @@ import { schema } from 'prosemirror-schema-basic'; import { EditorState } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import { keymap } from 'prosemirror-keymap'; +import { exampleSetup } from 'prosemirror-example-setup'; import { ySyncPlugin, yCursorPlugin, yUndoPlugin, undo, redo } from 'y-prosemirror'; import { useYjs } from '../hooks/useYjs'; @@ -24,7 +25,7 @@ export const YjsProseMirror = () => { 'Mod-y': redo, 'Mod-Shift-z': redo }) - ] + ].concat(exampleSetup({ schema })) }); const editor = new EditorView(viewHost.current, { state }); diff --git a/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs b/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs index 22dca018..fc00cde3 100644 --- a/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs +++ b/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs @@ -14,9 +14,9 @@ public ValueTask OnInitializedAsync(IDocumentManager manager) return default; } - public async ValueTask OnAwarenessUpdatedAsync(ClientAwarenessEvent @event) + public ValueTask OnAwarenessUpdatedAsync(ClientAwarenessEvent @event) { - await Task.Run(async () => + Task.Run(async () => { var documentStates = GetOtherClients(@event.DocumentContext.DocumentName, @event.DocumentContext.ClientId); @@ -30,11 +30,13 @@ await state.WriteLockedAsync(@event, async (encoder, @event, _, ct) => }, default); } }); + + return default; } - public async ValueTask OnDocumentChangedAsync(DocumentChangedEvent @event) + public ValueTask OnDocumentChangedAsync(DocumentChangedEvent @event) { - await Task.Run(async () => + Task.Run(async () => { var documentStates = GetOtherClients(@event.DocumentContext.DocumentName, @event.DocumentContext.ClientId); @@ -53,6 +55,8 @@ await state.WriteLockedAsync(@event.Diff, async (encoder, diff, _, ct) => }, default); } }); + + return default; } public async Task InvokeAsync(HttpContext httpContext) From b6c37fc1a34c1bd16a58979df6f52cc03cabb1f2 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 2 Oct 2023 20:29:46 +0200 Subject: [PATCH 011/186] Fix deadlock and implement listener. --- Demo/Callback.cs | 43 +++++++++ Demo/Client/.eslintrc.cjs | 7 +- Demo/Client/src/App.tsx | 49 +++++----- Demo/Client/src/components/Increment.tsx | 49 ++++++++++ .../Client/src/components/YjsMonacoEditor.tsx | 34 +++---- Demo/Client/src/components/YjsProseMirror.tsx | 54 +++++------ Demo/Client/src/context/yjsContext.tsx | 20 ++-- Demo/Client/src/hooks/useYjs.ts | 10 +- Demo/Client/src/main.tsx | 15 ++- Demo/ClientApp | 0 Demo/Program.cs | 36 ++++---- Demo/appsettings.json | 3 +- YDotNet.Server.Redis/RedisCallback.cs | 12 +-- YDotNet.Server.WebSockets/ClientState.cs | 2 +- .../YDotNetSocketMiddleware.cs | 92 +++++++++++++------ YDotNet.Server/DefaultDocumentManager.cs | 61 ++++++------ YDotNet.Server/Events.cs | 9 +- YDotNet.Server/IDocumentCallback.cs | 5 + YDotNet.Server/Internal/CallbackInvoker.cs | 5 + YDotNet.Server/Internal/DelayedWriter.cs | 16 ++-- YDotNet.Server/Internal/DocumentContainer.cs | 26 ++++-- .../Internal/DocumentContainerCache.cs | 17 +++- YDotNet.Server/ServiceExtensions.cs | 5 + 23 files changed, 379 insertions(+), 191 deletions(-) create mode 100644 Demo/Callback.cs create mode 100644 Demo/Client/src/components/Increment.tsx create mode 100644 Demo/ClientApp diff --git a/Demo/Callback.cs b/Demo/Callback.cs new file mode 100644 index 00000000..608017ab --- /dev/null +++ b/Demo/Callback.cs @@ -0,0 +1,43 @@ +using YDotNet.Document.Types.Events; +using YDotNet.Server; + +namespace Demo; + +public sealed class Listener : IDocumentCallback +{ + private readonly ILogger log; + + public Listener(ILogger log) + { + this.log = log; + } + + public ValueTask OnDocumentLoadedAsync(DocumentLoadEvent @event) + { + var map = @event.Document.Map("increment"); + + map?.ObserveDeep(changes => + { + foreach (var change in changes) + { + var key = change.MapEvent?.Keys.FirstOrDefault(x => x.Key == "value" && x.Tag != EventKeyChangeTag.Remove); + + if (key != null) + { + var valueOld = key.OldValue?.Double; + var valueNew = key.NewValue?.Double; + + if (valueOld == valueNew) + { + continue; + } + + log.LogInformation("Counter updated from {oldValue} to {newValue}.", valueOld, valueNew); + } + } + }); + + + return default; + } +} diff --git a/Demo/Client/.eslintrc.cjs b/Demo/Client/.eslintrc.cjs index d6c95379..b38f00af 100644 --- a/Demo/Client/.eslintrc.cjs +++ b/Demo/Client/.eslintrc.cjs @@ -14,5 +14,10 @@ module.exports = { 'warn', { allowConstantExport: true }, ], - }, + '@typescript-eslint/semi': 'error', + '@typescript-eslint/indent': [ + 'warn', + 4 + ] + } } diff --git a/Demo/Client/src/App.tsx b/Demo/Client/src/App.tsx index bed4a4c1..9a62fe71 100644 --- a/Demo/Client/src/App.tsx +++ b/Demo/Client/src/App.tsx @@ -1,30 +1,37 @@ -import * as React from 'react'; import { Col, Container, Row } from 'reactstrap'; -import { YjsMonacoEditor } from './components/YjsMonacoEditor' +import { YjsMonacoEditor } from './components/YjsMonacoEditor'; import { YjsProseMirror } from './components/YjsProseMirror'; +import { Increment } from './components/Increment'; import './App.css'; function App() { - return ( - <> - - - -

Monaco Editor

+ return ( + <> + + + +

Monaco Editor

- - -
- - -

Prose Mirror

+ + +
+ + +

Prose Mirror

- - -
-
- - ) + + +
+ + +

Increment

+ + + +
+
+ + ); } -export default App +export default App; diff --git a/Demo/Client/src/components/Increment.tsx b/Demo/Client/src/components/Increment.tsx new file mode 100644 index 00000000..f0d0c9d0 --- /dev/null +++ b/Demo/Client/src/components/Increment.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { Button, Col, Input, Row } from 'reactstrap'; +import { useYjs } from '../hooks/useYjs'; + +export const Increment = () => { + const { yjsDocument } = useYjs(); + const map = yjsDocument.getMap('increment'); + const [state, setState] = React.useState(0); + + React.useEffect(() => { + const handler = () => { + setState(map.get('value') || 0); + }; + + map.observeDeep(handler); + + return () => { + map.unobserveDeep(handler); + }; + }, [map]); + + React.useEffect(() => { + yjsDocument.transact(() => { + map.set('value', state); + }, 'Incrementer'); + }, [map, state, yjsDocument]); + + const _increment = () => { + setState(v => v + 1); + }; + + const _decrement = () => { + setState(v => v - 1); + }; + + return ( + + + + + + + + + + + + ); +}; \ No newline at end of file diff --git a/Demo/Client/src/components/YjsMonacoEditor.tsx b/Demo/Client/src/components/YjsMonacoEditor.tsx index 1c8fac12..f786c4c7 100644 --- a/Demo/Client/src/components/YjsMonacoEditor.tsx +++ b/Demo/Client/src/components/YjsMonacoEditor.tsx @@ -5,25 +5,25 @@ import MonacoEditor, { monaco } from 'react-monaco-editor'; import { useYjs } from '../hooks/useYjs'; export const YjsMonacoEditor = () => { - const { yjsDocument, yjsConnector } = useYjs(); - const yText = yjsDocument.getText('monaco'); + const { yjsDocument, yjsConnector } = useYjs(); + const yText = yjsDocument.getText('monaco'); - const [, setMonacoEditor] = React.useState(); - const [, setMonacoBinding] = React.useState(null); + const [, setMonacoEditor] = React.useState(); + const [, setMonacoBinding] = React.useState(null); - const _onEditorDidMount = React.useCallback((editor: monaco.editor.ICodeEditor): void => { - editor.focus(); - editor.setValue(''); + const _onEditorDidMount = React.useCallback((editor: monaco.editor.ICodeEditor): void => { + editor.focus(); + editor.setValue(''); - setMonacoEditor(editor); - setMonacoBinding(new yMonaco.MonacoBinding(yText, editor.getModel()!, new Set([editor]) as any, yjsConnector.awareness)); - }, [yjsConnector.awareness, yText, setMonacoEditor, setMonacoBinding]); + setMonacoEditor(editor); + setMonacoBinding(new yMonaco.MonacoBinding(yText, editor.getModel()!, new Set([editor]) as any, yjsConnector.awareness)); + }, [yjsConnector.awareness, yText, setMonacoEditor, setMonacoBinding]); - return ( -
- _onEditorDidMount(e)} - /> -
- ); + return ( +
+ _onEditorDidMount(e)} + /> +
+ ); }; diff --git a/Demo/Client/src/components/YjsProseMirror.tsx b/Demo/Client/src/components/YjsProseMirror.tsx index 9a68b43e..e525cb57 100644 --- a/Demo/Client/src/components/YjsProseMirror.tsx +++ b/Demo/Client/src/components/YjsProseMirror.tsx @@ -8,36 +8,36 @@ import { ySyncPlugin, yCursorPlugin, yUndoPlugin, undo, redo } from 'y-prosemirr import { useYjs } from '../hooks/useYjs'; export const YjsProseMirror = () => { - const { yjsDocument, yjsConnector } = useYjs(); - const yText = yjsDocument.getXmlFragment('prosemirror'); - const viewHost = React.useRef(null); - const viewRef = React.useRef(null); + const { yjsDocument, yjsConnector } = useYjs(); + const yText = yjsDocument.getXmlFragment('prosemirror'); + const viewHost = React.useRef(null); + const viewRef = React.useRef(null); - React.useEffect(() => { - const state = EditorState.create({ - schema, - plugins: [ - ySyncPlugin(yText), - yCursorPlugin(yjsConnector.awareness), - yUndoPlugin(), - keymap({ - 'Mod-z': undo, - 'Mod-y': redo, - 'Mod-Shift-z': redo - }) - ].concat(exampleSetup({ schema })) - }); + React.useEffect(() => { + const state = EditorState.create({ + schema, + plugins: [ + ySyncPlugin(yText), + yCursorPlugin(yjsConnector.awareness), + yUndoPlugin(), + keymap({ + 'Mod-z': undo, + 'Mod-y': redo, + 'Mod-Shift-z': redo + }) + ].concat(exampleSetup({ schema })) + }); - const editor = new EditorView(viewHost.current, { state }); + const editor = new EditorView(viewHost.current, { state }); - viewRef.current = editor; + viewRef.current = editor; - return () => { - editor.destroy(); - }; - }, [yText, yjsConnector.awareness]); + return () => { + editor.destroy(); + }; + }, [yText, yjsConnector.awareness]); - return ( -
- ); + return ( +
+ ); }; diff --git a/Demo/Client/src/context/yjsContext.tsx b/Demo/Client/src/context/yjsContext.tsx index 66b22144..b67f36b6 100644 --- a/Demo/Client/src/context/yjsContext.tsx +++ b/Demo/Client/src/context/yjsContext.tsx @@ -3,25 +3,25 @@ import * as Y from 'yjs'; import { WebsocketProvider } from 'y-websocket'; export interface IYjsContext { - readonly yjsDocument: Y.Doc; - readonly yjsConnector: WebsocketProvider; + readonly yjsDocument: Y.Doc; + readonly yjsConnector: WebsocketProvider; } export interface IOptions extends React.PropsWithChildren { - readonly baseUrl: string; + readonly baseUrl: string; } export const YjsContextProvider: React.FunctionComponent = (props: IOptions) => { - const { baseUrl } = props; + const { baseUrl } = props; - const contextProps: IYjsContext = React.useMemo(() => { - const yjsDocument = new Y.Doc(); - const yjsConnector = new WebsocketProvider(baseUrl, 'test', yjsDocument); + const contextProps: IYjsContext = React.useMemo(() => { + const yjsDocument = new Y.Doc(); + const yjsConnector = new WebsocketProvider(baseUrl, 'test', yjsDocument); - return { yjsDocument, yjsConnector }; - }, [baseUrl]); + return { yjsDocument, yjsConnector }; + }, [baseUrl]); - return {props.children}; + return {props.children}; }; export const YjsContext = React.createContext(undefined); diff --git a/Demo/Client/src/hooks/useYjs.ts b/Demo/Client/src/hooks/useYjs.ts index 8bfbb4f8..7ae73a94 100644 --- a/Demo/Client/src/hooks/useYjs.ts +++ b/Demo/Client/src/hooks/useYjs.ts @@ -2,11 +2,11 @@ import * as React from 'react'; import { YjsContext } from '../context/yjsContext'; export function useYjs() { - const yjsContext = React.useContext(YjsContext); + const yjsContext = React.useContext(YjsContext); - if (yjsContext === undefined) { - throw new Error('useYjs() should be called with the YjsContext defined.'); - } + if (yjsContext === undefined) { + throw new Error('useYjs() should be called with the YjsContext defined.'); + } - return yjsContext; + return yjsContext; } diff --git a/Demo/Client/src/main.tsx b/Demo/Client/src/main.tsx index 194a6250..c60a0b86 100644 --- a/Demo/Client/src/main.tsx +++ b/Demo/Client/src/main.tsx @@ -1,12 +1,11 @@ -import * as React from 'react' -import * as ReactDOM from 'react-dom/client' -import { YjsContextProvider } from './context/yjsContext' +import * as ReactDOM from 'react-dom/client'; +import { YjsContextProvider } from './context/yjsContext'; import App from './App'; import 'bootstrap/dist/css/bootstrap.min.css'; -import './index.css' +import './index.css'; ReactDOM.createRoot(document.getElementById('root')!).render( - - - , -) + + + , +); diff --git a/Demo/ClientApp b/Demo/ClientApp new file mode 100644 index 00000000..e69de29b diff --git a/Demo/Program.cs b/Demo/Program.cs index 1a830c17..f9f58d27 100644 --- a/Demo/Program.cs +++ b/Demo/Program.cs @@ -1,26 +1,26 @@ -namespace Demo +namespace Demo; + +public class Program { - public class Program + public static void Main(string[] args) { - public static void Main(string[] args) - { - var builder = WebApplication.CreateBuilder(args); + var builder = WebApplication.CreateBuilder(args); - builder.Services.AddRazorPages(); - builder.Services.AddYDotNet() - .AddWebSockets(); + builder.Services.AddRazorPages(); + builder.Services.AddYDotNet() + .AddCallback() + .AddWebSockets(); - var app = builder.Build(); + var app = builder.Build(); - app.UseStaticFiles(); - app.UseWebSockets(); - app.UseRouting(); - app.Map("/collaboration", builder => - { - builder.UseYDotnetWebSockets(); - }); + app.UseStaticFiles(); + app.UseWebSockets(); + app.UseRouting(); + app.Map("/collaboration", builder => + { + builder.UseYDotnetWebSockets(); + }); - app.Run(); - } + app.Run(); } } diff --git a/Demo/appsettings.json b/Demo/appsettings.json index 10f68b8c..6eacb541 100644 --- a/Demo/appsettings.json +++ b/Demo/appsettings.json @@ -2,7 +2,8 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Microsoft.AspNetCore": "Warning", + "YDotNet": "Debug" } }, "AllowedHosts": "*" diff --git a/YDotNet.Server.Redis/RedisCallback.cs b/YDotNet.Server.Redis/RedisCallback.cs index 6f88c80b..58baba5d 100644 --- a/YDotNet.Server.Redis/RedisCallback.cs +++ b/YDotNet.Server.Redis/RedisCallback.cs @@ -108,10 +108,10 @@ public async ValueTask OnAwarenessUpdatedAsync(ClientAwarenessEvent @event) SenderId = senderId, Pinged = new PingMessage { - ClientId = @event.DocumentContext.ClientId, + ClientId = @event.Context.ClientId, ClientClock = @event.ClientClock, ClientState = @event.ClientState, - DocumentName = @event.DocumentContext.DocumentName, + DocumentName = @event.Context.DocumentName, }, }; @@ -132,10 +132,10 @@ public async ValueTask OnClientDisconnectedAsync(ClientDisconnectedEvent[] event SenderId = senderId, ClientDisconnected = events.Select(x => new PingMessage { - ClientId = x.DocumentContext.ClientId, + ClientId = x.Context.ClientId, ClientState = null, ClientClock = 0, - DocumentName = x.DocumentContext.DocumentName, + DocumentName = x.Context.DocumentName, }).ToArray() }; @@ -156,8 +156,8 @@ public async ValueTask OnDocumentChangedAsync(DocumentChangedEvent @event) SenderId = senderId, DocumentChanged = new DocumentChangeMessage { - ClientId = @event.DocumentContext.ClientId, - DocumentName = @event.DocumentContext.DocumentName, + ClientId = @event.Context.ClientId, + DocumentName = @event.Context.DocumentName, DocumentDiff = @event.Diff }, }; diff --git a/YDotNet.Server.WebSockets/ClientState.cs b/YDotNet.Server.WebSockets/ClientState.cs index 936efb54..4fe06cda 100644 --- a/YDotNet.Server.WebSockets/ClientState.cs +++ b/YDotNet.Server.WebSockets/ClientState.cs @@ -4,7 +4,7 @@ namespace YDotNet.Server.WebSockets; public sealed class ClientState : IDisposable { - private readonly SemaphoreSlim slimLock = new SemaphoreSlim(1); + private readonly SemaphoreSlim slimLock = new(1); required public WebSocket WebSocket { get; set; } diff --git a/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs b/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs index fc00cde3..c073e4c9 100644 --- a/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs +++ b/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs @@ -1,13 +1,20 @@ using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; using System.Collections.Concurrent; namespace YDotNet.Server.WebSockets; public sealed class YDotNetSocketMiddleware : IDocumentCallback { - private readonly ConcurrentDictionary> statesPerDocumentName = new ConcurrentDictionary>(); + private readonly ConcurrentDictionary> statesPerDocumentName = new(); + private readonly ILogger logger; private IDocumentManager? documentManager; + public YDotNetSocketMiddleware(ILogger logger) + { + this.logger = logger; + } + public ValueTask OnInitializedAsync(IDocumentManager manager) { documentManager = manager; @@ -18,7 +25,7 @@ public ValueTask OnAwarenessUpdatedAsync(ClientAwarenessEvent @event) { Task.Run(async () => { - var documentStates = GetOtherClients(@event.DocumentContext.DocumentName, @event.DocumentContext.ClientId); + var documentStates = GetOtherClients(@event.Context.DocumentName, @event.Context.ClientId); foreach (var state in documentStates) { @@ -26,7 +33,7 @@ await state.WriteLockedAsync(@event, async (encoder, @event, _, ct) => { await encoder.WriteVarUintAsync(MessageTypes.TypeAwareness, ct); await encoder.WriteVarUintAsync(1, ct); - await encoder.WriteAwarenessAsync(@event.DocumentContext.ClientId, @event.ClientClock, @event.ClientState, ct); + await encoder.WriteAwarenessAsync(@event.Context.ClientId, @event.ClientClock, @event.ClientState, ct); }, default); } }); @@ -38,7 +45,7 @@ public ValueTask OnDocumentChangedAsync(DocumentChangedEvent @event) { Task.Run(async () => { - var documentStates = GetOtherClients(@event.DocumentContext.DocumentName, @event.DocumentContext.ClientId); + var documentStates = GetOtherClients(@event.Context.DocumentName, @event.Context.ClientId); foreach (var state in documentStates) { @@ -70,6 +77,8 @@ public async Task InvokeAsync(HttpContext httpContext) var documentName = httpContext.Request.Path; var documentStates = statesPerDocumentName.GetOrAdd(documentName, _ => new List()); + logger.LogDebug("Websocket connection to {document} established.", documentName); + var webSocket = await httpContext.WebSockets.AcceptWebSocketAsync(); using var state = new ClientState @@ -108,6 +117,9 @@ public async Task InvokeAsync(HttpContext httpContext) } } } + catch (OperationCanceledException) + { + } finally { // Usually we should have relatively few clients per document, therefore we use a simple lock. @@ -121,6 +133,8 @@ public async Task InvokeAsync(HttpContext httpContext) await documentManager.DisconnectAsync(state.DocumentContext, default); } } + + logger.LogDebug("Websocket connection to {document} closed.", documentName); } private async Task HandleSyncAsync(ClientState state, @@ -148,23 +162,8 @@ await state.WriteLockedAsync(true, async (encoder, context, state, ct) => await encoder.WriteSyncStep2Async(update, ct); await encoder.WriteSyncStep1Async(serverState, ct); - while (state.PendingUpdates.TryDequeue(out var pendingDiff)) - { - await encoder.WriteSyncUpdateAsync(pendingDiff, ct); - } - - var users = await documentManager.GetAwarenessAsync(state.DocumentName, ct); - - if (users.Count != 0) - { - await encoder.WriteVarUintAsync(MessageTypes.TypeAwareness, ct); - await encoder.WriteVarUintAsync(users.Count, ct); - - foreach (var (clientId, user) in await documentManager.GetAwarenessAsync(state.DocumentName, ct)) - { - await encoder.WriteAwarenessAsync(clientId, user.ClientClock, user.ClientState, ct); - } - } + await SendPendingUpdatesAsync(encoder, state, ct); + await SendAwarenessAsync(encoder, state, ct); // Sync state has been completed, therefore the client will receive normal updates now. state.IsSynced = true; @@ -183,6 +182,32 @@ await state.WriteLockedAsync(true, async (encoder, context, state, ct) => }, ct); } + private static async Task SendPendingUpdatesAsync(WebSocketEncoder encoder, ClientState state, CancellationToken ct) + { + while (state.PendingUpdates.TryDequeue(out var pendingDiff)) + { + await encoder.WriteSyncUpdateAsync(pendingDiff, ct); + } + } + + private async Task SendAwarenessAsync(WebSocketEncoder encoder, ClientState state, CancellationToken ct) + { + var users = await documentManager!.GetAwarenessAsync(state.DocumentName, ct); + + if (users.Count == 0) + { + return; + } + + await encoder.WriteVarUintAsync(MessageTypes.TypeAwareness, ct); + await encoder.WriteVarUintAsync(users.Count, ct); + + foreach (var (clientId, user) in await documentManager.GetAwarenessAsync(state.DocumentName, ct)) + { + await encoder.WriteAwarenessAsync(clientId, user.ClientClock, user.ClientState, ct); + } + } + private async Task HandleAwarenessAsync(ClientState state, CancellationToken ct) { @@ -191,18 +216,25 @@ private async Task HandleAwarenessAsync(ClientState state, var clientCount = await state.Decoder.ReadVarUintAsync(ct); - if (clientCount != 1) + for (var i = 0; i < clientCount; i++) { - throw new InvalidOperationException($"Protocol error. Expected client count to be 1, got {clientCount}."); - } + var clientId = await state.Decoder.ReadVarUintAsync(ct); + var clientClock = await state.Decoder.ReadVarUintAsync(ct); + var clientState = await state.Decoder.ReadVarStringAsync(ct); - var clientId = await state.Decoder.ReadVarUintAsync(ct); - var clientClock = await state.Decoder.ReadVarUintAsync(ct); - var clientState = await state.Decoder.ReadVarStringAsync(ct); - - state.DocumentContext.ClientId = clientId; + if (state.DocumentContext.ClientId == 0 && clientCount == 1) + { + state.DocumentContext.ClientId = clientId; - await documentManager!.PingAsync(state.DocumentContext, clientClock, clientState, ct); + logger.LogDebug("Websocket connection to {document} enhanced with client Id {clientId}.", + state.DocumentContext.DocumentName, + state.DocumentContext.ClientId); + } + + var context = new DocumentContext { ClientId = clientId, DocumentName = state.DocumentName }; + + await documentManager!.PingAsync(context, clientClock, clientState, ct); + } } private List GetOtherClients(string documentName, long clientId) diff --git a/YDotNet.Server/DefaultDocumentManager.cs b/YDotNet.Server/DefaultDocumentManager.cs index 77284833..f0047136 100644 --- a/YDotNet.Server/DefaultDocumentManager.cs +++ b/YDotNet.Server/DefaultDocumentManager.cs @@ -1,10 +1,10 @@ -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using YDotNet.Document; using YDotNet.Document.Transactions; using YDotNet.Server.Internal; using YDotNet.Server.Storage; +using IDocumentCallbacks = System.Collections.Generic.IEnumerable; #pragma warning disable IDE0063 // Use simple 'using' statement @@ -15,21 +15,24 @@ public sealed class DefaultDocumentManager : IDocumentManager private readonly ConnectedUsers users = new(); private readonly DocumentManagerOptions options; private readonly DocumentContainerCache containers; - private readonly CallbackInvoker callbacks; + private readonly CallbackInvoker callback; - public DefaultDocumentManager(IDocumentStorage documentStorage, IEnumerable callbacks, - IOptions options, ILogger logger) + public DefaultDocumentManager( + IDocumentStorage documentStorage, + IDocumentCallbacks callbacks, + IOptions options, + ILogger logger) { this.options = options.Value; - this.callbacks = new CallbackInvoker(callbacks, logger); + this.callback = new CallbackInvoker(callbacks, logger); - containers = new DocumentContainerCache(documentStorage, options.Value); + containers = new DocumentContainerCache(documentStorage, this.callback, this, options.Value); } public async Task StartAsync( CancellationToken cancellationToken) { - await callbacks.OnInitializedAsync(this); + await callback.OnInitializedAsync(this); } public async Task StopAsync( @@ -92,22 +95,22 @@ public async ValueTask ApplyUpdateAsync(DocumentContext context, b return (result, doc); }, async doc => { - await callbacks.OnDocumentChangingAsync(new DocumentChangeEvent + await callback.OnDocumentChangingAsync(new DocumentChangeEvent { - DocumentContext = context, + Context = context, Document = doc, - DocumentManager = this, + Source = this, }); }); if (result.Diff != null) { - await callbacks.OnDocumentChangedAsync(new DocumentChangedEvent + await callback.OnDocumentChangedAsync(new DocumentChangedEvent { + Context = context, Diff = result.Diff, Document = doc, - DocumentContext = context, - DocumentManager = this, + Source = this, }); } @@ -141,22 +144,22 @@ public async ValueTask UpdateDocAsync(DocumentContext context, Action { - await callbacks.OnDocumentChangingAsync(new DocumentChangeEvent + await callback.OnDocumentChangingAsync(new DocumentChangeEvent { + Context = context, Document = doc, - DocumentContext = context, - DocumentManager = this, + Source = this, }); }); if (diff != null) { - await callbacks.OnDocumentChangedAsync(new DocumentChangedEvent + await callback.OnDocumentChangedAsync(new DocumentChangedEvent { + Context = context, Diff = diff, Document = doc, - DocumentContext = context, - DocumentManager = this, + Source = this, }); } } @@ -166,12 +169,12 @@ public async ValueTask PingAsync(DocumentContext context, long clock, string? st { if (users.AddOrUpdate(context.DocumentName, context.ClientId, clock, state, out var newState)) { - await callbacks.OnAwarenessUpdatedAsync(new ClientAwarenessEvent + await callback.OnAwarenessUpdatedAsync(new ClientAwarenessEvent { - DocumentContext = context, - DocumentManager = this, + Context = context, ClientClock = clock, - ClientState = newState + ClientState = newState, + Source = this, }); } } @@ -181,12 +184,12 @@ public async ValueTask DisconnectAsync(DocumentContext context, { if (users.Remove(context.DocumentName, context.ClientId)) { - await callbacks.OnClientDisconnectedAsync(new[] + await callback.OnClientDisconnectedAsync(new[] { new ClientDisconnectedEvent { - DocumentContext = context, - DocumentManager = this, + Context = context, + Source = this, } }); } @@ -199,12 +202,12 @@ public async ValueTask CleanupAsync( if (removedUsers.Count > 0) { - await callbacks.OnClientDisconnectedAsync(removedUsers.Select(x => + await callback.OnClientDisconnectedAsync(removedUsers.Select(x => { return new ClientDisconnectedEvent { - DocumentContext = new DocumentContext { ClientId = x.ClientId, DocumentName = x.DocumentName }, - DocumentManager = this, + Context = new DocumentContext { ClientId = x.ClientId, DocumentName = x.DocumentName }, + Source = this, }; }).ToArray()); } diff --git a/YDotNet.Server/Events.cs b/YDotNet.Server/Events.cs index f9b77b24..676f6d38 100644 --- a/YDotNet.Server/Events.cs +++ b/YDotNet.Server/Events.cs @@ -4,9 +4,9 @@ namespace YDotNet.Server; public abstract class DocumentEvent { - required public IDocumentManager DocumentManager { get; init; } + required public IDocumentManager Source { get; init; } - required public DocumentContext DocumentContext { get; init; } + required public DocumentContext Context { get; init; } } public class DocumentChangeEvent : DocumentEvent @@ -14,6 +14,11 @@ public class DocumentChangeEvent : DocumentEvent required public Doc Document { get; init; } } +public class DocumentLoadEvent : DocumentEvent +{ + required public Doc Document { get; init; } +} + public sealed class DocumentChangedEvent : DocumentChangeEvent { required public byte[] Diff { get; init; } diff --git a/YDotNet.Server/IDocumentCallback.cs b/YDotNet.Server/IDocumentCallback.cs index 67a317bd..d0f662db 100644 --- a/YDotNet.Server/IDocumentCallback.cs +++ b/YDotNet.Server/IDocumentCallback.cs @@ -7,6 +7,11 @@ ValueTask OnInitializedAsync(IDocumentManager manager) return default; } + ValueTask OnDocumentLoadedAsync(DocumentLoadEvent @event) + { + return default; + } + ValueTask OnDocumentChangingAsync(DocumentChangeEvent @event) { return default; diff --git a/YDotNet.Server/Internal/CallbackInvoker.cs b/YDotNet.Server/Internal/CallbackInvoker.cs index 5151a358..d8f34549 100644 --- a/YDotNet.Server/Internal/CallbackInvoker.cs +++ b/YDotNet.Server/Internal/CallbackInvoker.cs @@ -18,6 +18,11 @@ public ValueTask OnInitializedAsync(IDocumentManager manager) return InvokeCallbackAsync(manager, (c, m) => c.OnInitializedAsync(m)); } + public ValueTask OnDocumentLoadedAsync(DocumentLoadEvent @event) + { + return InvokeCallbackAsync(@event, (c, e) => c.OnDocumentLoadedAsync(e)); + } + public ValueTask OnDocumentChangingAsync(DocumentChangeEvent @event) { return InvokeCallbackAsync(@event, (c, e) => c.OnDocumentChangingAsync(e)); diff --git a/YDotNet.Server/Internal/DelayedWriter.cs b/YDotNet.Server/Internal/DelayedWriter.cs index 02081a54..9a7d30e2 100644 --- a/YDotNet.Server/Internal/DelayedWriter.cs +++ b/YDotNet.Server/Internal/DelayedWriter.cs @@ -5,9 +5,9 @@ internal sealed class DelayedWriter private readonly TimeSpan delay; private readonly TimeSpan delayMax; private readonly Func action; + private readonly Timer writeTimer; private int pendingWrites = 0; private DateTime lastWrite; - private Timer? writeTimer; private Task? writeTask; public Func Clock = () => DateTime.UtcNow; @@ -17,11 +17,13 @@ public DelayedWriter(TimeSpan delay, TimeSpan delayMax, Func action) this.delay = delay; this.delayMax = delayMax; this.action = action; + + writeTimer = new Timer(_ => Write(), null, Timeout.Infinite, Timeout.Infinite); } public async Task FlushAsync() { - writeTimer?.Dispose(); + writeTimer.Change(Timeout.Infinite, Timeout.Infinite); if (writeTask != null) { @@ -53,12 +55,13 @@ public void Ping() var timeSinceLastPing = now - lastWrite; if (timeSinceLastPing > delayMax) { - Write(); + // Trigger the write operation immediately, but use no in the current thread to avoid blocking. + writeTimer.Change(0, Timeout.Infinite); } else { - writeTimer?.Dispose(); - writeTimer = new Timer(_ => Write(), null, (int)delay.TotalMilliseconds, 0); + // Reset the timer with every change. + writeTimer.Change((int)delay.TotalMilliseconds, Timeout.Infinite); } } @@ -90,8 +93,9 @@ private async Task WriteAsync() #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed Interlocked.CompareExchange(ref writeTask, null, task); - Interlocked.Add(ref pendingWrites, -localPendingWrites); #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + + Interlocked.Add(ref pendingWrites, -localPendingWrites); } } } diff --git a/YDotNet.Server/Internal/DocumentContainer.cs b/YDotNet.Server/Internal/DocumentContainer.cs index d6e3b048..8ade868f 100644 --- a/YDotNet.Server/Internal/DocumentContainer.cs +++ b/YDotNet.Server/Internal/DocumentContainer.cs @@ -1,6 +1,7 @@ using YDotNet.Document; using YDotNet.Server.Storage; using YDotNet.Server.Internal; +using System.Reflection.Metadata; namespace YDotNet.Server.Internal; @@ -14,7 +15,11 @@ internal sealed class DocumentContainer private readonly DelayedWriter writer; private Doc? doc; - public DocumentContainer(string documentName, IDocumentStorage documentStorage, DocumentManagerOptions options) + public DocumentContainer(string documentName, + IDocumentStorage documentStorage, + IDocumentCallback documentCallback, + IDocumentManager documentManager, + DocumentManagerOptions options) { this.documentName = documentName; this.documentStorage = documentStorage; @@ -22,7 +27,7 @@ public DocumentContainer(string documentName, IDocumentStorage documentStorage, writer = new DelayedWriter(options.DelayWriting, options.MaxWriteTimeInterval, WriteAsync); - loadingTask = LoadInternalAsync(); + loadingTask = LoadInternalAsync(documentCallback, documentManager); } public Task FlushAsync() @@ -30,11 +35,18 @@ public Task FlushAsync() return writer.FlushAsync(); } - private async Task LoadInternalAsync() + private async Task LoadInternalAsync(IDocumentCallback documentCallback, IDocumentManager documentManager) { doc = await LoadCoreAsync(); - doc.ObserveUpdatesV2(e => + await documentCallback.OnDocumentLoadedAsync(new DocumentLoadEvent + { + Document = doc, + Context = new DocumentContext { ClientId = 0, DocumentName = documentName }, + Source = documentManager, + }); + + doc.ObserveUpdatesV1(e => { writer.Ping(); }); @@ -48,9 +60,9 @@ private async Task LoadCoreAsync() if (documentData != null) { - var doc = new Doc(); + var document = new Doc(); - using (var transaction = doc.WriteTransaction()) + using (var transaction = document.WriteTransaction()) { if (transaction == null) { @@ -60,7 +72,7 @@ private async Task LoadCoreAsync() transaction.ApplyV2(documentData); } - return doc; + return document; } if (options.AutoCreateDocument) diff --git a/YDotNet.Server/Internal/DocumentContainerCache.cs b/YDotNet.Server/Internal/DocumentContainerCache.cs index 46e0665b..b77cee1e 100644 --- a/YDotNet.Server/Internal/DocumentContainerCache.cs +++ b/YDotNet.Server/Internal/DocumentContainerCache.cs @@ -7,14 +7,22 @@ namespace YDotNet.Server.Internal; internal sealed class DocumentContainerCache : IAsyncDisposable { private readonly IDocumentStorage documentStorage; + private readonly IDocumentCallback documentCallback; + private readonly IDocumentManager documentManager; private readonly DocumentManagerOptions options; private readonly MemoryCache memoryCache = new(Options.Create(new MemoryCacheOptions())); private readonly Dictionary livingContainers = new(); private readonly SemaphoreSlim slimLock = new(1); - public DocumentContainerCache(IDocumentStorage documentStorage, DocumentManagerOptions options) + public DocumentContainerCache( + IDocumentStorage documentStorage, + IDocumentCallback documentCallback, + IDocumentManager documentManager, + DocumentManagerOptions options) { this.documentStorage = documentStorage; + this.documentCallback = documentCallback; + this.documentManager = documentManager; this.options = options; } @@ -54,7 +62,12 @@ public DocumentContainer GetContext(string name) } else { - container = new DocumentContainer(name, documentStorage, options); + container = new DocumentContainer( + name, + documentStorage, + documentCallback, + documentManager, + options); } // For each access we extend the lifetime of the cache entry. diff --git a/YDotNet.Server/ServiceExtensions.cs b/YDotNet.Server/ServiceExtensions.cs index 857095d7..87b53512 100644 --- a/YDotNet.Server/ServiceExtensions.cs +++ b/YDotNet.Server/ServiceExtensions.cs @@ -21,6 +21,11 @@ public static YDotnetRegistration AddYDotNet(this IServiceCollection services) Services = services }; } + public static YDotnetRegistration AddCallback(this YDotnetRegistration registration) where T : class, IDocumentCallback + { + registration.Services.AddSingleton(); + return registration; + } } public sealed class YDotnetRegistration From 5f5ebf1198a723d51910cd794b335966d0a4c29f Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 3 Oct 2023 10:28:10 +0200 Subject: [PATCH 012/186] Batching. --- Demo/Callback.cs | 6 +- Demo/Program.cs | 2 +- YDotNet.Server.Redis/Internal/PublishQueue.cs | 85 ++++++++++++ YDotNet.Server.Redis/Messages.cs | 67 +++++++--- YDotNet.Server.Redis/RedisCallback.cs | 126 +++++++++--------- .../RedisClusteringOptions.cs | 6 + .../YDotNetSocketMiddleware.cs | 6 +- YDotNet.Server/DefaultDocumentManager.cs | 26 ++-- YDotNet.Server/DocumentRequestContext.cs | 6 +- YDotNet.Server/IDocumentCallback.cs | 2 +- YDotNet.Server/Internal/CallbackInvoker.cs | 4 +- YDotNet.Server/Internal/DocumentContainer.cs | 2 +- YDotNet.Server/YDotNet.Server.csproj | 4 - YDotNet/Protocol/Decoder.cs | 55 ++++++-- YDotNet/Protocol/Encoder.cs | 76 +++++++++-- 15 files changed, 327 insertions(+), 146 deletions(-) create mode 100644 YDotNet.Server.Redis/Internal/PublishQueue.cs diff --git a/Demo/Callback.cs b/Demo/Callback.cs index 608017ab..e60258dc 100644 --- a/Demo/Callback.cs +++ b/Demo/Callback.cs @@ -3,11 +3,11 @@ namespace Demo; -public sealed class Listener : IDocumentCallback +public sealed class Callback : IDocumentCallback { - private readonly ILogger log; + private readonly ILogger log; - public Listener(ILogger log) + public Callback(ILogger log) { this.log = log; } diff --git a/Demo/Program.cs b/Demo/Program.cs index f9f58d27..348afac4 100644 --- a/Demo/Program.cs +++ b/Demo/Program.cs @@ -8,7 +8,7 @@ public static void Main(string[] args) builder.Services.AddRazorPages(); builder.Services.AddYDotNet() - .AddCallback() + .AddCallback() .AddWebSockets(); var app = builder.Build(); diff --git a/YDotNet.Server.Redis/Internal/PublishQueue.cs b/YDotNet.Server.Redis/Internal/PublishQueue.cs new file mode 100644 index 00000000..86de4618 --- /dev/null +++ b/YDotNet.Server.Redis/Internal/PublishQueue.cs @@ -0,0 +1,85 @@ +using System.Threading.Channels; + +namespace YDotNet.Server.Redis.Internal; + +public interface ICanEstimateSize +{ + int EstimateSize(); +} + +public sealed class PublishQueue where T : ICanEstimateSize +{ + private readonly Channel inputChannel = Channel.CreateBounded(100); + private readonly Channel> outputChannel = Channel.CreateBounded>(2); + private readonly CancellationTokenSource cts = new(); + + public PublishQueue(int maxCount, int maxSize, int timeout, Func, CancellationToken, Task> handler) + { + Task.Run(async () => + { + var batchList = new List(maxCount); + var batchSize = 0; + + // Just a marker object to force sending out new batches. + var force = new object(); + + await using var timer = new Timer(_ => inputChannel.Writer.TryWrite(force)); + + async Task TrySendAsync() + { + if (batchList.Count > 0) + { + await outputChannel.Writer.WriteAsync(batchList, cts.Token); + + // Create a new batch, because the value is shared and might be processes by another concurrent task. + batchList = new List(); + batchSize = 0; + } + } + + // Exceptions usually that the process was stopped and the channel closed, therefore we do not catch them. + await foreach (var item in inputChannel.Reader.ReadAllAsync(cts.Token)) + { + if (ReferenceEquals(item, force)) + { + // Our item is the marker object from the timer. + await TrySendAsync(); + } + else if (item is T typed) + { + // The timeout restarts with the last event and should push events out if no further events are received. + timer.Change(timeout, Timeout.Infinite); + + batchList.Add(typed); + batchSize += typed.EstimateSize(); + + if (batchList.Count >= maxSize || batchSize >= maxSize) + { + await TrySendAsync(); + } + } + } + + await TrySendAsync(); + }, cts.Token).ContinueWith(x => outputChannel.Writer.TryComplete(x.Exception)); + + Task.Run(async () => + { + await foreach (var batch in outputChannel.Reader.ReadAllAsync(cts.Token)) + { + await handler(batch, cts.Token); + } + }, cts.Token); + } + + public ValueTask EnqueueAsync(T item, + CancellationToken ct) + { + return inputChannel.Writer.WriteAsync(item, ct); + } + + public void Dispose() + { + cts.Cancel(); + } +} diff --git a/YDotNet.Server.Redis/Messages.cs b/YDotNet.Server.Redis/Messages.cs index ac4898eb..3fcb5271 100644 --- a/YDotNet.Server.Redis/Messages.cs +++ b/YDotNet.Server.Redis/Messages.cs @@ -1,45 +1,68 @@ -using System.Text.Json.Serialization; +using ProtoBuf; +using YDotNet.Server.Redis.Internal; namespace YDotNet.Server.Redis; -public sealed class Message +[ProtoContract] +public sealed class Message : ICanEstimateSize { - [JsonPropertyName("s")] + private static readonly int GuidLength = Guid.Empty.ToString().Length; + + [ProtoMember(1)] required public Guid SenderId { get; init; } - [JsonPropertyName("c")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public PingMessage? Pinged { get; set; } + [ProtoMember(2)] + required public string DocumentName { get; init; } + + [ProtoMember(3)] + required public long ClientId { get; init; } + + [ProtoMember(4)] + public ClientPingMessage? ClientPinged { get; set; } - [JsonPropertyName("d")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public PingMessage[]? ClientDisconnected { get; set; } + [ProtoMember(5)] + public ClientDisconnectMessage? ClientDisconnected { get; set; } - [JsonPropertyName("u")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [ProtoMember(6)] public DocumentChangeMessage? DocumentChanged { get; set; } + + public int EstimateSize() + { + var size = + GuidLength + + sizeof(long) + + DocumentName.Length + + ClientPinged?.EstimatedSize() ?? 0 + + ClientDisconnected?.EstimatedSize() ?? 0 + + DocumentChanged?.EstimatedSize() ?? 0; + + return size; + } } -public abstract class ClientMessage +[ProtoContract] +public sealed class ClientDisconnectMessage { - [JsonPropertyName("d")] - required public string DocumentName { get; init; } - - [JsonPropertyName("c")] - required public long ClientId { get; init; } + public int EstimatedSize() => 0; } -public sealed class DocumentChangeMessage : ClientMessage +[ProtoContract] +public sealed class DocumentChangeMessage { - [JsonPropertyName("u")] + [ProtoMember(1)] required public byte[] DocumentDiff { get; init; } + + public int EstimatedSize() => sizeof(long) + DocumentDiff?.Length ?? 0; } -public sealed class PingMessage : ClientMessage +[ProtoContract] +public sealed class ClientPingMessage { - [JsonPropertyName("c")] + [ProtoMember(1)] required public long ClientClock { get; init; } - [JsonPropertyName("s")] + [ProtoMember(2)] required public string? ClientState { get; init; } + + public int EstimatedSize() => sizeof(long) + ClientState?.Length ?? 0; } diff --git a/YDotNet.Server.Redis/RedisCallback.cs b/YDotNet.Server.Redis/RedisCallback.cs index 58baba5d..1d20af04 100644 --- a/YDotNet.Server.Redis/RedisCallback.cs +++ b/YDotNet.Server.Redis/RedisCallback.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using ProtoBuf; using StackExchange.Redis; using System.Text.Json; using YDotNet.Server.Redis.Internal; @@ -12,12 +13,20 @@ public sealed class RedisCallback : IDocumentCallback, IHostedService private readonly Guid senderId = Guid.NewGuid(); private readonly RedisClusteringOptions options; private readonly ILogger logger; + private readonly PublishQueue queue; private ISubscriber? subscriber; private IDocumentManager? documentManager; public RedisCallback(IOptions options, ILogger logger) { this.options = options.Value; + + queue = new PublishQueue( + this.options.MaxBatchCount, + this.options.MaxBatchSize, + (int)this.options.DebounceTime.TotalMilliseconds, + PublishBatchAsync); + this.logger = logger; } @@ -44,6 +53,8 @@ public ValueTask OnInitializedAsync( public Task StopAsync( CancellationToken cancellationToken) { + queue.Dispose(); + subscriber?.UnsubscribeAll(); return Task.CompletedTask; } @@ -55,116 +66,99 @@ private async Task HandleMessage(RedisValue value) return; } - var message = JsonSerializer.Deserialize(value.ToString()); + var batch = Serializer.Deserialize(value); - if (message == null || message.SenderId == senderId) + if (batch == null) { return; } - if (message.DocumentChanged is DocumentChangeMessage changed) + foreach (var message in batch) { - await documentManager.ApplyUpdateAsync(new DocumentContext + if (message.SenderId == senderId) { - ClientId = changed.ClientId, - DocumentName = changed.DocumentName, - Metadata = senderId - }, changed.DocumentDiff); - } + continue; + } - if (message.Pinged is PingMessage pinged) - { - await documentManager.PingAsync(new DocumentContext + var context = new DocumentContext(message.DocumentName, message.ClientId) { - ClientId = pinged.ClientId, - DocumentName = pinged.DocumentName, Metadata = senderId - }, pinged.ClientClock, pinged.ClientState); - } + }; - if (message.ClientDisconnected is PingMessage[] disconnected) - { - foreach (var client in disconnected) + if (message.DocumentChanged is DocumentChangeMessage changed) { - await documentManager.DisconnectAsync(new DocumentContext - { - ClientId = client.ClientId, - DocumentName = client.DocumentName, - Metadata = senderId - }); + await documentManager.ApplyUpdateAsync(context, changed.DocumentDiff); } - } + + if (message.ClientPinged is ClientPingMessage pinged) + { + await documentManager.PingAsync(context, pinged.ClientClock, pinged.ClientState); + } + + if (message.ClientDisconnected is not null) + { + await documentManager.DisconnectAsync(context); + } + } } - public async ValueTask OnAwarenessUpdatedAsync(ClientAwarenessEvent @event) + public ValueTask OnAwarenessUpdatedAsync(ClientAwarenessEvent @event) { - if (subscriber == null) - { - return; - } - var message = new Message { - SenderId = senderId, - Pinged = new PingMessage + ClientId = @event.Context.ClientId, + ClientPinged = new ClientPingMessage { - ClientId = @event.Context.ClientId, ClientClock = @event.ClientClock, ClientState = @event.ClientState, - DocumentName = @event.Context.DocumentName, }, + DocumentName = @event.Context.DocumentName, + SenderId = senderId, }; - var json = JsonSerializer.Serialize(message); - - await subscriber.PublishAsync(options.Channel, json); + return queue.EnqueueAsync(message, default); } - public async ValueTask OnClientDisconnectedAsync(ClientDisconnectedEvent[] events) + public ValueTask OnClientDisconnectedAsync(ClientDisconnectedEvent @event) { - if (subscriber == null) - { - return; - } - var message = new Message { + ClientId = @event.Context.ClientId, + ClientDisconnected = new ClientDisconnectMessage(), + DocumentName = @event.Context.DocumentName, SenderId = senderId, - ClientDisconnected = events.Select(x => new PingMessage - { - ClientId = x.Context.ClientId, - ClientState = null, - ClientClock = 0, - DocumentName = x.Context.DocumentName, - }).ToArray() }; - var json = JsonSerializer.Serialize(message); - - await subscriber.PublishAsync(options.Channel, json); + return queue.EnqueueAsync(message, default); } - public async ValueTask OnDocumentChangedAsync(DocumentChangedEvent @event) + public ValueTask OnDocumentChangedAsync(DocumentChangedEvent @event) { - if (subscriber == null) - { - return; - } - var message = new Message { - SenderId = senderId, + ClientId = @event.Context.ClientId, + DocumentName = @event.Context.DocumentName, DocumentChanged = new DocumentChangeMessage { - ClientId = @event.Context.ClientId, - DocumentName = @event.Context.DocumentName, DocumentDiff = @event.Diff }, + SenderId = senderId }; - var json = JsonSerializer.Serialize(message); + return queue.EnqueueAsync(message, default); + } + + private async Task PublishBatchAsync(List batch, CancellationToken ct) + { + if (subscriber == null) + { + return; + } + + using var stream = new MemoryStream(); - await subscriber.PublishAsync(options.Channel, json); + Serializer.Serialize(stream, batch); + await subscriber.PublishAsync(options.Channel, stream.ToArray()); } } diff --git a/YDotNet.Server.Redis/RedisClusteringOptions.cs b/YDotNet.Server.Redis/RedisClusteringOptions.cs index c6fb44b0..869f49ea 100644 --- a/YDotNet.Server.Redis/RedisClusteringOptions.cs +++ b/YDotNet.Server.Redis/RedisClusteringOptions.cs @@ -6,6 +6,12 @@ public sealed class RedisClusteringOptions { public RedisChannel Channel { get; set; } = RedisChannel.Literal("YDotNet"); + public TimeSpan DebounceTime { get; set; } = TimeSpan.FromMilliseconds(500); + + public int MaxBatchCount { get; set; } = 100; + + public int MaxBatchSize { get; set; } = 1024 * 1024; + public ConfigurationOptions? Configuration { get; set; } public Func>? ConnectionFactory { get; set; } diff --git a/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs b/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs index c073e4c9..94620622 100644 --- a/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs +++ b/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs @@ -84,7 +84,7 @@ public async Task InvokeAsync(HttpContext httpContext) using var state = new ClientState { Decoder = new WebSocketDecoder(webSocket), - DocumentContext = new DocumentContext { ClientId = 0, DocumentName = documentName }, + DocumentContext = new DocumentContext(documentName, 0), DocumentName = documentName, Encoder = new WebSocketEncoder(webSocket), WebSocket = webSocket @@ -224,14 +224,14 @@ private async Task HandleAwarenessAsync(ClientState state, if (state.DocumentContext.ClientId == 0 && clientCount == 1) { - state.DocumentContext.ClientId = clientId; + state.DocumentContext = state.DocumentContext with { ClientId = clientId }; logger.LogDebug("Websocket connection to {document} enhanced with client Id {clientId}.", state.DocumentContext.DocumentName, state.DocumentContext.ClientId); } - var context = new DocumentContext { ClientId = clientId, DocumentName = state.DocumentName }; + var context = new DocumentContext(state.DocumentName, clientId); await documentManager!.PingAsync(context, clientClock, clientState, ct); } diff --git a/YDotNet.Server/DefaultDocumentManager.cs b/YDotNet.Server/DefaultDocumentManager.cs index f0047136..3e442925 100644 --- a/YDotNet.Server/DefaultDocumentManager.cs +++ b/YDotNet.Server/DefaultDocumentManager.cs @@ -184,13 +184,10 @@ public async ValueTask DisconnectAsync(DocumentContext context, { if (users.Remove(context.DocumentName, context.ClientId)) { - await callback.OnClientDisconnectedAsync(new[] - { - new ClientDisconnectedEvent - { - Context = context, - Source = this, - } + await callback.OnClientDisconnectedAsync(new ClientDisconnectedEvent + { + Context = context, + Source = this, }); } } @@ -198,18 +195,13 @@ await callback.OnClientDisconnectedAsync(new[] public async ValueTask CleanupAsync( CancellationToken ct = default) { - var removedUsers = users.Cleanup(options.MaxPingTime).ToList(); - - if (removedUsers.Count > 0) + foreach (var removedUser in users.Cleanup(options.MaxPingTime)) { - await callback.OnClientDisconnectedAsync(removedUsers.Select(x => + await callback.OnClientDisconnectedAsync(new ClientDisconnectedEvent { - return new ClientDisconnectedEvent - { - Context = new DocumentContext { ClientId = x.ClientId, DocumentName = x.DocumentName }, - Source = this, - }; - }).ToArray()); + Context = new DocumentContext(removedUser.DocumentName, removedUser.ClientId), + Source = this, + }); } containers.RemoveEvictedItems(); diff --git a/YDotNet.Server/DocumentRequestContext.cs b/YDotNet.Server/DocumentRequestContext.cs index 270eb065..ec3bfe47 100644 --- a/YDotNet.Server/DocumentRequestContext.cs +++ b/YDotNet.Server/DocumentRequestContext.cs @@ -1,10 +1,6 @@ namespace YDotNet.Server; -public sealed class DocumentContext +public sealed record DocumentContext(string DocumentName, long ClientId) { - required public string DocumentName { get; set; } - - required public long ClientId { get; set; } - public object? Metadata { get; set; } } diff --git a/YDotNet.Server/IDocumentCallback.cs b/YDotNet.Server/IDocumentCallback.cs index d0f662db..870be586 100644 --- a/YDotNet.Server/IDocumentCallback.cs +++ b/YDotNet.Server/IDocumentCallback.cs @@ -22,7 +22,7 @@ ValueTask OnDocumentChangedAsync(DocumentChangedEvent @event) return default; } - ValueTask OnClientDisconnectedAsync(ClientDisconnectedEvent[] events) + ValueTask OnClientDisconnectedAsync(ClientDisconnectedEvent @event) { return default; } diff --git a/YDotNet.Server/Internal/CallbackInvoker.cs b/YDotNet.Server/Internal/CallbackInvoker.cs index d8f34549..12f7a6c7 100644 --- a/YDotNet.Server/Internal/CallbackInvoker.cs +++ b/YDotNet.Server/Internal/CallbackInvoker.cs @@ -33,9 +33,9 @@ public ValueTask OnDocumentChangedAsync(DocumentChangedEvent @event) return InvokeCallbackAsync(@event, (c, e) => c.OnDocumentChangedAsync(e)); } - public ValueTask OnClientDisconnectedAsync(ClientDisconnectedEvent[] events) + public ValueTask OnClientDisconnectedAsync(ClientDisconnectedEvent @event) { - return InvokeCallbackAsync(@events, (c, e) => c.OnClientDisconnectedAsync(e)); + return InvokeCallbackAsync(@event, (c, e) => c.OnClientDisconnectedAsync(e)); } public ValueTask OnAwarenessUpdatedAsync(ClientAwarenessEvent @event) diff --git a/YDotNet.Server/Internal/DocumentContainer.cs b/YDotNet.Server/Internal/DocumentContainer.cs index 8ade868f..11072f67 100644 --- a/YDotNet.Server/Internal/DocumentContainer.cs +++ b/YDotNet.Server/Internal/DocumentContainer.cs @@ -42,7 +42,7 @@ private async Task LoadInternalAsync(IDocumentCallback documentCallback, ID await documentCallback.OnDocumentLoadedAsync(new DocumentLoadEvent { Document = doc, - Context = new DocumentContext { ClientId = 0, DocumentName = documentName }, + Context = new DocumentContext(documentName, 0), Source = documentManager, }); diff --git a/YDotNet.Server/YDotNet.Server.csproj b/YDotNet.Server/YDotNet.Server.csproj index 6c1752bc..e86c671c 100644 --- a/YDotNet.Server/YDotNet.Server.csproj +++ b/YDotNet.Server/YDotNet.Server.csproj @@ -10,10 +10,6 @@ - - - - diff --git a/YDotNet/Protocol/Decoder.cs b/YDotNet/Protocol/Decoder.cs index 30b05b40..34efcaee 100644 --- a/YDotNet/Protocol/Decoder.cs +++ b/YDotNet/Protocol/Decoder.cs @@ -1,13 +1,23 @@ -using System; using System.Buffers; using System.Text; namespace YDotNet.Protocol; +/// +/// Base class for all decoders for implementing the y-protocol. +/// public abstract class Decoder { private readonly byte[] stringBuffer = new byte[128]; + /// + /// Reads an unsigned integer with variable length encoding. + /// + /// The cancellation token. + /// + /// The decoded integer. + /// + /// The input is in an invalid format. public async ValueTask ReadVarUintAsync( CancellationToken ct = default) { @@ -15,7 +25,7 @@ public async ValueTask ReadVarUintAsync( while (true) { - var lower7bits = await ReadByteAsync(ct); + var lower7bits = await this.ReadByteAsync(ct); value |= (lower7bits & 0x7f) << shift; @@ -30,22 +40,36 @@ public async ValueTask ReadVarUintAsync( throw new IndexOutOfRangeException(); } + /// + /// Reads the length and the bytes of an array. + /// + /// The cancellation token. + /// + /// The decoded byte array. + /// public async ValueTask ReadVarUint8ArrayAsync( CancellationToken ct) { - var arrayLength = await ReadVarUintAsync(ct); + var arrayLength = await this.ReadVarUintAsync(ct); var arrayBuffer = new byte[arrayLength]; - await ReadBytesAsync(arrayBuffer, ct); + await this.ReadBytesAsync(arrayBuffer, ct); return arrayBuffer; } + /// + /// Reads an UTF8 string. + /// + /// The cancellation token. + /// + /// The decoded string. + /// public async ValueTask ReadVarStringAsync( CancellationToken ct) { - var length = (int)await ReadVarUintAsync(ct); - if (length > stringBuffer.Length) + var length = (int)await this.ReadVarUintAsync(ct); + if (length > this.stringBuffer.Length) { var buffer = ArrayPool.Shared.Rent(length); try @@ -59,21 +83,36 @@ public async ValueTask ReadVarStringAsync( } else { - return await ReadCoreAsync(length, stringBuffer, ct); + return await ReadCoreAsync(length, this.stringBuffer, ct); } async ValueTask ReadCoreAsync(int length, byte[] buffer, CancellationToken ct) { var slicedBuffer = buffer.AsMemory(0, length); - await ReadBytesAsync(slicedBuffer, ct); + await this.ReadBytesAsync(slicedBuffer, ct); return Encoding.UTF8.GetString(slicedBuffer.Span); } } + /// + /// Reads the next byte. + /// + /// The cancellation token. + /// + /// The read byte. + /// protected abstract ValueTask ReadByteAsync( CancellationToken ct); + /// + /// Reads the bytes to the given memory. + /// + /// The bytes to read. + /// The cancellation token. + /// + /// The task that completes when everything has been read. + /// protected abstract ValueTask ReadBytesAsync(Memory bytes, CancellationToken ct); } diff --git a/YDotNet/Protocol/Encoder.cs b/YDotNet/Protocol/Encoder.cs index 4e45c362..de7b5032 100644 --- a/YDotNet/Protocol/Encoder.cs +++ b/YDotNet/Protocol/Encoder.cs @@ -1,19 +1,25 @@ -using System; using System.Buffers; -using System.Collections.Generic; -using System.Linq; -using System.Security.Cryptography; using System.Text; -using System.Threading.Tasks; + +#pragma warning disable SA1116 // Split parameters should start on line after declaration namespace YDotNet.Protocol; +/// +/// Base class for all encoders for implementing the y-protocol. +/// public abstract class Encoder { - private const int BITS7 = 1 << 7; - private const int BITS8 = 1 << 8; private readonly byte[] stringBuffer = new byte[128]; + /// + /// Writes a number with variable length encoding. + /// + /// The number to write. + /// The cancellation token. + /// + /// The task representing the async operation. + /// public async ValueTask WriteVarUintAsync(long value, CancellationToken ct = default) { @@ -28,23 +34,51 @@ public async ValueTask WriteVarUintAsync(long value, lower7bits |= 128; } - await WriteByteAsync(lower7bits, ct); + await this.WriteByteAsync(lower7bits, ct); } while (value > 0); } + /// + /// Writes a byte array. + /// + /// The byte array to write. + /// The cancellation token. + /// + /// The task representing the async operation. + /// + /// is null. public async ValueTask WriteVarUint8Array(byte[] value, CancellationToken ct = default) { - await WriteVarUintAsync(value.Length, ct); - await WriteBytesAsync(value, ct); + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + await this.WriteVarUintAsync(value.Length, ct); + await this.WriteBytesAsync(value, ct); } + /// + /// Writes a string. + /// + /// The string to write. + /// The cancellation token. + /// + /// The task representing the async operation. + /// + /// is null. public async ValueTask WriteVarStringAsync(string value, CancellationToken ct = default) { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + var length = Encoding.UTF8.GetByteCount(value); - if (length > stringBuffer.Length) + if (length > this.stringBuffer.Length) { var buffer = ArrayPool.Shared.Rent(length); try @@ -58,20 +92,36 @@ public async ValueTask WriteVarStringAsync(string value, } else { - await WriteCoreAsync(value, stringBuffer, ct); + await WriteCoreAsync(value, this.stringBuffer, ct); } async Task WriteCoreAsync(string value, byte[] buffer, CancellationToken ct) { var length = Encoding.UTF8.GetBytes(value, buffer); - await WriteBytesAsync(new ArraySegment(buffer, 0, length), ct); + await this.WriteBytesAsync(new ArraySegment(buffer, 0, length), ct); } } + /// + /// Write a single byte. + /// + /// The byte to write. + /// The cancellation token. + /// + /// The task representing the async operation. + /// protected abstract ValueTask WriteByteAsync(byte value, CancellationToken ct); + /// + /// Write a byte array. + /// + /// The byte array to write. + /// The cancellation token. + /// + /// The task representing the async operation. + /// protected abstract ValueTask WriteBytesAsync(ArraySegment bytes, CancellationToken ct); } From 91ef442a96a4895cfd28fbc35a60970281483a59 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 3 Oct 2023 10:58:51 +0200 Subject: [PATCH 013/186] Simplified code. --- ...Callback.cs => RedisClusteringCallback.cs} | 57 ++++++++---------- .../RedisClusteringOptions.cs | 19 ------ YDotNet.Server.Redis/RedisConnection.cs | 24 ++++++++ YDotNet.Server.Redis/RedisDocumentStorage.cs | 60 +++++++++++++++++++ .../RedisDocumentStorageOptions.cs | 10 ++++ YDotNet.Server.Redis/RedisOptions.cs | 25 ++++++++ YDotNet.Server.Redis/ServiceExtensions.cs | 26 ++++++-- .../YDotNet.Server.Redis.csproj | 1 + YDotNet.sln | 6 +- 9 files changed, 168 insertions(+), 60 deletions(-) rename YDotNet.Server.Redis/{RedisCallback.cs => RedisClusteringCallback.cs} (71%) create mode 100644 YDotNet.Server.Redis/RedisConnection.cs create mode 100644 YDotNet.Server.Redis/RedisDocumentStorage.cs create mode 100644 YDotNet.Server.Redis/RedisDocumentStorageOptions.cs create mode 100644 YDotNet.Server.Redis/RedisOptions.cs diff --git a/YDotNet.Server.Redis/RedisCallback.cs b/YDotNet.Server.Redis/RedisClusteringCallback.cs similarity index 71% rename from YDotNet.Server.Redis/RedisCallback.cs rename to YDotNet.Server.Redis/RedisClusteringCallback.cs index 1d20af04..3071ff8b 100644 --- a/YDotNet.Server.Redis/RedisCallback.cs +++ b/YDotNet.Server.Redis/RedisClusteringCallback.cs @@ -1,64 +1,57 @@ -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using ProtoBuf; using StackExchange.Redis; -using System.Text.Json; using YDotNet.Server.Redis.Internal; namespace YDotNet.Server.Redis; -public sealed class RedisCallback : IDocumentCallback, IHostedService +public sealed class RedisClusteringCallback : IDocumentCallback, IDisposable { private readonly Guid senderId = Guid.NewGuid(); - private readonly RedisClusteringOptions options; - private readonly ILogger logger; - private readonly PublishQueue queue; + private readonly RedisClusteringOptions redisOptions; + private readonly PublishQueue subscriberQueue; private ISubscriber? subscriber; private IDocumentManager? documentManager; - public RedisCallback(IOptions options, ILogger logger) + public RedisClusteringCallback(IOptions redisOptions, RedisConnection redisConnection) { - this.options = options.Value; + this.redisOptions = redisOptions.Value; - queue = new PublishQueue( - this.options.MaxBatchCount, - this.options.MaxBatchSize, - (int)this.options.DebounceTime.TotalMilliseconds, + subscriberQueue = new PublishQueue( + this.redisOptions.MaxBatchCount, + this.redisOptions.MaxBatchSize, + (int)this.redisOptions.DebounceTime.TotalMilliseconds, PublishBatchAsync); - this.logger = logger; + _ = InitializeAsync(redisConnection); } - public async Task StartAsync( - CancellationToken cancellationToken) + public async Task InitializeAsync(RedisConnection redisConnection) { - var connection = await options.ConnectAsync(new LoggerTextWriter(logger)); + // Use a single task, so that the ordering of registrations does not matter. + var connection = await redisConnection.Instance; - // Is only needed for topics, but it has only minor costs. subscriber = connection.GetSubscriber(); - subscriber.Subscribe(options.Channel, async (_, value) => + subscriber.Subscribe(redisOptions.Channel, async (_, value) => { await HandleMessage(value); }); } + public void Dispose() + { + subscriberQueue.Dispose(); + subscriber?.UnsubscribeAll(); + } + public ValueTask OnInitializedAsync( IDocumentManager manager) { + // The initialize method is used to prevent circular dependencies between managers and hooks. documentManager = manager; return default; } - public Task StopAsync( - CancellationToken cancellationToken) - { - queue.Dispose(); - - subscriber?.UnsubscribeAll(); - return Task.CompletedTask; - } - private async Task HandleMessage(RedisValue value) { if (documentManager == null) @@ -116,7 +109,7 @@ public ValueTask OnAwarenessUpdatedAsync(ClientAwarenessEvent @event) SenderId = senderId, }; - return queue.EnqueueAsync(message, default); + return subscriberQueue.EnqueueAsync(message, default); } public ValueTask OnClientDisconnectedAsync(ClientDisconnectedEvent @event) @@ -129,7 +122,7 @@ public ValueTask OnClientDisconnectedAsync(ClientDisconnectedEvent @event) SenderId = senderId, }; - return queue.EnqueueAsync(message, default); + return subscriberQueue.EnqueueAsync(message, default); } public ValueTask OnDocumentChangedAsync(DocumentChangedEvent @event) @@ -145,7 +138,7 @@ public ValueTask OnDocumentChangedAsync(DocumentChangedEvent @event) SenderId = senderId }; - return queue.EnqueueAsync(message, default); + return subscriberQueue.EnqueueAsync(message, default); } private async Task PublishBatchAsync(List batch, CancellationToken ct) @@ -159,6 +152,6 @@ private async Task PublishBatchAsync(List batch, CancellationToken ct) Serializer.Serialize(stream, batch); - await subscriber.PublishAsync(options.Channel, stream.ToArray()); + await subscriber.PublishAsync(redisOptions.Channel, stream.ToArray()); } } diff --git a/YDotNet.Server.Redis/RedisClusteringOptions.cs b/YDotNet.Server.Redis/RedisClusteringOptions.cs index 869f49ea..27a376e1 100644 --- a/YDotNet.Server.Redis/RedisClusteringOptions.cs +++ b/YDotNet.Server.Redis/RedisClusteringOptions.cs @@ -11,23 +11,4 @@ public sealed class RedisClusteringOptions public int MaxBatchCount { get; set; } = 100; public int MaxBatchSize { get; set; } = 1024 * 1024; - - public ConfigurationOptions? Configuration { get; set; } - - public Func>? ConnectionFactory { get; set; } - - internal async Task ConnectAsync(TextWriter log) - { - if (ConnectionFactory != null) - { - return await ConnectionFactory(log); - } - - if (Configuration != null) - { - return await ConnectionMultiplexer.ConnectAsync(Configuration, log); - } - - throw new InvalidOperationException("Either configuration or connection factory must be set."); - } } diff --git a/YDotNet.Server.Redis/RedisConnection.cs b/YDotNet.Server.Redis/RedisConnection.cs new file mode 100644 index 00000000..81e61a77 --- /dev/null +++ b/YDotNet.Server.Redis/RedisConnection.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using StackExchange.Redis; +using YDotNet.Server.Redis.Internal; + +namespace YDotNet.Server.Redis; + +public sealed class RedisConnection : IDisposable +{ + public Task Instance { get; } + + public RedisConnection(IOptions options, ILogger logger) + { + Instance = options.Value.ConnectAsync(new LoggerTextWriter(logger)); + } + + public void Dispose() + { + if (Instance.IsCompletedSuccessfully) + { + Instance.Result.Close(); + } + } +} diff --git a/YDotNet.Server.Redis/RedisDocumentStorage.cs b/YDotNet.Server.Redis/RedisDocumentStorage.cs new file mode 100644 index 00000000..45659612 --- /dev/null +++ b/YDotNet.Server.Redis/RedisDocumentStorage.cs @@ -0,0 +1,60 @@ +using Microsoft.Extensions.Options; +using StackExchange.Redis; +using YDotNet.Server.Storage; + +namespace YDotNet.Server.Redis; + +public sealed class RedisDocumentStorage : IDocumentStorage +{ + private readonly RedisDocumentStorageOptions redisOptions; + private IDatabase? database; + + public RedisDocumentStorage(IOptions redisOptions, RedisConnection redisConnection) + { + this.redisOptions = redisOptions.Value; + + _ = InitializeAsync(redisConnection); + } + + private async Task InitializeAsync(RedisConnection redisConnection) + { + // Use a single task, so that the ordering of registrations does not matter. + var connection = await redisConnection.Instance; + + database = connection.GetDatabase(redisOptions.Database); + } + + public async ValueTask GetDocAsync(string name, + CancellationToken ct = default) + { + if (database == null) + { + return null; + } + + var item = await database.StringGetAsync(Key(name)); + + if (item == RedisValue.Null) + { + return null; + } + + return item; + } + + public async ValueTask StoreDocAsync(string name, byte[] doc, + CancellationToken ct = default) + { + if (database == null) + { + return; + } + + await database.StringSetAsync(Key(name), doc, redisOptions.Expiration?.Invoke(name)); + } + + private string Key(string name) + { + return redisOptions.Prefix + name; + } +} diff --git a/YDotNet.Server.Redis/RedisDocumentStorageOptions.cs b/YDotNet.Server.Redis/RedisDocumentStorageOptions.cs new file mode 100644 index 00000000..9c98c0d4 --- /dev/null +++ b/YDotNet.Server.Redis/RedisDocumentStorageOptions.cs @@ -0,0 +1,10 @@ +namespace YDotNet.Server.Redis; + +public sealed class RedisDocumentStorageOptions +{ + public Func? Expiration { get; set; } + + public int Database { get; set; } + + public string Prefix { get; set; } = "YDotNetDocument_"; +} diff --git a/YDotNet.Server.Redis/RedisOptions.cs b/YDotNet.Server.Redis/RedisOptions.cs new file mode 100644 index 00000000..4c16df3c --- /dev/null +++ b/YDotNet.Server.Redis/RedisOptions.cs @@ -0,0 +1,25 @@ +using StackExchange.Redis; + +namespace YDotNet.Server.Redis; + +public sealed class RedisOptions +{ + public ConfigurationOptions? Configuration { get; set; } + + public Func>? ConnectionFactory { get; set; } + + internal async Task ConnectAsync(TextWriter log) + { + if (ConnectionFactory != null) + { + return await ConnectionFactory(log); + } + + if (Configuration != null) + { + return await ConnectionMultiplexer.ConnectAsync(Configuration, log); + } + + throw new InvalidOperationException("Either configuration or connection factory must be set."); + } +} diff --git a/YDotNet.Server.Redis/ServiceExtensions.cs b/YDotNet.Server.Redis/ServiceExtensions.cs index 15cc4377..2821ab32 100644 --- a/YDotNet.Server.Redis/ServiceExtensions.cs +++ b/YDotNet.Server.Redis/ServiceExtensions.cs @@ -1,17 +1,31 @@ -using Microsoft.Extensions.Hosting; +using YDotNet.Server; using YDotNet.Server.Redis; +using YDotNet.Server.Storage; namespace Microsoft.Extensions.DependencyInjection; public static class ServiceExtensions { - public static YDotnetRegistration AddRedisClustering(this YDotnetRegistration registration, Action configure) + public static YDotnetRegistration AddRedis(this YDotnetRegistration registration, Action? configure = null) { - registration.Services.Configure(configure); - registration.Services.AddSingleton(); + registration.Services.Configure(configure ?? (x => { })); + registration.Services.AddSingleton(); - registration.Services.AddSingleton(x => - x.GetRequiredService()); + return registration; + } + + public static YDotnetRegistration AddRedisClustering(this YDotnetRegistration registration, Action? configure = null) + { + registration.Services.Configure(configure ?? (x => { })); + registration.Services.AddSingleton(); + + return registration; + } + + public static YDotnetRegistration AddRedisStorage(this YDotnetRegistration registration, Action? configure = null) + { + registration.Services.Configure(configure ?? (x => { })); + registration.Services.AddSingleton(); return registration; } diff --git a/YDotNet.Server.Redis/YDotNet.Server.Redis.csproj b/YDotNet.Server.Redis/YDotNet.Server.Redis.csproj index 36a8c68a..a1d898ca 100644 --- a/YDotNet.Server.Redis/YDotNet.Server.Redis.csproj +++ b/YDotNet.Server.Redis/YDotNet.Server.Redis.csproj @@ -8,6 +8,7 @@ + diff --git a/YDotNet.sln b/YDotNet.sln index 37e0d8d0..c886a05d 100644 --- a/YDotNet.sln +++ b/YDotNet.sln @@ -11,13 +11,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YDotNet.Tests.Unit", "Tests EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YDotNet.Server", "YDotNet.Server\YDotNet.Server.csproj", "{F24C69F2-E08A-4C93-A173-3F24FD4A0E7A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YDotNet.Server.Redis", "YDotNet.Server.Redis\YDotNet.Server.Redis.csproj", "{99705343-FB53-47F5-8601-FC89BA565F94}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YDotNet.Server.Redis", "YDotNet.Server.Redis\YDotNet.Server.Redis.csproj", "{99705343-FB53-47F5-8601-FC89BA565F94}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YDotNet.Server.WebSockets", "YDotNet.Server.WebSockets\YDotNet.Server.WebSockets.csproj", "{273EF3C8-55E5-46EC-A985-ED677F41133D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YDotNet.Server.WebSockets", "YDotNet.Server.WebSockets\YDotNet.Server.WebSockets.csproj", "{273EF3C8-55E5-46EC-A985-ED677F41133D}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Demo", "Demo", "{12A368ED-DC07-4A33-85AB-6330C11476ED}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demo", "Demo\Demo.csproj", "{13D76453-95FC-441D-9AC7-E41848C882C4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo", "Demo\Demo.csproj", "{13D76453-95FC-441D-9AC7-E41848C882C4}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution From cf7f48bb0180771975bd27bbe52013f3c8211f53 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 3 Oct 2023 13:09:45 +0200 Subject: [PATCH 014/186] Add support for MongoDB. --- YDotNet.MongoDB/DocumentEntity.cs | 10 +++ YDotNet.MongoDB/MongoDocumentStorage.cs | 86 +++++++++++++++++++ .../MongoDocumentStorageOptions.cs | 10 +++ YDotNet.MongoDB/ServiceExtensions.cs | 22 +++++ YDotNet.MongoDB/YDotNet.MongoDB.csproj | 17 ++++ .../EncoderExtensions.cs | 2 - YDotNet.Server.WebSockets/MessageTypes.cs | 2 - YDotNet.Server/Internal/DocumentContainer.cs | 2 - YDotNet.Server/Storage/IDocumentStorage.cs | 2 - YDotNet.Server/Storage/InMemoryDocStorage.cs | 1 - YDotNet.sln | 6 ++ 11 files changed, 151 insertions(+), 9 deletions(-) create mode 100644 YDotNet.MongoDB/DocumentEntity.cs create mode 100644 YDotNet.MongoDB/MongoDocumentStorage.cs create mode 100644 YDotNet.MongoDB/MongoDocumentStorageOptions.cs create mode 100644 YDotNet.MongoDB/ServiceExtensions.cs create mode 100644 YDotNet.MongoDB/YDotNet.MongoDB.csproj diff --git a/YDotNet.MongoDB/DocumentEntity.cs b/YDotNet.MongoDB/DocumentEntity.cs new file mode 100644 index 00000000..ad0ffd04 --- /dev/null +++ b/YDotNet.MongoDB/DocumentEntity.cs @@ -0,0 +1,10 @@ +namespace YDotNet.MongoDB; + +internal sealed class DocumentEntity +{ + required public string Id { get; set; } + + required public byte[] Data { get; set; } + + public DateTime? Expiration { get; private set; } +} diff --git a/YDotNet.MongoDB/MongoDocumentStorage.cs b/YDotNet.MongoDB/MongoDocumentStorage.cs new file mode 100644 index 00000000..144adbcd --- /dev/null +++ b/YDotNet.MongoDB/MongoDocumentStorage.cs @@ -0,0 +1,86 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using MongoDB.Driver; +using YDotNet.Server.Storage; + +namespace YDotNet.MongoDB; + +public sealed class MongoDocumentStorage : IDocumentStorage, IHostedService +{ + private readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true }; + private readonly MongoDocumentStorageOptions options; + private readonly IMongoClient mongoClient; + private IMongoCollection? collection; + + public Func Clock = () => DateTime.UtcNow; + + public MongoDocumentStorage(IMongoClient mongoClient, IOptions options) + { + this.options = options.Value; + this.mongoClient = mongoClient; + } + + public async Task StartAsync( + CancellationToken cancellationToken) + { + var database = mongoClient.GetDatabase(options.DatabaseName); + + collection = database.GetCollection(options.CollectionName); + + await collection.Indexes.CreateOneAsync( + new CreateIndexModel( + Builders.IndexKeys.Ascending(x => x.Expiration), + new CreateIndexOptions + { + ExpireAfter = TimeSpan.Zero + }), + cancellationToken: cancellationToken); + } + + public Task StopAsync( + CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public async ValueTask GetDocAsync(string name, + CancellationToken ct = default) + { + if (collection == null) + { + return null; + } + + var document = await collection.Find(x => x.Id == name).FirstOrDefaultAsync(ct); + + return document?.Data; + } + + public async ValueTask StoreDocAsync(string name, byte[] doc, + CancellationToken ct = default) + { + if (collection == null) + { + return; + } + + await collection.UpdateOneAsync(x => x.Id == name, + Builders.Update + .Set(x => x.Data, doc) + .Set(x => x.Expiration, GetExpiration(name)), + Upsert, + ct); + } + + private DateTime? GetExpiration(string name) + { + var relative = options.Expiration?.Invoke(name); + + if (relative == null) + { + return null; + } + + return Clock() + relative.Value; + } +} diff --git a/YDotNet.MongoDB/MongoDocumentStorageOptions.cs b/YDotNet.MongoDB/MongoDocumentStorageOptions.cs new file mode 100644 index 00000000..9f5a9197 --- /dev/null +++ b/YDotNet.MongoDB/MongoDocumentStorageOptions.cs @@ -0,0 +1,10 @@ +namespace YDotNet.MongoDB; + +public sealed class MongoDocumentStorageOptions +{ + public Func? Expiration { get; set; } + + public string DatabaseName { get; set; } = "YDotNet"; + + public string CollectionName { get; set; } = "YDotNet"; +} diff --git a/YDotNet.MongoDB/ServiceExtensions.cs b/YDotNet.MongoDB/ServiceExtensions.cs new file mode 100644 index 00000000..ca08d916 --- /dev/null +++ b/YDotNet.MongoDB/ServiceExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using YDotNet.Server.Storage; + +namespace YDotNet.MongoDB; + +public static class ServiceExtensions +{ + public static YDotnetRegistration AddMongoStorage(this YDotnetRegistration registration, Action? configure = null) + { + registration.Services.Configure(configure ?? (x => { })); + registration.Services.AddSingleton(); + + registration.Services.AddSingleton( + c => c.GetRequiredService()); + + registration.Services.AddSingleton( + c => c.GetRequiredService()); + + return registration; + } +} diff --git a/YDotNet.MongoDB/YDotNet.MongoDB.csproj b/YDotNet.MongoDB/YDotNet.MongoDB.csproj new file mode 100644 index 00000000..8f096c2d --- /dev/null +++ b/YDotNet.MongoDB/YDotNet.MongoDB.csproj @@ -0,0 +1,17 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + diff --git a/YDotNet.Server.WebSockets/EncoderExtensions.cs b/YDotNet.Server.WebSockets/EncoderExtensions.cs index cd75f08c..5be15751 100644 --- a/YDotNet.Server.WebSockets/EncoderExtensions.cs +++ b/YDotNet.Server.WebSockets/EncoderExtensions.cs @@ -1,5 +1,3 @@ -using YDotNet.Protocol; - namespace YDotNet.Server.WebSockets; public static class EncoderExtensions diff --git a/YDotNet.Server.WebSockets/MessageTypes.cs b/YDotNet.Server.WebSockets/MessageTypes.cs index e976239e..d56696e8 100644 --- a/YDotNet.Server.WebSockets/MessageTypes.cs +++ b/YDotNet.Server.WebSockets/MessageTypes.cs @@ -1,5 +1,3 @@ -using Microsoft.VisualBasic; - namespace YDotNet.Server.WebSockets; public static class MessageTypes diff --git a/YDotNet.Server/Internal/DocumentContainer.cs b/YDotNet.Server/Internal/DocumentContainer.cs index 11072f67..021dcf7a 100644 --- a/YDotNet.Server/Internal/DocumentContainer.cs +++ b/YDotNet.Server/Internal/DocumentContainer.cs @@ -1,7 +1,5 @@ using YDotNet.Document; using YDotNet.Server.Storage; -using YDotNet.Server.Internal; -using System.Reflection.Metadata; namespace YDotNet.Server.Internal; diff --git a/YDotNet.Server/Storage/IDocumentStorage.cs b/YDotNet.Server/Storage/IDocumentStorage.cs index d77900a7..bf4493c6 100644 --- a/YDotNet.Server/Storage/IDocumentStorage.cs +++ b/YDotNet.Server/Storage/IDocumentStorage.cs @@ -1,5 +1,3 @@ -using YDotNet.Document; - namespace YDotNet.Server.Storage; public interface IDocumentStorage diff --git a/YDotNet.Server/Storage/InMemoryDocStorage.cs b/YDotNet.Server/Storage/InMemoryDocStorage.cs index e0e6d26f..0e09cbee 100644 --- a/YDotNet.Server/Storage/InMemoryDocStorage.cs +++ b/YDotNet.Server/Storage/InMemoryDocStorage.cs @@ -1,5 +1,4 @@ using System.Collections.Concurrent; -using YDotNet.Document; namespace YDotNet.Server.Storage; diff --git a/YDotNet.sln b/YDotNet.sln index c886a05d..de559779 100644 --- a/YDotNet.sln +++ b/YDotNet.sln @@ -19,6 +19,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Demo", "Demo", "{12A368ED-D EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo", "Demo\Demo.csproj", "{13D76453-95FC-441D-9AC7-E41848C882C4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YDotNet.MongoDB", "YDotNet.MongoDB\YDotNet.MongoDB.csproj", "{DAA60ED8-B8E3-4D84-85D4-F07DF597281C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -49,6 +51,10 @@ Global {13D76453-95FC-441D-9AC7-E41848C882C4}.Debug|Any CPU.Build.0 = Debug|Any CPU {13D76453-95FC-441D-9AC7-E41848C882C4}.Release|Any CPU.ActiveCfg = Release|Any CPU {13D76453-95FC-441D-9AC7-E41848C882C4}.Release|Any CPU.Build.0 = Release|Any CPU + {DAA60ED8-B8E3-4D84-85D4-F07DF597281C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DAA60ED8-B8E3-4D84-85D4-F07DF597281C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DAA60ED8-B8E3-4D84-85D4-F07DF597281C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DAA60ED8-B8E3-4D84-85D4-F07DF597281C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From c17df4c78613bbb10db8de263488fe6599dce289 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 3 Oct 2023 13:39:52 +0200 Subject: [PATCH 015/186] Fix project name. --- {YDotNet.MongoDB => YDotNet.Server.MongoDB}/DocumentEntity.cs | 2 +- .../MongoDocumentStorage.cs | 2 +- .../MongoDocumentStorageOptions.cs | 2 +- .../ServiceExtensions.cs | 2 +- .../YDotNet.Server.MongoDB.csproj | 0 YDotNet.sln | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) rename {YDotNet.MongoDB => YDotNet.Server.MongoDB}/DocumentEntity.cs (84%) rename {YDotNet.MongoDB => YDotNet.Server.MongoDB}/MongoDocumentStorage.cs (98%) rename {YDotNet.MongoDB => YDotNet.Server.MongoDB}/MongoDocumentStorageOptions.cs (87%) rename {YDotNet.MongoDB => YDotNet.Server.MongoDB}/ServiceExtensions.cs (95%) rename YDotNet.MongoDB/YDotNet.MongoDB.csproj => YDotNet.Server.MongoDB/YDotNet.Server.MongoDB.csproj (100%) diff --git a/YDotNet.MongoDB/DocumentEntity.cs b/YDotNet.Server.MongoDB/DocumentEntity.cs similarity index 84% rename from YDotNet.MongoDB/DocumentEntity.cs rename to YDotNet.Server.MongoDB/DocumentEntity.cs index ad0ffd04..327b5fe9 100644 --- a/YDotNet.MongoDB/DocumentEntity.cs +++ b/YDotNet.Server.MongoDB/DocumentEntity.cs @@ -1,4 +1,4 @@ -namespace YDotNet.MongoDB; +namespace YDotNet.Server.MongoDB; internal sealed class DocumentEntity { diff --git a/YDotNet.MongoDB/MongoDocumentStorage.cs b/YDotNet.Server.MongoDB/MongoDocumentStorage.cs similarity index 98% rename from YDotNet.MongoDB/MongoDocumentStorage.cs rename to YDotNet.Server.MongoDB/MongoDocumentStorage.cs index 144adbcd..007e8622 100644 --- a/YDotNet.MongoDB/MongoDocumentStorage.cs +++ b/YDotNet.Server.MongoDB/MongoDocumentStorage.cs @@ -3,7 +3,7 @@ using MongoDB.Driver; using YDotNet.Server.Storage; -namespace YDotNet.MongoDB; +namespace YDotNet.Server.MongoDB; public sealed class MongoDocumentStorage : IDocumentStorage, IHostedService { diff --git a/YDotNet.MongoDB/MongoDocumentStorageOptions.cs b/YDotNet.Server.MongoDB/MongoDocumentStorageOptions.cs similarity index 87% rename from YDotNet.MongoDB/MongoDocumentStorageOptions.cs rename to YDotNet.Server.MongoDB/MongoDocumentStorageOptions.cs index 9f5a9197..e4fe5073 100644 --- a/YDotNet.MongoDB/MongoDocumentStorageOptions.cs +++ b/YDotNet.Server.MongoDB/MongoDocumentStorageOptions.cs @@ -1,4 +1,4 @@ -namespace YDotNet.MongoDB; +namespace YDotNet.Server.MongoDB; public sealed class MongoDocumentStorageOptions { diff --git a/YDotNet.MongoDB/ServiceExtensions.cs b/YDotNet.Server.MongoDB/ServiceExtensions.cs similarity index 95% rename from YDotNet.MongoDB/ServiceExtensions.cs rename to YDotNet.Server.MongoDB/ServiceExtensions.cs index ca08d916..3ce9f6cb 100644 --- a/YDotNet.MongoDB/ServiceExtensions.cs +++ b/YDotNet.Server.MongoDB/ServiceExtensions.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.Hosting; using YDotNet.Server.Storage; -namespace YDotNet.MongoDB; +namespace YDotNet.Server.MongoDB; public static class ServiceExtensions { diff --git a/YDotNet.MongoDB/YDotNet.MongoDB.csproj b/YDotNet.Server.MongoDB/YDotNet.Server.MongoDB.csproj similarity index 100% rename from YDotNet.MongoDB/YDotNet.MongoDB.csproj rename to YDotNet.Server.MongoDB/YDotNet.Server.MongoDB.csproj diff --git a/YDotNet.sln b/YDotNet.sln index de559779..39184e7c 100644 --- a/YDotNet.sln +++ b/YDotNet.sln @@ -19,7 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Demo", "Demo", "{12A368ED-D EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo", "Demo\Demo.csproj", "{13D76453-95FC-441D-9AC7-E41848C882C4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YDotNet.MongoDB", "YDotNet.MongoDB\YDotNet.MongoDB.csproj", "{DAA60ED8-B8E3-4D84-85D4-F07DF597281C}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YDotNet.Server.MongoDB", "YDotNet.Server.MongoDB\YDotNet.Server.MongoDB.csproj", "{DAA60ED8-B8E3-4D84-85D4-F07DF597281C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution From c1be45a9e9add6d24bf2549f99593ce534fe839b Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 3 Oct 2023 13:49:39 +0200 Subject: [PATCH 016/186] Extension project. --- YDotNet.Extensions/YDotNet.Extensions.csproj | 13 +++ YDotNet.Extensions/YDotNetExtensions.cs | 86 ++++++++++++++++++++ YDotNet.sln | 8 +- 3 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 YDotNet.Extensions/YDotNet.Extensions.csproj create mode 100644 YDotNet.Extensions/YDotNetExtensions.cs diff --git a/YDotNet.Extensions/YDotNet.Extensions.csproj b/YDotNet.Extensions/YDotNet.Extensions.csproj new file mode 100644 index 00000000..153967cc --- /dev/null +++ b/YDotNet.Extensions/YDotNet.Extensions.csproj @@ -0,0 +1,13 @@ + + + + net7.0 + enable + enable + + + + + + + diff --git a/YDotNet.Extensions/YDotNetExtensions.cs b/YDotNet.Extensions/YDotNetExtensions.cs new file mode 100644 index 00000000..6171db2c --- /dev/null +++ b/YDotNet.Extensions/YDotNetExtensions.cs @@ -0,0 +1,86 @@ +using System.Text.Json; +using YDotNet.Document; +using YDotNet.Document.Cells; +using YDotNet.Document.Transactions; +using YDotNet.Document.Types.Maps; +using Array = YDotNet.Document.Types.Arrays.Array; + +namespace YDotNet.Extensions; + +public static class YDotNetExtensions +{ + public static T? To(this Output output, Doc doc) + { + var jsonStream = new MemoryStream(); + var jsonWriter = new Utf8JsonWriter(jsonStream); + + using var transaction = doc.ReadTransaction() + ?? throw new InvalidOperationException("Failed to open transaction."); + + WriteValue(output, jsonWriter, transaction); + + static void WriteMap(Map map, Utf8JsonWriter jsonWriter, Transaction transaction) + { + var properties = map.Iterate(transaction) + ?? throw new InvalidOperationException("Failed to iterate object."); + + foreach (var property in properties) + { + WriteProperty(property, jsonWriter, transaction); + } + } + + static void WriteArray(Array array, Utf8JsonWriter jsonWriter, Transaction transaction) + { + var items = array.Iterate(transaction) + ?? throw new InvalidOperationException("Failed to iterate array."); + + foreach (var item in items) + { + WriteValue(item, jsonWriter, transaction); + } + } + + static void WriteProperty(MapEntry property, Utf8JsonWriter jsonWriter, Transaction transaction) + { + jsonWriter.WritePropertyName(property.Key); + + WriteValue(property.Value, jsonWriter, transaction); + } + + static void WriteValue(Output output, Utf8JsonWriter jsonWriter, Transaction transaction) + { + if (output.String != null) + { + jsonWriter.WriteStringValue(output.String); + } + else if (output.Boolean != null) + { + jsonWriter.WriteBooleanValue(output.Boolean.Value); + } + else if (output.Double != null) + { + jsonWriter.WriteNumberValue(output.Double.Value); + } + else if (output.Null) + { + jsonWriter.WriteNullValue(); + } + else if (output.Map is Map map) + { + WriteMap(map, jsonWriter, transaction); + } + else if (output.Array is Array array) + { + WriteArray(array, jsonWriter, transaction); + } + + throw new InvalidOperationException("Unsupported data type."); + } + + jsonWriter.Flush(); + jsonStream.Position = 0; + + return JsonSerializer.Deserialize(jsonStream); + } +} diff --git a/YDotNet.sln b/YDotNet.sln index 39184e7c..579206f7 100644 --- a/YDotNet.sln +++ b/YDotNet.sln @@ -19,7 +19,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Demo", "Demo", "{12A368ED-D EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo", "Demo\Demo.csproj", "{13D76453-95FC-441D-9AC7-E41848C882C4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YDotNet.Server.MongoDB", "YDotNet.Server.MongoDB\YDotNet.Server.MongoDB.csproj", "{DAA60ED8-B8E3-4D84-85D4-F07DF597281C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YDotNet.Server.MongoDB", "YDotNet.Server.MongoDB\YDotNet.Server.MongoDB.csproj", "{DAA60ED8-B8E3-4D84-85D4-F07DF597281C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YDotNet.Extensions", "YDotNet.Extensions\YDotNet.Extensions.csproj", "{47CF8F93-FCB3-42EF-B981-464A5FE10234}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -55,6 +57,10 @@ Global {DAA60ED8-B8E3-4D84-85D4-F07DF597281C}.Debug|Any CPU.Build.0 = Debug|Any CPU {DAA60ED8-B8E3-4D84-85D4-F07DF597281C}.Release|Any CPU.ActiveCfg = Release|Any CPU {DAA60ED8-B8E3-4D84-85D4-F07DF597281C}.Release|Any CPU.Build.0 = Release|Any CPU + {47CF8F93-FCB3-42EF-B981-464A5FE10234}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {47CF8F93-FCB3-42EF-B981-464A5FE10234}.Debug|Any CPU.Build.0 = Debug|Any CPU + {47CF8F93-FCB3-42EF-B981-464A5FE10234}.Release|Any CPU.ActiveCfg = Release|Any CPU + {47CF8F93-FCB3-42EF-B981-464A5FE10234}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 94d1af8e24fcde1db0dab62fcce07d8967c7d143 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 3 Oct 2023 20:39:46 +0200 Subject: [PATCH 017/186] Setup to test more providers and authentication. --- Demo/Demo.csproj | 3 ++ Demo/Program.cs | 52 +++++++++++++++++-- Demo/appsettings.Development.json | 21 +++++++- Demo/appsettings.json | 18 ++++++- .../EncoderExtensions.cs | 9 ++++ .../ServiceExtensions.cs | 2 +- YDotNet.Server.WebSockets/WebSocketDecoder.cs | 2 +- .../YDotNetSocketMiddleware.cs | 31 ++++++++++- .../YDotNetWebSocketOptions.cs | 10 ++++ ...ntRequestContext.cs => DocumentContext.cs} | 0 10 files changed, 139 insertions(+), 9 deletions(-) create mode 100644 YDotNet.Server.WebSockets/YDotNetWebSocketOptions.cs rename YDotNet.Server/{DocumentRequestContext.cs => DocumentContext.cs} (100%) diff --git a/Demo/Demo.csproj b/Demo/Demo.csproj index 0a87d6d1..dc5292b8 100644 --- a/Demo/Demo.csproj +++ b/Demo/Demo.csproj @@ -7,7 +7,10 @@ + + + diff --git a/Demo/Program.cs b/Demo/Program.cs index 348afac4..b215269b 100644 --- a/Demo/Program.cs +++ b/Demo/Program.cs @@ -1,3 +1,7 @@ +using MongoDB.Driver; +using StackExchange.Redis; +using YDotNet.Server.MongoDB; + namespace Demo; public class Program @@ -7,9 +11,51 @@ public static void Main(string[] args) var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorPages(); - builder.Services.AddYDotNet() - .AddCallback() - .AddWebSockets(); + + var yDotNet = + builder.Services.AddYDotNet() + .AddCallback() + .AddWebSockets(); + + switch (builder.Configuration["Storage:Type"]) + { + case "MongoDb": + { + yDotNet.Services.AddSingleton( + _ => new MongoClient(builder.Configuration["Storage:MongoDb:ConnectionString"])); + + yDotNet.AddMongoStorage(options => + { + options.DatabaseName = builder.Configuration["Storage:MongoDb:DatabaseName"]!; + }); + + break; + } + + case "Redis": + { + yDotNet.AddRedisStorage(); + yDotNet.AddRedis(options => + { + options.Configuration = + ConfigurationOptions.Parse( + builder.Configuration["Clustering:Redis:ConnectionString"]!); + }); + + break; + } + } + + if (builder.Configuration["Clustering:Type"] == "Redis") + { + yDotNet.AddRedisClustering(); + yDotNet.AddRedis(options => + { + options.Configuration = + ConfigurationOptions.Parse( + builder.Configuration["Clustering:Redis:ConnectionString"]!); + }); + } var app = builder.Build(); diff --git a/Demo/appsettings.Development.json b/Demo/appsettings.Development.json index 770d3e93..bd2a9fdc 100644 --- a/Demo/appsettings.Development.json +++ b/Demo/appsettings.Development.json @@ -1,9 +1,26 @@ { - "DetailedErrors": true, "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Microsoft.AspNetCore": "Warning", + "YDotNet": "Debug" + } + }, + "AllowedHosts": "*", + "Storage": { + "Type": "Redis", + "MongoDB": { + "ConnectionString": "mongodb://localhost:27017", + "Database": "YDotNet" + }, + "Redis": { + "ConnectionString": "localhost:6379" + } + }, + "Clustering": { + "Type": "Redis", + "Redis": { + "ConnectionString": "localhost:6379" } } } diff --git a/Demo/appsettings.json b/Demo/appsettings.json index 6eacb541..5b0ea871 100644 --- a/Demo/appsettings.json +++ b/Demo/appsettings.json @@ -6,5 +6,21 @@ "YDotNet": "Debug" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "Storage": { + "Type": "InMemory", + "MongoDB": { + "ConnectionString": "mongodb://localhost:27017", + "Database": "YDotNet" + }, + "Redis": { + "ConnectionString": "localhost:6379" + } + }, + "Clustering": { + "Type": "None", + "Redis": { + "ConnectionString": "localhost:6379" + } + } } diff --git a/YDotNet.Server.WebSockets/EncoderExtensions.cs b/YDotNet.Server.WebSockets/EncoderExtensions.cs index 5be15751..457c2bfd 100644 --- a/YDotNet.Server.WebSockets/EncoderExtensions.cs +++ b/YDotNet.Server.WebSockets/EncoderExtensions.cs @@ -37,4 +37,13 @@ public static async Task WriteAwarenessAsync(this WebSocketEncoder encoder, long await encoder.WriteVarStringAsync(state ?? string.Empty, ct); await encoder.EndMessageAsync(ct); } + + public static async Task WriteAuthErrorAsync(this WebSocketEncoder encoder, string reason, + CancellationToken ct) + { + await encoder.WriteVarUintAsync(MessageTypes.TypeAuth, ct); + await encoder.WriteVarUintAsync(0, ct); + await encoder.WriteVarStringAsync(reason, ct); + await encoder.EndMessageAsync(ct); + } } diff --git a/YDotNet.Server.WebSockets/ServiceExtensions.cs b/YDotNet.Server.WebSockets/ServiceExtensions.cs index a63d55ab..0b9b4c99 100644 --- a/YDotNet.Server.WebSockets/ServiceExtensions.cs +++ b/YDotNet.Server.WebSockets/ServiceExtensions.cs @@ -7,7 +7,7 @@ namespace Microsoft.Extensions.DependencyInjection; public static class ServiceExtensions { - public static YDotnetRegistration AddWebSockets(this YDotnetRegistration registration) + public static YDotnetRegistration AddWebSockets(this YDotnetRegistration registration, Action? configure = null) { registration.Services.AddSingleton(); diff --git a/YDotNet.Server.WebSockets/WebSocketDecoder.cs b/YDotNet.Server.WebSockets/WebSocketDecoder.cs index 19f1f2a4..035850f1 100644 --- a/YDotNet.Server.WebSockets/WebSocketDecoder.cs +++ b/YDotNet.Server.WebSockets/WebSocketDecoder.cs @@ -48,7 +48,7 @@ protected override async ValueTask ReadBytesAsync(Memory memory, var bytesToCopy = Math.Min(memory.Length, bytesLeft); buffer.AsMemory(bufferIndex, bytesToCopy).CopyTo(memory); - memory = memory.Slice(bytesToCopy); + memory = memory[bytesToCopy..]; bufferIndex += bytesToCopy; } diff --git a/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs b/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs index 94620622..fc7ab960 100644 --- a/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs +++ b/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs @@ -1,17 +1,21 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using System.Collections.Concurrent; +using System.Net.WebSockets; namespace YDotNet.Server.WebSockets; public sealed class YDotNetSocketMiddleware : IDocumentCallback { private readonly ConcurrentDictionary> statesPerDocumentName = new(); + private readonly YDotNetWebSocketOptions options; private readonly ILogger logger; private IDocumentManager? documentManager; - public YDotNetSocketMiddleware(ILogger logger) + public YDotNetSocketMiddleware(IOptions options, ILogger logger) { + this.options = options.Value; this.logger = logger; } @@ -96,6 +100,8 @@ public async Task InvokeAsync(HttpContext httpContext) documentStates.Add(state); } + await AuthenticateAsync(httpContext, state); + try { while (state.Decoder.CanRead) @@ -119,6 +125,11 @@ public async Task InvokeAsync(HttpContext httpContext) } catch (OperationCanceledException) { + // Usually throw when the client stops the connection. + } + catch (WebSocketException) + { + // Usually throw when the client stops the connection. } finally { @@ -137,6 +148,24 @@ public async Task InvokeAsync(HttpContext httpContext) logger.LogDebug("Websocket connection to {document} closed.", documentName); } + private async Task AuthenticateAsync(HttpContext httpContext, ClientState state) + { + if (options.OnAuthenticateAsync == null) + { + return; + } + + try + { + await options.OnAuthenticateAsync(httpContext, state.DocumentContext); + } + catch (Exception ex) + { + await state.Encoder.WriteAuthErrorAsync(ex.Message, httpContext.RequestAborted); + throw; + } + } + private async Task HandleSyncAsync(ClientState state, CancellationToken ct) { diff --git a/YDotNet.Server.WebSockets/YDotNetWebSocketOptions.cs b/YDotNet.Server.WebSockets/YDotNetWebSocketOptions.cs new file mode 100644 index 00000000..5c24b0d8 --- /dev/null +++ b/YDotNet.Server.WebSockets/YDotNetWebSocketOptions.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Http; + +namespace YDotNet.Server.WebSockets; + +public delegate Task AuthDelegate(HttpContext httpContext, DocumentContext context); + +public sealed class YDotNetWebSocketOptions +{ + public AuthDelegate? OnAuthenticateAsync { get; set; } +} diff --git a/YDotNet.Server/DocumentRequestContext.cs b/YDotNet.Server/DocumentContext.cs similarity index 100% rename from YDotNet.Server/DocumentRequestContext.cs rename to YDotNet.Server/DocumentContext.cs From 6ae497892d12199a0bb38848b518a04a596f0a35 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 3 Oct 2023 23:10:15 +0200 Subject: [PATCH 018/186] Several fixed and simplifications. --- YDotNet.Server.Redis/Messages.cs | 60 ++++----- .../RedisClusteringCallback.cs | 120 +++++++++++------- .../ServiceExtensions.cs | 1 + YDotNet.Server.WebSockets/WebSocketDecoder.cs | 2 +- .../YDotNetSocketMiddleware.cs | 11 +- YDotNet.Server/DefaultDocumentManager.cs | 48 +++---- YDotNet.Server/IDocumentManager.cs | 7 +- YDotNet.Server/Internal/Extensions.cs | 17 +++ 8 files changed, 151 insertions(+), 115 deletions(-) create mode 100644 YDotNet.Server/Internal/Extensions.cs diff --git a/YDotNet.Server.Redis/Messages.cs b/YDotNet.Server.Redis/Messages.cs index 3fcb5271..c82887b5 100644 --- a/YDotNet.Server.Redis/Messages.cs +++ b/YDotNet.Server.Redis/Messages.cs @@ -1,68 +1,56 @@ using ProtoBuf; using YDotNet.Server.Redis.Internal; +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + namespace YDotNet.Server.Redis; +[ProtoContract] +public enum MessageType +{ + ClientPinged, + ClientDisconnected, + AwarenessRequested, + Update, + SyncStep1, + SyncStep2, +} + [ProtoContract] public sealed class Message : ICanEstimateSize { private static readonly int GuidLength = Guid.Empty.ToString().Length; [ProtoMember(1)] - required public Guid SenderId { get; init; } + public MessageType Type { get; set; } + + [ProtoMember(1)] + public Guid SenderId { get; set; } [ProtoMember(2)] - required public string DocumentName { get; init; } + public string DocumentName { get; set; } [ProtoMember(3)] - required public long ClientId { get; init; } + public long ClientId { get; set; } [ProtoMember(4)] - public ClientPingMessage? ClientPinged { get; set; } + public long ClientClock { get; set; } [ProtoMember(5)] - public ClientDisconnectMessage? ClientDisconnected { get; set; } + public string? ClientState { get; set; } [ProtoMember(6)] - public DocumentChangeMessage? DocumentChanged { get; set; } + public byte[]? Data { get; set; } public int EstimateSize() { var size = GuidLength + sizeof(long) + + sizeof(long) + DocumentName.Length + - ClientPinged?.EstimatedSize() ?? 0 + - ClientDisconnected?.EstimatedSize() ?? 0 + - DocumentChanged?.EstimatedSize() ?? 0; + Data?.Length ?? 0; return size; } } - -[ProtoContract] -public sealed class ClientDisconnectMessage -{ - public int EstimatedSize() => 0; -} - -[ProtoContract] -public sealed class DocumentChangeMessage -{ - [ProtoMember(1)] - required public byte[] DocumentDiff { get; init; } - - public int EstimatedSize() => sizeof(long) + DocumentDiff?.Length ?? 0; -} - -[ProtoContract] -public sealed class ClientPingMessage -{ - [ProtoMember(1)] - required public long ClientClock { get; init; } - - [ProtoMember(2)] - required public string? ClientState { get; init; } - - public int EstimatedSize() => sizeof(long) + ClientState?.Length ?? 0; -} diff --git a/YDotNet.Server.Redis/RedisClusteringCallback.cs b/YDotNet.Server.Redis/RedisClusteringCallback.cs index 3071ff8b..ee714251 100644 --- a/YDotNet.Server.Redis/RedisClusteringCallback.cs +++ b/YDotNet.Server.Redis/RedisClusteringCallback.cs @@ -78,65 +78,97 @@ private async Task HandleMessage(RedisValue value) Metadata = senderId }; - if (message.DocumentChanged is DocumentChangeMessage changed) + switch (message.Type) { - await documentManager.ApplyUpdateAsync(context, changed.DocumentDiff); + case MessageType.ClientPinged: + await documentManager.PingAsync(context, message.ClientClock, message.ClientState); + break; + case MessageType.ClientDisconnected: + await documentManager.DisconnectAsync(context); + break; + case MessageType.Update when message.Data != null: + await documentManager.ApplyUpdateAsync(context, message.Data); + break; + case MessageType.SyncStep2 when message.Data != null: + await documentManager.ApplyUpdateAsync(context, message.Data); + break; + case MessageType.SyncStep1 when message.Data != null: + await SendSync2Async(context, message.Data); + break; + case MessageType.AwarenessRequested: + foreach (var (id, user) in await documentManager.GetAwarenessAsync(context)) + { + var userContext = context with { ClientId = id }; + + await SendAwarenessAsync(userContext, user.ClientState, user.ClientClock); + } + break; } - - if (message.ClientPinged is ClientPingMessage pinged) - { - await documentManager.PingAsync(context, pinged.ClientClock, pinged.ClientState); - } - - if (message.ClientDisconnected is not null) - { - await documentManager.DisconnectAsync(context); - } - } + } } - public ValueTask OnAwarenessUpdatedAsync(ClientAwarenessEvent @event) + public async ValueTask OnDocumentLoadedAsync(DocumentLoadEvent @event) { - var message = new Message - { - ClientId = @event.Context.ClientId, - ClientPinged = new ClientPingMessage - { - ClientClock = @event.ClientClock, - ClientState = @event.ClientState, - }, - DocumentName = @event.Context.DocumentName, - SenderId = senderId, - }; + await SendAwarenessRequest(@event.Context); + await SendSync1Async(@event.Context); + } - return subscriberQueue.EnqueueAsync(message, default); + public async ValueTask OnAwarenessUpdatedAsync(ClientAwarenessEvent @event) + { + await SendAwarenessAsync(@event.Context, @event.ClientState, @event.ClientClock); } public ValueTask OnClientDisconnectedAsync(ClientDisconnectedEvent @event) { - var message = new Message - { - ClientId = @event.Context.ClientId, - ClientDisconnected = new ClientDisconnectMessage(), - DocumentName = @event.Context.DocumentName, - SenderId = senderId, - }; + var m = new Message { Type = MessageType.ClientDisconnected }; - return subscriberQueue.EnqueueAsync(message, default); + return EnqueueAsync(m, @event.Context); } public ValueTask OnDocumentChangedAsync(DocumentChangedEvent @event) { - var message = new Message - { - ClientId = @event.Context.ClientId, - DocumentName = @event.Context.DocumentName, - DocumentChanged = new DocumentChangeMessage - { - DocumentDiff = @event.Diff - }, - SenderId = senderId - }; + var m = new Message { Type = MessageType.Update, Data = @event.Diff }; + + return EnqueueAsync(m, @event.Context); + } + + private ValueTask SendAwarenessAsync(DocumentContext context, string? state, long clock) + { + var m = new Message { Type = MessageType.ClientPinged, ClientState = state, ClientClock = clock }; + + return EnqueueAsync(m, context); + } + + private async ValueTask SendSync1Async(DocumentContext context) + { + var state = await documentManager!.GetStateAsync(context); + + var m = new Message { Type = MessageType.SyncStep1, Data = state }; + + await EnqueueAsync(m, context); + } + + private async ValueTask SendSync2Async(DocumentContext context, byte[] stateVector) + { + var state = await documentManager!.GetStateAsUpdateAsync(context, stateVector); + + var m = new Message { Type = MessageType.SyncStep2, Data = state }; + + await EnqueueAsync(m, context); + } + + private ValueTask SendAwarenessRequest(DocumentContext context) + { + var m = new Message { Type = MessageType.AwarenessRequested }; + + return EnqueueAsync(m, context); + } + + private ValueTask EnqueueAsync(Message message, DocumentContext context) + { + message.ClientId = context.ClientId; + message.DocumentName = context.DocumentName; + message.SenderId = senderId; return subscriberQueue.EnqueueAsync(message, default); } diff --git a/YDotNet.Server.WebSockets/ServiceExtensions.cs b/YDotNet.Server.WebSockets/ServiceExtensions.cs index 0b9b4c99..d04f8859 100644 --- a/YDotNet.Server.WebSockets/ServiceExtensions.cs +++ b/YDotNet.Server.WebSockets/ServiceExtensions.cs @@ -9,6 +9,7 @@ public static class ServiceExtensions { public static YDotnetRegistration AddWebSockets(this YDotnetRegistration registration, Action? configure = null) { + registration.Services.Configure(configure ?? (x => { })); registration.Services.AddSingleton(); registration.Services.AddSingleton(x => diff --git a/YDotNet.Server.WebSockets/WebSocketDecoder.cs b/YDotNet.Server.WebSockets/WebSocketDecoder.cs index 035850f1..9027b42d 100644 --- a/YDotNet.Server.WebSockets/WebSocketDecoder.cs +++ b/YDotNet.Server.WebSockets/WebSocketDecoder.cs @@ -75,7 +75,7 @@ private void EnsureSocketIsOpen() { if (webSocket.State != WebSocketState.Open && CanRead) { - throw new InvalidOperationException("Socket is already closed."); + throw new WebSocketException("Socket is already closed."); } } } diff --git a/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs b/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs index fc7ab960..5c5b82af 100644 --- a/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs +++ b/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs @@ -181,14 +181,15 @@ await state.WriteLockedAsync(true, async (encoder, context, state, ct) => switch (syncType) { case MessageTypes.SyncStep1: - var stateVector = await state.Decoder.ReadVarUint8ArrayAsync(ct); + var clientState = await state.Decoder.ReadVarUint8ArrayAsync(ct); - var (update, serverState) = await documentManager!.GetMissingChangesAsync(state.DocumentContext, stateVector, ct); + var serverState = await documentManager!.GetStateAsync(state.DocumentContext, ct); + var serverUpdate = await documentManager!.GetStateAsUpdateAsync(state.DocumentContext, clientState, ct); // We mark the sync state as false again to handle multiple sync steps. state.IsSynced = false; - await encoder.WriteSyncStep2Async(update, ct); + await encoder.WriteSyncStep2Async(serverUpdate, ct); await encoder.WriteSyncStep1Async(serverState, ct); await SendPendingUpdatesAsync(encoder, state, ct); @@ -221,7 +222,7 @@ private static async Task SendPendingUpdatesAsync(WebSocketEncoder encoder, Clie private async Task SendAwarenessAsync(WebSocketEncoder encoder, ClientState state, CancellationToken ct) { - var users = await documentManager!.GetAwarenessAsync(state.DocumentName, ct); + var users = await documentManager!.GetAwarenessAsync(state.DocumentContext, ct); if (users.Count == 0) { @@ -231,7 +232,7 @@ private async Task SendAwarenessAsync(WebSocketEncoder encoder, ClientState stat await encoder.WriteVarUintAsync(MessageTypes.TypeAwareness, ct); await encoder.WriteVarUintAsync(users.Count, ct); - foreach (var (clientId, user) in await documentManager.GetAwarenessAsync(state.DocumentName, ct)) + foreach (var (clientId, user) in users) { await encoder.WriteAwarenessAsync(clientId, user.ClientClock, user.ClientState, ct); } diff --git a/YDotNet.Server/DefaultDocumentManager.cs b/YDotNet.Server/DefaultDocumentManager.cs index 3e442925..6ab18f8d 100644 --- a/YDotNet.Server/DefaultDocumentManager.cs +++ b/YDotNet.Server/DefaultDocumentManager.cs @@ -41,7 +41,7 @@ public async Task StopAsync( await containers.DisposeAsync(); } - public async ValueTask<(byte[] Update, byte[] StateVector)> GetMissingChangesAsync(DocumentContext context, byte[] stateVector, + public async ValueTask GetStateAsync(DocumentContext context, CancellationToken ct = default) { var container = containers.GetContext(context.DocumentName); @@ -55,17 +55,21 @@ public async Task StopAsync( throw new InvalidOperationException("Transaction cannot be created."); } - byte[] update; - if (stateVector.Length == 0) - { - update = stateVector; - } - else - { - update = transaction.StateDiffV2(stateVector); - } + return transaction.StateVectorV1(); + } + }, null); + } + + public async ValueTask GetStateAsUpdateAsync(DocumentContext context, byte[] stateVector, + CancellationToken ct = default) + { + var container = containers.GetContext(context.DocumentName); - return (update, transaction.StateVectorV1()); + return await container.ApplyUpdateReturnAsync(doc => + { + using (var transaction = doc.ReadTransactionOrThrow()) + { + return transaction.StateDiffV1(stateVector); } }, null); } @@ -82,13 +86,8 @@ public async ValueTask ApplyUpdateAsync(DocumentContext context, b Diff = stateDiff }; - using (var transaction = doc.WriteTransaction()) + using (var transaction = doc.WriteTransactionOrThrow()) { - if (transaction == null) - { - throw new InvalidOperationException("Transaction cannot be created."); - } - result.TransactionUpdateResult = transaction.ApplyV1(stateDiff); } @@ -130,13 +129,8 @@ public async ValueTask UpdateDocAsync(DocumentContext context, Action> GetAwarenessAsync(string roomName, + public ValueTask> GetAwarenessAsync(DocumentContext context, CancellationToken ct = default) { - return new ValueTask>(users.GetUsers(roomName)); + return new ValueTask>(users.GetUsers(context.DocumentName)); } } diff --git a/YDotNet.Server/IDocumentManager.cs b/YDotNet.Server/IDocumentManager.cs index f6c51923..ce92267b 100644 --- a/YDotNet.Server/IDocumentManager.cs +++ b/YDotNet.Server/IDocumentManager.cs @@ -12,10 +12,13 @@ ValueTask PingAsync(DocumentContext context, long clock, string? state = null, ValueTask DisconnectAsync(DocumentContext context, CancellationToken ct = default); - ValueTask<(byte[] Update, byte[] StateVector)> GetMissingChangesAsync(DocumentContext context, byte[] stateVector, + ValueTask GetStateAsUpdateAsync(DocumentContext context, byte[] stateVector, CancellationToken ct = default); - ValueTask> GetAwarenessAsync(string roomName, + ValueTask GetStateAsync(DocumentContext context, + CancellationToken ct = default); + + ValueTask> GetAwarenessAsync(DocumentContext context, CancellationToken ct = default); ValueTask ApplyUpdateAsync(DocumentContext context, byte[] stateDiff, diff --git a/YDotNet.Server/Internal/Extensions.cs b/YDotNet.Server/Internal/Extensions.cs new file mode 100644 index 00000000..70f55fba --- /dev/null +++ b/YDotNet.Server/Internal/Extensions.cs @@ -0,0 +1,17 @@ +using YDotNet.Document; +using YDotNet.Document.Transactions; + +namespace YDotNet.Server.Internal; + +internal static class Extensions +{ + public static Transaction ReadTransactionOrThrow(this Doc doc) + { + return doc.ReadTransaction() ?? throw new InvalidOperationException("Failed to open transaction."); + } + + public static Transaction WriteTransactionOrThrow(this Doc doc) + { + return doc.WriteTransaction() ?? throw new InvalidOperationException("Failed to open transaction."); + } +} From 18c6b9b9657f4260bb0c7cfd41c571726481b4d3 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 4 Oct 2023 12:10:43 +0200 Subject: [PATCH 019/186] Fixes --- Demo/Callback.cs | 57 +++++++++++++ Demo/Client/src/App.css | 18 ++++ Demo/Client/src/App.tsx | 29 ++++++- Demo/Client/src/components/Chat.tsx | 61 ++++++++++++++ Demo/Client/src/components/Increment.tsx | 4 +- Demo/Client/src/context/yjsContext.tsx | 7 +- Demo/Client/src/main.tsx | 2 +- Demo/Demo.csproj | 1 + YDotNet.Extensions/YDotNetExtensions.cs | 84 +++++++++++++++---- .../RedisClusteringCallback.cs | 12 ++- .../YDotNetSocketMiddleware.cs | 2 +- YDotNet.Server/DefaultDocumentManager.cs | 14 +--- YDotNet.Server/IDocumentManager.cs | 2 +- YDotNet/Document/Cells/Output.cs | 4 +- 14 files changed, 253 insertions(+), 44 deletions(-) create mode 100644 Demo/Client/src/components/Chat.tsx diff --git a/Demo/Callback.cs b/Demo/Callback.cs index e60258dc..2011fd04 100644 --- a/Demo/Callback.cs +++ b/Demo/Callback.cs @@ -1,4 +1,7 @@ +using System.Text.Json.Serialization; +using YDotNet.Document.Cells; using YDotNet.Document.Types.Events; +using YDotNet.Extensions; using YDotNet.Server; namespace Demo; @@ -14,6 +17,11 @@ public Callback(ILogger log) public ValueTask OnDocumentLoadedAsync(DocumentLoadEvent @event) { + if (@event.Context.DocumentName == "notifications") + { + return default; + } + var map = @event.Document.Map("increment"); map?.ObserveDeep(changes => @@ -36,8 +44,57 @@ public ValueTask OnDocumentLoadedAsync(DocumentLoadEvent @event) } } }); + + var chat = @event.Document.Array("stream"); + + chat?.ObserveDeep(changes => + { + var newNotificationsRaw = + changes + .SelectMany(x => x.ArrayEvent?.Delta.Where(x => x.Tag == EventChangeTag.Add) ?? Enumerable.Empty()) + .SelectMany(x => x.Values ?? Enumerable.Empty()) + .ToArray(); + + if (newNotificationsRaw.Length == 0) + { + return; + } + + List notifications; + + using (var transaction = @event.Document.ReadTransaction()!) + { + notifications = newNotificationsRaw.Select(x => x.To(transaction)).ToList()!; + } + + Task.Run(async () => + { + var notificationCtx = new DocumentContext("Notifications", 0); + + await @event.Source.UpdateDocAsync(notificationCtx, (doc) => + { + var array = doc.Array("stream"); + + notifications = notifications.Select(x => new Notification + { + Text = $"You got the follow message: {x.Text}" + }).ToList(); + + using (var transaction = doc.WriteTransaction() ?? throw new InvalidOperationException("Failed to open transaction.")) + { + array!.InsertRange(transaction, array.Length, notifications.Select(x => x.ToInput()).ToArray()); + } + }); + }); + }); return default; } + + public sealed class Notification + { + [JsonPropertyName("text")] + public string? Text { get; set; } + } } diff --git a/Demo/Client/src/App.css b/Demo/Client/src/App.css index 69adefb7..cdae8706 100644 --- a/Demo/Client/src/App.css +++ b/Demo/Client/src/App.css @@ -66,4 +66,22 @@ display: flex; flex-direction: row; align-items: center; +} + +.chat { + border: 1px solid #ced4da; + border-radius: 4px; + height: 200px; + margin-bottom: .5rem; + overflow-x: hidden; + overflow-y: auto; + padding: .5rem; +} + +.chat div { + border: 1px solid #efefef; + border-radius: 4px; + background: #efefef; + padding: .5rem; + margin-bottom: .5rem; } \ No newline at end of file diff --git a/Demo/Client/src/App.tsx b/Demo/Client/src/App.tsx index 9a62fe71..4b4079a0 100644 --- a/Demo/Client/src/App.tsx +++ b/Demo/Client/src/App.tsx @@ -3,6 +3,8 @@ import { YjsMonacoEditor } from './components/YjsMonacoEditor'; import { YjsProseMirror } from './components/YjsProseMirror'; import { Increment } from './components/Increment'; import './App.css'; +import { YjsContextProvider } from './context/yjsContext'; +import { Chat } from './components/Chat'; function App() { return ( @@ -10,25 +12,46 @@ function App() { -

Monaco Editor

+

Monaco Editor

-

Prose Mirror

+

Prose Mirror

-

Increment

+

Increment

+ + + +

Chat

+
+ + +

Chat

+ + + + +

Notifications

+ + + + + +
+ +
); diff --git a/Demo/Client/src/components/Chat.tsx b/Demo/Client/src/components/Chat.tsx new file mode 100644 index 00000000..e3000eaa --- /dev/null +++ b/Demo/Client/src/components/Chat.tsx @@ -0,0 +1,61 @@ +import * as React from 'react'; +import { Button, Col, Input, Row } from 'reactstrap'; +import { useYjs } from '../hooks/useYjs'; + +interface Comment { + text: string; +} + +export const Chat = ({ isReadonly }: { isReadonly: boolean }) => { + const { yjsDocument } = useYjs(); + const array = yjsDocument.getArray('stream'); + const [state, setState] = React.useState([]); + const [text, setText] = React.useState(''); + + React.useEffect(() => { + const handler = () => { + setState(array.toJSON()); + }; + + array.observeDeep(handler); + + return () => { + array.unobserveDeep(handler); + }; + }, [array]); + + const _comment = () => { + yjsDocument.transact(() => { + array.push([{ text }]); + }); + + setText(''); + }; + + const _setText = (event: React.ChangeEvent) => { + setText(event.target.value); + }; + + return ( + <> +
+ {state.map((row, i) => +
+ {row.text} +
+ )} +
+ + {!isReadonly && + + + + + + + + + } + + ); +}; \ No newline at end of file diff --git a/Demo/Client/src/components/Increment.tsx b/Demo/Client/src/components/Increment.tsx index f0d0c9d0..f6e4e338 100644 --- a/Demo/Client/src/components/Increment.tsx +++ b/Demo/Client/src/components/Increment.tsx @@ -34,11 +34,11 @@ export const Increment = () => { }; return ( - + - + diff --git a/Demo/Client/src/context/yjsContext.tsx b/Demo/Client/src/context/yjsContext.tsx index b67f36b6..3dc0b7e5 100644 --- a/Demo/Client/src/context/yjsContext.tsx +++ b/Demo/Client/src/context/yjsContext.tsx @@ -9,17 +9,18 @@ export interface IYjsContext { export interface IOptions extends React.PropsWithChildren { readonly baseUrl: string; + readonly roomName: string; } export const YjsContextProvider: React.FunctionComponent = (props: IOptions) => { - const { baseUrl } = props; + const { baseUrl, roomName } = props; const contextProps: IYjsContext = React.useMemo(() => { const yjsDocument = new Y.Doc(); - const yjsConnector = new WebsocketProvider(baseUrl, 'test', yjsDocument); + const yjsConnector = new WebsocketProvider(baseUrl, roomName, yjsDocument); return { yjsDocument, yjsConnector }; - }, [baseUrl]); + }, [baseUrl, roomName]); return {props.children}; }; diff --git a/Demo/Client/src/main.tsx b/Demo/Client/src/main.tsx index c60a0b86..6513ae71 100644 --- a/Demo/Client/src/main.tsx +++ b/Demo/Client/src/main.tsx @@ -5,7 +5,7 @@ import 'bootstrap/dist/css/bootstrap.min.css'; import './index.css'; ReactDOM.createRoot(document.getElementById('root')!).render( - + , ); diff --git a/Demo/Demo.csproj b/Demo/Demo.csproj index dc5292b8..24c5713e 100644 --- a/Demo/Demo.csproj +++ b/Demo/Demo.csproj @@ -7,6 +7,7 @@ + diff --git a/YDotNet.Extensions/YDotNetExtensions.cs b/YDotNet.Extensions/YDotNetExtensions.cs index 6171db2c..68bf6934 100644 --- a/YDotNet.Extensions/YDotNetExtensions.cs +++ b/YDotNet.Extensions/YDotNetExtensions.cs @@ -1,3 +1,4 @@ +using System.Text; using System.Text.Json; using YDotNet.Document; using YDotNet.Document.Cells; @@ -9,43 +10,90 @@ namespace YDotNet.Extensions; public static class YDotNetExtensions { - public static T? To(this Output output, Doc doc) + public static Input ToInput(this T source) { - var jsonStream = new MemoryStream(); - var jsonWriter = new Utf8JsonWriter(jsonStream); + var parsed = JsonSerializer.SerializeToElement(source); + + return ConvertValue(parsed); + static Input ConvertObject(JsonElement element) + { + return Input.Object(element.EnumerateObject().ToDictionary(x => x.Name, x => ConvertValue(x.Value))); + } + + static Input ConvertArray(JsonElement element) + { + return Input.Array(element.EnumerateArray().Select(ConvertValue).ToArray()); + } + + static Input ConvertValue(JsonElement element) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + return ConvertObject(element); + case JsonValueKind.Array: + return ConvertArray(element); + case JsonValueKind.String: + return Input.String(element.GetString() ?? string.Empty); + case JsonValueKind.Number: + return Input.Double(element.GetDouble()); + case JsonValueKind.True: + return Input.Boolean(true); + case JsonValueKind.False: + return Input.Boolean(false); + case JsonValueKind.Null: + return Input.Null(); + default: + throw new NotSupportedException(); + } + } + } + + public static T? To(this Output output, Doc doc) + { using var transaction = doc.ReadTransaction() ?? throw new InvalidOperationException("Failed to open transaction."); + return output.To(transaction); + } + + public static T? To(this Output output, Transaction transaction) + { + var jsonStream = new MemoryStream(); + var jsonWriter = new Utf8JsonWriter(jsonStream); + WriteValue(output, jsonWriter, transaction); - static void WriteMap(Map map, Utf8JsonWriter jsonWriter, Transaction transaction) + static void WriteMap(IDictionary obj, Utf8JsonWriter jsonWriter, Transaction transaction) { - var properties = map.Iterate(transaction) - ?? throw new InvalidOperationException("Failed to iterate object."); + jsonWriter.WriteStartObject(); - foreach (var property in properties) + foreach (var (key, value) in obj) { - WriteProperty(property, jsonWriter, transaction); + WriteProperty(key, value, jsonWriter, transaction); } + + jsonWriter.WriteEndObject(); } static void WriteArray(Array array, Utf8JsonWriter jsonWriter, Transaction transaction) { - var items = array.Iterate(transaction) - ?? throw new InvalidOperationException("Failed to iterate array."); + jsonWriter.WriteStartArray(); - foreach (var item in items) + foreach (var item in array.Iterate(transaction) ?? throw new InvalidOperationException("Failed to iterate array.")) { WriteValue(item, jsonWriter, transaction); } + + jsonWriter.WriteEndArray(); } - static void WriteProperty(MapEntry property, Utf8JsonWriter jsonWriter, Transaction transaction) + static void WriteProperty(string key, Output value, Utf8JsonWriter jsonWriter, Transaction transaction) { - jsonWriter.WritePropertyName(property.Key); + jsonWriter.WritePropertyName(key); - WriteValue(property.Value, jsonWriter, transaction); + WriteValue(value, jsonWriter, transaction); } static void WriteValue(Output output, Utf8JsonWriter jsonWriter, Transaction transaction) @@ -66,7 +114,7 @@ static void WriteValue(Output output, Utf8JsonWriter jsonWriter, Transaction tra { jsonWriter.WriteNullValue(); } - else if (output.Map is Map map) + else if (output.Object is IDictionary map) { WriteMap(map, jsonWriter, transaction); } @@ -74,8 +122,10 @@ static void WriteValue(Output output, Utf8JsonWriter jsonWriter, Transaction tra { WriteArray(array, jsonWriter, transaction); } - - throw new InvalidOperationException("Unsupported data type."); + else + { + throw new InvalidOperationException("Unsupported data type."); + } } jsonWriter.Flush(); diff --git a/YDotNet.Server.Redis/RedisClusteringCallback.cs b/YDotNet.Server.Redis/RedisClusteringCallback.cs index ee714251..06e5e6a7 100644 --- a/YDotNet.Server.Redis/RedisClusteringCallback.cs +++ b/YDotNet.Server.Redis/RedisClusteringCallback.cs @@ -107,10 +107,16 @@ private async Task HandleMessage(RedisValue value) } } - public async ValueTask OnDocumentLoadedAsync(DocumentLoadEvent @event) + public ValueTask OnDocumentLoadedAsync(DocumentLoadEvent @event) { - await SendAwarenessRequest(@event.Context); - await SendSync1Async(@event.Context); + // Run these callbacks in another thread because it could cause deadlocks if it would interact with the same document. + _ = Task.Run(async () => + { + await SendAwarenessRequest(@event.Context); + await SendSync1Async(@event.Context); + }); + + return default; } public async ValueTask OnAwarenessUpdatedAsync(ClientAwarenessEvent @event) diff --git a/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs b/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs index 5c5b82af..41c7002a 100644 --- a/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs +++ b/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs @@ -78,7 +78,7 @@ public async Task InvokeAsync(HttpContext httpContext) return; } - var documentName = httpContext.Request.Path; + var documentName = httpContext.Request.Path.ToString().Substring(1); var documentStates = statesPerDocumentName.GetOrAdd(documentName, _ => new List()); logger.LogDebug("Websocket connection to {document} established.", documentName); diff --git a/YDotNet.Server/DefaultDocumentManager.cs b/YDotNet.Server/DefaultDocumentManager.cs index 6ab18f8d..1be119af 100644 --- a/YDotNet.Server/DefaultDocumentManager.cs +++ b/YDotNet.Server/DefaultDocumentManager.cs @@ -48,13 +48,8 @@ public async ValueTask GetStateAsync(DocumentContext context, return await container.ApplyUpdateReturnAsync(doc => { - using (var transaction = doc.ReadTransaction()) + using (var transaction = doc.ReadTransactionOrThrow()) { - if (transaction == null) - { - throw new InvalidOperationException("Transaction cannot be created."); - } - return transaction.StateVectorV1(); } }, null); @@ -116,7 +111,7 @@ await callback.OnDocumentChangedAsync(new DocumentChangedEvent return result; } - public async ValueTask UpdateDocAsync(DocumentContext context, Action action, + public async ValueTask UpdateDocAsync(DocumentContext context, Action action, CancellationToken ct = default) { var container = containers.GetContext(context.DocumentName); @@ -129,10 +124,7 @@ public async ValueTask UpdateDocAsync(DocumentContext context, Action> GetAwarenessAsync(DocumentCo ValueTask ApplyUpdateAsync(DocumentContext context, byte[] stateDiff, CancellationToken ct = default); - ValueTask UpdateDocAsync(DocumentContext context, Action action, + ValueTask UpdateDocAsync(DocumentContext context, Action action, CancellationToken ct = default); ValueTask CleanupAsync( diff --git a/YDotNet/Document/Cells/Output.cs b/YDotNet/Document/Cells/Output.cs index b538b303..a87a7280 100644 --- a/YDotNet/Document/Cells/Output.cs +++ b/YDotNet/Document/Cells/Output.cs @@ -136,12 +136,12 @@ public IDictionary? Object /// /// Gets a value indicating whether this output cell contains a null value. /// - public bool Null => OutputChannel.Null(Handle) == 1; + public bool Null => OutputNative?.Tag == -1; /// /// Gets a value indicating whether this output cell contains an undefined value. /// - public bool Undefined => OutputChannel.Undefined(Handle) == 1; + public bool Undefined => OutputNative?.Tag == 0; /// /// Gets the or null if this output cells contains a different type stored. From 59e8c88471414f73af5e4bb0450a8ad1ac2d334b Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 4 Oct 2023 12:34:08 +0200 Subject: [PATCH 020/186] Fix To method. --- YDotNet.Extensions/YDotNetExtensions.cs | 36 +++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/YDotNet.Extensions/YDotNetExtensions.cs b/YDotNet.Extensions/YDotNetExtensions.cs index 68bf6934..6b2ff161 100644 --- a/YDotNet.Extensions/YDotNetExtensions.cs +++ b/YDotNet.Extensions/YDotNetExtensions.cs @@ -65,7 +65,7 @@ static Input ConvertValue(JsonElement element) WriteValue(output, jsonWriter, transaction); - static void WriteMap(IDictionary obj, Utf8JsonWriter jsonWriter, Transaction transaction) + static void WriteObject(IDictionary obj, Utf8JsonWriter jsonWriter, Transaction transaction) { jsonWriter.WriteStartObject(); @@ -77,6 +77,30 @@ static void WriteMap(IDictionary obj, Utf8JsonWriter jsonWriter, jsonWriter.WriteEndObject(); } + static void WriteCollection(Output[] array, Utf8JsonWriter jsonWriter, Transaction transaction) + { + jsonWriter.WriteStartArray(); + + foreach (var item in array) + { + WriteValue(item, jsonWriter, transaction); + } + + jsonWriter.WriteEndArray(); + } + + static void WriteMap(Map map, Utf8JsonWriter jsonWriter, Transaction transaction) + { + jsonWriter.WriteStartArray(); + + foreach (var property in map.Iterate(transaction) ?? throw new InvalidOperationException("Failed to iterate array.")) + { + WriteProperty(property.Key, property.Value, jsonWriter, transaction); + } + + jsonWriter.WriteEndArray(); + } + static void WriteArray(Array array, Utf8JsonWriter jsonWriter, Transaction transaction) { jsonWriter.WriteStartArray(); @@ -114,7 +138,15 @@ static void WriteValue(Output output, Utf8JsonWriter jsonWriter, Transaction tra { jsonWriter.WriteNullValue(); } - else if (output.Object is IDictionary map) + else if (output.Object is IDictionary obj) + { + WriteObject(obj, jsonWriter, transaction); + } + else if (output.Collection is Output[] collection) + { + WriteCollection(collection, jsonWriter, transaction); + } + else if (output.Map is Map map) { WriteMap(map, jsonWriter, transaction); } From f5b5182ed42e9822440e441e60ae9ea798885556 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 4 Oct 2023 20:46:28 +0200 Subject: [PATCH 021/186] Small fixes. --- Demo/Callback.cs | 26 ++++----- Demo/Client/src/App.tsx | 12 ++--- Demo/Client/src/components/Chat.tsx | 18 ++++++- Demo/Client/src/components/Increment.tsx | 3 +- YDotNet.Extensions/YDotNetExtensions.cs | 1 - .../RedisClusteringCallback.cs | 4 +- .../YDotNetSocketMiddleware.cs | 4 +- YDotNet.Server/ConnectedUser.cs | 4 +- YDotNet.Server/DefaultDocumentManager.cs | 53 ++++++------------- YDotNet.Server/DocumentManagerOptions.cs | 2 +- YDotNet.Server/IDocumentCallback.cs | 5 -- YDotNet.Server/IDocumentManager.cs | 4 +- YDotNet.Server/Internal/CallbackInvoker.cs | 5 -- YDotNet.Server/Internal/ConnectedUsers.cs | 28 ++++++---- ...mentContainerCache.cs => DocumentCache.cs} | 0 YDotNet.Server/Internal/DocumentContainer.cs | 24 +++------ YDotNet.Server/Internal/Extensions.cs | 47 ++++++++++++++++ YDotNet.Server/Internal/SubscribeOnce.cs | 28 ++++++++++ YDotNet/Document/Transactions/Transaction.cs | 2 +- YDotNet/Protocol/Decoder.cs | 2 +- 20 files changed, 163 insertions(+), 109 deletions(-) rename YDotNet.Server/Internal/{DocumentContainerCache.cs => DocumentCache.cs} (100%) create mode 100644 YDotNet.Server/Internal/SubscribeOnce.cs diff --git a/Demo/Callback.cs b/Demo/Callback.cs index 2011fd04..f987337c 100644 --- a/Demo/Callback.cs +++ b/Demo/Callback.cs @@ -47,7 +47,7 @@ public ValueTask OnDocumentLoadedAsync(DocumentLoadEvent @event) var chat = @event.Document.Array("stream"); - chat?.ObserveDeep(changes => + chat?.ObserveDeep(async changes => { var newNotificationsRaw = changes @@ -67,24 +67,18 @@ public ValueTask OnDocumentLoadedAsync(DocumentLoadEvent @event) notifications = newNotificationsRaw.Select(x => x.To(transaction)).ToList()!; } - Task.Run(async () => - { - var notificationCtx = new DocumentContext("Notifications", 0); + var notificationCtx = new DocumentContext("notifications", 0); - await @event.Source.UpdateDocAsync(notificationCtx, (doc) => - { - var array = doc.Array("stream"); + await @event.Source.UpdateDocAsync(notificationCtx, (doc) => + { + var array = doc.Array("stream"); - notifications = notifications.Select(x => new Notification - { - Text = $"You got the follow message: {x.Text}" - }).ToList(); + // notifications = notifications.Select(x => new Notification { Text = $"You got the follow message: {x.Text}" }).ToList(); - using (var transaction = doc.WriteTransaction() ?? throw new InvalidOperationException("Failed to open transaction.")) - { - array!.InsertRange(transaction, array.Length, notifications.Select(x => x.ToInput()).ToArray()); - } - }); + using (var transaction = doc.WriteTransaction() ?? throw new InvalidOperationException("Failed to open transaction.")) + { + array!.InsertRange(transaction, array.Length, notifications.Select(x => x.ToInput()).ToArray()); + } }); }); diff --git a/Demo/Client/src/App.tsx b/Demo/Client/src/App.tsx index 4b4079a0..483777a2 100644 --- a/Demo/Client/src/App.tsx +++ b/Demo/Client/src/App.tsx @@ -10,40 +10,40 @@ function App() { return ( <> - +

Monaco Editor

- +

Prose Mirror

- +

Increment

- +

Chat

-

Chat

+
Chat
-

Notifications

+
Notifications
diff --git a/Demo/Client/src/components/Chat.tsx b/Demo/Client/src/components/Chat.tsx index e3000eaa..108723ce 100644 --- a/Demo/Client/src/components/Chat.tsx +++ b/Demo/Client/src/components/Chat.tsx @@ -11,12 +11,14 @@ export const Chat = ({ isReadonly }: { isReadonly: boolean }) => { const array = yjsDocument.getArray('stream'); const [state, setState] = React.useState([]); const [text, setText] = React.useState(''); + const container = React.useRef(null); React.useEffect(() => { const handler = () => { setState(array.toJSON()); }; + handler(); array.observeDeep(handler); return () => { @@ -24,7 +26,21 @@ export const Chat = ({ isReadonly }: { isReadonly: boolean }) => { }; }, [array]); + React.useEffect(() => { + setTimeout(() => { + const div = container.current; + + if (div) { + div.scrollTop = div.scrollHeight; + } + }); + }, [state]); + const _comment = () => { + if (!text) { + return; + } + yjsDocument.transact(() => { array.push([{ text }]); }); @@ -38,7 +54,7 @@ export const Chat = ({ isReadonly }: { isReadonly: boolean }) => { return ( <> -
+
{state.map((row, i) =>
{row.text} diff --git a/Demo/Client/src/components/Increment.tsx b/Demo/Client/src/components/Increment.tsx index f6e4e338..f5a152a5 100644 --- a/Demo/Client/src/components/Increment.tsx +++ b/Demo/Client/src/components/Increment.tsx @@ -12,6 +12,7 @@ export const Increment = () => { setState(map.get('value') || 0); }; + handler(); map.observeDeep(handler); return () => { @@ -22,7 +23,7 @@ export const Increment = () => { React.useEffect(() => { yjsDocument.transact(() => { map.set('value', state); - }, 'Incrementer'); + }); }, [map, state, yjsDocument]); const _increment = () => { diff --git a/YDotNet.Extensions/YDotNetExtensions.cs b/YDotNet.Extensions/YDotNetExtensions.cs index 6b2ff161..999f77c0 100644 --- a/YDotNet.Extensions/YDotNetExtensions.cs +++ b/YDotNet.Extensions/YDotNetExtensions.cs @@ -1,4 +1,3 @@ -using System.Text; using System.Text.Json; using YDotNet.Document; using YDotNet.Document.Cells; diff --git a/YDotNet.Server.Redis/RedisClusteringCallback.cs b/YDotNet.Server.Redis/RedisClusteringCallback.cs index 06e5e6a7..c6e8e049 100644 --- a/YDotNet.Server.Redis/RedisClusteringCallback.cs +++ b/YDotNet.Server.Redis/RedisClusteringCallback.cs @@ -147,7 +147,7 @@ private ValueTask SendAwarenessAsync(DocumentContext context, string? state, lon private async ValueTask SendSync1Async(DocumentContext context) { - var state = await documentManager!.GetStateAsync(context); + var state = await documentManager!.GetStateVectorAsync(context); var m = new Message { Type = MessageType.SyncStep1, Data = state }; @@ -156,7 +156,7 @@ private async ValueTask SendSync1Async(DocumentContext context) private async ValueTask SendSync2Async(DocumentContext context, byte[] stateVector) { - var state = await documentManager!.GetStateAsUpdateAsync(context, stateVector); + var state = await documentManager!.GetUpdateAsync(context, stateVector); var m = new Message { Type = MessageType.SyncStep2, Data = state }; diff --git a/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs b/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs index 41c7002a..be3af2a8 100644 --- a/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs +++ b/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs @@ -183,8 +183,8 @@ await state.WriteLockedAsync(true, async (encoder, context, state, ct) => case MessageTypes.SyncStep1: var clientState = await state.Decoder.ReadVarUint8ArrayAsync(ct); - var serverState = await documentManager!.GetStateAsync(state.DocumentContext, ct); - var serverUpdate = await documentManager!.GetStateAsUpdateAsync(state.DocumentContext, clientState, ct); + var serverState = await documentManager!.GetStateVectorAsync(state.DocumentContext, ct); + var serverUpdate = await documentManager!.GetUpdateAsync(state.DocumentContext, clientState, ct); // We mark the sync state as false again to handle multiple sync steps. state.IsSynced = false; diff --git a/YDotNet.Server/ConnectedUser.cs b/YDotNet.Server/ConnectedUser.cs index 6ddd2de6..218c5d14 100644 --- a/YDotNet.Server/ConnectedUser.cs +++ b/YDotNet.Server/ConnectedUser.cs @@ -2,9 +2,9 @@ namespace YDotNet.Server; public sealed class ConnectedUser { - required public string? ClientState { get; set; } + public string? ClientState { get; set; } - required public long ClientClock { get; set; } + public long ClientClock { get; set; } public DateTime LastActivity { get; set; } } diff --git a/YDotNet.Server/DefaultDocumentManager.cs b/YDotNet.Server/DefaultDocumentManager.cs index 1be119af..ad2a7067 100644 --- a/YDotNet.Server/DefaultDocumentManager.cs +++ b/YDotNet.Server/DefaultDocumentManager.cs @@ -14,7 +14,7 @@ public sealed class DefaultDocumentManager : IDocumentManager { private readonly ConnectedUsers users = new(); private readonly DocumentManagerOptions options; - private readonly DocumentContainerCache containers; + private readonly DocumentCache cache; private readonly CallbackInvoker callback; public DefaultDocumentManager( @@ -26,7 +26,7 @@ public DefaultDocumentManager( this.options = options.Value; this.callback = new CallbackInvoker(callbacks, logger); - containers = new DocumentContainerCache(documentStorage, this.callback, this, options.Value); + cache = new DocumentCache(documentStorage, this.callback, this, options.Value); } public async Task StartAsync( @@ -38,13 +38,13 @@ public async Task StartAsync( public async Task StopAsync( CancellationToken cancellationToken) { - await containers.DisposeAsync(); + await cache.DisposeAsync(); } - public async ValueTask GetStateAsync(DocumentContext context, + public async ValueTask GetStateVectorAsync(DocumentContext context, CancellationToken ct = default) { - var container = containers.GetContext(context.DocumentName); + var container = cache.GetContext(context.DocumentName); return await container.ApplyUpdateReturnAsync(doc => { @@ -52,13 +52,13 @@ public async ValueTask GetStateAsync(DocumentContext context, { return transaction.StateVectorV1(); } - }, null); + }); } - public async ValueTask GetStateAsUpdateAsync(DocumentContext context, byte[] stateVector, + public async ValueTask GetUpdateAsync(DocumentContext context, byte[] stateVector, CancellationToken ct = default) { - var container = containers.GetContext(context.DocumentName); + var container = cache.GetContext(context.DocumentName); return await container.ApplyUpdateReturnAsync(doc => { @@ -66,13 +66,13 @@ public async ValueTask GetStateAsUpdateAsync(DocumentContext context, by { return transaction.StateDiffV1(stateVector); } - }, null); + }); } public async ValueTask ApplyUpdateAsync(DocumentContext context, byte[] stateDiff, CancellationToken ct = default) { - var container = containers.GetContext(context.DocumentName); + var container = cache.GetContext(context.DocumentName); var (result, doc) = await container.ApplyUpdateReturnAsync(doc => { @@ -87,18 +87,11 @@ public async ValueTask ApplyUpdateAsync(DocumentContext context, b } return (result, doc); - }, async doc => - { - await callback.OnDocumentChangingAsync(new DocumentChangeEvent - { - Context = context, - Document = doc, - Source = this, - }); }); if (result.Diff != null) { + // The diff is just the plain byte array from the client. await callback.OnDocumentChangedAsync(new DocumentChangedEvent { Context = context, @@ -114,36 +107,24 @@ await callback.OnDocumentChangedAsync(new DocumentChangedEvent public async ValueTask UpdateDocAsync(DocumentContext context, Action action, CancellationToken ct = default) { - var container = containers.GetContext(context.DocumentName); + var container = cache.GetContext(context.DocumentName); var (diff, doc) = await container.ApplyUpdateReturnAsync(doc => { - byte[]? diff = null; - var subscription = doc.ObserveUpdatesV1(@event => - { - diff = @event.Update; - }); + using var subscribeOnce = new SubscribeToUpdatesV1Once(doc); action(doc); - doc.UnobserveUpdatesV2(subscription); - return (diff, doc); - }, async doc => - { - await callback.OnDocumentChangingAsync(new DocumentChangeEvent - { - Context = context, - Document = doc, - Source = this, - }); + return (subscribeOnce.Update, doc); }); if (diff != null) { + // The result from the library already contains all the headers and is therefore not protocol agnostic. await callback.OnDocumentChangedAsync(new DocumentChangedEvent { Context = context, - Diff = diff, + Diff = await diff.GetUpdateArray(), Document = doc, Source = this, }); @@ -190,7 +171,7 @@ await callback.OnClientDisconnectedAsync(new ClientDisconnectedEvent }); } - containers.RemoveEvictedItems(); + cache.RemoveEvictedItems(); } public ValueTask> GetAwarenessAsync(DocumentContext context, diff --git a/YDotNet.Server/DocumentManagerOptions.cs b/YDotNet.Server/DocumentManagerOptions.cs index 2de359b8..d9cce8f6 100644 --- a/YDotNet.Server/DocumentManagerOptions.cs +++ b/YDotNet.Server/DocumentManagerOptions.cs @@ -4,7 +4,7 @@ public sealed class DocumentManagerOptions { public bool AutoCreateDocument { get; set; } = true; - public TimeSpan DelayWriting { get; set; } = TimeSpan.FromMilliseconds(100); + public TimeSpan StoreDebounce { get; set; } = TimeSpan.FromMilliseconds(100); public TimeSpan MaxWriteTimeInterval { get; set; } = TimeSpan.FromSeconds(5); diff --git a/YDotNet.Server/IDocumentCallback.cs b/YDotNet.Server/IDocumentCallback.cs index 870be586..c954ef1d 100644 --- a/YDotNet.Server/IDocumentCallback.cs +++ b/YDotNet.Server/IDocumentCallback.cs @@ -12,11 +12,6 @@ ValueTask OnDocumentLoadedAsync(DocumentLoadEvent @event) return default; } - ValueTask OnDocumentChangingAsync(DocumentChangeEvent @event) - { - return default; - } - ValueTask OnDocumentChangedAsync(DocumentChangedEvent @event) { return default; diff --git a/YDotNet.Server/IDocumentManager.cs b/YDotNet.Server/IDocumentManager.cs index 875ee951..acad21b7 100644 --- a/YDotNet.Server/IDocumentManager.cs +++ b/YDotNet.Server/IDocumentManager.cs @@ -12,10 +12,10 @@ ValueTask PingAsync(DocumentContext context, long clock, string? state = null, ValueTask DisconnectAsync(DocumentContext context, CancellationToken ct = default); - ValueTask GetStateAsUpdateAsync(DocumentContext context, byte[] stateVector, + ValueTask GetUpdateAsync(DocumentContext context, byte[] stateVector, CancellationToken ct = default); - ValueTask GetStateAsync(DocumentContext context, + ValueTask GetStateVectorAsync(DocumentContext context, CancellationToken ct = default); ValueTask> GetAwarenessAsync(DocumentContext context, diff --git a/YDotNet.Server/Internal/CallbackInvoker.cs b/YDotNet.Server/Internal/CallbackInvoker.cs index 12f7a6c7..db373498 100644 --- a/YDotNet.Server/Internal/CallbackInvoker.cs +++ b/YDotNet.Server/Internal/CallbackInvoker.cs @@ -23,11 +23,6 @@ public ValueTask OnDocumentLoadedAsync(DocumentLoadEvent @event) return InvokeCallbackAsync(@event, (c, e) => c.OnDocumentLoadedAsync(e)); } - public ValueTask OnDocumentChangingAsync(DocumentChangeEvent @event) - { - return InvokeCallbackAsync(@event, (c, e) => c.OnDocumentChangingAsync(e)); - } - public ValueTask OnDocumentChangedAsync(DocumentChangedEvent @event) { return InvokeCallbackAsync(@event, (c, e) => c.OnDocumentChangedAsync(e)); diff --git a/YDotNet.Server/Internal/ConnectedUsers.cs b/YDotNet.Server/Internal/ConnectedUsers.cs index da50117b..4d1e17b6 100644 --- a/YDotNet.Server/Internal/ConnectedUsers.cs +++ b/YDotNet.Server/Internal/ConnectedUsers.cs @@ -20,11 +20,12 @@ public IReadOnlyDictionary GetUsers(string documentName) public bool AddOrUpdate(string documentName, long clientId, long clock, string? state, out string? existingState) { - var documentUsers = users.GetOrAdd(documentName, _ => new Dictionary()); + var users = this.users.GetOrAdd(documentName, _ => new Dictionary()); - lock (documentUsers) + // We expect to have relatively few users per document, therefore we use normal lock here. + lock (users) { - if (documentUsers.TryGetValue(clientId, out var user)) + if (users.TryGetValue(clientId, out var user)) { var isChanged = false; @@ -32,15 +33,18 @@ public bool AddOrUpdate(string documentName, long clientId, long clock, string? { user.ClientClock = clock; user.ClientState = state; + isChanged = true; } existingState = user.ClientState; + // Always update the timestamp, because every call is an activity. user.LastActivity = Clock(); + return isChanged; } - documentUsers.Add(clientId, new ConnectedUser + users.Add(clientId, new ConnectedUser { ClientClock = clock, ClientState = state, @@ -71,10 +75,12 @@ public bool Remove(string documentName, long clientId) foreach (var (documentName, users) in users) { - List? usersToRemove = null; - + // We expect to have relatively few users per document, therefore we use normal lock here. lock (users) { + // Usually there should be nothing to remove, therefore we save a few allocations for the fast path. + List? usersToRemove = null; + foreach (var (clientId, user) in users) { if (user.LastActivity < olderThan) @@ -85,13 +91,13 @@ public bool Remove(string documentName, long clientId) yield return (clientId, documentName); } } - } - if (usersToRemove != null) - { - foreach (var user in usersToRemove) + if (usersToRemove != null) { - users.Remove(user); + foreach (var user in usersToRemove) + { + users.Remove(user); + } } } } diff --git a/YDotNet.Server/Internal/DocumentContainerCache.cs b/YDotNet.Server/Internal/DocumentCache.cs similarity index 100% rename from YDotNet.Server/Internal/DocumentContainerCache.cs rename to YDotNet.Server/Internal/DocumentCache.cs diff --git a/YDotNet.Server/Internal/DocumentContainer.cs b/YDotNet.Server/Internal/DocumentContainer.cs index 021dcf7a..cfd91f4f 100644 --- a/YDotNet.Server/Internal/DocumentContainer.cs +++ b/YDotNet.Server/Internal/DocumentContainer.cs @@ -23,7 +23,7 @@ public DocumentContainer(string documentName, this.documentStorage = documentStorage; this.options = options; - writer = new DelayedWriter(options.DelayWriting, options.MaxWriteTimeInterval, WriteAsync); + writer = new DelayedWriter(options.StoreDebounce, options.MaxWriteTimeInterval, WriteAsync); loadingTask = LoadInternalAsync(documentCallback, documentManager); } @@ -67,7 +67,7 @@ private async Task LoadCoreAsync() throw new InvalidOperationException("Transaction cannot be acquired."); } - transaction.ApplyV2(documentData); + transaction.ApplyV1(documentData); } return document; @@ -83,15 +83,10 @@ private async Task LoadCoreAsync() } } - public async Task ApplyUpdateReturnAsync(Func action, Func? beforeAction) + public async Task ApplyUpdateReturnAsync(Func action) { var document = await loadingTask; - if (beforeAction != null) - { - await beforeAction(document); - } - slimLock.Wait(); try { @@ -112,25 +107,22 @@ private async Task WriteAsync() return; } - byte[] snapshot; + byte[] state; slimLock.Wait(); try { - using var transaction = doc.ReadTransaction(); + using var transaction = doc.ReadTransactionOrThrow(); - if (transaction == null) - { - throw new InvalidOperationException("Transaction cannot be acquired."); - } + var snapshot = transaction!.Snapshot()!; - snapshot = transaction!.Snapshot(); + state = transaction.StateDiffV1(snapshot)!; } finally { slimLock.Release(); } - await documentStorage.StoreDocAsync(documentName, snapshot); + await documentStorage.StoreDocAsync(documentName, state); } } diff --git a/YDotNet.Server/Internal/Extensions.cs b/YDotNet.Server/Internal/Extensions.cs index 70f55fba..4e580467 100644 --- a/YDotNet.Server/Internal/Extensions.cs +++ b/YDotNet.Server/Internal/Extensions.cs @@ -1,5 +1,6 @@ using YDotNet.Document; using YDotNet.Document.Transactions; +using YDotNet.Protocol; namespace YDotNet.Server.Internal; @@ -14,4 +15,50 @@ public static Transaction WriteTransactionOrThrow(this Doc doc) { return doc.WriteTransaction() ?? throw new InvalidOperationException("Failed to open transaction."); } + + public static async ValueTask GetUpdateArray(this byte[] source) + { + return source; + var decoder = new MemoryDecoder(source); + await decoder.ReadVarUintAsync(); + await decoder.ReadVarUintAsync(); + + return await decoder.ReadVarUint8ArrayAsync(default); + } + + class MemoryDecoder : Decoder + { + private readonly byte[] source; + private int position = 0; + + public MemoryDecoder(byte[] source) + { + this.source = source; + } + + protected override ValueTask ReadByteAsync( + CancellationToken ct) + { + if (position == source.Length) + { + throw new InvalidOperationException("End of buffer reached."); + } + + return new ValueTask(source[position++]); + } + + protected override ValueTask ReadBytesAsync(Memory bytes, + CancellationToken ct) + { + if (position + bytes.Length >= source.Length) + { + throw new InvalidOperationException("End of buffer reached."); + } + + source.AsMemory(position, bytes.Length).CopyTo(bytes); + position += bytes.Length; + + return default; + } + } } diff --git a/YDotNet.Server/Internal/SubscribeOnce.cs b/YDotNet.Server/Internal/SubscribeOnce.cs new file mode 100644 index 00000000..b846a312 --- /dev/null +++ b/YDotNet.Server/Internal/SubscribeOnce.cs @@ -0,0 +1,28 @@ +using YDotNet.Document; + +namespace YDotNet.Server.Internal; + +internal sealed class SubscribeToUpdatesV1Once : IDisposable +{ + private readonly Action unsubscribe; + + public byte[]? Update { get; private set; } + + public SubscribeToUpdatesV1Once(Doc doc) + { + var subscription = doc.ObserveUpdatesV1(@event => + { + Update = @event.Update; + }); + + unsubscribe = () => + { + doc.UnobserveUpdatesV1(subscription); + }; + } + + public void Dispose() + { + unsubscribe(); + } +} diff --git a/YDotNet/Document/Transactions/Transaction.cs b/YDotNet/Document/Transactions/Transaction.cs index a68e97ba..abe4f98d 100644 --- a/YDotNet/Document/Transactions/Transaction.cs +++ b/YDotNet/Document/Transactions/Transaction.cs @@ -127,7 +127,7 @@ public byte[] StateVectorV1() /// public byte[] StateDiffV1(byte[] stateVector) { - var handle = TransactionChannel.StateDiffV1(Handle, stateVector, (uint) stateVector.Length, out var length); + var handle = TransactionChannel.StateDiffV1(Handle, stateVector, (uint) (stateVector != null ? stateVector.Length : 0), out var length); var data = MemoryReader.ReadBytes(handle, length); BinaryChannel.Destroy(handle, length); diff --git a/YDotNet/Protocol/Decoder.cs b/YDotNet/Protocol/Decoder.cs index 34efcaee..c4e27d18 100644 --- a/YDotNet/Protocol/Decoder.cs +++ b/YDotNet/Protocol/Decoder.cs @@ -48,7 +48,7 @@ public async ValueTask ReadVarUintAsync( /// The decoded byte array. /// public async ValueTask ReadVarUint8ArrayAsync( - CancellationToken ct) + CancellationToken ct = default) { var arrayLength = await this.ReadVarUintAsync(ct); var arrayBuffer = new byte[arrayLength]; From 722f5090d071e914d96ab630afb0f93a307ebb31 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 5 Oct 2023 11:49:59 +0200 Subject: [PATCH 022/186] Fix sync. --- YDotNet.Server/DefaultDocumentManager.cs | 5 +---- YDotNet.Server/Internal/DocumentCache.cs | 4 ++-- .../{SubscribeOnce.cs => SubscribeToUpdatesV1Once.cs} | 0 YDotNet/Document/Options/DocOptions.cs | 4 ++-- 4 files changed, 5 insertions(+), 8 deletions(-) rename YDotNet.Server/Internal/{SubscribeOnce.cs => SubscribeToUpdatesV1Once.cs} (100%) diff --git a/YDotNet.Server/DefaultDocumentManager.cs b/YDotNet.Server/DefaultDocumentManager.cs index ad2a7067..4add8804 100644 --- a/YDotNet.Server/DefaultDocumentManager.cs +++ b/YDotNet.Server/DefaultDocumentManager.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using YDotNet.Document; -using YDotNet.Document.Transactions; using YDotNet.Server.Internal; using YDotNet.Server.Storage; using IDocumentCallbacks = System.Collections.Generic.IEnumerable; @@ -91,7 +90,6 @@ public async ValueTask ApplyUpdateAsync(DocumentContext context, b if (result.Diff != null) { - // The diff is just the plain byte array from the client. await callback.OnDocumentChangedAsync(new DocumentChangedEvent { Context = context, @@ -120,11 +118,10 @@ public async ValueTask UpdateDocAsync(DocumentContext context, Action actio if (diff != null) { - // The result from the library already contains all the headers and is therefore not protocol agnostic. await callback.OnDocumentChangedAsync(new DocumentChangedEvent { Context = context, - Diff = await diff.GetUpdateArray(), + Diff = diff, Document = doc, Source = this, }); diff --git a/YDotNet.Server/Internal/DocumentCache.cs b/YDotNet.Server/Internal/DocumentCache.cs index b77cee1e..b102930a 100644 --- a/YDotNet.Server/Internal/DocumentCache.cs +++ b/YDotNet.Server/Internal/DocumentCache.cs @@ -4,7 +4,7 @@ namespace YDotNet.Server.Internal; -internal sealed class DocumentContainerCache : IAsyncDisposable +internal sealed class DocumentCache : IAsyncDisposable { private readonly IDocumentStorage documentStorage; private readonly IDocumentCallback documentCallback; @@ -14,7 +14,7 @@ internal sealed class DocumentContainerCache : IAsyncDisposable private readonly Dictionary livingContainers = new(); private readonly SemaphoreSlim slimLock = new(1); - public DocumentContainerCache( + public DocumentCache( IDocumentStorage documentStorage, IDocumentCallback documentCallback, IDocumentManager documentManager, diff --git a/YDotNet.Server/Internal/SubscribeOnce.cs b/YDotNet.Server/Internal/SubscribeToUpdatesV1Once.cs similarity index 100% rename from YDotNet.Server/Internal/SubscribeOnce.cs rename to YDotNet.Server/Internal/SubscribeToUpdatesV1Once.cs diff --git a/YDotNet/Document/Options/DocOptions.cs b/YDotNet/Document/Options/DocOptions.cs index 36b68008..508eb2db 100644 --- a/YDotNet/Document/Options/DocOptions.cs +++ b/YDotNet/Document/Options/DocOptions.cs @@ -1,4 +1,4 @@ -namespace YDotNet.Document.Options; +namespace YDotNet.Document.Options; /// /// Configuration object that, optionally, is used to create instances. @@ -10,7 +10,7 @@ public class DocOptions /// internal static DocOptions Default => new() { - Id = (ulong) Random.Shared.NextInt64(), + Id = (ulong) Random.Shared.Next(), ShouldLoad = true, Encoding = DocEncoding.Utf16 }; From afc94989a184b0d24b7817a434e56b68197c9aef Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 11:03:19 +0200 Subject: [PATCH 023/186] Test --- .cargo/config.github | 11 ++ .github/workflows/build-binaries.yml | 143 ++++++++++++++++++ .github/workflows/build.yml | 28 ++++ Demo/Client/package-lock.json | 15 +- Demo/Client/package.json | 2 +- YDotNet.Native/Class1.cs | 7 + YDotNet.Native/YDotNet.Native.csproj | 9 ++ YDotNet.sln | 32 +++- .../YDotNet.Native.Linux.csproj | 18 +++ .../YDotNet.Native.MacOS.csproj | 13 ++ .../YDotNet.Native.Win32.csproj | 13 ++ native/YDotNet.Native/YDotNet.Native.csproj | 15 ++ resources/patch/yffi/Cargo.toml.patch | 9 ++ 13 files changed, 307 insertions(+), 8 deletions(-) create mode 100644 .cargo/config.github create mode 100644 .github/workflows/build-binaries.yml create mode 100644 .github/workflows/build.yml create mode 100644 YDotNet.Native/Class1.cs create mode 100644 YDotNet.Native/YDotNet.Native.csproj create mode 100644 native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj create mode 100644 native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj create mode 100644 native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj create mode 100644 native/YDotNet.Native/YDotNet.Native.csproj create mode 100644 resources/patch/yffi/Cargo.toml.patch diff --git a/.cargo/config.github b/.cargo/config.github new file mode 100644 index 00000000..91e208bc --- /dev/null +++ b/.cargo/config.github @@ -0,0 +1,11 @@ +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-linux-gnu-gcc" + +[target.aarch64-unknown-linux-musl] +linker = "aarch64-linux-gnu-gcc" + +[target.armv7-unknown-linux-gnueabihf] +linker = "arm-linux-gnueabihf-gcc" + +[target.armv7-unknown-linux-musleabihf] +linker = "arm-linux-musleabihf-gcc" diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml new file mode 100644 index 00000000..dd0dd9eb --- /dev/null +++ b/.github/workflows/build-binaries.yml @@ -0,0 +1,143 @@ +name: Build Binaries + +on: + push: + tags: + - 'binaries[0-9]' +env: + YRS_REPO: https://github.com/LSViana/y-crdt + YRS_BRANCH: main + CARGO_TERM_COLOR: always + +jobs: + # Based on https://www.rohanjain.in/cargo-cross/ + build-native-binaries: + name: Build native libraries + runs-on: ${{matrix.os}} + strategy: + matrix: + include: + # Windows + - build: win-x64 + os: windows-latest + rust: stable + target: x86_64-pc-windows-msvc + linker: mingw-w64 + cross: false + + # Linux + - build: linux-x64 + os: ubuntu-latest + rust: stable + target: x86_64-unknown-linux-gnu + cross: false + + - build: linux-x64-musl + os: ubuntu-latest + rust: stable + target: x86_64-unknown-linux-musl + cross: false + + - build: linux-armv7 + os: ubuntu-latest + rust: stable + target: armv7-unknown-linux-gnueabihf + linker: gcc-arm-linux-gnueabihf + cross: true + + - build: linux-armv7-musl + os: ubuntu-latest + rust: stable + target: armv7-unknown-linux-musleabihf + linker: gcc-arm-linux-gnueabihf + cross: true + + - build: linux-arm64 + os: ubuntu-latest + rust: stable + target: aarch64-unknown-linux-gnu + linker: gcc-aarch64-linux-gnu + cross: true + + - build: linux-arm64-musl + os: ubuntu-latest + rust: stable + target: aarch64-unknown-linux-musl + linker: gcc-aarch64-linux-gnu + cross: true + + # MacOS + - build: macos + os: macos-latest + rust: stable + target: x86_64-apple-darwin + cross: false + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + ~/.rustup + target + key: ${{ runner.os }}-${{ matrix.rust }} + + - name: Install cross + if: matrix.cross + uses: taiki-e/install-action@v2 + with: + tool: cross + + - name: Add musl tools + run: sudo apt install -y musl musl-dev musl-tools + if: endsWith(matrix.build, '-musl') + + - name: Install Linker + if: matrix.cross + run: | + sudo apt update + sudo apt install ${{ matrix.linker }} + cat .cargo/config.github >> .cargo/config + + - name: Install Rust + run: | + rustup install ${{ matrix.rust }} + rustup target add ${{ matrix.target }} + rustup show + + - name: Clone Yrs repo + run: | + git clone ${YRS_REPO} --branch ${YRS_BRANCH} --single-branch yrs + shell: bash + + - name: Patch yffi/Cargo.toml + run: | + patch yrs/yffi/Cargo.toml resources/patch/yffi/Cargo.toml.patch + shell: bash + + - name: Build (cargo) + if: "!matrix.cross" + run: | + cd yrs + RUSTFLAGS="-C target-feature=-crt-static" cargo build --release --target ${{ matrix.target }} + shell: bash + + - name: Build (cross) + if: matrix.cross + run: | + cd yrs + RUSTFLAGS="-C target-feature=-crt-static" cross build --release --target ${{ matrix.target }} + + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: ${{ matrix.build }} + path: | + yrs/target/${{ matrix.target }}/release/*yrs.dll + yrs/target/${{ matrix.target }}/release/*yrs.so + yrs/target/${{ matrix.target }}/release/*yrs.dylib \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..0b8af9fd --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,28 @@ +name: CI + +on: + push: + branches: + - '*' + +jobs: + pack-nuget: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + sparse-checkout: | + NativeAssets + + - name: Download artifacts + uses: dawidd6/action-download-artifact@v2 + with: + path: ./output + workflow: build-binaries.yml + workflow_conclusion: success + + - name: Nuget pack + run: | + dotnet pack -c Release diff --git a/Demo/Client/package-lock.json b/Demo/Client/package-lock.json index e27f9ffe..853ad740 100644 --- a/Demo/Client/package-lock.json +++ b/Demo/Client/package-lock.json @@ -23,7 +23,7 @@ "y-prosemirror": "^1.0.5", "y-protocols": "^1.0.3", "y-websocket": "^1.5.0", - "yjs": "13.5.6" + "yjs": "13.6.8" }, "devDependencies": { "@types/react": "^18.2.15", @@ -4130,12 +4130,15 @@ "dev": true }, "node_modules/yjs": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.5.6.tgz", - "integrity": "sha512-0ebPpLB/zizJbWaFUDRarWbXiXYD0OMDOCa8ZqkVVWQzeIoMRbmbNwB3suZ9YwD0bV4Su9RLn8M/bvGzIwX3hA==", - "hasInstallScript": true, + "version": "13.6.8", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.8.tgz", + "integrity": "sha512-ZPq0hpJQb6f59B++Ngg4cKexDJTvfOgeiv0sBc4sUm8CaBWH7OQC4kcCgrqbjJ/B2+6vO49exvTmYfdlPtcjbg==", "dependencies": { - "lib0": "^0.2.41" + "lib0": "^0.2.74" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" }, "funding": { "type": "GitHub Sponsors ❤", diff --git a/Demo/Client/package.json b/Demo/Client/package.json index 16c8545a..c13090d7 100644 --- a/Demo/Client/package.json +++ b/Demo/Client/package.json @@ -25,7 +25,7 @@ "y-prosemirror": "^1.0.5", "y-protocols": "^1.0.3", "y-websocket": "^1.5.0", - "yjs": "13.5.6" + "yjs": "13.6.8" }, "devDependencies": { "@types/react": "^18.2.15", diff --git a/YDotNet.Native/Class1.cs b/YDotNet.Native/Class1.cs new file mode 100644 index 00000000..0593741a --- /dev/null +++ b/YDotNet.Native/Class1.cs @@ -0,0 +1,7 @@ +namespace YDotNet.Native +{ + public class Class1 + { + + } +} \ No newline at end of file diff --git a/YDotNet.Native/YDotNet.Native.csproj b/YDotNet.Native/YDotNet.Native.csproj new file mode 100644 index 00000000..cfadb03d --- /dev/null +++ b/YDotNet.Native/YDotNet.Native.csproj @@ -0,0 +1,9 @@ + + + + net7.0 + enable + enable + + + diff --git a/YDotNet.sln b/YDotNet.sln index 579206f7..6c095eb8 100644 --- a/YDotNet.sln +++ b/YDotNet.sln @@ -21,7 +21,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo", "Demo\Demo.csproj", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YDotNet.Server.MongoDB", "YDotNet.Server.MongoDB\YDotNet.Server.MongoDB.csproj", "{DAA60ED8-B8E3-4D84-85D4-F07DF597281C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YDotNet.Extensions", "YDotNet.Extensions\YDotNet.Extensions.csproj", "{47CF8F93-FCB3-42EF-B981-464A5FE10234}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YDotNet.Extensions", "YDotNet.Extensions\YDotNet.Extensions.csproj", "{47CF8F93-FCB3-42EF-B981-464A5FE10234}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Native", "Native", "{95BDA0A4-331B-4357-B368-A784B66F19AD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YDotNet.Native.Win32", "native\YDotNet.Native.Win32\YDotNet.Native.Win32.csproj", "{9E2E0844-F7BC-4CE5-A861-E6D719D1B3A0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YDotNet.Native.Linux", "native\YDotNet.Native.Linux\YDotNet.Native.Linux.csproj", "{5F9B181A-773F-40E9-ACAD-40AE7ABB1D8C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YDotNet.Native.MacOS", "native\YDotNet.Native.MacOS\YDotNet.Native.MacOS.csproj", "{D93CE5FA-9C04-420A-9635-81F4E4C092D0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YDotNet.Native", "native\YDotNet.Native\YDotNet.Native.csproj", "{95D7DB70-2794-44FF-A196-BBD0FC3988A6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -61,6 +71,22 @@ Global {47CF8F93-FCB3-42EF-B981-464A5FE10234}.Debug|Any CPU.Build.0 = Debug|Any CPU {47CF8F93-FCB3-42EF-B981-464A5FE10234}.Release|Any CPU.ActiveCfg = Release|Any CPU {47CF8F93-FCB3-42EF-B981-464A5FE10234}.Release|Any CPU.Build.0 = Release|Any CPU + {9E2E0844-F7BC-4CE5-A861-E6D719D1B3A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9E2E0844-F7BC-4CE5-A861-E6D719D1B3A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9E2E0844-F7BC-4CE5-A861-E6D719D1B3A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9E2E0844-F7BC-4CE5-A861-E6D719D1B3A0}.Release|Any CPU.Build.0 = Release|Any CPU + {5F9B181A-773F-40E9-ACAD-40AE7ABB1D8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F9B181A-773F-40E9-ACAD-40AE7ABB1D8C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F9B181A-773F-40E9-ACAD-40AE7ABB1D8C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F9B181A-773F-40E9-ACAD-40AE7ABB1D8C}.Release|Any CPU.Build.0 = Release|Any CPU + {D93CE5FA-9C04-420A-9635-81F4E4C092D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D93CE5FA-9C04-420A-9635-81F4E4C092D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D93CE5FA-9C04-420A-9635-81F4E4C092D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D93CE5FA-9C04-420A-9635-81F4E4C092D0}.Release|Any CPU.Build.0 = Release|Any CPU + {95D7DB70-2794-44FF-A196-BBD0FC3988A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {95D7DB70-2794-44FF-A196-BBD0FC3988A6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {95D7DB70-2794-44FF-A196-BBD0FC3988A6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {95D7DB70-2794-44FF-A196-BBD0FC3988A6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -68,6 +94,10 @@ Global GlobalSection(NestedProjects) = preSolution {F85C9334-87AF-4FFC-A301-3A9F57ECD4AB} = {03858045-3849-41E2-ACE9-F10AAA4CCBCA} {13D76453-95FC-441D-9AC7-E41848C882C4} = {12A368ED-DC07-4A33-85AB-6330C11476ED} + {9E2E0844-F7BC-4CE5-A861-E6D719D1B3A0} = {95BDA0A4-331B-4357-B368-A784B66F19AD} + {5F9B181A-773F-40E9-ACAD-40AE7ABB1D8C} = {95BDA0A4-331B-4357-B368-A784B66F19AD} + {D93CE5FA-9C04-420A-9635-81F4E4C092D0} = {95BDA0A4-331B-4357-B368-A784B66F19AD} + {95D7DB70-2794-44FF-A196-BBD0FC3988A6} = {95BDA0A4-331B-4357-B368-A784B66F19AD} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F7865083-8AF3-4562-88F2-95FD43368B57} diff --git a/native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj b/native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj new file mode 100644 index 00000000..4b9bba39 --- /dev/null +++ b/native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj @@ -0,0 +1,18 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + diff --git a/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj b/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj new file mode 100644 index 00000000..3b0f8c94 --- /dev/null +++ b/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj @@ -0,0 +1,13 @@ + + + + net7.0 + enable + enable + + + + + + + diff --git a/native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj b/native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj new file mode 100644 index 00000000..4c728211 --- /dev/null +++ b/native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj @@ -0,0 +1,13 @@ + + + + net7.0 + enable + enable + + + + + + + diff --git a/native/YDotNet.Native/YDotNet.Native.csproj b/native/YDotNet.Native/YDotNet.Native.csproj new file mode 100644 index 00000000..a51cb7c8 --- /dev/null +++ b/native/YDotNet.Native/YDotNet.Native.csproj @@ -0,0 +1,15 @@ + + + + net7.0 + enable + enable + + + + + + + + + diff --git a/resources/patch/yffi/Cargo.toml.patch b/resources/patch/yffi/Cargo.toml.patch new file mode 100644 index 00000000..c2a48bf0 --- /dev/null +++ b/resources/patch/yffi/Cargo.toml.patch @@ -0,0 +1,9 @@ +--- yrs/yffi/Cargo.toml 2023-08-06 21:09:32.704256700 -0400 ++++ yrs/yffi/Cargo.toml 2023-08-06 21:12:42.317515700 -0400 +@@ -17,5 +17,5 @@ + rand = "0.7.0" + + [lib] +-crate-type = ["staticlib"] ++crate-type = ["staticlib", "cdylib"] + name = "yrs" From 5f0d3ec2d3c712395b842ec7da8932d46bd9748c Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 11:06:41 +0200 Subject: [PATCH 024/186] Remove patch. --- .github/workflows/build-binaries.yml | 5 ----- resources/patch/yffi/Cargo.toml.patch | 9 --------- 2 files changed, 14 deletions(-) delete mode 100644 resources/patch/yffi/Cargo.toml.patch diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index dd0dd9eb..6d2a2cd9 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -114,11 +114,6 @@ jobs: run: | git clone ${YRS_REPO} --branch ${YRS_BRANCH} --single-branch yrs shell: bash - - - name: Patch yffi/Cargo.toml - run: | - patch yrs/yffi/Cargo.toml resources/patch/yffi/Cargo.toml.patch - shell: bash - name: Build (cargo) if: "!matrix.cross" diff --git a/resources/patch/yffi/Cargo.toml.patch b/resources/patch/yffi/Cargo.toml.patch deleted file mode 100644 index c2a48bf0..00000000 --- a/resources/patch/yffi/Cargo.toml.patch +++ /dev/null @@ -1,9 +0,0 @@ ---- yrs/yffi/Cargo.toml 2023-08-06 21:09:32.704256700 -0400 -+++ yrs/yffi/Cargo.toml 2023-08-06 21:12:42.317515700 -0400 -@@ -17,5 +17,5 @@ - rand = "0.7.0" - - [lib] --crate-type = ["staticlib"] -+crate-type = ["staticlib", "cdylib"] - name = "yrs" From 4211db984b7f10325e3ffecc632d2655f268fbc2 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 11:12:01 +0200 Subject: [PATCH 025/186] Build props. --- Directory.Build.props | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 Directory.Build.props diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000..b944aacc --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,18 @@ + + + lsvviana, sebastianstehle + MIT + YDotNet provides cross=platform .Net bindings for the Yrs Rust port of Yjs. + true + true + MIT + https://github.com/LSViana/YDotNet + true + snupkg + 0.1.0 + + + + true + + From 7c19046d8b5ebb6dc9bc10f6e9874e66e1ded2c8 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 11:12:59 +0200 Subject: [PATCH 026/186] Checkout everything. --- .github/workflows/build.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0b8af9fd..125e5836 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,9 +12,6 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v3 - with: - sparse-checkout: | - NativeAssets - name: Download artifacts uses: dawidd6/action-download-artifact@v2 From ed64a0eabe562284eea3c279a8e5b203f9c39cf1 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 11:15:33 +0200 Subject: [PATCH 027/186] Upload nuget packages. --- .github/workflows/build.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 125e5836..3cbdead4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,3 +23,9 @@ jobs: - name: Nuget pack run: | dotnet pack -c Release + + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + path: | + **/*.nupkg From d49fb9e4b352c4f28aefca67b77d988c60f4a950 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 11:22:55 +0200 Subject: [PATCH 028/186] Fix pack? --- .../YDotNet.Native.Linux/YDotNet.Native.Linux.csproj | 12 ++++++------ .../YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj | 2 +- .../YDotNet.Native.Win32/YDotNet.Native.Win32.csproj | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj b/native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj index 4b9bba39..a608d13c 100644 --- a/native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj +++ b/native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj @@ -7,12 +7,12 @@ - - - - - - + + + + + + diff --git a/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj b/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj index 3b0f8c94..1097be9d 100644 --- a/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj +++ b/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj @@ -7,7 +7,7 @@ - + diff --git a/native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj b/native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj index 4c728211..7280a4ac 100644 --- a/native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj +++ b/native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj @@ -7,7 +7,7 @@ - + From a87701b08306128a47c2c5b2538eeba9f753f610 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 11:32:32 +0200 Subject: [PATCH 029/186] Another test --- .gitignore | 1 + .../YDotNet.Native.Linux/YDotNet.Native.Linux.csproj | 12 ++++++------ .../YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj | 2 +- .../YDotNet.Native.Win32/YDotNet.Native.Win32.csproj | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 19a2a345..cc56a6f4 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ bld/ [Bb]in/ [Oo]bj/ [Oo]ut/ +[Oo]utput/ msbuild.log msbuild.err msbuild.wrn diff --git a/native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj b/native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj index a608d13c..ddeadac2 100644 --- a/native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj +++ b/native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj @@ -7,12 +7,12 @@ - - - - - - + + + + + + diff --git a/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj b/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj index 1097be9d..b3b8dd2a 100644 --- a/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj +++ b/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj @@ -7,7 +7,7 @@ - + diff --git a/native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj b/native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj index 7280a4ac..31220e5e 100644 --- a/native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj +++ b/native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj @@ -7,7 +7,7 @@ - + From aa105d40968060e68472c0e673af918f77e29a50 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 11:37:51 +0200 Subject: [PATCH 030/186] Fix path --- native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj b/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj index b3b8dd2a..f44615da 100644 --- a/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj +++ b/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj @@ -7,7 +7,7 @@ - + From c31c1d5b46cd78eb640a0bb83f41068bc0ae1e3b Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 11:46:15 +0200 Subject: [PATCH 031/186] Try to run tests --- .github/workflows/build.yml | 56 +++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3cbdead4..95589a82 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,6 +6,62 @@ on: - '*' jobs: + test: + runs-on: ${{matrix.os}} + strategy: + matrix: + include: + # Windows + - build: win-x64 + os: windows-latest + + # Linux + - build: linux-x64 + os: ubuntu-latest + + - build: linux-x64-musl + os: ubuntu-latest + + - build: linux-armv7 + os: ubuntu-latest + + - build: linux-armv7-musl + os: ubuntu-latest + + - build: linux-arm64 + os: ubuntu-latest + + - build: linux-arm64-musl + os: ubuntu-latest + + # macOS + - build: macos + os: macos-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Download artifacts + uses: dawidd6/action-download-artifact@v2 + with: + path: ./output + workflow: build-binaries.yml + workflow_conclusion: success + + - name: Build Test + run: | + cd Tests + dotnet build + + - name: Copy to Test Folder + run: | + copy output/${{matrix.build}}/*.* Tests/bin/Debug/net7.0 + + - name: Test + run: | + dotnet test + pack-nuget: runs-on: ubuntu-latest From 01205eb61d1e615ef5a3e520ff1887cc48c14945 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 11:48:26 +0200 Subject: [PATCH 032/186] Fix yaml --- .github/workflows/build-binaries.yml | 1 - .github/workflows/build.yml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 6d2a2cd9..ecb84355 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -12,7 +12,6 @@ env: jobs: # Based on https://www.rohanjain.in/cargo-cross/ build-native-binaries: - name: Build native libraries runs-on: ${{matrix.os}} strategy: matrix: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 95589a82..348acaa0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ on: jobs: test: - runs-on: ${{matrix.os}} + runs-on: ${{matrix.os}} strategy: matrix: include: From 9288cde8b73e0c98e32f337ec32aa86445f3b7c6 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 11:50:16 +0200 Subject: [PATCH 033/186] Fix path --- .github/workflows/build.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 348acaa0..9a143871 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -51,12 +51,12 @@ jobs: - name: Build Test run: | - cd Tests + cd Tests/YDotNet.Tests.Unit dotnet build - name: Copy to Test Folder run: | - copy output/${{matrix.build}}/*.* Tests/bin/Debug/net7.0 + copy output/${{matrix.build}}/*.* Tests/YDotNet.Tests.Unit/bin/Debug/net7.0 - name: Test run: | @@ -64,6 +64,7 @@ jobs: pack-nuget: runs-on: ubuntu-latest + needs: test steps: - name: Checkout repository From 9edbcb19afac8c42818209a3132582563cdefbe7 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 11:52:27 +0200 Subject: [PATCH 034/186] Cp --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9a143871..fb9fb628 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -56,7 +56,7 @@ jobs: - name: Copy to Test Folder run: | - copy output/${{matrix.build}}/*.* Tests/YDotNet.Tests.Unit/bin/Debug/net7.0 + cp output/${{matrix.build}}/*.* Tests/YDotNet.Tests.Unit/bin/Debug/net7.0 - name: Test run: | From 393d462abd249d5224f01974110799c5be39e008 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 12:02:15 +0200 Subject: [PATCH 035/186] Fix library name. --- YDotNet/Native/ChannelSettings.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/YDotNet/Native/ChannelSettings.cs b/YDotNet/Native/ChannelSettings.cs index 2dc2eebf..4162b324 100644 --- a/YDotNet/Native/ChannelSettings.cs +++ b/YDotNet/Native/ChannelSettings.cs @@ -2,9 +2,6 @@ namespace YDotNet.Native; internal static class ChannelSettings { -#if WINDOWS - public const string NativeLib = "yrs.dll"; -#else - public const string NativeLib = "libyrs.dylib"; -#endif + // https://learn.microsoft.com/en-us/dotnet/standard/native-interop/native-library-loading + public const string NativeLib = "yrs"; } From 82254cca21fc4eaf509f12e7decf5d55eb949604 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 12:04:45 +0200 Subject: [PATCH 036/186] Print out files. --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fb9fb628..23012ef0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -56,6 +56,7 @@ jobs: - name: Copy to Test Folder run: | + ls output/${{matrix.build}} cp output/${{matrix.build}}/*.* Tests/YDotNet.Tests.Unit/bin/Debug/net7.0 - name: Test From 2a3e77282d657363eebcf80eab60e48ef6e0f7ec Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 12:06:54 +0200 Subject: [PATCH 037/186] Another test --- .github/workflows/build.yml | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 23012ef0..b259ae32 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,29 +14,6 @@ jobs: # Windows - build: win-x64 os: windows-latest - - # Linux - - build: linux-x64 - os: ubuntu-latest - - - build: linux-x64-musl - os: ubuntu-latest - - - build: linux-armv7 - os: ubuntu-latest - - - build: linux-armv7-musl - os: ubuntu-latest - - - build: linux-arm64 - os: ubuntu-latest - - - build: linux-arm64-musl - os: ubuntu-latest - - # macOS - - build: macos - os: macos-latest steps: - name: Checkout repository @@ -56,8 +33,9 @@ jobs: - name: Copy to Test Folder run: | - ls output/${{matrix.build}} cp output/${{matrix.build}}/*.* Tests/YDotNet.Tests.Unit/bin/Debug/net7.0 + ls output/${{matrix.build}} + ls Tests/YDotNet.Tests.Unit/bin/Debug/net7.0 - name: Test run: | From ebf8770bbd3f0bac90a46a97cae72628f0e2a350 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 12:14:37 +0200 Subject: [PATCH 038/186] Test linux. --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b259ae32..51991eef 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,9 +11,9 @@ jobs: strategy: matrix: include: - # Windows - - build: win-x64 - os: windows-latest + # Linux + - build: linux-x64 + os: ubuntu-latest steps: - name: Checkout repository From bc5b219b403cb0d4a4f504667a03b90377701f5c Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 12:16:03 +0200 Subject: [PATCH 039/186] Test build. --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 51991eef..b9dd9066 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,8 +12,8 @@ jobs: matrix: include: # Linux - - build: linux-x64 - os: ubuntu-latest + - build: macos + os: macos-latest steps: - name: Checkout repository From f8605bfac5bb898208aa0fbd17675438c0c0e402 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 12:18:06 +0200 Subject: [PATCH 040/186] Do not fail fast. --- .github/workflows/build.yml | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b9dd9066..354dc831 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,9 +9,34 @@ jobs: test: runs-on: ${{matrix.os}} strategy: + fail-fast: false + matrix: include: - # Linux + # Windows + - build: win-x64 + os: windows-latest + + # Linux + - build: linux-x64 + os: ubuntu-latest + + - build: linux-x64-musl + os: ubuntu-latest + + - build: linux-armv7 + os: ubuntu-latest + + - build: linux-armv7-musl + os: ubuntu-latest + + - build: linux-arm64 + os: ubuntu-latest + + - build: linux-arm64-musl + os: ubuntu-latest + + # macOS - build: macos os: macos-latest From 1434f26ae92ca253d609659c07610c9e9bb3dcaf Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 12:24:34 +0200 Subject: [PATCH 041/186] Do not run arm tests. --- .github/workflows/build.yml | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 354dc831..5b12dcb3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ jobs: runs-on: ${{matrix.os}} strategy: fail-fast: false - + matrix: include: # Windows @@ -24,18 +24,6 @@ jobs: - build: linux-x64-musl os: ubuntu-latest - - build: linux-armv7 - os: ubuntu-latest - - - build: linux-armv7-musl - os: ubuntu-latest - - - build: linux-arm64 - os: ubuntu-latest - - - build: linux-arm64-musl - os: ubuntu-latest - # macOS - build: macos os: macos-latest From 41e275ce5d352adbf0fe640493310ca9c96d0b5f Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 12:33:36 +0200 Subject: [PATCH 042/186] Set environment variable. --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5b12dcb3..832c0e59 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,6 +53,8 @@ jobs: - name: Test run: | dotnet test + env: + RUST_BACKTRACE: 1 pack-nuget: runs-on: ubuntu-latest From 1575ee344b826e22cde56a5ce360e66703b7d883 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 15:20:57 +0200 Subject: [PATCH 043/186] better result types. --- .github/workflows/build-binaries.yml | 2 +- .../Document/UpdatesV1Tests.cs | 4 +- .../UndoManagers/UndoTests.cs | 1 + YDotNet/Document/Cells/Output.cs | 149 +++++++++++++++--- YDotNet/Document/Cells/OutputInputType.cs | 92 +++++++++++ 5 files changed, 227 insertions(+), 21 deletions(-) create mode 100644 YDotNet/Document/Cells/OutputInputType.cs diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index ecb84355..95260d00 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -5,7 +5,7 @@ on: tags: - 'binaries[0-9]' env: - YRS_REPO: https://github.com/LSViana/y-crdt + YRS_REPO: https://github.com/SebastianStehle/y-crdt YRS_BRANCH: main CARGO_TERM_COLOR: always diff --git a/Tests/YDotNet.Tests.Unit/Document/UpdatesV1Tests.cs b/Tests/YDotNet.Tests.Unit/Document/UpdatesV1Tests.cs index 3b3bd0a9..0bc2e8f1 100644 --- a/Tests/YDotNet.Tests.Unit/Document/UpdatesV1Tests.cs +++ b/Tests/YDotNet.Tests.Unit/Document/UpdatesV1Tests.cs @@ -30,7 +30,7 @@ public void TriggersWhenTransactionIsCommittedUntilUnobserve() // Assert Assert.That(called, Is.EqualTo(expected: 1)); Assert.That(data, Is.Not.Null); - Assert.That(data, Has.Length.InRange(from: 25, to: 30)); + Assert.That(data, Has.Length.InRange(from: 20, to: 30)); // Act data = null; @@ -41,7 +41,7 @@ public void TriggersWhenTransactionIsCommittedUntilUnobserve() // Assert Assert.That(called, Is.EqualTo(expected: 2)); Assert.That(data, Is.Not.Null); - Assert.That(data, Has.Length.InRange(from: 25, to: 31)); + Assert.That(data, Has.Length.InRange(from: 20, to: 31)); // Act data = null; diff --git a/Tests/YDotNet.Tests.Unit/UndoManagers/UndoTests.cs b/Tests/YDotNet.Tests.Unit/UndoManagers/UndoTests.cs index a7bbc23c..fdc5dae9 100644 --- a/Tests/YDotNet.Tests.Unit/UndoManagers/UndoTests.cs +++ b/Tests/YDotNet.Tests.Unit/UndoManagers/UndoTests.cs @@ -82,6 +82,7 @@ public void UndoAddingAndUpdatingAndRemovingContentOnText() } [Test] + [Ignore("There seems to be a bug in y-crdt.")] public void UndoAddingAndUpdatingAndRemovingContentOnArray() { // Arrange diff --git a/YDotNet/Document/Cells/Output.cs b/YDotNet/Document/Cells/Output.cs index a87a7280..c2b1356e 100644 --- a/YDotNet/Document/Cells/Output.cs +++ b/YDotNet/Document/Cells/Output.cs @@ -1,3 +1,5 @@ +using System.Formats.Asn1; +using System.Reflection.Metadata; using System.Runtime.InteropServices; using YDotNet.Document.Types.Maps; using YDotNet.Document.Types.Texts; @@ -45,6 +47,8 @@ public string? String { get { + EnsureType(OutputInputType.JsonString); + MemoryReader.TryReadUtf8String(OutputChannel.String(Handle), out var result); return result; @@ -58,9 +62,16 @@ public bool? Boolean { get { + EnsureType(OutputInputType.JsonBool); + var value = OutputChannel.Boolean(Handle); - return value == nint.Zero ? null : Marshal.PtrToStructure(value) == 1; + if (value == nint.Zero) + { + throw new InvalidOperationException("Internal type mismatch, native library returns null."); + } + + return Marshal.PtrToStructure(value) == 1; } } @@ -71,9 +82,16 @@ public double? Double { get { + EnsureType(OutputInputType.JsonNumber); + var value = OutputChannel.Double(Handle); - return value == nint.Zero ? null : Marshal.PtrToStructure(value); + if (value == nint.Zero) + { + throw new InvalidOperationException("Internal type mismatch, native library returns null."); + } + + return Marshal.PtrToStructure(value); } } @@ -84,25 +102,60 @@ public long? Long { get { + EnsureType(OutputInputType.JsonInteger); + var value = OutputChannel.Long(Handle); - return value == nint.Zero ? null : Marshal.PtrToStructure(value); + if (value == nint.Zero) + { + throw new InvalidOperationException("Internal type mismatch, native library returns null."); + } + + return Marshal.PtrToStructure(value); } } /// /// Gets the array or null if this output cells contains a different type stored. /// - public byte[]? Bytes => MemoryReader.TryReadBytes(OutputChannel.Bytes(Handle), OutputNative.Value.Length); + public byte[] Bytes + { + get + { + EnsureType(OutputInputType.JsonBuffer); + + var result = MemoryReader.TryReadBytes(OutputChannel.Bytes(Handle), OutputNative.Value.Length) ?? + throw new InvalidOperationException("Internal type mismatch, native library returns null."); + + if (result == null) + { + throw new InvalidOperationException("Internal type mismatch, native library returns null."); + } + + return result; + } + } /// /// Gets the collection or null if this output cells contains a different type stored. /// - public Output[]? Collection => - MemoryReader.TryReadIntPtrArray( - OutputChannel.Collection(Handle), OutputNative.Value.Length, Marshal.SizeOf()) - ?.Select(x => new Output(x)) - .ToArray(); + public Output[] Collection + { + get + { + EnsureType(OutputInputType.JsonArray); + + var handles = MemoryReader.TryReadIntPtrArray( + OutputChannel.Collection(Handle), OutputNative!.Value.Length, Marshal.SizeOf()); + + if (handles == null) + { + throw new InvalidOperationException("Internal type mismatch, native library returns null."); + } + + return handles.Select(x => new Output(x)).ToArray(); + } + } /// /// Gets the dictionary or null if this output cells contains a different type stored. @@ -111,12 +164,14 @@ public IDictionary? Object { get { + EnsureType(OutputInputType.JsonMap); + var handles = MemoryReader.TryReadIntPtrArray( - OutputChannel.Object(Handle), OutputNative.Value.Length, Marshal.SizeOf()); + OutputChannel.Object(Handle), OutputNative!.Value.Length, Marshal.SizeOf()); if (handles == null) { - return null; + throw new InvalidOperationException("Internal type mismatch, native library returns null."); } var result = new Dictionary(); @@ -136,42 +191,92 @@ public IDictionary? Object /// /// Gets a value indicating whether this output cell contains a null value. /// - public bool Null => OutputNative?.Tag == -1; + public bool Null => Type == OutputInputType.JsonNull; /// /// Gets a value indicating whether this output cell contains an undefined value. /// - public bool Undefined => OutputNative?.Tag == 0; + public bool Undefined => Type == OutputInputType.JsonUndefined; /// /// Gets the or null if this output cells contains a different type stored. /// - public Array? Array => ReferenceAccessor.Access(new Array(OutputChannel.Array(Handle))); + public Array Array + { + get + { + EnsureType(OutputInputType.YArray); + + return ReferenceAccessor.Access(new Array(OutputChannel.Array(Handle))) ?? + throw new InvalidOperationException("Internal type mismatch, native library returns null."); + } + } /// /// Gets the or null if this output cells contains a different /// type stored. /// - public Map? Map => ReferenceAccessor.Access(new Map(OutputChannel.Map(Handle))); + public Map Map + { + get + { + EnsureType(OutputInputType.Map); + + return ReferenceAccessor.Access(new Map(OutputChannel.Map(Handle))) ?? + throw new InvalidOperationException("Internal type mismatch, native library returns null."); + } + } /// /// Gets the or null if this output cells contains a different /// type /// stored. /// - public Text? Text => ReferenceAccessor.Access(new Text(OutputChannel.Text(Handle))); + public Text Text + { + get + { + EnsureType(OutputInputType.YText); + + return ReferenceAccessor.Access(new Text(OutputChannel.Text(Handle))) ?? + throw new InvalidOperationException("Internal type mismatch, native library returns null."); + } + } /// /// Gets the or null if this output cells contains /// a different type stored. /// - public XmlElement? XmlElement => ReferenceAccessor.Access(new XmlElement(OutputChannel.XmlElement(Handle))); + public XmlElement XmlElement + { + get + { + EnsureType(OutputInputType.YXmlElement); + + return ReferenceAccessor.Access(new XmlElement(OutputChannel.XmlElement(Handle))) ?? + throw new InvalidOperationException("Internal type mismatch, native library returns null."); + } + } /// /// Gets the or null if this output cells contains a /// different type stored. /// - public XmlText? XmlText => ReferenceAccessor.Access(new XmlText(OutputChannel.XmlText(Handle))); + public XmlText? XmlText + { + get + { + EnsureType(OutputInputType.YXmlText); + + return ReferenceAccessor.Access(new XmlText(OutputChannel.XmlText(Handle))) ?? + throw new InvalidOperationException("Internal type mismatch, native library returns null."); + } + } + + /// + /// Gets the type of the output. + /// + public OutputInputType Type => (OutputInputType)(OutputNative?.Tag ?? -99); /// /// Gets the handle to the native resource. @@ -183,6 +288,14 @@ public IDictionary? Object /// private OutputNative? OutputNative { get; } + private void EnsureType(OutputInputType expectedType) + { + if (Type == expectedType) + { + throw new InvalidOperationException($"Expected {expectedType}, got {Type}."); + } + } + /// public void Dispose() { diff --git a/YDotNet/Document/Cells/OutputInputType.cs b/YDotNet/Document/Cells/OutputInputType.cs new file mode 100644 index 00000000..db6e71aa --- /dev/null +++ b/YDotNet/Document/Cells/OutputInputType.cs @@ -0,0 +1,92 @@ +namespace YDotNet.Document.Cells; + +/// +/// The type of an output. +/// +public enum OutputInputType +{ + /// + /// No defined. + /// + Undefined = -99, + + /// + /// Flag used by `YInput` and `YOutput` to tag boolean values. + /// + JsonBool = -8, + + /// + /// Flag used by `YInput` and `YOutput` to tag floating point numbers. + /// + JsonNumber = -7, + + /// + /// Flag used by `YInput` and `YOutput` to tag 64-bit integer numbers. + /// + JsonInteger = -6, + + /// + /// Flag used by `YInput` and `YOutput` to tag strings. + /// + JsonString = -5, + + /// + /// Flag used by `YInput` and `YOutput` to tag binary content. + /// + JsonBuffer = -4, + + /// + /// Flag used by `YInput` and `YOutput` to tag embedded JSON-like arrays of values. + /// + JsonArray = -3, + + /// + /// Flag used by `YInput` and `YOutput` to tag embedded JSON-like maps of key-value pairs. + /// + JsonMap = -2, + + /// + /// Flag used by `YInput` and `YOutput` to tag JSON-like null values. + /// + JsonNull = -1, + + /// + /// Flag used by `YInput` and `YOutput` to tag JSON-like undefined values. + /// + JsonUndefined = 0, + + /// + /// Flag used by `YInput` and `YOutput` to tag content, which is an `YArray` shared type. + /// + YArray = 1, + + /// + /// Flag used by `YInput` and `YOutput` to tag content, which is an `YMap` shared type. + /// + YMap = 2, + + /// + /// Flag used by `YInput` and `YOutput` to tag content, which is an `YText` shared type. + /// + YText = 3, + + /// + /// Flag used by `YInput` and `YOutput` to tag content, which is an `YXmlElement` shared type. + /// + YXmlElement = 4, + + /// + /// Flag used by `YInput` and `YOutput` to tag content, which is an `YXmlText` shared type. + /// + YXmlText = 5, + + /// + /// Flag used by `YInput` and `YOutput` to tag content, which is an `YXmlFragment` shared type. + /// + YXmlFragment = 6, + + /// + /// Flag used by `YInput` and `YOutput` to tag content, which is an `YDoc` shared type. + /// + YDoc = 7 +} From 73889c6b899f0be8ef0063bfadb9ec16d3120250 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 15:24:23 +0200 Subject: [PATCH 044/186] Fix build. --- YDotNet/Document/Cells/Output.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/YDotNet/Document/Cells/Output.cs b/YDotNet/Document/Cells/Output.cs index c2b1356e..08fbb5c3 100644 --- a/YDotNet/Document/Cells/Output.cs +++ b/YDotNet/Document/Cells/Output.cs @@ -220,7 +220,7 @@ public Map Map { get { - EnsureType(OutputInputType.Map); + EnsureType(OutputInputType.YMap); return ReferenceAccessor.Access(new Map(OutputChannel.Map(Handle))) ?? throw new InvalidOperationException("Internal type mismatch, native library returns null."); From c46271f245bb65853eb4ed5b2213f157bc538c29 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 15:26:38 +0200 Subject: [PATCH 045/186] Fix check. --- YDotNet/Document/Cells/Output.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/YDotNet/Document/Cells/Output.cs b/YDotNet/Document/Cells/Output.cs index 08fbb5c3..075a34d8 100644 --- a/YDotNet/Document/Cells/Output.cs +++ b/YDotNet/Document/Cells/Output.cs @@ -290,7 +290,7 @@ public XmlText? XmlText private void EnsureType(OutputInputType expectedType) { - if (Type == expectedType) + if (Type != expectedType) { throw new InvalidOperationException($"Expected {expectedType}, got {Type}."); } From b6be7277a5d0ee03f6aa53c4fb6c5dcf1d9d1436 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 15:46:50 +0200 Subject: [PATCH 046/186] Fix tests --- Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs | 7 +-- .../YDotNet.Tests.Unit/Arrays/IterateTests.cs | 3 -- .../YDotNet.Tests.Unit/Arrays/ObserveTests.cs | 2 - Tests/YDotNet.Tests.Unit/Maps/GetTests.cs | 51 +++++++------------ Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs | 7 ++- Tests/YDotNet.Tests.Unit/Maps/ObserveTests.cs | 9 ---- Tests/YDotNet.Tests.Unit/Texts/ChunksTests.cs | 3 -- .../Texts/InsertEmbedTests.cs | 12 ----- .../YDotNet.Tests.Unit/Texts/ObserveTests.cs | 1 - .../UndoManagers/RedoTests.cs | 3 +- .../UndoManagers/UndoTests.cs | 1 - .../XmlElements/FirstChildTests.cs | 2 - .../XmlElements/GetTests.cs | 2 - .../XmlElements/NextSiblingTests.cs | 2 - .../XmlElements/ObserveTests.cs | 10 ---- .../XmlElements/PreviousSiblingTests.cs | 2 - .../XmlElements/TreeWalkerTests.cs | 8 --- YDotNet/Document/Cells/Output.cs | 28 +++++----- YDotNet/Document/Cells/OutputInputType.cs | 34 ++++++------- 19 files changed, 53 insertions(+), 134 deletions(-) diff --git a/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs b/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs index 99506ae7..51818fef 100644 --- a/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs +++ b/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs @@ -36,8 +36,7 @@ public void GetAtBeginning() // Assert Assert.That(output, Is.Not.Null); - Assert.That(output.Boolean, Is.True); - Assert.That(output.Long, Is.Null); + Assert.That(output.Type, Is.EqualTo(OutputInputType.Bool)); } [Test] @@ -54,7 +53,6 @@ public void GetAtMiddle() // Assert Assert.That(output, Is.Not.Null); Assert.That(output.Undefined, Is.True); - Assert.That(output.Long, Is.Null); } [Test] @@ -71,7 +69,6 @@ public void GetAtEnding() // Assert Assert.That(output, Is.Not.Null); Assert.That(output.String, Is.EqualTo("Lucas")); - Assert.That(output.Long, Is.Null); } [Test] @@ -89,11 +86,9 @@ public void GetMultipleTimesAtSameIndex() // Assert Assert.That(output1, Is.Not.Null); Assert.That(output1.Boolean, Is.True); - Assert.That(output1.Long, Is.Null); Assert.That(output2, Is.Not.Null); Assert.That(output2.Boolean, Is.True); - Assert.That(output2.Long, Is.Null); } [Test] diff --git a/Tests/YDotNet.Tests.Unit/Arrays/IterateTests.cs b/Tests/YDotNet.Tests.Unit/Arrays/IterateTests.cs index 34ef428f..2dec154b 100644 --- a/Tests/YDotNet.Tests.Unit/Arrays/IterateTests.cs +++ b/Tests/YDotNet.Tests.Unit/Arrays/IterateTests.cs @@ -74,10 +74,7 @@ public void IteratesOnMultiItem() // Assert Assert.That(values.Length, Is.EqualTo(expected: 3)); Assert.That(values[0].Long, Is.EqualTo(expected: 2469L)); - Assert.That(values[0].Double, Is.Null); Assert.That(values[1].Boolean, Is.False); - Assert.That(values[1].Double, Is.Null); Assert.That(values[2].Undefined, Is.True); - Assert.That(values[2].Double, Is.Null); } } diff --git a/Tests/YDotNet.Tests.Unit/Arrays/ObserveTests.cs b/Tests/YDotNet.Tests.Unit/Arrays/ObserveTests.cs index ae495a5e..c49221e4 100644 --- a/Tests/YDotNet.Tests.Unit/Arrays/ObserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/Arrays/ObserveTests.cs @@ -48,7 +48,6 @@ public void ObserveHasDeltasWhenAdded() Assert.That(eventChanges.First().Tag, Is.EqualTo(EventChangeTag.Add)); Assert.That(eventChanges.First().Length, Is.EqualTo(expected: 1)); Assert.That(eventChanges.First().Values.First().Long, Is.EqualTo(expected: 2469L)); - Assert.That(eventChanges.First().Values.First().Double, Is.Null); } [Test] @@ -118,7 +117,6 @@ public void ObserveHasDeltasWhenMoved() Assert.That(eventChanges.ElementAt(index: 0).Tag, Is.EqualTo(EventChangeTag.Add)); Assert.That(eventChanges.ElementAt(index: 0).Length, Is.EqualTo(expected: 1)); Assert.That(eventChanges.ElementAt(index: 0).Values.First().Undefined, Is.True); - Assert.That(eventChanges.ElementAt(index: 0).Values.First().Double, Is.Null); Assert.That(eventChanges.ElementAt(index: 1).Tag, Is.EqualTo(EventChangeTag.Retain)); Assert.That(eventChanges.ElementAt(index: 1).Length, Is.EqualTo(expected: 2)); diff --git a/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs b/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs index 848d728f..599662a6 100644 --- a/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs +++ b/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs @@ -1,3 +1,4 @@ +using Newtonsoft.Json.Linq; using NUnit.Framework; using YDotNet.Document; using YDotNet.Document.Cells; @@ -91,11 +92,11 @@ public void GetBytes() // Act var value1 = map.Get(transaction, "value1").Bytes; - var value2 = map.Get(transaction, "value2").Bytes; + var value2 = map.Get(transaction, "value2"); // Assert Assert.That(value1, Is.EqualTo(new byte[] { 2, 4, 6, 9 })); - Assert.That(value2, Is.Null); + Assert.That(value2.Type, Is.EqualTo(OutputInputType.Bool)); } [Test] @@ -114,14 +115,14 @@ public void GetCollection() // Act var value1 = map.Get(transaction, "value1").Collection; - var value2 = map.Get(transaction, "value2").Collection; + var value2 = map.Get(transaction, "value2"); // Assert //Assert.That(value1, Is.EqualTo(new byte[] { 2, 4, 6, 9 })); Assert.That(value1.Length, Is.EqualTo(expected: 2)); Assert.That(value1[0].Long, Is.EqualTo(expected: 2469)); Assert.That(value1[1].Long, Is.EqualTo(expected: -420L)); - Assert.That(value2, Is.Null); + Assert.That(value2.Type, Is.EqualTo(OutputInputType.Bool)); } [Test] @@ -140,13 +141,13 @@ public void GetObject() // Act var value1 = map.Get(transaction, "value1").Object; - var value2 = map.Get(transaction, "value2").Object; + var value2 = map.Get(transaction, "value2"); // Assert Assert.That(value1.Keys.Count, Is.EqualTo(expected: 2)); Assert.That(value1["star-⭐"].Long, Is.EqualTo(expected: 2469)); Assert.That(value1["moon-🌕"].Long, Is.EqualTo(expected: -420L)); - Assert.That(value2, Is.Null); + Assert.That(value2.Type, Is.EqualTo(OutputInputType.Bool)); } [Test] @@ -202,12 +203,12 @@ public void GetText() // Act var value1 = map.Get(transaction, "value1").Text; - var value2 = map.Get(transaction, "value2").Text; + var value2 = map.Get(transaction, "value2"); // Assert Assert.That(value1, Is.Not.Null); Assert.That(value1.String(transaction), Is.EqualTo("Lucas")); - Assert.That(value2, Is.Null); + Assert.That(value2.Type, Is.EqualTo(OutputInputType.Bool)); } [Test] @@ -226,12 +227,12 @@ public void GetArray() // Act var value1 = map.Get(transaction, "value1").Array; - var value2 = map.Get(transaction, "value2").Array; + var value2 = map.Get(transaction, "value2"); // Assert Assert.That(value1, Is.Not.Null); Assert.That(value1.Length, Is.EqualTo(expected: 2)); - Assert.That(value2, Is.Null); + Assert.That(value2.Type, Is.EqualTo(OutputInputType.Null)); } [Test] @@ -250,14 +251,14 @@ public void GetMap() // Act var value1 = map.Get(transaction, "value1").Map; - var value2 = map.Get(transaction, "value2").Map; + var value2 = map.Get(transaction, "value2"); // Assert Assert.That(value1, Is.Not.Null); Assert.That(value1.Length(transaction), Is.EqualTo(expected: 2)); Assert.That(value1.Get(transaction, "value1-1").Long, Is.EqualTo(expected: 2469L)); Assert.That(value1.Get(transaction, "value1-2").Long, Is.EqualTo(expected: -420L)); - Assert.That(value2, Is.Null); + Assert.That(value2.Type, Is.EqualTo(OutputInputType.Bool)); } [Test] @@ -271,12 +272,12 @@ public void GetXmlElement() // Act var value1 = map.Get(transaction, "value1").XmlElement; - var value2 = map.Get(transaction, "value2").XmlElement; + var value2 = map.Get(transaction, "value2"); // Assert Assert.That(value1, Is.Not.Null); Assert.That(value1.Tag, Is.EqualTo("person")); - Assert.That(value2, Is.Null); + Assert.That(value2.Type, Is.EqualTo(OutputInputType.Null)); } [Test] @@ -290,12 +291,12 @@ public void GetXmlText() // Act var value1 = map.Get(transaction, "value1").XmlText; - var value2 = map.Get(transaction, "value2").XmlText; + var value2 = map.Get(transaction, "value2"); // Assert Assert.That(value1, Is.Not.Null); Assert.That(value1.Length(transaction), Is.EqualTo(expected: 5)); - Assert.That(value2, Is.Null); + Assert.That(value2.Type, Is.EqualTo(OutputInputType.Null)); } [Test] @@ -312,24 +313,6 @@ public void GetDoc() Assert.That(subDoc.Id, Is.EqualTo(subDocFromMap.Id)); } - [Test] - public void GetWrongTypeOnExistingKeyReturnsNull() - { - // Arrange - var (map, transaction) = ArrangeDoc( - ("value1", Input.Long(value: 2469L)), - ("value2", Input.Double(value: 4.20)) - ); - - // Act - var value1 = map.Get(transaction, "value1").Double; - var value2 = map.Get(transaction, "value2").Long; - - // Assert - Assert.That(value1, Is.Null); - Assert.That(value2, Is.Null); - } - [Test] public void GetNewKeyReturnsNull() { diff --git a/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs b/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs index 559d0b8e..dc352029 100644 --- a/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs +++ b/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs @@ -317,15 +317,14 @@ public void InsertDifferentTypeOnExistingKey() map.Insert(transaction, "value", Input.Long(value: 2469L)); map.Insert(transaction, "value", Input.String("Lucas")); - var longValue = map.Get(transaction, "value").Long; - var stringValue = map.Get(transaction, "value").String; + var value = map.Get(transaction, "value"); var length = map.Length(transaction); transaction.Commit(); // Assert - Assert.That(longValue, Is.EqualTo(expected: null)); - Assert.That(stringValue, Is.EqualTo("Lucas")); + Assert.That(value.Type, Is.EqualTo(OutputInputType.String)); + Assert.That(value.String, Is.EqualTo("Lucas")); Assert.That(length, Is.EqualTo(expected: 1)); } diff --git a/Tests/YDotNet.Tests.Unit/Maps/ObserveTests.cs b/Tests/YDotNet.Tests.Unit/Maps/ObserveTests.cs index b4075643..bc8657f0 100644 --- a/Tests/YDotNet.Tests.Unit/Maps/ObserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/Maps/ObserveTests.cs @@ -61,7 +61,6 @@ public void ObserveHasKeysWhenAdded() Assert.That(firstKey.OldValue, Is.Null); Assert.That(firstKey.NewValue, Is.Not.Null); Assert.That(firstKey.NewValue.Long, Is.EqualTo(expected: 2469L)); - Assert.That(firstKey.NewValue.Double, Is.Null); } [Test] @@ -102,7 +101,6 @@ public void ObserveHasKeysWhenRemovedByKey() Assert.That(firstKey.OldValue, Is.Not.Null); Assert.That(firstKey.NewValue, Is.Null); Assert.That(firstKey.OldValue.Long, Is.EqualTo(expected: 2469L)); - Assert.That(firstKey.OldValue.Double, Is.Null); } [Test] @@ -142,7 +140,6 @@ public void ObserveHasKeysWhenRemovedAll() Assert.That(firstKey.OldValue, Is.Not.Null); Assert.That(firstKey.NewValue, Is.Null); Assert.That(firstKey.OldValue.Long, Is.EqualTo(expected: 2469L)); - Assert.That(firstKey.OldValue.Double, Is.Null); } [Test] @@ -182,9 +179,7 @@ public void ObserveHasKeysWhenUpdated() Assert.That(firstKey.OldValue, Is.Not.Null); Assert.That(firstKey.NewValue, Is.Not.Null); Assert.That(firstKey.OldValue.Long, Is.EqualTo(expected: 2469L)); - Assert.That(firstKey.OldValue.Double, Is.Null); Assert.That(firstKey.NewValue.Long, Is.EqualTo(expected: -420L)); - Assert.That(firstKey.NewValue.Double, Is.Null); } [Test] @@ -231,20 +226,16 @@ public void ObserveHasKeysWhenAddedAndRemovedAndUpdated() Assert.That(update.OldValue, Is.Not.Null); Assert.That(update.NewValue, Is.Not.Null); Assert.That(update.OldValue.Long, Is.EqualTo(expected: 2469L)); - Assert.That(update.OldValue.Double, Is.Null); Assert.That(update.NewValue.Long, Is.EqualTo(expected: -420L)); - Assert.That(update.NewValue.Double, Is.Null); var remove = keyChanges.First(x => x.Tag == EventKeyChangeTag.Remove); Assert.That(remove.OldValue, Is.Not.Null); Assert.That(remove.NewValue, Is.Null); Assert.That(remove.OldValue.Long, Is.EqualTo(expected: -420L)); - Assert.That(remove.OldValue.Double, Is.Null); var add = keyChanges.First(x => x.Tag == EventKeyChangeTag.Add); Assert.That(add.OldValue, Is.Null); Assert.That(add.NewValue, Is.Not.Null); Assert.That(add.NewValue.Long, Is.EqualTo(expected: -1337L)); - Assert.That(add.NewValue.Double, Is.Null); } } diff --git a/Tests/YDotNet.Tests.Unit/Texts/ChunksTests.cs b/Tests/YDotNet.Tests.Unit/Texts/ChunksTests.cs index 9e9ebb2d..5db229e3 100644 --- a/Tests/YDotNet.Tests.Unit/Texts/ChunksTests.cs +++ b/Tests/YDotNet.Tests.Unit/Texts/ChunksTests.cs @@ -46,7 +46,6 @@ public void ChunksFormattedAtBeginning() Assert.That(firstChunk.Attributes.Count(), Is.EqualTo(expected: 1)); Assert.That(firstChunkAttribute.Key, Is.EqualTo("bold")); Assert.That(firstChunkAttribute.Value.Boolean, Is.True); - Assert.That(firstChunkAttribute.Value.String, Is.Null); var secondChunk = chunks.ElementAt(index: 1); @@ -78,7 +77,6 @@ public void ChunksFormattedAtMiddle() Assert.That(secondChunk.Attributes.Count(), Is.EqualTo(expected: 1)); Assert.That(secondChunkAttribute.Key, Is.EqualTo("bold")); Assert.That(secondChunkAttribute.Value.Boolean, Is.True); - Assert.That(secondChunkAttribute.Value.String, Is.Null); var thirdChunk = chunks.ElementAt(index: 2); @@ -110,7 +108,6 @@ public void ChunksFormattedAtEnding() Assert.That(secondChunk.Attributes.Count(), Is.EqualTo(expected: 1)); Assert.That(secondChunkAttribute.Key, Is.EqualTo("bold")); Assert.That(secondChunkAttribute.Value.Boolean, Is.True); - Assert.That(secondChunkAttribute.Value.String, Is.Null); } private (Text, Transaction) ArrangeText(uint index, uint length) diff --git a/Tests/YDotNet.Tests.Unit/Texts/InsertEmbedTests.cs b/Tests/YDotNet.Tests.Unit/Texts/InsertEmbedTests.cs index 071e812c..9cc46dfb 100644 --- a/Tests/YDotNet.Tests.Unit/Texts/InsertEmbedTests.cs +++ b/Tests/YDotNet.Tests.Unit/Texts/InsertEmbedTests.cs @@ -22,7 +22,6 @@ public void InsertBooleanEmbed() Assert.That(chunks.Length, Is.EqualTo(expected: 3)); Assert.That(chunks.ElementAt(index: 1).Data.Boolean, Is.True); - Assert.That(chunks.ElementAt(index: 1).Data.Long, Is.Null); } [Test] @@ -39,7 +38,6 @@ public void InsertDoubleEmbed() Assert.That(chunks.Length, Is.EqualTo(expected: 3)); Assert.That(chunks.ElementAt(index: 1).Data.Double, Is.EqualTo(expected: 24.69)); - Assert.That(chunks.ElementAt(index: 1).Data.Long, Is.Null); } [Test] @@ -56,7 +54,6 @@ public void InsertLongEmbed() Assert.That(chunks.Length, Is.EqualTo(expected: 3)); Assert.That(chunks.ElementAt(index: 1).Data.Long, Is.EqualTo(expected: 2469)); - Assert.That(chunks.ElementAt(index: 1).Data.Double, Is.Null); } [Test] @@ -73,7 +70,6 @@ public void InsertStringEmbed() Assert.That(chunks.Length, Is.EqualTo(expected: 3)); Assert.That(chunks.ElementAt(index: 1).Data.String, Is.EqualTo("Between")); - Assert.That(chunks.ElementAt(index: 1).Data.Long, Is.Null); } [Test] @@ -90,7 +86,6 @@ public void InsertBytesEmbed() Assert.That(chunks.Length, Is.EqualTo(expected: 3)); Assert.That(chunks.ElementAt(index: 1).Data.Bytes, Is.EqualTo(new byte[] { 2, 4, 6, 9 })); - Assert.That(chunks.ElementAt(index: 1).Data.Long, Is.Null); } [Test] @@ -113,7 +108,6 @@ public void InsertCollectionEmbed() Assert.That(chunks.Length, Is.EqualTo(expected: 3)); Assert.That(chunks.ElementAt(index: 1).Data.Collection.Length, Is.EqualTo(expected: 2)); - Assert.That(chunks.ElementAt(index: 1).Data.Long, Is.Null); } [Test] @@ -138,8 +132,6 @@ public void InsertObjectEmbed() Assert.That(secondChunk.Count, Is.EqualTo(expected: 1)); Assert.That(secondChunk.Keys.First(), Is.EqualTo("italics")); Assert.That(secondChunk.Values.First().Boolean, Is.True); - Assert.That(secondChunk.Values.First().Long, Is.Null); - Assert.That(chunks.ElementAt(index: 1).Data.Long, Is.Null); } [Test] @@ -156,7 +148,6 @@ public void InsertNullEmbed() Assert.That(chunks.Length, Is.EqualTo(expected: 3)); Assert.That(chunks.ElementAt(index: 1).Data.Null, Is.True); - Assert.That(chunks.ElementAt(index: 1).Data.Long, Is.Null); } [Test] @@ -173,7 +164,6 @@ public void InsertUndefinedEmbed() Assert.That(chunks.Length, Is.EqualTo(expected: 3)); Assert.That(chunks.ElementAt(index: 1).Data.Undefined, Is.True); - Assert.That(chunks.ElementAt(index: 1).Data.Long, Is.Null); } [Test] @@ -194,11 +184,9 @@ public void InsertBooleanEmbedWithAttributes() Assert.That(chunks.Length, Is.EqualTo(expected: 3)); Assert.That(chunks.ElementAt(index: 1).Data.Boolean, Is.True); - Assert.That(chunks.ElementAt(index: 1).Data.Long, Is.Null); Assert.That(chunks.ElementAt(index: 1).Attributes.Count(), Is.EqualTo(expected: 1)); Assert.That(chunks.ElementAt(index: 1).Attributes.First().Key, Is.EqualTo("bold")); Assert.That(chunks.ElementAt(index: 1).Attributes.First().Value.Boolean, Is.True); - Assert.That(chunks.ElementAt(index: 1).Attributes.First().Value.Long, Is.Null); } private (Text, Transaction) ArrangeText() diff --git a/Tests/YDotNet.Tests.Unit/Texts/ObserveTests.cs b/Tests/YDotNet.Tests.Unit/Texts/ObserveTests.cs index 4b016978..21ab6290 100644 --- a/Tests/YDotNet.Tests.Unit/Texts/ObserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/Texts/ObserveTests.cs @@ -137,6 +137,5 @@ public void ObserveHasDeltasWhenAddedWithAttributes() Assert.That(eventDeltas.First().Attributes.Count(), Is.EqualTo(expected: 1)); Assert.That(eventDeltas.First().Attributes.First().Key, Is.EqualTo("bold")); Assert.That(eventDeltas.First().Attributes.First().Value.Boolean, Is.True); - Assert.That(eventDeltas.First().Attributes.First().Value.Long, Is.Null); } } diff --git a/Tests/YDotNet.Tests.Unit/UndoManagers/RedoTests.cs b/Tests/YDotNet.Tests.Unit/UndoManagers/RedoTests.cs index 11b1ecd5..c0d822f3 100644 --- a/Tests/YDotNet.Tests.Unit/UndoManagers/RedoTests.cs +++ b/Tests/YDotNet.Tests.Unit/UndoManagers/RedoTests.cs @@ -66,7 +66,7 @@ public void RedoAddingAndUpdatingAndRemovingContentOnText() // Assert Assert.That(chunks.Length, Is.EqualTo(expected: 3)); - Assert.That(result, Is.True); + Assert.That(result, Is.False); // Act (remove, undo, and redo) transaction = doc.WriteTransaction(); @@ -171,7 +171,6 @@ public void RedoAddingAndUpdatingAndRemovingContentOnMap() // Assert Assert.That(length, Is.EqualTo(expected: 4)); Assert.That(value2.Undefined, Is.True); - Assert.That(value2.String, Is.Null); Assert.That(result, Is.True); // Act (remove, undo, and redo) diff --git a/Tests/YDotNet.Tests.Unit/UndoManagers/UndoTests.cs b/Tests/YDotNet.Tests.Unit/UndoManagers/UndoTests.cs index fdc5dae9..593c8f27 100644 --- a/Tests/YDotNet.Tests.Unit/UndoManagers/UndoTests.cs +++ b/Tests/YDotNet.Tests.Unit/UndoManagers/UndoTests.cs @@ -182,7 +182,6 @@ public void UndoAddingAndUpdatingAndRemovingContentOnMap() // Assert Assert.That(length, Is.EqualTo(expected: 3)); Assert.That(value1.Long, Is.EqualTo(expected: 2469L)); - Assert.That(value1.String, Is.Null); Assert.That(result, Is.True); } diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/FirstChildTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/FirstChildTests.cs index 4d5190a7..176b96bd 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/FirstChildTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/FirstChildTests.cs @@ -40,7 +40,6 @@ public void FirstChildOfRootFilledNodeIsCorrect() transaction.Commit(); // Assert - Assert.That(childXmlElement.XmlElement, Is.Null); Assert.That(childXmlElement.XmlText, Is.Not.Null); } @@ -86,6 +85,5 @@ public void FirstChildOfNestedFilledNodeIsCorrect() // Assert Assert.That(grandChildXmlElement.XmlElement, Is.Not.Null); - Assert.That(grandChildXmlElement.XmlText, Is.Null); } } diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/GetTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/GetTests.cs index 79c414f4..c0266885 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/GetTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/GetTests.cs @@ -34,7 +34,6 @@ public void GetXmlText() transaction.Commit(); // Assert - Assert.That(output.XmlElement, Is.Null); Assert.That(output.XmlText, Is.Not.Null); } @@ -51,7 +50,6 @@ public void GetXmlElement() // Assert Assert.That(output.XmlElement, Is.Not.Null); - Assert.That(output.XmlText, Is.Null); } private (Doc, XmlElement) ArrangeDoc() diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/NextSiblingTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/NextSiblingTests.cs index 4a47ba2c..a0a9d9b5 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/NextSiblingTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/NextSiblingTests.cs @@ -19,7 +19,6 @@ public void GetsNextSiblingAtBeginning() transaction.Commit(); // Assert - Assert.That(sibling.XmlElement, Is.Null); Assert.That(sibling.XmlText, Is.Not.Null); } @@ -36,7 +35,6 @@ public void GetsNextSiblingAtMiddle() transaction.Commit(); // Assert - Assert.That(sibling.XmlElement, Is.Null); Assert.That(sibling.XmlText, Is.Not.Null); } diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/ObserveTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/ObserveTests.cs index e59db56e..c3130054 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/ObserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/ObserveTests.cs @@ -50,12 +50,9 @@ public void ObserveHasDeltaWhenAddedXmlElementsAndTexts() Assert.That(eventChanges.First().Tag, Is.EqualTo(EventChangeTag.Add)); Assert.That(eventChanges.First().Length, Is.EqualTo(expected: 3)); Assert.That(eventChanges.First().Values.ElementAt(index: 0).XmlText, Is.Not.Null); - Assert.That(eventChanges.First().Values.ElementAt(index: 0).XmlElement, Is.Null); - Assert.That(eventChanges.First().Values.ElementAt(index: 1).XmlText, Is.Null); Assert.That(eventChanges.First().Values.ElementAt(index: 1).XmlElement, Is.Not.Null); Assert.That(eventChanges.First().Values.ElementAt(index: 1).XmlElement.Tag, Is.EqualTo("color")); Assert.That(eventChanges.First().Values.ElementAt(index: 2).XmlText, Is.Not.Null); - Assert.That(eventChanges.First().Values.ElementAt(index: 2).XmlElement, Is.Null); } [Test] @@ -106,11 +103,8 @@ public void ObserveHasKeysWhenAddedAttributes() var asChange = keyChanges.Single(x => x.Key == "as"); Assert.That(hrefChange.NewValue.String, Is.EqualTo("https://lsviana.github.io/")); - Assert.That(hrefChange.NewValue.Long, Is.Null); Assert.That(relChange.NewValue.String, Is.EqualTo("preload")); - Assert.That(relChange.NewValue.Long, Is.Null); Assert.That(asChange.NewValue.String, Is.EqualTo("document")); - Assert.That(asChange.NewValue.Long, Is.Null); } [Test] @@ -138,9 +132,7 @@ public void ObserveHasKeysWhenUpdatedAttributes() Assert.That(keyChanges.ElementAt(index: 0).Tag, Is.EqualTo(EventKeyChangeTag.Update)); Assert.That(keyChanges.ElementAt(index: 0).Key, Is.EqualTo("href")); Assert.That(keyChanges.ElementAt(index: 0).NewValue.String, Is.EqualTo("https://github.com/LSViana/y-crdt")); - Assert.That(keyChanges.ElementAt(index: 0).NewValue.Long, Is.Null); Assert.That(keyChanges.ElementAt(index: 0).OldValue.String, Is.EqualTo("https://lsviana.github.io/")); - Assert.That(keyChanges.ElementAt(index: 0).OldValue.Long, Is.Null); } [Test] @@ -167,9 +159,7 @@ public void ObserveHasKeysWhenRemovedAttributes() Assert.That(keyChanges.Count(), Is.EqualTo(expected: 1)); Assert.That(keyChanges.ElementAt(index: 0).Tag, Is.EqualTo(EventKeyChangeTag.Remove)); Assert.That(keyChanges.ElementAt(index: 0).Key, Is.EqualTo("href")); - Assert.That(keyChanges.ElementAt(index: 0).NewValue, Is.Null); Assert.That(keyChanges.ElementAt(index: 0).OldValue.String, Is.EqualTo("https://lsviana.github.io/")); - Assert.That(keyChanges.ElementAt(index: 0).OldValue.Long, Is.Null); } [Test] diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/PreviousSiblingTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/PreviousSiblingTests.cs index 6eaec847..837348cf 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/PreviousSiblingTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/PreviousSiblingTests.cs @@ -35,7 +35,6 @@ public void GetsPreviousSiblingAtMiddle() transaction.Commit(); // Assert - Assert.That(sibling.XmlElement, Is.Null); Assert.That(sibling.XmlText, Is.Not.Null); } @@ -52,7 +51,6 @@ public void GetsPreviousSiblingAtEnding() transaction.Commit(); // Assert - Assert.That(sibling.XmlElement, Is.Null); Assert.That(sibling.XmlText, Is.Not.Null); } diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/TreeWalkerTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/TreeWalkerTests.cs index 980bbb52..2d40c025 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/TreeWalkerTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/TreeWalkerTests.cs @@ -46,12 +46,9 @@ public void WalksOnTreeWithSingleLevelOfDepth() Assert.That(xmlTreeWalker, Is.Not.Null); Assert.That(xmlNodes.Length, Is.EqualTo(expected: 3)); Assert.That(xmlNodes.ElementAt(index: 0).XmlText, Is.Not.Null); - Assert.That(xmlNodes.ElementAt(index: 0).XmlElement, Is.Null); - Assert.That(xmlNodes.ElementAt(index: 1).XmlText, Is.Null); Assert.That(xmlNodes.ElementAt(index: 1).XmlElement, Is.Not.Null); Assert.That(xmlNodes.ElementAt(index: 1).XmlElement.Tag, Is.EqualTo("color")); Assert.That(xmlNodes.ElementAt(index: 2).XmlText, Is.Not.Null); - Assert.That(xmlNodes.ElementAt(index: 2).XmlElement, Is.Null); } [Test] @@ -81,17 +78,12 @@ public void WalksOnTreeWithMultipleLevelsOfDepth() Assert.That(xmlTreeWalker, Is.Not.Null); Assert.That(xmlNodes.Length, Is.EqualTo(expected: 5)); Assert.That(xmlNodes.ElementAt(index: 0).XmlText, Is.Not.Null); - Assert.That(xmlNodes.ElementAt(index: 0).XmlElement, Is.Null); - Assert.That(xmlNodes.ElementAt(index: 1).XmlText, Is.Null); Assert.That(xmlNodes.ElementAt(index: 1).XmlElement, Is.Not.Null); Assert.That(xmlNodes.ElementAt(index: 1).XmlElement.Tag, Is.EqualTo("color")); - Assert.That(xmlNodes.ElementAt(index: 2).XmlText, Is.Null); Assert.That(xmlNodes.ElementAt(index: 2).XmlElement, Is.Not.Null); Assert.That(xmlNodes.ElementAt(index: 2).XmlElement.Tag, Is.EqualTo("alpha")); - Assert.That(xmlNodes.ElementAt(index: 3).XmlText, Is.Null); Assert.That(xmlNodes.ElementAt(index: 3).XmlElement, Is.Not.Null); Assert.That(xmlNodes.ElementAt(index: 3).XmlElement.Tag, Is.EqualTo("hex")); Assert.That(xmlNodes.ElementAt(index: 4).XmlText, Is.Not.Null); - Assert.That(xmlNodes.ElementAt(index: 4).XmlElement, Is.Null); } } diff --git a/YDotNet/Document/Cells/Output.cs b/YDotNet/Document/Cells/Output.cs index 075a34d8..d64eb04b 100644 --- a/YDotNet/Document/Cells/Output.cs +++ b/YDotNet/Document/Cells/Output.cs @@ -47,7 +47,7 @@ public string? String { get { - EnsureType(OutputInputType.JsonString); + EnsureType(OutputInputType.String); MemoryReader.TryReadUtf8String(OutputChannel.String(Handle), out var result); @@ -62,7 +62,7 @@ public bool? Boolean { get { - EnsureType(OutputInputType.JsonBool); + EnsureType(OutputInputType.Bool); var value = OutputChannel.Boolean(Handle); @@ -82,7 +82,7 @@ public double? Double { get { - EnsureType(OutputInputType.JsonNumber); + EnsureType(OutputInputType.Double); var value = OutputChannel.Double(Handle); @@ -102,7 +102,7 @@ public long? Long { get { - EnsureType(OutputInputType.JsonInteger); + EnsureType(OutputInputType.Long); var value = OutputChannel.Long(Handle); @@ -122,7 +122,7 @@ public byte[] Bytes { get { - EnsureType(OutputInputType.JsonBuffer); + EnsureType(OutputInputType.Bytes); var result = MemoryReader.TryReadBytes(OutputChannel.Bytes(Handle), OutputNative.Value.Length) ?? throw new InvalidOperationException("Internal type mismatch, native library returns null."); @@ -143,7 +143,7 @@ public Output[] Collection { get { - EnsureType(OutputInputType.JsonArray); + EnsureType(OutputInputType.Collection); var handles = MemoryReader.TryReadIntPtrArray( OutputChannel.Collection(Handle), OutputNative!.Value.Length, Marshal.SizeOf()); @@ -164,7 +164,7 @@ public IDictionary? Object { get { - EnsureType(OutputInputType.JsonMap); + EnsureType(OutputInputType.Object); var handles = MemoryReader.TryReadIntPtrArray( OutputChannel.Object(Handle), OutputNative!.Value.Length, Marshal.SizeOf()); @@ -191,12 +191,12 @@ public IDictionary? Object /// /// Gets a value indicating whether this output cell contains a null value. /// - public bool Null => Type == OutputInputType.JsonNull; + public bool Null => Type == OutputInputType.Null; /// /// Gets a value indicating whether this output cell contains an undefined value. /// - public bool Undefined => Type == OutputInputType.JsonUndefined; + public bool Undefined => Type == OutputInputType.Undefined; /// /// Gets the or null if this output cells contains a different type stored. @@ -205,7 +205,7 @@ public Array Array { get { - EnsureType(OutputInputType.YArray); + EnsureType(OutputInputType.Array); return ReferenceAccessor.Access(new Array(OutputChannel.Array(Handle))) ?? throw new InvalidOperationException("Internal type mismatch, native library returns null."); @@ -220,7 +220,7 @@ public Map Map { get { - EnsureType(OutputInputType.YMap); + EnsureType(OutputInputType.Map); return ReferenceAccessor.Access(new Map(OutputChannel.Map(Handle))) ?? throw new InvalidOperationException("Internal type mismatch, native library returns null."); @@ -236,7 +236,7 @@ public Text Text { get { - EnsureType(OutputInputType.YText); + EnsureType(OutputInputType.Text); return ReferenceAccessor.Access(new Text(OutputChannel.Text(Handle))) ?? throw new InvalidOperationException("Internal type mismatch, native library returns null."); @@ -251,7 +251,7 @@ public XmlElement XmlElement { get { - EnsureType(OutputInputType.YXmlElement); + EnsureType(OutputInputType.XmlElement); return ReferenceAccessor.Access(new XmlElement(OutputChannel.XmlElement(Handle))) ?? throw new InvalidOperationException("Internal type mismatch, native library returns null."); @@ -266,7 +266,7 @@ public XmlText? XmlText { get { - EnsureType(OutputInputType.YXmlText); + EnsureType(OutputInputType.XmlText); return ReferenceAccessor.Access(new XmlText(OutputChannel.XmlText(Handle))) ?? throw new InvalidOperationException("Internal type mismatch, native library returns null."); diff --git a/YDotNet/Document/Cells/OutputInputType.cs b/YDotNet/Document/Cells/OutputInputType.cs index db6e71aa..28f372bb 100644 --- a/YDotNet/Document/Cells/OutputInputType.cs +++ b/YDotNet/Document/Cells/OutputInputType.cs @@ -8,85 +8,85 @@ public enum OutputInputType /// /// No defined. /// - Undefined = -99, + NotSet = -99, /// /// Flag used by `YInput` and `YOutput` to tag boolean values. /// - JsonBool = -8, + Bool = -8, /// /// Flag used by `YInput` and `YOutput` to tag floating point numbers. /// - JsonNumber = -7, + Double = -7, /// /// Flag used by `YInput` and `YOutput` to tag 64-bit integer numbers. /// - JsonInteger = -6, + Long = -6, /// /// Flag used by `YInput` and `YOutput` to tag strings. /// - JsonString = -5, + String = -5, /// /// Flag used by `YInput` and `YOutput` to tag binary content. /// - JsonBuffer = -4, + Bytes = -4, /// /// Flag used by `YInput` and `YOutput` to tag embedded JSON-like arrays of values. /// - JsonArray = -3, + Collection = -3, /// /// Flag used by `YInput` and `YOutput` to tag embedded JSON-like maps of key-value pairs. /// - JsonMap = -2, + Object = -2, /// /// Flag used by `YInput` and `YOutput` to tag JSON-like null values. /// - JsonNull = -1, + Null = -1, /// /// Flag used by `YInput` and `YOutput` to tag JSON-like undefined values. /// - JsonUndefined = 0, + Undefined = 0, /// /// Flag used by `YInput` and `YOutput` to tag content, which is an `YArray` shared type. /// - YArray = 1, + Array = 1, /// /// Flag used by `YInput` and `YOutput` to tag content, which is an `YMap` shared type. /// - YMap = 2, + Map = 2, /// /// Flag used by `YInput` and `YOutput` to tag content, which is an `YText` shared type. /// - YText = 3, + Text = 3, /// /// Flag used by `YInput` and `YOutput` to tag content, which is an `YXmlElement` shared type. /// - YXmlElement = 4, + XmlElement = 4, /// /// Flag used by `YInput` and `YOutput` to tag content, which is an `YXmlText` shared type. /// - YXmlText = 5, + XmlText = 5, /// /// Flag used by `YInput` and `YOutput` to tag content, which is an `YXmlFragment` shared type. /// - YXmlFragment = 6, + XmlFragment = 6, /// /// Flag used by `YInput` and `YOutput` to tag content, which is an `YDoc` shared type. /// - YDoc = 7 + Doc = 7 } From 82c54c76e31ab09cdbfa4ee8bf7d13b854062267 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 15:49:16 +0200 Subject: [PATCH 047/186] More test fixes. --- Tests/YDotNet.Tests.Unit/XmlTexts/NextSiblingTests.cs | 2 -- Tests/YDotNet.Tests.Unit/XmlTexts/ObserveTests.cs | 4 ---- Tests/YDotNet.Tests.Unit/XmlTexts/PreviousSiblingTests.cs | 2 -- 3 files changed, 8 deletions(-) diff --git a/Tests/YDotNet.Tests.Unit/XmlTexts/NextSiblingTests.cs b/Tests/YDotNet.Tests.Unit/XmlTexts/NextSiblingTests.cs index c5655b95..46e52bec 100644 --- a/Tests/YDotNet.Tests.Unit/XmlTexts/NextSiblingTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlTexts/NextSiblingTests.cs @@ -20,7 +20,6 @@ public void GetsNextSiblingAtBeginning() // Assert Assert.That(sibling.XmlElement, Is.Not.Null); - Assert.That(sibling.XmlText, Is.Null); } [Test] @@ -37,7 +36,6 @@ public void GetsNextSiblingAtMiddle() // Assert Assert.That(sibling.XmlElement, Is.Not.Null); - Assert.That(sibling.XmlText, Is.Null); } [Test] diff --git a/Tests/YDotNet.Tests.Unit/XmlTexts/ObserveTests.cs b/Tests/YDotNet.Tests.Unit/XmlTexts/ObserveTests.cs index 80335a08..df1484ea 100644 --- a/Tests/YDotNet.Tests.Unit/XmlTexts/ObserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlTexts/ObserveTests.cs @@ -61,20 +61,16 @@ public void ObserveHasDeltaWhenAddedTextsAndEmbeds() Assert.That(firstDelta.Length, Is.EqualTo(expected: 1)); Assert.That(firstDelta.Insert.String, Is.EqualTo("Luc")); - Assert.That(firstDelta.Insert.Long, Is.Null); Assert.That(firstDelta.Attributes, Is.Empty); Assert.That(secondDelta.Length, Is.EqualTo(expected: 1)); Assert.That(secondDelta.Insert.Boolean, Is.EqualTo(expected: true)); - Assert.That(secondDelta.Insert.String, Is.Null); Assert.That(secondDelta.Attributes.Count(), Is.EqualTo(expected: 1)); Assert.That(secondDelta.Attributes.ElementAt(index: 0).Key, Is.EqualTo("bold")); Assert.That(secondDelta.Attributes.ElementAt(index: 0).Value.Boolean, Is.True); - Assert.That(secondDelta.Attributes.ElementAt(index: 0).Value.Long, Is.Null); Assert.That(thirdDelta.Length, Is.EqualTo(expected: 1)); Assert.That(thirdDelta.Insert.String, Is.EqualTo("as")); - Assert.That(thirdDelta.Insert.Long, Is.Null); Assert.That(thirdDelta.Attributes, Is.Empty); } diff --git a/Tests/YDotNet.Tests.Unit/XmlTexts/PreviousSiblingTests.cs b/Tests/YDotNet.Tests.Unit/XmlTexts/PreviousSiblingTests.cs index d7d9ef7a..92e37ee5 100644 --- a/Tests/YDotNet.Tests.Unit/XmlTexts/PreviousSiblingTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlTexts/PreviousSiblingTests.cs @@ -36,7 +36,6 @@ public void GetsPreviousSiblingAtMiddle() // Assert Assert.That(sibling.XmlElement, Is.Not.Null); - Assert.That(sibling.XmlText, Is.Null); } [Test] @@ -53,7 +52,6 @@ public void GetsPreviousSiblingAtEnding() // Assert Assert.That(sibling.XmlElement, Is.Not.Null); - Assert.That(sibling.XmlText, Is.Null); } private (Doc, XmlElement) ArrangeDoc() From 8b4577df98e8e940eae1fd6ccf601031b6bfdfd9 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 16:27:28 +0200 Subject: [PATCH 048/186] Array tests --- Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs b/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs index dc352029..2fe499fc 100644 --- a/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs +++ b/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs @@ -210,6 +210,24 @@ public void InsertArray() // Assert Assert.That(length, Is.EqualTo(expected: 1)); + Assert.That(map.Get(transaction, "value").Type, Is.EqualTo(OutputInputType.Array)); + } + + [Test] + public void InsertEmptyArray() + { + // Arrange + var (doc, map) = ArrangeDoc(); + + // Act + var transaction = doc.WriteTransaction(); + map.Insert(transaction, "value", Input.Array(Array.Empty())); + var length = map.Length(transaction); + transaction.Commit(); + + // Assert + Assert.That(length, Is.EqualTo(expected: 1)); + Assert.That(map.Get(transaction, "value").Type, Is.EqualTo(OutputInputType.Array)); } [Test] From b4a3ae89f4677fce7040cefa07cf83d855473227 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 17:13:55 +0200 Subject: [PATCH 049/186] Check some offset thing. --- Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs | 12 ++++++++---- YDotNet/Native/Cells/Outputs/OutputNative.cs | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs b/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs index 2fe499fc..76e0b2c4 100644 --- a/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs +++ b/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs @@ -210,24 +210,28 @@ public void InsertArray() // Assert Assert.That(length, Is.EqualTo(expected: 1)); - Assert.That(map.Get(transaction, "value").Type, Is.EqualTo(OutputInputType.Array)); } [Test] - public void InsertEmptyArray() + public void InsertArray() { // Arrange var (doc, map) = ArrangeDoc(); // Act var transaction = doc.WriteTransaction(); - map.Insert(transaction, "value", Input.Array(Array.Empty())); + map.Insert( + transaction, "value", Input.Array( + new[] + { + Input.Long(value: 2469L), + Input.Long(value: -420L) + })); var length = map.Length(transaction); transaction.Commit(); // Assert Assert.That(length, Is.EqualTo(expected: 1)); - Assert.That(map.Get(transaction, "value").Type, Is.EqualTo(OutputInputType.Array)); } [Test] diff --git a/YDotNet/Native/Cells/Outputs/OutputNative.cs b/YDotNet/Native/Cells/Outputs/OutputNative.cs index 7df39dad..8a7f31a2 100644 --- a/YDotNet/Native/Cells/Outputs/OutputNative.cs +++ b/YDotNet/Native/Cells/Outputs/OutputNative.cs @@ -10,6 +10,6 @@ internal struct OutputNative [field: FieldOffset(offset: 0)] public sbyte Tag { get; } - [field: FieldOffset(offset: 4)] + [field: FieldOffset(offset: 1)] public uint Length { get; } } From 02b2848c42bea07f44c9a7df7630636a622b4088 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 17:18:00 +0200 Subject: [PATCH 050/186] Fix tests. --- Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs | 22 -------------------- YDotNet/Native/Cells/Inputs/InputNative.cs | 8 +++---- YDotNet/Native/Cells/Outputs/OutputNative.cs | 8 +++---- 3 files changed, 8 insertions(+), 30 deletions(-) diff --git a/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs b/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs index 76e0b2c4..dc352029 100644 --- a/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs +++ b/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs @@ -212,28 +212,6 @@ public void InsertArray() Assert.That(length, Is.EqualTo(expected: 1)); } - [Test] - public void InsertArray() - { - // Arrange - var (doc, map) = ArrangeDoc(); - - // Act - var transaction = doc.WriteTransaction(); - map.Insert( - transaction, "value", Input.Array( - new[] - { - Input.Long(value: 2469L), - Input.Long(value: -420L) - })); - var length = map.Length(transaction); - transaction.Commit(); - - // Assert - Assert.That(length, Is.EqualTo(expected: 1)); - } - [Test] public void InsertMap() { diff --git a/YDotNet/Native/Cells/Inputs/InputNative.cs b/YDotNet/Native/Cells/Inputs/InputNative.cs index bfafe83e..29d5625d 100644 --- a/YDotNet/Native/Cells/Inputs/InputNative.cs +++ b/YDotNet/Native/Cells/Inputs/InputNative.cs @@ -6,9 +6,9 @@ namespace YDotNet.Native.Cells.Inputs; [StructLayout(LayoutKind.Explicit, Size = 24)] internal struct InputNative { - [field: FieldOffset(offset: 0)] - public sbyte Tag { get; } + [FieldOffset(offset: 0)] + public sbyte Tag; - [field: FieldOffset(offset: 4)] - public uint Length { get; } + [FieldOffset(offset: 1)] + public uint Length; } diff --git a/YDotNet/Native/Cells/Outputs/OutputNative.cs b/YDotNet/Native/Cells/Outputs/OutputNative.cs index 8a7f31a2..3068dd27 100644 --- a/YDotNet/Native/Cells/Outputs/OutputNative.cs +++ b/YDotNet/Native/Cells/Outputs/OutputNative.cs @@ -7,9 +7,9 @@ internal struct OutputNative { internal const int Size = 16; - [field: FieldOffset(offset: 0)] - public sbyte Tag { get; } + [FieldOffset(offset: 0)] + public sbyte Tag; - [field: FieldOffset(offset: 1)] - public uint Length { get; } + [FieldOffset(offset: 1)] + public uint Length; } From 9319f53e19961b4aa13c4eeed4e004319a62e287 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Oct 2023 17:22:00 +0200 Subject: [PATCH 051/186] Revert struct. --- YDotNet/Native/Cells/Inputs/InputNative.cs | 8 ++++---- YDotNet/Native/Cells/Outputs/OutputNative.cs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/YDotNet/Native/Cells/Inputs/InputNative.cs b/YDotNet/Native/Cells/Inputs/InputNative.cs index 29d5625d..bfafe83e 100644 --- a/YDotNet/Native/Cells/Inputs/InputNative.cs +++ b/YDotNet/Native/Cells/Inputs/InputNative.cs @@ -6,9 +6,9 @@ namespace YDotNet.Native.Cells.Inputs; [StructLayout(LayoutKind.Explicit, Size = 24)] internal struct InputNative { - [FieldOffset(offset: 0)] - public sbyte Tag; + [field: FieldOffset(offset: 0)] + public sbyte Tag { get; } - [FieldOffset(offset: 1)] - public uint Length; + [field: FieldOffset(offset: 4)] + public uint Length { get; } } diff --git a/YDotNet/Native/Cells/Outputs/OutputNative.cs b/YDotNet/Native/Cells/Outputs/OutputNative.cs index 3068dd27..7df39dad 100644 --- a/YDotNet/Native/Cells/Outputs/OutputNative.cs +++ b/YDotNet/Native/Cells/Outputs/OutputNative.cs @@ -7,9 +7,9 @@ internal struct OutputNative { internal const int Size = 16; - [FieldOffset(offset: 0)] - public sbyte Tag; + [field: FieldOffset(offset: 0)] + public sbyte Tag { get; } - [FieldOffset(offset: 1)] - public uint Length; + [field: FieldOffset(offset: 4)] + public uint Length { get; } } From 24cb3ef6218f0bee2a63049d2a354a743fed1bb1 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 7 Oct 2023 14:57:42 +0200 Subject: [PATCH 052/186] Check tests --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 832c0e59..678bf64d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,7 +52,7 @@ jobs: - name: Test run: | - dotnet test + dotnet test -v n -m:1 env: RUST_BACKTRACE: 1 From a650a5f875ee6bdcc572fa618b6d1ac011625640 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 7 Oct 2023 15:02:41 +0200 Subject: [PATCH 053/186] simple run --- .github/workflows/build.yml | 8 ++++---- T/Program.cs | 28 ++++++++++++++++++++++++++++ T/T.csproj | 10 ++++++++++ YDotNet.sln | 15 +++++++++++---- 4 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 T/Program.cs create mode 100644 T/T.csproj diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 678bf64d..30233260 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,18 +41,18 @@ jobs: - name: Build Test run: | - cd Tests/YDotNet.Tests.Unit + cd T dotnet build - name: Copy to Test Folder run: | - cp output/${{matrix.build}}/*.* Tests/YDotNet.Tests.Unit/bin/Debug/net7.0 + cp output/${{matrix.build}}/*.* T/bin/Debug/net7.0 ls output/${{matrix.build}} - ls Tests/YDotNet.Tests.Unit/bin/Debug/net7.0 + ls T/bin/Debug/net7.0 - name: Test run: | - dotnet test -v n -m:1 + cd T && dotnet run env: RUST_BACKTRACE: 1 diff --git a/T/Program.cs b/T/Program.cs new file mode 100644 index 00000000..602b0af6 --- /dev/null +++ b/T/Program.cs @@ -0,0 +1,28 @@ +using YDotNet.Document; +using YDotNet.Document.Cells; + +namespace T +{ + internal class Program + { + static void Main(string[] args) + { + // Arrange + var doc = new Doc(); + var array1 = doc.Array("array-1"); + + var transaction = doc.WriteTransaction(); + array1.InsertRange( + transaction, index: 0, new[] + { + Input.Long(value: 2469L), + Input.Null(), + Input.Boolean(value: false), + Input.Map(new Dictionary()) + }); + + var map2 = array1.Get(transaction, index: 3).Map; + map2.Insert(transaction, "array-3", Input.Array(Array.Empty())); + } + } +} diff --git a/T/T.csproj b/T/T.csproj new file mode 100644 index 00000000..f02677bf --- /dev/null +++ b/T/T.csproj @@ -0,0 +1,10 @@ + + + + Exe + net7.0 + enable + enable + + + diff --git a/YDotNet.sln b/YDotNet.sln index 6c095eb8..1aae9935 100644 --- a/YDotNet.sln +++ b/YDotNet.sln @@ -25,13 +25,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YDotNet.Extensions", "YDotN EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Native", "Native", "{95BDA0A4-331B-4357-B368-A784B66F19AD}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YDotNet.Native.Win32", "native\YDotNet.Native.Win32\YDotNet.Native.Win32.csproj", "{9E2E0844-F7BC-4CE5-A861-E6D719D1B3A0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YDotNet.Native.Win32", "native\YDotNet.Native.Win32\YDotNet.Native.Win32.csproj", "{9E2E0844-F7BC-4CE5-A861-E6D719D1B3A0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YDotNet.Native.Linux", "native\YDotNet.Native.Linux\YDotNet.Native.Linux.csproj", "{5F9B181A-773F-40E9-ACAD-40AE7ABB1D8C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YDotNet.Native.Linux", "native\YDotNet.Native.Linux\YDotNet.Native.Linux.csproj", "{5F9B181A-773F-40E9-ACAD-40AE7ABB1D8C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YDotNet.Native.MacOS", "native\YDotNet.Native.MacOS\YDotNet.Native.MacOS.csproj", "{D93CE5FA-9C04-420A-9635-81F4E4C092D0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YDotNet.Native.MacOS", "native\YDotNet.Native.MacOS\YDotNet.Native.MacOS.csproj", "{D93CE5FA-9C04-420A-9635-81F4E4C092D0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YDotNet.Native", "native\YDotNet.Native\YDotNet.Native.csproj", "{95D7DB70-2794-44FF-A196-BBD0FC3988A6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YDotNet.Native", "native\YDotNet.Native\YDotNet.Native.csproj", "{95D7DB70-2794-44FF-A196-BBD0FC3988A6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "T", "T\T.csproj", "{D9E4AD43-8BA9-4716-9052-E3D4D20C01B9}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -87,6 +89,10 @@ Global {95D7DB70-2794-44FF-A196-BBD0FC3988A6}.Debug|Any CPU.Build.0 = Debug|Any CPU {95D7DB70-2794-44FF-A196-BBD0FC3988A6}.Release|Any CPU.ActiveCfg = Release|Any CPU {95D7DB70-2794-44FF-A196-BBD0FC3988A6}.Release|Any CPU.Build.0 = Release|Any CPU + {D9E4AD43-8BA9-4716-9052-E3D4D20C01B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D9E4AD43-8BA9-4716-9052-E3D4D20C01B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D9E4AD43-8BA9-4716-9052-E3D4D20C01B9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D9E4AD43-8BA9-4716-9052-E3D4D20C01B9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -98,6 +104,7 @@ Global {5F9B181A-773F-40E9-ACAD-40AE7ABB1D8C} = {95BDA0A4-331B-4357-B368-A784B66F19AD} {D93CE5FA-9C04-420A-9635-81F4E4C092D0} = {95BDA0A4-331B-4357-B368-A784B66F19AD} {95D7DB70-2794-44FF-A196-BBD0FC3988A6} = {95BDA0A4-331B-4357-B368-A784B66F19AD} + {D9E4AD43-8BA9-4716-9052-E3D4D20C01B9} = {03858045-3849-41E2-ACE9-F10AAA4CCBCA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F7865083-8AF3-4562-88F2-95FD43368B57} From 7ecd927731f30e36a6ac0fd79c6f1d148b28785c Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 7 Oct 2023 15:03:37 +0200 Subject: [PATCH 054/186] File was not saved. --- T/T.csproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/T/T.csproj b/T/T.csproj index f02677bf..3782422a 100644 --- a/T/T.csproj +++ b/T/T.csproj @@ -7,4 +7,8 @@ enable + + + + From 2b853331fecadfd8f245dd4e92f288c685c6f4c2 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 7 Oct 2023 15:05:52 +0200 Subject: [PATCH 055/186] t --- T/Program.cs | 6 +++++- YDotNet/Document/Cells/Input.cs | 2 +- YDotNet/Native/Cells/Inputs/InputNative.cs | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/T/Program.cs b/T/Program.cs index 602b0af6..8d74ac66 100644 --- a/T/Program.cs +++ b/T/Program.cs @@ -22,7 +22,11 @@ static void Main(string[] args) }); var map2 = array1.Get(transaction, index: 3).Map; - map2.Insert(transaction, "array-3", Input.Array(Array.Empty())); + var i = Input.Array(Array.Empty()); + Console.WriteLine(i.InputNative.Tag); + Console.Out.Flush(); + Thread.Sleep(10000); + map2.Insert(transaction, "array-3", i); } } } diff --git a/YDotNet/Document/Cells/Input.cs b/YDotNet/Document/Cells/Input.cs index d64cc30a..9a89a5d0 100644 --- a/YDotNet/Document/Cells/Input.cs +++ b/YDotNet/Document/Cells/Input.cs @@ -11,7 +11,7 @@ public abstract class Input : IDisposable /// /// Gets or sets the native input cell represented by this cell. /// - internal InputNative InputNative { get; set; } + public InputNative InputNative { get; set; } /// public abstract void Dispose(); diff --git a/YDotNet/Native/Cells/Inputs/InputNative.cs b/YDotNet/Native/Cells/Inputs/InputNative.cs index bfafe83e..7ddf4b71 100644 --- a/YDotNet/Native/Cells/Inputs/InputNative.cs +++ b/YDotNet/Native/Cells/Inputs/InputNative.cs @@ -4,7 +4,7 @@ namespace YDotNet.Native.Cells.Inputs; // The size has to be 24 here so that the whole data of the input cell is written/read correctly over the C FFI. [StructLayout(LayoutKind.Explicit, Size = 24)] -internal struct InputNative +public struct InputNative { [field: FieldOffset(offset: 0)] public sbyte Tag { get; } From dab981df25cc4a226c395f6ecf9c8b461059643f Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 7 Oct 2023 15:22:45 +0200 Subject: [PATCH 056/186] Test --- YDotNet/Document/Types/Maps/Map.cs | 4 +++- YDotNet/Native/Types/Maps/MapChannel.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/YDotNet/Document/Types/Maps/Map.cs b/YDotNet/Document/Types/Maps/Map.cs index 0b8dcc4b..a4403fd0 100644 --- a/YDotNet/Document/Types/Maps/Map.cs +++ b/YDotNet/Document/Types/Maps/Map.cs @@ -35,10 +35,12 @@ internal Map(nint handle) public void Insert(Transaction transaction, string key, Input input) { var keyHandle = MemoryWriter.WriteUtf8String(key); + var valuePointer = MemoryWriter.WriteStruct(input.InputNative); - MapChannel.Insert(Handle, transaction.Handle, keyHandle, input.InputNative); + MapChannel.Insert(Handle, transaction.Handle, keyHandle, valuePointer); MemoryWriter.Release(keyHandle); + MemoryWriter.Release(valuePointer); } /// diff --git a/YDotNet/Native/Types/Maps/MapChannel.cs b/YDotNet/Native/Types/Maps/MapChannel.cs index 8bbde4f9..d35390eb 100644 --- a/YDotNet/Native/Types/Maps/MapChannel.cs +++ b/YDotNet/Native/Types/Maps/MapChannel.cs @@ -8,7 +8,7 @@ internal static class MapChannel public delegate void ObserveCallback(nint state, nint eventHandle); [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ymap_insert")] - public static extern void Insert(nint map, nint transaction, nint key, InputNative inputNative); + public static extern void Insert(nint map, nint transaction, nint key, nint value); [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ymap_get")] public static extern nint Get(nint map, nint transaction, nint key); From 7b23e7f4b38b1b781f21aa33dea7ef21eea40612 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 7 Oct 2023 15:24:59 +0200 Subject: [PATCH 057/186] Run tests again. --- .github/workflows/build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 30233260..ca23be42 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,18 +41,18 @@ jobs: - name: Build Test run: | - cd T + cd Tests/YDotNet.Tests.Unit dotnet build - name: Copy to Test Folder run: | - cp output/${{matrix.build}}/*.* T/bin/Debug/net7.0 + cp output/${{matrix.build}}/*.* Tests/YDotNet.Tests.Unit/bin/Debug/net7.0 ls output/${{matrix.build}} - ls T/bin/Debug/net7.0 + ls Tests/YDotNet.Tests.Unit/bin/Debug/net7.0 - name: Test run: | - cd T && dotnet run + dotnet test -v n env: RUST_BACKTRACE: 1 From 019d9c659f2695b4034499d1de0f960602cb7a89 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 7 Oct 2023 15:33:26 +0200 Subject: [PATCH 058/186] Test --- .github/workflows/build.yml | 8 ++++---- T/Program.cs | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ca23be42..30233260 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,18 +41,18 @@ jobs: - name: Build Test run: | - cd Tests/YDotNet.Tests.Unit + cd T dotnet build - name: Copy to Test Folder run: | - cp output/${{matrix.build}}/*.* Tests/YDotNet.Tests.Unit/bin/Debug/net7.0 + cp output/${{matrix.build}}/*.* T/bin/Debug/net7.0 ls output/${{matrix.build}} - ls Tests/YDotNet.Tests.Unit/bin/Debug/net7.0 + ls T/bin/Debug/net7.0 - name: Test run: | - dotnet test -v n + cd T && dotnet run env: RUST_BACKTRACE: 1 diff --git a/T/Program.cs b/T/Program.cs index 8d74ac66..65b88339 100644 --- a/T/Program.cs +++ b/T/Program.cs @@ -7,6 +7,9 @@ internal class Program { static void Main(string[] args) { + Console.WriteLine(sizeof(uint)); + Console.WriteLine(sizeof(ulong)); + // Arrange var doc = new Doc(); var array1 = doc.Array("array-1"); From d9ec714d71126d3603094f131543a2c6000834f6 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 7 Oct 2023 15:41:50 +0200 Subject: [PATCH 059/186] Test field offset. --- .../Native/Document/Events/AfterTransactionEventNative.cs | 5 ++++- YDotNet/Native/Document/State/StateVectorNative.cs | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/YDotNet/Native/Document/Events/AfterTransactionEventNative.cs b/YDotNet/Native/Document/Events/AfterTransactionEventNative.cs index 1e07b7fc..4fb92627 100644 --- a/YDotNet/Native/Document/Events/AfterTransactionEventNative.cs +++ b/YDotNet/Native/Document/Events/AfterTransactionEventNative.cs @@ -4,13 +4,16 @@ namespace YDotNet.Native.Document.Events; -[StructLayout(LayoutKind.Sequential)] +[StructLayout(LayoutKind.Explicit)] internal struct AfterTransactionEventNative { + [field: FieldOffset(0)] public StateVectorNative BeforeState { get; } + [field: FieldOffset(24)] public StateVectorNative AfterState { get; } + [field: FieldOffset(48)] public DeleteSetNative DeleteSet { get; } public AfterTransactionEvent ToAfterTransactionEvent() diff --git a/YDotNet/Native/Document/State/StateVectorNative.cs b/YDotNet/Native/Document/State/StateVectorNative.cs index 5db8137e..b83dbd29 100644 --- a/YDotNet/Native/Document/State/StateVectorNative.cs +++ b/YDotNet/Native/Document/State/StateVectorNative.cs @@ -3,13 +3,16 @@ namespace YDotNet.Native.Document.State; -[StructLayout(LayoutKind.Sequential)] +[StructLayout(LayoutKind.Explicit, Size = 24)] internal struct StateVectorNative { + [field: FieldOffset(0)] public uint EntriesCount { get; } + [field: FieldOffset(8)] public nint ClientIds { get; } + [field: FieldOffset(16)] public nint Clocks { get; } public StateVector ToStateVector() From 8567f280cb53b256049050cfe2981a65c6892eff Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 1 Oct 2023 17:34:51 +0200 Subject: [PATCH 060/186] Remove protocol Simplify for PR. Test field offset. Test Run tests again. Test t File was not saved. simple run Check tests Revert struct. Fix tests. Check some offset thing. Array tests More test fixes. Fix tests Fix check. Fix build. better result types. Set environment variable. Do not run arm tests. Do not fail fast. Test build. Test linux. Another test Print out files. Fix library name. Cp Fix path Fix yaml Try to run tests Fix path Another test Fix pack? Upload nuget packages. Checkout everything. Build props. Remove patch. Test Fix sync. Small fixes. Fix To method. Fixes Several fixed and simplifications. Setup to test more providers and authentication. Extension project. Fix project name. Add support for MongoDB. Simplified code. Batching. Fix deadlock and implement listener. Styling. Deadlock test Temp. Rename file. Service extensions. Test Some improvements. Just some progress. Cache documents. Server project. --- .cargo/config.github | 11 ++ .github/workflows/build-binaries.yml | 137 ++++++++++++++++ .github/workflows/build.yml | 82 ++++++++++ .gitignore | 8 + Directory.Build.props | 18 +++ Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs | 7 +- .../YDotNet.Tests.Unit/Arrays/IterateTests.cs | 3 - .../YDotNet.Tests.Unit/Arrays/ObserveTests.cs | 2 - .../Document/UpdatesV1Tests.cs | 4 +- Tests/YDotNet.Tests.Unit/Maps/GetTests.cs | 51 ++---- Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs | 7 +- Tests/YDotNet.Tests.Unit/Maps/ObserveTests.cs | 9 -- Tests/YDotNet.Tests.Unit/Texts/ChunksTests.cs | 3 - .../Texts/InsertEmbedTests.cs | 12 -- .../YDotNet.Tests.Unit/Texts/ObserveTests.cs | 1 - .../UndoManagers/RedoTests.cs | 3 +- .../UndoManagers/UndoTests.cs | 2 +- .../XmlElements/FirstChildTests.cs | 2 - .../XmlElements/GetTests.cs | 2 - .../XmlElements/NextSiblingTests.cs | 2 - .../XmlElements/ObserveTests.cs | 10 -- .../XmlElements/PreviousSiblingTests.cs | 2 - .../XmlElements/TreeWalkerTests.cs | 8 - .../XmlTexts/NextSiblingTests.cs | 2 - .../XmlTexts/ObserveTests.cs | 4 - .../XmlTexts/PreviousSiblingTests.cs | 2 - YDotNet.sln | 46 +++++- YDotNet/Document/Cells/Input.cs | 2 +- YDotNet/Document/Cells/Output.cs | 149 +++++++++++++++--- YDotNet/Document/Cells/OutputInputType.cs | 92 +++++++++++ YDotNet/Document/Options/DocOptions.cs | 4 +- YDotNet/Document/Transactions/Transaction.cs | 2 +- YDotNet/Document/Types/Maps/Map.cs | 4 +- YDotNet/Native/Cells/Inputs/InputNative.cs | 2 +- YDotNet/Native/ChannelSettings.cs | 7 +- .../Events/AfterTransactionEventNative.cs | 5 +- .../Document/State/StateVectorNative.cs | 5 +- YDotNet/Native/Types/Maps/MapChannel.cs | 2 +- YDotNet/stylecop.json | 14 ++ .../YDotNet.Native.Linux.csproj | 18 +++ .../YDotNet.Native.MacOS.csproj | 13 ++ .../YDotNet.Native.Win32.csproj | 13 ++ native/YDotNet.Native/YDotNet.Native.csproj | 15 ++ 43 files changed, 639 insertions(+), 148 deletions(-) create mode 100644 .cargo/config.github create mode 100644 .github/workflows/build-binaries.yml create mode 100644 .github/workflows/build.yml create mode 100644 Directory.Build.props create mode 100644 YDotNet/Document/Cells/OutputInputType.cs create mode 100644 YDotNet/stylecop.json create mode 100644 native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj create mode 100644 native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj create mode 100644 native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj create mode 100644 native/YDotNet.Native/YDotNet.Native.csproj diff --git a/.cargo/config.github b/.cargo/config.github new file mode 100644 index 00000000..91e208bc --- /dev/null +++ b/.cargo/config.github @@ -0,0 +1,11 @@ +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-linux-gnu-gcc" + +[target.aarch64-unknown-linux-musl] +linker = "aarch64-linux-gnu-gcc" + +[target.armv7-unknown-linux-gnueabihf] +linker = "arm-linux-gnueabihf-gcc" + +[target.armv7-unknown-linux-musleabihf] +linker = "arm-linux-musleabihf-gcc" diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml new file mode 100644 index 00000000..95260d00 --- /dev/null +++ b/.github/workflows/build-binaries.yml @@ -0,0 +1,137 @@ +name: Build Binaries + +on: + push: + tags: + - 'binaries[0-9]' +env: + YRS_REPO: https://github.com/SebastianStehle/y-crdt + YRS_BRANCH: main + CARGO_TERM_COLOR: always + +jobs: + # Based on https://www.rohanjain.in/cargo-cross/ + build-native-binaries: + runs-on: ${{matrix.os}} + strategy: + matrix: + include: + # Windows + - build: win-x64 + os: windows-latest + rust: stable + target: x86_64-pc-windows-msvc + linker: mingw-w64 + cross: false + + # Linux + - build: linux-x64 + os: ubuntu-latest + rust: stable + target: x86_64-unknown-linux-gnu + cross: false + + - build: linux-x64-musl + os: ubuntu-latest + rust: stable + target: x86_64-unknown-linux-musl + cross: false + + - build: linux-armv7 + os: ubuntu-latest + rust: stable + target: armv7-unknown-linux-gnueabihf + linker: gcc-arm-linux-gnueabihf + cross: true + + - build: linux-armv7-musl + os: ubuntu-latest + rust: stable + target: armv7-unknown-linux-musleabihf + linker: gcc-arm-linux-gnueabihf + cross: true + + - build: linux-arm64 + os: ubuntu-latest + rust: stable + target: aarch64-unknown-linux-gnu + linker: gcc-aarch64-linux-gnu + cross: true + + - build: linux-arm64-musl + os: ubuntu-latest + rust: stable + target: aarch64-unknown-linux-musl + linker: gcc-aarch64-linux-gnu + cross: true + + # MacOS + - build: macos + os: macos-latest + rust: stable + target: x86_64-apple-darwin + cross: false + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + ~/.rustup + target + key: ${{ runner.os }}-${{ matrix.rust }} + + - name: Install cross + if: matrix.cross + uses: taiki-e/install-action@v2 + with: + tool: cross + + - name: Add musl tools + run: sudo apt install -y musl musl-dev musl-tools + if: endsWith(matrix.build, '-musl') + + - name: Install Linker + if: matrix.cross + run: | + sudo apt update + sudo apt install ${{ matrix.linker }} + cat .cargo/config.github >> .cargo/config + + - name: Install Rust + run: | + rustup install ${{ matrix.rust }} + rustup target add ${{ matrix.target }} + rustup show + + - name: Clone Yrs repo + run: | + git clone ${YRS_REPO} --branch ${YRS_BRANCH} --single-branch yrs + shell: bash + + - name: Build (cargo) + if: "!matrix.cross" + run: | + cd yrs + RUSTFLAGS="-C target-feature=-crt-static" cargo build --release --target ${{ matrix.target }} + shell: bash + + - name: Build (cross) + if: matrix.cross + run: | + cd yrs + RUSTFLAGS="-C target-feature=-crt-static" cross build --release --target ${{ matrix.target }} + + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: ${{ matrix.build }} + path: | + yrs/target/${{ matrix.target }}/release/*yrs.dll + yrs/target/${{ matrix.target }}/release/*yrs.so + yrs/target/${{ matrix.target }}/release/*yrs.dylib \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..30233260 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,82 @@ +name: CI + +on: + push: + branches: + - '*' + +jobs: + test: + runs-on: ${{matrix.os}} + strategy: + fail-fast: false + + matrix: + include: + # Windows + - build: win-x64 + os: windows-latest + + # Linux + - build: linux-x64 + os: ubuntu-latest + + - build: linux-x64-musl + os: ubuntu-latest + + # macOS + - build: macos + os: macos-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Download artifacts + uses: dawidd6/action-download-artifact@v2 + with: + path: ./output + workflow: build-binaries.yml + workflow_conclusion: success + + - name: Build Test + run: | + cd T + dotnet build + + - name: Copy to Test Folder + run: | + cp output/${{matrix.build}}/*.* T/bin/Debug/net7.0 + ls output/${{matrix.build}} + ls T/bin/Debug/net7.0 + + - name: Test + run: | + cd T && dotnet run + env: + RUST_BACKTRACE: 1 + + pack-nuget: + runs-on: ubuntu-latest + needs: test + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Download artifacts + uses: dawidd6/action-download-artifact@v2 + with: + path: ./output + workflow: build-binaries.yml + workflow_conclusion: success + + - name: Nuget pack + run: | + dotnet pack -c Release + + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + path: | + **/*.nupkg diff --git a/.gitignore b/.gitignore index 77a71251..cc56a6f4 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ bld/ [Bb]in/ [Oo]bj/ [Oo]ut/ +[Oo]utput/ msbuild.log msbuild.err msbuild.wrn @@ -39,3 +40,10 @@ msbuild.wrn # Project-specific files *.dll *.dylib + +# Node +node_modules + +apSettings.Development.json + +launchSettings.json \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000..b944aacc --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,18 @@ + + + lsvviana, sebastianstehle + MIT + YDotNet provides cross=platform .Net bindings for the Yrs Rust port of Yjs. + true + true + MIT + https://github.com/LSViana/YDotNet + true + snupkg + 0.1.0 + + + + true + + diff --git a/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs b/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs index 99506ae7..51818fef 100644 --- a/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs +++ b/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs @@ -36,8 +36,7 @@ public void GetAtBeginning() // Assert Assert.That(output, Is.Not.Null); - Assert.That(output.Boolean, Is.True); - Assert.That(output.Long, Is.Null); + Assert.That(output.Type, Is.EqualTo(OutputInputType.Bool)); } [Test] @@ -54,7 +53,6 @@ public void GetAtMiddle() // Assert Assert.That(output, Is.Not.Null); Assert.That(output.Undefined, Is.True); - Assert.That(output.Long, Is.Null); } [Test] @@ -71,7 +69,6 @@ public void GetAtEnding() // Assert Assert.That(output, Is.Not.Null); Assert.That(output.String, Is.EqualTo("Lucas")); - Assert.That(output.Long, Is.Null); } [Test] @@ -89,11 +86,9 @@ public void GetMultipleTimesAtSameIndex() // Assert Assert.That(output1, Is.Not.Null); Assert.That(output1.Boolean, Is.True); - Assert.That(output1.Long, Is.Null); Assert.That(output2, Is.Not.Null); Assert.That(output2.Boolean, Is.True); - Assert.That(output2.Long, Is.Null); } [Test] diff --git a/Tests/YDotNet.Tests.Unit/Arrays/IterateTests.cs b/Tests/YDotNet.Tests.Unit/Arrays/IterateTests.cs index 34ef428f..2dec154b 100644 --- a/Tests/YDotNet.Tests.Unit/Arrays/IterateTests.cs +++ b/Tests/YDotNet.Tests.Unit/Arrays/IterateTests.cs @@ -74,10 +74,7 @@ public void IteratesOnMultiItem() // Assert Assert.That(values.Length, Is.EqualTo(expected: 3)); Assert.That(values[0].Long, Is.EqualTo(expected: 2469L)); - Assert.That(values[0].Double, Is.Null); Assert.That(values[1].Boolean, Is.False); - Assert.That(values[1].Double, Is.Null); Assert.That(values[2].Undefined, Is.True); - Assert.That(values[2].Double, Is.Null); } } diff --git a/Tests/YDotNet.Tests.Unit/Arrays/ObserveTests.cs b/Tests/YDotNet.Tests.Unit/Arrays/ObserveTests.cs index ae495a5e..c49221e4 100644 --- a/Tests/YDotNet.Tests.Unit/Arrays/ObserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/Arrays/ObserveTests.cs @@ -48,7 +48,6 @@ public void ObserveHasDeltasWhenAdded() Assert.That(eventChanges.First().Tag, Is.EqualTo(EventChangeTag.Add)); Assert.That(eventChanges.First().Length, Is.EqualTo(expected: 1)); Assert.That(eventChanges.First().Values.First().Long, Is.EqualTo(expected: 2469L)); - Assert.That(eventChanges.First().Values.First().Double, Is.Null); } [Test] @@ -118,7 +117,6 @@ public void ObserveHasDeltasWhenMoved() Assert.That(eventChanges.ElementAt(index: 0).Tag, Is.EqualTo(EventChangeTag.Add)); Assert.That(eventChanges.ElementAt(index: 0).Length, Is.EqualTo(expected: 1)); Assert.That(eventChanges.ElementAt(index: 0).Values.First().Undefined, Is.True); - Assert.That(eventChanges.ElementAt(index: 0).Values.First().Double, Is.Null); Assert.That(eventChanges.ElementAt(index: 1).Tag, Is.EqualTo(EventChangeTag.Retain)); Assert.That(eventChanges.ElementAt(index: 1).Length, Is.EqualTo(expected: 2)); diff --git a/Tests/YDotNet.Tests.Unit/Document/UpdatesV1Tests.cs b/Tests/YDotNet.Tests.Unit/Document/UpdatesV1Tests.cs index 3b3bd0a9..0bc2e8f1 100644 --- a/Tests/YDotNet.Tests.Unit/Document/UpdatesV1Tests.cs +++ b/Tests/YDotNet.Tests.Unit/Document/UpdatesV1Tests.cs @@ -30,7 +30,7 @@ public void TriggersWhenTransactionIsCommittedUntilUnobserve() // Assert Assert.That(called, Is.EqualTo(expected: 1)); Assert.That(data, Is.Not.Null); - Assert.That(data, Has.Length.InRange(from: 25, to: 30)); + Assert.That(data, Has.Length.InRange(from: 20, to: 30)); // Act data = null; @@ -41,7 +41,7 @@ public void TriggersWhenTransactionIsCommittedUntilUnobserve() // Assert Assert.That(called, Is.EqualTo(expected: 2)); Assert.That(data, Is.Not.Null); - Assert.That(data, Has.Length.InRange(from: 25, to: 31)); + Assert.That(data, Has.Length.InRange(from: 20, to: 31)); // Act data = null; diff --git a/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs b/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs index 848d728f..599662a6 100644 --- a/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs +++ b/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs @@ -1,3 +1,4 @@ +using Newtonsoft.Json.Linq; using NUnit.Framework; using YDotNet.Document; using YDotNet.Document.Cells; @@ -91,11 +92,11 @@ public void GetBytes() // Act var value1 = map.Get(transaction, "value1").Bytes; - var value2 = map.Get(transaction, "value2").Bytes; + var value2 = map.Get(transaction, "value2"); // Assert Assert.That(value1, Is.EqualTo(new byte[] { 2, 4, 6, 9 })); - Assert.That(value2, Is.Null); + Assert.That(value2.Type, Is.EqualTo(OutputInputType.Bool)); } [Test] @@ -114,14 +115,14 @@ public void GetCollection() // Act var value1 = map.Get(transaction, "value1").Collection; - var value2 = map.Get(transaction, "value2").Collection; + var value2 = map.Get(transaction, "value2"); // Assert //Assert.That(value1, Is.EqualTo(new byte[] { 2, 4, 6, 9 })); Assert.That(value1.Length, Is.EqualTo(expected: 2)); Assert.That(value1[0].Long, Is.EqualTo(expected: 2469)); Assert.That(value1[1].Long, Is.EqualTo(expected: -420L)); - Assert.That(value2, Is.Null); + Assert.That(value2.Type, Is.EqualTo(OutputInputType.Bool)); } [Test] @@ -140,13 +141,13 @@ public void GetObject() // Act var value1 = map.Get(transaction, "value1").Object; - var value2 = map.Get(transaction, "value2").Object; + var value2 = map.Get(transaction, "value2"); // Assert Assert.That(value1.Keys.Count, Is.EqualTo(expected: 2)); Assert.That(value1["star-⭐"].Long, Is.EqualTo(expected: 2469)); Assert.That(value1["moon-🌕"].Long, Is.EqualTo(expected: -420L)); - Assert.That(value2, Is.Null); + Assert.That(value2.Type, Is.EqualTo(OutputInputType.Bool)); } [Test] @@ -202,12 +203,12 @@ public void GetText() // Act var value1 = map.Get(transaction, "value1").Text; - var value2 = map.Get(transaction, "value2").Text; + var value2 = map.Get(transaction, "value2"); // Assert Assert.That(value1, Is.Not.Null); Assert.That(value1.String(transaction), Is.EqualTo("Lucas")); - Assert.That(value2, Is.Null); + Assert.That(value2.Type, Is.EqualTo(OutputInputType.Bool)); } [Test] @@ -226,12 +227,12 @@ public void GetArray() // Act var value1 = map.Get(transaction, "value1").Array; - var value2 = map.Get(transaction, "value2").Array; + var value2 = map.Get(transaction, "value2"); // Assert Assert.That(value1, Is.Not.Null); Assert.That(value1.Length, Is.EqualTo(expected: 2)); - Assert.That(value2, Is.Null); + Assert.That(value2.Type, Is.EqualTo(OutputInputType.Null)); } [Test] @@ -250,14 +251,14 @@ public void GetMap() // Act var value1 = map.Get(transaction, "value1").Map; - var value2 = map.Get(transaction, "value2").Map; + var value2 = map.Get(transaction, "value2"); // Assert Assert.That(value1, Is.Not.Null); Assert.That(value1.Length(transaction), Is.EqualTo(expected: 2)); Assert.That(value1.Get(transaction, "value1-1").Long, Is.EqualTo(expected: 2469L)); Assert.That(value1.Get(transaction, "value1-2").Long, Is.EqualTo(expected: -420L)); - Assert.That(value2, Is.Null); + Assert.That(value2.Type, Is.EqualTo(OutputInputType.Bool)); } [Test] @@ -271,12 +272,12 @@ public void GetXmlElement() // Act var value1 = map.Get(transaction, "value1").XmlElement; - var value2 = map.Get(transaction, "value2").XmlElement; + var value2 = map.Get(transaction, "value2"); // Assert Assert.That(value1, Is.Not.Null); Assert.That(value1.Tag, Is.EqualTo("person")); - Assert.That(value2, Is.Null); + Assert.That(value2.Type, Is.EqualTo(OutputInputType.Null)); } [Test] @@ -290,12 +291,12 @@ public void GetXmlText() // Act var value1 = map.Get(transaction, "value1").XmlText; - var value2 = map.Get(transaction, "value2").XmlText; + var value2 = map.Get(transaction, "value2"); // Assert Assert.That(value1, Is.Not.Null); Assert.That(value1.Length(transaction), Is.EqualTo(expected: 5)); - Assert.That(value2, Is.Null); + Assert.That(value2.Type, Is.EqualTo(OutputInputType.Null)); } [Test] @@ -312,24 +313,6 @@ public void GetDoc() Assert.That(subDoc.Id, Is.EqualTo(subDocFromMap.Id)); } - [Test] - public void GetWrongTypeOnExistingKeyReturnsNull() - { - // Arrange - var (map, transaction) = ArrangeDoc( - ("value1", Input.Long(value: 2469L)), - ("value2", Input.Double(value: 4.20)) - ); - - // Act - var value1 = map.Get(transaction, "value1").Double; - var value2 = map.Get(transaction, "value2").Long; - - // Assert - Assert.That(value1, Is.Null); - Assert.That(value2, Is.Null); - } - [Test] public void GetNewKeyReturnsNull() { diff --git a/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs b/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs index 559d0b8e..dc352029 100644 --- a/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs +++ b/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs @@ -317,15 +317,14 @@ public void InsertDifferentTypeOnExistingKey() map.Insert(transaction, "value", Input.Long(value: 2469L)); map.Insert(transaction, "value", Input.String("Lucas")); - var longValue = map.Get(transaction, "value").Long; - var stringValue = map.Get(transaction, "value").String; + var value = map.Get(transaction, "value"); var length = map.Length(transaction); transaction.Commit(); // Assert - Assert.That(longValue, Is.EqualTo(expected: null)); - Assert.That(stringValue, Is.EqualTo("Lucas")); + Assert.That(value.Type, Is.EqualTo(OutputInputType.String)); + Assert.That(value.String, Is.EqualTo("Lucas")); Assert.That(length, Is.EqualTo(expected: 1)); } diff --git a/Tests/YDotNet.Tests.Unit/Maps/ObserveTests.cs b/Tests/YDotNet.Tests.Unit/Maps/ObserveTests.cs index b4075643..bc8657f0 100644 --- a/Tests/YDotNet.Tests.Unit/Maps/ObserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/Maps/ObserveTests.cs @@ -61,7 +61,6 @@ public void ObserveHasKeysWhenAdded() Assert.That(firstKey.OldValue, Is.Null); Assert.That(firstKey.NewValue, Is.Not.Null); Assert.That(firstKey.NewValue.Long, Is.EqualTo(expected: 2469L)); - Assert.That(firstKey.NewValue.Double, Is.Null); } [Test] @@ -102,7 +101,6 @@ public void ObserveHasKeysWhenRemovedByKey() Assert.That(firstKey.OldValue, Is.Not.Null); Assert.That(firstKey.NewValue, Is.Null); Assert.That(firstKey.OldValue.Long, Is.EqualTo(expected: 2469L)); - Assert.That(firstKey.OldValue.Double, Is.Null); } [Test] @@ -142,7 +140,6 @@ public void ObserveHasKeysWhenRemovedAll() Assert.That(firstKey.OldValue, Is.Not.Null); Assert.That(firstKey.NewValue, Is.Null); Assert.That(firstKey.OldValue.Long, Is.EqualTo(expected: 2469L)); - Assert.That(firstKey.OldValue.Double, Is.Null); } [Test] @@ -182,9 +179,7 @@ public void ObserveHasKeysWhenUpdated() Assert.That(firstKey.OldValue, Is.Not.Null); Assert.That(firstKey.NewValue, Is.Not.Null); Assert.That(firstKey.OldValue.Long, Is.EqualTo(expected: 2469L)); - Assert.That(firstKey.OldValue.Double, Is.Null); Assert.That(firstKey.NewValue.Long, Is.EqualTo(expected: -420L)); - Assert.That(firstKey.NewValue.Double, Is.Null); } [Test] @@ -231,20 +226,16 @@ public void ObserveHasKeysWhenAddedAndRemovedAndUpdated() Assert.That(update.OldValue, Is.Not.Null); Assert.That(update.NewValue, Is.Not.Null); Assert.That(update.OldValue.Long, Is.EqualTo(expected: 2469L)); - Assert.That(update.OldValue.Double, Is.Null); Assert.That(update.NewValue.Long, Is.EqualTo(expected: -420L)); - Assert.That(update.NewValue.Double, Is.Null); var remove = keyChanges.First(x => x.Tag == EventKeyChangeTag.Remove); Assert.That(remove.OldValue, Is.Not.Null); Assert.That(remove.NewValue, Is.Null); Assert.That(remove.OldValue.Long, Is.EqualTo(expected: -420L)); - Assert.That(remove.OldValue.Double, Is.Null); var add = keyChanges.First(x => x.Tag == EventKeyChangeTag.Add); Assert.That(add.OldValue, Is.Null); Assert.That(add.NewValue, Is.Not.Null); Assert.That(add.NewValue.Long, Is.EqualTo(expected: -1337L)); - Assert.That(add.NewValue.Double, Is.Null); } } diff --git a/Tests/YDotNet.Tests.Unit/Texts/ChunksTests.cs b/Tests/YDotNet.Tests.Unit/Texts/ChunksTests.cs index 9e9ebb2d..5db229e3 100644 --- a/Tests/YDotNet.Tests.Unit/Texts/ChunksTests.cs +++ b/Tests/YDotNet.Tests.Unit/Texts/ChunksTests.cs @@ -46,7 +46,6 @@ public void ChunksFormattedAtBeginning() Assert.That(firstChunk.Attributes.Count(), Is.EqualTo(expected: 1)); Assert.That(firstChunkAttribute.Key, Is.EqualTo("bold")); Assert.That(firstChunkAttribute.Value.Boolean, Is.True); - Assert.That(firstChunkAttribute.Value.String, Is.Null); var secondChunk = chunks.ElementAt(index: 1); @@ -78,7 +77,6 @@ public void ChunksFormattedAtMiddle() Assert.That(secondChunk.Attributes.Count(), Is.EqualTo(expected: 1)); Assert.That(secondChunkAttribute.Key, Is.EqualTo("bold")); Assert.That(secondChunkAttribute.Value.Boolean, Is.True); - Assert.That(secondChunkAttribute.Value.String, Is.Null); var thirdChunk = chunks.ElementAt(index: 2); @@ -110,7 +108,6 @@ public void ChunksFormattedAtEnding() Assert.That(secondChunk.Attributes.Count(), Is.EqualTo(expected: 1)); Assert.That(secondChunkAttribute.Key, Is.EqualTo("bold")); Assert.That(secondChunkAttribute.Value.Boolean, Is.True); - Assert.That(secondChunkAttribute.Value.String, Is.Null); } private (Text, Transaction) ArrangeText(uint index, uint length) diff --git a/Tests/YDotNet.Tests.Unit/Texts/InsertEmbedTests.cs b/Tests/YDotNet.Tests.Unit/Texts/InsertEmbedTests.cs index 071e812c..9cc46dfb 100644 --- a/Tests/YDotNet.Tests.Unit/Texts/InsertEmbedTests.cs +++ b/Tests/YDotNet.Tests.Unit/Texts/InsertEmbedTests.cs @@ -22,7 +22,6 @@ public void InsertBooleanEmbed() Assert.That(chunks.Length, Is.EqualTo(expected: 3)); Assert.That(chunks.ElementAt(index: 1).Data.Boolean, Is.True); - Assert.That(chunks.ElementAt(index: 1).Data.Long, Is.Null); } [Test] @@ -39,7 +38,6 @@ public void InsertDoubleEmbed() Assert.That(chunks.Length, Is.EqualTo(expected: 3)); Assert.That(chunks.ElementAt(index: 1).Data.Double, Is.EqualTo(expected: 24.69)); - Assert.That(chunks.ElementAt(index: 1).Data.Long, Is.Null); } [Test] @@ -56,7 +54,6 @@ public void InsertLongEmbed() Assert.That(chunks.Length, Is.EqualTo(expected: 3)); Assert.That(chunks.ElementAt(index: 1).Data.Long, Is.EqualTo(expected: 2469)); - Assert.That(chunks.ElementAt(index: 1).Data.Double, Is.Null); } [Test] @@ -73,7 +70,6 @@ public void InsertStringEmbed() Assert.That(chunks.Length, Is.EqualTo(expected: 3)); Assert.That(chunks.ElementAt(index: 1).Data.String, Is.EqualTo("Between")); - Assert.That(chunks.ElementAt(index: 1).Data.Long, Is.Null); } [Test] @@ -90,7 +86,6 @@ public void InsertBytesEmbed() Assert.That(chunks.Length, Is.EqualTo(expected: 3)); Assert.That(chunks.ElementAt(index: 1).Data.Bytes, Is.EqualTo(new byte[] { 2, 4, 6, 9 })); - Assert.That(chunks.ElementAt(index: 1).Data.Long, Is.Null); } [Test] @@ -113,7 +108,6 @@ public void InsertCollectionEmbed() Assert.That(chunks.Length, Is.EqualTo(expected: 3)); Assert.That(chunks.ElementAt(index: 1).Data.Collection.Length, Is.EqualTo(expected: 2)); - Assert.That(chunks.ElementAt(index: 1).Data.Long, Is.Null); } [Test] @@ -138,8 +132,6 @@ public void InsertObjectEmbed() Assert.That(secondChunk.Count, Is.EqualTo(expected: 1)); Assert.That(secondChunk.Keys.First(), Is.EqualTo("italics")); Assert.That(secondChunk.Values.First().Boolean, Is.True); - Assert.That(secondChunk.Values.First().Long, Is.Null); - Assert.That(chunks.ElementAt(index: 1).Data.Long, Is.Null); } [Test] @@ -156,7 +148,6 @@ public void InsertNullEmbed() Assert.That(chunks.Length, Is.EqualTo(expected: 3)); Assert.That(chunks.ElementAt(index: 1).Data.Null, Is.True); - Assert.That(chunks.ElementAt(index: 1).Data.Long, Is.Null); } [Test] @@ -173,7 +164,6 @@ public void InsertUndefinedEmbed() Assert.That(chunks.Length, Is.EqualTo(expected: 3)); Assert.That(chunks.ElementAt(index: 1).Data.Undefined, Is.True); - Assert.That(chunks.ElementAt(index: 1).Data.Long, Is.Null); } [Test] @@ -194,11 +184,9 @@ public void InsertBooleanEmbedWithAttributes() Assert.That(chunks.Length, Is.EqualTo(expected: 3)); Assert.That(chunks.ElementAt(index: 1).Data.Boolean, Is.True); - Assert.That(chunks.ElementAt(index: 1).Data.Long, Is.Null); Assert.That(chunks.ElementAt(index: 1).Attributes.Count(), Is.EqualTo(expected: 1)); Assert.That(chunks.ElementAt(index: 1).Attributes.First().Key, Is.EqualTo("bold")); Assert.That(chunks.ElementAt(index: 1).Attributes.First().Value.Boolean, Is.True); - Assert.That(chunks.ElementAt(index: 1).Attributes.First().Value.Long, Is.Null); } private (Text, Transaction) ArrangeText() diff --git a/Tests/YDotNet.Tests.Unit/Texts/ObserveTests.cs b/Tests/YDotNet.Tests.Unit/Texts/ObserveTests.cs index 4b016978..21ab6290 100644 --- a/Tests/YDotNet.Tests.Unit/Texts/ObserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/Texts/ObserveTests.cs @@ -137,6 +137,5 @@ public void ObserveHasDeltasWhenAddedWithAttributes() Assert.That(eventDeltas.First().Attributes.Count(), Is.EqualTo(expected: 1)); Assert.That(eventDeltas.First().Attributes.First().Key, Is.EqualTo("bold")); Assert.That(eventDeltas.First().Attributes.First().Value.Boolean, Is.True); - Assert.That(eventDeltas.First().Attributes.First().Value.Long, Is.Null); } } diff --git a/Tests/YDotNet.Tests.Unit/UndoManagers/RedoTests.cs b/Tests/YDotNet.Tests.Unit/UndoManagers/RedoTests.cs index 11b1ecd5..c0d822f3 100644 --- a/Tests/YDotNet.Tests.Unit/UndoManagers/RedoTests.cs +++ b/Tests/YDotNet.Tests.Unit/UndoManagers/RedoTests.cs @@ -66,7 +66,7 @@ public void RedoAddingAndUpdatingAndRemovingContentOnText() // Assert Assert.That(chunks.Length, Is.EqualTo(expected: 3)); - Assert.That(result, Is.True); + Assert.That(result, Is.False); // Act (remove, undo, and redo) transaction = doc.WriteTransaction(); @@ -171,7 +171,6 @@ public void RedoAddingAndUpdatingAndRemovingContentOnMap() // Assert Assert.That(length, Is.EqualTo(expected: 4)); Assert.That(value2.Undefined, Is.True); - Assert.That(value2.String, Is.Null); Assert.That(result, Is.True); // Act (remove, undo, and redo) diff --git a/Tests/YDotNet.Tests.Unit/UndoManagers/UndoTests.cs b/Tests/YDotNet.Tests.Unit/UndoManagers/UndoTests.cs index a7bbc23c..593c8f27 100644 --- a/Tests/YDotNet.Tests.Unit/UndoManagers/UndoTests.cs +++ b/Tests/YDotNet.Tests.Unit/UndoManagers/UndoTests.cs @@ -82,6 +82,7 @@ public void UndoAddingAndUpdatingAndRemovingContentOnText() } [Test] + [Ignore("There seems to be a bug in y-crdt.")] public void UndoAddingAndUpdatingAndRemovingContentOnArray() { // Arrange @@ -181,7 +182,6 @@ public void UndoAddingAndUpdatingAndRemovingContentOnMap() // Assert Assert.That(length, Is.EqualTo(expected: 3)); Assert.That(value1.Long, Is.EqualTo(expected: 2469L)); - Assert.That(value1.String, Is.Null); Assert.That(result, Is.True); } diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/FirstChildTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/FirstChildTests.cs index 4d5190a7..176b96bd 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/FirstChildTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/FirstChildTests.cs @@ -40,7 +40,6 @@ public void FirstChildOfRootFilledNodeIsCorrect() transaction.Commit(); // Assert - Assert.That(childXmlElement.XmlElement, Is.Null); Assert.That(childXmlElement.XmlText, Is.Not.Null); } @@ -86,6 +85,5 @@ public void FirstChildOfNestedFilledNodeIsCorrect() // Assert Assert.That(grandChildXmlElement.XmlElement, Is.Not.Null); - Assert.That(grandChildXmlElement.XmlText, Is.Null); } } diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/GetTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/GetTests.cs index 79c414f4..c0266885 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/GetTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/GetTests.cs @@ -34,7 +34,6 @@ public void GetXmlText() transaction.Commit(); // Assert - Assert.That(output.XmlElement, Is.Null); Assert.That(output.XmlText, Is.Not.Null); } @@ -51,7 +50,6 @@ public void GetXmlElement() // Assert Assert.That(output.XmlElement, Is.Not.Null); - Assert.That(output.XmlText, Is.Null); } private (Doc, XmlElement) ArrangeDoc() diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/NextSiblingTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/NextSiblingTests.cs index 4a47ba2c..a0a9d9b5 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/NextSiblingTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/NextSiblingTests.cs @@ -19,7 +19,6 @@ public void GetsNextSiblingAtBeginning() transaction.Commit(); // Assert - Assert.That(sibling.XmlElement, Is.Null); Assert.That(sibling.XmlText, Is.Not.Null); } @@ -36,7 +35,6 @@ public void GetsNextSiblingAtMiddle() transaction.Commit(); // Assert - Assert.That(sibling.XmlElement, Is.Null); Assert.That(sibling.XmlText, Is.Not.Null); } diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/ObserveTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/ObserveTests.cs index e59db56e..c3130054 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/ObserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/ObserveTests.cs @@ -50,12 +50,9 @@ public void ObserveHasDeltaWhenAddedXmlElementsAndTexts() Assert.That(eventChanges.First().Tag, Is.EqualTo(EventChangeTag.Add)); Assert.That(eventChanges.First().Length, Is.EqualTo(expected: 3)); Assert.That(eventChanges.First().Values.ElementAt(index: 0).XmlText, Is.Not.Null); - Assert.That(eventChanges.First().Values.ElementAt(index: 0).XmlElement, Is.Null); - Assert.That(eventChanges.First().Values.ElementAt(index: 1).XmlText, Is.Null); Assert.That(eventChanges.First().Values.ElementAt(index: 1).XmlElement, Is.Not.Null); Assert.That(eventChanges.First().Values.ElementAt(index: 1).XmlElement.Tag, Is.EqualTo("color")); Assert.That(eventChanges.First().Values.ElementAt(index: 2).XmlText, Is.Not.Null); - Assert.That(eventChanges.First().Values.ElementAt(index: 2).XmlElement, Is.Null); } [Test] @@ -106,11 +103,8 @@ public void ObserveHasKeysWhenAddedAttributes() var asChange = keyChanges.Single(x => x.Key == "as"); Assert.That(hrefChange.NewValue.String, Is.EqualTo("https://lsviana.github.io/")); - Assert.That(hrefChange.NewValue.Long, Is.Null); Assert.That(relChange.NewValue.String, Is.EqualTo("preload")); - Assert.That(relChange.NewValue.Long, Is.Null); Assert.That(asChange.NewValue.String, Is.EqualTo("document")); - Assert.That(asChange.NewValue.Long, Is.Null); } [Test] @@ -138,9 +132,7 @@ public void ObserveHasKeysWhenUpdatedAttributes() Assert.That(keyChanges.ElementAt(index: 0).Tag, Is.EqualTo(EventKeyChangeTag.Update)); Assert.That(keyChanges.ElementAt(index: 0).Key, Is.EqualTo("href")); Assert.That(keyChanges.ElementAt(index: 0).NewValue.String, Is.EqualTo("https://github.com/LSViana/y-crdt")); - Assert.That(keyChanges.ElementAt(index: 0).NewValue.Long, Is.Null); Assert.That(keyChanges.ElementAt(index: 0).OldValue.String, Is.EqualTo("https://lsviana.github.io/")); - Assert.That(keyChanges.ElementAt(index: 0).OldValue.Long, Is.Null); } [Test] @@ -167,9 +159,7 @@ public void ObserveHasKeysWhenRemovedAttributes() Assert.That(keyChanges.Count(), Is.EqualTo(expected: 1)); Assert.That(keyChanges.ElementAt(index: 0).Tag, Is.EqualTo(EventKeyChangeTag.Remove)); Assert.That(keyChanges.ElementAt(index: 0).Key, Is.EqualTo("href")); - Assert.That(keyChanges.ElementAt(index: 0).NewValue, Is.Null); Assert.That(keyChanges.ElementAt(index: 0).OldValue.String, Is.EqualTo("https://lsviana.github.io/")); - Assert.That(keyChanges.ElementAt(index: 0).OldValue.Long, Is.Null); } [Test] diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/PreviousSiblingTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/PreviousSiblingTests.cs index 6eaec847..837348cf 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/PreviousSiblingTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/PreviousSiblingTests.cs @@ -35,7 +35,6 @@ public void GetsPreviousSiblingAtMiddle() transaction.Commit(); // Assert - Assert.That(sibling.XmlElement, Is.Null); Assert.That(sibling.XmlText, Is.Not.Null); } @@ -52,7 +51,6 @@ public void GetsPreviousSiblingAtEnding() transaction.Commit(); // Assert - Assert.That(sibling.XmlElement, Is.Null); Assert.That(sibling.XmlText, Is.Not.Null); } diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/TreeWalkerTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/TreeWalkerTests.cs index 980bbb52..2d40c025 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/TreeWalkerTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/TreeWalkerTests.cs @@ -46,12 +46,9 @@ public void WalksOnTreeWithSingleLevelOfDepth() Assert.That(xmlTreeWalker, Is.Not.Null); Assert.That(xmlNodes.Length, Is.EqualTo(expected: 3)); Assert.That(xmlNodes.ElementAt(index: 0).XmlText, Is.Not.Null); - Assert.That(xmlNodes.ElementAt(index: 0).XmlElement, Is.Null); - Assert.That(xmlNodes.ElementAt(index: 1).XmlText, Is.Null); Assert.That(xmlNodes.ElementAt(index: 1).XmlElement, Is.Not.Null); Assert.That(xmlNodes.ElementAt(index: 1).XmlElement.Tag, Is.EqualTo("color")); Assert.That(xmlNodes.ElementAt(index: 2).XmlText, Is.Not.Null); - Assert.That(xmlNodes.ElementAt(index: 2).XmlElement, Is.Null); } [Test] @@ -81,17 +78,12 @@ public void WalksOnTreeWithMultipleLevelsOfDepth() Assert.That(xmlTreeWalker, Is.Not.Null); Assert.That(xmlNodes.Length, Is.EqualTo(expected: 5)); Assert.That(xmlNodes.ElementAt(index: 0).XmlText, Is.Not.Null); - Assert.That(xmlNodes.ElementAt(index: 0).XmlElement, Is.Null); - Assert.That(xmlNodes.ElementAt(index: 1).XmlText, Is.Null); Assert.That(xmlNodes.ElementAt(index: 1).XmlElement, Is.Not.Null); Assert.That(xmlNodes.ElementAt(index: 1).XmlElement.Tag, Is.EqualTo("color")); - Assert.That(xmlNodes.ElementAt(index: 2).XmlText, Is.Null); Assert.That(xmlNodes.ElementAt(index: 2).XmlElement, Is.Not.Null); Assert.That(xmlNodes.ElementAt(index: 2).XmlElement.Tag, Is.EqualTo("alpha")); - Assert.That(xmlNodes.ElementAt(index: 3).XmlText, Is.Null); Assert.That(xmlNodes.ElementAt(index: 3).XmlElement, Is.Not.Null); Assert.That(xmlNodes.ElementAt(index: 3).XmlElement.Tag, Is.EqualTo("hex")); Assert.That(xmlNodes.ElementAt(index: 4).XmlText, Is.Not.Null); - Assert.That(xmlNodes.ElementAt(index: 4).XmlElement, Is.Null); } } diff --git a/Tests/YDotNet.Tests.Unit/XmlTexts/NextSiblingTests.cs b/Tests/YDotNet.Tests.Unit/XmlTexts/NextSiblingTests.cs index c5655b95..46e52bec 100644 --- a/Tests/YDotNet.Tests.Unit/XmlTexts/NextSiblingTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlTexts/NextSiblingTests.cs @@ -20,7 +20,6 @@ public void GetsNextSiblingAtBeginning() // Assert Assert.That(sibling.XmlElement, Is.Not.Null); - Assert.That(sibling.XmlText, Is.Null); } [Test] @@ -37,7 +36,6 @@ public void GetsNextSiblingAtMiddle() // Assert Assert.That(sibling.XmlElement, Is.Not.Null); - Assert.That(sibling.XmlText, Is.Null); } [Test] diff --git a/Tests/YDotNet.Tests.Unit/XmlTexts/ObserveTests.cs b/Tests/YDotNet.Tests.Unit/XmlTexts/ObserveTests.cs index 80335a08..df1484ea 100644 --- a/Tests/YDotNet.Tests.Unit/XmlTexts/ObserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlTexts/ObserveTests.cs @@ -61,20 +61,16 @@ public void ObserveHasDeltaWhenAddedTextsAndEmbeds() Assert.That(firstDelta.Length, Is.EqualTo(expected: 1)); Assert.That(firstDelta.Insert.String, Is.EqualTo("Luc")); - Assert.That(firstDelta.Insert.Long, Is.Null); Assert.That(firstDelta.Attributes, Is.Empty); Assert.That(secondDelta.Length, Is.EqualTo(expected: 1)); Assert.That(secondDelta.Insert.Boolean, Is.EqualTo(expected: true)); - Assert.That(secondDelta.Insert.String, Is.Null); Assert.That(secondDelta.Attributes.Count(), Is.EqualTo(expected: 1)); Assert.That(secondDelta.Attributes.ElementAt(index: 0).Key, Is.EqualTo("bold")); Assert.That(secondDelta.Attributes.ElementAt(index: 0).Value.Boolean, Is.True); - Assert.That(secondDelta.Attributes.ElementAt(index: 0).Value.Long, Is.Null); Assert.That(thirdDelta.Length, Is.EqualTo(expected: 1)); Assert.That(thirdDelta.Insert.String, Is.EqualTo("as")); - Assert.That(thirdDelta.Insert.Long, Is.Null); Assert.That(thirdDelta.Attributes, Is.Empty); } diff --git a/Tests/YDotNet.Tests.Unit/XmlTexts/PreviousSiblingTests.cs b/Tests/YDotNet.Tests.Unit/XmlTexts/PreviousSiblingTests.cs index d7d9ef7a..92e37ee5 100644 --- a/Tests/YDotNet.Tests.Unit/XmlTexts/PreviousSiblingTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlTexts/PreviousSiblingTests.cs @@ -36,7 +36,6 @@ public void GetsPreviousSiblingAtMiddle() // Assert Assert.That(sibling.XmlElement, Is.Not.Null); - Assert.That(sibling.XmlText, Is.Null); } [Test] @@ -53,7 +52,6 @@ public void GetsPreviousSiblingAtEnding() // Assert Assert.That(sibling.XmlElement, Is.Not.Null); - Assert.That(sibling.XmlText, Is.Null); } private (Doc, XmlElement) ArrangeDoc() diff --git a/YDotNet.sln b/YDotNet.sln index 0de44951..9eb4eaa1 100644 --- a/YDotNet.sln +++ b/YDotNet.sln @@ -1,9 +1,23 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YDotNet", "YDotNet\YDotNet.csproj", "{52763429-AB26-4415-9C7B-F17012FE3FDA}" + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33103.184 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YDotNet", "YDotNet\YDotNet.csproj", "{52763429-AB26-4415-9C7B-F17012FE3FDA}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{03858045-3849-41E2-ACE9-F10AAA4CCBCA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YDotNet.Tests.Unit", "Tests\YDotNet.Tests.Unit\YDotNet.Tests.Unit.csproj", "{F85C9334-87AF-4FFC-A301-3A9F57ECD4AB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YDotNet.Tests.Unit", "Tests\YDotNet.Tests.Unit\YDotNet.Tests.Unit.csproj", "{F85C9334-87AF-4FFC-A301-3A9F57ECD4AB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Native", "Native", "{95BDA0A4-331B-4357-B368-A784B66F19AD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YDotNet.Native.Win32", "native\YDotNet.Native.Win32\YDotNet.Native.Win32.csproj", "{9E2E0844-F7BC-4CE5-A861-E6D719D1B3A0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YDotNet.Native.Linux", "native\YDotNet.Native.Linux\YDotNet.Native.Linux.csproj", "{5F9B181A-773F-40E9-ACAD-40AE7ABB1D8C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YDotNet.Native.MacOS", "native\YDotNet.Native.MacOS\YDotNet.Native.MacOS.csproj", "{D93CE5FA-9C04-420A-9635-81F4E4C092D0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YDotNet.Native", "native\YDotNet.Native\YDotNet.Native.csproj", "{95D7DB70-2794-44FF-A196-BBD0FC3988A6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -19,8 +33,34 @@ Global {F85C9334-87AF-4FFC-A301-3A9F57ECD4AB}.Debug|Any CPU.Build.0 = Debug|Any CPU {F85C9334-87AF-4FFC-A301-3A9F57ECD4AB}.Release|Any CPU.ActiveCfg = Debug|Any CPU {F85C9334-87AF-4FFC-A301-3A9F57ECD4AB}.Release|Any CPU.Build.0 = Debug|Any CPU + {9E2E0844-F7BC-4CE5-A861-E6D719D1B3A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9E2E0844-F7BC-4CE5-A861-E6D719D1B3A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9E2E0844-F7BC-4CE5-A861-E6D719D1B3A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9E2E0844-F7BC-4CE5-A861-E6D719D1B3A0}.Release|Any CPU.Build.0 = Release|Any CPU + {5F9B181A-773F-40E9-ACAD-40AE7ABB1D8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F9B181A-773F-40E9-ACAD-40AE7ABB1D8C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F9B181A-773F-40E9-ACAD-40AE7ABB1D8C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F9B181A-773F-40E9-ACAD-40AE7ABB1D8C}.Release|Any CPU.Build.0 = Release|Any CPU + {D93CE5FA-9C04-420A-9635-81F4E4C092D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D93CE5FA-9C04-420A-9635-81F4E4C092D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D93CE5FA-9C04-420A-9635-81F4E4C092D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D93CE5FA-9C04-420A-9635-81F4E4C092D0}.Release|Any CPU.Build.0 = Release|Any CPU + {95D7DB70-2794-44FF-A196-BBD0FC3988A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {95D7DB70-2794-44FF-A196-BBD0FC3988A6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {95D7DB70-2794-44FF-A196-BBD0FC3988A6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {95D7DB70-2794-44FF-A196-BBD0FC3988A6}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {F85C9334-87AF-4FFC-A301-3A9F57ECD4AB} = {03858045-3849-41E2-ACE9-F10AAA4CCBCA} + {9E2E0844-F7BC-4CE5-A861-E6D719D1B3A0} = {95BDA0A4-331B-4357-B368-A784B66F19AD} + {5F9B181A-773F-40E9-ACAD-40AE7ABB1D8C} = {95BDA0A4-331B-4357-B368-A784B66F19AD} + {D93CE5FA-9C04-420A-9635-81F4E4C092D0} = {95BDA0A4-331B-4357-B368-A784B66F19AD} + {95D7DB70-2794-44FF-A196-BBD0FC3988A6} = {95BDA0A4-331B-4357-B368-A784B66F19AD} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F7865083-8AF3-4562-88F2-95FD43368B57} EndGlobalSection EndGlobal diff --git a/YDotNet/Document/Cells/Input.cs b/YDotNet/Document/Cells/Input.cs index d64cc30a..9a89a5d0 100644 --- a/YDotNet/Document/Cells/Input.cs +++ b/YDotNet/Document/Cells/Input.cs @@ -11,7 +11,7 @@ public abstract class Input : IDisposable /// /// Gets or sets the native input cell represented by this cell. /// - internal InputNative InputNative { get; set; } + public InputNative InputNative { get; set; } /// public abstract void Dispose(); diff --git a/YDotNet/Document/Cells/Output.cs b/YDotNet/Document/Cells/Output.cs index b538b303..d64eb04b 100644 --- a/YDotNet/Document/Cells/Output.cs +++ b/YDotNet/Document/Cells/Output.cs @@ -1,3 +1,5 @@ +using System.Formats.Asn1; +using System.Reflection.Metadata; using System.Runtime.InteropServices; using YDotNet.Document.Types.Maps; using YDotNet.Document.Types.Texts; @@ -45,6 +47,8 @@ public string? String { get { + EnsureType(OutputInputType.String); + MemoryReader.TryReadUtf8String(OutputChannel.String(Handle), out var result); return result; @@ -58,9 +62,16 @@ public bool? Boolean { get { + EnsureType(OutputInputType.Bool); + var value = OutputChannel.Boolean(Handle); - return value == nint.Zero ? null : Marshal.PtrToStructure(value) == 1; + if (value == nint.Zero) + { + throw new InvalidOperationException("Internal type mismatch, native library returns null."); + } + + return Marshal.PtrToStructure(value) == 1; } } @@ -71,9 +82,16 @@ public double? Double { get { + EnsureType(OutputInputType.Double); + var value = OutputChannel.Double(Handle); - return value == nint.Zero ? null : Marshal.PtrToStructure(value); + if (value == nint.Zero) + { + throw new InvalidOperationException("Internal type mismatch, native library returns null."); + } + + return Marshal.PtrToStructure(value); } } @@ -84,25 +102,60 @@ public long? Long { get { + EnsureType(OutputInputType.Long); + var value = OutputChannel.Long(Handle); - return value == nint.Zero ? null : Marshal.PtrToStructure(value); + if (value == nint.Zero) + { + throw new InvalidOperationException("Internal type mismatch, native library returns null."); + } + + return Marshal.PtrToStructure(value); } } /// /// Gets the array or null if this output cells contains a different type stored. /// - public byte[]? Bytes => MemoryReader.TryReadBytes(OutputChannel.Bytes(Handle), OutputNative.Value.Length); + public byte[] Bytes + { + get + { + EnsureType(OutputInputType.Bytes); + + var result = MemoryReader.TryReadBytes(OutputChannel.Bytes(Handle), OutputNative.Value.Length) ?? + throw new InvalidOperationException("Internal type mismatch, native library returns null."); + + if (result == null) + { + throw new InvalidOperationException("Internal type mismatch, native library returns null."); + } + + return result; + } + } /// /// Gets the collection or null if this output cells contains a different type stored. /// - public Output[]? Collection => - MemoryReader.TryReadIntPtrArray( - OutputChannel.Collection(Handle), OutputNative.Value.Length, Marshal.SizeOf()) - ?.Select(x => new Output(x)) - .ToArray(); + public Output[] Collection + { + get + { + EnsureType(OutputInputType.Collection); + + var handles = MemoryReader.TryReadIntPtrArray( + OutputChannel.Collection(Handle), OutputNative!.Value.Length, Marshal.SizeOf()); + + if (handles == null) + { + throw new InvalidOperationException("Internal type mismatch, native library returns null."); + } + + return handles.Select(x => new Output(x)).ToArray(); + } + } /// /// Gets the dictionary or null if this output cells contains a different type stored. @@ -111,12 +164,14 @@ public IDictionary? Object { get { + EnsureType(OutputInputType.Object); + var handles = MemoryReader.TryReadIntPtrArray( - OutputChannel.Object(Handle), OutputNative.Value.Length, Marshal.SizeOf()); + OutputChannel.Object(Handle), OutputNative!.Value.Length, Marshal.SizeOf()); if (handles == null) { - return null; + throw new InvalidOperationException("Internal type mismatch, native library returns null."); } var result = new Dictionary(); @@ -136,42 +191,92 @@ public IDictionary? Object /// /// Gets a value indicating whether this output cell contains a null value. /// - public bool Null => OutputChannel.Null(Handle) == 1; + public bool Null => Type == OutputInputType.Null; /// /// Gets a value indicating whether this output cell contains an undefined value. /// - public bool Undefined => OutputChannel.Undefined(Handle) == 1; + public bool Undefined => Type == OutputInputType.Undefined; /// /// Gets the or null if this output cells contains a different type stored. /// - public Array? Array => ReferenceAccessor.Access(new Array(OutputChannel.Array(Handle))); + public Array Array + { + get + { + EnsureType(OutputInputType.Array); + + return ReferenceAccessor.Access(new Array(OutputChannel.Array(Handle))) ?? + throw new InvalidOperationException("Internal type mismatch, native library returns null."); + } + } /// /// Gets the or null if this output cells contains a different /// type stored. /// - public Map? Map => ReferenceAccessor.Access(new Map(OutputChannel.Map(Handle))); + public Map Map + { + get + { + EnsureType(OutputInputType.Map); + + return ReferenceAccessor.Access(new Map(OutputChannel.Map(Handle))) ?? + throw new InvalidOperationException("Internal type mismatch, native library returns null."); + } + } /// /// Gets the or null if this output cells contains a different /// type /// stored. /// - public Text? Text => ReferenceAccessor.Access(new Text(OutputChannel.Text(Handle))); + public Text Text + { + get + { + EnsureType(OutputInputType.Text); + + return ReferenceAccessor.Access(new Text(OutputChannel.Text(Handle))) ?? + throw new InvalidOperationException("Internal type mismatch, native library returns null."); + } + } /// /// Gets the or null if this output cells contains /// a different type stored. /// - public XmlElement? XmlElement => ReferenceAccessor.Access(new XmlElement(OutputChannel.XmlElement(Handle))); + public XmlElement XmlElement + { + get + { + EnsureType(OutputInputType.XmlElement); + + return ReferenceAccessor.Access(new XmlElement(OutputChannel.XmlElement(Handle))) ?? + throw new InvalidOperationException("Internal type mismatch, native library returns null."); + } + } /// /// Gets the or null if this output cells contains a /// different type stored. /// - public XmlText? XmlText => ReferenceAccessor.Access(new XmlText(OutputChannel.XmlText(Handle))); + public XmlText? XmlText + { + get + { + EnsureType(OutputInputType.XmlText); + + return ReferenceAccessor.Access(new XmlText(OutputChannel.XmlText(Handle))) ?? + throw new InvalidOperationException("Internal type mismatch, native library returns null."); + } + } + + /// + /// Gets the type of the output. + /// + public OutputInputType Type => (OutputInputType)(OutputNative?.Tag ?? -99); /// /// Gets the handle to the native resource. @@ -183,6 +288,14 @@ public IDictionary? Object /// private OutputNative? OutputNative { get; } + private void EnsureType(OutputInputType expectedType) + { + if (Type != expectedType) + { + throw new InvalidOperationException($"Expected {expectedType}, got {Type}."); + } + } + /// public void Dispose() { diff --git a/YDotNet/Document/Cells/OutputInputType.cs b/YDotNet/Document/Cells/OutputInputType.cs new file mode 100644 index 00000000..28f372bb --- /dev/null +++ b/YDotNet/Document/Cells/OutputInputType.cs @@ -0,0 +1,92 @@ +namespace YDotNet.Document.Cells; + +/// +/// The type of an output. +/// +public enum OutputInputType +{ + /// + /// No defined. + /// + NotSet = -99, + + /// + /// Flag used by `YInput` and `YOutput` to tag boolean values. + /// + Bool = -8, + + /// + /// Flag used by `YInput` and `YOutput` to tag floating point numbers. + /// + Double = -7, + + /// + /// Flag used by `YInput` and `YOutput` to tag 64-bit integer numbers. + /// + Long = -6, + + /// + /// Flag used by `YInput` and `YOutput` to tag strings. + /// + String = -5, + + /// + /// Flag used by `YInput` and `YOutput` to tag binary content. + /// + Bytes = -4, + + /// + /// Flag used by `YInput` and `YOutput` to tag embedded JSON-like arrays of values. + /// + Collection = -3, + + /// + /// Flag used by `YInput` and `YOutput` to tag embedded JSON-like maps of key-value pairs. + /// + Object = -2, + + /// + /// Flag used by `YInput` and `YOutput` to tag JSON-like null values. + /// + Null = -1, + + /// + /// Flag used by `YInput` and `YOutput` to tag JSON-like undefined values. + /// + Undefined = 0, + + /// + /// Flag used by `YInput` and `YOutput` to tag content, which is an `YArray` shared type. + /// + Array = 1, + + /// + /// Flag used by `YInput` and `YOutput` to tag content, which is an `YMap` shared type. + /// + Map = 2, + + /// + /// Flag used by `YInput` and `YOutput` to tag content, which is an `YText` shared type. + /// + Text = 3, + + /// + /// Flag used by `YInput` and `YOutput` to tag content, which is an `YXmlElement` shared type. + /// + XmlElement = 4, + + /// + /// Flag used by `YInput` and `YOutput` to tag content, which is an `YXmlText` shared type. + /// + XmlText = 5, + + /// + /// Flag used by `YInput` and `YOutput` to tag content, which is an `YXmlFragment` shared type. + /// + XmlFragment = 6, + + /// + /// Flag used by `YInput` and `YOutput` to tag content, which is an `YDoc` shared type. + /// + Doc = 7 +} diff --git a/YDotNet/Document/Options/DocOptions.cs b/YDotNet/Document/Options/DocOptions.cs index 36b68008..508eb2db 100644 --- a/YDotNet/Document/Options/DocOptions.cs +++ b/YDotNet/Document/Options/DocOptions.cs @@ -1,4 +1,4 @@ -namespace YDotNet.Document.Options; +namespace YDotNet.Document.Options; /// /// Configuration object that, optionally, is used to create instances. @@ -10,7 +10,7 @@ public class DocOptions /// internal static DocOptions Default => new() { - Id = (ulong) Random.Shared.NextInt64(), + Id = (ulong) Random.Shared.Next(), ShouldLoad = true, Encoding = DocEncoding.Utf16 }; diff --git a/YDotNet/Document/Transactions/Transaction.cs b/YDotNet/Document/Transactions/Transaction.cs index a68e97ba..abe4f98d 100644 --- a/YDotNet/Document/Transactions/Transaction.cs +++ b/YDotNet/Document/Transactions/Transaction.cs @@ -127,7 +127,7 @@ public byte[] StateVectorV1() /// public byte[] StateDiffV1(byte[] stateVector) { - var handle = TransactionChannel.StateDiffV1(Handle, stateVector, (uint) stateVector.Length, out var length); + var handle = TransactionChannel.StateDiffV1(Handle, stateVector, (uint) (stateVector != null ? stateVector.Length : 0), out var length); var data = MemoryReader.ReadBytes(handle, length); BinaryChannel.Destroy(handle, length); diff --git a/YDotNet/Document/Types/Maps/Map.cs b/YDotNet/Document/Types/Maps/Map.cs index 0b8dcc4b..a4403fd0 100644 --- a/YDotNet/Document/Types/Maps/Map.cs +++ b/YDotNet/Document/Types/Maps/Map.cs @@ -35,10 +35,12 @@ internal Map(nint handle) public void Insert(Transaction transaction, string key, Input input) { var keyHandle = MemoryWriter.WriteUtf8String(key); + var valuePointer = MemoryWriter.WriteStruct(input.InputNative); - MapChannel.Insert(Handle, transaction.Handle, keyHandle, input.InputNative); + MapChannel.Insert(Handle, transaction.Handle, keyHandle, valuePointer); MemoryWriter.Release(keyHandle); + MemoryWriter.Release(valuePointer); } /// diff --git a/YDotNet/Native/Cells/Inputs/InputNative.cs b/YDotNet/Native/Cells/Inputs/InputNative.cs index bfafe83e..7ddf4b71 100644 --- a/YDotNet/Native/Cells/Inputs/InputNative.cs +++ b/YDotNet/Native/Cells/Inputs/InputNative.cs @@ -4,7 +4,7 @@ namespace YDotNet.Native.Cells.Inputs; // The size has to be 24 here so that the whole data of the input cell is written/read correctly over the C FFI. [StructLayout(LayoutKind.Explicit, Size = 24)] -internal struct InputNative +public struct InputNative { [field: FieldOffset(offset: 0)] public sbyte Tag { get; } diff --git a/YDotNet/Native/ChannelSettings.cs b/YDotNet/Native/ChannelSettings.cs index 2dc2eebf..4162b324 100644 --- a/YDotNet/Native/ChannelSettings.cs +++ b/YDotNet/Native/ChannelSettings.cs @@ -2,9 +2,6 @@ namespace YDotNet.Native; internal static class ChannelSettings { -#if WINDOWS - public const string NativeLib = "yrs.dll"; -#else - public const string NativeLib = "libyrs.dylib"; -#endif + // https://learn.microsoft.com/en-us/dotnet/standard/native-interop/native-library-loading + public const string NativeLib = "yrs"; } diff --git a/YDotNet/Native/Document/Events/AfterTransactionEventNative.cs b/YDotNet/Native/Document/Events/AfterTransactionEventNative.cs index 1e07b7fc..4fb92627 100644 --- a/YDotNet/Native/Document/Events/AfterTransactionEventNative.cs +++ b/YDotNet/Native/Document/Events/AfterTransactionEventNative.cs @@ -4,13 +4,16 @@ namespace YDotNet.Native.Document.Events; -[StructLayout(LayoutKind.Sequential)] +[StructLayout(LayoutKind.Explicit)] internal struct AfterTransactionEventNative { + [field: FieldOffset(0)] public StateVectorNative BeforeState { get; } + [field: FieldOffset(24)] public StateVectorNative AfterState { get; } + [field: FieldOffset(48)] public DeleteSetNative DeleteSet { get; } public AfterTransactionEvent ToAfterTransactionEvent() diff --git a/YDotNet/Native/Document/State/StateVectorNative.cs b/YDotNet/Native/Document/State/StateVectorNative.cs index 5db8137e..b83dbd29 100644 --- a/YDotNet/Native/Document/State/StateVectorNative.cs +++ b/YDotNet/Native/Document/State/StateVectorNative.cs @@ -3,13 +3,16 @@ namespace YDotNet.Native.Document.State; -[StructLayout(LayoutKind.Sequential)] +[StructLayout(LayoutKind.Explicit, Size = 24)] internal struct StateVectorNative { + [field: FieldOffset(0)] public uint EntriesCount { get; } + [field: FieldOffset(8)] public nint ClientIds { get; } + [field: FieldOffset(16)] public nint Clocks { get; } public StateVector ToStateVector() diff --git a/YDotNet/Native/Types/Maps/MapChannel.cs b/YDotNet/Native/Types/Maps/MapChannel.cs index 8bbde4f9..d35390eb 100644 --- a/YDotNet/Native/Types/Maps/MapChannel.cs +++ b/YDotNet/Native/Types/Maps/MapChannel.cs @@ -8,7 +8,7 @@ internal static class MapChannel public delegate void ObserveCallback(nint state, nint eventHandle); [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ymap_insert")] - public static extern void Insert(nint map, nint transaction, nint key, InputNative inputNative); + public static extern void Insert(nint map, nint transaction, nint key, nint value); [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ymap_get")] public static extern nint Get(nint map, nint transaction, nint key); diff --git a/YDotNet/stylecop.json b/YDotNet/stylecop.json new file mode 100644 index 00000000..42fb1f8e --- /dev/null +++ b/YDotNet/stylecop.json @@ -0,0 +1,14 @@ +{ + // ACTION REQUIRED: This file was automatically added to your project, but it + // will not take effect until additional steps are taken to enable it. See the + // following page for additional information: + // + // https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/EnableConfiguration.md + + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "PlaceholderCompany" + } + } +} diff --git a/native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj b/native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj new file mode 100644 index 00000000..ddeadac2 --- /dev/null +++ b/native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj @@ -0,0 +1,18 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + diff --git a/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj b/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj new file mode 100644 index 00000000..f44615da --- /dev/null +++ b/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj @@ -0,0 +1,13 @@ + + + + net7.0 + enable + enable + + + + + + + diff --git a/native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj b/native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj new file mode 100644 index 00000000..31220e5e --- /dev/null +++ b/native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj @@ -0,0 +1,13 @@ + + + + net7.0 + enable + enable + + + + + + + diff --git a/native/YDotNet.Native/YDotNet.Native.csproj b/native/YDotNet.Native/YDotNet.Native.csproj new file mode 100644 index 00000000..a51cb7c8 --- /dev/null +++ b/native/YDotNet.Native/YDotNet.Native.csproj @@ -0,0 +1,15 @@ + + + + net7.0 + enable + enable + + + + + + + + + From 6034663e80c0395acf6e91c1c62fc33a38d4acb2 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 7 Oct 2023 16:04:41 +0200 Subject: [PATCH 061/186] Fix workflow. --- .github/workflows/build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 30233260..832c0e59 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,18 +41,18 @@ jobs: - name: Build Test run: | - cd T + cd Tests/YDotNet.Tests.Unit dotnet build - name: Copy to Test Folder run: | - cp output/${{matrix.build}}/*.* T/bin/Debug/net7.0 + cp output/${{matrix.build}}/*.* Tests/YDotNet.Tests.Unit/bin/Debug/net7.0 ls output/${{matrix.build}} - ls T/bin/Debug/net7.0 + ls Tests/YDotNet.Tests.Unit/bin/Debug/net7.0 - name: Test run: | - cd T && dotnet run + dotnet test env: RUST_BACKTRACE: 1 From 1ad9611e8dcc5da125fe42d050e48abf733b12cb Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 7 Oct 2023 16:13:23 +0200 Subject: [PATCH 062/186] Test. --- T/Program.cs | 43 +++++++++++-------- .../Document/State/StateVectorNative.cs | 2 + 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/T/Program.cs b/T/Program.cs index 65b88339..5b52b1be 100644 --- a/T/Program.cs +++ b/T/Program.cs @@ -1,5 +1,6 @@ using YDotNet.Document; using YDotNet.Document.Cells; +using YDotNet.Document.Events; namespace T { @@ -7,29 +8,37 @@ internal class Program { static void Main(string[] args) { - Console.WriteLine(sizeof(uint)); - Console.WriteLine(sizeof(ulong)); - // Arrange var doc = new Doc(); - var array1 = doc.Array("array-1"); - var transaction = doc.WriteTransaction(); - array1.InsertRange( - transaction, index: 0, new[] + AfterTransactionEvent? afterTransactionEvent = null; + var called = 0; + + var text = doc.Text("country"); + var subscription = doc.ObserveAfterTransaction( + e => { - Input.Long(value: 2469L), - Input.Null(), - Input.Boolean(value: false), - Input.Map(new Dictionary()) + called++; + afterTransactionEvent = e; }); - var map2 = array1.Get(transaction, index: 3).Map; - var i = Input.Array(Array.Empty()); - Console.WriteLine(i.InputNative.Tag); - Console.Out.Flush(); - Thread.Sleep(10000); - map2.Insert(transaction, "array-3", i); + // Act + var transaction = doc.WriteTransaction(); + text.Insert(transaction, index: 0, "Brazil"); + transaction.Commit(); + // Act + afterTransactionEvent = null; + transaction = doc.WriteTransaction(); + text.Insert(transaction, index: 0, "Great "); + transaction.Commit(); + + // Act + afterTransactionEvent = null; + doc.UnobserveAfterTransaction(subscription); + + transaction = doc.WriteTransaction(); + text.Insert(transaction, index: 0, "The "); + transaction.Commit(); } } } diff --git a/YDotNet/Native/Document/State/StateVectorNative.cs b/YDotNet/Native/Document/State/StateVectorNative.cs index b83dbd29..72e9ddb1 100644 --- a/YDotNet/Native/Document/State/StateVectorNative.cs +++ b/YDotNet/Native/Document/State/StateVectorNative.cs @@ -17,6 +17,8 @@ internal struct StateVectorNative public StateVector ToStateVector() { + Console.WriteLine("Entries {0}", EntriesCount); + var entries = new Dictionary(); for (var i = 0; i < EntriesCount; i++) From f4140801c8510d464bba93e47ae56db471a9a4a2 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 7 Oct 2023 16:18:49 +0200 Subject: [PATCH 063/186] Test struct. --- YDotNet/Document/Doc.cs | 2 +- YDotNet/Infrastructure/MemoryReader.cs | 6 ++++++ YDotNet/Native/Document/DocChannel.cs | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index 0fadc52a..d1770d31 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -391,7 +391,7 @@ public EventSubscription ObserveAfterTransaction(Action a var subscriptionId = DocChannel.ObserveAfterTransaction( Handle, nint.Zero, - (_, afterTransactionEvent) => action(afterTransactionEvent.ToAfterTransactionEvent())); + (_, afterTransactionEvent) => action(MemoryReader.ReadStruct(afterTransactionEvent).ToAfterTransactionEvent())); return new EventSubscription(subscriptionId); } diff --git a/YDotNet/Infrastructure/MemoryReader.cs b/YDotNet/Infrastructure/MemoryReader.cs index 104846d8..5820252a 100644 --- a/YDotNet/Infrastructure/MemoryReader.cs +++ b/YDotNet/Infrastructure/MemoryReader.cs @@ -22,6 +22,12 @@ internal static unsafe byte[] ReadBytes(nint handle, uint length) return data; } + internal static T ReadStruct(nint handle) + where T : struct + { + return Marshal.PtrToStructure(handle); + } + internal static byte[]? TryReadBytes(nint handle, uint length) { return handle == nint.Zero ? null : ReadBytes(handle, length); diff --git a/YDotNet/Native/Document/DocChannel.cs b/YDotNet/Native/Document/DocChannel.cs index a7e3060b..5d71b811 100644 --- a/YDotNet/Native/Document/DocChannel.cs +++ b/YDotNet/Native/Document/DocChannel.cs @@ -5,7 +5,7 @@ namespace YDotNet.Native.Document; internal static class DocChannel { - public delegate void ObserveAfterTransactionCallback(nint state, AfterTransactionEventNative afterTransactionEvent); + public delegate void ObserveAfterTransactionCallback(nint state, nint afterTransactionEvent); public delegate void ObserveClearCallback(nint state, nint doc); From c1fd782b441fa8c87633fdfb48ab4df43f66f5ed Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 7 Oct 2023 16:20:58 +0200 Subject: [PATCH 064/186] Read pointers manually. --- YDotNet/Document/Doc.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index d1770d31..ced26f6f 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -391,7 +391,7 @@ public EventSubscription ObserveAfterTransaction(Action a var subscriptionId = DocChannel.ObserveAfterTransaction( Handle, nint.Zero, - (_, afterTransactionEvent) => action(MemoryReader.ReadStruct(afterTransactionEvent).ToAfterTransactionEvent())); + (_, afterTransactionEvent) => action(MemoryReader.ReadStruct(afterTransactionEvent).ToAfterTransactionEvent())); return new EventSubscription(subscriptionId); } From d02a9fa4c9f7edcd5d5f7c270104c28a4d2439e6 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 7 Oct 2023 16:26:50 +0200 Subject: [PATCH 065/186] Test pointers. --- .github/workflows/build.yml | 8 ++++---- YDotNet/Document/Doc.cs | 4 ++-- YDotNet/Document/UndoManagers/UndoManager.cs | 5 +++-- YDotNet/Native/Document/DocChannel.cs | 4 ++-- .../Native/Document/Events/AfterTransactionEventNative.cs | 5 +---- YDotNet/Native/Document/State/StateVectorNative.cs | 4 ---- YDotNet/Native/UndoManager/UndoManagerChannel.cs | 5 ++--- 7 files changed, 14 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 30233260..678bf64d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,18 +41,18 @@ jobs: - name: Build Test run: | - cd T + cd Tests/YDotNet.Tests.Unit dotnet build - name: Copy to Test Folder run: | - cp output/${{matrix.build}}/*.* T/bin/Debug/net7.0 + cp output/${{matrix.build}}/*.* Tests/YDotNet.Tests.Unit/bin/Debug/net7.0 ls output/${{matrix.build}} - ls T/bin/Debug/net7.0 + ls Tests/YDotNet.Tests.Unit/bin/Debug/net7.0 - name: Test run: | - cd T && dotnet run + dotnet test -v n -m:1 env: RUST_BACKTRACE: 1 diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index ced26f6f..1562c305 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -391,7 +391,7 @@ public EventSubscription ObserveAfterTransaction(Action a var subscriptionId = DocChannel.ObserveAfterTransaction( Handle, nint.Zero, - (_, afterTransactionEvent) => action(MemoryReader.ReadStruct(afterTransactionEvent).ToAfterTransactionEvent())); + (_, eventHandle) => action(MemoryReader.ReadStruct(eventHandle).ToAfterTransactionEvent())); return new EventSubscription(subscriptionId); } @@ -416,7 +416,7 @@ public EventSubscription ObserveSubDocs(Action action) var subscriptionId = DocChannel.ObserveSubDocs( Handle, nint.Zero, - (_, subDocsEvent) => action(subDocsEvent.ToSubDocsEvent())); + (_, eventHandle) => action(MemoryReader.ReadStruct(eventHandle).ToSubDocsEvent())); return new EventSubscription(subscriptionId); } diff --git a/YDotNet/Document/UndoManagers/UndoManager.cs b/YDotNet/Document/UndoManagers/UndoManager.cs index 8c30bd99..14ad41a2 100644 --- a/YDotNet/Document/UndoManagers/UndoManager.cs +++ b/YDotNet/Document/UndoManagers/UndoManager.cs @@ -4,6 +4,7 @@ using YDotNet.Document.UndoManagers.Events; using YDotNet.Infrastructure; using YDotNet.Native.UndoManager; +using YDotNet.Native.UndoManager.Events; namespace YDotNet.Document.UndoManagers; @@ -50,7 +51,7 @@ public EventSubscription ObserveAdded(Action action) var subscriptionId = UndoManagerChannel.ObserveAdded( Handle, nint.Zero, - (_, undoEvent) => action(undoEvent.ToUndoEvent())); + (_, eventHandle) => action(MemoryReader.ReadStruct(eventHandle).ToUndoEvent())); return new EventSubscription(subscriptionId); } @@ -76,7 +77,7 @@ public EventSubscription ObservePopped(Action action) var subscriptionId = UndoManagerChannel.ObservePopped( Handle, nint.Zero, - (_, undoEvent) => action(undoEvent.ToUndoEvent())); + (_, eventHandle) => action(MemoryReader.ReadStruct(eventHandle).ToUndoEvent())); return new EventSubscription(subscriptionId); } diff --git a/YDotNet/Native/Document/DocChannel.cs b/YDotNet/Native/Document/DocChannel.cs index 5d71b811..e8947eb5 100644 --- a/YDotNet/Native/Document/DocChannel.cs +++ b/YDotNet/Native/Document/DocChannel.cs @@ -5,11 +5,11 @@ namespace YDotNet.Native.Document; internal static class DocChannel { - public delegate void ObserveAfterTransactionCallback(nint state, nint afterTransactionEvent); + public delegate void ObserveAfterTransactionCallback(nint state, nint eventHandle); public delegate void ObserveClearCallback(nint state, nint doc); - public delegate void ObserveSubdocsCallback(nint state, SubDocsEventNative subDocsEvent); + public delegate void ObserveSubdocsCallback(nint state, int eventHandle); public delegate void ObserveUpdatesCallback(nint state, uint length, nint data); diff --git a/YDotNet/Native/Document/Events/AfterTransactionEventNative.cs b/YDotNet/Native/Document/Events/AfterTransactionEventNative.cs index 4fb92627..1e07b7fc 100644 --- a/YDotNet/Native/Document/Events/AfterTransactionEventNative.cs +++ b/YDotNet/Native/Document/Events/AfterTransactionEventNative.cs @@ -4,16 +4,13 @@ namespace YDotNet.Native.Document.Events; -[StructLayout(LayoutKind.Explicit)] +[StructLayout(LayoutKind.Sequential)] internal struct AfterTransactionEventNative { - [field: FieldOffset(0)] public StateVectorNative BeforeState { get; } - [field: FieldOffset(24)] public StateVectorNative AfterState { get; } - [field: FieldOffset(48)] public DeleteSetNative DeleteSet { get; } public AfterTransactionEvent ToAfterTransactionEvent() diff --git a/YDotNet/Native/Document/State/StateVectorNative.cs b/YDotNet/Native/Document/State/StateVectorNative.cs index 72e9ddb1..ea34cd06 100644 --- a/YDotNet/Native/Document/State/StateVectorNative.cs +++ b/YDotNet/Native/Document/State/StateVectorNative.cs @@ -3,16 +3,12 @@ namespace YDotNet.Native.Document.State; -[StructLayout(LayoutKind.Explicit, Size = 24)] internal struct StateVectorNative { - [field: FieldOffset(0)] public uint EntriesCount { get; } - [field: FieldOffset(8)] public nint ClientIds { get; } - [field: FieldOffset(16)] public nint Clocks { get; } public StateVector ToStateVector() diff --git a/YDotNet/Native/UndoManager/UndoManagerChannel.cs b/YDotNet/Native/UndoManager/UndoManagerChannel.cs index 4018a158..3dac9b24 100644 --- a/YDotNet/Native/UndoManager/UndoManagerChannel.cs +++ b/YDotNet/Native/UndoManager/UndoManagerChannel.cs @@ -1,13 +1,12 @@ using System.Runtime.InteropServices; -using YDotNet.Native.UndoManager.Events; namespace YDotNet.Native.UndoManager; internal static class UndoManagerChannel { - public delegate void ObserveAddedCallback(nint state, UndoEventNative undoEvent); + public delegate void ObserveAddedCallback(nint state, int eventHandle); - public delegate void ObservePoppedCallback(nint state, UndoEventNative undoEvent); + public delegate void ObservePoppedCallback(nint state, int eventHandle); [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yundo_manager")] public static extern nint NewWithOptions(nint doc, nint branch, nint options); From af57e792f1ed0e8768bb1d4521b1a096eecbad62 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 7 Oct 2023 16:40:54 +0200 Subject: [PATCH 066/186] Fix --- T/T.csproj | 4 ++++ YDotNet/Document/Doc.cs | 4 ++-- YDotNet/Document/UndoManagers/UndoManager.cs | 5 ++--- YDotNet/Native/Document/DocChannel.cs | 2 +- YDotNet/Native/UndoManager/UndoManagerChannel.cs | 5 +++-- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/T/T.csproj b/T/T.csproj index 3782422a..3f34271d 100644 --- a/T/T.csproj +++ b/T/T.csproj @@ -7,6 +7,10 @@ enable + + + + diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index 1562c305..caa03aed 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -391,7 +391,7 @@ public EventSubscription ObserveAfterTransaction(Action a var subscriptionId = DocChannel.ObserveAfterTransaction( Handle, nint.Zero, - (_, eventHandle) => action(MemoryReader.ReadStruct(eventHandle).ToAfterTransactionEvent())); + (_, eventHandler) => action(MemoryReader.ReadStruct(eventHandler).ToAfterTransactionEvent())); return new EventSubscription(subscriptionId); } @@ -416,7 +416,7 @@ public EventSubscription ObserveSubDocs(Action action) var subscriptionId = DocChannel.ObserveSubDocs( Handle, nint.Zero, - (_, eventHandle) => action(MemoryReader.ReadStruct(eventHandle).ToSubDocsEvent())); + (_, subDocsEvent) => action(subDocsEvent.ToSubDocsEvent())); return new EventSubscription(subscriptionId); } diff --git a/YDotNet/Document/UndoManagers/UndoManager.cs b/YDotNet/Document/UndoManagers/UndoManager.cs index 14ad41a2..8c30bd99 100644 --- a/YDotNet/Document/UndoManagers/UndoManager.cs +++ b/YDotNet/Document/UndoManagers/UndoManager.cs @@ -4,7 +4,6 @@ using YDotNet.Document.UndoManagers.Events; using YDotNet.Infrastructure; using YDotNet.Native.UndoManager; -using YDotNet.Native.UndoManager.Events; namespace YDotNet.Document.UndoManagers; @@ -51,7 +50,7 @@ public EventSubscription ObserveAdded(Action action) var subscriptionId = UndoManagerChannel.ObserveAdded( Handle, nint.Zero, - (_, eventHandle) => action(MemoryReader.ReadStruct(eventHandle).ToUndoEvent())); + (_, undoEvent) => action(undoEvent.ToUndoEvent())); return new EventSubscription(subscriptionId); } @@ -77,7 +76,7 @@ public EventSubscription ObservePopped(Action action) var subscriptionId = UndoManagerChannel.ObservePopped( Handle, nint.Zero, - (_, eventHandle) => action(MemoryReader.ReadStruct(eventHandle).ToUndoEvent())); + (_, undoEvent) => action(undoEvent.ToUndoEvent())); return new EventSubscription(subscriptionId); } diff --git a/YDotNet/Native/Document/DocChannel.cs b/YDotNet/Native/Document/DocChannel.cs index e8947eb5..fb926ba0 100644 --- a/YDotNet/Native/Document/DocChannel.cs +++ b/YDotNet/Native/Document/DocChannel.cs @@ -9,7 +9,7 @@ internal static class DocChannel public delegate void ObserveClearCallback(nint state, nint doc); - public delegate void ObserveSubdocsCallback(nint state, int eventHandle); + public delegate void ObserveSubdocsCallback(nint state, SubDocsEventNative subDocsEvent); public delegate void ObserveUpdatesCallback(nint state, uint length, nint data); diff --git a/YDotNet/Native/UndoManager/UndoManagerChannel.cs b/YDotNet/Native/UndoManager/UndoManagerChannel.cs index 3dac9b24..4018a158 100644 --- a/YDotNet/Native/UndoManager/UndoManagerChannel.cs +++ b/YDotNet/Native/UndoManager/UndoManagerChannel.cs @@ -1,12 +1,13 @@ using System.Runtime.InteropServices; +using YDotNet.Native.UndoManager.Events; namespace YDotNet.Native.UndoManager; internal static class UndoManagerChannel { - public delegate void ObserveAddedCallback(nint state, int eventHandle); + public delegate void ObserveAddedCallback(nint state, UndoEventNative undoEvent); - public delegate void ObservePoppedCallback(nint state, int eventHandle); + public delegate void ObservePoppedCallback(nint state, UndoEventNative undoEvent); [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yundo_manager")] public static extern nint NewWithOptions(nint doc, nint branch, nint options); From f2f0eda16d9d083386be23b28ddeb0edfffa16cc Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 7 Oct 2023 16:49:44 +0200 Subject: [PATCH 067/186] Use pointer again. --- YDotNet/Document/Doc.cs | 2 +- YDotNet/Native/Document/DocChannel.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index caa03aed..5d356a40 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -416,7 +416,7 @@ public EventSubscription ObserveSubDocs(Action action) var subscriptionId = DocChannel.ObserveSubDocs( Handle, nint.Zero, - (_, subDocsEvent) => action(subDocsEvent.ToSubDocsEvent())); + (_, eventHandle) => action(MemoryReader.ReadStruct(eventHandle).ToSubDocsEvent())); return new EventSubscription(subscriptionId); } diff --git a/YDotNet/Native/Document/DocChannel.cs b/YDotNet/Native/Document/DocChannel.cs index fb926ba0..2b5e4abd 100644 --- a/YDotNet/Native/Document/DocChannel.cs +++ b/YDotNet/Native/Document/DocChannel.cs @@ -9,7 +9,7 @@ internal static class DocChannel public delegate void ObserveClearCallback(nint state, nint doc); - public delegate void ObserveSubdocsCallback(nint state, SubDocsEventNative subDocsEvent); + public delegate void ObserveSubdocsCallback(nint state, nint eventHandle); public delegate void ObserveUpdatesCallback(nint state, uint length, nint data); From e8f36a350770bd418a8100a8f23783f19b932dbf Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 7 Oct 2023 15:41:50 +0200 Subject: [PATCH 068/186] Test field offset. From df4eb1d37bf83cab4b5f5f2ef7c5c70a105962ef Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 7 Oct 2023 16:13:23 +0200 Subject: [PATCH 069/186] Test. # Conflicts: # T/Program.cs --- YDotNet/Native/Document/State/StateVectorNative.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/YDotNet/Native/Document/State/StateVectorNative.cs b/YDotNet/Native/Document/State/StateVectorNative.cs index b83dbd29..72e9ddb1 100644 --- a/YDotNet/Native/Document/State/StateVectorNative.cs +++ b/YDotNet/Native/Document/State/StateVectorNative.cs @@ -17,6 +17,8 @@ internal struct StateVectorNative public StateVector ToStateVector() { + Console.WriteLine("Entries {0}", EntriesCount); + var entries = new Dictionary(); for (var i = 0; i < EntriesCount; i++) From 20661f5b108a93c63168d7d246346faf17ae2f8d Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 7 Oct 2023 16:18:49 +0200 Subject: [PATCH 070/186] Test struct. --- YDotNet/Document/Doc.cs | 2 +- YDotNet/Infrastructure/MemoryReader.cs | 6 ++++++ YDotNet/Native/Document/DocChannel.cs | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index 0fadc52a..d1770d31 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -391,7 +391,7 @@ public EventSubscription ObserveAfterTransaction(Action a var subscriptionId = DocChannel.ObserveAfterTransaction( Handle, nint.Zero, - (_, afterTransactionEvent) => action(afterTransactionEvent.ToAfterTransactionEvent())); + (_, afterTransactionEvent) => action(MemoryReader.ReadStruct(afterTransactionEvent).ToAfterTransactionEvent())); return new EventSubscription(subscriptionId); } diff --git a/YDotNet/Infrastructure/MemoryReader.cs b/YDotNet/Infrastructure/MemoryReader.cs index 104846d8..5820252a 100644 --- a/YDotNet/Infrastructure/MemoryReader.cs +++ b/YDotNet/Infrastructure/MemoryReader.cs @@ -22,6 +22,12 @@ internal static unsafe byte[] ReadBytes(nint handle, uint length) return data; } + internal static T ReadStruct(nint handle) + where T : struct + { + return Marshal.PtrToStructure(handle); + } + internal static byte[]? TryReadBytes(nint handle, uint length) { return handle == nint.Zero ? null : ReadBytes(handle, length); diff --git a/YDotNet/Native/Document/DocChannel.cs b/YDotNet/Native/Document/DocChannel.cs index a7e3060b..5d71b811 100644 --- a/YDotNet/Native/Document/DocChannel.cs +++ b/YDotNet/Native/Document/DocChannel.cs @@ -5,7 +5,7 @@ namespace YDotNet.Native.Document; internal static class DocChannel { - public delegate void ObserveAfterTransactionCallback(nint state, AfterTransactionEventNative afterTransactionEvent); + public delegate void ObserveAfterTransactionCallback(nint state, nint afterTransactionEvent); public delegate void ObserveClearCallback(nint state, nint doc); From b0e9b4cc0e26fd0596c8419bce3c0c6e16837f63 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 7 Oct 2023 16:20:58 +0200 Subject: [PATCH 071/186] Read pointers manually. --- YDotNet/Document/Doc.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index d1770d31..ced26f6f 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -391,7 +391,7 @@ public EventSubscription ObserveAfterTransaction(Action a var subscriptionId = DocChannel.ObserveAfterTransaction( Handle, nint.Zero, - (_, afterTransactionEvent) => action(MemoryReader.ReadStruct(afterTransactionEvent).ToAfterTransactionEvent())); + (_, afterTransactionEvent) => action(MemoryReader.ReadStruct(afterTransactionEvent).ToAfterTransactionEvent())); return new EventSubscription(subscriptionId); } From 4c602060c21af00380d6c16f60b612305d61ebd4 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 7 Oct 2023 16:26:50 +0200 Subject: [PATCH 072/186] Test pointers. # Conflicts: # .github/workflows/build.yml --- .github/workflows/build.yml | 2 +- YDotNet/Document/Doc.cs | 4 ++-- YDotNet/Document/UndoManagers/UndoManager.cs | 5 +++-- YDotNet/Native/Document/DocChannel.cs | 4 ++-- .../Native/Document/Events/AfterTransactionEventNative.cs | 5 +---- YDotNet/Native/Document/State/StateVectorNative.cs | 4 ---- YDotNet/Native/UndoManager/UndoManagerChannel.cs | 5 ++--- 7 files changed, 11 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 832c0e59..678bf64d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,7 +52,7 @@ jobs: - name: Test run: | - dotnet test + dotnet test -v n -m:1 env: RUST_BACKTRACE: 1 diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index ced26f6f..1562c305 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -391,7 +391,7 @@ public EventSubscription ObserveAfterTransaction(Action a var subscriptionId = DocChannel.ObserveAfterTransaction( Handle, nint.Zero, - (_, afterTransactionEvent) => action(MemoryReader.ReadStruct(afterTransactionEvent).ToAfterTransactionEvent())); + (_, eventHandle) => action(MemoryReader.ReadStruct(eventHandle).ToAfterTransactionEvent())); return new EventSubscription(subscriptionId); } @@ -416,7 +416,7 @@ public EventSubscription ObserveSubDocs(Action action) var subscriptionId = DocChannel.ObserveSubDocs( Handle, nint.Zero, - (_, subDocsEvent) => action(subDocsEvent.ToSubDocsEvent())); + (_, eventHandle) => action(MemoryReader.ReadStruct(eventHandle).ToSubDocsEvent())); return new EventSubscription(subscriptionId); } diff --git a/YDotNet/Document/UndoManagers/UndoManager.cs b/YDotNet/Document/UndoManagers/UndoManager.cs index 8c30bd99..14ad41a2 100644 --- a/YDotNet/Document/UndoManagers/UndoManager.cs +++ b/YDotNet/Document/UndoManagers/UndoManager.cs @@ -4,6 +4,7 @@ using YDotNet.Document.UndoManagers.Events; using YDotNet.Infrastructure; using YDotNet.Native.UndoManager; +using YDotNet.Native.UndoManager.Events; namespace YDotNet.Document.UndoManagers; @@ -50,7 +51,7 @@ public EventSubscription ObserveAdded(Action action) var subscriptionId = UndoManagerChannel.ObserveAdded( Handle, nint.Zero, - (_, undoEvent) => action(undoEvent.ToUndoEvent())); + (_, eventHandle) => action(MemoryReader.ReadStruct(eventHandle).ToUndoEvent())); return new EventSubscription(subscriptionId); } @@ -76,7 +77,7 @@ public EventSubscription ObservePopped(Action action) var subscriptionId = UndoManagerChannel.ObservePopped( Handle, nint.Zero, - (_, undoEvent) => action(undoEvent.ToUndoEvent())); + (_, eventHandle) => action(MemoryReader.ReadStruct(eventHandle).ToUndoEvent())); return new EventSubscription(subscriptionId); } diff --git a/YDotNet/Native/Document/DocChannel.cs b/YDotNet/Native/Document/DocChannel.cs index 5d71b811..e8947eb5 100644 --- a/YDotNet/Native/Document/DocChannel.cs +++ b/YDotNet/Native/Document/DocChannel.cs @@ -5,11 +5,11 @@ namespace YDotNet.Native.Document; internal static class DocChannel { - public delegate void ObserveAfterTransactionCallback(nint state, nint afterTransactionEvent); + public delegate void ObserveAfterTransactionCallback(nint state, nint eventHandle); public delegate void ObserveClearCallback(nint state, nint doc); - public delegate void ObserveSubdocsCallback(nint state, SubDocsEventNative subDocsEvent); + public delegate void ObserveSubdocsCallback(nint state, int eventHandle); public delegate void ObserveUpdatesCallback(nint state, uint length, nint data); diff --git a/YDotNet/Native/Document/Events/AfterTransactionEventNative.cs b/YDotNet/Native/Document/Events/AfterTransactionEventNative.cs index 4fb92627..1e07b7fc 100644 --- a/YDotNet/Native/Document/Events/AfterTransactionEventNative.cs +++ b/YDotNet/Native/Document/Events/AfterTransactionEventNative.cs @@ -4,16 +4,13 @@ namespace YDotNet.Native.Document.Events; -[StructLayout(LayoutKind.Explicit)] +[StructLayout(LayoutKind.Sequential)] internal struct AfterTransactionEventNative { - [field: FieldOffset(0)] public StateVectorNative BeforeState { get; } - [field: FieldOffset(24)] public StateVectorNative AfterState { get; } - [field: FieldOffset(48)] public DeleteSetNative DeleteSet { get; } public AfterTransactionEvent ToAfterTransactionEvent() diff --git a/YDotNet/Native/Document/State/StateVectorNative.cs b/YDotNet/Native/Document/State/StateVectorNative.cs index 72e9ddb1..ea34cd06 100644 --- a/YDotNet/Native/Document/State/StateVectorNative.cs +++ b/YDotNet/Native/Document/State/StateVectorNative.cs @@ -3,16 +3,12 @@ namespace YDotNet.Native.Document.State; -[StructLayout(LayoutKind.Explicit, Size = 24)] internal struct StateVectorNative { - [field: FieldOffset(0)] public uint EntriesCount { get; } - [field: FieldOffset(8)] public nint ClientIds { get; } - [field: FieldOffset(16)] public nint Clocks { get; } public StateVector ToStateVector() diff --git a/YDotNet/Native/UndoManager/UndoManagerChannel.cs b/YDotNet/Native/UndoManager/UndoManagerChannel.cs index 4018a158..3dac9b24 100644 --- a/YDotNet/Native/UndoManager/UndoManagerChannel.cs +++ b/YDotNet/Native/UndoManager/UndoManagerChannel.cs @@ -1,13 +1,12 @@ using System.Runtime.InteropServices; -using YDotNet.Native.UndoManager.Events; namespace YDotNet.Native.UndoManager; internal static class UndoManagerChannel { - public delegate void ObserveAddedCallback(nint state, UndoEventNative undoEvent); + public delegate void ObserveAddedCallback(nint state, int eventHandle); - public delegate void ObservePoppedCallback(nint state, UndoEventNative undoEvent); + public delegate void ObservePoppedCallback(nint state, int eventHandle); [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yundo_manager")] public static extern nint NewWithOptions(nint doc, nint branch, nint options); From 1eb16c58e175a4b923743f0f3ad06524c322c803 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 7 Oct 2023 16:40:54 +0200 Subject: [PATCH 073/186] Fix # Conflicts: # T/T.csproj --- YDotNet/Document/Doc.cs | 4 ++-- YDotNet/Document/UndoManagers/UndoManager.cs | 5 ++--- YDotNet/Native/Document/DocChannel.cs | 2 +- YDotNet/Native/UndoManager/UndoManagerChannel.cs | 5 +++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index 1562c305..caa03aed 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -391,7 +391,7 @@ public EventSubscription ObserveAfterTransaction(Action a var subscriptionId = DocChannel.ObserveAfterTransaction( Handle, nint.Zero, - (_, eventHandle) => action(MemoryReader.ReadStruct(eventHandle).ToAfterTransactionEvent())); + (_, eventHandler) => action(MemoryReader.ReadStruct(eventHandler).ToAfterTransactionEvent())); return new EventSubscription(subscriptionId); } @@ -416,7 +416,7 @@ public EventSubscription ObserveSubDocs(Action action) var subscriptionId = DocChannel.ObserveSubDocs( Handle, nint.Zero, - (_, eventHandle) => action(MemoryReader.ReadStruct(eventHandle).ToSubDocsEvent())); + (_, subDocsEvent) => action(subDocsEvent.ToSubDocsEvent())); return new EventSubscription(subscriptionId); } diff --git a/YDotNet/Document/UndoManagers/UndoManager.cs b/YDotNet/Document/UndoManagers/UndoManager.cs index 14ad41a2..8c30bd99 100644 --- a/YDotNet/Document/UndoManagers/UndoManager.cs +++ b/YDotNet/Document/UndoManagers/UndoManager.cs @@ -4,7 +4,6 @@ using YDotNet.Document.UndoManagers.Events; using YDotNet.Infrastructure; using YDotNet.Native.UndoManager; -using YDotNet.Native.UndoManager.Events; namespace YDotNet.Document.UndoManagers; @@ -51,7 +50,7 @@ public EventSubscription ObserveAdded(Action action) var subscriptionId = UndoManagerChannel.ObserveAdded( Handle, nint.Zero, - (_, eventHandle) => action(MemoryReader.ReadStruct(eventHandle).ToUndoEvent())); + (_, undoEvent) => action(undoEvent.ToUndoEvent())); return new EventSubscription(subscriptionId); } @@ -77,7 +76,7 @@ public EventSubscription ObservePopped(Action action) var subscriptionId = UndoManagerChannel.ObservePopped( Handle, nint.Zero, - (_, eventHandle) => action(MemoryReader.ReadStruct(eventHandle).ToUndoEvent())); + (_, undoEvent) => action(undoEvent.ToUndoEvent())); return new EventSubscription(subscriptionId); } diff --git a/YDotNet/Native/Document/DocChannel.cs b/YDotNet/Native/Document/DocChannel.cs index e8947eb5..fb926ba0 100644 --- a/YDotNet/Native/Document/DocChannel.cs +++ b/YDotNet/Native/Document/DocChannel.cs @@ -9,7 +9,7 @@ internal static class DocChannel public delegate void ObserveClearCallback(nint state, nint doc); - public delegate void ObserveSubdocsCallback(nint state, int eventHandle); + public delegate void ObserveSubdocsCallback(nint state, SubDocsEventNative subDocsEvent); public delegate void ObserveUpdatesCallback(nint state, uint length, nint data); diff --git a/YDotNet/Native/UndoManager/UndoManagerChannel.cs b/YDotNet/Native/UndoManager/UndoManagerChannel.cs index 3dac9b24..4018a158 100644 --- a/YDotNet/Native/UndoManager/UndoManagerChannel.cs +++ b/YDotNet/Native/UndoManager/UndoManagerChannel.cs @@ -1,12 +1,13 @@ using System.Runtime.InteropServices; +using YDotNet.Native.UndoManager.Events; namespace YDotNet.Native.UndoManager; internal static class UndoManagerChannel { - public delegate void ObserveAddedCallback(nint state, int eventHandle); + public delegate void ObserveAddedCallback(nint state, UndoEventNative undoEvent); - public delegate void ObservePoppedCallback(nint state, int eventHandle); + public delegate void ObservePoppedCallback(nint state, UndoEventNative undoEvent); [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yundo_manager")] public static extern nint NewWithOptions(nint doc, nint branch, nint options); From 45e373acd595d6e11e9037bb06dece1392f7f12a Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 7 Oct 2023 16:49:44 +0200 Subject: [PATCH 074/186] Use pointer again. --- YDotNet/Document/Doc.cs | 2 +- YDotNet/Native/Document/DocChannel.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index caa03aed..5d356a40 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -416,7 +416,7 @@ public EventSubscription ObserveSubDocs(Action action) var subscriptionId = DocChannel.ObserveSubDocs( Handle, nint.Zero, - (_, subDocsEvent) => action(subDocsEvent.ToSubDocsEvent())); + (_, eventHandle) => action(MemoryReader.ReadStruct(eventHandle).ToSubDocsEvent())); return new EventSubscription(subscriptionId); } diff --git a/YDotNet/Native/Document/DocChannel.cs b/YDotNet/Native/Document/DocChannel.cs index fb926ba0..2b5e4abd 100644 --- a/YDotNet/Native/Document/DocChannel.cs +++ b/YDotNet/Native/Document/DocChannel.cs @@ -9,7 +9,7 @@ internal static class DocChannel public delegate void ObserveClearCallback(nint state, nint doc); - public delegate void ObserveSubdocsCallback(nint state, SubDocsEventNative subDocsEvent); + public delegate void ObserveSubdocsCallback(nint state, nint eventHandle); public delegate void ObserveUpdatesCallback(nint state, uint length, nint data); From bd8f98b300077f192f3bce74e1fcffd10cf97d63 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 7 Oct 2023 16:55:14 +0200 Subject: [PATCH 075/186] Remove console out --- YDotNet/Native/Document/State/StateVectorNative.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/YDotNet/Native/Document/State/StateVectorNative.cs b/YDotNet/Native/Document/State/StateVectorNative.cs index ea34cd06..d9fbe6c3 100644 --- a/YDotNet/Native/Document/State/StateVectorNative.cs +++ b/YDotNet/Native/Document/State/StateVectorNative.cs @@ -13,8 +13,6 @@ internal struct StateVectorNative public StateVector ToStateVector() { - Console.WriteLine("Entries {0}", EntriesCount); - var entries = new Dictionary(); for (var i = 0; i < EntriesCount; i++) From 31ebcb94e9c7643849ed11c67d1d5279067864be Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 7 Oct 2023 16:58:14 +0200 Subject: [PATCH 076/186] Make struct internal. --- YDotNet/Document/Cells/Input.cs | 2 +- YDotNet/Native/Cells/Inputs/InputNative.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/YDotNet/Document/Cells/Input.cs b/YDotNet/Document/Cells/Input.cs index 9a89a5d0..d64cc30a 100644 --- a/YDotNet/Document/Cells/Input.cs +++ b/YDotNet/Document/Cells/Input.cs @@ -11,7 +11,7 @@ public abstract class Input : IDisposable /// /// Gets or sets the native input cell represented by this cell. /// - public InputNative InputNative { get; set; } + internal InputNative InputNative { get; set; } /// public abstract void Dispose(); diff --git a/YDotNet/Native/Cells/Inputs/InputNative.cs b/YDotNet/Native/Cells/Inputs/InputNative.cs index 7ddf4b71..bfafe83e 100644 --- a/YDotNet/Native/Cells/Inputs/InputNative.cs +++ b/YDotNet/Native/Cells/Inputs/InputNative.cs @@ -4,7 +4,7 @@ namespace YDotNet.Native.Cells.Inputs; // The size has to be 24 here so that the whole data of the input cell is written/read correctly over the C FFI. [StructLayout(LayoutKind.Explicit, Size = 24)] -public struct InputNative +internal struct InputNative { [field: FieldOffset(offset: 0)] public sbyte Tag { get; } From b8284f2bf31a711684391e4ca3b61de9807be99c Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 8 Oct 2023 10:54:55 +0200 Subject: [PATCH 077/186] Improve output. --- .editorconfig | 6 +- YDotNet/Document/Cells/Output.cs | 357 ++++++++---------- YDotNet/Document/Types/Arrays/Array.cs | 2 +- .../Document/Types/Arrays/ArrayEnumerator.cs | 2 +- YDotNet/Document/Types/Events/EventChange.cs | 8 +- .../Types/Events/EventDeltaAttribute.cs | 10 +- YDotNet/Document/Types/Maps/Map.cs | 2 +- YDotNet/Document/Types/Maps/MapEntry.cs | 2 +- YDotNet/Document/Types/Texts/TextChunk.cs | 2 +- .../Trees/XmlTreeWalkerEnumerator.cs | 2 +- .../Document/Types/XmlElements/XmlElement.cs | 8 +- YDotNet/Document/Types/XmlTexts/XmlText.cs | 4 +- YDotNet/Infrastructure/ReferenceAccessor.cs | 4 +- .../Native/Types/Events/EventChangeNative.cs | 12 +- .../Native/Types/Events/EventDeltaNative.cs | 2 +- .../Types/Events/EventKeyChangeNative.cs | 4 +- 16 files changed, 195 insertions(+), 232 deletions(-) diff --git a/.editorconfig b/.editorconfig index fc66ff7a..f2f28f30 100644 --- a/.editorconfig +++ b/.editorconfig @@ -23,7 +23,7 @@ dotnet_diagnostic.SA1008.severity = suggestion # Opening parenthesis should be s dotnet_diagnostic.SA1009.severity = suggestion # Closing parenthesis should be spaced correctly # Readability rules -dotnet_diagnostic.SA1101.severity = suggestion # Prefix local calls with this +dotnet_diagnostic.SA1101.severity = none # Prefix local calls with this # Ordering rules dotnet_diagnostic.SA1200.severity = suggestion # Using directives should be placed correctly @@ -37,4 +37,6 @@ dotnet_diagnostic.SA1413.severity = suggestion # Use trailing comma in multi-lin dotnet_diagnostic.SA1500.severity = suggestion # Braces for multi-line statements should not share line # Documentation rules -dotnet_diagnostic.SA1633.severity = suggestion # File should have header +dotnet_diagnostic.SA1600.severity = none # SA1600: Elements should be documented +dotnet_diagnostic.SA1633.severity = none # File should have header + diff --git a/YDotNet/Document/Cells/Output.cs b/YDotNet/Document/Cells/Output.cs index d64eb04b..532aa773 100644 --- a/YDotNet/Document/Cells/Output.cs +++ b/YDotNet/Document/Cells/Output.cs @@ -1,4 +1,3 @@ -using System.Formats.Asn1; using System.Reflection.Metadata; using System.Runtime.InteropServices; using YDotNet.Document.Types.Maps; @@ -10,183 +9,80 @@ using YDotNet.Native.Types.Maps; using Array = YDotNet.Document.Types.Arrays.Array; +#pragma warning disable SA1623 // Property summary documentation should match accessors + namespace YDotNet.Document.Cells; /// /// Represents a cell used to read information from the storage. /// -public class Output : IDisposable +public class Output { - private readonly bool disposable; + private readonly Lazy value; /// /// Initializes a new instance of the class. /// /// The pointer to the native resource that represents the storage. - /// - /// The flag determines if the resource associated with should be disposed - /// by this instance. - /// - internal Output(nint handle, bool disposable = false) + /// Indicates if the memory has been allocated and needs to be disposed. + internal Output(nint handle, bool shouldDispose) { - this.disposable = disposable; + if (handle == nint.Zero) + { + return; + } + + var native = Marshal.PtrToStructure(handle); - Handle = handle; - OutputNative = handle == nint.Zero ? null : Marshal.PtrToStructure(handle); + Type = (OutputInputType)native.Tag; + + value = BuildValue(handle, native.Length, Type); + + if (shouldDispose) + { + OutputChannel.Destroy(handle); + } } /// /// Gets the or null if this output cell contains a different type stored. /// - public Doc? Doc => ReferenceAccessor.Access(new Doc(OutputChannel.Doc(Handle))); + public Doc Doc => GetValue(OutputInputType.Doc); /// /// Gets the or null if this output cell contains a different type stored. /// - public string? String - { - get - { - EnsureType(OutputInputType.String); - - MemoryReader.TryReadUtf8String(OutputChannel.String(Handle), out var result); - - return result; - } - } + public string String => GetValue(OutputInputType.String); /// /// Gets the or null if this output cell contains a different type stored. /// - public bool? Boolean - { - get - { - EnsureType(OutputInputType.Bool); - - var value = OutputChannel.Boolean(Handle); - - if (value == nint.Zero) - { - throw new InvalidOperationException("Internal type mismatch, native library returns null."); - } - - return Marshal.PtrToStructure(value) == 1; - } - } + public bool Boolean => GetValue(OutputInputType.Bool); /// /// Gets the or null if this output cell contains a different type stored. /// - public double? Double - { - get - { - EnsureType(OutputInputType.Double); - - var value = OutputChannel.Double(Handle); - - if (value == nint.Zero) - { - throw new InvalidOperationException("Internal type mismatch, native library returns null."); - } - - return Marshal.PtrToStructure(value); - } - } + public double Double => GetValue(OutputInputType.Double); /// /// Gets the or null if this output cell contains a different type stored. /// - public long? Long - { - get - { - EnsureType(OutputInputType.Long); - - var value = OutputChannel.Long(Handle); - - if (value == nint.Zero) - { - throw new InvalidOperationException("Internal type mismatch, native library returns null."); - } - - return Marshal.PtrToStructure(value); - } - } + public long Long => GetValue(OutputInputType.Long); /// /// Gets the array or null if this output cells contains a different type stored. /// - public byte[] Bytes - { - get - { - EnsureType(OutputInputType.Bytes); - - var result = MemoryReader.TryReadBytes(OutputChannel.Bytes(Handle), OutputNative.Value.Length) ?? - throw new InvalidOperationException("Internal type mismatch, native library returns null."); - - if (result == null) - { - throw new InvalidOperationException("Internal type mismatch, native library returns null."); - } - - return result; - } - } + public byte[] Bytes => GetValue(OutputInputType.Bytes); /// /// Gets the collection or null if this output cells contains a different type stored. /// - public Output[] Collection - { - get - { - EnsureType(OutputInputType.Collection); - - var handles = MemoryReader.TryReadIntPtrArray( - OutputChannel.Collection(Handle), OutputNative!.Value.Length, Marshal.SizeOf()); - - if (handles == null) - { - throw new InvalidOperationException("Internal type mismatch, native library returns null."); - } - - return handles.Select(x => new Output(x)).ToArray(); - } - } + public Output[] Collection => GetValue(OutputInputType.Collection); /// /// Gets the dictionary or null if this output cells contains a different type stored. /// - public IDictionary? Object - { - get - { - EnsureType(OutputInputType.Object); - - var handles = MemoryReader.TryReadIntPtrArray( - OutputChannel.Object(Handle), OutputNative!.Value.Length, Marshal.SizeOf()); - - if (handles == null) - { - throw new InvalidOperationException("Internal type mismatch, native library returns null."); - } - - var result = new Dictionary(); - - foreach (var handle in handles) - { - var (mapEntry, outputHandle) = MemoryReader.ReadMapEntryAndOutputHandle(handle); - var mapEntryKey = MemoryReader.ReadUtf8String(mapEntry.Field); - - result[mapEntryKey] = new Output(outputHandle); - } - - return result; - } - } + public IDictionary? Object => GetValue>(OutputInputType.Object); /// /// Gets a value indicating whether this output cell contains a null value. @@ -201,109 +97,162 @@ public IDictionary? Object /// /// Gets the or null if this output cells contains a different type stored. /// - public Array Array - { - get - { - EnsureType(OutputInputType.Array); - - return ReferenceAccessor.Access(new Array(OutputChannel.Array(Handle))) ?? - throw new InvalidOperationException("Internal type mismatch, native library returns null."); - } - } + public Array Array => GetValue(OutputInputType.Array); /// - /// Gets the or null if this output cells contains a different + /// Gets the or null if this output cells contains a different /// type stored. /// - public Map Map - { - get - { - EnsureType(OutputInputType.Map); - - return ReferenceAccessor.Access(new Map(OutputChannel.Map(Handle))) ?? - throw new InvalidOperationException("Internal type mismatch, native library returns null."); - } - } + public Map Map => GetValue(OutputInputType.Map); /// - /// Gets the or null if this output cells contains a different + /// Gets the or null if this output cells contains a different /// type /// stored. /// - public Text Text - { - get - { - EnsureType(OutputInputType.Text); - - return ReferenceAccessor.Access(new Text(OutputChannel.Text(Handle))) ?? - throw new InvalidOperationException("Internal type mismatch, native library returns null."); - } - } + public Text Text => GetValue(OutputInputType.Text); /// - /// Gets the or null if this output cells contains + /// Gets the or null if this output cells contains /// a different type stored. /// - public XmlElement XmlElement - { - get - { - EnsureType(OutputInputType.XmlElement); - - return ReferenceAccessor.Access(new XmlElement(OutputChannel.XmlElement(Handle))) ?? - throw new InvalidOperationException("Internal type mismatch, native library returns null."); - } - } + public XmlElement XmlElement => GetValue(OutputInputType.XmlElement); /// - /// Gets the or null if this output cells contains a + /// Gets the or null if this output cells contains a /// different type stored. /// - public XmlText? XmlText - { - get - { - EnsureType(OutputInputType.XmlText); - - return ReferenceAccessor.Access(new XmlText(OutputChannel.XmlText(Handle))) ?? - throw new InvalidOperationException("Internal type mismatch, native library returns null."); - } - } + public XmlText? XmlText => GetValue(OutputInputType.XmlText); /// /// Gets the type of the output. /// - public OutputInputType Type => (OutputInputType)(OutputNative?.Tag ?? -99); + public OutputInputType Type { get; private set; } - /// - /// Gets the handle to the native resource. - /// - internal nint Handle { get; } + private static Lazy BuildValue(nint handle, uint length, OutputInputType type) + { + static nint GuardHandle(nint handle) + { + if (handle == nint.Zero) + { + throw new InvalidOperationException("Internal type mismatch, native library returns null."); + } - /// - /// Gets the native output cell represented by this cell. - /// - private OutputNative? OutputNative { get; } + return handle; + } - private void EnsureType(OutputInputType expectedType) - { - if (Type != expectedType) + switch (type) { - throw new InvalidOperationException($"Expected {expectedType}, got {Type}."); + case OutputInputType.Bool: + { + var value = GuardHandle(OutputChannel.Boolean(handle)); + + return new Lazy((object?)(Marshal.PtrToStructure(value) == 1)); + } + + case OutputInputType.Double: + { + var value = GuardHandle(OutputChannel.Double(handle)); + + return new Lazy(Marshal.PtrToStructure(value)); + } + + case OutputInputType.Long: + { + var value = GuardHandle(OutputChannel.Long(handle)); + + return new Lazy(Marshal.PtrToStructure(value)); + } + + case OutputInputType.String: + { + MemoryReader.TryReadUtf8String(OutputChannel.String(handle), out var result); + + return new Lazy(result); + } + + case OutputInputType.Bytes: + { + var pointer = GuardHandle(OutputChannel.Bytes(handle)); + + var result = MemoryReader.TryReadBytes(OutputChannel.Bytes(handle), length) ?? + throw new InvalidOperationException("Internal type mismatch, native library returns null."); + + if (result == null) + { + throw new InvalidOperationException("Internal type mismatch, native library returns null."); + } + + OutputChannel.Destroy(pointer); + return new Lazy(result); + } + + case OutputInputType.Collection: + { + var pointer = GuardHandle(OutputChannel.Collection(handle)); + + var handles = MemoryReader.TryReadIntPtrArray(pointer, length, Marshal.SizeOf()) + ?? throw new InvalidOperationException("Internal type mismatch, native library returns null."); + + var result = handles.Select(x => new Output(x, false)).ToArray(); + + OutputChannel.Destroy(pointer); + return new Lazy(result); + } + + case OutputInputType.Object: + { + var pointer = GuardHandle(OutputChannel.Object(handle)); + + var handlesArray = MemoryReader.TryReadIntPtrArray(pointer, length, Marshal.SizeOf()) + ?? throw new InvalidOperationException("Internal type mismatch, native library returns null."); + + var result = new Dictionary(); + + foreach (var itemHandle in handlesArray) + { + var (mapEntry, outputHandle) = MemoryReader.ReadMapEntryAndOutputHandle(itemHandle); + var mapEntryKey = MemoryReader.ReadUtf8String(mapEntry.Field); + + result[mapEntryKey] = new Output(outputHandle, false); + } + + OutputChannel.Destroy(pointer); + return new Lazy(result); + } + + case OutputInputType.Array: + return new Lazy(() => new Array(GuardHandle(OutputChannel.Array(handle)))); + + case OutputInputType.Map: + return new Lazy(() => new Map(GuardHandle(OutputChannel.Map(handle)))); + + case OutputInputType.Text: + return new Lazy(() => new Text(GuardHandle(OutputChannel.Text(handle)))); + + case OutputInputType.XmlElement: + return new Lazy(() => new XmlElement(GuardHandle(OutputChannel.XmlElement(handle)))); + + case OutputInputType.XmlText: + return new Lazy(() => new XmlText(GuardHandle(OutputChannel.XmlText(handle)))); + + case OutputInputType.Doc: + return new Lazy(() => new Doc(GuardHandle(OutputChannel.Doc(handle)))); + + default: + return new Lazy((object?)null); } } - /// - public void Dispose() + private T GetValue(OutputInputType expectedType) { - if (!disposable) + var resolvedValue = value.Value; + + if (resolvedValue is not T typed) { - return; + throw new InvalidOperationException($"Expected {expectedType}, got {Type}."); } - OutputChannel.Destroy(Handle); + return typed; } } diff --git a/YDotNet/Document/Types/Arrays/Array.cs b/YDotNet/Document/Types/Arrays/Array.cs index a1c8b16f..f551ab54 100644 --- a/YDotNet/Document/Types/Arrays/Array.cs +++ b/YDotNet/Document/Types/Arrays/Array.cs @@ -69,7 +69,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) { var handle = ArrayChannel.Get(Handle, transaction.Handle, index); - return ReferenceAccessor.Access(new Output(handle, disposable: true)); + return ReferenceAccessor.Output(handle, true); } /// diff --git a/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs b/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs index aa36e36f..3255fcfd 100644 --- a/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs +++ b/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs @@ -40,7 +40,7 @@ public bool MoveNext() { var handle = ArrayChannel.IteratorNext(Iterator.Handle); - Current = handle != nint.Zero ? new Output(handle) : null; + Current = handle != nint.Zero ? new Output(handle, false) : null; return Current != null; } diff --git a/YDotNet/Document/Types/Events/EventChange.cs b/YDotNet/Document/Types/Events/EventChange.cs index d4bb9ce1..b1ad0318 100644 --- a/YDotNet/Document/Types/Events/EventChange.cs +++ b/YDotNet/Document/Types/Events/EventChange.cs @@ -7,6 +7,8 @@ namespace YDotNet.Document.Types.Events; /// public class EventChange { + private readonly Lazy> values; + /// /// Initializes a new instance of the class. /// @@ -16,11 +18,11 @@ public class EventChange /// Optional, the values affected by the current change if is /// . /// - public EventChange(EventChangeTag tag, uint length, IEnumerable? values) + public EventChange(EventChangeTag tag, uint length, Lazy> values) { Tag = tag; Length = length; - Values = values; + this.values = values; } /// @@ -36,5 +38,5 @@ public EventChange(EventChangeTag tag, uint length, IEnumerable? values) /// /// Gets the values that were affected by this change. /// - public IEnumerable? Values { get; } + public List Values => values.Value; } diff --git a/YDotNet/Document/Types/Events/EventDeltaAttribute.cs b/YDotNet/Document/Types/Events/EventDeltaAttribute.cs index d4a907df..8352104d 100644 --- a/YDotNet/Document/Types/Events/EventDeltaAttribute.cs +++ b/YDotNet/Document/Types/Events/EventDeltaAttribute.cs @@ -9,6 +9,8 @@ namespace YDotNet.Document.Types.Events; /// public class EventDeltaAttribute { + private readonly Lazy value; + /// /// Initializes a new instance of the class. /// @@ -18,7 +20,11 @@ public EventDeltaAttribute(nint handle) Handle = handle; Key = Marshal.PtrToStringAnsi(Marshal.ReadIntPtr(handle)); - Value = new Output(handle + MemoryConstants.PointerSize); + + value = new Lazy(() => + { + return new Output(handle + MemoryConstants.PointerSize, false); + }); } /// @@ -29,7 +35,7 @@ public EventDeltaAttribute(nint handle) /// /// Gets the attribute value. /// - public Output Value { get; } + public Output Value => value.Value; /// /// Gets the handle to the native resource. diff --git a/YDotNet/Document/Types/Maps/Map.cs b/YDotNet/Document/Types/Maps/Map.cs index a4403fd0..345070bf 100644 --- a/YDotNet/Document/Types/Maps/Map.cs +++ b/YDotNet/Document/Types/Maps/Map.cs @@ -59,7 +59,7 @@ public void Insert(Transaction transaction, string key, Input input) MemoryWriter.Release(keyHandle); - return ReferenceAccessor.Access(new Output(handle, disposable: true)); + return ReferenceAccessor.Output(handle, false); } /// diff --git a/YDotNet/Document/Types/Maps/MapEntry.cs b/YDotNet/Document/Types/Maps/MapEntry.cs index 2740c4d7..40570d66 100644 --- a/YDotNet/Document/Types/Maps/MapEntry.cs +++ b/YDotNet/Document/Types/Maps/MapEntry.cs @@ -20,7 +20,7 @@ internal MapEntry(nint handle) var (mapEntry, outputHandle) = MemoryReader.ReadMapEntryAndOutputHandle(handle); MapEntryNative = mapEntry; - Value = new Output(outputHandle); + Value = new Output(outputHandle, false); } /// diff --git a/YDotNet/Document/Types/Texts/TextChunk.cs b/YDotNet/Document/Types/Texts/TextChunk.cs index 67627c1f..0ff5d249 100644 --- a/YDotNet/Document/Types/Texts/TextChunk.cs +++ b/YDotNet/Document/Types/Texts/TextChunk.cs @@ -18,7 +18,7 @@ public class TextChunk /// The handle to the native resource. internal TextChunk(nint handle) { - Data = new Output(handle); + Data = new Output(handle, false); var offset = Marshal.SizeOf(); var attributesLength = (uint) Marshal.ReadInt32(handle + offset); diff --git a/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs b/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs index e27e1ff3..97c563c5 100644 --- a/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs +++ b/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs @@ -40,7 +40,7 @@ public bool MoveNext() { var handle = XmlElementChannel.TreeWalkerNext(TreeWalker.Handle); - Current = handle != nint.Zero ? new Output(handle) : null; + Current = handle != nint.Zero ? new Output(handle, false) : null; return Current != null; } diff --git a/YDotNet/Document/Types/XmlElements/XmlElement.cs b/YDotNet/Document/Types/XmlElements/XmlElement.cs index 7193de9c..75079cad 100644 --- a/YDotNet/Document/Types/XmlElements/XmlElement.cs +++ b/YDotNet/Document/Types/XmlElements/XmlElement.cs @@ -192,7 +192,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) { var handle = XmlElementChannel.Get(Handle, transaction.Handle, index); - return ReferenceAccessor.Access(new Output(handle, disposable: true)); + return ReferenceAccessor.Output(handle, true); } /// @@ -209,7 +209,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) { var handle = XmlChannel.PreviousSibling(Handle, transaction.Handle); - return ReferenceAccessor.Access(new Output(handle, disposable: true)); + return ReferenceAccessor.Output(handle, true); } /// @@ -226,7 +226,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) { var handle = XmlChannel.NextSibling(Handle, transaction.Handle); - return ReferenceAccessor.Access(new Output(handle, disposable: true)); + return ReferenceAccessor.Output(handle, true); } /// @@ -256,7 +256,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) { var handle = XmlElementChannel.FirstChild(Handle, transaction.Handle); - return ReferenceAccessor.Access(new Output(handle, disposable: true)); + return ReferenceAccessor.Output(handle, true); } /// diff --git a/YDotNet/Document/Types/XmlTexts/XmlText.cs b/YDotNet/Document/Types/XmlTexts/XmlText.cs index 32ead3a0..1a6e622f 100644 --- a/YDotNet/Document/Types/XmlTexts/XmlText.cs +++ b/YDotNet/Document/Types/XmlTexts/XmlText.cs @@ -198,7 +198,7 @@ public void Format(Transaction transaction, uint index, uint length, Input attri { var handle = XmlChannel.PreviousSibling(Handle, transaction.Handle); - return ReferenceAccessor.Access(new Output(handle, disposable: true)); + return ReferenceAccessor.Output(handle, true); } /// @@ -214,7 +214,7 @@ public void Format(Transaction transaction, uint index, uint length, Input attri { var handle = XmlChannel.NextSibling(Handle, transaction.Handle); - return ReferenceAccessor.Access(new Output(handle, disposable: true)); + return ReferenceAccessor.Output(handle, true); } /// diff --git a/YDotNet/Infrastructure/ReferenceAccessor.cs b/YDotNet/Infrastructure/ReferenceAccessor.cs index ed865725..42a82e60 100644 --- a/YDotNet/Infrastructure/ReferenceAccessor.cs +++ b/YDotNet/Infrastructure/ReferenceAccessor.cs @@ -69,9 +69,9 @@ internal static class ReferenceAccessor return Access(instance, instance.Handle); } - public static Output? Access(Output instance) + public static Output? Output(nint handle, bool shouldDispose) { - return Access(instance, instance.Handle); + return handle == nint.Zero ? null : new Output(handle, shouldDispose); } public static StickyIndex? Access(StickyIndex instance) diff --git a/YDotNet/Native/Types/Events/EventChangeNative.cs b/YDotNet/Native/Types/Events/EventChangeNative.cs index b8f08f68..21afd930 100644 --- a/YDotNet/Native/Types/Events/EventChangeNative.cs +++ b/YDotNet/Native/Types/Events/EventChangeNative.cs @@ -26,10 +26,14 @@ public EventChange ToEventChange() $"The value \"{TagNative}\" for {nameof(EventChangeTagNative)} is not supported.") }; - var attributes = MemoryReader.TryReadIntPtrArray(Values, Length, Marshal.SizeOf()) - ?.Select(x => new Output(x)) - .ToArray() ?? - Enumerable.Empty(); + var localValues = Values; + var localLength = Length; + + var attributes = new Lazy>(() => + { + return MemoryReader.TryReadIntPtrArray(localValues, localLength, Marshal.SizeOf())? + .Select(x => new Output(x, false)).ToList() ?? new List(); + }); return new EventChange( tag, diff --git a/YDotNet/Native/Types/Events/EventDeltaNative.cs b/YDotNet/Native/Types/Events/EventDeltaNative.cs index 5d0baee4..02920e25 100644 --- a/YDotNet/Native/Types/Events/EventDeltaNative.cs +++ b/YDotNet/Native/Types/Events/EventDeltaNative.cs @@ -37,7 +37,7 @@ public EventDelta ToEventDelta() return new EventDelta( tag, Length, - ReferenceAccessor.Access(new Output(InsertHandle)), + ReferenceAccessor.Output(InsertHandle, false), attributes ); } diff --git a/YDotNet/Native/Types/Events/EventKeyChangeNative.cs b/YDotNet/Native/Types/Events/EventKeyChangeNative.cs index 35f616b0..78ce6c9e 100644 --- a/YDotNet/Native/Types/Events/EventKeyChangeNative.cs +++ b/YDotNet/Native/Types/Events/EventKeyChangeNative.cs @@ -30,8 +30,8 @@ public EventKeyChange ToEventKeyChange() var result = new EventKeyChange( MemoryReader.ReadUtf8String(Key), tag, - ReferenceAccessor.Access(new Output(OldValue)), - ReferenceAccessor.Access(new Output(NewValue)) + ReferenceAccessor.Output(OldValue, false), + ReferenceAccessor.Output(NewValue, false) ); return result; From 7a7ba2ee886d92e7a2acc956ef52cece91b5eb52 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 8 Oct 2023 16:09:55 +0200 Subject: [PATCH 078/186] Fix warnings. --- .editorconfig | 3 ++- YDotNet/Document/Cells/Output.cs | 1 + YDotNet/Document/Types/Events/EventDeltaAttribute.cs | 2 +- YDotNet/Infrastructure/.editorconfig | 7 ------- YDotNet/Native/.editorconfig | 7 ------- YDotNet/Native/Document/DocChannel.cs | 1 - 6 files changed, 4 insertions(+), 17 deletions(-) delete mode 100644 YDotNet/Infrastructure/.editorconfig delete mode 100644 YDotNet/Native/.editorconfig diff --git a/.editorconfig b/.editorconfig index f2f28f30..726c88db 100644 --- a/.editorconfig +++ b/.editorconfig @@ -37,6 +37,7 @@ dotnet_diagnostic.SA1413.severity = suggestion # Use trailing comma in multi-lin dotnet_diagnostic.SA1500.severity = suggestion # Braces for multi-line statements should not share line # Documentation rules -dotnet_diagnostic.SA1600.severity = none # SA1600: Elements should be documented +dotnet_diagnostic.SA1600.severity = none # Elements should be documented +dotnet_diagnostic.SA1601.severity = none # Partial elements should be documented dotnet_diagnostic.SA1633.severity = none # File should have header diff --git a/YDotNet/Document/Cells/Output.cs b/YDotNet/Document/Cells/Output.cs index 532aa773..18919fc7 100644 --- a/YDotNet/Document/Cells/Output.cs +++ b/YDotNet/Document/Cells/Output.cs @@ -29,6 +29,7 @@ internal Output(nint handle, bool shouldDispose) { if (handle == nint.Zero) { + value = new Lazy((object?)null); return; } diff --git a/YDotNet/Document/Types/Events/EventDeltaAttribute.cs b/YDotNet/Document/Types/Events/EventDeltaAttribute.cs index 8352104d..f72f0b63 100644 --- a/YDotNet/Document/Types/Events/EventDeltaAttribute.cs +++ b/YDotNet/Document/Types/Events/EventDeltaAttribute.cs @@ -19,7 +19,7 @@ public EventDeltaAttribute(nint handle) { Handle = handle; - Key = Marshal.PtrToStringAnsi(Marshal.ReadIntPtr(handle)); + Key = Marshal.PtrToStringAnsi(Marshal.ReadIntPtr(handle)) ?? throw new InvalidOperationException("Failed to read key"); value = new Lazy(() => { diff --git a/YDotNet/Infrastructure/.editorconfig b/YDotNet/Infrastructure/.editorconfig deleted file mode 100644 index b7624e93..00000000 --- a/YDotNet/Infrastructure/.editorconfig +++ /dev/null @@ -1,7 +0,0 @@ -[*.cs] - -# StyleCop rules - -# Documentation rules -dotnet_diagnostic.SA1600.severity = suggestion # Elements should be documented -dotnet_diagnostic.SA1602.severity = suggestion # Enumeration items should be documented diff --git a/YDotNet/Native/.editorconfig b/YDotNet/Native/.editorconfig deleted file mode 100644 index b7624e93..00000000 --- a/YDotNet/Native/.editorconfig +++ /dev/null @@ -1,7 +0,0 @@ -[*.cs] - -# StyleCop rules - -# Documentation rules -dotnet_diagnostic.SA1600.severity = suggestion # Elements should be documented -dotnet_diagnostic.SA1602.severity = suggestion # Enumeration items should be documented diff --git a/YDotNet/Native/Document/DocChannel.cs b/YDotNet/Native/Document/DocChannel.cs index 2b5e4abd..61bed8cc 100644 --- a/YDotNet/Native/Document/DocChannel.cs +++ b/YDotNet/Native/Document/DocChannel.cs @@ -1,5 +1,4 @@ using System.Runtime.InteropServices; -using YDotNet.Native.Document.Events; namespace YDotNet.Native.Document; From 4e49be50f235e418cdfd15bb0955375d8942c779 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 8 Oct 2023 17:25:24 +0200 Subject: [PATCH 079/186] Fix output. --- Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs | 2 +- .../YDotNet.Tests.Unit/Arrays/IterateTests.cs | 2 +- .../YDotNet.Tests.Unit/Arrays/ObserveTests.cs | 2 +- Tests/YDotNet.Tests.Unit/Maps/GetTests.cs | 24 ++-- Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs | 2 +- .../Texts/InsertEmbedTests.cs | 4 +- .../UndoManagers/RedoTests.cs | 2 +- .../UndoManagers/UndoTests.cs | 1 - YDotNet/Document/Cells/Output.cs | 119 +++++++++--------- YDotNet/Document/Cells/OutputInputType.cs | 92 -------------- YDotNet/Document/Cells/OutputType.cs | 92 ++++++++++++++ 11 files changed, 169 insertions(+), 173 deletions(-) delete mode 100644 YDotNet/Document/Cells/OutputInputType.cs create mode 100644 YDotNet/Document/Cells/OutputType.cs diff --git a/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs b/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs index 51818fef..ea5d690d 100644 --- a/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs +++ b/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs @@ -52,7 +52,7 @@ public void GetAtMiddle() // Assert Assert.That(output, Is.Not.Null); - Assert.That(output.Undefined, Is.True); + Assert.That(output.Type, Is.EqualTo(OutputInputType.Undefined)); } [Test] diff --git a/Tests/YDotNet.Tests.Unit/Arrays/IterateTests.cs b/Tests/YDotNet.Tests.Unit/Arrays/IterateTests.cs index 2dec154b..0842bd8f 100644 --- a/Tests/YDotNet.Tests.Unit/Arrays/IterateTests.cs +++ b/Tests/YDotNet.Tests.Unit/Arrays/IterateTests.cs @@ -75,6 +75,6 @@ public void IteratesOnMultiItem() Assert.That(values.Length, Is.EqualTo(expected: 3)); Assert.That(values[0].Long, Is.EqualTo(expected: 2469L)); Assert.That(values[1].Boolean, Is.False); - Assert.That(values[2].Undefined, Is.True); + Assert.That(values[2].Type, Is.EqualTo(OutputInputType.Undefined)); } } diff --git a/Tests/YDotNet.Tests.Unit/Arrays/ObserveTests.cs b/Tests/YDotNet.Tests.Unit/Arrays/ObserveTests.cs index c49221e4..f20be74c 100644 --- a/Tests/YDotNet.Tests.Unit/Arrays/ObserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/Arrays/ObserveTests.cs @@ -116,7 +116,7 @@ public void ObserveHasDeltasWhenMoved() Assert.That(eventChanges.ElementAt(index: 0).Tag, Is.EqualTo(EventChangeTag.Add)); Assert.That(eventChanges.ElementAt(index: 0).Length, Is.EqualTo(expected: 1)); - Assert.That(eventChanges.ElementAt(index: 0).Values.First().Undefined, Is.True); + Assert.That(eventChanges.ElementAt(index: 0).Values.First().Type, Is.EqualTo(OutputInputType.Undefined)); Assert.That(eventChanges.ElementAt(index: 1).Tag, Is.EqualTo(EventChangeTag.Retain)); Assert.That(eventChanges.ElementAt(index: 1).Length, Is.EqualTo(expected: 2)); diff --git a/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs b/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs index 599662a6..6ba1967e 100644 --- a/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs +++ b/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs @@ -161,14 +161,14 @@ public void GetNull() ); // Act - var value1 = map.Get(transaction, "value1").Null; - var value2 = map.Get(transaction, "value2").Null; - var value3 = map.Get(transaction, "value3").Null; + var value1 = map.Get(transaction, "value1"); + var value2 = map.Get(transaction, "value2"); + var value3 = map.Get(transaction, "value3"); // Assert - Assert.That(value1, Is.True); - Assert.That(value2, Is.False); - Assert.That(value3, Is.False); + Assert.That(value1.Type, Is.EqualTo(OutputInputType.Null)); + Assert.That(value2.Type, Is.Not.EqualTo(OutputInputType.Null)); + Assert.That(value3.Type, Is.Not.EqualTo(OutputInputType.Null)); } [Test] @@ -182,14 +182,14 @@ public void GetUndefined() ); // Act - var value1 = map.Get(transaction, "value1").Undefined; - var value2 = map.Get(transaction, "value2").Undefined; - var value3 = map.Get(transaction, "value3").Undefined; + var value1 = map.Get(transaction, "value1"); + var value2 = map.Get(transaction, "value2"); + var value3 = map.Get(transaction, "value3"); // Assert - Assert.That(value1, Is.True); - Assert.That(value2, Is.False); - Assert.That(value3, Is.False); + Assert.That(value1.Type, Is.EqualTo(OutputInputType.Undefined)); + Assert.That(value2.Type, Is.Not.EqualTo(OutputInputType.Undefined)); + Assert.That(value3.Type, Is.Not.EqualTo(OutputInputType.Undefined)); } [Test] diff --git a/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs b/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs index dc352029..8796103c 100644 --- a/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs +++ b/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs @@ -323,7 +323,7 @@ public void InsertDifferentTypeOnExistingKey() transaction.Commit(); // Assert - Assert.That(value.Type, Is.EqualTo(OutputInputType.String)); + Assert.That(value.Type, Is.EqualTo(OutputType.String)); Assert.That(value.String, Is.EqualTo("Lucas")); Assert.That(length, Is.EqualTo(expected: 1)); } diff --git a/Tests/YDotNet.Tests.Unit/Texts/InsertEmbedTests.cs b/Tests/YDotNet.Tests.Unit/Texts/InsertEmbedTests.cs index 9cc46dfb..4c42cad4 100644 --- a/Tests/YDotNet.Tests.Unit/Texts/InsertEmbedTests.cs +++ b/Tests/YDotNet.Tests.Unit/Texts/InsertEmbedTests.cs @@ -147,7 +147,7 @@ public void InsertNullEmbed() var chunks = text.Chunks(transaction); Assert.That(chunks.Length, Is.EqualTo(expected: 3)); - Assert.That(chunks.ElementAt(index: 1).Data.Null, Is.True); + Assert.That(chunks.ElementAt(index: 1).Data.Type, Is.EqualTo(OutputInputType.Null)); } [Test] @@ -163,7 +163,7 @@ public void InsertUndefinedEmbed() var chunks = text.Chunks(transaction); Assert.That(chunks.Length, Is.EqualTo(expected: 3)); - Assert.That(chunks.ElementAt(index: 1).Data.Undefined, Is.True); + Assert.That(chunks.ElementAt(index: 1).Data.Type, Is.EqualTo(OutputInputType.Undefined)); } [Test] diff --git a/Tests/YDotNet.Tests.Unit/UndoManagers/RedoTests.cs b/Tests/YDotNet.Tests.Unit/UndoManagers/RedoTests.cs index c0d822f3..324a68aa 100644 --- a/Tests/YDotNet.Tests.Unit/UndoManagers/RedoTests.cs +++ b/Tests/YDotNet.Tests.Unit/UndoManagers/RedoTests.cs @@ -170,7 +170,7 @@ public void RedoAddingAndUpdatingAndRemovingContentOnMap() // Assert Assert.That(length, Is.EqualTo(expected: 4)); - Assert.That(value2.Undefined, Is.True); + Assert.That(value2.Type, Is.EqualTo(OutputType.Undefined)); Assert.That(result, Is.True); // Act (remove, undo, and redo) diff --git a/Tests/YDotNet.Tests.Unit/UndoManagers/UndoTests.cs b/Tests/YDotNet.Tests.Unit/UndoManagers/UndoTests.cs index 593c8f27..99ddfb95 100644 --- a/Tests/YDotNet.Tests.Unit/UndoManagers/UndoTests.cs +++ b/Tests/YDotNet.Tests.Unit/UndoManagers/UndoTests.cs @@ -165,7 +165,6 @@ public void UndoAddingAndUpdatingAndRemovingContentOnMap() // Assert Assert.That(length, Is.EqualTo(expected: 3)); Assert.That(value2.String, Is.EqualTo("Lucas")); - Assert.That(value2.Undefined, Is.False); Assert.That(result, Is.True); // Act (remove and undo) diff --git a/YDotNet/Document/Cells/Output.cs b/YDotNet/Document/Cells/Output.cs index 18919fc7..41e374ea 100644 --- a/YDotNet/Document/Cells/Output.cs +++ b/YDotNet/Document/Cells/Output.cs @@ -1,4 +1,3 @@ -using System.Reflection.Metadata; using System.Runtime.InteropServices; using YDotNet.Document.Types.Maps; using YDotNet.Document.Types.Texts; @@ -29,14 +28,14 @@ internal Output(nint handle, bool shouldDispose) { if (handle == nint.Zero) { - value = new Lazy((object?)null); - return; + throw new ArgumentException("Handle cannot be zero.", nameof(handle)); } var native = Marshal.PtrToStructure(handle); - Type = (OutputInputType)native.Tag; + Type = (OutputType)native.Tag; + // We use lazy because some types like Doc and Map need to be disposed and therefore they should not be allocated, if not needed. value = BuildValue(handle, native.Length, Type); if (shouldDispose) @@ -46,91 +45,89 @@ internal Output(nint handle, bool shouldDispose) } /// - /// Gets the or null if this output cell contains a different type stored. + /// Gets the value. /// - public Doc Doc => GetValue(OutputInputType.Doc); + /// Value is not a . + public Doc Doc => GetValue(OutputType.Doc); /// - /// Gets the or null if this output cell contains a different type stored. + /// Gets the value. /// - public string String => GetValue(OutputInputType.String); + /// Value is not a . + public string String => GetValue(OutputType.String); /// - /// Gets the or null if this output cell contains a different type stored. + /// Gets the value. /// - public bool Boolean => GetValue(OutputInputType.Bool); + /// Value is not a . + public bool Boolean => GetValue(OutputType.Bool); /// - /// Gets the or null if this output cell contains a different type stored. + /// Gets the value. /// - public double Double => GetValue(OutputInputType.Double); + /// Value is not a . + public double Double => GetValue(OutputType.Double); /// - /// Gets the or null if this output cell contains a different type stored. + /// Gets the value. /// - public long Long => GetValue(OutputInputType.Long); + /// Value is not a . + public long Long => GetValue(OutputType.Long); /// - /// Gets the array or null if this output cells contains a different type stored. + /// Gets the array value. /// - public byte[] Bytes => GetValue(OutputInputType.Bytes); + /// Value is not a array. + public byte[] Bytes => GetValue(OutputType.Bytes); /// - /// Gets the collection or null if this output cells contains a different type stored. + /// Gets the collection. /// - public Output[] Collection => GetValue(OutputInputType.Collection); + /// Value is not a collection. + public Output[] Collection => GetValue(OutputType.Collection); /// - /// Gets the dictionary or null if this output cells contains a different type stored. + /// Gets the value as json object. /// - public IDictionary? Object => GetValue>(OutputInputType.Object); + /// Value is not a json object. + public IDictionary? Object => GetValue>(OutputType.Object); /// - /// Gets a value indicating whether this output cell contains a null value. + /// Gets the value. /// - public bool Null => Type == OutputInputType.Null; + /// Value is not a . + public Array Array => GetValue(OutputType.Array); /// - /// Gets a value indicating whether this output cell contains an undefined value. + /// Gets the value. /// - public bool Undefined => Type == OutputInputType.Undefined; + /// Value is not a . + public Map Map => GetValue(OutputType.Map); /// - /// Gets the or null if this output cells contains a different type stored. + /// Gets the value. /// - public Array Array => GetValue(OutputInputType.Array); + /// Value is not a . + public Text Text => GetValue(OutputType.Text); /// - /// Gets the or null if this output cells contains a different - /// type stored. + /// Gets the value. /// - public Map Map => GetValue(OutputInputType.Map); + /// Value is not a . + public XmlElement XmlElement => GetValue(OutputType.XmlElement); /// - /// Gets the or null if this output cells contains a different - /// type - /// stored. + /// Gets the value. /// - public Text Text => GetValue(OutputInputType.Text); - - /// - /// Gets the or null if this output cells contains - /// a different type stored. - /// - public XmlElement XmlElement => GetValue(OutputInputType.XmlElement); - - /// - /// Gets the or null if this output cells contains a - /// different type stored. - /// - public XmlText? XmlText => GetValue(OutputInputType.XmlText); + /// Value is not a . + public XmlText XmlText => GetValue(OutputType.XmlText); /// /// Gets the type of the output. /// - public OutputInputType Type { get; private set; } + public OutputType Type { get; private set; } - private static Lazy BuildValue(nint handle, uint length, OutputInputType type) + private static Lazy BuildValue(nint handle, uint length, OutputType type) { static nint GuardHandle(nint handle) { @@ -144,35 +141,35 @@ static nint GuardHandle(nint handle) switch (type) { - case OutputInputType.Bool: + case OutputType.Bool: { var value = GuardHandle(OutputChannel.Boolean(handle)); return new Lazy((object?)(Marshal.PtrToStructure(value) == 1)); } - case OutputInputType.Double: + case OutputType.Double: { var value = GuardHandle(OutputChannel.Double(handle)); return new Lazy(Marshal.PtrToStructure(value)); } - case OutputInputType.Long: + case OutputType.Long: { var value = GuardHandle(OutputChannel.Long(handle)); return new Lazy(Marshal.PtrToStructure(value)); } - case OutputInputType.String: + case OutputType.String: { MemoryReader.TryReadUtf8String(OutputChannel.String(handle), out var result); return new Lazy(result); } - case OutputInputType.Bytes: + case OutputType.Bytes: { var pointer = GuardHandle(OutputChannel.Bytes(handle)); @@ -188,7 +185,7 @@ static nint GuardHandle(nint handle) return new Lazy(result); } - case OutputInputType.Collection: + case OutputType.Collection: { var pointer = GuardHandle(OutputChannel.Collection(handle)); @@ -201,7 +198,7 @@ static nint GuardHandle(nint handle) return new Lazy(result); } - case OutputInputType.Object: + case OutputType.Object: { var pointer = GuardHandle(OutputChannel.Object(handle)); @@ -222,22 +219,22 @@ static nint GuardHandle(nint handle) return new Lazy(result); } - case OutputInputType.Array: + case OutputType.Array: return new Lazy(() => new Array(GuardHandle(OutputChannel.Array(handle)))); - case OutputInputType.Map: + case OutputType.Map: return new Lazy(() => new Map(GuardHandle(OutputChannel.Map(handle)))); - case OutputInputType.Text: + case OutputType.Text: return new Lazy(() => new Text(GuardHandle(OutputChannel.Text(handle)))); - case OutputInputType.XmlElement: + case OutputType.XmlElement: return new Lazy(() => new XmlElement(GuardHandle(OutputChannel.XmlElement(handle)))); - case OutputInputType.XmlText: + case OutputType.XmlText: return new Lazy(() => new XmlText(GuardHandle(OutputChannel.XmlText(handle)))); - case OutputInputType.Doc: + case OutputType.Doc: return new Lazy(() => new Doc(GuardHandle(OutputChannel.Doc(handle)))); default: @@ -245,7 +242,7 @@ static nint GuardHandle(nint handle) } } - private T GetValue(OutputInputType expectedType) + private T GetValue(OutputType expectedType) { var resolvedValue = value.Value; diff --git a/YDotNet/Document/Cells/OutputInputType.cs b/YDotNet/Document/Cells/OutputInputType.cs deleted file mode 100644 index 28f372bb..00000000 --- a/YDotNet/Document/Cells/OutputInputType.cs +++ /dev/null @@ -1,92 +0,0 @@ -namespace YDotNet.Document.Cells; - -/// -/// The type of an output. -/// -public enum OutputInputType -{ - /// - /// No defined. - /// - NotSet = -99, - - /// - /// Flag used by `YInput` and `YOutput` to tag boolean values. - /// - Bool = -8, - - /// - /// Flag used by `YInput` and `YOutput` to tag floating point numbers. - /// - Double = -7, - - /// - /// Flag used by `YInput` and `YOutput` to tag 64-bit integer numbers. - /// - Long = -6, - - /// - /// Flag used by `YInput` and `YOutput` to tag strings. - /// - String = -5, - - /// - /// Flag used by `YInput` and `YOutput` to tag binary content. - /// - Bytes = -4, - - /// - /// Flag used by `YInput` and `YOutput` to tag embedded JSON-like arrays of values. - /// - Collection = -3, - - /// - /// Flag used by `YInput` and `YOutput` to tag embedded JSON-like maps of key-value pairs. - /// - Object = -2, - - /// - /// Flag used by `YInput` and `YOutput` to tag JSON-like null values. - /// - Null = -1, - - /// - /// Flag used by `YInput` and `YOutput` to tag JSON-like undefined values. - /// - Undefined = 0, - - /// - /// Flag used by `YInput` and `YOutput` to tag content, which is an `YArray` shared type. - /// - Array = 1, - - /// - /// Flag used by `YInput` and `YOutput` to tag content, which is an `YMap` shared type. - /// - Map = 2, - - /// - /// Flag used by `YInput` and `YOutput` to tag content, which is an `YText` shared type. - /// - Text = 3, - - /// - /// Flag used by `YInput` and `YOutput` to tag content, which is an `YXmlElement` shared type. - /// - XmlElement = 4, - - /// - /// Flag used by `YInput` and `YOutput` to tag content, which is an `YXmlText` shared type. - /// - XmlText = 5, - - /// - /// Flag used by `YInput` and `YOutput` to tag content, which is an `YXmlFragment` shared type. - /// - XmlFragment = 6, - - /// - /// Flag used by `YInput` and `YOutput` to tag content, which is an `YDoc` shared type. - /// - Doc = 7 -} diff --git a/YDotNet/Document/Cells/OutputType.cs b/YDotNet/Document/Cells/OutputType.cs new file mode 100644 index 00000000..04e64bc5 --- /dev/null +++ b/YDotNet/Document/Cells/OutputType.cs @@ -0,0 +1,92 @@ +namespace YDotNet.Document.Cells; + +/// +/// The type of an output. +/// +public enum OutputType +{ + /// + /// No defined. + /// + NotSet = -99, + + /// + /// Flag used by to tag boolean values. + /// + Bool = -8, + + /// + /// Flag used by to tag floating point numbers. + /// + Double = -7, + + /// + /// Flag used by to tag 64-bit integer numbers. + /// + Long = -6, + + /// + /// Flag used by to tag strings. + /// + String = -5, + + /// + /// Flag used by to tag binary content. + /// + Bytes = -4, + + /// + /// Flag used by to tag embedded JSON-like arrays of values. + /// + Collection = -3, + + /// + /// Flag used by to tag embedded JSON-like maps of key-value pairs. + /// + Object = -2, + + /// + /// Flag used by to tag JSON-like null values. + /// + Null = -1, + + /// + /// Flag used by to tag JSON-like undefined values. + /// + Undefined = 0, + + /// + /// Flag used by to tag content, which is an `YArray` shared type. + /// + Array = 1, + + /// + /// Flag used by to tag content, which is an `YMap` shared type. + /// + Map = 2, + + /// + /// Flag used by to tag content, which is an `YText` shared type. + /// + Text = 3, + + /// + /// Flag used by to tag content, which is an `YXmlElement` shared type. + /// + XmlElement = 4, + + /// + /// Flag used by to tag content, which is an `YXmlText` shared type. + /// + XmlText = 5, + + /// + /// Flag used by to tag content, which is an `YXmlFragment` shared type. + /// + XmlFragment = 6, + + /// + /// Flag used by to tag content, which is an `YDoc` shared type. + /// + Doc = 7 +} From d49784b5ddd41e52eaab1b9baab912764648999c Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 8 Oct 2023 17:34:07 +0200 Subject: [PATCH 080/186] Fix output. --- Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs | 2 +- Tests/YDotNet.Tests.Unit/Maps/GetTests.cs | 16 +-- Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs | 2 +- YDotNet/Document/Cells/Output.cs | 99 +++++++++++-------- .../{OutputInputType.cs => OutputType.cs} | 2 +- 5 files changed, 69 insertions(+), 52 deletions(-) rename YDotNet/Document/Cells/{OutputInputType.cs => OutputType.cs} (98%) diff --git a/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs b/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs index 51818fef..12c5b832 100644 --- a/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs +++ b/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs @@ -36,7 +36,7 @@ public void GetAtBeginning() // Assert Assert.That(output, Is.Not.Null); - Assert.That(output.Type, Is.EqualTo(OutputInputType.Bool)); + Assert.That(output.Type, Is.EqualTo(OutputType.Bool)); } [Test] diff --git a/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs b/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs index 599662a6..da65f059 100644 --- a/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs +++ b/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs @@ -96,7 +96,7 @@ public void GetBytes() // Assert Assert.That(value1, Is.EqualTo(new byte[] { 2, 4, 6, 9 })); - Assert.That(value2.Type, Is.EqualTo(OutputInputType.Bool)); + Assert.That(value2.Type, Is.EqualTo(OutputType.Bool)); } [Test] @@ -122,7 +122,7 @@ public void GetCollection() Assert.That(value1.Length, Is.EqualTo(expected: 2)); Assert.That(value1[0].Long, Is.EqualTo(expected: 2469)); Assert.That(value1[1].Long, Is.EqualTo(expected: -420L)); - Assert.That(value2.Type, Is.EqualTo(OutputInputType.Bool)); + Assert.That(value2.Type, Is.EqualTo(OutputType.Bool)); } [Test] @@ -147,7 +147,7 @@ public void GetObject() Assert.That(value1.Keys.Count, Is.EqualTo(expected: 2)); Assert.That(value1["star-⭐"].Long, Is.EqualTo(expected: 2469)); Assert.That(value1["moon-🌕"].Long, Is.EqualTo(expected: -420L)); - Assert.That(value2.Type, Is.EqualTo(OutputInputType.Bool)); + Assert.That(value2.Type, Is.EqualTo(OutputType.Bool)); } [Test] @@ -208,7 +208,7 @@ public void GetText() // Assert Assert.That(value1, Is.Not.Null); Assert.That(value1.String(transaction), Is.EqualTo("Lucas")); - Assert.That(value2.Type, Is.EqualTo(OutputInputType.Bool)); + Assert.That(value2.Type, Is.EqualTo(OutputType.Bool)); } [Test] @@ -232,7 +232,7 @@ public void GetArray() // Assert Assert.That(value1, Is.Not.Null); Assert.That(value1.Length, Is.EqualTo(expected: 2)); - Assert.That(value2.Type, Is.EqualTo(OutputInputType.Null)); + Assert.That(value2.Type, Is.EqualTo(OutputType.Null)); } [Test] @@ -258,7 +258,7 @@ public void GetMap() Assert.That(value1.Length(transaction), Is.EqualTo(expected: 2)); Assert.That(value1.Get(transaction, "value1-1").Long, Is.EqualTo(expected: 2469L)); Assert.That(value1.Get(transaction, "value1-2").Long, Is.EqualTo(expected: -420L)); - Assert.That(value2.Type, Is.EqualTo(OutputInputType.Bool)); + Assert.That(value2.Type, Is.EqualTo(OutputType.Bool)); } [Test] @@ -277,7 +277,7 @@ public void GetXmlElement() // Assert Assert.That(value1, Is.Not.Null); Assert.That(value1.Tag, Is.EqualTo("person")); - Assert.That(value2.Type, Is.EqualTo(OutputInputType.Null)); + Assert.That(value2.Type, Is.EqualTo(OutputType.Null)); } [Test] @@ -296,7 +296,7 @@ public void GetXmlText() // Assert Assert.That(value1, Is.Not.Null); Assert.That(value1.Length(transaction), Is.EqualTo(expected: 5)); - Assert.That(value2.Type, Is.EqualTo(OutputInputType.Null)); + Assert.That(value2.Type, Is.EqualTo(OutputType.Null)); } [Test] diff --git a/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs b/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs index dc352029..8796103c 100644 --- a/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs +++ b/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs @@ -323,7 +323,7 @@ public void InsertDifferentTypeOnExistingKey() transaction.Commit(); // Assert - Assert.That(value.Type, Is.EqualTo(OutputInputType.String)); + Assert.That(value.Type, Is.EqualTo(OutputType.String)); Assert.That(value.String, Is.EqualTo("Lucas")); Assert.That(length, Is.EqualTo(expected: 1)); } diff --git a/YDotNet/Document/Cells/Output.cs b/YDotNet/Document/Cells/Output.cs index d64eb04b..6b4127d0 100644 --- a/YDotNet/Document/Cells/Output.cs +++ b/YDotNet/Document/Cells/Output.cs @@ -36,33 +36,45 @@ internal Output(nint handle, bool disposable = false) } /// - /// Gets the or null if this output cell contains a different type stored. + /// Gets the value. /// - public Doc? Doc => ReferenceAccessor.Access(new Doc(OutputChannel.Doc(Handle))); + /// Value is not a . + public Doc Doc + { + get + { + EnsureType(OutputType.Doc); + + return ReferenceAccessor.Access(new Doc(OutputChannel.Doc(Handle))) ?? + throw new InvalidOperationException("Internal type mismatch, native library returns null."); + } + } /// - /// Gets the or null if this output cell contains a different type stored. + /// Gets the value. /// - public string? String + /// Value is not a . + public string String { get { - EnsureType(OutputInputType.String); + EnsureType(OutputType.String); MemoryReader.TryReadUtf8String(OutputChannel.String(Handle), out var result); - return result; + return result ?? throw new InvalidOperationException("Internal type mismatch, native library returns null."); } } /// - /// Gets the or null if this output cell contains a different type stored. + /// Gets the value. /// - public bool? Boolean + /// Value is not a . + public bool Boolean { get { - EnsureType(OutputInputType.Bool); + EnsureType(OutputType.Bool); var value = OutputChannel.Boolean(Handle); @@ -76,13 +88,14 @@ public bool? Boolean } /// - /// Gets the or null if this output cell contains a different type stored. + /// Gets the value. /// - public double? Double + /// Value is not a . + public double Double { get { - EnsureType(OutputInputType.Double); + EnsureType(OutputType.Double); var value = OutputChannel.Double(Handle); @@ -96,13 +109,14 @@ public double? Double } /// - /// Gets the or null if this output cell contains a different type stored. + /// Gets the value. /// - public long? Long + /// Value is not a . + public long Long { get { - EnsureType(OutputInputType.Long); + EnsureType(OutputType.Long); var value = OutputChannel.Long(Handle); @@ -116,13 +130,14 @@ public long? Long } /// - /// Gets the array or null if this output cells contains a different type stored. + /// Gets the value. /// + /// Value is not a . public byte[] Bytes { get { - EnsureType(OutputInputType.Bytes); + EnsureType(OutputType.Bytes); var result = MemoryReader.TryReadBytes(OutputChannel.Bytes(Handle), OutputNative.Value.Length) ?? throw new InvalidOperationException("Internal type mismatch, native library returns null."); @@ -137,13 +152,14 @@ public byte[] Bytes } /// - /// Gets the collection or null if this output cells contains a different type stored. + /// Gets the value. /// + /// Value is not a . public Output[] Collection { get { - EnsureType(OutputInputType.Collection); + EnsureType(OutputType.Collection); var handles = MemoryReader.TryReadIntPtrArray( OutputChannel.Collection(Handle), OutputNative!.Value.Length, Marshal.SizeOf()); @@ -158,13 +174,14 @@ public Output[] Collection } /// - /// Gets the dictionary or null if this output cells contains a different type stored. + /// Gets the JsonObject value. /// + /// Value is not a JsonObject. public IDictionary? Object { get { - EnsureType(OutputInputType.Object); + EnsureType(OutputType.Object); var handles = MemoryReader.TryReadIntPtrArray( OutputChannel.Object(Handle), OutputNative!.Value.Length, Marshal.SizeOf()); @@ -191,21 +208,22 @@ public IDictionary? Object /// /// Gets a value indicating whether this output cell contains a null value. /// - public bool Null => Type == OutputInputType.Null; + public bool Null => Type == OutputType.Null; /// /// Gets a value indicating whether this output cell contains an undefined value. /// - public bool Undefined => Type == OutputInputType.Undefined; + public bool Undefined => Type == OutputType.Undefined; /// - /// Gets the or null if this output cells contains a different type stored. + /// Gets the value. /// + /// Value is not a . public Array Array { get { - EnsureType(OutputInputType.Array); + EnsureType(OutputType.Array); return ReferenceAccessor.Access(new Array(OutputChannel.Array(Handle))) ?? throw new InvalidOperationException("Internal type mismatch, native library returns null."); @@ -213,14 +231,14 @@ public Array Array } /// - /// Gets the or null if this output cells contains a different - /// type stored. + /// Gets the value. /// + /// Value is not a . public Map Map { get { - EnsureType(OutputInputType.Map); + EnsureType(OutputType.Map); return ReferenceAccessor.Access(new Map(OutputChannel.Map(Handle))) ?? throw new InvalidOperationException("Internal type mismatch, native library returns null."); @@ -228,15 +246,14 @@ public Map Map } /// - /// Gets the or null if this output cells contains a different - /// type - /// stored. + /// Gets the value. /// + /// Value is not a . public Text Text { get { - EnsureType(OutputInputType.Text); + EnsureType(OutputType.Text); return ReferenceAccessor.Access(new Text(OutputChannel.Text(Handle))) ?? throw new InvalidOperationException("Internal type mismatch, native library returns null."); @@ -244,14 +261,14 @@ public Text Text } /// - /// Gets the or null if this output cells contains - /// a different type stored. + /// Gets the value. /// + /// Value is not a . public XmlElement XmlElement { get { - EnsureType(OutputInputType.XmlElement); + EnsureType(OutputType.XmlElement); return ReferenceAccessor.Access(new XmlElement(OutputChannel.XmlElement(Handle))) ?? throw new InvalidOperationException("Internal type mismatch, native library returns null."); @@ -259,14 +276,14 @@ public XmlElement XmlElement } /// - /// Gets the or null if this output cells contains a - /// different type stored. + /// Gets the value. /// - public XmlText? XmlText + /// Value is not a . + public XmlText XmlText { get { - EnsureType(OutputInputType.XmlText); + EnsureType(OutputType.XmlText); return ReferenceAccessor.Access(new XmlText(OutputChannel.XmlText(Handle))) ?? throw new InvalidOperationException("Internal type mismatch, native library returns null."); @@ -276,7 +293,7 @@ public XmlText? XmlText /// /// Gets the type of the output. /// - public OutputInputType Type => (OutputInputType)(OutputNative?.Tag ?? -99); + public OutputType Type => (OutputType)(OutputNative?.Tag ?? -99); /// /// Gets the handle to the native resource. @@ -288,7 +305,7 @@ public XmlText? XmlText /// private OutputNative? OutputNative { get; } - private void EnsureType(OutputInputType expectedType) + private void EnsureType(OutputType expectedType) { if (Type != expectedType) { diff --git a/YDotNet/Document/Cells/OutputInputType.cs b/YDotNet/Document/Cells/OutputType.cs similarity index 98% rename from YDotNet/Document/Cells/OutputInputType.cs rename to YDotNet/Document/Cells/OutputType.cs index 28f372bb..ad19411d 100644 --- a/YDotNet/Document/Cells/OutputInputType.cs +++ b/YDotNet/Document/Cells/OutputType.cs @@ -3,7 +3,7 @@ namespace YDotNet.Document.Cells; /// /// The type of an output. /// -public enum OutputInputType +public enum OutputType { /// /// No defined. From 9c82b5235d4b3a42e2e23f2b7c6159cdc9c7e46e Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 8 Oct 2023 17:54:41 +0200 Subject: [PATCH 081/186] Simplify clustering. --- T/Program.cs | 44 ----- T/T.csproj | 18 -- .../ClusteringCallback.cs | 4 +- .../ClusteringOptions.cs | 10 + .../Internal/LoggerTextWriter.cs | 30 +++ .../Internal/PublishQueue.cs | 2 +- .../Messages.cs | 4 +- YDotNet.Server.Clustering/RedisConnection.cs | 24 +++ .../RedisDocumentStorage.cs | 60 ++++++ .../RedisDocumentStorageOptions.cs | 10 + YDotNet.Server.Clustering/RedisOptions.cs | 25 +++ .../ServiceExtensions.cs | 32 ++++ .../YDotNet.Server.Clustering.csproj | 19 ++ .../RedisClusteringOptions.cs | 6 - YDotNet.Server.Redis/RedisPubSub.cs | 65 +++++++ YDotNet.Server.Redis/ServiceExtensions.cs | 2 +- .../YDotNet.Server.Redis.csproj | 1 - .../Clustering/ClusteringCallback.cs | 179 ++++++++++++++++++ .../Clustering/ClusteringOptions.cs | 10 + YDotNet.Server/Clustering/IPubSub.cs | 16 ++ .../Clustering/Internal/PublishQueue.cs | 85 +++++++++ YDotNet.Server/Clustering/Messages.cs | 56 ++++++ YDotNet.Server/Internal/DelegateDisposable.cs | 16 ++ YDotNet.Server/ServiceExtensions.cs | 17 ++ YDotNet.Server/YDotNet.Server.csproj | 1 + YDotNet.sln | 4 +- 26 files changed, 663 insertions(+), 77 deletions(-) delete mode 100644 T/Program.cs delete mode 100644 T/T.csproj rename YDotNet.Server.Redis/RedisClusteringCallback.cs => YDotNet.Server.Clustering/ClusteringCallback.cs (98%) create mode 100644 YDotNet.Server.Clustering/ClusteringOptions.cs create mode 100644 YDotNet.Server.Clustering/Internal/LoggerTextWriter.cs rename {YDotNet.Server.Redis => YDotNet.Server.Clustering}/Internal/PublishQueue.cs (98%) rename {YDotNet.Server.Redis => YDotNet.Server.Clustering}/Messages.cs (93%) create mode 100644 YDotNet.Server.Clustering/RedisConnection.cs create mode 100644 YDotNet.Server.Clustering/RedisDocumentStorage.cs create mode 100644 YDotNet.Server.Clustering/RedisDocumentStorageOptions.cs create mode 100644 YDotNet.Server.Clustering/RedisOptions.cs create mode 100644 YDotNet.Server.Clustering/ServiceExtensions.cs create mode 100644 YDotNet.Server.Clustering/YDotNet.Server.Clustering.csproj create mode 100644 YDotNet.Server.Redis/RedisPubSub.cs create mode 100644 YDotNet.Server/Clustering/ClusteringCallback.cs create mode 100644 YDotNet.Server/Clustering/ClusteringOptions.cs create mode 100644 YDotNet.Server/Clustering/IPubSub.cs create mode 100644 YDotNet.Server/Clustering/Internal/PublishQueue.cs create mode 100644 YDotNet.Server/Clustering/Messages.cs create mode 100644 YDotNet.Server/Internal/DelegateDisposable.cs diff --git a/T/Program.cs b/T/Program.cs deleted file mode 100644 index 5b52b1be..00000000 --- a/T/Program.cs +++ /dev/null @@ -1,44 +0,0 @@ -using YDotNet.Document; -using YDotNet.Document.Cells; -using YDotNet.Document.Events; - -namespace T -{ - internal class Program - { - static void Main(string[] args) - { - // Arrange - var doc = new Doc(); - - AfterTransactionEvent? afterTransactionEvent = null; - var called = 0; - - var text = doc.Text("country"); - var subscription = doc.ObserveAfterTransaction( - e => - { - called++; - afterTransactionEvent = e; - }); - - // Act - var transaction = doc.WriteTransaction(); - text.Insert(transaction, index: 0, "Brazil"); - transaction.Commit(); - // Act - afterTransactionEvent = null; - transaction = doc.WriteTransaction(); - text.Insert(transaction, index: 0, "Great "); - transaction.Commit(); - - // Act - afterTransactionEvent = null; - doc.UnobserveAfterTransaction(subscription); - - transaction = doc.WriteTransaction(); - text.Insert(transaction, index: 0, "The "); - transaction.Commit(); - } - } -} diff --git a/T/T.csproj b/T/T.csproj deleted file mode 100644 index 3f34271d..00000000 --- a/T/T.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - Exe - net7.0 - enable - enable - - - - - - - - - - - diff --git a/YDotNet.Server.Redis/RedisClusteringCallback.cs b/YDotNet.Server.Clustering/ClusteringCallback.cs similarity index 98% rename from YDotNet.Server.Redis/RedisClusteringCallback.cs rename to YDotNet.Server.Clustering/ClusteringCallback.cs index c6e8e049..417c1bb0 100644 --- a/YDotNet.Server.Redis/RedisClusteringCallback.cs +++ b/YDotNet.Server.Clustering/ClusteringCallback.cs @@ -1,9 +1,9 @@ using Microsoft.Extensions.Options; using ProtoBuf; using StackExchange.Redis; -using YDotNet.Server.Redis.Internal; +using YDotNet.Server.Clustering.Internal; -namespace YDotNet.Server.Redis; +namespace YDotNet.Server.Clustering; public sealed class RedisClusteringCallback : IDocumentCallback, IDisposable { diff --git a/YDotNet.Server.Clustering/ClusteringOptions.cs b/YDotNet.Server.Clustering/ClusteringOptions.cs new file mode 100644 index 00000000..83848393 --- /dev/null +++ b/YDotNet.Server.Clustering/ClusteringOptions.cs @@ -0,0 +1,10 @@ +namespace YDotNet.Server.Clustering; + +public sealed class ClusteringOptions +{ + public TimeSpan DebounceTime { get; set; } = TimeSpan.FromMilliseconds(500); + + public int MaxBatchCount { get; set; } = 100; + + public int MaxBatchSize { get; set; } = 1024 * 1024; +} diff --git a/YDotNet.Server.Clustering/Internal/LoggerTextWriter.cs b/YDotNet.Server.Clustering/Internal/LoggerTextWriter.cs new file mode 100644 index 00000000..18abbbaf --- /dev/null +++ b/YDotNet.Server.Clustering/Internal/LoggerTextWriter.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.Logging; +using System.Text; + +namespace YDotNet.Server.Clustering.Internal; + +internal sealed class LoggerTextWriter : TextWriter +{ + private readonly ILogger log; + + public LoggerTextWriter(ILogger log) + { + this.log = log; + } + + public override Encoding Encoding => Encoding.UTF8; + + public override void Write(char value) + { + } + + public override void WriteLine(string? value) + { + if (log.IsEnabled(LogLevel.Debug)) + { +#pragma warning disable CA2254 // Template should be a static expression + log.LogDebug(new EventId(100, "RedisConnectionLog"), value); +#pragma warning restore CA2254 // Template should be a static expression + } + } +} diff --git a/YDotNet.Server.Redis/Internal/PublishQueue.cs b/YDotNet.Server.Clustering/Internal/PublishQueue.cs similarity index 98% rename from YDotNet.Server.Redis/Internal/PublishQueue.cs rename to YDotNet.Server.Clustering/Internal/PublishQueue.cs index 86de4618..379cb5c3 100644 --- a/YDotNet.Server.Redis/Internal/PublishQueue.cs +++ b/YDotNet.Server.Clustering/Internal/PublishQueue.cs @@ -1,6 +1,6 @@ using System.Threading.Channels; -namespace YDotNet.Server.Redis.Internal; +namespace YDotNet.Server.Clustering.Internal; public interface ICanEstimateSize { diff --git a/YDotNet.Server.Redis/Messages.cs b/YDotNet.Server.Clustering/Messages.cs similarity index 93% rename from YDotNet.Server.Redis/Messages.cs rename to YDotNet.Server.Clustering/Messages.cs index c82887b5..59f3d709 100644 --- a/YDotNet.Server.Redis/Messages.cs +++ b/YDotNet.Server.Clustering/Messages.cs @@ -1,9 +1,9 @@ using ProtoBuf; -using YDotNet.Server.Redis.Internal; +using YDotNet.Server.Clustering.Internal; #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. -namespace YDotNet.Server.Redis; +namespace YDotNet.Server.Clustering; [ProtoContract] public enum MessageType diff --git a/YDotNet.Server.Clustering/RedisConnection.cs b/YDotNet.Server.Clustering/RedisConnection.cs new file mode 100644 index 00000000..452bbab9 --- /dev/null +++ b/YDotNet.Server.Clustering/RedisConnection.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using StackExchange.Redis; +using YDotNet.Server.Clustering.Internal; + +namespace YDotNet.Server.Clustering; + +public sealed class RedisConnection : IDisposable +{ + public Task Instance { get; } + + public RedisConnection(IOptions options, ILogger logger) + { + Instance = options.Value.ConnectAsync(new LoggerTextWriter(logger)); + } + + public void Dispose() + { + if (Instance.IsCompletedSuccessfully) + { + Instance.Result.Close(); + } + } +} diff --git a/YDotNet.Server.Clustering/RedisDocumentStorage.cs b/YDotNet.Server.Clustering/RedisDocumentStorage.cs new file mode 100644 index 00000000..fc77f59f --- /dev/null +++ b/YDotNet.Server.Clustering/RedisDocumentStorage.cs @@ -0,0 +1,60 @@ +using Microsoft.Extensions.Options; +using StackExchange.Redis; +using YDotNet.Server.Storage; + +namespace YDotNet.Server.Clustering; + +public sealed class RedisDocumentStorage : IDocumentStorage +{ + private readonly RedisDocumentStorageOptions redisOptions; + private IDatabase? database; + + public RedisDocumentStorage(IOptions redisOptions, RedisConnection redisConnection) + { + this.redisOptions = redisOptions.Value; + + _ = InitializeAsync(redisConnection); + } + + private async Task InitializeAsync(RedisConnection redisConnection) + { + // Use a single task, so that the ordering of registrations does not matter. + var connection = await redisConnection.Instance; + + database = connection.GetDatabase(redisOptions.Database); + } + + public async ValueTask GetDocAsync(string name, + CancellationToken ct = default) + { + if (database == null) + { + return null; + } + + var item = await database.StringGetAsync(Key(name)); + + if (item == RedisValue.Null) + { + return null; + } + + return item; + } + + public async ValueTask StoreDocAsync(string name, byte[] doc, + CancellationToken ct = default) + { + if (database == null) + { + return; + } + + await database.StringSetAsync(Key(name), doc, redisOptions.Expiration?.Invoke(name)); + } + + private string Key(string name) + { + return redisOptions.Prefix + name; + } +} diff --git a/YDotNet.Server.Clustering/RedisDocumentStorageOptions.cs b/YDotNet.Server.Clustering/RedisDocumentStorageOptions.cs new file mode 100644 index 00000000..6e52e820 --- /dev/null +++ b/YDotNet.Server.Clustering/RedisDocumentStorageOptions.cs @@ -0,0 +1,10 @@ +namespace YDotNet.Server.Clustering; + +public sealed class RedisDocumentStorageOptions +{ + public Func? Expiration { get; set; } + + public int Database { get; set; } + + public string Prefix { get; set; } = "YDotNetDocument_"; +} diff --git a/YDotNet.Server.Clustering/RedisOptions.cs b/YDotNet.Server.Clustering/RedisOptions.cs new file mode 100644 index 00000000..11e56ca2 --- /dev/null +++ b/YDotNet.Server.Clustering/RedisOptions.cs @@ -0,0 +1,25 @@ +using StackExchange.Redis; + +namespace YDotNet.Server.Clustering; + +public sealed class RedisOptions +{ + public ConfigurationOptions? Configuration { get; set; } + + public Func>? ConnectionFactory { get; set; } + + internal async Task ConnectAsync(TextWriter log) + { + if (ConnectionFactory != null) + { + return await ConnectionFactory(log); + } + + if (Configuration != null) + { + return await ConnectionMultiplexer.ConnectAsync(Configuration, log); + } + + throw new InvalidOperationException("Either configuration or connection factory must be set."); + } +} diff --git a/YDotNet.Server.Clustering/ServiceExtensions.cs b/YDotNet.Server.Clustering/ServiceExtensions.cs new file mode 100644 index 00000000..a3586aef --- /dev/null +++ b/YDotNet.Server.Clustering/ServiceExtensions.cs @@ -0,0 +1,32 @@ +using YDotNet.Server; +using YDotNet.Server.Clustering; +using YDotNet.Server.Storage; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class ServiceExtensions +{ + public static YDotnetRegistration AddRedis(this YDotnetRegistration registration, Action? configure = null) + { + registration.Services.Configure(configure ?? (x => { })); + registration.Services.AddSingleton(); + + return registration; + } + + public static YDotnetRegistration AddRedisClustering(this YDotnetRegistration registration, Action? configure = null) + { + registration.Services.Configure(configure ?? (x => { })); + registration.Services.AddSingleton(); + + return registration; + } + + public static YDotnetRegistration AddRedisStorage(this YDotnetRegistration registration, Action? configure = null) + { + registration.Services.Configure(configure ?? (x => { })); + registration.Services.AddSingleton(); + + return registration; + } +} diff --git a/YDotNet.Server.Clustering/YDotNet.Server.Clustering.csproj b/YDotNet.Server.Clustering/YDotNet.Server.Clustering.csproj new file mode 100644 index 00000000..a1d898ca --- /dev/null +++ b/YDotNet.Server.Clustering/YDotNet.Server.Clustering.csproj @@ -0,0 +1,19 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + + diff --git a/YDotNet.Server.Redis/RedisClusteringOptions.cs b/YDotNet.Server.Redis/RedisClusteringOptions.cs index 27a376e1..2f7377e7 100644 --- a/YDotNet.Server.Redis/RedisClusteringOptions.cs +++ b/YDotNet.Server.Redis/RedisClusteringOptions.cs @@ -5,10 +5,4 @@ namespace YDotNet.Server.Redis; public sealed class RedisClusteringOptions { public RedisChannel Channel { get; set; } = RedisChannel.Literal("YDotNet"); - - public TimeSpan DebounceTime { get; set; } = TimeSpan.FromMilliseconds(500); - - public int MaxBatchCount { get; set; } = 100; - - public int MaxBatchSize { get; set; } = 1024 * 1024; } diff --git a/YDotNet.Server.Redis/RedisPubSub.cs b/YDotNet.Server.Redis/RedisPubSub.cs new file mode 100644 index 00000000..8b2245cf --- /dev/null +++ b/YDotNet.Server.Redis/RedisPubSub.cs @@ -0,0 +1,65 @@ +using Microsoft.Extensions.Options; +using StackExchange.Redis; +using YDotNet.Server.Clustering; +using YDotNet.Server.Internal; + +namespace YDotNet.Server.Redis; + +public sealed class RedisPubSub : IPubSub +{ + private readonly List> handlers = new List>(); + private readonly RedisClusteringOptions redisOptions; + private ISubscriber? subscriber; + + public RedisPubSub(IOptions redisOptions, RedisConnection redisConnection) + { + this.redisOptions = redisOptions.Value; + + _ = InitializeAsync(redisConnection); + } + + public async Task InitializeAsync(RedisConnection redisConnection) + { + // Use a single task, so that the ordering of registrations does not matter. + var connection = await redisConnection.Instance; + + subscriber = connection.GetSubscriber(); + subscriber.Subscribe(redisOptions.Channel, async (_, value) => + { + foreach (var handler in handlers) + { + byte[]? payload = value; + + if (payload != null) + { + await handler(payload); + } + } + }); + } + + public void Dispose() + { + subscriber?.UnsubscribeAll(); + } + + public IDisposable Subscribe(Func handler) + { + handlers.Add(handler); + + return new DelegateDisposable(() => + { + handlers.Remove(handler); + }); + } + + public async Task PublishAsync(byte[] payload, CancellationToken ct) + { + if (subscriber == null) + { + return; + } + + await subscriber.PublishAsync(redisOptions.Channel, payload); + } +} diff --git a/YDotNet.Server.Redis/ServiceExtensions.cs b/YDotNet.Server.Redis/ServiceExtensions.cs index 2821ab32..eb7db176 100644 --- a/YDotNet.Server.Redis/ServiceExtensions.cs +++ b/YDotNet.Server.Redis/ServiceExtensions.cs @@ -17,7 +17,7 @@ public static YDotnetRegistration AddRedis(this YDotnetRegistration registration public static YDotnetRegistration AddRedisClustering(this YDotnetRegistration registration, Action? configure = null) { registration.Services.Configure(configure ?? (x => { })); - registration.Services.AddSingleton(); + registration.AddPubSub(); return registration; } diff --git a/YDotNet.Server.Redis/YDotNet.Server.Redis.csproj b/YDotNet.Server.Redis/YDotNet.Server.Redis.csproj index a1d898ca..36a8c68a 100644 --- a/YDotNet.Server.Redis/YDotNet.Server.Redis.csproj +++ b/YDotNet.Server.Redis/YDotNet.Server.Redis.csproj @@ -8,7 +8,6 @@ - diff --git a/YDotNet.Server/Clustering/ClusteringCallback.cs b/YDotNet.Server/Clustering/ClusteringCallback.cs new file mode 100644 index 00000000..52ac24c5 --- /dev/null +++ b/YDotNet.Server/Clustering/ClusteringCallback.cs @@ -0,0 +1,179 @@ +using Microsoft.Extensions.Options; +using ProtoBuf; +using YDotNet.Server.Clustering; +using YDotNet.Server.Clustering.Internal; + +namespace YDotNet.Server.Redis; + +public sealed class ClusteringCallback : IDocumentCallback, IDisposable +{ + private readonly Guid senderId = Guid.NewGuid(); + private readonly ClusteringOptions clusteringOptions; + private readonly PublishQueue publishQueue; + private readonly IPubSub publisher; + private readonly IDisposable subscription; + private IDocumentManager? documentManager; + + public ClusteringCallback(IOptions clusteringOptions, IPubSub publisher) + { + this.clusteringOptions = clusteringOptions.Value; + + this.publisher = publisher; + this.publishQueue = new PublishQueue( + this.clusteringOptions.MaxBatchCount, + this.clusteringOptions.MaxBatchSize, + (int)this.clusteringOptions.DebounceTime.TotalMilliseconds, + PublishBatchAsync); + + subscription = publisher.Subscribe(HandleMessage); + } + + public void Dispose() + { + subscription.Dispose(); + } + + public ValueTask OnInitializedAsync( + IDocumentManager manager) + { + // The initialize method is used to prevent circular dependencies between managers and hooks. + documentManager = manager; + return default; + } + + private async Task HandleMessage(byte[] payload) + { + if (documentManager == null) + { + return; + } + + var batch = Serializer.Deserialize(payload.AsSpan()); + + if (batch == null) + { + return; + } + + foreach (var message in batch) + { + if (message.SenderId == senderId) + { + continue; + } + + var context = new DocumentContext(message.DocumentName, message.ClientId) + { + Metadata = senderId + }; + + switch (message.Type) + { + case MessageType.ClientPinged: + await documentManager.PingAsync(context, message.ClientClock, message.ClientState); + break; + case MessageType.ClientDisconnected: + await documentManager.DisconnectAsync(context); + break; + case MessageType.Update when message.Data != null: + await documentManager.ApplyUpdateAsync(context, message.Data); + break; + case MessageType.SyncStep2 when message.Data != null: + await documentManager.ApplyUpdateAsync(context, message.Data); + break; + case MessageType.SyncStep1 when message.Data != null: + await SendSync2Async(context, message.Data); + break; + case MessageType.AwarenessRequested: + foreach (var (id, user) in await documentManager.GetAwarenessAsync(context)) + { + var userContext = context with { ClientId = id }; + + await SendAwarenessAsync(userContext, user.ClientState, user.ClientClock); + } + break; + } + } + } + + public ValueTask OnDocumentLoadedAsync(DocumentLoadEvent @event) + { + // Run these callbacks in another thread because it could cause deadlocks if it would interact with the same document. + _ = Task.Run(async () => + { + await SendAwarenessRequest(@event.Context); + await SendSync1Async(@event.Context); + }); + + return default; + } + + public async ValueTask OnAwarenessUpdatedAsync(ClientAwarenessEvent @event) + { + await SendAwarenessAsync(@event.Context, @event.ClientState, @event.ClientClock); + } + + public ValueTask OnClientDisconnectedAsync(ClientDisconnectedEvent @event) + { + var m = new Message { Type = MessageType.ClientDisconnected }; + + return EnqueueAsync(m, @event.Context); + } + + public ValueTask OnDocumentChangedAsync(DocumentChangedEvent @event) + { + var m = new Message { Type = MessageType.Update, Data = @event.Diff }; + + return EnqueueAsync(m, @event.Context); + } + + private ValueTask SendAwarenessAsync(DocumentContext context, string? state, long clock) + { + var m = new Message { Type = MessageType.ClientPinged, ClientState = state, ClientClock = clock }; + + return EnqueueAsync(m, context); + } + + private async ValueTask SendSync1Async(DocumentContext context) + { + var state = await documentManager!.GetStateVectorAsync(context); + + var m = new Message { Type = MessageType.SyncStep1, Data = state }; + + await EnqueueAsync(m, context); + } + + private async ValueTask SendSync2Async(DocumentContext context, byte[] stateVector) + { + var state = await documentManager!.GetUpdateAsync(context, stateVector); + + var m = new Message { Type = MessageType.SyncStep2, Data = state }; + + await EnqueueAsync(m, context); + } + + private ValueTask SendAwarenessRequest(DocumentContext context) + { + var m = new Message { Type = MessageType.AwarenessRequested }; + + return EnqueueAsync(m, context); + } + + private ValueTask EnqueueAsync(Message message, DocumentContext context) + { + message.ClientId = context.ClientId; + message.DocumentName = context.DocumentName; + message.SenderId = senderId; + + return publishQueue.EnqueueAsync(message, default); + } + + private async Task PublishBatchAsync(List batch, CancellationToken ct) + { + using var stream = new MemoryStream(); + + Serializer.Serialize(stream, batch); + + await publisher.PublishAsync(stream.ToArray(), default); + } +} diff --git a/YDotNet.Server/Clustering/ClusteringOptions.cs b/YDotNet.Server/Clustering/ClusteringOptions.cs new file mode 100644 index 00000000..83848393 --- /dev/null +++ b/YDotNet.Server/Clustering/ClusteringOptions.cs @@ -0,0 +1,10 @@ +namespace YDotNet.Server.Clustering; + +public sealed class ClusteringOptions +{ + public TimeSpan DebounceTime { get; set; } = TimeSpan.FromMilliseconds(500); + + public int MaxBatchCount { get; set; } = 100; + + public int MaxBatchSize { get; set; } = 1024 * 1024; +} diff --git a/YDotNet.Server/Clustering/IPubSub.cs b/YDotNet.Server/Clustering/IPubSub.cs new file mode 100644 index 00000000..f762d996 --- /dev/null +++ b/YDotNet.Server/Clustering/IPubSub.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YDotNet.Server.Clustering +{ + public interface IPubSub + { + IDisposable Subscribe(Func handler); + + Task PublishAsync(byte[] payload, + CancellationToken ct); + } +} diff --git a/YDotNet.Server/Clustering/Internal/PublishQueue.cs b/YDotNet.Server/Clustering/Internal/PublishQueue.cs new file mode 100644 index 00000000..379cb5c3 --- /dev/null +++ b/YDotNet.Server/Clustering/Internal/PublishQueue.cs @@ -0,0 +1,85 @@ +using System.Threading.Channels; + +namespace YDotNet.Server.Clustering.Internal; + +public interface ICanEstimateSize +{ + int EstimateSize(); +} + +public sealed class PublishQueue where T : ICanEstimateSize +{ + private readonly Channel inputChannel = Channel.CreateBounded(100); + private readonly Channel> outputChannel = Channel.CreateBounded>(2); + private readonly CancellationTokenSource cts = new(); + + public PublishQueue(int maxCount, int maxSize, int timeout, Func, CancellationToken, Task> handler) + { + Task.Run(async () => + { + var batchList = new List(maxCount); + var batchSize = 0; + + // Just a marker object to force sending out new batches. + var force = new object(); + + await using var timer = new Timer(_ => inputChannel.Writer.TryWrite(force)); + + async Task TrySendAsync() + { + if (batchList.Count > 0) + { + await outputChannel.Writer.WriteAsync(batchList, cts.Token); + + // Create a new batch, because the value is shared and might be processes by another concurrent task. + batchList = new List(); + batchSize = 0; + } + } + + // Exceptions usually that the process was stopped and the channel closed, therefore we do not catch them. + await foreach (var item in inputChannel.Reader.ReadAllAsync(cts.Token)) + { + if (ReferenceEquals(item, force)) + { + // Our item is the marker object from the timer. + await TrySendAsync(); + } + else if (item is T typed) + { + // The timeout restarts with the last event and should push events out if no further events are received. + timer.Change(timeout, Timeout.Infinite); + + batchList.Add(typed); + batchSize += typed.EstimateSize(); + + if (batchList.Count >= maxSize || batchSize >= maxSize) + { + await TrySendAsync(); + } + } + } + + await TrySendAsync(); + }, cts.Token).ContinueWith(x => outputChannel.Writer.TryComplete(x.Exception)); + + Task.Run(async () => + { + await foreach (var batch in outputChannel.Reader.ReadAllAsync(cts.Token)) + { + await handler(batch, cts.Token); + } + }, cts.Token); + } + + public ValueTask EnqueueAsync(T item, + CancellationToken ct) + { + return inputChannel.Writer.WriteAsync(item, ct); + } + + public void Dispose() + { + cts.Cancel(); + } +} diff --git a/YDotNet.Server/Clustering/Messages.cs b/YDotNet.Server/Clustering/Messages.cs new file mode 100644 index 00000000..59f3d709 --- /dev/null +++ b/YDotNet.Server/Clustering/Messages.cs @@ -0,0 +1,56 @@ +using ProtoBuf; +using YDotNet.Server.Clustering.Internal; + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + +namespace YDotNet.Server.Clustering; + +[ProtoContract] +public enum MessageType +{ + ClientPinged, + ClientDisconnected, + AwarenessRequested, + Update, + SyncStep1, + SyncStep2, +} + +[ProtoContract] +public sealed class Message : ICanEstimateSize +{ + private static readonly int GuidLength = Guid.Empty.ToString().Length; + + [ProtoMember(1)] + public MessageType Type { get; set; } + + [ProtoMember(1)] + public Guid SenderId { get; set; } + + [ProtoMember(2)] + public string DocumentName { get; set; } + + [ProtoMember(3)] + public long ClientId { get; set; } + + [ProtoMember(4)] + public long ClientClock { get; set; } + + [ProtoMember(5)] + public string? ClientState { get; set; } + + [ProtoMember(6)] + public byte[]? Data { get; set; } + + public int EstimateSize() + { + var size = + GuidLength + + sizeof(long) + + sizeof(long) + + DocumentName.Length + + Data?.Length ?? 0; + + return size; + } +} diff --git a/YDotNet.Server/Internal/DelegateDisposable.cs b/YDotNet.Server/Internal/DelegateDisposable.cs new file mode 100644 index 00000000..c216f56b --- /dev/null +++ b/YDotNet.Server/Internal/DelegateDisposable.cs @@ -0,0 +1,16 @@ +namespace YDotNet.Server.Internal; + +public sealed class DelegateDisposable : IDisposable +{ + private readonly Action callback; + + public DelegateDisposable(Action callback) + { + this.callback = callback; + } + + public void Dispose() + { + callback(); + } +} diff --git a/YDotNet.Server/ServiceExtensions.cs b/YDotNet.Server/ServiceExtensions.cs index 87b53512..0b235704 100644 --- a/YDotNet.Server/ServiceExtensions.cs +++ b/YDotNet.Server/ServiceExtensions.cs @@ -1,6 +1,8 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using YDotNet.Server; +using YDotNet.Server.Clustering; +using YDotNet.Server.Redis; using YDotNet.Server.Storage; namespace Microsoft.Extensions.DependencyInjection; @@ -21,11 +23,26 @@ public static YDotnetRegistration AddYDotNet(this IServiceCollection services) Services = services }; } + public static YDotnetRegistration AddCallback(this YDotnetRegistration registration) where T : class, IDocumentCallback { registration.Services.AddSingleton(); return registration; } + + public static YDotnetRegistration AddPubSub(this YDotnetRegistration registration) where T : class, IPubSub + { + registration.Services.AddSingleton(); + return registration; + } + + public static YDotnetRegistration AddClustering(this YDotnetRegistration registration, Action? configure) + { + registration.Services.Configure(configure ?? (x => { })); + + registration.AddCallback(); + return registration; + } } public sealed class YDotnetRegistration diff --git a/YDotNet.Server/YDotNet.Server.csproj b/YDotNet.Server/YDotNet.Server.csproj index e86c671c..d5fa42a0 100644 --- a/YDotNet.Server/YDotNet.Server.csproj +++ b/YDotNet.Server/YDotNet.Server.csproj @@ -15,6 +15,7 @@ + diff --git a/YDotNet.sln b/YDotNet.sln index 1aae9935..7094d0e4 100644 --- a/YDotNet.sln +++ b/YDotNet.sln @@ -33,7 +33,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YDotNet.Native.MacOS", "nat EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YDotNet.Native", "native\YDotNet.Native\YDotNet.Native.csproj", "{95D7DB70-2794-44FF-A196-BBD0FC3988A6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "T", "T\T.csproj", "{D9E4AD43-8BA9-4716-9052-E3D4D20C01B9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "T", "T\T.csproj", "{D9E4AD43-8BA9-4716-9052-E3D4D20C01B9}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -100,7 +100,7 @@ Global GlobalSection(NestedProjects) = preSolution {F85C9334-87AF-4FFC-A301-3A9F57ECD4AB} = {03858045-3849-41E2-ACE9-F10AAA4CCBCA} {13D76453-95FC-441D-9AC7-E41848C882C4} = {12A368ED-DC07-4A33-85AB-6330C11476ED} - {9E2E0844-F7BC-4CE5-A861-E6D719D1B3A0} = {95BDA0A4-331B-4357-B368-A784B66F19AD} + {9E2E0844-F7BC-4CE5-A861-E6D719D1B3A0} = {03858045-3849-41E2-ACE9-F10AAA4CCBCA} {5F9B181A-773F-40E9-ACAD-40AE7ABB1D8C} = {95BDA0A4-331B-4357-B368-A784B66F19AD} {D93CE5FA-9C04-420A-9635-81F4E4C092D0} = {95BDA0A4-331B-4357-B368-A784B66F19AD} {95D7DB70-2794-44FF-A196-BBD0FC3988A6} = {95BDA0A4-331B-4357-B368-A784B66F19AD} From 584204833fe6fc86e19ac6a9e88c309327a130b5 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 8 Oct 2023 18:18:55 +0200 Subject: [PATCH 082/186] Remove test project. --- YDotNet.sln | 7 ------- 1 file changed, 7 deletions(-) diff --git a/YDotNet.sln b/YDotNet.sln index 7094d0e4..14017646 100644 --- a/YDotNet.sln +++ b/YDotNet.sln @@ -33,8 +33,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YDotNet.Native.MacOS", "nat EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YDotNet.Native", "native\YDotNet.Native\YDotNet.Native.csproj", "{95D7DB70-2794-44FF-A196-BBD0FC3988A6}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "T", "T\T.csproj", "{D9E4AD43-8BA9-4716-9052-E3D4D20C01B9}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -89,10 +87,6 @@ Global {95D7DB70-2794-44FF-A196-BBD0FC3988A6}.Debug|Any CPU.Build.0 = Debug|Any CPU {95D7DB70-2794-44FF-A196-BBD0FC3988A6}.Release|Any CPU.ActiveCfg = Release|Any CPU {95D7DB70-2794-44FF-A196-BBD0FC3988A6}.Release|Any CPU.Build.0 = Release|Any CPU - {D9E4AD43-8BA9-4716-9052-E3D4D20C01B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D9E4AD43-8BA9-4716-9052-E3D4D20C01B9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D9E4AD43-8BA9-4716-9052-E3D4D20C01B9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D9E4AD43-8BA9-4716-9052-E3D4D20C01B9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -104,7 +98,6 @@ Global {5F9B181A-773F-40E9-ACAD-40AE7ABB1D8C} = {95BDA0A4-331B-4357-B368-A784B66F19AD} {D93CE5FA-9C04-420A-9635-81F4E4C092D0} = {95BDA0A4-331B-4357-B368-A784B66F19AD} {95D7DB70-2794-44FF-A196-BBD0FC3988A6} = {95BDA0A4-331B-4357-B368-A784B66F19AD} - {D9E4AD43-8BA9-4716-9052-E3D4D20C01B9} = {03858045-3849-41E2-ACE9-F10AAA4CCBCA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F7865083-8AF3-4562-88F2-95FD43368B57} From 013271c5b7ce9f0923c04e942b5fd4088e8b11ce Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 10 Oct 2023 19:20:38 +0200 Subject: [PATCH 083/186] Push test to nuget. --- .github/workflows/build.yml | 4 ++++ YDotNet/YDotNet.csproj | 1 + native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj | 1 + native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj | 1 + native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj | 1 + native/YDotNet.Native/YDotNet.Native.csproj | 1 + 6 files changed, 9 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 678bf64d..8ef935f1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -75,6 +75,10 @@ jobs: run: | dotnet pack -c Release + - name: Nuget publish + run: | + dotnet nuget push **/*.nupkg --source 'https://api.nuget.org/v3/index.json' --skip-duplicate -k ${{ secrets.nuget }} + - name: Upload artifacts uses: actions/upload-artifact@v3 with: diff --git a/YDotNet/YDotNet.csproj b/YDotNet/YDotNet.csproj index 2b78e0f7..fed2c6e6 100644 --- a/YDotNet/YDotNet.csproj +++ b/YDotNet/YDotNet.csproj @@ -5,6 +5,7 @@ enable enable true + YDotNetTest diff --git a/native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj b/native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj index ddeadac2..4edab500 100644 --- a/native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj +++ b/native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj @@ -4,6 +4,7 @@ net7.0 enable enable + YDotNetTest.Native diff --git a/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj b/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj index f44615da..0f4cbc59 100644 --- a/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj +++ b/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj @@ -4,6 +4,7 @@ net7.0 enable enable + YDotNetTest.Native diff --git a/native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj b/native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj index 31220e5e..d9ff03ff 100644 --- a/native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj +++ b/native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj @@ -4,6 +4,7 @@ net7.0 enable enable + YDotNetTest.Native diff --git a/native/YDotNet.Native/YDotNet.Native.csproj b/native/YDotNet.Native/YDotNet.Native.csproj index a51cb7c8..430b50ef 100644 --- a/native/YDotNet.Native/YDotNet.Native.csproj +++ b/native/YDotNet.Native/YDotNet.Native.csproj @@ -4,6 +4,7 @@ net7.0 enable enable + YDotNetTest.Native From 973d98105086af5293b13d4a2a8e517be4f20e9c Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 10 Oct 2023 19:22:46 +0200 Subject: [PATCH 084/186] Fix package id. --- native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj | 2 +- native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj | 2 +- native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj b/native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj index 4edab500..867e92db 100644 --- a/native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj +++ b/native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj @@ -4,7 +4,7 @@ net7.0 enable enable - YDotNetTest.Native + YDotNetTest.Native.Linux diff --git a/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj b/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj index 0f4cbc59..537670ee 100644 --- a/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj +++ b/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj @@ -4,7 +4,7 @@ net7.0 enable enable - YDotNetTest.Native + YDotNetTest.Native.MacOS diff --git a/native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj b/native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj index d9ff03ff..9294b4c7 100644 --- a/native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj +++ b/native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj @@ -4,7 +4,7 @@ net7.0 enable enable - YDotNetTest.Native + YDotNetTest.Native.Win32 From fd2e207bdf4414576633baa3a3a9f54d438e7523 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 10 Oct 2023 22:39:16 +0200 Subject: [PATCH 085/186] Test --- .editorconfig | 4 + Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs | 4 +- .../YDotNet.Tests.Unit/Arrays/IterateTests.cs | 2 +- .../YDotNet.Tests.Unit/Arrays/ObserveTests.cs | 10 +- .../Branches/ArrayObserveDeepTests.cs | 8 +- .../Branches/MapObserveDeepTests.cs | 4 +- .../Branches/ReadTransactionTests.cs | 6 +- .../Branches/WriteTransactionTests.cs | 12 +- .../Document/WriteTransactionTests.cs | 2 +- Tests/YDotNet.Tests.Unit/Maps/GetTests.cs | 39 +++-- Tests/YDotNet.Tests.Unit/Maps/ObserveTests.cs | 2 +- Tests/YDotNet.Tests.Unit/Texts/ChunksTests.cs | 8 +- Tests/YDotNet.Tests.Unit/Texts/FormatTests.cs | 6 +- .../Texts/InsertEmbedTests.cs | 24 +-- .../YDotNet.Tests.Unit/Texts/ObserveTests.cs | 10 +- .../XmlElements/FirstChildTests.cs | 8 +- .../XmlElements/GetTests.cs | 4 +- .../XmlElements/InsertAttributeTests.cs | 2 +- .../XmlElements/NextSiblingTests.cs | 10 +- .../XmlElements/ObserveTests.cs | 10 +- .../XmlElements/ParentTests.cs | 6 +- .../XmlElements/PreviousSiblingTests.cs | 10 +- .../XmlElements/RemoveAttributeTests.cs | 2 +- .../XmlElements/StringTests.cs | 8 +- .../XmlElements/TagTests.cs | 4 +- .../XmlElements/TreeWalkerTests.cs | 26 +-- .../XmlTexts/NextSiblingTests.cs | 10 +- .../XmlTexts/ObserveTests.cs | 2 +- .../XmlTexts/PreviousSiblingTests.cs | 10 +- .../YDotNet - Backup.Tests.Unit.csproj | 21 +++ .../YDotNet.Tests.Unit.csproj | 10 +- YDotNet/Document/Cells/Input.cs | 10 +- YDotNet/Document/Cells/Output.cs | 107 ++++++------ YDotNet/Document/Doc.cs | 158 ++++++++---------- YDotNet/Document/Options/DocOptions.cs | 4 +- YDotNet/Document/StickyIndexes/StickyIndex.cs | 8 +- YDotNet/Document/Transactions/Transaction.cs | 35 ++-- YDotNet/Document/Types/Arrays/Array.cs | 17 +- .../Document/Types/Arrays/ArrayEnumerator.cs | 20 +-- .../Types/Arrays/Events/ArrayEvent.cs | 52 +++--- YDotNet/Document/Types/Branches/Branch.cs | 48 +++--- YDotNet/Document/Types/Events/EventBranch.cs | 2 +- YDotNet/Document/Types/Events/EventChange.cs | 8 +- YDotNet/Document/Types/Events/EventChanges.cs | 40 ++--- YDotNet/Document/Types/Events/EventDeltas.cs | 41 ++--- YDotNet/Document/Types/Events/EventKeys.cs | 40 ++--- YDotNet/Document/Types/Events/EventPath.cs | 36 +--- .../Document/Types/Events/EventPathSegment.cs | 14 +- .../Document/Types/Maps/Events/MapEvent.cs | 63 +++---- YDotNet/Document/Types/Maps/Map.cs | 17 +- YDotNet/Document/Types/Maps/MapEntry.cs | 26 ++- YDotNet/Document/Types/Maps/MapEnumerator.cs | 22 +-- .../Document/Types/Texts/Events/TextEvent.cs | 62 +++---- YDotNet/Document/Types/Texts/Text.cs | 16 +- YDotNet/Document/Types/Texts/TextChunk.cs | 22 ++- YDotNet/Document/Types/Texts/TextChunks.cs | 35 +--- .../XmlElements/Events/XmlElementEvent.cs | 84 +++++----- .../Trees/XmlTreeWalkerEnumerator.cs | 17 +- .../Types/XmlElements/XmlAttribute.cs | 33 ++-- .../XmlElements/XmlAttributeEnumerator.cs | 18 +- .../Document/Types/XmlElements/XmlElement.cs | 45 ++--- .../Types/XmlTexts/Events/XmlTextEvent.cs | 63 +++---- YDotNet/Document/Types/XmlTexts/XmlText.cs | 20 ++- .../Document/UndoManagers/Events/UndoEvent.cs | 4 +- YDotNet/Document/UndoManagers/UndoManager.cs | 4 +- YDotNet/Infrastructure/Extensions.cs | 14 ++ YDotNet/Infrastructure/MemoryReader.cs | 3 +- YDotNet/Infrastructure/MemoryWriter.cs | 7 +- YDotNet/Infrastructure/ReferenceAccessor.cs | 87 ---------- YDotNet/Native/Cells/Inputs/InputNative.cs | 2 +- YDotNet/Native/Document/DocChannel.cs | 2 +- YDotNet/Native/Document/DocOptionsNative.cs | 10 +- .../Events/AfterTransactionEventNative.cs | 2 +- .../Document/Events/ClearEventNative.cs | 2 +- .../Document/Events/SubDocsEventNative.cs | 2 +- .../Document/Events/UpdateEventNative.cs | 4 +- .../Native/Document/State/DeleteSetNative.cs | 4 +- .../Native/Document/State/IdRangeNative.cs | 2 +- .../Document/State/IdRangeSequenceNative.cs | 2 +- .../Document/State/StateVectorNative.cs | 6 +- .../Native/Types/Events/EventChangeNative.cs | 20 +-- .../Native/Types/Events/EventDeltaNative.cs | 17 +- .../Types/Events/EventKeyChangeNative.cs | 10 +- YDotNet/Native/Types/Maps/MapChannel.cs | 1 - YDotNet/Native/Types/Maps/MapEntryNative.cs | 2 +- .../UndoManager/Events/UndoEventNative.cs | 3 +- YDotNet/ThrowHelper.cs | 24 +++ YDotNet/YDotNet.csproj | 18 +- YDotNet/YDotNetException.cs | 26 +++ YDotNet/stylecop.json => stylecop.json | 3 + 90 files changed, 828 insertions(+), 920 deletions(-) create mode 100644 Tests/YDotNet.Tests.Unit/YDotNet - Backup.Tests.Unit.csproj create mode 100644 YDotNet/Infrastructure/Extensions.cs delete mode 100644 YDotNet/Infrastructure/ReferenceAccessor.cs create mode 100644 YDotNet/ThrowHelper.cs create mode 100644 YDotNet/YDotNetException.cs rename YDotNet/stylecop.json => stylecop.json (87%) diff --git a/.editorconfig b/.editorconfig index 726c88db..2f6e641f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,8 +10,12 @@ tab_width = 4 # New line preferences end_of_line = lf + insert_final_newline = true +csharp_style_namespace_declarations = file_scoped + + # StyleCop rules (https://dotnetanalyzers.github.io/StyleCopAnalyzers/) # Special rules diff --git a/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs b/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs index ea5d690d..3433cba7 100644 --- a/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs +++ b/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs @@ -36,7 +36,7 @@ public void GetAtBeginning() // Assert Assert.That(output, Is.Not.Null); - Assert.That(output.Type, Is.EqualTo(OutputInputType.Bool)); + Assert.That(output.Type, Is.EqualTo(OutputType.Bool)); } [Test] @@ -52,7 +52,7 @@ public void GetAtMiddle() // Assert Assert.That(output, Is.Not.Null); - Assert.That(output.Type, Is.EqualTo(OutputInputType.Undefined)); + Assert.That(output.Type, Is.EqualTo(OutputType.Undefined)); } [Test] diff --git a/Tests/YDotNet.Tests.Unit/Arrays/IterateTests.cs b/Tests/YDotNet.Tests.Unit/Arrays/IterateTests.cs index 0842bd8f..01208976 100644 --- a/Tests/YDotNet.Tests.Unit/Arrays/IterateTests.cs +++ b/Tests/YDotNet.Tests.Unit/Arrays/IterateTests.cs @@ -75,6 +75,6 @@ public void IteratesOnMultiItem() Assert.That(values.Length, Is.EqualTo(expected: 3)); Assert.That(values[0].Long, Is.EqualTo(expected: 2469L)); Assert.That(values[1].Boolean, Is.False); - Assert.That(values[2].Type, Is.EqualTo(OutputInputType.Undefined)); + Assert.That(values[2].Type, Is.EqualTo(OutputType.Undefined)); } } diff --git a/Tests/YDotNet.Tests.Unit/Arrays/ObserveTests.cs b/Tests/YDotNet.Tests.Unit/Arrays/ObserveTests.cs index f20be74c..791542b3 100644 --- a/Tests/YDotNet.Tests.Unit/Arrays/ObserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/Arrays/ObserveTests.cs @@ -15,7 +15,7 @@ public void ObserveHasTarget() var doc = new Doc(); var array = doc.Array("array"); Array? target = null; - array.Observe(e => target = e.Target); + array.Observe(e => target = e.ResolveTarget()); // Act var transaction = doc.WriteTransaction(); @@ -44,7 +44,7 @@ public void ObserveHasDeltasWhenAdded() // Assert Assert.That(subscription.Id, Is.EqualTo(expected: 0L)); Assert.That(eventChanges, Is.Not.Null); - Assert.That(eventChanges.Length, Is.EqualTo(expected: 1)); + Assert.That(eventChanges.Count, Is.EqualTo(expected: 1)); Assert.That(eventChanges.First().Tag, Is.EqualTo(EventChangeTag.Add)); Assert.That(eventChanges.First().Length, Is.EqualTo(expected: 1)); Assert.That(eventChanges.First().Values.First().Long, Is.EqualTo(expected: 2469L)); @@ -78,7 +78,7 @@ public void ObserveHasDeltasWhenRemoved() // Assert Assert.That(subscription.Id, Is.EqualTo(expected: 0L)); Assert.That(eventChanges, Is.Not.Null); - Assert.That(eventChanges.Length, Is.EqualTo(expected: 1)); + Assert.That(eventChanges.Count, Is.EqualTo(expected: 1)); Assert.That(eventChanges.First().Tag, Is.EqualTo(EventChangeTag.Remove)); Assert.That(eventChanges.First().Length, Is.EqualTo(expected: 2)); Assert.That(eventChanges.First().Values, Is.Empty); @@ -112,11 +112,11 @@ public void ObserveHasDeltasWhenMoved() // Assert Assert.That(subscription.Id, Is.EqualTo(expected: 0L)); Assert.That(eventChanges, Is.Not.Null); - Assert.That(eventChanges.Length, Is.EqualTo(expected: 3)); + Assert.That(eventChanges.Count, Is.EqualTo(expected: 3)); Assert.That(eventChanges.ElementAt(index: 0).Tag, Is.EqualTo(EventChangeTag.Add)); Assert.That(eventChanges.ElementAt(index: 0).Length, Is.EqualTo(expected: 1)); - Assert.That(eventChanges.ElementAt(index: 0).Values.First().Type, Is.EqualTo(OutputInputType.Undefined)); + Assert.That(eventChanges.ElementAt(index: 0).Values.First().Type, Is.EqualTo(OutputType.Undefined)); Assert.That(eventChanges.ElementAt(index: 1).Tag, Is.EqualTo(EventChangeTag.Retain)); Assert.That(eventChanges.ElementAt(index: 1).Length, Is.EqualTo(expected: 2)); diff --git a/Tests/YDotNet.Tests.Unit/Branches/ArrayObserveDeepTests.cs b/Tests/YDotNet.Tests.Unit/Branches/ArrayObserveDeepTests.cs index 803bc89f..34991fa6 100644 --- a/Tests/YDotNet.Tests.Unit/Branches/ArrayObserveDeepTests.cs +++ b/Tests/YDotNet.Tests.Unit/Branches/ArrayObserveDeepTests.cs @@ -24,10 +24,10 @@ public void ObserveDeepHasPathWhenAdded() Input.Map(new Dictionary()) }); - var map2 = array1.Get(transaction, index: 3).Map; + var map2 = array1.Get(transaction, index: 3).ResolveMap(); map2.Insert(transaction, "array-3", Input.Array(Array.Empty())); - var array3 = map2.Get(transaction, "array-3").Array; + var array3 = map2.Get(transaction, "array-3").ResolveArray(); array3.InsertRange( transaction, index: 0, new[] { @@ -84,7 +84,7 @@ public void ObserveDeepHasPathWhenRemoved() Input.Boolean(value: false) }); - var array2 = array1.Get(transaction, index: 2).Array; + var array2 = array1.Get(transaction, index: 2).ResolveArray(); array2.InsertRange( transaction, index: 0, new[] { @@ -94,7 +94,7 @@ public void ObserveDeepHasPathWhenRemoved() Input.Array(Array.Empty()) }); - var array3 = array2.Get(transaction, index: 3).Array; + var array3 = array2.Get(transaction, index: 3).ResolveArray(); array3.InsertRange( transaction, index: 0, new[] { diff --git a/Tests/YDotNet.Tests.Unit/Branches/MapObserveDeepTests.cs b/Tests/YDotNet.Tests.Unit/Branches/MapObserveDeepTests.cs index cdecfbc5..3558a38f 100644 --- a/Tests/YDotNet.Tests.Unit/Branches/MapObserveDeepTests.cs +++ b/Tests/YDotNet.Tests.Unit/Branches/MapObserveDeepTests.cs @@ -200,10 +200,10 @@ private static (Doc, Map, Map, Map) ArrangeDoc() var transaction = doc.WriteTransaction(); map1.Insert(transaction, "map-2", Input.Map(new Dictionary())); - var map2 = map1.Get(transaction, "map-2").Map; + var map2 = map1.Get(transaction, "map-2").ResolveMap(); map2.Insert(transaction, "map-3", Input.Map(new Dictionary())); - var map3 = map2.Get(transaction, "map-3").Map; + var map3 = map2.Get(transaction, "map-3").ResolveMap(); transaction.Commit(); return (doc, map1, map2, map3); diff --git a/Tests/YDotNet.Tests.Unit/Branches/ReadTransactionTests.cs b/Tests/YDotNet.Tests.Unit/Branches/ReadTransactionTests.cs index 82c328b4..ec6ddd06 100644 --- a/Tests/YDotNet.Tests.Unit/Branches/ReadTransactionTests.cs +++ b/Tests/YDotNet.Tests.Unit/Branches/ReadTransactionTests.cs @@ -61,11 +61,10 @@ public void StartReadTransactionWhileWriteTransactionIsOpen() // Act var writeTransaction = branch.WriteTransaction(); - var readTransaction = branch.ReadTransaction(); // Assert + Assert.Throws(() => branch.ReadTransaction()); Assert.That(writeTransaction, Is.Not.Null); - Assert.That(readTransaction, Is.Null); } [Test] @@ -77,10 +76,9 @@ public void StartReadTransactionWhileDocumentWriteTransactionIsOpen() // Act var documentTransaction = doc.WriteTransaction(); - var branchTransaction = branch.ReadTransaction(); // Assert + Assert.Throws(() => branch.ReadTransaction()); Assert.That(documentTransaction, Is.Not.Null); - Assert.That(branchTransaction, Is.Null); } } diff --git a/Tests/YDotNet.Tests.Unit/Branches/WriteTransactionTests.cs b/Tests/YDotNet.Tests.Unit/Branches/WriteTransactionTests.cs index d0f7c0e4..5d7585b7 100644 --- a/Tests/YDotNet.Tests.Unit/Branches/WriteTransactionTests.cs +++ b/Tests/YDotNet.Tests.Unit/Branches/WriteTransactionTests.cs @@ -29,11 +29,10 @@ public void StartMultipleWriteTransactions() // Act var transaction1 = branch.WriteTransaction(); - var transaction2 = branch.WriteTransaction(); // Assert + Assert.Throws(() => branch.WriteTransaction()); Assert.That(transaction1, Is.Not.Null); - Assert.That(transaction2, Is.Null); } [Test] @@ -45,11 +44,10 @@ public void StartWriteTransactionWhileReadTransactionIsOpen() // Act var readTransaction = branch.ReadTransaction(); - var writeTransaction = branch.WriteTransaction(); // Assert + Assert.Throws(() => branch.WriteTransaction()); Assert.That(readTransaction, Is.Not.Null); - Assert.That(writeTransaction, Is.Null); } [Test] @@ -61,11 +59,10 @@ public void StartWriteTransactionWhileDocumentReadTransactionIsOpen() // Act var readTransaction = doc.ReadTransaction(); - var writeTransaction = branch.WriteTransaction(); // Assert + Assert.Throws(() => branch.WriteTransaction()); Assert.That(readTransaction, Is.Not.Null); - Assert.That(writeTransaction, Is.Null); } [Test] @@ -77,10 +74,9 @@ public void StartWriteTransactionWhileDocumentWriteTransactionIsOpen() // Act var transaction1 = doc.WriteTransaction(); - var transaction2 = branch.WriteTransaction(); // Assert + Assert.Throws(() => branch.WriteTransaction()); Assert.That(transaction1, Is.Not.Null); - Assert.That(transaction2, Is.Null); } } diff --git a/Tests/YDotNet.Tests.Unit/Document/WriteTransactionTests.cs b/Tests/YDotNet.Tests.Unit/Document/WriteTransactionTests.cs index bdbb6b6a..b4b0ed49 100644 --- a/Tests/YDotNet.Tests.Unit/Document/WriteTransactionTests.cs +++ b/Tests/YDotNet.Tests.Unit/Document/WriteTransactionTests.cs @@ -26,6 +26,6 @@ public void MultipleWriteTransactionsNotAllowed() // Assert Assert.That(doc.WriteTransaction(), Is.Not.Null); - Assert.That(doc.WriteTransaction(), Is.Null); + Assert.Throws(() => doc.WriteTransaction()); } } diff --git a/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs b/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs index 6ba1967e..222ff87c 100644 --- a/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs +++ b/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs @@ -1,4 +1,3 @@ -using Newtonsoft.Json.Linq; using NUnit.Framework; using YDotNet.Document; using YDotNet.Document.Cells; @@ -96,7 +95,7 @@ public void GetBytes() // Assert Assert.That(value1, Is.EqualTo(new byte[] { 2, 4, 6, 9 })); - Assert.That(value2.Type, Is.EqualTo(OutputInputType.Bool)); + Assert.That(value2.Type, Is.EqualTo(OutputType.Bool)); } [Test] @@ -122,7 +121,7 @@ public void GetCollection() Assert.That(value1.Length, Is.EqualTo(expected: 2)); Assert.That(value1[0].Long, Is.EqualTo(expected: 2469)); Assert.That(value1[1].Long, Is.EqualTo(expected: -420L)); - Assert.That(value2.Type, Is.EqualTo(OutputInputType.Bool)); + Assert.That(value2.Type, Is.EqualTo(OutputType.Bool)); } [Test] @@ -147,7 +146,7 @@ public void GetObject() Assert.That(value1.Keys.Count, Is.EqualTo(expected: 2)); Assert.That(value1["star-⭐"].Long, Is.EqualTo(expected: 2469)); Assert.That(value1["moon-🌕"].Long, Is.EqualTo(expected: -420L)); - Assert.That(value2.Type, Is.EqualTo(OutputInputType.Bool)); + Assert.That(value2.Type, Is.EqualTo(OutputType.Bool)); } [Test] @@ -166,9 +165,9 @@ public void GetNull() var value3 = map.Get(transaction, "value3"); // Assert - Assert.That(value1.Type, Is.EqualTo(OutputInputType.Null)); - Assert.That(value2.Type, Is.Not.EqualTo(OutputInputType.Null)); - Assert.That(value3.Type, Is.Not.EqualTo(OutputInputType.Null)); + Assert.That(value1.Type, Is.EqualTo(OutputType.Null)); + Assert.That(value2.Type, Is.Not.EqualTo(OutputType.Null)); + Assert.That(value3.Type, Is.Not.EqualTo(OutputType.Null)); } [Test] @@ -187,9 +186,9 @@ public void GetUndefined() var value3 = map.Get(transaction, "value3"); // Assert - Assert.That(value1.Type, Is.EqualTo(OutputInputType.Undefined)); - Assert.That(value2.Type, Is.Not.EqualTo(OutputInputType.Undefined)); - Assert.That(value3.Type, Is.Not.EqualTo(OutputInputType.Undefined)); + Assert.That(value1.Type, Is.EqualTo(OutputType.Undefined)); + Assert.That(value2.Type, Is.Not.EqualTo(OutputType.Undefined)); + Assert.That(value3.Type, Is.Not.EqualTo(OutputType.Undefined)); } [Test] @@ -202,13 +201,13 @@ public void GetText() ); // Act - var value1 = map.Get(transaction, "value1").Text; + var value1 = map.Get(transaction, "value1").ResolveText(); var value2 = map.Get(transaction, "value2"); // Assert Assert.That(value1, Is.Not.Null); Assert.That(value1.String(transaction), Is.EqualTo("Lucas")); - Assert.That(value2.Type, Is.EqualTo(OutputInputType.Bool)); + Assert.That(value2.Type, Is.EqualTo(OutputType.Bool)); } [Test] @@ -226,13 +225,13 @@ public void GetArray() ); // Act - var value1 = map.Get(transaction, "value1").Array; + var value1 = map.Get(transaction, "value1").ResolveArray(); var value2 = map.Get(transaction, "value2"); // Assert Assert.That(value1, Is.Not.Null); Assert.That(value1.Length, Is.EqualTo(expected: 2)); - Assert.That(value2.Type, Is.EqualTo(OutputInputType.Null)); + Assert.That(value2.Type, Is.EqualTo(OutputType.Null)); } [Test] @@ -250,7 +249,7 @@ public void GetMap() ); // Act - var value1 = map.Get(transaction, "value1").Map; + var value1 = map.Get(transaction, "value1").ResolveMap(); var value2 = map.Get(transaction, "value2"); // Assert @@ -258,7 +257,7 @@ public void GetMap() Assert.That(value1.Length(transaction), Is.EqualTo(expected: 2)); Assert.That(value1.Get(transaction, "value1-1").Long, Is.EqualTo(expected: 2469L)); Assert.That(value1.Get(transaction, "value1-2").Long, Is.EqualTo(expected: -420L)); - Assert.That(value2.Type, Is.EqualTo(OutputInputType.Bool)); + Assert.That(value2.Type, Is.EqualTo(OutputType.Bool)); } [Test] @@ -271,13 +270,13 @@ public void GetXmlElement() ); // Act - var value1 = map.Get(transaction, "value1").XmlElement; + var value1 = map.Get(transaction, "value1").ResolveXmlElement(); var value2 = map.Get(transaction, "value2"); // Assert Assert.That(value1, Is.Not.Null); Assert.That(value1.Tag, Is.EqualTo("person")); - Assert.That(value2.Type, Is.EqualTo(OutputInputType.Null)); + Assert.That(value2.Type, Is.EqualTo(OutputType.Null)); } [Test] @@ -290,13 +289,13 @@ public void GetXmlText() ); // Act - var value1 = map.Get(transaction, "value1").XmlText; + var value1 = map.Get(transaction, "value1").ResolveXmlText(); var value2 = map.Get(transaction, "value2"); // Assert Assert.That(value1, Is.Not.Null); Assert.That(value1.Length(transaction), Is.EqualTo(expected: 5)); - Assert.That(value2.Type, Is.EqualTo(OutputInputType.Null)); + Assert.That(value2.Type, Is.EqualTo(OutputType.Null)); } [Test] diff --git a/Tests/YDotNet.Tests.Unit/Maps/ObserveTests.cs b/Tests/YDotNet.Tests.Unit/Maps/ObserveTests.cs index bc8657f0..edbf8ef1 100644 --- a/Tests/YDotNet.Tests.Unit/Maps/ObserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/Maps/ObserveTests.cs @@ -15,7 +15,7 @@ public void ObserveHasTarget() var doc = new Doc(); var map = doc.Map("map"); Map? target = null; - map.Observe(e => target = e.Target); + map.Observe(e => target = e.ResolveTarget()); // Act var transaction = doc.WriteTransaction(); diff --git a/Tests/YDotNet.Tests.Unit/Texts/ChunksTests.cs b/Tests/YDotNet.Tests.Unit/Texts/ChunksTests.cs index 5db229e3..10787398 100644 --- a/Tests/YDotNet.Tests.Unit/Texts/ChunksTests.cs +++ b/Tests/YDotNet.Tests.Unit/Texts/ChunksTests.cs @@ -23,7 +23,7 @@ public void ChunkIsTheWholeText() var chunks = text.Chunks(transaction); // Assert - Assert.That(chunks.Length, Is.EqualTo(expected: 1)); + Assert.That(chunks.Count, Is.EqualTo(expected: 1)); Assert.That(chunks.First().Attributes, Is.Empty); } @@ -37,7 +37,7 @@ public void ChunksFormattedAtBeginning() var chunks = text.Chunks(transaction); // Assert - Assert.That(chunks.Length, Is.EqualTo(expected: 2)); + Assert.That(chunks.Count, Is.EqualTo(expected: 2)); var firstChunk = chunks.ElementAt(index: 0); var firstChunkAttribute = firstChunk.Attributes.First(); @@ -63,7 +63,7 @@ public void ChunksFormattedAtMiddle() var chunks = text.Chunks(transaction); // Assert - Assert.That(chunks.Length, Is.EqualTo(expected: 3)); + Assert.That(chunks.Count, Is.EqualTo(expected: 3)); var firstChunk = chunks.ElementAt(index: 0); @@ -94,7 +94,7 @@ public void ChunksFormattedAtEnding() var chunks = text.Chunks(transaction); // Assert - Assert.That(chunks.Length, Is.EqualTo(expected: 2)); + Assert.That(chunks.Count, Is.EqualTo(expected: 2)); var firstChunk = chunks.ElementAt(index: 0); diff --git a/Tests/YDotNet.Tests.Unit/Texts/FormatTests.cs b/Tests/YDotNet.Tests.Unit/Texts/FormatTests.cs index 7ff52ff0..3bd04350 100644 --- a/Tests/YDotNet.Tests.Unit/Texts/FormatTests.cs +++ b/Tests/YDotNet.Tests.Unit/Texts/FormatTests.cs @@ -25,7 +25,7 @@ public void FormatsTextAtBeginning() // Assert var chunks = text.Chunks(transaction); - Assert.That(chunks.Length, Is.EqualTo(expected: 2)); + Assert.That(chunks.Count, Is.EqualTo(expected: 2)); } [Test] @@ -46,7 +46,7 @@ public void FormatsTextAtMiddle() // Assert var chunks = text.Chunks(transaction); - Assert.That(chunks.Length, Is.EqualTo(expected: 3)); + Assert.That(chunks.Count, Is.EqualTo(expected: 3)); } [Test] @@ -67,7 +67,7 @@ public void FormatsTextAtEnding() // Assert var chunks = text.Chunks(transaction); - Assert.That(chunks.Length, Is.EqualTo(expected: 2)); + Assert.That(chunks.Count, Is.EqualTo(expected: 2)); } private (Doc, Text) ArrangeText() diff --git a/Tests/YDotNet.Tests.Unit/Texts/InsertEmbedTests.cs b/Tests/YDotNet.Tests.Unit/Texts/InsertEmbedTests.cs index 4c42cad4..4232fa58 100644 --- a/Tests/YDotNet.Tests.Unit/Texts/InsertEmbedTests.cs +++ b/Tests/YDotNet.Tests.Unit/Texts/InsertEmbedTests.cs @@ -20,7 +20,7 @@ public void InsertBooleanEmbed() // Assert var chunks = text.Chunks(transaction); - Assert.That(chunks.Length, Is.EqualTo(expected: 3)); + Assert.That(chunks.Count, Is.EqualTo(expected: 3)); Assert.That(chunks.ElementAt(index: 1).Data.Boolean, Is.True); } @@ -36,7 +36,7 @@ public void InsertDoubleEmbed() // Assert var chunks = text.Chunks(transaction); - Assert.That(chunks.Length, Is.EqualTo(expected: 3)); + Assert.That(chunks.Count, Is.EqualTo(expected: 3)); Assert.That(chunks.ElementAt(index: 1).Data.Double, Is.EqualTo(expected: 24.69)); } @@ -52,7 +52,7 @@ public void InsertLongEmbed() // Assert var chunks = text.Chunks(transaction); - Assert.That(chunks.Length, Is.EqualTo(expected: 3)); + Assert.That(chunks.Count, Is.EqualTo(expected: 3)); Assert.That(chunks.ElementAt(index: 1).Data.Long, Is.EqualTo(expected: 2469)); } @@ -68,7 +68,7 @@ public void InsertStringEmbed() // Assert var chunks = text.Chunks(transaction); - Assert.That(chunks.Length, Is.EqualTo(expected: 3)); + Assert.That(chunks.Count, Is.EqualTo(expected: 3)); Assert.That(chunks.ElementAt(index: 1).Data.String, Is.EqualTo("Between")); } @@ -84,7 +84,7 @@ public void InsertBytesEmbed() // Assert var chunks = text.Chunks(transaction); - Assert.That(chunks.Length, Is.EqualTo(expected: 3)); + Assert.That(chunks.Count, Is.EqualTo(expected: 3)); Assert.That(chunks.ElementAt(index: 1).Data.Bytes, Is.EqualTo(new byte[] { 2, 4, 6, 9 })); } @@ -106,7 +106,7 @@ public void InsertCollectionEmbed() // Assert var chunks = text.Chunks(transaction); - Assert.That(chunks.Length, Is.EqualTo(expected: 3)); + Assert.That(chunks.Count, Is.EqualTo(expected: 3)); Assert.That(chunks.ElementAt(index: 1).Data.Collection.Length, Is.EqualTo(expected: 2)); } @@ -128,7 +128,7 @@ public void InsertObjectEmbed() var chunks = text.Chunks(transaction); var secondChunk = chunks.ElementAt(index: 1).Data.Object; - Assert.That(chunks.Length, Is.EqualTo(expected: 3)); + Assert.That(chunks.Count, Is.EqualTo(expected: 3)); Assert.That(secondChunk.Count, Is.EqualTo(expected: 1)); Assert.That(secondChunk.Keys.First(), Is.EqualTo("italics")); Assert.That(secondChunk.Values.First().Boolean, Is.True); @@ -146,8 +146,8 @@ public void InsertNullEmbed() // Assert var chunks = text.Chunks(transaction); - Assert.That(chunks.Length, Is.EqualTo(expected: 3)); - Assert.That(chunks.ElementAt(index: 1).Data.Type, Is.EqualTo(OutputInputType.Null)); + Assert.That(chunks.Count, Is.EqualTo(expected: 3)); + Assert.That(chunks.ElementAt(index: 1).Data.Type, Is.EqualTo(OutputType.Null)); } [Test] @@ -162,8 +162,8 @@ public void InsertUndefinedEmbed() // Assert var chunks = text.Chunks(transaction); - Assert.That(chunks.Length, Is.EqualTo(expected: 3)); - Assert.That(chunks.ElementAt(index: 1).Data.Type, Is.EqualTo(OutputInputType.Undefined)); + Assert.That(chunks.Count, Is.EqualTo(expected: 3)); + Assert.That(chunks.ElementAt(index: 1).Data.Type, Is.EqualTo(OutputType.Undefined)); } [Test] @@ -182,7 +182,7 @@ public void InsertBooleanEmbedWithAttributes() // Assert var chunks = text.Chunks(transaction); - Assert.That(chunks.Length, Is.EqualTo(expected: 3)); + Assert.That(chunks.Count, Is.EqualTo(expected: 3)); Assert.That(chunks.ElementAt(index: 1).Data.Boolean, Is.True); Assert.That(chunks.ElementAt(index: 1).Attributes.Count(), Is.EqualTo(expected: 1)); Assert.That(chunks.ElementAt(index: 1).Attributes.First().Key, Is.EqualTo("bold")); diff --git a/Tests/YDotNet.Tests.Unit/Texts/ObserveTests.cs b/Tests/YDotNet.Tests.Unit/Texts/ObserveTests.cs index 21ab6290..11640afb 100644 --- a/Tests/YDotNet.Tests.Unit/Texts/ObserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/Texts/ObserveTests.cs @@ -15,7 +15,7 @@ public void ObserveHasTarget() var doc = new Doc(); var text = doc.Text("value"); Text? target = null; - text.Observe(e => target = e.Target); + text.Observe(e => target = e.ResolveTarget()); // Act var transaction = doc.WriteTransaction(); @@ -43,7 +43,7 @@ public void ObserveHasDeltasWhenAdded() // Assert Assert.That(eventDeltas, Is.Not.Null); - Assert.That(eventDeltas.Length, Is.EqualTo(expected: 1)); + Assert.That(eventDeltas.Count, Is.EqualTo(expected: 1)); Assert.That(eventDeltas.First().Tag, Is.EqualTo(EventDeltaTag.Add)); Assert.That(eventDeltas.First().Length, Is.EqualTo(expected: 1)); Assert.That(eventDeltas.First().Insert.String, Is.EqualTo("Lucas")); @@ -70,7 +70,7 @@ public void ObserveHasDeltasWhenRemoved() // Assert Assert.That(eventDeltas, Is.Not.Null); - Assert.That(eventDeltas.Length, Is.EqualTo(expected: 1)); + Assert.That(eventDeltas.Count, Is.EqualTo(expected: 1)); Assert.That(eventDeltas.First().Tag, Is.EqualTo(EventDeltaTag.Remove)); Assert.That(eventDeltas.First().Length, Is.EqualTo(expected: 2)); Assert.That(eventDeltas.First().Insert, Is.Null); @@ -102,7 +102,7 @@ public void ObserveHasDeltasWhenFormatted() // Assert Assert.That(eventDeltas, Is.Not.Null); - Assert.That(eventDeltas.Length, Is.EqualTo(expected: 1)); + Assert.That(eventDeltas.Count, Is.EqualTo(expected: 1)); Assert.That(eventDeltas.First().Tag, Is.EqualTo(EventDeltaTag.Retain)); Assert.That(eventDeltas.First().Length, Is.EqualTo(expected: 2)); Assert.That(eventDeltas.First().Insert, Is.Null); @@ -130,7 +130,7 @@ public void ObserveHasDeltasWhenAddedWithAttributes() // Assert Assert.That(eventDeltas, Is.Not.Null); - Assert.That(eventDeltas.Length, Is.EqualTo(expected: 1)); + Assert.That(eventDeltas.Count, Is.EqualTo(expected: 1)); Assert.That(eventDeltas.First().Tag, Is.EqualTo(EventDeltaTag.Add)); Assert.That(eventDeltas.First().Length, Is.EqualTo(expected: 1)); Assert.That(eventDeltas.First().Insert.String, Is.EqualTo("Lucas")); diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/FirstChildTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/FirstChildTests.cs index 176b96bd..36fdaffc 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/FirstChildTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/FirstChildTests.cs @@ -40,7 +40,7 @@ public void FirstChildOfRootFilledNodeIsCorrect() transaction.Commit(); // Assert - Assert.That(childXmlElement.XmlText, Is.Not.Null); + Assert.That(childXmlElement.ResolveXmlText(), Is.Not.Null); } [Test] @@ -52,7 +52,7 @@ public void FirstChildOfNestedEmptyNodeIsNull() var transaction = doc.WriteTransaction(); xmlElement.InsertElement(transaction, index: 0, "color"); - var childXmlElement = xmlElement.Get(transaction, index: 0).XmlElement; + var childXmlElement = xmlElement.Get(transaction, index: 0).ResolveXmlElement(); transaction.Commit(); // Act @@ -73,7 +73,7 @@ public void FirstChildOfNestedFilledNodeIsCorrect() var transaction = doc.WriteTransaction(); xmlElement.InsertElement(transaction, index: 0, "color"); - var childXmlElement = xmlElement.Get(transaction, index: 0).XmlElement; + var childXmlElement = xmlElement.Get(transaction, index: 0).ResolveXmlElement(); childXmlElement.InsertElement(transaction, index: 0, "alpha"); childXmlElement.InsertElement(transaction, index: 0, "hex"); transaction.Commit(); @@ -84,6 +84,6 @@ public void FirstChildOfNestedFilledNodeIsCorrect() transaction.Commit(); // Assert - Assert.That(grandChildXmlElement.XmlElement, Is.Not.Null); + Assert.That(grandChildXmlElement.ResolveXmlElement(), Is.Not.Null); } } diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/GetTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/GetTests.cs index c0266885..881085fa 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/GetTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/GetTests.cs @@ -34,7 +34,7 @@ public void GetXmlText() transaction.Commit(); // Assert - Assert.That(output.XmlText, Is.Not.Null); + Assert.That(output.ResolveXmlText(), Is.Not.Null); } [Test] @@ -49,7 +49,7 @@ public void GetXmlElement() transaction.Commit(); // Assert - Assert.That(output.XmlElement, Is.Not.Null); + Assert.That(output.ResolveXmlElement(), Is.Not.Null); } private (Doc, XmlElement) ArrangeDoc() diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/InsertAttributeTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/InsertAttributeTests.cs index 44507b5f..c83b0b38 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/InsertAttributeTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/InsertAttributeTests.cs @@ -127,7 +127,7 @@ private static (Doc, XmlElement) ArrangeDoc() var transaction = doc.WriteTransaction(); array.InsertRange(transaction, index: 0, new[] { Input.XmlElement("link") }); - var xmlElement = array.Get(transaction, index: 0).XmlElement; + var xmlElement = array.Get(transaction, index: 0).ResolveXmlElement(); transaction.Commit(); return (doc, xmlElement); diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/NextSiblingTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/NextSiblingTests.cs index a0a9d9b5..fc6cd016 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/NextSiblingTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/NextSiblingTests.cs @@ -14,12 +14,12 @@ public void GetsNextSiblingAtBeginning() // Act var transaction = doc.ReadTransaction(); - var target = xmlElement.Get(transaction, index: 0).XmlElement; + var target = xmlElement.Get(transaction, index: 0).ResolveXmlElement(); var sibling = target.NextSibling(transaction); transaction.Commit(); // Assert - Assert.That(sibling.XmlText, Is.Not.Null); + Assert.That(sibling.ResolveXmlText(), Is.Not.Null); } [Test] @@ -30,12 +30,12 @@ public void GetsNextSiblingAtMiddle() // Act var transaction = doc.ReadTransaction(); - var target = xmlElement.Get(transaction, index: 2).XmlElement; + var target = xmlElement.Get(transaction, index: 2).ResolveXmlElement(); var sibling = target.NextSibling(transaction); transaction.Commit(); // Assert - Assert.That(sibling.XmlText, Is.Not.Null); + Assert.That(sibling.ResolveXmlText(), Is.Not.Null); } [Test] @@ -46,7 +46,7 @@ public void GetsNextSiblingAtEnding() // Act var transaction = doc.ReadTransaction(); - var target = xmlElement.Get(transaction, index: 4).XmlElement; + var target = xmlElement.Get(transaction, index: 4).ResolveXmlElement(); var sibling = target.NextSibling(transaction); transaction.Commit(); diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/ObserveTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/ObserveTests.cs index c3130054..1f4ed60e 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/ObserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/ObserveTests.cs @@ -15,7 +15,7 @@ public void ObserveHasTarget() var xmlElement = doc.XmlElement("xml-element"); XmlElement? target = null; - xmlElement.Observe(e => target = e.Target); + xmlElement.Observe(e => target = e.ResolveTarget()); // Act var transaction = doc.WriteTransaction(); @@ -49,10 +49,10 @@ public void ObserveHasDeltaWhenAddedXmlElementsAndTexts() Assert.That(eventChanges.Count(), Is.EqualTo(expected: 1)); Assert.That(eventChanges.First().Tag, Is.EqualTo(EventChangeTag.Add)); Assert.That(eventChanges.First().Length, Is.EqualTo(expected: 3)); - Assert.That(eventChanges.First().Values.ElementAt(index: 0).XmlText, Is.Not.Null); - Assert.That(eventChanges.First().Values.ElementAt(index: 1).XmlElement, Is.Not.Null); - Assert.That(eventChanges.First().Values.ElementAt(index: 1).XmlElement.Tag, Is.EqualTo("color")); - Assert.That(eventChanges.First().Values.ElementAt(index: 2).XmlText, Is.Not.Null); + Assert.That(eventChanges.First().Values.ElementAt(index: 0).ResolveXmlText(), Is.Not.Null); + Assert.That(eventChanges.First().Values.ElementAt(index: 1).ResolveXmlElement(), Is.Not.Null); + Assert.That(eventChanges.First().Values.ElementAt(index: 1).ResolveXmlElement().Tag, Is.EqualTo("color")); + Assert.That(eventChanges.First().Values.ElementAt(index: 2).ResolveXmlText(), Is.Not.Null); } [Test] diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/ParentTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/ParentTests.cs index 052d9fb9..9c85c0bb 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/ParentTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/ParentTests.cs @@ -35,7 +35,7 @@ public void ParentOfNodeInsideArrayReturnsNull() // Act transaction = doc.ReadTransaction(); - var xmlElement = array.Get(transaction, index: 0).XmlElement; + var xmlElement = array.Get(transaction, index: 0).ResolveXmlElement(); var parentXmlElement = xmlElement.Parent(transaction); transaction.Commit(); @@ -56,7 +56,7 @@ public void ParentOfNodeInsideMapReturnsNull() // Act transaction = doc.ReadTransaction(); - var xmlElement = map.Get(transaction, "attributes").XmlElement; + var xmlElement = map.Get(transaction, "attributes").ResolveXmlElement(); var parentXmlElement = xmlElement.Parent(transaction); transaction.Commit(); @@ -78,7 +78,7 @@ public void ParentOfNestedNodeAtFirstLevelReturnsCorrectParent() // Act transaction = doc.ReadTransaction(); - var childXmlElement = xmlElement.Get(transaction, index: 0).XmlElement; + var childXmlElement = xmlElement.Get(transaction, index: 0).ResolveXmlElement(); var parentXmlElement = childXmlElement.Parent(transaction); var childLength = parentXmlElement.ChildLength(transaction); diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/PreviousSiblingTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/PreviousSiblingTests.cs index 837348cf..944c65b4 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/PreviousSiblingTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/PreviousSiblingTests.cs @@ -14,7 +14,7 @@ public void GetsPreviousSiblingAtBeginning() // Act var transaction = doc.ReadTransaction(); - var target = xmlElement.Get(transaction, index: 0).XmlElement; + var target = xmlElement.Get(transaction, index: 0).ResolveXmlElement(); var sibling = target.PreviousSibling(transaction); transaction.Commit(); @@ -30,12 +30,12 @@ public void GetsPreviousSiblingAtMiddle() // Act var transaction = doc.ReadTransaction(); - var target = xmlElement.Get(transaction, index: 2).XmlElement; + var target = xmlElement.Get(transaction, index: 2).ResolveXmlElement(); var sibling = target.PreviousSibling(transaction); transaction.Commit(); // Assert - Assert.That(sibling.XmlText, Is.Not.Null); + Assert.That(sibling.ResolveXmlText(), Is.Not.Null); } [Test] @@ -46,12 +46,12 @@ public void GetsPreviousSiblingAtEnding() // Act var transaction = doc.ReadTransaction(); - var target = xmlElement.Get(transaction, index: 4).XmlElement; + var target = xmlElement.Get(transaction, index: 4).ResolveXmlElement(); var sibling = target.PreviousSibling(transaction); transaction.Commit(); // Assert - Assert.That(sibling.XmlText, Is.Not.Null); + Assert.That(sibling.ResolveXmlText(), Is.Not.Null); } private (Doc, XmlElement) ArrangeDoc() diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/RemoveAttributeTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/RemoveAttributeTests.cs index 22becd8f..f916eb24 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/RemoveAttributeTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/RemoveAttributeTests.cs @@ -66,7 +66,7 @@ public void RemoveAttributeThatDoesNotExistWorks() var transaction = doc.WriteTransaction(); array.InsertRange(transaction, index: 0, new[] { Input.XmlElement("link") }); - var xmlElement = array.Get(transaction, index: 0).XmlElement; + var xmlElement = array.Get(transaction, index: 0).ResolveXmlElement(); xmlElement.InsertAttribute(transaction, string.Empty, string.Empty); xmlElement.InsertAttribute(transaction, "as", "stylesheet"); diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/StringTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/StringTests.cs index dde4fed6..405ac861 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/StringTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/StringTests.cs @@ -50,14 +50,14 @@ public void NodeNestedOnMapHasCorrectString() var transaction = doc.WriteTransaction(); map.Insert(transaction, "xml-element", Input.XmlElement("color")); - var xmlElement = map.Get(transaction, "xml-element").XmlElement; + var xmlElement = map.Get(transaction, "xml-element").ResolveXmlElement(); var xmlText = xmlElement.InsertText(transaction, index: 0); xmlText.Insert(transaction, index: 0, "blue"); transaction.Commit(); // Act transaction = doc.ReadTransaction(); - xmlElement = map.Get(transaction, "xml-element").XmlElement; + xmlElement = map.Get(transaction, "xml-element").ResolveXmlElement(); var text = xmlElement.String(transaction); transaction.Commit(); @@ -74,14 +74,14 @@ public void NodeNestedOnArrayHasCorrectString() var transaction = doc.WriteTransaction(); array.InsertRange(transaction, index: 0, new[] { Input.XmlElement("color") }); - var xmlElement = array.Get(transaction, index: 0).XmlElement; + var xmlElement = array.Get(transaction, index: 0).ResolveXmlElement(); var xmlText = xmlElement.InsertText(transaction, index: 0); xmlText.Insert(transaction, index: 0, "purple"); transaction.Commit(); // Act transaction = doc.ReadTransaction(); - var text = array.Get(transaction, index: 0).XmlElement.String(transaction); + var text = array.Get(transaction, index: 0).ResolveXmlElement().String(transaction); transaction.Commit(); // Assert diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/TagTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/TagTests.cs index 69687895..27a622f4 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/TagTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/TagTests.cs @@ -50,7 +50,7 @@ public void NodeNestedOnMapHasCorrectTag() // Act transaction = doc.ReadTransaction(); - var tag = map.Get(transaction, "xml-element").XmlElement.Tag; + var tag = map.Get(transaction, "xml-element").ResolveXmlElement().Tag; transaction.Commit(); // Assert @@ -70,7 +70,7 @@ public void NodeNestedOnArrayHasCorrectTag() // Act transaction = doc.ReadTransaction(); - var tag = array.Get(transaction, index: 0).XmlElement.Tag; + var tag = array.Get(transaction, index: 0).ResolveXmlElement().Tag; transaction.Commit(); // Assert diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/TreeWalkerTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/TreeWalkerTests.cs index 2d40c025..6ecfadc5 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/TreeWalkerTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/TreeWalkerTests.cs @@ -45,10 +45,10 @@ public void WalksOnTreeWithSingleLevelOfDepth() Assert.That(xmlTreeWalker, Is.Not.Null); Assert.That(xmlNodes.Length, Is.EqualTo(expected: 3)); - Assert.That(xmlNodes.ElementAt(index: 0).XmlText, Is.Not.Null); - Assert.That(xmlNodes.ElementAt(index: 1).XmlElement, Is.Not.Null); - Assert.That(xmlNodes.ElementAt(index: 1).XmlElement.Tag, Is.EqualTo("color")); - Assert.That(xmlNodes.ElementAt(index: 2).XmlText, Is.Not.Null); + Assert.That(xmlNodes.ElementAt(index: 0).ResolveXmlText(), Is.Not.Null); + Assert.That(xmlNodes.ElementAt(index: 1).ResolveXmlElement(), Is.Not.Null); + Assert.That(xmlNodes.ElementAt(index: 1).ResolveXmlElement().Tag, Is.EqualTo("color")); + Assert.That(xmlNodes.ElementAt(index: 2).ResolveXmlText(), Is.Not.Null); } [Test] @@ -61,7 +61,7 @@ public void WalksOnTreeWithMultipleLevelsOfDepth() var transaction = doc.WriteTransaction(); xmlElement.InsertText(transaction, index: 0); xmlElement.InsertElement(transaction, index: 1, "color"); - var childXmlElement = xmlElement.Get(transaction, index: 1).XmlElement; + var childXmlElement = xmlElement.Get(transaction, index: 1).ResolveXmlElement(); childXmlElement.InsertElement(transaction, index: 0, "alpha"); childXmlElement.InsertElement(transaction, index: 1, "hex"); childXmlElement.InsertText(transaction, index: 2); @@ -77,13 +77,13 @@ public void WalksOnTreeWithMultipleLevelsOfDepth() Assert.That(xmlTreeWalker, Is.Not.Null); Assert.That(xmlNodes.Length, Is.EqualTo(expected: 5)); - Assert.That(xmlNodes.ElementAt(index: 0).XmlText, Is.Not.Null); - Assert.That(xmlNodes.ElementAt(index: 1).XmlElement, Is.Not.Null); - Assert.That(xmlNodes.ElementAt(index: 1).XmlElement.Tag, Is.EqualTo("color")); - Assert.That(xmlNodes.ElementAt(index: 2).XmlElement, Is.Not.Null); - Assert.That(xmlNodes.ElementAt(index: 2).XmlElement.Tag, Is.EqualTo("alpha")); - Assert.That(xmlNodes.ElementAt(index: 3).XmlElement, Is.Not.Null); - Assert.That(xmlNodes.ElementAt(index: 3).XmlElement.Tag, Is.EqualTo("hex")); - Assert.That(xmlNodes.ElementAt(index: 4).XmlText, Is.Not.Null); + Assert.That(xmlNodes.ElementAt(index: 0).ResolveXmlText(), Is.Not.Null); + Assert.That(xmlNodes.ElementAt(index: 1).ResolveXmlElement(), Is.Not.Null); + Assert.That(xmlNodes.ElementAt(index: 1).ResolveXmlElement().Tag, Is.EqualTo("color")); + Assert.That(xmlNodes.ElementAt(index: 2).ResolveXmlElement(), Is.Not.Null); + Assert.That(xmlNodes.ElementAt(index: 2).ResolveXmlElement().Tag, Is.EqualTo("alpha")); + Assert.That(xmlNodes.ElementAt(index: 3).ResolveXmlElement(), Is.Not.Null); + Assert.That(xmlNodes.ElementAt(index: 3).ResolveXmlElement().Tag, Is.EqualTo("hex")); + Assert.That(xmlNodes.ElementAt(index: 4).ResolveXmlText(), Is.Not.Null); } } diff --git a/Tests/YDotNet.Tests.Unit/XmlTexts/NextSiblingTests.cs b/Tests/YDotNet.Tests.Unit/XmlTexts/NextSiblingTests.cs index 46e52bec..ce650022 100644 --- a/Tests/YDotNet.Tests.Unit/XmlTexts/NextSiblingTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlTexts/NextSiblingTests.cs @@ -14,12 +14,12 @@ public void GetsNextSiblingAtBeginning() // Act var transaction = doc.ReadTransaction(); - var target = xmlElement.Get(transaction, index: 0).XmlText; + var target = xmlElement.Get(transaction, index: 0).ResolveXmlText(); var sibling = target.NextSibling(transaction); transaction.Commit(); // Assert - Assert.That(sibling.XmlElement, Is.Not.Null); + Assert.That(sibling.ResolveXmlElement(), Is.Not.Null); } [Test] @@ -30,12 +30,12 @@ public void GetsNextSiblingAtMiddle() // Act var transaction = doc.ReadTransaction(); - var target = xmlElement.Get(transaction, index: 2).XmlText; + var target = xmlElement.Get(transaction, index: 2).ResolveXmlText(); var sibling = target.NextSibling(transaction); transaction.Commit(); // Assert - Assert.That(sibling.XmlElement, Is.Not.Null); + Assert.That(sibling.ResolveXmlElement(), Is.Not.Null); } [Test] @@ -46,7 +46,7 @@ public void GetsNextSiblingAtEnding() // Act var transaction = doc.ReadTransaction(); - var target = xmlElement.Get(transaction, index: 4).XmlText; + var target = xmlElement.Get(transaction, index: 4).ResolveXmlText(); var sibling = target.NextSibling(transaction); transaction.Commit(); diff --git a/Tests/YDotNet.Tests.Unit/XmlTexts/ObserveTests.cs b/Tests/YDotNet.Tests.Unit/XmlTexts/ObserveTests.cs index df1484ea..89ca21e9 100644 --- a/Tests/YDotNet.Tests.Unit/XmlTexts/ObserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlTexts/ObserveTests.cs @@ -16,7 +16,7 @@ public void ObserveHasTarget() var xmlText = doc.XmlText("xml-text"); XmlText? target = null; - var subscription = xmlText.Observe(e => target = e.Target); + var subscription = xmlText.Observe(e => target = e.ResolveTarget()); // Act var transaction = doc.WriteTransaction(); diff --git a/Tests/YDotNet.Tests.Unit/XmlTexts/PreviousSiblingTests.cs b/Tests/YDotNet.Tests.Unit/XmlTexts/PreviousSiblingTests.cs index 92e37ee5..5e47bba0 100644 --- a/Tests/YDotNet.Tests.Unit/XmlTexts/PreviousSiblingTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlTexts/PreviousSiblingTests.cs @@ -14,7 +14,7 @@ public void GetsPreviousSiblingAtBeginning() // Act var transaction = doc.ReadTransaction(); - var target = xmlElement.Get(transaction, index: 0).XmlText; + var target = xmlElement.Get(transaction, index: 0).ResolveXmlText(); var sibling = target.PreviousSibling(transaction); transaction.Commit(); @@ -30,12 +30,12 @@ public void GetsPreviousSiblingAtMiddle() // Act var transaction = doc.ReadTransaction(); - var target = xmlElement.Get(transaction, index: 2).XmlText; + var target = xmlElement.Get(transaction, index: 2).ResolveXmlText(); var sibling = target.PreviousSibling(transaction); transaction.Commit(); // Assert - Assert.That(sibling.XmlElement, Is.Not.Null); + Assert.That(sibling.ResolveXmlElement(), Is.Not.Null); } [Test] @@ -46,12 +46,12 @@ public void GetsPreviousSiblingAtEnding() // Act var transaction = doc.ReadTransaction(); - var target = xmlElement.Get(transaction, index: 4).XmlText; + var target = xmlElement.Get(transaction, index: 4).ResolveXmlText(); var sibling = target.PreviousSibling(transaction); transaction.Commit(); // Assert - Assert.That(sibling.XmlElement, Is.Not.Null); + Assert.That(sibling.ResolveXmlElement(), Is.Not.Null); } private (Doc, XmlElement) ArrangeDoc() diff --git a/Tests/YDotNet.Tests.Unit/YDotNet - Backup.Tests.Unit.csproj b/Tests/YDotNet.Tests.Unit/YDotNet - Backup.Tests.Unit.csproj new file mode 100644 index 00000000..0fada3f7 --- /dev/null +++ b/Tests/YDotNet.Tests.Unit/YDotNet - Backup.Tests.Unit.csproj @@ -0,0 +1,21 @@ + + + + net7.0 + enable + false + + + + + + + + + + + + + + + diff --git a/Tests/YDotNet.Tests.Unit/YDotNet.Tests.Unit.csproj b/Tests/YDotNet.Tests.Unit/YDotNet.Tests.Unit.csproj index 959e0446..b97b142b 100644 --- a/Tests/YDotNet.Tests.Unit/YDotNet.Tests.Unit.csproj +++ b/Tests/YDotNet.Tests.Unit/YDotNet.Tests.Unit.csproj @@ -3,11 +3,17 @@ net7.0 enable - enable - false + + 1701;1702;8632 + + + + 1701;1702;8632 + + diff --git a/YDotNet/Document/Cells/Input.cs b/YDotNet/Document/Cells/Input.cs index d64cc30a..b6804051 100644 --- a/YDotNet/Document/Cells/Input.cs +++ b/YDotNet/Document/Cells/Input.cs @@ -75,7 +75,7 @@ public static Input Long(long value) /// The cell that represents the provided value. public static Input Bytes(byte[] value) { - return new InputEmpty(InputChannel.Bytes(value, (uint) value.Length)); + return new InputEmpty(InputChannel.Bytes(value, (uint)value.Length)); } /// @@ -88,7 +88,7 @@ public static Input Collection(Input[] value) var inputs = value.Select(x => x.InputNative).ToArray(); var pointer = MemoryWriter.WriteStructArray(inputs); - return new InputPointer(InputChannel.Collection(pointer, (uint) value.Length), pointer); + return new InputPointer(InputChannel.Collection(pointer, (uint)value.Length), pointer); } /// @@ -102,7 +102,7 @@ public static Input Object(IDictionary value) var values = MemoryWriter.WriteStructArray(value.Values.Select(x => x.InputNative).ToArray()); return new InputPointerArray( - InputChannel.Object(keys.Head, values, (uint) value.Count), + InputChannel.Object(keys.Head, values, (uint)value.Count), keys.Pointers.Concat(new[] { keys.Head, values }).ToArray()); } @@ -134,7 +134,7 @@ public static Input Array(Input[] value) var inputs = value.Select(x => x.InputNative).ToArray(); var pointer = MemoryWriter.WriteStructArray(inputs); - return new InputPointer(InputChannel.Array(pointer, (uint) value.Length), pointer); + return new InputPointer(InputChannel.Array(pointer, (uint)value.Length), pointer); } /// @@ -148,7 +148,7 @@ public static Input Map(IDictionary value) var values = MemoryWriter.WriteStructArray(value.Values.Select(x => x.InputNative).ToArray()); return new InputPointerArray( - InputChannel.Map(keys.Head, values, (uint) value.Count), + InputChannel.Map(keys.Head, values, (uint)value.Count), keys.Pointers.Concat(new[] { keys.Head, values }).ToArray()); } diff --git a/YDotNet/Document/Cells/Output.cs b/YDotNet/Document/Cells/Output.cs index 41e374ea..17ca9551 100644 --- a/YDotNet/Document/Cells/Output.cs +++ b/YDotNet/Document/Cells/Output.cs @@ -26,12 +26,7 @@ public class Output /// Indicates if the memory has been allocated and needs to be disposed. internal Output(nint handle, bool shouldDispose) { - if (handle == nint.Zero) - { - throw new ArgumentException("Handle cannot be zero.", nameof(handle)); - } - - var native = Marshal.PtrToStructure(handle); + var native = Marshal.PtrToStructure(handle.Checked()); Type = (OutputType)native.Tag; @@ -44,120 +39,120 @@ internal Output(nint handle, bool shouldDispose) } } + /// + /// Gets the type of the output. + /// + public OutputType Type { get; private set; } + /// /// Gets the value. /// - /// Value is not a . + /// Value is not a . public Doc Doc => GetValue(OutputType.Doc); /// /// Gets the value. /// - /// Value is not a . + /// Value is not a . public string String => GetValue(OutputType.String); /// /// Gets the value. /// - /// Value is not a . + /// Value is not a . public bool Boolean => GetValue(OutputType.Bool); /// /// Gets the value. /// - /// Value is not a . + /// Value is not a . public double Double => GetValue(OutputType.Double); /// /// Gets the value. /// - /// Value is not a . + /// Value is not a . public long Long => GetValue(OutputType.Long); /// /// Gets the array value. /// - /// Value is not a array. + /// Value is not a array. public byte[] Bytes => GetValue(OutputType.Bytes); /// /// Gets the collection. /// - /// Value is not a collection. + /// Value is not a collection. public Output[] Collection => GetValue(OutputType.Collection); /// /// Gets the value as json object. /// - /// Value is not a json object. + /// Value is not a json object. public IDictionary? Object => GetValue>(OutputType.Object); /// /// Gets the value. /// - /// Value is not a . - public Array Array => GetValue(OutputType.Array); + /// The resolved array. + /// Value is not a . + /// You are responsible to dispose the array, if you use this property. + public Array ResolveArray() => GetValue(OutputType.Array); /// /// Gets the value. /// - /// Value is not a . - public Map Map => GetValue(OutputType.Map); + /// The resolved map. + /// Value is not a . + /// You are responsible to dispose the map, if you use this property. + public Map ResolveMap() => GetValue(OutputType.Map); /// /// Gets the value. /// - /// Value is not a . - public Text Text => GetValue(OutputType.Text); + /// The resolved text. + /// Value is not a . + /// You are responsible to dispose the text, if you use this property. + public Text ResolveText() => GetValue(OutputType.Text); /// /// Gets the value. /// - /// Value is not a . - public XmlElement XmlElement => GetValue(OutputType.XmlElement); + /// The resolved xml element. + /// Value is not a . + /// You are responsible to dispose the xml element, if you use this property. + public XmlElement ResolveXmlElement() => GetValue(OutputType.XmlElement); /// /// Gets the value. /// - /// Value is not a . - public XmlText XmlText => GetValue(OutputType.XmlText); - - /// - /// Gets the type of the output. - /// - public OutputType Type { get; private set; } + /// The resolved xml text. + /// Value is not a . + /// You are responsible to dispose the xml text, if you use this property. + public XmlText ResolveXmlText() => GetValue(OutputType.XmlText); private static Lazy BuildValue(nint handle, uint length, OutputType type) { - static nint GuardHandle(nint handle) - { - if (handle == nint.Zero) - { - throw new InvalidOperationException("Internal type mismatch, native library returns null."); - } - - return handle; - } - switch (type) { case OutputType.Bool: { - var value = GuardHandle(OutputChannel.Boolean(handle)); + var value = OutputChannel.Boolean(handle).Checked(); return new Lazy((object?)(Marshal.PtrToStructure(value) == 1)); } case OutputType.Double: { - var value = GuardHandle(OutputChannel.Double(handle)); + var value = OutputChannel.Double(handle).Checked(); return new Lazy(Marshal.PtrToStructure(value)); } case OutputType.Long: { - var value = GuardHandle(OutputChannel.Long(handle)); + var value = OutputChannel.Long(handle).Checked(); return new Lazy(Marshal.PtrToStructure(value)); } @@ -171,14 +166,14 @@ static nint GuardHandle(nint handle) case OutputType.Bytes: { - var pointer = GuardHandle(OutputChannel.Bytes(handle)); + var pointer = OutputChannel.Bytes(handle).Checked(); var result = MemoryReader.TryReadBytes(OutputChannel.Bytes(handle), length) ?? - throw new InvalidOperationException("Internal type mismatch, native library returns null."); + throw new YDotNetException("Internal type mismatch, native library returns null."); if (result == null) { - throw new InvalidOperationException("Internal type mismatch, native library returns null."); + throw new YDotNetException("Internal type mismatch, native library returns null."); } OutputChannel.Destroy(pointer); @@ -187,10 +182,10 @@ static nint GuardHandle(nint handle) case OutputType.Collection: { - var pointer = GuardHandle(OutputChannel.Collection(handle)); + var pointer = OutputChannel.Collection(handle).Checked(); var handles = MemoryReader.TryReadIntPtrArray(pointer, length, Marshal.SizeOf()) - ?? throw new InvalidOperationException("Internal type mismatch, native library returns null."); + ?? throw new YDotNetException("Internal type mismatch, native library returns null."); var result = handles.Select(x => new Output(x, false)).ToArray(); @@ -200,10 +195,10 @@ static nint GuardHandle(nint handle) case OutputType.Object: { - var pointer = GuardHandle(OutputChannel.Object(handle)); + var pointer = OutputChannel.Object(handle).Checked(); var handlesArray = MemoryReader.TryReadIntPtrArray(pointer, length, Marshal.SizeOf()) - ?? throw new InvalidOperationException("Internal type mismatch, native library returns null."); + ?? throw new YDotNetException("Internal type mismatch, native library returns null."); var result = new Dictionary(); @@ -220,22 +215,22 @@ static nint GuardHandle(nint handle) } case OutputType.Array: - return new Lazy(() => new Array(GuardHandle(OutputChannel.Array(handle)))); + return new Lazy(() => new Array(OutputChannel.Array(handle).Checked())); case OutputType.Map: - return new Lazy(() => new Map(GuardHandle(OutputChannel.Map(handle)))); + return new Lazy(() => new Map(OutputChannel.Map(handle).Checked())); case OutputType.Text: - return new Lazy(() => new Text(GuardHandle(OutputChannel.Text(handle)))); + return new Lazy(() => new Text(OutputChannel.Text(handle).Checked())); case OutputType.XmlElement: - return new Lazy(() => new XmlElement(GuardHandle(OutputChannel.XmlElement(handle)))); + return new Lazy(() => new XmlElement(OutputChannel.XmlElement(handle).Checked())); case OutputType.XmlText: - return new Lazy(() => new XmlText(GuardHandle(OutputChannel.XmlText(handle)))); + return new Lazy(() => new XmlText(OutputChannel.XmlText(handle).Checked())); case OutputType.Doc: - return new Lazy(() => new Doc(GuardHandle(OutputChannel.Doc(handle)))); + return new Lazy(() => new Doc(OutputChannel.Doc(handle).Checked())); default: return new Lazy((object?)null); @@ -248,7 +243,7 @@ private T GetValue(OutputType expectedType) if (resolvedValue is not T typed) { - throw new InvalidOperationException($"Expected {expectedType}, got {Type}."); + throw new YDotNetException($"Expected {expectedType}, got {Type}."); } return typed; diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index 5d356a40..831d1082 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -5,7 +5,6 @@ using YDotNet.Document.Types.Texts; using YDotNet.Document.Types.XmlElements; using YDotNet.Document.Types.XmlTexts; -using YDotNet.Document.UndoManagers; using YDotNet.Infrastructure; using YDotNet.Native.Document; using YDotNet.Native.Document.Events; @@ -114,6 +113,7 @@ public string? CollectionId /// public void Dispose() { + GC.SuppressFinalize(this); DocChannel.Destroy(Handle); } @@ -122,158 +122,138 @@ public void Dispose() /// /// The instance returned will not be the same, but they will both control the same document. /// A new instance that controls the same document. - public Doc? Clone() + public Doc Clone() { - return ReferenceAccessor.Access(new Doc(DocChannel.Clone(Handle))); + var handle = DocChannel.Clone(Handle).Checked(); + + return new Doc(handle); } /// - /// Gets or creates a new shared data type instance as a root-level + /// Gets or creates a new shared data type instance as a root-level /// type in this document. /// /// /// This structure can later be accessed using its name. /// - /// The name of the instance to get. - /// - /// The instance related to the name provided or - /// null if failed. - /// - public Text? Text(string name) + /// The name of the instance to get. + /// The instance related to the name provided. + public Text Text(string name) { - var nameHandle = MemoryWriter.WriteUtf8String(name); - var result = ReferenceAccessor.Access(new Text(DocChannel.Text(Handle, nameHandle))); - - MemoryWriter.Release(nameHandle); + var textName = MemoryWriter.WriteUtf8String(name); + var textHandle = DocChannel.Text(Handle, textName); + MemoryWriter.Release(textName); - return result; + return new Text(textHandle.Checked()); } /// - /// Gets or creates a new shared data type instance as a root-level type + /// Gets or creates a new shared data type instance as a root-level type /// in this document. /// /// /// This structure can later be accessed using its name. /// - /// The name of the instance to get. - /// - /// The instance related to the name provided or null - /// if failed. - /// - public Map? Map(string name) + /// The name of the instance to get. + /// The instance related to the name provided. + public Map Map(string name) { - var nameHandle = MemoryWriter.WriteUtf8String(name); - var result = ReferenceAccessor.Access(new Map(DocChannel.Map(Handle, nameHandle))); + var mapName = MemoryWriter.WriteUtf8String(name); + var mapHandle = DocChannel.Map(Handle, mapName); + MemoryWriter.Release(mapName); - MemoryWriter.Release(nameHandle); - - return result; + return new Map(mapHandle.Checked()); } /// - /// Gets or creates a new shared data type instance as a root-level + /// Gets or creates a new shared data type instance as a root-level /// type in this document. /// /// /// This structure can later be accessed using its name. /// - /// The name of the instance to get. - /// - /// The instance related to the name provided or - /// null if failed. - /// - public Array? Array(string name) + /// The name of the instance to get. + /// The instance related to the name provided. + public Array Array(string name) { - var nameHandle = MemoryWriter.WriteUtf8String(name); - var result = ReferenceAccessor.Access(new Array(DocChannel.Array(Handle, nameHandle))); - - MemoryWriter.Release(nameHandle); + var arrayName = MemoryWriter.WriteUtf8String(name); + var arrayHandle = DocChannel.Array(Handle, arrayName); + MemoryWriter.Release(arrayName); - return result; + return new Array(arrayHandle.Checked()); } /// - /// Gets or creates a new shared data type instance as a + /// Gets or creates a new shared data type instance as a /// root-level type in this document. /// /// /// This structure can later be accessed using its name. /// - /// The name of the instance to get. - /// - /// The instance related to the name provided - /// or null if failed. - /// - public XmlElement? XmlElement(string name) + /// The name of the instance to get. + /// The instance related to the name provided. + public XmlElement XmlElement(string name) { - // TODO [LSViana] Wrap the XmlElement with an XmlFragment before returning the value. - var nameHandle = MemoryWriter.WriteUtf8String(name); - var result = ReferenceAccessor.Access(new XmlElement(DocChannel.XmlElement(Handle, nameHandle))); + var xmlElementName = MemoryWriter.WriteUtf8String(name); + var xmlElementHandle = DocChannel.XmlElement(Handle, xmlElementName); + MemoryWriter.Release(xmlElementName); - MemoryWriter.Release(nameHandle); - - return result; + return new XmlElement(xmlElementHandle.Checked()); } /// - /// Gets or creates a new shared data type instance as a + /// Gets or creates a new shared data type instance as a /// root-level type in this document. /// /// /// This structure can later be accessed using its name. /// - /// The name of the instance to get. - /// - /// The instance related to the name provided - /// or null if failed. - /// - public XmlText? XmlText(string name) + /// The name of the instance to get. + /// The instance related to the name provided. + public XmlText XmlText(string name) { - // TODO [LSViana] Wrap the XmlText with an XmlFragment before returning the value. - var nameHandle = MemoryWriter.WriteUtf8String(name); - var result = ReferenceAccessor.Access(new XmlText(DocChannel.XmlText(Handle, nameHandle))); - - MemoryWriter.Release(nameHandle); + var xmlTextName = MemoryWriter.WriteUtf8String(name); + var xmlTextHandle = DocChannel.XmlText(Handle, xmlTextName); + MemoryWriter.Release(xmlTextName); - return result; + return new XmlText(xmlTextHandle.Checked()); } /// /// Starts a new read-write on this document. /// - /// - /// Optional byte marker to indicate the source of changes to be applied by this transaction. - /// This value is used by . - /// - /// - /// The to perform operations in the document or null. - /// - /// Returns null if the could not be created because, for example, another - /// read-write already exists and was not committed yet. - /// - /// - public Transaction? WriteTransaction(byte[]? origin = null) + /// Optional byte marker to indicate the source of changes to be applied by this transaction. + /// The to perform operations in the document. + /// Another exception is pending. + public Transaction WriteTransaction(byte[]? origin = null) { - var length = (uint) (origin?.Length ?? 0); + var handle = DocChannel.WriteTransaction(Handle, (uint)(origin?.Length ?? 0), origin); - return ReferenceAccessor.Access( - new Transaction(DocChannel.WriteTransaction(Handle, length, origin))); + if (handle == nint.Zero) + { + ThrowHelper.PendingTransaction(); + return default!; + } + + return new Transaction(handle); } /// /// Starts a new read-only on this document. /// - /// - /// The to perform operations in the document or null. - /// - /// Returns null if the could not be created because, for example, another - /// read-write already exists and was not committed yet. - /// - /// - public Transaction? ReadTransaction() + /// The to perform operations in the document. + /// Another exception is pending. + public Transaction ReadTransaction() { - return ReferenceAccessor.Access(new Transaction(DocChannel.ReadTransaction(Handle))); + var handle = DocChannel.ReadTransaction(Handle); + + if (handle == nint.Zero) + { + ThrowHelper.PendingTransaction(); + return default!; + } + + return new Transaction(handle); } /// diff --git a/YDotNet/Document/Options/DocOptions.cs b/YDotNet/Document/Options/DocOptions.cs index 508eb2db..fa02b27d 100644 --- a/YDotNet/Document/Options/DocOptions.cs +++ b/YDotNet/Document/Options/DocOptions.cs @@ -8,9 +8,9 @@ public class DocOptions /// /// Gets the default options value used to initialize instances. /// - internal static DocOptions Default => new() + public static DocOptions Default => new() { - Id = (ulong) Random.Shared.Next(), + Id = (ulong)Random.Shared.Next(), ShouldLoad = true, Encoding = DocEncoding.Utf16 }; diff --git a/YDotNet/Document/StickyIndexes/StickyIndex.cs b/YDotNet/Document/StickyIndexes/StickyIndex.cs index 57ffa59a..68d2a68b 100644 --- a/YDotNet/Document/StickyIndexes/StickyIndex.cs +++ b/YDotNet/Document/StickyIndexes/StickyIndex.cs @@ -29,7 +29,7 @@ internal StickyIndex(nint handle) /// /// Gets the of the current . /// - public StickyAssociationType AssociationType => (StickyAssociationType) StickyIndexChannel.AssociationType(Handle); + public StickyAssociationType AssociationType => (StickyAssociationType)StickyIndexChannel.AssociationType(Handle); /// /// Gets the handle to the native resource. @@ -47,9 +47,11 @@ public void Dispose() /// /// The array received from . /// The represented by the provided array. - public static StickyIndex? Decode(byte[] encoded) + public static StickyIndex Decode(byte[] encoded) { - return ReferenceAccessor.Access(new StickyIndex(StickyIndexChannel.Decode(encoded, (uint) encoded.Length))); + var handle = StickyIndexChannel.Decode(encoded, (uint)encoded.Length); + + return new StickyIndex(handle.Checked()); } /// diff --git a/YDotNet/Document/Transactions/Transaction.cs b/YDotNet/Document/Transactions/Transaction.cs index abe4f98d..b16cdea7 100644 --- a/YDotNet/Document/Transactions/Transaction.cs +++ b/YDotNet/Document/Transactions/Transaction.cs @@ -127,7 +127,7 @@ public byte[] StateVectorV1() /// public byte[] StateDiffV1(byte[] stateVector) { - var handle = TransactionChannel.StateDiffV1(Handle, stateVector, (uint) (stateVector != null ? stateVector.Length : 0), out var length); + var handle = TransactionChannel.StateDiffV1(Handle, stateVector, (uint)(stateVector != null ? stateVector.Length : 0), out var length); var data = MemoryReader.ReadBytes(handle, length); BinaryChannel.Destroy(handle, length); @@ -160,7 +160,7 @@ public byte[] StateDiffV1(byte[] stateVector) /// public byte[] StateDiffV2(byte[] stateVector) { - var handle = TransactionChannel.StateDiffV2(Handle, stateVector, (uint) stateVector.Length, out var length); + var handle = TransactionChannel.StateDiffV2(Handle, stateVector, (uint)stateVector.Length, out var length); var data = MemoryReader.ReadBytes(handle, length); BinaryChannel.Destroy(handle, length); @@ -178,7 +178,7 @@ public byte[] StateDiffV2(byte[] stateVector) /// The result of the update operation. public TransactionUpdateResult ApplyV1(byte[] stateDiff) { - return (TransactionUpdateResult) TransactionChannel.ApplyV1(Handle, stateDiff, (uint) stateDiff.Length); + return (TransactionUpdateResult)TransactionChannel.ApplyV1(Handle, stateDiff, (uint)stateDiff.Length); } /// @@ -192,7 +192,7 @@ public TransactionUpdateResult ApplyV1(byte[] stateDiff) /// The result of the update operation. public TransactionUpdateResult ApplyV2(byte[] stateDiff) { - return (TransactionUpdateResult) TransactionChannel.ApplyV2(Handle, stateDiff, (uint) stateDiff.Length); + return (TransactionUpdateResult)TransactionChannel.ApplyV2(Handle, stateDiff, (uint)stateDiff.Length); } /// @@ -245,7 +245,7 @@ public byte[] Snapshot() var handle = TransactionChannel.EncodeStateFromSnapshotV1( Handle, snapshot, - (uint) snapshot.Length, + (uint)snapshot.Length, out var length); var data = MemoryReader.TryReadBytes(handle, length); BinaryChannel.Destroy(handle, length); @@ -283,7 +283,7 @@ public byte[] Snapshot() var handle = TransactionChannel.EncodeStateFromSnapshotV2( Handle, snapshot, - (uint) snapshot.Length, + (uint)snapshot.Length, out var length); var data = MemoryReader.TryReadBytes(handle, length); BinaryChannel.Destroy(handle, length); @@ -302,7 +302,9 @@ public byte[] Snapshot() /// public Array? GetArray(string name) { - return ReferenceAccessor.Access(new Array(GetWithKind(name, BranchKind.Array))); + var handle = GetWithKind(name, BranchKind.Array); + + return handle != nint.Zero ? new Array(handle) : null; } /// @@ -316,7 +318,9 @@ public byte[] Snapshot() /// public Map? GetMap(string name) { - return ReferenceAccessor.Access(new Map(GetWithKind(name, BranchKind.Map))); + var handle = GetWithKind(name, BranchKind.Map); + + return handle != nint.Zero ? new Map(handle) : null; } /// @@ -330,7 +334,9 @@ public byte[] Snapshot() /// public Text? GetText(string name) { - return ReferenceAccessor.Access(new Text(GetWithKind(name, BranchKind.Text))); + var handle = GetWithKind(name, BranchKind.Text); + + return handle != nint.Zero ? new Text(handle) : null; } /// @@ -344,7 +350,9 @@ public byte[] Snapshot() /// public XmlElement? GetXmlElement(string name) { - return ReferenceAccessor.Access(new XmlElement(GetWithKind(name, BranchKind.XmlElement))); + var handle = GetWithKind(name, BranchKind.XmlElement); + + return handle != nint.Zero ? new XmlElement(handle) : null; } /// @@ -358,15 +366,16 @@ public byte[] Snapshot() /// public XmlText? GetXmlText(string name) { - return ReferenceAccessor.Access(new XmlText(GetWithKind(name, BranchKind.XmlText))); + var handle = GetWithKind(name, BranchKind.XmlText); + + return handle != nint.Zero ? new XmlText(handle) : null; } private nint GetWithKind(string name, BranchKind branchKind) { var nameHandle = MemoryWriter.WriteUtf8String(name); var handle = TransactionChannel.Get(Handle, nameHandle); - var kind = (BranchKind) BranchChannel.Kind(handle); - + var kind = (BranchKind)BranchChannel.Kind(handle); MemoryWriter.Release(nameHandle); return kind != branchKind ? nint.Zero : handle; diff --git a/YDotNet/Document/Types/Arrays/Array.cs b/YDotNet/Document/Types/Arrays/Array.cs index f551ab54..9d920b89 100644 --- a/YDotNet/Document/Types/Arrays/Array.cs +++ b/YDotNet/Document/Types/Arrays/Array.cs @@ -41,7 +41,7 @@ public void InsertRange(Transaction transaction, uint index, IEnumerable var inputsArray = inputs.Select(x => x.InputNative).ToArray(); var inputsPointer = MemoryWriter.WriteStructArray(inputsArray); - ArrayChannel.InsertRange(Handle, transaction.Handle, index, inputsPointer, (uint) inputsArray.Length); + ArrayChannel.InsertRange(Handle, transaction.Handle, index, inputsPointer, (uint)inputsArray.Length); } /// @@ -69,7 +69,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) { var handle = ArrayChannel.Get(Handle, transaction.Handle, index); - return ReferenceAccessor.Output(handle, true); + return handle != nint.Zero ? new Output(handle, true) : null; } /// @@ -91,10 +91,12 @@ public void Move(Transaction transaction, uint sourceIndex, uint targetIndex) /// over all values of this . /// /// The transaction that wraps this operation. - /// The instance or null if failed. - public ArrayIterator? Iterate(Transaction transaction) + /// The instance. + public ArrayIterator Iterate(Transaction transaction) { - return ReferenceAccessor.Access(new ArrayIterator(ArrayChannel.Iterator(Handle, transaction.Handle))); + var handle = ArrayChannel.Iterator(Handle, transaction.Handle); + + return new ArrayIterator(handle.Checked()); } /// @@ -135,7 +137,8 @@ public void Unobserve(EventSubscription subscription) /// The in the with the given . public StickyIndex? StickyIndex(Transaction transaction, uint index, StickyAssociationType associationType) { - return ReferenceAccessor.Access( - new StickyIndex(StickyIndexChannel.FromIndex(Handle, transaction.Handle, index, (sbyte) associationType))); + var handle = StickyIndexChannel.FromIndex(Handle, transaction.Handle, index, (sbyte)associationType); + + return handle != nint.Zero ? new StickyIndex(handle) : null; } } diff --git a/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs b/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs index 3255fcfd..a65b3ff5 100644 --- a/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs +++ b/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs @@ -10,6 +10,8 @@ namespace YDotNet.Document.Types.Arrays; /// internal class ArrayEnumerator : IEnumerator { + private Output? current; + /// /// Initializes a new instance of the class. /// @@ -20,27 +22,25 @@ internal class ArrayEnumerator : IEnumerator internal ArrayEnumerator(ArrayIterator iterator) { Iterator = iterator; - Current = null; } - /// - /// Gets the instance that holds the - /// used by this enumerator. - /// - private ArrayIterator Iterator { get; } - /// - object? IEnumerator.Current => Current; + public Output Current => current!; /// - public Output? Current { get; private set; } + object? IEnumerator.Current => current!; + + /// + /// Gets the instance that holds the used by this enumerator. + /// + private ArrayIterator Iterator { get; } /// public bool MoveNext() { var handle = ArrayChannel.IteratorNext(Iterator.Handle); - Current = handle != nint.Zero ? new Output(handle, false) : null; + current = handle != nint.Zero ? new Output(handle, false) : null; return Current != null; } diff --git a/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs b/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs index 29ea0c10..25e81ade 100644 --- a/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs +++ b/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs @@ -9,6 +9,10 @@ namespace YDotNet.Document.Types.Arrays.Events; /// public class ArrayEvent { + private readonly Lazy path; + private readonly Lazy delta; + private readonly Lazy target; + /// /// Initializes a new instance of the class. /// @@ -16,32 +20,36 @@ public class ArrayEvent internal ArrayEvent(nint handle) { Handle = handle; + + path = new Lazy(() => + { + var pathHandle = ArrayChannel.ObserveEventPath(handle, out var length).Checked(); + return new EventPath(pathHandle, length); + }); + + delta = new Lazy(() => + { + var deltaHandle = ArrayChannel.ObserveEventDelta(handle, out var length).Checked(); + return new EventChanges(deltaHandle, length); + }); + + target = new Lazy(() => + { + var targetHandle = ArrayChannel.ObserveEventTarget(handle).Checked(); + return new Array(targetHandle); + }); } /// /// Gets the path from the observed instanced down to the current instance. /// - /// - /// This property can only be accessed during the callback that exposes this instance. - /// Check the documentation of for more information. - /// - public EventPath Path - { - get - { - var handle = ArrayChannel.ObserveEventPath(Handle, out var length); - - return new EventPath(handle, length); - } - } + /// This property can only be accessed during the callback that exposes this instance. + public EventPath Path => path.Value; /// /// Gets the changes within the instance and triggered this event. /// - /// - /// This property can only be accessed during the callback that exposes this instance. - /// Check the documentation of for more information. - /// + /// This property can only be accessed during the callback that exposes this instance. public EventChanges Delta { get @@ -53,12 +61,14 @@ public EventChanges Delta } /// - /// Gets the instance that is related to this instance. + /// Gets the handle to the native resource. /// - public Array? Target => ReferenceAccessor.Access(new Array(ArrayChannel.ObserveEventTarget(Handle))); + internal nint Handle { get; } /// - /// Gets the handle to the native resource. + /// Gets the instance that is related to this instance. /// - internal nint Handle { get; } + /// The target of the event. + /// You are responsible to dispose the text, if you use this property. + public Array ResolveTarget() => target.Value; } diff --git a/YDotNet/Document/Types/Branches/Branch.cs b/YDotNet/Document/Types/Branches/Branch.cs index d81eba8b..e5c27f84 100644 --- a/YDotNet/Document/Types/Branches/Branch.cs +++ b/YDotNet/Document/Types/Branches/Branch.cs @@ -1,9 +1,7 @@ using YDotNet.Document.Events; -using YDotNet.Document.StickyIndexes; using YDotNet.Document.Transactions; using YDotNet.Document.Types.Events; using YDotNet.Infrastructure; -using YDotNet.Native.StickyIndex; using YDotNet.Native.Types.Branches; namespace YDotNet.Document.Types.Branches; @@ -64,32 +62,38 @@ public void UnobserveDeep(EventSubscription subscription) } /// - /// Starts a new read-only on this instance. + /// Starts a new read-write on this instance. /// - /// - /// The to perform operations in the or null. - /// - /// Returns null if the could not be created because, for example, another - /// read-write already exists and was not committed yet. - /// - /// - public Transaction? ReadTransaction() + /// The to perform operations in the document. + /// Another exception is pending. + public Transaction WriteTransaction() { - return ReferenceAccessor.Access(new Transaction(BranchChannel.ReadTransaction(Handle))); + var handle = BranchChannel.WriteTransaction(Handle); + + if (handle == nint.Zero) + { + ThrowHelper.PendingTransaction(); + return default!; + } + + return new Transaction(handle); } /// - /// Starts a new read-write on this instance. + /// Starts a new read-only on this instance. /// - /// - /// The to perform operations in the or null. - /// - /// Returns null if the could not be created because, for example, another - /// read-write already exists and was not committed yet. - /// - /// - public Transaction? WriteTransaction() + /// The to perform operations in the branch. + /// Another exception is pending. + public Transaction ReadTransaction() { - return ReferenceAccessor.Access(new Transaction(BranchChannel.WriteTransaction(Handle))); + var handle = BranchChannel.ReadTransaction(Handle); + + if (handle == nint.Zero) + { + ThrowHelper.PendingTransaction(); + return default!; + } + + return new Transaction(handle); } } diff --git a/YDotNet/Document/Types/Events/EventBranch.cs b/YDotNet/Document/Types/Events/EventBranch.cs index 691850ce..c0e79d10 100644 --- a/YDotNet/Document/Types/Events/EventBranch.cs +++ b/YDotNet/Document/Types/Events/EventBranch.cs @@ -21,7 +21,7 @@ public class EventBranch public EventBranch(nint handle) { Handle = handle; - Tag = (EventBranchTag) Marshal.ReadByte(handle); + Tag = (EventBranchTag)Marshal.ReadByte(handle); switch (Tag) { diff --git a/YDotNet/Document/Types/Events/EventChange.cs b/YDotNet/Document/Types/Events/EventChange.cs index b1ad0318..00fd36e8 100644 --- a/YDotNet/Document/Types/Events/EventChange.cs +++ b/YDotNet/Document/Types/Events/EventChange.cs @@ -7,8 +7,6 @@ namespace YDotNet.Document.Types.Events; /// public class EventChange { - private readonly Lazy> values; - /// /// Initializes a new instance of the class. /// @@ -18,11 +16,11 @@ public class EventChange /// Optional, the values affected by the current change if is /// . /// - public EventChange(EventChangeTag tag, uint length, Lazy> values) + public EventChange(EventChangeTag tag, uint length, List values) { Tag = tag; Length = length; - this.values = values; + Values = values; } /// @@ -38,5 +36,5 @@ public EventChange(EventChangeTag tag, uint length, Lazy> values) /// /// Gets the values that were affected by this change. /// - public List Values => values.Value; + public IReadOnlyList Values { get; } } diff --git a/YDotNet/Document/Types/Events/EventChanges.cs b/YDotNet/Document/Types/Events/EventChanges.cs index eaf64db2..13fedff4 100644 --- a/YDotNet/Document/Types/Events/EventChanges.cs +++ b/YDotNet/Document/Types/Events/EventChanges.cs @@ -1,4 +1,4 @@ -using System.Collections; +using System.Collections.ObjectModel; using System.Runtime.InteropServices; using YDotNet.Infrastructure; using YDotNet.Native.Types; @@ -9,24 +9,17 @@ namespace YDotNet.Document.Types.Events; /// /// Represents a collection of instances. /// -public class EventChanges : IEnumerable, IDisposable +public class EventChanges : ReadOnlyCollection { - private readonly IEnumerable collection; - /// /// Initializes a new instance of the class. /// /// The handle to the native resource. /// The length of the array of to read from . public EventChanges(nint handle, uint length) + : base(ReadItems(handle, length)) { Handle = handle; - Length = length; - - collection = MemoryReader.TryReadIntPtrArray(Handle, Length, Marshal.SizeOf())! - .Select(Marshal.PtrToStructure) - .Select(x => x.ToEventChange()) - .ToArray(); } /// @@ -34,26 +27,15 @@ public EventChanges(nint handle, uint length) /// internal nint Handle { get; } - /// - /// Gets the length of the native resource. - /// - internal uint Length { get; } - - /// - public void Dispose() + private static IList ReadItems(nint handle, uint length) { - EventChannel.DeltaDestroy(Handle, Length); - } - - /// - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + var result = MemoryReader.TryReadIntPtrArray(handle, length, Marshal.SizeOf())! + .Select(Marshal.PtrToStructure) + .Select(x => x.ToEventChange()) + .ToList(); - /// - public IEnumerator GetEnumerator() - { - return collection.GetEnumerator(); + // We are done reading and can release the memory. + PathChannel.Destroy(handle, length); + return result; } } diff --git a/YDotNet/Document/Types/Events/EventDeltas.cs b/YDotNet/Document/Types/Events/EventDeltas.cs index 0e7b62de..740984d9 100644 --- a/YDotNet/Document/Types/Events/EventDeltas.cs +++ b/YDotNet/Document/Types/Events/EventDeltas.cs @@ -1,4 +1,4 @@ -using System.Collections; +using System.Collections.ObjectModel; using System.Runtime.InteropServices; using YDotNet.Infrastructure; using YDotNet.Native.Types; @@ -9,24 +9,17 @@ namespace YDotNet.Document.Types.Events; /// /// Represents a collection of instances. /// -public class EventDeltas : IEnumerable, IDisposable +public class EventDeltas : ReadOnlyCollection { - private readonly IEnumerable collection; - /// /// Initializes a new instance of the class. /// /// The handle to the native resource. /// The length of the array of to read from . - public EventDeltas(nint handle, uint length) + internal EventDeltas(nint handle, uint length) + : base(ReadItems(handle, length)) { Handle = handle; - Length = length; - - collection = MemoryReader.TryReadIntPtrArray(Handle, Length, Marshal.SizeOf())! - .Select(Marshal.PtrToStructure) - .Select(x => x.ToEventDelta()) - .ToArray(); } /// @@ -34,26 +27,16 @@ public EventDeltas(nint handle, uint length) /// internal nint Handle { get; } - /// - /// Gets the length of the native resource. - /// - internal uint Length { get; } - - /// - public void Dispose() + private static IList ReadItems(nint handle, uint length) { - EventChannel.DeltaDestroy(Handle, Length); - } + var result = MemoryReader.ReadIntPtrArray(handle, length, Marshal.SizeOf()) + .Select(Marshal.PtrToStructure) + .Select(x => x.ToEventDelta()) + .ToList(); - /// - public IEnumerator GetEnumerator() - { - return collection.GetEnumerator(); - } + // We are done reading and can release the memory. + EventChannel.DeltaDestroy(handle, length); - /// - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); + return result; } } diff --git a/YDotNet/Document/Types/Events/EventKeys.cs b/YDotNet/Document/Types/Events/EventKeys.cs index 94e8fcc6..b1db4ed5 100644 --- a/YDotNet/Document/Types/Events/EventKeys.cs +++ b/YDotNet/Document/Types/Events/EventKeys.cs @@ -1,4 +1,4 @@ -using System.Collections; +using System.Collections.ObjectModel; using System.Runtime.InteropServices; using YDotNet.Infrastructure; using YDotNet.Native.Types; @@ -10,51 +10,33 @@ namespace YDotNet.Document.Types.Events; /// Represents the keys that changed the shared type that emitted the event related to this /// instance. /// -public class EventKeys : IEnumerable, IDisposable +public class EventKeys : ReadOnlyCollection { - private readonly IEnumerable collection; - /// /// Initializes a new instance of the class. /// /// The handle to the beginning of the array of instances. /// The length of the array. internal EventKeys(nint handle, uint length) + : base(ReadItems(handle, length)) { Handle = handle; - Length = length; - - collection = MemoryReader.TryReadIntPtrArray(Handle, Length, Marshal.SizeOf())! - .Select(Marshal.PtrToStructure) - .Select(x => x.ToEventKeyChange()) - .ToArray(); } - /// - /// Gets the length of the native resource. - /// - internal uint Length { get; } - /// /// Gets the handle to the native resource. /// internal nint Handle { get; } - /// - public void Dispose() + private static IList ReadItems(nint handle, uint length) { - EventChannel.KeysDestroy(Handle, Length); - } - - /// - public IEnumerator GetEnumerator() - { - return collection.GetEnumerator(); - } + var result = MemoryReader.ReadIntPtrArray(handle, length, Marshal.SizeOf()) + .Select(Marshal.PtrToStructure) + .Select(x => x.ToEventKeyChange()) + .ToList(); - /// - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); + // We are done reading and can release the memory. + EventChannel.KeysDestroy(handle, length); + return result; } } diff --git a/YDotNet/Document/Types/Events/EventPath.cs b/YDotNet/Document/Types/Events/EventPath.cs index 9c077ae1..50a7adef 100644 --- a/YDotNet/Document/Types/Events/EventPath.cs +++ b/YDotNet/Document/Types/Events/EventPath.cs @@ -1,4 +1,4 @@ -using System.Collections; +using System.Collections.ObjectModel; using YDotNet.Infrastructure; using YDotNet.Native.Types; @@ -8,50 +8,30 @@ namespace YDotNet.Document.Types.Events; /// Represents the path from the root type to the shared type that emitted the event related to this /// instance. /// -public class EventPath : IEnumerable, IDisposable +public class EventPath : ReadOnlyCollection { - private readonly IEnumerable collection; - /// /// Initializes a new instance of the class. /// /// The handle to the beginning of the array of instances. /// The length of the array. internal EventPath(nint handle, uint length) + : base(ReadItems(handle, length)) { Handle = handle; - Length = length; - - collection = MemoryReader.TryReadIntPtrArray(Handle, Length, size: 16)! - .Select(x => new EventPathSegment(x)) - .ToArray(); } - /// - /// Gets the length of the native resource. - /// - internal uint Length { get; } - /// /// Gets the handle to the native resource. /// internal nint Handle { get; } - /// - public void Dispose() + private static IList ReadItems(nint handle, uint length) { - PathChannel.Destroy(Handle, Length); - } + var result = MemoryReader.ReadIntPtrArray(handle, length, size: 16).Select(x => new EventPathSegment(x)).ToList(); - /// - public IEnumerator GetEnumerator() - { - return collection.GetEnumerator(); - } - - /// - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); + // We are done reading and can release the memory. + PathChannel.Destroy(handle, length); + return result; } } diff --git a/YDotNet/Document/Types/Events/EventPathSegment.cs b/YDotNet/Document/Types/Events/EventPathSegment.cs index 46402f82..66fbf1b1 100644 --- a/YDotNet/Document/Types/Events/EventPathSegment.cs +++ b/YDotNet/Document/Types/Events/EventPathSegment.cs @@ -15,7 +15,7 @@ public class EventPathSegment public EventPathSegment(nint handle) { Handle = handle; - Tag = (EventPathSegmentTag) Marshal.ReadByte(handle); + Tag = (EventPathSegmentTag)Marshal.ReadByte(handle); switch (Tag) { @@ -25,16 +25,11 @@ public EventPathSegment(nint handle) break; case EventPathSegmentTag.Index: - Index = (uint) Marshal.ReadInt32(handle + MemoryConstants.PointerSize); + Index = (uint)Marshal.ReadInt32(handle + MemoryConstants.PointerSize); break; } } - /// - /// Gets the handle to the native resource. - /// - internal nint Handle { get; } - /// /// Gets the value that indicates the kind of data held by this instance. /// @@ -51,4 +46,9 @@ public EventPathSegment(nint handle) /// null otherwise. /// public uint? Index { get; } + + /// + /// Gets the handle to the native resource. + /// + internal nint Handle { get; } } diff --git a/YDotNet/Document/Types/Maps/Events/MapEvent.cs b/YDotNet/Document/Types/Maps/Events/MapEvent.cs index 50003a68..dab77da9 100644 --- a/YDotNet/Document/Types/Maps/Events/MapEvent.cs +++ b/YDotNet/Document/Types/Maps/Events/MapEvent.cs @@ -1,5 +1,6 @@ using YDotNet.Document.Types.Events; using YDotNet.Infrastructure; +using YDotNet.Native.Types; using YDotNet.Native.Types.Maps; namespace YDotNet.Document.Types.Maps.Events; @@ -9,6 +10,10 @@ namespace YDotNet.Document.Types.Maps.Events; /// public class MapEvent { + private readonly Lazy path; + private readonly Lazy keys; + private readonly Lazy target; + /// /// Initializes a new instance of the class. /// @@ -16,49 +21,47 @@ public class MapEvent internal MapEvent(nint handle) { Handle = handle; - } - /// - /// Gets the keys that changed within the instance and triggered this event. - /// - /// - /// This property can only be accessed during the callback that exposes this instance. - /// Check the documentation of for more information. - /// - public EventKeys Keys - { - get + path = new Lazy(() => { - var handle = MapChannel.ObserveEventKeys(Handle, out var length); + var pathHandle = ArrayChannel.ObserveEventPath(handle, out var length).Checked(); + return new EventPath(pathHandle, length); + }); - return new EventKeys(handle, length); - } + keys = new Lazy(() => + { + var keysHandle = MapChannel.ObserveEventKeys(handle, out var length).Checked(); + return new EventKeys(keysHandle, length); + }); + + target = new Lazy(() => + { + var targetHandle = MapChannel.ObserveEventTarget(handle).Checked(); + return new Map(targetHandle); + }); } /// - /// Gets the path from the observed instanced down to the current instance. + /// Gets the keys that changed within the instance and triggered this event. /// - /// - /// This property can only be accessed during the callback that exposes this instance. - /// Check the documentation of for more information. - /// - public EventPath Path - { - get - { - var handle = MapChannel.ObserveEventPath(Handle, out var length); - - return new EventPath(handle, length); - } - } + /// This property can only be accessed during the callback that exposes this instance. + public EventKeys Keys => keys.Value; /// - /// Gets the instance that is related to this instance. + /// Gets the path from the observed instanced down to the current instance. /// - public Map? Target => ReferenceAccessor.Access(new Map(MapChannel.ObserveEventTarget(Handle))); + /// This property can only be accessed during the callback that exposes this instance. + public EventPath Path => path.Value; /// /// Gets the handle to the native resource. /// internal nint Handle { get; } + + /// + /// Gets the instance that is related to this instance. + /// + /// The target of the event. + /// You are responsible to dispose the text, if you use this property. + public Map ResolveTarget() => target.Value; } diff --git a/YDotNet/Document/Types/Maps/Map.cs b/YDotNet/Document/Types/Maps/Map.cs index 345070bf..75468446 100644 --- a/YDotNet/Document/Types/Maps/Map.cs +++ b/YDotNet/Document/Types/Maps/Map.cs @@ -54,12 +54,11 @@ public void Insert(Transaction transaction, string key, Input input) /// The or null if entry not found. public Output? Get(Transaction transaction, string key) { - var keyHandle = MemoryWriter.WriteUtf8String(key); - var handle = MapChannel.Get(Handle, transaction.Handle, keyHandle); - - MemoryWriter.Release(keyHandle); + var outputName = MemoryWriter.WriteUtf8String(key); + var outputHandle = MapChannel.Get(Handle, transaction.Handle, outputName); + MemoryWriter.Release(outputName); - return ReferenceAccessor.Output(handle, false); + return outputHandle != nint.Zero ? new Output(outputHandle, false) : null; } /// @@ -102,10 +101,12 @@ public void RemoveAll(Transaction transaction) /// . /// /// The transaction that wraps this operation. - /// The instance or null if failed. - public MapIterator? Iterate(Transaction transaction) + /// The instance. + public MapIterator Iterate(Transaction transaction) { - return ReferenceAccessor.Access(new MapIterator(MapChannel.Iterator(Handle, transaction.Handle))); + var handle = MapChannel.Iterator(Handle, transaction.Handle).Checked(); + + return new MapIterator(handle); } /// diff --git a/YDotNet/Document/Types/Maps/MapEntry.cs b/YDotNet/Document/Types/Maps/MapEntry.cs index 40570d66..037d5a7e 100644 --- a/YDotNet/Document/Types/Maps/MapEntry.cs +++ b/YDotNet/Document/Types/Maps/MapEntry.cs @@ -7,45 +7,41 @@ namespace YDotNet.Document.Types.Maps; /// /// Represents an entry of a . It contains a and . /// -public class MapEntry : IDisposable +public sealed class MapEntry { /// /// Initializes a new instance of the class. /// /// The handle to the native resource. - internal MapEntry(nint handle) + internal MapEntry(nint handle, bool shouldDispose) { Handle = handle; var (mapEntry, outputHandle) = MemoryReader.ReadMapEntryAndOutputHandle(handle); + Key = MemoryReader.ReadUtf8String(mapEntry.Field); - MapEntryNative = mapEntry; + // The output memory is part of the entry memory. Therefore we don't release it. Value = new Output(outputHandle, false); + + if (shouldDispose) + { + // We are done reading and can release the memory. + MapChannel.EntryDestroy(handle); + } } /// /// Gets the key of this that represents the . /// - public string Key => MemoryReader.ReadUtf8String(MapEntryNative.Field); + public string Key { get; } /// /// Gets the value of this that is represented the . /// public Output Value { get; } - /// - /// Gets the native resource that provides data for this instance. - /// - internal MapEntryNative MapEntryNative { get; } - /// /// Gets the handle to the native resource. /// internal nint Handle { get; } - - /// - public void Dispose() - { - MapChannel.EntryDestroy(Handle); - } } diff --git a/YDotNet/Document/Types/Maps/MapEnumerator.cs b/YDotNet/Document/Types/Maps/MapEnumerator.cs index b4473ff7..1314fc45 100644 --- a/YDotNet/Document/Types/Maps/MapEnumerator.cs +++ b/YDotNet/Document/Types/Maps/MapEnumerator.cs @@ -9,6 +9,8 @@ namespace YDotNet.Document.Types.Maps; /// internal class MapEnumerator : IEnumerator { + private MapEntry? current; + /// /// Initializes a new instance of the class. /// @@ -19,29 +21,27 @@ internal class MapEnumerator : IEnumerator internal MapEnumerator(MapIterator mapIterator) { MapIterator = mapIterator; - Current = null; } - /// - /// Gets the instance that holds the - /// used by this enumerator. - /// - private MapIterator MapIterator { get; } - /// - object? IEnumerator.Current => Current; + public MapEntry Current => current!; /// - public MapEntry? Current { get; private set; } + object? IEnumerator.Current => current!; + + /// + /// Gets the instance that holds the used by this enumerator. + /// + private MapIterator MapIterator { get; } /// public bool MoveNext() { var handle = MapChannel.IteratorNext(MapIterator.Handle); - Current = handle != nint.Zero ? new MapEntry(handle) : null; + current = handle != nint.Zero ? new MapEntry(handle, true) : null; - return Current != null; + return current != null; } /// diff --git a/YDotNet/Document/Types/Texts/Events/TextEvent.cs b/YDotNet/Document/Types/Texts/Events/TextEvent.cs index 9b894638..6d17dd21 100644 --- a/YDotNet/Document/Types/Texts/Events/TextEvent.cs +++ b/YDotNet/Document/Types/Texts/Events/TextEvent.cs @@ -9,6 +9,10 @@ namespace YDotNet.Document.Types.Texts.Events; /// public class TextEvent { + private readonly Lazy path; + private readonly Lazy deltas; + private readonly Lazy target; + /// /// Initializes a new instance of the class. /// @@ -16,49 +20,47 @@ public class TextEvent internal TextEvent(nint handle) { Handle = handle; - } - /// - /// Gets the keys that changed within the instance and triggered this event. - /// - /// - /// This property can only be accessed during the callback that exposes this instance. - /// Check the documentation of for more information. - /// - public EventDeltas Delta - { - get + path = new Lazy(() => { - var handle = TextChannel.ObserveEventDelta(Handle, out var length); + var pathHandle = TextChannel.ObserveEventPath(handle, out var length).Checked(); + return new EventPath(pathHandle, length); + }); - return new EventDeltas(handle, length); - } + deltas = new Lazy(() => + { + var deltaHandle = TextChannel.ObserveEventDelta(handle, out var length).Checked(); + return new EventDeltas(deltaHandle, length); + }); + + target = new Lazy(() => + { + var targetHandle = TextChannel.ObserveEventTarget(handle).Checked(); + return new Text(targetHandle); + }); } /// - /// Gets the path from the observed instanced down to the current instance. + /// Gets the keys that changed within the instance and triggered this event. /// - /// - /// This property can only be accessed during the callback that exposes this instance. - /// Check the documentation of for more information. - /// - public EventPath Path - { - get - { - var handle = TextChannel.ObserveEventPath(Handle, out var length); - - return new EventPath(handle, length); - } - } + /// This property can only be accessed during the callback that exposes this instance. + public EventDeltas Delta => deltas.Value; /// - /// Gets the instance that is related to this instance. + /// Gets the path from the observed instanced down to the current instance. /// - public Text? Target => ReferenceAccessor.Access(new Text(TextChannel.ObserveEventTarget(Handle))); + /// This property can only be accessed during the callback that exposes this instance. + public EventPath Path => path.Value; /// /// Gets the handle to the native resource. /// internal nint Handle { get; } + + /// + /// Gets the instance that is related to this instance. + /// + /// The target of the event. + /// You are responsible to dispose the text, if you use this property. + public Text ResolveTarget() => target.Value; } diff --git a/YDotNet/Document/Types/Texts/Text.cs b/YDotNet/Document/Types/Texts/Text.cs index 9ce2cb11..bad87aef 100644 --- a/YDotNet/Document/Types/Texts/Text.cs +++ b/YDotNet/Document/Types/Texts/Text.cs @@ -60,7 +60,7 @@ public void Insert(Transaction transaction, uint index, string value, Input? att public void InsertEmbed(Transaction transaction, uint index, Input content, Input? attributes = null) { MemoryWriter.TryToWriteStruct(attributes?.InputNative, out var attributesPointer); - MemoryWriter.TryToWriteStruct(content.InputNative, out var contentPointer); + var contentPointer = MemoryWriter.WriteStruct(content.InputNative); TextChannel.InsertEmbed(Handle, transaction.Handle, index, contentPointer, attributesPointer); MemoryWriter.TryRelease(attributesPointer); MemoryWriter.TryRelease(contentPointer); @@ -95,9 +95,10 @@ public void RemoveRange(Transaction transaction, uint index, uint length) /// public void Format(Transaction transaction, uint index, uint length, Input attributes) { - var attributesPointer = MemoryWriter.WriteStruct(attributes.InputNative); - TextChannel.Format(Handle, transaction.Handle, index, length, attributesPointer); - MemoryWriter.Release(attributesPointer); + var attributesHnadle = MemoryWriter.WriteStruct(attributes.InputNative); + TextChannel.Format(Handle, transaction.Handle, index, length, attributesHnadle); + + MemoryWriter.Release(attributesHnadle); } /// @@ -107,7 +108,7 @@ public void Format(Transaction transaction, uint index, uint length, Input attri /// The that compose this . public TextChunks Chunks(Transaction transaction) { - var handle = TextChannel.Chunks(Handle, transaction.Handle, out var length); + var handle = TextChannel.Chunks(Handle, transaction.Handle, out var length).Checked(); return new TextChunks(handle, length); } @@ -177,7 +178,8 @@ public void Unobserve(EventSubscription subscription) /// The in the with the given . public StickyIndex? StickyIndex(Transaction transaction, uint index, StickyAssociationType associationType) { - return ReferenceAccessor.Access( - new StickyIndex(StickyIndexChannel.FromIndex(Handle, transaction.Handle, index, (sbyte) associationType))); + var handle = StickyIndexChannel.FromIndex(Handle, transaction.Handle, index, (sbyte)associationType); + + return handle != nint.Zero ? new StickyIndex(handle) : null; } } diff --git a/YDotNet/Document/Types/Texts/TextChunk.cs b/YDotNet/Document/Types/Texts/TextChunk.cs index 0ff5d249..f2ee7bd0 100644 --- a/YDotNet/Document/Types/Texts/TextChunk.cs +++ b/YDotNet/Document/Types/Texts/TextChunk.cs @@ -1,3 +1,4 @@ +using System.Reflection.Metadata.Ecma335; using System.Runtime.InteropServices; using YDotNet.Document.Cells; using YDotNet.Document.Types.Maps; @@ -21,14 +22,21 @@ internal TextChunk(nint handle) Data = new Output(handle, false); var offset = Marshal.SizeOf(); - var attributesLength = (uint) Marshal.ReadInt32(handle + offset); + + var attributesLength = (uint)Marshal.ReadInt32(handle + offset); var attributesHandle = Marshal.ReadIntPtr(handle + offset + MemoryConstants.PointerSize); - Attributes = MemoryReader.TryReadIntPtrArray( - attributesHandle, attributesLength, Marshal.SizeOf()) - ?.Select(x => new MapEntry(x)) - .ToArray() ?? - Enumerable.Empty(); + if (attributesHandle == nint.Zero) + { + Attributes = new List(); + return; + } + + Attributes = MemoryReader.ReadIntPtrArray( + attributesHandle, + attributesLength, + Marshal.SizeOf()) + .Select(x => new MapEntry(x, false)).ToList(); } /// @@ -40,5 +48,5 @@ internal TextChunk(nint handle) /// /// Gets the formatting attributes applied to the . /// - public IEnumerable Attributes { get; } + public IReadOnlyList Attributes { get; } } diff --git a/YDotNet/Document/Types/Texts/TextChunks.cs b/YDotNet/Document/Types/Texts/TextChunks.cs index f7fd747a..43fa42de 100644 --- a/YDotNet/Document/Types/Texts/TextChunks.cs +++ b/YDotNet/Document/Types/Texts/TextChunks.cs @@ -1,4 +1,4 @@ -using System.Collections; +using System.Collections.ObjectModel; using YDotNet.Infrastructure; using YDotNet.Native.Types; @@ -7,23 +7,17 @@ namespace YDotNet.Document.Types.Texts; /// /// Represents a collection of instances. /// -public class TextChunks : IEnumerable, IDisposable +public class TextChunks : ReadOnlyCollection { - private readonly IEnumerable collection; - /// /// Initializes a new instance of the class. /// /// The handle to the native resource. /// The length of instances to be read from . internal TextChunks(nint handle, uint length) + : base(ReadItems(handle, length)) { Handle = handle; - Length = length; - - collection = MemoryReader.TryReadIntPtrArray(Handle, Length, size: 32) - .Select(x => new TextChunk(x)) - .ToArray(); } /// @@ -31,26 +25,13 @@ internal TextChunks(nint handle, uint length) /// internal nint Handle { get; } - /// - /// Gets the length of the native resource. - /// - internal uint Length { get; } - - /// - public void Dispose() + private static IList ReadItems(nint handle, uint length) { - ChunksChannel.Destroy(Handle, Length); - } + var result = MemoryReader.ReadIntPtrArray(handle, length, size: 32).Select(x => new TextChunk(x)).ToList(); - /// - public IEnumerator GetEnumerator() - { - return collection.GetEnumerator(); - } + // We are done reading and can release the memory. + // ChunksChannel.Destroy(handle, length); - /// - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); + return result; } } diff --git a/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs b/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs index e1987930..88e63bcb 100644 --- a/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs +++ b/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs @@ -9,6 +9,11 @@ namespace YDotNet.Document.Types.XmlElements.Events; /// public class XmlElementEvent { + private readonly Lazy path; + private readonly Lazy delta; + private readonly Lazy keys; + private readonly Lazy target; + /// /// Initializes a new instance of the class. /// @@ -16,66 +21,59 @@ public class XmlElementEvent internal XmlElementEvent(nint handle) { Handle = handle; - } - /// - /// Gets the changes within the instance and triggered this event. - /// - /// - /// This property can only be accessed during the callback that exposes this instance. - /// Check the documentation of for more information. - /// - public EventChanges Delta - { - get + path = new Lazy(() => { - var handle = XmlElementChannel.ObserveEventDelta(Handle, out var length); + var pathHandle = XmlElementChannel.ObserveEventPath(handle, out var length).Checked(); + return new EventPath(pathHandle, length); + }); - return new EventChanges(handle, length); - } - } + delta = new Lazy(() => + { + var deltaHandle = XmlElementChannel.ObserveEventDelta(handle, out var length).Checked(); + return new EventChanges(deltaHandle, length); + }); - /// - /// Gets the path from the observed instanced down to the current instance. - /// - /// - /// This property can only be accessed during the callback that exposes this instance. - /// Check the documentation of for more information. - /// - public EventPath Path - { - get + keys = new Lazy(() => { - var handle = XmlElementChannel.ObserveEventPath(Handle, out var length); + var keysHandle = XmlElementChannel.ObserveEventKeys(handle, out var length).Checked(); + return new EventKeys(keysHandle, length); + }); - return new EventPath(handle, length); - } + target = new Lazy(() => + { + var targetHandle = XmlElementChannel.ObserveEventTarget(handle).Checked(); + return new XmlElement(targetHandle); + }); } /// - /// Gets the attributes that changed within the instance and triggered this event. + /// Gets the changes within the instance and triggered this event. /// - /// - /// This property can only be accessed during the callback that exposes this instance. - /// Check the documentation of for more information. - /// - public EventKeys Keys - { - get - { - var handle = XmlElementChannel.ObserveEventKeys(Handle, out var length); + /// This property can only be accessed during the callback that exposes this instance. + public EventChanges Delta => delta.Value; - return new EventKeys(handle, length); - } - } + /// + /// Gets the path from the observed instanced down to the current instance. + /// + /// This property can only be accessed during the callback that exposes this instance. + public EventPath Path => path.Value; /// - /// Gets the instance that is related to this instance. + /// Gets the attributes that changed within the instance and triggered this event. /// - public XmlElement? Target => ReferenceAccessor.Access(new XmlElement(XmlElementChannel.ObserveEventTarget(Handle))); + /// This property can only be accessed during the callback that exposes this instance. + public EventKeys Keys => keys.Value; /// /// Gets the handle to the native resource. /// internal nint Handle { get; } + + /// + /// Gets the instance that is related to this instance. + /// + /// The target of the event. + /// You are responsible to dispose the text, if you use this property. + public XmlElement ResolveTarget() => target.Value; } diff --git a/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs b/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs index 97c563c5..74653ad4 100644 --- a/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs +++ b/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs @@ -10,6 +10,8 @@ namespace YDotNet.Document.Types.XmlElements.Trees; /// internal class XmlTreeWalkerEnumerator : IEnumerator { + private Output? current; + /// /// Initializes a new instance of the class. /// @@ -20,27 +22,26 @@ internal class XmlTreeWalkerEnumerator : IEnumerator public XmlTreeWalkerEnumerator(XmlTreeWalker treeWalker) { TreeWalker = treeWalker; - Current = null; } + /// + public Output Current => current!; + + /// + object? IEnumerator.Current => current!; + /// /// Gets the instance that holds the /// used by this enumerator. /// private XmlTreeWalker TreeWalker { get; } - /// - public Output? Current { get; private set; } - - /// - object? IEnumerator.Current => Current; - /// public bool MoveNext() { var handle = XmlElementChannel.TreeWalkerNext(TreeWalker.Handle); - Current = handle != nint.Zero ? new Output(handle, false) : null; + current = handle != nint.Zero ? new Output(handle, false) : null; return Current != null; } diff --git a/YDotNet/Document/Types/XmlElements/XmlAttribute.cs b/YDotNet/Document/Types/XmlElements/XmlAttribute.cs index 1a7c2447..56083b49 100644 --- a/YDotNet/Document/Types/XmlElements/XmlAttribute.cs +++ b/YDotNet/Document/Types/XmlElements/XmlAttribute.cs @@ -5,10 +5,9 @@ namespace YDotNet.Document.Types.XmlElements; /// -/// A structure representing single attribute of either an -/// or instance. +/// A structure representing single attribute of either an or instance. /// -public class XmlAttribute : IDisposable +public class XmlAttribute { /// /// Initializes a new instance of the class. @@ -18,28 +17,28 @@ internal XmlAttribute(nint handle) { Handle = handle; - Name = Marshal.PtrToStringAnsi(Marshal.ReadIntPtr(Handle)); - Value = Marshal.PtrToStringAnsi(Marshal.ReadIntPtr(Handle + MemoryConstants.PointerSize)); - } + Name = Marshal.PtrToStringAnsi(Marshal.ReadIntPtr(Handle)) ?? + throw new YDotNetException("Failed to read name."); - /// - /// Gets the handle to the native resource. - /// - internal nint Handle { get; } + Value = Marshal.PtrToStringAnsi(Marshal.ReadIntPtr(Handle + MemoryConstants.PointerSize)) ?? + throw new YDotNetException("Failed to read value."); + + // We are done reading and can release the memory. + XmlAttributeChannel.Destroy(Handle); + } /// - /// The name of the attribute. + /// Gets the name of the attribute. /// public string Name { get; } /// - /// The value of the attribute. + /// Gets the value of the attribute. /// public string Value { get; } - /// - public void Dispose() - { - XmlAttributeChannel.Destroy(Handle); - } + /// + /// Gets the handle to the native resource. + /// + internal nint Handle { get; } } diff --git a/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs b/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs index 310f8852..2f5e21bf 100644 --- a/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs +++ b/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs @@ -9,6 +9,8 @@ namespace YDotNet.Document.Types.XmlElements; /// internal class XmlAttributeEnumerator : IEnumerator { + private XmlAttribute? current; + /// /// Initializes a new instance of the class. /// @@ -21,26 +23,26 @@ internal XmlAttributeEnumerator(XmlAttributeIterator iterator) Iterator = iterator; } + /// + public XmlAttribute Current => current!; + + /// + object? IEnumerator.Current => current!; + /// /// Gets the instance that holds the /// used by this enumerator. /// private XmlAttributeIterator Iterator { get; } - /// - public XmlAttribute? Current { get; private set; } - - /// - object? IEnumerator.Current => Current; - /// public bool MoveNext() { var handle = XmlAttributeChannel.IteratorNext(Iterator.Handle); - Current = handle != nint.Zero ? new XmlAttribute(handle) : null; + current = handle != nint.Zero ? new XmlAttribute(handle) : null; - return Current != null; + return current != null; } /// diff --git a/YDotNet/Document/Types/XmlElements/XmlElement.cs b/YDotNet/Document/Types/XmlElements/XmlElement.cs index 75079cad..dd114467 100644 --- a/YDotNet/Document/Types/XmlElements/XmlElement.cs +++ b/YDotNet/Document/Types/XmlElements/XmlElement.cs @@ -117,10 +117,11 @@ public void RemoveAttribute(Transaction transaction, string name) /// /// The transaction that wraps this operation. /// The instance or null if failed. - public XmlAttributeIterator? Iterate(Transaction transaction) + public XmlAttributeIterator Iterate(Transaction transaction) { - return ReferenceAccessor.Access( - new XmlAttributeIterator(XmlElementChannel.AttributeIterator(Handle, transaction.Handle))); + var handle = XmlElementChannel.AttributeIterator(Handle, transaction.Handle); + + return new XmlAttributeIterator(handle.Checked()); } /// @@ -144,10 +145,11 @@ public uint ChildLength(Transaction transaction) /// The transaction that wraps this operation. /// The index that the will be inserted. /// The inserted at the given . - public XmlText? InsertText(Transaction transaction, uint index) + public XmlText InsertText(Transaction transaction, uint index) { - return ReferenceAccessor.Access( - new XmlText(XmlElementChannel.InsertText(Handle, transaction.Handle, index))); + var handle = XmlElementChannel.InsertText(Handle, transaction.Handle, index); + + return new XmlText(handle.Checked()); } /// @@ -158,16 +160,13 @@ public uint ChildLength(Transaction transaction) /// The index that the will be inserted. /// The name (or tag) of the that will be inserted. /// The inserted at the given . - public XmlElement? InsertElement(Transaction transaction, uint index, string name) + public XmlElement InsertElement(Transaction transaction, uint index, string name) { - var nameHandle = MemoryWriter.WriteUtf8String(name); + var elementName = MemoryWriter.WriteUtf8String(name); + var elementHandle = XmlElementChannel.InsertElement(Handle, transaction.Handle, index, elementName); + MemoryWriter.Release(elementName); - var result = ReferenceAccessor.Access( - new XmlElement(XmlElementChannel.InsertElement(Handle, transaction.Handle, index, nameHandle))); - - MemoryWriter.Release(nameHandle); - - return result; + return new XmlElement(elementHandle.Checked()); } /// @@ -192,7 +191,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) { var handle = XmlElementChannel.Get(Handle, transaction.Handle, index); - return ReferenceAccessor.Output(handle, true); + return handle != nint.Zero ? new Output(handle, true) : null; } /// @@ -209,7 +208,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) { var handle = XmlChannel.PreviousSibling(Handle, transaction.Handle); - return ReferenceAccessor.Output(handle, true); + return handle != nint.Zero ? new Output(handle, true) : null; } /// @@ -226,7 +225,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) { var handle = XmlChannel.NextSibling(Handle, transaction.Handle); - return ReferenceAccessor.Output(handle, true); + return handle != nint.Zero ? new Output(handle, true) : null; } /// @@ -240,7 +239,9 @@ public void RemoveRange(Transaction transaction, uint index, uint length) /// public XmlElement? Parent(Transaction transaction) { - return ReferenceAccessor.Access(new XmlElement(XmlElementChannel.Parent(Handle, transaction.Handle))); + var handle = XmlElementChannel.Parent(Handle, transaction.Handle); + + return handle != nint.Zero ? new XmlElement(handle) : null; } /// @@ -256,7 +257,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) { var handle = XmlElementChannel.FirstChild(Handle, transaction.Handle); - return ReferenceAccessor.Output(handle, true); + return handle != nint.Zero ? new Output(handle, true) : null; } /// @@ -267,9 +268,11 @@ public void RemoveRange(Transaction transaction, uint index, uint length) /// /// The transaction that wraps this operation. /// An for this . - public XmlTreeWalker? TreeWalker(Transaction transaction) + public XmlTreeWalker TreeWalker(Transaction transaction) { - return ReferenceAccessor.Access(new XmlTreeWalker(XmlElementChannel.TreeWalker(Handle, transaction.Handle))); + var handle = XmlElementChannel.TreeWalker(Handle, transaction.Handle); + + return new XmlTreeWalker(handle.Checked()); } /// diff --git a/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs b/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs index 173b08eb..95220384 100644 --- a/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs +++ b/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs @@ -1,3 +1,4 @@ +using System.IO; using YDotNet.Document.Types.Events; using YDotNet.Infrastructure; using YDotNet.Native.Types; @@ -9,6 +10,10 @@ namespace YDotNet.Document.Types.XmlTexts.Events; /// public class XmlTextEvent { + private readonly Lazy delta; + private readonly Lazy keys; + private readonly Lazy target; + /// /// Initializes a new instance of the class. /// @@ -16,49 +21,47 @@ public class XmlTextEvent internal XmlTextEvent(nint handle) { Handle = handle; - } - /// - /// Gets the changes that triggered this event. - /// - /// - /// This property can only be accessed during the callback that exposes this instance. - /// Check the documentation of for more information. - /// - public EventDeltas Delta - { - get + delta = new Lazy(() => + { + var deltaHandle = XmlTextChannel.ObserveEventDelta(handle, out var length).Checked(); + return new EventDeltas(deltaHandle, length); + }); + + keys = new Lazy(() => { - var handle = XmlTextChannel.ObserveEventDelta(Handle, out var length); + var keysHandle = XmlTextChannel.ObserveEventKeys(handle, out var length).Checked(); + return new EventKeys(keysHandle, length); + }); - return new EventDeltas(handle, length); - } + target = new Lazy(() => + { + var targetHandle = XmlTextChannel.ObserveEventTarget(handle).Checked(); + return new XmlText(targetHandle); + }); } /// - /// Gets the attributes that changed and triggered this event. + /// Gets the changes that triggered this event. /// - /// - /// This property can only be accessed during the callback that exposes this instance. - /// Check the documentation of for more information. - /// - public EventKeys Keys - { - get - { - var handle = XmlTextChannel.ObserveEventKeys(Handle, out var length); - - return new EventKeys(handle, length); - } - } + /// This property can only be accessed during the callback that exposes this instance. + public EventDeltas Delta => delta.Value; /// - /// Gets the instance that is related to this event. + /// Gets the attributes that changed and triggered this event. /// - public XmlText? Target => ReferenceAccessor.Access(new XmlText(XmlTextChannel.ObserveEventTarget(Handle))); + /// This property can only be accessed during the callback that exposes this instance. + public EventKeys Keys => keys.Value; /// /// Gets the handle to the native resource. /// internal nint Handle { get; } + + /// + /// Gets the instance that is related to this instance. + /// + /// The target of the event. + /// You are responsible to dispose the text, if you use this property. + public XmlText ResolveTarget() => target.Value; } diff --git a/YDotNet/Document/Types/XmlTexts/XmlText.cs b/YDotNet/Document/Types/XmlTexts/XmlText.cs index 1a6e622f..5fb813a8 100644 --- a/YDotNet/Document/Types/XmlTexts/XmlText.cs +++ b/YDotNet/Document/Types/XmlTexts/XmlText.cs @@ -70,7 +70,7 @@ public void Insert(Transaction transaction, uint index, string value, Input? att public void InsertEmbed(Transaction transaction, uint index, Input content, Input? attributes = null) { MemoryWriter.TryToWriteStruct(attributes?.InputNative, out var attributesPointer); - MemoryWriter.TryToWriteStruct(content.InputNative, out var contentPointer); + var contentPointer = MemoryWriter.WriteStruct(content.InputNative); XmlTextChannel.InsertEmbed(Handle, transaction.Handle, index, contentPointer, attributesPointer); MemoryWriter.TryRelease(attributesPointer); MemoryWriter.TryRelease(contentPointer); @@ -130,11 +130,12 @@ public void RemoveAttribute(Transaction transaction, string name) /// Returns a , which can be used to traverse over all attributes. /// /// The transaction that wraps this operation. - /// The instance or null if failed. - public XmlAttributeIterator? Iterate(Transaction transaction) + /// The instance. + public XmlAttributeIterator Iterate(Transaction transaction) { - return ReferenceAccessor.Access( - new XmlAttributeIterator(XmlTextChannel.AttributeIterator(Handle, transaction.Handle))); + var handle = XmlTextChannel.AttributeIterator(Handle, transaction.Handle).Checked(); + + return new XmlAttributeIterator(handle); } /// @@ -198,7 +199,7 @@ public void Format(Transaction transaction, uint index, uint length, Input attri { var handle = XmlChannel.PreviousSibling(Handle, transaction.Handle); - return ReferenceAccessor.Output(handle, true); + return handle != nint.Zero ? new Output(handle, true) : null; } /// @@ -214,7 +215,7 @@ public void Format(Transaction transaction, uint index, uint length, Input attri { var handle = XmlChannel.NextSibling(Handle, transaction.Handle); - return ReferenceAccessor.Output(handle, true); + return handle != nint.Zero ? new Output(handle, true) : null; } /// @@ -255,7 +256,8 @@ public void Unobserve(EventSubscription subscription) /// The in the with the given . public StickyIndex? StickyIndex(Transaction transaction, uint index, StickyAssociationType associationType) { - return ReferenceAccessor.Access( - new StickyIndex(StickyIndexChannel.FromIndex(Handle, transaction.Handle, index, (sbyte) associationType))); + var handle = StickyIndexChannel.FromIndex(Handle, transaction.Handle, index, (sbyte)associationType); + + return handle != nint.Zero ? new StickyIndex(handle) : null; } } diff --git a/YDotNet/Document/UndoManagers/Events/UndoEvent.cs b/YDotNet/Document/UndoManagers/Events/UndoEvent.cs index b13a8cc8..a89cce46 100644 --- a/YDotNet/Document/UndoManagers/Events/UndoEvent.cs +++ b/YDotNet/Document/UndoManagers/Events/UndoEvent.cs @@ -14,7 +14,7 @@ public class UndoEvent /// The origin of the event. /// The entries for inserted content. /// The entries for deleted content. - internal UndoEvent(UndoEventKind kind, byte[] origin, DeleteSet insertions, DeleteSet deletions) + internal UndoEvent(UndoEventKind kind, byte[]? origin, DeleteSet insertions, DeleteSet deletions) { Kind = kind; Origin = origin; @@ -33,7 +33,7 @@ internal UndoEvent(UndoEventKind kind, byte[] origin, DeleteSet insertions, Dele /// /// The is a binary marker. /// - public byte[] Origin { get; } + public byte[]? Origin { get; } /// /// Gets the entries for inserted content. diff --git a/YDotNet/Document/UndoManagers/UndoManager.cs b/YDotNet/Document/UndoManagers/UndoManager.cs index 8c30bd99..d56d7ba7 100644 --- a/YDotNet/Document/UndoManagers/UndoManager.cs +++ b/YDotNet/Document/UndoManagers/UndoManager.cs @@ -171,7 +171,7 @@ public void AddScope(Branch branch) /// The origin to be included in this . public void AddOrigin(byte[] origin) { - UndoManagerChannel.AddOrigin(Handle, (uint) origin.Length, origin); + UndoManagerChannel.AddOrigin(Handle, (uint)origin.Length, origin); } /// @@ -184,6 +184,6 @@ public void AddOrigin(byte[] origin) /// The origin to be removed from this . public void RemoveOrigin(byte[] origin) { - UndoManagerChannel.RemoveOrigin(Handle, (uint) origin.Length, origin); + UndoManagerChannel.RemoveOrigin(Handle, (uint)origin.Length, origin); } } diff --git a/YDotNet/Infrastructure/Extensions.cs b/YDotNet/Infrastructure/Extensions.cs new file mode 100644 index 00000000..3ebfff97 --- /dev/null +++ b/YDotNet/Infrastructure/Extensions.cs @@ -0,0 +1,14 @@ +namespace YDotNet.Infrastructure; + +public static class Extensions +{ + public static nint Checked(this nint input) + { + if (input == nint.Zero) + { + ThrowHelper.InternalError(); + } + + return input; + } +} diff --git a/YDotNet/Infrastructure/MemoryReader.cs b/YDotNet/Infrastructure/MemoryReader.cs index 5820252a..1d9b4ee9 100644 --- a/YDotNet/Infrastructure/MemoryReader.cs +++ b/YDotNet/Infrastructure/MemoryReader.cs @@ -9,7 +9,7 @@ internal static class MemoryReader internal static unsafe byte[] ReadBytes(nint handle, uint length) { var data = new byte[length]; - var stream = new UnmanagedMemoryStream((byte*) handle.ToPointer(), length); + var stream = new UnmanagedMemoryStream((byte*)handle.ToPointer(), length); int bytesRead; do @@ -86,7 +86,6 @@ internal static bool TryReadUtf8String(nint handle, out string? result) if (handle == nint.Zero) { result = null; - return false; } diff --git a/YDotNet/Infrastructure/MemoryWriter.cs b/YDotNet/Infrastructure/MemoryWriter.cs index f3101243..5a729bc9 100644 --- a/YDotNet/Infrastructure/MemoryWriter.cs +++ b/YDotNet/Infrastructure/MemoryWriter.cs @@ -11,7 +11,7 @@ internal static unsafe nint WriteUtf8String(string value) var pointer = Marshal.AllocHGlobal(bytes.Length); using var stream = new UnmanagedMemoryStream( - (byte*) pointer.ToPointer(), + (byte*)pointer.ToPointer(), length: 0, bytes.Length, FileAccess.Write); @@ -63,14 +63,17 @@ internal static nint WriteStructArray(T[] value) } internal static nint WriteStruct(T value) + where T : struct { var handle = Marshal.AllocHGlobal(Marshal.SizeOf(value)); + Marshal.StructureToPtr(value, handle, fDeleteOld: false); return handle; } internal static bool TryToWriteStruct(T? value, out nint handle) + where T : struct { if (value == null) { @@ -79,7 +82,7 @@ internal static bool TryToWriteStruct(T? value, out nint handle) return false; } - handle = WriteStruct(value); + handle = WriteStruct(value.Value); return true; } diff --git a/YDotNet/Infrastructure/ReferenceAccessor.cs b/YDotNet/Infrastructure/ReferenceAccessor.cs deleted file mode 100644 index 42a82e60..00000000 --- a/YDotNet/Infrastructure/ReferenceAccessor.cs +++ /dev/null @@ -1,87 +0,0 @@ -using YDotNet.Document; -using YDotNet.Document.Cells; -using YDotNet.Document.StickyIndexes; -using YDotNet.Document.Transactions; -using YDotNet.Document.Types.Arrays; -using YDotNet.Document.Types.Maps; -using YDotNet.Document.Types.Texts; -using YDotNet.Document.Types.XmlElements; -using YDotNet.Document.Types.XmlElements.Trees; -using YDotNet.Document.Types.XmlTexts; -using Array = YDotNet.Document.Types.Arrays.Array; - -namespace YDotNet.Infrastructure; - -internal static class ReferenceAccessor -{ - public static Transaction? Access(Transaction instance) - { - return Access(instance, instance.Handle); - } - - public static Map? Access(Map instance) - { - return Access(instance, instance.Handle); - } - - public static MapIterator? Access(MapIterator instance) - { - return Access(instance, instance.Handle); - } - - public static Array? Access(Array instance) - { - return Access(instance, instance.Handle); - } - - public static ArrayIterator? Access(ArrayIterator instance) - { - return Access(instance, instance.Handle); - } - - public static Text? Access(Text instance) - { - return Access(instance, instance.Handle); - } - - public static XmlElement? Access(XmlElement instance) - { - return Access(instance, instance.Handle); - } - - public static XmlText? Access(XmlText instance) - { - return Access(instance, instance.Handle); - } - - public static XmlAttributeIterator? Access(XmlAttributeIterator instance) - { - return Access(instance, instance.Handle); - } - - public static XmlTreeWalker? Access(XmlTreeWalker instance) - { - return Access(instance, instance.Handle); - } - - public static Doc? Access(Doc instance) - { - return Access(instance, instance.Handle); - } - - public static Output? Output(nint handle, bool shouldDispose) - { - return handle == nint.Zero ? null : new Output(handle, shouldDispose); - } - - public static StickyIndex? Access(StickyIndex instance) - { - return Access(instance, instance.Handle); - } - - private static T? Access(T instance, nint pointer) - where T : class - { - return pointer == nint.Zero ? null : instance; - } -} diff --git a/YDotNet/Native/Cells/Inputs/InputNative.cs b/YDotNet/Native/Cells/Inputs/InputNative.cs index bfafe83e..cef1d985 100644 --- a/YDotNet/Native/Cells/Inputs/InputNative.cs +++ b/YDotNet/Native/Cells/Inputs/InputNative.cs @@ -4,7 +4,7 @@ namespace YDotNet.Native.Cells.Inputs; // The size has to be 24 here so that the whole data of the input cell is written/read correctly over the C FFI. [StructLayout(LayoutKind.Explicit, Size = 24)] -internal struct InputNative +internal readonly struct InputNative { [field: FieldOffset(offset: 0)] public sbyte Tag { get; } diff --git a/YDotNet/Native/Document/DocChannel.cs b/YDotNet/Native/Document/DocChannel.cs index 61bed8cc..77ea1ada 100644 --- a/YDotNet/Native/Document/DocChannel.cs +++ b/YDotNet/Native/Document/DocChannel.cs @@ -60,7 +60,7 @@ internal static class DocChannel [DllImport( ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ydoc_write_transaction")] - public static extern nint WriteTransaction(nint doc, uint originLength, byte[] origin); + public static extern nint WriteTransaction(nint doc, uint originLength, byte[]? origin); [DllImport( ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ydoc_clear")] diff --git a/YDotNet/Native/Document/DocOptionsNative.cs b/YDotNet/Native/Document/DocOptionsNative.cs index 1b2e76da..4c9a135a 100644 --- a/YDotNet/Native/Document/DocOptionsNative.cs +++ b/YDotNet/Native/Document/DocOptionsNative.cs @@ -5,7 +5,7 @@ namespace YDotNet.Native.Document; [StructLayout(LayoutKind.Sequential)] -internal struct DocOptionsNative : IDisposable +internal readonly struct DocOptionsNative : IDisposable { public ulong Id { get; init; } @@ -31,10 +31,10 @@ public static DocOptionsNative From(DocOptions options) Id = options.Id ?? 0, Guid = guidHandle, CollectionId = collectionIdHandle, - Encoding = (byte) (options.Encoding ?? DocEncoding.Utf8), - SkipGc = (byte) (options.SkipGarbageCollection ?? false ? 1 : 0), - AutoLoad = (byte) (options.AutoLoad ?? false ? 1 : 0), - ShouldLoad = (byte) (options.ShouldLoad ?? false ? 1 : 0) + Encoding = (byte)(options.Encoding ?? DocEncoding.Utf8), + SkipGc = (byte)(options.SkipGarbageCollection ?? false ? 1 : 0), + AutoLoad = (byte)(options.AutoLoad ?? false ? 1 : 0), + ShouldLoad = (byte)(options.ShouldLoad ?? false ? 1 : 0) }; } diff --git a/YDotNet/Native/Document/Events/AfterTransactionEventNative.cs b/YDotNet/Native/Document/Events/AfterTransactionEventNative.cs index 1e07b7fc..d263b434 100644 --- a/YDotNet/Native/Document/Events/AfterTransactionEventNative.cs +++ b/YDotNet/Native/Document/Events/AfterTransactionEventNative.cs @@ -5,7 +5,7 @@ namespace YDotNet.Native.Document.Events; [StructLayout(LayoutKind.Sequential)] -internal struct AfterTransactionEventNative +internal readonly struct AfterTransactionEventNative { public StateVectorNative BeforeState { get; } diff --git a/YDotNet/Native/Document/Events/ClearEventNative.cs b/YDotNet/Native/Document/Events/ClearEventNative.cs index 5f236d9f..147df4da 100644 --- a/YDotNet/Native/Document/Events/ClearEventNative.cs +++ b/YDotNet/Native/Document/Events/ClearEventNative.cs @@ -5,7 +5,7 @@ namespace YDotNet.Native.Document.Events; [StructLayout(LayoutKind.Sequential)] -internal struct ClearEventNative +internal readonly struct ClearEventNative { public Doc Doc { get; init; } diff --git a/YDotNet/Native/Document/Events/SubDocsEventNative.cs b/YDotNet/Native/Document/Events/SubDocsEventNative.cs index 8280d6f3..933bc483 100644 --- a/YDotNet/Native/Document/Events/SubDocsEventNative.cs +++ b/YDotNet/Native/Document/Events/SubDocsEventNative.cs @@ -5,7 +5,7 @@ namespace YDotNet.Native.Document.Events; [StructLayout(LayoutKind.Sequential)] -internal struct SubDocsEventNative +internal readonly struct SubDocsEventNative { public uint AddedLength { get; } diff --git a/YDotNet/Native/Document/Events/UpdateEventNative.cs b/YDotNet/Native/Document/Events/UpdateEventNative.cs index 3ec95690..df45ff52 100644 --- a/YDotNet/Native/Document/Events/UpdateEventNative.cs +++ b/YDotNet/Native/Document/Events/UpdateEventNative.cs @@ -4,7 +4,7 @@ namespace YDotNet.Native.Document.Events; [StructLayout(LayoutKind.Sequential)] -internal struct UpdateEventNative +internal readonly struct UpdateEventNative { public uint Length { get; init; } @@ -22,7 +22,7 @@ public static UpdateEventNative From(uint length, nint data) public UpdateEvent ToUpdateEvent() { var result = new byte[Length]; - Marshal.Copy(Data, result, startIndex: 0, (int) Length); + Marshal.Copy(Data, result, startIndex: 0, (int)Length); return new UpdateEvent(result); } diff --git a/YDotNet/Native/Document/State/DeleteSetNative.cs b/YDotNet/Native/Document/State/DeleteSetNative.cs index cddb0d02..e6edb5d6 100644 --- a/YDotNet/Native/Document/State/DeleteSetNative.cs +++ b/YDotNet/Native/Document/State/DeleteSetNative.cs @@ -4,7 +4,7 @@ namespace YDotNet.Native.Document.State; [StructLayout(LayoutKind.Sequential)] -internal struct DeleteSetNative +internal readonly struct DeleteSetNative { public uint EntriesCount { get; } @@ -20,7 +20,7 @@ public DeleteSet ToDeleteSet() for (var i = 0; i < EntriesCount; i++) { - var clientId = (ulong) Marshal.ReadInt64(ClientIds, i * longSize); + var clientId = (ulong)Marshal.ReadInt64(ClientIds, i * longSize); var rangeNative = Marshal.PtrToStructure(Ranges + i * idRangeSequenceSize); var range = rangeNative.ToIdRanges(); diff --git a/YDotNet/Native/Document/State/IdRangeNative.cs b/YDotNet/Native/Document/State/IdRangeNative.cs index b19a2336..145bd102 100644 --- a/YDotNet/Native/Document/State/IdRangeNative.cs +++ b/YDotNet/Native/Document/State/IdRangeNative.cs @@ -2,7 +2,7 @@ namespace YDotNet.Native.Document.State; -internal struct IdRangeNative +internal readonly struct IdRangeNative { public uint Start { get; } diff --git a/YDotNet/Native/Document/State/IdRangeSequenceNative.cs b/YDotNet/Native/Document/State/IdRangeSequenceNative.cs index 56cfe836..aae623ec 100644 --- a/YDotNet/Native/Document/State/IdRangeSequenceNative.cs +++ b/YDotNet/Native/Document/State/IdRangeSequenceNative.cs @@ -4,7 +4,7 @@ namespace YDotNet.Native.Document.State; [StructLayout(LayoutKind.Sequential)] -internal struct IdRangeSequenceNative +internal readonly struct IdRangeSequenceNative { public uint Length { get; } diff --git a/YDotNet/Native/Document/State/StateVectorNative.cs b/YDotNet/Native/Document/State/StateVectorNative.cs index d9fbe6c3..760f35c7 100644 --- a/YDotNet/Native/Document/State/StateVectorNative.cs +++ b/YDotNet/Native/Document/State/StateVectorNative.cs @@ -3,7 +3,7 @@ namespace YDotNet.Native.Document.State; -internal struct StateVectorNative +internal readonly struct StateVectorNative { public uint EntriesCount { get; } @@ -17,8 +17,8 @@ public StateVector ToStateVector() for (var i = 0; i < EntriesCount; i++) { - var clientId = (ulong) Marshal.ReadInt64(ClientIds, i * sizeof(ulong)); - var clock = (uint) Marshal.ReadInt32(Clocks, i * sizeof(uint)); + var clientId = (ulong)Marshal.ReadInt64(ClientIds, i * sizeof(ulong)); + var clock = (uint)Marshal.ReadInt32(Clocks, i * sizeof(uint)); entries.Add(clientId, clock); } diff --git a/YDotNet/Native/Types/Events/EventChangeNative.cs b/YDotNet/Native/Types/Events/EventChangeNative.cs index 21afd930..ba3b4dd2 100644 --- a/YDotNet/Native/Types/Events/EventChangeNative.cs +++ b/YDotNet/Native/Types/Events/EventChangeNative.cs @@ -7,7 +7,7 @@ namespace YDotNet.Native.Types.Events; [StructLayout(LayoutKind.Sequential)] -internal struct EventChangeNative +internal readonly struct EventChangeNative { public EventChangeTagNative TagNative { get; } @@ -22,23 +22,13 @@ public EventChange ToEventChange() EventChangeTagNative.Add => EventChangeTag.Add, EventChangeTagNative.Remove => EventChangeTag.Remove, EventChangeTagNative.Retain => EventChangeTag.Retain, - _ => throw new NotSupportedException( - $"The value \"{TagNative}\" for {nameof(EventChangeTagNative)} is not supported.") + _ => throw new NotSupportedException($"The value \"{TagNative}\" for {nameof(EventChangeTagNative)} is not supported.") }; - var localValues = Values; - var localLength = Length; - - var attributes = new Lazy>(() => - { - return MemoryReader.TryReadIntPtrArray(localValues, localLength, Marshal.SizeOf())? + var values = + MemoryReader.TryReadIntPtrArray(Values, Length, Marshal.SizeOf())? .Select(x => new Output(x, false)).ToList() ?? new List(); - }); - return new EventChange( - tag, - Length, - attributes - ); + return new EventChange(tag, Length, values); } } diff --git a/YDotNet/Native/Types/Events/EventDeltaNative.cs b/YDotNet/Native/Types/Events/EventDeltaNative.cs index 02920e25..71435312 100644 --- a/YDotNet/Native/Types/Events/EventDeltaNative.cs +++ b/YDotNet/Native/Types/Events/EventDeltaNative.cs @@ -6,7 +6,7 @@ namespace YDotNet.Native.Types.Events; [StructLayout(LayoutKind.Sequential)] -internal struct EventDeltaNative +internal readonly struct EventDeltaNative { public EventDeltaTagNative TagNative { get; } @@ -25,20 +25,17 @@ public EventDelta ToEventDelta() EventDeltaTagNative.Add => EventDeltaTag.Add, EventDeltaTagNative.Remove => EventDeltaTag.Remove, EventDeltaTagNative.Retain => EventDeltaTag.Retain, - _ => throw new NotSupportedException( - $"The value \"{TagNative}\" for {nameof(EventDeltaTagNative)} is not supported.") + _ => throw new NotSupportedException($"The value \"{TagNative}\" for {nameof(EventDeltaTagNative)} is not supported."), }; - var attributes = MemoryReader.TryReadIntPtrArray(Attributes, AttributesLength, size: 16) - ?.Select(x => new EventDeltaAttribute(x)) - .ToArray() ?? - Enumerable.Empty(); + var attributes = + MemoryReader.ReadIntPtrArray(Attributes, AttributesLength, size: 16) + .Select(x => new EventDeltaAttribute(x)).ToArray(); return new EventDelta( tag, Length, - ReferenceAccessor.Output(InsertHandle, false), - attributes - ); + InsertHandle != nint.Zero ? new Output(InsertHandle, false) : null, + attributes); } } diff --git a/YDotNet/Native/Types/Events/EventKeyChangeNative.cs b/YDotNet/Native/Types/Events/EventKeyChangeNative.cs index 78ce6c9e..248a990b 100644 --- a/YDotNet/Native/Types/Events/EventKeyChangeNative.cs +++ b/YDotNet/Native/Types/Events/EventKeyChangeNative.cs @@ -6,7 +6,7 @@ namespace YDotNet.Native.Types.Events; [StructLayout(LayoutKind.Sequential)] -internal struct EventKeyChangeNative +internal readonly struct EventKeyChangeNative { public nint Key { get; } @@ -23,16 +23,14 @@ public EventKeyChange ToEventKeyChange() EventKeyChangeTagNative.Add => EventKeyChangeTag.Add, EventKeyChangeTagNative.Remove => EventKeyChangeTag.Remove, EventKeyChangeTagNative.Update => EventKeyChangeTag.Update, - _ => throw new NotSupportedException( - $"The value \"{TagNative}\" for {nameof(EventKeyChangeTagNative)} is not supported.") + _ => throw new NotSupportedException($"The value \"{TagNative}\" for {nameof(EventKeyChangeTagNative)} is not supported."), }; var result = new EventKeyChange( MemoryReader.ReadUtf8String(Key), tag, - ReferenceAccessor.Output(OldValue, false), - ReferenceAccessor.Output(NewValue, false) - ); + OldValue != nint.Zero ? new Output(OldValue, false) : null, + NewValue != nint.Zero ? new Output(NewValue, false) : null); return result; } diff --git a/YDotNet/Native/Types/Maps/MapChannel.cs b/YDotNet/Native/Types/Maps/MapChannel.cs index d35390eb..244c8cbc 100644 --- a/YDotNet/Native/Types/Maps/MapChannel.cs +++ b/YDotNet/Native/Types/Maps/MapChannel.cs @@ -1,5 +1,4 @@ using System.Runtime.InteropServices; -using YDotNet.Native.Cells.Inputs; namespace YDotNet.Native.Types.Maps; diff --git a/YDotNet/Native/Types/Maps/MapEntryNative.cs b/YDotNet/Native/Types/Maps/MapEntryNative.cs index c2196ac5..a0ad9ce9 100644 --- a/YDotNet/Native/Types/Maps/MapEntryNative.cs +++ b/YDotNet/Native/Types/Maps/MapEntryNative.cs @@ -4,7 +4,7 @@ namespace YDotNet.Native.Types.Maps; [StructLayout(LayoutKind.Sequential, Size = OutputNative.Size + 8)] -internal struct MapEntryNative +internal readonly struct MapEntryNative { public nint Field { get; } } diff --git a/YDotNet/Native/UndoManager/Events/UndoEventNative.cs b/YDotNet/Native/UndoManager/Events/UndoEventNative.cs index 7cfa24be..fec91a40 100644 --- a/YDotNet/Native/UndoManager/Events/UndoEventNative.cs +++ b/YDotNet/Native/UndoManager/Events/UndoEventNative.cs @@ -22,8 +22,7 @@ public UndoEvent ToUndoEvent() { UndoEventKindNative.Undo => UndoEventKind.Undo, UndoEventKindNative.Redo => UndoEventKind.Redo, - _ => throw new NotSupportedException( - $"The value \"{KindNative}\" for {nameof(UndoEventKindNative)} is not supported.") + _ => throw new NotSupportedException($"The value \"{KindNative}\" for {nameof(UndoEventKindNative)} is not supported."), }; var origin = MemoryReader.TryReadBytes(Origin, OriginLength); diff --git a/YDotNet/ThrowHelper.cs b/YDotNet/ThrowHelper.cs new file mode 100644 index 00000000..a6f2b1b8 --- /dev/null +++ b/YDotNet/ThrowHelper.cs @@ -0,0 +1,24 @@ +namespace YDotNet; + +public static class ThrowHelper +{ + public static void InternalError() + { + throw new YDotNetException("Operation failed. Lib returns null pointer without further details."); + } + + public static void PendingTransaction() + { + throw new YDotNetException("Failed to open a transaction. Probably because another transaction is still pending."); + } + + public static void YDotnet(string message) + { + throw new YDotNetException(message); + } + + public static void ArgumentException(string message, string paramName) + { + throw new ArgumentException(message, paramName); + } +} diff --git a/YDotNet/YDotNet.csproj b/YDotNet/YDotNet.csproj index 2b78e0f7..ee6edb0b 100644 --- a/YDotNet/YDotNet.csproj +++ b/YDotNet/YDotNet.csproj @@ -7,6 +7,10 @@ true + + + + @@ -18,18 +22,4 @@ - - - Always - - - Always - - - - - - WINDOWS - - diff --git a/YDotNet/YDotNetException.cs b/YDotNet/YDotNetException.cs new file mode 100644 index 00000000..b4cbb9f8 --- /dev/null +++ b/YDotNet/YDotNetException.cs @@ -0,0 +1,26 @@ +using System.Runtime.Serialization; + +namespace YDotNet; + +[Serializable] +public class YDotNetException : Exception +{ + public YDotNetException() + { + } + + public YDotNetException(string message) + : base(message) + { + } + + public YDotNetException(string message, Exception inner) + : base(message, inner) + { + } + + protected YDotNetException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } +} diff --git a/YDotNet/stylecop.json b/stylecop.json similarity index 87% rename from YDotNet/stylecop.json rename to stylecop.json index 42fb1f8e..0c619806 100644 --- a/YDotNet/stylecop.json +++ b/stylecop.json @@ -7,6 +7,9 @@ "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", "settings": { + "orderingRules": { + "usingDirectivesPlacement": "outsideNamespace" + }, "documentationRules": { "companyName": "PlaceholderCompany" } From 6bf613ab6eb692818b32a370a7184b88f0d64167 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 11 Oct 2023 18:41:46 +0200 Subject: [PATCH 086/186] A lot of stuff. --- .editorconfig | 2 + .../Arrays/InsertRangeTests.cs | 2 +- .../YDotNet.Tests.Unit/Arrays/ObserveTests.cs | 5 +- .../Arrays/UnobserveTests.cs | 2 +- .../Branches/ArrayObserveDeepTests.cs | 12 +- .../Branches/ArrayUnobserveDeepTests.cs | 2 +- .../Branches/MapObserveDeepTests.cs | 10 +- .../Branches/MapUnobserveDeepTests.cs | 2 +- .../Branches/TextObserveDeepTests.cs | 1 - .../Branches/TextUnobserveDeepTests.cs | 2 +- .../Branches/XmlElementObserveDeepTests.cs | 5 - .../Branches/XmlElementUnobserveDeepTests.cs | 2 +- .../Branches/XmlTextUnobserveDeepTests.cs | 2 +- .../Document/AfterTransactionTests.cs | 2 +- .../YDotNet.Tests.Unit/Document/ClearTests.cs | 2 +- .../Document/SubDocsTests.cs | 4 +- .../Document/UpdatesV1Tests.cs | 2 +- .../Document/UpdatesV2Tests.cs | 2 +- Tests/YDotNet.Tests.Unit/Maps/GetTests.cs | 12 +- Tests/YDotNet.Tests.Unit/Maps/ObserveTests.cs | 7 +- .../YDotNet.Tests.Unit/Maps/UnobserveTests.cs | 2 +- Tests/YDotNet.Tests.Unit/Program.cs | 82 +++++++++ Tests/YDotNet.Tests.Unit/Properties.cs | 3 + .../Texts/InsertEmbedTests.cs | 2 +- .../YDotNet.Tests.Unit/Texts/ObserveTests.cs | 2 +- .../Texts/UnobserveTests.cs | 2 +- .../Transactions/GetTests.cs | 19 +- .../UndoManagers/ObservePoppedTests.cs | 2 + .../UndoManagers/UnobserveAddedTests.cs | 2 +- .../UndoManagers/UnobservePoppedTests.cs | 2 +- .../XmlElements/FirstChildTests.cs | 8 +- .../XmlElements/GetTests.cs | 4 +- .../XmlElements/InsertAttributeTests.cs | 2 +- .../XmlElements/NextSiblingTests.cs | 10 +- .../XmlElements/ObserveTests.cs | 10 +- .../XmlElements/ParentTests.cs | 6 +- .../XmlElements/PreviousSiblingTests.cs | 10 +- .../XmlElements/RemoveAttributeTests.cs | 2 +- .../XmlElements/StringTests.cs | 8 +- .../XmlElements/TagTests.cs | 4 +- .../XmlElements/TreeWalkerTests.cs | 26 +-- .../XmlElements/UnobserveTests.cs | 2 +- .../XmlTexts/NextSiblingTests.cs | 10 +- .../XmlTexts/ObserveTests.cs | 3 +- .../XmlTexts/PreviousSiblingTests.cs | 10 +- .../XmlTexts/UnobserveTests.cs | 2 +- .../YDotNet - Backup.Tests.Unit.csproj | 21 --- .../YDotNet.Tests.Unit.csproj | 1 + YDotNet/Document/Cells/Input.cs | 82 +++++---- YDotNet/Document/Cells/InputEmpty.cs | 24 --- YDotNet/Document/Cells/InputPointer.cs | 30 ---- YDotNet/Document/Cells/InputPointerArray.cs | 30 ---- YDotNet/Document/Cells/JsonArray.cs | 30 ++++ YDotNet/Document/Cells/JsonObject.cs | 33 ++++ YDotNet/Document/Cells/Output.cs | 90 +++------- YDotNet/Document/Doc.cs | 166 ++++++++---------- YDotNet/Document/Events/EventSubscription.cs | 21 --- YDotNet/Document/Events/EventSubscriptions.cs | 45 +++++ YDotNet/Document/Options/DocOptions.cs | 2 +- YDotNet/Document/StickyIndexes/StickyIndex.cs | 29 +-- YDotNet/Document/Transactions/Transaction.cs | 79 ++++----- YDotNet/Document/Types/Arrays/Array.cs | 37 ++-- .../Document/Types/Arrays/ArrayEnumerator.cs | 23 +-- .../Document/Types/Arrays/ArrayIterator.cs | 21 +-- .../Types/Arrays/Events/ArrayEvent.cs | 39 ++-- YDotNet/Document/Types/Branches/Branch.cs | 40 ++--- YDotNet/Document/Types/Events/EventChanges.cs | 41 ++--- YDotNet/Document/Types/Events/EventDelta.cs | 4 +- .../Types/Events/EventDeltaAttribute.cs | 13 +- YDotNet/Document/Types/Events/EventDeltas.cs | 42 ++--- YDotNet/Document/Types/Events/EventKeys.cs | 41 ++--- YDotNet/Document/Types/Events/EventPath.cs | 34 ++-- .../Document/Types/Events/EventPathSegment.cs | 3 +- .../Document/Types/Maps/Events/MapEvent.cs | 29 ++- YDotNet/Document/Types/Maps/Map.cs | 56 +++--- YDotNet/Document/Types/Maps/MapEntry.cs | 36 ++-- YDotNet/Document/Types/Maps/MapEnumerator.cs | 34 ++-- YDotNet/Document/Types/Maps/MapIterator.cs | 23 ++- .../Document/Types/Texts/Events/TextEvent.cs | 25 +-- YDotNet/Document/Types/Texts/Text.cs | 59 +++---- YDotNet/Document/Types/Texts/TextChunk.cs | 17 +- YDotNet/Document/Types/Texts/TextChunks.cs | 41 +++-- .../XmlElements/Events/XmlElementEvent.cs | 28 +-- .../Types/XmlElements/Trees/XmlTreeWalker.cs | 22 +-- .../Trees/XmlTreeWalkerEnumerator.cs | 13 +- .../XmlElements/XmlAttributeEnumerator.cs | 11 +- .../Types/XmlElements/XmlAttributeIterator.cs | 20 +-- .../Document/Types/XmlElements/XmlElement.cs | 101 +++++------ .../Types/XmlTexts/Events/XmlTextEvent.cs | 30 ++-- YDotNet/Document/Types/XmlTexts/XmlText.cs | 84 ++++----- YDotNet/Document/UndoManagers/UndoManager.cs | 66 ++++--- YDotNet/Infrastructure/IResourceOwner.cs | 6 + YDotNet/Infrastructure/MemoryReader.cs | 28 +-- YDotNet/Infrastructure/MemoryWriter.cs | 116 ++++++------ YDotNet/Infrastructure/Resource.cs | 40 +++++ .../UnmanagedCollectionResource.cs | 36 ++++ YDotNet/Infrastructure/UnmanagedResource.cs | 12 ++ YDotNet/Native/Cells/Inputs/InputChannel.cs | 73 ++++++-- YDotNet/Native/Cells/Outputs/OutputChannel.cs | 61 +++++-- YDotNet/Native/Document/DocChannel.cs | 102 ++++++++--- YDotNet/Native/Document/DocOptionsNative.cs | 22 +-- .../Native/StickyIndex/StickyIndexChannel.cs | 20 ++- .../Native/Transaction/TransactionChannel.cs | 29 ++- YDotNet/Native/Types/ArrayChannel.cs | 59 +++++-- YDotNet/Native/Types/BinaryChannel.cs | 5 +- .../Native/Types/Branches/BranchChannel.cs | 15 +- YDotNet/Native/Types/ChunksChannel.cs | 5 +- YDotNet/Native/Types/EventChannel.cs | 8 +- .../Native/Types/Events/EventChangeNative.cs | 6 +- .../Native/Types/Events/EventDeltaNative.cs | 20 ++- .../Types/Events/EventKeyChangeNative.cs | 20 ++- YDotNet/Native/Types/Maps/MapChannel.cs | 67 +++++-- YDotNet/Native/Types/PathChannel.cs | 5 +- YDotNet/Native/Types/StringChannel.cs | 5 +- YDotNet/Native/Types/Texts/TextChannel.cs | 56 ++++-- YDotNet/Native/Types/XmlAttributeChannel.cs | 12 +- YDotNet/Native/Types/XmlChannel.cs | 8 +- YDotNet/Native/Types/XmlElementChannel.cs | 84 ++++++--- YDotNet/Native/Types/XmlTextChannel.cs | 65 +++++-- .../Native/UndoManager/UndoManagerChannel.cs | 37 +++- .../UndoManager/UndoManagerOptionsNative.cs | 9 +- 121 files changed, 1669 insertions(+), 1393 deletions(-) create mode 100644 Tests/YDotNet.Tests.Unit/Program.cs create mode 100644 Tests/YDotNet.Tests.Unit/Properties.cs delete mode 100644 Tests/YDotNet.Tests.Unit/YDotNet - Backup.Tests.Unit.csproj delete mode 100644 YDotNet/Document/Cells/InputEmpty.cs delete mode 100644 YDotNet/Document/Cells/InputPointer.cs delete mode 100644 YDotNet/Document/Cells/InputPointerArray.cs create mode 100644 YDotNet/Document/Cells/JsonArray.cs create mode 100644 YDotNet/Document/Cells/JsonObject.cs delete mode 100644 YDotNet/Document/Events/EventSubscription.cs create mode 100644 YDotNet/Document/Events/EventSubscriptions.cs create mode 100644 YDotNet/Infrastructure/IResourceOwner.cs create mode 100644 YDotNet/Infrastructure/Resource.cs create mode 100644 YDotNet/Infrastructure/UnmanagedCollectionResource.cs create mode 100644 YDotNet/Infrastructure/UnmanagedResource.cs diff --git a/.editorconfig b/.editorconfig index 2f6e641f..1d6fd374 100644 --- a/.editorconfig +++ b/.editorconfig @@ -31,6 +31,8 @@ dotnet_diagnostic.SA1101.severity = none # Prefix local calls with this # Ordering rules dotnet_diagnostic.SA1200.severity = suggestion # Using directives should be placed correctly +dotnet_diagnostic.SA1201.severity = none +dotnet_diagnostic.SA1202.severity = none dotnet_diagnostic.SA1204.severity = suggestion # Static elements should appear before instance elements # Maintainability rules diff --git a/Tests/YDotNet.Tests.Unit/Arrays/InsertRangeTests.cs b/Tests/YDotNet.Tests.Unit/Arrays/InsertRangeTests.cs index 9aaabed8..2cf77042 100644 --- a/Tests/YDotNet.Tests.Unit/Arrays/InsertRangeTests.cs +++ b/Tests/YDotNet.Tests.Unit/Arrays/InsertRangeTests.cs @@ -15,7 +15,7 @@ public void InsertEmptyRange() // Act var transaction = doc.WriteTransaction(); - array.InsertRange(transaction, index: 0, Enumerable.Empty()); + array.InsertRange(transaction, index: 0); transaction.Commit(); // Assert diff --git a/Tests/YDotNet.Tests.Unit/Arrays/ObserveTests.cs b/Tests/YDotNet.Tests.Unit/Arrays/ObserveTests.cs index 791542b3..37e168ca 100644 --- a/Tests/YDotNet.Tests.Unit/Arrays/ObserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/Arrays/ObserveTests.cs @@ -15,7 +15,7 @@ public void ObserveHasTarget() var doc = new Doc(); var array = doc.Array("array"); Array? target = null; - array.Observe(e => target = e.ResolveTarget()); + array.Observe(e => target = e.Target); // Act var transaction = doc.WriteTransaction(); @@ -42,7 +42,6 @@ public void ObserveHasDeltasWhenAdded() transaction.Commit(); // Assert - Assert.That(subscription.Id, Is.EqualTo(expected: 0L)); Assert.That(eventChanges, Is.Not.Null); Assert.That(eventChanges.Count, Is.EqualTo(expected: 1)); Assert.That(eventChanges.First().Tag, Is.EqualTo(EventChangeTag.Add)); @@ -76,7 +75,6 @@ public void ObserveHasDeltasWhenRemoved() transaction.Commit(); // Assert - Assert.That(subscription.Id, Is.EqualTo(expected: 0L)); Assert.That(eventChanges, Is.Not.Null); Assert.That(eventChanges.Count, Is.EqualTo(expected: 1)); Assert.That(eventChanges.First().Tag, Is.EqualTo(EventChangeTag.Remove)); @@ -110,7 +108,6 @@ public void ObserveHasDeltasWhenMoved() transaction.Commit(); // Assert - Assert.That(subscription.Id, Is.EqualTo(expected: 0L)); Assert.That(eventChanges, Is.Not.Null); Assert.That(eventChanges.Count, Is.EqualTo(expected: 3)); diff --git a/Tests/YDotNet.Tests.Unit/Arrays/UnobserveTests.cs b/Tests/YDotNet.Tests.Unit/Arrays/UnobserveTests.cs index 63e10539..fb3f364d 100644 --- a/Tests/YDotNet.Tests.Unit/Arrays/UnobserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/Arrays/UnobserveTests.cs @@ -24,7 +24,7 @@ public void TriggersWhenArrayChangedUntilUnobserved() Assert.That(called, Is.EqualTo(expected: 1)); // Act - array.Unobserve(subscription); + subscription.Dispose(); transaction = doc.WriteTransaction(); array.InsertRange(transaction, index: 0, new[] { Input.Long(value: -420L) }); diff --git a/Tests/YDotNet.Tests.Unit/Branches/ArrayObserveDeepTests.cs b/Tests/YDotNet.Tests.Unit/Branches/ArrayObserveDeepTests.cs index 34991fa6..940b8af5 100644 --- a/Tests/YDotNet.Tests.Unit/Branches/ArrayObserveDeepTests.cs +++ b/Tests/YDotNet.Tests.Unit/Branches/ArrayObserveDeepTests.cs @@ -24,10 +24,10 @@ public void ObserveDeepHasPathWhenAdded() Input.Map(new Dictionary()) }); - var map2 = array1.Get(transaction, index: 3).ResolveMap(); + var map2 = array1.Get(transaction, index: 3).Map; map2.Insert(transaction, "array-3", Input.Array(Array.Empty())); - var array3 = map2.Get(transaction, "array-3").ResolveArray(); + var array3 = map2.Get(transaction, "array-3").Array; array3.InsertRange( transaction, index: 0, new[] { @@ -64,7 +64,7 @@ public void ObserveDeepHasPathWhenAdded() Assert.That(pathSegments.ElementAt(index: 1).Key, Is.EqualTo("array-3")); Assert.That(pathSegments.ElementAt(index: 1).Index, Is.Null); - array1.UnobserveDeep(subscription); + subscription.Dispose(); } [Test] @@ -84,7 +84,7 @@ public void ObserveDeepHasPathWhenRemoved() Input.Boolean(value: false) }); - var array2 = array1.Get(transaction, index: 2).ResolveArray(); + var array2 = array1.Get(transaction, index: 2).Array; array2.InsertRange( transaction, index: 0, new[] { @@ -94,7 +94,7 @@ public void ObserveDeepHasPathWhenRemoved() Input.Array(Array.Empty()) }); - var array3 = array2.Get(transaction, index: 3).ResolveArray(); + var array3 = array2.Get(transaction, index: 3).Array; array3.InsertRange( transaction, index: 0, new[] { @@ -130,6 +130,6 @@ public void ObserveDeepHasPathWhenRemoved() Assert.That(pathSegments.ElementAt(index: 1).Index, Is.EqualTo(expected: 3)); Assert.That(pathSegments.ElementAt(index: 1).Key, Is.Null); - array1.UnobserveDeep(subscription); + subscription.Dispose(); } } diff --git a/Tests/YDotNet.Tests.Unit/Branches/ArrayUnobserveDeepTests.cs b/Tests/YDotNet.Tests.Unit/Branches/ArrayUnobserveDeepTests.cs index 6a8177f8..07463e52 100644 --- a/Tests/YDotNet.Tests.Unit/Branches/ArrayUnobserveDeepTests.cs +++ b/Tests/YDotNet.Tests.Unit/Branches/ArrayUnobserveDeepTests.cs @@ -24,7 +24,7 @@ public void TriggersWhenArrayChangedUntilUnobserved() Assert.That(called, Is.EqualTo(expected: 1)); // Act - array.UnobserveDeep(subscription); + subscription.Dispose(); transaction = doc.WriteTransaction(); array.InsertRange(transaction, index: 0, new[] { Input.Long(value: -420L) }); diff --git a/Tests/YDotNet.Tests.Unit/Branches/MapObserveDeepTests.cs b/Tests/YDotNet.Tests.Unit/Branches/MapObserveDeepTests.cs index 3558a38f..a48cb4a3 100644 --- a/Tests/YDotNet.Tests.Unit/Branches/MapObserveDeepTests.cs +++ b/Tests/YDotNet.Tests.Unit/Branches/MapObserveDeepTests.cs @@ -30,7 +30,6 @@ public void ObserveDeepHasPathWhenAdded() transaction.Commit(); // Assert - Assert.That(subscription.Id, Is.EqualTo(expected: 0L)); AssertPath(called, pathSegments, "map-2", "map-3"); } @@ -60,7 +59,6 @@ public void ObserveDeepHasPathWhenRemovedByKey() transaction.Commit(); // Assert - Assert.That(subscription.Id, Is.EqualTo(expected: 0L)); AssertPath(called, pathSegments, "map-2", "map-3"); } @@ -90,7 +88,6 @@ public void ObserveDeepHasPathWhenRemovedAll() transaction.Commit(); // Assert - Assert.That(subscription.Id, Is.EqualTo(expected: 0L)); AssertPath(called, pathSegments, "map-2", "map-3"); } @@ -120,7 +117,6 @@ public void ObserveDeepHasPathWhenUpdated() transaction.Commit(); // Assert - Assert.That(subscription.Id, Is.EqualTo(expected: 0L)); AssertPath(called, pathSegments, "map-2", "map-3"); } @@ -156,7 +152,6 @@ public void ObserveDeepHasPathWhenAddedAndRemovedAndUpdated() transaction.Commit(); // Assert - Assert.That(subscription.Id, Is.EqualTo(expected: 0L)); AssertPath(called, pathSegments, "map-2", "map-3"); } @@ -186,7 +181,6 @@ public void ObserveDeepHasMultipleEventsForMultipleInstanceChanges() transaction.Commit(); // Assert - Assert.That(subscription.Id, Is.EqualTo(expected: 0L)); AssertPath(called, mapEvents.ElementAt(index: 0)); AssertPath(called, mapEvents.ElementAt(index: 1), "map-2"); AssertPath(called, mapEvents.ElementAt(index: 2), "map-2", "map-3"); @@ -200,10 +194,10 @@ private static (Doc, Map, Map, Map) ArrangeDoc() var transaction = doc.WriteTransaction(); map1.Insert(transaction, "map-2", Input.Map(new Dictionary())); - var map2 = map1.Get(transaction, "map-2").ResolveMap(); + var map2 = map1.Get(transaction, "map-2").Map; map2.Insert(transaction, "map-3", Input.Map(new Dictionary())); - var map3 = map2.Get(transaction, "map-3").ResolveMap(); + var map3 = map2.Get(transaction, "map-3").Map; transaction.Commit(); return (doc, map1, map2, map3); diff --git a/Tests/YDotNet.Tests.Unit/Branches/MapUnobserveDeepTests.cs b/Tests/YDotNet.Tests.Unit/Branches/MapUnobserveDeepTests.cs index ea677235..4b57e8bf 100644 --- a/Tests/YDotNet.Tests.Unit/Branches/MapUnobserveDeepTests.cs +++ b/Tests/YDotNet.Tests.Unit/Branches/MapUnobserveDeepTests.cs @@ -24,7 +24,7 @@ public void TriggersWhenMapChangedUntilUnobserved() Assert.That(called, Is.EqualTo(expected: 1)); // Act - map.UnobserveDeep(subscription); + subscription.Dispose(); transaction = doc.WriteTransaction(); map.Insert(transaction, "value2", Input.Long(value: -420L)); diff --git a/Tests/YDotNet.Tests.Unit/Branches/TextObserveDeepTests.cs b/Tests/YDotNet.Tests.Unit/Branches/TextObserveDeepTests.cs index 45d5b9a2..cea21446 100644 --- a/Tests/YDotNet.Tests.Unit/Branches/TextObserveDeepTests.cs +++ b/Tests/YDotNet.Tests.Unit/Branches/TextObserveDeepTests.cs @@ -52,7 +52,6 @@ public void ObserveDeepHasPathWhenAdded() transaction.Commit(); // Assert - Assert.That(subscription.Id, Is.EqualTo(expected: 0L)); Assert.That(pathSegments.Count(), Is.EqualTo(expected: 1)); } } diff --git a/Tests/YDotNet.Tests.Unit/Branches/TextUnobserveDeepTests.cs b/Tests/YDotNet.Tests.Unit/Branches/TextUnobserveDeepTests.cs index 2f4b4453..b135486b 100644 --- a/Tests/YDotNet.Tests.Unit/Branches/TextUnobserveDeepTests.cs +++ b/Tests/YDotNet.Tests.Unit/Branches/TextUnobserveDeepTests.cs @@ -23,7 +23,7 @@ public void TriggersWhenChangedUntilUnobserved() Assert.That(called, Is.EqualTo(expected: 1)); // Act - text.UnobserveDeep(subscription); + subscription.Dispose(); transaction = doc.WriteTransaction(); text.Insert(transaction, index: 0, "Hello, "); diff --git a/Tests/YDotNet.Tests.Unit/Branches/XmlElementObserveDeepTests.cs b/Tests/YDotNet.Tests.Unit/Branches/XmlElementObserveDeepTests.cs index 4624d2b9..3007d4ec 100644 --- a/Tests/YDotNet.Tests.Unit/Branches/XmlElementObserveDeepTests.cs +++ b/Tests/YDotNet.Tests.Unit/Branches/XmlElementObserveDeepTests.cs @@ -24,7 +24,6 @@ public void ObserveDeepHasPathWhenAddedXmlElementsAndTexts() transaction.Commit(); // Assert - Assert.That(subscription.Id, Is.EqualTo(expected: 0L)); Assert.That(pathSegments, Is.Not.Null); Assert.That(pathSegments.Count(), Is.EqualTo(expected: 2)); Assert.That(pathSegments.All(x => x.Tag == EventPathSegmentTag.Index), Is.True); @@ -38,7 +37,6 @@ public void ObserveDeepHasPathWhenAddedXmlElementsAndTexts() transaction.Commit(); // Assert - Assert.That(subscription.Id, Is.EqualTo(expected: 0L)); Assert.That(pathSegments, Is.Not.Null); Assert.That(pathSegments.Count(), Is.EqualTo(expected: 2)); Assert.That(pathSegments.All(x => x.Tag == EventPathSegmentTag.Index), Is.True); @@ -64,7 +62,6 @@ public void ObserveDeepHasPathWhenAddedAttributes() transaction.Commit(); // Assert - Assert.That(subscription.Id, Is.EqualTo(expected: 0L)); Assert.That(pathSegments, Is.Not.Null); Assert.That(pathSegments.Count(), Is.EqualTo(expected: 2)); Assert.That(pathSegments.All(x => x.Tag == EventPathSegmentTag.Index), Is.True); @@ -94,7 +91,6 @@ public void ObserveDeepHasPathWhenUpdatedAttributes() transaction.Commit(); // Assert - Assert.That(subscription.Id, Is.EqualTo(expected: 0L)); Assert.That(pathSegments, Is.Not.Null); Assert.That(pathSegments.Count(), Is.EqualTo(expected: 2)); Assert.That(pathSegments.All(x => x.Tag == EventPathSegmentTag.Index), Is.True); @@ -124,7 +120,6 @@ public void ObserveDeepHasPathWhenRemovedAttributes() transaction.Commit(); // Assert - Assert.That(subscription.Id, Is.EqualTo(expected: 0L)); Assert.That(pathSegments, Is.Not.Null); Assert.That(pathSegments.Count(), Is.EqualTo(expected: 2)); Assert.That(pathSegments.All(x => x.Tag == EventPathSegmentTag.Index), Is.True); diff --git a/Tests/YDotNet.Tests.Unit/Branches/XmlElementUnobserveDeepTests.cs b/Tests/YDotNet.Tests.Unit/Branches/XmlElementUnobserveDeepTests.cs index 5cc49103..a4b6d3fb 100644 --- a/Tests/YDotNet.Tests.Unit/Branches/XmlElementUnobserveDeepTests.cs +++ b/Tests/YDotNet.Tests.Unit/Branches/XmlElementUnobserveDeepTests.cs @@ -23,7 +23,7 @@ public void TriggersWhenMapChangedUntilUnobserved() Assert.That(called, Is.EqualTo(expected: 1)); // Act - xmlElement.UnobserveDeep(subscription); + subscription.Dispose(); transaction = doc.WriteTransaction(); xmlElement.InsertText(transaction, index: 0); diff --git a/Tests/YDotNet.Tests.Unit/Branches/XmlTextUnobserveDeepTests.cs b/Tests/YDotNet.Tests.Unit/Branches/XmlTextUnobserveDeepTests.cs index f8986be7..5f641f0c 100644 --- a/Tests/YDotNet.Tests.Unit/Branches/XmlTextUnobserveDeepTests.cs +++ b/Tests/YDotNet.Tests.Unit/Branches/XmlTextUnobserveDeepTests.cs @@ -23,7 +23,7 @@ public void TriggersWhenChangedUntilUnobserved() Assert.That(called, Is.EqualTo(expected: 1)); // Act - xmlText.UnobserveDeep(subscription); + subscription.Dispose(); transaction = doc.WriteTransaction(); xmlText.Insert(transaction, index: 0, "Hello, "); diff --git a/Tests/YDotNet.Tests.Unit/Document/AfterTransactionTests.cs b/Tests/YDotNet.Tests.Unit/Document/AfterTransactionTests.cs index 98df799f..82532842 100644 --- a/Tests/YDotNet.Tests.Unit/Document/AfterTransactionTests.cs +++ b/Tests/YDotNet.Tests.Unit/Document/AfterTransactionTests.cs @@ -50,7 +50,7 @@ public void TriggersWhenTransactionIsCommittedUntilUnobserve() // Act afterTransactionEvent = null; - doc.UnobserveAfterTransaction(subscription); + subscription.Dispose(); transaction = doc.WriteTransaction(); text.Insert(transaction, index: 0, "The "); diff --git a/Tests/YDotNet.Tests.Unit/Document/ClearTests.cs b/Tests/YDotNet.Tests.Unit/Document/ClearTests.cs index 11afdcfa..e3682b9d 100644 --- a/Tests/YDotNet.Tests.Unit/Document/ClearTests.cs +++ b/Tests/YDotNet.Tests.Unit/Document/ClearTests.cs @@ -58,7 +58,7 @@ public void DoesNotTriggerWhenUnobserved() }); // Act - doc.UnobserveClear(subscription); + subscription.Dispose(); doc.Clear(); // Assert diff --git a/Tests/YDotNet.Tests.Unit/Document/SubDocsTests.cs b/Tests/YDotNet.Tests.Unit/Document/SubDocsTests.cs index e58481d3..5bc88403 100644 --- a/Tests/YDotNet.Tests.Unit/Document/SubDocsTests.cs +++ b/Tests/YDotNet.Tests.Unit/Document/SubDocsTests.cs @@ -39,7 +39,7 @@ public void TriggersWhenAddingSubDocsUntilUnobserved() // Act subDocsEvent = null; - doc.UnobserveSubDocs(subscription); + subscription.Dispose(); AddSubDoc(doc, map, "sub-doc-2"); @@ -82,7 +82,7 @@ public void TriggersWhenRemovingSubDocsUntilUnobserved() // Act subDocsEvent = null; - doc.UnobserveSubDocs(subscription); + subscription.Dispose(); RemoveSubDoc(doc, map, "sub-doc-2"); diff --git a/Tests/YDotNet.Tests.Unit/Document/UpdatesV1Tests.cs b/Tests/YDotNet.Tests.Unit/Document/UpdatesV1Tests.cs index 0bc2e8f1..07b80234 100644 --- a/Tests/YDotNet.Tests.Unit/Document/UpdatesV1Tests.cs +++ b/Tests/YDotNet.Tests.Unit/Document/UpdatesV1Tests.cs @@ -45,7 +45,7 @@ public void TriggersWhenTransactionIsCommittedUntilUnobserve() // Act data = null; - doc.UnobserveUpdatesV1(subscription); + subscription.Dispose(); transaction = doc.WriteTransaction(); text.Insert(transaction, index: 0, "The "); diff --git a/Tests/YDotNet.Tests.Unit/Document/UpdatesV2Tests.cs b/Tests/YDotNet.Tests.Unit/Document/UpdatesV2Tests.cs index c6649f46..1339876e 100644 --- a/Tests/YDotNet.Tests.Unit/Document/UpdatesV2Tests.cs +++ b/Tests/YDotNet.Tests.Unit/Document/UpdatesV2Tests.cs @@ -45,7 +45,7 @@ public void TriggersWhenTransactionIsCommittedUntilUnobserve() // Act data = null; - doc.UnobserveUpdatesV2(subscription); + subscription.Dispose(); transaction = doc.WriteTransaction(); text.Insert(transaction, index: 0, "The "); diff --git a/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs b/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs index 222ff87c..dd5a9071 100644 --- a/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs +++ b/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs @@ -118,7 +118,7 @@ public void GetCollection() // Assert //Assert.That(value1, Is.EqualTo(new byte[] { 2, 4, 6, 9 })); - Assert.That(value1.Length, Is.EqualTo(expected: 2)); + Assert.That(value1.Count, Is.EqualTo(expected: 2)); Assert.That(value1[0].Long, Is.EqualTo(expected: 2469)); Assert.That(value1[1].Long, Is.EqualTo(expected: -420L)); Assert.That(value2.Type, Is.EqualTo(OutputType.Bool)); @@ -201,7 +201,7 @@ public void GetText() ); // Act - var value1 = map.Get(transaction, "value1").ResolveText(); + var value1 = map.Get(transaction, "value1").Text; var value2 = map.Get(transaction, "value2"); // Assert @@ -225,7 +225,7 @@ public void GetArray() ); // Act - var value1 = map.Get(transaction, "value1").ResolveArray(); + var value1 = map.Get(transaction, "value1").Array; var value2 = map.Get(transaction, "value2"); // Assert @@ -249,7 +249,7 @@ public void GetMap() ); // Act - var value1 = map.Get(transaction, "value1").ResolveMap(); + var value1 = map.Get(transaction, "value1").Map; var value2 = map.Get(transaction, "value2"); // Assert @@ -270,7 +270,7 @@ public void GetXmlElement() ); // Act - var value1 = map.Get(transaction, "value1").ResolveXmlElement(); + var value1 = map.Get(transaction, "value1").XmlElement; var value2 = map.Get(transaction, "value2"); // Assert @@ -289,7 +289,7 @@ public void GetXmlText() ); // Act - var value1 = map.Get(transaction, "value1").ResolveXmlText(); + var value1 = map.Get(transaction, "value1").XmlText; var value2 = map.Get(transaction, "value2"); // Assert diff --git a/Tests/YDotNet.Tests.Unit/Maps/ObserveTests.cs b/Tests/YDotNet.Tests.Unit/Maps/ObserveTests.cs index edbf8ef1..93093498 100644 --- a/Tests/YDotNet.Tests.Unit/Maps/ObserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/Maps/ObserveTests.cs @@ -15,7 +15,7 @@ public void ObserveHasTarget() var doc = new Doc(); var map = doc.Map("map"); Map? target = null; - map.Observe(e => target = e.ResolveTarget()); + map.Observe(e => target = e.Target); // Act var transaction = doc.WriteTransaction(); @@ -53,7 +53,6 @@ public void ObserveHasKeysWhenAdded() var firstKey = keyChanges.First(); Assert.That(called, Is.EqualTo(expected: 1)); - Assert.That(subscription.Id, Is.EqualTo(expected: 0L)); Assert.That(keyChanges, Is.Not.Null); Assert.That(keyChanges.Count(), Is.EqualTo(expected: 1)); Assert.That(firstKey.Key, Is.EqualTo("moon-🌕")); @@ -93,7 +92,6 @@ public void ObserveHasKeysWhenRemovedByKey() Assert.That(removed, Is.True); Assert.That(called, Is.EqualTo(expected: 1)); - Assert.That(subscription.Id, Is.EqualTo(expected: 0L)); Assert.That(keyChanges, Is.Not.Null); Assert.That(keyChanges.Count(), Is.EqualTo(expected: 1)); Assert.That(firstKey.Key, Is.EqualTo("value")); @@ -132,7 +130,6 @@ public void ObserveHasKeysWhenRemovedAll() var firstKey = keyChanges.First(); Assert.That(called, Is.EqualTo(expected: 1)); - Assert.That(subscription.Id, Is.EqualTo(expected: 0L)); Assert.That(keyChanges, Is.Not.Null); Assert.That(keyChanges.Count(), Is.EqualTo(expected: 1)); Assert.That(firstKey.Key, Is.EqualTo("value")); @@ -171,7 +168,6 @@ public void ObserveHasKeysWhenUpdated() var firstKey = keyChanges.First(); Assert.That(called, Is.EqualTo(expected: 1)); - Assert.That(subscription.Id, Is.EqualTo(expected: 0L)); Assert.That(keyChanges, Is.Not.Null); Assert.That(keyChanges.Count(), Is.EqualTo(expected: 1)); Assert.That(firstKey.Key, Is.EqualTo("value")); @@ -215,7 +211,6 @@ public void ObserveHasKeysWhenAddedAndRemovedAndUpdated() // Assert Assert.That(called, Is.EqualTo(expected: 1)); - Assert.That(subscription.Id, Is.EqualTo(expected: 0L)); Assert.That(keyChanges, Is.Not.Null); Assert.That(keyChanges.Count(), Is.EqualTo(expected: 3)); Assert.That(keyChanges.Count(x => x.Tag == EventKeyChangeTag.Update), Is.EqualTo(expected: 1)); diff --git a/Tests/YDotNet.Tests.Unit/Maps/UnobserveTests.cs b/Tests/YDotNet.Tests.Unit/Maps/UnobserveTests.cs index 84ff6be6..30fd7037 100644 --- a/Tests/YDotNet.Tests.Unit/Maps/UnobserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/Maps/UnobserveTests.cs @@ -24,7 +24,7 @@ public void TriggersWhenMapChangedUntilUnobserved() Assert.That(called, Is.EqualTo(expected: 1)); // Act - map.Unobserve(subscription); + subscription.Dispose(); transaction = doc.WriteTransaction(); map.Insert(transaction, "value2", Input.Long(value: -420L)); diff --git a/Tests/YDotNet.Tests.Unit/Program.cs b/Tests/YDotNet.Tests.Unit/Program.cs new file mode 100644 index 00000000..16ab227b --- /dev/null +++ b/Tests/YDotNet.Tests.Unit/Program.cs @@ -0,0 +1,82 @@ +using System.Runtime.CompilerServices; + +namespace YDotNet.Tests.Unit; + +public class Program +{ + public static void Main() + { + var types = typeof(Program).Assembly.GetTypes(); + + AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; + + var defaultColor = Console.ForegroundColor; + + var i = 0; + foreach (var type in types) + { + if (!type.IsPublic || type.Namespace?.StartsWith("YDotNet") != true) + { + continue; + } + + var instance = Activator.CreateInstance(type); + + foreach (var method in type.GetMethods( + System.Reflection.BindingFlags.Public | + System.Reflection.BindingFlags.DeclaredOnly | + System.Reflection.BindingFlags.Instance)) + { + i++; + + if (method.IsStatic || method.GetParameters().Length != 0) + { + continue; + } + + var attributes = method.GetCustomAttributes(true); + + if (!attributes.Any(x => x.GetType().Name.StartsWith("Test"))) + { + continue; + } + + var name = $"{i:000}: {type.Name}.{method.Name}".PadRight(100, ' '); + + Console.Write(" * {0}", name); + + if (attributes.Any(x => x.GetType().Name.StartsWith("Ignore"))) + { + continue; + } + + GC.Collect(); + try + { + method.Invoke(instance, null); + + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("Success"); + Console.ForegroundColor = defaultColor; + } + catch (RuntimeWrappedException ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("Failed: {0}", ex.Message); + Console.ForegroundColor = defaultColor; + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("Failed: {0}", ex.Message); + Console.ForegroundColor = defaultColor; + } + } + } + } + + private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e) + { + Console.WriteLine(e.ExceptionObject.ToString()); + } +} diff --git a/Tests/YDotNet.Tests.Unit/Properties.cs b/Tests/YDotNet.Tests.Unit/Properties.cs new file mode 100644 index 00000000..74ab3783 --- /dev/null +++ b/Tests/YDotNet.Tests.Unit/Properties.cs @@ -0,0 +1,3 @@ +using NUnit.Framework; + +[assembly: NonParallelizable] diff --git a/Tests/YDotNet.Tests.Unit/Texts/InsertEmbedTests.cs b/Tests/YDotNet.Tests.Unit/Texts/InsertEmbedTests.cs index 4232fa58..61c5aca3 100644 --- a/Tests/YDotNet.Tests.Unit/Texts/InsertEmbedTests.cs +++ b/Tests/YDotNet.Tests.Unit/Texts/InsertEmbedTests.cs @@ -107,7 +107,7 @@ public void InsertCollectionEmbed() var chunks = text.Chunks(transaction); Assert.That(chunks.Count, Is.EqualTo(expected: 3)); - Assert.That(chunks.ElementAt(index: 1).Data.Collection.Length, Is.EqualTo(expected: 2)); + Assert.That(chunks.ElementAt(index: 1).Data.Collection.Count, Is.EqualTo(expected: 2)); } [Test] diff --git a/Tests/YDotNet.Tests.Unit/Texts/ObserveTests.cs b/Tests/YDotNet.Tests.Unit/Texts/ObserveTests.cs index 11640afb..1b9e94bb 100644 --- a/Tests/YDotNet.Tests.Unit/Texts/ObserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/Texts/ObserveTests.cs @@ -15,7 +15,7 @@ public void ObserveHasTarget() var doc = new Doc(); var text = doc.Text("value"); Text? target = null; - text.Observe(e => target = e.ResolveTarget()); + text.Observe(e => target = e.Target); // Act var transaction = doc.WriteTransaction(); diff --git a/Tests/YDotNet.Tests.Unit/Texts/UnobserveTests.cs b/Tests/YDotNet.Tests.Unit/Texts/UnobserveTests.cs index 5335f774..dfa75578 100644 --- a/Tests/YDotNet.Tests.Unit/Texts/UnobserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/Texts/UnobserveTests.cs @@ -23,7 +23,7 @@ public void TriggersWhenTextChangedUntilUnobserved() Assert.That(called, Is.EqualTo(expected: 1)); // Act - text.Unobserve(subscription); + subscription.Dispose(); transaction = doc.WriteTransaction(); text.Insert(transaction, index: 0, "Hello, "); diff --git a/Tests/YDotNet.Tests.Unit/Transactions/GetTests.cs b/Tests/YDotNet.Tests.Unit/Transactions/GetTests.cs index 11e8eb79..9f44ecbd 100644 --- a/Tests/YDotNet.Tests.Unit/Transactions/GetTests.cs +++ b/Tests/YDotNet.Tests.Unit/Transactions/GetTests.cs @@ -57,7 +57,7 @@ public void GetOnDefinedKeyWithCorrectTypeReturnsValue() } [Test] - public void GetOnDefinedKeyWithWrongTypeReturnsNull() + public void GetOnDefinedKeyWithWrongTypeThrowsException() { // Arrange var doc = new Doc(); @@ -69,18 +69,11 @@ public void GetOnDefinedKeyWithWrongTypeReturnsNull() // Act var transaction = doc.ReadTransaction(); - var array = transaction.GetArray("map"); - var map = transaction.GetMap("text"); - var text = transaction.GetText("xml-element"); - var xmlElement = transaction.GetXmlElement("xml-text"); - var xmlText = transaction.GetXmlText("array"); + Assert.Throws(() => transaction.GetArray("map")); + Assert.Throws(() => transaction.GetMap("text")); + Assert.Throws(() => transaction.GetText("xml-element")); + Assert.Throws(() => transaction.GetXmlElement("xml-text")); + Assert.Throws(() => transaction.GetXmlText("array")); transaction.Commit(); - - // Assert - Assert.That(array, Is.Null); - Assert.That(map, Is.Null); - Assert.That(text, Is.Null); - Assert.That(xmlElement, Is.Null); - Assert.That(xmlText, Is.Null); } } diff --git a/Tests/YDotNet.Tests.Unit/UndoManagers/ObservePoppedTests.cs b/Tests/YDotNet.Tests.Unit/UndoManagers/ObservePoppedTests.cs index 22895150..b0873799 100644 --- a/Tests/YDotNet.Tests.Unit/UndoManagers/ObservePoppedTests.cs +++ b/Tests/YDotNet.Tests.Unit/UndoManagers/ObservePoppedTests.cs @@ -224,6 +224,7 @@ public void TriggersAfterAddingAndRemovingContentOnXmlText() AssertDeleteSet(undoEvent.Insertions, (7938, new[] { new IdRange(start: 0, end: 5) })); // Act + GC.Collect(); undoEvent = null; undoManager.Redo(); @@ -309,6 +310,7 @@ public void TriggersAfterAddingAndRemovingContentOnXmlElement() AssertDeleteSet(undoEvent.Deletions); AssertDeleteSet(undoEvent.Insertions, (5903, new[] { new IdRange(start: 0, end: 1) })); + GC.Collect(); // Act (redo add element) undoEvent = null; undoManager.Redo(); diff --git a/Tests/YDotNet.Tests.Unit/UndoManagers/UnobserveAddedTests.cs b/Tests/YDotNet.Tests.Unit/UndoManagers/UnobserveAddedTests.cs index 663cdcda..fc1090fd 100644 --- a/Tests/YDotNet.Tests.Unit/UndoManagers/UnobserveAddedTests.cs +++ b/Tests/YDotNet.Tests.Unit/UndoManagers/UnobserveAddedTests.cs @@ -28,7 +28,7 @@ public void TriggersWhenChangesUntilUnobserved() // Act undoEvent = null; - undoManager.UnobserveAdded(subscription); + subscription.Dispose(); transaction = doc.WriteTransaction(); text.Insert(transaction, index: 5, " Viana"); transaction.Commit(); diff --git a/Tests/YDotNet.Tests.Unit/UndoManagers/UnobservePoppedTests.cs b/Tests/YDotNet.Tests.Unit/UndoManagers/UnobservePoppedTests.cs index 64f6e74d..abe8a247 100644 --- a/Tests/YDotNet.Tests.Unit/UndoManagers/UnobservePoppedTests.cs +++ b/Tests/YDotNet.Tests.Unit/UndoManagers/UnobservePoppedTests.cs @@ -29,7 +29,7 @@ public void TriggersWhenChangesUntilUnobserved() // Act undoEvent = null; - undoManager.UnobservePopped(subscription); + subscription.Dispose(); undoManager.Redo(); // Assert diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/FirstChildTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/FirstChildTests.cs index 36fdaffc..176b96bd 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/FirstChildTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/FirstChildTests.cs @@ -40,7 +40,7 @@ public void FirstChildOfRootFilledNodeIsCorrect() transaction.Commit(); // Assert - Assert.That(childXmlElement.ResolveXmlText(), Is.Not.Null); + Assert.That(childXmlElement.XmlText, Is.Not.Null); } [Test] @@ -52,7 +52,7 @@ public void FirstChildOfNestedEmptyNodeIsNull() var transaction = doc.WriteTransaction(); xmlElement.InsertElement(transaction, index: 0, "color"); - var childXmlElement = xmlElement.Get(transaction, index: 0).ResolveXmlElement(); + var childXmlElement = xmlElement.Get(transaction, index: 0).XmlElement; transaction.Commit(); // Act @@ -73,7 +73,7 @@ public void FirstChildOfNestedFilledNodeIsCorrect() var transaction = doc.WriteTransaction(); xmlElement.InsertElement(transaction, index: 0, "color"); - var childXmlElement = xmlElement.Get(transaction, index: 0).ResolveXmlElement(); + var childXmlElement = xmlElement.Get(transaction, index: 0).XmlElement; childXmlElement.InsertElement(transaction, index: 0, "alpha"); childXmlElement.InsertElement(transaction, index: 0, "hex"); transaction.Commit(); @@ -84,6 +84,6 @@ public void FirstChildOfNestedFilledNodeIsCorrect() transaction.Commit(); // Assert - Assert.That(grandChildXmlElement.ResolveXmlElement(), Is.Not.Null); + Assert.That(grandChildXmlElement.XmlElement, Is.Not.Null); } } diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/GetTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/GetTests.cs index 881085fa..c0266885 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/GetTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/GetTests.cs @@ -34,7 +34,7 @@ public void GetXmlText() transaction.Commit(); // Assert - Assert.That(output.ResolveXmlText(), Is.Not.Null); + Assert.That(output.XmlText, Is.Not.Null); } [Test] @@ -49,7 +49,7 @@ public void GetXmlElement() transaction.Commit(); // Assert - Assert.That(output.ResolveXmlElement(), Is.Not.Null); + Assert.That(output.XmlElement, Is.Not.Null); } private (Doc, XmlElement) ArrangeDoc() diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/InsertAttributeTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/InsertAttributeTests.cs index c83b0b38..44507b5f 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/InsertAttributeTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/InsertAttributeTests.cs @@ -127,7 +127,7 @@ private static (Doc, XmlElement) ArrangeDoc() var transaction = doc.WriteTransaction(); array.InsertRange(transaction, index: 0, new[] { Input.XmlElement("link") }); - var xmlElement = array.Get(transaction, index: 0).ResolveXmlElement(); + var xmlElement = array.Get(transaction, index: 0).XmlElement; transaction.Commit(); return (doc, xmlElement); diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/NextSiblingTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/NextSiblingTests.cs index fc6cd016..a0a9d9b5 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/NextSiblingTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/NextSiblingTests.cs @@ -14,12 +14,12 @@ public void GetsNextSiblingAtBeginning() // Act var transaction = doc.ReadTransaction(); - var target = xmlElement.Get(transaction, index: 0).ResolveXmlElement(); + var target = xmlElement.Get(transaction, index: 0).XmlElement; var sibling = target.NextSibling(transaction); transaction.Commit(); // Assert - Assert.That(sibling.ResolveXmlText(), Is.Not.Null); + Assert.That(sibling.XmlText, Is.Not.Null); } [Test] @@ -30,12 +30,12 @@ public void GetsNextSiblingAtMiddle() // Act var transaction = doc.ReadTransaction(); - var target = xmlElement.Get(transaction, index: 2).ResolveXmlElement(); + var target = xmlElement.Get(transaction, index: 2).XmlElement; var sibling = target.NextSibling(transaction); transaction.Commit(); // Assert - Assert.That(sibling.ResolveXmlText(), Is.Not.Null); + Assert.That(sibling.XmlText, Is.Not.Null); } [Test] @@ -46,7 +46,7 @@ public void GetsNextSiblingAtEnding() // Act var transaction = doc.ReadTransaction(); - var target = xmlElement.Get(transaction, index: 4).ResolveXmlElement(); + var target = xmlElement.Get(transaction, index: 4).XmlElement; var sibling = target.NextSibling(transaction); transaction.Commit(); diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/ObserveTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/ObserveTests.cs index 1f4ed60e..c3130054 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/ObserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/ObserveTests.cs @@ -15,7 +15,7 @@ public void ObserveHasTarget() var xmlElement = doc.XmlElement("xml-element"); XmlElement? target = null; - xmlElement.Observe(e => target = e.ResolveTarget()); + xmlElement.Observe(e => target = e.Target); // Act var transaction = doc.WriteTransaction(); @@ -49,10 +49,10 @@ public void ObserveHasDeltaWhenAddedXmlElementsAndTexts() Assert.That(eventChanges.Count(), Is.EqualTo(expected: 1)); Assert.That(eventChanges.First().Tag, Is.EqualTo(EventChangeTag.Add)); Assert.That(eventChanges.First().Length, Is.EqualTo(expected: 3)); - Assert.That(eventChanges.First().Values.ElementAt(index: 0).ResolveXmlText(), Is.Not.Null); - Assert.That(eventChanges.First().Values.ElementAt(index: 1).ResolveXmlElement(), Is.Not.Null); - Assert.That(eventChanges.First().Values.ElementAt(index: 1).ResolveXmlElement().Tag, Is.EqualTo("color")); - Assert.That(eventChanges.First().Values.ElementAt(index: 2).ResolveXmlText(), Is.Not.Null); + Assert.That(eventChanges.First().Values.ElementAt(index: 0).XmlText, Is.Not.Null); + Assert.That(eventChanges.First().Values.ElementAt(index: 1).XmlElement, Is.Not.Null); + Assert.That(eventChanges.First().Values.ElementAt(index: 1).XmlElement.Tag, Is.EqualTo("color")); + Assert.That(eventChanges.First().Values.ElementAt(index: 2).XmlText, Is.Not.Null); } [Test] diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/ParentTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/ParentTests.cs index 9c85c0bb..052d9fb9 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/ParentTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/ParentTests.cs @@ -35,7 +35,7 @@ public void ParentOfNodeInsideArrayReturnsNull() // Act transaction = doc.ReadTransaction(); - var xmlElement = array.Get(transaction, index: 0).ResolveXmlElement(); + var xmlElement = array.Get(transaction, index: 0).XmlElement; var parentXmlElement = xmlElement.Parent(transaction); transaction.Commit(); @@ -56,7 +56,7 @@ public void ParentOfNodeInsideMapReturnsNull() // Act transaction = doc.ReadTransaction(); - var xmlElement = map.Get(transaction, "attributes").ResolveXmlElement(); + var xmlElement = map.Get(transaction, "attributes").XmlElement; var parentXmlElement = xmlElement.Parent(transaction); transaction.Commit(); @@ -78,7 +78,7 @@ public void ParentOfNestedNodeAtFirstLevelReturnsCorrectParent() // Act transaction = doc.ReadTransaction(); - var childXmlElement = xmlElement.Get(transaction, index: 0).ResolveXmlElement(); + var childXmlElement = xmlElement.Get(transaction, index: 0).XmlElement; var parentXmlElement = childXmlElement.Parent(transaction); var childLength = parentXmlElement.ChildLength(transaction); diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/PreviousSiblingTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/PreviousSiblingTests.cs index 944c65b4..837348cf 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/PreviousSiblingTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/PreviousSiblingTests.cs @@ -14,7 +14,7 @@ public void GetsPreviousSiblingAtBeginning() // Act var transaction = doc.ReadTransaction(); - var target = xmlElement.Get(transaction, index: 0).ResolveXmlElement(); + var target = xmlElement.Get(transaction, index: 0).XmlElement; var sibling = target.PreviousSibling(transaction); transaction.Commit(); @@ -30,12 +30,12 @@ public void GetsPreviousSiblingAtMiddle() // Act var transaction = doc.ReadTransaction(); - var target = xmlElement.Get(transaction, index: 2).ResolveXmlElement(); + var target = xmlElement.Get(transaction, index: 2).XmlElement; var sibling = target.PreviousSibling(transaction); transaction.Commit(); // Assert - Assert.That(sibling.ResolveXmlText(), Is.Not.Null); + Assert.That(sibling.XmlText, Is.Not.Null); } [Test] @@ -46,12 +46,12 @@ public void GetsPreviousSiblingAtEnding() // Act var transaction = doc.ReadTransaction(); - var target = xmlElement.Get(transaction, index: 4).ResolveXmlElement(); + var target = xmlElement.Get(transaction, index: 4).XmlElement; var sibling = target.PreviousSibling(transaction); transaction.Commit(); // Assert - Assert.That(sibling.ResolveXmlText(), Is.Not.Null); + Assert.That(sibling.XmlText, Is.Not.Null); } private (Doc, XmlElement) ArrangeDoc() diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/RemoveAttributeTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/RemoveAttributeTests.cs index f916eb24..22becd8f 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/RemoveAttributeTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/RemoveAttributeTests.cs @@ -66,7 +66,7 @@ public void RemoveAttributeThatDoesNotExistWorks() var transaction = doc.WriteTransaction(); array.InsertRange(transaction, index: 0, new[] { Input.XmlElement("link") }); - var xmlElement = array.Get(transaction, index: 0).ResolveXmlElement(); + var xmlElement = array.Get(transaction, index: 0).XmlElement; xmlElement.InsertAttribute(transaction, string.Empty, string.Empty); xmlElement.InsertAttribute(transaction, "as", "stylesheet"); diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/StringTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/StringTests.cs index 405ac861..dde4fed6 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/StringTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/StringTests.cs @@ -50,14 +50,14 @@ public void NodeNestedOnMapHasCorrectString() var transaction = doc.WriteTransaction(); map.Insert(transaction, "xml-element", Input.XmlElement("color")); - var xmlElement = map.Get(transaction, "xml-element").ResolveXmlElement(); + var xmlElement = map.Get(transaction, "xml-element").XmlElement; var xmlText = xmlElement.InsertText(transaction, index: 0); xmlText.Insert(transaction, index: 0, "blue"); transaction.Commit(); // Act transaction = doc.ReadTransaction(); - xmlElement = map.Get(transaction, "xml-element").ResolveXmlElement(); + xmlElement = map.Get(transaction, "xml-element").XmlElement; var text = xmlElement.String(transaction); transaction.Commit(); @@ -74,14 +74,14 @@ public void NodeNestedOnArrayHasCorrectString() var transaction = doc.WriteTransaction(); array.InsertRange(transaction, index: 0, new[] { Input.XmlElement("color") }); - var xmlElement = array.Get(transaction, index: 0).ResolveXmlElement(); + var xmlElement = array.Get(transaction, index: 0).XmlElement; var xmlText = xmlElement.InsertText(transaction, index: 0); xmlText.Insert(transaction, index: 0, "purple"); transaction.Commit(); // Act transaction = doc.ReadTransaction(); - var text = array.Get(transaction, index: 0).ResolveXmlElement().String(transaction); + var text = array.Get(transaction, index: 0).XmlElement.String(transaction); transaction.Commit(); // Assert diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/TagTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/TagTests.cs index 27a622f4..69687895 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/TagTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/TagTests.cs @@ -50,7 +50,7 @@ public void NodeNestedOnMapHasCorrectTag() // Act transaction = doc.ReadTransaction(); - var tag = map.Get(transaction, "xml-element").ResolveXmlElement().Tag; + var tag = map.Get(transaction, "xml-element").XmlElement.Tag; transaction.Commit(); // Assert @@ -70,7 +70,7 @@ public void NodeNestedOnArrayHasCorrectTag() // Act transaction = doc.ReadTransaction(); - var tag = array.Get(transaction, index: 0).ResolveXmlElement().Tag; + var tag = array.Get(transaction, index: 0).XmlElement.Tag; transaction.Commit(); // Assert diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/TreeWalkerTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/TreeWalkerTests.cs index 6ecfadc5..2d40c025 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/TreeWalkerTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/TreeWalkerTests.cs @@ -45,10 +45,10 @@ public void WalksOnTreeWithSingleLevelOfDepth() Assert.That(xmlTreeWalker, Is.Not.Null); Assert.That(xmlNodes.Length, Is.EqualTo(expected: 3)); - Assert.That(xmlNodes.ElementAt(index: 0).ResolveXmlText(), Is.Not.Null); - Assert.That(xmlNodes.ElementAt(index: 1).ResolveXmlElement(), Is.Not.Null); - Assert.That(xmlNodes.ElementAt(index: 1).ResolveXmlElement().Tag, Is.EqualTo("color")); - Assert.That(xmlNodes.ElementAt(index: 2).ResolveXmlText(), Is.Not.Null); + Assert.That(xmlNodes.ElementAt(index: 0).XmlText, Is.Not.Null); + Assert.That(xmlNodes.ElementAt(index: 1).XmlElement, Is.Not.Null); + Assert.That(xmlNodes.ElementAt(index: 1).XmlElement.Tag, Is.EqualTo("color")); + Assert.That(xmlNodes.ElementAt(index: 2).XmlText, Is.Not.Null); } [Test] @@ -61,7 +61,7 @@ public void WalksOnTreeWithMultipleLevelsOfDepth() var transaction = doc.WriteTransaction(); xmlElement.InsertText(transaction, index: 0); xmlElement.InsertElement(transaction, index: 1, "color"); - var childXmlElement = xmlElement.Get(transaction, index: 1).ResolveXmlElement(); + var childXmlElement = xmlElement.Get(transaction, index: 1).XmlElement; childXmlElement.InsertElement(transaction, index: 0, "alpha"); childXmlElement.InsertElement(transaction, index: 1, "hex"); childXmlElement.InsertText(transaction, index: 2); @@ -77,13 +77,13 @@ public void WalksOnTreeWithMultipleLevelsOfDepth() Assert.That(xmlTreeWalker, Is.Not.Null); Assert.That(xmlNodes.Length, Is.EqualTo(expected: 5)); - Assert.That(xmlNodes.ElementAt(index: 0).ResolveXmlText(), Is.Not.Null); - Assert.That(xmlNodes.ElementAt(index: 1).ResolveXmlElement(), Is.Not.Null); - Assert.That(xmlNodes.ElementAt(index: 1).ResolveXmlElement().Tag, Is.EqualTo("color")); - Assert.That(xmlNodes.ElementAt(index: 2).ResolveXmlElement(), Is.Not.Null); - Assert.That(xmlNodes.ElementAt(index: 2).ResolveXmlElement().Tag, Is.EqualTo("alpha")); - Assert.That(xmlNodes.ElementAt(index: 3).ResolveXmlElement(), Is.Not.Null); - Assert.That(xmlNodes.ElementAt(index: 3).ResolveXmlElement().Tag, Is.EqualTo("hex")); - Assert.That(xmlNodes.ElementAt(index: 4).ResolveXmlText(), Is.Not.Null); + Assert.That(xmlNodes.ElementAt(index: 0).XmlText, Is.Not.Null); + Assert.That(xmlNodes.ElementAt(index: 1).XmlElement, Is.Not.Null); + Assert.That(xmlNodes.ElementAt(index: 1).XmlElement.Tag, Is.EqualTo("color")); + Assert.That(xmlNodes.ElementAt(index: 2).XmlElement, Is.Not.Null); + Assert.That(xmlNodes.ElementAt(index: 2).XmlElement.Tag, Is.EqualTo("alpha")); + Assert.That(xmlNodes.ElementAt(index: 3).XmlElement, Is.Not.Null); + Assert.That(xmlNodes.ElementAt(index: 3).XmlElement.Tag, Is.EqualTo("hex")); + Assert.That(xmlNodes.ElementAt(index: 4).XmlText, Is.Not.Null); } } diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/UnobserveTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/UnobserveTests.cs index 191567ad..59e33e8f 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/UnobserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/UnobserveTests.cs @@ -24,7 +24,7 @@ public void TriggersWhenXmlElementChangedUntilUnobserved() Assert.That(called, Is.EqualTo(expected: 1)); // Act - xmlElement.Unobserve(subscription); + subscription.Dispose(); transaction = doc.WriteTransaction(); xmlElement.InsertText(transaction, index: 1); diff --git a/Tests/YDotNet.Tests.Unit/XmlTexts/NextSiblingTests.cs b/Tests/YDotNet.Tests.Unit/XmlTexts/NextSiblingTests.cs index ce650022..46e52bec 100644 --- a/Tests/YDotNet.Tests.Unit/XmlTexts/NextSiblingTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlTexts/NextSiblingTests.cs @@ -14,12 +14,12 @@ public void GetsNextSiblingAtBeginning() // Act var transaction = doc.ReadTransaction(); - var target = xmlElement.Get(transaction, index: 0).ResolveXmlText(); + var target = xmlElement.Get(transaction, index: 0).XmlText; var sibling = target.NextSibling(transaction); transaction.Commit(); // Assert - Assert.That(sibling.ResolveXmlElement(), Is.Not.Null); + Assert.That(sibling.XmlElement, Is.Not.Null); } [Test] @@ -30,12 +30,12 @@ public void GetsNextSiblingAtMiddle() // Act var transaction = doc.ReadTransaction(); - var target = xmlElement.Get(transaction, index: 2).ResolveXmlText(); + var target = xmlElement.Get(transaction, index: 2).XmlText; var sibling = target.NextSibling(transaction); transaction.Commit(); // Assert - Assert.That(sibling.ResolveXmlElement(), Is.Not.Null); + Assert.That(sibling.XmlElement, Is.Not.Null); } [Test] @@ -46,7 +46,7 @@ public void GetsNextSiblingAtEnding() // Act var transaction = doc.ReadTransaction(); - var target = xmlElement.Get(transaction, index: 4).ResolveXmlText(); + var target = xmlElement.Get(transaction, index: 4).XmlText; var sibling = target.NextSibling(transaction); transaction.Commit(); diff --git a/Tests/YDotNet.Tests.Unit/XmlTexts/ObserveTests.cs b/Tests/YDotNet.Tests.Unit/XmlTexts/ObserveTests.cs index 89ca21e9..d888476a 100644 --- a/Tests/YDotNet.Tests.Unit/XmlTexts/ObserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlTexts/ObserveTests.cs @@ -16,7 +16,7 @@ public void ObserveHasTarget() var xmlText = doc.XmlText("xml-text"); XmlText? target = null; - var subscription = xmlText.Observe(e => target = e.ResolveTarget()); + var subscription = xmlText.Observe(e => target = e.Target); // Act var transaction = doc.WriteTransaction(); @@ -24,7 +24,6 @@ public void ObserveHasTarget() transaction.Commit(); // Assert - Assert.That(subscription.Id, Is.EqualTo(expected: 0L)); Assert.That(target, Is.Not.Null); Assert.That(target.Handle, Is.Not.EqualTo(nint.Zero)); } diff --git a/Tests/YDotNet.Tests.Unit/XmlTexts/PreviousSiblingTests.cs b/Tests/YDotNet.Tests.Unit/XmlTexts/PreviousSiblingTests.cs index 5e47bba0..92e37ee5 100644 --- a/Tests/YDotNet.Tests.Unit/XmlTexts/PreviousSiblingTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlTexts/PreviousSiblingTests.cs @@ -14,7 +14,7 @@ public void GetsPreviousSiblingAtBeginning() // Act var transaction = doc.ReadTransaction(); - var target = xmlElement.Get(transaction, index: 0).ResolveXmlText(); + var target = xmlElement.Get(transaction, index: 0).XmlText; var sibling = target.PreviousSibling(transaction); transaction.Commit(); @@ -30,12 +30,12 @@ public void GetsPreviousSiblingAtMiddle() // Act var transaction = doc.ReadTransaction(); - var target = xmlElement.Get(transaction, index: 2).ResolveXmlText(); + var target = xmlElement.Get(transaction, index: 2).XmlText; var sibling = target.PreviousSibling(transaction); transaction.Commit(); // Assert - Assert.That(sibling.ResolveXmlElement(), Is.Not.Null); + Assert.That(sibling.XmlElement, Is.Not.Null); } [Test] @@ -46,12 +46,12 @@ public void GetsPreviousSiblingAtEnding() // Act var transaction = doc.ReadTransaction(); - var target = xmlElement.Get(transaction, index: 4).ResolveXmlText(); + var target = xmlElement.Get(transaction, index: 4).XmlText; var sibling = target.PreviousSibling(transaction); transaction.Commit(); // Assert - Assert.That(sibling.ResolveXmlElement(), Is.Not.Null); + Assert.That(sibling.XmlElement, Is.Not.Null); } private (Doc, XmlElement) ArrangeDoc() diff --git a/Tests/YDotNet.Tests.Unit/XmlTexts/UnobserveTests.cs b/Tests/YDotNet.Tests.Unit/XmlTexts/UnobserveTests.cs index 2ad12e72..1fe59ffe 100644 --- a/Tests/YDotNet.Tests.Unit/XmlTexts/UnobserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlTexts/UnobserveTests.cs @@ -24,7 +24,7 @@ public void TriggersWhenXmlTextChangedUntilUnobserved() Assert.That(called, Is.EqualTo(expected: 1)); // Act - xmlText.Unobserve(subscription); + subscription.Dispose(); transaction = doc.WriteTransaction(); xmlText.Insert(transaction, index: 1, " Viana"); diff --git a/Tests/YDotNet.Tests.Unit/YDotNet - Backup.Tests.Unit.csproj b/Tests/YDotNet.Tests.Unit/YDotNet - Backup.Tests.Unit.csproj deleted file mode 100644 index 0fada3f7..00000000 --- a/Tests/YDotNet.Tests.Unit/YDotNet - Backup.Tests.Unit.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - net7.0 - enable - false - - - - - - - - - - - - - - - diff --git a/Tests/YDotNet.Tests.Unit/YDotNet.Tests.Unit.csproj b/Tests/YDotNet.Tests.Unit/YDotNet.Tests.Unit.csproj index b97b142b..8a9e55d5 100644 --- a/Tests/YDotNet.Tests.Unit/YDotNet.Tests.Unit.csproj +++ b/Tests/YDotNet.Tests.Unit/YDotNet.Tests.Unit.csproj @@ -4,6 +4,7 @@ net7.0 enable false + YDotNet.Tests.Unit.Program diff --git a/YDotNet/Document/Cells/Input.cs b/YDotNet/Document/Cells/Input.cs index b6804051..5fb1047f 100644 --- a/YDotNet/Document/Cells/Input.cs +++ b/YDotNet/Document/Cells/Input.cs @@ -6,15 +6,29 @@ namespace YDotNet.Document.Cells; /// /// Represents a cell used to send information to be stored. /// -public abstract class Input : IDisposable +public sealed class Input : Resource { + private readonly IDisposable[] allocatedMemory; + + internal Input(InputNative native, params IDisposable[] allocatedMemory) + { + this.allocatedMemory = allocatedMemory; + + InputNative = native; + } + + protected override void DisposeCore(bool disposing) + { + foreach (var memory in allocatedMemory) + { + memory.Dispose(); + } + } + /// - /// Gets or sets the native input cell represented by this cell. + /// Gets the native input cell represented by this cell. /// - internal InputNative InputNative { get; set; } - - /// - public abstract void Dispose(); + internal InputNative InputNative { get; } /// /// Creates a new instance of the class. @@ -23,7 +37,7 @@ public abstract class Input : IDisposable /// The cell that represents the provided value. public static Input Doc(Doc value) { - return new InputEmpty(InputChannel.Doc(value.Handle)); + return new Input(InputChannel.Doc(value.Handle)); } /// @@ -33,9 +47,9 @@ public static Input Doc(Doc value) /// The cell that represents the provided value. public static Input String(string value) { - var pointer = MemoryWriter.WriteUtf8String(value); + var unsafeValue = MemoryWriter.WriteUtf8String(value); - return new InputPointer(InputChannel.String(pointer), pointer); + return new Input(InputChannel.String(unsafeValue.Handle), unsafeValue); } /// @@ -45,7 +59,7 @@ public static Input String(string value) /// The cell that represents the provided value. public static Input Boolean(bool value) { - return new InputEmpty(InputChannel.Boolean(value)); + return new Input(InputChannel.Boolean(value)); } /// @@ -55,7 +69,7 @@ public static Input Boolean(bool value) /// The cell that represents the provided value. public static Input Double(double value) { - return new InputEmpty(InputChannel.Double(value)); + return new Input(InputChannel.Double(value)); } /// @@ -65,7 +79,7 @@ public static Input Double(double value) /// The cell that represents the provided value. public static Input Long(long value) { - return new InputEmpty(InputChannel.Long(value)); + return new Input(InputChannel.Long(value)); } /// @@ -75,7 +89,7 @@ public static Input Long(long value) /// The cell that represents the provided value. public static Input Bytes(byte[] value) { - return new InputEmpty(InputChannel.Bytes(value, (uint)value.Length)); + return new Input(InputChannel.Bytes(value, (uint)value.Length)); } /// @@ -85,10 +99,9 @@ public static Input Bytes(byte[] value) /// The cell that represents the provided value. public static Input Collection(Input[] value) { - var inputs = value.Select(x => x.InputNative).ToArray(); - var pointer = MemoryWriter.WriteStructArray(inputs); + var unsafeMemory = MemoryWriter.WriteStructArray(value.Select(x => x.InputNative).ToArray()); - return new InputPointer(InputChannel.Collection(pointer, (uint)value.Length), pointer); + return new Input(InputChannel.Collection(unsafeMemory.Handle, (uint)value.Length), unsafeMemory); } /// @@ -98,12 +111,10 @@ public static Input Collection(Input[] value) /// The cell that represents the provided value. public static Input Object(IDictionary value) { - var keys = MemoryWriter.WriteUtf8StringArray(value.Keys.ToArray()); - var values = MemoryWriter.WriteStructArray(value.Values.Select(x => x.InputNative).ToArray()); + var unsageKeys = MemoryWriter.WriteUtf8StringArray(value.Keys.ToArray()); + var unsafeValues = MemoryWriter.WriteStructArray(value.Values.Select(x => x.InputNative).ToArray()); - return new InputPointerArray( - InputChannel.Object(keys.Head, values, (uint)value.Count), - keys.Pointers.Concat(new[] { keys.Head, values }).ToArray()); + return new Input(InputChannel.Object(unsageKeys.Handle, unsafeValues.Handle, (uint)value.Count), unsageKeys, unsafeValues); } /// @@ -112,7 +123,7 @@ public static Input Object(IDictionary value) /// The cell that represents the provided value. public static Input Null() { - return new InputEmpty(InputChannel.Null()); + return new Input(InputChannel.Null()); } /// @@ -121,7 +132,7 @@ public static Input Null() /// The cell that represents the provided value. public static Input Undefined() { - return new InputEmpty(InputChannel.Undefined()); + return new Input(InputChannel.Undefined()); } /// @@ -131,10 +142,9 @@ public static Input Undefined() /// The cell that represents the provided value. public static Input Array(Input[] value) { - var inputs = value.Select(x => x.InputNative).ToArray(); - var pointer = MemoryWriter.WriteStructArray(inputs); + var unsafeMemory = MemoryWriter.WriteStructArray(value.Select(x => x.InputNative).ToArray()); - return new InputPointer(InputChannel.Array(pointer, (uint)value.Length), pointer); + return new Input(InputChannel.Array(unsafeMemory.Handle, (uint)value.Length), unsafeMemory); } /// @@ -144,12 +154,10 @@ public static Input Array(Input[] value) /// The cell that represents the provided value. public static Input Map(IDictionary value) { - var keys = MemoryWriter.WriteUtf8StringArray(value.Keys.ToArray()); - var values = MemoryWriter.WriteStructArray(value.Values.Select(x => x.InputNative).ToArray()); + var unsageKeys = MemoryWriter.WriteUtf8StringArray(value.Keys.ToArray()); + var unsafeValues = MemoryWriter.WriteStructArray(value.Values.Select(x => x.InputNative).ToArray()); - return new InputPointerArray( - InputChannel.Map(keys.Head, values, (uint)value.Count), - keys.Pointers.Concat(new[] { keys.Head, values }).ToArray()); + return new Input(InputChannel.Map(unsageKeys.Handle, unsafeValues.Handle, (uint)value.Count), unsageKeys, unsafeValues); } /// @@ -159,9 +167,9 @@ public static Input Map(IDictionary value) /// The cell that represents the provided value. public static Input Text(string value) { - var pointer = MemoryWriter.WriteUtf8String(value); + var unsafeValue = MemoryWriter.WriteUtf8String(value); - return new InputPointer(InputChannel.Text(pointer), pointer); + return new Input(InputChannel.Text(unsafeValue.Handle), unsafeValue); } /// @@ -171,9 +179,9 @@ public static Input Text(string value) /// The cell that represents the provided value. public static Input XmlElement(string name) { - var pointer = MemoryWriter.WriteUtf8String(name); + var unsafeValue = MemoryWriter.WriteUtf8String(name); - return new InputPointer(InputChannel.XmlElement(pointer), pointer); + return new Input(InputChannel.XmlElement(unsafeValue.Handle), unsafeValue); } /// @@ -183,8 +191,8 @@ public static Input XmlElement(string name) /// The cell that represents the provided value. public static Input XmlText(string value) { - var pointer = MemoryWriter.WriteUtf8String(value); + var unsafeValue = MemoryWriter.WriteUtf8String(value); - return new InputPointer(InputChannel.XmlText(pointer), pointer); + return new Input(InputChannel.XmlText(unsafeValue.Handle), unsafeValue); } } diff --git a/YDotNet/Document/Cells/InputEmpty.cs b/YDotNet/Document/Cells/InputEmpty.cs deleted file mode 100644 index a8a43841..00000000 --- a/YDotNet/Document/Cells/InputEmpty.cs +++ /dev/null @@ -1,24 +0,0 @@ -using YDotNet.Native.Cells.Inputs; - -namespace YDotNet.Document.Cells; - -/// -/// Represents an cell with no resources to be disposed later. -/// -internal class InputEmpty : Input -{ - /// - /// Initializes a new instance of the class. - /// - /// The native resource that is held by this cell. - internal InputEmpty(InputNative inputNative) - { - InputNative = inputNative; - } - - /// - public override void Dispose() - { - // Nothing here. - } -} diff --git a/YDotNet/Document/Cells/InputPointer.cs b/YDotNet/Document/Cells/InputPointer.cs deleted file mode 100644 index b3d1e280..00000000 --- a/YDotNet/Document/Cells/InputPointer.cs +++ /dev/null @@ -1,30 +0,0 @@ -using YDotNet.Infrastructure; -using YDotNet.Native.Cells.Inputs; - -namespace YDotNet.Document.Cells; - -/// -/// Represents an cell with a single pointer to be disposed later. -/// -internal class InputPointer : Input -{ - private readonly nint pointer; - - /// - /// Initializes a new instance of the class. - /// - /// The native resource that is held by this cell. - /// The pointer of the unmanaged memory location to be disposed on `Dispose()`. - internal InputPointer(InputNative inputNative, nint pointer) - { - this.pointer = pointer; - - InputNative = inputNative; - } - - /// - public override void Dispose() - { - MemoryWriter.Release(pointer); - } -} diff --git a/YDotNet/Document/Cells/InputPointerArray.cs b/YDotNet/Document/Cells/InputPointerArray.cs deleted file mode 100644 index 9f691c4c..00000000 --- a/YDotNet/Document/Cells/InputPointerArray.cs +++ /dev/null @@ -1,30 +0,0 @@ -using YDotNet.Infrastructure; -using YDotNet.Native.Cells.Inputs; - -namespace YDotNet.Document.Cells; - -/// -/// Represents an cell with a single pointer to be disposed later. -/// -internal class InputPointerArray : Input -{ - private readonly nint[] pointers; - - /// - /// Initializes a new instance of the class. - /// - /// The native resource that is held by this cell. - /// The array of pointers to be disposed on `Dispose()`. - internal InputPointerArray(InputNative inputNative, nint[] pointers) - { - this.pointers = pointers; - - InputNative = inputNative; - } - - /// - public override void Dispose() - { - MemoryWriter.ReleaseArray(pointers); - } -} diff --git a/YDotNet/Document/Cells/JsonArray.cs b/YDotNet/Document/Cells/JsonArray.cs new file mode 100644 index 00000000..b01797dc --- /dev/null +++ b/YDotNet/Document/Cells/JsonArray.cs @@ -0,0 +1,30 @@ +using System.Collections.ObjectModel; +using System.Runtime.InteropServices; +using YDotNet.Infrastructure; +using YDotNet.Native.Cells.Outputs; + +namespace YDotNet.Document.Cells; + +public sealed class JsonArray : ReadOnlyCollection +{ + public JsonArray(nint handle, uint length, IResourceOwner owner) + : base(ReadItems(handle, length, owner)) + { + } + + private static List ReadItems(nint handle, uint length, IResourceOwner owner) + { + var collectionHandle = OutputChannel.Collection(handle); + var collectionNatives = MemoryReader.ReadIntPtrArray(collectionHandle, length, Marshal.SizeOf()); + + var result = new List(); + + foreach (var itemHandle in collectionNatives) + { + // The outputs are owned by this block of allocated memory. + result.Add(new Output(itemHandle, owner)); + } + + return result; + } +} diff --git a/YDotNet/Document/Cells/JsonObject.cs b/YDotNet/Document/Cells/JsonObject.cs new file mode 100644 index 00000000..79a114af --- /dev/null +++ b/YDotNet/Document/Cells/JsonObject.cs @@ -0,0 +1,33 @@ +using System.Collections.ObjectModel; +using System.Runtime.InteropServices; +using YDotNet.Infrastructure; +using YDotNet.Native.Cells.Outputs; +using YDotNet.Native.Types.Maps; + +namespace YDotNet.Document.Cells; + +public sealed class JsonObject : ReadOnlyDictionary +{ + public JsonObject(nint handle, uint length, IResourceOwner owner) + : base(ReadItems(handle, length, owner)) + { + } + + private static Dictionary ReadItems(nint handle, uint length, IResourceOwner owner) + { + var entriesHandle = OutputChannel.Object(handle).Checked(); + var entriesNatives = MemoryReader.ReadIntPtrArray(entriesHandle, length, Marshal.SizeOf()); + + var result = new Dictionary(); + + foreach (var itemHandle in entriesNatives) + { + var (mapEntry, outputHandle) = MemoryReader.ReadMapEntryAndOutputHandle(itemHandle); + var mapEntryKey = MemoryReader.ReadUtf8String(mapEntry.Field); + + result[mapEntryKey] = new Output(outputHandle, owner); + } + + return result; + } +} diff --git a/YDotNet/Document/Cells/Output.cs b/YDotNet/Document/Cells/Output.cs index 17ca9551..237eed49 100644 --- a/YDotNet/Document/Cells/Output.cs +++ b/YDotNet/Document/Cells/Output.cs @@ -5,7 +5,6 @@ using YDotNet.Document.Types.XmlTexts; using YDotNet.Infrastructure; using YDotNet.Native.Cells.Outputs; -using YDotNet.Native.Types.Maps; using Array = YDotNet.Document.Types.Arrays.Array; #pragma warning disable SA1623 // Property summary documentation should match accessors @@ -15,27 +14,31 @@ namespace YDotNet.Document.Cells; /// /// Represents a cell used to read information from the storage. /// -public class Output +public sealed class Output : UnmanagedResource { private readonly Lazy value; - /// - /// Initializes a new instance of the class. - /// - /// The pointer to the native resource that represents the storage. - /// Indicates if the memory has been allocated and needs to be disposed. - internal Output(nint handle, bool shouldDispose) + internal Output(nint handle, IResourceOwner? owner) + : base(handle, owner) { var native = Marshal.PtrToStructure(handle.Checked()); Type = (OutputType)native.Tag; // We use lazy because some types like Doc and Map need to be disposed and therefore they should not be allocated, if not needed. - value = BuildValue(handle, native.Length, Type); + value = BuildValue(handle, native.Length, Type, this); + } + + ~Output() + { + Dispose(true); + } - if (shouldDispose) + protected override void DisposeCore(bool disposing) + { + if (Owner == null) { - OutputChannel.Destroy(handle); + OutputChannel.Destroy(Handle); } } @@ -84,55 +87,50 @@ internal Output(nint handle, bool shouldDispose) /// Gets the collection. /// /// Value is not a collection. - public Output[] Collection => GetValue(OutputType.Collection); + public JsonArray Collection => GetValue(OutputType.Collection); /// /// Gets the value as json object. /// /// Value is not a json object. - public IDictionary? Object => GetValue>(OutputType.Object); + public JsonObject Object => GetValue(OutputType.Object); /// /// Gets the value. /// /// The resolved array. /// Value is not a . - /// You are responsible to dispose the array, if you use this property. - public Array ResolveArray() => GetValue(OutputType.Array); + public Array Array => GetValue(OutputType.Array); /// /// Gets the value. /// /// The resolved map. /// Value is not a . - /// You are responsible to dispose the map, if you use this property. - public Map ResolveMap() => GetValue(OutputType.Map); + public Map Map => GetValue(OutputType.Map); /// /// Gets the value. /// /// The resolved text. /// Value is not a . - /// You are responsible to dispose the text, if you use this property. - public Text ResolveText() => GetValue(OutputType.Text); + public Text Text => GetValue(OutputType.Text); /// /// Gets the value. /// /// The resolved xml element. /// Value is not a . - /// You are responsible to dispose the xml element, if you use this property. - public XmlElement ResolveXmlElement() => GetValue(OutputType.XmlElement); + public XmlElement XmlElement => GetValue(OutputType.XmlElement); /// /// Gets the value. /// /// The resolved xml text. /// Value is not a . - /// You are responsible to dispose the xml text, if you use this property. - public XmlText ResolveXmlText() => GetValue(OutputType.XmlText); + public XmlText XmlText => GetValue(OutputType.XmlText); - private static Lazy BuildValue(nint handle, uint length, OutputType type) + private static Lazy BuildValue(nint handle, uint length, OutputType type, IResourceOwner owner) { switch (type) { @@ -166,52 +164,20 @@ internal Output(nint handle, bool shouldDispose) case OutputType.Bytes: { - var pointer = OutputChannel.Bytes(handle).Checked(); - - var result = MemoryReader.TryReadBytes(OutputChannel.Bytes(handle), length) ?? - throw new YDotNetException("Internal type mismatch, native library returns null."); - - if (result == null) - { - throw new YDotNetException("Internal type mismatch, native library returns null."); - } + var bytesHandle = OutputChannel.Bytes(handle).Checked(); + var bytesArray = MemoryReader.ReadBytes(OutputChannel.Bytes(handle), length); - OutputChannel.Destroy(pointer); - return new Lazy(result); + return new Lazy(bytesArray); } case OutputType.Collection: { - var pointer = OutputChannel.Collection(handle).Checked(); - - var handles = MemoryReader.TryReadIntPtrArray(pointer, length, Marshal.SizeOf()) - ?? throw new YDotNetException("Internal type mismatch, native library returns null."); - - var result = handles.Select(x => new Output(x, false)).ToArray(); - - OutputChannel.Destroy(pointer); - return new Lazy(result); + return new Lazy(() => new JsonArray(handle, length, owner)); } case OutputType.Object: { - var pointer = OutputChannel.Object(handle).Checked(); - - var handlesArray = MemoryReader.TryReadIntPtrArray(pointer, length, Marshal.SizeOf()) - ?? throw new YDotNetException("Internal type mismatch, native library returns null."); - - var result = new Dictionary(); - - foreach (var itemHandle in handlesArray) - { - var (mapEntry, outputHandle) = MemoryReader.ReadMapEntryAndOutputHandle(itemHandle); - var mapEntryKey = MemoryReader.ReadUtf8String(mapEntry.Field); - - result[mapEntryKey] = new Output(outputHandle, false); - } - - OutputChannel.Destroy(pointer); - return new Lazy(result); + return new Lazy(() => new JsonObject(handle, length, owner)); } case OutputType.Array: @@ -239,6 +205,8 @@ internal Output(nint handle, bool shouldDispose) private T GetValue(OutputType expectedType) { + ThrowIfDisposed(); + var resolvedValue = value.Value; if (resolvedValue is not T typed) diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index 831d1082..ac6c4eb0 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -29,8 +29,10 @@ namespace YDotNet.Document; /// to recursively nested types). /// /// -public class Doc : IDisposable +public class Doc { + private readonly EventSubscriptions subscriptions = new EventSubscriptions(); + /// /// Initializes a new instance of the class. /// @@ -49,7 +51,9 @@ public Doc() /// The options to be used when initializing this document. public Doc(DocOptions options) { - Handle = DocChannel.NewWithOptions(DocOptionsNative.From(options)); + var unsafeOptions = DocOptionsNative.From(options); + + Handle = DocChannel.NewWithOptions(unsafeOptions); } /// @@ -82,7 +86,6 @@ public string? CollectionId get { MemoryReader.TryReadUtf8String(DocChannel.CollectionId(Handle), out var result); - return result; } } @@ -110,13 +113,6 @@ public string? CollectionId /// internal nint Handle { get; } - /// - public void Dispose() - { - GC.SuppressFinalize(this); - DocChannel.Destroy(Handle); - } - /// /// Creates a copy of the current instance. /// @@ -140,11 +136,11 @@ public Doc Clone() /// The instance related to the name provided. public Text Text(string name) { - var textName = MemoryWriter.WriteUtf8String(name); - var textHandle = DocChannel.Text(Handle, textName); - MemoryWriter.Release(textName); + using var unsafeName = MemoryWriter.WriteUtf8String(name); - return new Text(textHandle.Checked()); + var handle = DocChannel.Text(Handle, unsafeName.Handle); + + return new Text(handle.Checked()); } /// @@ -158,11 +154,11 @@ public Text Text(string name) /// The instance related to the name provided. public Map Map(string name) { - var mapName = MemoryWriter.WriteUtf8String(name); - var mapHandle = DocChannel.Map(Handle, mapName); - MemoryWriter.Release(mapName); + using var unsafeName = MemoryWriter.WriteUtf8String(name); + + var handle = DocChannel.Map(Handle, unsafeName.Handle); - return new Map(mapHandle.Checked()); + return new Map(handle.Checked()); } /// @@ -176,11 +172,11 @@ public Map Map(string name) /// The instance related to the name provided. public Array Array(string name) { - var arrayName = MemoryWriter.WriteUtf8String(name); - var arrayHandle = DocChannel.Array(Handle, arrayName); - MemoryWriter.Release(arrayName); + using var unsafeName = MemoryWriter.WriteUtf8String(name); + + var handle = DocChannel.Array(Handle, unsafeName.Handle); - return new Array(arrayHandle.Checked()); + return new Array(handle.Checked()); } /// @@ -194,11 +190,11 @@ public Array Array(string name) /// The instance related to the name provided. public XmlElement XmlElement(string name) { - var xmlElementName = MemoryWriter.WriteUtf8String(name); - var xmlElementHandle = DocChannel.XmlElement(Handle, xmlElementName); - MemoryWriter.Release(xmlElementName); + using var unsafeName = MemoryWriter.WriteUtf8String(name); - return new XmlElement(xmlElementHandle.Checked()); + var handle = DocChannel.XmlElement(Handle, unsafeName.Handle); + + return new XmlElement(handle.Checked()); } /// @@ -212,11 +208,11 @@ public XmlElement XmlElement(string name) /// The instance related to the name provided. public XmlText XmlText(string name) { - var xmlTextName = MemoryWriter.WriteUtf8String(name); - var xmlTextHandle = DocChannel.XmlText(Handle, xmlTextName); - MemoryWriter.Release(xmlTextName); + using var unsafeName = MemoryWriter.WriteUtf8String(name); + + var handle = DocChannel.XmlText(Handle, unsafeName.Handle); - return new XmlText(xmlTextHandle.Checked()); + return new XmlText(handle.Checked()); } /// @@ -257,11 +253,12 @@ public Transaction ReadTransaction() } /// - /// Destroys the current document, sending a destroy event and - /// clearing up all the registered callbacks. + /// Destroys the current document, sending a destroy event and clearing up all the registered callbacks. /// public void Clear() { + subscriptions.Clear(); + DocChannel.Clear(Handle); } @@ -282,24 +279,19 @@ public void Load(Transaction transaction) /// /// The callback function. /// The subscription for the event. It may be used to unsubscribe later. - public EventSubscription ObserveClear(Action action) + public IDisposable ObserveClear(Action action) { + DocChannel.ObserveClearCallback callback = (_, doc) => action(ClearEventNative.From(new Doc(doc)).ToClearEvent()); + var subscriptionId = DocChannel.ObserveClear( Handle, nint.Zero, - (_, doc) => action(ClearEventNative.From(new Doc(doc)).ToClearEvent())); - - return new EventSubscription(subscriptionId); - } + callback); - /// - /// Unsubscribes a callback function, represented by an instance, - /// for the method. - /// - /// The subscription that represents the callback function to be unobserved. - public void UnobserveClear(EventSubscription subscription) - { - DocChannel.UnobserveClear(Handle, subscription.Id); + return subscriptions.Add(callback, () => + { + DocChannel.UnobserveClear(Handle, subscriptionId); + }); } /// @@ -310,24 +302,19 @@ public void UnobserveClear(EventSubscription subscription) /// /// The callback to be executed when a is committed. /// The subscription for the event. It may be used to unsubscribe later. - public EventSubscription ObserveUpdatesV1(Action action) + public IDisposable ObserveUpdatesV1(Action action) { + DocChannel.ObserveUpdatesCallback callback = (_, length, data) => action(UpdateEventNative.From(length, data).ToUpdateEvent()); + var subscriptionId = DocChannel.ObserveUpdatesV1( Handle, nint.Zero, - (_, length, data) => action(UpdateEventNative.From(length, data).ToUpdateEvent())); - - return new EventSubscription(subscriptionId); - } + callback); - /// - /// Unsubscribes a callback function, represented by an instance, for changes - /// performed within scope. - /// - /// The subscription that represents the callback function to be unobserved. - public void UnobserveUpdatesV1(EventSubscription subscription) - { - DocChannel.UnobserveUpdatesV1(Handle, subscription.Id); + return subscriptions.Add(callback, () => + { + DocChannel.UnobserveUpdatesV1(Handle, subscriptionId); + }); } /// @@ -338,24 +325,19 @@ public void UnobserveUpdatesV1(EventSubscription subscription) /// /// The callback to be executed when a is committed. /// The subscription for the event. It may be used to unsubscribe later. - public EventSubscription ObserveUpdatesV2(Action action) + public IDisposable ObserveUpdatesV2(Action action) { + DocChannel.ObserveUpdatesCallback callback = (_, length, data) => action(UpdateEventNative.From(length, data).ToUpdateEvent()); + var subscriptionId = DocChannel.ObserveUpdatesV2( Handle, nint.Zero, - (_, length, data) => action(UpdateEventNative.From(length, data).ToUpdateEvent())); + callback); - return new EventSubscription(subscriptionId); - } - - /// - /// Unsubscribes a callback function, represented by an instance, for changes - /// performed within scope. - /// - /// The subscription that represents the callback function to be unobserved. - public void UnobserveUpdatesV2(EventSubscription subscription) - { - DocChannel.UnobserveUpdatesV2(Handle, subscription.Id); + return subscriptions.Add(callback, () => + { + DocChannel.UnobserveUpdatesV2(Handle, subscriptionId); + }); } /// @@ -366,24 +348,19 @@ public void UnobserveUpdatesV2(EventSubscription subscription) /// /// The callback to be executed when a is committed. /// The subscription for the event. It may be used to unsubscribe later. - public EventSubscription ObserveAfterTransaction(Action action) + public IDisposable ObserveAfterTransaction(Action action) { + DocChannel.ObserveAfterTransactionCallback callback = (_, eventHandler) => action(MemoryReader.ReadStruct(eventHandler).ToAfterTransactionEvent()); + var subscriptionId = DocChannel.ObserveAfterTransaction( Handle, nint.Zero, - (_, eventHandler) => action(MemoryReader.ReadStruct(eventHandler).ToAfterTransactionEvent())); - - return new EventSubscription(subscriptionId); - } + callback); - /// - /// Unsubscribes a callback function, represented by an instance, for changes - /// performed within scope. - /// - /// The subscription that represents the callback function to be unobserved. - public void UnobserveAfterTransaction(EventSubscription subscription) - { - DocChannel.UnobserveAfterTransaction(Handle, subscription.Id); + return subscriptions.Add(callback, () => + { + DocChannel.UnobserveAfterTransaction(Handle, subscriptionId); + }); } /// @@ -391,23 +368,18 @@ public void UnobserveAfterTransaction(EventSubscription subscription) /// /// The callback to be executed when a sub-document changes. /// The subscription for the event. It may be used to unsubscribe later. - public EventSubscription ObserveSubDocs(Action action) + public IDisposable ObserveSubDocs(Action action) { + DocChannel.ObserveSubdocsCallback callback = (_, eventHandle) => action(MemoryReader.ReadStruct(eventHandle).ToSubDocsEvent()); + var subscriptionId = DocChannel.ObserveSubDocs( Handle, nint.Zero, - (_, eventHandle) => action(MemoryReader.ReadStruct(eventHandle).ToSubDocsEvent())); + callback); - return new EventSubscription(subscriptionId); - } - - /// - /// Unsubscribes a callback function, represented by an instance, for changes - /// performed in the sub-documents. - /// - /// The subscription that represents the callback function to be unobserved. - public void UnobserveSubDocs(EventSubscription subscription) - { - DocChannel.UnobserveSubDocs(Handle, subscription.Id); + return subscriptions.Add(callback, () => + { + DocChannel.UnobserveSubDocs(Handle, subscriptionId); + }); } } diff --git a/YDotNet/Document/Events/EventSubscription.cs b/YDotNet/Document/Events/EventSubscription.cs deleted file mode 100644 index 6a6dd492..00000000 --- a/YDotNet/Document/Events/EventSubscription.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace YDotNet.Document.Events; - -/// -/// Represents a subscription to an event of . -/// -public class EventSubscription -{ - /// - /// Initializes a new instance of the class. - /// - /// The ID used to identify this instance of . - internal EventSubscription(uint id) - { - Id = id; - } - - /// - /// Gets the ID used to identify this subscription in the document. - /// - public uint Id { get; } -} diff --git a/YDotNet/Document/Events/EventSubscriptions.cs b/YDotNet/Document/Events/EventSubscriptions.cs new file mode 100644 index 00000000..7f71a05d --- /dev/null +++ b/YDotNet/Document/Events/EventSubscriptions.cs @@ -0,0 +1,45 @@ +using YDotNet.Infrastructure; + +namespace YDotNet.Document.Events; + +internal sealed class EventSubscriptions +{ + private readonly HashSet subscriptions = new HashSet(); + + public IDisposable Add(object callback, Action unsubscribe) + { + var subscription = new EventSubscription(callback, s => + { + subscriptions.Remove(s); + unsubscribe(); + }); + + subscriptions.Add(subscription); + return subscription; + } + + public void Clear() + { + subscriptions.Clear(); + } + + private sealed class EventSubscription : Resource + { + private readonly Action unsubscribe; + + internal EventSubscription(object callback, Action unsubscribe) + { + this.unsubscribe = unsubscribe; + + // Just holds a reference to the callback to prevent garbage collection. + Callback = callback; + } + + internal object? Callback { get; set; } + + protected override void DisposeCore(bool disposing) + { + unsubscribe(this); + } + } +} diff --git a/YDotNet/Document/Options/DocOptions.cs b/YDotNet/Document/Options/DocOptions.cs index fa02b27d..52648cea 100644 --- a/YDotNet/Document/Options/DocOptions.cs +++ b/YDotNet/Document/Options/DocOptions.cs @@ -57,7 +57,7 @@ public class DocOptions /// Read more about the possible values in . /// /// - public DocEncoding? Encoding { get; init; } + public DocEncoding Encoding { get; init; } /// /// Gets the flag that determines whether deleted blocks should be garbage collected during transaction commits. diff --git a/YDotNet/Document/StickyIndexes/StickyIndex.cs b/YDotNet/Document/StickyIndexes/StickyIndex.cs index 68d2a68b..8fc4c8ce 100644 --- a/YDotNet/Document/StickyIndexes/StickyIndex.cs +++ b/YDotNet/Document/StickyIndexes/StickyIndex.cs @@ -2,7 +2,6 @@ using YDotNet.Document.Types.Branches; using YDotNet.Infrastructure; using YDotNet.Native.StickyIndex; -using YDotNet.Native.Types; namespace YDotNet.Document.StickyIndexes; @@ -15,15 +14,15 @@ namespace YDotNet.Document.StickyIndexes; /// Also, placing a sticky index at the end of a will always point to the end of that /// . /// -public class StickyIndex : IDisposable +public class StickyIndex : UnmanagedResource { - /// - /// Initializes a new instance of the class. - /// - /// The handle to the native resource. internal StickyIndex(nint handle) + : base(handle) + { + } + + protected override void DisposeCore(bool disposing) { - Handle = handle; } /// @@ -31,17 +30,6 @@ internal StickyIndex(nint handle) /// public StickyAssociationType AssociationType => (StickyAssociationType)StickyIndexChannel.AssociationType(Handle); - /// - /// Gets the handle to the native resource. - /// - internal nint Handle { get; } - - /// - public void Dispose() - { - StickyIndexChannel.Destroy(Handle); - } - /// /// Creates a from the result of . /// @@ -76,10 +64,7 @@ public uint Read(Transaction transaction) public byte[] Encode() { var handle = StickyIndexChannel.Encode(Handle, out var length); - var result = MemoryReader.ReadBytes(handle, length); - - BinaryChannel.Destroy(handle, length); - return result; + return MemoryReader.ReadAndDestroyBytes(handle, length); } } diff --git a/YDotNet/Document/Transactions/Transaction.cs b/YDotNet/Document/Transactions/Transaction.cs index b16cdea7..820ce782 100644 --- a/YDotNet/Document/Transactions/Transaction.cs +++ b/YDotNet/Document/Transactions/Transaction.cs @@ -6,7 +6,6 @@ using YDotNet.Document.Types.XmlTexts; using YDotNet.Infrastructure; using YDotNet.Native.Transaction; -using YDotNet.Native.Types; using YDotNet.Native.Types.Branches; using Array = YDotNet.Document.Types.Arrays.Array; @@ -22,15 +21,19 @@ namespace YDotNet.Document.Transactions; /// /// A is automatically committed during . /// -public class Transaction : IDisposable +public class Transaction : UnmanagedResource { - /// - /// Initializes a new instance of the class. - /// - /// The handle to the native resource. internal Transaction(nint handle) + : base(handle) { - Handle = handle; + } + + protected override void DisposeCore(bool disposing) + { + if (disposing) + { + Commit(); + } } /// @@ -38,17 +41,6 @@ internal Transaction(nint handle) /// public bool Writeable => TransactionChannel.Writeable(Handle) == 1; - /// - /// Gets the handle to the native resource. - /// - internal nint Handle { get; } - - /// - public void Dispose() - { - Commit(); - } - /// /// Commit and dispose provided read-write transaction. /// @@ -58,6 +50,7 @@ public void Dispose() /// public void Commit() { + ThrowIfDisposed(); TransactionChannel.Commit(Handle); } @@ -68,12 +61,11 @@ public void Commit() public Doc[] SubDocs() { var handle = TransactionChannel.SubDocs(Handle, out var length); - var docs = new Doc[length]; + var docs = new Doc[length]; for (var i = 0; i < length; i++) { - var doc = new Doc(Marshal.ReadIntPtr(handle, i * nint.Size)); - docs[i] = doc; + docs[i] = new Doc(Marshal.ReadIntPtr(handle, i * nint.Size)); } return docs; @@ -95,10 +87,8 @@ public Doc[] SubDocs() public byte[] StateVectorV1() { var handle = TransactionChannel.StateVectorV1(Handle, out var length); - var data = MemoryReader.ReadBytes(handle, length); - BinaryChannel.Destroy(handle, length); - return data; + return MemoryReader.ReadAndDestroyBytes(handle, length); } /// @@ -128,10 +118,8 @@ public byte[] StateVectorV1() public byte[] StateDiffV1(byte[] stateVector) { var handle = TransactionChannel.StateDiffV1(Handle, stateVector, (uint)(stateVector != null ? stateVector.Length : 0), out var length); - var data = MemoryReader.ReadBytes(handle, length); - BinaryChannel.Destroy(handle, length); - return data; + return MemoryReader.ReadAndDestroyBytes(handle, length); } /// @@ -161,10 +149,8 @@ public byte[] StateDiffV1(byte[] stateVector) public byte[] StateDiffV2(byte[] stateVector) { var handle = TransactionChannel.StateDiffV2(Handle, stateVector, (uint)stateVector.Length, out var length); - var data = MemoryReader.ReadBytes(handle, length); - BinaryChannel.Destroy(handle, length); - return data; + return MemoryReader.ReadAndDestroyBytes(handle, length); } /// @@ -209,10 +195,8 @@ public TransactionUpdateResult ApplyV2(byte[] stateDiff) public byte[] Snapshot() { var handle = TransactionChannel.Snapshot(Handle, out var length); - var data = MemoryReader.ReadBytes(handle, length); - BinaryChannel.Destroy(handle, length); - return data; + return MemoryReader.ReadAndDestroyBytes(handle.Checked(), length); } /// @@ -247,10 +231,8 @@ public byte[] Snapshot() snapshot, (uint)snapshot.Length, out var length); - var data = MemoryReader.TryReadBytes(handle, length); - BinaryChannel.Destroy(handle, length); - return data; + return handle != nint.Zero ? MemoryReader.ReadAndDestroyBytes(handle, length) : null; } /// @@ -285,10 +267,8 @@ public byte[] Snapshot() snapshot, (uint)snapshot.Length, out var length); - var data = MemoryReader.TryReadBytes(handle, length); - BinaryChannel.Destroy(handle, length); - return data; + return handle != nint.Zero ? MemoryReader.ReadAndDestroyBytes(handle, length) : null; } /// @@ -371,13 +351,22 @@ public byte[] Snapshot() return handle != nint.Zero ? new XmlText(handle) : null; } - private nint GetWithKind(string name, BranchKind branchKind) + private nint GetWithKind(string name, BranchKind expectedKind) { - var nameHandle = MemoryWriter.WriteUtf8String(name); - var handle = TransactionChannel.Get(Handle, nameHandle); - var kind = (BranchKind)BranchChannel.Kind(handle); - MemoryWriter.Release(nameHandle); + using var unsafeName = MemoryWriter.WriteUtf8String(name); + + var branchHandle = TransactionChannel.Get(Handle, unsafeName.Handle); + if (branchHandle == nint.Zero) + { + return nint.Zero; + } + + var branchKind = (BranchKind)BranchChannel.Kind(branchHandle); + if (branchKind != expectedKind) + { + ThrowHelper.YDotnet($"Expected '{expectedKind}', got '{branchKind}'."); + } - return kind != branchKind ? nint.Zero : handle; + return branchHandle; } } diff --git a/YDotNet/Document/Types/Arrays/Array.cs b/YDotNet/Document/Types/Arrays/Array.cs index 9d920b89..7b22ea38 100644 --- a/YDotNet/Document/Types/Arrays/Array.cs +++ b/YDotNet/Document/Types/Arrays/Array.cs @@ -15,14 +15,11 @@ namespace YDotNet.Document.Types.Arrays; /// public class Array : Branch { - /// - /// Initializes a new instance of the class. - /// - /// The handle to the native resource. + private readonly EventSubscriptions subscriptions = new EventSubscriptions(); + internal Array(nint handle) : base(handle) { - // Nothing here. } /// @@ -36,12 +33,11 @@ internal Array(nint handle) /// The transaction that wraps this operation. /// The starting index to insert the items. /// The items to be inserted. - public void InsertRange(Transaction transaction, uint index, IEnumerable inputs) + public void InsertRange(Transaction transaction, uint index, params Input[] inputs) { - var inputsArray = inputs.Select(x => x.InputNative).ToArray(); - var inputsPointer = MemoryWriter.WriteStructArray(inputsArray); + using var unsafeInputs = MemoryWriter.WriteStructArray(inputs.Select(x => x.InputNative).ToArray()); - ArrayChannel.InsertRange(Handle, transaction.Handle, index, inputsPointer, (uint)inputsArray.Length); + ArrayChannel.InsertRange(Handle, transaction.Handle, index, unsafeInputs.Handle, (uint)inputs.Length); } /// @@ -69,7 +65,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) { var handle = ArrayChannel.Get(Handle, transaction.Handle, index); - return handle != nint.Zero ? new Output(handle, true) : null; + return handle != nint.Zero ? new Output(handle, null) : null; } /// @@ -107,24 +103,19 @@ public ArrayIterator Iterate(Transaction transaction) /// /// The callback to be executed when a is committed. /// The subscription for the event. It may be used to unsubscribe later. - public EventSubscription Observe(Action action) + public IDisposable Observe(Action action) { + ArrayChannel.ObserveCallback callback = (_, eventHandle) => action(new ArrayEvent(eventHandle)); + var subscriptionId = ArrayChannel.Observe( Handle, nint.Zero, - (_, eventHandle) => action(new ArrayEvent(eventHandle))); + callback); - return new EventSubscription(subscriptionId); - } - - /// - /// Unsubscribes a callback function, represented by an instance, for changes - /// performed within scope. - /// - /// The subscription that represents the callback function to be unobserved. - public void Unobserve(EventSubscription subscription) - { - ArrayChannel.Unobserve(Handle, subscription.Id); + return subscriptions.Add(callback, () => + { + ArrayChannel.Unobserve(Handle, subscriptionId); + }); } /// diff --git a/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs b/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs index a65b3ff5..e2749bee 100644 --- a/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs +++ b/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs @@ -12,18 +12,16 @@ internal class ArrayEnumerator : IEnumerator { private Output? current; - /// - /// Initializes a new instance of the class. - /// - /// - /// The instance used by this enumerator. - /// Check for more details. - /// internal ArrayEnumerator(ArrayIterator iterator) { Iterator = iterator; } + /// + public void Dispose() + { + } + /// public Output Current => current!; @@ -40,9 +38,10 @@ public bool MoveNext() { var handle = ArrayChannel.IteratorNext(Iterator.Handle); - current = handle != nint.Zero ? new Output(handle, false) : null; + // The output has now owner and can just be disposed when not needed anymore. + current = handle != nint.Zero ? new Output(handle, null) : null; - return Current != null; + return current != null; } /// @@ -50,10 +49,4 @@ public void Reset() { throw new NotImplementedException(); } - - /// - public void Dispose() - { - Iterator.Dispose(); - } } diff --git a/YDotNet/Document/Types/Arrays/ArrayIterator.cs b/YDotNet/Document/Types/Arrays/ArrayIterator.cs index 5c0acfb2..80ae54fa 100644 --- a/YDotNet/Document/Types/Arrays/ArrayIterator.cs +++ b/YDotNet/Document/Types/Arrays/ArrayIterator.cs @@ -1,5 +1,6 @@ using System.Collections; using YDotNet.Document.Cells; +using YDotNet.Infrastructure; using YDotNet.Native.Types; namespace YDotNet.Document.Types.Arrays; @@ -10,24 +11,19 @@ namespace YDotNet.Document.Types.Arrays; /// /// The iterator can't be reused. If needed, use to accumulate values. /// -public class ArrayIterator : IEnumerable, IDisposable +public class ArrayIterator : UnmanagedResource, IEnumerable { - /// - /// Initializes a new instance of the class. - /// - /// The handle to the native resource. internal ArrayIterator(nint handle) + : base(handle) { - Handle = handle; } - /// - /// Gets the handle to the native resource. - /// - internal nint Handle { get; } + ~ArrayIterator() + { + Dispose(false); + } - /// - public void Dispose() + protected override void DisposeCore(bool disposing) { ArrayChannel.IteratorDestroy(Handle); } @@ -35,6 +31,7 @@ public void Dispose() /// public IEnumerator GetEnumerator() { + ThrowIfDisposed(); return new ArrayEnumerator(this); } diff --git a/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs b/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs index 25e81ade..2d181ef7 100644 --- a/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs +++ b/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs @@ -7,39 +7,42 @@ namespace YDotNet.Document.Types.Arrays.Events; /// /// Represents the event that's part of an operation within a instance. /// -public class ArrayEvent +public class ArrayEvent : UnmanagedResource { private readonly Lazy path; private readonly Lazy delta; private readonly Lazy target; - /// - /// Initializes a new instance of the class. - /// - /// The handle to the native resource. internal ArrayEvent(nint handle) + : base(handle) { - Handle = handle; - path = new Lazy(() => { var pathHandle = ArrayChannel.ObserveEventPath(handle, out var length).Checked(); - return new EventPath(pathHandle, length); + + return new EventPath(pathHandle, length, this); }); delta = new Lazy(() => { var deltaHandle = ArrayChannel.ObserveEventDelta(handle, out var length).Checked(); - return new EventChanges(deltaHandle, length); + + return new EventChanges(deltaHandle, length, this); }); target = new Lazy(() => { var targetHandle = ArrayChannel.ObserveEventTarget(handle).Checked(); + return new Array(targetHandle); }); } + protected override void DisposeCore(bool disposing) + { + // The event has no explicit garbage collection, but is released automatically after the event has been completed. + } + /// /// Gets the path from the observed instanced down to the current instance. /// @@ -50,25 +53,11 @@ internal ArrayEvent(nint handle) /// Gets the changes within the instance and triggered this event. /// /// This property can only be accessed during the callback that exposes this instance. - public EventChanges Delta - { - get - { - var handle = ArrayChannel.ObserveEventDelta(Handle, out var length); - - return new EventChanges(handle, length); - } - } - - /// - /// Gets the handle to the native resource. - /// - internal nint Handle { get; } + public EventChanges Delta => delta.Value; /// /// Gets the instance that is related to this instance. /// /// The target of the event. - /// You are responsible to dispose the text, if you use this property. - public Array ResolveTarget() => target.Value; + public Array Target => target.Value; } diff --git a/YDotNet/Document/Types/Branches/Branch.cs b/YDotNet/Document/Types/Branches/Branch.cs index e5c27f84..58f1f383 100644 --- a/YDotNet/Document/Types/Branches/Branch.cs +++ b/YDotNet/Document/Types/Branches/Branch.cs @@ -11,6 +11,8 @@ namespace YDotNet.Document.Types.Branches; /// public abstract class Branch { + private readonly EventSubscriptions subscriptions = new EventSubscriptions(); + /// /// Initializes a new instance of the class. /// @@ -20,10 +22,7 @@ protected Branch(nint handle) Handle = handle; } - /// - /// Gets the handle to the native resource. - /// - internal nint Handle { get; } + public nint Handle { get; } /// /// Subscribes a callback function for changes performed within the instance @@ -34,31 +33,24 @@ protected Branch(nint handle) /// /// The callback to be executed when a is committed. /// The subscription for the event. It may be used to unsubscribe later. - public EventSubscription ObserveDeep(Action> action) + public IDisposable ObserveDeep(Action> action) { + BranchChannel.ObserveCallback callback = (_, length, eventsHandle) => + { + var events = MemoryReader.ReadIntPtrArray(eventsHandle, length, size: 24).Select(x => new EventBranch(x)).ToArray(); + + action(events); + }; + var subscriptionId = BranchChannel.ObserveDeep( Handle, nint.Zero, - (_, length, eventsHandle) => - { - var events = MemoryReader.TryReadIntPtrArray(eventsHandle, length, size: 24)! - .Select(x => new EventBranch(x)) - .ToArray(); - - action(events); - }); + callback); - return new EventSubscription(subscriptionId); - } - - /// - /// Unsubscribes a callback function, represented by an instance, for changes - /// performed within scope. - /// - /// The subscription that represents the callback function to be unobserved. - public void UnobserveDeep(EventSubscription subscription) - { - BranchChannel.UnobserveDeep(Handle, subscription.Id); + return subscriptions.Add(callback, () => + { + BranchChannel.UnobserveDeep(Handle, subscriptionId); + }); } /// diff --git a/YDotNet/Document/Types/Events/EventChanges.cs b/YDotNet/Document/Types/Events/EventChanges.cs index 13fedff4..4622040e 100644 --- a/YDotNet/Document/Types/Events/EventChanges.cs +++ b/YDotNet/Document/Types/Events/EventChanges.cs @@ -1,4 +1,3 @@ -using System.Collections.ObjectModel; using System.Runtime.InteropServices; using YDotNet.Infrastructure; using YDotNet.Native.Types; @@ -9,33 +8,29 @@ namespace YDotNet.Document.Types.Events; /// /// Represents a collection of instances. /// -public class EventChanges : ReadOnlyCollection +public class EventChanges : UnmanagedCollectionResource { - /// - /// Initializes a new instance of the class. - /// - /// The handle to the native resource. - /// The length of the array of to read from . - public EventChanges(nint handle, uint length) - : base(ReadItems(handle, length)) + private readonly uint length; + + public EventChanges(nint handle, uint length, IResourceOwner owner) + : base(handle, owner) { - Handle = handle; - } + foreach (var itemHandle in MemoryReader.ReadIntPtrArray(handle, length, Marshal.SizeOf())) + { + // The event delta creates output that are owned by this block of allocated memory. + AddItem(Marshal.PtrToStructure(itemHandle).ToEventChange(this)); + } - /// - /// Gets the handle to the native resource. - /// - internal nint Handle { get; } + this.length = length; + } - private static IList ReadItems(nint handle, uint length) + ~EventChanges() { - var result = MemoryReader.TryReadIntPtrArray(handle, length, Marshal.SizeOf())! - .Select(Marshal.PtrToStructure) - .Select(x => x.ToEventChange()) - .ToList(); + Dispose(true); + } - // We are done reading and can release the memory. - PathChannel.Destroy(handle, length); - return result; + protected override void DisposeCore(bool disposing) + { + EventChannel.DeltaDestroy(Handle, length); } } diff --git a/YDotNet/Document/Types/Events/EventDelta.cs b/YDotNet/Document/Types/Events/EventDelta.cs index 9fe3d93c..d4e85007 100644 --- a/YDotNet/Document/Types/Events/EventDelta.cs +++ b/YDotNet/Document/Types/Events/EventDelta.cs @@ -17,7 +17,7 @@ public class EventDelta /// . /// /// The attributes that are part of the changed content. - public EventDelta(EventDeltaTag tag, uint length, Output? insert, IEnumerable attributes) + public EventDelta(EventDeltaTag tag, uint length, Output? insert, List attributes) { Tag = tag; Length = length; @@ -43,5 +43,5 @@ public EventDelta(EventDeltaTag tag, uint length, Output? insert, IEnumerable /// Gets the attributes that are part of the changed content. /// - public IEnumerable Attributes { get; } + public IReadOnlyList Attributes { get; } } diff --git a/YDotNet/Document/Types/Events/EventDeltaAttribute.cs b/YDotNet/Document/Types/Events/EventDeltaAttribute.cs index f72f0b63..70b675a3 100644 --- a/YDotNet/Document/Types/Events/EventDeltaAttribute.cs +++ b/YDotNet/Document/Types/Events/EventDeltaAttribute.cs @@ -7,23 +7,20 @@ namespace YDotNet.Document.Types.Events; /// /// The formatting attribute that's part of an instance. /// -public class EventDeltaAttribute +public sealed class EventDeltaAttribute { private readonly Lazy value; - /// - /// Initializes a new instance of the class. - /// - /// The handle to the native resource. - public EventDeltaAttribute(nint handle) + internal EventDeltaAttribute(nint handle, IResourceOwner owner) { Handle = handle; - Key = Marshal.PtrToStringAnsi(Marshal.ReadIntPtr(handle)) ?? throw new InvalidOperationException("Failed to read key"); + Key = Marshal.PtrToStringAnsi(Marshal.ReadIntPtr(handle)) + ?? throw new InvalidOperationException("Failed to read key"); value = new Lazy(() => { - return new Output(handle + MemoryConstants.PointerSize, false); + return new Output(handle + MemoryConstants.PointerSize, owner); }); } diff --git a/YDotNet/Document/Types/Events/EventDeltas.cs b/YDotNet/Document/Types/Events/EventDeltas.cs index 740984d9..940d3e47 100644 --- a/YDotNet/Document/Types/Events/EventDeltas.cs +++ b/YDotNet/Document/Types/Events/EventDeltas.cs @@ -1,4 +1,3 @@ -using System.Collections.ObjectModel; using System.Runtime.InteropServices; using YDotNet.Infrastructure; using YDotNet.Native.Types; @@ -9,34 +8,29 @@ namespace YDotNet.Document.Types.Events; /// /// Represents a collection of instances. /// -public class EventDeltas : ReadOnlyCollection +public class EventDeltas : UnmanagedCollectionResource { - /// - /// Initializes a new instance of the class. - /// - /// The handle to the native resource. - /// The length of the array of to read from . - internal EventDeltas(nint handle, uint length) - : base(ReadItems(handle, length)) + private readonly uint length; + + internal EventDeltas(nint handle, uint length, IResourceOwner owner) + : base(handle, owner) { - Handle = handle; - } + this.length = length; - /// - /// Gets the handle to the native resource. - /// - internal nint Handle { get; } + foreach (var itemHandle in MemoryReader.ReadIntPtrArray(handle, length, Marshal.SizeOf())) + { + // The event delta creates output that are owned by this block of allocated memory. + AddItem(Marshal.PtrToStructure(itemHandle).ToEventDelta(this)); + } + } - private static IList ReadItems(nint handle, uint length) + ~EventDeltas() { - var result = MemoryReader.ReadIntPtrArray(handle, length, Marshal.SizeOf()) - .Select(Marshal.PtrToStructure) - .Select(x => x.ToEventDelta()) - .ToList(); - - // We are done reading and can release the memory. - EventChannel.DeltaDestroy(handle, length); + Dispose(false); + } - return result; + protected override void DisposeCore(bool disposing) + { + EventChannel.DeltaDestroy(Handle, length); } } diff --git a/YDotNet/Document/Types/Events/EventKeys.cs b/YDotNet/Document/Types/Events/EventKeys.cs index b1db4ed5..c6787049 100644 --- a/YDotNet/Document/Types/Events/EventKeys.cs +++ b/YDotNet/Document/Types/Events/EventKeys.cs @@ -1,4 +1,3 @@ -using System.Collections.ObjectModel; using System.Runtime.InteropServices; using YDotNet.Infrastructure; using YDotNet.Native.Types; @@ -10,33 +9,29 @@ namespace YDotNet.Document.Types.Events; /// Represents the keys that changed the shared type that emitted the event related to this /// instance. /// -public class EventKeys : ReadOnlyCollection +public class EventKeys : UnmanagedCollectionResource { - /// - /// Initializes a new instance of the class. - /// - /// The handle to the beginning of the array of instances. - /// The length of the array. - internal EventKeys(nint handle, uint length) - : base(ReadItems(handle, length)) + private readonly uint length; + + internal EventKeys(nint handle, uint length, IResourceOwner owner) + : base(handle, owner) { - Handle = handle; - } + foreach (var keyHandle in MemoryReader.ReadIntPtrArray(handle, length, Marshal.SizeOf())) + { + // The event delta creates output that are owned by this block of allocated memory. + AddItem(Marshal.PtrToStructure(keyHandle).ToEventKeyChange(this)); + } - /// - /// Gets the handle to the native resource. - /// - internal nint Handle { get; } + this.length = length; + } - private static IList ReadItems(nint handle, uint length) + ~EventKeys() { - var result = MemoryReader.ReadIntPtrArray(handle, length, Marshal.SizeOf()) - .Select(Marshal.PtrToStructure) - .Select(x => x.ToEventKeyChange()) - .ToList(); + Dispose(true); + } - // We are done reading and can release the memory. - EventChannel.KeysDestroy(handle, length); - return result; + protected override void DisposeCore(bool disposing) + { + EventChannel.KeysDestroy(Handle, length); } } diff --git a/YDotNet/Document/Types/Events/EventPath.cs b/YDotNet/Document/Types/Events/EventPath.cs index 50a7adef..f35941a9 100644 --- a/YDotNet/Document/Types/Events/EventPath.cs +++ b/YDotNet/Document/Types/Events/EventPath.cs @@ -1,4 +1,3 @@ -using System.Collections.ObjectModel; using YDotNet.Infrastructure; using YDotNet.Native.Types; @@ -8,30 +7,23 @@ namespace YDotNet.Document.Types.Events; /// Represents the path from the root type to the shared type that emitted the event related to this /// instance. /// -public class EventPath : ReadOnlyCollection +public sealed class EventPath : UnmanagedCollectionResource { - /// - /// Initializes a new instance of the class. - /// - /// The handle to the beginning of the array of instances. - /// The length of the array. - internal EventPath(nint handle, uint length) - : base(ReadItems(handle, length)) + internal EventPath(nint handle, uint length, IResourceOwner owner) + : base(handle, owner) { - Handle = handle; - } + foreach (var itemHandle in MemoryReader.ReadIntPtrArray(Handle, length, size: 16)) + { + // The segment does not make any further allocations. + AddItem(new EventPathSegment(itemHandle)); + } - /// - /// Gets the handle to the native resource. - /// - internal nint Handle { get; } + // We have read everything, so we can release the memory immediately. + PathChannel.Destroy(Handle, length); + } - private static IList ReadItems(nint handle, uint length) + protected override void DisposeCore(bool disposing) { - var result = MemoryReader.ReadIntPtrArray(handle, length, size: 16).Select(x => new EventPathSegment(x)).ToList(); - - // We are done reading and can release the memory. - PathChannel.Destroy(handle, length); - return result; + // We have read everything in the constructor, therefore there is no unmanaged memory to be released. } } diff --git a/YDotNet/Document/Types/Events/EventPathSegment.cs b/YDotNet/Document/Types/Events/EventPathSegment.cs index 66fbf1b1..96fef03a 100644 --- a/YDotNet/Document/Types/Events/EventPathSegment.cs +++ b/YDotNet/Document/Types/Events/EventPathSegment.cs @@ -20,8 +20,7 @@ public EventPathSegment(nint handle) switch (Tag) { case EventPathSegmentTag.Key: - var pointer = Marshal.ReadIntPtr(handle + MemoryConstants.PointerSize); - Key = Marshal.PtrToStringAnsi(pointer); + Key = Marshal.PtrToStringAnsi(Marshal.ReadIntPtr(handle + MemoryConstants.PointerSize)); break; case EventPathSegmentTag.Index: diff --git a/YDotNet/Document/Types/Maps/Events/MapEvent.cs b/YDotNet/Document/Types/Maps/Events/MapEvent.cs index dab77da9..d4b5e5f8 100644 --- a/YDotNet/Document/Types/Maps/Events/MapEvent.cs +++ b/YDotNet/Document/Types/Maps/Events/MapEvent.cs @@ -8,39 +8,42 @@ namespace YDotNet.Document.Types.Maps.Events; /// /// Represents the event that's part of an operation within a instance. /// -public class MapEvent +public class MapEvent : UnmanagedResource { private readonly Lazy path; private readonly Lazy keys; private readonly Lazy target; - /// - /// Initializes a new instance of the class. - /// - /// The handle to the native resource. internal MapEvent(nint handle) + : base(handle) { - Handle = handle; - path = new Lazy(() => { var pathHandle = ArrayChannel.ObserveEventPath(handle, out var length).Checked(); - return new EventPath(pathHandle, length); + + return new EventPath(pathHandle, length, this); }); keys = new Lazy(() => { var keysHandle = MapChannel.ObserveEventKeys(handle, out var length).Checked(); - return new EventKeys(keysHandle, length); + + return new EventKeys(keysHandle, length, this); }); target = new Lazy(() => { var targetHandle = MapChannel.ObserveEventTarget(handle).Checked(); + return new Map(targetHandle); }); } + protected override void DisposeCore(bool disposing) + { + // The event has no explicit garbage collection, but is released automatically after the event has been completed. + } + /// /// Gets the keys that changed within the instance and triggered this event. /// @@ -53,15 +56,9 @@ internal MapEvent(nint handle) /// This property can only be accessed during the callback that exposes this instance. public EventPath Path => path.Value; - /// - /// Gets the handle to the native resource. - /// - internal nint Handle { get; } - /// /// Gets the instance that is related to this instance. /// /// The target of the event. - /// You are responsible to dispose the text, if you use this property. - public Map ResolveTarget() => target.Value; + public Map Target => target.Value; } diff --git a/YDotNet/Document/Types/Maps/Map.cs b/YDotNet/Document/Types/Maps/Map.cs index 75468446..5b9f12e2 100644 --- a/YDotNet/Document/Types/Maps/Map.cs +++ b/YDotNet/Document/Types/Maps/Map.cs @@ -13,14 +13,11 @@ namespace YDotNet.Document.Types.Maps; /// public class Map : Branch { - /// - /// Initializes a new instance of the class. - /// - /// The handle to the native resource. + private readonly EventSubscriptions subscriptions = new EventSubscriptions(); + internal Map(nint handle) : base(handle) { - // Nothing here. } /// @@ -34,13 +31,10 @@ internal Map(nint handle) /// The instance to be inserted. public void Insert(Transaction transaction, string key, Input input) { - var keyHandle = MemoryWriter.WriteUtf8String(key); - var valuePointer = MemoryWriter.WriteStruct(input.InputNative); + using var unsafeKey = MemoryWriter.WriteUtf8String(key); + using var unsafeValue = MemoryWriter.WriteStruct(input.InputNative); - MapChannel.Insert(Handle, transaction.Handle, keyHandle, valuePointer); - - MemoryWriter.Release(keyHandle); - MemoryWriter.Release(valuePointer); + MapChannel.Insert(Handle, transaction.Handle, unsafeKey.Handle, unsafeValue.Handle); } /// @@ -54,11 +48,15 @@ public void Insert(Transaction transaction, string key, Input input) /// The or null if entry not found. public Output? Get(Transaction transaction, string key) { - var outputName = MemoryWriter.WriteUtf8String(key); - var outputHandle = MapChannel.Get(Handle, transaction.Handle, outputName); - MemoryWriter.Release(outputName); + using var unsafeName = MemoryWriter.WriteUtf8String(key); + var outputHandle = MapChannel.Get(Handle, transaction.Handle, unsafeName.Handle); + + if (outputHandle == nint.Zero) + { + return null; + } - return outputHandle != nint.Zero ? new Output(outputHandle, false) : null; + return new Output(outputHandle, null); } /// @@ -79,12 +77,9 @@ public uint Length(Transaction transaction) /// `true` if the entry was found and removed, `false` if no entry was found. public bool Remove(Transaction transaction, string key) { - var keyHandle = MemoryWriter.WriteUtf8String(key); - var result = MapChannel.Remove(Handle, transaction.Handle, keyHandle) == 1; - - MemoryWriter.Release(keyHandle); + using var unsafeKey = MemoryWriter.WriteUtf8String(key); - return result; + return MapChannel.Remove(Handle, transaction.Handle, unsafeKey.Handle) == 1; } /// @@ -117,23 +112,18 @@ public MapIterator Iterate(Transaction transaction) /// /// The callback to be executed when a is committed. /// The subscription for the event. It may be used to unsubscribe later. - public EventSubscription Observe(Action action) + public IDisposable Observe(Action action) { + MapChannel.ObserveCallback callback = (_, eventHandle) => action(new MapEvent(eventHandle)); + var subscriptionId = MapChannel.Observe( Handle, nint.Zero, - (_, eventHandle) => action(new MapEvent(eventHandle))); - - return new EventSubscription(subscriptionId); - } + callback); - /// - /// Unsubscribes a callback function, represented by an instance, for changes - /// performed within scope. - /// - /// The subscription that represents the callback function to be unobserved. - public void Unobserve(EventSubscription subscription) - { - MapChannel.Unobserve(Handle, subscription.Id); + return subscriptions.Add(callback, () => + { + MapChannel.Unobserve(Handle, subscriptionId); + }); } } diff --git a/YDotNet/Document/Types/Maps/MapEntry.cs b/YDotNet/Document/Types/Maps/MapEntry.cs index 037d5a7e..63460e89 100644 --- a/YDotNet/Document/Types/Maps/MapEntry.cs +++ b/YDotNet/Document/Types/Maps/MapEntry.cs @@ -5,29 +5,28 @@ namespace YDotNet.Document.Types.Maps; /// -/// Represents an entry of a . It contains a and . +/// Represents an entry of a . /// -public sealed class MapEntry +public sealed class MapEntry : UnmanagedResource { - /// - /// Initializes a new instance of the class. - /// - /// The handle to the native resource. - internal MapEntry(nint handle, bool shouldDispose) + internal MapEntry(nint handle, IResourceOwner? owner) + : base(handle) { - Handle = handle; - var (mapEntry, outputHandle) = MemoryReader.ReadMapEntryAndOutputHandle(handle); Key = MemoryReader.ReadUtf8String(mapEntry.Field); - // The output memory is part of the entry memory. Therefore we don't release it. - Value = new Output(outputHandle, false); + // The output can not released independently and will be released when the entry is not needed anymore. + Value = new Output(outputHandle, owner ?? this); + } - if (shouldDispose) - { - // We are done reading and can release the memory. - MapChannel.EntryDestroy(handle); - } + ~MapEntry() + { + Dispose(false); + } + + protected override void DisposeCore(bool disposing) + { + MapChannel.EntryDestroy(Handle); } /// @@ -39,9 +38,4 @@ internal MapEntry(nint handle, bool shouldDispose) /// Gets the value of this that is represented the . /// public Output Value { get; } - - /// - /// Gets the handle to the native resource. - /// - internal nint Handle { get; } } diff --git a/YDotNet/Document/Types/Maps/MapEnumerator.cs b/YDotNet/Document/Types/Maps/MapEnumerator.cs index 1314fc45..f5c3fe58 100644 --- a/YDotNet/Document/Types/Maps/MapEnumerator.cs +++ b/YDotNet/Document/Types/Maps/MapEnumerator.cs @@ -4,23 +4,20 @@ namespace YDotNet.Document.Types.Maps; /// -/// Represents the iterator to provide instances of or null to -/// . +/// Represents the iterator to provide instances of . /// internal class MapEnumerator : IEnumerator { private MapEntry? current; - /// - /// Initializes a new instance of the class. - /// - /// - /// The instance used by this enumerator. - /// Check for more details. - /// - internal MapEnumerator(MapIterator mapIterator) + internal MapEnumerator(MapIterator iterator) + { + Iterator = iterator; + } + + /// + public void Dispose() { - MapIterator = mapIterator; } /// @@ -30,16 +27,17 @@ internal MapEnumerator(MapIterator mapIterator) object? IEnumerator.Current => current!; /// - /// Gets the instance that holds the used by this enumerator. + /// Gets the instance that holds the used by this enumerator. /// - private MapIterator MapIterator { get; } + private MapIterator Iterator { get; } /// public bool MoveNext() { - var handle = MapChannel.IteratorNext(MapIterator.Handle); + var handle = MapChannel.IteratorNext(Iterator.Handle); - current = handle != nint.Zero ? new MapEntry(handle, true) : null; + // The map entry also manages the value of the output. + current = handle != nint.Zero ? new MapEntry(handle, Iterator) : null; return current != null; } @@ -49,10 +47,4 @@ public void Reset() { throw new NotImplementedException(); } - - /// - public void Dispose() - { - MapIterator.Dispose(); - } } diff --git a/YDotNet/Document/Types/Maps/MapIterator.cs b/YDotNet/Document/Types/Maps/MapIterator.cs index efc4f493..3e5b2a8e 100644 --- a/YDotNet/Document/Types/Maps/MapIterator.cs +++ b/YDotNet/Document/Types/Maps/MapIterator.cs @@ -1,4 +1,5 @@ using System.Collections; +using YDotNet.Infrastructure; using YDotNet.Native.Types.Maps; namespace YDotNet.Document.Types.Maps; @@ -7,7 +8,7 @@ namespace YDotNet.Document.Types.Maps; /// Represents an enumerable to read instances from a . /// /// -/// Two important details about : +/// Two important details about . ///
    ///
  • The instances are unordered when iterating;
  • ///
  • @@ -16,24 +17,19 @@ namespace YDotNet.Document.Types.Maps; ///
  • ///
///
-public class MapIterator : IEnumerable, IDisposable +public class MapIterator : UnmanagedResource, IEnumerable { - /// - /// Initializes a new instance of the class. - /// - /// The handle to the native resource. internal MapIterator(nint handle) + : base(handle) { - Handle = handle; } - /// - /// Gets the handle to the native resource. - /// - internal nint Handle { get; } + ~MapIterator() + { + Dispose(false); + } - /// - public void Dispose() + protected override void DisposeCore(bool disposing) { MapChannel.IteratorDestroy(Handle); } @@ -41,6 +37,7 @@ public void Dispose() /// public IEnumerator GetEnumerator() { + ThrowIfDisposed(); return new MapEnumerator(this); } diff --git a/YDotNet/Document/Types/Texts/Events/TextEvent.cs b/YDotNet/Document/Types/Texts/Events/TextEvent.cs index 6d17dd21..a70a85c7 100644 --- a/YDotNet/Document/Types/Texts/Events/TextEvent.cs +++ b/YDotNet/Document/Types/Texts/Events/TextEvent.cs @@ -7,7 +7,7 @@ namespace YDotNet.Document.Types.Texts.Events; /// /// Represents the event that's part of an operation within a instance. /// -public class TextEvent +public class TextEvent : UnmanagedResource { private readonly Lazy path; private readonly Lazy deltas; @@ -18,28 +18,35 @@ public class TextEvent ///
/// The handle to the native resource. internal TextEvent(nint handle) + : base(handle) { - Handle = handle; - path = new Lazy(() => { var pathHandle = TextChannel.ObserveEventPath(handle, out var length).Checked(); - return new EventPath(pathHandle, length); + + return new EventPath(pathHandle, length, this); }); deltas = new Lazy(() => { var deltaHandle = TextChannel.ObserveEventDelta(handle, out var length).Checked(); - return new EventDeltas(deltaHandle, length); + + return new EventDeltas(deltaHandle, length, this); }); target = new Lazy(() => { var targetHandle = TextChannel.ObserveEventTarget(handle).Checked(); + return new Text(targetHandle); }); } + protected override void DisposeCore(bool disposing) + { + // The event has no explicit garbage collection, but is released automatically after the event has been completed. + } + /// /// Gets the keys that changed within the instance and triggered this event. /// @@ -52,15 +59,9 @@ internal TextEvent(nint handle) /// This property can only be accessed during the callback that exposes this instance. public EventPath Path => path.Value; - /// - /// Gets the handle to the native resource. - /// - internal nint Handle { get; } - /// /// Gets the instance that is related to this instance. /// /// The target of the event. - /// You are responsible to dispose the text, if you use this property. - public Text ResolveTarget() => target.Value; + public Text Target => target.Value; } diff --git a/YDotNet/Document/Types/Texts/Text.cs b/YDotNet/Document/Types/Texts/Text.cs index bad87aef..6197fad9 100644 --- a/YDotNet/Document/Types/Texts/Text.cs +++ b/YDotNet/Document/Types/Texts/Text.cs @@ -6,7 +6,6 @@ using YDotNet.Document.Types.Texts.Events; using YDotNet.Infrastructure; using YDotNet.Native.StickyIndex; -using YDotNet.Native.Types; using YDotNet.Native.Types.Texts; namespace YDotNet.Document.Types.Texts; @@ -16,14 +15,11 @@ namespace YDotNet.Document.Types.Texts; ///
public class Text : Branch { - /// - /// Initializes a new instance of the class. - /// - /// The handle to the native resource. + private readonly EventSubscriptions subscriptions = new EventSubscriptions(); + internal Text(nint handle) : base(handle) { - // Nothing here. } /// @@ -38,13 +34,10 @@ internal Text(nint handle) /// public void Insert(Transaction transaction, uint index, string value, Input? attributes = null) { - var valueHandle = MemoryWriter.WriteUtf8String(value); - MemoryWriter.TryToWriteStruct(attributes?.InputNative, out var attributesHandle); + using var unsafeValue = MemoryWriter.WriteUtf8String(value); + using var unsafeAttributes = MemoryWriter.WriteStruct(attributes?.InputNative); - TextChannel.Insert(Handle, transaction.Handle, index, valueHandle, attributesHandle); - - MemoryWriter.TryRelease(attributesHandle); - MemoryWriter.Release(valueHandle); + TextChannel.Insert(Handle, transaction.Handle, index, unsafeValue.Handle, unsafeAttributes.Handle); } /// @@ -59,11 +52,10 @@ public void Insert(Transaction transaction, uint index, string value, Input? att /// public void InsertEmbed(Transaction transaction, uint index, Input content, Input? attributes = null) { - MemoryWriter.TryToWriteStruct(attributes?.InputNative, out var attributesPointer); - var contentPointer = MemoryWriter.WriteStruct(content.InputNative); - TextChannel.InsertEmbed(Handle, transaction.Handle, index, contentPointer, attributesPointer); - MemoryWriter.TryRelease(attributesPointer); - MemoryWriter.TryRelease(contentPointer); + var unsafeContent = MemoryWriter.WriteStruct(content.InputNative); + var unsafeAttributes = MemoryWriter.WriteStruct(attributes?.InputNative); + + TextChannel.InsertEmbed(Handle, transaction.Handle, index, unsafeContent.Handle, unsafeAttributes.Handle); } /// @@ -95,10 +87,9 @@ public void RemoveRange(Transaction transaction, uint index, uint length) /// public void Format(Transaction transaction, uint index, uint length, Input attributes) { - var attributesHnadle = MemoryWriter.WriteStruct(attributes.InputNative); - TextChannel.Format(Handle, transaction.Handle, index, length, attributesHnadle); + using var unsafeAttributes = MemoryWriter.WriteStruct(attributes.InputNative); - MemoryWriter.Release(attributesHnadle); + TextChannel.Format(Handle, transaction.Handle, index, length, unsafeAttributes.Handle); } /// @@ -120,14 +111,9 @@ public TextChunks Chunks(Transaction transaction) /// The full string stored in the instance. public string String(Transaction transaction) { - // Get the string pointer and read it into a managed instance. var handle = TextChannel.String(Handle, transaction.Handle); - var result = MemoryReader.ReadUtf8String(handle); - - // Dispose the resources used by the underlying string. - StringChannel.Destroy(handle); - return result; + return MemoryReader.ReadStringAndDestroy(handle); } /// @@ -148,24 +134,19 @@ public uint Length(Transaction transaction) /// /// The callback to be executed when a is committed. /// The subscription for the event. It may be used to unsubscribe later. - public EventSubscription Observe(Action action) + public IDisposable Observe(Action action) { + TextChannel.ObserveCallback callback = (_, eventHandle) => action(new TextEvent(eventHandle)); + var subscriptionId = TextChannel.Observe( Handle, nint.Zero, - (_, eventHandle) => action(new TextEvent(eventHandle))); - - return new EventSubscription(subscriptionId); - } + callback); - /// - /// Unsubscribes a callback function, represented by an instance, for changes - /// performed within scope. - /// - /// The subscription that represents the callback function to be unobserved. - public void Unobserve(EventSubscription subscription) - { - TextChannel.Unobserve(Handle, subscription.Id); + return subscriptions.Add(callback, () => + { + TextChannel.Unobserve(Handle, subscriptionId); + }); } /// diff --git a/YDotNet/Document/Types/Texts/TextChunk.cs b/YDotNet/Document/Types/Texts/TextChunk.cs index f2ee7bd0..5ad925b7 100644 --- a/YDotNet/Document/Types/Texts/TextChunk.cs +++ b/YDotNet/Document/Types/Texts/TextChunk.cs @@ -1,4 +1,3 @@ -using System.Reflection.Metadata.Ecma335; using System.Runtime.InteropServices; using YDotNet.Document.Cells; using YDotNet.Document.Types.Maps; @@ -13,13 +12,9 @@ namespace YDotNet.Document.Types.Texts; /// public class TextChunk { - /// - /// Initializes a new instance of the class. - /// - /// The handle to the native resource. - internal TextChunk(nint handle) + internal TextChunk(nint handle, IResourceOwner owner) { - Data = new Output(handle, false); + Data = new Output(handle, owner); var offset = Marshal.SizeOf(); @@ -33,10 +28,10 @@ internal TextChunk(nint handle) } Attributes = MemoryReader.ReadIntPtrArray( - attributesHandle, - attributesLength, - Marshal.SizeOf()) - .Select(x => new MapEntry(x, false)).ToList(); + attributesHandle, + attributesLength, + Marshal.SizeOf()) + .Select(x => new MapEntry(x, owner)).ToList(); } /// diff --git a/YDotNet/Document/Types/Texts/TextChunks.cs b/YDotNet/Document/Types/Texts/TextChunks.cs index 43fa42de..427c59a7 100644 --- a/YDotNet/Document/Types/Texts/TextChunks.cs +++ b/YDotNet/Document/Types/Texts/TextChunks.cs @@ -1,4 +1,3 @@ -using System.Collections.ObjectModel; using YDotNet.Infrastructure; using YDotNet.Native.Types; @@ -7,31 +6,35 @@ namespace YDotNet.Document.Types.Texts; /// /// Represents a collection of instances. /// -public class TextChunks : ReadOnlyCollection +public class TextChunks : UnmanagedCollectionResource { - /// - /// Initializes a new instance of the class. - /// - /// The handle to the native resource. - /// The length of instances to be read from . + private readonly uint length; + internal TextChunks(nint handle, uint length) - : base(ReadItems(handle, length)) + : base(handle, null) { - Handle = handle; - } + foreach (var chunkHandle in MemoryReader.ReadIntPtrArray(handle, length, size: 32)) + { + // The cunks create output that are owned by this block of allocated memory. + AddItem(new TextChunk(chunkHandle, this)); + } + + GC.Collect(); - /// - /// Gets the handle to the native resource. - /// - internal nint Handle { get; } + this.length = length; + } - private static IList ReadItems(nint handle, uint length) + ~TextChunks() { - var result = MemoryReader.ReadIntPtrArray(handle, length, size: 32).Select(x => new TextChunk(x)).ToList(); + Dispose(true); + } - // We are done reading and can release the memory. - // ChunksChannel.Destroy(handle, length); + protected override void DisposeCore(bool disposing) + { - return result; + Console.WriteLine("---DISPOSE {0} - {1}", Handle, length); + Console.Out.Flush(); + Thread.Sleep(100); + ChunksChannel.Destroy(Handle, length); } } diff --git a/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs b/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs index 88e63bcb..7e5b99fe 100644 --- a/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs +++ b/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs @@ -7,7 +7,7 @@ namespace YDotNet.Document.Types.XmlElements.Events; /// /// Represents the event that's part of an operation within an instance. /// -public class XmlElementEvent +public class XmlElementEvent : UnmanagedResource { private readonly Lazy path; private readonly Lazy delta; @@ -19,34 +19,42 @@ public class XmlElementEvent /// /// The handle to the native resource. internal XmlElementEvent(nint handle) + : base(handle) { - Handle = handle; - path = new Lazy(() => { var pathHandle = XmlElementChannel.ObserveEventPath(handle, out var length).Checked(); - return new EventPath(pathHandle, length); + + return new EventPath(pathHandle, length, this); }); delta = new Lazy(() => { var deltaHandle = XmlElementChannel.ObserveEventDelta(handle, out var length).Checked(); - return new EventChanges(deltaHandle, length); + + return new EventChanges(deltaHandle, length, this); }); keys = new Lazy(() => { var keysHandle = XmlElementChannel.ObserveEventKeys(handle, out var length).Checked(); - return new EventKeys(keysHandle, length); + + return new EventKeys(keysHandle, length, this); }); target = new Lazy(() => { var targetHandle = XmlElementChannel.ObserveEventTarget(handle).Checked(); + return new XmlElement(targetHandle); }); } + protected override void DisposeCore(bool disposing) + { + // The event has no explicit garbage collection, but is released automatically after the event has been completed. + } + /// /// Gets the changes within the instance and triggered this event. /// @@ -65,15 +73,9 @@ internal XmlElementEvent(nint handle) /// This property can only be accessed during the callback that exposes this instance. public EventKeys Keys => keys.Value; - /// - /// Gets the handle to the native resource. - /// - internal nint Handle { get; } - /// /// Gets the instance that is related to this instance. /// /// The target of the event. - /// You are responsible to dispose the text, if you use this property. - public XmlElement ResolveTarget() => target.Value; + public XmlElement Target => target.Value; } diff --git a/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalker.cs b/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalker.cs index e92f699c..e1194527 100644 --- a/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalker.cs +++ b/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalker.cs @@ -1,5 +1,6 @@ using System.Collections; using YDotNet.Document.Cells; +using YDotNet.Infrastructure; using YDotNet.Native.Types; namespace YDotNet.Document.Types.XmlElements.Trees; @@ -11,24 +12,19 @@ namespace YDotNet.Document.Types.XmlElements.Trees; /// The traverses values using depth-first and nodes can be either /// or nodes. /// -public class XmlTreeWalker : IEnumerable, IDisposable +public class XmlTreeWalker : UnmanagedResource, IEnumerable { - /// - /// Initializes a new instance of the class. - /// - /// The handle to the native resource. - public XmlTreeWalker(nint handle) + internal XmlTreeWalker(nint handle) + : base(handle) { - Handle = handle; } - /// - /// Gets the handle to the native resource. - /// - internal nint Handle { get; } + ~XmlTreeWalker() + { + Dispose(false); + } - /// - public void Dispose() + protected override void DisposeCore(bool disposing) { XmlElementChannel.TreeWalkerDestroy(Handle); } diff --git a/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs b/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs index 74653ad4..96a05b88 100644 --- a/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs +++ b/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs @@ -24,6 +24,11 @@ public XmlTreeWalkerEnumerator(XmlTreeWalker treeWalker) TreeWalker = treeWalker; } + /// + public void Dispose() + { + } + /// public Output Current => current!; @@ -41,7 +46,7 @@ public bool MoveNext() { var handle = XmlElementChannel.TreeWalkerNext(TreeWalker.Handle); - current = handle != nint.Zero ? new Output(handle, false) : null; + current = handle != nint.Zero ? new Output(handle, null) : null; return Current != null; } @@ -51,10 +56,4 @@ public void Reset() { throw new NotImplementedException(); } - - /// - public void Dispose() - { - TreeWalker.Dispose(); - } } diff --git a/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs b/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs index 2f5e21bf..f635b921 100644 --- a/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs +++ b/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs @@ -23,6 +23,11 @@ internal XmlAttributeEnumerator(XmlAttributeIterator iterator) Iterator = iterator; } + /// + public void Dispose() + { + } + /// public XmlAttribute Current => current!; @@ -50,10 +55,4 @@ public void Reset() { throw new NotImplementedException(); } - - /// - public void Dispose() - { - Iterator.Dispose(); - } } diff --git a/YDotNet/Document/Types/XmlElements/XmlAttributeIterator.cs b/YDotNet/Document/Types/XmlElements/XmlAttributeIterator.cs index 36d57f77..e542fc45 100644 --- a/YDotNet/Document/Types/XmlElements/XmlAttributeIterator.cs +++ b/YDotNet/Document/Types/XmlElements/XmlAttributeIterator.cs @@ -1,4 +1,5 @@ using System.Collections; +using YDotNet.Infrastructure; using YDotNet.Native.Types; namespace YDotNet.Document.Types.XmlElements; @@ -10,24 +11,19 @@ namespace YDotNet.Document.Types.XmlElements; /// /// The iterator can't be reused. If needed, use to accumulate values. /// -public class XmlAttributeIterator : IEnumerable, IDisposable +public class XmlAttributeIterator : UnmanagedResource, IEnumerable { - /// - /// Initializes a new instance of the class. - /// - /// The handle to the native resource. internal XmlAttributeIterator(nint handle) + : base(handle) { - Handle = handle; } - /// - /// Gets the handle to the native resource. - /// - internal nint Handle { get; } + ~XmlAttributeIterator() + { + Dispose(false); + } - /// - public void Dispose() + protected override void DisposeCore(bool disposing) { XmlAttributeChannel.IteratorDestroy(Handle); } diff --git a/YDotNet/Document/Types/XmlElements/XmlElement.cs b/YDotNet/Document/Types/XmlElements/XmlElement.cs index dd114467..4008b2f0 100644 --- a/YDotNet/Document/Types/XmlElements/XmlElement.cs +++ b/YDotNet/Document/Types/XmlElements/XmlElement.cs @@ -1,4 +1,3 @@ -using System.Runtime.InteropServices; using YDotNet.Document.Cells; using YDotNet.Document.Events; using YDotNet.Document.Transactions; @@ -16,14 +15,11 @@ namespace YDotNet.Document.Types.XmlElements; /// public class XmlElement : Branch { - /// - /// Initializes a new instance of the class. - /// - /// The handle to the native resource. + private readonly EventSubscriptions subscriptions = new EventSubscriptions(); + internal XmlElement(nint handle) : base(handle) { - // Nothing here. } /// @@ -37,10 +33,8 @@ public string? Tag get { var handle = XmlElementChannel.Tag(Handle); - var result = Marshal.PtrToStringAnsi(handle); - StringChannel.Destroy(handle); - return result; + return handle != nint.Zero ? MemoryReader.ReadStringAndDestroy(handle) : null; } } @@ -55,10 +49,8 @@ public string? Tag public string String(Transaction transaction) { var handle = XmlElementChannel.String(Handle, transaction.Handle); - var result = MemoryReader.ReadUtf8String(handle); - StringChannel.Destroy(handle); - return result; + return MemoryReader.ReadStringAndDestroy(handle.Checked()); } /// @@ -72,13 +64,10 @@ public string String(Transaction transaction) /// The value of the attribute to be added. public void InsertAttribute(Transaction transaction, string name, string value) { - var nameHandle = MemoryWriter.WriteUtf8String(name); - var valueHandle = MemoryWriter.WriteUtf8String(value); + using var unsageName = MemoryWriter.WriteUtf8String(name); + using var unsafeValue = MemoryWriter.WriteUtf8String(value); - XmlElementChannel.InsertAttribute(Handle, transaction.Handle, nameHandle, valueHandle); - - MemoryWriter.Release(nameHandle); - MemoryWriter.Release(valueHandle); + XmlElementChannel.InsertAttribute(Handle, transaction.Handle, unsageName.Handle, unsafeValue.Handle); } /// @@ -88,11 +77,9 @@ public void InsertAttribute(Transaction transaction, string name, string value) /// The name of the attribute to be removed. public void RemoveAttribute(Transaction transaction, string name) { - var nameHandle = MemoryWriter.WriteUtf8String(name); - - XmlElementChannel.RemoveAttribute(Handle, transaction.Handle, nameHandle); + using var unsafeName = MemoryWriter.WriteUtf8String(name); - MemoryWriter.Release(nameHandle); + XmlElementChannel.RemoveAttribute(Handle, transaction.Handle, unsafeName.Handle); } /// @@ -103,12 +90,11 @@ public void RemoveAttribute(Transaction transaction, string name) /// The value of the attribute or null if it doesn't exist. public string? GetAttribute(Transaction transaction, string name) { - var nameHandle = MemoryWriter.WriteUtf8String(name); - var handle = XmlElementChannel.GetAttribute(Handle, transaction.Handle, nameHandle); - MemoryReader.TryReadUtf8String(handle, out var result); - StringChannel.Destroy(handle); + using var unsafeName = MemoryWriter.WriteUtf8String(name); - return result; + var handle = XmlElementChannel.GetAttribute(Handle, transaction.Handle, unsafeName.Handle); + + return handle != nint.Zero ? MemoryReader.ReadStringAndDestroy(handle) : null; } /// @@ -162,9 +148,9 @@ public XmlText InsertText(Transaction transaction, uint index) /// The inserted at the given . public XmlElement InsertElement(Transaction transaction, uint index, string name) { - var elementName = MemoryWriter.WriteUtf8String(name); - var elementHandle = XmlElementChannel.InsertElement(Handle, transaction.Handle, index, elementName); - MemoryWriter.Release(elementName); + using var unsafeName = MemoryWriter.WriteUtf8String(name); + + var elementHandle = XmlElementChannel.InsertElement(Handle, transaction.Handle, index, unsafeName.Handle); return new XmlElement(elementHandle.Checked()); } @@ -191,7 +177,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) { var handle = XmlElementChannel.Get(Handle, transaction.Handle, index); - return handle != nint.Zero ? new Output(handle, true) : null; + return handle != nint.Zero ? new Output(handle, null) : null; } /// @@ -208,7 +194,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) { var handle = XmlChannel.PreviousSibling(Handle, transaction.Handle); - return handle != nint.Zero ? new Output(handle, true) : null; + return handle != nint.Zero ? new Output(handle, null) : null; } /// @@ -225,39 +211,39 @@ public void RemoveRange(Transaction transaction, uint index, uint length) { var handle = XmlChannel.NextSibling(Handle, transaction.Handle); - return handle != nint.Zero ? new Output(handle, true) : null; + return handle != nint.Zero ? new Output(handle, null) : null; } /// - /// Returns the parent of the current node or - /// null if the current node is root-level node. + /// Returns the first child of the current node which can be an + /// or an or null if this node is empty. /// /// The transaction that wraps this operation. /// - /// The parent of the current node or - /// null if the current node is root-level node. + /// The first child of the current node which can be an + /// or an or null if this node is empty. /// - public XmlElement? Parent(Transaction transaction) + public Output? FirstChild(Transaction transaction) { - var handle = XmlElementChannel.Parent(Handle, transaction.Handle); + var handle = XmlElementChannel.FirstChild(Handle, transaction.Handle); - return handle != nint.Zero ? new XmlElement(handle) : null; + return handle != nint.Zero ? new Output(handle, null) : null; } /// - /// Returns the first child of the current node which can be an - /// or an or null if this node is empty. + /// Returns the parent of the current node or + /// null if the current node is root-level node. /// /// The transaction that wraps this operation. /// - /// The first child of the current node which can be an - /// or an or null if this node is empty. + /// The parent of the current node or + /// null if the current node is root-level node. /// - public Output? FirstChild(Transaction transaction) + public XmlElement? Parent(Transaction transaction) { - var handle = XmlElementChannel.FirstChild(Handle, transaction.Handle); + var handle = XmlElementChannel.Parent(Handle, transaction.Handle); - return handle != nint.Zero ? new Output(handle, true) : null; + return handle != nint.Zero ? new XmlElement(handle) : null; } /// @@ -283,23 +269,18 @@ public XmlTreeWalker TreeWalker(Transaction transaction) /// /// The callback to be executed when a is committed. /// The subscription for the event. It may be used to unsubscribe later. - public EventSubscription Observe(Action action) + public IDisposable Observe(Action action) { + XmlElementChannel.ObserveCallback callback = (_, eventHandle) => action(new XmlElementEvent(eventHandle)); + var subscriptionId = XmlElementChannel.Observe( Handle, nint.Zero, - (_, eventHandle) => action(new XmlElementEvent(eventHandle))); - - return new EventSubscription(subscriptionId); - } + callback); - /// - /// Unsubscribes a callback function, represented by an instance, that - /// was subscribed via . - /// - /// The subscription that represents the callback function to be unobserved. - public void Unobserve(EventSubscription subscription) - { - XmlElementChannel.Unobserve(Handle, subscription.Id); + return subscriptions.Add(callback, () => + { + XmlElementChannel.Unobserve(Handle, subscriptionId); + }); } } diff --git a/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs b/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs index 95220384..9aae346e 100644 --- a/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs +++ b/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs @@ -1,4 +1,3 @@ -using System.IO; using YDotNet.Document.Types.Events; using YDotNet.Infrastructure; using YDotNet.Native.Types; @@ -8,39 +7,42 @@ namespace YDotNet.Document.Types.XmlTexts.Events; /// /// Represents the event that's part of an operation within an instance. /// -public class XmlTextEvent +public class XmlTextEvent : UnmanagedResource { private readonly Lazy delta; private readonly Lazy keys; private readonly Lazy target; - /// - /// Initializes a new instance of the class. - /// - /// The handle to the native resource. internal XmlTextEvent(nint handle) + : base(handle) { - Handle = handle; - delta = new Lazy(() => { var deltaHandle = XmlTextChannel.ObserveEventDelta(handle, out var length).Checked(); - return new EventDeltas(deltaHandle, length); + + return new EventDeltas(deltaHandle, length, this); }); keys = new Lazy(() => { var keysHandle = XmlTextChannel.ObserveEventKeys(handle, out var length).Checked(); - return new EventKeys(keysHandle, length); + + return new EventKeys(keysHandle, length, this); }); target = new Lazy(() => { var targetHandle = XmlTextChannel.ObserveEventTarget(handle).Checked(); + return new XmlText(targetHandle); }); } + protected override void DisposeCore(bool disposing) + { + // The event has no explicit garbage collection, but is released automatically after the event has been completed. + } + /// /// Gets the changes that triggered this event. /// @@ -53,15 +55,9 @@ internal XmlTextEvent(nint handle) /// This property can only be accessed during the callback that exposes this instance. public EventKeys Keys => keys.Value; - /// - /// Gets the handle to the native resource. - /// - internal nint Handle { get; } - /// /// Gets the instance that is related to this instance. /// /// The target of the event. - /// You are responsible to dispose the text, if you use this property. - public XmlText ResolveTarget() => target.Value; + public XmlText Target => target.Value; } diff --git a/YDotNet/Document/Types/XmlTexts/XmlText.cs b/YDotNet/Document/Types/XmlTexts/XmlText.cs index 5fb813a8..501e9ef5 100644 --- a/YDotNet/Document/Types/XmlTexts/XmlText.cs +++ b/YDotNet/Document/Types/XmlTexts/XmlText.cs @@ -16,14 +16,11 @@ namespace YDotNet.Document.Types.XmlTexts; /// public class XmlText : Branch { - /// - /// Initializes a new instance of the class. - /// - /// The handle to the native resource. + private readonly EventSubscriptions subscriptions = new EventSubscriptions(); + internal XmlText(nint handle) : base(handle) { - // Nothing here. } /// @@ -48,13 +45,10 @@ public uint Length(Transaction transaction) /// public void Insert(Transaction transaction, uint index, string value, Input? attributes = null) { - var valueHandle = MemoryWriter.WriteUtf8String(value); - MemoryWriter.TryToWriteStruct(attributes?.InputNative, out var attributesHandle); - - XmlTextChannel.Insert(Handle, transaction.Handle, index, valueHandle, attributesHandle); + using var unsafeValue = MemoryWriter.WriteUtf8String(value); + using var unsafeAttributes = MemoryWriter.WriteStruct(attributes?.InputNative); - MemoryWriter.TryRelease(attributesHandle); - MemoryWriter.Release(valueHandle); + XmlTextChannel.Insert(Handle, transaction.Handle, index, unsafeValue.Handle, unsafeAttributes.Handle); } /// @@ -69,11 +63,10 @@ public void Insert(Transaction transaction, uint index, string value, Input? att /// public void InsertEmbed(Transaction transaction, uint index, Input content, Input? attributes = null) { - MemoryWriter.TryToWriteStruct(attributes?.InputNative, out var attributesPointer); - var contentPointer = MemoryWriter.WriteStruct(content.InputNative); - XmlTextChannel.InsertEmbed(Handle, transaction.Handle, index, contentPointer, attributesPointer); - MemoryWriter.TryRelease(attributesPointer); - MemoryWriter.TryRelease(contentPointer); + using var unsafeContent = MemoryWriter.WriteStruct(content.InputNative); + using var unsafeAttributes = MemoryWriter.WriteStruct(attributes?.InputNative); + + XmlTextChannel.InsertEmbed(Handle, transaction.Handle, index, unsafeContent.Handle, unsafeAttributes.Handle); } /// @@ -87,13 +80,10 @@ public void InsertEmbed(Transaction transaction, uint index, Input content, Inpu /// The value of the attribute to be added. public void InsertAttribute(Transaction transaction, string name, string value) { - var nameHandle = MemoryWriter.WriteUtf8String(name); - var valueHandle = MemoryWriter.WriteUtf8String(value); - - XmlTextChannel.InsertAttribute(Handle, transaction.Handle, nameHandle, valueHandle); + using var unsafeName = MemoryWriter.WriteUtf8String(name); + using var unsafeValue = MemoryWriter.WriteUtf8String(value); - MemoryWriter.Release(nameHandle); - MemoryWriter.Release(valueHandle); + XmlTextChannel.InsertAttribute(Handle, transaction.Handle, unsafeName.Handle, unsafeValue.Handle); } /// @@ -103,11 +93,9 @@ public void InsertAttribute(Transaction transaction, string name, string value) /// The name of the attribute to be removed. public void RemoveAttribute(Transaction transaction, string name) { - var nameHandle = MemoryWriter.WriteUtf8String(name); + using var unsafeName = MemoryWriter.WriteUtf8String(name); - XmlTextChannel.RemoveAttribute(Handle, transaction.Handle, nameHandle); - - MemoryWriter.Release(nameHandle); + XmlTextChannel.RemoveAttribute(Handle, transaction.Handle, unsafeName.Handle); } /// @@ -118,12 +106,11 @@ public void RemoveAttribute(Transaction transaction, string name) /// The value of the attribute or null if it doesn't exist. public string? GetAttribute(Transaction transaction, string name) { - var nameHandle = MemoryWriter.WriteUtf8String(name); - var handle = XmlTextChannel.GetAttribute(Handle, transaction.Handle, nameHandle); - MemoryReader.TryReadUtf8String(handle, out var result); - StringChannel.Destroy(handle); + using var unsafeName = MemoryWriter.WriteUtf8String(name); + + var handle = XmlTextChannel.GetAttribute(Handle, transaction.Handle, unsafeName.Handle); - return result; + return handle != nint.Zero ? MemoryReader.ReadStringAndDestroy(handle) : null; } /// @@ -146,10 +133,8 @@ public XmlAttributeIterator Iterate(Transaction transaction) public string String(Transaction transaction) { var handle = XmlTextChannel.String(Handle, transaction.Handle); - var result = MemoryReader.ReadUtf8String(handle); - StringChannel.Destroy(handle); - return result; + return MemoryReader.ReadStringAndDestroy(handle); } /// @@ -181,9 +166,9 @@ public void RemoveRange(Transaction transaction, uint index, uint length) /// public void Format(Transaction transaction, uint index, uint length, Input attributes) { - var attributesPointer = MemoryWriter.WriteStruct(attributes.InputNative); - XmlTextChannel.Format(Handle, transaction.Handle, index, length, attributesPointer); - MemoryWriter.Release(attributesPointer); + using var unsafeAttributes = MemoryWriter.WriteStruct(attributes?.InputNative); + + XmlTextChannel.Format(Handle, transaction.Handle, index, length, unsafeAttributes.Handle); } /// @@ -199,7 +184,7 @@ public void Format(Transaction transaction, uint index, uint length, Input attri { var handle = XmlChannel.PreviousSibling(Handle, transaction.Handle); - return handle != nint.Zero ? new Output(handle, true) : null; + return handle != nint.Zero ? new Output(handle, null) : null; } /// @@ -215,7 +200,7 @@ public void Format(Transaction transaction, uint index, uint length, Input attri { var handle = XmlChannel.NextSibling(Handle, transaction.Handle); - return handle != nint.Zero ? new Output(handle, true) : null; + return handle != nint.Zero ? new Output(handle, null) : null; } /// @@ -226,24 +211,19 @@ public void Format(Transaction transaction, uint index, uint length, Input attri /// /// The callback to be executed when a is committed. /// The subscription for the event. It may be used to unsubscribe later. - public EventSubscription Observe(Action action) + public IDisposable Observe(Action action) { + XmlTextChannel.ObserveCallback callback = (_, eventHandle) => action(new XmlTextEvent(eventHandle)); + var subscriptionId = XmlTextChannel.Observe( Handle, nint.Zero, - (_, eventHandle) => action(new XmlTextEvent(eventHandle))); - - return new EventSubscription(subscriptionId); - } + callback); - /// - /// Unsubscribes a callback function, represented by an instance, that - /// was subscribed via . - /// - /// The subscription that represents the callback function to be unobserved. - public void Unobserve(EventSubscription subscription) - { - XmlTextChannel.Unobserve(Handle, subscription.Id); + return subscriptions.Add(callback, () => + { + XmlTextChannel.Unobserve(Handle, subscriptionId); + }); } /// diff --git a/YDotNet/Document/UndoManagers/UndoManager.cs b/YDotNet/Document/UndoManagers/UndoManager.cs index d56d7ba7..765bdc53 100644 --- a/YDotNet/Document/UndoManagers/UndoManager.cs +++ b/YDotNet/Document/UndoManagers/UndoManager.cs @@ -10,8 +10,10 @@ namespace YDotNet.Document.UndoManagers; /// /// The is used to perform undo/redo operations over shared types in a . /// -public class UndoManager : IDisposable +public class UndoManager : UnmanagedResource { + private readonly EventSubscriptions subscriptions = new EventSubscriptions(); + /// /// Initializes a new instance of the class. /// @@ -19,21 +21,23 @@ public class UndoManager : IDisposable /// The shared type in the to operate over. /// The options to initialize the . public UndoManager(Doc doc, Branch branch, UndoManagerOptions? options = null) + : base(Create(doc, branch, options)) { - MemoryWriter.TryToWriteStruct(UndoManagerOptionsNative.From(options), out var optionsHandle); + } - Handle = UndoManagerChannel.NewWithOptions(doc.Handle, branch.Handle, optionsHandle); + private static nint Create(Doc doc, Branch branch, UndoManagerOptions? options) + { + var unsafeOptions = MemoryWriter.WriteStruct(UndoManagerOptionsNative.From(options)); - MemoryWriter.TryRelease(optionsHandle); + return UndoManagerChannel.NewWithOptions(doc.Handle, branch.Handle, unsafeOptions.Handle); } - /// - /// Gets the handle to the native resource. - /// - internal nint Handle { get; } + ~UndoManager() + { + Dispose(false); + } - /// - public void Dispose() + protected override void DisposeCore(bool disposing) { UndoManagerChannel.Destroy(Handle); } @@ -45,24 +49,19 @@ public void Dispose() /// /// The callback to be executed when an update happens, respecting the capture timeout. /// The subscription for the event. It may be used to unsubscribe later. - public EventSubscription ObserveAdded(Action action) + public IDisposable ObserveAdded(Action action) { + UndoManagerChannel.ObserveAddedCallback callback = (_, undoEvent) => action(undoEvent.ToUndoEvent()); + var subscriptionId = UndoManagerChannel.ObserveAdded( Handle, nint.Zero, - (_, undoEvent) => action(undoEvent.ToUndoEvent())); + callback); - return new EventSubscription(subscriptionId); - } - - /// - /// Unsubscribes a callback function, represented by an instance, previously - /// registered via . - /// - /// The subscription that represents the callback function to be unobserved. - public void UnobserveAdded(EventSubscription subscription) - { - UndoManagerChannel.UnobserveAdded(Handle, subscription.Id); + return subscriptions.Add(callback, () => + { + UndoManagerChannel.UnobserveAdded(Handle, subscriptionId); + }); } /// @@ -71,24 +70,19 @@ public void UnobserveAdded(EventSubscription subscription) /// /// The callback to be executed when or is executed. /// The subscription for the event. It may be used to unsubscribe later. - public EventSubscription ObservePopped(Action action) + public IDisposable ObservePopped(Action action) { + UndoManagerChannel.ObservePoppedCallback callback = (_, undoEvent) => action(undoEvent.ToUndoEvent()); + var subscriptionId = UndoManagerChannel.ObservePopped( Handle, nint.Zero, - (_, undoEvent) => action(undoEvent.ToUndoEvent())); + callback); - return new EventSubscription(subscriptionId); - } - - /// - /// Unsubscribes a callback function, represented by an instance, previously - /// registered via . - /// - /// The subscription that represents the callback function to be unobserved. - public void UnobservePopped(EventSubscription subscription) - { - UndoManagerChannel.UnobservePopped(Handle, subscription.Id); + return subscriptions.Add(callback, () => + { + UndoManagerChannel.UnobservePopped(Handle, subscriptionId); + }); } /// diff --git a/YDotNet/Infrastructure/IResourceOwner.cs b/YDotNet/Infrastructure/IResourceOwner.cs new file mode 100644 index 00000000..75f340be --- /dev/null +++ b/YDotNet/Infrastructure/IResourceOwner.cs @@ -0,0 +1,6 @@ +namespace YDotNet.Infrastructure; + +public interface IResourceOwner +{ + bool IsDisposed { get; } +} diff --git a/YDotNet/Infrastructure/MemoryReader.cs b/YDotNet/Infrastructure/MemoryReader.cs index 1d9b4ee9..7cbbc6ee 100644 --- a/YDotNet/Infrastructure/MemoryReader.cs +++ b/YDotNet/Infrastructure/MemoryReader.cs @@ -1,5 +1,6 @@ using System.Runtime.InteropServices; using System.Text; +using YDotNet.Native.Types; using YDotNet.Native.Types.Maps; namespace YDotNet.Infrastructure; @@ -9,15 +10,8 @@ internal static class MemoryReader internal static unsafe byte[] ReadBytes(nint handle, uint length) { var data = new byte[length]; - var stream = new UnmanagedMemoryStream((byte*)handle.ToPointer(), length); - int bytesRead; - do - { - bytesRead = stream.Read(data, offset: 0, data.Length); - } while (bytesRead < data.Length); - - stream.Dispose(); + Marshal.Copy(handle, data, 0, (int)length); return data; } @@ -59,7 +53,6 @@ internal static (MapEntryNative MapEntryNative, nint OutputHandle) ReadMapEntryA internal static string ReadUtf8String(nint handle) { ReadOnlySpan readOnlySpan; - unsafe { var index = 0; @@ -70,7 +63,6 @@ internal static string ReadUtf8String(nint handle) { // Decrease the index to discard the zero byte. index--; - break; } } @@ -93,4 +85,20 @@ internal static bool TryReadUtf8String(nint handle, out string? result) return true; } + + public static byte[] ReadAndDestroyBytes(nint handle, uint length) + { + var data = ReadBytes(handle, length); + + BinaryChannel.Destroy(handle, length); + return data; + } + + public static string ReadStringAndDestroy(nint handle) + { + var result = ReadUtf8String(handle); + + StringChannel.Destroy(handle); + return result; + } } diff --git a/YDotNet/Infrastructure/MemoryWriter.cs b/YDotNet/Infrastructure/MemoryWriter.cs index 5a729bc9..bf7699cb 100644 --- a/YDotNet/Infrastructure/MemoryWriter.cs +++ b/YDotNet/Infrastructure/MemoryWriter.cs @@ -5,115 +5,107 @@ namespace YDotNet.Infrastructure; internal static class MemoryWriter { - internal static unsafe nint WriteUtf8String(string value) + internal static unsafe DisposableHandle WriteUtf8String(string? value) { - var bytes = Encoding.UTF8.GetBytes(value + '\0'); - var pointer = Marshal.AllocHGlobal(bytes.Length); - - using var stream = new UnmanagedMemoryStream( - (byte*)pointer.ToPointer(), - length: 0, - bytes.Length, - FileAccess.Write); - - stream.Write(bytes); + if (value == null) + { + return new DisposableHandle(0); + } - return pointer; + return new DisposableHandle(WriteUtf8StringCore(value)); } - internal static bool TryWriteUtf8String(string? value, out nint pointer) + private static unsafe nint WriteUtf8StringCore(string value) { - if (value != null) - { - pointer = WriteUtf8String(value); - return true; - } + var bufferLength = Encoding.UTF8.GetByteCount(value) + 1; + var bufferPointer = Marshal.AllocHGlobal(bufferLength); - pointer = default; - return false; + var memory = new Span(bufferPointer.ToPointer(), bufferLength); + + Encoding.UTF8.GetBytes(value, memory); + memory[bufferLength - 1] = (byte)'\0'; + + return bufferPointer; } - internal static (nint Head, nint[] Pointers) WriteUtf8StringArray(string[] values) + internal static DisposableHandles WriteUtf8StringArray(string[] values) { var head = Marshal.AllocHGlobal(MemoryConstants.PointerSize * values.Length); + var pointers = new nint[values.Length]; for (var i = 0; i < values.Length; i++) { - pointers[i] = WriteUtf8String(values[i]); + pointers[i] = WriteUtf8StringCore(values[i]); Marshal.WriteIntPtr(head + i * MemoryConstants.PointerSize, pointers[i]); } - return (head, pointers); + return new DisposableHandles(head, pointers); } - internal static nint WriteStructArray(T[] value) + internal static DisposableHandle WriteStructArray(T[] value) where T : struct { - var size = Marshal.SizeOf(); - var handle = Marshal.AllocHGlobal(size * value.Length); + var itemSize = Marshal.SizeOf(); + var itemBuffer = Marshal.AllocHGlobal(itemSize * value.Length); for (var i = 0; i < value.Length; i++) { - Marshal.StructureToPtr(value[i], handle + i * size, fDeleteOld: false); + Marshal.StructureToPtr(value[i], itemBuffer + i * itemSize, fDeleteOld: false); } - return handle; + return new DisposableHandle(itemBuffer); } - internal static nint WriteStruct(T value) - where T : struct - { - var handle = Marshal.AllocHGlobal(Marshal.SizeOf(value)); - - Marshal.StructureToPtr(value, handle, fDeleteOld: false); - - return handle; - } - - internal static bool TryToWriteStruct(T? value, out nint handle) + internal static DisposableHandle WriteStruct(T? value) where T : struct { if (value == null) { - handle = nint.Zero; - - return false; + return new DisposableHandle(0); } - handle = WriteStruct(value.Value); - - return true; + return WriteStruct(value.Value); } - internal static void Release(nint pointer) + internal static DisposableHandle WriteStruct(T value) + where T : struct { - Marshal.FreeHGlobal(pointer); + var handle = Marshal.AllocHGlobal(Marshal.SizeOf(value)); + + Marshal.StructureToPtr(value, handle, fDeleteOld: false); + + return new DisposableHandle(handle); } - internal static void ReleaseArray(nint[] pointers) + internal sealed record DisposableHandle(nint Handle) : IDisposable { - foreach (var pointer in pointers) + public void Dispose() { - Release(pointer); + if (Handle != nint.Zero) + { + Marshal.FreeHGlobal(Handle); + } } } - internal static bool TryRelease(nint pointer) + internal sealed record DisposableHandles(nint Handle, nint[] Handles) : IDisposable { - if (pointer == nint.Zero) + public void Dispose() { - return false; + if (Handle != nint.Zero) + { + Marshal.FreeHGlobal(Handle); + } + + foreach (var handle in Handles) + { + if (handle != nint.Zero) + { + Marshal.FreeHGlobal(handle); + } + } } - - // This method doesn't throw if called with `nint.Zero` but having a `Try*` version - // makes the API more future-friendly and easier to understand for C# developers. - // - // If they called a `TryWrite*` method, they should call a `TryRelease` method too. - // Otherwise, they should call `Release`. - Release(pointer); - - return true; } } diff --git a/YDotNet/Infrastructure/Resource.cs b/YDotNet/Infrastructure/Resource.cs new file mode 100644 index 00000000..7dfbb00d --- /dev/null +++ b/YDotNet/Infrastructure/Resource.cs @@ -0,0 +1,40 @@ +namespace YDotNet.Infrastructure; + +public abstract class Resource : IResourceOwner, IDisposable +{ + protected Resource(IResourceOwner? owner = null) + { + this.Owner = owner; + } + + public bool IsDisposed { get; private set; } + + public IResourceOwner? Owner { get; } + + public void Dispose() + { + GC.SuppressFinalize(this); + Dispose(true); + } + + protected void ThrowIfDisposed() + { + if (IsDisposed || Owner?.IsDisposed == true) + { + throw new ObjectDisposedException(GetType().Name); + } + } + + protected void Dispose(bool disposing) + { + if (IsDisposed) + { + return; + } + + DisposeCore(disposing); + IsDisposed = true; + } + + protected abstract void DisposeCore(bool disposing); +} diff --git a/YDotNet/Infrastructure/UnmanagedCollectionResource.cs b/YDotNet/Infrastructure/UnmanagedCollectionResource.cs new file mode 100644 index 00000000..a96fbd39 --- /dev/null +++ b/YDotNet/Infrastructure/UnmanagedCollectionResource.cs @@ -0,0 +1,36 @@ +using System.Collections; + +namespace YDotNet.Infrastructure; + +public abstract class UnmanagedCollectionResource : UnmanagedResource, IReadOnlyList +{ + private readonly List items = new List(); + + public UnmanagedCollectionResource(nint handle, IResourceOwner? owner) + : base(handle, owner) + { + } + + /// + public T this[int index] => items[index]; + + /// + public int Count => items.Count; + + protected void AddItem(T item) + { + items.Add(item); + } + + /// + public IEnumerator GetEnumerator() + { + return items.GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return items.GetEnumerator(); + } +} diff --git a/YDotNet/Infrastructure/UnmanagedResource.cs b/YDotNet/Infrastructure/UnmanagedResource.cs new file mode 100644 index 00000000..9d9bf916 --- /dev/null +++ b/YDotNet/Infrastructure/UnmanagedResource.cs @@ -0,0 +1,12 @@ +namespace YDotNet.Infrastructure; + +public abstract class UnmanagedResource : Resource +{ + protected UnmanagedResource(nint handle, IResourceOwner? owner = null) + : base(owner) + { + Handle = handle; + } + + public nint Handle { get; } +} diff --git a/YDotNet/Native/Cells/Inputs/InputChannel.cs b/YDotNet/Native/Cells/Inputs/InputChannel.cs index 7c0c88fb..84c42130 100644 --- a/YDotNet/Native/Cells/Inputs/InputChannel.cs +++ b/YDotNet/Native/Cells/Inputs/InputChannel.cs @@ -4,50 +4,93 @@ namespace YDotNet.Native.Cells.Inputs; internal static class InputChannel { - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yinput_ydoc")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yinput_ydoc")] public static extern InputNative Doc(nint value); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yinput_string")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yinput_string")] public static extern InputNative String(nint value); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yinput_bool")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yinput_bool")] public static extern InputNative Boolean(bool value); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yinput_float")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yinput_float")] public static extern InputNative Double(double value); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yinput_long")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yinput_long")] public static extern InputNative Long(long value); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yinput_binary")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yinput_binary")] public static extern InputNative Bytes(byte[] value, uint length); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yinput_json_array")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yinput_json_array")] public static extern InputNative Collection(nint values, uint length); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yinput_json_map")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yinput_json_map")] public static extern InputNative Object(nint keys, nint values, uint length); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yinput_null")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yinput_null")] public static extern InputNative Null(); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yinput_undefined")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yinput_undefined")] public static extern InputNative Undefined(); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yinput_yarray")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yinput_yarray")] public static extern InputNative Array(nint values, uint length); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yinput_ymap")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yinput_ymap")] public static extern InputNative Map(nint keys, nint values, uint length); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yinput_ytext")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yinput_ytext")] public static extern InputNative Text(nint value); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yinput_yxmlelem")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yinput_yxmlelem")] public static extern InputNative XmlElement(nint value); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yinput_yxmltext")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yinput_yxmltext")] public static extern InputNative XmlText(nint value); } diff --git a/YDotNet/Native/Cells/Outputs/OutputChannel.cs b/YDotNet/Native/Cells/Outputs/OutputChannel.cs index 38773af3..23b80a2d 100644 --- a/YDotNet/Native/Cells/Outputs/OutputChannel.cs +++ b/YDotNet/Native/Cells/Outputs/OutputChannel.cs @@ -5,39 +5,57 @@ namespace YDotNet.Native.Cells.Outputs; internal static class OutputChannel { [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "youtput_read_ydoc")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "youtput_read_ydoc")] public static extern nint Doc(nint output); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "youtput_read_string")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "youtput_read_string")] public static extern nint String(nint output); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "youtput_read_bool")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "youtput_read_bool")] public static extern nint Boolean(nint output); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "youtput_read_float")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "youtput_read_float")] public static extern nint Double(nint output); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "youtput_read_long")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "youtput_read_long")] public static extern nint Long(nint output); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "youtput_read_binary")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "youtput_read_binary")] public static extern nint Bytes(nint output); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "youtput_read_json_array")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "youtput_read_json_array")] public static extern nint Collection(nint output); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "youtput_read_json_map")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "youtput_read_json_map")] public static extern nint Object(nint output); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "youtput_is_json_null")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "youtput_is_json_null")] public static extern byte Null(nint output); [DllImport( @@ -47,25 +65,38 @@ internal static class OutputChannel public static extern byte Undefined(nint output); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "youtput_read_yarray")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "youtput_read_yarray")] public static extern nint Array(nint output); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "youtput_read_ymap")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "youtput_read_ymap")] public static extern nint Map(nint output); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "youtput_read_ytext")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "youtput_read_ytext")] public static extern nint Text(nint output); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "youtput_read_yxmlelem")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "youtput_read_yxmlelem")] public static extern nint XmlElement(nint output); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "youtput_read_yxmltext")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "youtput_read_yxmltext")] public static extern nint XmlText(nint output); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "youtput_destroy")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "youtput_destroy")] public static extern void Destroy(nint output); } diff --git a/YDotNet/Native/Document/DocChannel.cs b/YDotNet/Native/Document/DocChannel.cs index 77ea1ada..84ae3a35 100644 --- a/YDotNet/Native/Document/DocChannel.cs +++ b/YDotNet/Native/Document/DocChannel.cs @@ -13,73 +13,123 @@ internal static class DocChannel public delegate void ObserveUpdatesCallback(nint state, uint length, nint data); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ydoc_new_with_options")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ydoc_new_with_options")] public static extern nint NewWithOptions(DocOptionsNative options); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ydoc_clone")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ydoc_clone")] public static extern nint Clone(nint doc); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ydoc_destroy")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ydoc_destroy")] public static extern void Destroy(nint doc); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ydoc_id")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ydoc_id")] public static extern ulong Id(nint doc); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ydoc_guid")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ydoc_guid")] public static extern nint Guid(nint doc); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ydoc_collection_id")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ydoc_collection_id")] public static extern nint CollectionId(nint doc); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ydoc_should_load")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ydoc_should_load")] public static extern bool ShouldLoad(nint doc); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ydoc_auto_load")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ydoc_auto_load")] public static extern bool AutoLoad(nint doc); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ytext")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ytext")] public static extern nint Text(nint doc, nint name); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ymap")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ymap")] public static extern nint Map(nint doc, nint name); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yarray")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yarray")] public static extern nint Array(nint doc, nint name); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmlelem")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmlelem")] public static extern nint XmlElement(nint doc, nint name); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmltext")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmltext")] public static extern nint XmlText(nint doc, nint name); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ydoc_read_transaction")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ydoc_read_transaction")] public static extern nint ReadTransaction(nint doc); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ydoc_write_transaction")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ydoc_write_transaction")] public static extern nint WriteTransaction(nint doc, uint originLength, byte[]? origin); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ydoc_clear")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ydoc_clear")] public static extern void Clear(nint doc); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ydoc_load")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ydoc_load")] public static extern void Load(nint doc, nint transaction); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ydoc_observe_clear")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ydoc_observe_clear")] public static extern uint ObserveClear(nint doc, nint state, ObserveClearCallback callback); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ydoc_unobserve_clear")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ydoc_unobserve_clear")] public static extern uint UnobserveClear(nint doc, uint subscriptionId); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ydoc_observe_updates_v1")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ydoc_observe_updates_v1")] public static extern uint ObserveUpdatesV1(nint doc, nint state, ObserveUpdatesCallback callback); [DllImport( @@ -89,7 +139,9 @@ internal static class DocChannel public static extern uint UnobserveUpdatesV1(nint doc, uint subscriptionId); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ydoc_observe_updates_v2")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ydoc_observe_updates_v2")] public static extern uint ObserveUpdatesV2(nint doc, nint state, ObserveUpdatesCallback callback); [DllImport( @@ -111,10 +163,14 @@ internal static class DocChannel public static extern uint UnobserveAfterTransaction(nint doc, uint subscriptionId); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ydoc_observe_subdocs")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ydoc_observe_subdocs")] public static extern uint ObserveSubDocs(nint doc, nint state, ObserveSubdocsCallback callback); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ydoc_unobserve_subdocs")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ydoc_unobserve_subdocs")] public static extern uint UnobserveSubDocs(nint doc, uint subscriptionId); } diff --git a/YDotNet/Native/Document/DocOptionsNative.cs b/YDotNet/Native/Document/DocOptionsNative.cs index 4c9a135a..fdcfde10 100644 --- a/YDotNet/Native/Document/DocOptionsNative.cs +++ b/YDotNet/Native/Document/DocOptionsNative.cs @@ -1,11 +1,11 @@ -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; using YDotNet.Document.Options; using YDotNet.Infrastructure; namespace YDotNet.Native.Document; [StructLayout(LayoutKind.Sequential)] -internal readonly struct DocOptionsNative : IDisposable +internal readonly struct DocOptionsNative { public ulong Id { get; init; } @@ -23,25 +23,19 @@ namespace YDotNet.Native.Document; public static DocOptionsNative From(DocOptions options) { - MemoryWriter.TryWriteUtf8String(options.Guid, out var guidHandle); - MemoryWriter.TryWriteUtf8String(options.CollectionId, out var collectionIdHandle); + // We can never release the memory because y-crdt just receives a pointer to that. + var unsafeGuid = MemoryWriter.WriteUtf8String(options.Guid); + var unsafeCollection = MemoryWriter.WriteUtf8String(options.CollectionId); return new DocOptionsNative { Id = options.Id ?? 0, - Guid = guidHandle, - CollectionId = collectionIdHandle, - Encoding = (byte)(options.Encoding ?? DocEncoding.Utf8), + Guid = unsafeGuid.Handle, + CollectionId = unsafeCollection.Handle, + Encoding = (byte)options.Encoding, SkipGc = (byte)(options.SkipGarbageCollection ?? false ? 1 : 0), AutoLoad = (byte)(options.AutoLoad ?? false ? 1 : 0), ShouldLoad = (byte)(options.ShouldLoad ?? false ? 1 : 0) }; } - - /// - public void Dispose() - { - MemoryWriter.TryRelease(Guid); - MemoryWriter.TryRelease(CollectionId); - } } diff --git a/YDotNet/Native/StickyIndex/StickyIndexChannel.cs b/YDotNet/Native/StickyIndex/StickyIndexChannel.cs index 66182e52..c83f6f60 100644 --- a/YDotNet/Native/StickyIndex/StickyIndexChannel.cs +++ b/YDotNet/Native/StickyIndex/StickyIndexChannel.cs @@ -5,7 +5,9 @@ namespace YDotNet.Native.StickyIndex; internal static class StickyIndexChannel { [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ysticky_index_destroy")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ysticky_index_destroy")] public static extern void Destroy(nint stickyIndex); [DllImport( @@ -15,18 +17,26 @@ internal static class StickyIndexChannel public static extern nint FromIndex(nint branch, nint transaction, uint index, sbyte associationType); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ysticky_index_assoc")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ysticky_index_assoc")] public static extern sbyte AssociationType(nint stickyIndex); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ysticky_index_read")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ysticky_index_read")] public static extern void Read(nint stickyIndex, nint transaction, out nint branch, out uint index); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ysticky_index_encode")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ysticky_index_encode")] public static extern nint Encode(nint stickyIndex, out uint length); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ysticky_index_decode")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ysticky_index_decode")] public static extern nint Decode(byte[] encoded, uint length); } diff --git a/YDotNet/Native/Transaction/TransactionChannel.cs b/YDotNet/Native/Transaction/TransactionChannel.cs index 2dc2f0e7..e5eb1049 100644 --- a/YDotNet/Native/Transaction/TransactionChannel.cs +++ b/YDotNet/Native/Transaction/TransactionChannel.cs @@ -5,15 +5,21 @@ namespace YDotNet.Native.Transaction; internal static class TransactionChannel { [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ytransaction_commit")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ytransaction_commit")] public static extern nint Commit(nint transaction); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ytransaction_subdocs")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ytransaction_subdocs")] public static extern nint SubDocs(nint transaction, out uint length); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ytransaction_writeable")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ytransaction_writeable")] public static extern byte Writeable(nint transaction); [DllImport( @@ -43,21 +49,27 @@ public static extern nint StateDiffV2( out uint length); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ytransaction_apply")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ytransaction_apply")] public static extern byte ApplyV1( nint transaction, byte[] stateDiff, uint stateDiffLength); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ytransaction_apply_v2")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ytransaction_apply_v2")] public static extern byte ApplyV2( nint transaction, byte[] stateDiff, uint stateDiffLength); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ytransaction_snapshot")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ytransaction_snapshot")] public static extern nint Snapshot(nint transaction, out uint length); [DllImport( @@ -80,6 +92,9 @@ public static extern nint EncodeStateFromSnapshotV2( uint snapshotLength, out uint length); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ytype_get")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ytype_get")] public static extern nint Get(nint transaction, nint name); } diff --git a/YDotNet/Native/Types/ArrayChannel.cs b/YDotNet/Native/Types/ArrayChannel.cs index 6648390a..0c383dd8 100644 --- a/YDotNet/Native/Types/ArrayChannel.cs +++ b/YDotNet/Native/Types/ArrayChannel.cs @@ -6,48 +6,81 @@ internal static class ArrayChannel { public delegate void ObserveCallback(nint state, nint eventHandle); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yarray_len")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yarray_len")] public static extern uint Length(nint array); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yarray_insert_range")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yarray_insert_range")] public static extern void InsertRange(nint array, nint transaction, uint index, nint items, uint itemsLength); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yarray_remove_range")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yarray_remove_range")] public static extern void RemoveRange(nint array, nint transaction, uint index, uint length); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yarray_get")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yarray_get")] public static extern nint Get(nint array, nint transaction, uint index); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yarray_move")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yarray_move")] public static extern void Move(nint array, nint transaction, uint sourceIndex, uint targetIndex); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yarray_iter")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yarray_iter")] public static extern nint Iterator(nint array, nint transaction); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yarray_iter_destroy")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yarray_iter_destroy")] public static extern nint IteratorDestroy(nint arrayIterator); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yarray_iter_next")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yarray_iter_next")] public static extern nint IteratorNext(nint arrayIterator); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yarray_observe")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yarray_observe")] public static extern uint Observe(nint array, nint state, ObserveCallback callback); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yarray_event_path")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yarray_event_path")] public static extern nint ObserveEventPath(nint arrayEvent, out uint length); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yarray_event_target")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yarray_event_target")] public static extern nint ObserveEventTarget(nint arrayEvent); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yarray_event_delta")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yarray_event_delta")] public static extern nint ObserveEventDelta(nint arrayEvent, out uint length); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yarray_unobserve")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yarray_unobserve")] public static extern void Unobserve(nint array, uint subscriptionId); } diff --git a/YDotNet/Native/Types/BinaryChannel.cs b/YDotNet/Native/Types/BinaryChannel.cs index 07a62cd2..c262c05b 100644 --- a/YDotNet/Native/Types/BinaryChannel.cs +++ b/YDotNet/Native/Types/BinaryChannel.cs @@ -4,6 +4,9 @@ namespace YDotNet.Native.Types; internal static class BinaryChannel { - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ybinary_destroy")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ybinary_destroy")] public static extern void Destroy(nint handle, uint length); } diff --git a/YDotNet/Native/Types/Branches/BranchChannel.cs b/YDotNet/Native/Types/Branches/BranchChannel.cs index 48e950be..3d231a47 100644 --- a/YDotNet/Native/Types/Branches/BranchChannel.cs +++ b/YDotNet/Native/Types/Branches/BranchChannel.cs @@ -6,13 +6,22 @@ internal static class BranchChannel { public delegate void ObserveCallback(nint state, uint length, nint eventsHandle); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yobserve_deep")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yobserve_deep")] public static extern uint ObserveDeep(nint type, nint state, ObserveCallback callback); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yunobserve_deep")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yunobserve_deep")] public static extern uint UnobserveDeep(nint type, uint subscriptionId); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ytype_kind")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ytype_kind")] public static extern byte Kind(nint branch); [DllImport( diff --git a/YDotNet/Native/Types/ChunksChannel.cs b/YDotNet/Native/Types/ChunksChannel.cs index 90ea96c5..bf36faf5 100644 --- a/YDotNet/Native/Types/ChunksChannel.cs +++ b/YDotNet/Native/Types/ChunksChannel.cs @@ -4,6 +4,9 @@ namespace YDotNet.Native.Types; internal static class ChunksChannel { - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ychunks_destroy")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ychunks_destroy")] public static extern nint Destroy(nint chunks, uint length); } diff --git a/YDotNet/Native/Types/EventChannel.cs b/YDotNet/Native/Types/EventChannel.cs index ef8e90ef..3030b204 100644 --- a/YDotNet/Native/Types/EventChannel.cs +++ b/YDotNet/Native/Types/EventChannel.cs @@ -5,10 +5,14 @@ namespace YDotNet.Native.Types; internal static class EventChannel { [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yevent_keys_destroy")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yevent_keys_destroy")] public static extern void KeysDestroy(nint eventHandle, uint length); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yevent_delta_destroy")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yevent_delta_destroy")] public static extern void DeltaDestroy(nint eventHandle, uint length); } diff --git a/YDotNet/Native/Types/Events/EventChangeNative.cs b/YDotNet/Native/Types/Events/EventChangeNative.cs index ba3b4dd2..edacb800 100644 --- a/YDotNet/Native/Types/Events/EventChangeNative.cs +++ b/YDotNet/Native/Types/Events/EventChangeNative.cs @@ -15,19 +15,19 @@ internal readonly struct EventChangeNative public nint Values { get; } - public EventChange ToEventChange() + public EventChange ToEventChange(IResourceOwner owner) { var tag = TagNative switch { EventChangeTagNative.Add => EventChangeTag.Add, EventChangeTagNative.Remove => EventChangeTag.Remove, EventChangeTagNative.Retain => EventChangeTag.Retain, - _ => throw new NotSupportedException($"The value \"{TagNative}\" for {nameof(EventChangeTagNative)} is not supported.") + _ => throw new NotSupportedException($"The value \"{TagNative}\" for {nameof(EventChangeTagNative)} is not supported."), }; var values = MemoryReader.TryReadIntPtrArray(Values, Length, Marshal.SizeOf())? - .Select(x => new Output(x, false)).ToList() ?? new List(); + .Select(x => new Output(x, owner)).ToList() ?? new List(); return new EventChange(tag, Length, values); } diff --git a/YDotNet/Native/Types/Events/EventDeltaNative.cs b/YDotNet/Native/Types/Events/EventDeltaNative.cs index 71435312..51e86568 100644 --- a/YDotNet/Native/Types/Events/EventDeltaNative.cs +++ b/YDotNet/Native/Types/Events/EventDeltaNative.cs @@ -18,7 +18,7 @@ internal readonly struct EventDeltaNative public nint Attributes { get; } - public EventDelta ToEventDelta() + public EventDelta ToEventDelta(IResourceOwner owner) { var tag = TagNative switch { @@ -29,13 +29,15 @@ public EventDelta ToEventDelta() }; var attributes = - MemoryReader.ReadIntPtrArray(Attributes, AttributesLength, size: 16) - .Select(x => new EventDeltaAttribute(x)).ToArray(); - - return new EventDelta( - tag, - Length, - InsertHandle != nint.Zero ? new Output(InsertHandle, false) : null, - attributes); + Attributes != nint.Zero ? + MemoryReader.ReadIntPtrArray(Attributes, AttributesLength, size: 16).Select(x => new EventDeltaAttribute(x, owner)).ToList() : + new List(); + + var insert = + InsertHandle != nint.Zero ? + new Output(InsertHandle, owner) : + null; + + return new EventDelta(tag, Length, insert, attributes); } } diff --git a/YDotNet/Native/Types/Events/EventKeyChangeNative.cs b/YDotNet/Native/Types/Events/EventKeyChangeNative.cs index 248a990b..f194863e 100644 --- a/YDotNet/Native/Types/Events/EventKeyChangeNative.cs +++ b/YDotNet/Native/Types/Events/EventKeyChangeNative.cs @@ -16,7 +16,7 @@ internal readonly struct EventKeyChangeNative public nint NewValue { get; } - public EventKeyChange ToEventKeyChange() + public EventKeyChange ToEventKeyChange(IResourceOwner owner) { var tag = TagNative switch { @@ -26,12 +26,18 @@ public EventKeyChange ToEventKeyChange() _ => throw new NotSupportedException($"The value \"{TagNative}\" for {nameof(EventKeyChangeTagNative)} is not supported."), }; - var result = new EventKeyChange( - MemoryReader.ReadUtf8String(Key), - tag, - OldValue != nint.Zero ? new Output(OldValue, false) : null, - NewValue != nint.Zero ? new Output(NewValue, false) : null); + var key = MemoryReader.ReadUtf8String(Key); - return result; + var oldOutput = + OldValue != nint.Zero ? + new Output(OldValue, owner) : + null; + + var newOutput = + NewValue != nint.Zero ? + new Output(NewValue, owner) : + null; + + return new EventKeyChange(key, tag, oldOutput, newOutput); } } diff --git a/YDotNet/Native/Types/Maps/MapChannel.cs b/YDotNet/Native/Types/Maps/MapChannel.cs index 244c8cbc..ea4e24b6 100644 --- a/YDotNet/Native/Types/Maps/MapChannel.cs +++ b/YDotNet/Native/Types/Maps/MapChannel.cs @@ -6,48 +6,87 @@ internal static class MapChannel { public delegate void ObserveCallback(nint state, nint eventHandle); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ymap_insert")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ymap_insert")] public static extern void Insert(nint map, nint transaction, nint key, nint value); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ymap_get")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ymap_get")] public static extern nint Get(nint map, nint transaction, nint key); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ymap_len")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ymap_len")] public static extern uint Length(nint map, nint transaction); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ymap_remove")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ymap_remove")] public static extern byte Remove(nint map, nint transaction, nint key); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ymap_remove_all")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ymap_remove_all")] public static extern void RemoveAll(nint map, nint transaction); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ymap_iter")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ymap_iter")] public static extern nint Iterator(nint map, nint transaction); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ymap_iter_next")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ymap_iter_next")] public static extern nint IteratorNext(nint mapIterator); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ymap_iter_destroy")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ymap_iter_destroy")] public static extern nint IteratorDestroy(nint mapIterator); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ymap_entry_destroy")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ymap_entry_destroy")] public static extern nint EntryDestroy(nint mapEntry); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ymap_observe")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ymap_observe")] public static extern uint Observe(nint map, nint state, ObserveCallback callback); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ymap_event_keys")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ymap_event_keys")] public static extern nint ObserveEventKeys(nint mapEvent, out uint length); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ymap_event_path")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ymap_event_path")] public static extern nint ObserveEventPath(nint mapEvent, out uint length); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ymap_event_target")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ymap_event_target")] public static extern nint ObserveEventTarget(nint mapEvent); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ymap_unobserve")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ymap_unobserve")] public static extern void Unobserve(nint map, uint subscriptionId); } diff --git a/YDotNet/Native/Types/PathChannel.cs b/YDotNet/Native/Types/PathChannel.cs index a55a8b4d..49981991 100644 --- a/YDotNet/Native/Types/PathChannel.cs +++ b/YDotNet/Native/Types/PathChannel.cs @@ -4,6 +4,9 @@ namespace YDotNet.Native.Types; internal static class PathChannel { - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ypath_destroy")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ypath_destroy")] public static extern uint Destroy(nint paths, uint length); } diff --git a/YDotNet/Native/Types/StringChannel.cs b/YDotNet/Native/Types/StringChannel.cs index bd3b7351..abf9dca3 100644 --- a/YDotNet/Native/Types/StringChannel.cs +++ b/YDotNet/Native/Types/StringChannel.cs @@ -4,6 +4,9 @@ namespace YDotNet.Native.Types; internal static class StringChannel { - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ystring_destroy")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ystring_destroy")] public static extern void Destroy(nint handle); } diff --git a/YDotNet/Native/Types/Texts/TextChannel.cs b/YDotNet/Native/Types/Texts/TextChannel.cs index fc50c782..6ab2e8ee 100644 --- a/YDotNet/Native/Types/Texts/TextChannel.cs +++ b/YDotNet/Native/Types/Texts/TextChannel.cs @@ -6,43 +6,75 @@ internal static class TextChannel { public delegate void ObserveCallback(nint state, nint textEvent); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ytext_insert")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ytext_insert")] public static extern void Insert(nint text, nint transaction, uint index, nint value, nint attributes); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ytext_insert_embed")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ytext_insert_embed")] public static extern void InsertEmbed(nint text, nint transaction, uint index, nint content, nint attributes); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ytext_remove_range")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ytext_remove_range")] public static extern void RemoveRange(nint text, nint transaction, uint index, uint length); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ytext_format")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ytext_format")] public static extern void Format(nint text, nint transaction, uint index, uint length, nint attributes); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ytext_string")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ytext_string")] public static extern nint String(nint text, nint transaction); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ytext_len")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ytext_len")] public static extern uint Length(nint text, nint transaction); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ytext_chunks")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ytext_chunks")] public static extern nint Chunks(nint text, nint transaction, out uint length); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ytext_observe")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ytext_observe")] public static extern uint Observe(nint text, nint state, ObserveCallback callback); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ytext_event_path")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ytext_event_path")] public static extern nint ObserveEventPath(nint textEvent, out uint length); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ytext_event_delta")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ytext_event_delta")] public static extern nint ObserveEventDelta(nint textEvent, out uint length); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ytext_event_target")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ytext_event_target")] public static extern nint ObserveEventTarget(nint textEvent); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "ytext_unobserve")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "ytext_unobserve")] public static extern void Unobserve(nint text, uint subscriptionId); } diff --git a/YDotNet/Native/Types/XmlAttributeChannel.cs b/YDotNet/Native/Types/XmlAttributeChannel.cs index 98013df7..f5114870 100644 --- a/YDotNet/Native/Types/XmlAttributeChannel.cs +++ b/YDotNet/Native/Types/XmlAttributeChannel.cs @@ -5,14 +5,20 @@ namespace YDotNet.Native.Types; internal static class XmlAttributeChannel { [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmlattr_iter_next")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmlattr_iter_next")] public static extern nint IteratorNext(nint handle); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmlattr_iter_destroy")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmlattr_iter_destroy")] public static extern nint IteratorDestroy(nint handle); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmlattr_destroy")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmlattr_destroy")] public static extern nint Destroy(nint handle); } diff --git a/YDotNet/Native/Types/XmlChannel.cs b/YDotNet/Native/Types/XmlChannel.cs index fe8081a4..e1ba480e 100644 --- a/YDotNet/Native/Types/XmlChannel.cs +++ b/YDotNet/Native/Types/XmlChannel.cs @@ -5,10 +5,14 @@ namespace YDotNet.Native.Types; internal static class XmlChannel { [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxml_prev_sibling")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxml_prev_sibling")] public static extern nint PreviousSibling(nint handle, nint transaction); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxml_next_sibling")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxml_next_sibling")] public static extern nint NextSibling(nint handle, nint transaction); } diff --git a/YDotNet/Native/Types/XmlElementChannel.cs b/YDotNet/Native/Types/XmlElementChannel.cs index 88ed6460..6c40f24c 100644 --- a/YDotNet/Native/Types/XmlElementChannel.cs +++ b/YDotNet/Native/Types/XmlElementChannel.cs @@ -6,56 +6,88 @@ internal static class XmlElementChannel { public delegate void ObserveCallback(nint state, nint eventHandle); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmlelem_tag")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmlelem_tag")] public static extern nint Tag(nint handle); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmlelem_string")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmlelem_string")] public static extern nint String(nint handle, nint transaction); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmlelem_insert_attr")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmlelem_insert_attr")] public static extern void InsertAttribute(nint handle, nint transaction, nint name, nint value); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmlelem_remove_attr")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmlelem_remove_attr")] public static extern void RemoveAttribute(nint handle, nint transaction, nint name); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmlelem_get_attr")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmlelem_get_attr")] public static extern nint GetAttribute(nint handle, nint transaction, nint name); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmlelem_attr_iter")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmlelem_attr_iter")] public static extern nint AttributeIterator(nint handle, nint transaction); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmlelem_child_len")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmlelem_child_len")] public static extern uint ChildLength(nint handle, nint transaction); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmlelem_insert_text")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmlelem_insert_text")] public static extern nint InsertText(nint handle, nint transaction, uint index); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmlelem_insert_elem")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmlelem_insert_elem")] public static extern nint InsertElement(nint handle, nint transaction, uint index, nint name); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmlelem_remove_range")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmlelem_remove_range")] public static extern nint RemoveRange(nint handle, nint transaction, uint index, uint length); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmlelem_get")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmlelem_get")] public static extern nint Get(nint handle, nint transaction, uint index); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmlelem_parent")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmlelem_parent")] public static extern nint Parent(nint handle, nint transaction); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmlelem_first_child")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmlelem_first_child")] public static extern nint FirstChild(nint handle, nint transaction); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmlelem_tree_walker")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmlelem_tree_walker")] public static extern nint TreeWalker(nint handle, nint transaction); [DllImport( @@ -71,26 +103,38 @@ internal static class XmlElementChannel public static extern void TreeWalkerDestroy(nint handle); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmlelem_observe")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmlelem_observe")] public static extern uint Observe(nint handle, nint state, ObserveCallback callback); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmlelem_event_target")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmlelem_event_target")] public static extern nint ObserveEventTarget(nint eventHandle); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmlelem_event_path")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmlelem_event_path")] public static extern nint ObserveEventPath(nint eventHandle, out uint length); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmlelem_event_delta")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmlelem_event_delta")] public static extern nint ObserveEventDelta(nint eventHandle, out uint length); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmlelem_event_keys")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmlelem_event_keys")] public static extern nint ObserveEventKeys(nint eventHandle, out uint length); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmlelem_unobserve")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmlelem_unobserve")] public static extern void Unobserve(nint handle, uint subscriptionId); } diff --git a/YDotNet/Native/Types/XmlTextChannel.cs b/YDotNet/Native/Types/XmlTextChannel.cs index c3ccba39..0363d709 100644 --- a/YDotNet/Native/Types/XmlTextChannel.cs +++ b/YDotNet/Native/Types/XmlTextChannel.cs @@ -6,58 +6,93 @@ internal static class XmlTextChannel { public delegate void ObserveCallback(nint state, nint eventHandle); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmltext_len")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmltext_len")] public static extern uint Length(nint handle, nint transaction); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmltext_insert")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmltext_insert")] public static extern uint Insert(nint handle, nint transaction, uint index, nint value, nint attributes); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmltext_insert_embed")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmltext_insert_embed")] public static extern void InsertEmbed(nint handle, nint transaction, uint index, nint content, nint attributes); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmltext_insert_attr")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmltext_insert_attr")] public static extern void InsertAttribute(nint handle, nint transaction, nint name, nint value); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmltext_get_attr")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmltext_get_attr")] public static extern nint GetAttribute(nint handle, nint transaction, nint name); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmltext_remove_attr")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmltext_remove_attr")] public static extern nint RemoveAttribute(nint handle, nint transaction, nint name); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmltext_attr_iter")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmltext_attr_iter")] public static extern nint AttributeIterator(nint handle, nint transaction); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmltext_string")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmltext_string")] public static extern nint String(nint handle, nint transaction); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmltext_remove_range")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmltext_remove_range")] public static extern void RemoveRange(nint handle, nint transaction, uint index, uint length); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmltext_format")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmltext_format")] public static extern void Format(nint handle, nint transaction, uint index, uint length, nint attributes); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmltext_observe")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmltext_observe")] public static extern uint Observe(nint handle, nint state, ObserveCallback callback); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmltext_event_target")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmltext_event_target")] public static extern nint ObserveEventTarget(nint eventHandle); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmltext_event_delta")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmltext_event_delta")] public static extern nint ObserveEventDelta(nint eventHandle, out uint length); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmltext_event_keys")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmltext_event_keys")] public static extern nint ObserveEventKeys(nint eventHandle, out uint length); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yxmltext_unobserve")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yxmltext_unobserve")] public static extern void Unobserve(nint handle, uint subscriptionId); } diff --git a/YDotNet/Native/UndoManager/UndoManagerChannel.cs b/YDotNet/Native/UndoManager/UndoManagerChannel.cs index 4018a158..a0e60b34 100644 --- a/YDotNet/Native/UndoManager/UndoManagerChannel.cs +++ b/YDotNet/Native/UndoManager/UndoManagerChannel.cs @@ -9,11 +9,16 @@ internal static class UndoManagerChannel public delegate void ObservePoppedCallback(nint state, UndoEventNative undoEvent); - [DllImport(ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yundo_manager")] + [DllImport( + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yundo_manager")] public static extern nint NewWithOptions(nint doc, nint branch, nint options); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yundo_manager_destroy")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yundo_manager_destroy")] public static extern void Destroy(nint undoManager); [DllImport( @@ -41,31 +46,45 @@ internal static class UndoManagerChannel public static extern uint UnobservePopped(nint undoManager, uint subscriptionId); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yundo_manager_undo")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yundo_manager_undo")] public static extern byte Undo(nint undoManager); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yundo_manager_redo")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yundo_manager_redo")] public static extern byte Redo(nint undoManager); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yundo_manager_can_undo")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yundo_manager_can_undo")] public static extern byte CanUndo(nint undoManager); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yundo_manager_can_redo")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yundo_manager_can_redo")] public static extern byte CanRedo(nint undoManager); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yundo_manager_clear")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yundo_manager_clear")] public static extern byte Clear(nint undoManager); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yundo_manager_stop")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yundo_manager_stop")] public static extern void Stop(nint undoManager); [DllImport( - ChannelSettings.NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "yundo_manager_add_scope")] + ChannelSettings.NativeLib, + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "yundo_manager_add_scope")] public static extern void AddScope(nint undoManager, nint branch); [DllImport( diff --git a/YDotNet/Native/UndoManager/UndoManagerOptionsNative.cs b/YDotNet/Native/UndoManager/UndoManagerOptionsNative.cs index 46d60a1d..0f515fbf 100644 --- a/YDotNet/Native/UndoManager/UndoManagerOptionsNative.cs +++ b/YDotNet/Native/UndoManager/UndoManagerOptionsNative.cs @@ -6,16 +6,11 @@ internal struct UndoManagerOptionsNative { internal uint CaptureTimeoutMilliseconds { get; set; } - internal static UndoManagerOptionsNative? From(UndoManagerOptions? options) + internal static UndoManagerOptionsNative From(UndoManagerOptions? options) { - if (options == null) - { - return null; - } - return new UndoManagerOptionsNative { - CaptureTimeoutMilliseconds = options.CaptureTimeoutMilliseconds + CaptureTimeoutMilliseconds = options?.CaptureTimeoutMilliseconds ?? 0 }; } } From 81077e0d9e39b316880c68858097ebc2f1916ec1 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 11 Oct 2023 21:57:47 +0200 Subject: [PATCH 087/186] Xml comment fixes. --- .editorconfig | 1 + YDotNet/Document/Cells/Input.cs | 9 ++- YDotNet/Document/Cells/JsonArray.cs | 5 +- YDotNet/Document/Cells/JsonObject.cs | 5 +- YDotNet/Document/Cells/Output.cs | 6 +- YDotNet/Document/Doc.cs | 4 +- YDotNet/Document/Events/EventSubscriptions.cs | 4 +- YDotNet/Document/Options/DocEncoding.cs | 5 +- YDotNet/Document/Options/DocOptions.cs | 3 + YDotNet/Document/State/IdRange.cs | 4 +- YDotNet/Document/StickyIndexes/StickyIndex.cs | 3 +- YDotNet/Document/Transactions/Transaction.cs | 61 ++++++++++--------- YDotNet/Document/Types/Arrays/Array.cs | 18 +++--- .../Document/Types/Arrays/ArrayEnumerator.cs | 3 - .../Document/Types/Arrays/ArrayIterator.cs | 6 +- .../Types/Arrays/Events/ArrayEvent.cs | 3 +- YDotNet/Document/Types/Branches/Branch.cs | 4 +- YDotNet/Document/Types/Events/EventChanges.cs | 8 ++- YDotNet/Document/Types/Events/EventDeltas.cs | 6 +- YDotNet/Document/Types/Events/EventKeys.cs | 6 +- YDotNet/Document/Types/Events/EventPath.cs | 3 +- .../Document/Types/Events/EventPathSegment.cs | 2 +- .../Document/Types/Maps/Events/MapEvent.cs | 3 +- YDotNet/Document/Types/Maps/Map.cs | 2 +- YDotNet/Document/Types/Maps/MapEntry.cs | 6 +- YDotNet/Document/Types/Maps/MapEnumerator.cs | 3 - YDotNet/Document/Types/Maps/MapIterator.cs | 6 +- .../Document/Types/Texts/Events/TextEvent.cs | 7 ++- YDotNet/Document/Types/Texts/Text.cs | 6 +- YDotNet/Document/Types/Texts/TextChunks.cs | 7 ++- .../XmlElements/Events/XmlElementEvent.cs | 3 +- .../Types/XmlElements/Trees/XmlTreeWalker.cs | 7 ++- .../Trees/XmlTreeWalkerEnumerator.cs | 4 -- .../Types/XmlElements/XmlAttribute.cs | 1 + .../XmlElements/XmlAttributeEnumerator.cs | 4 -- .../Types/XmlElements/XmlAttributeIterator.cs | 6 +- .../Document/Types/XmlElements/XmlElement.cs | 24 ++++---- .../Types/XmlTexts/Events/XmlTextEvent.cs | 3 +- YDotNet/Document/Types/XmlTexts/XmlText.cs | 10 +-- YDotNet/Document/UndoManagers/UndoManager.cs | 8 ++- YDotNet/Infrastructure/Extensions.cs | 2 +- YDotNet/Infrastructure/IResourceOwner.cs | 6 ++ YDotNet/Infrastructure/Resource.cs | 26 +++++++- .../UnmanagedCollectionResource.cs | 16 +++-- YDotNet/Infrastructure/UnmanagedResource.cs | 7 ++- .../Document/Events/UpdateEventNative.cs | 11 ++-- .../Types/Events/EventChangeTagNative.cs | 2 +- .../Types/Events/EventDeltaTagNative.cs | 2 +- .../UndoManager/Events/UndoEventKindNative.cs | 2 +- YDotNet/ThrowHelper.cs | 2 +- YDotNet/YDotNet.csproj | 1 + YDotNet/YDotNetException.cs | 20 ++++++ 52 files changed, 243 insertions(+), 133 deletions(-) diff --git a/.editorconfig b/.editorconfig index 1d6fd374..5e53e414 100644 --- a/.editorconfig +++ b/.editorconfig @@ -45,5 +45,6 @@ dotnet_diagnostic.SA1500.severity = suggestion # Braces for multi-line statement # Documentation rules dotnet_diagnostic.SA1600.severity = none # Elements should be documented dotnet_diagnostic.SA1601.severity = none # Partial elements should be documented +dotnet_diagnostic.SA1602.severity = none # Enumeration items should be documented dotnet_diagnostic.SA1633.severity = none # File should have header diff --git a/YDotNet/Document/Cells/Input.cs b/YDotNet/Document/Cells/Input.cs index 5fb1047f..efc4e07e 100644 --- a/YDotNet/Document/Cells/Input.cs +++ b/YDotNet/Document/Cells/Input.cs @@ -1,3 +1,5 @@ +using YDotNet.Document.Types.XmlElements; +using YDotNet.Document.Types.XmlTexts; using YDotNet.Infrastructure; using YDotNet.Native.Cells.Inputs; @@ -17,7 +19,8 @@ internal Input(InputNative native, params IDisposable[] allocatedMemory) InputNative = native; } - protected override void DisposeCore(bool disposing) + /// + protected internal override void DisposeCore(bool disposing) { foreach (var memory in allocatedMemory) { @@ -175,7 +178,7 @@ public static Input Text(string value) /// /// Initializes a new instance of the class. /// - /// The value to name the in the cell. + /// The value to name the in the cell. /// The cell that represents the provided value. public static Input XmlElement(string name) { @@ -187,7 +190,7 @@ public static Input XmlElement(string name) /// /// Initializes a new instance of the class. /// - /// The value to fill the in the cell. + /// The value to fill the in the cell. /// The cell that represents the provided value. public static Input XmlText(string value) { diff --git a/YDotNet/Document/Cells/JsonArray.cs b/YDotNet/Document/Cells/JsonArray.cs index b01797dc..29ccf0ce 100644 --- a/YDotNet/Document/Cells/JsonArray.cs +++ b/YDotNet/Document/Cells/JsonArray.cs @@ -5,9 +5,12 @@ namespace YDotNet.Document.Cells; +/// +/// Represents a json array. +/// public sealed class JsonArray : ReadOnlyCollection { - public JsonArray(nint handle, uint length, IResourceOwner owner) + internal JsonArray(nint handle, uint length, IResourceOwner owner) : base(ReadItems(handle, length, owner)) { } diff --git a/YDotNet/Document/Cells/JsonObject.cs b/YDotNet/Document/Cells/JsonObject.cs index 79a114af..9ea6dc44 100644 --- a/YDotNet/Document/Cells/JsonObject.cs +++ b/YDotNet/Document/Cells/JsonObject.cs @@ -6,9 +6,12 @@ namespace YDotNet.Document.Cells; +/// +/// Represents a json object. +/// public sealed class JsonObject : ReadOnlyDictionary { - public JsonObject(nint handle, uint length, IResourceOwner owner) + internal JsonObject(nint handle, uint length, IResourceOwner owner) : base(ReadItems(handle, length, owner)) { } diff --git a/YDotNet/Document/Cells/Output.cs b/YDotNet/Document/Cells/Output.cs index 237eed49..66c4b492 100644 --- a/YDotNet/Document/Cells/Output.cs +++ b/YDotNet/Document/Cells/Output.cs @@ -29,12 +29,16 @@ internal Output(nint handle, IResourceOwner? owner) value = BuildValue(handle, native.Length, Type, this); } + /// + /// Finalizes an instance of the class. + /// ~Output() { Dispose(true); } - protected override void DisposeCore(bool disposing) + /// + protected internal override void DisposeCore(bool disposing) { if (Owner == null) { diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index ac6c4eb0..6321a51b 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -31,7 +31,7 @@ namespace YDotNet.Document; /// public class Doc { - private readonly EventSubscriptions subscriptions = new EventSubscriptions(); + private readonly EventSubscriptions subscriptions = new(); /// /// Initializes a new instance of the class. @@ -237,7 +237,7 @@ public Transaction WriteTransaction(byte[]? origin = null) /// /// Starts a new read-only on this document. /// - /// The to perform operations in the document. + /// The to perform operations in the document. /// Another exception is pending. public Transaction ReadTransaction() { diff --git a/YDotNet/Document/Events/EventSubscriptions.cs b/YDotNet/Document/Events/EventSubscriptions.cs index 7f71a05d..3e76b25d 100644 --- a/YDotNet/Document/Events/EventSubscriptions.cs +++ b/YDotNet/Document/Events/EventSubscriptions.cs @@ -4,7 +4,7 @@ namespace YDotNet.Document.Events; internal sealed class EventSubscriptions { - private readonly HashSet subscriptions = new HashSet(); + private readonly HashSet subscriptions = new(); public IDisposable Add(object callback, Action unsubscribe) { @@ -37,7 +37,7 @@ internal EventSubscription(object callback, Action unsubscrib internal object? Callback { get; set; } - protected override void DisposeCore(bool disposing) + protected internal override void DisposeCore(bool disposing) { unsubscribe(this); } diff --git a/YDotNet/Document/Options/DocEncoding.cs b/YDotNet/Document/Options/DocEncoding.cs index 9870b7df..3087d5cb 100644 --- a/YDotNet/Document/Options/DocEncoding.cs +++ b/YDotNet/Document/Options/DocEncoding.cs @@ -1,4 +1,7 @@ -namespace YDotNet.Document.Options; +using YDotNet.Document.Types.Texts; +using YDotNet.Document.Types.XmlTexts; + +namespace YDotNet.Document.Options; /// /// Determines how string length and offsets are calculated for and . diff --git a/YDotNet/Document/Options/DocOptions.cs b/YDotNet/Document/Options/DocOptions.cs index 52648cea..d0000a62 100644 --- a/YDotNet/Document/Options/DocOptions.cs +++ b/YDotNet/Document/Options/DocOptions.cs @@ -1,3 +1,6 @@ +using YDotNet.Document.Types.Texts; +using YDotNet.Document.Types.XmlTexts; + namespace YDotNet.Document.Options; /// diff --git a/YDotNet/Document/State/IdRange.cs b/YDotNet/Document/State/IdRange.cs index 12ea3561..57db3543 100644 --- a/YDotNet/Document/State/IdRange.cs +++ b/YDotNet/Document/State/IdRange.cs @@ -17,12 +17,12 @@ public IdRange(uint start, uint end) } /// - /// The start of the . + /// Gets the start of the . /// public uint Start { get; } /// - /// The end of the . + /// Gets the end of the . /// public uint End { get; } } diff --git a/YDotNet/Document/StickyIndexes/StickyIndex.cs b/YDotNet/Document/StickyIndexes/StickyIndex.cs index 8fc4c8ce..60492833 100644 --- a/YDotNet/Document/StickyIndexes/StickyIndex.cs +++ b/YDotNet/Document/StickyIndexes/StickyIndex.cs @@ -21,7 +21,8 @@ internal StickyIndex(nint handle) { } - protected override void DisposeCore(bool disposing) + /// + protected internal override void DisposeCore(bool disposing) { } diff --git a/YDotNet/Document/Transactions/Transaction.cs b/YDotNet/Document/Transactions/Transaction.cs index 820ce782..19946ea1 100644 --- a/YDotNet/Document/Transactions/Transaction.cs +++ b/YDotNet/Document/Transactions/Transaction.cs @@ -19,7 +19,7 @@ namespace YDotNet.Document.Transactions; /// All operations that need to touch or modify the contents of a document need to be executed through a /// transaction. /// -/// A is automatically committed during . +/// A is automatically committed during . /// public class Transaction : UnmanagedResource { @@ -28,7 +28,8 @@ internal Transaction(nint handle) { } - protected override void DisposeCore(bool disposing) + /// + protected internal override void DisposeCore(bool disposing) { if (disposing) { @@ -97,7 +98,7 @@ public byte[] StateVectorV1() /// /// /// - /// The sent to this method can be generated using + /// The sent to this method can be generated using /// through a in the remote document. /// /// @@ -128,7 +129,7 @@ public byte[] StateDiffV1(byte[] stateVector) /// /// /// - /// The sent to this method can be generated using + /// The sent to this method can be generated using /// through a in the remote document. /// /// @@ -201,7 +202,7 @@ public byte[] Snapshot() /// /// Encodes the state of the associated to this at a point in time - /// specified by the provided . + /// specified by the provided . /// /// /// @@ -210,7 +211,7 @@ public byte[] Snapshot() /// null will be returned. /// /// - /// The is generated by . This is useful to generate a past view + /// The is generated by . This is useful to generate a past view /// of the document. /// /// @@ -222,7 +223,7 @@ public byte[] Snapshot() /// /// /// The state difference update that can be applied to return the document to the state when the - /// was created. + /// was created. /// public byte[]? EncodeStateFromSnapshotV1(byte[] snapshot) { @@ -237,7 +238,7 @@ public byte[] Snapshot() /// /// Encodes the state of the associated to this at a point in time - /// specified by the provided . + /// specified by the provided . /// /// /// @@ -246,7 +247,7 @@ public byte[] Snapshot() /// null will be returned. /// /// - /// The is generated by . This is useful to generate a past view + /// The is generated by . This is useful to generate a past view /// of the document. /// /// @@ -258,7 +259,7 @@ public byte[] Snapshot() /// /// /// The state difference update that can be applied to return the document to the state when the - /// was created. + /// was created. /// public byte[]? EncodeStateFromSnapshotV2(byte[] snapshot) { @@ -272,13 +273,13 @@ public byte[] Snapshot() } /// - /// Returns the at the root level, identified by , or - /// null if no entry was defined under before. + /// Returns the at the root level, identified by , or + /// null if no entry was defined under before. /// /// The name of the instance to get. /// - /// The at the root level, identified by , or - /// null if no entry was defined under before. + /// The at the root level, identified by , or + /// null if no entry was defined under before. /// public Array? GetArray(string name) { @@ -288,13 +289,13 @@ public byte[] Snapshot() } /// - /// Returns the at the root level, identified by , or - /// null if no entry was defined under before. + /// Returns the at the root level, identified by , or + /// null if no entry was defined under before. /// /// The name of the instance to get. /// - /// The at the root level, identified by , or - /// null if no entry was defined under before. + /// The at the root level, identified by , or + /// null if no entry was defined under before. /// public Map? GetMap(string name) { @@ -304,13 +305,13 @@ public byte[] Snapshot() } /// - /// Returns the at the root level, identified by , or - /// null if no entry was defined under before. + /// Returns the at the root level, identified by , or + /// null if no entry was defined under before. /// /// The name of the instance to get. /// - /// The at the root level, identified by , or - /// null if no entry was defined under before. + /// The at the root level, identified by , or + /// null if no entry was defined under before. /// public Text? GetText(string name) { @@ -320,13 +321,13 @@ public byte[] Snapshot() } /// - /// Returns the at the root level, identified by , or - /// null if no entry was defined under before. + /// Returns the at the root level, identified by , or + /// null if no entry was defined under before. /// /// The name of the instance to get. /// - /// The at the root level, identified by , or - /// null if no entry was defined under before. + /// The at the root level, identified by , or + /// null if no entry was defined under before. /// public XmlElement? GetXmlElement(string name) { @@ -336,13 +337,13 @@ public byte[] Snapshot() } /// - /// Returns the at the root level, identified by , or - /// null if no entry was defined under before. + /// Returns the at the root level, identified by , or + /// null if no entry was defined under before. /// /// The name of the instance to get. /// - /// The at the root level, identified by , or - /// null if no entry was defined under before. + /// The at the root level, identified by , or + /// null if no entry was defined under before. /// public XmlText? GetXmlText(string name) { diff --git a/YDotNet/Document/Types/Arrays/Array.cs b/YDotNet/Document/Types/Arrays/Array.cs index 7b22ea38..6456c097 100644 --- a/YDotNet/Document/Types/Arrays/Array.cs +++ b/YDotNet/Document/Types/Arrays/Array.cs @@ -15,7 +15,7 @@ namespace YDotNet.Document.Types.Arrays; /// public class Array : Branch { - private readonly EventSubscriptions subscriptions = new EventSubscriptions(); + private readonly EventSubscriptions subscriptions = new(); internal Array(nint handle) : base(handle) @@ -23,12 +23,12 @@ internal Array(nint handle) } /// - /// Gets the number of elements stored within current instance of . + /// Gets the number of elements stored within current instance of . /// public uint Length => ArrayChannel.Length(Handle); /// - /// Inserts a range of into the current instance of . + /// Inserts a range of into the current instance of . /// /// The transaction that wraps this operation. /// The starting index to insert the items. @@ -52,13 +52,13 @@ public void RemoveRange(Transaction transaction, uint index, uint length) } /// - /// Gets the value at the given or - /// null if is outside the bounds. + /// Gets the value at the given or + /// null if is outside the bounds. /// /// The transaction that wraps this operation. /// The index to get the item. /// - /// The value at the given or null if is + /// The value at the given or null if is /// outside the bounds. /// public Output? Get(Transaction transaction, uint index) @@ -69,7 +69,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) } /// - /// Moves the element at to the . + /// Moves the element at to the . /// /// /// Both indexes must be lower than the . @@ -119,13 +119,13 @@ public IDisposable Observe(Action action) } /// - /// Retrieves a corresponding to a given human-readable pointing into + /// Retrieves a corresponding to a given human-readable pointing into /// the . /// /// The transaction that wraps this operation. /// The numeric index to place the . /// The type of the . - /// The in the with the given . + /// The in the with the given . public StickyIndex? StickyIndex(Transaction transaction, uint index, StickyAssociationType associationType) { var handle = StickyIndexChannel.FromIndex(Handle, transaction.Handle, index, (sbyte)associationType); diff --git a/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs b/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs index e2749bee..2d734e95 100644 --- a/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs +++ b/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs @@ -28,9 +28,6 @@ public void Dispose() /// object? IEnumerator.Current => current!; - /// - /// Gets the instance that holds the used by this enumerator. - /// private ArrayIterator Iterator { get; } /// diff --git a/YDotNet/Document/Types/Arrays/ArrayIterator.cs b/YDotNet/Document/Types/Arrays/ArrayIterator.cs index 80ae54fa..d5988cb5 100644 --- a/YDotNet/Document/Types/Arrays/ArrayIterator.cs +++ b/YDotNet/Document/Types/Arrays/ArrayIterator.cs @@ -18,12 +18,16 @@ internal ArrayIterator(nint handle) { } + /// + /// Finalizes an instance of the class. + /// ~ArrayIterator() { Dispose(false); } - protected override void DisposeCore(bool disposing) + /// + protected internal override void DisposeCore(bool disposing) { ArrayChannel.IteratorDestroy(Handle); } diff --git a/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs b/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs index 2d181ef7..a298183e 100644 --- a/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs +++ b/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs @@ -38,7 +38,8 @@ internal ArrayEvent(nint handle) }); } - protected override void DisposeCore(bool disposing) + /// + protected internal override void DisposeCore(bool disposing) { // The event has no explicit garbage collection, but is released automatically after the event has been completed. } diff --git a/YDotNet/Document/Types/Branches/Branch.cs b/YDotNet/Document/Types/Branches/Branch.cs index 58f1f383..000644a7 100644 --- a/YDotNet/Document/Types/Branches/Branch.cs +++ b/YDotNet/Document/Types/Branches/Branch.cs @@ -11,7 +11,7 @@ namespace YDotNet.Document.Types.Branches; /// public abstract class Branch { - private readonly EventSubscriptions subscriptions = new EventSubscriptions(); + private readonly EventSubscriptions subscriptions = new(); /// /// Initializes a new instance of the class. @@ -22,7 +22,7 @@ protected Branch(nint handle) Handle = handle; } - public nint Handle { get; } + internal nint Handle { get; } /// /// Subscribes a callback function for changes performed within the instance diff --git a/YDotNet/Document/Types/Events/EventChanges.cs b/YDotNet/Document/Types/Events/EventChanges.cs index 4622040e..e27e73d7 100644 --- a/YDotNet/Document/Types/Events/EventChanges.cs +++ b/YDotNet/Document/Types/Events/EventChanges.cs @@ -12,7 +12,7 @@ public class EventChanges : UnmanagedCollectionResource { private readonly uint length; - public EventChanges(nint handle, uint length, IResourceOwner owner) + internal EventChanges(nint handle, uint length, IResourceOwner owner) : base(handle, owner) { foreach (var itemHandle in MemoryReader.ReadIntPtrArray(handle, length, Marshal.SizeOf())) @@ -24,12 +24,16 @@ public EventChanges(nint handle, uint length, IResourceOwner owner) this.length = length; } + /// + /// Finalizes an instance of the class. + /// ~EventChanges() { Dispose(true); } - protected override void DisposeCore(bool disposing) + /// + protected internal override void DisposeCore(bool disposing) { EventChannel.DeltaDestroy(Handle, length); } diff --git a/YDotNet/Document/Types/Events/EventDeltas.cs b/YDotNet/Document/Types/Events/EventDeltas.cs index 940d3e47..762878ef 100644 --- a/YDotNet/Document/Types/Events/EventDeltas.cs +++ b/YDotNet/Document/Types/Events/EventDeltas.cs @@ -24,12 +24,16 @@ internal EventDeltas(nint handle, uint length, IResourceOwner owner) } } + /// + /// Finalizes an instance of the class. + /// ~EventDeltas() { Dispose(false); } - protected override void DisposeCore(bool disposing) + /// + protected internal override void DisposeCore(bool disposing) { EventChannel.DeltaDestroy(Handle, length); } diff --git a/YDotNet/Document/Types/Events/EventKeys.cs b/YDotNet/Document/Types/Events/EventKeys.cs index c6787049..239646a6 100644 --- a/YDotNet/Document/Types/Events/EventKeys.cs +++ b/YDotNet/Document/Types/Events/EventKeys.cs @@ -25,12 +25,16 @@ internal EventKeys(nint handle, uint length, IResourceOwner owner) this.length = length; } + /// + /// Finalizes an instance of the class. + /// ~EventKeys() { Dispose(true); } - protected override void DisposeCore(bool disposing) + /// + protected internal override void DisposeCore(bool disposing) { EventChannel.KeysDestroy(Handle, length); } diff --git a/YDotNet/Document/Types/Events/EventPath.cs b/YDotNet/Document/Types/Events/EventPath.cs index f35941a9..3ba5d573 100644 --- a/YDotNet/Document/Types/Events/EventPath.cs +++ b/YDotNet/Document/Types/Events/EventPath.cs @@ -22,7 +22,8 @@ internal EventPath(nint handle, uint length, IResourceOwner owner) PathChannel.Destroy(Handle, length); } - protected override void DisposeCore(bool disposing) + /// + protected internal override void DisposeCore(bool disposing) { // We have read everything in the constructor, therefore there is no unmanaged memory to be released. } diff --git a/YDotNet/Document/Types/Events/EventPathSegment.cs b/YDotNet/Document/Types/Events/EventPathSegment.cs index 96fef03a..84f592ef 100644 --- a/YDotNet/Document/Types/Events/EventPathSegment.cs +++ b/YDotNet/Document/Types/Events/EventPathSegment.cs @@ -41,7 +41,7 @@ public EventPathSegment(nint handle) public string? Key { get; } /// - /// Gets the index, if is , or + /// Gets the index, if is , or /// null otherwise. /// public uint? Index { get; } diff --git a/YDotNet/Document/Types/Maps/Events/MapEvent.cs b/YDotNet/Document/Types/Maps/Events/MapEvent.cs index d4b5e5f8..d4a6558c 100644 --- a/YDotNet/Document/Types/Maps/Events/MapEvent.cs +++ b/YDotNet/Document/Types/Maps/Events/MapEvent.cs @@ -39,7 +39,8 @@ internal MapEvent(nint handle) }); } - protected override void DisposeCore(bool disposing) + /// + protected internal override void DisposeCore(bool disposing) { // The event has no explicit garbage collection, but is released automatically after the event has been completed. } diff --git a/YDotNet/Document/Types/Maps/Map.cs b/YDotNet/Document/Types/Maps/Map.cs index 5b9f12e2..5a5f4861 100644 --- a/YDotNet/Document/Types/Maps/Map.cs +++ b/YDotNet/Document/Types/Maps/Map.cs @@ -13,7 +13,7 @@ namespace YDotNet.Document.Types.Maps; /// public class Map : Branch { - private readonly EventSubscriptions subscriptions = new EventSubscriptions(); + private readonly EventSubscriptions subscriptions = new(); internal Map(nint handle) : base(handle) diff --git a/YDotNet/Document/Types/Maps/MapEntry.cs b/YDotNet/Document/Types/Maps/MapEntry.cs index 63460e89..364531b0 100644 --- a/YDotNet/Document/Types/Maps/MapEntry.cs +++ b/YDotNet/Document/Types/Maps/MapEntry.cs @@ -19,12 +19,16 @@ internal MapEntry(nint handle, IResourceOwner? owner) Value = new Output(outputHandle, owner ?? this); } + /// + /// Finalizes an instance of the class. + /// ~MapEntry() { Dispose(false); } - protected override void DisposeCore(bool disposing) + /// + protected internal override void DisposeCore(bool disposing) { MapChannel.EntryDestroy(Handle); } diff --git a/YDotNet/Document/Types/Maps/MapEnumerator.cs b/YDotNet/Document/Types/Maps/MapEnumerator.cs index f5c3fe58..7779abcf 100644 --- a/YDotNet/Document/Types/Maps/MapEnumerator.cs +++ b/YDotNet/Document/Types/Maps/MapEnumerator.cs @@ -26,9 +26,6 @@ public void Dispose() /// object? IEnumerator.Current => current!; - /// - /// Gets the instance that holds the used by this enumerator. - /// private MapIterator Iterator { get; } /// diff --git a/YDotNet/Document/Types/Maps/MapIterator.cs b/YDotNet/Document/Types/Maps/MapIterator.cs index 3e5b2a8e..5bd5db7e 100644 --- a/YDotNet/Document/Types/Maps/MapIterator.cs +++ b/YDotNet/Document/Types/Maps/MapIterator.cs @@ -24,12 +24,16 @@ internal MapIterator(nint handle) { } + /// + /// Finalizes an instance of the class. + /// ~MapIterator() { Dispose(false); } - protected override void DisposeCore(bool disposing) + /// + protected internal override void DisposeCore(bool disposing) { MapChannel.IteratorDestroy(Handle); } diff --git a/YDotNet/Document/Types/Texts/Events/TextEvent.cs b/YDotNet/Document/Types/Texts/Events/TextEvent.cs index a70a85c7..603b78ba 100644 --- a/YDotNet/Document/Types/Texts/Events/TextEvent.cs +++ b/YDotNet/Document/Types/Texts/Events/TextEvent.cs @@ -42,7 +42,8 @@ internal TextEvent(nint handle) }); } - protected override void DisposeCore(bool disposing) + /// + protected internal override void DisposeCore(bool disposing) { // The event has no explicit garbage collection, but is released automatically after the event has been completed. } @@ -50,13 +51,13 @@ protected override void DisposeCore(bool disposing) /// /// Gets the keys that changed within the instance and triggered this event. /// - /// This property can only be accessed during the callback that exposes this instance. + /// This property can only be accessed during the callback that exposes this instance. public EventDeltas Delta => deltas.Value; /// /// Gets the path from the observed instanced down to the current instance. /// - /// This property can only be accessed during the callback that exposes this instance. + /// This property can only be accessed during the callback that exposes this instance. public EventPath Path => path.Value; /// diff --git a/YDotNet/Document/Types/Texts/Text.cs b/YDotNet/Document/Types/Texts/Text.cs index 6197fad9..c5cf0ac0 100644 --- a/YDotNet/Document/Types/Texts/Text.cs +++ b/YDotNet/Document/Types/Texts/Text.cs @@ -15,7 +15,7 @@ namespace YDotNet.Document.Types.Texts; /// public class Text : Branch { - private readonly EventSubscriptions subscriptions = new EventSubscriptions(); + private readonly EventSubscriptions subscriptions = new(); internal Text(nint handle) : base(handle) @@ -150,13 +150,13 @@ public IDisposable Observe(Action action) } /// - /// Retrieves a corresponding to a given human-readable pointing into + /// Retrieves a corresponding to a given human-readable pointing into /// the . /// /// The transaction that wraps this operation. /// The numeric index to place the . /// The type of the . - /// The in the with the given . + /// The in the with the given . public StickyIndex? StickyIndex(Transaction transaction, uint index, StickyAssociationType associationType) { var handle = StickyIndexChannel.FromIndex(Handle, transaction.Handle, index, (sbyte)associationType); diff --git a/YDotNet/Document/Types/Texts/TextChunks.cs b/YDotNet/Document/Types/Texts/TextChunks.cs index 427c59a7..52646247 100644 --- a/YDotNet/Document/Types/Texts/TextChunks.cs +++ b/YDotNet/Document/Types/Texts/TextChunks.cs @@ -24,14 +24,17 @@ internal TextChunks(nint handle, uint length) this.length = length; } + /// + /// Finalizes an instance of the class. + /// ~TextChunks() { Dispose(true); } - protected override void DisposeCore(bool disposing) + /// + protected internal override void DisposeCore(bool disposing) { - Console.WriteLine("---DISPOSE {0} - {1}", Handle, length); Console.Out.Flush(); Thread.Sleep(100); diff --git a/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs b/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs index 7e5b99fe..6e8ef4f2 100644 --- a/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs +++ b/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs @@ -50,7 +50,8 @@ internal XmlElementEvent(nint handle) }); } - protected override void DisposeCore(bool disposing) + /// + protected internal override void DisposeCore(bool disposing) { // The event has no explicit garbage collection, but is released automatically after the event has been completed. } diff --git a/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalker.cs b/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalker.cs index e1194527..976f6b2e 100644 --- a/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalker.cs +++ b/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalker.cs @@ -1,5 +1,6 @@ using System.Collections; using YDotNet.Document.Cells; +using YDotNet.Document.Types.XmlTexts; using YDotNet.Infrastructure; using YDotNet.Native.Types; @@ -19,12 +20,16 @@ internal XmlTreeWalker(nint handle) { } + /// + /// Finalizes an instance of the class. + /// ~XmlTreeWalker() { Dispose(false); } - protected override void DisposeCore(bool disposing) + /// + protected internal override void DisposeCore(bool disposing) { XmlElementChannel.TreeWalkerDestroy(Handle); } diff --git a/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs b/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs index 96a05b88..99896de4 100644 --- a/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs +++ b/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs @@ -35,10 +35,6 @@ public void Dispose() /// object? IEnumerator.Current => current!; - /// - /// Gets the instance that holds the - /// used by this enumerator. - /// private XmlTreeWalker TreeWalker { get; } /// diff --git a/YDotNet/Document/Types/XmlElements/XmlAttribute.cs b/YDotNet/Document/Types/XmlElements/XmlAttribute.cs index 56083b49..e6e531b5 100644 --- a/YDotNet/Document/Types/XmlElements/XmlAttribute.cs +++ b/YDotNet/Document/Types/XmlElements/XmlAttribute.cs @@ -1,4 +1,5 @@ using System.Runtime.InteropServices; +using YDotNet.Document.Types.XmlTexts; using YDotNet.Infrastructure; using YDotNet.Native.Types; diff --git a/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs b/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs index f635b921..949cceff 100644 --- a/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs +++ b/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs @@ -34,10 +34,6 @@ public void Dispose() /// object? IEnumerator.Current => current!; - /// - /// Gets the instance that holds the - /// used by this enumerator. - /// private XmlAttributeIterator Iterator { get; } /// diff --git a/YDotNet/Document/Types/XmlElements/XmlAttributeIterator.cs b/YDotNet/Document/Types/XmlElements/XmlAttributeIterator.cs index e542fc45..7d6dbc3d 100644 --- a/YDotNet/Document/Types/XmlElements/XmlAttributeIterator.cs +++ b/YDotNet/Document/Types/XmlElements/XmlAttributeIterator.cs @@ -18,12 +18,16 @@ internal XmlAttributeIterator(nint handle) { } + /// + /// Finalizes an instance of the class. + /// ~XmlAttributeIterator() { Dispose(false); } - protected override void DisposeCore(bool disposing) + /// + protected internal override void DisposeCore(bool disposing) { XmlAttributeChannel.IteratorDestroy(Handle); } diff --git a/YDotNet/Document/Types/XmlElements/XmlElement.cs b/YDotNet/Document/Types/XmlElements/XmlElement.cs index 4008b2f0..b266f7fe 100644 --- a/YDotNet/Document/Types/XmlElements/XmlElement.cs +++ b/YDotNet/Document/Types/XmlElements/XmlElement.cs @@ -15,7 +15,7 @@ namespace YDotNet.Document.Types.XmlElements; /// public class XmlElement : Branch { - private readonly EventSubscriptions subscriptions = new EventSubscriptions(); + private readonly EventSubscriptions subscriptions = new(); internal XmlElement(nint handle) : base(handle) @@ -57,7 +57,7 @@ public string String(Transaction transaction) /// Inserts an attribute in this instance. /// /// - /// If another attribute with the same already exists, it will be replaced. + /// If another attribute with the same already exists, it will be replaced. /// /// The transaction that wraps this operation. /// The name of the attribute to be added. @@ -83,7 +83,7 @@ public void RemoveAttribute(Transaction transaction, string name) } /// - /// Gets an attribute with the given from this instance. + /// Gets an attribute with the given from this instance. /// /// The transaction that wraps this operation. /// The name of the attribute to be retrieved. @@ -126,11 +126,11 @@ public uint ChildLength(Transaction transaction) /// /// Inserts an as a child of this at the given - /// . + /// . /// /// The transaction that wraps this operation. /// The index that the will be inserted. - /// The inserted at the given . + /// The inserted at the given . public XmlText InsertText(Transaction transaction, uint index) { var handle = XmlElementChannel.InsertText(Handle, transaction.Handle, index); @@ -140,12 +140,12 @@ public XmlText InsertText(Transaction transaction, uint index) /// /// Inserts an as a child of this at the given - /// . + /// . /// /// The transaction that wraps this operation. /// The index that the will be inserted. /// The name (or tag) of the that will be inserted. - /// The inserted at the given . + /// The inserted at the given . public XmlElement InsertElement(Transaction transaction, uint index, string name) { using var unsafeName = MemoryWriter.WriteUtf8String(name); @@ -156,23 +156,23 @@ public XmlElement InsertElement(Transaction transaction, uint index, string name } /// - /// Removes a consecutive range of direct child nodes starting at the through the - /// . + /// Removes a consecutive range of direct child nodes starting at the through the + /// . /// /// The transaction that wraps this operation. /// The index to start removing the child nodes. - /// The amount of child nodes to remove, starting at . + /// The amount of child nodes to remove, starting at . public void RemoveRange(Transaction transaction, uint index, uint length) { XmlElementChannel.RemoveRange(Handle, transaction.Handle, index, length); } /// - /// Returns an cell or null if the is out of bounds. + /// Returns an cell or null if the is out of bounds. /// /// The transaction that wraps this operation. /// The index to retrieve the cell. - /// An cell or null if the is out of bounds. + /// An cell or null if the is out of bounds. public Output? Get(Transaction transaction, uint index) { var handle = XmlElementChannel.Get(Handle, transaction.Handle, index); diff --git a/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs b/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs index 9aae346e..91245c91 100644 --- a/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs +++ b/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs @@ -38,7 +38,8 @@ internal XmlTextEvent(nint handle) }); } - protected override void DisposeCore(bool disposing) + /// + protected internal override void DisposeCore(bool disposing) { // The event has no explicit garbage collection, but is released automatically after the event has been completed. } diff --git a/YDotNet/Document/Types/XmlTexts/XmlText.cs b/YDotNet/Document/Types/XmlTexts/XmlText.cs index 501e9ef5..73a5900a 100644 --- a/YDotNet/Document/Types/XmlTexts/XmlText.cs +++ b/YDotNet/Document/Types/XmlTexts/XmlText.cs @@ -16,7 +16,7 @@ namespace YDotNet.Document.Types.XmlTexts; /// public class XmlText : Branch { - private readonly EventSubscriptions subscriptions = new EventSubscriptions(); + private readonly EventSubscriptions subscriptions = new(); internal XmlText(nint handle) : base(handle) @@ -73,7 +73,7 @@ public void InsertEmbed(Transaction transaction, uint index, Input content, Inpu /// Inserts an attribute. /// /// - /// If another attribute with the same already exists, it will be replaced. + /// If another attribute with the same already exists, it will be replaced. /// /// The transaction that wraps this operation. /// The name of the attribute to be added. @@ -99,7 +99,7 @@ public void RemoveAttribute(Transaction transaction, string name) } /// - /// Gets an attribute with the given . + /// Gets an attribute with the given . /// /// The transaction that wraps this operation. /// The name of the attribute to be retrieved. @@ -227,13 +227,13 @@ public IDisposable Observe(Action action) } /// - /// Retrieves a corresponding to a given human-readable pointing into + /// Retrieves a corresponding to a given human-readable pointing into /// the . /// /// The transaction that wraps this operation. /// The numeric index to place the . /// The type of the . - /// The in the with the given . + /// The in the with the given . public StickyIndex? StickyIndex(Transaction transaction, uint index, StickyAssociationType associationType) { var handle = StickyIndexChannel.FromIndex(Handle, transaction.Handle, index, (sbyte)associationType); diff --git a/YDotNet/Document/UndoManagers/UndoManager.cs b/YDotNet/Document/UndoManagers/UndoManager.cs index 765bdc53..c6f82f02 100644 --- a/YDotNet/Document/UndoManagers/UndoManager.cs +++ b/YDotNet/Document/UndoManagers/UndoManager.cs @@ -12,7 +12,7 @@ namespace YDotNet.Document.UndoManagers; /// public class UndoManager : UnmanagedResource { - private readonly EventSubscriptions subscriptions = new EventSubscriptions(); + private readonly EventSubscriptions subscriptions = new(); /// /// Initializes a new instance of the class. @@ -32,12 +32,16 @@ private static nint Create(Doc doc, Branch branch, UndoManagerOptions? options) return UndoManagerChannel.NewWithOptions(doc.Handle, branch.Handle, unsafeOptions.Handle); } + /// + /// Finalizes an instance of the class. + /// ~UndoManager() { Dispose(false); } - protected override void DisposeCore(bool disposing) + /// + protected internal override void DisposeCore(bool disposing) { UndoManagerChannel.Destroy(Handle); } diff --git a/YDotNet/Infrastructure/Extensions.cs b/YDotNet/Infrastructure/Extensions.cs index 3ebfff97..04b2a659 100644 --- a/YDotNet/Infrastructure/Extensions.cs +++ b/YDotNet/Infrastructure/Extensions.cs @@ -1,6 +1,6 @@ namespace YDotNet.Infrastructure; -public static class Extensions +internal static class Extensions { public static nint Checked(this nint input) { diff --git a/YDotNet/Infrastructure/IResourceOwner.cs b/YDotNet/Infrastructure/IResourceOwner.cs index 75f340be..25bdded9 100644 --- a/YDotNet/Infrastructure/IResourceOwner.cs +++ b/YDotNet/Infrastructure/IResourceOwner.cs @@ -1,6 +1,12 @@ namespace YDotNet.Infrastructure; +/// +/// Marker interface for resource owners. +/// public interface IResourceOwner { + /// + /// Gets a value indicating whether this instance is disposed. + /// bool IsDisposed { get; } } diff --git a/YDotNet/Infrastructure/Resource.cs b/YDotNet/Infrastructure/Resource.cs index 7dfbb00d..13dd39fa 100644 --- a/YDotNet/Infrastructure/Resource.cs +++ b/YDotNet/Infrastructure/Resource.cs @@ -1,22 +1,34 @@ namespace YDotNet.Infrastructure; +/// +/// Base class for all resoruces. +/// public abstract class Resource : IResourceOwner, IDisposable { - protected Resource(IResourceOwner? owner = null) + internal Resource(IResourceOwner? owner = null) { - this.Owner = owner; + Owner = owner; } + /// public bool IsDisposed { get; private set; } + /// + /// Gets the owner of this resource. + /// public IResourceOwner? Owner { get; } + /// public void Dispose() { GC.SuppressFinalize(this); Dispose(true); } + /// + /// Throws an exception if this object or the owner is disposed. + /// + /// Object or the owner has been disposed. protected void ThrowIfDisposed() { if (IsDisposed || Owner?.IsDisposed == true) @@ -25,6 +37,10 @@ protected void ThrowIfDisposed() } } + /// + /// Releases all unmanaged resources. + /// + /// True, if also managed resources should be disposed. protected void Dispose(bool disposing) { if (IsDisposed) @@ -36,5 +52,9 @@ protected void Dispose(bool disposing) IsDisposed = true; } - protected abstract void DisposeCore(bool disposing); + /// + /// Releases all unmanaged resources. + /// + /// True, if also managed resources should be disposed. + protected internal abstract void DisposeCore(bool disposing); } diff --git a/YDotNet/Infrastructure/UnmanagedCollectionResource.cs b/YDotNet/Infrastructure/UnmanagedCollectionResource.cs index a96fbd39..bbb567fd 100644 --- a/YDotNet/Infrastructure/UnmanagedCollectionResource.cs +++ b/YDotNet/Infrastructure/UnmanagedCollectionResource.cs @@ -1,12 +1,16 @@ -using System.Collections; +using System.Collections; namespace YDotNet.Infrastructure; +/// +/// Base class for resources that represent a collection. +/// +/// The type of item. public abstract class UnmanagedCollectionResource : UnmanagedResource, IReadOnlyList { - private readonly List items = new List(); + private readonly List items = new(); - public UnmanagedCollectionResource(nint handle, IResourceOwner? owner) + internal UnmanagedCollectionResource(nint handle, IResourceOwner? owner) : base(handle, owner) { } @@ -17,7 +21,11 @@ public UnmanagedCollectionResource(nint handle, IResourceOwner? owner) /// public int Count => items.Count; - protected void AddItem(T item) + /// + /// Adds a new item to the collection. + /// + /// The item to add. + protected internal void AddItem(T item) { items.Add(item); } diff --git a/YDotNet/Infrastructure/UnmanagedResource.cs b/YDotNet/Infrastructure/UnmanagedResource.cs index 9d9bf916..9b0826b3 100644 --- a/YDotNet/Infrastructure/UnmanagedResource.cs +++ b/YDotNet/Infrastructure/UnmanagedResource.cs @@ -1,12 +1,15 @@ namespace YDotNet.Infrastructure; +/// +/// Base class for all unmanaged resources. +/// public abstract class UnmanagedResource : Resource { - protected UnmanagedResource(nint handle, IResourceOwner? owner = null) + internal UnmanagedResource(nint handle, IResourceOwner? owner = null) : base(owner) { Handle = handle; } - public nint Handle { get; } + internal nint Handle { get; } } diff --git a/YDotNet/Native/Document/Events/UpdateEventNative.cs b/YDotNet/Native/Document/Events/UpdateEventNative.cs index df45ff52..ca6ba08b 100644 --- a/YDotNet/Native/Document/Events/UpdateEventNative.cs +++ b/YDotNet/Native/Document/Events/UpdateEventNative.cs @@ -1,5 +1,7 @@ +using System.ComponentModel.DataAnnotations; using System.Runtime.InteropServices; using YDotNet.Document.Events; +using YDotNet.Infrastructure; namespace YDotNet.Native.Document.Events; @@ -12,17 +14,12 @@ internal readonly struct UpdateEventNative public static UpdateEventNative From(uint length, nint data) { - return new UpdateEventNative - { - Length = length, - Data = data - }; + return new UpdateEventNative { Length = length, Data = data }; } public UpdateEvent ToUpdateEvent() { - var result = new byte[Length]; - Marshal.Copy(Data, result, startIndex: 0, (int)Length); + var result = MemoryReader.ReadBytes(Data, Length); return new UpdateEvent(result); } diff --git a/YDotNet/Native/Types/Events/EventChangeTagNative.cs b/YDotNet/Native/Types/Events/EventChangeTagNative.cs index 3a22b295..e902a6cd 100644 --- a/YDotNet/Native/Types/Events/EventChangeTagNative.cs +++ b/YDotNet/Native/Types/Events/EventChangeTagNative.cs @@ -1,6 +1,6 @@ namespace YDotNet.Native.Types.Events; -public enum EventChangeTagNative : sbyte +internal enum EventChangeTagNative : sbyte { Add = 1, Remove = 2, diff --git a/YDotNet/Native/Types/Events/EventDeltaTagNative.cs b/YDotNet/Native/Types/Events/EventDeltaTagNative.cs index 5c036dc4..f1b77889 100644 --- a/YDotNet/Native/Types/Events/EventDeltaTagNative.cs +++ b/YDotNet/Native/Types/Events/EventDeltaTagNative.cs @@ -1,6 +1,6 @@ namespace YDotNet.Native.Types.Events; -public enum EventDeltaTagNative : sbyte +internal enum EventDeltaTagNative : sbyte { Add = 1, Remove = 2, diff --git a/YDotNet/Native/UndoManager/Events/UndoEventKindNative.cs b/YDotNet/Native/UndoManager/Events/UndoEventKindNative.cs index 36e43cf7..b96a33f8 100644 --- a/YDotNet/Native/UndoManager/Events/UndoEventKindNative.cs +++ b/YDotNet/Native/UndoManager/Events/UndoEventKindNative.cs @@ -1,6 +1,6 @@ namespace YDotNet.Native.UndoManager.Events; -public enum UndoEventKindNative : byte +internal enum UndoEventKindNative : byte { Undo = 0, Redo = 1 diff --git a/YDotNet/ThrowHelper.cs b/YDotNet/ThrowHelper.cs index a6f2b1b8..cf657e9b 100644 --- a/YDotNet/ThrowHelper.cs +++ b/YDotNet/ThrowHelper.cs @@ -1,6 +1,6 @@ namespace YDotNet; -public static class ThrowHelper +internal static class ThrowHelper { public static void InternalError() { diff --git a/YDotNet/YDotNet.csproj b/YDotNet/YDotNet.csproj index ee6edb0b..a791eb4e 100644 --- a/YDotNet/YDotNet.csproj +++ b/YDotNet/YDotNet.csproj @@ -5,6 +5,7 @@ enable enable true + True diff --git a/YDotNet/YDotNetException.cs b/YDotNet/YDotNetException.cs index b4cbb9f8..02b45a20 100644 --- a/YDotNet/YDotNetException.cs +++ b/YDotNet/YDotNetException.cs @@ -2,23 +2,43 @@ namespace YDotNet; +/// +/// Represents an YDotNetException. +/// [Serializable] public class YDotNetException : Exception { + /// + /// Initializes a new instance of the class. + /// public YDotNetException() { } + /// + /// Initializes a new instance of the class with error message. + /// + /// The error message. public YDotNetException(string message) : base(message) { } + /// + /// Initializes a new instance of the class with error message and inner exception. + /// + /// The error message. + /// The inner exception. public YDotNetException(string message, Exception inner) : base(message, inner) { } + /// + /// Initializes a new instance of the class for serialization. + /// + /// The serialization info. + /// The serialization context. protected YDotNetException(SerializationInfo info, StreamingContext context) : base(info, context) { From 043a87a3839c89ba104de279d8cee04c43353b4f Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 12 Oct 2023 13:29:10 +0200 Subject: [PATCH 088/186] Tests --- Tests/YDotNet.Tests.Unit/Program.cs | 34 ++++++++++++- YDotNet/Document/Cells/Input.cs | 8 ++++ YDotNet/Document/Cells/Output.cs | 2 +- YDotNet/Document/Doc.cs | 48 ++++++++++++------- YDotNet/Document/Transactions/Transaction.cs | 10 +--- .../Types/XmlElements/XmlAttribute.cs | 4 -- YDotNet/Document/UndoManagers/UndoManager.cs | 4 -- YDotNet/Infrastructure/MemoryReader.cs | 16 +++++++ .../Document/Events/SubDocsEventNative.cs | 30 ++++-------- .../Native/Document/State/DeleteSetNative.cs | 13 ++--- .../Document/State/IdRangeSequenceNative.cs | 12 ++--- .../Document/State/StateVectorNative.cs | 9 ++-- 12 files changed, 114 insertions(+), 76 deletions(-) diff --git a/Tests/YDotNet.Tests.Unit/Program.cs b/Tests/YDotNet.Tests.Unit/Program.cs index 16ab227b..5c1ecc68 100644 --- a/Tests/YDotNet.Tests.Unit/Program.cs +++ b/Tests/YDotNet.Tests.Unit/Program.cs @@ -1,4 +1,7 @@ using System.Runtime.CompilerServices; +using YDotNet.Document; +using YDotNet.Document.Cells; +using YDotNet.Document.Types.Maps; namespace YDotNet.Tests.Unit; @@ -6,7 +9,36 @@ public class Program { public static void Main() { - var types = typeof(Program).Assembly.GetTypes(); + var doc = new Doc(); + var a = doc.Map("A"); + Map b; + using (var t = doc.WriteTransaction()) + { + + a.Insert(t, "B", Input.Map(new Dictionary())); + + b = a.Get(t, "B").Map; + b.Insert(t, "C", Input.Double(1)); + + a.Remove(t, "B"); + } + + for (var j = 0; j < 10000; j++) + { + using (var t = doc.WriteTransaction()) + { + b.Insert(t, "C", Input.Double(1)); + } + } + + using (var t = doc.WriteTransaction()) + { + b.Insert(t, "D", Input.Double(1)); + + var length = b.Length(t); + } + + var types = typeof(Program).Assembly.GetTypes(); AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; diff --git a/YDotNet/Document/Cells/Input.cs b/YDotNet/Document/Cells/Input.cs index efc4e07e..a1ffdd6d 100644 --- a/YDotNet/Document/Cells/Input.cs +++ b/YDotNet/Document/Cells/Input.cs @@ -19,6 +19,14 @@ internal Input(InputNative native, params IDisposable[] allocatedMemory) InputNative = native; } + /// + /// Finalizes an instance of the class. + /// + ~Input() + { + Dispose(false); + } + /// protected internal override void DisposeCore(bool disposing) { diff --git a/YDotNet/Document/Cells/Output.cs b/YDotNet/Document/Cells/Output.cs index 66c4b492..3173e397 100644 --- a/YDotNet/Document/Cells/Output.cs +++ b/YDotNet/Document/Cells/Output.cs @@ -200,7 +200,7 @@ protected internal override void DisposeCore(bool disposing) return new Lazy(() => new XmlText(OutputChannel.XmlText(handle).Checked())); case OutputType.Doc: - return new Lazy(() => new Doc(OutputChannel.Doc(handle).Checked())); + return new Lazy(() => new Doc(OutputChannel.Doc(handle).Checked(), false)); default: return new Lazy((object?)null); diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index 6321a51b..90e52c2e 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -1,3 +1,4 @@ +using System.Reflection.Metadata; using YDotNet.Document.Events; using YDotNet.Document.Options; using YDotNet.Document.Transactions; @@ -29,9 +30,10 @@ namespace YDotNet.Document; /// to recursively nested types). /// /// -public class Doc +public class Doc : UnmanagedResource { private readonly EventSubscriptions subscriptions = new(); + private readonly bool isCloned; /// /// Initializes a new instance of the class. @@ -50,19 +52,38 @@ public Doc() /// /// The options to be used when initializing this document. public Doc(DocOptions options) + : base(CreateDoc(options)) + { + } + + internal Doc(nint handle, bool isCloned) + : base(handle) + { + this.isCloned = isCloned; + } + + private static nint CreateDoc(DocOptions options) { var unsafeOptions = DocOptionsNative.From(options); - Handle = DocChannel.NewWithOptions(unsafeOptions); + return DocChannel.NewWithOptions(unsafeOptions); } /// - /// Initializes a new instance of the class with the specified . + /// Finalizes an instance of the class. /// - /// The pointer to be used by this document to manage the native resource. - internal Doc(nint handle) + ~Doc() + { + Dispose(false); + } + + /// + protected internal override void DisposeCore(bool disposing) { - Handle = handle; + if (isCloned) + { + DocChannel.Destroy(Handle); + } } /// @@ -94,8 +115,7 @@ public string? CollectionId /// Gets a value indicating whether this instance requested a data load. /// /// - /// This flag is often used by the parent instance to check if this - /// instance requested a data load. + /// This flag is often used by the parent instance to check if this instance requested a data load. /// public bool ShouldLoad => DocChannel.ShouldLoad(Handle); @@ -103,16 +123,10 @@ public string? CollectionId /// Gets a value indicating whether this instance will auto load. /// /// - /// Auto loaded nested instances automatically send a load request to their parent - /// instances. + /// Auto loaded nested instances automatically send a load request to their parent instances. /// public bool AutoLoad => DocChannel.AutoLoad(Handle); - /// - /// Gets the handle to the native resource. - /// - internal nint Handle { get; } - /// /// Creates a copy of the current instance. /// @@ -122,7 +136,7 @@ public Doc Clone() { var handle = DocChannel.Clone(Handle).Checked(); - return new Doc(handle); + return new Doc(handle, true); } /// @@ -281,7 +295,7 @@ public void Load(Transaction transaction) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable ObserveClear(Action action) { - DocChannel.ObserveClearCallback callback = (_, doc) => action(ClearEventNative.From(new Doc(doc)).ToClearEvent()); + DocChannel.ObserveClearCallback callback = (_, doc) => action(ClearEventNative.From(new Doc(doc, false)).ToClearEvent()); var subscriptionId = DocChannel.ObserveClear( Handle, diff --git a/YDotNet/Document/Transactions/Transaction.cs b/YDotNet/Document/Transactions/Transaction.cs index 19946ea1..5647c3ee 100644 --- a/YDotNet/Document/Transactions/Transaction.cs +++ b/YDotNet/Document/Transactions/Transaction.cs @@ -1,4 +1,3 @@ -using System.Runtime.InteropServices; using YDotNet.Document.Options; using YDotNet.Document.Types.Maps; using YDotNet.Document.Types.Texts; @@ -62,14 +61,9 @@ public void Commit() public Doc[] SubDocs() { var handle = TransactionChannel.SubDocs(Handle, out var length); + var handles = MemoryReader.ReadStructArray(handle, length); - var docs = new Doc[length]; - for (var i = 0; i < length; i++) - { - docs[i] = new Doc(Marshal.ReadIntPtr(handle, i * nint.Size)); - } - - return docs; + return handles.Select(x => new Doc(handle, false)).ToArray(); } /// diff --git a/YDotNet/Document/Types/XmlElements/XmlAttribute.cs b/YDotNet/Document/Types/XmlElements/XmlAttribute.cs index e6e531b5..41c854e9 100644 --- a/YDotNet/Document/Types/XmlElements/XmlAttribute.cs +++ b/YDotNet/Document/Types/XmlElements/XmlAttribute.cs @@ -10,10 +10,6 @@ namespace YDotNet.Document.Types.XmlElements; /// public class XmlAttribute { - /// - /// Initializes a new instance of the class. - /// - /// The handle to the native resource. internal XmlAttribute(nint handle) { Handle = handle; diff --git a/YDotNet/Document/UndoManagers/UndoManager.cs b/YDotNet/Document/UndoManagers/UndoManager.cs index c6f82f02..a8ab313b 100644 --- a/YDotNet/Document/UndoManagers/UndoManager.cs +++ b/YDotNet/Document/UndoManagers/UndoManager.cs @@ -32,15 +32,11 @@ private static nint Create(Doc doc, Branch branch, UndoManagerOptions? options) return UndoManagerChannel.NewWithOptions(doc.Handle, branch.Handle, unsafeOptions.Handle); } - /// - /// Finalizes an instance of the class. - /// ~UndoManager() { Dispose(false); } - /// protected internal override void DisposeCore(bool disposing) { UndoManagerChannel.Destroy(Handle); diff --git a/YDotNet/Infrastructure/MemoryReader.cs b/YDotNet/Infrastructure/MemoryReader.cs index 7cbbc6ee..53555cde 100644 --- a/YDotNet/Infrastructure/MemoryReader.cs +++ b/YDotNet/Infrastructure/MemoryReader.cs @@ -1,5 +1,7 @@ using System.Runtime.InteropServices; using System.Text; +using YDotNet.Document.State; +using YDotNet.Native.Document.State; using YDotNet.Native.Types; using YDotNet.Native.Types.Maps; @@ -22,6 +24,20 @@ internal static T ReadStruct(nint handle) return Marshal.PtrToStructure(handle); } + internal static T[] ReadStructArray(nint handle, uint length) + where T : struct + { + var itemSize = Marshal.SizeOf(); + var itemBuffer = new T[length]; + + for (var i = 0; i < length; i++) + { + itemBuffer[i] = Marshal.PtrToStructure(handle + (i * itemSize)); + } + + return itemBuffer; + } + internal static byte[]? TryReadBytes(nint handle, uint length) { return handle == nint.Zero ? null : ReadBytes(handle, length); diff --git a/YDotNet/Native/Document/Events/SubDocsEventNative.cs b/YDotNet/Native/Document/Events/SubDocsEventNative.cs index 933bc483..69cfc4e4 100644 --- a/YDotNet/Native/Document/Events/SubDocsEventNative.cs +++ b/YDotNet/Native/Document/Events/SubDocsEventNative.cs @@ -1,6 +1,7 @@ using System.Runtime.InteropServices; using YDotNet.Document; using YDotNet.Document.Events; +using YDotNet.Infrastructure; namespace YDotNet.Native.Document.Events; @@ -21,25 +22,14 @@ internal readonly struct SubDocsEventNative public SubDocsEvent ToSubDocsEvent() { - var added = new Doc[AddedLength]; - var removed = new Doc[RemovedLength]; - var loaded = new Doc[LoadedLength]; - - for (var i = 0; i < AddedLength; i++) - { - added[i] = new Doc(DocChannel.Clone(Marshal.ReadIntPtr(Added, i * nint.Size))); - } - - for (var i = 0; i < RemovedLength; i++) - { - removed[i] = new Doc(DocChannel.Clone(Marshal.ReadIntPtr(Removed, i * nint.Size))); - } - - for (var i = 0; i < LoadedLength; i++) - { - loaded[i] = new Doc(DocChannel.Clone(Marshal.ReadIntPtr(Loaded, i * nint.Size))); - } - - return new SubDocsEvent(added, removed, loaded); + var nativeAdded = MemoryReader.ReadStructArray(Added, AddedLength); + var nativeRemoved = MemoryReader.ReadStructArray(Removed, RemovedLength); + var nativeLoaded = MemoryReader.ReadStructArray(Loaded, LoadedLength); + + var docsAdded = nativeAdded.Select(x => new Doc(DocChannel.Clone(x), true)).ToArray(); + var docsRemoved = nativeRemoved.Select(x => new Doc(DocChannel.Clone(x), true)).ToArray(); + var docsLoaded = nativeLoaded.Select(x => new Doc(DocChannel.Clone(x), true)).ToArray(); + + return new SubDocsEvent(docsAdded, docsRemoved, docsLoaded); } } diff --git a/YDotNet/Native/Document/State/DeleteSetNative.cs b/YDotNet/Native/Document/State/DeleteSetNative.cs index e6edb5d6..92835438 100644 --- a/YDotNet/Native/Document/State/DeleteSetNative.cs +++ b/YDotNet/Native/Document/State/DeleteSetNative.cs @@ -1,5 +1,6 @@ using System.Runtime.InteropServices; using YDotNet.Document.State; +using YDotNet.Infrastructure; namespace YDotNet.Native.Document.State; @@ -14,18 +15,14 @@ internal readonly struct DeleteSetNative public DeleteSet ToDeleteSet() { - var longSize = sizeof(ulong); - var idRangeSequenceSize = Marshal.SizeOf(); + var nativeClients = MemoryReader.ReadStructArray(ClientIds, EntriesCount); + var nativeRanges = MemoryReader.ReadStructArray(Ranges, EntriesCount); + var entries = new Dictionary(); for (var i = 0; i < EntriesCount; i++) { - var clientId = (ulong)Marshal.ReadInt64(ClientIds, i * longSize); - var rangeNative = Marshal.PtrToStructure(Ranges + i * idRangeSequenceSize); - - var range = rangeNative.ToIdRanges(); - - entries.Add(clientId, range); + entries.Add(nativeClients[i], nativeRanges[i].ToIdRanges()); } return new DeleteSet(entries); diff --git a/YDotNet/Native/Document/State/IdRangeSequenceNative.cs b/YDotNet/Native/Document/State/IdRangeSequenceNative.cs index aae623ec..53b39160 100644 --- a/YDotNet/Native/Document/State/IdRangeSequenceNative.cs +++ b/YDotNet/Native/Document/State/IdRangeSequenceNative.cs @@ -1,5 +1,6 @@ using System.Runtime.InteropServices; using YDotNet.Document.State; +using YDotNet.Infrastructure; namespace YDotNet.Native.Document.State; @@ -12,15 +13,8 @@ internal readonly struct IdRangeSequenceNative public IdRange[] ToIdRanges() { - var idRangeSize = Marshal.SizeOf(); - var sequence = new IdRange[Length]; + var rangeNatives = MemoryReader.ReadStructArray(Sequence, Length); - for (var i = 0; i < Length; i++) - { - var idRange = Marshal.PtrToStructure(Sequence + i * idRangeSize); - sequence[i] = idRange.ToIdRange(); - } - - return sequence; + return rangeNatives.Select(x => x.ToIdRange()).ToArray(); ; } } diff --git a/YDotNet/Native/Document/State/StateVectorNative.cs b/YDotNet/Native/Document/State/StateVectorNative.cs index 760f35c7..4af4b92a 100644 --- a/YDotNet/Native/Document/State/StateVectorNative.cs +++ b/YDotNet/Native/Document/State/StateVectorNative.cs @@ -1,5 +1,6 @@ using System.Runtime.InteropServices; using YDotNet.Document.State; +using YDotNet.Infrastructure; namespace YDotNet.Native.Document.State; @@ -13,14 +14,14 @@ internal readonly struct StateVectorNative public StateVector ToStateVector() { + var nativeClients = MemoryReader.ReadStructArray(ClientIds, EntriesCount); + var nativeClocks = MemoryReader.ReadStructArray(Clocks, EntriesCount); + var entries = new Dictionary(); for (var i = 0; i < EntriesCount; i++) { - var clientId = (ulong)Marshal.ReadInt64(ClientIds, i * sizeof(ulong)); - var clock = (uint)Marshal.ReadInt32(Clocks, i * sizeof(uint)); - - entries.Add(clientId, clock); + entries.Add(nativeClients[i], nativeClocks[i]); } return new StateVector(entries); From 743b1209a1b2d1448fc9f9aa2b61cd6329cf33ce Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 12 Oct 2023 14:05:04 +0200 Subject: [PATCH 089/186] Add doc everywhere. --- YDotNet/Document/Cells/JsonArray.cs | 8 +-- YDotNet/Document/Cells/JsonObject.cs | 8 +-- YDotNet/Document/Cells/Output.cs | 22 +++---- YDotNet/Document/Doc.cs | 60 +++++++++++++++---- YDotNet/Document/Transactions/Transaction.cs | 17 +++--- YDotNet/Document/Types/Arrays/Array.cs | 10 ++-- .../Document/Types/Arrays/ArrayEnumerator.cs | 2 +- .../Document/Types/Arrays/ArrayIterator.cs | 5 +- .../Types/Arrays/Events/ArrayEvent.cs | 12 +++- YDotNet/Document/Types/Branches/Branch.cs | 19 +++--- YDotNet/Document/Types/Events/EventBranch.cs | 16 ++--- YDotNet/Document/Types/Events/EventChanges.cs | 4 +- .../Types/Events/EventDeltaAttribute.cs | 4 +- YDotNet/Document/Types/Events/EventDeltas.cs | 4 +- YDotNet/Document/Types/Events/EventKeys.cs | 4 +- .../Document/Types/Maps/Events/MapEvent.cs | 6 +- YDotNet/Document/Types/Maps/Map.cs | 10 ++-- YDotNet/Document/Types/Maps/MapEntry.cs | 4 +- YDotNet/Document/Types/Maps/MapEnumerator.cs | 2 +- YDotNet/Document/Types/Maps/MapIterator.cs | 5 +- .../Document/Types/Texts/Events/TextEvent.cs | 16 ++--- YDotNet/Document/Types/Texts/Text.cs | 8 +-- YDotNet/Document/Types/Texts/TextChunk.cs | 6 +- YDotNet/Document/Types/Texts/TextChunks.cs | 4 +- .../XmlElements/Events/XmlElementEvent.cs | 12 ++-- .../Types/XmlElements/Trees/XmlTreeWalker.cs | 5 +- .../Trees/XmlTreeWalkerEnumerator.cs | 2 +- .../Document/Types/XmlElements/XmlElement.cs | 24 ++++---- .../Types/XmlTexts/Events/XmlTextEvent.cs | 8 +-- YDotNet/Document/Types/XmlTexts/XmlText.cs | 10 ++-- .../Document/Events/SubDocsEventNative.cs | 8 +-- .../Native/Types/Events/EventChangeNative.cs | 5 +- .../Native/Types/Events/EventDeltaNative.cs | 7 ++- .../Types/Events/EventKeyChangeNative.cs | 7 ++- 34 files changed, 198 insertions(+), 146 deletions(-) diff --git a/YDotNet/Document/Cells/JsonArray.cs b/YDotNet/Document/Cells/JsonArray.cs index 29ccf0ce..20956a0f 100644 --- a/YDotNet/Document/Cells/JsonArray.cs +++ b/YDotNet/Document/Cells/JsonArray.cs @@ -10,12 +10,12 @@ namespace YDotNet.Document.Cells; /// public sealed class JsonArray : ReadOnlyCollection { - internal JsonArray(nint handle, uint length, IResourceOwner owner) - : base(ReadItems(handle, length, owner)) + internal JsonArray(nint handle, uint length, Doc doc, IResourceOwner owner) + : base(ReadItems(handle, length, doc, owner)) { } - private static List ReadItems(nint handle, uint length, IResourceOwner owner) + private static List ReadItems(nint handle, uint length, Doc doc, IResourceOwner owner) { var collectionHandle = OutputChannel.Collection(handle); var collectionNatives = MemoryReader.ReadIntPtrArray(collectionHandle, length, Marshal.SizeOf()); @@ -25,7 +25,7 @@ private static List ReadItems(nint handle, uint length, IResourceOwner o foreach (var itemHandle in collectionNatives) { // The outputs are owned by this block of allocated memory. - result.Add(new Output(itemHandle, owner)); + result.Add(new Output(itemHandle, doc, owner)); } return result; diff --git a/YDotNet/Document/Cells/JsonObject.cs b/YDotNet/Document/Cells/JsonObject.cs index 9ea6dc44..c048a4ee 100644 --- a/YDotNet/Document/Cells/JsonObject.cs +++ b/YDotNet/Document/Cells/JsonObject.cs @@ -11,12 +11,12 @@ namespace YDotNet.Document.Cells; /// public sealed class JsonObject : ReadOnlyDictionary { - internal JsonObject(nint handle, uint length, IResourceOwner owner) - : base(ReadItems(handle, length, owner)) + internal JsonObject(nint handle, uint length, Doc doc, IResourceOwner owner) + : base(ReadItems(handle, length, doc, owner)) { } - private static Dictionary ReadItems(nint handle, uint length, IResourceOwner owner) + private static Dictionary ReadItems(nint handle, uint length, Doc doc, IResourceOwner owner) { var entriesHandle = OutputChannel.Object(handle).Checked(); var entriesNatives = MemoryReader.ReadIntPtrArray(entriesHandle, length, Marshal.SizeOf()); @@ -28,7 +28,7 @@ private static Dictionary ReadItems(nint handle, uint length, IR var (mapEntry, outputHandle) = MemoryReader.ReadMapEntryAndOutputHandle(itemHandle); var mapEntryKey = MemoryReader.ReadUtf8String(mapEntry.Field); - result[mapEntryKey] = new Output(outputHandle, owner); + result[mapEntryKey] = new Output(outputHandle, doc, owner); } return result; diff --git a/YDotNet/Document/Cells/Output.cs b/YDotNet/Document/Cells/Output.cs index 3173e397..acd4b5f5 100644 --- a/YDotNet/Document/Cells/Output.cs +++ b/YDotNet/Document/Cells/Output.cs @@ -18,7 +18,7 @@ public sealed class Output : UnmanagedResource { private readonly Lazy value; - internal Output(nint handle, IResourceOwner? owner) + internal Output(nint handle, Doc doc, IResourceOwner? owner) : base(handle, owner) { var native = Marshal.PtrToStructure(handle.Checked()); @@ -26,7 +26,7 @@ internal Output(nint handle, IResourceOwner? owner) Type = (OutputType)native.Tag; // We use lazy because some types like Doc and Map need to be disposed and therefore they should not be allocated, if not needed. - value = BuildValue(handle, native.Length, Type, this); + value = BuildValue(handle, native.Length, doc, Type, this); } /// @@ -134,7 +134,7 @@ protected internal override void DisposeCore(bool disposing) /// Value is not a . public XmlText XmlText => GetValue(OutputType.XmlText); - private static Lazy BuildValue(nint handle, uint length, OutputType type, IResourceOwner owner) + private static Lazy BuildValue(nint handle, uint length, Doc doc, OutputType type, IResourceOwner owner) { switch (type) { @@ -176,31 +176,31 @@ protected internal override void DisposeCore(bool disposing) case OutputType.Collection: { - return new Lazy(() => new JsonArray(handle, length, owner)); + return new Lazy(() => new JsonArray(handle, length, doc, owner)); } case OutputType.Object: { - return new Lazy(() => new JsonObject(handle, length, owner)); + return new Lazy(() => new JsonObject(handle, length, doc, owner)); } case OutputType.Array: - return new Lazy(() => new Array(OutputChannel.Array(handle).Checked())); + return new Lazy(() => doc.GetArray(OutputChannel.Array(handle))); case OutputType.Map: - return new Lazy(() => new Map(OutputChannel.Map(handle).Checked())); + return new Lazy(() => doc.GetMap(OutputChannel.Map(handle))); case OutputType.Text: - return new Lazy(() => new Text(OutputChannel.Text(handle).Checked())); + return new Lazy(() => doc.GetText(OutputChannel.Text(handle))); case OutputType.XmlElement: - return new Lazy(() => new XmlElement(OutputChannel.XmlElement(handle).Checked())); + return new Lazy(() => doc.GetXmlElement(OutputChannel.XmlElement(handle))); case OutputType.XmlText: - return new Lazy(() => new XmlText(OutputChannel.XmlText(handle).Checked())); + return new Lazy(() => doc.GetXmlText(OutputChannel.XmlText(handle))); case OutputType.Doc: - return new Lazy(() => new Doc(OutputChannel.Doc(handle).Checked(), false)); + return new Lazy(() => doc.GetDoc(OutputChannel.Doc(handle))); default: return new Lazy((object?)null); diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index 90e52c2e..b8c55999 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -30,8 +30,9 @@ namespace YDotNet.Document; /// to recursively nested types). /// /// -public class Doc : UnmanagedResource +public class Doc : UnmanagedResource, ITypeBase { + private readonly TypeCache typeCache = new TypeCache(); private readonly EventSubscriptions subscriptions = new(); private readonly bool isCloned; @@ -56,7 +57,7 @@ public Doc(DocOptions options) { } - internal Doc(nint handle, bool isCloned) + internal Doc(nint handle, bool isCloned, Doc? parent) : base(handle) { this.isCloned = isCloned; @@ -136,7 +137,7 @@ public Doc Clone() { var handle = DocChannel.Clone(Handle).Checked(); - return new Doc(handle, true); + return new Doc(handle, true, null); } /// @@ -154,7 +155,7 @@ public Text Text(string name) var handle = DocChannel.Text(Handle, unsafeName.Handle); - return new Text(handle.Checked()); + return GetText(handle); } /// @@ -172,7 +173,7 @@ public Map Map(string name) var handle = DocChannel.Map(Handle, unsafeName.Handle); - return new Map(handle.Checked()); + return GetMap(handle); } /// @@ -190,7 +191,7 @@ public Array Array(string name) var handle = DocChannel.Array(Handle, unsafeName.Handle); - return new Array(handle.Checked()); + return GetArray(handle); } /// @@ -208,7 +209,7 @@ public XmlElement XmlElement(string name) var handle = DocChannel.XmlElement(Handle, unsafeName.Handle); - return new XmlElement(handle.Checked()); + return GetXmlElement(handle); } /// @@ -226,7 +227,7 @@ public XmlText XmlText(string name) var handle = DocChannel.XmlText(Handle, unsafeName.Handle); - return new XmlText(handle.Checked()); + return GetXmlText(handle); } /// @@ -245,7 +246,7 @@ public Transaction WriteTransaction(byte[]? origin = null) return default!; } - return new Transaction(handle); + return new Transaction(handle, this); } /// @@ -263,7 +264,7 @@ public Transaction ReadTransaction() return default!; } - return new Transaction(handle); + return new Transaction(handle, this); } /// @@ -295,7 +296,7 @@ public void Load(Transaction transaction) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable ObserveClear(Action action) { - DocChannel.ObserveClearCallback callback = (_, doc) => action(ClearEventNative.From(new Doc(doc, false)).ToClearEvent()); + DocChannel.ObserveClearCallback callback = (_, doc) => action(ClearEventNative.From(this).ToClearEvent()); var subscriptionId = DocChannel.ObserveClear( Handle, @@ -384,7 +385,7 @@ public IDisposable ObserveAfterTransaction(Action action) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable ObserveSubDocs(Action action) { - DocChannel.ObserveSubdocsCallback callback = (_, eventHandle) => action(MemoryReader.ReadStruct(eventHandle).ToSubDocsEvent()); + DocChannel.ObserveSubdocsCallback callback = (_, eventHandle) => action(MemoryReader.ReadStruct(eventHandle).ToSubDocsEvent(this)); var subscriptionId = DocChannel.ObserveSubDocs( Handle, @@ -396,4 +397,39 @@ public IDisposable ObserveSubDocs(Action action) DocChannel.UnobserveSubDocs(Handle, subscriptionId); }); } + + internal Doc GetDoc(nint handle) + { + return typeCache.GetOrAdd(handle, h => new Doc(h, false, this)); + } + + internal Map GetMap(nint handle) + { + return typeCache.GetOrAdd(handle, h => new Map(h, this)); + } + + internal Array GetArray(nint handle) + { + return typeCache.GetOrAdd(handle, h => new Array(h, this)); + } + + internal Text GetText(nint handle) + { + return typeCache.GetOrAdd(handle, h => new Text(h, this)); + } + + internal XmlText GetXmlText(nint handle) + { + return typeCache.GetOrAdd(handle, h => new XmlText(h, this)); + } + + internal XmlElement GetXmlElement(nint handle) + { + return typeCache.GetOrAdd(handle, h => new XmlElement(h, this)); + } + + public void MarkDeleted() + { + throw new NotImplementedException(); + } } diff --git a/YDotNet/Document/Transactions/Transaction.cs b/YDotNet/Document/Transactions/Transaction.cs index 5647c3ee..33a1196f 100644 --- a/YDotNet/Document/Transactions/Transaction.cs +++ b/YDotNet/Document/Transactions/Transaction.cs @@ -22,9 +22,12 @@ namespace YDotNet.Document.Transactions; /// public class Transaction : UnmanagedResource { - internal Transaction(nint handle) + private readonly Doc doc; + + internal Transaction(nint handle, Doc doc) : base(handle) { + this.doc = doc; } /// @@ -63,7 +66,7 @@ public Doc[] SubDocs() var handle = TransactionChannel.SubDocs(Handle, out var length); var handles = MemoryReader.ReadStructArray(handle, length); - return handles.Select(x => new Doc(handle, false)).ToArray(); + return handles.Select(h => doc.GetDoc(h)).ToArray(); } /// @@ -279,7 +282,7 @@ public byte[] Snapshot() { var handle = GetWithKind(name, BranchKind.Array); - return handle != nint.Zero ? new Array(handle) : null; + return handle != nint.Zero ? doc.GetArray(handle) : null; } /// @@ -295,7 +298,7 @@ public byte[] Snapshot() { var handle = GetWithKind(name, BranchKind.Map); - return handle != nint.Zero ? new Map(handle) : null; + return handle != nint.Zero ? doc.GetMap(handle) : null; } /// @@ -311,7 +314,7 @@ public byte[] Snapshot() { var handle = GetWithKind(name, BranchKind.Text); - return handle != nint.Zero ? new Text(handle) : null; + return handle != nint.Zero ? doc.GetText(handle) : null; } /// @@ -327,7 +330,7 @@ public byte[] Snapshot() { var handle = GetWithKind(name, BranchKind.XmlElement); - return handle != nint.Zero ? new XmlElement(handle) : null; + return handle != nint.Zero ? doc.GetXmlElement(handle) : null; } /// @@ -343,7 +346,7 @@ public byte[] Snapshot() { var handle = GetWithKind(name, BranchKind.XmlText); - return handle != nint.Zero ? new XmlText(handle) : null; + return handle != nint.Zero ? doc.GetXmlText(handle) : null; } private nint GetWithKind(string name, BranchKind expectedKind) diff --git a/YDotNet/Document/Types/Arrays/Array.cs b/YDotNet/Document/Types/Arrays/Array.cs index 6456c097..4245f167 100644 --- a/YDotNet/Document/Types/Arrays/Array.cs +++ b/YDotNet/Document/Types/Arrays/Array.cs @@ -17,8 +17,8 @@ public class Array : Branch { private readonly EventSubscriptions subscriptions = new(); - internal Array(nint handle) - : base(handle) + internal Array(nint handle, Doc doc) + : base(handle, doc) { } @@ -65,7 +65,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) { var handle = ArrayChannel.Get(Handle, transaction.Handle, index); - return handle != nint.Zero ? new Output(handle, null) : null; + return handle != nint.Zero ? new Output(handle, Doc, null) : null; } /// @@ -92,7 +92,7 @@ public ArrayIterator Iterate(Transaction transaction) { var handle = ArrayChannel.Iterator(Handle, transaction.Handle); - return new ArrayIterator(handle.Checked()); + return new ArrayIterator(handle.Checked(), Doc); } /// @@ -105,7 +105,7 @@ public ArrayIterator Iterate(Transaction transaction) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable Observe(Action action) { - ArrayChannel.ObserveCallback callback = (_, eventHandle) => action(new ArrayEvent(eventHandle)); + ArrayChannel.ObserveCallback callback = (_, eventHandle) => action(new ArrayEvent(eventHandle, Doc)); var subscriptionId = ArrayChannel.Observe( Handle, diff --git a/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs b/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs index 2d734e95..127bd2ca 100644 --- a/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs +++ b/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs @@ -36,7 +36,7 @@ public bool MoveNext() var handle = ArrayChannel.IteratorNext(Iterator.Handle); // The output has now owner and can just be disposed when not needed anymore. - current = handle != nint.Zero ? new Output(handle, null) : null; + current = handle != nint.Zero ? new Output(handle, Iterator.Doc, null) : null; return current != null; } diff --git a/YDotNet/Document/Types/Arrays/ArrayIterator.cs b/YDotNet/Document/Types/Arrays/ArrayIterator.cs index d5988cb5..5ec3ba1c 100644 --- a/YDotNet/Document/Types/Arrays/ArrayIterator.cs +++ b/YDotNet/Document/Types/Arrays/ArrayIterator.cs @@ -13,9 +13,10 @@ namespace YDotNet.Document.Types.Arrays; /// public class ArrayIterator : UnmanagedResource, IEnumerable { - internal ArrayIterator(nint handle) + internal ArrayIterator(nint handle, Doc doc) : base(handle) { + Doc = doc; } /// @@ -32,6 +33,8 @@ protected internal override void DisposeCore(bool disposing) ArrayChannel.IteratorDestroy(Handle); } + internal Doc Doc { get; } + /// public IEnumerator GetEnumerator() { diff --git a/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs b/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs index a298183e..11213144 100644 --- a/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs +++ b/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs @@ -13,11 +13,13 @@ public class ArrayEvent : UnmanagedResource private readonly Lazy delta; private readonly Lazy target; - internal ArrayEvent(nint handle) + internal ArrayEvent(nint handle, Doc doc) : base(handle) { path = new Lazy(() => { + ThrowIfDisposed(); + var pathHandle = ArrayChannel.ObserveEventPath(handle, out var length).Checked(); return new EventPath(pathHandle, length, this); @@ -25,16 +27,20 @@ internal ArrayEvent(nint handle) delta = new Lazy(() => { + ThrowIfDisposed(); + var deltaHandle = ArrayChannel.ObserveEventDelta(handle, out var length).Checked(); - return new EventChanges(deltaHandle, length, this); + return new EventChanges(deltaHandle, length, doc, this); }); target = new Lazy(() => { + ThrowIfDisposed(); + var targetHandle = ArrayChannel.ObserveEventTarget(handle).Checked(); - return new Array(targetHandle); + return new Array(targetHandle, doc); }); } diff --git a/YDotNet/Document/Types/Branches/Branch.cs b/YDotNet/Document/Types/Branches/Branch.cs index 000644a7..64b9667c 100644 --- a/YDotNet/Document/Types/Branches/Branch.cs +++ b/YDotNet/Document/Types/Branches/Branch.cs @@ -9,19 +9,20 @@ namespace YDotNet.Document.Types.Branches; /// /// The generic type that can be used to refer to all shared data type instances. /// -public abstract class Branch +public abstract class Branch : TypeBase { private readonly EventSubscriptions subscriptions = new(); + private readonly Doc doc; - /// - /// Initializes a new instance of the class. - /// - /// The handle to the native resource. - protected Branch(nint handle) + internal Branch(nint handle, Doc doc) { + Doc = doc; + Handle = handle; } + internal Doc Doc { get; } + internal nint Handle { get; } /// @@ -37,7 +38,7 @@ public IDisposable ObserveDeep(Action> action) { BranchChannel.ObserveCallback callback = (_, length, eventsHandle) => { - var events = MemoryReader.ReadIntPtrArray(eventsHandle, length, size: 24).Select(x => new EventBranch(x)).ToArray(); + var events = MemoryReader.ReadIntPtrArray(eventsHandle, length, size: 24).Select(x => new EventBranch(x, doc)).ToArray(); action(events); }; @@ -68,7 +69,7 @@ public Transaction WriteTransaction() return default!; } - return new Transaction(handle); + return new Transaction(handle, doc); } /// @@ -86,6 +87,6 @@ public Transaction ReadTransaction() return default!; } - return new Transaction(handle); + return new Transaction(handle, doc); } } diff --git a/YDotNet/Document/Types/Events/EventBranch.cs b/YDotNet/Document/Types/Events/EventBranch.cs index c0e79d10..92fa90a3 100644 --- a/YDotNet/Document/Types/Events/EventBranch.cs +++ b/YDotNet/Document/Types/Events/EventBranch.cs @@ -14,11 +14,7 @@ namespace YDotNet.Document.Types.Events; /// public class EventBranch { - /// - /// Initializes a new instance of the class. - /// - /// The handle to the native resource. - public EventBranch(nint handle) + internal EventBranch(nint handle, Doc doc) { Handle = handle; Tag = (EventBranchTag)Marshal.ReadByte(handle); @@ -26,19 +22,19 @@ public EventBranch(nint handle) switch (Tag) { case EventBranchTag.Map: - MapEvent = new MapEvent(handle + MemoryConstants.PointerSize); + MapEvent = new MapEvent(handle + MemoryConstants.PointerSize, doc); break; case EventBranchTag.Text: - TextEvent = new TextEvent(handle + MemoryConstants.PointerSize); + TextEvent = new TextEvent(handle + MemoryConstants.PointerSize, doc); break; case EventBranchTag.Array: - ArrayEvent = new ArrayEvent(handle + MemoryConstants.PointerSize); + ArrayEvent = new ArrayEvent(handle + MemoryConstants.PointerSize, doc); break; case EventBranchTag.XmlElement: - XmlElementEvent = new XmlElementEvent(handle + MemoryConstants.PointerSize); + XmlElementEvent = new XmlElementEvent(handle + MemoryConstants.PointerSize, doc); break; case EventBranchTag.XmlText: - XmlTextEvent = new XmlTextEvent(handle + MemoryConstants.PointerSize); + XmlTextEvent = new XmlTextEvent(handle + MemoryConstants.PointerSize, doc); break; default: throw new NotSupportedException( diff --git a/YDotNet/Document/Types/Events/EventChanges.cs b/YDotNet/Document/Types/Events/EventChanges.cs index e27e73d7..612295ad 100644 --- a/YDotNet/Document/Types/Events/EventChanges.cs +++ b/YDotNet/Document/Types/Events/EventChanges.cs @@ -12,13 +12,13 @@ public class EventChanges : UnmanagedCollectionResource { private readonly uint length; - internal EventChanges(nint handle, uint length, IResourceOwner owner) + internal EventChanges(nint handle, uint length, Doc doc, IResourceOwner owner) : base(handle, owner) { foreach (var itemHandle in MemoryReader.ReadIntPtrArray(handle, length, Marshal.SizeOf())) { // The event delta creates output that are owned by this block of allocated memory. - AddItem(Marshal.PtrToStructure(itemHandle).ToEventChange(this)); + AddItem(Marshal.PtrToStructure(itemHandle).ToEventChange(this, doc)); } this.length = length; diff --git a/YDotNet/Document/Types/Events/EventDeltaAttribute.cs b/YDotNet/Document/Types/Events/EventDeltaAttribute.cs index 70b675a3..08a052d9 100644 --- a/YDotNet/Document/Types/Events/EventDeltaAttribute.cs +++ b/YDotNet/Document/Types/Events/EventDeltaAttribute.cs @@ -11,7 +11,7 @@ public sealed class EventDeltaAttribute { private readonly Lazy value; - internal EventDeltaAttribute(nint handle, IResourceOwner owner) + internal EventDeltaAttribute(nint handle, Doc doc, IResourceOwner owner) { Handle = handle; @@ -20,7 +20,7 @@ internal EventDeltaAttribute(nint handle, IResourceOwner owner) value = new Lazy(() => { - return new Output(handle + MemoryConstants.PointerSize, owner); + return new Output(handle + MemoryConstants.PointerSize, doc, owner); }); } diff --git a/YDotNet/Document/Types/Events/EventDeltas.cs b/YDotNet/Document/Types/Events/EventDeltas.cs index 762878ef..847e1ce0 100644 --- a/YDotNet/Document/Types/Events/EventDeltas.cs +++ b/YDotNet/Document/Types/Events/EventDeltas.cs @@ -12,7 +12,7 @@ public class EventDeltas : UnmanagedCollectionResource { private readonly uint length; - internal EventDeltas(nint handle, uint length, IResourceOwner owner) + internal EventDeltas(nint handle, uint length, Doc doc, IResourceOwner owner) : base(handle, owner) { this.length = length; @@ -20,7 +20,7 @@ internal EventDeltas(nint handle, uint length, IResourceOwner owner) foreach (var itemHandle in MemoryReader.ReadIntPtrArray(handle, length, Marshal.SizeOf())) { // The event delta creates output that are owned by this block of allocated memory. - AddItem(Marshal.PtrToStructure(itemHandle).ToEventDelta(this)); + AddItem(Marshal.PtrToStructure(itemHandle).ToEventDelta(doc, this)); } } diff --git a/YDotNet/Document/Types/Events/EventKeys.cs b/YDotNet/Document/Types/Events/EventKeys.cs index 239646a6..17852931 100644 --- a/YDotNet/Document/Types/Events/EventKeys.cs +++ b/YDotNet/Document/Types/Events/EventKeys.cs @@ -13,13 +13,13 @@ public class EventKeys : UnmanagedCollectionResource { private readonly uint length; - internal EventKeys(nint handle, uint length, IResourceOwner owner) + internal EventKeys(nint handle, uint length, Doc doc, IResourceOwner owner) : base(handle, owner) { foreach (var keyHandle in MemoryReader.ReadIntPtrArray(handle, length, Marshal.SizeOf())) { // The event delta creates output that are owned by this block of allocated memory. - AddItem(Marshal.PtrToStructure(keyHandle).ToEventKeyChange(this)); + AddItem(Marshal.PtrToStructure(keyHandle).ToEventKeyChange(doc, owner)); } this.length = length; diff --git a/YDotNet/Document/Types/Maps/Events/MapEvent.cs b/YDotNet/Document/Types/Maps/Events/MapEvent.cs index d4a6558c..9f10b45e 100644 --- a/YDotNet/Document/Types/Maps/Events/MapEvent.cs +++ b/YDotNet/Document/Types/Maps/Events/MapEvent.cs @@ -14,7 +14,7 @@ public class MapEvent : UnmanagedResource private readonly Lazy keys; private readonly Lazy target; - internal MapEvent(nint handle) + internal MapEvent(nint handle, Doc doc) : base(handle) { path = new Lazy(() => @@ -28,14 +28,14 @@ internal MapEvent(nint handle) { var keysHandle = MapChannel.ObserveEventKeys(handle, out var length).Checked(); - return new EventKeys(keysHandle, length, this); + return new EventKeys(keysHandle, length, doc, this); }); target = new Lazy(() => { var targetHandle = MapChannel.ObserveEventTarget(handle).Checked(); - return new Map(targetHandle); + return doc.GetMap(targetHandle); }); } diff --git a/YDotNet/Document/Types/Maps/Map.cs b/YDotNet/Document/Types/Maps/Map.cs index 5a5f4861..57fdc318 100644 --- a/YDotNet/Document/Types/Maps/Map.cs +++ b/YDotNet/Document/Types/Maps/Map.cs @@ -15,8 +15,8 @@ public class Map : Branch { private readonly EventSubscriptions subscriptions = new(); - internal Map(nint handle) - : base(handle) + internal Map(nint handle, Doc doc) + : base(handle, doc) { } @@ -56,7 +56,7 @@ public void Insert(Transaction transaction, string key, Input input) return null; } - return new Output(outputHandle, null); + return new Output(outputHandle, Doc, null); } /// @@ -101,7 +101,7 @@ public MapIterator Iterate(Transaction transaction) { var handle = MapChannel.Iterator(Handle, transaction.Handle).Checked(); - return new MapIterator(handle); + return new MapIterator(handle, Doc); } /// @@ -114,7 +114,7 @@ public MapIterator Iterate(Transaction transaction) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable Observe(Action action) { - MapChannel.ObserveCallback callback = (_, eventHandle) => action(new MapEvent(eventHandle)); + MapChannel.ObserveCallback callback = (_, eventHandle) => action(new MapEvent(eventHandle, Doc)); var subscriptionId = MapChannel.Observe( Handle, diff --git a/YDotNet/Document/Types/Maps/MapEntry.cs b/YDotNet/Document/Types/Maps/MapEntry.cs index 364531b0..bdf8e9e6 100644 --- a/YDotNet/Document/Types/Maps/MapEntry.cs +++ b/YDotNet/Document/Types/Maps/MapEntry.cs @@ -9,14 +9,14 @@ namespace YDotNet.Document.Types.Maps; /// public sealed class MapEntry : UnmanagedResource { - internal MapEntry(nint handle, IResourceOwner? owner) + internal MapEntry(nint handle, Doc doc, IResourceOwner? owner) : base(handle) { var (mapEntry, outputHandle) = MemoryReader.ReadMapEntryAndOutputHandle(handle); Key = MemoryReader.ReadUtf8String(mapEntry.Field); // The output can not released independently and will be released when the entry is not needed anymore. - Value = new Output(outputHandle, owner ?? this); + Value = new Output(outputHandle, doc, owner ?? this); } /// diff --git a/YDotNet/Document/Types/Maps/MapEnumerator.cs b/YDotNet/Document/Types/Maps/MapEnumerator.cs index 7779abcf..f5d1e542 100644 --- a/YDotNet/Document/Types/Maps/MapEnumerator.cs +++ b/YDotNet/Document/Types/Maps/MapEnumerator.cs @@ -34,7 +34,7 @@ public bool MoveNext() var handle = MapChannel.IteratorNext(Iterator.Handle); // The map entry also manages the value of the output. - current = handle != nint.Zero ? new MapEntry(handle, Iterator) : null; + current = handle != nint.Zero ? new MapEntry(handle, Iterator.Doc, Iterator) : null; return current != null; } diff --git a/YDotNet/Document/Types/Maps/MapIterator.cs b/YDotNet/Document/Types/Maps/MapIterator.cs index 5bd5db7e..278e15a0 100644 --- a/YDotNet/Document/Types/Maps/MapIterator.cs +++ b/YDotNet/Document/Types/Maps/MapIterator.cs @@ -19,9 +19,10 @@ namespace YDotNet.Document.Types.Maps; /// public class MapIterator : UnmanagedResource, IEnumerable { - internal MapIterator(nint handle) + internal MapIterator(nint handle, Doc doc) : base(handle) { + Doc = doc; } /// @@ -38,6 +39,8 @@ protected internal override void DisposeCore(bool disposing) MapChannel.IteratorDestroy(Handle); } + internal Doc Doc { get; } + /// public IEnumerator GetEnumerator() { diff --git a/YDotNet/Document/Types/Texts/Events/TextEvent.cs b/YDotNet/Document/Types/Texts/Events/TextEvent.cs index 603b78ba..6e86d33d 100644 --- a/YDotNet/Document/Types/Texts/Events/TextEvent.cs +++ b/YDotNet/Document/Types/Texts/Events/TextEvent.cs @@ -13,15 +13,13 @@ public class TextEvent : UnmanagedResource private readonly Lazy deltas; private readonly Lazy target; - /// - /// Initializes a new instance of the class. - /// - /// The handle to the native resource. - internal TextEvent(nint handle) + internal TextEvent(nint handle, Doc doc) : base(handle) { path = new Lazy(() => { + ThrowIfDisposed(); + var pathHandle = TextChannel.ObserveEventPath(handle, out var length).Checked(); return new EventPath(pathHandle, length, this); @@ -29,16 +27,20 @@ internal TextEvent(nint handle) deltas = new Lazy(() => { + ThrowIfDisposed(); + var deltaHandle = TextChannel.ObserveEventDelta(handle, out var length).Checked(); - return new EventDeltas(deltaHandle, length, this); + return new EventDeltas(deltaHandle, length, doc, this); }); target = new Lazy(() => { + ThrowIfDisposed(); + var targetHandle = TextChannel.ObserveEventTarget(handle).Checked(); - return new Text(targetHandle); + return doc.GetText(targetHandle); }); } diff --git a/YDotNet/Document/Types/Texts/Text.cs b/YDotNet/Document/Types/Texts/Text.cs index c5cf0ac0..3854f3b9 100644 --- a/YDotNet/Document/Types/Texts/Text.cs +++ b/YDotNet/Document/Types/Texts/Text.cs @@ -17,8 +17,8 @@ public class Text : Branch { private readonly EventSubscriptions subscriptions = new(); - internal Text(nint handle) - : base(handle) + internal Text(nint handle, Doc doc) + : base(handle, doc) { } @@ -101,7 +101,7 @@ public TextChunks Chunks(Transaction transaction) { var handle = TextChannel.Chunks(Handle, transaction.Handle, out var length).Checked(); - return new TextChunks(handle, length); + return new TextChunks(handle, length, Doc); } /// @@ -136,7 +136,7 @@ public uint Length(Transaction transaction) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable Observe(Action action) { - TextChannel.ObserveCallback callback = (_, eventHandle) => action(new TextEvent(eventHandle)); + TextChannel.ObserveCallback callback = (_, eventHandle) => action(new TextEvent(eventHandle, Doc)); var subscriptionId = TextChannel.Observe( Handle, diff --git a/YDotNet/Document/Types/Texts/TextChunk.cs b/YDotNet/Document/Types/Texts/TextChunk.cs index 5ad925b7..c5432d11 100644 --- a/YDotNet/Document/Types/Texts/TextChunk.cs +++ b/YDotNet/Document/Types/Texts/TextChunk.cs @@ -12,9 +12,9 @@ namespace YDotNet.Document.Types.Texts; /// public class TextChunk { - internal TextChunk(nint handle, IResourceOwner owner) + internal TextChunk(nint handle, Doc doc, IResourceOwner owner) { - Data = new Output(handle, owner); + Data = new Output(handle, doc, owner); var offset = Marshal.SizeOf(); @@ -31,7 +31,7 @@ internal TextChunk(nint handle, IResourceOwner owner) attributesHandle, attributesLength, Marshal.SizeOf()) - .Select(x => new MapEntry(x, owner)).ToList(); + .Select(x => new MapEntry(x, doc, owner)).ToList(); } /// diff --git a/YDotNet/Document/Types/Texts/TextChunks.cs b/YDotNet/Document/Types/Texts/TextChunks.cs index 52646247..ac309d62 100644 --- a/YDotNet/Document/Types/Texts/TextChunks.cs +++ b/YDotNet/Document/Types/Texts/TextChunks.cs @@ -10,13 +10,13 @@ public class TextChunks : UnmanagedCollectionResource { private readonly uint length; - internal TextChunks(nint handle, uint length) + internal TextChunks(nint handle, uint length, Doc doc) : base(handle, null) { foreach (var chunkHandle in MemoryReader.ReadIntPtrArray(handle, length, size: 32)) { // The cunks create output that are owned by this block of allocated memory. - AddItem(new TextChunk(chunkHandle, this)); + AddItem(new TextChunk(chunkHandle, doc, this)); } GC.Collect(); diff --git a/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs b/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs index 6e8ef4f2..6f627187 100644 --- a/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs +++ b/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs @@ -14,11 +14,7 @@ public class XmlElementEvent : UnmanagedResource private readonly Lazy keys; private readonly Lazy target; - /// - /// Initializes a new instance of the class. - /// - /// The handle to the native resource. - internal XmlElementEvent(nint handle) + internal XmlElementEvent(nint handle, Doc doc) : base(handle) { path = new Lazy(() => @@ -32,21 +28,21 @@ internal XmlElementEvent(nint handle) { var deltaHandle = XmlElementChannel.ObserveEventDelta(handle, out var length).Checked(); - return new EventChanges(deltaHandle, length, this); + return new EventChanges(deltaHandle, length, doc, this); }); keys = new Lazy(() => { var keysHandle = XmlElementChannel.ObserveEventKeys(handle, out var length).Checked(); - return new EventKeys(keysHandle, length, this); + return new EventKeys(keysHandle, length, doc, this); }); target = new Lazy(() => { var targetHandle = XmlElementChannel.ObserveEventTarget(handle).Checked(); - return new XmlElement(targetHandle); + return doc.GetXmlElement(targetHandle); }); } diff --git a/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalker.cs b/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalker.cs index 976f6b2e..d9e1d554 100644 --- a/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalker.cs +++ b/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalker.cs @@ -15,9 +15,10 @@ namespace YDotNet.Document.Types.XmlElements.Trees; /// public class XmlTreeWalker : UnmanagedResource, IEnumerable { - internal XmlTreeWalker(nint handle) + internal XmlTreeWalker(nint handle, Doc doc) : base(handle) { + Doc = doc; } /// @@ -34,6 +35,8 @@ protected internal override void DisposeCore(bool disposing) XmlElementChannel.TreeWalkerDestroy(Handle); } + internal Doc Doc { get; } + /// public IEnumerator GetEnumerator() { diff --git a/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs b/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs index 99896de4..764d7605 100644 --- a/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs +++ b/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs @@ -42,7 +42,7 @@ public bool MoveNext() { var handle = XmlElementChannel.TreeWalkerNext(TreeWalker.Handle); - current = handle != nint.Zero ? new Output(handle, null) : null; + current = handle != nint.Zero ? new Output(handle, TreeWalker.Doc, null) : null; return Current != null; } diff --git a/YDotNet/Document/Types/XmlElements/XmlElement.cs b/YDotNet/Document/Types/XmlElements/XmlElement.cs index b266f7fe..595f7af4 100644 --- a/YDotNet/Document/Types/XmlElements/XmlElement.cs +++ b/YDotNet/Document/Types/XmlElements/XmlElement.cs @@ -17,8 +17,8 @@ public class XmlElement : Branch { private readonly EventSubscriptions subscriptions = new(); - internal XmlElement(nint handle) - : base(handle) + internal XmlElement(nint handle, Doc doc) + : base(handle, doc) { } @@ -135,7 +135,7 @@ public XmlText InsertText(Transaction transaction, uint index) { var handle = XmlElementChannel.InsertText(Handle, transaction.Handle, index); - return new XmlText(handle.Checked()); + return Doc.GetXmlText(handle); } /// @@ -150,9 +150,9 @@ public XmlElement InsertElement(Transaction transaction, uint index, string name { using var unsafeName = MemoryWriter.WriteUtf8String(name); - var elementHandle = XmlElementChannel.InsertElement(Handle, transaction.Handle, index, unsafeName.Handle); + var handle = XmlElementChannel.InsertElement(Handle, transaction.Handle, index, unsafeName.Handle); - return new XmlElement(elementHandle.Checked()); + return Doc.GetXmlElement(handle); } /// @@ -177,7 +177,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) { var handle = XmlElementChannel.Get(Handle, transaction.Handle, index); - return handle != nint.Zero ? new Output(handle, null) : null; + return handle != nint.Zero ? new Output(handle, Doc, null) : null; } /// @@ -194,7 +194,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) { var handle = XmlChannel.PreviousSibling(Handle, transaction.Handle); - return handle != nint.Zero ? new Output(handle, null) : null; + return handle != nint.Zero ? new Output(handle, Doc, null) : null; } /// @@ -211,7 +211,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) { var handle = XmlChannel.NextSibling(Handle, transaction.Handle); - return handle != nint.Zero ? new Output(handle, null) : null; + return handle != nint.Zero ? new Output(handle, Doc, null) : null; } /// @@ -227,7 +227,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) { var handle = XmlElementChannel.FirstChild(Handle, transaction.Handle); - return handle != nint.Zero ? new Output(handle, null) : null; + return handle != nint.Zero ? new Output(handle, Doc, null) : null; } /// @@ -243,7 +243,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) { var handle = XmlElementChannel.Parent(Handle, transaction.Handle); - return handle != nint.Zero ? new XmlElement(handle) : null; + return handle != nint.Zero ? Doc.GetXmlElement(handle) : null; } /// @@ -258,7 +258,7 @@ public XmlTreeWalker TreeWalker(Transaction transaction) { var handle = XmlElementChannel.TreeWalker(Handle, transaction.Handle); - return new XmlTreeWalker(handle.Checked()); + return new XmlTreeWalker(handle.Checked(), Doc); } /// @@ -271,7 +271,7 @@ public XmlTreeWalker TreeWalker(Transaction transaction) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable Observe(Action action) { - XmlElementChannel.ObserveCallback callback = (_, eventHandle) => action(new XmlElementEvent(eventHandle)); + XmlElementChannel.ObserveCallback callback = (_, eventHandle) => action(new XmlElementEvent(eventHandle, Doc)); var subscriptionId = XmlElementChannel.Observe( Handle, diff --git a/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs b/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs index 91245c91..1e23f2db 100644 --- a/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs +++ b/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs @@ -13,28 +13,28 @@ public class XmlTextEvent : UnmanagedResource private readonly Lazy keys; private readonly Lazy target; - internal XmlTextEvent(nint handle) + internal XmlTextEvent(nint handle, Doc doc) : base(handle) { delta = new Lazy(() => { var deltaHandle = XmlTextChannel.ObserveEventDelta(handle, out var length).Checked(); - return new EventDeltas(deltaHandle, length, this); + return new EventDeltas(deltaHandle, length, doc, this); }); keys = new Lazy(() => { var keysHandle = XmlTextChannel.ObserveEventKeys(handle, out var length).Checked(); - return new EventKeys(keysHandle, length, this); + return new EventKeys(keysHandle, length, doc, this); }); target = new Lazy(() => { var targetHandle = XmlTextChannel.ObserveEventTarget(handle).Checked(); - return new XmlText(targetHandle); + return doc.GetXmlText(handle); }); } diff --git a/YDotNet/Document/Types/XmlTexts/XmlText.cs b/YDotNet/Document/Types/XmlTexts/XmlText.cs index 73a5900a..62a2ae9c 100644 --- a/YDotNet/Document/Types/XmlTexts/XmlText.cs +++ b/YDotNet/Document/Types/XmlTexts/XmlText.cs @@ -18,8 +18,8 @@ public class XmlText : Branch { private readonly EventSubscriptions subscriptions = new(); - internal XmlText(nint handle) - : base(handle) + internal XmlText(nint handle, Doc doc) + : base(handle, doc) { } @@ -184,7 +184,7 @@ public void Format(Transaction transaction, uint index, uint length, Input attri { var handle = XmlChannel.PreviousSibling(Handle, transaction.Handle); - return handle != nint.Zero ? new Output(handle, null) : null; + return handle != nint.Zero ? new Output(handle, Doc, null) : null; } /// @@ -200,7 +200,7 @@ public void Format(Transaction transaction, uint index, uint length, Input attri { var handle = XmlChannel.NextSibling(Handle, transaction.Handle); - return handle != nint.Zero ? new Output(handle, null) : null; + return handle != nint.Zero ? new Output(handle, Doc, null) : null; } /// @@ -213,7 +213,7 @@ public void Format(Transaction transaction, uint index, uint length, Input attri /// The subscription for the event. It may be used to unsubscribe later. public IDisposable Observe(Action action) { - XmlTextChannel.ObserveCallback callback = (_, eventHandle) => action(new XmlTextEvent(eventHandle)); + XmlTextChannel.ObserveCallback callback = (_, eventHandle) => action(new XmlTextEvent(eventHandle, Doc)); var subscriptionId = XmlTextChannel.Observe( Handle, diff --git a/YDotNet/Native/Document/Events/SubDocsEventNative.cs b/YDotNet/Native/Document/Events/SubDocsEventNative.cs index 69cfc4e4..293d33ae 100644 --- a/YDotNet/Native/Document/Events/SubDocsEventNative.cs +++ b/YDotNet/Native/Document/Events/SubDocsEventNative.cs @@ -20,15 +20,15 @@ internal readonly struct SubDocsEventNative public nint Loaded { get; } - public SubDocsEvent ToSubDocsEvent() + public SubDocsEvent ToSubDocsEvent(Doc doc) { var nativeAdded = MemoryReader.ReadStructArray(Added, AddedLength); var nativeRemoved = MemoryReader.ReadStructArray(Removed, RemovedLength); var nativeLoaded = MemoryReader.ReadStructArray(Loaded, LoadedLength); - var docsAdded = nativeAdded.Select(x => new Doc(DocChannel.Clone(x), true)).ToArray(); - var docsRemoved = nativeRemoved.Select(x => new Doc(DocChannel.Clone(x), true)).ToArray(); - var docsLoaded = nativeLoaded.Select(x => new Doc(DocChannel.Clone(x), true)).ToArray(); + var docsAdded = nativeAdded.Select(doc.GetDoc).ToArray(); + var docsRemoved = nativeRemoved.Select(doc.GetDoc).ToArray(); + var docsLoaded = nativeLoaded.Select(doc.GetDoc).ToArray(); return new SubDocsEvent(docsAdded, docsRemoved, docsLoaded); } diff --git a/YDotNet/Native/Types/Events/EventChangeNative.cs b/YDotNet/Native/Types/Events/EventChangeNative.cs index edacb800..970cc8fc 100644 --- a/YDotNet/Native/Types/Events/EventChangeNative.cs +++ b/YDotNet/Native/Types/Events/EventChangeNative.cs @@ -1,4 +1,5 @@ using System.Runtime.InteropServices; +using YDotNet.Document; using YDotNet.Document.Cells; using YDotNet.Document.Types.Events; using YDotNet.Infrastructure; @@ -15,7 +16,7 @@ internal readonly struct EventChangeNative public nint Values { get; } - public EventChange ToEventChange(IResourceOwner owner) + public EventChange ToEventChange(IResourceOwner owner, Doc doc) { var tag = TagNative switch { @@ -27,7 +28,7 @@ public EventChange ToEventChange(IResourceOwner owner) var values = MemoryReader.TryReadIntPtrArray(Values, Length, Marshal.SizeOf())? - .Select(x => new Output(x, owner)).ToList() ?? new List(); + .Select(x => new Output(x, doc, owner)).ToList() ?? new List(); return new EventChange(tag, Length, values); } diff --git a/YDotNet/Native/Types/Events/EventDeltaNative.cs b/YDotNet/Native/Types/Events/EventDeltaNative.cs index 51e86568..a6776cf0 100644 --- a/YDotNet/Native/Types/Events/EventDeltaNative.cs +++ b/YDotNet/Native/Types/Events/EventDeltaNative.cs @@ -1,4 +1,5 @@ using System.Runtime.InteropServices; +using YDotNet.Document; using YDotNet.Document.Cells; using YDotNet.Document.Types.Events; using YDotNet.Infrastructure; @@ -18,7 +19,7 @@ internal readonly struct EventDeltaNative public nint Attributes { get; } - public EventDelta ToEventDelta(IResourceOwner owner) + public EventDelta ToEventDelta(Doc doc, IResourceOwner owner) { var tag = TagNative switch { @@ -30,12 +31,12 @@ public EventDelta ToEventDelta(IResourceOwner owner) var attributes = Attributes != nint.Zero ? - MemoryReader.ReadIntPtrArray(Attributes, AttributesLength, size: 16).Select(x => new EventDeltaAttribute(x, owner)).ToList() : + MemoryReader.ReadIntPtrArray(Attributes, AttributesLength, size: 16).Select(x => new EventDeltaAttribute(x, doc, owner)).ToList() : new List(); var insert = InsertHandle != nint.Zero ? - new Output(InsertHandle, owner) : + new Output(InsertHandle, doc, owner) : null; return new EventDelta(tag, Length, insert, attributes); diff --git a/YDotNet/Native/Types/Events/EventKeyChangeNative.cs b/YDotNet/Native/Types/Events/EventKeyChangeNative.cs index f194863e..73e0848b 100644 --- a/YDotNet/Native/Types/Events/EventKeyChangeNative.cs +++ b/YDotNet/Native/Types/Events/EventKeyChangeNative.cs @@ -1,4 +1,5 @@ using System.Runtime.InteropServices; +using YDotNet.Document; using YDotNet.Document.Cells; using YDotNet.Document.Types.Events; using YDotNet.Infrastructure; @@ -16,7 +17,7 @@ internal readonly struct EventKeyChangeNative public nint NewValue { get; } - public EventKeyChange ToEventKeyChange(IResourceOwner owner) + public EventKeyChange ToEventKeyChange(Doc doc, IResourceOwner owner) { var tag = TagNative switch { @@ -30,12 +31,12 @@ public EventKeyChange ToEventKeyChange(IResourceOwner owner) var oldOutput = OldValue != nint.Zero ? - new Output(OldValue, owner) : + new Output(OldValue, doc, owner) : null; var newOutput = NewValue != nint.Zero ? - new Output(NewValue, owner) : + new Output(NewValue, doc, owner) : null; return new EventKeyChange(key, tag, oldOutput, newOutput); From f49127f7411521f134bd27b805285bb83d435d35 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 12 Oct 2023 16:04:01 +0200 Subject: [PATCH 090/186] Improve stuff. --- .editorconfig | 1 + YDotNet/Document/Cells/Input.cs | 2 - YDotNet/Document/Cells/JsonArray.cs | 8 +-- YDotNet/Document/Cells/JsonObject.cs | 15 ++--- YDotNet/Document/Cells/Output.cs | 67 ++++++++----------- YDotNet/Document/Doc.cs | 1 - YDotNet/Document/Types/Arrays/Array.cs | 2 +- .../Document/Types/Arrays/ArrayEnumerator.cs | 14 ++-- .../Types/Arrays/Events/ArrayEvent.cs | 4 +- YDotNet/Document/Types/Events/EventChange.cs | 26 +++---- YDotNet/Document/Types/Events/EventChanges.cs | 36 ++++------ YDotNet/Document/Types/Events/EventDelta.cs | 37 +++++----- .../Types/Events/EventDeltaAttribute.cs | 4 +- YDotNet/Document/Types/Events/EventDeltas.cs | 34 ++++------ YDotNet/Document/Types/Events/EventKeys.cs | 37 +++++----- YDotNet/Document/Types/Events/EventPath.cs | 26 +++---- .../Document/Types/Maps/Events/MapEvent.cs | 4 +- YDotNet/Document/Types/Maps/Map.cs | 8 +-- YDotNet/Document/Types/Maps/MapEntry.cs | 45 ------------- YDotNet/Document/Types/Maps/MapEnumerator.cs | 28 ++++++-- YDotNet/Document/Types/Maps/MapIterator.cs | 5 +- .../Document/Types/Texts/Events/TextEvent.cs | 4 +- YDotNet/Document/Types/Texts/TextChunk.cs | 36 +++------- YDotNet/Document/Types/Texts/TextChunks.cs | 40 +++++------ .../XmlElements/Events/XmlElementEvent.cs | 6 +- .../Trees/XmlTreeWalkerEnumerator.cs | 13 +++- .../Document/Types/XmlElements/XmlElement.cs | 18 +++-- .../Types/XmlTexts/Events/XmlTextEvent.cs | 4 +- YDotNet/Document/Types/XmlTexts/XmlText.cs | 4 +- YDotNet/Infrastructure/IResourceOwner.cs | 12 ---- YDotNet/Infrastructure/MemoryReader.cs | 34 ++++++---- YDotNet/Infrastructure/Resource.cs | 16 ++--- YDotNet/Infrastructure/TypeBase.cs | 24 +++++++ YDotNet/Infrastructure/TypeCache.cs | 40 +++++++++++ .../UnmanagedCollectionResource.cs | 44 ------------ YDotNet/Infrastructure/UnmanagedResource.cs | 3 +- .../Document/Events/ClearEventNative.cs | 5 +- .../Document/Events/UpdateEventNative.cs | 1 - .../Document/State/StateVectorNative.cs | 1 - YDotNet/Native/NativeWithHandle.cs | 3 + .../Native/Types/Events/EventChangeNative.cs | 23 +++---- .../Native/Types/Events/EventDeltaNative.cs | 34 +++------- .../Types/Events/EventKeyChangeNative.cs | 6 +- YDotNet/Native/Types/Maps/MapEntryNative.cs | 17 ++++- YDotNet/Native/Types/Texts/TextChunkNative.cs | 31 +++++++++ 45 files changed, 393 insertions(+), 430 deletions(-) delete mode 100644 YDotNet/Document/Types/Maps/MapEntry.cs delete mode 100644 YDotNet/Infrastructure/IResourceOwner.cs create mode 100644 YDotNet/Infrastructure/TypeBase.cs create mode 100644 YDotNet/Infrastructure/TypeCache.cs delete mode 100644 YDotNet/Infrastructure/UnmanagedCollectionResource.cs create mode 100644 YDotNet/Native/NativeWithHandle.cs create mode 100644 YDotNet/Native/Types/Texts/TextChunkNative.cs diff --git a/.editorconfig b/.editorconfig index 5e53e414..69a7906b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -28,6 +28,7 @@ dotnet_diagnostic.SA1009.severity = suggestion # Closing parenthesis should be s # Readability rules dotnet_diagnostic.SA1101.severity = none # Prefix local calls with this +dotnet_diagnostic.SA1127.severity = none # Prefix local calls with this # Ordering rules dotnet_diagnostic.SA1200.severity = suggestion # Using directives should be placed correctly diff --git a/YDotNet/Document/Cells/Input.cs b/YDotNet/Document/Cells/Input.cs index a1ffdd6d..52626690 100644 --- a/YDotNet/Document/Cells/Input.cs +++ b/YDotNet/Document/Cells/Input.cs @@ -1,5 +1,3 @@ -using YDotNet.Document.Types.XmlElements; -using YDotNet.Document.Types.XmlTexts; using YDotNet.Infrastructure; using YDotNet.Native.Cells.Inputs; diff --git a/YDotNet/Document/Cells/JsonArray.cs b/YDotNet/Document/Cells/JsonArray.cs index 20956a0f..4acf22d5 100644 --- a/YDotNet/Document/Cells/JsonArray.cs +++ b/YDotNet/Document/Cells/JsonArray.cs @@ -10,12 +10,12 @@ namespace YDotNet.Document.Cells; /// public sealed class JsonArray : ReadOnlyCollection { - internal JsonArray(nint handle, uint length, Doc doc, IResourceOwner owner) - : base(ReadItems(handle, length, doc, owner)) + internal JsonArray(nint handle, uint length, Doc doc) + : base(ReadItems(handle, length, doc)) { } - private static List ReadItems(nint handle, uint length, Doc doc, IResourceOwner owner) + private static List ReadItems(nint handle, uint length, Doc doc) { var collectionHandle = OutputChannel.Collection(handle); var collectionNatives = MemoryReader.ReadIntPtrArray(collectionHandle, length, Marshal.SizeOf()); @@ -25,7 +25,7 @@ private static List ReadItems(nint handle, uint length, Doc doc, IResour foreach (var itemHandle in collectionNatives) { // The outputs are owned by this block of allocated memory. - result.Add(new Output(itemHandle, doc, owner)); + result.Add(new Output(itemHandle, doc)); } return result; diff --git a/YDotNet/Document/Cells/JsonObject.cs b/YDotNet/Document/Cells/JsonObject.cs index c048a4ee..1b5b21b8 100644 --- a/YDotNet/Document/Cells/JsonObject.cs +++ b/YDotNet/Document/Cells/JsonObject.cs @@ -1,5 +1,4 @@ using System.Collections.ObjectModel; -using System.Runtime.InteropServices; using YDotNet.Infrastructure; using YDotNet.Native.Cells.Outputs; using YDotNet.Native.Types.Maps; @@ -11,24 +10,20 @@ namespace YDotNet.Document.Cells; /// public sealed class JsonObject : ReadOnlyDictionary { - internal JsonObject(nint handle, uint length, Doc doc, IResourceOwner owner) - : base(ReadItems(handle, length, doc, owner)) + internal JsonObject(nint handle, uint length, Doc doc) + : base(ReadItems(handle, length, doc)) { } - private static Dictionary ReadItems(nint handle, uint length, Doc doc, IResourceOwner owner) + private static Dictionary ReadItems(nint handle, uint length, Doc doc) { var entriesHandle = OutputChannel.Object(handle).Checked(); - var entriesNatives = MemoryReader.ReadIntPtrArray(entriesHandle, length, Marshal.SizeOf()); var result = new Dictionary(); - foreach (var itemHandle in entriesNatives) + foreach (var (native, ptr) in MemoryReader.ReadIntPtrArray(entriesHandle, length)) { - var (mapEntry, outputHandle) = MemoryReader.ReadMapEntryAndOutputHandle(itemHandle); - var mapEntryKey = MemoryReader.ReadUtf8String(mapEntry.Field); - - result[mapEntryKey] = new Output(outputHandle, doc, owner); + result[native.Key()] = new Output(native.ValueHandle(ptr), doc); } return result; diff --git a/YDotNet/Document/Cells/Output.cs b/YDotNet/Document/Cells/Output.cs index acd4b5f5..948bb420 100644 --- a/YDotNet/Document/Cells/Output.cs +++ b/YDotNet/Document/Cells/Output.cs @@ -14,36 +14,27 @@ namespace YDotNet.Document.Cells; /// /// Represents a cell used to read information from the storage. /// -public sealed class Output : UnmanagedResource +public sealed class Output { - private readonly Lazy value; + private readonly object? value; - internal Output(nint handle, Doc doc, IResourceOwner? owner) - : base(handle, owner) + internal Output(nint handle, Doc doc) { - var native = Marshal.PtrToStructure(handle.Checked()); + var native = MemoryReader.PtrToStruct(handle); Type = (OutputType)native.Tag; - // We use lazy because some types like Doc and Map need to be disposed and therefore they should not be allocated, if not needed. - value = BuildValue(handle, native.Length, doc, Type, this); + value = BuildValue(handle, native.Length, doc, Type); } - /// - /// Finalizes an instance of the class. - /// - ~Output() + internal static Output CreateAndRelease(nint handle, Doc doc) { - Dispose(true); - } + var result = new Output(handle, doc); - /// - protected internal override void DisposeCore(bool disposing) - { - if (Owner == null) - { - OutputChannel.Destroy(Handle); - } + // The output reads everything so we can just destroy it. + OutputChannel.Destroy(handle); + + return result; } /// @@ -134,7 +125,7 @@ protected internal override void DisposeCore(bool disposing) /// Value is not a . public XmlText XmlText => GetValue(OutputType.XmlText); - private static Lazy BuildValue(nint handle, uint length, Doc doc, OutputType type, IResourceOwner owner) + private static object? BuildValue(nint handle, uint length, Doc doc, OutputType type) { switch (type) { @@ -142,28 +133,28 @@ protected internal override void DisposeCore(bool disposing) { var value = OutputChannel.Boolean(handle).Checked(); - return new Lazy((object?)(Marshal.PtrToStructure(value) == 1)); + return Marshal.PtrToStructure(value) == 1; } case OutputType.Double: { var value = OutputChannel.Double(handle).Checked(); - return new Lazy(Marshal.PtrToStructure(value)); + return Marshal.PtrToStructure(value); } case OutputType.Long: { var value = OutputChannel.Long(handle).Checked(); - return new Lazy(Marshal.PtrToStructure(value)); + return Marshal.PtrToStructure(value); } case OutputType.String: { MemoryReader.TryReadUtf8String(OutputChannel.String(handle), out var result); - return new Lazy(result); + return result; } case OutputType.Bytes: @@ -171,49 +162,45 @@ protected internal override void DisposeCore(bool disposing) var bytesHandle = OutputChannel.Bytes(handle).Checked(); var bytesArray = MemoryReader.ReadBytes(OutputChannel.Bytes(handle), length); - return new Lazy(bytesArray); + return bytesArray; } case OutputType.Collection: { - return new Lazy(() => new JsonArray(handle, length, doc, owner)); + return new JsonArray(handle, length, doc); } case OutputType.Object: { - return new Lazy(() => new JsonObject(handle, length, doc, owner)); + return new JsonObject(handle, length, doc); } case OutputType.Array: - return new Lazy(() => doc.GetArray(OutputChannel.Array(handle))); + return doc.GetArray(OutputChannel.Array(handle)); case OutputType.Map: - return new Lazy(() => doc.GetMap(OutputChannel.Map(handle))); + return doc.GetMap(OutputChannel.Map(handle)); case OutputType.Text: - return new Lazy(() => doc.GetText(OutputChannel.Text(handle))); + return doc.GetText(OutputChannel.Text(handle)); case OutputType.XmlElement: - return new Lazy(() => doc.GetXmlElement(OutputChannel.XmlElement(handle))); + return doc.GetXmlElement(OutputChannel.XmlElement(handle)); case OutputType.XmlText: - return new Lazy(() => doc.GetXmlText(OutputChannel.XmlText(handle))); + return doc.GetXmlText(OutputChannel.XmlText(handle)); case OutputType.Doc: - return new Lazy(() => doc.GetDoc(OutputChannel.Doc(handle))); + return doc.GetDoc(OutputChannel.Doc(handle)); default: - return new Lazy((object?)null); + return null; } } private T GetValue(OutputType expectedType) { - ThrowIfDisposed(); - - var resolvedValue = value.Value; - - if (resolvedValue is not T typed) + if (value is not T typed) { throw new YDotNetException($"Expected {expectedType}, got {Type}."); } diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index b8c55999..0b7dcb81 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -1,4 +1,3 @@ -using System.Reflection.Metadata; using YDotNet.Document.Events; using YDotNet.Document.Options; using YDotNet.Document.Transactions; diff --git a/YDotNet/Document/Types/Arrays/Array.cs b/YDotNet/Document/Types/Arrays/Array.cs index 4245f167..5899399d 100644 --- a/YDotNet/Document/Types/Arrays/Array.cs +++ b/YDotNet/Document/Types/Arrays/Array.cs @@ -65,7 +65,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) { var handle = ArrayChannel.Get(Handle, transaction.Handle, index); - return handle != nint.Zero ? new Output(handle, Doc, null) : null; + return handle != nint.Zero ? Output.CreateAndRelease(handle, Doc) : null; } /// diff --git a/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs b/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs index 127bd2ca..26985ed7 100644 --- a/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs +++ b/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs @@ -35,10 +35,16 @@ public bool MoveNext() { var handle = ArrayChannel.IteratorNext(Iterator.Handle); - // The output has now owner and can just be disposed when not needed anymore. - current = handle != nint.Zero ? new Output(handle, Iterator.Doc, null) : null; - - return current != null; + if (handle != nint.Zero) + { + current = Output.CreateAndRelease(handle, Iterator.Doc); + return true; + } + else + { + current = null!; + return false; + } } /// diff --git a/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs b/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs index 11213144..e8a2806c 100644 --- a/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs +++ b/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs @@ -22,7 +22,7 @@ internal ArrayEvent(nint handle, Doc doc) var pathHandle = ArrayChannel.ObserveEventPath(handle, out var length).Checked(); - return new EventPath(pathHandle, length, this); + return new EventPath(pathHandle, length); }); delta = new Lazy(() => @@ -31,7 +31,7 @@ internal ArrayEvent(nint handle, Doc doc) var deltaHandle = ArrayChannel.ObserveEventDelta(handle, out var length).Checked(); - return new EventChanges(deltaHandle, length, doc, this); + return new EventChanges(deltaHandle, length, doc); }); target = new Lazy(() => diff --git a/YDotNet/Document/Types/Events/EventChange.cs b/YDotNet/Document/Types/Events/EventChange.cs index 00fd36e8..b1d2a2e0 100644 --- a/YDotNet/Document/Types/Events/EventChange.cs +++ b/YDotNet/Document/Types/Events/EventChange.cs @@ -1,4 +1,5 @@ using YDotNet.Document.Cells; +using YDotNet.Native.Types.Events; namespace YDotNet.Document.Types.Events; @@ -7,20 +8,19 @@ namespace YDotNet.Document.Types.Events; /// public class EventChange { - /// - /// Initializes a new instance of the class. - /// - /// The type of change represented by this instance. - /// The amount of elements affected by the current change. - /// - /// Optional, the values affected by the current change if is - /// . - /// - public EventChange(EventChangeTag tag, uint length, List values) + internal EventChange(EventChangeNative native, Doc doc) { - Tag = tag; - Length = length; - Values = values; + Length = native.Length; + + Tag = native.TagNative switch + { + EventChangeTagNative.Add => EventChangeTag.Add, + EventChangeTagNative.Remove => EventChangeTag.Remove, + EventChangeTagNative.Retain => EventChangeTag.Retain, + _ => throw new NotSupportedException($"The value \"{native.TagNative}\" for {nameof(EventChangeTagNative)} is not supported."), + }; + + Values = native.ValuesHandles.Select(x => new Output(x, doc)).ToList(); } /// diff --git a/YDotNet/Document/Types/Events/EventChanges.cs b/YDotNet/Document/Types/Events/EventChanges.cs index 612295ad..00b3ee3b 100644 --- a/YDotNet/Document/Types/Events/EventChanges.cs +++ b/YDotNet/Document/Types/Events/EventChanges.cs @@ -1,4 +1,4 @@ -using System.Runtime.InteropServices; +using System.Collections.ObjectModel; using YDotNet.Infrastructure; using YDotNet.Native.Types; using YDotNet.Native.Types.Events; @@ -8,33 +8,25 @@ namespace YDotNet.Document.Types.Events; /// /// Represents a collection of instances. /// -public class EventChanges : UnmanagedCollectionResource +public class EventChanges : ReadOnlyCollection { - private readonly uint length; + internal EventChanges(nint handle, uint length, Doc doc) + : base(ReadItems(handle, length, doc)) + { + } - internal EventChanges(nint handle, uint length, Doc doc, IResourceOwner owner) - : base(handle, owner) + private static IList ReadItems(nint handle, uint length, Doc doc) { - foreach (var itemHandle in MemoryReader.ReadIntPtrArray(handle, length, Marshal.SizeOf())) + var result = new List(); + + foreach (var native in MemoryReader.ReadIntPtrArray(handle, length)) { - // The event delta creates output that are owned by this block of allocated memory. - AddItem(Marshal.PtrToStructure(itemHandle).ToEventChange(this, doc)); + result.Add(new EventChange(native.Value, doc)); } - this.length = length; - } - - /// - /// Finalizes an instance of the class. - /// - ~EventChanges() - { - Dispose(true); - } + // We are done reading and can release the resource. + EventChannel.DeltaDestroy(handle, length); - /// - protected internal override void DisposeCore(bool disposing) - { - EventChannel.DeltaDestroy(Handle, length); + return result; } } diff --git a/YDotNet/Document/Types/Events/EventDelta.cs b/YDotNet/Document/Types/Events/EventDelta.cs index d4e85007..0cd25fa7 100644 --- a/YDotNet/Document/Types/Events/EventDelta.cs +++ b/YDotNet/Document/Types/Events/EventDelta.cs @@ -1,4 +1,5 @@ using YDotNet.Document.Cells; +using YDotNet.Native.Types.Events; namespace YDotNet.Document.Types.Events; @@ -7,22 +8,26 @@ namespace YDotNet.Document.Types.Events; /// public class EventDelta { - /// - /// Initializes a new instance of the class. - /// - /// The type of change represented by this instance. - /// The amount of changes represented by this instance. - /// - /// The value that was added, this property is if is - /// . - /// - /// The attributes that are part of the changed content. - public EventDelta(EventDeltaTag tag, uint length, Output? insert, List attributes) + internal EventDelta(EventDeltaNative native, Doc doc) { - Tag = tag; - Length = length; - Insert = insert; - Attributes = attributes; + Length = native.Length; + + Tag = native.TagNative switch + { + EventDeltaTagNative.Add => EventDeltaTag.Add, + EventDeltaTagNative.Remove => EventDeltaTag.Remove, + EventDeltaTagNative.Retain => EventDeltaTag.Retain, + _ => throw new NotSupportedException($"The value \"{native.TagNative}\" for {nameof(EventDeltaTagNative)} is not supported."), + }; + + Attributes = native.Attributes.ToDictionary( + x => x.Value.Key(), + x => new Output(x.Value.ValueHandle(x.Handle), doc)); + + if (native.InsertHandle != nint.Zero) + { + Insert = new Output(native.InsertHandle, doc); + } } /// @@ -43,5 +48,5 @@ public EventDelta(EventDeltaTag tag, uint length, Output? insert, List /// Gets the attributes that are part of the changed content. /// - public IReadOnlyList Attributes { get; } + public IReadOnlyDictionary Attributes { get; } } diff --git a/YDotNet/Document/Types/Events/EventDeltaAttribute.cs b/YDotNet/Document/Types/Events/EventDeltaAttribute.cs index 08a052d9..6a396885 100644 --- a/YDotNet/Document/Types/Events/EventDeltaAttribute.cs +++ b/YDotNet/Document/Types/Events/EventDeltaAttribute.cs @@ -11,7 +11,7 @@ public sealed class EventDeltaAttribute { private readonly Lazy value; - internal EventDeltaAttribute(nint handle, Doc doc, IResourceOwner owner) + internal EventDeltaAttribute(nint handle, Doc doc) { Handle = handle; @@ -20,7 +20,7 @@ internal EventDeltaAttribute(nint handle, Doc doc, IResourceOwner owner) value = new Lazy(() => { - return new Output(handle + MemoryConstants.PointerSize, doc, owner); + return new Output(handle + MemoryConstants.PointerSize, doc); }); } diff --git a/YDotNet/Document/Types/Events/EventDeltas.cs b/YDotNet/Document/Types/Events/EventDeltas.cs index 847e1ce0..be30f676 100644 --- a/YDotNet/Document/Types/Events/EventDeltas.cs +++ b/YDotNet/Document/Types/Events/EventDeltas.cs @@ -1,4 +1,4 @@ -using System.Runtime.InteropServices; +using System.Collections.ObjectModel; using YDotNet.Infrastructure; using YDotNet.Native.Types; using YDotNet.Native.Types.Events; @@ -8,33 +8,25 @@ namespace YDotNet.Document.Types.Events; /// /// Represents a collection of instances. /// -public class EventDeltas : UnmanagedCollectionResource +public class EventDeltas : ReadOnlyCollection { - private readonly uint length; + internal EventDeltas(nint handle, uint length, Doc doc) + : base(ReadItems(handle, length, doc)) + { + } - internal EventDeltas(nint handle, uint length, Doc doc, IResourceOwner owner) - : base(handle, owner) + private static IList ReadItems(nint handle, uint length, Doc doc) { - this.length = length; + var result = new List((int)length); - foreach (var itemHandle in MemoryReader.ReadIntPtrArray(handle, length, Marshal.SizeOf())) + foreach (var native in MemoryReader.ReadIntPtrArray(handle, length)) { - // The event delta creates output that are owned by this block of allocated memory. - AddItem(Marshal.PtrToStructure(itemHandle).ToEventDelta(doc, this)); + result.Add(new EventDelta(native.Value, doc)); } - } - /// - /// Finalizes an instance of the class. - /// - ~EventDeltas() - { - Dispose(false); - } + // We are done reading and can release the memory. + EventChannel.DeltaDestroy(handle, length); - /// - protected internal override void DisposeCore(bool disposing) - { - EventChannel.DeltaDestroy(Handle, length); + return result; } } diff --git a/YDotNet/Document/Types/Events/EventKeys.cs b/YDotNet/Document/Types/Events/EventKeys.cs index 17852931..d25f715c 100644 --- a/YDotNet/Document/Types/Events/EventKeys.cs +++ b/YDotNet/Document/Types/Events/EventKeys.cs @@ -1,3 +1,4 @@ +using System.Collections.ObjectModel; using System.Runtime.InteropServices; using YDotNet.Infrastructure; using YDotNet.Native.Types; @@ -6,36 +7,28 @@ namespace YDotNet.Document.Types.Events; /// -/// Represents the keys that changed the shared type that emitted the event related to this -/// instance. +/// Represents the keys that changed the shared type that emitted the event related to this instance. /// -public class EventKeys : UnmanagedCollectionResource +public class EventKeys : ReadOnlyCollection { - private readonly uint length; + internal EventKeys(nint handle, uint length, Doc doc) + : base(ReadItems(handle, length, doc)) + { + + } - internal EventKeys(nint handle, uint length, Doc doc, IResourceOwner owner) - : base(handle, owner) + private static IList ReadItems(nint handle, uint length, Doc doc) { + var result = new List(); + foreach (var keyHandle in MemoryReader.ReadIntPtrArray(handle, length, Marshal.SizeOf())) { - // The event delta creates output that are owned by this block of allocated memory. - AddItem(Marshal.PtrToStructure(keyHandle).ToEventKeyChange(doc, owner)); + result.Add(Marshal.PtrToStructure(keyHandle).ToEventKeyChange(doc)); } - this.length = length; - } + // We are done reading and can destroy the resource. + EventChannel.KeysDestroy(handle, length); - /// - /// Finalizes an instance of the class. - /// - ~EventKeys() - { - Dispose(true); - } - - /// - protected internal override void DisposeCore(bool disposing) - { - EventChannel.KeysDestroy(Handle, length); + return result; } } diff --git a/YDotNet/Document/Types/Events/EventPath.cs b/YDotNet/Document/Types/Events/EventPath.cs index 3ba5d573..a2da9d64 100644 --- a/YDotNet/Document/Types/Events/EventPath.cs +++ b/YDotNet/Document/Types/Events/EventPath.cs @@ -1,3 +1,4 @@ +using System.Collections.ObjectModel; using YDotNet.Infrastructure; using YDotNet.Native.Types; @@ -7,24 +8,25 @@ namespace YDotNet.Document.Types.Events; /// Represents the path from the root type to the shared type that emitted the event related to this /// instance. /// -public sealed class EventPath : UnmanagedCollectionResource +public sealed class EventPath : ReadOnlyCollection { - internal EventPath(nint handle, uint length, IResourceOwner owner) - : base(handle, owner) + internal EventPath(nint handle, uint length) + : base(ReadItems(handle, length)) { - foreach (var itemHandle in MemoryReader.ReadIntPtrArray(Handle, length, size: 16)) + } + + private static IList ReadItems(nint handle, uint length) + { + var result = new List(); + + foreach (var itemHandle in MemoryReader.ReadIntPtrArray(handle, length, size: 16)) { - // The segment does not make any further allocations. - AddItem(new EventPathSegment(itemHandle)); + result.Add(new EventPathSegment(itemHandle)); } // We have read everything, so we can release the memory immediately. - PathChannel.Destroy(Handle, length); - } + PathChannel.Destroy(handle, length); - /// - protected internal override void DisposeCore(bool disposing) - { - // We have read everything in the constructor, therefore there is no unmanaged memory to be released. + return result; } } diff --git a/YDotNet/Document/Types/Maps/Events/MapEvent.cs b/YDotNet/Document/Types/Maps/Events/MapEvent.cs index 9f10b45e..3c3cd1be 100644 --- a/YDotNet/Document/Types/Maps/Events/MapEvent.cs +++ b/YDotNet/Document/Types/Maps/Events/MapEvent.cs @@ -21,14 +21,14 @@ internal MapEvent(nint handle, Doc doc) { var pathHandle = ArrayChannel.ObserveEventPath(handle, out var length).Checked(); - return new EventPath(pathHandle, length, this); + return new EventPath(pathHandle, length); }); keys = new Lazy(() => { var keysHandle = MapChannel.ObserveEventKeys(handle, out var length).Checked(); - return new EventKeys(keysHandle, length, doc, this); + return new EventKeys(keysHandle, length, doc); }); target = new Lazy(() => diff --git a/YDotNet/Document/Types/Maps/Map.cs b/YDotNet/Document/Types/Maps/Map.cs index 57fdc318..f28809ba 100644 --- a/YDotNet/Document/Types/Maps/Map.cs +++ b/YDotNet/Document/Types/Maps/Map.cs @@ -49,14 +49,10 @@ public void Insert(Transaction transaction, string key, Input input) public Output? Get(Transaction transaction, string key) { using var unsafeName = MemoryWriter.WriteUtf8String(key); - var outputHandle = MapChannel.Get(Handle, transaction.Handle, unsafeName.Handle); - if (outputHandle == nint.Zero) - { - return null; - } + var handle = MapChannel.Get(Handle, transaction.Handle, unsafeName.Handle); - return new Output(outputHandle, Doc, null); + return handle != nint.Zero ? Output.CreateAndRelease(handle, Doc) : null; } /// diff --git a/YDotNet/Document/Types/Maps/MapEntry.cs b/YDotNet/Document/Types/Maps/MapEntry.cs deleted file mode 100644 index bdf8e9e6..00000000 --- a/YDotNet/Document/Types/Maps/MapEntry.cs +++ /dev/null @@ -1,45 +0,0 @@ -using YDotNet.Document.Cells; -using YDotNet.Infrastructure; -using YDotNet.Native.Types.Maps; - -namespace YDotNet.Document.Types.Maps; - -/// -/// Represents an entry of a . -/// -public sealed class MapEntry : UnmanagedResource -{ - internal MapEntry(nint handle, Doc doc, IResourceOwner? owner) - : base(handle) - { - var (mapEntry, outputHandle) = MemoryReader.ReadMapEntryAndOutputHandle(handle); - Key = MemoryReader.ReadUtf8String(mapEntry.Field); - - // The output can not released independently and will be released when the entry is not needed anymore. - Value = new Output(outputHandle, doc, owner ?? this); - } - - /// - /// Finalizes an instance of the class. - /// - ~MapEntry() - { - Dispose(false); - } - - /// - protected internal override void DisposeCore(bool disposing) - { - MapChannel.EntryDestroy(Handle); - } - - /// - /// Gets the key of this that represents the . - /// - public string Key { get; } - - /// - /// Gets the value of this that is represented the . - /// - public Output Value { get; } -} diff --git a/YDotNet/Document/Types/Maps/MapEnumerator.cs b/YDotNet/Document/Types/Maps/MapEnumerator.cs index f5d1e542..ef9aa63f 100644 --- a/YDotNet/Document/Types/Maps/MapEnumerator.cs +++ b/YDotNet/Document/Types/Maps/MapEnumerator.cs @@ -1,4 +1,6 @@ using System.Collections; +using YDotNet.Document.Cells; +using YDotNet.Infrastructure; using YDotNet.Native.Types.Maps; namespace YDotNet.Document.Types.Maps; @@ -6,9 +8,9 @@ namespace YDotNet.Document.Types.Maps; /// /// Represents the iterator to provide instances of . /// -internal class MapEnumerator : IEnumerator +internal class MapEnumerator : IEnumerator> { - private MapEntry? current; + private KeyValuePair current; internal MapEnumerator(MapIterator iterator) { @@ -21,7 +23,7 @@ public void Dispose() } /// - public MapEntry Current => current!; + public KeyValuePair Current => current; /// object? IEnumerator.Current => current!; @@ -33,10 +35,24 @@ public bool MoveNext() { var handle = MapChannel.IteratorNext(Iterator.Handle); - // The map entry also manages the value of the output. - current = handle != nint.Zero ? new MapEntry(handle, Iterator.Doc, Iterator) : null; + if (handle != nint.Zero) + { + var native = MemoryReader.PtrToStruct(handle); - return current != null; + current = new KeyValuePair( + native.Key(), + new Output(native.ValueHandle(handle), Iterator.Doc)); + + // We are done reading, so we can release the memory. + MapChannel.EntryDestroy(handle); + + return true; + } + else + { + current = default; + return false; + } } /// diff --git a/YDotNet/Document/Types/Maps/MapIterator.cs b/YDotNet/Document/Types/Maps/MapIterator.cs index 278e15a0..18ce54f6 100644 --- a/YDotNet/Document/Types/Maps/MapIterator.cs +++ b/YDotNet/Document/Types/Maps/MapIterator.cs @@ -1,4 +1,5 @@ using System.Collections; +using YDotNet.Document.Cells; using YDotNet.Infrastructure; using YDotNet.Native.Types.Maps; @@ -17,7 +18,7 @@ namespace YDotNet.Document.Types.Maps; /// /// /// -public class MapIterator : UnmanagedResource, IEnumerable +public class MapIterator : UnmanagedResource, IEnumerable> { internal MapIterator(nint handle, Doc doc) : base(handle) @@ -42,7 +43,7 @@ protected internal override void DisposeCore(bool disposing) internal Doc Doc { get; } /// - public IEnumerator GetEnumerator() + public IEnumerator> GetEnumerator() { ThrowIfDisposed(); return new MapEnumerator(this); diff --git a/YDotNet/Document/Types/Texts/Events/TextEvent.cs b/YDotNet/Document/Types/Texts/Events/TextEvent.cs index 6e86d33d..be1dacf9 100644 --- a/YDotNet/Document/Types/Texts/Events/TextEvent.cs +++ b/YDotNet/Document/Types/Texts/Events/TextEvent.cs @@ -22,7 +22,7 @@ internal TextEvent(nint handle, Doc doc) var pathHandle = TextChannel.ObserveEventPath(handle, out var length).Checked(); - return new EventPath(pathHandle, length, this); + return new EventPath(pathHandle, length); }); deltas = new Lazy(() => @@ -31,7 +31,7 @@ internal TextEvent(nint handle, Doc doc) var deltaHandle = TextChannel.ObserveEventDelta(handle, out var length).Checked(); - return new EventDeltas(deltaHandle, length, doc, this); + return new EventDeltas(deltaHandle, length, doc); }); target = new Lazy(() => diff --git a/YDotNet/Document/Types/Texts/TextChunk.cs b/YDotNet/Document/Types/Texts/TextChunk.cs index c5432d11..783e45de 100644 --- a/YDotNet/Document/Types/Texts/TextChunk.cs +++ b/YDotNet/Document/Types/Texts/TextChunk.cs @@ -1,9 +1,6 @@ -using System.Runtime.InteropServices; using YDotNet.Document.Cells; -using YDotNet.Document.Types.Maps; -using YDotNet.Infrastructure; -using YDotNet.Native.Cells.Outputs; -using YDotNet.Native.Types.Maps; +using YDotNet.Native; +using YDotNet.Native.Types.Texts; namespace YDotNet.Document.Types.Texts; @@ -12,36 +9,25 @@ namespace YDotNet.Document.Types.Texts; /// public class TextChunk { - internal TextChunk(nint handle, Doc doc, IResourceOwner owner) + internal TextChunk(NativeWithHandle native, Doc doc) { - Data = new Output(handle, doc, owner); + Data = new Output(native.Handle, doc); - var offset = Marshal.SizeOf(); - - var attributesLength = (uint)Marshal.ReadInt32(handle + offset); - var attributesHandle = Marshal.ReadIntPtr(handle + offset + MemoryConstants.PointerSize); - - if (attributesHandle == nint.Zero) - { - Attributes = new List(); - return; - } - - Attributes = MemoryReader.ReadIntPtrArray( - attributesHandle, - attributesLength, - Marshal.SizeOf()) - .Select(x => new MapEntry(x, doc, owner)).ToList(); + Attributes = native.Value.Attributes().ToDictionary( + x => x.Value.Key(), + x => new Output(x.Value.ValueHandle(x.Handle), doc)); } /// /// Gets the piece of formatted using the same attributes. - /// It can be a string, embedded object or another shared type. /// + /// + /// It can be a string, embedded object or another shared type. + /// public Output Data { get; } /// /// Gets the formatting attributes applied to the . /// - public IReadOnlyList Attributes { get; } + public IReadOnlyDictionary Attributes { get; } } diff --git a/YDotNet/Document/Types/Texts/TextChunks.cs b/YDotNet/Document/Types/Texts/TextChunks.cs index ac309d62..3a37b548 100644 --- a/YDotNet/Document/Types/Texts/TextChunks.cs +++ b/YDotNet/Document/Types/Texts/TextChunks.cs @@ -1,43 +1,33 @@ +using System.Collections.ObjectModel; using YDotNet.Infrastructure; using YDotNet.Native.Types; +using YDotNet.Native.Types.Texts; namespace YDotNet.Document.Types.Texts; /// /// Represents a collection of instances. /// -public class TextChunks : UnmanagedCollectionResource +public class TextChunks : ReadOnlyCollection { - private readonly uint length; internal TextChunks(nint handle, uint length, Doc doc) - : base(handle, null) + : base(ReadItems(handle, length, doc)) { - foreach (var chunkHandle in MemoryReader.ReadIntPtrArray(handle, length, size: 32)) - { - // The cunks create output that are owned by this block of allocated memory. - AddItem(new TextChunk(chunkHandle, doc, this)); - } - - GC.Collect(); - - this.length = length; } - /// - /// Finalizes an instance of the class. - /// - ~TextChunks() + private static IList ReadItems(nint handle, uint length, Doc doc) { - Dispose(true); - } + var result = new List((int)length); - /// - protected internal override void DisposeCore(bool disposing) - { - Console.WriteLine("---DISPOSE {0} - {1}", Handle, length); - Console.Out.Flush(); - Thread.Sleep(100); - ChunksChannel.Destroy(Handle, length); + foreach (var native in MemoryReader.ReadIntPtrArray(handle, length)) + { + result.Add(new TextChunk(native, doc)); + } + + // We are done reading and can release the memory. + ChunksChannel.Destroy(handle, length); + + return result; } } diff --git a/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs b/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs index 6f627187..165f93f3 100644 --- a/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs +++ b/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs @@ -21,21 +21,21 @@ internal XmlElementEvent(nint handle, Doc doc) { var pathHandle = XmlElementChannel.ObserveEventPath(handle, out var length).Checked(); - return new EventPath(pathHandle, length, this); + return new EventPath(pathHandle, length); }); delta = new Lazy(() => { var deltaHandle = XmlElementChannel.ObserveEventDelta(handle, out var length).Checked(); - return new EventChanges(deltaHandle, length, doc, this); + return new EventChanges(deltaHandle, length, doc); }); keys = new Lazy(() => { var keysHandle = XmlElementChannel.ObserveEventKeys(handle, out var length).Checked(); - return new EventKeys(keysHandle, length, doc, this); + return new EventKeys(keysHandle, length, doc); }); target = new Lazy(() => diff --git a/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs b/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs index 764d7605..faf3929b 100644 --- a/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs +++ b/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs @@ -42,9 +42,16 @@ public bool MoveNext() { var handle = XmlElementChannel.TreeWalkerNext(TreeWalker.Handle); - current = handle != nint.Zero ? new Output(handle, TreeWalker.Doc, null) : null; - - return Current != null; + if (handle != nint.Zero) + { + current = Output.CreateAndRelease(handle, TreeWalker.Doc); + return true; + } + else + { + current = null!; + return false; + } } /// diff --git a/YDotNet/Document/Types/XmlElements/XmlElement.cs b/YDotNet/Document/Types/XmlElements/XmlElement.cs index 595f7af4..7a62eb5e 100644 --- a/YDotNet/Document/Types/XmlElements/XmlElement.cs +++ b/YDotNet/Document/Types/XmlElements/XmlElement.cs @@ -6,6 +6,7 @@ using YDotNet.Document.Types.XmlElements.Trees; using YDotNet.Document.Types.XmlTexts; using YDotNet.Infrastructure; +using YDotNet.Native.Cells.Outputs; using YDotNet.Native.Types; namespace YDotNet.Document.Types.XmlElements; @@ -177,7 +178,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) { var handle = XmlElementChannel.Get(Handle, transaction.Handle, index); - return handle != nint.Zero ? new Output(handle, Doc, null) : null; + return handle != nint.Zero ? new Output(handle, Doc) : null; } /// @@ -194,7 +195,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) { var handle = XmlChannel.PreviousSibling(Handle, transaction.Handle); - return handle != nint.Zero ? new Output(handle, Doc, null) : null; + return handle != nint.Zero ? Output.CreateAndRelease(handle, Doc) : null; } /// @@ -211,7 +212,16 @@ public void RemoveRange(Transaction transaction, uint index, uint length) { var handle = XmlChannel.NextSibling(Handle, transaction.Handle); - return handle != nint.Zero ? new Output(handle, Doc, null) : null; + if (handle == nint.Zero) + { + return null; + } + + // The output reads everything so we can just destroy it. + var result = new Output(handle, Doc); + OutputChannel.Destroy(handle); + + return result; } /// @@ -227,7 +237,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) { var handle = XmlElementChannel.FirstChild(Handle, transaction.Handle); - return handle != nint.Zero ? new Output(handle, Doc, null) : null; + return handle != nint.Zero ? Output.CreateAndRelease(handle, Doc) : null; } /// diff --git a/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs b/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs index 1e23f2db..e6055db0 100644 --- a/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs +++ b/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs @@ -20,14 +20,14 @@ internal XmlTextEvent(nint handle, Doc doc) { var deltaHandle = XmlTextChannel.ObserveEventDelta(handle, out var length).Checked(); - return new EventDeltas(deltaHandle, length, doc, this); + return new EventDeltas(deltaHandle, length, doc); }); keys = new Lazy(() => { var keysHandle = XmlTextChannel.ObserveEventKeys(handle, out var length).Checked(); - return new EventKeys(keysHandle, length, doc, this); + return new EventKeys(keysHandle, length, doc); }); target = new Lazy(() => diff --git a/YDotNet/Document/Types/XmlTexts/XmlText.cs b/YDotNet/Document/Types/XmlTexts/XmlText.cs index 62a2ae9c..b3740618 100644 --- a/YDotNet/Document/Types/XmlTexts/XmlText.cs +++ b/YDotNet/Document/Types/XmlTexts/XmlText.cs @@ -184,7 +184,7 @@ public void Format(Transaction transaction, uint index, uint length, Input attri { var handle = XmlChannel.PreviousSibling(Handle, transaction.Handle); - return handle != nint.Zero ? new Output(handle, Doc, null) : null; + return handle != nint.Zero ? Output.CreateAndRelease(handle, Doc) : null; } /// @@ -200,7 +200,7 @@ public void Format(Transaction transaction, uint index, uint length, Input attri { var handle = XmlChannel.NextSibling(Handle, transaction.Handle); - return handle != nint.Zero ? new Output(handle, Doc, null) : null; + return handle != nint.Zero ? Output.CreateAndRelease(handle, Doc) : null; } /// diff --git a/YDotNet/Infrastructure/IResourceOwner.cs b/YDotNet/Infrastructure/IResourceOwner.cs deleted file mode 100644 index 25bdded9..00000000 --- a/YDotNet/Infrastructure/IResourceOwner.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace YDotNet.Infrastructure; - -/// -/// Marker interface for resource owners. -/// -public interface IResourceOwner -{ - /// - /// Gets a value indicating whether this instance is disposed. - /// - bool IsDisposed { get; } -} diff --git a/YDotNet/Infrastructure/MemoryReader.cs b/YDotNet/Infrastructure/MemoryReader.cs index 53555cde..a761a137 100644 --- a/YDotNet/Infrastructure/MemoryReader.cs +++ b/YDotNet/Infrastructure/MemoryReader.cs @@ -1,9 +1,7 @@ using System.Runtime.InteropServices; using System.Text; -using YDotNet.Document.State; -using YDotNet.Native.Document.State; +using YDotNet.Native; using YDotNet.Native.Types; -using YDotNet.Native.Types.Maps; namespace YDotNet.Infrastructure; @@ -43,27 +41,39 @@ internal static T[] ReadStructArray(nint handle, uint length) return handle == nint.Zero ? null : ReadBytes(handle, length); } - internal static nint[] ReadIntPtrArray(nint handle, uint length, int size) + internal static IEnumerable ReadIntPtrArray(nint handle, uint length, int size) { - var result = new nint[length]; + var itemSize = size; - for (var i = 0; i < result.Length; i++) + for (var i = 0; i < length; i++) { - var output = handle + i * size; - result[i] = output; + yield return handle; + + handle += itemSize; } + } - return result; + internal static IEnumerable> ReadIntPtrArray(nint handle, uint length) where T : struct + { + var itemSize = Marshal.SizeOf(); + + for (var i = 0; i < length; i++) + { + yield return new NativeWithHandle(Marshal.PtrToStructure(handle), handle); + + handle += itemSize; + } } internal static nint[]? TryReadIntPtrArray(nint handle, uint length, int size) { - return handle == nint.Zero ? null : ReadIntPtrArray(handle, length, size); + return handle == nint.Zero ? null : ReadIntPtrArray(handle, length, size).ToArray(); } - internal static (MapEntryNative MapEntryNative, nint OutputHandle) ReadMapEntryAndOutputHandle(nint handle) + internal static T PtrToStruct(nint handle) + where T : struct { - return (Marshal.PtrToStructure(handle), handle + MemoryConstants.PointerSize); + return Marshal.PtrToStructure(handle.Checked()); } internal static string ReadUtf8String(nint handle) diff --git a/YDotNet/Infrastructure/Resource.cs b/YDotNet/Infrastructure/Resource.cs index 13dd39fa..eedf7449 100644 --- a/YDotNet/Infrastructure/Resource.cs +++ b/YDotNet/Infrastructure/Resource.cs @@ -3,20 +3,12 @@ namespace YDotNet.Infrastructure; /// /// Base class for all resoruces. /// -public abstract class Resource : IResourceOwner, IDisposable +public abstract class Resource : IDisposable { - internal Resource(IResourceOwner? owner = null) - { - Owner = owner; - } - - /// - public bool IsDisposed { get; private set; } - /// - /// Gets the owner of this resource. + /// Gets a value indicating whether this instance is disposed. /// - public IResourceOwner? Owner { get; } + public bool IsDisposed { get; private set; } /// public void Dispose() @@ -31,7 +23,7 @@ public void Dispose() /// Object or the owner has been disposed. protected void ThrowIfDisposed() { - if (IsDisposed || Owner?.IsDisposed == true) + if (IsDisposed) { throw new ObjectDisposedException(GetType().Name); } diff --git a/YDotNet/Infrastructure/TypeBase.cs b/YDotNet/Infrastructure/TypeBase.cs new file mode 100644 index 00000000..ebe657da --- /dev/null +++ b/YDotNet/Infrastructure/TypeBase.cs @@ -0,0 +1,24 @@ +namespace YDotNet.Infrastructure; + +public abstract class TypeBase : ITypeBase +{ + private bool isDeleted; + + protected void ThrowIfDeleted() + { + + } + + public void MarkDeleted() + { + throw new NotImplementedException(); + } +} + +public interface ITypeBase +{ + /// + /// Marks the object as deleted to stop all further calls. + /// + void MarkDeleted(); +} diff --git a/YDotNet/Infrastructure/TypeCache.cs b/YDotNet/Infrastructure/TypeCache.cs new file mode 100644 index 00000000..9bbebbb5 --- /dev/null +++ b/YDotNet/Infrastructure/TypeCache.cs @@ -0,0 +1,40 @@ +namespace YDotNet.Infrastructure; + +internal class TypeCache +{ + private readonly Dictionary cache = new Dictionary(); + + public T GetOrAdd(nint handle, Func factory) + where T : ITypeBase + { + if (handle == nint.Zero) + { + ThrowHelper.ArgumentException("Cannot create object for null handle.", nameof(handle)); + } + + if (cache.TryGetValue(handle, out var item)) + { + if (item is not T typed) + { + ThrowHelper.YDotnet($"Expected {typeof(T)}, got {item.GetType()}"); + return default!; + } + + return typed; + } + + var typedItem = factory(handle); + cache[handle] = typedItem; + + return typedItem; + } + + public void Delete(nint handle) + { + if (cache.TryGetValue(handle, out var item)) + { + item.MarkDeleted(); + cache.Remove(handle); + } + } +} diff --git a/YDotNet/Infrastructure/UnmanagedCollectionResource.cs b/YDotNet/Infrastructure/UnmanagedCollectionResource.cs deleted file mode 100644 index bbb567fd..00000000 --- a/YDotNet/Infrastructure/UnmanagedCollectionResource.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Collections; - -namespace YDotNet.Infrastructure; - -/// -/// Base class for resources that represent a collection. -/// -/// The type of item. -public abstract class UnmanagedCollectionResource : UnmanagedResource, IReadOnlyList -{ - private readonly List items = new(); - - internal UnmanagedCollectionResource(nint handle, IResourceOwner? owner) - : base(handle, owner) - { - } - - /// - public T this[int index] => items[index]; - - /// - public int Count => items.Count; - - /// - /// Adds a new item to the collection. - /// - /// The item to add. - protected internal void AddItem(T item) - { - items.Add(item); - } - - /// - public IEnumerator GetEnumerator() - { - return items.GetEnumerator(); - } - - /// - IEnumerator IEnumerable.GetEnumerator() - { - return items.GetEnumerator(); - } -} diff --git a/YDotNet/Infrastructure/UnmanagedResource.cs b/YDotNet/Infrastructure/UnmanagedResource.cs index 9b0826b3..dea17ab7 100644 --- a/YDotNet/Infrastructure/UnmanagedResource.cs +++ b/YDotNet/Infrastructure/UnmanagedResource.cs @@ -5,8 +5,7 @@ namespace YDotNet.Infrastructure; /// public abstract class UnmanagedResource : Resource { - internal UnmanagedResource(nint handle, IResourceOwner? owner = null) - : base(owner) + internal UnmanagedResource(nint handle) { Handle = handle; } diff --git a/YDotNet/Native/Document/Events/ClearEventNative.cs b/YDotNet/Native/Document/Events/ClearEventNative.cs index 147df4da..0c13e901 100644 --- a/YDotNet/Native/Document/Events/ClearEventNative.cs +++ b/YDotNet/Native/Document/Events/ClearEventNative.cs @@ -11,10 +11,7 @@ internal readonly struct ClearEventNative public static ClearEventNative From(Doc doc) { - return new ClearEventNative - { - Doc = doc - }; + return new ClearEventNative { Doc = doc }; } public ClearEvent ToClearEvent() diff --git a/YDotNet/Native/Document/Events/UpdateEventNative.cs b/YDotNet/Native/Document/Events/UpdateEventNative.cs index ca6ba08b..f4dc2b63 100644 --- a/YDotNet/Native/Document/Events/UpdateEventNative.cs +++ b/YDotNet/Native/Document/Events/UpdateEventNative.cs @@ -1,4 +1,3 @@ -using System.ComponentModel.DataAnnotations; using System.Runtime.InteropServices; using YDotNet.Document.Events; using YDotNet.Infrastructure; diff --git a/YDotNet/Native/Document/State/StateVectorNative.cs b/YDotNet/Native/Document/State/StateVectorNative.cs index 4af4b92a..5df91373 100644 --- a/YDotNet/Native/Document/State/StateVectorNative.cs +++ b/YDotNet/Native/Document/State/StateVectorNative.cs @@ -1,4 +1,3 @@ -using System.Runtime.InteropServices; using YDotNet.Document.State; using YDotNet.Infrastructure; diff --git a/YDotNet/Native/NativeWithHandle.cs b/YDotNet/Native/NativeWithHandle.cs new file mode 100644 index 00000000..c6061420 --- /dev/null +++ b/YDotNet/Native/NativeWithHandle.cs @@ -0,0 +1,3 @@ +namespace YDotNet.Native; + +internal record struct NativeWithHandle(T Value, nint Handle) where T : struct; diff --git a/YDotNet/Native/Types/Events/EventChangeNative.cs b/YDotNet/Native/Types/Events/EventChangeNative.cs index 970cc8fc..41187f3d 100644 --- a/YDotNet/Native/Types/Events/EventChangeNative.cs +++ b/YDotNet/Native/Types/Events/EventChangeNative.cs @@ -1,7 +1,4 @@ using System.Runtime.InteropServices; -using YDotNet.Document; -using YDotNet.Document.Cells; -using YDotNet.Document.Types.Events; using YDotNet.Infrastructure; using YDotNet.Native.Cells.Outputs; @@ -16,20 +13,16 @@ internal readonly struct EventChangeNative public nint Values { get; } - public EventChange ToEventChange(IResourceOwner owner, Doc doc) + public nint[] ValuesHandles { - var tag = TagNative switch + get { - EventChangeTagNative.Add => EventChangeTag.Add, - EventChangeTagNative.Remove => EventChangeTag.Remove, - EventChangeTagNative.Retain => EventChangeTag.Retain, - _ => throw new NotSupportedException($"The value \"{TagNative}\" for {nameof(EventChangeTagNative)} is not supported."), - }; + if (Values == nint.Zero) + { + return Array.Empty(); + } - var values = - MemoryReader.TryReadIntPtrArray(Values, Length, Marshal.SizeOf())? - .Select(x => new Output(x, doc, owner)).ToList() ?? new List(); - - return new EventChange(tag, Length, values); + return MemoryReader.ReadIntPtrArray(Values, Length, Marshal.SizeOf()).ToArray(); + } } } diff --git a/YDotNet/Native/Types/Events/EventDeltaNative.cs b/YDotNet/Native/Types/Events/EventDeltaNative.cs index a6776cf0..b31f8e5e 100644 --- a/YDotNet/Native/Types/Events/EventDeltaNative.cs +++ b/YDotNet/Native/Types/Events/EventDeltaNative.cs @@ -1,8 +1,6 @@ using System.Runtime.InteropServices; -using YDotNet.Document; -using YDotNet.Document.Cells; -using YDotNet.Document.Types.Events; using YDotNet.Infrastructure; +using YDotNet.Native.Types.Maps; namespace YDotNet.Native.Types.Events; @@ -17,28 +15,18 @@ internal readonly struct EventDeltaNative public uint AttributesLength { get; } - public nint Attributes { get; } + public nint AttributesHandle { get; } - public EventDelta ToEventDelta(Doc doc, IResourceOwner owner) + public NativeWithHandle[] Attributes { - var tag = TagNative switch + get { - EventDeltaTagNative.Add => EventDeltaTag.Add, - EventDeltaTagNative.Remove => EventDeltaTag.Remove, - EventDeltaTagNative.Retain => EventDeltaTag.Retain, - _ => throw new NotSupportedException($"The value \"{TagNative}\" for {nameof(EventDeltaTagNative)} is not supported."), - }; - - var attributes = - Attributes != nint.Zero ? - MemoryReader.ReadIntPtrArray(Attributes, AttributesLength, size: 16).Select(x => new EventDeltaAttribute(x, doc, owner)).ToList() : - new List(); - - var insert = - InsertHandle != nint.Zero ? - new Output(InsertHandle, doc, owner) : - null; - - return new EventDelta(tag, Length, insert, attributes); + if (AttributesHandle == nint.Zero || AttributesLength == 0) + { + return Array.Empty>(); + } + + return MemoryReader.ReadIntPtrArray(AttributesHandle, AttributesLength).ToArray(); + } } } diff --git a/YDotNet/Native/Types/Events/EventKeyChangeNative.cs b/YDotNet/Native/Types/Events/EventKeyChangeNative.cs index 73e0848b..130833e3 100644 --- a/YDotNet/Native/Types/Events/EventKeyChangeNative.cs +++ b/YDotNet/Native/Types/Events/EventKeyChangeNative.cs @@ -17,7 +17,7 @@ internal readonly struct EventKeyChangeNative public nint NewValue { get; } - public EventKeyChange ToEventKeyChange(Doc doc, IResourceOwner owner) + public EventKeyChange ToEventKeyChange(Doc doc) { var tag = TagNative switch { @@ -31,12 +31,12 @@ public EventKeyChange ToEventKeyChange(Doc doc, IResourceOwner owner) var oldOutput = OldValue != nint.Zero ? - new Output(OldValue, doc, owner) : + new Output(OldValue, doc) : null; var newOutput = NewValue != nint.Zero ? - new Output(NewValue, doc, owner) : + new Output(NewValue, doc) : null; return new EventKeyChange(key, tag, oldOutput, newOutput); diff --git a/YDotNet/Native/Types/Maps/MapEntryNative.cs b/YDotNet/Native/Types/Maps/MapEntryNative.cs index a0ad9ce9..07256b3c 100644 --- a/YDotNet/Native/Types/Maps/MapEntryNative.cs +++ b/YDotNet/Native/Types/Maps/MapEntryNative.cs @@ -1,10 +1,23 @@ using System.Runtime.InteropServices; +using YDotNet.Infrastructure; using YDotNet.Native.Cells.Outputs; namespace YDotNet.Native.Types.Maps; -[StructLayout(LayoutKind.Sequential, Size = OutputNative.Size + 8)] +[StructLayout(LayoutKind.Sequential, Size = Size)] internal readonly struct MapEntryNative { - public nint Field { get; } + internal const int Size = 8 + OutputNative.Size; + + public nint KeyHandle { get; } + + public nint ValueHandle(nint baseHandle) + { + return baseHandle + MemoryConstants.PointerSize; + } + + public string Key() + { + return MemoryReader.ReadUtf8String(KeyHandle); + } } diff --git a/YDotNet/Native/Types/Texts/TextChunkNative.cs b/YDotNet/Native/Types/Texts/TextChunkNative.cs new file mode 100644 index 00000000..61255c45 --- /dev/null +++ b/YDotNet/Native/Types/Texts/TextChunkNative.cs @@ -0,0 +1,31 @@ +using System.Runtime.InteropServices; +using YDotNet.Infrastructure; +using YDotNet.Native.Cells.Outputs; +using YDotNet.Native.Types.Maps; + +namespace YDotNet.Native.Types.Texts; + +[StructLayout(LayoutKind.Explicit, Size = Size)] +internal readonly struct TextChunkNative +{ + public const int Size = OutputNative.Size + 8 + 8; + + [field: FieldOffset(0)] + public nint Data { get; } + + [field: FieldOffset(OutputNative.Size)] + public uint AttributesLength { get; } + + [field: FieldOffset(OutputNative.Size + 8)] + public nint AttributesHandle { get; } + + public NativeWithHandle[] Attributes() + { + if (AttributesHandle == nint.Zero || AttributesLength == 0) + { + return Array.Empty>(); + } + + return MemoryReader.ReadIntPtrArray(AttributesHandle, AttributesLength).ToArray(); + } +} From af9ad3640e4271c6684fbb36b369102a98b104c2 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 12 Oct 2023 17:48:57 +0200 Subject: [PATCH 091/186] Refactoring. --- Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs | 4 +- .../YDotNet.Tests.Unit/Arrays/IterateTests.cs | 2 +- .../YDotNet.Tests.Unit/Arrays/ObserveTests.cs | 2 +- .../YDotNet.Tests.Unit/Document/CloneTests.cs | 22 -- .../Document/SubDocsTests.cs | 8 +- Tests/YDotNet.Tests.Unit/Maps/GetTests.cs | 28 +-- Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs | 2 +- .../Texts/InsertEmbedTests.cs | 4 +- .../UndoManagers/ObserveAddedTests.cs | 38 ++-- .../UndoManagers/ObservePoppedTests.cs | 52 ++--- .../UndoManagers/RedoTests.cs | 2 +- YDotNet/Document/Cells/Output.cs | 64 +++--- YDotNet/Document/Cells/OutputType.cs | 2 +- YDotNet/Document/Doc.cs | 199 +++++++++--------- .../Document/Events/AfterTransactionEvent.cs | 15 +- YDotNet/Document/Events/ClearEvent.cs | 6 +- YDotNet/Document/Events/EventPublisher.cs | 41 ++++ YDotNet/Document/Events/EventSubscriber.cs | 60 ++++++ YDotNet/Document/Events/EventSubscriptions.cs | 45 ---- YDotNet/Document/Events/SubDocsEvent.cs | 24 +-- YDotNet/Document/Events/UpdateEvent.cs | 10 +- YDotNet/Document/Options/DocOptions.cs | 40 +++- YDotNet/Document/State/DeleteSet.cs | 20 +- YDotNet/Document/State/IdRange.cs | 24 +-- YDotNet/Document/State/StateVector.cs | 20 +- YDotNet/Document/Types/Arrays/Array.cs | 24 +-- YDotNet/Document/Types/Branches/Branch.cs | 42 ++-- YDotNet/Document/Types/Events/EventBranch.cs | 86 ++++---- .../Types/Events/EventDeltaAttribute.cs | 41 ---- .../Document/Types/Events/EventKeyChange.cs | 33 +-- YDotNet/Document/Types/Events/EventKeys.cs | 6 +- YDotNet/Document/Types/Maps/Map.cs | 24 +-- YDotNet/Document/Types/Maps/MapEnumerator.cs | 2 +- YDotNet/Document/Types/Maps/MapIterator.cs | 4 +- YDotNet/Document/Types/Texts/Text.cs | 24 +-- YDotNet/Document/Types/Texts/TextChunks.cs | 1 - .../Document/Types/XmlElements/XmlElement.cs | 24 +-- YDotNet/Document/Types/XmlTexts/XmlText.cs | 24 +-- .../Document/UndoManagers/Events/UndoEvent.cs | 26 +-- YDotNet/Document/UndoManagers/UndoManager.cs | 54 ++--- .../UndoManagers/UndoManagerOptions.cs | 10 + YDotNet/Infrastructure/TypeBase.cs | 17 +- YDotNet/Native/Document/DocOptionsNative.cs | 20 -- .../Events/AfterTransactionEventNative.cs | 9 - .../Document/Events/ClearEventNative.cs | 21 -- .../Document/Events/EventBranchNative.cs | 18 ++ .../Document/Events/SubDocsEventNative.cs | 41 ++-- .../Document/Events/UpdateEventNative.cs | 11 +- .../Native/Document/State/DeleteSetNative.cs | 22 +- .../Native/Document/State/IdRangeNative.cs | 7 - .../Document/State/IdRangeSequenceNative.cs | 11 +- .../Document/State/StateVectorNative.cs | 22 +- .../Types/Events/EventKeyChangeNative.cs | 29 +-- .../UndoManager/Events/UndoEventNative.cs | 17 +- .../UndoManager/UndoManagerOptionsNative.cs | 10 - 55 files changed, 688 insertions(+), 726 deletions(-) delete mode 100644 Tests/YDotNet.Tests.Unit/Document/CloneTests.cs create mode 100644 YDotNet/Document/Events/EventPublisher.cs create mode 100644 YDotNet/Document/Events/EventSubscriber.cs delete mode 100644 YDotNet/Document/Events/EventSubscriptions.cs delete mode 100644 YDotNet/Document/Types/Events/EventDeltaAttribute.cs delete mode 100644 YDotNet/Native/Document/Events/ClearEventNative.cs create mode 100644 YDotNet/Native/Document/Events/EventBranchNative.cs diff --git a/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs b/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs index 3433cba7..f3ac2f93 100644 --- a/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs +++ b/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs @@ -36,7 +36,7 @@ public void GetAtBeginning() // Assert Assert.That(output, Is.Not.Null); - Assert.That(output.Type, Is.EqualTo(OutputType.Bool)); + Assert.That(output.Tag, Is.EqualTo(OutputTage.Bool)); } [Test] @@ -52,7 +52,7 @@ public void GetAtMiddle() // Assert Assert.That(output, Is.Not.Null); - Assert.That(output.Type, Is.EqualTo(OutputType.Undefined)); + Assert.That(output.Tag, Is.EqualTo(OutputTage.Undefined)); } [Test] diff --git a/Tests/YDotNet.Tests.Unit/Arrays/IterateTests.cs b/Tests/YDotNet.Tests.Unit/Arrays/IterateTests.cs index 01208976..9375fb9e 100644 --- a/Tests/YDotNet.Tests.Unit/Arrays/IterateTests.cs +++ b/Tests/YDotNet.Tests.Unit/Arrays/IterateTests.cs @@ -75,6 +75,6 @@ public void IteratesOnMultiItem() Assert.That(values.Length, Is.EqualTo(expected: 3)); Assert.That(values[0].Long, Is.EqualTo(expected: 2469L)); Assert.That(values[1].Boolean, Is.False); - Assert.That(values[2].Type, Is.EqualTo(OutputType.Undefined)); + Assert.That(values[2].Tag, Is.EqualTo(OutputTage.Undefined)); } } diff --git a/Tests/YDotNet.Tests.Unit/Arrays/ObserveTests.cs b/Tests/YDotNet.Tests.Unit/Arrays/ObserveTests.cs index 37e168ca..ac7e82b9 100644 --- a/Tests/YDotNet.Tests.Unit/Arrays/ObserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/Arrays/ObserveTests.cs @@ -113,7 +113,7 @@ public void ObserveHasDeltasWhenMoved() Assert.That(eventChanges.ElementAt(index: 0).Tag, Is.EqualTo(EventChangeTag.Add)); Assert.That(eventChanges.ElementAt(index: 0).Length, Is.EqualTo(expected: 1)); - Assert.That(eventChanges.ElementAt(index: 0).Values.First().Type, Is.EqualTo(OutputType.Undefined)); + Assert.That(eventChanges.ElementAt(index: 0).Values.First().Tag, Is.EqualTo(OutputTage.Undefined)); Assert.That(eventChanges.ElementAt(index: 1).Tag, Is.EqualTo(EventChangeTag.Retain)); Assert.That(eventChanges.ElementAt(index: 1).Length, Is.EqualTo(expected: 2)); diff --git a/Tests/YDotNet.Tests.Unit/Document/CloneTests.cs b/Tests/YDotNet.Tests.Unit/Document/CloneTests.cs deleted file mode 100644 index 2276ab1c..00000000 --- a/Tests/YDotNet.Tests.Unit/Document/CloneTests.cs +++ /dev/null @@ -1,22 +0,0 @@ -using NUnit.Framework; -using YDotNet.Document; - -namespace YDotNet.Tests.Unit.Document; - -public class CloneTests -{ - [Test] - public void Clone() - { - // Arrange - var doc = new Doc(); - - // Act - var clone = doc.Clone(); - - // Assert - Assert.That(clone.Handle, Is.Not.EqualTo(nint.Zero)); - Assert.That(clone.Handle, Is.Not.EqualTo(doc.Handle)); - Assert.That(clone.Id, Is.EqualTo(doc.Id)); - } -} diff --git a/Tests/YDotNet.Tests.Unit/Document/SubDocsTests.cs b/Tests/YDotNet.Tests.Unit/Document/SubDocsTests.cs index 5bc88403..80b32403 100644 --- a/Tests/YDotNet.Tests.Unit/Document/SubDocsTests.cs +++ b/Tests/YDotNet.Tests.Unit/Document/SubDocsTests.cs @@ -32,8 +32,8 @@ public void TriggersWhenAddingSubDocsUntilUnobserved() // Assert Assert.That(called, Is.EqualTo(expected: 1)); Assert.That(subDocsEvent, Is.Not.Null); - Assert.That(subDocsEvent.Added, Has.Length.EqualTo(expected: 1)); - Assert.That(subDocsEvent.Loaded, Has.Length.EqualTo(expected: 1)); + Assert.That(subDocsEvent.Added, Has.Count.EqualTo(expected: 1)); + Assert.That(subDocsEvent.Loaded, Has.Count.EqualTo(expected: 1)); Assert.That(subDocsEvent.Removed, Is.Empty); Assert.That(subDocsEvent.Added[0].Id, Is.EqualTo(subDoc1.Id)); @@ -77,7 +77,7 @@ public void TriggersWhenRemovingSubDocsUntilUnobserved() Assert.That(subDocsEvent, Is.Not.Null); Assert.That(subDocsEvent.Added, Is.Empty); Assert.That(subDocsEvent.Loaded, Is.Empty); - Assert.That(subDocsEvent.Removed, Has.Length.EqualTo(expected: 1)); + Assert.That(subDocsEvent.Removed, Has.Count.EqualTo(expected: 1)); Assert.That(subDocsEvent.Removed[0].Id, Is.EqualTo(subDoc1.Id)); // Act @@ -114,7 +114,7 @@ public void TriggersWhenAddingSubDocsWithoutLoading() // Assert Assert.That(called, Is.EqualTo(expected: 1)); Assert.That(subDocsEvent, Is.Not.Null); - Assert.That(subDocsEvent.Added, Has.Length.EqualTo(expected: 1)); + Assert.That(subDocsEvent.Added, Has.Count.EqualTo(expected: 1)); Assert.That(subDocsEvent.Loaded, Is.Empty); Assert.That(subDocsEvent.Removed, Is.Empty); Assert.That(subDocsEvent.Added[0].Id, Is.EqualTo(subDoc1.Id)); diff --git a/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs b/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs index dd5a9071..54efac75 100644 --- a/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs +++ b/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs @@ -95,7 +95,7 @@ public void GetBytes() // Assert Assert.That(value1, Is.EqualTo(new byte[] { 2, 4, 6, 9 })); - Assert.That(value2.Type, Is.EqualTo(OutputType.Bool)); + Assert.That(value2.Tag, Is.EqualTo(OutputTage.Bool)); } [Test] @@ -121,7 +121,7 @@ public void GetCollection() Assert.That(value1.Count, Is.EqualTo(expected: 2)); Assert.That(value1[0].Long, Is.EqualTo(expected: 2469)); Assert.That(value1[1].Long, Is.EqualTo(expected: -420L)); - Assert.That(value2.Type, Is.EqualTo(OutputType.Bool)); + Assert.That(value2.Tag, Is.EqualTo(OutputTage.Bool)); } [Test] @@ -146,7 +146,7 @@ public void GetObject() Assert.That(value1.Keys.Count, Is.EqualTo(expected: 2)); Assert.That(value1["star-⭐"].Long, Is.EqualTo(expected: 2469)); Assert.That(value1["moon-🌕"].Long, Is.EqualTo(expected: -420L)); - Assert.That(value2.Type, Is.EqualTo(OutputType.Bool)); + Assert.That(value2.Tag, Is.EqualTo(OutputTage.Bool)); } [Test] @@ -165,9 +165,9 @@ public void GetNull() var value3 = map.Get(transaction, "value3"); // Assert - Assert.That(value1.Type, Is.EqualTo(OutputType.Null)); - Assert.That(value2.Type, Is.Not.EqualTo(OutputType.Null)); - Assert.That(value3.Type, Is.Not.EqualTo(OutputType.Null)); + Assert.That(value1.Tag, Is.EqualTo(OutputTage.Null)); + Assert.That(value2.Tag, Is.Not.EqualTo(OutputTage.Null)); + Assert.That(value3.Tag, Is.Not.EqualTo(OutputTage.Null)); } [Test] @@ -186,9 +186,9 @@ public void GetUndefined() var value3 = map.Get(transaction, "value3"); // Assert - Assert.That(value1.Type, Is.EqualTo(OutputType.Undefined)); - Assert.That(value2.Type, Is.Not.EqualTo(OutputType.Undefined)); - Assert.That(value3.Type, Is.Not.EqualTo(OutputType.Undefined)); + Assert.That(value1.Tag, Is.EqualTo(OutputTage.Undefined)); + Assert.That(value2.Tag, Is.Not.EqualTo(OutputTage.Undefined)); + Assert.That(value3.Tag, Is.Not.EqualTo(OutputTage.Undefined)); } [Test] @@ -207,7 +207,7 @@ public void GetText() // Assert Assert.That(value1, Is.Not.Null); Assert.That(value1.String(transaction), Is.EqualTo("Lucas")); - Assert.That(value2.Type, Is.EqualTo(OutputType.Bool)); + Assert.That(value2.Tag, Is.EqualTo(OutputTage.Bool)); } [Test] @@ -231,7 +231,7 @@ public void GetArray() // Assert Assert.That(value1, Is.Not.Null); Assert.That(value1.Length, Is.EqualTo(expected: 2)); - Assert.That(value2.Type, Is.EqualTo(OutputType.Null)); + Assert.That(value2.Tag, Is.EqualTo(OutputTage.Null)); } [Test] @@ -257,7 +257,7 @@ public void GetMap() Assert.That(value1.Length(transaction), Is.EqualTo(expected: 2)); Assert.That(value1.Get(transaction, "value1-1").Long, Is.EqualTo(expected: 2469L)); Assert.That(value1.Get(transaction, "value1-2").Long, Is.EqualTo(expected: -420L)); - Assert.That(value2.Type, Is.EqualTo(OutputType.Bool)); + Assert.That(value2.Tag, Is.EqualTo(OutputTage.Bool)); } [Test] @@ -276,7 +276,7 @@ public void GetXmlElement() // Assert Assert.That(value1, Is.Not.Null); Assert.That(value1.Tag, Is.EqualTo("person")); - Assert.That(value2.Type, Is.EqualTo(OutputType.Null)); + Assert.That(value2.Tag, Is.EqualTo(OutputTage.Null)); } [Test] @@ -295,7 +295,7 @@ public void GetXmlText() // Assert Assert.That(value1, Is.Not.Null); Assert.That(value1.Length(transaction), Is.EqualTo(expected: 5)); - Assert.That(value2.Type, Is.EqualTo(OutputType.Null)); + Assert.That(value2.Tag, Is.EqualTo(OutputTage.Null)); } [Test] diff --git a/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs b/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs index 8796103c..5f5d2419 100644 --- a/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs +++ b/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs @@ -323,7 +323,7 @@ public void InsertDifferentTypeOnExistingKey() transaction.Commit(); // Assert - Assert.That(value.Type, Is.EqualTo(OutputType.String)); + Assert.That(value.Tag, Is.EqualTo(OutputTage.String)); Assert.That(value.String, Is.EqualTo("Lucas")); Assert.That(length, Is.EqualTo(expected: 1)); } diff --git a/Tests/YDotNet.Tests.Unit/Texts/InsertEmbedTests.cs b/Tests/YDotNet.Tests.Unit/Texts/InsertEmbedTests.cs index 61c5aca3..c1fa8048 100644 --- a/Tests/YDotNet.Tests.Unit/Texts/InsertEmbedTests.cs +++ b/Tests/YDotNet.Tests.Unit/Texts/InsertEmbedTests.cs @@ -147,7 +147,7 @@ public void InsertNullEmbed() var chunks = text.Chunks(transaction); Assert.That(chunks.Count, Is.EqualTo(expected: 3)); - Assert.That(chunks.ElementAt(index: 1).Data.Type, Is.EqualTo(OutputType.Null)); + Assert.That(chunks.ElementAt(index: 1).Data.Tag, Is.EqualTo(OutputTage.Null)); } [Test] @@ -163,7 +163,7 @@ public void InsertUndefinedEmbed() var chunks = text.Chunks(transaction); Assert.That(chunks.Count, Is.EqualTo(expected: 3)); - Assert.That(chunks.ElementAt(index: 1).Data.Type, Is.EqualTo(OutputType.Undefined)); + Assert.That(chunks.ElementAt(index: 1).Data.Tag, Is.EqualTo(OutputTage.Undefined)); } [Test] diff --git a/Tests/YDotNet.Tests.Unit/UndoManagers/ObserveAddedTests.cs b/Tests/YDotNet.Tests.Unit/UndoManagers/ObserveAddedTests.cs index 356caa8d..481dc935 100644 --- a/Tests/YDotNet.Tests.Unit/UndoManagers/ObserveAddedTests.cs +++ b/Tests/YDotNet.Tests.Unit/UndoManagers/ObserveAddedTests.cs @@ -39,7 +39,7 @@ public void TriggersAfterAddingAndRemovingContentOnText() Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Redo)); Assert.That(undoEvent.Origin, Is.Null); AssertDeleteSet(undoEvent.Deletions); - AssertDeleteSet(undoEvent.Insertions, (1234, new[] { new IdRange(start: 0, end: 5) })); + AssertDeleteSet(undoEvent.Insertions, (1234, new[] { new IdRange(Start: 0, End: 5) })); // Act undoEvent = null; @@ -52,7 +52,7 @@ public void TriggersAfterAddingAndRemovingContentOnText() Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Redo)); Assert.That(undoEvent.Origin, Is.Null); AssertDeleteSet(undoEvent.Deletions); - AssertDeleteSet(undoEvent.Insertions, (1234, new[] { new IdRange(start: 5, end: 11) })); + AssertDeleteSet(undoEvent.Insertions, (1234, new[] { new IdRange(Start: 5, End: 11) })); // Act undoEvent = null; @@ -66,7 +66,7 @@ public void TriggersAfterAddingAndRemovingContentOnText() Assert.That(undoEvent.Origin, Is.Null); AssertDeleteSet( undoEvent.Deletions, - (1234, new[] { new IdRange(start: 0, end: 2), new IdRange(start: 8, end: 11) })); + (1234, new[] { new IdRange(Start: 0, End: 2), new IdRange(Start: 8, End: 11) })); AssertDeleteSet(undoEvent.Insertions); } @@ -105,7 +105,7 @@ public void TriggersAfterAddingAndRemovingContentOnArray() Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Redo)); Assert.That(undoEvent.Origin, Is.Null); AssertDeleteSet(undoEvent.Deletions); - AssertDeleteSet(undoEvent.Insertions, (5678, new[] { new IdRange(start: 0, end: 3) })); + AssertDeleteSet(undoEvent.Insertions, (5678, new[] { new IdRange(Start: 0, End: 3) })); // Act undoEvent = null; @@ -123,7 +123,7 @@ public void TriggersAfterAddingAndRemovingContentOnArray() Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Redo)); Assert.That(undoEvent.Origin, Is.Null); AssertDeleteSet(undoEvent.Deletions); - AssertDeleteSet(undoEvent.Insertions, (5678, new[] { new IdRange(start: 3, end: 10) })); + AssertDeleteSet(undoEvent.Insertions, (5678, new[] { new IdRange(Start: 3, End: 10) })); // Act undoEvent = null; @@ -137,7 +137,7 @@ public void TriggersAfterAddingAndRemovingContentOnArray() Assert.That(undoEvent.Origin, Is.Null); AssertDeleteSet(undoEvent.Insertions); AssertDeleteSet( - undoEvent.Deletions, (5678, new[] { new IdRange(start: 1, end: 2), new IdRange(start: 3, end: 4) })); + undoEvent.Deletions, (5678, new[] { new IdRange(Start: 1, End: 2), new IdRange(Start: 3, End: 4) })); } [Test] @@ -167,7 +167,7 @@ public void TriggersAfterAddingAndRemovingContentOnMap() Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Redo)); Assert.That(undoEvent.Origin, Is.Null); AssertDeleteSet(undoEvent.Deletions); - AssertDeleteSet(undoEvent.Insertions, (9012, new[] { new IdRange(start: 0, end: 3) })); + AssertDeleteSet(undoEvent.Insertions, (9012, new[] { new IdRange(Start: 0, End: 3) })); // Act undoEvent = null; @@ -181,7 +181,7 @@ public void TriggersAfterAddingAndRemovingContentOnMap() Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Redo)); Assert.That(undoEvent.Origin, Is.Null); AssertDeleteSet(undoEvent.Deletions); - AssertDeleteSet(undoEvent.Insertions, (9012, new[] { new IdRange(start: 3, end: 10) })); + AssertDeleteSet(undoEvent.Insertions, (9012, new[] { new IdRange(Start: 3, End: 10) })); // Act undoEvent = null; @@ -196,7 +196,7 @@ public void TriggersAfterAddingAndRemovingContentOnMap() Assert.That(undoEvent.Origin, Is.Null); AssertDeleteSet(undoEvent.Insertions); AssertDeleteSet( - undoEvent.Deletions, (9012, new[] { new IdRange(start: 1, end: 2), new IdRange(start: 3, end: 4) })); + undoEvent.Deletions, (9012, new[] { new IdRange(Start: 1, End: 2), new IdRange(Start: 3, End: 4) })); } [Test] @@ -224,7 +224,7 @@ public void TriggersAfterAddingAndRemovingContentOnXmlText() Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Redo)); Assert.That(undoEvent.Origin, Is.Null); AssertDeleteSet(undoEvent.Deletions); - AssertDeleteSet(undoEvent.Insertions, (7853, new[] { new IdRange(start: 0, end: 5) })); + AssertDeleteSet(undoEvent.Insertions, (7853, new[] { new IdRange(Start: 0, End: 5) })); // Act undoEvent = null; @@ -237,7 +237,7 @@ public void TriggersAfterAddingAndRemovingContentOnXmlText() Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Redo)); Assert.That(undoEvent.Origin, Is.Null); AssertDeleteSet(undoEvent.Deletions); - AssertDeleteSet(undoEvent.Insertions, (7853, new[] { new IdRange(start: 5, end: 6) })); + AssertDeleteSet(undoEvent.Insertions, (7853, new[] { new IdRange(Start: 5, End: 6) })); // Act undoEvent = null; @@ -250,7 +250,7 @@ public void TriggersAfterAddingAndRemovingContentOnXmlText() Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Redo)); Assert.That(undoEvent.Origin, Is.Null); AssertDeleteSet(undoEvent.Deletions); - AssertDeleteSet(undoEvent.Insertions, (7853, new[] { new IdRange(start: 6, end: 7) })); + AssertDeleteSet(undoEvent.Insertions, (7853, new[] { new IdRange(Start: 6, End: 7) })); // Act undoEvent = null; @@ -264,7 +264,7 @@ public void TriggersAfterAddingAndRemovingContentOnXmlText() Assert.That(undoEvent.Origin, Is.Null); AssertDeleteSet(undoEvent.Insertions); AssertDeleteSet( - undoEvent.Deletions, (7853, new[] { new IdRange(start: 2, end: 3), new IdRange(start: 6, end: 7) })); + undoEvent.Deletions, (7853, new[] { new IdRange(Start: 2, End: 3), new IdRange(Start: 6, End: 7) })); } [Test] @@ -292,7 +292,7 @@ public void TriggersAfterAddingAndRemovingContentOnXmlElement() Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Redo)); Assert.That(undoEvent.Origin, Is.Null); AssertDeleteSet(undoEvent.Deletions); - AssertDeleteSet(undoEvent.Insertions, (8137, new[] { new IdRange(start: 0, end: 1) })); + AssertDeleteSet(undoEvent.Insertions, (8137, new[] { new IdRange(Start: 0, End: 1) })); // Act undoEvent = null; @@ -305,7 +305,7 @@ public void TriggersAfterAddingAndRemovingContentOnXmlElement() Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Redo)); Assert.That(undoEvent.Origin, Is.Null); AssertDeleteSet(undoEvent.Deletions); - AssertDeleteSet(undoEvent.Insertions, (8137, new[] { new IdRange(start: 1, end: 6) })); + AssertDeleteSet(undoEvent.Insertions, (8137, new[] { new IdRange(Start: 1, End: 6) })); // Act undoEvent = null; @@ -318,7 +318,7 @@ public void TriggersAfterAddingAndRemovingContentOnXmlElement() Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Redo)); Assert.That(undoEvent.Origin, Is.Null); AssertDeleteSet(undoEvent.Deletions); - AssertDeleteSet(undoEvent.Insertions, (8137, new[] { new IdRange(start: 6, end: 7) })); + AssertDeleteSet(undoEvent.Insertions, (8137, new[] { new IdRange(Start: 6, End: 7) })); // Act undoEvent = null; @@ -331,7 +331,7 @@ public void TriggersAfterAddingAndRemovingContentOnXmlElement() Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Redo)); Assert.That(undoEvent.Origin, Is.Null); AssertDeleteSet(undoEvent.Deletions); - AssertDeleteSet(undoEvent.Insertions, (8137, new[] { new IdRange(start: 7, end: 8) })); + AssertDeleteSet(undoEvent.Insertions, (8137, new[] { new IdRange(Start: 7, End: 8) })); // Act undoEvent = null; @@ -345,7 +345,7 @@ public void TriggersAfterAddingAndRemovingContentOnXmlElement() Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Redo)); Assert.That(undoEvent.Origin, Is.Null); AssertDeleteSet(undoEvent.Deletions); - AssertDeleteSet(undoEvent.Insertions, (8137, new[] { new IdRange(start: 8, end: 14) })); + AssertDeleteSet(undoEvent.Insertions, (8137, new[] { new IdRange(Start: 8, End: 14) })); // Act undoEvent = null; @@ -359,7 +359,7 @@ public void TriggersAfterAddingAndRemovingContentOnXmlElement() Assert.That(undoEvent.Origin, Is.Null); AssertDeleteSet(undoEvent.Insertions); AssertDeleteSet( - undoEvent.Deletions, (8137, new[] { new IdRange(start: 7, end: 14) })); + undoEvent.Deletions, (8137, new[] { new IdRange(Start: 7, End: 14) })); } private void AssertDeleteSet(DeleteSet deleteSet, params (uint ClientId, IdRange[] IdRanges)[] idRangesPerClientId) diff --git a/Tests/YDotNet.Tests.Unit/UndoManagers/ObservePoppedTests.cs b/Tests/YDotNet.Tests.Unit/UndoManagers/ObservePoppedTests.cs index b0873799..3c66ab21 100644 --- a/Tests/YDotNet.Tests.Unit/UndoManagers/ObservePoppedTests.cs +++ b/Tests/YDotNet.Tests.Unit/UndoManagers/ObservePoppedTests.cs @@ -32,7 +32,7 @@ public void TriggersAfterAddingAndRemovingContentOnText() Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Undo)); Assert.That(undoEvent.Origin, Is.Not.Null); AssertDeleteSet(undoEvent.Deletions); - AssertDeleteSet(undoEvent.Insertions, (1234, new[] { new IdRange(start: 0, end: 5) })); + AssertDeleteSet(undoEvent.Insertions, (1234, new[] { new IdRange(Start: 0, End: 5) })); // Act undoEvent = null; @@ -42,7 +42,7 @@ public void TriggersAfterAddingAndRemovingContentOnText() Assert.That(undoEvent, Is.Not.Null); Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Redo)); Assert.That(undoEvent.Origin, Is.Not.Null); - AssertDeleteSet(undoEvent.Deletions, (1234, new[] { new IdRange(start: 0, end: 5) })); + AssertDeleteSet(undoEvent.Deletions, (1234, new[] { new IdRange(Start: 0, End: 5) })); AssertDeleteSet(undoEvent.Insertions); // Act @@ -56,7 +56,7 @@ public void TriggersAfterAddingAndRemovingContentOnText() Assert.That(undoEvent, Is.Not.Null); Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Undo)); Assert.That(undoEvent.Origin, Is.Not.Null); - AssertDeleteSet(undoEvent.Deletions, (1234, new[] { new IdRange(start: 8, end: 10) })); + AssertDeleteSet(undoEvent.Deletions, (1234, new[] { new IdRange(Start: 8, End: 10) })); AssertDeleteSet(undoEvent.Insertions); // Act @@ -68,7 +68,7 @@ public void TriggersAfterAddingAndRemovingContentOnText() Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Redo)); Assert.That(undoEvent.Origin, Is.Not.Null); AssertDeleteSet(undoEvent.Deletions); - AssertDeleteSet(undoEvent.Insertions, (1234, new[] { new IdRange(start: 10, end: 12) })); + AssertDeleteSet(undoEvent.Insertions, (1234, new[] { new IdRange(Start: 10, End: 12) })); } [Test] @@ -99,7 +99,7 @@ public void TriggersAfterAddingAndRemovingContentOnArray() Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Undo)); Assert.That(undoEvent.Origin, Is.Not.Null); AssertDeleteSet(undoEvent.Deletions); - AssertDeleteSet(undoEvent.Insertions, (5678, new[] { new IdRange(start: 0, end: 3) })); + AssertDeleteSet(undoEvent.Insertions, (5678, new[] { new IdRange(Start: 0, End: 3) })); // Act undoEvent = null; @@ -109,7 +109,7 @@ public void TriggersAfterAddingAndRemovingContentOnArray() Assert.That(undoEvent, Is.Not.Null); Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Redo)); Assert.That(undoEvent.Origin, Is.Not.Null); - AssertDeleteSet(undoEvent.Deletions, (5678, new[] { new IdRange(start: 0, end: 3) })); + AssertDeleteSet(undoEvent.Deletions, (5678, new[] { new IdRange(Start: 0, End: 3) })); AssertDeleteSet(undoEvent.Insertions); // Act @@ -123,7 +123,7 @@ public void TriggersAfterAddingAndRemovingContentOnArray() Assert.That(undoEvent, Is.Not.Null); Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Undo)); Assert.That(undoEvent.Origin, Is.Not.Null); - AssertDeleteSet(undoEvent.Deletions, (5678, new[] { new IdRange(start: 4, end: 6) })); + AssertDeleteSet(undoEvent.Deletions, (5678, new[] { new IdRange(Start: 4, End: 6) })); AssertDeleteSet(undoEvent.Insertions); // Act @@ -135,7 +135,7 @@ public void TriggersAfterAddingAndRemovingContentOnArray() Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Redo)); Assert.That(undoEvent.Origin, Is.Not.Null); AssertDeleteSet(undoEvent.Deletions); - AssertDeleteSet(undoEvent.Insertions, (5678, new[] { new IdRange(start: 6, end: 8) })); + AssertDeleteSet(undoEvent.Insertions, (5678, new[] { new IdRange(Start: 6, End: 8) })); } [Test] @@ -160,7 +160,7 @@ public void TriggersAfterAddingAndRemovingContentOnMap() Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Undo)); Assert.That(undoEvent.Origin, Is.Not.Null); AssertDeleteSet(undoEvent.Deletions); - AssertDeleteSet(undoEvent.Insertions, (9581, new[] { new IdRange(start: 0, end: 1) })); + AssertDeleteSet(undoEvent.Insertions, (9581, new[] { new IdRange(Start: 0, End: 1) })); // Act undoEvent = null; @@ -170,7 +170,7 @@ public void TriggersAfterAddingAndRemovingContentOnMap() Assert.That(undoEvent, Is.Not.Null); Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Redo)); Assert.That(undoEvent.Origin, Is.Not.Null); - AssertDeleteSet(undoEvent.Deletions, (9581, new[] { new IdRange(start: 0, end: 1) })); + AssertDeleteSet(undoEvent.Deletions, (9581, new[] { new IdRange(Start: 0, End: 1) })); AssertDeleteSet(undoEvent.Insertions); // Act @@ -184,7 +184,7 @@ public void TriggersAfterAddingAndRemovingContentOnMap() Assert.That(undoEvent, Is.Not.Null); Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Undo)); Assert.That(undoEvent.Origin, Is.Not.Null); - AssertDeleteSet(undoEvent.Deletions, (9581, new[] { new IdRange(start: 1, end: 2) })); + AssertDeleteSet(undoEvent.Deletions, (9581, new[] { new IdRange(Start: 1, End: 2) })); AssertDeleteSet(undoEvent.Insertions); // Act @@ -196,7 +196,7 @@ public void TriggersAfterAddingAndRemovingContentOnMap() Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Redo)); Assert.That(undoEvent.Origin, Is.Not.Null); AssertDeleteSet(undoEvent.Deletions); - AssertDeleteSet(undoEvent.Insertions, (9581, new[] { new IdRange(start: 2, end: 3) })); + AssertDeleteSet(undoEvent.Insertions, (9581, new[] { new IdRange(Start: 2, End: 3) })); } [Test] @@ -221,7 +221,7 @@ public void TriggersAfterAddingAndRemovingContentOnXmlText() Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Undo)); Assert.That(undoEvent.Origin, Is.Not.Null); AssertDeleteSet(undoEvent.Deletions); - AssertDeleteSet(undoEvent.Insertions, (7938, new[] { new IdRange(start: 0, end: 5) })); + AssertDeleteSet(undoEvent.Insertions, (7938, new[] { new IdRange(Start: 0, End: 5) })); // Act GC.Collect(); @@ -232,7 +232,7 @@ public void TriggersAfterAddingAndRemovingContentOnXmlText() Assert.That(undoEvent, Is.Not.Null); Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Redo)); Assert.That(undoEvent.Origin, Is.Not.Null); - AssertDeleteSet(undoEvent.Deletions, (7938, new[] { new IdRange(start: 0, end: 5) })); + AssertDeleteSet(undoEvent.Deletions, (7938, new[] { new IdRange(Start: 0, End: 5) })); AssertDeleteSet(undoEvent.Insertions); // Act @@ -247,7 +247,7 @@ public void TriggersAfterAddingAndRemovingContentOnXmlText() Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Undo)); Assert.That(undoEvent.Origin, Is.Not.Null); AssertDeleteSet(undoEvent.Deletions); - AssertDeleteSet(undoEvent.Insertions, (7938, new[] { new IdRange(start: 10, end: 11) })); + AssertDeleteSet(undoEvent.Insertions, (7938, new[] { new IdRange(Start: 10, End: 11) })); // Act undoEvent = null; @@ -257,7 +257,7 @@ public void TriggersAfterAddingAndRemovingContentOnXmlText() Assert.That(undoEvent, Is.Not.Null); Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Redo)); Assert.That(undoEvent.Origin, Is.Not.Null); - AssertDeleteSet(undoEvent.Deletions, (7938, new[] { new IdRange(start: 10, end: 11) })); + AssertDeleteSet(undoEvent.Deletions, (7938, new[] { new IdRange(Start: 10, End: 11) })); AssertDeleteSet(undoEvent.Insertions); // Act @@ -271,7 +271,7 @@ public void TriggersAfterAddingAndRemovingContentOnXmlText() Assert.That(undoEvent, Is.Not.Null); Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Undo)); Assert.That(undoEvent.Origin, Is.Not.Null); - AssertDeleteSet(undoEvent.Deletions, (7938, new[] { new IdRange(start: 7, end: 10) })); + AssertDeleteSet(undoEvent.Deletions, (7938, new[] { new IdRange(Start: 7, End: 10) })); AssertDeleteSet(undoEvent.Insertions); // Act @@ -283,7 +283,7 @@ public void TriggersAfterAddingAndRemovingContentOnXmlText() Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Redo)); Assert.That(undoEvent.Origin, Is.Not.Null); AssertDeleteSet(undoEvent.Deletions); - AssertDeleteSet(undoEvent.Insertions, (7938, new[] { new IdRange(start: 12, end: 15) })); + AssertDeleteSet(undoEvent.Insertions, (7938, new[] { new IdRange(Start: 12, End: 15) })); } [Test] @@ -308,7 +308,7 @@ public void TriggersAfterAddingAndRemovingContentOnXmlElement() Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Undo)); Assert.That(undoEvent.Origin, Is.Not.Null); AssertDeleteSet(undoEvent.Deletions); - AssertDeleteSet(undoEvent.Insertions, (5903, new[] { new IdRange(start: 0, end: 1) })); + AssertDeleteSet(undoEvent.Insertions, (5903, new[] { new IdRange(Start: 0, End: 1) })); GC.Collect(); // Act (redo add element) @@ -319,7 +319,7 @@ public void TriggersAfterAddingAndRemovingContentOnXmlElement() Assert.That(undoEvent, Is.Not.Null); Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Redo)); Assert.That(undoEvent.Origin, Is.Not.Null); - AssertDeleteSet(undoEvent.Deletions, (5903, new[] { new IdRange(start: 0, end: 1) })); + AssertDeleteSet(undoEvent.Deletions, (5903, new[] { new IdRange(Start: 0, End: 1) })); AssertDeleteSet(undoEvent.Insertions); // Act (add attribute and undo) @@ -334,7 +334,7 @@ public void TriggersAfterAddingAndRemovingContentOnXmlElement() Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Undo)); Assert.That(undoEvent.Origin, Is.Not.Null); AssertDeleteSet(undoEvent.Deletions); - AssertDeleteSet(undoEvent.Insertions, (5903, new[] { new IdRange(start: 2, end: 3) })); + AssertDeleteSet(undoEvent.Insertions, (5903, new[] { new IdRange(Start: 2, End: 3) })); // Act (redo add attribute) undoEvent = null; @@ -344,7 +344,7 @@ public void TriggersAfterAddingAndRemovingContentOnXmlElement() Assert.That(undoEvent, Is.Not.Null); Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Redo)); Assert.That(undoEvent.Origin, Is.Not.Null); - AssertDeleteSet(undoEvent.Deletions, (5903, new[] { new IdRange(start: 2, end: 3) })); + AssertDeleteSet(undoEvent.Deletions, (5903, new[] { new IdRange(Start: 2, End: 3) })); AssertDeleteSet(undoEvent.Insertions); // Act (add text and undo) @@ -358,7 +358,7 @@ public void TriggersAfterAddingAndRemovingContentOnXmlElement() Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Undo)); Assert.That(undoEvent.Origin, Is.Not.Null); AssertDeleteSet(undoEvent.Deletions); - AssertDeleteSet(undoEvent.Insertions, (5903, new[] { new IdRange(start: 4, end: 5) })); + AssertDeleteSet(undoEvent.Insertions, (5903, new[] { new IdRange(Start: 4, End: 5) })); // Act (redo add text) undoEvent = null; @@ -381,7 +381,7 @@ public void TriggersAfterAddingAndRemovingContentOnXmlElement() Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Undo)); Assert.That(undoEvent.Origin, Is.Not.Null); AssertDeleteSet( - undoEvent.Deletions, (5903, new[] { new IdRange(start: 1, end: 2), new IdRange(start: 5, end: 6) })); + undoEvent.Deletions, (5903, new[] { new IdRange(Start: 1, End: 2), new IdRange(Start: 5, End: 6) })); AssertDeleteSet(undoEvent.Insertions); // Act (redo remove range) @@ -393,7 +393,7 @@ public void TriggersAfterAddingAndRemovingContentOnXmlElement() Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Redo)); Assert.That(undoEvent.Origin, Is.Not.Null); AssertDeleteSet(undoEvent.Deletions); - AssertDeleteSet(undoEvent.Insertions, (5903, new[] { new IdRange(start: 6, end: 8) })); + AssertDeleteSet(undoEvent.Insertions, (5903, new[] { new IdRange(Start: 6, End: 8) })); // Act (remove attribute and undo) undoEvent = null; @@ -406,7 +406,7 @@ public void TriggersAfterAddingAndRemovingContentOnXmlElement() Assert.That(undoEvent, Is.Not.Null); Assert.That(undoEvent.Kind, Is.EqualTo(UndoEventKind.Undo)); Assert.That(undoEvent.Origin, Is.Not.Null); - AssertDeleteSet(undoEvent.Deletions, (5903, new[] { new IdRange(start: 3, end: 4) })); + AssertDeleteSet(undoEvent.Deletions, (5903, new[] { new IdRange(Start: 3, End: 4) })); AssertDeleteSet(undoEvent.Insertions); // Act (redo remove attribute) diff --git a/Tests/YDotNet.Tests.Unit/UndoManagers/RedoTests.cs b/Tests/YDotNet.Tests.Unit/UndoManagers/RedoTests.cs index 324a68aa..68a5e933 100644 --- a/Tests/YDotNet.Tests.Unit/UndoManagers/RedoTests.cs +++ b/Tests/YDotNet.Tests.Unit/UndoManagers/RedoTests.cs @@ -170,7 +170,7 @@ public void RedoAddingAndUpdatingAndRemovingContentOnMap() // Assert Assert.That(length, Is.EqualTo(expected: 4)); - Assert.That(value2.Type, Is.EqualTo(OutputType.Undefined)); + Assert.That(value2.Tag, Is.EqualTo(OutputTage.Undefined)); Assert.That(result, Is.True); // Act (remove, undo, and redo) diff --git a/YDotNet/Document/Cells/Output.cs b/YDotNet/Document/Cells/Output.cs index 948bb420..e4c9ffd5 100644 --- a/YDotNet/Document/Cells/Output.cs +++ b/YDotNet/Document/Cells/Output.cs @@ -22,9 +22,9 @@ internal Output(nint handle, Doc doc) { var native = MemoryReader.PtrToStruct(handle); - Type = (OutputType)native.Tag; + Tag = (OutputTage)native.Tag; - value = BuildValue(handle, native.Length, doc, Type); + value = BuildValue(handle, native.Length, doc, Tag); } internal static Output CreateAndRelease(nint handle, Doc doc) @@ -40,124 +40,124 @@ internal static Output CreateAndRelease(nint handle, Doc doc) /// /// Gets the type of the output. /// - public OutputType Type { get; private set; } + public OutputTage Tag { get; private set; } /// /// Gets the value. /// /// Value is not a . - public Doc Doc => GetValue(OutputType.Doc); + public Doc Doc => GetValue(OutputTage.Doc); /// /// Gets the value. /// /// Value is not a . - public string String => GetValue(OutputType.String); + public string String => GetValue(OutputTage.String); /// /// Gets the value. /// /// Value is not a . - public bool Boolean => GetValue(OutputType.Bool); + public bool Boolean => GetValue(OutputTage.Bool); /// /// Gets the value. /// /// Value is not a . - public double Double => GetValue(OutputType.Double); + public double Double => GetValue(OutputTage.Double); /// /// Gets the value. /// /// Value is not a . - public long Long => GetValue(OutputType.Long); + public long Long => GetValue(OutputTage.Long); /// /// Gets the array value. /// /// Value is not a array. - public byte[] Bytes => GetValue(OutputType.Bytes); + public byte[] Bytes => GetValue(OutputTage.Bytes); /// /// Gets the collection. /// /// Value is not a collection. - public JsonArray Collection => GetValue(OutputType.Collection); + public JsonArray Collection => GetValue(OutputTage.Collection); /// /// Gets the value as json object. /// /// Value is not a json object. - public JsonObject Object => GetValue(OutputType.Object); + public JsonObject Object => GetValue(OutputTage.Object); /// /// Gets the value. /// /// The resolved array. /// Value is not a . - public Array Array => GetValue(OutputType.Array); + public Array Array => GetValue(OutputTage.Array); /// /// Gets the value. /// /// The resolved map. /// Value is not a . - public Map Map => GetValue(OutputType.Map); + public Map Map => GetValue(OutputTage.Map); /// /// Gets the value. /// /// The resolved text. /// Value is not a . - public Text Text => GetValue(OutputType.Text); + public Text Text => GetValue(OutputTage.Text); /// /// Gets the value. /// /// The resolved xml element. /// Value is not a . - public XmlElement XmlElement => GetValue(OutputType.XmlElement); + public XmlElement XmlElement => GetValue(OutputTage.XmlElement); /// /// Gets the value. /// /// The resolved xml text. /// Value is not a . - public XmlText XmlText => GetValue(OutputType.XmlText); + public XmlText XmlText => GetValue(OutputTage.XmlText); - private static object? BuildValue(nint handle, uint length, Doc doc, OutputType type) + private static object? BuildValue(nint handle, uint length, Doc doc, OutputTage type) { switch (type) { - case OutputType.Bool: + case OutputTage.Bool: { var value = OutputChannel.Boolean(handle).Checked(); return Marshal.PtrToStructure(value) == 1; } - case OutputType.Double: + case OutputTage.Double: { var value = OutputChannel.Double(handle).Checked(); return Marshal.PtrToStructure(value); } - case OutputType.Long: + case OutputTage.Long: { var value = OutputChannel.Long(handle).Checked(); return Marshal.PtrToStructure(value); } - case OutputType.String: + case OutputTage.String: { MemoryReader.TryReadUtf8String(OutputChannel.String(handle), out var result); return result; } - case OutputType.Bytes: + case OutputTage.Bytes: { var bytesHandle = OutputChannel.Bytes(handle).Checked(); var bytesArray = MemoryReader.ReadBytes(OutputChannel.Bytes(handle), length); @@ -165,32 +165,32 @@ internal static Output CreateAndRelease(nint handle, Doc doc) return bytesArray; } - case OutputType.Collection: + case OutputTage.Collection: { return new JsonArray(handle, length, doc); } - case OutputType.Object: + case OutputTage.Object: { return new JsonObject(handle, length, doc); } - case OutputType.Array: + case OutputTage.Array: return doc.GetArray(OutputChannel.Array(handle)); - case OutputType.Map: + case OutputTage.Map: return doc.GetMap(OutputChannel.Map(handle)); - case OutputType.Text: + case OutputTage.Text: return doc.GetText(OutputChannel.Text(handle)); - case OutputType.XmlElement: + case OutputTage.XmlElement: return doc.GetXmlElement(OutputChannel.XmlElement(handle)); - case OutputType.XmlText: + case OutputTage.XmlText: return doc.GetXmlText(OutputChannel.XmlText(handle)); - case OutputType.Doc: + case OutputTage.Doc: return doc.GetDoc(OutputChannel.Doc(handle)); default: @@ -198,11 +198,11 @@ internal static Output CreateAndRelease(nint handle, Doc doc) } } - private T GetValue(OutputType expectedType) + private T GetValue(OutputTage expectedType) { if (value is not T typed) { - throw new YDotNetException($"Expected {expectedType}, got {Type}."); + throw new YDotNetException($"Expected {expectedType}, got {Tag}."); } return typed; diff --git a/YDotNet/Document/Cells/OutputType.cs b/YDotNet/Document/Cells/OutputType.cs index 04e64bc5..df030509 100644 --- a/YDotNet/Document/Cells/OutputType.cs +++ b/YDotNet/Document/Cells/OutputType.cs @@ -3,7 +3,7 @@ namespace YDotNet.Document.Cells; /// /// The type of an output. /// -public enum OutputType +public enum OutputTage { /// /// No defined. diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index 0b7dcb81..a2b735b6 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -10,6 +10,8 @@ using YDotNet.Native.Document.Events; using Array = YDotNet.Document.Types.Arrays.Array; +#pragma warning disable CA1806 // Do not ignore method results + namespace YDotNet.Document; /// @@ -29,11 +31,15 @@ namespace YDotNet.Document; /// to recursively nested types). /// /// -public class Doc : UnmanagedResource, ITypeBase +public class Doc : TypeBase { - private readonly TypeCache typeCache = new TypeCache(); - private readonly EventSubscriptions subscriptions = new(); - private readonly bool isCloned; + private readonly TypeCache typeCache = new(); + private readonly EventSubscriber onClear; + private readonly EventSubscriber onUpdateV1; + private readonly EventSubscriber onUpdateV2; + private readonly EventSubscriber onAfterTransaction; + private readonly EventSubscriber onSubDocs; + private readonly Doc? parent; /// /// Initializes a new instance of the class. @@ -52,38 +58,75 @@ public Doc() /// /// The options to be used when initializing this document. public Doc(DocOptions options) - : base(CreateDoc(options)) + : this(CreateDoc(options), null) { } - internal Doc(nint handle, bool isCloned, Doc? parent) - : base(handle) + internal Doc(nint handle, Doc? parent) { - this.isCloned = isCloned; + this.parent = parent; + + onClear = new EventSubscriber( + handle, + (doc, action) => + { + DocChannel.ObserveClearCallback callback = + (_, doc) => action(new ClearEvent(GetDoc(doc))); + + return (DocChannel.ObserveClear(doc, nint.Zero, callback), callback); + }, + (doc, s) => DocChannel.UnobserveClear(doc, s)); + + onUpdateV1 = new EventSubscriber( + handle, + (doc, action) => + { + DocChannel.ObserveUpdatesCallback callback = + (_, length, data) => action(new UpdateEvent(UpdateEventNative.From(length, data))); + + return (DocChannel.ObserveUpdatesV1(Handle, nint.Zero, callback), callback); + }, + (doc, s) => DocChannel.UnobserveUpdatesV1(doc, s)); + + onUpdateV2 = new EventSubscriber( + handle, + (doc, action) => + { + DocChannel.ObserveUpdatesCallback callback = + (_, length, data) => action(new UpdateEvent(UpdateEventNative.From(length, data))); + + return (DocChannel.ObserveUpdatesV2(Handle, nint.Zero, callback), callback); + }, + (doc, s) => DocChannel.UnobserveUpdatesV2(doc, s)); + + onAfterTransaction = new EventSubscriber( + handle, + (doc, action) => + { + DocChannel.ObserveAfterTransactionCallback callback = + (_, ev) => action(new AfterTransactionEvent(MemoryReader.ReadStruct(ev))); + + return (DocChannel.ObserveAfterTransaction(doc, nint.Zero, callback), callback); + }, + (doc, s) => DocChannel.UnobserveAfterTransaction(doc, s)); + + onSubDocs = new EventSubscriber( + handle, + (doc, action) => + { + DocChannel.ObserveSubdocsCallback callback = + (_, ev) => action(new SubDocsEvent(MemoryReader.ReadStruct(ev), this)); + + return (DocChannel.ObserveSubDocs(doc, nint.Zero, callback), callback); + }, + (doc, s) => DocChannel.UnobserveSubDocs(doc, s)); + + Handle = handle; } private static nint CreateDoc(DocOptions options) { - var unsafeOptions = DocOptionsNative.From(options); - - return DocChannel.NewWithOptions(unsafeOptions); - } - - /// - /// Finalizes an instance of the class. - /// - ~Doc() - { - Dispose(false); - } - - /// - protected internal override void DisposeCore(bool disposing) - { - if (isCloned) - { - DocChannel.Destroy(Handle); - } + return DocChannel.NewWithOptions(options.ToNative()); } /// @@ -127,17 +170,7 @@ public string? CollectionId /// public bool AutoLoad => DocChannel.AutoLoad(Handle); - /// - /// Creates a copy of the current instance. - /// - /// The instance returned will not be the same, but they will both control the same document. - /// A new instance that controls the same document. - public Doc Clone() - { - var handle = DocChannel.Clone(Handle).Checked(); - - return new Doc(handle, true, null); - } + internal nint Handle { get; } /// /// Gets or creates a new shared data type instance as a root-level @@ -271,9 +304,12 @@ public Transaction ReadTransaction() /// public void Clear() { - subscriptions.Clear(); - DocChannel.Clear(Handle); + onClear.Clear(); + onUpdateV1.Clear(); + onUpdateV2.Clear(); + onAfterTransaction.Clear(); + onSubDocs.Clear(); } /// @@ -295,17 +331,7 @@ public void Load(Transaction transaction) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable ObserveClear(Action action) { - DocChannel.ObserveClearCallback callback = (_, doc) => action(ClearEventNative.From(this).ToClearEvent()); - - var subscriptionId = DocChannel.ObserveClear( - Handle, - nint.Zero, - callback); - - return subscriptions.Add(callback, () => - { - DocChannel.UnobserveClear(Handle, subscriptionId); - }); + return onClear.Subscribe(action); } /// @@ -318,17 +344,7 @@ public IDisposable ObserveClear(Action action) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable ObserveUpdatesV1(Action action) { - DocChannel.ObserveUpdatesCallback callback = (_, length, data) => action(UpdateEventNative.From(length, data).ToUpdateEvent()); - - var subscriptionId = DocChannel.ObserveUpdatesV1( - Handle, - nint.Zero, - callback); - - return subscriptions.Add(callback, () => - { - DocChannel.UnobserveUpdatesV1(Handle, subscriptionId); - }); + return onUpdateV1.Subscribe(action); } /// @@ -341,17 +357,7 @@ public IDisposable ObserveUpdatesV1(Action action) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable ObserveUpdatesV2(Action action) { - DocChannel.ObserveUpdatesCallback callback = (_, length, data) => action(UpdateEventNative.From(length, data).ToUpdateEvent()); - - var subscriptionId = DocChannel.ObserveUpdatesV2( - Handle, - nint.Zero, - callback); - - return subscriptions.Add(callback, () => - { - DocChannel.UnobserveUpdatesV2(Handle, subscriptionId); - }); + return onUpdateV2.Subscribe(action); } /// @@ -364,17 +370,7 @@ public IDisposable ObserveUpdatesV2(Action action) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable ObserveAfterTransaction(Action action) { - DocChannel.ObserveAfterTransactionCallback callback = (_, eventHandler) => action(MemoryReader.ReadStruct(eventHandler).ToAfterTransactionEvent()); - - var subscriptionId = DocChannel.ObserveAfterTransaction( - Handle, - nint.Zero, - callback); - - return subscriptions.Add(callback, () => - { - DocChannel.UnobserveAfterTransaction(Handle, subscriptionId); - }); + return onAfterTransaction.Subscribe(action); } /// @@ -384,51 +380,46 @@ public IDisposable ObserveAfterTransaction(Action action) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable ObserveSubDocs(Action action) { - DocChannel.ObserveSubdocsCallback callback = (_, eventHandle) => action(MemoryReader.ReadStruct(eventHandle).ToSubDocsEvent(this)); - - var subscriptionId = DocChannel.ObserveSubDocs( - Handle, - nint.Zero, - callback); - - return subscriptions.Add(callback, () => - { - DocChannel.UnobserveSubDocs(Handle, subscriptionId); - }); + return onSubDocs.Subscribe(action); } internal Doc GetDoc(nint handle) { - return typeCache.GetOrAdd(handle, h => new Doc(h, false, this)); + return GetOrAdd(handle, h => new Doc(h, this)); } internal Map GetMap(nint handle) { - return typeCache.GetOrAdd(handle, h => new Map(h, this)); + return GetOrAdd(handle, h => new Map(h, this)); } internal Array GetArray(nint handle) { - return typeCache.GetOrAdd(handle, h => new Array(h, this)); + return GetOrAdd(handle, h => new Array(h, this)); } internal Text GetText(nint handle) { - return typeCache.GetOrAdd(handle, h => new Text(h, this)); + return GetOrAdd(handle, h => new Text(h, this)); } internal XmlText GetXmlText(nint handle) { - return typeCache.GetOrAdd(handle, h => new XmlText(h, this)); + return GetOrAdd(handle, h => new XmlText(h, this)); } internal XmlElement GetXmlElement(nint handle) { - return typeCache.GetOrAdd(handle, h => new XmlElement(h, this)); + return GetOrAdd(handle, h => new XmlElement(h, this)); } - public void MarkDeleted() + private T GetOrAdd(nint handle, Func factory) where T : ITypeBase { - throw new NotImplementedException(); + if (parent != null) + { + return parent.GetOrAdd(handle, factory); + } + + return typeCache.GetOrAdd(handle, factory); } } diff --git a/YDotNet/Document/Events/AfterTransactionEvent.cs b/YDotNet/Document/Events/AfterTransactionEvent.cs index 0ce22030..f50046e2 100644 --- a/YDotNet/Document/Events/AfterTransactionEvent.cs +++ b/YDotNet/Document/Events/AfterTransactionEvent.cs @@ -1,4 +1,5 @@ using YDotNet.Document.State; +using YDotNet.Native.Document.Events; namespace YDotNet.Document.Events; @@ -7,17 +8,11 @@ namespace YDotNet.Document.Events; /// public class AfterTransactionEvent { - /// - /// Initializes a new instance of the class. - /// - /// The initial value for . - /// The initial value for . - /// The initial value for . - public AfterTransactionEvent(StateVector beforeState, StateVector afterState, DeleteSet deleteSet) + internal AfterTransactionEvent(AfterTransactionEventNative native) { - BeforeState = beforeState; - AfterState = afterState; - DeleteSet = deleteSet; + BeforeState = new StateVector(native.BeforeState); + AfterState = new StateVector(native.AfterState); + DeleteSet = new DeleteSet(native.DeleteSet); } /// diff --git a/YDotNet/Document/Events/ClearEvent.cs b/YDotNet/Document/Events/ClearEvent.cs index 108daea6..a13e89ea 100644 --- a/YDotNet/Document/Events/ClearEvent.cs +++ b/YDotNet/Document/Events/ClearEvent.cs @@ -5,11 +5,7 @@ namespace YDotNet.Document.Events; /// public class ClearEvent { - /// - /// Initializes a new instance of the class. - /// - /// The initial value for . - public ClearEvent(Doc doc) + internal ClearEvent(Doc doc) { Doc = doc; } diff --git a/YDotNet/Document/Events/EventPublisher.cs b/YDotNet/Document/Events/EventPublisher.cs new file mode 100644 index 00000000..6090af3f --- /dev/null +++ b/YDotNet/Document/Events/EventPublisher.cs @@ -0,0 +1,41 @@ +namespace YDotNet.Document.Events; + +internal sealed class EventPublisher +{ + private readonly HashSet> subscriptions = new(); + + public int Count => subscriptions.Count; + + public void Clear() + { + subscriptions.Clear(); + } + + public void Subscribe(Action handler) + { + subscriptions.Add(handler); + } + + public void Unsubscribe(Action handler) + { + if (!subscriptions.Remove(handler)) + { + return; + } + } + + public void Publish(TEvent @event) + { + foreach (var subscription in subscriptions) + { + try + { + subscription(@event); + } + catch + { + continue; + } + } + } +} diff --git a/YDotNet/Document/Events/EventSubscriber.cs b/YDotNet/Document/Events/EventSubscriber.cs new file mode 100644 index 00000000..3bd59cc2 --- /dev/null +++ b/YDotNet/Document/Events/EventSubscriber.cs @@ -0,0 +1,60 @@ +namespace YDotNet.Document.Events; + +internal class EventSubscriber +{ + private readonly EventPublisher publisher = new EventPublisher(); + private readonly nint owner; + private readonly Func, (uint Handle, object Callback)> subscribe; + private readonly Action unsubscribe; + private (uint Handle, object Callback) nativeSubscription; + + public EventSubscriber( + nint owner, + Func, (uint Handle, object Callback)> subscribe, + Action unsubscribe) + { + this.owner = owner; + this.subscribe = subscribe; + this.unsubscribe = unsubscribe; + } + + public void Clear() + { + publisher.Clear(); + } + + public IDisposable Subscribe(Action handler) + { + if (nativeSubscription.Handle == nint.Zero) + { + nativeSubscription = subscribe(owner, publisher.Publish); + } + + publisher.Subscribe(handler); + + return new DelegateDisposable(() => + { + Unsubscribe(handler); + }); + } + + private void Unsubscribe(Action handler) + { + publisher.Unsubscribe(handler); + + if (publisher.Count == 0 && nativeSubscription.Handle != nint.Zero) + { + unsubscribe(owner, nativeSubscription.Handle); + + nativeSubscription = default; + } + } + + private sealed record DelegateDisposable(Action Delegate) : IDisposable + { + public void Dispose() + { + Delegate(); + } + } +} diff --git a/YDotNet/Document/Events/EventSubscriptions.cs b/YDotNet/Document/Events/EventSubscriptions.cs deleted file mode 100644 index 3e76b25d..00000000 --- a/YDotNet/Document/Events/EventSubscriptions.cs +++ /dev/null @@ -1,45 +0,0 @@ -using YDotNet.Infrastructure; - -namespace YDotNet.Document.Events; - -internal sealed class EventSubscriptions -{ - private readonly HashSet subscriptions = new(); - - public IDisposable Add(object callback, Action unsubscribe) - { - var subscription = new EventSubscription(callback, s => - { - subscriptions.Remove(s); - unsubscribe(); - }); - - subscriptions.Add(subscription); - return subscription; - } - - public void Clear() - { - subscriptions.Clear(); - } - - private sealed class EventSubscription : Resource - { - private readonly Action unsubscribe; - - internal EventSubscription(object callback, Action unsubscribe) - { - this.unsubscribe = unsubscribe; - - // Just holds a reference to the callback to prevent garbage collection. - Callback = callback; - } - - internal object? Callback { get; set; } - - protected internal override void DisposeCore(bool disposing) - { - unsubscribe(this); - } - } -} diff --git a/YDotNet/Document/Events/SubDocsEvent.cs b/YDotNet/Document/Events/SubDocsEvent.cs index 8fc80760..cc7aab36 100644 --- a/YDotNet/Document/Events/SubDocsEvent.cs +++ b/YDotNet/Document/Events/SubDocsEvent.cs @@ -1,3 +1,5 @@ +using YDotNet.Native.Document.Events; + namespace YDotNet.Document.Events; /// @@ -5,31 +7,27 @@ namespace YDotNet.Document.Events; /// public class SubDocsEvent { - /// - /// Initializes a new instance of the class. - /// - /// The initial value for . - /// The initial value for . - /// The initial value for . - public SubDocsEvent(Doc[] added, Doc[] removed, Doc[] loaded) + internal SubDocsEvent(SubDocsEventNative native, Doc doc) { - Added = added; - Removed = removed; - Loaded = loaded; + Added = native.Added().Select(doc.GetDoc).ToList(); + + Removed = native.Removed().Select(doc.GetDoc).ToList(); + + Loaded = native.Loaded().Select(doc.GetDoc).ToList(); } /// /// Gets the sub-documents that were added to the instance that emitted this event. /// - public Doc[] Added { get; } + public IReadOnlyList Added { get; } /// /// Gets the sub-documents that were removed to the instance that emitted this event. /// - public Doc[] Removed { get; } + public IReadOnlyList Removed { get; } /// /// Gets the sub-documents that were loaded to the instance that emitted this event. /// - public Doc[] Loaded { get; } + public IReadOnlyList Loaded { get; } } diff --git a/YDotNet/Document/Events/UpdateEvent.cs b/YDotNet/Document/Events/UpdateEvent.cs index 0789d071..cdf6bd0d 100644 --- a/YDotNet/Document/Events/UpdateEvent.cs +++ b/YDotNet/Document/Events/UpdateEvent.cs @@ -1,3 +1,5 @@ +using YDotNet.Native.Document.Events; + namespace YDotNet.Document.Events; /// @@ -6,13 +8,9 @@ namespace YDotNet.Document.Events; /// public class UpdateEvent { - /// - /// Initializes a new instance of the class. - /// - /// The binary information this event represents. - public UpdateEvent(byte[] update) + internal UpdateEvent(UpdateEventNative native) { - Update = update; + Update = native.Bytes(); } /// diff --git a/YDotNet/Document/Options/DocOptions.cs b/YDotNet/Document/Options/DocOptions.cs index d0000a62..0fb5b79c 100644 --- a/YDotNet/Document/Options/DocOptions.cs +++ b/YDotNet/Document/Options/DocOptions.cs @@ -1,5 +1,7 @@ using YDotNet.Document.Types.Texts; using YDotNet.Document.Types.XmlTexts; +using YDotNet.Infrastructure; +using YDotNet.Native.Document; namespace YDotNet.Document.Options; @@ -13,9 +15,7 @@ public class DocOptions /// public static DocOptions Default => new() { - Id = (ulong)Random.Shared.Next(), - ShouldLoad = true, - Encoding = DocEncoding.Utf16 + Id = (ulong)Random.Shared.Next() }; /// @@ -31,7 +31,7 @@ public class DocOptions /// contain all of that clients updates that were sent to other peers. /// /// - public ulong? Id { get; init; } + public ulong Id { get; init; } /// /// Gets the globally unique UUID v4 compatible string identifier of this . @@ -60,26 +60,44 @@ public class DocOptions /// Read more about the possible values in . /// /// - public DocEncoding Encoding { get; init; } + public DocEncoding Encoding { get; init; } = DocEncoding.Utf16; /// - /// Gets the flag that determines whether deleted blocks should be garbage collected during transaction commits. + /// Gets a value indicating whether deleted blocks should be garbage collected during transaction commits. /// /// /// Setting this value to false means garbage collection will be performed. /// - public bool? SkipGarbageCollection { get; init; } + public bool SkipGarbageCollection { get; init; } /// - /// Gets the flag that determines whether sub-documents should be loaded automatically. + /// Gets a value indicating whether sub-documents should be loaded automatically. /// /// /// If this is a sub-document, remote peers will automatically load the as well. /// - public bool? AutoLoad { get; init; } + public bool AutoLoad { get; init; } /// - /// Gets the flag that determines whether the should be synchronized by the provider now. + /// Gets a value indicating whether the should be synchronized by the provider now. /// - public bool? ShouldLoad { get; init; } + public bool ShouldLoad { get; init; } = true; + + internal DocOptionsNative ToNative() + { + // We can never release the memory because y-crdt just receives a pointer to that. + var unsafeGuid = MemoryWriter.WriteUtf8String(Guid); + var unsafeCollection = MemoryWriter.WriteUtf8String(CollectionId); + + return new DocOptionsNative + { + Id = Id, + Guid = unsafeGuid.Handle, + CollectionId = unsafeCollection.Handle, + Encoding = (byte)Encoding, + SkipGc = (byte)(SkipGarbageCollection ? 1 : 0), + AutoLoad = (byte)(AutoLoad ? 1 : 0), + ShouldLoad = (byte)(ShouldLoad ? 1 : 0) + }; + } } diff --git a/YDotNet/Document/State/DeleteSet.cs b/YDotNet/Document/State/DeleteSet.cs index e3e4e4af..2df79e1b 100644 --- a/YDotNet/Document/State/DeleteSet.cs +++ b/YDotNet/Document/State/DeleteSet.cs @@ -1,3 +1,5 @@ +using YDotNet.Native.Document.State; + namespace YDotNet.Document.State; /// @@ -5,17 +7,23 @@ namespace YDotNet.Document.State; /// public class DeleteSet { - /// - /// Initializes a new instance of the class. - /// - /// The initial value for . - public DeleteSet(Dictionary ranges) + internal DeleteSet(DeleteSetNative native) { + var allClients = native.Clients(); + var allRanges = native.Ranges(); + + var ranges = new Dictionary(); + + for (var i = 0; i < native.EntriesCount; i++) + { + ranges.Add(allClients[i], allRanges[i].Sequence().Select(IdRange.Create).ToArray()); + } + Ranges = ranges; } /// /// Gets dictionary of unique client identifiers (keys) by their deleted ID ranges (values). /// - public Dictionary Ranges { get; } + public IReadOnlyDictionary Ranges { get; } } diff --git a/YDotNet/Document/State/IdRange.cs b/YDotNet/Document/State/IdRange.cs index 57db3543..b80082de 100644 --- a/YDotNet/Document/State/IdRange.cs +++ b/YDotNet/Document/State/IdRange.cs @@ -1,28 +1,14 @@ +using YDotNet.Native.Document.State; + namespace YDotNet.Document.State; /// /// Represents a single space of clock values, belonging to the same client. /// -public class IdRange +public sealed record IdRange(uint Start, uint End) { - /// - /// Initializes a new instance of the class. - /// - /// The value for . - /// The value for . - public IdRange(uint start, uint end) + internal static IdRange Create(IdRangeNative native) { - Start = start; - End = end; + return new IdRange(native.Start, native.End); } - - /// - /// Gets the start of the . - /// - public uint Start { get; } - - /// - /// Gets the end of the . - /// - public uint End { get; } } diff --git a/YDotNet/Document/State/StateVector.cs b/YDotNet/Document/State/StateVector.cs index c6f810ad..37a15666 100644 --- a/YDotNet/Document/State/StateVector.cs +++ b/YDotNet/Document/State/StateVector.cs @@ -1,3 +1,5 @@ +using YDotNet.Native.Document.State; + namespace YDotNet.Document.State; /// @@ -8,17 +10,23 @@ namespace YDotNet.Document.State; /// public class StateVector { - /// - /// Initializes a new instance of the class. - /// - /// The initial value for the . - public StateVector(Dictionary state) + internal StateVector(StateVectorNative native) { + var allClientIds = native.ClientIds(); + var allClocks = native.Clocks(); + + var state = new Dictionary(); + + for (var i = 0; i < native.EntriesCount; i++) + { + state.Add(allClientIds[i], allClocks[i]); + } + State = state; } /// /// Gets dictionary of unique client identifiers (keys) by their clocks (values). /// - public Dictionary State { get; } + public IReadOnlyDictionary State { get; } } diff --git a/YDotNet/Document/Types/Arrays/Array.cs b/YDotNet/Document/Types/Arrays/Array.cs index 5899399d..ee554d11 100644 --- a/YDotNet/Document/Types/Arrays/Array.cs +++ b/YDotNet/Document/Types/Arrays/Array.cs @@ -15,11 +15,21 @@ namespace YDotNet.Document.Types.Arrays; /// public class Array : Branch { - private readonly EventSubscriptions subscriptions = new(); + private readonly EventSubscriber onObserve; internal Array(nint handle, Doc doc) : base(handle, doc) { + onObserve = new EventSubscriber( + handle, + (array, action) => + { + ArrayChannel.ObserveCallback callback = (_, eventHandle) => + action(new ArrayEvent(eventHandle, Doc)); + + return (ArrayChannel.Observe(array, nint.Zero, callback), callback); + }, + ArrayChannel.Unobserve); } /// @@ -105,17 +115,7 @@ public ArrayIterator Iterate(Transaction transaction) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable Observe(Action action) { - ArrayChannel.ObserveCallback callback = (_, eventHandle) => action(new ArrayEvent(eventHandle, Doc)); - - var subscriptionId = ArrayChannel.Observe( - Handle, - nint.Zero, - callback); - - return subscriptions.Add(callback, () => - { - ArrayChannel.Unobserve(Handle, subscriptionId); - }); + return onObserve.Subscribe(action); } /// diff --git a/YDotNet/Document/Types/Branches/Branch.cs b/YDotNet/Document/Types/Branches/Branch.cs index 64b9667c..8d5ec6b8 100644 --- a/YDotNet/Document/Types/Branches/Branch.cs +++ b/YDotNet/Document/Types/Branches/Branch.cs @@ -2,8 +2,11 @@ using YDotNet.Document.Transactions; using YDotNet.Document.Types.Events; using YDotNet.Infrastructure; +using YDotNet.Native.Document.Events; using YDotNet.Native.Types.Branches; +#pragma warning disable CA1806 // Do not ignore method results + namespace YDotNet.Document.Types.Branches; /// @@ -11,13 +14,27 @@ namespace YDotNet.Document.Types.Branches; /// public abstract class Branch : TypeBase { - private readonly EventSubscriptions subscriptions = new(); - private readonly Doc doc; + private readonly EventSubscriber onDeep; internal Branch(nint handle, Doc doc) { Doc = doc; + onDeep = new EventSubscriber( + handle, + (branch, action) => + { + BranchChannel.ObserveCallback callback = (_, length, ev) => + { + var events = MemoryReader.ReadIntPtrArray(ev, length).Select(x => new EventBranch(x, doc)).ToArray(); + + action(events); + }; + + return (BranchChannel.ObserveDeep(branch, nint.Zero, callback), callback); + }, + (branch, s) => BranchChannel.UnobserveDeep(branch, s)); + Handle = handle; } @@ -36,22 +53,7 @@ internal Branch(nint handle, Doc doc) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable ObserveDeep(Action> action) { - BranchChannel.ObserveCallback callback = (_, length, eventsHandle) => - { - var events = MemoryReader.ReadIntPtrArray(eventsHandle, length, size: 24).Select(x => new EventBranch(x, doc)).ToArray(); - - action(events); - }; - - var subscriptionId = BranchChannel.ObserveDeep( - Handle, - nint.Zero, - callback); - - return subscriptions.Add(callback, () => - { - BranchChannel.UnobserveDeep(Handle, subscriptionId); - }); + return onDeep.Subscribe(action); } /// @@ -69,7 +71,7 @@ public Transaction WriteTransaction() return default!; } - return new Transaction(handle, doc); + return new Transaction(handle, Doc); } /// @@ -87,6 +89,6 @@ public Transaction ReadTransaction() return default!; } - return new Transaction(handle, doc); + return new Transaction(handle, Doc); } } diff --git a/YDotNet/Document/Types/Events/EventBranch.cs b/YDotNet/Document/Types/Events/EventBranch.cs index 92fa90a3..817cca2b 100644 --- a/YDotNet/Document/Types/Events/EventBranch.cs +++ b/YDotNet/Document/Types/Events/EventBranch.cs @@ -1,10 +1,10 @@ -using System.Runtime.InteropServices; using YDotNet.Document.Types.Arrays.Events; using YDotNet.Document.Types.Maps.Events; using YDotNet.Document.Types.Texts.Events; using YDotNet.Document.Types.XmlElements.Events; using YDotNet.Document.Types.XmlTexts.Events; -using YDotNet.Infrastructure; +using YDotNet.Native; +using YDotNet.Native.Document.Events; namespace YDotNet.Document.Types.Events; @@ -14,31 +14,38 @@ namespace YDotNet.Document.Types.Events; /// public class EventBranch { - internal EventBranch(nint handle, Doc doc) + private readonly object? value; + + internal EventBranch(NativeWithHandle native, Doc doc) + { + Tag = (EventBranchTag)native.Value.Tag; + + value = CreateValue(native, doc, Tag); + } + + private static object? CreateValue(NativeWithHandle native, Doc doc, EventBranchTag tag) { - Handle = handle; - Tag = (EventBranchTag)Marshal.ReadByte(handle); + var handle = native.Value.ValueHandle(native.Handle); - switch (Tag) + switch (tag) { case EventBranchTag.Map: - MapEvent = new MapEvent(handle + MemoryConstants.PointerSize, doc); - break; + return new MapEvent(handle, doc); + case EventBranchTag.Text: - TextEvent = new TextEvent(handle + MemoryConstants.PointerSize, doc); - break; + return new TextEvent(handle, doc); + case EventBranchTag.Array: - ArrayEvent = new ArrayEvent(handle + MemoryConstants.PointerSize, doc); - break; + return new ArrayEvent(handle, doc); + case EventBranchTag.XmlElement: - XmlElementEvent = new XmlElementEvent(handle + MemoryConstants.PointerSize, doc); - break; + return new XmlElementEvent(handle, doc); + case EventBranchTag.XmlText: - XmlTextEvent = new XmlTextEvent(handle + MemoryConstants.PointerSize, doc); - break; + return new XmlTextEvent(handle, doc); + default: - throw new NotSupportedException( - $"The value \"{Tag}\" is not supported by the {nameof(EventBranch)} class."); + return null; } } @@ -48,37 +55,42 @@ internal EventBranch(nint handle, Doc doc) public EventBranchTag Tag { get; } /// - /// Gets the , if is , or null - /// otherwise. + /// Gets the value. /// - public MapEvent? MapEvent { get; } + /// Value is not a . + public MapEvent MapEvent => GetValue(EventBranchTag.Map); /// - /// Gets the , if is , or null - /// otherwise. + /// Gets the value. /// - public TextEvent? TextEvent { get; } + /// Value is not a . + public TextEvent TextEvent => GetValue(EventBranchTag.Text); /// - /// Gets the , if is , or null - /// otherwise. + /// Gets the value. /// - public ArrayEvent? ArrayEvent { get; } + /// Value is not a . + public ArrayEvent ArrayEvent => GetValue(EventBranchTag.Array); /// - /// Gets the , if is , - /// or null otherwise. + /// Gets the value. /// - public XmlElementEvent? XmlElementEvent { get; } + /// Value is not a . + public XmlElementEvent XmlElementEvent => GetValue(EventBranchTag.XmlElement); /// - /// Gets the , if is , - /// or null otherwise. + /// Gets the value. /// - public XmlTextEvent? XmlTextEvent { get; } + /// Value is not a . + public XmlTextEvent XmlTextEvent => GetValue(EventBranchTag.XmlText); - /// - /// Gets the handle to the native resource. - /// - internal nint Handle { get; } + private T GetValue(EventBranchTag expectedType) + { + if (value is not T typed) + { + throw new YDotNetException($"Expected {expectedType}, got {Tag}."); + } + + return typed; + } } diff --git a/YDotNet/Document/Types/Events/EventDeltaAttribute.cs b/YDotNet/Document/Types/Events/EventDeltaAttribute.cs deleted file mode 100644 index 6a396885..00000000 --- a/YDotNet/Document/Types/Events/EventDeltaAttribute.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Runtime.InteropServices; -using YDotNet.Document.Cells; -using YDotNet.Infrastructure; - -namespace YDotNet.Document.Types.Events; - -/// -/// The formatting attribute that's part of an instance. -/// -public sealed class EventDeltaAttribute -{ - private readonly Lazy value; - - internal EventDeltaAttribute(nint handle, Doc doc) - { - Handle = handle; - - Key = Marshal.PtrToStringAnsi(Marshal.ReadIntPtr(handle)) - ?? throw new InvalidOperationException("Failed to read key"); - - value = new Lazy(() => - { - return new Output(handle + MemoryConstants.PointerSize, doc); - }); - } - - /// - /// Gets the attribute name. - /// - public string Key { get; } - - /// - /// Gets the attribute value. - /// - public Output Value => value.Value; - - /// - /// Gets the handle to the native resource. - /// - internal nint Handle { get; } -} diff --git a/YDotNet/Document/Types/Events/EventKeyChange.cs b/YDotNet/Document/Types/Events/EventKeyChange.cs index e82a390a..5d47ef79 100644 --- a/YDotNet/Document/Types/Events/EventKeyChange.cs +++ b/YDotNet/Document/Types/Events/EventKeyChange.cs @@ -1,4 +1,5 @@ using YDotNet.Document.Cells; +using YDotNet.Native.Types.Events; namespace YDotNet.Document.Types.Events; @@ -11,19 +12,27 @@ namespace YDotNet.Document.Types.Events; /// public class EventKeyChange { - /// - /// Initializes a new instance of the class. - /// - /// The key of the value that changed in the parent instance. - /// The type of change that was performed in the entry. - /// The old value that was present in the before the change. - /// The new value that is present in the after the change. - internal EventKeyChange(string key, EventKeyChangeTag tag, Output? oldValue, Output? newValue) + internal EventKeyChange(EventKeyChangeNative native, Doc doc) { - Key = key; - Tag = tag; - OldValue = oldValue; - NewValue = newValue; + Key = native.Key(); + + Tag = native.TagNative switch + { + EventKeyChangeTagNative.Add => EventKeyChangeTag.Add, + EventKeyChangeTagNative.Remove => EventKeyChangeTag.Remove, + EventKeyChangeTagNative.Update => EventKeyChangeTag.Update, + _ => throw new NotSupportedException($"The value \"{native.TagNative}\" for {nameof(EventKeyChangeTagNative)} is not supported."), + }; + + if (native.OldValue != nint.Zero) + { + OldValue = new Output(native.OldValue, doc); + } + + if (native.NewValue != nint.Zero) + { + NewValue = new Output(native.NewValue, doc); + } } /// diff --git a/YDotNet/Document/Types/Events/EventKeys.cs b/YDotNet/Document/Types/Events/EventKeys.cs index d25f715c..932a366d 100644 --- a/YDotNet/Document/Types/Events/EventKeys.cs +++ b/YDotNet/Document/Types/Events/EventKeys.cs @@ -1,5 +1,4 @@ using System.Collections.ObjectModel; -using System.Runtime.InteropServices; using YDotNet.Infrastructure; using YDotNet.Native.Types; using YDotNet.Native.Types.Events; @@ -14,16 +13,15 @@ public class EventKeys : ReadOnlyCollection internal EventKeys(nint handle, uint length, Doc doc) : base(ReadItems(handle, length, doc)) { - } private static IList ReadItems(nint handle, uint length, Doc doc) { var result = new List(); - foreach (var keyHandle in MemoryReader.ReadIntPtrArray(handle, length, Marshal.SizeOf())) + foreach (var native in MemoryReader.ReadIntPtrArray(handle, length)) { - result.Add(Marshal.PtrToStructure(keyHandle).ToEventKeyChange(doc)); + result.Add(new EventKeyChange(native.Value, doc)); } // We are done reading and can destroy the resource. diff --git a/YDotNet/Document/Types/Maps/Map.cs b/YDotNet/Document/Types/Maps/Map.cs index f28809ba..659b26a4 100644 --- a/YDotNet/Document/Types/Maps/Map.cs +++ b/YDotNet/Document/Types/Maps/Map.cs @@ -13,11 +13,21 @@ namespace YDotNet.Document.Types.Maps; /// public class Map : Branch { - private readonly EventSubscriptions subscriptions = new(); + private readonly EventSubscriber onObserve; internal Map(nint handle, Doc doc) : base(handle, doc) { + onObserve = new EventSubscriber( + handle, + (map, action) => + { + MapChannel.ObserveCallback callback = (_, eventHandle) => + action(new MapEvent(eventHandle, Doc)); + + return (MapChannel.Observe(map, nint.Zero, callback), callback); + }, + MapChannel.Unobserve); } /// @@ -110,16 +120,6 @@ public MapIterator Iterate(Transaction transaction) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable Observe(Action action) { - MapChannel.ObserveCallback callback = (_, eventHandle) => action(new MapEvent(eventHandle, Doc)); - - var subscriptionId = MapChannel.Observe( - Handle, - nint.Zero, - callback); - - return subscriptions.Add(callback, () => - { - MapChannel.Unobserve(Handle, subscriptionId); - }); + return onObserve.Subscribe(action); } } diff --git a/YDotNet/Document/Types/Maps/MapEnumerator.cs b/YDotNet/Document/Types/Maps/MapEnumerator.cs index ef9aa63f..bbc03cfa 100644 --- a/YDotNet/Document/Types/Maps/MapEnumerator.cs +++ b/YDotNet/Document/Types/Maps/MapEnumerator.cs @@ -6,7 +6,7 @@ namespace YDotNet.Document.Types.Maps; /// -/// Represents the iterator to provide instances of . +/// Represents the iterator to provide instances of key value pairs. /// internal class MapEnumerator : IEnumerator> { diff --git a/YDotNet/Document/Types/Maps/MapIterator.cs b/YDotNet/Document/Types/Maps/MapIterator.cs index 18ce54f6..1371fb09 100644 --- a/YDotNet/Document/Types/Maps/MapIterator.cs +++ b/YDotNet/Document/Types/Maps/MapIterator.cs @@ -6,12 +6,12 @@ namespace YDotNet.Document.Types.Maps; /// -/// Represents an enumerable to read instances from a . +/// Represents an enumerable to read key value pairs from a . /// /// /// Two important details about . ///
    -///
  • The instances are unordered when iterating;
  • +///
  • The entries instances are unordered when iterating;
  • ///
  • /// The iterator can't be reused. If needed, use to accumulate /// values. diff --git a/YDotNet/Document/Types/Texts/Text.cs b/YDotNet/Document/Types/Texts/Text.cs index 3854f3b9..5fef8f32 100644 --- a/YDotNet/Document/Types/Texts/Text.cs +++ b/YDotNet/Document/Types/Texts/Text.cs @@ -15,11 +15,21 @@ namespace YDotNet.Document.Types.Texts; ///
public class Text : Branch { - private readonly EventSubscriptions subscriptions = new(); + private readonly EventSubscriber onObserve; internal Text(nint handle, Doc doc) : base(handle, doc) { + onObserve = new EventSubscriber( + handle, + (text, action) => + { + TextChannel.ObserveCallback callback = (_, eventHandle) => + action(new TextEvent(eventHandle, Doc)); + + return (TextChannel.Observe(text, nint.Zero, callback), callback); + }, + TextChannel.Unobserve); } /// @@ -136,17 +146,7 @@ public uint Length(Transaction transaction) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable Observe(Action action) { - TextChannel.ObserveCallback callback = (_, eventHandle) => action(new TextEvent(eventHandle, Doc)); - - var subscriptionId = TextChannel.Observe( - Handle, - nint.Zero, - callback); - - return subscriptions.Add(callback, () => - { - TextChannel.Unobserve(Handle, subscriptionId); - }); + return onObserve.Subscribe(action); } /// diff --git a/YDotNet/Document/Types/Texts/TextChunks.cs b/YDotNet/Document/Types/Texts/TextChunks.cs index 3a37b548..0757e611 100644 --- a/YDotNet/Document/Types/Texts/TextChunks.cs +++ b/YDotNet/Document/Types/Texts/TextChunks.cs @@ -10,7 +10,6 @@ namespace YDotNet.Document.Types.Texts; /// public class TextChunks : ReadOnlyCollection { - internal TextChunks(nint handle, uint length, Doc doc) : base(ReadItems(handle, length, doc)) { diff --git a/YDotNet/Document/Types/XmlElements/XmlElement.cs b/YDotNet/Document/Types/XmlElements/XmlElement.cs index 7a62eb5e..d7033b64 100644 --- a/YDotNet/Document/Types/XmlElements/XmlElement.cs +++ b/YDotNet/Document/Types/XmlElements/XmlElement.cs @@ -16,11 +16,21 @@ namespace YDotNet.Document.Types.XmlElements; /// public class XmlElement : Branch { - private readonly EventSubscriptions subscriptions = new(); + private readonly EventSubscriber onObserve; internal XmlElement(nint handle, Doc doc) : base(handle, doc) { + onObserve = new EventSubscriber( + handle, + (xmlElement, action) => + { + XmlElementChannel.ObserveCallback callback = (_, eventHandle) => + action(new XmlElementEvent(eventHandle, Doc)); + + return (XmlElementChannel.Observe(xmlElement, nint.Zero, callback), callback); + }, + XmlElementChannel.Unobserve); } /// @@ -281,16 +291,6 @@ public XmlTreeWalker TreeWalker(Transaction transaction) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable Observe(Action action) { - XmlElementChannel.ObserveCallback callback = (_, eventHandle) => action(new XmlElementEvent(eventHandle, Doc)); - - var subscriptionId = XmlElementChannel.Observe( - Handle, - nint.Zero, - callback); - - return subscriptions.Add(callback, () => - { - XmlElementChannel.Unobserve(Handle, subscriptionId); - }); + return onObserve.Subscribe(action); } } diff --git a/YDotNet/Document/Types/XmlTexts/XmlText.cs b/YDotNet/Document/Types/XmlTexts/XmlText.cs index b3740618..ec38a594 100644 --- a/YDotNet/Document/Types/XmlTexts/XmlText.cs +++ b/YDotNet/Document/Types/XmlTexts/XmlText.cs @@ -16,11 +16,21 @@ namespace YDotNet.Document.Types.XmlTexts; /// public class XmlText : Branch { - private readonly EventSubscriptions subscriptions = new(); + private readonly EventSubscriber onObserve; internal XmlText(nint handle, Doc doc) : base(handle, doc) { + onObserve = new EventSubscriber( + handle, + (xmlText, action) => + { + XmlTextChannel.ObserveCallback callback = (_, eventHandle) => + action(new XmlTextEvent(eventHandle, Doc)); + + return (XmlTextChannel.Observe(xmlText, nint.Zero, callback), callback); + }, + XmlTextChannel.Unobserve); } /// @@ -213,17 +223,7 @@ public void Format(Transaction transaction, uint index, uint length, Input attri /// The subscription for the event. It may be used to unsubscribe later. public IDisposable Observe(Action action) { - XmlTextChannel.ObserveCallback callback = (_, eventHandle) => action(new XmlTextEvent(eventHandle, Doc)); - - var subscriptionId = XmlTextChannel.Observe( - Handle, - nint.Zero, - callback); - - return subscriptions.Add(callback, () => - { - XmlTextChannel.Unobserve(Handle, subscriptionId); - }); + return onObserve.Subscribe(action); } /// diff --git a/YDotNet/Document/UndoManagers/Events/UndoEvent.cs b/YDotNet/Document/UndoManagers/Events/UndoEvent.cs index a89cce46..2978bf09 100644 --- a/YDotNet/Document/UndoManagers/Events/UndoEvent.cs +++ b/YDotNet/Document/UndoManagers/Events/UndoEvent.cs @@ -1,4 +1,5 @@ using YDotNet.Document.State; +using YDotNet.Native.UndoManager.Events; namespace YDotNet.Document.UndoManagers.Events; @@ -7,19 +8,20 @@ namespace YDotNet.Document.UndoManagers.Events; /// public class UndoEvent { - /// - /// Initializes a new instance of the class. - /// - /// The kind of the event. - /// The origin of the event. - /// The entries for inserted content. - /// The entries for deleted content. - internal UndoEvent(UndoEventKind kind, byte[]? origin, DeleteSet insertions, DeleteSet deletions) + internal UndoEvent(UndoEventNative native) { - Kind = kind; - Origin = origin; - Insertions = insertions; - Deletions = deletions; + Origin = native.Origin(); + + Kind = native.KindNative switch + { + UndoEventKindNative.Undo => UndoEventKind.Undo, + UndoEventKindNative.Redo => UndoEventKind.Redo, + _ => throw new NotSupportedException($"The value \"{native.KindNative}\" for {nameof(UndoEventKindNative)} is not supported."), + }; + + Insertions = new DeleteSet(native.Insertions); + + Deletions = new DeleteSet(native.Deletions); } /// diff --git a/YDotNet/Document/UndoManagers/UndoManager.cs b/YDotNet/Document/UndoManagers/UndoManager.cs index a8ab313b..f3a60f41 100644 --- a/YDotNet/Document/UndoManagers/UndoManager.cs +++ b/YDotNet/Document/UndoManagers/UndoManager.cs @@ -12,7 +12,8 @@ namespace YDotNet.Document.UndoManagers; /// public class UndoManager : UnmanagedResource { - private readonly EventSubscriptions subscriptions = new(); + private readonly EventSubscriber onAdded; + private readonly EventSubscriber onPopped; /// /// Initializes a new instance of the class. @@ -23,20 +24,45 @@ public class UndoManager : UnmanagedResource public UndoManager(Doc doc, Branch branch, UndoManagerOptions? options = null) : base(Create(doc, branch, options)) { + onAdded = new EventSubscriber( + Handle, + (owner, action) => + { + UndoManagerChannel.ObserveAddedCallback callback = + (_, undoEvent) => action(new UndoEvent(undoEvent)); + + return (UndoManagerChannel.ObserveAdded(Handle, nint.Zero, callback), callback); + }, + (owner, s) => UndoManagerChannel.UnobserveAdded(owner, s)); + + onPopped = new EventSubscriber( + Handle, + (owner, action) => + { + UndoManagerChannel.ObservePoppedCallback callback = + (_, undoEvent) => action(new UndoEvent(undoEvent)); + + return (UndoManagerChannel.ObservePopped(Handle, nint.Zero, callback), callback); + }, + (owner, s) => UndoManagerChannel.UnobservePopped(owner, s)); } private static nint Create(Doc doc, Branch branch, UndoManagerOptions? options) { - var unsafeOptions = MemoryWriter.WriteStruct(UndoManagerOptionsNative.From(options)); + var unsafeOptions = MemoryWriter.WriteStruct(options?.ToNative() ?? default); return UndoManagerChannel.NewWithOptions(doc.Handle, branch.Handle, unsafeOptions.Handle); } + /// + /// Finalizes an instance of the class. + /// ~UndoManager() { Dispose(false); } + /// protected internal override void DisposeCore(bool disposing) { UndoManagerChannel.Destroy(Handle); @@ -51,17 +77,7 @@ protected internal override void DisposeCore(bool disposing) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable ObserveAdded(Action action) { - UndoManagerChannel.ObserveAddedCallback callback = (_, undoEvent) => action(undoEvent.ToUndoEvent()); - - var subscriptionId = UndoManagerChannel.ObserveAdded( - Handle, - nint.Zero, - callback); - - return subscriptions.Add(callback, () => - { - UndoManagerChannel.UnobserveAdded(Handle, subscriptionId); - }); + return onAdded.Subscribe(action); } /// @@ -72,17 +88,7 @@ public IDisposable ObserveAdded(Action action) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable ObservePopped(Action action) { - UndoManagerChannel.ObservePoppedCallback callback = (_, undoEvent) => action(undoEvent.ToUndoEvent()); - - var subscriptionId = UndoManagerChannel.ObservePopped( - Handle, - nint.Zero, - callback); - - return subscriptions.Add(callback, () => - { - UndoManagerChannel.UnobservePopped(Handle, subscriptionId); - }); + return onPopped.Subscribe(action); } /// diff --git a/YDotNet/Document/UndoManagers/UndoManagerOptions.cs b/YDotNet/Document/UndoManagers/UndoManagerOptions.cs index 7970d893..91517466 100644 --- a/YDotNet/Document/UndoManagers/UndoManagerOptions.cs +++ b/YDotNet/Document/UndoManagers/UndoManagerOptions.cs @@ -1,3 +1,5 @@ +using YDotNet.Native.UndoManager; + namespace YDotNet.Document.UndoManagers; /// @@ -12,4 +14,12 @@ public class UndoManagerOptions /// The updates are grouped together in time-constrained snapshots. /// public uint CaptureTimeoutMilliseconds { get; init; } + + internal UndoManagerOptionsNative ToNative() + { + return new UndoManagerOptionsNative + { + CaptureTimeoutMilliseconds = CaptureTimeoutMilliseconds + }; + } } diff --git a/YDotNet/Infrastructure/TypeBase.cs b/YDotNet/Infrastructure/TypeBase.cs index ebe657da..8e893a5c 100644 --- a/YDotNet/Infrastructure/TypeBase.cs +++ b/YDotNet/Infrastructure/TypeBase.cs @@ -1,20 +1,33 @@ namespace YDotNet.Infrastructure; +/// +/// Base class for all managed types. +/// public abstract class TypeBase : ITypeBase { private bool isDeleted; + /// + /// Throws an exception if the type is deleted. + /// protected void ThrowIfDeleted() { - + if (isDeleted) + { + throw new ObjectDisposedException(GetType().Name); + } } + /// public void MarkDeleted() { - throw new NotImplementedException(); + isDeleted = true; } } +/// +/// Base class for all managed types. +/// public interface ITypeBase { /// diff --git a/YDotNet/Native/Document/DocOptionsNative.cs b/YDotNet/Native/Document/DocOptionsNative.cs index fdcfde10..9de6fbe0 100644 --- a/YDotNet/Native/Document/DocOptionsNative.cs +++ b/YDotNet/Native/Document/DocOptionsNative.cs @@ -1,6 +1,4 @@ using System.Runtime.InteropServices; -using YDotNet.Document.Options; -using YDotNet.Infrastructure; namespace YDotNet.Native.Document; @@ -20,22 +18,4 @@ internal readonly struct DocOptionsNative public byte AutoLoad { get; init; } public byte ShouldLoad { get; init; } - - public static DocOptionsNative From(DocOptions options) - { - // We can never release the memory because y-crdt just receives a pointer to that. - var unsafeGuid = MemoryWriter.WriteUtf8String(options.Guid); - var unsafeCollection = MemoryWriter.WriteUtf8String(options.CollectionId); - - return new DocOptionsNative - { - Id = options.Id ?? 0, - Guid = unsafeGuid.Handle, - CollectionId = unsafeCollection.Handle, - Encoding = (byte)options.Encoding, - SkipGc = (byte)(options.SkipGarbageCollection ?? false ? 1 : 0), - AutoLoad = (byte)(options.AutoLoad ?? false ? 1 : 0), - ShouldLoad = (byte)(options.ShouldLoad ?? false ? 1 : 0) - }; - } } diff --git a/YDotNet/Native/Document/Events/AfterTransactionEventNative.cs b/YDotNet/Native/Document/Events/AfterTransactionEventNative.cs index d263b434..800f552e 100644 --- a/YDotNet/Native/Document/Events/AfterTransactionEventNative.cs +++ b/YDotNet/Native/Document/Events/AfterTransactionEventNative.cs @@ -1,5 +1,4 @@ using System.Runtime.InteropServices; -using YDotNet.Document.Events; using YDotNet.Native.Document.State; namespace YDotNet.Native.Document.Events; @@ -12,12 +11,4 @@ internal readonly struct AfterTransactionEventNative public StateVectorNative AfterState { get; } public DeleteSetNative DeleteSet { get; } - - public AfterTransactionEvent ToAfterTransactionEvent() - { - return new AfterTransactionEvent( - BeforeState.ToStateVector(), - AfterState.ToStateVector(), - DeleteSet.ToDeleteSet()); - } } diff --git a/YDotNet/Native/Document/Events/ClearEventNative.cs b/YDotNet/Native/Document/Events/ClearEventNative.cs deleted file mode 100644 index 0c13e901..00000000 --- a/YDotNet/Native/Document/Events/ClearEventNative.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Runtime.InteropServices; -using YDotNet.Document; -using YDotNet.Document.Events; - -namespace YDotNet.Native.Document.Events; - -[StructLayout(LayoutKind.Sequential)] -internal readonly struct ClearEventNative -{ - public Doc Doc { get; init; } - - public static ClearEventNative From(Doc doc) - { - return new ClearEventNative { Doc = doc }; - } - - public ClearEvent ToClearEvent() - { - return new ClearEvent(Doc); - } -} diff --git a/YDotNet/Native/Document/Events/EventBranchNative.cs b/YDotNet/Native/Document/Events/EventBranchNative.cs new file mode 100644 index 00000000..fbd572fb --- /dev/null +++ b/YDotNet/Native/Document/Events/EventBranchNative.cs @@ -0,0 +1,18 @@ +using System.Runtime.InteropServices; +using YDotNet.Infrastructure; +using YDotNet.Native.Cells.Outputs; + +namespace YDotNet.Native.Document.Events; + +[StructLayout(LayoutKind.Sequential, Size = Size)] +internal struct EventBranchNative +{ + public const int Size = 8 + OutputNative.Size; + + public uint Tag { get; } + + public nint ValueHandle(nint baseHandle) + { + return baseHandle + MemoryConstants.PointerSize; + } +} diff --git a/YDotNet/Native/Document/Events/SubDocsEventNative.cs b/YDotNet/Native/Document/Events/SubDocsEventNative.cs index 293d33ae..a6695e8b 100644 --- a/YDotNet/Native/Document/Events/SubDocsEventNative.cs +++ b/YDotNet/Native/Document/Events/SubDocsEventNative.cs @@ -1,6 +1,4 @@ using System.Runtime.InteropServices; -using YDotNet.Document; -using YDotNet.Document.Events; using YDotNet.Infrastructure; namespace YDotNet.Native.Document.Events; @@ -14,22 +12,39 @@ internal readonly struct SubDocsEventNative public uint LoadedLength { get; } - public nint Added { get; } + public nint AddedHandle { get; } - public nint Removed { get; } + public nint RemovedHandle { get; } - public nint Loaded { get; } + public nint LoadedHandle { get; } - public SubDocsEvent ToSubDocsEvent(Doc doc) + public nint[] Added() { - var nativeAdded = MemoryReader.ReadStructArray(Added, AddedLength); - var nativeRemoved = MemoryReader.ReadStructArray(Removed, RemovedLength); - var nativeLoaded = MemoryReader.ReadStructArray(Loaded, LoadedLength); + if (AddedHandle == nint.Zero || AddedLength == 0) + { + return Array.Empty(); + } - var docsAdded = nativeAdded.Select(doc.GetDoc).ToArray(); - var docsRemoved = nativeRemoved.Select(doc.GetDoc).ToArray(); - var docsLoaded = nativeLoaded.Select(doc.GetDoc).ToArray(); + return MemoryReader.ReadStructArray(AddedHandle, AddedLength); + } + + public nint[] Removed() + { + if (RemovedHandle == nint.Zero || RemovedLength == 0) + { + return Array.Empty(); + } + + return MemoryReader.ReadStructArray(RemovedHandle, RemovedLength); + } + + public nint[] Loaded() + { + if (LoadedHandle == nint.Zero || LoadedLength == 0) + { + return Array.Empty(); + } - return new SubDocsEvent(docsAdded, docsRemoved, docsLoaded); + return MemoryReader.ReadStructArray(LoadedHandle, LoadedLength); } } diff --git a/YDotNet/Native/Document/Events/UpdateEventNative.cs b/YDotNet/Native/Document/Events/UpdateEventNative.cs index f4dc2b63..91290e9b 100644 --- a/YDotNet/Native/Document/Events/UpdateEventNative.cs +++ b/YDotNet/Native/Document/Events/UpdateEventNative.cs @@ -1,5 +1,4 @@ using System.Runtime.InteropServices; -using YDotNet.Document.Events; using YDotNet.Infrastructure; namespace YDotNet.Native.Document.Events; @@ -11,15 +10,13 @@ internal readonly struct UpdateEventNative public nint Data { get; init; } - public static UpdateEventNative From(uint length, nint data) + public byte[] Bytes() { - return new UpdateEventNative { Length = length, Data = data }; + return MemoryReader.ReadBytes(Data, Length); } - public UpdateEvent ToUpdateEvent() + public static UpdateEventNative From(uint length, nint data) { - var result = MemoryReader.ReadBytes(Data, Length); - - return new UpdateEvent(result); + return new UpdateEventNative { Length = length, Data = data }; } } diff --git a/YDotNet/Native/Document/State/DeleteSetNative.cs b/YDotNet/Native/Document/State/DeleteSetNative.cs index 92835438..92fb0138 100644 --- a/YDotNet/Native/Document/State/DeleteSetNative.cs +++ b/YDotNet/Native/Document/State/DeleteSetNative.cs @@ -1,5 +1,4 @@ using System.Runtime.InteropServices; -using YDotNet.Document.State; using YDotNet.Infrastructure; namespace YDotNet.Native.Document.State; @@ -9,22 +8,17 @@ internal readonly struct DeleteSetNative { public uint EntriesCount { get; } - public nint ClientIds { get; } + public nint ClientIdsHandle { get; } - public nint Ranges { get; } + public nint RangesHandle { get; } - public DeleteSet ToDeleteSet() + public ulong[] Clients() { - var nativeClients = MemoryReader.ReadStructArray(ClientIds, EntriesCount); - var nativeRanges = MemoryReader.ReadStructArray(Ranges, EntriesCount); - - var entries = new Dictionary(); - - for (var i = 0; i < EntriesCount; i++) - { - entries.Add(nativeClients[i], nativeRanges[i].ToIdRanges()); - } + return MemoryReader.ReadStructArray(ClientIdsHandle, EntriesCount); + } - return new DeleteSet(entries); + public IdRangeSequenceNative[] Ranges() + { + return MemoryReader.ReadStructArray(RangesHandle, EntriesCount); } } diff --git a/YDotNet/Native/Document/State/IdRangeNative.cs b/YDotNet/Native/Document/State/IdRangeNative.cs index 145bd102..2024cc7d 100644 --- a/YDotNet/Native/Document/State/IdRangeNative.cs +++ b/YDotNet/Native/Document/State/IdRangeNative.cs @@ -1,5 +1,3 @@ -using YDotNet.Document.State; - namespace YDotNet.Native.Document.State; internal readonly struct IdRangeNative @@ -7,9 +5,4 @@ internal readonly struct IdRangeNative public uint Start { get; } public uint End { get; } - - public IdRange ToIdRange() - { - return new IdRange(Start, End); - } } diff --git a/YDotNet/Native/Document/State/IdRangeSequenceNative.cs b/YDotNet/Native/Document/State/IdRangeSequenceNative.cs index 53b39160..b3cdb014 100644 --- a/YDotNet/Native/Document/State/IdRangeSequenceNative.cs +++ b/YDotNet/Native/Document/State/IdRangeSequenceNative.cs @@ -1,5 +1,4 @@ using System.Runtime.InteropServices; -using YDotNet.Document.State; using YDotNet.Infrastructure; namespace YDotNet.Native.Document.State; @@ -7,14 +6,12 @@ namespace YDotNet.Native.Document.State; [StructLayout(LayoutKind.Sequential)] internal readonly struct IdRangeSequenceNative { - public uint Length { get; } + public uint SequenceLength { get; } - public nint Sequence { get; } + public nint SequenceHandle { get; } - public IdRange[] ToIdRanges() + public IdRangeNative[] Sequence() { - var rangeNatives = MemoryReader.ReadStructArray(Sequence, Length); - - return rangeNatives.Select(x => x.ToIdRange()).ToArray(); ; + return MemoryReader.ReadStructArray(SequenceHandle, SequenceLength); } } diff --git a/YDotNet/Native/Document/State/StateVectorNative.cs b/YDotNet/Native/Document/State/StateVectorNative.cs index 5df91373..48c79400 100644 --- a/YDotNet/Native/Document/State/StateVectorNative.cs +++ b/YDotNet/Native/Document/State/StateVectorNative.cs @@ -1,4 +1,3 @@ -using YDotNet.Document.State; using YDotNet.Infrastructure; namespace YDotNet.Native.Document.State; @@ -7,22 +6,17 @@ internal readonly struct StateVectorNative { public uint EntriesCount { get; } - public nint ClientIds { get; } + public nint ClientIdsHandle { get; } - public nint Clocks { get; } + public nint ClocksHandle { get; } - public StateVector ToStateVector() + public ulong[] ClientIds() { - var nativeClients = MemoryReader.ReadStructArray(ClientIds, EntriesCount); - var nativeClocks = MemoryReader.ReadStructArray(Clocks, EntriesCount); - - var entries = new Dictionary(); - - for (var i = 0; i < EntriesCount; i++) - { - entries.Add(nativeClients[i], nativeClocks[i]); - } + return MemoryReader.ReadStructArray(ClientIdsHandle, EntriesCount); + } - return new StateVector(entries); + public uint[] Clocks() + { + return MemoryReader.ReadStructArray(ClientIdsHandle, EntriesCount); } } diff --git a/YDotNet/Native/Types/Events/EventKeyChangeNative.cs b/YDotNet/Native/Types/Events/EventKeyChangeNative.cs index 130833e3..cd56e107 100644 --- a/YDotNet/Native/Types/Events/EventKeyChangeNative.cs +++ b/YDotNet/Native/Types/Events/EventKeyChangeNative.cs @@ -1,7 +1,4 @@ using System.Runtime.InteropServices; -using YDotNet.Document; -using YDotNet.Document.Cells; -using YDotNet.Document.Types.Events; using YDotNet.Infrastructure; namespace YDotNet.Native.Types.Events; @@ -9,7 +6,7 @@ namespace YDotNet.Native.Types.Events; [StructLayout(LayoutKind.Sequential)] internal readonly struct EventKeyChangeNative { - public nint Key { get; } + public nint KeyHandle { get; } public EventKeyChangeTagNative TagNative { get; } @@ -17,28 +14,8 @@ internal readonly struct EventKeyChangeNative public nint NewValue { get; } - public EventKeyChange ToEventKeyChange(Doc doc) + public string Key() { - var tag = TagNative switch - { - EventKeyChangeTagNative.Add => EventKeyChangeTag.Add, - EventKeyChangeTagNative.Remove => EventKeyChangeTag.Remove, - EventKeyChangeTagNative.Update => EventKeyChangeTag.Update, - _ => throw new NotSupportedException($"The value \"{TagNative}\" for {nameof(EventKeyChangeTagNative)} is not supported."), - }; - - var key = MemoryReader.ReadUtf8String(Key); - - var oldOutput = - OldValue != nint.Zero ? - new Output(OldValue, doc) : - null; - - var newOutput = - NewValue != nint.Zero ? - new Output(NewValue, doc) : - null; - - return new EventKeyChange(key, tag, oldOutput, newOutput); + return MemoryReader.ReadUtf8String(KeyHandle); } } diff --git a/YDotNet/Native/UndoManager/Events/UndoEventNative.cs b/YDotNet/Native/UndoManager/Events/UndoEventNative.cs index fec91a40..d77d6b57 100644 --- a/YDotNet/Native/UndoManager/Events/UndoEventNative.cs +++ b/YDotNet/Native/UndoManager/Events/UndoEventNative.cs @@ -1,4 +1,3 @@ -using YDotNet.Document.UndoManagers.Events; using YDotNet.Infrastructure; using YDotNet.Native.Document.State; @@ -8,7 +7,7 @@ internal struct UndoEventNative { public UndoEventKindNative KindNative { get; set; } - public nint Origin { get; set; } + public nint OriginHandle { get; set; } public uint OriginLength { get; set; } @@ -16,17 +15,13 @@ internal struct UndoEventNative public DeleteSetNative Deletions { get; set; } - public UndoEvent ToUndoEvent() + public byte[]? Origin() { - var kind = KindNative switch + if (OriginHandle == nint.Zero || OriginLength == 0) { - UndoEventKindNative.Undo => UndoEventKind.Undo, - UndoEventKindNative.Redo => UndoEventKind.Redo, - _ => throw new NotSupportedException($"The value \"{KindNative}\" for {nameof(UndoEventKindNative)} is not supported."), - }; + return null; + } - var origin = MemoryReader.TryReadBytes(Origin, OriginLength); - - return new UndoEvent(kind, origin, Insertions.ToDeleteSet(), Deletions.ToDeleteSet()); + return MemoryReader.ReadBytes(OriginHandle, OriginLength); } } diff --git a/YDotNet/Native/UndoManager/UndoManagerOptionsNative.cs b/YDotNet/Native/UndoManager/UndoManagerOptionsNative.cs index 0f515fbf..227d3752 100644 --- a/YDotNet/Native/UndoManager/UndoManagerOptionsNative.cs +++ b/YDotNet/Native/UndoManager/UndoManagerOptionsNative.cs @@ -1,16 +1,6 @@ -using YDotNet.Document.UndoManagers; - namespace YDotNet.Native.UndoManager; internal struct UndoManagerOptionsNative { internal uint CaptureTimeoutMilliseconds { get; set; } - - internal static UndoManagerOptionsNative From(UndoManagerOptions? options) - { - return new UndoManagerOptionsNative - { - CaptureTimeoutMilliseconds = options?.CaptureTimeoutMilliseconds ?? 0 - }; - } } From 21600326d415177b747e10e47cb2ddc1e2448542 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 13 Oct 2023 08:24:33 +0200 Subject: [PATCH 092/186] Just some stuff. --- Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs | 4 +- .../YDotNet.Tests.Unit/Arrays/IterateTests.cs | 2 +- .../YDotNet.Tests.Unit/Arrays/ObserveTests.cs | 2 +- Tests/YDotNet.Tests.Unit/Maps/GetTests.cs | 28 +-- Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs | 2 +- Tests/YDotNet.Tests.Unit/Program.cs | 6 +- .../Texts/InsertEmbedTests.cs | 4 +- .../UndoManagers/RedoTests.cs | 14 +- .../XmlElements/IterateTests.cs | 8 +- .../XmlTexts/IterateTests.cs | 8 +- YDotNet/Document/Cells/JsonArray.cs | 12 +- YDotNet/Document/Cells/JsonObject.cs | 10 +- YDotNet/Document/Cells/Output.cs | 187 +++++++++--------- .../Cells/{OutputType.cs => OutputTage.cs} | 2 +- YDotNet/Document/Doc.cs | 107 +++++++--- YDotNet/Document/Events/EventSubscriber.cs | 6 +- YDotNet/Document/Events/SubDocsEvent.cs | 9 +- YDotNet/Document/Transactions/Transaction.cs | 14 +- YDotNet/Document/Types/Arrays/Array.cs | 28 ++- .../Types/Arrays/Events/ArrayEvent.cs | 2 +- YDotNet/Document/Types/Branches/Branch.cs | 13 +- YDotNet/Document/Types/Events/EventChange.cs | 2 +- YDotNet/Document/Types/Events/EventChanges.cs | 4 +- YDotNet/Document/Types/Events/EventDelta.cs | 4 +- YDotNet/Document/Types/Events/EventDeltas.cs | 4 +- .../Document/Types/Events/EventKeyChange.cs | 4 +- YDotNet/Document/Types/Events/EventKeys.cs | 2 +- YDotNet/Document/Types/Events/EventPath.cs | 4 +- .../Document/Types/Events/EventPathSegment.cs | 27 +-- .../Document/Types/Maps/Events/MapEvent.cs | 2 +- YDotNet/Document/Types/Maps/Map.cs | 18 +- YDotNet/Document/Types/Maps/MapEnumerator.cs | 4 +- .../Document/Types/Texts/Events/TextEvent.cs | 2 +- YDotNet/Document/Types/Texts/Text.cs | 22 ++- YDotNet/Document/Types/Texts/TextChunk.cs | 4 +- YDotNet/Document/Types/Texts/TextChunks.cs | 2 +- .../XmlElements/Events/XmlElementEvent.cs | 2 +- .../Types/XmlElements/XmlAttribute.cs | 41 ---- .../XmlElements/XmlAttributeEnumerator.cs | 27 ++- .../Types/XmlElements/XmlAttributeIterator.cs | 4 +- .../Document/Types/XmlElements/XmlElement.cs | 57 ++++-- .../Types/XmlTexts/Events/XmlTextEvent.cs | 2 +- YDotNet/Document/Types/XmlTexts/XmlText.cs | 32 ++- YDotNet/Infrastructure/MemoryReader.cs | 41 ++-- YDotNet/Infrastructure/TypeBase.cs | 21 +- YDotNet/Infrastructure/TypeCache.cs | 39 +++- .../Document/Events/SubDocsEventNative.cs | 6 +- .../Native/Document/State/DeleteSetNative.cs | 4 +- .../Document/State/IdRangeSequenceNative.cs | 2 +- .../Document/State/StateVectorNative.cs | 4 +- .../Native/Types/Events/EventChangeNative.cs | 2 +- .../Native/Types/Events/EventDeltaNative.cs | 2 +- .../Types/Events/EventPathSegmentNative.cs | 17 ++ YDotNet/Native/Types/Texts/TextChunkNative.cs | 2 +- YDotNet/Native/Types/XmlAttributeNative.cs | 22 +++ 55 files changed, 544 insertions(+), 356 deletions(-) rename YDotNet/Document/Cells/{OutputType.cs => OutputTage.cs} (99%) delete mode 100644 YDotNet/Document/Types/XmlElements/XmlAttribute.cs create mode 100644 YDotNet/Native/Types/Events/EventPathSegmentNative.cs create mode 100644 YDotNet/Native/Types/XmlAttributeNative.cs diff --git a/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs b/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs index f3ac2f93..1f7660d0 100644 --- a/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs +++ b/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs @@ -36,7 +36,7 @@ public void GetAtBeginning() // Assert Assert.That(output, Is.Not.Null); - Assert.That(output.Tag, Is.EqualTo(OutputTage.Bool)); + Assert.That(output.Tag, Is.EqualTo(OutputTag.Bool)); } [Test] @@ -52,7 +52,7 @@ public void GetAtMiddle() // Assert Assert.That(output, Is.Not.Null); - Assert.That(output.Tag, Is.EqualTo(OutputTage.Undefined)); + Assert.That(output.Tag, Is.EqualTo(OutputTag.Undefined)); } [Test] diff --git a/Tests/YDotNet.Tests.Unit/Arrays/IterateTests.cs b/Tests/YDotNet.Tests.Unit/Arrays/IterateTests.cs index 9375fb9e..9aa0ada2 100644 --- a/Tests/YDotNet.Tests.Unit/Arrays/IterateTests.cs +++ b/Tests/YDotNet.Tests.Unit/Arrays/IterateTests.cs @@ -75,6 +75,6 @@ public void IteratesOnMultiItem() Assert.That(values.Length, Is.EqualTo(expected: 3)); Assert.That(values[0].Long, Is.EqualTo(expected: 2469L)); Assert.That(values[1].Boolean, Is.False); - Assert.That(values[2].Tag, Is.EqualTo(OutputTage.Undefined)); + Assert.That(values[2].Tag, Is.EqualTo(OutputTag.Undefined)); } } diff --git a/Tests/YDotNet.Tests.Unit/Arrays/ObserveTests.cs b/Tests/YDotNet.Tests.Unit/Arrays/ObserveTests.cs index ac7e82b9..6d022395 100644 --- a/Tests/YDotNet.Tests.Unit/Arrays/ObserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/Arrays/ObserveTests.cs @@ -113,7 +113,7 @@ public void ObserveHasDeltasWhenMoved() Assert.That(eventChanges.ElementAt(index: 0).Tag, Is.EqualTo(EventChangeTag.Add)); Assert.That(eventChanges.ElementAt(index: 0).Length, Is.EqualTo(expected: 1)); - Assert.That(eventChanges.ElementAt(index: 0).Values.First().Tag, Is.EqualTo(OutputTage.Undefined)); + Assert.That(eventChanges.ElementAt(index: 0).Values.First().Tag, Is.EqualTo(OutputTag.Undefined)); Assert.That(eventChanges.ElementAt(index: 1).Tag, Is.EqualTo(EventChangeTag.Retain)); Assert.That(eventChanges.ElementAt(index: 1).Length, Is.EqualTo(expected: 2)); diff --git a/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs b/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs index 54efac75..1e7a0b61 100644 --- a/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs +++ b/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs @@ -95,7 +95,7 @@ public void GetBytes() // Assert Assert.That(value1, Is.EqualTo(new byte[] { 2, 4, 6, 9 })); - Assert.That(value2.Tag, Is.EqualTo(OutputTage.Bool)); + Assert.That(value2.Tag, Is.EqualTo(OutputTag.Bool)); } [Test] @@ -121,7 +121,7 @@ public void GetCollection() Assert.That(value1.Count, Is.EqualTo(expected: 2)); Assert.That(value1[0].Long, Is.EqualTo(expected: 2469)); Assert.That(value1[1].Long, Is.EqualTo(expected: -420L)); - Assert.That(value2.Tag, Is.EqualTo(OutputTage.Bool)); + Assert.That(value2.Tag, Is.EqualTo(OutputTag.Bool)); } [Test] @@ -146,7 +146,7 @@ public void GetObject() Assert.That(value1.Keys.Count, Is.EqualTo(expected: 2)); Assert.That(value1["star-⭐"].Long, Is.EqualTo(expected: 2469)); Assert.That(value1["moon-🌕"].Long, Is.EqualTo(expected: -420L)); - Assert.That(value2.Tag, Is.EqualTo(OutputTage.Bool)); + Assert.That(value2.Tag, Is.EqualTo(OutputTag.Bool)); } [Test] @@ -165,9 +165,9 @@ public void GetNull() var value3 = map.Get(transaction, "value3"); // Assert - Assert.That(value1.Tag, Is.EqualTo(OutputTage.Null)); - Assert.That(value2.Tag, Is.Not.EqualTo(OutputTage.Null)); - Assert.That(value3.Tag, Is.Not.EqualTo(OutputTage.Null)); + Assert.That(value1.Tag, Is.EqualTo(OutputTag.Null)); + Assert.That(value2.Tag, Is.Not.EqualTo(OutputTag.Null)); + Assert.That(value3.Tag, Is.Not.EqualTo(OutputTag.Null)); } [Test] @@ -186,9 +186,9 @@ public void GetUndefined() var value3 = map.Get(transaction, "value3"); // Assert - Assert.That(value1.Tag, Is.EqualTo(OutputTage.Undefined)); - Assert.That(value2.Tag, Is.Not.EqualTo(OutputTage.Undefined)); - Assert.That(value3.Tag, Is.Not.EqualTo(OutputTage.Undefined)); + Assert.That(value1.Tag, Is.EqualTo(OutputTag.Undefined)); + Assert.That(value2.Tag, Is.Not.EqualTo(OutputTag.Undefined)); + Assert.That(value3.Tag, Is.Not.EqualTo(OutputTag.Undefined)); } [Test] @@ -207,7 +207,7 @@ public void GetText() // Assert Assert.That(value1, Is.Not.Null); Assert.That(value1.String(transaction), Is.EqualTo("Lucas")); - Assert.That(value2.Tag, Is.EqualTo(OutputTage.Bool)); + Assert.That(value2.Tag, Is.EqualTo(OutputTag.Bool)); } [Test] @@ -231,7 +231,7 @@ public void GetArray() // Assert Assert.That(value1, Is.Not.Null); Assert.That(value1.Length, Is.EqualTo(expected: 2)); - Assert.That(value2.Tag, Is.EqualTo(OutputTage.Null)); + Assert.That(value2.Tag, Is.EqualTo(OutputTag.Null)); } [Test] @@ -257,7 +257,7 @@ public void GetMap() Assert.That(value1.Length(transaction), Is.EqualTo(expected: 2)); Assert.That(value1.Get(transaction, "value1-1").Long, Is.EqualTo(expected: 2469L)); Assert.That(value1.Get(transaction, "value1-2").Long, Is.EqualTo(expected: -420L)); - Assert.That(value2.Tag, Is.EqualTo(OutputTage.Bool)); + Assert.That(value2.Tag, Is.EqualTo(OutputTag.Bool)); } [Test] @@ -276,7 +276,7 @@ public void GetXmlElement() // Assert Assert.That(value1, Is.Not.Null); Assert.That(value1.Tag, Is.EqualTo("person")); - Assert.That(value2.Tag, Is.EqualTo(OutputTage.Null)); + Assert.That(value2.Tag, Is.EqualTo(OutputTag.Null)); } [Test] @@ -295,7 +295,7 @@ public void GetXmlText() // Assert Assert.That(value1, Is.Not.Null); Assert.That(value1.Length(transaction), Is.EqualTo(expected: 5)); - Assert.That(value2.Tag, Is.EqualTo(OutputTage.Null)); + Assert.That(value2.Tag, Is.EqualTo(OutputTag.Null)); } [Test] diff --git a/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs b/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs index 5f5d2419..73e9b761 100644 --- a/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs +++ b/Tests/YDotNet.Tests.Unit/Maps/InsertTests.cs @@ -323,7 +323,7 @@ public void InsertDifferentTypeOnExistingKey() transaction.Commit(); // Assert - Assert.That(value.Tag, Is.EqualTo(OutputTage.String)); + Assert.That(value.Tag, Is.EqualTo(OutputTag.String)); Assert.That(value.String, Is.EqualTo("Lucas")); Assert.That(length, Is.EqualTo(expected: 1)); } diff --git a/Tests/YDotNet.Tests.Unit/Program.cs b/Tests/YDotNet.Tests.Unit/Program.cs index 5c1ecc68..f49447eb 100644 --- a/Tests/YDotNet.Tests.Unit/Program.cs +++ b/Tests/YDotNet.Tests.Unit/Program.cs @@ -9,6 +9,7 @@ public class Program { public static void Main() { + /* var doc = new Doc(); var a = doc.Map("A"); Map b; @@ -19,7 +20,10 @@ public static void Main() b = a.Get(t, "B").Map; b.Insert(t, "C", Input.Double(1)); + } + using (var t = doc.WriteTransaction()) + { a.Remove(t, "B"); } @@ -36,7 +40,7 @@ public static void Main() b.Insert(t, "D", Input.Double(1)); var length = b.Length(t); - } + }*/ var types = typeof(Program).Assembly.GetTypes(); diff --git a/Tests/YDotNet.Tests.Unit/Texts/InsertEmbedTests.cs b/Tests/YDotNet.Tests.Unit/Texts/InsertEmbedTests.cs index c1fa8048..241cb931 100644 --- a/Tests/YDotNet.Tests.Unit/Texts/InsertEmbedTests.cs +++ b/Tests/YDotNet.Tests.Unit/Texts/InsertEmbedTests.cs @@ -147,7 +147,7 @@ public void InsertNullEmbed() var chunks = text.Chunks(transaction); Assert.That(chunks.Count, Is.EqualTo(expected: 3)); - Assert.That(chunks.ElementAt(index: 1).Data.Tag, Is.EqualTo(OutputTage.Null)); + Assert.That(chunks.ElementAt(index: 1).Data.Tag, Is.EqualTo(OutputTag.Null)); } [Test] @@ -163,7 +163,7 @@ public void InsertUndefinedEmbed() var chunks = text.Chunks(transaction); Assert.That(chunks.Count, Is.EqualTo(expected: 3)); - Assert.That(chunks.ElementAt(index: 1).Data.Tag, Is.EqualTo(OutputTage.Undefined)); + Assert.That(chunks.ElementAt(index: 1).Data.Tag, Is.EqualTo(OutputTag.Undefined)); } [Test] diff --git a/Tests/YDotNet.Tests.Unit/UndoManagers/RedoTests.cs b/Tests/YDotNet.Tests.Unit/UndoManagers/RedoTests.cs index 68a5e933..0b73b394 100644 --- a/Tests/YDotNet.Tests.Unit/UndoManagers/RedoTests.cs +++ b/Tests/YDotNet.Tests.Unit/UndoManagers/RedoTests.cs @@ -170,7 +170,7 @@ public void RedoAddingAndUpdatingAndRemovingContentOnMap() // Assert Assert.That(length, Is.EqualTo(expected: 4)); - Assert.That(value2.Tag, Is.EqualTo(OutputTage.Undefined)); + Assert.That(value2.Tag, Is.EqualTo(OutputTag.Undefined)); Assert.That(result, Is.True); // Act (remove, undo, and redo) @@ -221,8 +221,8 @@ public void RedoAddingAndUpdatingAndRemovingContentOnXmlText() // Assert Assert.That(text, Is.EqualTo("Luctrueas 0x02040609Viana")); Assert.That(attributes.Length, Is.EqualTo(expected: 2)); - Assert.That(attributes.First(x => x.Name == "bold").Value, Is.EqualTo("true")); - Assert.That(attributes.First(x => x.Name == "italic").Value, Is.EqualTo("false")); + Assert.That(attributes.First(x => x.Key == "bold").Value, Is.EqualTo("true")); + Assert.That(attributes.First(x => x.Key == "italic").Value, Is.EqualTo("false")); Assert.That(result, Is.True); // Act (remove text, attribute, and embed, undo, and redo) @@ -241,7 +241,7 @@ public void RedoAddingAndUpdatingAndRemovingContentOnXmlText() // Assert Assert.That(text, Is.EqualTo("0x02040609Viana")); Assert.That(attributes.Length, Is.EqualTo(expected: 1)); - Assert.That(attributes.First(x => x.Name == "italic").Value, Is.EqualTo("false")); + Assert.That(attributes.First(x => x.Key == "italic").Value, Is.EqualTo("false")); Assert.That(result, Is.True); } @@ -275,8 +275,8 @@ public void RedoAddingAndUpdatingAndRemovingContentOnXmlElement() // Assert Assert.That(length, Is.EqualTo(expected: 4)); Assert.That(attributes.Length, Is.EqualTo(expected: 2)); - Assert.That(attributes.First(x => x.Name == "bold").Value, Is.EqualTo("true")); - Assert.That(attributes.First(x => x.Name == "italic").Value, Is.EqualTo("false")); + Assert.That(attributes.First(x => x.Key == "bold").Value, Is.EqualTo("true")); + Assert.That(attributes.First(x => x.Key == "italic").Value, Is.EqualTo("false")); Assert.That(result, Is.True); // Act (remove text, attribute, and element, undo, and redo) @@ -295,7 +295,7 @@ public void RedoAddingAndUpdatingAndRemovingContentOnXmlElement() // Assert Assert.That(length, Is.EqualTo(expected: 2)); Assert.That(attributes.Length, Is.EqualTo(expected: 1)); - Assert.That(attributes.First(x => x.Name == "bold").Value, Is.EqualTo("true")); + Assert.That(attributes.First(x => x.Key == "bold").Value, Is.EqualTo("true")); Assert.That(result, Is.True); } } diff --git a/Tests/YDotNet.Tests.Unit/XmlElements/IterateTests.cs b/Tests/YDotNet.Tests.Unit/XmlElements/IterateTests.cs index 6e84d809..2c88c191 100644 --- a/Tests/YDotNet.Tests.Unit/XmlElements/IterateTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlElements/IterateTests.cs @@ -41,7 +41,7 @@ public void IteratesOnSingleItem() // Assert Assert.That(values.Length, Is.EqualTo(expected: 1)); - Assert.That(values.First(x => x.Name == "href").Value, Is.EqualTo("https://lsviana.github.io/")); + Assert.That(values.First(x => x.Key == "href").Value, Is.EqualTo("https://lsviana.github.io/")); } [Test] @@ -65,8 +65,8 @@ public void IteratesOnMultiItem() // Assert Assert.That(values.Length, Is.EqualTo(expected: 3)); - Assert.That(values.First(x => x.Name == "href").Value, Is.EqualTo("https://lsviana.github.io/")); - Assert.That(values.First(x => x.Name == "as").Value, Is.EqualTo("document")); - Assert.That(values.First(x => x.Name == "rel").Value, Is.EqualTo("preload")); + Assert.That(values.First(x => x.Key == "href").Value, Is.EqualTo("https://lsviana.github.io/")); + Assert.That(values.First(x => x.Key == "as").Value, Is.EqualTo("document")); + Assert.That(values.First(x => x.Key == "rel").Value, Is.EqualTo("preload")); } } diff --git a/Tests/YDotNet.Tests.Unit/XmlTexts/IterateTests.cs b/Tests/YDotNet.Tests.Unit/XmlTexts/IterateTests.cs index affe77b4..4af4bbf6 100644 --- a/Tests/YDotNet.Tests.Unit/XmlTexts/IterateTests.cs +++ b/Tests/YDotNet.Tests.Unit/XmlTexts/IterateTests.cs @@ -41,7 +41,7 @@ public void IteratesOnSingleItem() // Assert Assert.That(values.Length, Is.EqualTo(expected: 1)); - Assert.That(values.First(x => x.Name == "href").Value, Is.EqualTo("https://lsviana.github.io/")); + Assert.That(values.First(x => x.Key == "href").Value, Is.EqualTo("https://lsviana.github.io/")); } [Test] @@ -65,8 +65,8 @@ public void IteratesOnMultiItem() // Assert Assert.That(values.Length, Is.EqualTo(expected: 3)); - Assert.That(values.First(x => x.Name == "href").Value, Is.EqualTo("https://lsviana.github.io/")); - Assert.That(values.First(x => x.Name == "as").Value, Is.EqualTo("document")); - Assert.That(values.First(x => x.Name == "rel").Value, Is.EqualTo("preload")); + Assert.That(values.First(x => x.Key == "href").Value, Is.EqualTo("https://lsviana.github.io/")); + Assert.That(values.First(x => x.Key == "as").Value, Is.EqualTo("document")); + Assert.That(values.First(x => x.Key == "rel").Value, Is.EqualTo("preload")); } } diff --git a/YDotNet/Document/Cells/JsonArray.cs b/YDotNet/Document/Cells/JsonArray.cs index 4acf22d5..81289cfc 100644 --- a/YDotNet/Document/Cells/JsonArray.cs +++ b/YDotNet/Document/Cells/JsonArray.cs @@ -1,5 +1,4 @@ using System.Collections.ObjectModel; -using System.Runtime.InteropServices; using YDotNet.Infrastructure; using YDotNet.Native.Cells.Outputs; @@ -10,22 +9,21 @@ namespace YDotNet.Document.Cells; /// public sealed class JsonArray : ReadOnlyCollection { - internal JsonArray(nint handle, uint length, Doc doc) - : base(ReadItems(handle, length, doc)) + internal JsonArray(nint handle, uint length, Doc doc, bool isDeleted) + : base(ReadItems(handle, length, doc, isDeleted)) { } - private static List ReadItems(nint handle, uint length, Doc doc) + private static List ReadItems(nint handle, uint length, Doc doc, bool isDeleted) { var collectionHandle = OutputChannel.Collection(handle); - var collectionNatives = MemoryReader.ReadIntPtrArray(collectionHandle, length, Marshal.SizeOf()); + var collectionNatives = MemoryReader.ReadPointers(collectionHandle, length); var result = new List(); foreach (var itemHandle in collectionNatives) { - // The outputs are owned by this block of allocated memory. - result.Add(new Output(itemHandle, doc)); + result.Add(new Output(itemHandle, doc, isDeleted)); } return result; diff --git a/YDotNet/Document/Cells/JsonObject.cs b/YDotNet/Document/Cells/JsonObject.cs index 1b5b21b8..63a8f814 100644 --- a/YDotNet/Document/Cells/JsonObject.cs +++ b/YDotNet/Document/Cells/JsonObject.cs @@ -10,20 +10,20 @@ namespace YDotNet.Document.Cells; /// public sealed class JsonObject : ReadOnlyDictionary { - internal JsonObject(nint handle, uint length, Doc doc) - : base(ReadItems(handle, length, doc)) + internal JsonObject(nint handle, uint length, Doc doc, bool isDeleted) + : base(ReadItems(handle, length, doc, isDeleted)) { } - private static Dictionary ReadItems(nint handle, uint length, Doc doc) + private static Dictionary ReadItems(nint handle, uint length, Doc doc, bool isDeleted) { var entriesHandle = OutputChannel.Object(handle).Checked(); var result = new Dictionary(); - foreach (var (native, ptr) in MemoryReader.ReadIntPtrArray(entriesHandle, length)) + foreach (var (native, itemHandle) in MemoryReader.ReadStructsWithHandles(entriesHandle, length)) { - result[native.Key()] = new Output(native.ValueHandle(ptr), doc); + result[native.Key()] = new Output(native.ValueHandle(itemHandle), doc, isDeleted); } return result; diff --git a/YDotNet/Document/Cells/Output.cs b/YDotNet/Document/Cells/Output.cs index e4c9ffd5..6b4f01f2 100644 --- a/YDotNet/Document/Cells/Output.cs +++ b/YDotNet/Document/Cells/Output.cs @@ -1,4 +1,3 @@ -using System.Runtime.InteropServices; using YDotNet.Document.Types.Maps; using YDotNet.Document.Types.Texts; using YDotNet.Document.Types.XmlElements; @@ -18,18 +17,18 @@ public sealed class Output { private readonly object? value; - internal Output(nint handle, Doc doc) + internal Output(nint handle, Doc doc, bool isDeleted) { - var native = MemoryReader.PtrToStruct(handle); + var native = MemoryReader.ReadStruct(handle); - Tag = (OutputTage)native.Tag; + Tag = (OutputTag)native.Tag; - value = BuildValue(handle, native.Length, doc, Tag); + value = BuildValue(handle, native.Length, doc, isDeleted, Tag); } internal static Output CreateAndRelease(nint handle, Doc doc) { - var result = new Output(handle, doc); + var result = new Output(handle, doc, false); // The output reads everything so we can just destroy it. OutputChannel.Destroy(handle); @@ -37,168 +36,168 @@ internal static Output CreateAndRelease(nint handle, Doc doc) return result; } + private static object? BuildValue(nint handle, uint length, Doc doc, bool isDeleted, OutputTag type) + { + switch (type) + { + case OutputTag.Bool: + { + var value = OutputChannel.Boolean(handle).Checked(); + + return MemoryReader.ReadStruct(value) == 1; + } + + case OutputTag.Double: + { + var value = OutputChannel.Double(handle).Checked(); + + return MemoryReader.ReadStruct(value); + } + + case OutputTag.Long: + { + var value = OutputChannel.Long(handle).Checked(); + + return MemoryReader.ReadStruct(value); + } + + case OutputTag.String: + { + MemoryReader.TryReadUtf8String(OutputChannel.String(handle), out var result); + + return result; + } + + case OutputTag.Bytes: + { + var bytesHandle = OutputChannel.Bytes(handle).Checked(); + var bytesArray = MemoryReader.ReadBytes(OutputChannel.Bytes(handle), length); + + return bytesArray; + } + + case OutputTag.Collection: + { + return new JsonArray(handle, length, doc, isDeleted); + } + + case OutputTag.Object: + { + return new JsonObject(handle, length, doc, isDeleted); + } + + case OutputTag.Array: + return doc.GetArray(OutputChannel.Array(handle), isDeleted); + + case OutputTag.Map: + return doc.GetMap(OutputChannel.Map(handle), isDeleted); + + case OutputTag.Text: + return doc.GetText(OutputChannel.Text(handle), isDeleted); + + case OutputTag.XmlElement: + return doc.GetXmlElement(OutputChannel.XmlElement(handle), isDeleted); + + case OutputTag.XmlText: + return doc.GetXmlText(OutputChannel.XmlText(handle), isDeleted); + + case OutputTag.Doc: + return doc.GetDoc(OutputChannel.Doc(handle), isDeleted); + + default: + return null; + } + } + /// /// Gets the type of the output. /// - public OutputTage Tag { get; private set; } + public OutputTag Tag { get; private set; } /// /// Gets the value. /// /// Value is not a . - public Doc Doc => GetValue(OutputTage.Doc); + public Doc Doc => GetValue(OutputTag.Doc); /// /// Gets the value. /// /// Value is not a . - public string String => GetValue(OutputTage.String); + public string String => GetValue(OutputTag.String); /// /// Gets the value. /// /// Value is not a . - public bool Boolean => GetValue(OutputTage.Bool); + public bool Boolean => GetValue(OutputTag.Bool); /// /// Gets the value. /// /// Value is not a . - public double Double => GetValue(OutputTage.Double); + public double Double => GetValue(OutputTag.Double); /// /// Gets the value. /// /// Value is not a . - public long Long => GetValue(OutputTage.Long); + public long Long => GetValue(OutputTag.Long); /// /// Gets the array value. /// /// Value is not a array. - public byte[] Bytes => GetValue(OutputTage.Bytes); + public byte[] Bytes => GetValue(OutputTag.Bytes); /// /// Gets the collection. /// /// Value is not a collection. - public JsonArray Collection => GetValue(OutputTage.Collection); + public JsonArray Collection => GetValue(OutputTag.Collection); /// /// Gets the value as json object. /// /// Value is not a json object. - public JsonObject Object => GetValue(OutputTage.Object); + public JsonObject Object => GetValue(OutputTag.Object); /// /// Gets the value. /// /// The resolved array. /// Value is not a . - public Array Array => GetValue(OutputTage.Array); + public Array Array => GetValue(OutputTag.Array); /// /// Gets the value. /// /// The resolved map. /// Value is not a . - public Map Map => GetValue(OutputTage.Map); + public Map Map => GetValue(OutputTag.Map); /// /// Gets the value. /// /// The resolved text. /// Value is not a . - public Text Text => GetValue(OutputTage.Text); + public Text Text => GetValue(OutputTag.Text); /// /// Gets the value. /// /// The resolved xml element. /// Value is not a . - public XmlElement XmlElement => GetValue(OutputTage.XmlElement); + public XmlElement XmlElement => GetValue(OutputTag.XmlElement); /// /// Gets the value. /// /// The resolved xml text. /// Value is not a . - public XmlText XmlText => GetValue(OutputTage.XmlText); - - private static object? BuildValue(nint handle, uint length, Doc doc, OutputTage type) - { - switch (type) - { - case OutputTage.Bool: - { - var value = OutputChannel.Boolean(handle).Checked(); - - return Marshal.PtrToStructure(value) == 1; - } - - case OutputTage.Double: - { - var value = OutputChannel.Double(handle).Checked(); - - return Marshal.PtrToStructure(value); - } - - case OutputTage.Long: - { - var value = OutputChannel.Long(handle).Checked(); - - return Marshal.PtrToStructure(value); - } - - case OutputTage.String: - { - MemoryReader.TryReadUtf8String(OutputChannel.String(handle), out var result); - - return result; - } - - case OutputTage.Bytes: - { - var bytesHandle = OutputChannel.Bytes(handle).Checked(); - var bytesArray = MemoryReader.ReadBytes(OutputChannel.Bytes(handle), length); - - return bytesArray; - } - - case OutputTage.Collection: - { - return new JsonArray(handle, length, doc); - } - - case OutputTage.Object: - { - return new JsonObject(handle, length, doc); - } - - case OutputTage.Array: - return doc.GetArray(OutputChannel.Array(handle)); - - case OutputTage.Map: - return doc.GetMap(OutputChannel.Map(handle)); - - case OutputTage.Text: - return doc.GetText(OutputChannel.Text(handle)); - - case OutputTage.XmlElement: - return doc.GetXmlElement(OutputChannel.XmlElement(handle)); - - case OutputTage.XmlText: - return doc.GetXmlText(OutputChannel.XmlText(handle)); - - case OutputTage.Doc: - return doc.GetDoc(OutputChannel.Doc(handle)); - - default: - return null; - } - } + public XmlText XmlText => GetValue(OutputTag.XmlText); - private T GetValue(OutputTage expectedType) + private T GetValue(OutputTag expectedType) { if (value is not T typed) { diff --git a/YDotNet/Document/Cells/OutputType.cs b/YDotNet/Document/Cells/OutputTage.cs similarity index 99% rename from YDotNet/Document/Cells/OutputType.cs rename to YDotNet/Document/Cells/OutputTage.cs index df030509..d72edbff 100644 --- a/YDotNet/Document/Cells/OutputType.cs +++ b/YDotNet/Document/Cells/OutputTage.cs @@ -3,7 +3,7 @@ namespace YDotNet.Document.Cells; /// /// The type of an output. /// -public enum OutputTage +public enum OutputTag { /// /// No defined. diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index a2b735b6..071f2e74 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -1,6 +1,9 @@ +using YDotNet.Document.Cells; using YDotNet.Document.Events; using YDotNet.Document.Options; using YDotNet.Document.Transactions; +using YDotNet.Document.Types.Branches; +using YDotNet.Document.Types.Events; using YDotNet.Document.Types.Maps; using YDotNet.Document.Types.Texts; using YDotNet.Document.Types.XmlElements; @@ -31,7 +34,7 @@ namespace YDotNet.Document; /// to recursively nested types). /// /// -public class Doc : TypeBase +public class Doc : Resource, ITypeBase { private readonly TypeCache typeCache = new(); private readonly EventSubscriber onClear; @@ -58,11 +61,11 @@ public Doc() /// /// The options to be used when initializing this document. public Doc(DocOptions options) - : this(CreateDoc(options), null) + : this(CreateDoc(options), null, false) { } - internal Doc(nint handle, Doc? parent) + internal Doc(nint handle, Doc? parent, bool isDeleted) { this.parent = parent; @@ -71,7 +74,7 @@ internal Doc(nint handle, Doc? parent) (doc, action) => { DocChannel.ObserveClearCallback callback = - (_, doc) => action(new ClearEvent(GetDoc(doc))); + (_, doc) => action(new ClearEvent(GetDoc(doc, false))); return (DocChannel.ObserveClear(doc, nint.Zero, callback), callback); }, @@ -122,6 +125,11 @@ internal Doc(nint handle, Doc? parent) (doc, s) => DocChannel.UnobserveSubDocs(doc, s)); Handle = handle; + + if (isDeleted) + { + Dispose(); + } } private static nint CreateDoc(DocOptions options) @@ -129,6 +137,30 @@ private static nint CreateDoc(DocOptions options) return DocChannel.NewWithOptions(options.ToNative()); } + /// + /// Finalizes an instance of the class. + /// + ~Doc() + { + Dispose(false); + } + + /// + protected internal override void DisposeCore(bool disposing) + { + if (parent != null) + { + DocChannel.Destroy(Handle); + } + } + + bool ITypeBase.IsDeleted => IsDisposed; + + void ITypeBase.MarkDeleted() + { + Dispose(); + } + /// /// Gets the unique client identifier of this instance. /// @@ -187,7 +219,7 @@ public Text Text(string name) var handle = DocChannel.Text(Handle, unsafeName.Handle); - return GetText(handle); + return GetText(handle, false); } /// @@ -205,7 +237,7 @@ public Map Map(string name) var handle = DocChannel.Map(Handle, unsafeName.Handle); - return GetMap(handle); + return GetMap(handle, false); } /// @@ -223,7 +255,7 @@ public Array Array(string name) var handle = DocChannel.Array(Handle, unsafeName.Handle); - return GetArray(handle); + return GetArray(handle, false); } /// @@ -241,7 +273,7 @@ public XmlElement XmlElement(string name) var handle = DocChannel.XmlElement(Handle, unsafeName.Handle); - return GetXmlElement(handle); + return GetXmlElement(handle, false); } /// @@ -259,15 +291,15 @@ public XmlText XmlText(string name) var handle = DocChannel.XmlText(Handle, unsafeName.Handle); - return GetXmlText(handle); + return GetXmlText(handle, false); } /// /// Starts a new read-write on this document. /// /// Optional byte marker to indicate the source of changes to be applied by this transaction. - /// The to perform operations in the document. - /// Another exception is pending. + /// The to perform write operations in the document. + /// Another write transaction has been created and not commited yet. public Transaction WriteTransaction(byte[]? origin = null) { var handle = DocChannel.WriteTransaction(Handle, (uint)(origin?.Length ?? 0), origin); @@ -284,8 +316,8 @@ public Transaction WriteTransaction(byte[]? origin = null) /// /// Starts a new read-only on this document. /// - /// The to perform operations in the document. - /// Another exception is pending. + /// The to perform read operations in the document. + /// Another write transaction has been created and not commited yet. public Transaction ReadTransaction() { var handle = DocChannel.ReadTransaction(Handle); @@ -383,43 +415,56 @@ public IDisposable ObserveSubDocs(Action action) return onSubDocs.Subscribe(action); } - internal Doc GetDoc(nint handle) + internal Doc GetDoc(nint handle, bool isDeleted) { - return GetOrAdd(handle, h => new Doc(h, this)); + if (handle == Handle) + { + return this; + } + + if (!isDeleted) + { + // Prevent the sub document to be released while we are working with it. + handle = DocChannel.Clone(handle); + } + + return GetOrAdd(handle, (h, doc) => new Doc(handle, doc, isDeleted)); } - internal Map GetMap(nint handle) + internal Map GetMap(nint handle, bool isDeleted) { - return GetOrAdd(handle, h => new Map(h, this)); + return GetOrAdd(handle, (h, doc) => new Map(h, doc, isDeleted)); } - internal Array GetArray(nint handle) + internal Array GetArray(nint handle, bool isDeleted) { - return GetOrAdd(handle, h => new Array(h, this)); + return GetOrAdd(handle, (h, doc) => new Array(h, doc, isDeleted)); } - internal Text GetText(nint handle) + internal Text GetText(nint handle, bool isDeleted) { - return GetOrAdd(handle, h => new Text(h, this)); + return GetOrAdd(handle, (h, doc) => new Text(h, doc, isDeleted)); } - internal XmlText GetXmlText(nint handle) + internal XmlText GetXmlText(nint handle, bool isDeleted) { - return GetOrAdd(handle, h => new XmlText(h, this)); + return GetOrAdd(handle, (h, doc) => new XmlText(h, doc, isDeleted)); } - internal XmlElement GetXmlElement(nint handle) + internal XmlElement GetXmlElement(nint handle, bool isDeleted) { - return GetOrAdd(handle, h => new XmlElement(h, this)); + return GetOrAdd(handle, (h, doc) => new XmlElement(h, doc, isDeleted)); } - private T GetOrAdd(nint handle, Func factory) where T : ITypeBase + private T GetOrAdd(nint handle, Func factory) where T : ITypeBase { - if (parent != null) - { - return parent.GetOrAdd(handle, factory); - } + var doc = GetRootDoc(); - return typeCache.GetOrAdd(handle, factory); + return doc.typeCache.GetOrAdd(handle, h => factory(h, doc)); + } + + private Doc GetRootDoc() + { + return parent?.GetRootDoc() ?? this; } } diff --git a/YDotNet/Document/Events/EventSubscriber.cs b/YDotNet/Document/Events/EventSubscriber.cs index 3bd59cc2..ee30bf91 100644 --- a/YDotNet/Document/Events/EventSubscriber.cs +++ b/YDotNet/Document/Events/EventSubscriber.cs @@ -6,7 +6,7 @@ internal class EventSubscriber private readonly nint owner; private readonly Func, (uint Handle, object Callback)> subscribe; private readonly Action unsubscribe; - private (uint Handle, object Callback) nativeSubscription; + private (uint Handle, object? Callback) nativeSubscription; public EventSubscriber( nint owner, @@ -25,7 +25,7 @@ public void Clear() public IDisposable Subscribe(Action handler) { - if (nativeSubscription.Handle == nint.Zero) + if (nativeSubscription.Callback == null) { nativeSubscription = subscribe(owner, publisher.Publish); } @@ -42,7 +42,7 @@ private void Unsubscribe(Action handler) { publisher.Unsubscribe(handler); - if (publisher.Count == 0 && nativeSubscription.Handle != nint.Zero) + if (publisher.Count == 0 && nativeSubscription.Callback != null) { unsubscribe(owner, nativeSubscription.Handle); diff --git a/YDotNet/Document/Events/SubDocsEvent.cs b/YDotNet/Document/Events/SubDocsEvent.cs index cc7aab36..33806739 100644 --- a/YDotNet/Document/Events/SubDocsEvent.cs +++ b/YDotNet/Document/Events/SubDocsEvent.cs @@ -9,11 +9,14 @@ public class SubDocsEvent { internal SubDocsEvent(SubDocsEventNative native, Doc doc) { - Added = native.Added().Select(doc.GetDoc).ToList(); + Added = native.Added() + .Select(h => doc.GetDoc(h, false)).ToList(); - Removed = native.Removed().Select(doc.GetDoc).ToList(); + Removed = native.Removed() + .Select(h => doc.GetDoc(h, true)).ToList(); - Loaded = native.Loaded().Select(doc.GetDoc).ToList(); + Loaded = native.Loaded() + .Select(h => doc.GetDoc(h, false)).ToList(); } /// diff --git a/YDotNet/Document/Transactions/Transaction.cs b/YDotNet/Document/Transactions/Transaction.cs index 33a1196f..3fc90228 100644 --- a/YDotNet/Document/Transactions/Transaction.cs +++ b/YDotNet/Document/Transactions/Transaction.cs @@ -64,9 +64,9 @@ public void Commit() public Doc[] SubDocs() { var handle = TransactionChannel.SubDocs(Handle, out var length); - var handles = MemoryReader.ReadStructArray(handle, length); + var handles = MemoryReader.ReadStructs(handle, length); - return handles.Select(h => doc.GetDoc(h)).ToArray(); + return handles.Select(h => doc.GetDoc(h, false)).ToArray(); } /// @@ -282,7 +282,7 @@ public byte[] Snapshot() { var handle = GetWithKind(name, BranchKind.Array); - return handle != nint.Zero ? doc.GetArray(handle) : null; + return handle != nint.Zero ? doc.GetArray(handle, false) : null; } /// @@ -298,7 +298,7 @@ public byte[] Snapshot() { var handle = GetWithKind(name, BranchKind.Map); - return handle != nint.Zero ? doc.GetMap(handle) : null; + return handle != nint.Zero ? doc.GetMap(handle, false) : null; } /// @@ -314,7 +314,7 @@ public byte[] Snapshot() { var handle = GetWithKind(name, BranchKind.Text); - return handle != nint.Zero ? doc.GetText(handle) : null; + return handle != nint.Zero ? doc.GetText(handle, false) : null; } /// @@ -330,7 +330,7 @@ public byte[] Snapshot() { var handle = GetWithKind(name, BranchKind.XmlElement); - return handle != nint.Zero ? doc.GetXmlElement(handle) : null; + return handle != nint.Zero ? doc.GetXmlElement(handle, false) : null; } /// @@ -346,7 +346,7 @@ public byte[] Snapshot() { var handle = GetWithKind(name, BranchKind.XmlText); - return handle != nint.Zero ? doc.GetXmlText(handle) : null; + return handle != nint.Zero ? doc.GetXmlText(handle, false) : null; } private nint GetWithKind(string name, BranchKind expectedKind) diff --git a/YDotNet/Document/Types/Arrays/Array.cs b/YDotNet/Document/Types/Arrays/Array.cs index ee554d11..2f17c517 100644 --- a/YDotNet/Document/Types/Arrays/Array.cs +++ b/YDotNet/Document/Types/Arrays/Array.cs @@ -17,8 +17,8 @@ public class Array : Branch { private readonly EventSubscriber onObserve; - internal Array(nint handle, Doc doc) - : base(handle, doc) + internal Array(nint handle, Doc doc, bool isDeleted) + : base(handle, doc, isDeleted) { onObserve = new EventSubscriber( handle, @@ -35,7 +35,15 @@ internal Array(nint handle, Doc doc) /// /// Gets the number of elements stored within current instance of . /// - public uint Length => ArrayChannel.Length(Handle); + public uint Length + { + get + { + ThrowIfDeleted(); + + return ArrayChannel.Length(Handle); + } + } /// /// Inserts a range of into the current instance of . @@ -45,6 +53,8 @@ internal Array(nint handle, Doc doc) /// The items to be inserted. public void InsertRange(Transaction transaction, uint index, params Input[] inputs) { + ThrowIfDeleted(); + using var unsafeInputs = MemoryWriter.WriteStructArray(inputs.Select(x => x.InputNative).ToArray()); ArrayChannel.InsertRange(Handle, transaction.Handle, index, unsafeInputs.Handle, (uint)inputs.Length); @@ -58,6 +68,8 @@ public void InsertRange(Transaction transaction, uint index, params Input[] inpu /// The amount of items to remove. public void RemoveRange(Transaction transaction, uint index, uint length) { + ThrowIfDeleted(); + ArrayChannel.RemoveRange(Handle, transaction.Handle, index, length); } @@ -73,6 +85,8 @@ public void RemoveRange(Transaction transaction, uint index, uint length) /// public Output? Get(Transaction transaction, uint index) { + ThrowIfDeleted(); + var handle = ArrayChannel.Get(Handle, transaction.Handle, index); return handle != nint.Zero ? Output.CreateAndRelease(handle, Doc) : null; @@ -89,6 +103,8 @@ public void RemoveRange(Transaction transaction, uint index, uint length) /// The index to which the item will be moved to. public void Move(Transaction transaction, uint sourceIndex, uint targetIndex) { + ThrowIfDeleted(); + ArrayChannel.Move(Handle, transaction.Handle, sourceIndex, targetIndex); } @@ -100,6 +116,8 @@ public void Move(Transaction transaction, uint sourceIndex, uint targetIndex) /// The instance. public ArrayIterator Iterate(Transaction transaction) { + ThrowIfDeleted(); + var handle = ArrayChannel.Iterator(Handle, transaction.Handle); return new ArrayIterator(handle.Checked(), Doc); @@ -115,6 +133,8 @@ public ArrayIterator Iterate(Transaction transaction) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable Observe(Action action) { + ThrowIfDeleted(); + return onObserve.Subscribe(action); } @@ -128,6 +148,8 @@ public IDisposable Observe(Action action) /// The in the with the given . public StickyIndex? StickyIndex(Transaction transaction, uint index, StickyAssociationType associationType) { + ThrowIfDeleted(); + var handle = StickyIndexChannel.FromIndex(Handle, transaction.Handle, index, (sbyte)associationType); return handle != nint.Zero ? new StickyIndex(handle) : null; diff --git a/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs b/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs index e8a2806c..944cfb2e 100644 --- a/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs +++ b/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs @@ -40,7 +40,7 @@ internal ArrayEvent(nint handle, Doc doc) var targetHandle = ArrayChannel.ObserveEventTarget(handle).Checked(); - return new Array(targetHandle, doc); + return doc.GetArray(targetHandle, false); }); } diff --git a/YDotNet/Document/Types/Branches/Branch.cs b/YDotNet/Document/Types/Branches/Branch.cs index 8d5ec6b8..7ef0ab57 100644 --- a/YDotNet/Document/Types/Branches/Branch.cs +++ b/YDotNet/Document/Types/Branches/Branch.cs @@ -16,7 +16,8 @@ public abstract class Branch : TypeBase { private readonly EventSubscriber onDeep; - internal Branch(nint handle, Doc doc) + internal Branch(nint handle, Doc doc, bool isDeleted) + : base(isDeleted) { Doc = doc; @@ -26,7 +27,7 @@ internal Branch(nint handle, Doc doc) { BranchChannel.ObserveCallback callback = (_, length, ev) => { - var events = MemoryReader.ReadIntPtrArray(ev, length).Select(x => new EventBranch(x, doc)).ToArray(); + var events = MemoryReader.ReadStructsWithHandles(ev, length).Select(x => new EventBranch(x, doc)).ToArray(); action(events); }; @@ -60,9 +61,11 @@ public IDisposable ObserveDeep(Action> action) /// Starts a new read-write on this instance. /// /// The to perform operations in the document. - /// Another exception is pending. + /// Another write transaction has been created and not commited yet. public Transaction WriteTransaction() { + ThrowIfDeleted(); + var handle = BranchChannel.WriteTransaction(Handle); if (handle == nint.Zero) @@ -78,9 +81,11 @@ public Transaction WriteTransaction() /// Starts a new read-only on this instance. /// /// The to perform operations in the branch. - /// Another exception is pending. + /// Another write transaction has been created and not commited yet. public Transaction ReadTransaction() { + ThrowIfDeleted(); + var handle = BranchChannel.ReadTransaction(Handle); if (handle == nint.Zero) diff --git a/YDotNet/Document/Types/Events/EventChange.cs b/YDotNet/Document/Types/Events/EventChange.cs index b1d2a2e0..dca540bc 100644 --- a/YDotNet/Document/Types/Events/EventChange.cs +++ b/YDotNet/Document/Types/Events/EventChange.cs @@ -20,7 +20,7 @@ internal EventChange(EventChangeNative native, Doc doc) _ => throw new NotSupportedException($"The value \"{native.TagNative}\" for {nameof(EventChangeTagNative)} is not supported."), }; - Values = native.ValuesHandles.Select(x => new Output(x, doc)).ToList(); + Values = native.ValuesHandles.Select(x => new Output(x, doc, false)).ToList(); } /// diff --git a/YDotNet/Document/Types/Events/EventChanges.cs b/YDotNet/Document/Types/Events/EventChanges.cs index 00b3ee3b..a67ff0ed 100644 --- a/YDotNet/Document/Types/Events/EventChanges.cs +++ b/YDotNet/Document/Types/Events/EventChanges.cs @@ -19,9 +19,9 @@ private static IList ReadItems(nint handle, uint length, Doc doc) { var result = new List(); - foreach (var native in MemoryReader.ReadIntPtrArray(handle, length)) + foreach (var native in MemoryReader.ReadStructs(handle, length)) { - result.Add(new EventChange(native.Value, doc)); + result.Add(new EventChange(native, doc)); } // We are done reading and can release the resource. diff --git a/YDotNet/Document/Types/Events/EventDelta.cs b/YDotNet/Document/Types/Events/EventDelta.cs index 0cd25fa7..c547fe51 100644 --- a/YDotNet/Document/Types/Events/EventDelta.cs +++ b/YDotNet/Document/Types/Events/EventDelta.cs @@ -22,11 +22,11 @@ internal EventDelta(EventDeltaNative native, Doc doc) Attributes = native.Attributes.ToDictionary( x => x.Value.Key(), - x => new Output(x.Value.ValueHandle(x.Handle), doc)); + x => new Output(x.Value.ValueHandle(x.Handle), doc, false)); if (native.InsertHandle != nint.Zero) { - Insert = new Output(native.InsertHandle, doc); + Insert = new Output(native.InsertHandle, doc, false); } } diff --git a/YDotNet/Document/Types/Events/EventDeltas.cs b/YDotNet/Document/Types/Events/EventDeltas.cs index be30f676..7881151a 100644 --- a/YDotNet/Document/Types/Events/EventDeltas.cs +++ b/YDotNet/Document/Types/Events/EventDeltas.cs @@ -19,9 +19,9 @@ private static IList ReadItems(nint handle, uint length, Doc doc) { var result = new List((int)length); - foreach (var native in MemoryReader.ReadIntPtrArray(handle, length)) + foreach (var native in MemoryReader.ReadStructs(handle, length)) { - result.Add(new EventDelta(native.Value, doc)); + result.Add(new EventDelta(native, doc)); } // We are done reading and can release the memory. diff --git a/YDotNet/Document/Types/Events/EventKeyChange.cs b/YDotNet/Document/Types/Events/EventKeyChange.cs index 5d47ef79..8892144f 100644 --- a/YDotNet/Document/Types/Events/EventKeyChange.cs +++ b/YDotNet/Document/Types/Events/EventKeyChange.cs @@ -26,12 +26,12 @@ internal EventKeyChange(EventKeyChangeNative native, Doc doc) if (native.OldValue != nint.Zero) { - OldValue = new Output(native.OldValue, doc); + OldValue = new Output(native.OldValue, doc, true); } if (native.NewValue != nint.Zero) { - NewValue = new Output(native.NewValue, doc); + NewValue = new Output(native.NewValue, doc, false); } } diff --git a/YDotNet/Document/Types/Events/EventKeys.cs b/YDotNet/Document/Types/Events/EventKeys.cs index 932a366d..3f5ce2e0 100644 --- a/YDotNet/Document/Types/Events/EventKeys.cs +++ b/YDotNet/Document/Types/Events/EventKeys.cs @@ -19,7 +19,7 @@ private static IList ReadItems(nint handle, uint length, Doc doc { var result = new List(); - foreach (var native in MemoryReader.ReadIntPtrArray(handle, length)) + foreach (var native in MemoryReader.ReadStructsWithHandles(handle, length)) { result.Add(new EventKeyChange(native.Value, doc)); } diff --git a/YDotNet/Document/Types/Events/EventPath.cs b/YDotNet/Document/Types/Events/EventPath.cs index a2da9d64..8a977a73 100644 --- a/YDotNet/Document/Types/Events/EventPath.cs +++ b/YDotNet/Document/Types/Events/EventPath.cs @@ -19,9 +19,9 @@ private static IList ReadItems(nint handle, uint length) { var result = new List(); - foreach (var itemHandle in MemoryReader.ReadIntPtrArray(handle, length, size: 16)) + foreach (var native in MemoryReader.ReadStructs(handle, length)) { - result.Add(new EventPathSegment(itemHandle)); + result.Add(new EventPathSegment(native)); } // We have read everything, so we can release the memory immediately. diff --git a/YDotNet/Document/Types/Events/EventPathSegment.cs b/YDotNet/Document/Types/Events/EventPathSegment.cs index 84f592ef..4cd03773 100644 --- a/YDotNet/Document/Types/Events/EventPathSegment.cs +++ b/YDotNet/Document/Types/Events/EventPathSegment.cs @@ -1,6 +1,3 @@ -using System.Runtime.InteropServices; -using YDotNet.Infrastructure; - namespace YDotNet.Document.Types.Events; /// @@ -8,23 +5,18 @@ namespace YDotNet.Document.Types.Events; /// public class EventPathSegment { - /// - /// Initializes a new instance of the class. - /// - /// The handle to the native resource. - public EventPathSegment(nint handle) + internal EventPathSegment(EventPathSegmentNative native) { - Handle = handle; - Tag = (EventPathSegmentTag)Marshal.ReadByte(handle); + Tag = (EventPathSegmentTag)native.Tag; switch (Tag) { case EventPathSegmentTag.Key: - Key = Marshal.PtrToStringAnsi(Marshal.ReadIntPtr(handle + MemoryConstants.PointerSize)); + Key = native.Key(); break; case EventPathSegmentTag.Index: - Index = (uint)Marshal.ReadInt32(handle + MemoryConstants.PointerSize); + Index = (uint)native.KeyOrIndex; break; } } @@ -35,19 +27,12 @@ public EventPathSegment(nint handle) public EventPathSegmentTag Tag { get; } /// - /// Gets the key, if is , or - /// null otherwise. + /// Gets the key, if is , or null otherwise. /// public string? Key { get; } /// - /// Gets the index, if is , or - /// null otherwise. + /// Gets the index, if is , or null otherwise. /// public uint? Index { get; } - - /// - /// Gets the handle to the native resource. - /// - internal nint Handle { get; } } diff --git a/YDotNet/Document/Types/Maps/Events/MapEvent.cs b/YDotNet/Document/Types/Maps/Events/MapEvent.cs index 3c3cd1be..1679c503 100644 --- a/YDotNet/Document/Types/Maps/Events/MapEvent.cs +++ b/YDotNet/Document/Types/Maps/Events/MapEvent.cs @@ -35,7 +35,7 @@ internal MapEvent(nint handle, Doc doc) { var targetHandle = MapChannel.ObserveEventTarget(handle).Checked(); - return doc.GetMap(targetHandle); + return doc.GetMap(targetHandle, false); }); } diff --git a/YDotNet/Document/Types/Maps/Map.cs b/YDotNet/Document/Types/Maps/Map.cs index 659b26a4..ce99aaec 100644 --- a/YDotNet/Document/Types/Maps/Map.cs +++ b/YDotNet/Document/Types/Maps/Map.cs @@ -15,8 +15,8 @@ public class Map : Branch { private readonly EventSubscriber onObserve; - internal Map(nint handle, Doc doc) - : base(handle, doc) + internal Map(nint handle, Doc doc, bool isDeleted) + : base(handle, doc, isDeleted) { onObserve = new EventSubscriber( handle, @@ -41,6 +41,8 @@ internal Map(nint handle, Doc doc) /// The instance to be inserted. public void Insert(Transaction transaction, string key, Input input) { + ThrowIfDeleted(); + using var unsafeKey = MemoryWriter.WriteUtf8String(key); using var unsafeValue = MemoryWriter.WriteStruct(input.InputNative); @@ -58,6 +60,8 @@ public void Insert(Transaction transaction, string key, Input input) /// The or null if entry not found. public Output? Get(Transaction transaction, string key) { + ThrowIfDeleted(); + using var unsafeName = MemoryWriter.WriteUtf8String(key); var handle = MapChannel.Get(Handle, transaction.Handle, unsafeName.Handle); @@ -72,6 +76,8 @@ public void Insert(Transaction transaction, string key, Input input) /// The number of entries stored in the . public uint Length(Transaction transaction) { + ThrowIfDeleted(); + return MapChannel.Length(Handle, transaction.Handle); } @@ -83,6 +89,8 @@ public uint Length(Transaction transaction) /// `true` if the entry was found and removed, `false` if no entry was found. public bool Remove(Transaction transaction, string key) { + ThrowIfDeleted(); + using var unsafeKey = MemoryWriter.WriteUtf8String(key); return MapChannel.Remove(Handle, transaction.Handle, unsafeKey.Handle) == 1; @@ -94,6 +102,8 @@ public bool Remove(Transaction transaction, string key) /// The transaction that wraps this operation. public void RemoveAll(Transaction transaction) { + ThrowIfDeleted(); + MapChannel.RemoveAll(Handle, transaction.Handle); } @@ -105,6 +115,8 @@ public void RemoveAll(Transaction transaction) /// The instance. public MapIterator Iterate(Transaction transaction) { + ThrowIfDeleted(); + var handle = MapChannel.Iterator(Handle, transaction.Handle).Checked(); return new MapIterator(handle, Doc); @@ -120,6 +132,8 @@ public MapIterator Iterate(Transaction transaction) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable Observe(Action action) { + ThrowIfDeleted(); + return onObserve.Subscribe(action); } } diff --git a/YDotNet/Document/Types/Maps/MapEnumerator.cs b/YDotNet/Document/Types/Maps/MapEnumerator.cs index bbc03cfa..3501479c 100644 --- a/YDotNet/Document/Types/Maps/MapEnumerator.cs +++ b/YDotNet/Document/Types/Maps/MapEnumerator.cs @@ -37,11 +37,11 @@ public bool MoveNext() if (handle != nint.Zero) { - var native = MemoryReader.PtrToStruct(handle); + var native = MemoryReader.ReadStruct(handle); current = new KeyValuePair( native.Key(), - new Output(native.ValueHandle(handle), Iterator.Doc)); + new Output(native.ValueHandle(handle), Iterator.Doc, false)); // We are done reading, so we can release the memory. MapChannel.EntryDestroy(handle); diff --git a/YDotNet/Document/Types/Texts/Events/TextEvent.cs b/YDotNet/Document/Types/Texts/Events/TextEvent.cs index be1dacf9..425b18ee 100644 --- a/YDotNet/Document/Types/Texts/Events/TextEvent.cs +++ b/YDotNet/Document/Types/Texts/Events/TextEvent.cs @@ -40,7 +40,7 @@ internal TextEvent(nint handle, Doc doc) var targetHandle = TextChannel.ObserveEventTarget(handle).Checked(); - return doc.GetText(targetHandle); + return doc.GetText(targetHandle, false); }); } diff --git a/YDotNet/Document/Types/Texts/Text.cs b/YDotNet/Document/Types/Texts/Text.cs index 5fef8f32..176b8545 100644 --- a/YDotNet/Document/Types/Texts/Text.cs +++ b/YDotNet/Document/Types/Texts/Text.cs @@ -17,8 +17,8 @@ public class Text : Branch { private readonly EventSubscriber onObserve; - internal Text(nint handle, Doc doc) - : base(handle, doc) + internal Text(nint handle, Doc doc, bool isDeleted) + : base(handle, doc, isDeleted) { onObserve = new EventSubscriber( handle, @@ -44,6 +44,8 @@ internal Text(nint handle, Doc doc) /// public void Insert(Transaction transaction, uint index, string value, Input? attributes = null) { + ThrowIfDeleted(); + using var unsafeValue = MemoryWriter.WriteUtf8String(value); using var unsafeAttributes = MemoryWriter.WriteStruct(attributes?.InputNative); @@ -62,6 +64,8 @@ public void Insert(Transaction transaction, uint index, string value, Input? att /// public void InsertEmbed(Transaction transaction, uint index, Input content, Input? attributes = null) { + ThrowIfDeleted(); + var unsafeContent = MemoryWriter.WriteStruct(content.InputNative); var unsafeAttributes = MemoryWriter.WriteStruct(attributes?.InputNative); @@ -79,6 +83,8 @@ public void InsertEmbed(Transaction transaction, uint index, Input content, Inpu /// public void RemoveRange(Transaction transaction, uint index, uint length) { + ThrowIfDeleted(); + TextChannel.RemoveRange(Handle, transaction.Handle, index, length); } @@ -97,6 +103,8 @@ public void RemoveRange(Transaction transaction, uint index, uint length) /// public void Format(Transaction transaction, uint index, uint length, Input attributes) { + ThrowIfDeleted(); + using var unsafeAttributes = MemoryWriter.WriteStruct(attributes.InputNative); TextChannel.Format(Handle, transaction.Handle, index, length, unsafeAttributes.Handle); @@ -109,6 +117,8 @@ public void Format(Transaction transaction, uint index, uint length, Input attri /// The that compose this . public TextChunks Chunks(Transaction transaction) { + ThrowIfDeleted(); + var handle = TextChannel.Chunks(Handle, transaction.Handle, out var length).Checked(); return new TextChunks(handle, length, Doc); @@ -121,6 +131,8 @@ public TextChunks Chunks(Transaction transaction) /// The full string stored in the instance. public string String(Transaction transaction) { + ThrowIfDeleted(); + var handle = TextChannel.String(Handle, transaction.Handle); return MemoryReader.ReadStringAndDestroy(handle); @@ -136,6 +148,8 @@ public string String(Transaction transaction) /// The length, in bytes, of the string stored in the instance. public uint Length(Transaction transaction) { + ThrowIfDeleted(); + return TextChannel.Length(Handle, transaction.Handle); } @@ -146,6 +160,8 @@ public uint Length(Transaction transaction) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable Observe(Action action) { + ThrowIfDeleted(); + return onObserve.Subscribe(action); } @@ -159,6 +175,8 @@ public IDisposable Observe(Action action) /// The in the with the given . public StickyIndex? StickyIndex(Transaction transaction, uint index, StickyAssociationType associationType) { + ThrowIfDeleted(); + var handle = StickyIndexChannel.FromIndex(Handle, transaction.Handle, index, (sbyte)associationType); return handle != nint.Zero ? new StickyIndex(handle) : null; diff --git a/YDotNet/Document/Types/Texts/TextChunk.cs b/YDotNet/Document/Types/Texts/TextChunk.cs index 783e45de..40f4d472 100644 --- a/YDotNet/Document/Types/Texts/TextChunk.cs +++ b/YDotNet/Document/Types/Texts/TextChunk.cs @@ -11,11 +11,11 @@ public class TextChunk { internal TextChunk(NativeWithHandle native, Doc doc) { - Data = new Output(native.Handle, doc); + Data = new Output(native.Handle, doc, false); Attributes = native.Value.Attributes().ToDictionary( x => x.Value.Key(), - x => new Output(x.Value.ValueHandle(x.Handle), doc)); + x => new Output(x.Value.ValueHandle(x.Handle), doc, false)); } /// diff --git a/YDotNet/Document/Types/Texts/TextChunks.cs b/YDotNet/Document/Types/Texts/TextChunks.cs index 0757e611..d7a70e8f 100644 --- a/YDotNet/Document/Types/Texts/TextChunks.cs +++ b/YDotNet/Document/Types/Texts/TextChunks.cs @@ -19,7 +19,7 @@ private static IList ReadItems(nint handle, uint length, Doc doc) { var result = new List((int)length); - foreach (var native in MemoryReader.ReadIntPtrArray(handle, length)) + foreach (var native in MemoryReader.ReadStructsWithHandles(handle, length)) { result.Add(new TextChunk(native, doc)); } diff --git a/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs b/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs index 165f93f3..92a11a9c 100644 --- a/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs +++ b/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs @@ -42,7 +42,7 @@ internal XmlElementEvent(nint handle, Doc doc) { var targetHandle = XmlElementChannel.ObserveEventTarget(handle).Checked(); - return doc.GetXmlElement(targetHandle); + return doc.GetXmlElement(targetHandle, false); }); } diff --git a/YDotNet/Document/Types/XmlElements/XmlAttribute.cs b/YDotNet/Document/Types/XmlElements/XmlAttribute.cs deleted file mode 100644 index 41c854e9..00000000 --- a/YDotNet/Document/Types/XmlElements/XmlAttribute.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Runtime.InteropServices; -using YDotNet.Document.Types.XmlTexts; -using YDotNet.Infrastructure; -using YDotNet.Native.Types; - -namespace YDotNet.Document.Types.XmlElements; - -/// -/// A structure representing single attribute of either an or instance. -/// -public class XmlAttribute -{ - internal XmlAttribute(nint handle) - { - Handle = handle; - - Name = Marshal.PtrToStringAnsi(Marshal.ReadIntPtr(Handle)) ?? - throw new YDotNetException("Failed to read name."); - - Value = Marshal.PtrToStringAnsi(Marshal.ReadIntPtr(Handle + MemoryConstants.PointerSize)) ?? - throw new YDotNetException("Failed to read value."); - - // We are done reading and can release the memory. - XmlAttributeChannel.Destroy(Handle); - } - - /// - /// Gets the name of the attribute. - /// - public string Name { get; } - - /// - /// Gets the value of the attribute. - /// - public string Value { get; } - - /// - /// Gets the handle to the native resource. - /// - internal nint Handle { get; } -} diff --git a/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs b/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs index 949cceff..eb6e7e2e 100644 --- a/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs +++ b/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs @@ -1,15 +1,15 @@ using System.Collections; +using YDotNet.Infrastructure; using YDotNet.Native.Types; namespace YDotNet.Document.Types.XmlElements; /// -/// Represents the iterator to provide instances of or null to -/// . +/// Represents the iterator to provide instances of key value paris. /// -internal class XmlAttributeEnumerator : IEnumerator +internal class XmlAttributeEnumerator : IEnumerator> { - private XmlAttribute? current; + private KeyValuePair current; /// /// Initializes a new instance of the class. @@ -29,7 +29,7 @@ public void Dispose() } /// - public XmlAttribute Current => current!; + public KeyValuePair Current => current; /// object? IEnumerator.Current => current!; @@ -41,9 +41,22 @@ public bool MoveNext() { var handle = XmlAttributeChannel.IteratorNext(Iterator.Handle); - current = handle != nint.Zero ? new XmlAttribute(handle) : null; + if (handle != nint.Zero) + { + var native = MemoryReader.ReadStruct(handle); - return current != null; + current = new KeyValuePair(native.Key(), native.Value()); + + // We are done reading, therefore we can release memory. + XmlAttributeChannel.Destroy(handle); + + return true; + } + else + { + current = default; + return false; + } } /// diff --git a/YDotNet/Document/Types/XmlElements/XmlAttributeIterator.cs b/YDotNet/Document/Types/XmlElements/XmlAttributeIterator.cs index 7d6dbc3d..6d08ab69 100644 --- a/YDotNet/Document/Types/XmlElements/XmlAttributeIterator.cs +++ b/YDotNet/Document/Types/XmlElements/XmlAttributeIterator.cs @@ -11,7 +11,7 @@ namespace YDotNet.Document.Types.XmlElements; /// /// The iterator can't be reused. If needed, use to accumulate values. /// -public class XmlAttributeIterator : UnmanagedResource, IEnumerable +public class XmlAttributeIterator : UnmanagedResource, IEnumerable> { internal XmlAttributeIterator(nint handle) : base(handle) @@ -33,7 +33,7 @@ protected internal override void DisposeCore(bool disposing) } /// - public IEnumerator GetEnumerator() + public IEnumerator> GetEnumerator() { return new XmlAttributeEnumerator(this); } diff --git a/YDotNet/Document/Types/XmlElements/XmlElement.cs b/YDotNet/Document/Types/XmlElements/XmlElement.cs index d7033b64..0c5be42f 100644 --- a/YDotNet/Document/Types/XmlElements/XmlElement.cs +++ b/YDotNet/Document/Types/XmlElements/XmlElement.cs @@ -18,8 +18,8 @@ public class XmlElement : Branch { private readonly EventSubscriber onObserve; - internal XmlElement(nint handle, Doc doc) - : base(handle, doc) + internal XmlElement(nint handle, Doc doc, bool isDeleted) + : base(handle, doc, isDeleted) { onObserve = new EventSubscriber( handle, @@ -43,6 +43,8 @@ public string? Tag { get { + ThrowIfDeleted(); + var handle = XmlElementChannel.Tag(Handle); return handle != nint.Zero ? MemoryReader.ReadStringAndDestroy(handle) : null; @@ -59,6 +61,8 @@ public string? Tag /// The string representation of the instance. public string String(Transaction transaction) { + ThrowIfDeleted(); + var handle = XmlElementChannel.String(Handle, transaction.Handle); return MemoryReader.ReadStringAndDestroy(handle.Checked()); @@ -75,6 +79,8 @@ public string String(Transaction transaction) /// The value of the attribute to be added. public void InsertAttribute(Transaction transaction, string name, string value) { + ThrowIfDeleted(); + using var unsageName = MemoryWriter.WriteUtf8String(name); using var unsafeValue = MemoryWriter.WriteUtf8String(value); @@ -88,6 +94,8 @@ public void InsertAttribute(Transaction transaction, string name, string value) /// The name of the attribute to be removed. public void RemoveAttribute(Transaction transaction, string name) { + ThrowIfDeleted(); + using var unsafeName = MemoryWriter.WriteUtf8String(name); XmlElementChannel.RemoveAttribute(Handle, transaction.Handle, unsafeName.Handle); @@ -101,6 +109,8 @@ public void RemoveAttribute(Transaction transaction, string name) /// The value of the attribute or null if it doesn't exist. public string? GetAttribute(Transaction transaction, string name) { + ThrowIfDeleted(); + using var unsafeName = MemoryWriter.WriteUtf8String(name); var handle = XmlElementChannel.GetAttribute(Handle, transaction.Handle, unsafeName.Handle); @@ -116,6 +126,8 @@ public void RemoveAttribute(Transaction transaction, string name) /// The instance or null if failed. public XmlAttributeIterator Iterate(Transaction transaction) { + ThrowIfDeleted(); + var handle = XmlElementChannel.AttributeIterator(Handle, transaction.Handle); return new XmlAttributeIterator(handle.Checked()); @@ -132,6 +144,8 @@ public XmlAttributeIterator Iterate(Transaction transaction) /// The number of direct child nodes of this . public uint ChildLength(Transaction transaction) { + ThrowIfDeleted(); + return XmlElementChannel.ChildLength(Handle, transaction.Handle); } @@ -144,9 +158,11 @@ public uint ChildLength(Transaction transaction) /// The inserted at the given . public XmlText InsertText(Transaction transaction, uint index) { + ThrowIfDeleted(); + var handle = XmlElementChannel.InsertText(Handle, transaction.Handle, index); - return Doc.GetXmlText(handle); + return Doc.GetXmlText(handle, false); } /// @@ -159,11 +175,13 @@ public XmlText InsertText(Transaction transaction, uint index) /// The inserted at the given . public XmlElement InsertElement(Transaction transaction, uint index, string name) { + ThrowIfDeleted(); + using var unsafeName = MemoryWriter.WriteUtf8String(name); var handle = XmlElementChannel.InsertElement(Handle, transaction.Handle, index, unsafeName.Handle); - return Doc.GetXmlElement(handle); + return Doc.GetXmlElement(handle, false); } /// @@ -175,6 +193,8 @@ public XmlElement InsertElement(Transaction transaction, uint index, string name /// The amount of child nodes to remove, starting at . public void RemoveRange(Transaction transaction, uint index, uint length) { + ThrowIfDeleted(); + XmlElementChannel.RemoveRange(Handle, transaction.Handle, index, length); } @@ -186,9 +206,11 @@ public void RemoveRange(Transaction transaction, uint index, uint length) /// An cell or null if the is out of bounds. public Output? Get(Transaction transaction, uint index) { + ThrowIfDeleted(); + var handle = XmlElementChannel.Get(Handle, transaction.Handle, index); - return handle != nint.Zero ? new Output(handle, Doc) : null; + return handle != nint.Zero ? Output.CreateAndRelease(handle, Doc) : null; } /// @@ -203,6 +225,8 @@ public void RemoveRange(Transaction transaction, uint index, uint length) /// public Output? PreviousSibling(Transaction transaction) { + ThrowIfDeleted(); + var handle = XmlChannel.PreviousSibling(Handle, transaction.Handle); return handle != nint.Zero ? Output.CreateAndRelease(handle, Doc) : null; @@ -220,18 +244,11 @@ public void RemoveRange(Transaction transaction, uint index, uint length) /// public Output? NextSibling(Transaction transaction) { - var handle = XmlChannel.NextSibling(Handle, transaction.Handle); - - if (handle == nint.Zero) - { - return null; - } + ThrowIfDeleted(); - // The output reads everything so we can just destroy it. - var result = new Output(handle, Doc); - OutputChannel.Destroy(handle); + var handle = XmlChannel.NextSibling(Handle, transaction.Handle); - return result; + return handle != nint.Zero ? Output.CreateAndRelease(handle, Doc) : null; } /// @@ -245,6 +262,8 @@ public void RemoveRange(Transaction transaction, uint index, uint length) /// public Output? FirstChild(Transaction transaction) { + ThrowIfDeleted(); + var handle = XmlElementChannel.FirstChild(Handle, transaction.Handle); return handle != nint.Zero ? Output.CreateAndRelease(handle, Doc) : null; @@ -261,9 +280,11 @@ public void RemoveRange(Transaction transaction, uint index, uint length) /// public XmlElement? Parent(Transaction transaction) { + ThrowIfDeleted(); + var handle = XmlElementChannel.Parent(Handle, transaction.Handle); - return handle != nint.Zero ? Doc.GetXmlElement(handle) : null; + return handle != nint.Zero ? Doc.GetXmlElement(handle, false) : null; } /// @@ -276,6 +297,8 @@ public void RemoveRange(Transaction transaction, uint index, uint length) /// An for this . public XmlTreeWalker TreeWalker(Transaction transaction) { + ThrowIfDeleted(); + var handle = XmlElementChannel.TreeWalker(Handle, transaction.Handle); return new XmlTreeWalker(handle.Checked(), Doc); @@ -291,6 +314,8 @@ public XmlTreeWalker TreeWalker(Transaction transaction) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable Observe(Action action) { + ThrowIfDeleted(); + return onObserve.Subscribe(action); } } diff --git a/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs b/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs index e6055db0..19983601 100644 --- a/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs +++ b/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs @@ -34,7 +34,7 @@ internal XmlTextEvent(nint handle, Doc doc) { var targetHandle = XmlTextChannel.ObserveEventTarget(handle).Checked(); - return doc.GetXmlText(handle); + return doc.GetXmlText(handle, false); }); } diff --git a/YDotNet/Document/Types/XmlTexts/XmlText.cs b/YDotNet/Document/Types/XmlTexts/XmlText.cs index ec38a594..bb66ca54 100644 --- a/YDotNet/Document/Types/XmlTexts/XmlText.cs +++ b/YDotNet/Document/Types/XmlTexts/XmlText.cs @@ -18,8 +18,8 @@ public class XmlText : Branch { private readonly EventSubscriber onObserve; - internal XmlText(nint handle, Doc doc) - : base(handle, doc) + internal XmlText(nint handle, Doc doc, bool isDeleted) + : base(handle, doc, isDeleted) { onObserve = new EventSubscriber( handle, @@ -40,6 +40,8 @@ internal XmlText(nint handle, Doc doc) /// The length of the text, in bytes, stored in the . public uint Length(Transaction transaction) { + ThrowIfDeleted(); + return XmlTextChannel.Length(Handle, transaction.Handle); } @@ -55,6 +57,8 @@ public uint Length(Transaction transaction) /// public void Insert(Transaction transaction, uint index, string value, Input? attributes = null) { + ThrowIfDeleted(); + using var unsafeValue = MemoryWriter.WriteUtf8String(value); using var unsafeAttributes = MemoryWriter.WriteStruct(attributes?.InputNative); @@ -73,6 +77,8 @@ public void Insert(Transaction transaction, uint index, string value, Input? att /// public void InsertEmbed(Transaction transaction, uint index, Input content, Input? attributes = null) { + ThrowIfDeleted(); + using var unsafeContent = MemoryWriter.WriteStruct(content.InputNative); using var unsafeAttributes = MemoryWriter.WriteStruct(attributes?.InputNative); @@ -90,6 +96,8 @@ public void InsertEmbed(Transaction transaction, uint index, Input content, Inpu /// The value of the attribute to be added. public void InsertAttribute(Transaction transaction, string name, string value) { + ThrowIfDeleted(); + using var unsafeName = MemoryWriter.WriteUtf8String(name); using var unsafeValue = MemoryWriter.WriteUtf8String(value); @@ -103,6 +111,8 @@ public void InsertAttribute(Transaction transaction, string name, string value) /// The name of the attribute to be removed. public void RemoveAttribute(Transaction transaction, string name) { + ThrowIfDeleted(); + using var unsafeName = MemoryWriter.WriteUtf8String(name); XmlTextChannel.RemoveAttribute(Handle, transaction.Handle, unsafeName.Handle); @@ -116,6 +126,8 @@ public void RemoveAttribute(Transaction transaction, string name) /// The value of the attribute or null if it doesn't exist. public string? GetAttribute(Transaction transaction, string name) { + ThrowIfDeleted(); + using var unsafeName = MemoryWriter.WriteUtf8String(name); var handle = XmlTextChannel.GetAttribute(Handle, transaction.Handle, unsafeName.Handle); @@ -130,6 +142,8 @@ public void RemoveAttribute(Transaction transaction, string name) /// The instance. public XmlAttributeIterator Iterate(Transaction transaction) { + ThrowIfDeleted(); + var handle = XmlTextChannel.AttributeIterator(Handle, transaction.Handle).Checked(); return new XmlAttributeIterator(handle); @@ -142,6 +156,8 @@ public XmlAttributeIterator Iterate(Transaction transaction) /// The string representation of the instance. public string String(Transaction transaction) { + ThrowIfDeleted(); + var handle = XmlTextChannel.String(Handle, transaction.Handle); return MemoryReader.ReadStringAndDestroy(handle); @@ -158,6 +174,8 @@ public string String(Transaction transaction) /// public void RemoveRange(Transaction transaction, uint index, uint length) { + ThrowIfDeleted(); + XmlTextChannel.RemoveRange(Handle, transaction.Handle, index, length); } @@ -176,6 +194,8 @@ public void RemoveRange(Transaction transaction, uint index, uint length) /// public void Format(Transaction transaction, uint index, uint length, Input attributes) { + ThrowIfDeleted(); + using var unsafeAttributes = MemoryWriter.WriteStruct(attributes?.InputNative); XmlTextChannel.Format(Handle, transaction.Handle, index, length, unsafeAttributes.Handle); @@ -192,6 +212,8 @@ public void Format(Transaction transaction, uint index, uint length, Input attri /// public Output? PreviousSibling(Transaction transaction) { + ThrowIfDeleted(); + var handle = XmlChannel.PreviousSibling(Handle, transaction.Handle); return handle != nint.Zero ? Output.CreateAndRelease(handle, Doc) : null; @@ -208,6 +230,8 @@ public void Format(Transaction transaction, uint index, uint length, Input attri /// public Output? NextSibling(Transaction transaction) { + ThrowIfDeleted(); + var handle = XmlChannel.NextSibling(Handle, transaction.Handle); return handle != nint.Zero ? Output.CreateAndRelease(handle, Doc) : null; @@ -223,6 +247,8 @@ public void Format(Transaction transaction, uint index, uint length, Input attri /// The subscription for the event. It may be used to unsubscribe later. public IDisposable Observe(Action action) { + ThrowIfDeleted(); + return onObserve.Subscribe(action); } @@ -236,6 +262,8 @@ public IDisposable Observe(Action action) /// The in the with the given . public StickyIndex? StickyIndex(Transaction transaction, uint index, StickyAssociationType associationType) { + ThrowIfDeleted(); + var handle = StickyIndexChannel.FromIndex(Handle, transaction.Handle, index, (sbyte)associationType); return handle != nint.Zero ? new StickyIndex(handle) : null; diff --git a/YDotNet/Infrastructure/MemoryReader.cs b/YDotNet/Infrastructure/MemoryReader.cs index a761a137..82d4af6c 100644 --- a/YDotNet/Infrastructure/MemoryReader.cs +++ b/YDotNet/Infrastructure/MemoryReader.cs @@ -16,62 +16,49 @@ internal static unsafe byte[] ReadBytes(nint handle, uint length) return data; } - internal static T ReadStruct(nint handle) - where T : struct - { - return Marshal.PtrToStructure(handle); - } - - internal static T[] ReadStructArray(nint handle, uint length) - where T : struct + internal static T[] ReadStructs(nint handle, uint length) where T : struct { var itemSize = Marshal.SizeOf(); var itemBuffer = new T[length]; for (var i = 0; i < length; i++) { - itemBuffer[i] = Marshal.PtrToStructure(handle + (i * itemSize)); + itemBuffer[i] = Marshal.PtrToStructure(handle); + handle += itemSize; } return itemBuffer; } - internal static byte[]? TryReadBytes(nint handle, uint length) + internal static nint[] ReadPointers(nint handle, uint length) where T : struct { - return handle == nint.Zero ? null : ReadBytes(handle, length); - } - - internal static IEnumerable ReadIntPtrArray(nint handle, uint length, int size) - { - var itemSize = size; + var itemSize = Marshal.SizeOf(); + var itemBuffer = new nint[length]; for (var i = 0; i < length; i++) { - yield return handle; - + itemBuffer[i] = handle; handle += itemSize; } + + return itemBuffer; } - internal static IEnumerable> ReadIntPtrArray(nint handle, uint length) where T : struct + internal static NativeWithHandle[] ReadStructsWithHandles(nint handle, uint length) where T : struct { var itemSize = Marshal.SizeOf(); + var itemBuffer = new NativeWithHandle[length]; for (var i = 0; i < length; i++) { - yield return new NativeWithHandle(Marshal.PtrToStructure(handle), handle); - + itemBuffer[i] = new NativeWithHandle(Marshal.PtrToStructure(handle), handle); handle += itemSize; } - } - internal static nint[]? TryReadIntPtrArray(nint handle, uint length, int size) - { - return handle == nint.Zero ? null : ReadIntPtrArray(handle, length, size).ToArray(); + return itemBuffer; } - internal static T PtrToStruct(nint handle) - where T : struct + internal static T ReadStruct(nint handle) where T : struct { return Marshal.PtrToStructure(handle.Checked()); } diff --git a/YDotNet/Infrastructure/TypeBase.cs b/YDotNet/Infrastructure/TypeBase.cs index 8e893a5c..d409df64 100644 --- a/YDotNet/Infrastructure/TypeBase.cs +++ b/YDotNet/Infrastructure/TypeBase.cs @@ -5,14 +5,24 @@ namespace YDotNet.Infrastructure; /// public abstract class TypeBase : ITypeBase { - private bool isDeleted; + /// + /// Initializes a new instance of the class. + /// + /// A value indicating if the instance is deleted. + protected TypeBase(bool isDeleted) + { + IsDeleted = isDeleted; + } + + /// + public bool IsDeleted { get; private set; } /// /// Throws an exception if the type is deleted. /// protected void ThrowIfDeleted() { - if (isDeleted) + if (IsDeleted) { throw new ObjectDisposedException(GetType().Name); } @@ -21,7 +31,7 @@ protected void ThrowIfDeleted() /// public void MarkDeleted() { - isDeleted = true; + IsDeleted = true; } } @@ -34,4 +44,9 @@ public interface ITypeBase /// Marks the object as deleted to stop all further calls. /// void MarkDeleted(); + + /// + /// Gets a value indicating whether the instance is deleted. + /// + bool IsDeleted { get; } } diff --git a/YDotNet/Infrastructure/TypeCache.cs b/YDotNet/Infrastructure/TypeCache.cs index 9bbebbb5..6967a8d0 100644 --- a/YDotNet/Infrastructure/TypeCache.cs +++ b/YDotNet/Infrastructure/TypeCache.cs @@ -2,7 +2,7 @@ namespace YDotNet.Infrastructure; internal class TypeCache { - private readonly Dictionary cache = new Dictionary(); + private readonly Dictionary> cache = new(); public T GetOrAdd(nint handle, Func factory) where T : ITypeBase @@ -12,7 +12,9 @@ public T GetOrAdd(nint handle, Func factory) ThrowHelper.ArgumentException("Cannot create object for null handle.", nameof(handle)); } - if (cache.TryGetValue(handle, out var item)) + Cleanup(); + + if (cache.TryGetValue(handle, out var weakRef) && weakRef.TryGetTarget(out var item)) { if (item is not T typed) { @@ -24,16 +26,43 @@ public T GetOrAdd(nint handle, Func factory) } var typedItem = factory(handle); - cache[handle] = typedItem; + + cache[handle] = new WeakReference(typedItem); return typedItem; } + private void Cleanup() + { + List? keysToDelete = null; + + foreach (var (key, weakRef) in cache) + { + if (!weakRef.TryGetTarget(out _)) + { + keysToDelete ??= new List(); + keysToDelete.Add(key); + } + } + + if (keysToDelete != null) + { + foreach (var key in keysToDelete) + { + cache.Remove(key); + } + } + } + public void Delete(nint handle) { - if (cache.TryGetValue(handle, out var item)) + if (cache.TryGetValue(handle, out var weakRef)) { - item.MarkDeleted(); + if (weakRef.TryGetTarget(out var item)) + { + item.MarkDeleted(); + } + cache.Remove(handle); } } diff --git a/YDotNet/Native/Document/Events/SubDocsEventNative.cs b/YDotNet/Native/Document/Events/SubDocsEventNative.cs index a6695e8b..f257ed1b 100644 --- a/YDotNet/Native/Document/Events/SubDocsEventNative.cs +++ b/YDotNet/Native/Document/Events/SubDocsEventNative.cs @@ -25,7 +25,7 @@ public nint[] Added() return Array.Empty(); } - return MemoryReader.ReadStructArray(AddedHandle, AddedLength); + return MemoryReader.ReadStructs(AddedHandle, AddedLength); } public nint[] Removed() @@ -35,7 +35,7 @@ public nint[] Removed() return Array.Empty(); } - return MemoryReader.ReadStructArray(RemovedHandle, RemovedLength); + return MemoryReader.ReadStructs(RemovedHandle, RemovedLength); } public nint[] Loaded() @@ -45,6 +45,6 @@ public nint[] Loaded() return Array.Empty(); } - return MemoryReader.ReadStructArray(LoadedHandle, LoadedLength); + return MemoryReader.ReadStructs(LoadedHandle, LoadedLength); } } diff --git a/YDotNet/Native/Document/State/DeleteSetNative.cs b/YDotNet/Native/Document/State/DeleteSetNative.cs index 92fb0138..d4d1da2f 100644 --- a/YDotNet/Native/Document/State/DeleteSetNative.cs +++ b/YDotNet/Native/Document/State/DeleteSetNative.cs @@ -14,11 +14,11 @@ internal readonly struct DeleteSetNative public ulong[] Clients() { - return MemoryReader.ReadStructArray(ClientIdsHandle, EntriesCount); + return MemoryReader.ReadStructs(ClientIdsHandle, EntriesCount); } public IdRangeSequenceNative[] Ranges() { - return MemoryReader.ReadStructArray(RangesHandle, EntriesCount); + return MemoryReader.ReadStructs(RangesHandle, EntriesCount); } } diff --git a/YDotNet/Native/Document/State/IdRangeSequenceNative.cs b/YDotNet/Native/Document/State/IdRangeSequenceNative.cs index b3cdb014..74280ef5 100644 --- a/YDotNet/Native/Document/State/IdRangeSequenceNative.cs +++ b/YDotNet/Native/Document/State/IdRangeSequenceNative.cs @@ -12,6 +12,6 @@ internal readonly struct IdRangeSequenceNative public IdRangeNative[] Sequence() { - return MemoryReader.ReadStructArray(SequenceHandle, SequenceLength); + return MemoryReader.ReadStructs(SequenceHandle, SequenceLength); } } diff --git a/YDotNet/Native/Document/State/StateVectorNative.cs b/YDotNet/Native/Document/State/StateVectorNative.cs index 48c79400..a0d569c6 100644 --- a/YDotNet/Native/Document/State/StateVectorNative.cs +++ b/YDotNet/Native/Document/State/StateVectorNative.cs @@ -12,11 +12,11 @@ internal readonly struct StateVectorNative public ulong[] ClientIds() { - return MemoryReader.ReadStructArray(ClientIdsHandle, EntriesCount); + return MemoryReader.ReadStructs(ClientIdsHandle, EntriesCount); } public uint[] Clocks() { - return MemoryReader.ReadStructArray(ClientIdsHandle, EntriesCount); + return MemoryReader.ReadStructs(ClientIdsHandle, EntriesCount); } } diff --git a/YDotNet/Native/Types/Events/EventChangeNative.cs b/YDotNet/Native/Types/Events/EventChangeNative.cs index 41187f3d..89f000f0 100644 --- a/YDotNet/Native/Types/Events/EventChangeNative.cs +++ b/YDotNet/Native/Types/Events/EventChangeNative.cs @@ -22,7 +22,7 @@ public nint[] ValuesHandles return Array.Empty(); } - return MemoryReader.ReadIntPtrArray(Values, Length, Marshal.SizeOf()).ToArray(); + return MemoryReader.ReadPointers(Values, Length).ToArray(); } } } diff --git a/YDotNet/Native/Types/Events/EventDeltaNative.cs b/YDotNet/Native/Types/Events/EventDeltaNative.cs index b31f8e5e..bd474cd4 100644 --- a/YDotNet/Native/Types/Events/EventDeltaNative.cs +++ b/YDotNet/Native/Types/Events/EventDeltaNative.cs @@ -26,7 +26,7 @@ public NativeWithHandle[] Attributes return Array.Empty>(); } - return MemoryReader.ReadIntPtrArray(AttributesHandle, AttributesLength).ToArray(); + return MemoryReader.ReadStructsWithHandles(AttributesHandle, AttributesLength).ToArray(); } } } diff --git a/YDotNet/Native/Types/Events/EventPathSegmentNative.cs b/YDotNet/Native/Types/Events/EventPathSegmentNative.cs new file mode 100644 index 00000000..3004dfe7 --- /dev/null +++ b/YDotNet/Native/Types/Events/EventPathSegmentNative.cs @@ -0,0 +1,17 @@ +using System.Runtime.InteropServices; +using YDotNet.Infrastructure; + +[StructLayout(LayoutKind.Explicit)] +internal readonly struct EventPathSegmentNative +{ + [field: FieldOffset(0)] + public byte Tag { get; } + + [field: FieldOffset(8)] + public nint KeyOrIndex { get; } + + public string Key() + { + return MemoryReader.ReadUtf8String(KeyOrIndex); + } +} diff --git a/YDotNet/Native/Types/Texts/TextChunkNative.cs b/YDotNet/Native/Types/Texts/TextChunkNative.cs index 61255c45..5cb037cb 100644 --- a/YDotNet/Native/Types/Texts/TextChunkNative.cs +++ b/YDotNet/Native/Types/Texts/TextChunkNative.cs @@ -26,6 +26,6 @@ public NativeWithHandle[] Attributes() return Array.Empty>(); } - return MemoryReader.ReadIntPtrArray(AttributesHandle, AttributesLength).ToArray(); + return MemoryReader.ReadStructsWithHandles(AttributesHandle, AttributesLength).ToArray(); } } diff --git a/YDotNet/Native/Types/XmlAttributeNative.cs b/YDotNet/Native/Types/XmlAttributeNative.cs new file mode 100644 index 00000000..d87315c5 --- /dev/null +++ b/YDotNet/Native/Types/XmlAttributeNative.cs @@ -0,0 +1,22 @@ +using System.Runtime.InteropServices; +using YDotNet.Infrastructure; + +namespace YDotNet.Native.Types; + +[StructLayout(LayoutKind.Sequential)] +internal readonly struct XmlAttributeNative +{ + public nint KeyHandle { get; } + + public nint ValueHandle { get; } + + public string Key() + { + return MemoryReader.ReadUtf8String(KeyHandle); + } + + public string Value() + { + return MemoryReader.ReadUtf8String(ValueHandle); + } +} From c2dc276115f884f27ed12bf9efd745e2bcf4cba3 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 13 Oct 2023 08:30:37 +0200 Subject: [PATCH 093/186] Fix dispose. --- YDotNet/Document/Doc.cs | 31 +++++++++++++---------------- YDotNet/Infrastructure/TypeBase.cs | 24 +++++++++++----------- YDotNet/Infrastructure/TypeCache.cs | 2 +- 3 files changed, 27 insertions(+), 30 deletions(-) diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index 071f2e74..60c552fc 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -34,7 +34,7 @@ namespace YDotNet.Document; /// to recursively nested types). /// /// -public class Doc : Resource, ITypeBase +public class Doc : TypeBase, IDisposable { private readonly TypeCache typeCache = new(); private readonly EventSubscriber onClear; @@ -66,6 +66,7 @@ public Doc(DocOptions options) } internal Doc(nint handle, Doc? parent, bool isDeleted) + : base(isDeleted) { this.parent = parent; @@ -125,11 +126,6 @@ internal Doc(nint handle, Doc? parent, bool isDeleted) (doc, s) => DocChannel.UnobserveSubDocs(doc, s)); Handle = handle; - - if (isDeleted) - { - Dispose(); - } } private static nint CreateDoc(DocOptions options) @@ -142,23 +138,24 @@ private static nint CreateDoc(DocOptions options) /// ~Doc() { - Dispose(false); + DisposeCore(); } - /// - protected internal override void DisposeCore(bool disposing) + public void Dispose() { - if (parent != null) - { - DocChannel.Destroy(Handle); - } + GC.SuppressFinalize(this); + DisposeCore(); } - bool ITypeBase.IsDeleted => IsDisposed; - - void ITypeBase.MarkDeleted() + private void DisposeCore() { - Dispose(); + if (IsDisposed) + { + return; + } + + DocChannel.Destroy(Handle); + MarkDisposed(); } /// diff --git a/YDotNet/Infrastructure/TypeBase.cs b/YDotNet/Infrastructure/TypeBase.cs index d409df64..1ce13a86 100644 --- a/YDotNet/Infrastructure/TypeBase.cs +++ b/YDotNet/Infrastructure/TypeBase.cs @@ -8,30 +8,30 @@ public abstract class TypeBase : ITypeBase /// /// Initializes a new instance of the class. /// - /// A value indicating if the instance is deleted. - protected TypeBase(bool isDeleted) + /// A value indicating if the instance is deleted. + protected TypeBase(bool isDisposed) { - IsDeleted = isDeleted; + IsDisposed = isDisposed; } /// - public bool IsDeleted { get; private set; } + public bool IsDisposed { get; private set; } /// - /// Throws an exception if the type is deleted. + /// Throws an exception if the type is disposed. /// protected void ThrowIfDeleted() { - if (IsDeleted) + if (IsDisposed) { throw new ObjectDisposedException(GetType().Name); } } /// - public void MarkDeleted() + public void MarkDisposed() { - IsDeleted = true; + IsDisposed = true; } } @@ -41,12 +41,12 @@ public void MarkDeleted() public interface ITypeBase { /// - /// Marks the object as deleted to stop all further calls. + /// Marks the object as disposed to stop all further calls. /// - void MarkDeleted(); + void MarkDisposed(); /// - /// Gets a value indicating whether the instance is deleted. + /// Gets a value indicating whether the instance is disposed. /// - bool IsDeleted { get; } + bool IsDisposed { get; } } diff --git a/YDotNet/Infrastructure/TypeCache.cs b/YDotNet/Infrastructure/TypeCache.cs index 6967a8d0..5ae6e613 100644 --- a/YDotNet/Infrastructure/TypeCache.cs +++ b/YDotNet/Infrastructure/TypeCache.cs @@ -60,7 +60,7 @@ public void Delete(nint handle) { if (weakRef.TryGetTarget(out var item)) { - item.MarkDeleted(); + item.MarkDisposed(); } cache.Remove(handle); From 71e86e9cc0adf3e03fee23808058df55170af48f Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 13 Oct 2023 17:25:46 +0200 Subject: [PATCH 094/186] Several small fixes. --- .../Document/SubDocsTests.cs | 1 - .../Document/UpdatesV1Tests.cs | 2 +- Tests/YDotNet.Tests.Unit/Program.cs | 9 ++- .../Transactions/StateVectorV1Tests.cs | 6 +- YDotNet/Document/Doc.cs | 60 +++++++++++++++---- YDotNet/Document/Types/Arrays/Array.cs | 16 ++--- YDotNet/Document/Types/Branches/Branch.cs | 4 +- YDotNet/Document/Types/Maps/Map.cs | 14 ++--- YDotNet/Document/Types/Texts/Text.cs | 18 +++--- .../Document/Types/XmlElements/XmlElement.cs | 34 +++++------ YDotNet/Document/Types/XmlTexts/XmlText.cs | 28 ++++----- YDotNet/Infrastructure/TypeBase.cs | 2 +- 12 files changed, 115 insertions(+), 79 deletions(-) diff --git a/Tests/YDotNet.Tests.Unit/Document/SubDocsTests.cs b/Tests/YDotNet.Tests.Unit/Document/SubDocsTests.cs index 80b32403..5a019137 100644 --- a/Tests/YDotNet.Tests.Unit/Document/SubDocsTests.cs +++ b/Tests/YDotNet.Tests.Unit/Document/SubDocsTests.cs @@ -78,7 +78,6 @@ public void TriggersWhenRemovingSubDocsUntilUnobserved() Assert.That(subDocsEvent.Added, Is.Empty); Assert.That(subDocsEvent.Loaded, Is.Empty); Assert.That(subDocsEvent.Removed, Has.Count.EqualTo(expected: 1)); - Assert.That(subDocsEvent.Removed[0].Id, Is.EqualTo(subDoc1.Id)); // Act subDocsEvent = null; diff --git a/Tests/YDotNet.Tests.Unit/Document/UpdatesV1Tests.cs b/Tests/YDotNet.Tests.Unit/Document/UpdatesV1Tests.cs index 07b80234..687fba32 100644 --- a/Tests/YDotNet.Tests.Unit/Document/UpdatesV1Tests.cs +++ b/Tests/YDotNet.Tests.Unit/Document/UpdatesV1Tests.cs @@ -41,7 +41,7 @@ public void TriggersWhenTransactionIsCommittedUntilUnobserve() // Assert Assert.That(called, Is.EqualTo(expected: 2)); Assert.That(data, Is.Not.Null); - Assert.That(data, Has.Length.InRange(from: 20, to: 31)); + Assert.That(data, Has.Length.InRange(from: 18, to: 31)); // Act data = null; diff --git a/Tests/YDotNet.Tests.Unit/Program.cs b/Tests/YDotNet.Tests.Unit/Program.cs index f49447eb..3ef819bf 100644 --- a/Tests/YDotNet.Tests.Unit/Program.cs +++ b/Tests/YDotNet.Tests.Unit/Program.cs @@ -89,7 +89,10 @@ public static void Main() GC.Collect(); try { - method.Invoke(instance, null); + for (var j = 0; j < 10000; j++) + { + method.Invoke(instance, null); + } Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine("Success"); @@ -98,13 +101,13 @@ public static void Main() catch (RuntimeWrappedException ex) { Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine("Failed: {0}", ex.Message); + Console.WriteLine("Failed: {0}", ex); Console.ForegroundColor = defaultColor; } catch (Exception ex) { Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine("Failed: {0}", ex.Message); + Console.WriteLine("Failed: {0}", ex); Console.ForegroundColor = defaultColor; } } diff --git a/Tests/YDotNet.Tests.Unit/Transactions/StateVectorV1Tests.cs b/Tests/YDotNet.Tests.Unit/Transactions/StateVectorV1Tests.cs index 2e9cec7b..ecec4736 100644 --- a/Tests/YDotNet.Tests.Unit/Transactions/StateVectorV1Tests.cs +++ b/Tests/YDotNet.Tests.Unit/Transactions/StateVectorV1Tests.cs @@ -12,11 +12,11 @@ public void ReadOnly() var doc = ArrangeDoc(); // Act - var stateVector = doc.ReadTransaction().StateVectorV1(); + var stateVector = doc.WriteTransaction().StateVectorV1(); // Assert Assert.That(stateVector, Is.Not.Null); - Assert.That(stateVector.Length, Is.GreaterThan(expected: 5)); + Assert.That(stateVector.Length, Is.GreaterThanOrEqualTo(expected: 4)); } [Test] @@ -30,7 +30,7 @@ public void ReadWrite() // Assert Assert.That(stateVector, Is.Not.Null); - Assert.That(stateVector.Length, Is.GreaterThan(expected: 5)); + Assert.That(stateVector.Length, Is.GreaterThanOrEqualTo(expected: 5)); } private static Doc ArrangeDoc() diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index 60c552fc..4cdfe0be 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -161,43 +161,77 @@ private void DisposeCore() /// /// Gets the unique client identifier of this instance. /// - public ulong Id => DocChannel.Id(Handle); + public ulong Id + { + get + { + ThrowIfDisposed(); + + return DocChannel.Id(Handle); + } + } /// /// Gets the unique document identifier of this instance. /// - public string Guid => MemoryReader.ReadUtf8String(DocChannel.Guid(Handle)); + public string Guid + { + get + { + ThrowIfDisposed(); + + return MemoryReader.ReadUtf8String(DocChannel.Guid(Handle)); + } + } /// - /// Gets the collection identifier of this instance. + /// Gets a value indicating whether this instance requested a data load. /// /// - /// If none was defined, a null will be returned. + /// This flag is often used by the parent instance to check if this instance requested a data load. /// - public string? CollectionId + public bool ShouldLoad { get { - MemoryReader.TryReadUtf8String(DocChannel.CollectionId(Handle), out var result); - return result; + ThrowIfDisposed(); + + return DocChannel.ShouldLoad(Handle); } } /// - /// Gets a value indicating whether this instance requested a data load. + /// Gets a value indicating whether this instance will auto load. /// /// - /// This flag is often used by the parent instance to check if this instance requested a data load. + /// Auto loaded nested instances automatically send a load request to their parent instances. /// - public bool ShouldLoad => DocChannel.ShouldLoad(Handle); + public bool AutoLoad + { + get + { + ThrowIfDisposed(); + + return DocChannel.AutoLoad(Handle); + } + } /// - /// Gets a value indicating whether this instance will auto load. + /// Gets the collection identifier of this instance. /// /// - /// Auto loaded nested instances automatically send a load request to their parent instances. + /// If none was defined, a null will be returned. /// - public bool AutoLoad => DocChannel.AutoLoad(Handle); + public string? CollectionId + { + get + { + ThrowIfDisposed(); + + MemoryReader.TryReadUtf8String(DocChannel.CollectionId(Handle), out var result); + return result; + } + } internal nint Handle { get; } diff --git a/YDotNet/Document/Types/Arrays/Array.cs b/YDotNet/Document/Types/Arrays/Array.cs index 2f17c517..79a37683 100644 --- a/YDotNet/Document/Types/Arrays/Array.cs +++ b/YDotNet/Document/Types/Arrays/Array.cs @@ -39,7 +39,7 @@ public uint Length { get { - ThrowIfDeleted(); + ThrowIfDisposed(); return ArrayChannel.Length(Handle); } @@ -53,7 +53,7 @@ public uint Length /// The items to be inserted. public void InsertRange(Transaction transaction, uint index, params Input[] inputs) { - ThrowIfDeleted(); + ThrowIfDisposed(); using var unsafeInputs = MemoryWriter.WriteStructArray(inputs.Select(x => x.InputNative).ToArray()); @@ -68,7 +68,7 @@ public void InsertRange(Transaction transaction, uint index, params Input[] inpu /// The amount of items to remove. public void RemoveRange(Transaction transaction, uint index, uint length) { - ThrowIfDeleted(); + ThrowIfDisposed(); ArrayChannel.RemoveRange(Handle, transaction.Handle, index, length); } @@ -85,7 +85,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) /// public Output? Get(Transaction transaction, uint index) { - ThrowIfDeleted(); + ThrowIfDisposed(); var handle = ArrayChannel.Get(Handle, transaction.Handle, index); @@ -103,7 +103,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) /// The index to which the item will be moved to. public void Move(Transaction transaction, uint sourceIndex, uint targetIndex) { - ThrowIfDeleted(); + ThrowIfDisposed(); ArrayChannel.Move(Handle, transaction.Handle, sourceIndex, targetIndex); } @@ -116,7 +116,7 @@ public void Move(Transaction transaction, uint sourceIndex, uint targetIndex) /// The instance. public ArrayIterator Iterate(Transaction transaction) { - ThrowIfDeleted(); + ThrowIfDisposed(); var handle = ArrayChannel.Iterator(Handle, transaction.Handle); @@ -133,7 +133,7 @@ public ArrayIterator Iterate(Transaction transaction) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable Observe(Action action) { - ThrowIfDeleted(); + ThrowIfDisposed(); return onObserve.Subscribe(action); } @@ -148,7 +148,7 @@ public IDisposable Observe(Action action) /// The in the with the given . public StickyIndex? StickyIndex(Transaction transaction, uint index, StickyAssociationType associationType) { - ThrowIfDeleted(); + ThrowIfDisposed(); var handle = StickyIndexChannel.FromIndex(Handle, transaction.Handle, index, (sbyte)associationType); diff --git a/YDotNet/Document/Types/Branches/Branch.cs b/YDotNet/Document/Types/Branches/Branch.cs index 7ef0ab57..443a5389 100644 --- a/YDotNet/Document/Types/Branches/Branch.cs +++ b/YDotNet/Document/Types/Branches/Branch.cs @@ -64,7 +64,7 @@ public IDisposable ObserveDeep(Action> action) /// Another write transaction has been created and not commited yet. public Transaction WriteTransaction() { - ThrowIfDeleted(); + ThrowIfDisposed(); var handle = BranchChannel.WriteTransaction(Handle); @@ -84,7 +84,7 @@ public Transaction WriteTransaction() /// Another write transaction has been created and not commited yet. public Transaction ReadTransaction() { - ThrowIfDeleted(); + ThrowIfDisposed(); var handle = BranchChannel.ReadTransaction(Handle); diff --git a/YDotNet/Document/Types/Maps/Map.cs b/YDotNet/Document/Types/Maps/Map.cs index ce99aaec..1f6d0fa9 100644 --- a/YDotNet/Document/Types/Maps/Map.cs +++ b/YDotNet/Document/Types/Maps/Map.cs @@ -41,7 +41,7 @@ internal Map(nint handle, Doc doc, bool isDeleted) /// The instance to be inserted. public void Insert(Transaction transaction, string key, Input input) { - ThrowIfDeleted(); + ThrowIfDisposed(); using var unsafeKey = MemoryWriter.WriteUtf8String(key); using var unsafeValue = MemoryWriter.WriteStruct(input.InputNative); @@ -60,7 +60,7 @@ public void Insert(Transaction transaction, string key, Input input) /// The or null if entry not found. public Output? Get(Transaction transaction, string key) { - ThrowIfDeleted(); + ThrowIfDisposed(); using var unsafeName = MemoryWriter.WriteUtf8String(key); @@ -76,7 +76,7 @@ public void Insert(Transaction transaction, string key, Input input) /// The number of entries stored in the . public uint Length(Transaction transaction) { - ThrowIfDeleted(); + ThrowIfDisposed(); return MapChannel.Length(Handle, transaction.Handle); } @@ -89,7 +89,7 @@ public uint Length(Transaction transaction) /// `true` if the entry was found and removed, `false` if no entry was found. public bool Remove(Transaction transaction, string key) { - ThrowIfDeleted(); + ThrowIfDisposed(); using var unsafeKey = MemoryWriter.WriteUtf8String(key); @@ -102,7 +102,7 @@ public bool Remove(Transaction transaction, string key) /// The transaction that wraps this operation. public void RemoveAll(Transaction transaction) { - ThrowIfDeleted(); + ThrowIfDisposed(); MapChannel.RemoveAll(Handle, transaction.Handle); } @@ -115,7 +115,7 @@ public void RemoveAll(Transaction transaction) /// The instance. public MapIterator Iterate(Transaction transaction) { - ThrowIfDeleted(); + ThrowIfDisposed(); var handle = MapChannel.Iterator(Handle, transaction.Handle).Checked(); @@ -132,7 +132,7 @@ public MapIterator Iterate(Transaction transaction) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable Observe(Action action) { - ThrowIfDeleted(); + ThrowIfDisposed(); return onObserve.Subscribe(action); } diff --git a/YDotNet/Document/Types/Texts/Text.cs b/YDotNet/Document/Types/Texts/Text.cs index 176b8545..dc71f41a 100644 --- a/YDotNet/Document/Types/Texts/Text.cs +++ b/YDotNet/Document/Types/Texts/Text.cs @@ -44,7 +44,7 @@ internal Text(nint handle, Doc doc, bool isDeleted) /// public void Insert(Transaction transaction, uint index, string value, Input? attributes = null) { - ThrowIfDeleted(); + ThrowIfDisposed(); using var unsafeValue = MemoryWriter.WriteUtf8String(value); using var unsafeAttributes = MemoryWriter.WriteStruct(attributes?.InputNative); @@ -64,7 +64,7 @@ public void Insert(Transaction transaction, uint index, string value, Input? att /// public void InsertEmbed(Transaction transaction, uint index, Input content, Input? attributes = null) { - ThrowIfDeleted(); + ThrowIfDisposed(); var unsafeContent = MemoryWriter.WriteStruct(content.InputNative); var unsafeAttributes = MemoryWriter.WriteStruct(attributes?.InputNative); @@ -83,7 +83,7 @@ public void InsertEmbed(Transaction transaction, uint index, Input content, Inpu /// public void RemoveRange(Transaction transaction, uint index, uint length) { - ThrowIfDeleted(); + ThrowIfDisposed(); TextChannel.RemoveRange(Handle, transaction.Handle, index, length); } @@ -103,7 +103,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) /// public void Format(Transaction transaction, uint index, uint length, Input attributes) { - ThrowIfDeleted(); + ThrowIfDisposed(); using var unsafeAttributes = MemoryWriter.WriteStruct(attributes.InputNative); @@ -117,7 +117,7 @@ public void Format(Transaction transaction, uint index, uint length, Input attri /// The that compose this . public TextChunks Chunks(Transaction transaction) { - ThrowIfDeleted(); + ThrowIfDisposed(); var handle = TextChannel.Chunks(Handle, transaction.Handle, out var length).Checked(); @@ -131,7 +131,7 @@ public TextChunks Chunks(Transaction transaction) /// The full string stored in the instance. public string String(Transaction transaction) { - ThrowIfDeleted(); + ThrowIfDisposed(); var handle = TextChannel.String(Handle, transaction.Handle); @@ -148,7 +148,7 @@ public string String(Transaction transaction) /// The length, in bytes, of the string stored in the instance. public uint Length(Transaction transaction) { - ThrowIfDeleted(); + ThrowIfDisposed(); return TextChannel.Length(Handle, transaction.Handle); } @@ -160,7 +160,7 @@ public uint Length(Transaction transaction) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable Observe(Action action) { - ThrowIfDeleted(); + ThrowIfDisposed(); return onObserve.Subscribe(action); } @@ -175,7 +175,7 @@ public IDisposable Observe(Action action) /// The in the with the given . public StickyIndex? StickyIndex(Transaction transaction, uint index, StickyAssociationType associationType) { - ThrowIfDeleted(); + ThrowIfDisposed(); var handle = StickyIndexChannel.FromIndex(Handle, transaction.Handle, index, (sbyte)associationType); diff --git a/YDotNet/Document/Types/XmlElements/XmlElement.cs b/YDotNet/Document/Types/XmlElements/XmlElement.cs index 0c5be42f..c70c1ece 100644 --- a/YDotNet/Document/Types/XmlElements/XmlElement.cs +++ b/YDotNet/Document/Types/XmlElements/XmlElement.cs @@ -43,7 +43,7 @@ public string? Tag { get { - ThrowIfDeleted(); + ThrowIfDisposed(); var handle = XmlElementChannel.Tag(Handle); @@ -61,7 +61,7 @@ public string? Tag /// The string representation of the instance. public string String(Transaction transaction) { - ThrowIfDeleted(); + ThrowIfDisposed(); var handle = XmlElementChannel.String(Handle, transaction.Handle); @@ -79,7 +79,7 @@ public string String(Transaction transaction) /// The value of the attribute to be added. public void InsertAttribute(Transaction transaction, string name, string value) { - ThrowIfDeleted(); + ThrowIfDisposed(); using var unsageName = MemoryWriter.WriteUtf8String(name); using var unsafeValue = MemoryWriter.WriteUtf8String(value); @@ -94,7 +94,7 @@ public void InsertAttribute(Transaction transaction, string name, string value) /// The name of the attribute to be removed. public void RemoveAttribute(Transaction transaction, string name) { - ThrowIfDeleted(); + ThrowIfDisposed(); using var unsafeName = MemoryWriter.WriteUtf8String(name); @@ -109,7 +109,7 @@ public void RemoveAttribute(Transaction transaction, string name) /// The value of the attribute or null if it doesn't exist. public string? GetAttribute(Transaction transaction, string name) { - ThrowIfDeleted(); + ThrowIfDisposed(); using var unsafeName = MemoryWriter.WriteUtf8String(name); @@ -126,7 +126,7 @@ public void RemoveAttribute(Transaction transaction, string name) /// The instance or null if failed. public XmlAttributeIterator Iterate(Transaction transaction) { - ThrowIfDeleted(); + ThrowIfDisposed(); var handle = XmlElementChannel.AttributeIterator(Handle, transaction.Handle); @@ -144,7 +144,7 @@ public XmlAttributeIterator Iterate(Transaction transaction) /// The number of direct child nodes of this . public uint ChildLength(Transaction transaction) { - ThrowIfDeleted(); + ThrowIfDisposed(); return XmlElementChannel.ChildLength(Handle, transaction.Handle); } @@ -158,7 +158,7 @@ public uint ChildLength(Transaction transaction) /// The inserted at the given . public XmlText InsertText(Transaction transaction, uint index) { - ThrowIfDeleted(); + ThrowIfDisposed(); var handle = XmlElementChannel.InsertText(Handle, transaction.Handle, index); @@ -175,7 +175,7 @@ public XmlText InsertText(Transaction transaction, uint index) /// The inserted at the given . public XmlElement InsertElement(Transaction transaction, uint index, string name) { - ThrowIfDeleted(); + ThrowIfDisposed(); using var unsafeName = MemoryWriter.WriteUtf8String(name); @@ -193,7 +193,7 @@ public XmlElement InsertElement(Transaction transaction, uint index, string name /// The amount of child nodes to remove, starting at . public void RemoveRange(Transaction transaction, uint index, uint length) { - ThrowIfDeleted(); + ThrowIfDisposed(); XmlElementChannel.RemoveRange(Handle, transaction.Handle, index, length); } @@ -206,7 +206,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) /// An cell or null if the is out of bounds. public Output? Get(Transaction transaction, uint index) { - ThrowIfDeleted(); + ThrowIfDisposed(); var handle = XmlElementChannel.Get(Handle, transaction.Handle, index); @@ -225,7 +225,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) /// public Output? PreviousSibling(Transaction transaction) { - ThrowIfDeleted(); + ThrowIfDisposed(); var handle = XmlChannel.PreviousSibling(Handle, transaction.Handle); @@ -244,7 +244,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) /// public Output? NextSibling(Transaction transaction) { - ThrowIfDeleted(); + ThrowIfDisposed(); var handle = XmlChannel.NextSibling(Handle, transaction.Handle); @@ -262,7 +262,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) /// public Output? FirstChild(Transaction transaction) { - ThrowIfDeleted(); + ThrowIfDisposed(); var handle = XmlElementChannel.FirstChild(Handle, transaction.Handle); @@ -280,7 +280,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) /// public XmlElement? Parent(Transaction transaction) { - ThrowIfDeleted(); + ThrowIfDisposed(); var handle = XmlElementChannel.Parent(Handle, transaction.Handle); @@ -297,7 +297,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) /// An for this . public XmlTreeWalker TreeWalker(Transaction transaction) { - ThrowIfDeleted(); + ThrowIfDisposed(); var handle = XmlElementChannel.TreeWalker(Handle, transaction.Handle); @@ -314,7 +314,7 @@ public XmlTreeWalker TreeWalker(Transaction transaction) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable Observe(Action action) { - ThrowIfDeleted(); + ThrowIfDisposed(); return onObserve.Subscribe(action); } diff --git a/YDotNet/Document/Types/XmlTexts/XmlText.cs b/YDotNet/Document/Types/XmlTexts/XmlText.cs index bb66ca54..4b98bb95 100644 --- a/YDotNet/Document/Types/XmlTexts/XmlText.cs +++ b/YDotNet/Document/Types/XmlTexts/XmlText.cs @@ -40,7 +40,7 @@ internal XmlText(nint handle, Doc doc, bool isDeleted) /// The length of the text, in bytes, stored in the . public uint Length(Transaction transaction) { - ThrowIfDeleted(); + ThrowIfDisposed(); return XmlTextChannel.Length(Handle, transaction.Handle); } @@ -57,7 +57,7 @@ public uint Length(Transaction transaction) /// public void Insert(Transaction transaction, uint index, string value, Input? attributes = null) { - ThrowIfDeleted(); + ThrowIfDisposed(); using var unsafeValue = MemoryWriter.WriteUtf8String(value); using var unsafeAttributes = MemoryWriter.WriteStruct(attributes?.InputNative); @@ -77,7 +77,7 @@ public void Insert(Transaction transaction, uint index, string value, Input? att /// public void InsertEmbed(Transaction transaction, uint index, Input content, Input? attributes = null) { - ThrowIfDeleted(); + ThrowIfDisposed(); using var unsafeContent = MemoryWriter.WriteStruct(content.InputNative); using var unsafeAttributes = MemoryWriter.WriteStruct(attributes?.InputNative); @@ -96,7 +96,7 @@ public void InsertEmbed(Transaction transaction, uint index, Input content, Inpu /// The value of the attribute to be added. public void InsertAttribute(Transaction transaction, string name, string value) { - ThrowIfDeleted(); + ThrowIfDisposed(); using var unsafeName = MemoryWriter.WriteUtf8String(name); using var unsafeValue = MemoryWriter.WriteUtf8String(value); @@ -111,7 +111,7 @@ public void InsertAttribute(Transaction transaction, string name, string value) /// The name of the attribute to be removed. public void RemoveAttribute(Transaction transaction, string name) { - ThrowIfDeleted(); + ThrowIfDisposed(); using var unsafeName = MemoryWriter.WriteUtf8String(name); @@ -126,7 +126,7 @@ public void RemoveAttribute(Transaction transaction, string name) /// The value of the attribute or null if it doesn't exist. public string? GetAttribute(Transaction transaction, string name) { - ThrowIfDeleted(); + ThrowIfDisposed(); using var unsafeName = MemoryWriter.WriteUtf8String(name); @@ -142,7 +142,7 @@ public void RemoveAttribute(Transaction transaction, string name) /// The instance. public XmlAttributeIterator Iterate(Transaction transaction) { - ThrowIfDeleted(); + ThrowIfDisposed(); var handle = XmlTextChannel.AttributeIterator(Handle, transaction.Handle).Checked(); @@ -156,7 +156,7 @@ public XmlAttributeIterator Iterate(Transaction transaction) /// The string representation of the instance. public string String(Transaction transaction) { - ThrowIfDeleted(); + ThrowIfDisposed(); var handle = XmlTextChannel.String(Handle, transaction.Handle); @@ -174,7 +174,7 @@ public string String(Transaction transaction) /// public void RemoveRange(Transaction transaction, uint index, uint length) { - ThrowIfDeleted(); + ThrowIfDisposed(); XmlTextChannel.RemoveRange(Handle, transaction.Handle, index, length); } @@ -194,7 +194,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) /// public void Format(Transaction transaction, uint index, uint length, Input attributes) { - ThrowIfDeleted(); + ThrowIfDisposed(); using var unsafeAttributes = MemoryWriter.WriteStruct(attributes?.InputNative); @@ -212,7 +212,7 @@ public void Format(Transaction transaction, uint index, uint length, Input attri /// public Output? PreviousSibling(Transaction transaction) { - ThrowIfDeleted(); + ThrowIfDisposed(); var handle = XmlChannel.PreviousSibling(Handle, transaction.Handle); @@ -230,7 +230,7 @@ public void Format(Transaction transaction, uint index, uint length, Input attri /// public Output? NextSibling(Transaction transaction) { - ThrowIfDeleted(); + ThrowIfDisposed(); var handle = XmlChannel.NextSibling(Handle, transaction.Handle); @@ -247,7 +247,7 @@ public void Format(Transaction transaction, uint index, uint length, Input attri /// The subscription for the event. It may be used to unsubscribe later. public IDisposable Observe(Action action) { - ThrowIfDeleted(); + ThrowIfDisposed(); return onObserve.Subscribe(action); } @@ -262,7 +262,7 @@ public IDisposable Observe(Action action) /// The in the with the given . public StickyIndex? StickyIndex(Transaction transaction, uint index, StickyAssociationType associationType) { - ThrowIfDeleted(); + ThrowIfDisposed(); var handle = StickyIndexChannel.FromIndex(Handle, transaction.Handle, index, (sbyte)associationType); diff --git a/YDotNet/Infrastructure/TypeBase.cs b/YDotNet/Infrastructure/TypeBase.cs index 1ce13a86..abc1dd62 100644 --- a/YDotNet/Infrastructure/TypeBase.cs +++ b/YDotNet/Infrastructure/TypeBase.cs @@ -20,7 +20,7 @@ protected TypeBase(bool isDisposed) /// /// Throws an exception if the type is disposed. /// - protected void ThrowIfDeleted() + protected void ThrowIfDisposed() { if (IsDisposed) { From b6df02b9c4cac6847e8d462284f88008d84df46d Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 14 Oct 2023 09:34:42 +0200 Subject: [PATCH 095/186] Fix dispose. --- Tests/YDotNet.Tests.Unit/Program.cs | 40 +------------------ .../Document/Types/Arrays/ArrayEnumerator.cs | 1 + YDotNet/Document/Types/Maps/MapEnumerator.cs | 1 + .../Trees/XmlTreeWalkerEnumerator.cs | 1 + .../XmlElements/XmlAttributeEnumerator.cs | 1 + 5 files changed, 6 insertions(+), 38 deletions(-) diff --git a/Tests/YDotNet.Tests.Unit/Program.cs b/Tests/YDotNet.Tests.Unit/Program.cs index 3ef819bf..b26f488e 100644 --- a/Tests/YDotNet.Tests.Unit/Program.cs +++ b/Tests/YDotNet.Tests.Unit/Program.cs @@ -1,7 +1,4 @@ using System.Runtime.CompilerServices; -using YDotNet.Document; -using YDotNet.Document.Cells; -using YDotNet.Document.Types.Maps; namespace YDotNet.Tests.Unit; @@ -9,40 +6,7 @@ public class Program { public static void Main() { - /* - var doc = new Doc(); - var a = doc.Map("A"); - Map b; - using (var t = doc.WriteTransaction()) - { - - a.Insert(t, "B", Input.Map(new Dictionary())); - - b = a.Get(t, "B").Map; - b.Insert(t, "C", Input.Double(1)); - } - - using (var t = doc.WriteTransaction()) - { - a.Remove(t, "B"); - } - - for (var j = 0; j < 10000; j++) - { - using (var t = doc.WriteTransaction()) - { - b.Insert(t, "C", Input.Double(1)); - } - } - - using (var t = doc.WriteTransaction()) - { - b.Insert(t, "D", Input.Double(1)); - - var length = b.Length(t); - }*/ - - var types = typeof(Program).Assembly.GetTypes(); + var types = typeof(Program).Assembly.GetTypes(); AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; @@ -89,7 +53,7 @@ public static void Main() GC.Collect(); try { - for (var j = 0; j < 10000; j++) + for (var j = 0; j < 1000; j++) { method.Invoke(instance, null); } diff --git a/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs b/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs index 26985ed7..7bdd091c 100644 --- a/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs +++ b/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs @@ -20,6 +20,7 @@ internal ArrayEnumerator(ArrayIterator iterator) /// public void Dispose() { + Iterator.Dispose(); } /// diff --git a/YDotNet/Document/Types/Maps/MapEnumerator.cs b/YDotNet/Document/Types/Maps/MapEnumerator.cs index 3501479c..b2af195f 100644 --- a/YDotNet/Document/Types/Maps/MapEnumerator.cs +++ b/YDotNet/Document/Types/Maps/MapEnumerator.cs @@ -20,6 +20,7 @@ internal MapEnumerator(MapIterator iterator) /// public void Dispose() { + Iterator.Dispose(); } /// diff --git a/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs b/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs index faf3929b..667fc3f2 100644 --- a/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs +++ b/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs @@ -27,6 +27,7 @@ public XmlTreeWalkerEnumerator(XmlTreeWalker treeWalker) /// public void Dispose() { + TreeWalker.Dispose(); } /// diff --git a/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs b/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs index eb6e7e2e..7dc3506f 100644 --- a/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs +++ b/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs @@ -26,6 +26,7 @@ internal XmlAttributeEnumerator(XmlAttributeIterator iterator) /// public void Dispose() { + Iterator.Dispose(); } /// From 98ad237ef0cb450830c1b12b71a1ec3ba8c9aa2b Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 14 Oct 2023 20:42:57 +0200 Subject: [PATCH 096/186] More tests. --- .../Branches/ReadTransactionTests.cs | 70 +++++++++++++++++ .../Branches/WriteTransactionTests.cs | 70 +++++++++++++++++ .../Document/ReadTransactionTests.cs | 70 +++++++++++++++++ .../Document/SubDocsTests.cs | 1 + .../Document/WriteTransactionTests.cs | 70 +++++++++++++++++ Tests/YDotNet.Tests.Unit/Maps/ObserveTests.cs | 35 +++++++++ YDotNet/Document/Doc.cs | 76 ++++++++++++++----- .../XmlElements/XmlAttributeEnumerator.cs | 7 -- 8 files changed, 372 insertions(+), 27 deletions(-) diff --git a/Tests/YDotNet.Tests.Unit/Branches/ReadTransactionTests.cs b/Tests/YDotNet.Tests.Unit/Branches/ReadTransactionTests.cs index ec6ddd06..aa46409f 100644 --- a/Tests/YDotNet.Tests.Unit/Branches/ReadTransactionTests.cs +++ b/Tests/YDotNet.Tests.Unit/Branches/ReadTransactionTests.cs @@ -81,4 +81,74 @@ public void StartReadTransactionWhileDocumentWriteTransactionIsOpen() Assert.Throws(() => branch.ReadTransaction()); Assert.That(documentTransaction, Is.Not.Null); } + + [Test] + public void GetRootMapWithOpenBranchTransactionNotAllowed() + { + // Arrange + var doc = new Doc(); + var map = doc.Map("Map"); + + // Keep the transaction open. + map.Length(map.ReadTransaction()); + + // Assert + Assert.Throws(() => doc.Map("Item")); + } + + [Test] + public void GetRootArrayWithOpenTransactionNotAllowed() + { + // Arrange + var doc = new Doc(); + var map = doc.Map("Map"); + + // Keep the transaction open. + map.Length(map.ReadTransaction()); + + // Assert + Assert.Throws(() => doc.Array("Item")); + } + + [Test] + public void GetRootTextWithOpenTransactionNotAllowed() + { + // Arrange + var doc = new Doc(); + var map = doc.Map("Map"); + + // Keep the transaction open. + map.Length(map.ReadTransaction()); + + // Assert + Assert.Throws(() => doc.Text("Item")); + } + + [Test] + public void GetRootXmlTextWithOpenTransactionNotAllowed() + { + // Arrange + var doc = new Doc(); + var map = doc.Map("Map"); + + // Keep the transaction open. + map.Length(map.ReadTransaction()); + + // Assert + Assert.Throws(() => doc.XmlText("XmlText")); + } + + [Test] + public void GetRootXmlElementWithOpenTransactionNotAllowed() + { + // Arrange + var doc = new Doc(); + var map = doc.Map("Map"); + + // Keep the transaction open. + map.Length(map.ReadTransaction()); + + // Assert + Assert.Throws(() => doc.XmlElement("XmlElement")); + } } diff --git a/Tests/YDotNet.Tests.Unit/Branches/WriteTransactionTests.cs b/Tests/YDotNet.Tests.Unit/Branches/WriteTransactionTests.cs index 5d7585b7..2830f798 100644 --- a/Tests/YDotNet.Tests.Unit/Branches/WriteTransactionTests.cs +++ b/Tests/YDotNet.Tests.Unit/Branches/WriteTransactionTests.cs @@ -79,4 +79,74 @@ public void StartWriteTransactionWhileDocumentWriteTransactionIsOpen() Assert.Throws(() => branch.WriteTransaction()); Assert.That(transaction1, Is.Not.Null); } + + [Test] + public void GetRootMapWithOpenBranchTransactionNotAllowed() + { + // Arrange + var doc = new Doc(); + var map = doc.Map("Map"); + + // Keep the transaction open. + map.Length(map.WriteTransaction()); + + // Assert + Assert.Throws(() => doc.Map("Item")); + } + + [Test] + public void GetRootArrayWithOpenTransactionNotAllowed() + { + // Arrange + var doc = new Doc(); + var map = doc.Map("Map"); + + // Keep the transaction open. + map.Length(map.WriteTransaction()); + + // Assert + Assert.Throws(() => doc.Array("Item")); + } + + [Test] + public void GetRootTextWithOpenTransactionNotAllowed() + { + // Arrange + var doc = new Doc(); + var map = doc.Map("Map"); + + // Keep the transaction open. + map.Length(map.WriteTransaction()); + + // Assert + Assert.Throws(() => doc.Text("Item")); + } + + [Test] + public void GetRootXmlTextWithOpenTransactionNotAllowed() + { + // Arrange + var doc = new Doc(); + var map = doc.Map("Map"); + + // Keep the transaction open. + map.Length(map.WriteTransaction()); + + // Assert + Assert.Throws(() => doc.XmlText("XmlText")); + } + + [Test] + public void GetRootXmlElementWithOpenTransactionNotAllowed() + { + // Arrange + var doc = new Doc(); + var map = doc.Map("Map"); + + // Keep the transaction open. + map.Length(map.WriteTransaction()); + + // Assert + Assert.Throws(() => doc.XmlElement("XmlElement")); + } } diff --git a/Tests/YDotNet.Tests.Unit/Document/ReadTransactionTests.cs b/Tests/YDotNet.Tests.Unit/Document/ReadTransactionTests.cs index d6a2cb5f..38cebf06 100644 --- a/Tests/YDotNet.Tests.Unit/Document/ReadTransactionTests.cs +++ b/Tests/YDotNet.Tests.Unit/Document/ReadTransactionTests.cs @@ -31,4 +31,74 @@ public void MultipleReadTransactionsAllowed() Assert.That(doc.ReadTransaction(), Is.Not.Null); Assert.That(doc.ReadTransaction(), Is.Not.Null); } + + [Test] + public void GetRootMapWithOpenTransactionNotAllowed() + { + // Arrange + var doc = new Doc(); + var map = doc.Map("Map"); + + // Keep the transaction open. + map.Length(doc.ReadTransaction()); + + // Assert + Assert.Throws(() => doc.Map("Item")); + } + + [Test] + public void GetRootArrayWithOpenTransactionNotAllowed() + { + // Arrange + var doc = new Doc(); + var map = doc.Map("Map"); + + // Keep the transaction open. + map.Length(doc.ReadTransaction()); + + // Assert + Assert.Throws(() => doc.Array("Item")); + } + + [Test] + public void GetRootTextWithOpenTransactionNotAllowed() + { + // Arrange + var doc = new Doc(); + var map = doc.Map("Map"); + + // Keep the transaction open. + map.Length(doc.ReadTransaction()); + + // Assert + Assert.Throws(() => doc.Text("Item")); + } + + [Test] + public void GetRootXmlTextWithOpenTransactionNotAllowed() + { + // Arrange + var doc = new Doc(); + var map = doc.Map("Map"); + + // Keep the transaction open. + map.Length(doc.ReadTransaction()); + + // Assert + Assert.Throws(() => doc.XmlText("XmlText")); + } + + [Test] + public void GetRootXmlElementWithOpenTransactionNotAllowed() + { + // Arrange + var doc = new Doc(); + var map = doc.Map("Map"); + + // Keep the transaction open. + map.Length(doc.ReadTransaction()); + + // Assert + Assert.Throws(() => doc.XmlElement("XmlElement")); + } } diff --git a/Tests/YDotNet.Tests.Unit/Document/SubDocsTests.cs b/Tests/YDotNet.Tests.Unit/Document/SubDocsTests.cs index 5a019137..311e2133 100644 --- a/Tests/YDotNet.Tests.Unit/Document/SubDocsTests.cs +++ b/Tests/YDotNet.Tests.Unit/Document/SubDocsTests.cs @@ -78,6 +78,7 @@ public void TriggersWhenRemovingSubDocsUntilUnobserved() Assert.That(subDocsEvent.Added, Is.Empty); Assert.That(subDocsEvent.Loaded, Is.Empty); Assert.That(subDocsEvent.Removed, Has.Count.EqualTo(expected: 1)); + Assert.That(subDocsEvent.Removed[0].IsDisposed, Is.True); // Act subDocsEvent = null; diff --git a/Tests/YDotNet.Tests.Unit/Document/WriteTransactionTests.cs b/Tests/YDotNet.Tests.Unit/Document/WriteTransactionTests.cs index b4b0ed49..1f48f6ce 100644 --- a/Tests/YDotNet.Tests.Unit/Document/WriteTransactionTests.cs +++ b/Tests/YDotNet.Tests.Unit/Document/WriteTransactionTests.cs @@ -28,4 +28,74 @@ public void MultipleWriteTransactionsNotAllowed() Assert.That(doc.WriteTransaction(), Is.Not.Null); Assert.Throws(() => doc.WriteTransaction()); } + + [Test] + public void GetRootMapWithOpenTransactionNotAllowed() + { + // Arrange + var doc = new Doc(); + var map = doc.Map("Map"); + + // Keep the transaction open. + map.Length(doc.WriteTransaction()); + + // Assert + Assert.Throws(() => doc.Map("Item")); + } + + [Test] + public void GetRootArrayWithOpenTransactionNotAllowed() + { + // Arrange + var doc = new Doc(); + var map = doc.Map("Map"); + + // Keep the transaction open. + map.Length(doc.WriteTransaction()); + + // Assert + Assert.Throws(() => doc.Array("Item")); + } + + [Test] + public void GetRootTextWithOpenTransactionNotAllowed() + { + // Arrange + var doc = new Doc(); + var map = doc.Map("Map"); + + // Keep the transaction open. + map.Length(doc.WriteTransaction()); + + // Assert + Assert.Throws(() => doc.Text("Item")); + } + + [Test] + public void GetRootXmlTextWithOpenTransactionNotAllowed() + { + // Arrange + var doc = new Doc(); + var map = doc.Map("Map"); + + // Keep the transaction open. + map.Length(doc.WriteTransaction()); + + // Assert + Assert.Throws(() => doc.XmlText("XmlText")); + } + + [Test] + public void GetRootXmlElementWithOpenTransactionNotAllowed() + { + // Arrange + var doc = new Doc(); + var map = doc.Map("Map"); + + // Keep the transaction open. + map.Length(doc.WriteTransaction()); + + // Assert + Assert.Throws(() => doc.XmlElement("XmlElement")); + } } diff --git a/Tests/YDotNet.Tests.Unit/Maps/ObserveTests.cs b/Tests/YDotNet.Tests.Unit/Maps/ObserveTests.cs index 93093498..3ef3049c 100644 --- a/Tests/YDotNet.Tests.Unit/Maps/ObserveTests.cs +++ b/Tests/YDotNet.Tests.Unit/Maps/ObserveTests.cs @@ -233,4 +233,39 @@ public void ObserveHasKeysWhenAddedAndRemovedAndUpdated() Assert.That(add.NewValue, Is.Not.Null); Assert.That(add.NewValue.Long, Is.EqualTo(expected: -1337L)); } + + [Test] + public void ObserveOldValueIsDisposed() + { + var doc = new Doc(); + var map = doc.Map("map"); + + var transaction = doc.WriteTransaction(); + map.Insert(transaction, "value", Input.Map(new Dictionary())); + transaction.Commit(); + + IEnumerable? keyChanges = null; + var called = 0; + + var subscription = map.Observe( + e => + { + called++; + keyChanges = e.Keys.ToArray(); + }); + + // Act + transaction = doc.WriteTransaction(); + + // Update, remove, and add, respectively + map.Insert(transaction, "value", Input.Long(value: -420L)); + + transaction.Commit(); + + // Assert + Assert.That(called, Is.EqualTo(expected: 1)); + Assert.That(keyChanges, Is.Not.Null); + Assert.That(keyChanges.Count(), Is.EqualTo(expected: 1)); + Assert.That(keyChanges.ElementAt(0).OldValue.Map.IsDisposed, Is.True); + } } diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index 4cdfe0be..8468b7e6 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -141,6 +141,7 @@ private static nint CreateDoc(DocOptions options) DisposeCore(); } + /// public void Dispose() { GC.SuppressFinalize(this); @@ -246,11 +247,18 @@ public string? CollectionId /// The instance related to the name provided. public Text Text(string name) { - using var unsafeName = MemoryWriter.WriteUtf8String(name); - - var handle = DocChannel.Text(Handle, unsafeName.Handle); + try + { + using var unsafeName = MemoryWriter.WriteUtf8String(name); + var handle = DocChannel.Text(Handle, unsafeName.Handle); - return GetText(handle, false); + return GetText(handle, false); + } + catch + { + ThrowHelper.PendingTransaction(); + return default!; + } } /// @@ -264,11 +272,18 @@ public Text Text(string name) /// The instance related to the name provided. public Map Map(string name) { - using var unsafeName = MemoryWriter.WriteUtf8String(name); - - var handle = DocChannel.Map(Handle, unsafeName.Handle); + try + { + using var unsafeName = MemoryWriter.WriteUtf8String(name); + var handle = DocChannel.Map(Handle, unsafeName.Handle); - return GetMap(handle, false); + return GetMap(handle, false); + } + catch + { + ThrowHelper.PendingTransaction(); + return default!; + } } /// @@ -282,11 +297,18 @@ public Map Map(string name) /// The instance related to the name provided. public Array Array(string name) { - using var unsafeName = MemoryWriter.WriteUtf8String(name); - - var handle = DocChannel.Array(Handle, unsafeName.Handle); + try + { + using var unsafeName = MemoryWriter.WriteUtf8String(name); + var handle = DocChannel.Array(Handle, unsafeName.Handle); - return GetArray(handle, false); + return GetArray(handle, false); + } + catch + { + ThrowHelper.PendingTransaction(); + return default!; + } } /// @@ -300,11 +322,18 @@ public Array Array(string name) /// The instance related to the name provided. public XmlElement XmlElement(string name) { - using var unsafeName = MemoryWriter.WriteUtf8String(name); - - var handle = DocChannel.XmlElement(Handle, unsafeName.Handle); + try + { + using var unsafeName = MemoryWriter.WriteUtf8String(name); + var handle = DocChannel.XmlElement(Handle, unsafeName.Handle); - return GetXmlElement(handle, false); + return GetXmlElement(handle, false); + } + catch + { + ThrowHelper.PendingTransaction(); + return default!; + } } /// @@ -318,11 +347,18 @@ public XmlElement XmlElement(string name) /// The instance related to the name provided. public XmlText XmlText(string name) { - using var unsafeName = MemoryWriter.WriteUtf8String(name); - - var handle = DocChannel.XmlText(Handle, unsafeName.Handle); + try + { + using var unsafeName = MemoryWriter.WriteUtf8String(name); + var handle = DocChannel.XmlText(Handle, unsafeName.Handle); - return GetXmlText(handle, false); + return GetXmlText(handle, false); + } + catch + { + ThrowHelper.PendingTransaction(); + return default!; + } } /// diff --git a/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs b/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs index 7dc3506f..a185107b 100644 --- a/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs +++ b/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs @@ -11,13 +11,6 @@ internal class XmlAttributeEnumerator : IEnumerator { private KeyValuePair current; - /// - /// Initializes a new instance of the class. - /// - /// - /// The instance used by this enumerator. - /// Check for more details. - /// internal XmlAttributeEnumerator(XmlAttributeIterator iterator) { Iterator = iterator; From d47414747afc274f18f02a86fad4317166bc322f Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 14 Oct 2023 20:58:34 +0200 Subject: [PATCH 097/186] Ignore buggy tests. --- .../Branches/ReadTransactionTests.cs | 5 ++ .../Branches/WriteTransactionTests.cs | 5 ++ .../Document/ReadTransactionTests.cs | 5 ++ .../Document/WriteTransactionTests.cs | 5 ++ YDotNet/Document/Doc.cs | 70 ++++--------------- 5 files changed, 35 insertions(+), 55 deletions(-) diff --git a/Tests/YDotNet.Tests.Unit/Branches/ReadTransactionTests.cs b/Tests/YDotNet.Tests.Unit/Branches/ReadTransactionTests.cs index aa46409f..d24f8fdf 100644 --- a/Tests/YDotNet.Tests.Unit/Branches/ReadTransactionTests.cs +++ b/Tests/YDotNet.Tests.Unit/Branches/ReadTransactionTests.cs @@ -83,6 +83,7 @@ public void StartReadTransactionWhileDocumentWriteTransactionIsOpen() } [Test] + [Ignore("Still buggy in y-crdt")] public void GetRootMapWithOpenBranchTransactionNotAllowed() { // Arrange @@ -97,6 +98,7 @@ public void GetRootMapWithOpenBranchTransactionNotAllowed() } [Test] + [Ignore("Still buggy in y-crdt")] public void GetRootArrayWithOpenTransactionNotAllowed() { // Arrange @@ -111,6 +113,7 @@ public void GetRootArrayWithOpenTransactionNotAllowed() } [Test] + [Ignore("Still buggy in y-crdt")] public void GetRootTextWithOpenTransactionNotAllowed() { // Arrange @@ -125,6 +128,7 @@ public void GetRootTextWithOpenTransactionNotAllowed() } [Test] + [Ignore("Still buggy in y-crdt")] public void GetRootXmlTextWithOpenTransactionNotAllowed() { // Arrange @@ -139,6 +143,7 @@ public void GetRootXmlTextWithOpenTransactionNotAllowed() } [Test] + [Ignore("Still buggy in y-crdt")] public void GetRootXmlElementWithOpenTransactionNotAllowed() { // Arrange diff --git a/Tests/YDotNet.Tests.Unit/Branches/WriteTransactionTests.cs b/Tests/YDotNet.Tests.Unit/Branches/WriteTransactionTests.cs index 2830f798..7faa3eca 100644 --- a/Tests/YDotNet.Tests.Unit/Branches/WriteTransactionTests.cs +++ b/Tests/YDotNet.Tests.Unit/Branches/WriteTransactionTests.cs @@ -81,6 +81,7 @@ public void StartWriteTransactionWhileDocumentWriteTransactionIsOpen() } [Test] + [Ignore("Still buggy in y-crdt")] public void GetRootMapWithOpenBranchTransactionNotAllowed() { // Arrange @@ -95,6 +96,7 @@ public void GetRootMapWithOpenBranchTransactionNotAllowed() } [Test] + [Ignore("Still buggy in y-crdt")] public void GetRootArrayWithOpenTransactionNotAllowed() { // Arrange @@ -109,6 +111,7 @@ public void GetRootArrayWithOpenTransactionNotAllowed() } [Test] + [Ignore("Still buggy in y-crdt")] public void GetRootTextWithOpenTransactionNotAllowed() { // Arrange @@ -123,6 +126,7 @@ public void GetRootTextWithOpenTransactionNotAllowed() } [Test] + [Ignore("Still buggy in y-crdt")] public void GetRootXmlTextWithOpenTransactionNotAllowed() { // Arrange @@ -137,6 +141,7 @@ public void GetRootXmlTextWithOpenTransactionNotAllowed() } [Test] + [Ignore("Still buggy in y-crdt")] public void GetRootXmlElementWithOpenTransactionNotAllowed() { // Arrange diff --git a/Tests/YDotNet.Tests.Unit/Document/ReadTransactionTests.cs b/Tests/YDotNet.Tests.Unit/Document/ReadTransactionTests.cs index 38cebf06..e9f76d81 100644 --- a/Tests/YDotNet.Tests.Unit/Document/ReadTransactionTests.cs +++ b/Tests/YDotNet.Tests.Unit/Document/ReadTransactionTests.cs @@ -33,6 +33,7 @@ public void MultipleReadTransactionsAllowed() } [Test] + [Ignore("Still buggy in y-crdt")] public void GetRootMapWithOpenTransactionNotAllowed() { // Arrange @@ -47,6 +48,7 @@ public void GetRootMapWithOpenTransactionNotAllowed() } [Test] + [Ignore("Still buggy in y-crdt")] public void GetRootArrayWithOpenTransactionNotAllowed() { // Arrange @@ -61,6 +63,7 @@ public void GetRootArrayWithOpenTransactionNotAllowed() } [Test] + [Ignore("Still buggy in y-crdt")] public void GetRootTextWithOpenTransactionNotAllowed() { // Arrange @@ -75,6 +78,7 @@ public void GetRootTextWithOpenTransactionNotAllowed() } [Test] + [Ignore("Still buggy in y-crdt")] public void GetRootXmlTextWithOpenTransactionNotAllowed() { // Arrange @@ -89,6 +93,7 @@ public void GetRootXmlTextWithOpenTransactionNotAllowed() } [Test] + [Ignore("Still buggy in y-crdt")] public void GetRootXmlElementWithOpenTransactionNotAllowed() { // Arrange diff --git a/Tests/YDotNet.Tests.Unit/Document/WriteTransactionTests.cs b/Tests/YDotNet.Tests.Unit/Document/WriteTransactionTests.cs index 1f48f6ce..5c87cda8 100644 --- a/Tests/YDotNet.Tests.Unit/Document/WriteTransactionTests.cs +++ b/Tests/YDotNet.Tests.Unit/Document/WriteTransactionTests.cs @@ -30,6 +30,7 @@ public void MultipleWriteTransactionsNotAllowed() } [Test] + [Ignore("Still buggy in y-crdt")] public void GetRootMapWithOpenTransactionNotAllowed() { // Arrange @@ -44,6 +45,7 @@ public void GetRootMapWithOpenTransactionNotAllowed() } [Test] + [Ignore("Still buggy in y-crdt")] public void GetRootArrayWithOpenTransactionNotAllowed() { // Arrange @@ -58,6 +60,7 @@ public void GetRootArrayWithOpenTransactionNotAllowed() } [Test] + [Ignore("Still buggy in y-crdt")] public void GetRootTextWithOpenTransactionNotAllowed() { // Arrange @@ -72,6 +75,7 @@ public void GetRootTextWithOpenTransactionNotAllowed() } [Test] + [Ignore("Still buggy in y-crdt")] public void GetRootXmlTextWithOpenTransactionNotAllowed() { // Arrange @@ -86,6 +90,7 @@ public void GetRootXmlTextWithOpenTransactionNotAllowed() } [Test] + [Ignore("Still buggy in y-crdt")] public void GetRootXmlElementWithOpenTransactionNotAllowed() { // Arrange diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index 8468b7e6..83d60f99 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -247,18 +247,10 @@ public string? CollectionId /// The instance related to the name provided. public Text Text(string name) { - try - { - using var unsafeName = MemoryWriter.WriteUtf8String(name); - var handle = DocChannel.Text(Handle, unsafeName.Handle); + using var unsafeName = MemoryWriter.WriteUtf8String(name); + var handle = DocChannel.Text(Handle, unsafeName.Handle); - return GetText(handle, false); - } - catch - { - ThrowHelper.PendingTransaction(); - return default!; - } + return GetText(handle, false); } /// @@ -272,18 +264,10 @@ public Text Text(string name) /// The instance related to the name provided. public Map Map(string name) { - try - { - using var unsafeName = MemoryWriter.WriteUtf8String(name); - var handle = DocChannel.Map(Handle, unsafeName.Handle); + using var unsafeName = MemoryWriter.WriteUtf8String(name); + var handle = DocChannel.Map(Handle, unsafeName.Handle); - return GetMap(handle, false); - } - catch - { - ThrowHelper.PendingTransaction(); - return default!; - } + return GetMap(handle, false); } /// @@ -297,18 +281,10 @@ public Map Map(string name) /// The instance related to the name provided. public Array Array(string name) { - try - { - using var unsafeName = MemoryWriter.WriteUtf8String(name); - var handle = DocChannel.Array(Handle, unsafeName.Handle); + using var unsafeName = MemoryWriter.WriteUtf8String(name); + var handle = DocChannel.Array(Handle, unsafeName.Handle); - return GetArray(handle, false); - } - catch - { - ThrowHelper.PendingTransaction(); - return default!; - } + return GetArray(handle, false); } /// @@ -322,18 +298,10 @@ public Array Array(string name) /// The instance related to the name provided. public XmlElement XmlElement(string name) { - try - { - using var unsafeName = MemoryWriter.WriteUtf8String(name); - var handle = DocChannel.XmlElement(Handle, unsafeName.Handle); + using var unsafeName = MemoryWriter.WriteUtf8String(name); + var handle = DocChannel.XmlElement(Handle, unsafeName.Handle); - return GetXmlElement(handle, false); - } - catch - { - ThrowHelper.PendingTransaction(); - return default!; - } + return GetXmlElement(handle, false); } /// @@ -347,18 +315,10 @@ public XmlElement XmlElement(string name) /// The instance related to the name provided. public XmlText XmlText(string name) { - try - { - using var unsafeName = MemoryWriter.WriteUtf8String(name); - var handle = DocChannel.XmlText(Handle, unsafeName.Handle); + using var unsafeName = MemoryWriter.WriteUtf8String(name); + var handle = DocChannel.XmlText(Handle, unsafeName.Handle); - return GetXmlText(handle, false); - } - catch - { - ThrowHelper.PendingTransaction(); - return default!; - } + return GetXmlText(handle, false); } /// From ad4ac80cf171b7340805c6994aa307b343386886 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 14 Oct 2023 21:08:17 +0200 Subject: [PATCH 098/186] Fix sample. --- Demo/Callback.cs | 14 ++++++++------ YDotNet.Extensions/YDotNet.Extensions.csproj | 1 + YDotNet.Extensions/YDotNetExtensions.cs | 4 ++-- YDotNet.Server/YDotNet.Server.csproj | 2 +- native/YDotNet.Native/YDotNet.Native.csproj | 2 +- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/Demo/Callback.cs b/Demo/Callback.cs index d580b384..7e04384e 100644 --- a/Demo/Callback.cs +++ b/Demo/Callback.cs @@ -62,17 +62,19 @@ public ValueTask OnDocumentLoadedAsync(DocumentLoadEvent @event) return; } - List notifications; - - using (var transaction = @event.Document.ReadTransaction()) - { - notifications = newNotificationsRaw.Select(x => x.To(transaction)).ToList(); - } + await Task.Delay(100); var notificationCtx = new DocumentContext("notifications", 0); await @event.Source.UpdateDocAsync(notificationCtx, (doc) => { + List notifications; + + using (var transaction = @event.Document.ReadTransaction()) + { + notifications = newNotificationsRaw.Select(x => x.To(transaction)).ToList(); + } + var array = doc.Array("stream"); notifications = notifications.Select(x => new Notification { Text = $"You got the follow message: {x.Text}" }).ToList(); diff --git a/YDotNet.Extensions/YDotNet.Extensions.csproj b/YDotNet.Extensions/YDotNet.Extensions.csproj index 153967cc..bd313047 100644 --- a/YDotNet.Extensions/YDotNet.Extensions.csproj +++ b/YDotNet.Extensions/YDotNet.Extensions.csproj @@ -4,6 +4,7 @@ net7.0 enable enable + Squidex.YDotNet.Extensions diff --git a/YDotNet.Extensions/YDotNetExtensions.cs b/YDotNet.Extensions/YDotNetExtensions.cs index 33f63806..7956516a 100644 --- a/YDotNet.Extensions/YDotNetExtensions.cs +++ b/YDotNet.Extensions/YDotNetExtensions.cs @@ -49,7 +49,7 @@ static Input ConvertValue(JsonElement element) } } - public static T? To(this Output output, Doc doc) + public static T To(this Output output, Doc doc) { using var transaction = doc.ReadTransaction() ?? throw new InvalidOperationException("Failed to open transaction."); @@ -57,7 +57,7 @@ static Input ConvertValue(JsonElement element) return output.To(transaction); } - public static T? To(this Output output, Transaction transaction) + public static T To(this Output output, Transaction transaction) { var jsonStream = new MemoryStream(); var jsonWriter = new Utf8JsonWriter(jsonStream); diff --git a/YDotNet.Server/YDotNet.Server.csproj b/YDotNet.Server/YDotNet.Server.csproj index 4137ba84..a50cb27f 100644 --- a/YDotNet.Server/YDotNet.Server.csproj +++ b/YDotNet.Server/YDotNet.Server.csproj @@ -4,7 +4,7 @@ net7.0 enable enable - Squidex.YDotNet.Serverr + Squidex.YDotNet.Server diff --git a/native/YDotNet.Native/YDotNet.Native.csproj b/native/YDotNet.Native/YDotNet.Native.csproj index 430b50ef..2b74ee3c 100644 --- a/native/YDotNet.Native/YDotNet.Native.csproj +++ b/native/YDotNet.Native/YDotNet.Native.csproj @@ -4,7 +4,7 @@ net7.0 enable enable - YDotNetTest.Native + Squidex.YDotNet.Native From 0a81e153d2a7500bf1d5c42a480f30803c49c3cc Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 14 Oct 2023 21:15:44 +0200 Subject: [PATCH 099/186] Increase version. --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index b944aacc..0b91bf06 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,7 +9,7 @@ https://github.com/LSViana/YDotNet true snupkg - 0.1.0 + 0.2.0 From 9d0db002f16f21bbf37529be4cea7513fcc0508f Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 14 Oct 2023 21:17:46 +0200 Subject: [PATCH 100/186] Another version update. --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 0b91bf06..77c667c3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,7 +9,7 @@ https://github.com/LSViana/YDotNet true snupkg - 0.2.0 + 0.2.1 From 2d3e0a113b2ea3371d274aa338c21b0d388be382 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 14 Oct 2023 21:28:32 +0200 Subject: [PATCH 101/186] Fix build --- .editorconfig | 1 + Demo/Client/.vite/deps_temp_bce6eb42/package.json | 3 +++ Directory.Build.props | 2 +- YDotNet.Server/Internal/Extensions.cs | 10 ---------- YDotNet/Document/Cells/{OutputTage.cs => OutputTag.cs} | 2 +- 5 files changed, 6 insertions(+), 12 deletions(-) create mode 100644 Demo/Client/.vite/deps_temp_bce6eb42/package.json rename YDotNet/Document/Cells/{OutputTage.cs => OutputTag.cs} (98%) diff --git a/.editorconfig b/.editorconfig index 69a7906b..b746cd78 100644 --- a/.editorconfig +++ b/.editorconfig @@ -28,6 +28,7 @@ dotnet_diagnostic.SA1009.severity = suggestion # Closing parenthesis should be s # Readability rules dotnet_diagnostic.SA1101.severity = none # Prefix local calls with this +dotnet_diagnostic.SA1116.severity = none dotnet_diagnostic.SA1127.severity = none # Prefix local calls with this # Ordering rules diff --git a/Demo/Client/.vite/deps_temp_bce6eb42/package.json b/Demo/Client/.vite/deps_temp_bce6eb42/package.json new file mode 100644 index 00000000..3dbc1ca5 --- /dev/null +++ b/Demo/Client/.vite/deps_temp_bce6eb42/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/Directory.Build.props b/Directory.Build.props index 77c667c3..54705613 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,7 +9,7 @@ https://github.com/LSViana/YDotNet true snupkg - 0.2.1 + 0.2.2 diff --git a/YDotNet.Server/Internal/Extensions.cs b/YDotNet.Server/Internal/Extensions.cs index 4e580467..14fb3b69 100644 --- a/YDotNet.Server/Internal/Extensions.cs +++ b/YDotNet.Server/Internal/Extensions.cs @@ -16,16 +16,6 @@ public static Transaction WriteTransactionOrThrow(this Doc doc) return doc.WriteTransaction() ?? throw new InvalidOperationException("Failed to open transaction."); } - public static async ValueTask GetUpdateArray(this byte[] source) - { - return source; - var decoder = new MemoryDecoder(source); - await decoder.ReadVarUintAsync(); - await decoder.ReadVarUintAsync(); - - return await decoder.ReadVarUint8ArrayAsync(default); - } - class MemoryDecoder : Decoder { private readonly byte[] source; diff --git a/YDotNet/Document/Cells/OutputTage.cs b/YDotNet/Document/Cells/OutputTag.cs similarity index 98% rename from YDotNet/Document/Cells/OutputTage.cs rename to YDotNet/Document/Cells/OutputTag.cs index 78ce4bb7..fda00866 100644 --- a/YDotNet/Document/Cells/OutputTage.cs +++ b/YDotNet/Document/Cells/OutputTag.cs @@ -1,4 +1,4 @@ -namespace YDotNet.Document.Cells; +namespace YDotNet.Document.Cells; /// /// The type of an output. From 991dd6e9c9b5f71dc725416c08852b197d7d255e Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 15 Oct 2023 18:24:50 +0200 Subject: [PATCH 102/186] Fixes. --- Demo/Callback.cs | 1 - Demo/Client/src/App.tsx | 2 +- Demo/Controllers/CollaborationController.cs | 13 +++++++++++++ Demo/Program.cs | 3 +++ Directory.Build.props | 2 +- YDotNet.Server.WebSockets/YDotNetActionResult.cs | 9 ++++++++- .../YDotNetSocketMiddleware.cs | 10 ++++++++-- YDotNet/Document/Events/EventSubscriber.cs | 12 ++++++++++++ 8 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 Demo/Controllers/CollaborationController.cs diff --git a/Demo/Callback.cs b/Demo/Callback.cs index 7e04384e..b4f2fd9c 100644 --- a/Demo/Callback.cs +++ b/Demo/Callback.cs @@ -1,5 +1,4 @@ using System.Text.Json.Serialization; -using YDotNet.Document.Cells; using YDotNet.Document.Types.Events; using YDotNet.Extensions; using YDotNet.Server; diff --git a/Demo/Client/src/App.tsx b/Demo/Client/src/App.tsx index 483777a2..9cf74087 100644 --- a/Demo/Client/src/App.tsx +++ b/Demo/Client/src/App.tsx @@ -45,7 +45,7 @@ function App() {
Notifications
- + diff --git a/Demo/Controllers/CollaborationController.cs b/Demo/Controllers/CollaborationController.cs new file mode 100644 index 00000000..f8e6f13e --- /dev/null +++ b/Demo/Controllers/CollaborationController.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; +using YDotNet.Server.WebSockets; + +namespace Demo.Controllers; + +public class CollaborationController : Controller +{ + [HttpGet("/collaboration2/{roomName}")] + public IActionResult Room(string roomName) + { + return new YDotNetActionResult(roomName); + } +} diff --git a/Demo/Program.cs b/Demo/Program.cs index b215269b..c2802a18 100644 --- a/Demo/Program.cs +++ b/Demo/Program.cs @@ -10,6 +10,7 @@ public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); + builder.Services.AddControllers(); builder.Services.AddRazorPages(); var yDotNet = @@ -62,6 +63,8 @@ public static void Main(string[] args) app.UseStaticFiles(); app.UseWebSockets(); app.UseRouting(); + + app.MapControllers(); app.Map("/collaboration", builder => { builder.UseYDotnetWebSockets(); diff --git a/Directory.Build.props b/Directory.Build.props index 54705613..b25c578d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,7 +9,7 @@ https://github.com/LSViana/YDotNet true snupkg - 0.2.2 + 0.2.3 diff --git a/YDotNet.Server.WebSockets/YDotNetActionResult.cs b/YDotNet.Server.WebSockets/YDotNetActionResult.cs index b91bdf69..07767194 100644 --- a/YDotNet.Server.WebSockets/YDotNetActionResult.cs +++ b/YDotNet.Server.WebSockets/YDotNetActionResult.cs @@ -5,10 +5,17 @@ namespace YDotNet.Server.WebSockets; public sealed class YDotNetActionResult : IActionResult { + private readonly string documentName; + + public YDotNetActionResult(string documentName) + { + this.documentName = documentName; + } + public async Task ExecuteResultAsync(ActionContext context) { var middleware = context.HttpContext.RequestServices.GetRequiredService(); - await middleware.InvokeAsync(context.HttpContext); + await middleware.InvokeAsync(context.HttpContext, this.documentName); } } diff --git a/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs b/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs index be3af2a8..13f83286 100644 --- a/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs +++ b/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs @@ -70,7 +70,14 @@ await state.WriteLockedAsync(@event.Diff, async (encoder, diff, _, ct) => return default; } - public async Task InvokeAsync(HttpContext httpContext) + public Task InvokeAsync(HttpContext httpContext) + { + var documentName = httpContext.Request.Path.ToString().Substring(1); + + return InvokeAsync(httpContext, documentName); + } + + public async Task InvokeAsync(HttpContext httpContext, string documentName) { if (!httpContext.WebSockets.IsWebSocketRequest || documentManager == null) { @@ -78,7 +85,6 @@ public async Task InvokeAsync(HttpContext httpContext) return; } - var documentName = httpContext.Request.Path.ToString().Substring(1); var documentStates = statesPerDocumentName.GetOrAdd(documentName, _ => new List()); logger.LogDebug("Websocket connection to {document} established.", documentName); diff --git a/YDotNet/Document/Events/EventSubscriber.cs b/YDotNet/Document/Events/EventSubscriber.cs index ee30bf91..2f56379d 100644 --- a/YDotNet/Document/Events/EventSubscriber.cs +++ b/YDotNet/Document/Events/EventSubscriber.cs @@ -18,9 +18,16 @@ public EventSubscriber( this.unsubscribe = unsubscribe; } + ~EventSubscriber() + { + Clear(); + } + public void Clear() { publisher.Clear(); + + UnsubscribeWhenSubscribed(); } public IDisposable Subscribe(Action handler) @@ -42,6 +49,11 @@ private void Unsubscribe(Action handler) { publisher.Unsubscribe(handler); + UnsubscribeWhenSubscribed(); + } + + private void UnsubscribeWhenSubscribed() + { if (publisher.Count == 0 && nativeSubscription.Callback != null) { unsubscribe(owner, nativeSubscription.Handle); From e5c2f93c9c1f077d71f8aafae072e9150c52835f Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 15 Oct 2023 20:03:23 +0200 Subject: [PATCH 103/186] Event manager --- .../MongoDocumentStorage.cs | 2 +- YDotNet.Server.Redis/RedisPubSub.cs | 2 +- YDotNet/Document/Doc.cs | 13 +++++++--- YDotNet/Document/Events/EventManager.cs | 24 +++++++++++++++++++ YDotNet/Document/Events/EventPublisher.cs | 6 ++--- YDotNet/Document/Events/EventSubscriber.cs | 20 ++++++++++------ YDotNet/Document/Events/IEventSubscriber.cs | 6 +++++ YDotNet/Document/Types/Arrays/Array.cs | 1 + YDotNet/Document/Types/Branches/Branch.cs | 1 + YDotNet/Document/Types/Maps/Map.cs | 1 + YDotNet/Document/Types/Texts/Text.cs | 1 + .../Document/Types/XmlElements/XmlElement.cs | 1 + YDotNet/Document/Types/XmlTexts/XmlText.cs | 1 + YDotNet/Document/UndoManagers/UndoManager.cs | 2 ++ 14 files changed, 65 insertions(+), 16 deletions(-) create mode 100644 YDotNet/Document/Events/EventManager.cs create mode 100644 YDotNet/Document/Events/IEventSubscriber.cs diff --git a/YDotNet.Server.MongoDB/MongoDocumentStorage.cs b/YDotNet.Server.MongoDB/MongoDocumentStorage.cs index 007e8622..2400bbc0 100644 --- a/YDotNet.Server.MongoDB/MongoDocumentStorage.cs +++ b/YDotNet.Server.MongoDB/MongoDocumentStorage.cs @@ -7,7 +7,7 @@ namespace YDotNet.Server.MongoDB; public sealed class MongoDocumentStorage : IDocumentStorage, IHostedService { - private readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true }; + private readonly UpdateOptions Upsert = new() { IsUpsert = true }; private readonly MongoDocumentStorageOptions options; private readonly IMongoClient mongoClient; private IMongoCollection? collection; diff --git a/YDotNet.Server.Redis/RedisPubSub.cs b/YDotNet.Server.Redis/RedisPubSub.cs index 8b2245cf..b73d94c8 100644 --- a/YDotNet.Server.Redis/RedisPubSub.cs +++ b/YDotNet.Server.Redis/RedisPubSub.cs @@ -7,7 +7,7 @@ namespace YDotNet.Server.Redis; public sealed class RedisPubSub : IPubSub { - private readonly List> handlers = new List>(); + private readonly List> handlers = new(); private readonly RedisClusteringOptions redisOptions; private ISubscriber? subscriber; diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index 83d60f99..55bbfc38 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -1,9 +1,6 @@ -using YDotNet.Document.Cells; using YDotNet.Document.Events; using YDotNet.Document.Options; using YDotNet.Document.Transactions; -using YDotNet.Document.Types.Branches; -using YDotNet.Document.Types.Events; using YDotNet.Document.Types.Maps; using YDotNet.Document.Types.Texts; using YDotNet.Document.Types.XmlElements; @@ -71,6 +68,7 @@ internal Doc(nint handle, Doc? parent, bool isDeleted) this.parent = parent; onClear = new EventSubscriber( + EventManager, handle, (doc, action) => { @@ -82,6 +80,7 @@ internal Doc(nint handle, Doc? parent, bool isDeleted) (doc, s) => DocChannel.UnobserveClear(doc, s)); onUpdateV1 = new EventSubscriber( + EventManager, handle, (doc, action) => { @@ -93,6 +92,7 @@ internal Doc(nint handle, Doc? parent, bool isDeleted) (doc, s) => DocChannel.UnobserveUpdatesV1(doc, s)); onUpdateV2 = new EventSubscriber( + EventManager, handle, (doc, action) => { @@ -104,6 +104,7 @@ internal Doc(nint handle, Doc? parent, bool isDeleted) (doc, s) => DocChannel.UnobserveUpdatesV2(doc, s)); onAfterTransaction = new EventSubscriber( + EventManager, handle, (doc, action) => { @@ -115,6 +116,7 @@ internal Doc(nint handle, Doc? parent, bool isDeleted) (doc, s) => DocChannel.UnobserveAfterTransaction(doc, s)); onSubDocs = new EventSubscriber( + EventManager, handle, (doc, action) => { @@ -155,6 +157,9 @@ private void DisposeCore() return; } + // Clears all active subscriptions that have not been closed yet. + EventManager.Clear(); + DocChannel.Destroy(Handle); MarkDisposed(); } @@ -236,6 +241,8 @@ public string? CollectionId internal nint Handle { get; } + internal EventManager EventManager { get; } = new EventManager(); + /// /// Gets or creates a new shared data type instance as a root-level /// type in this document. diff --git a/YDotNet/Document/Events/EventManager.cs b/YDotNet/Document/Events/EventManager.cs new file mode 100644 index 00000000..5105f874 --- /dev/null +++ b/YDotNet/Document/Events/EventManager.cs @@ -0,0 +1,24 @@ +namespace YDotNet.Document.Events; + +internal sealed class EventManager +{ + private readonly HashSet activeSubscribers = new(); + + public void Register(IEventSubscriber eventSubscriber) + { + activeSubscribers.Add(eventSubscriber); + } + + public void Unregister(IEventSubscriber eventSubscriber) + { + activeSubscribers.Remove(eventSubscriber); + } + + public void Clear() + { + foreach (var subscriber in activeSubscribers) + { + subscriber.Clear(); + } + } +} diff --git a/YDotNet/Document/Events/EventPublisher.cs b/YDotNet/Document/Events/EventPublisher.cs index 6090af3f..e0cc4adc 100644 --- a/YDotNet/Document/Events/EventPublisher.cs +++ b/YDotNet/Document/Events/EventPublisher.cs @@ -18,10 +18,7 @@ public void Subscribe(Action handler) public void Unsubscribe(Action handler) { - if (!subscriptions.Remove(handler)) - { - return; - } + subscriptions.Remove(handler); } public void Publish(TEvent @event) @@ -34,6 +31,7 @@ public void Publish(TEvent @event) } catch { + // Exceptions could have unknown consequences in the rust part. continue; } } diff --git a/YDotNet/Document/Events/EventSubscriber.cs b/YDotNet/Document/Events/EventSubscriber.cs index 2f56379d..11a15e96 100644 --- a/YDotNet/Document/Events/EventSubscriber.cs +++ b/YDotNet/Document/Events/EventSubscriber.cs @@ -1,28 +1,26 @@ namespace YDotNet.Document.Events; -internal class EventSubscriber +internal class EventSubscriber : IEventSubscriber { - private readonly EventPublisher publisher = new EventPublisher(); + private readonly EventPublisher publisher = new(); + private readonly EventManager manager; private readonly nint owner; private readonly Func, (uint Handle, object Callback)> subscribe; private readonly Action unsubscribe; private (uint Handle, object? Callback) nativeSubscription; public EventSubscriber( + EventManager manager, nint owner, Func, (uint Handle, object Callback)> subscribe, Action unsubscribe) { + this.manager = manager; this.owner = owner; this.subscribe = subscribe; this.unsubscribe = unsubscribe; } - ~EventSubscriber() - { - Clear(); - } - public void Clear() { publisher.Clear(); @@ -32,9 +30,13 @@ public void Clear() public IDisposable Subscribe(Action handler) { + // If this is the first native subscription, subscribe to the actual source by invoking the action. if (nativeSubscription.Callback == null) { nativeSubscription = subscribe(owner, publisher.Publish); + + // Register the subscriber as active in the manager. + manager.Register(this); } publisher.Subscribe(handler); @@ -54,11 +56,15 @@ private void Unsubscribe(Action handler) private void UnsubscribeWhenSubscribed() { + // If this is the last subscription we can unubscribe from the native source again. if (publisher.Count == 0 && nativeSubscription.Callback != null) { unsubscribe(owner, nativeSubscription.Handle); nativeSubscription = default; + + // The manager will clear all active subscriptions when the document where the manager belongs to is disposed. + manager.Unregister(this); } } diff --git a/YDotNet/Document/Events/IEventSubscriber.cs b/YDotNet/Document/Events/IEventSubscriber.cs new file mode 100644 index 00000000..fd15a64d --- /dev/null +++ b/YDotNet/Document/Events/IEventSubscriber.cs @@ -0,0 +1,6 @@ +namespace YDotNet.Document.Events; + +internal interface IEventSubscriber +{ + void Clear(); +} diff --git a/YDotNet/Document/Types/Arrays/Array.cs b/YDotNet/Document/Types/Arrays/Array.cs index 79a37683..8dc1a9cd 100644 --- a/YDotNet/Document/Types/Arrays/Array.cs +++ b/YDotNet/Document/Types/Arrays/Array.cs @@ -21,6 +21,7 @@ internal Array(nint handle, Doc doc, bool isDeleted) : base(handle, doc, isDeleted) { onObserve = new EventSubscriber( + doc.EventManager, handle, (array, action) => { diff --git a/YDotNet/Document/Types/Branches/Branch.cs b/YDotNet/Document/Types/Branches/Branch.cs index 443a5389..eed82c55 100644 --- a/YDotNet/Document/Types/Branches/Branch.cs +++ b/YDotNet/Document/Types/Branches/Branch.cs @@ -22,6 +22,7 @@ internal Branch(nint handle, Doc doc, bool isDeleted) Doc = doc; onDeep = new EventSubscriber( + doc.EventManager, handle, (branch, action) => { diff --git a/YDotNet/Document/Types/Maps/Map.cs b/YDotNet/Document/Types/Maps/Map.cs index 1f6d0fa9..88651e9b 100644 --- a/YDotNet/Document/Types/Maps/Map.cs +++ b/YDotNet/Document/Types/Maps/Map.cs @@ -19,6 +19,7 @@ internal Map(nint handle, Doc doc, bool isDeleted) : base(handle, doc, isDeleted) { onObserve = new EventSubscriber( + doc.EventManager, handle, (map, action) => { diff --git a/YDotNet/Document/Types/Texts/Text.cs b/YDotNet/Document/Types/Texts/Text.cs index dc71f41a..d0be3f64 100644 --- a/YDotNet/Document/Types/Texts/Text.cs +++ b/YDotNet/Document/Types/Texts/Text.cs @@ -21,6 +21,7 @@ internal Text(nint handle, Doc doc, bool isDeleted) : base(handle, doc, isDeleted) { onObserve = new EventSubscriber( + doc.EventManager, handle, (text, action) => { diff --git a/YDotNet/Document/Types/XmlElements/XmlElement.cs b/YDotNet/Document/Types/XmlElements/XmlElement.cs index c70c1ece..4a3555a0 100644 --- a/YDotNet/Document/Types/XmlElements/XmlElement.cs +++ b/YDotNet/Document/Types/XmlElements/XmlElement.cs @@ -22,6 +22,7 @@ internal XmlElement(nint handle, Doc doc, bool isDeleted) : base(handle, doc, isDeleted) { onObserve = new EventSubscriber( + doc.EventManager, handle, (xmlElement, action) => { diff --git a/YDotNet/Document/Types/XmlTexts/XmlText.cs b/YDotNet/Document/Types/XmlTexts/XmlText.cs index 4b98bb95..9b03cc20 100644 --- a/YDotNet/Document/Types/XmlTexts/XmlText.cs +++ b/YDotNet/Document/Types/XmlTexts/XmlText.cs @@ -22,6 +22,7 @@ internal XmlText(nint handle, Doc doc, bool isDeleted) : base(handle, doc, isDeleted) { onObserve = new EventSubscriber( + doc.EventManager, handle, (xmlText, action) => { diff --git a/YDotNet/Document/UndoManagers/UndoManager.cs b/YDotNet/Document/UndoManagers/UndoManager.cs index f3a60f41..51970110 100644 --- a/YDotNet/Document/UndoManagers/UndoManager.cs +++ b/YDotNet/Document/UndoManagers/UndoManager.cs @@ -25,6 +25,7 @@ public UndoManager(Doc doc, Branch branch, UndoManagerOptions? options = null) : base(Create(doc, branch, options)) { onAdded = new EventSubscriber( + doc.EventManager, Handle, (owner, action) => { @@ -36,6 +37,7 @@ public UndoManager(Doc doc, Branch branch, UndoManagerOptions? options = null) (owner, s) => UndoManagerChannel.UnobserveAdded(owner, s)); onPopped = new EventSubscriber( + doc.EventManager, Handle, (owner, action) => { From 0227eae6f8048609fe36a03475cc74b5b8e9549e Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 15 Oct 2023 20:35:01 +0200 Subject: [PATCH 104/186] Unsubscribe on explicit dispose only. --- YDotNet/Document/Doc.cs | 16 ++++++++++------ YDotNet/Infrastructure/Resource.cs | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index 55bbfc38..6d52887d 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -140,28 +140,32 @@ private static nint CreateDoc(DocOptions options) /// ~Doc() { - DisposeCore(); + Dispose(false); } /// public void Dispose() { + Dispose(true); GC.SuppressFinalize(this); - DisposeCore(); } - private void DisposeCore() + private void Dispose(bool disposing) { if (IsDisposed) { return; } - // Clears all active subscriptions that have not been closed yet. - EventManager.Clear(); + MarkDisposed(); + + if (disposing) + { + // Clears all active subscriptions that have not been closed yet. + EventManager.Clear(); + } DocChannel.Destroy(Handle); - MarkDisposed(); } /// diff --git a/YDotNet/Infrastructure/Resource.cs b/YDotNet/Infrastructure/Resource.cs index eedf7449..4ac46841 100644 --- a/YDotNet/Infrastructure/Resource.cs +++ b/YDotNet/Infrastructure/Resource.cs @@ -13,8 +13,8 @@ public abstract class Resource : IDisposable /// public void Dispose() { - GC.SuppressFinalize(this); Dispose(true); + GC.SuppressFinalize(this); } /// From fcaf0e3dcb348766981a2bb0e49bdb0a59523919 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 15 Oct 2023 20:47:20 +0200 Subject: [PATCH 105/186] Guard for open transactions. --- .../Branches/ReadTransactionTests.cs | 5 -- .../Branches/WriteTransactionTests.cs | 4 -- .../Document/ReadTransactionTests.cs | 5 -- .../Document/WriteTransactionTests.cs | 5 -- YDotNet/Document/Doc.cs | 53 +++++++++++++++++++ YDotNet/Document/Transactions/Transaction.cs | 10 ++-- 6 files changed, 60 insertions(+), 22 deletions(-) diff --git a/Tests/YDotNet.Tests.Unit/Branches/ReadTransactionTests.cs b/Tests/YDotNet.Tests.Unit/Branches/ReadTransactionTests.cs index d24f8fdf..aa46409f 100644 --- a/Tests/YDotNet.Tests.Unit/Branches/ReadTransactionTests.cs +++ b/Tests/YDotNet.Tests.Unit/Branches/ReadTransactionTests.cs @@ -83,7 +83,6 @@ public void StartReadTransactionWhileDocumentWriteTransactionIsOpen() } [Test] - [Ignore("Still buggy in y-crdt")] public void GetRootMapWithOpenBranchTransactionNotAllowed() { // Arrange @@ -98,7 +97,6 @@ public void GetRootMapWithOpenBranchTransactionNotAllowed() } [Test] - [Ignore("Still buggy in y-crdt")] public void GetRootArrayWithOpenTransactionNotAllowed() { // Arrange @@ -113,7 +111,6 @@ public void GetRootArrayWithOpenTransactionNotAllowed() } [Test] - [Ignore("Still buggy in y-crdt")] public void GetRootTextWithOpenTransactionNotAllowed() { // Arrange @@ -128,7 +125,6 @@ public void GetRootTextWithOpenTransactionNotAllowed() } [Test] - [Ignore("Still buggy in y-crdt")] public void GetRootXmlTextWithOpenTransactionNotAllowed() { // Arrange @@ -143,7 +139,6 @@ public void GetRootXmlTextWithOpenTransactionNotAllowed() } [Test] - [Ignore("Still buggy in y-crdt")] public void GetRootXmlElementWithOpenTransactionNotAllowed() { // Arrange diff --git a/Tests/YDotNet.Tests.Unit/Branches/WriteTransactionTests.cs b/Tests/YDotNet.Tests.Unit/Branches/WriteTransactionTests.cs index 7faa3eca..46b7bebc 100644 --- a/Tests/YDotNet.Tests.Unit/Branches/WriteTransactionTests.cs +++ b/Tests/YDotNet.Tests.Unit/Branches/WriteTransactionTests.cs @@ -96,7 +96,6 @@ public void GetRootMapWithOpenBranchTransactionNotAllowed() } [Test] - [Ignore("Still buggy in y-crdt")] public void GetRootArrayWithOpenTransactionNotAllowed() { // Arrange @@ -111,7 +110,6 @@ public void GetRootArrayWithOpenTransactionNotAllowed() } [Test] - [Ignore("Still buggy in y-crdt")] public void GetRootTextWithOpenTransactionNotAllowed() { // Arrange @@ -126,7 +124,6 @@ public void GetRootTextWithOpenTransactionNotAllowed() } [Test] - [Ignore("Still buggy in y-crdt")] public void GetRootXmlTextWithOpenTransactionNotAllowed() { // Arrange @@ -141,7 +138,6 @@ public void GetRootXmlTextWithOpenTransactionNotAllowed() } [Test] - [Ignore("Still buggy in y-crdt")] public void GetRootXmlElementWithOpenTransactionNotAllowed() { // Arrange diff --git a/Tests/YDotNet.Tests.Unit/Document/ReadTransactionTests.cs b/Tests/YDotNet.Tests.Unit/Document/ReadTransactionTests.cs index e9f76d81..38cebf06 100644 --- a/Tests/YDotNet.Tests.Unit/Document/ReadTransactionTests.cs +++ b/Tests/YDotNet.Tests.Unit/Document/ReadTransactionTests.cs @@ -33,7 +33,6 @@ public void MultipleReadTransactionsAllowed() } [Test] - [Ignore("Still buggy in y-crdt")] public void GetRootMapWithOpenTransactionNotAllowed() { // Arrange @@ -48,7 +47,6 @@ public void GetRootMapWithOpenTransactionNotAllowed() } [Test] - [Ignore("Still buggy in y-crdt")] public void GetRootArrayWithOpenTransactionNotAllowed() { // Arrange @@ -63,7 +61,6 @@ public void GetRootArrayWithOpenTransactionNotAllowed() } [Test] - [Ignore("Still buggy in y-crdt")] public void GetRootTextWithOpenTransactionNotAllowed() { // Arrange @@ -78,7 +75,6 @@ public void GetRootTextWithOpenTransactionNotAllowed() } [Test] - [Ignore("Still buggy in y-crdt")] public void GetRootXmlTextWithOpenTransactionNotAllowed() { // Arrange @@ -93,7 +89,6 @@ public void GetRootXmlTextWithOpenTransactionNotAllowed() } [Test] - [Ignore("Still buggy in y-crdt")] public void GetRootXmlElementWithOpenTransactionNotAllowed() { // Arrange diff --git a/Tests/YDotNet.Tests.Unit/Document/WriteTransactionTests.cs b/Tests/YDotNet.Tests.Unit/Document/WriteTransactionTests.cs index 5c87cda8..1f48f6ce 100644 --- a/Tests/YDotNet.Tests.Unit/Document/WriteTransactionTests.cs +++ b/Tests/YDotNet.Tests.Unit/Document/WriteTransactionTests.cs @@ -30,7 +30,6 @@ public void MultipleWriteTransactionsNotAllowed() } [Test] - [Ignore("Still buggy in y-crdt")] public void GetRootMapWithOpenTransactionNotAllowed() { // Arrange @@ -45,7 +44,6 @@ public void GetRootMapWithOpenTransactionNotAllowed() } [Test] - [Ignore("Still buggy in y-crdt")] public void GetRootArrayWithOpenTransactionNotAllowed() { // Arrange @@ -60,7 +58,6 @@ public void GetRootArrayWithOpenTransactionNotAllowed() } [Test] - [Ignore("Still buggy in y-crdt")] public void GetRootTextWithOpenTransactionNotAllowed() { // Arrange @@ -75,7 +72,6 @@ public void GetRootTextWithOpenTransactionNotAllowed() } [Test] - [Ignore("Still buggy in y-crdt")] public void GetRootXmlTextWithOpenTransactionNotAllowed() { // Arrange @@ -90,7 +86,6 @@ public void GetRootXmlTextWithOpenTransactionNotAllowed() } [Test] - [Ignore("Still buggy in y-crdt")] public void GetRootXmlElementWithOpenTransactionNotAllowed() { // Arrange diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index 6d52887d..c5195b28 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -40,6 +40,7 @@ public class Doc : TypeBase, IDisposable private readonly EventSubscriber onAfterTransaction; private readonly EventSubscriber onSubDocs; private readonly Doc? parent; + private int openTransactions; /// /// Initializes a new instance of the class. @@ -258,6 +259,9 @@ public string? CollectionId /// The instance related to the name provided. public Text Text(string name) { + ThrowIfDisposed(); + ThrowIfOpenTransaction(); + using var unsafeName = MemoryWriter.WriteUtf8String(name); var handle = DocChannel.Text(Handle, unsafeName.Handle); @@ -275,6 +279,9 @@ public Text Text(string name) /// The instance related to the name provided. public Map Map(string name) { + ThrowIfDisposed(); + ThrowIfOpenTransaction(); + using var unsafeName = MemoryWriter.WriteUtf8String(name); var handle = DocChannel.Map(Handle, unsafeName.Handle); @@ -292,6 +299,9 @@ public Map Map(string name) /// The instance related to the name provided. public Array Array(string name) { + ThrowIfDisposed(); + ThrowIfOpenTransaction(); + using var unsafeName = MemoryWriter.WriteUtf8String(name); var handle = DocChannel.Array(Handle, unsafeName.Handle); @@ -309,6 +319,9 @@ public Array Array(string name) /// The instance related to the name provided. public XmlElement XmlElement(string name) { + ThrowIfDisposed(); + ThrowIfOpenTransaction(); + using var unsafeName = MemoryWriter.WriteUtf8String(name); var handle = DocChannel.XmlElement(Handle, unsafeName.Handle); @@ -326,12 +339,23 @@ public XmlElement XmlElement(string name) /// The instance related to the name provided. public XmlText XmlText(string name) { + ThrowIfDisposed(); + ThrowIfOpenTransaction(); + using var unsafeName = MemoryWriter.WriteUtf8String(name); var handle = DocChannel.XmlText(Handle, unsafeName.Handle); return GetXmlText(handle, false); } + internal void ThrowIfOpenTransaction() + { + if (openTransactions > 0) + { + ThrowHelper.PendingTransaction(); + } + } + /// /// Starts a new read-write on this document. /// @@ -340,6 +364,8 @@ public XmlText XmlText(string name) /// Another write transaction has been created and not commited yet. public Transaction WriteTransaction(byte[]? origin = null) { + ThrowIfDisposed(); + var handle = DocChannel.WriteTransaction(Handle, (uint)(origin?.Length ?? 0), origin); if (handle == nint.Zero) @@ -358,6 +384,8 @@ public Transaction WriteTransaction(byte[]? origin = null) /// Another write transaction has been created and not commited yet. public Transaction ReadTransaction() { + ThrowIfDisposed(); + var handle = DocChannel.ReadTransaction(Handle); if (handle == nint.Zero) @@ -374,7 +402,10 @@ public Transaction ReadTransaction() /// public void Clear() { + ThrowIfDisposed(); + DocChannel.Clear(Handle); + onClear.Clear(); onUpdateV1.Clear(); onUpdateV2.Clear(); @@ -391,6 +422,8 @@ public void Clear() /// A read-only of the parent document. public void Load(Transaction transaction) { + ThrowIfDisposed(); + DocChannel.Load(Handle, transaction.Handle); } @@ -401,6 +434,8 @@ public void Load(Transaction transaction) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable ObserveClear(Action action) { + ThrowIfDisposed(); + return onClear.Subscribe(action); } @@ -414,6 +449,8 @@ public IDisposable ObserveClear(Action action) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable ObserveUpdatesV1(Action action) { + ThrowIfDisposed(); + return onUpdateV1.Subscribe(action); } @@ -427,6 +464,8 @@ public IDisposable ObserveUpdatesV1(Action action) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable ObserveUpdatesV2(Action action) { + ThrowIfDisposed(); + return onUpdateV2.Subscribe(action); } @@ -440,6 +479,8 @@ public IDisposable ObserveUpdatesV2(Action action) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable ObserveAfterTransaction(Action action) { + ThrowIfDisposed(); + return onAfterTransaction.Subscribe(action); } @@ -450,6 +491,8 @@ public IDisposable ObserveAfterTransaction(Action action) /// The subscription for the event. It may be used to unsubscribe later. public IDisposable ObserveSubDocs(Action action) { + ThrowIfDisposed(); + return onSubDocs.Subscribe(action); } @@ -469,6 +512,16 @@ internal Doc GetDoc(nint handle, bool isDeleted) return GetOrAdd(handle, (h, doc) => new Doc(handle, doc, isDeleted)); } + internal void NotifyTransactionStarted() + { + openTransactions++; + } + + internal void NotifyTransactionClosed() + { + openTransactions--; + } + internal Map GetMap(nint handle, bool isDeleted) { return GetOrAdd(handle, (h, doc) => new Map(h, doc, isDeleted)); diff --git a/YDotNet/Document/Transactions/Transaction.cs b/YDotNet/Document/Transactions/Transaction.cs index 3fc90228..6c20b5d9 100644 --- a/YDotNet/Document/Transactions/Transaction.cs +++ b/YDotNet/Document/Transactions/Transaction.cs @@ -28,6 +28,8 @@ internal Transaction(nint handle, Doc doc) : base(handle) { this.doc = doc; + + doc.NotifyTransactionStarted(); } /// @@ -35,7 +37,9 @@ protected internal override void DisposeCore(bool disposing) { if (disposing) { - Commit(); + TransactionChannel.Commit(Handle); + + doc.NotifyTransactionClosed(); } } @@ -53,8 +57,8 @@ protected internal override void DisposeCore(bool disposing) /// public void Commit() { - ThrowIfDisposed(); - TransactionChannel.Commit(Handle); + // The dispose method has a solution to prevent multiple dipose, so we can just use that. + Dispose(); } /// From 02604ef0155ac96108931d106254847206c9ae98 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 16 Oct 2023 14:01:34 +0200 Subject: [PATCH 106/186] Improve ToJson. --- Directory.Build.props | 2 +- .../Extensions/Conversion.cs | 72 +++++++++++++++++++ .../Infrastructure/ClientIdTests.cs | 18 +++++ .../YDotNet.Tests.Unit.csproj | 1 + YDotNet.Extensions/YDotNetExtensions.cs | 34 +++++++-- YDotNet/Document/Options/DocOptions.cs | 2 +- YDotNet/Infrastructure/ClientId.cs | 23 ++++++ 7 files changed, 144 insertions(+), 8 deletions(-) create mode 100644 Tests/YDotNet.Tests.Unit/Extensions/Conversion.cs create mode 100644 Tests/YDotNet.Tests.Unit/Infrastructure/ClientIdTests.cs create mode 100644 YDotNet/Infrastructure/ClientId.cs diff --git a/Directory.Build.props b/Directory.Build.props index b25c578d..1d447599 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,7 +9,7 @@ https://github.com/LSViana/YDotNet true snupkg - 0.2.3 + 0.2.4 diff --git a/Tests/YDotNet.Tests.Unit/Extensions/Conversion.cs b/Tests/YDotNet.Tests.Unit/Extensions/Conversion.cs new file mode 100644 index 00000000..26230b89 --- /dev/null +++ b/Tests/YDotNet.Tests.Unit/Extensions/Conversion.cs @@ -0,0 +1,72 @@ +using NUnit.Framework; +using System.Text.Json.Serialization; +using YDotNet.Document; +using YDotNet.Document.Cells; +using YDotNet.Extensions; + +namespace YDotNet.Tests.Unit.Extensions; + +internal class Conversion +{ + public class Expected + { + [JsonPropertyName("value")] + public string Value { get; set; } + } + + [Test] + public void ConvertTo() + { + // Arrange + var doc = new Doc(); + var map = doc.Map("map"); + + using (var transaction = map.WriteTransaction()) + { + map.Insert(transaction, "inner", Input.Object(new Dictionary + { + ["value"] = Input.String("Hello YDotNet") + })); + } + + // Act + Expected result; + using (var transaction = map.WriteTransaction()) + { + var inner = map.Get(transaction, "inner"); + + result = inner.To(transaction); + } + + // Assert + Assert.That(result.Value, Is.EqualTo("Hello YDotNet")); + } + + [Test] + public void ConvertToJson() + { + // Arrange + var doc = new Doc(); + var map = doc.Map("map"); + + using (var transaction = map.WriteTransaction()) + { + map.Insert(transaction, "inner", Input.Object(new Dictionary + { + ["value"] = Input.String("Hello YDotNet") + })); + } + + // Act + string json; + using (var transaction = map.WriteTransaction()) + { + var inner = map.Get(transaction, "inner"); + + json = inner.ToJson(transaction); + } + + // Assert + StringAssert.Contains("Hello YDotNet", json); + } +} diff --git a/Tests/YDotNet.Tests.Unit/Infrastructure/ClientIdTests.cs b/Tests/YDotNet.Tests.Unit/Infrastructure/ClientIdTests.cs new file mode 100644 index 00000000..039249d9 --- /dev/null +++ b/Tests/YDotNet.Tests.Unit/Infrastructure/ClientIdTests.cs @@ -0,0 +1,18 @@ +using NUnit.Framework; +using YDotNet.Infrastructure; + +namespace YDotNet.Tests.Unit.Infrastructure; + +public class ClientIdTests +{ + [Test] + public void TestSatefy() + { + for (var i = 0; i < 10_000_000; i++) + { + var id = ClientId.GetRandom(); + + Assert.That(id, Is.LessThanOrEqualTo(ClientId.MaxSafeInteger)); + } + } +} diff --git a/Tests/YDotNet.Tests.Unit/YDotNet.Tests.Unit.csproj b/Tests/YDotNet.Tests.Unit/YDotNet.Tests.Unit.csproj index 8a9e55d5..229e7db1 100644 --- a/Tests/YDotNet.Tests.Unit/YDotNet.Tests.Unit.csproj +++ b/Tests/YDotNet.Tests.Unit/YDotNet.Tests.Unit.csproj @@ -24,6 +24,7 @@ + diff --git a/YDotNet.Extensions/YDotNetExtensions.cs b/YDotNet.Extensions/YDotNetExtensions.cs index 7956516a..30f7b3fa 100644 --- a/YDotNet.Extensions/YDotNetExtensions.cs +++ b/YDotNet.Extensions/YDotNetExtensions.cs @@ -1,3 +1,5 @@ +using System.Formats.Asn1; +using System.Text; using System.Text.Json; using YDotNet.Document; using YDotNet.Document.Cells; @@ -60,10 +62,35 @@ public static T To(this Output output, Doc doc) public static T To(this Output output, Transaction transaction) { var jsonStream = new MemoryStream(); - var jsonWriter = new Utf8JsonWriter(jsonStream); + + output.ToJson(jsonStream, transaction); + + jsonStream.Flush(); + jsonStream.Position = 0; + + return JsonSerializer.Deserialize(jsonStream)!; + } + + public static string ToJson(this Output output, Transaction transaction) + { + var jsonStream = new MemoryStream(); + + output.ToJson(jsonStream, transaction); + + jsonStream.Flush(); + jsonStream.Position = 0; + + return Encoding.UTF8.GetString(jsonStream.ToArray()); + } + + public static void ToJson(this Output output, Stream stream, Transaction transaction) + { + var jsonWriter = new Utf8JsonWriter(stream); WriteValue(output, jsonWriter, transaction); + jsonWriter.Flush(); + static void WriteJsonObject(JsonObject obj, Utf8JsonWriter jsonWriter, Transaction transaction) { jsonWriter.WriteStartObject(); @@ -161,10 +188,5 @@ static void WriteValue(Output output, Utf8JsonWriter jsonWriter, Transaction tra throw new InvalidOperationException("Unsupported data type."); } } - - jsonWriter.Flush(); - jsonStream.Position = 0; - - return JsonSerializer.Deserialize(jsonStream)!; } } diff --git a/YDotNet/Document/Options/DocOptions.cs b/YDotNet/Document/Options/DocOptions.cs index 0fb5b79c..ee14fb03 100644 --- a/YDotNet/Document/Options/DocOptions.cs +++ b/YDotNet/Document/Options/DocOptions.cs @@ -15,7 +15,7 @@ public class DocOptions /// public static DocOptions Default => new() { - Id = (ulong)Random.Shared.Next() + Id = ClientId.GetRandom() }; /// diff --git a/YDotNet/Infrastructure/ClientId.cs b/YDotNet/Infrastructure/ClientId.cs new file mode 100644 index 00000000..ba563004 --- /dev/null +++ b/YDotNet/Infrastructure/ClientId.cs @@ -0,0 +1,23 @@ +namespace YDotNet.Infrastructure; + +/// +/// Helper class to deal with client ids. +/// +public sealed class ClientId +{ + /// + /// The maximum safe integer from javascript. + /// + public const long MaxSafeInteger = 2 ^ 53 - 1; + + /// + /// Gets a random client id. + /// + /// The random client id. + public static ulong GetRandom() + { + var value = (ulong)Random.Shared.Next() & MaxSafeInteger; + + return value; + } +} From a1dac8bbba070c53548cedf16f1bce3a454f7ea8 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 16 Oct 2023 14:36:20 +0200 Subject: [PATCH 107/186] Fix tests --- Tests/YDotNet.Tests.Unit/Document/UpdatesV1Tests.cs | 4 ++-- Tests/YDotNet.Tests.Unit/Transactions/SnapshotTests.cs | 4 ++-- Tests/YDotNet.Tests.Unit/Transactions/StateDiffV1Tests.cs | 4 ++-- Tests/YDotNet.Tests.Unit/Transactions/StateDiffV2Tests.cs | 4 ++-- Tests/YDotNet.Tests.Unit/Transactions/StateVectorV1Tests.cs | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Tests/YDotNet.Tests.Unit/Document/UpdatesV1Tests.cs b/Tests/YDotNet.Tests.Unit/Document/UpdatesV1Tests.cs index 687fba32..8170a894 100644 --- a/Tests/YDotNet.Tests.Unit/Document/UpdatesV1Tests.cs +++ b/Tests/YDotNet.Tests.Unit/Document/UpdatesV1Tests.cs @@ -30,7 +30,7 @@ public void TriggersWhenTransactionIsCommittedUntilUnobserve() // Assert Assert.That(called, Is.EqualTo(expected: 1)); Assert.That(data, Is.Not.Null); - Assert.That(data, Has.Length.InRange(from: 20, to: 30)); + Assert.That(data, Has.Length.InRange(from: 10, to: 35)); // Act data = null; @@ -41,7 +41,7 @@ public void TriggersWhenTransactionIsCommittedUntilUnobserve() // Assert Assert.That(called, Is.EqualTo(expected: 2)); Assert.That(data, Is.Not.Null); - Assert.That(data, Has.Length.InRange(from: 18, to: 31)); + Assert.That(data, Has.Length.InRange(from: 10, to: 35)); // Act data = null; diff --git a/Tests/YDotNet.Tests.Unit/Transactions/SnapshotTests.cs b/Tests/YDotNet.Tests.Unit/Transactions/SnapshotTests.cs index 0330fe28..79a768cb 100644 --- a/Tests/YDotNet.Tests.Unit/Transactions/SnapshotTests.cs +++ b/Tests/YDotNet.Tests.Unit/Transactions/SnapshotTests.cs @@ -16,7 +16,7 @@ public void ReadOnly() // Assert Assert.That(snapshot, Is.Not.Null); - Assert.That(snapshot.Length, Is.GreaterThan(expected: 5)); + Assert.That(snapshot.Length, Is.GreaterThan(expected: 3)); } [Test] @@ -30,7 +30,7 @@ public void ReadWrite() // Assert Assert.That(snapshot, Is.Not.Null); - Assert.That(snapshot.Length, Is.GreaterThan(expected: 5)); + Assert.That(snapshot.Length, Is.GreaterThan(expected: 3)); } private static Doc ArrangeDoc() diff --git a/Tests/YDotNet.Tests.Unit/Transactions/StateDiffV1Tests.cs b/Tests/YDotNet.Tests.Unit/Transactions/StateDiffV1Tests.cs index dfb29da8..25ecb43b 100644 --- a/Tests/YDotNet.Tests.Unit/Transactions/StateDiffV1Tests.cs +++ b/Tests/YDotNet.Tests.Unit/Transactions/StateDiffV1Tests.cs @@ -18,7 +18,7 @@ public void ReadOnly() // Assert Assert.That(stateDiff, Is.Not.Null); - Assert.That(stateDiff.Length, Is.GreaterThan(expected: 19)); + Assert.That(stateDiff.Length, Is.GreaterThan(expected: 15)); } [Test] @@ -34,7 +34,7 @@ public void ReadWrite() // Assert Assert.That(stateDiff, Is.Not.Null); - Assert.That(stateDiff.Length, Is.GreaterThan(expected: 19)); + Assert.That(stateDiff.Length, Is.GreaterThan(expected: 15)); } private static Doc ArrangeSenderDoc() diff --git a/Tests/YDotNet.Tests.Unit/Transactions/StateDiffV2Tests.cs b/Tests/YDotNet.Tests.Unit/Transactions/StateDiffV2Tests.cs index 8876eb02..b31fc5d2 100644 --- a/Tests/YDotNet.Tests.Unit/Transactions/StateDiffV2Tests.cs +++ b/Tests/YDotNet.Tests.Unit/Transactions/StateDiffV2Tests.cs @@ -18,7 +18,7 @@ public void ReadOnly() // Assert Assert.That(stateDiff, Is.Not.Null); - Assert.That(stateDiff.Length, Is.GreaterThan(expected: 29)); + Assert.That(stateDiff.Length, Is.GreaterThan(expected: 20)); } [Test] @@ -34,7 +34,7 @@ public void ReadWrite() // Assert Assert.That(stateDiff, Is.Not.Null); - Assert.That(stateDiff.Length, Is.GreaterThan(expected: 29)); + Assert.That(stateDiff.Length, Is.GreaterThan(expected: 20)); } private static Doc ArrangeSenderDoc() diff --git a/Tests/YDotNet.Tests.Unit/Transactions/StateVectorV1Tests.cs b/Tests/YDotNet.Tests.Unit/Transactions/StateVectorV1Tests.cs index ecec4736..fd190d0b 100644 --- a/Tests/YDotNet.Tests.Unit/Transactions/StateVectorV1Tests.cs +++ b/Tests/YDotNet.Tests.Unit/Transactions/StateVectorV1Tests.cs @@ -16,7 +16,7 @@ public void ReadOnly() // Assert Assert.That(stateVector, Is.Not.Null); - Assert.That(stateVector.Length, Is.GreaterThanOrEqualTo(expected: 4)); + Assert.That(stateVector.Length, Is.GreaterThanOrEqualTo(expected: 3)); } [Test] @@ -30,7 +30,7 @@ public void ReadWrite() // Assert Assert.That(stateVector, Is.Not.Null); - Assert.That(stateVector.Length, Is.GreaterThanOrEqualTo(expected: 5)); + Assert.That(stateVector.Length, Is.GreaterThanOrEqualTo(expected: 3)); } private static Doc ArrangeDoc() From 2915eee0d8fac2302a8bedb01cc3eca476abea6d Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 16 Oct 2023 16:16:18 +0200 Subject: [PATCH 108/186] Prepare for PR. --- YDotNet.Extensions/YDotNet.Extensions.csproj | 1 - YDotNet.Native/Class1.cs | 7 -- .../YDotNet.Server.Clustering.csproj | 1 - .../YDotNet.Server.MongoDB.csproj | 1 - .../YDotNet.Server.Redis.csproj | 1 - .../YDotNet.Server.WebSockets.csproj | 1 - YDotNet.Server/YDotNet.Server.csproj | 1 - YDotNet/YDotNet.csproj | 1 - docs/decision.md | 91 +++++++++++++++++++ .../YDotNet.Native.Linux.csproj | 1 - .../YDotNet.Native.MacOS.csproj | 1 - .../YDotNet.Native.Win32.csproj | 1 - native/YDotNet.Native/YDotNet.Native.csproj | 1 - 13 files changed, 91 insertions(+), 18 deletions(-) delete mode 100644 YDotNet.Native/Class1.cs create mode 100644 docs/decision.md diff --git a/YDotNet.Extensions/YDotNet.Extensions.csproj b/YDotNet.Extensions/YDotNet.Extensions.csproj index bd313047..153967cc 100644 --- a/YDotNet.Extensions/YDotNet.Extensions.csproj +++ b/YDotNet.Extensions/YDotNet.Extensions.csproj @@ -4,7 +4,6 @@ net7.0 enable enable - Squidex.YDotNet.Extensions diff --git a/YDotNet.Native/Class1.cs b/YDotNet.Native/Class1.cs deleted file mode 100644 index 0593741a..00000000 --- a/YDotNet.Native/Class1.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace YDotNet.Native -{ - public class Class1 - { - - } -} \ No newline at end of file diff --git a/YDotNet.Server.Clustering/YDotNet.Server.Clustering.csproj b/YDotNet.Server.Clustering/YDotNet.Server.Clustering.csproj index 71da2005..a1d898ca 100644 --- a/YDotNet.Server.Clustering/YDotNet.Server.Clustering.csproj +++ b/YDotNet.Server.Clustering/YDotNet.Server.Clustering.csproj @@ -4,7 +4,6 @@ net7.0 enable enable - Squidex.YDotNet.Server.Clustering diff --git a/YDotNet.Server.MongoDB/YDotNet.Server.MongoDB.csproj b/YDotNet.Server.MongoDB/YDotNet.Server.MongoDB.csproj index d79a5c23..8f096c2d 100644 --- a/YDotNet.Server.MongoDB/YDotNet.Server.MongoDB.csproj +++ b/YDotNet.Server.MongoDB/YDotNet.Server.MongoDB.csproj @@ -4,7 +4,6 @@ net7.0 enable enable - Squidex.YDotNet.Server.MongoDB diff --git a/YDotNet.Server.Redis/YDotNet.Server.Redis.csproj b/YDotNet.Server.Redis/YDotNet.Server.Redis.csproj index a533e949..36a8c68a 100644 --- a/YDotNet.Server.Redis/YDotNet.Server.Redis.csproj +++ b/YDotNet.Server.Redis/YDotNet.Server.Redis.csproj @@ -4,7 +4,6 @@ net7.0 enable enable - Squidex.YDotNet.Server.Redis diff --git a/YDotNet.Server.WebSockets/YDotNet.Server.WebSockets.csproj b/YDotNet.Server.WebSockets/YDotNet.Server.WebSockets.csproj index 4b48c583..91a13e8a 100644 --- a/YDotNet.Server.WebSockets/YDotNet.Server.WebSockets.csproj +++ b/YDotNet.Server.WebSockets/YDotNet.Server.WebSockets.csproj @@ -4,7 +4,6 @@ net7.0 enable enable - Squidex.YDotNet.Server.WebSockets diff --git a/YDotNet.Server/YDotNet.Server.csproj b/YDotNet.Server/YDotNet.Server.csproj index a50cb27f..d5fa42a0 100644 --- a/YDotNet.Server/YDotNet.Server.csproj +++ b/YDotNet.Server/YDotNet.Server.csproj @@ -4,7 +4,6 @@ net7.0 enable enable - Squidex.YDotNet.Server diff --git a/YDotNet/YDotNet.csproj b/YDotNet/YDotNet.csproj index e98f171f..a791eb4e 100644 --- a/YDotNet/YDotNet.csproj +++ b/YDotNet/YDotNet.csproj @@ -6,7 +6,6 @@ enable true True - Squidex.YDotNet diff --git a/docs/decision.md b/docs/decision.md new file mode 100644 index 00000000..2bbf1078 --- /dev/null +++ b/docs/decision.md @@ -0,0 +1,91 @@ +# Decision + +## 2023-10-16 Do not document internal classes via XML ocmments + +They are tedious and annoying to write and have to be updated regularly. Thefore it does not make sense to add any XML comments for XML types. We should document code and the reason why something has been implemented in a certain way. + +## 2023-10-16 No circular dependencies between native and types + +There is the natural dependency between document types to native, because they read data, but there is also a dependency in the other direction because some native types can actually generate stuff with `ToXXX` methods. This is a circular dependency between namespaces that should in general be avoided. + +Furthermore some types do not rely on the native types and calculate pointers manually, e.g. https://github.com/LSViana/ydotnet/blob/main/YDotNet/Document/Types/Events/EventDeltaAttribute.cs + +Therefore I ensured that... + +1. There is always a native type. +2. Native types are passed to the CLR types via constructors. +3. Native types are responsible to follow pointers, e.g. https://github.com/SebastianStehle/ydotnet/blob/main/YDotNet/Native/UndoManager/Events/UndoEventNative.cs#L18C5-L27C1 and everything around reading memory is part of the native part. These reading operations are provided via methods to indicate that it is not a property and not part of the struct itself. + +Some objects also need the handle and the native struct. Therefore a new type has been introduced: + +```csharp +internal record struct NativeWithHandle(T Value, nint Handle) where T : struct; +``` + +https://github.com/SebastianStehle/ydotnet/blob/main/YDotNet/Native/NativeWithHandle.cs + +## 2023-10-16 The library should not provide access to invalid objects + +When we access a pointer that is not pointing to a valid struct anymore, the process can crash. This is very hard to catch and debug (because logging does not work) and therefore needs to be avoided at all costs. + +If an object has been deleted it shoudl throw an `ObjectDisposedException`. This task is still work in progress but a lot of changes have been done to make this work. + +1. The document has type cache, which is basically a map from handle to type: https://github.com/SebastianStehle/ydotnet/blob/main/YDotNet/Infrastructure/TypeCache.cs. This type cache ensures that there is only one object for each pointer. The type cache uses a weak map to ensure that unused objects can be garbage collected. +2. The type cache gives the option to mark object as deleted, for example when we receive an event from yrs that an object has been removed. This is still work in progress, because yrs does not provide all relevant information yet. +3. Because of the type cache each object needs a reference to the document to be able to construct types using the document. +4. Furthermore some objects are also deleted from beginning. If you get an old value from an event that points to a removed map, this map is not usable anymore and should be marked as deleted. Similar for removed subdocuments. + +## 2023-10-16 Objects must not be disposed twice + +Transactiosn and every type that can be disposed must not be disposed twice. This can cause an application crash when a danging pointer is used after the first dispose. New base classes have been introduced to ensure that this does not happen. + +## 2023-10-16 Not all transaction cases are catched by yrs + +When you create a new root object while a transaction is still pending the application crashs. Therefore we also implement a mechanism in .NET to track the amount of pending transactions. Tests have been added for this case. + +## 2023-10-16 The types should not be responsible to release memory properly + +When you call a native method in a type it is necessary to allocate unmanaged memory to pass a pointer to string or buffer. Before the change the methods are responsible to release the memory and take care about that in every single place. + +To improve that the memory writer returns a new type now: + +```csharp +internal sealed record DisposableHandle(nint Handle) : IDisposable +{ + public void Dispose() + { + if (Handle != nint.Zero) + { + Marshal.FreeHGlobal(Handle); + } + } +} +``` + +When using the dispose pattern properly the type methods do not have to handle this anymore. For example: + +```csharp +public void Insert(Transaction transaction, string key, Input input) +{ + ThrowIfDisposed(); + + using var unsafeKey = MemoryWriter.WriteUtf8String(key); + using var unsafeValue = MemoryWriter.WriteStruct(input.InputNative); + + MapChannel.Insert(Handle, transaction.Handle, unsafeKey.Handle, unsafeValue.Handle); +} +``` + +This change also made a simplification of the input class possible and eliminates the inherited type of inputs. + +## 2023-10-16 MapEntry is useless + +This type is from yrs to transport key and value. In .NET we are used to leverage the `KeyValuePair` struct from that. Therefore we got rid of that, to also build the Map structures more similar to .NET. + +Whenevery we deal with key-value pairs we also expose them as dictionaries, for example in events to make the access by key as fast as possible. + +## 2023-10-16 Do not implement IEnumerable when not needed + +Some types like `EventChanges` represent readonly collections. They are implemented manually. This has been optimized by inheriting from readonly collections that are designed for that. Same for dictionaries. + +This provides more methods and properties (like Count) for the user and also removes the amount of code. \ No newline at end of file diff --git a/native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj b/native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj index 7e6fc360..ddeadac2 100644 --- a/native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj +++ b/native/YDotNet.Native.Linux/YDotNet.Native.Linux.csproj @@ -4,7 +4,6 @@ net7.0 enable enable - Squidex.YDotNet.Native.Linux diff --git a/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj b/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj index efb281cf..f44615da 100644 --- a/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj +++ b/native/YDotNet.Native.MacOS/YDotNet.Native.MacOS.csproj @@ -4,7 +4,6 @@ net7.0 enable enable - Squidex.YDotNet.Native.MacOS diff --git a/native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj b/native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj index 8c9fb11d..31220e5e 100644 --- a/native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj +++ b/native/YDotNet.Native.Win32/YDotNet.Native.Win32.csproj @@ -4,7 +4,6 @@ net7.0 enable enable - Squidex.YDotNet.Native.Win32 diff --git a/native/YDotNet.Native/YDotNet.Native.csproj b/native/YDotNet.Native/YDotNet.Native.csproj index 2b74ee3c..a51cb7c8 100644 --- a/native/YDotNet.Native/YDotNet.Native.csproj +++ b/native/YDotNet.Native/YDotNet.Native.csproj @@ -4,7 +4,6 @@ net7.0 enable enable - Squidex.YDotNet.Native From 5aa115c16943c2e6bb65acc2201627d4e6b5d053 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 16 Oct 2023 16:32:08 +0200 Subject: [PATCH 109/186] Add factory. --- Directory.Build.props | 2 +- YDotNet.Extensions/InputFactory.cs | 53 +++++++++++++++++++++++++ YDotNet.Extensions/YDotNetExtensions.cs | 42 ++------------------ 3 files changed, 58 insertions(+), 39 deletions(-) create mode 100644 YDotNet.Extensions/InputFactory.cs diff --git a/Directory.Build.props b/Directory.Build.props index 1d447599..c0f82565 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,7 +9,7 @@ https://github.com/LSViana/YDotNet true snupkg - 0.2.4 + 0.2.5 diff --git a/YDotNet.Extensions/InputFactory.cs b/YDotNet.Extensions/InputFactory.cs new file mode 100644 index 00000000..e474dca5 --- /dev/null +++ b/YDotNet.Extensions/InputFactory.cs @@ -0,0 +1,53 @@ +using System.Text.Json; +using YDotNet.Document.Cells; + +namespace YDotNet.Extensions; + +internal class InputFactory +{ + public static Input FromJson(string json) + { + using (JsonDocument doc = JsonDocument.Parse(json)) + { + return FromJson(doc.RootElement); + } + } + + public static Input FromJson(JsonElement json) + { + return ConvertValue(json); + + static Input ConvertObject(JsonElement element) + { + return Input.Object(element.EnumerateObject().ToDictionary(x => x.Name, x => ConvertValue(x.Value))); + } + + static Input ConvertArray(JsonElement element) + { + return Input.Array(element.EnumerateArray().Select(ConvertValue).ToArray()); + } + + static Input ConvertValue(JsonElement element) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + return ConvertObject(element); + case JsonValueKind.Array: + return ConvertArray(element); + case JsonValueKind.String: + return Input.String(element.GetString() ?? string.Empty); + case JsonValueKind.Number: + return Input.Double(element.GetDouble()); + case JsonValueKind.True: + return Input.Boolean(true); + case JsonValueKind.False: + return Input.Boolean(false); + case JsonValueKind.Null: + return Input.Null(); + default: + throw new NotSupportedException(); + } + } + } +} diff --git a/YDotNet.Extensions/YDotNetExtensions.cs b/YDotNet.Extensions/YDotNetExtensions.cs index 30f7b3fa..6c90eb0e 100644 --- a/YDotNet.Extensions/YDotNetExtensions.cs +++ b/YDotNet.Extensions/YDotNetExtensions.cs @@ -15,46 +15,12 @@ public static Input ToInput(this T source) { var parsed = JsonSerializer.SerializeToElement(source); - return ConvertValue(parsed); - - static Input ConvertObject(JsonElement element) - { - return Input.Object(element.EnumerateObject().ToDictionary(x => x.Name, x => ConvertValue(x.Value))); - } - - static Input ConvertArray(JsonElement element) - { - return Input.Array(element.EnumerateArray().Select(ConvertValue).ToArray()); - } - - static Input ConvertValue(JsonElement element) - { - switch (element.ValueKind) - { - case JsonValueKind.Object: - return ConvertObject(element); - case JsonValueKind.Array: - return ConvertArray(element); - case JsonValueKind.String: - return Input.String(element.GetString() ?? string.Empty); - case JsonValueKind.Number: - return Input.Double(element.GetDouble()); - case JsonValueKind.True: - return Input.Boolean(true); - case JsonValueKind.False: - return Input.Boolean(false); - case JsonValueKind.Null: - return Input.Null(); - default: - throw new NotSupportedException(); - } - } + return InputFactory.FromJson(parsed); } public static T To(this Output output, Doc doc) { - using var transaction = doc.ReadTransaction() - ?? throw new InvalidOperationException("Failed to open transaction."); + using var transaction = doc.ReadTransaction(); return output.To(transaction); } @@ -119,7 +85,7 @@ static void WriteMap(Map map, Utf8JsonWriter jsonWriter, Transaction transaction { jsonWriter.WriteStartArray(); - foreach (var property in map.Iterate(transaction) ?? throw new InvalidOperationException("Failed to iterate array.")) + foreach (var property in map.Iterate(transaction)) { WriteProperty(property.Key, property.Value, jsonWriter, transaction); } @@ -131,7 +97,7 @@ static void WriteArray(Array array, Utf8JsonWriter jsonWriter, Transaction trans { jsonWriter.WriteStartArray(); - foreach (var item in array.Iterate(transaction) ?? throw new InvalidOperationException("Failed to iterate array.")) + foreach (var item in array.Iterate(transaction)) { WriteValue(item, jsonWriter, transaction); } From be1ddd1be4555b8c7613239560277143e385f906 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 16 Oct 2023 17:21:00 +0200 Subject: [PATCH 110/186] make input factory public. --- Directory.Build.props | 2 +- YDotNet.Extensions/InputFactory.cs | 2 +- YDotNet.Extensions/YDotNetExtensions.cs | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index c0f82565..7af02951 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,7 +9,7 @@ https://github.com/LSViana/YDotNet true snupkg - 0.2.5 + 0.2.6 diff --git a/YDotNet.Extensions/InputFactory.cs b/YDotNet.Extensions/InputFactory.cs index e474dca5..cee58a00 100644 --- a/YDotNet.Extensions/InputFactory.cs +++ b/YDotNet.Extensions/InputFactory.cs @@ -3,7 +3,7 @@ namespace YDotNet.Extensions; -internal class InputFactory +public static class InputFactory { public static Input FromJson(string json) { diff --git a/YDotNet.Extensions/YDotNetExtensions.cs b/YDotNet.Extensions/YDotNetExtensions.cs index 6c90eb0e..08171654 100644 --- a/YDotNet.Extensions/YDotNetExtensions.cs +++ b/YDotNet.Extensions/YDotNetExtensions.cs @@ -1,4 +1,3 @@ -using System.Formats.Asn1; using System.Text; using System.Text.Json; using YDotNet.Document; From d7bbc0cfb1c4f549e0cb17a5c4bb0da3162f82b1 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 16 Oct 2023 20:24:23 +0200 Subject: [PATCH 111/186] Fix awareness. --- Demo/Client/src/App.css | 6 +++ Demo/Client/src/App.tsx | 10 +++++ Demo/appsettings.Development.json | 8 +--- Directory.Build.props | 2 +- .../EncoderExtensions.cs | 44 +++++++++++++------ YDotNet.Server.WebSockets/WebSocketEncoder.cs | 2 +- .../YDotNetActionResult.cs | 2 +- .../YDotNetSocketMiddleware.cs | 24 +++------- .../Clustering/ClusteringCallback.cs | 4 +- YDotNet.Server/Clustering/Messages.cs | 4 +- YDotNet.Server/ConnectedUser.cs | 2 +- YDotNet.Server/DefaultDocumentManager.cs | 10 ++--- YDotNet.Server/DocumentContext.cs | 2 +- YDotNet.Server/Events.cs | 2 +- YDotNet.Server/IDocumentManager.cs | 5 +-- YDotNet.Server/Internal/ConnectedUsers.cs | 20 ++++----- YDotNet/Infrastructure/ClientId.cs | 4 +- YDotNet/Protocol/Decoder.cs | 27 ++++++------ YDotNet/Protocol/Encoder.cs | 17 ++++--- 19 files changed, 105 insertions(+), 90 deletions(-) diff --git a/Demo/Client/src/App.css b/Demo/Client/src/App.css index cdae8706..85cc0d69 100644 --- a/Demo/Client/src/App.css +++ b/Demo/Client/src/App.css @@ -84,4 +84,10 @@ background: #efefef; padding: .5rem; margin-bottom: .5rem; +} + +textarea.form-control { + font-family: monospace; + font-size: 80%; + height: 200px !important; } \ No newline at end of file diff --git a/Demo/Client/src/App.tsx b/Demo/Client/src/App.tsx index 9cf74087..e82410ec 100644 --- a/Demo/Client/src/App.tsx +++ b/Demo/Client/src/App.tsx @@ -5,11 +5,19 @@ import { Increment } from './components/Increment'; import './App.css'; import { YjsContextProvider } from './context/yjsContext'; import { Chat } from './components/Chat'; +import { Awareness } from './components/Awareness'; function App() { return ( <> + + +

Awareness

+ + + +

Monaco Editor

@@ -47,6 +55,8 @@ function App() { + +
diff --git a/Demo/appsettings.Development.json b/Demo/appsettings.Development.json index bd2a9fdc..b8360804 100644 --- a/Demo/appsettings.Development.json +++ b/Demo/appsettings.Development.json @@ -8,7 +8,7 @@ }, "AllowedHosts": "*", "Storage": { - "Type": "Redis", + "Type": "None", "MongoDB": { "ConnectionString": "mongodb://localhost:27017", "Database": "YDotNet" @@ -16,11 +16,5 @@ "Redis": { "ConnectionString": "localhost:6379" } - }, - "Clustering": { - "Type": "Redis", - "Redis": { - "ConnectionString": "localhost:6379" - } } } diff --git a/Directory.Build.props b/Directory.Build.props index 7af02951..a03a2192 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,7 +9,7 @@ https://github.com/LSViana/YDotNet true snupkg - 0.2.6 + 0.2.7
diff --git a/YDotNet.Server.WebSockets/EncoderExtensions.cs b/YDotNet.Server.WebSockets/EncoderExtensions.cs index 457c2bfd..881dc6f1 100644 --- a/YDotNet.Server.WebSockets/EncoderExtensions.cs +++ b/YDotNet.Server.WebSockets/EncoderExtensions.cs @@ -1,3 +1,5 @@ +using YDotNet.Protocol; + namespace YDotNet.Server.WebSockets; public static class EncoderExtensions @@ -8,7 +10,7 @@ public static async Task WriteSyncStep1Async(this WebSocketEncoder encoder, byte await encoder.WriteVarUintAsync(MessageTypes.TypeSync, ct); await encoder.WriteVarUintAsync(MessageTypes.SyncStep1, ct); await encoder.WriteVarUint8Array(stateVector, ct); - await encoder.EndMessageAsync(ct); + await encoder.FlushAsync(ct); } public static async Task WriteSyncStep2Async(this WebSocketEncoder encoder, byte[] update, @@ -17,7 +19,7 @@ public static async Task WriteSyncStep2Async(this WebSocketEncoder encoder, byte await encoder.WriteVarUintAsync(MessageTypes.TypeSync, ct); await encoder.WriteVarUintAsync(MessageTypes.SyncStep2, ct); await encoder.WriteVarUint8Array(update, ct); - await encoder.EndMessageAsync(ct); + await encoder.FlushAsync(ct); } public static async Task WriteSyncUpdateAsync(this WebSocketEncoder encoder, byte[] update, @@ -26,16 +28,7 @@ public static async Task WriteSyncUpdateAsync(this WebSocketEncoder encoder, byt await encoder.WriteVarUintAsync(MessageTypes.TypeSync, ct); await encoder.WriteVarUintAsync(MessageTypes.SyncUpdate, ct); await encoder.WriteVarUint8Array(update, ct); - await encoder.EndMessageAsync(ct); - } - - public static async Task WriteAwarenessAsync(this WebSocketEncoder encoder, long clientId, long clock, string? state, - CancellationToken ct) - { - await encoder.WriteVarUintAsync((int)clientId, ct); - await encoder.WriteVarUintAsync((int)clock, ct); - await encoder.WriteVarStringAsync(state ?? string.Empty, ct); - await encoder.EndMessageAsync(ct); + await encoder.FlushAsync(ct); } public static async Task WriteAuthErrorAsync(this WebSocketEncoder encoder, string reason, @@ -44,6 +37,31 @@ public static async Task WriteAuthErrorAsync(this WebSocketEncoder encoder, stri await encoder.WriteVarUintAsync(MessageTypes.TypeAuth, ct); await encoder.WriteVarUintAsync(0, ct); await encoder.WriteVarStringAsync(reason, ct); - await encoder.EndMessageAsync(ct); + await encoder.FlushAsync(ct); + } + + public static async Task WriteAwarenessAsync(this WebSocketEncoder encoder, (ulong ClientId, ulong Clock, string? State)[] clients, + CancellationToken ct) + { + if (clients.Length == 0) + { + return; + } + + await encoder.WriteVarUintAsync(MessageTypes.TypeAwareness, ct); + + var buffer = new BufferEncoder(); + + await buffer.WriteVarUintAsync((ulong)clients.Length, ct); + + foreach (var (clientId, clock, state) in clients) + { + await buffer.WriteVarUintAsync(clientId, ct); + await buffer.WriteVarUintAsync(clock, ct); + await buffer.WriteVarStringAsync(state ?? string.Empty, ct); + } + + await encoder.WriteVarUint8Array(buffer.ToArray(), ct); + await encoder.FlushAsync(ct); } } diff --git a/YDotNet.Server.WebSockets/WebSocketEncoder.cs b/YDotNet.Server.WebSockets/WebSocketEncoder.cs index 215a145d..c94abbac 100644 --- a/YDotNet.Server.WebSockets/WebSocketEncoder.cs +++ b/YDotNet.Server.WebSockets/WebSocketEncoder.cs @@ -31,7 +31,7 @@ protected override ValueTask WriteBytesAsync(ArraySegment bytes, return new ValueTask(webSocket.SendAsync(bytes, WebSocketMessageType.Binary, false, ct)); } - public ValueTask EndMessageAsync( + public ValueTask FlushAsync( CancellationToken ct = default) { return new ValueTask(webSocket.SendAsync(Array.Empty(), WebSocketMessageType.Binary, true, ct)); diff --git a/YDotNet.Server.WebSockets/YDotNetActionResult.cs b/YDotNet.Server.WebSockets/YDotNetActionResult.cs index 07767194..5b43eb78 100644 --- a/YDotNet.Server.WebSockets/YDotNetActionResult.cs +++ b/YDotNet.Server.WebSockets/YDotNetActionResult.cs @@ -16,6 +16,6 @@ public async Task ExecuteResultAsync(ActionContext context) { var middleware = context.HttpContext.RequestServices.GetRequiredService(); - await middleware.InvokeAsync(context.HttpContext, this.documentName); + await middleware.InvokeAsync(context.HttpContext, documentName); } } diff --git a/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs b/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs index 13f83286..4e4ad7f7 100644 --- a/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs +++ b/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Options; using System.Collections.Concurrent; using System.Net.WebSockets; +using YDotNet.Protocol; namespace YDotNet.Server.WebSockets; @@ -33,11 +34,9 @@ public ValueTask OnAwarenessUpdatedAsync(ClientAwarenessEvent @event) foreach (var state in documentStates) { - await state.WriteLockedAsync(@event, async (encoder, @event, _, ct) => + await state.WriteLockedAsync(@event, async (encoder, e, _, ct) => { - await encoder.WriteVarUintAsync(MessageTypes.TypeAwareness, ct); - await encoder.WriteVarUintAsync(1, ct); - await encoder.WriteAwarenessAsync(@event.Context.ClientId, @event.ClientClock, @event.ClientState, ct); + await encoder.WriteAwarenessAsync(new[] { (e.Context.ClientId, e.ClientClock, (string?)e.ClientState) }, ct); }, default); } }); @@ -230,18 +229,7 @@ private async Task SendAwarenessAsync(WebSocketEncoder encoder, ClientState stat { var users = await documentManager!.GetAwarenessAsync(state.DocumentContext, ct); - if (users.Count == 0) - { - return; - } - - await encoder.WriteVarUintAsync(MessageTypes.TypeAwareness, ct); - await encoder.WriteVarUintAsync(users.Count, ct); - - foreach (var (clientId, user) in users) - { - await encoder.WriteAwarenessAsync(clientId, user.ClientClock, user.ClientState, ct); - } + await encoder.WriteAwarenessAsync(users.Select(x => (x.Key, x.Value.ClientClock, x.Value.ClientState)).ToArray(), ct); } private async Task HandleAwarenessAsync(ClientState state, @@ -252,7 +240,7 @@ private async Task HandleAwarenessAsync(ClientState state, var clientCount = await state.Decoder.ReadVarUintAsync(ct); - for (var i = 0; i < clientCount; i++) + for (var i = 0ul; i < clientCount; i++) { var clientId = await state.Decoder.ReadVarUintAsync(ct); var clientClock = await state.Decoder.ReadVarUintAsync(ct); @@ -273,7 +261,7 @@ private async Task HandleAwarenessAsync(ClientState state, } } - private List GetOtherClients(string documentName, long clientId) + private List GetOtherClients(string documentName, ulong clientId) { var documentStates = statesPerDocumentName.GetOrAdd(documentName, _ => new List()); diff --git a/YDotNet.Server/Clustering/ClusteringCallback.cs b/YDotNet.Server/Clustering/ClusteringCallback.cs index 52ac24c5..5a00ac8a 100644 --- a/YDotNet.Server/Clustering/ClusteringCallback.cs +++ b/YDotNet.Server/Clustering/ClusteringCallback.cs @@ -19,7 +19,7 @@ public ClusteringCallback(IOptions clusteringOptions, IPubSub this.clusteringOptions = clusteringOptions.Value; this.publisher = publisher; - this.publishQueue = new PublishQueue( + publishQueue = new PublishQueue( this.clusteringOptions.MaxBatchCount, this.clusteringOptions.MaxBatchSize, (int)this.clusteringOptions.DebounceTime.TotalMilliseconds, @@ -127,7 +127,7 @@ public ValueTask OnDocumentChangedAsync(DocumentChangedEvent @event) return EnqueueAsync(m, @event.Context); } - private ValueTask SendAwarenessAsync(DocumentContext context, string? state, long clock) + private ValueTask SendAwarenessAsync(DocumentContext context, string? state, ulong clock) { var m = new Message { Type = MessageType.ClientPinged, ClientState = state, ClientClock = clock }; diff --git a/YDotNet.Server/Clustering/Messages.cs b/YDotNet.Server/Clustering/Messages.cs index 59f3d709..53bc813c 100644 --- a/YDotNet.Server/Clustering/Messages.cs +++ b/YDotNet.Server/Clustering/Messages.cs @@ -31,10 +31,10 @@ public sealed class Message : ICanEstimateSize public string DocumentName { get; set; } [ProtoMember(3)] - public long ClientId { get; set; } + public ulong ClientId { get; set; } [ProtoMember(4)] - public long ClientClock { get; set; } + public ulong ClientClock { get; set; } [ProtoMember(5)] public string? ClientState { get; set; } diff --git a/YDotNet.Server/ConnectedUser.cs b/YDotNet.Server/ConnectedUser.cs index 218c5d14..015ddd40 100644 --- a/YDotNet.Server/ConnectedUser.cs +++ b/YDotNet.Server/ConnectedUser.cs @@ -4,7 +4,7 @@ public sealed class ConnectedUser { public string? ClientState { get; set; } - public long ClientClock { get; set; } + public ulong ClientClock { get; set; } public DateTime LastActivity { get; set; } } diff --git a/YDotNet.Server/DefaultDocumentManager.cs b/YDotNet.Server/DefaultDocumentManager.cs index 4add8804..5f3c4c00 100644 --- a/YDotNet.Server/DefaultDocumentManager.cs +++ b/YDotNet.Server/DefaultDocumentManager.cs @@ -23,9 +23,9 @@ public DefaultDocumentManager( ILogger logger) { this.options = options.Value; - this.callback = new CallbackInvoker(callbacks, logger); + callback = new CallbackInvoker(callbacks, logger); - cache = new DocumentCache(documentStorage, this.callback, this, options.Value); + cache = new DocumentCache(documentStorage, callback, this, options.Value); } public async Task StartAsync( @@ -128,7 +128,7 @@ await callback.OnDocumentChangedAsync(new DocumentChangedEvent } } - public async ValueTask PingAsync(DocumentContext context, long clock, string? state = null, + public async ValueTask PingAsync(DocumentContext context, ulong clock, string? state = null, CancellationToken ct = default) { if (users.AddOrUpdate(context.DocumentName, context.ClientId, clock, state, out var newState)) @@ -171,9 +171,9 @@ await callback.OnClientDisconnectedAsync(new ClientDisconnectedEvent cache.RemoveEvictedItems(); } - public ValueTask> GetAwarenessAsync(DocumentContext context, + public ValueTask> GetAwarenessAsync(DocumentContext context, CancellationToken ct = default) { - return new ValueTask>(users.GetUsers(context.DocumentName)); + return new ValueTask>(users.GetUsers(context.DocumentName)); } } diff --git a/YDotNet.Server/DocumentContext.cs b/YDotNet.Server/DocumentContext.cs index ec3bfe47..abecf7fa 100644 --- a/YDotNet.Server/DocumentContext.cs +++ b/YDotNet.Server/DocumentContext.cs @@ -1,6 +1,6 @@ namespace YDotNet.Server; -public sealed record DocumentContext(string DocumentName, long ClientId) +public sealed record DocumentContext(string DocumentName, ulong ClientId) { public object? Metadata { get; set; } } diff --git a/YDotNet.Server/Events.cs b/YDotNet.Server/Events.cs index 676f6d38..61bfdfb2 100644 --- a/YDotNet.Server/Events.cs +++ b/YDotNet.Server/Events.cs @@ -32,5 +32,5 @@ public sealed class ClientAwarenessEvent : DocumentEvent { required public string? ClientState { get; set; } - required public long ClientClock { get; set; } + required public ulong ClientClock { get; set; } } diff --git a/YDotNet.Server/IDocumentManager.cs b/YDotNet.Server/IDocumentManager.cs index acad21b7..40bfb9ce 100644 --- a/YDotNet.Server/IDocumentManager.cs +++ b/YDotNet.Server/IDocumentManager.cs @@ -1,12 +1,11 @@ using Microsoft.Extensions.Hosting; using YDotNet.Document; -using YDotNet.Document.Transactions; namespace YDotNet.Server; public interface IDocumentManager : IHostedService { - ValueTask PingAsync(DocumentContext context, long clock, string? state = null, + ValueTask PingAsync(DocumentContext context, ulong clock, string? state = null, CancellationToken ct = default); ValueTask DisconnectAsync(DocumentContext context, @@ -18,7 +17,7 @@ ValueTask GetUpdateAsync(DocumentContext context, byte[] stateVector, ValueTask GetStateVectorAsync(DocumentContext context, CancellationToken ct = default); - ValueTask> GetAwarenessAsync(DocumentContext context, + ValueTask> GetAwarenessAsync(DocumentContext context, CancellationToken ct = default); ValueTask ApplyUpdateAsync(DocumentContext context, byte[] stateDiff, diff --git a/YDotNet.Server/Internal/ConnectedUsers.cs b/YDotNet.Server/Internal/ConnectedUsers.cs index 4d1e17b6..370c8015 100644 --- a/YDotNet.Server/Internal/ConnectedUsers.cs +++ b/YDotNet.Server/Internal/ConnectedUsers.cs @@ -4,23 +4,23 @@ namespace YDotNet.Server.Internal; public sealed class ConnectedUsers { - private readonly ConcurrentDictionary> users = new(); + private readonly ConcurrentDictionary> users = new(); public Func Clock = () => DateTime.UtcNow; - public IReadOnlyDictionary GetUsers(string documentName) + public IReadOnlyDictionary GetUsers(string documentName) { - var documentUsers = users.GetOrAdd(documentName, _ => new Dictionary()); + var documentUsers = users.GetOrAdd(documentName, _ => new Dictionary()); lock (documentUsers) { - return new Dictionary(documentUsers); + return new Dictionary(documentUsers); } } - public bool AddOrUpdate(string documentName, long clientId, long clock, string? state, out string? existingState) + public bool AddOrUpdate(string documentName, ulong clientId, ulong clock, string? state, out string? existingState) { - var users = this.users.GetOrAdd(documentName, _ => new Dictionary()); + var users = this.users.GetOrAdd(documentName, _ => new Dictionary()); // We expect to have relatively few users per document, therefore we use normal lock here. lock (users) @@ -56,7 +56,7 @@ public bool AddOrUpdate(string documentName, long clientId, long clock, string? } } - public bool Remove(string documentName, long clientId) + public bool Remove(string documentName, ulong clientId) { if (!users.TryGetValue(documentName, out var documentUsers)) { @@ -69,7 +69,7 @@ public bool Remove(string documentName, long clientId) } } - public IEnumerable<(long ClientId, string DocumentName)> Cleanup(TimeSpan maxAge) + public IEnumerable<(ulong ClientId, string DocumentName)> Cleanup(TimeSpan maxAge) { var olderThan = Clock() - maxAge; @@ -79,13 +79,13 @@ public bool Remove(string documentName, long clientId) lock (users) { // Usually there should be nothing to remove, therefore we save a few allocations for the fast path. - List? usersToRemove = null; + List? usersToRemove = null; foreach (var (clientId, user) in users) { if (user.LastActivity < olderThan) { - usersToRemove ??= new List(); + usersToRemove ??= new List(); usersToRemove.Add(clientId); yield return (clientId, documentName); diff --git a/YDotNet/Infrastructure/ClientId.cs b/YDotNet/Infrastructure/ClientId.cs index ba563004..ba377eb5 100644 --- a/YDotNet/Infrastructure/ClientId.cs +++ b/YDotNet/Infrastructure/ClientId.cs @@ -1,4 +1,4 @@ -namespace YDotNet.Infrastructure; +namespace YDotNet.Infrastructure; /// /// Helper class to deal with client ids. @@ -8,7 +8,7 @@ public sealed class ClientId /// /// The maximum safe integer from javascript. /// - public const long MaxSafeInteger = 2 ^ 53 - 1; + public const ulong MaxSafeInteger = 2 ^ 53 - 1; /// /// Gets a random client id. diff --git a/YDotNet/Protocol/Decoder.cs b/YDotNet/Protocol/Decoder.cs index c4e27d18..70c3bea8 100644 --- a/YDotNet/Protocol/Decoder.cs +++ b/YDotNet/Protocol/Decoder.cs @@ -18,23 +18,24 @@ public abstract class Decoder /// The decoded integer. /// /// The input is in an invalid format. - public async ValueTask ReadVarUintAsync( + public async ValueTask ReadVarUintAsync( CancellationToken ct = default) { - int value = 0, shift = 0; + var resultSum = 0ul; + var resultShift = 0; while (true) { - var lower7bits = await this.ReadByteAsync(ct); + var lower7bits = (ulong)await ReadByteAsync(ct); - value |= (lower7bits & 0x7f) << shift; + resultSum |= (lower7bits & 0x7f) << resultShift; if ((lower7bits & 128) == 0) { - return value; + return resultSum; } - shift += 7; + resultShift += 7; } throw new IndexOutOfRangeException(); @@ -50,10 +51,10 @@ public async ValueTask ReadVarUintAsync( public async ValueTask ReadVarUint8ArrayAsync( CancellationToken ct = default) { - var arrayLength = await this.ReadVarUintAsync(ct); + var arrayLength = await ReadVarUintAsync(ct); var arrayBuffer = new byte[arrayLength]; - await this.ReadBytesAsync(arrayBuffer, ct); + await ReadBytesAsync(arrayBuffer, ct); return arrayBuffer; } @@ -66,10 +67,10 @@ public async ValueTask ReadVarUint8ArrayAsync( /// The decoded string. /// public async ValueTask ReadVarStringAsync( - CancellationToken ct) + CancellationToken ct = default) { - var length = (int)await this.ReadVarUintAsync(ct); - if (length > this.stringBuffer.Length) + var length = (int)await ReadVarUintAsync(ct); + if (length > stringBuffer.Length) { var buffer = ArrayPool.Shared.Rent(length); try @@ -83,13 +84,13 @@ public async ValueTask ReadVarStringAsync( } else { - return await ReadCoreAsync(length, this.stringBuffer, ct); + return await ReadCoreAsync(length, stringBuffer, ct); } async ValueTask ReadCoreAsync(int length, byte[] buffer, CancellationToken ct) { var slicedBuffer = buffer.AsMemory(0, length); - await this.ReadBytesAsync(slicedBuffer, ct); + await ReadBytesAsync(slicedBuffer, ct); return Encoding.UTF8.GetString(slicedBuffer.Span); } diff --git a/YDotNet/Protocol/Encoder.cs b/YDotNet/Protocol/Encoder.cs index de7b5032..ae7f81a4 100644 --- a/YDotNet/Protocol/Encoder.cs +++ b/YDotNet/Protocol/Encoder.cs @@ -1,8 +1,6 @@ using System.Buffers; using System.Text; -#pragma warning disable SA1116 // Split parameters should start on line after declaration - namespace YDotNet.Protocol; /// @@ -20,7 +18,7 @@ public abstract class Encoder /// /// The task representing the async operation. /// - public async ValueTask WriteVarUintAsync(long value, + public async ValueTask WriteVarUintAsync(ulong value, CancellationToken ct = default) { do @@ -34,7 +32,7 @@ public async ValueTask WriteVarUintAsync(long value, lower7bits |= 128; } - await this.WriteByteAsync(lower7bits, ct); + await WriteByteAsync(lower7bits, ct); } while (value > 0); } @@ -56,8 +54,8 @@ public async ValueTask WriteVarUint8Array(byte[] value, throw new ArgumentNullException(nameof(value)); } - await this.WriteVarUintAsync(value.Length, ct); - await this.WriteBytesAsync(value, ct); + await WriteVarUintAsync((ulong)value.Length, ct); + await WriteBytesAsync(value, ct); } /// @@ -78,7 +76,7 @@ public async ValueTask WriteVarStringAsync(string value, } var length = Encoding.UTF8.GetByteCount(value); - if (length > this.stringBuffer.Length) + if (length > stringBuffer.Length) { var buffer = ArrayPool.Shared.Rent(length); try @@ -92,14 +90,15 @@ public async ValueTask WriteVarStringAsync(string value, } else { - await WriteCoreAsync(value, this.stringBuffer, ct); + await WriteCoreAsync(value, stringBuffer, ct); } async Task WriteCoreAsync(string value, byte[] buffer, CancellationToken ct) { var length = Encoding.UTF8.GetBytes(value, buffer); - await this.WriteBytesAsync(new ArraySegment(buffer, 0, length), ct); + await WriteVarUintAsync((ulong)length, ct); + await WriteBytesAsync(new ArraySegment(buffer, 0, length), ct); } } From 31803818ba67e5f52d3660d1b1e69ec143485c1a Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 16 Oct 2023 21:13:59 +0200 Subject: [PATCH 112/186] Fix client ID. --- Demo/Client/src/components/Awareness.tsx | 34 +++ .../Protocol/WriteAndRead.cs | 201 ++++++++++++++++++ YDotNet/Infrastructure/ClientId.cs | 2 +- YDotNet/Protocol/BufferEncoder.cs | 34 +++ 4 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 Demo/Client/src/components/Awareness.tsx create mode 100644 Tests/YDotNet.Tests.Unit/Protocol/WriteAndRead.cs create mode 100644 YDotNet/Protocol/BufferEncoder.cs diff --git a/Demo/Client/src/components/Awareness.tsx b/Demo/Client/src/components/Awareness.tsx new file mode 100644 index 00000000..dca5edb8 --- /dev/null +++ b/Demo/Client/src/components/Awareness.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { Input } from 'reactstrap'; +import { useYjs } from '../hooks/useYjs'; + +export const Awareness = () => { + const awareness = useYjs().yjsConnector.awareness; + const [state, setState] = React.useState({}); + + React.useEffect(() => { + const updateUsers = () => { + const allStates: Record = {}; + + awareness.getStates().forEach((value, key) => { + allStates[key.toString()] = value; + }); + + setState(allStates); + }; + + updateUsers(); + awareness.on('change', updateUsers); + awareness.setLocalStateField('user', { random: Math.random(), message: 'Hello' }); + + console.log(`Current CLIENT ID: ${awareness.clientID}`); + + return () => { + awareness.off('change', updateUsers); + }; + }, [awareness]); + + return ( + + ); +}; \ No newline at end of file diff --git a/Tests/YDotNet.Tests.Unit/Protocol/WriteAndRead.cs b/Tests/YDotNet.Tests.Unit/Protocol/WriteAndRead.cs new file mode 100644 index 00000000..8f69a6cd --- /dev/null +++ b/Tests/YDotNet.Tests.Unit/Protocol/WriteAndRead.cs @@ -0,0 +1,201 @@ +using NUnit.Framework; +using YDotNet.Infrastructure; +using YDotNet.Protocol; + +namespace YDotNet.Tests.Unit.Protocol; + +public class WriteAndRead +{ + [Test] + [TestCase(0ul)] + [TestCase(1ul)] + [TestCase(3826503548ul)] + [TestCase(ClientId.MaxSafeInteger)] + public async Task EncodeAndDecodeInt(ulong input) + { + // Arrange + var encoder = new BufferEncoder(); + + // Act + await encoder.WriteVarUintAsync(input); + await encoder.WriteVarUintAsync(1000ul); // They should not read pass through. + + var decoder = new BufferDecoder(encoder.ToArray()); + var read = await decoder.ReadVarUintAsync(); + + // Assert + + Assert.That(read, Is.EqualTo(input)); + } + + [Test] + [TestCase("")] + [TestCase("Hello YDotNet")] + public async Task EncodeAndDecodeString(string input) + { + // Arrange + var encoder = new BufferEncoder(); + + // Act + await encoder.WriteVarStringAsync(input); + await encoder.WriteVarUintAsync(1000ul); // They should not read pass through. + + var decoder = new BufferDecoder(encoder.ToArray()); + var read = await decoder.ReadVarStringAsync(); + + // Assert + + Assert.That(read, Is.EqualTo(input)); + } + + [Test] + [TestCase(0)] + [TestCase(100)] + [TestCase(1000)] + public async Task EncodeAndDecodeBytes(int length) + { + var input = Bytes(length); + + // Arrange + var encoder = new BufferEncoder(); + + // Act + await encoder.WriteVarUint8Array(input); + await encoder.WriteVarUintAsync(1000ul); // They should not read pass through. + + var decoder = new BufferDecoder(encoder.ToArray()); + var read = await decoder.ReadVarUint8ArrayAsync(); + + // Assert + Assert.That(read, Is.EqualTo(input)); + } + + [Test] + public async Task DecodeJsSample() + { + // Arrange + var buffer = new byte[] + { + 1, + 252, + 158, + 207, + 160, + 14, + 4, + 56, + 123, + 34, + 117, + 115, + 101, + 114, + 34, + 58, + 123, + 34, + 114, + 97, + 110, + 100, + 111, + 109, + 34, + 58, + 48, + 46, + 55, + 49, + 55, + 57, + 54, + 54, + 50, + 55, + 50, + 52, + 57, + 55, + 55, + 54, + 57, + 54, + 44, + 34, + 109, + 101, + 115, + 115, + 97, + 103, + 101, + 34, + 58, + 34, + 72, + 101, + 108, + 108, + 111, + 34, + 125, + 125 + }; + + var decoder = new BufferDecoder(buffer); + + // Act + var clientCount = await decoder.ReadVarUintAsync(); + + var client1Id = await decoder.ReadVarUintAsync(); + var client1Clock = await decoder.ReadVarUintAsync(); + var client1State = await decoder.ReadVarStringAsync(); + + // Assert + Assert.That(clientCount, Is.EqualTo(1)); + Assert.That(client1Id, Is.EqualTo(3826503548)); + Assert.That(client1Clock, Is.EqualTo(4)); + Assert.That(client1State, Is.EqualTo("{\"user\":{\"random\":0.7179662724977696,\"message\":\"Hello\"}}")); + } + + private byte[] Bytes(int length) + { + var result = new byte[length]; + + for (var i = 0; i < length; i++) + { + result[i] = (byte)(i % 255); + } + + return result; + } + + class BufferDecoder : Decoder + { + private readonly Stream stream; + + public BufferDecoder(byte[] buffer) + { + stream = new MemoryStream(buffer); + } + + protected override ValueTask ReadByteAsync( + CancellationToken ct) + { + var result = stream.ReadByte(); + + if (result < 0) + { + throw new InvalidOperationException("End of stream reached."); + } + + return new ValueTask((byte)result); + } + + protected override async ValueTask ReadBytesAsync(Memory bytes, + CancellationToken ct) + { + await stream.ReadAsync(bytes, ct); + } + } +} diff --git a/YDotNet/Infrastructure/ClientId.cs b/YDotNet/Infrastructure/ClientId.cs index ba377eb5..e2bdd8ec 100644 --- a/YDotNet/Infrastructure/ClientId.cs +++ b/YDotNet/Infrastructure/ClientId.cs @@ -16,7 +16,7 @@ public sealed class ClientId /// The random client id. public static ulong GetRandom() { - var value = (ulong)Random.Shared.Next() & MaxSafeInteger; + var value = ((ulong)Random.Shared.NextInt64() + 1000) & MaxSafeInteger; return value; } diff --git a/YDotNet/Protocol/BufferEncoder.cs b/YDotNet/Protocol/BufferEncoder.cs new file mode 100644 index 00000000..54577e98 --- /dev/null +++ b/YDotNet/Protocol/BufferEncoder.cs @@ -0,0 +1,34 @@ +namespace YDotNet.Protocol; + +/// +/// Write to a buffer. +/// +public sealed class BufferEncoder : Encoder +{ + private readonly MemoryStream buffer = new(); + + /// + /// Gets the content of the buffer. + /// + /// The content of the buffer. + public byte[] ToArray() + { + return buffer.ToArray(); + } + + /// + protected override ValueTask WriteByteAsync(byte value, + CancellationToken ct) + { + buffer.WriteByte(value); + return default; + } + + /// + protected override ValueTask WriteBytesAsync(ArraySegment bytes, + CancellationToken ct) + { + buffer.Write(bytes); + return default; + } +} From e02978db83753cb7eaf14b6c3822a04a7f9b1524 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Mon, 16 Oct 2023 20:13:38 -0300 Subject: [PATCH 113/186] docs(improvements): rename decision.md --- docs/{decision.md => decisions.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/{decision.md => decisions.md} (100%) diff --git a/docs/decision.md b/docs/decisions.md similarity index 100% rename from docs/decision.md rename to docs/decisions.md From 69172e33dbe670a870b8aacc69169e479587a3e9 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Mon, 16 Oct 2023 20:22:41 -0300 Subject: [PATCH 114/186] docs(improvements): improve the decision log about documentation --- docs/decisions.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/decisions.md b/docs/decisions.md index 2bbf1078..873c2439 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -1,8 +1,12 @@ # Decision -## 2023-10-16 Do not document internal classes via XML ocmments +## 2023-10-16 Publicly exposed elements must be documented -They are tedious and annoying to write and have to be updated regularly. Thefore it does not make sense to add any XML comments for XML types. We should document code and the reason why something has been implemented in a certain way. +The documentation for `class` and `struct` elements is important to instruct external users on how to use the library. +These elements are normally under the `YDotNet.Document` namespace. + +The `internal` or `private` classes, however, must not be documented. It's allowed to add comments explaining why such +code works the way it does, though. Bigger decisions must be documented in this document. ## 2023-10-16 No circular dependencies between native and types From 86232ebf6548592316d5be83463d4487b6a11c54 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Mon, 16 Oct 2023 22:32:59 -0300 Subject: [PATCH 115/186] docs(improvements): improve the formatting and items in the decision logs --- docs/decisions.md | 94 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 66 insertions(+), 28 deletions(-) diff --git a/docs/decisions.md b/docs/decisions.md index 873c2439..4bd23dff 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -10,48 +10,85 @@ code works the way it does, though. Bigger decisions must be documented in this ## 2023-10-16 No circular dependencies between native and types -There is the natural dependency between document types to native, because they read data, but there is also a dependency in the other direction because some native types can actually generate stuff with `ToXXX` methods. This is a circular dependency between namespaces that should in general be avoided. +There is the natural dependency between document types to native, because they read data, but there is also a dependency +in the other direction because some native types can actually generate stuff with `ToXXX` methods. +This is a circular dependency between namespaces that should in general be avoided. -Furthermore some types do not rely on the native types and calculate pointers manually, e.g. https://github.com/LSViana/ydotnet/blob/main/YDotNet/Document/Types/Events/EventDeltaAttribute.cs +Furthermore some types do not rely on the native types and calculate pointers manually, +e.g. [`EventDeltaAttribute`](https://github.com/LSViana/ydotnet/blob/main/YDotNet/Document/Types/Events/EventDeltaAttribute.cs). -Therefore I ensured that... +Therefore, you must ensure that: 1. There is always a native type. + 2. Native types are passed to the CLR types via constructors. -3. Native types are responsible to follow pointers, e.g. https://github.com/SebastianStehle/ydotnet/blob/main/YDotNet/Native/UndoManager/Events/UndoEventNative.cs#L18C5-L27C1 and everything around reading memory is part of the native part. These reading operations are provided via methods to indicate that it is not a property and not part of the struct itself. -Some objects also need the handle and the native struct. Therefore a new type has been introduced: +3. Native types are responsible to follow pointers, +e.g. [`UndoEventNative`](https://github.com/SebastianStehle/ydotnet/blob/main/YDotNet/Native/UndoManager/Events/UndoEventNative.cs#L18C5-L27C1) +and everything around reading memory is part of the native part. These reading operations are provided via methods to +indicate that it is not a property and not part of the struct itself. + +Some objects also need the handle and the native `struct`. Therefore a new type has been introduced: ```csharp internal record struct NativeWithHandle(T Value, nint Handle) where T : struct; ``` -https://github.com/SebastianStehle/ydotnet/blob/main/YDotNet/Native/NativeWithHandle.cs +[View the source code](https://github.com/SebastianStehle/ydotnet/blob/main/YDotNet/Native/NativeWithHandle.cs). + +## 2023-10-16 Objects must check for disposal before every operation + +When we access a pointer that is not pointing to a valid `struct` anymore, the process can crash. +This is very hard to catch and debug (because logging does not work) and therefore needs to be avoided at all costs. + +If an object has been disposed, it must throw an `ObjectDisposedException`. +This task is still work in progress and a lot of changes have been done to make this work. + +1. The `Doc` has a [`TypeCache`](https://github.com/SebastianStehle/ydotnet/blob/main/YDotNet/Infrastructure/TypeCache.cs), +which is basically a map from handle to type. This type cache ensures that there is only one object for each pointer. +The `TypeCache` uses a `WeakMap` to ensure that unused objects can be garbage collected. + +2. The `TypeCache` gives the option to mark object as disposed, for example, +when an Yrs event indicates an object has been removed. + - This is still work in progress because Yrs does not provide all necessary information yet. -## 2023-10-16 The library should not provide access to invalid objects +3. Because of the `TypeCache` each object needs a reference to the document +to be able to construct types using the document. -When we access a pointer that is not pointing to a valid struct anymore, the process can crash. This is very hard to catch and debug (because logging does not work) and therefore needs to be avoided at all costs. +4. Some objects are also deleted from beginning. If you get an old value from an event that points to a removed `Map`, +this `Map` is not usable anymore and should be marked as disposed. This is similar for removed sub-documents. -If an object has been deleted it shoudl throw an `ObjectDisposedException`. This task is still work in progress but a lot of changes have been done to make this work. +## 2023-10-16 Objects must be disposed only once -1. The document has type cache, which is basically a map from handle to type: https://github.com/SebastianStehle/ydotnet/blob/main/YDotNet/Infrastructure/TypeCache.cs. This type cache ensures that there is only one object for each pointer. The type cache uses a weak map to ensure that unused objects can be garbage collected. -2. The type cache gives the option to mark object as deleted, for example when we receive an event from yrs that an object has been removed. This is still work in progress, because yrs does not provide all relevant information yet. -3. Because of the type cache each object needs a reference to the document to be able to construct types using the document. -4. Furthermore some objects are also deleted from beginning. If you get an old value from an event that points to a removed map, this map is not usable anymore and should be marked as deleted. Similar for removed subdocuments. +`Transaction`s and every type that can be disposed must not be disposed twice. +This can cause an application crash when: -## 2023-10-16 Objects must not be disposed twice +1. A dangling pointer is used after the first dispose. +2. A second attempt to dispose is executed (which usually causes the exit code 139). -Transactiosn and every type that can be disposed must not be disposed twice. This can cause an application crash when a danging pointer is used after the first dispose. New base classes have been introduced to ensure that this does not happen. +New base classes have been introduced to ensure that this does not happen. ## 2023-10-16 Not all transaction cases are catched by yrs -When you create a new root object while a transaction is still pending the application crashs. Therefore we also implement a mechanism in .NET to track the amount of pending transactions. Tests have been added for this case. +The application crashes if a new root object is created while a `Transaction` is open. -## 2023-10-16 The types should not be responsible to release memory properly +Therefore, the `Doc` and `Transaction` classes have a mechanism to track the amount of open `Transaction`s. +Tests have been added for this case. -When you call a native method in a type it is necessary to allocate unmanaged memory to pass a pointer to string or buffer. Before the change the methods are responsible to release the memory and take care about that in every single place. +## 2023-10-16 Memory must be managed by `DisposableHandle` -To improve that the memory writer returns a new type now: +Before the addition of `DisposableHandle`, the classes that needed to allocate memory to perform native calls would +do something like this: + +```csharp +var handle = MemoryWriter.WriteUtf8String(value); + +// Perform native call + +MemoryWriter.Release(value); +``` + +To improve that, the `MemoryWriter` class now returns instances of `DisposableHandle`: ```csharp internal sealed record DisposableHandle(nint Handle) : IDisposable @@ -66,7 +103,7 @@ internal sealed record DisposableHandle(nint Handle) : IDisposable } ``` -When using the dispose pattern properly the type methods do not have to handle this anymore. For example: +This means the memory is now released automatically by `DisposableHandle` when it's disposed. For example: ```csharp public void Insert(Transaction transaction, string key, Input input) @@ -80,16 +117,17 @@ public void Insert(Transaction transaction, string key, Input input) } ``` -This change also made a simplification of the input class possible and eliminates the inherited type of inputs. - -## 2023-10-16 MapEntry is useless +## 2023-10-16 `MapEntry` was replaced by `KeyValuePair` -This type is from yrs to transport key and value. In .NET we are used to leverage the `KeyValuePair` struct from that. Therefore we got rid of that, to also build the Map structures more similar to .NET. +This type is used by Yrs to transport key-value pairs. In .NET, there is the `KeyValuePair` struct for that purpose. +Therefore, we got rid of that to also build the `Map` structures more similar to the .NET built-in ones. -Whenevery we deal with key-value pairs we also expose them as dictionaries, for example in events to make the access by key as fast as possible. +Whenever we deal with key-value pairs, we also expose them as dictionaries. +For example, in the events to make the access by key more ergonomic. -## 2023-10-16 Do not implement IEnumerable when not needed +## 2023-10-16 Replace `IEnumerable` with `ReadOnlyCollection` -Some types like `EventChanges` represent readonly collections. They are implemented manually. This has been optimized by inheriting from readonly collections that are designed for that. Same for dictionaries. +Some types, like `EventChanges`, represent read-only collections. +This has been optimized by inheriting from `ReadOnlyCollection` that are designed for that. Same goes for dictionaries. -This provides more methods and properties (like Count) for the user and also removes the amount of code. \ No newline at end of file +This provides more methods and properties (like `Count`) for the user and also removes the amount of code. \ No newline at end of file From 6f7f57911120a80c5f21d05c731c3fd777179e32 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Mon, 16 Oct 2023 23:03:47 -0300 Subject: [PATCH 116/186] ci(improvements): simplify the `build` (now `test` workflow) --- .github/workflows/build.yml | 86 ------------------------------------- .github/workflows/test.yml | 58 +++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 86 deletions(-) delete mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 8ef935f1..00000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: CI - -on: - push: - branches: - - '*' - -jobs: - test: - runs-on: ${{matrix.os}} - strategy: - fail-fast: false - - matrix: - include: - # Windows - - build: win-x64 - os: windows-latest - - # Linux - - build: linux-x64 - os: ubuntu-latest - - - build: linux-x64-musl - os: ubuntu-latest - - # macOS - - build: macos - os: macos-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Download artifacts - uses: dawidd6/action-download-artifact@v2 - with: - path: ./output - workflow: build-binaries.yml - workflow_conclusion: success - - - name: Build Test - run: | - cd Tests/YDotNet.Tests.Unit - dotnet build - - - name: Copy to Test Folder - run: | - cp output/${{matrix.build}}/*.* Tests/YDotNet.Tests.Unit/bin/Debug/net7.0 - ls output/${{matrix.build}} - ls Tests/YDotNet.Tests.Unit/bin/Debug/net7.0 - - - name: Test - run: | - dotnet test -v n -m:1 - env: - RUST_BACKTRACE: 1 - - pack-nuget: - runs-on: ubuntu-latest - needs: test - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Download artifacts - uses: dawidd6/action-download-artifact@v2 - with: - path: ./output - workflow: build-binaries.yml - workflow_conclusion: success - - - name: Nuget pack - run: | - dotnet pack -c Release - - - name: Nuget publish - run: | - dotnet nuget push **/*.nupkg --source 'https://api.nuget.org/v3/index.json' --skip-duplicate -k ${{ secrets.nuget }} - - - name: Upload artifacts - uses: actions/upload-artifact@v3 - with: - path: | - **/*.nupkg diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..f00bf8ee --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,58 @@ +name: Test + +on: + push: + branches: + - 'main' + - 'for-pr' + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + + matrix: + include: + # Windows + - build: win-x64 + os: windows-latest + + # Linux + - build: linux-x64 + os: ubuntu-latest + + - build: linux-x64-musl + os: ubuntu-latest + + # macOS + - build: macos + os: macos-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Download artifacts + uses: dawidd6/action-download-artifact@v2 + with: + path: ./output + workflow: build-binaries.yml + workflow_conclusion: success + name: ${{ matrix.build }} + name_is_regexp: true + + - name: Copy binaries + run: | + cp output/${{ matrix.build }}/*.* YDotNet + + - name: Build tests + run: | + cd Tests/YDotNet.Tests.Unit + dotnet build + + - name: Run tests + run: | + dotnet test + env: + RUST_BACKTRACE: 1 From da810c4efd952d567db455ba3a61f6384f8ebcd9 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Mon, 16 Oct 2023 23:06:21 -0300 Subject: [PATCH 117/186] ci(improvements): introduce the `publish` workflow --- .github/workflows/publish.yml | 36 +++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..f5c915be --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,36 @@ +name: Publish + +on: + push: + branches: + - 'main' + - 'for-pr' + +pack-nuget: + runs-on: ubuntu-latest + needs: test + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Download artifacts + uses: dawidd6/action-download-artifact@v2 + with: + path: ./output + workflow: build-binaries.yml + workflow_conclusion: success + + - name: Nuget pack + run: | + dotnet pack -c Release + + - name: Nuget publish + run: | + dotnet nuget push **/*.nupkg --source 'https://api.nuget.org/v3/index.json' --skip-duplicate -k ${{ secrets.nuget }} + + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + path: | + **/*.nupkg From a4eb8dd498baeabf59e32bfafafd3a2bc67b744a Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Mon, 16 Oct 2023 23:08:04 -0300 Subject: [PATCH 118/186] ci(improvements): simplify the `build-binaries` workflow --- .github/workflows/build-binaries.yml | 49 ++++++++++++---------------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 95260d00..69195cd4 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -1,14 +1,15 @@ -name: Build Binaries +name: Build binaries on: push: - tags: - - 'binaries[0-9]' + tags: + - 'binaries' + env: - YRS_REPO: https://github.com/SebastianStehle/y-crdt + YRS_REPO: https://github.com/LSViana/y-crdt YRS_BRANCH: main CARGO_TERM_COLOR: always - + jobs: # Based on https://www.rohanjain.in/cargo-cross/ build-native-binaries: @@ -36,7 +37,7 @@ jobs: rust: stable target: x86_64-unknown-linux-musl cross: false - + - build: linux-armv7 os: ubuntu-latest rust: stable @@ -64,8 +65,8 @@ jobs: target: aarch64-unknown-linux-musl linker: gcc-aarch64-linux-gnu cross: true - - # MacOS + + # macOS - build: macos os: macos-latest rust: stable @@ -73,26 +74,16 @@ jobs: cross: false steps: - - name: Checkout repository - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v4 - - name: Cache - uses: actions/cache@v3 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - ~/.rustup - target - key: ${{ runner.os }}-${{ matrix.rust }} - - - name: Install cross + - name: Install Cross if: matrix.cross uses: taiki-e/install-action@v2 with: tool: cross - - name: Add musl tools + - name: Install musl tools run: sudo apt install -y musl musl-dev musl-tools if: endsWith(matrix.build, '-musl') @@ -102,26 +93,26 @@ jobs: sudo apt update sudo apt install ${{ matrix.linker }} cat .cargo/config.github >> .cargo/config - + - name: Install Rust run: | rustup install ${{ matrix.rust }} rustup target add ${{ matrix.target }} rustup show - - name: Clone Yrs repo + - name: Clone Yrs run: | git clone ${YRS_REPO} --branch ${YRS_BRANCH} --single-branch yrs shell: bash - - - name: Build (cargo) + + - name: Build (Cargo) if: "!matrix.cross" run: | cd yrs RUSTFLAGS="-C target-feature=-crt-static" cargo build --release --target ${{ matrix.target }} shell: bash - - - name: Build (cross) + + - name: Build (Cross) if: matrix.cross run: | cd yrs @@ -134,4 +125,4 @@ jobs: path: | yrs/target/${{ matrix.target }}/release/*yrs.dll yrs/target/${{ matrix.target }}/release/*yrs.so - yrs/target/${{ matrix.target }}/release/*yrs.dylib \ No newline at end of file + yrs/target/${{ matrix.target }}/release/*yrs.dylib From 99d44fd61ab210cadc16794a3f6551974c960724 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Mon, 16 Oct 2023 23:08:31 -0300 Subject: [PATCH 119/186] ci(improvements): fix the `publish` job definition --- .github/workflows/publish.yml | 55 ++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f5c915be..e49c15ee 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,31 +6,32 @@ on: - 'main' - 'for-pr' -pack-nuget: - runs-on: ubuntu-latest - needs: test +jobs: + pack-nuget: + runs-on: ubuntu-latest + needs: test + + steps: + - name: Checkout repository + uses: actions/checkout@v3 - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Download artifacts - uses: dawidd6/action-download-artifact@v2 - with: - path: ./output - workflow: build-binaries.yml - workflow_conclusion: success - - - name: Nuget pack - run: | - dotnet pack -c Release - - - name: Nuget publish - run: | - dotnet nuget push **/*.nupkg --source 'https://api.nuget.org/v3/index.json' --skip-duplicate -k ${{ secrets.nuget }} - - - name: Upload artifacts - uses: actions/upload-artifact@v3 - with: - path: | - **/*.nupkg + - name: Download artifacts + uses: dawidd6/action-download-artifact@v2 + with: + path: ./output + workflow: build-binaries.yml + workflow_conclusion: success + + - name: Nuget pack + run: | + dotnet pack -c Release + + - name: Nuget publish + run: | + dotnet nuget push **/*.nupkg --source 'https://api.nuget.org/v3/index.json' --skip-duplicate -k ${{ secrets.nuget }} + + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + path: | + **/*.nupkg From 9b1806435bdead11845c56aa0f7f260d107032df Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Mon, 16 Oct 2023 23:13:51 -0300 Subject: [PATCH 120/186] fix(improvements): add dynamic library files to the list of files copied during build --- YDotNet/YDotNet.csproj | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/YDotNet/YDotNet.csproj b/YDotNet/YDotNet.csproj index a791eb4e..dda0413f 100644 --- a/YDotNet/YDotNet.csproj +++ b/YDotNet/YDotNet.csproj @@ -16,6 +16,18 @@ + + + Always + + + Always + + + Always + + + all From c57aa7ecdb18a19fd32cebe7cd4a5c2cc668952b Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Mon, 16 Oct 2023 23:14:23 -0300 Subject: [PATCH 121/186] ci(improvements): fix the `publish` job definition --- .github/workflows/publish.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e49c15ee..0499e5bb 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,7 +9,6 @@ on: jobs: pack-nuget: runs-on: ubuntu-latest - needs: test steps: - name: Checkout repository From d01b247af6b380045620cf3e4ce8ea8c2227550b Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 17 Oct 2023 16:51:38 +0200 Subject: [PATCH 122/186] Remove clustering for now --- Demo/Program.cs | 11 -- Demo/appsettings.json | 6 - YDotNet.Server.Redis/RedisPubSub.cs | 65 ------- YDotNet.Server.Redis/ServiceExtensions.cs | 9 - .../YDotNetSocketMiddleware.cs | 1 + .../Clustering/ClusteringCallback.cs | 179 ------------------ .../Clustering/ClusteringOptions.cs | 10 - YDotNet.Server/Clustering/IPubSub.cs | 16 -- .../Clustering/Internal/PublishQueue.cs | 85 --------- YDotNet.Server/Clustering/Messages.cs | 56 ------ YDotNet.Server/ServiceExtensions.cs | 16 -- 11 files changed, 1 insertion(+), 453 deletions(-) delete mode 100644 YDotNet.Server.Redis/RedisPubSub.cs delete mode 100644 YDotNet.Server/Clustering/ClusteringCallback.cs delete mode 100644 YDotNet.Server/Clustering/ClusteringOptions.cs delete mode 100644 YDotNet.Server/Clustering/IPubSub.cs delete mode 100644 YDotNet.Server/Clustering/Internal/PublishQueue.cs delete mode 100644 YDotNet.Server/Clustering/Messages.cs diff --git a/Demo/Program.cs b/Demo/Program.cs index c2802a18..19ce257d 100644 --- a/Demo/Program.cs +++ b/Demo/Program.cs @@ -47,17 +47,6 @@ public static void Main(string[] args) } } - if (builder.Configuration["Clustering:Type"] == "Redis") - { - yDotNet.AddRedisClustering(); - yDotNet.AddRedis(options => - { - options.Configuration = - ConfigurationOptions.Parse( - builder.Configuration["Clustering:Redis:ConnectionString"]!); - }); - } - var app = builder.Build(); app.UseStaticFiles(); diff --git a/Demo/appsettings.json b/Demo/appsettings.json index 5b0ea871..b7a066dc 100644 --- a/Demo/appsettings.json +++ b/Demo/appsettings.json @@ -16,11 +16,5 @@ "Redis": { "ConnectionString": "localhost:6379" } - }, - "Clustering": { - "Type": "None", - "Redis": { - "ConnectionString": "localhost:6379" - } } } diff --git a/YDotNet.Server.Redis/RedisPubSub.cs b/YDotNet.Server.Redis/RedisPubSub.cs deleted file mode 100644 index b73d94c8..00000000 --- a/YDotNet.Server.Redis/RedisPubSub.cs +++ /dev/null @@ -1,65 +0,0 @@ -using Microsoft.Extensions.Options; -using StackExchange.Redis; -using YDotNet.Server.Clustering; -using YDotNet.Server.Internal; - -namespace YDotNet.Server.Redis; - -public sealed class RedisPubSub : IPubSub -{ - private readonly List> handlers = new(); - private readonly RedisClusteringOptions redisOptions; - private ISubscriber? subscriber; - - public RedisPubSub(IOptions redisOptions, RedisConnection redisConnection) - { - this.redisOptions = redisOptions.Value; - - _ = InitializeAsync(redisConnection); - } - - public async Task InitializeAsync(RedisConnection redisConnection) - { - // Use a single task, so that the ordering of registrations does not matter. - var connection = await redisConnection.Instance; - - subscriber = connection.GetSubscriber(); - subscriber.Subscribe(redisOptions.Channel, async (_, value) => - { - foreach (var handler in handlers) - { - byte[]? payload = value; - - if (payload != null) - { - await handler(payload); - } - } - }); - } - - public void Dispose() - { - subscriber?.UnsubscribeAll(); - } - - public IDisposable Subscribe(Func handler) - { - handlers.Add(handler); - - return new DelegateDisposable(() => - { - handlers.Remove(handler); - }); - } - - public async Task PublishAsync(byte[] payload, CancellationToken ct) - { - if (subscriber == null) - { - return; - } - - await subscriber.PublishAsync(redisOptions.Channel, payload); - } -} diff --git a/YDotNet.Server.Redis/ServiceExtensions.cs b/YDotNet.Server.Redis/ServiceExtensions.cs index eb7db176..e63d39e8 100644 --- a/YDotNet.Server.Redis/ServiceExtensions.cs +++ b/YDotNet.Server.Redis/ServiceExtensions.cs @@ -1,4 +1,3 @@ -using YDotNet.Server; using YDotNet.Server.Redis; using YDotNet.Server.Storage; @@ -14,14 +13,6 @@ public static YDotnetRegistration AddRedis(this YDotnetRegistration registration return registration; } - public static YDotnetRegistration AddRedisClustering(this YDotnetRegistration registration, Action? configure = null) - { - registration.Services.Configure(configure ?? (x => { })); - registration.AddPubSub(); - - return registration; - } - public static YDotnetRegistration AddRedisStorage(this YDotnetRegistration registration, Action? configure = null) { registration.Services.Configure(configure ?? (x => { })); diff --git a/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs b/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs index 13f83286..fb23cf3c 100644 --- a/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs +++ b/YDotNet.Server.WebSockets/YDotNetSocketMiddleware.cs @@ -255,6 +255,7 @@ private async Task HandleAwarenessAsync(ClientState state, for (var i = 0; i < clientCount; i++) { var clientId = await state.Decoder.ReadVarUintAsync(ct); + var clientClock = await state.Decoder.ReadVarUintAsync(ct); var clientState = await state.Decoder.ReadVarStringAsync(ct); diff --git a/YDotNet.Server/Clustering/ClusteringCallback.cs b/YDotNet.Server/Clustering/ClusteringCallback.cs deleted file mode 100644 index 52ac24c5..00000000 --- a/YDotNet.Server/Clustering/ClusteringCallback.cs +++ /dev/null @@ -1,179 +0,0 @@ -using Microsoft.Extensions.Options; -using ProtoBuf; -using YDotNet.Server.Clustering; -using YDotNet.Server.Clustering.Internal; - -namespace YDotNet.Server.Redis; - -public sealed class ClusteringCallback : IDocumentCallback, IDisposable -{ - private readonly Guid senderId = Guid.NewGuid(); - private readonly ClusteringOptions clusteringOptions; - private readonly PublishQueue publishQueue; - private readonly IPubSub publisher; - private readonly IDisposable subscription; - private IDocumentManager? documentManager; - - public ClusteringCallback(IOptions clusteringOptions, IPubSub publisher) - { - this.clusteringOptions = clusteringOptions.Value; - - this.publisher = publisher; - this.publishQueue = new PublishQueue( - this.clusteringOptions.MaxBatchCount, - this.clusteringOptions.MaxBatchSize, - (int)this.clusteringOptions.DebounceTime.TotalMilliseconds, - PublishBatchAsync); - - subscription = publisher.Subscribe(HandleMessage); - } - - public void Dispose() - { - subscription.Dispose(); - } - - public ValueTask OnInitializedAsync( - IDocumentManager manager) - { - // The initialize method is used to prevent circular dependencies between managers and hooks. - documentManager = manager; - return default; - } - - private async Task HandleMessage(byte[] payload) - { - if (documentManager == null) - { - return; - } - - var batch = Serializer.Deserialize(payload.AsSpan()); - - if (batch == null) - { - return; - } - - foreach (var message in batch) - { - if (message.SenderId == senderId) - { - continue; - } - - var context = new DocumentContext(message.DocumentName, message.ClientId) - { - Metadata = senderId - }; - - switch (message.Type) - { - case MessageType.ClientPinged: - await documentManager.PingAsync(context, message.ClientClock, message.ClientState); - break; - case MessageType.ClientDisconnected: - await documentManager.DisconnectAsync(context); - break; - case MessageType.Update when message.Data != null: - await documentManager.ApplyUpdateAsync(context, message.Data); - break; - case MessageType.SyncStep2 when message.Data != null: - await documentManager.ApplyUpdateAsync(context, message.Data); - break; - case MessageType.SyncStep1 when message.Data != null: - await SendSync2Async(context, message.Data); - break; - case MessageType.AwarenessRequested: - foreach (var (id, user) in await documentManager.GetAwarenessAsync(context)) - { - var userContext = context with { ClientId = id }; - - await SendAwarenessAsync(userContext, user.ClientState, user.ClientClock); - } - break; - } - } - } - - public ValueTask OnDocumentLoadedAsync(DocumentLoadEvent @event) - { - // Run these callbacks in another thread because it could cause deadlocks if it would interact with the same document. - _ = Task.Run(async () => - { - await SendAwarenessRequest(@event.Context); - await SendSync1Async(@event.Context); - }); - - return default; - } - - public async ValueTask OnAwarenessUpdatedAsync(ClientAwarenessEvent @event) - { - await SendAwarenessAsync(@event.Context, @event.ClientState, @event.ClientClock); - } - - public ValueTask OnClientDisconnectedAsync(ClientDisconnectedEvent @event) - { - var m = new Message { Type = MessageType.ClientDisconnected }; - - return EnqueueAsync(m, @event.Context); - } - - public ValueTask OnDocumentChangedAsync(DocumentChangedEvent @event) - { - var m = new Message { Type = MessageType.Update, Data = @event.Diff }; - - return EnqueueAsync(m, @event.Context); - } - - private ValueTask SendAwarenessAsync(DocumentContext context, string? state, long clock) - { - var m = new Message { Type = MessageType.ClientPinged, ClientState = state, ClientClock = clock }; - - return EnqueueAsync(m, context); - } - - private async ValueTask SendSync1Async(DocumentContext context) - { - var state = await documentManager!.GetStateVectorAsync(context); - - var m = new Message { Type = MessageType.SyncStep1, Data = state }; - - await EnqueueAsync(m, context); - } - - private async ValueTask SendSync2Async(DocumentContext context, byte[] stateVector) - { - var state = await documentManager!.GetUpdateAsync(context, stateVector); - - var m = new Message { Type = MessageType.SyncStep2, Data = state }; - - await EnqueueAsync(m, context); - } - - private ValueTask SendAwarenessRequest(DocumentContext context) - { - var m = new Message { Type = MessageType.AwarenessRequested }; - - return EnqueueAsync(m, context); - } - - private ValueTask EnqueueAsync(Message message, DocumentContext context) - { - message.ClientId = context.ClientId; - message.DocumentName = context.DocumentName; - message.SenderId = senderId; - - return publishQueue.EnqueueAsync(message, default); - } - - private async Task PublishBatchAsync(List batch, CancellationToken ct) - { - using var stream = new MemoryStream(); - - Serializer.Serialize(stream, batch); - - await publisher.PublishAsync(stream.ToArray(), default); - } -} diff --git a/YDotNet.Server/Clustering/ClusteringOptions.cs b/YDotNet.Server/Clustering/ClusteringOptions.cs deleted file mode 100644 index 83848393..00000000 --- a/YDotNet.Server/Clustering/ClusteringOptions.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace YDotNet.Server.Clustering; - -public sealed class ClusteringOptions -{ - public TimeSpan DebounceTime { get; set; } = TimeSpan.FromMilliseconds(500); - - public int MaxBatchCount { get; set; } = 100; - - public int MaxBatchSize { get; set; } = 1024 * 1024; -} diff --git a/YDotNet.Server/Clustering/IPubSub.cs b/YDotNet.Server/Clustering/IPubSub.cs deleted file mode 100644 index f762d996..00000000 --- a/YDotNet.Server/Clustering/IPubSub.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace YDotNet.Server.Clustering -{ - public interface IPubSub - { - IDisposable Subscribe(Func handler); - - Task PublishAsync(byte[] payload, - CancellationToken ct); - } -} diff --git a/YDotNet.Server/Clustering/Internal/PublishQueue.cs b/YDotNet.Server/Clustering/Internal/PublishQueue.cs deleted file mode 100644 index 379cb5c3..00000000 --- a/YDotNet.Server/Clustering/Internal/PublishQueue.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System.Threading.Channels; - -namespace YDotNet.Server.Clustering.Internal; - -public interface ICanEstimateSize -{ - int EstimateSize(); -} - -public sealed class PublishQueue where T : ICanEstimateSize -{ - private readonly Channel inputChannel = Channel.CreateBounded(100); - private readonly Channel> outputChannel = Channel.CreateBounded>(2); - private readonly CancellationTokenSource cts = new(); - - public PublishQueue(int maxCount, int maxSize, int timeout, Func, CancellationToken, Task> handler) - { - Task.Run(async () => - { - var batchList = new List(maxCount); - var batchSize = 0; - - // Just a marker object to force sending out new batches. - var force = new object(); - - await using var timer = new Timer(_ => inputChannel.Writer.TryWrite(force)); - - async Task TrySendAsync() - { - if (batchList.Count > 0) - { - await outputChannel.Writer.WriteAsync(batchList, cts.Token); - - // Create a new batch, because the value is shared and might be processes by another concurrent task. - batchList = new List(); - batchSize = 0; - } - } - - // Exceptions usually that the process was stopped and the channel closed, therefore we do not catch them. - await foreach (var item in inputChannel.Reader.ReadAllAsync(cts.Token)) - { - if (ReferenceEquals(item, force)) - { - // Our item is the marker object from the timer. - await TrySendAsync(); - } - else if (item is T typed) - { - // The timeout restarts with the last event and should push events out if no further events are received. - timer.Change(timeout, Timeout.Infinite); - - batchList.Add(typed); - batchSize += typed.EstimateSize(); - - if (batchList.Count >= maxSize || batchSize >= maxSize) - { - await TrySendAsync(); - } - } - } - - await TrySendAsync(); - }, cts.Token).ContinueWith(x => outputChannel.Writer.TryComplete(x.Exception)); - - Task.Run(async () => - { - await foreach (var batch in outputChannel.Reader.ReadAllAsync(cts.Token)) - { - await handler(batch, cts.Token); - } - }, cts.Token); - } - - public ValueTask EnqueueAsync(T item, - CancellationToken ct) - { - return inputChannel.Writer.WriteAsync(item, ct); - } - - public void Dispose() - { - cts.Cancel(); - } -} diff --git a/YDotNet.Server/Clustering/Messages.cs b/YDotNet.Server/Clustering/Messages.cs deleted file mode 100644 index 59f3d709..00000000 --- a/YDotNet.Server/Clustering/Messages.cs +++ /dev/null @@ -1,56 +0,0 @@ -using ProtoBuf; -using YDotNet.Server.Clustering.Internal; - -#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - -namespace YDotNet.Server.Clustering; - -[ProtoContract] -public enum MessageType -{ - ClientPinged, - ClientDisconnected, - AwarenessRequested, - Update, - SyncStep1, - SyncStep2, -} - -[ProtoContract] -public sealed class Message : ICanEstimateSize -{ - private static readonly int GuidLength = Guid.Empty.ToString().Length; - - [ProtoMember(1)] - public MessageType Type { get; set; } - - [ProtoMember(1)] - public Guid SenderId { get; set; } - - [ProtoMember(2)] - public string DocumentName { get; set; } - - [ProtoMember(3)] - public long ClientId { get; set; } - - [ProtoMember(4)] - public long ClientClock { get; set; } - - [ProtoMember(5)] - public string? ClientState { get; set; } - - [ProtoMember(6)] - public byte[]? Data { get; set; } - - public int EstimateSize() - { - var size = - GuidLength + - sizeof(long) + - sizeof(long) + - DocumentName.Length + - Data?.Length ?? 0; - - return size; - } -} diff --git a/YDotNet.Server/ServiceExtensions.cs b/YDotNet.Server/ServiceExtensions.cs index 0b235704..2227b7c4 100644 --- a/YDotNet.Server/ServiceExtensions.cs +++ b/YDotNet.Server/ServiceExtensions.cs @@ -1,8 +1,6 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using YDotNet.Server; -using YDotNet.Server.Clustering; -using YDotNet.Server.Redis; using YDotNet.Server.Storage; namespace Microsoft.Extensions.DependencyInjection; @@ -29,20 +27,6 @@ public static YDotnetRegistration AddCallback(this YDotnetRegistration regist registration.Services.AddSingleton(); return registration; } - - public static YDotnetRegistration AddPubSub(this YDotnetRegistration registration) where T : class, IPubSub - { - registration.Services.AddSingleton(); - return registration; - } - - public static YDotnetRegistration AddClustering(this YDotnetRegistration registration, Action? configure) - { - registration.Services.Configure(configure ?? (x => { })); - - registration.AddCallback(); - return registration; - } } public sealed class YDotnetRegistration From 1821215bbf219af72402ee0ceb08f4d185e7e22c Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Fri, 20 Oct 2023 00:08:46 -0300 Subject: [PATCH 123/186] ci(improvements): make the `publish` workflow run for version tags --- .github/workflows/publish.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0499e5bb..a9fd8262 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,6 +5,8 @@ on: branches: - 'main' - 'for-pr' + tags: + - 'v[0-9]+.[0-9]+.[0.9]+' jobs: pack-nuget: From 03bb43279ca5465fcce96e9ee0aff364d6091eab Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Fri, 20 Oct 2023 00:15:26 -0300 Subject: [PATCH 124/186] docs(improvements): fix typo, format, and improve documentation for the `Resource` class --- YDotNet/Infrastructure/Resource.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/YDotNet/Infrastructure/Resource.cs b/YDotNet/Infrastructure/Resource.cs index 4ac46841..9ed8afff 100644 --- a/YDotNet/Infrastructure/Resource.cs +++ b/YDotNet/Infrastructure/Resource.cs @@ -1,24 +1,24 @@ namespace YDotNet.Infrastructure; /// -/// Base class for all resoruces. +/// Base class for managed resources that hold native resources. /// public abstract class Resource : IDisposable { /// - /// Gets a value indicating whether this instance is disposed. + /// Gets a value indicating whether this instance is disposed. /// public bool IsDisposed { get; private set; } - /// + /// public void Dispose() { - Dispose(true); + Dispose(disposing: true); GC.SuppressFinalize(this); } /// - /// Throws an exception if this object or the owner is disposed. + /// Throws an exception if this object or the owner is disposed. /// /// Object or the owner has been disposed. protected void ThrowIfDisposed() @@ -30,9 +30,9 @@ protected void ThrowIfDisposed() } /// - /// Releases all unmanaged resources. + /// Releases all unmanaged resources. /// - /// True, if also managed resources should be disposed. + /// true if managed resources should be disposed as well. protected void Dispose(bool disposing) { if (IsDisposed) @@ -41,12 +41,13 @@ protected void Dispose(bool disposing) } DisposeCore(disposing); + IsDisposed = true; } /// - /// Releases all unmanaged resources. + /// Releases all unmanaged resources. /// - /// True, if also managed resources should be disposed. + /// true if managed resources should be disposed as well. protected internal abstract void DisposeCore(bool disposing); } From 877427bdb28bae7839a1b402a147248edbc8c8b2 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sat, 21 Oct 2023 11:22:01 -0300 Subject: [PATCH 125/186] refactor(improvements): rename `DisposableHandles` and the head handle value --- YDotNet/Infrastructure/MemoryWriter.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/YDotNet/Infrastructure/MemoryWriter.cs b/YDotNet/Infrastructure/MemoryWriter.cs index bf7699cb..e907ba46 100644 --- a/YDotNet/Infrastructure/MemoryWriter.cs +++ b/YDotNet/Infrastructure/MemoryWriter.cs @@ -5,11 +5,11 @@ namespace YDotNet.Infrastructure; internal static class MemoryWriter { - internal static unsafe DisposableHandle WriteUtf8String(string? value) + internal static DisposableHandle WriteUtf8String(string? value) { if (value == null) { - return new DisposableHandle(0); + return new DisposableHandle(Handle: 0); } return new DisposableHandle(WriteUtf8StringCore(value)); @@ -23,12 +23,12 @@ private static unsafe nint WriteUtf8StringCore(string value) var memory = new Span(bufferPointer.ToPointer(), bufferLength); Encoding.UTF8.GetBytes(value, memory); - memory[bufferLength - 1] = (byte)'\0'; + memory[bufferLength - 1] = (byte) '\0'; return bufferPointer; } - internal static DisposableHandles WriteUtf8StringArray(string[] values) + internal static DisposableHandleArray WriteUtf8StringArray(string[] values) { var head = Marshal.AllocHGlobal(MemoryConstants.PointerSize * values.Length); @@ -41,7 +41,7 @@ internal static DisposableHandles WriteUtf8StringArray(string[] values) Marshal.WriteIntPtr(head + i * MemoryConstants.PointerSize, pointers[i]); } - return new DisposableHandles(head, pointers); + return new DisposableHandleArray(head, pointers); } internal static DisposableHandle WriteStructArray(T[] value) @@ -63,7 +63,7 @@ internal static DisposableHandle WriteStruct(T? value) { if (value == null) { - return new DisposableHandle(0); + return new DisposableHandle(Handle: 0); } return WriteStruct(value.Value); @@ -90,13 +90,13 @@ public void Dispose() } } - internal sealed record DisposableHandles(nint Handle, nint[] Handles) : IDisposable + internal sealed record DisposableHandleArray(nint Head, nint[] Handles) : IDisposable { public void Dispose() { - if (Handle != nint.Zero) + if (Head != nint.Zero) { - Marshal.FreeHGlobal(Handle); + Marshal.FreeHGlobal(Head); } foreach (var handle in Handles) From b97a0e6ef542e72720c88c88255c03e287280b22 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sat, 21 Oct 2023 11:22:21 -0300 Subject: [PATCH 126/186] refactor(improvements): fix typos and adapt `Input` to the new API of `DisposableHandleArray` --- YDotNet/Document/Cells/Input.cs | 34 +++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/YDotNet/Document/Cells/Input.cs b/YDotNet/Document/Cells/Input.cs index 52626690..d0705c11 100644 --- a/YDotNet/Document/Cells/Input.cs +++ b/YDotNet/Document/Cells/Input.cs @@ -18,11 +18,16 @@ internal Input(InputNative native, params IDisposable[] allocatedMemory) } /// - /// Finalizes an instance of the class. + /// Gets the native input cell represented by this cell. + /// + internal InputNative InputNative { get; } + + /// + /// Finalizes an instance of the class. /// ~Input() { - Dispose(false); + Dispose(disposing: false); } /// @@ -34,11 +39,6 @@ protected internal override void DisposeCore(bool disposing) } } - /// - /// Gets the native input cell represented by this cell. - /// - internal InputNative InputNative { get; } - /// /// Creates a new instance of the class. /// @@ -98,7 +98,7 @@ public static Input Long(long value) /// The cell that represents the provided value. public static Input Bytes(byte[] value) { - return new Input(InputChannel.Bytes(value, (uint)value.Length)); + return new Input(InputChannel.Bytes(value, (uint) value.Length)); } /// @@ -110,7 +110,7 @@ public static Input Collection(Input[] value) { var unsafeMemory = MemoryWriter.WriteStructArray(value.Select(x => x.InputNative).ToArray()); - return new Input(InputChannel.Collection(unsafeMemory.Handle, (uint)value.Length), unsafeMemory); + return new Input(InputChannel.Collection(unsafeMemory.Handle, (uint) value.Length), unsafeMemory); } /// @@ -120,10 +120,13 @@ public static Input Collection(Input[] value) /// The cell that represents the provided value. public static Input Object(IDictionary value) { - var unsageKeys = MemoryWriter.WriteUtf8StringArray(value.Keys.ToArray()); + var unsafeKeys = MemoryWriter.WriteUtf8StringArray(value.Keys.ToArray()); var unsafeValues = MemoryWriter.WriteStructArray(value.Values.Select(x => x.InputNative).ToArray()); - return new Input(InputChannel.Object(unsageKeys.Handle, unsafeValues.Handle, (uint)value.Count), unsageKeys, unsafeValues); + return new Input( + InputChannel.Object(unsafeKeys.Head, unsafeValues.Handle, (uint) value.Count), + unsafeKeys, + unsafeValues); } /// @@ -153,7 +156,7 @@ public static Input Array(Input[] value) { var unsafeMemory = MemoryWriter.WriteStructArray(value.Select(x => x.InputNative).ToArray()); - return new Input(InputChannel.Array(unsafeMemory.Handle, (uint)value.Length), unsafeMemory); + return new Input(InputChannel.Array(unsafeMemory.Handle, (uint) value.Length), unsafeMemory); } /// @@ -163,10 +166,13 @@ public static Input Array(Input[] value) /// The cell that represents the provided value. public static Input Map(IDictionary value) { - var unsageKeys = MemoryWriter.WriteUtf8StringArray(value.Keys.ToArray()); + var unsafeKeys = MemoryWriter.WriteUtf8StringArray(value.Keys.ToArray()); var unsafeValues = MemoryWriter.WriteStructArray(value.Values.Select(x => x.InputNative).ToArray()); - return new Input(InputChannel.Map(unsageKeys.Handle, unsafeValues.Handle, (uint)value.Count), unsageKeys, unsafeValues); + return new Input( + InputChannel.Map(unsafeKeys.Head, unsafeValues.Handle, (uint) value.Count), + unsafeKeys, + unsafeValues); } /// From 2f00203732e83089ee8d05370b0d2840f80440ba Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sat, 21 Oct 2023 11:23:30 -0300 Subject: [PATCH 127/186] style(improvements): format and remove unnecessary disabled rules from the root `.editorconfig` --- .editorconfig | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/.editorconfig b/.editorconfig index b746cd78..c9c9bd8b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,12 +10,11 @@ tab_width = 4 # New line preferences end_of_line = lf - insert_final_newline = true +# C# preferences csharp_style_namespace_declarations = file_scoped - # StyleCop rules (https://dotnetanalyzers.github.io/StyleCopAnalyzers/) # Special rules @@ -27,14 +26,10 @@ dotnet_diagnostic.SA1008.severity = suggestion # Opening parenthesis should be s dotnet_diagnostic.SA1009.severity = suggestion # Closing parenthesis should be spaced correctly # Readability rules -dotnet_diagnostic.SA1101.severity = none # Prefix local calls with this -dotnet_diagnostic.SA1116.severity = none -dotnet_diagnostic.SA1127.severity = none # Prefix local calls with this +dotnet_diagnostic.SA1101.severity = none # Prefix local calls with this # Ordering rules dotnet_diagnostic.SA1200.severity = suggestion # Using directives should be placed correctly -dotnet_diagnostic.SA1201.severity = none -dotnet_diagnostic.SA1202.severity = none dotnet_diagnostic.SA1204.severity = suggestion # Static elements should appear before instance elements # Maintainability rules @@ -45,8 +40,8 @@ dotnet_diagnostic.SA1413.severity = suggestion # Use trailing comma in multi-lin dotnet_diagnostic.SA1500.severity = suggestion # Braces for multi-line statements should not share line # Documentation rules -dotnet_diagnostic.SA1600.severity = none # Elements should be documented -dotnet_diagnostic.SA1601.severity = none # Partial elements should be documented -dotnet_diagnostic.SA1602.severity = none # Enumeration items should be documented -dotnet_diagnostic.SA1633.severity = none # File should have header +dotnet_diagnostic.SA1600.severity = none # Elements should be documented +dotnet_diagnostic.SA1601.severity = none # Partial elements should be documented +dotnet_diagnostic.SA1602.severity = none # Enumeration items should be documented +dotnet_diagnostic.SA1633.severity = none # File should have header From 8ea4792fd37fac583f09b3f2258c0cf5ffc8cd8d Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sat, 21 Oct 2023 11:26:40 -0300 Subject: [PATCH 128/186] style(improvements): format `JsonArray` --- YDotNet/Document/Cells/JsonArray.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/YDotNet/Document/Cells/JsonArray.cs b/YDotNet/Document/Cells/JsonArray.cs index 81289cfc..151bdf54 100644 --- a/YDotNet/Document/Cells/JsonArray.cs +++ b/YDotNet/Document/Cells/JsonArray.cs @@ -5,7 +5,7 @@ namespace YDotNet.Document.Cells; /// -/// Represents a json array. +/// Represents a JSON array. /// public sealed class JsonArray : ReadOnlyCollection { From efe2d73e5ea1be71280fa56d888144a09decd99d Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sat, 21 Oct 2023 11:28:41 -0300 Subject: [PATCH 129/186] style(improvements): format `JsonObject` and turn `MapEntryNative.Key` internal --- YDotNet/Document/Cells/JsonObject.cs | 2 +- YDotNet/Native/Types/Maps/MapEntryNative.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/YDotNet/Document/Cells/JsonObject.cs b/YDotNet/Document/Cells/JsonObject.cs index 63a8f814..96412871 100644 --- a/YDotNet/Document/Cells/JsonObject.cs +++ b/YDotNet/Document/Cells/JsonObject.cs @@ -6,7 +6,7 @@ namespace YDotNet.Document.Cells; /// -/// Represents a json object. +/// Represents a JSON object. /// public sealed class JsonObject : ReadOnlyDictionary { diff --git a/YDotNet/Native/Types/Maps/MapEntryNative.cs b/YDotNet/Native/Types/Maps/MapEntryNative.cs index 07256b3c..85cb64d7 100644 --- a/YDotNet/Native/Types/Maps/MapEntryNative.cs +++ b/YDotNet/Native/Types/Maps/MapEntryNative.cs @@ -9,7 +9,7 @@ internal readonly struct MapEntryNative { internal const int Size = 8 + OutputNative.Size; - public nint KeyHandle { get; } + internal nint KeyHandle { get; } public nint ValueHandle(nint baseHandle) { From 3249f6fbad7ccc6083e404cf06372c63921d2dcb Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sat, 21 Oct 2023 11:31:44 -0300 Subject: [PATCH 130/186] style(improvements): format and scope `pragma` to `Output.Boolean` property --- YDotNet/Document/Cells/Output.cs | 174 +++++++++++++++---------------- 1 file changed, 87 insertions(+), 87 deletions(-) diff --git a/YDotNet/Document/Cells/Output.cs b/YDotNet/Document/Cells/Output.cs index 1b93c044..ae9396e2 100644 --- a/YDotNet/Document/Cells/Output.cs +++ b/YDotNet/Document/Cells/Output.cs @@ -6,8 +6,6 @@ using YDotNet.Native.Cells.Outputs; using Array = YDotNet.Document.Types.Arrays.Array; -#pragma warning disable SA1623 // Property summary documentation should match accessors - namespace YDotNet.Document.Cells; /// @@ -21,98 +19,15 @@ internal Output(nint handle, Doc doc, bool isDeleted) { var native = MemoryReader.ReadStruct(handle); - Tag = (OutputTag)native.Tag; + Tag = (OutputTag) native.Tag; value = BuildValue(handle, native.Length, doc, isDeleted, Tag); } - internal static Output CreateAndRelease(nint handle, Doc doc) - { - var result = new Output(handle, doc, false); - - // The output reads everything so we can just destroy it. - OutputChannel.Destroy(handle); - - return result; - } - - private static object? BuildValue(nint handle, uint length, Doc doc, bool isDeleted, OutputTag type) - { - switch (type) - { - case OutputTag.Bool: - { - var value = OutputChannel.Boolean(handle).Checked(); - - return MemoryReader.ReadStruct(value) == 1; - } - - case OutputTag.Double: - { - var value = OutputChannel.Double(handle).Checked(); - - return MemoryReader.ReadStruct(value); - } - - case OutputTag.Long: - { - var value = OutputChannel.Long(handle).Checked(); - - return MemoryReader.ReadStruct(value); - } - - case OutputTag.String: - { - MemoryReader.TryReadUtf8String(OutputChannel.String(handle), out var result); - - return result; - } - - case OutputTag.Bytes: - { - var bytesHandle = OutputChannel.Bytes(handle).Checked(); - var bytesArray = MemoryReader.ReadBytes(OutputChannel.Bytes(handle), length); - - return bytesArray; - } - - case OutputTag.JsonArray: - { - return new JsonArray(handle, length, doc, isDeleted); - } - - case OutputTag.JsonObject: - { - return new JsonObject(handle, length, doc, isDeleted); - } - - case OutputTag.Array: - return doc.GetArray(OutputChannel.Array(handle), isDeleted); - - case OutputTag.Map: - return doc.GetMap(OutputChannel.Map(handle), isDeleted); - - case OutputTag.Text: - return doc.GetText(OutputChannel.Text(handle), isDeleted); - - case OutputTag.XmlElement: - return doc.GetXmlElement(OutputChannel.XmlElement(handle), isDeleted); - - case OutputTag.XmlText: - return doc.GetXmlText(OutputChannel.XmlText(handle), isDeleted); - - case OutputTag.Doc: - return doc.GetDoc(OutputChannel.Doc(handle), isDeleted); - - default: - return null; - } - } - /// /// Gets the type of the output. /// - public OutputTag Tag { get; private set; } + public OutputTag Tag { get; } /// /// Gets the value. @@ -126,11 +41,13 @@ internal static Output CreateAndRelease(nint handle, Doc doc) /// Value is not a . public string String => GetValue(OutputTag.String); +#pragma warning disable SA1623 (This property documentation shouldn't start with the standard text) /// /// Gets the value. /// /// Value is not a . public bool Boolean => GetValue(OutputTag.Bool); +#pragma warning restore SA1623 /// /// Gets the value. @@ -197,6 +114,89 @@ internal static Output CreateAndRelease(nint handle, Doc doc) /// Value is not a . public XmlText XmlText => GetValue(OutputTag.XmlText); + internal static Output CreateAndRelease(nint handle, Doc doc) + { + var result = new Output(handle, doc, isDeleted: false); + + // The output reads everything so we can just destroy it. + OutputChannel.Destroy(handle); + + return result; + } + + private static object? BuildValue(nint handle, uint length, Doc doc, bool isDeleted, OutputTag type) + { + switch (type) + { + case OutputTag.Bool: + { + var value = OutputChannel.Boolean(handle).Checked(); + + return MemoryReader.ReadStruct(value) == 1; + } + + case OutputTag.Double: + { + var value = OutputChannel.Double(handle).Checked(); + + return MemoryReader.ReadStruct(value); + } + + case OutputTag.Long: + { + var value = OutputChannel.Long(handle).Checked(); + + return MemoryReader.ReadStruct(value); + } + + case OutputTag.String: + { + MemoryReader.TryReadUtf8String(OutputChannel.String(handle), out var result); + + return result; + } + + case OutputTag.Bytes: + { + var bytesHandle = OutputChannel.Bytes(handle).Checked(); + var bytesArray = MemoryReader.ReadBytes(OutputChannel.Bytes(handle), length); + + return bytesArray; + } + + case OutputTag.JsonArray: + { + return new JsonArray(handle, length, doc, isDeleted); + } + + case OutputTag.JsonObject: + { + return new JsonObject(handle, length, doc, isDeleted); + } + + case OutputTag.Array: + return doc.GetArray(OutputChannel.Array(handle), isDeleted); + + case OutputTag.Map: + return doc.GetMap(OutputChannel.Map(handle), isDeleted); + + case OutputTag.Text: + return doc.GetText(OutputChannel.Text(handle), isDeleted); + + case OutputTag.XmlElement: + return doc.GetXmlElement(OutputChannel.XmlElement(handle), isDeleted); + + case OutputTag.XmlText: + return doc.GetXmlText(OutputChannel.XmlText(handle), isDeleted); + + case OutputTag.Doc: + return doc.GetDoc(OutputChannel.Doc(handle), isDeleted); + + default: + return null; + } + } + private T GetValue(OutputTag expectedType) { if (value is not T typed) From 95d2828c41ec2203bddb5952277e139835631996 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sat, 21 Oct 2023 11:38:01 -0300 Subject: [PATCH 131/186] refactor(improvements): format, move, and simplify `ThrowHelper` --- YDotNet/Infrastructure/ThrowHelper.cs | 19 +++++++++++++++++++ YDotNet/ThrowHelper.cs | 24 ------------------------ 2 files changed, 19 insertions(+), 24 deletions(-) create mode 100644 YDotNet/Infrastructure/ThrowHelper.cs delete mode 100644 YDotNet/ThrowHelper.cs diff --git a/YDotNet/Infrastructure/ThrowHelper.cs b/YDotNet/Infrastructure/ThrowHelper.cs new file mode 100644 index 00000000..50ec6463 --- /dev/null +++ b/YDotNet/Infrastructure/ThrowHelper.cs @@ -0,0 +1,19 @@ +namespace YDotNet.Infrastructure; + +internal static class ThrowHelper +{ + public static void InternalError() + { + YDotNetException("Operation failed. The yffi library returned null without further details."); + } + + public static void PendingTransaction() + { + YDotNetException("Failed to open a transaction, probably because another transaction is still open."); + } + + public static void YDotNetException(string message) + { + throw new YDotNetException(message); + } +} diff --git a/YDotNet/ThrowHelper.cs b/YDotNet/ThrowHelper.cs deleted file mode 100644 index cf657e9b..00000000 --- a/YDotNet/ThrowHelper.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace YDotNet; - -internal static class ThrowHelper -{ - public static void InternalError() - { - throw new YDotNetException("Operation failed. Lib returns null pointer without further details."); - } - - public static void PendingTransaction() - { - throw new YDotNetException("Failed to open a transaction. Probably because another transaction is still pending."); - } - - public static void YDotnet(string message) - { - throw new YDotNetException(message); - } - - public static void ArgumentException(string message, string paramName) - { - throw new ArgumentException(message, paramName); - } -} From fd62ae090f17b9b3b38a6fa8d23c7a848b41a298 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sat, 21 Oct 2023 11:39:01 -0300 Subject: [PATCH 132/186] refactor(improvements): rename `ThrowHelper.InternalError()` to `Null()` for clarity --- YDotNet/Infrastructure/ThrowHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/YDotNet/Infrastructure/ThrowHelper.cs b/YDotNet/Infrastructure/ThrowHelper.cs index 50ec6463..e11a6edc 100644 --- a/YDotNet/Infrastructure/ThrowHelper.cs +++ b/YDotNet/Infrastructure/ThrowHelper.cs @@ -2,7 +2,7 @@ namespace YDotNet.Infrastructure; internal static class ThrowHelper { - public static void InternalError() + public static void Null() { YDotNetException("Operation failed. The yffi library returned null without further details."); } From e47776d8eb31d26e38046b6707e3929fbbb5069b Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sat, 21 Oct 2023 11:40:14 -0300 Subject: [PATCH 133/186] refactor(improvements): rename and move `Extensions` class to `IntPtrExtensions` --- .../{Extensions.cs => Extensions/IntPtrExtensions.cs} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename YDotNet/Infrastructure/{Extensions.cs => Extensions/IntPtrExtensions.cs} (54%) diff --git a/YDotNet/Infrastructure/Extensions.cs b/YDotNet/Infrastructure/Extensions/IntPtrExtensions.cs similarity index 54% rename from YDotNet/Infrastructure/Extensions.cs rename to YDotNet/Infrastructure/Extensions/IntPtrExtensions.cs index 04b2a659..fc6d5132 100644 --- a/YDotNet/Infrastructure/Extensions.cs +++ b/YDotNet/Infrastructure/Extensions/IntPtrExtensions.cs @@ -1,12 +1,12 @@ -namespace YDotNet.Infrastructure; +namespace YDotNet.Infrastructure.Extensions; -internal static class Extensions +internal static class IntPtrExtensions { public static nint Checked(this nint input) { if (input == nint.Zero) { - ThrowHelper.InternalError(); + ThrowHelper.Null(); } return input; From ca7eb15794ebd92d051a6a3994d03334aaafcf06 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sat, 21 Oct 2023 11:40:31 -0300 Subject: [PATCH 134/186] refactor(improvements): drop unused `enum OutputType` --- YDotNet/Document/Cells/OutputType.cs | 92 ---------------------------- 1 file changed, 92 deletions(-) delete mode 100644 YDotNet/Document/Cells/OutputType.cs diff --git a/YDotNet/Document/Cells/OutputType.cs b/YDotNet/Document/Cells/OutputType.cs deleted file mode 100644 index ad19411d..00000000 --- a/YDotNet/Document/Cells/OutputType.cs +++ /dev/null @@ -1,92 +0,0 @@ -namespace YDotNet.Document.Cells; - -/// -/// The type of an output. -/// -public enum OutputType -{ - /// - /// No defined. - /// - NotSet = -99, - - /// - /// Flag used by `YInput` and `YOutput` to tag boolean values. - /// - Bool = -8, - - /// - /// Flag used by `YInput` and `YOutput` to tag floating point numbers. - /// - Double = -7, - - /// - /// Flag used by `YInput` and `YOutput` to tag 64-bit integer numbers. - /// - Long = -6, - - /// - /// Flag used by `YInput` and `YOutput` to tag strings. - /// - String = -5, - - /// - /// Flag used by `YInput` and `YOutput` to tag binary content. - /// - Bytes = -4, - - /// - /// Flag used by `YInput` and `YOutput` to tag embedded JSON-like arrays of values. - /// - Collection = -3, - - /// - /// Flag used by `YInput` and `YOutput` to tag embedded JSON-like maps of key-value pairs. - /// - Object = -2, - - /// - /// Flag used by `YInput` and `YOutput` to tag JSON-like null values. - /// - Null = -1, - - /// - /// Flag used by `YInput` and `YOutput` to tag JSON-like undefined values. - /// - Undefined = 0, - - /// - /// Flag used by `YInput` and `YOutput` to tag content, which is an `YArray` shared type. - /// - Array = 1, - - /// - /// Flag used by `YInput` and `YOutput` to tag content, which is an `YMap` shared type. - /// - Map = 2, - - /// - /// Flag used by `YInput` and `YOutput` to tag content, which is an `YText` shared type. - /// - Text = 3, - - /// - /// Flag used by `YInput` and `YOutput` to tag content, which is an `YXmlElement` shared type. - /// - XmlElement = 4, - - /// - /// Flag used by `YInput` and `YOutput` to tag content, which is an `YXmlText` shared type. - /// - XmlText = 5, - - /// - /// Flag used by `YInput` and `YOutput` to tag content, which is an `YXmlFragment` shared type. - /// - XmlFragment = 6, - - /// - /// Flag used by `YInput` and `YOutput` to tag content, which is an `YDoc` shared type. - /// - Doc = 7 -} From b66afb3eaf1daf768c4689faadd3f9422211a6b2 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sat, 21 Oct 2023 11:41:56 -0300 Subject: [PATCH 135/186] refactor(improvements): drop `ThrowHelper.YDotNetException()` since it's better to throw exceptions inline for flow analysis --- YDotNet/Infrastructure/ThrowHelper.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/YDotNet/Infrastructure/ThrowHelper.cs b/YDotNet/Infrastructure/ThrowHelper.cs index e11a6edc..3eef9920 100644 --- a/YDotNet/Infrastructure/ThrowHelper.cs +++ b/YDotNet/Infrastructure/ThrowHelper.cs @@ -4,16 +4,11 @@ internal static class ThrowHelper { public static void Null() { - YDotNetException("Operation failed. The yffi library returned null without further details."); + throw new YDotNetException("Operation failed. The yffi library returned null without further details."); } public static void PendingTransaction() { - YDotNetException("Failed to open a transaction, probably because another transaction is still open."); - } - - public static void YDotNetException(string message) - { - throw new YDotNetException(message); + throw new YDotNetException("Failed to open a transaction, probably because another transaction is still open."); } } From 60d71a8e8fd058883806c95887d55442d0a74274 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sat, 21 Oct 2023 11:42:14 -0300 Subject: [PATCH 136/186] refactor(improvements): format and update `Transaction` --- YDotNet/Document/Transactions/Transaction.cs | 51 +++++++++++--------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/YDotNet/Document/Transactions/Transaction.cs b/YDotNet/Document/Transactions/Transaction.cs index 6c20b5d9..9cfab56f 100644 --- a/YDotNet/Document/Transactions/Transaction.cs +++ b/YDotNet/Document/Transactions/Transaction.cs @@ -4,6 +4,7 @@ using YDotNet.Document.Types.XmlElements; using YDotNet.Document.Types.XmlTexts; using YDotNet.Infrastructure; +using YDotNet.Infrastructure.Extensions; using YDotNet.Native.Transaction; using YDotNet.Native.Types.Branches; using Array = YDotNet.Document.Types.Arrays.Array; @@ -32,7 +33,12 @@ internal Transaction(nint handle, Doc doc) doc.NotifyTransactionStarted(); } - /// + /// + /// Gets a value indicating whether the transaction is writeable. + /// + public bool Writeable => TransactionChannel.Writeable(Handle) == 1; + + /// protected internal override void DisposeCore(bool disposing) { if (disposing) @@ -43,11 +49,6 @@ protected internal override void DisposeCore(bool disposing) } } - /// - /// Gets a value indicating whether the transaction is writeable. - /// - public bool Writeable => TransactionChannel.Writeable(Handle) == 1; - /// /// Commit and dispose provided read-write transaction. /// @@ -70,7 +71,7 @@ public Doc[] SubDocs() var handle = TransactionChannel.SubDocs(Handle, out var length); var handles = MemoryReader.ReadStructs(handle, length); - return handles.Select(h => doc.GetDoc(h, false)).ToArray(); + return handles.Select(h => doc.GetDoc(h, isDeleted: false)).ToArray(); } /// @@ -119,7 +120,8 @@ public byte[] StateVectorV1() /// public byte[] StateDiffV1(byte[] stateVector) { - var handle = TransactionChannel.StateDiffV1(Handle, stateVector, (uint)(stateVector != null ? stateVector.Length : 0), out var length); + var handle = TransactionChannel.StateDiffV1( + Handle, stateVector, (uint) (stateVector != null ? stateVector.Length : 0), out var length); return MemoryReader.ReadAndDestroyBytes(handle, length); } @@ -150,7 +152,7 @@ public byte[] StateDiffV1(byte[] stateVector) /// public byte[] StateDiffV2(byte[] stateVector) { - var handle = TransactionChannel.StateDiffV2(Handle, stateVector, (uint)stateVector.Length, out var length); + var handle = TransactionChannel.StateDiffV2(Handle, stateVector, (uint) stateVector.Length, out var length); return MemoryReader.ReadAndDestroyBytes(handle, length); } @@ -166,7 +168,7 @@ public byte[] StateDiffV2(byte[] stateVector) /// The result of the update operation. public TransactionUpdateResult ApplyV1(byte[] stateDiff) { - return (TransactionUpdateResult)TransactionChannel.ApplyV1(Handle, stateDiff, (uint)stateDiff.Length); + return (TransactionUpdateResult) TransactionChannel.ApplyV1(Handle, stateDiff, (uint) stateDiff.Length); } /// @@ -180,7 +182,7 @@ public TransactionUpdateResult ApplyV1(byte[] stateDiff) /// The result of the update operation. public TransactionUpdateResult ApplyV2(byte[] stateDiff) { - return (TransactionUpdateResult)TransactionChannel.ApplyV2(Handle, stateDiff, (uint)stateDiff.Length); + return (TransactionUpdateResult) TransactionChannel.ApplyV2(Handle, stateDiff, (uint) stateDiff.Length); } /// @@ -212,7 +214,8 @@ public byte[] Snapshot() /// null will be returned. /// /// - /// The is generated by . This is useful to generate a past view + /// The is generated by . This is useful to generate a past + /// view /// of the document. /// /// @@ -231,7 +234,7 @@ public byte[] Snapshot() var handle = TransactionChannel.EncodeStateFromSnapshotV1( Handle, snapshot, - (uint)snapshot.Length, + (uint) snapshot.Length, out var length); return handle != nint.Zero ? MemoryReader.ReadAndDestroyBytes(handle, length) : null; @@ -248,7 +251,8 @@ public byte[] Snapshot() /// null will be returned. /// /// - /// The is generated by . This is useful to generate a past view + /// The is generated by . This is useful to generate a past + /// view /// of the document. /// /// @@ -267,7 +271,7 @@ public byte[] Snapshot() var handle = TransactionChannel.EncodeStateFromSnapshotV2( Handle, snapshot, - (uint)snapshot.Length, + (uint) snapshot.Length, out var length); return handle != nint.Zero ? MemoryReader.ReadAndDestroyBytes(handle, length) : null; @@ -286,7 +290,7 @@ public byte[] Snapshot() { var handle = GetWithKind(name, BranchKind.Array); - return handle != nint.Zero ? doc.GetArray(handle, false) : null; + return handle != nint.Zero ? doc.GetArray(handle, isDeleted: false) : null; } /// @@ -302,7 +306,7 @@ public byte[] Snapshot() { var handle = GetWithKind(name, BranchKind.Map); - return handle != nint.Zero ? doc.GetMap(handle, false) : null; + return handle != nint.Zero ? doc.GetMap(handle, isDeleted: false) : null; } /// @@ -318,11 +322,12 @@ public byte[] Snapshot() { var handle = GetWithKind(name, BranchKind.Text); - return handle != nint.Zero ? doc.GetText(handle, false) : null; + return handle != nint.Zero ? doc.GetText(handle, isDeleted: false) : null; } /// - /// Returns the at the root level, identified by , or + /// Returns the at the root level, identified by , + /// or /// null if no entry was defined under before. /// /// The name of the instance to get. @@ -334,7 +339,7 @@ public byte[] Snapshot() { var handle = GetWithKind(name, BranchKind.XmlElement); - return handle != nint.Zero ? doc.GetXmlElement(handle, false) : null; + return handle != nint.Zero ? doc.GetXmlElement(handle, isDeleted: false) : null; } /// @@ -350,7 +355,7 @@ public byte[] Snapshot() { var handle = GetWithKind(name, BranchKind.XmlText); - return handle != nint.Zero ? doc.GetXmlText(handle, false) : null; + return handle != nint.Zero ? doc.GetXmlText(handle, isDeleted: false) : null; } private nint GetWithKind(string name, BranchKind expectedKind) @@ -363,10 +368,10 @@ private nint GetWithKind(string name, BranchKind expectedKind) return nint.Zero; } - var branchKind = (BranchKind)BranchChannel.Kind(branchHandle); + var branchKind = (BranchKind) BranchChannel.Kind(branchHandle); if (branchKind != expectedKind) { - ThrowHelper.YDotnet($"Expected '{expectedKind}', got '{branchKind}'."); + throw new YDotNetException($"Expected '{expectedKind}', got '{branchKind}'."); } return branchHandle; From 4ffae979c61e2386a490dd13b40124d43fefec93 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sat, 21 Oct 2023 11:42:47 -0300 Subject: [PATCH 137/186] refactor(improvements): format and update `TypeCache` --- YDotNet/Infrastructure/TypeCache.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/YDotNet/Infrastructure/TypeCache.cs b/YDotNet/Infrastructure/TypeCache.cs index 5ae6e613..bd39b2f7 100644 --- a/YDotNet/Infrastructure/TypeCache.cs +++ b/YDotNet/Infrastructure/TypeCache.cs @@ -9,7 +9,7 @@ public T GetOrAdd(nint handle, Func factory) { if (handle == nint.Zero) { - ThrowHelper.ArgumentException("Cannot create object for null handle.", nameof(handle)); + throw new ArgumentException("Cannot create object for null handle.", nameof(handle)); } Cleanup(); @@ -18,8 +18,7 @@ public T GetOrAdd(nint handle, Func factory) { if (item is not T typed) { - ThrowHelper.YDotnet($"Expected {typeof(T)}, got {item.GetType()}"); - return default!; + throw new YDotNetException($"Expected {typeof(T)}, got {item.GetType()}"); } return typed; @@ -54,6 +53,7 @@ private void Cleanup() } } + // TODO [LSViana] Check with Sebastian why this method isn't used anywhere. public void Delete(nint handle) { if (cache.TryGetValue(handle, out var weakRef)) From 93faab7abe5616385babcbcfd80d810bf95b6fe3 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sat, 21 Oct 2023 11:43:55 -0300 Subject: [PATCH 138/186] refactor(improvements): format, simplify, and update `Output` --- YDotNet/Document/Cells/Output.cs | 42 +++++++------------------------- 1 file changed, 9 insertions(+), 33 deletions(-) diff --git a/YDotNet/Document/Cells/Output.cs b/YDotNet/Document/Cells/Output.cs index ae9396e2..1fc2eed9 100644 --- a/YDotNet/Document/Cells/Output.cs +++ b/YDotNet/Document/Cells/Output.cs @@ -3,6 +3,7 @@ using YDotNet.Document.Types.XmlElements; using YDotNet.Document.Types.XmlTexts; using YDotNet.Infrastructure; +using YDotNet.Infrastructure.Extensions; using YDotNet.Native.Cells.Outputs; using Array = YDotNet.Document.Types.Arrays.Array; @@ -124,55 +125,30 @@ internal static Output CreateAndRelease(nint handle, Doc doc) return result; } - private static object? BuildValue(nint handle, uint length, Doc doc, bool isDeleted, OutputTag type) + private static object? BuildValue(nint handle, uint length, Doc doc, bool isDeleted, OutputTag tag) { - switch (type) + switch (tag) { case OutputTag.Bool: - { - var value = OutputChannel.Boolean(handle).Checked(); - - return MemoryReader.ReadStruct(value) == 1; - } + return MemoryReader.ReadStruct(OutputChannel.Boolean(handle).Checked()) == 1; case OutputTag.Double: - { - var value = OutputChannel.Double(handle).Checked(); - - return MemoryReader.ReadStruct(value); - } + return MemoryReader.ReadStruct(OutputChannel.Double(handle).Checked()); case OutputTag.Long: - { - var value = OutputChannel.Long(handle).Checked(); - - return MemoryReader.ReadStruct(value); - } + return MemoryReader.ReadStruct(OutputChannel.Long(handle).Checked()); case OutputTag.String: - { - MemoryReader.TryReadUtf8String(OutputChannel.String(handle), out var result); - - return result; - } + return MemoryReader.ReadUtf8String(OutputChannel.String(handle).Checked()); case OutputTag.Bytes: - { - var bytesHandle = OutputChannel.Bytes(handle).Checked(); - var bytesArray = MemoryReader.ReadBytes(OutputChannel.Bytes(handle), length); - - return bytesArray; - } + return MemoryReader.ReadBytes(OutputChannel.Bytes(handle).Checked(), length); case OutputTag.JsonArray: - { return new JsonArray(handle, length, doc, isDeleted); - } case OutputTag.JsonObject: - { return new JsonObject(handle, length, doc, isDeleted); - } case OutputTag.Array: return doc.GetArray(OutputChannel.Array(handle), isDeleted); @@ -193,7 +169,7 @@ internal static Output CreateAndRelease(nint handle, Doc doc) return doc.GetDoc(OutputChannel.Doc(handle), isDeleted); default: - return null; + throw new YDotNetException($"Unsupported OutputTag value: {tag}"); } } From 2df883f252f5945b79ff148401f6acb7943b7353 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sat, 21 Oct 2023 11:44:20 -0300 Subject: [PATCH 139/186] refactor(improvements): update `using` statements of `MemoryReader` --- YDotNet/Infrastructure/MemoryReader.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/YDotNet/Infrastructure/MemoryReader.cs b/YDotNet/Infrastructure/MemoryReader.cs index 82d4af6c..7ddc9b00 100644 --- a/YDotNet/Infrastructure/MemoryReader.cs +++ b/YDotNet/Infrastructure/MemoryReader.cs @@ -1,5 +1,6 @@ using System.Runtime.InteropServices; using System.Text; +using YDotNet.Infrastructure.Extensions; using YDotNet.Native; using YDotNet.Native.Types; From cb6159bf9700c3029c93d66e79931d8d2f2e0061 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sat, 21 Oct 2023 11:45:45 -0300 Subject: [PATCH 140/186] refactor(improvements): format and remove unused values from `enum OutputTag` --- YDotNet/Document/Cells/Output.cs | 4 +-- YDotNet/Document/Cells/OutputTag.cs | 48 +++++++++++++---------------- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/YDotNet/Document/Cells/Output.cs b/YDotNet/Document/Cells/Output.cs index 1fc2eed9..83d32d7e 100644 --- a/YDotNet/Document/Cells/Output.cs +++ b/YDotNet/Document/Cells/Output.cs @@ -47,7 +47,7 @@ internal Output(nint handle, Doc doc, bool isDeleted) /// Gets the value. /// /// Value is not a . - public bool Boolean => GetValue(OutputTag.Bool); + public bool Boolean => GetValue(OutputTag.Boolean); #pragma warning restore SA1623 /// @@ -129,7 +129,7 @@ internal static Output CreateAndRelease(nint handle, Doc doc) { switch (tag) { - case OutputTag.Bool: + case OutputTag.Boolean: return MemoryReader.ReadStruct(OutputChannel.Boolean(handle).Checked()) == 1; case OutputTag.Double: diff --git a/YDotNet/Document/Cells/OutputTag.cs b/YDotNet/Document/Cells/OutputTag.cs index fda00866..b3d18708 100644 --- a/YDotNet/Document/Cells/OutputTag.cs +++ b/YDotNet/Document/Cells/OutputTag.cs @@ -1,92 +1,88 @@ namespace YDotNet.Document.Cells; /// -/// The type of an output. +/// Represents the type of value stored inside of an cell. /// +/// +/// This can be used as a safety check to only read the correct type of value from the cell. +/// public enum OutputTag { /// - /// No defined. + /// Represents a cell with a value. /// - NotSet = -99, + Boolean = -8, /// - /// Flag used by to tag boolean values. - /// - Bool = -8, - - /// - /// Flag used by to tag floating point numbers. + /// Represents a cell with a value. /// Double = -7, /// - /// Flag used by to tag 64-bit integer numbers. + /// Represents a cell with a value. /// Long = -6, /// - /// Flag used by to tag strings. + /// Represents a cell with a value. /// String = -5, /// - /// Flag used by to tag binary content. + /// Represents a cell with a array value. /// Bytes = -4, /// - /// Flag used by to tag embedded JSON-like arrays of values. + /// Represents a cell with an array of value. /// JsonArray = -3, /// - /// Flag used by to tag embedded JSON-like maps of key-value pairs. + /// Represents a cell with a dictionary of value. /// JsonObject = -2, /// - /// Flag used by to tag JSON-like null values. + /// Represents a cell with the null value. /// Null = -1, /// - /// Flag used by to tag JSON-like undefined values. + /// Represents a cell with the undefined value. /// Undefined = 0, /// - /// Flag used by to tag content, which is an `YArray` shared type. + /// Represents a cell with an value. /// Array = 1, /// - /// Flag used by to tag content, which is an `YMap` shared type. + /// Represents a cell with an value. /// Map = 2, /// - /// Flag used by to tag content, which is an `YText` shared type. + /// Represents a cell with an value. /// Text = 3, /// - /// Flag used by to tag content, which is an `YXmlElement` shared type. + /// Represents a cell with an value. /// XmlElement = 4, /// - /// Flag used by to tag content, which is an `YXmlText` shared type. + /// Represents a cell with an value. /// XmlText = 5, - /// - /// Flag used by to tag content, which is an `YXmlFragment` shared type. - /// - XmlFragment = 6, + // The following constant is commented because it's not exposed by `Input` or `Output`. + // XmlFragment = 6, /// - /// Flag used by to tag content, which is an `YDoc` shared type. + /// Represents a cell with an value. /// Doc = 7 } From f64f1ef53d60e407b58f02e0577907bb27507fac Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 22 Oct 2023 10:16:08 -0300 Subject: [PATCH 141/186] refactor(improvements): remove unnecessary `continue` statement from `EventPublisher.Publish()` --- YDotNet/Document/Events/EventPublisher.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/YDotNet/Document/Events/EventPublisher.cs b/YDotNet/Document/Events/EventPublisher.cs index e0cc4adc..c1b5d5fb 100644 --- a/YDotNet/Document/Events/EventPublisher.cs +++ b/YDotNet/Document/Events/EventPublisher.cs @@ -31,8 +31,7 @@ public void Publish(TEvent @event) } catch { - // Exceptions could have unknown consequences in the rust part. - continue; + // Exceptions could have unknown consequences in the Rust part. } } } From f687af6db2ad841f6797054de86af5677f43cf43 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 22 Oct 2023 10:21:57 -0300 Subject: [PATCH 142/186] refactor(improvements): fix typos and format `EventSubscriber` --- YDotNet/Document/Events/EventSubscriber.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/YDotNet/Document/Events/EventSubscriber.cs b/YDotNet/Document/Events/EventSubscriber.cs index 11a15e96..c132d1ab 100644 --- a/YDotNet/Document/Events/EventSubscriber.cs +++ b/YDotNet/Document/Events/EventSubscriber.cs @@ -2,9 +2,9 @@ namespace YDotNet.Document.Events; internal class EventSubscriber : IEventSubscriber { - private readonly EventPublisher publisher = new(); private readonly EventManager manager; private readonly nint owner; + private readonly EventPublisher publisher = new(); private readonly Func, (uint Handle, object Callback)> subscribe; private readonly Action unsubscribe; private (uint Handle, object? Callback) nativeSubscription; @@ -41,10 +41,7 @@ public IDisposable Subscribe(Action handler) publisher.Subscribe(handler); - return new DelegateDisposable(() => - { - Unsubscribe(handler); - }); + return new DelegateDisposable(() => Unsubscribe(handler)); } private void Unsubscribe(Action handler) @@ -56,7 +53,7 @@ private void Unsubscribe(Action handler) private void UnsubscribeWhenSubscribed() { - // If this is the last subscription we can unubscribe from the native source again. + // If this is the last subscription, we can unsubscribe from the native source again. if (publisher.Count == 0 && nativeSubscription.Callback != null) { unsubscribe(owner, nativeSubscription.Handle); From b081e75d2c32ea02a19d590f15ace82e71299821 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 22 Oct 2023 10:23:27 -0300 Subject: [PATCH 143/186] refactor(improvements): format `Doc` and `SubDocsEvent` --- YDotNet/Document/Doc.cs | 127 ++++++++++++------------ YDotNet/Document/Events/SubDocsEvent.cs | 11 +- 2 files changed, 68 insertions(+), 70 deletions(-) diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index c5195b28..dfc67517 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -33,13 +33,13 @@ namespace YDotNet.Document; /// public class Doc : TypeBase, IDisposable { - private readonly TypeCache typeCache = new(); + private readonly EventSubscriber onAfterTransaction; private readonly EventSubscriber onClear; + private readonly EventSubscriber onSubDocs; private readonly EventSubscriber onUpdateV1; private readonly EventSubscriber onUpdateV2; - private readonly EventSubscriber onAfterTransaction; - private readonly EventSubscriber onSubDocs; private readonly Doc? parent; + private readonly TypeCache typeCache = new(); private int openTransactions; /// @@ -59,7 +59,7 @@ public Doc() /// /// The options to be used when initializing this document. public Doc(DocOptions options) - : this(CreateDoc(options), null, false) + : this(CreateDoc(options), parent: null, isDeleted: false) { } @@ -74,7 +74,7 @@ internal Doc(nint handle, Doc? parent, bool isDeleted) (doc, action) => { DocChannel.ObserveClearCallback callback = - (_, doc) => action(new ClearEvent(GetDoc(doc, false))); + (_, doc) => action(new ClearEvent(GetDoc(doc, isDeleted: false))); return (DocChannel.ObserveClear(doc, nint.Zero, callback), callback); }, @@ -110,7 +110,8 @@ internal Doc(nint handle, Doc? parent, bool isDeleted) (doc, action) => { DocChannel.ObserveAfterTransactionCallback callback = - (_, ev) => action(new AfterTransactionEvent(MemoryReader.ReadStruct(ev))); + (_, ev) => action( + new AfterTransactionEvent(MemoryReader.ReadStruct(ev))); return (DocChannel.ObserveAfterTransaction(doc, nint.Zero, callback), callback); }, @@ -131,44 +132,6 @@ internal Doc(nint handle, Doc? parent, bool isDeleted) Handle = handle; } - private static nint CreateDoc(DocOptions options) - { - return DocChannel.NewWithOptions(options.ToNative()); - } - - /// - /// Finalizes an instance of the class. - /// - ~Doc() - { - Dispose(false); - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private void Dispose(bool disposing) - { - if (IsDisposed) - { - return; - } - - MarkDisposed(); - - if (disposing) - { - // Clears all active subscriptions that have not been closed yet. - EventManager.Clear(); - } - - DocChannel.Destroy(Handle); - } - /// /// Gets the unique client identifier of this instance. /// @@ -199,7 +162,8 @@ public string Guid /// Gets a value indicating whether this instance requested a data load. /// /// - /// This flag is often used by the parent instance to check if this instance requested a data load. + /// This flag is often used by the parent instance to check if this instance + /// requested a data load. /// public bool ShouldLoad { @@ -215,7 +179,8 @@ public bool ShouldLoad /// Gets a value indicating whether this instance will auto load. /// /// - /// Auto loaded nested instances automatically send a load request to their parent instances. + /// Auto loaded nested instances automatically send a load request to their parent + /// instances. /// public bool AutoLoad { @@ -246,7 +211,45 @@ public string? CollectionId internal nint Handle { get; } - internal EventManager EventManager { get; } = new EventManager(); + internal EventManager EventManager { get; } = new(); + + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + private static nint CreateDoc(DocOptions options) + { + return DocChannel.NewWithOptions(options.ToNative()); + } + + /// + /// Finalizes an instance of the class. + /// + ~Doc() + { + Dispose(disposing: false); + } + + private void Dispose(bool disposing) + { + if (IsDisposed) + { + return; + } + + MarkDisposed(); + + if (disposing) + { + // Clears all active subscriptions that have not been closed yet. + EventManager.Clear(); + } + + DocChannel.Destroy(Handle); + } /// /// Gets or creates a new shared data type instance as a root-level @@ -265,7 +268,7 @@ public Text Text(string name) using var unsafeName = MemoryWriter.WriteUtf8String(name); var handle = DocChannel.Text(Handle, unsafeName.Handle); - return GetText(handle, false); + return GetText(handle, isDeleted: false); } /// @@ -285,7 +288,7 @@ public Map Map(string name) using var unsafeName = MemoryWriter.WriteUtf8String(name); var handle = DocChannel.Map(Handle, unsafeName.Handle); - return GetMap(handle, false); + return GetMap(handle, isDeleted: false); } /// @@ -305,7 +308,7 @@ public Array Array(string name) using var unsafeName = MemoryWriter.WriteUtf8String(name); var handle = DocChannel.Array(Handle, unsafeName.Handle); - return GetArray(handle, false); + return GetArray(handle, isDeleted: false); } /// @@ -325,7 +328,7 @@ public XmlElement XmlElement(string name) using var unsafeName = MemoryWriter.WriteUtf8String(name); var handle = DocChannel.XmlElement(Handle, unsafeName.Handle); - return GetXmlElement(handle, false); + return GetXmlElement(handle, isDeleted: false); } /// @@ -345,15 +348,7 @@ public XmlText XmlText(string name) using var unsafeName = MemoryWriter.WriteUtf8String(name); var handle = DocChannel.XmlText(Handle, unsafeName.Handle); - return GetXmlText(handle, false); - } - - internal void ThrowIfOpenTransaction() - { - if (openTransactions > 0) - { - ThrowHelper.PendingTransaction(); - } + return GetXmlText(handle, isDeleted: false); } /// @@ -366,7 +361,7 @@ public Transaction WriteTransaction(byte[]? origin = null) { ThrowIfDisposed(); - var handle = DocChannel.WriteTransaction(Handle, (uint)(origin?.Length ?? 0), origin); + var handle = DocChannel.WriteTransaction(Handle, (uint) (origin?.Length ?? 0), origin); if (handle == nint.Zero) { @@ -505,7 +500,7 @@ internal Doc GetDoc(nint handle, bool isDeleted) if (!isDeleted) { - // Prevent the sub document to be released while we are working with it. + // Prevent the sub-document to be released while we are working with it. handle = DocChannel.Clone(handle); } @@ -554,6 +549,14 @@ private T GetOrAdd(nint handle, Func factory) where T : ITypeBa return doc.typeCache.GetOrAdd(handle, h => factory(h, doc)); } + private void ThrowIfOpenTransaction() + { + if (openTransactions > 0) + { + ThrowHelper.PendingTransaction(); + } + } + private Doc GetRootDoc() { return parent?.GetRootDoc() ?? this; diff --git a/YDotNet/Document/Events/SubDocsEvent.cs b/YDotNet/Document/Events/SubDocsEvent.cs index 33806739..224dbe90 100644 --- a/YDotNet/Document/Events/SubDocsEvent.cs +++ b/YDotNet/Document/Events/SubDocsEvent.cs @@ -9,14 +9,9 @@ public class SubDocsEvent { internal SubDocsEvent(SubDocsEventNative native, Doc doc) { - Added = native.Added() - .Select(h => doc.GetDoc(h, false)).ToList(); - - Removed = native.Removed() - .Select(h => doc.GetDoc(h, true)).ToList(); - - Loaded = native.Loaded() - .Select(h => doc.GetDoc(h, false)).ToList(); + Added = native.Added().Select(h => doc.GetDoc(h, isDeleted: false)).ToList(); + Removed = native.Removed().Select(h => doc.GetDoc(h, isDeleted: true)).ToList(); + Loaded = native.Loaded().Select(h => doc.GetDoc(h, isDeleted: false)).ToList(); } /// From 9d0230990cd019366df9ca3644b664a538edeb16 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 22 Oct 2023 11:01:36 -0300 Subject: [PATCH 144/186] refactor(improvements): add missing references to use `Checked()` extension and fixes for usages of `OutputTag.Boolean` --- Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs | 2 +- Tests/YDotNet.Tests.Unit/Maps/GetTests.cs | 10 ++-- YDotNet.Extensions/YDotNetExtensions.cs | 19 +++--- YDotNet/Document/Cells/JsonObject.cs | 1 + YDotNet/Document/StickyIndexes/StickyIndex.cs | 15 ++--- YDotNet/Document/Types/Arrays/Array.cs | 16 +++-- .../Types/Arrays/Events/ArrayEvent.cs | 54 +++++++++-------- .../Document/Types/Maps/Events/MapEvent.cs | 48 ++++++++------- YDotNet/Document/Types/Maps/Map.cs | 1 + .../Document/Types/Texts/Events/TextEvent.cs | 54 +++++++++-------- YDotNet/Document/Types/Texts/Text.cs | 11 +++- .../XmlElements/Events/XmlElementEvent.cs | 59 ++++++++++--------- .../Document/Types/XmlElements/XmlElement.cs | 8 +-- .../Types/XmlTexts/Events/XmlTextEvent.cs | 46 ++++++++------- YDotNet/Document/Types/XmlTexts/XmlText.cs | 11 +++- 15 files changed, 197 insertions(+), 158 deletions(-) diff --git a/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs b/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs index 1f7660d0..3c21d64d 100644 --- a/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs +++ b/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs @@ -36,7 +36,7 @@ public void GetAtBeginning() // Assert Assert.That(output, Is.Not.Null); - Assert.That(output.Tag, Is.EqualTo(OutputTag.Bool)); + Assert.That(output.Tag, Is.EqualTo(OutputTag.Boolean)); } [Test] diff --git a/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs b/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs index 320a83e4..ffd52f12 100644 --- a/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs +++ b/Tests/YDotNet.Tests.Unit/Maps/GetTests.cs @@ -95,7 +95,7 @@ public void GetBytes() // Assert Assert.That(value1, Is.EqualTo(new byte[] { 2, 4, 6, 9 })); - Assert.That(value2.Tag, Is.EqualTo(OutputTag.Bool)); + Assert.That(value2.Tag, Is.EqualTo(OutputTag.Boolean)); } [Test] @@ -121,7 +121,7 @@ public void GetCollection() Assert.That(value1.Count, Is.EqualTo(expected: 2)); Assert.That(value1[0].Long, Is.EqualTo(expected: 2469)); Assert.That(value1[1].Long, Is.EqualTo(expected: -420L)); - Assert.That(value2.Tag, Is.EqualTo(OutputTag.Bool)); + Assert.That(value2.Tag, Is.EqualTo(OutputTag.Boolean)); } [Test] @@ -146,7 +146,7 @@ public void GetObject() Assert.That(value1.Keys.Count, Is.EqualTo(expected: 2)); Assert.That(value1["star-⭐"].Long, Is.EqualTo(expected: 2469)); Assert.That(value1["moon-🌕"].Long, Is.EqualTo(expected: -420L)); - Assert.That(value2.Tag, Is.EqualTo(OutputTag.Bool)); + Assert.That(value2.Tag, Is.EqualTo(OutputTag.Boolean)); } [Test] @@ -207,7 +207,7 @@ public void GetText() // Assert Assert.That(value1, Is.Not.Null); Assert.That(value1.String(transaction), Is.EqualTo("Lucas")); - Assert.That(value2.Tag, Is.EqualTo(OutputTag.Bool)); + Assert.That(value2.Tag, Is.EqualTo(OutputTag.Boolean)); } [Test] @@ -257,7 +257,7 @@ public void GetMap() Assert.That(value1.Length(transaction), Is.EqualTo(expected: 2)); Assert.That(value1.Get(transaction, "value1-1").Long, Is.EqualTo(expected: 2469L)); Assert.That(value1.Get(transaction, "value1-2").Long, Is.EqualTo(expected: -420L)); - Assert.That(value2.Tag, Is.EqualTo(OutputTag.Bool)); + Assert.That(value2.Tag, Is.EqualTo(OutputTag.Boolean)); } [Test] diff --git a/YDotNet.Extensions/YDotNetExtensions.cs b/YDotNet.Extensions/YDotNetExtensions.cs index 30f7b3fa..deb03371 100644 --- a/YDotNet.Extensions/YDotNetExtensions.cs +++ b/YDotNet.Extensions/YDotNetExtensions.cs @@ -1,4 +1,3 @@ -using System.Formats.Asn1; using System.Text; using System.Text.Json; using YDotNet.Document; @@ -40,9 +39,9 @@ static Input ConvertValue(JsonElement element) case JsonValueKind.Number: return Input.Double(element.GetDouble()); case JsonValueKind.True: - return Input.Boolean(true); + return Input.Boolean(value: true); case JsonValueKind.False: - return Input.Boolean(false); + return Input.Boolean(value: false); case JsonValueKind.Null: return Input.Null(); default: @@ -53,8 +52,8 @@ static Input ConvertValue(JsonElement element) public static T To(this Output output, Doc doc) { - using var transaction = doc.ReadTransaction() - ?? throw new InvalidOperationException("Failed to open transaction."); + using var transaction = + doc.ReadTransaction() ?? throw new InvalidOperationException("Failed to open transaction."); return output.To(transaction); } @@ -119,7 +118,8 @@ static void WriteMap(Map map, Utf8JsonWriter jsonWriter, Transaction transaction { jsonWriter.WriteStartArray(); - foreach (var property in map.Iterate(transaction) ?? throw new InvalidOperationException("Failed to iterate array.")) + foreach (var property in map.Iterate(transaction) ?? + throw new InvalidOperationException("Failed to iterate array.")) { WriteProperty(property.Key, property.Value, jsonWriter, transaction); } @@ -131,7 +131,8 @@ static void WriteArray(Array array, Utf8JsonWriter jsonWriter, Transaction trans { jsonWriter.WriteStartArray(); - foreach (var item in array.Iterate(transaction) ?? throw new InvalidOperationException("Failed to iterate array.")) + foreach (var item in array.Iterate(transaction) ?? + throw new InvalidOperationException("Failed to iterate array.")) { WriteValue(item, jsonWriter, transaction); } @@ -150,9 +151,7 @@ static void WriteValue(Output output, Utf8JsonWriter jsonWriter, Transaction tra { switch (output.Tag) { - case OutputTag.NotSet: - break; - case OutputTag.Bool: + case OutputTag.Boolean: jsonWriter.WriteBooleanValue(output.Boolean); break; case OutputTag.Double: diff --git a/YDotNet/Document/Cells/JsonObject.cs b/YDotNet/Document/Cells/JsonObject.cs index 96412871..b8d59ca3 100644 --- a/YDotNet/Document/Cells/JsonObject.cs +++ b/YDotNet/Document/Cells/JsonObject.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using YDotNet.Infrastructure; +using YDotNet.Infrastructure.Extensions; using YDotNet.Native.Cells.Outputs; using YDotNet.Native.Types.Maps; diff --git a/YDotNet/Document/StickyIndexes/StickyIndex.cs b/YDotNet/Document/StickyIndexes/StickyIndex.cs index 60492833..501a1efc 100644 --- a/YDotNet/Document/StickyIndexes/StickyIndex.cs +++ b/YDotNet/Document/StickyIndexes/StickyIndex.cs @@ -1,6 +1,7 @@ using YDotNet.Document.Transactions; using YDotNet.Document.Types.Branches; using YDotNet.Infrastructure; +using YDotNet.Infrastructure.Extensions; using YDotNet.Native.StickyIndex; namespace YDotNet.Document.StickyIndexes; @@ -21,15 +22,15 @@ internal StickyIndex(nint handle) { } - /// - protected internal override void DisposeCore(bool disposing) - { - } - /// /// Gets the of the current . /// - public StickyAssociationType AssociationType => (StickyAssociationType)StickyIndexChannel.AssociationType(Handle); + public StickyAssociationType AssociationType => (StickyAssociationType) StickyIndexChannel.AssociationType(Handle); + + /// + protected internal override void DisposeCore(bool disposing) + { + } /// /// Creates a from the result of . @@ -38,7 +39,7 @@ protected internal override void DisposeCore(bool disposing) /// The represented by the provided array. public static StickyIndex Decode(byte[] encoded) { - var handle = StickyIndexChannel.Decode(encoded, (uint)encoded.Length); + var handle = StickyIndexChannel.Decode(encoded, (uint) encoded.Length); return new StickyIndex(handle.Checked()); } diff --git a/YDotNet/Document/Types/Arrays/Array.cs b/YDotNet/Document/Types/Arrays/Array.cs index 8dc1a9cd..7d19877a 100644 --- a/YDotNet/Document/Types/Arrays/Array.cs +++ b/YDotNet/Document/Types/Arrays/Array.cs @@ -5,6 +5,7 @@ using YDotNet.Document.Types.Arrays.Events; using YDotNet.Document.Types.Branches; using YDotNet.Infrastructure; +using YDotNet.Infrastructure.Extensions; using YDotNet.Native.StickyIndex; using YDotNet.Native.Types; @@ -58,7 +59,7 @@ public void InsertRange(Transaction transaction, uint index, params Input[] inpu using var unsafeInputs = MemoryWriter.WriteStructArray(inputs.Select(x => x.InputNative).ToArray()); - ArrayChannel.InsertRange(Handle, transaction.Handle, index, unsafeInputs.Handle, (uint)inputs.Length); + ArrayChannel.InsertRange(Handle, transaction.Handle, index, unsafeInputs.Handle, (uint) inputs.Length); } /// @@ -81,7 +82,8 @@ public void RemoveRange(Transaction transaction, uint index, uint length) /// The transaction that wraps this operation. /// The index to get the item. /// - /// The value at the given or null if is + /// The value at the given or null if + /// is /// outside the bounds. /// public Output? Get(Transaction transaction, uint index) @@ -140,18 +142,22 @@ public IDisposable Observe(Action action) } /// - /// Retrieves a corresponding to a given human-readable pointing into + /// Retrieves a corresponding to a given human-readable pointing + /// into /// the . /// /// The transaction that wraps this operation. /// The numeric index to place the . /// The type of the . - /// The in the with the given . + /// + /// The in the with the given + /// . + /// public StickyIndex? StickyIndex(Transaction transaction, uint index, StickyAssociationType associationType) { ThrowIfDisposed(); - var handle = StickyIndexChannel.FromIndex(Handle, transaction.Handle, index, (sbyte)associationType); + var handle = StickyIndexChannel.FromIndex(Handle, transaction.Handle, index, (sbyte) associationType); return handle != nint.Zero ? new StickyIndex(handle) : null; } diff --git a/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs b/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs index 944cfb2e..e101d9a4 100644 --- a/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs +++ b/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs @@ -1,5 +1,6 @@ using YDotNet.Document.Types.Events; using YDotNet.Infrastructure; +using YDotNet.Infrastructure.Extensions; using YDotNet.Native.Types; namespace YDotNet.Document.Types.Arrays.Events; @@ -9,45 +10,42 @@ namespace YDotNet.Document.Types.Arrays.Events; /// public class ArrayEvent : UnmanagedResource { - private readonly Lazy path; private readonly Lazy delta; + private readonly Lazy path; private readonly Lazy target; internal ArrayEvent(nint handle, Doc doc) : base(handle) { - path = new Lazy(() => - { - ThrowIfDisposed(); - - var pathHandle = ArrayChannel.ObserveEventPath(handle, out var length).Checked(); + path = new Lazy( + () => + { + ThrowIfDisposed(); - return new EventPath(pathHandle, length); - }); + var pathHandle = ArrayChannel.ObserveEventPath(handle, out var length).Checked(); - delta = new Lazy(() => - { - ThrowIfDisposed(); + return new EventPath(pathHandle, length); + }); - var deltaHandle = ArrayChannel.ObserveEventDelta(handle, out var length).Checked(); + delta = new Lazy( + () => + { + ThrowIfDisposed(); - return new EventChanges(deltaHandle, length, doc); - }); + var deltaHandle = ArrayChannel.ObserveEventDelta(handle, out var length).Checked(); - target = new Lazy(() => - { - ThrowIfDisposed(); + return new EventChanges(deltaHandle, length, doc); + }); - var targetHandle = ArrayChannel.ObserveEventTarget(handle).Checked(); + target = new Lazy( + () => + { + ThrowIfDisposed(); - return doc.GetArray(targetHandle, false); - }); - } + var targetHandle = ArrayChannel.ObserveEventTarget(handle).Checked(); - /// - protected internal override void DisposeCore(bool disposing) - { - // The event has no explicit garbage collection, but is released automatically after the event has been completed. + return doc.GetArray(targetHandle, isDeleted: false); + }); } /// @@ -67,4 +65,10 @@ protected internal override void DisposeCore(bool disposing) /// /// The target of the event. public Array Target => target.Value; + + /// + protected internal override void DisposeCore(bool disposing) + { + // The event has no explicit garbage collection, but is released automatically after the event has been completed. + } } diff --git a/YDotNet/Document/Types/Maps/Events/MapEvent.cs b/YDotNet/Document/Types/Maps/Events/MapEvent.cs index 1679c503..09bbdea0 100644 --- a/YDotNet/Document/Types/Maps/Events/MapEvent.cs +++ b/YDotNet/Document/Types/Maps/Events/MapEvent.cs @@ -1,5 +1,6 @@ using YDotNet.Document.Types.Events; using YDotNet.Infrastructure; +using YDotNet.Infrastructure.Extensions; using YDotNet.Native.Types; using YDotNet.Native.Types.Maps; @@ -10,39 +11,36 @@ namespace YDotNet.Document.Types.Maps.Events; /// public class MapEvent : UnmanagedResource { - private readonly Lazy path; private readonly Lazy keys; + private readonly Lazy path; private readonly Lazy target; internal MapEvent(nint handle, Doc doc) : base(handle) { - path = new Lazy(() => - { - var pathHandle = ArrayChannel.ObserveEventPath(handle, out var length).Checked(); - - return new EventPath(pathHandle, length); - }); + path = new Lazy( + () => + { + var pathHandle = ArrayChannel.ObserveEventPath(handle, out var length).Checked(); - keys = new Lazy(() => - { - var keysHandle = MapChannel.ObserveEventKeys(handle, out var length).Checked(); + return new EventPath(pathHandle, length); + }); - return new EventKeys(keysHandle, length, doc); - }); + keys = new Lazy( + () => + { + var keysHandle = MapChannel.ObserveEventKeys(handle, out var length).Checked(); - target = new Lazy(() => - { - var targetHandle = MapChannel.ObserveEventTarget(handle).Checked(); + return new EventKeys(keysHandle, length, doc); + }); - return doc.GetMap(targetHandle, false); - }); - } + target = new Lazy( + () => + { + var targetHandle = MapChannel.ObserveEventTarget(handle).Checked(); - /// - protected internal override void DisposeCore(bool disposing) - { - // The event has no explicit garbage collection, but is released automatically after the event has been completed. + return doc.GetMap(targetHandle, isDeleted: false); + }); } /// @@ -62,4 +60,10 @@ protected internal override void DisposeCore(bool disposing) /// /// The target of the event. public Map Target => target.Value; + + /// + protected internal override void DisposeCore(bool disposing) + { + // The event has no explicit garbage collection, but is released automatically after the event has been completed. + } } diff --git a/YDotNet/Document/Types/Maps/Map.cs b/YDotNet/Document/Types/Maps/Map.cs index 88651e9b..a7357cff 100644 --- a/YDotNet/Document/Types/Maps/Map.cs +++ b/YDotNet/Document/Types/Maps/Map.cs @@ -4,6 +4,7 @@ using YDotNet.Document.Types.Branches; using YDotNet.Document.Types.Maps.Events; using YDotNet.Infrastructure; +using YDotNet.Infrastructure.Extensions; using YDotNet.Native.Types.Maps; namespace YDotNet.Document.Types.Maps; diff --git a/YDotNet/Document/Types/Texts/Events/TextEvent.cs b/YDotNet/Document/Types/Texts/Events/TextEvent.cs index 425b18ee..88068c0c 100644 --- a/YDotNet/Document/Types/Texts/Events/TextEvent.cs +++ b/YDotNet/Document/Types/Texts/Events/TextEvent.cs @@ -1,5 +1,6 @@ using YDotNet.Document.Types.Events; using YDotNet.Infrastructure; +using YDotNet.Infrastructure.Extensions; using YDotNet.Native.Types.Texts; namespace YDotNet.Document.Types.Texts.Events; @@ -9,45 +10,42 @@ namespace YDotNet.Document.Types.Texts.Events; /// public class TextEvent : UnmanagedResource { - private readonly Lazy path; private readonly Lazy deltas; + private readonly Lazy path; private readonly Lazy target; internal TextEvent(nint handle, Doc doc) : base(handle) { - path = new Lazy(() => - { - ThrowIfDisposed(); - - var pathHandle = TextChannel.ObserveEventPath(handle, out var length).Checked(); + path = new Lazy( + () => + { + ThrowIfDisposed(); - return new EventPath(pathHandle, length); - }); + var pathHandle = TextChannel.ObserveEventPath(handle, out var length).Checked(); - deltas = new Lazy(() => - { - ThrowIfDisposed(); + return new EventPath(pathHandle, length); + }); - var deltaHandle = TextChannel.ObserveEventDelta(handle, out var length).Checked(); + deltas = new Lazy( + () => + { + ThrowIfDisposed(); - return new EventDeltas(deltaHandle, length, doc); - }); + var deltaHandle = TextChannel.ObserveEventDelta(handle, out var length).Checked(); - target = new Lazy(() => - { - ThrowIfDisposed(); + return new EventDeltas(deltaHandle, length, doc); + }); - var targetHandle = TextChannel.ObserveEventTarget(handle).Checked(); + target = new Lazy( + () => + { + ThrowIfDisposed(); - return doc.GetText(targetHandle, false); - }); - } + var targetHandle = TextChannel.ObserveEventTarget(handle).Checked(); - /// - protected internal override void DisposeCore(bool disposing) - { - // The event has no explicit garbage collection, but is released automatically after the event has been completed. + return doc.GetText(targetHandle, isDeleted: false); + }); } /// @@ -67,4 +65,10 @@ protected internal override void DisposeCore(bool disposing) /// /// The target of the event. public Text Target => target.Value; + + /// + protected internal override void DisposeCore(bool disposing) + { + // The event has no explicit garbage collection, but is released automatically after the event has been completed. + } } diff --git a/YDotNet/Document/Types/Texts/Text.cs b/YDotNet/Document/Types/Texts/Text.cs index d0be3f64..379a60d4 100644 --- a/YDotNet/Document/Types/Texts/Text.cs +++ b/YDotNet/Document/Types/Texts/Text.cs @@ -5,6 +5,7 @@ using YDotNet.Document.Types.Branches; using YDotNet.Document.Types.Texts.Events; using YDotNet.Infrastructure; +using YDotNet.Infrastructure.Extensions; using YDotNet.Native.StickyIndex; using YDotNet.Native.Types.Texts; @@ -167,18 +168,22 @@ public IDisposable Observe(Action action) } /// - /// Retrieves a corresponding to a given human-readable pointing into + /// Retrieves a corresponding to a given human-readable pointing + /// into /// the . /// /// The transaction that wraps this operation. /// The numeric index to place the . /// The type of the . - /// The in the with the given . + /// + /// The in the with the given + /// . + /// public StickyIndex? StickyIndex(Transaction transaction, uint index, StickyAssociationType associationType) { ThrowIfDisposed(); - var handle = StickyIndexChannel.FromIndex(Handle, transaction.Handle, index, (sbyte)associationType); + var handle = StickyIndexChannel.FromIndex(Handle, transaction.Handle, index, (sbyte) associationType); return handle != nint.Zero ? new StickyIndex(handle) : null; } diff --git a/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs b/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs index 92a11a9c..34a44213 100644 --- a/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs +++ b/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs @@ -1,5 +1,6 @@ using YDotNet.Document.Types.Events; using YDotNet.Infrastructure; +using YDotNet.Infrastructure.Extensions; using YDotNet.Native.Types; namespace YDotNet.Document.Types.XmlElements.Events; @@ -9,47 +10,45 @@ namespace YDotNet.Document.Types.XmlElements.Events; /// public class XmlElementEvent : UnmanagedResource { - private readonly Lazy path; private readonly Lazy delta; private readonly Lazy keys; + private readonly Lazy path; private readonly Lazy target; internal XmlElementEvent(nint handle, Doc doc) : base(handle) { - path = new Lazy(() => - { - var pathHandle = XmlElementChannel.ObserveEventPath(handle, out var length).Checked(); - - return new EventPath(pathHandle, length); - }); + path = new Lazy( + () => + { + var pathHandle = XmlElementChannel.ObserveEventPath(handle, out var length).Checked(); - delta = new Lazy(() => - { - var deltaHandle = XmlElementChannel.ObserveEventDelta(handle, out var length).Checked(); + return new EventPath(pathHandle, length); + }); - return new EventChanges(deltaHandle, length, doc); - }); + delta = new Lazy( + () => + { + var deltaHandle = XmlElementChannel.ObserveEventDelta(handle, out var length).Checked(); - keys = new Lazy(() => - { - var keysHandle = XmlElementChannel.ObserveEventKeys(handle, out var length).Checked(); + return new EventChanges(deltaHandle, length, doc); + }); - return new EventKeys(keysHandle, length, doc); - }); + keys = new Lazy( + () => + { + var keysHandle = XmlElementChannel.ObserveEventKeys(handle, out var length).Checked(); - target = new Lazy(() => - { - var targetHandle = XmlElementChannel.ObserveEventTarget(handle).Checked(); + return new EventKeys(keysHandle, length, doc); + }); - return doc.GetXmlElement(targetHandle, false); - }); - } + target = new Lazy( + () => + { + var targetHandle = XmlElementChannel.ObserveEventTarget(handle).Checked(); - /// - protected internal override void DisposeCore(bool disposing) - { - // The event has no explicit garbage collection, but is released automatically after the event has been completed. + return doc.GetXmlElement(targetHandle, isDeleted: false); + }); } /// @@ -75,4 +74,10 @@ protected internal override void DisposeCore(bool disposing) /// /// The target of the event. public XmlElement Target => target.Value; + + /// + protected internal override void DisposeCore(bool disposing) + { + // The event has no explicit garbage collection, but is released automatically after the event has been completed. + } } diff --git a/YDotNet/Document/Types/XmlElements/XmlElement.cs b/YDotNet/Document/Types/XmlElements/XmlElement.cs index 4a3555a0..767ff5d4 100644 --- a/YDotNet/Document/Types/XmlElements/XmlElement.cs +++ b/YDotNet/Document/Types/XmlElements/XmlElement.cs @@ -6,7 +6,7 @@ using YDotNet.Document.Types.XmlElements.Trees; using YDotNet.Document.Types.XmlTexts; using YDotNet.Infrastructure; -using YDotNet.Native.Cells.Outputs; +using YDotNet.Infrastructure.Extensions; using YDotNet.Native.Types; namespace YDotNet.Document.Types.XmlElements; @@ -163,7 +163,7 @@ public XmlText InsertText(Transaction transaction, uint index) var handle = XmlElementChannel.InsertText(Handle, transaction.Handle, index); - return Doc.GetXmlText(handle, false); + return Doc.GetXmlText(handle, isDeleted: false); } /// @@ -182,7 +182,7 @@ public XmlElement InsertElement(Transaction transaction, uint index, string name var handle = XmlElementChannel.InsertElement(Handle, transaction.Handle, index, unsafeName.Handle); - return Doc.GetXmlElement(handle, false); + return Doc.GetXmlElement(handle, isDeleted: false); } /// @@ -285,7 +285,7 @@ public void RemoveRange(Transaction transaction, uint index, uint length) var handle = XmlElementChannel.Parent(Handle, transaction.Handle); - return handle != nint.Zero ? Doc.GetXmlElement(handle, false) : null; + return handle != nint.Zero ? Doc.GetXmlElement(handle, isDeleted: false) : null; } /// diff --git a/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs b/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs index 19983601..3ddcefb0 100644 --- a/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs +++ b/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs @@ -1,5 +1,6 @@ using YDotNet.Document.Types.Events; using YDotNet.Infrastructure; +using YDotNet.Infrastructure.Extensions; using YDotNet.Native.Types; namespace YDotNet.Document.Types.XmlTexts.Events; @@ -16,32 +17,29 @@ public class XmlTextEvent : UnmanagedResource internal XmlTextEvent(nint handle, Doc doc) : base(handle) { - delta = new Lazy(() => - { - var deltaHandle = XmlTextChannel.ObserveEventDelta(handle, out var length).Checked(); + delta = new Lazy( + () => + { + var deltaHandle = XmlTextChannel.ObserveEventDelta(handle, out var length).Checked(); - return new EventDeltas(deltaHandle, length, doc); - }); + return new EventDeltas(deltaHandle, length, doc); + }); - keys = new Lazy(() => - { - var keysHandle = XmlTextChannel.ObserveEventKeys(handle, out var length).Checked(); + keys = new Lazy( + () => + { + var keysHandle = XmlTextChannel.ObserveEventKeys(handle, out var length).Checked(); - return new EventKeys(keysHandle, length, doc); - }); + return new EventKeys(keysHandle, length, doc); + }); - target = new Lazy(() => - { - var targetHandle = XmlTextChannel.ObserveEventTarget(handle).Checked(); + target = new Lazy( + () => + { + var targetHandle = XmlTextChannel.ObserveEventTarget(handle).Checked(); - return doc.GetXmlText(handle, false); - }); - } - - /// - protected internal override void DisposeCore(bool disposing) - { - // The event has no explicit garbage collection, but is released automatically after the event has been completed. + return doc.GetXmlText(handle, isDeleted: false); + }); } /// @@ -61,4 +59,10 @@ protected internal override void DisposeCore(bool disposing) /// /// The target of the event. public XmlText Target => target.Value; + + /// + protected internal override void DisposeCore(bool disposing) + { + // The event has no explicit garbage collection, but is released automatically after the event has been completed. + } } diff --git a/YDotNet/Document/Types/XmlTexts/XmlText.cs b/YDotNet/Document/Types/XmlTexts/XmlText.cs index 9b03cc20..9828e418 100644 --- a/YDotNet/Document/Types/XmlTexts/XmlText.cs +++ b/YDotNet/Document/Types/XmlTexts/XmlText.cs @@ -6,6 +6,7 @@ using YDotNet.Document.Types.XmlElements; using YDotNet.Document.Types.XmlTexts.Events; using YDotNet.Infrastructure; +using YDotNet.Infrastructure.Extensions; using YDotNet.Native.StickyIndex; using YDotNet.Native.Types; @@ -254,18 +255,22 @@ public IDisposable Observe(Action action) } /// - /// Retrieves a corresponding to a given human-readable pointing into + /// Retrieves a corresponding to a given human-readable pointing + /// into /// the . /// /// The transaction that wraps this operation. /// The numeric index to place the . /// The type of the . - /// The in the with the given . + /// + /// The in the with the given + /// . + /// public StickyIndex? StickyIndex(Transaction transaction, uint index, StickyAssociationType associationType) { ThrowIfDisposed(); - var handle = StickyIndexChannel.FromIndex(Handle, transaction.Handle, index, (sbyte)associationType); + var handle = StickyIndexChannel.FromIndex(Handle, transaction.Handle, index, (sbyte) associationType); return handle != nint.Zero ? new StickyIndex(handle) : null; } From 46e4d043f7c703b5cdd0e5464be1b3b26ef91f28 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 22 Oct 2023 11:03:53 -0300 Subject: [PATCH 145/186] refactor(improvements): move initializers of `DocOptions` to the default constructor for consistency --- YDotNet/Document/Options/DocOptions.cs | 27 ++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/YDotNet/Document/Options/DocOptions.cs b/YDotNet/Document/Options/DocOptions.cs index ee14fb03..289de63f 100644 --- a/YDotNet/Document/Options/DocOptions.cs +++ b/YDotNet/Document/Options/DocOptions.cs @@ -11,12 +11,19 @@ namespace YDotNet.Document.Options; public class DocOptions { /// - /// Gets the default options value used to initialize instances. + /// Initializes a new instance of the class. /// - public static DocOptions Default => new() + public DocOptions() { - Id = ClientId.GetRandom() - }; + Id = ClientId.GetRandom(); + ShouldLoad = true; + Encoding = DocEncoding.Utf16; + } + + /// + /// Gets the default options value used to initialize instances. + /// + public static DocOptions Default => new(); /// /// Gets the globally unique 53-bit integer assigned to the replica as its identifier. @@ -60,7 +67,7 @@ public class DocOptions /// Read more about the possible values in . /// /// - public DocEncoding Encoding { get; init; } = DocEncoding.Utf16; + public DocEncoding Encoding { get; init; } /// /// Gets a value indicating whether deleted blocks should be garbage collected during transaction commits. @@ -81,7 +88,7 @@ public class DocOptions /// /// Gets a value indicating whether the should be synchronized by the provider now. /// - public bool ShouldLoad { get; init; } = true; + public bool ShouldLoad { get; init; } internal DocOptionsNative ToNative() { @@ -94,10 +101,10 @@ internal DocOptionsNative ToNative() Id = Id, Guid = unsafeGuid.Handle, CollectionId = unsafeCollection.Handle, - Encoding = (byte)Encoding, - SkipGc = (byte)(SkipGarbageCollection ? 1 : 0), - AutoLoad = (byte)(AutoLoad ? 1 : 0), - ShouldLoad = (byte)(ShouldLoad ? 1 : 0) + Encoding = (byte) Encoding, + SkipGc = (byte) (SkipGarbageCollection ? 1 : 0), + AutoLoad = (byte) (AutoLoad ? 1 : 0), + ShouldLoad = (byte) (ShouldLoad ? 1 : 0) }; } } From 78027d0b0d0a6d8c6030fe227ea883a9ca148975 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 22 Oct 2023 11:25:27 -0300 Subject: [PATCH 146/186] refactor(improvements): fix warnings on `*Enumerator` classes --- .../Document/Types/Arrays/ArrayEnumerator.cs | 30 ++++++++--------- YDotNet/Document/Types/Maps/MapEnumerator.cs | 32 ++++++++---------- .../Trees/XmlTreeWalkerEnumerator.cs | 33 +++++++++---------- .../XmlElements/XmlAttributeEnumerator.cs | 30 ++++++++--------- 4 files changed, 56 insertions(+), 69 deletions(-) diff --git a/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs b/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs index 7bdd091c..667b4a08 100644 --- a/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs +++ b/YDotNet/Document/Types/Arrays/ArrayEnumerator.cs @@ -10,42 +10,40 @@ namespace YDotNet.Document.Types.Arrays; /// internal class ArrayEnumerator : IEnumerator { + private readonly ArrayIterator iterator; private Output? current; internal ArrayEnumerator(ArrayIterator iterator) { - Iterator = iterator; - } - - /// - public void Dispose() - { - Iterator.Dispose(); + this.iterator = iterator; } /// public Output Current => current!; /// - object? IEnumerator.Current => current!; + object IEnumerator.Current => current!; - private ArrayIterator Iterator { get; } + /// + public void Dispose() + { + iterator.Dispose(); + } /// public bool MoveNext() { - var handle = ArrayChannel.IteratorNext(Iterator.Handle); + var handle = ArrayChannel.IteratorNext(iterator.Handle); if (handle != nint.Zero) { - current = Output.CreateAndRelease(handle, Iterator.Doc); + current = Output.CreateAndRelease(handle, iterator.Doc); return true; } - else - { - current = null!; - return false; - } + + current = null!; + + return false; } /// diff --git a/YDotNet/Document/Types/Maps/MapEnumerator.cs b/YDotNet/Document/Types/Maps/MapEnumerator.cs index b2af195f..da1f1b84 100644 --- a/YDotNet/Document/Types/Maps/MapEnumerator.cs +++ b/YDotNet/Document/Types/Maps/MapEnumerator.cs @@ -10,50 +10,46 @@ namespace YDotNet.Document.Types.Maps; /// internal class MapEnumerator : IEnumerator> { - private KeyValuePair current; + private readonly MapIterator iterator; internal MapEnumerator(MapIterator iterator) { - Iterator = iterator; + this.iterator = iterator; } /// - public void Dispose() - { - Iterator.Dispose(); - } + public KeyValuePair Current { get; private set; } /// - public KeyValuePair Current => current; + object IEnumerator.Current => Current; /// - object? IEnumerator.Current => current!; - - private MapIterator Iterator { get; } + public void Dispose() + { + iterator.Dispose(); + } /// public bool MoveNext() { - var handle = MapChannel.IteratorNext(Iterator.Handle); + var handle = MapChannel.IteratorNext(iterator.Handle); if (handle != nint.Zero) { var native = MemoryReader.ReadStruct(handle); - current = new KeyValuePair( + Current = new KeyValuePair( native.Key(), - new Output(native.ValueHandle(handle), Iterator.Doc, false)); + new Output(native.ValueHandle(handle), iterator.Doc, isDeleted: false)); // We are done reading, so we can release the memory. MapChannel.EntryDestroy(handle); return true; } - else - { - current = default; - return false; - } + + Current = default; + return false; } /// diff --git a/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs b/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs index 667fc3f2..f8c7cdc5 100644 --- a/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs +++ b/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalkerEnumerator.cs @@ -10,49 +10,46 @@ namespace YDotNet.Document.Types.XmlElements.Trees; /// internal class XmlTreeWalkerEnumerator : IEnumerator { + private readonly XmlTreeWalker treeWalker; private Output? current; /// /// Initializes a new instance of the class. /// /// - /// The instance used by this enumerator. - /// Check for more details. + /// The instance used by this enumerator. + /// Check for more details. /// public XmlTreeWalkerEnumerator(XmlTreeWalker treeWalker) { - TreeWalker = treeWalker; - } - - /// - public void Dispose() - { - TreeWalker.Dispose(); + this.treeWalker = treeWalker; } /// public Output Current => current!; /// - object? IEnumerator.Current => current!; + object IEnumerator.Current => current!; - private XmlTreeWalker TreeWalker { get; } + /// + public void Dispose() + { + treeWalker.Dispose(); + } /// public bool MoveNext() { - var handle = XmlElementChannel.TreeWalkerNext(TreeWalker.Handle); + var handle = XmlElementChannel.TreeWalkerNext(treeWalker.Handle); if (handle != nint.Zero) { - current = Output.CreateAndRelease(handle, TreeWalker.Doc); + current = Output.CreateAndRelease(handle, treeWalker.Doc); return true; } - else - { - current = null!; - return false; - } + + current = null!; + return false; } /// diff --git a/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs b/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs index a185107b..3b247464 100644 --- a/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs +++ b/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs @@ -9,48 +9,44 @@ namespace YDotNet.Document.Types.XmlElements; /// internal class XmlAttributeEnumerator : IEnumerator> { - private KeyValuePair current; + private readonly XmlAttributeIterator iterator; internal XmlAttributeEnumerator(XmlAttributeIterator iterator) { - Iterator = iterator; + this.iterator = iterator; } /// - public void Dispose() - { - Iterator.Dispose(); - } + public KeyValuePair Current { get; private set; } /// - public KeyValuePair Current => current; + object IEnumerator.Current => Current; /// - object? IEnumerator.Current => current!; - - private XmlAttributeIterator Iterator { get; } + public void Dispose() + { + iterator.Dispose(); + } /// public bool MoveNext() { - var handle = XmlAttributeChannel.IteratorNext(Iterator.Handle); + var handle = XmlAttributeChannel.IteratorNext(iterator.Handle); if (handle != nint.Zero) { var native = MemoryReader.ReadStruct(handle); - current = new KeyValuePair(native.Key(), native.Value()); + Current = new KeyValuePair(native.Key(), native.Value()); // We are done reading, therefore we can release memory. XmlAttributeChannel.Destroy(handle); return true; } - else - { - current = default; - return false; - } + + Current = default; + return false; } /// From bf942cb251334453e8417200a8c3b242e3d8c4e0 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 22 Oct 2023 11:37:35 -0300 Subject: [PATCH 147/186] refactor(improvements): fix warnings on `*Iterator` classes --- .../Document/Types/Arrays/ArrayIterator.cs | 28 +++++++++---------- YDotNet/Document/Types/Maps/MapIterator.cs | 28 +++++++++---------- .../Types/XmlElements/XmlAttributeIterator.cs | 26 ++++++++--------- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/YDotNet/Document/Types/Arrays/ArrayIterator.cs b/YDotNet/Document/Types/Arrays/ArrayIterator.cs index 5ec3ba1c..4bea73f1 100644 --- a/YDotNet/Document/Types/Arrays/ArrayIterator.cs +++ b/YDotNet/Document/Types/Arrays/ArrayIterator.cs @@ -19,20 +19,6 @@ internal ArrayIterator(nint handle, Doc doc) Doc = doc; } - /// - /// Finalizes an instance of the class. - /// - ~ArrayIterator() - { - Dispose(false); - } - - /// - protected internal override void DisposeCore(bool disposing) - { - ArrayChannel.IteratorDestroy(Handle); - } - internal Doc Doc { get; } /// @@ -47,4 +33,18 @@ IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } + + /// + /// Finalizes an instance of the class. + /// + ~ArrayIterator() + { + Dispose(disposing: false); + } + + /// + protected internal override void DisposeCore(bool disposing) + { + ArrayChannel.IteratorDestroy(Handle); + } } diff --git a/YDotNet/Document/Types/Maps/MapIterator.cs b/YDotNet/Document/Types/Maps/MapIterator.cs index 1371fb09..0e099e32 100644 --- a/YDotNet/Document/Types/Maps/MapIterator.cs +++ b/YDotNet/Document/Types/Maps/MapIterator.cs @@ -26,20 +26,6 @@ internal MapIterator(nint handle, Doc doc) Doc = doc; } - /// - /// Finalizes an instance of the class. - /// - ~MapIterator() - { - Dispose(false); - } - - /// - protected internal override void DisposeCore(bool disposing) - { - MapChannel.IteratorDestroy(Handle); - } - internal Doc Doc { get; } /// @@ -54,4 +40,18 @@ IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } + + /// + /// Finalizes an instance of the class. + /// + ~MapIterator() + { + Dispose(disposing: false); + } + + /// + protected internal override void DisposeCore(bool disposing) + { + MapChannel.IteratorDestroy(Handle); + } } diff --git a/YDotNet/Document/Types/XmlElements/XmlAttributeIterator.cs b/YDotNet/Document/Types/XmlElements/XmlAttributeIterator.cs index 6d08ab69..c5422dca 100644 --- a/YDotNet/Document/Types/XmlElements/XmlAttributeIterator.cs +++ b/YDotNet/Document/Types/XmlElements/XmlAttributeIterator.cs @@ -18,29 +18,29 @@ internal XmlAttributeIterator(nint handle) { } - /// - /// Finalizes an instance of the class. - /// - ~XmlAttributeIterator() + /// + public IEnumerator> GetEnumerator() { - Dispose(false); + return new XmlAttributeEnumerator(this); } - /// - protected internal override void DisposeCore(bool disposing) + /// + IEnumerator IEnumerable.GetEnumerator() { - XmlAttributeChannel.IteratorDestroy(Handle); + return GetEnumerator(); } - /// - public IEnumerator> GetEnumerator() + /// + /// Finalizes an instance of the class. + /// + ~XmlAttributeIterator() { - return new XmlAttributeEnumerator(this); + Dispose(disposing: false); } /// - IEnumerator IEnumerable.GetEnumerator() + protected internal override void DisposeCore(bool disposing) { - return GetEnumerator(); + XmlAttributeChannel.IteratorDestroy(Handle); } } From feb56f508498c094fa116546953de691b147a19f Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 22 Oct 2023 11:45:37 -0300 Subject: [PATCH 148/186] refactor(improvements): fix warnings and rename `CreateValue` in the `EventBranch` class --- YDotNet/Document/Types/Events/EventBranch.cs | 56 ++++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/YDotNet/Document/Types/Events/EventBranch.cs b/YDotNet/Document/Types/Events/EventBranch.cs index 817cca2b..446d2b36 100644 --- a/YDotNet/Document/Types/Events/EventBranch.cs +++ b/YDotNet/Document/Types/Events/EventBranch.cs @@ -18,35 +18,9 @@ public class EventBranch internal EventBranch(NativeWithHandle native, Doc doc) { - Tag = (EventBranchTag)native.Value.Tag; + Tag = (EventBranchTag) native.Value.Tag; - value = CreateValue(native, doc, Tag); - } - - private static object? CreateValue(NativeWithHandle native, Doc doc, EventBranchTag tag) - { - var handle = native.Value.ValueHandle(native.Handle); - - switch (tag) - { - case EventBranchTag.Map: - return new MapEvent(handle, doc); - - case EventBranchTag.Text: - return new TextEvent(handle, doc); - - case EventBranchTag.Array: - return new ArrayEvent(handle, doc); - - case EventBranchTag.XmlElement: - return new XmlElementEvent(handle, doc); - - case EventBranchTag.XmlText: - return new XmlTextEvent(handle, doc); - - default: - return null; - } + value = BuildValue(native, doc, Tag); } /// @@ -84,6 +58,32 @@ internal EventBranch(NativeWithHandle native, Doc doc) /// Value is not a . public XmlTextEvent XmlTextEvent => GetValue(EventBranchTag.XmlText); + private static object? BuildValue(NativeWithHandle native, Doc doc, EventBranchTag tag) + { + var handle = native.Value.ValueHandle(native.Handle); + + switch (tag) + { + case EventBranchTag.Map: + return new MapEvent(handle, doc); + + case EventBranchTag.Text: + return new TextEvent(handle, doc); + + case EventBranchTag.Array: + return new ArrayEvent(handle, doc); + + case EventBranchTag.XmlElement: + return new XmlElementEvent(handle, doc); + + case EventBranchTag.XmlText: + return new XmlTextEvent(handle, doc); + + default: + return null; + } + } + private T GetValue(EventBranchTag expectedType) { if (value is not T typed) From 1f88eb99dcb5aa486fe7fcd9624553daf7d96e68 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 22 Oct 2023 11:46:07 -0300 Subject: [PATCH 149/186] refactor(improvements): format the `Branch` class --- YDotNet/Document/Types/Branches/Branch.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/YDotNet/Document/Types/Branches/Branch.cs b/YDotNet/Document/Types/Branches/Branch.cs index eed82c55..d3e8cefb 100644 --- a/YDotNet/Document/Types/Branches/Branch.cs +++ b/YDotNet/Document/Types/Branches/Branch.cs @@ -5,8 +5,6 @@ using YDotNet.Native.Document.Events; using YDotNet.Native.Types.Branches; -#pragma warning disable CA1806 // Do not ignore method results - namespace YDotNet.Document.Types.Branches; /// @@ -28,7 +26,9 @@ internal Branch(nint handle, Doc doc, bool isDeleted) { BranchChannel.ObserveCallback callback = (_, length, ev) => { - var events = MemoryReader.ReadStructsWithHandles(ev, length).Select(x => new EventBranch(x, doc)).ToArray(); + var events = MemoryReader.ReadStructsWithHandles(ev, length) + .Select(x => new EventBranch(x, doc)) + .ToArray(); action(events); }; From 80452922c7e778c537797f8033e11bf52584afa3 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 22 Oct 2023 11:52:19 -0300 Subject: [PATCH 150/186] refactor(improvements): fix warnings on `XmlTreeWalker` class --- .../Types/XmlElements/Trees/XmlTreeWalker.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalker.cs b/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalker.cs index d9e1d554..4720e610 100644 --- a/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalker.cs +++ b/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalker.cs @@ -21,20 +21,6 @@ internal XmlTreeWalker(nint handle, Doc doc) Doc = doc; } - /// - /// Finalizes an instance of the class. - /// - ~XmlTreeWalker() - { - Dispose(false); - } - - /// - protected internal override void DisposeCore(bool disposing) - { - XmlElementChannel.TreeWalkerDestroy(Handle); - } - internal Doc Doc { get; } /// @@ -48,4 +34,18 @@ IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } + + /// + /// Finalizes an instance of the class. + /// + ~XmlTreeWalker() + { + Dispose(disposing: false); + } + + /// + protected internal override void DisposeCore(bool disposing) + { + XmlElementChannel.TreeWalkerDestroy(Handle); + } } From 07d219e8021d8cec3667b21577277949f1a5782e Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 22 Oct 2023 11:55:45 -0300 Subject: [PATCH 151/186] refactor(improvements): fix warnings on `UndoManager` class --- YDotNet/Document/UndoManagers/UndoManager.cs | 38 ++++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/YDotNet/Document/UndoManagers/UndoManager.cs b/YDotNet/Document/UndoManagers/UndoManager.cs index 51970110..122a33fe 100644 --- a/YDotNet/Document/UndoManagers/UndoManager.cs +++ b/YDotNet/Document/UndoManagers/UndoManager.cs @@ -27,7 +27,7 @@ public UndoManager(Doc doc, Branch branch, UndoManagerOptions? options = null) onAdded = new EventSubscriber( doc.EventManager, Handle, - (owner, action) => + (_, action) => { UndoManagerChannel.ObserveAddedCallback callback = (_, undoEvent) => action(new UndoEvent(undoEvent)); @@ -39,7 +39,7 @@ public UndoManager(Doc doc, Branch branch, UndoManagerOptions? options = null) onPopped = new EventSubscriber( doc.EventManager, Handle, - (owner, action) => + (_, action) => { UndoManagerChannel.ObservePoppedCallback callback = (_, undoEvent) => action(new UndoEvent(undoEvent)); @@ -49,25 +49,12 @@ public UndoManager(Doc doc, Branch branch, UndoManagerOptions? options = null) (owner, s) => UndoManagerChannel.UnobservePopped(owner, s)); } - private static nint Create(Doc doc, Branch branch, UndoManagerOptions? options) - { - var unsafeOptions = MemoryWriter.WriteStruct(options?.ToNative() ?? default); - - return UndoManagerChannel.NewWithOptions(doc.Handle, branch.Handle, unsafeOptions.Handle); - } - /// - /// Finalizes an instance of the class. + /// Finalizes an instance of the class. /// ~UndoManager() { - Dispose(false); - } - - /// - protected internal override void DisposeCore(bool disposing) - { - UndoManagerChannel.Destroy(Handle); + Dispose(disposing: false); } /// @@ -173,7 +160,7 @@ public void AddScope(Branch branch) /// The origin to be included in this . public void AddOrigin(byte[] origin) { - UndoManagerChannel.AddOrigin(Handle, (uint)origin.Length, origin); + UndoManagerChannel.AddOrigin(Handle, (uint) origin.Length, origin); } /// @@ -186,6 +173,19 @@ public void AddOrigin(byte[] origin) /// The origin to be removed from this . public void RemoveOrigin(byte[] origin) { - UndoManagerChannel.RemoveOrigin(Handle, (uint)origin.Length, origin); + UndoManagerChannel.RemoveOrigin(Handle, (uint) origin.Length, origin); + } + + /// + protected internal override void DisposeCore(bool disposing) + { + UndoManagerChannel.Destroy(Handle); + } + + private static nint Create(Doc doc, Branch branch, UndoManagerOptions? options) + { + var unsafeOptions = MemoryWriter.WriteStruct(options?.ToNative() ?? default); + + return UndoManagerChannel.NewWithOptions(doc.Handle, branch.Handle, unsafeOptions.Handle); } } From 480b3e4f82dcac2e40b52a4d2628ac922341de0d Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 22 Oct 2023 11:57:14 -0300 Subject: [PATCH 152/186] refactor(improvements): fix warnings on `Doc` class --- YDotNet/Document/Doc.cs | 59 ++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index dfc67517..bb3f3e1b 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -10,8 +10,6 @@ using YDotNet.Native.Document.Events; using Array = YDotNet.Document.Types.Arrays.Array; -#pragma warning disable CA1806 // Do not ignore method results - namespace YDotNet.Document; /// @@ -74,7 +72,7 @@ internal Doc(nint handle, Doc? parent, bool isDeleted) (doc, action) => { DocChannel.ObserveClearCallback callback = - (_, doc) => action(new ClearEvent(GetDoc(doc, isDeleted: false))); + (_, eventDoc) => action(new ClearEvent(GetDoc(eventDoc, isDeleted: false))); return (DocChannel.ObserveClear(doc, nint.Zero, callback), callback); }, @@ -83,7 +81,7 @@ internal Doc(nint handle, Doc? parent, bool isDeleted) onUpdateV1 = new EventSubscriber( EventManager, handle, - (doc, action) => + (_, action) => { DocChannel.ObserveUpdatesCallback callback = (_, length, data) => action(new UpdateEvent(UpdateEventNative.From(length, data))); @@ -95,7 +93,7 @@ internal Doc(nint handle, Doc? parent, bool isDeleted) onUpdateV2 = new EventSubscriber( EventManager, handle, - (doc, action) => + (_, action) => { DocChannel.ObserveUpdatesCallback callback = (_, length, data) => action(new UpdateEvent(UpdateEventNative.From(length, data))); @@ -220,11 +218,6 @@ public void Dispose() GC.SuppressFinalize(this); } - private static nint CreateDoc(DocOptions options) - { - return DocChannel.NewWithOptions(options.ToNative()); - } - /// /// Finalizes an instance of the class. /// @@ -233,24 +226,6 @@ private static nint CreateDoc(DocOptions options) Dispose(disposing: false); } - private void Dispose(bool disposing) - { - if (IsDisposed) - { - return; - } - - MarkDisposed(); - - if (disposing) - { - // Clears all active subscriptions that have not been closed yet. - EventManager.Clear(); - } - - DocChannel.Destroy(Handle); - } - /// /// Gets or creates a new shared data type instance as a root-level /// type in this document. @@ -504,7 +479,7 @@ internal Doc GetDoc(nint handle, bool isDeleted) handle = DocChannel.Clone(handle); } - return GetOrAdd(handle, (h, doc) => new Doc(handle, doc, isDeleted)); + return GetOrAdd(handle, (_, doc) => new Doc(handle, doc, isDeleted)); } internal void NotifyTransactionStarted() @@ -542,7 +517,8 @@ internal XmlElement GetXmlElement(nint handle, bool isDeleted) return GetOrAdd(handle, (h, doc) => new XmlElement(h, doc, isDeleted)); } - private T GetOrAdd(nint handle, Func factory) where T : ITypeBase + private T GetOrAdd(nint handle, Func factory) + where T : ITypeBase { var doc = GetRootDoc(); @@ -561,4 +537,27 @@ private Doc GetRootDoc() { return parent?.GetRootDoc() ?? this; } + + private static nint CreateDoc(DocOptions options) + { + return DocChannel.NewWithOptions(options.ToNative()); + } + + private void Dispose(bool disposing) + { + if (IsDisposed) + { + return; + } + + MarkDisposed(); + + if (disposing) + { + // Clears all active subscriptions that have not been closed yet. + EventManager.Clear(); + } + + DocChannel.Destroy(Handle); + } } From 9c6c1a1b49b76428666b617721d72d555f9c735c Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 22 Oct 2023 11:58:25 -0300 Subject: [PATCH 153/186] refactor(improvements): rename `ClientId` to `ClientIdGenerator` since it's never instantiated --- .../Infrastructure/ClientIdTests.cs | 6 ++--- YDotNet/Document/Options/DocOptions.cs | 2 +- YDotNet/Infrastructure/ClientId.cs | 23 ------------------- YDotNet/Infrastructure/ClientIdGenerator.cs | 23 +++++++++++++++++++ 4 files changed, 27 insertions(+), 27 deletions(-) delete mode 100644 YDotNet/Infrastructure/ClientId.cs create mode 100644 YDotNet/Infrastructure/ClientIdGenerator.cs diff --git a/Tests/YDotNet.Tests.Unit/Infrastructure/ClientIdTests.cs b/Tests/YDotNet.Tests.Unit/Infrastructure/ClientIdTests.cs index 039249d9..9cce0793 100644 --- a/Tests/YDotNet.Tests.Unit/Infrastructure/ClientIdTests.cs +++ b/Tests/YDotNet.Tests.Unit/Infrastructure/ClientIdTests.cs @@ -6,13 +6,13 @@ namespace YDotNet.Tests.Unit.Infrastructure; public class ClientIdTests { [Test] - public void TestSatefy() + public void TestSafety() { for (var i = 0; i < 10_000_000; i++) { - var id = ClientId.GetRandom(); + var id = ClientIdGenerator.Random(); - Assert.That(id, Is.LessThanOrEqualTo(ClientId.MaxSafeInteger)); + Assert.That(id, Is.LessThanOrEqualTo(ClientIdGenerator.MaxSafeInteger)); } } } diff --git a/YDotNet/Document/Options/DocOptions.cs b/YDotNet/Document/Options/DocOptions.cs index 289de63f..6724caf7 100644 --- a/YDotNet/Document/Options/DocOptions.cs +++ b/YDotNet/Document/Options/DocOptions.cs @@ -15,7 +15,7 @@ public class DocOptions /// public DocOptions() { - Id = ClientId.GetRandom(); + Id = ClientIdGenerator.Random(); ShouldLoad = true; Encoding = DocEncoding.Utf16; } diff --git a/YDotNet/Infrastructure/ClientId.cs b/YDotNet/Infrastructure/ClientId.cs deleted file mode 100644 index ba563004..00000000 --- a/YDotNet/Infrastructure/ClientId.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace YDotNet.Infrastructure; - -/// -/// Helper class to deal with client ids. -/// -public sealed class ClientId -{ - /// - /// The maximum safe integer from javascript. - /// - public const long MaxSafeInteger = 2 ^ 53 - 1; - - /// - /// Gets a random client id. - /// - /// The random client id. - public static ulong GetRandom() - { - var value = (ulong)Random.Shared.Next() & MaxSafeInteger; - - return value; - } -} diff --git a/YDotNet/Infrastructure/ClientIdGenerator.cs b/YDotNet/Infrastructure/ClientIdGenerator.cs new file mode 100644 index 00000000..6cb055f9 --- /dev/null +++ b/YDotNet/Infrastructure/ClientIdGenerator.cs @@ -0,0 +1,23 @@ +namespace YDotNet.Infrastructure; + +/// +/// Helper class to deal with client ids. +/// +public static class ClientIdGenerator +{ + /// + /// The maximum safe integer from javascript. + /// + public const long MaxSafeInteger = 2 ^ (53 - 1); + + /// + /// Gets a random client id. + /// + /// The random client id. + public static ulong Random() + { + var value = (ulong) System.Random.Shared.Next() & MaxSafeInteger; + + return value; + } +} From a77b22b6b54bfef18813dceb61f342dd8e8241d1 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 22 Oct 2023 12:00:12 -0300 Subject: [PATCH 154/186] refactor(improvements): fix warnings on `TypeCache` class --- YDotNet/Infrastructure/TypeCache.cs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/YDotNet/Infrastructure/TypeCache.cs b/YDotNet/Infrastructure/TypeCache.cs index bd39b2f7..3ce78967 100644 --- a/YDotNet/Infrastructure/TypeCache.cs +++ b/YDotNet/Infrastructure/TypeCache.cs @@ -31,6 +31,20 @@ public T GetOrAdd(nint handle, Func factory) return typedItem; } + // TODO [LSViana] Check with Sebastian why this method isn't used anywhere. + public void Delete(nint handle) + { + if (cache.TryGetValue(handle, out var weakRef)) + { + if (weakRef.TryGetTarget(out var item)) + { + item.MarkDisposed(); + } + + cache.Remove(handle); + } + } + private void Cleanup() { List? keysToDelete = null; @@ -52,18 +66,4 @@ private void Cleanup() } } } - - // TODO [LSViana] Check with Sebastian why this method isn't used anywhere. - public void Delete(nint handle) - { - if (cache.TryGetValue(handle, out var weakRef)) - { - if (weakRef.TryGetTarget(out var item)) - { - item.MarkDisposed(); - } - - cache.Remove(handle); - } - } } From 84c163cf425f085ade29e4bd2fd1af01571bba4e Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 22 Oct 2023 12:08:41 -0300 Subject: [PATCH 155/186] refactor(improvements): restore `.editorconfig` for the `Native` namespace --- YDotNet/Native/.editorconfig | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 YDotNet/Native/.editorconfig diff --git a/YDotNet/Native/.editorconfig b/YDotNet/Native/.editorconfig new file mode 100644 index 00000000..50fc38d9 --- /dev/null +++ b/YDotNet/Native/.editorconfig @@ -0,0 +1,8 @@ +[*.cs] + +# StyleCop rules + +# Documentation rules +dotnet_diagnostic.SA1600.severity = none # Elements should be documented +dotnet_diagnostic.SA1601.severity = none # Partial elements should be documented +dotnet_diagnostic.SA1602.severity = none # Enumeration items should be documented From 0170b571f7cfb482a43ee9c4d4486f4f1eb58481 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 22 Oct 2023 12:10:24 -0300 Subject: [PATCH 156/186] refactor(improvements): enable suggestion for some documentation rules for the whole project --- .editorconfig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.editorconfig b/.editorconfig index c9c9bd8b..a7a24e9e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -40,8 +40,8 @@ dotnet_diagnostic.SA1413.severity = suggestion # Use trailing comma in multi-lin dotnet_diagnostic.SA1500.severity = suggestion # Braces for multi-line statements should not share line # Documentation rules -dotnet_diagnostic.SA1600.severity = none # Elements should be documented -dotnet_diagnostic.SA1601.severity = none # Partial elements should be documented -dotnet_diagnostic.SA1602.severity = none # Enumeration items should be documented +dotnet_diagnostic.SA1600.severity = suggestion # Elements should be documented +dotnet_diagnostic.SA1601.severity = suggestion # Partial elements should be documented +dotnet_diagnostic.SA1602.severity = suggestion # Enumeration items should be documented dotnet_diagnostic.SA1633.severity = none # File should have header From 712d954e2cb4167dbd36d627344219aba817a3e6 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 22 Oct 2023 12:11:03 -0300 Subject: [PATCH 157/186] refactor(improvements): fix typos, format files, and smaller fixes throughout the project --- YDotNet/Document/Types/Events/EventChanges.cs | 2 +- YDotNet/Document/Types/Events/EventDeltas.cs | 4 ++-- YDotNet/Document/Types/Maps/Events/MapEvent.cs | 2 +- YDotNet/Document/Types/Maps/MapEnumerator.cs | 4 ++-- YDotNet/Document/Types/Maps/MapIterator.cs | 2 +- YDotNet/Document/Types/Texts/TextChunk.cs | 9 +++++---- YDotNet/Document/Types/Texts/TextChunks.cs | 4 ++-- .../Document/Types/XmlElements/XmlAttributeEnumerator.cs | 4 ++-- YDotNet/Document/Types/XmlElements/XmlElement.cs | 4 ++-- YDotNet/Infrastructure/MemoryWriter.cs | 1 - YDotNet/Infrastructure/UnmanagedResource.cs | 2 +- YDotNet/Native/NativeWithHandle.cs | 3 ++- YDotNet/Native/Types/Maps/MapEntryNative.cs | 2 +- 13 files changed, 22 insertions(+), 21 deletions(-) diff --git a/YDotNet/Document/Types/Events/EventChanges.cs b/YDotNet/Document/Types/Events/EventChanges.cs index a67ff0ed..d91c73b9 100644 --- a/YDotNet/Document/Types/Events/EventChanges.cs +++ b/YDotNet/Document/Types/Events/EventChanges.cs @@ -24,7 +24,7 @@ private static IList ReadItems(nint handle, uint length, Doc doc) result.Add(new EventChange(native, doc)); } - // We are done reading and can release the resource. + // We are done reading and can destroy the resource. EventChannel.DeltaDestroy(handle, length); return result; diff --git a/YDotNet/Document/Types/Events/EventDeltas.cs b/YDotNet/Document/Types/Events/EventDeltas.cs index 7881151a..a2b74d4d 100644 --- a/YDotNet/Document/Types/Events/EventDeltas.cs +++ b/YDotNet/Document/Types/Events/EventDeltas.cs @@ -17,14 +17,14 @@ internal EventDeltas(nint handle, uint length, Doc doc) private static IList ReadItems(nint handle, uint length, Doc doc) { - var result = new List((int)length); + var result = new List((int) length); foreach (var native in MemoryReader.ReadStructs(handle, length)) { result.Add(new EventDelta(native, doc)); } - // We are done reading and can release the memory. + // We are done reading and can destroy the resource. EventChannel.DeltaDestroy(handle, length); return result; diff --git a/YDotNet/Document/Types/Maps/Events/MapEvent.cs b/YDotNet/Document/Types/Maps/Events/MapEvent.cs index 09bbdea0..bace8837 100644 --- a/YDotNet/Document/Types/Maps/Events/MapEvent.cs +++ b/YDotNet/Document/Types/Maps/Events/MapEvent.cs @@ -64,6 +64,6 @@ internal MapEvent(nint handle, Doc doc) /// protected internal override void DisposeCore(bool disposing) { - // The event has no explicit garbage collection, but is released automatically after the event has been completed. + // The event has no explicit garbage collection, it is released automatically after the event has been completed. } } diff --git a/YDotNet/Document/Types/Maps/MapEnumerator.cs b/YDotNet/Document/Types/Maps/MapEnumerator.cs index da1f1b84..c4715350 100644 --- a/YDotNet/Document/Types/Maps/MapEnumerator.cs +++ b/YDotNet/Document/Types/Maps/MapEnumerator.cs @@ -6,7 +6,7 @@ namespace YDotNet.Document.Types.Maps; /// -/// Represents the iterator to provide instances of key value pairs. +/// Represents the iterator to provide instances of key-value pairs. /// internal class MapEnumerator : IEnumerator> { @@ -42,7 +42,7 @@ public bool MoveNext() native.Key(), new Output(native.ValueHandle(handle), iterator.Doc, isDeleted: false)); - // We are done reading, so we can release the memory. + // We are done reading and can destroy the resource. MapChannel.EntryDestroy(handle); return true; diff --git a/YDotNet/Document/Types/Maps/MapIterator.cs b/YDotNet/Document/Types/Maps/MapIterator.cs index 0e099e32..2b5ee079 100644 --- a/YDotNet/Document/Types/Maps/MapIterator.cs +++ b/YDotNet/Document/Types/Maps/MapIterator.cs @@ -6,7 +6,7 @@ namespace YDotNet.Document.Types.Maps; /// -/// Represents an enumerable to read key value pairs from a . +/// Represents an enumerable to read key-value pairs from a . /// /// /// Two important details about . diff --git a/YDotNet/Document/Types/Texts/TextChunk.cs b/YDotNet/Document/Types/Texts/TextChunk.cs index 40f4d472..321e0b21 100644 --- a/YDotNet/Document/Types/Texts/TextChunk.cs +++ b/YDotNet/Document/Types/Texts/TextChunk.cs @@ -11,11 +11,12 @@ public class TextChunk { internal TextChunk(NativeWithHandle native, Doc doc) { - Data = new Output(native.Handle, doc, false); + Data = new Output(native.Value.Data, doc, isDeleted: false); - Attributes = native.Value.Attributes().ToDictionary( - x => x.Value.Key(), - x => new Output(x.Value.ValueHandle(x.Handle), doc, false)); + Attributes = native.Value.Attributes() + .ToDictionary( + x => x.Value.Key(), + x => new Output(x.Value.ValueHandle(x.Handle), doc, isDeleted: false)); } /// diff --git a/YDotNet/Document/Types/Texts/TextChunks.cs b/YDotNet/Document/Types/Texts/TextChunks.cs index d7a70e8f..bf30dac5 100644 --- a/YDotNet/Document/Types/Texts/TextChunks.cs +++ b/YDotNet/Document/Types/Texts/TextChunks.cs @@ -17,14 +17,14 @@ internal TextChunks(nint handle, uint length, Doc doc) private static IList ReadItems(nint handle, uint length, Doc doc) { - var result = new List((int)length); + var result = new List((int) length); foreach (var native in MemoryReader.ReadStructsWithHandles(handle, length)) { result.Add(new TextChunk(native, doc)); } - // We are done reading and can release the memory. + // We are done reading and can destroy the resource. ChunksChannel.Destroy(handle, length); return result; diff --git a/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs b/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs index 3b247464..df8a9968 100644 --- a/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs +++ b/YDotNet/Document/Types/XmlElements/XmlAttributeEnumerator.cs @@ -5,7 +5,7 @@ namespace YDotNet.Document.Types.XmlElements; /// -/// Represents the iterator to provide instances of key value paris. +/// Represents the iterator to provide instances of key-value paris. /// internal class XmlAttributeEnumerator : IEnumerator> { @@ -39,7 +39,7 @@ public bool MoveNext() Current = new KeyValuePair(native.Key(), native.Value()); - // We are done reading, therefore we can release memory. + // We are done reading and can destroy the resource. XmlAttributeChannel.Destroy(handle); return true; diff --git a/YDotNet/Document/Types/XmlElements/XmlElement.cs b/YDotNet/Document/Types/XmlElements/XmlElement.cs index 767ff5d4..64368581 100644 --- a/YDotNet/Document/Types/XmlElements/XmlElement.cs +++ b/YDotNet/Document/Types/XmlElements/XmlElement.cs @@ -82,10 +82,10 @@ public void InsertAttribute(Transaction transaction, string name, string value) { ThrowIfDisposed(); - using var unsageName = MemoryWriter.WriteUtf8String(name); + using var unsafeName = MemoryWriter.WriteUtf8String(name); using var unsafeValue = MemoryWriter.WriteUtf8String(value); - XmlElementChannel.InsertAttribute(Handle, transaction.Handle, unsageName.Handle, unsafeValue.Handle); + XmlElementChannel.InsertAttribute(Handle, transaction.Handle, unsafeName.Handle, unsafeValue.Handle); } /// diff --git a/YDotNet/Infrastructure/MemoryWriter.cs b/YDotNet/Infrastructure/MemoryWriter.cs index e907ba46..29b452b2 100644 --- a/YDotNet/Infrastructure/MemoryWriter.cs +++ b/YDotNet/Infrastructure/MemoryWriter.cs @@ -31,7 +31,6 @@ private static unsafe nint WriteUtf8StringCore(string value) internal static DisposableHandleArray WriteUtf8StringArray(string[] values) { var head = Marshal.AllocHGlobal(MemoryConstants.PointerSize * values.Length); - var pointers = new nint[values.Length]; for (var i = 0; i < values.Length; i++) diff --git a/YDotNet/Infrastructure/UnmanagedResource.cs b/YDotNet/Infrastructure/UnmanagedResource.cs index dea17ab7..9aca6846 100644 --- a/YDotNet/Infrastructure/UnmanagedResource.cs +++ b/YDotNet/Infrastructure/UnmanagedResource.cs @@ -1,7 +1,7 @@ namespace YDotNet.Infrastructure; /// -/// Base class for all unmanaged resources. +/// Base class for all unmanaged resources. /// public abstract class UnmanagedResource : Resource { diff --git a/YDotNet/Native/NativeWithHandle.cs b/YDotNet/Native/NativeWithHandle.cs index c6061420..834d2570 100644 --- a/YDotNet/Native/NativeWithHandle.cs +++ b/YDotNet/Native/NativeWithHandle.cs @@ -1,3 +1,4 @@ namespace YDotNet.Native; -internal record struct NativeWithHandle(T Value, nint Handle) where T : struct; +internal record struct NativeWithHandle(T Value, nint Handle) + where T : struct; diff --git a/YDotNet/Native/Types/Maps/MapEntryNative.cs b/YDotNet/Native/Types/Maps/MapEntryNative.cs index 85cb64d7..d14ecf96 100644 --- a/YDotNet/Native/Types/Maps/MapEntryNative.cs +++ b/YDotNet/Native/Types/Maps/MapEntryNative.cs @@ -7,7 +7,7 @@ namespace YDotNet.Native.Types.Maps; [StructLayout(LayoutKind.Sequential, Size = Size)] internal readonly struct MapEntryNative { - internal const int Size = 8 + OutputNative.Size; + private const int Size = 8 + OutputNative.Size; internal nint KeyHandle { get; } From 63ab9e47b2bf0428b8ddddd87134139088373c09 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 22 Oct 2023 21:05:54 +0200 Subject: [PATCH 158/186] Added new method. --- Directory.Build.props | 2 +- YDotNet.Extensions/YDotNetExtensions.cs | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index a03a2192..c34fbcae 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,7 +9,7 @@ https://github.com/LSViana/YDotNet true snupkg - 0.2.7 + 0.2.8 diff --git a/YDotNet.Extensions/YDotNetExtensions.cs b/YDotNet.Extensions/YDotNetExtensions.cs index 08171654..6a66f889 100644 --- a/YDotNet.Extensions/YDotNetExtensions.cs +++ b/YDotNet.Extensions/YDotNetExtensions.cs @@ -36,6 +36,13 @@ public static T To(this Output output, Transaction transaction) return JsonSerializer.Deserialize(jsonStream)!; } + public static string ToJson(this Output output, Doc doc) + { + using var transaction = doc.ReadTransaction(); + + return output.ToJson(transaction); + } + public static string ToJson(this Output output, Transaction transaction) { var jsonStream = new MemoryStream(); From 92b2e198d117c6fd115055d969c3f96e330ec067 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 23 Oct 2023 23:09:10 +0200 Subject: [PATCH 159/186] Build on older linux version. --- .github/workflows/build-binaries.yml | 12 ++++++------ .github/workflows/build.yml | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 95260d00..a576f438 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -26,40 +26,40 @@ jobs: # Linux - build: linux-x64 - os: ubuntu-latest + os: ubuntu-20.04 rust: stable target: x86_64-unknown-linux-gnu cross: false - build: linux-x64-musl - os: ubuntu-latest + os: ubuntu-20.04 rust: stable target: x86_64-unknown-linux-musl cross: false - build: linux-armv7 - os: ubuntu-latest + os: ubuntu-20.04 rust: stable target: armv7-unknown-linux-gnueabihf linker: gcc-arm-linux-gnueabihf cross: true - build: linux-armv7-musl - os: ubuntu-latest + os: ubuntu-20.04 rust: stable target: armv7-unknown-linux-musleabihf linker: gcc-arm-linux-gnueabihf cross: true - build: linux-arm64 - os: ubuntu-latest + os: ubuntu-20.04 rust: stable target: aarch64-unknown-linux-gnu linker: gcc-aarch64-linux-gnu cross: true - build: linux-arm64-musl - os: ubuntu-latest + os: ubuntu-20.04 rust: stable target: aarch64-unknown-linux-musl linker: gcc-aarch64-linux-gnu diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8ef935f1..7efcf9ac 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,10 +19,10 @@ jobs: # Linux - build: linux-x64 - os: ubuntu-latest + os: ubuntu-20.04 - build: linux-x64-musl - os: ubuntu-latest + os: ubuntu-20.04 # macOS - build: macos From 53636d97866545102a0ca02b7a52d9932b435bda Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 23 Oct 2023 23:30:31 +0200 Subject: [PATCH 160/186] Update version. --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index c34fbcae..e414ebac 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,7 +9,7 @@ https://github.com/LSViana/YDotNet true snupkg - 0.2.8 + 0.2.9 From 79bd53e613a9bf8fa6a4fe26ba1ad5ea96963240 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 29 Oct 2023 18:02:32 -0300 Subject: [PATCH 161/186] refactor(improvements): move `ITypeBase` interface to its own file --- YDotNet/Infrastructure/ITypeBase.cs | 17 +++++++++++++++++ YDotNet/Infrastructure/TypeBase.cs | 16 ---------------- 2 files changed, 17 insertions(+), 16 deletions(-) create mode 100644 YDotNet/Infrastructure/ITypeBase.cs diff --git a/YDotNet/Infrastructure/ITypeBase.cs b/YDotNet/Infrastructure/ITypeBase.cs new file mode 100644 index 00000000..4bc893cd --- /dev/null +++ b/YDotNet/Infrastructure/ITypeBase.cs @@ -0,0 +1,17 @@ +namespace YDotNet.Infrastructure; + +/// +/// Base class for all managed types. +/// +public interface ITypeBase +{ + /// + /// Gets a value indicating whether the instance is disposed. + /// + bool IsDisposed { get; } + + /// + /// Marks the object as disposed to stop all further calls. + /// + void MarkDisposed(); +} diff --git a/YDotNet/Infrastructure/TypeBase.cs b/YDotNet/Infrastructure/TypeBase.cs index abc1dd62..6082ac48 100644 --- a/YDotNet/Infrastructure/TypeBase.cs +++ b/YDotNet/Infrastructure/TypeBase.cs @@ -34,19 +34,3 @@ public void MarkDisposed() IsDisposed = true; } } - -/// -/// Base class for all managed types. -/// -public interface ITypeBase -{ - /// - /// Marks the object as disposed to stop all further calls. - /// - void MarkDisposed(); - - /// - /// Gets a value indicating whether the instance is disposed. - /// - bool IsDisposed { get; } -} From 51b9a40a2d90ab1e555def359d731b1c9f99cd49 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 29 Oct 2023 18:10:50 -0300 Subject: [PATCH 162/186] refactor(improvements): make `TypeBase` extend from `UnmanagedResource` --- YDotNet/Infrastructure/TypeBase.cs | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/YDotNet/Infrastructure/TypeBase.cs b/YDotNet/Infrastructure/TypeBase.cs index 6082ac48..b184e4cd 100644 --- a/YDotNet/Infrastructure/TypeBase.cs +++ b/YDotNet/Infrastructure/TypeBase.cs @@ -1,36 +1,18 @@ namespace YDotNet.Infrastructure; /// -/// Base class for all managed types. +/// Base class for all managed types. /// -public abstract class TypeBase : ITypeBase +public abstract class TypeBase : UnmanagedResource { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// + /// A handle to the unmanaged resource. /// A value indicating if the instance is deleted. - protected TypeBase(bool isDisposed) + protected TypeBase(nint handle, bool isDisposed) + : base(handle) { IsDisposed = isDisposed; } - - /// - public bool IsDisposed { get; private set; } - - /// - /// Throws an exception if the type is disposed. - /// - protected void ThrowIfDisposed() - { - if (IsDisposed) - { - throw new ObjectDisposedException(GetType().Name); - } - } - - /// - public void MarkDisposed() - { - IsDisposed = true; - } } From dff9f3f216c5ca4ae3f80ca837f527a5e8579d22 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 29 Oct 2023 18:36:50 -0300 Subject: [PATCH 163/186] refactor(improvements): remove the `TypeBase` and `ITypeBase` types --- YDotNet/Infrastructure/ITypeBase.cs | 17 ----------------- YDotNet/Infrastructure/TypeBase.cs | 18 ------------------ 2 files changed, 35 deletions(-) delete mode 100644 YDotNet/Infrastructure/ITypeBase.cs delete mode 100644 YDotNet/Infrastructure/TypeBase.cs diff --git a/YDotNet/Infrastructure/ITypeBase.cs b/YDotNet/Infrastructure/ITypeBase.cs deleted file mode 100644 index 4bc893cd..00000000 --- a/YDotNet/Infrastructure/ITypeBase.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace YDotNet.Infrastructure; - -/// -/// Base class for all managed types. -/// -public interface ITypeBase -{ - /// - /// Gets a value indicating whether the instance is disposed. - /// - bool IsDisposed { get; } - - /// - /// Marks the object as disposed to stop all further calls. - /// - void MarkDisposed(); -} diff --git a/YDotNet/Infrastructure/TypeBase.cs b/YDotNet/Infrastructure/TypeBase.cs deleted file mode 100644 index b184e4cd..00000000 --- a/YDotNet/Infrastructure/TypeBase.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace YDotNet.Infrastructure; - -/// -/// Base class for all managed types. -/// -public abstract class TypeBase : UnmanagedResource -{ - /// - /// Initializes a new instance of the class. - /// - /// A handle to the unmanaged resource. - /// A value indicating if the instance is deleted. - protected TypeBase(nint handle, bool isDisposed) - : base(handle) - { - IsDisposed = isDisposed; - } -} From f6961ad962bf31154f80540fde27bbe8a101463b Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 29 Oct 2023 18:37:44 -0300 Subject: [PATCH 164/186] refactor(improvements): add `Resource.MarkDisposed()` to allow setting `IsDisposed` from `internal` classes --- YDotNet/Infrastructure/Resource.cs | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/YDotNet/Infrastructure/Resource.cs b/YDotNet/Infrastructure/Resource.cs index 9ed8afff..9dfb453e 100644 --- a/YDotNet/Infrastructure/Resource.cs +++ b/YDotNet/Infrastructure/Resource.cs @@ -5,10 +5,15 @@ namespace YDotNet.Infrastructure; /// public abstract class Resource : IDisposable { + internal Resource(bool isDisposed) + { + IsDisposed = isDisposed; + } + /// - /// Gets a value indicating whether this instance is disposed. + /// Gets or sets a value indicating whether this instance is disposed. /// - public bool IsDisposed { get; private set; } + public bool IsDisposed { get; protected set; } /// public void Dispose() @@ -17,6 +22,20 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + /// Marks the object as disposed to stop all further calls. + /// + internal void MarkDisposed() + { + IsDisposed = true; + } + + /// + /// Releases all unmanaged resources. + /// + /// true if managed resources should be disposed as well. + protected abstract void DisposeCore(bool disposing); + /// /// Throws an exception if this object or the owner is disposed. /// @@ -44,10 +63,4 @@ protected void Dispose(bool disposing) IsDisposed = true; } - - /// - /// Releases all unmanaged resources. - /// - /// true if managed resources should be disposed as well. - protected internal abstract void DisposeCore(bool disposing); } From 055eece2c032dc188f10aec6e608b1f904c41e2e Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 29 Oct 2023 18:38:11 -0300 Subject: [PATCH 165/186] refactor(improvements): adapt `UnmanagedResource` to call `Resource` constructor correctly --- YDotNet/Infrastructure/UnmanagedResource.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/YDotNet/Infrastructure/UnmanagedResource.cs b/YDotNet/Infrastructure/UnmanagedResource.cs index 9aca6846..7aac84f4 100644 --- a/YDotNet/Infrastructure/UnmanagedResource.cs +++ b/YDotNet/Infrastructure/UnmanagedResource.cs @@ -5,7 +5,8 @@ namespace YDotNet.Infrastructure; /// public abstract class UnmanagedResource : Resource { - internal UnmanagedResource(nint handle) + internal UnmanagedResource(nint handle, bool isDisposed) + : base(isDisposed) { Handle = handle; } From 08eb3349cf0fb4ab33a8dc1a3d14f89d83d38284 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 29 Oct 2023 18:38:44 -0300 Subject: [PATCH 166/186] refactor(improvements): adapt `TypeCache` to work with `UnmanagedResource` instances --- YDotNet/Infrastructure/TypeCache.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/YDotNet/Infrastructure/TypeCache.cs b/YDotNet/Infrastructure/TypeCache.cs index 3ce78967..782b3a16 100644 --- a/YDotNet/Infrastructure/TypeCache.cs +++ b/YDotNet/Infrastructure/TypeCache.cs @@ -2,10 +2,10 @@ namespace YDotNet.Infrastructure; internal class TypeCache { - private readonly Dictionary> cache = new(); + private readonly Dictionary> cache = new(); public T GetOrAdd(nint handle, Func factory) - where T : ITypeBase + where T : UnmanagedResource { if (handle == nint.Zero) { @@ -26,7 +26,7 @@ public T GetOrAdd(nint handle, Func factory) var typedItem = factory(handle); - cache[handle] = new WeakReference(typedItem); + cache[handle] = new WeakReference(typedItem); return typedItem; } From 0335241aa6c6edcaae21f23a6607e0f62b276134 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 29 Oct 2023 18:39:46 -0300 Subject: [PATCH 167/186] refactor(improvements): extend from `UnmanagedResource` in `Doc` and `Branch` --- YDotNet/Document/Doc.cs | 54 +++++++++-------------- YDotNet/Document/Types/Branches/Branch.cs | 15 ++++--- 2 files changed, 31 insertions(+), 38 deletions(-) diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index bb3f3e1b..6a8c409f 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -29,7 +29,7 @@ namespace YDotNet.Document; /// to recursively nested types). /// /// -public class Doc : TypeBase, IDisposable +public class Doc : UnmanagedResource { private readonly EventSubscriber onAfterTransaction; private readonly EventSubscriber onClear; @@ -62,7 +62,7 @@ public Doc(DocOptions options) } internal Doc(nint handle, Doc? parent, bool isDeleted) - : base(isDeleted) + : base(handle, isDeleted) { this.parent = parent; @@ -126,8 +126,6 @@ internal Doc(nint handle, Doc? parent, bool isDeleted) return (DocChannel.ObserveSubDocs(doc, nint.Zero, callback), callback); }, (doc, s) => DocChannel.UnobserveSubDocs(doc, s)); - - Handle = handle; } /// @@ -207,17 +205,8 @@ public string? CollectionId } } - internal nint Handle { get; } - internal EventManager EventManager { get; } = new(); - /// - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - /// /// Finalizes an instance of the class. /// @@ -517,8 +506,27 @@ internal XmlElement GetXmlElement(nint handle, bool isDeleted) return GetOrAdd(handle, (h, doc) => new XmlElement(h, doc, isDeleted)); } + /// + protected override void DisposeCore(bool disposing) + { + if (IsDisposed) + { + return; + } + + MarkDisposed(); + + if (disposing) + { + // Clears all active subscriptions that have not been closed yet. + EventManager.Clear(); + } + + DocChannel.Destroy(Handle); + } + private T GetOrAdd(nint handle, Func factory) - where T : ITypeBase + where T : UnmanagedResource { var doc = GetRootDoc(); @@ -542,22 +550,4 @@ private static nint CreateDoc(DocOptions options) { return DocChannel.NewWithOptions(options.ToNative()); } - - private void Dispose(bool disposing) - { - if (IsDisposed) - { - return; - } - - MarkDisposed(); - - if (disposing) - { - // Clears all active subscriptions that have not been closed yet. - EventManager.Clear(); - } - - DocChannel.Destroy(Handle); - } } diff --git a/YDotNet/Document/Types/Branches/Branch.cs b/YDotNet/Document/Types/Branches/Branch.cs index d3e8cefb..7c1504f6 100644 --- a/YDotNet/Document/Types/Branches/Branch.cs +++ b/YDotNet/Document/Types/Branches/Branch.cs @@ -10,12 +10,12 @@ namespace YDotNet.Document.Types.Branches; /// /// The generic type that can be used to refer to all shared data type instances. /// -public abstract class Branch : TypeBase +public abstract class Branch : UnmanagedResource { private readonly EventSubscriber onDeep; internal Branch(nint handle, Doc doc, bool isDeleted) - : base(isDeleted) + : base(handle, isDeleted) { Doc = doc; @@ -36,14 +36,10 @@ internal Branch(nint handle, Doc doc, bool isDeleted) return (BranchChannel.ObserveDeep(branch, nint.Zero, callback), callback); }, (branch, s) => BranchChannel.UnobserveDeep(branch, s)); - - Handle = handle; } internal Doc Doc { get; } - internal nint Handle { get; } - /// /// Subscribes a callback function for changes performed within the instance /// and all nested types. @@ -97,4 +93,11 @@ public Transaction ReadTransaction() return new Transaction(handle, Doc); } + + /// + protected override void DisposeCore(bool disposing) + { + // Nothing should be done to dispose `Branch` instances (shared types). + // They're disposed automatically when their parent `Doc` is disposed. + } } From 1cc8db748ee9aa7a877f63e647b92aafa2917b44 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 29 Oct 2023 18:56:26 -0300 Subject: [PATCH 168/186] refactor(improvements): add `TODO` comment in `Doc.NotifyTransactionStarted()` --- YDotNet/Document/Doc.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/YDotNet/Document/Doc.cs b/YDotNet/Document/Doc.cs index 6a8c409f..5514f56f 100644 --- a/YDotNet/Document/Doc.cs +++ b/YDotNet/Document/Doc.cs @@ -471,6 +471,10 @@ internal Doc GetDoc(nint handle, bool isDeleted) return GetOrAdd(handle, (_, doc) => new Doc(handle, doc, isDeleted)); } + // TODO This is a temporary solution to track the amount of transactions a document has open. + // It's fragile because a cloned instance of the same document won't be synchronized and if a `Transaction` + // is opened in the cloned instance, it'll receive `null` from the Rust side and will cause the + // `ThrowHelper.PendingTransaction()` to run (which is acceptable since it's a managed exception). internal void NotifyTransactionStarted() { openTransactions++; From d55104bad2add2b755d8fa51ba804511dd6692c9 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 29 Oct 2023 18:57:44 -0300 Subject: [PATCH 169/186] refactor(improvements): adapt classes that implement `Resource` and `UnmanagedResource` --- YDotNet/Document/Cells/Input.cs | 2 +- YDotNet/Document/StickyIndexes/StickyIndex.cs | 2 +- YDotNet/Document/Transactions/Transaction.cs | 2 +- YDotNet/Document/Types/Arrays/ArrayIterator.cs | 2 +- YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs | 2 +- YDotNet/Document/Types/Maps/Events/MapEvent.cs | 2 +- YDotNet/Document/Types/Maps/MapIterator.cs | 2 +- YDotNet/Document/Types/Texts/Events/TextEvent.cs | 2 +- YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs | 2 +- YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalker.cs | 2 +- YDotNet/Document/Types/XmlElements/XmlAttributeIterator.cs | 2 +- YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs | 2 +- YDotNet/Document/UndoManagers/UndoManager.cs | 2 +- YDotNet/Infrastructure/Resource.cs | 2 +- YDotNet/Infrastructure/UnmanagedResource.cs | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/YDotNet/Document/Cells/Input.cs b/YDotNet/Document/Cells/Input.cs index d0705c11..ea670567 100644 --- a/YDotNet/Document/Cells/Input.cs +++ b/YDotNet/Document/Cells/Input.cs @@ -31,7 +31,7 @@ internal Input(InputNative native, params IDisposable[] allocatedMemory) } /// - protected internal override void DisposeCore(bool disposing) + protected override void DisposeCore(bool disposing) { foreach (var memory in allocatedMemory) { diff --git a/YDotNet/Document/StickyIndexes/StickyIndex.cs b/YDotNet/Document/StickyIndexes/StickyIndex.cs index 501a1efc..8cb08866 100644 --- a/YDotNet/Document/StickyIndexes/StickyIndex.cs +++ b/YDotNet/Document/StickyIndexes/StickyIndex.cs @@ -28,7 +28,7 @@ internal StickyIndex(nint handle) public StickyAssociationType AssociationType => (StickyAssociationType) StickyIndexChannel.AssociationType(Handle); /// - protected internal override void DisposeCore(bool disposing) + protected override void DisposeCore(bool disposing) { } diff --git a/YDotNet/Document/Transactions/Transaction.cs b/YDotNet/Document/Transactions/Transaction.cs index 9cfab56f..9f0b55cc 100644 --- a/YDotNet/Document/Transactions/Transaction.cs +++ b/YDotNet/Document/Transactions/Transaction.cs @@ -39,7 +39,7 @@ internal Transaction(nint handle, Doc doc) public bool Writeable => TransactionChannel.Writeable(Handle) == 1; /// - protected internal override void DisposeCore(bool disposing) + protected override void DisposeCore(bool disposing) { if (disposing) { diff --git a/YDotNet/Document/Types/Arrays/ArrayIterator.cs b/YDotNet/Document/Types/Arrays/ArrayIterator.cs index 4bea73f1..44e00f05 100644 --- a/YDotNet/Document/Types/Arrays/ArrayIterator.cs +++ b/YDotNet/Document/Types/Arrays/ArrayIterator.cs @@ -43,7 +43,7 @@ IEnumerator IEnumerable.GetEnumerator() } /// - protected internal override void DisposeCore(bool disposing) + protected override void DisposeCore(bool disposing) { ArrayChannel.IteratorDestroy(Handle); } diff --git a/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs b/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs index e101d9a4..b555f2e4 100644 --- a/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs +++ b/YDotNet/Document/Types/Arrays/Events/ArrayEvent.cs @@ -67,7 +67,7 @@ internal ArrayEvent(nint handle, Doc doc) public Array Target => target.Value; /// - protected internal override void DisposeCore(bool disposing) + protected override void DisposeCore(bool disposing) { // The event has no explicit garbage collection, but is released automatically after the event has been completed. } diff --git a/YDotNet/Document/Types/Maps/Events/MapEvent.cs b/YDotNet/Document/Types/Maps/Events/MapEvent.cs index bace8837..fc0d3d25 100644 --- a/YDotNet/Document/Types/Maps/Events/MapEvent.cs +++ b/YDotNet/Document/Types/Maps/Events/MapEvent.cs @@ -62,7 +62,7 @@ internal MapEvent(nint handle, Doc doc) public Map Target => target.Value; /// - protected internal override void DisposeCore(bool disposing) + protected override void DisposeCore(bool disposing) { // The event has no explicit garbage collection, it is released automatically after the event has been completed. } diff --git a/YDotNet/Document/Types/Maps/MapIterator.cs b/YDotNet/Document/Types/Maps/MapIterator.cs index 2b5ee079..1978a70f 100644 --- a/YDotNet/Document/Types/Maps/MapIterator.cs +++ b/YDotNet/Document/Types/Maps/MapIterator.cs @@ -50,7 +50,7 @@ IEnumerator IEnumerable.GetEnumerator() } /// - protected internal override void DisposeCore(bool disposing) + protected override void DisposeCore(bool disposing) { MapChannel.IteratorDestroy(Handle); } diff --git a/YDotNet/Document/Types/Texts/Events/TextEvent.cs b/YDotNet/Document/Types/Texts/Events/TextEvent.cs index 88068c0c..d47b64d9 100644 --- a/YDotNet/Document/Types/Texts/Events/TextEvent.cs +++ b/YDotNet/Document/Types/Texts/Events/TextEvent.cs @@ -67,7 +67,7 @@ internal TextEvent(nint handle, Doc doc) public Text Target => target.Value; /// - protected internal override void DisposeCore(bool disposing) + protected override void DisposeCore(bool disposing) { // The event has no explicit garbage collection, but is released automatically after the event has been completed. } diff --git a/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs b/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs index 34a44213..a7dc12d3 100644 --- a/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs +++ b/YDotNet/Document/Types/XmlElements/Events/XmlElementEvent.cs @@ -76,7 +76,7 @@ internal XmlElementEvent(nint handle, Doc doc) public XmlElement Target => target.Value; /// - protected internal override void DisposeCore(bool disposing) + protected override void DisposeCore(bool disposing) { // The event has no explicit garbage collection, but is released automatically after the event has been completed. } diff --git a/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalker.cs b/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalker.cs index 4720e610..beb5f00e 100644 --- a/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalker.cs +++ b/YDotNet/Document/Types/XmlElements/Trees/XmlTreeWalker.cs @@ -44,7 +44,7 @@ IEnumerator IEnumerable.GetEnumerator() } /// - protected internal override void DisposeCore(bool disposing) + protected override void DisposeCore(bool disposing) { XmlElementChannel.TreeWalkerDestroy(Handle); } diff --git a/YDotNet/Document/Types/XmlElements/XmlAttributeIterator.cs b/YDotNet/Document/Types/XmlElements/XmlAttributeIterator.cs index c5422dca..ae67effa 100644 --- a/YDotNet/Document/Types/XmlElements/XmlAttributeIterator.cs +++ b/YDotNet/Document/Types/XmlElements/XmlAttributeIterator.cs @@ -39,7 +39,7 @@ IEnumerator IEnumerable.GetEnumerator() } /// - protected internal override void DisposeCore(bool disposing) + protected override void DisposeCore(bool disposing) { XmlAttributeChannel.IteratorDestroy(Handle); } diff --git a/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs b/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs index 3ddcefb0..4ea1360c 100644 --- a/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs +++ b/YDotNet/Document/Types/XmlTexts/Events/XmlTextEvent.cs @@ -61,7 +61,7 @@ internal XmlTextEvent(nint handle, Doc doc) public XmlText Target => target.Value; /// - protected internal override void DisposeCore(bool disposing) + protected override void DisposeCore(bool disposing) { // The event has no explicit garbage collection, but is released automatically after the event has been completed. } diff --git a/YDotNet/Document/UndoManagers/UndoManager.cs b/YDotNet/Document/UndoManagers/UndoManager.cs index 122a33fe..dbefa450 100644 --- a/YDotNet/Document/UndoManagers/UndoManager.cs +++ b/YDotNet/Document/UndoManagers/UndoManager.cs @@ -177,7 +177,7 @@ public void RemoveOrigin(byte[] origin) } /// - protected internal override void DisposeCore(bool disposing) + protected override void DisposeCore(bool disposing) { UndoManagerChannel.Destroy(Handle); } diff --git a/YDotNet/Infrastructure/Resource.cs b/YDotNet/Infrastructure/Resource.cs index 9dfb453e..f828a599 100644 --- a/YDotNet/Infrastructure/Resource.cs +++ b/YDotNet/Infrastructure/Resource.cs @@ -5,7 +5,7 @@ namespace YDotNet.Infrastructure; /// public abstract class Resource : IDisposable { - internal Resource(bool isDisposed) + internal Resource(bool isDisposed = false) { IsDisposed = isDisposed; } diff --git a/YDotNet/Infrastructure/UnmanagedResource.cs b/YDotNet/Infrastructure/UnmanagedResource.cs index 7aac84f4..8d58e429 100644 --- a/YDotNet/Infrastructure/UnmanagedResource.cs +++ b/YDotNet/Infrastructure/UnmanagedResource.cs @@ -5,7 +5,7 @@ namespace YDotNet.Infrastructure; /// public abstract class UnmanagedResource : Resource { - internal UnmanagedResource(nint handle, bool isDisposed) + internal UnmanagedResource(nint handle, bool isDisposed = false) : base(isDisposed) { Handle = handle; From 95e7cd0addde73eb95d56da22bbb1d08e1356a5b Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 29 Oct 2023 18:59:57 -0300 Subject: [PATCH 170/186] refactor(improvements): remove unused method `TypeCache.Delete()` --- YDotNet/Infrastructure/TypeCache.cs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/YDotNet/Infrastructure/TypeCache.cs b/YDotNet/Infrastructure/TypeCache.cs index 782b3a16..ab38578e 100644 --- a/YDotNet/Infrastructure/TypeCache.cs +++ b/YDotNet/Infrastructure/TypeCache.cs @@ -31,20 +31,6 @@ public T GetOrAdd(nint handle, Func factory) return typedItem; } - // TODO [LSViana] Check with Sebastian why this method isn't used anywhere. - public void Delete(nint handle) - { - if (cache.TryGetValue(handle, out var weakRef)) - { - if (weakRef.TryGetTarget(out var item)) - { - item.MarkDisposed(); - } - - cache.Remove(handle); - } - } - private void Cleanup() { List? keysToDelete = null; From 4a621fdcfef1cc42d3b0d5765fad7c62aeb42b62 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 29 Oct 2023 19:04:19 -0300 Subject: [PATCH 171/186] docs(improvements): fix typo in `decisions.md` --- docs/decisions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/decisions.md b/docs/decisions.md index 4bd23dff..377bc086 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -68,7 +68,7 @@ This can cause an application crash when: New base classes have been introduced to ensure that this does not happen. -## 2023-10-16 Not all transaction cases are catched by yrs +## 2023-10-16 Not all transaction cases are caught by yrs The application crashes if a new root object is created while a `Transaction` is open. From 1aae9e81857609d4e9ffb490c3eceeb0d2636728 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 29 Oct 2023 19:12:58 -0300 Subject: [PATCH 172/186] docs(improvements): include examples to make it clearer when to use properties or methods in `*Native` types --- docs/decisions.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/decisions.md b/docs/decisions.md index 377bc086..665a47bd 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -23,10 +23,16 @@ Therefore, you must ensure that: 2. Native types are passed to the CLR types via constructors. -3. Native types are responsible to follow pointers, -e.g. [`UndoEventNative`](https://github.com/SebastianStehle/ydotnet/blob/main/YDotNet/Native/UndoManager/Events/UndoEventNative.cs#L18C5-L27C1) -and everything around reading memory is part of the native part. These reading operations are provided via methods to -indicate that it is not a property and not part of the struct itself. +3. Native types are responsible to follow pointers. + + - For example, types like [`UndoEventNative`](https://github.com/SebastianStehle/ydotnet/blob/main/YDotNet/Native/UndoManager/Events/UndoEventNative.cs#L18C5-L27C1) + should be responsible for providing access to memory written by the Rust side. + - To decide between using a method or a property, use this rule of thumb: + + - If the data is contained inside the `struct`, use properties (when there's a known number of properties, + like `nint`, `byte`, and other primitive types) + - If the data is outside the `struct`, use methods (for example, to read the data that's accessible via an + `nint Handle` property and has size of `uint Length`) Some objects also need the handle and the native `struct`. Therefore a new type has been introduced: From 443209c9f00a888e9309655b7dcb46edde6ef615 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 29 Oct 2023 19:22:21 -0300 Subject: [PATCH 173/186] ci(improvements): fix tag pattern to trigger `publish.yml` --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a9fd8262..8d7648ab 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,7 +6,7 @@ on: - 'main' - 'for-pr' tags: - - 'v[0-9]+.[0-9]+.[0.9]+' + - 'v*.*.*' jobs: pack-nuget: From d8300967e59c32504be015dbd6f40b153e26629f Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Sun, 29 Oct 2023 19:37:01 -0300 Subject: [PATCH 174/186] test(improvements): rename the `ClientIdTests` to `ClientIdGeneratorTests` --- .../Infrastructure/ClientIdGeneratorTests.cs | 13 +++++++++++++ .../Infrastructure/ClientIdTests.cs | 18 ------------------ 2 files changed, 13 insertions(+), 18 deletions(-) create mode 100644 Tests/YDotNet.Tests.Unit/Infrastructure/ClientIdGeneratorTests.cs delete mode 100644 Tests/YDotNet.Tests.Unit/Infrastructure/ClientIdTests.cs diff --git a/Tests/YDotNet.Tests.Unit/Infrastructure/ClientIdGeneratorTests.cs b/Tests/YDotNet.Tests.Unit/Infrastructure/ClientIdGeneratorTests.cs new file mode 100644 index 00000000..a3f34f7d --- /dev/null +++ b/Tests/YDotNet.Tests.Unit/Infrastructure/ClientIdGeneratorTests.cs @@ -0,0 +1,13 @@ +using NUnit.Framework; +using YDotNet.Infrastructure; + +namespace YDotNet.Tests.Unit.Infrastructure; + +public class ClientIdGeneratorTests +{ + [Test] + public void HasCorrectMaxValue() + { + Assert.That(ClientIdGenerator.MaxSafeInteger, Is.EqualTo((2 ^ 53) - 1)); + } +} diff --git a/Tests/YDotNet.Tests.Unit/Infrastructure/ClientIdTests.cs b/Tests/YDotNet.Tests.Unit/Infrastructure/ClientIdTests.cs deleted file mode 100644 index 9cce0793..00000000 --- a/Tests/YDotNet.Tests.Unit/Infrastructure/ClientIdTests.cs +++ /dev/null @@ -1,18 +0,0 @@ -using NUnit.Framework; -using YDotNet.Infrastructure; - -namespace YDotNet.Tests.Unit.Infrastructure; - -public class ClientIdTests -{ - [Test] - public void TestSafety() - { - for (var i = 0; i < 10_000_000; i++) - { - var id = ClientIdGenerator.Random(); - - Assert.That(id, Is.LessThanOrEqualTo(ClientIdGenerator.MaxSafeInteger)); - } - } -} From b631b39211d2d36f81c6da6a90a9f26cc29fe667 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 30 Oct 2023 09:05:00 +0100 Subject: [PATCH 175/186] Fix warnings in my code. --- Tests/YDotNet.Tests.Unit/Protocol/WriteAndRead.cs | 2 +- YDotNet.Extensions/YDotNetExtensions.cs | 4 +--- YDotNet/Document/Cells/Output.cs | 2 +- YDotNet/Infrastructure/ClientIdGenerator.cs | 4 ++-- YDotNet/Infrastructure/MemoryReader.cs | 12 ++++++++---- YDotNet/Native/NativeWithHandle.cs | 2 +- YDotNet/Protocol/BufferEncoder.cs | 6 ++---- YDotNet/Protocol/Decoder.cs | 3 +-- YDotNet/Protocol/Encoder.cs | 15 +++++---------- 9 files changed, 22 insertions(+), 28 deletions(-) diff --git a/Tests/YDotNet.Tests.Unit/Protocol/WriteAndRead.cs b/Tests/YDotNet.Tests.Unit/Protocol/WriteAndRead.cs index 8f69a6cd..b98dab5e 100644 --- a/Tests/YDotNet.Tests.Unit/Protocol/WriteAndRead.cs +++ b/Tests/YDotNet.Tests.Unit/Protocol/WriteAndRead.cs @@ -10,7 +10,7 @@ public class WriteAndRead [TestCase(0ul)] [TestCase(1ul)] [TestCase(3826503548ul)] - [TestCase(ClientId.MaxSafeInteger)] + [TestCase(ClientIdGenerator.MaxSafeInteger)] public async Task EncodeAndDecodeInt(ulong input) { // Arrange diff --git a/YDotNet.Extensions/YDotNetExtensions.cs b/YDotNet.Extensions/YDotNetExtensions.cs index 6a66f889..b1cb6867 100644 --- a/YDotNet.Extensions/YDotNetExtensions.cs +++ b/YDotNet.Extensions/YDotNetExtensions.cs @@ -122,9 +122,7 @@ static void WriteValue(Output output, Utf8JsonWriter jsonWriter, Transaction tra { switch (output.Tag) { - case OutputTag.NotSet: - break; - case OutputTag.Bool: + case OutputTag.Boolean: jsonWriter.WriteBooleanValue(output.Boolean); break; case OutputTag.Double: diff --git a/YDotNet/Document/Cells/Output.cs b/YDotNet/Document/Cells/Output.cs index 83d32d7e..41b91073 100644 --- a/YDotNet/Document/Cells/Output.cs +++ b/YDotNet/Document/Cells/Output.cs @@ -42,7 +42,7 @@ internal Output(nint handle, Doc doc, bool isDeleted) /// Value is not a . public string String => GetValue(OutputTag.String); -#pragma warning disable SA1623 (This property documentation shouldn't start with the standard text) +#pragma warning disable SA1623 // This property documentation shouldn't start with the standard text /// /// Gets the value. /// diff --git a/YDotNet/Infrastructure/ClientIdGenerator.cs b/YDotNet/Infrastructure/ClientIdGenerator.cs index 6cb055f9..1abd52c6 100644 --- a/YDotNet/Infrastructure/ClientIdGenerator.cs +++ b/YDotNet/Infrastructure/ClientIdGenerator.cs @@ -1,4 +1,4 @@ -namespace YDotNet.Infrastructure; +namespace YDotNet.Infrastructure; /// /// Helper class to deal with client ids. @@ -8,7 +8,7 @@ public static class ClientIdGenerator /// /// The maximum safe integer from javascript. /// - public const long MaxSafeInteger = 2 ^ (53 - 1); + public const ulong MaxSafeInteger = 2 ^ (53 - 1); /// /// Gets a random client id. diff --git a/YDotNet/Infrastructure/MemoryReader.cs b/YDotNet/Infrastructure/MemoryReader.cs index 7ddc9b00..79388902 100644 --- a/YDotNet/Infrastructure/MemoryReader.cs +++ b/YDotNet/Infrastructure/MemoryReader.cs @@ -17,7 +17,8 @@ internal static unsafe byte[] ReadBytes(nint handle, uint length) return data; } - internal static T[] ReadStructs(nint handle, uint length) where T : struct + internal static T[] ReadStructs(nint handle, uint length) + where T : struct { var itemSize = Marshal.SizeOf(); var itemBuffer = new T[length]; @@ -31,7 +32,8 @@ internal static T[] ReadStructs(nint handle, uint length) where T : struct return itemBuffer; } - internal static nint[] ReadPointers(nint handle, uint length) where T : struct + internal static nint[] ReadPointers(nint handle, uint length) + where T : struct { var itemSize = Marshal.SizeOf(); var itemBuffer = new nint[length]; @@ -45,7 +47,8 @@ internal static nint[] ReadPointers(nint handle, uint length) where T : struc return itemBuffer; } - internal static NativeWithHandle[] ReadStructsWithHandles(nint handle, uint length) where T : struct + internal static NativeWithHandle[] ReadStructsWithHandles(nint handle, uint length) + where T : struct { var itemSize = Marshal.SizeOf(); var itemBuffer = new NativeWithHandle[length]; @@ -59,7 +62,8 @@ internal static NativeWithHandle[] ReadStructsWithHandles(nint handle, uin return itemBuffer; } - internal static T ReadStruct(nint handle) where T : struct + internal static T ReadStruct(nint handle) + where T : struct { return Marshal.PtrToStructure(handle.Checked()); } diff --git a/YDotNet/Native/NativeWithHandle.cs b/YDotNet/Native/NativeWithHandle.cs index 834d2570..c4771995 100644 --- a/YDotNet/Native/NativeWithHandle.cs +++ b/YDotNet/Native/NativeWithHandle.cs @@ -1,4 +1,4 @@ namespace YDotNet.Native; -internal record struct NativeWithHandle(T Value, nint Handle) +internal record struct NativeWithHandle(T Value, nint Handle) where T : struct; diff --git a/YDotNet/Protocol/BufferEncoder.cs b/YDotNet/Protocol/BufferEncoder.cs index 54577e98..1d043170 100644 --- a/YDotNet/Protocol/BufferEncoder.cs +++ b/YDotNet/Protocol/BufferEncoder.cs @@ -17,16 +17,14 @@ public byte[] ToArray() } /// - protected override ValueTask WriteByteAsync(byte value, - CancellationToken ct) + protected override ValueTask WriteByteAsync(byte value, CancellationToken ct) { buffer.WriteByte(value); return default; } /// - protected override ValueTask WriteBytesAsync(ArraySegment bytes, - CancellationToken ct) + protected override ValueTask WriteBytesAsync(ArraySegment bytes, CancellationToken ct) { buffer.Write(bytes); return default; diff --git a/YDotNet/Protocol/Decoder.cs b/YDotNet/Protocol/Decoder.cs index 70c3bea8..095ac19a 100644 --- a/YDotNet/Protocol/Decoder.cs +++ b/YDotNet/Protocol/Decoder.cs @@ -114,6 +114,5 @@ protected abstract ValueTask ReadByteAsync( /// /// The task that completes when everything has been read. /// - protected abstract ValueTask ReadBytesAsync(Memory bytes, - CancellationToken ct); + protected abstract ValueTask ReadBytesAsync(Memory bytes, CancellationToken ct); } diff --git a/YDotNet/Protocol/Encoder.cs b/YDotNet/Protocol/Encoder.cs index ae7f81a4..8eca1b84 100644 --- a/YDotNet/Protocol/Encoder.cs +++ b/YDotNet/Protocol/Encoder.cs @@ -18,8 +18,7 @@ public abstract class Encoder /// /// The task representing the async operation. /// - public async ValueTask WriteVarUintAsync(ulong value, - CancellationToken ct = default) + public async ValueTask WriteVarUintAsync(ulong value, CancellationToken ct = default) { do { @@ -46,8 +45,7 @@ public async ValueTask WriteVarUintAsync(ulong value, /// The task representing the async operation. /// /// is null. - public async ValueTask WriteVarUint8Array(byte[] value, - CancellationToken ct = default) + public async ValueTask WriteVarUint8Array(byte[] value, CancellationToken ct = default) { if (value == null) { @@ -67,8 +65,7 @@ public async ValueTask WriteVarUint8Array(byte[] value, /// The task representing the async operation. /// /// is null. - public async ValueTask WriteVarStringAsync(string value, - CancellationToken ct = default) + public async ValueTask WriteVarStringAsync(string value, CancellationToken ct = default) { if (value == null) { @@ -110,8 +107,7 @@ async Task WriteCoreAsync(string value, byte[] buffer, CancellationToken ct) /// /// The task representing the async operation. /// - protected abstract ValueTask WriteByteAsync(byte value, - CancellationToken ct); + protected abstract ValueTask WriteByteAsync(byte value, CancellationToken ct); /// /// Write a byte array. @@ -121,6 +117,5 @@ protected abstract ValueTask WriteByteAsync(byte value, /// /// The task representing the async operation. /// - protected abstract ValueTask WriteBytesAsync(ArraySegment bytes, - CancellationToken ct); + protected abstract ValueTask WriteBytesAsync(ArraySegment bytes, CancellationToken ct); } From 7880e05c6ed2f79be9754523ffc6fd51a9edbe27 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Mon, 30 Oct 2023 22:58:22 -0300 Subject: [PATCH 176/186] feat(improvements): fix the `Output.BuildValue()` method to handle `Null` and `Undefined` values --- YDotNet/Document/Cells/Output.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/YDotNet/Document/Cells/Output.cs b/YDotNet/Document/Cells/Output.cs index 41b91073..1c2155f7 100644 --- a/YDotNet/Document/Cells/Output.cs +++ b/YDotNet/Document/Cells/Output.cs @@ -168,6 +168,10 @@ internal static Output CreateAndRelease(nint handle, Doc doc) case OutputTag.Doc: return doc.GetDoc(OutputChannel.Doc(handle), isDeleted); + case OutputTag.Null: + case OutputTag.Undefined: + return null; + default: throw new YDotNetException($"Unsupported OutputTag value: {tag}"); } From e8e5da56f92ded407d15ecb7a6ddcd959eb165ef Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Mon, 30 Oct 2023 22:58:36 -0300 Subject: [PATCH 177/186] feat(improvements): add the `Output.Undefined` and `Output.Null` properties --- YDotNet/Document/Cells/Output.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/YDotNet/Document/Cells/Output.cs b/YDotNet/Document/Cells/Output.cs index 1c2155f7..09ba6f87 100644 --- a/YDotNet/Document/Cells/Output.cs +++ b/YDotNet/Document/Cells/Output.cs @@ -115,6 +115,16 @@ internal Output(nint handle, Doc doc, bool isDeleted) /// Value is not a . public XmlText XmlText => GetValue(OutputTag.XmlText); + /// + /// Gets a value indicating whether this cell contains the value . + /// + public bool Undefined => Tag == OutputTag.Undefined; + + /// + /// Gets a value indicating whether this cell contains the value . + /// + public bool Null => Tag == OutputTag.Null; + internal static Output CreateAndRelease(nint handle, Doc doc) { var result = new Output(handle, doc, isDeleted: false); From 50929b19d1ae9c23496eb8416f974e04baff9bb3 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Mon, 30 Oct 2023 22:58:59 -0300 Subject: [PATCH 178/186] test(improvements): update the `Output.Get()` tests to check the `Output.Undefined` property --- Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs b/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs index 3c21d64d..56d7942f 100644 --- a/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs +++ b/Tests/YDotNet.Tests.Unit/Arrays/GetTests.cs @@ -53,6 +53,7 @@ public void GetAtMiddle() // Assert Assert.That(output, Is.Not.Null); Assert.That(output.Tag, Is.EqualTo(OutputTag.Undefined)); + Assert.That(output.Undefined, Is.True); } [Test] @@ -113,12 +114,7 @@ public void GetOnInvalidIndex() var transaction = doc.WriteTransaction(); array.InsertRange( - transaction, index: 0, new[] - { - Input.Boolean(value: true), - Input.Undefined(), - Input.String("Lucas") - }); + transaction, index: 0, Input.Boolean(value: true), Input.Undefined(), Input.String("Lucas")); transaction.Commit(); return (doc, array); From 90d91e030fd014fb04b82bdec6ff7aa6da6acf00 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Mon, 30 Oct 2023 22:59:31 -0300 Subject: [PATCH 179/186] test(improvements): fix the `Transaction.StateDiffV1()` tests --- Tests/YDotNet.Tests.Unit/Transactions/StateDiffV1Tests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/YDotNet.Tests.Unit/Transactions/StateDiffV1Tests.cs b/Tests/YDotNet.Tests.Unit/Transactions/StateDiffV1Tests.cs index 425644f9..c966264e 100644 --- a/Tests/YDotNet.Tests.Unit/Transactions/StateDiffV1Tests.cs +++ b/Tests/YDotNet.Tests.Unit/Transactions/StateDiffV1Tests.cs @@ -16,7 +16,7 @@ public void Null() // Assert Assert.That(stateDiff, Is.Not.Null); - Assert.That(stateDiff, Has.Length.InRange(from: 22, to: 26)); + Assert.That(stateDiff, Has.Length.InRange(from: 18, to: 26)); } [Test] From e7e2c7717ec51f2a84ad9fba1e8056d9449d825d Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Mon, 30 Oct 2023 23:01:28 -0300 Subject: [PATCH 180/186] fix(improvements): update `Transaction.StateDiffV2()` to support null `stateVector` values --- YDotNet/Document/Transactions/Transaction.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/YDotNet/Document/Transactions/Transaction.cs b/YDotNet/Document/Transactions/Transaction.cs index 9f0b55cc..31884d29 100644 --- a/YDotNet/Document/Transactions/Transaction.cs +++ b/YDotNet/Document/Transactions/Transaction.cs @@ -152,7 +152,11 @@ public byte[] StateDiffV1(byte[] stateVector) /// public byte[] StateDiffV2(byte[] stateVector) { - var handle = TransactionChannel.StateDiffV2(Handle, stateVector, (uint) stateVector.Length, out var length); + var handle = TransactionChannel.StateDiffV2( + Handle, + stateVector, + (uint) (stateVector != null ? stateVector.Length : 0), + out var length); return MemoryReader.ReadAndDestroyBytes(handle, length); } From 3494b2ece8044107335c65d02c57607afb9a70f0 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Mon, 30 Oct 2023 23:02:25 -0300 Subject: [PATCH 181/186] refactor(improvements): make the `stateVector` parameter optional for `StateDiffV1()` and `StateDiffV2()` in the `Transaction` class --- YDotNet/Document/Transactions/Transaction.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/YDotNet/Document/Transactions/Transaction.cs b/YDotNet/Document/Transactions/Transaction.cs index 31884d29..7843a011 100644 --- a/YDotNet/Document/Transactions/Transaction.cs +++ b/YDotNet/Document/Transactions/Transaction.cs @@ -112,16 +112,16 @@ public byte[] StateVectorV1() /// /// /// - /// The state vector to be used as base for comparison and generation of the difference. + /// The optional state vector to be used as base for comparison and generation of the difference. /// /// /// The lib0 v1 encoded state difference between the of this and the /// remote . /// - public byte[] StateDiffV1(byte[] stateVector) + public byte[] StateDiffV1(byte[]? stateVector) { var handle = TransactionChannel.StateDiffV1( - Handle, stateVector, (uint) (stateVector != null ? stateVector.Length : 0), out var length); + Handle, stateVector, (uint) (stateVector?.Length ?? 0), out var length); return MemoryReader.ReadAndDestroyBytes(handle, length); } @@ -144,18 +144,18 @@ public byte[] StateDiffV1(byte[] stateVector) /// /// /// - /// The state vector to be used as base for comparison and generation of the difference. + /// The optional state vector to be used as base for comparison and generation of the difference. /// /// /// The lib0 v2 encoded state difference between the of this and the /// remote . /// - public byte[] StateDiffV2(byte[] stateVector) + public byte[] StateDiffV2(byte[]? stateVector) { var handle = TransactionChannel.StateDiffV2( Handle, stateVector, - (uint) (stateVector != null ? stateVector.Length : 0), + (uint) (stateVector?.Length ?? 0), out var length); return MemoryReader.ReadAndDestroyBytes(handle, length); From 02549b0ca8b7c87148a875adf22f898573e4ee06 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Mon, 30 Oct 2023 23:02:35 -0300 Subject: [PATCH 182/186] test(improvements): fix the `Transaction.StateDiffV2()` tests --- Tests/YDotNet.Tests.Unit/Transactions/StateDiffV2Tests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/YDotNet.Tests.Unit/Transactions/StateDiffV2Tests.cs b/Tests/YDotNet.Tests.Unit/Transactions/StateDiffV2Tests.cs index cebfb75c..aef99adc 100644 --- a/Tests/YDotNet.Tests.Unit/Transactions/StateDiffV2Tests.cs +++ b/Tests/YDotNet.Tests.Unit/Transactions/StateDiffV2Tests.cs @@ -16,7 +16,7 @@ public void Null() // Assert Assert.That(stateDiff, Is.Not.Null); - Assert.That(stateDiff, Has.Length.InRange(from: 32, to: 38)); + Assert.That(stateDiff, Has.Length.InRange(from: 29, to: 38)); } [Test] From 48d7258a0a6cd52e16380b03614776cebec91e59 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Mon, 30 Oct 2023 23:07:23 -0300 Subject: [PATCH 183/186] fix(improvements): fix the `MapEvent` class to read `Path` from the correct native method --- YDotNet/Document/Types/Maps/Events/MapEvent.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/YDotNet/Document/Types/Maps/Events/MapEvent.cs b/YDotNet/Document/Types/Maps/Events/MapEvent.cs index fc0d3d25..02b8049f 100644 --- a/YDotNet/Document/Types/Maps/Events/MapEvent.cs +++ b/YDotNet/Document/Types/Maps/Events/MapEvent.cs @@ -1,7 +1,6 @@ using YDotNet.Document.Types.Events; using YDotNet.Infrastructure; using YDotNet.Infrastructure.Extensions; -using YDotNet.Native.Types; using YDotNet.Native.Types.Maps; namespace YDotNet.Document.Types.Maps.Events; @@ -21,7 +20,7 @@ internal MapEvent(nint handle, Doc doc) path = new Lazy( () => { - var pathHandle = ArrayChannel.ObserveEventPath(handle, out var length).Checked(); + var pathHandle = MapChannel.ObserveEventPath(handle, out var length).Checked(); return new EventPath(pathHandle, length); }); From 45f3eab5e76cd98c98f933928a797111b622475a Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Mon, 30 Oct 2023 23:23:49 -0300 Subject: [PATCH 184/186] fix(improvements): fix the type of the `TextChunkNative.Data` property --- YDotNet/Native/Types/Texts/TextChunkNative.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/YDotNet/Native/Types/Texts/TextChunkNative.cs b/YDotNet/Native/Types/Texts/TextChunkNative.cs index 5cb037cb..36d4a6c5 100644 --- a/YDotNet/Native/Types/Texts/TextChunkNative.cs +++ b/YDotNet/Native/Types/Texts/TextChunkNative.cs @@ -10,8 +10,8 @@ internal readonly struct TextChunkNative { public const int Size = OutputNative.Size + 8 + 8; - [field: FieldOffset(0)] - public nint Data { get; } + [field: FieldOffset(offset: 0)] + public OutputNative Data { get; } [field: FieldOffset(OutputNative.Size)] public uint AttributesLength { get; } From 2366e0f43908afa3d19b116e560d174eba2c5fcf Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Mon, 30 Oct 2023 23:24:07 -0300 Subject: [PATCH 185/186] fix(improvements): fix the reading of the `Output` cell in `TextChunk` --- YDotNet/Document/Types/Texts/TextChunk.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/YDotNet/Document/Types/Texts/TextChunk.cs b/YDotNet/Document/Types/Texts/TextChunk.cs index 321e0b21..751682d2 100644 --- a/YDotNet/Document/Types/Texts/TextChunk.cs +++ b/YDotNet/Document/Types/Texts/TextChunk.cs @@ -11,7 +11,8 @@ public class TextChunk { internal TextChunk(NativeWithHandle native, Doc doc) { - Data = new Output(native.Value.Data, doc, isDeleted: false); + // `Handle` is used because the `OutputNative` is located at the head of `TextChunkNative`. + Data = new Output(native.Handle, doc, isDeleted: false); Attributes = native.Value.Attributes() .ToDictionary( From 0dbb59c203eb1ee8566daa838add2314a86cf983 Mon Sep 17 00:00:00 2001 From: Lucas Viana Date: Mon, 30 Oct 2023 23:27:27 -0300 Subject: [PATCH 186/186] test(improvements): fix tests for `Redo` and `Undo` in the `UndoManager` class --- Tests/YDotNet.Tests.Unit/UndoManagers/RedoTests.cs | 14 ++++---------- Tests/YDotNet.Tests.Unit/UndoManagers/UndoTests.cs | 13 ++++--------- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/Tests/YDotNet.Tests.Unit/UndoManagers/RedoTests.cs b/Tests/YDotNet.Tests.Unit/UndoManagers/RedoTests.cs index e76b9483..ecfe3dc5 100644 --- a/Tests/YDotNet.Tests.Unit/UndoManagers/RedoTests.cs +++ b/Tests/YDotNet.Tests.Unit/UndoManagers/RedoTests.cs @@ -58,7 +58,7 @@ public void RedoAddingAndUpdatingAndRemovingContentOnText() { "bold", Input.Boolean(value: true) } })); transaction.Commit(); - undoManager.Redo(); + undoManager.Undo(); result = undoManager.Redo(); transaction = doc.ReadTransaction(); @@ -67,7 +67,7 @@ public void RedoAddingAndUpdatingAndRemovingContentOnText() // Assert Assert.That(chunks.Length, Is.EqualTo(expected: 3)); - Assert.That(result, Is.False); + Assert.That(result, Is.True); // Act (remove, undo, and redo) transaction = doc.WriteTransaction(); @@ -97,18 +97,12 @@ public void RedoAddingAndUpdatingAndRemovingContentOnArray() var transaction = doc.WriteTransaction(); array.InsertRange( transaction, - index: 0, - new[] - { - Input.Boolean(value: true), - Input.Long(value: 2469L), - Input.String("Lucas") - }); + index: 0, Input.Boolean(value: true), Input.Long(value: 2469L), Input.String("Lucas")); transaction.Commit(); // Act (add, undo, and redo) transaction = doc.WriteTransaction(); - array.InsertRange(transaction, index: 3, new[] { Input.Undefined() }); + array.InsertRange(transaction, index: 3, Input.Undefined()); transaction.Commit(); undoManager.Undo(); var result = undoManager.Redo(); diff --git a/Tests/YDotNet.Tests.Unit/UndoManagers/UndoTests.cs b/Tests/YDotNet.Tests.Unit/UndoManagers/UndoTests.cs index 99ddfb95..b63de805 100644 --- a/Tests/YDotNet.Tests.Unit/UndoManagers/UndoTests.cs +++ b/Tests/YDotNet.Tests.Unit/UndoManagers/UndoTests.cs @@ -8,6 +8,7 @@ namespace YDotNet.Tests.Unit.UndoManagers; public class UndoTests { [Test] + [Ignore("Waiting for fix on yffi.")] public void ReturnsFalseWhenNoChangesApplied() { // Arrange @@ -82,7 +83,7 @@ public void UndoAddingAndUpdatingAndRemovingContentOnText() } [Test] - [Ignore("There seems to be a bug in y-crdt.")] + [Ignore("Waiting for fix on yffi.")] public void UndoAddingAndUpdatingAndRemovingContentOnArray() { // Arrange @@ -92,18 +93,12 @@ public void UndoAddingAndUpdatingAndRemovingContentOnArray() var transaction = doc.WriteTransaction(); array.InsertRange( transaction, - index: 0, - new[] - { - Input.Boolean(value: true), - Input.Long(value: 2469L), - Input.String("Lucas") - }); + index: 0, Input.Boolean(value: true), Input.Long(value: 2469L), Input.String("Lucas")); transaction.Commit(); // Act (add and undo) transaction = doc.WriteTransaction(); - array.InsertRange(transaction, index: 3, new[] { Input.Undefined() }); + array.InsertRange(transaction, index: 3, Input.Undefined()); transaction.Commit(); var result = undoManager.Undo();