import {
Owner,
createContext,
createMemo,
useContext,
runWithOwner,
onError,
Accessor,
Setter,
Signal,
castError,
onCleanup,
cleanNode,
BRANCH
} from "./reactive.js";
import type { JSX } from "../jsx.js";
export type Component
= (props: P) => JSX.Element;
export type VoidProps
= P & { children?: never };
export type VoidComponent
= Component>;
export type ParentProps = P & { children?: JSX.Element };
export type ParentComponent
= Component>;
export type FlowProps = P & { children: C };
export type FlowComponent
= Component>;
export type Ref = T | ((val: T) => void);
export type ValidComponent = keyof JSX.IntrinsicElements | Component | (string & {});
export type ComponentProps = T extends Component
? P
: T extends keyof JSX.IntrinsicElements
? JSX.IntrinsicElements[T]
: Record;
function resolveSSRNode(node: any): string {
const t = typeof node;
if (t === "string") return node;
if (node == null || t === "boolean") return "";
if (Array.isArray(node)) {
let mapped = "";
for (let i = 0, len = node.length; i < len; i++) mapped += resolveSSRNode(node[i]);
return mapped;
}
if (t === "object") return node.t;
if (t === "function") return resolveSSRNode(node());
return String(node);
}
type SharedConfig = {
context?: HydrationContext;
};
export const sharedConfig: SharedConfig = {};
function setHydrateContext(context?: HydrationContext): void {
sharedConfig.context = context;
}
function nextHydrateContext(): HydrationContext | undefined {
return sharedConfig.context
? {
...sharedConfig.context,
id: `${sharedConfig.context.id}${sharedConfig.context.count++}-`,
count: 0
}
: undefined;
}
export function createUniqueId(): string {
const ctx = sharedConfig.context;
if (!ctx) throw new Error(`createUniqueId cannot be used under non-hydrating context`);
return `${ctx.id}${ctx.count++}`;
}
export function createComponent(Comp: (props: T) => JSX.Element, props: T): JSX.Element {
if (sharedConfig.context && !sharedConfig.context.noHydrate) {
const c = sharedConfig.context;
setHydrateContext(nextHydrateContext());
const r = Comp(props || ({} as T));
setHydrateContext(c);
return r;
}
return Comp(props || ({} as T));
}
export function mergeProps(source: T, source1: U): T & U;
export function mergeProps(source: T, source1: U, source2: V): T & U & V;
export function mergeProps(
source: T,
source1: U,
source2: V,
source3: W
): T & U & V & W;
export function mergeProps(...sources: any): any {
const target = {};
for (let i = 0; i < sources.length; i++) {
let source = sources[i];
if (typeof source === "function") source = source();
if (source) Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
}
return target;
}
export function splitProps(
props: T,
...keys: [K1[]]
): [Pick, Omit];
export function splitProps(
props: T,
...keys: [K1[], K2[]]
): [Pick, Pick, Omit];
export function splitProps<
T extends object,
K1 extends keyof T,
K2 extends keyof T,
K3 extends keyof T
>(
props: T,
...keys: [K1[], K2[], K3[]]
): [Pick, Pick, Pick, Omit];
export function splitProps<
T extends object,
K1 extends keyof T,
K2 extends keyof T,
K3 extends keyof T,
K4 extends keyof T
>(
props: T,
...keys: [K1[], K2[], K3[], K4[]]
): [Pick, Pick, Pick, Pick, Omit];
export function splitProps<
T extends object,
K1 extends keyof T,
K2 extends keyof T,
K3 extends keyof T,
K4 extends keyof T,
K5 extends keyof T
>(
props: T,
...keys: [K1[], K2[], K3[], K4[], K5[]]
): [
Pick,
Pick,
Pick,
Pick,
Pick,
Omit
];
export function splitProps(props: T, ...keys: [(keyof T)[]]) {
const descriptors = Object.getOwnPropertyDescriptors(props),
split = (k: (keyof T)[]) => {
const clone: Partial = {};
for (let i = 0; i < k.length; i++) {
const key = k[i];
if (descriptors[key]) {
Object.defineProperty(clone, key, descriptors[key]);
delete descriptors[key];
}
}
return clone;
};
return keys.map(split).concat(split(Object.keys(descriptors) as (keyof T)[]));
}
function simpleMap(
props: { each: any[]; children: Function; fallback?: string },
wrap: (fn: Function, item: any, i: number) => string
) {
const list = props.each || [],
len = list.length,
fn = props.children;
if (len) {
let mapped = Array(len);
for (let i = 0; i < len; i++) mapped[i] = wrap(fn, list[i], i);
return mapped;
}
return props.fallback;
}
export function For(props: {
each: T[];
fallback?: string;
children: (item: T, index: () => number) => string;
}) {
return simpleMap(props, (fn, item, i) => fn(item, () => i));
}
// non-keyed
export function Index(props: {
each: T[];
fallback?: string;
children: (item: () => T, index: number) => string;
}) {
return simpleMap(props, (fn, item, i) => fn(() => item, i));
}
export function Show(props: {
when: T | undefined | null | false;
keyed?: boolean;
fallback?: string;
children: string | ((item: NonNullable) => string);
}) {
let c: string | ((item: NonNullable) => string);
return props.when
? typeof (c = props.children) === "function"
? c(props.when!)
: c
: props.fallback || "";
}
export function Switch(props: {
fallback?: string;
children: MatchProps | MatchProps[];
}) {
let conditions = props.children;
Array.isArray(conditions) || (conditions = [conditions]);
for (let i = 0; i < conditions.length; i++) {
const w = conditions[i].when;
if (w) {
const c = conditions[i].children;
return typeof c === "function" ? c(w) : c;
}
}
return props.fallback || "";
}
type MatchProps = {
when: T | false;
keyed?: boolean;
children: string | ((item: T) => string);
};
export function Match(props: MatchProps) {
return props;
}
export function resetErrorBoundaries() {}
export function ErrorBoundary(props: {
fallback: string | ((err: any, reset: () => void) => string);
children: string;
}) {
let error: any,
res: any,
clean: any,
sync = true;
const ctx = sharedConfig.context!;
const id = ctx.id + ctx.count;
function displayFallback() {
cleanNode(clean);
ctx.writeResource(id, error, true);
setHydrateContext({ ...ctx, count: 0 });
const f = props.fallback;
return typeof f === "function" && f.length ? f(error, () => {}) : f;
}
onError(err => {
error = err;
!sync && ctx.replace("e" + id, displayFallback);
sync = true;
});
onCleanup(() => cleanNode(clean));
createMemo(() => {
Owner!.context = { [BRANCH]: (clean = {}) };
return (res = props.children);
});
if (error) return displayFallback();
sync = false;
return { t: `${resolveSSRNode(res)}` };
}
// Suspense Context
export interface Resource {
(): T | undefined;
state: "unresolved" | "pending" | "ready" | "refreshing" | "errored";
loading: boolean;
error: any;
latest: T | undefined;
}
type SuspenseContextType = {
resources: Map;
completed: () => void;
};
export type ResourceActions = { mutate: Setter; refetch: (info?: unknown) => void };
export type ResourceReturn = [Resource, ResourceActions];
export type ResourceSource = S | false | null | undefined | (() => S | false | null | undefined);
export type ResourceFetcher = (k: S, info: ResourceFetcherInfo) => T | Promise;
export type ResourceFetcherInfo = { value: T | undefined; refetching?: unknown };
export type ResourceOptions = undefined extends T
? {
initialValue?: T;
name?: string;
deferStream?: boolean;
ssrLoadFrom?: "initial" | "server";
storage?: () => Signal;
onHydrated?: (k: S, info: ResourceFetcherInfo) => void;
}
: {
initialValue: T;
name?: string;
deferStream?: boolean;
ssrLoadFrom?: "initial" | "server";
storage?: (v?: T) => Signal;
onHydrated?: (k: S, info: ResourceFetcherInfo) => void;
};
const SuspenseContext = createContext();
let resourceContext: any[] | null = null;
export function createResource(
fetcher: ResourceFetcher,
options?: ResourceOptions
): ResourceReturn;
export function createResource(
fetcher: ResourceFetcher,
options: ResourceOptions
): ResourceReturn;
export function createResource(
source: ResourceSource,
fetcher: ResourceFetcher,
options?: ResourceOptions
): ResourceReturn;
export function createResource(
source: ResourceSource,
fetcher: ResourceFetcher,
options: ResourceOptions
): ResourceReturn;
export function createResource(
source: ResourceSource | ResourceFetcher,
fetcher?: ResourceFetcher | ResourceOptions | ResourceOptions,
options: ResourceOptions | ResourceOptions = {}
): ResourceReturn | ResourceReturn {
if (arguments.length === 2) {
if (typeof fetcher === "object") {
options = fetcher as ResourceOptions | ResourceOptions;
fetcher = source as ResourceFetcher;
source = true as ResourceSource;
}
} else if (arguments.length === 1) {
fetcher = source as ResourceFetcher;
source = true as ResourceSource;
}
const contexts = new Set();
const id = sharedConfig.context!.id + sharedConfig.context!.count++;
let resource: { ref?: any; data?: T } = {};
let value = options.storage ? options.storage(options.initialValue)[0]() : options.initialValue;
let p: Promise | T | null;
let error: any;
if (sharedConfig.context!.async && options.ssrLoadFrom !== "initial") {
resource = sharedConfig.context!.resources[id] || (sharedConfig.context!.resources[id] = {});
if (resource.ref) {
if (!resource.data && !resource.ref[0].loading && !resource.ref[0].error)
resource.ref[1].refetch();
return resource.ref;
}
}
const read = () => {
if (error) throw error;
if (resourceContext && p) resourceContext.push(p!);
const resolved =
options.ssrLoadFrom !== "initial" &&
sharedConfig.context!.async &&
"data" in sharedConfig.context!.resources[id];
if (!resolved && read.loading) {
const ctx = useContext(SuspenseContext);
if (ctx) {
ctx.resources.set(id, read);
contexts.add(ctx);
}
}
return resolved ? sharedConfig.context!.resources[id].data : value;
};
read.loading = false;
read.error = undefined as any;
read.state = "initialValue" in options ? "resolved" : "unresolved";
Object.defineProperty(read, "latest", {
get() {
return read();
}
});
function load() {
const ctx = sharedConfig.context!;
if (!ctx.async)
return (read.loading = !!(typeof source === "function" ? (source as () => S)() : source));
if (ctx.resources && id in ctx.resources && "data" in ctx.resources[id]) {
value = ctx.resources[id].data;
return;
}
resourceContext = [];
const lookup = typeof source === "function" ? (source as () => S)() : source;
if (resourceContext.length) {
p = Promise.all(resourceContext).then(() =>
(fetcher as ResourceFetcher)((source as Accessor)(), { value })
);
}
resourceContext = null;
if (!p) {
if (lookup == null || lookup === false) return;
p = (fetcher as ResourceFetcher)(lookup, { value });
}
if (p != undefined && typeof p === "object" && "then" in p) {
read.loading = true;
read.state = "pending";
if (ctx.writeResource) ctx.writeResource(id, p, undefined, options.deferStream);
return p
.then(res => {
read.loading = false;
read.state = "resolved";
ctx.resources[id].data = res;
p = null;
notifySuspense(contexts);
return res;
})
.catch(err => {
read.loading = false;
read.state = "errored";
read.error = error = castError(err);
p = null;
notifySuspense(contexts);
});
}
ctx.resources[id].data = p;
if (ctx.writeResource) ctx.writeResource(id, p);
p = null;
return ctx.resources[id].data;
}
if (options.ssrLoadFrom !== "initial") load();
return (resource.ref = [
read,
{ refetch: load, mutate: (v: T) => (value = v) }
] as ResourceReturn);
}
export function lazy>(
fn: () => Promise<{ default: T }>
): T & { preload: () => Promise<{ default: T }> } {
let resolved: T;
let p: Promise<{ default: T }>;
let load = () => {
if (!p) {
p = fn();
p.then(mod => (resolved = mod.default));
}
return p;
};
const contexts = new Set();
const wrap: Component> & {
preload?: () => Promise<{ default: T }>;
} = props => {
load();
const id = sharedConfig.context!.id.slice(0, -1);
if (resolved) return resolved(props);
const ctx = useContext(SuspenseContext);
const track = { loading: true, error: undefined };
if (ctx) {
ctx.resources.set(id, track);
contexts.add(ctx);
}
if (sharedConfig.context!.async) {
sharedConfig.context!.block(
p.then(() => {
track.loading = false;
notifySuspense(contexts);
})
);
}
return "";
};
wrap.preload = load;
return wrap as T & { preload: () => Promise<{ default: T }> };
}
function suspenseComplete(c: SuspenseContextType) {
for (const r of c.resources.values()) {
if (r.loading) return false;
}
return true;
}
function notifySuspense(contexts: Set) {
for (const c of contexts) {
if (suspenseComplete(c)) c.completed();
}
contexts.clear();
}
export function enableScheduling() {}
export function enableHydration() {}
export function startTransition(fn: () => any): void {
fn();
}
export function useTransition(): [() => boolean, (fn: () => any) => void] {
return [
() => false,
fn => {
fn();
}
];
}
type HydrationContext = {
id: string;
count: number;
writeResource: (
id: string,
v: Promise | any,
error?: boolean,
deferStream?: boolean
) => void;
replace: (id: string, replacement: () => any) => void;
block: (p: Promise) => void;
resources: Record;
suspense: Record;
registerFragment: (v: string) => (v?: string, err?: any) => boolean;
async?: boolean;
noHydrate: boolean;
};
export function SuspenseList(props: {
children: string;
revealOrder: "forwards" | "backwards" | "together";
tail?: "collapsed" | "hidden";
}) {
// TODO: support tail options
return props.children;
}
export function Suspense(props: { fallback?: string; children: string }) {
let done: undefined | ((html?: string, error?: any) => boolean);
let clean: any;
const ctx = sharedConfig.context!;
const id = ctx.id + ctx.count;
const o = Owner;
if (o) {
if (o.context) o.context[BRANCH] = clean = {};
else o.context = { [BRANCH]: (clean = {}) };
}
const value: SuspenseContextType =
ctx.suspense[id] ||
(ctx.suspense[id] = {
resources: new Map(),
completed: () => {
const res = runSuspense();
if (suspenseComplete(value)) {
done!(resolveSSRNode(res));
}
}
});
function runSuspense() {
setHydrateContext({ ...ctx, count: 0 });
return runWithOwner(o!, () => {
return createComponent(SuspenseContext.Provider, {
value,
get children() {
clean && cleanNode(clean);
return props.children;
}
});
});
}
const res = runSuspense();
// never suspended
if (suspenseComplete(value)) return res;
onError(err => {
if (!done || !done(undefined, err)) {
if (o)
runWithOwner(o.owner!, () => {
throw err;
});
else throw err;
}
});
done = ctx.async ? ctx.registerFragment(id) : undefined;
if (ctx.async) {
setHydrateContext({ ...ctx, count: 0, id: ctx.id + "0.f", noHydrate: true });
const res = { t: `${resolveSSRNode(props.fallback)}` };
setHydrateContext(ctx);
return res;
}
setHydrateContext({ ...ctx, count: 0, id: ctx.id + "0.f" });
ctx.writeResource(id, "$$f");
return props.fallback;
}