Skip to content

Commit 8d5c868

Browse files
committed
feature: suspense api
1 parent 0661e11 commit 8d5c868

File tree

11 files changed

+330
-30
lines changed

11 files changed

+330
-30
lines changed

src/components/Application.gts

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
runDestructors,
44
Component,
55
tracked,
6+
getRoot,
67
} from '@lifeart/gxt';
78
import { PageOne } from './pages/PageOne.gts';
89
import { PageTwo } from './pages/PageTwo.gts';
@@ -11,8 +12,10 @@ import { Benchmark } from './pages/Benchmark.gts';
1112
import { NestedRouter } from './pages/NestedRouter.gts';
1213
import { router } from './../services/router';
1314

15+
let version = 0;
1416
export class Application extends Component {
1517
router = router;
18+
version = version++;
1619
@tracked
1720
now = Date.now();
1821
rootNode!: HTMLElement;
@@ -23,7 +26,7 @@ export class Application extends Component {
2326
benchmark: Benchmark,
2427
};
2528
async destroy() {
26-
await Promise.all(runDestructors(this));
29+
await Promise.all(runDestructors(getRoot()!));
2730
this.rootNode.innerHTML = '';
2831
this.rootNode = null!;
2932
}

src/components/Fallback.gts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Component } from '@lifeart/gxt';
2+
3+
export default class Fallback extends Component {
4+
<template>
5+
<div class='inline-flex flex-col items-center'>
6+
<div
7+
class='w-28 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full animate-pulse mt-2 mb-1'
8+
></div>
9+
</div>
10+
</template>
11+
}

src/components/LoadMeAsync.gts

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { context } from '@/utils/context';
2+
import { SUSPENSE_CONTEXT } from '@/utils/suspense';
3+
import { Component } from '@lifeart/gxt';
4+
5+
export default class LoadMeAsync extends Component<{
6+
Args: { name: string };
7+
}> {
8+
constructor() {
9+
// @ts-ignore
10+
super(...arguments);
11+
console.log('LoadMeAsync created');
12+
this.suspense?.start();
13+
}
14+
@context(SUSPENSE_CONTEXT) suspense!: {
15+
start: () => void;
16+
end: () => void;
17+
};
18+
loadData = (_: HTMLElement) => {
19+
setTimeout(() => {
20+
this.suspense?.end();
21+
console.log('Data loaded');
22+
}, 2000);
23+
};
24+
<template>
25+
{{log 'loadMeAsync rendered'}}
26+
<div {{this.loadData}} class='inline-flex flex-col items-center'>Async
27+
component "{{@name}}"</div>
28+
</template>
29+
}

src/components/pages/PageOne.gts

+20-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
import { Component, cell } from '@lifeart/gxt';
1+
import { cell } from '@lifeart/gxt';
22
import { Smile } from './page-one/Smile';
33
import { Table } from './page-one/Table.gts';
4+
import { Suspense, lazy } from '@/utils/suspense';
5+
import Fallback from '@/components/Fallback';
6+
7+
const LoadMeAsync = lazy(() => import('@/components/LoadMeAsync'));
48

