Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(headless): refresh commerce recommendations server-side #4617

Merged
merged 40 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
187fc0b
recs server-side working
y-lakhdar Oct 31, 2024
d571439
recognize search action promises
y-lakhdar Nov 1, 2024
4dc2717
add typeguard
y-lakhdar Nov 1, 2024
2e5f794
prevent multiple recommendations with the same slot
y-lakhdar Nov 1, 2024
de43049
clean
y-lakhdar Nov 3, 2024
9254b93
draft
y-lakhdar Nov 14, 2024
f2d1fa1
working draft
y-lakhdar Nov 15, 2024
bc63a92
test
y-lakhdar Nov 15, 2024
8d170d2
Merge branch 'master' of github.com:coveo/ui-kit into ssr-recs
y-lakhdar Nov 15, 2024
e60bfe5
update sample
y-lakhdar Nov 15, 2024
be888c8
recommendation hydration working
y-lakhdar Nov 15, 2024
c4ff2b9
clean build function
y-lakhdar Nov 15, 2024
2783740
clean exports
y-lakhdar Nov 15, 2024
8a6f9d8
filter invalid and duplicate recommendations
y-lakhdar Nov 16, 2024
025b34f
refacto
y-lakhdar Nov 16, 2024
07d1ec8
simplify build factory
y-lakhdar Nov 17, 2024
9b049fd
adjust factory params and throw error on bad engine definition
y-lakhdar Nov 17, 2024
84e5c53
do not ask props for disabled controllers
y-lakhdar Nov 17, 2024
018c0be
remove comments
y-lakhdar Nov 17, 2024
a7200d8
update warning message
y-lakhdar Nov 17, 2024
785951f
fix export
y-lakhdar Nov 17, 2024
f342da5
clean PR
y-lakhdar Nov 18, 2024
677fd45
clean controller build condition
y-lakhdar Nov 18, 2024
87d0792
create commerce build result type
y-lakhdar Nov 18, 2024
16c2b5e
remove unnecessary error
y-lakhdar Nov 18, 2024
dbffb51
revert EngineDefinitionControllersPropsOption
y-lakhdar Nov 18, 2024
961c28e
update UT
y-lakhdar Nov 18, 2024
c58b3ae
Add recs to sample
alexprudhomme Nov 18, 2024
81ba114
Merge branch 'master' into ssr-recs
alexprudhomme Nov 18, 2024
b8e6e16
no core engine
alexprudhomme Nov 18, 2024
3f93d51
add todo
alexprudhomme Nov 18, 2024
93b3967
Merge branch 'master' into ssr-recs
alexprudhomme Nov 22, 2024
a73f175
fix build
alexprudhomme Nov 22, 2024
feb0b81
docs(headless): navigator context annotations (#4712)
jpmarceau Nov 25, 2024
1959079
extract product-button-with-image
alexprudhomme Nov 26, 2024
41cb3aa
Update packages/headless/src/app/commerce-ssr-engine/factories/build-…
alexprudhomme Nov 26, 2024
0cbd535
simpler unavailableInSolutionType
alexprudhomme Nov 26, 2024
7674f10
Merge branch 'master' into ssr-recs
alexprudhomme Nov 26, 2024
9eeba4a
cleaner filter
alexprudhomme Nov 26, 2024
c34adf1
fix tests
alexprudhomme Nov 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe('Headless react SSR utils', () => {
listingEngineDefinition,
searchEngineDefinition,
standaloneEngineDefinition,
recommendationEngineDefinition,
...rest
} = defineCommerceEngine({configuration: sampleConfig});
const {
Expand Down
11 changes: 11 additions & 0 deletions packages/headless-react/src/ssr-commerce/commerce-engine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,14 @@ export function defineCommerceEngine<
>;
type ListingContext = ContextStateType<SolutionType.listing>;
type SearchContext = ContextStateType<SolutionType.search>;
type RecommendationContext = ContextStateType<SolutionType.recommendation>;
type StandaloneContext = ContextStateType<SolutionType.standalone>;

const {
listingEngineDefinition,
searchEngineDefinition,
standaloneEngineDefinition,
recommendationEngineDefinition,
} = defineBaseCommerceEngine({...options});
return {
useEngine: buildEngineHook(singletonContext),
Expand Down Expand Up @@ -84,5 +86,14 @@ export function defineCommerceEngine<
singletonContext as StandaloneContext
),
},
recommendationEngineDefinition: {
...recommendationEngineDefinition,
StaticStateProvider: buildStaticStateProvider(
singletonContext as RecommendationContext
),
HydratedStateProvider: buildHydratedStateProvider(
singletonContext as RecommendationContext
),
},
};
}
289 changes: 55 additions & 234 deletions packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts
Original file line number Diff line number Diff line change
@@ -1,111 +1,22 @@
/**
* Utility functions to be used for Commerce Server Side Rendering.
*/
import {Action, UnknownAction} from '@reduxjs/toolkit';
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 {buildControllerDefinitions} from '../commerce-ssr-engine/common.js';
import {
buildFactory,
CommerceEngineDefinitionOptions,
} from '../commerce-ssr-engine/factories/build-factory.js';
import {hydratedStaticStateFactory} from '../commerce-ssr-engine/factories/hydrated-state-factory.js';
import {hydratedRecommendationStaticStateFactory} from '../commerce-ssr-engine/factories/recommendation-hydrated-state-factory.js';
import {fetchRecommendationStaticStateFactory} from '../commerce-ssr-engine/factories/recommendation-static-state-factory.js';
import {fetchStaticStateFactory} from '../commerce-ssr-engine/factories/static-state-factory.js';
import {
ControllerDefinitionsMap,
InferControllerStaticStateMapFromDefinitionsWithSolutionType,
SolutionType,
} from '../commerce-ssr-engine/types/common.js';
import {
EngineDefinition,
EngineDefinitionOptions,
} from '../commerce-ssr-engine/types/core-engine.js';
import {buildLogger} from '../logger.js';
import {EngineDefinition} from '../commerce-ssr-engine/types/core-engine.js';
import {NavigatorContextProvider} from '../navigatorContextProvider.js';
import {composeFunction} from '../ssr-engine/common.js';
import {createStaticState} from '../ssr-engine/common.js';
import {
EngineStaticState,
InferControllerPropsMapFromDefinitions,
} from '../ssr-engine/types/common.js';
import {
CommerceEngine,
CommerceEngineOptions,
buildCommerceEngine,
} from './commerce-engine.js';

