@zeix/cause-effect 1.0.0 → 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 +1 -1
- package/CHANGELOG.md +15 -0
- package/README.md +41 -0
- package/REQUIREMENTS.md +3 -3
- package/eslint.config.js +2 -1
- package/package.json +5 -4
- 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 +61 -100
- 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 +2 -0
- package/src/nodes/collection.ts +38 -0
- package/src/nodes/effect.ts +13 -1
- package/src/nodes/list.ts +23 -2
- 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/tsconfig.json +9 -0
|
@@ -10,7 +10,7 @@ Cause & Effect is a reactive state management library for JavaScript/TypeScript
|
|
|
10
10
|
- **Nodes**: StateNode (source + equality), MemoNode (source + sink), TaskNode (source + sink + async), EffectNode (sink + owner)
|
|
11
11
|
- **Edges**: Doubly-linked list connecting sources to sinks
|
|
12
12
|
- **Operations**: `link()` creates edges, `propagate()` flags sinks dirty, `flush()` runs queued effects, `batch()` defers flushing
|
|
13
|
-
- **Flags**: FLAG_CLEAN, FLAG_CHECK, FLAG_DIRTY, FLAG_RUNNING for efficient dirty checking
|
|
13
|
+
- **Flags**: FLAG_CLEAN, FLAG_CHECK, FLAG_DIRTY, FLAG_RUNNING, FLAG_RELINK for efficient dirty checking
|
|
14
14
|
|
|
15
15
|
### Signal Types (all in `src/nodes/`)
|
|
16
16
|
- **State** (`createState`): Mutable signals for values (`get`, `set`, `update`)
|
|
@@ -36,6 +36,7 @@ Cause & Effect is a reactive state management library for JavaScript/TypeScript
|
|
|
36
36
|
- `src/nodes/list.ts` - createList, isList, List type
|
|
37
37
|
- `src/nodes/collection.ts` - createCollection, isCollection, Collection type, deriveCollection (internal)
|
|
38
38
|
- `src/nodes/slot.ts` - createSlot, isSlot, Slot type
|
|
39
|
+
- `src/signal.ts` - Polymorphic factories (createSignal, createMutableSignal, createComputed) and type predicates (isSignal, isMutableSignal, isComputed)
|
|
39
40
|
- `src/util.ts` - Utility functions and type checks
|
|
40
41
|
- `index.ts` - Entry point / main export file
|
|
41
42
|
|
package/.zed/settings.json
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
2
|
+
"project_name": "Cause & Effect",
|
|
3
|
+
"languages": {
|
|
4
|
+
"TypeScript": {
|
|
5
|
+
"language_servers": ["!eslint", "..."],
|
|
6
|
+
"code_actions_on_format": {
|
|
7
|
+
"source.fixAll.biome": true,
|
|
8
|
+
"source.organizeImports.biome": true,
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
"JavaScript": {
|
|
12
|
+
"language_servers": ["!eslint", "..."],
|
|
13
|
+
"code_actions_on_format": {
|
|
14
|
+
"source.fixAll.biome": true,
|
|
15
|
+
"source.organizeImports.biome": true,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
"TSX": {
|
|
19
|
+
"language_servers": ["!eslint", "..."],
|
|
20
|
+
"code_actions_on_format": {
|
|
21
|
+
"source.fixAll.biome": true,
|
|
22
|
+
"source.organizeImports.biome": true,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
3
26
|
}
|
package/ARCHITECTURE.md
CHANGED
|
@@ -178,7 +178,7 @@ Creates an ownership scope without an effect. The scope becomes `activeOwner` du
|
|
|
178
178
|
|
|
179
179
|
### State (`src/nodes/state.ts`)
|
|
180
180
|
|
|
181
|
-
**Graph node**: `
|
|
181
|
+
**Graph node**: `StateNode<T>` (source only)
|
|
182
182
|
|
|
183
183
|
A mutable value container. The simplest signal type — `get()` links and returns the value, `set()` validates, calls `setState()`, which propagates changes to dependents.
|
|
184
184
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.1
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`cause-effect` skill for consumer projects**: New Claude Code skill with self-contained API knowledge in `references/` — no library source access required. Covers three workflows: `use-api`, `debug`, and `answer-question`.
|
|
8
|
+
- **`README.md` Utilities section**: Documents the previously undocumented `createSignal`, `createMutableSignal`, `createComputed` factories and `isSignal`, `isMutableSignal`, `isComputed` predicates exported from `index.ts`.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **`cause-effect-dev` skill restructured**: Refactored to progressive disclosure pattern with separate `workflows/` and `references/` modules. Scoped explicitly to library development; external references to `REQUIREMENTS.md`, `ARCHITECTURE.md`, and `src/` are now clearly library-repo-only.
|
|
13
|
+
- **Documentation alignment**: Corrected wrong graph node type for `State` in `ARCHITECTURE.md`; added missing `FLAG_RELINK` and `src/signal.ts` to `copilot-instructions.md`; updated `REQUIREMENTS.md` stability section to reflect 1.0 release; completed and corrected JSDoc across `Sensor`, `Memo`, `Store`, `List`, `Collection`, and utility types. No runtime behaviour changed.
|
|
14
|
+
- **TypeScript 6 compatibility**: Added `erasableSyntaxOnly` to `tsconfig.json` (requires TS ≥5.8); replaced `@types/bun` with `bun-types` directly and added `"types": ["bun-types"]` to `tsconfig.json` to fix module resolution under TypeScript 6.
|
|
15
|
+
- **Package management cleanup**: Added `typescript` to `devDependencies` (was only in `peerDependencies`, causing stale version installs); updated `peerDependencies` range to `>=5.8.0`; removed `package-lock.json` and gitignored npm/yarn/pnpm lockfiles — Bun is required for development.
|
|
16
|
+
- **Zed editor configuration**: Disabled ESLint language server for JS/TS/TSX in `.zed/settings.json` — project uses Biome for linting.
|
|
17
|
+
|
|
3
18
|
## 1.0.0
|
|
4
19
|
|
|
5
20
|
### Changed
|
package/README.md
CHANGED
|
@@ -399,6 +399,47 @@ createEffect(() => {
|
|
|
399
399
|
})
|
|
400
400
|
```
|
|
401
401
|
|
|
402
|
+
### Utilities
|
|
403
|
+
|
|
404
|
+
Polymorphic factories and type predicates for generic and library-author code.
|
|
405
|
+
|
|
406
|
+
**`createSignal(value)`** converts any value to its corresponding signal type:
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
import { createSignal } from '@zeix/cause-effect'
|
|
410
|
+
|
|
411
|
+
createSignal(0) // → State<number>
|
|
412
|
+
createSignal([1, 2, 3]) // → List<number>
|
|
413
|
+
createSignal({ x: 0 }) // → Store<{ x: number }>
|
|
414
|
+
createSignal(() => x.get() * 2) // → Memo<number>
|
|
415
|
+
createSignal(async (_, s) =>
|
|
416
|
+
fetch('/api', { signal: s }).then(r => r.json())) // → Task<Response>
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
If the value is already a signal, it is returned unchanged.
|
|
420
|
+
|
|
421
|
+
**`createMutableSignal(value)`** is the same, but restricted to mutable signals — returns `State`, `Store`, or `List`. Throws `InvalidSignalValueError` if passed a function or a read-only signal.
|
|
422
|
+
|
|
423
|
+
**`createComputed(callback, options?)`** creates a `Memo` or `Task` by detecting whether the callback is async:
|
|
424
|
+
|
|
425
|
+
```ts
|
|
426
|
+
import { createComputed } from '@zeix/cause-effect'
|
|
427
|
+
|
|
428
|
+
const doubled = createComputed(() => count.get() * 2)
|
|
429
|
+
const data = createComputed(async (_, signal) =>
|
|
430
|
+
fetch(url.get(), { signal }).then(r => r.json()))
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
**Type predicates**
|
|
434
|
+
|
|
435
|
+
| Predicate | True for |
|
|
436
|
+
|---|---|
|
|
437
|
+
| `isSignal(value)` | Any signal (all 9 types) |
|
|
438
|
+
| `isMutableSignal(value)` | `State`, `Store`, `List` — signals with `.set()` and `.update()` |
|
|
439
|
+
| `isComputed(value)` | `Memo`, `Task` — derived signals |
|
|
440
|
+
|
|
441
|
+
The `MutableSignal<T>` type is the corresponding TypeScript type for `isMutableSignal` — use it as a parameter type in generic code that accepts any writable signal.
|
|
442
|
+
|
|
402
443
|
## Choosing the Right Signal
|
|
403
444
|
|
|
404
445
|
```
|
package/REQUIREMENTS.md
CHANGED
|
@@ -84,11 +84,11 @@ The following are explicitly out of scope and will not be added to the library:
|
|
|
84
84
|
|
|
85
85
|
## Stability
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
The library is stable at 1.0.0. The API surface — how signals are created and consumed — will not change except under the following conditions:
|
|
88
88
|
|
|
89
|
-
- **Breaking changes**
|
|
89
|
+
- **Breaking changes** only if major new features of the Web Platform shift the optimal way to achieve the goals this library already covers.
|
|
90
90
|
- **New features** are not expected. The signal type set is complete.
|
|
91
|
-
- **Backward compatibility**
|
|
91
|
+
- **Backward compatibility** is maintained from 1.0 onward.
|
|
92
92
|
|
|
93
93
|
## Success Criteria
|
|
94
94
|
|
package/eslint.config.js
CHANGED
|
@@ -4,8 +4,9 @@ import tseslint from 'typescript-eslint'
|
|
|
4
4
|
|
|
5
5
|
/** @type {import('eslint').Linter.Config[]} */
|
|
6
6
|
export default [
|
|
7
|
+
// Global ignores to prevent warnings about these files
|
|
7
8
|
{
|
|
8
|
-
ignores: ['index.js', '**/*.min.js'],
|
|
9
|
+
ignores: ['index.js', 'index.dev.js', 'types/**/*.d.ts', '**/*.min.js'],
|
|
9
10
|
},
|
|
10
11
|
{
|
|
11
12
|
files: ['**/*.{js,mjs,cjs,ts}'],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeix/cause-effect",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"author": "Esther Brunner",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
"types": "types/index.d.ts",
|
|
9
9
|
"devDependencies": {
|
|
10
10
|
"@biomejs/biome": "2.4.6",
|
|
11
|
-
"
|
|
11
|
+
"bun-types": "latest",
|
|
12
12
|
"mitata": "^1.0.34",
|
|
13
|
-
"random": "^5.4.1"
|
|
13
|
+
"random": "^5.4.1",
|
|
14
|
+
"typescript": "latest"
|
|
14
15
|
},
|
|
15
16
|
"peerDependencies": {
|
|
16
|
-
"typescript": "
|
|
17
|
+
"typescript": ">=5.8.0"
|
|
17
18
|
},
|
|
18
19
|
"description": "Cause & Effect - reactive state management primitives library for TypeScript.",
|
|
19
20
|
"license": "MIT",
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cause-effect
|
|
3
|
+
description: >
|
|
4
|
+
Expert guidance for using the @zeix/cause-effect reactive signals library in any project.
|
|
5
|
+
Use when implementing reactive patterns, debugging unexpected behavior, or answering
|
|
6
|
+
questions about the public API, signal types, or design decisions. Works in any project
|
|
7
|
+
that depends on @zeix/cause-effect — all knowledge is embedded, no library source required.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<scope>
|
|
11
|
+
This skill is for **consumer projects** that use `@zeix/cause-effect` as a dependency.
|
|
12
|
+
All domain knowledge is embedded in `references/` — no library source files are required.
|
|
13
|
+
|
|
14
|
+
For development work on the library itself, use the `cause-effect-dev` skill instead.
|
|
15
|
+
</scope>
|
|
16
|
+
|
|
17
|
+
<essential_principles>
|
|
18
|
+
**`T extends {}`** — all signal generics exclude `null` and `undefined`. Use wrapper types or sentinel values to represent absence.
|
|
19
|
+
|
|
20
|
+
**`createEffect` must be inside an owner.** Always wrap effect creation in `createScope` or nest it inside another effect.
|
|
21
|
+
|
|
22
|
+
**Never guess API shapes or behaviors.** The answers are in `references/`. If the embedded knowledge is insufficient, check the library's README or GUIDE — do not invent behavior.
|
|
23
|
+
</essential_principles>
|
|
24
|
+
|
|
25
|
+
<intake>
|
|
26
|
+
What kind of task is this?
|
|
27
|
+
|
|
28
|
+
1. **Use** — implement reactive patterns using the library's public API
|
|
29
|
+
2. **Debug** — investigate unexpected or broken reactive behavior
|
|
30
|
+
3. **Question** — understand the API, which signal to use, or a design decision
|
|
31
|
+
|
|
32
|
+
**Wait for response before proceeding.**
|
|
33
|
+
</intake>
|
|
34
|
+
|
|
35
|
+
<routing>
|
|
36
|
+
| Response | Workflow |
|
|
37
|
+
|---|---|
|
|
38
|
+
| 1, "use", "implement", "add", "build", "write" | workflows/use-api.md |
|
|
39
|
+
| 2, "debug", "fix", "broken", "not working", "wrong", "unexpected" | workflows/debug.md |
|
|
40
|
+
| 3, "question", "explain", "how", "why", "what", "which", "when" | workflows/answer-question.md |
|
|
41
|
+
|
|
42
|
+
**Intent-based routing** (if user provides clear context without selecting):
|
|
43
|
+
- Describes code to write or a feature to add → workflows/use-api.md
|
|
44
|
+
- Describes something not working as expected → workflows/debug.md
|
|
45
|
+
- Asks how something works or which signal to use → workflows/answer-question.md
|
|
46
|
+
|
|
47
|
+
**After identifying the workflow, read it and follow it exactly.**
|
|
48
|
+
</routing>
|
|
49
|
+
|
|
50
|
+
<reference_index>
|
|
51
|
+
All in `references/` — all knowledge is self-contained, no external files required:
|
|
52
|
+
|
|
53
|
+
| File | Contents |
|
|
54
|
+
|---|---|
|
|
55
|
+
| signal-types.md | What each signal is for, when to use each, decision guide |
|
|
56
|
+
| api-facts.md | Key API constraints, core functions, options, callback patterns |
|
|
57
|
+
| non-obvious-behaviors.md | Counterintuitive behaviors with correct vs incorrect examples |
|
|
58
|
+
| error-classes.md | Error classes, trigger conditions, and how to handle them |
|
|
59
|
+
</reference_index>
|
|
60
|
+
|
|
61
|
+
<workflows_index>
|
|
62
|
+
All in `workflows/`:
|
|
63
|
+
|
|
64
|
+
| Workflow | Purpose |
|
|
65
|
+
|---|---|
|
|
66
|
+
| use-api.md | Implement reactive patterns using the public API |
|
|
67
|
+
| debug.md | Diagnose and fix unexpected reactive behavior |
|
|
68
|
+
| answer-question.md | Answer questions about the API, signals, or design |
|
|
69
|
+
</workflows_index>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "Cause & Effect"
|
|
3
|
+
short_description: "Expert guidance for using the @zeix/cause-effect reactive signals library in your project"
|
|
4
|
+
default_prompt: "Use $cause-effect to implement reactive patterns, debug unexpected behavior, or answer questions about the @zeix/cause-effect public API."
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
<overview>
|
|
2
|
+
Key API constraints, defaults, and callback patterns for @zeix/cause-effect. All knowledge is
|
|
3
|
+
self-contained — no library source files required. Read this when writing or reviewing any
|
|
4
|
+
code that uses the public API.
|
|
5
|
+
</overview>
|
|
6
|
+
|
|
7
|
+
<type_constraint>
|
|
8
|
+
**`T extends {}`** — all signal generics exclude `null` and `undefined` at the type level.
|
|
9
|
+
This is intentional: signals always have a value; absence must be modelled explicitly.
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
// Wrong — TypeScript will reject this
|
|
13
|
+
const count = createState<number | null>(null)
|
|
14
|
+
|
|
15
|
+
// Correct — use a sentinel or a wrapper type
|
|
16
|
+
const count = createState<number>(0)
|
|
17
|
+
const selected = createState<{ id: string } | { id: never }>({ id: '' })
|
|
18
|
+
```
|
|
19
|
+
</type_constraint>
|
|
20
|
+
|
|
21
|
+
<core_functions>
|
|
22
|
+
**`createScope(fn)`**
|
|
23
|
+
- Returns a single `Cleanup` function
|
|
24
|
+
- `fn` receives no arguments and may return an optional cleanup
|
|
25
|
+
- Use to group effects and control their shared lifetime
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
const dispose = createScope(() => {
|
|
29
|
+
createEffect(() => console.log(count.get()))
|
|
30
|
+
// all effects inside are disposed when dispose() is called
|
|
31
|
+
})
|
|
32
|
+
dispose() // cleans up everything inside
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**`createEffect(fn)`**
|
|
36
|
+
- Returns a `Cleanup` function
|
|
37
|
+
- **Must be called inside an owner** (a `createScope` callback or another `createEffect` callback)
|
|
38
|
+
- Throws `RequiredOwnerError` if called without an active owner
|
|
39
|
+
- Runs `fn` immediately, then re-runs whenever tracked dependencies change
|
|
40
|
+
|
|
41
|
+
**`batch(fn)`**
|
|
42
|
+
- Defers the reactive flush until `fn` returns
|
|
43
|
+
- Multiple state writes inside `fn` coalesce into a single propagation pass
|
|
44
|
+
- Use when updating several signals that feed the same downstream computation
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
batch(() => {
|
|
48
|
+
x.set(1)
|
|
49
|
+
y.set(2)
|
|
50
|
+
z.set(3)
|
|
51
|
+
// only one propagation pass runs after all three writes
|
|
52
|
+
})
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**`untrack(fn)`**
|
|
56
|
+
- Runs `fn` without recording dependency edges
|
|
57
|
+
- Reads inside `fn` do not subscribe the current computation to those signals
|
|
58
|
+
- Use to read a signal's current value without creating a reactive dependency
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
createEffect(() => {
|
|
62
|
+
const a = reactive.get() // tracked — effect re-runs when reactive changes
|
|
63
|
+
const b = untrack(() => other.get()) // untracked — no dependency on other
|
|
64
|
+
render(a, b)
|
|
65
|
+
})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**`unown(fn)`**
|
|
69
|
+
- Runs `fn` without registering cleanups in the current owner
|
|
70
|
+
- Use in `connectedCallback` and similar DOM lifecycle methods where the DOM —
|
|
71
|
+
not the reactive graph — manages the element's lifetime
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
connectedCallback() {
|
|
75
|
+
// cleanup is tied to disconnectedCallback, not to a reactive owner
|
|
76
|
+
this.#cleanup = unown(() => createEffect(() => this.render()))
|
|
77
|
+
}
|
|
78
|
+
disconnectedCallback() {
|
|
79
|
+
this.#cleanup?.()
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
</core_functions>
|
|
83
|
+
|
|
84
|
+
<options>
|
|
85
|
+
**`equals`**
|
|
86
|
+
- Available on `createState`, `createSensor`, `createMemo`, `createTask`
|
|
87
|
+
- Default: strict equality (`===`)
|
|
88
|
+
- When a new value is considered equal to the previous one, propagation stops —
|
|
89
|
+
downstream nodes are not re-run
|
|
90
|
+
- **`SKIP_EQUALITY`** — special sentinel value for `equals`; forces propagation on every
|
|
91
|
+
update regardless of value. Use with mutable-reference sensors where the object
|
|
92
|
+
reference never changes but the contents do:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { createSensor, SKIP_EQUALITY } from '@zeix/cause-effect'
|
|
96
|
+
|
|
97
|
+
const mouse = createSensor<{ x: number; y: number }>(
|
|
98
|
+
set => {
|
|
99
|
+
const handler = (e: MouseEvent) => set({ x: e.clientX, y: e.clientY })
|
|
100
|
+
window.addEventListener('mousemove', handler)
|
|
101
|
+
return () => window.removeEventListener('mousemove', handler)
|
|
102
|
+
},
|
|
103
|
+
{ equals: SKIP_EQUALITY } // new object every time, so skip reference equality
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**`guard`**
|
|
108
|
+
- Available on `createState`, `createSensor`
|
|
109
|
+
- A predicate `(value: unknown) => value is T`
|
|
110
|
+
- Throws `InvalidSignalValueError` if a set value fails the predicate
|
|
111
|
+
- Use to enforce runtime type safety at signal boundaries
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
const age = createState(0, {
|
|
115
|
+
guard: (v): v is number => typeof v === 'number' && v >= 0,
|
|
116
|
+
})
|
|
117
|
+
```
|
|
118
|
+
</options>
|
|
119
|
+
|
|
120
|
+
<callback_patterns>
|
|
121
|
+
**Memo and Task callbacks receive `prev`**
|
|
122
|
+
- Signature: `(prev: T) => T` for Memo; `(prev: T, signal: AbortSignal) => Promise<T>` for Task
|
|
123
|
+
- `prev` is the previous computed value, enabling reducer-style patterns without external state:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
const runningTotal = createMemo((prev: number) => prev + newValue.get())
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Task carries an `AbortSignal`**
|
|
130
|
+
- The second argument to the Task callback is an `AbortSignal`
|
|
131
|
+
- The signal is aborted when dependencies change before the previous async run completes
|
|
132
|
+
- Always forward it to any `fetch` or cancellable async operation:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
const results = createTask(async (prev, signal) => {
|
|
136
|
+
const res = await fetch(`/api/search?q=${query.get()}`, { signal })
|
|
137
|
+
return res.json()
|
|
138
|
+
})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**`Slot` is a property descriptor**
|
|
142
|
+
- Has `get`, `set`, `configurable`, `enumerable` fields
|
|
143
|
+
- Can be passed directly to `Object.defineProperty()`:
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
const nameSlot = createSlot(store, 'name')
|
|
147
|
+
Object.defineProperty(element, 'name', nameSlot)
|
|
148
|
+
```
|
|
149
|
+
</callback_patterns>
|
|
150
|
+
|
|
151
|
+
<match_helper>
|
|
152
|
+
`match` reads one or more Sensor/Task signals and routes to `ok` or `nil` based on whether
|
|
153
|
+
all signals have a value. Use it to safely handle the unset state without try/catch:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { match } from '@zeix/cause-effect'
|
|
157
|
+
|
|
158
|
+
createEffect(() => {
|
|
159
|
+
match([task, sensor], {
|
|
160
|
+
ok: ([taskResult, sensorValue]) => render(taskResult, sensorValue),
|
|
161
|
+
nil: () => showSpinner(),
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Read signals you care about eagerly inside `match`'s array — not inside individual branches.
|
|
167
|
+
See `non-obvious-behaviors.md → conditional-reads-delay-watched` for why.
|
|
168
|
+
</match_helper>
|
|
169
|
+
|
|
170
|
+
<lifecycle_summary>
|
|
171
|
+
| Function | Requires owner? | Returns | Reactive? |
|
|
172
|
+
|---|---|---|---|
|
|
173
|
+
| `createScope(fn)` | No | `Cleanup` | No (fn runs once) |
|
|
174
|
+
| `createEffect(fn)` | **Yes** | `Cleanup` | Yes — re-runs on dependency change |
|
|
175
|
+
| `createMemo(fn)` | No | `Memo<T>` | Lazy — recomputes on read if stale |
|
|
176
|
+
| `createTask(fn)` | No | `Task<T>` | Yes — re-runs async on dependency change |
|
|
177
|
+
| `createState(value)` | No | `State<T>` | Source — never recomputes |
|
|
178
|
+
| `createSensor(setup)` | No | `Sensor<T>` | Source — set by external callback |
|
|
179
|
+
</lifecycle_summary>
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<overview>
|
|
2
|
+
Error classes thrown by @zeix/cause-effect and the conditions that trigger them. All knowledge
|
|
3
|
+
is self-contained — no library source files required. Read this when writing error-handling
|
|
4
|
+
code, testing error conditions, or diagnosing an unexpected throw.
|
|
5
|
+
</overview>
|
|
6
|
+
|
|
7
|
+
<import>
|
|
8
|
+
All error classes are exported from the package root:
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
import {
|
|
12
|
+
NullishSignalValueError,
|
|
13
|
+
InvalidSignalValueError,
|
|
14
|
+
InvalidCallbackError,
|
|
15
|
+
DuplicateKeyError,
|
|
16
|
+
UnsetSignalValueError,
|
|
17
|
+
ReadonlySignalError,
|
|
18
|
+
RequiredOwnerError,
|
|
19
|
+
CircularDependencyError,
|
|
20
|
+
} from '@zeix/cause-effect'
|
|
21
|
+
```
|
|
22
|
+
</import>
|
|
23
|
+
|
|
24
|
+
<error_table>
|
|
25
|
+
| Class | When thrown |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `NullishSignalValueError` | Signal value is `null` or `undefined` |
|
|
28
|
+
| `InvalidSignalValueError` | Value fails the `guard` predicate |
|
|
29
|
+
| `InvalidCallbackError` | A required callback argument is not a function |
|
|
30
|
+
| `DuplicateKeyError` | List/Collection key collision on insert |
|
|
31
|
+
| `UnsetSignalValueError` | Reading a Sensor or Task before it has produced its first value |
|
|
32
|
+
| `ReadonlySignalError` | Attempting to write to a read-only signal |
|
|
33
|
+
| `RequiredOwnerError` | `createEffect` called outside an owner (scope or parent effect) |
|
|
34
|
+
| `CircularDependencyError` | A cycle is detected in the reactive graph |
|
|
35
|
+
</error_table>
|
|
36
|
+
|
|
37
|
+
<error_details>
|
|
38
|
+
|
|
39
|
+
<NullishSignalValueError>
|
|
40
|
+
Thrown when a signal's value is `null` or `undefined`. Because all signal generics use
|
|
41
|
+
`T extends {}`, nullish values are excluded by design — this error surfaces the constraint
|
|
42
|
+
at runtime if type safety is bypassed (e.g. via a type assertion or untyped interop).
|
|
43
|
+
|
|
44
|
+
**Prevention:** model absence explicitly with a sentinel value or wrapper type instead of `null`.
|
|
45
|
+
</NullishSignalValueError>
|
|
46
|
+
|
|
47
|
+
<InvalidSignalValueError>
|
|
48
|
+
Thrown when a value passed to `.set()` fails the `guard` predicate supplied in the signal's
|
|
49
|
+
options. This is the runtime enforcement of custom type narrowing at signal boundaries.
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { createState, InvalidSignalValueError } from '@zeix/cause-effect'
|
|
53
|
+
|
|
54
|
+
const age = createState(0, {
|
|
55
|
+
guard: (v): v is number => typeof v === 'number' && v >= 0,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
age.set(-1) // throws InvalidSignalValueError
|
|
59
|
+
```
|
|
60
|
+
</InvalidSignalValueError>
|
|
61
|
+
|
|
62
|
+
<InvalidCallbackError>
|
|
63
|
+
Thrown when a required callback argument — such as the computation function passed to
|
|
64
|
+
`createMemo`, `createTask`, or `createEffect` — is not a function. Catches programming
|
|
65
|
+
errors like passing `undefined` or a non-function value by mistake.
|
|
66
|
+
</InvalidCallbackError>
|
|
67
|
+
|
|
68
|
+
<DuplicateKeyError>
|
|
69
|
+
Thrown when inserting an item into a List or Collection whose key already exists. Keys must
|
|
70
|
+
be unique within a given List or Collection.
|
|
71
|
+
|
|
72
|
+
**Fix:** use the collection's update or set method to change an existing entry rather than
|
|
73
|
+
inserting a new one with the same key.
|
|
74
|
+
</DuplicateKeyError>
|
|
75
|
+
|
|
76
|
+
<UnsetSignalValueError>
|
|
77
|
+
Thrown when `.get()` is called on a Sensor or Task before it has emitted its first value.
|
|
78
|
+
Unlike State, Sensor and Task start in an explicitly unset state with no initial value.
|
|
79
|
+
|
|
80
|
+
**Fix:** use `match` to handle the unset state (`nil` branch) instead of calling `.get()`
|
|
81
|
+
directly:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { match } from '@zeix/cause-effect'
|
|
85
|
+
|
|
86
|
+
createEffect(() => {
|
|
87
|
+
match([sensor, task], {
|
|
88
|
+
ok: ([s, t]) => render(s, t),
|
|
89
|
+
nil: () => showSpinner(),
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
```
|
|
93
|
+
</UnsetSignalValueError>
|
|
94
|
+
|
|
95
|
+
<ReadonlySignalError>
|
|
96
|
+
Thrown when code attempts to call `.set()` on a read-only signal. Derived signals (Memo,
|
|
97
|
+
Task) are inherently read-only. Certain factory options may also produce read-only State
|
|
98
|
+
or Sensor instances.
|
|
99
|
+
|
|
100
|
+
**Fix:** only write to signals you own (State, Sensor via the internal setter callback).
|
|
101
|
+
</ReadonlySignalError>
|
|
102
|
+
|
|
103
|
+
<RequiredOwnerError>
|
|
104
|
+
Thrown when `createEffect` is called without an active owner in the current execution context.
|
|
105
|
+
Effects must be created inside a `createScope` callback or inside another `createEffect`
|
|
106
|
+
callback so their cleanup can be registered and managed.
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { createEffect, createScope } from '@zeix/cause-effect'
|
|
110
|
+
|
|
111
|
+
// Wrong — no active owner
|
|
112
|
+
createEffect(() => console.log('runs')) // throws RequiredOwnerError
|
|
113
|
+
|
|
114
|
+
// Correct — wrapped in a scope
|
|
115
|
+
const dispose = createScope(() => {
|
|
116
|
+
createEffect(() => console.log('runs'))
|
|
117
|
+
})
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Exception:** use `unown` when the DOM manages the element's lifetime (e.g. inside
|
|
121
|
+
`connectedCallback`/`disconnectedCallback`) and you intentionally want to bypass owner
|
|
122
|
+
registration.
|
|
123
|
+
</RequiredOwnerError>
|
|
124
|
+
|
|
125
|
+
<CircularDependencyError>
|
|
126
|
+
Thrown when the graph engine detects a cycle during propagation — a signal that, directly
|
|
127
|
+
or transitively, depends on itself. Cycles make it impossible to determine a stable
|
|
128
|
+
evaluation order and are always a programming error.
|
|
129
|
+
|
|
130
|
+
**Common causes:**
|
|
131
|
+
- A Memo or Task that writes to a State it also reads
|
|
132
|
+
- Two Memos that read each other
|
|
133
|
+
|
|
134
|
+
**Fix:** restructure the data flow so that values move in one direction only.
|
|
135
|
+
</CircularDependencyError>
|
|
136
|
+
|
|
137
|
+
</error_details>
|
|
138
|
+
|
|
139
|
+
<testing_error_conditions>
|
|
140
|
+
Use `expect(() => ...).toThrow(ErrorClass)` to assert that a specific error is thrown.
|
|
141
|
+
Import the error class from the package root:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { createState, InvalidSignalValueError } from '@zeix/cause-effect'
|
|
145
|
+
|
|
146
|
+
test('rejects negative age', () => {
|
|
147
|
+
const age = createState(0, {
|
|
148
|
+
guard: (v): v is number => typeof v === 'number' && v >= 0,
|
|
149
|
+
})
|
|
150
|
+
expect(() => age.set(-1)).toThrow(InvalidSignalValueError)
|
|
151
|
+
})
|
|
152
|
+
```
|
|
153
|
+
</testing_error_conditions>
|