import type {
InferActivityParams,
RegisteredActivityName,
} from "@stackflow/config";
import type { ComponentType, ReactNode } from "react";
import { preloadableLazyComponent } from "./utils/PreloadableLazyComponent";
import {
inspect,
PromiseStatus,
reject,
resolve,
type SyncInspectablePromise,
} from "./utils/SyncInspectablePromise";
export const STRUCTURED_ACTIVITY_COMPONENT_TYPE: unique symbol = Symbol(
"STRUCTURED_ACTIVITY_COMPONENT_TYPE",
);
export interface StructuredActivityComponentType
{
[STRUCTURED_ACTIVITY_COMPONENT_TYPE]: true;
content: Content
| (() => Promise<{ default: Content
}>);
layout?: Layout
;
loading?: Loading
;
errorHandler?: ErrorHandler
;
}
export function structuredActivityComponent<
ActivityName extends RegisteredActivityName,
>(options: {
content:
| Content>
| (() => Promise<{ default: Content> }>);
layout?: Layout>;
loading?: Loading>;
errorHandler?: ErrorHandler>;
}): StructuredActivityComponentType> {
return {
...options,
[STRUCTURED_ACTIVITY_COMPONENT_TYPE]: true,
};
}
export function isStructuredActivityComponent(
value: unknown,
): value is StructuredActivityComponentType<{}> {
return (
value !== null &&
typeof value === "object" &&
STRUCTURED_ACTIVITY_COMPONENT_TYPE in value
);
}
export interface Content {
component: ComponentType<{ params: P }>;
}
export function content(
component: ComponentType<{ params: InferActivityParams }>,
): Content> {
return { component };
}
const ContentComponentMap = new WeakMap<
StructuredActivityComponentType<{}>,
{ Component: Content<{}>["component"]; preload: () => Promise }
>();
export function getContentComponent(
structuredActivityComponent: StructuredActivityComponentType<{}>,
): { Component: Content<{}>["component"]; preload: () => Promise } {
if (ContentComponentMap.has(structuredActivityComponent)) {
return ContentComponentMap.get(structuredActivityComponent)!;
}
const { Component, preload } = preloadableLazyComponent(() => {
const content = structuredActivityComponent.content;
const contentPromise = resolve(
typeof content === "function" ? content() : { default: content },
);
const state = inspect(contentPromise);
if (state.status === PromiseStatus.FULFILLED) {
return resolve({
default: state.value.default.component,
});
} else if (state.status === PromiseStatus.REJECTED) {
return reject(state.reason);
}
return resolve(
contentPromise.then((value) => ({
default: value.default.component,
})),
);
});
ContentComponentMap.set(structuredActivityComponent, { Component, preload });
return { Component, preload };
}
export interface Layout {
component: ComponentType<{ params: P; children: ReactNode }>;
}
export function layout(
component: ComponentType<{
params: InferActivityParams;
children: ReactNode;
}>,
): Layout> {
return { component };
}
export interface Loading {
component: ComponentType<{ params: P }>;
}
export function loading(
component: ComponentType<{ params: InferActivityParams }>,
): Loading> {
return { component };
}
export interface ErrorHandler {
component: ComponentType<{ params: P; error: unknown; reset: () => void }>;
boundary?: CustomErrorBoundary;
}
export type CustomErrorBoundary = ComponentType<{
children: ReactNode;
renderFallback: (error: unknown, reset: () => void) => ReactNode;
}>;
export function errorHandler(
component: ComponentType<{
params: InferActivityParams;
error: unknown;
reset: () => void;
}>,
options?: {
boundary?: CustomErrorBoundary;
},
): ErrorHandler> {
return { component, boundary: options?.boundary };
}