magic: added a way for running async functions in the top level jsx code

pull/1/head
parent 71280fba5c
commit fab8398a7a

@ -1,20 +1,26 @@
import renderToString from 'preact-render-to-string'
// import { App } from './App'
import { MetadataContext, ServerContext } from './hooks'
import { ServerAsyncCallbacksContext, DatabaseContext, MetadataContext, ServerContext } from './hooks'
import { RenderedPage } from '../shared/ssr'
import { RenderedPage, ServerAsyncCallback } from '../shared/ssr'
import { App } from './App'
import { DatabaseConnection } from '../shared/database'
export default (url: string): RenderedPage => {
export default (url: string, db: DatabaseConnection): RenderedPage => {
const metadata = {}
const asyncCallbacks: ServerAsyncCallback[] = []
const html = renderToString(
<MetadataContext.Provider value={metadata}>
<ServerContext.Provider value={true}>
<App url={url} />
<DatabaseContext.Provider value={db}>
<ServerAsyncCallbacksContext.Provider value={asyncCallbacks}>
<App url={url} />
</ServerAsyncCallbacksContext.Provider>
</DatabaseContext.Provider>
</ServerContext.Provider>
</MetadataContext.Provider>
)
return { html, metadata }
return { html, metadata, asyncCallbacks }
}

@ -1,7 +1,9 @@
import { StateUpdater, useEffect, useState } from 'preact/hooks'
import { StateUpdater, useContext, useEffect, useState } from 'preact/hooks'
import { createContext } from 'preact'
import { prependBaseUrl } from '../shared/utils'
import { DatabaseConnection } from '../shared/database'
import { ServerAsyncCallback } from '../shared/ssr'
type Metadata = {
title?: string
@ -13,6 +15,8 @@ export const MetadataContext = createContext<Metadata>({})
export const ServerContext = createContext<boolean>(false)
export const ClientContext = createContext<boolean>(false)
export const DatabaseContext = createContext<DatabaseConnection | null>(null)
type RefreshFunction = () => AbortController
type HeuristicStateUpdater<S> = StateUpdater<S>
@ -73,3 +77,10 @@ export const useListResource = <T,>(
return [list, refreshList, setItemHeuristic, setListHeuristic]
}
export const ServerAsyncCallbacksContext = createContext<ServerAsyncCallback[]>([])
export const useServerAsyncCallback = (fn: () => Promise<void>) => {
const registeredAsyncCallbacks = useContext(ServerAsyncCallbacksContext)
registeredAsyncCallbacks.push(fn)
}

@ -7,7 +7,7 @@ import { Header } from '../components/Header'
import { MarkdownEditor } from '../components/MarkdownEditor'
import { Problem } from '../components/Problem'
import { Solution } from '../components/Solution'
import { MetadataContext, useListResource, useResource } from '../hooks'
import { MetadataContext, useListResource, useResource, ServerContext, DatabaseContext, useServerAsyncCallback } from '../hooks'
import { useCurrentUser } from '../hooks/useCurrentUser'
type RouteProps = {
@ -16,8 +16,16 @@ type RouteProps = {
export const ProblemPage = ({ id }: RouteProps) => {
const metadata = useContext(MetadataContext)
metadata.title = `Problem ${id}`
metadata.description = 'Bacheca di problemi del PHC'
const db = useContext(DatabaseContext)
if (db) {
useServerAsyncCallback(async () => {
const problem = await db.getProblem(id)
if (problem) {
metadata.title = `PHC Problemi | ${problem.title}`
metadata.description = problem.content
}
})
}
const [user] = useCurrentUser()
@ -25,7 +33,7 @@ export const ProblemPage = ({ id }: RouteProps) => {
const [problem, refreshProblem] = useResource<ProblemModel | null>(`/api/problem/${id}`, null, problem => {
if (problem === null) {
route(prependBaseUrl('/'))
route(prependBaseUrl(`/error?message=${encodeURIComponent(`Il problema "${id}" non esiste`)}`))
}
})

@ -10,6 +10,7 @@ import { createServer as createViteServer } from 'vite'
import { createApiRouter } from './server/routes'
import { RenderFunction } from './shared/ssr'
import { DatabaseConnection } from './shared/database'
// Load ".env"
dotenv.config()
@ -24,7 +25,7 @@ if (config.isDevelopment) {
console.log(`[Config] PORT = ${config.port}`)
}
async function createDevRouter() {
async function createDevRouter(db: DatabaseConnection) {
const r = express.Router()
const vite = await createViteServer({
@ -41,7 +42,8 @@ async function createDevRouter() {
// Load (to be bundled) entry point for server side rendering
const render: RenderFunction = (await vite.ssrLoadModule('./client/entry-server.tsx')).default
const { html, metadata } = render(req.originalUrl)
const { html, metadata, asyncCallbacks } = render(req.originalUrl, db)
await Promise.all(asyncCallbacks.map(fn => fn()))
const fullUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`
const metaTagsHtml =
@ -64,7 +66,7 @@ async function createDevRouter() {
return r
}
async function createProductionRouter() {
async function createProductionRouter(db: DatabaseConnection) {
// Load bundled entry point for server side rendering
// @ts-ignore
@ -75,11 +77,8 @@ async function createProductionRouter() {
const handleSSR = async (req, res) => {
const transformedTemplate = await fs.readFile(path.resolve('./dist/entry-client/index.html'), 'utf-8')
const { html, metadata } = render(req.originalUrl)
console.dir(transformedTemplate, { depth: null })
console.dir(html, { depth: null })
console.dir(metadata, { depth: null })
const { html, metadata, asyncCallbacks } = render(req.originalUrl, db)
await Promise.all(asyncCallbacks.map(fn => fn()))
const fullUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`
const metaTagsHtml =
@ -112,12 +111,14 @@ async function main() {
app.use(morgan('[Request] :method :url :status :response-time ms - :res[content-length]'))
app.use('/', await createApiRouter())
const [r, db] = await createApiRouter()
app.use('/', r)
if (config.isDevelopment) {
app.use('/', await createDevRouter())
app.use('/', await createDevRouter(db))
} else {
app.use('/', await createProductionRouter())
app.use('/', await createProductionRouter(db))
}
app.listen(config.port, () => {

@ -1,6 +1,7 @@
import crypto from 'crypto'
import { readFile, writeFile, access, constants } from 'fs/promises'
import { Database, DatabaseConnection } from '../../shared/database'
import { MetadataProps as MetaProps, Problem, ProblemId, Solution, SolutionId, User, UserId, UserRole } from '../../shared/model'
@ -53,16 +54,18 @@ function createMutex(): Mutex {
return { lock }
}
export type DatabaseConnection = {
type DatabaseInternal = {
path: string
initialValue: Database
mu: Mutex
}
export type Database = {
users: Record<string, User>
problems: Record<string, Problem>
solutions: Record<string, Solution>
export function createDatabaseWrapper(db: DatabaseInternal): DatabaseConnection {
return {
getProblem(id: string): Promise<Problem | null> {
return getProblem(db, id)
},
}
}
export function createDatabase(path: string, initialValue: Database) {
@ -73,7 +76,7 @@ export function createDatabase(path: string, initialValue: Database) {
}
}
async function withDatabase<R>({ path, initialValue, mu }: DatabaseConnection, fn: (db: Database) => R | Promise<R>): Promise<R> {
async function withDatabase<R>({ path, initialValue, mu }: DatabaseInternal, fn: (db: Database) => R | Promise<R>): Promise<R> {
const unlock: Lock = await mu.lock()
try {
@ -100,17 +103,17 @@ async function withDatabase<R>({ path, initialValue, mu }: DatabaseConnection, f
// Users
//
export const getUsers = (db: DatabaseConnection) =>
export const getUsers = (db: DatabaseInternal) =>
withDatabase(db, state => {
return Object.values(state.users)
})
export const getUser: (db: DatabaseConnection, id: string) => Promise<User | null> = (db, id) =>
export const getUser: (db: DatabaseInternal, id: string) => Promise<User | null> = (db, id) =>
withDatabase(db, state => {
return state.users[id] ?? null
})
export const createUser: (db: DatabaseConnection, user: User) => Promise<void> = (db, user) =>
export const createUser: (db: DatabaseInternal, user: User) => Promise<void> = (db, user) =>
withDatabase(db, state => {
state.users[user.id] = user
})
@ -119,7 +122,7 @@ export const createUser: (db: DatabaseConnection, user: User) => Promise<void> =
// Problems
//
export const createProblem = (db: DatabaseConnection, { title, content, createdBy }: Omit<Problem, MetaProps>): Promise<ProblemId> =>
export const createProblem = (db: DatabaseInternal, { title, content, createdBy }: Omit<Problem, MetaProps>): Promise<ProblemId> =>
withDatabase(db, state => {
const id = crypto.randomBytes(4).toString('hex') as ProblemId
@ -136,18 +139,18 @@ export const createProblem = (db: DatabaseConnection, { title, content, createdB
return id
})
export const getProblem = (db: DatabaseConnection, id: string): Promise<Problem | null> =>
export const getProblem = (db: DatabaseInternal, id: string): Promise<Problem | null> =>
withDatabase(db, state => {
const problem = (state.problems[id] ?? null) as Problem | null
return problem && !problem.deleted ? problem : null
})
export const getProblems = (db: DatabaseConnection): Promise<Problem[]> =>
export const getProblems = (db: DatabaseInternal): Promise<Problem[]> =>
withDatabase(db, state => {
return Object.values(state.problems).filter(p => !p.deleted)
})
export const updateProblem = (db: DatabaseConnection, id: ProblemId, problem: Partial<Omit<Problem, MetaProps>>): Promise<Problem> =>
export const updateProblem = (db: DatabaseInternal, id: ProblemId, problem: Partial<Omit<Problem, MetaProps>>): Promise<Problem> =>
withDatabase(db, state => {
state.problems[id] = {
...state.problems[id],
@ -157,7 +160,7 @@ export const updateProblem = (db: DatabaseConnection, id: ProblemId, problem: Pa
return state.problems[id]
})
export const deleteProblem = (db: DatabaseConnection, id: ProblemId): Promise<void> =>
export const deleteProblem = (db: DatabaseInternal, id: ProblemId): Promise<void> =>
withDatabase(db, state => {
state.problems[id].deleted = true
})
@ -167,7 +170,7 @@ export const deleteProblem = (db: DatabaseConnection, id: ProblemId): Promise<vo
//
export const createSolution = (
db: DatabaseConnection,
db: DatabaseInternal,
{ sentBy, forProblem, content }: Omit<Solution, MetaProps | 'status' | 'visible'>
): Promise<SolutionId> =>
withDatabase(db, state => {
@ -188,25 +191,25 @@ export const createSolution = (
return id
})
export const getSolution = (db: DatabaseConnection, id: SolutionId): Promise<Solution | null> =>
export const getSolution = (db: DatabaseInternal, id: SolutionId): Promise<Solution | null> =>
withDatabase(db, state => {
const solution = (state.solutions[id] ?? null) as Solution | null
return solution && !solution.deleted ? solution : null
})
export const getSolutions = (db: DatabaseConnection) =>
export const getSolutions = (db: DatabaseInternal) =>
withDatabase(db, state => {
return Object.values(state.solutions).filter(s => !s.deleted)
})
export const getVisibleSolutions = (db: DatabaseConnection): Promise<Solution[]> =>
export const getVisibleSolutions = (db: DatabaseInternal): Promise<Solution[]> =>
withDatabase(db, state => {
return Object.values(state.solutions)
.filter(s => !s.deleted)
.filter(s => s.visible)
})
export const updateSolution = (db: DatabaseConnection, id: SolutionId, solution: Partial<Omit<Solution, MetaProps>>): Promise<Solution> =>
export const updateSolution = (db: DatabaseInternal, id: SolutionId, solution: Partial<Omit<Solution, MetaProps>>): Promise<Solution> =>
withDatabase(db, state => {
state.solutions[id] = {
...state.solutions[id],
@ -216,7 +219,7 @@ export const updateSolution = (db: DatabaseConnection, id: SolutionId, solution:
return state.solutions[id]
})
export const deleteSolution = (db: DatabaseConnection, id: SolutionId): Promise<void> =>
export const deleteSolution = (db: DatabaseInternal, id: SolutionId): Promise<void> =>
withDatabase(db, state => {
state.solutions[id].deleted = true
})

@ -9,6 +9,7 @@ import { StatusCodes } from 'http-status-codes'
import {
createDatabase,
createDatabaseWrapper,
createProblem,
createSolution,
deleteProblem,
@ -39,6 +40,7 @@ import { initialDatabaseValue } from './db/example-data'
import { validateObjectKeys } from '../shared/utils'
import { setupOAuth } from './auth'
import { DatabaseConnection } from '../shared/database'
type SessionId = Opaque<string, string, 'session'>
@ -47,7 +49,7 @@ export interface SessionService {
getUserForSession(sid: SessionId): UserId | null
}
export async function createApiRouter() {
export async function createApiRouter(): Promise<[Router, DatabaseConnection]> {
const sessionStore: Record<SessionId, UserId> = {}
const sessions: SessionService = {
createSession(userId: UserId) {
@ -428,5 +430,5 @@ export async function createApiRouter() {
res.json(requestedUser)
})
return r
return [r, createDatabaseWrapper(db)]
}

@ -0,0 +1,11 @@
import { Problem, Solution, User } from './model'
export type DatabaseConnection = {
getProblem(id: string): Promise<Problem | null>
}
export type Database = {
users: Record<string, User>
problems: Record<string, Problem>
solutions: Record<string, Solution>
}

@ -1,6 +1,11 @@
import { DatabaseConnection } from './database'
export type ServerAsyncCallback = () => Promise<void>
export type RenderedPage = {
html: string
metadata: any
asyncCallbacks: ServerAsyncCallback[]
}
export type RenderFunction = (url: string) => RenderedPage
export type RenderFunction = (url: string, db: DatabaseConnection) => RenderedPage

Loading…
Cancel
Save