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 5eb0bdf commit 7c1b5c8
Show file tree
Hide file tree
Showing 14 changed files with 384 additions and 42 deletions.
38 changes: 37 additions & 1 deletion plugins/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,43 @@
import { expect, test, describe } from 'vitest';
import { toSafeJSPath } from './utils';
import { toSafeJSPath, escapeString } from './utils';

const f = (str: string) => toSafeJSPath(str);
const e = (str: any) => escapeString(str as string);


describe('escapeString', () => {
test('works for classic case', () => {
expect(e('this.foo.bar.baz')).toEqual(`"this.foo.bar.baz"`);
});
test('works for string with quotes', () => {
expect(e('this.foo.bar.baz"')).toEqual(`"this.foo.bar.baz\\""`);
});
test('works for string with double quotes', () => {
expect(e('"this.foo.bar.baz"')).toEqual(`"this.foo.bar.baz"`);
});
test('works for string with double quotes #2', () => {
expect(e('this.foo.bar"baz')).toEqual(`"this.foo.bar\\"baz"`);
});
test('works for strings with template literals', () => {
expect(e('this.foo.bar`baz`')).toEqual(`"this.foo.bar\`baz\`"`);
});
test('works for strings like numbers', () => {
expect(e('123')).toEqual(`"123"`);
});
test('works for strings like numbers #2', () => {
expect(e('123.123')).toEqual(`"123.123"`);
});
test('works for strings like numbers #3', () => {
expect(e('123.123.123')).toEqual(`"123.123.123"`);
});
test('throw error if input is not a string', () => {
expect(() => e(123)).toThrow('Not a string')
});
test('skip already escaped strings', () => {
expect(e('"this.foo.bar.baz"')).toEqual(`"this.foo.bar.baz"`);
});
});


describe('toSafeJSPath', () => {
test('works for classic case', () => {
Expand Down
19 changes: 9 additions & 10 deletions plugins/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,16 @@ export function resetContextCounter() {
}

export function escapeString(str: string) {
const lines = str.split('\n');
if (lines.length === 1) {
if (str.startsWith("'")) {
return str;
} else if (str.startsWith('"')) {
return str;
} else {
return `"${str}"`;
if (typeof str !== 'string') {
throw new Error('Not a string');
}
try {
if (typeof JSON.parse(str) !== 'string') {
return JSON.stringify(str);
}
} else {
return `\`${str}\``;
return JSON.stringify(JSON.parse(str));
} catch (e) {
return JSON.stringify(str);
}
}

Expand Down
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
Loading

0 comments on commit 7c1b5c8

Please sign in to comment.