diff --git a/Demo/Callback.cs b/Demo/Callback.cs index 63f66f46..26e3a513 100644 --- a/Demo/Callback.cs +++ b/Demo/Callback.cs @@ -14,6 +14,18 @@ public Callback(ILogger log) this.log = log; } + public ValueTask OnAwarenessUpdatedAsync(ClientAwarenessEvent @event) + { + log.LogInformation("Client {clientId} awareness changed.", @event.Context.ClientId); + return default; + } + + public ValueTask OnClientDisconnectedAsync(ClientDisconnectedEvent @event) + { + log.LogInformation("Client {clientId} disconnected.", @event.Context.ClientId); + return default; + } + public ValueTask OnDocumentLoadedAsync(DocumentLoadEvent @event) { if (@event.Context.DocumentName == "notifications") @@ -69,6 +81,7 @@ await @event.Source.UpdateDocAsync(notificationCtx, (doc) => { List notifications; + // Keep the transaction open as short as possible. using (var transaction = @event.Document.ReadTransaction()) { notifications = newNotificationsRaw.Select(x => x.To(transaction)).ToList(); @@ -78,7 +91,8 @@ await @event.Source.UpdateDocAsync(notificationCtx, (doc) => 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.")) + // Keep the transaction open as short as possible. + using (var transaction = doc.WriteTransaction()) { array.InsertRange(transaction, array.Length, notifications.Select(x => x.ToInput()).ToArray()); } diff --git a/Demo/Client/src/components/Awareness.tsx b/Demo/Client/src/components/Awareness.tsx index dca5edb8..0284e413 100644 --- a/Demo/Client/src/components/Awareness.tsx +++ b/Demo/Client/src/components/Awareness.tsx @@ -29,6 +29,6 @@ export const Awareness = () => { }, [awareness]); return ( - + ); }; \ No newline at end of file diff --git a/Demo/Client/src/components/Chat.tsx b/Demo/Client/src/components/Chat.tsx index 843e6d73..e926f01b 100644 --- a/Demo/Client/src/components/Chat.tsx +++ b/Demo/Client/src/components/Chat.tsx @@ -63,7 +63,7 @@ export const Chat = ({ isReadonly }: { isReadonly: boolean }) => { {!isReadonly && - + diff --git a/Demo/Client/src/components/Increment.tsx b/Demo/Client/src/components/Increment.tsx index f5a152a5..8c5528a5 100644 --- a/Demo/Client/src/components/Increment.tsx +++ b/Demo/Client/src/components/Increment.tsx @@ -9,7 +9,7 @@ export const Increment = () => { React.useEffect(() => { const handler = () => { - setState(map.get('value') || 0); + setState(map.get('value') as number || 0); }; handler(); @@ -35,7 +35,7 @@ export const Increment = () => { }; return ( - + diff --git a/Demo/Client/src/components/YjsMonacoEditor.tsx b/Demo/Client/src/components/YjsMonacoEditor.tsx index f786c4c7..5054d441 100644 --- a/Demo/Client/src/components/YjsMonacoEditor.tsx +++ b/Demo/Client/src/components/YjsMonacoEditor.tsx @@ -21,9 +21,7 @@ export const YjsMonacoEditor = () => { return (
- _onEditorDidMount(e)} - /> + _onEditorDidMount(e)} />
); }; diff --git a/Demo/Program.cs b/Demo/Program.cs index a66051cc..24441cde 100644 --- a/Demo/Program.cs +++ b/Demo/Program.cs @@ -15,6 +15,7 @@ public static void Main(string[] args) var yDotNet = builder.Services.AddYDotNet() + .AutoCleanup() .AddCallback() .AddWebSockets(); diff --git a/Directory.Build.props b/Directory.Build.props index 74268a27..2d7d2454 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -11,7 +11,7 @@ README.md true snupkg - 0.2.13 + 0.3.0 diff --git a/YDotNet.Server.MongoDB/MongoDocumentStorage.cs b/YDotNet.Server.MongoDB/MongoDocumentStorage.cs index baab25e9..3c7854bc 100644 --- a/YDotNet.Server.MongoDB/MongoDocumentStorage.cs +++ b/YDotNet.Server.MongoDB/MongoDocumentStorage.cs @@ -32,7 +32,7 @@ await collection.Indexes.CreateOneAsync( Builders.IndexKeys.Ascending(x => x.Expiration), new CreateIndexOptions { - ExpireAfter = TimeSpan.Zero + ExpireAfter = TimeSpan.Zero, }), cancellationToken: cancellationToken).ConfigureAwait(false); } diff --git a/YDotNet.Server.WebSockets/WebSocketDecoder.cs b/YDotNet.Server.WebSockets/WebSocketDecoder.cs index d9ba1668..890e4a15 100644 --- a/YDotNet.Server.WebSockets/WebSocketDecoder.cs +++ b/YDotNet.Server.WebSockets/WebSocketDecoder.cs @@ -16,7 +16,7 @@ public WebSocketDecoder(WebSocket webSocket) this.webSocket = webSocket; } - public bool CanRead { get; set; } = true; + public bool CanRead { get; private set; } = true; public bool HasMore => bufferIndex < bufferLength; @@ -61,7 +61,7 @@ private async Task ReadIfEndOfBufferReachedAsync(CancellationToken ct) if (received.CloseStatus != null) { CanRead = false; - throw new InvalidOperationException("Socket is already closed."); + throw new WebSocketException("Socket is already closed."); } bufferLength = received.Count; diff --git a/YDotNet.Server/CleanupOptions.cs b/YDotNet.Server/CleanupOptions.cs new file mode 100644 index 00000000..cf695532 --- /dev/null +++ b/YDotNet.Server/CleanupOptions.cs @@ -0,0 +1,8 @@ +namespace YDotNet.Server; + +public sealed class CleanupOptions +{ + public TimeSpan Interval { get; set; } = TimeSpan.FromMinutes(1); + + public TimeSpan LogWaitTime { get; set; } = TimeSpan.FromMinutes(10); +} diff --git a/YDotNet.Server/DefaultDocumentCleaner.cs b/YDotNet.Server/DefaultDocumentCleaner.cs new file mode 100644 index 00000000..db858b6f --- /dev/null +++ b/YDotNet.Server/DefaultDocumentCleaner.cs @@ -0,0 +1,54 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace YDotNet.Server; + +internal class DefaultDocumentCleaner : BackgroundService +{ + private readonly IDocumentManager documentManager; + private readonly ILogger logger; + private readonly CleanupOptions options; + private DateTime lastLogging; + + public Func Clock { get; } = () => DateTime.UtcNow; + + public DefaultDocumentCleaner(IDocumentManager documentManager, IOptions options, ILogger logger) + { + this.documentManager = documentManager; + this.logger = logger; + this.options = options.Value; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var timer = new PeriodicTimer(options.Interval); + + while (await timer.WaitForNextTickAsync(stoppingToken).ConfigureAwait(false)) + { + try + { + await documentManager.CleanupAsync(stoppingToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // This is an expected exception when the process stops. There is no good reason to handle that. + } + catch (Exception ex) + { + var now = Clock(); + + var timeSinceLastLogging = now - lastLogging; + // We run this loop very often. If there is an exception, it could flood the log with duplicate log entries. + // Therefore use a wait time between two log calls. + if (timeSinceLastLogging < options.LogWaitTime) + { + return; + } + + logger.LogError(ex, "Failed to cleanup document manager."); + lastLogging = now; + } + } + } +} diff --git a/YDotNet.Server/DefaultDocumentManager.cs b/YDotNet.Server/DefaultDocumentManager.cs index d2527473..df694368 100644 --- a/YDotNet.Server/DefaultDocumentManager.cs +++ b/YDotNet.Server/DefaultDocumentManager.cs @@ -76,7 +76,7 @@ public async ValueTask ApplyUpdateAsync(DocumentContext context, b { var result = new UpdateResult { - Diff = stateDiff + Diff = stateDiff, }; using (var transaction = doc.WriteTransactionOrThrow()) @@ -150,6 +150,7 @@ await callback.OnClientDisconnectedAsync(new ClientDisconnectedEvent { Context = context, Source = this, + Reason = DisconnectReason.Disconnect, }).ConfigureAwait(false); } } @@ -163,16 +164,19 @@ await callback.OnClientDisconnectedAsync(new ClientDisconnectedEvent { Context = new DocumentContext(documentName, clientId), Source = this, + Reason = DisconnectReason.Cleanup, }).ConfigureAwait(false); } cache.RemoveEvictedItems(); } - public ValueTask> GetAwarenessAsync( + public async ValueTask> GetAwarenessAsync( DocumentContext context, CancellationToken ct = default) { - return new ValueTask>(users.GetUsers(context.DocumentName)); + await CleanupAsync(default).ConfigureAwait(false); + + return users.GetUsers(context.DocumentName); } } diff --git a/YDotNet.Server/Events.cs b/YDotNet.Server/Events.cs index 1e3159af..13f7a008 100644 --- a/YDotNet.Server/Events.cs +++ b/YDotNet.Server/Events.cs @@ -30,6 +30,7 @@ public sealed class DocumentChangedEvent : DocumentChangeEvent public sealed class ClientDisconnectedEvent : DocumentEvent { + required public DisconnectReason Reason { get; init; } } public sealed class ClientAwarenessEvent : DocumentEvent @@ -38,3 +39,9 @@ public sealed class ClientAwarenessEvent : DocumentEvent required public ulong ClientClock { get; set; } } + +public enum DisconnectReason +{ + Disconnect, + Cleanup, +} diff --git a/YDotNet.Server/ServiceExtensions.cs b/YDotNet.Server/ServiceExtensions.cs index f101a8d8..abc70829 100644 --- a/YDotNet.Server/ServiceExtensions.cs +++ b/YDotNet.Server/ServiceExtensions.cs @@ -18,7 +18,7 @@ public static YDotnetRegistration AddYDotNet(this IServiceCollection services) return new YDotnetRegistration { - Services = services + Services = services, }; } @@ -28,6 +28,13 @@ public static YDotnetRegistration AddCallback(this YDotnetRegistration regist registration.Services.AddSingleton(); return registration; } + + public static YDotnetRegistration AutoCleanup(this YDotnetRegistration registration, Action? configure = null) + { + registration.Services.Configure(configure ?? (x => { })); + registration.Services.AddSingleton(); + return registration; + } } #pragma warning disable MA0048 // File name must match type name diff --git a/YDotNet/Document/Cells/OutputTag.cs b/YDotNet/Document/Cells/OutputTag.cs index b3d18708..df06a9c4 100644 --- a/YDotNet/Document/Cells/OutputTag.cs +++ b/YDotNet/Document/Cells/OutputTag.cs @@ -84,5 +84,5 @@ public enum OutputTag /// /// Represents a cell with an value. /// - Doc = 7 + Doc = 7, } diff --git a/YDotNet/Document/Options/DocEncoding.cs b/YDotNet/Document/Options/DocEncoding.cs index 3087d5cb..59ead9ac 100644 --- a/YDotNet/Document/Options/DocEncoding.cs +++ b/YDotNet/Document/Options/DocEncoding.cs @@ -21,5 +21,5 @@ public enum DocEncoding /// /// Compute editable strings length and offset using UTF-32 (Unicode) code points number. /// - Utf32 = 2 + Utf32 = 2, } diff --git a/YDotNet/Document/Options/DocOptions.cs b/YDotNet/Document/Options/DocOptions.cs index 2a8a04c0..95995473 100644 --- a/YDotNet/Document/Options/DocOptions.cs +++ b/YDotNet/Document/Options/DocOptions.cs @@ -104,7 +104,7 @@ internal DocOptionsNative ToNative() Encoding = (byte)Encoding, SkipGc = (byte)(SkipGarbageCollection ? 1 : 0), AutoLoad = (byte)(AutoLoad ? 1 : 0), - ShouldLoad = (byte)(ShouldLoad ? 1 : 0) + ShouldLoad = (byte)(ShouldLoad ? 1 : 0), }; } } diff --git a/YDotNet/Document/StickyIndexes/StickyAssociationType.cs b/YDotNet/Document/StickyIndexes/StickyAssociationType.cs index 8c3647b0..8ef7dab3 100644 --- a/YDotNet/Document/StickyIndexes/StickyAssociationType.cs +++ b/YDotNet/Document/StickyIndexes/StickyAssociationType.cs @@ -24,5 +24,5 @@ public enum StickyAssociationType : sbyte /// /// The corresponding points to space before the referenced element. /// - Before = -1 + Before = -1, } diff --git a/YDotNet/Document/Transactions/TransactionUpdateResult.cs b/YDotNet/Document/Transactions/TransactionUpdateResult.cs index 7032c4d5..02c515aa 100644 --- a/YDotNet/Document/Transactions/TransactionUpdateResult.cs +++ b/YDotNet/Document/Transactions/TransactionUpdateResult.cs @@ -33,5 +33,5 @@ public enum TransactionUpdateResult /// /// Failure when trying to decode JSON content. /// - Other = 5 + Other = 5, } diff --git a/YDotNet/Document/Types/Events/EventBranchTag.cs b/YDotNet/Document/Types/Events/EventBranchTag.cs index e13443e1..bd398d91 100644 --- a/YDotNet/Document/Types/Events/EventBranchTag.cs +++ b/YDotNet/Document/Types/Events/EventBranchTag.cs @@ -34,5 +34,5 @@ public enum EventBranchTag : sbyte /// /// This event holds an instance. /// - XmlText = 5 + XmlText = 5, } diff --git a/YDotNet/Document/Types/Events/EventChangeTag.cs b/YDotNet/Document/Types/Events/EventChangeTag.cs index 6a66107e..5abd9085 100644 --- a/YDotNet/Document/Types/Events/EventChangeTag.cs +++ b/YDotNet/Document/Types/Events/EventChangeTag.cs @@ -21,5 +21,5 @@ public enum EventChangeTag /// /// Represents the update of content. /// - Retain + Retain, } diff --git a/YDotNet/Document/Types/Events/EventDeltaTag.cs b/YDotNet/Document/Types/Events/EventDeltaTag.cs index 5010c76e..312a663a 100644 --- a/YDotNet/Document/Types/Events/EventDeltaTag.cs +++ b/YDotNet/Document/Types/Events/EventDeltaTag.cs @@ -21,5 +21,5 @@ public enum EventDeltaTag /// /// Represents the update of content. /// - Retain + Retain, } diff --git a/YDotNet/Document/Types/Events/EventKeyChangeTag.cs b/YDotNet/Document/Types/Events/EventKeyChangeTag.cs index fc267c39..22829cf4 100644 --- a/YDotNet/Document/Types/Events/EventKeyChangeTag.cs +++ b/YDotNet/Document/Types/Events/EventKeyChangeTag.cs @@ -18,5 +18,5 @@ public enum EventKeyChangeTag /// /// Represents that the value under this key was updated. /// - Update + Update, } diff --git a/YDotNet/Document/Types/Events/EventPathSegmentTag.cs b/YDotNet/Document/Types/Events/EventPathSegmentTag.cs index 439710d9..5edd4c88 100644 --- a/YDotNet/Document/Types/Events/EventPathSegmentTag.cs +++ b/YDotNet/Document/Types/Events/EventPathSegmentTag.cs @@ -13,5 +13,5 @@ public enum EventPathSegmentTag : sbyte /// /// The contains an value. /// - Index = 2 + Index = 2, } diff --git a/YDotNet/Document/UndoManagers/Events/UndoEventKind.cs b/YDotNet/Document/UndoManagers/Events/UndoEventKind.cs index a8bf64bd..a6521aac 100644 --- a/YDotNet/Document/UndoManagers/Events/UndoEventKind.cs +++ b/YDotNet/Document/UndoManagers/Events/UndoEventKind.cs @@ -13,5 +13,5 @@ public enum UndoEventKind /// /// Represents a redo operation. /// - Redo = 1 + Redo = 1, } diff --git a/YDotNet/Document/UndoManagers/UndoManagerOptions.cs b/YDotNet/Document/UndoManagers/UndoManagerOptions.cs index 91517466..b91e6a6d 100644 --- a/YDotNet/Document/UndoManagers/UndoManagerOptions.cs +++ b/YDotNet/Document/UndoManagers/UndoManagerOptions.cs @@ -19,7 +19,7 @@ internal UndoManagerOptionsNative ToNative() { return new UndoManagerOptionsNative { - CaptureTimeoutMilliseconds = CaptureTimeoutMilliseconds + CaptureTimeoutMilliseconds = CaptureTimeoutMilliseconds, }; } } diff --git a/YDotNet/Native/Types/Branches/BranchKind.cs b/YDotNet/Native/Types/Branches/BranchKind.cs index b5ea401f..73ec7fb3 100644 --- a/YDotNet/Native/Types/Branches/BranchKind.cs +++ b/YDotNet/Native/Types/Branches/BranchKind.cs @@ -7,5 +7,5 @@ internal enum BranchKind Map = 2, Text = 3, XmlElement = 4, - XmlText = 5 + XmlText = 5, } diff --git a/YDotNet/Native/Types/Events/EventChangeTagNative.cs b/YDotNet/Native/Types/Events/EventChangeTagNative.cs index e902a6cd..09a00f81 100644 --- a/YDotNet/Native/Types/Events/EventChangeTagNative.cs +++ b/YDotNet/Native/Types/Events/EventChangeTagNative.cs @@ -4,5 +4,5 @@ internal enum EventChangeTagNative : sbyte { Add = 1, Remove = 2, - Retain = 3 + Retain = 3, } diff --git a/YDotNet/Native/Types/Events/EventDeltaTagNative.cs b/YDotNet/Native/Types/Events/EventDeltaTagNative.cs index f1b77889..a6a3a7bd 100644 --- a/YDotNet/Native/Types/Events/EventDeltaTagNative.cs +++ b/YDotNet/Native/Types/Events/EventDeltaTagNative.cs @@ -4,5 +4,5 @@ internal enum EventDeltaTagNative : sbyte { Add = 1, Remove = 2, - Retain = 3 + Retain = 3, } diff --git a/YDotNet/Native/Types/Events/EventKeyChangeTagNative.cs b/YDotNet/Native/Types/Events/EventKeyChangeTagNative.cs index cfe8fc56..c781c918 100644 --- a/YDotNet/Native/Types/Events/EventKeyChangeTagNative.cs +++ b/YDotNet/Native/Types/Events/EventKeyChangeTagNative.cs @@ -4,5 +4,5 @@ internal enum EventKeyChangeTagNative : byte { Add = 4, Remove = 5, - Update = 6 + Update = 6, } diff --git a/YDotNet/Native/UndoManager/Events/UndoEventKindNative.cs b/YDotNet/Native/UndoManager/Events/UndoEventKindNative.cs index b96a33f8..c049c07d 100644 --- a/YDotNet/Native/UndoManager/Events/UndoEventKindNative.cs +++ b/YDotNet/Native/UndoManager/Events/UndoEventKindNative.cs @@ -3,5 +3,5 @@ namespace YDotNet.Native.UndoManager.Events; internal enum UndoEventKindNative : byte { Undo = 0, - Redo = 1 + Redo = 1, }