{"_id":"@dammers/use-signalr","_rev":"5-31a8b120efa2ad70a322880dd45f6435","name":"@dammers/use-signalr","dist-tags":{"latest":"0.3.0"},"versions":{"0.1.0":{"name":"@dammers/use-signalr","version":"0.1.0","keywords":["react","signalr","hooks","typescript","realtime","websocket"],"author":{"name":"Dammers"},"license":"MIT","_id":"@dammers/use-signalr@0.1.0","maintainers":[{"name":"dammers","email":"dannyheld2001@gmx.de"}],"homepage":"https://github.com/DammersCode/use-signalr#readme","bugs":{"url":"https://github.com/DammersCode/use-signalr/issues"},"dist":{"shasum":"ce3af8d3368d3651dd938271e7c3ae5dcbcb4c9c","tarball":"https://registry.npmjs.org/@dammers/use-signalr/-/use-signalr-0.1.0.tgz","fileCount":25,"integrity":"sha512-GQ8R5kGop/EgsddeMsLc3ZxookNdZJDNskjV9qyjP3CggPKbMvXplHpNEUqhXnlzlIEvufJbw31FJ3xyBUZJ6A==","signatures":[{"sig":"MEQCIHIU3VtrpGeyaylXybqOlpJsDaYv4+XVrKQof7iMxJhxAiB23LkGj2brmaHaTrzq60YyGjTQxJYgOpPH8j5DaGJgIg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":48206},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"288ec2a3e21316eb7e96e29db7bffc24bdf45aea","scripts":{"build":"tsc -p tsconfig.json","typecheck":"tsc --noEmit","prepublishOnly":"npm run build"},"_npmUser":{"name":"dammers","email":"dannyheld2001@gmx.de"},"repository":{"url":"git+https://github.com/DammersCode/use-signalr.git","type":"git"},"_npmVersion":"11.6.2","description":"Fully-typed multi-hub SignalR provider + hooks for React: auto-reconnect, invoke retry, lazy hubs, live status.","directories":{},"sideEffects":false,"_nodeVersion":"24.13.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"react":">=19","typescript":">=5","@types/react":">=19","@microsoft/signalr":">=8"},"peerDependencies":{"react":">=19","react-dom":">=19","@microsoft/signalr":">=8"},"_npmOperationalInternal":{"tmp":"tmp/use-signalr_0.1.0_1781621151465_0.5173695915752863","host":"s3://npm-registry-packages-npm-production"},"deprecated":"Moved to @dammers/use-signalr-react"},"0.1.1":{"name":"@dammers/use-signalr","version":"0.1.1","keywords":["react","signalr","hooks","typescript","realtime","websocket"],"author":{"name":"Dammers"},"license":"MIT","_id":"@dammers/use-signalr@0.1.1","maintainers":[{"name":"dammers","email":"dannyheld2001@gmx.de"}],"homepage":"https://github.com/DammersCode/use-signalr#readme","bugs":{"url":"https://github.com/DammersCode/use-signalr/issues"},"dist":{"shasum":"d658d246bfe4774ed88f09bb2f729f27404b09fc","tarball":"https://registry.npmjs.org/@dammers/use-signalr/-/use-signalr-0.1.1.tgz","fileCount":25,"integrity":"sha512-71v3x4Q4PYpor8C2WLQIFRuk8ymYV4CEBhhH7PzmgnosjKXcV9gD8PgXblyfa243TIsq6J0LwiIbttld4PbJ8A==","signatures":[{"sig":"MEYCIQCHPsrH++aKFKGbI/baVOYrO3Adza4UUjSb2cOlSjRBFwIhALu+p/uculsNhMOPBSfngG7reZ3KurUqbPzfaF7q8Eeq","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":48338},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"52d404472ebccf4ec290816e1688a618a0592a4e","scripts":{"build":"tsc -p tsconfig.json","typecheck":"tsc --noEmit","prepublishOnly":"npm run build"},"_npmUser":{"name":"dammers","email":"dannyheld2001@gmx.de"},"repository":{"url":"git+https://github.com/DammersCode/use-signalr.git","type":"git"},"_npmVersion":"11.6.2","description":"Fully-typed multi-hub SignalR provider + hooks for React: auto-reconnect, invoke retry, lazy hubs, live status.","directories":{},"sideEffects":false,"_nodeVersion":"24.13.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"react":">=19","typescript":">=5","@types/react":">=19","@microsoft/signalr":">=8"},"peerDependencies":{"react":">=19","react-dom":">=19","@microsoft/signalr":">=8"},"_npmOperationalInternal":{"tmp":"tmp/use-signalr_0.1.1_1781621877895_0.4669667540127127","host":"s3://npm-registry-packages-npm-production"},"deprecated":"Moved to @dammers/use-signalr-react"},"0.2.1":{"name":"@dammers/use-signalr","version":"0.2.1","keywords":["react","signalr","hooks","typescript","realtime","websocket"],"author":{"name":"Dammers"},"license":"MIT","_id":"@dammers/use-signalr@0.2.1","maintainers":[{"name":"dammers","email":"dannyheld2001@gmx.de"}],"homepage":"https://github.com/DammersCode/use-signalr#readme","bugs":{"url":"https://github.com/DammersCode/use-signalr/issues"},"dist":{"shasum":"5605da73b8969d00b22779e7c5a99aba29436e74","tarball":"https://registry.npmjs.org/@dammers/use-signalr/-/use-signalr-0.2.1.tgz","fileCount":25,"integrity":"sha512-04Rdz3RY3JdwGPGY4g3z7nVZ5sycUFUhNRRPdcm2mwkk17TNuFGsFYAgdLIIT0uC1FAvju2oPmcciJeMAPTl1Q==","signatures":[{"sig":"MEYCIQD8mYLFX932WsIwjuPA96oLSDEOLjsZRqn9y+JiuThWzQIhAKmjlWexWTJeCi5xHoZVJiEX5lh8KI9UJbbZdG1gFOMv","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":65713},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"2c6be1d57f79dcb9d983088b6ef2a5f2f6a86762","scripts":{"test":"vitest run","build":"tsc -p tsconfig.json","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"npm run build"},"_npmUser":{"name":"dammers","email":"dannyheld2001@gmx.de"},"repository":{"url":"git+https://github.com/DammersCode/use-signalr.git","type":"git"},"_npmVersion":"11.6.2","description":"Fully-typed multi-hub SignalR provider + hooks for React: auto-reconnect, invoke retry, lazy hubs, live status.","directories":{},"sideEffects":false,"_nodeVersion":"24.13.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^29.1.1","react":">=19","vitest":"^4.1.9","react-dom":">=19","typescript":">=5","@types/react":">=19","@microsoft/signalr":">=8","@testing-library/react":"^16.1.0"},"peerDependencies":{"react":">=19","react-dom":">=19","@microsoft/signalr":">=8"},"_npmOperationalInternal":{"tmp":"tmp/use-signalr_0.2.1_1782302112329_0.6367579549702675","host":"s3://npm-registry-packages-npm-production"},"deprecated":"Moved to @dammers/use-signalr-react"},"0.3.0":{"name":"@dammers/use-signalr","version":"0.3.0","keywords":["react","signalr","hooks","typescript","realtime","websocket"],"author":{"name":"Dammers"},"license":"MIT","_id":"@dammers/use-signalr@0.3.0","maintainers":[{"name":"dammers","email":"dannyheld2001@gmx.de"}],"homepage":"https://github.com/DammersCode/use-signalr#readme","bugs":{"url":"https://github.com/DammersCode/use-signalr/issues"},"dist":{"shasum":"7cc3e4dbb88b9d09eace14291830d685f1e58323","tarball":"https://registry.npmjs.org/@dammers/use-signalr/-/use-signalr-0.3.0.tgz","fileCount":25,"integrity":"sha512-Jp9m01VJqKPbwYYjfR9JQRUHkbe4Ukte2DVKnP8BquyPalgphdchge7dZ+q6A3YXFwpFR2gRbOgYzYOjvDTDEw==","signatures":[{"sig":"MEUCIDzZNlL/CUtbLg+9FlgS9i44ija1hbuKvQvS9HYG/pTuAiEAquakaJsWWYLf73Zhr3X6dgAdK3RcwuViFBGaE8tt/Kw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":71062},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"dcc5ce831c89f52b7a29937efb5b85d6fbbdcda0","scripts":{"test":"vitest run","build":"tsc -p tsconfig.build.json","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"npm run build"},"_npmUser":{"name":"dammers","email":"dannyheld2001@gmx.de"},"repository":{"url":"git+https://github.com/DammersCode/use-signalr.git","type":"git"},"_npmVersion":"11.6.2","description":"Fully-typed multi-hub SignalR provider + hooks for React: auto-reconnect, invoke retry, lazy hubs, live status.","directories":{},"sideEffects":false,"_nodeVersion":"24.13.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^29.1.1","react":">=19","vitest":"^4.1.9","react-dom":">=19","typescript":">=5","@types/react":">=19","@microsoft/signalr":">=8","@testing-library/react":"^16.1.0"},"peerDependencies":{"react":">=19","react-dom":">=19","@microsoft/signalr":">=8"},"_npmOperationalInternal":{"tmp":"tmp/use-signalr_0.3.0_1783719078976_0.2603765021523985","host":"s3://npm-registry-packages-npm-production"},"deprecated":"Moved to @dammers/use-signalr-react"}},"time":{"created":"2026-06-16T14:45:51.193Z","modified":"2026-08-09T20:53:33.188Z","0.1.0":"2026-06-16T14:45:51.597Z","0.1.1":"2026-06-16T14:57:58.038Z","0.2.1":"2026-06-24T11:55:12.472Z","0.3.0":"2026-07-10T21:31:19.132Z"},"bugs":{"url":"https://github.com/DammersCode/use-signalr/issues"},"author":{"name":"Dammers"},"license":"MIT","homepage":"https://github.com/DammersCode/use-signalr#readme","keywords":["react","signalr","hooks","typescript","realtime","websocket"],"repository":{"url":"git+https://github.com/DammersCode/use-signalr.git","type":"git"},"description":"Fully-typed multi-hub SignalR provider + hooks for React: auto-reconnect, invoke retry, lazy hubs, live status.","maintainers":[{"name":"dammers","email":"dannyheld2001@gmx.de"}],"readme":"# 🔌 @dammers/use-signalr\n\n> Fully-typed, reusable [SignalR](https://learn.microsoft.com/aspnet/core/signalr) provider + hooks for React — driven entirely by **your** contract.\n\n[![npm](https://img.shields.io/npm/v/@dammers/use-signalr.svg)](https://www.npmjs.com/package/@dammers/use-signalr)\n[![types](https://img.shields.io/badge/types-included-blue.svg)](#-api)\n[![license](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)\n\n```ts\nconst { SignalRProvider, useSignalRInvoke } = createSignalRClient({\n  hubs: {\n    \"/hubs/chat\": {\n      methods: { SendMessage: method<[roomId: string, message: string]>() },\n    },\n  },\n});\n```\n\nOne factory call gives you a provider and a set of hooks, every one of them typed against your hub contract — inferred straight from the config, no separately hand-written contract type. Event args, method args and return values are all inferred.\n\n---\n\n## ✨ Features\n\n- 🌐 **Many hubs, one provider.** Manage any number of hubs side by side — each gets its own connection, status, config and lifecycle. List them as keys; the hooks take the hub you want.\n- 🧠 **Fully typed, contract inferred from config.** Declare each hub's events/methods once with `event()`/`method()` — no hand-written contract type, no `declare module`, no globals. Event args, method args and return values are all inferred.\n- 🔇 **No \"No client method found\" warnings, by construction.** Every event you declare is automatically pre-bound to a no-op handler at connection build time — nothing to opt into, nothing that can drift from the contract.\n- ⚙️ **Per-hub & global config.** Set defaults once, override anything per hub: reconnect strategy, retries, transport, logging, lazy behavior.\n- ♻️ **Auto-reconnect.** Built-in: `true`, a custom delay array, or your own retry policy. Plus a connect-retry budget for the first connect.\n- 🔁 **Invoke retry.** Opt-in per call, idempotent-safe, with jittered backoff and smart retriable-vs-business-error detection.\n- 💤 **Lazy hubs.** Connect on first use, disconnect (after a grace period) on last unmount. Ref-counted and StrictMode-safe.\n- 🟢 **Live per-hub status.** Subscribe to a hub's connection state; components re-render only when _that_ hub changes.\n- 🔄 **Reconnect hooks.** Run a callback after a hub reconnects — e.g. refetch state that went stale.\n- 🔑 **Auth via props.** Pass `baseUrl` + `accessTokenFactory` (gate with the optional `enabled`); the token is re-read on every negotiate, so rotation needs no rebuild.\n- 🪶 **Zero runtime deps.** Only peer deps: `react`, `react-dom`, `@microsoft/signalr`.\n\n## 📦 Install\n\n```bash\nnpm i @dammers/use-signalr @microsoft/signalr\n```\n\nPeer deps: `react` ≥ 19, `react-dom` ≥ 19, `@microsoft/signalr` ≥ 8 (tested against 8–10).\nReact 19 is required — the library uses the `use` hook and JSX context providers.\n\n## 🚀 Usage\n\n### 1. Define your contract and create the client\n\nYour app contract isn't hand-written — it's **inferred from the config**. The\n**keys of `config.hubs` declare the hubs**; each hub's `events` (what the\nserver pushes to you) and `methods` (what you invoke) are declared inline\nusing the `event()` and `method()` markers.\n\n```ts\n// signalr.ts\nimport { createSignalRClient, event, method } from \"@dammers/use-signalr\";\n\nexport const {\n  SignalRProvider,\n  useSignalR,\n  useSignalREffect,\n  useSignalRInvoke,\n  useSignalRSend,\n  useSignalRTeardown,\n  useHubStatus,\n  useOnReconnected,\n  useHubConsumer,\n} = createSignalRClient({\n  hubs: {\n    \"/hubs/chat\": {\n      events: {\n        ReceiveMessage: event<[user: string, message: string]>(),\n      },\n      methods: {\n        SendMessage: method<[roomId: string, message: string]>(),\n        JoinRoom: method<[roomId: string], { success: boolean }>(),\n      },\n      // per-hub config also goes here (see \"Per-hub config\")\n    },\n  },\n  // global defaults (all optional):\n  // lazy: false, reconnect: true, maxConnectRetries: 2, logLevel: LogLevel.Information\n});\n```\n\n`event<Args>()` takes the handler's argument tuple; `method<Args, Return>()`\ntakes the argument tuple and the resolved return type (defaults to `void` if\nomitted). Neither returns anything meaningful at runtime — they're phantom-typed\nmarkers whose only job is to carry the types for inference. `createSignalRClient`\nis called with **no explicit generic**: its type is inferred from the config\nobject you pass.\n\n### 2. Mount the provider with your auth\n\nThe provider takes **no hubs prop** — it already knows them from the config.\n\n```tsx\nimport { SignalRProvider } from \"./signalr\";\n\n<SignalRProvider\n  baseUrl={serverUrl} // e.g. \"https://api.example.com\"\n  accessTokenFactory={() => getAccessToken()} // sync or async; read on every (re)negotiate\n  enabled={isAuthenticated} // optional, default true; false -> stops + clears all connections\n  connectionKey={accessToken} // optional: forces reconnect when it changes (re-login)\n  onError={(hub, err) => toast.error(`Connection to ${hub} failed`)}\n  onStatusChange={(hub, status) => {\n    if (status === \"reconnecting\") toast.warning(`Reconnecting to ${hub}…`);\n    if (status === \"reconnected\") toast.success(`Reconnected to ${hub}`);\n  }}\n>\n  <App />\n</SignalRProvider>;\n```\n\n### 3. Use the hooks — everything below is fully typed\n\n```tsx\n// 📥 Listen to a server event — args inferred from the contract\nuseSignalREffect(\"/hubs/chat\", \"ReceiveMessage\", (user, message) => {\n  console.log(user, message);\n});\n\n// 📤 Invoke a server method — args + return inferred, waits for connection\nconst sendMessage = useSignalRInvoke(\"/hubs/chat\", \"SendMessage\");\nawait sendMessage(roomId, \"hello\"); // typed params, Promise<void>\n\n// 🏹 Typed fire-and-forget — no connect-wait, dropped if the hub isn't connected.\n// Stable across renders, so it's safe to capture in an unmount cleanup.\nconst send = useSignalRSend(\"/hubs/chat\", \"SendMessage\");\nawait send(roomId, \"bye\"); // typed args; Promise<boolean> (true = dispatched)\n\n// 🚪 Reliable teardown — for a method called in an effect cleanup. Survives\n// unmount, queues if the hub is still connecting (instead of dropping), holds a\n// lazy hub open until it flushes. Best-effort: Promise<boolean> (true = dispatched).\nconst leaveRoom = useSignalRTeardown(\"/hubs/chat\", \"LeaveRoomAsync\");\nuseEffect(() => {\n  joinRoom(roomId);\n  return () => {\n    leaveRoom(roomId);\n  }; // lands even mid-connect or on unmount\n}, [roomId, joinRoom, leaveRoom]);\n\n// 🟢 Live connection status (re-renders only when THIS hub's status changes)\nconst status = useHubStatus(\"/hubs/chat\"); // \"connecting\" | \"connected\" | \"reconnecting\" | ...\n\n// 🔄 Re-sync after a reconnect (e.g. refetch a query)\nuseOnReconnected(\"/hubs/chat\", () => refetchMessages());\n\n// ⚓ Keep a lazy hub connected for this component's lifetime without subscribing\nuseHubConsumer(\"/hubs/chat\");\n\n// 🛠️ Last resort: the raw HubConnection (prefer the typed hooks above)\nconst { getConnection } = useSignalR();\ngetConnection(\"/hubs/chat\")?.send(\"SendMessage\", roomId, \"bye\");\n```\n\n## ⚙️ Per-hub config\n\nEach value in `config.hubs` overrides the global defaults for that hub, alongside its `events`/`methods` declarations:\n\n```ts\ncreateSignalRClient({\n  hubs: {\n    \"/hubs/chat\": {\n      events: { ReceiveMessage: event<[user: string, message: string]>() },\n      methods: { SendMessage: method<[roomId: string, message: string]>() },\n    },\n    \"/hubs/presence\": {\n      events: { UserOnline: event<[userId: string]>() },\n      lazy: true, // connect only when first used\n      graceMs: 5000, // wait 5s after last consumer before disconnect\n      reconnect: [0, 2000, 10000, 30000], // custom retry delays (ms)\n      maxConnectRetries: 5,\n      transport: HttpTransportType.WebSockets,\n      skipNegotiation: true,\n    },\n  },\n  lazy: false, // global default for all hubs\n  reconnect: true, // true | false | number[] | IRetryPolicy\n  maxConnectRetries: 2,\n});\n```\n\n### 🔇 No \"No client method found\" warnings — by construction\n\n`@microsoft/signalr` logs a warning whenever the server pushes an event with\nno registered handler — which happens for any event no mounted component\ncurrently subscribes to via `useSignalREffect`. Every event you declare with\n`event()` in a hub's config is automatically pre-bound to a no-op handler at\nconnection build time (before `start()`) — there's no separate opt-in list to\nkeep in sync, and nothing to forget: if it's in the contract, it's pre-bound.\nReal handlers registered later via `useSignalREffect` (or `connection.on`)\nstill receive events normally — SignalR fans out to every registered handler.\nThis has no effect on connection lifecycle (lazy/eager behavior is unchanged).\n\n### 💤 Lazy hubs\n\nWith `lazy: true`, a hub connects only when the first component using it mounts (any hook for that hub) and disconnects `graceMs` after the last one unmounts. Ref-counted and StrictMode-safe. Default is eager.\n\n### 🔁 Invoke retry\n\n`useSignalRInvoke` fails fast by default (`retries: 0`, rethrows the raw server error). Opt in **only for idempotent methods** — a retried invoke is at-least-once:\n\n```ts\nconst undo = useSignalRInvoke(\"/hubs/flow\", \"UndoAsync\", {\n  retries: 2, // retry RETRIABLE failures (transport drops, 5xx, timeouts)\n  timeout: 15000, // per-attempt deadline\n  backoff: [250, 1000, 3000], // or (attempt) => ms; capped 30s, jittered\n});\n```\n\nBusiness errors (a `HubException` thrown while still connected) are **never** retried.\n\n### 🚪 send vs invoke vs teardown — which call to use\n\nThe three \"call the server\" hooks differ in how they wait, what they return, and what happens on unmount. Pick by intent:\n\n|                           | `useSignalRInvoke`                      | `useSignalRSend`                                | `useSignalRTeardown`             |\n| ------------------------- | --------------------------------------- | ----------------------------------------------- | -------------------------------- |\n| **Waits for connection**  | yes (up to `timeout`)                   | no                                              | yes (up to `timeout`)            |\n| **Not connected yet**     | waits, then invokes                     | **drops** (resolves `false`)                    | **queues**, flushes on connect   |\n| **Returns**               | the method's typed result               | `boolean` (dispatched?)                         | `boolean` (dispatched?)          |\n| **On unmount**            | aborts in-flight call¹                  | unaffected (reads conn at call time)            | **survives** (runs detached)     |\n| **Holds a lazy hub open** | while mounted                           | while mounted                                   | until the flush completes        |\n| **Use for**               | request/response you need the result of | high-frequency loss-OK signals (typing, cursor) | one-shot teardown that must land |\n\n¹ Only a mid-backoff retry is actually cancelled; pass `{ keepAliveOnUnmount: true }` to keep it alive.\n\n#### Reliable join/leave (session pattern)\n\nA common pattern: join a session on mount, leave it in the effect cleanup.\n\n```tsx\nconst joinRoom = useSignalRInvoke(\"/hubs/chat\", \"JoinRoomAsync\");\nconst leaveRoom = useSignalRTeardown(\"/hubs/chat\", \"LeaveRoomAsync\");\n\nuseEffect(() => {\n  joinRoom(roomId);\n  return () => {\n    leaveRoom(roomId);\n  };\n}, [roomId, joinRoom, leaveRoom]);\n```\n\nA plain `useSignalRInvoke` or `useSignalRSend` makes the **leave** unreliable:\n\n- `useSignalRInvoke` aborts in-flight calls on unmount — a leave issued in cleanup can be cancelled before it reaches the server.\n- `useSignalRSend` drops silently if the hub isn't `Connected` — so a leave that races a still-connecting socket (StrictMode's first mount, fast route switches) is lost.\n\n`useSignalRTeardown` fixes both. It:\n\n- **survives the calling component's unmount** (runs detached, never aborted),\n- **queues while connecting** — waits up to `timeout` (default 10s) for the hub, then sends, instead of dropping,\n- **holds a lazy hub open** until the flush completes, even if the unmounting component was its last consumer.\n\nIt's best-effort fire-and-forget: resolves `true` once dispatched, `false` if the hub never connected in time; it never throws. Under StrictMode's mount→cleanup→mount, the intermediate teardown **does** land (then the remount re-runs setup) — so the server is never left in a stale joined state, at the cost of one extra round-trip.\n\n> Already use `useSignalRInvoke` for your leave and only need it not to be aborted on unmount? Pass `{ keepAliveOnUnmount: true }`. That covers the abort half but **not** the still-connecting race — for that, use `useSignalRTeardown`.\n\n## 📚 API\n\n| Export                                   | What it does                                                                                                                                                         |\n| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `createSignalRClient(config)`            | Returns the Provider + hooks, typed against the contract **inferred** from `config`. Config keys declare the hubs; no explicit generic needed.                       |\n| `event<Args>()`                          | Declares a server-pushed event inside a hub's `events`; `Args` is the handler's argument tuple.                                                                       |\n| `method<Args, Return?>()`                | Declares an invocable server method inside a hub's `methods`; `Args` is the argument tuple, `Return` the resolved return type (default `void`).                      |\n| `<SignalRProvider>`                      | Builds/starts connections, retries, auto-reconnects, exposes them via context. No `hubs` prop.                                                                       |\n| `useSignalREffect(hub, event, handler)`  | Subscribe to a server event for the component lifetime.                                                                                                              |\n| `useSignalRInvoke(hub, method, opts?)`   | Typed request/response invoker; waits for the connection, returns the method's result. Optional retry/backoff/timeout; `keepAliveOnUnmount` to not abort on unmount. |\n| `useSignalRSend(hub, method)`            | Typed fire-and-forget sender; **drops** if not connected. For high-frequency loss-OK signals. Safe in unmount cleanups.                                              |\n| `useSignalRTeardown(hub, method, opts?)` | Reliable teardown sender for a method called in cleanup: survives unmount, **queues** while connecting (instead of dropping), holds a lazy hub open until flushed.   |\n| `useHubStatus(hub)`                      | Live connection status; re-renders only when that hub changes.                                                                                                       |\n| `useOnReconnected(hub, cb)`              | Run `cb` after the hub reconnects (e.g. refetch).                                                                                                                    |\n| `useHubConsumer(hub)`                    | Keep a lazy hub connected for the component's lifetime without subscribing.                                                                                          |\n| `useSignalR()`                           | Last-resort raw context: `getConnection`, `isHubConnected`, `getStatus`.                                                                                             |\n\n### Provider props\n\n`baseUrl`, `accessTokenFactory` (required); `enabled` (optional, default `true`), `connectionKey`, `onStatusChange`, `onError` (optional). Connection behavior (`lazy`, `reconnect`, `maxConnectRetries`, `logLevel`, per-hub overrides) lives in the **config** passed to `createSignalRClient`, not on the provider.\n\n## 📝 Notes\n\n- The provider rebuilds connections when `baseUrl`, `enabled`, or `connectionKey` change. Token _rotation_ alone does **not** rebuild — `accessTokenFactory` is re-read on every negotiate.\n- `accessTokenFactory` and the `on*` callbacks are read through refs, so passing fresh closures each render is fine — no reconnect storm.\n\n## 🤝 Contributing\n\nSetup, scripts and workflow live in [CONTRIBUTING.md](./CONTRIBUTING.md).\n\n## 📄 License\n\n[MIT](./LICENSE) © [DammersCode](https://github.com/DammersCode)\n","readmeFilename":"README.md"}