@zeix/cause-effect 0.18.5 → 1.0.1
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/.github/copilot-instructions.md +2 -1
- package/.zed/settings.json +24 -1
- package/ARCHITECTURE.md +2 -2
- package/CHANGELOG.md +23 -0
- package/README.md +42 -1
- package/REQUIREMENTS.md +3 -3
- package/bench/reactivity.bench.ts +18 -7
- package/biome.json +1 -1
- package/eslint.config.js +2 -1
- package/index.dev.js +11 -5
- package/index.js +1 -1
- package/index.ts +1 -1
- package/package.json +6 -6
- package/skills/cause-effect/SKILL.md +69 -0
- package/skills/cause-effect/agents/openai.yaml +4 -0
- package/skills/cause-effect/references/api-facts.md +179 -0
- package/skills/cause-effect/references/error-classes.md +153 -0
- package/skills/cause-effect/references/non-obvious-behaviors.md +173 -0
- package/skills/cause-effect/references/signal-types.md +288 -0
- package/skills/cause-effect/workflows/answer-question.md +54 -0
- package/skills/cause-effect/workflows/debug.md +71 -0
- package/skills/cause-effect/workflows/use-api.md +63 -0
- package/skills/cause-effect-dev/SKILL.md +75 -0
- package/skills/cause-effect-dev/agents/openai.yaml +4 -0
- package/skills/cause-effect-dev/references/api-facts.md +96 -0
- package/skills/cause-effect-dev/references/error-classes.md +97 -0
- package/skills/cause-effect-dev/references/internal-types.md +54 -0
- package/skills/cause-effect-dev/references/non-obvious-behaviors.md +146 -0
- package/skills/cause-effect-dev/references/source-map.md +45 -0
- package/skills/cause-effect-dev/workflows/answer-question.md +55 -0
- package/skills/cause-effect-dev/workflows/fix-bug.md +63 -0
- package/skills/cause-effect-dev/workflows/implement-feature.md +46 -0
- package/skills/cause-effect-dev/workflows/write-tests.md +64 -0
- package/skills/changelog-keeper/SKILL.md +47 -37
- package/skills/tech-writer/SKILL.md +94 -0
- package/skills/tech-writer/references/document-map.md +199 -0
- package/skills/tech-writer/references/tone-guide.md +189 -0
- package/skills/tech-writer/workflows/consistency-review.md +98 -0
- package/skills/tech-writer/workflows/update-after-change.md +65 -0
- package/skills/tech-writer/workflows/update-agent-docs.md +77 -0
- package/skills/tech-writer/workflows/update-architecture.md +61 -0
- package/skills/tech-writer/workflows/update-jsdoc.md +72 -0
- package/skills/tech-writer/workflows/update-public-api.md +59 -0
- package/skills/tech-writer/workflows/update-requirements.md +80 -0
- package/src/graph.ts +8 -4
- package/src/nodes/collection.ts +42 -2
- package/src/nodes/effect.ts +13 -1
- package/src/nodes/list.ts +28 -4
- package/src/nodes/memo.ts +0 -1
- package/src/nodes/sensor.ts +10 -4
- package/src/nodes/store.ts +11 -0
- package/src/signal.ts +6 -0
- package/test/benchmark.test.ts +25 -11
- package/test/collection.test.ts +6 -3
- package/test/effect.test.ts +2 -1
- package/test/list.test.ts +8 -4
- package/test/regression.test.ts +4 -2
- package/test/store.test.ts +8 -4
- package/test/util/dependency-graph.ts +12 -6
- package/tsconfig.json +14 -1
- package/types/index.d.ts +1 -1
- package/types/src/graph.d.ts +2 -2
- package/OWNERSHIP_BUG.md +0 -95
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
<overview>
|
|
2
|
+
What each signal type in @zeix/cause-effect is for, when to use each one, and how to choose between similar types. All knowledge is embedded — no external files required.
|
|
3
|
+
</overview>
|
|
4
|
+
|
|
5
|
+
<signal_catalog>
|
|
6
|
+
|
|
7
|
+
<State>
|
|
8
|
+
**What it is:** A mutable reactive value you own and update explicitly.
|
|
9
|
+
|
|
10
|
+
**Use when:**
|
|
11
|
+
- You control when and how the value changes
|
|
12
|
+
- The value is UI state, form input, a counter, a toggle, a selection
|
|
13
|
+
- You need to write to it directly with `.set()`
|
|
14
|
+
|
|
15
|
+
**Key facts:**
|
|
16
|
+
- Requires an initial value
|
|
17
|
+
- Synchronous reads and writes
|
|
18
|
+
- Supports `equals` (default `===`) and `guard` options
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
const count = createState(0)
|
|
22
|
+
count.set(count.get() + 1)
|
|
23
|
+
count.update(n => n + 1) // if update helper exists; else use set
|
|
24
|
+
```
|
|
25
|
+
</State>
|
|
26
|
+
|
|
27
|
+
<Sensor>
|
|
28
|
+
**What it is:** A reactive value produced by an external source you don't control.
|
|
29
|
+
|
|
30
|
+
**Use when:**
|
|
31
|
+
- The value comes from outside the reactive graph: DOM events, timers, WebSocket messages, geolocation, device orientation, IntersectionObserver, etc.
|
|
32
|
+
- You need `watched`/`unwatched` hooks to start and stop the external subscription efficiently
|
|
33
|
+
- The value has no meaningful initial state before the source fires
|
|
34
|
+
|
|
35
|
+
**Key facts:**
|
|
36
|
+
- Starts **unset** — reading before the first value throws `UnsetSignalValueError`; use `match` to handle the initial state
|
|
37
|
+
- `watched` fires when the first downstream effect subscribes; `unwatched` fires when the last one unsubscribes
|
|
38
|
+
- The setup function receives a `set` callback; return a cleanup function to tear down the subscription
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
const pointer = createSensor<{ x: number; y: number }>(set => {
|
|
42
|
+
const handler = (e: PointerEvent) => set({ x: e.clientX, y: e.clientY })
|
|
43
|
+
window.addEventListener('pointermove', handler)
|
|
44
|
+
return () => window.removeEventListener('pointermove', handler)
|
|
45
|
+
})
|
|
46
|
+
```
|
|
47
|
+
</Sensor>
|
|
48
|
+
|
|
49
|
+
<Memo>
|
|
50
|
+
**What it is:** A synchronously derived value that stays in sync with its dependencies.
|
|
51
|
+
|
|
52
|
+
**Use when:**
|
|
53
|
+
- The value can be computed from other signals without async work
|
|
54
|
+
- You want to avoid recomputing an expensive derivation on every read
|
|
55
|
+
- You need a stable reference: Memo caches the last computed value and only recomputes when dependencies change
|
|
56
|
+
|
|
57
|
+
**Key facts:**
|
|
58
|
+
- Lazy — only recomputes when read after a dependency has changed
|
|
59
|
+
- Receives `prev` as its first argument (enables referential stability patterns)
|
|
60
|
+
- Supports `equals` to suppress downstream propagation when the new value is equivalent
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
const fullName = createMemo(() => `${firstName.get()} ${lastName.get()}`)
|
|
64
|
+
```
|
|
65
|
+
</Memo>
|
|
66
|
+
|
|
67
|
+
<Task>
|
|
68
|
+
**What it is:** An asynchronously derived value — like Memo, but async.
|
|
69
|
+
|
|
70
|
+
**Use when:**
|
|
71
|
+
- The derivation requires `await` (data fetching, async transforms, indexed DB reads)
|
|
72
|
+
- You want automatic cancellation of in-flight work when dependencies change
|
|
73
|
+
|
|
74
|
+
**Key facts:**
|
|
75
|
+
- Starts **unset** until the first async operation completes; use `match` for the loading state
|
|
76
|
+
- Receives `(prev, signal: AbortSignal)` — always forward `signal` to `fetch` or any cancellable async operation to prevent stale responses overwriting fresh ones
|
|
77
|
+
- Re-runs automatically when tracked dependencies change, aborting the previous run
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
const results = createTask(async (prev, signal) => {
|
|
81
|
+
const res = await fetch(`/api/search?q=${query.get()}`, { signal })
|
|
82
|
+
return res.json()
|
|
83
|
+
})
|
|
84
|
+
```
|
|
85
|
+
</Task>
|
|
86
|
+
|
|
87
|
+
<Effect>
|
|
88
|
+
**What it is:** A side effect that runs when its tracked dependencies change.
|
|
89
|
+
|
|
90
|
+
**Use when:**
|
|
91
|
+
- You need to synchronise reactive state with the outside world: update the DOM, write to localStorage, send analytics, call an imperative library
|
|
92
|
+
- You need a reactive subscription that runs code (not just derives a value)
|
|
93
|
+
|
|
94
|
+
**Key facts:**
|
|
95
|
+
- **Must be created inside an owner** (`createScope` or another effect) — throws `RequiredOwnerError` otherwise
|
|
96
|
+
- Runs immediately on creation, then re-runs on dependency changes
|
|
97
|
+
- Returns a `Cleanup` function; calling it disposes the effect and all its children
|
|
98
|
+
- Use `unown` inside `connectedCallback` / `disconnectedCallback` when the DOM manages the element's lifetime
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
const dispose = createScope(() => {
|
|
102
|
+
createEffect(() => {
|
|
103
|
+
document.title = pageTitle.get()
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
// later: dispose()
|
|
107
|
+
```
|
|
108
|
+
</Effect>
|
|
109
|
+
|
|
110
|
+
<Slot>
|
|
111
|
+
**What it is:** A reactive property descriptor — a signal packaged as a getter/setter pair compatible with `Object.defineProperty`.
|
|
112
|
+
|
|
113
|
+
**Use when:**
|
|
114
|
+
- You need to attach a reactive value as a property on an object (e.g. a Web Component's observed attribute)
|
|
115
|
+
- You want property access (`element.name`) to participate in the reactive graph
|
|
116
|
+
|
|
117
|
+
**Key facts:**
|
|
118
|
+
- Has `get`, `set`, `configurable`, and `enumerable` fields
|
|
119
|
+
- Can be passed directly to `Object.defineProperty`
|
|
120
|
+
- Backed by a `State` internally
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
const nameSlot = createSlot(store, 'name')
|
|
124
|
+
Object.defineProperty(element, 'name', nameSlot)
|
|
125
|
+
```
|
|
126
|
+
</Slot>
|
|
127
|
+
|
|
128
|
+
<Store>
|
|
129
|
+
**What it is:** A reactive object whose properties are individually reactive.
|
|
130
|
+
|
|
131
|
+
**Use when:**
|
|
132
|
+
- You have a group of related values that are read and updated independently
|
|
133
|
+
- You want fine-grained reactivity on an object's fields rather than replacing the whole object
|
|
134
|
+
|
|
135
|
+
**Key facts:**
|
|
136
|
+
- Reading a property inside an effect creates a dependency on that property only
|
|
137
|
+
- Updating one property does not re-run effects that only read other properties
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
const user = createStore({ name: 'Alice', age: 30 })
|
|
141
|
+
user.name = 'Bob' // only effects reading `user.name` re-run
|
|
142
|
+
```
|
|
143
|
+
</Store>
|
|
144
|
+
|
|
145
|
+
<List>
|
|
146
|
+
**What it is:** An ordered, keyed reactive collection — an array where each item has a stable identity.
|
|
147
|
+
|
|
148
|
+
**Use when:**
|
|
149
|
+
- Order matters and items have identity (drag-and-drop lists, ranked results, timelines)
|
|
150
|
+
- You need to react to structural changes (items added, removed, reordered) as well as value changes
|
|
151
|
+
|
|
152
|
+
**Key facts:**
|
|
153
|
+
- Items are identified by a key; keys must be unique
|
|
154
|
+
- `byKey()`, `at()`, `keyAt()`, and `indexOfKey()` are direct lookups — they **do not create graph edges**
|
|
155
|
+
- To react to structural changes, read `get()`, `keys()`, or `length` instead
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
const todos = createList<Todo, 'id'>('id', [
|
|
159
|
+
{ id: '1', text: 'Buy milk', done: false },
|
|
160
|
+
])
|
|
161
|
+
todos.push({ id: '2', text: 'Walk dog', done: false })
|
|
162
|
+
```
|
|
163
|
+
</List>
|
|
164
|
+
|
|
165
|
+
<Collection>
|
|
166
|
+
**What it is:** A keyed reactive collection — a reactive Map.
|
|
167
|
+
|
|
168
|
+
**Use when:**
|
|
169
|
+
- Items are identified by key and order is not meaningful or variable
|
|
170
|
+
- You need fast key-based lookup with reactive tracking on the key set and individual entries
|
|
171
|
+
- Use cases: entity caches, normalised data stores, lookup tables
|
|
172
|
+
|
|
173
|
+
**Key facts:**
|
|
174
|
+
- `createCollection` creates a collection from an initial set of entries
|
|
175
|
+
- `deriveCollection` creates a collection derived from another reactive source
|
|
176
|
+
- Same tracking rules as List: `byKey()` does not create graph edges; read `get()`, `keys()`, or `length` to subscribe to structural changes
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
const users = createCollection<string, User>(
|
|
180
|
+
existingUsers.map(u => [u.id, u])
|
|
181
|
+
)
|
|
182
|
+
```
|
|
183
|
+
</Collection>
|
|
184
|
+
|
|
185
|
+
</signal_catalog>
|
|
186
|
+
|
|
187
|
+
<decision_guide>
|
|
188
|
+
|
|
189
|
+
<choose_by_value_source>
|
|
190
|
+
**Who controls the value?**
|
|
191
|
+
|
|
192
|
+
- You set it explicitly → **State**
|
|
193
|
+
- An external event or subscription provides it → **Sensor**
|
|
194
|
+
- It is computed from other signals (sync) → **Memo**
|
|
195
|
+
- It is computed from other signals (async) → **Task**
|
|
196
|
+
</choose_by_value_source>
|
|
197
|
+
|
|
198
|
+
<choose_by_purpose>
|
|
199
|
+
**What do you need to do with it?**
|
|
200
|
+
|
|
201
|
+
- Read a derived value without side effects → **Memo** or **Task**
|
|
202
|
+
- Run a side effect when something changes → **Effect**
|
|
203
|
+
- Expose a reactive value as an object property → **Slot**
|
|
204
|
+
- Group related reactive values on an object → **Store**
|
|
205
|
+
- Maintain an ordered list of keyed items → **List**
|
|
206
|
+
- Maintain an unordered map of keyed items → **Collection**
|
|
207
|
+
</choose_by_purpose>
|
|
208
|
+
|
|
209
|
+
<direct_comparisons>
|
|
210
|
+
|
|
211
|
+
**State vs Sensor**
|
|
212
|
+
Use `State` when you call `.set()` yourself. Use `Sensor` when an external source calls the setter — the library manages the subscription lifecycle via `watched`/`unwatched`.
|
|
213
|
+
|
|
214
|
+
**Memo vs Task**
|
|
215
|
+
Use `Memo` for synchronous derivations. Use `Task` when derivation requires `await`. Both receive `prev` and both support `equals`.
|
|
216
|
+
|
|
217
|
+
**Memo vs Effect**
|
|
218
|
+
`Memo` derives a value (no side effects, lazy). `Effect` runs side effects (imperative, eager, requires owner).
|
|
219
|
+
|
|
220
|
+
**State vs Store**
|
|
221
|
+
Use `State` for a single primitive or object that is always replaced wholesale. Use `Store` for an object whose individual properties are read and updated independently — Store gives you field-level reactivity.
|
|
222
|
+
|
|
223
|
+
**List vs Collection**
|
|
224
|
+
Both are keyed. Use `List` when order is significant (rendering order, ranking, sorting). Use `Collection` when items are looked up by key and order is not meaningful.
|
|
225
|
+
|
|
226
|
+
**List / Collection vs Store**
|
|
227
|
+
Use `Store` for a fixed set of named properties on a single object. Use `List` or `Collection` for a dynamic number of items with uniform shape.
|
|
228
|
+
|
|
229
|
+
</direct_comparisons>
|
|
230
|
+
|
|
231
|
+
</decision_guide>
|
|
232
|
+
|
|
233
|
+
<common_patterns>
|
|
234
|
+
|
|
235
|
+
<loading_state>
|
|
236
|
+
Sensor and Task start unset. Use `match` to handle all states in one expression:
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
createEffect(() => {
|
|
240
|
+
match([task], {
|
|
241
|
+
ok: ([data]) => renderData(data),
|
|
242
|
+
err: ([error]) => renderError(error),
|
|
243
|
+
nil: () => renderSpinner(),
|
|
244
|
+
})
|
|
245
|
+
})
|
|
246
|
+
```
|
|
247
|
+
</loading_state>
|
|
248
|
+
|
|
249
|
+
<grouping_effects>
|
|
250
|
+
Always wrap top-level effects in `createScope` to control their lifetime:
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
const dispose = createScope(() => {
|
|
254
|
+
createEffect(() => { /* ... */ })
|
|
255
|
+
createEffect(() => { /* ... */ })
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
// When done (e.g. component unmounted):
|
|
259
|
+
dispose()
|
|
260
|
+
```
|
|
261
|
+
</grouping_effects>
|
|
262
|
+
|
|
263
|
+
<coalescing_updates>
|
|
264
|
+
Use `batch` when multiple state writes should trigger only one downstream propagation:
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
batch(() => {
|
|
268
|
+
x.set(1)
|
|
269
|
+
y.set(2)
|
|
270
|
+
z.set(3)
|
|
271
|
+
// downstream effects run once, after all three are set
|
|
272
|
+
})
|
|
273
|
+
```
|
|
274
|
+
</coalescing_updates>
|
|
275
|
+
|
|
276
|
+
<reading_without_subscribing>
|
|
277
|
+
Use `untrack` to read a signal's current value without creating a dependency edge:
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
createEffect(() => {
|
|
281
|
+
const primary = primary.get() // tracked — re-runs when primary changes
|
|
282
|
+
const snapshot = untrack(() => log.get()) // not tracked — just reads current value
|
|
283
|
+
console.log(primary, snapshot)
|
|
284
|
+
})
|
|
285
|
+
```
|
|
286
|
+
</reading_without_subscribing>
|
|
287
|
+
|
|
288
|
+
</common_patterns>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<required_reading>
|
|
2
|
+
Load references based on question type — only what is needed:
|
|
3
|
+
|
|
4
|
+
- Which signal to use → references/signal-types.md
|
|
5
|
+
- API usage, call signatures, options → references/api-facts.md
|
|
6
|
+
- Unexpected or counterintuitive behavior → references/non-obvious-behaviors.md
|
|
7
|
+
- A thrown error → references/error-classes.md
|
|
8
|
+
- Design rationale or constraints → references/signal-types.md + references/api-facts.md
|
|
9
|
+
</required_reading>
|
|
10
|
+
|
|
11
|
+
<process>
|
|
12
|
+
## Step 1: Categorise the question
|
|
13
|
+
|
|
14
|
+
| Category | Signal words | Load |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| Which signal to use | "should I use", "difference between", "when to use", "State vs", "Memo vs" | signal-types.md |
|
|
17
|
+
| API usage / call signature | "how do I", "what does X return", "what are the options for", "how to create" | api-facts.md |
|
|
18
|
+
| Unexpected behavior | "why does this not work", "is this a bug", "why is watched not firing", "not re-running" | non-obvious-behaviors.md |
|
|
19
|
+
| Error meaning | "what does X error mean", "when is Y thrown", "getting RequiredOwnerError" | error-classes.md |
|
|
20
|
+
| Design rationale | "why was X designed this way", "why no null", "why is T extends {}" | signal-types.md + api-facts.md |
|
|
21
|
+
|
|
22
|
+
## Step 2: Load only the relevant references
|
|
23
|
+
|
|
24
|
+
Do not load all references speculatively. Load only the file(s) listed for the identified category.
|
|
25
|
+
|
|
26
|
+
## Step 3: Answer from embedded knowledge
|
|
27
|
+
|
|
28
|
+
All knowledge needed to answer public-API questions is in `references/`. Ground every claim in a reference. Cite the section when the answer is non-obvious (e.g. "per `non-obvious-behaviors.md`…").
|
|
29
|
+
|
|
30
|
+
For counterintuitive behaviors, always show a correct vs incorrect code example alongside the explanation.
|
|
31
|
+
|
|
32
|
+
## Step 4: When embedded knowledge is insufficient
|
|
33
|
+
|
|
34
|
+
If the question requires detail beyond what the references cover (e.g. exact internal flag values, propagation algorithm specifics, architecture decisions), do not guess. Instead:
|
|
35
|
+
- Point to the library's README and GUIDE in the npm package or GitHub repository for public API depth
|
|
36
|
+
- Note that library internals are available in `node_modules/@zeix/cause-effect/src/` if truly needed, but internal details are not part of the public API contract and may change
|
|
37
|
+
|
|
38
|
+
## Step 5: Design rationale questions
|
|
39
|
+
|
|
40
|
+
When asked why the library was designed a certain way, distinguish between:
|
|
41
|
+
- **Hard constraints** (e.g. `T extends {}` excludes null/undefined by design to guarantee signals always have a value; the signal type set is intentionally minimal and closed to new types)
|
|
42
|
+
- **Soft conventions** (naming patterns, file organisation, option defaults)
|
|
43
|
+
|
|
44
|
+
If the distinction is unclear from the embedded references, say so rather than speculating.
|
|
45
|
+
</process>
|
|
46
|
+
|
|
47
|
+
<success_criteria>
|
|
48
|
+
- Answer is grounded in embedded reference knowledge
|
|
49
|
+
- No references to external files that may not exist in the consumer project (REQUIREMENTS.md, ARCHITECTURE.md, src/, etc.)
|
|
50
|
+
- Source cited when the answer is non-obvious
|
|
51
|
+
- Counterintuitive behaviors include correct vs incorrect code examples
|
|
52
|
+
- When knowledge is insufficient, points to upstream documentation rather than guessing
|
|
53
|
+
- No reference files loaded beyond what the question required
|
|
54
|
+
</success_criteria>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<required_reading>
|
|
2
|
+
1. references/non-obvious-behaviors.md — most reactive bugs are documented here
|
|
3
|
+
2. references/error-classes.md — if a specific error is thrown
|
|
4
|
+
3. references/api-facts.md — to verify the signal is being used correctly
|
|
5
|
+
</required_reading>
|
|
6
|
+
|
|
7
|
+
<process>
|
|
8
|
+
## Step 1: Categorise the symptom
|
|
9
|
+
|
|
10
|
+
Match the symptom to the most likely cause before reading any code:
|
|
11
|
+
|
|
12
|
+
| Symptom | Most likely cause | Where to look |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| Effect doesn't re-run when a signal changes | No graph edge established (conditional read or direct lookup) | non-obvious-behaviors: `conditional-reads-delay-watched`, `direct-lookups-do-not-track` |
|
|
15
|
+
| Effect re-runs too often | Missing `equals` option, or mutable reference without `SKIP_EQUALITY` | api-facts: `options.equals`, `SKIP_EQUALITY` |
|
|
16
|
+
| `watched` never fires | Signal only read inside a conditional branch | non-obvious-behaviors: `conditional-reads-delay-watched` |
|
|
17
|
+
| Collection/List update not reflected in effect | Using `byKey()`, `at()`, `keyAt()`, or `indexOfKey()` | non-obvious-behaviors: `direct-lookups-do-not-track` |
|
|
18
|
+
| Stale async result overwrites fresh one | `AbortSignal` not forwarded to `fetch` or async operation | non-obvious-behaviors: `task-abort-on-dependency-change` |
|
|
19
|
+
| `UnsetSignalValueError` thrown | Reading Sensor or Task before first value | non-obvious-behaviors: `sensor-unset-before-first-value` |
|
|
20
|
+
| `RequiredOwnerError` thrown | `createEffect` called outside a `createScope` or parent effect | api-facts: `createEffect`, `createScope` |
|
|
21
|
+
| `CircularDependencyError` thrown | A signal depends on itself, directly or transitively | error-classes: `CircularDependencyError` |
|
|
22
|
+
| Downstream Memo or Effect never updates despite source changing | Custom `equals` on an intermediate Memo is suppressing the subgraph | non-obvious-behaviors: `equals-suppresses-subtrees` |
|
|
23
|
+
| Cleanup not running / memory leak suspected | Effect created without an active owner, or `unown` used incorrectly | api-facts: `unown`, `createScope` |
|
|
24
|
+
|
|
25
|
+
## Step 2: Read the relevant section
|
|
26
|
+
|
|
27
|
+
Read the specific section in references/non-obvious-behaviors.md or references/error-classes.md that matches the symptom. Compare the pattern shown there to the code in question.
|
|
28
|
+
|
|
29
|
+
## Step 3: Verify API usage
|
|
30
|
+
|
|
31
|
+
If the symptom does not match a known gotcha, read references/api-facts.md to confirm:
|
|
32
|
+
- Signal generics use `T extends {}` (no `null` or `undefined`)
|
|
33
|
+
- `createEffect` is inside an owner (`createScope` or another effect)
|
|
34
|
+
- `unown` is used only for DOM-owned lifecycles, not to bypass ownership generally
|
|
35
|
+
- `batch` is used if multiple state writes should coalesce
|
|
36
|
+
|
|
37
|
+
## Step 4: Inspect library source (last resort)
|
|
38
|
+
|
|
39
|
+
If the embedded references do not explain the behavior, the library source is available at:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
node_modules/@zeix/cause-effect/src/
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Read the relevant file there — do **not** assume library source files exist at the project root.
|
|
46
|
+
|
|
47
|
+
## Step 5: Fix
|
|
48
|
+
|
|
49
|
+
Apply the minimal change that addresses the root cause. Do not suppress the symptom (e.g. wrapping a read in `untrack`) without first confirming that is the intended behavior.
|
|
50
|
+
|
|
51
|
+
## Step 6: Verify
|
|
52
|
+
|
|
53
|
+
Run the project's own test suite:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# use whichever applies to this project
|
|
57
|
+
npm test
|
|
58
|
+
pnpm test
|
|
59
|
+
yarn test
|
|
60
|
+
npx vitest
|
|
61
|
+
npx jest
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Do not assume `bun` or any specific test runner is available.
|
|
65
|
+
</process>
|
|
66
|
+
|
|
67
|
+
<success_criteria>
|
|
68
|
+
- Root cause identified, not symptom suppressed
|
|
69
|
+
- Fix matches the correct pattern from references/non-obvious-behaviors.md where applicable
|
|
70
|
+
- Project test suite passes after the fix
|
|
71
|
+
</success_criteria>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<required_reading>
|
|
2
|
+
1. references/signal-types.md — choose the right signal type(s) for the task
|
|
3
|
+
2. references/api-facts.md — correct API usage, constraints, and callback patterns
|
|
4
|
+
3. references/non-obvious-behaviors.md — avoid common pitfalls before writing any code
|
|
5
|
+
</required_reading>
|
|
6
|
+
|
|
7
|
+
<process>
|
|
8
|
+
## Step 1: Choose the right signal type
|
|
9
|
+
|
|
10
|
+
Read references/signal-types.md. Match the task to the appropriate signal(s) using the decision guide. If multiple signal types seem applicable, the guide has explicit comparisons.
|
|
11
|
+
|
|
12
|
+
## Step 2: Check ownership requirements
|
|
13
|
+
|
|
14
|
+
Before writing any `createEffect` call, confirm there is an active owner in scope:
|
|
15
|
+
- Top-level effects must be wrapped in `createScope`
|
|
16
|
+
- Effects in Web Component lifecycle methods that use DOM-managed lifetime should use `unown`
|
|
17
|
+
|
|
18
|
+
See references/api-facts.md → `<core_functions>` for the rules.
|
|
19
|
+
|
|
20
|
+
## Step 3: Check for known pitfalls
|
|
21
|
+
|
|
22
|
+
Skim references/non-obvious-behaviors.md for anything that applies to the task:
|
|
23
|
+
|
|
24
|
+
| If the task involves… | Check… |
|
|
25
|
+
|---|---|
|
|
26
|
+
| List or Collection | direct-lookups-do-not-track |
|
|
27
|
+
| Conditional rendering or `match` | conditional-reads-delay-watched |
|
|
28
|
+
| Async data fetching | task-abort-on-dependency-change |
|
|
29
|
+
| Sensor or Task before first value | sensor-unset-before-first-value |
|
|
30
|
+
| Multiple state updates at once | references/api-facts.md → `batch` |
|
|
31
|
+
|
|
32
|
+
## Step 4: Import what you need
|
|
33
|
+
|
|
34
|
+
All public API is imported from the package root:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import {
|
|
38
|
+
createState, createSensor, createMemo, createTask,
|
|
39
|
+
createEffect, createScope, createSlot, createStore,
|
|
40
|
+
createList, createCollection, deriveCollection,
|
|
41
|
+
batch, untrack, unown, match,
|
|
42
|
+
SKIP_EQUALITY,
|
|
43
|
+
} from '@zeix/cause-effect'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Only import what the task requires.
|
|
47
|
+
|
|
48
|
+
## Step 5: Implement
|
|
49
|
+
|
|
50
|
+
Write the code following the patterns in references/. Prefer the examples shown there over inventing new patterns — the non-obvious behaviors exist precisely because intuitive patterns are often wrong.
|
|
51
|
+
|
|
52
|
+
## Step 6: Verify
|
|
53
|
+
|
|
54
|
+
Run the project's own test suite using whatever command applies to this project (e.g. `npm test`, `pnpm test`, `npx vitest`, `deno test`). Do not assume a specific test runner or package manager.
|
|
55
|
+
</process>
|
|
56
|
+
|
|
57
|
+
<success_criteria>
|
|
58
|
+
- Correct signal type(s) chosen for the task
|
|
59
|
+
- No `null` or `undefined` in signal generics (`T extends {}`)
|
|
60
|
+
- Every `createEffect` is inside a `createScope` or another effect
|
|
61
|
+
- Code follows the patterns from references/ rather than inventing new ones
|
|
62
|
+
- Project's own test suite passes (if applicable)
|
|
63
|
+
</success_criteria>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cause-effect-dev
|
|
3
|
+
description: >
|
|
4
|
+
Expert developer for the @zeix/cause-effect reactive signals library. Use when
|
|
5
|
+
implementing features, fixing bugs, writing tests, or answering questions about
|
|
6
|
+
the library's internals, public API, or design decisions.
|
|
7
|
+
user_invocable: false
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<scope>
|
|
11
|
+
This skill is for development work **on the @zeix/cause-effect library itself** — use it only inside the cause-effect repository where `REQUIREMENTS.md`, `ARCHITECTURE.md`, and `src/` are present at the project root.
|
|
12
|
+
|
|
13
|
+
For consumer projects that use `@zeix/cause-effect` as a dependency, use the `cause-effect` skill instead.
|
|
14
|
+
</scope>
|
|
15
|
+
|
|
16
|
+
<essential_principles>
|
|
17
|
+
**Read before writing.** Always read the relevant source file(s) before proposing or making changes.
|
|
18
|
+
|
|
19
|
+
**The signal type set is complete.** Check `REQUIREMENTS.md` before proposing anything new — new signal types are explicitly out of scope.
|
|
20
|
+
|
|
21
|
+
**`T extends {}`** — all signal generics exclude `null` and `undefined`. Use wrapper types or sentinel values to represent absence.
|
|
22
|
+
|
|
23
|
+
**Run `bun test`** after every change.
|
|
24
|
+
</essential_principles>
|
|
25
|
+
|
|
26
|
+
<intake>
|
|
27
|
+
What kind of task is this?
|
|
28
|
+
|
|
29
|
+
1. **Implement** — add or extend functionality
|
|
30
|
+
2. **Fix** — debug or fix unexpected behavior
|
|
31
|
+
3. **Test** — write or update tests
|
|
32
|
+
4. **Question** — understand the API, internals, or a design decision
|
|
33
|
+
|
|
34
|
+
**Wait for response before proceeding.**
|
|
35
|
+
</intake>
|
|
36
|
+
|
|
37
|
+
<routing>
|
|
38
|
+
| Response | Workflow |
|
|
39
|
+
|---|---|
|
|
40
|
+
| 1, "implement", "add", "extend", "build" | workflows/implement-feature.md |
|
|
41
|
+
| 2, "fix", "bug", "debug", "broken", "wrong" | workflows/fix-bug.md |
|
|
42
|
+
| 3, "test", "spec", "coverage" | workflows/write-tests.md |
|
|
43
|
+
| 4, "question", "explain", "how", "why", "what" | workflows/answer-question.md |
|
|
44
|
+
|
|
45
|
+
**Intent-based routing** (if user provides clear context without selecting):
|
|
46
|
+
- Describes a change to make → workflows/implement-feature.md
|
|
47
|
+
- Describes something not working → workflows/fix-bug.md
|
|
48
|
+
- Asks to write/update tests → workflows/write-tests.md
|
|
49
|
+
- Asks how something works → workflows/answer-question.md
|
|
50
|
+
|
|
51
|
+
**After identifying the workflow, read it and follow it exactly.**
|
|
52
|
+
</routing>
|
|
53
|
+
|
|
54
|
+
<reference_index>
|
|
55
|
+
All in `references/`:
|
|
56
|
+
|
|
57
|
+
| File | Contents |
|
|
58
|
+
|---|---|
|
|
59
|
+
| source-map.md | Authoritative documents + signal source file locations |
|
|
60
|
+
| internal-types.md | Node shapes and global pointers |
|
|
61
|
+
| api-facts.md | Key API constraints and callback patterns |
|
|
62
|
+
| non-obvious-behaviors.md | Counterintuitive behaviors with examples |
|
|
63
|
+
| error-classes.md | Error classes and when they are thrown |
|
|
64
|
+
</reference_index>
|
|
65
|
+
|
|
66
|
+
<workflows_index>
|
|
67
|
+
All in `workflows/`:
|
|
68
|
+
|
|
69
|
+
| Workflow | Purpose |
|
|
70
|
+
|---|---|
|
|
71
|
+
| implement-feature.md | Add or extend library functionality |
|
|
72
|
+
| fix-bug.md | Diagnose and fix unexpected behavior |
|
|
73
|
+
| write-tests.md | Write or update tests for a signal type or behavior |
|
|
74
|
+
| answer-question.md | Answer questions about the API, internals, or design |
|
|
75
|
+
</workflows_index>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<overview>
|
|
2
|
+
Key API constraints, defaults, and callback patterns for @zeix/cause-effect. Read this when writing or reviewing any code that uses the public API.
|
|
3
|
+
</overview>
|
|
4
|
+
|
|
5
|
+
<type_constraint>
|
|
6
|
+
**`T extends {}`** — all signal generics exclude `null` and `undefined` at the type level. This is intentional. Use wrapper types or sentinel values to represent absence:
|
|
7
|
+
|
|
8
|
+
```typescript
|
|
9
|
+
// Wrong — TypeScript will reject this
|
|
10
|
+
const count = createState<number | null>(null)
|
|
11
|
+
|
|
12
|
+
// Correct — use a sentinel or wrapper
|
|
13
|
+
const count = createState<number>(0)
|
|
14
|
+
const value = createState<{ data: string } | never>({ data: '' })
|
|
15
|
+
```
|
|
16
|
+
</type_constraint>
|
|
17
|
+
|
|
18
|
+
<core_functions>
|
|
19
|
+
**`createScope(fn)`**
|
|
20
|
+
- Returns a single `Cleanup` function
|
|
21
|
+
- `fn` receives no arguments
|
|
22
|
+
- `fn` may return an optional cleanup that runs when the scope is disposed
|
|
23
|
+
- Used to group effects and control their lifetime
|
|
24
|
+
|
|
25
|
+
**`createEffect(fn)`**
|
|
26
|
+
- Returns a `Cleanup` function
|
|
27
|
+
- **Must be called inside an owner** (another effect or a scope) — throws `RequiredOwnerError` otherwise
|
|
28
|
+
- `fn` runs immediately and re-runs whenever its tracked dependencies change
|
|
29
|
+
- Registers cleanup with the current `activeOwner`
|
|
30
|
+
|
|
31
|
+
**`batch(fn)`**
|
|
32
|
+
- Defers the reactive flush until `fn` returns
|
|
33
|
+
- Multiple state writes inside `fn` coalesce into a single propagation pass
|
|
34
|
+
- Use when updating several signals that feed the same downstream computation
|
|
35
|
+
|
|
36
|
+
**`untrack(fn)`**
|
|
37
|
+
- Runs `fn` without recording dependency edges (nulls `activeSink`)
|
|
38
|
+
- Reads inside `fn` do not subscribe the current computation to those signals
|
|
39
|
+
- Use to read a signal's current value without creating a reactive dependency
|
|
40
|
+
|
|
41
|
+
**`unown(fn)`**
|
|
42
|
+
- Runs `fn` without registering cleanups in the current owner (nulls `activeOwner`)
|
|
43
|
+
- Use in `connectedCallback` and similar DOM lifecycle hooks where the DOM — not the reactive graph — owns the element's lifetime
|
|
44
|
+
</core_functions>
|
|
45
|
+
|
|
46
|
+
<options>
|
|
47
|
+
**`equals`**
|
|
48
|
+
- Available on `createState`, `createSensor`, `createMemo`, `createTask`
|
|
49
|
+
- Default: strict equality (`===`)
|
|
50
|
+
- When a new value is `equals` to the previous, propagation stops — downstream nodes are not re-run
|
|
51
|
+
- **`SKIP_EQUALITY`** — special sentinel for `equals`; forces propagation on every update regardless of value. Use with mutable-reference sensors where the reference never changes but the contents do
|
|
52
|
+
|
|
53
|
+
**`guard`**
|
|
54
|
+
- Available on `createState`, `createSensor`
|
|
55
|
+
- A predicate `(value: unknown) => value is T`
|
|
56
|
+
- Throws `InvalidSignalValueError` if a set value fails the guard
|
|
57
|
+
- Use to enforce runtime type safety at signal boundaries
|
|
58
|
+
</options>
|
|
59
|
+
|
|
60
|
+
<callback_patterns>
|
|
61
|
+
**Memo and Task callbacks receive `prev`**
|
|
62
|
+
- Signature: `(prev: T) => T` for Memo; `(prev: T, signal: AbortSignal) => Promise<T>` for Task
|
|
63
|
+
- `prev` is the previous value on every run after the first
|
|
64
|
+
- Enables reducer patterns without external state:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
const count = createState(0)
|
|
68
|
+
const doubled = createMemo((prev) => {
|
|
69
|
+
const next = count.get() * 2
|
|
70
|
+
return next === prev ? prev : next // referential stability
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**`Slot` is a property descriptor**
|
|
75
|
+
- Has `get`, `set`, `configurable`, `enumerable` fields
|
|
76
|
+
- Can be passed directly to `Object.defineProperty()`:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
const slot = createSlot(store, 'name')
|
|
80
|
+
Object.defineProperty(element, 'name', slot)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**`Task` carries an `AbortSignal`**
|
|
84
|
+
- The second argument to the Task callback is an `AbortSignal`
|
|
85
|
+
- The signal is aborted when the Task's dependencies change before the previous async operation completes
|
|
86
|
+
- Always pass it to any `fetch` or cancellable async operation inside a Task
|
|
87
|
+
</callback_patterns>
|
|
88
|
+
|
|
89
|
+
<lifecycle_summary>
|
|
90
|
+
| Function | Must be in owner? | Returns | Re-runs on dependency change? |
|
|
91
|
+
|---|---|---|---|
|
|
92
|
+
| `createScope(fn)` | No | `Cleanup` | No (fn runs once) |
|
|
93
|
+
| `createEffect(fn)` | **Yes** | `Cleanup` | Yes |
|
|
94
|
+
| `createMemo(fn)` | No | `Memo<T>` | Lazily (on read) |
|
|
95
|
+
| `createTask(fn)` | No | `Task<T>` | Yes (async) |
|
|
96
|
+
</lifecycle_summary>
|