64e1fac897
Move our bemo playground from dotcom to the examples app. In preparation for more multiplayer examples, I built our a little bit of chrome around example room IDs: if you create an example with `multiplayer: true`, the examples app will render a little room ID picker above your example. The room IDs are scoped to each example, and each deploy of the examples app. By default people on the same example will be in the same room, but the default ID changes every hour. As I was doing this, I noticed you could get an ugly situation where the docs site was in dark mode, tldraw was in dark mode, but the little bit of examples chrome was in light mode. To fix this I through together an extremely rough dark mode for the examples which switches on whenever the tldraw instance inside is in dark mode. ### Change type - [x] `other` --------- Co-authored-by: David Sheldrick <d.j.sheldrick@gmail.com>
169 lines
4.1 KiB
TypeScript
169 lines
4.1 KiB
TypeScript
import { ChildProcessWithoutNullStreams, spawn } from 'child_process'
|
|
import kleur from 'kleur'
|
|
import { lock } from 'proper-lockfile'
|
|
import stripAnsi from 'strip-ansi'
|
|
|
|
const lockfileName = __dirname
|
|
|
|
/**
|
|
* a long time ago, workerd would regularly crash with a segfault but the error is not caught by the
|
|
* process, so it will just hang. this script wraps the process, tailing the logs and restarting the
|
|
* process if we encounter the string 'Segmentation fault'. we think this error is probably fixed
|
|
* now.
|
|
*
|
|
* there's a separate issue where spawning multiple workerd instances at once would cause them to
|
|
* fight and crash. we use a lockfile to start our wrokerd instances one at a time, waiting for the
|
|
* previous one to be ready before we start the next.
|
|
*/
|
|
class MiniflareMonitor {
|
|
private process: ChildProcessWithoutNullStreams | null = null
|
|
|
|
constructor(
|
|
private command: string,
|
|
private args: string[] = []
|
|
) {}
|
|
|
|
public async start(): Promise<void> {
|
|
await this.stop() // Ensure any existing process is stopped
|
|
await this.lock()
|
|
await console.log(`Starting wrangler...`)
|
|
this.process = spawn(this.command, this.args, {
|
|
env: {
|
|
NODE_ENV: 'development',
|
|
...process.env,
|
|
},
|
|
})
|
|
|
|
this.process.stdout.on('data', (data: Buffer) => {
|
|
this.handleOutput(stripAnsi(data.toString().replace('\r', '').trim()))
|
|
})
|
|
|
|
this.process.stderr.on('data', (data: Buffer) => {
|
|
this.handleOutput(stripAnsi(data.toString().replace('\r', '').trim()), true)
|
|
})
|
|
}
|
|
|
|
private handleOutput(output: string, err = false): void {
|
|
if (!output) return
|
|
|
|
if (output.includes('Ready on') && this.isLocked()) {
|
|
this.release()
|
|
}
|
|
|
|
if (output.includes('Segmentation fault')) {
|
|
console.error('Segmentation fault detected. Restarting Miniflare...')
|
|
this.restart()
|
|
} else if (!err) {
|
|
console.log(output.replace('[mf:inf]', '')) // or handle the output differently
|
|
} else {
|
|
console.error(output.replace('[mf:err]', '')) // or handle the output differently
|
|
}
|
|
}
|
|
|
|
private async restart(): Promise<void> {
|
|
console.log('Restarting wrangler...')
|
|
await this.stop()
|
|
setTimeout(() => this.start(), 3000) // Restart after a short delay
|
|
}
|
|
|
|
private async stop(): Promise<void> {
|
|
if (this.isLocked()) await this.release()
|
|
if (this.process) {
|
|
this.process.kill()
|
|
this.process = null
|
|
}
|
|
}
|
|
|
|
private _lockPromise?: Promise<() => Promise<void>>
|
|
private isLocked() {
|
|
return !!this._lockPromise
|
|
}
|
|
private async lock() {
|
|
if (this.isLocked()) throw new Error('Already locked')
|
|
console.log('Locking...')
|
|
this._lockPromise = lock(lockfileName, {
|
|
retries: {
|
|
minTimeout: 500,
|
|
retries: 10,
|
|
},
|
|
})
|
|
await this._lockPromise
|
|
console.log('Locked')
|
|
}
|
|
private async release() {
|
|
if (!this.isLocked()) throw new Error('Not locked')
|
|
console.log('Releasing...')
|
|
const lockPromise = this._lockPromise!
|
|
this._lockPromise = undefined
|
|
const release = await lockPromise
|
|
await release()
|
|
console.log('Released')
|
|
}
|
|
}
|
|
|
|
class SizeReporter {
|
|
lastLineTime = Date.now()
|
|
nextTick?: NodeJS.Timeout
|
|
|
|
size = 0
|
|
|
|
start() {
|
|
console.log('Spawning size reporter...')
|
|
const proc = spawn('yarn', [
|
|
'run',
|
|
'-T',
|
|
'esbuild',
|
|
'src/worker.ts',
|
|
'--bundle',
|
|
'--minify',
|
|
'--watch',
|
|
'--external:cloudflare:*',
|
|
'--target=esnext',
|
|
'--format=esm',
|
|
])
|
|
// listen for lines on stdin
|
|
proc.stdout.on('data', (data) => {
|
|
this.size += data.length
|
|
this.lastLineTime = Date.now()
|
|
clearTimeout(this.nextTick)
|
|
this.nextTick = setTimeout(() => {
|
|
console.log(
|
|
kleur.bold(kleur.yellow('worker')),
|
|
'is roughly',
|
|
kleur.bold(kleur.cyan(Math.floor(this.size / 1024) + 'kb')),
|
|
'(minified)\n'
|
|
)
|
|
this.size = 0
|
|
}, 10)
|
|
})
|
|
proc.stderr.on('data', (data) => {
|
|
console.log(data.toString())
|
|
})
|
|
process.on('SIGINT', () => {
|
|
console.log('Int')
|
|
proc.kill()
|
|
})
|
|
process.on('SIGTERM', () => {
|
|
console.log('Term')
|
|
proc.kill()
|
|
})
|
|
process.on('exit', () => {
|
|
console.log('Exiting')
|
|
proc.kill()
|
|
})
|
|
}
|
|
}
|
|
|
|
new MiniflareMonitor('wrangler', [
|
|
'dev',
|
|
'--env',
|
|
'dev',
|
|
'--test-scheduled',
|
|
'--log-level',
|
|
'info',
|
|
'--var',
|
|
'IS_LOCAL:true',
|
|
...process.argv.slice(2),
|
|
]).start()
|
|
|
|
new SizeReporter().start()
|