magic: added a way for running async functions in the top level jsx code
This commit is contained in:
+11
-5
@@ -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 }
|
||||
}
|
||||
|
||||
+12
-1
@@ -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, () => {
|
||||
|
||||
+23
-20
@@ -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
|
||||
})
|
||||
|
||||
+4
-2
@@ -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>
|
||||
}
|
||||
+6
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user