From 8c9bb9a3b0b381c78427cc40ea45f5b62c88f7ba Mon Sep 17 00:00:00 2001 From: Chris Villa Date: Thu, 2 Nov 2023 22:55:44 +0000 Subject: [PATCH] chore: resolve all data asynchronously --- .../lib/__tests__/use-resolved-data.spec.tsx | 106 +++++++++--------- packages/core/lib/resolve-all-data.ts | 6 +- packages/core/lib/resolve-all-props.ts | 73 ------------ packages/core/lib/resolve-component-data.ts | 86 ++++++++++++++ packages/core/lib/use-resolved-data.ts | 78 ++++++++----- 5 files changed, 193 insertions(+), 156 deletions(-) delete mode 100644 packages/core/lib/resolve-all-props.ts create mode 100644 packages/core/lib/resolve-component-data.ts diff --git a/packages/core/lib/__tests__/use-resolved-data.spec.tsx b/packages/core/lib/__tests__/use-resolved-data.spec.tsx index 14f3da1418..86d62786fc 100644 --- a/packages/core/lib/__tests__/use-resolved-data.spec.tsx +++ b/packages/core/lib/__tests__/use-resolved-data.spec.tsx @@ -2,7 +2,7 @@ import { act, cleanup, renderHook, waitFor } from "@testing-library/react"; import { Config, Data } from "../../types/Config"; import { useResolvedData } from "../use-resolved-data"; import { SetDataAction } from "../../reducer"; -import { cache } from "../resolve-all-props"; +import { cache } from "../resolve-component-data"; const item1 = { type: "MyComponent", props: { id: "MyComponent-1" } }; const item2 = { type: "MyComponent", props: { id: "MyComponent-2" } }; @@ -55,27 +55,55 @@ describe("use-resolved-data", () => { }); it("should call the `setData` action with resolved data", async () => { - let dispatchedEvent: SetDataAction = {} as any; + let dispatchedEvents: SetDataAction[] = []; + let currentData = data; + + const renderedHook = renderHook(() => { + return useResolvedData(currentData, config, (args) => { + const action = args as SetDataAction; + const newData = action.data as any; + + dispatchedEvents.push(action); + + currentData = { ...currentData, ...newData(currentData) }; + }); + }); - const renderedHook = renderHook(() => - useResolvedData(data, config, (args) => { - dispatchedEvent = args as any; - }) - ); - const { resolveData } = renderedHook.result.current; await act(async () => { - resolveData(); + // resolveData gets called on render + renderedHook.rerender(); }); - expect(dispatchedEvent?.type).toBe("setData"); + expect(dispatchedEvents.length).toBe(4); // calls dispatcher for each resolver - if (typeof dispatchedEvent?.data === "function") { - expect(dispatchedEvent?.data(data)).toMatchInlineSnapshot(` - { - "content": [ + const fn = dispatchedEvents[dispatchedEvents.length - 1].data as any; + expect(currentData).toMatchInlineSnapshot(` + { + "content": [ + { + "props": { + "id": "MyComponent-1", + "prop": "Hello, world", + }, + "readOnly": { + "prop": true, + }, + "type": "MyComponent", + }, + ], + "root": { + "props": { + "title": "Resolved title", + }, + "readOnly": { + "title": true, + }, + }, + "zones": { + "MyComponent-1:zone": [ { "props": { - "id": "MyComponent-1", + "id": "MyComponent-2", "prop": "Hello, world", }, "readOnly": { @@ -84,43 +112,21 @@ describe("use-resolved-data", () => { "type": "MyComponent", }, ], - "root": { - "props": { - "title": "Resolved title", - }, - "readOnly": { - "title": true, - }, - }, - "zones": { - "MyComponent-1:zone": [ - { - "props": { - "id": "MyComponent-2", - "prop": "Hello, world", - }, - "readOnly": { - "prop": true, - }, - "type": "MyComponent", + "MyComponent-2:zone": [ + { + "props": { + "id": "MyComponent-3", + "prop": "Hello, world", }, - ], - "MyComponent-2:zone": [ - { - "props": { - "id": "MyComponent-3", - "prop": "Hello, world", - }, - "readOnly": { - "prop": true, - }, - "type": "MyComponent", + "readOnly": { + "prop": true, }, - ], - }, - } - `); - } + "type": "MyComponent", + }, + ], + }, + } + `); }); it("should NOT call the `setData` action with resolved data, when the data is unchanged", async () => { diff --git a/packages/core/lib/resolve-all-data.ts b/packages/core/lib/resolve-all-data.ts index 80dbb6b6a7..5ee83c6d14 100644 --- a/packages/core/lib/resolve-all-data.ts +++ b/packages/core/lib/resolve-all-data.ts @@ -1,5 +1,5 @@ import { Config, Data, MappedItem } from "../types/Config"; -import { resolveAllProps } from "./resolve-all-props"; +import { resolveAllComponentData } from "./resolve-component-data"; import { resolveRootData } from "./resolve-root-data"; export const resolveAllData = async ( @@ -17,7 +17,7 @@ export const resolveAllData = async ( for (let i = 0; i < zoneKeys.length; i++) { const zoneKey = zoneKeys[i]; - resolvedZones[zoneKey] = await resolveAllProps( + resolvedZones[zoneKey] = await resolveAllComponentData( zones[zoneKey], config, onResolveStart, @@ -28,7 +28,7 @@ export const resolveAllData = async ( return { ...data, root: dynamicRoot, - content: await resolveAllProps( + content: await resolveAllComponentData( data.content, config, onResolveStart, diff --git a/packages/core/lib/resolve-all-props.ts b/packages/core/lib/resolve-all-props.ts deleted file mode 100644 index 3912bf14ea..0000000000 --- a/packages/core/lib/resolve-all-props.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { Config, MappedItem } from "../types/Config"; - -export const cache = { lastChange: {} }; - -export const resolveAllProps = async ( - content: MappedItem[], - config: Config, - onResolveStart?: (item: MappedItem) => void, - onResolveEnd?: (item: MappedItem) => void -) => { - return await Promise.all( - content.map(async (item) => { - const configForItem = config.components[item.type]; - - if (configForItem.resolveData) { - let changed = Object.keys(item.props).reduce( - (acc, item) => ({ ...acc, [item]: true }), - {} - ); - - if (cache.lastChange[item.props.id]) { - const { item: oldItem, resolved } = cache.lastChange[item.props.id]; - - if (oldItem === item) { - return resolved; - } - - Object.keys(item.props).forEach((propName) => { - if (oldItem.props[propName] === item.props[propName]) { - changed[propName] = false; - } - }); - } - - if (onResolveStart) { - onResolveStart(item); - } - - const { props: resolvedProps, readOnly = {} } = - await configForItem.resolveData(item, { changed }); - - const { readOnly: existingReadOnly = {} } = item || {}; - - const newReadOnly = { ...existingReadOnly, ...readOnly }; - - const resolvedItem = { - ...item, - props: { - ...item.props, - ...resolvedProps, - }, - }; - - if (Object.keys(newReadOnly).length) { - resolvedItem.readOnly = newReadOnly; - } - - cache.lastChange[item.props.id] = { - item, - resolved: resolvedItem, - }; - - if (onResolveEnd) { - onResolveEnd(resolvedItem); - } - - return resolvedItem; - } - - return item; - }) - ); -}; diff --git a/packages/core/lib/resolve-component-data.ts b/packages/core/lib/resolve-component-data.ts new file mode 100644 index 0000000000..566d247f4e --- /dev/null +++ b/packages/core/lib/resolve-component-data.ts @@ -0,0 +1,86 @@ +import { ComponentData, Config, MappedItem } from "../types/Config"; + +export const cache = { lastChange: {} }; + +export const resolveAllComponentData = async ( + content: MappedItem[], + config: Config, + onResolveStart?: (item: MappedItem) => void, + onResolveEnd?: (item: MappedItem) => void +) => { + return await Promise.all( + content.map(async (item) => { + return await resolveComponentData( + item, + config, + onResolveStart, + onResolveEnd + ); + }) + ); +}; + +export const resolveComponentData = async ( + item: ComponentData, + config: Config, + onResolveStart?: (item: MappedItem) => void, + onResolveEnd?: (item: MappedItem) => void +) => { + const configForItem = config.components[item.type]; + if (configForItem.resolveData) { + let changed = Object.keys(item.props).reduce( + (acc, item) => ({ ...acc, [item]: true }), + {} + ); + + if (cache.lastChange[item.props.id]) { + const { item: oldItem, resolved } = cache.lastChange[item.props.id]; + + if (oldItem === item) { + return resolved; + } + + Object.keys(item.props).forEach((propName) => { + if (oldItem.props[propName] === item.props[propName]) { + changed[propName] = false; + } + }); + } + + if (onResolveStart) { + onResolveStart(item); + } + + const { props: resolvedProps, readOnly = {} } = + await configForItem.resolveData(item, { changed }); + + const { readOnly: existingReadOnly = {} } = item || {}; + + const newReadOnly = { ...existingReadOnly, ...readOnly }; + + const resolvedItem = { + ...item, + props: { + ...item.props, + ...resolvedProps, + }, + }; + + if (Object.keys(newReadOnly).length) { + resolvedItem.readOnly = newReadOnly; + } + + cache.lastChange[item.props.id] = { + item, + resolved: resolvedItem, + }; + + if (onResolveEnd) { + onResolveEnd(resolvedItem); + } + + return resolvedItem; + } + + return item; +}; diff --git a/packages/core/lib/use-resolved-data.ts b/packages/core/lib/use-resolved-data.ts index b2b00d61c7..804069f149 100644 --- a/packages/core/lib/use-resolved-data.ts +++ b/packages/core/lib/use-resolved-data.ts @@ -1,7 +1,7 @@ -import { Config, Data } from "../types/Config"; +import { ComponentData, Config, Data, RootData } from "../types/Config"; import { Dispatch, useCallback, useEffect, useState } from "react"; import { PuckAction } from "../reducer"; -import { resolveAllProps } from "./resolve-all-props"; +import { resolveComponentData } from "./resolve-component-data"; import { applyDynamicProps } from "./apply-dynamic-props"; import { resolveRootData } from "./resolve-root-data"; @@ -41,39 +41,18 @@ export const useResolvedData = ( [] ); - const runResolvers = () => { + const runResolvers = async () => { // Flatten zones const flatContent = Object.keys(newData.zones || {}) .reduce((acc, zone) => [...acc, ...newData.zones![zone]], newData.content) .filter((item) => !!config.components[item.type].resolveData); - resolveAllProps( - flatContent, - config, - (item) => { - setComponentLoading(item.props.id, true, 50); - }, - (item) => { - deferredSetStates[item.props.id]; - - setComponentLoading(item.props.id, false); - } - ).then(async (dynamicContent) => { - setComponentLoading("puck-root", true, 50); - - const dynamicRoot = await resolveRootData(newData, config); - - setComponentLoading("puck-root", false); - - const newDynamicProps = dynamicContent.reduce>( - (acc, item) => { - return { ...acc, [item.props.id]: item }; - }, - {} - ); - + const applyIfChange = ( + dynamicDataMap: Record, + dynamicRoot?: RootData + ) => { // Apply the dynamic content to `data`, not `newData`, in case `data` has been changed by the user - const processed = applyDynamicProps(data, newDynamicProps, dynamicRoot); + const processed = applyDynamicProps(data, dynamicDataMap, dynamicRoot); const containsChanges = JSON.stringify(data) !== JSON.stringify(processed); @@ -81,11 +60,50 @@ export const useResolvedData = ( if (containsChanges) { dispatch({ type: "setData", - data: (prev) => applyDynamicProps(prev, newDynamicProps, dynamicRoot), + data: (prev) => applyDynamicProps(prev, dynamicDataMap, dynamicRoot), recordHistory: resolverKey > 0, }); } + }; + + const promises: Promise[] = []; + + promises.push( + (async () => { + setComponentLoading("puck-root", true, 50); + + const dynamicRoot = await resolveRootData(newData, config); + + applyIfChange({}, dynamicRoot); + + setComponentLoading("puck-root", false); + })() + ); + + flatContent.forEach((item) => { + promises.push( + (async () => { + const dynamicData: ComponentData = await resolveComponentData( + item, + config, + (item) => { + setComponentLoading(item.props.id, true, 50); + }, + (item) => { + deferredSetStates[item.props.id]; + + setComponentLoading(item.props.id, false); + } + ); + + const dynamicDataMap = { [item.props.id]: dynamicData }; + + applyIfChange(dynamicDataMap); + })() + ); }); + + await Promise.all(promises); }; useEffect(() => {