@wcstack/state 2.3.0 โ 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +177 -52
- package/README.md +176 -51
- package/dist/auto.min.js +1 -1
- package/dist/auto.min.js.map +1 -1
- package/dist/index.d.ts +186 -5
- package/dist/index.esm.js +6347 -3619
- package/dist/index.esm.js.map +1 -1
- package/dist/manifest.esm.js +2 -0
- package/dist/wcs-manifest.json +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> ๐ค **AI coding agents**: This README is a package-level reference, not the primary entry point for building a wcstack application. If you have not already done so, first read the repository [README](https://github.com/wcstack/wcstack#readme) and [AGENTS.md](https://github.com/wcstack/wcstack/blob/main/AGENTS.md), then use the [wcstack-app skill](https://github.com/wcstack/wcstack-skill).
|
|
4
4
|
|
|
5
|
-
**This is not another convenient frontend framework. It
|
|
5
|
+
**This is not another convenient frontend framework. It brings a lineage established outside frontend development โ the one where a path string is the contract between view and model โ onto web standards.**
|
|
6
6
|
|
|
7
7
|
Most libraries place the coupling point between UI, state, and components inside JavaScript. `@wcstack/state` does not. It assumes no virtual DOM, no compilation step, no hooks, no selectors. UI and state are connected by HTML and path strings alone.
|
|
8
8
|
|
|
@@ -32,7 +32,7 @@ This is not React / Vue / Solid with a different syntax. Those put the coupling
|
|
|
32
32
|
| JavaScript is the center of rendering | HTML and the DOM are the center |
|
|
33
33
|
| State is pulled into components | Paths are declared and the DOM connects to state |
|
|
34
34
|
| hooks / selectors / signals express subscriptions | Attributes and paths express bindings |
|
|
35
|
-
| The whole app runs inside a framework execution model | A
|
|
35
|
+
| The whole app runs inside a framework execution model | A reactive layer is added on top of web standards, and the page stays a page |
|
|
36
36
|
|
|
37
37
|
The nearer relatives are the **attribute-directive, no-build libraries** โ Alpine.js, petite-vue and their kind. They share the premise (attributes on plain HTML, no compiler) and differ on two points that decide the choice:
|
|
38
38
|
|
|
@@ -45,6 +45,23 @@ The nearer relatives are the **attribute-directive, no-build libraries** โ Alp
|
|
|
45
45
|
|
|
46
46
|
On those axes the comparison is concrete: the [Performance](#performance) section below is one, and the drivers under `e2e/bench/` regenerate it on your own hardware.
|
|
47
47
|
|
|
48
|
+
### The Lineage Outside JavaScript
|
|
49
|
+
|
|
50
|
+
The premise โ *a path string is the whole contract between a view and a model* โ is older than the framework era, and most of it was worked out outside JavaScript. Inside it, the direct ancestors are Knockout's `data-bind="text: user.name"` (an attribute carrying the binding, though it evaluates expressions and needs `ko.observable` wrappers) and Polymer's path system, which had dotted paths, `items.*` observers and `this.set("users.0.name", v)` โ but required `set()` / `notifyPath()`, because plain assignment could not be observed on the platform of its time. Naming the older lineage is more useful than claiming novelty:
|
|
51
|
+
|
|
52
|
+
| Lineage | What it already had | What differs here |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| **Spreadsheets** (VisiCalc, 1979) | An address, a formula declaring what a cell **is**, a dependency graph, lazy recomputation โ and no update code anywhere | Names instead of grid coordinates, and one formula per *shape* rather than per cell: `get "cart.items.*.subtotal"()` is not filled down into the rows; the wildcard is the definition |
|
|
55
|
+
| **Cocoa Bindings / KVCโKVO** (NeXT's EOF, 1994; Mac OS X 10.3, 2003) | Key paths (`person.address.street`), a binding triple of target + key path + value transformer, and collection operators that aggregate along a path (`@sum.items.price`) | The triple lives in the markup instead of a nib or a `bind:toObject:withKeyPath:` call, so it can be grepped, linted and diffed. Change detection is an ES Proxy over plain objects rather than KVC compliance |
|
|
56
|
+
| **XForms** (W3C Recommendation, 2003) | Model / instance / view separation, `ref` paths into the instance, and `<bind calculate="โฆ">` โ a computed value declared **at a path**, the direct ancestor of a path getter | The path is an address and nothing else: the computation is a JavaScript getter on the state, not XPath inside an attribute. And it runs in a stock browser, with no XForms processor |
|
|
57
|
+
| **WPF / XAML** (2006) | `{Binding Path=User.Name, Mode=TwoWay}`; a `DataContext` that re-roots a whole subtree; `UpdateSourceTrigger` choosing when the source is written; `IValueConverter` between the ends | One state tree per root, rather than a context inherited down the visual tree with `RelativeSource` / `ElementName` escapes โ `state: user` is that re-rooting, written in the host's HTML. Converters are a closed set of 46 filters, not classes you register, and nothing is compiled |
|
|
58
|
+
| **Android Data Binding** (2015) | The path in the layout file itself โ `android:text="@{user.name}"`, `@={}` for two-way | No build step and no generated binding class, and no expressions inside the attribute |
|
|
59
|
+
| **SCADA / HMI tag binding** (industrial, decades) | Widget properties wired to tag paths (`Line1/Tank/Level`) by configuration alone, and *indirect* bindings that parameterize the path (`Folder/Tag_{1}`) so one screen drives many devices | The tree carries derived values, lists and mounted components, not a flat namespace of scalars; the parameter is a loop's wildcard, resolved by the row the binding sits in rather than assigned from a dropdown |
|
|
60
|
+
|
|
61
|
+
Wildcards also resemble MQTT topic filters (`sensor/+/temperature`) and OSC address patterns (`/synth/*/freq`), but those select **messages in flight**. `items.*.price` names state addresses, and the one string is both the subscription and the write target.
|
|
62
|
+
|
|
63
|
+
What survives the comparison as genuinely new is narrow: the **wildcard path getter** โ a getter whose *key* is a path pattern, so one definition serves every row and the dependency edge is held per pattern instead of per cell. The rest is a recombination of the lineage above onto three things none of them could assume: Custom Elements, ES Proxy and Import Maps.
|
|
64
|
+
|
|
48
65
|
## First Principle: Path as the Universal Contract
|
|
49
66
|
|
|
50
67
|
In every existing framework, the **component** is the coupling point between UI and state. Components import state hooks, selectors, or reactive primitives, and the binding happens inside JavaScript. No matter how cleanly you separate your state store, there is always glue code in the component that pulls state in.
|
|
@@ -55,23 +72,23 @@ In every existing framework, the **component** is the coupling point between UI
|
|
|
55
72
|
|-------|---------------|----------------------|
|
|
56
73
|
| **State** (`<wcs-state>`) | Data structure and business logic | Which DOM nodes are bound |
|
|
57
74
|
| **UI** (`data-wcs`) | Path strings and display intent | How state is stored or computed |
|
|
58
|
-
| **Components** (`state: path`) | The mount table the host writes |
|
|
75
|
+
| **Components** (`state: path`) | The mount table the host writes | Who mounted it, and what the rest of the tree holds |
|
|
59
76
|
|
|
60
77
|
Three levels of path contracts keep everything loosely coupled:
|
|
61
78
|
|
|
62
|
-
1. **UI โ State** โ A `data-wcs="textContent: user.name"` attribute is the entire binding. No hooks, no selectors, no reactive primitives.
|
|
79
|
+
1. **UI โ State** โ A `data-wcs="textContent: user.name"` attribute is the entire binding. No hooks, no selectors, no reactive primitives: no component code imports a reactive primitive or registers a subscription. A `bind-component` class still declares its own plain `state` object and reads it like a plain object โ what never appears is glue that pulls state into the component.
|
|
63
80
|
|
|
64
|
-
2. **Component โ Component** โ The host mounts a subtree onto each component (`<my-card data-wcs="state: user">`), and volumes graft extra modules onto the tree (`<wcs-state mount="i18n">`). Components never import
|
|
81
|
+
2. **Component โ Component** โ The host mounts a subtree onto each component (`<my-card data-wcs="state: user">`), and volumes graft extra modules onto the tree (`<wcs-state mount="i18n">`). Components never import one another, and a whole-object mount is a path prefix on the single tree and nothing more. Two declarative forms reach further, each spelled out where it is defined: the [per-property form](#host-usage) (`state.message: user.name`) has the host name the component's own keys, and an [exported getter](#exported-getters-reading-a-components-getter-from-outside) lets the host read a value the component computes โ that binding then depends on a component being mounted there.
|
|
65
82
|
|
|
66
83
|
3. **Loop context** โ Inside a `for` loop, `*` acts as an abstract index. Bindings like `items.*.price` resolve to the current element automatically. The template doesn't know its concrete position โ the wildcard is the contract.
|
|
67
84
|
|
|
68
85
|
### Why This Matters
|
|
69
86
|
|
|
70
|
-
This
|
|
87
|
+
This separates UI and state with **no JavaScript intermediary**. You can:
|
|
71
88
|
|
|
72
|
-
- Redesign the
|
|
89
|
+
- Redesign the UI without touching state logic โ as far as the logic does not hang off what is rendered: a live binding is one of the three [demand roots](#demand-roots--what-makes-a-getter-run), so an element you think of as display-only can be the page's only subscription
|
|
73
90
|
- Refactor state structure and only update path strings
|
|
74
|
-
- Read the HTML
|
|
91
|
+
- Read the HTML and know every binding; the dependencies that are not in the HTML (`$watch`, `$streams`, `$scan`) are all declared in one place, the state
|
|
75
92
|
|
|
76
93
|
The path contract works like a URL in a REST API โ a simple string that both sides agree on, with no shared code between them. It's the natural result of building on HTML's declarative nature rather than inventing a template language on top of JavaScript.
|
|
77
94
|
|
|
@@ -101,29 +118,46 @@ That's it. No build, no bootstrap code, no framework.
|
|
|
101
118
|
|
|
102
119
|
## Features Derived from This Principle
|
|
103
120
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
121
|
+
Every row is a section of this README. Unless it appears under [Where the neighbours come in](#where-the-neighbours-come-in), it ships in this package.
|
|
122
|
+
|
|
123
|
+
| Area | In one line | Reference |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| **Path model** | Dot paths address state; `*` is an abstract index, `**` a depth, `$1` / `$2` name the axes | [First principle](#first-principle-path-as-the-universal-contract) ยท [Loop index variables](#loop-index-variables-1-2) |
|
|
126
|
+
| **Binding syntax** | One `data-wcs` attribute carries property / text / class / style / attribute / event bindings; `{{ }}` in text nodes; the same inside `<svg>` | [Binding syntax](#binding-syntax) ยท [Mustache](#mustache-syntax) ยท [SVG](#svg-support) |
|
|
127
|
+
| **Structural directives** | `for` and `if` / `elseif` / `else` on `<template>` elements | [Structural directives](#structural-directives) |
|
|
128
|
+
| **Row identity** | Rows diff by reference, so a sort or a filter reuses the DOM; `$listKeys` keeps row DOM and row objects across refetched arrays | [`$listKeys`](#listkeys--identity-for-refetched-rows) |
|
|
129
|
+
| **Forms** | Two-way binding for `input` / `select` / `textarea`, a radio group to one value, a checkbox group to an array, `#ro` / `#onchange` / `#prevent` / `#stop` | [Two-way binding](#two-way-binding) ยท [Modifiers](#modifiers) |
|
|
130
|
+
| **Filters** | 46 built-ins, chainable, locale-aware formatting that reads `<html lang>` | [Filters](#filters) ยท [Locale](#locale) |
|
|
131
|
+
| **Derived state** | Path getters declare virtual properties at any depth from one flat place; they chain, and they take setters | [Path getters](#path-getters-computed-properties) |
|
|
132
|
+
| **Aggregation and bulk write** | `$getAll` / `$setAll` / `$resolve` read and write across `items.*.price` without rebuilding the array | [Proxy APIs](#proxy-apis) |
|
|
133
|
+
| **Recursive paths** | `$recursion` declares where a tree's shape repeats; one `**` getter covers every depth | [Recursive paths](#recursive-paths-recursion) |
|
|
134
|
+
| **Reactivity** | An ES Proxy tracks reads per address, caches per address, invalidates in dependency order and batches DOM writes on a microtask | [Updating state](#updating-state) ยท [Dependency tracking boundaries](#dependency-tracking-boundaries) |
|
|
135
|
+
| **What makes a getter run** | Getters are lazy. Demand comes from a live binding, a `$watch` or a `$streams` `args` โ and from nowhere else | [Demand roots](#demand-roots--what-makes-a-getter-run) |
|
|
136
|
+
| **Modularity** | `mount=` grafts a module onto the one tree; `state: path` mounts a subtree onto a component; the per-property form maps single keys, and a mounted component's getters are exported at its mount point | [Volumes](#mounting-additional-state-mount) ยท [Whole-object mount](#whole-object-mount-state-path) |
|
|
137
|
+
| **Components** | Two mutually exclusive mechanisms: a JavaScript class with `bind-component`, or HTML-only DCC | [Choosing a mechanism](#choosing-a-component-mechanism) |
|
|
138
|
+
| **Wiring to other elements** | The wc-bindable protocol, spread (`...: obj`), `#init=` / `#sync=` authority, property-to-attribute mirroring | [Binding authority](#binding-authority-init--sync) ยท [Spread](#spread-binding) ยท [Inputs](#inputs-and-attribute-mirror) |
|
|
139
|
+
| **Tokens** | Command tokens call an element's methods from state; event tokens carry the element's events back | [Command token](#command-token-method-binding) ยท [Event token](#event-token-event-binding) |
|
|
140
|
+
| **Time** | `$streams` folds an async source, `$watch` reacts headlessly, `$scan` owns an accumulation that outlives both | [Choosing a time mechanism](#choosing-a-time-mechanism) |
|
|
141
|
+
| **Initialization and lifecycle** | Six ways to supply the state; `$connectedCallback` โฆ `$stateReadyCallback`; `bootstrapState()` / `createState()` | [State initialization](#state-initialization) ยท [Lifecycle hooks](#lifecycle-hooks) ยท [API reference](#api-reference) |
|
|
142
|
+
| **Diagnostics** | Unresolved paths, index arity, wildcard rank and getter cycles are reported; one failing binding stays confined, and neither values nor the DOM are rolled back | [Diagnostics](#diagnostics-and-failure-handling) |
|
|
143
|
+
| **Delivery** | Zero runtime dependencies, no build step, ESM, one CDN `/auto` tag; no `unsafe-eval`, Trusted Types supported | [Installation](#installation) ยท [docs/csp.md](../../docs/csp.md) |
|
|
144
|
+
|
|
145
|
+
### Where the neighbours come in
|
|
146
|
+
|
|
147
|
+
`@wcstack/state` is the reactive core and nothing else. Tooling, I/O and routing live in sibling packages, and the split is always the same shape: this package provides the hook and the contract, the neighbour provides the machinery.
|
|
148
|
+
|
|
149
|
+
| Package | What it adds | What this package already provides |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| [`@wcstack/server`](../server/) | Renders the page on the server and hydrates the markup the client receives | The `enable-ssr` attribute and the hydration contract โ [SSR](#server-side-rendering) |
|
|
152
|
+
| [`@wcstack/lint`](../lint/) | `npx @wcstack/lint <file>` checks every `data-wcs` in an HTML file before it runs | The diagnostic codes and `getWcsManifest()`, both derived from this implementation โ [Diagnostics](#diagnostics-and-failure-handling) |
|
|
153
|
+
| VS Code extension (`wcstack-intellisense`) | The same diagnostics, plus completion, inside the editor | The same manifest and codes |
|
|
154
|
+
| [`@wcstack/typescript`](../typescript/) | `wcs-schema` carries the types into the HTML validator; `wcs-tsc` type-checks inline state scripts | `defineState()`, `WcsPaths<T>` / `WcsPathValue<T, P>` โ [TypeScript support](#typescript-support) |
|
|
155
|
+
| [`@wcstack/devtools`](../devtools/) | A browser panel over state, wiring and update history | The instrumentation the panel reads |
|
|
156
|
+
| [`@wcstack/testing`](../testing/) | `mount()` / `settle()` / `fire()` as one import | The bare recipes that need no extra package โ [Testing your page](#testing-your-page) |
|
|
157
|
+
| [`@wcstack/view-transition`](../view-transition/) | Animates list moves, removals and branch swaps through the View Transition API | The transition-runner hand-off; with no arbiter on the page the mutation applies directly โ [Transition animations](#transition-animations) |
|
|
158
|
+
| [`@wcstack/router`](../router/) ยท [`@wcstack/autoloader`](../autoloader/) | Declarative routing; automatic loading of undefined custom elements | Paths a route can write into, and bindings that wait for a late definition |
|
|
159
|
+
| The [I/O nodes](../../README.md#additional-packages) โ `fetch`, `storage`, `ws`, `midi`, โฆ | The platform APIs as elements | The wc-bindable wiring, spread and the token protocols that connect them โ [Spread](#spread-binding) |
|
|
160
|
+
| [`@wcstack/signals`](../signals/) | A different reactive core, 2.5โ3.5ร faster on create / append for very large keyed lists | Interop โ both speak wc-bindable, so the I/O nodes and DCCs are shared โ [Performance](#performance) |
|
|
127
161
|
|
|
128
162
|
## Installation
|
|
129
163
|
|
|
@@ -248,7 +282,7 @@ There is **one state tree per root**. To split state across modules, mount a vol
|
|
|
248
282
|
<div data-wcs="textContent: cart.total"></div>
|
|
249
283
|
```
|
|
250
284
|
|
|
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). 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.
|
|
285
|
+
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, so connecting a corrected root afterwards does not bring it back. A volume that settles without grafting โ orphaned, failed to load, or failed to graft โ releases its mount slot, and so does a volume detached while it is still loading or waiting for its root. Such a volume takes the slot back when it is re-attached to the same root, or otherwise just before it grafts, and still grafts as before when the slot is free โ even while detached; if another volume took the slot in the meantime, it reports that and does not graft. A synchronous throw from a volume's `$connectedCallback` is reported like an asynchronous one, and the volume counts as grafted. To recover without reloading the page, remove the broken root and the orphaned volumes and add new elements. A grafted volume keeps its slot even when detached, because its data stays in the tree. 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.
|
|
252
286
|
|
|
253
287
|
> **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.
|
|
254
288
|
|
|
@@ -1104,6 +1138,8 @@ export default {
|
|
|
1104
1138
|
};
|
|
1105
1139
|
```
|
|
1106
1140
|
|
|
1141
|
+
Swapping rows this way moves the rendered rows with their values once the swap is complete: the row blocks are reordered rather than rewritten in place, so a row's `$1` and any state it holds outside bindings (such as text typed into an unbound input) follow the value. Writing a value that was not in the list replaces that row in place: its block stays where it is and its bindings show the new value, so an input bound to the row keeps focus while you type. In a list of primitives, equal values cannot be told apart, so writes that end in a reordering of the same values count as a swap.
|
|
1142
|
+
|
|
1107
1143
|
## Recursive Paths (`$recursion`)
|
|
1108
1144
|
|
|
1109
1145
|
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".
|
|
@@ -1253,7 +1289,7 @@ The read-only rule does not depend on spelling `**`. A recursive getter's concre
|
|
|
1253
1289
|
|
|
1254
1290
|
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
1291
|
|
|
1256
|
-
|
|
1292
|
+
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
1293
|
|
|
1258
1294
|
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
1295
|
|
|
@@ -1436,10 +1472,10 @@ Many frameworks use patterns like prop drilling, context providers, or external
|
|
|
1436
1472
|
|
|
1437
1473
|
1. The child references and updates the parent's state through its own state proxy โ no props, no events, no awareness of the parent.
|
|
1438
1474
|
2. When the parent's state changes, the Proxy `set` trap automatically notifies any child bindings that reference the affected path.
|
|
1439
|
-
3. Because the only coupling is the **path name**, both sides
|
|
1440
|
-
4. The cost is path resolution (cached at O(1) after first access)
|
|
1475
|
+
3. Because the only coupling is the **path name**, both sides stay loosely coupled. A Shadow DOM component also runs on its own ([standalone injection](#standalone-web-component-injection-e2esingle-component)); a Light DOM one does not โ the host has to wire it.
|
|
1476
|
+
4. The cost is path resolution (cached at O(1) after first access), change propagation through the dependency graph, and the per-row binding ledger the package builds for every row it renders.
|
|
1441
1477
|
|
|
1442
|
-
This
|
|
1478
|
+
This is cross-component state management built on path resolution rather than on component-level abstractions. It is not the cheapest way to render: [Performance](#performance) puts create and append at 2.5โ3.5ร [`@wcstack/signals`](../signals/), which is what the per-row ledger costs. What it buys is wiring that stays declarative and inspectable.
|
|
1443
1479
|
|
|
1444
1480
|
### Component Definition (Shadow DOM)
|
|
1445
1481
|
|
|
@@ -1608,6 +1644,7 @@ customElements.define("my-component", MyComponent);
|
|
|
1608
1644
|
- `<wcs-state>` with `bind-component` must be a **direct child** of the component element (top-level)
|
|
1609
1645
|
- The parent element must be a **custom element** (tag name containing a hyphen)
|
|
1610
1646
|
- Light DOM components must be wired from the host (the plain, unwired form was removed in v2)
|
|
1647
|
+
- A **mounted** scope does not execute declaration surfaces: `$watch`, `$streams` and `$scan` are ignored there with a one-time warning, and `$recursion` / `**` getters are rejected. Declare them on the root state โ a volume (`<wcs-state mount>`) can host `$watch`, while `$scan` and `$recursion` are root-only. An unwired Shadow DOM child owns an independent tree and can declare all of them
|
|
1611
1648
|
|
|
1612
1649
|
### Loop with Components
|
|
1613
1650
|
|
|
@@ -1782,7 +1819,7 @@ interface CommandToken {
|
|
|
1782
1819
|
|
|
1783
1820
|
- 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
|
|
1784
1821
|
- On `emit`, if the WeakRef has been collected or the element is no longer connected (`isConnected === false`), the subscription is purged automatically (lazy purge)
|
|
1785
|
-
-
|
|
1822
|
+
- 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`
|
|
1786
1823
|
|
|
1787
1824
|
The element's method is invoked with the arguments from `emit`:
|
|
1788
1825
|
|
|
@@ -1959,7 +1996,23 @@ $on: {
|
|
|
1959
1996
|
|
|
1960
1997
|
### Token API
|
|
1961
1998
|
|
|
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.
|
|
1999
|
+
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.
|
|
2000
|
+
|
|
2001
|
+
## Choosing a Time Mechanism
|
|
2002
|
+
|
|
2003
|
+
The next four sections answer four different questions, and the usual mistake is to reach for the wrong one. Choose by **what you are declaring**, not by where the data comes from:
|
|
2004
|
+
|
|
2005
|
+
| Declaration | What you declare | Owns a value | Fires | Typical use |
|
|
2006
|
+
|---|---|---|---|---|
|
|
2007
|
+
| [Path getter](#path-getters-computed-properties) | What a value **is**, in terms of the current state | No โ it is recomputed and cached per address | Lazily, when a demand root reads it | Subtotals, classification, aggregates |
|
|
2008
|
+
| [`$streams`](#streams-streams) | An async producer, and the value folded **within one run** | Yes โ the runtime owns the output | Per chunk; restarts, back to `initial`, when `args` change | Feeds, sockets, continuous observation |
|
|
2009
|
+
| [`$watch`](#watch-watch) | A reaction to a change | No | Once per batch per changed address, after the scan write | Side effects, "when this becomes true" |
|
|
2010
|
+
| [`$scan`](#scan-scan) | An accumulation over time, and what resets it | Yes โ the runtime owns the output | Once per landing (`from`) or once per event (`on`) | Paging accumulation, history, counters |
|
|
2011
|
+
|
|
2012
|
+
Two rules cut most of the confusion:
|
|
2013
|
+
|
|
2014
|
+
- **`$updatedCallback` is not on this list.** It reports the bindings that were applied, so anything hung on it silently depends on what is rendered. See [Demand roots](#demand-roots--what-makes-a-getter-run).
|
|
2015
|
+
- **A `$streams` fold resets on every restart; a `$scan` does not.** When the value has to survive the restart, or has to count events rather than states, it belongs in `$scan`.
|
|
1963
2016
|
|
|
1964
2017
|
## Streams (`$streams`)
|
|
1965
2018
|
|
|
@@ -2074,9 +2127,9 @@ $updatedCallback(paths) {
|
|
|
2074
2127
|
}
|
|
2075
2128
|
```
|
|
2076
2129
|
|
|
2077
|
-
**The rule:** logic that must not depend on what is rendered belongs on a `$watch
|
|
2130
|
+
**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".
|
|
2078
2131
|
|
|
2079
|
-
That example now
|
|
2132
|
+
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`**.
|
|
2080
2133
|
|
|
2081
2134
|
### The limitation that remains
|
|
2082
2135
|
|
|
@@ -2118,10 +2171,10 @@ The handler runs with `this` bound to a **writable** state proxy, so it can writ
|
|
|
2118
2171
|
| Argument | Contract |
|
|
2119
2172
|
|---|---|
|
|
2120
2173
|
| `cur` | The value at drain time (the settled value for the batch) |
|
|
2121
|
-
| `prev` | The value at the **start of the batch** (first-write-wins).
|
|
2174
|
+
| `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 |
|
|
2122
2175
|
| `...indexes` | Only for wildcard paths: this scope's own loop indexes, same convention as `$1`, `$2` |
|
|
2123
2176
|
|
|
2124
|
-
**`prev`
|
|
2177
|
+
**`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`.
|
|
2125
2178
|
|
|
2126
2179
|
**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.
|
|
2127
2180
|
|
|
@@ -2131,16 +2184,17 @@ Firing order is defined in three layers, and only the middle one is yours to ste
|
|
|
2131
2184
|
|
|
2132
2185
|
| Layer | Order | Your control |
|
|
2133
2186
|
|---|---|---|
|
|
2134
|
-
| Mechanisms | `$updatedCallback` โ `$watch` โ `$streams` restart | fixed |
|
|
2187
|
+
| Mechanisms | `$updatedCallback` โ `$scan` โ `$watch` โ `$streams` restart | fixed |
|
|
2135
2188
|
| Between handlers | declaration order in `$watch` | **reorder the declarations** |
|
|
2136
2189
|
| Between rows of one path | ascending `indexes` | fixed |
|
|
2137
2190
|
|
|
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 `$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.
|
|
2191
|
+
**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.
|
|
2139
2192
|
|
|
2140
2193
|
Key rules:
|
|
2141
2194
|
|
|
2142
2195
|
- **Paths of the tree only** โ a path may not contain `@` (the v1 name selector); such a declaration is rejected loudly.
|
|
2143
2196
|
- **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.
|
|
2197
|
+
- **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.
|
|
2144
2198
|
- **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.
|
|
2145
2199
|
- **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.
|
|
2146
2200
|
- **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.
|
|
@@ -2148,6 +2202,76 @@ Key rules:
|
|
|
2148
2202
|
- **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.
|
|
2149
2203
|
- **SSR does not run watches** โ handler side effects would otherwise execute on both server and client.
|
|
2150
2204
|
|
|
2205
|
+
## Scan (`$scan`)
|
|
2206
|
+
|
|
2207
|
+
`$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.
|
|
2208
|
+
|
|
2209
|
+
```html
|
|
2210
|
+
<wcs-state>
|
|
2211
|
+
<script type="module">
|
|
2212
|
+
export default {
|
|
2213
|
+
page: 1,
|
|
2214
|
+
host: "a",
|
|
2215
|
+
$eventTokens: ["message"],
|
|
2216
|
+
$streams: {
|
|
2217
|
+
pageResult: { args: (s) => s.page, source: loadPage },
|
|
2218
|
+
},
|
|
2219
|
+
$scan: {
|
|
2220
|
+
// from: fold each landing of a state path โ here, the stream's value
|
|
2221
|
+
feed: {
|
|
2222
|
+
from: "pageResult",
|
|
2223
|
+
initial: { items: [], pages: [] },
|
|
2224
|
+
fold: (feed, chunk) =>
|
|
2225
|
+
chunk?.kind === "success" && !feed.pages.includes(chunk.page)
|
|
2226
|
+
? { items: feed.items.concat(chunk.items), pages: [...feed.pages, chunk.page] }
|
|
2227
|
+
: feed,
|
|
2228
|
+
},
|
|
2229
|
+
// on: fold each event of a declared event token
|
|
2230
|
+
log: {
|
|
2231
|
+
on: "message",
|
|
2232
|
+
initial: [],
|
|
2233
|
+
fold: (log, event) => [...log.slice(-49), event.detail],
|
|
2234
|
+
resetOn: ["host"], // back to [] whenever host changes
|
|
2235
|
+
},
|
|
2236
|
+
},
|
|
2237
|
+
};
|
|
2238
|
+
</script>
|
|
2239
|
+
</wcs-state>
|
|
2240
|
+
|
|
2241
|
+
<template data-wcs="for: feed.items">โฆ</template>
|
|
2242
|
+
```
|
|
2243
|
+
|
|
2244
|
+
| Field | Contract |
|
|
2245
|
+
|---|---|
|
|
2246
|
+
| `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`. |
|
|
2247
|
+
| `on` | An event-token name declared in `$eventTokens`. |
|
|
2248
|
+
| `initial` | Required. The seed of the accumulator, and what `resetOn` returns to. |
|
|
2249
|
+
| `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. |
|
|
2250
|
+
| `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. |
|
|
2251
|
+
|
|
2252
|
+
**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.
|
|
2253
|
+
|
|
2254
|
+
How the two sources fire:
|
|
2255
|
+
|
|
2256
|
+
| | `from` (a path) | `on` (an event token) |
|
|
2257
|
+
|---|---|---|
|
|
2258
|
+
| 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. |
|
|
2259
|
+
| When | At the end of the drain, before `$watch`. | Inside the event, before that token's `$on` handlers. |
|
|
2260
|
+
| 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. |
|
|
2261
|
+
|
|
2262
|
+
Key rules:
|
|
2263
|
+
|
|
2264
|
+
- **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`).
|
|
2265
|
+
- **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.
|
|
2266
|
+
- **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.
|
|
2267
|
+
- **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`.
|
|
2268
|
+
- **Keep folds bounded.** An infinite source must fold into a bounded value (the last N, a count), exactly as with `$streams`.
|
|
2269
|
+
- **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.
|
|
2270
|
+
- **`$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`.
|
|
2271
|
+
- **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.
|
|
2272
|
+
|
|
2273
|
+
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).
|
|
2274
|
+
|
|
2151
2275
|
## Inputs and Attribute Mirror
|
|
2152
2276
|
|
|
2153
2277
|
`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.
|
|
@@ -2433,7 +2557,7 @@ li {
|
|
|
2433
2557
|
Two consequences to know while that tag accepts the `state` participant:
|
|
2434
2558
|
|
|
2435
2559
|
- 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.
|
|
2436
|
-
- Because `$watch` and the `$streams` restart stay on the original microtask, they now run **before** `$updatedCallback` instead of after it.
|
|
2560
|
+
- Because `$scan`, `$watch` and the `$streams` restart stay on the original microtask, they now run **before** `$updatedCallback` instead of after it.
|
|
2437
2561
|
|
|
2438
2562
|
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.
|
|
2439
2563
|
|
|
@@ -2441,7 +2565,7 @@ Only a batch that actually has bindings to apply is handed to the tag, so a writ
|
|
|
2441
2565
|
|
|
2442
2566
|
### Wiring to a path that does not exist is reported
|
|
2443
2567
|
|
|
2444
|
-
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:
|
|
2568
|
+
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:
|
|
2445
2569
|
|
|
2446
2570
|
```
|
|
2447
2571
|
[@wcstack/state] [wcs/binding-path-missing] Bound path "user.nmae" does not resolve on the state tree:
|
|
@@ -2454,6 +2578,7 @@ dropped. Validate statically: npx @wcstack/lint <file>.
|
|
|
2454
2578
|
| Typo in a nested path (`user.nmae`) | `console.warn` (`wcs/binding-path-missing`). Updates still never arrive โ you fix it |
|
|
2455
2579
|
| Typo in a top-level path (`cout`) | Throws on read, with the same wording and did-you-mean |
|
|
2456
2580
|
| Typo in a `$watch` key | `console.warn` (`wcs/watch-path-missing`), reported even for a single segment |
|
|
2581
|
+
| 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) |
|
|
2457
2582
|
|
|
2458
2583
|
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:
|
|
2459
2584
|
|
|
@@ -2680,7 +2805,7 @@ it("renders, re-renders, and runs handlers", async () => {
|
|
|
2680
2805
|
|
|
2681
2806
|
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()`.
|
|
2682
2807
|
|
|
2683
|
-
- `getBindingsReady(root)` resolves once every binding under `root` (a `document` or a shadow root) is built, and rejects if binding initialization fails (v1.26+).
|
|
2808
|
+
- `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".
|
|
2684
2809
|
- Updates settle on the microtask queue; a single `setTimeout(0)` after a write is enough.
|
|
2685
2810
|
- `state.items = [...state.items, "cherry"]` is the reactive form โ `state.items.push()` is not observed (same rule as in handlers).
|
|
2686
2811
|
- 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.
|
|
@@ -2767,7 +2892,7 @@ bootstrapState();
|
|
|
2767
2892
|
|
|
2768
2893
|
| Export | Description |
|
|
2769
2894
|
|---|---|
|
|
2770
|
-
| `getBindingsReady(root)` | Resolves once every binding under `root` (a `document` or a shadow root) is built; rejects if binding initialization fails |
|
|
2895
|
+
| `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 |
|
|
2771
2896
|
| `buildBindings(root)` | Build the bindings under a `document` or `ShadowRoot` explicitly โ what the first `<wcs-state>` registered on a root schedules for it |
|
|
2772
2897
|
| `getConfig()` | The current configuration (read-only view) |
|
|
2773
2898
|
| `defineState(obj)` | Identity function that types `this` inside methods and getters โ see [TypeScript Support](#typescript-support) |
|
|
@@ -2793,14 +2918,14 @@ Subpath entries for tooling: `@wcstack/state/parser` (the `data-wcs` parser as a
|
|
|
2793
2918
|
|
|
2794
2919
|
| Property / Method | Description |
|
|
2795
2920
|
|---|---|
|
|
2796
|
-
| `initializePromise` | Resolves when state is fully initialized |
|
|
2797
|
-
| `connectedCallbackPromise` | Resolves once `connectedCallback` has completed (state loaded, `$connectedCallback` run) โ what the testing recipes await |
|
|
2921
|
+
| `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` |
|
|
2922
|
+
| `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 |
|
|
2798
2923
|
| `listPaths` | Set of paths used in `for` loops |
|
|
2799
2924
|
| `getterPaths` | Set of paths defined as getters |
|
|
2800
2925
|
| `setterPaths` | Set of paths defined as setters |
|
|
2801
2926
|
| `createState(mutability, callback)` | Create a state proxy (`"readonly"` or `"writable"`) |
|
|
2802
2927
|
| `createStateAsync(mutability, callback)` | Async version of `createState` |
|
|
2803
|
-
| `setInitialState(state)` | Set state programmatically
|
|
2928
|
+
| `setInitialState(state)` | Set state programmatically. Before initialization it supplies the initial state. On an initialized element it replaces the whole state and re-applies every established binding to the new state before it returns (a detached element re-applies them when it reconnects); a binding whose path the new state no longer has reports a failed apply instead of keeping the old text. A re-set is not a write: it fires no `$watch` handler and no `$updatedCallback`. Lists are matched by array identity, so pass a new array when a list's length changed โ re-setting with the same array instance after pushing to or splicing it in place is not supported. Throws on a loaded volume (`<wcs-state mount="โฆ">` โ its data was copied into the root tree, so write the paths under the mount path on the root instead), on a tree with grafted volumes or mounted components, and on an element that already failed to initialize โ such an element cannot be re-armed; remove it and create a new one |
|
|
2804
2929
|
| `nextVersion()` | Increment and return version number |
|
|
2805
2930
|
|
|
2806
2931
|
## Architecture
|