@wcstack/state 2.2.0 → 2.4.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/README.ja.md +305 -18
- package/README.md +305 -18
- package/dist/auto.min.js +1 -1
- package/dist/auto.min.js.map +1 -1
- package/dist/index.d.ts +375 -4
- package/dist/index.esm.js +3797 -257
- package/dist/index.esm.js.map +1 -1
- package/dist/manifest.esm.js +4 -0
- package/dist/parser.esm.js +21 -4
- package/dist/wcs-manifest.json +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -114,6 +114,7 @@ That's it. No build, no bootstrap code, no framework.
|
|
|
114
114
|
- **Event tokens** — the dual of command tokens: receive a wc-bindable element's dispatched events in state via `eventToken.<prop>: tokenName` + the `$on` map
|
|
115
115
|
- **Streams** — fold continuous async flows (async iterables / `ReadableStream`) into reactive properties via the `$streams` declaration, with switchMap-style dependency-driven restart
|
|
116
116
|
- **Path getters** — dot-path key getters (`get "users.*.fullName"()`) for virtual properties at any depth in a data tree, all defined flat in one place with automatic dependency tracking and caching
|
|
117
|
+
- **Recursive paths** — `$recursion: { "nodes.*": "children.*" }` declares where a tree's shape repeats, and one `**` getter (`get "nodes.**.total"()`) covers every depth; `$getAll(path, [])` unions all depths and `$setAll(path, [], value)` broadcasts to all of them
|
|
117
118
|
- **Mustache syntax** — `{{ path|filter }}` in text nodes
|
|
118
119
|
- **Multiple state sources** — JSON, JS module, inline script, API, attribute
|
|
119
120
|
- **SVG support** — full binding support inside `<svg>` elements
|
|
@@ -247,7 +248,7 @@ There is **one state tree per root**. To split state across modules, mount a vol
|
|
|
247
248
|
<div data-wcs="textContent: cart.total"></div>
|
|
248
249
|
```
|
|
249
250
|
|
|
250
|
-
A volume may declare getters, `$watch`, `$listKeys`, `$updatedCallback`, and `$connectedCallback`/`$disconnectedCallback` — all relative to its mount path. `$errorCallback` is root-only (a binding failure is reported once, to the tree's owner). Load order does not matter (a volume connected before the root is grafted when the root registers). Mount paths must be static (`*`, `$`, `#`, `@` are rejected). Changing `mount` after the element has initialized is not supported: the change is ignored with a console warning — remove the element and add a new one with the desired path.
|
|
251
|
+
A volume may declare getters, `$watch`, `$listKeys`, `$updatedCallback`, and `$connectedCallback`/`$disconnectedCallback` — all relative to its mount path. `$errorCallback` is root-only (a binding failure is reported once, to the tree's owner). Load order does not matter (a volume connected before the root is grafted when the root registers). If the root `<wcs-state>` fails to initialize, the volumes already waiting for it settle with a report of their own instead of waiting forever. That report is the end of the line for those volumes: a volume reported as an orphan does not graft itself later, and its mount slot stays reserved for as long as that root node is alive — the slot ledger is a `WeakMap` keyed by the root node and a slot is never released — so connecting a corrected root afterwards does not bring it back. Fix the root `<wcs-state>` and reload the page. Mount paths must be static (`*`, `$`, `#`, `@` are rejected). Changing `mount` after the element has initialized is not supported: the change is ignored with a console warning — remove the element and add a new one with the desired path.
|
|
251
252
|
|
|
252
253
|
> **Migrating from v1's named states:** `<wcs-state name="cart">` + `total@cart` becomes `<wcs-state mount="cart">` + `cart.total`. In v2 the `name` attribute fails fast and `@` in a path is a parse error, each with this exact guidance. Migration table: [docs/state-mount-design.md](../../docs/state-mount-design.md) §9.
|
|
253
254
|
|
|
@@ -1103,6 +1104,207 @@ export default {
|
|
|
1103
1104
|
};
|
|
1104
1105
|
```
|
|
1105
1106
|
|
|
1107
|
+
## Recursive Paths (`$recursion`)
|
|
1108
|
+
|
|
1109
|
+
A path burns its depth into the string. `nodes.*.children.*.total` has exactly two wildcard levels, and nothing about it stretches to three when the tree grows a level — but a tree's depth belongs to the data, not to the code. `$recursion` closes that gap: declare where the shape repeats, then write `**` for "however deep this is".
|
|
1110
|
+
|
|
1111
|
+
```javascript
|
|
1112
|
+
export default {
|
|
1113
|
+
$recursion: { "nodes.*": "children.*" }, // anchor → repeating sub-path
|
|
1114
|
+
|
|
1115
|
+
nodes: [
|
|
1116
|
+
{ value: 1, selected: false, children: [
|
|
1117
|
+
{ value: 10, selected: false, children: [
|
|
1118
|
+
{ value: 100, selected: false, children: [] }
|
|
1119
|
+
]},
|
|
1120
|
+
{ value: 20, selected: false, children: [] }
|
|
1121
|
+
]},
|
|
1122
|
+
{ value: 2, selected: false, children: [] }
|
|
1123
|
+
],
|
|
1124
|
+
|
|
1125
|
+
// One getter, every depth: `**` is bound to the depth being evaluated
|
|
1126
|
+
get "nodes.**.total"() {
|
|
1127
|
+
return this["nodes.**.value"]
|
|
1128
|
+
+ this.$getAll("nodes.**.children.*.total").reduce((a, b) => a + b, 0);
|
|
1129
|
+
},
|
|
1130
|
+
|
|
1131
|
+
// Whole-tree aggregate: `[]` unions every depth
|
|
1132
|
+
get treeTotal() {
|
|
1133
|
+
return this.$getAll("nodes.**.value", []).reduce((a, b) => a + b, 0);
|
|
1134
|
+
},
|
|
1135
|
+
|
|
1136
|
+
clearSelection() {
|
|
1137
|
+
this.$setAll("nodes.**.selected", [], false);
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
```
|
|
1141
|
+
|
|
1142
|
+
For that forest the totals are `131 / 110 / 100 / 20 / 2` and `treeTotal` is `133`.
|
|
1143
|
+
|
|
1144
|
+
**`**` is authoring notation only — it never reaches the engine.** Reading a concrete path (`nodes.*.children.*.total`) materializes the getter for *that* depth on demand, one accessor per depth you actually touch, and everything downstream — `PathInfo`, the dependency graph, `$1`…`$n`, `$resolve`, the list diff — still sees an ordinary fixed-arity path. The reactive core did not learn a new shape.
|
|
1145
|
+
|
|
1146
|
+
### Declaring the recursion point
|
|
1147
|
+
|
|
1148
|
+
`$recursion` maps one **anchor** to the **repeating sub-path** that descends one level. Both name the *element* of a list — a fixed property chain ending in `.*`, never the list itself:
|
|
1149
|
+
|
|
1150
|
+
```javascript
|
|
1151
|
+
$recursion: { "nodes.*": "children.*" } // nodes[i].children[j].children[k]…
|
|
1152
|
+
$recursion: { "data.tree.*": "kids.*" } // a deeper anchor is fine
|
|
1153
|
+
$recursion: { "nodes.*": "nodes.*" } // self-similar spelling is fine too
|
|
1154
|
+
```
|
|
1155
|
+
|
|
1156
|
+
The declaration is what gives `**` a meaning at all: with no `$recursion` on the state, `**` is not a path character (`wcs/recursion-unsupported`), so the notation can never quietly slide into a descendant search. This version accepts **exactly one self-recursive anchor per state**. A wildcard in the middle of an anchor, a second entry, mutual recursion between two anchors, a second `**` in one path, `get "nodes.**"` (that names the node itself, not a computed path under it), a `**` getter whose suffix names the structure (`get "nodes.**.children"()`, `.children.*`, `.children.length` — it would hide the real child list at every depth), two `**` getters that expand to the same concrete path, and recursive *setters* are all rejected when the declaration is read — never reinterpreted.
|
|
1157
|
+
|
|
1158
|
+
The family a declaration defines is infinite, and the state only ever grows the depths it is asked for:
|
|
1159
|
+
|
|
1160
|
+
```
|
|
1161
|
+
k=0 nodes.*
|
|
1162
|
+
k=1 nodes.*.children.*
|
|
1163
|
+
k=2 nodes.*.children.*.children.*
|
|
1164
|
+
```
|
|
1165
|
+
|
|
1166
|
+
### What `**` means where
|
|
1167
|
+
|
|
1168
|
+
`**` is a variable over depth, and whether it is *bound* or *unioned* is decided by context — the same split `*` already has between "the current row" and "every row":
|
|
1169
|
+
|
|
1170
|
+
| Where `**` appears | What it means |
|
|
1171
|
+
|---|---|
|
|
1172
|
+
| A getter key — `get "nodes.**.total"()` | Bound to the depth being evaluated |
|
|
1173
|
+
| A path read inside that getter — `this["nodes.**.value"]` | Bound to the same depth |
|
|
1174
|
+
| `$getAll(path)`, indexes **omitted** | Bound to that depth; only the wildcards *after* `**` expand |
|
|
1175
|
+
| `$getAll(path, [])`, **explicit** | **Union of every depth** — depth-first, pre-order, ascending index |
|
|
1176
|
+
| `$getAll(path, [i, …])` | Rejected: a prefix cannot say which depth it applies to (`wcs/recursion-getall-form`) |
|
|
1177
|
+
| `$setAll(path, [], value)` | Broadcast to every depth, in that same order |
|
|
1178
|
+
| `$resolve`, `$postUpdate`, `$trackDependency`, `$watch` keys, `$listKeys` keys, `data-wcs` in markup, direct assignment | Rejected (`wcs/recursion-unsupported`) |
|
|
1179
|
+
|
|
1180
|
+
The bound forms need a depth to bind to, so they only resolve **inside** a recursive getter — or inside an ordinary row getter under the anchor, or an event handler bound to such a row — each of those carries a real `ListIndex` to read the depth from. Read `this["nodes.**.value"]` from the top level and you get `wcs/recursion-context`, not a silent guess at which node you meant. The depth is read from the innermost evaluation frame only, the same frame the row index comes from: a plain getter that a recursive getter calls (`get "nodes.**.x"() { return this.helper }` with `get helper() { return this["nodes.**.value"] }`) has no row of its own and gets `wcs/recursion-context` too — read `**` in the recursive getter and pass the value on. The union form needs no depth, so it can be read from anywhere: a top-level getter, a plain row getter, a method.
|
|
1181
|
+
|
|
1182
|
+
```javascript
|
|
1183
|
+
this.$getAll("nodes.**.value", []); // [1, 10, 100, 20, 2] — depth-first, pre-order
|
|
1184
|
+
```
|
|
1185
|
+
|
|
1186
|
+
Wildcards *after* `**` expand at each node in the ordinary fixed-arity order, and the walk finishes them before descending to that node's children. With `tags` on the nodes above (`1` → `[3, 4]`, `10` → `[5]`, `20` → `[7]`, the rest empty):
|
|
1187
|
+
|
|
1188
|
+
```javascript
|
|
1189
|
+
this.$getAll("nodes.**.tags.*.v", []); // [3, 4, 5, 7] — node 1's tags, then node 10's, then node 20's
|
|
1190
|
+
```
|
|
1191
|
+
|
|
1192
|
+
### Aggregating without counting grandchildren twice
|
|
1193
|
+
|
|
1194
|
+
That split is the whole game for aggregation, because the recursive getter is what folds the tree:
|
|
1195
|
+
|
|
1196
|
+
```javascript
|
|
1197
|
+
// ✅ Omitted — bound to this depth, so the sum walks only the direct children
|
|
1198
|
+
get "nodes.**.total"() {
|
|
1199
|
+
return this["nodes.**.value"]
|
|
1200
|
+
+ this.$getAll("nodes.**.children.*.total").reduce((a, b) => a + b, 0);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// ❌ `[]` — every child total at every depth. Each node's total would contain its
|
|
1204
|
+
// own descendants' totals again, and the getter ends up asking for itself: in
|
|
1205
|
+
// practice you do not get a wrong number, you get `wcs/getter-cycle`.
|
|
1206
|
+
get "nodes.**.total"() {
|
|
1207
|
+
return this["nodes.**.value"]
|
|
1208
|
+
+ this.$getAll("nodes.**.children.*.total", []).reduce((a, b) => a + b, 0);
|
|
1209
|
+
}
|
|
1210
|
+
```
|
|
1211
|
+
|
|
1212
|
+
The same mistake made from *outside* the recursion is the quiet one — there is no cycle to trip over, just a plausible number that is too big. A union of an aggregate counts every grandchild once inside its parent's total, and once more as an element of the union:
|
|
1213
|
+
|
|
1214
|
+
```javascript
|
|
1215
|
+
// ❌ 363 — every node's total, and every total already contains its subtree
|
|
1216
|
+
get treeTotalWrong() {
|
|
1217
|
+
return this.$getAll("nodes.**.total", []).reduce((a, b) => a + b, 0);
|
|
1218
|
+
}
|
|
1219
|
+
// ✅ 133 — union the raw leaf values
|
|
1220
|
+
get treeTotal() {
|
|
1221
|
+
return this.$getAll("nodes.**.value", []).reduce((a, b) => a + b, 0);
|
|
1222
|
+
}
|
|
1223
|
+
// ✅ 133 — or add up the roots, since each root total already folds its subtree
|
|
1224
|
+
get treeTotalFromRoots() {
|
|
1225
|
+
return this.$getAll("nodes.*.total", []).reduce((a, b) => a + b, 0);
|
|
1226
|
+
}
|
|
1227
|
+
```
|
|
1228
|
+
|
|
1229
|
+
**Union raw values, or sum the roots — never union something that already aggregates its own subtree.** Whether an aggregate double-counts is not decidable from the path string, so no diagnostic claims to catch this one.
|
|
1230
|
+
|
|
1231
|
+
### Writing: broadcast only
|
|
1232
|
+
|
|
1233
|
+
`$setAll` accepts `**` in exactly one form — `[]` plus a plain value — and returns the number of addresses written (5 for the forest above):
|
|
1234
|
+
|
|
1235
|
+
```javascript
|
|
1236
|
+
this.$setAll("nodes.**.selected", [], false); // every node, at every depth
|
|
1237
|
+
```
|
|
1238
|
+
|
|
1239
|
+
Every other form is refused *before* the walk writes anything, so a rejected call leaves the tree untouched. That guarantee covers the checks on the *form*; a leaf under an index spelling (`nodes.**.children.0.value`) can still stop part-way on the *data* — a node whose `children` is empty has no child `0` to write into — exactly as the fixed-arity `$setAll("nodes.*.children.0.value", [], v)` does.
|
|
1240
|
+
|
|
1241
|
+
| Form | Why it is refused |
|
|
1242
|
+
|---|---|
|
|
1243
|
+
| a non-empty prefix | A prefix cannot say which depth it applies to (`wcs/recursion-setall-form`) |
|
|
1244
|
+
| omitted indexes | The write API takes no context, so there is no depth to bind to — pass `[]` |
|
|
1245
|
+
| a mapper function | `(current, ...indexes)` has a different arity at every depth |
|
|
1246
|
+
| `{ spread: true }` | Handing a flat array to a tree needs the author to know the walk order |
|
|
1247
|
+
| `nodes.**`, `nodes.**.children`, `nodes.**.children.*`, `nodes.**.children.length` — and, for a multi-segment repeat such as `branch.children.*`, the `nodes.**.branch` on the way to the list. Index spellings fold to the same forms: `nodes.**.children.0` is a child node, `nodes.**.children.0.total` is the getter | Writing the structure itself (assigning `length` truncates the list) invalidates the child addresses this very write already resolved (`wcs/recursion-structural-write`) |
|
|
1248
|
+
| `nodes.**.total`, or a path inside its value | A recursive getter has no setter — write what it derives from (`wcs/recursion-readonly`) |
|
|
1249
|
+
|
|
1250
|
+
The read-only rule does not depend on spelling `**`. A recursive getter's concrete expansions — `nodes.*.total`, `nodes.*.children.*.total`, … — are refused at the write entry as well, whether the write is a fixed-arity `$setAll`, a `$resolve(path, indexes, value)` or a direct assignment, and whether or not that depth has been materialized yet. Before this check, an unmaterialized expansion looked like a plain missing key and the write landed on the node object, pinning the assigned value as the getter's cached result.
|
|
1251
|
+
|
|
1252
|
+
### The input has to be a tree
|
|
1253
|
+
|
|
1254
|
+
The walk descends by depth and checks the shape it needs as it goes: reaching the **same array instance** twice is refused. If that array belongs to one of the current node's ancestors it is a cycle (`wcs/recursion-cycle`); otherwise two nodes share one child list (`wcs/recursion-shared-list`). Give every node its own `children` array — sharing an *empty* one is fine and untracked, because it has no rows to alias.
|
|
1255
|
+
|
|
1256
|
+
Replacing a row object while keeping its `children` array — `this.nodes = this.nodes.map(n => ({ ...n }))` — is an ordinary update, and the aggregates follow it. The child list keeps its existing row objects — so anything keyed by row identity, such as a `bind-component` child scope's rendered rows and any state you have not bound there, survives — and only the retired row they hung under is swapped for the live one, so the next leaf update dirties the row that is actually on screen ([#256](https://github.com/wcstack/wcstack/issues/256)). Two rows *sharing* one `children` array is a different thing. While both rows are in the list it is unchanged: an array has one set of rows, so both rows always agree on every value, and a row getter that reads its parent (`this["nodes.*.value"]`) is evaluated in the context of the row that owns those rows — the one that first expanded the array. What changed is what happens when that owner is removed from the list: the rows follow one of the rows still on screen, so that row's aggregate tracks the shared data instead of freezing at the removed row's numbers — an array still has one set of rows, so with three rows sharing one array a single survivor follows and the others stay frozen. Putting the removed row back hands them straight back to it when that row's own object comes back — whether you reassign the same array instance, build a new array holding the same rows, or put the row back at a different position. When every row is rebuilt instead (`this.nodes = this.nodes.map(n => ({ ...n }))`), no row object matches and the rows end up under whichever row now occupies the owner's old position. Only the same-array-instance restore behaves this way on 2.3.0; restoring with a new array leaves both rows frozen there. Give every node its own array when a child getter reads upward.
|
|
1257
|
+
|
|
1258
|
+
The ceiling is **128 wildcard levels** on the expanded path. The aggregate above reads one level below the node it is evaluating, so it folds a chain 127 deep and stops at 128 with `wcs/recursion-depth-exceeded`, naming the anchor, the depth reached, the path it was building, and the limit. That check trips before the getter stack's own 128-frame limit (`wcs/getter-depth-exceeded`), so a deep tree is reported as deep instead of being accused of a cycle. Nothing is truncated on the way: a partial aggregate would be a wrong number reported as a right one.
|
|
1259
|
+
|
|
1260
|
+
### Rendering the tree
|
|
1261
|
+
|
|
1262
|
+
`**` cannot appear in markup and there is no recursive `<template>`. A tree is rendered by a **self-referential component** — one custom element whose shadow mounts itself for each child. Inside every scope only one level of path is ever used (`node.children.*`), so the markup does not depend on the depth, and `node.total` resolves through the mount onto the root state's recursive getter, so each node shows its own subtree's aggregate.
|
|
1263
|
+
|
|
1264
|
+
```html
|
|
1265
|
+
<!-- host -->
|
|
1266
|
+
<template data-wcs="for: nodes">
|
|
1267
|
+
<tree-node data-wcs="state.node: nodes.*"></tree-node>
|
|
1268
|
+
</template>
|
|
1269
|
+
```
|
|
1270
|
+
|
|
1271
|
+
```javascript
|
|
1272
|
+
const markup = `
|
|
1273
|
+
<wcs-state bind-component="state"></wcs-state>
|
|
1274
|
+
<span data-wcs="textContent: node.label"></span>
|
|
1275
|
+
<span data-wcs="textContent: node.total"></span>
|
|
1276
|
+
<template data-wcs="for: node.children">
|
|
1277
|
+
<tree-node data-wcs="state.node: node.children.*"></tree-node>
|
|
1278
|
+
</template>`;
|
|
1279
|
+
|
|
1280
|
+
customElements.define("tree-node", class extends HTMLElement {
|
|
1281
|
+
state = {}; // ← no own `node` key — it arrives from the mount
|
|
1282
|
+
constructor() { super(); this.attachShadow({ mode: "open" }); }
|
|
1283
|
+
connectedCallback() { // ← build the shadow here, not in the constructor
|
|
1284
|
+
if (this.shadowRoot.childNodes.length === 0) this.shadowRoot.innerHTML = markup;
|
|
1285
|
+
}
|
|
1286
|
+
});
|
|
1287
|
+
```
|
|
1288
|
+
|
|
1289
|
+
Two things bite here, and both were hit for real:
|
|
1290
|
+
|
|
1291
|
+
- **The component's `state` must not declare the key it is mounted over.** Unrelated methods and private keys are fine — a `node` of its own is not: it hides the mount, so the child shows its own default and never descends. The runtime names that one (`wcs/mount-own-key-shadow`).
|
|
1292
|
+
- **Build the shadow in `connectedCallback`, not in the constructor.** Assigning `innerHTML` in the constructor upgrades the elements inside `<template>` on implementations that do not keep template content inert, and a self-referential element then recurses forever in its own constructor. Real browsers survive it, which makes it an environment-dependent trap rather than an honest crash.
|
|
1293
|
+
|
|
1294
|
+
Fixed depths need none of this: the expanded paths are ordinary paths, so nested `for` templates bind `nodes.*.total` and `nodes.*.children.*.total` like anything else.
|
|
1295
|
+
|
|
1296
|
+
### Not in this version
|
|
1297
|
+
|
|
1298
|
+
Each of these is a diagnostic, never a silent reinterpretation:
|
|
1299
|
+
|
|
1300
|
+
- More than one anchor, mutual recursion, a wildcard in the middle of an anchor, a second `**` in one path
|
|
1301
|
+
- Recursive setters, a `**` getter whose suffix names the structure (`get "nodes.**.children"()`), a concrete getter with the same name as a `**` getter's expansion, and writing through `**` by assignment (`this["nodes.**.x"] = v`, `++` included)
|
|
1302
|
+
- In a recursive `$setAll`: a mapper, `{ spread: true }`, omitted indexes, a non-empty prefix, or a non-array `indexes`. The write API has no evaluation context to bind a depth to, so `[]` is mandatory
|
|
1303
|
+
- In a recursive `$getAll`: a non-empty prefix, or a non-array `indexes`. **Omitting the indexes is valid** — inside a recursive getter it is the bound form, and it reads the depth being evaluated
|
|
1304
|
+
- `**` in `data-wcs`, in `$watch` or `$listKeys` keys, or in `$resolve` / `$postUpdate` / `$trackDependency`
|
|
1305
|
+
- `$recursion` and `**` getters in a volume (`mount=`) or a mounted component (`bind-component`) — declare them on the root state
|
|
1306
|
+
- A recursive `<template>`, a `$depth` variable, and a public `maxDepth` option — none of the three exist
|
|
1307
|
+
|
|
1106
1308
|
## Event Handling
|
|
1107
1309
|
|
|
1108
1310
|
Bind event handlers with `on*` properties:
|
|
@@ -1580,7 +1782,7 @@ interface CommandToken {
|
|
|
1580
1782
|
|
|
1581
1783
|
- The subscriber holds the element via `WeakRef`, so a removed element can still be garbage collected even while it remains in the token's subscriber set
|
|
1582
1784
|
- On `emit`, if the WeakRef has been collected or the element is no longer connected (`isConnected === false`), the subscription is purged automatically (lazy purge)
|
|
1583
|
-
-
|
|
1785
|
+
- Disconnecting the owning `<wcs-state>` keeps the token registry, so the subscriptions still receive commands after the root `<wcs-state>` is re-attached (for example when its host moves in the DOM). While it is disconnected, the state cannot be created, so nothing emits through `$command`
|
|
1584
1786
|
|
|
1585
1787
|
The element's method is invoked with the arguments from `emit`:
|
|
1586
1788
|
|
|
@@ -1757,7 +1959,7 @@ $on: {
|
|
|
1757
1959
|
|
|
1758
1960
|
### Token API
|
|
1759
1961
|
|
|
1760
|
-
Event tokens share the same `Token` pub/sub primitive as command tokens — `name` / `size` / `subscribe` / `unsubscribe` / `emit`, with subscribe-order preservation (see [Token API](#token-api)). The token is resolved from the registry on every event so a re-`setInitialState()` rebuild still reaches the latest `$on` subscribers.
|
|
1962
|
+
Event tokens share the same `Token` pub/sub primitive as command tokens — `name` / `size` / `subscribe` / `unsubscribe` / `emit`, with subscribe-order preservation (see [Token API](#token-api)). The token is resolved from the registry on every event so a re-`setInitialState()` rebuild still reaches the latest `$on` subscribers. Disconnecting the owning `<wcs-state>` keeps the event-token registry, so `$on` handlers (and `on` scans) receive events again once the root `<wcs-state>` is re-attached; an event dispatched while it is disconnected finds no state tree and is not delivered.
|
|
1761
1963
|
|
|
1762
1964
|
## Streams (`$streams`)
|
|
1763
1965
|
|
|
@@ -1872,9 +2074,9 @@ $updatedCallback(paths) {
|
|
|
1872
2074
|
}
|
|
1873
2075
|
```
|
|
1874
2076
|
|
|
1875
|
-
**The rule:** logic that must not depend on what is rendered belongs on a `$watch
|
|
2077
|
+
**The rule:** logic that must not depend on what is rendered belongs on a `$watch`, a `$scan`, or a `$streams` `args`. Keep `$updatedCallback` for "follow what was drawn".
|
|
1876
2078
|
|
|
1877
|
-
That example now
|
|
2079
|
+
That example now accumulates its feed with `$scan` (and re-arms the sentinel from a `$watch`), and the `<b>` is display-only again. This shape — `$updatedCallback` testing a path that is not bound anywhere — is detected statically as **`wcs/updated-callback-unbound`**.
|
|
1878
2080
|
|
|
1879
2081
|
### The limitation that remains
|
|
1880
2082
|
|
|
@@ -1916,10 +2118,10 @@ The handler runs with `this` bound to a **writable** state proxy, so it can writ
|
|
|
1916
2118
|
| Argument | Contract |
|
|
1917
2119
|
|---|---|
|
|
1918
2120
|
| `cur` | The value at drain time (the settled value for the batch) |
|
|
1919
|
-
| `prev` | The value at the **start of the batch** (first-write-wins).
|
|
2121
|
+
| `prev` | The value at the **start of the batch** (first-write-wins). Recorded **only when a primitive is written** (the value before may be an object) — see below |
|
|
1920
2122
|
| `...indexes` | Only for wildcard paths: this scope's own loop indexes, same convention as `$1`, `$2` |
|
|
1921
2123
|
|
|
1922
|
-
**`prev`
|
|
2124
|
+
**`prev` comes only with primitive writes.** It reuses the old value the same-value guard reads before writing a primitive, so watch costs no extra read — and it is `undefined` when the new value is a reference type (an in-place mutation would give you the same reference anyway), for `$postUpdate`, and when `config.sameValueGuard` is off. A primitive written over an object passes that object as `prev`.
|
|
1923
2125
|
|
|
1924
2126
|
**Watch adds no firing condition of its own.** It fires for whatever landed in the update batch. That falls out well: an equal primitive write is already dropped before it is enqueued (so you effectively get change-only firing), while an occurrence write — a `semantics: "event"` property — is deliberately *not* dropped, and still fires with `cur === prev`. If you need edge detection, compare `cur` and `prev` in the handler.
|
|
1925
2127
|
|
|
@@ -1929,16 +2131,17 @@ Firing order is defined in three layers, and only the middle one is yours to ste
|
|
|
1929
2131
|
|
|
1930
2132
|
| Layer | Order | Your control |
|
|
1931
2133
|
|---|---|---|
|
|
1932
|
-
| Mechanisms | `$updatedCallback` → `$watch` → `$streams` restart | fixed |
|
|
2134
|
+
| Mechanisms | `$updatedCallback` → `$scan` → `$watch` → `$streams` restart | fixed |
|
|
1933
2135
|
| Between handlers | declaration order in `$watch` | **reorder the declarations** |
|
|
1934
2136
|
| Between rows of one path | ascending `indexes` | fixed |
|
|
1935
2137
|
|
|
1936
|
-
**The one thing that moves the mechanism layer** is a `<wcs-view-transition>` that accepts the `state` participant. Binding application — and with it `$updatedCallback` — then lands on a frame, while `$watch` and the `$streams` restart stay on the microtask the drain was queued on, because they consume state addresses and not the DOM. For as long as the tag is present the order is `$watch` → `$streams` restart → `$updatedCallback`. Nothing else on the page reorders this layer; see [docs/timing-and-firing-contract.md](https://github.com/wcstack/wcstack/blob/main/docs/timing-and-firing-contract.md) §4.3.
|
|
2138
|
+
**The one thing that moves the mechanism layer** is a `<wcs-view-transition>` that accepts the `state` participant. Binding application — and with it `$updatedCallback` — then lands on a frame, while `$scan`, `$watch` and the `$streams` restart stay on the microtask the drain was queued on, because they consume state addresses and not the DOM. For as long as the tag is present the order is `$scan` → `$watch` → `$streams` restart → `$updatedCallback`. Nothing else on the page reorders this layer; see [docs/timing-and-firing-contract.md](https://github.com/wcstack/wcstack/blob/main/docs/timing-and-firing-contract.md) §4.3.
|
|
1937
2139
|
|
|
1938
2140
|
Key rules:
|
|
1939
2141
|
|
|
1940
2142
|
- **Paths of the tree only** — a path may not contain `@` (the v1 name selector); such a declaration is rejected loudly.
|
|
1941
2143
|
- **Intermediate values are not observable** — a batch that goes `a → b → c` fires once with `cur = c`, `prev = a`, the same contract as binding updates.
|
|
2144
|
+
- **Rows follow the list as it stands at the drain** — a row written and then removed, replaced or cut off in the same job does not fire, a row that only moved into another position does not fire, and each position fires at most once. Replacing a nested list fires for every row of the new array.
|
|
1942
2145
|
- **Row-level diffs want `$listKeys`** — without it, assigning a whole array fires the row watch for *every* row with `prev === undefined`, because no row went through a path write. With `$listKeys` declared, the key match decomposes the assignment into per-field writes, so only changed rows fire and `prev` is a real scalar.
|
|
1943
2146
|
- **A headless row watch requires `$listKeys`** — this is the one place `$watch` is *not* headless on its own. Expanding `items` into `items.*.price` is driven by the list's `for` binding, and declaring a watch deliberately does not register the path as a list. So with neither a `for` binding nor `$listKeys`, assigning the array fires the row watch **zero** times. Add `$listKeys` (the key match writes each field by path, bypassing the expansion) or render the list. Scalar paths — including nested ones like `user.name` — are headless with no such condition.
|
|
1944
2147
|
- **Handler exceptions are isolated** — a throw is reported to the console and the remaining watches (and stream restarts) still run. This differs from `$connectedCallback` / `$updatedCallback`, which fail loudly.
|
|
@@ -1946,6 +2149,76 @@ Key rules:
|
|
|
1946
2149
|
- **Not run on a mounted `bind-component` scope** — mounted components do not execute declaration surfaces: the `$watch` declaration is ignored with a one-time console warning that points to the root state (or a volume — `<wcs-state mount>` hosts `$watch` / `$listKeys` / `$updatedCallback`). This applies to `$streams` too. A plain (unwired Shadow) child owns an independent tree and can declare it.
|
|
1947
2150
|
- **SSR does not run watches** — handler side effects would otherwise execute on both server and client.
|
|
1948
2151
|
|
|
2152
|
+
## Scan (`$scan`)
|
|
2153
|
+
|
|
2154
|
+
`$streams` folds *within* one run — every restart resets the value to `initial` — and `$watch` owns no value. **`$scan`** declares the value that has to outlive both: an accumulation over time, with an owner, a firing unit and a reset condition.
|
|
2155
|
+
|
|
2156
|
+
```html
|
|
2157
|
+
<wcs-state>
|
|
2158
|
+
<script type="module">
|
|
2159
|
+
export default {
|
|
2160
|
+
page: 1,
|
|
2161
|
+
host: "a",
|
|
2162
|
+
$eventTokens: ["message"],
|
|
2163
|
+
$streams: {
|
|
2164
|
+
pageResult: { args: (s) => s.page, source: loadPage },
|
|
2165
|
+
},
|
|
2166
|
+
$scan: {
|
|
2167
|
+
// from: fold each landing of a state path — here, the stream's value
|
|
2168
|
+
feed: {
|
|
2169
|
+
from: "pageResult",
|
|
2170
|
+
initial: { items: [], pages: [] },
|
|
2171
|
+
fold: (feed, chunk) =>
|
|
2172
|
+
chunk?.kind === "success" && !feed.pages.includes(chunk.page)
|
|
2173
|
+
? { items: feed.items.concat(chunk.items), pages: [...feed.pages, chunk.page] }
|
|
2174
|
+
: feed,
|
|
2175
|
+
},
|
|
2176
|
+
// on: fold each event of a declared event token
|
|
2177
|
+
log: {
|
|
2178
|
+
on: "message",
|
|
2179
|
+
initial: [],
|
|
2180
|
+
fold: (log, event) => [...log.slice(-49), event.detail],
|
|
2181
|
+
resetOn: ["host"], // back to [] whenever host changes
|
|
2182
|
+
},
|
|
2183
|
+
},
|
|
2184
|
+
};
|
|
2185
|
+
</script>
|
|
2186
|
+
</wcs-state>
|
|
2187
|
+
|
|
2188
|
+
<template data-wcs="for: feed.items">…</template>
|
|
2189
|
+
```
|
|
2190
|
+
|
|
2191
|
+
| Field | Contract |
|
|
2192
|
+
|---|---|
|
|
2193
|
+
| `from` | A state path. Wildcards are allowed; it may not start with `$`, and may not be a getter or sit under one. Declare exactly one of `from` / `on`. |
|
|
2194
|
+
| `on` | An event-token name declared in `$eventTokens`. |
|
|
2195
|
+
| `initial` | Required. The seed of the accumulator, and what `resetOn` returns to. |
|
|
2196
|
+
| `fold` | Required. `from`: `(acc, cur, prev, ...indexes) => next`. `on`: `(acc, event, ...indexes) => next`. Synchronous, called without `this`, returns a new value. Returning `acc` itself writes nothing. |
|
|
2197
|
+
| `resetOn` | Optional array of plain state paths. When one of them is written, the output returns to `initial`: a `from` scan skips that batch's fold, and an `on` scan folds any event that comes after the write into `initial`. A path under `from` raises; an ancestor of `from` is allowed (start over when the parent is replaced). An object path resets only when that object itself is written, not on writes to its children — list the leaf paths or use a nonce. |
|
|
2198
|
+
|
|
2199
|
+
**The runtime owns the output**, like a `$streams` value. It is materialized from `initial` when the state does not already have that property (plain data is copied, so writing a child path in the plain part of the output never changes the declared `initial`; class instances, frozen values and other non-plain values stay shared with it), and you bind it like any other path. It survives stream restarts, disconnect and reconnect, and a re-set of the same object; a re-set with a new declaration rebuilds the scan. An output name that collides with a getter, a setter, a method or a `$streams` entry raises.
|
|
2200
|
+
|
|
2201
|
+
How the two sources fire:
|
|
2202
|
+
|
|
2203
|
+
| | `from` (a path) | `on` (an event token) |
|
|
2204
|
+
|---|---|---|
|
|
2205
|
+
| Unit | One fold per address that landed in an update batch. Writes made in one job are coalesced. | One fold per event. Two events in one task fold twice. |
|
|
2206
|
+
| When | At the end of the drain, before `$watch`. | Inside the event, before that token's `$on` handlers. |
|
|
2207
|
+
| Output visible | From the next batch. A `$watch` on the output fires then, normally with `prev === undefined` (see below). | Immediately. The `$on` handlers of the same event already see it. |
|
|
2208
|
+
|
|
2209
|
+
Key rules:
|
|
2210
|
+
|
|
2211
|
+
- **Never fold a getter.** A getter re-evaluates whenever its inputs change, so a fold over it would count re-evaluations, not events. A getter as `from` or `resetOn` — or an expansion of a `$recursion` `**` getter such as `nodes.*.total` as `from` — raises at declaration (`wcs/scan-source-computed`).
|
|
2212
|
+
- **One fold per landing, not per page.** A retry after the page is `done`, or reconnecting the page, lands the same page again. When that matters, keep an idempotency key in the fold — the `pages` list above.
|
|
2213
|
+
- **Do not derive a stream's `args` from its own scan output.** A getter over `feed` — or over another scan that folds `feed` — read by `pageResult`'s `args` would restart the stream on its own result, so the runtime raises `wcs/scan-feedback-loop`. Advance the cursor from an event instead. A chunk that lands in the same batch as its stream's restart belongs to the aborted run and is not folded.
|
|
2214
|
+
- **Receive element events through `on`.** A `from` path sees every write to that path, including a bound element's initial sync and a whole-parent write (which arrives with `prev === undefined`). `prev` follows `$watch`'s ledger, so it is also `undefined` for a write made inside the `$scan` / `$watch` listener — by a `$watch` handler, or by another scan whose output is the `from`. The ledger is cleared at the end of that listener, so the `$streams` restart that runs after it in the same drain keeps `prev`.
|
|
2215
|
+
- **Keep folds bounded.** An infinite source must fold into a bounded value (the last N, a count), exactly as with `$streams`.
|
|
2216
|
+
- **Errors are isolated.** A throw, a returned Promise or a value that cannot be read is reported to the console and DevTools and writes nothing (an unreadable row of a wildcard `from` is skipped alone, and row landings are narrowed to one per list position); the other scans, watches and stream restarts still run.
|
|
2217
|
+
- **`$watch` runs after the scan write.** A `$watch` handler in the same drain reads the output as folded, and a value it writes to the output stays. When the `from` source is written again before the output's landing drains — by a `$watch` handler in that drain, say — both land in one batch: a `$watch` on the output then gets the landed value in `prev`, sees `cur` one step ahead, and can fire again with the same value in the next batch, so make it tolerant of a repeated value. Clear an accumulation from a user action with a nonce read by `resetOn`.
|
|
2218
|
+
- **Root only.** A volume (`mount=`) refuses `$scan`, and a mounted `bind-component` scope ignores it with a one-time warning. Under SSR, `from` does not fold; the output is still materialized.
|
|
2219
|
+
|
|
2220
|
+
Reference: [docs/scan.md](https://github.com/wcstack/wcstack/blob/main/packages/state/docs/scan.md). Design record: [docs/state-scan-design.md](https://github.com/wcstack/wcstack/blob/main/docs/state-scan-design.md).
|
|
2221
|
+
|
|
1949
2222
|
## Inputs and Attribute Mirror
|
|
1950
2223
|
|
|
1951
2224
|
`wcBindable.inputs` declares one-way property inputs (state → element). When an entry sets `attribute`, the framework writes the value to that HTML attribute every time it writes the property, so `attributeChangedCallback`, CSS attribute selectors, and DevTools all stay in sync with the property value.
|
|
@@ -2231,7 +2504,7 @@ li {
|
|
|
2231
2504
|
Two consequences to know while that tag accepts the `state` participant:
|
|
2232
2505
|
|
|
2233
2506
|
- The drain lands on a frame instead of a microtask, so code that writes state and then reads the DOM after `await Promise.resolve()` must wait for the transition. `$updatedCallback` still fires immediately after the bindings are applied — its *position* is unchanged, but it moves a frame later along with them.
|
|
2234
|
-
- Because `$watch` and the `$streams` restart stay on the original microtask, they now run **before** `$updatedCallback` instead of after it.
|
|
2507
|
+
- Because `$scan`, `$watch` and the `$streams` restart stay on the original microtask, they now run **before** `$updatedCallback` instead of after it.
|
|
2235
2508
|
|
|
2236
2509
|
Only a batch that actually has bindings to apply is handed to the tag, so a write to a headless path never starts a transition. Without the tag the drain is exactly what it was. See [docs/timing-and-firing-contract.md](https://github.com/wcstack/wcstack/blob/main/docs/timing-and-firing-contract.md) §4.3.
|
|
2237
2510
|
|
|
@@ -2239,7 +2512,7 @@ Only a batch that actually has bindings to apply is handed to the tag, so a writ
|
|
|
2239
2512
|
|
|
2240
2513
|
### Wiring to a path that does not exist is reported
|
|
2241
2514
|
|
|
2242
|
-
When a wired path provably does not resolve against the state, you get one warning at binding time (at declaration time for `$watch`). The diagnostic codes are shared by the console, `@wcstack/lint`, and the VS Code extension:
|
|
2515
|
+
When a wired path provably does not resolve against the state, you get one warning at binding time (at declaration time for `$watch` and `$scan`). The diagnostic codes are shared by the console, `@wcstack/lint`, and the VS Code extension:
|
|
2243
2516
|
|
|
2244
2517
|
```
|
|
2245
2518
|
[@wcstack/state] [wcs/binding-path-missing] Bound path "user.nmae" does not resolve on the state tree:
|
|
@@ -2252,6 +2525,7 @@ dropped. Validate statically: npx @wcstack/lint <file>.
|
|
|
2252
2525
|
| Typo in a nested path (`user.nmae`) | `console.warn` (`wcs/binding-path-missing`). Updates still never arrive — you fix it |
|
|
2253
2526
|
| Typo in a top-level path (`cout`) | Throws on read, with the same wording and did-you-mean |
|
|
2254
2527
|
| Typo in a `$watch` key | `console.warn` (`wcs/watch-path-missing`), reported even for a single segment |
|
|
2528
|
+
| Typo in a `$scan` `from` / `resetOn` path | `console.warn` (`wcs/scan-path-missing`), reported even for a single segment. The scan never folds (or never resets) |
|
|
2255
2529
|
|
|
2256
2530
|
The check **under-approximates**: it stays silent for anything it cannot decide statically, because a false alarm costs more than a missed one. None of these warn:
|
|
2257
2531
|
|
|
@@ -2265,13 +2539,26 @@ So **no warning is not a proof of correctness.** For exhaustive checking, run `n
|
|
|
2265
2539
|
|
|
2266
2540
|
### Index arity, wildcard rank, and getter cycles are checked too
|
|
2267
2541
|
|
|
2268
|
-
Anything that follows mechanically from the path string is reported at runtime and by the linter under the same diagnostic code.
|
|
2542
|
+
Anything that follows mechanically from the path string is reported at runtime and by the linter under the same diagnostic code. Six of the codes below are **runtime-only** in this release — the linter does not emit them: `wcs/getter-depth-exceeded`, `wcs/index-param-range`, `wcs/recursion-context`, `wcs/recursion-shared-list`, `wcs/recursion-cycle` and `wcs/recursion-depth-exceeded`.
|
|
2269
2543
|
|
|
2270
2544
|
| Diagnostic | What it checks | Fix |
|
|
2271
2545
|
|---|---|---|
|
|
2272
2546
|
| `wcs/index-arity` | `$resolve(path, indexes)` must match the `*` count **exactly**; `$getAll(path, indexes)` / `$setAll(path, indexes, …)` have it as an **upper bound** (fewer is a legitimate prefix meaning "expand the rest") | Match the count |
|
|
2273
2547
|
| `wcs/wildcard-rank` | The path's `*` count (and the N in `$N`) must not exceed the enclosing `for` nesting | Add a `for`, or name the row with `$resolve(path, indexes)` |
|
|
2274
|
-
| `wcs/getter-cycle` | Path getters must not form a dependency cycle | Break the cycle |
|
|
2548
|
+
| `wcs/getter-cycle` | Path getters must not form a dependency cycle. At runtime this is the address stack revisiting an address it already holds | Break the cycle |
|
|
2549
|
+
| `wcs/getter-depth-exceeded` | Getter evaluation nests deeper than the engine evaluates in one pass (128 frames), with no address visited twice — the data is simply that deep | Aggregate in fewer levels, or flatten the tree |
|
|
2550
|
+
| `wcs/index-param-range` | `$N` must name an existing wildcard level: `$1` through `$128`, no leading zeros | Use a level that exists |
|
|
2551
|
+
| `wcs/recursion-unsupported` | `**` reached something that does not interpret it — markup, a `$watch` or `$listKeys` key, `$resolve` / `$postUpdate` / `$trackDependency`, an assignment — or the state declares no `$recursion` at all | Use a concrete path, or declare the anchor |
|
|
2552
|
+
| `wcs/recursion-declaration-invalid` | The `$recursion` declaration or a `**` getter key has a shape this version refuses: an anchor or repeat that is not a list element or carries an index segment (`"nodes.0.items.*"`), more than one anchor, a `**` key that is not a getter or has a setter, `get "nodes.**"`, a getter that names the structure, two getters expanding to one concrete path, a concrete getter with the same name as an expansion. The linter reports it first; at runtime it throws when the declaration is read | Fix the declaration as the message says |
|
|
2553
|
+
| `wcs/recursion-anchor` | A `**` path that does not match the one declared anchor (this version takes exactly one self-recursive anchor per state), or whose suffix after `**` is not well-formed — an empty segment (`nodes.**.`, `nodes.**..x`) or a bare `*` right after `**` (`nodes.**.*`) | Spell the anchor as declared, and a real path after it |
|
|
2554
|
+
| `wcs/recursion-context` | A **bound** `**` was read where there is no depth to bind to — the top level, or a getter outside the anchor | Read it from a recursive or row getter, or pass `[]` to union every depth |
|
|
2555
|
+
| `wcs/recursion-getall-form` / `wcs/recursion-setall-form` | An `indexes` argument `**` cannot define. The code is carried by the non-empty prefix (both APIs) and by a non-array `indexes` on `$getAll` (`null`, a string…); omission, a mapper and `{ spread: true }` in a `$setAll` are the same mistake and the linter reports them under the same code, but at runtime they throw with the form named in the message and no code | Omit for the current depth, `[]` for every depth |
|
|
2556
|
+
| `wcs/recursion-structural-write` | A recursive `$setAll` targets the structure itself — a node, its child list, that list's `length`, a child node, or an object on the way to the child list when the repeating sub-path has several segments | Broadcast to a leaf property instead |
|
|
2557
|
+
| `wcs/recursion-readonly` | A write targets a recursive getter, or a path inside the value it derives — a recursive `$setAll` on `nodes.**.total`, or any write to a concrete expansion such as `nodes.*.children.*.total` (fixed-arity `$setAll`, `$resolve` with a value, direct assignment) | Write what the getter derives from |
|
|
2558
|
+
| `wcs/recursion-shared-list` / `wcs/recursion-cycle` | The walk reached the same array instance twice: two nodes sharing one child list, or a list reachable from its own ancestor | Give every node its own child array |
|
|
2559
|
+
| `wcs/recursion-depth-exceeded` | The expanded path needs more than 128 wildcard levels — the tree nests deeper than the engine can address, or it contains a cycle | Flatten the tree, or find the cycle |
|
|
2560
|
+
|
|
2561
|
+
The form each `wcs/recursion-*` row is refusing — and the form to write instead — is spelled out under [Recursive Paths](#recursive-paths-recursion).
|
|
2275
2562
|
|
|
2276
2563
|
Previously **extra indexes were silently discarded** by both APIs, so a mixed-up call returned a plausible-looking wrong value. Both now throw:
|
|
2277
2564
|
|
|
@@ -2465,7 +2752,7 @@ it("renders, re-renders, and runs handlers", async () => {
|
|
|
2465
2752
|
|
|
2466
2753
|
To drive the page the way a user does, keep the state inline (methods included) and dispatch DOM events; a `data-wcs="onclick: up"` handler runs on `button.click()`, and the DOM reflects the write after one `settle()`.
|
|
2467
2754
|
|
|
2468
|
-
- `getBindingsReady(root)` resolves once every binding under `root` (a `document` or a shadow root) is built, and rejects if binding initialization fails (v1.26+).
|
|
2755
|
+
- `getBindingsReady(root)` resolves once every binding under `root` (a `document` or a shadow root) is built, and rejects if binding initialization fails (v1.26+) or if the root's `<wcs-state>` failed to initialize — a root that never loaded reports the failure instead of "ready".
|
|
2469
2756
|
- Updates settle on the microtask queue; a single `setTimeout(0)` after a write is enough.
|
|
2470
2757
|
- `state.items = [...state.items, "cherry"]` is the reactive form — `state.items.push()` is not observed (same rule as in handlers).
|
|
2471
2758
|
- Under happy-dom, `customElements.define` upgrades existing nodes by **replacing** them; "a value reaches the same node after a late define" cannot be asserted headlessly. Event timing differences between happy-dom and real browsers are the other blind spot — keep one browser e2e (Playwright) for those.
|
|
@@ -2552,7 +2839,7 @@ bootstrapState();
|
|
|
2552
2839
|
|
|
2553
2840
|
| Export | Description |
|
|
2554
2841
|
|---|---|
|
|
2555
|
-
| `getBindingsReady(root)` | Resolves once every binding under `root` (a `document` or a shadow root) is built; rejects if binding initialization fails |
|
|
2842
|
+
| `getBindingsReady(root)` | Resolves once every binding under `root` (a `document` or a shadow root) is built; rejects if binding initialization fails, or if the root's state element failed to initialize |
|
|
2556
2843
|
| `buildBindings(root)` | Build the bindings under a `document` or `ShadowRoot` explicitly — what the first `<wcs-state>` registered on a root schedules for it |
|
|
2557
2844
|
| `getConfig()` | The current configuration (read-only view) |
|
|
2558
2845
|
| `defineState(obj)` | Identity function that types `this` inside methods and getters — see [TypeScript Support](#typescript-support) |
|
|
@@ -2578,14 +2865,14 @@ Subpath entries for tooling: `@wcstack/state/parser` (the `data-wcs` parser as a
|
|
|
2578
2865
|
|
|
2579
2866
|
| Property / Method | Description |
|
|
2580
2867
|
|---|---|
|
|
2581
|
-
| `initializePromise` | Resolves when state is fully initialized |
|
|
2582
|
-
| `connectedCallbackPromise` | Resolves once `connectedCallback` has completed (state loaded, `$connectedCallback` run) — what the testing recipes await |
|
|
2868
|
+
| `initializePromise` | Resolves when state is fully initialized — and also **when initialization fails**, so one element's failure never blocks the rest of the page's bindings; the error is delivered on `connectedCallbackPromise` |
|
|
2869
|
+
| `connectedCallbackPromise` | Resolves once `connectedCallback` has completed (state loaded, `$connectedCallback` run) — what the testing recipes await. A **root** element that fails to initialize **rejects** it with the original error, unwrapped, and reports the failure once with `console.error`: an invalid `$` declaration, a source it cannot load, the SSR data merge, a DCC or `bind-component` setup error, or a second root `<wcs-state>` on the same root node (that second element stays unregistered but keeps the state it loaded, so remove it; moving a healthy element in the DOM is not a duplicate and is never refused). A **volume** (`<wcs-state mount="…">`) never rejects it — a volume failure resolves it instead, and some volume failures report nothing of their own: the error leaves as the `connectedCallback` promise that custom-element reactions discard, which a browser console shows as "Uncaught (in promise)" but nothing awaiting these promises (a test recipe, `renderToString()`) ever sees. Detaching an element while its source is still loading rejects nothing — that connection just ends, and re-appending the element (row pooling) initializes it and resolves normally. For the exact behaviour of any single failure site, read `__tests__/integration.initFailureDiagnostics.test.ts`: it pins every case |
|
|
2583
2870
|
| `listPaths` | Set of paths used in `for` loops |
|
|
2584
2871
|
| `getterPaths` | Set of paths defined as getters |
|
|
2585
2872
|
| `setterPaths` | Set of paths defined as setters |
|
|
2586
2873
|
| `createState(mutability, callback)` | Create a state proxy (`"readonly"` or `"writable"`) |
|
|
2587
2874
|
| `createStateAsync(mutability, callback)` | Async version of `createState` |
|
|
2588
|
-
| `setInitialState(state)` | Set state programmatically (before initialization) |
|
|
2875
|
+
| `setInitialState(state)` | Set state programmatically (before initialization). Throws if the element already failed to initialize — such an element cannot be re-armed; remove it and create a new one |
|
|
2589
2876
|
| `nextVersion()` | Increment and return version number |
|
|
2590
2877
|
|
|
2591
2878
|
## Architecture
|