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": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"@webnet/tsconnect": "*"
|
"@webnet/tsconnect": "*",
|
||||||
|
"@webnet/tsconnect-react": "*",
|
||||||
|
"@webnet/tsconnect-redux": "*",
|
||||||
|
"@webnet/tsconnect-worker": "*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.27.1",
|
"@babel/core": "^7.27.1",
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ function BaseDebug() {
|
|||||||
<br />
|
<br />
|
||||||
ipn:{" "}
|
ipn:{" "}
|
||||||
{ipn ? (
|
{ipn ? (
|
||||||
"present"
|
prepare.useWorker ? "present (SharedWorker)" : "present (main thread)"
|
||||||
) : prepare.willBuild ? (
|
) : prepare.willBuild ? (
|
||||||
"waiting..."
|
"waiting..."
|
||||||
) : (
|
) : (
|
||||||
@@ -88,25 +88,53 @@ function BaseDebug() {
|
|||||||
<br />
|
<br />
|
||||||
caCerts: {caCerts ? fmtSize(caCerts.length) : "loading..."}
|
caCerts: {caCerts ? fmtSize(caCerts.length) : "loading..."}
|
||||||
<br />
|
<br />
|
||||||
state:{" "}
|
<label>
|
||||||
<select
|
<input
|
||||||
disabled={!!ipn}
|
type="checkbox"
|
||||||
value={prepare.state}
|
disabled={!!ipn || !prepare.workerAvailable}
|
||||||
onChange={(e) => prepare.setState(e.target.value as typeof prepare.state)}
|
checked={prepare.useWorker && prepare.workerAvailable}
|
||||||
>
|
onChange={(e) => prepare.setUseWorker(e.target.checked)}
|
||||||
<option>disable</option>
|
/>{" "}
|
||||||
<option>localStorage</option>
|
use SharedWorker{!prepare.workerAvailable ? " (unavailable)" : ""}
|
||||||
<option>sessionStorage</option>
|
</label>
|
||||||
</select>
|
|
||||||
<br />
|
<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
|
<select
|
||||||
disabled={!!ipn}
|
disabled={!!ipn}
|
||||||
value={prepare.taildrop}
|
value={prepare.taildrop}
|
||||||
onChange={(e) => prepare.setTaildrop(e.target.value as typeof prepare.taildrop)}
|
onChange={(e) => prepare.setTaildrop(e.target.value as typeof prepare.taildrop)}
|
||||||
>
|
>
|
||||||
<option>disable</option>
|
<option>disable</option>
|
||||||
<option>memory</option>
|
<option value="memory">{prepare.useWorker ? "opfs" : "memory"}</option>
|
||||||
</select>
|
</select>
|
||||||
<br />
|
<br />
|
||||||
<label>
|
<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 { buildIpnStore, getIpnState, type IpnStore } from "@webnet/tsconnect-redux"
|
||||||
|
import type { WorkerConfig } from "@webnet/tsconnect-worker"
|
||||||
import {
|
import {
|
||||||
ContextType,
|
ContextType,
|
||||||
createContext,
|
createContext,
|
||||||
@@ -12,9 +13,13 @@ import {
|
|||||||
} from "react"
|
} from "react"
|
||||||
import { useIpnBuilder } from "./IpnBuilderContext"
|
import { useIpnBuilder } from "./IpnBuilderContext"
|
||||||
import { IpnContext, IpnStoreProvider } from "./tsconnect"
|
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 { useIpnSelector } from "../hooks/tsconnect"
|
||||||
import { useClient } from "../hooks/useClient"
|
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<{
|
export const IpnPrepareContext = createContext<{
|
||||||
// pre-built parts
|
// pre-built parts
|
||||||
@@ -38,6 +43,11 @@ export const IpnPrepareContext = createContext<{
|
|||||||
// automatically login to tailscale
|
// automatically login to tailscale
|
||||||
autoLogin: boolean
|
autoLogin: boolean
|
||||||
setAutoLogin: (autoLogin: boolean) => void
|
setAutoLogin: (autoLogin: boolean) => void
|
||||||
|
|
||||||
|
// worker vs main-thread mode
|
||||||
|
workerAvailable: boolean
|
||||||
|
useWorker: boolean
|
||||||
|
setUseWorker: (useWorker: boolean) => void
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
export function IpnProvider({
|
export function IpnProvider({
|
||||||
@@ -57,6 +67,8 @@ export function IpnProvider({
|
|||||||
defer?: boolean
|
defer?: boolean
|
||||||
autoLogin?: boolean
|
autoLogin?: boolean
|
||||||
}) {
|
}) {
|
||||||
|
const workerAvailable = typeof SharedWorker !== "undefined"
|
||||||
|
|
||||||
const storeRef = useRef<IpnStore | null>(null)
|
const storeRef = useRef<IpnStore | null>(null)
|
||||||
if (!storeRef.current) {
|
if (!storeRef.current) {
|
||||||
storeRef.current = buildIpnStore()
|
storeRef.current = buildIpnStore()
|
||||||
@@ -67,6 +79,7 @@ export function IpnProvider({
|
|||||||
const [willBuild, setWillBuild] = useState(!defer)
|
const [willBuild, setWillBuild] = useState(!defer)
|
||||||
const build = useCallback(() => setWillBuild(true), [setWillBuild])
|
const build = useCallback(() => setWillBuild(true), [setWillBuild])
|
||||||
|
|
||||||
|
const [useWorker, setUseWorker] = useState(workerAvailable)
|
||||||
const [state, setState] = useState(defaultState)
|
const [state, setState] = useState(defaultState)
|
||||||
const [taildrop, setTaildrop] = useState(defaultTaildrop)
|
const [taildrop, setTaildrop] = useState(defaultTaildrop)
|
||||||
|
|
||||||
@@ -77,6 +90,27 @@ export function IpnProvider({
|
|||||||
const [autoLogin, setAutoLogin] = useState(defaultAutoLogin)
|
const [autoLogin, setAutoLogin] = useState(defaultAutoLogin)
|
||||||
|
|
||||||
const client = useClient()
|
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 = useMemo(() => {
|
||||||
const builderParams = { ...ipnBuilderParams }
|
const builderParams = { ...ipnBuilderParams }
|
||||||
|
|
||||||
@@ -96,7 +130,17 @@ export function IpnProvider({
|
|||||||
return builderParams
|
return builderParams
|
||||||
}, [ipnBuilderParams, state, taildrop, hostname, controlURL, authKey, client])
|
}, [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 })
|
if (typeof window !== "undefined") Object.assign(window, { ipn })
|
||||||
|
|
||||||
const prepare = useMemo<NonNullable<ContextType<typeof IpnPrepareContext>>>(
|
const prepare = useMemo<NonNullable<ContextType<typeof IpnPrepareContext>>>(
|
||||||
@@ -118,6 +162,10 @@ export function IpnProvider({
|
|||||||
|
|
||||||
autoLogin,
|
autoLogin,
|
||||||
setAutoLogin,
|
setAutoLogin,
|
||||||
|
|
||||||
|
workerAvailable,
|
||||||
|
useWorker,
|
||||||
|
setUseWorker,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
state,
|
state,
|
||||||
@@ -134,11 +182,14 @@ export function IpnProvider({
|
|||||||
build,
|
build,
|
||||||
autoLogin,
|
autoLogin,
|
||||||
setAutoLogin,
|
setAutoLogin,
|
||||||
|
workerAvailable,
|
||||||
|
useWorker,
|
||||||
|
setUseWorker,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IpnStoreProvider store={store}>
|
<IpnStoreProvider store={activeStore}>
|
||||||
{ipn && autoLogin && <AutoLogin ipn={ipn} />}
|
{ipn && autoLogin && <AutoLogin ipn={ipn} />}
|
||||||
<IpnContext value={ipn}>
|
<IpnContext value={ipn}>
|
||||||
<IpnPrepareContext value={prepare}>{children}</IpnPrepareContext>
|
<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)
|
const state = useIpnSelector(getIpnState)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state === "NeedsLogin") ipn.login()
|
if (state === "NeedsLogin") ipn.login()
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
import "@webnet/tsconnect-worker/worker"
|
||||||
@@ -39,12 +39,13 @@ export function useBuildIpnWorker(
|
|||||||
worker: string | URL | null,
|
worker: string | URL | null,
|
||||||
config: WorkerConfig | null = null,
|
config: WorkerConfig | null = null,
|
||||||
runParams: Parameters<IPN["run"]>[0] | null = null,
|
runParams: Parameters<IPN["run"]>[0] | null = null,
|
||||||
|
workerOptions?: WorkerOptions,
|
||||||
): IpnWorkerClient | null {
|
): IpnWorkerClient | null {
|
||||||
const clientRef = useRef<IpnWorkerClient | null>(null)
|
const clientRef = useRef<IpnWorkerClient | null>(null)
|
||||||
const [client, setClient] = useState<IpnWorkerClient | null>(null)
|
const [client, setClient] = useState<IpnWorkerClient | null>(null)
|
||||||
const connect = useEffectEvent(
|
const connect = useEffectEvent(
|
||||||
(worker: string | URL, config: WorkerConfig) => {
|
(worker: string | URL, config: WorkerConfig, workerOptions: WorkerOptions | undefined) => {
|
||||||
const sw = new SharedWorker(worker, { type: "module" })
|
const sw = new SharedWorker(worker, workerOptions)
|
||||||
let cleanedUp = false
|
let cleanedUp = false
|
||||||
let activeClient: IpnWorkerClient | null = null
|
let activeClient: IpnWorkerClient | null = null
|
||||||
IpnWorkerClient.connect(sw.port, config).then((c) => {
|
IpnWorkerClient.connect(sw.port, config).then((c) => {
|
||||||
@@ -60,8 +61,8 @@ export function useBuildIpnWorker(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (worker && config && !clientRef.current) return connect(worker, config)
|
if (worker && config && !clientRef.current) return connect(worker, config, workerOptions)
|
||||||
}, [worker, config])
|
}, [worker, config, workerOptions])
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user