feat(example-app): SharedWorker demo with main-thread fallback
- Add src/worker.ts as the webpack worker entry (imports @webnet/tsconnect-worker/worker)
- IpnProvider: add useWorker/workerAvailable state; when useWorker is on,
useBuildIpnWorker() creates the client and its embedded store is passed to
IpnStoreProvider; when off, existing useBuildIpn() path runs unchanged
- Debug: add "use SharedWorker" checkbox (checked by default when available,
disabled when not); state dropdown shows indexeddb/memory in worker mode
and localStorage/sessionStorage/disable in main-thread mode; taildrop
option label reflects opfs vs memory depending on mode
- useBuildIpnWorker: add optional workerOptions param instead of hardcoding
{ type: "module" } — webpack bundles the worker as a classic script, so
the caller passes no options; module-mode callers opt in explicitly
- Add @webnet/tsconnect-react, @webnet/tsconnect-redux, @webnet/tsconnect-worker
to example-app package.json dependencies
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,10 @@
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"@webnet/tsconnect": "*"
|
||||
"@webnet/tsconnect": "*",
|
||||
"@webnet/tsconnect-react": "*",
|
||||
"@webnet/tsconnect-redux": "*",
|
||||
"@webnet/tsconnect-worker": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.27.1",
|
||||
|
||||
@@ -79,7 +79,7 @@ function BaseDebug() {
|
||||
<br />
|
||||
ipn:{" "}
|
||||
{ipn ? (
|
||||
"present"
|
||||
prepare.useWorker ? "present (SharedWorker)" : "present (main thread)"
|
||||
) : prepare.willBuild ? (
|
||||
"waiting..."
|
||||
) : (
|
||||
@@ -88,25 +88,53 @@ function BaseDebug() {
|
||||
<br />
|
||||
caCerts: {caCerts ? fmtSize(caCerts.length) : "loading..."}
|
||||
<br />
|
||||
state:{" "}
|
||||
<select
|
||||
disabled={!!ipn}
|
||||
value={prepare.state}
|
||||
onChange={(e) => prepare.setState(e.target.value as typeof prepare.state)}
|
||||
>
|
||||
<option>disable</option>
|
||||
<option>localStorage</option>
|
||||
<option>sessionStorage</option>
|
||||
</select>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={!!ipn || !prepare.workerAvailable}
|
||||
checked={prepare.useWorker && prepare.workerAvailable}
|
||||
onChange={(e) => prepare.setUseWorker(e.target.checked)}
|
||||
/>{" "}
|
||||
use SharedWorker{!prepare.workerAvailable ? " (unavailable)" : ""}
|
||||
</label>
|
||||
<br />
|
||||
taildrop:
|
||||
{prepare.useWorker ? (
|
||||
<>
|
||||
state:{" "}
|
||||
<select
|
||||
disabled={!!ipn}
|
||||
value={prepare.state === "disable" ? "memory" : "indexeddb"}
|
||||
onChange={(e) =>
|
||||
prepare.setState(e.target.value === "memory" ? "disable" : "localStorage")
|
||||
}
|
||||
>
|
||||
<option value="indexeddb">indexeddb</option>
|
||||
<option value="memory">memory</option>
|
||||
</select>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
state:{" "}
|
||||
<select
|
||||
disabled={!!ipn}
|
||||
value={prepare.state}
|
||||
onChange={(e) => prepare.setState(e.target.value as typeof prepare.state)}
|
||||
>
|
||||
<option>disable</option>
|
||||
<option>localStorage</option>
|
||||
<option>sessionStorage</option>
|
||||
</select>
|
||||
</>
|
||||
)}
|
||||
<br />
|
||||
taildrop:{" "}
|
||||
<select
|
||||
disabled={!!ipn}
|
||||
value={prepare.taildrop}
|
||||
onChange={(e) => prepare.setTaildrop(e.target.value as typeof prepare.taildrop)}
|
||||
>
|
||||
<option>disable</option>
|
||||
<option>memory</option>
|
||||
<option value="memory">{prepare.useWorker ? "opfs" : "memory"}</option>
|
||||
</select>
|
||||
<br />
|
||||
<label>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useBuildIpn } from "@webnet/tsconnect-react"
|
||||
import { useBuildIpn, useBuildIpnWorker } from "@webnet/tsconnect-react"
|
||||
import { buildIpnStore, getIpnState, type IpnStore } from "@webnet/tsconnect-redux"
|
||||
import type { WorkerConfig } from "@webnet/tsconnect-worker"
|
||||
import {
|
||||
ContextType,
|
||||
createContext,
|
||||
@@ -12,9 +13,13 @@ import {
|
||||
} from "react"
|
||||
import { useIpnBuilder } from "./IpnBuilderContext"
|
||||
import { IpnContext, IpnStoreProvider } from "./tsconnect"
|
||||
import { InMemoryFileOps, IPN, WebStorageState } from "@webnet/tsconnect"
|
||||
import { InMemoryFileOps, WebStorageState } from "@webnet/tsconnect"
|
||||
import type { IpnClient } from "@webnet/tsconnect"
|
||||
import { useIpnSelector } from "../hooks/tsconnect"
|
||||
import { useClient } from "../hooks/useClient"
|
||||
import wasmUrl from "@webnet/tsconnect/main.wasm"
|
||||
|
||||
const workerScriptUrl = new URL("../worker.ts", import.meta.url)
|
||||
|
||||
export const IpnPrepareContext = createContext<{
|
||||
// pre-built parts
|
||||
@@ -38,6 +43,11 @@ export const IpnPrepareContext = createContext<{
|
||||
// automatically login to tailscale
|
||||
autoLogin: boolean
|
||||
setAutoLogin: (autoLogin: boolean) => void
|
||||
|
||||
// worker vs main-thread mode
|
||||
workerAvailable: boolean
|
||||
useWorker: boolean
|
||||
setUseWorker: (useWorker: boolean) => void
|
||||
} | null>(null)
|
||||
|
||||
export function IpnProvider({
|
||||
@@ -57,6 +67,8 @@ export function IpnProvider({
|
||||
defer?: boolean
|
||||
autoLogin?: boolean
|
||||
}) {
|
||||
const workerAvailable = typeof SharedWorker !== "undefined"
|
||||
|
||||
const storeRef = useRef<IpnStore | null>(null)
|
||||
if (!storeRef.current) {
|
||||
storeRef.current = buildIpnStore()
|
||||
@@ -67,6 +79,7 @@ export function IpnProvider({
|
||||
const [willBuild, setWillBuild] = useState(!defer)
|
||||
const build = useCallback(() => setWillBuild(true), [setWillBuild])
|
||||
|
||||
const [useWorker, setUseWorker] = useState(workerAvailable)
|
||||
const [state, setState] = useState(defaultState)
|
||||
const [taildrop, setTaildrop] = useState(defaultTaildrop)
|
||||
|
||||
@@ -77,6 +90,27 @@ export function IpnProvider({
|
||||
const [autoLogin, setAutoLogin] = useState(defaultAutoLogin)
|
||||
|
||||
const client = useClient()
|
||||
|
||||
// ── Worker mode ──────────────────────────────────────────────────────────────
|
||||
const workerConfig = useMemo<WorkerConfig>(
|
||||
() => ({
|
||||
wasmUrl: wasmUrl as string,
|
||||
hostname: hostname ?? undefined,
|
||||
controlURL: controlURL ?? undefined,
|
||||
authKey: authKey ?? undefined,
|
||||
stateStorage: state === "disable" ? "memory" : "indexeddb",
|
||||
fileOps: taildrop !== "disable",
|
||||
}),
|
||||
[hostname, controlURL, authKey, state, taildrop],
|
||||
)
|
||||
|
||||
const workerClient = useBuildIpnWorker(
|
||||
useWorker && willBuild ? workerScriptUrl : null,
|
||||
workerConfig,
|
||||
runParams,
|
||||
)
|
||||
|
||||
// ── Main-thread mode ─────────────────────────────────────────────────────────
|
||||
const builderParams = useMemo(() => {
|
||||
const builderParams = { ...ipnBuilderParams }
|
||||
|
||||
@@ -96,7 +130,17 @@ export function IpnProvider({
|
||||
return builderParams
|
||||
}, [ipnBuilderParams, state, taildrop, hostname, controlURL, authKey, client])
|
||||
|
||||
const ipn = useBuildIpn(store, client && willBuild ? ipnBuilder : null, builderParams, runParams)
|
||||
const mainIpn = useBuildIpn(
|
||||
store,
|
||||
!useWorker && client && willBuild ? ipnBuilder : null,
|
||||
builderParams,
|
||||
runParams,
|
||||
)
|
||||
|
||||
// ── Active client ─────────────────────────────────────────────────────────────
|
||||
const ipn = useWorker ? workerClient : mainIpn
|
||||
const activeStore = useWorker && workerClient ? workerClient.store : store
|
||||
|
||||
if (typeof window !== "undefined") Object.assign(window, { ipn })
|
||||
|
||||
const prepare = useMemo<NonNullable<ContextType<typeof IpnPrepareContext>>>(
|
||||
@@ -118,6 +162,10 @@ export function IpnProvider({
|
||||
|
||||
autoLogin,
|
||||
setAutoLogin,
|
||||
|
||||
workerAvailable,
|
||||
useWorker,
|
||||
setUseWorker,
|
||||
}),
|
||||
[
|
||||
state,
|
||||
@@ -134,11 +182,14 @@ export function IpnProvider({
|
||||
build,
|
||||
autoLogin,
|
||||
setAutoLogin,
|
||||
workerAvailable,
|
||||
useWorker,
|
||||
setUseWorker,
|
||||
],
|
||||
)
|
||||
|
||||
return (
|
||||
<IpnStoreProvider store={store}>
|
||||
<IpnStoreProvider store={activeStore}>
|
||||
{ipn && autoLogin && <AutoLogin ipn={ipn} />}
|
||||
<IpnContext value={ipn}>
|
||||
<IpnPrepareContext value={prepare}>{children}</IpnPrepareContext>
|
||||
@@ -147,7 +198,7 @@ export function IpnProvider({
|
||||
)
|
||||
}
|
||||
|
||||
function AutoLogin({ ipn }: { ipn: IPN }) {
|
||||
function AutoLogin({ ipn }: { ipn: IpnClient }) {
|
||||
const state = useIpnSelector(getIpnState)
|
||||
useEffect(() => {
|
||||
if (state === "NeedsLogin") ipn.login()
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
import "@webnet/tsconnect-worker/worker"
|
||||
@@ -39,12 +39,13 @@ export function useBuildIpnWorker(
|
||||
worker: string | URL | null,
|
||||
config: WorkerConfig | null = null,
|
||||
runParams: Parameters<IPN["run"]>[0] | null = null,
|
||||
workerOptions?: WorkerOptions,
|
||||
): IpnWorkerClient | null {
|
||||
const clientRef = useRef<IpnWorkerClient | null>(null)
|
||||
const [client, setClient] = useState<IpnWorkerClient | null>(null)
|
||||
const connect = useEffectEvent(
|
||||
(worker: string | URL, config: WorkerConfig) => {
|
||||
const sw = new SharedWorker(worker, { type: "module" })
|
||||
(worker: string | URL, config: WorkerConfig, workerOptions: WorkerOptions | undefined) => {
|
||||
const sw = new SharedWorker(worker, workerOptions)
|
||||
let cleanedUp = false
|
||||
let activeClient: IpnWorkerClient | null = null
|
||||
IpnWorkerClient.connect(sw.port, config).then((c) => {
|
||||
@@ -60,8 +61,8 @@ export function useBuildIpnWorker(
|
||||
},
|
||||
)
|
||||
useEffect(() => {
|
||||
if (worker && config && !clientRef.current) return connect(worker, config)
|
||||
}, [worker, config])
|
||||
if (worker && config && !clientRef.current) return connect(worker, config, workerOptions)
|
||||
}, [worker, config, workerOptions])
|
||||
return client
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user