Skip to content

Commit

Permalink
feat(headless): server side commerce recommendations
Browse files Browse the repository at this point in the history
  • Loading branch information
alexprudhomme committed Nov 13, 2024
1 parent 710b22e commit c4b69b0
Show file tree
Hide file tree
Showing 12 changed files with 192 additions and 130 deletions.
44 changes: 27 additions & 17 deletions packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Action>[];
waitForRequestCompletedAction(): Promise<Action>;
}

export type CommerceEngineDefinitionOptions<
Expand Down Expand Up @@ -126,10 +126,7 @@ function buildSSRCommerceEngine(
},

waitForRequestCompletedAction() {
return [
actionCompletionMiddleware.promise,
...recommendationActionMiddlewares.map(({promise}) => promise),
];
return actionCompletionMiddleware.promise;
},
};
}
Expand Down Expand Up @@ -170,6 +167,10 @@ export function defineCommerceEngine<
TControllerDefinitions,
SolutionType.standalone
>;
recommendationDefinition: CommerceEngineDefinition<
TControllerDefinitions,
SolutionType.recommendation
>;
} {
const {controllers: controllerDefinitions, ...engineOptions} = options;
type Definition = CommerceEngineDefinition<
Expand Down Expand Up @@ -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,
Expand All @@ -297,7 +299,7 @@ export function defineCommerceEngine<
solutionType
).fromBuildResult({
buildResult,
searchActions: params[0]!.searchActions,
searchAction: params[0]!.searchAction,
});
return staticState;
},
Expand All @@ -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};
},
}
Expand All @@ -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
>,
};
}
2 changes: 2 additions & 0 deletions packages/headless/src/app/commerce-ssr-engine/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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;
}
Expand Down
13 changes: 13 additions & 0 deletions packages/headless/src/app/commerce-ssr-engine/types/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export enum SolutionType {
search = 'search',
listing = 'listing',
standalone = 'standalone',
recommendation = 'recommendation',
}

export interface ControllerDefinitionWithoutProps<
Expand Down Expand Up @@ -172,6 +173,13 @@ interface ListingOnlyController {
[SolutionType.listing]: true;
}

interface RecommendationOnlyController {
/**
* @internal
*/
[SolutionType.recommendation]: true;
}

interface SearchAndListingController {
/**
* @internal
Expand All @@ -183,6 +191,11 @@ interface SearchAndListingController {
[SolutionType.listing]: true;
}

export type RecommendationOnlyControllerDefinitionWithoutProps<
TController extends Controller,
> = ControllerDefinitionWithoutProps<CommerceEngine, TController> &
RecommendationOnlyController;

export type SearchOnlyControllerDefinitionWithoutProps<
TController extends Controller,
> = ControllerDefinitionWithoutProps<CommerceEngine, TController> &
Expand Down
57 changes: 9 additions & 48 deletions packages/headless/src/app/search-engine/search-engine.ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<SearchCompletedAction>[];
waitForSearchCompletedAction(): Promise<SearchCompletedAction>;
}

/**
Expand All @@ -62,45 +61,21 @@ 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,
get state() {
return searchEngine.state;
},
waitForSearchCompletedAction() {
return [
promise,
...recommendationActionMiddlewares.map(({promise}) => promise),
];
return promise;
},
};
}
Expand Down Expand Up @@ -147,10 +122,6 @@ export function defineSearchEngine<
type HydrateStaticStateFromBuildResultParameters =
Parameters<HydrateStaticStateFromBuildResultFunction>;

const recommendationHelper = buildRecommendationFilter(
controllerDefinitions ?? {}
);

const getOptions = () => {
return engineOptions;
};
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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;
},
Expand All @@ -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};
},
Expand Down
6 changes: 3 additions & 3 deletions packages/headless/src/app/ssr-engine/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ export function buildControllerDefinitions<
}

export function createStaticState<TSearchAction extends UnknownAction>({
searchActions,
searchAction,
controllers,
}: {
searchActions: TSearchAction[];
searchAction: TSearchAction;
controllers: ControllersMap;
}): EngineStaticState<
TSearchAction,
Expand All @@ -71,7 +71,7 @@ export function createStaticState<TSearchAction extends UnknownAction>({
controllers: mapObject(controllers, (controller) => ({
state: clone(controller.state),
})) as InferControllerStaticStateMapFromControllers<ControllersMap>,
searchActions: searchActions.map((action) => clone(action)),
searchAction: clone(searchAction),
};
}

Expand Down
2 changes: 1 addition & 1 deletion packages/headless/src/app/ssr-engine/types/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export interface EngineStaticState<
TSearchAction extends UnknownAction,
TControllers extends ControllerStaticStateMap,
> {
searchActions: TSearchAction[];
searchAction: TSearchAction;
controllers: TControllers;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import {FromBuildResult} from './from-build-result.js';

export interface HydrateStaticStateOptions<TSearchAction> {
searchActions: TSearchAction[];
searchAction: TSearchAction;
}

export type HydrateStaticState<
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -16,7 +16,7 @@ export type RecommendationsDefinitionMeta = {
};

export interface RecommendationsDefinition
extends UniversalControllerDefinitionWithoutProps<Recommendations> {}
extends RecommendationOnlyControllerDefinitionWithoutProps<Recommendations> {}
/**
* @internal
* Defines a `Recommendations` controller instance.
Expand All @@ -29,9 +29,7 @@ export function defineRecommendations(
props: RecommendationsProps
): RecommendationsDefinition & RecommendationsDefinitionMeta {
return {
search: true,
listing: true,
standalone: true,
recommendation: true,
_recommendationProps: {
...props.options,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export default async function Listing({params}: {params: {category: string}}) {
},
});

// const recStaticState = await recsDefinition.fetchStaticState()

return (
<ListingProvider
staticState={staticState}
Expand Down Expand Up @@ -86,6 +88,11 @@ export default async function Listing({params}: {params: {category: string}}) {
</div>

<div style={{flex: 4}}>
{/* <RecommendationProvider
staticState={recStaticState}
navigatorContext={navigatorContext.marshal}>
</RecommendationProvider> */}
<Recommendations />
</div>
</div>
Expand Down
Loading

0 comments on commit c4b69b0

Please sign in to comment.