Skip to content

Commit

Permalink
Async Rendering: A new approach ⚡ (#722)
Browse files Browse the repository at this point in the history
* async rendering

* Allow props resolve start to return undefined

Signed-off-by: Marcos Candeia <[email protected]>

---------

Signed-off-by: Marcos Candeia <[email protected]>
Co-authored-by: Marcos Candeia <[email protected]>
  • Loading branch information
tlgimenes and mcandeia authored Jul 25, 2024
1 parent d908b7a commit 9e0189a
Show file tree
Hide file tree
Showing 9 changed files with 90 additions and 61 deletions.
36 changes: 20 additions & 16 deletions blocks/section.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import StubSection, { Empty } from "../components/StubSection.tsx";
import { alwaysThrow, withSection } from "../components/section.tsx";
import { Context } from "../deco.ts";
import type { JSX } from "../deps.ts";
import type { DeepPartial, JSX } from "../deps.ts";
import type {
Block,
BlockModule,
Expand All @@ -18,8 +18,8 @@ import type {
PreactComponent,
} from "../engine/block.ts";
import type { Resolver } from "../engine/core/resolver.ts";
import type { AppManifest, FunctionContext } from "../types.ts";
import { HttpError } from "../engine/errors.ts";
import type { AppManifest } from "../types.ts";

/**
* @widget none
Expand Down Expand Up @@ -54,6 +54,8 @@ export type SectionProps<LoaderFunc, ActionFunc = LoaderFunc> =
| ReturnProps<LoaderFunc>
| ReturnProps<ActionFunc>;

export type LoadingFallbackProps<TProps> = DeepPartial<TProps>;

export interface ErrorBoundaryParams<TProps> {
error: any;
props: TProps;
Expand All @@ -74,7 +76,7 @@ export interface SectionModule<
PreactComponent
> {
// Fix this ComponentType<any>
LoadingFallback?: ComponentType<any>;
LoadingFallback?: ComponentType<DeepPartial<TConfig>>;
ErrorFallback?: ComponentType<{ error?: Error }>;
loader?: PropsLoader<TConfig, TLoaderProps>;
action?: PropsLoader<TConfig, TActionProps>;
Expand Down Expand Up @@ -103,12 +105,16 @@ export const createSectionBlock = (
TConfig,
HttpContext<RequestState>
> => {
const withMainComponent = (mainComponent: ComponentFunc) =>
wrapper<TProps>(
const withMainComponent = (
mainComponent: ComponentFunc,
loaderProps?: TConfig,
) =>
wrapper<TProps, TConfig>(
resolver,
mainComponent,
mod.LoadingFallback,
mod.ErrorFallback,
loaderProps,
);
const useExportDefaultComponent = withMainComponent(mod.default);
if (!mod.action && !mod.loader) {
Expand All @@ -125,8 +131,6 @@ export const createSectionBlock = (
): Promise<PreactComponent<TProps>> => {
const {
request,
context,
resolve,
} = httpCtx;

const loaderSectionProps = request.method === "GET"
Expand All @@ -137,26 +141,26 @@ export const createSectionBlock = (
return useExportDefaultComponent(props as unknown as TProps, httpCtx);
}

const ctx = {
...context,
state: { ...context.state, $live: props, resolve },
} as FunctionContext;

const fnContext = fnContextFromHttpContext(httpCtx);
const useExportDefaultComponentWithProps = withMainComponent(
mod.default,
props,
);

return await propsLoader(
loaderSectionProps,
ctx.state.$live,
props,
request,
fnContext,
).then((props) => {
return useExportDefaultComponent(props, httpCtx);
return useExportDefaultComponentWithProps(props, httpCtx);
}).catch((err) => {
if (err instanceof HttpError) {
throw err;
}
const allowErrorBoundary = withMainComponent(alwaysThrow(err));
const allowErrorBoundary = withMainComponent(alwaysThrow(err), props);
return allowErrorBoundary(
ctx.state.$live as unknown as TProps,
props as unknown as TProps,
httpCtx,
);
});
Expand Down
53 changes: 30 additions & 23 deletions components/section.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { PartialProps } from "$fresh/src/runtime/Partial.tsx";
import { Component, type ComponentType, createContext, Fragment } from "preact";
import { Component, type ComponentType, createContext } from "preact";
import { useContext } from "preact/hooks";
import type { HttpContext } from "../blocks/handler.ts";
import type { RequestState } from "../blocks/utils.tsx";
import { Context, RequestContext } from "../deco.ts";
import { Murmurhash3 } from "../deps.ts";
import { Context } from "../deco.ts";
import { type DeepPartial, Murmurhash3 } from "../deps.ts";
import type { ComponentFunc } from "../engine/block.ts";
import type { FieldResolver } from "../engine/core/resolver.ts";
import { HttpError } from "../engine/errors.ts";
Expand All @@ -18,7 +18,7 @@ export interface SectionContext extends HttpContext<RequestState> {
device: Device;
framework: "fresh" | "htmx";
deploymentId?: string;
loading?: "eager" | "lazy";
FallbackWrapper: ComponentType<any>;
}

export const SectionContext = createContext<SectionContext | undefined>(
Expand Down Expand Up @@ -103,7 +103,9 @@ export interface Framework {
isDeploy: boolean;
error: Error;
}>;
LoadingFallback: ComponentType<{ id: string }>;
LoadingFallback: ComponentType<
{ id: string; props?: Record<string, unknown> }
>;
}

export const bindings = {
Expand All @@ -116,11 +118,12 @@ export const alwaysThrow =
throw err;
};
const MAX_RENDER_COUNT = 5_00; // for saved sections this number should mark a restart.
export const withSection = <TProps,>(
export const withSection = <TProps, TLoaderProps = TProps>(
resolver: string,
ComponentFunc: ComponentFunc,
LoadingFallback: ComponentType = Fragment,
LoadingFallback?: ComponentType<DeepPartial<TLoaderProps>>,
ErrorFallback?: ComponentType<{ error?: Error }>,
loaderProps?: TLoaderProps,
) =>
(
props: TProps,
Expand All @@ -147,43 +150,36 @@ export const withSection = <TProps,>(
};
let device: Device | null = null;

/**
* Get the signal created during the request;
*/
const signal = RequestContext.signal;

return {
props,
Component: (props: TProps) => {
const { isDeploy, request, deploymentId } = Context.active();

const framework = frameworkFromState ?? request?.framework ?? "fresh";
const binding = bindings[framework];

// if parent salt is not defined it means that we are at the root level, meaning that we are the first partial in the rendering tree.
const {
loading,
renderSalt: parentRenderSalt,
} = useContext(SectionContext) ?? {};
const { renderSalt: parentRenderSalt } = useContext(SectionContext) ?? {};

// if this is the case, so we can use the renderSaltFromState - which means that we are in a partial rendering phase
const renderSalt = parentRenderSalt === undefined
? renderSaltFromState ?? `${renderCount}`
: `${parentRenderSalt ?? ""}${renderCount}`; // the render salt is used to prevent duplicate ids in the same page, it starts with parent renderSalt and appends how many time this function is called.
const id = `${idPrefix}-${renderSalt}`; // all children of the same parent will have the same renderSalt, but different renderCount
renderCount = ++renderCount % MAX_RENDER_COUNT;

const Throw = () => {
// If the signal from request is aborted, then throw
signal?.throwIfAborted();
return null;
};

return (
<SectionContext.Provider
value={{
...ctx,
deploymentId,
renderSalt,
framework,
FallbackWrapper: ({ children, ...props }) => (
<binding.LoadingFallback id={id} {...props}>
{children}
</binding.LoadingFallback>
),
get device() {
return device ??= deviceOf(ctx.request);
},
Expand Down Expand Up @@ -229,7 +225,6 @@ export const withSection = <TProps,>(
)
)}
>
{loading === "lazy" && <Throw />}
<ComponentFunc {...props} />
</ErrorBoundary>
</section>
Expand All @@ -238,5 +233,17 @@ export const withSection = <TProps,>(
);
},
metadata,
...LoadingFallback
? {
LoadingFallback: () => {
return (
// @ts-ignore: could not it type well
<LoadingFallback
{...(loaderProps ?? props) as DeepPartial<TLoaderProps>}
/>
);
},
}
: {},
};
};
1 change: 1 addition & 0 deletions deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export type {
export { Component } from "https://esm.sh/v135/[email protected]?pin=102";
export type { JSX } from "https://esm.sh/v135/[email protected]?pin=102";
export type {
DeepPartial,
Diff,
Intersection,
OptionalKeys,
Expand Down
2 changes: 2 additions & 0 deletions engine/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,4 +199,6 @@ export interface PreactComponent<
Component: ComponentFunc<TProps>;
props: TProps;
metadata?: ComponentMetadata;
// deno-lint-ignore ban-types
LoadingFallback?: ComponentFunc<{}>;
}
43 changes: 27 additions & 16 deletions engine/core/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ const resolvePropsWithHints = async <
const props = onBeforeResolveProps(_thisProps as T, hints);
const ctx = type ? withResolveChainOfType(_ctx, type) : _ctx;

const proceed = () => {
const proceed = (resolveId?: string) => {
return Promise.all(
Object.entries(hints).map(
async ([_key, hint]) => {
Expand All @@ -380,10 +380,15 @@ const resolvePropsWithHints = async <
const resolved = await resolvePropsWithHints(
props[key],
hint as HintNode<T[typeof key]>,
withResolveChain(ctx, {
type: "prop",
value: key.toString(),
}),
withResolveChain(
resolveId
? { ...ctx, resolveId: resolveId ?? ctx.resolveId }
: ctx,
{
type: "prop",
value: key.toString(),
},
),
opts,
);
return { key, resolved } as ResolvedKey<T, typeof key>;
Expand All @@ -401,14 +406,15 @@ const resolvePropsWithHints = async <
? [...props] as T
: { ...props };

const resolvedProps = await (type
? opts.hooks?.onPropsResolveStart?.(
const propsResolveStart = opts.hooks?.onPropsResolveStart;
const resolvedProps = await (type && propsResolveStart
? propsResolveStart(
proceed,
mutableProps,
_ctx.resolvers[type],
type,
ctx,
) ?? proceed()
)
: proceed());
for (const { key, resolved } of resolvedProps.filter(notUndefined)) {
mutableProps[key] = resolved;
Expand Down Expand Up @@ -526,13 +532,16 @@ const resolveWithType = <
resolveType,
context,
);
return opts?.hooks?.onResolveStart?.(
proceed,
props,
resolver,
resolveType,
context,
) ?? proceed();
const resolveStart = opts?.hooks?.onResolveStart;
return resolveStart
? resolveStart(
proceed,
props,
resolver,
resolveType,
context,
)
: proceed();
}

const ctx = withResolveChain(context, {
Expand Down Expand Up @@ -610,7 +619,9 @@ export interface ResolveHooks {
T,
TContext extends BaseContext = BaseContext,
>(
resolve: () => Promise<Array<ResolvedKey<T, keyof T> | undefined>>,
resolve: (
resolveId?: string,
) => Promise<Array<ResolvedKey<T, keyof T> | undefined>>,
props: T,
resolver: Resolver,
__resolveType: string,
Expand Down
1 change: 1 addition & 0 deletions mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export type {
LoaderReturnType,
PropsLoader,
Route,
LoadingFallbackProps,
SectionProps,
} from "./types.ts";
export { allowCorsFor } from "./utils/http.ts";
Expand Down
4 changes: 2 additions & 2 deletions runtime/fresh/Bindings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ const bindings: Framework = {
Wrapper: ({ id, partialMode, children }) => (
<Partial name={id} mode={partialMode}>{children}</Partial>
),
LoadingFallback: ({ id, children }) => {
LoadingFallback: ({ id, children, props }) => {
const btnId = `${id}-partial-onload`;
const { "f-partial": href, ...rest } = usePartialSection();
const { "f-partial": href, ...rest } = usePartialSection({ props });

return (
<>
Expand Down
6 changes: 3 additions & 3 deletions runtime/htmx/Bindings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ const bindings: Framework = {
</div>
);
},
LoadingFallback: function ({ children }) {
LoadingFallback: function ({ children, props }) {
return (
<div
hx-get={useSection()}
hx-trigger="intersect once"
hx-get={useSection({ props })}
hx-trigger="load once delay:6s, intersect once threshold:0.0"
hx-target="closest section"
hx-swap="outerHTML transition:true"
>
Expand Down
5 changes: 4 additions & 1 deletion types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,10 @@ export type DecoState<
};

export type { PropsLoader } from "./blocks/propsLoader.ts";
export type { SectionProps } from "./blocks/section.ts";
export type {
LoadingFallbackProps,
SectionProps,
} from "./blocks/section.ts";
export type { FnContext } from "./blocks/utils.tsx";
export type ActionContext<
TState = {},
Expand Down

0 comments on commit 9e0189a

Please sign in to comment.