@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/framework.js CHANGED
@@ -86,7 +86,18 @@ const __asyncSignalModule = (() => {
86
86
  signals: registry,
87
87
  id: registeredId,
88
88
  get server() {
89
- return registry._context?.().server;
89
+ const context = registry._context?.() ?? {};
90
+ const server = context.server;
91
+ if (typeof server?._withContext === "function") {
92
+ return server._withContext({
93
+ signals: registry,
94
+ router: context.router,
95
+ loader: context.loader,
96
+ cache: context.cache,
97
+ abort: activeAbort
98
+ });
99
+ }
100
+ return server;
90
101
  },
91
102
  get router() {
92
103
  return registry._context?.().router;
@@ -280,6 +291,10 @@ const __registryStoreModule = (() => {
280
291
  return value;
281
292
  },
282
293
 
294
+ unregister(type, id) {
295
+ return registry.delete(type, id);
296
+ },
297
+
283
298
  delete(type, id) {
284
299
  return registry._map(type).delete(id);
285
300
  },
@@ -540,6 +555,11 @@ const __cacheModule = (() => {
540
555
  return registryApi;
541
556
  },
542
557
 
558
+ unregister(id) {
559
+ assertId(id);
560
+ return definitions.delete(id);
561
+ },
562
+
543
563
  resolve(id) {
544
564
  assertId(id);
545
565
  return definitions.get(id);
@@ -667,6 +687,62 @@ const __cacheModule = (() => {
667
687
  return { defineCache, createCacheRegistry };
668
688
  })();
669
689
 
690
+ const __attributesModule = (() => {
691
+ const defaultPrefixes = Object.freeze({
692
+ async: ["async:"],
693
+ class: ["class:"],
694
+ signal: ["signal:"],
695
+ on: ["on:"]
696
+ });
697
+
698
+ function defineAttributeConfig(config = {}) {
699
+ return normalizeAttributeConfig(config);
700
+ }
701
+
702
+ function normalizeAttributeConfig(config = {}) {
703
+ return {
704
+ async: normalizePrefixes(config.async, defaultPrefixes.async),
705
+ class: normalizePrefixes(config.class, defaultPrefixes.class),
706
+ signal: normalizePrefixes(config.signal, defaultPrefixes.signal),
707
+ on: normalizePrefixes(config.on, defaultPrefixes.on)
708
+ };
709
+ }
710
+
711
+ function attributeName(attributes, type, name) {
712
+ return normalizeAttributeConfig(attributes)[type][0] + name;
713
+ }
714
+
715
+ function readAttribute(element, attributes, type, name) {
716
+ for (const prefix of normalizeAttributeConfig(attributes)[type]) {
717
+ const attr = `${prefix}${name}`;
718
+ if (element.hasAttribute?.(attr)) {
719
+ return element.getAttribute(attr);
720
+ }
721
+ }
722
+ return null;
723
+ }
724
+
725
+ function matchAttribute(name, attributes, type) {
726
+ for (const prefix of normalizeAttributeConfig(attributes)[type]) {
727
+ if (name.startsWith(prefix)) {
728
+ return name.slice(prefix.length);
729
+ }
730
+ }
731
+ return null;
732
+ }
733
+
734
+ function normalizePrefixes(value, fallback) {
735
+ const prefixes = value == null ? fallback : Array.isArray(value) ? value : [value];
736
+ return prefixes.map((prefix) => {
737
+ if (typeof prefix !== "string" || prefix.length === 0) {
738
+ throw new TypeError("Attribute prefixes must be non-empty strings.");
739
+ }
740
+ return prefix;
741
+ });
742
+ }
743
+ return { defineAttributeConfig, normalizeAttributeConfig, attributeName, readAttribute, matchAttribute };
744
+ })();
745
+
670
746
  const __signalsModule = (() => {
671
747
  const { asyncSignal: createAsyncSignal, isAsyncSignal } = __asyncSignalModule;
672
748
  const { attachRegistryInspection, createRegistryStore } = __registryStoreModule;
@@ -790,7 +866,7 @@ const __signalsModule = (() => {
790
866
  const registryStore = options.registry ?? createRegistryStore();
791
867
  const type = options.type ?? "signal";
792
868
  const entries = registryStore._map(type);
793
- const registryCleanups = new Set();
869
+ const registryCleanups = new Map();
794
870
  const runtimeContext = {};
795
871
  const boundEntries = new Set();
796
872
 
@@ -813,6 +889,19 @@ const __signalsModule = (() => {
813
889
  return registry;
814
890
  },
815
891
 
892
+ unregister(id) {
893
+ assertId(id);
894
+ if (!entries.has(id)) {
895
+ return false;
896
+ }
897
+ registryCleanups.get(id)?.();
898
+ registryCleanups.delete(id);
899
+ entries.get(id)?._dispose?.();
900
+ entries.delete(id);
901
+ boundEntries.delete(id);
902
+ return true;
903
+ },
904
+
816
905
  ensure(id, initial) {
817
906
  assertId(id);
818
907
  if (!entries.has(id)) {
@@ -925,7 +1014,7 @@ const __signalsModule = (() => {
925
1014
  },
926
1015
 
927
1016
  destroy() {
928
- for (const cleanup of registryCleanups) {
1017
+ for (const cleanup of registryCleanups.values()) {
929
1018
  cleanup();
930
1019
  }
931
1020
  registryCleanups.clear();
@@ -982,7 +1071,7 @@ const __signalsModule = (() => {
982
1071
  boundEntries.add(id);
983
1072
  const cleanup = entry._bindRegistry(registry, id);
984
1073
  if (typeof cleanup === "function") {
985
- registryCleanups.add(cleanup);
1074
+ registryCleanups.set(id, cleanup);
986
1075
  }
987
1076
  }
988
1077
  }
@@ -1175,62 +1264,6 @@ const __signalsModule = (() => {
1175
1264
  return { createSignal, computed, effect, createSignalRegistry, isSignalRef, signal };
1176
1265
  })();
1177
1266
 
1178
- const __attributesModule = (() => {
1179
- const defaultPrefixes = Object.freeze({
1180
- async: ["async:"],
1181
- class: ["class:"],
1182
- signal: ["signal:"],
1183
- on: ["on:"]
1184
- });
1185
-
1186
- function defineAttributeConfig(config = {}) {
1187
- return normalizeAttributeConfig(config);
1188
- }
1189
-
1190
- function normalizeAttributeConfig(config = {}) {
1191
- return {
1192
- async: normalizePrefixes(config.async, defaultPrefixes.async),
1193
- class: normalizePrefixes(config.class, defaultPrefixes.class),
1194
- signal: normalizePrefixes(config.signal, defaultPrefixes.signal),
1195
- on: normalizePrefixes(config.on, defaultPrefixes.on)
1196
- };
1197
- }
1198
-
1199
- function attributeName(attributes, type, name) {
1200
- return normalizeAttributeConfig(attributes)[type][0] + name;
1201
- }
1202
-
1203
- function readAttribute(element, attributes, type, name) {
1204
- for (const prefix of normalizeAttributeConfig(attributes)[type]) {
1205
- const attr = `${prefix}${name}`;
1206
- if (element.hasAttribute?.(attr)) {
1207
- return element.getAttribute(attr);
1208
- }
1209
- }
1210
- return null;
1211
- }
1212
-
1213
- function matchAttribute(name, attributes, type) {
1214
- for (const prefix of normalizeAttributeConfig(attributes)[type]) {
1215
- if (name.startsWith(prefix)) {
1216
- return name.slice(prefix.length);
1217
- }
1218
- }
1219
- return null;
1220
- }
1221
-
1222
- function normalizePrefixes(value, fallback) {
1223
- const prefixes = value == null ? fallback : Array.isArray(value) ? value : [value];
1224
- return prefixes.map((prefix) => {
1225
- if (typeof prefix !== "string" || prefix.length === 0) {
1226
- throw new TypeError("Attribute prefixes must be non-empty strings.");
1227
- }
1228
- return prefix;
1229
- });
1230
- }
1231
- return { defineAttributeConfig, normalizeAttributeConfig, attributeName, readAttribute, matchAttribute };
1232
- })();
1233
-
1234
1267
  const __htmlModule = (() => {
1235
1268
  const { isSignalRef } = __signalsModule;
1236
1269
  const { attributeName, matchAttribute, normalizeAttributeConfig } = __attributesModule;
@@ -1393,7 +1426,8 @@ const __htmlModule = (() => {
1393
1426
  })();
1394
1427
 
1395
1428
  const __componentModule = (() => {
1396
- const { rawHtml, renderTemplate } = __htmlModule;
1429
+ const { attributeName } = __attributesModule;
1430
+ const { escapeHtml, rawHtml, renderTemplate } = __htmlModule;
1397
1431
  const { attachRegistryInspection, createRegistryStore } = __registryStoreModule;
1398
1432
  const componentKind = Symbol.for("@async/framework.component");
1399
1433
  let componentCounter = 0;
@@ -1438,6 +1472,13 @@ const __componentModule = (() => {
1438
1472
  return registry;
1439
1473
  },
1440
1474
 
1475
+ unregister(id) {
1476
+ if (typeof id !== "string" || id.length === 0) {
1477
+ throw new TypeError("Component id must be a non-empty string.");
1478
+ }
1479
+ return entries.delete(id);
1480
+ },
1481
+
1441
1482
  resolve(id) {
1442
1483
  if (typeof id !== "string" || id.length === 0) {
1443
1484
  throw new TypeError("Component id must be a non-empty string.");
@@ -1469,29 +1510,32 @@ const __componentModule = (() => {
1469
1510
  const visibleHooks = [];
1470
1511
  const destroyHooks = [];
1471
1512
  const bindingIds = [];
1472
- const context = createComponentContext({
1473
- runtime,
1474
- scope,
1475
- cleanups,
1476
- attachHooks,
1477
- visibleHooks,
1478
- destroyHooks
1479
- });
1480
-
1481
- const output = Component.call(context, props);
1482
- const html = renderTemplate(output, {
1513
+ const templateOptions = {
1483
1514
  attributes: runtime.attributes,
1484
1515
  signals: runtime.signals,
1485
1516
  bind(value) {
1486
1517
  const id = runtime.loader?._registerBinding?.(value);
1487
1518
  if (!id) {
1488
- throw new Error("Inline template bindings require an AsyncLoader.");
1519
+ throw new Error("Inline template bindings require a Loader.");
1489
1520
  }
1490
1521
  bindingIds.push(id);
1491
1522
  return id;
1492
1523
  }
1524
+ };
1525
+ const renderScopedTemplate = (value) => renderTemplate(value, templateOptions);
1526
+ const context = createComponentContext({
1527
+ runtime,
1528
+ scope,
1529
+ cleanups,
1530
+ attachHooks,
1531
+ visibleHooks,
1532
+ destroyHooks,
1533
+ renderScopedTemplate
1493
1534
  });
1494
1535
 
1536
+ const output = Component.call(context, props);
1537
+ const html = renderScopedTemplate(output);
1538
+
1495
1539
  return {
1496
1540
  html,
1497
1541
  attach(target) {
@@ -1527,7 +1571,7 @@ const __componentModule = (() => {
1527
1571
  };
1528
1572
  }
1529
1573
 
1530
- function createComponentContext({ runtime, scope, cleanups, attachHooks, visibleHooks, destroyHooks }) {
1574
+ function createComponentContext({ runtime, scope, cleanups, attachHooks, visibleHooks, destroyHooks, renderScopedTemplate }) {
1531
1575
  const { signals, handlers, loader, server, router, cache } = runtime;
1532
1576
  const generatedHandlers = new WeakMap();
1533
1577
  let generatedHandlerCounter = 0;
@@ -1543,14 +1587,27 @@ const __componentModule = (() => {
1543
1587
 
1544
1588
  signal(name, initial) {
1545
1589
  if (arguments.length === 1) {
1546
- return signals.ensure(scoped(scope, `signal.${++generatedSignalCounter}`), name);
1590
+ const id = scoped(scope, `signal.${++generatedSignalCounter}`);
1591
+ const ref = signals.ensure(id, name);
1592
+ cleanups.push(() => signals.unregister?.(id));
1593
+ return ref;
1594
+ }
1595
+ const id = scoped(scope, name);
1596
+ const created = !signals.has(id);
1597
+ const ref = signals.ensure(id, initial);
1598
+ if (created) {
1599
+ cleanups.push(() => signals.unregister?.(id));
1547
1600
  }
1548
- return signals.ensure(scoped(scope, name), initial);
1601
+ return ref;
1549
1602
  },
1550
1603
 
1551
1604
  computed(name, fn) {
1552
1605
  const id = scoped(scope, name);
1606
+ const created = !signals.has(id);
1553
1607
  const ref = signals.ensure(id, undefined);
1608
+ if (created) {
1609
+ cleanups.push(() => signals.unregister?.(id));
1610
+ }
1554
1611
  const cleanup = signals.effect(() => {
1555
1612
  signals.set(id, fn.call(context));
1556
1613
  });
@@ -1560,9 +1617,13 @@ const __componentModule = (() => {
1560
1617
 
1561
1618
  asyncSignal(name, fn) {
1562
1619
  const id = scoped(scope, name);
1620
+ const created = !signals.has(id);
1563
1621
  if (!signals.has(id)) {
1564
1622
  signals.asyncSignal(id, fn);
1565
1623
  }
1624
+ if (created) {
1625
+ cleanups.push(() => signals.unregister?.(id));
1626
+ }
1566
1627
  return signals.ref(id);
1567
1628
  },
1568
1629
 
@@ -1596,6 +1657,26 @@ const __componentModule = (() => {
1596
1657
  return rawHtml(child.html);
1597
1658
  },
1598
1659
 
1660
+ suspense(signalRef, views) {
1661
+ const id = signalRef?.id;
1662
+ if (!id) {
1663
+ throw new TypeError("this.suspense(signalRef, views) requires a signal ref.");
1664
+ }
1665
+
1666
+ const normalized = normalizeSuspenseViews(views);
1667
+ const chunks = [];
1668
+ for (const state of ["loading", "ready", "error"]) {
1669
+ const view = normalized[state];
1670
+ if (!view) {
1671
+ continue;
1672
+ }
1673
+ const attr = attributeName(runtime.attributes, "async", state);
1674
+ const body = renderScopedTemplate(view.call(context, signalRef));
1675
+ chunks.push(`<template ${attr}="${escapeHtml(id)}">${body}</template>`);
1676
+ }
1677
+ return rawHtml(chunks.join(""));
1678
+ },
1679
+
1599
1680
  on(eventName, fn) {
1600
1681
  if (typeof eventName !== "string" || eventName.length === 0) {
1601
1682
  throw new TypeError("Component lifecycle event must be a non-empty string.");
@@ -1635,6 +1716,7 @@ const __componentModule = (() => {
1635
1716
  handlers.register(id, function runComponentHandler(handlerContext) {
1636
1717
  return fn.call({ ...context, ...handlerContext }, handlerContext);
1637
1718
  });
1719
+ cleanups.push(() => handlers.unregister?.(id));
1638
1720
  return id;
1639
1721
  }
1640
1722
  }
@@ -1646,6 +1728,21 @@ const __componentModule = (() => {
1646
1728
  return `${scope}.${name}`;
1647
1729
  }
1648
1730
 
1731
+ function normalizeSuspenseViews(views) {
1732
+ const normalized = typeof views === "function" ? { ready: views } : views;
1733
+ if (!normalized || typeof normalized !== "object" || Array.isArray(normalized)) {
1734
+ throw new TypeError("this.suspense(signalRef, views) requires views to be a function or object.");
1735
+ }
1736
+
1737
+ for (const state of ["loading", "ready", "error"]) {
1738
+ if (Object.hasOwn(normalized, state) && normalized[state] !== undefined && typeof normalized[state] !== "function") {
1739
+ throw new TypeError(`this.suspense(signalRef, views) view "${state}" must be a function.`);
1740
+ }
1741
+ }
1742
+
1743
+ return normalized;
1744
+ }
1745
+
1649
1746
  function componentName(Component) {
1650
1747
  return Component.displayName || Component.name || "anonymous";
1651
1748
  }
@@ -1682,6 +1779,11 @@ const __serverModule = (() => {
1682
1779
  return registry;
1683
1780
  },
1684
1781
 
1782
+ unregister(id) {
1783
+ assertServerId(id);
1784
+ return entries.delete(id);
1785
+ },
1786
+
1685
1787
  resolve(id) {
1686
1788
  assertServerId(id);
1687
1789
  return entries.get(id);
@@ -1697,7 +1799,7 @@ const __serverModule = (() => {
1697
1799
  let runContext;
1698
1800
  const server = createServerNamespace((childId, childArgs, childContext = {}) => {
1699
1801
  return registry.run(childId, childArgs, { ...runContext, ...childContext });
1700
- });
1802
+ }, {}, () => runContext);
1701
1803
 
1702
1804
  const mergedContext = {
1703
1805
  ...defaults,
@@ -1730,7 +1832,7 @@ const __serverModule = (() => {
1730
1832
  }, registryStore, type);
1731
1833
 
1732
1834
  registry.registerMany(initialMap);
1733
- return createServerNamespace((id, args, context) => registry.run(id, args, context), registry);
1835
+ return createServerNamespace((id, args, context) => registry.run(id, args, context), registry, () => defaults);
1734
1836
  }
1735
1837
 
1736
1838
  function createServerProxy({
@@ -1781,7 +1883,7 @@ const __serverModule = (() => {
1781
1883
  _setContext(context = {}) {
1782
1884
  Object.assign(defaults, context);
1783
1885
  }
1784
- });
1886
+ }, () => defaults);
1785
1887
  }
1786
1888
 
1787
1889
  function resolveServerCommandArguments(args, context = {}) {
@@ -1859,7 +1961,7 @@ const __serverModule = (() => {
1859
1961
  };
1860
1962
  }
1861
1963
 
1862
- function createServerNamespace(run, root = {}) {
1964
+ function createServerNamespace(run, root = {}, contextProvider = () => ({})) {
1863
1965
  const cache = new Map();
1864
1966
 
1865
1967
  function namespace(parts) {
@@ -1868,11 +1970,14 @@ const __serverModule = (() => {
1868
1970
  return cache.get(cacheKey);
1869
1971
  }
1870
1972
 
1871
- const callable = (...args) => {
1973
+ const callable = async (...args) => {
1872
1974
  if (parts.length === 0) {
1873
1975
  throw new Error("Server namespace is not directly callable.");
1874
1976
  }
1875
- return Promise.resolve(run(parts.join("."), args)).then(unwrapServerResult);
1977
+ const context = contextProvider() ?? {};
1978
+ const result = await run(parts.join("."), args, context);
1979
+ await applyServerResult(result, context);
1980
+ return unwrapServerResult(result);
1876
1981
  };
1877
1982
 
1878
1983
  const proxy = new Proxy(callable, {
@@ -1883,6 +1988,18 @@ const __serverModule = (() => {
1883
1988
  if (prop in _target) {
1884
1989
  return _target[prop];
1885
1990
  }
1991
+ if (parts.length === 0 && prop === "_withContext") {
1992
+ return (context = {}) => createServerNamespace(run, root, () => ({
1993
+ ...(contextProvider() ?? {}),
1994
+ ...context
1995
+ }));
1996
+ }
1997
+ if (parts.length === 0 && prop === "run" && typeof root.run === "function") {
1998
+ return (id, args = [], context = {}) => root.run(id, args, {
1999
+ ...(contextProvider() ?? {}),
2000
+ ...context
2001
+ });
2002
+ }
1886
2003
  if (parts.length === 0 && Object.hasOwn(root, prop)) {
1887
2004
  return root[prop];
1888
2005
  }
@@ -2035,11 +2152,10 @@ const __serverModule = (() => {
2035
2152
  const __handlersModule = (() => {
2036
2153
  const { applyServerResult, defaultInput, resolveServerCommandArguments, unwrapServerResult } = __serverModule;
2037
2154
  const { attachRegistryInspection, createRegistryStore } = __registryStoreModule;
2038
- const builtInTokens = new Set(["preventDefault", "stopPropagation", "stopImmediatePropagation"]);
2155
+ const builtInTokens = new Set(["prevent", "preventDefault", "stopPropagation", "stopImmediatePropagation"]);
2039
2156
  const builtInHandlers = {
2040
- preventDefault() {
2041
- this.event?.preventDefault?.();
2042
- },
2157
+ prevent: preventDefault,
2158
+ preventDefault,
2043
2159
  stopPropagation() {
2044
2160
  this.event?.stopPropagation?.();
2045
2161
  },
@@ -2048,6 +2164,10 @@ const __handlersModule = (() => {
2048
2164
  }
2049
2165
  };
2050
2166
 
2167
+ function preventDefault() {
2168
+ this.event?.preventDefault?.();
2169
+ }
2170
+
2051
2171
  function createHandlerRegistry(initialMap = {}, options = {}) {
2052
2172
  const registryStore = options.registry ?? createRegistryStore();
2053
2173
  const type = options.type ?? "handler";
@@ -2073,6 +2193,11 @@ const __handlersModule = (() => {
2073
2193
  return registry;
2074
2194
  },
2075
2195
 
2196
+ unregister(id) {
2197
+ assertId(id);
2198
+ return handlers.delete(id);
2199
+ },
2200
+
2076
2201
  resolve(id) {
2077
2202
  assertId(id);
2078
2203
  return handlers.get(id);
@@ -2231,7 +2356,7 @@ const __loaderModule = (() => {
2231
2356
  const { matchAttribute, normalizeAttributeConfig, readAttribute } = __attributesModule;
2232
2357
  const inlineBindingPrefix = "__async:inline:";
2233
2358
 
2234
- function AsyncLoader({ root, signals, handlers, server, router, cache, attributes } = {}) {
2359
+ function Loader({ root, signals, handlers, server, router, cache, attributes } = {}) {
2235
2360
  const documentRef = root?.ownerDocument ?? root ?? globalThis.document;
2236
2361
  const rootNode = root ?? documentRef;
2237
2362
  const signalRegistry = signals ?? createSignalRegistry();
@@ -2245,6 +2370,7 @@ const __loaderModule = (() => {
2245
2370
  const boundaryState = new WeakMap();
2246
2371
  const renderingBoundaries = new WeakSet();
2247
2372
  const inlineBindings = new Map();
2373
+ const scopedCleanups = new WeakMap();
2248
2374
  let inlineBindingCounter = 0;
2249
2375
  let destroyed = false;
2250
2376
 
@@ -2279,6 +2405,7 @@ const __loaderModule = (() => {
2279
2405
  if (!boundary) {
2280
2406
  throw new Error(`Boundary "${boundaryId}" was not found.`);
2281
2407
  }
2408
+ cleanupChildren(boundary);
2282
2409
  boundary.replaceChildren(toFragment(fragmentOrTemplate, documentRef));
2283
2410
  api.scan(boundary);
2284
2411
  return boundary;
@@ -2295,11 +2422,12 @@ const __loaderModule = (() => {
2295
2422
  cache: api.cache,
2296
2423
  attributes: attributeConfig
2297
2424
  });
2425
+ cleanupChildren(target);
2298
2426
  target.replaceChildren(toFragment(rendered.html, target.ownerDocument));
2299
2427
  api.scan(target);
2300
2428
  rendered.mount(target);
2301
2429
  rendered.visible(target, api._observeVisible);
2302
- cleanups.add(rendered.cleanup);
2430
+ addCleanup(rendered.cleanup, target, "children");
2303
2431
  return rendered;
2304
2432
  },
2305
2433
 
@@ -2309,7 +2437,7 @@ const __loaderModule = (() => {
2309
2437
  }
2310
2438
  destroyed = true;
2311
2439
  for (const cleanup of [...cleanups]) {
2312
- cleanup();
2440
+ runCleanup(cleanup);
2313
2441
  }
2314
2442
  cleanups.clear();
2315
2443
  },
@@ -2330,6 +2458,13 @@ const __loaderModule = (() => {
2330
2458
  };
2331
2459
 
2332
2460
  signalRegistry._setContext?.({ server: api.server, router: api.router, loader: api, cache: api.cache });
2461
+ api.server?._setContext?.({
2462
+ signals: signalRegistry,
2463
+ handlers: handlerRegistry,
2464
+ loader: api,
2465
+ router: api.router,
2466
+ cache: api.cache
2467
+ });
2333
2468
 
2334
2469
  function bindEventAttributes(scope) {
2335
2470
  for (const element of elementsIn(scope)) {
@@ -2378,7 +2513,7 @@ const __loaderModule = (() => {
2378
2513
  };
2379
2514
 
2380
2515
  element.addEventListener(eventName, listener);
2381
- cleanups.add(() => element.removeEventListener(eventName, listener));
2516
+ addCleanup(() => element.removeEventListener(eventName, listener), element);
2382
2517
  }
2383
2518
 
2384
2519
  function bindSignalAttributes(scope) {
@@ -2413,6 +2548,12 @@ const __loaderModule = (() => {
2413
2548
  bindSignal(element, `attr:${attr}:${path}`, path, (value) => updateAttribute(element, attr, value));
2414
2549
  continue;
2415
2550
  }
2551
+ if (signalName.startsWith("prop:")) {
2552
+ const prop = signalName.slice("prop:".length);
2553
+ const path = element.getAttribute(name);
2554
+ bindSignal(element, `prop:${prop}:${path}`, path, (value) => updateProperty(element, prop, value));
2555
+ continue;
2556
+ }
2416
2557
  if (signalName.startsWith("class:")) {
2417
2558
  const className = signalName.slice("class:".length);
2418
2559
  const path = element.getAttribute(name);
@@ -2481,7 +2622,7 @@ const __loaderModule = (() => {
2481
2622
 
2482
2623
  const read = () => readBinding(path, options);
2483
2624
  apply(read());
2484
- cleanups.add(subscribeBinding(path, () => apply(read())));
2625
+ addCleanup(subscribeBinding(path, () => apply(read())), element);
2485
2626
  }
2486
2627
 
2487
2628
  function bindValueWriter(element, path) {
@@ -2545,7 +2686,7 @@ const __loaderModule = (() => {
2545
2686
  cleanup: signalRegistry.subscribe(`${id}.$status`, () => renderBoundary(boundary))
2546
2687
  };
2547
2688
  boundaryState.set(boundary, state);
2548
- cleanups.add(state.cleanup);
2689
+ addCleanup(state.cleanup, boundary);
2549
2690
  }
2550
2691
  renderBoundary(boundary);
2551
2692
  }
@@ -2561,6 +2702,7 @@ const __loaderModule = (() => {
2561
2702
  if (!template) {
2562
2703
  return;
2563
2704
  }
2705
+ cleanupChildren(boundary);
2564
2706
  boundary.replaceChildren(template.content.cloneNode(true));
2565
2707
  renderingBoundaries.add(boundary);
2566
2708
  try {
@@ -2594,7 +2736,7 @@ const __loaderModule = (() => {
2594
2736
  continue;
2595
2737
  }
2596
2738
  visibleElements.add(element);
2597
- cleanups.add(observeVisible(element, () => runPseudo(element, ref)));
2739
+ addCleanup(observeVisible(element, () => runPseudo(element, ref)), element);
2598
2740
  }
2599
2741
  }
2600
2742
 
@@ -2624,7 +2766,7 @@ const __loaderModule = (() => {
2624
2766
  });
2625
2767
  for (const result of results) {
2626
2768
  if (typeof result === "function") {
2627
- cleanups.add(result);
2769
+ addCleanup(result, element);
2628
2770
  }
2629
2771
  }
2630
2772
  } catch (error) {
@@ -2656,13 +2798,72 @@ const __loaderModule = (() => {
2656
2798
 
2657
2799
  function assertActive() {
2658
2800
  if (destroyed) {
2659
- throw new Error("AsyncLoader has been destroyed.");
2801
+ throw new Error("Loader has been destroyed.");
2802
+ }
2803
+ }
2804
+
2805
+ function addCleanup(cleanup, owner, mode = "self") {
2806
+ if (typeof cleanup !== "function") {
2807
+ return cleanup;
2808
+ }
2809
+ cleanups.add(cleanup);
2810
+ if (owner) {
2811
+ const records = scopedCleanups.get(owner) ?? [];
2812
+ records.push({ cleanup, mode });
2813
+ scopedCleanups.set(owner, records);
2814
+ }
2815
+ return cleanup;
2816
+ }
2817
+
2818
+ function runCleanup(cleanup) {
2819
+ if (typeof cleanup !== "function" || !cleanups.has(cleanup)) {
2820
+ return;
2821
+ }
2822
+ cleanups.delete(cleanup);
2823
+ cleanup();
2824
+ }
2825
+
2826
+ function cleanupChildren(container) {
2827
+ runScopedCleanups(container, "children");
2828
+ for (const child of [...(container.childNodes ?? [])]) {
2829
+ cleanupNode(child);
2830
+ }
2831
+ }
2832
+
2833
+ function cleanupNode(node) {
2834
+ if (node.nodeType !== 1) {
2835
+ return;
2836
+ }
2837
+ for (const element of elementsIn(node)) {
2838
+ runScopedCleanups(element);
2839
+ }
2840
+ }
2841
+
2842
+ function runScopedCleanups(element, mode) {
2843
+ const records = scopedCleanups.get(element);
2844
+ if (!records) {
2845
+ return;
2846
+ }
2847
+ const remaining = [];
2848
+ for (const record of records) {
2849
+ if (mode && record.mode !== mode) {
2850
+ remaining.push(record);
2851
+ continue;
2852
+ }
2853
+ runCleanup(record.cleanup);
2854
+ }
2855
+ if (remaining.length > 0) {
2856
+ scopedCleanups.set(element, remaining);
2857
+ } else {
2858
+ scopedCleanups.delete(element);
2660
2859
  }
2661
2860
  }
2662
2861
 
2663
2862
  return api;
2664
2863
  }
2665
2864
 
2865
+ const AsyncLoader = Loader;
2866
+
2666
2867
  function normalizeClassTokens(value, tokens = new Set()) {
2667
2868
  if (value == null || value === false) {
2668
2869
  return tokens;
@@ -2807,6 +3008,14 @@ const __loaderModule = (() => {
2807
3008
  }
2808
3009
  }
2809
3010
 
3011
+ function updateProperty(element, prop, value) {
3012
+ if (value == null) {
3013
+ element[prop] = "";
3014
+ return;
3015
+ }
3016
+ element[prop] = value;
3017
+ }
3018
+
2810
3019
  function selectAll(scope, selector) {
2811
3020
  const elements = [];
2812
3021
  if (scope?.nodeType === 1 && scope.matches?.(selector)) {
@@ -2855,7 +3064,7 @@ const __loaderModule = (() => {
2855
3064
  })
2856
3065
  );
2857
3066
  }
2858
- return { AsyncLoader };
3067
+ return { Loader, AsyncLoader };
2859
3068
  })();
2860
3069
 
2861
3070
  const __partialsModule = (() => {
@@ -2886,6 +3095,11 @@ const __partialsModule = (() => {
2886
3095
  return registry;
2887
3096
  },
2888
3097
 
3098
+ unregister(id) {
3099
+ assertId(id);
3100
+ return entries.delete(id);
3101
+ },
3102
+
2889
3103
  resolve(id) {
2890
3104
  assertId(id);
2891
3105
  return entries.get(id);
@@ -2973,7 +3187,7 @@ const __partialsModule = (() => {
2973
3187
  })();
2974
3188
 
2975
3189
  const __routerModule = (() => {
2976
- const { AsyncLoader } = __loaderModule;
3190
+ const { Loader } = __loaderModule;
2977
3191
  const { createHandlerRegistry } = __handlersModule;
2978
3192
  const { createSignalRegistry } = __signalsModule;
2979
3193
  const { applyServerResult } = __serverModule;
@@ -3015,6 +3229,15 @@ const __routerModule = (() => {
3015
3229
  return registry;
3016
3230
  },
3017
3231
 
3232
+ unregister(pattern) {
3233
+ assertPattern(pattern);
3234
+ const index = routes.findIndex((candidate) => candidate.pattern === pattern);
3235
+ if (index !== -1) {
3236
+ routes.splice(index, 1);
3237
+ }
3238
+ return entries.delete(pattern);
3239
+ },
3240
+
3018
3241
  match(url) {
3019
3242
  const path = toUrl(url).pathname;
3020
3243
  for (const candidate of routes) {
@@ -3093,7 +3316,7 @@ const __routerModule = (() => {
3093
3316
  const attributeConfig = normalizeAttributeConfig(attributes ?? loader?.attributes);
3094
3317
  const loaderInstance =
3095
3318
  loader ??
3096
- AsyncLoader({
3319
+ Loader({
3097
3320
  root: rootNode,
3098
3321
  signals: signalRegistry,
3099
3322
  handlers: handlerRegistry,
@@ -3444,7 +3667,7 @@ const __appModule = (() => {
3444
3667
  const { createCacheRegistry } = __cacheModule;
3445
3668
  const { createComponentRegistry } = __componentModule;
3446
3669
  const { createHandlerRegistry } = __handlersModule;
3447
- const { AsyncLoader } = __loaderModule;
3670
+ const { Loader } = __loaderModule;
3448
3671
  const { createPartialRegistry } = __partialsModule;
3449
3672
  const { createRouteRegistry, createRouter } = __routerModule;
3450
3673
  const { createServerRegistry } = __serverModule;
@@ -3543,7 +3766,7 @@ const __appModule = (() => {
3543
3766
  started = true;
3544
3767
 
3545
3768
  if (target !== "server") {
3546
- loader = loader ?? AsyncLoader({
3769
+ loader = loader ?? Loader({
3547
3770
  root: options.root,
3548
3771
  signals,
3549
3772
  handlers,
@@ -3894,6 +4117,7 @@ const { defineComponent: defineComponent } = __componentModule;
3894
4117
  const { delay: delay } = __delayModule;
3895
4118
  const { createHandlerRegistry: createHandlerRegistry } = __handlersModule;
3896
4119
  const { html: html } = __htmlModule;
4120
+ const { Loader: Loader } = __loaderModule;
3897
4121
  const { AsyncLoader: AsyncLoader } = __loaderModule;
3898
4122
  const { createPartialRegistry: createPartialRegistry } = __partialsModule;
3899
4123
  const { createRegistryStore: createRegistryStore } = __registryStoreModule;
@@ -3909,4 +4133,4 @@ const { createSignalRegistry: createSignalRegistry } = __signalsModule;
3909
4133
  const { effect: effect } = __signalsModule;
3910
4134
  const { signal: signal } = __signalsModule;
3911
4135
 
3912
- export { asyncSignal, Async, createApp, defineApp, attributeName, defineAttributeConfig, createCacheRegistry, defineCache, component, createComponentRegistry, defineComponent, delay, createHandlerRegistry, html, AsyncLoader, createPartialRegistry, createRegistryStore, createRouteRegistry, createRouter, defineRoute, route, createServerProxy, createServerRegistry, computed, createSignal, createSignalRegistry, effect, signal };
4136
+ export { asyncSignal, Async, createApp, defineApp, attributeName, defineAttributeConfig, createCacheRegistry, defineCache, component, createComponentRegistry, defineComponent, delay, createHandlerRegistry, html, Loader, AsyncLoader, createPartialRegistry, createRegistryStore, createRouteRegistry, createRouter, defineRoute, route, createServerProxy, createServerRegistry, computed, createSignal, createSignalRegistry, effect, signal };