@async/framework 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0 - 2026-06-17
4
+
5
+ - Added `Loader` as the canonical public loader factory, including
6
+ `Async.Loader(...)` for UMD script-tag usage, while keeping `AsyncLoader` as
7
+ a compatibility alias.
8
+ - Added generated root CDN artifacts: `framework.min.js`, `framework.umd.js`,
9
+ `framework.umd.min.js`, `framework.ts`, and `framework.d.ts`.
10
+ - Added package exports and docs for ESM, compact ESM, UMD, compact UMD, and
11
+ TypeScript source/types entrypoints.
12
+ - Added exported helpers to the UMD-only `globalThis.Async` object for
13
+ script-tag CDN usage while keeping ESM `Async` as the app hub export.
14
+ - Added UMD namespace conflict checks so generated helpers cannot silently
15
+ overwrite app-hub fields such as `use`, `start`, or `registry`.
16
+ - Added `registry:lint`, a cached package linter that emits a local registry
17
+ manifest and detects conflicting signal, handler, server, partial, route, or
18
+ component declarations while skipping generated root bundles.
19
+
20
+ ## 0.5.0 - 2026-06-17
21
+
22
+ - Added `this.suspense(signalRef, views)` for component-owned async boundary
23
+ templates without adding a wrapper, rerender loop, hydration, or promise
24
+ throwing.
25
+ - Added `signal:prop:*` property bindings and tests for inline signal refs in
26
+ `signal:text`, `signal:attr:*`, and `signal:prop:*`.
27
+ - Added explicit `unregister(id)` APIs to runtime registries and component
28
+ cleanup for scoped signals, async signals, computed signals, and handlers.
29
+ - Added boundary swap cleanup for mounted component fragments and old DOM
30
+ bindings.
31
+ - Added server-call normalization for async signals, including returned signal
32
+ effects, proxy abort propagation, and stable server error messages.
33
+ - Added `prevent` as a command-event alias for `preventDefault`.
34
+
3
35
  ## 0.4.0 - 2026-06-17
4
36
 
5
37
  - Added a generated root `framework.js` ESM bundle for UNPKG browser imports.
package/README.md CHANGED
@@ -90,9 +90,18 @@ and package lifecycle tooling. Browser consumers import ESM directly.
90
90
 
91
91
  ## CDN
92
92
 
93
- The package ships a root `framework.js` ESM bundle for UNPKG and can be loaded
94
- without a build step. Use `@latest` for quick prototypes, and pin an exact
95
- version in production:
93
+ The package ships root CDN artifacts for UNPKG and can be loaded without a
94
+ build step. Use `@latest` for quick prototypes, and pin an exact version in
95
+ production:
96
+
97
+ | File | Format | Use |
98
+ | --- | --- | --- |
99
+ | `framework.js` | ESM | Readable browser module bundle |
100
+ | `framework.min.js` | ESM | Compact browser module bundle |
101
+ | `framework.umd.js` | UMD | Readable script-tag/CommonJS-style bundle |
102
+ | `framework.umd.min.js` | UMD | Compact script-tag/CommonJS-style bundle and default CDN file |
103
+ | `framework.ts` | TypeScript source facade | TS-aware runtimes and higher-layer tooling |
104
+ | `framework.d.ts` | Type declarations | TypeScript declarations for the public API |
96
105
 
97
106
  ```html
98
107
  <main async:container>
@@ -121,6 +130,29 @@ version in production:
121
130
  </script>
122
131
  ```
123
132
 
133
+ For a plain script tag, use the UMD bundle. In this UMD-only global form,
134
+ `globalThis.Async` is the app hub plus the exported helper functions, with
135
+ `globalThis.AsyncFramework` kept as an alias. Lower-level bootloader code can
136
+ call `Async.Loader(...)` directly.
137
+
138
+ ```html
139
+ <script src="https://unpkg.com/@async/framework@latest/framework.umd.min.js"></script>
140
+ <script>
141
+ Async.use({
142
+ signal: {
143
+ count: Async.createSignal(0)
144
+ },
145
+ handler: {
146
+ increment() {
147
+ this.signals.update("count", (count) => count + 1);
148
+ }
149
+ }
150
+ });
151
+
152
+ Async.start({ root: document });
153
+ </script>
154
+ ```
155
+
124
156
  You can also use an import map so app code imports `@async/framework` by name:
125
157
 
126
158
  ```html
@@ -157,8 +189,8 @@ You can also use an import map so app code imports `@async/framework` by name:
157
189
 
158
190
  ```js
159
191
  import {
160
- AsyncLoader,
161
192
  Async,
193
+ Loader,
162
194
  attributeName,
163
195
  asyncSignal,
164
196
  createApp,
@@ -188,6 +220,9 @@ import {
188
220
  } from "@async/framework";
189
221
  ```
190
222
 
223
+ `Loader` is the canonical loader factory. `AsyncLoader` remains as a
224
+ compatibility alias for older code.
225
+
191
226
  ### App Hub
192
227
 
193
228
  `Async` is an exported app hub singleton. It is not installed on `globalThis`
@@ -253,6 +288,7 @@ Naming rules:
253
288
  | `create*` | Runtime instance or mutable runtime primitive |
254
289
  | `Async.use(...)` | App-level declaration registration |
255
290
  | `registry.register(...)` | Low-level registration on a concrete runtime registry |
291
+ | `registry.unregister(...)` | Low-level removal from a concrete runtime registry |
256
292
 
257
293
  Singular registry keys are canonical: `signal`, `handler`, `server`,
258
294
  `partial`, `route`, `component`, and nested `cache.browser` / `cache.server`.
@@ -314,6 +350,7 @@ signals.set("count", 1);
314
350
  signals.update("count", (count) => count + 1);
315
351
  signals.subscribe("count", (count) => console.log(count));
316
352
  signals.ref("count").value;
353
+ signals.unregister("count");
317
354
  ```
318
355
 
319
356
  Initializer maps are supported:
@@ -376,7 +413,7 @@ reruns and the previous run is aborted.
376
413
 
377
414
  ## HTML Protocol
378
415
 
379
- AsyncLoader scans regular HTML attributes:
416
+ Loader scans regular HTML attributes:
380
417
 
381
418
  | Attribute | Behavior |
382
419
  | --- | --- |
@@ -389,6 +426,7 @@ AsyncLoader scans regular HTML attributes:
389
426
  | `signal:text="product.title"` | Text binding |
390
427
  | `signal:value="productId"` | Form value binding with writeback |
391
428
  | `signal:attr:disabled="product.$loading"` | Attribute binding |
429
+ | `signal:prop:checked="selected"` | DOM property binding |
392
430
  | `class:selected="selected"` | Class toggle from a signal path |
393
431
  | `signal:class="buttonClasses"` | Class set from a signal value: string, object, or array |
394
432
  | `async:boundary="product"` | Async or streamed replacement boundary |
@@ -428,6 +466,24 @@ Async.start({
428
466
  That maps to `data-async-container`, `data-on-click="save"`,
429
467
  `data-signal-text="product.title"`, and `data-class-selected="selected"`.
430
468
 
469
+ Inside `html` templates, signal refs can be passed directly to binding
470
+ attributes:
471
+
472
+ ```js
473
+ const title = this.signal("Keyboard");
474
+ const disabled = this.signal(false);
475
+ const checked = this.signal(true);
476
+
477
+ return html`
478
+ <h1 signal:text="${title}"></h1>
479
+ <button signal:attr:disabled="${disabled}">Save</button>
480
+ <input type="checkbox" signal:prop:checked="${checked}">
481
+ `;
482
+ ```
483
+
484
+ Use `signal:value` for form value binding with writeback. Use `signal:prop:*`
485
+ when you only need one-way DOM property updates.
486
+
431
487
  Named class toggles use their own top-level namespace:
432
488
 
433
489
  ```html
@@ -518,6 +574,7 @@ Plain commands resolve through the handler registry. Built-ins are registered by
518
574
  default:
519
575
 
520
576
  ```txt
577
+ prevent
521
578
  preventDefault
522
579
  stopPropagation
523
580
  stopImmediatePropagation
@@ -584,6 +641,12 @@ await server.cart.add("sku-1", 2);
584
641
 
585
642
  Server responses can include `value`, `signals`, `boundary`, `html`, `redirect`,
586
643
  or `error`. Signal patches are applied before boundary swaps and redirects.
644
+ Namespace calls such as `server.cart.add(...)` return the unwrapped `value`.
645
+
646
+ When an async signal calls a server namespace function, the framework passes the
647
+ active abort signal through proxy calls. Returned server effects such as
648
+ `signals`, `cache.browser`, `boundary/html`, and `redirect` are applied before
649
+ the async signal stores the unwrapped `value`.
587
650
 
588
651
  ### Router And Partials
589
652
 
@@ -764,7 +827,7 @@ createApp(browserApp, {
764
827
  ## Components
765
828
 
766
829
  Components are scoped fragment functions. They return strings or `html`
767
- templates; AsyncLoader inserts and scans the result. There is no virtual node
830
+ templates; Loader inserts and scans the result. There is no virtual node
768
831
  type and no rerender loop.
769
832
 
770
833
  ```js
@@ -794,7 +857,7 @@ const Toggle = defineComponent(function Toggle() {
794
857
  `;
795
858
  });
796
859
 
797
- const loader = AsyncLoader({ root: document });
860
+ const loader = Loader({ root: document });
798
861
  loader.mount(document.querySelector("#app"), Toggle);
799
862
  ```
800
863
 
@@ -812,10 +875,55 @@ Component helpers:
812
875
  | `this.handler(name, fn)` | Scoped named handler registry entry |
813
876
  | `this.handler(fn)` | Generated scoped handler registry entry |
814
877
  | `this.render(Component, props)` | Child fragment rendering |
878
+ | `this.suspense(signalRef, views)` | Async boundary template helper |
815
879
  | `this.on(event, fn)` | Fragment lifecycle fallback for `attach`, `visible`, and `destroy` |
816
880
  | `this.onMount(fn)` | Compatibility alias for `this.on("attach", fn)` |
817
881
  | `this.onVisible(fn)` | Compatibility alias for `this.on("visible", fn)` |
818
882
 
883
+ `this.suspense(...)` is sugar for Loader boundaries:
884
+ `asyncSignal + async:boundary + async:* templates`. It emits only templates. The
885
+ caller owns the boundary element, and the loader chooses the loading, ready, or
886
+ error template from the async signal status.
887
+
888
+ ```js
889
+ const Product = defineComponent(function Product() {
890
+ const product = this.asyncSignal("product", async function () {
891
+ return this.server.products.get("sku-1");
892
+ });
893
+
894
+ return html`
895
+ <article async:boundary="${product.id}">
896
+ ${this.suspense(product, {
897
+ loading() {
898
+ return html`<p>Loading...</p>`;
899
+ },
900
+ ready(product) {
901
+ return html`<h1 signal:text="${product.id}.title"></h1>`;
902
+ },
903
+ error(product) {
904
+ return html`<p signal:text="${product.id}.$error.message"></p>`;
905
+ }
906
+ })}
907
+ </article>
908
+ `;
909
+ });
910
+ ```
911
+
912
+ The shorthand form treats the callback as the ready template:
913
+
914
+ ```js
915
+ this.suspense(product, (product) => html`
916
+ <h1 signal:text="${product.id}.title"></h1>
917
+ `);
918
+ ```
919
+
920
+ `this.suspense(...)` is not React Suspense. It does not throw promises,
921
+ hydrate, diff, rerender a component tree, or emit a wrapper element.
922
+
923
+ Component-scoped signals and handlers are unregistered when the mounted
924
+ fragment is destroyed. `loader.swap(...)` cleans up old DOM bindings and mounted
925
+ component fragments under the swapped boundary before inserting the new HTML.
926
+
819
927
  Put component lifecycle on the component root element when there is one:
820
928
 
821
929
  ```js
@@ -894,10 +1002,19 @@ Useful commands:
894
1002
  ```bash
895
1003
  pnpm run pipeline:verify
896
1004
  pnpm run pipeline:pages
1005
+ pnpm run registry:lint
897
1006
  pnpm run pipeline:release:doctor
898
1007
  pnpm run release:check
899
1008
  ```
900
1009
 
1010
+ `registry:lint` scans package source and examples for declared registry ids
1011
+ such as signals, handlers, server functions, partials, routes, and components.
1012
+ It writes `.async/registry-manifest.json` plus a per-file cache at
1013
+ `.async/registry-lint-cache.json`, skips generated root bundles such as
1014
+ `framework.umd.min.js`, and fails only when the same registry type and id are
1015
+ declared with different normalized content. Duplicate declarations with the
1016
+ same content are reported as dedupe candidates, not errors.
1017
+
901
1018
  GitHub Pages builds through the generated `pages` job. This private repository
902
1019
  needs GitHub Pages support enabled before the generated job can deploy.
903
1020
 
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>AsyncLoader Cache</title>
6
+ <title>Async Loader Cache</title>
7
7
  </head>
8
8
  <body>
9
9
  <main async:container>
@@ -2,7 +2,7 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
- <title>AsyncLoader Components</title>
5
+ <title>Async Loader Components</title>
6
6
  </head>
7
7
  <body>
8
8
  <main id="app"></main>
@@ -1,4 +1,4 @@
1
- import { AsyncLoader, defineComponent, html } from "../../src/index.js";
1
+ import { Loader, defineComponent, html } from "../../src/index.js";
2
2
 
3
3
  const Toggle = defineComponent(function Toggle() {
4
4
  const selected = this.signal(false);
@@ -22,5 +22,5 @@ const Toggle = defineComponent(function Toggle() {
22
22
  `;
23
23
  });
24
24
 
25
- const loader = AsyncLoader({ root: document });
25
+ const loader = Loader({ root: document });
26
26
  loader.mount(document.querySelector("#app"), Toggle);
@@ -2,7 +2,7 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
- <title>AsyncLoader Counter</title>
5
+ <title>Async Loader Counter</title>
6
6
  </head>
7
7
  <body async:container>
8
8
  <main>
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>AsyncLoader Partials</title>
6
+ <title>Async Loader Partials</title>
7
7
  </head>
8
8
  <body>
9
9
  <main async:container>
@@ -2,7 +2,7 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
- <title>AsyncLoader Product</title>
5
+ <title>Async Loader Product</title>
6
6
  </head>
7
7
  <body async:container>
8
8
  <main>
@@ -1,4 +1,4 @@
1
- import { AsyncLoader, createSignal, createSignalRegistry, delay } from "../../src/index.js";
1
+ import { Loader, createSignal, createSignalRegistry, delay } from "../../src/index.js";
2
2
 
3
3
  const products = {
4
4
  "sku-1": {
@@ -21,4 +21,4 @@ signals.asyncSignal("product", async function () {
21
21
  return products[id];
22
22
  });
23
23
 
24
- AsyncLoader({ root: document.body, signals }).start();
24
+ Loader({ root: document.body, signals }).start();
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>AsyncLoader CSR Router</title>
6
+ <title>Async Loader CSR Router</title>
7
7
  </head>
8
8
  <body>
9
9
  <main async:container>
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>AsyncLoader Server Call</title>
6
+ <title>Async Loader Server Call</title>
7
7
  </head>
8
8
  <body>
9
9
  <main async:container>
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>AsyncLoader SSR Activation</title>
6
+ <title>Async Loader SSR Activation</title>
7
7
  </head>
8
8
  <body>
9
9
  <main id="app" async:container></main>
@@ -2,7 +2,7 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
- <title>AsyncLoader Streaming</title>
5
+ <title>Async Loader Streaming</title>
6
6
  </head>
7
7
  <body async:container>
8
8
  <main>