/**
* The SSR commerce engine.
*/
export interface SSRCommerceEngine extends CommerceEngine {
/**
* Waits for the search to be completed and returns a promise that resolves to a `SearchCompletedAction`.
*/
waitForRequestCompletedAction(): Promise<Action>;
}

export type CommerceEngineDefinitionOptions<
TControllers extends ControllerDefinitionsMap<Controller>,
> = EngineDefinitionOptions<CommerceEngineOptions, TControllers>;

function isListingFetchCompletedAction(action: unknown): action is Action {
return /^commerce\/productListing\/fetch\/(fulfilled|rejected)$/.test(
(action as UnknownAction).type
);
}

function isSearchCompletedAction(action: unknown): action is Action {
return /^commerce\/search\/executeSearch\/(fulfilled|rejected)$/.test(
(action as UnknownAction).type
);
}

function noSearchActionRequired(_action: unknown): _action is Action {
return true;
}

function buildSSRCommerceEngine(
solutionType: SolutionType,
options: CommerceEngineOptions
): SSRCommerceEngine {
let actionCompletionMiddleware: ReturnType<
typeof createWaitForActionMiddleware
>;

switch (solutionType) {
case SolutionType.listing:
actionCompletionMiddleware = createWaitForActionMiddleware(
isListingFetchCompletedAction
);
break;
case SolutionType.search:
actionCompletionMiddleware = createWaitForActionMiddleware(
isSearchCompletedAction
);
break;
default:
actionCompletionMiddleware = createWaitForActionMiddleware(
noSearchActionRequired
);
}

const commerceEngine = buildCommerceEngine({
...options,
middlewares: [
...(options.middlewares ?? []),
actionCompletionMiddleware.middleware,
],
});

return {
...commerceEngine,

get [stateKey]() {
return commerceEngine[stateKey];
},

waitForRequestCompletedAction() {
return actionCompletionMiddleware.promise;
},
};
}
import {CommerceEngineOptions} from './commerce-engine.js';

