feat(tsconnect-react): add useBuildIpnWorker hook for SharedWorker clients

- Add disconnect() to IpnWorkerClient (releases Web Lock without shutdown)
- Widen IpnContext from IPN to IpnClient
- Add useBuildIpnWorker(worker, config, runParams) hook with StrictMode-safe
  cleanup: disconnect() fires if the cleanup runs before connect() resolves,
  or immediately after if it resolves first
- Export useBuildIpnWorker from package index
- Add @webnet/tsconnect-worker to package dependencies

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 20:13:06 +00:00
co-authored by Claude
parent 34c35fa91a
commit 8d4133575e
5 changed files with 47 additions and 3 deletions
+2 -1
View File
@@ -17,7 +17,8 @@
},
"dependencies": {
"@webnet/tsconnect": "*",
"@webnet/tsconnect-redux": "*"
"@webnet/tsconnect-redux": "*",
"@webnet/tsconnect-worker": "*"
},
"devDependencies": {
"@types/react": "^19.2.14",
+2 -2
View File
@@ -1,14 +1,14 @@
import { Provider, type ReactReduxContextValue, type ProviderProps } from "react-redux"
import type { IpnAction, IpnState } from "@webnet/tsconnect-redux"
import { createContext } from "react"
import type { IPN } from "@webnet/tsconnect"
import type { IpnClient } from "@webnet/tsconnect"
export type IpnStoreContext = ReturnType<typeof createIpnStoreContext>
export const createIpnStoreContext = () =>
createContext<ReactReduxContextValue<IpnState, IpnAction> | null>(null)
export type IpnContext = ReturnType<typeof createIpnContext>
export const createIpnContext = () => createContext<IPN | null>(null)
export const createIpnContext = () => createContext<IpnClient | null>(null)
export const IpnStoreProvider = (props: ProviderProps<IpnAction, IpnState>) =>
Provider<IpnAction, IpnState>(props)
+32
View File
@@ -2,6 +2,8 @@ import { useEffect, useEffectEvent, useRef, useState } from "react"
import type { initIPN, IPN } from "@webnet/tsconnect"
import type { IpnDispatch, IpnState, IpnStore } from "@webnet/tsconnect-redux"
import { runWithStore } from "@webnet/tsconnect-redux"
import { IpnWorkerClient } from "@webnet/tsconnect-worker"
import type { WorkerConfig } from "@webnet/tsconnect-worker"
import {
createDispatchHook,
createSelectorHook,
@@ -33,6 +35,36 @@ export function useBuildIpn(
return ipn
}
export function useBuildIpnWorker(
worker: SharedWorker | string | URL | null,
config: WorkerConfig | null = null,
runParams: Parameters<IPN["run"]>[0] | null = null,
): IpnWorkerClient | null {
const clientRef = useRef<IpnWorkerClient | null>(null)
const [client, setClient] = useState<IpnWorkerClient | null>(null)
const connect = useEffectEvent(
(worker: SharedWorker | string | URL, config: WorkerConfig) => {
const sw = worker instanceof SharedWorker ? worker : new SharedWorker(worker, { type: "module" })
let cleanedUp = false
let activeClient: IpnWorkerClient | null = null
IpnWorkerClient.connect(sw.port, config).then((c) => {
if (cleanedUp) { c.disconnect(); return }
activeClient = clientRef.current = c
c.run(runParams ?? {})
setClient(c)
})
return () => {
cleanedUp = true
if (activeClient) { activeClient.disconnect(); clientRef.current = null }
}
},
)
useEffect(() => {
if (worker && config && !clientRef.current) return connect(worker, config)
}, [worker, config])
return client
}
export const useIpnStore = useStore.withTypes<IpnStore>()
export const useIpnSelector = useSelector.withTypes<IpnState>()
export const useIpnDispatch = useDispatch.withTypes<IpnDispatch>()
+1
View File
@@ -11,6 +11,7 @@ export {
createUseIpnSelector,
createUseIpnStore,
useBuildIpn,
useBuildIpnWorker,
useIpnDispatch,
useIpnSelector,
useIpnStore,
+10
View File
@@ -548,9 +548,19 @@ export class IpnWorkerClient implements IpnClient {
if (ofList.length) opts.notifyOutgoingFiles?.(ofList as IPNOutgoingFile[])
}
/** Release this client's hold on the worker without shutting the worker down.
* The worker cleans up all resources opened by this client and shuts down
* only if no other clients remain. Safe to call after {@link shutdown}. */
disconnect(): void {
this.#running = false
this.#releaseLock?.()
this.#releaseLock = null
}
async shutdown(): Promise<void> {
this.#running = false
this.#releaseLock?.()
this.#releaseLock = null
await this.#call("shutdown", [])
}