# Preact Missing Hooks

<p align="left">
  <a href="https://www.npmjs.com/package/preact-missing-hooks">
    <img src="https://img.shields.io/npm/v/preact-missing-hooks?color=crimson&label=npm%20version" alt="npm version" />
  </a>
  <a href="https://www.npmjs.com/package/preact-missing-hooks">
    <img src="https://img.shields.io/npm/dt/preact-missing-hooks?label=total%20downloads" alt="total downloads" />
  </a>

  <a href="https://github.com/prakhardubey2002/preact-missing-hooks/actions/workflows/test-hooks.yml">
    <img src="https://img.shields.io/github/actions/workflow/status/prakhardubey2002/preact-missing-hooks/test-hooks.yml?branch=main&label=build%20status" alt="Build Status" />
  </a>
</p>

If this package helps you, please consider dropping a star on the [GitHub repo](https://github.com/prakhardubey2002/Preact-Missing-Hooks).

A lightweight, extendable collection of React-like hooks for Preact, including utilities for transitions, DOM mutation observation, global event buses, theme detection, network status, clipboard access, rage-click detection (e.g. for Sentry), a priority task queue (sequential or parallel), a production-ready **IndexedDB** hook with tables, transactions, and a full CRUD API, and **WebRTC-based IP detection** (`useWebRTCIP`) for frontend-only IP hints.

---

## Features

- **`useTransition`** — Defers state updates to yield a smoother UI experience.
- **`useMutationObserver`** — Reactively observes DOM changes with a familiar hook API.
- **`useEventBus`** — A simple publish/subscribe system, eliminating props drilling or overuse of context.
- **`useWrappedChildren`** — Injects props into child components with flexible merging strategies.
- **`usePreferredTheme`** — Detects the user's preferred color scheme (light/dark) from system preferences.
- **`useNetworkState`** — Tracks online/offline status and connection details (type, downlink, RTT, save-data).
- **`usePrefetch`** — Preload URLs (documents or data) so they are cached before navigation or use. Ideal for link hover or route preloading. Returns `prefetch(url, options?)` and `isPrefetched(url)`.
- **`usePoll`** — Polls an async function at a fixed interval until it returns `{ done: true, data? }`. Stops on error. Returns `data`, `done`, `error`, `pollCount`, `start`, `stop`. Good for readiness checks or waiting on a backend job.
- **`useDeviceData`** — Extracts device and browser data from native Navigator, Screen, `window`, and `matchMedia` APIs (browser name/version, OS name/version, language, platform, CPUs, memory, screen/viewport size, touch, color scheme, reduced motion, Client Hints). Uses Client Hints high-entropy values when available. Optionally polls the Battery Status API. Updates on resize, orientation, and preference changes.
- **`useClipboard`** — Copy and paste text with the Clipboard API, with copied/error state.
- **`useRageClick`** — Detects rage clicks (repeated rapid clicks in the same spot). Use with Sentry or similar to detect and fix rage-click issues and lower rage-click-related support.
- **`useThreadedWorker`** — Run async work in a queue with **sequential** (single worker, priority-ordered) or **parallel** (worker pool) mode. Optional priority (1 = highest); FIFO within same priority.
- **`useIndexedDB`** — IndexedDB abstraction with database/table init, insert, update, delete, exists, query (cursor + filter), upsert, bulk insert, clear, count, and full transaction support. Singleton connection, Promise-based API, optional `onSuccess`/`onError` callbacks.
- **`useWebRTCIP`** — Detects client IP addresses using WebRTC ICE candidates and a STUN server (frontend-only). **Not highly reliable**; use as a first-priority hint and fall back to a public IP API (e.g. [ipapi.co](https://ipapi.co), [ipify](https://www.ipify.org), [ip-api.com](https://ip-api.com)) when it fails or returns empty.
- **`useWasmCompute`** — Runs WebAssembly computation off the main thread via a Web Worker. Validates environment (browser, Worker, WebAssembly) and returns `compute(input)`, `result`, `loading`, `error`, `ready`.
- **`useWorkerNotifications`** — Listens to a Worker's messages and maintains state: running tasks, completed/failed counts, event history, average task duration, throughput per second, and queue size. Worker posts `task_start` / `task_end` / `task_fail` / `queue_size`; returns `progress` (default view of all active worker data) plus individual stats.
- **`useLLMMetadata`** — Injects an AI-readable metadata block into the document head on route change. Works in React 18+ and Preact 10+. Supports **manual** (title, description, tags) and **auto-extract** (from `document.title`, visible `h1`/`h2`, first 3 `p`). Cacheable, SSR-safe, no router dependency.
- **`useRefPrint`** — Binds a ref to a printable section and provides `print()` to open the native print dialog. Uses `@media print` CSS so only that section is printed (or saved as PDF). Options: `documentTitle`, `downloadAsPdf`.
- **`useRBAC`** — Frontend-only role-based access control. Define roles with conditions, assign capabilities per role. Pluggable user source: `localStorage`, `sessionStorage`, API, memory, or custom. Returns `user`, `roles`, `capabilities`, `hasRole(role)`, `can(capability)`, and storage helpers.
- Fully TypeScript compatible
- Bundled with Microbundle
- Zero dependencies (peer: `preact` or `react` — use `/react` for React)

---

## Installation

```bash
npm install preact-missing-hooks
```

Ensure your app has either **preact** or **react** installed (the package uses whichever is present).

---

## Import options

Use the same import in Preact and React projects:

```ts
import { useThreadedWorker, useClipboard } from "preact-missing-hooks";
```

- **How it picks Preact vs React**
  - **CommonJS / Node:** The package detects which of `preact` or `react` is installed and uses that build automatically.
  - **ESM (Vite, Webpack, etc.):** Default is the Preact build. In a **React** app, add the `react` condition so the package resolves to the React build:
    - **Vite:** `vite.config.ts` → `resolve: { conditions: ['react'] }`
    - **Webpack:** `resolve.conditionNames` (or similar) to include `'react'`
  - **Or** in React projects you can always import from the explicit entry: `preact-missing-hooks/react`.

- **Subpath exports (tree-shakeable)** — Import a single hook:

  ```ts
  import { useThreadedWorker } from "preact-missing-hooks/useThreadedWorker";
  import { useClipboard } from "preact-missing-hooks/useClipboard";
  import { usePrefetch } from "preact-missing-hooks/usePrefetch";
  import { usePoll } from "preact-missing-hooks/usePoll";
  import { useDeviceData } from "preact-missing-hooks/useDeviceData";
  import { useWebRTCIP } from "preact-missing-hooks/useWebRTCIP";
  import { useWasmCompute } from "preact-missing-hooks/useWasmCompute";
  import { useWorkerNotifications } from "preact-missing-hooks/useWorkerNotifications";
  ```

  All hooks are available: `useTransition`, `useMutationObserver`, `useEventBus`, `useWrappedChildren`, `usePreferredTheme`, `useNetworkState`, `useClipboard`, `usePrefetch`, `usePoll`, `useDeviceData`, `useRageClick`, `useThreadedWorker`, `useIndexedDB`, `useWebRTCIP`, `useWasmCompute`, `useWorkerNotifications`, `useLLMMetadata`, `useRefPrint`, `useRBAC`.

---

## Quick start

Minimal example (Preact or React):

```tsx
import {
  useTransition,
  useClipboard,
  usePreferredTheme,
} from "preact-missing-hooks";

function App() {
  const [startTransition, isPending] = useTransition();
  const { copy, copied } = useClipboard();
  const theme = usePreferredTheme();

  return (
    <div>
      <button
        onClick={() =>
          startTransition(() => {
            /* heavy update */
          })
        }
        disabled={isPending}
      >
        {isPending ? "Loading…" : "Update"}
      </button>
      <button onClick={() => copy("Hello!")}>
        {copied ? "Copied!" : "Copy"}
      </button>
      <span>Theme: {theme}</span>
    </div>
  );
}
```

**Live demo:** Try every hook with live examples:

- **Online:** [preact-missing-hooks.vercel.app](https://preact-missing-hooks.vercel.app/)
- **Local:** Run the docs demo:

```bash
npm run build && npx serve -l 5000
# Open http://localhost:5000/docs/
```

Or open `docs/index.html` after building (see [docs/README.md](docs/README.md) for details).

**Usage at a glance:**

| Hook                                              | One-liner                                                                                     |
| ------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| [useTransition](#usetransition)                   | `const [startTransition, isPending] = useTransition();`                                       |
| [useMutationObserver](#usemutationobserver)       | `useMutationObserver(ref, callback, { childList: true });`                                    |
| [useEventBus](#useeventbus)                       | `const { emit, on } = useEventBus();`                                                         |
| [useWrappedChildren](#usewrappedchildren)         | `const wrapped = useWrappedChildren(children, { className: 'x' });`                           |
| [usePreferredTheme](#usepreferredtheme)           | `const theme = usePreferredTheme(); // 'light' \| 'dark' \| 'no-preference'`                  |
| [useNetworkState](#usenetworkstate)               | `const { online, effectiveType } = useNetworkState();`                                        |
| [usePrefetch](#useprefetch)                       | `const { prefetch, isPrefetched } = usePrefetch();`                                           |
| [usePoll](#usepoll)                               | `const { data, done, pollCount, stop } = usePoll(pollFn, { intervalMs });`                    |
| [useDeviceData](#usedevicedata)                   | `const { browser, os } = useDeviceData();` — `browser.name`, `os.version`, viewport, etc.    |
| [useClipboard](#useclipboard)                     | `const { copy, paste, copied } = useClipboard();`                                             |
| [useRageClick](#userageclick)                     | `useRageClick(ref, { onRageClick, threshold: 5 });`                                           |
| [useThreadedWorker](#usethreadedworker)           | `const { run, loading, result } = useThreadedWorker(fn, { mode: 'sequential' });`             |
| [useIndexedDB](#useindexeddb)                     | `const { db, isReady } = useIndexedDB({ name, version, tables });`                            |
| [useWebRTCIP](#usewebrtcip)                       | `const { ips, loading, error } = useWebRTCIP({ timeout: 3000 });`                             |
| [useWasmCompute](#usewasmcompute)                 | `const { compute, result, ready } = useWasmCompute({ wasmUrl });`                             |
| [useWorkerNotifications](#useworkernotifications) | `const { progress, eventHistory } = useWorkerNotifications(worker);`                          |
| [useLLMMetadata](#usellmmetadata)                 | `useLLMMetadata({ route: pathname, mode: 'auto-extract' });`                                  |
| [useRefPrint](#userefprint)                       | `const { print } = useRefPrint(printRef, { documentTitle: 'Report' });`                       |
| [useRBAC](#userbac)                               | `const { can, hasRole, roles } = useRBAC({ userSource, roleDefinitions, roleCapabilities });` |

---

## Usage Examples

### `useTransition`

```tsx
import { useTransition } from "preact-missing-hooks";

function ExampleTransition() {
  const [startTransition, isPending] = useTransition();

  const handleClick = () => {
    startTransition(() => {
      // perform an expensive update here
    });
  };

  return (
    <button onClick={handleClick} disabled={isPending}>
      {isPending ? "Loading..." : "Click Me"}
    </button>
  );
}
```

---

### `useMutationObserver`

```tsx
import { useRef } from "preact/hooks";
import { useMutationObserver } from "preact-missing-hooks";

function ExampleMutation() {
  const ref = useRef<HTMLDivElement>(null);

  useMutationObserver(
    ref,
    (mutations) => {
      console.log("Detected mutations:", mutations);
    },
    { childList: true, subtree: true }
  );

  return <div ref={ref}>Observe this content</div>;
}
```

---

### `useEventBus`

```tsx
// types.ts
export type Events = {
  notify: (message: string) => void;
};

// Sender.tsx
import { useEventBus } from "preact-missing-hooks";
import type { Events } from "./types";

function Sender() {
  const { emit } = useEventBus<Events>();
  return <button onClick={() => emit("notify", "Hello World!")}>Send</button>;
}

// Receiver.tsx
import { useEventBus } from "preact-missing-hooks";
import { useState, useEffect } from "preact/hooks";
import type { Events } from "./types";

function Receiver() {
  const [msg, setMsg] = useState<string>("");
  const { on } = useEventBus<Events>();

  useEffect(() => {
    const unsubscribe = on("notify", setMsg);
    return unsubscribe;
  }, []);

  return <div>Message: {msg}</div>;
}
```

---

### `useWrappedChildren`

```tsx
import { useWrappedChildren } from "preact-missing-hooks";

function ParentComponent({ children }) {
  // Inject common props into all children
  const injectProps = {
    className: "enhanced-child",
    onClick: () => console.log("Child clicked!"),
    style: { border: "1px solid #ccc" },
  };

  const wrappedChildren = useWrappedChildren(children, injectProps);

  return <div className="parent">{wrappedChildren}</div>;
}

// Usage with preserve strategy (default - existing props are preserved)
function PreserveExample() {
  return (
    <ParentComponent>
      <button className="btn">Existing class preserved</button>
      <span style={{ color: "red" }}>Both styles applied</span>
    </ParentComponent>
  );
}

// Usage with override strategy (injected props override existing ones)
function OverrideExample() {
  const injectProps = { className: "new-class" };
  const children = (
    <button className="old-class">Class will be overridden</button>
  );

  const wrappedChildren = useWrappedChildren(children, injectProps, "override");

  return <div>{wrappedChildren}</div>;
}
```

---

### `usePreferredTheme`

```tsx
import { usePreferredTheme } from "preact-missing-hooks";

function ThemeAwareComponent() {
  const theme = usePreferredTheme(); // 'light' | 'dark' | 'no-preference'

  return <div data-theme={theme}>Your system prefers: {theme}</div>;
}
```

---

### `useNetworkState`

```tsx
import { useNetworkState } from "preact-missing-hooks";

function NetworkStatus() {
  const { online, effectiveType, saveData } = useNetworkState();

  return (
    <div>
      Status: {online ? "Online" : "Offline"}
      {effectiveType && ` (${effectiveType})`}
      {saveData && " — Reduced data mode enabled"}
    </div>
  );
}
```

---

### `useDeviceData`

```tsx
import { useDeviceData } from "preact-missing-hooks";

function EnvironmentBadge() {
  const { browser, os, viewport } = useDeviceData({
    includeBattery: false,
    includeHighEntropy: true,
  });

  return (
    <span>
      {browser.name} {browser.version} · {os.name} {os.version} ·{" "}
      {viewport.width}×{viewport.height}
    </span>
  );
}
```

See [full `useDeviceData` docs](#usedevicedata) for all fields, `getDeviceData()`, and `parseUserAgent()`.

---

### `useClipboard`

```tsx
import { useState } from "preact/hooks";
import { useClipboard } from "preact-missing-hooks";

function CopyButton() {
  const { copy, copied, error } = useClipboard({ resetDelay: 2000 });

  return (
    <button onClick={() => copy("Hello, World!")}>
      {copied ? "Copied!" : "Copy"}
    </button>
  );
}

function PasteInput() {
  const [text, setText] = useState("");
  const { paste } = useClipboard();

  const handlePaste = async () => {
    const content = await paste();
    setText(content);
  };

  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button onClick={handlePaste}>Paste</button>
    </div>
  );
}
```

---

### `usePrefetch`

Preload URLs (documents or data) so they are cached before navigation or use. Ideal for link hover or route preloading. Use `prefetch(url)` with optional `{ as: 'document' | 'fetch' }`; `as: 'fetch'` warms the HTTP cache (e.g. for API URLs).

```tsx
import { usePrefetch } from "preact-missing-hooks";

function NavLink({ href, children }) {
  const { prefetch, isPrefetched } = usePrefetch();
  return (
    <a href={href} onMouseEnter={() => prefetch(href)}>
      {children}
      {isPrefetched(href) && " ✓"}
    </a>
  );
}

// Prefetch API data
function DataLoader() {
  const { prefetch } = usePrefetch();
  prefetch("/api/user", { as: "fetch" });
  // ...
}
```

---

### `usePoll`

Polls an async function at a fixed interval until it returns `{ done: true, data? }`. Stops on error. Options: `intervalMs`, `immediate`, `enabled`. Returns `data`, `done`, `error`, `pollCount`, `start`, `stop`.

```tsx
import { usePoll } from "preact-missing-hooks";

function StatusPoller() {
  const { data, done, error, pollCount, stop } = usePoll(
    async () => {
      const res = await fetch("/api/job/status");
      const json = await res.json();
      return json.ready ? { done: true, data: json } : { done: false };
    },
    { intervalMs: 1000, immediate: true }
  );

  if (error) return <div>Error: {error.message}</div>;
  if (done) return <div>Result: {JSON.stringify(data)}</div>;
  return (
    <div>
      Polling… ({pollCount} calls) <button onClick={stop}>Stop</button>
    </div>
  );
}
```

---

### `useDeviceData`

Reads device and browser information from native APIs (`navigator`, `screen`, `window`, `matchMedia`). Detects **browser** and **OS** name/version via [Client Hints](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgentData) when supported, with user-agent parsing as fallback. High-entropy hints (`platformVersion`, `fullVersionList`) refine versions when the browser allows it.

No permissions required for the base snapshot; battery uses the [Battery Status API](https://developer.mozilla.org/en-US/docs/Web/API/Battery_Status_API) when available.

**Options**

| Option | Default | Description |
| --- | --- | --- |
| `includeBattery` | `true` | Poll `navigator.getBattery()` when available |
| `batteryPollIntervalMs` | `60000` | Battery refresh interval (ms) |
| `includeHighEntropy` | `true` | Request `platformVersion` / `fullVersionList` from Client Hints |

**Key return fields**

| Field | Description |
| --- | --- |
| `browser.name`, `browser.version` | Detected browser (e.g. Chrome, 120.0.0.0) |
| `os.name`, `os.version` | Detected OS (e.g. Windows, 10.0) |
| `language`, `languages` | Locale from `navigator` |
| `screen`, `viewport` | Display and window size |
| `hardwareConcurrency`, `deviceMemory` | CPU count, RAM hint (GB) |
| `colorScheme`, `reducedMotion` | `prefers-color-scheme` / `prefers-reduced-motion` |
| `userAgentData` | Raw Client Hints snapshot when available |
| `battery` | Charging state and level (0–1) when enabled |

Updates on resize, orientation, online/offline, and media preference changes.

```tsx
import { useDeviceData, getDeviceData, parseUserAgent } from "preact-missing-hooks";

function DeviceInfo() {
  const device = useDeviceData({
    includeBattery: true,
    includeHighEntropy: true,
  });

  return (
    <section>
      <h2>Environment</h2>
      <p>
        {device.browser.name} {device.browser.version} on {device.os.name}{" "}
        {device.os.version}
      </p>
      <ul>
        <li>Language: {device.language}</li>
        <li>
          Viewport: {device.viewport.width}×{device.viewport.height}
        </li>
        <li>CPUs: {device.hardwareConcurrency ?? "—"}</li>
        <li>Theme: {device.colorScheme}</li>
        {device.battery && (
          <li>
            Battery: {Math.round(device.battery.level * 100)}%
            {device.battery.charging ? " (charging)" : ""}
          </li>
        )}
      </ul>
    </section>
  );
}

// One-off snapshot (e.g. analytics on page load)
const snapshot = getDeviceData();
sendAnalytics({
  browser: snapshot.browser.name,
  browserVersion: snapshot.browser.version,
  os: snapshot.os.name,
  osVersion: snapshot.os.version,
});

// Parse a custom UA string without the hook
const { browser, os } = parseUserAgent(customUserAgent);
```

---

### `useRageClick`

Detects rage clicks (multiple rapid clicks in the same area), e.g. when the UI is unresponsive. Report them to [Sentry](https://docs.sentry.io/product/issues/issue-details/replay-issues/rage-clicks/) or your error tracker to surface rage-click issues and lower rage-click-related support.

```tsx
import { useRef } from "preact/hooks";
import { useRageClick } from "preact-missing-hooks";

function SubmitButton() {
  const ref = useRef<HTMLButtonElement>(null);

  useRageClick(ref, {
    onRageClick: ({ count, event }) => {
      // Report to Sentry (or your error tracker) to create rage-click issues
      Sentry.captureMessage("Rage click detected", {
        level: "warning",
        extra: { count, target: event.target, tag: "rage_click" },
      });
    },
    threshold: 5, // min clicks (default 5, Sentry-style)
    timeWindow: 1000, // ms (default 1000)
    distanceThreshold: 30, // px (default 30)
  });

  return <button ref={ref}>Submit</button>;
}
```

---

### `useThreadedWorker`

Runs async work in a queue with **sequential** (one task at a time, by priority) or **parallel** (worker pool) execution. Lower priority number = higher priority; same priority is FIFO.

```tsx
import { useThreadedWorker } from "preact-missing-hooks";

// Sequential: one task at a time, sorted by priority
const sequential = useThreadedWorker(fetchUser, { mode: "sequential" });

// Parallel: up to N tasks at once
const parallel = useThreadedWorker(processItem, {
  mode: "parallel",
  concurrency: 4,
});

// API (same for both modes)
const {
  run, // (data, { priority?: number }) => Promise<TResult>
  loading, // true while any task is queued or running
  result, // last successful result
  error, // last error
  queueSize, // tasks queued + running
  clearQueue, // clear pending tasks (running continue)
  terminate, // clear queue and reject new run()
} = sequential;

// Run with priority (1 = highest)
await run({ userId: 1 }, { priority: 1 });
await run({ userId: 2 }, { priority: 3 });
```

---

### `useIndexedDB`

Production-ready IndexedDB hook: database initialization, table creation (with keyPath, autoIncrement, indexes), singleton connection, and a full table API. All operations are Promise-based and support optional `onSuccess`/`onError` callbacks.

**Config:** `name`, `version`, and `tables` (each table: `keyPath`, `autoIncrement?`, `indexes?`).

**Table API:** `insert`, `update`, `delete`, `exists`, `query(filterFn)`, `upsert`, `bulkInsert`, `clear`, `count`.

**Database API:** `db.table(name)`, `db.hasTable(name)`, `db.transaction(storeNames, mode, callback, options?)`.

```tsx
import { useIndexedDB } from "preact-missing-hooks";

function App() {
  const { db, isReady, error } = useIndexedDB({
    name: "my-app-db",
    version: 1,
    tables: {
      users: { keyPath: "id", autoIncrement: true, indexes: ["email"] },
      settings: { keyPath: "key" },
    },
  });

  if (error) return <div>Failed to open database</div>;
  if (!isReady || !db) return <div>Loading...</div>;

  const users = db.table("users");

  // All operations return Promises and accept optional { onSuccess, onError }
  await users.insert({ email: "a@b.com", name: "Alice" });
  await users.update(1, { name: "Alice Smith" });
  const found = await users.query((u) => u.email.startsWith("a@"));
  const n = await users.count();
  await users.delete(1);
  await users.upsert({ id: 2, email: "b@b.com" });
  await users.bulkInsert([{ email: "c@b.com" }, { email: "d@b.com" }]);
  await users.clear();

  // Full transaction support
  await db.transaction(["users", "settings"], "readwrite", async (tx) => {
    await tx.table("users").insert({ email: "e@b.com" });
    await tx.table("settings").upsert({ key: "theme", value: "dark" });
  });

  return <div>DB ready. Tables: {db.hasTable("users") ? "users" : ""}</div>;
}
```

---

### `useWebRTCIP`

Detects client IP addresses using WebRTC ICE candidates and a STUN server (**frontend-only**, no backend). **Not highly reliable** — use as a **first-priority** hint; if it fails or returns empty, fall back to a public IP API (e.g. [ipapi.co](https://ipapi.co), [ipify](https://www.ipify.org), [ip-api.com](https://ip-api.com)).

Returns `{ ips: string[], loading: boolean, error: string | null }`. Options: `stunServers`, `timeout` (ms), `onDetect(ip)`.

```tsx
import { useWebRTCIP } from "preact-missing-hooks";
import { useState, useEffect } from "preact/hooks";

function ClientIP() {
  const { ips, loading, error } = useWebRTCIP({
    timeout: 4000,
    onDetect: (ip) => {
      /* optional: e.g. analytics */
    },
  });
  const [fallbackIP, setFallbackIP] = useState<string | null>(null);

  // Fallback to public IP API when WebRTC fails or returns empty
  useEffect(() => {
    if (loading || ips.length > 0) return;
    if (error) {
      fetch("https://api.ipify.org?format=json")
        .then((r) => r.json())
        .then((d) => setFallbackIP(d.ip))
        .catch(() => {});
    }
  }, [loading, ips.length, error]);

  if (loading) return <p>Detecting IP…</p>;
  if (ips.length > 0) return <p>IPs (WebRTC): {ips.join(", ")}</p>;
  if (fallbackIP) return <p>IP (fallback API): {fallbackIP}</p>;
  if (error) return <p>WebRTC failed. Try fallback API.</p>;
  return null;
}
```

---

### `useWasmCompute`

Runs WebAssembly computation in a Web Worker so the main thread stays responsive. Flow: **Preact Component → useWasmCompute() → Web Worker → WASM Module → return result.** The hook checks that the environment supports `window`, `Worker`, and `WebAssembly`; in SSR or unsupported environments it sets `error` and leaves `ready` false.

Returns `{ compute, result, loading, error, ready }`. Options: `wasmUrl` (required), `exportName` (default `'compute'`), optional `workerUrl` (custom worker script), optional `importObject` (must be serializable for the default worker).

```tsx
import { useWasmCompute } from "preact-missing-hooks";

function AddWithWasm() {
  const { compute, result, loading, error, ready } = useWasmCompute<
    number,
    number
  >({
    wasmUrl: "/add.wasm",
    exportName: "add",
  });

  const handleClick = () => {
    if (ready) compute(2).then(() => {});
  };

  if (error) return <p>WASM unavailable: {error}</p>;
  if (!ready) return <p>Loading WASM…</p>;
  return (
    <div>
      <button onClick={handleClick} disabled={loading}>
        Add 2
      </button>
      {result != null && <p>Result: {result}</p>}
    </div>
  );
}
```

---

### `useWorkerNotifications`

Listens to a Worker's `message` events and maintains state and derived stats. Your worker should `postMessage` with: `{ type: 'task_start', taskId? }`, `{ type: 'task_end', taskId?, duration? }`, `{ type: 'task_fail', taskId?, error? }`, and optionally `{ type: 'queue_size', size }`.

Returns `runningTasks`, `completedCount`, `failedCount`, `eventHistory`, `averageDurationMs`, `throughputPerSecond`, `currentQueueSize`, and **`progress`** — a single object with all active worker data (running, completed, failed, totalProcessed, avg duration, throughput/s, queue). Options: `maxHistory` (default 100), `throughputWindowMs` (default 1000).

```tsx
import { useWorkerNotifications } from "preact-missing-hooks";

function WorkerDashboard({ worker }) {
  const { progress, eventHistory } = useWorkerNotifications(worker, {
    maxHistory: 50,
  });

  return (
    <div>
      <p>
        Running: {progress.runningTasks.length} | Done:{" "}
        {progress.completedCount} | Failed: {progress.failedCount}
      </p>
      <p>
        Avg: {progress.averageDurationMs.toFixed(0)}ms | Throughput:{" "}
        {progress.throughputPerSecond.toFixed(2)}/s | Queue:{" "}
        {progress.currentQueueSize}
      </p>
      <small>Events: {eventHistory.length}</small>
    </div>
  );
}
```

---

### `useLLMMetadata`

Injects an AI-readable metadata block into the document head when the route changes. Works in **React 18+** and **Preact 10+** (framework-agnostic). No router dependency — you pass the current `route` string and the hook updates the script when it changes.

**Safe usage:** The hook **never throws**. It accepts `config` or `null`/`undefined`. When `config` is `null` or `undefined`, it injects a minimal payload with `route: "/"` and `generatedAt`. Invalid or missing values are normalized; all strings are length-limited and URLs validated; DOM access is wrapped in try/catch. Safe for SSR (no-op when `window` is undefined).

**API:**

```ts
type OGType =
  | "website"
  | "article"
  | "profile"
  | "video.other"
  | "product"
  | "music.song"
  | "book";

interface LLMConfig {
  route: string;
  mode?: "manual" | "auto-extract";
  title?: string;
  description?: string;
  tags?: string[];
  canonicalUrl?: string; // absolute URL
  language?: string; // e.g. "en", "en-US"
  ogType?: OGType; // Open Graph type
  ogImage?: string; // absolute image URL
  ogImageAlt?: string;
  siteName?: string;
  author?: string;
  publishedTime?: string; // ISO date
  modifiedTime?: string; // ISO date
  robots?: string; // e.g. "index, follow"
  extra?: Record<string, string | number | boolean | string[]>;
}

function useLLMMetadata(config: LLMConfig | null | undefined): void;
```

**Behavior:**

- When `config` is `null` or `undefined`: injects a minimal payload with `route: "/"` and `generatedAt` (no throw).
- When `config.route` (or other deps) change: removes any existing `<script data-llm="true">`, then injects a new one.
- Script tag: `<script type="application/llm+json" data-llm="true">` with JSON payload. Only defined, safe fields are included.
- **Cacheable:** If the generated payload is unchanged, the script is not replaced.
- **SSR-safe:** No-op when `typeof window === "undefined"`.
- Cleans up on unmount (removes the script).

**Modes:**

- **`manual`** (default): Uses `title`, `description`, `tags`, and any other config fields you pass.
- **`auto-extract`**: Fills `title`, `description`, and `outline` from the DOM (`document.title`, visible `<h1>`/`<h2>`, first 3 visible `<p>`). You can still override with config. Ignores content inside `nav`, `footer`, `script`, `style`.

**Example payload (rich):**

```json
{
  "route": "/blog/ai-hooks",
  "title": "AI Hooks in Preact",
  "description": "A short summary...",
  "tags": ["preact", "react", "hooks"],
  "outline": ["Intro", "Problem", "Solution"],
  "canonicalUrl": "https://example.com/blog/ai-hooks",
  "language": "en",
  "ogType": "article",
  "ogImage": "https://example.com/og.png",
  "siteName": "My Blog",
  "author": "Jane Doe",
  "publishedTime": "2025-02-14T10:00:00.000Z",
  "modifiedTime": "2025-02-14T12:00:00.000Z",
  "robots": "index, follow",
  "generatedAt": "2025-02-14T12:00:00.000Z"
}
```

**Example: React Router**

```tsx
import { useLocation } from "react-router-dom";
import { useLLMMetadata } from "preact-missing-hooks"; // or "preact-missing-hooks/react"

function App() {
  const { pathname } = useLocation();
  useLLMMetadata({
    route: pathname,
    mode: "auto-extract",
    title: document.title,
    tags: ["my-app"],
  });
  return <Outlet />;
}
```

**Example: Preact Router**

```tsx
import { useLocation } from "preact-router";
import { useLLMMetadata } from "preact-missing-hooks";

function App() {
  const [pathname] = useLocation();
  useLLMMetadata({
    route: pathname ?? "/",
    mode: "manual",
    title: "My Page",
    description: "Page description",
    tags: ["preact", "hooks"],
  });
  return <div>{/* your routes / children */}</div>;
}
```

---

### `useRefPrint`

Binds a ref to a DOM section and provides `print()` to open the native print dialog. Uses `@media print` CSS so only that section is visible when printing (user can then print or choose “Save as PDF”). Options: `documentTitle` (title for the print document), `downloadAsPdf` (hint that the same flow supports saving as PDF).

```tsx
import { useRef } from "preact/hooks";
import { useRefPrint } from "preact-missing-hooks";

function Report() {
  const printRef = useRef<HTMLDivElement>(null);
  const { print } = useRefPrint(printRef, {
    documentTitle: "Monthly Report",
    downloadAsPdf: true,
  });

  return (
    <div>
      <div ref={printRef}>
        <h1>Report content</h1>
        <p>Only this section is printed when you click Print.</p>
      </div>
      <button onClick={print}>Print / Save as PDF</button>
    </div>
  );
}
```

---

### `useRBAC`

Frontend-only role-based access control. Define roles with a condition (e.g. `user.role === 'admin'`), assign capabilities per role (use `'*'` for full access), and plug in where the current user comes from: `localStorage`, `sessionStorage`, API, memory, or a custom getter. Returns `user`, `roles`, `capabilities`, `hasRole(role)`, `can(capability)`, `refetch`, and helpers like `setUserInStorage` for persisting auth in storage.

**User source types:** `localStorage`, `sessionStorage` (key to read user JSON), `api` (`fetch` returning user), `memory` (`getUser()`), `custom` (`getAuth()` returning `{ user?, roles?, capabilities? }`). Optional `capabilitiesOverride` can read capabilities from storage or API instead of deriving from roles.

```tsx
import { useRBAC } from "preact-missing-hooks";

const roleDefinitions = [
  { role: "admin", condition: (u) => u?.role === "admin" },
  {
    role: "editor",
    condition: (u) => u?.role === "editor" || u?.role === "admin",
  },
  { role: "viewer", condition: (u) => !!u?.id },
];
const roleCapabilities = {
  admin: ["*"],
  editor: ["posts:edit", "posts:create", "posts:read"],
  viewer: ["posts:read"],
};

function App() {
  const { user, roles, capabilities, hasRole, can, setUserInStorage } = useRBAC(
    {
      userSource: { type: "localStorage", key: "user" },
      roleDefinitions,
      roleCapabilities,
    }
  );

  const login = (role) => {
    setUserInStorage(
      { id: 1, role, email: role + "@app.com" },
      "localStorage",
      "user"
    );
  };
  const logout = () => setUserInStorage(null, "localStorage", "user");

  return (
    <div>
      {!user ? (
        <div>
          <button onClick={() => login("admin")}>Login as Admin</button>
          <button onClick={() => login("editor")}>Login as Editor</button>
          <button onClick={() => login("viewer")}>Login as Viewer</button>
        </div>
      ) : (
        <div>
          <p>Roles: {roles.join(", ")}</p>
          {can("posts:edit") && <button>Edit post</button>}
          {can("*") && <button>Admin panel</button>}
          <button onClick={logout}>Logout</button>
        </div>
      )}
    </div>
  );
}
```

---

## Built With

- [Preact](https://preactjs.com)
- [Microbundle](https://github.com/developit/microbundle)
- [TypeScript](https://www.typescriptlang.org)
- [Vitest](https://vitest.dev) for testing

---

## License

MIT © [Prakhar Dubey](https://github.com/prakhardubey2002)

---

## Contributing

Contributions are welcome! Please open issues or submit PRs with new hooks or improvements.