export interface CommerceEngineDefinition<
TControllers extends ControllerDefinitionsMap<Controller>,
Expand Down Expand Up @@ -139,161 +50,71 @@ export function defineCommerceEngine<
TControllerDefinitions,
SolutionType.standalone
>;
} {
const {controllers: controllerDefinitions, ...engineOptions} = options;
type Definition = CommerceEngineDefinition<
recommendationEngineDefinition: CommerceEngineDefinition<
TControllerDefinitions,
SolutionType
SolutionType.recommendation
>;
type BuildFunction = Definition['build'];
type FetchStaticStateFunction = Definition['fetchStaticState'];
type HydrateStaticStateFunction = Definition['hydrateStaticState'];
type FetchStaticStateFromBuildResultFunction =
FetchStaticStateFunction['fromBuildResult'];
type HydrateStaticStateFromBuildResultFunction =
HydrateStaticStateFunction['fromBuildResult'];
type BuildParameters = Parameters<BuildFunction>;
type FetchStaticStateParameters = Parameters<FetchStaticStateFunction>;
type HydrateStaticStateParameters = Parameters<HydrateStaticStateFunction>;
type FetchStaticStateFromBuildResultParameters =
Parameters<FetchStaticStateFromBuildResultFunction>;
type HydrateStaticStateFromBuildResultParameters =
Parameters<HydrateStaticStateFromBuildResultFunction>;
} {
const {controllers: controllerDefinitions, ...engineOptions} = options;

const getOptions = () => {
return engineOptions;
};
const getOptions = () => engineOptions;

const setNavigatorContextProvider = (
navigatorContextProvider: NavigatorContextProvider
) => {
engineOptions.navigatorContextProvider = navigatorContextProvider;
};

const buildFactory =
<T extends SolutionType>(solutionType: T) =>
async (...[buildOptions]: BuildParameters) => {
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()'
);
}
const engine = buildSSRCommerceEngine(
solutionType,
buildOptions?.extend
? await buildOptions.extend(getOptions())
: getOptions()
);
const controllers = buildControllerDefinitions({
definitionsMap: (controllerDefinitions ?? {}) as TControllerDefinitions,
engine,
solutionType,
propsMap: (buildOptions && 'controllers' in buildOptions
? buildOptions.controllers
: {}) as InferControllerPropsMapFromDefinitions<TControllerDefinitions>,
});

return {
engine,
controllers,
};
};

