DrSensor / nusa

incremental runtime that bring both simplicity and power into webdev (buildless, cross-language, data-driven)
MIT License
4 stars 0 forks source link

Prerender (SSR/SSG) #38

Open DrSensor opened 1 year ago

DrSensor commented 1 year ago
import { href } from "w/prerender"

@href(import.meta.url)
export default class Money {
  static sum = 0
  /** @type {string} */
  #currency
  get currency() { return this.#currency }
  set currency(value) {
    this.#currency = `$${this.value}`
  }
  accessor count = 0
  increment() {
    this.currency = ++this.count
    Money.sum += sum
  }
}
import { href } from "w/prerender"
import USD from "./usd.js"

@href(import.meta.url)
export default class Money extends USD {
  /** @type {string} */
  set currency(value) {
    super.currency = `¥${this.count}`
  }
  async toUSD() {
    this.currency = await fetch(`https://trade.xyz/v1/convert/from=jpy&to=usd&price=${this.count}`)
  }
}
export { default as USD } from "../usd.js"
export { default as JPY } from "../jpy.js"
import { bind, global } from "w/prerender"
import { USD, JPY } from "./modules/money.js"

// automatic <render-scope>
async function Item({ id }) {
  const dollar = <link href={USD}/>
  const yen = <link href={JPY}/>
  // const money = bind() // bind all module default classes as long as <link href/> has been called inside this function. However, all <link> followed by bind() must always be on top

  const item = await fetch(`https://api.mystore.com/v1/inventory?item=${id}`)

  const money = bind(dollar, yen) // <link> doesn't need to be on top

  dollar.count = yen.count = item.price;
  dollar[global].sum = item.total * item.price;

  // run script every time static sum has changed
  <script>console.log({money`^sum`})</script>

  return <label name="dollar">
    price {money`count->toUSD currency`}
    <button on:click={money`increment`}>increment</button>
  </label>
}

stream html on deadline

class DeadlineController extends AbortController {
  abort() { // cancel both streaming and ongoing fetch
    super.abort()
    this.cancel()
  }
  cancel() {/*cancel streaming*/}
}
export default new DeadlineController()
import { render } from "w/prerender"
import deadline from "w/prerender/deadline"

const IPAddress = async () => {
  const resp = await fetch("//ipfy.com", { signal: deadline.signal })
  const addr = await resp.text()
  return <span>Your IP is {addr}</span>
}

Page.href = "user/dashboard"
function Page() {/*use <IPAddress />*/}

// edge/serverless function
export default(request) {
  if (no_AllowStreamingHeader_in(request))
    deadline.cancel()
  if (has_DontShowIpQuery_in(request))
    deadline.abort()
  return render(request, [<Page/>], {
    limit: { await: .1*s, response: 5*kb }, // each stream are buffered and has a limit
    deadline: 200*ms, timeout: 1*s,
  }) // if render not completed in 200ms (cause by async-await)
  // then switch to stream mode for 1s
  // and send the remaining html via SSE
}

timing boundary

Sometimes you want to control what to render when static deadline or stream timeout expired.

\<Deadline> boundary

<Deadline fallback={
  <>
    can't get IP address
    probably service timeout
  </>
}>
  <IPAddress/>
</Deadline>

which can be simplified as

<IPAddress
  fallback:deadline={<>
    can't get IP address
    probably service timeout
  </>}
}/>

\<Timeout> boundary

<Timeout fallback={
  <>server timeout</>
}>
  <IPAddress/>
</Timeout>

which can be simplified as

<IPAddress
  fallback:timeout={
    <>server timeout</>
}}/>

Island

Nothing special here

export const IPAddress = async (props) => {
  const resp = await fetch("//ipfy.com", { signal: deadline.signal })
  const addr = await resp.text()
  return <span {...props}>Your IP is {addr}</span>
}
import { href } from "w/prerender"
import { build } from "w/std"
import { IPAddress } from "./ipaddr.jsx"

@href(import.meta.url)
export default class {
  @build(IPAddress) ip
}
import { bind } from "w/prerender"
import Dashboard from "../dashboard.js"

function Page() {
  <link href={Dashboard}/>

  return <div class="dashboard">
    <slot name={bind`ip`}>Loading…</slot>
    …
  <div>
}

auto Island

But it can be special

export const IPAddress = async (props) => {
  const resp = await fetch("//ipfy.com", { signal: deadline.signal })
  const addr = await resp.text()
  return <span {...props}>Your IP is {addr}</span>
}
+ IPAddress.href = import.meta.url
import { IPAddress } from "../ipaddr.jsx"

function Page() {
  return <div class="dashboard">
    <slot name="ip">Loading…</slot>
    <IPAddress slot="ip"/>
    …
  <div>
}

The trick is simple: if function.href.endsWith(".js") then import(href) on the client, get the function name from Comment string, and finally render/build that function.

\<Static> boundary

Sometimes you want to keep it on the server and render it as static html

import { IPAddress } from "../ipaddr.jsx"

function Page() {
  return <div class="dashboard">
    <slot name="ip">Loading…</slot>
+   <Static>
      <IPAddress slot="ip"/>
+   </Static>
    …
  <div>
}

You can use timing boundary to fallback into client rendering

import { IPAddress } from "../ipaddr.jsx"

function Page() {
  return <div class="dashboard">
    <slot name="ip">Loading…</slot>
    <Static>
+     <Deadline fallback="client">    
        <IPAddress slot="ip"/>
+     </Deadline>
    </Static>
    …
  <div>
}

which can be simplified as

<Static.Deadline>    
  <IPAddress slot="ip"/>
</Static.Deadline>

or

<IPAddress fallback:deadline="client" slot="ip"/>

function vs =>

EXCEPT function *generator (doesn't matter if it async or not)

Generator functions are special because it's dynamic Component. Each yield and return doesn't wrap the value in <render-scope>. To scope/isolate the DOM, you must return a function.

async function *Component() {
  yield <span class="spin">Loading…</span> // not isolated
  const data = await service()
  yield function() { // <render-scope>
    const c = <link href={Class}/>
    [c, c[global]].forEach(each => Object.assign(each, data))
    return <form>
      <input type="text" name="name" value={bind`^name`} on:change={bind`update`}/>
      <button type="submit" name="count" value={bind`count`}>Submit</button>
    </form>
  }
}

use(Class) vs <link href={Class}/>

TL;DR

  • <link/> - only for SSR/SSG and it have safeguard
  • use() - primarily for Island but can be used in SSR/SSG via \<Static> boundary with 1 caveat: no safeguard