From 187fc0bc8fa241b976296091140d0fd0b0960295 Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Thu, 31 Oct 2024 16:17:14 -0400 Subject: [PATCH 01/13] recs server-side working --- .../commerce-engine/commerce-engine.ssr.ts | 51 +++++++++++++---- .../src/app/commerce-ssr-engine/common.ts | 35 ++++++++++++ .../app/search-engine/search-engine.ssr.ts | 57 ++++++++++++++++--- .../headless/src/app/ssr-engine/common.ts | 6 +- .../src/app/ssr-engine/types/common.ts | 2 +- .../ssr-engine/types/hydrate-static-state.ts | 2 +- .../headless-recommendations.ssr.ts | 3 +- .../app/_components/pages/listing-page.tsx | 5 +- 8 files changed, 132 insertions(+), 29 deletions(-) diff --git a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts index dc32ca921ee..b3ff825c307 100644 --- a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts +++ b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts @@ -7,7 +7,10 @@ import {buildProductListing} from '../../controllers/commerce/product-listing/he import {buildSearch} from '../../controllers/commerce/search/headless-search.js'; import type {Controller} from '../../controllers/controller/headless-controller.js'; import {createWaitForActionMiddleware} from '../../utils/utils.js'; -import {buildControllerDefinitions} from '../commerce-ssr-engine/common.js'; +import { + buildControllerDefinitions, + buildRecommendationFilter, +} from '../commerce-ssr-engine/common.js'; import { ControllerDefinitionsMap, InferControllerStaticStateMapFromDefinitionsWithSolutionType, @@ -37,7 +40,7 @@ export interface SSRCommerceEngine extends CommerceEngine { /** * Waits for the search to be completed and returns a promise that resolves to a `SearchCompletedAction`. */ - waitForRequestCompletedAction(): Promise; + waitForRequestCompletedAction(): Promise[]; } export type CommerceEngineDefinitionOptions< @@ -56,13 +59,20 @@ function isSearchCompletedAction(action: unknown): action is Action { ); } +function isRecommendationCompletedAction(action: unknown): action is Action { + return /^commerce\/recommendations\/fetch\/(fulfilled|rejected)$/.test( + (action as UnknownAction).type + ); +} + function noSearchActionRequired(_action: unknown): _action is Action { return true; } function buildSSRCommerceEngine( solutionType: SolutionType, - options: CommerceEngineOptions + options: CommerceEngineOptions, + recommendationCount: number ): SSRCommerceEngine { let actionCompletionMiddleware: ReturnType< typeof createWaitForActionMiddleware @@ -85,11 +95,17 @@ function buildSSRCommerceEngine( ); } + const recommendationActionMiddlewares = Array.from( + {length: recommendationCount}, + () => createWaitForActionMiddleware(isRecommendationCompletedAction) + ); + const commerceEngine = buildCommerceEngine({ ...options, middlewares: [ ...(options.middlewares ?? []), actionCompletionMiddleware.middleware, + ...recommendationActionMiddlewares.map(({middleware}) => middleware), ], }); @@ -101,7 +117,10 @@ function buildSSRCommerceEngine( }, waitForRequestCompletedAction() { - return actionCompletionMiddleware.promise; + return [ + actionCompletionMiddleware.promise, + ...recommendationActionMiddlewares.map(({promise}) => promise), + ]; }, }; } @@ -163,6 +182,10 @@ export function defineCommerceEngine< type HydrateStaticStateFromBuildResultParameters = Parameters; + const recommendationHelper = buildRecommendationFilter( + controllerDefinitions ?? {} + ); + const getOptions = () => { return engineOptions; }; @@ -180,7 +203,8 @@ export function defineCommerceEngine< solutionType, buildOptions?.extend ? await buildOptions.extend(getOptions()) - : getOptions() + : getOptions(), + recommendationHelper.count ); const controllers = buildControllerDefinitions({ definitionsMap: (controllerDefinitions ?? {}) as TControllerDefinitions, @@ -232,10 +256,14 @@ export function defineCommerceEngine< buildSearch(engine).executeFirstSearch(); } - const searchAction = await engine.waitForRequestCompletedAction(); + recommendationHelper.refresh(controllers); + + const searchActions = await Promise.all( + engine.waitForRequestCompletedAction() + ); return createStaticState({ - searchAction, + searchActions, controllers, }) as EngineStaticState< UnknownAction, @@ -266,7 +294,7 @@ export function defineCommerceEngine< solutionType ).fromBuildResult({ buildResult, - searchAction: params[0]!.searchAction, + searchActions: params[0]!.searchActions, }); return staticState; }, @@ -277,10 +305,13 @@ export function defineCommerceEngine< const [ { buildResult: {engine, controllers}, - searchAction, + searchActions, }, ] = params; - engine.dispatch(searchAction); + + searchActions.forEach((action) => { + engine.dispatch(action); + }); await engine.waitForRequestCompletedAction(); return {engine, controllers}; }, diff --git a/packages/headless/src/app/commerce-ssr-engine/common.ts b/packages/headless/src/app/commerce-ssr-engine/common.ts index 2b8907bf678..e6a72dede23 100644 --- a/packages/headless/src/app/commerce-ssr-engine/common.ts +++ b/packages/headless/src/app/commerce-ssr-engine/common.ts @@ -1,3 +1,4 @@ +import {Recommendations} from '../../controllers/commerce/recommendations/headless-recommendations.js'; import {Controller} from '../../controllers/controller/headless-controller.js'; import {InvalidControllerDefinition} from '../../utils/errors.js'; import {filterObject, mapObject} from '../../utils/utils.js'; @@ -103,3 +104,37 @@ export function ensureAtLeastOneSolutionType( throw new InvalidControllerDefinition(); } } +export function buildRecommendationFilter< + TEngine extends CoreEngine | CoreEngineNext, + TControllerDefinitions extends ControllerDefinitionsMap, +>(controllerDefinitions: TControllerDefinitions) { + const keys = Object.entries(controllerDefinitions) + .filter(([_, value]) => 'isRecs' in value && value.isRecs) + .map(([key, _]) => key); + + return { + /** + * Gets the number of recommendation controllers from the controller definitions map. + * + * @returns {number} The number of recommendation controllers in the controller definition map + */ + get count() { + return keys.length; + }, + + /** + * Go through all the controllers passed in argument and only refresh recommendation controllers. + * + * @param controllers - A record of all controllers where the key is the controller name and the value is the controller instance. + */ + refresh(controllers: Record) { + const isRecommendationController = (key: string) => keys.includes(key); + + Object.entries(controllers) + .filter(([key, _]) => isRecommendationController(key)) + .forEach(([_, controller]) => + (controller as Recommendations).refresh?.() + ); + }, + }; +} diff --git a/packages/headless/src/app/search-engine/search-engine.ssr.ts b/packages/headless/src/app/search-engine/search-engine.ssr.ts index b1361574c7e..f14b5191c6f 100644 --- a/packages/headless/src/app/search-engine/search-engine.ssr.ts +++ b/packages/headless/src/app/search-engine/search-engine.ssr.ts @@ -5,6 +5,7 @@ import {UnknownAction} from '@reduxjs/toolkit'; import type {Controller} from '../../controllers/controller/headless-controller.js'; import {LegacySearchAction} from '../../features/analytics/analytics-utils.js'; import {createWaitForActionMiddleware} from '../../utils/utils.js'; +import {buildRecommendationFilter} from '../commerce-ssr-engine/common.js'; import {NavigatorContextProvider} from '../navigatorContextProvider.js'; import { buildControllerDefinitions, @@ -36,7 +37,7 @@ export interface SSRSearchEngine extends SearchEngine { /** * Waits for the search to be completed and returns a promise that resolves to a `SearchCompletedAction`. */ - waitForSearchCompletedAction(): Promise; + waitForSearchCompletedAction(): Promise[]; } /** @@ -60,13 +61,34 @@ function isSearchCompletedAction( ); } -function buildSSRSearchEngine(options: SearchEngineOptions): SSRSearchEngine { +function isRecommendationCompletedAction( + action: unknown +): action is SearchCompletedAction { + return /^recommendation\/get\/(fulfilled|rejected)$/.test( + (action as UnknownAction).type + ); +} + +function buildSSRSearchEngine( + options: SearchEngineOptions, + recommendationCount: number +): SSRSearchEngine { const {middleware, promise} = createWaitForActionMiddleware( isSearchCompletedAction ); + + const recommendationActionMiddlewares = Array.from( + {length: recommendationCount}, + () => createWaitForActionMiddleware(isRecommendationCompletedAction) + ); + const searchEngine = buildSearchEngine({ ...options, - middlewares: [...(options.middlewares ?? []), middleware], + middlewares: [ + ...(options.middlewares ?? []), + middleware, + ...recommendationActionMiddlewares.map(({middleware}) => middleware), + ], }); return { ...searchEngine, @@ -74,7 +96,10 @@ function buildSSRSearchEngine(options: SearchEngineOptions): SSRSearchEngine { return searchEngine.state; }, waitForSearchCompletedAction() { - return promise; + return [ + promise, + ...recommendationActionMiddlewares.map(({promise}) => promise), + ]; }, }; } @@ -121,6 +146,10 @@ export function defineSearchEngine< type HydrateStaticStateFromBuildResultParameters = Parameters; + const recommendationHelper = buildRecommendationFilter( + controllerDefinitions ?? {} + ); + const getOptions = () => { return engineOptions; }; @@ -135,7 +164,8 @@ export function defineSearchEngine< const engine = buildSSRSearchEngine( buildOptions?.extend ? await buildOptions.extend(getOptions()) - : getOptions() + : getOptions(), + recommendationHelper.count ); const controllers = buildControllerDefinitions({ definitionsMap: (controllerDefinitions ?? {}) as TControllerDefinitions, @@ -175,8 +205,14 @@ export function defineSearchEngine< ] = params; engine.executeFirstSearch(); + recommendationHelper.refresh(controllers); + + const searchActions = await Promise.all( + engine.waitForSearchCompletedAction() + ); + return createStaticState({ - searchAction: await engine.waitForSearchCompletedAction(), + searchActions, controllers, }) as EngineStaticState< UnknownAction, @@ -197,7 +233,7 @@ export function defineSearchEngine< const buildResult = await build(...(params as BuildParameters)); const staticState = await hydrateStaticState.fromBuildResult({ buildResult, - searchAction: params[0]!.searchAction, + searchActions: params[0]!.searchActions, }); return staticState; }, @@ -208,10 +244,13 @@ export function defineSearchEngine< const [ { buildResult: {engine, controllers}, - searchAction, + searchActions, }, ] = params; - engine.dispatch(searchAction); + + searchActions.forEach((action) => { + engine.dispatch(action); + }); await engine.waitForSearchCompletedAction(); return {engine, controllers}; }, diff --git a/packages/headless/src/app/ssr-engine/common.ts b/packages/headless/src/app/ssr-engine/common.ts index 26b0503f1dd..79eef38ef33 100644 --- a/packages/headless/src/app/ssr-engine/common.ts +++ b/packages/headless/src/app/ssr-engine/common.ts @@ -58,10 +58,10 @@ export function buildControllerDefinitions< } export function createStaticState({ - searchAction, + searchActions, controllers, }: { - searchAction: TSearchAction; + searchActions: TSearchAction[]; controllers: ControllersMap; }): EngineStaticState< TSearchAction, @@ -71,7 +71,7 @@ export function createStaticState({ controllers: mapObject(controllers, (controller) => ({ state: clone(controller.state), })) as InferControllerStaticStateMapFromControllers, - searchAction: clone(searchAction), + searchActions: searchActions.map((action) => clone(action)), }; } diff --git a/packages/headless/src/app/ssr-engine/types/common.ts b/packages/headless/src/app/ssr-engine/types/common.ts index 831b23ec423..853beb17cc5 100644 --- a/packages/headless/src/app/ssr-engine/types/common.ts +++ b/packages/headless/src/app/ssr-engine/types/common.ts @@ -105,7 +105,7 @@ export interface EngineStaticState< TSearchAction extends UnknownAction, TControllers extends ControllerStaticStateMap, > { - searchAction: TSearchAction; + searchActions: TSearchAction[]; controllers: TControllers; } diff --git a/packages/headless/src/app/ssr-engine/types/hydrate-static-state.ts b/packages/headless/src/app/ssr-engine/types/hydrate-static-state.ts index f7164fa0932..9f4a265badb 100644 --- a/packages/headless/src/app/ssr-engine/types/hydrate-static-state.ts +++ b/packages/headless/src/app/ssr-engine/types/hydrate-static-state.ts @@ -10,7 +10,7 @@ import { import {FromBuildResult} from './from-build-result.js'; export interface HydrateStaticStateOptions { - searchAction: TSearchAction; + searchActions: TSearchAction[]; } export type HydrateStaticState< diff --git a/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts b/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts index 2996ef6e9de..b0335ba6e90 100644 --- a/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts +++ b/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts @@ -20,11 +20,12 @@ export interface RecommendationsDefinition * */ export function defineRecommendations( props: RecommendationsProps -): RecommendationsDefinition { +): RecommendationsDefinition & {isRecs: true} { return { search: true, listing: true, standalone: true, + isRecs: true, build: (engine) => buildRecommendations(engine, props), }; } diff --git a/packages/samples/headless-ssr-commerce/app/_components/pages/listing-page.tsx b/packages/samples/headless-ssr-commerce/app/_components/pages/listing-page.tsx index 05612968ed2..73e7718fa58 100644 --- a/packages/samples/headless-ssr-commerce/app/_components/pages/listing-page.tsx +++ b/packages/samples/headless-ssr-commerce/app/_components/pages/listing-page.tsx @@ -34,13 +34,10 @@ export default function ListingPage({ useEffect(() => { listingEngineDefinition .hydrateStaticState({ - searchAction: staticState.searchAction, + searchActions: staticState.searchActions, }) .then(({engine, controllers}) => { setHydratedState({engine, controllers}); - // Refreshing recommendations in the browser after hydrating the state in the client-side - // Recommendation refresh in the server is not supported yet. - controllers.popularBoughtRecs.refresh(); }); }, [staticState]); From d571439c51d5b64e0b7591cea1f6ed64f82fe5da Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Thu, 31 Oct 2024 21:25:12 -0400 Subject: [PATCH 02/13] recognize search action promises --- .../commerce-engine/commerce-engine.ssr.ts | 18 +++++++--- packages/headless/src/utils/utils.ts | 36 +++++++++++++++++++ .../app/_components/pages/listing-page.tsx | 4 +++ .../app/_components/pages/product-page.tsx | 2 +- .../app/_components/pages/recommendation.tsx | 2 +- .../app/_components/pages/search-page.tsx | 2 +- 6 files changed, 56 insertions(+), 8 deletions(-) diff --git a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts index b3ff825c307..cb12cf95f40 100644 --- a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts +++ b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts @@ -6,7 +6,10 @@ import {stateKey} from '../../app/state-key.js'; import {buildProductListing} from '../../controllers/commerce/product-listing/headless-product-listing.js'; import {buildSearch} from '../../controllers/commerce/search/headless-search.js'; import type {Controller} from '../../controllers/controller/headless-controller.js'; -import {createWaitForActionMiddleware} from '../../utils/utils.js'; +import { + createWaitForActionMiddleware, + createWaitForActionMiddlewareForRecommendation, +} from '../../utils/utils.js'; import { buildControllerDefinitions, buildRecommendationFilter, @@ -95,9 +98,14 @@ function buildSSRCommerceEngine( ); } + const memo: Set = new Set(); const recommendationActionMiddlewares = Array.from( {length: recommendationCount}, - () => createWaitForActionMiddleware(isRecommendationCompletedAction) + () => + createWaitForActionMiddlewareForRecommendation( + isRecommendationCompletedAction, + memo + ) ); const commerceEngine = buildCommerceEngine({ @@ -182,7 +190,7 @@ export function defineCommerceEngine< type HydrateStaticStateFromBuildResultParameters = Parameters; - const recommendationHelper = buildRecommendationFilter( + const recommendationFilter = buildRecommendationFilter( controllerDefinitions ?? {} ); @@ -204,7 +212,7 @@ export function defineCommerceEngine< buildOptions?.extend ? await buildOptions.extend(getOptions()) : getOptions(), - recommendationHelper.count + recommendationFilter.count ); const controllers = buildControllerDefinitions({ definitionsMap: (controllerDefinitions ?? {}) as TControllerDefinitions, @@ -256,7 +264,7 @@ export function defineCommerceEngine< buildSearch(engine).executeFirstSearch(); } - recommendationHelper.refresh(controllers); + recommendationFilter.refresh(controllers); const searchActions = await Promise.all( engine.waitForRequestCompletedAction() diff --git a/packages/headless/src/utils/utils.ts b/packages/headless/src/utils/utils.ts index 92af128c4d1..4e6a52ce460 100644 --- a/packages/headless/src/utils/utils.ts +++ b/packages/headless/src/utils/utils.ts @@ -157,3 +157,39 @@ export function createWaitForActionMiddleware( return {promise, middleware}; } + +export function createWaitForActionMiddlewareForRecommendation< + TAction extends Action, +>( + isDesiredAction: (action: unknown) => action is TAction, + memo: Set +): {promise: Promise; middleware: Middleware} { + const {promise, resolve} = createDeferredPromise(); + let resolved = false; + + const middleware: Middleware = () => (next) => (action) => { + next(action); + // [x] Should not resolve the same action more than once + // [x] Do not resolve a recommendation action if it is in the slot id + if ( + isDesiredAction(action) && + // TODO: merge these two conditions + !resolved && + !memo.has( + (action as unknown as {meta: {arg: {slotId: string}}})?.meta?.arg + ?.slotId + ) //TODO:: this will not work for non recommendation action + ) { + resolved = true; + // TODO: fix this type casting + memo.add( + (action as unknown as {meta: {arg: {slotId: string}}})?.meta?.arg + ?.slotId + ); + console.log(' --- isDesiredAction ---', action); + resolve(action); + } + }; + + return {promise, middleware}; +} diff --git a/packages/samples/headless-ssr-commerce/app/_components/pages/listing-page.tsx b/packages/samples/headless-ssr-commerce/app/_components/pages/listing-page.tsx index 73e7718fa58..48f800bfa95 100644 --- a/packages/samples/headless-ssr-commerce/app/_components/pages/listing-page.tsx +++ b/packages/samples/headless-ssr-commerce/app/_components/pages/listing-page.tsx @@ -107,6 +107,10 @@ export default function ListingPage({ staticState={staticState.controllers.popularBoughtRecs.state} controller={hydratedState?.controllers.popularBoughtRecs} /> + diff --git a/packages/samples/headless-ssr-commerce/app/_components/pages/product-page.tsx b/packages/samples/headless-ssr-commerce/app/_components/pages/product-page.tsx index d964f2b9b08..733217d24f1 100644 --- a/packages/samples/headless-ssr-commerce/app/_components/pages/product-page.tsx +++ b/packages/samples/headless-ssr-commerce/app/_components/pages/product-page.tsx @@ -36,7 +36,7 @@ export default function ProductPage(props: IProductPageProps) { useEffect(() => { standaloneEngineDefinition .hydrateStaticState({ - searchAction: staticState.searchAction, + searchActions: staticState.searchActions, }) .then(({engine, controllers}) => { setHydratedState({engine, controllers}); diff --git a/packages/samples/headless-ssr-commerce/app/_components/pages/recommendation.tsx b/packages/samples/headless-ssr-commerce/app/_components/pages/recommendation.tsx index 55d7ce66d52..90ffd1d4600 100644 --- a/packages/samples/headless-ssr-commerce/app/_components/pages/recommendation.tsx +++ b/packages/samples/headless-ssr-commerce/app/_components/pages/recommendation.tsx @@ -28,7 +28,7 @@ export default function Recommendation({ useEffect(() => { standaloneEngineDefinition .hydrateStaticState({ - searchAction: staticState.searchAction, + searchActions: staticState.searchActions, }) .then(({engine, controllers}) => { setHydratedState({engine, controllers}); diff --git a/packages/samples/headless-ssr-commerce/app/_components/pages/search-page.tsx b/packages/samples/headless-ssr-commerce/app/_components/pages/search-page.tsx index 3e5d7644bcb..61cff84da1c 100644 --- a/packages/samples/headless-ssr-commerce/app/_components/pages/search-page.tsx +++ b/packages/samples/headless-ssr-commerce/app/_components/pages/search-page.tsx @@ -33,7 +33,7 @@ export default function SearchPage({ useEffect(() => { searchEngineDefinition .hydrateStaticState({ - searchAction: staticState.searchAction, + searchActions: staticState.searchActions, }) .then(({engine, controllers}) => { setHydratedState({engine, controllers}); From 4dc2717481e6cf03edf130618ed87335f6ff05a3 Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Fri, 1 Nov 2024 10:29:18 -0400 Subject: [PATCH 03/13] add typeguard --- packages/headless/src/utils/utils.ts | 41 +++++++++++-------- .../app/_lib/commerce-engine-config.ts | 8 ++++ 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/packages/headless/src/utils/utils.ts b/packages/headless/src/utils/utils.ts index 4e6a52ce460..badd60b0401 100644 --- a/packages/headless/src/utils/utils.ts +++ b/packages/headless/src/utils/utils.ts @@ -1,4 +1,5 @@ -import {Middleware, Action} from '@reduxjs/toolkit'; +import {Middleware, Action, PayloadAction} from '@reduxjs/toolkit'; +import {FetchRecommendationsPayload} from '../features/commerce/recommendations/recommendations-actions.js'; export const randomID = (prepend?: string, length = 5) => prepend + @@ -158,35 +159,41 @@ export function createWaitForActionMiddleware( return {promise, middleware}; } +function isRecommendationActionPayload

