@zeix/cause-effect 0.18.4 → 1.0.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.
@@ -0,0 +1,179 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ createEffect,
4
+ createScope,
5
+ createState,
6
+ unown,
7
+ } from '../index.ts'
8
+
9
+ describe('unown', () => {
10
+
11
+ test('should return the value of the callback', () => {
12
+ const result = unown(() => 42)
13
+ expect(result).toBe(42)
14
+ })
15
+
16
+ test('should run the callback immediately and synchronously', () => {
17
+ let ran = false
18
+ unown(() => { ran = true })
19
+ expect(ran).toBe(true)
20
+ })
21
+
22
+ test('scope created inside unown is not registered on the enclosing scope', () => {
23
+ let innerCleanupRan = false
24
+ const outerDispose = createScope(() => {
25
+ unown(() => {
26
+ createScope(() => {
27
+ return () => { innerCleanupRan = true }
28
+ })
29
+ })
30
+ })
31
+ outerDispose()
32
+ expect(innerCleanupRan).toBe(false)
33
+ })
34
+
35
+ test('scope created inside unown is not registered on the enclosing effect', () => {
36
+ const trigger = createState(0)
37
+ let innerCleanupRuns = 0
38
+
39
+ const outerDispose = createScope(() => {
40
+ createEffect((): undefined => {
41
+ trigger.get()
42
+ unown(() => {
43
+ createScope(() => {
44
+ return () => { innerCleanupRuns++ }
45
+ })
46
+ })
47
+ })
48
+ })
49
+
50
+ expect(innerCleanupRuns).toBe(0)
51
+ trigger.set(1)
52
+ expect(innerCleanupRuns).toBe(0)
53
+ trigger.set(2)
54
+ expect(innerCleanupRuns).toBe(0)
55
+
56
+ outerDispose()
57
+ })
58
+
59
+ test('effects inside an unowned scope survive effect re-runs (ownership bug regression)', () => {
60
+ const listChange = createState(0)
61
+ let componentEffectRuns = 0
62
+ let componentCleanupRuns = 0
63
+
64
+ const connectComponent = () => unown(() =>
65
+ createScope(() => {
66
+ createEffect((): undefined => {
67
+ componentEffectRuns++
68
+ })
69
+ return () => { componentCleanupRuns++ }
70
+ })
71
+ )
72
+
73
+ const outerDispose = createScope(() => {
74
+ createEffect((): undefined => {
75
+ listChange.get()
76
+ if (listChange.get() === 0) connectComponent()
77
+ })
78
+ })
79
+
80
+ expect(componentEffectRuns).toBe(1)
81
+ expect(componentCleanupRuns).toBe(0)
82
+
83
+ listChange.set(1)
84
+ expect(componentEffectRuns).toBe(1)
85
+ expect(componentCleanupRuns).toBe(0)
86
+
87
+ outerDispose()
88
+ })
89
+
90
+ test('effects inside an unowned scope still run reactively', () => {
91
+ const source = createState('a')
92
+ let effectRuns = 0
93
+
94
+ const dispose = unown(() =>
95
+ createScope(() => {
96
+ createEffect((): undefined => {
97
+ source.get()
98
+ effectRuns++
99
+ })
100
+ })
101
+ )
102
+
103
+ expect(effectRuns).toBe(1)
104
+ source.set('b')
105
+ expect(effectRuns).toBe(2)
106
+
107
+ dispose()
108
+ source.set('c')
109
+ expect(effectRuns).toBe(2)
110
+ })
111
+
112
+ test('dispose returned from an unowned scope still works', () => {
113
+ let cleanupRan = false
114
+ const dispose = unown(() =>
115
+ createScope(() => {
116
+ return () => { cleanupRan = true }
117
+ })
118
+ )
119
+ expect(cleanupRan).toBe(false)
120
+ dispose()
121
+ expect(cleanupRan).toBe(true)
122
+ })
123
+
124
+ test('nested unown calls work correctly', () => {
125
+ let innerCleanupRan = false
126
+ const outerDispose = createScope(() => {
127
+ unown(() => {
128
+ unown(() => {
129
+ createScope(() => {
130
+ return () => { innerCleanupRan = true }
131
+ })
132
+ })
133
+ })
134
+ })
135
+ outerDispose()
136
+ expect(innerCleanupRan).toBe(false)
137
+ })
138
+
139
+ test('restores the active owner after the callback completes', () => {
140
+ let postCleanupRan = false
141
+ const outerDispose = createScope(() => {
142
+ unown(() => { /* some unowned work */ })
143
+ createScope(() => {
144
+ return () => { postCleanupRan = true }
145
+ })
146
+ })
147
+ outerDispose()
148
+ expect(postCleanupRan).toBe(true)
149
+ })
150
+
151
+ test('restores the active owner even if the callback throws', () => {
152
+ let postCleanupRan = false
153
+ const outerDispose = createScope(() => {
154
+ try {
155
+ unown(() => { throw new Error('boom') })
156
+ } catch {
157
+ // swallow
158
+ }
159
+ createScope(() => {
160
+ return () => { postCleanupRan = true }
161
+ })
162
+ })
163
+ outerDispose()
164
+ expect(postCleanupRan).toBe(true)
165
+ })
166
+
167
+ test('works correctly when called outside any scope or effect', () => {
168
+ let ran = false
169
+ const dispose = unown(() => {
170
+ ran = true
171
+ return createScope(() => {
172
+ return () => {}
173
+ })
174
+ })
175
+ expect(ran).toBe(true)
176
+ expect(typeof dispose).toBe('function')
177
+ })
178
+
179
+ })
@@ -53,7 +53,8 @@ export function runGraph(
53
53
  ): number {
54
54
  const rand = new Random('seed')
55
55
  const { sources, layers } = graph
56
- const leaves = layers[layers.length - 1]
56
+ // biome-ignore lint/style/noNonNullAssertion: test
57
+ const leaves = layers[layers.length - 1]!
57
58
  const skipCount = Math.round(leaves.length * (1 - readFraction))
58
59
  const readLeaves = removeElems(leaves, skipCount, rand)
59
60
  const frameworkName = framework.name.toLowerCase()
@@ -65,7 +66,8 @@ export function runGraph(
65
66
  for (let i = 0; i < iterations; i++) {
66
67
  framework.withBatch(() => {
67
68
  const sourceDex = i % sources.length
68
- sources[sourceDex].write(i + sourceDex)
69
+ // biome-ignore lint/style/noNonNullAssertion: test
70
+ sources[sourceDex]!.write(i + sourceDex)
69
71
  })
70
72
 
71
73
  for (const leaf of readLeaves) {
@@ -87,7 +89,8 @@ export function runGraph(
87
89
  } */
88
90
 
89
91
  const sourceDex = i % sources.length
90
- sources[sourceDex].write(i + sourceDex)
92
+ // biome-ignore lint/style/noNonNullAssertion: test
93
+ sources[sourceDex]!.write(i + sourceDex)
91
94
 
92
95
  for (const leaf of readLeaves) {
93
96
  leaf.read()
@@ -153,7 +156,8 @@ function makeRow(
153
156
  return sources.map((_, myDex) => {
154
157
  const mySources: Computed<number>[] = []
155
158
  for (let sourceDex = 0; sourceDex < nSources; sourceDex++) {
156
- mySources.push(sources[(myDex + sourceDex) % sources.length])
159
+ // biome-ignore lint/style/noNonNullAssertion: test
160
+ mySources.push(sources[(myDex + sourceDex) % sources.length]!)
157
161
  }
158
162
 
159
163
  const staticNode = random.float() < staticFraction
@@ -170,7 +174,8 @@ function makeRow(
170
174
  })
171
175
  } else {
172
176
  // dynamic node, drops one of the sources depending on the value of the first element
173
- const first = mySources[0]
177
+ // biome-ignore lint/style/noNonNullAssertion: test
178
+ const first = mySources[0]!
174
179
  const tail = mySources.slice(1)
175
180
  const node = framework.computed(() => {
176
181
  counter.count++
@@ -180,7 +185,8 @@ function makeRow(
180
185
 
181
186
  for (let i = 0; i < tail.length; i++) {
182
187
  if (shouldDrop && i === dropDex) continue
183
- sum += tail[i].read()
188
+ // biome-ignore lint/style/noNonNullAssertion: test
189
+ sum += tail[i]!.read()
184
190
  }
185
191
 
186
192
  return sum
package/tsconfig.json CHANGED
@@ -19,6 +19,10 @@
19
19
  // Best practices
20
20
  "strict": true,
21
21
  "skipLibCheck": true,
22
+ "noUncheckedIndexedAccess": true,
23
+ "exactOptionalPropertyTypes": true,
24
+ "useUnknownInCatchVariables": true,
25
+ "noUncheckedSideEffectImports": true,
22
26
  "noFallthroughCasesInSwitch": true,
23
27
 
24
28
  // Some stricter flags (disabled by default)
@@ -27,5 +31,5 @@
27
31
  "noPropertyAccessFromIndexSignature": false,
28
32
  },
29
33
  "include": ["./**/*.ts"],
30
- "exclude": ["node_modules", "types"],
34
+ "exclude": ["node_modules", "types", "index.js"],
31
35
  }
package/types/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * @name Cause & Effect
3
- * @version 0.18.3
3
+ * @version 1.0.0
4
4
  * @author Esther Brunner
5
5
  */
6
6
  export { CircularDependencyError, type Guard, InvalidCallbackError, InvalidSignalValueError, NullishSignalValueError, ReadonlySignalError, RequiredOwnerError, UnsetSignalValueError, } from './src/errors';
7
- export { batch, type Cleanup, type ComputedOptions, createScope, type EffectCallback, type MaybeCleanup, type MemoCallback, type Signal, type SignalOptions, SKIP_EQUALITY, type TaskCallback, untrack, } from './src/graph';
7
+ export { batch, type Cleanup, type ComputedOptions, createScope, type EffectCallback, type MaybeCleanup, type MemoCallback, type Signal, type SignalOptions, SKIP_EQUALITY, type TaskCallback, unown, untrack, } from './src/graph';
8
8
  export { type Collection, type CollectionCallback, type CollectionChanges, type CollectionOptions, createCollection, type DeriveCollectionCallback, isCollection, } from './src/nodes/collection';
9
9
  export { createEffect, type MatchHandlers, type MaybePromise, match, } from './src/nodes/effect';
10
10
  export { createList, isEqual, isList, type KeyConfig, type List, type ListOptions, } from './src/nodes/list';
@@ -3,11 +3,11 @@ type SourceFields<T extends {}> = {
3
3
  value: T;
4
4
  sinks: Edge | null;
5
5
  sinksTail: Edge | null;
6
- stop?: Cleanup;
6
+ stop?: Cleanup | undefined;
7
7
  };
8
8
  type OptionsFields<T extends {}> = {
9
9
  equals: (a: T, b: T) => boolean;
10
- guard?: Guard<T>;
10
+ guard?: Guard<T> | undefined;
11
11
  };
12
12
  type SinkFields = {
13
13
  fn: unknown;
@@ -218,4 +218,14 @@ declare function untrack<T>(fn: () => T): T;
218
218
  * ```
219
219
  */
220
220
  declare function createScope(fn: () => MaybeCleanup): Cleanup;
221
- export { type Cleanup, type ComputedOptions, type EffectCallback, type EffectNode, type MaybeCleanup, type MemoCallback, type MemoNode, type Scope, type Signal, type SignalOptions, type SinkNode, type StateNode, type TaskCallback, type TaskNode, activeOwner, activeSink, batch, batchDepth, createScope, DEFAULT_EQUALITY, SKIP_EQUALITY, FLAG_CHECK, FLAG_CLEAN, FLAG_DIRTY, FLAG_RELINK, flush, link, propagate, refresh, registerCleanup, runCleanup, runEffect, setState, trimSources, TYPE_COLLECTION, TYPE_LIST, TYPE_MEMO, TYPE_SENSOR, TYPE_STATE, TYPE_SLOT, TYPE_STORE, TYPE_TASK, unlink, untrack, };
221
+ /**
222
+ * Runs a callback without any active owner.
223
+ * Any scopes or effects created inside the callback will not be registered as
224
+ * children of the current active owner (e.g. a re-runnable effect). Use this
225
+ * when a component or resource manages its own lifecycle independently of the
226
+ * reactive graph.
227
+ *
228
+ * @since 0.18.5
229
+ */
230
+ declare function unown<T>(fn: () => T): T;
231
+ export { type Cleanup, type ComputedOptions, type EffectCallback, type EffectNode, type MaybeCleanup, type MemoCallback, type MemoNode, type Scope, type Signal, type SignalOptions, type SinkNode, type StateNode, type TaskCallback, type TaskNode, activeOwner, activeSink, batch, batchDepth, createScope, DEFAULT_EQUALITY, SKIP_EQUALITY, FLAG_CHECK, FLAG_CLEAN, FLAG_DIRTY, FLAG_RELINK, flush, link, propagate, refresh, registerCleanup, runCleanup, runEffect, setState, trimSources, TYPE_COLLECTION, TYPE_LIST, TYPE_MEMO, TYPE_SENSOR, TYPE_STATE, TYPE_SLOT, TYPE_STORE, TYPE_TASK, unlink, unown, untrack, };
package/.ai-context.md DELETED
@@ -1,281 +0,0 @@
1
- # AI Context for Cause & Effect
2
-
3
- ## What is Cause & Effect?
4
-
5
- Cause & Effect is a modern reactive state management library for JavaScript/TypeScript that implements the signals pattern. It provides fine-grained reactivity with automatic dependency tracking, making it easy to build responsive applications with predictable state updates.
6
-
7
- ## Core Architecture
8
-
9
- ### Graph-Based Reactivity (`src/graph.ts`)
10
-
11
- The reactive engine is a linked graph of source and sink nodes connected by `Edge` entries:
12
- - **Sources** (StateNode) maintain a linked list of sink edges
13
- - **Sinks** (MemoNode, TaskNode, EffectNode) maintain a linked list of source edges
14
- - `link()` creates edges between sources and sinks during `.get()` calls
15
- - `propagate()` flags sinks as dirty when sources change
16
- - `flush()` processes queued effects after propagation
17
- - `trimSources()` removes stale edges after recomputation
18
-
19
- ### Node Types
20
- ```
21
- StateNode<T> — source-only with equality + guard (State, Sensor)
22
- MemoNode<T> — source + sink (Memo, Slot, Store, List, Collection)
23
- TaskNode<T> — source + sink + async (Task)
24
- EffectNode — sink + owner (Effect)
25
- Scope — owner-only (createScope)
26
- ```
27
-
28
- ### Signal Types
29
- - **State** (`createState`): Mutable signals for primitive values and objects
30
- - **Sensor** (`createSensor`): Read-only signals for external input streams with automatic state updates. Use `SKIP_EQUALITY` for mutable object observation.
31
- - **Memo** (`createMemo`): Synchronous derived computations with memoization, reducer capabilities, and optional `watched(invalidate)` for external invalidation
32
- - **Task** (`createTask`): Async derived computations with automatic abort/cancellation and optional `watched(invalidate)` for external invalidation
33
- - **Store** (`createStore`): Mutable object signals with individually reactive properties via Proxy
34
- - **List** (`createList`): Mutable array signals with stable keys and reactive items
35
- - **Collection** (`createCollection`): Reactive collections — either externally-driven with watched lifecycle, or derived from List/Collection with item-level memoization
36
- - **Slot** (`createSlot`): Stable delegation signal with swappable backing signal, designed for integration layers (property descriptors, custom elements)
37
- - **Effect** (`createEffect`): Side effect handlers that react to signal changes
38
-
39
- ### Key Principles
40
- 1. **Functional API**: All signals created via `create*()` factory functions (no classes)
41
- 2. **Type Safety**: Full TypeScript support with strict type constraints (`T extends {}`)
42
- 3. **Performance**: Flag-based dirty checking, linked-list edge traversal, batched flushing
43
- 4. **Async Support**: Built-in cancellation with AbortSignal in Tasks
44
- 5. **Tree-shaking**: Optimized for minimal bundle size with `/*#__PURE__*/` annotations
45
-
46
- ## Project Structure
47
-
48
- ```
49
- cause-effect/
50
- ├── src/
51
- │ ├── graph.ts # Core reactive engine (nodes, edges, link, propagate, flush, batch)
52
- │ ├── nodes/
53
- │ │ ├── state.ts # createState — mutable state signals
54
- │ │ ├── sensor.ts # createSensor — external input tracking (also covers mutable object observation)
55
- │ │ ├── memo.ts # createMemo — synchronous derived computations
56
- │ │ ├── task.ts # createTask — async derived computations
57
- │ │ ├── effect.ts # createEffect, match — side effects
58
- │ │ ├── store.ts # createStore — reactive object stores
59
- │ │ ├── list.ts # createList — reactive arrays with stable keys
60
- │ │ ├── collection.ts # createCollection — externally-driven and derived collections
61
- │ │ └── slot.ts # createSlot — stable delegation with swappable backing signal
62
- │ ├── util.ts # Utility functions and type checks
63
- │ └── ...
64
- ├── index.ts # Entry point / main export file
65
- ├── test/
66
- │ ├── state.test.ts
67
- │ ├── memo.test.ts
68
- │ ├── task.test.ts
69
- │ ├── effect.test.ts
70
- │ ├── store.test.ts
71
- │ ├── list.test.ts
72
- │ └── collection.test.ts
73
- └── package.json
74
- ```
75
-
76
- ## API Patterns
77
-
78
- ### Signal Creation
79
- ```typescript
80
- // State for primitives and objects
81
- const count = createState(42)
82
- const name = createState('Alice')
83
-
84
- // Sensor for external input
85
- const mousePos = createSensor<{ x: number; y: number }>((set) => {
86
- const handler = (e: MouseEvent) => set({ x: e.clientX, y: e.clientY })
87
- window.addEventListener('mousemove', handler)
88
- return () => window.removeEventListener('mousemove', handler)
89
- })
90
-
91
- // Sensor for mutable object observation (SKIP_EQUALITY)
92
- const element = createSensor<HTMLElement>((set) => {
93
- const node = document.getElementById('box')!
94
- set(node)
95
- const obs = new MutationObserver(() => set(node))
96
- obs.observe(node, { attributes: true })
97
- return () => obs.disconnect()
98
- }, { value: node, equals: SKIP_EQUALITY })
99
-
100
- // Store for objects with reactive properties
101
- const user = createStore({ name: 'Alice', age: 30 })
102
-
103
- // List with stable keys for arrays
104
- const items = createList(['apple', 'banana', 'cherry'])
105
- const users = createList(
106
- [{ id: 'alice', name: 'Alice' }],
107
- { keyConfig: user => user.id }
108
- )
109
-
110
- // Memo for synchronous derived values
111
- const doubled = createMemo(() => count.get() * 2)
112
-
113
- // Memo with reducer capabilities (access to previous value)
114
- const counter = createMemo(prev => {
115
- const action = actions.get()
116
- return action === 'increment' ? prev + 1 : prev - 1
117
- }, { value: 0 })
118
-
119
- // Task for async derived values with cancellation
120
- const userData = createTask(async (prev, abort) => {
121
- const id = userId.get()
122
- if (!id) return prev
123
- const response = await fetch(`/users/${id}`, { signal: abort })
124
- return response.json()
125
- })
126
-
127
- // Collection for derived transformations
128
- const doubled = numbers.deriveCollection((value: number) => value * 2)
129
- const enriched = users.deriveCollection(async (user, abort) => {
130
- const res = await fetch(`/api/${user.id}`, { signal: abort })
131
- return { ...user, details: await res.json() }
132
- })
133
-
134
- // Collection for externally-driven data
135
- const feed = createCollection<{ id: string; text: string }>((applyChanges) => {
136
- const ws = new WebSocket('/feed')
137
- ws.onmessage = (e) => applyChanges(JSON.parse(e.data))
138
- return () => ws.close()
139
- }, { keyConfig: item => item.id })
140
-
141
- // Slot for stable property delegation
142
- const local = createState('default')
143
- const slot = createSlot(local)
144
- Object.defineProperty(element, 'label', slot)
145
- slot.replace(createMemo(() => parentState.get())) // swap backing signal
146
- ```
147
-
148
- ### Reactivity
149
- ```typescript
150
- // Effects run when dependencies change
151
- const dispose = createEffect(() => {
152
- console.log(`Count: ${count.get()}`)
153
- })
154
-
155
- // Effects can return cleanup functions
156
- createEffect(() => {
157
- const timer = setInterval(() => console.log(count.get()), 1000)
158
- return () => clearInterval(timer)
159
- })
160
-
161
- // match() for ergonomic signal value extraction inside effects
162
- createEffect(() => {
163
- match([userData], {
164
- ok: ([data]) => updateUI(data),
165
- nil: () => showLoading(),
166
- err: errors => showError(errors[0].message)
167
- })
168
- })
169
- ```
170
-
171
- ### Value Access Patterns
172
- ```typescript
173
- // All signals use .get() for value access
174
- const value = signal.get()
175
-
176
- // State has .set() and .update()
177
- state.set(newValue)
178
- state.update(current => current + 1)
179
-
180
- // Store properties are individually reactive via Proxy
181
- user.name.set('Bob') // Only name watchers trigger
182
- user.age.update(age => age + 1) // Only age watchers trigger
183
-
184
- // List with stable keys
185
- const items = createList(['apple', 'banana'], { keyConfig: 'fruit' })
186
- const appleSignal = items.byKey('fruit0')
187
- const firstKey = items.keyAt(0)
188
- const appleIndex = items.indexOfKey('fruit0')
189
- items.splice(1, 0, 'cherry')
190
- items.sort()
191
-
192
- // Collections via deriveCollection
193
- const processed = items.deriveCollection(item => item.toUpperCase())
194
- ```
195
-
196
- ## Coding Conventions
197
-
198
- ### TypeScript Style
199
- - Generic constraints: `T extends {}` to exclude null/undefined
200
- - Use `const` for immutable values
201
- - Function overloads for complex type scenarios (e.g., `createCollection`, `deriveCollection`)
202
- - JSDoc comments on all public APIs
203
- - Pure function annotations: `/*#__PURE__*/` for tree-shaking
204
-
205
- ### Naming Conventions
206
- - Factory functions: `create*` prefix (createState, createMemo, createEffect, createStore, createList, createCollection, createSensor, createSlot)
207
- - Type predicates: `is*` prefix (isState, isMemo, isTask, isStore, isList, isCollection, isSensor, isSlot)
208
- - Type constants: `TYPE_*` format (TYPE_STATE, TYPE_STORE, TYPE_SENSOR, TYPE_COLLECTION)
209
- - Callback types: `*Callback` suffix (MemoCallback, TaskCallback, EffectCallback, SensorCallback, CollectionCallback, DeriveCollectionCallback)
210
-
211
- ### Error Handling
212
- - Custom error classes in `src/errors.ts`: CircularDependencyError, NullishSignalValueError, InvalidSignalValueError, InvalidCallbackError, RequiredOwnerError, UnsetSignalValueError
213
- - Descriptive error messages with `[TypeName]` prefix
214
- - Input validation via `validateSignalValue()` and `validateCallback()`
215
- - Optional type guards via `guard` option in SignalOptions
216
-
217
- ## Performance Considerations
218
-
219
- ### Optimization Patterns
220
- - Linked-list edges for O(1) link/unlink operations
221
- - Flag-based dirty checking: FLAG_CLEAN, FLAG_CHECK, FLAG_DIRTY, FLAG_RUNNING
222
- - Batched updates via `batch()` to minimize effect re-runs
223
- - Lazy evaluation: Memos only recompute when accessed and dirty
224
- - Automatic abort of in-flight Tasks when sources change
225
-
226
- ### Memory Management
227
- - `trimSources()` removes stale edges after each recomputation
228
- - `unlink()` calls `source.stop()` when last sink disconnects (auto-cleanup for Sensor/Collection/Store/List)
229
- - AbortSignal integration for canceling async operations
230
- - `createScope()` for hierarchical cleanup of nested effects
231
-
232
- ## Resource Management
233
-
234
- **Sensor and Collection** use a watched callback that returns a Cleanup function:
235
- ```typescript
236
- const sensor = createSensor<T>((set) => {
237
- // setup input tracking, call set(value) to update
238
- return () => { /* cleanup */ }
239
- })
240
-
241
- const feed = createCollection<T>((applyChanges) => {
242
- // setup external data source, call applyChanges(diffResult) on changes
243
- return () => { /* cleanup */ }
244
- }, { keyConfig: item => item.id })
245
- ```
246
-
247
- **Memo and Task** use an optional `watched` callback in options that receives an `invalidate` function:
248
- ```typescript
249
- const derived = createMemo(() => element.get().textContent ?? '', {
250
- watched: (invalidate) => {
251
- const obs = new MutationObserver(() => invalidate())
252
- obs.observe(element.get(), { childList: true })
253
- return () => obs.disconnect()
254
- }
255
- })
256
- ```
257
-
258
- **Store and List** use an optional `watched` callback in options:
259
- ```typescript
260
- const store = createStore(initialValue, {
261
- watched: () => {
262
- // setup resources
263
- return () => { /* cleanup */ }
264
- }
265
- })
266
- ```
267
-
268
- Resources activate on first sink link and cleanup when last sink unlinks.
269
-
270
- ## Build and Development
271
-
272
- ### Tools
273
- - **Runtime**: Bun (also works with Node.js)
274
- - **Build**: Bun build with TypeScript compilation
275
- - **Testing**: Bun test runner (`bun test`)
276
- - **Linting**: Biome for code formatting and linting
277
-
278
- ### Package Configuration
279
- - ES modules only (`"type": "module"`)
280
- - TypeScript declarations generated automatically
281
- - Tree-shaking friendly with proper sideEffects configuration