Skip to content

Commit

Permalink
SSR - initial streaming implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
LankyMoose committed May 1, 2023
1 parent a4544c5 commit 9b3ecee
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 68 deletions.
21 changes: 21 additions & 0 deletions apps/ssr/src/Template.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as Cinnabun from "cinnabun"
import { GenericComponent } from "cinnabun/types"

export const Template = (App: { (): GenericComponent }) => {
return (
<>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>SSR App</title>
<link rel="stylesheet" href="/static/index.css" />
</head>

<body style={{ background: "#222", color: "#eee" }}>
<div id="app">
<App />
</div>
</body>
</>
)
}
7 changes: 6 additions & 1 deletion apps/ssr/src/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { Hydration } from "cinnabun/hydration"
import { App } from "../App"
import { Template } from "../Template"
import { SSRProps } from "cinnabun/src/types"
import "./index.css"
import { createLiveSocket } from "./liveSocket"
import { Cinnabun } from "cinnabun"

if ("__cbData" in window) {
Cinnabun.registerRuntimeServices(createLiveSocket())
Hydration.hydrate(App(), window.__cbData as SSRProps)
// streaming
Hydration.hydrate(Template(App), window.__cbData as SSRProps)
// non-streaming
//Hydration.hydrate(App(), window.__cbData as SSRProps)

//TestSerialization()
}

Expand Down
40 changes: 34 additions & 6 deletions apps/ssr/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import websocket from "@fastify/websocket"
import fs from "fs"
import path from "path"

import { SSR } from "cinnabun/ssr"
import { SSR, SSRConfig } from "cinnabun/ssr"
import { App } from "../App"
import { Cinnabun } from "cinnabun"
import { socketHandler } from "./socket"
import { configureAuthRoutes } from "./auth"
import { configureChatRoutes } from "./chat"
import { Template } from "../Template"

