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): server side commerce recommendations #4673

Closed
wants to merge 14 commits into from
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 @@ -60,11 +60,13 @@ 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,
recommendationEngineDefinition,
standaloneEngineDefinition,
} = defineBaseCommerceEngine({...options});
return {
Expand All @@ -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(
Expand Down
235 changes: 230 additions & 5 deletions packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,20 @@ 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 {
// defineFacetGenerator,
// defineRecommendations,
// defineStandaloneSearchBox,
// getSampleCommerceEngineConfiguration,
// } from '../../ssr-commerce.index.js';
import {
createWaitForActionMiddleware,
createWaitForActionMiddlewareForRecommendation,
} from '../../utils/utils.js';
import {
buildControllerDefinitions,
buildRecommendationFilter,
} from '../commerce-ssr-engine/common.js';
import {
ControllerDefinitionsMap,
InferControllerStaticStateMapFromDefinitionsWithSolutionType,
Expand All @@ -16,11 +28,13 @@ import {
import {
EngineDefinition,
EngineDefinitionOptions,
RecommendationEngineDefinition,
} from '../commerce-ssr-engine/types/core-engine.js';
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,
Expand Down Expand Up @@ -57,13 +71,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
Expand All @@ -86,11 +107,22 @@ function buildSSRCommerceEngine(
);
}

const memo: Set<string> = new Set();
const recommendationActionMiddlewares = Array.from(
{length: recommendationCount},
() =>
createWaitForActionMiddlewareForRecommendation(
isRecommendationCompletedAction,
memo
)
);

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

Expand All @@ -107,6 +139,10 @@ function buildSSRCommerceEngine(
};
}

interface RecommendationCommerceEngineDefinition<
TControllers extends ControllerDefinitionsMap<SSRCommerceEngine, Controller>,
> extends RecommendationEngineDefinition<SSRCommerceEngine, TControllers> {}

export interface CommerceEngineDefinition<
TControllers extends ControllerDefinitionsMap<SSRCommerceEngine, Controller>,
TSolutionType extends SolutionType,
Expand Down Expand Up @@ -143,6 +179,7 @@ export function defineCommerceEngine<
TControllerDefinitions,
SolutionType.standalone
>;
recommendationEngineDefinition: RecommendationCommerceEngineDefinition<TControllerDefinitions>;
} {
const {controllers: controllerDefinitions, ...engineOptions} = options;
type Definition = CommerceEngineDefinition<
Expand All @@ -158,12 +195,33 @@ export function defineCommerceEngine<
HydrateStaticStateFunction['fromBuildResult'];
type BuildParameters = Parameters<BuildFunction>;
type FetchStaticStateParameters = Parameters<FetchStaticStateFunction>;

type HydrateStaticStateParameters = Parameters<HydrateStaticStateFunction>;
type FetchStaticStateFromBuildResultParameters =
Parameters<FetchStaticStateFromBuildResultFunction>;
type HydrateStaticStateFromBuildResultParameters =
Parameters<HydrateStaticStateFromBuildResultFunction>;

type RecommendationDefinition =
RecommendationCommerceEngineDefinition<TControllerDefinitions>;
type RecommendationBuildFunction = RecommendationDefinition['build'];
type RecommendationFetchStaticStateFunction =
RecommendationDefinition['fetchStaticState'];
// type RecommendationHydrateStaticStateFunction =
// RecommendationDefinition['hydrateStaticState'];
type RecommendationFetchStaticStateFromBuildResultFunction =
RecommendationFetchStaticStateFunction['fromBuildResult'];
// type RecommendationHydrateStaticStateFromBuildResultFunction =
// RecommendationHydrateStaticStateFunction['fromBuildResult'];
type RecommendationBuildParameters = Parameters<RecommendationBuildFunction>;
type RecommendationFetchStaticStateParameters =
Parameters<RecommendationFetchStaticStateFunction>;
type RecommendationFetchStaticFromBuildResultsParameters =
Parameters<RecommendationFetchStaticStateFromBuildResultFunction>;
const recommendationFilter = buildRecommendationFilter(
controllerDefinitions ?? {}
);

const getOptions = () => {
return engineOptions;
};
Expand All @@ -183,11 +241,17 @@ 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
? await buildOptions.extend(getOptions())
: getOptions()
: getOptions(),
recommendationFilter.count
);
const controllers = buildControllerDefinitions({
definitionsMap: (controllerDefinitions ?? {}) as TControllerDefinitions,
Expand All @@ -209,6 +273,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
Expand All @@ -231,6 +296,11 @@ 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);
}

const searchAction = await engine.waitForRequestCompletedAction();
Expand Down Expand Up @@ -275,12 +345,110 @@ export function defineCommerceEngine<
searchAction,
},
] = params;

engine.dispatch(searchAction);
await engine.waitForRequestCompletedAction();
engine.waitForRequestCompletedAction();
return {engine, controllers};
},
}
);

// The fetch static state problem might come from here ?
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);

// This cast is no good. Or is it ?
const buildOptionsCasted =
buildOptions as BuildOptions<CommerceEngineOptions>;

const engine = buildSSRCommerceEngine(
SolutionType.recommendation,
buildOptionsCasted?.extend
? await buildOptionsCasted.extend(getOptions())
: getOptions(),
recommendationFilter.count
);

// const recommendationControllerDefinitions = controllerDefinitions?.

const controllers = buildControllerDefinitions({
definitionsMap: (controllerDefinitions ?? {}) as TControllerDefinitions,
engine,
solutionType: SolutionType.recommendation,
propsMap: (buildOptions && 'controllers' in buildOptions
? buildOptions.controllers
: {}) as InferControllerPropsMapFromDefinitions<TControllerDefinitions>,
});

return {
engine,
controllers,
};
};

const recommendationFetchStaticStateFactory = () => {
const fetchStaticState = async (
...params: RecommendationFetchStaticStateParameters
) => {
const buildResult = await recommendationBuildFactory()(...params);

const [c] = params;

const staticState = await fromBuildResult(c, {
buildResult,
});
return staticState;
};

fetchStaticState.fromBuildResult = fromBuildResult;

return fetchStaticState;
};

const fromBuildResult = async (
...params: RecommendationFetchStaticFromBuildResultsParameters
) => {
const [
c,
{
buildResult: {engine, controllers},
},
] = params;

console.log('********');
console.log(controllers);
console.log('********');

recommendationFilter.refresh(controllers, c);

const searchAction = await engine.waitForRequestCompletedAction();

console.log('AFTER REFRESH');
console.log(controllers);

return createStaticState({
searchAction,
controllers,
}) as EngineStaticState<
UnknownAction,
InferControllerStaticStateMapFromDefinitionsWithSolutionType<
TControllerDefinitions,
SolutionType.recommendation
>
>;
};

return {
listingEngineDefinition: {
build: buildFactory(SolutionType.listing),
Expand All @@ -303,5 +471,62 @@ export function defineCommerceEngine<
TControllerDefinitions,
SolutionType.standalone
>,
recommendationEngineDefinition: {
build: recommendationBuildFactory(),
fetchStaticState: recommendationFetchStaticStateFactory(),
hydrateStaticState: hydrateStaticStateFactory(
SolutionType.recommendation
),
setNavigatorContextProvider,
} as RecommendationCommerceEngineDefinition<TControllerDefinitions>,
};
}
/// 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;

// 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;

// //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
Loading
Loading