tutuca 0.13.2 → 0.14.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.
@@ -79,14 +79,38 @@ it: whoever is nearest above and provides that name answers, and a
79
79
  `*name` that names the component's own `provide` resolves to that
80
80
  (including its own).
81
81
 
82
- Because a lookup does not name its producer, **one provide name has one
83
- producer per scope chain** — two components providing `color` is a lint
84
- error (`PROVIDE_NAME_COLLISION`), not a last-one-wins surprise. That is
85
- also what lets the render-target teleport below find the producer.
82
+ More than one component may provide the same name. The live render stack
83
+ is a normal shadowing stack, so the **nearest rendered provider wins** —
84
+ a `Theme` nested inside another `Theme` overrides it for its own subtree,
85
+ and nothing has to name a producer.
86
86
 
87
87
  A lookup name starting with an **uppercase** letter is a component
88
88
  **type** rather than a value; see *Looking up component types*.
89
89
 
90
+ ### Ambient names without a provider
91
+
92
+ A name can also be registered **lexically**, as an absolute path from the
93
+ app state root — for a session, a theme, or anything host-owned that no
94
+ component in the tree should have to publish:
95
+
96
+ ```js
97
+ import { path } from "tutuca";
98
+
99
+ app.registerComponents([Workspace, Toolbar], {
100
+ paths: { session: path().field("session"), theme: path().field("ui").field("theme") },
101
+ });
102
+ ```
103
+
104
+ `path()` is the same address builder `ctx.at` hands a handler — `.field(name)`,
105
+ `.index(name, i)`, `.key(name, k)`, chained freely from the state root.
106
+
107
+ Descendants declare `lookup: ["session", "theme"]` and read or render
108
+ `*session` / `*theme` as usual. The mounted root does not have to publish
109
+ them, and no wrapper component needs to exist solely to. Register on a
110
+ nested scope (`scope.registerPaths({…})`) to narrow a name; nearest
111
+ registration wins. The order is still `dyn lex`, so a rendered provider
112
+ answers before a registered path.
113
+
90
114
  ### Looking up component types
91
115
 
92
116
  The same two environments answer a **type** name, and a handler asks with
@@ -139,11 +163,14 @@ component-render target and an iteration source:
139
163
  ```
140
164
 
141
165
  A `provide` value must be **addressable** — a `.field` or a `.seq[.key]`
142
- seq-access, nothing else. (It is both read as `*name` *and* used as a
143
- render-target / teleport path, so a method or constant — which has no
144
- path — is a lint error.) A `lookup` `default`, by contrast, is only a
145
- value fallback and accepts the full grammar, including constants like
146
- `'gray'`. A `provide` can be a sequence/map item access:
166
+ seq-access, nothing else. It is both read as `*name` *and* used as the
167
+ path a render target resumes at, so a method or constant — which has no
168
+ path — is a lint error (`PROVIDE_NOT_ADDRESSABLE`), and the bad provide is
169
+ dropped: `*name` then reads its `default`, or `null`. If a dynamic binding
170
+ reads as its fallback everywhere, lint first and suspect the producer's
171
+ expression. A `lookup` `default`, by contrast, is only a value fallback and
172
+ accepts the full grammar, including constants like `'gray'`. A `provide`
173
+ can be a sequence/map item access:
147
174
 
148
175
  ```js
