@async/framework 0.4.0 → 0.5.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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0 - 2026-06-17
4
+
5
+ - Added `this.suspense(signalRef, views)` for component-owned async boundary
6
+ templates without adding a wrapper, rerender loop, hydration, or promise
7
+ throwing.
8
+ - Added `signal:prop:*` property bindings and tests for inline signal refs in
9
+ `signal:text`, `signal:attr:*`, and `signal:prop:*`.
10
+ - Added explicit `unregister(id)` APIs to runtime registries and component
11
+ cleanup for scoped signals, async signals, computed signals, and handlers.
12
+ - Added boundary swap cleanup for mounted component fragments and old DOM
13
+ bindings.
14
+ - Added server-call normalization for async signals, including returned signal
15
+ effects, proxy abort propagation, and stable server error messages.
16
+ - Added `prevent` as a command-event alias for `preventDefault`.
17
+
3
18
  ## 0.4.0 - 2026-06-17
4
19
 
5
20
  - Added a generated root `framework.js` ESM bundle for UNPKG browser imports.
package/README.md CHANGED
@@ -253,6 +253,7 @@ Naming rules:
253
253
  | `create*` | Runtime instance or mutable runtime primitive |
254
254
  | `Async.use(...)` | App-level declaration registration |
255
255
  | `registry.register(...)` | Low-level registration on a concrete runtime registry |
256
+ | `registry.unregister(...)` | Low-level removal from a concrete runtime registry |
256
257
 
257
258
  Singular registry keys are canonical: `signal`, `handler`, `server`,
258
259
  `partial`, `route`, `component`, and nested `cache.browser` / `cache.server`.
@@ -314,6 +315,7 @@ signals.set("count", 1);
314
315
  signals.update("count", (count) => count + 1);
315
316
  signals.subscribe("count", (count) => console.log(count));
316
317
  signals.ref("count").value;
318
+ signals.unregister("count");
317
319
  ```
318
320
 
319
321
  Initializer maps are supported:
@@ -389,6 +391,7 @@ AsyncLoader scans regular HTML attributes:
389
391
  | `signal:text="product.title"` | Text binding |
390
392
  | `signal:value="productId"` | Form value binding with writeback |
391
393
  | `signal:attr:disabled="product.$loading"` | Attribute binding |
394
+ | `signal:prop:checked="selected"` | DOM property binding |
392
395
  | `class:selected="selected"` | Class toggle from a signal path |
393
396
  | `signal:class="buttonClasses"` | Class set from a signal value: string, object, or array |
394
397
  | `async:boundary="product"` | Async or streamed replacement boundary |
@@ -428,6 +431,24 @@ Async.start({
428
431
  That maps to `data-async-container`, `data-on-click="save"`,
429
432
  `data-signal-text="product.title"`, and `data-class-selected="selected"`.
430
433
 
434
+ Inside `html` templates, signal refs can be passed directly to binding
435
+ attributes:
436
+
437
+ ```js
438
+ const title = this.signal("Keyboard");
439
+ const disabled = this.signal(false);
440
+ const checked = this.signal(true);
441
+
442
+ return html`
443
+ <h1 signal:text="${title}"></h1>
444
+ <button signal:attr:disabled="${disabled}">Save</button>
445
+ <input type="checkbox" signal:prop:checked="${checked}">
446
+ `;
447
+ ```
448
+
449
+ Use `signal:value` for form value binding with writeback. Use `signal:prop:*`
450
+ when you only need one-way DOM property updates.
451
+
431
452
  Named class toggles use their own top-level namespace:
432
453
 
433
454
  ```html
@@ -518,6 +539,7 @@ Plain commands resolve through the handler registry. Built-ins are registered by
518
539
  default:
519
540
 
520
541
  ```txt
542
+ prevent
521
543
  preventDefault
522
544
  stopPropagation
523
545
  stopImmediatePropagation
@@ -584,6 +606,12 @@ await server.cart.add("sku-1", 2);
584
606
 
585
607
  Server responses can include `value`, `signals`, `boundary`, `html`, `redirect`,
586
608
  or `error`. Signal patches are applied before boundary swaps and redirects.
609
+ Namespace calls such as `server.cart.add(...)` return the unwrapped `value`.
610
+
611
+ When an async signal calls a server namespace function, the framework passes the
612
+ active abort signal through proxy calls. Returned server effects such as
613
+ `signals`, `cache.browser`, `boundary/html`, and `redirect` are applied before
614
+ the async signal stores the unwrapped `value`.
587
615
 
588
616
  ### Router And Partials
589
617
 
@@ -812,10 +840,55 @@ Component helpers:
812
840
  | `this.handler(name, fn)` | Scoped named handler registry entry |
813
841
  | `this.handler(fn)` | Generated scoped handler registry entry |
814
842
  | `this.render(Component, props)` | Child fragment rendering |
843
+ | `this.suspense(signalRef, views)` | Async boundary template helper |
815
844
  | `this.on(event, fn)` | Fragment lifecycle fallback for `attach`, `visible`, and `destroy` |
816
845
  | `this.onMount(fn)` | Compatibility alias for `this.on("attach", fn)` |
817
846
  | `this.onVisible(fn)` | Compatibility alias for `this.on("visible", fn)` |
818
847
 
848
+ `this.suspense(...)` is sugar for AsyncLoader boundaries:
849
+ `asyncSignal + async:boundary + async:* templates`. It emits only templates. The
850
+ caller owns the boundary element, and the loader chooses the loading, ready, or
851
+ error template from the async signal status.
852
+
853
+ ```js
854
+ const Product = defineComponent(function Product() {
855
+ const product = this.asyncSignal("product", async function () {
856
+ return this.server.products.get("sku-1");
857
+ });
858
+
859
+ return html`
860
+ <article async:boundary="${product.id}">
861
+ ${this.suspense(product, {
862
+ loading() {
863
+ return html`<p>Loading...</p>`;
864
+ },
865
+ ready(product) {
866
+ return html`<h1 signal:text="${product.id}.title"></h1>`;
867
+ },
868
+ error(product) {
869
+ return html`<p signal:text="${product.id}.$error.message"></p>`;
870
+ }
871
+ })}
872
+ </article>
873
+ `;
874
+ });
875
+ ```
876
+
877
+ The shorthand form treats the callback as the ready template:
878
+
879
+ ```js
880
+ this.suspense(product, (product) => html`
881
+ <h1 signal:text="${product.id}.title"></h1>
882
+ `);
883
+ ```
884
+
885
+ `this.suspense(...)` is not React Suspense. It does not throw promises,
886
+ hydrate, diff, rerender a component tree, or emit a wrapper element.
887
+
888
+ Component-scoped signals and handlers are unregistered when the mounted
889
+ fragment is destroyed. `loader.swap(...)` cleans up old DOM bindings and mounted
890
+ component fragments under the swapped boundary before inserting the new HTML.
891
+
819
892
  Put component lifecycle on the component root element when there is one:
820
893
 
821
894
  ```js