tutuca 0.9.97 → 0.9.99

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.
@@ -83,9 +83,10 @@ return curLeaf !== newLeaf ? txnPath.setValue(curRoot, newLeaf) : curRoot;
83
83
  ```
84
84
 
85
85
  The root swap is atomic and identity-cheap: unchanged subtrees keep their
86
- references, so re-render is incremental. Cross-transaction ordering and
87
- fan-out completion are tracked by `Task` (a transaction's `task` resolves
88
- once its dependency tasks do).
86
+ references, so re-render is incremental. Per-dispatch completion is tracked
87
+ by `Completion` (counter-based): `whenSettled()` resolves once a
88
+ transaction's own work finishes, `whenSubtreeSettled()` once the subtree it
89
+ spawned (requests, follow-on sends) settles too.
89
90
 
90
91
  ## Dispatch channels, semantically
91
92
 
@@ -27,7 +27,7 @@ same file is a valid target for `tutuca lint` / `test` / `render` too.
27
27
 
28
28
  | Export | Returns | Used for |
29
29
  | ------ | ------- | -------- |
30
- | `getComponents()` | `[Comp, ...]` | stories — return **every** component the module defines, children and helpers included |
30
+ | `getComponents()` | `[Comp, ...]` | stories — return **every** component the module defines, children and helpers included. Components dedup by identity, so re-listing a leaf that another module also lists is safe (a composition module can re-list every leaf it uses). |
31
31
  | `getExamples()` | one section, or an array of sections | the catalog cards |
32
32
  | `getTests({ describe, test, expect })` | tests | the pre-serve test run (optional) |
33
33
  | `getMacros()` | `{ name: macro }` | macros referenced in views (optional) |
@@ -37,7 +37,12 @@ same file is a valid target for `tutuca lint` / `test` / `render` too.
37
37
  ## Authoring stories (`getExamples`)
38
38
 
39
39
  Return one section, or an array of sections to group examples under multiple
40
- headings. A section is `{ title, description?, items: [...] }`:
40
+ headings. A section is `{ title, description?, items: [...] }`. An array of one
41
+ section behaves exactly like returning that section directly — both go through
42
+ the same `Section.fromData`, which **throws** on a malformed section (missing
43
+ `title`, not an object) rather than rendering a placeholder title. (If you saw
44
+ broken titles from a one-element array in an older build, that predates array
45
+ support — it has shipped since well before 0.9.88.)
41
46
 
42
47
  ```js
43
48
  import { component, html } from "tutuca";
@@ -114,10 +114,21 @@ export function getTests({ describe, test, expect, drive }) {
114
114
  - `drive(value, phase, opts?)` builds a transactor over `value`, dispatches the
115
115
  phase's actions at the root, awaits the whole cascade (including async
116
116
  requests), and returns the **settled** instance.
117
+ - `drive` **always originates at the root** — there is no `at:`/path option. To
118
+ exercise a handler on a nested child, call it directly with `.call(child, …)`.
117
119
  - `phase` is the same shape as an example's `on.init`
