Skip to content

Commit

Permalink
code
Browse files Browse the repository at this point in the history
  • Loading branch information
arthurfiorette committed Feb 8, 2024
1 parent 5e7ffe9 commit 20a9db3
Show file tree
Hide file tree
Showing 7 changed files with 673 additions and 79 deletions.
81 changes: 81 additions & 0 deletions .html
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<html>
<body>
<template id="B:1" data-gf></template>
</body>
</html>
<script id="kita-html-suspense">
/*! Apache-2.0 https://kita.js.org */ function $KITA_RC(i) {
var d = document,
q = d.querySelector.bind(d),
v = q('div[id="B:' + i + '"][data-sf]'),
t = q('template[id="N:' + i + '"][data-sr]'),
s = q('script[id="S:' + i + '"][data-ss]'),
f = d.createDocumentFragment(),
c,
j,
r;
if (t && v && s) {
while ((c = t.content.firstChild)) f.appendChild(c);
v.parentNode.replaceChild(f, v);
t.remove();
s.remove();
r = d.querySelectorAll('template[id][data-sr]');
do {
j = 0;
for (c = 0; c < r.length; c++)
if (r[c] != t) j = $KITA_RC(r[c].id.slice(2)) ? !0 : j;
} while (j);
return !0;
}
}
</script>

<template id="N:1" data-gr>Slept for 0ms</template>
<script id="S:1" data-gs>
$KITA_RC(1);
</script>

<template id="N:1" data-gr>Slept for 200ms</template>
<script id="S:1" data-gs>
$KITA_RC(1);
</script>

<template id="N:1" data-gr>Slept for 400ms</template>
<script id="S:1" data-gs>
$KITA_RC(1);
</script>

<template id="N:1" data-gr>Slept for 600ms</template>
<script id="S:1" data-gs>
$KITA_RC(1);
</script>

<template id="N:1" data-gr>Slept for 800ms</template>
<script id="S:1" data-gs>
$KITA_RC(1);
</script>

<template id="N:1" data-gr>Slept for 1000ms</template>
<script id="S:1" data-gs>
$KITA_RC(1);
</script>

<template id="N:1" data-gr>Slept for 1200ms</template>
<script id="S:1" data-gs>
$KITA_RC(1);
</script>

<template id="N:1" data-gr>Slept for 1400ms</template>
<script id="S:1" data-gs>
$KITA_RC(1);
</script>

<template id="N:1" data-gr>Slept for 1600ms</template>
<script id="S:1" data-gs>
$KITA_RC(1);
</script>

<template id="N:1" data-gr>Slept for 1800ms</template>
<script id="S:1" data-gs>
$KITA_RC(1);
</script>
63 changes: 30 additions & 33 deletions examples/suspense-server.tsx
Original file line number Diff line number Diff line change
@@ -1,55 +1,52 @@
import http from 'http';
import { setTimeout } from 'timers/promises';
import Html, { PropsWithChildren } from '../index';
import { Suspense, renderToStream } from '../suspense';

async function SleepForMs({ ms, children }: PropsWithChildren<{ ms: number }>) {
await setTimeout(ms * 2);
return Html.contentsToString([children || String(ms)]);
}
import Html from '../index';
import { SuspenseGenerator, pipeHtml } from '../suspense';

function renderLayout(rid: number | string) {
return (
<html>
<div>
{Array.from({ length: 5 }, (_, i) => (
<Suspense rid={rid} fallback={<div>{i} FIuter</div>}>
<div>Outer {i}!</div>

<SleepForMs ms={i % 2 === 0 ? i * 2500 : i * 5000}>
<Suspense rid={rid} fallback={<div>{i} FInner!</div>}>
<SleepForMs ms={i * 5000}>
<div>Inner {i}!</div>
</SleepForMs>
</Suspense>
</SleepForMs>
</Suspense>
))}
</div>
<body>
<SuspenseGenerator rid={rid} source={SleepGenerator()} />
</body>
</html>
);
}

async function* SleepGenerator() {
for (let i = 0; i < 10; i++) {
await setTimeout(i * 200);
yield 'Slept for ' + i * 200 + 'ms';
}
}

SUSPENSE_ROOT.enabled = true;