( + action: unknown +): action is PayloadAction { + // TODO: clean that thing!! + if (typeof action === 'object' && action !== null && 'meta' in action) { + return ( + (action as PayloadAction).meta + ?.arg?.slotId !== undefined + ); + } + return false; +} + export function createWaitForActionMiddlewareForRecommendation< TAction extends Action, >( isDesiredAction: (action: unknown) => action is TAction, memo: Set + //TODO:: this will not work for non recommendation action ): {promise: Promise; middleware: Middleware} { const {promise, resolve} = createDeferredPromise(); - let resolved = false; + let hasBeenResolved = false; + const hasSlotBeenProcessed = (slotId: string) => memo.has(slotId); const middleware: Middleware = () => (next) => (action) => { next(action); - // [x] Should not resolve the same action more than once - // [x] Do not resolve a recommendation action if it is in the slot id + if ( isDesiredAction(action) && - // TODO: merge these two conditions - !resolved && - !memo.has( - (action as unknown as {meta: {arg: {slotId: string}}})?.meta?.arg - ?.slotId - ) //TODO:: this will not work for non recommendation action + !hasBeenResolved && + isRecommendationActionPayload(action) && + !hasSlotBeenProcessed(action.meta.arg.slotId) ) { - resolved = true; - // TODO: fix this type casting - memo.add( - (action as unknown as {meta: {arg: {slotId: string}}})?.meta?.arg - ?.slotId - ); - console.log(' --- isDesiredAction ---', action); + hasBeenResolved = true; + memo.add(action.meta.arg.slotId); resolve(action); } }; diff --git a/packages/samples/headless-ssr-commerce/app/_lib/commerce-engine-config.ts b/packages/samples/headless-ssr-commerce/app/_lib/commerce-engine-config.ts index ce281375e97..e236754b8c3 100644 --- a/packages/samples/headless-ssr-commerce/app/_lib/commerce-engine-config.ts +++ b/packages/samples/headless-ssr-commerce/app/_lib/commerce-engine-config.ts @@ -53,6 +53,14 @@ export default { slotId: 'af4fb7ba-6641-4b67-9cf9-be67e9f30174', }, }), + // TODO: check for invalid slotId + // TODO: check for duplicate slotId + // TODO: encounter for multiple recommendations with same slot id + // popwularBoughtRecs: defineRecommendations({ + // options: { + // slotId: 'af4fb7ba-6641-4b67-9cf9-be67e9f30172', + // }, + // }), cart: defineCart(), searchBox: defineSearchBox(), context: defineContext(), From 2e5f79451e5948e11efe6f907fcf2be6360a33c6 Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Fri, 1 Nov 2024 16:29:04 -0400 Subject: [PATCH 04/13] prevent multiple recommendations with the same slot --- .../src/app/commerce-ssr-engine/common.ts | 24 ++++++++++++++++--- .../headless-recommendations.ssr.ts | 9 +++++-- packages/headless/src/utils/utils.ts | 10 ++++++++ .../app/_lib/commerce-engine-config.ts | 10 ++++++-- 4 files changed, 46 insertions(+), 7 deletions(-) diff --git a/packages/headless/src/app/commerce-ssr-engine/common.ts b/packages/headless/src/app/commerce-ssr-engine/common.ts index e6a72dede23..a103fdf0546 100644 --- a/packages/headless/src/app/commerce-ssr-engine/common.ts +++ b/packages/headless/src/app/commerce-ssr-engine/common.ts @@ -108,9 +108,27 @@ export function buildRecommendationFilter< TEngine extends CoreEngine | CoreEngineNext, TControllerDefinitions extends ControllerDefinitionsMap, >(controllerDefinitions: TControllerDefinitions) { - const keys = Object.entries(controllerDefinitions) - .filter(([_, value]) => 'isRecs' in value && value.isRecs) - .map(([key, _]) => key); + const seenSlotIds = new Set(); + const filtered = Object.entries(controllerDefinitions).filter( + ([_, value]) => { + if ('isRecs' in value && value.isRecs) { + const slotId = (value as unknown as {slotId: string}).slotId; // TODO: fix type and CLEAN THAT! + // TODO: use a combination of slotId and productId name to identify the controller + if (seenSlotIds.has(slotId)) { + console.log( + 'WARNING: Multiple recommendation controllers found for the same slotId', + slotId + ); + return false; + } else { + seenSlotIds.add(slotId); + return true; + } + } + } + ); + + const keys = filtered.map(([key, _]) => key); return { /** diff --git a/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts b/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts index b0335ba6e90..3dcafd6118c 100644 --- a/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts +++ b/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts @@ -20,12 +20,17 @@ export interface RecommendationsDefinition * */ export function defineRecommendations( props: RecommendationsProps -): RecommendationsDefinition & {isRecs: true} { +): RecommendationsDefinition & { + isRecs: true; +} & RecommendationsProps['options'] { + // TODO: have an extended recommendationDefinition that is not exposed return { search: true, listing: true, standalone: true, - isRecs: true, + // TODO: encapsulate into a single object called meta (e.g. meta: {isRecs: true, ...props.options}) + isRecs: true, // TODO: mark internal + ...props.options, // TODO: mark internal build: (engine) => buildRecommendations(engine, props), }; } diff --git a/packages/headless/src/utils/utils.ts b/packages/headless/src/utils/utils.ts index badd60b0401..ee2ec13dd87 100644 --- a/packages/headless/src/utils/utils.ts +++ b/packages/headless/src/utils/utils.ts @@ -186,12 +186,22 @@ export function createWaitForActionMiddlewareForRecommendation< const middleware: Middleware = () => (next) => (action) => { next(action); + // if (isDesiredAction(action) && isRecommendationActionPayload(action)) { + // console.log(''); + // console.log('slotId:', action.meta.arg.slotId); + // console.log( + // 'condition: ', + // hasBeenResolved, + // memo.has(action.meta.arg.slotId) + // ); + // } if ( isDesiredAction(action) && !hasBeenResolved && isRecommendationActionPayload(action) && !hasSlotBeenProcessed(action.meta.arg.slotId) ) { + // console.log(' --- RESOLVE ---'); hasBeenResolved = true; memo.add(action.meta.arg.slotId); resolve(action); diff --git a/packages/samples/headless-ssr-commerce/app/_lib/commerce-engine-config.ts b/packages/samples/headless-ssr-commerce/app/_lib/commerce-engine-config.ts index e236754b8c3..2598a2b4d54 100644 --- a/packages/samples/headless-ssr-commerce/app/_lib/commerce-engine-config.ts +++ b/packages/samples/headless-ssr-commerce/app/_lib/commerce-engine-config.ts @@ -53,8 +53,14 @@ export default { slotId: 'af4fb7ba-6641-4b67-9cf9-be67e9f30174', }, }), - // TODO: check for invalid slotId - // TODO: check for duplicate slotId + popularBoughtRecs_DUPLICATE: defineRecommendations({ + // TODO: support option to run only on specific + options: { + slotId: 'af4fb7ba-6641-4b67-9cf9-be67e9f30174', + }, + }), + // TODO: [x] check for invalid slotId => it will reject as expected + // TODO: [ ] check for duplicate slotId // TODO: encounter for multiple recommendations with same slot id // popwularBoughtRecs: defineRecommendations({ // options: { From de430495d723f33f333f9422cae35292e02b4e3b Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Sun, 3 Nov 2024 07:05:24 -0500 Subject: [PATCH 05/13] clean --- .../src/app/commerce-ssr-engine/common.ts | 49 ++++++++++++------- .../headless-recommendations.ssr.ts | 18 ++++--- 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/packages/headless/src/app/commerce-ssr-engine/common.ts b/packages/headless/src/app/commerce-ssr-engine/common.ts index a103fdf0546..6c3d8cb039d 100644 --- a/packages/headless/src/app/commerce-ssr-engine/common.ts +++ b/packages/headless/src/app/commerce-ssr-engine/common.ts @@ -1,4 +1,5 @@ import {Recommendations} from '../../controllers/commerce/recommendations/headless-recommendations.js'; +import {RecommendationsDefinitionMeta} from '../../controllers/commerce/recommendations/headless-recommendations.ssr.js'; import {Controller} from '../../controllers/controller/headless-controller.js'; import {InvalidControllerDefinition} from '../../utils/errors.js'; import {filterObject, mapObject} from '../../utils/utils.js'; @@ -104,31 +105,45 @@ export function ensureAtLeastOneSolutionType( throw new InvalidControllerDefinition(); } } + export function buildRecommendationFilter< TEngine extends CoreEngine | CoreEngineNext, TControllerDefinitions extends ControllerDefinitionsMap, >(controllerDefinitions: TControllerDefinitions) { - const seenSlotIds = new Set(); + const slotIdSet = new Set(); + + const isRecommendationDefinition = < + C extends ControllerDefinition, + >( + controller: C + ): controller is C & RecommendationsDefinitionMeta => { + return '_recommendationProps' in controller; + }; + + const warnDuplicateRecommendation = (slotId: string, productId?: string) => { + console.warn( + 'Multiple recommendation controllers found for the same slotId and productId', + {slotId, productId} + ); + }; + const filtered = Object.entries(controllerDefinitions).filter( ([_, value]) => { - if ('isRecs' in value && value.isRecs) { - const slotId = (value as unknown as {slotId: string}).slotId; // TODO: fix type and CLEAN THAT! - // TODO: use a combination of slotId and productId name to identify the controller - if (seenSlotIds.has(slotId)) { - console.log( - 'WARNING: Multiple recommendation controllers found for the same slotId', - slotId - ); - return false; - } else { - seenSlotIds.add(slotId); - return true; - } + if (!isRecommendationDefinition(value)) { + return false; + } + const {slotId, productId} = value._recommendationProps; + const key = `${slotId}${productId || ''}`; + if (slotIdSet.has(key)) { + warnDuplicateRecommendation(slotId, productId); + return false; } + slotIdSet.add(key); + return true; } ); - const keys = filtered.map(([key, _]) => key); + const name = filtered.map(([name, _]) => name); return { /** @@ -137,7 +152,7 @@ export function buildRecommendationFilter< * @returns {number} The number of recommendation controllers in the controller definition map */ get count() { - return keys.length; + return name.length; }, /** @@ -146,7 +161,7 @@ export function buildRecommendationFilter< * @param controllers - A record of all controllers where the key is the controller name and the value is the controller instance. */ refresh(controllers: Record) { - const isRecommendationController = (key: string) => keys.includes(key); + const isRecommendationController = (key: string) => name.includes(key); Object.entries(controllers) .filter(([key, _]) => isRecommendationController(key)) diff --git a/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts b/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts index 3dcafd6118c..542426a2377 100644 --- a/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts +++ b/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts @@ -8,6 +8,13 @@ import { export type {Recommendations, RecommendationsState}; +/** + * @internal + * */ +export type RecommendationsDefinitionMeta = { + _recommendationProps: {} & RecommendationsProps['options']; +}; + export interface RecommendationsDefinition extends UniversalControllerDefinitionWithoutProps {} /** @@ -20,17 +27,14 @@ export interface RecommendationsDefinition * */ export function defineRecommendations( props: RecommendationsProps -): RecommendationsDefinition & { - isRecs: true; -} & RecommendationsProps['options'] { - // TODO: have an extended recommendationDefinition that is not exposed +): RecommendationsDefinition & RecommendationsDefinitionMeta { return { search: true, listing: true, standalone: true, - // TODO: encapsulate into a single object called meta (e.g. meta: {isRecs: true, ...props.options}) - isRecs: true, // TODO: mark internal - ...props.options, // TODO: mark internal + _recommendationProps: { + ...props.options, + }, build: (engine) => buildRecommendations(engine, props), }; } From c4b69b0a42523330ccb1c9617ae801b91287a20d Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Wed, 13 Nov 2024 11:22:44 -0500 Subject: [PATCH 06/13] feat(headless): server side commerce recommendations --- .../commerce-engine/commerce-engine.ssr.ts | 44 +++++++----- .../src/app/commerce-ssr-engine/common.ts | 2 + .../app/commerce-ssr-engine/types/common.ts | 13 ++++ .../app/search-engine/search-engine.ssr.ts | 57 +++------------ .../headless/src/app/ssr-engine/common.ts | 6 +- .../src/app/ssr-engine/types/common.ts | 2 +- .../ssr-engine/types/hydrate-static-state.ts | 2 +- .../headless-recommendations.ssr.ts | 8 +-- .../app/(listing)/[category]/page.tsx | 7 ++ .../app/_components/pages/recommendation.tsx | 55 -------------- .../components/pages/product-page.tsx | 71 +++++++++++++++++++ .../components/pages/recommendation.tsx | 55 ++++++++++++++ 12 files changed, 192 insertions(+), 130 deletions(-) delete mode 100644 packages/samples/headless-ssr-commerce/app/_components/pages/recommendation.tsx create mode 100644 packages/samples/headless-ssr-commerce/components/pages/product-page.tsx create mode 100644 packages/samples/headless-ssr-commerce/components/pages/recommendation.tsx diff --git a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts index e234c302ca6..09363d37c87 100644 --- a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts +++ b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts @@ -44,7 +44,7 @@ export interface SSRCommerceEngine extends CommerceEngine { /** * Waits for the search to be completed and returns a promise that resolves to a `SearchCompletedAction`. */ - waitForRequestCompletedAction(): Promise[]; + waitForRequestCompletedAction(): Promise; } export type CommerceEngineDefinitionOptions< @@ -126,10 +126,7 @@ function buildSSRCommerceEngine( }, waitForRequestCompletedAction() { - return [ - actionCompletionMiddleware.promise, - ...recommendationActionMiddlewares.map(({promise}) => promise), - ]; + return actionCompletionMiddleware.promise; }, }; } @@ -170,6 +167,10 @@ export function defineCommerceEngine< TControllerDefinitions, SolutionType.standalone >; + recommendationDefinition: CommerceEngineDefinition< + TControllerDefinitions, + SolutionType.recommendation + >; } { const {controllers: controllerDefinitions, ...engineOptions} = options; type Definition = CommerceEngineDefinition< @@ -263,16 +264,17 @@ export function defineCommerceEngine< buildProductListing(engine).executeFirstRequest(); } else if (solutionType === SolutionType.search) { buildSearch(engine).executeFirstSearch(); + } else if (solutionType === SolutionType.recommendation) { + // here build the filter and refresh them all + // build every recommendation and refresh them all ? + // buildRecommendations(engine).refresh(); + recommendationFilter.refresh(controllers); } - recommendationFilter.refresh(controllers); - - const searchActions = await Promise.all( - engine.waitForRequestCompletedAction() - ); + const searchAction = await engine.waitForRequestCompletedAction(); return createStaticState({ - searchActions, + searchAction, controllers, }) as EngineStaticState< UnknownAction, @@ -297,7 +299,7 @@ export function defineCommerceEngine< solutionType ).fromBuildResult({ buildResult, - searchActions: params[0]!.searchActions, + searchAction: params[0]!.searchAction, }); return staticState; }, @@ -308,14 +310,12 @@ export function defineCommerceEngine< const [ { buildResult: {engine, controllers}, - searchActions, + searchAction, }, ] = params; - searchActions.forEach((action) => { - engine.dispatch(action); - }); - await engine.waitForRequestCompletedAction(); + engine.dispatch(searchAction); + engine.waitForRequestCompletedAction(); return {engine, controllers}; }, } @@ -342,5 +342,15 @@ export function defineCommerceEngine< TControllerDefinitions, SolutionType.standalone >, + recommendationDefinition: { + fetchStaticState: fetchStaticStateFactory(SolutionType.recommendation), + hydrateStaticState: hydrateStaticStateFactory( + SolutionType.recommendation + ), + setNavigatorContextProvider, + } as CommerceEngineDefinition< + TControllerDefinitions, + SolutionType.recommendation + >, }; } diff --git a/packages/headless/src/app/commerce-ssr-engine/common.ts b/packages/headless/src/app/commerce-ssr-engine/common.ts index 6c3d8cb039d..6f2b68407cb 100644 --- a/packages/headless/src/app/commerce-ssr-engine/common.ts +++ b/packages/headless/src/app/commerce-ssr-engine/common.ts @@ -121,6 +121,7 @@ export function buildRecommendationFilter< }; const warnDuplicateRecommendation = (slotId: string, productId?: string) => { + // Use logger here console.warn( 'Multiple recommendation controllers found for the same slotId and productId', {slotId, productId} @@ -135,6 +136,7 @@ export function buildRecommendationFilter< const {slotId, productId} = value._recommendationProps; const key = `${slotId}${productId || ''}`; if (slotIdSet.has(key)) { + // We keep this warning it is good warnDuplicateRecommendation(slotId, productId); return false; } diff --git a/packages/headless/src/app/commerce-ssr-engine/types/common.ts b/packages/headless/src/app/commerce-ssr-engine/types/common.ts index b773dd642f0..36af8541ff1 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/common.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/common.ts @@ -19,6 +19,7 @@ export enum SolutionType { search = 'search', listing = 'listing', standalone = 'standalone', + recommendation = 'recommendation', } export interface ControllerDefinitionWithoutProps< @@ -172,6 +173,13 @@ interface ListingOnlyController { [SolutionType.listing]: true; } +interface RecommendationOnlyController { + /** + * @internal + */ + [SolutionType.recommendation]: true; +} + interface SearchAndListingController { /** * @internal @@ -183,6 +191,11 @@ interface SearchAndListingController { [SolutionType.listing]: true; } +export type RecommendationOnlyControllerDefinitionWithoutProps< + TController extends Controller, +> = ControllerDefinitionWithoutProps & + RecommendationOnlyController; + export type SearchOnlyControllerDefinitionWithoutProps< TController extends Controller, > = ControllerDefinitionWithoutProps & diff --git a/packages/headless/src/app/search-engine/search-engine.ssr.ts b/packages/headless/src/app/search-engine/search-engine.ssr.ts index 7134bff4be0..d08ac059f3f 100644 --- a/packages/headless/src/app/search-engine/search-engine.ssr.ts +++ b/packages/headless/src/app/search-engine/search-engine.ssr.ts @@ -5,7 +5,6 @@ import {UnknownAction} from '@reduxjs/toolkit'; import type {Controller} from '../../controllers/controller/headless-controller.js'; import {LegacySearchAction} from '../../features/analytics/analytics-utils.js'; import {createWaitForActionMiddleware} from '../../utils/utils.js'; -import {buildRecommendationFilter} from '../commerce-ssr-engine/common.js'; import {buildLogger} from '../logger.js'; import {NavigatorContextProvider} from '../navigatorContextProvider.js'; import { @@ -38,7 +37,7 @@ export interface SSRSearchEngine extends SearchEngine { /** * Waits for the search to be completed and returns a promise that resolves to a `SearchCompletedAction`. */ - waitForSearchCompletedAction(): Promise[]; + waitForSearchCompletedAction(): Promise; } /** @@ -62,34 +61,13 @@ function isSearchCompletedAction( ); } -function isRecommendationCompletedAction( - action: unknown -): action is SearchCompletedAction { - return /^recommendation\/get\/(fulfilled|rejected)$/.test( - (action as UnknownAction).type - ); -} - -function buildSSRSearchEngine( - options: SearchEngineOptions, - recommendationCount: number -): SSRSearchEngine { +function buildSSRSearchEngine(options: SearchEngineOptions): SSRSearchEngine { const {middleware, promise} = createWaitForActionMiddleware( isSearchCompletedAction ); - - const recommendationActionMiddlewares = Array.from( - {length: recommendationCount}, - () => createWaitForActionMiddleware(isRecommendationCompletedAction) - ); - const searchEngine = buildSearchEngine({ ...options, - middlewares: [ - ...(options.middlewares ?? []), - middleware, - ...recommendationActionMiddlewares.map(({middleware}) => middleware), - ], + middlewares: [...(options.middlewares ?? []), middleware], }); return { ...searchEngine, @@ -97,10 +75,7 @@ function buildSSRSearchEngine( return searchEngine.state; }, waitForSearchCompletedAction() { - return [ - promise, - ...recommendationActionMiddlewares.map(({promise}) => promise), - ]; + return promise; }, }; } @@ -147,10 +122,6 @@ export function defineSearchEngine< type HydrateStaticStateFromBuildResultParameters = Parameters; - const recommendationHelper = buildRecommendationFilter( - controllerDefinitions ?? {} - ); - const getOptions = () => { return engineOptions; }; @@ -171,8 +142,7 @@ export function defineSearchEngine< const engine = buildSSRSearchEngine( buildOptions?.extend ? await buildOptions.extend(getOptions()) - : getOptions(), - recommendationHelper.count + : getOptions() ); const controllers = buildControllerDefinitions({ definitionsMap: (controllerDefinitions ?? {}) as TControllerDefinitions, @@ -206,14 +176,8 @@ export function defineSearchEngine< ] = params; engine.executeFirstSearch(); - recommendationHelper.refresh(controllers); - - const searchActions = await Promise.all( - engine.waitForSearchCompletedAction() - ); - return createStaticState({ - searchActions, + searchAction: await engine.waitForSearchCompletedAction(), controllers, }) as EngineStaticState< UnknownAction, @@ -228,7 +192,7 @@ export function defineSearchEngine< const buildResult = await build(...(params as BuildParameters)); const staticState = await hydrateStaticState.fromBuildResult({ buildResult, - searchActions: params[0]!.searchActions, + searchAction: params[0]!.searchAction, }); return staticState; }, @@ -239,13 +203,10 @@ export function defineSearchEngine< const [ { buildResult: {engine, controllers}, - searchActions, + searchAction, }, ] = params; - - searchActions.forEach((action) => { - engine.dispatch(action); - }); + engine.dispatch(searchAction); await engine.waitForSearchCompletedAction(); return {engine, controllers}; }, diff --git a/packages/headless/src/app/ssr-engine/common.ts b/packages/headless/src/app/ssr-engine/common.ts index 79eef38ef33..26b0503f1dd 100644 --- a/packages/headless/src/app/ssr-engine/common.ts +++ b/packages/headless/src/app/ssr-engine/common.ts @@ -58,10 +58,10 @@ export function buildControllerDefinitions< } export function createStaticState({ - searchActions, + searchAction, controllers, }: { - searchActions: TSearchAction[]; + searchAction: TSearchAction; controllers: ControllersMap; }): EngineStaticState< TSearchAction, @@ -71,7 +71,7 @@ export function createStaticState({ controllers: mapObject(controllers, (controller) => ({ state: clone(controller.state), })) as InferControllerStaticStateMapFromControllers, - searchActions: searchActions.map((action) => clone(action)), + searchAction: clone(searchAction), }; } diff --git a/packages/headless/src/app/ssr-engine/types/common.ts b/packages/headless/src/app/ssr-engine/types/common.ts index 853beb17cc5..831b23ec423 100644 --- a/packages/headless/src/app/ssr-engine/types/common.ts +++ b/packages/headless/src/app/ssr-engine/types/common.ts @@ -105,7 +105,7 @@ export interface EngineStaticState< TSearchAction extends UnknownAction, TControllers extends ControllerStaticStateMap, > { - searchActions: TSearchAction[]; + searchAction: TSearchAction; controllers: TControllers; } diff --git a/packages/headless/src/app/ssr-engine/types/hydrate-static-state.ts b/packages/headless/src/app/ssr-engine/types/hydrate-static-state.ts index 9f4a265badb..f7164fa0932 100644 --- a/packages/headless/src/app/ssr-engine/types/hydrate-static-state.ts +++ b/packages/headless/src/app/ssr-engine/types/hydrate-static-state.ts @@ -10,7 +10,7 @@ import { import {FromBuildResult} from './from-build-result.js'; export interface HydrateStaticStateOptions { - searchActions: TSearchAction[]; + searchAction: TSearchAction; } export type HydrateStaticState< diff --git a/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts b/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts index 542426a2377..a146c85db64 100644 --- a/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts +++ b/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts @@ -1,4 +1,4 @@ -import {UniversalControllerDefinitionWithoutProps} from '../../../app/commerce-ssr-engine/types/common.js'; +import {RecommendationOnlyControllerDefinitionWithoutProps} from '../../../app/commerce-ssr-engine/types/common.js'; import {RecommendationsState} from '../recommendations/headless-recommendations.js'; import { RecommendationsProps, @@ -16,7 +16,7 @@ export type RecommendationsDefinitionMeta = { }; export interface RecommendationsDefinition - extends UniversalControllerDefinitionWithoutProps {} + extends RecommendationOnlyControllerDefinitionWithoutProps {} /** * @internal * Defines a `Recommendations` controller instance. @@ -29,9 +29,7 @@ export function defineRecommendations( props: RecommendationsProps ): RecommendationsDefinition & RecommendationsDefinitionMeta { return { - search: true, - listing: true, - standalone: true, + recommendation: true, _recommendationProps: { ...props.options, }, diff --git a/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx b/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx index 740a6869fa6..5e9fe7b1278 100644 --- a/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx +++ b/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx @@ -54,6 +54,8 @@ export default async function Listing({params}: {params: {category: string}}) { }, }); + // const recStaticState = await recsDefinition.fetchStaticState() + return (

+ {/* + + */}
diff --git a/packages/samples/headless-ssr-commerce/app/_components/pages/recommendation.tsx b/packages/samples/headless-ssr-commerce/app/_components/pages/recommendation.tsx deleted file mode 100644 index 90ffd1d4600..00000000000 --- a/packages/samples/headless-ssr-commerce/app/_components/pages/recommendation.tsx +++ /dev/null @@ -1,55 +0,0 @@ -'use client'; - -import {NavigatorContext} from '@coveo/headless/ssr-commerce'; -import {useEffect, useState} from 'react'; -import { - StandaloneStaticState, - StandaloneHydratedState, - standaloneEngineDefinition, -} from '../../_lib/commerce-engine'; -import {Recommendations} from '../recommendation-list'; - -export default function Recommendation({ - staticState, - navigatorContext, -}: { - staticState: StandaloneStaticState; - navigatorContext: NavigatorContext; -}) { - const [hydratedState, setHydratedState] = useState< - StandaloneHydratedState | undefined - >(undefined); - - // Setting the navigator context provider also in client-side before hydrating the application - standaloneEngineDefinition.setNavigatorContextProvider( - () => navigatorContext - ); - - useEffect(() => { - standaloneEngineDefinition - .hydrateStaticState({ - searchActions: staticState.searchActions, - }) - .then(({engine, controllers}) => { - setHydratedState({engine, controllers}); - - // Refreshing recommendations in the browser after hydrating the state in the client-side - // Recommendation refresh in the server is not supported yet. - controllers.popularBoughtRecs.refresh(); - controllers.popularViewedRecs.refresh(); - }); - }, [staticState]); - - return ( - <> - - - - ); -} diff --git a/packages/samples/headless-ssr-commerce/components/pages/product-page.tsx b/packages/samples/headless-ssr-commerce/components/pages/product-page.tsx new file mode 100644 index 00000000000..5f69987bdfe --- /dev/null +++ b/packages/samples/headless-ssr-commerce/components/pages/product-page.tsx @@ -0,0 +1,71 @@ +'use client'; + +import { + standaloneEngineDefinition, + StandaloneHydratedState, + StandaloneStaticState, +} from '@/lib/commerce-engine'; +import {NavigatorContext} from '@coveo/headless-react/ssr-commerce'; +import {useSearchParams} from 'next/navigation'; +import {useEffect, useState} from 'react'; +import Recommendations from '../recommendation-list'; + +interface IProductPageProps { + staticState: StandaloneStaticState; + navigatorContext: NavigatorContext; + productId: string; +} + +export default function ProductPage(props: IProductPageProps) { + const [hydratedState, setHydratedState] = useState< + StandaloneHydratedState | undefined + >(undefined); + + const {staticState, navigatorContext, productId} = props; + + const searchParams = useSearchParams(); + + const price = Number(searchParams.get('price')) ?? NaN; + const name = searchParams.get('name') ?? productId; + + // Setting the navigator context provider also in client-side before hydrating the application + standaloneEngineDefinition.setNavigatorContextProvider( + () => navigatorContext + ); + + useEffect(() => { + standaloneEngineDefinition + .hydrateStaticState({ + searchAction: staticState.searchAction, + controllers: { + cart: { + initialState: {items: staticState.controllers.cart.state.items}, + }, + context: staticState.controllers.context.state, + }, + }) + .then(({engine, controllers}) => { + setHydratedState({engine, controllers}); + + // Refreshing recommendations in the browser after hydrating the state in the client-side + // Recommendation refresh in the server is not supported yet. + controllers.popularBoughtRecs.refresh(); + }); + }, [staticState]); + + const viewController = hydratedState?.controllers.productView; + + useEffect(() => { + viewController?.view({name, productId, price}); + }, [viewController, productId, name, price]); + + return ( + <> +

+ {name} ({productId}) - ${price} +

+
+ + + ); +} diff --git a/packages/samples/headless-ssr-commerce/components/pages/recommendation.tsx b/packages/samples/headless-ssr-commerce/components/pages/recommendation.tsx new file mode 100644 index 00000000000..59305bcf481 --- /dev/null +++ b/packages/samples/headless-ssr-commerce/components/pages/recommendation.tsx @@ -0,0 +1,55 @@ +'use client'; + +// import {NavigatorContext} from '@coveo/headless/ssr-commerce'; +// import {useEffect, useState} from 'react'; +// import { +// StandaloneStaticState, +// StandaloneHydratedState, +// standaloneEngineDefinition, +// } from '@/lib/commerce-engine'; +// import Recommendations from '../recommendation-list'; + +// export default function Recommendation({ +// staticState, +// navigatorContext, +// }: { +// staticState: StandaloneStaticState; +// navigatorContext: NavigatorContext; +// }) { +// const [hydratedState, setHydratedState] = useState< +// StandaloneHydratedState | undefined +// >(undefined); + +// // Setting the navigator context provider also in client-side before hydrating the application +// standaloneEngineDefinition.setNavigatorContextProvider( +// () => navigatorContext +// ); + +// useEffect(() => { +// standaloneEngineDefinition +// .hydrateStaticState({ +// searchActions: staticState.searchActions, +// }) +// .then(({engine, controllers}) => { +// setHydratedState({engine, controllers}); + +// // Refreshing recommendations in the browser after hydrating the state in the client-side +// // Recommendation refresh in the server is not supported yet. +// controllers.popularBoughtRecs.refresh(); +// controllers.popularViewedRecs.refresh(); +// }); +// }, [staticState]); + +// // return ( +// // <> +// // +// // +// // +// // ); +// } From 9a7aea093366254167f105880d3ca63be53558f8 Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Thu, 14 Nov 2024 11:19:47 -0500 Subject: [PATCH 07/13] drafting --- .../src/ssr-commerce/commerce-engine.tsx | 1 + .../commerce-engine/commerce-engine.ssr.ts | 6 +- .../commerce-ssr-engine/types/core-engine.ts | 57 +++++++++++++------ .../src/app/ssr-engine/types/build.ts | 18 ++++++ .../ssr-engine/types/fetch-static-state.ts | 28 +++++++++ .../app/(listing)/[category]/page.tsx | 10 ++-- .../components/pages/product-page.tsx | 2 +- 7 files changed, 97 insertions(+), 25 deletions(-) diff --git a/packages/headless-react/src/ssr-commerce/commerce-engine.tsx b/packages/headless-react/src/ssr-commerce/commerce-engine.tsx index 76a0f755837..26f16b386d4 100644 --- a/packages/headless-react/src/ssr-commerce/commerce-engine.tsx +++ b/packages/headless-react/src/ssr-commerce/commerce-engine.tsx @@ -65,6 +65,7 @@ export function defineCommerceEngine< const { listingEngineDefinition, searchEngineDefinition, + recommendationEngineDefinition, standaloneEngineDefinition, } = defineBaseCommerceEngine({...options}); return { diff --git a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts index 09363d37c87..75c07c04caf 100644 --- a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts +++ b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts @@ -167,7 +167,7 @@ export function defineCommerceEngine< TControllerDefinitions, SolutionType.standalone >; - recommendationDefinition: CommerceEngineDefinition< + recommendationEngineDefinition: CommerceEngineDefinition< TControllerDefinitions, SolutionType.recommendation >; @@ -186,6 +186,7 @@ export function defineCommerceEngine< HydrateStaticStateFunction['fromBuildResult']; type BuildParameters = Parameters; type FetchStaticStateParameters = Parameters; + type HydrateStaticStateParameters = Parameters; type FetchStaticStateFromBuildResultParameters = Parameters; @@ -342,7 +343,8 @@ export function defineCommerceEngine< TControllerDefinitions, SolutionType.standalone >, - recommendationDefinition: { + recommendationEngineDefinition: { + build: buildFactory(SolutionType.recommendation), fetchStaticState: fetchStaticStateFactory(SolutionType.recommendation), hydrateStaticState: hydrateStaticStateFactory( SolutionType.recommendation diff --git a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts index a9d39706164..fc2af464a75 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts @@ -3,9 +3,12 @@ import type {Controller} from '../../../controllers/controller/headless-controll import {EngineConfiguration} from '../../engine-configuration.js'; import {CoreEngine, CoreEngineNext} from '../../engine.js'; import {NavigatorContextProvider} from '../../navigatorContextProvider.js'; -import {Build} from '../../ssr-engine/types/build.js'; +import {Build, BuildWithList} from '../../ssr-engine/types/build.js'; import {InferControllerPropsMapFromDefinitions} from '../../ssr-engine/types/common.js'; -import {FetchStaticState} from '../../ssr-engine/types/fetch-static-state.js'; +import { + FetchStaticState, + FetchStaticStateWithList, +} from '../../ssr-engine/types/fetch-static-state.js'; import {HydrateStaticState} from '../../ssr-engine/types/hydrate-static-state.js'; import { ControllerDefinitionsMap, @@ -37,16 +40,27 @@ export interface EngineDefinition< /** * Fetches the static state on the server side using your engine definition. */ - fetchStaticState: FetchStaticState< - TEngine, - InferControllersMapFromDefinition, - UnknownAction, - InferControllerStaticStateMapFromDefinitionsWithSolutionType< - TControllers, - TSolutionType - >, - InferControllerPropsMapFromDefinitions - >; + fetchStaticState: TSolutionType extends SolutionType.recommendation + ? FetchStaticStateWithList< + TEngine, + InferControllersMapFromDefinition, + UnknownAction, + InferControllerStaticStateMapFromDefinitionsWithSolutionType< + TControllers, + TSolutionType + >, + InferControllerPropsMapFromDefinitions + > + : FetchStaticState< + TEngine, + InferControllersMapFromDefinition, + UnknownAction, + InferControllerStaticStateMapFromDefinitionsWithSolutionType< + TControllers, + TSolutionType + >, + InferControllerPropsMapFromDefinitions + >; /** * Fetches the hydrated state on the client side using your engine definition and the static state. */ @@ -59,12 +73,19 @@ export interface EngineDefinition< /** * Builds an engine and its controllers from an engine definition. */ - build: Build< - TEngine, - TEngineOptions, - InferControllersMapFromDefinition, - InferControllerPropsMapFromDefinitions - >; + build: TSolutionType extends SolutionType.recommendation + ? BuildWithList< + TEngine, + TEngineOptions, + InferControllersMapFromDefinition, + InferControllerPropsMapFromDefinitions + > + : Build< + TEngine, + TEngineOptions, + InferControllersMapFromDefinition, + InferControllerPropsMapFromDefinitions + >; /** * Sets the navigator context provider. diff --git a/packages/headless/src/app/ssr-engine/types/build.ts b/packages/headless/src/app/ssr-engine/types/build.ts index e7dcabba4dd..bb6c9eca00b 100644 --- a/packages/headless/src/app/ssr-engine/types/build.ts +++ b/packages/headless/src/app/ssr-engine/types/build.ts @@ -12,6 +12,24 @@ export interface BuildOptions { extend?: OptionsExtender; } +export interface BuildWithList< + TEngine extends CoreEngine | CoreEngineNext, + TEngineOptions, + TControllersMap extends ControllersMap, + TControllersProps extends ControllersPropsMap, +> { + /** + * Initializes an engine and controllers from the definition. + */ + ( + c: (keyof TControllersMap)[], + ...params: OptionsTuple< + BuildOptions & + EngineDefinitionControllersPropsOption + > + ): Promise>; +} + export interface Build< TEngine extends CoreEngine | CoreEngineNext, TEngineOptions, diff --git a/packages/headless/src/app/ssr-engine/types/fetch-static-state.ts b/packages/headless/src/app/ssr-engine/types/fetch-static-state.ts index a1f87a7781a..936a7609082 100644 --- a/packages/headless/src/app/ssr-engine/types/fetch-static-state.ts +++ b/packages/headless/src/app/ssr-engine/types/fetch-static-state.ts @@ -38,3 +38,31 @@ export type FetchStaticState< EngineStaticState >; }; + +export type FetchStaticStateWithList< + TEngine extends CoreEngine | CoreEngineNext, + TControllers extends ControllersMap, + TSearchAction extends UnknownAction, + TControllersStaticState extends ControllerStaticStateMap, + TControllersProps extends ControllersPropsMap, +> = { + /** + * Executes only the initial search for a given configuration, then returns a resumable snapshot of engine state along with the state of the controllers. + * + * Useful for static generation and server-side rendering. + */ + ( + c: (keyof TControllers)[], + ...params: OptionsTuple< + FetchStaticStateOptions & + EngineDefinitionControllersPropsOption + > + ): Promise>; + + fromBuildResult: FromBuildResult< + TEngine, + TControllers, + FetchStaticStateOptions, + EngineStaticState + >; +}; diff --git a/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx b/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx index 5e9fe7b1278..6249acc537e 100644 --- a/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx +++ b/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx @@ -6,7 +6,6 @@ import FacetGenerator from '@/components/facets/facet-generator'; import Pagination from '@/components/pagination'; import ProductList from '@/components/product-list'; import ListingProvider from '@/components/providers/listing-provider'; -import Recommendations from '@/components/recommendation-list'; import Sort from '@/components/sort'; import StandaloneSearchBox from '@/components/standalone-search-box'; import Summary from '@/components/summary'; @@ -54,7 +53,9 @@ export default async function Listing({params}: {params: {category: string}}) { }, }); - // const recStaticState = await recsDefinition.fetchStaticState() + staticState.searchAction; + + // const recsStaticState = await recsDefinition.fetchStaticState('recs1', 'recs2'); return ( {/* - */} + + */} diff --git a/packages/samples/headless-ssr-commerce/components/pages/product-page.tsx b/packages/samples/headless-ssr-commerce/components/pages/product-page.tsx index 5f69987bdfe..2d8c77b6902 100644 --- a/packages/samples/headless-ssr-commerce/components/pages/product-page.tsx +++ b/packages/samples/headless-ssr-commerce/components/pages/product-page.tsx @@ -49,7 +49,7 @@ export default function ProductPage(props: IProductPageProps) { // Refreshing recommendations in the browser after hydrating the state in the client-side // Recommendation refresh in the server is not supported yet. - controllers.popularBoughtRecs.refresh(); + // controllers.popularBoughtRecs.refresh(); }); }, [staticState]); From 82e8cb1145f6879c63ef77effbc5316d4fec06e6 Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Thu, 14 Nov 2024 13:04:53 -0500 Subject: [PATCH 08/13] more drafting --- .../commerce-engine/commerce-engine.ssr.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts index 75c07c04caf..d8d55bf05ce 100644 --- a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts +++ b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts @@ -6,6 +6,12 @@ import {stateKey} from '../../app/state-key.js'; import {buildProductListing} from '../../controllers/commerce/product-listing/headless-product-listing.js'; import {buildSearch} from '../../controllers/commerce/search/headless-search.js'; import type {Controller} from '../../controllers/controller/headless-controller.js'; +import { + defineFacetGenerator, + defineRecommendations, + defineStandaloneSearchBox, + getSampleCommerceEngineConfiguration, +} from '../../ssr-commerce.index.js'; import { createWaitForActionMiddleware, createWaitForActionMiddlewareForRecommendation, @@ -356,3 +362,33 @@ export function defineCommerceEngine< >, }; } +/// Sandbox +// const { +// recommendationEngineDefinition, +// searchEngineDefinition, +// standaloneEngineDefinition, +// } = defineCommerceEngine({ +// configuration: getSampleCommerceEngineConfiguration(), +// controllers: { +// standaloneSearchBox: defineStandaloneSearchBox({ +// options: {redirectionUrl: 'rest'}, +// }), +// facets: defineFacetGenerator(), +// trending: defineRecommendations({ +// options: {slotId: 'ttt'}, +// }), +// popular: defineRecommendations({ +// options: {slotId: 'ppp'}, +// }), +// }, +// }); + +// // TODO: should have a way to select which recommendation to fetch +// const r = await standaloneEngineDefinition.fetchStaticState(); +// r.controllers.standaloneSearchBox; + +// const b = await recommendationEngineDefinition.fetchStaticState(['trending']); +// b.controllers.trending; + +// const a = await searchEngineDefinition.fetchStaticState(); +// a.controllers; // TODO: should throw an error since it's not defined in search From f2184e67a98cf93e38dcd62353aae05ccfcc2f8a Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Thu, 14 Nov 2024 16:07:14 -0500 Subject: [PATCH 09/13] only 2 errors --- .../commerce-engine/commerce-engine.ssr.ts | 190 ++++++++++++++---- .../commerce-ssr-engine/types/core-engine.ts | 108 ++++++---- .../src/app/ssr-engine/types/build.ts | 3 + 3 files changed, 228 insertions(+), 73 deletions(-) diff --git a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts index d8d55bf05ce..503207a4b29 100644 --- a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts +++ b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts @@ -28,6 +28,7 @@ import { import { EngineDefinition, EngineDefinitionOptions, + RecommendationEngineDefinition, } from '../commerce-ssr-engine/types/core-engine.js'; import {buildLogger} from '../logger.js'; import {NavigatorContextProvider} from '../navigatorContextProvider.js'; @@ -137,6 +138,14 @@ function buildSSRCommerceEngine( }; } +interface RecommendationCommerceEngineDefinition< + TControllers extends ControllerDefinitionsMap, +> extends RecommendationEngineDefinition< + SSRCommerceEngine, + TControllers, + CommerceEngineOptions + > {} + export interface CommerceEngineDefinition< TControllers extends ControllerDefinitionsMap, TSolutionType extends SolutionType, @@ -173,10 +182,7 @@ export function defineCommerceEngine< TControllerDefinitions, SolutionType.standalone >; - recommendationEngineDefinition: CommerceEngineDefinition< - TControllerDefinitions, - SolutionType.recommendation - >; + recommendationEngineDefinition: RecommendationCommerceEngineDefinition; } { const {controllers: controllerDefinitions, ...engineOptions} = options; type Definition = CommerceEngineDefinition< @@ -199,6 +205,22 @@ export function defineCommerceEngine< type HydrateStaticStateFromBuildResultParameters = Parameters; + type RecommendationDefinition = + RecommendationCommerceEngineDefinition; + type RecommendationBuildFunction = RecommendationDefinition['build']; + type RecommendationFetchStaticStateFunction = + RecommendationDefinition['fetchStaticState']; + type RecommendationHydrateStaticStateFunction = + RecommendationDefinition['hydrateStaticState']; + type RecommendationFetchStaticStateFromBuildResultFunction = + RecommendationFetchStaticStateFunction['fromBuildResult']; + type RecommendationHydrateStaticStateFromBuildResultFunction = + RecommendationHydrateStaticStateFunction['fromBuildResult']; + type RecommendationBuildParameters = Parameters; + type RecommendationFetchStaticStateParameters = + Parameters; + type RecommendationFetchStaticFromBuildResultsParameters = + Parameters; const recommendationFilter = buildRecommendationFilter( controllerDefinitions ?? {} ); @@ -222,6 +244,11 @@ export function defineCommerceEngine< '[WARNING] Missing navigator context in server-side code. Make sure to set it with `setNavigatorContextProvider` before calling fetchStaticState()' ); } + + // These are the recs.. + // But why do I even need it in the build factory ????? + // logger.warn(buildOptions?.c); + const engine = buildSSRCommerceEngine( solutionType, buildOptions?.extend @@ -249,6 +276,7 @@ export function defineCommerceEngine< ) => FetchStaticStateFunction = (solutionType: SolutionType) => composeFunction( async (...params: FetchStaticStateParameters) => { + // I can't do it all, I need to split them all const buildResult = await buildFactory(solutionType)(...params); const staticState = await fetchStaticStateFactory( solutionType @@ -327,6 +355,93 @@ export function defineCommerceEngine< }, } ); + + const recommendationBuildFactory = + () => + async (...[buildOptions]: RecommendationBuildParameters) => { + const logger = buildLogger(options.loggerOptions); + if (!getOptions().navigatorContextProvider) { + logger.warn( + '[WARNING] Missing navigator context in server-side code. Make sure to set it with `setNavigatorContextProvider` before calling fetchStaticState()' + ); + } + + // These are the recs.. + // But why do I even need it in the build factory ????? + // logger.warn(buildOptions?.c); + + const engine = buildSSRCommerceEngine( + SolutionType.recommendation, + buildOptions?.extend + ? await buildOptions.extend(getOptions()) + : getOptions(), + recommendationFilter.count + ); + const controllers = buildControllerDefinitions({ + definitionsMap: (controllerDefinitions ?? {}) as TControllerDefinitions, + engine, + solutionType: SolutionType.recommendation, + propsMap: (buildOptions && 'controllers' in buildOptions + ? buildOptions.controllers + : {}) as InferControllerPropsMapFromDefinitions, + }); + + return { + engine, + controllers, + }; + }; + + const recommendationFetchStaticStateFactory: () => RecommendationFetchStaticStateFunction = + (solutionType: SolutionType) => + composeFunction( + async (...params: RecommendationFetchStaticStateParameters) => { + const buildResult = await recommendationBuildFactory()(...params); + // I can't do it all, I need to split them all + //What the hell, this function calls itself ? + const staticState = + await recommendationFetchStaticStateFactory().fromBuildResult({ + buildResult, + }); + return staticState; + }, + { + fromBuildResult: async ( + ...params: RecommendationFetchStaticFromBuildResultsParameters + ) => { + const [ + { + buildResult: {engine, controllers}, + }, + ] = params; + + if (solutionType === SolutionType.listing) { + buildProductListing(engine).executeFirstRequest(); + } else if (solutionType === SolutionType.search) { + buildSearch(engine).executeFirstSearch(); + } else if (solutionType === SolutionType.recommendation) { + // here build the filter and refresh them all + // build every recommendation and refresh them all ? + // buildRecommendations(engine).refresh(); + recommendationFilter.refresh(controllers); + } + + const searchAction = await engine.waitForRequestCompletedAction(); + + return createStaticState({ + searchAction, + controllers, + }) as EngineStaticState< + UnknownAction, + InferControllerStaticStateMapFromDefinitionsWithSolutionType< + TControllerDefinitions, + SolutionType.recommendation + > + >; + }, + } + ); + return { listingEngineDefinition: { build: buildFactory(SolutionType.listing), @@ -350,45 +465,42 @@ export function defineCommerceEngine< SolutionType.standalone >, recommendationEngineDefinition: { - build: buildFactory(SolutionType.recommendation), - fetchStaticState: fetchStaticStateFactory(SolutionType.recommendation), + build: recommendationBuildFactory(), + fetchStaticState: recommendationFetchStaticStateFactory(), hydrateStaticState: hydrateStaticStateFactory( SolutionType.recommendation ), setNavigatorContextProvider, - } as CommerceEngineDefinition< - TControllerDefinitions, - SolutionType.recommendation - >, + } as RecommendationCommerceEngineDefinition, }; } /// Sandbox -// const { -// recommendationEngineDefinition, -// searchEngineDefinition, -// standaloneEngineDefinition, -// } = defineCommerceEngine({ -// configuration: getSampleCommerceEngineConfiguration(), -// controllers: { -// standaloneSearchBox: defineStandaloneSearchBox({ -// options: {redirectionUrl: 'rest'}, -// }), -// facets: defineFacetGenerator(), -// trending: defineRecommendations({ -// options: {slotId: 'ttt'}, -// }), -// popular: defineRecommendations({ -// options: {slotId: 'ppp'}, -// }), -// }, -// }); - -// // TODO: should have a way to select which recommendation to fetch -// const r = await standaloneEngineDefinition.fetchStaticState(); -// r.controllers.standaloneSearchBox; - -// const b = await recommendationEngineDefinition.fetchStaticState(['trending']); -// b.controllers.trending; - -// const a = await searchEngineDefinition.fetchStaticState(); -// a.controllers; // TODO: should throw an error since it's not defined in search +const { + recommendationEngineDefinition, + searchEngineDefinition, + standaloneEngineDefinition, +} = defineCommerceEngine({ + configuration: getSampleCommerceEngineConfiguration(), + controllers: { + standaloneSearchBox: defineStandaloneSearchBox({ + options: {redirectionUrl: 'rest'}, + }), + facets: defineFacetGenerator(), + trending: defineRecommendations({ + options: {slotId: 'ttt'}, + }), + popular: defineRecommendations({ + options: {slotId: 'ppp'}, + }), + }, +}); + +// TODO: should have a way to select which recommendation to fetch +const r = await standaloneEngineDefinition.fetchStaticState(); +r.controllers.standaloneSearchBox; + +const b = await recommendationEngineDefinition.fetchStaticState(['popular']); +b.controllers.trending; + +const a = await searchEngineDefinition.fetchStaticState(); +a.controllers; // TODO: should throw an error since it's not defined in search diff --git a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts index fc2af464a75..5e43667a39b 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts @@ -40,27 +40,16 @@ export interface EngineDefinition< /** * Fetches the static state on the server side using your engine definition. */ - fetchStaticState: TSolutionType extends SolutionType.recommendation - ? FetchStaticStateWithList< - TEngine, - InferControllersMapFromDefinition, - UnknownAction, - InferControllerStaticStateMapFromDefinitionsWithSolutionType< - TControllers, - TSolutionType - >, - InferControllerPropsMapFromDefinitions - > - : FetchStaticState< - TEngine, - InferControllersMapFromDefinition, - UnknownAction, - InferControllerStaticStateMapFromDefinitionsWithSolutionType< - TControllers, - TSolutionType - >, - InferControllerPropsMapFromDefinitions - >; + fetchStaticState: FetchStaticState< + TEngine, + InferControllersMapFromDefinition, + UnknownAction, + InferControllerStaticStateMapFromDefinitionsWithSolutionType< + TControllers, + TSolutionType + >, + InferControllerPropsMapFromDefinitions + >; /** * Fetches the hydrated state on the client side using your engine definition and the static state. */ @@ -73,19 +62,70 @@ export interface EngineDefinition< /** * Builds an engine and its controllers from an engine definition. */ - build: TSolutionType extends SolutionType.recommendation - ? BuildWithList< - TEngine, - TEngineOptions, - InferControllersMapFromDefinition, - InferControllerPropsMapFromDefinitions - > - : Build< - TEngine, - TEngineOptions, - InferControllersMapFromDefinition, - InferControllerPropsMapFromDefinitions - >; + build: Build< + TEngine, + TEngineOptions, + InferControllersMapFromDefinition, + InferControllerPropsMapFromDefinitions + >; + + /** + * Sets the navigator context provider. + * This provider is essential for retrieving navigation-related data such as referrer, userAgent, location, and clientId, which are crucial for handling both server-side and client-side API requests effectively. + * + * Note: The implementation specifics of the navigator context provider depend on the Node.js framework being utilized. It is the developer's responsibility to appropriately define and implement the navigator context provider to ensure accurate navigation context is available throughout the application. If the user fails to provide a navigator context provider, a warning will be logged either on the server or the browser console. + */ + setNavigatorContextProvider: ( + navigatorContextProvider: NavigatorContextProvider + ) => void; +} + +export interface RecommendationEngineDefinition< + TEngine extends CoreEngine | CoreEngineNext, + TControllers extends ControllerDefinitionsMap, + TEngineOptions, +> { + /** + * Fetches the static state on the server side using your engine definition. + */ + fetchStaticState: FetchStaticStateWithList< + TEngine, + InferControllersMapFromDefinition< + TControllers, + SolutionType.recommendation + >, + UnknownAction, + InferControllerStaticStateMapFromDefinitionsWithSolutionType< + TControllers, + SolutionType.recommendation + >, + InferControllerPropsMapFromDefinitions + >; + + /** + * Fetches the hydrated state on the client side using your engine definition and the static state. + */ + hydrateStaticState: HydrateStaticState< + TEngine, + InferControllersMapFromDefinition< + TControllers, + SolutionType.recommendation + >, + UnknownAction, + InferControllerPropsMapFromDefinitions + >; + /** + * Builds an engine and its controllers from an engine definition. + */ + build: BuildWithList< + TEngine, + TEngineOptions, + InferControllersMapFromDefinition< + TControllers, + SolutionType.recommendation + >, + InferControllerPropsMapFromDefinitions + >; /** * Sets the navigator context provider. diff --git a/packages/headless/src/app/ssr-engine/types/build.ts b/packages/headless/src/app/ssr-engine/types/build.ts index bb6c9eca00b..65d5072e17a 100644 --- a/packages/headless/src/app/ssr-engine/types/build.ts +++ b/packages/headless/src/app/ssr-engine/types/build.ts @@ -8,7 +8,10 @@ import { OptionsTuple, } from './common.js'; +// can I build them like so ? export interface BuildOptions { + // c: (keyof TControllersMap)[]; // list of controllers to build, but how did that work ?? + //Why is it optional ????? extend?: OptionsExtender; } From f9173a80f1a1fe250ea038ec4b42de431d910100 Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Thu, 14 Nov 2024 16:18:04 -0500 Subject: [PATCH 10/13] only 1 error --- .../commerce-engine/commerce-engine.ssr.ts | 190 +++++++++++++----- 1 file changed, 139 insertions(+), 51 deletions(-) diff --git a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts index 503207a4b29..a97a4d8bb8a 100644 --- a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts +++ b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts @@ -392,55 +392,143 @@ export function defineCommerceEngine< }; }; - const recommendationFetchStaticStateFactory: () => RecommendationFetchStaticStateFunction = - (solutionType: SolutionType) => - composeFunction( - async (...params: RecommendationFetchStaticStateParameters) => { - const buildResult = await recommendationBuildFactory()(...params); - // I can't do it all, I need to split them all - //What the hell, this function calls itself ? - const staticState = - await recommendationFetchStaticStateFactory().fromBuildResult({ - buildResult, - }); - return staticState; - }, - { - fromBuildResult: async ( - ...params: RecommendationFetchStaticFromBuildResultsParameters - ) => { - const [ - { - buildResult: {engine, controllers}, - }, - ] = params; - - if (solutionType === SolutionType.listing) { - buildProductListing(engine).executeFirstRequest(); - } else if (solutionType === SolutionType.search) { - buildSearch(engine).executeFirstSearch(); - } else if (solutionType === SolutionType.recommendation) { - // here build the filter and refresh them all - // build every recommendation and refresh them all ? - // buildRecommendations(engine).refresh(); - recommendationFilter.refresh(controllers); - } - - const searchAction = await engine.waitForRequestCompletedAction(); - - return createStaticState({ - searchAction, - controllers, - }) as EngineStaticState< - UnknownAction, - InferControllerStaticStateMapFromDefinitionsWithSolutionType< - TControllerDefinitions, - SolutionType.recommendation - > - >; - }, - } - ); + // const recommendationFetchStaticStateFactory: () => RecommendationFetchStaticStateFunction = + // () => + // composeFunction( + // async (...params: RecommendationFetchStaticStateParameters) => { + // const buildResult = await recommendationBuildFactory()(...params); + // // I can't do it all, I need to split them all + // //What the hell, this function calls itself ? + // const staticState = + // await recommendationFetchStaticStateFactory().fromBuildResult({ + // buildResult, + // }); + // return staticState; + // }, + // { + // fromBuildResult: async ( + // ...params: RecommendationFetchStaticFromBuildResultsParameters + // ) => { + // const [ + // { + // buildResult: {engine, controllers}, + // }, + // ] = params; + + // // here build the filter and refresh them all + // // build every recommendation and refresh them all ? + // // buildRecommendations(engine).refresh(); + // recommendationFilter.refresh(controllers); + + // const searchAction = await engine.waitForRequestCompletedAction(); + + // return createStaticState({ + // searchAction, + // controllers, + // }) as EngineStaticState< + // UnknownAction, + // InferControllerStaticStateMapFromDefinitionsWithSolutionType< + // TControllerDefinitions, + // SolutionType.recommendation + // > + // >; + // }, + // } + // ); + + const recommendationFetchStaticStateFactory3 = () => { + // Primary function logic + const fetchStaticState = async ( + ...params: RecommendationFetchStaticStateParameters + ) => { + const buildResult = await recommendationBuildFactory()(...params); + + const staticState = await fromBuildResult({ + buildResult, + }); + return staticState; + }; + + // Attach fromBuildResult as a property to match the expected type + fetchStaticState.fromBuildResult = fromBuildResult; + + return fetchStaticState; + }; + + // Define fromBuildResult separately + const fromBuildResult = async ( + ...params: RecommendationFetchStaticFromBuildResultsParameters + ) => { + const [ + { + buildResult: {engine, controllers}, + }, + ] = params; + + // Handle solutionType conditions and refresh logic + + recommendationFilter.refresh(controllers); + + const searchAction = await engine.waitForRequestCompletedAction(); + + return createStaticState({ + searchAction, + controllers, + }) as EngineStaticState< + UnknownAction, + InferControllerStaticStateMapFromDefinitionsWithSolutionType< + TControllerDefinitions, + SolutionType.recommendation + > + >; + }; + // const recommendationFetchStaticStateFactory2: ( + // solutionType: SolutionType + // ) => RecommendationFetchStaticStateFunction = (solutionType) => () => + // composeFunction( + // async (...params: RecommendationFetchStaticStateParameters) => { + // const buildResult = await recommendationBuildFactory()(...params); + + // const staticState = await recommendationFetchStaticStateFactory( + // solutionType + // ).fromBuildResult({ + // buildResult, + // }); + // return staticState; + // }, + // { + // fromBuildResult: async ( + // ...params: RecommendationFetchStaticFromBuildResultsParameters + // ) => { + // const [ + // { + // buildResult: {engine, controllers}, + // }, + // ] = params; + + // if (solutionType === SolutionType.listing) { + // buildProductListing(engine).executeFirstRequest(); + // } else if (solutionType === SolutionType.search) { + // buildSearch(engine).executeFirstSearch(); + // } else if (solutionType === SolutionType.recommendation) { + // recommendationFilter.refresh(controllers); + // } + + // const searchAction = await engine.waitForRequestCompletedAction(); + + // return createStaticState({ + // searchAction, + // controllers, + // }) as EngineStaticState< + // UnknownAction, + // InferControllerStaticStateMapFromDefinitionsWithSolutionType< + // TControllerDefinitions, + // SolutionType.recommendation + // > + // >; + // }, + // } + // ); return { listingEngineDefinition: { @@ -466,7 +554,7 @@ export function defineCommerceEngine< >, recommendationEngineDefinition: { build: recommendationBuildFactory(), - fetchStaticState: recommendationFetchStaticStateFactory(), + fetchStaticState: recommendationFetchStaticStateFactory3(), hydrateStaticState: hydrateStaticStateFactory( SolutionType.recommendation ), @@ -499,7 +587,7 @@ const { const r = await standaloneEngineDefinition.fetchStaticState(); r.controllers.standaloneSearchBox; -const b = await recommendationEngineDefinition.fetchStaticState(['popular']); +const b = await recommendationEngineDefinition.fetchStaticState(['trending']); b.controllers.trending; const a = await searchEngineDefinition.fetchStaticState(); From f82b8a3e12c5792412691db0d6267fbbd7f96116 Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Fri, 15 Nov 2024 08:54:01 -0500 Subject: [PATCH 11/13] builds :smile: --- .../src/ssr-commerce/commerce-engine.tsx | 1 - .../commerce-engine/commerce-engine.ssr.ts | 98 ++++++++++++------- .../src/app/ssr-engine/types/build.ts | 6 +- 3 files changed, 66 insertions(+), 39 deletions(-) diff --git a/packages/headless-react/src/ssr-commerce/commerce-engine.tsx b/packages/headless-react/src/ssr-commerce/commerce-engine.tsx index 26f16b386d4..76a0f755837 100644 --- a/packages/headless-react/src/ssr-commerce/commerce-engine.tsx +++ b/packages/headless-react/src/ssr-commerce/commerce-engine.tsx @@ -65,7 +65,6 @@ export function defineCommerceEngine< const { listingEngineDefinition, searchEngineDefinition, - recommendationEngineDefinition, standaloneEngineDefinition, } = defineBaseCommerceEngine({...options}); return { diff --git a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts index a97a4d8bb8a..e50903b6350 100644 --- a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts +++ b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts @@ -6,12 +6,12 @@ import {stateKey} from '../../app/state-key.js'; import {buildProductListing} from '../../controllers/commerce/product-listing/headless-product-listing.js'; import {buildSearch} from '../../controllers/commerce/search/headless-search.js'; import type {Controller} from '../../controllers/controller/headless-controller.js'; -import { - defineFacetGenerator, - defineRecommendations, - defineStandaloneSearchBox, - getSampleCommerceEngineConfiguration, -} from '../../ssr-commerce.index.js'; +// import { +// defineFacetGenerator, +// defineRecommendations, +// defineStandaloneSearchBox, +// getSampleCommerceEngineConfiguration, +// } from '../../ssr-commerce.index.js'; import { createWaitForActionMiddleware, createWaitForActionMiddlewareForRecommendation, @@ -34,6 +34,7 @@ import {buildLogger} from '../logger.js'; import {NavigatorContextProvider} from '../navigatorContextProvider.js'; import {composeFunction} from '../ssr-engine/common.js'; import {createStaticState} from '../ssr-engine/common.js'; +import {BuildOptions} from '../ssr-engine/types/build.js'; import { EngineStaticState, InferControllerPropsMapFromDefinitions, @@ -210,12 +211,12 @@ export function defineCommerceEngine< type RecommendationBuildFunction = RecommendationDefinition['build']; type RecommendationFetchStaticStateFunction = RecommendationDefinition['fetchStaticState']; - type RecommendationHydrateStaticStateFunction = - RecommendationDefinition['hydrateStaticState']; + // type RecommendationHydrateStaticStateFunction = + // RecommendationDefinition['hydrateStaticState']; type RecommendationFetchStaticStateFromBuildResultFunction = RecommendationFetchStaticStateFunction['fromBuildResult']; - type RecommendationHydrateStaticStateFromBuildResultFunction = - RecommendationHydrateStaticStateFunction['fromBuildResult']; + // type RecommendationHydrateStaticStateFromBuildResultFunction = + // RecommendationHydrateStaticStateFunction['fromBuildResult']; type RecommendationBuildParameters = Parameters; type RecommendationFetchStaticStateParameters = Parameters; @@ -370,10 +371,14 @@ export function defineCommerceEngine< // But why do I even need it in the build factory ????? // logger.warn(buildOptions?.c); + // This cast is no good. Or is it ? + const buildOptionsCasted = + buildOptions as BuildOptions; + const engine = buildSSRCommerceEngine( SolutionType.recommendation, - buildOptions?.extend - ? await buildOptions.extend(getOptions()) + buildOptionsCasted?.extend + ? await buildOptionsCasted.extend(getOptions()) : getOptions(), recommendationFilter.count ); @@ -563,32 +568,51 @@ export function defineCommerceEngine< }; } /// Sandbox -const { - recommendationEngineDefinition, - searchEngineDefinition, - standaloneEngineDefinition, -} = defineCommerceEngine({ - configuration: getSampleCommerceEngineConfiguration(), - controllers: { - standaloneSearchBox: defineStandaloneSearchBox({ - options: {redirectionUrl: 'rest'}, - }), - facets: defineFacetGenerator(), - trending: defineRecommendations({ - options: {slotId: 'ttt'}, - }), - popular: defineRecommendations({ - options: {slotId: 'ppp'}, - }), - }, -}); +// const { +// recommendationEngineDefinition, +// searchEngineDefinition, +// standaloneEngineDefinition, +// } = defineCommerceEngine({ +// configuration: getSampleCommerceEngineConfiguration(), +// controllers: { +// standaloneSearchBox: defineStandaloneSearchBox({ +// options: {redirectionUrl: 'rest'}, +// }), +// facets: defineFacetGenerator(), +// trending: defineRecommendations({ +// options: {slotId: 'ttt'}, +// }), +// popular: defineRecommendations({ +// options: {slotId: 'ppp'}, +// }), +// }, +// }); // TODO: should have a way to select which recommendation to fetch -const r = await standaloneEngineDefinition.fetchStaticState(); -r.controllers.standaloneSearchBox; +// const r = await standaloneEngineDefinition.fetchStaticState(); +// r.controllers.standaloneSearchBox; + +// IMPORTANT : instead of doing all that, maybe we could have this functional style thingy. +// const b_1 = await recommendationEngineDefinition.fetchStaticState()([ +// 'popular', +// 'trending', +// ]); + +// const b = await recommendationEngineDefinition.fetchStaticState([ +// 'popular', +// 'trending', +// ]); + +// //This should not be allowed +// const c = await recommendationEngineDefinition.fetchStaticState([ +// 'trending', +// 'trending', +// ]); + +// b.controllers.trending; -const b = await recommendationEngineDefinition.fetchStaticState(['trending']); -b.controllers.trending; +// //This should not be allowed +// c.controllers.popular; -const a = await searchEngineDefinition.fetchStaticState(); -a.controllers; // TODO: should throw an error since it's not defined in search +// const a = await searchEngineDefinition.fetchStaticState(); +// a.controllers; // TODO: should throw an error since it's not defined in search diff --git a/packages/headless/src/app/ssr-engine/types/build.ts b/packages/headless/src/app/ssr-engine/types/build.ts index 65d5072e17a..b51a854c5f4 100644 --- a/packages/headless/src/app/ssr-engine/types/build.ts +++ b/packages/headless/src/app/ssr-engine/types/build.ts @@ -9,12 +9,16 @@ import { } from './common.js'; // can I build them like so ? -export interface BuildOptions { +export interface ListBuildOptions { // c: (keyof TControllersMap)[]; // list of controllers to build, but how did that work ?? //Why is it optional ????? extend?: OptionsExtender; } +export interface BuildOptions { + extend?: OptionsExtender; +} + export interface BuildWithList< TEngine extends CoreEngine | CoreEngineNext, TEngineOptions, From f0ac743f4e8e0afe595ad9348e626efeeaa25db5 Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Fri, 15 Nov 2024 11:38:18 -0500 Subject: [PATCH 12/13] clean up and add to headless-react --- .../src/ssr-commerce/commerce-engine.tsx | 11 ++ .../commerce-engine/commerce-engine.ssr.ts | 105 ++---------------- .../commerce-ssr-engine/types/core-engine.ts | 2 +- .../ssr-engine/types/fetch-static-state.ts | 4 +- .../app/ssr-engine/types/from-build-result.ts | 12 ++ packages/headless/src/ssr-commerce.index.ts | 3 +- 6 files changed, 35 insertions(+), 102 deletions(-) diff --git a/packages/headless-react/src/ssr-commerce/commerce-engine.tsx b/packages/headless-react/src/ssr-commerce/commerce-engine.tsx index 76a0f755837..8807fa2c773 100644 --- a/packages/headless-react/src/ssr-commerce/commerce-engine.tsx +++ b/packages/headless-react/src/ssr-commerce/commerce-engine.tsx @@ -60,11 +60,13 @@ export function defineCommerceEngine< >; type ListingContext = ContextStateType; type SearchContext = ContextStateType; + type RecommendationContext = ContextStateType; type StandaloneContext = ContextStateType; const { listingEngineDefinition, searchEngineDefinition, + recommendationEngineDefinition, standaloneEngineDefinition, } = defineBaseCommerceEngine({...options}); return { @@ -89,6 +91,15 @@ export function defineCommerceEngine< singletonContext as SearchContext ), }, + recommendationEngineDefinition: { + ...recommendationEngineDefinition, + StaticStateProvider: buildStaticStateProvider( + singletonContext as RecommendationContext + ), + HydratedStateProvider: buildHydratedStateProvider( + singletonContext as RecommendationContext + ), + }, standaloneEngineDefinition: { ...standaloneEngineDefinition, StaticStateProvider: buildStaticStateProvider( diff --git a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts index e50903b6350..89c1a764493 100644 --- a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts +++ b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts @@ -397,80 +397,36 @@ export function defineCommerceEngine< }; }; - // const recommendationFetchStaticStateFactory: () => RecommendationFetchStaticStateFunction = - // () => - // composeFunction( - // async (...params: RecommendationFetchStaticStateParameters) => { - // const buildResult = await recommendationBuildFactory()(...params); - // // I can't do it all, I need to split them all - // //What the hell, this function calls itself ? - // const staticState = - // await recommendationFetchStaticStateFactory().fromBuildResult({ - // buildResult, - // }); - // return staticState; - // }, - // { - // fromBuildResult: async ( - // ...params: RecommendationFetchStaticFromBuildResultsParameters - // ) => { - // const [ - // { - // buildResult: {engine, controllers}, - // }, - // ] = params; - - // // here build the filter and refresh them all - // // build every recommendation and refresh them all ? - // // buildRecommendations(engine).refresh(); - // recommendationFilter.refresh(controllers); - - // const searchAction = await engine.waitForRequestCompletedAction(); - - // return createStaticState({ - // searchAction, - // controllers, - // }) as EngineStaticState< - // UnknownAction, - // InferControllerStaticStateMapFromDefinitionsWithSolutionType< - // TControllerDefinitions, - // SolutionType.recommendation - // > - // >; - // }, - // } - // ); - - const recommendationFetchStaticStateFactory3 = () => { - // Primary function logic + const recommendationFetchStaticStateFactory = () => { const fetchStaticState = async ( ...params: RecommendationFetchStaticStateParameters ) => { const buildResult = await recommendationBuildFactory()(...params); - const staticState = await fromBuildResult({ + const [c] = params; + + const staticState = await fromBuildResult(c, { buildResult, }); return staticState; }; - // Attach fromBuildResult as a property to match the expected type fetchStaticState.fromBuildResult = fromBuildResult; return fetchStaticState; }; - // Define fromBuildResult separately const fromBuildResult = async ( ...params: RecommendationFetchStaticFromBuildResultsParameters ) => { const [ + c, { buildResult: {engine, controllers}, }, ] = params; - // Handle solutionType conditions and refresh logic + console.log(c); recommendationFilter.refresh(controllers); @@ -487,53 +443,6 @@ export function defineCommerceEngine< > >; }; - // const recommendationFetchStaticStateFactory2: ( - // solutionType: SolutionType - // ) => RecommendationFetchStaticStateFunction = (solutionType) => () => - // composeFunction( - // async (...params: RecommendationFetchStaticStateParameters) => { - // const buildResult = await recommendationBuildFactory()(...params); - - // const staticState = await recommendationFetchStaticStateFactory( - // solutionType - // ).fromBuildResult({ - // buildResult, - // }); - // return staticState; - // }, - // { - // fromBuildResult: async ( - // ...params: RecommendationFetchStaticFromBuildResultsParameters - // ) => { - // const [ - // { - // buildResult: {engine, controllers}, - // }, - // ] = params; - - // if (solutionType === SolutionType.listing) { - // buildProductListing(engine).executeFirstRequest(); - // } else if (solutionType === SolutionType.search) { - // buildSearch(engine).executeFirstSearch(); - // } else if (solutionType === SolutionType.recommendation) { - // recommendationFilter.refresh(controllers); - // } - - // const searchAction = await engine.waitForRequestCompletedAction(); - - // return createStaticState({ - // searchAction, - // controllers, - // }) as EngineStaticState< - // UnknownAction, - // InferControllerStaticStateMapFromDefinitionsWithSolutionType< - // TControllerDefinitions, - // SolutionType.recommendation - // > - // >; - // }, - // } - // ); return { listingEngineDefinition: { @@ -559,7 +468,7 @@ export function defineCommerceEngine< >, recommendationEngineDefinition: { build: recommendationBuildFactory(), - fetchStaticState: recommendationFetchStaticStateFactory3(), + fetchStaticState: recommendationFetchStaticStateFactory(), hydrateStaticState: hydrateStaticStateFactory( SolutionType.recommendation ), diff --git a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts index 5e43667a39b..7e79213a355 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts @@ -17,7 +17,7 @@ import { InferControllerStaticStateMapFromDefinitionsWithSolutionType, } from './common.js'; -export type {HydrateStaticState, FetchStaticState}; +export type {HydrateStaticState, FetchStaticState, FetchStaticStateWithList}; export type EngineDefinitionOptions< TOptions extends {configuration: EngineConfiguration}, TControllers extends ControllerDefinitionsMap< diff --git a/packages/headless/src/app/ssr-engine/types/fetch-static-state.ts b/packages/headless/src/app/ssr-engine/types/fetch-static-state.ts index 936a7609082..45b8fedd8b1 100644 --- a/packages/headless/src/app/ssr-engine/types/fetch-static-state.ts +++ b/packages/headless/src/app/ssr-engine/types/fetch-static-state.ts @@ -8,7 +8,7 @@ import { EngineStaticState, OptionsTuple, } from './common.js'; -import {FromBuildResult} from './from-build-result.js'; +import {FromBuildResult, FromBuildResultWithList} from './from-build-result.js'; export type FetchStaticStateOptions = {}; @@ -59,7 +59,7 @@ export type FetchStaticStateWithList< > ): Promise>; - fromBuildResult: FromBuildResult< + fromBuildResult: FromBuildResultWithList< TEngine, TControllers, FetchStaticStateOptions, diff --git a/packages/headless/src/app/ssr-engine/types/from-build-result.ts b/packages/headless/src/app/ssr-engine/types/from-build-result.ts index 5851651b59b..a74ae82617a 100644 --- a/packages/headless/src/app/ssr-engine/types/from-build-result.ts +++ b/packages/headless/src/app/ssr-engine/types/from-build-result.ts @@ -18,3 +18,15 @@ export interface FromBuildResult< options: FromBuildResultOptions & TOptions ): Promise; } + +export interface FromBuildResultWithList< + TEngine extends CoreEngine | CoreEngineNext, + TControllers extends ControllersMap, + TOptions, + TReturn, +> { + ( + c: (keyof TControllers)[], + options: FromBuildResultOptions & TOptions + ): Promise; +} diff --git a/packages/headless/src/ssr-commerce.index.ts b/packages/headless/src/ssr-commerce.index.ts index 461befbd6f7..bb18cee938a 100644 --- a/packages/headless/src/ssr-commerce.index.ts +++ b/packages/headless/src/ssr-commerce.index.ts @@ -98,7 +98,7 @@ export type { InferControllerStaticStateMapFromDefinitionsWithSolutionType, InferControllerPropsMapFromDefinitions, } from './app/commerce-ssr-engine/types/common.js'; -export type {Build} from './app/ssr-engine/types/build.js'; +export type {Build, BuildWithList} from './app/ssr-engine/types/build.js'; export type { EngineDefinition, InferStaticState, @@ -106,6 +106,7 @@ export type { InferBuildResult, HydrateStaticState, FetchStaticState, + FetchStaticStateWithList, } from './app/commerce-ssr-engine/types/core-engine.js'; export type {LoggerOptions} from './app/logger.js'; export type { From fac83b075d1c955b8e221fedebbbc8bca8382af0 Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Fri, 15 Nov 2024 15:20:21 -0500 Subject: [PATCH 13/13] refreshing but no static state --- .../commerce-engine/commerce-engine.ssr.ts | 21 +++-- .../src/app/commerce-ssr-engine/common.ts | 27 +++++-- .../commerce-ssr-engine/types/core-engine.ts | 11 +-- .../src/app/ssr-engine/types/build.ts | 8 +- .../ssr-engine/types/fetch-static-state.ts | 7 +- .../app/(listing)/[category]/page.tsx | 30 +++++--- .../providers/recommendation-provider.tsx | 76 +++++++++++++++++++ .../components/recommendation-list.tsx | 1 + .../lib/commerce-engine.ts | 8 ++ 9 files changed, 143 insertions(+), 46 deletions(-) create mode 100644 packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx diff --git a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts index 89c1a764493..364dd8f935f 100644 --- a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts +++ b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts @@ -141,11 +141,7 @@ function buildSSRCommerceEngine( interface RecommendationCommerceEngineDefinition< TControllers extends ControllerDefinitionsMap, -> extends RecommendationEngineDefinition< - SSRCommerceEngine, - TControllers, - CommerceEngineOptions - > {} +> extends RecommendationEngineDefinition {} export interface CommerceEngineDefinition< TControllers extends ControllerDefinitionsMap, @@ -304,7 +300,7 @@ export function defineCommerceEngine< // here build the filter and refresh them all // build every recommendation and refresh them all ? // buildRecommendations(engine).refresh(); - recommendationFilter.refresh(controllers); + // recommendationFilter.refresh(controllers); } const searchAction = await engine.waitForRequestCompletedAction(); @@ -357,6 +353,7 @@ export function defineCommerceEngine< } ); + // The fetch static state problem might come from here ? const recommendationBuildFactory = () => async (...[buildOptions]: RecommendationBuildParameters) => { @@ -382,6 +379,9 @@ export function defineCommerceEngine< : getOptions(), recommendationFilter.count ); + + // const recommendationControllerDefinitions = controllerDefinitions?. + const controllers = buildControllerDefinitions({ definitionsMap: (controllerDefinitions ?? {}) as TControllerDefinitions, engine, @@ -426,12 +426,17 @@ export function defineCommerceEngine< }, ] = params; - console.log(c); + console.log('********'); + console.log(controllers); + console.log('********'); - recommendationFilter.refresh(controllers); + recommendationFilter.refresh(controllers, c); const searchAction = await engine.waitForRequestCompletedAction(); + console.log('AFTER REFRESH'); + console.log(controllers); + return createStaticState({ searchAction, controllers, diff --git a/packages/headless/src/app/commerce-ssr-engine/common.ts b/packages/headless/src/app/commerce-ssr-engine/common.ts index 6f2b68407cb..d19d013c70a 100644 --- a/packages/headless/src/app/commerce-ssr-engine/common.ts +++ b/packages/headless/src/app/commerce-ssr-engine/common.ts @@ -73,10 +73,24 @@ export function buildControllerDefinitions< ? definition['standalone'] === false : false; + // const unavailableInRecommendationSolutionType = + // solutionType === SolutionType['recommendation'] && + // 'recommendation' in definition + // ? definition['recommendation'] === false + // : false; + + const unavailableInRecommendationSolutionType = + (solutionType === SolutionType['recommendation'] && + !('recommendation' in definition)) || + ('recommendation' in definition && + definition['recommendation'] === false && + solutionType === SolutionType['recommendation']); + if ( unavailableInSearchSolutionType || unavailableInListingSolutionType || - unavailableInStandaloneSolutionType + unavailableInStandaloneSolutionType || + unavailableInRecommendationSolutionType ) { return null; } @@ -162,14 +176,17 @@ export function buildRecommendationFilter< * * @param controllers - A record of all controllers where the key is the controller name and the value is the controller instance. */ - refresh(controllers: Record) { + refresh(controllers: Record, whitelist: string[]) { + console.log(controllers); + console.log(whitelist); const isRecommendationController = (key: string) => name.includes(key); Object.entries(controllers) .filter(([key, _]) => isRecommendationController(key)) - .forEach(([_, controller]) => - (controller as Recommendations).refresh?.() - ); + .forEach(([key, controller]) => { + console.log(`Refreshing recommendation controller: ${key}`); + (controller as Recommendations).refresh?.(); + }); }, }; } diff --git a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts index 7e79213a355..de05a9c9b3c 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts @@ -83,7 +83,6 @@ export interface EngineDefinition< export interface RecommendationEngineDefinition< TEngine extends CoreEngine | CoreEngineNext, TControllers extends ControllerDefinitionsMap, - TEngineOptions, > { /** * Fetches the static state on the server side using your engine definition. @@ -98,8 +97,7 @@ export interface RecommendationEngineDefinition< InferControllerStaticStateMapFromDefinitionsWithSolutionType< TControllers, SolutionType.recommendation - >, - InferControllerPropsMapFromDefinitions + > >; /** @@ -119,12 +117,7 @@ export interface RecommendationEngineDefinition< */ build: BuildWithList< TEngine, - TEngineOptions, - InferControllersMapFromDefinition< - TControllers, - SolutionType.recommendation - >, - InferControllerPropsMapFromDefinitions + InferControllersMapFromDefinition >; /** diff --git a/packages/headless/src/app/ssr-engine/types/build.ts b/packages/headless/src/app/ssr-engine/types/build.ts index b51a854c5f4..eb60348effa 100644 --- a/packages/headless/src/app/ssr-engine/types/build.ts +++ b/packages/headless/src/app/ssr-engine/types/build.ts @@ -21,19 +21,13 @@ export interface BuildOptions { export interface BuildWithList< TEngine extends CoreEngine | CoreEngineNext, - TEngineOptions, TControllersMap extends ControllersMap, - TControllersProps extends ControllersPropsMap, > { /** * Initializes an engine and controllers from the definition. */ ( - c: (keyof TControllersMap)[], - ...params: OptionsTuple< - BuildOptions & - EngineDefinitionControllersPropsOption - > + c: (keyof TControllersMap)[] ): Promise>; } diff --git a/packages/headless/src/app/ssr-engine/types/fetch-static-state.ts b/packages/headless/src/app/ssr-engine/types/fetch-static-state.ts index 45b8fedd8b1..cf15159944f 100644 --- a/packages/headless/src/app/ssr-engine/types/fetch-static-state.ts +++ b/packages/headless/src/app/ssr-engine/types/fetch-static-state.ts @@ -44,7 +44,6 @@ export type FetchStaticStateWithList< TControllers extends ControllersMap, TSearchAction extends UnknownAction, TControllersStaticState extends ControllerStaticStateMap, - TControllersProps extends ControllersPropsMap, > = { /** * Executes only the initial search for a given configuration, then returns a resumable snapshot of engine state along with the state of the controllers. @@ -52,11 +51,7 @@ export type FetchStaticStateWithList< * Useful for static generation and server-side rendering. */ ( - c: (keyof TControllers)[], - ...params: OptionsTuple< - FetchStaticStateOptions & - EngineDefinitionControllersPropsOption - > + c: (keyof TControllers)[] ): Promise>; fromBuildResult: FromBuildResultWithList< diff --git a/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx b/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx index 6249acc537e..ac68480ca02 100644 --- a/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx +++ b/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx @@ -6,10 +6,15 @@ import FacetGenerator from '@/components/facets/facet-generator'; import Pagination from '@/components/pagination'; import ProductList from '@/components/product-list'; import ListingProvider from '@/components/providers/listing-provider'; +import RecommendationProvider from '@/components/providers/recommendation-provider'; +import Recommendations from '@/components/recommendation-list'; import Sort from '@/components/sort'; import StandaloneSearchBox from '@/components/standalone-search-box'; import Summary from '@/components/summary'; -import {listingEngineDefinition} from '@/lib/commerce-engine'; +import { + listingEngineDefinition, + recommendationEngineDefinition, +} from '@/lib/commerce-engine'; import {NextJsNavigatorContext} from '@/lib/navigatorContextProvider'; import {defaultContext} from '@/utils/context'; import {headers} from 'next/headers'; @@ -53,9 +58,13 @@ export default async function Listing({params}: {params: {category: string}}) { }, }); - staticState.searchAction; - - // const recsStaticState = await recsDefinition.fetchStaticState('recs1', 'recs2'); + const recsStaticState = await recommendationEngineDefinition.fetchStaticState( + ['popularBoughtRecs'] + ); + console.log( + recsStaticState.controllers.popularBoughtRecs.state.products, + 'RECS STATIC STATE ' + ); return (
- {/* - - - - */} + + +
diff --git a/packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx b/packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx new file mode 100644 index 00000000000..05628014dba --- /dev/null +++ b/packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx @@ -0,0 +1,76 @@ +'use client'; + +import { + recommendationEngineDefinition, + RecommendationHydratedState, + RecommendationStaticState, +} from '@/lib/commerce-engine'; +import {defaultContext} from '@/utils/context'; +import {NavigatorContext} from '@coveo/headless-react/ssr-commerce'; +import {PropsWithChildren, useEffect, useState} from 'react'; + +interface RecomendationProviderProps { + staticState: RecommendationStaticState; + navigatorContext: NavigatorContext; +} + +export default function RecommendationProvider({ + staticState, + navigatorContext, + children, +}: PropsWithChildren) { + const [hydratedState, setHydratedState] = useState< + RecommendationHydratedState | undefined + >(undefined); + + // Setting the navigator context provider also in client-side before hydrating the application + recommendationEngineDefinition.setNavigatorContextProvider( + () => navigatorContext + ); + + useEffect(() => { + recommendationEngineDefinition + .hydrateStaticState({ + searchAction: staticState.searchAction, + // This is all bad ? I don't want to put that here, ONLY RECS FOR RECS + controllers: { + cart: { + initialState: {items: []}, + }, + context: { + ...defaultContext, + view: { + url: 'https://sports.barca.group/browse/promotions', + }, + }, + }, + }) + .then(({engine, controllers}) => { + setHydratedState({engine, controllers}); + // Refreshing recommendations in the browser after hydrating the state in the client-side + // Recommendation refresh in the server is not supported yet. + // controllers.popularBoughtRecs.refresh(); // FIXME: does not work + }); + }, [staticState]); + + if (hydratedState) { + return ( + + <>{children} + + ); + } else { + return ( + + {/* // TODO: Add KIT-3701: Type 'React.ReactNode' is not assignable to type 'import(".../node_modules/@types/react/index").ReactNode'. + Type 'bigint' is not assignable to type 'ReactNode'.*/} + <>{children} + + ); + } +} diff --git a/packages/samples/headless-ssr-commerce/components/recommendation-list.tsx b/packages/samples/headless-ssr-commerce/components/recommendation-list.tsx index 5a62113537d..5b9ee7f4136 100644 --- a/packages/samples/headless-ssr-commerce/components/recommendation-list.tsx +++ b/packages/samples/headless-ssr-commerce/components/recommendation-list.tsx @@ -20,6 +20,7 @@ export default function Recommendations() { return ( <>
    + RECOMMENDATIONS {state.products.length}

    {state.headline}

    {state.products.map((product) => (
  • diff --git a/packages/samples/headless-ssr-commerce/lib/commerce-engine.ts b/packages/samples/headless-ssr-commerce/lib/commerce-engine.ts index 91ea7d56473..e56f759420e 100644 --- a/packages/samples/headless-ssr-commerce/lib/commerce-engine.ts +++ b/packages/samples/headless-ssr-commerce/lib/commerce-engine.ts @@ -10,6 +10,7 @@ export const engineDefinition = defineCommerceEngine(engineConfig); export const { listingEngineDefinition, searchEngineDefinition, + recommendationEngineDefinition, standaloneEngineDefinition, useEngine, } = engineDefinition; @@ -48,6 +49,13 @@ export type SearchHydratedState = InferHydratedState< typeof searchEngineDefinition >; +export type RecommendationStaticState = InferStaticState< + typeof recommendationEngineDefinition +>; +export type RecommendationHydratedState = InferHydratedState< + typeof recommendationEngineDefinition +>; + export type StandaloneStaticState = InferStaticState< typeof standaloneEngineDefinition >;