{"id":51339648,"url":"https://github.com/async/framework","last_synced_at":"2026-07-02T06:04:45.773Z","repository":{"id":259169230,"uuid":"874018436","full_name":"async/framework","owner":"async","description":"No-build AsyncLoader app runtime with signals, command events, server calls, route partials, cache split, SSR activation, and streaming boundaries.","archived":false,"fork":false,"pushed_at":"2026-06-19T22:06:11.000Z","size":1543,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2026-06-20T20:27:24.435Z","etag":null,"topics":["backend","frontend","fullstack","resumability","web"],"latest_commit_sha":null,"homepage":"https://async.github.io/framework/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/async.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-10-17T05:48:54.000Z","updated_at":"2026-06-19T22:06:06.000Z","dependencies_parsed_at":"2024-10-26T03:09:21.944Z","dependency_job_id":null,"html_url":"https://github.com/async/framework","commit_stats":null,"previous_names":["async-framework/async-framework","async/framework"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/async/framework","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/async%2Fframework","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/async%2Fframework/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/async%2Fframework/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/async%2Fframework/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/async","download_url":"https://codeload.github.com/async/framework/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/async%2Fframework/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35035005,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-02T02:00:06.368Z","response_time":173,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["backend","frontend","fullstack","resumability","web"],"created_at":"2026-07-02T06:04:45.001Z","updated_at":"2026-07-02T06:04:45.739Z","avatar_url":"https://github.com/async.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @async/framework\n\nAsync is a layered framework plan that starts as a no-build browser bootloader:\nsignals, async signals, delegated command events, scoped fragment components,\nserver calls, route partials, and out-of-order boundary swaps without a virtual\nDOM.\n\n```bash\npnpm add @async/framework\n```\n\n```html\n\u003cmain async:container\u003e\n  \u003cbutton type=\"button\" on:click=\"decrement\"\u003e-\u003c/button\u003e\n  \u003cstrong signal:text=\"count\"\u003e\u003c/strong\u003e\n  \u003cbutton type=\"button\" on:click=\"increment\"\u003e+\u003c/button\u003e\n\u003c/main\u003e\n\u003cscript type=\"module\" src=\"./main.js\"\u003e\u003c/script\u003e\n```\n\n```js\nimport {\n  Async,\n  createSignal\n} from \"@async/framework\";\n\nAsync.use({\n  signal: {\n    count: createSignal(0)\n  },\n  handler: {\n    increment() {\n      this.signals.update(\"count\", (count) =\u003e count + 1);\n    },\n    decrement() {\n      this.signals.update(\"count\", (count) =\u003e count - 1);\n    }\n  }\n});\n\nAsync.start({ root: document });\n```\n\n## What It Is\n\n`@async/framework` is the L1 runtime plus the first L1.5 app/server and\nstreaming primitives. It keeps the runtime small and explicit:\n\n- No build step for L1 consumers.\n- No virtual DOM, diff path, hydration runtime, or component rerender loop.\n- Signals are the state boundary.\n- `Async.use(...)` registers app declarations before or after startup.\n- Handlers live in a registry and run through delegated DOM events.\n- Async signals use native `AbortSignal` cancellation and suppress stale async\n  completions.\n- A small scheduler batches signal-driven DOM bindings, lifecycle callbacks,\n  effects, and async refreshes without adding a render loop.\n- Browser and server cache declarations are structurally split.\n- Boundaries can be swapped out of order and rescanned, which keeps server\n  streaming and partial HTML replacement simple.\n\nHigher layers can add JSX lowering, TypeScript, chunk manifests, compiler-owned\nserver/client splits, and intent-first authoring later. They should compile down\nto the same runtime registries and HTML protocol.\n\n## Layers\n\nAsync is designed as layers, so each level can stay useful without forcing the\nnext level on every app.\n\n| Shorthand | Name | Requirement | Purpose |\n| --- | --- | --- | --- |\n| L1 | Runtime bootloader | No build. CDN or direct ESM import. | Signals, async signals, scheduler, handlers, command events, lifecycle pseudo-events, scoped fragments, and boundary swaps. |\n| L1.5 | App/server and streaming bridge | Light server integration. No app compiler required. | `Async.use(...)`, router modes, server function proxy, partial declarations, SSR output, browser activation, split browser/server cache, and streamed boundary patches. |\n| L2 | Build-required authoring and compiler profile | Build step required. | JSX, ESM, and TypeScript authoring, optimizer reports, generated plans, generated registries, chunks, manifests, and future resumability records that lower onto L1 and L1.5 protocols. |\n\nThe package in this repository intentionally focuses on L1 and L1.5. L2 is a\nhigher authoring surface, not an extra runtime requirement for plain HTML apps.\n\n## Install\n\n```bash\npnpm add @async/framework\n```\n\nThe package is ESM-only and supports Node.js 24 and newer for tests, examples,\nand package lifecycle tooling. Browser consumers import ESM directly.\n\n## Vite And Hono\n\nThe Vite entry can run a Hono app as the local development server while keeping\nthe browser runtime at L1. Install the optional Hono dev packages in apps that\nuse this profile:\n\n```bash\npnpm add hono\npnpm add -D vite @hono/vite-dev-server\n```\n\n```js\n// vite.config.js\nimport { defineConfig } from \"vite\";\nimport { asyncFramework } from \"@async/framework/vite\";\n\nexport default defineConfig({\n  plugins: [\n    asyncFramework({\n      layer: 1,\n      server: {\n        entry: \"src/server.js\"\n      },\n      client: {\n        entry: \"src/client.js\",\n        outDir: \"public/static\"\n      }\n    })\n  ]\n});\n```\n\nDuring local development, run Vite:\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"vite\"\n  }\n}\n```\n\n`asyncFramework({ server })` composes `@hono/vite-dev-server`, serves the\ndefault-exported Hono app, and leaves Hono's client reload injection enabled.\nThe Hono entry owns the HTML shell:\n\n```js\n// src/server.js\nimport { Hono } from \"hono\";\n\nconst app = new Hono();\n\napp.get(\"/\", (context) =\u003e {\n  const clientScript = import.meta.env?.DEV ? \"/src/client.js\" : \"/static/client.js\";\n\n  return context.html(`\u003c!doctype html\u003e\n    \u003chtml\u003e\n      \u003cbody async:app\u003e\n        \u003cbutton type=\"button\" on:click=\"increment\"\u003e\n          Count: \u003cspan signal:text=\"count\"\u003e\u003c/span\u003e\n        \u003c/button\u003e\n        \u003cscript type=\"module\" src=\"${clientScript}\"\u003e\u003c/script\u003e\n      \u003c/body\u003e\n    \u003c/html\u003e`);\n});\n\nexport default app;\n```\n\nThe client entry stays ordinary L1 framework code:\n\n```js\n// src/client.js\nimport {\n  Async,\n  createSignal\n} from \"@async/framework/browser\";\n\nAsync.use({\n  signal: {\n    count: createSignal(0)\n  },\n  handler: {\n    increment() {\n      this.signals.update(\"count\", (count) =\u003e count + 1);\n    }\n  }\n});\n\nAsync.start({ root: document });\n```\n\nFor production assets, build only the client bundle:\n\n```json\n{\n  \"scripts\": {\n    \"build\": \"vite build --mode client\"\n  }\n}\n```\n\nThe client build emits into `public/static` by default. Vercel serves\n`public/**` as static assets and runs the Hono app through its native Hono\nsupport when the app is default-exported from an entry such as `src/server.js`.\nThere is no `target` option in this profile yet; production platform behavior\nbelongs to the host until Async adds an explicit build target contract.\n\nSee [`examples/vite-hono`](./examples/vite-hono) for a local Hono app and\nclient build setup. See [`examples/vite-jsx-streaming`](./examples/vite-jsx-streaming)\nfor the Vite JSX optimizer lane that hides bootstrap setup and selects the\nstream runtime slice from Suspense and Reveal intent.\n\n## CDN\n\nThe package ships browser CDN artifacts for UNPKG and can be loaded without a\nbuild step. Use `@latest` for quick prototypes, and pin an exact version in\nproduction:\n\n| File | Format | Use |\n| --- | --- | --- |\n| `browser.js` | ESM | Readable browser module bundle |\n| `browser.min.js` | ESM | Compact browser module bundle |\n| `browser.umd.js` | UMD | Readable script-tag/CommonJS-style bundle |\n| `browser.umd.min.js` | UMD | Compact script-tag/CommonJS-style bundle and default CDN file |\n| `browser.ts` | Bundled browser TypeScript source | TS-aware runtimes and higher-layer tooling |\n| `browser.d.ts` | Type declarations | TypeScript declarations for the browser API |\n| `server.js` | ESM | Server-capable Node.js bundle |\n| `framework.ts` | Bundled server-capable TypeScript source | TS-aware runtimes and higher-layer tooling |\n| `framework.d.ts` | Type declarations | TypeScript declarations for the server-capable API |\n\n```html\n\u003cmain async:container\u003e\n  \u003cbutton type=\"button\" on:click=\"increment\"\u003e+\u003c/button\u003e\n  \u003cstrong signal:text=\"count\"\u003e\u003c/strong\u003e\n\u003c/main\u003e\n\n\u003cscript type=\"module\"\u003e\n  import {\n    Async,\n    createSignal\n  } from \"https://unpkg.com/@async/framework@latest/browser.js\";\n\n  Async.use({\n    signal: {\n      count: createSignal(0)\n    },\n    handler: {\n      increment() {\n        this.signals.update(\"count\", (count) =\u003e count + 1);\n      }\n    }\n  });\n\n  Async.start({ root: document });\n\u003c/script\u003e\n```\n\nFor a plain script tag, use the UMD bundle. In this UMD-only global form,\n`globalThis.Async` is the app hub plus the exported helper functions, with\n`globalThis.AsyncFramework` kept as an alias. Lower-level bootloader code can\ncall `Async.Loader(...)` directly.\n\n```html\n\u003cscript src=\"https://unpkg.com/@async/framework@latest/browser.umd.min.js\"\u003e\u003c/script\u003e\n\u003cscript\u003e\n  Async.use({\n    signal: {\n      count: Async.createSignal(0)\n    },\n    handler: {\n      increment() {\n        this.signals.update(\"count\", (count) =\u003e count + 1);\n      }\n    }\n  });\n\n  Async.start({ root: document });\n\u003c/script\u003e\n```\n\nYou can also use an import map so app code imports `@async/framework` by name:\n\n```html\n\u003cscript type=\"importmap\"\u003e\n{\n  \"imports\": {\n    \"@async/framework\": \"https://unpkg.com/@async/framework@latest/browser.js\"\n  }\n}\n\u003c/script\u003e\n\n\u003cscript type=\"module\"\u003e\n  import {\n    Async,\n    createSignal\n  } from \"@async/framework\";\n\n  Async.use({\n    signal: {\n      count: createSignal(0)\n    },\n    handler: {\n      increment() {\n        this.signals.update(\"count\", (count) =\u003e count + 1);\n      }\n    }\n  });\n\n  Async.start({ root: document });\n\u003c/script\u003e\n```\n\n## Advanced Build-Step Runtime\n\nLayer 1 still works with no build step. A build step can optimize the same\nruntime by emitting SSR HTML plus compact registry descriptors. The browser can\nstart in the document head, apply snapshots, and wait for a root to appear:\n\n```html\n\u003cscript type=\"importmap\"\u003e\n{\n  \"imports\": {\n    \"@async/framework\": \"https://unpkg.com/@async/framework@latest/browser.js\"\n  }\n}\n\u003c/script\u003e\n\n\u003cscript type=\"application/json\" async:snapshot\u003e\n{\n  \"signal\": {\n    \"productId\": \"sku-1\"\n  },\n  \"handler\": {\n    \"cart.add\": { \"url\": \"cart.add.js\" }\n  },\n  \"component\": {\n    \"ProductCard\": { \"url\": \"ProductCard.js\" }\n  },\n  \"asyncSignal\": {\n    \"product.load\": { \"url\": \"product.load.js\" }\n  }\n}\n\u003c/script\u003e\n\n\u003cscript type=\"module\"\u003e\n  import {\n    Async,\n    defineAsyncContainerElement,\n    defineAsyncSuspenseElement,\n    readSnapshot\n  } from \"@async/framework\";\n\n  Async.start({\n    snapshot: readSnapshot(document),\n    registryAssets: { baseUrl: \"_async\" }\n  });\n\n  defineAsyncContainerElement();\n  defineAsyncSuspenseElement();\n\u003c/script\u003e\n```\n\n`Async.start()` defaults to rootless browser startup. It creates registries,\napplies snapshots, and prepares the scheduler/server proxy context without\nscanning DOM. Attach a root later with `Async.attachRoot(root)` or by using\n`\u003casync-container\u003e`:\n\n```html\n\u003casync-container\u003e\n  \u003cbutton type=\"button\" on:click=\"cart.add\"\u003eAdd\u003c/button\u003e\n\u003c/async-container\u003e\n```\n\nDescriptor URLs are relative to a type folder under `registryAssets.baseUrl`.\nThe default is:\n\n```js\n{\n  baseUrl: \"_async\",\n  paths: {\n    component: \"component\",\n    handler: \"handler\",\n    asyncSignal: \"asyncSignal\",\n    partial: \"partial\",\n    route: \"route\"\n  }\n}\n```\n\nSo this descriptor:\n\n```json\n{ \"url\": \"ProductCard.js#ProductCard\" }\n```\n\nresolves as:\n\n```txt\n/_async/component/ProductCard.js#ProductCard\n```\n\nIf `#export` is omitted, Async tries the registry id leaf, then the file\nbasename, then `default`.\n\nFor declarative async boundaries, use `\u003casync-suspense\u003e` or keep using\n`this.suspense(...)` inside components:\n\n```html\n\u003casync-suspense for=\"product.load\"\u003e\n  \u003ctemplate loading\u003eLoading...\u003c/template\u003e\n  \u003ctemplate ready\u003e\n    \u003ch1 signal:text=\"product.load.title\"\u003e\u003c/h1\u003e\n  \u003c/template\u003e\n  \u003ctemplate error\u003e\n    \u003cp signal:text=\"product.load.$error.message\"\u003e\u003c/p\u003e\n  \u003c/template\u003e\n\u003c/async-suspense\u003e\n```\n\nThe build layer can hide `createBoundaryReceiver(...)` setup, but streaming is\nstill explicit boundary patches: boundary id, sequence number, HTML, signal\npatches, and browser-cache patches. Async does not ship a component resume graph.\n\n## Core API\n\nFor npm consumers, `@async/framework` uses conditional exports: browser-aware\ntooling receives the browser entry, while Node receives the server-capable\nentry. Use explicit subpaths when the target matters.\nThe root export also uses condition-specific declarations, so browser-conditioned\nroot imports expose the same API as `@async/framework/browser`; server-only APIs\nremain declared on the Node/server entrypoints.\n\n```js\nimport {\n  Async,\n  Loader,\n  attributeName,\n  asyncSignal,\n  createApp,\n  createCacheRegistry,\n  createComponentRegistry,\n  createLazyRegistry,\n  component,\n  computed,\n  component,\n  createSignal,\n  createHandlerRegistry,\n  createRegistryStore,\n  createScheduler,\n  createServerProxy,\n  createSignalRegistry,\n  defineAsyncContainerElement,\n  defineAsyncSuspenseElement,\n  defineAttributeConfig,\n  defineApp,\n  defineCache,\n  defineRegistrySnapshot,\n  delay,\n  effect,\n  html,\n  readSnapshot,\n  signal\n} from \"@async/framework/browser\";\n```\n\nUse feature subpaths when an app needs the larger browser systems:\n\n```js\nimport { AsyncStream } from \"@async/framework/stream\";\nimport { Async, defineRoute } from \"@async/framework/router\";\nimport { flow, flowSignal } from \"@async/framework/flow\";\n```\n\nServer-only APIs live behind the server entry:\n\n```js\nimport {\n  createRequestContextStore,\n  createServerRegistry\n} from \"@async/framework/server\";\n```\n\n`Loader` is the canonical loader factory. `AsyncLoader` remains as a\ncompatibility alias for older code.\n\n### App Hub\n\n`Async` is an exported app hub singleton. It is not installed on `globalThis`\nunless you assign it there yourself.\n\n```js\nimport {\n  Async,\n  createSignal,\n  defineCache,\n  defineRoute\n} from \"@async/framework/router\";\n\nAsync.use({\n  signal: {\n    count: createSignal(0)\n  },\n  handler: {\n    increment() {\n      this.signals.update(\"count\", (count) =\u003e count + 1);\n    }\n  },\n  server: {\n    async \"products.get\"(id) {\n      return this.cache.getOrSet(`products:${id}`, () =\u003e db.products.get(id));\n    }\n  },\n  route: {\n    \"/products/:id\": defineRoute(\"product.page\")\n  },\n  cache: {\n    browser: {\n      product: defineCache({ ttl: 60_000 })\n    },\n    server: {\n      \"products.get\": defineCache({ ttl: 30_000 })\n    }\n  }\n});\n\nAsync.start({ root: document });\n```\n\nYou can also create isolated app hubs and runtimes:\n\n```js\nconst app = defineApp();\napp.use(\"signal\", { count: createSignal(0) });\napp.use(\"handler\", {\n  increment() {\n    this.signals.update(\"count\", (count) =\u003e count + 1);\n  }\n});\n\nconst runtime = createApp(app, { root: document }).start();\n```\n\nNaming rules:\n\n| Shape | Meaning |\n| --- | --- |\n| `define*` | Declaration or app shape that can be registered before runtime |\n| `create*` | Runtime instance or mutable runtime primitive |\n| `Async.use(...)` | App-level declaration registration |\n| `registry.register(...)` | Low-level registration on a concrete runtime registry |\n| `registry.unregister(...)` | Low-level removal from a concrete runtime registry |\n\nSingular registry keys are canonical: `signal`, `handler`, `server`,\n`partial`, `route`, `component`, and nested `cache.browser` / `cache.server`.\n\n### Registry Inspection\n\n`Async.registry` is the global inspection surface for registered app pieces.\nEvery runtime owns fresh mutable signal and cache state materialized from the\napp declaration store. Concrete registries inside one runtime share that\nruntime's registry view:\n\n```js\nAsync.registry.keys(\"signal\");\nAsync.registry.entries(\"route\");\nAsync.registry.snapshot();\n\nconst runtime = Async.start({ root: document });\n\nruntime.registry.keys(\"handler\");\nruntime.signals.registry === runtime.registry;\nruntime.browser.cache.registry === runtime.registry;\n```\n\nSupported inspection types:\n\n```txt\nsignal\nhandler\nserver\npartial\nroute\ncomponent\ncache.browser\ncache.server\ncache.browser.entries\ncache.server.entries\n```\n\nBrowser runtime inspection exposes server ids as descriptors, not executable\nserver functions, and does not expose server cache contents:\n\n```js\nruntime.registry.keys(\"server\");\nruntime.registry.get(\"server\", \"products.get\");\n// { id: \"products.get\", kind: \"server\" }\n\nruntime.registry.snapshot().entries.server;\n// {}\n```\n\nThe singleton runtime is intentionally internal. Use app-level methods for\nglobal lifecycle work, and use `inspectRuntime()` for diagnostics:\n\n```js\nAsync.attachRoot(document.body);\nAsync.applySnapshot(snapshot);\n\nAsync.inspectRuntime();\n// {\n//   active: true,\n//   started: true,\n//   destroyed: false,\n//   target: \"browser\",\n//   roots: { count: 1, roots: [...] },\n//   loader: { ready: true, pending: 0, root: document.body },\n//   router: false\n// }\n```\n\n`Async.runtime` is not public API. If you need direct instance ownership, keep\nthe handle returned from `Async.start(...)` or `createApp(...).start()`.\n\n### Signals\n\n```js\nconst signals = createSignalRegistry();\n\nsignals.register(\"count\", createSignal(0));\nsignals.register(\"products\", createSignal([]));\n\nsignals.get(\"count\");\nsignals.set(\"count\", 1);\nsignals.update(\"count\", (count) =\u003e count + 1);\nsignals.subscribe(\"count\", (count) =\u003e console.log(count));\nsignals.ref(\"count\").value;\nsignals.unregister(\"count\");\n```\n\nInitializer maps are supported:\n\n```js\nconst signals = createSignalRegistry({\n  count: createSignal(0),\n  products: createSignal([])\n});\n```\n\nNested paths read through the first registered signal id:\n\n```js\nsignals.register(\"product\", createSignal({ title: \"Keyboard\" }));\nsignals.get(\"product.title\");\nsignals.set(\"product.title\", \"Headphones\");\n```\n\n`signal(...)` remains a compatibility alias for `createSignal(...)`.\n\n### Scheduler\n\nThe scheduler is the Layer 1.5 ordering engine. Signal writes are still\nsynchronous:\n\n```js\nsignals.set(\"count\", 3);\nsignals.get(\"count\");\n// 3\n```\n\nDOM bindings, component lifecycle callbacks, component effects, and async signal\nrefreshes are scheduled through deterministic phases:\n\n```txt\nbinding -\u003e lifecycle -\u003e effect -\u003e async -\u003e post\n```\n\nBrowser runtimes use a microtask scheduler by default. Server runtimes use a\nmanual scheduler and drain it during `runtime.render(...)`.\n\n```js\nimport {\n  createScheduler\n} from \"@async/framework\";\n\nconst scheduler = createScheduler({\n  strategy: \"manual\"\n});\n\nconst runtime = Async.start({\n  root: document,\n  scheduler\n});\n\nsignals.set(\"count\", 1);\nawait scheduler.flush();\n```\n\nMost apps do not need to call the scheduler directly. It is exposed for tests,\ncustom runtimes, streaming receivers, and higher layers that need explicit flush\nboundaries.\n\n### Async Signals\n\nAsync signals add loading state, error state, versions, refresh, and cancel to a\nnormal signal value.\n\n```js\nconst signals = createSignalRegistry({\n  productId: createSignal(\"sku-1\")\n});\n\nconst product = signals.asyncSignal(\"product\", async function () {\n  const id = this.signals.get(\"productId\");\n  const response = await fetch(`/api/products/${id}`, {\n    signal: this.abort\n  });\n\n  return response.json();\n});\n```\n\nThe async function context includes:\n\n| Field | Purpose |\n| --- | --- |\n| `this.signals` | The signal registry |\n| `this.id` | Current async signal id |\n| `this.version` | Run version |\n| `this.abort` | Native `AbortSignal` with non-enumerable `cancel(reason?)` |\n| `this.scheduler` | Current runtime scheduler |\n| `this.refresh()` | Start a new run |\n\n`this.abort` can be passed directly to `fetch` or to `delay`:\n\n```js\nawait delay(250, this.abort);\n```\n\nIf a dependency read through `this.signals.get(...)` changes, the async signal\nreruns and the previous run is aborted.\n\nDependency reads are captured while the async signal function starts running.\nRead signal dependencies before the first `await`; reads that happen later are\nordinary reads and do not create refresh subscriptions.\n\n## HTML Protocol\n\nLoader scans regular HTML attributes:\n\n| Attribute | Behavior |\n| --- | --- |\n| `async:container` | Marks a scannable app root |\n| `on:click=\"selectProduct\"` | Delegated command event |\n| `on:submit=\"preventDefault; save\"` | Sequential command chain |\n| `on:click=\"server.cart.add(productId)\"` | Server command with signal args |\n| `on:attach=\"setup\"` | Component root attach lifecycle pseudo-event |\n| `on:visible=\"trackView\"` | Component root visible lifecycle pseudo-event |\n| `on:intersect=\"trackSection\"` | Continuous intersection lifecycle pseudo-event |\n| `intersect:threshold=\"0,0.5,1\"` | Intersection threshold option for `on:intersect` |\n| `intersect:root-margin=\"-20% 0px -55% 0px\"` | Intersection root margin option for `on:intersect` |\n| `intersect:once=\"true\"` | Disconnect `on:intersect` after the first intersecting entry |\n| `signal:text=\"product.title\"` | Text binding |\n| `signal:value=\"productId\"` | Form value binding with writeback |\n| `signal:attr:disabled=\"product.$loading\"` | Attribute binding |\n| `signal:prop:checked=\"selected\"` | DOM property binding |\n| `class:selected=\"selected\"` | Class toggle from a signal path |\n| `signal:class=\"buttonClasses\"` | Class set from a signal value: string, object, or array |\n| `async:boundary=\"product\"` | Async or streamed replacement boundary |\n| `async:loading=\"product\"` | Boundary loading template |\n| `async:ready=\"product\"` | Boundary ready template |\n| `async:error=\"product\"` | Boundary error template |\n\n```html\n\u003csection async:boundary=\"product\"\u003e\n  \u003ctemplate async:loading=\"product\"\u003e\n    \u003cp\u003eLoading...\u003c/p\u003e\n  \u003c/template\u003e\n  \u003ctemplate async:ready=\"product\"\u003e\n    \u003ch1 signal:text=\"product.title\"\u003e\u003c/h1\u003e\n  \u003c/template\u003e\n  \u003ctemplate async:error=\"product\"\u003e\n    \u003cp signal:text=\"product.$error.message\"\u003e\u003c/p\u003e\n  \u003c/template\u003e\n\u003c/section\u003e\n```\n\nThe default prefixes are `async:`, `signal:`, and `on:`. You can switch to\ndata attributes when a host needs that shape:\n\n```js\nAsync.start({\n  root: document,\n  attributes: {\n    async: \"data-async-\",\n    class: \"data-class-\",\n    intersect: \"data-intersect-\",\n    signal: \"data-signal-\",\n    on: \"data-on-\"\n  }\n});\n```\n\nThat maps to `data-async-container`, `data-on-click=\"save\"`,\n`data-signal-text=\"product.title\"`, `data-class-selected=\"selected\"`, and\n`data-intersect-threshold=\"0.5\"`.\n\nInside `html` templates, signal refs can be passed directly to binding\nattributes:\n\n```js\nconst title = this.signal(\"Keyboard\");\nconst disabled = this.signal(false);\nconst checked = this.signal(true);\n\nreturn html`\n  \u003ch1 signal:text=\"${title}\"\u003e\u003c/h1\u003e\n  \u003cbutton signal:attr:disabled=\"${disabled}\"\u003eSave\u003c/button\u003e\n  \u003cinput type=\"checkbox\" signal:prop:checked=\"${checked}\"\u003e\n`;\n```\n\nUse `signal:value` for form value binding with writeback. Use `signal:prop:*`\nwhen you only need one-way DOM property updates.\n\nNamed class toggles use their own top-level namespace:\n\n```html\n\u003cbutton\n  class=\"button\"\n  class:selected=\"selected\"\n\u003e\n  Add\n\u003c/button\u003e\n```\n\nAggregate class binding uses `signal:class`. It reads the current signal value\nand accepts strings, objects, and arrays:\n\n```js\nAsync.use({\n  signal: {\n    buttonClasses: createSignal([\n      \"button-primary\",\n      { selected: true, disabled: false },\n      [\"compact\"]\n    ])\n  }\n});\n```\n\n```html\n\u003cbutton signal:class=\"buttonClasses\"\u003eAdd\u003c/button\u003e\n```\n\nInside `html` templates, `signal:class` can also receive objects or arrays\ndirectly. Signal refs inside the object or array are tracked:\n\n```js\nconst selected = this.signal(\"selected\", false);\nconst tone = this.signal(\"tone\", \"primary\");\n\nreturn html`\n  \u003carticle signal:class=\"${[\"card\", tone, { selected }]}\"}\u003e\n    ...\n  \u003c/article\u003e\n`;\n```\n\nFor component-local state that does not need a stable public id, omit the name.\nThe signal is still registered under the component scope:\n\n```js\nconst selected = this.signal(false);\nconst tone = this.signal(\"primary\");\n\nreturn html`\n  \u003carticle signal:class=\"${[\"card\", selected, tone]}\"\u003e\n    ...\n  \u003c/article\u003e\n`;\n```\n\n`value=\"${signalRef}\"` in an `html` template is equivalent to adding\n`signal:value` for that signal. It writes back on input/change:\n\n```js\nconst productId = this.signal(\"productId\", \"sku-1\");\n\nreturn html`\u003cinput value=\"${productId}\"\u003e`;\n```\n\n`signal:class:selected=\"selected\"` remains supported as a compatibility alias,\nbut new examples should use `class:selected`. The parser-safe top-level\naggregate form `class:=\"buttonClasses\"` also remains supported.\n\n### Command Events\n\n`on:*` works with any native DOM event name. `on:attach` and `on:visible` are\nreserved component lifecycle pseudo-events with cleanup support. `on:mount`\nremains as a compatibility alias for `on:attach` and warns when used.\nWhen an `on:attach` handler installs listeners, observers, timers, or DOM\nhelpers, return a cleanup function. Boundary swaps destroy the old subtree and\nrun returned cleanup functions before inserting the next fragment.\n\nCommand chains use semicolons and are awaited sequentially:\n\n```html\n\u003cform on:submit=\"preventDefault; server.products.save(productId, $form)\"\u003e\n  \u003cinput name=\"title\"\u003e\n  \u003cbutton\u003eSave\u003c/button\u003e\n\u003c/form\u003e\n```\n\nPlain commands resolve through the handler registry. Built-ins are registered by\ndefault:\n\n```txt\nprevent\npreventDefault\nstopPropagation\nstopImmediatePropagation\n```\n\n`server.\u003cid\u003e(...)` resolves through the server registry or client proxy. Bare\narguments read signals. `$*` arguments read event locals:\n\n| Argument | Value |\n| --- | --- |\n| `productId` | `signals.get(\"productId\")` |\n| `cart.quantity` | `signals.get(\"cart.quantity\")` |\n| `$value` | Current element value |\n| `$checked` | Current element checked state |\n| `$form` | Current form as a plain object |\n| `$dataset` | Current element dataset as a plain object |\n| `$event` | Raw DOM event, client-only |\n| `$el` | Current element, client-only |\n\n`$event` and `$el` are intentionally not serializable and cannot be passed to\n`server.*(...)` commands.\n\nInline commands are not JavaScript. There is no `eval`, assignment, branching,\narithmetic, or inline `await`. Complex logic belongs in a registered handler:\n\n```js\nhandlers.register(\"addToCart\", async function () {\n  const productId = this.signals.get(\"productId\");\n  const result = await this.server.cart.add(productId);\n  this.signals.set(\"cart\", result.cart);\n});\n```\n\n### Server Calls\n\nServer registries run locally on the server. Browser proxies use an explicit\ntransport supplied by the app, so network access is opt-in. Both expose the same\ndotted call shape.\n\n```js\nimport {\n  createServerRegistry\n} from \"@async/framework/server\";\n\nconst server = createServerRegistry({\n  \"cart.add\"(productId, quantity) {\n    return {\n      __async_server_result__: 1,\n      value: { ok: true },\n      signals: {\n        cartCount: 3\n      }\n    };\n  }\n});\n```\n\nClient proxy:\n\n```js\nimport {\n  createServerProxy\n} from \"@async/framework/browser\";\n\nconst server = createServerProxy({\n  endpoint: \"/__async/server\",\n  transport: httpTransport,\n  signals,\n  loader,\n  router\n});\n\nawait server.cart.add(\"sku-1\", 2);\n```\n\nProxy requests validate their `args`, default `input`, and selected signal\nvalues before transport runs. Supported values are `null`, booleans, strings,\nfinite numbers, dense arrays, and plain objects composed from those values.\nValues that JSON would silently change or drop, such as `undefined`, functions,\nsymbols, `Map`, `Set`, `Date`, sparse arrays, class instances, non-finite\nnumbers, circular objects, file-like values, streams, buffers, and typed arrays\nare rejected with a path to the invalid value.\n\nServer responses can include `value`, `signals`, `boundary`, `html`, `redirect`,\nor `error`. Signal patches are applied before boundary swaps and redirects.\nNamespace calls such as `server.cart.add(...)` return the unwrapped `value`.\n\nWhen an async signal calls a server namespace function, the framework passes the\nactive abort signal through proxy calls. Returned server effects such as\n`signals`, `cache.browser`, `boundary/html`, and `redirect` are applied before\nthe async signal stores the unwrapped `value`.\n\n### Router And Partials\n\nAsync includes a built-in router behind the `@async/framework/router` browser\nsubpath. Use it for URL matching, route params, hash-based static-host routes,\nsame-origin link and GET form interception, route partial swaps, and route-only\n`router.*` state.\n\n```js\nimport { Async, defineRoute } from \"@async/framework/router\";\n```\n\nFor app code, register routes and partials through the app registry:\n\n- `Async.use({ route, partial })` plus `Async.start({ mode, boundary })` for app\n  hub setup.\n\nMost apps should start at that layer and only move down when they need a more\nspecific routing shape:\n\n| If the app is doing this | Use this pattern |\n| --- | --- |\n| Client-rendered route pages | `Async.use({ route, partial })` and `Async.start({ mode: \"csr\" })` |\n| Static-host routes | Add `urlMode: \"hash\"` and link to `#/path` routes |\n| Existing SSR or static HTML should stay visible until navigation | Use `mode: \"spa\"` |\n| Buttons, handlers, redirects, or preloads need routing | Use `Async.router.navigate(...)` or `Async.router.prefetch(...)` |\n| A dashboard renders from URL state | Use `mode: \"signals\"` with `Async.router.loader.swap(...)` |\n| Several route-driven boundaries refresh independently | Use `Async.router.loader.defineRefreshPlan(...)` and `refresh(...)` |\n| Code needs the router object itself | Await `Async.router.ready()` |\n| Navigation belongs to the server or separate documents | Use `mode: \"ssr\"` or `mode: \"mpa\"` |\n| A custom runtime already owns materialized registries | Use `createRouter(...)` directly |\n\n`createRouter(...)` is lower-level custom runtime wiring for already-materialized\nruntime registries. It is not a second registration API, and it starts\nimmediately when called.\n\nRouter pieces:\n\n| Piece | Purpose |\n| --- | --- |\n| `Async.use({ route, partial })` | Registers URL patterns and fragment renderers |\n| `defineRoute(...)` | Creates route records that point to partials or metadata |\n| `Async.start({ mode, boundary })` | Materializes the runtime router from app declarations |\n| `Async.router` | Queues or runs navigation, history handling, matching, and prefetch |\n| `async:boundary=\"route\"` | Receives rendered route partial HTML in `csr` and `spa` modes |\n| `router.*` signals | Publish path, params, query, matched route, pending state, and errors |\n\nPartials are server-rendered fragment functions. They return HTML, `html`\ntemplates, DOM fragments, or a response envelope.\n\n```js\nAsync.use({\n  partial: {\n    \"product.page\": async function ({ id }) {\n      const product = await this.server.products.get(id);\n      return html`\u003ch1\u003e${product.title}\u003c/h1\u003e`;\n    }\n  }\n});\n```\n\nThe router swaps route partials into a boundary. `csr` starts from an empty\nroute boundary, renders the current route partial locally, then keeps future\nnavigation local too:\n\n```js\nAsync.use({\n  partial: {\n    home() {\n      return html`\u003ch1\u003eHome\u003c/h1\u003e`;\n    },\n    \"product.page\"({ id }) {\n      return html`\u003ch1\u003eProduct ${id}\u003c/h1\u003e`;\n    }\n  },\n  route: {\n    \"/\": defineRoute(\"home\"),\n    \"/products/:id\": defineRoute(\"product.page\")\n  }\n});\n\nAsync.start({\n  mode: \"csr\",\n  boundary: \"route\",\n  root: document\n});\n```\n\n`route(...)` remains a compatibility alias for `defineRoute(...)`.\n\nRoute patterns support static paths, params, and wildcard fallback:\n\n```js\nAsync.use({\n  route: {\n    \"/\": defineRoute(\"home.page\"),\n    \"/products/:id\": defineRoute(\"product.page\"),\n    \"/docs/:section/:page\": defineRoute(\"docs.page\"),\n    \"*\": defineRoute(\"notFound.page\")\n  }\n});\n```\n\nThe router publishes params and query strings through signals:\n\n```txt\n/products/sku-1?tab=reviews\nrouter.path   -\u003e \"/products/sku-1\"\nrouter.params -\u003e { id: \"sku-1\" }\nrouter.query  -\u003e { tab: \"reviews\" }\n```\n\nRoutes that only drive URL-backed state can use route metadata without a\npartial. Use `mode: \"signals\"` for dashboards or app shells that already render\nfrom state:\n\n```js\nAsync.use({\n  route: {\n    \"/pbi\": defineRoute({ render: \"none\", meta: { page: \"pbi\" } }),\n    \"/fy26\": defineRoute({ render: \"none\", meta: { page: \"fy26\" } })\n  }\n});\n\nAsync.start({\n  mode: \"signals\",\n  urlMode: \"hash\"\n});\n```\n\nRouter modes:\n\n| Mode | Initial route | Later navigation | Use when |\n| --- | --- | --- | --- |\n| `csr` | Client renders local partial into boundary | Client renders local partial and swaps | A no-build page owns route content on the client |\n| `spa` | Existing HTML may already contain route | Client renders local partial and swaps | SSR or static HTML should stay visible until navigation |\n| `signals` | Existing HTML stays mounted | Updates `router.*` signals and history only | A shell renderer reacts to URL state itself |\n| `ssr` | Server-rendered document plus snapshot activation | Browser navigates normally | Navigation belongs to the server |\n| `mpa` | Any document source | Browser navigates normally | Traditional multi-page navigation |\n\nIn `signals` mode, route changes update `router.url`, `router.path`,\n`router.params`, `router.query`, `router.route`, `router.pending`, and\n`router.error` without rendering partials or swapping boundaries.\n\nClient navigation modes intercept same-origin links, GET forms, browser\nback/forward, and route hashes such as `#/products/sku-1`. They do not intercept\nexternal links, downloads, modified clicks, non-GET forms, or plain section\nanchors such as `#quickstart`.\n\nCSR startup can use an empty route boundary:\n\n```html\n\u003cmain async:container\u003e\n  \u003cnav\u003e\n    \u003ca href=\"/\"\u003eHome\u003c/a\u003e\n    \u003ca href=\"/products/sku-1\"\u003eProduct\u003c/a\u003e\n  \u003c/nav\u003e\n\n  \u003csection async:boundary=\"route\"\u003e\u003c/section\u003e\n\u003c/main\u003e\n```\n\nRouter state lives under `router.*` signals:\n\n```txt\nrouter.url\nrouter.path\nrouter.params\nrouter.query\nrouter.route\nrouter.pending\nrouter.error\n```\n\nProgrammatic navigation uses the same matcher and history handling:\n\n```js\nawait Async.router.navigate(\"/products/sku-1\");\nawait Async.router.navigate(\"/products/sku-2\", { replace: true });\nawait Async.router.prefetch(\"/products/sku-3\");\n```\n\nWhen a partial envelope owns an `html` key with `undefined`, the router treats\nit as no route HTML replacement and leaves the active boundary intact. Use\n`html: \"\"` to intentionally clear the route boundary.\n\nThe full router guide lives in `docs/runtime/router-partials.md` and the\nrunnable example is `examples/router`.\n\n### Cache\n\nCache declarations are split by runtime target:\n\n```js\nAsync.use({\n  cache: {\n    browser: {\n      product: defineCache({ ttl: 60_000 })\n    },\n    server: {\n      \"products.get\": defineCache({ ttl: 30_000 })\n    }\n  },\n  server: {\n    async \"products.get\"(id) {\n      return this.cache.getOrSet(`products:${id}`, () =\u003e db.products.get(id));\n    }\n  }\n});\n```\n\nBrowser handlers and browser async signals receive `runtime.browser.cache`.\nServer functions and server partials receive `runtime.server.cache`. Server\ncache config and contents are never serialized to the browser. Browser cache is\nseeded only by explicit SSR response data.\n\nRuntime cache registries support:\n\n```js\ncache.register(\"product\", defineCache({ ttl: 60_000 }));\ncache.get(\"product:sku-1\");\ncache.set(\"product:sku-1\", product);\nawait cache.getOrSet(\"product:sku-1\", () =\u003e loadProduct());\ncache.delete(\"product:sku-1\");\ncache.clear(\"product:\");\n```\n\n### SSR Flow\n\nSSR uses related app definitions: a server runtime with server functions,\nserver cache, partials, and route rendering; and a browser runtime with DOM\nhandlers, browser cache, signals, and usually a server proxy.\n\n```js\nconst serverRuntime = createApp(serverApp, {\n  target: \"server\",\n  request\n});\n\nconst response = await serverRuntime.render(\"/products/123\");\n```\n\n`runtime.render(url)` returns:\n\n```js\n{\n  html,\n  status,\n  signals,\n  cache: {\n    browser: {}\n  }\n}\n```\n\nThe returned HTML includes a route boundary plus a JSON snapshot:\n\n```html\n\u003csection async:boundary=\"route\"\u003e\n  \u003c!-- server-rendered route partial --\u003e\n\u003c/section\u003e\n\u003cscript type=\"application/json\" async:snapshot\u003e{}\u003c/script\u003e\n```\n\nBrowser activation scans the existing HTML and attaches events. It does not\nhydrate, diff, patch, rerender, or fetch route fragments:\n\n```js\ncreateApp(browserApp, {\n  root: document\n}).start();\n```\n\nIf browser handlers or async signals need server commands, pass a server proxy\nwith an explicit transport:\n\n```js\ncreateApp(browserApp, {\n  root: document,\n  server: createServerProxy({\n    endpoint: \"/__async/server\",\n    transport: httpTransport\n  })\n}).start();\n```\n\nIf an `async:snapshot` script is present under the root or document,\n`createApp(...)` reads it automatically. You can also inspect it directly:\n\n```js\nconst snapshot = readSnapshot(document);\n```\n\n## Loader Bootstrap Queue\n\n`Async.loader` is a promise-returning facade for script-friendly loader work\nthat may run before the app has attached a root. Calls to `scan`, `swap`, and\n`mount` queue until `Async.start({ root })` or `Async.attachRoot(root)` creates\nthe concrete runtime loader:\n\n```js\nAsync.use(\"handler\", {\n  selectProduct() {\n    this.signals.set(\"selected\", true);\n  }\n});\n\nconst swapped = Async.loader.swap(\n  \"route\",\n  `\u003cbutton type=\"button\" on:click=\"selectProduct\"\u003eSelect\u003c/button\u003e`\n);\n\nAsync.start({ root: document, router: false });\nawait swapped;\n```\n\n`Async.loader.ready()` resolves with the concrete `runtime.loader`.\n`Async.loader.inspect()` reports whether a loader is ready and how many loader\noperations are still pending. The concrete `runtime.loader` remains\nsynchronous for routers, boundary receivers, and server-result application.\n\n## Components\n\nComponents are scoped fragment functions. They return strings or `html`\ntemplates; Loader inserts and scans the result. There is no virtual node\ntype and no rerender loop.\n\n```js\nconst Toggle = component(function Toggle() {\n  const selected = this.signal(false);\n  const attach = this.handler(\"attach\", function ({ element }) {\n    element.dataset.attached = \"true\";\n  });\n  const visible = this.handler(\"visible\", function ({ element }) {\n    element.dataset.visible = \"true\";\n  });\n\n  return html`\n    \u003cbutton\n      type=\"button\"\n      on:attach=\"${attach}\"\n      on:visible=\"${visible}\"\n      on:click=\"${this.handler(function () {\n        selected.update((value) =\u003e !value);\n      })}\"\n      class:selected=\"${selected}\"\n      signal:class=\"${[\"toggle\", { active: selected }]}\"\n      signal:attr:aria-pressed=\"${selected}\"\n    \u003e\n      Toggle\n    \u003c/button\u003e\n  `;\n});\n\nconst loader = Loader({ root: document });\nloader.mount(document.querySelector(\"#app\"), Toggle);\n```\n\nComponent helpers:\n\n| Helper | Behavior |\n| --- | --- |\n| `this.signal(name, initial)` | Scoped named get-or-create signal |\n| `this.signal(initial)` | Generated scoped local signal |\n| `this.computed(name, fn)` | Scoped computed signal |\n| `this.asyncSignal(name, fn)` | Scoped async signal |\n| `this.effect(fn)` | Scoped effect with cleanup |\n| `this.handler(name, fn)` | Scoped named handler registry entry |\n| `this.handler(fn)` | Generated scoped handler registry entry |\n| `this.render(Component, props, children?)` | Child fragment rendering with optional default children |\n| `this.slot(Component, propsOrFn)` | Child component outlet using an `on:attach` target |\n| `this.suspense(signalRef, views)` | Async boundary template helper |\n| `this.on(event, fn)` | Fragment lifecycle fallback for `attach`, `visible`, and `destroy` |\n| `this.onAttach(fn)` | Fragment attach lifecycle fallback |\n| `this.onMount(fn)` | Compatibility alias for `this.onAttach(fn)` that warns when used |\n| `this.onVisible(fn)` | Compatibility alias for `this.on(\"visible\", fn)` |\n| `this.on(\"intersect\", options?, fn)` | Continuous intersection lifecycle for the mounted component scope |\n| `this.intersect(element, options?, fn)` | Component-owned continuous intersection observer for a direct element |\n\n`this.suspense(...)` is sugar for Loader boundaries:\n`asyncSignal + async:boundary + async:* templates`. It emits only templates. The\ncaller owns the boundary element, and the loader chooses the loading, ready, or\nerror template from the async signal status.\n\n```js\nconst Product = component(function Product() {\n  const product = this.asyncSignal(\"product\", async function () {\n    return this.server.products.get(\"sku-1\");\n  });\n\n  return html`\n    \u003carticle async:boundary=\"${product.id}\"\u003e\n      ${this.suspense(product, {\n        loading() {\n          return html`\u003cp\u003eLoading...\u003c/p\u003e`;\n        },\n        ready(product) {\n          return html`\u003ch1 signal:text=\"${product.id}.title\"\u003e\u003c/h1\u003e`;\n        },\n        error(product) {\n          return html`\u003cp signal:text=\"${product.id}.$error.message\"\u003e\u003c/p\u003e`;\n        }\n      })}\n    \u003c/article\u003e\n  `;\n});\n```\n\nThe shorthand form treats the callback as the ready template:\n\n```js\nthis.suspense(product, (product) =\u003e html`\n  \u003ch1 signal:text=\"${product.id}.title\"\u003e\u003c/h1\u003e\n`);\n```\n\n`this.suspense(...)` is not React Suspense. It does not throw promises,\nhydrate, diff, rerender a component tree, or emit a wrapper element.\n\nDefault children are a scoped fragment owned by the framework. Pass them as the\nthird `this.render(...)` argument, then interpolate `children` in the child\ncomponent:\n\n```js\nconst Card = component(function Card({ title, children }) {\n  return html`\n    \u003carticle\u003e\n      \u003ch2\u003e${title}\u003c/h2\u003e\n      ${children}\n    \u003c/article\u003e\n  `;\n});\n\nconst Page = component(function Page() {\n  return html`\n    ${this.render(Card, { title: \"Status\" }, html`\n      \u003cp\u003eReady\u003c/p\u003e\n    `)}\n  `;\n});\n```\n\nChildren can also be lazy when the caller supplies a factory. The factory runs\nonly if the child component interpolates `children`, and any nested components\nor handlers created while rendering the fragment are cleaned up with the\nconsuming component fragment:\n\n```js\nthis.render(Card, { title: \"Status\" }, function children() {\n  return html`\u003cp\u003e${this.render(Badge, { label: \"Live\" })}\u003c/p\u003e`;\n});\n```\n\nNo-build HTML component hosts use an explicit inert template for default\nchildren:\n\n```html\n\u003csection async:component=\"Card\"\u003e\n  \u003ctemplate async:children\u003e\n    \u003cp\u003eReady\u003c/p\u003e\n  \u003c/template\u003e\n\u003c/section\u003e\n```\n\nThe loader captures only a direct child `\u003ctemplate async:children\u003e` before\nmounting the registered component. Ordinary host content is not implicitly\ncaptured, and the template content is inserted and scanned only if the\ncomponent interpolates `children`.\n\nDo not pass `children` in the props object when also using the third argument.\nDefault children are consumed once by interpolation; use `this.slot(...)` for\npost-mount replacement and use ordinary props when the child needs data from the\ncaller.\n\nComponent-scoped signals and handlers are unregistered when the mounted\nfragment is destroyed. `loader.swap(...)` cleans up old DOM bindings and mounted\ncomponent fragments under the swapped boundary before inserting the new HTML.\n\nLifecycle fallbacks are scoped to the component fragment that registered them.\nA component mounted directly with `loader.mount(target, Component)` receives the\nmount target. A child rendered through `this.render(Child)` receives its own\nsingle element root when one exists. If the child returns text or multiple root\nnodes, the fallback target is the nearest containing element. `this.onVisible`\nand `this.on(\"intersect\", ...)` observe the same scoped target.\n\nPut component lifecycle on the component root element when there is one:\n\n```js\nconst attach = this.handler(\"attach\", function ({ element }) {\n  element.dataset.attached = \"true\";\n});\nconst visible = this.handler(\"visible\", function ({ element }) {\n  element.dataset.visible = \"true\";\n});\n\nreturn html`\u003carticle on:attach=\"${attach}\" on:visible=\"${visible}\"\u003e...\u003c/article\u003e`;\n```\n\nIf a component returns text or multiple root nodes, use the scoped fallback:\n\n```js\nthis.on(\"attach\", (target) =\u003e {\n  target.dataset.attached = \"true\";\n});\n\nthis.on(\"destroy\", () =\u003e {\n  // Clean up fragment-scoped resources.\n});\n```\n\n`on:visible` is defined as a component lifecycle pseudo-event. It runs once when\nthe component root first becomes visible. Lifecycle events do not drive\ncomponent rerenders.\n\nUse `on:intersect` when markup should receive continuous intersection updates\nthrough a registered handler:\n\n```html\n\u003csection\n  on:intersect=\"trackSection\"\n  intersect:threshold=\"0,0.25,0.5,0.75,1\"\n  intersect:root-margin=\"-20% 0px -55% 0px\"\n\u003e\n  ...\n\u003c/section\u003e\n```\n\nThe handler receives `element`, `entry`, `entries`, `observer`,\n`isIntersecting`, `intersectionRatio`, and `unsupported`. Custom roots are not\nselector-based; use `this.intersect(...)` with a direct root element when a\ncustom observer root is needed.\n\nUse `this.on(\"intersect\", ...)` when a component needs continuous visibility\nstate:\n\n```js\nconst Card = component(function Card() {\n  const visible = this.signal(false);\n\n  this.on(\"intersect\", { threshold: 0.5 }, ({ isIntersecting }) =\u003e {\n    visible.set(isIntersecting);\n  });\n\n  return html`\u003carticle class:visible=\"${visible}\"\u003e...\u003c/article\u003e`;\n});\n```\n\nUse `this.intersect(...)` with a direct element when a parent owns scroll-spy or\nactive-section state:\n\n```js\nconst Section = component(function Section({ id, observeSection }) {\n  const attach = this.handler(\"attach\", function ({ element }) {\n    return observeSection(id, element);\n  });\n\n  return html`\u003csection on:attach=\"${attach}\"\u003e\u003ch2\u003e${id}\u003c/h2\u003e\u003c/section\u003e`;\n});\n\nconst Page = component(function Page() {\n  const active = this.signal(\"intro\");\n  const ratios = new Map();\n  const options = {\n    rootMargin: \"-20% 0px -55% 0px\",\n    threshold: [0, 0.25, 0.5, 0.75, 1]\n  };\n\n  const observeSection = (id, element) =\u003e this.intersect(element, options, ({ entry }) =\u003e {\n    ratios.set(id, entry.isIntersecting ? entry.intersectionRatio : 0);\n    const best = [...ratios.entries()].sort((a, b) =\u003e b[1] - a[1])[0];\n    active.set(best?.[0] ?? id);\n  });\n\n  return html`\n    \u003cnav signal:text=\"${active}\"\u003e\u003c/nav\u003e\n    ${this.render(Section, { id: \"intro\", observeSection })}\n    ${this.render(Section, { id: \"runtime\", observeSection })}\n  `;\n});\n```\n\n## Streaming\n\nOut-of-order HTML can target a boundary and keep delegated handlers working:\n\n```js\nloader.swap(\n  \"product\",\n  `\n    \u003carticle\u003e\n      \u003ch1 signal:text=\"product.title\"\u003e\u003c/h1\u003e\n      \u003cbutton type=\"button\" on:click=\"selectProduct\"\u003eSelect\u003c/button\u003e\n    \u003c/article\u003e\n  `\n);\n```\n\n`swap(boundaryId, fragmentOrTemplate, options?)` replaces the boundary contents\nand rescans inserted content by default. For large stable shells that refresh\nfrom local state, pass `strategy: \"morph\"` to preserve matching DOM nodes while\nupdating changed text, attributes, and children.\n\nUse config-first `swap(...)` for the advanced variants:\n\n```js\nloader.swap({ boundary: \"view\", html });\nloader.swap({ type: \"ifChanged\", boundary: \"view\", html: renderView });\nloader.swap({ type: \"many\", updates: { filters, timeline }, scan: \"once\" });\nloader.swap({ type: \"many\", ifChanged: true, updates, scan: \"once\" });\n```\n\n`type: \"ifChanged\"` skips cleanup, DOM replacement, and rescanning when the\nnext rendered HTML matches the previous swap for that boundary. The render\nfunction form receives `{ boundary, boundaryId, loader, signals, handlers,\nserver, router, cache, scheduler }`.\n\n`type: \"many\"` applies several boundary replacements before activation.\n`updates` can be an object, `Map`, or iterable of `[boundaryId, html]` entries.\nEach entry may also be `{ html, strategy, attach }` for per-boundary morph or\nattach behavior. Pass `ifChanged: true` to skip unchanged entries inside the\nbatch. `scan: \"once\"` defers scanning until every update has been inserted, which\navoids interleaving cleanup/scan work across multiple same-tick refreshes.\n\nUse `loader.defineRefreshPlan(...)` and `loader.refresh(scope)` for declarative\nscope-to-boundary orchestration in signal-router dashboards:\n\n```js\nloader.defineRefreshPlan({\n  timeline: {\n    boundaries: [\"view-timeline\"],\n    render({ signals }) {\n      return {\n        \"view-timeline\": { html: buildTimeline(signals), strategy: \"morph\" }\n      };\n    }\n  },\n  chrome: [\"app-chrome\", \"view-filters\"]\n});\n\nloader.refresh(\"timeline\");\nloader.refresh(\"chrome\", { \"app-chrome\": chromeHtml, \"view-filters\": filtersHtml });\n```\n\nUse `type: \"bind\"` when local signal state owns a large region. The render\nfunction runs once, tracks signal reads made while rendering, and schedules one\nunchanged-aware refresh for same-tick signal changes. Pass `deps: [...]` to\nsubscribe only to explicit signal paths instead of every read inside `render`.\nIt returns a cleanup function.\n\n```js\nconst stopTimeline = loader.swap({\n  type: \"bind\",\n  boundary: \"view-timeline\",\n  deps: [\"demoState.settings.rangeMode\"],\n  render({ signals }) {\n    const view = buildTimelineView(signals.get(\"timeline.filters\"));\n    return html`\u003csection\u003e${view.items.map(renderTimelineItem)}\u003c/section\u003e`;\n  },\n  strategy: \"morph\"\n});\n```\n\nThe `strategy` option controls how the boundary changes:\n\n| Option | Behavior |\n| --- | --- |\n| `replace` | Default. Clean up all existing children, replace them, and activate the inserted subtree. |\n| `morph` | Reconcile matching children by tag and stable identity, preserving unchanged nodes and cleaning up removed or replaced nodes. |\n\nThe `attach` option applies to morph swaps:\n\n| Option | Behavior |\n| --- | --- |\n| `preserve` | Default. Preserved `on:attach` nodes keep their attach handlers across morph. |\n| `rebind` | Preserved `on:attach` nodes rerun attach handlers after morph. |\n\nMorph matching uses `async:key`, `data-key`, or `id` when present. Without a\nstable identity it falls back to sibling order and tag name.\n\nThe `scan` option controls activation:\n\n| Option | Behavior |\n| --- | --- |\n| `auto` | Default. For replacement, scan inserted roots. For morphing, scan changed or inserted roots. |\n| `full` | Scan the boundary element and its subtree. |\n| `none` | Do not scan inserted content; call `loader.scan(...)` later if needed. |\n\n`type: \"many\"` also accepts `scan: \"once\"` as a batched `auto` scan after all\nupdates are applied.\n\nWhen boundary patches can arrive independently, use `createBoundaryReceiver`.\nIt keeps per-boundary sequence state, applies signal/cache effects before the\nHTML swap, flushes scheduled bindings, and ignores stale child patches after a\nparent scope is destroyed.\n\n```js\nimport { createBoundaryReceiver } from \"@async/framework/browser\";\n\nconst receiver = createBoundaryReceiver({\n  loader: runtime.loader,\n  signals: runtime.signals,\n  cache: runtime.browser.cache,\n  scheduler: runtime.scheduler,\n  router: runtime.router\n});\n\nawait receiver.apply({\n  boundary: \"product\",\n  seq: 1,\n  signals: {\n    product: { title: \"Keyboard\" }\n  },\n  cache: {\n    browser: {\n      \"product:sku-1\": { title: \"Keyboard\" }\n    }\n  },\n  html: `\n    \u003carticle\u003e\n      \u003ch1 signal:text=\"product.title\"\u003e\u003c/h1\u003e\n      \u003cbutton type=\"button\" on:click=\"server.cart.add(productId)\"\u003eAdd\u003c/button\u003e\n    \u003c/article\u003e\n  `\n});\n```\n\nSequence numbers are tracked per boundary: `hero` patch `10` can apply before\n`reviews` patch `2`, while a later `hero` patch `9` is ignored. The receiver\ndoes not add transport management, a transaction log, hydration, or component\nrerendering.\n\n## Examples\n\nSee [`examples/README.md`](./examples/README.md) for start commands and a short\ndescription of every example.\n\n| Example | Shows |\n| --- | --- |\n| [`examples/counter`](./examples/counter) | Signal text binding and delegated handlers |\n| [`examples/product`](./examples/product) | Async signal loading, ready, and error boundaries |\n| [`examples/components`](./examples/components) | Scoped fragment components and lifecycle hooks |\n| [`examples/streaming`](./examples/streaming) | Boundary swaps with rescanned handlers |\n| [`examples/server-call`](./examples/server-call) | Command events calling server functions |\n| [`examples/router`](./examples/router) | CSR first render and local route boundary swaps |\n| [`examples/partials`](./examples/partials) | Server-rendered partial fragments |\n| [`examples/cache`](./examples/cache) | Browser/server cache declarations |\n| [`examples/ssr`](./examples/ssr) | Server render output and browser activation snapshot |\n| [`examples/vite-hono`](./examples/vite-hono) | Hono-backed Vite dev server plus client asset build |\n| [`examples/vite-jsx-streaming`](./examples/vite-jsx-streaming) | JSX optimizer bootstrap with stream runtime slice selection |\n| [`examples/size`](./examples/size) | Scenario-size fixtures for bundle and runtime slices |\n\n## Pipeline\n\n`@async/pipeline` owns GitHub Actions, Pages, and release lifecycle automation.\nEdit [`pipeline.ts`](./pipeline.ts), then regenerate:\n\n```bash\npnpm run pipeline:sync:generate\npnpm run pipeline:sync:check\npnpm run pipeline:github:check\n```\n\nUseful commands:\n\n```bash\npnpm run bundle\npnpm run bundle:clean\npnpm run pipeline:verify\npnpm run pipeline:pages\npnpm run registry:lint\npnpm run pipeline:release:doctor\npnpm run release:check\n```\n\nRelease artifacts such as `browser.js`, `browser.min.js`,\n`browser.umd.min.js`, `browser.ts`, `browser.d.ts`, `framework.ts`,\n`framework.d.ts`, and `server.js` are generated into `dist/`. The generated\n`dist/` directory is the package root for `npm pack` and release publishing, so\nthe published package and CDN surface still expose those files at package root\nrather than under `dist/`. The source `package.json` stays private and owns the\nminimal public export spec, while omitting legacy `main`/`module`/`browser`\nentry fields and generated package file lists. `scripts/build-framework-bundle.js`\nderives the generated `dist/package.json` and staged artifact names from that\nspec. Feature branches should edit source files and let `pnpm run bundle`,\n`pnpm test`, or the generated release workflow materialize the publish tree.\nUse `pnpm run bundle:clean` to remove local generated artifacts after\ninspection.\n\n`registry:lint` scans package source and examples for declared registry ids\nsuch as signals, handlers, server functions, partials, routes, and components.\nIt writes `.async/registry-manifest.json` plus a per-file cache at\n`.async/registry-lint-cache.json`, skips generated root bundles such as\n`browser.umd.min.js`, and fails only when the same registry type and id are\ndeclared with different normalized content. Duplicate declarations with the\nsame content are reported as dedupe candidates, not errors.\n\nGitHub Pages builds through the generated `pages` job. This private repository\nneeds GitHub Pages support enabled before the generated job can deploy.\n\nStable releases use the generated `publish` job: it verifies the package,\ncreates or verifies the tag and GitHub Release, publishes npm with provenance,\nthen runs release doctor.\n\n## Status\n\nThe core runtime is intentionally small. Build-required JSX has optimizer\nartifacts for event, signal, stream, and children-fragment lowering, while full\ncompiler emission, lazy chunk manifests, TSRX lowering, server resource\ncompilation, and higher-level resumability metadata remain later layers. See\n`specs/framework/12-composition-patterns.md` for composition pattern guidance\nand planned source forms.\n\n## Async And htmx\n\nAsync and htmx are both HTML-first and avoid a virtual DOM, but they optimize\nfor different boundaries.\n\n| Area | htmx | Async |\n| --- | --- | --- |\n| Primary model | HTML attributes issue HTTP requests and swap server responses. | HTML attributes bind signals, command events, server calls, and route boundaries. |\n| State | Server-owned hypermedia state; browser state is intentionally minimal. | Browser signal registry plus server signal patches and cache snapshots. |\n| Server interaction | DOM attributes describe HTTP verbs, targets, and swaps. | `server.*(...)` commands call registered server functions and apply returned effects. |\n| Routing | Usually server navigation or htmx-boosted navigation. | CSR, SPA, SSR, SSR-SPA, and MPA router modes built around partial boundaries. |\n| Components | Server-rendered HTML fragments. | Scoped fragment functions today; higher layers can compile JSX/TSRX later. |\n| Build story | No build by default. | Layer 1 is no-build/CDN; higher layers can add build or compiler steps. |\n\nUse htmx when the server should own most interaction through hypermedia and\nHTTP swaps. Use Async when you want an HTML-first runtime that also has local\nsignals, async resources, registered browser/server handlers, route partials,\nand a path to higher compiler layers without changing the Layer 1 protocol.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fasync%2Fframework","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fasync%2Fframework","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fasync%2Fframework/lists"}