refactor(tsconnect): move the fork's own TS onto the init callback
build-pkg runs tsc and dts-bundle-generator over src/, so the demo app and the package entry point have to follow the runtime off the global factory or the wasm build stops working. Both now start the runtime through a shared startIPN helper, which installs the callback, passes its name in through go.env, and races readiness against the runtime exiting so a startup crash rejects instead of hanging. The panic handler is wired to that exit rather than being attached to a floating go.run() promise. Co-Authored-By: claude-opus-5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import "../wasm_exec"
|
||||
import wasmUrl from "./main.wasm"
|
||||
import { sessionStateStorage } from "../lib/js-state-store"
|
||||
import { renderApp } from "./app"
|
||||
import { startIPN } from "../lib/start-ipn"
|
||||
|
||||
async function main() {
|
||||
const app = await renderApp()
|
||||
@@ -13,23 +14,25 @@ async function main() {
|
||||
fetch(`./dist/${wasmUrl}`),
|
||||
go.importObject
|
||||
)
|
||||
// The Go process should never exit, if it does then it's an unhandled panic.
|
||||
go.run(wasmInstance.instance).then(() =>
|
||||
app.handleGoPanic("Unexpected shutdown")
|
||||
)
|
||||
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const authKey = params.get("authkey") ?? undefined
|
||||
|
||||
const ipn = newIPN({
|
||||
// Persist IPN state in sessionStorage in development, so that we don't need
|
||||
// to re-authorize every time we reload the page.
|
||||
stateStorage: DEBUG ? sessionStateStorage : undefined,
|
||||
// authKey allows for an auth key to be
|
||||
// specified as a url param which automatically
|
||||
// authorizes the client for use.
|
||||
authKey: DEBUG ? authKey : undefined,
|
||||
})
|
||||
// The Go process should never exit, if it does then it's an unhandled panic.
|
||||
const ipn = await startIPN(
|
||||
go,
|
||||
wasmInstance.instance,
|
||||
{
|
||||
// Persist IPN state in sessionStorage in development, so that we don't
|
||||
// need to re-authorize every time we reload the page.
|
||||
stateStorage: DEBUG ? sessionStateStorage : undefined,
|
||||
// authKey allows for an auth key to be
|
||||
// specified as a url param which automatically
|
||||
// authorizes the client for use.
|
||||
authKey: DEBUG ? authKey : undefined,
|
||||
},
|
||||
(reason) => app.handleGoPanic(reason)
|
||||
)
|
||||
app.runWithIPN(ipn)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
/**
|
||||
* Starts a Go runtime and returns the single IPN it owns.
|
||||
*
|
||||
* The runtime does not publish its bridge on a global. It reads the name of a
|
||||
* callback from its environment and invokes it once the bridge is ready, so
|
||||
* 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.
|
||||
*/
|
||||
export async function startIPN(
|
||||
go: Go,
|
||||
instance: WebAssembly.Instance,
|
||||
config: IPNConfig,
|
||||
onExit: (reason: string) => void
|
||||
): Promise<IPN> {
|
||||
const name = `__tsconnectInit_${Math.random().toString(36).slice(2)}`
|
||||
const globals = globalThis as Record<string, unknown>
|
||||
|
||||
const ready = new Promise<(config: IPNConfig) => Promise<IPN>>(
|
||||
(resolve) => {
|
||||
globals[name] = (newIPN: (config: IPNConfig) => Promise<IPN>) => {
|
||||
delete globals[name]
|
||||
resolve(newIPN)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
go.env[INIT_CALLBACK_ENV] = name
|
||||
const exited = go.run(instance).then(() => {
|
||||
delete globals[name]
|
||||
onExit("Unexpected shutdown")
|
||||
throw new Error("Go runtime exited before the IPN was ready")
|
||||
})
|
||||
|
||||
const newIPN = await Promise.race([ready, exited])
|
||||
return newIPN(config)
|
||||
}
|
||||
|
||||
/** Must match initCallbackEnv in wasm_js.go. */
|
||||
const INIT_CALLBACK_ENV = "TSCONNECT_INIT_CALLBACK"
|
||||
@@ -7,6 +7,7 @@
|
||||
/// <reference path="../types/wasm_js.d.ts" />
|
||||
|
||||
import "../wasm_exec"
|
||||
import { startIPN } from "../lib/start-ipn"
|
||||
import wasmURL from "./main.wasm"
|
||||
|
||||
/**
|
||||
@@ -30,11 +31,7 @@ export async function createIPN(config: IPNPackageConfig): Promise<IPN> {
|
||||
go.importObject
|
||||
)
|
||||
// The Go process should never exit, if it does then it's an unhandled panic.
|
||||
go.run(wasmInstance.instance).then(() =>
|
||||
config.panicHandler("Unexpected shutdown")
|
||||
)
|
||||
|
||||
return newIPN(config)
|
||||
return startIPN(go, wasmInstance.instance, config, config.panicHandler)
|
||||
}
|
||||
|
||||
export { runSSHSession } from "../lib/ssh"
|
||||
|
||||
Vendored
-2
@@ -7,8 +7,6 @@
|
||||
*/
|
||||
|
||||
declare global {
|
||||
function newIPN(config: IPNConfig): IPN
|
||||
|
||||
interface IPN {
|
||||
run(callbacks: IPNCallbacks): void
|
||||
login(): void
|
||||
|
||||
Reference in New Issue
Block a user