wasm: one IPN per runtime with an explicit readiness handshake #20
@@ -9,6 +9,10 @@
|
||||
* this resolves on an explicit signal rather than on the Go scheduler having
|
||||
* run far enough. The name is generated per runtime, so several runtimes can
|
||||
* start in one page without racing each other.
|
||||
*
|
||||
* The returned IPN replaces the bridge's shutdown() with one that resolves when
|
||||
* the runtime has actually exited. The raw promise cannot be awaited: it races
|
||||
* with the runtime tearing itself down, so it may never settle.
|
||||
*/
|
||||
export async function startIPN(
|
||||
go: Go,
|
||||
@@ -28,35 +32,52 @@ export async function startIPN(
|
||||
|
||||
go.env[INIT_CALLBACK_ENV] = name
|
||||
|
||||
// An exit before the IPN reaches the caller is a startup failure, and throwing
|
||||
// hands it back as a rejection. Afterwards the caller holds the only shutdown
|
||||
// path, so an exit is either that shutdown or a panic; report it either way,
|
||||
// because the IPN is dead in both cases.
|
||||
let handedOver = false
|
||||
const exited: Promise<never> = go.run(instance).then(() => {
|
||||
// Only an exit the caller did not ask for is worth reporting. Before the
|
||||
// handover every exit is a startup failure and rejecting hands it back as an
|
||||
// error; afterwards, only an exit that shutdown() did not cause is a panic.
|
||||
let stopping = false
|
||||
const exited: Promise<void> = go.run(instance).then(() => {
|
||||
delete globals[name]
|
||||
if (handedOver) onExit("Go runtime exited")
|
||||
// Always reject: before the handover this is what fails the race below,
|
||||
// and after it the race has settled, so nothing observes the rejection.
|
||||
throw new Error(
|
||||
handedOver
|
||||
? "Go runtime exited"
|
||||
: "Go runtime exited before the IPN was ready"
|
||||
)
|
||||
if (!stopping) onExit("Unexpected shutdown")
|
||||
})
|
||||
// Reject alongside it, so an exit during startup fails the awaits below
|
||||
// instead of leaving them pending forever.
|
||||
const failed: Promise<never> = exited.then(() => {
|
||||
throw new Error("Go runtime exited before the IPN was ready")
|
||||
})
|
||||
|
||||
const [newIPN, terminate] = await Promise.race([ready, exited])
|
||||
const [newIPN, terminate] = await Promise.race([ready, failed])
|
||||
let ipn: IPN
|
||||
try {
|
||||
const ipn = await newIPN(config)
|
||||
handedOver = true
|
||||
return ipn
|
||||
// Keep racing the runtime: building the backend runs in a Go goroutine, and
|
||||
// if the runtime dies partway that goroutine dies with it and its promise
|
||||
// never settles.
|
||||
ipn = await Promise.race([newIPN(config), failed])
|
||||
} catch (err) {
|
||||
// Nothing was built, so nothing can shut the runtime down. Exit it here and
|
||||
// wait for it, or the page keeps a blocked runtime for a failed startup.
|
||||
// Calling terminate on an already-exited runtime does nothing.
|
||||
stopping = true
|
||||
terminate()
|
||||
await exited.catch(() => {})
|
||||
await exited
|
||||
throw err
|
||||
}
|
||||
|
||||
// Replace shutdown in place rather than wrapping the object: the bridge hands
|
||||
// back a plain map of Go-backed functions, and copying it would leave the
|
||||
// caller with something that only looks like the IPN.
|
||||
const rawShutdown = ipn.shutdown.bind(ipn)
|
||||
ipn.shutdown = async () => {
|
||||
stopping = true
|
||||
try {
|
||||
void rawShutdown()
|
||||
} catch {
|
||||
// The runtime may already be gone, in which case there is nothing to ask
|
||||
// and the await below returns immediately.
|
||||
}
|
||||
await exited
|
||||
}
|
||||
return ipn
|
||||
}
|
||||
|
||||
type NewIPN = (config: IPNConfig) => Promise<IPN>
|
||||
|
||||
Vendored
+8
@@ -11,6 +11,14 @@ declare global {
|
||||
run(callbacks: IPNCallbacks): void
|
||||
login(): void
|
||||
logout(): void
|
||||
/**
|
||||
* Tears down the backend and exits the Go runtime that owns this IPN.
|
||||
*
|
||||
* The promise the bridge returns races with the runtime exiting and may
|
||||
* never settle; startIPN replaces it with one that resolves when the
|
||||
* runtime has actually gone.
|
||||
*/
|
||||
shutdown(): Promise<void>
|
||||
ssh(
|
||||
host: string,
|
||||
username: string,
|
||||
|
||||
Reference in New Issue
Block a user