support SSE for ServerLoad (#10)

This commit is contained in:
Samuel Colvin
2023-11-28 11:41:34 +00:00
committed by GitHub
parent c589e30a61
commit e2422e5a56
5 changed files with 84 additions and 27 deletions

View File

@@ -1,7 +1,7 @@
import { FC, useContext, useEffect, useState } from 'react'
import { FC, useCallback, useContext, useEffect, useState } from 'react'
import { ErrorContext } from '../hooks/error'
import { useRequest } from '../tools'
import { useRequest, useSSE } from '../tools'
import { DefaultLoading } from '../DefaultLoading'
import { ConfigContext } from '../hooks/config'
import { PageEvent, usePageEventListen } from '../events'
@@ -14,26 +14,30 @@ export interface ServerLoadProps {
path: string
components?: FastProps[]
loadTrigger?: PageEvent
sse?: boolean
}
export const ServerLoadComp: FC<ServerLoadProps> = ({ path, components, loadTrigger }) => {
export const ServerLoadComp: FC<ServerLoadProps> = ({ path, components, loadTrigger, sse }) => {
if (components) {
return <ServerLoadDefer path={path} components={components} loadTrigger={loadTrigger} />
return <ServerLoadDefer path={path} components={components} loadTrigger={loadTrigger} sse={sse} />
} else if (sse) {
return <ServerLoadSSE path={path} />
} else {
return <ServerLoadDirect path={path} />
return <ServerLoadFetch path={path} />
}
}
const ServerLoadDefer: FC<{ path: string; components: FastProps[]; loadTrigger?: PageEvent }> = ({
const ServerLoadDefer: FC<{ path: string; components: FastProps[]; loadTrigger?: PageEvent; sse?: boolean }> = ({
components,
path,
loadTrigger,
sse,
}) => {
const { eventContext } = usePageEventListen(loadTrigger)
if (eventContext) {
return (
<EventContextProvider context={eventContext}>
<ServerLoadDirect path={path} />
{sse ? <ServerLoadSSE path={path} /> : <ServerLoadFetch path={path} />}
</EventContextProvider>
)
} else {
@@ -41,31 +45,39 @@ const ServerLoadDefer: FC<{ path: string; components: FastProps[]; loadTrigger?:
}
}
export const ServerLoadDirect: FC<{ path: string; devReload?: number }> = ({ path, devReload }) => {
export const ServerLoadFetch: FC<{ path: string; devReload?: number }> = ({ path, devReload }) => {
const [componentProps, setComponentProps] = useState<FastProps[] | null>(null)
const { error } = useContext(ErrorContext)
const { rootUrl, pathSendMode, Loading } = useContext(ConfigContext)
const url = useServerUrl(path)
const request = useRequest()
const applyContext = useEventContext()
useEffect(() => {
let fetchUrl = rootUrl
const requestPath = applyContext(path)
if (pathSendMode === 'query') {
fetchUrl += `?path=${encodeURIComponent(requestPath)}`
} else {
fetchUrl += requestPath
}
const promise = request({ url: fetchUrl })
const promise = request({ url })
promise.then(([, data]) => setComponentProps(data as FastProps[]))
return () => {
promise.then(() => null)
}
}, [rootUrl, pathSendMode, path, applyContext, request, devReload])
}, [url, request, devReload])
if (componentProps === null) {
return <Render propsList={componentProps} />
}
export const ServerLoadSSE: FC<{ path: string }> = ({ path }) => {
const [componentProps, setComponentProps] = useState<FastProps[] | null>(null)
const url = useServerUrl(path)
const onMessage = useCallback((data: any) => setComponentProps(data as FastProps[]), [])
useSSE(url, onMessage)
return <Render propsList={componentProps} />
}
const Render: FC<{ propsList: FastProps[] | null }> = ({ propsList }) => {
const { error } = useContext(ErrorContext)
const { Loading } = useContext(ConfigContext)
if (propsList === null) {
if (error) {
return <></>
} else if (Loading) {
@@ -74,6 +86,18 @@ export const ServerLoadDirect: FC<{ path: string; devReload?: number }> = ({ pat
return <DefaultLoading />
}
} else {
return <AnyCompList propsList={componentProps} />
return <AnyCompList propsList={propsList} />
}
}
function useServerUrl(path: string): string {
const { rootUrl, pathSendMode } = useContext(ConfigContext)
const applyContext = useEventContext()
const requestPath = applyContext(path)
if (pathSendMode === 'query') {
return `${rootUrl}?path=${encodeURIComponent(requestPath)}`
} else {
return rootUrl + requestPath
}
}

View File

@@ -1,7 +1,7 @@
import { useContext, useEffect, useState } from 'react'
import { LocationContext } from './hooks/locationContext'
import { ServerLoadDirect } from './components/ServerLoad'
import { ServerLoadFetch } from './components/ServerLoad'
import { loadEvent, LoadEventDetail } from './events'
export function FastUIController() {
@@ -24,5 +24,5 @@ export function FastUIController() {
}
}, [fullPath])
return <ServerLoadDirect path={path} devReload={reloadValue} />
return <ServerLoadFetch path={path} devReload={reloadValue} />
}

View File

@@ -1,4 +1,4 @@
import { useCallback, useContext } from 'react'
import { useCallback, useContext, useEffect } from 'react'
import { ErrorContext } from './hooks/error'
@@ -18,6 +18,24 @@ export function useRequest(): (args: Request) => Promise<[number, any]> {
)
}
export function useSSE(url: string, onMessage: (data: any) => void): void {
const { setError } = useContext(ErrorContext)
useEffect(() => {
const source = new EventSource(url)
source.onmessage = (e) => {
const data = JSON.parse(e.data)
onMessage(data)
}
source.onerror = (e) => {
setError({ title: 'SSE Error', description: (e as any)?.message })
}
return () => {
source.close()
}
}, [url, setError, onMessage])
}
interface Request {
url: string
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'

View File

@@ -2,7 +2,7 @@ from __future__ import annotations as _annotations
import asyncio
from collections import defaultdict
from datetime import date
from datetime import date, datetime
from enum import StrEnum
from typing import Annotated, Literal
@@ -15,6 +15,7 @@ from fastui.forms import FormFile, FormResponse, SelectSearchResponse, fastui_fo
from httpx import AsyncClient
from pydantic import BaseModel, Field, SecretStr, field_validator
from pydantic_core import PydanticCustomError
from sse_starlette import EventSourceResponse
# app = FastAPI()
app = dev_fastapi_app()
@@ -80,6 +81,7 @@ assert x + y == 3
),
],
),
c.ServerLoad(path='/sse', sse=True),
c.Modal(
title='Static Modal',
body=[c.Paragraph(text='This is some static content in a modal.')],
@@ -275,5 +277,17 @@ def form_content(kind: Literal['one', 'two', 'three']):
@app.post('/api/form')
async def form_post(form: Annotated[MyFormModel, fastui_form(MyFormModel)]) -> FormResponse:
debug(form)
return FormResponse(event=GoToEvent(url='/'))
async def sse_generator():
while True:
d = datetime.now()
m = FastUI(root=[c.Div(components=[c.Text(text=f'Time {d:%H:%M:%S.%f}'[:-4])], class_name='font-monospace')])
yield dict(data=m.model_dump_json(by_alias=True))
await asyncio.sleep(0.15)
@app.get('/api/sse', response_class=EventSourceResponse)
async def sse_experiment():
return EventSourceResponse(sse_generator())

View File

@@ -149,6 +149,7 @@ class ServerLoad(pydantic.BaseModel, extra='forbid'):
path: str
load_trigger: events.PageEvent | None = pydantic.Field(default=None, serialization_alias='loadTrigger')
components: list[AnyComponent] | None = None
sse: bool | None = None
type: typing.Literal['ServerLoad'] = 'ServerLoad'