const fetchStaticStateFactory: (
solutionType: SolutionType
) => FetchStaticStateFunction = (solutionType: SolutionType) =>
composeFunction(
async (...params: FetchStaticStateParameters) => {
const buildResult = await buildFactory(solutionType)(...params);
const staticState = await fetchStaticStateFactory(
solutionType
).fromBuildResult({
buildResult,
});
return staticState;
},
{
fromBuildResult: async (
...params: FetchStaticStateFromBuildResultParameters
) => {
const [
{
buildResult: {engine, controllers},
},
] = params;

if (solutionType === SolutionType.listing) {
buildProductListing(engine).executeFirstRequest();
} else if (solutionType === SolutionType.search) {
buildSearch(engine).executeFirstSearch();
}

const searchAction = await engine.waitForRequestCompletedAction();

return createStaticState({
searchAction,
controllers,
}) as EngineStaticState<
UnknownAction,
InferControllerStaticStateMapFromDefinitionsWithSolutionType<
TControllerDefinitions,
SolutionType
>
>;
},
}
const build = buildFactory<TControllerDefinitions>(
controllerDefinitions,
getOptions()
);
const fetchStaticState = fetchStaticStateFactory<TControllerDefinitions>(
controllerDefinitions,
getOptions()
);
const hydrateStaticState = hydratedStaticStateFactory<TControllerDefinitions>(
controllerDefinitions,
getOptions()
);
const fetchRecommendationStaticState =
fetchRecommendationStaticStateFactory<TControllerDefinitions>(
controllerDefinitions,
getOptions()
);
alexprudhomme marked this conversation as resolved.
Show resolved Hide resolved

const hydrateStaticStateFactory: (
solutionType: SolutionType
) => HydrateStaticStateFunction = (solutionType: SolutionType) =>
composeFunction(
async (...params: HydrateStaticStateParameters) => {
const buildResult = await buildFactory(solutionType)(
...(params as BuildParameters)
);
const staticState = await hydrateStaticStateFactory(
solutionType
).fromBuildResult({
buildResult,
searchAction: params[0]!.searchAction,
});
return staticState;
},
{
fromBuildResult: async (
...params: HydrateStaticStateFromBuildResultParameters
) => {
const [
{
buildResult: {engine, controllers},
searchAction,
},
] = params;
engine.dispatch(searchAction);
await engine.waitForRequestCompletedAction();
return {engine, controllers};
},
}
const hydrateRecommendationStaticState =
hydratedRecommendationStaticStateFactory<TControllerDefinitions>(
controllerDefinitions,
getOptions()
);

return {
listingEngineDefinition: {
build: buildFactory(SolutionType.listing),
fetchStaticState: fetchStaticStateFactory(SolutionType.listing),
hydrateStaticState: hydrateStaticStateFactory(SolutionType.listing),
build: build(SolutionType.listing),
fetchStaticState: fetchStaticState(SolutionType.listing),
hydrateStaticState: hydrateStaticState(SolutionType.listing),
setNavigatorContextProvider,
} as CommerceEngineDefinition<TControllerDefinitions, SolutionType.listing>,
searchEngineDefinition: {
build: buildFactory(SolutionType.search),
fetchStaticState: fetchStaticStateFactory(SolutionType.search),
hydrateStaticState: hydrateStaticStateFactory(SolutionType.search),
build: build(SolutionType.search),
fetchStaticState: fetchStaticState(SolutionType.search),
hydrateStaticState: hydrateStaticState(SolutionType.search),
setNavigatorContextProvider,
} as CommerceEngineDefinition<TControllerDefinitions, SolutionType.search>,
recommendationEngineDefinition: {
build: build(SolutionType.recommendation),
fetchStaticState: fetchRecommendationStaticState,
hydrateStaticState: hydrateRecommendationStaticState,
setNavigatorContextProvider,
} as CommerceEngineDefinition<
TControllerDefinitions,
SolutionType.recommendation
>,
// TODO KIT-3738 : The standaloneEngineDefinition should not be async since no request is sent to the API
standaloneEngineDefinition: {
build: buildFactory(SolutionType.standalone),
fetchStaticState: fetchStaticStateFactory(SolutionType.standalone),
hydrateStaticState: hydrateStaticStateFactory(SolutionType.standalone),
build: build(SolutionType.standalone),
fetchStaticState: fetchStaticState(SolutionType.standalone),
hydrateStaticState: hydrateStaticState(SolutionType.standalone),
setNavigatorContextProvider,
} as CommerceEngineDefinition<
TControllerDefinitions,
Expand Down
Loading
Loading