{"owner":"plotly","repo":"dash","hasSkills":true,"totalSkillsCount":5,"totalTokensCount":13062,"categories":["root-instruction","claude-rule","plugin-manifest","project-spec","copilot-instructions"],"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md","CLAUDE.md","components/dash-core-components/packages/README.md",".ai/ARCHITECTURE.md",".github/copilot-instructions.md"],"skills":{"AGENTS.md":"# AGENTS.md\n\nFor guidance on working with this repository, see the [`.ai/`](.ai/) directory:\n\n- [COMMANDS.md](.ai/COMMANDS.md) - Build, test, lint\n- [ARCHITECTURE.md](.ai/ARCHITECTURE.md) - Backend, callbacks, layout\n- [RENDERER.md](.ai/RENDERER.md) - Frontend, React, Redux\n- [COMPONENTS.md](.ai/COMPONENTS.md) - Component system\n- [TESTING.md](.ai/TESTING.md) - Testing patterns\n- [TROUBLESHOOTING.md](.ai/TROUBLESHOOTING.md) - Common issues\n","CLAUDE.md":"# CLAUDE.md\n\nGuidance for Claude Code (claude.ai/code) working in this repo.\n\n## Project Overview\n\nDash is a Python framework for building reactive web-based data visualization applications. Built on Plotly.js, React, and Flask.\n\n## Docs\n\nDeeper reference lives in `.ai/`. Open the one that covers what you're touching, don't read them all up front:\n\n- `COMMANDS.md` - build, test, lint commands\n- `ARCHITECTURE.md` - backend, callbacks, pages, background callbacks, stores, async, security\n- `RENDERER.md` - frontend, crawlLayout, Redux store, clientside API\n- `COMPONENTS.md` - component system, generation, resources\n- `TESTING.md` - test framework, fixtures, patterns\n- `TROUBLESHOOTING.md` - common errors and fixes\n- `PERFORMANCE.md` - benchmark harness, profiling, and performance findings\n\nTo find code, grep. Don't rely on a hand-kept file map, it goes stale.\n\n## Writing Style\n\nApplies to code comments, commit messages, PR descriptions, review comments, and CHANGELOG entries.\n\n- **Comments are a last resort.** The code is Python and should read on its own. Write a comment only when the code cannot explain itself: a non-obvious why, a workaround, a gotcha. Never narrate what the next line already says. If in doubt, leave it out.\n- **No em dashes or en dashes (`—` `–`).** Use a colon, comma, parentheses, or two sentences.\n- **Straight ASCII only.** Straight quotes and apostrophes (`'` `\"`), no curly ones. No ellipsis character (`…`); write three periods if you really need them. No decorative unicode.\n- **Plain words.** Say the thing directly, in words most people know. No AI filler, no thesaurus vocabulary, no heavy styling. Short and to the point.\n\n## Quality Gate\n\nBefore you say a change works:\n\n1. **Prove it in a running app.** Write a small repro app in the scratchpad and drive it headless with `dash_duo` (a throwaway pytest test is the practical way, see `.ai/TESTING.md`). Watch the real behavior. \"The code looks right\" is not done.\n2. **Leave a test behind.** Any behavior change or fix needs a test that stays in the tree: an integration/acceptance test under `tests/integration/`, or a renderer test for frontend-only work. It should fail before your change and pass after. Never run the whole integration suite at once, always a specific file or `-k`.\n3. **Second-model review.** Before handing back, launch a review subagent on a different model (the Agent tool takes a `model` override, e.g. Sonnet when you're on Opus) to check the diff for correctness and cases you missed. Fold in what holds up. A fresh model catches what the author misses.\n","components/dash-core-components/packages/README.md":"Folder to place temporary modules e.g. plotly.js-dist-min in .tgz format\n",".ai/ARCHITECTURE.md":"# Architecture\n\n## Python Backend Framework\n\n- **`dash/dash.py`** - Main `Dash` application class (~2000 lines). Orchestrates the server backend, layout management, callback registration, routing, and asset serving. Key methods: `layout` property, `callback()`, `clientside_callback()`, `run()`.\n\n- **`dash/backends/`** - Server backend implementations. See [Server Backends](#server-backends) section for details.\n\n- **`dash/_callback.py`** - Callback registration and execution. Contains `callback()` decorator (usable as `@dash.callback` without app instance), `clientside_callback()`, and `register_callback()` which inserts callbacks into the callback map.\n\n- **`dash/dependencies.py`** - Dependency classes for callbacks:\n  - `Input` - Triggers callback when value changes\n  - `Output` - Component property to update (supports `allow_duplicate=True`)\n  - `State` - Read value without triggering callback\n  - `ClientsideFunction` - Reference to JS function for clientside callbacks\n  - Wildcards: `MATCH`, `ALL`, `ALLSMALLER` for pattern-matching IDs\n\n- **`dash/development/base_component.py`** - `Component` base class with `ComponentMeta` metaclass. All Dash components inherit from this. Components auto-register in `ComponentRegistry` and serialize to JSON via `to_plotly_json()`.\n\n- **`dash/_pages.py`** - Multi-page app support. `PAGE_REGISTRY` holds registered pages, `register_page()` decorator registers page modules with routes.\n\n## Layout System\n\nThe layout defines the UI as a tree of components:\n\n```python\napp.layout = html.Div([\n    dcc.Input(id='input', value='initial'),\n    html.Div(id='output')\n])\n```\n\n- **Static layout**: Assigned directly as a component tree\n- **Dynamic layout**: Assigned as a function that returns components (called on each page load, useful for per-session state)\n- Layout is serialized to JSON and sent to the React frontend via `/_dash-layout`\n- Components can contain other components via `children` prop\n- Component IDs can be strings or dicts (for pattern-matching callbacks)\n\n## Callback Types\n\n### 1. Regular Callbacks\n\n`@app.callback` or `@dash.callback`:\n\n```python\n@app.callback(Output('output', 'children'), Input('input', 'value'))\ndef update(value):\n    return f'You entered: {value}'\n```\n\nServer-side Python function called when inputs change. Outputs update component properties.\n\n### 2. Clientside Callbacks\n\n`app.clientside_callback`:\n\n```python\napp.clientside_callback(\n    \"\"\"function(value) { return 'You entered: ' + value; }\"\"\",\n    Output('output', 'children'),\n    Input('input', 'value')\n)\n```\n\nJavaScript function runs in browser. Faster for simple transformations, no server round-trip. Can reference `window.dash_clientside.namespace.function_name` or inline JS string.\n\n### 3. Background Callbacks\n\n`background=True`:\n\n```python\n@app.callback(Output('output', 'children'), Input('btn', 'n_clicks'),\n              background=True, manager=diskcache_manager,\n              running=[(Output('btn', 'disabled'), True, False)],\n              progress=[Output('progress', 'value')])\ndef compute(set_progress, n_clicks):\n    for i in range(10):\n        set_progress(i * 10)\n        time.sleep(1)\n    return 'Done'\n```\n\nCallbacks executed in separate process via Celery or Diskcache manager. Supports `progress` updates, `running` state changes, and `cancel` inputs. See [Background Callbacks](#background-callbacks) section for details.\n\n### 4. Pattern-Matching Callbacks\n\n```python\n@app.callback(\n    Output({'type': 'output', 'index': MATCH}, 'children'),\n    Input({'type': 'input', 'index': MATCH}, 'value')\n)\ndef update(value):\n    return value\n```\n\nUse dict IDs with wildcards (`MATCH`, `ALL`, `ALLSMALLER`) to target dynamically-generated components.\n\n## Server Routes\n\n- `/_dash-layout` - Returns initial component tree as JSON\n- `/_dash-dependencies` - Returns callback definitions\n- `/_dash-update-component` - Executes callbacks, returns updated props\n- `/_dash-component-suites/<package>/<path>` - Serves component JS/CSS assets\n- `/assets/<path>` - Serves static assets from app's assets folder\n\n## Server Backends\n\nDash supports multiple web server backends. The backend abstraction is in `dash/backends/`.\n\n### Available Backends\n\n| Backend | Type | Install | Use Case |\n|---------|------|---------|----------|\n| **Flask** (default) | WSGI (sync) | `pip install dash` | Standard deployments, simplicity |\n| **Quart** | ASGI (async) | `pip install dash[quart]` | Async callbacks, WebSocket support |\n| **FastAPI** | ASGI (async) | `pip install dash[fastapi]` | OpenAPI docs, async, modern Python |\n\n### Usage\n\n**Default (Flask):**\n```python\nfrom dash import Dash\napp = Dash(__name__)\n```\n\n**With existing server instance:**\n```python\nfrom flask import Flask\nfrom dash import Dash\n\nserver = Flask(__name__)\napp = Dash(__name__, server=server)\n```\n\n**Quart backend:**\n```python\nfrom quart import Quart\nfrom dash import Dash\n\nserver = Quart(__name__)\napp = Dash(__name__, server=server)\n```\n\n**FastAPI backend:**\n```python\nfrom fastapi import FastAPI\nfrom dash import Dash\n\nserver = FastAPI()\napp = Dash(__name__, server=server)\n\n# Run with: uvicorn module:app.server --reload\n```\n\n### Architecture\n\nThe backend system uses an abstract interface:\n\n- **`BaseDashServer`** (`dash/backends/base_server.py`) - Abstract base class defining the server interface. All backends implement this.\n\n- **`RequestAdapter`** - Normalizes HTTP request objects across frameworks. Provides unified access to `args`, `cookies`, `headers`, `get_json()`, etc.\n\n- **`ResponseAdapter`** - Normalizes response creation. Handles `set_cookie()`, `set_header()`, `set_response()`.\n\n- **`get_backend(name)`** - Factory function to get backend class by name (`\"flask\"`, `\"quart\"`, `\"fastapi\"`).\n\n- **`get_server_type(server)`** - Auto-detects backend from a server instance.\n\n### Backend Implementations\n\n**Flask** (`dash/backends/_flask.py`):\n- `FlaskDashServer` - Wraps Flask app\n- `FlaskRequestAdapter` - Uses `flask.request` proxy\n- `FlaskResponseAdapter` - Uses `flask.Response`\n- Compression via `flask-compress`\n\n**Quart** (`dash/backends/_quart.py`):\n- `QuartDashServer` - Wraps Quart app (async Flask API)\n- `QuartRequestAdapter` - Uses `quart.request` proxy\n- `QuartResponseAdapter` - Uses `quart.Response`\n- All route handlers are `async def`\n- Compression via `quart-compress`\n\n**FastAPI** (`dash/backends/_fastapi.py`):\n- `FastAPIDashServer` - Wraps FastAPI app\n- `FastAPIRequestAdapter` - Uses context variable for current request\n- `FastAPIResponseAdapter` - Uses Starlette responses\n- `DashMiddleware` - Consolidated ASGI middleware for request handling\n  - Runs `before_request` hooks on every HTTP route (user routes, assets, mounted apps) and the WebSocket handshake; a non-None hook result is sent as the response, like Flask. Only Dash's own routes get their JSON body pre-read, timing recorded and Dash's error handling. Quart registers the same hooks as `before_websocket` too.\n- Runs with uvicorn, supports hot reload\n- Built-in GZip compression\n\n### Key Interface Methods\n\nAll backends implement:\n\n```python\nclass BaseDashServer(ABC):\n    def create_app(name, config) -> server        # Create new server\n    def add_url_rule(rule, view_func, ...)        # Register routes\n    def before_request(func)                       # Request hooks\n    def after_request(func)                        # Response hooks\n    def run(dash_app, host, port, debug)          # Start dev server\n    def make_response(data, mimetype, status)     # Create response\n    def jsonify(obj)                              # JSON response\n    def setup_index(dash_app)                     # Register / route\n    def serve_callback(dash_app)                  # Callback endpoint\n    def setup_component_suites(dash_app)          # JS/CSS serving\n```\n\n### Accessing the Backend\n\n```python\napp = Dash(__name__)\n\n# Get the underlying server\napp.server          # Flask/Quart/FastAPI instance\n\n# Get the backend wrapper\napp.backend         # BaseDashServer subclass instance\napp.backend.server_type  # \"flask\", \"quart\", or \"fastapi\"\n\n# Access request in callbacks\nfrom dash import dash\ndash.get_app().backend.request_adapter()  # RequestAdapter instance\n```\n\n## Frontend (dash-renderer)\n\n**`dash/dash-renderer/src/`** contains the TypeScript/React frontend. See [RENDERER.md](RENDERER.md) for detailed documentation on:\n\n- Layout traversal (`crawlLayout`) and `children_props`\n- Component resolution from `window[namespace][type]`\n- Callback triggering via `setProps` and `notifyObservers`\n- Redux store structure (layout, paths, callbacks, graphs)\n- Observer system for callback processing\n- `window.dash_clientside` API\n- `window.dash_component_api` API\n\n### React Version\n\nDash supports multiple React versions. Configured in `dash/_dash_renderer.py`.\n\n**Available versions:** 18.3.1 (default), 18.2.0, 19.2.4 (experimental)\n\nReact 19 has no official UMD builds; Dash serves the `umd-react` package for it, plus a small shim (`dash-renderer/build/react-shim.min.js`, source `dash/dash-renderer/src/react-shim.js`) loaded right after react-dom and before any component package. The shim stubs the React <=18 secret internals (`ReactCurrentOwner`) some component libraries touch at load time, redirects the legacy `react.element` `$$typeof` symbol so libraries that pre-bundled a React <=18 jsx-runtime don't hit React error #525, and provides `window.ReactJSXRuntime`, the global that component bundles externalize `react/jsx-runtime` to.\n\n**Convention for component libraries:** externalize `react/jsx-runtime` and `react/jsx-dev-runtime` using the *defensive* external expression found in `components/dash-core-components/webpack.config.js` (`jsxRuntimeExternal`), not a bare `'ReactJSXRuntime'` string. The expression falls back to building the runtime from `window.React.createElement` when the global is missing, so the same bundle works on Dash versions that predate the shim. A bare `'ReactJSXRuntime'` external throws `ReactJSXRuntime is not defined` / `Cannot read properties of undefined (reading 'jsx')` at bundle load on older Dash.\n\nSet via environment variable (experimental):\n\n```bash\nREACT_VERSION=19.2.4 python app.py\n```\n\nOr programmatically before creating the app:\n\n```python\nfrom dash._dash_renderer import _set_react_version\n_set_react_version(\"19.2.4\")\n\nfrom dash import Dash\napp = Dash(__name__)\n```\n\nThis is useful for compatibility with older component libraries that require React 16.\n\n## Pages System\n\nMulti-page apps use `dash/_pages.py` with automatic routing via `dcc.Location`.\n\n### Page Registration\n\nEach page module calls `register_page()`:\n\n```python\n# pages/analytics.py\nfrom dash import register_page, html\n\nregister_page(__name__)  # infers path /analytics from module name\n\nlayout = html.Div(\"Analytics page\")\n```\n\n- **`PAGE_REGISTRY`** - `OrderedDict` storing all registered pages with metadata\n- **`register_page(module, path=None, ...)`** - Registers page with inferred or explicit path, title, description, image\n\n### Page Container\n\nWhen `use_pages=True`, Dash injects `page_container` as the layout (`dash/dash.py:148-158`):\n\n```python\npage_container = html.Div([\n    dcc.Location(id=\"_pages_location\", refresh=\"callback-nav\"),\n    html.Div(id=\"_pages_content\"),      # current page layout injected here\n    dcc.Store(id=\"_pages_store\"),       # stores page title/metadata\n])\n```\n\n### Routing Mechanism\n\n1. `dcc.Location` tracks browser URL changes\n2. Internal callback listens to `pathname` and `search` inputs\n3. `_path_to_page()` matches URL to registered page in `PAGE_REGISTRY`\n4. Page layout injected into `_pages_content` div\n\n### Path Templates (Dynamic Routes)\n\nPages can capture URL variables:\n\n```python\nregister_page(__name__, path_template=\"/asset/<asset_id>\")\n\ndef layout(asset_id=None):\n    return html.Div(f\"Asset: {asset_id}\")\n```\n\n`_parse_path_variables()` extracts variables via regex and passes them as kwargs to the layout function.\n\n### Auto-Discovery\n\n`_import_layouts_from_pages()` walks the `pages/` folder:\n- Skips files starting with `_` or `.`\n- Only imports `.py` files containing `register_page`\n- Auto-assigns `layout` attribute from each module to the registry\n\n### Page Ordering\n\nPages sorted by: numeric `order` → string `order` → no order → module name. Home page (`/`) defaults to order `0`.\n\n## Assets and Static Files\n\n### Asset Directory\n\nThe `assets/` folder is automatically scanned at startup (`dash/dash.py:_walk_assets_directory`):\n\n- `.css` files → appended to stylesheets\n- `.js` files → appended to scripts\n- `favicon.ico` → used as app favicon\n- Files matching `assets_ignore` regex are skipped\n\n### Loading Order\n\nResources load in this order (`dash/dash.py:1127-1165`):\n\n1. React dependencies (from dash-renderer)\n2. Component library scripts (dash-html-components, dash-core-components, etc.)\n3. External scripts (`external_scripts` parameter)\n4. Dash renderer bundle\n5. Clientside callback scripts (inline)\n\nCSS follows similar ordering with external stylesheets first.\n\n### Fingerprinting and Caching\n\nComponent assets use fingerprinted URLs for cache busting (`dash/fingerprint.py`):\n\n```\n/_dash-component-suites/dash_core_components/dash_core_components.v2_14_0m1699900000.min.js\n```\n\n- Fingerprinted resources: 1-year cache header\n- Non-fingerprinted: ETag validation\n- Asset files: query string `?m={modification_time}`\n\n### Configuration Options\n\n```python\nDash(\n    assets_folder='assets',           # path to assets directory\n    assets_url_path='assets',         # URL path segment\n    assets_ignore='.*ignored.*',      # regex to skip files\n    assets_external_path=None,        # CDN base URL for assets\n    serve_locally=True,               # True=local files, False=CDN\n    external_scripts=[],              # additional JS URLs\n    external_stylesheets=[],          # additional CSS URLs\n)\n```\n\n### Asset URL Generation\n\n`app.get_asset_url(path)` returns the correct URL accounting for `requests_pathname_prefix` (important for Dash Enterprise deployments where apps have URL prefixes).\n\n## Error Handling\n\n### Debug Mode\n\nDebug mode enables developer tools (`dash/dash.py:_setup_dev_tools`):\n\n```python\napp.run(debug=True)\n# Or via environment: DASH_DEBUG=true\n```\n\n### Dev Tools Options\n\n```python\napp.enable_dev_tools(\n    dev_tools_ui=True,              # show error UI overlay\n    dev_tools_props_check=True,     # validate component prop types\n    dev_tools_serve_dev_bundles=True,  # use development JS (better errors)\n    dev_tools_hot_reload=True,      # auto-reload on file changes\n    dev_tools_prune_errors=True,    # strip internal frames from tracebacks\n)\n```\n\nEnvironment variables: `DASH_DEBUG`, `DASH_UI`, `DASH_PROPS_CHECK`, `DASH_HOT_RELOAD`, etc.\n\n### Callback Exceptions\n\n**`PreventUpdate`** - Skip updating outputs without error:\n\n```python\nfrom dash.exceptions import PreventUpdate\n\n@app.callback(Output('out', 'children'), Input('in', 'value'))\ndef update(value):\n    if not value:\n        raise PreventUpdate\n    return value\n```\n\n**`no_update`** - Skip specific outputs in multi-output callbacks:\n\n```python\nfrom dash import no_update\n\n@app.callback(Output('a', 'children'), Output('b', 'children'), Input('in', 'value'))\ndef update(value):\n    return value, no_update  # only updates 'a'\n```\n\n### Error Handlers\n\nCallbacks support `on_error` for custom error handling:\n\n```python\ndef handle_error(err):\n    logging.error(f\"Callback failed: {err}\")\n    return \"Error occurred\"  # returned to output\n\n@app.callback(Output('out', 'children'), Input('in', 'value'), on_error=handle_error)\ndef update(value):\n    return 1 / 0  # triggers error handler\n```\n\nApp-level error handler set via constructor.\n\n### Validation\n\n- **Layout validation**: When `suppress_callback_exceptions=False` (default), checks that callback IDs exist in layout\n- **Callback validation**: `dev_tools_validate_callbacks=True` checks for circular dependencies\n- **Props checking**: Validates component prop types against schema in dev mode\n\n### Hot Reload\n\nWhen enabled, a watch thread monitors:\n- `assets/` folder for CSS/JS changes\n- Component package directories\n\nFrontend polls `/_reload-hash` and triggers reload when hash changes. Configurable via `hot_reload_interval` (default 3s) and `hot_reload_watch_interval` (default 0.5s).\n\n## Background Callbacks\n\nBackground callbacks execute in separate processes, allowing the main server to remain responsive. Managed by `dash/background_callback/managers/`.\n\n### Definition\n\n```python\nfrom dash import callback, Input, Output\nfrom dash.background_callback import DiskcacheManager\n\ncache_manager = DiskcacheManager()\n\n@callback(\n    Output(\"result\", \"children\"),\n    Input(\"button\", \"n_clicks\"),\n    background=True,\n    manager=cache_manager,\n    interval=500,  # polling interval in ms\n)\ndef compute(n_clicks):\n    # Expensive computation\n    return result\n```\n\n### Callback Managers\n\n**`DiskcacheManager`** (`dash/background_callback/managers/diskcache_manager.py`):\n- Uses `diskcache.Cache` for persistent storage\n- Spawns `multiprocess.Process` for each job\n- Results stored on disk, survives server restarts\n- Good for single-server deployments\n\n**`CeleryManager`** (`dash/background_callback/managers/celery_manager.py`):\n- Requires Celery app with result backend (Redis/RabbitMQ)\n- Jobs distributed across Celery workers\n- Supports horizontal scaling\n- Good for production multi-worker deployments\n\n```python\nfrom celery import Celery\nfrom dash.background_callback import CeleryManager\n\ncelery_app = Celery(__name__, broker=\"redis://localhost:6379/0\")\ncache_manager = CeleryManager(celery_app)\n```\n\n### Progress Updates\n\nThe `progress` parameter defines outputs updated during execution:\n\n```python\n@callback(\n    Output(\"result\", \"children\"),\n    Input(\"button\", \"n_clicks\"),\n    progress=Output(\"progress-bar\", \"value\"),\n    progress_default=0,\n    background=True,\n    manager=cache_manager,\n)\ndef compute(set_progress, n_clicks):\n    for i in range(100):\n        set_progress(i)\n        time.sleep(0.1)\n    return \"Complete\"\n```\n\n- `set_progress` is injected as first argument when `progress` is specified\n- Can be single Output or list of Outputs\n- `progress_default` sets value when callback not running\n\n### Running State\n\nThe `running` parameter updates outputs while the job executes:\n\n```python\n@callback(\n    Output(\"result\", \"children\"),\n    Input(\"button\", \"n_clicks\"),\n    running=[\n        (Output(\"button\", \"disabled\"), True, False),\n        (Output(\"status\", \"children\"), \"Computing...\", \"Ready\"),\n    ],\n    background=True,\n    manager=cache_manager,\n)\ndef compute(n_clicks):\n    time.sleep(5)\n    return \"Done\"\n```\n\nEach tuple: `(Output, value_while_running, value_when_complete)`\n\n### Cancellation\n\nThe `cancel` parameter specifies inputs that abort the job:\n\n```python\n@callback(\n    Output(\"result\", \"children\"),\n    Input(\"start-btn\", \"n_clicks\"),\n    cancel=[Input(\"cancel-btn\", \"n_clicks\")],\n    background=True,\n    manager=cache_manager,\n)\ndef compute(n_clicks):\n    # Job terminates if cancel-btn clicked\n    return result\n```\n\nManagers call `terminate_job()` which kills the process (Diskcache) or revokes the task (Celery).\n\n### Result Caching\n\nResults can be cached to avoid recomputation:\n\n```python\ndef get_user_id():\n    return flask.session.get(\"user_id\")\n\ncache_manager = DiskcacheManager(\n    cache_by=[get_user_id],  # cache key includes user ID\n    expire=3600,             # TTL in seconds\n)\n```\n\n- `cache_by` - List of functions whose return values are included in cache key\n- `expire` - Time-to-live for cached results\n- `cache_args_to_ignore` - Argument indices to exclude from cache key\n\n### How It Works\n\n1. **Initial request**: Frontend triggers callback, backend returns `cacheKey` and `job` ID\n2. **Polling**: Frontend polls `/_dash-update-component?cacheKey=...&job=...` at configured interval\n3. **Progress**: Each poll returns current progress value if set\n4. **Completion**: When job finishes, poll returns final result\n5. **Cleanup**: Results cleared from cache (unless `cache_by` specified)\n\nCache key is SHA256 hash of: callback key + function source + arguments + triggered inputs + cache_by values. Without `cache_by` a random nonce is added, so every run gets its own key: the job runs with the requester's context, so two users with equal inputs must never share a result. Diskcache writes those uncached entries with a one day expiry (`UNCACHED_RESULT_EXPIRE`) so results nobody polls for do not pile up; Celery's result backend already expires them.\n\n### Key Files\n\n- `dash/_callback.py:188-219` - Background spec construction\n- `dash/background_callback/managers/__init__.py` - `BaseBackgroundCallbackManager` abstract class\n- `dash/background_callback/managers/diskcache_manager.py` - Diskcache implementation\n- `dash/background_callback/managers/celery_manager.py` - Celery implementation\n- `dash/dash-renderer/src/actions/callbacks.ts:458-685` - Frontend polling logic\n\n## Jupyter Integration\n\nDash apps can run directly in Jupyter notebooks and JupyterLab. The integration is handled by `dash/_jupyter.py`.\n\n### Display Modes\n\n```python\napp.run(\n    jupyter_mode=\"inline\",      # Display in notebook cell (default)\n    jupyter_width=\"100%\",       # IFrame width\n    jupyter_height=650,         # IFrame height in pixels\n)\n```\n\n| Mode | Behavior |\n|------|----------|\n| `\"inline\"` | App displays in notebook cell via IFrame |\n| `\"external\"` | Prints URL, user opens in browser tab |\n| `\"jupyterlab\"` | Opens in dedicated JupyterLab tab |\n| `\"tab\"` | Auto-opens URL in new browser tab |\n\n### How It Works\n\n1. `app.run()` detects Jupyter environment via `get_ipython()`\n2. Server starts in background daemon thread\n3. Jupyter comm protocol negotiates proxy configuration\n4. App displays according to selected mode\n\n```\napp.run() in notebook\n    ↓\nDetect Jupyter → Start server in background thread\n    ↓\nComm request → Extension responds with base_url\n    ↓\nCompute dashboard URL with proxy path\n    ↓\nDisplay: IFrame (inline) / URL (external) / Tab (jupyterlab)\n```\n\n### Notebook Extension\n\nClassic Jupyter notebooks use `dash/nbextension/`:\n\n- `main.js` - Registers \"dash\" comm target\n- `dash.json` - Extension loader configuration\n\nThe extension handles comm messages:\n- `base_url_request` → responds with server URL and base path\n- Enables proper proxy routing in JupyterHub environments\n\n### JupyterLab Extension\n\nJupyterLab uses `@plotly/dash-jupyterlab/`:\n\n- `src/index.ts` - TypeScript plugin implementing `JupyterFrontEndPlugin`\n- `DashIFrameWidget` - Lumino widget for rendering apps in tabs\n\nHandles messages:\n- `base_url_request` → responds with JupyterLab server config\n- `show` → creates dedicated tab with IFrame widget\n\nCompatible with JupyterLab 2.x, 3.x, and 4.x.\n\n### Proxy Configuration\n\nIn JupyterHub/proxy environments, the extension negotiates `requests_pathname_prefix`:\n\n```python\n# Computed from Jupyter base path\nrequests_pathname_prefix = \"/user/username/proxy/8050/\"\n```\n\nThis ensures callbacks route correctly through the Jupyter proxy.\n\n### Google Colab\n\nSpecial handling for Colab:\n- Uses `google.colab.output.serve_kernel_port_as_iframe()` for inline\n- Uses `google.colab.output.serve_kernel_port_as_window()` for external\n- Only supports \"inline\" and \"external\" modes\n\n### Key Files\n\n- `dash/_jupyter.py` - `JupyterDash` class, comm handling, server thread\n- `dash/nbextension/main.js` - Classic notebook extension\n- `@plotly/dash-jupyterlab/src/index.ts` - JupyterLab extension\n\n## Configuration Reference\n\n### Dash() Constructor Parameters\n\n**Basic Setup:**\n- `name` - Application name (default: infers from `__name__`)\n- `server` - Server instance (Flask, Quart, or FastAPI) or `True` to create Flask (default: `True`)\n- `title` - Browser tab title (default: `\"Dash\"`)\n- `update_title` - Title during callbacks (default: `\"Updating...\"`)\n\n**Assets & Resources:**\n- `assets_folder` - Path to assets directory (default: `\"assets\"`)\n- `assets_url_path` - URL path for assets (default: `\"assets\"`)\n- `assets_ignore` - Regex to exclude assets (default: `\"\"`)\n- `serve_locally` - Serve from local vs CDN (default: `True`)\n- `external_scripts` - Additional JS URLs\n- `external_stylesheets` - Additional CSS URLs\n\n**Routing:**\n- `url_base_pathname` - Base URL prefix for entire app\n- `requests_pathname_prefix` - Prefix for AJAX requests\n- `routes_pathname_prefix` - Prefix for API routes\n\n**Multi-Page:**\n- `use_pages` - Enable pages system (default: auto-detect)\n- `pages_folder` - Path to pages directory (default: `\"pages\"`)\n\n**Behavior:**\n- `suppress_callback_exceptions` - Skip callback validation (default: `False`)\n- `prevent_initial_callbacks` - Skip callbacks on load (default: `False`)\n- `background_callback_manager` - DiskcacheManager or CeleryManager\n- `on_error` - Global callback error handler\n- `shared_storage` - Cross-process state + pub/sub backend (default: `LocalSharedStorage`). Pass `None` to disable, or a `BaseSharedStorage` subclass/instance to swap. See [Shared Storage](#shared-storage).\n\n**WebSocket Callbacks:**\n- `websocket_callbacks` - Enable WebSocket for all callbacks (default: `False`). Requires FastAPI backend.\n- `websocket_allowed_origins` - List of allowed origins for WebSocket connections\n- `websocket_inactivity_timeout` - Disconnect WebSocket after inactivity period in ms (default: `300000` = 5 minutes). Set to `0` to disable.\n\n### app.run() Parameters\n\n- `host` - Server IP (default: `\"127.0.0.1\"`, env: `HOST`)\n- `port` - Server port (default: `8050`, env: `PORT`)\n- `debug` - Enable dev tools (default: `False`, env: `DASH_DEBUG`)\n- `jupyter_mode` - Display mode: `\"inline\"`, `\"external\"`, `\"tab\"`\n\n### Environment Variables\n\n| Variable | Purpose |\n|----------|---------|\n| `DASH_DEBUG` | Enable debug mode |\n| `DASH_URL_BASE_PATHNAME` | Base URL prefix |\n| `DASH_SUPPRESS_CALLBACK_EXCEPTIONS` | Skip validation |\n| `DASH_HOT_RELOAD` | Enable hot reload |\n| `DASH_PROPS_CHECK` | Validate prop types |\n| `DASH_PRUNE_ERRORS` | Simplify tracebacks |\n| `HOST` | Server host |\n| `PORT` | Server port |\n| `DASH_SECRET_KEY` | Signing secret for page, background and stream tokens when `server.secret_key` is unset |\n| `DASH_SHARED_STORAGE` | Shared-storage backend when `shared_storage=` is not passed (see Shared Storage) |\n\n## Stores and Client-Side State\n\n### dcc.Store\n\nStore data client-side with configurable persistence:\n\n```python\ndcc.Store(id='my-store', storage_type='local', data={'key': 'value'})\n```\n\n| Storage Type | Persists | Scope | Use Case |\n|--------------|----------|-------|----------|\n| `'memory'` | Page view only | Tab | Temporary state, debugging |\n| `'session'` | Browser session | Tab | Form state, filters |\n| `'local'` | Forever | All tabs | User preferences, settings |\n\n**Usage pattern:**\n```python\n@app.callback(Output('output', 'children'), Input('store', 'data'))\ndef use_store(data):\n    return data['key']\n\n@app.callback(Output('store', 'data'), Input('input', 'value'))\ndef update_store(value):\n    return {'key': value}\n```\n\n### Component Persistence\n\nAutomatically persist user edits to component props:\n\n```python\ndcc.Dropdown(\n    id='dropdown',\n    options=[...],\n    persistence=True,           # Enable persistence\n    persistence_type='local',   # local, session, or memory\n    persisted_props=['value'],  # Props to persist (default varies by component)\n)\n```\n\n- **`persistence`** - `True` or unique key to enable\n- **`persistence_type`** - Storage backend (default: `'local'`)\n- **`persisted_props`** - List of prop names to persist\n\nSupported components: Input, Dropdown, Checklist, RadioItems, Slider, RangeSlider, DatePickerSingle, DatePickerRange, Textarea, Tabs, DataTable.\n\n### When to Use Each\n\n| Need | Solution |\n|------|----------|\n| Server-controlled state | `dcc.Store` with callbacks |\n| Remember user selections | Component `persistence=True` |\n| Share state across tabs | `dcc.Store` with `storage_type='local'` |\n| Session-only state | `persistence_type='session'` |\n\n## Shared Storage\n\nBackend-agnostic **server-side** state shared across worker processes: a\ncross-process key/value store plus an ordered, replayable publish/subscribe\nchannel. Unlike `dcc.Store` (which lives in the browser), shared storage lives\non the server and lets callbacks running in different workers see the same keys\nand topics without standing up an external service like Redis.\n\nEnabled by default on every app. It starts lazily on first use, so it costs\nnothing until touched.\n\n### Accessing It\n\n```python\nimport dash\nfrom dash import Input, Output, callback\n\n@callback(Output(\"out\", \"children\"), Input(\"btn\", \"n_clicks\"))\ndef handler(n):\n    store = dash.ctx.shared_storage      # inside a callback\n    store.set(\"clicks\", n)\n    return store.get(\"clicks\", 0)\n\n# Or off the app directly:\napp = dash.Dash()\napp.shared_storage.set(\"key\", {\"any\": \"json-compatible value\"})\n```\n\nValues (and published messages) must be **JSON-compatible** — dict / list / str\n/ int / float / bool / None — the same constraint as `dcc.Store` and callback\noutputs. Messages are encoded with `msgspec` (msgpack), a hard dependency.\n\n### API\n\n`app.shared_storage` (and `dash.ctx.shared_storage`) is a `BaseSharedStorage`:\n\n| Method | Purpose |\n|--------|---------|\n| `get(key, default=None)` | Read a value |\n| `set(key, value, ttl=None)` | Write a value; `ttl` = optional lifetime in seconds |\n| `delete(key)` | Remove a key |\n| `publish(topic, message, ttl=None)` | Append a message to a topic; `ttl` = release the topic after that long idle |\n| `subscribe(topic, replay_from=None)` | Return a `Subscription` |\n\n**Key expiry (TTL).** `set(key, value, ttl=<seconds>)` gives a key a bounded\nlifetime; once it elapses, reads return the `default` again. `ttl=None` (the\ndefault) never expires. Expiry is lazy/best-effort — an expired key is dropped\non its next read, not at a guaranteed instant. Each backend uses its native\nmechanism: a monotonic deadline on the in-memory owner, diskcache `expire`,\nRedis `PX`. Use it for session-scoped or cache-like state you don't want to\naccumulate unboundedly.\n\n**Ordered pub/sub.** Each `publish` to a topic gets a monotonically increasing\nsequence number (per topic, starting at 1; `0` means \"before the first\nmessage\"). A subscriber receives every message published after it subscribed,\nin order:\n\n```python\n# Producer (one worker):\nstore.publish(\"progress\", {\"pct\": 50})\n\n# Consumer (another worker/callback):\nwith store.subscribe(\"progress\") as sub:\n    for message in sub:          # or: async for message in sub\n        print(message[\"pct\"])\n```\n\n**Replay on reconnect.** Pass `replay_from=<last-seen-seq>` to resume after a\ndrop; buffered messages since that cursor replay first, so a reconnecting\nconsumer doesn't miss messages. If the consumer fell farther behind than the\nbounded buffer holds, the subscription raises `SharedStorageGap` (an explicit\ngap rather than a silent hole). A `Subscription` is iterable synchronously\n(`for`) or asynchronously (`async for`), is a context manager, and has\n`close()`.\n\n### Backends\n\nThree backends ship, differing only in **how far state reaches**. Pick by the\ndeployment topology — critically, whether the app runs behind a load balancer as\nmore than one container/pod (see [Deployment Topology](#deployment-topology-and-shared-storage) below).\n\n| Backend | Backing store | Reaches | Extra |\n|---------|---------------|---------|-------|\n| `LocalSharedStorage` *(default)* | in-memory, owner-elected socket | one **container** (all its workers) | none |\n| `DiskcacheSharedStorage` | a `diskcache.Cache` | one **host** (all processes sharing the dir) | `dash[diskcache]` |\n| `RedisSharedStorage` | Redis (Streams for pub/sub) | **any** process/container/pod | `dash[redis]` |\n\nAll three implement the same `BaseSharedStorage` contract (KV + ordered,\nreplayable pub/sub with `SharedStorageGap` on buffer overrun), so app code is\nidentical across them — only the constructor differs.\n\n```python\nfrom dash import Dash\nfrom dash._shared_storage import (\n    LocalSharedStorage, DiskcacheSharedStorage, RedisSharedStorage,\n)\n\nDash(shared_storage=LocalSharedStorage)                       # default\nDash(shared_storage=DiskcacheSharedStorage(directory=\"/tmp/ss\"))\nDash(shared_storage=RedisSharedStorage(url=\"redis://host:6379\"))\nDash(shared_storage=None)                                     # disabled\n```\n\nWhen disabled, accessing `app.shared_storage` / `dash.ctx.shared_storage`\nraises `SharedStorageError`.\n\n**`LocalSharedStorage`** — in-memory, no external service. On first use every\nworker races to become the single **owner** for the machine by binding a stable\naddress (`AF_UNIX` on POSIX, `127.0.0.1` loopback on Windows); the winner hosts\nthe engine, losers proxy over the socket. The bind *is* the lease — when the\nowner dies the address frees and a survivor re-elects. A single-process\ndeployment is its own owner and pays no socket overhead. Knobs: `namespace`\n(defaults to a hash of cwd + argv[0]), `buffer_size` (default 32 — small on\npurpose, since each topic retains that many arbitrary payloads; raise it for a\nwider reconnect window).\n\n**Topic lifetime.** `publish(topic, message, ttl=None)` takes an optional ttl\n(seconds, at least `MIN_TOPIC_TTL` = 1s, so a reconnecting reader is not\noutrun). A topic with a ttl is released, buffer and sequence both, once nobody\nhas published to or read from it for that long; a later publish starts it over\nat 1, so a consumer returning with an old cursor gets `SharedStorageGap`.\nWithout a ttl a topic lives as long as the store. The latest publish's ttl\nwins. Streaming publishes with `STREAM_TOPIC_TTL` (300s); user topics default\nto no ttl. Every backend expires a topic as a whole, never message by message\nwhile it is read (`tests/shared_storage/test_topic_ttl.py` runs the same cases\non all three). Local: the engine records the ttl on the topic and sweeps idle\nones (no publish, head or poll, and no call holding it) at most once a second,\non the next pub/sub call. The sweep also drops empty, unheld topics, such as\nthe one a returning reader's poll recreates after a release. Redis: the ttl sits in a third key; the publish\nscript `PEXPIRE`s all three to 1.25 ttl, and a poll script renews them. A poll\non such a topic blocks in `XREAD` for at most a third of that lifetime, not the\nusual 5s, so a waiting reader renews it in time. Diskcache: the ttl\nsits in a key; on publish or poll, once less than one ttl is left, every key of\nthe topic (counter, ttl, buffered messages) is renewed to 1.25 ttl, and at\nonce when a publish changes the ttl. A poll on such a topic waits at most half\na ttl before it renews again.\n\n*Durability* is controlled by `mode` (the key/value store only — pub/sub is\nalways transient):\n\n| `mode` | Writes | On owner death / restart |\n|--------|--------|--------------------------|\n| `\"memory\"` *(default)* | none | re-elects **cold** (state lost) |\n| `\"persist\"` | write-through on every set/delete | re-elected/fresh owner **recovers** from disk |\n| `\"persist-reset\"` | flushed every `flush_interval`s (default 60) + on clean exit | recovers to the last snapshot (up to one interval may be lost on an unclean crash) |\n\nOnly the owner persists. The on-disk store is *chunked* (keys sharded across\nmsgpack chunk files via a key→chunk index, so a mutation rewrites only the\naffected chunk — adapted from raposa's `BinaryStorage`); each chunk write is\natomic (temp file + `os.replace`). TTLs survive restarts (stored as wall-clock\ndeadlines; expired keys are dropped on load). `path` sets the store directory\n(default: a per-namespace folder under the user cache dir). Recovery runs on\nevery election, so persisted data also survives owner re-election:\n\n```python\nDash(shared_storage=LocalSharedStorage(mode=\"persist\"))\nDash(shared_storage=LocalSharedStorage(mode=\"persist-reset\", flush_interval=30))\n```\n\n**`DiskcacheSharedStorage`** — KV + an append-log pub/sub on a `diskcache.Cache`,\nthe same store `DiskcacheManager` uses; pass an existing `cache` to share one.\nEvery process on one host that opens the same directory shares state, so it\ngives multi-worker parity with the local backend without a socket. **Not for\npods** — each pod has its own ephemeral disk.\n\n**`RedisSharedStorage`** — KV on Redis strings, pub/sub on a Redis Stream per\ntopic (an atomic `INCR`+`XADD` script keeps sequences ordered; `MAXLEN` bounds\nthe replay window; a subscriber past the trimmed floor gets `SharedStorageGap`).\nRedis is the single source of truth, so no owner election. Pass a `url`\n(defaults to `$REDIS_URL`) or an existing `client` to reuse a connection pool.\nThis is the only backend correct for **horizontally-scaled, multi-pod**\ndeployments. On an event loop it uses `redis.asyncio` (one client per loop),\nand one reader task per loop serves every async subscription with a single\nmulti-stream `XREAD`; a new subscription interrupts it through a private wake\nstream. Never park a blocking read in the executor per subscription: at a dozen\nopen downlinks that starved every other store call in the worker (25-50% failed\nstreams at 25 browsers on uvicorn `--workers 4`). Diskcache's async\nsubscriptions likewise poll without blocking and sleep on the loop.\n\n### Deployment Topology and Shared Storage\n\nThe default `LocalSharedStorage` is **per-container**: its socket/loopback\nelection only reaches processes in the same network + filesystem namespace.\n\n- **Single process / single pod** (e.g. Plotly Cloud apps): fine — one owner,\n  nothing to fragment.\n- **Multiple gunicorn workers in one container**: fine — they share the pod's\n  loopback/socket and elect one owner.\n- **Multiple pods behind a load balancer** (e.g. Dash Enterprise apps scaled by\n  an HPA, routed round-robin with no session affinity): **each pod elects its own\n  isolated owner**, so `set()`/`publish()` on one pod are invisible on another.\n  This works at 1 pod and fragments silently once it scales — use\n  `RedisSharedStorage` (one Redis shared by all pods) instead.\n\nA hosting platform can switch the backend without editing the app through\n`DASH_SHARED_STORAGE` (`_shared_storage/_env.py`), read only when the app did\nnot pass `shared_storage=` (the default is a sentinel, so an explicit argument,\n`None` included, always wins): `local`, `none`, `diskcache:///abs/path`, or a\n`redis://` / `rediss://` URL. A `key_prefix` query parameter on the Redis URL\n(`redis://host/0?key_prefix=myapp`) sets `RedisSharedStorage(key_prefix=)` and\nis stripped before the URL reaches redis-py; the diskcache path is\npercent-decoded. `cluster://` is reserved and raises; anything else raises\n`InvalidConfig` at construction, without echoing the value (it can hold\ncredentials). The value becomes a zero-argument\nfactory, so nothing is built or connected until `app.shared_storage` is first\nread, but a missing extra (`dash[redis]`, `dash[diskcache]`) fails at\nconstruction with the backend's own `ImportError`.\n\n### Custom and Out-of-Tree Backends\n\n`BaseSharedStorage` is the stable, public extension point. A backend — shipped\nin-tree or as a **separate package** — implements:\n\n- `get(key, default)` / `set(key, value)` / `delete(key)` — JSON-compatible values\n- `publish(topic, message, ttl=None)` and `subscribe(topic, replay_from=None) -> Subscription`\n- topic expiry: with a `ttl`, release the topic as a whole once nobody has\n  published to or read from it for that long, never message by message while\n  it is read; reject a ttl under `MIN_TOPIC_TTL` (run `test_topic_ttl.py`\n  against a new backend)\n- optional `start()` / `close()` (idempotent, called once per worker)\n\nand returns a `Subscription` (`__iter__` / `__aiter__` / `close`) that raises\n`SharedStorageGap` when the replay buffer overran. That trio —\n`BaseSharedStorage`, `Subscription`, `SharedStorageGap` — is exported from\ntop-level `dash`; the poll-loop helpers (`PollResult`, the polling subscription)\nare private and not part of the contract.\n\nCore only ships backends whose dependency is already a Dash extra (`msgspec`\nbase; `dash[diskcache]`; `dash[redis]`). Anything needing a heavier dependency\nbelongs **out of tree** behind this same interface — e.g. a Postgres backend\n(`LISTEN`/`NOTIFY` for push pub/sub + a table for KV and replay) lives in its own\npackage so a `psycopg` connection is never pulled into Dash core. It plugs in the\nsame way as a built-in: `Dash(shared_storage=PostgresSharedStorage(...))`.\n\n### Module Layout\n\n| File | Responsibility |\n|------|----------------|\n| `_shared_storage/base.py` | Abstract interface: `BaseSharedStorage`, `Subscription`, `SharedStorageError`, `SharedStorageGap` |\n| `_shared_storage/_engine.py` | `StoreEngine`: authoritative in-memory kv map + per-topic ordered log with bounded replay buffer (thread-safe, transport-agnostic) |\n| `_shared_storage/local.py` | `LocalSharedStorage`: owner election; owner hosts the engine, clients proxy |\n| `_shared_storage/diskcache.py` | `DiskcacheSharedStorage`: KV + append-log pub/sub on a `diskcache.Cache` |\n| `_shared_storage/redis.py` | `RedisSharedStorage`: KV + Redis Streams pub/sub (atomic `INCR`+`XADD`) |\n| `_shared_storage/_polling.py` | `PollingSubscription`: shared poll-loop subscription for the diskcache/Redis backends |\n| `_shared_storage/_transport.py` | Length-prefixed, token-gated socket transport (local backend) |\n| `_shared_storage/_codec.py` | msgspec msgpack codec (data-only) |\n| `_shared_storage/_env.py` | `DASH_SHARED_STORAGE` parsing |\n| `dash.py` | `shared_storage` constructor arg + lazy `app.shared_storage` property |\n| `_callback_context.py` | `dash.ctx.shared_storage` accessor |\n\nRequires `msgspec` (in `requirements/install.txt`); `DiskcacheSharedStorage`\nneeds the `diskcache` extra and `RedisSharedStorage` the `redis` extra.\n\n## Async Callbacks\n\nDash supports `async def` callbacks for non-blocking execution.\n\n### Setup\n\n**With Flask backend:**\n```bash\npip install dash[async]\n```\n\nAsync is auto-enabled when `asgiref` is detected. Or explicitly:\n\n```python\napp = Dash(__name__, use_async=True)\n```\n\n**With Quart or FastAPI backend:** Async is native - no extra dependencies needed.\n\n```python\nfrom fastapi import FastAPI\nfrom dash import Dash\n\nserver = FastAPI()\napp = Dash(__name__, server=server)  # Async works automatically\n```\n\n### Usage\n\n```python\nimport asyncio\n\n@app.callback(Output('output', 'children'), Input('input', 'value'))\nasync def async_update(value):\n    await asyncio.sleep(1)  # Non-blocking\n    return f\"Processed: {value}\"\n```\n\n### Key Points\n\n- Regular async callbacks are **non-blocking** - multiple can run concurrently\n- Background callbacks also support `async def`\n- Jupyter uses `nest_asyncio` for event loop compatibility\n- With Flask backend: requires `dash[async]`, coroutines raise error without it\n- With Quart/FastAPI backends: async is native, no extra setup needed\n\n### Async with Background Callbacks\n\n```python\n@app.callback(\n    Output('result', 'children'),\n    Input('btn', 'n_clicks'),\n    background=True,\n    manager=diskcache_manager,\n)\nasync def async_background(n_clicks):\n    await asyncio.sleep(5)\n    return \"Done\"\n```\n\nBoth DiskcacheManager and CeleryManager support async functions via `asyncio.run()`.\n\n## WebSocket Callbacks\n\nWebSocket callbacks use a persistent WebSocket connection instead of HTTP POST for callback execution. This reduces latency and connection overhead for applications with frequent callbacks.\n\n### Requirements\n\n- **FastAPI backend required**: WebSocket callbacks only work with FastAPI\n- **SharedWorker support**: Modern browsers (not IE)\n\n### Usage\n\n**Enable globally for all callbacks:**\n```python\nfrom fastapi import FastAPI\nfrom dash import Dash\n\nserver = FastAPI()\napp = Dash(__name__, server=server, websocket_callbacks=True)\n```\n\n**Enable per-callback:**\n```python\n@app.callback(\n    Output('output', 'children'),\n    Input('input', 'value'),\n    websocket=True  # Use WebSocket for this callback only\n)\ndef update(value):\n    return f\"Value: {value}\"\n```\n\n### Configuration\n\n```python\napp = Dash(\n    __name__,\n    server=server,\n    websocket_callbacks=True,\n    websocket_inactivity_timeout=300000,  # 5 minutes (default)\n    websocket_heartbeat_interval=30000,   # 30 seconds (default)\n    websocket_allowed_origins=['https://example.com'],\n)\n```\n\n- **`websocket_callbacks`** - Enable WebSocket for all callbacks (default: `False`)\n- **`websocket_inactivity_timeout`** - Close WebSocket after period of inactivity in milliseconds (default: `300000` = 5 minutes). Heartbeats do not count as activity. Set to `0` to disable timeout. Connection automatically reconnects when needed.\n- **`websocket_heartbeat_interval`** - Interval for heartbeat/keep-alive checks in milliseconds (default: `30000` = 30 seconds). Also determines how frequently inactivity timeout is checked.\n- **`websocket_allowed_origins`** - List of allowed origins for WebSocket connections (security)\n\n### Architecture\n\n```\n┌─────────────────────────────────────────────────────────────────────────┐\n│ Browser Tab 1                          Browser Tab 2                    │\n│ ┌─────────────┐                       ┌─────────────┐                   │\n│ │  Renderer   │                       │  Renderer   │                   │\n│ └──────┬──────┘                       └──────┬──────┘                   │\n│        │ postMessage                         │ postMessage              │\n│        └────────────┬───────────────────────┘                           │\n│                     ▼                                                   │\n│         ┌─────────────────────┐                                         │\n│         │    SharedWorker     │  (one per origin)                       │\n│         │   dash-ws-worker    │                                         │\n│         └──────────┬──────────┘                                         │\n└────────────────────│────────────────────────────────────────────────────┘\n                     │ WebSocket\n                     ▼\n┌─────────────────────────────────────────────────────────────────────────┐\n│ Server (FastAPI)                                                        │\n│   WebSocket Endpoint: /_dash-ws-callback                                │\n└─────────────────────────────────────────────────────────────────────────┘\n```\n\n**Connection & Reconnection Flow:**\n```\nRenderer                   SharedWorker                 Server\n    │                              │                        │\n    │──[CONNECT]──────────────────>│                        │\n    │                              │──[WebSocket Connect]──>│\n    │<─[CONNECTED]─────────────────│<─[Connected]───────────│\n    │                              │                        │\n    │──[CALLBACK_REQUEST]─────────>│──[callback request]───>│\n    │<─[CALLBACK_RESPONSE]─────────│<─[callback response]───│\n    │                              │                        │\n    │      (inactivity)            │    (heartbeat check)   │\n    │                              │──[close 4001]─────────>│\n    │<─[DISCONNECTED]──────────────│                        │\n    │                              │                        │\n    │──[CALLBACK_REQUEST]─────────>│──[reconnect + send]───>│\n    │<─[CALLBACK_RESPONSE]─────────│<─[response]────────────│\n```\n\n- **SharedWorker**: Single WebSocket connection shared across browser tabs\n- **Heartbeat**: Periodic ping/pong to detect dead connections (30s interval)\n- **Inactivity timeout**: Closes connection after no actual callback activity (not heartbeats)\n- **Auto-reconnect**: Reconnects automatically when a callback is triggered after timeout\n\n### Long-Running Callbacks with set_props/get_props\n\nWebSocket callbacks can stream updates to the client during execution using `set_props()` and read current component values using `ctx.websocket`:\n\n```python\nimport asyncio\nfrom dash import callback, Output, Input, set_props, ctx\nfrom dash.exceptions import PreventUpdate\n\n@callback(\n    Output('result', 'children'),\n    Input('start-btn', 'n_clicks'),\n    prevent_initial_call=True\n)\nasync def long_running_task(n_clicks):\n    ws = ctx.websocket\n    if not ws:\n        return \"WebSocket not available\"\n\n    # Stream progress updates to the client\n    for i in range(100):\n        # IMPORTANT: Check is_shutdown in loops to detect disconnections\n        if ws.is_shutdown:\n            raise PreventUpdate  # Exit gracefully on disconnect\n        await asyncio.sleep(0.1)\n        set_props('progress-bar', {'value': i + 1})\n        set_props('status', {'children': f'Processing step {i + 1}/100...'})\n\n    # Read current value from another component\n    current_value = await ws.get_prop('input-field', 'value')\n\n    return f\"Completed! Input was: {current_value}\"\n```\n\n**IMPORTANT - Checking `is_shutdown` in Loops:**\n\nLong-running callbacks that use loops **must** check `ws.is_shutdown` to detect when the WebSocket connection has closed. Without this check:\n- Callbacks continue running after the client disconnects, wasting server resources\n- `set_props` calls go to a closed connection and are lost\n- The callback result is never delivered to the client\n\nOnly \"persistent callbacks\" (callbacks with no Output and no Input that use only `set_props`) are automatically restarted when the WebSocket reconnects. Regular callbacks with outputs are not restarted.\n\n**API:**\n- `set_props(component_id, props_dict)` - Stream prop updates immediately to client\n- `ctx.websocket` - Get WebSocket interface (returns `None` if not in WS context)\n- `ws.is_shutdown` - Check if the WebSocket connection has been closed\n- `await ws.get_prop(component_id, prop_name, timeout=30.0, *, path=None)` - Read a full or nested prop value\n- `await ws.set_prop(component_id, prop_name, value)` - Set single prop (async version)\n- `await ws.close(code, reason)` - Close the WebSocket connection\n\n### Partial Reads with get_prop\n\nUse the keyword-only `path` argument with the same string keys and integer list\nindices supported by `Patch`:\n\n```python\nevent = await ws.get_prop(\n    'store', 'data', path=[",".github/copilot-instructions.md":"# Copilot Instructions\n\nThis file provides guidance to GitHub Copilot when working with this repository.\n\nFor detailed documentation, see the [`.ai/`](../.ai/) directory:\n- [Commands](../.ai/COMMANDS.md) - Build, test, lint\n- [Architecture](../.ai/ARCHITECTURE.md) - Backend, layout, callbacks\n- [Components](../.ai/COMPONENTS.md) - Component system, generation, resources\n\n## Project Overview\n\nDash is a Python framework for building reactive web-based data visualization applications. Built on Plotly.js, React, and Flask.\n\n## Quick Reference\n\n```bash\n# Setup\npip install -e .[ci,dev,testing,celery,diskcache] && npm ci\n\n# Build\nnpm run build                                    # Linux/Mac\nnpm run first-build                              # Windows (use Bash)\ndash-update-components \"dash-core-components\"    # Single component\n\n# Test\npytest tests/unit                                # Unit tests\npytest tests/integration                         # Integration tests\npytest -k test_name                              # Specific test\n\n# Lint\nnpm run lint                                     # All linters\nnpm run private::format.black                    # Auto-format Python\n```\n\n## Key Files\n\n- `dash/dash.py` - Main Dash app class, layout, callbacks, routing\n- `dash/_callback.py` - `@callback` decorator and execution\n- `dash/dependencies.py` - `Input`, `Output`, `State`, wildcards\n- `dash/development/base_component.py` - Component base class, `to_plotly_json()`\n- `dash/dash-renderer/` - TypeScript/React frontend\n- `components/` - Component packages (dcc, html, table)\n"},"files":{"AGENTS.md":"# AGENTS.md\n\nFor guidance on working with this repository, see the [`.ai/`](.ai/) directory:\n\n- [COMMANDS.md](.ai/COMMANDS.md) - Build, test, lint\n- [ARCHITECTURE.md](.ai/ARCHITECTURE.md) - Backend, callbacks, layout\n- [RENDERER.md](.ai/RENDERER.md) - Frontend, React, Redux\n- [COMPONENTS.md](.ai/COMPONENTS.md) - Component system\n- [TESTING.md](.ai/TESTING.md) - Testing patterns\n- [TROUBLESHOOTING.md](.ai/TROUBLESHOOTING.md) - Common issues\n","CLAUDE.md":"# CLAUDE.md\n\nGuidance for Claude Code (claude.ai/code) working in this repo.\n\n## Project Overview\n\nDash is a Python framework for building reactive web-based data visualization applications. Built on Plotly.js, React, and Flask.\n\n## Docs\n\nDeeper reference lives in `.ai/`. Open the one that covers what you're touching, don't read them all up front:\n\n- `COMMANDS.md` - build, test, lint commands\n- `ARCHITECTURE.md` - backend, callbacks, pages, background callbacks, stores, async, security\n- `RENDERER.md` - frontend, crawlLayout, Redux store, clientside API\n- `COMPONENTS.md` - component system, generation, resources\n- `TESTING.md` - test framework, fixtures, patterns\n- `TROUBLESHOOTING.md` - common errors and fixes\n- `PERFORMANCE.md` - benchmark harness, profiling, and performance findings\n\nTo find code, grep. Don't rely on a hand-kept file map, it goes stale.\n\n## Writing Style\n\nApplies to code comments, commit messages, PR descriptions, review comments, and CHANGELOG entries.\n\n- **Comments are a last resort.** The code is Python and should read on its own. Write a comment only when the code cannot explain itself: a non-obvious why, a workaround, a gotcha. Never narrate what the next line already says. If in doubt, leave it out.\n- **No em dashes or en dashes (`—` `–`).** Use a colon, comma, parentheses, or two sentences.\n- **Straight ASCII only.** Straight quotes and apostrophes (`'` `\"`), no curly ones. No ellipsis character (`…`); write three periods if you really need them. No decorative unicode.\n- **Plain words.** Say the thing directly, in words most people know. No AI filler, no thesaurus vocabulary, no heavy styling. Short and to the point.\n\n## Quality Gate\n\nBefore you say a change works:\n\n1. **Prove it in a running app.** Write a small repro app in the scratchpad and drive it headless with `dash_duo` (a throwaway pytest test is the practical way, see `.ai/TESTING.md`). Watch the real behavior. \"The code looks right\" is not done.\n2. **Leave a test behind.** Any behavior change or fix needs a test that stays in the tree: an integration/acceptance test under `tests/integration/`, or a renderer test for frontend-only work. It should fail before your change and pass after. Never run the whole integration suite at once, always a specific file or `-k`.\n3. **Second-model review.** Before handing back, launch a review subagent on a different model (the Agent tool takes a `model` override, e.g. Sonnet when you're on Opus) to check the diff for correctness and cases you missed. Fold in what holds up. A fresh model catches what the author misses.\n","components/dash-core-components/packages/README.md":"Folder to place temporary modules e.g. plotly.js-dist-min in .tgz format\n",".ai/ARCHITECTURE.md":"# Architecture\n\n## Python Backend Framework\n\n- **`dash/dash.py`** - Main `Dash` application class (~2000 lines). Orchestrates the server backend, layout management, callback registration, routing, and asset serving. Key methods: `layout` property, `callback()`, `clientside_callback()`, `run()`.\n\n- **`dash/backends/`** - Server backend implementations. See [Server Backends](#server-backends) section for details.\n\n- **`dash/_callback.py`** - Callback registration and execution. Contains `callback()` decorator (usable as `@dash.callback` without app instance), `clientside_callback()`, and `register_callback()` which inserts callbacks into the callback map.\n\n- **`dash/dependencies.py`** - Dependency classes for callbacks:\n  - `Input` - Triggers callback when value changes\n  - `Output` - Component property to update (supports `allow_duplicate=True`)\n  - `State` - Read value without triggering callback\n  - `ClientsideFunction` - Reference to JS function for clientside callbacks\n  - Wildcards: `MATCH`, `ALL`, `ALLSMALLER` for pattern-matching IDs\n\n- **`dash/development/base_component.py`** - `Component` base class with `ComponentMeta` metaclass. All Dash components inherit from this. Components auto-register in `ComponentRegistry` and serialize to JSON via `to_plotly_json()`.\n\n- **`dash/_pages.py`** - Multi-page app support. `PAGE_REGISTRY` holds registered pages, `register_page()` decorator registers page modules with routes.\n\n## Layout System\n\nThe layout defines the UI as a tree of components:\n\n```python\napp.layout = html.Div([\n    dcc.Input(id='input', value='initial'),\n    html.Div(id='output')\n])\n```\n\n- **Static layout**: Assigned directly as a component tree\n- **Dynamic layout**: Assigned as a function that returns components (called on each page load, useful for per-session state)\n- Layout is serialized to JSON and sent to the React frontend via `/_dash-layout`\n- Components can contain other components via `children` prop\n- Component IDs can be strings or dicts (for pattern-matching callbacks)\n\n## Callback Types\n\n### 1. Regular Callbacks\n\n`@app.callback` or `@dash.callback`:\n\n```python\n@app.callback(Output('output', 'children'), Input('input', 'value'))\ndef update(value):\n    return f'You entered: {value}'\n```\n\nServer-side Python function called when inputs change. Outputs update component properties.\n\n### 2. Clientside Callbacks\n\n`app.clientside_callback`:\n\n```python\napp.clientside_callback(\n    \"\"\"function(value) { return 'You entered: ' + value; }\"\"\",\n    Output('output', 'children'),\n    Input('input', 'value')\n)\n```\n\nJavaScript function runs in browser. Faster for simple transformations, no server round-trip. Can reference `window.dash_clientside.namespace.function_name` or inline JS string.\n\n### 3. Background Callbacks\n\n`background=True`:\n\n```python\n@app.callback(Output('output', 'children'), Input('btn', 'n_clicks'),\n              background=True, manager=diskcache_manager,\n              running=[(Output('btn', 'disabled'), True, False)],\n              progress=[Output('progress', 'value')])\ndef compute(set_progress, n_clicks):\n    for i in range(10):\n        set_progress(i * 10)\n        time.sleep(1)\n    return 'Done'\n```\n\nCallbacks executed in separate process via Celery or Diskcache manager. Supports `progress` updates, `running` state changes, and `cancel` inputs. See [Background Callbacks](#background-callbacks) section for details.\n\n### 4. Pattern-Matching Callbacks\n\n```python\n@app.callback(\n    Output({'type': 'output', 'index': MATCH}, 'children'),\n    Input({'type': 'input', 'index': MATCH}, 'value')\n)\ndef update(value):\n    return value\n```\n\nUse dict IDs with wildcards (`MATCH`, `ALL`, `ALLSMALLER`) to target dynamically-generated components.\n\n## Server Routes\n\n- `/_dash-layout` - Returns initial component tree as JSON\n- `/_dash-dependencies` - Returns callback definitions\n- `/_dash-update-component` - Executes callbacks, returns updated props\n- `/_dash-component-suites/<package>/<path>` - Serves component JS/CSS assets\n- `/assets/<path>` - Serves static assets from app's assets folder\n\n## Server Backends\n\nDash supports multiple web server backends. The backend abstraction is in `dash/backends/`.\n\n### Available Backends\n\n| Backend | Type | Install | Use Case |\n|---------|------|---------|----------|\n| **Flask** (default) | WSGI (sync) | `pip install dash` | Standard deployments, simplicity |\n| **Quart** | ASGI (async) | `pip install dash[quart]` | Async callbacks, WebSocket support |\n| **FastAPI** | ASGI (async) | `pip install dash[fastapi]` | OpenAPI docs, async, modern Python |\n\n### Usage\n\n**Default (Flask):**\n```python\nfrom dash import Dash\napp = Dash(__name__)\n```\n\n**With existing server instance:**\n```python\nfrom flask import Flask\nfrom dash import Dash\n\nserver = Flask(__name__)\napp = Dash(__name__, server=server)\n```\n\n**Quart backend:**\n```python\nfrom quart import Quart\nfrom dash import Dash\n\nserver = Quart(__name__)\napp = Dash(__name__, server=server)\n```\n\n**FastAPI backend:**\n```python\nfrom fastapi import FastAPI\nfrom dash import Dash\n\nserver = FastAPI()\napp = Dash(__name__, server=server)\n\n# Run with: uvicorn module:app.server --reload\n```\n\n### Architecture\n\nThe backend system uses an abstract interface:\n\n- **`BaseDashServer`** (`dash/backends/base_server.py`) - Abstract base class defining the server interface. All backends implement this.\n\n- **`RequestAdapter`** - Normalizes HTTP request objects across frameworks. Provides unified access to `args`, `cookies`, `headers`, `get_json()`, etc.\n\n- **`ResponseAdapter`** - Normalizes response creation. Handles `set_cookie()`, `set_header()`, `set_response()`.\n\n- **`get_backend(name)`** - Factory function to get backend class by name (`\"flask\"`, `\"quart\"`, `\"fastapi\"`).\n\n- **`get_server_type(server)`** - Auto-detects backend from a server instance.\n\n### Backend Implementations\n\n**Flask** (`dash/backends/_flask.py`):\n- `FlaskDashServer` - Wraps Flask app\n- `FlaskRequestAdapter` - Uses `flask.request` proxy\n- `FlaskResponseAdapter` - Uses `flask.Response`\n- Compression via `flask-compress`\n\n**Quart** (`dash/backends/_quart.py`):\n- `QuartDashServer` - Wraps Quart app (async Flask API)\n- `QuartRequestAdapter` - Uses `quart.request` proxy\n- `QuartResponseAdapter` - Uses `quart.Response`\n- All route handlers are `async def`\n- Compression via `quart-compress`\n\n**FastAPI** (`dash/backends/_fastapi.py`):\n- `FastAPIDashServer` - Wraps FastAPI app\n- `FastAPIRequestAdapter` - Uses context variable for current request\n- `FastAPIResponseAdapter` - Uses Starlette responses\n- `DashMiddleware` - Consolidated ASGI middleware for request handling\n  - Runs `before_request` hooks on every HTTP route (user routes, assets, mounted apps) and the WebSocket handshake; a non-None hook result is sent as the response, like Flask. Only Dash's own routes get their JSON body pre-read, timing recorded and Dash's error handling. Quart registers the same hooks as `before_websocket` too.\n- Runs with uvicorn, supports hot reload\n- Built-in GZip compression\n\n### Key Interface Methods\n\nAll backends implement:\n\n```python\nclass BaseDashServer(ABC):\n    def create_app(name, config) -> server        # Create new server\n    def add_url_rule(rule, view_func, ...)        # Register routes\n    def before_request(func)                       # Request hooks\n    def after_request(func)                        # Response hooks\n    def run(dash_app, host, port, debug)          # Start dev server\n    def make_response(data, mimetype, status)     # Create response\n    def jsonify(obj)                              # JSON response\n    def setup_index(dash_app)                     # Register / route\n    def serve_callback(dash_app)                  # Callback endpoint\n    def setup_component_suites(dash_app)          # JS/CSS serving\n```\n\n### Accessing the Backend\n\n```python\napp = Dash(__name__)\n\n# Get the underlying server\napp.server          # Flask/Quart/FastAPI instance\n\n# Get the backend wrapper\napp.backend         # BaseDashServer subclass instance\napp.backend.server_type  # \"flask\", \"quart\", or \"fastapi\"\n\n# Access request in callbacks\nfrom dash import dash\ndash.get_app().backend.request_adapter()  # RequestAdapter instance\n```\n\n## Frontend (dash-renderer)\n\n**`dash/dash-renderer/src/`** contains the TypeScript/React frontend. See [RENDERER.md](RENDERER.md) for detailed documentation on:\n\n- Layout traversal (`crawlLayout`) and `children_props`\n- Component resolution from `window[namespace][type]`\n- Callback triggering via `setProps` and `notifyObservers`\n- Redux store structure (layout, paths, callbacks, graphs)\n- Observer system for callback processing\n- `window.dash_clientside` API\n- `window.dash_component_api` API\n\n### React Version\n\nDash supports multiple React versions. Configured in `dash/_dash_renderer.py`.\n\n**Available versions:** 18.3.1 (default), 18.2.0, 19.2.4 (experimental)\n\nReact 19 has no official UMD builds; Dash serves the `umd-react` package for it, plus a small shim (`dash-renderer/build/react-shim.min.js`, source `dash/dash-renderer/src/react-shim.js`) loaded right after react-dom and before any component package. The shim stubs the React <=18 secret internals (`ReactCurrentOwner`) some component libraries touch at load time, redirects the legacy `react.element` `$$typeof` symbol so libraries that pre-bundled a React <=18 jsx-runtime don't hit React error #525, and provides `window.ReactJSXRuntime`, the global that component bundles externalize `react/jsx-runtime` to.\n\n**Convention for component libraries:** externalize `react/jsx-runtime` and `react/jsx-dev-runtime` using the *defensive* external expression found in `components/dash-core-components/webpack.config.js` (`jsxRuntimeExternal`), not a bare `'ReactJSXRuntime'` string. The expression falls back to building the runtime from `window.React.createElement` when the global is missing, so the same bundle works on Dash versions that predate the shim. A bare `'ReactJSXRuntime'` external throws `ReactJSXRuntime is not defined` / `Cannot read properties of undefined (reading 'jsx')` at bundle load on older Dash.\n\nSet via environment variable (experimental):\n\n```bash\nREACT_VERSION=19.2.4 python app.py\n```\n\nOr programmatically before creating the app:\n\n```python\nfrom dash._dash_renderer import _set_react_version\n_set_react_version(\"19.2.4\")\n\nfrom dash import Dash\napp = Dash(__name__)\n```\n\nThis is useful for compatibility with older component libraries that require React 16.\n\n## Pages System\n\nMulti-page apps use `dash/_pages.py` with automatic routing via `dcc.Location`.\n\n### Page Registration\n\nEach page module calls `register_page()`:\n\n```python\n# pages/analytics.py\nfrom dash import register_page, html\n\nregister_page(__name__)  # infers path /analytics from module name\n\nlayout = html.Div(\"Analytics page\")\n```\n\n- **`PAGE_REGISTRY`** - `OrderedDict` storing all registered pages with metadata\n- **`register_page(module, path=None, ...)`** - Registers page with inferred or explicit path, title, description, image\n\n### Page Container\n\nWhen `use_pages=True`, Dash injects `page_container` as the layout (`dash/dash.py:148-158`):\n\n```python\npage_container = html.Div([\n    dcc.Location(id=\"_pages_location\", refresh=\"callback-nav\"),\n    html.Div(id=\"_pages_content\"),      # current page layout injected here\n    dcc.Store(id=\"_pages_store\"),       # stores page title/metadata\n])\n```\n\n### Routing Mechanism\n\n1. `dcc.Location` tracks browser URL changes\n2. Internal callback listens to `pathname` and `search` inputs\n3. `_path_to_page()` matches URL to registered page in `PAGE_REGISTRY`\n4. Page layout injected into `_pages_content` div\n\n### Path Templates (Dynamic Routes)\n\nPages can capture URL variables:\n\n```python\nregister_page(__name__, path_template=\"/asset/<asset_id>\")\n\ndef layout(asset_id=None):\n    return html.Div(f\"Asset: {asset_id}\")\n```\n\n`_parse_path_variables()` extracts variables via regex and passes them as kwargs to the layout function.\n\n### Auto-Discovery\n\n`_import_layouts_from_pages()` walks the `pages/` folder:\n- Skips files starting with `_` or `.`\n- Only imports `.py` files containing `register_page`\n- Auto-assigns `layout` attribute from each module to the registry\n\n### Page Ordering\n\nPages sorted by: numeric `order` → string `order` → no order → module name. Home page (`/`) defaults to order `0`.\n\n## Assets and Static Files\n\n### Asset Directory\n\nThe `assets/` folder is automatically scanned at startup (`dash/dash.py:_walk_assets_directory`):\n\n- `.css` files → appended to stylesheets\n- `.js` files → appended to scripts\n- `favicon.ico` → used as app favicon\n- Files matching `assets_ignore` regex are skipped\n\n### Loading Order\n\nResources load in this order (`dash/dash.py:1127-1165`):\n\n1. React dependencies (from dash-renderer)\n2. Component library scripts (dash-html-components, dash-core-components, etc.)\n3. External scripts (`external_scripts` parameter)\n4. Dash renderer bundle\n5. Clientside callback scripts (inline)\n\nCSS follows similar ordering with external stylesheets first.\n\n### Fingerprinting and Caching\n\nComponent assets use fingerprinted URLs for cache busting (`dash/fingerprint.py`):\n\n```\n/_dash-component-suites/dash_core_components/dash_core_components.v2_14_0m1699900000.min.js\n```\n\n- Fingerprinted resources: 1-year cache header\n- Non-fingerprinted: ETag validation\n- Asset files: query string `?m={modification_time}`\n\n### Configuration Options\n\n```python\nDash(\n    assets_folder='assets',           # path to assets directory\n    assets_url_path='assets',         # URL path segment\n    assets_ignore='.*ignored.*',      # regex to skip files\n    assets_external_path=None,        # CDN base URL for assets\n    serve_locally=True,               # True=local files, False=CDN\n    external_scripts=[],              # additional JS URLs\n    external_stylesheets=[],          # additional CSS URLs\n)\n```\n\n### Asset URL Generation\n\n`app.get_asset_url(path)` returns the correct URL accounting for `requests_pathname_prefix` (important for Dash Enterprise deployments where apps have URL prefixes).\n\n## Error Handling\n\n### Debug Mode\n\nDebug mode enables developer tools (`dash/dash.py:_setup_dev_tools`):\n\n```python\napp.run(debug=True)\n# Or via environment: DASH_DEBUG=true\n```\n\n### Dev Tools Options\n\n```python\napp.enable_dev_tools(\n    dev_tools_ui=True,              # show error UI overlay\n    dev_tools_props_check=True,     # validate component prop types\n    dev_tools_serve_dev_bundles=True,  # use development JS (better errors)\n    dev_tools_hot_reload=True,      # auto-reload on file changes\n    dev_tools_prune_errors=True,    # strip internal frames from tracebacks\n)\n```\n\nEnvironment variables: `DASH_DEBUG`, `DASH_UI`, `DASH_PROPS_CHECK`, `DASH_HOT_RELOAD`, etc.\n\n### Callback Exceptions\n\n**`PreventUpdate`** - Skip updating outputs without error:\n\n```python\nfrom dash.exceptions import PreventUpdate\n\n@app.callback(Output('out', 'children'), Input('in', 'value'))\ndef update(value):\n    if not value:\n        raise PreventUpdate\n    return value\n```\n\n**`no_update`** - Skip specific outputs in multi-output callbacks:\n\n```python\nfrom dash import no_update\n\n@app.callback(Output('a', 'children'), Output('b', 'children'), Input('in', 'value'))\ndef update(value):\n    return value, no_update  # only updates 'a'\n```\n\n### Error Handlers\n\nCallbacks support `on_error` for custom error handling:\n\n```python\ndef handle_error(err):\n    logging.error(f\"Callback failed: {err}\")\n    return \"Error occurred\"  # returned to output\n\n@app.callback(Output('out', 'children'), Input('in', 'value'), on_error=handle_error)\ndef update(value):\n    return 1 / 0  # triggers error handler\n```\n\nApp-level error handler set via constructor.\n\n### Validation\n\n- **Layout validation**: When `suppress_callback_exceptions=False` (default), checks that callback IDs exist in layout\n- **Callback validation**: `dev_tools_validate_callbacks=True` checks for circular dependencies\n- **Props checking**: Validates component prop types against schema in dev mode\n\n### Hot Reload\n\nWhen enabled, a watch thread monitors:\n- `assets/` folder for CSS/JS changes\n- Component package directories\n\nFrontend polls `/_reload-hash` and triggers reload when hash changes. Configurable via `hot_reload_interval` (default 3s) and `hot_reload_watch_interval` (default 0.5s).\n\n## Background Callbacks\n\nBackground callbacks execute in separate processes, allowing the main server to remain responsive. Managed by `dash/background_callback/managers/`.\n\n### Definition\n\n```python\nfrom dash import callback, Input, Output\nfrom dash.background_callback import DiskcacheManager\n\ncache_manager = DiskcacheManager()\n\n@callback(\n    Output(\"result\", \"children\"),\n    Input(\"button\", \"n_clicks\"),\n    background=True,\n    manager=cache_manager,\n    interval=500,  # polling interval in ms\n)\ndef compute(n_clicks):\n    # Expensive computation\n    return result\n```\n\n### Callback Managers\n\n**`DiskcacheManager`** (`dash/background_callback/managers/diskcache_manager.py`):\n- Uses `diskcache.Cache` for persistent storage\n- Spawns `multiprocess.Process` for each job\n- Results stored on disk, survives server restarts\n- Good for single-server deployments\n\n**`CeleryManager`** (`dash/background_callback/managers/celery_manager.py`):\n- Requires Celery app with result backend (Redis/RabbitMQ)\n- Jobs distributed across Celery workers\n- Supports horizontal scaling\n- Good for production multi-worker deployments\n\n```python\nfrom celery import Celery\nfrom dash.background_callback import CeleryManager\n\ncelery_app = Celery(__name__, broker=\"redis://localhost:6379/0\")\ncache_manager = CeleryManager(celery_app)\n```\n\n### Progress Updates\n\nThe `progress` parameter defines outputs updated during execution:\n\n```python\n@callback(\n    Output(\"result\", \"children\"),\n    Input(\"button\", \"n_clicks\"),\n    progress=Output(\"progress-bar\", \"value\"),\n    progress_default=0,\n    background=True,\n    manager=cache_manager,\n)\ndef compute(set_progress, n_clicks):\n    for i in range(100):\n        set_progress(i)\n        time.sleep(0.1)\n    return \"Complete\"\n```\n\n- `set_progress` is injected as first argument when `progress` is specified\n- Can be single Output or list of Outputs\n- `progress_default` sets value when callback not running\n\n### Running State\n\nThe `running` parameter updates outputs while the job executes:\n\n```python\n@callback(\n    Output(\"result\", \"children\"),\n    Input(\"button\", \"n_clicks\"),\n    running=[\n        (Output(\"button\", \"disabled\"), True, False),\n        (Output(\"status\", \"children\"), \"Computing...\", \"Ready\"),\n    ],\n    background=True,\n    manager=cache_manager,\n)\ndef compute(n_clicks):\n    time.sleep(5)\n    return \"Done\"\n```\n\nEach tuple: `(Output, value_while_running, value_when_complete)`\n\n### Cancellation\n\nThe `cancel` parameter specifies inputs that abort the job:\n\n```python\n@callback(\n    Output(\"result\", \"children\"),\n    Input(\"start-btn\", \"n_clicks\"),\n    cancel=[Input(\"cancel-btn\", \"n_clicks\")],\n    background=True,\n    manager=cache_manager,\n)\ndef compute(n_clicks):\n    # Job terminates if cancel-btn clicked\n    return result\n```\n\nManagers call `terminate_job()` which kills the process (Diskcache) or revokes the task (Celery).\n\n### Result Caching\n\nResults can be cached to avoid recomputation:\n\n```python\ndef get_user_id():\n    return flask.session.get(\"user_id\")\n\ncache_manager = DiskcacheManager(\n    cache_by=[get_user_id],  # cache key includes user ID\n    expire=3600,             # TTL in seconds\n)\n```\n\n- `cache_by` - List of functions whose return values are included in cache key\n- `expire` - Time-to-live for cached results\n- `cache_args_to_ignore` - Argument indices to exclude from cache key\n\n### How It Works\n\n1. **Initial request**: Frontend triggers callback, backend returns `cacheKey` and `job` ID\n2. **Polling**: Frontend polls `/_dash-update-component?cacheKey=...&job=...` at configured interval\n3. **Progress**: Each poll returns current progress value if set\n4. **Completion**: When job finishes, poll returns final result\n5. **Cleanup**: Results cleared from cache (unless `cache_by` specified)\n\nCache key is SHA256 hash of: callback key + function source + arguments + triggered inputs + cache_by values. Without `cache_by` a random nonce is added, so every run gets its own key: the job runs with the requester's context, so two users with equal inputs must never share a result. Diskcache writes those uncached entries with a one day expiry (`UNCACHED_RESULT_EXPIRE`) so results nobody polls for do not pile up; Celery's result backend already expires them.\n\n### Key Files\n\n- `dash/_callback.py:188-219` - Background spec construction\n- `dash/background_callback/managers/__init__.py` - `BaseBackgroundCallbackManager` abstract class\n- `dash/background_callback/managers/diskcache_manager.py` - Diskcache implementation\n- `dash/background_callback/managers/celery_manager.py` - Celery implementation\n- `dash/dash-renderer/src/actions/callbacks.ts:458-685` - Frontend polling logic\n\n## Jupyter Integration\n\nDash apps can run directly in Jupyter notebooks and JupyterLab. The integration is handled by `dash/_jupyter.py`.\n\n### Display Modes\n\n```python\napp.run(\n    jupyter_mode=\"inline\",      # Display in notebook cell (default)\n    jupyter_width=\"100%\",       # IFrame width\n    jupyter_height=650,         # IFrame height in pixels\n)\n```\n\n| Mode | Behavior |\n|------|----------|\n| `\"inline\"` | App displays in notebook cell via IFrame |\n| `\"external\"` | Prints URL, user opens in browser tab |\n| `\"jupyterlab\"` | Opens in dedicated JupyterLab tab |\n| `\"tab\"` | Auto-opens URL in new browser tab |\n\n### How It Works\n\n1. `app.run()` detects Jupyter environment via `get_ipython()`\n2. Server starts in background daemon thread\n3. Jupyter comm protocol negotiates proxy configuration\n4. App displays according to selected mode\n\n```\napp.run() in notebook\n    ↓\nDetect Jupyter → Start server in background thread\n    ↓\nComm request → Extension responds with base_url\n    ↓\nCompute dashboard URL with proxy path\n    ↓\nDisplay: IFrame (inline) / URL (external) / Tab (jupyterlab)\n```\n\n### Notebook Extension\n\nClassic Jupyter notebooks use `dash/nbextension/`:\n\n- `main.js` - Registers \"dash\" comm target\n- `dash.json` - Extension loader configuration\n\nThe extension handles comm messages:\n- `base_url_request` → responds with server URL and base path\n- Enables proper proxy routing in JupyterHub environments\n\n### JupyterLab Extension\n\nJupyterLab uses `@plotly/dash-jupyterlab/`:\n\n- `src/index.ts` - TypeScript plugin implementing `JupyterFrontEndPlugin`\n- `DashIFrameWidget` - Lumino widget for rendering apps in tabs\n\nHandles messages:\n- `base_url_request` → responds with JupyterLab server config\n- `show` → creates dedicated tab with IFrame widget\n\nCompatible with JupyterLab 2.x, 3.x, and 4.x.\n\n### Proxy Configuration\n\nIn JupyterHub/proxy environments, the extension negotiates `requests_pathname_prefix`:\n\n```python\n# Computed from Jupyter base path\nrequests_pathname_prefix = \"/user/username/proxy/8050/\"\n```\n\nThis ensures callbacks route correctly through the Jupyter proxy.\n\n### Google Colab\n\nSpecial handling for Colab:\n- Uses `google.colab.output.serve_kernel_port_as_iframe()` for inline\n- Uses `google.colab.output.serve_kernel_port_as_window()` for external\n- Only supports \"inline\" and \"external\" modes\n\n### Key Files\n\n- `dash/_jupyter.py` - `JupyterDash` class, comm handling, server thread\n- `dash/nbextension/main.js` - Classic notebook extension\n- `@plotly/dash-jupyterlab/src/index.ts` - JupyterLab extension\n\n## Configuration Reference\n\n### Dash() Constructor Parameters\n\n**Basic Setup:**\n- `name` - Application name (default: infers from `__name__`)\n- `server` - Server instance (Flask, Quart, or FastAPI) or `True` to create Flask (default: `True`)\n- `title` - Browser tab title (default: `\"Dash\"`)\n- `update_title` - Title during callbacks (default: `\"Updating...\"`)\n\n**Assets & Resources:**\n- `assets_folder` - Path to assets directory (default: `\"assets\"`)\n- `assets_url_path` - URL path for assets (default: `\"assets\"`)\n- `assets_ignore` - Regex to exclude assets (default: `\"\"`)\n- `serve_locally` - Serve from local vs CDN (default: `True`)\n- `external_scripts` - Additional JS URLs\n- `external_stylesheets` - Additional CSS URLs\n\n**Routing:**\n- `url_base_pathname` - Base URL prefix for entire app\n- `requests_pathname_prefix` - Prefix for AJAX requests\n- `routes_pathname_prefix` - Prefix for API routes\n\n**Multi-Page:**\n- `use_pages` - Enable pages system (default: auto-detect)\n- `pages_folder` - Path to pages directory (default: `\"pages\"`)\n\n**Behavior:**\n- `suppress_callback_exceptions` - Skip callback validation (default: `False`)\n- `prevent_initial_callbacks` - Skip callbacks on load (default: `False`)\n- `background_callback_manager` - DiskcacheManager or CeleryManager\n- `on_error` - Global callback error handler\n- `shared_storage` - Cross-process state + pub/sub backend (default: `LocalSharedStorage`). Pass `None` to disable, or a `BaseSharedStorage` subclass/instance to swap. See [Shared Storage](#shared-storage).\n\n**WebSocket Callbacks:**\n- `websocket_callbacks` - Enable WebSocket for all callbacks (default: `False`). Requires FastAPI backend.\n- `websocket_allowed_origins` - List of allowed origins for WebSocket connections\n- `websocket_inactivity_timeout` - Disconnect WebSocket after inactivity period in ms (default: `300000` = 5 minutes). Set to `0` to disable.\n\n### app.run() Parameters\n\n- `host` - Server IP (default: `\"127.0.0.1\"`, env: `HOST`)\n- `port` - Server port (default: `8050`, env: `PORT`)\n- `debug` - Enable dev tools (default: `False`, env: `DASH_DEBUG`)\n- `jupyter_mode` - Display mode: `\"inline\"`, `\"external\"`, `\"tab\"`\n\n### Environment Variables\n\n| Variable | Purpose |\n|----------|---------|\n| `DASH_DEBUG` | Enable debug mode |\n| `DASH_URL_BASE_PATHNAME` | Base URL prefix |\n| `DASH_SUPPRESS_CALLBACK_EXCEPTIONS` | Skip validation |\n| `DASH_HOT_RELOAD` | Enable hot reload |\n| `DASH_PROPS_CHECK` | Validate prop types |\n| `DASH_PRUNE_ERRORS` | Simplify tracebacks |\n| `HOST` | Server host |\n| `PORT` | Server port |\n| `DASH_SECRET_KEY` | Signing secret for page, background and stream tokens when `server.secret_key` is unset |\n| `DASH_SHARED_STORAGE` | Shared-storage backend when `shared_storage=` is not passed (see Shared Storage) |\n\n## Stores and Client-Side State\n\n### dcc.Store\n\nStore data client-side with configurable persistence:\n\n```python\ndcc.Store(id='my-store', storage_type='local', data={'key': 'value'})\n```\n\n| Storage Type | Persists | Scope | Use Case |\n|--------------|----------|-------|----------|\n| `'memory'` | Page view only | Tab | Temporary state, debugging |\n| `'session'` | Browser session | Tab | Form state, filters |\n| `'local'` | Forever | All tabs | User preferences, settings |\n\n**Usage pattern:**\n```python\n@app.callback(Output('output', 'children'), Input('store', 'data'))\ndef use_store(data):\n    return data['key']\n\n@app.callback(Output('store', 'data'), Input('input', 'value'))\ndef update_store(value):\n    return {'key': value}\n```\n\n### Component Persistence\n\nAutomatically persist user edits to component props:\n\n```python\ndcc.Dropdown(\n    id='dropdown',\n    options=[...],\n    persistence=True,           # Enable persistence\n    persistence_type='local',   # local, session, or memory\n    persisted_props=['value'],  # Props to persist (default varies by component)\n)\n```\n\n- **`persistence`** - `True` or unique key to enable\n- **`persistence_type`** - Storage backend (default: `'local'`)\n- **`persisted_props`** - List of prop names to persist\n\nSupported components: Input, Dropdown, Checklist, RadioItems, Slider, RangeSlider, DatePickerSingle, DatePickerRange, Textarea, Tabs, DataTable.\n\n### When to Use Each\n\n| Need | Solution |\n|------|----------|\n| Server-controlled state | `dcc.Store` with callbacks |\n| Remember user selections | Component `persistence=True` |\n| Share state across tabs | `dcc.Store` with `storage_type='local'` |\n| Session-only state | `persistence_type='session'` |\n\n## Shared Storage\n\nBackend-agnostic **server-side** state shared across worker processes: a\ncross-process key/value store plus an ordered, replayable publish/subscribe\nchannel. Unlike `dcc.Store` (which lives in the browser), shared storage lives\non the server and lets callbacks running in different workers see the same keys\nand topics without standing up an external service like Redis.\n\nEnabled by default on every app. It starts lazily on first use, so it costs\nnothing until touched.\n\n### Accessing It\n\n```python\nimport dash\nfrom dash import Input, Output, callback\n\n@callback(Output(\"out\", \"children\"), Input(\"btn\", \"n_clicks\"))\ndef handler(n):\n    store = dash.ctx.shared_storage      # inside a callback\n    store.set(\"clicks\", n)\n    return store.get(\"clicks\", 0)\n\n# Or off the app directly:\napp = dash.Dash()\napp.shared_storage.set(\"key\", {\"any\": \"json-compatible value\"})\n```\n\nValues (and published messages) must be **JSON-compatible** — dict / list / str\n/ int / float / bool / None — the same constraint as `dcc.Store` and callback\noutputs. Messages are encoded with `msgspec` (msgpack), a hard dependency.\n\n### API\n\n`app.shared_storage` (and `dash.ctx.shared_storage`) is a `BaseSharedStorage`:\n\n| Method | Purpose |\n|--------|---------|\n| `get(key, default=None)` | Read a value |\n| `set(key, value, ttl=None)` | Write a value; `ttl` = optional lifetime in seconds |\n| `delete(key)` | Remove a key |\n| `publish(topic, message, ttl=None)` | Append a message to a topic; `ttl` = release the topic after that long idle |\n| `subscribe(topic, replay_from=None)` | Return a `Subscription` |\n\n**Key expiry (TTL).** `set(key, value, ttl=<seconds>)` gives a key a bounded\nlifetime; once it elapses, reads return the `default` again. `ttl=None` (the\ndefault) never expires. Expiry is lazy/best-effort — an expired key is dropped\non its next read, not at a guaranteed instant. Each backend uses its native\nmechanism: a monotonic deadline on the in-memory owner, diskcache `expire`,\nRedis `PX`. Use it for session-scoped or cache-like state you don't want to\naccumulate unboundedly.\n\n**Ordered pub/sub.** Each `publish` to a topic gets a monotonically increasing\nsequence number (per topic, starting at 1; `0` means \"before the first\nmessage\"). A subscriber receives every message published after it subscribed,\nin order:\n\n```python\n# Producer (one worker):\nstore.publish(\"progress\", {\"pct\": 50})\n\n# Consumer (another worker/callback):\nwith store.subscribe(\"progress\") as sub:\n    for message in sub:          # or: async for message in sub\n        print(message[\"pct\"])\n```\n\n**Replay on reconnect.** Pass `replay_from=<last-seen-seq>` to resume after a\ndrop; buffered messages since that cursor replay first, so a reconnecting\nconsumer doesn't miss messages. If the consumer fell farther behind than the\nbounded buffer holds, the subscription raises `SharedStorageGap` (an explicit\ngap rather than a silent hole). A `Subscription` is iterable synchronously\n(`for`) or asynchronously (`async for`), is a context manager, and has\n`close()`.\n\n### Backends\n\nThree backends ship, differing only in **how far state reaches**. Pick by the\ndeployment topology — critically, whether the app runs behind a load balancer as\nmore than one container/pod (see [Deployment Topology](#deployment-topology-and-shared-storage) below).\n\n| Backend | Backing store | Reaches | Extra |\n|---------|---------------|---------|-------|\n| `LocalSharedStorage` *(default)* | in-memory, owner-elected socket | one **container** (all its workers) | none |\n| `DiskcacheSharedStorage` | a `diskcache.Cache` | one **host** (all processes sharing the dir) | `dash[diskcache]` |\n| `RedisSharedStorage` | Redis (Streams for pub/sub) | **any** process/container/pod | `dash[redis]` |\n\nAll three implement the same `BaseSharedStorage` contract (KV + ordered,\nreplayable pub/sub with `SharedStorageGap` on buffer overrun), so app code is\nidentical across them — only the constructor differs.\n\n```python\nfrom dash import Dash\nfrom dash._shared_storage import (\n    LocalSharedStorage, DiskcacheSharedStorage, RedisSharedStorage,\n)\n\nDash(shared_storage=LocalSharedStorage)                       # default\nDash(shared_storage=DiskcacheSharedStorage(directory=\"/tmp/ss\"))\nDash(shared_storage=RedisSharedStorage(url=\"redis://host:6379\"))\nDash(shared_storage=None)                                     # disabled\n```\n\nWhen disabled, accessing `app.shared_storage` / `dash.ctx.shared_storage`\nraises `SharedStorageError`.\n\n**`LocalSharedStorage`** — in-memory, no external service. On first use every\nworker races to become the single **owner** for the machine by binding a stable\naddress (`AF_UNIX` on POSIX, `127.0.0.1` loopback on Windows); the winner hosts\nthe engine, losers proxy over the socket. The bind *is* the lease — when the\nowner dies the address frees and a survivor re-elects. A single-process\ndeployment is its own owner and pays no socket overhead. Knobs: `namespace`\n(defaults to a hash of cwd + argv[0]), `buffer_size` (default 32 — small on\npurpose, since each topic retains that many arbitrary payloads; raise it for a\nwider reconnect window).\n\n**Topic lifetime.** `publish(topic, message, ttl=None)` takes an optional ttl\n(seconds, at least `MIN_TOPIC_TTL` = 1s, so a reconnecting reader is not\noutrun). A topic with a ttl is released, buffer and sequence both, once nobody\nhas published to or read from it for that long; a later publish starts it over\nat 1, so a consumer returning with an old cursor gets `SharedStorageGap`.\nWithout a ttl a topic lives as long as the store. The latest publish's ttl\nwins. Streaming publishes with `STREAM_TOPIC_TTL` (300s); user topics default\nto no ttl. Every backend expires a topic as a whole, never message by message\nwhile it is read (`tests/shared_storage/test_topic_ttl.py` runs the same cases\non all three). Local: the engine records the ttl on the topic and sweeps idle\nones (no publish, head or poll, and no call holding it) at most once a second,\non the next pub/sub call. The sweep also drops empty, unheld topics, such as\nthe one a returning reader's poll recreates after a release. Redis: the ttl sits in a third key; the publish\nscript `PEXPIRE`s all three to 1.25 ttl, and a poll script renews them. A poll\non such a topic blocks in `XREAD` for at most a third of that lifetime, not the\nusual 5s, so a waiting reader renews it in time. Diskcache: the ttl\nsits in a key; on publish or poll, once less than one ttl is left, every key of\nthe topic (counter, ttl, buffered messages) is renewed to 1.25 ttl, and at\nonce when a publish changes the ttl. A poll on such a topic waits at most half\na ttl before it renews again.\n\n*Durability* is controlled by `mode` (the key/value store only — pub/sub is\nalways transient):\n\n| `mode` | Writes | On owner death / restart |\n|--------|--------|--------------------------|\n| `\"memory\"` *(default)* | none | re-elects **cold** (state lost) |\n| `\"persist\"` | write-through on every set/delete | re-elected/fresh owner **recovers** from disk |\n| `\"persist-reset\"` | flushed every `flush_interval`s (default 60) + on clean exit | recovers to the last snapshot (up to one interval may be lost on an unclean crash) |\n\nOnly the owner persists. The on-disk store is *chunked* (keys sharded across\nmsgpack chunk files via a key→chunk index, so a mutation rewrites only the\naffected chunk — adapted from raposa's `BinaryStorage`); each chunk write is\natomic (temp file + `os.replace`). TTLs survive restarts (stored as wall-clock\ndeadlines; expired keys are dropped on load). `path` sets the store directory\n(default: a per-namespace folder under the user cache dir). Recovery runs on\nevery election, so persisted data also survives owner re-election:\n\n```python\nDash(shared_storage=LocalSharedStorage(mode=\"persist\"))\nDash(shared_storage=LocalSharedStorage(mode=\"persist-reset\", flush_interval=30))\n```\n\n**`DiskcacheSharedStorage`** — KV + an append-log pub/sub on a `diskcache.Cache`,\nthe same store `DiskcacheManager` uses; pass an existing `cache` to share one.\nEvery process on one host that opens the same directory shares state, so it\ngives multi-worker parity with the local backend without a socket. **Not for\npods** — each pod has its own ephemeral disk.\n\n**`RedisSharedStorage`** — KV on Redis strings, pub/sub on a Redis Stream per\ntopic (an atomic `INCR`+`XADD` script keeps sequences ordered; `MAXLEN` bounds\nthe replay window; a subscriber past the trimmed floor gets `SharedStorageGap`).\nRedis is the single source of truth, so no owner election. Pass a `url`\n(defaults to `$REDIS_URL`) or an existing `client` to reuse a connection pool.\nThis is the only backend correct for **horizontally-scaled, multi-pod**\ndeployments. On an event loop it uses `redis.asyncio` (one client per loop),\nand one reader task per loop serves every async subscription with a single\nmulti-stream `XREAD`; a new subscription interrupts it through a private wake\nstream. Never park a blocking read in the executor per subscription: at a dozen\nopen downlinks that starved every other store call in the worker (25-50% failed\nstreams at 25 browsers on uvicorn `--workers 4`). Diskcache's async\nsubscriptions likewise poll without blocking and sleep on the loop.\n\n### Deployment Topology and Shared Storage\n\nThe default `LocalSharedStorage` is **per-container**: its socket/loopback\nelection only reaches processes in the same network + filesystem namespace.\n\n- **Single process / single pod** (e.g. Plotly Cloud apps): fine — one owner,\n  nothing to fragment.\n- **Multiple gunicorn workers in one container**: fine — they share the pod's\n  loopback/socket and elect one owner.\n- **Multiple pods behind a load balancer** (e.g. Dash Enterprise apps scaled by\n  an HPA, routed round-robin with no session affinity): **each pod elects its own\n  isolated owner**, so `set()`/`publish()` on one pod are invisible on another.\n  This works at 1 pod and fragments silently once it scales — use\n  `RedisSharedStorage` (one Redis shared by all pods) instead.\n\nA hosting platform can switch the backend without editing the app through\n`DASH_SHARED_STORAGE` (`_shared_storage/_env.py`), read only when the app did\nnot pass `shared_storage=` (the default is a sentinel, so an explicit argument,\n`None` included, always wins): `local`, `none`, `diskcache:///abs/path`, or a\n`redis://` / `rediss://` URL. A `key_prefix` query parameter on the Redis URL\n(`redis://host/0?key_prefix=myapp`) sets `RedisSharedStorage(key_prefix=)` and\nis stripped before the URL reaches redis-py; the diskcache path is\npercent-decoded. `cluster://` is reserved and raises; anything else raises\n`InvalidConfig` at construction, without echoing the value (it can hold\ncredentials). The value becomes a zero-argument\nfactory, so nothing is built or connected until `app.shared_storage` is first\nread, but a missing extra (`dash[redis]`, `dash[diskcache]`) fails at\nconstruction with the backend's own `ImportError`.\n\n### Custom and Out-of-Tree Backends\n\n`BaseSharedStorage` is the stable, public extension point. A backend — shipped\nin-tree or as a **separate package** — implements:\n\n- `get(key, default)` / `set(key, value)` / `delete(key)` — JSON-compatible values\n- `publish(topic, message, ttl=None)` and `subscribe(topic, replay_from=None) -> Subscription`\n- topic expiry: with a `ttl`, release the topic as a whole once nobody has\n  published to or read from it for that long, never message by message while\n  it is read; reject a ttl under `MIN_TOPIC_TTL` (run `test_topic_ttl.py`\n  against a new backend)\n- optional `start()` / `close()` (idempotent, called once per worker)\n\nand returns a `Subscription` (`__iter__` / `__aiter__` / `close`) that raises\n`SharedStorageGap` when the replay buffer overran. That trio —\n`BaseSharedStorage`, `Subscription`, `SharedStorageGap` — is exported from\ntop-level `dash`; the poll-loop helpers (`PollResult`, the polling subscription)\nare private and not part of the contract.\n\nCore only ships backends whose dependency is already a Dash extra (`msgspec`\nbase; `dash[diskcache]`; `dash[redis]`). Anything needing a heavier dependency\nbelongs **out of tree** behind this same interface — e.g. a Postgres backend\n(`LISTEN`/`NOTIFY` for push pub/sub + a table for KV and replay) lives in its own\npackage so a `psycopg` connection is never pulled into Dash core. It plugs in the\nsame way as a built-in: `Dash(shared_storage=PostgresSharedStorage(...))`.\n\n### Module Layout\n\n| File | Responsibility |\n|------|----------------|\n| `_shared_storage/base.py` | Abstract interface: `BaseSharedStorage`, `Subscription`, `SharedStorageError`, `SharedStorageGap` |\n| `_shared_storage/_engine.py` | `StoreEngine`: authoritative in-memory kv map + per-topic ordered log with bounded replay buffer (thread-safe, transport-agnostic) |\n| `_shared_storage/local.py` | `LocalSharedStorage`: owner election; owner hosts the engine, clients proxy |\n| `_shared_storage/diskcache.py` | `DiskcacheSharedStorage`: KV + append-log pub/sub on a `diskcache.Cache` |\n| `_shared_storage/redis.py` | `RedisSharedStorage`: KV + Redis Streams pub/sub (atomic `INCR`+`XADD`) |\n| `_shared_storage/_polling.py` | `PollingSubscription`: shared poll-loop subscription for the diskcache/Redis backends |\n| `_shared_storage/_transport.py` | Length-prefixed, token-gated socket transport (local backend) |\n| `_shared_storage/_codec.py` | msgspec msgpack codec (data-only) |\n| `_shared_storage/_env.py` | `DASH_SHARED_STORAGE` parsing |\n| `dash.py` | `shared_storage` constructor arg + lazy `app.shared_storage` property |\n| `_callback_context.py` | `dash.ctx.shared_storage` accessor |\n\nRequires `msgspec` (in `requirements/install.txt`); `DiskcacheSharedStorage`\nneeds the `diskcache` extra and `RedisSharedStorage` the `redis` extra.\n\n## Async Callbacks\n\nDash supports `async def` callbacks for non-blocking execution.\n\n### Setup\n\n**With Flask backend:**\n```bash\npip install dash[async]\n```\n\nAsync is auto-enabled when `asgiref` is detected. Or explicitly:\n\n```python\napp = Dash(__name__, use_async=True)\n```\n\n**With Quart or FastAPI backend:** Async is native - no extra dependencies needed.\n\n```python\nfrom fastapi import FastAPI\nfrom dash import Dash\n\nserver = FastAPI()\napp = Dash(__name__, server=server)  # Async works automatically\n```\n\n### Usage\n\n```python\nimport asyncio\n\n@app.callback(Output('output', 'children'), Input('input', 'value'))\nasync def async_update(value):\n    await asyncio.sleep(1)  # Non-blocking\n    return f\"Processed: {value}\"\n```\n\n### Key Points\n\n- Regular async callbacks are **non-blocking** - multiple can run concurrently\n- Background callbacks also support `async def`\n- Jupyter uses `nest_asyncio` for event loop compatibility\n- With Flask backend: requires `dash[async]`, coroutines raise error without it\n- With Quart/FastAPI backends: async is native, no extra setup needed\n\n### Async with Background Callbacks\n\n```python\n@app.callback(\n    Output('result', 'children'),\n    Input('btn', 'n_clicks'),\n    background=True,\n    manager=diskcache_manager,\n)\nasync def async_background(n_clicks):\n    await asyncio.sleep(5)\n    return \"Done\"\n```\n\nBoth DiskcacheManager and CeleryManager support async functions via `asyncio.run()`.\n\n## WebSocket Callbacks\n\nWebSocket callbacks use a persistent WebSocket connection instead of HTTP POST for callback execution. This reduces latency and connection overhead for applications with frequent callbacks.\n\n### Requirements\n\n- **FastAPI backend required**: WebSocket callbacks only work with FastAPI\n- **SharedWorker support**: Modern browsers (not IE)\n\n### Usage\n\n**Enable globally for all callbacks:**\n```python\nfrom fastapi import FastAPI\nfrom dash import Dash\n\nserver = FastAPI()\napp = Dash(__name__, server=server, websocket_callbacks=True)\n```\n\n**Enable per-callback:**\n```python\n@app.callback(\n    Output('output', 'children'),\n    Input('input', 'value'),\n    websocket=True  # Use WebSocket for this callback only\n)\ndef update(value):\n    return f\"Value: {value}\"\n```\n\n### Configuration\n\n```python\napp = Dash(\n    __name__,\n    server=server,\n    websocket_callbacks=True,\n    websocket_inactivity_timeout=300000,  # 5 minutes (default)\n    websocket_heartbeat_interval=30000,   # 30 seconds (default)\n    websocket_allowed_origins=['https://example.com'],\n)\n```\n\n- **`websocket_callbacks`** - Enable WebSocket for all callbacks (default: `False`)\n- **`websocket_inactivity_timeout`** - Close WebSocket after period of inactivity in milliseconds (default: `300000` = 5 minutes). Heartbeats do not count as activity. Set to `0` to disable timeout. Connection automatically reconnects when needed.\n- **`websocket_heartbeat_interval`** - Interval for heartbeat/keep-alive checks in milliseconds (default: `30000` = 30 seconds). Also determines how frequently inactivity timeout is checked.\n- **`websocket_allowed_origins`** - List of allowed origins for WebSocket connections (security)\n\n### Architecture\n\n```\n┌─────────────────────────────────────────────────────────────────────────┐\n│ Browser Tab 1                          Browser Tab 2                    │\n│ ┌─────────────┐                       ┌─────────────┐                   │\n│ │  Renderer   │                       │  Renderer   │                   │\n│ └──────┬──────┘                       └──────┬──────┘                   │\n│        │ postMessage                         │ postMessage              │\n│        └────────────┬───────────────────────┘                           │\n│                     ▼                                                   │\n│         ┌─────────────────────┐                                         │\n│         │    SharedWorker     │  (one per origin)                       │\n│         │   dash-ws-worker    │                                         │\n│         └──────────┬──────────┘                                         │\n└────────────────────│────────────────────────────────────────────────────┘\n                     │ WebSocket\n                     ▼\n┌─────────────────────────────────────────────────────────────────────────┐\n│ Server (FastAPI)                                                        │\n│   WebSocket Endpoint: /_dash-ws-callback                                │\n└─────────────────────────────────────────────────────────────────────────┘\n```\n\n**Connection & Reconnection Flow:**\n```\nRenderer                   SharedWorker                 Server\n    │                              │                        │\n    │──[CONNECT]──────────────────>│                        │\n    │                              │──[WebSocket Connect]──>│\n    │<─[CONNECTED]─────────────────│<─[Connected]───────────│\n    │                              │                        │\n    │──[CALLBACK_REQUEST]─────────>│──[callback request]───>│\n    │<─[CALLBACK_RESPONSE]─────────│<─[callback response]───│\n    │                              │                        │\n    │      (inactivity)            │    (heartbeat check)   │\n    │                              │──[close 4001]─────────>│\n    │<─[DISCONNECTED]──────────────│                        │\n    │                              │                        │\n    │──[CALLBACK_REQUEST]─────────>│──[reconnect + send]───>│\n    │<─[CALLBACK_RESPONSE]─────────│<─[response]────────────│\n```\n\n- **SharedWorker**: Single WebSocket connection shared across browser tabs\n- **Heartbeat**: Periodic ping/pong to detect dead connections (30s interval)\n- **Inactivity timeout**: Closes connection after no actual callback activity (not heartbeats)\n- **Auto-reconnect**: Reconnects automatically when a callback is triggered after timeout\n\n### Long-Running Callbacks with set_props/get_props\n\nWebSocket callbacks can stream updates to the client during execution using `set_props()` and read current component values using `ctx.websocket`:\n\n```python\nimport asyncio\nfrom dash import callback, Output, Input, set_props, ctx\nfrom dash.exceptions import PreventUpdate\n\n@callback(\n    Output('result', 'children'),\n    Input('start-btn', 'n_clicks'),\n    prevent_initial_call=True\n)\nasync def long_running_task(n_clicks):\n    ws = ctx.websocket\n    if not ws:\n        return \"WebSocket not available\"\n\n    # Stream progress updates to the client\n    for i in range(100):\n        # IMPORTANT: Check is_shutdown in loops to detect disconnections\n        if ws.is_shutdown:\n            raise PreventUpdate  # Exit gracefully on disconnect\n        await asyncio.sleep(0.1)\n        set_props('progress-bar', {'value': i + 1})\n        set_props('status', {'children': f'Processing step {i + 1}/100...'})\n\n    # Read current value from another component\n    current_value = await ws.get_prop('input-field', 'value')\n\n    return f\"Completed! Input was: {current_value}\"\n```\n\n**IMPORTANT - Checking `is_shutdown` in Loops:**\n\nLong-running callbacks that use loops **must** check `ws.is_shutdown` to detect when the WebSocket connection has closed. Without this check:\n- Callbacks continue running after the client disconnects, wasting server resources\n- `set_props` calls go to a closed connection and are lost\n- The callback result is never delivered to the client\n\nOnly \"persistent callbacks\" (callbacks with no Output and no Input that use only `set_props`) are automatically restarted when the WebSocket reconnects. Regular callbacks with outputs are not restarted.\n\n**API:**\n- `set_props(component_id, props_dict)` - Stream prop updates immediately to client\n- `ctx.websocket` - Get WebSocket interface (returns `None` if not in WS context)\n- `ws.is_shutdown` - Check if the WebSocket connection has been closed\n- `await ws.get_prop(component_id, prop_name, timeout=30.0, *, path=None)` - Read a full or nested prop value\n- `await ws.set_prop(component_id, prop_name, value)` - Set single prop (async version)\n- `await ws.close(code, reason)` - Close the WebSocket connection\n\n### Partial Reads with get_prop\n\nUse the keyword-only `path` argument with the same string keys and integer list\nindices supported by `Patch`:\n\n```python\nevent = await ws.get_prop(\n    'store', 'data', path=[",".github/copilot-instructions.md":"# Copilot Instructions\n\nThis file provides guidance to GitHub Copilot when working with this repository.\n\nFor detailed documentation, see the [`.ai/`](../.ai/) directory:\n- [Commands](../.ai/COMMANDS.md) - Build, test, lint\n- [Architecture](../.ai/ARCHITECTURE.md) - Backend, layout, callbacks\n- [Components](../.ai/COMPONENTS.md) - Component system, generation, resources\n\n## Project Overview\n\nDash is a Python framework for building reactive web-based data visualization applications. Built on Plotly.js, React, and Flask.\n\n## Quick Reference\n\n```bash\n# Setup\npip install -e .[ci,dev,testing,celery,diskcache] && npm ci\n\n# Build\nnpm run build                                    # Linux/Mac\nnpm run first-build                              # Windows (use Bash)\ndash-update-components \"dash-core-components\"    # Single component\n\n# Test\npytest tests/unit                                # Unit tests\npytest tests/integration                         # Integration tests\npytest -k test_name                              # Specific test\n\n# Lint\nnpm run lint                                     # All linters\nnpm run private::format.black                    # Auto-format Python\n```\n\n## Key Files\n\n- `dash/dash.py` - Main Dash app class, layout, callbacks, routing\n- `dash/_callback.py` - `@callback` decorator and execution\n- `dash/dependencies.py` - `Input`, `Output`, `State`, wildcards\n- `dash/development/base_component.py` - Component base class, `to_plotly_json()`\n- `dash/dash-renderer/` - TypeScript/React frontend\n- `components/` - Component packages (dcc, html, table)\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/plotly/dash/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# AGENTS.md\n\nFor guidance on working with this repository, see the [`.ai/`](.ai/) directory:\n\n- [COMMANDS.md](.ai/COMMANDS.md) - Build, test, lint\n- [ARCHITECTURE.md](.ai/ARCHITECTURE.md) - Backend, callbacks, layout\n- [RENDERER.md](.ai/RENDERER.md) - Frontend, React, Redux\n- [COMPONENTS.md](.ai/COMPONENTS.md) - Component system\n- [TESTING.md](.ai/TESTING.md) - Testing patterns\n- [TROUBLESHOOTING.md](.ai/TROUBLESHOOTING.md) - Common issues\n","isInternal":false,"tokens":152,"sizeBytes":444},{"name":"ARCHITECTURE.md","path":".ai/ARCHITECTURE.md","rawUrl":"https://raw.githubusercontent.com/plotly/dash/HEAD/.ai/ARCHITECTURE.md","title":".ai - System Architecture & Module Boundaries","category":"project-spec","format":"markdown","content":"# Architecture\n\n## Python Backend Framework\n\n- **`dash/dash.py`** - Main `Dash` application class (~2000 lines). Orchestrates the server backend, layout management, callback registration, routing, and asset serving. Key methods: `layout` property, `callback()`, `clientside_callback()`, `run()`.\n\n- **`dash/backends/`** - Server backend implementations. See [Server Backends](#server-backends) section for details.\n\n- **`dash/_callback.py`** - Callback registration and execution. Contains `callback()` decorator (usable as `@dash.callback` without app instance), `clientside_callback()`, and `register_callback()` which inserts callbacks into the callback map.\n\n- **`dash/dependencies.py`** - Dependency classes for callbacks:\n  - `Input` - Triggers callback when value changes\n  - `Output` - Component property to update (supports `allow_duplicate=True`)\n  - `State` - Read value without triggering callback\n  - `ClientsideFunction` - Reference to JS function for clientside callbacks\n  - Wildcards: `MATCH`, `ALL`, `ALLSMALLER` for pattern-matching IDs\n\n- **`dash/development/base_component.py`** - `Component` base class with `ComponentMeta` metaclass. All Dash components inherit from this. Components auto-register in `ComponentRegistry` and serialize to JSON via `to_plotly_json()`.\n\n- **`dash/_pages.py`** - Multi-page app support. `PAGE_REGISTRY` holds registered pages, `register_page()` decorator registers page modules with routes.\n\n## Layout System\n\nThe layout defines the UI as a tree of components:\n\n```python\napp.layout = html.Div([\n    dcc.Input(id='input', value='initial'),\n    html.Div(id='output')\n])\n```\n\n- **Static layout**: Assigned directly as a component tree\n- **Dynamic layout**: Assigned as a function that returns components (called on each page load, useful for per-session state)\n- Layout is serialized to JSON and sent to the React frontend via `/_dash-layout`\n- Components can contain other components via `children` prop\n- Component IDs can be strings or dicts (for pattern-matching callbacks)\n\n## Callback Types\n\n### 1. Regular Callbacks\n\n`@app.callback` or `@dash.callback`:\n\n```python\n@app.callback(Output('output', 'children'), Input('input', 'value'))\ndef update(value):\n    return f'You entered: {value}'\n```\n\nServer-side Python function called when inputs change. Outputs update component properties.\n\n### 2. Clientside Callbacks\n\n`app.clientside_callback`:\n\n```python\napp.clientside_callback(\n    \"\"\"function(value) { return 'You entered: ' + value; }\"\"\",\n    Output('output', 'children'),\n    Input('input', 'value')\n)\n```\n\nJavaScript function runs in browser. Faster for simple transformations, no server round-trip. Can reference `window.dash_clientside.namespace.function_name` or inline JS string.\n\n### 3. Background Callbacks\n\n`background=True`:\n\n```python\n@app.callback(Output('output', 'children'), Input('btn', 'n_clicks'),\n              background=True, manager=diskcache_manager,\n              running=[(Output('btn', 'disabled'), True, False)],\n              progress=[Output('progress', 'value')])\ndef compute(set_progress, n_clicks):\n    for i in range(10):\n        set_progress(i * 10)\n        time.sleep(1)\n    return 'Done'\n```\n\nCallbacks executed in separate process via Celery or Diskcache manager. Supports `progress` updates, `running` state changes, and `cancel` inputs. See [Background Callbacks](#background-callbacks) section for details.\n\n### 4. Pattern-Matching Callbacks\n\n```python\n@app.callback(\n    Output({'type': 'output', 'index': MATCH}, 'children'),\n    Input({'type': 'input', 'index': MATCH}, 'value')\n)\ndef update(value):\n    return value\n```\n\nUse dict IDs with wildcards (`MATCH`, `ALL`, `ALLSMALLER`) to target dynamically-generated components.\n\n## Server Routes\n\n- `/_dash-layout` - Returns initial component tree as JSON\n- `/_dash-dependencies` - Returns callback definitions\n- `/_dash-update-component` - Executes callbacks, returns updated props\n- `/_dash-component-suites/<package>/<path>` - Serves component JS/CSS assets\n- `/assets/<path>` - Serves static assets from app's assets folder\n\n## Server Backends\n\nDash supports multiple web server backends. The backend abstraction is in `dash/backends/`.\n\n### Available Backends\n\n| Backend | Type | Install | Use Case |\n|---------|------|---------|----------|\n| **Flask** (default) | WSGI (sync) | `pip install dash` | Standard deployments, simplicity |\n| **Quart** | ASGI (async) | `pip install dash[quart]` | Async callbacks, WebSocket support |\n| **FastAPI** | ASGI (async) | `pip install dash[fastapi]` | OpenAPI docs, async, modern Python |\n\n### Usage\n\n**Default (Flask):**\n```python\nfrom dash import Dash\napp = Dash(__name__)\n```\n\n**With existing server instance:**\n```python\nfrom flask import Flask\nfrom dash import Dash\n\nserver = Flask(__name__)\napp = Dash(__name__, server=server)\n```\n\n**Quart backend:**\n```python\nfrom quart import Quart\nfrom dash import Dash\n\nserver = Quart(__name__)\napp = Dash(__name__, server=server)\n```\n\n**FastAPI backend:**\n```python\nfrom fastapi import FastAPI\nfrom dash import Dash\n\nserver = FastAPI()\napp = Dash(__name__, server=server)\n\n# Run with: uvicorn module:app.server --reload\n```\n\n### Architecture\n\nThe backend system uses an abstract interface:\n\n- **`BaseDashServer`** (`dash/backends/base_server.py`) - Abstract base class defining the server interface. All backends implement this.\n\n- **`RequestAdapter`** - Normalizes HTTP request objects across frameworks. Provides unified access to `args`, `cookies`, `headers`, `get_json()`, etc.\n\n- **`ResponseAdapter`** - Normalizes response creation. Handles `set_cookie()`, `set_header()`, `set_response()`.\n\n- **`get_backend(name)`** - Factory function to get backend class by name (`\"flask\"`, `\"quart\"`, `\"fastapi\"`).\n\n- **`get_server_type(server)`** - Auto-detects backend from a server instance.\n\n### Backend Implementations\n\n**Flask** (`dash/backends/_flask.py`):\n- `FlaskDashServer` - Wraps Flask app\n- `FlaskRequestAdapter` - Uses `flask.request` proxy\n- `FlaskResponseAdapter` - Uses `flask.Response`\n- Compression via `flask-compress`\n\n**Quart** (`dash/backends/_quart.py`):\n- `QuartDashServer` - Wraps Quart app (async Flask API)\n- `QuartRequestAdapter` - Uses `quart.request` proxy\n- `QuartResponseAdapter` - Uses `quart.Response`\n- All route handlers are `async def`\n- Compression via `quart-compress`\n\n**FastAPI** (`dash/backends/_fastapi.py`):\n- `FastAPIDashServer` - Wraps FastAPI app\n- `FastAPIRequestAdapter` - Uses context variable for current request\n- `FastAPIResponseAdapter` - Uses Starlette responses\n- `DashMiddleware` - Consolidated ASGI middleware for request handling\n  - Runs `before_request` hooks on every HTTP route (user routes, assets, mounted apps) and the WebSocket handshake; a non-None hook result is sent as the response, like Flask. Only Dash's own routes get their JSON body pre-read, timing recorded and Dash's error handling. Quart registers the same hooks as `before_websocket` too.\n- Runs with uvicorn, supports hot reload\n- Built-in GZip compression\n\n### Key Interface Methods\n\nAll backends implement:\n\n```python\nclass BaseDashServer(ABC):\n    def create_app(name, config) -> server        # Create new server\n    def add_url_rule(rule, view_func, ...)        # Register routes\n    def before_request(func)                       # Request hooks\n    def after_request(func)                        # Response hooks\n    def run(dash_app, host, port, debug)          # Start dev server\n    def make_response(data, mimetype, status)     # Create response\n    def jsonify(obj)                              # JSON response\n    def setup_index(dash_app)                     # Register / route\n    def serve_callback(dash_app)                  # Callback endpoint\n    def setup_component_suites(dash_app)          # JS/CSS serving\n```\n\n### Accessing the Backend\n\n```python\napp = Dash(__name__)\n\n# Get the underlying server\napp.server          # Flask/Quart/FastAPI instance\n\n# Get the backend wrapper\napp.backend         # BaseDashServer subclass instance\napp.backend.server_type  # \"flask\", \"quart\", or \"fastapi\"\n\n# Access request in callbacks\nfrom dash import dash\ndash.get_app().backend.request_adapter()  # RequestAdapter instance\n```\n\n## Frontend (dash-renderer)\n\n**`dash/dash-renderer/src/`** contains the TypeScript/React frontend. See [RENDERER.md](RENDERER.md) for detailed documentation on:\n\n- Layout traversal (`crawlLayout`) and `children_props`\n- Component resolution from `window[namespace][type]`\n- Callback triggering via `setProps` and `notifyObservers`\n- Redux store structure (layout, paths, callbacks, graphs)\n- Observer system for callback processing\n- `window.dash_clientside` API\n- `window.dash_component_api` API\n\n### React Version\n\nDash supports multiple React versions. Configured in `dash/_dash_renderer.py`.\n\n**Available versions:** 18.3.1 (default), 18.2.0, 19.2.4 (experimental)\n\nReact 19 has no official UMD builds; Dash serves the `umd-react` package for it, plus a small shim (`dash-renderer/build/react-shim.min.js`, source `dash/dash-renderer/src/react-shim.js`) loaded right after react-dom and before any component package. The shim stubs the React <=18 secret internals (`ReactCurrentOwner`) some component libraries touch at load time, redirects the legacy `react.element` `$$typeof` symbol so libraries that pre-bundled a React <=18 jsx-runtime don't hit React error #525, and provides `window.ReactJSXRuntime`, the global that component bundles externalize `react/jsx-runtime` to.\n\n**Convention for component libraries:** externalize `react/jsx-runtime` and `react/jsx-dev-runtime` using the *defensive* external expression found in `components/dash-core-components/webpack.config.js` (`jsxRuntimeExternal`), not a bare `'ReactJSXRuntime'` string. The expression falls back to building the runtime from `window.React.createElement` when the global is missing, so the same bundle works on Dash versions that predate the shim. A bare `'ReactJSXRuntime'` external throws `ReactJSXRuntime is not defined` / `Cannot read properties of undefined (reading 'jsx')` at bundle load on older Dash.\n\nSet via environment variable (experimental):\n\n```bash\nREACT_VERSION=19.2.4 python app.py\n```\n\nOr programmatically before creating the app:\n\n```python\nfrom dash._dash_renderer import _set_react_version\n_set_react_version(\"19.2.4\")\n\nfrom dash import Dash\napp = Dash(__name__)\n```\n\nThis is useful for compatibility with older component libraries that require React 16.\n\n## Pages System\n\nMulti-page apps use `dash/_pages.py` with automatic routing via `dcc.Location`.\n\n### Page Registration\n\nEach page module calls `register_page()`:\n\n```python\n# pages/analytics.py\nfrom dash import register_page, html\n\nregister_page(__name__)  # infers path /analytics from module name\n\nlayout = html.Div(\"Analytics page\")\n```\n\n- **`PAGE_REGISTRY`** - `OrderedDict` storing all registered pages with metadata\n- **`register_page(module, path=None, ...)`** - Registers page with inferred or explicit path, title, description, image\n\n### Page Container\n\nWhen `use_pages=True`, Dash injects `page_container` as the layout (`dash/dash.py:148-158`):\n\n```python\npage_container = html.Div([\n    dcc.Location(id=\"_pages_location\", refresh=\"callback-nav\"),\n    html.Div(id=\"_pages_content\"),      # current page layout injected here\n    dcc.Store(id=\"_pages_store\"),       # stores page title/metadata\n])\n```\n\n### Routing Mechanism\n\n1. `dcc.Location` tracks browser URL changes\n2. Internal callback listens to `pathname` and `search` inputs\n3. `_path_to_page()` matches URL to registered page in `PAGE_REGISTRY`\n4. Page layout injected into `_pages_content` div\n\n### Path Templates (Dynamic Routes)\n\nPages can capture URL variables:\n\n```python\nregister_page(__name__, path_template=\"/asset/<asset_id>\")\n\ndef layout(asset_id=None):\n    return html.Div(f\"Asset: {asset_id}\")\n```\n\n`_parse_path_variables()` extracts variables via regex and passes them as kwargs to the layout function.\n\n### Auto-Discovery\n\n`_import_layouts_from_pages()` walks the `pages/` folder:\n- Skips files starting with `_` or `.`\n- Only imports `.py` files containing `register_page`\n- Auto-assigns `layout` attribute from each module to the registry\n\n### Page Ordering\n\nPages sorted by: numeric `order` → string `order` → no order → module name. Home page (`/`) defaults to order `0`.\n\n## Assets and Static Files\n\n### Asset Directory\n\nThe `assets/` folder is automatically scanned at startup (`dash/dash.py:_walk_assets_directory`):\n\n- `.css` files → appended to stylesheets\n- `.js` files → appended to scripts\n- `favicon.ico` → used as app favicon\n- Files matching `assets_ignore` regex are skipped\n\n### Loading Order\n\nResources load in this order (`dash/dash.py:1127-1165`):\n\n1. React dependencies (from dash-renderer)\n2. Component library scripts (dash-html-components, dash-core-components, etc.)\n3. External scripts (`external_scripts` parameter)\n4. Dash renderer bundle\n5. Clientside callback scripts (inline)\n\nCSS follows similar ordering with external stylesheets first.\n\n### Fingerprinting and Caching\n\nComponent assets use fingerprinted URLs for cache busting (`dash/fingerprint.py`):\n\n```\n/_dash-component-suites/dash_core_components/dash_core_components.v2_14_0m1699900000.min.js\n```\n\n- Fingerprinted resources: 1-year cache header\n- Non-fingerprinted: ETag validation\n- Asset files: query string `?m={modification_time}`\n\n### Configuration Options\n\n```python\nDash(\n    assets_folder='assets',           # path to assets directory\n    assets_url_path='assets',         # URL path segment\n    assets_ignore='.*ignored.*',      # regex to skip files\n    assets_external_path=None,        # CDN base URL for assets\n    serve_locally=True,               # True=local files, False=CDN\n    external_scripts=[],              # additional JS URLs\n    external_stylesheets=[],          # additional CSS URLs\n)\n```\n\n### Asset URL Generation\n\n`app.get_asset_url(path)` returns the correct URL accounting for `requests_pathname_prefix` (important for Dash Enterprise deployments where apps have URL prefixes).\n\n## Error Handling\n\n### Debug Mode\n\nDebug mode enables developer tools (`dash/dash.py:_setup_dev_tools`):\n\n```python\napp.run(debug=True)\n# Or via environment: DASH_DEBUG=true\n```\n\n### Dev Tools Options\n\n```python\napp.enable_dev_tools(\n    dev_tools_ui=True,              # show error UI overlay\n    dev_tools_props_check=True,     # validate component prop types\n    dev_tools_serve_dev_bundles=True,  # use development JS (better errors)\n    dev_tools_hot_reload=True,      # auto-reload on file changes\n    dev_tools_prune_errors=True,    # strip internal frames from tracebacks\n)\n```\n\nEnvironment variables: `DASH_DEBUG`, `DASH_UI`, `DASH_PROPS_CHECK`, `DASH_HOT_RELOAD`, etc.\n\n### Callback Exceptions\n\n**`PreventUpdate`** - Skip updating outputs without error:\n\n```python\nfrom dash.exceptions import PreventUpdate\n\n@app.callback(Output('out', 'children'), Input('in', 'value'))\ndef update(value):\n    if not value:\n        raise PreventUpdate\n    return value\n```\n\n**`no_update`** - Skip specific outputs in multi-output callbacks:\n\n```python\nfrom dash import no_update\n\n@app.callback(Output('a', 'children'), Output('b', 'children'), Input('in', 'value'))\ndef update(value):\n    return value, no_update  # only updates 'a'\n```\n\n### Error Handlers\n\nCallbacks support `on_error` for custom error handling:\n\n```python\ndef handle_error(err):\n    logging.error(f\"Callback failed: {err}\")\n    return \"Error occurred\"  # returned to output\n\n@app.callback(Output('out', 'children'), Input('in', 'value'), on_error=handle_error)\ndef update(value):\n    return 1 / 0  # triggers error handler\n```\n\nApp-level error handler set via constructor.\n\n### Validation\n\n- **Layout validation**: When `suppress_callback_exceptions=False` (default), checks that callback IDs exist in layout\n- **Callback validation**: `dev_tools_validate_callbacks=True` checks for circular dependencies\n- **Props checking**: Validates component prop types against schema in dev mode\n\n### Hot Reload\n\nWhen enabled, a watch thread monitors:\n- `assets/` folder for CSS/JS changes\n- Component package directories\n\nFrontend polls `/_reload-hash` and triggers reload when hash changes. Configurable via `hot_reload_interval` (default 3s) and `hot_reload_watch_interval` (default 0.5s).\n\n## Background Callbacks\n\nBackground callbacks execute in separate processes, allowing the main server to remain responsive. Managed by `dash/background_callback/managers/`.\n\n### Definition\n\n```python\nfrom dash import callback, Input, Output\nfrom dash.background_callback import DiskcacheManager\n\ncache_manager = DiskcacheManager()\n\n@callback(\n    Output(\"result\", \"children\"),\n    Input(\"button\", \"n_clicks\"),\n    background=True,\n    manager=cache_manager,\n    interval=500,  # polling interval in ms\n)\ndef compute(n_clicks):\n    # Expensive computation\n    return result\n```\n\n### Callback Managers\n\n**`DiskcacheManager`** (`dash/background_callback/managers/diskcache_manager.py`):\n- Uses `diskcache.Cache` for persistent storage\n- Spawns `multiprocess.Process` for each job\n- Results stored on disk, survives server restarts\n- Good for single-server deployments\n\n**`CeleryManager`** (`dash/background_callback/managers/celery_manager.py`):\n- Requires Celery app with result backend (Redis/RabbitMQ)\n- Jobs distributed across Celery workers\n- Supports horizontal scaling\n- Good for production multi-worker deployments\n\n```python\nfrom celery import Celery\nfrom dash.background_callback import CeleryManager\n\ncelery_app = Celery(__name__, broker=\"redis://localhost:6379/0\")\ncache_manager = CeleryManager(celery_app)\n```\n\n### Progress Updates\n\nThe `progress` parameter defines outputs updated during execution:\n\n```python\n@callback(\n    Output(\"result\", \"children\"),\n    Input(\"button\", \"n_clicks\"),\n    progress=Output(\"progress-bar\", \"value\"),\n    progress_default=0,\n    background=True,\n    manager=cache_manager,\n)\ndef compute(set_progress, n_clicks):\n    for i in range(100):\n        set_progress(i)\n        time.sleep(0.1)\n    return \"Complete\"\n```\n\n- `set_progress` is injected as first argument when `progress` is specified\n- Can be single Output or list of Outputs\n- `progress_default` sets value when callback not running\n\n### Running State\n\nThe `running` parameter updates outputs while the job executes:\n\n```python\n@callback(\n    Output(\"result\", \"children\"),\n    Input(\"button\", \"n_clicks\"),\n    running=[\n        (Output(\"button\", \"disabled\"), True, False),\n        (Output(\"status\", \"children\"), \"Computing...\", \"Ready\"),\n    ],\n    background=True,\n    manager=cache_manager,\n)\ndef compute(n_clicks):\n    time.sleep(5)\n    return \"Done\"\n```\n\nEach tuple: `(Output, value_while_running, value_when_complete)`\n\n### Cancellation\n\nThe `cancel` parameter specifies inputs that abort the job:\n\n```python\n@callback(\n    Output(\"result\", \"children\"),\n    Input(\"start-btn\", \"n_clicks\"),\n    cancel=[Input(\"cancel-btn\", \"n_clicks\")],\n    background=True,\n    manager=cache_manager,\n)\ndef compute(n_clicks):\n    # Job terminates if cancel-btn clicked\n    return result\n```\n\nManagers call `terminate_job()` which kills the process (Diskcache) or revokes the task (Celery).\n\n### Result Caching\n\nResults can be cached to avoid recomputation:\n\n```python\ndef get_user_id():\n    return flask.session.get(\"user_id\")\n\ncache_manager = DiskcacheManager(\n    cache_by=[get_user_id],  # cache key includes user ID\n    expire=3600,             # TTL in seconds\n)\n```\n\n- `cache_by` - List of functions whose return values are included in cache key\n- `expire` - Time-to-live for cached results\n- `cache_args_to_ignore` - Argument indices to exclude from cache key\n\n### How It Works\n\n1. **Initial request**: Frontend triggers callback, backend returns `cacheKey` and `job` ID\n2. **Polling**: Frontend polls `/_dash-update-component?cacheKey=...&job=...` at configured interval\n3. **Progress**: Each poll returns current progress value if set\n4. **Completion**: When job finishes, poll returns final result\n5. **Cleanup**: Results cleared from cache (unless `cache_by` specified)\n\nCache key is SHA256 hash of: callback key + function source + arguments + triggered inputs + cache_by values. Without `cache_by` a random nonce is added, so every run gets its own key: the job runs with the requester's context, so two users with equal inputs must never share a result. Diskcache writes those uncached entries with a one day expiry (`UNCACHED_RESULT_EXPIRE`) so results nobody polls for do not pile up; Celery's result backend already expires them.\n\n### Key Files\n\n- `dash/_callback.py:188-219` - Background spec construction\n- `dash/background_callback/managers/__init__.py` - `BaseBackgroundCallbackManager` abstract class\n- `dash/background_callback/managers/diskcache_manager.py` - Diskcache implementation\n- `dash/background_callback/managers/celery_manager.py` - Celery implementation\n- `dash/dash-renderer/src/actions/callbacks.ts:458-685` - Frontend polling logic\n\n## Jupyter Integration\n\nDash apps can run directly in Jupyter notebooks and JupyterLab. The integration is handled by `dash/_jupyter.py`.\n\n### Display Modes\n\n```python\napp.run(\n    jupyter_mode=\"inline\",      # Display in notebook cell (default)\n    jupyter_width=\"100%\",       # IFrame width\n    jupyter_height=650,         # IFrame height in pixels\n)\n```\n\n| Mode | Behavior |\n|------|----------|\n| `\"inline\"` | App displays in notebook cell via IFrame |\n| `\"external\"` | Prints URL, user opens in browser tab |\n| `\"jupyterlab\"` | Opens in dedicated JupyterLab tab |\n| `\"tab\"` | Auto-opens URL in new browser tab |\n\n### How It Works\n\n1. `app.run()` detects Jupyter environment via `get_ipython()`\n2. Server starts in background daemon thread\n3. Jupyter comm protocol negotiates proxy configuration\n4. App displays according to selected mode\n\n```\napp.run() in notebook\n    ↓\nDetect Jupyter → Start server in background thread\n    ↓\nComm request → Extension responds with base_url\n    ↓\nCompute dashboard URL with proxy path\n    ↓\nDisplay: IFrame (inline) / URL (external) / Tab (jupyterlab)\n```\n\n### Notebook Extension\n\nClassic Jupyter notebooks use `dash/nbextension/`:\n\n- `main.js` - Registers \"dash\" comm target\n- `dash.json` - Extension loader configuration\n\nThe extension handles comm messages:\n- `base_url_request` → responds with server URL and base path\n- Enables proper proxy routing in JupyterHub environments\n\n### JupyterLab Extension\n\nJupyterLab uses `@plotly/dash-jupyterlab/`:\n\n- `src/index.ts` - TypeScript plugin implementing `JupyterFrontEndPlugin`\n- `DashIFrameWidget` - Lumino widget for rendering apps in tabs\n\nHandles messages:\n- `base_url_request` → responds with JupyterLab server config\n- `show` → creates dedicated tab with IFrame widget\n\nCompatible with JupyterLab 2.x, 3.x, and 4.x.\n\n### Proxy Configuration\n\nIn JupyterHub/proxy environments, the extension negotiates `requests_pathname_prefix`:\n\n```python\n# Computed from Jupyter base path\nrequests_pathname_prefix = \"/user/username/proxy/8050/\"\n```\n\nThis ensures callbacks route correctly through the Jupyter proxy.\n\n### Google Colab\n\nSpecial handling for Colab:\n- Uses `google.colab.output.serve_kernel_port_as_iframe()` for inline\n- Uses `google.colab.output.serve_kernel_port_as_window()` for external\n- Only supports \"inline\" and \"external\" modes\n\n### Key Files\n\n- `dash/_jupyter.py` - `JupyterDash` class, comm handling, server thread\n- `dash/nbextension/main.js` - Classic notebook extension\n- `@plotly/dash-jupyterlab/src/index.ts` - JupyterLab extension\n\n## Configuration Reference\n\n### Dash() Constructor Parameters\n\n**Basic Setup:**\n- `name` - Application name (default: infers from `__name__`)\n- `server` - Server instance (Flask, Quart, or FastAPI) or `True` to create Flask (default: `True`)\n- `title` - Browser tab title (default: `\"Dash\"`)\n- `update_title` - Title during callbacks (default: `\"Updating...\"`)\n\n**Assets & Resources:**\n- `assets_folder` - Path to assets directory (default: `\"assets\"`)\n- `assets_url_path` - URL path for assets (default: `\"assets\"`)\n- `assets_ignore` - Regex to exclude assets (default: `\"\"`)\n- `serve_locally` - Serve from local vs CDN (default: `True`)\n- `external_scripts` - Additional JS URLs\n- `external_stylesheets` - Additional CSS URLs\n\n**Routing:**\n- `url_base_pathname` - Base URL prefix for entire app\n- `requests_pathname_prefix` - Prefix for AJAX requests\n- `routes_pathname_prefix` - Prefix for API routes\n\n**Multi-Page:**\n- `use_pages` - Enable pages system (default: auto-detect)\n- `pages_folder` - Path to pages directory (default: `\"pages\"`)\n\n**Behavior:**\n- `suppress_callback_exceptions` - Skip callback validation (default: `False`)\n- `prevent_initial_callbacks` - Skip callbacks on load (default: `False`)\n- `background_callback_manager` - DiskcacheManager or CeleryManager\n- `on_error` - Global callback error handler\n- `shared_storage` - Cross-process state + pub/sub backend (default: `LocalSharedStorage`). Pass `None` to disable, or a `BaseSharedStorage` subclass/instance to swap. See [Shared Storage](#shared-storage).\n\n**WebSocket Callbacks:**\n- `websocket_callbacks` - Enable WebSocket for all callbacks (default: `False`). Requires FastAPI backend.\n- `websocket_allowed_origins` - List of allowed origins for WebSocket connections\n- `websocket_inactivity_timeout` - Disconnect WebSocket after inactivity period in ms (default: `300000` = 5 minutes). Set to `0` to disable.\n\n### app.run() Parameters\n\n- `host` - Server IP (default: `\"127.0.0.1\"`, env: `HOST`)\n- `port` - Server port (default: `8050`, env: `PORT`)\n- `debug` - Enable dev tools (default: `False`, env: `DASH_DEBUG`)\n- `jupyter_mode` - Display mode: `\"inline\"`, `\"external\"`, `\"tab\"`\n\n### Environment Variables\n\n| Variable | Purpose |\n|----------|---------|\n| `DASH_DEBUG` | Enable debug mode |\n| `DASH_URL_BASE_PATHNAME` | Base URL prefix |\n| `DASH_SUPPRESS_CALLBACK_EXCEPTIONS` | Skip validation |\n| `DASH_HOT_RELOAD` | Enable hot reload |\n| `DASH_PROPS_CHECK` | Validate prop types |\n| `DASH_PRUNE_ERRORS` | Simplify tracebacks |\n| `HOST` | Server host |\n| `PORT` | Server port |\n| `DASH_SECRET_KEY` | Signing secret for page, background and stream tokens when `server.secret_key` is unset |\n| `DASH_SHARED_STORAGE` | Shared-storage backend when `shared_storage=` is not passed (see Shared Storage) |\n\n## Stores and Client-Side State\n\n### dcc.Store\n\nStore data client-side with configurable persistence:\n\n```python\ndcc.Store(id='my-store', storage_type='local', data={'key': 'value'})\n```\n\n| Storage Type | Persists | Scope | Use Case |\n|--------------|----------|-------|----------|\n| `'memory'` | Page view only | Tab | Temporary state, debugging |\n| `'session'` | Browser session | Tab | Form state, filters |\n| `'local'` | Forever | All tabs | User preferences, settings |\n\n**Usage pattern:**\n```python\n@app.callback(Output('output', 'children'), Input('store', 'data'))\ndef use_store(data):\n    return data['key']\n\n@app.callback(Output('store', 'data'), Input('input', 'value'))\ndef update_store(value):\n    return {'key': value}\n```\n\n### Component Persistence\n\nAutomatically persist user edits to component props:\n\n```python\ndcc.Dropdown(\n    id='dropdown',\n    options=[...],\n    persistence=True,           # Enable persistence\n    persistence_type='local',   # local, session, or memory\n    persisted_props=['value'],  # Props to persist (default varies by component)\n)\n```\n\n- **`persistence`** - `True` or unique key to enable\n- **`persistence_type`** - Storage backend (default: `'local'`)\n- **`persisted_props`** - List of prop names to persist\n\nSupported components: Input, Dropdown, Checklist, RadioItems, Slider, RangeSlider, DatePickerSingle, DatePickerRange, Textarea, Tabs, DataTable.\n\n### When to Use Each\n\n| Need | Solution |\n|------|----------|\n| Server-controlled state | `dcc.Store` with callbacks |\n| Remember user selections | Component `persistence=True` |\n| Share state across tabs | `dcc.Store` with `storage_type='local'` |\n| Session-only state | `persistence_type='session'` |\n\n## Shared Storage\n\nBackend-agnostic **server-side** state shared across worker processes: a\ncross-process key/value store plus an ordered, replayable publish/subscribe\nchannel. Unlike `dcc.Store` (which lives in the browser), shared storage lives\non the server and lets callbacks running in different workers see the same keys\nand topics without standing up an external service like Redis.\n\nEnabled by default on every app. It starts lazily on first use, so it costs\nnothing until touched.\n\n### Accessing It\n\n```python\nimport dash\nfrom dash import Input, Output, callback\n\n@callback(Output(\"out\", \"children\"), Input(\"btn\", \"n_clicks\"))\ndef handler(n):\n    store = dash.ctx.shared_storage      # inside a callback\n    store.set(\"clicks\", n)\n    return store.get(\"clicks\", 0)\n\n# Or off the app directly:\napp = dash.Dash()\napp.shared_storage.set(\"key\", {\"any\": \"json-compatible value\"})\n```\n\nValues (and published messages) must be **JSON-compatible** — dict / list / str\n/ int / float / bool / None — the same constraint as `dcc.Store` and callback\noutputs. Messages are encoded with `msgspec` (msgpack), a hard dependency.\n\n### API\n\n`app.shared_storage` (and `dash.ctx.shared_storage`) is a `BaseSharedStorage`:\n\n| Method | Purpose |\n|--------|---------|\n| `get(key, default=None)` | Read a value |\n| `set(key, value, ttl=None)` | Write a value; `ttl` = optional lifetime in seconds |\n| `delete(key)` | Remove a key |\n| `publish(topic, message, ttl=None)` | Append a message to a topic; `ttl` = release the topic after that long idle |\n| `subscribe(topic, replay_from=None)` | Return a `Subscription` |\n\n**Key expiry (TTL).** `set(key, value, ttl=<seconds>)` gives a key a bounded\nlifetime; once it elapses, reads return the `default` again. `ttl=None` (the\ndefault) never expires. Expiry is lazy/best-effort — an expired key is dropped\non its next read, not at a guaranteed instant. Each backend uses its native\nmechanism: a monotonic deadline on the in-memory owner, diskcache `expire`,\nRedis `PX`. Use it for session-scoped or cache-like state you don't want to\naccumulate unboundedly.\n\n**Ordered pub/sub.** Each `publish` to a topic gets a monotonically increasing\nsequence number (per topic, starting at 1; `0` means \"before the first\nmessage\"). A subscriber receives every message published after it subscribed,\nin order:\n\n```python\n# Producer (one worker):\nstore.publish(\"progress\", {\"pct\": 50})\n\n# Consumer (another worker/callback):\nwith store.subscribe(\"progress\") as sub:\n    for message in sub:          # or: async for message in sub\n        print(message[\"pct\"])\n```\n\n**Replay on reconnect.** Pass `replay_from=<last-seen-seq>` to resume after a\ndrop; buffered messages since that cursor replay first, so a reconnecting\nconsumer doesn't miss messages. If the consumer fell farther behind than the\nbounded buffer holds, the subscription raises `SharedStorageGap` (an explicit\ngap rather than a silent hole). A `Subscription` is iterable synchronously\n(`for`) or asynchronously (`async for`), is a context manager, and has\n`close()`.\n\n### Backends\n\nThree backends ship, differing only in **how far state reaches**. Pick by the\ndeployment topology — critically, whether the app runs behind a load balancer as\nmore than one container/pod (see [Deployment Topology](#deployment-topology-and-shared-storage) below).\n\n| Backend | Backing store | Reaches | Extra |\n|---------|---------------|---------|-------|\n| `LocalSharedStorage` *(default)* | in-memory, owner-elected socket | one **container** (all its workers) | none |\n| `DiskcacheSharedStorage` | a `diskcache.Cache` | one **host** (all processes sharing the dir) | `dash[diskcache]` |\n| `RedisSharedStorage` | Redis (Streams for pub/sub) | **any** process/container/pod | `dash[redis]` |\n\nAll three implement the same `BaseSharedStorage` contract (KV + ordered,\nreplayable pub/sub with `SharedStorageGap` on buffer overrun), so app code is\nidentical across them — only the constructor differs.\n\n```python\nfrom dash import Dash\nfrom dash._shared_storage import (\n    LocalSharedStorage, DiskcacheSharedStorage, RedisSharedStorage,\n)\n\nDash(shared_storage=LocalSharedStorage)                       # default\nDash(shared_storage=DiskcacheSharedStorage(directory=\"/tmp/ss\"))\nDash(shared_storage=RedisSharedStorage(url=\"redis://host:6379\"))\nDash(shared_storage=None)                                     # disabled\n```\n\nWhen disabled, accessing `app.shared_storage` / `dash.ctx.shared_storage`\nraises `SharedStorageError`.\n\n**`LocalSharedStorage`** — in-memory, no external service. On first use every\nworker races to become the single **owner** for the machine by binding a stable\naddress (`AF_UNIX` on POSIX, `127.0.0.1` loopback on Windows); the winner hosts\nthe engine, losers proxy over the socket. The bind *is* the lease — when the\nowner dies the address frees and a survivor re-elects. A single-process\ndeployment is its own owner and pays no socket overhead. Knobs: `namespace`\n(defaults to a hash of cwd + argv[0]), `buffer_size` (default 32 — small on\npurpose, since each topic retains that many arbitrary payloads; raise it for a\nwider reconnect window).\n\n**Topic lifetime.** `publish(topic, message, ttl=None)` takes an optional ttl\n(seconds, at least `MIN_TOPIC_TTL` = 1s, so a reconnecting reader is not\noutrun). A topic with a ttl is released, buffer and sequence both, once nobody\nhas published to or read from it for that long; a later publish starts it over\nat 1, so a consumer returning with an old cursor gets `SharedStorageGap`.\nWithout a ttl a topic lives as long as the store. The latest publish's ttl\nwins. Streaming publishes with `STREAM_TOPIC_TTL` (300s); user topics default\nto no ttl. Every backend expires a topic as a whole, never message by message\nwhile it is read (`tests/shared_storage/test_topic_ttl.py` runs the same cases\non all three). Local: the engine records the ttl on the topic and sweeps idle\nones (no publish, head or poll, and no call holding it) at most once a second,\non the next pub/sub call. The sweep also drops empty, unheld topics, such as\nthe one a returning reader's poll recreates after a release. Redis: the ttl sits in a third key; the publish\nscript `PEXPIRE`s all three to 1.25 ttl, and a poll script renews them. A poll\non such a topic blocks in `XREAD` for at most a third of that lifetime, not the\nusual 5s, so a waiting reader renews it in time. Diskcache: the ttl\nsits in a key; on publish or poll, once less than one ttl is left, every key of\nthe topic (counter, ttl, buffered messages) is renewed to 1.25 ttl, and at\nonce when a publish changes the ttl. A poll on such a topic waits at most half\na ttl before it renews again.\n\n*Durability* is controlled by `mode` (the key/value store only — pub/sub is\nalways transient):\n\n| `mode` | Writes | On owner death / restart |\n|--------|--------|--------------------------|\n| `\"memory\"` *(default)* | none | re-elects **cold** (state lost) |\n| `\"persist\"` | write-through on every set/delete | re-elected/fresh owner **recovers** from disk |\n| `\"persist-reset\"` | flushed every `flush_interval`s (default 60) + on clean exit | recovers to the last snapshot (up to one interval may be lost on an unclean crash) |\n\nOnly the owner persists. The on-disk store is *chunked* (keys sharded across\nmsgpack chunk files via a key→chunk index, so a mutation rewrites only the\naffected chunk — adapted from raposa's `BinaryStorage`); each chunk write is\natomic (temp file + `os.replace`). TTLs survive restarts (stored as wall-clock\ndeadlines; expired keys are dropped on load). `path` sets the store directory\n(default: a per-namespace folder under the user cache dir). Recovery runs on\nevery election, so persisted data also survives owner re-election:\n\n```python\nDash(shared_storage=LocalSharedStorage(mode=\"persist\"))\nDash(shared_storage=LocalSharedStorage(mode=\"persist-reset\", flush_interval=30))\n```\n\n**`DiskcacheSharedStorage`** — KV + an append-log pub/sub on a `diskcache.Cache`,\nthe same store `DiskcacheManager` uses; pass an existing `cache` to share one.\nEvery process on one host that opens the same directory shares state, so it\ngives multi-worker parity with the local backend without a socket. **Not for\npods** — each pod has its own ephemeral disk.\n\n**`RedisSharedStorage`** — KV on Redis strings, pub/sub on a Redis Stream per\ntopic (an atomic `INCR`+`XADD` script keeps sequences ordered; `MAXLEN` bounds\nthe replay window; a subscriber past the trimmed floor gets `SharedStorageGap`).\nRedis is the single source of truth, so no owner election. Pass a `url`\n(defaults to `$REDIS_URL`) or an existing `client` to reuse a connection pool.\nThis is the only backend correct for **horizontally-scaled, multi-pod**\ndeployments. On an event loop it uses `redis.asyncio` (one client per loop),\nand one reader task per loop serves every async subscription with a single\nmulti-stream `XREAD`; a new subscription interrupts it through a private wake\nstream. Never park a blocking read in the executor per subscription: at a dozen\nopen downlinks that starved every other store call in the worker (25-50% failed\nstreams at 25 browsers on uvicorn `--workers 4`). Diskcache's async\nsubscriptions likewise poll without blocking and sleep on the loop.\n\n### Deployment Topology and Shared Storage\n\nThe default `LocalSharedStorage` is **per-container**: its socket/loopback\nelection only reaches processes in the same network + filesystem namespace.\n\n- **Single process / single pod** (e.g. Plotly Cloud apps): fine — one owner,\n  nothing to fragment.\n- **Multiple gunicorn workers in one container**: fine — they share the pod's\n  loopback/socket and elect one owner.\n- **Multiple pods behind a load balancer** (e.g. Dash Enterprise apps scaled by\n  an HPA, routed round-robin with no session affinity): **each pod elects its own\n  isolated owner**, so `set()`/`publish()` on one pod are invisible on another.\n  This works at 1 pod and fragments silently once it scales — use\n  `RedisSharedStorage` (one Redis shared by all pods) instead.\n\nA hosting platform can switch the backend without editing the app through\n`DASH_SHARED_STORAGE` (`_shared_storage/_env.py`), read only when the app did\nnot pass `shared_storage=` (the default is a sentinel, so an explicit argument,\n`None` included, always wins): `local`, `none`, `diskcache:///abs/path`, or a\n`redis://` / `rediss://` URL. A `key_prefix` query parameter on the Redis URL\n(`redis://host/0?key_prefix=myapp`) sets `RedisSharedStorage(key_prefix=)` and\nis stripped before the URL reaches redis-py; the diskcache path is\npercent-decoded. `cluster://` is reserved and raises; anything else raises\n`InvalidConfig` at construction, without echoing the value (it can hold\ncredentials). The value becomes a zero-argument\nfactory, so nothing is built or connected until `app.shared_storage` is first\nread, but a missing extra (`dash[redis]`, `dash[diskcache]`) fails at\nconstruction with the backend's own `ImportError`.\n\n### Custom and Out-of-Tree Backends\n\n`BaseSharedStorage` is the stable, public extension point. A backend — shipped\nin-tree or as a **separate package** — implements:\n\n- `get(key, default)` / `set(key, value)` / `delete(key)` — JSON-compatible values\n- `publish(topic, message, ttl=None)` and `subscribe(topic, replay_from=None) -> Subscription`\n- topic expiry: with a `ttl`, release the topic as a whole once nobody has\n  published to or read from it for that long, never message by message while\n  it is read; reject a ttl under `MIN_TOPIC_TTL` (run `test_topic_ttl.py`\n  against a new backend)\n- optional `start()` / `close()` (idempotent, called once per worker)\n\nand returns a `Subscription` (`__iter__` / `__aiter__` / `close`) that raises\n`SharedStorageGap` when the replay buffer overran. That trio —\n`BaseSharedStorage`, `Subscription`, `SharedStorageGap` — is exported from\ntop-level `dash`; the poll-loop helpers (`PollResult`, the polling subscription)\nare private and not part of the contract.\n\nCore only ships backends whose dependency is already a Dash extra (`msgspec`\nbase; `dash[diskcache]`; `dash[redis]`). Anything needing a heavier dependency\nbelongs **out of tree** behind this same interface — e.g. a Postgres backend\n(`LISTEN`/`NOTIFY` for push pub/sub + a table for KV and replay) lives in its own\npackage so a `psycopg` connection is never pulled into Dash core. It plugs in the\nsame way as a built-in: `Dash(shared_storage=PostgresSharedStorage(...))`.\n\n### Module Layout\n\n| File | Responsibility |\n|------|----------------|\n| `_shared_storage/base.py` | Abstract interface: `BaseSharedStorage`, `Subscription`, `SharedStorageError`, `SharedStorageGap` |\n| `_shared_storage/_engine.py` | `StoreEngine`: authoritative in-memory kv map + per-topic ordered log with bounded replay buffer (thread-safe, transport-agnostic) |\n| `_shared_storage/local.py` | `LocalSharedStorage`: owner election; owner hosts the engine, clients proxy |\n| `_shared_storage/diskcache.py` | `DiskcacheSharedStorage`: KV + append-log pub/sub on a `diskcache.Cache` |\n| `_shared_storage/redis.py` | `RedisSharedStorage`: KV + Redis Streams pub/sub (atomic `INCR`+`XADD`) |\n| `_shared_storage/_polling.py` | `PollingSubscription`: shared poll-loop subscription for the diskcache/Redis backends |\n| `_shared_storage/_transport.py` | Length-prefixed, token-gated socket transport (local backend) |\n| `_shared_storage/_codec.py` | msgspec msgpack codec (data-only) |\n| `_shared_storage/_env.py` | `DASH_SHARED_STORAGE` parsing |\n| `dash.py` | `shared_storage` constructor arg + lazy `app.shared_storage` property |\n| `_callback_context.py` | `dash.ctx.shared_storage` accessor |\n\nRequires `msgspec` (in `requirements/install.txt`); `DiskcacheSharedStorage`\nneeds the `diskcache` extra and `RedisSharedStorage` the `redis` extra.\n\n## Async Callbacks\n\nDash supports `async def` callbacks for non-blocking execution.\n\n### Setup\n\n**With Flask backend:**\n```bash\npip install dash[async]\n```\n\nAsync is auto-enabled when `asgiref` is detected. Or explicitly:\n\n```python\napp = Dash(__name__, use_async=True)\n```\n\n**With Quart or FastAPI backend:** Async is native - no extra dependencies needed.\n\n```python\nfrom fastapi import FastAPI\nfrom dash import Dash\n\nserver = FastAPI()\napp = Dash(__name__, server=server)  # Async works automatically\n```\n\n### Usage\n\n```python\nimport asyncio\n\n@app.callback(Output('output', 'children'), Input('input', 'value'))\nasync def async_update(value):\n    await asyncio.sleep(1)  # Non-blocking\n    return f\"Processed: {value}\"\n```\n\n### Key Points\n\n- Regular async callbacks are **non-blocking** - multiple can run concurrently\n- Background callbacks also support `async def`\n- Jupyter uses `nest_asyncio` for event loop compatibility\n- With Flask backend: requires `dash[async]`, coroutines raise error without it\n- With Quart/FastAPI backends: async is native, no extra setup needed\n\n### Async with Background Callbacks\n\n```python\n@app.callback(\n    Output('result', 'children'),\n    Input('btn', 'n_clicks'),\n    background=True,\n    manager=diskcache_manager,\n)\nasync def async_background(n_clicks):\n    await asyncio.sleep(5)\n    return \"Done\"\n```\n\nBoth DiskcacheManager and CeleryManager support async functions via `asyncio.run()`.\n\n## WebSocket Callbacks\n\nWebSocket callbacks use a persistent WebSocket connection instead of HTTP POST for callback execution. This reduces latency and connection overhead for applications with frequent callbacks.\n\n### Requirements\n\n- **FastAPI backend required**: WebSocket callbacks only work with FastAPI\n- **SharedWorker support**: Modern browsers (not IE)\n\n### Usage\n\n**Enable globally for all callbacks:**\n```python\nfrom fastapi import FastAPI\nfrom dash import Dash\n\nserver = FastAPI()\napp = Dash(__name__, server=server, websocket_callbacks=True)\n```\n\n**Enable per-callback:**\n```python\n@app.callback(\n    Output('output', 'children'),\n    Input('input', 'value'),\n    websocket=True  # Use WebSocket for this callback only\n)\ndef update(value):\n    return f\"Value: {value}\"\n```\n\n### Configuration\n\n```python\napp = Dash(\n    __name__,\n    server=server,\n    websocket_callbacks=True,\n    websocket_inactivity_timeout=300000,  # 5 minutes (default)\n    websocket_heartbeat_interval=30000,   # 30 seconds (default)\n    websocket_allowed_origins=['https://example.com'],\n)\n```\n\n- **`websocket_callbacks`** - Enable WebSocket for all callbacks (default: `False`)\n- **`websocket_inactivity_timeout`** - Close WebSocket after period of inactivity in milliseconds (default: `300000` = 5 minutes). Heartbeats do not count as activity. Set to `0` to disable timeout. Connection automatically reconnects when needed.\n- **`websocket_heartbeat_interval`** - Interval for heartbeat/keep-alive checks in milliseconds (default: `30000` = 30 seconds). Also determines how frequently inactivity timeout is checked.\n- **`websocket_allowed_origins`** - List of allowed origins for WebSocket connections (security)\n\n### Architecture\n\n```\n┌─────────────────────────────────────────────────────────────────────────┐\n│ Browser Tab 1                          Browser Tab 2                    │\n│ ┌─────────────┐                       ┌─────────────┐                   │\n│ │  Renderer   │                       │  Renderer   │                   │\n│ └──────┬──────┘                       └──────┬──────┘                   │\n│        │ postMessage                         │ postMessage              │\n│        └────────────┬───────────────────────┘                           │\n│                     ▼                                                   │\n│         ┌─────────────────────┐                                         │\n│         │    SharedWorker     │  (one per origin)                       │\n│         │   dash-ws-worker    │                                         │\n│         └──────────┬──────────┘                                         │\n└────────────────────│────────────────────────────────────────────────────┘\n                     │ WebSocket\n                     ▼\n┌─────────────────────────────────────────────────────────────────────────┐\n│ Server (FastAPI)                                                        │\n│   WebSocket Endpoint: /_dash-ws-callback                                │\n└─────────────────────────────────────────────────────────────────────────┘\n```\n\n**Connection & Reconnection Flow:**\n```\nRenderer                   SharedWorker                 Server\n    │                              │                        │\n    │──[CONNECT]──────────────────>│                        │\n    │                              │──[WebSocket Connect]──>│\n    │<─[CONNECTED]─────────────────│<─[Connected]───────────│\n    │                              │                        │\n    │──[CALLBACK_REQUEST]─────────>│──[callback request]───>│\n    │<─[CALLBACK_RESPONSE]─────────│<─[callback response]───│\n    │                              │                        │\n    │      (inactivity)            │    (heartbeat check)   │\n    │                              │──[close 4001]─────────>│\n    │<─[DISCONNECTED]──────────────│                        │\n    │                              │                        │\n    │──[CALLBACK_REQUEST]─────────>│──[reconnect + send]───>│\n    │<─[CALLBACK_RESPONSE]─────────│<─[response]────────────│\n```\n\n- **SharedWorker**: Single WebSocket connection shared across browser tabs\n- **Heartbeat**: Periodic ping/pong to detect dead connections (30s interval)\n- **Inactivity timeout**: Closes connection after no actual callback activity (not heartbeats)\n- **Auto-reconnect**: Reconnects automatically when a callback is triggered after timeout\n\n### Long-Running Callbacks with set_props/get_props\n\nWebSocket callbacks can stream updates to the client during execution using `set_props()` and read current component values using `ctx.websocket`:\n\n```python\nimport asyncio\nfrom dash import callback, Output, Input, set_props, ctx\nfrom dash.exceptions import PreventUpdate\n\n@callback(\n    Output('result', 'children'),\n    Input('start-btn', 'n_clicks'),\n    prevent_initial_call=True\n)\nasync def long_running_task(n_clicks):\n    ws = ctx.websocket\n    if not ws:\n        return \"WebSocket not available\"\n\n    # Stream progress updates to the client\n    for i in range(100):\n        # IMPORTANT: Check is_shutdown in loops to detect disconnections\n        if ws.is_shutdown:\n            raise PreventUpdate  # Exit gracefully on disconnect\n        await asyncio.sleep(0.1)\n        set_props('progress-bar', {'value': i + 1})\n        set_props('status', {'children': f'Processing step {i + 1}/100...'})\n\n    # Read current value from another component\n    current_value = await ws.get_prop('input-field', 'value')\n\n    return f\"Completed! Input was: {current_value}\"\n```\n\n**IMPORTANT - Checking `is_shutdown` in Loops:**\n\nLong-running callbacks that use loops **must** check `ws.is_shutdown` to detect when the WebSocket connection has closed. Without this check:\n- Callbacks continue running after the client disconnects, wasting server resources\n- `set_props` calls go to a closed connection and are lost\n- The callback result is never delivered to the client\n\nOnly \"persistent callbacks\" (callbacks with no Output and no Input that use only `set_props`) are automatically restarted when the WebSocket reconnects. Regular callbacks with outputs are not restarted.\n\n**API:**\n- `set_props(component_id, props_dict)` - Stream prop updates immediately to client\n- `ctx.websocket` - Get WebSocket interface (returns `None` if not in WS context)\n- `ws.is_shutdown` - Check if the WebSocket connection has been closed\n- `await ws.get_prop(component_id, prop_name, timeout=30.0, *, path=None)` - Read a full or nested prop value\n- `await ws.set_prop(component_id, prop_name, value)` - Set single prop (async version)\n- `await ws.close(code, reason)` - Close the WebSocket connection\n\n### Partial Reads with get_prop\n\nUse the keyword-only `path` argument with the same string keys and integer list\nindices supported by `Patch`:\n\n```python\nevent = await ws.get_prop(\n    'store', 'data', path=[","isInternal":false,"tokens":11896,"sizeBytes":51424},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/plotly/dash/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# CLAUDE.md\n\nGuidance for Claude Code (claude.ai/code) working in this repo.\n\n## Project Overview\n\nDash is a Python framework for building reactive web-based data visualization applications. Built on Plotly.js, React, and Flask.\n\n## Docs\n\nDeeper reference lives in `.ai/`. Open the one that covers what you're touching, don't read them all up front:\n\n- `COMMANDS.md` - build, test, lint commands\n- `ARCHITECTURE.md` - backend, callbacks, pages, background callbacks, stores, async, security\n- `RENDERER.md` - frontend, crawlLayout, Redux store, clientside API\n- `COMPONENTS.md` - component system, generation, resources\n- `TESTING.md` - test framework, fixtures, patterns\n- `TROUBLESHOOTING.md` - common errors and fixes\n- `PERFORMANCE.md` - benchmark harness, profiling, and performance findings\n\nTo find code, grep. Don't rely on a hand-kept file map, it goes stale.\n\n## Writing Style\n\nApplies to code comments, commit messages, PR descriptions, review comments, and CHANGELOG entries.\n\n- **Comments are a last resort.** The code is Python and should read on its own. Write a comment only when the code cannot explain itself: a non-obvious why, a workaround, a gotcha. Never narrate what the next line already says. If in doubt, leave it out.\n- **No em dashes or en dashes (`—` `–`).** Use a colon, comma, parentheses, or two sentences.\n- **Straight ASCII only.** Straight quotes and apostrophes (`'` `\"`), no curly ones. No ellipsis character (`…`); write three periods if you really need them. No decorative unicode.\n- **Plain words.** Say the thing directly, in words most people know. No AI filler, no thesaurus vocabulary, no heavy styling. Short and to the point.\n\n## Quality Gate\n\nBefore you say a change works:\n\n1. **Prove it in a running app.** Write a small repro app in the scratchpad and drive it headless with `dash_duo` (a throwaway pytest test is the practical way, see `.ai/TESTING.md`). Watch the real behavior. \"The code looks right\" is not done.\n2. **Leave a test behind.** Any behavior change or fix needs a test that stays in the tree: an integration/acceptance test under `tests/integration/`, or a renderer test for frontend-only work. It should fail before your change and pass after. Never run the whole integration suite at once, always a specific file or `-k`.\n3. **Second-model review.** Before handing back, launch a review subagent on a different model (the Agent tool takes a `model` override, e.g. Sonnet when you're on Opus) to check the diff for correctness and cases you missed. Fold in what holds up. A fresh model catches what the author misses.\n","isInternal":false,"tokens":634,"sizeBytes":2591},{"name":"copilot-instructions.md","path":".github/copilot-instructions.md","rawUrl":"https://raw.githubusercontent.com/plotly/dash/HEAD/.github/copilot-instructions.md","title":"GitHub Copilot Instructions","category":"copilot-instructions","format":"markdown","content":"# Copilot Instructions\n\nThis file provides guidance to GitHub Copilot when working with this repository.\n\nFor detailed documentation, see the [`.ai/`](../.ai/) directory:\n- [Commands](../.ai/COMMANDS.md) - Build, test, lint\n- [Architecture](../.ai/ARCHITECTURE.md) - Backend, layout, callbacks\n- [Components](../.ai/COMPONENTS.md) - Component system, generation, resources\n\n## Project Overview\n\nDash is a Python framework for building reactive web-based data visualization applications. Built on Plotly.js, React, and Flask.\n\n## Quick Reference\n\n```bash\n# Setup\npip install -e .[ci,dev,testing,celery,diskcache] && npm ci\n\n# Build\nnpm run build                                    # Linux/Mac\nnpm run first-build                              # Windows (use Bash)\ndash-update-components \"dash-core-components\"    # Single component\n\n# Test\npytest tests/unit                                # Unit tests\npytest tests/integration                         # Integration tests\npytest -k test_name                              # Specific test\n\n# Lint\nnpm run lint                                     # All linters\nnpm run private::format.black                    # Auto-format Python\n```\n\n## Key Files\n\n- `dash/dash.py` - Main Dash app class, layout, callbacks, routing\n- `dash/_callback.py` - `@callback` decorator and execution\n- `dash/dependencies.py` - `Input`, `Output`, `State`, wildcards\n- `dash/development/base_component.py` - Component base class, `to_plotly_json()`\n- `dash/dash-renderer/` - TypeScript/React frontend\n- `components/` - Component packages (dcc, html, table)\n","isInternal":false,"tokens":361,"sizeBytes":1576},{"name":"README.md","path":"components/dash-core-components/packages/README.md","rawUrl":"https://raw.githubusercontent.com/plotly/dash/HEAD/components/dash-core-components/packages/README.md","title":"packages Documentation","category":"plugin-manifest","format":"markdown","content":"Folder to place temporary modules e.g. plotly.js-dist-min in .tgz format\n","isInternal":false,"tokens":19,"sizeBytes":73}],"systemPromptSnippet":"<yakaai_skills repo=\"plotly/dash\">\n<!-- File: AGENTS.md (Tokens: ~152 | Category: root-instruction) -->\n# AGENTS.md\n\nFor guidance on working with this repository, see the [`.ai/`](.ai/) directory:\n\n- [COMMANDS.md](.ai/COMMANDS.md) - Build, test, lint\n- [ARCHITECTURE.md](.ai/ARCHITECTURE.md) - Backend, callbacks, layout\n- [RENDERER.md](.ai/RENDERER.md) - Frontend, React, Redux\n- [COMPONENTS.md](.ai/COMPONENTS.md) - Component system\n- [TESTING.md](.ai/TESTING.md) - Testing patterns\n- [TROUBLESHOOTING.md](.ai/TROUBLESHOOTING.md) - Common issues\n\n<!-- File: .ai/ARCHITECTURE.md (Tokens: ~11896 | Category: project-spec) -->\n# Architecture\n\n## Python Backend Framework\n\n- **`dash/dash.py`** - Main `Dash` application class (~2000 lines). Orchestrates the server backend, layout management, callback registration, routing, and asset serving. Key methods: `layout` property, `callback()`, `clientside_callback()`, `run()`.\n\n- **`dash/backends/`** - Server backend implementations. See [Server Backends](#server-backends) section for details.\n\n- **`dash/_callback.py`** - Callback registration and execution. Contains `callback()` decorator (usable as `@dash.callback` without app instance), `clientside_callback()`, and `register_callback()` which inserts callbacks into the callback map.\n\n- **`dash/dependencies.py`** - Dependency classes for callbacks:\n  - `Input` - Triggers callback when value changes\n  - `Output` - Component property to update (supports `allow_duplicate=True`)\n  - `State` - Read value without triggering callback\n  - `ClientsideFunction` - Reference to JS function for clientside callbacks\n  - Wildcards: `MATCH`, `ALL`, `ALLSMALLER` for pattern-matching IDs\n\n- **`dash/development/base_component.py`** - `Component` base class with `ComponentMeta` metaclass. All Dash components inherit from this. Components auto-register in `ComponentRegistry` and serialize to JSON via `to_plotly_json()`.\n\n- **`dash/_pages.py`** - Multi-page app support. `PAGE_REGISTRY` holds registered pages, `register_page()` decorator registers page modules with routes.\n\n## Layout System\n\nThe layout defines the UI as a tree of components:\n\n```python\napp.layout = html.Div([\n    dcc.Input(id='input', value='initial'),\n    html.Div(id='output')\n])\n```\n\n- **Static layout**: Assigned directly as a component tree\n- **Dynamic layout**: Assigned as a function that returns components (called on each page load, useful for per-session state)\n- Layout is serialized to JSON and sent to the React frontend via `/_dash-layout`\n- Components can contain other components via `children` prop\n- Component IDs can be strings or dicts (for pattern-matching callbacks)\n\n## Callback Types\n\n### 1. Regular Callbacks\n\n`@app.callback` or `@dash.callback`:\n\n```python\n@app.callback(Output('output', 'children'), Input('input', 'value'))\ndef update(value):\n    return f'You entered: {value}'\n```\n\nServer-side Python function called when inputs change. Outputs update component properties.\n\n### 2. Clientside Callbacks\n\n`app.clientside_callback`:\n\n```python\napp.clientside_callback(\n    \"\"\"function(value) { return 'You entered: ' + value; }\"\"\",\n    Output('output', 'children'),\n    Input('input', 'value')\n)\n```\n\nJavaScript function runs in browser. Faster for simple transformations, no server round-trip. Can reference `window.dash_clientside.namespace.function_name` or inline JS string.\n\n### 3. Background Callbacks\n\n`background=True`:\n\n```python\n@app.callback(Output('output', 'children'), Input('btn', 'n_clicks'),\n              background=True, manager=diskcache_manager,\n              running=[(Output('btn', 'disabled'), True, False)],\n              progress=[Output('progress', 'value')])\ndef compute(set_progress, n_clicks):\n    for i in range(10):\n        set_progress(i * 10)\n        time.sleep(1)\n    return 'Done'\n```\n\nCallbacks executed in separate process via Celery or Diskcache manager. Supports `progress` updates, `running` state changes, and `cancel` inputs. See [Background Callbacks](#background-callbacks) section for details.\n\n### 4. Pattern-Matching Callbacks\n\n```python\n@app.callback(\n    Output({'type': 'output', 'index': MATCH}, 'children'),\n    Input({'type': 'input', 'index': MATCH}, 'value')\n)\ndef update(value):\n    return value\n```\n\nUse dict IDs with wildcards (`MATCH`, `ALL`, `ALLSMALLER`) to target dynamically-generated components.\n\n## Server Routes\n\n- `/_dash-layout` - Returns initial component tree as JSON\n- `/_dash-dependencies` - Returns callback definitions\n- `/_dash-update-component` - Executes callbacks, returns updated props\n- `/_dash-component-suites/<package>/<path>` - Serves component JS/CSS assets\n- `/assets/<path>` - Serves static assets from app's assets folder\n\n## Server Backends\n\nDash supports multiple web server backends. The backend abstraction is in `dash/backends/`.\n\n### Available Backends\n\n| Backend | Type | Install | Use Case |\n|---------|------|---------|----------|\n| **Flask** (default) | WSGI (sync) | `pip install dash` | Standard deployments, simplicity |\n| **Quart** | ASGI (async) | `pip install dash[quart]` | Async callbacks, WebSocket support |\n| **FastAPI** | ASGI (async) | `pip install dash[fastapi]` | OpenAPI docs, async, modern Python |\n\n### Usage\n\n**Default (Flask):**\n```python\nfrom dash import Dash\napp = Dash(__name__)\n```\n\n**With existing server instance:**\n```python\nfrom flask import Flask\nfrom dash import Dash\n\nserver = Flask(__name__)\napp = Dash(__name__, server=server)\n```\n\n**Quart backend:**\n```python\nfrom quart import Quart\nfrom dash import Dash\n\nserver = Quart(__name__)\napp = Dash(__name__, server=server)\n```\n\n**FastAPI backend:**\n```python\nfrom fastapi import FastAPI\nfrom dash import Dash\n\nserver = FastAPI()\napp = Dash(__name__, server=server)\n\n# Run with: uvicorn module:app.server --reload\n```\n\n### Architecture\n\nThe backend system uses an abstract interface:\n\n- **`BaseDashServer`** (`dash/backends/base_server.py`) - Abstract base class defining the server interface. All backends implement this.\n\n- **`RequestAdapter`** - Normalizes HTTP request objects across frameworks. Provides unified access to `args`, `cookies`, `headers`, `get_json()`, etc.\n\n- **`ResponseAdapter`** - Normalizes response creation. Handles `set_cookie()`, `set_header()`, `set_response()`.\n\n- **`get_backend(name)`** - Factory function to get backend class by name (`\"flask\"`, `\"quart\"`, `\"fastapi\"`).\n\n- **`get_server_type(server)`** - Auto-detects backend from a server instance.\n\n### Backend Implementations\n\n**Flask** (`dash/backends/_flask.py`):\n- `FlaskDashServer` - Wraps Flask app\n- `FlaskRequestAdapter` - Uses `flask.request` proxy\n- `FlaskResponseAdapter` - Uses `flask.Response`\n- Compression via `flask-compress`\n\n**Quart** (`dash/backends/_quart.py`):\n- `QuartDashServer` - Wraps Quart app (async Flask API)\n- `QuartRequestAdapter` - Uses `quart.request` proxy\n- `QuartResponseAdapter` - Uses `quart.Response`\n- All route handlers are `async def`\n- Compression via `quart-compress`\n\n**FastAPI** (`dash/backends/_fastapi.py`):\n- `FastAPIDashServer` - Wraps FastAPI app\n- `FastAPIRequestAdapter` - Uses context variable for current request\n- `FastAPIResponseAdapter` - Uses Starlette responses\n- `DashMiddleware` - Consolidated ASGI middleware for request handling\n  - Runs `before_request` hooks on every HTTP route (user routes, assets, mounted apps) and the WebSocket handshake; a non-None hook result is sent as the response, like Flask. Only Dash's own routes get their JSON body pre-read, timing recorded and Dash's error handling. Quart registers the same hooks as `before_websocket` too.\n- Runs with uvicorn, supports hot reload\n- Built-in GZip compression\n\n### Key Interface Methods\n\nAll backends implement:\n\n```python\nclass BaseDashServer(ABC):\n    def create_app(name, config) -> server        # Create new server\n    def add_url_rule(rule, view_func, ...)        # Register routes\n    def before_request(func)                       # Request hooks\n    def after_request(func)                        # Response hooks\n    def run(dash_app, host, port, debug)          # Start dev server\n    def make_response(data, mimetype, status)     # Create response\n    def jsonify(obj)                              # JSON response\n    def setup_index(dash_app)                     # Register / route\n    def serve_callback(dash_app)                  # Callback endpoint\n    def setup_component_suites(dash_app)          # JS/CSS serving\n```\n\n### Accessing the Backend\n\n```python\napp = Dash(__name__)\n\n# Get the underlying server\napp.server          # Flask/Quart/FastAPI instance\n\n# Get the backend wrapper\napp.backend         # BaseDashServer subclass instance\napp.backend.server_type  # \"flask\", \"quart\", or \"fastapi\"\n\n# Access request in callbacks\nfrom dash import dash\ndash.get_app().backend.request_adapter()  # RequestAdapter instance\n```\n\n## Frontend (dash-renderer)\n\n**`dash/dash-renderer/src/`** contains the TypeScript/React frontend. See [RENDERER.md](RENDERER.md) for detailed documentation on:\n\n- Layout traversal (`crawlLayout`) and `children_props`\n- Component resolution from `window[namespace][type]`\n- Callback triggering via `setProps` and `notifyObservers`\n- Redux store structure (layout, paths, callbacks, graphs)\n- Observer system for callback processing\n- `window.dash_clientside` API\n- `window.dash_component_api` API\n\n### React Version\n\nDash supports multiple React versions. Configured in `dash/_dash_renderer.py`.\n\n**Available versions:** 18.3.1 (default), 18.2.0, 19.2.4 (experimental)\n\nReact 19 has no official UMD builds; Dash serves the `umd-react` package for it, plus a small shim (`dash-renderer/build/react-shim.min.js`, source `dash/dash-renderer/src/react-shim.js`) loaded right after react-dom and before any component package. The shim stubs the React <=18 secret internals (`ReactCurrentOwner`) some component libraries touch at load time, redirects the legacy `react.element` `$$typeof` symbol so libraries that pre-bundled a React <=18 jsx-runtime don't hit React error #525, and provides `window.ReactJSXRuntime`, the global that component bundles externalize `react/jsx-runtime` to.\n\n**Convention for component libraries:** externalize `react/jsx-runtime` and `react/jsx-dev-runtime` using the *defensive* external expression found in `components/dash-core-components/webpack.config.js` (`jsxRuntimeExternal`), not a bare `'ReactJSXRuntime'` string. The expression falls back to building the runtime from `window.React.createElement` when the global is missing, so the same bundle works on Dash versions that predate the shim. A bare `'ReactJSXRuntime'` external throws `ReactJSXRuntime is not defined` / `Cannot read properties of undefined (reading 'jsx')` at bundle load on older Dash.\n\nSet via environment variable (experimental):\n\n```bash\nREACT_VERSION=19.2.4 python app.py\n```\n\nOr programmatically before creating the app:\n\n```python\nfrom dash._dash_renderer import _set_react_version\n_set_react_version(\"19.2.4\")\n\nfrom dash import Dash\napp = Dash(__name__)\n```\n\nThis is useful for compatibility with older component libraries that require React 16.\n\n## Pages System\n\nMulti-page apps use `dash/_pages.py` with automatic routing via `dcc.Location`.\n\n### Page Registration\n\nEach page module calls `register_page()`:\n\n```python\n# pages/analytics.py\nfrom dash import register_page, html\n\nregister_page(__name__)  # infers path /analytics from module name\n\nlayout = html.Div(\"Analytics page\")\n```\n\n- **`PAGE_REGISTRY`** - `OrderedDict` storing all registered pages with metadata\n- **`register_page(module, path=None, ...)`** - Registers page with inferred or explicit path, title, description, image\n\n### Page Container\n\nWhen `use_pages=True`, Dash injects `page_container` as the layout (`dash/dash.py:148-158`):\n\n```python\npage_container = html.Div([\n    dcc.Location(id=\"_pages_location\", refresh=\"callback-nav\"),\n    html.Div(id=\"_pages_content\"),      # current page layout injected here\n    dcc.Store(id=\"_pages_store\"),       # stores page title/metadata\n])\n```\n\n### Routing Mechanism\n\n1. `dcc.Location` tracks browser URL changes\n2. Internal callback listens to `pathname` and `search` inputs\n3. `_path_to_page()` matches URL to registered page in `PAGE_REGISTRY`\n4. Page layout injected into `_pages_content` div\n\n### Path Templates (Dynamic Routes)\n\nPages can capture URL variables:\n\n```python\nregister_page(__name__, path_template=\"/asset/<asset_id>\")\n\ndef layout(asset_id=None):\n    return html.Div(f\"Asset: {asset_id}\")\n```\n\n`_parse_path_variables()` extracts variables via regex and passes them as kwargs to the layout function.\n\n### Auto-Discovery\n\n`_import_layouts_from_pages()` walks the `pages/` folder:\n- Skips files starting with `_` or `.`\n- Only imports `.py` files containing `register_page`\n- Auto-assigns `layout` attribute from each module to the registry\n\n### Page Ordering\n\nPages sorted by: numeric `order` → string `order` → no order → module name. Home page (`/`) defaults to order `0`.\n\n## Assets and Static Files\n\n### Asset Directory\n\nThe `assets/` folder is automatically scanned at startup (`dash/dash.py:_walk_assets_directory`):\n\n- `.css` files → appended to stylesheets\n- `.js` files → appended to scripts\n- `favicon.ico` → used as app favicon\n- Files matching `assets_ignore` regex are skipped\n\n### Loading Order\n\nResources load in this order (`dash/dash.py:1127-1165`):\n\n1. React dependencies (from dash-renderer)\n2. Component library scripts (dash-html-components, dash-core-components, etc.)\n3. External scripts (`external_scripts` parameter)\n4. Dash renderer bundle\n5. Clientside callback scripts (inline)\n\nCSS follows similar ordering with external stylesheets first.\n\n### Fingerprinting and Caching\n\nComponent assets use fingerprinted URLs for cache busting (`dash/fingerprint.py`):\n\n```\n/_dash-component-suites/dash_core_components/dash_core_components.v2_14_0m1699900000.min.js\n```\n\n- Fingerprinted resources: 1-year cache header\n- Non-fingerprinted: ETag validation\n- Asset files: query string `?m={modification_time}`\n\n### Configuration Options\n\n```python\nDash(\n    assets_folder='assets',           # path to assets directory\n    assets_url_path='assets',         # URL path segment\n    assets_ignore='.*ignored.*',      # regex to skip files\n    assets_external_path=None,        # CDN base URL for assets\n    serve_locally=True,               # True=local files, False=CDN\n    external_scripts=[],              # additional JS URLs\n    external_stylesheets=[],          # additional CSS URLs\n)\n```\n\n### Asset URL Generation\n\n`app.get_asset_url(path)` returns the correct URL accounting for `requests_pathname_prefix` (important for Dash Enterprise deployments where apps have URL prefixes).\n\n## Error Handling\n\n### Debug Mode\n\nDebug mode enables developer tools (`dash/dash.py:_setup_dev_tools`):\n\n```python\napp.run(debug=True)\n# Or via environment: DASH_DEBUG=true\n```\n\n### Dev Tools Options\n\n```python\napp.enable_dev_tools(\n    dev_tools_ui=True,              # show error UI overlay\n    dev_tools_props_check=True,     # validate component prop types\n    dev_tools_serve_dev_bundles=True,  # use development JS (better errors)\n    dev_tools_hot_reload=True,      # auto-reload on file changes\n    dev_tools_prune_errors=True,    # strip internal frames from tracebacks\n)\n```\n\nEnvironment variables: `DASH_DEBUG`, `DASH_UI`, `DASH_PROPS_CHECK`, `DASH_HOT_RELOAD`, etc.\n\n### Callback Exceptions\n\n**`PreventUpdate`** - Skip updating outputs without error:\n\n```python\nfrom dash.exceptions import PreventUpdate\n\n@app.callback(Output('out', 'children'), Input('in', 'value'))\ndef update(value):\n    if not value:\n        raise PreventUpdate\n    return value\n```\n\n**`no_update`** - Skip specific outputs in multi-output callbacks:\n\n```python\nfrom dash import no_update\n\n@app.callback(Output('a', 'children'), Output('b', 'children'), Input('in', 'value'))\ndef update(value):\n    return value, no_update  # only updates 'a'\n```\n\n### Error Handlers\n\nCallbacks support `on_error` for custom error handling:\n\n```python\ndef handle_error(err):\n    logging.error(f\"Callback failed: {err}\")\n    return \"Error occurred\"  # returned to output\n\n@app.callback(Output('out', 'children'), Input('in', 'value'), on_error=handle_error)\ndef update(value):\n    return 1 / 0  # triggers error handler\n```\n\nApp-level error handler set via constructor.\n\n### Validation\n\n- **Layout validation**: When `suppress_callback_exceptions=False` (default), checks that callback IDs exist in layout\n- **Callback validation**: `dev_tools_validate_callbacks=True` checks for circular dependencies\n- **Props checking**: Validates component prop types against schema in dev mode\n\n### Hot Reload\n\nWhen enabled, a watch thread monitors:\n- `assets/` folder for CSS/JS changes\n- Component package directories\n\nFrontend polls `/_reload-hash` and triggers reload when hash changes. Configurable via `hot_reload_interval` (default 3s) and `hot_reload_watch_interval` (default 0.5s).\n\n## Background Callbacks\n\nBackground callbacks execute in separate processes, allowing the main server to remain responsive. Managed by `dash/background_callback/managers/`.\n\n### Definition\n\n```python\nfrom dash import callback, Input, Output\nfrom dash.background_callback import DiskcacheManager\n\ncache_manager = DiskcacheManager()\n\n@callback(\n    Output(\"result\", \"children\"),\n    Input(\"button\", \"n_clicks\"),\n    background=True,\n    manager=cache_manager,\n    interval=500,  # polling interval in ms\n)\ndef compute(n_clicks):\n    # Expensive computation\n    return result\n```\n\n### Callback Managers\n\n**`DiskcacheManager`** (`dash/background_callback/managers/diskcache_manager.py`):\n- Uses `diskcache.Cache` for persistent storage\n- Spawns `multiprocess.Process` for each job\n- Results stored on disk, survives server restarts\n- Good for single-server deployments\n\n**`CeleryManager`** (`dash/background_callback/managers/celery_manager.py`):\n- Requires Celery app with result backend (Redis/RabbitMQ)\n- Jobs distributed across Celery workers\n- Supports horizontal scaling\n- Good for production multi-worker deployments\n\n```python\nfrom celery import Celery\nfrom dash.background_callback import CeleryManager\n\ncelery_app = Celery(__name__, broker=\"redis://localhost:6379/0\")\ncache_manager = CeleryManager(celery_app)\n```\n\n### Progress Updates\n\nThe `progress` parameter defines outputs updated during execution:\n\n```python\n@callback(\n    Output(\"result\", \"children\"),\n    Input(\"button\", \"n_clicks\"),\n    progress=Output(\"progress-bar\", \"value\"),\n    progress_default=0,\n    background=True,\n    manager=cache_manager,\n)\ndef compute(set_progress, n_clicks):\n    for i in range(100):\n        set_progress(i)\n        time.sleep(0.1)\n    return \"Complete\"\n```\n\n- `set_progress` is injected as first argument when `progress` is specified\n- Can be single Output or list of Outputs\n- `progress_default` sets value when callback not running\n\n### Running State\n\nThe `running` parameter updates outputs while the job executes:\n\n```python\n@callback(\n    Output(\"result\", \"children\"),\n    Input(\"button\", \"n_clicks\"),\n    running=[\n        (Output(\"button\", \"disabled\"), True, False),\n        (Output(\"status\", \"children\"), \"Computing...\", \"Ready\"),\n    ],\n    background=True,\n    manager=cache_manager,\n)\ndef compute(n_clicks):\n    time.sleep(5)\n    return \"Done\"\n```\n\nEach tuple: `(Output, value_while_running, value_when_complete)`\n\n### Cancellation\n\nThe `cancel` parameter specifies inputs that abort the job:\n\n```python\n@callback(\n    Output(\"result\", \"children\"),\n    Input(\"start-btn\", \"n_clicks\"),\n    cancel=[Input(\"cancel-btn\", \"n_clicks\")],\n    background=True,\n    manager=cache_manager,\n)\ndef compute(n_clicks):\n    # Job terminates if cancel-btn clicked\n    return result\n```\n\nManagers call `terminate_job()` which kills the process (Diskcache) or revokes the task (Celery).\n\n### Result Caching\n\nResults can be cached to avoid recomputation:\n\n```python\ndef get_user_id():\n    return flask.session.get(\"user_id\")\n\ncache_manager = DiskcacheManager(\n    cache_by=[get_user_id],  # cache key includes user ID\n    expire=3600,             # TTL in seconds\n)\n```\n\n- `cache_by` - List of functions whose return values are included in cache key\n- `expire` - Time-to-live for cached results\n- `cache_args_to_ignore` - Argument indices to exclude from cache key\n\n### How It Works\n\n1. **Initial request**: Frontend triggers callback, backend returns `cacheKey` and `job` ID\n2. **Polling**: Frontend polls `/_dash-update-component?cacheKey=...&job=...` at configured interval\n3. **Progress**: Each poll returns current progress value if set\n4. **Completion**: When job finishes, poll returns final result\n5. **Cleanup**: Results cleared from cache (unless `cache_by` specified)\n\nCache key is SHA256 hash of: callback key + function source + arguments + triggered inputs + cache_by values. Without `cache_by` a random nonce is added, so every run gets its own key: the job runs with the requester's context, so two users with equal inputs must never share a result. Diskcache writes those uncached entries with a one day expiry (`UNCACHED_RESULT_EXPIRE`) so results nobody polls for do not pile up; Celery's result backend already expires them.\n\n### Key Files\n\n- `dash/_callback.py:188-219` - Background spec construction\n- `dash/background_callback/managers/__init__.py` - `BaseBackgroundCallbackManager` abstract class\n- `dash/background_callback/managers/diskcache_manager.py` - Diskcache implementation\n- `dash/background_callback/managers/celery_manager.py` - Celery implementation\n- `dash/dash-renderer/src/actions/callbacks.ts:458-685` - Frontend polling logic\n\n## Jupyter Integration\n\nDash apps can run directly in Jupyter notebooks and JupyterLab. The integration is handled by `dash/_jupyter.py`.\n\n### Display Modes\n\n```python\napp.run(\n    jupyter_mode=\"inline\",      # Display in notebook cell (default)\n    jupyter_width=\"100%\",       # IFrame width\n    jupyter_height=650,         # IFrame height in pixels\n)\n```\n\n| Mode | Behavior |\n|------|----------|\n| `\"inline\"` | App displays in notebook cell via IFrame |\n| `\"external\"` | Prints URL, user opens in browser tab |\n| `\"jupyterlab\"` | Opens in dedicated JupyterLab tab |\n| `\"tab\"` | Auto-opens URL in new browser tab |\n\n### How It Works\n\n1. `app.run()` detects Jupyter environment via `get_ipython()`\n2. Server starts in background daemon thread\n3. Jupyter comm protocol negotiates proxy configuration\n4. App displays according to selected mode\n\n```\napp.run() in notebook\n    ↓\nDetect Jupyter → Start server in background thread\n    ↓\nComm request → Extension responds with base_url\n    ↓\nCompute dashboard URL with proxy path\n    ↓\nDisplay: IFrame (inline) / URL (external) / Tab (jupyterlab)\n```\n\n### Notebook Extension\n\nClassic Jupyter notebooks use `dash/nbextension/`:\n\n- `main.js` - Registers \"dash\" comm target\n- `dash.json` - Extension loader configuration\n\nThe extension handles comm messages:\n- `base_url_request` → responds with server URL and base path\n- Enables proper proxy routing in JupyterHub environments\n\n### JupyterLab Extension\n\nJupyterLab uses `@plotly/dash-jupyterlab/`:\n\n- `src/index.ts` - TypeScript plugin implementing `JupyterFrontEndPlugin`\n- `DashIFrameWidget` - Lumino widget for rendering apps in tabs\n\nHandles messages:\n- `base_url_request` → responds with JupyterLab server config\n- `show` → creates dedicated tab with IFrame widget\n\nCompatible with JupyterLab 2.x, 3.x, and 4.x.\n\n### Proxy Configuration\n\nIn JupyterHub/proxy environments, the extension negotiates `requests_pathname_prefix`:\n\n```python\n# Computed from Jupyter base path\nrequests_pathname_prefix = \"/user/username/proxy/8050/\"\n```\n\nThis ensures callbacks route correctly through the Jupyter proxy.\n\n### Google Colab\n\nSpecial handling for Colab:\n- Uses `google.colab.output.serve_kernel_port_as_iframe()` for inline\n- Uses `google.colab.output.serve_kernel_port_as_window()` for external\n- Only supports \"inline\" and \"external\" modes\n\n### Key Files\n\n- `dash/_jupyter.py` - `JupyterDash` class, comm handling, server thread\n- `dash/nbextension/main.js` - Classic notebook extension\n- `@plotly/dash-jupyterlab/src/index.ts` - JupyterLab extension\n\n## Configuration Reference\n\n### Dash() Constructor Parameters\n\n**Basic Setup:**\n- `name` - Application name (default: infers from `__name__`)\n- `server` - Server instance (Flask, Quart, or FastAPI) or `True` to create Flask (default: `True`)\n- `title` - Browser tab title (default: `\"Dash\"`)\n- `update_title` - Title during callbacks (default: `\"Updating...\"`)\n\n**Assets & Resources:**\n- `assets_folder` - Path to assets directory (default: `\"assets\"`)\n- `assets_url_path` - URL path for assets (default: `\"assets\"`)\n- `assets_ignore` - Regex to exclude assets (default: `\"\"`)\n- `serve_locally` - Serve from local vs CDN (default: `True`)\n- `external_scripts` - Additional JS URLs\n- `external_stylesheets` - Additional CSS URLs\n\n**Routing:**\n- `url_base_pathname` - Base URL prefix for entire app\n- `requests_pathname_prefix` - Prefix for AJAX requests\n- `routes_pathname_prefix` - Prefix for API routes\n\n**Multi-Page:**\n- `use_pages` - Enable pages system (default: auto-detect)\n- `pages_folder` - Path to pages directory (default: `\"pages\"`)\n\n**Behavior:**\n- `suppress_callback_exceptions` - Skip callback validation (default: `False`)\n- `prevent_initial_callbacks` - Skip callbacks on load (default: `False`)\n- `background_callback_manager` - DiskcacheManager or CeleryManager\n- `on_error` - Global callback error handler\n- `shared_storage` - Cross-process state + pub/sub backend (default: `LocalSharedStorage`). Pass `None` to disable, or a `BaseSharedStorage` subclass/instance to swap. See [Shared Storage](#shared-storage).\n\n**WebSocket Callbacks:**\n- `websocket_callbacks` - Enable WebSocket for all callbacks (default: `False`). Requires FastAPI backend.\n- `websocket_allowed_origins` - List of allowed origins for WebSocket connections\n- `websocket_inactivity_timeout` - Disconnect WebSocket after inactivity period in ms (default: `300000` = 5 minutes). Set to `0` to disable.\n\n### app.run() Parameters\n\n- `host` - Server IP (default: `\"127.0.0.1\"`, env: `HOST`)\n- `port` - Server port (default: `8050`, env: `PORT`)\n- `debug` - Enable dev tools (default: `False`, env: `DASH_DEBUG`)\n- `jupyter_mode` - Display mode: `\"inline\"`, `\"external\"`, `\"tab\"`\n\n### Environment Variables\n\n| Variable | Purpose |\n|----------|---------|\n| `DASH_DEBUG` | Enable debug mode |\n| `DASH_URL_BASE_PATHNAME` | Base URL prefix |\n| `DASH_SUPPRESS_CALLBACK_EXCEPTIONS` | Skip validation |\n| `DASH_HOT_RELOAD` | Enable hot reload |\n| `DASH_PROPS_CHECK` | Validate prop types |\n| `DASH_PRUNE_ERRORS` | Simplify tracebacks |\n| `HOST` | Server host |\n| `PORT` | Server port |\n| `DASH_SECRET_KEY` | Signing secret for page, background and stream tokens when `server.secret_key` is unset |\n| `DASH_SHARED_STORAGE` | Shared-storage backend when `shared_storage=` is not passed (see Shared Storage) |\n\n## Stores and Client-Side State\n\n### dcc.Store\n\nStore data client-side with configurable persistence:\n\n```python\ndcc.Store(id='my-store', storage_type='local', data={'key': 'value'})\n```\n\n| Storage Type | Persists | Scope | Use Case |\n|--------------|----------|-------|----------|\n| `'memory'` | Page view only | Tab | Temporary state, debugging |\n| `'session'` | Browser session | Tab | Form state, filters |\n| `'local'` | Forever | All tabs | User preferences, settings |\n\n**Usage pattern:**\n```python\n@app.callback(Output('output', 'children'), Input('store', 'data'))\ndef use_store(data):\n    return data['key']\n\n@app.callback(Output('store', 'data'), Input('input', 'value'))\ndef update_store(value):\n    return {'key': value}\n```\n\n### Component Persistence\n\nAutomatically persist user edits to component props:\n\n```python\ndcc.Dropdown(\n    id='dropdown',\n    options=[...],\n    persistence=True,           # Enable persistence\n    persistence_type='local',   # local, session, or memory\n    persisted_props=['value'],  # Props to persist (default varies by component)\n)\n```\n\n- **`persistence`** - `True` or unique key to enable\n- **`persistence_type`** - Storage backend (default: `'local'`)\n- **`persisted_props`** - List of prop names to persist\n\nSupported components: Input, Dropdown, Checklist, RadioItems, Slider, RangeSlider, DatePickerSingle, DatePickerRange, Textarea, Tabs, DataTable.\n\n### When to Use Each\n\n| Need | Solution |\n|------|----------|\n| Server-controlled state | `dcc.Store` with callbacks |\n| Remember user selections | Component `persistence=True` |\n| Share state across tabs | `dcc.Store` with `storage_type='local'` |\n| Session-only state | `persistence_type='session'` |\n\n## Shared Storage\n\nBackend-agnostic **server-side** state shared across worker processes: a\ncross-process key/value store plus an ordered, replayable publish/subscribe\nchannel. Unlike `dcc.Store` (which lives in the browser), shared storage lives\non the server and lets callbacks running in different workers see the same keys\nand topics without standing up an external service like Redis.\n\nEnabled by default on every app. It starts lazily on first use, so it costs\nnothing until touched.\n\n### Accessing It\n\n```python\nimport dash\nfrom dash import Input, Output, callback\n\n@callback(Output(\"out\", \"children\"), Input(\"btn\", \"n_clicks\"))\ndef handler(n):\n    store = dash.ctx.shared_storage      # inside a callback\n    store.set(\"clicks\", n)\n    return store.get(\"clicks\", 0)\n\n# Or off the app directly:\napp = dash.Dash()\napp.shared_storage.set(\"key\", {\"any\": \"json-compatible value\"})\n```\n\nValues (and published messages) must be **JSON-compatible** — dict / list / str\n/ int / float / bool / None — the same constraint as `dcc.Store` and callback\noutputs. Messages are encoded with `msgspec` (msgpack), a hard dependency.\n\n### API\n\n`app.shared_storage` (and `dash.ctx.shared_storage`) is a `BaseSharedStorage`:\n\n| Method | Purpose |\n|--------|---------|\n| `get(key, default=None)` | Read a value |\n| `set(key, value, ttl=None)` | Write a value; `ttl` = optional lifetime in seconds |\n| `delete(key)` | Remove a key |\n| `publish(topic, message, ttl=None)` | Append a message to a topic; `ttl` = release the topic after that long idle |\n| `subscribe(topic, replay_from=None)` | Return a `Subscription` |\n\n**Key expiry (TTL).** `set(key, value, ttl=<seconds>)` gives a key a bounded\nlifetime; once it elapses, reads return the `default` again. `ttl=None` (the\ndefault) never expires. Expiry is lazy/best-effort — an expired key is dropped\non its next read, not at a guaranteed instant. Each backend uses its native\nmechanism: a monotonic deadline on the in-memory owner, diskcache `expire`,\nRedis `PX`. Use it for session-scoped or cache-like state you don't want to\naccumulate unboundedly.\n\n**Ordered pub/sub.** Each `publish` to a topic gets a monotonically increasing\nsequence number (per topic, starting at 1; `0` means \"before the first\nmessage\"). A subscriber receives every message published after it subscribed,\nin order:\n\n```python\n# Producer (one worker):\nstore.publish(\"progress\", {\"pct\": 50})\n\n# Consumer (another worker/callback):\nwith store.subscribe(\"progress\") as sub:\n    for message in sub:          # or: async for message in sub\n        print(message[\"pct\"])\n```\n\n**Replay on reconnect.** Pass `replay_from=<last-seen-seq>` to resume after a\ndrop; buffered messages since that cursor replay first, so a reconnecting\nconsumer doesn't miss messages. If the consumer fell farther behind than the\nbounded buffer holds, the subscription raises `SharedStorageGap` (an explicit\ngap rather than a silent hole). A `Subscription` is iterable synchronously\n(`for`) or asynchronously (`async for`), is a context manager, and has\n`close()`.\n\n### Backends\n\nThree backends ship, differing only in **how far state reaches**. Pick by the\ndeployment topology — critically, whether the app runs behind a load balancer as\nmore than one container/pod (see [Deployment Topology](#deployment-topology-and-shared-storage) below).\n\n| Backend | Backing store | Reaches | Extra |\n|---------|---------------|---------|-------|\n| `LocalSharedStorage` *(default)* | in-memory, owner-elected socket | one **container** (all its workers) | none |\n| `DiskcacheSharedStorage` | a `diskcache.Cache` | one **host** (all processes sharing the dir) | `dash[diskcache]` |\n| `RedisSharedStorage` | Redis (Streams for pub/sub) | **any** process/container/pod | `dash[redis]` |\n\nAll three implement the same `BaseSharedStorage` contract (KV + ordered,\nreplayable pub/sub with `SharedStorageGap` on buffer overrun), so app code is\nidentical across them — only the constructor differs.\n\n```python\nfrom dash import Dash\nfrom dash._shared_storage import (\n    LocalSharedStorage, DiskcacheSharedStorage, RedisSharedStorage,\n)\n\nDash(shared_storage=LocalSharedStorage)                       # default\nDash(shared_storage=DiskcacheSharedStorage(directory=\"/tmp/ss\"))\nDash(shared_storage=RedisSharedStorage(url=\"redis://host:6379\"))\nDash(shared_storage=None)                                     # disabled\n```\n\nWhen disabled, accessing `app.shared_storage` / `dash.ctx.shared_storage`\nraises `SharedStorageError`.\n\n**`LocalSharedStorage`** — in-memory, no external service. On first use every\nworker races to become the single **owner** for the machine by binding a stable\naddress (`AF_UNIX` on POSIX, `127.0.0.1` loopback on Windows); the winner hosts\nthe engine, losers proxy over the socket. The bind *is* the lease — when the\nowner dies the address frees and a survivor re-elects. A single-process\ndeployment is its own owner and pays no socket overhead. Knobs: `namespace`\n(defaults to a hash of cwd + argv[0]), `buffer_size` (default 32 — small on\npurpose, since each topic retains that many arbitrary payloads; raise it for a\nwider reconnect window).\n\n**Topic lifetime.** `publish(topic, message, ttl=None)` takes an optional ttl\n(seconds, at least `MIN_TOPIC_TTL` = 1s, so a reconnecting reader is not\noutrun). A topic with a ttl is released, buffer and sequence both, once nobody\nhas published to or read from it for that long; a later publish starts it over\nat 1, so a consumer returning with an old cursor gets `SharedStorageGap`.\nWithout a ttl a topic lives as long as the store. The latest publish's ttl\nwins. Streaming publishes with `STREAM_TOPIC_TTL` (300s); user topics default\nto no ttl. Every backend expires a topic as a whole, never message by message\nwhile it is read (`tests/shared_storage/test_topic_ttl.py` runs the same cases\non all three). Local: the engine records the ttl on the topic and sweeps idle\nones (no publish, head or poll, and no call holding it) at most once a second,\non the next pub/sub call. The sweep also drops empty, unheld topics, such as\nthe one a returning reader's poll recreates after a release. Redis: the ttl sits in a third key; the publish\nscript `PEXPIRE`s all three to 1.25 ttl, and a poll script renews them. A poll\non such a topic blocks in `XREAD` for at most a third of that lifetime, not the\nusual 5s, so a waiting reader renews it in time. Diskcache: the ttl\nsits in a key; on publish or poll, once less than one ttl is left, every key of\nthe topic (counter, ttl, buffered messages) is renewed to 1.25 ttl, and at\nonce when a publish changes the ttl. A poll on such a topic waits at most half\na ttl before it renews again.\n\n*Durability* is controlled by `mode` (the key/value store only — pub/sub is\nalways transient):\n\n| `mode` | Writes | On owner death / restart |\n|--------|--------|--------------------------|\n| `\"memory\"` *(default)* | none | re-elects **cold** (state lost) |\n| `\"persist\"` | write-through on every set/delete | re-elected/fresh owner **recovers** from disk |\n| `\"persist-reset\"` | flushed every `flush_interval`s (default 60) + on clean exit | recovers to the last snapshot (up to one interval may be lost on an unclean crash) |\n\nOnly the owner persists. The on-disk store is *chunked* (keys sharded across\nmsgpack chunk files via a key→chunk index, so a mutation rewrites only the\naffected chunk — adapted from raposa's `BinaryStorage`); each chunk write is\natomic (temp file + `os.replace`). TTLs survive restarts (stored as wall-clock\ndeadlines; expired keys are dropped on load). `path` sets the store directory\n(default: a per-namespace folder under the user cache dir). Recovery runs on\nevery election, so persisted data also survives owner re-election:\n\n```python\nDash(shared_storage=LocalSharedStorage(mode=\"persist\"))\nDash(shared_storage=LocalSharedStorage(mode=\"persist-reset\", flush_interval=30))\n```\n\n**`DiskcacheSharedStorage`** — KV + an append-log pub/sub on a `diskcache.Cache`,\nthe same store `DiskcacheManager` uses; pass an existing `cache` to share one.\nEvery process on one host that opens the same directory shares state, so it\ngives multi-worker parity with the local backend without a socket. **Not for\npods** — each pod has its own ephemeral disk.\n\n**`RedisSharedStorage`** — KV on Redis strings, pub/sub on a Redis Stream per\ntopic (an atomic `INCR`+`XADD` script keeps sequences ordered; `MAXLEN` bounds\nthe replay window; a subscriber past the trimmed floor gets `SharedStorageGap`).\nRedis is the single source of truth, so no owner election. Pass a `url`\n(defaults to `$REDIS_URL`) or an existing `client` to reuse a connection pool.\nThis is the only backend correct for **horizontally-scaled, multi-pod**\ndeployments. On an event loop it uses `redis.asyncio` (one client per loop),\nand one reader task per loop serves every async subscription with a single\nmulti-stream `XREAD`; a new subscription interrupts it through a private wake\nstream. Never park a blocking read in the executor per subscription: at a dozen\nopen downlinks that starved every other store call in the worker (25-50% failed\nstreams at 25 browsers on uvicorn `--workers 4`). Diskcache's async\nsubscriptions likewise poll without blocking and sleep on the loop.\n\n### Deployment Topology and Shared Storage\n\nThe default `LocalSharedStorage` is **per-container**: its socket/loopback\nelection only reaches processes in the same network + filesystem namespace.\n\n- **Single process / single pod** (e.g. Plotly Cloud apps): fine — one owner,\n  nothing to fragment.\n- **Multiple gunicorn workers in one container**: fine — they share the pod's\n  loopback/socket and elect one owner.\n- **Multiple pods behind a load balancer** (e.g. Dash Enterprise apps scaled by\n  an HPA, routed round-robin with no session affinity): **each pod elects its own\n  isolated owner**, so `set()`/`publish()` on one pod are invisible on another.\n  This works at 1 pod and fragments silently once it scales — use\n  `RedisSharedStorage` (one Redis shared by all pods) instead.\n\nA hosting platform can switch the backend without editing the app through\n`DASH_SHARED_STORAGE` (`_shared_storage/_env.py`), read only when the app did\nnot pass `shared_storage=` (the default is a sentinel, so an explicit argument,\n`None` included, always wins): `local`, `none`, `diskcache:///abs/path`, or a\n`redis://` / `rediss://` URL. A `key_prefix` query parameter on the Redis URL\n(`redis://host/0?key_prefix=myapp`) sets `RedisSharedStorage(key_prefix=)` and\nis stripped before the URL reaches redis-py; the diskcache path is\npercent-decoded. `cluster://` is reserved and raises; anything else raises\n`InvalidConfig` at construction, without echoing the value (it can hold\ncredentials). The value becomes a zero-argument\nfactory, so nothing is built or connected until `app.shared_storage` is first\nread, but a missing extra (`dash[redis]`, `dash[diskcache]`) fails at\nconstruction with the backend's own `ImportError`.\n\n### Custom and Out-of-Tree Backends\n\n`BaseSharedStorage` is the stable, public extension point. A backend — shipped\nin-tree or as a **separate package** — implements:\n\n- `get(key, default)` / `set(key, value)` / `delete(key)` — JSON-compatible values\n- `publish(topic, message, ttl=None)` and `subscribe(topic, replay_from=None) -> Subscription`\n- topic expiry: with a `ttl`, release the topic as a whole once nobody has\n  published to or read from it for that long, never message by message while\n  it is read; reject a ttl under `MIN_TOPIC_TTL` (run `test_topic_ttl.py`\n  against a new backend)\n- optional `start()` / `close()` (idempotent, called once per worker)\n\nand returns a `Subscription` (`__iter__` / `__aiter__` / `close`) that raises\n`SharedStorageGap` when the replay buffer overran. That trio —\n`BaseSharedStorage`, `Subscription`, `SharedStorageGap` — is exported from\ntop-level `dash`; the poll-loop helpers (`PollResult`, the polling subscription)\nare private and not part of the contract.\n\nCore only ships backends whose dependency is already a Dash extra (`msgspec`\nbase; `dash[diskcache]`; `dash[redis]`). Anything needing a heavier dependency\nbelongs **out of tree** behind this same interface — e.g. a Postgres backend\n(`LISTEN`/`NOTIFY` for push pub/sub + a table for KV and replay) lives in its own\npackage so a `psycopg` connection is never pulled into Dash core. It plugs in the\nsame way as a built-in: `Dash(shared_storage=PostgresSharedStorage(...))`.\n\n### Module Layout\n\n| File | Responsibility |\n|------|----------------|\n| `_shared_storage/base.py` | Abstract interface: `BaseSharedStorage`, `Subscription`, `SharedStorageError`, `SharedStorageGap` |\n| `_shared_storage/_engine.py` | `StoreEngine`: authoritative in-memory kv map + per-topic ordered log with bounded replay buffer (thread-safe, transport-agnostic) |\n| `_shared_storage/local.py` | `LocalSharedStorage`: owner election; owner hosts the engine, clients proxy |\n| `_shared_storage/diskcache.py` | `DiskcacheSharedStorage`: KV + append-log pub/sub on a `diskcache.Cache` |\n| `_shared_storage/redis.py` | `RedisSharedStorage`: KV + Redis Streams pub/sub (atomic `INCR`+`XADD`) |\n| `_shared_storage/_polling.py` | `PollingSubscription`: shared poll-loop subscription for the diskcache/Redis backends |\n| `_shared_storage/_transport.py` | Length-prefixed, token-gated socket transport (local backend) |\n| `_shared_storage/_codec.py` | msgspec msgpack codec (data-only) |\n| `_shared_storage/_env.py` | `DASH_SHARED_STORAGE` parsing |\n| `dash.py` | `shared_storage` constructor arg + lazy `app.shared_storage` property |\n| `_callback_context.py` | `dash.ctx.shared_storage` accessor |\n\nRequires `msgspec` (in `requirements/install.txt`); `DiskcacheSharedStorage`\nneeds the `diskcache` extra and `RedisSharedStorage` the `redis` extra.\n\n## Async Callbacks\n\nDash supports `async def` callbacks for non-blocking execution.\n\n### Setup\n\n**With Flask backend:**\n```bash\npip install dash[async]\n```\n\nAsync is auto-enabled when `asgiref` is detected. Or explicitly:\n\n```python\napp = Dash(__name__, use_async=True)\n```\n\n**With Quart or FastAPI backend:** Async is native - no extra dependencies needed.\n\n```python\nfrom fastapi import FastAPI\nfrom dash import Dash\n\nserver = FastAPI()\napp = Dash(__name__, server=server)  # Async works automatically\n```\n\n### Usage\n\n```python\nimport asyncio\n\n@app.callback(Output('output', 'children'), Input('input', 'value'))\nasync def async_update(value):\n    await asyncio.sleep(1)  # Non-blocking\n    return f\"Processed: {value}\"\n```\n\n### Key Points\n\n- Regular async callbacks are **non-blocking** - multiple can run concurrently\n- Background callbacks also support `async def`\n- Jupyter uses `nest_asyncio` for event loop compatibility\n- With Flask backend: requires `dash[async]`, coroutines raise error without it\n- With Quart/FastAPI backends: async is native, no extra setup needed\n\n### Async with Background Callbacks\n\n```python\n@app.callback(\n    Output('result', 'children'),\n    Input('btn', 'n_clicks'),\n    background=True,\n    manager=diskcache_manager,\n)\nasync def async_background(n_clicks):\n    await asyncio.sleep(5)\n    return \"Done\"\n```\n\nBoth DiskcacheManager and CeleryManager support async functions via `asyncio.run()`.\n\n## WebSocket Callbacks\n\nWebSocket callbacks use a persistent WebSocket connection instead of HTTP POST for callback execution. This reduces latency and connection overhead for applications with frequent callbacks.\n\n### Requirements\n\n- **FastAPI backend required**: WebSocket callbacks only work with FastAPI\n- **SharedWorker support**: Modern browsers (not IE)\n\n### Usage\n\n**Enable globally for all callbacks:**\n```python\nfrom fastapi import FastAPI\nfrom dash import Dash\n\nserver = FastAPI()\napp = Dash(__name__, server=server, websocket_callbacks=True)\n```\n\n**Enable per-callback:**\n```python\n@app.callback(\n    Output('output', 'children'),\n    Input('input', 'value'),\n    websocket=True  # Use WebSocket for this callback only\n)\ndef update(value):\n    return f\"Value: {value}\"\n```\n\n### Configuration\n\n```python\napp = Dash(\n    __name__,\n    server=server,\n    websocket_callbacks=True,\n    websocket_inactivity_timeout=300000,  # 5 minutes (default)\n    websocket_heartbeat_interval=30000,   # 30 seconds (default)\n    websocket_allowed_origins=['https://example.com'],\n)\n```\n\n- **`websocket_callbacks`** - Enable WebSocket for all callbacks (default: `False`)\n- **`websocket_inactivity_timeout`** - Close WebSocket after period of inactivity in milliseconds (default: `300000` = 5 minutes). Heartbeats do not count as activity. Set to `0` to disable timeout. Connection automatically reconnects when needed.\n- **`websocket_heartbeat_interval`** - Interval for heartbeat/keep-alive checks in milliseconds (default: `30000` = 30 seconds). Also determines how frequently inactivity timeout is checked.\n- **`websocket_allowed_origins`** - List of allowed origins for WebSocket connections (security)\n\n### Architecture\n\n```\n┌─────────────────────────────────────────────────────────────────────────┐\n│ Browser Tab 1                          Browser Tab 2                    │\n│ ┌─────────────┐                       ┌─────────────┐                   │\n│ │  Renderer   │                       │  Renderer   │                   │\n│ └──────┬──────┘                       └──────┬──────┘                   │\n│        │ postMessage                         │ postMessage              │\n│        └────────────┬───────────────────────┘                           │\n│                     ▼                                                   │\n│         ┌─────────────────────┐                                         │\n│         │    SharedWorker     │  (one per origin)                       │\n│         │   dash-ws-worker    │                                         │\n│         └──────────┬──────────┘                                         │\n└────────────────────│────────────────────────────────────────────────────┘\n                     │ WebSocket\n                     ▼\n┌─────────────────────────────────────────────────────────────────────────┐\n│ Server (FastAPI)                                                        │\n│   WebSocket Endpoint: /_dash-ws-callback                                │\n└─────────────────────────────────────────────────────────────────────────┘\n```\n\n**Connection & Reconnection Flow:**\n```\nRenderer                   SharedWorker                 Server\n    │                              │                        │\n    │──[CONNECT]──────────────────>│                        │\n    │                              │──[WebSocket Connect]──>│\n    │<─[CONNECTED]─────────────────│<─[Connected]───────────│\n    │                              │                        │\n    │──[CALLBACK_REQUEST]─────────>│──[callback request]───>│\n    │<─[CALLBACK_RESPONSE]─────────│<─[callback response]───│\n    │                              │                        │\n    │      (inactivity)            │    (heartbeat check)   │\n    │                              │──[close 4001]─────────>│\n    │<─[DISCONNECTED]──────────────│                        │\n    │                              │                        │\n    │──[CALLBACK_REQUEST]─────────>│──[reconnect + send]───>│\n    │<─[CALLBACK_RESPONSE]─────────│<─[response]────────────│\n```\n\n- **SharedWorker**: Single WebSocket connection shared across browser tabs\n- **Heartbeat**: Periodic ping/pong to detect dead connections (30s interval)\n- **Inactivity timeout**: Closes connection after no actual callback activity (not heartbeats)\n- **Auto-reconnect**: Reconnects automatically when a callback is triggered after timeout\n\n### Long-Running Callbacks with set_props/get_props\n\nWebSocket callbacks can stream updates to the client during execution using `set_props()` and read current component values using `ctx.websocket`:\n\n```python\nimport asyncio\nfrom dash import callback, Output, Input, set_props, ctx\nfrom dash.exceptions import PreventUpdate\n\n@callback(\n    Output('result', 'children'),\n    Input('start-btn', 'n_clicks'),\n    prevent_initial_call=True\n)\nasync def long_running_task(n_clicks):\n    ws = ctx.websocket\n    if not ws:\n        return \"WebSocket not available\"\n\n    # Stream progress updates to the client\n    for i in range(100):\n        # IMPORTANT: Check is_shutdown in loops to detect disconnections\n        if ws.is_shutdown:\n            raise PreventUpdate  # Exit gracefully on disconnect\n        await asyncio.sleep(0.1)\n        set_props('progress-bar', {'value': i + 1})\n        set_props('status', {'children': f'Processing step {i + 1}/100...'})\n\n    # Read current value from another component\n    current_value = await ws.get_prop('input-field', 'value')\n\n    return f\"Completed! Input was: {current_value}\"\n```\n\n**IMPORTANT - Checking `is_shutdown` in Loops:**\n\nLong-running callbacks that use loops **must** check `ws.is_shutdown` to detect when the WebSocket connection has closed. Without this check:\n- Callbacks continue running after the client disconnects, wasting server resources\n- `set_props` calls go to a closed connection and are lost\n- The callback result is never delivered to the client\n\nOnly \"persistent callbacks\" (callbacks with no Output and no Input that use only `set_props`) are automatically restarted when the WebSocket reconnects. Regular callbacks with outputs are not restarted.\n\n**API:**\n- `set_props(component_id, props_dict)` - Stream prop updates immediately to client\n- `ctx.websocket` - Get WebSocket interface (returns `None` if not in WS context)\n- `ws.is_shutdown` - Check if the WebSocket connection has been closed\n- `await ws.get_prop(component_id, prop_name, timeout=30.0, *, path=None)` - Read a full or nested prop value\n- `await ws.set_prop(component_id, prop_name, value)` - Set single prop (async version)\n- `await ws.close(code, reason)` - Close the WebSocket connection\n\n### Partial Reads with get_prop\n\nUse the keyword-only `path` argument with the same string keys and integer list\nindices supported by `Patch`:\n\n```python\nevent = await ws.get_prop(\n    'store', 'data', path=[\n\n<!-- File: CLAUDE.md (Tokens: ~634 | Category: claude-rule) -->\n# CLAUDE.md\n\nGuidance for Claude Code (claude.ai/code) working in this repo.\n\n## Project Overview\n\nDash is a Python framework for building reactive web-based data visualization applications. Built on Plotly.js, React, and Flask.\n\n## Docs\n\nDeeper reference lives in `.ai/`. Open the one that covers what you're touching, don't read them all up front:\n\n- `COMMANDS.md` - build, test, lint commands\n- `ARCHITECTURE.md` - backend, callbacks, pages, background callbacks, stores, async, security\n- `RENDERER.md` - frontend, crawlLayout, Redux store, clientside API\n- `COMPONENTS.md` - component system, generation, resources\n- `TESTING.md` - test framework, fixtures, patterns\n- `TROUBLESHOOTING.md` - common errors and fixes\n- `PERFORMANCE.md` - benchmark harness, profiling, and performance findings\n\nTo find code, grep. Don't rely on a hand-kept file map, it goes stale.\n\n## Writing Style\n\nApplies to code comments, commit messages, PR descriptions, review comments, and CHANGELOG entries.\n\n- **Comments are a last resort.** The code is Python and should read on its own. Write a comment only when the code cannot explain itself: a non-obvious why, a workaround, a gotcha. Never narrate what the next line already says. If in doubt, leave it out.\n- **No em dashes or en dashes (`—` `–`).** Use a colon, comma, parentheses, or two sentences.\n- **Straight ASCII only.** Straight quotes and apostrophes (`'` `\"`), no curly ones. No ellipsis character (`…`); write three periods if you really need them. No decorative unicode.\n- **Plain words.** Say the thing directly, in words most people know. No AI filler, no thesaurus vocabulary, no heavy styling. Short and to the point.\n\n## Quality Gate\n\nBefore you say a change works:\n\n1. **Prove it in a running app.** Write a small repro app in the scratchpad and drive it headless with `dash_duo` (a throwaway pytest test is the practical way, see `.ai/TESTING.md`). Watch the real behavior. \"The code looks right\" is not done.\n2. **Leave a test behind.** Any behavior change or fix needs a test that stays in the tree: an integration/acceptance test under `tests/integration/`, or a renderer test for frontend-only work. It should fail before your change and pass after. Never run the whole integration suite at once, always a specific file or `-k`.\n3. **Second-model review.** Before handing back, launch a review subagent on a different model (the Agent tool takes a `model` override, e.g. Sonnet when you're on Opus) to check the diff for correctness and cases you missed. Fold in what holds up. A fresh model catches what the author misses.\n\n<!-- File: .github/copilot-instructions.md (Tokens: ~361 | Category: copilot-instructions) -->\n# Copilot Instructions\n\nThis file provides guidance to GitHub Copilot when working with this repository.\n\nFor detailed documentation, see the [`.ai/`](../.ai/) directory:\n- [Commands](../.ai/COMMANDS.md) - Build, test, lint\n- [Architecture](../.ai/ARCHITECTURE.md) - Backend, layout, callbacks\n- [Components](../.ai/COMPONENTS.md) - Component system, generation, resources\n\n## Project Overview\n\nDash is a Python framework for building reactive web-based data visualization applications. Built on Plotly.js, React, and Flask.\n\n## Quick Reference\n\n```bash\n# Setup\npip install -e .[ci,dev,testing,celery,diskcache] && npm ci\n\n# Build\nnpm run build                                    # Linux/Mac\nnpm run first-build                              # Windows (use Bash)\ndash-update-components \"dash-core-components\"    # Single component\n\n# Test\npytest tests/unit                                # Unit tests\npytest tests/integration                         # Integration tests\npytest -k test_name                              # Specific test\n\n# Lint\nnpm run lint                                     # All linters\nnpm run private::format.black                    # Auto-format Python\n```\n\n## Key Files\n\n- `dash/dash.py` - Main Dash app class, layout, callbacks, routing\n- `dash/_callback.py` - `@callback` decorator and execution\n- `dash/dependencies.py` - `Input`, `Output`, `State`, wildcards\n- `dash/development/base_component.py` - Component base class, `to_plotly_json()`\n- `dash/dash-renderer/` - TypeScript/React frontend\n- `components/` - Component packages (dcc, html, table)\n\n<!-- File: components/dash-core-components/packages/README.md (Tokens: ~19 | Category: plugin-manifest) -->\nFolder to place temporary modules e.g. plotly.js-dist-min in .tgz format\n</yakaai_skills>"}