internal static void CleanupTransactions(IList <Transaction> transactionCleanups, int i) { if (i < transactionCleanups.Count) { var transaction = transactionCleanups[i]; var doc = transaction.Doc; var store = doc.Store; var ds = transaction.DeleteSet; var mergeStructs = transaction._mergeStructs; var actions = new List <Action>(); try { ds.SortAndMergeDeleteSet(); transaction.AfterState = store.GetStateVector(); doc._transaction = null; actions.Add(() => { doc.InvokeOnBeforeObserverCalls(transaction); }); actions.Add(() => { foreach (var kvp in transaction.Changed) { var itemType = kvp.Key; var subs = kvp.Value; if (itemType._item == null || !itemType._item.Deleted) { itemType.CallObserver(transaction, subs); } } }); actions.Add(() => { // Deep observe events. foreach (var kvp in transaction.ChangedParentTypes) { var type = kvp.Key; var events = kvp.Value; // We need to think about the possibility that the user transforms the YDoc in the event. if (type._item == null || !type._item.Deleted) { foreach (var evt in events) { if (evt.Target._item == null || !evt.Target._item.Deleted) { evt.CurrentTarget = type; } } // Sort events by path length so that top-level events are fired first. var sortedEvents = events.ToList(); sortedEvents.Sort((a, b) => a.Path.Count - b.Path.Count); Debug.Assert(sortedEvents.Count > 0); actions.Add(() => { type.CallDeepEventHandlerListeners(sortedEvents, transaction); }); } } }); actions.Add(() => { doc.InvokeOnAfterTransaction(transaction); }); CallAll(actions); } finally { // Replace deleted items with ItemDeleted / GC. // This is where content is actually removed from the Yjs Doc. if (doc.Gc) { ds.TryGcDeleteSet(store, doc.GcFilter); } ds.TryMergeDeleteSet(store); // On all affected store.clients props, try to merge. foreach (var kvp in transaction.AfterState) { var client = kvp.Key; var clock = kvp.Value; if (!transaction.BeforeState.TryGetValue(client, out int beforeClock)) { beforeClock = 0; } if (beforeClock != clock) { var structs = store.Clients[client]; var firstChangePos = Math.Max(StructStore.FindIndexSS(structs, beforeClock), 1); for (int j = structs.Count - 1; j >= firstChangePos; j--) { DeleteSet.TryToMergeWithLeft(structs, j); } } } // Try to merge mergeStructs. // TODO: It makes more sense to transform mergeStructs to a DS, sort it, and merge from right to left // but at the moment DS does not handle duplicates. for (int j = 0; j < mergeStructs.Count; j++) { var client = mergeStructs[j].Id.Client; var clock = mergeStructs[j].Id.Clock; var structs = store.Clients[client]; var replacedStructPos = StructStore.FindIndexSS(structs, clock); if (replacedStructPos + 1 < structs.Count) { DeleteSet.TryToMergeWithLeft(structs, replacedStructPos + 1); } if (replacedStructPos > 0) { DeleteSet.TryToMergeWithLeft(structs, replacedStructPos); } } if (!transaction.Local) { if (!transaction.AfterState.TryGetValue(doc.ClientId, out int afterClock)) { afterClock = -1; } if (!transaction.BeforeState.TryGetValue(doc.ClientId, out int beforeClock)) { beforeClock = -1; } if (afterClock != beforeClock) { doc.ClientId = YDoc.GenerateNewClientId(); // Debug.WriteLine($"{nameof(Transaction)}: Changed the client-id because another client seems to be using it."); } } // @todo: Merge all the transactions into one and provide send the data as a single update message. doc.InvokeOnAfterTransactionCleanup(transaction); doc.InvokeUpdateV2(transaction); foreach (var subDoc in transaction.SubdocsAdded) { doc.Subdocs.Add(subDoc); } foreach (var subDoc in transaction.SubdocsRemoved) { doc.Subdocs.Remove(subDoc); } doc.InvokeSubdocsChanged(transaction.SubdocsLoaded, transaction.SubdocsAdded, transaction.SubdocsRemoved); foreach (var subDoc in transaction.SubdocsRemoved) { subDoc.Destroy(); } if (transactionCleanups.Count <= i + 1) { doc._transactionCleanups.Clear(); doc.InvokeAfterAllTransactions(transactionCleanups); } else { CleanupTransactions(transactionCleanups, i + 1); } } } }