Skip to content

Commit

Permalink
feature: suspense api
Browse files Browse the repository at this point in the history
  • Loading branch information
lifeart committed Sep 17, 2024
1 parent 0661e11 commit 8d5c868
Show file tree
Hide file tree
Showing 11 changed files with 330 additions and 30 deletions.
5 changes: 4 additions & 1 deletion src/components/Application.gts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
runDestructors,
Component,
tracked,
getRoot,
} from '@lifeart/gxt';
import { PageOne } from './pages/PageOne.gts';
import { PageTwo } from './pages/PageTwo.gts';
Expand All @@ -11,8 +12,10 @@ import { Benchmark } from './pages/Benchmark.gts';
import { NestedRouter } from './pages/NestedRouter.gts';
import { router } from './../services/router';

let version = 0;
export class Application extends Component {
router = router;
version = version++;
@tracked
now = Date.now();
rootNode!: HTMLElement;
Expand All @@ -23,7 +26,7 @@ export class Application extends Component {
benchmark: Benchmark,
};
async destroy() {
await Promise.all(runDestructors(this));
await Promise.all(runDestructors(getRoot()!));
this.rootNode.innerHTML = '';
this.rootNode = null!;
}
Expand Down
11 changes: 11 additions & 0 deletions src/components/Fallback.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Component } from '@lifeart/gxt';

export default class Fallback extends Component {
<template>
<div class='inline-flex flex-col items-center'>
<div
class='w-28 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full animate-pulse mt-2 mb-1'
></div>
</div>
</template>
}
29 changes: 29 additions & 0 deletions src/components/LoadMeAsync.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { context } from '@/utils/context';
import { SUSPENSE_CONTEXT } from '@/utils/suspense';
import { Component } from '@lifeart/gxt';

export default class LoadMeAsync extends Component<{
Args: { name: string };
}> {
constructor() {
// @ts-ignore
super(...arguments);
console.log('LoadMeAsync created');
this.suspense?.start();
}
@context(SUSPENSE_CONTEXT) suspense!: {
start: () => void;
end: () => void;
};
loadData = (_: HTMLElement) => {
setTimeout(() => {
this.suspense?.end();
console.log('Data loaded');
}, 2000);
};
<template>
{{log 'loadMeAsync rendered'}}
<div {{this.loadData}} class='inline-flex flex-col items-center'>Async
component "{{@name}}"</div>
</template>
}
22 changes: 20 additions & 2 deletions src/components/pages/PageOne.gts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { Component, cell } from '@lifeart/gxt';
import { cell } from '@lifeart/gxt';
import { Smile } from './page-one/Smile';
import { Table } from './page-one/Table.gts';
import { Suspense, lazy } from '@/utils/suspense';
import Fallback from '@/components/Fallback';

const LoadMeAsync = lazy(() => import('@/components/LoadMeAsync'));