59
function Controls() {
610
const color = cell('red');
@@ -28,7 +32,21 @@ export function PageOne() {
2832
<div class='text-white p-3'>
2933
<Controls />
3034
<br />
31-
35+
<Suspense @fallback={{Fallback}}>
36+
<LoadMeAsync @name='foo' />
37+
<Suspense @fallback={{Fallback}}>
38+
<LoadMeAsync @name='bar' />
39+
<Suspense @fallback={{Fallback}}>
40+
<LoadMeAsync @name='baz' />
41+
<Suspense @fallback={{Fallback}}>
42+
<LoadMeAsync @name='boo' />
43+
<Suspense @fallback={{Fallback}}>
44+
<LoadMeAsync @name='doo' />
45+
</Suspense>
46+
</Suspense>
47+
</Suspense>
48+
</Suspense>
49+
</Suspense>
3250
<div>Imagine a world where the robust, mature ecosystems of development
3351
tools meet the cutting-edge performance of modern compilers. That's what
3452
we're building here! Our platform takes the best of established

src/utils/benchmark/benchmark.ts

+14-5
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,35 @@ import { withRehydration } from '@/utils/ssr/rehydration';
33
import { getDocument } from '@/utils/dom-api';
44
import { measureRender } from '@/utils/benchmark/measure-render';
55
import { setResolveRender } from '@/utils/runtime';
6+
import { runDestructors } from '@/utils/component';
7+
import { getRoot, resetRoot } from '@/utils/dom';
68

79
export function createBenchmark() {
810
return {
911
async render() {
1012
await measureRender('render', 'renderStart', 'renderEnd', () => {
1113
const root = getDocument().getElementById('app')!;
14+
let appRef: Application | null = null;
1215
if (root.childNodes.length > 1) {
1316
try {
1417
// @ts-expect-error
1518
withRehydration(function () {
16-
return new Application(root);
19+
appRef = new Application(root);
20+
return appRef;
1721
}, root);
1822
console.info('Rehydration successful');
1923
} catch (e) {
20-
console.error('Rehydration failed, fallback to normal render', e);
21-
root.innerHTML = '';
22-
new Application(root);
24+
(async() => {
25+
console.error('Rehydration failed, fallback to normal render', e);
26+
await runDestructors(getRoot()!);
27+
resetRoot();
28+
root.innerHTML = '';
29+
appRef = new Application(root);
30+
})();
31+
2332
}
2433
} else {
25-
new Application(root);
34+
appRef = new Application(root);
2635
}
2736
});
2837

src/utils/context.ts

+23-7
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,39 @@
11
import { registerDestructor } from './glimmer/destroyable';
22
import { Component } from './component';
3-
import { PARENT_GRAPH } from './shared';
4-
import { getRoot } from './dom';
3+
import { $args, PARENT_GRAPH } from './shared';
4+
import { $PARENT_SYMBOL, getRoot } from './dom';
55

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

8-
export function context(contextKey: symbol): (klass: any, key: string, descriptor?: PropertyDescriptor & { initializer?: () => any } ) => void {
8+
export function getAnyContext<T>(ctx: Component<any>, key: symbol): T | null {
9+
return (
10+
getContext(ctx, key) ||
11+
getContext(ctx[$args][$PARENT_SYMBOL], key) ||
12+
getContext(getRoot()!, key)
13+
);
14+
}
15+
16+
export function context(
17+
contextKey: symbol,
18+
): (
19+
klass: any,
20+
key: string,
21+
descriptor?: PropertyDescriptor & { initializer?: () => any },
22+
) => void {
923
return function contextDecorator(
1024
_: any,
1125
__: string,
1226
descriptor?: PropertyDescriptor & { initializer?: () => any },
1327
) {
1428
return {
1529
get() {
16-
return getContext(this, contextKey) || getContext(getRoot()!, contextKey) || descriptor!.initializer?.call(this);
30+
return (
31+
getAnyContext(this, contextKey) || descriptor!.initializer?.call(this)
32+
);
1733
},
18-
}
19-
}
20-
};
34+
};
35+
};
36+
}
2137

2238
export function getContext<T>(ctx: Component<any>, key: symbol): T | null {
2339
let current: Component<any> | null = ctx;

src/utils/control-flow/if.ts

+14-9
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import {
2222
} from '@/utils/shared';
2323
import { opcodeFor } from '@/utils/vm';
2424

25+
export type IfFunction = () => boolean;
26+
2527
export class IfCondition {
2628
isDestructorRunning = false;
2729
prevComponent: GenericReturnType | null = null;
@@ -33,15 +35,15 @@ export class IfCondition {
3335
placeholder: Comment;
3436
throwedError: Error | null = null;
3537
destroyPromise: Promise<any> | null = null;
36-
trueBranch: (ifContext: Component<any>) => GenericReturnType;
37-
falseBranch: (ifContext: Component<any>) => GenericReturnType;
38+
trueBranch: (ifContext: IfCondition) => GenericReturnType;
39+
falseBranch: (ifContext: IfCondition) => GenericReturnType;
3840
constructor(
3941
parentContext: Component<any>,
40-
maybeCondition: Cell<boolean>,
42+
maybeCondition: Cell<boolean> | IfFunction | MergedCell,
4143
target: DocumentFragment | HTMLElement,
4244
placeholder: Comment,
43-
trueBranch: (ifContext: Component<any>) => GenericReturnType,
44-
falseBranch: (ifContext: Component<any>) => GenericReturnType,
45+
trueBranch: (ifContext: IfCondition) => GenericReturnType,
46+
falseBranch: (ifContext: IfCondition) => GenericReturnType,
4547
) {
4648
this.target = target;
4749
this.placeholder = placeholder;
@@ -111,7 +113,7 @@ export class IfCondition {
111113
this.renderBranch(nextBranch, this.runNumber);
112114
}
113115
renderBranch(
114-
nextBranch: (ifContext: Component<any>) => GenericReturnType,
116+
nextBranch: (ifContext: IfCondition) => GenericReturnType,
115117
runNumber: number,
116118
) {
117119
if (this.destroyPromise) {
@@ -162,11 +164,11 @@ export class IfCondition {
162164
this.destroyPromise = destroyElement(branch);
163165
await this.destroyPromise;
164166
}
165-
renderState(nextBranch: (ifContext: Component<any>) => GenericReturnType) {
167+
renderState(nextBranch: (ifContext: IfCondition) => GenericReturnType) {
166168
if (IS_DEV_MODE) {
167169
$DEBUG_REACTIVE_CONTEXTS.push(`if:${String(this.lastValue)}`);
168170
}
169-
this.prevComponent = nextBranch(this as unknown as Component<any>);
171+
this.prevComponent = nextBranch(this);
170172
if (IS_DEV_MODE) {
171173
$DEBUG_REACTIVE_CONTEXTS.pop();
172174
}
@@ -178,6 +180,9 @@ export class IfCondition {
178180
return;
179181
}
180182
async destroy() {
183+
if (this.isDestructorRunning) {
184+
throw new Error('Already destroying');
185+
}
181186
this.isDestructorRunning = true;
182187
if (this.placeholder.isConnected) {
183188
// should be handled on the top level
@@ -186,7 +191,7 @@ export class IfCondition {
186191
await this.destroyBranch();
187192
await Promise.all(this.destructors.map((destroyFn) => destroyFn()));
188193
}
189-
setupCondition(maybeCondition: Cell<boolean>) {
194+
setupCondition(maybeCondition: Cell<boolean> | IfFunction | MergedCell) {
190195
if (isFn(maybeCondition)) {
191196
this.condition = formula(
192197
() => {

src/utils/dom.ts

+9-5
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ import {
4545
} from './shared';
4646
import { isRehydrationScheduled } from './ssr/rehydration';
4747
import { createHotReload } from './hmr';
48-
import { IfCondition } from './control-flow/if';
4948
import { CONSTANTS } from '../../plugins/symbols';
49+
import { IfCondition, type IfFunction } from './control-flow/if';
5050

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

7070
type Fn = () => unknown;
7171
type InElementFnArg = () => HTMLElement;
72-
type BranchCb = () => ComponentReturnType | Node;
72+
type BranchCb = (ctx: IfCondition) => ComponentReturnType | Node | null;
7373

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

7878
export const $SLOTS_SYMBOL = Symbol('slots');
7979
export const $PROPS_SYMBOL = Symbol('props');
80+
export const $PARENT_SYMBOL = Symbol('parent');
8081

8182
const $_className = 'className';
8283

@@ -657,7 +658,7 @@ export function $_inElement(
657658
export function $_ucw(
658659
roots: (context: Component<any>) => (Node | ComponentReturnType)[],
659660
ctx: any,
660-
) {
661+
): ComponentReturnType {
661662
return component(
662663
function UnstableChildWrapper(this: Component<any>) {
663664
if (IS_DEV_MODE) {
@@ -668,7 +669,7 @@ export function $_ucw(
668669
} as unknown as Component<any>,
669670
{},
670671
ctx,
671-
);
672+
) as ComponentReturnType;
672673
}
673674

674675
if (IS_DEV_MODE) {
@@ -854,6 +855,8 @@ function _component(
854855
comp = comp.value;
855856
}
856857
}
858+
// @ts-expect-error index type
859+
args[$PARENT_SYMBOL] = ctx;
857860
let instance =
858861
// @ts-expect-error construct signature
859862
comp.prototype === undefined
@@ -1086,8 +1089,9 @@ function toNodeReturnType(
10861089
};
10871090
}
10881091

1092+
10891093
function ifCond(
1090-
cell: Cell<boolean>,
1094+
cell: Cell<boolean> | MergedCell | IfFunction,
10911095
trueBranch: BranchCb,
10921096
falseBranch: BranchCb,
10931097
ctx: Component<any>,

src/utils/ssr/rehydration-dom-api.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getNodeCounter, incrementNodeCounter } from '@/utils/dom';
1+
import { getNodeCounter, getRoot, incrementNodeCounter } from '@/utils/dom';
22

33
import { getDocument } from '@/utils/dom-api';
44
import {
@@ -156,6 +156,10 @@ export const api = {
156156
targetIndex: number = 0,
157157
) {
158158
if (isRehydrationScheduled()) {
159+
if (!parent) {
160+
console.log(getRoot());
161+
// debugger;
162+
}
159163
// in this case likely child is a text node, and we don't need to append it, we need to prepend it
160164
const childNodes = Array.from(parent.childNodes);
161165
const maybeIndex = childNodes.indexOf(child as any);

src/utils/ssr/rehydration.ts

+1
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ export function withRehydration(
164164
withRehydrationStack.length = 0;
165165
nodesMap.clear();
166166
resetNodeCounter();
167+
console.log('rollbackDOMAPI');
167168
rollbackDOMAPI();
168169
throw e;
169170
}

0 commit comments

Comments
 (0)