@wcstack/signals 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +437 -0
- package/README.md +439 -0
- package/dist/core-DCHZTbGv.esm.min.js +2 -0
- package/dist/core-DCHZTbGv.esm.min.js.map +1 -0
- package/dist/core-DRLhLwcb.esm.js +0 -0
- package/dist/core-DRLhLwcb.esm.js.map +1 -0
- package/dist/dom.d.ts +311 -0
- package/dist/dom.esm.js +743 -0
- package/dist/dom.esm.js.map +1 -0
- package/dist/dom.esm.min.js +2 -0
- package/dist/dom.esm.min.js.map +1 -0
- package/dist/index.d.ts +248 -0
- package/dist/index.esm.js +2 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.esm.min.js +2 -0
- package/dist/index.esm.min.js.map +1 -0
- package/package.json +74 -0
package/README.md
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
# @wcstack/signals
|
|
2
|
+
|
|
3
|
+
`@wcstack/signals` is a **signals-based, fine-grained reactive core** — zero runtime dependencies, buildless, standards-first. See [`docs/signals-state-design.md`](../../docs/signals-state-design.md) for the design rationale.
|
|
4
|
+
|
|
5
|
+
Where [`@wcstack/state`](../state/README.md) connects UI and state through HTML path strings (no reactive primitives in your code), `@wcstack/signals` takes the opposite stance for the cases that want it: it **exposes the reactive primitives directly**. There is no DSL and no `data-wcs`; you call `signal()`, `computed()`, `effect()` in JavaScript. The two packages are complementary, not competing — same ecosystem, different coupling point.
|
|
6
|
+
|
|
7
|
+
The public API is shaped after the [TC39 Signals proposal](https://github.com/tc39/proposal-signals) (State / Computed / effect). The implementation is in-house and tiny, so it can later be swapped for a native or polyfilled signal without changing call sites.
|
|
8
|
+
|
|
9
|
+
## What's in the box
|
|
10
|
+
|
|
11
|
+
| Module | Entry | What it gives you |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| **Reactive core** | `@wcstack/signals` | `signal` / `computed` / `effect` / `createRoot` / `onCleanup` / `flushSync` |
|
|
14
|
+
| **Async resource** | `@wcstack/signals` | `resource` — an async producer as a reactive `{ value, loading, error }` triad (switchMap cancel/restart) |
|
|
15
|
+
| **Stream resource** | `@wcstack/signals` | `streamResource` — fold a continuous flow (async iterable / `ReadableStream`) into one reactive value |
|
|
16
|
+
| **wc-bindable adapter** | `@wcstack/signals` | `bindNode` (properties→signals, event-token streams via `on`, input write-back via `bindInput`, command-token via `bindCommand`) + `nodeSource` (node cancel bridge for `resource`) |
|
|
17
|
+
| **DOM layer** | `@wcstack/signals/dom` | `h` / `render` / `Fragment` / `For` / `Index` / `SignalsElement` (re-exports the core too) |
|
|
18
|
+
|
|
19
|
+
## Design in one breath
|
|
20
|
+
|
|
21
|
+
- **Pull-validated three-color marking** (after Reactively / Solid). A write marks direct observers DIRTY and transitive ones CHECK; effects run on a coalesced microtask. A computed that recomputes to an *equal* value does **not** propagate — downstream work is skipped.
|
|
22
|
+
- **Fine-grained `h`, no VDOM.** `h(tag, props, ...children)` builds **real DOM once**. A prop or child passed as a function/signal is wired to a targeted `effect`, so only that one binding updates. No **VDOM reconciler** is shipped; keyed lists are handled by `For` / `Index` (array diffing that reuses and moves rows), not a VDOM.
|
|
23
|
+
- **Ownership = lifecycle.** `createRoot` / effects collect the disposers of everything created during their run, so tearing down a subtree disposes its effects, listeners, and resources — no leaks.
|
|
24
|
+
- **IO is the node, reactivity is the core.** `bindNode` adapts a wc-bindable element (e.g. `<wcs-fetch>`) into signals. The element has no idea a signal is behind the binding.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install @wcstack/signals
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or buildless via an import map (both entries share one reactive core chunk — see [Buildless](#buildless-import-maps)):
|
|
33
|
+
|
|
34
|
+
```html
|
|
35
|
+
<script type="importmap">
|
|
36
|
+
{ "imports": {
|
|
37
|
+
"@wcstack/signals": "https://esm.run/@wcstack/signals",
|
|
38
|
+
"@wcstack/signals/dom": "https://esm.run/@wcstack/signals/dom"
|
|
39
|
+
} }
|
|
40
|
+
</script>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
### 1. Signals, computed, effect
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { signal, computed, effect } from "@wcstack/signals";
|
|
49
|
+
|
|
50
|
+
const count = signal(0);
|
|
51
|
+
const doubled = computed(() => count.get() * 2);
|
|
52
|
+
|
|
53
|
+
effect(() => {
|
|
54
|
+
console.log(`count=${count.peek()}, doubled=${doubled.get()}`);
|
|
55
|
+
});
|
|
56
|
+
// → logs "count=0, doubled=0"
|
|
57
|
+
|
|
58
|
+
count.set(1); // effect re-runs on the next microtask → "count=1, doubled=2"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- `get()` reads **and** tracks (registers the current effect/computed as a dependent).
|
|
62
|
+
- `peek()` reads **without** tracking (no dependency edge).
|
|
63
|
+
- `effect` re-runs are coalesced onto a microtask. Need it applied synchronously (e.g. to read the DOM back in a test)? Call `flushSync()`.
|
|
64
|
+
|
|
65
|
+
### 2. A reactive custom element
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
import { signal, computed, h, SignalsElement } from "@wcstack/signals/dom";
|
|
69
|
+
|
|
70
|
+
class SignalCounter extends SignalsElement {
|
|
71
|
+
count = signal(0);
|
|
72
|
+
doubled = computed(() => this.count.get() * 2);
|
|
73
|
+
|
|
74
|
+
render() {
|
|
75
|
+
return h("div", { class: "counter" },
|
|
76
|
+
h("button", { onClick: () => this.count.set(this.count.peek() - 1) }, "−"),
|
|
77
|
+
h("output", null, () => String(this.count.get())),
|
|
78
|
+
h("button", { onClick: () => this.count.set(this.count.peek() + 1) }, "+"),
|
|
79
|
+
// `doubled` is a computed: this only re-renders when the doubled VALUE changes.
|
|
80
|
+
h("span", { class: "muted" }, () => `×2 = ${this.doubled.get()}`),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
customElements.define("signal-counter", SignalCounter);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`connectedCallback` mounts `render()` under an ownership root; `disconnectedCallback` disposes every effect created there and clears the mount point. Subclasses only implement `render()`. Override `getMountPoint()` to return a shadow root.
|
|
88
|
+
|
|
89
|
+
### 3. Driving a real IO node — signals ↔ `<wcs-fetch>`
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { signal, computed, effect, createRoot, bindNode, NodeShape, h, render, For } from "@wcstack/signals/dom";
|
|
93
|
+
|
|
94
|
+
await customElements.whenDefined("wcs-fetch");
|
|
95
|
+
const fetchEl = document.getElementById("search-fetch");
|
|
96
|
+
|
|
97
|
+
// Optional: type the node's reactive surface so `signals` / `set` / `command` are typed.
|
|
98
|
+
interface FetchShape extends NodeShape {
|
|
99
|
+
signals: { value: Person[]; loading: boolean };
|
|
100
|
+
inputs: { url: string };
|
|
101
|
+
}
|
|
102
|
+
const bound = bindNode<FetchShape>(fetchEl); // descriptor read from fetchEl.constructor.wcBindable
|
|
103
|
+
|
|
104
|
+
const query = signal("");
|
|
105
|
+
const people = computed(() => bound.signals.value.get() ?? []); // value: ReadSignal<Person[]>, typed
|
|
106
|
+
|
|
107
|
+
createRoot(() => {
|
|
108
|
+
// query → url: <wcs-fetch> auto-fetches on url change and re-dispatches events,
|
|
109
|
+
// which the adapter folds back into bound.signals.* . Typing fast aborts the
|
|
110
|
+
// in-flight request (FetchCore cancels the superseded one).
|
|
111
|
+
effect(() => {
|
|
112
|
+
const q = query.get().trim();
|
|
113
|
+
bound.set("url", q ? `/api/people?q=${encodeURIComponent(q)}` : "/api/people");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
render(
|
|
117
|
+
h("div", null,
|
|
118
|
+
h("input", { type: "search", onInput: (e) => query.set(e.target.value) }),
|
|
119
|
+
h("p", null, () => bound.signals.loading.get() ? "Loading…" : `${people.get().length} result(s)`),
|
|
120
|
+
// Keyed list: one stable <li> per id, reconciled in place (see §6).
|
|
121
|
+
h("ul", null, For(() => people.get(), (p) => h("li", null, p.name), { key: (p) => p.id })),
|
|
122
|
+
),
|
|
123
|
+
document.getElementById("search-app"),
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
A full, runnable version of this lives in [`examples/signals-live-search`](../../examples/signals-live-search/README.md).
|
|
129
|
+
|
|
130
|
+
### 6. Keyed lists — `For` / `Index`
|
|
131
|
+
|
|
132
|
+
A bare reactive child (`() => items.map(render)`) rebuilds its whole subtree on every change. For lists, use `For` (keyed by value/identity) or `Index` (keyed by position) — each keeps a stable DOM row per item and reconciles in place, so reordering moves rows instead of rebuilding them (preserving per-row DOM, focus, and input state). Each row runs under its own ownership scope, so a removed row disposes exactly its effects.
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import { signal, h, For, Index } from "@wcstack/signals/dom";
|
|
136
|
+
|
|
137
|
+
const todos = signal([{ id: 1, text: "a" }, { id: 2, text: "b" }]);
|
|
138
|
+
|
|
139
|
+
// For — keyed by `id`. Rows are reused/moved on reorder; `each` receives the item
|
|
140
|
+
// and an index ACCESSOR (the index changes when the row moves).
|
|
141
|
+
h("ul", null,
|
|
142
|
+
For(todos, (t, index) => h("li", null, () => `${index()}: ${t.text}`), { key: (t) => t.id }),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Index — keyed by position (good for primitive arrays). `each` receives the item
|
|
146
|
+
// as an ACCESSOR (the value at a slot changes; the slot does not) and a fixed index.
|
|
147
|
+
const nums = signal([10, 20, 30]);
|
|
148
|
+
h("ul", null,
|
|
149
|
+
Index(nums, (n) => h("li", null, () => String(n() * 2))),
|
|
150
|
+
);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- `For` keys default to value identity (`===`); pass `{ key }` for objects. Keys must be unique (a duplicate throws). Use `Index` for primitive lists where position is the stable identity.
|
|
154
|
+
- `each` must return a **single** Node (one row = one node).
|
|
155
|
+
|
|
156
|
+
### 4. Async resource (switchMap)
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
import { signal, resource } from "@wcstack/signals";
|
|
160
|
+
|
|
161
|
+
const id = signal(1);
|
|
162
|
+
const user = resource(
|
|
163
|
+
async (userId, signal) => (await fetch(`/api/users/${userId}`, { signal })).json(),
|
|
164
|
+
{ args: () => id.get() }, // reading id here wires restart-on-change
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// user.value / user.loading / user.error are read-only signals.
|
|
168
|
+
id.set(2); // aborts the in-flight request and starts a fresh one.
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### 5. Stream resource (fold a flow)
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
import { streamResource } from "@wcstack/signals";
|
|
175
|
+
|
|
176
|
+
// latest (default): value becomes the last chunk.
|
|
177
|
+
const latest = streamResource((args, signal) => openEventStream(signal));
|
|
178
|
+
|
|
179
|
+
// reduce: accumulate. `initial` is required and is what value resets to on restart.
|
|
180
|
+
const log = streamResource((args, signal) => openLogStream(signal), {
|
|
181
|
+
fold: (acc, chunk) => [...(acc ?? []), chunk],
|
|
182
|
+
initial: [],
|
|
183
|
+
});
|
|
184
|
+
// log.value / log.status ("idle"|"active"|"done"|"error") / log.error
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## API reference
|
|
188
|
+
|
|
189
|
+
### Reactive core (`@wcstack/signals`)
|
|
190
|
+
|
|
191
|
+
| Export | Signature | Notes |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| `signal<T>` | `(initial: T, equals?) => WriteSignal<T>` | `get` (tracks) / `peek` (no track) / `set`. Default equality is `Object.is`. |
|
|
194
|
+
| `computed<T>` | `(fn: () => T, equals?) => ReadSignal<T>` | Lazy, memoized, equality short-circuit. Re-tracks deps on every run (conditional deps pruned). Reading itself throws a clear "circular dependency" error. |
|
|
195
|
+
| `effect` | `(fn: () => Cleanup \| void) => EffectHandle` | Runs once immediately, then on coalesced microtasks. Return a cleanup; it runs before each re-run and on dispose. `handle.dispose()` stops it. |
|
|
196
|
+
| `createRoot<T>` | `(fn: (dispose) => T) => T` | Fresh ownership scope. Everything created inside is disposed by `dispose`. The root is detached (not auto-disposed by an enclosing owner). If `fn` throws, the half-built scope is disposed before the error propagates. |
|
|
197
|
+
| `onCleanup` | `(fn: () => void) => void` | Register a teardown with the current owner. No-op with no owner. |
|
|
198
|
+
| `flushSync` | `() => void` | Run queued effects synchronously now. |
|
|
199
|
+
|
|
200
|
+
### DOM layer (`@wcstack/signals/dom`)
|
|
201
|
+
|
|
202
|
+
| Export | Signature | Notes |
|
|
203
|
+
|---|---|---|
|
|
204
|
+
| `h` | `(tag, props?, ...children) => Node` | `tag` is a tag string, a `Component`, or `Fragment`. Function/signal props & children are reactive. `onXxx` props are event listeners. |
|
|
205
|
+
| `render` | `(child, container) => Node` | Append a child (resolving fragments/arrays/reactives) into a container. |
|
|
206
|
+
| `Fragment` | `symbol` | `h(Fragment, null, ...children)` groups without a wrapper element. |
|
|
207
|
+
| `For<T>` | `(list, each, options?) => ListView` | Keyed list. `list` is a signal or `() => T[]`; `each(item, index: () => number)` returns one Node; `options.key(item, i)` defaults to value identity. Reuses/moves rows by key; disposes removed rows. |
|
|
208
|
+
| `Index<T>` | `(list, each) => ListView` | Position-keyed list. `each(item: () => T, index: number)` returns one Node. Rows are reused per slot (the value at a slot updates via a signal); grows/shrinks at the tail. |
|
|
209
|
+
| `SignalsElement` | `abstract` base (lazy `extends HTMLElement`) | Lifecycle base; implement `render()`, optionally `getMountPoint()`. `HTMLElement` is resolved lazily so the `./dom` module evaluates in non-DOM environments — see [SSR / non-DOM](#ssr--non-dom-the-dom-entry-loads-without-a-dom). |
|
|
210
|
+
| `createSignalsElement` | `() => SignalsElementClass` | Build (memoized) the `SignalsElement` base at call time. Resolves `HTMLElement` when called; throws a clear error if no DOM. The SSR-safe way to obtain the base. |
|
|
211
|
+
|
|
212
|
+
`setProp` rules: `style` accepts a string or an object (camelCase / kebab-case / `--custom` keys); `class` / `className` are set as the `class` **attribute** (works for SVG too; `null`/`false` clear it); a key that resolves to a writable DOM property is assigned as a property (with `null`/`undefined` normalized to `""` so a string prop like `id`/`src` clears instead of landing the literal `"null"`); otherwise it's an attribute (`true` → empty attr, `null`/`false` → removed).
|
|
213
|
+
|
|
214
|
+
### Resources (`@wcstack/signals`)
|
|
215
|
+
|
|
216
|
+
| Export | Shape |
|
|
217
|
+
|---|---|
|
|
218
|
+
| `resource<T, A>(source, options?)` | `{ value, loading, error, dispose }` — `source(args, signal) => Promise<T> \| T` |
|
|
219
|
+
| `streamResource<T, C, A>(source, options?)` | `{ value, status, error, dispose }` — `source(args, signal) => AsyncIterable<C> \| ReadableStream<C> \| Promise<…>` |
|
|
220
|
+
|
|
221
|
+
Both: `options.args` is a reactive getter (reading signals there wires restart-on-change, switchMap-style — the previous request is aborted via the `AbortSignal` passed to `source`); `options.initial` seeds `value`. A stale response from a superseded request is dropped (checked against `signal.aborted`). Created inside an owner, a resource auto-disposes on teardown.
|
|
222
|
+
|
|
223
|
+
### wc-bindable adapter (`@wcstack/signals`)
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
const bound = bindNode(target, descriptor?);
|
|
227
|
+
|
|
228
|
+
// element → signal
|
|
229
|
+
bound.signals.<propName>.get(); // output properties as read-only signals (latest value)
|
|
230
|
+
bound.on("propName", { fold?, initial? }); // event-token STREAM: folds every emit (latest by default)
|
|
231
|
+
|
|
232
|
+
// signal → element
|
|
233
|
+
bound.set("inputName", value); // write a declared input (imperative)
|
|
234
|
+
bound.bindInput("inputName", someSignal); // reactive write-back: mirror a signal into the input
|
|
235
|
+
bound.command("cmdName", ...args); // invoke a declared command (imperative)
|
|
236
|
+
bound.bindCommand("cmdName", trigger, mapArgs?); // command-token: invoke when `trigger` CHANGES
|
|
237
|
+
|
|
238
|
+
bound.dispose(); // detach all listeners/effects
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
If `descriptor` is omitted, it's read from `target.constructor.wcBindable`. The adapter covers the four wc-bindable mappings: `signals[name]` is the **state view** of a property (equality-guarded — same value, no update); `on(name)` is the **occurrence view** of the same event (a stream — updates on *every* emit, even an equal value), folded latest-by-default. `bindInput` mirrors a signal into an input with a same-value guard (`node[name] !== v`) so a write that re-dispatches an event cannot loop. `bindCommand` invokes a command when the trigger **changes** (the initial value does not fire), with `mapArgs` shaping the call. `set`/`bindInput` reject undeclared inputs; `command`/`bindCommand` reject undeclared (or non-function) commands. The `error` signal's initial value is `null`. After `dispose` the adapter is **inert** (signals/streams stop, methods throw); `dispose` is idempotent. `bindInput`/`bindCommand` also return a per-binding disposer.
|
|
242
|
+
|
|
243
|
+
#### Typed surface — `bindNode<NodeShape>`
|
|
244
|
+
|
|
245
|
+
`target` is a plain `EventTarget` (the adapter casts the indexing surface internally — it is **not** in the public signature, so it no longer erases your element's type). Pass an optional `NodeShape` type argument to type the whole result; omit it for the back-compat all-`unknown` shape.
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
import { bindNode, NodeShape } from "@wcstack/signals";
|
|
249
|
+
|
|
250
|
+
interface FetchShape extends NodeShape {
|
|
251
|
+
signals: { value: Person[]; loading: boolean }; // propName → snapshot value type
|
|
252
|
+
inputs: { url: string }; // inputName → settable value type
|
|
253
|
+
commands: { fetch: (url: string) => Promise<Person[]>; abort: () => void }; // commandName → signature
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const bound = bindNode<FetchShape>(fetchEl, FetchCore.wcBindable);
|
|
257
|
+
|
|
258
|
+
bound.signals.value.get(); // ReadSignal<Person[]> — key existence & value type checked
|
|
259
|
+
bound.signals.loading.get(); // ReadSignal<boolean>
|
|
260
|
+
bound.on("value"); // ReadSignal<Person[] | undefined>
|
|
261
|
+
bound.set("url", "/api/people"); // string enforced; bound.set("url", 123) is a type error
|
|
262
|
+
bound.command("fetch", "/api"); // args/return inferred; unknown command name is a type error
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The types are **erased at runtime** — the adapter still validates names against the descriptor and behaves identically; the type argument only sharpens the call sites. Without it, `bound.signals` is keyed by arbitrary `string → ReadSignal<unknown>` and `set`/`command` accept any name/value, exactly as before.
|
|
266
|
+
|
|
267
|
+
#### `nodeSource` — cancel bridge for `resource`
|
|
268
|
+
|
|
269
|
+
`nodeSource(bound, run, { abort? })` builds a `resource` source from a wc-bindable node: it wires the resource's `AbortSignal` to the node's cancel command (default `"abort"`), then delegates to `run`. Wrap it in `resource({ args })` and any node declaring an abort command gets switchMap cancel/restart — an `args` change aborts the in-flight call (cancelling the node's real `AbortController`) and starts the next. The node's own value/loading/error stay on `bound.signals`.
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
import { signal, resource } from "@wcstack/signals";
|
|
273
|
+
|
|
274
|
+
const bound = bindNode(fetchEl);
|
|
275
|
+
const id = signal(1);
|
|
276
|
+
const r = resource(
|
|
277
|
+
nodeSource(bound, (b, userId) => b.command("fetch", `/api/${userId}`)),
|
|
278
|
+
{ args: () => id.get() },
|
|
279
|
+
);
|
|
280
|
+
id.set(2); // aborts the in-flight fetch via the node's abort command, then refetches
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Using JSX (opt-in)
|
|
284
|
+
|
|
285
|
+
`h` is the classic JSX factory shape, so JSX is **available but not shipped** — opting in is your choice and means leaving the buildless path (JSX must be transpiled). The package ships only the substrate (`h` + `Fragment`); there is no `.tsx` and no `jsx-runtime` types.
|
|
286
|
+
|
|
287
|
+
Minimal setup — point the **classic** runtime at `h`/`Fragment` in your own `tsconfig.json`:
|
|
288
|
+
|
|
289
|
+
```jsonc
|
|
290
|
+
{
|
|
291
|
+
"compilerOptions": {
|
|
292
|
+
"jsx": "react",
|
|
293
|
+
"jsxFactory": "h",
|
|
294
|
+
"jsxFragmentFactory": "Fragment"
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Then import both into every `.tsx` file (the factory must be in scope) and write JSX:
|
|
300
|
+
|
|
301
|
+
```tsx
|
|
302
|
+
import { h, Fragment, signal, render } from "@wcstack/signals/dom";
|
|
303
|
+
|
|
304
|
+
const count = signal(0);
|
|
305
|
+
const view = (
|
|
306
|
+
<button onClick={() => count.set(count.peek() + 1)}>
|
|
307
|
+
count: {() => count.get()}
|
|
308
|
+
</button>
|
|
309
|
+
);
|
|
310
|
+
render(view, document.body);
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Note what carries over and what does **not**:
|
|
314
|
+
|
|
315
|
+
- **Reactive bits are still thunks/signals.** `{() => count.get()}` (or passing a signal) is what wires the targeted effect — a bare `{count.get()}` reads once and never updates. JSX changes the syntax, not the reactivity model.
|
|
316
|
+
- **The classic runtime only.** Automatic runtime (`"jsx": "react-jsx"` + `jsxImportSource`) is **not** supported — there is no `./jsx-runtime` export (it's a planned-but-unshipped seam). Use `"jsx": "react"` with the two factory options above.
|
|
317
|
+
- **Most JSX semantics are not in the contract.** `ref`, `context`, and controlled inputs are **not** implemented, and there is no JSX `key` attribute — `{items.map(...)}` rebuilds wholesale just as it does without JSX. For keyed lists, call [`For`/`Index`](#6-keyed-lists--for--index) directly (they work the same inside JSX).
|
|
318
|
+
|
|
319
|
+
A step-by-step guide (esbuild / tsc / Vite setups, a verifying `.tsx` example, troubleshooting) is in [`docs/signals-jsx-setup.md`](../../docs/signals-jsx-setup.md).
|
|
320
|
+
|
|
321
|
+
## Buildless (import maps)
|
|
322
|
+
|
|
323
|
+
Both entries ship as production code-split bundles that import **one shared reactive-core chunk**, so on a buildless page you can import the headless core from `@wcstack/signals` and the DOM layer from `@wcstack/signals/dom` and still get a **single** reactive instance (one tracking context). Map both specifiers; the shared `core-*.esm.js` chunk is resolved by relative path, so serve the whole `dist/` (or use a CDN that does). Bundler users dedupe via the module graph and may use either entry.
|
|
324
|
+
|
|
325
|
+
## Stability
|
|
326
|
+
|
|
327
|
+
The package version is **1.x**, but the public surface is split into a **stable** core and an **evolving / experimental** edge. The breaking changes are concentrated on the experimental side; the stable surface follows semver.
|
|
328
|
+
|
|
329
|
+
| API group | Exports | Stability |
|
|
330
|
+
|---|---|---|
|
|
331
|
+
| **Reactive core** | `signal` / `computed` / `effect` / `createRoot` / `onCleanup` / `flushSync` | **Stable** — semver-protected; shaped after the TC39 Signals proposal. |
|
|
332
|
+
| **Error contract** | `DisposedError` / `isDisposedError` | **Stable** — brand-based, realm-safe. |
|
|
333
|
+
| **Resources** | `resource` / `streamResource` | **Stable** (mostly). The shapes are settled; note the `streamResource` cooperative-cancellation contract (the `source` MUST honor its `AbortSignal` — see [Notes & limitations](#notes--limitations)). |
|
|
334
|
+
| **wc-bindable adapter** | `bindNode` / `nodeSource` | **Evolving / experimental** — runtime behaviour is stable, but the **typed surface** (`bindNode<NodeShape>`, `NodeShape`) may still be tuned. |
|
|
335
|
+
| **DOM layer** | `h` / `render` / `Fragment` / `For` / `Index` / `setProp` / `SignalsElement` / `createSignalsElement` / `ListView` | **Evolving / experimental** — usable today; signatures and the element factory shape may adjust before they are promoted to stable. |
|
|
336
|
+
| **Development mode** | `globalThis.__WCS_DEV__` + the warning codes | **Experimental** — diagnostics only; the set of warnings and their wording may change at any time. It never runs in production. |
|
|
337
|
+
|
|
338
|
+
**Deprecation policy.** Within the **1.x** line, the **stable** APIs keep backward compatibility: any incompatible change is announced as a deprecation at least one minor release before it lands. APIs marked **evolving / experimental** may change in a minor release (signatures, type shapes, or warning wording) — pin an exact version if you depend on their current shape.
|
|
339
|
+
|
|
340
|
+
## Notes & limitations
|
|
341
|
+
|
|
342
|
+
- **JSX is shaped but not shipped.** `h` is the classic JSX factory; the buildless path is calling `h` directly. Wiring JSX is opt-in (and leaves buildless) — see [Using JSX](#using-jsx-opt-in) and [`docs/signals-jsx-setup.md`](../../docs/signals-jsx-setup.md).
|
|
343
|
+
- **Use `For`/`Index` for lists, not a bare reactive child.** A function/signal child is an *insertion point*: on each run every node it produced before is removed and the freshly-resolved nodes inserted. That is fine for conditional/small dynamic regions, but for a list (`() => items.map(render)`) the whole subtree regenerates on any change — DOM-regen cost grows with size and per-row state (focus, an inline `<input>`/`<select>` being edited, scroll, selection) is lost. Use [`For`/`Index`](#6-keyed-lists--for--index) for keyed reuse. Each row must be a single Node.
|
|
344
|
+
- **No backpressure (stream).** The fold result *is* the buffer — demand does not flow back to the producer. Bound the fold (latest / count / window) for infinite streams; unbounded accumulation is a footgun.
|
|
345
|
+
- **Cooperative cancellation — the `source` MUST honor its `AbortSignal` (hard contract).** This is what drives switchMap restart/dispose. A `ReadableStream` is **fully** rescued on abort via `reader.cancel()` (the parked `read()` is force-unwound). A plain `AsyncIterable` / async generator is **partially** rescued: on abort the adapter calls the iterator's `return()` to fire your `finally`/cleanup, but a parked `await` (the producer stalling before its next `yield` while *ignoring* `signal`) cannot be force-unwound from outside — `return()` only takes effect when the generator next resumes. A generator that parks forever and never observes `signal` still leaks its `consume()` task. **Always observe `signal` in your `source`** (e.g. reject/break on `signal.aborted`).
|
|
346
|
+
- **`setProp` covers the common attribute↔property cases, not all.** A small remap table handles `for`→`htmlFor`, `tabindex`→`tabIndex`, `colspan`/`rowspan`, etc.; read-only DOM members fall through to `setAttribute`; SVG tags are created in the SVG namespace and `class` is always set as an attribute (so SVG works). `style` objects accept camelCase, kebab-case, and CSS custom properties (`--x`). `null`/`undefined` on a DOM property are normalized to `""` (so a string prop like `id`/`src` clears, not `"null"`), but `false` is left as-is — correct for boolean props (`disabled = false`), so a reactive `false` on a *string* prop still lands as `"false"`. The same `""` normalization also hits **object/array-valued custom-element props** — a reactive `null` lands as `""`, not `null`; pass an empty array/object to clear such a prop. Pass `""` or guard in the thunk; `class` is special-cased (`false` → `""`).
|
|
347
|
+
- **An effect that writes a value it depends on (mutating each run) loops.** A runaway flush is bounded by a hard iteration cap and throws rather than hanging.
|
|
348
|
+
- **Out of scope for v1.** SSR/hydration (initialize from JS, not markup), deep/proxy reactivity (use `@wcstack/state` for path-based deep tracking), and stream backpressure are deliberately not provided — see the design doc.
|
|
349
|
+
|
|
350
|
+
## Headless usage
|
|
351
|
+
|
|
352
|
+
The reactive core has no DOM dependency — `signal` / `computed` / `effect` / `resource` / `streamResource` / `bindNode` / `nodeSource` all work in plain JS (Node, workers, tests). Only the `/dom` entry (`h` / `For` / `Index` / `SignalsElement`) touches `document`.
|
|
353
|
+
|
|
354
|
+
### SSR / non-DOM: the `./dom` entry loads without a DOM
|
|
355
|
+
|
|
356
|
+
The two entries have distinct **evaluation-time** requirements:
|
|
357
|
+
|
|
358
|
+
- **`.` entry (`@wcstack/signals`)** — fully non-DOM. It evaluates and runs in SSR, Node, and Web Workers with no DOM globals.
|
|
359
|
+
- **`./dom` entry (`@wcstack/signals/dom`)** — **evaluates** without a DOM too: importing it for the headless re-exports in an SSR pre-pass (or a worker) no longer throws `ReferenceError: HTMLElement is not defined`. The DOM-touching *surface* — `h` / `render` / `For` / `Index` and the `SignalsElement` base — still **requires** DOM globals when actually **used**.
|
|
360
|
+
|
|
361
|
+
`SignalsElement` resolves `HTMLElement` lazily: the base class is built on first subclass/use, not at module load. So `class X extends SignalsElement {}` still works as before in a browser, while `import "@wcstack/signals/dom"` is safe in a non-DOM context. To obtain the base explicitly (e.g. in code that runs in both), call `createSignalsElement()` — it builds the (memoized) base when called, and **throws a clear error** if there is no `HTMLElement`, instead of a raw `ReferenceError`:
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
import { createSignalsElement } from "@wcstack/signals/dom";
|
|
365
|
+
|
|
366
|
+
const Base = createSignalsElement(); // throws a clear error if no DOM
|
|
367
|
+
class MyEl extends Base { protected render() { /* … */ } }
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## Browser & runtime support
|
|
371
|
+
|
|
372
|
+
- **Language target: ES2022.** The shipped bundles use modern syntax (private class fields, `??`, top-level `const`/`class`) and runtime features: `queueMicrotask` (effect scheduling), `AbortController` / `AbortSignal` (`resource` / `streamResource` cancellation), `WeakMap` / `WeakSet` (`bindNode` / DOM caches), and — for `streamResource` only — `ReadableStream` + async iteration.
|
|
373
|
+
- **Minimum browsers (evergreen):** Chrome / Edge **94+**, Firefox **90+**, **Safari 16.4+** (the practical floor — `ReadableStream` async iteration and Custom Elements / private-field support all align around there). Custom Elements (`SignalsElement`) require a real DOM; everything else runs without one.
|
|
374
|
+
- **Two entries, two environments.** The `.` entry (`@wcstack/signals`: core / `resource` / `streamResource` / `bindNode` / `nodeSource`) is **non-DOM** — it runs in SSR, Node, and Web Workers. The `./dom` entry (`@wcstack/signals/dom`: `h` / `render` / `For` / `Index` / `SignalsElement`) now also **evaluates** without a DOM (so an SSR pre-pass importing it does not crash), but its DOM-touching surface requires DOM globals (`document`, `HTMLElement`) when used. See [SSR / non-DOM](#ssr--non-dom-the-dom-entry-loads-without-a-dom).
|
|
375
|
+
|
|
376
|
+
## Bundle size
|
|
377
|
+
|
|
378
|
+
Zero runtime dependencies. Measured gzipped sizes of the minified bundles:
|
|
379
|
+
|
|
380
|
+
| Entry | gzip |
|
|
381
|
+
|---|---|
|
|
382
|
+
| Shared reactive core chunk (`core-*.esm.min.js`) | **≈ 2.5 KB** |
|
|
383
|
+
| `./dom` layer chunk (`dom.esm.min.js`, on top of the shared core) | **≈ 2.1 KB** |
|
|
384
|
+
|
|
385
|
+
Both published entries import the **one** shared core chunk (see [Buildless](#buildless-import-maps)), so a page using both pays for the core only once. `package.json` declares `"sideEffects": false`, so a bundler tree-shakes away anything you don't import.
|
|
386
|
+
|
|
387
|
+
## Development mode (diagnostics)
|
|
388
|
+
|
|
389
|
+
Some failure modes are *silent in production* — the page just stops updating, or an effect quietly leaks. An **opt-in development mode** surfaces them as `console.warn` diagnostics. It is **off by default** and pays nothing in production: every warning sits behind a runtime guard with no module-top-level work, so a bundler can tree-shake it (the package is `"sideEffects": false`).
|
|
390
|
+
|
|
391
|
+
Enable it by setting a global flag **before** the code you want to diagnose runs:
|
|
392
|
+
|
|
393
|
+
```html
|
|
394
|
+
<script>globalThis.__WCS_DEV__ = true;</script>
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
The flag is read at call time, so you can toggle it on for a debugging session without a rebuild. Each distinct warning is emitted **once** (deduped) to avoid flooding the console.
|
|
398
|
+
|
|
399
|
+
Warnings:
|
|
400
|
+
|
|
401
|
+
| Code | When | Why it matters |
|
|
402
|
+
| --- | --- | --- |
|
|
403
|
+
| `DUPLICATE_KEY` | `For` is given two items with the same key. | Rendering **throws** (production behaviour unchanged) and the list stops updating. The dev warning names the offending key + index so the silent "updates stopped" symptom is diagnosable. |
|
|
404
|
+
| `NON_PRIMITIVE_KEY` | `For` is used **without** a `key` option and the items are objects/functions. | The item's object identity is the key, but it changes across renders — every row is rebuilt and per-row state is lost. Pass `{ key: item => item.id }`. |
|
|
405
|
+
| `NULLISH_KEY` | A `For` key resolves to `null` / `undefined` / `NaN`. | These collide under `SameValueZero`, so two such rows silently merge/drop. Provide a stable unique key. |
|
|
406
|
+
| `UNOWNED_EFFECT` | `effect(...)` is created with no enclosing owner. | Nothing will ever dispose it — it (and its subscriptions) leak. Wrap it in `createRoot(dispose => …)` or a `SignalsElement` `render()`. |
|
|
407
|
+
| `UNOWNED_INSERT` | A reactive child (`h("div", null, () => …)`) is inserted with no owner. | Its update effect is never disposed and may leak. Mount under `createRoot` / `SignalsElement`. |
|
|
408
|
+
| `ORPHAN_CLEANUP` | `onCleanup(...)` is called outside any owner. | It is a no-op — the cleanup never runs. Call it inside an effect, `createRoot`, or a `SignalsElement` `render()`. |
|
|
409
|
+
| `REACTIVE_CYCLE` | The runaway-cycle guard (`MAX_FLUSH_ITERATIONS`) trips. | In dev the thrown `Error` message is enriched with the effects still re-running on the final pass and the stacks of where they were created, so you can find the cycle's participants. |
|
|
410
|
+
|
|
411
|
+
## Error-handling contract
|
|
412
|
+
|
|
413
|
+
- **Effect / computed exceptions are isolated, not propagated.** A throw from an `effect` body (or a `computed` refreshed inside a flush) is handed to `globalThis.reportError` when present — which dispatches a window `error` event / logs without aborting the task — and falls back to `console.error` otherwise. It is **never re-thrown** (that would abort the drain and strand sibling effects) and **never silently swallowed** (that would hide the bug). The node still settles to `CLEAN`, so a transient throw does not wedge it DIRTY — it re-runs on the next dependency change.
|
|
414
|
+
- **The only throw that escapes a flush** is the runaway-cycle guard: an effect that writes a signal it depends on with an ever-changing value is bounded by a hard iteration cap (`MAX_FLUSH_ITERATIONS`); on overflow the queue is dropped and an `Error` is thrown so the bug surfaces instead of hanging the page.
|
|
415
|
+
- **A direct `get()` / `peek()`** (outside a flush) still throws to *its* caller — isolation applies to the scheduled drain, not to synchronous reads you make yourself.
|
|
416
|
+
- **`DisposedError` / `isDisposedError`.** Every mutating `BoundNode` method (`on` / `set` / `bindInput` / `command` / `bindCommand`) throws a `DisposedError` after `dispose()`, so a use-after-dispose is loud. Prefer `isDisposedError(err)` over `instanceof` (it is brand-based, so it survives a bundler duplicating the class across realms). Use it to make teardown-order races robust — swallow a post-dispose throw while letting any other error surface:
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
import { isDisposedError } from "@wcstack/signals";
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
bound.command("abort");
|
|
423
|
+
} catch (err) {
|
|
424
|
+
if (!isDisposedError(err)) throw err; // expected during teardown; ignore
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
## Development
|
|
429
|
+
|
|
430
|
+
```bash
|
|
431
|
+
npm run build # clean → tsc → rollup (dual entry: index + dom)
|
|
432
|
+
npm test # vitest run
|
|
433
|
+
npm run test:coverage # coverage (thresholds 100/100/100/100)
|
|
434
|
+
npm run lint # eslint src
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
## License
|
|
438
|
+
|
|
439
|
+
MIT
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function e(e){const t=globalThis.reportError;"function"==typeof t?t(e):console.error(e)}function t(){return!0===globalThis.__WCS_DEV__}const n=new Set;function s(e,s,o,...r){if(!t())return;const i=e+"\0"+s;n.has(i)||(n.add(i),console.warn(`[wcstack/signals][${e}] ${o}`,...r))}let o=null;function r(e){null!==o&&(o._sources.add(e),e._observers.add(o))}function i(e){for(const t of e._sources)t._observers.delete(e);e._sources.clear()}let a=null;function c(e){null!==a&&a._owned.push(e)}function d(e){const t=e._owned;for(let e=t.length-1;e>=0;e--)t[e]();t.length=0}let l=new Set,u=!1;const f=[];function h(){return f.pop()??new Set}function _(){u||(u=!0,queueMicrotask(b))}const p=1e3;function w(e){const t=e.filter(e=>!e._disposed),n=t.map((e,t)=>` #${t+1} ${e._devStack?e._devStack.split("\n").slice(1,3).join(" | ").trim():"(no stack)"}`);return`\n[wcstack/signals] dev diagnostics: ${t.length} effect(s) still re-running on the final pass (likely cycle participants):\n${n.join("\n")}`}function b(){u=!1;let n=0,s=null;for(;l.size>0;){if(++n>p)throw l.clear(),new Error("flushEffects: exceeded "+p+" iterations — likely a reactive cycle (an effect writes a signal it depends on with an ever-changing value)."+(s?w(s):""));const o=l;l=h(),t()&&(s=[...o]);for(const t of o)if(!t._disposed)try{g(t)}catch(t){e(t)}o.clear(),f.push(o)}}function v(){b()}function m(e,t){if(e._state<t){const n=0===e._state;e._state=t,n&&e._isEffect&&(l.add(e),_());for(const t of e._observers)m(t,1)}}function g(e){if(0!==e._state){if(1===e._state)for(const t of e._sources)if(t instanceof E&&(g(t),2===e._state))break;try{2===e._state&&e._update()}finally{e._state=0}}}class y{_observers=new Set;_value;_equals;constructor(e,t){this._value=e,this._equals=t}get(){return r(this),this._value}peek(){return this._value}set(e){if(!this._equals(this._value,e)){this._value=e;for(const e of this._observers)m(e,2)}}}class E{_observers=new Set;_sources=new Set;_state=2;_isEffect=!1;_fn;_value=void 0;_initialized=!1;_running=!1;_equals;constructor(e,t){this._fn=e,this._equals=t,c(()=>i(this))}get(){return r(this),g(this),this._value}peek(){return g(this),this._value}_update(){if(this._running)throw new Error("computed: circular dependency — the computed reads itself.");const e=this._value;i(this);const t=o;o=this,this._running=!0;try{this._value=this._fn()}finally{this._running=!1,o=t}if(this._initialized){if(!this._equals(e,this._value))for(const e of this._observers)e._state=2,e._isEffect&&!e._disposed&&(l.add(e),_())}else this._initialized=!0}}class S{_observers=new Set;_sources=new Set;_owned=[];_state=2;_isEffect=!0;_disposed=!1;_devStack=void 0;_fn;_cleanup=void 0;constructor(e){this._fn=e,t()&&(this._devStack=(new Error).stack),c(()=>this.dispose()),g(this)}_update(){this._runCleanup(),d(this),i(this);const e=o,t=a;o=this,a=this;try{this._cleanup=this._fn()}finally{o=e,a=t}}_runCleanup(){"function"==typeof this._cleanup&&(this._cleanup(),this._cleanup=void 0)}dispose(){this._disposed||(this._disposed=!0,this._runCleanup(),d(this),i(this),l.delete(this))}}function k(e,t=Object.is){return new y(e,t)}function $(e,t=Object.is){return new E(e,t)}function C(e){return t()&&null===a&&s("UNOWNED_EFFECT","","effect created with no owner — it will never be disposed and may leak. Wrap it in createRoot(dispose => …) (or a SignalsElement render()) and dispose on teardown."),new S(e)}function N(e){const t={_owned:[]};let n=!1;const s=()=>{n||(n=!0,d(t))},r=a,i=o;a=t,o=null;try{return e(s)}catch(e){throw s(),e}finally{a=r,o=i}}function L(){return null!==a}function q(e){null===a&&s("ORPHAN_CLEANUP","","onCleanup called with no owner — the cleanup will never run. Call it inside an effect, createRoot, or a SignalsElement render()."),c(e)}function R(e,t={}){const n=k(t.initial),s=k(!1),o=k(null);let r=null;const i=C(()=>{const i=t.args?t.args():void 0;r?.abort();const a=new AbortController;let c;r=a,s.set(!0),o.set(null);try{c=e(i,a.signal)}catch(e){return o.set(e),void s.set(!1)}Promise.resolve(c).then(e=>{a.signal.aborted||(n.set(e),s.set(!1))},e=>{a.signal.aborted||(o.set(e),s.set(!1))})}),a=()=>{r?.abort(),i.dispose()};return q(a),{value:n,loading:s,error:o,dispose:a}}function I(e,t={}){const n=t.fold??((e,t)=>t),s=k(t.initial),o=k("idle"),r=k(null);let i=null;const a=C(()=>{const a=t.args?t.args():void 0;i?.abort();const c=new AbortController;i=c,s.set(t.initial),r.set(null),o.set("active"),async function(e,t,n,s,o,r,i){let a=null,c=null;const d=()=>{if(a&&a!==c){c=a;try{a.return?.()?.then?.(void 0,()=>{})}catch{}}};n.addEventListener("abort",d,{once:!0});try{const i=await e(t,n);if(a=function(e,t){if("function"==typeof e[Symbol.asyncIterator])return e;if("function"!=typeof e?.getReader)throw new TypeError("streamResource: source must return an AsyncIterable or a ReadableStream (got neither).");return async function*(e,t){const n=e.getReader(),s=()=>{n.cancel().catch(()=>{})};t.addEventListener("abort",s,{once:!0});try{for(;;){const{done:e,value:t}=await n.read();if(e)return;yield t}}finally{t.removeEventListener("abort",s),n.releaseLock()}}(e,t)}(i,n)[Symbol.asyncIterator](),n.aborted)return void d();for(;;){const e=await a.next();if(e.done)break;if(n.aborted)return;o.set(s(o.peek(),e.value))}if(n.aborted)return;r.set("done")}catch(e){if(n.aborted)return;i.set(e),r.set("error")}finally{n.removeEventListener("abort",d)}}(e,a,c.signal,n,s,o,r)}),c=()=>{i?.abort(),a.dispose()};return q(c),{value:s,status:o,error:r,dispose:c}}const T=Symbol("bindNode.disposed");class j extends Error{[T]=!0}function A(e){return"object"==typeof e&&null!==e&&!0===e[T]}function x(e,t){return e[t]}function z(e,t){const n=e,s=t??n.constructor.wcBindable;if(!s)throw new Error("bindNode: no wc-bindable descriptor provided and none found on target.constructor.wcBindable");const o=new Map(s.properties.map(e=>[e.name,e])),r=new Set((s.inputs??[]).map(e=>e.name)),i=new Set((s.commands??[]).map(e=>e.name)),a={},c=[];let d=!1;const l=(e,t)=>{if(d)throw new j(`bindNode.${e}: "${t}" called after dispose (the adapter is inert).`)};for(const e of s.properties){const t=k(x(n,e.name));a[e.name]=t;const s=s=>{t.set(e.getter?e.getter(s):x(n,e.name))};n.addEventListener(e.event,s),c.push(()=>n.removeEventListener(e.event,s)),t.set(x(n,e.name))}return{signals:a,on(e,t={}){l("on",e);const s=o.get(e);if(!s)throw new Error(`bindNode.on: "${e}" is not a declared property on this node.`);const r=t.fold??((e,t)=>t),i=k(t.initial,()=>!1),a=t=>{const o=s.getter?s.getter(t):x(n,e);i.set(r(i.peek(),o))};return n.addEventListener(s.event,a),c.push(()=>n.removeEventListener(s.event,a)),i},set(e,t){if(l("set",e),!r.has(e))throw new Error(`bindNode.set: "${e}" is not a declared input on this node.`);n[e]=t},bindInput(e,t){if(l("bindInput",e),!r.has(e))throw new Error(`bindNode.bindInput: "${e}" is not a declared input on this node.`);const s=C(()=>{const s=t.get();n[e]!==s&&(n[e]=s)});return c.push(()=>s.dispose()),()=>s.dispose()},command(e,...t){if(l("command",e),!i.has(e))throw new Error(`bindNode.command: "${e}" is not a declared command on this node.`);if("function"!=typeof n[e])throw new TypeError(`bindNode.command: "${e}" is declared but not a function on the node.`);return n[e](...t)},bindCommand(e,t,s){if(l("bindCommand",e),!i.has(e))throw new Error(`bindNode.bindCommand: "${e}" is not a declared command on this node.`);if("function"!=typeof n[e])throw new TypeError(`bindNode.bindCommand: "${e}" is declared but not a function on the node.`);let o=!1;const r=C(()=>{const r=t.get();if(!o)return void(o=!0);const i=s?s(r):[r];n[e](...i)});return c.push(()=>r.dispose()),()=>r.dispose()},dispose(){if(!d){d=!0;for(const e of c)e()}}}}function O(t,n,s={}){const o=s.abort??"abort";return(s,r)=>(r.addEventListener("abort",()=>{try{t.command(o)}catch(t){A(t)||e(t)}},{once:!0}),n(t,s))}export{j as D,N as a,z as b,$ as c,I as d,C as e,v as f,t as g,L as h,A as i,O as n,q as o,R as r,k as s,s as w};
|
|
2
|
+
//# sourceMappingURL=core-DCHZTbGv.esm.min.js.map
|