149
176
  const Root = component({
@@ -160,14 +187,18 @@ There is **no `*name[.key]` form** — a consumer never indexes a dynamic
160
187
  var. The seq-access lives in the producer's `provide` declaration; the
161
188
  consumer just reads the resolved value as `*name`.
162
189
 
163
- **Teleporting.** The component rendered via `<x render="*selected">`
164
- physically lives at the producer (e.g. `Root.items`), not under the
165
- consumer. When an event fires inside that dynamically-rendered subtree,
166
- the runtime expands the *render* path (consumer → … → the rendered
167
- node) to reconstruct the handler, but the *transaction* is teleported:
168
- the mutation skips the intermediate components and lands on the
169
- producer's data. Editing the entry in the consumer and the same entry
170
- in the producer's own view update in lock-step.
190
+ A published **type** is not a render target: it has no path, so
191
+ `<x render="*Cell">` stays unresolvable by construction.
192
+
193
+ **Resuming at the value's path.** The component rendered via
194
+ `<x render="*selected">` physically lives at the producer (e.g.
195
+ `Root.items`), not under the consumer. A provider publishes the value
196
+ *and* the absolute path it lives at, and rendering pushes that path as a
197
+ continuation frame. So an event inside that subtree mutates the selected
198
+ value where it really is, while bubbling pops back out to the visual
199
+ caller that wrote the `*name` and continues through its ancestry. Editing
200
+ the entry in the consumer and the same entry in the producer's own view
201
+ update in lock-step.
171
202
 
172
203
  ## Pseudo-`x` (`@x`)
173
204
 
@@ -160,8 +160,8 @@ A compact worked version of the first three (`method`, `send`/`receive`, and
160
160
  - [messages-and-intents.md](./messages-and-intents.md) — the channels in depth
161
161
  (`send`/`receive`, `intent` and its routes, `sendAtRoot`) and integrating
162
162
  with the outside world.
163
- - [advanced.md](./advanced.md) — `provide` / `lookup` / `*name` and the
164
- clean-namespace boundary.
163
+ - [advanced.md](./advanced.md) — `provide` / `lookup` / `*name`, lexically
164
+ registered paths, and the clean-namespace boundary.
165
165
  - [patterns/coordinate-components.md](./patterns/coordinate-components.md),
166
166
  [patterns/share-state-across-the-tree.md](./patterns/share-state-across-the-tree.md),
167
167
  [patterns/render-a-child-component.md](./patterns/render-a-child-component.md) —
@@ -381,6 +381,12 @@ native `Map`, and native `Set` stay native. Wrap nested component data with
381
381
  | `new Map()` | map | `draft.x.set(key, value)` |
382
382
  | `new Set()` | set | `draft.x.add(value)`, `draft.x.delete(value)` |
383
383
 
384
+ **Every numeric default is a float, whole numbers included** — `count: 0` is a
385
+ float field, not an int, because a JS literal can't say which one it means
386
+ (`0.0` IS `0`). Ask for int explicitly when you want the truncation:
387
+ `count: { type: "int", defaultValue: 0 }` turns a later `3.14` into `3`, while
388
+ the plain `count: 0` keeps it.
389
+
384
390
  Fields do not generate setters or collection mutators. Define only the named
385
391
  handlers your view/API needs; the linter reports a field/method name collision.
386
392
 
@@ -915,7 +921,7 @@ storybook in your own page — see [storybook.md](./storybook.md).
915
921
  - [margaui.md](./margaui.md) — setting up MargaUI styling: install
916
922
  (CDN / npm / vendoring), theme CSS, and `compileClassesToStyleText`.
917
923
  - [semantics.md](./semantics.md) — runtime semantics: path steps, the
918
- transaction lifecycle, dyn-var teleporting, and async key pinning
924
+ transaction lifecycle, located continuations, and async key pinning
919
925
  (`livePath`).
920
926
  - [testing.md](./testing.md) — `getTests` shape and the handler calling
921
927
  convention for tests.
@@ -409,7 +409,7 @@ rules are in [semantics.md](./semantics.md).
409
409
  blocks overview, and *Conventional Module Exports*.
410
410
  - [component-design.md](./component-design.md) — which channel to reach for when.
411
411
  - [semantics.md](./semantics.md) — the path/transaction model behind these
412
- channels: path steps, the transaction lifecycle, teleporting, and the
412
+ channels: path steps, the transaction lifecycle, continuation frames, and the
413
413
  key-pinning rules `livePath` toggles.
414
414
  - [testing.md](./testing.md) — driving message and intent flows from tests.
415
415
  - [cli.md](./cli.md) — the full linter rule list, exit codes, and
@@ -26,7 +26,7 @@ task.
26
26
  ## Context & dynamic bindings
27
27
 
28
28
  - [Share state across the tree](share-state-across-the-tree.md) — `provide` / `lookup` and reading `*name`.
29
- - [Edit through a dynamic target](edit-through-a-dynamic-target.md) — render `*name` and teleport edits back to the owner.
29
+ - [Edit through a dynamic target](edit-through-a-dynamic-target.md) — render `*name` and edit the owner's value in place.
30
30
 
31
31
  ## Composition
32
32
 
@@ -19,9 +19,14 @@ const Toolbar = component({
19
19
  });
20
20
  ```
21
21
 
22
- Because `*active` resolves to a real **path** (not a copied value), the event
23
- fired inside the rendered child is *teleported*: the mutation skips the
24
- intermediate components and lands on `Workspace.sheet`, so the owner and any
25
- other view of the same value update in lock-step. A `provide` can even point at
26
- a seq-access (`.items[.selectedKey]`) to expose "the selected item". This is
27
- the **edit** counterpart of the share-state-across-the-tree recipe.
22
+ Because `*active` carries a real **path** alongside its value (not a copy),
23
+ rendering it *resumes* there: the render site pushes `Workspace.sheet` as the
24
+ active position, so an event fired inside the rendered child mutates
25
+ `Workspace.sheet` itself, and the owner and any other view of the same value
26
+ update in lock-step. Bubbling still returns to the `Toolbar` that wrote the
27
+ `*active`, so an unhandled message keeps walking the visual ancestry.
28
+
29
+ A `provide` can point at a seq-access (`.items[.selectedKey]`) to expose "the
30
+ selected item", and a nested provider of the same name shadows an outer one for
31
+ its own subtree. This is the **edit** counterpart of the
32
+ share-state-across-the-tree recipe.
@@ -19,7 +19,7 @@ const Producer = component({
19
19
  provide: { entries: ".items" },
20
20
  });
21
21
 
22
- // consumer — forwards to the producer's binding by "Component.name"
22
+ // consumer — names what it WANTS, never who provides it
23
23
  const Consumer = component({
24
24
  name: "Selector",
25
25
  lookup: [{ name: "entries", default: ".items" }],
@@ -32,7 +32,22 @@ const Consumer = component({
32
32
  ```
33
33
 
34
34
  `provide` publishes a field under a name; a descendant's `lookup` resolves
35
- `*name` to the nearest matching producer, falling back to `default` when none
36
- is in scope. `*name` works wherever a `.field` does for iteration/rendering.
35
+ `*name` to the nearest matching producer in the live render ancestry, falling
36
+ back to `default` when none is above it. Several components may publish one
37
+ name — the nearest rendered one wins. `*name` works wherever a `.field` does
38
+ for iteration/rendering.
39
+
40
+ For a value nothing in the tree should have to publish — a session, a theme —
41
+ register it on the scope instead, as an absolute path from the state root:
42
+
43
+ ```js
44
+ import { path } from "tutuca";
45
+
46
+ app.registerComponents(comps, { paths: { theme: path().field("theme") } });
47
+ ```
48
+
49
+ Consumers still declare `lookup: ["theme"]` and read `*theme`; a rendered
50
+ provider of the same name still wins over it.
51
+
37
52
  This is the **read** side; to edit the producer's value through the dynamic,
38
53
  see the edit-through-a-dynamic-target recipe.
@@ -31,26 +31,29 @@ references* in [core.md](./core.md)). The step kinds:
31
31
  | `SeqStep` | a sequence entry by **literal** key/index | `.items[2]` |
32
32
  | `SeqAccessStep` | a sequence entry whose key is **read from another field** | `.sheets[.selId]` |
33
33
  | `EachRenderItStep` | an iterated `render-it` item | `<x render-it>` per iter |
34
- | `DynStep` / `DynEachStep` | a dynamic-var (`*x`) render target — a teleport marker | `<x render="*x">` |
35
34
  | `BindStep` / `EachBindStep` | nothing — frame-only (carry scope binds, no addressing) | `@each`, `@enrich-with` |
36
35
 
37
36
  `SeqAccessStep` is the important one for async correctness: it stores the
38
37
  field *names* `seqField` and `keyField`, and resolves the key from the
39
38
  live data each time it runs — see *Key resolution & async races* below.
40
39
 
41
- ### Two derived paths
40
+ ### A dispatch path is a stack of continuation frames
42
41
 
43
- The reconstructed path is transformed two ways depending on use:
42
+ What reconstruction actually returns is a `DispatchPath`: a list of frames,
43
+ each `{ base, items }`. Ordinary rendering extends the top frame's `items`;
44
+ rendering a located binding (`<x render="*sel">`) pushes a NEW frame based
45
+ at the value's own absolute path — see *Rendering with a resumed path*
46
+ below. Two projections come off it:
44
47
 
45
- - **`compact()` → the dispatch path.** Drops frame-only steps, keeps one
46
- step per crossed component (including `DynStep`s). `popStep()` over it
47
- walks every component. Used to drive `ctx.send` / `ctx.intent` and to
48
- locate handlers.
49
- - **`toTransactionPath()` → the transaction path.** Teleports every
50
- `DynStep` (drops the steps interior to its producer..consumer span and
51
- splices in the producer's own steps) so a mutation lands on the data's
52
- real location. A path with no `DynStep` is returned unchanged. Used by
53
- `lookup` / `setValue` to read and write state.
48
+ - **`compact()` → the dispatch path.** Drops frame-only steps inside every
49
+ frame independently, keeping one step per crossed component. `popStep()`
50
+ over it walks every component, and at the top of a frame returns to the
51
+ visual caller. Used to drive `ctx.send` / `ctx.intent` and to locate
52
+ handlers.
53
+ - **`toTransactionPath()` → the transaction path.** The ACTIVE frame alone:
54
+ its absolute base followed by its own steps, so a mutation lands on the
55
+ data's real location. The saved caller frames affect bubbling only. Used
56
+ by `lookup` / `setValue` to read and write state.
54
57
 
55
58
  ## Reconstructing a path from the DOM
56
59
 
@@ -111,21 +114,32 @@ The walk object is shared across hops on purpose: that is what makes the
111
114
  one-shot **per intent** rather than per hop, so a second `ctx.reply` — from this
112
115
  handler or one three hops up — finds the walk already ended.
113
116
 
114
- ## Dynamic-var teleporting
117
+ ## Rendering with a resumed path
115
118
 
116
119
  A component rendered through `<x render="*sel">` *physically lives* at the
117
120
  producer that declared `provide: { sel: … }`, not under the consumer that
118
- wrote the render. The reconstructed dispatch path keeps every intermediate
119
- component (so a walk visits them), but `toTransactionPath()` teleports
120
- the `DynStep`: it pops the steps tagged with the marker's `interiorCids`
121
- and splices in the producer's own steps (`DynStep.teleportSteps()`). The
122
- mutation therefore lands on the producer's data, and the consumer's view
123
- of it updates in lock-step. Authoring view: *Teleporting* in
124
- [advanced.md](./advanced.md).
125
-
126
- When the producer's `provide` value is a seq-access (`.sheets[.selId]`),
127
- the teleported steps include a `SeqAccessStep` — which is where async key
128
- races come from.
121
+ wrote the render. A provider evaluates both halves of a lowercase binding:
122
+ its value, and the absolute path of that value. A descendant retrieves the
123
+ nearest pair and renders the value after pushing its path as a continuation
124
+ frame; the frame's base is recorded in the DOM (`base` in the `§Comp§` meta,
125
+ `data-rp` on a fragment-rooted body) so reconstruction can rebuild it.
126
+
127
+ Consequently:
128
+
129
+ - mutation uses the ACTIVE frame, so an event inside the resumed component
130
+ updates the provider's data, and the consumer's view of it updates in
131
+ lock-step;
132
+ - bubbling reaches the top of that frame and pops directly back to the
133
+ visual caller, then continues through the caller's ancestry.
134
+
135
+ There is no producer search, producer id, interior list, or path rewriting
136
+ at dispatch. Nested providers therefore shadow by live render ancestry: the
137
+ nearest one that actually rendered wins. Authoring view: *Resuming at the
138
+ value's path* in [advanced.md](./advanced.md).
139
+
140
+ When the producer's `provide` value is a seq-access (`.sheets[.selId]`), the
141
+ frame's base ends in a `SeqAccessStep` — which is where async key races come
142
+ from.
129
143
 
130
144
  ## Key resolution & async races
131
145
 
@@ -143,8 +157,9 @@ path — each `SeqAccessStep(seq, keyField)` becomes a literal
143
157
  `SeqStep(seq, resolvedKey)`. The pinned path is stored on the
144
158
  `IntentWalk`, so the answer updates the item that raised the intent
145
159
  regardless of later key changes. (Pinning runs on the transaction path,
146
- after teleporting, because the `SeqAccessStep` may have come from a
147
- `DynStep`.)
160
+ because the `SeqAccessStep` may have come from a resumed frame's base. The
161
+ answer still DISPATCHES at the raiser's position, frames and all; the pinned
162
+ path only decides where its mutation lands.)
148
163
 
149
164
  **Opt out per intent with `livePath: true`:**
150
165
 
@@ -191,5 +206,5 @@ things per step kind:
191
206
  - [messages-and-intents.md](./messages-and-intents.md) — the dispatch **API**:
192
207
  addressed `send`-`receive` vs routed `intent`, `ctx.at`, `$unknown`,
193
208
  intent-handler registration, and the `livePath` option.
194
- - [advanced.md](./advanced.md) — dynamic bindings (`*x`) and the authoring
195
- view of teleporting.
209
+ - [advanced.md](./advanced.md) — dynamic bindings (`*x`), lexically
210
+ registered paths, and the authoring view of resuming at a value's path.