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 renderToString from 'preact-render-to-string'
|
||||||
// import { App } from './App'
|
// 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 { App } from './App'
|
||||||
|
import { DatabaseConnection } from '../shared/database'
|
||||||
|
|
||||||
export default (url: string): RenderedPage => {
|
export default (url: string, db: DatabaseConnection): RenderedPage => {
|
||||||
const metadata = {}
|
const metadata = {}
|
||||||
|
const asyncCallbacks: ServerAsyncCallback[] = []
|
||||||
|
|
||||||
const html = renderToString(
|
const html = renderToString(
|
||||||
<MetadataContext.Provider value={metadata}>
|
<MetadataContext.Provider value={metadata}>
|
||||||
<ServerContext.Provider value={true}>
|
<ServerContext.Provider value={true}>
|
||||||
<App url={url} />
|
<DatabaseContext.Provider value={db}>
|
||||||
|
<ServerAsyncCallbacksContext.Provider value={asyncCallbacks}>
|
||||||
|
<App url={url} />
|
||||||
|
</ServerAsyncCallbacksContext.Provider>
|
||||||
|
</DatabaseContext.Provider>
|
||||||
</ServerContext.Provider>
|
</ServerContext.Provider>
|
||||||
</MetadataContext.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 { createContext } from 'preact'
|
||||||
import { prependBaseUrl } from '../shared/utils'
|
import { prependBaseUrl } from '../shared/utils'
|
||||||
|
import { DatabaseConnection } from '../shared/database'
|
||||||
|
import { ServerAsyncCallback } from '../shared/ssr'
|
||||||
|
|
||||||
type Metadata = {
|
type Metadata = {
|
||||||
title?: string
|
title?: string
|
||||||
@@ -13,6 +15,8 @@ export const MetadataContext = createContext<Metadata>({})
|
|||||||
export const ServerContext = createContext<boolean>(false)
|
export const ServerContext = createContext<boolean>(false)
|
||||||
export const ClientContext = createContext<boolean>(false)
|
export const ClientContext = createContext<boolean>(false)
|
||||||
|
|
||||||
|
export const DatabaseContext = createContext<DatabaseConnection | null>(null)
|
||||||
|
|
||||||
type RefreshFunction = () => AbortController
|
type RefreshFunction = () => AbortController
|
||||||
type HeuristicStateUpdater<S> = StateUpdater<S>
|
type HeuristicStateUpdater<S> = StateUpdater<S>
|
||||||
|
|
||||||
@@ -73,3 +77,10 @@ export const useListResource = <T,>(
|
|||||||
|
|
||||||
return [list, refreshList, setItemHeuristic, setListHeuristic]
|
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 { MarkdownEditor } from '../components/MarkdownEditor'
|
||||||
import { Problem } from '../components/Problem'
|
import { Problem } from '../components/Problem'
|
||||||
import { Solution } from '../components/Solution'
|
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'
|
import { useCurrentUser } from '../hooks/useCurrentUser'
|
||||||
|
|
||||||
type RouteProps = {
|
type RouteProps = {
|
||||||
@@ -16,8 +16,16 @@ type RouteProps = {
|
|||||||
|
|
||||||
export const ProblemPage = ({ id }: RouteProps) => {
|
export const ProblemPage = ({ id }: RouteProps) => {
|
||||||
const metadata = useContext(MetadataContext)
|
const metadata = useContext(MetadataContext)
|
||||||
metadata.title = `Problem ${id}`
|
const db = useContext(DatabaseContext)
|
||||||
metadata.description = 'Bacheca di problemi del PHC'
|
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()
|
const [user] = useCurrentUser()
|
||||||
|
|
||||||
@@ -25,7 +33,7 @@ export const ProblemPage = ({ id }: RouteProps) => {
|
|||||||
|
|
||||||
const [problem, refreshProblem] = useResource<ProblemModel | null>(`/api/problem/${id}`, null, problem => {
|
const [problem, refreshProblem] = useResource<ProblemModel | null>(`/api/problem/${id}`, null, problem => {
|
||||||
if (problem === null) {
|
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 { createApiRouter } from './server/routes'
|
||||||
import { RenderFunction } from './shared/ssr'
|
import { RenderFunction } from './shared/ssr'
|
||||||
|
import { DatabaseConnection } from './shared/database'
|
||||||
|
|
||||||
// Load ".env"
|
// Load ".env"
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
@@ -24,7 +25,7 @@ if (config.isDevelopment) {
|
|||||||
console.log(`[Config] PORT = ${config.port}`)
|
console.log(`[Config] PORT = ${config.port}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createDevRouter() {
|
async function createDevRouter(db: DatabaseConnection) {
|
||||||
const r = express.Router()
|
const r = express.Router()
|
||||||
|
|
||||||
const vite = await createViteServer({
|
const vite = await createViteServer({
|
||||||
@@ -41,7 +42,8 @@ async function createDevRouter() {
|
|||||||
// Load (to be bundled) entry point for server side rendering
|
// Load (to be bundled) entry point for server side rendering
|
||||||
const render: RenderFunction = (await vite.ssrLoadModule('./client/entry-server.tsx')).default
|
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 fullUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`
|
||||||
const metaTagsHtml =
|
const metaTagsHtml =
|
||||||
@@ -64,7 +66,7 @@ async function createDevRouter() {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createProductionRouter() {
|
async function createProductionRouter(db: DatabaseConnection) {
|
||||||
// Load bundled entry point for server side rendering
|
// Load bundled entry point for server side rendering
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -75,11 +77,8 @@ async function createProductionRouter() {
|
|||||||
const handleSSR = async (req, res) => {
|
const handleSSR = async (req, res) => {
|
||||||
const transformedTemplate = await fs.readFile(path.resolve('./dist/entry-client/index.html'), 'utf-8')
|
const transformedTemplate = await fs.readFile(path.resolve('./dist/entry-client/index.html'), 'utf-8')
|
||||||
|
|
||||||
const { html, metadata } = render(req.originalUrl)
|
const { html, metadata, asyncCallbacks } = render(req.originalUrl, db)
|
||||||
|
await Promise.all(asyncCallbacks.map(fn => fn()))
|
||||||
console.dir(transformedTemplate, { depth: null })
|
|
||||||
console.dir(html, { depth: null })
|
|
||||||
console.dir(metadata, { depth: null })
|
|
||||||
|
|
||||||
const fullUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`
|
const fullUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`
|
||||||
const metaTagsHtml =
|
const metaTagsHtml =
|
||||||
@@ -112,12 +111,14 @@ async function main() {
|
|||||||
|
|
||||||
app.use(morgan('[Request] :method :url :status :response-time ms - :res[content-length]'))
|
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) {
|
if (config.isDevelopment) {
|
||||||
app.use('/', await createDevRouter())
|
app.use('/', await createDevRouter(db))
|
||||||
} else {
|
} else {
|
||||||
app.use('/', await createProductionRouter())
|
app.use('/', await createProductionRouter(db))
|
||||||
}
|
}
|
||||||
|
|
||||||
app.listen(config.port, () => {
|
app.listen(config.port, () => {
|
||||||
|
|||||||
+23
-20
@@ -1,6 +1,7 @@
|
|||||||
import crypto from 'crypto'
|
import crypto from 'crypto'
|
||||||
|
|
||||||
import { readFile, writeFile, access, constants } from 'fs/promises'
|
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'
|
import { MetadataProps as MetaProps, Problem, ProblemId, Solution, SolutionId, User, UserId, UserRole } from '../../shared/model'
|
||||||
|
|
||||||
@@ -53,16 +54,18 @@ function createMutex(): Mutex {
|
|||||||
return { lock }
|
return { lock }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DatabaseConnection = {
|
type DatabaseInternal = {
|
||||||
path: string
|
path: string
|
||||||
initialValue: Database
|
initialValue: Database
|
||||||
mu: Mutex
|
mu: Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Database = {
|
export function createDatabaseWrapper(db: DatabaseInternal): DatabaseConnection {
|
||||||
users: Record<string, User>
|
return {
|
||||||
problems: Record<string, Problem>
|
getProblem(id: string): Promise<Problem | null> {
|
||||||
solutions: Record<string, Solution>
|
return getProblem(db, id)
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createDatabase(path: string, initialValue: Database) {
|
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()
|
const unlock: Lock = await mu.lock()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -100,17 +103,17 @@ async function withDatabase<R>({ path, initialValue, mu }: DatabaseConnection, f
|
|||||||
// Users
|
// Users
|
||||||
//
|
//
|
||||||
|
|
||||||
export const getUsers = (db: DatabaseConnection) =>
|
export const getUsers = (db: DatabaseInternal) =>
|
||||||
withDatabase(db, state => {
|
withDatabase(db, state => {
|
||||||
return Object.values(state.users)
|
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 => {
|
withDatabase(db, state => {
|
||||||
return state.users[id] ?? null
|
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 => {
|
withDatabase(db, state => {
|
||||||
state.users[user.id] = user
|
state.users[user.id] = user
|
||||||
})
|
})
|
||||||
@@ -119,7 +122,7 @@ export const createUser: (db: DatabaseConnection, user: User) => Promise<void> =
|
|||||||
// Problems
|
// 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 => {
|
withDatabase(db, state => {
|
||||||
const id = crypto.randomBytes(4).toString('hex') as ProblemId
|
const id = crypto.randomBytes(4).toString('hex') as ProblemId
|
||||||
|
|
||||||
@@ -136,18 +139,18 @@ export const createProblem = (db: DatabaseConnection, { title, content, createdB
|
|||||||
return id
|
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 => {
|
withDatabase(db, state => {
|
||||||
const problem = (state.problems[id] ?? null) as Problem | null
|
const problem = (state.problems[id] ?? null) as Problem | null
|
||||||
return problem && !problem.deleted ? 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 => {
|
withDatabase(db, state => {
|
||||||
return Object.values(state.problems).filter(p => !p.deleted)
|
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 => {
|
withDatabase(db, state => {
|
||||||
state.problems[id] = {
|
state.problems[id] = {
|
||||||
...state.problems[id],
|
...state.problems[id],
|
||||||
@@ -157,7 +160,7 @@ export const updateProblem = (db: DatabaseConnection, id: ProblemId, problem: Pa
|
|||||||
return state.problems[id]
|
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 => {
|
withDatabase(db, state => {
|
||||||
state.problems[id].deleted = true
|
state.problems[id].deleted = true
|
||||||
})
|
})
|
||||||
@@ -167,7 +170,7 @@ export const deleteProblem = (db: DatabaseConnection, id: ProblemId): Promise<vo
|
|||||||
//
|
//
|
||||||
|
|
||||||
export const createSolution = (
|
export const createSolution = (
|
||||||
db: DatabaseConnection,
|
db: DatabaseInternal,
|
||||||
{ sentBy, forProblem, content }: Omit<Solution, MetaProps | 'status' | 'visible'>
|
{ sentBy, forProblem, content }: Omit<Solution, MetaProps | 'status' | 'visible'>
|
||||||
): Promise<SolutionId> =>
|
): Promise<SolutionId> =>
|
||||||
withDatabase(db, state => {
|
withDatabase(db, state => {
|
||||||
@@ -188,25 +191,25 @@ export const createSolution = (
|
|||||||
return id
|
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 => {
|
withDatabase(db, state => {
|
||||||
const solution = (state.solutions[id] ?? null) as Solution | null
|
const solution = (state.solutions[id] ?? null) as Solution | null
|
||||||
return solution && !solution.deleted ? solution : null
|
return solution && !solution.deleted ? solution : null
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getSolutions = (db: DatabaseConnection) =>
|
export const getSolutions = (db: DatabaseInternal) =>
|
||||||
withDatabase(db, state => {
|
withDatabase(db, state => {
|
||||||
return Object.values(state.solutions).filter(s => !s.deleted)
|
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 => {
|
withDatabase(db, state => {
|
||||||
return Object.values(state.solutions)
|
return Object.values(state.solutions)
|
||||||
.filter(s => !s.deleted)
|
.filter(s => !s.deleted)
|
||||||
.filter(s => s.visible)
|
.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 => {
|
withDatabase(db, state => {
|
||||||
state.solutions[id] = {
|
state.solutions[id] = {
|
||||||
...state.solutions[id],
|
...state.solutions[id],
|
||||||
@@ -216,7 +219,7 @@ export const updateSolution = (db: DatabaseConnection, id: SolutionId, solution:
|
|||||||
return state.solutions[id]
|
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 => {
|
withDatabase(db, state => {
|
||||||
state.solutions[id].deleted = true
|
state.solutions[id].deleted = true
|
||||||
})
|
})
|
||||||
|
|||||||
+4
-2
@@ -9,6 +9,7 @@ import { StatusCodes } from 'http-status-codes'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
createDatabase,
|
createDatabase,
|
||||||
|
createDatabaseWrapper,
|
||||||
createProblem,
|
createProblem,
|
||||||
createSolution,
|
createSolution,
|
||||||
deleteProblem,
|
deleteProblem,
|
||||||
@@ -39,6 +40,7 @@ import { initialDatabaseValue } from './db/example-data'
|
|||||||
|
|
||||||
import { validateObjectKeys } from '../shared/utils'
|
import { validateObjectKeys } from '../shared/utils'
|
||||||
import { setupOAuth } from './auth'
|
import { setupOAuth } from './auth'
|
||||||
|
import { DatabaseConnection } from '../shared/database'
|
||||||
|
|
||||||
type SessionId = Opaque<string, string, 'session'>
|
type SessionId = Opaque<string, string, 'session'>
|
||||||
|
|
||||||
@@ -47,7 +49,7 @@ export interface SessionService {
|
|||||||
getUserForSession(sid: SessionId): UserId | null
|
getUserForSession(sid: SessionId): UserId | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createApiRouter() {
|
export async function createApiRouter(): Promise<[Router, DatabaseConnection]> {
|
||||||
const sessionStore: Record<SessionId, UserId> = {}
|
const sessionStore: Record<SessionId, UserId> = {}
|
||||||
const sessions: SessionService = {
|
const sessions: SessionService = {
|
||||||
createSession(userId: UserId) {
|
createSession(userId: UserId) {
|
||||||
@@ -428,5 +430,5 @@ export async function createApiRouter() {
|
|||||||
res.json(requestedUser)
|
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 = {
|
export type RenderedPage = {
|
||||||
html: string
|
html: string
|
||||||
metadata: any
|
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