declare module "fastify" {
export interface FastifyInstance {
Expand Down Expand Up @@ -101,15 +102,42 @@ configureAuthRoutes(app)
configureChatRoutes(app)

app.get("/*", { onRequest: [app.verify] }, async (req, res) => {
console.time("render time")
const instance = new Cinnabun()
instance.setServerRequestData({
const cinnabunInstance = new Cinnabun()
cinnabunInstance.setServerRequestData({
path: req.url,
data: { user: req.user },
})

const { html, componentTree } = await SSR.serverBake(App(), instance)
console.timeEnd("render time")
const config: SSRConfig = {
cinnabunInstance,
stream: res.raw,
}

if (config.stream) {
res.header("Content-Type", "text/html").status(200)
res.header("Transfer-Encoding", "chunked")
res.raw.write("<!DOCTYPE html><html>")
}

const { html, componentTree } = await SSR.serverBake(
config.stream ? Template(App) : App(),
config
)

if (config.stream) {
res.raw.write(`
<script id="server-props">
window.__cbData = {
root: document.documentElement,
component: ${JSON.stringify(componentTree)}
}
</script>
<script src="/static/index.js"></script>
`)
res.raw.write("</html>")
res.raw.end()
return
}

res
.code(200)
Expand Down
63 changes: 30 additions & 33 deletions packages/lib/src/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,7 @@ import { matchPath } from "."
import { Signal, Component } from ".."
import { Cinnabun } from "../cinnabun"
import { DomInterop } from "../domInterop"
import {
ComponentChild,
ComponentSubscription,
PropsSetter,
RouteProps,
} from "../types"
import { ComponentChild, PropsSetter, RouteProps } from "../types"

class RouteComponent extends Component<any> {
constructor(path: string, component: ComponentChild) {
Expand All @@ -26,7 +21,7 @@ class RouteComponent extends Component<any> {
}

class RouterComponent extends Component<any> {
constructor(subscription: ComponentSubscription, children: RouteComponent[]) {
constructor(store: Signal<string>, children: RouteComponent[]) {
if (children.some((c) => !(c instanceof RouteComponent)))
throw new Error("Must provide Route as child of Router")

Expand All @@ -36,6 +31,33 @@ class RouterComponent extends Component<any> {
(a as RouteComponent).props.pathDepth
)
})

const subscription = (_: PropsSetter, self: Component<any>) => {
return store.subscribe((val) => {
let len = self.children.length
while (len--) {
const rc = self.children[len] as RouteComponent
rc.props.render = false
rc.props.params = {}
}
if (Cinnabun.isClient) DomInterop.unRender(self)

for (let i = 0; i < self.children.length; i++) {
const c = self.children[i] as RouteComponent
const matchRes = (self as RouterComponent).matchRoute(
c,
useRequestData<string>(self, "path", val)!
)
if (matchRes.routeMatch) {
c.props.render = !!matchRes.routeMatch
c.props.params = matchRes.params ?? {}
break
}
}
if (Cinnabun.isClient && self.mounted) DomInterop.reRender(self)
})
}

super("", { subscription, children })
}

Expand Down Expand Up @@ -71,30 +93,5 @@ export const Router = (
{ store }: { store: Signal<string> },
children: RouteComponent[]
) => {
const subscription = (_: PropsSetter, self: Component<any>) => {
return store.subscribe((val) => {
let len = self.children.length
while (len--) {
const rc = self.children[len] as RouteComponent
rc.props.render = false
rc.props.params = {}
}
if (Cinnabun.isClient) DomInterop.unRender(self)

for (let i = 0; i < self.children.length; i++) {
const c = self.children[i] as RouteComponent
const matchRes = (self as RouterComponent).matchRoute(
c,
useRequestData<string>(self, "path", val)!
)
if (matchRes.routeMatch) {
c.props.render = !!matchRes.routeMatch
c.props.params = matchRes.params ?? {}
break
}
}
if (Cinnabun.isClient && self.mounted) DomInterop.reRender(self)
})
}
return new RouterComponent(subscription, children)
return new RouterComponent(store, children)
}
82 changes: 54 additions & 28 deletions packages/lib/src/ssr.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Writable } from "stream"
import { Cinnabun } from "./cinnabun"
import { Component } from "./component"
import { Signal } from "./signal"
Expand All @@ -15,18 +16,27 @@ type Accumulator = {
promiseQueue: Promise<any>[]
}

export type SSRConfig = {
cinnabunInstance: Cinnabun
useFileBasedRouting?: boolean
stream?: Writable
}

export class SSR {
static async serverBake(
app: Component<any>,
cbInstance: Cinnabun
config: SSRConfig
): Promise<ServerBakeResult> {
console.time("render time")
const accumulator: Accumulator = {
html: [],
promiseQueue: [],
}

const serialized = await SSR.serialize(accumulator, app, cbInstance)
const serialized = await SSR.serialize(accumulator, app, config)
// resolve promises, components should replace their corresponding item in the html arr

console.timeEnd("render time")
return {
componentTree: { children: [serialized], props: {} },
html: accumulator.html.join(""),
Expand Down Expand Up @@ -65,9 +75,9 @@ export class SSR {
public static async serialize(
accumulator: Accumulator,
component: GenericComponent,
cbInstance: Cinnabun
config: SSRConfig
): Promise<SerializedComponent> {
component.cbInstance = cbInstance
component.cbInstance = config.cinnabunInstance
component.applyBindProps()

const res: SerializedComponent = {
Expand Down Expand Up @@ -97,7 +107,7 @@ export class SSR {
accumulator,
component,
shouldRender,
cbInstance
config
)
return {
props: SSR.serializeProps(component),
Expand All @@ -110,48 +120,60 @@ export class SSR {
if (component.tag === "svg") return SSR.serializeSvg(component)

const renderClosingTag =
["br", "hr", "img", "input"].indexOf(component.tag.toLowerCase()) === -1

accumulator.html.push(
`<${component.tag}${Object.entries(rest ?? {})
.filter(
([k]) =>
k !== "style" && !k.startsWith("bind:") && !k.startsWith("on")
)
.map(
([k, v]) =>
` ${SSR.serializePropName(k)}="${component.getPrimitive(v)}"`
)
.join("")}${renderClosingTag ? "" : "/"}>`
)
["br", "hr", "img", "input", "link", "meta"].indexOf(
component.tag.toLowerCase()
) === -1

const html = `<${component.tag}${Object.entries(rest ?? {})
.filter(
([k]) => k !== "style" && !k.startsWith("bind:") && !k.startsWith("on")
)
.map(
([k, v]) =>
` ${SSR.serializePropName(k)}="${component.getPrimitive(v)}"`
)
.join("")}${renderClosingTag ? "" : "/"}>`

SSR.render(html, config, accumulator)

res.children = await SSR.serializeChildren(
accumulator,
component,
shouldRender,
cbInstance
config
)

if (renderClosingTag) accumulator.html.push(`</${component.tag}>`)
if (renderClosingTag) {
const cTag = `</${component.tag}>`
SSR.render(cTag, config, accumulator)
}
return res
}

static render(content: string, config: SSRConfig, accumulator: Accumulator) {
if (config.stream) {
config.stream.write(content)
} else {
accumulator.html.push(content)
}
}

public static async serializeChildren(
accumulator: Accumulator,
component: GenericComponent,
shouldRender: boolean,
cbInstance: Cinnabun
config: SSRConfig
): Promise<SerializedComponent[]> {
const res: SerializedComponent[] = []
for await (const c of component.children) {
if (typeof c === "string" || typeof c === "number") {
if (shouldRender) accumulator.html.push(c.toString())
if (shouldRender) SSR.render(c.toString(), config, accumulator)
res.push({ children: [], props: {} })
continue
}

if (c instanceof Signal) {
if (shouldRender) accumulator.html.push(c.value)
if (shouldRender) SSR.render(c.value.toString(), config, accumulator)
res.push({ children: [], props: {} })
continue
}
Expand All @@ -160,7 +182,7 @@ export class SSR {
//instead of crashing from trying to serialize the object as a component

//@ts-ignore
if (shouldRender) accumulator.html.push(c.toString())
if (shouldRender) SSR.render(c.toString(), config, accumulator)
res.push({ children: [], props: {} })
continue
}
Expand All @@ -173,17 +195,17 @@ export class SSR {
const val = c(...component.childArgs)
if (val instanceof Component) {
val.parent = component
const sc = await SSR.serialize(accumulator, val, cbInstance)
const sc = await SSR.serialize(accumulator, val, config)
res.push(sc)
} else if (typeof val === "string" || typeof val === "number") {
if (shouldRender) accumulator.html.push(val.toString())
if (shouldRender) SSR.render(val.toString(), config, accumulator)
res.push({ children: [], props: {} })
continue
}
continue
}

const sc = await SSR.serialize(accumulator, c, cbInstance)
const sc = await SSR.serialize(accumulator, c, config)
res.push(sc)
}
return res
Expand All @@ -203,3 +225,7 @@ export function useRequestData<T>(
? fallback
: self.cbInstance?.getServerRequestData<T>(requestDataPath)
}

export const FileRouter = () => {
return new Component("")
}

0 comments on commit 9b3ecee

Please sign in to comment.