refactor(tailshare): address feedback
Checks / lint (pull_request) Successful in 1m37s
Checks / format (pull_request) Successful in 1m41s
Checks / typetest (pull_request) Successful in 8m27s
Checks / typecheck (pull_request) Successful in 8m50s
Checks / build (pull_request) Successful in 9m14s
Node Tests / node-tests (pull_request) Successful in 8m15s
Browser Tests / browser-tests (pull_request) Successful in 9m29s

- Extract useSharedWorkerAvailable() into @webnet/react
- Add explicit defaults (useWorker: true, fileOps: "memory") in parseConfig
  so consumers don't repeat them
- Restore markAutostart tri-state: undefined → latches to true on first
  Running, false → never autostart, true → autostart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-28 22:52:54 +00:00
parent bf90068ad0
commit 481f5603f2
4 changed files with 38 additions and 20 deletions
+1
View File
@@ -1,2 +1,3 @@
export { useClient } from "./useClient.js"
export { useLocalStorage } from "./useLocalStorage.js"
export { useSharedWorkerAvailable } from "./useSharedWorkerAvailable.js"
@@ -0,0 +1,9 @@
import { useSyncExternalStore } from "react"
const sub = () => () => void 0
const getClient = () => typeof SharedWorker !== "undefined"
const getServer = () => false
export function useSharedWorkerAvailable(): boolean {
return useSyncExternalStore(sub, getClient, getServer)
}
+26 -18
View File
@@ -25,11 +25,10 @@ import {
useMemo,
useRef,
useState,
useSyncExternalStore,
} from "react"
import { initIPN, InMemoryFileOps, InMemoryState } from "@webnet/tsconnect"
import type { IpnClient } from "@webnet/tsconnect"
import { useLocalStorage } from "@webnet/react"
import { useLocalStorage, useSharedWorkerAvailable } from "@webnet/react"
import wasmUrl from "@webnet/tsconnect/main.wasm"
import type { WorkerConfig } from "@webnet/tsconnect-worker"
@@ -52,10 +51,10 @@ export type TailshareConfig = {
}
function parseConfig(raw: string): TailshareConfig {
const cfg: TailshareConfig = { useWorker: true, fileOps: "memory" }
try {
const parsed = JSON.parse(raw)
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {}
const cfg: TailshareConfig = {}
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return cfg
if (typeof parsed.hostname === "string") cfg.hostname = parsed.hostname
if (typeof parsed.controlURL === "string") cfg.controlURL = parsed.controlURL
if (typeof parsed.authKey === "string") cfg.authKey = parsed.authKey
@@ -63,16 +62,12 @@ function parseConfig(raw: string): TailshareConfig {
if (typeof parsed.autostart === "boolean") cfg.autostart = parsed.autostart
if (typeof parsed.useWorker === "boolean") cfg.useWorker = parsed.useWorker
if (parsed.fileOps === "memory" || parsed.fileOps === "opfs") cfg.fileOps = parsed.fileOps
return cfg
} catch {
return {}
// return defaults
}
return cfg
}
const workerAvailableSub = () => () => void 0
const workerAvailableClient = () => typeof SharedWorker !== "undefined"
const workerAvailableServer = () => false
export const IpnPrepareContext = createContext<{
config: TailshareConfig
setConfig: (patch: Partial<TailshareConfig>) => void
@@ -89,11 +84,7 @@ export function IpnProvider({ children }: { children: ReactNode }) {
}
const localStore = storeRef.current
const workerAvailable = useSyncExternalStore(
workerAvailableSub,
workerAvailableClient,
workerAvailableServer,
)
const workerAvailable = useSharedWorkerAvailable()
const [configRaw, setConfigRaw] = useLocalStorage("tailshare:config")
const config = useMemo(() => parseConfig(configRaw), [configRaw])
@@ -104,7 +95,7 @@ export function IpnProvider({ children }: { children: ReactNode }) {
[setConfigRaw],
)
const useWorker = config.useWorker !== false && workerAvailable
const useWorker = !!config.useWorker && workerAvailable
const [willBuild, setWillBuild] = useState(false)
const build = useCallback(() => setWillBuild(true), [])
@@ -184,7 +175,7 @@ export function IpnProvider({ children }: { children: ReactNode }) {
<AutoExitNode ipn={ipn} config={config} setConfig={setConfig} />
</>
)}
<AutoInit build={build} config={config} />
<AutoInit build={build} config={config} setConfig={setConfig} />
<IpnContext value={ipn}>
<IpnPrepareContext value={prepare}>{children}</IpnPrepareContext>
</IpnContext>
@@ -237,7 +228,24 @@ function AutoExitNode({
return null
}
function AutoInit({ build, config }: { build: () => void; config: TailshareConfig }) {
function AutoInit({
build,
config,
setConfig,
}: {
build: () => void
config: TailshareConfig
setConfig: (patch: Partial<TailshareConfig>) => void
}) {
const state = useIpnSelector(getIpnState)
const markAutostart = useEffectEvent(() => {
if (config.autostart !== false) setConfig({ autostart: true })
})
useEffect(() => {
if (state === "Running") markAutostart()
}, [state])
useEffect(() => {
if (!config.autostart) return
const timeout = window.setTimeout(build, 1000)
+2 -2
View File
@@ -171,7 +171,7 @@ function TailscaleConfig() {
? "OPFS: files persist across reloads. Requires SharedWorker mode."
: "Memory: files are lost on reload."
}
value={config.fileOps ?? "memory"}
value={config.fileOps}
onChange={(e) => setConfig({ fileOps: e.target.value as "memory" | "opfs" })}
data={[
{ label: "Memory (volatile)", value: "memory" },
@@ -192,7 +192,7 @@ function TailscaleConfig() {
<Checkbox
mt="md"
label="Use SharedWorker (enables multi-tab support)"
checked={config.useWorker !== false}
checked={!!config.useWorker}
onChange={(e) => setConfig({ useWorker: e.target.checked })}
/>
)}