Skip to content

Commit

Permalink
Handle item cycles when applying live reload update properly
Browse files Browse the repository at this point in the history
  • Loading branch information
JiriLojda committed Jun 19, 2024
1 parent c0bb86a commit 9009fe8
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 6 deletions.
42 changes: 36 additions & 6 deletions src/utils/liveReload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ export const applyUpdateOnItemAndLoadLinkedItems = <Elements extends IContentIte
const applyUpdateOnItemOptionallyAsync = <Elements extends IContentItemElements>(
item: IContentItem<Elements>,
update: InternalUpdateMessage,
resolveElementCodename: (codename: string) => string
resolveElementCodename: (codename: string) => string,
updatedItem: IContentItem<Elements> | null = null,
processedItemsPath: ReadonlyArray<string> = []
): OptionallyAsync<IContentItem<Elements>> => {
const shouldApplyOnThisItem =
item.system.codename === update.item.codename && item.system.language === update.variant.codename;
Expand All @@ -56,6 +58,8 @@ const applyUpdateOnItemOptionallyAsync = <Elements extends IContentItemElements>
},
}));

const newUpdatedItem = !updatedItem && shouldApplyOnThisItem ? { ...item } : updatedItem; // We will mutate its elements to new values before returning. This is necesary to preserve cyclic dependencies between items without infinite recursion.

const updatedElements = mergeOptionalAsyncs(
Object.entries(item.elements).map(([elementCodename, element]) => {
const matchingUpdate = elementUpdates.find((u) => u.element.codename === elementCodename);
Expand All @@ -72,7 +76,22 @@ const applyUpdateOnItemOptionallyAsync = <Elements extends IContentItemElements>

return applyOnOptionallyAsync(
mergeOptionalAsyncs(
typedItemElement.linkedItems.map((i) => applyUpdateOnItemOptionallyAsync(i, update, resolveElementCodename))
typedItemElement.linkedItems.map((i) => {
if (updatedItem?.system.codename === i.system.codename) {
// we closed the cycle and on the updated item and need to connect to the new item
return createOptionallyAsync(() => updatedItem);
}
return closesCycleWithoutUpdate(
processedItemsPath,
i.system.codename,
updatedItem?.system.codename ?? null
)
? createOptionallyAsync(() => i) // we found a cycle that doesn't need any update so we just ignore it
: applyUpdateOnItemOptionallyAsync(i, update, resolveElementCodename, newUpdatedItem, [
...processedItemsPath,
i.system.codename,
]);
})
),
(linkedItems) => {
return linkedItems.some((newItem, index) => newItem !== typedItemElement.linkedItems[index])
Expand All @@ -86,11 +105,22 @@ const applyUpdateOnItemOptionallyAsync = <Elements extends IContentItemElements>
})
);

return applyOnOptionallyAsync(updatedElements, (newElements) =>
newElements.some(([codename, newEl]) => item.elements[codename] !== newEl)
return applyOnOptionallyAsync(updatedElements, (newElements) => {
if (newUpdatedItem?.system.codename === item.system.codename) {
newUpdatedItem.elements = Object.fromEntries(newElements) as Elements;
return newUpdatedItem;
}

return newElements.some(([codename, newEl]) => item.elements[codename] !== newEl)
? { ...item, elements: Object.fromEntries(newElements) as Elements }
: item
);
: item;
});
};

const closesCycleWithoutUpdate = (path: ReadonlyArray<string>, nextItem: string, updatedItem: string | null) => {
const cycleStartIndex = path.indexOf(nextItem);

return cycleStartIndex !== -1 && (!updatedItem || cycleStartIndex > path.indexOf(updatedItem));
};

const applyUpdateOnElement = (
Expand Down
104 changes: 104 additions & 0 deletions test-browser/utils/liveReload.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,110 @@ const system: IContentItem['system'] = {
item.elements.component.linkedItems[0].elements.inner
);
});

it('Does not cycle infinitely on circular references where the cycle contains the updated item', async () => {
type ElementsType = {
el: Elements.LinkedItemsElement;
el2?: Elements.TextElement;
};

const item: IContentItem<ElementsType> = {
system,
elements: {
el: {
type: ElementType.ModularContent,
name: 'linked items element',
value: ['item2'],
linkedItems: [],
} as Elements.LinkedItemsElement,
el2: {
type: ElementType.Text,
name: 'text element',
value: 'original value',
},
},
};

const item2: IContentItem<ElementsType> = {
system: { ...system, id: 'd5b7e5c2-5c4d-4e3f-8d2b-7f5f2e3e6f5b', codename: 'item2' },
elements: {
el: {
type: ElementType.ModularContent,
name: 'linked items element',
value: [system.codename],
linkedItems: [item],
} as Elements.LinkedItemsElement,
},
};

item.elements.el.linkedItems = [item2];

const update: IUpdateMessageData = {
item: { id: item.system.id, codename: item.system.codename },
variant: { id: '87767c98-3d1d-490f-bd19-e0157157d087', codename: item.system.language },
projectId: '5f53475c-de51-4cef-b373-463d56919cec',
elements: [
{
type: ElementType.Text,
element: { id: '467dc8c1-4fcb-4adc-a5fc-049d17ee1386', codename: 'el2' },
data: { value: 'new value' },
},
],
};

const result = await callTestFnc(item, update);

expect(result.elements.el.linkedItems[0].system.codename).toBe(item2.system.codename);
expect(result.elements.el2).toEqual({ ...item.elements.el2, value: 'new value' } as Elements.TextElement);
});
it('Does not cycle infinitely on circular references where the cycle does not contain the updated item', async () => {
type ElementsType = {
el: Elements.LinkedItemsElement;
};

const item: IContentItem<ElementsType> = {
system,
elements: {
el: {
type: ElementType.ModularContent,
name: 'linked items element',
value: ['item2'],
linkedItems: [],
} as Elements.LinkedItemsElement,
},
};

const item2: IContentItem<ElementsType> = {
system: { ...system, id: 'd5b7e5c2-5c4d-4e3f-8d2b-7f5f2e3e6f5b', codename: 'item2' },
elements: {
el: {
type: ElementType.ModularContent,
name: 'linked items element',
value: [system.codename],
linkedItems: [item],
} as Elements.LinkedItemsElement,
},
};

item.elements.el.linkedItems = [item2];

const update: IUpdateMessageData = {
item: { id: 'f5b69805-f059-4038-883a-ad2d31bc92f5', codename: 'non-existing-item' },
variant: { id: '87767c98-3d1d-490f-bd19-e0157157d087', codename: 'some-language' },
projectId: '5f53475c-de51-4cef-b373-463d56919cec',
elements: [
{
type: ElementType.Text,
element: { id: '467dc8c1-4fcb-4adc-a5fc-049d17ee1386', codename: 'el' },
data: { value: 'new value' },
},
],
};

const result = await callTestFnc(item, update);

expect(result).toBe(item);
});
});
});

Expand Down

0 comments on commit 9009fe8

Please sign in to comment.