http
.createServer((req, response) => {
.createServer((req, rep) => {
// This simple webserver only has a index.html file
if (req.url !== '/' && req.url !== '/index.html') {
response.end();
rep.statusCode = 404;
rep.statusMessage = 'Not Found';
rep.end();
return;
}

// ⚠️ Charset utf8 is important to avoid old browsers utf7 xss attacks
response.setHeader('Content-Type', 'text/html; charset=utf-8');
// Creates the request map
const id = SUSPENSE_ROOT.requestCounter++;
SUSPENSE_ROOT.requests.set(id, {
stream: new WeakRef(rep),
running: 0,
sent: false
});

// Creates the html stream
const htmlStream = renderToStream(renderLayout);
// ⚠️ Charset utf8 is important to avoid old browsers utf7 xss attacks
rep.writeHead(200, 'OK', { 'content-type': 'text/html; charset=utf-8' });

// Pipes it into the response
htmlStream.pipe(response);
rep.flushHeaders;

// If its an express or fastify server, just use
// response.type('text/html; charset=utf-8').send(htmlStream);
pipeHtml(renderLayout(id), rep, id);
})
.listen(8080, () => {
console.log('Listening to http://localhost:8080');
console.log('Listening to http://localhost:8080 ' + Math.random());
});
76 changes: 53 additions & 23 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ function toKebabCase(camel) {

const length = camel.length;

let start = 0;
let end = 0;
let kebab = '';
let prev = true;
let curr = isUpper(camel, 0);
let next;
let start = 0,
end = 0,
kebab = '',
prev = true,
curr = isUpper(camel, 0),
next;

for (; end < length; end++) {
next = isUpper(camel, end + 1);
Expand All @@ -46,6 +46,27 @@ function toKebabCase(camel) {
return kebab;
}

/** @type {import('.').escape} */
function escape(strings, ...values) {
const stringsLength = strings.length,
valuesLength = values.length;

let index = 0,
result = '';

for (; index < stringsLength; index++) {
result += strings[index];

if (index < valuesLength) {
result += values[index];
}
}

// Escape the entire string at once.
// This is faster than escaping each piece individually.
return escapeHtml(result);
}

/** @type {import('.').escapeHtml} */
let escapeHtml = function (value) {
if (typeof value !== 'string') {
Expand All @@ -59,10 +80,10 @@ let escapeHtml = function (value) {
}

const length = value.length;
let escaped = '';

let start = 0;
let end = 0;
let escaped = '',
start = 0,
end = 0;

// Escapes double quotes to be used inside attributes
// Faster than using regex
Expand Down Expand Up @@ -139,8 +160,8 @@ function styleToString(style) {

const length = style.length;

let escaped = '';
let start = 0;
let escaped = '',
start = 0;

// Escapes double quotes to be used inside attributes
// Faster than using regex
Expand All @@ -158,12 +179,15 @@ function styleToString(style) {
return escaped;
}

const keys = Object.keys(style);
const length = keys.length;
const keys = Object.keys(style),
length = keys.length;

let key, value, end, start;
let index = 0;
let result = '';
let key,
value,
end,
start,
index = 0,
result = '';

for (; index < length; index++) {
key = keys[index];
Expand Down Expand Up @@ -217,9 +241,15 @@ function attributesToString(attributes) {
const keys = Object.keys(attributes);
const length = keys.length;

let key, value, type, end, start, classItems, valueLength;
let result = '';
let index = 0;
let key,
value,
type,
end,
start,
classItems,
valueLength,
result = '',
index = 0;

for (; index < length; index++) {
key = keys[index];
Expand Down Expand Up @@ -345,10 +375,10 @@ function attributesToString(attributes) {
* @returns {any}
*/
function contentsToString(contents, escape) {
let length = contents.length;
let result = '';
let content;
let index = 0;
let length = contents.length,
result = '',
content,
index = 0;

for (; index < length; index++) {
content = contents[index];
Expand Down
71 changes: 57 additions & 14 deletions suspense.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,27 +65,37 @@ export type RequestData = {
*/
export function Suspense(props: SuspenseProps): JSX.Element;

/**
* A component that keeps injecting html while the generator is running.
*
* The `rid` prop is the one {@linkcode renderToStream} returns, this way the suspense
* knows which request it belongs to.
*/
export function SuspenseGenerator<T>(props: SuspenseGeneratorProps<T>): JSX.Element;

/**
* Transforms a component tree who may contain `Suspense` components into a stream of
* HTML.
*
* @example
*
* ```tsx
* // Simple nodejs webserver to render html http.createServer((req, res)
* => { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
*
* const stream = renderToStream(r => <AppWithSuspense rid={r} />)
*
* stream.pipe(res)
*
* }).listen(1227)
*
* // Prints out the rendered stream const stream = renderToStream(r => <AppWithSuspense
* rid={r} />)
*
* For await (const html of stream) { console.log(html.toString()) }
*
* // Simple nodejs webserver to render html
*
* http
* .createServer((req, res) => {
* res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
* const stream = renderToStream((r) => <AppWithSuspense rid={r} />);
* stream.pipe(res);
* })
* .listen(1227);
*
* // Prints out the rendered stream
* const stream = renderToStream((r) => <AppWithSuspense rid={r} />);
*
* for await (const html of stream) {
* console.log(html.toString());
* }
* ```
*
* @param factory The component tree to render.
Expand Down Expand Up @@ -161,6 +171,39 @@ export interface SuspenseProps {
catch?: JSX.Element | ((error: unknown) => JSX.Element);
}

export interface SuspenseGeneratorProps<T> {
/** The request id is used to identify the request for this suspense. */
rid: number | string;

/** The request id is used to identify the request for this suspense. */
source:
| AsyncGenerator<PromiseLike<T> | T, void>
| AsyncIterable<PromiseLike<T> | T>
| Generator<PromiseLike<T> | T, void>
| Iterable<PromiseLike<T> | T>;

/**
* The cork threshold for batching components during rendering. Batching rows instead of
* streaming each one reduces the network overhead by using fewer TCP round-trips to
* send the same amount of data. However, it may increase the time to first byte, as the
* server will wait for the entire batch to be ready before sending it.
*
* Adjusting this threshold allows controlling the number of items a stream should wait
* for before uncorking, optimizing the balance between network efficiency and time to
* first byte.
*
* @default 0 (no batching)
*/
corkThreshold?: number;

/**
* The map function to render each component as soon as a new value is yielded.
*
* @default value => value
*/
map?: (value: T) => JSX.Element | PromiseLike<JSX.Element>;
}

/**
* Internal function used to pipe the HTML to the stream. Users should not use this
* function directly. This function assumes that the stream is available and the request
Expand Down
Loading

0 comments on commit 20a9db3

Please sign in to comment.