function Controls() {
const color = cell('red');
Expand Down Expand Up @@ -28,7 +32,21 @@ export function PageOne() {
<div class='text-white p-3'>
<Controls />
<br />

<Suspense @fallback={{Fallback}}>
<LoadMeAsync @name='foo' />
<Suspense @fallback={{Fallback}}>
<LoadMeAsync @name='bar' />
<Suspense @fallback={{Fallback}}>
<LoadMeAsync @name='baz' />
<Suspense @fallback={{Fallback}}>
<LoadMeAsync @name='boo' />
<Suspense @fallback={{Fallback}}>
<LoadMeAsync @name='doo' />
</Suspense>
</Suspense>
</Suspense>
</Suspense>
</Suspense>
<div>Imagine a world where the robust, mature ecosystems of development
tools meet the cutting-edge performance of modern compilers. That's what
we're building here! Our platform takes the best of established
Expand Down
19 changes: 14 additions & 5 deletions src/utils/benchmark/benchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,35 @@ import { withRehydration } from '@/utils/ssr/rehydration';
import { getDocument } from '@/utils/dom-api';
import { measureRender } from '@/utils/benchmark/measure-render';
import { setResolveRender } from '@/utils/runtime';
import { runDestructors } from '@/utils/component';
import { getRoot, resetRoot } from '@/utils/dom';

export function createBenchmark() {
return {
async render() {
await measureRender('render', 'renderStart', 'renderEnd', () => {
const root = getDocument().getElementById('app')!;
let appRef: Application | null = null;
if (root.childNodes.length > 1) {
try {
// @ts-expect-error
withRehydration(function () {
return new Application(root);
appRef = new Application(root);
return appRef;
}, root);
console.info('Rehydration successful');
} catch (e) {
console.error('Rehydration failed, fallback to normal render', e);
root.innerHTML = '';
new Application(root);
(async() => {
console.error('Rehydration failed, fallback to normal render', e);
await runDestructors(getRoot()!);
resetRoot();
root.innerHTML = '';
appRef = new Application(root);
})();

}
} else {
new Application(root);
appRef = new Application(root);
}
});

Expand Down
30 changes: 23 additions & 7 deletions src/utils/context.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,39 @@
import { registerDestructor } from './glimmer/destroyable';
import { Component } from './component';
import { PARENT_GRAPH } from './shared';
import { getRoot } from './dom';
import { $args, PARENT_GRAPH } from './shared';
import { $PARENT_SYMBOL, getRoot } from './dom';

const CONTEXTS = new WeakMap<Component<any>, Map<symbol, any>>();

export function context(contextKey: symbol): (klass: any, key: string, descriptor?: PropertyDescriptor & { initializer?: () => any } ) => void {
export function getAnyContext<T>(ctx: Component<any>, key: symbol): T | null {
return (
getContext(ctx, key) ||
getContext(ctx[$args][$PARENT_SYMBOL], key) ||
getContext(getRoot()!, key)
);
}

export function context(
contextKey: symbol,
): (
klass: any,
key: string,
descriptor?: PropertyDescriptor & { initializer?: () => any },
) => void {
return function contextDecorator(
_: any,
__: string,
descriptor?: PropertyDescriptor & { initializer?: () => any },
) {
return {
get() {
return getContext(this, contextKey) || getContext(getRoot()!, contextKey) || descriptor!.initializer?.call(this);
return (
getAnyContext(this, contextKey) || descriptor!.initializer?.call(this)
);
},
}
}
};
};
};
}

export function getContext<T>(ctx: Component<any>, key: symbol): T | null {
let current: Component<any> | null = ctx;
Expand Down
23 changes: 14 additions & 9 deletions src/utils/control-flow/if.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
} from '@/utils/shared';
import { opcodeFor } from '@/utils/vm';

export type IfFunction = () => boolean;

export class IfCondition {
isDestructorRunning = false;
prevComponent: GenericReturnType | null = null;
Expand All @@ -33,15 +35,15 @@ export class IfCondition {
placeholder: Comment;
throwedError: Error | null = null;
destroyPromise: Promise<any> | null = null;
trueBranch: (ifContext: Component<any>) => GenericReturnType;
falseBranch: (ifContext: Component<any>) => GenericReturnType;
trueBranch: (ifContext: IfCondition) => GenericReturnType;
falseBranch: (ifContext: IfCondition) => GenericReturnType;
constructor(
parentContext: Component<any>,
maybeCondition: Cell<boolean>,
maybeCondition: Cell<boolean> | IfFunction | MergedCell,
target: DocumentFragment | HTMLElement,
placeholder: Comment,
trueBranch: (ifContext: Component<any>) => GenericReturnType,
falseBranch: (ifContext: Component<any>) => GenericReturnType,
trueBranch: (ifContext: IfCondition) => GenericReturnType,
falseBranch: (ifContext: IfCondition) => GenericReturnType,
) {
this.target = target;
this.placeholder = placeholder;
Expand Down Expand Up @@ -111,7 +113,7 @@ export class IfCondition {
this.renderBranch(nextBranch, this.runNumber);
}
renderBranch(
nextBranch: (ifContext: Component<any>) => GenericReturnType,
nextBranch: (ifContext: IfCondition) => GenericReturnType,
runNumber: number,
) {
if (this.destroyPromise) {
Expand Down Expand Up @@ -162,11 +164,11 @@ export class IfCondition {
this.destroyPromise = destroyElement(branch);
await this.destroyPromise;
}
renderState(nextBranch: (ifContext: Component<any>) => GenericReturnType) {
renderState(nextBranch: (ifContext: IfCondition) => GenericReturnType) {
if (IS_DEV_MODE) {
$DEBUG_REACTIVE_CONTEXTS.push(`if:${String(this.lastValue)}`);
}
this.prevComponent = nextBranch(this as unknown as Component<any>);
this.prevComponent = nextBranch(this);
if (IS_DEV_MODE) {
$DEBUG_REACTIVE_CONTEXTS.pop();
}
Expand All @@ -178,6 +180,9 @@ export class IfCondition {
return;
}
async destroy() {
if (this.isDestructorRunning) {
throw new Error('Already destroying');
}
this.isDestructorRunning = true;
if (this.placeholder.isConnected) {
// should be handled on the top level
Expand All @@ -186,7 +191,7 @@ export class IfCondition {
await this.destroyBranch();
await Promise.all(this.destructors.map((destroyFn) => destroyFn()));
}
setupCondition(maybeCondition: Cell<boolean>) {
setupCondition(maybeCondition: Cell<boolean> | IfFunction | MergedCell) {
if (isFn(maybeCondition)) {
this.condition = formula(
() => {
Expand Down
14 changes: 9 additions & 5 deletions src/utils/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ import {
} from './shared';
import { isRehydrationScheduled } from './ssr/rehydration';
import { createHotReload } from './hmr';
import { IfCondition } from './control-flow/if';
import { CONSTANTS } from '../../plugins/symbols';
import { IfCondition, type IfFunction } from './control-flow/if';

type RenderableType = Node | ComponentReturnType | string | number;
type ShadowRootMode = 'open' | 'closed' | null;
Expand All @@ -69,14 +69,15 @@ type Props = [TagProp[], TagAttr[], TagEvent[], FwType?];

type Fn = () => unknown;
type InElementFnArg = () => HTMLElement;
type BranchCb = () => ComponentReturnType | Node;
type BranchCb = (ctx: IfCondition) => ComponentReturnType | Node | null;

// EMPTY DOM PROPS
export const $_edp = [[], [], []] as Props;
export const $_emptySlot = Object.seal(Object.freeze({}));

export const $SLOTS_SYMBOL = Symbol('slots');
export const $PROPS_SYMBOL = Symbol('props');
export const $PARENT_SYMBOL = Symbol('parent');

const $_className = 'className';

Expand Down Expand Up @@ -657,7 +658,7 @@ export function $_inElement(
export function $_ucw(
roots: (context: Component<any>) => (Node | ComponentReturnType)[],
ctx: any,
) {
): ComponentReturnType {
return component(
function UnstableChildWrapper(this: Component<any>) {
if (IS_DEV_MODE) {
Expand All @@ -668,7 +669,7 @@ export function $_ucw(
} as unknown as Component<any>,
{},
ctx,
);
) as ComponentReturnType;
}

if (IS_DEV_MODE) {
Expand Down Expand Up @@ -854,6 +855,8 @@ function _component(
comp = comp.value;
}
}
// @ts-expect-error index type
args[$PARENT_SYMBOL] = ctx;
let instance =
// @ts-expect-error construct signature
comp.prototype === undefined
Expand Down Expand Up @@ -1086,8 +1089,9 @@ function toNodeReturnType(
};
}


function ifCond(
cell: Cell<boolean>,
cell: Cell<boolean> | MergedCell | IfFunction,
trueBranch: BranchCb,
falseBranch: BranchCb,
ctx: Component<any>,
Expand Down
6 changes: 5 additions & 1 deletion src/utils/ssr/rehydration-dom-api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getNodeCounter, incrementNodeCounter } from '@/utils/dom';
import { getNodeCounter, getRoot, incrementNodeCounter } from '@/utils/dom';

import { getDocument } from '@/utils/dom-api';
import {
Expand Down Expand Up @@ -156,6 +156,10 @@ export const api = {
targetIndex: number = 0,
) {
if (isRehydrationScheduled()) {
if (!parent) {
console.log(getRoot());
// debugger;
}
// in this case likely child is a text node, and we don't need to append it, we need to prepend it
const childNodes = Array.from(parent.childNodes);
const maybeIndex = childNodes.indexOf(child as any);
Expand Down
1 change: 1 addition & 0 deletions src/utils/ssr/rehydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ export function withRehydration(
withRehydrationStack.length = 0;
nodesMap.clear();
resetNodeCounter();
console.log('rollbackDOMAPI');
rollbackDOMAPI();
throw e;
}
Expand Down
Loading

0 comments on commit 8d5c868

Please sign in to comment.