118
120
  (`{ send, bubble, request, input, do }`; see
119
121
  [storybook.md](./storybook.md#lifecycle-hooks-on)). `args` may be a function
120
122
  `(self) => [...]`.
123
+ - A `bubble` action is a **no-op under `drive`**: bubbles travel child→parent, and
124
+ at the root there is no ancestor to receive it (the root's own `bubble` handler
125
+ is skipped too). To test a `bubble` handler, call it directly. (`drive` warns
126
+ when a phase contains a `bubble`.)
127
+ - These are *action kinds*, not methods. `$`-prefixed **methods** (auto setters/
128
+ togglers, `$foo`) are not an action kind — `on`/`drive` can only reach state
129
+ through `input`/`receive`/`response` handlers. To put a component into a method-
130
+ driven state for a test, call the method directly or route it through an `input`
131
+ handler.
121
132
  - `request` actions resolve against the module's `getRequestHandlers()`.
122
133
  - `opts.onMessage(message, before, after)` observes every committed transaction —
123
134
  `message` is `{ kind, name, args, path }`, `before`/`after` are the root values
@@ -258,12 +269,9 @@ The "bad" forms force every test to construct
258
269
  `{ target: { value: "42" } }` (or a fuller stub when more fields are
259
270
  read), which is brittle and obscures intent.
260
271
 
261
- Built-in named args (full list in [core.md](./core.md) *Event
262
- Handling*): `value`, `valueAsInt`, `valueAsFloat`, `target`, `event`,
263
- `isAlt`, `isShift`, `isCtrl` / `isCmd`, `key`, `keyCode`, `isUpKey`,
264
- `isDownKey`, `isSend`, `isCancel`, `isTabKey`, `ctx`, `dragInfo`.
265
- `ctx` is auto-appended last. Reach for `event` only when no narrower
266
- arg fits.
272
+ The built-in named args are listed in [core.md](./core.md) *Event
273
+ Handling*; `ctx` is auto-appended last. Reach for `event` only when no
274
+ narrower arg fits.
267
275
 
268
276
  ## Worked example
269
277
 
@@ -1744,37 +1744,47 @@ class Renderer {
1744
1744
  renderEach(stack, iterInfo, node, viewName) {
1745
1745
  const { seq, filter, loopWith } = iterInfo.eval(stack);
1746
1746
  const r = [];
1747
- const { iterData, start, end } = unpackLoopResult(loopWith.call(stack.it, seq), seq);
1748
- getSeqInfo(seq)(seq, (key, value, attrName) => {
1749
- if (filter.call(stack.it, key, value, iterData)) {
1750
- const dom = this.renderIt(stack.enter(value, { key }, true), node, key, viewName);
1751
- this.pushEachEntry(r, node.nodeId, attrName, key, dom);
1752
- }
1753
- }, start, end);
1747
+ const { iterData, start, end, keys } = unpackLoopResult(loopWith.call(stack.it, seq, makeLoopCtx(stack, filter)), seq);
1748
+ const renderOne = (key, value, attrName) => {
1749
+ const dom = this.renderIt(stack.enter(value, { key }, true), node, key, viewName);
1750
+ this.pushEachEntry(r, node.nodeId, attrName, key, dom);
1751
+ };
1752
+ if (keys)
1753
+ imKeysIter(seq, renderOne, keys);
1754
+ else
1755
+ getSeqInfo(seq)(seq, (key, value, attrName) => {
1756
+ if (filter.call(stack.it, key, value, iterData))
1757
+ renderOne(key, value, attrName);
1758
+ }, start, end);
1754
1759
  return r;
1755
1760
  }
1756
1761
  renderEachWhen(stack, iterInfo, view, nid) {
1757
1762
  const { seq, filter, loopWith, enricher } = iterInfo.eval(stack);
1758
1763
  const r = [];
1759
1764
  const it = stack.it;
1760
- const { iterData, start, end } = unpackLoopResult(loopWith.call(it, seq), seq);
1761
- getSeqInfo(seq)(seq, (key, value, attrName) => {
1762
- if (filter.call(it, key, value, iterData)) {
1763
- const cachePath = enricher ? [view, it, value] : [view, value];
1764
- const binds = { key, value };
1765
- const cacheKey = `${nid}-${key}`;
1766
- if (enricher)
1767
- enricher.call(it, binds, key, value, iterData);
1768
- const cachedNode = this.cache.get(cachePath, cacheKey);
1769
- if (cachedNode)
1770
- this.pushEachEntry(r, nid, attrName, key, cachedNode);
1771
- else {
1772
- const dom = this.renderView(view, stack.enter(value, binds, false));
1773
- this.pushEachEntry(r, nid, attrName, key, dom);
1774
- this.cache.set(cachePath, cacheKey, dom);
1775
- }
1765
+ const { iterData, start, end, keys } = unpackLoopResult(loopWith.call(it, seq, makeLoopCtx(stack, filter)), seq);
1766
+ const renderOne = (key, value, attrName) => {
1767
+ const cachePath = enricher ? [view, it, value] : [view, value];
1768
+ const binds = { key, value };
1769
+ const cacheKey = `${nid}-${key}`;
1770
+ if (enricher)
1771
+ enricher.call(it, binds, key, value, iterData);
1772
+ const cachedNode = this.cache.get(cachePath, cacheKey);
1773
+ if (cachedNode)
1774
+ this.pushEachEntry(r, nid, attrName, key, cachedNode);
1775
+ else {
1776
+ const dom = this.renderView(view, stack.enter(value, binds, false));
1777
+ this.pushEachEntry(r, nid, attrName, key, dom);
1778
+ this.cache.set(cachePath, cacheKey, dom);
1776
1779
  }
1777
- }, start, end);
1780
+ };
1781
+ if (keys)
1782
+ imKeysIter(seq, renderOne, keys);
1783
+ else
1784
+ getSeqInfo(seq)(seq, (key, value, attrName) => {
1785
+ if (filter.call(it, key, value, iterData))
1786
+ renderOne(key, value, attrName);
1787
+ }, start, end);
1778
1788
  return r;
1779
1789
  }
1780
1790
  renderView(view, stack) {
@@ -1810,8 +1820,17 @@ var filterAlwaysTrue = (_v, _k, _seq) => true;
1810
1820
  var nullLoopWith = (seq) => ({ iterData: { seq } });
1811
1821
  var unpackLoopResult = (result, seq) => {
1812
1822
  const r = result ?? {};
1813
- return { iterData: r.iterData ?? { seq }, start: r.start, end: r.end };
1823
+ return { iterData: r.iterData ?? { seq }, start: r.start, end: r.end, keys: r.keys };
1824
+ };
1825
+ var imKeysIter = (seq, visit, keys) => {
1826
+ const attrName = isIndexed(seq) ? "si" : "sk";
1827
+ for (const key of keys)
1828
+ visit(key, seq.get(key), attrName);
1814
1829
  };
1830
+ var makeLoopCtx = (stack, filter) => ({
1831
+ lookup: (name) => stack.lookupBind(name),
1832
+ filter: (key, value, iterData) => filter.call(stack.it, key, value, iterData)
1833
+ });
1815
1834
  var imIndexedIter = (seq, visit, start, end) => {
1816
1835
  const [s, e] = normalizeRange(start, end, seq.size);
1817
1836
  for (let i = s;i < e; i++)
@@ -2359,11 +2378,11 @@ class IterInfo {
2359
2378
  return { seq, filter, loopWith, enricher };
2360
2379
  }
2361
2380
  enrichBinds(stack, key) {
2362
- const { seq, loopWith, enricher } = this.eval(stack);
2381
+ const { seq, filter, loopWith, enricher } = this.eval(stack);
2363
2382
  const value = seq?.get ? seq.get(key, null) : null;
2364
2383
  const binds = { key, value };
2365
2384
  if (enricher) {
2366
- const { iterData } = unpackLoopResult(loopWith.call(stack.it, seq), seq);
2385
+ const { iterData } = unpackLoopResult(loopWith.call(stack.it, seq, makeLoopCtx(stack, filter)), seq);
2367
2386
  enricher.call(stack.it, binds, key, value, iterData);
2368
2387
  }
2369
2388
  return binds;
@@ -3714,6 +3733,13 @@ function phaseOps(phase) {
3714
3733
  function resolveArgs(args, self) {
3715
3734
  return typeof args === "function" ? args(self) ?? [] : args ?? [];
3716
3735
  }
3736
+ function phaseHasBubble(phase) {
3737
+ if (!phase)
3738
+ return false;
3739
+ if (phase.bubble?.length)
3740
+ return true;
3741
+ return (phase.do ?? []).some((op) => op.type === "bubble");
3742
+ }
3717
3743
  function dispatchPhase(dispatcher, targetPath, phase, self) {
3718
3744
  if (!phase)
3719
3745
  return;
@@ -4007,7 +4033,8 @@ class FieldSet extends Field {
4007
4033
  }
4008
4034
  function mkCompField(field, scope, args) {
4009
4035
  const Comp = scope?.lookupComponent(field.type) ?? null;
4010
- console.assert(!scope || Comp !== null, "component not found", { field });
4036
+ if (Comp === null)
4037
+ console.warn(scope ? `component field "${field.name}": component "${field.type}" not found in scope` : `component field "${field.name}": cannot resolve component "${field.type}" — built without a registered scope (use ${field.type}.make({}) as the default, or build via a registered component)`);
4011
4038
  return Comp?.make({ ...field.args, ...args }, { scope }) ?? null;
4012
4039
  }
4013
4040
 
@@ -4156,6 +4183,7 @@ export {
4156
4183
  removeIn,
4157
4184
  remove,
4158
4185
  phaseOps,
4186
+ phaseHasBubble,
4159
4187
  mergeWith,
4160
4188
  mergeDeepWith,
4161
4189
  mergeDeep,