diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs index 7e8a4b18e1..dd656b8019 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs @@ -664,6 +664,89 @@ namespace Umbraco.Tests.PublishedContent AssertDocuments(documents, "N1-fr-FR", "N4-fr-FR", null /*11*/, "N12-fr-FR", null /*5*/, "N6-fr-FR"); } + [Test] + public void RemoveTest() + { + Init(GetInvariantKits()); + + var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); + _snapshotAccessor.PublishedSnapshot = snapshot; + + var documents = snapshot.Content.GetAtRoot().ToArray(); + AssertDocuments(documents, "N1", "N2", "N3"); + + documents = snapshot.Content.GetById(1).Children().ToArray(); + AssertDocuments(documents, "N4", "N5", "N6"); + + documents = snapshot.Content.GetById(2).Children().ToArray(); + AssertDocuments(documents, "N9", "N8", "N7"); + + // notify + _snapshotService.Notify(new[] + { + new ContentCacheRefresher.JsonPayload(3, TreeChangeTypes.Remove), // remove last + new ContentCacheRefresher.JsonPayload(5, TreeChangeTypes.Remove), // remove middle + new ContentCacheRefresher.JsonPayload(9, TreeChangeTypes.Remove), // remove first + }, out _, out _); + + documents = snapshot.Content.GetAtRoot().ToArray(); + AssertDocuments(documents, "N1", "N2"); + + documents = snapshot.Content.GetById(1).Children().ToArray(); + AssertDocuments(documents, "N4", "N6"); + + documents = snapshot.Content.GetById(2).Children().ToArray(); + AssertDocuments(documents, "N8", "N7"); + + // notify + _snapshotService.Notify(new[] + { + new ContentCacheRefresher.JsonPayload(1, TreeChangeTypes.Remove), // remove first + new ContentCacheRefresher.JsonPayload(8, TreeChangeTypes.Remove), // remove + new ContentCacheRefresher.JsonPayload(7, TreeChangeTypes.Remove), // remove + }, out _, out _); + + documents = snapshot.Content.GetAtRoot().ToArray(); + AssertDocuments(documents, "N2"); + + documents = snapshot.Content.GetById(2).Children().ToArray(); + AssertDocuments(documents); + } + + [Test] + public void UpdateTest() + { + Init(GetInvariantKits()); + + var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); + _snapshotAccessor.PublishedSnapshot = snapshot; + + var documents = snapshot.Content.GetAtRoot().ToArray(); + AssertDocuments(documents, "N1", "N2", "N3"); + + documents = snapshot.Content.GetById(1).Children().ToArray(); + AssertDocuments(documents, "N4", "N5", "N6"); + + documents = snapshot.Content.GetById(2).Children().ToArray(); + AssertDocuments(documents, "N9", "N8", "N7"); + + // notify + _snapshotService.Notify(new[] + { + new ContentCacheRefresher.JsonPayload(1, TreeChangeTypes.RefreshBranch), + new ContentCacheRefresher.JsonPayload(2, TreeChangeTypes.RefreshNode), + }, out _, out _); + + documents = snapshot.Content.GetAtRoot().ToArray(); + AssertDocuments(documents, "N1", "N2", "N3"); + + documents = snapshot.Content.GetById(1).Children().ToArray(); + AssertDocuments(documents, "N4", "N5", "N6"); + + documents = snapshot.Content.GetById(2).Children().ToArray(); + AssertDocuments(documents, "N9", "N8", "N7"); + } + private void AssertDocuments(IPublishedContent[] documents, params string[] names) { Assert.AreEqual(names.Length, documents.Length); diff --git a/src/Umbraco.Web/Models/PublishedContentBase.cs b/src/Umbraco.Web/Models/PublishedContentBase.cs index cfc4fd1106..008bf10504 100644 --- a/src/Umbraco.Web/Models/PublishedContentBase.cs +++ b/src/Umbraco.Web/Models/PublishedContentBase.cs @@ -11,7 +11,7 @@ namespace Umbraco.Web.Models /// /// This base class does which (a) consistently resolves and caches the Url, (b) provides an implementation /// for this[alias], and (c) provides basic content set management. - [DebuggerDisplay("Content Id: {Id}}")] + [DebuggerDisplay("{Content Id: {Id}}")] public abstract class PublishedContentBase : IPublishedContent { #region ContentType diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index f1f8f137fb..e59d332525 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -511,6 +511,11 @@ namespace Umbraco.Web.PublishedCache.NuCache RemoveNodeLocked(existing); AddNodeLocked(kit.Node); } + else + { + // replacing existing, handle siblings + kit.Node.NextSiblingContentId = existing.NextSiblingContentId; + } _xmap[kit.Node.Uid] = kit.Node.Id; } @@ -604,7 +609,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // try to find the content // if it is not there, nothing to do - _contentNodes.TryGetValue(id, out LinkedNode link); // else null + _contentNodes.TryGetValue(id, out var link); // else null if (link?.Value == null) return false; var content = link.Value; @@ -674,6 +679,11 @@ namespace Umbraco.Web.PublishedCache.NuCache var parent = parentLink.Value; + // must have children + if (parent.FirstChildContentId < 0) + throw new Exception("panic: no children"); + + // if first, clone parent + remove first child if (parent.FirstChildContentId == content.Id) { parent = GenCloneLocked(parentLink); @@ -681,11 +691,13 @@ namespace Umbraco.Web.PublishedCache.NuCache } else { + // iterate children until the previous child var link = GetLinkedNode(parent.FirstChildContentId, "first child"); while (link.Value.NextSiblingContentId != content.Id) - link = GetLinkedNode(parent.NextSiblingContentId, "next child"); + link = GetLinkedNode(link.Value.NextSiblingContentId, "next child"); + // clone the previous child and replace next child var prevChild = GenCloneLocked(link); prevChild.NextSiblingContentId = content.NextSiblingContentId; } @@ -732,6 +744,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var parent = parentLink.Value; + // if parent has no children, clone parent + add as first child if (parent.FirstChildContentId < 0) { parent = GenCloneLocked(parentLink); @@ -739,39 +752,45 @@ namespace Umbraco.Web.PublishedCache.NuCache return; } - var prevChildLink = GetLinkedNode(parent.FirstChildContentId, "first child"); - var prevChild = prevChildLink.Value; + // get parent's first child + var childLink = GetLinkedNode(parent.FirstChildContentId, "first child"); + var child = childLink.Value; - if (prevChild.SortOrder > content.SortOrder) + // if first, clone parent + insert as first child + if (child.SortOrder > content.SortOrder) { content.NextSiblingContentId = parent.FirstChildContentId; - parent = GenCloneLocked(parentLink); parent.FirstChildContentId = content.Id; return; } - while (prevChild.NextSiblingContentId > 0) + // else lookup position + while (child.NextSiblingContentId > 0) { - var link = GetLinkedNode(prevChild.NextSiblingContentId, "next child"); - var nextChild = link.Value; + // get next child + var nextChildLink = GetLinkedNode(child.NextSiblingContentId, "next child"); + var nextChild = nextChildLink.Value; + // if here, clone previous + append/insert if (nextChild.SortOrder > content.SortOrder) { content.NextSiblingContentId = nextChild.Id; - prevChild = GenCloneLocked(prevChildLink); - prevChild.NextSiblingContentId = content.Id; + child = GenCloneLocked(childLink); + child.NextSiblingContentId = content.Id; return; } - prevChild = nextChild; - prevChildLink = link; + childLink = nextChildLink; + child = nextChild; } - prevChild = GenCloneLocked(prevChildLink); - prevChild.NextSiblingContentId = content.Id; + // if last, clone previous + append + child = GenCloneLocked(childLink); + child.NextSiblingContentId = content.Id; } + // replaces the root node private void SetRootLocked(ContentNode node) { if (_root.Gen != _liveGen) @@ -784,6 +803,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } } + // set a node (just the node, not the tree) private void SetValueLocked(ConcurrentDictionary> dict, TKey key, TValue value) where TValue : class {