Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introducing Multi-threading #487

Open
wants to merge 59 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
4c66757
multithreading WIP
vezaynk Apr 27, 2023
a49fdff
More WIP
vezaynk Apr 27, 2023
b956569
more WIP
vezaynk Apr 27, 2023
e1e88ab
apply fix
vezaynk Apr 28, 2023
fce6f6e
Dev and Prod working
vezaynk Apr 28, 2023
a8fb7a8
Fix up tests
vezaynk Apr 28, 2023
aef4297
Remove Worker.js
vezaynk Apr 29, 2023
8892002
Streaming works
vezaynk May 4, 2023
34e6535
Setup worker pool
vezaynk May 5, 2023
d305a74
Setup preloading
vezaynk May 5, 2023
c9b919b
Add transform support
vezaynk May 5, 2023
19002c7
Cleanup 1
vezaynk May 5, 2023
20df079
One more ignore
vezaynk May 5, 2023
9598aa8
bump dev dependencies
vezaynk May 8, 2023
d1f4dd1
Use `satisfies` on postMessage
vezaynk May 8, 2023
108bafc
Building works
vezaynk May 16, 2023
4a6f97b
Forward more types into pass-through
vezaynk May 16, 2023
f9b6aa5
Try-catch load ts-node
vezaynk May 16, 2023
8e8065e
Add comment
vezaynk May 16, 2023
0c95835
Set up transform hook test
vezaynk May 16, 2023
a82434f
Merge branch 'main' of https://github.com/gadget-inc/fastify-renderer
vezaynk Nov 2, 2023
2180e04
Things are cooking, but not working
vezaynk Nov 3, 2023
879f579
It's alive!
vezaynk Nov 3, 2023
a5fab24
lint
vezaynk Nov 3, 2023
94a23bc
Merge branch 'work-on-working'
vezaynk Nov 3, 2023
7d0e154
some cleanup
vezaynk Nov 3, 2023
4f67ad1
Some test cleanup
vezaynk Nov 3, 2023
ff8043e
Improving semantics
vezaynk Nov 3, 2023
0a99ad3
nonce WIP
vezaynk Nov 4, 2023
eee3c2b
Nonces work
vezaynk Nov 4, 2023
4951bba
Trying to fix tests
vezaynk Nov 4, 2023
fd33bae
Disable a bunch of tests
vezaynk Nov 4, 2023
0118f1b
version bumpings
vezaynk Nov 4, 2023
978cf5a
Fixing ts issues
vezaynk Nov 4, 2023
271765d
mo' version bumps, mo' problems
vezaynk Nov 4, 2023
0b26823
Merge branch 'main' of github.com:gadget-inc/fastify-renderer
vezaynk Nov 4, 2023
94bf9dc
More cleanup
vezaynk Nov 4, 2023
535c89b
remove settings
vezaynk Nov 4, 2023
866dd60
partially disable tests
vezaynk Nov 4, 2023
50af9e1
lint
vezaynk Nov 4, 2023
44b191c
pass root tests
vezaynk Nov 18, 2023
7a6a0ba
handle error
vezaynk Nov 18, 2023
7d9bc42
add some error handling
vezaynk Nov 18, 2023
e38fcc7
Upgrade to React 18
vezaynk Nov 18, 2023
e705dc0
fix deprecation warning
vezaynk Nov 18, 2023
aec6e19
add error stream
vezaynk Nov 18, 2023
e57a48e
fix useTransition
vezaynk Nov 18, 2023
858b98f
Demo React 18 Upgrade Errors
vezaynk Nov 19, 2023
3dd81d5
Jest to Vitest migration
vezaynk Nov 19, 2023
8de744d
Merge branch 'upgrade-react'
vezaynk Nov 19, 2023
1eb6484
All tests go
vezaynk Nov 19, 2023
8970f50
unskip test
vezaynk Nov 19, 2023
d61fc28
Add a couple tests
vezaynk Nov 19, 2023
c7a9585
improve consistency between dev and prod
vezaynk Nov 19, 2023
d89ac3b
Add defineRenderHook
vezaynk Nov 19, 2023
a20e882
remove error hook from server
vezaynk Nov 19, 2023
9eca2bc
Write test error hook
vezaynk Nov 19, 2023
6367fc7
fix comment
vezaynk Nov 19, 2023
7b160ee
remove excessive itmeout
vezaynk Nov 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
add error stream
vezaynk committed Nov 18, 2023
commit aec6e197813bd5aaf0eaf431742cd2ce2c1f6487
29 changes: 21 additions & 8 deletions packages/fastify-renderer/src/node/RenderBus.tsx
Original file line number Diff line number Diff line change
@@ -10,32 +10,35 @@ export interface Stack {

/** Holds groups of content during a render that eventually get pushed into the template. */
export class RenderBus {
streams: Record<string, PassThrough> = {}
streams: Map<string, PassThrough> = new Map()
included = new Set<string>()

private createStack(key: string) {
const stream = (this.streams[key] = new PassThrough())
const stream = new PassThrough()
this.streams.set(key, stream)

return stream
}

push(key: string, content: string | null, throwIfEnded = true) {
if (!this.streams[key]) this.createStack(key)
if (this.streams[key].writableEnded) {
let stream = this.streams.get(key)
if (!stream) stream = this.createStack(key)
if (stream.closed) {
if (throwIfEnded) throw new Error(`Stack with key=${key} has ended, no more content can be added`)
return
}

if (content === null) {
this.streams[key].end()
stream.end()
} else {
this.streams[key].write(content)
stream.write(content)
}
}

stack(key: string) {
if (!this.streams[key]) this.createStack(key)
return this.streams[key]
let stream = this.streams.get(key)
if (!stream) stream = this.createStack(key)
return stream
}

preloadModule(path: string) {
@@ -53,4 +56,14 @@ export class RenderBus {
loadScript(src: string, nonce?: string) {
this.push('tail', scriptTag(``, { src, nonce }))
}

endAll() {
// End all streams (error handling helper)
for (const stream of this.streams.values()) {
if (!stream.closed) {
stream.end()
stream.destroy()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -93,34 +93,32 @@ export class ReactRenderer implements Renderer {
const destination = this.stripBasePath(render.request.url, render.base)
if (!this.workerPool) throw new Error('WorkerPool not setup')

const expectedStreamEnds = new Set(['head', 'tail', 'content'] as const)
const expectedStreamEnds = new Set(['head', 'tail', 'content', 'error'] as const)

// Do not `await` or else it will not return
// until the whole stream is completed
return this.workerPool.use(
(worker) =>
new Promise<void>((resolve, reject) => {
const errorHandler = (error: Error) => {
// Close streams
bus.push('head', null, false)
bus.push('content', null, false)
bus.push('tail', null, false)
reject(error)
const cleanup = () => {
expectedStreamEnds.clear()
worker.off('message', messageHandler)
}

const messageHandler = ({ stack, content }: StreamWorkerEvent) => {
// console.log('Got message', stack, content)
if (stack === 'error' && content) {
reject(new Error(content))
}
bus.push(stack, content)
if (content === null) {
expectedStreamEnds.delete(stack)
if (expectedStreamEnds.size === 0) {
worker.off('message', messageHandler)
worker.off('error', errorHandler)
cleanup()
resolve()
}
}
}
worker.on('message', messageHandler)
worker.on('error', errorHandler)
worker.postMessage({
vezaynk marked this conversation as resolved.
Show resolved Hide resolved
modulePath: path.join(this.plugin.serverOutDir, mapFilepathToEntrypointName(requirePath)),
renderBase: render.base,
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ const port = parentPort

port.on('message', (args: WorkerRenderInput) => {
const bus = new RenderBus()
const stackStream = (stack: 'tail' | 'content' | 'head') => {
const stackStream = (stack: 'tail' | 'content' | 'head' | 'error') => {
const stream = bus.stack(stack)
const send = ({ stack, content }: StreamWorkerEvent) => {
port.postMessage({ stack, content } satisfies StreamWorkerEvent)
@@ -18,14 +18,12 @@ port.on('message', (args: WorkerRenderInput) => {
})

stream.on('end', () => {
console.log('Stream ended for', stack)
send({ stack, content: null })
})
// stream.on('error', () => {
// console.log('Ending stream with error')
// send({ stack, content: null })
// })
}

stackStream('error')
stackStream('head')
stackStream('content')
stackStream('tail')
109 changes: 59 additions & 50 deletions packages/fastify-renderer/src/node/renderers/react/ssr.ts
Original file line number Diff line number Diff line change
@@ -41,77 +41,86 @@ if (parentPort) {

export function staticRender({ mode, bus, bootProps, destination, renderBase, module, hooks }: RenderArgs) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { React, ReactDOMServer, Router, RenderBusContext, Layout, Entrypoint } = module
const thunkHooks = hooks.map((hook) => require(hook)!.default).map(unthunk) as FastifyRendererHook[]
try {
const { React, ReactDOMServer, Router, RenderBusContext, Layout, Entrypoint } = module
const thunkHooks = hooks.map((hook) => require(hook)!.default).map(unthunk) as FastifyRendererHook[]

for (const { heads } of thunkHooks) {
if (heads) {
bus.push('head', heads())
}
}
let app: ReactElement = React.createElement(
RenderBusContext.Provider,
null,
React.createElement(
Router,
{
base: renderBase,
hook: staticLocationHook(destination),
},
let app: ReactElement = React.createElement(
RenderBusContext.Provider,
null,
React.createElement(
Layout,
Router,
{
isNavigating: false,
navigationDestination: destination,
bootProps: bootProps,
base: renderBase,
hook: staticLocationHook(destination),
},
React.createElement(Entrypoint, bootProps)
React.createElement(
Layout,
{
isNavigating: false,
navigationDestination: destination,
bootProps: bootProps,
},
React.createElement(Entrypoint, bootProps)
)
)
)
)

for (const { name, transform } of thunkHooks) {
if (transform) {
try {
for (const { heads } of thunkHooks) {
if (heads) {
bus.push('head', heads())
}
}
for (const { transform } of thunkHooks) {
if (transform) {
app = transform(app)
} catch (error) {
console.error('Error in hook', name, error)
}
}
}

for (const { name, tails } of thunkHooks) {
if (tails) {
try {
for (const { tails } of thunkHooks) {
if (tails) {
bus.push('tail', tails())
} catch (error) {
console.error('Error in hook', name, error)
}
}
}
bus.push('tail', null)
bus.push('tail', null)

if (mode === 'streaming') {
;(ReactDOMServer as typeof _ReactDOMServer).renderToPipeableStream(app).pipe(bus.stack('content'))
} else {
try {
if (mode === 'streaming') {
const renderingPipe = (ReactDOMServer as typeof _ReactDOMServer).renderToPipeableStream(app, {
onError(error, errorInfo) {
console.error('Caught error streaming', error, errorInfo)
if (error instanceof Error) {
bus.push('error', error.message)
}
bus.endAll()
},
onAllReady() {
// onAllReady still fires if there were errors
bus.push('error', null, false)
},
})
// Send to content
renderingPipe.pipe(bus.stack('content'))
} else {
const content = (ReactDOMServer as typeof _ReactDOMServer).renderToString(app)
// no errors
bus.push('error', null)
bus.push('content', content)
} catch (error) {
console.error('Error rendering component', error)
}
bus.push('content', null)

for (const { name, postRenderHeads } of thunkHooks) {
if (postRenderHeads) {
try {
bus.push('content', null)

for (const { postRenderHeads } of thunkHooks) {
if (postRenderHeads) {
bus.push('head', postRenderHeads())
} catch (error) {
console.error('Error in hook', name, error)
}
}
}
}

bus.push('head', null)
bus.push('head', null)
} catch (error) {
console.error('Caught error while rendering', error)
if (error instanceof Error) {
bus.push('error', error.message)
}
bus.endAll()
}
}
2 changes: 1 addition & 1 deletion packages/fastify-renderer/src/node/types.ts
Original file line number Diff line number Diff line change
@@ -92,5 +92,5 @@ export interface WorkerRenderInput extends RenderInput {

export interface StreamWorkerEvent {
content: string | null
stack: 'tail' | 'content' | 'head'
stack: 'tail' | 'content' | 'head' | 'error'
}