@zeix/cause-effect 0.18.0 → 0.18.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/.ai-context.md +14 -3
- package/.github/copilot-instructions.md +15 -5
- package/ARCHITECTURE.md +15 -13
- package/CHANGELOG.md +29 -0
- package/CLAUDE.md +9 -7
- package/README.md +23 -5
- package/context7.json +4 -0
- package/examples/events-sensor.ts +187 -0
- package/examples/selector-sensor.ts +173 -0
- package/index.dev.js +276 -222
- package/index.js +1 -1
- package/index.ts +4 -2
- package/package.json +2 -2
- package/skills/changelog-keeper/SKILL.md +59 -0
- package/skills/changelog-keeper/agents/openai.yaml +4 -0
- package/src/graph.ts +13 -2
- package/src/nodes/collection.ts +166 -128
- package/src/nodes/list.ts +105 -104
- package/src/nodes/memo.ts +31 -3
- package/src/nodes/sensor.ts +27 -17
- package/src/nodes/state.ts +2 -2
- package/src/nodes/store.ts +55 -60
- package/src/nodes/task.ts +31 -3
- package/test/collection.test.ts +40 -51
- package/test/memo.test.ts +194 -0
- package/test/task.test.ts +134 -0
- package/types/index.d.ts +5 -5
- package/types/src/graph.d.ts +12 -2
- package/types/src/nodes/collection.d.ts +12 -7
- package/types/src/nodes/list.d.ts +12 -11
- package/types/src/nodes/memo.d.ts +6 -0
- package/types/src/nodes/sensor.d.ts +15 -9
- package/types/src/nodes/store.d.ts +4 -4
- package/types/src/nodes/task.d.ts +6 -0
- package/COLLECTION_REFACTORING.md +0 -161
package/.ai-context.md
CHANGED
|
@@ -28,8 +28,8 @@ Scope — owner-only (createScope)
|
|
|
28
28
|
### Signal Types
|
|
29
29
|
- **State** (`createState`): Mutable signals for primitive values and objects
|
|
30
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 and
|
|
32
|
-
- **Task** (`createTask`): Async derived computations with automatic abort/cancellation
|
|
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
33
|
- **Store** (`createStore`): Mutable object signals with individually reactive properties via Proxy
|
|
34
34
|
- **List** (`createList`): Mutable array signals with stable keys and reactive items
|
|
35
35
|
- **Collection** (`createCollection`): Reactive collections — either externally-driven with watched lifecycle, or derived from List/Collection with item-level memoization
|
|
@@ -223,7 +223,7 @@ const processed = items.deriveCollection(item => item.toUpperCase())
|
|
|
223
223
|
|
|
224
224
|
## Resource Management
|
|
225
225
|
|
|
226
|
-
**Sensor and Collection** use a
|
|
226
|
+
**Sensor and Collection** use a watched callback that returns a Cleanup function:
|
|
227
227
|
```typescript
|
|
228
228
|
const sensor = createSensor<T>((set) => {
|
|
229
229
|
// setup input tracking, call set(value) to update
|
|
@@ -236,6 +236,17 @@ const feed = createCollection<T>((applyChanges) => {
|
|
|
236
236
|
}, { keyConfig: item => item.id })
|
|
237
237
|
```
|
|
238
238
|
|
|
239
|
+
**Memo and Task** use an optional `watched` callback in options that receives an `invalidate` function:
|
|
240
|
+
```typescript
|
|
241
|
+
const derived = createMemo(() => element.get().textContent ?? '', {
|
|
242
|
+
watched: (invalidate) => {
|
|
243
|
+
const obs = new MutationObserver(() => invalidate())
|
|
244
|
+
obs.observe(element.get(), { childList: true })
|
|
245
|
+
return () => obs.disconnect()
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
```
|
|
249
|
+
|
|
239
250
|
**Store and List** use an optional `watched` callback in options:
|
|
240
251
|
```typescript
|
|
241
252
|
const store = createStore(initialValue, {
|
|
@@ -15,8 +15,8 @@ Cause & Effect is a reactive state management library for JavaScript/TypeScript
|
|
|
15
15
|
### Signal Types (all in `src/nodes/`)
|
|
16
16
|
- **State** (`createState`): Mutable signals for values (`get`, `set`, `update`)
|
|
17
17
|
- **Sensor** (`createSensor`): Read-only signals for external input with automatic state updates. Use `SKIP_EQUALITY` for mutable object observation.
|
|
18
|
-
- **Memo** (`createMemo`): Synchronous derived computations with memoization and
|
|
19
|
-
- **Task** (`createTask`): Async derived computations with automatic AbortController cancellation
|
|
18
|
+
- **Memo** (`createMemo`): Synchronous derived computations with memoization, reducer capabilities, and optional `watched(invalidate)` for external invalidation
|
|
19
|
+
- **Task** (`createTask`): Async derived computations with automatic AbortController cancellation and optional `watched(invalidate)` for external invalidation
|
|
20
20
|
- **Store** (`createStore`): Proxy-based reactive objects with per-property State/Store signals
|
|
21
21
|
- **List** (`createList`): Reactive arrays with stable keys and per-item State signals
|
|
22
22
|
- **Collection** (`createCollection`): Reactive collections — either externally-driven with watched lifecycle, or derived from List/Collection with item-level memoization
|
|
@@ -72,7 +72,8 @@ Cause & Effect is a reactive state management library for JavaScript/TypeScript
|
|
|
72
72
|
- All signals have `.get()` for value access
|
|
73
73
|
- Mutable signals (State) have `.set(value)` and `.update(fn)`
|
|
74
74
|
- Store properties are automatically reactive signals via Proxy
|
|
75
|
-
- Sensor/Collection use a
|
|
75
|
+
- Sensor/Collection use a watched callback returning Cleanup (lazy activation)
|
|
76
|
+
- Memo/Task use optional `watched(invalidate)` callback in options for external invalidation
|
|
76
77
|
- Store/List use optional `watched` callback in options returning Cleanup
|
|
77
78
|
- Effects return a dispose function (Cleanup)
|
|
78
79
|
|
|
@@ -191,18 +192,27 @@ const count = createState(0, {
|
|
|
191
192
|
## Resource Management
|
|
192
193
|
|
|
193
194
|
```typescript
|
|
194
|
-
// Sensor: lazy external input tracking
|
|
195
|
+
// Sensor: lazy external input tracking (watched callback with set)
|
|
195
196
|
const sensor = createSensor<T>((set) => {
|
|
196
197
|
// setup — call set(value) to update
|
|
197
198
|
return () => { /* cleanup — called when last effect stops watching */ }
|
|
198
199
|
})
|
|
199
200
|
|
|
200
|
-
// Collection: lazy external data source
|
|
201
|
+
// Collection: lazy external data source (watched callback with applyChanges)
|
|
201
202
|
const feed = createCollection<T>((applyChanges) => {
|
|
202
203
|
// setup — call applyChanges(diffResult) on changes
|
|
203
204
|
return () => { /* cleanup */ }
|
|
204
205
|
}, { keyConfig: item => item.id })
|
|
205
206
|
|
|
207
|
+
// Memo/Task: optional watched callback with invalidate
|
|
208
|
+
const derived = createMemo(() => element.get().textContent ?? '', {
|
|
209
|
+
watched: (invalidate) => {
|
|
210
|
+
const obs = new MutationObserver(() => invalidate())
|
|
211
|
+
obs.observe(element.get(), { childList: true })
|
|
212
|
+
return () => obs.disconnect()
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
|
|
206
216
|
// Store/List: optional watched callback
|
|
207
217
|
const store = createStore(initialValue, {
|
|
208
218
|
watched: () => {
|
package/ARCHITECTURE.md
CHANGED
|
@@ -178,14 +178,14 @@ A mutable value container. The simplest signal type — `get()` links and return
|
|
|
178
178
|
|
|
179
179
|
**Graph node**: `StateNode<T>` (source only)
|
|
180
180
|
|
|
181
|
-
A read-only signal that tracks external input. The `
|
|
181
|
+
A read-only signal that tracks external input. The `watched` callback receives a `set` function that updates the node's value via `setState()`. Sensors cover two patterns:
|
|
182
182
|
|
|
183
|
-
1. **Tracking external values** (default): Receives replacement values from events (mouse position, resize events). Equality checking (
|
|
183
|
+
1. **Tracking external values** (default): Receives replacement values from events (mouse position, resize events). Equality checking (`===` by default) prevents unnecessary propagation.
|
|
184
184
|
2. **Observing mutable objects** (with `SKIP_EQUALITY`): Holds a stable reference to a mutable object (DOM element, Map, Set). `set(sameRef)` with `equals: SKIP_EQUALITY` always propagates, notifying consumers that the object's internals have changed.
|
|
185
185
|
|
|
186
|
-
The value starts undefined unless `options.value` is provided. Reading a sensor before its `
|
|
186
|
+
The value starts undefined unless `options.value` is provided. Reading a sensor before its `watched` callback has called `set()` (and without `options.value`) throws `UnsetSignalValueError`.
|
|
187
187
|
|
|
188
|
-
**Lazy lifecycle**: The `
|
|
188
|
+
**Lazy lifecycle**: The `watched` callback is invoked on first sink attachment. The returned cleanup is stored as `node.stop` and called when the last sink detaches (via `unlink()`).
|
|
189
189
|
|
|
190
190
|
### Memo (`src/nodes/memo.ts`)
|
|
191
191
|
|
|
@@ -199,6 +199,8 @@ The `error` field preserves thrown errors: if `fn` throws, the error is stored a
|
|
|
199
199
|
|
|
200
200
|
**Reducer pattern**: The `prev` parameter enables state accumulation across recomputations without writable state.
|
|
201
201
|
|
|
202
|
+
**Watched lifecycle**: An optional `watched` callback in options provides lazy external invalidation. The callback receives an `invalidate` function and is invoked on first sink attachment. Calling `invalidate()` marks the node `FLAG_DIRTY`, propagates to sinks, and flushes — triggering re-evaluation of the memo's `fn` without changing any tracked dependency. The returned cleanup is stored as `node.stop` and called when the last sink detaches. This enables patterns like DOM observation (MutationObserver) where the memo re-derives its value in response to external events.
|
|
203
|
+
|
|
202
204
|
### Task (`src/nodes/task.ts`)
|
|
203
205
|
|
|
204
206
|
**Graph node**: `TaskNode<T>` (source + sink)
|
|
@@ -209,6 +211,8 @@ During dependency tracking, only the synchronous preamble of `fn` is tracked (be
|
|
|
209
211
|
|
|
210
212
|
`isPending()` returns `true` while a computation is in flight. `abort()` cancels the current computation manually. Errors are preserved like Memo, but old values are retained on errors (the last successful result remains accessible).
|
|
211
213
|
|
|
214
|
+
**Watched lifecycle**: Same pattern as Memo — an optional `watched` callback receives `invalidate` and is invoked on first sink attachment. Calling `invalidate()` marks the node dirty and triggers re-execution, which aborts any in-flight computation via the existing `AbortController` mechanism before starting a new one.
|
|
215
|
+
|
|
212
216
|
### Effect (`src/nodes/effect.ts`)
|
|
213
217
|
|
|
214
218
|
**Graph node**: `EffectNode` (sink only)
|
|
@@ -247,13 +251,13 @@ A reactive array with stable keys and per-item reactivity. Each item becomes a `
|
|
|
247
251
|
|
|
248
252
|
Collection implements two creation patterns that share the same `Collection<T>` interface:
|
|
249
253
|
|
|
250
|
-
#### `createCollection(
|
|
254
|
+
#### `createCollection(watched, options?)` — externally driven
|
|
251
255
|
|
|
252
256
|
**Graph node**: `MemoNode<T[]>` (source + sink, tracks item values)
|
|
253
257
|
|
|
254
|
-
An externally-driven reactive collection with a watched lifecycle, mirroring `createSensor(
|
|
258
|
+
An externally-driven reactive collection with a watched lifecycle, mirroring `createSensor(watched, options?)`. The `watched` callback receives an `applyChanges(diffResult)` function for granular add/change/remove operations. Initial items are provided via `options.value` (default `[]`).
|
|
255
259
|
|
|
256
|
-
**Lazy lifecycle**: Like Sensor, the `
|
|
260
|
+
**Lazy lifecycle**: Like Sensor, the `watched` callback is invoked on first sink attachment. The returned cleanup is stored as `node.stop` and called when the last sink detaches (via `unlink()`). The `startWatching()` guard ensures `watched` fires before `link()` so synchronous mutations inside `watched` update `node.value` before the activating effect reads it.
|
|
257
261
|
|
|
258
262
|
**External mutation via `applyChanges`**: Additions create new item signals (via configurable `createItem` factory, default `createState`). Changes update existing `State` signals. Removals delete signals and keys. Structural changes null out `node.sources` to force edge re-establishment. The node uses `equals: () => false` since structural changes are managed externally rather than detected by diffing.
|
|
259
263
|
|
|
@@ -261,14 +265,12 @@ An externally-driven reactive collection with a watched lifecycle, mirroring `cr
|
|
|
261
265
|
|
|
262
266
|
#### `deriveCollection(source, callback)` — internally derived
|
|
263
267
|
|
|
264
|
-
**Graph node**: `MemoNode<
|
|
268
|
+
**Graph node**: `MemoNode<T[]>` (source + sink, tracks item values)
|
|
265
269
|
|
|
266
270
|
An internal factory (not exported from the public API) that creates a read-only derived transformation of a List or another Collection. Exposed to users via the `.deriveCollection(callback)` method on List and Collection. Each source item is individually memoized: sync callbacks create `Memo` signals, async callbacks create `Task` signals.
|
|
267
271
|
|
|
268
|
-
**
|
|
269
|
-
|
|
270
|
-
**Key differences from Store/List**: The `MemoNode.value` is a `string[]` (the keys array), not the collection's actual values. The `equals` function is a shallow string array comparison (`keysEqual`). The node starts `FLAG_DIRTY` (unlike Store/List which start clean after initialization) to ensure the first `refresh()` establishes the edge from source to collection.
|
|
272
|
+
**Consistent with Store/List/createCollection**: The `MemoNode.value` is a `T[]` (cached computed values), and keys are tracked in a separate local `string[]` variable. The `equals` function uses shallow reference equality on array elements to prevent unnecessary downstream propagation when re-evaluation produces the same item references. The node starts `FLAG_DIRTY` to ensure the first `refresh()` establishes edges.
|
|
271
273
|
|
|
272
|
-
**
|
|
274
|
+
**Two-path access with structural fallback**: Same pattern as Store/List — first `get()` uses `refresh()` to establish edges; subsequent reads use `untrack(buildValue)`. A `syncKeys()` step inside `buildValue` syncs the signals map with `source.keys()`. If keys changed, `syncKeys` nulls `node.sources` to force edge re-establishment via `refresh()`, ensuring new child signals are properly linked.
|
|
273
275
|
|
|
274
|
-
**Chaining**: `.deriveCollection()` creates a new derived collection from an existing one, forming a pipeline. Each level in the chain has its own `MemoNode` for
|
|
276
|
+
**Chaining**: `.deriveCollection()` creates a new derived collection from an existing one, forming a pipeline. Each level in the chain has its own `MemoNode` for value caching and its own set of per-item derived signals.
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.18.1
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Memo `watched(invalidate)` option**: `createMemo(fn, { watched })` accepts a lazy lifecycle callback that receives an `invalidate` function. Calling `invalidate()` marks the memo dirty and triggers re-evaluation. The callback is invoked on first sink attachment and cleaned up when the last sink detaches. This enables patterns like DOM observation where a memo re-derives its value in response to external events (e.g., MutationObserver) without needing a separate Sensor.
|
|
8
|
+
- **Task `watched(invalidate)` option**: Same pattern as Memo. Calling `invalidate()` aborts any in-flight computation and triggers re-execution.
|
|
9
|
+
- **`CollectionChanges<T>` type**: New typed interface for collection mutations with `add?: T[]`, `change?: T[]`, `remove?: T[]` arrays. Replaces the untyped `DiffResult` records previously used by `CollectionCallback`.
|
|
10
|
+
- **`SensorOptions<T>` type**: Dedicated options type for `createSensor`, extending `SignalOptions<T>` with optional `value`.
|
|
11
|
+
- **`CollectionChanges` export** from public API (`index.ts`).
|
|
12
|
+
- **`SensorOptions` export** from public API (`index.ts`).
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- **`createSensor` parameter renamed**: `start` → `watched` for consistency with Store/List lifecycle terminology.
|
|
17
|
+
- **`createSensor` options type**: `ComputedOptions<T>` → `SensorOptions<T>`. This decouples Sensor options from `ComputedOptions`, which now carries the `watched(invalidate)` field for Memo/Task.
|
|
18
|
+
- **`createCollection` parameter renamed**: `start` → `watched` for consistency.
|
|
19
|
+
- **`CollectionCallback` is now generic**: `CollectionCallback` → `CollectionCallback<T>`. The `applyChanges` parameter accepts `CollectionChanges<T>` instead of `DiffResult`.
|
|
20
|
+
- **`CollectionOptions.createItem` signature**: `(key: string, value: T) => Signal<T>` → `(value: T) => Signal<T>`. Key generation is now handled internally.
|
|
21
|
+
- **`KeyConfig<T>` return type relaxed**: Key functions may now return `string | undefined`. Returning `undefined` falls back to synthetic key generation.
|
|
22
|
+
|
|
23
|
+
### Removed
|
|
24
|
+
|
|
25
|
+
- **`DiffResult` removed from public API**: No longer re-exported from `index.ts`. The type remains available from `src/nodes/list.ts` for internal use but is superseded by `CollectionChanges<T>` for collection mutations.
|
|
26
|
+
|
|
27
|
+
## 0.18.0
|
|
28
|
+
|
|
29
|
+
Baseline release. Factory function API (`createState`, `createMemo`, `createTask`, `createEffect`, `createStore`, `createList`, `createCollection`, `createSensor`) with linked-list graph engine.
|
package/CLAUDE.md
CHANGED
|
@@ -86,10 +86,10 @@ The generic constraint `T extends {}` is crucial - it excludes `null` and `undef
|
|
|
86
86
|
### Collection Architecture
|
|
87
87
|
|
|
88
88
|
**Collections** (`createCollection`): Externally-driven collections with watched lifecycle
|
|
89
|
-
- Created via `createCollection(
|
|
90
|
-
- The `
|
|
89
|
+
- Created via `createCollection(watched, options?)` — mirrors `createSensor(watched, options?)`
|
|
90
|
+
- The `watched` callback receives an `applyChanges(diffResult)` function for granular add/change/remove operations
|
|
91
91
|
- `options.value` provides initial items (default `[]`), `options.keyConfig` configures key generation
|
|
92
|
-
- Lazy activation: `
|
|
92
|
+
- Lazy activation: `watched` callback invoked on first effect access, cleanup when unwatched
|
|
93
93
|
|
|
94
94
|
**Derived Collections** (`deriveCollection`): Transformed from Lists or other Collections
|
|
95
95
|
- Created via `list.deriveCollection(callback)` or `collection.deriveCollection(callback)`
|
|
@@ -124,7 +124,7 @@ Computed signals implement smart memoization:
|
|
|
124
124
|
|
|
125
125
|
## Resource Management with Watch Callbacks
|
|
126
126
|
|
|
127
|
-
Sensor and
|
|
127
|
+
Sensor, Collection, Memo (with `watched` option), and Task (with `watched` option) use a **watched callback** pattern for lazy resource management. Resources are allocated only when a signal is first accessed by an effect and automatically cleaned up when no effects are watching:
|
|
128
128
|
|
|
129
129
|
```typescript
|
|
130
130
|
// Sensor: track external input with state updates
|
|
@@ -164,9 +164,9 @@ const user = createStore({ name: 'Alice', email: 'alice@example.com' }, {
|
|
|
164
164
|
```
|
|
165
165
|
|
|
166
166
|
**Watch Lifecycle**:
|
|
167
|
-
1. First effect accesses signal →
|
|
167
|
+
1. First effect accesses signal → watched callback executed
|
|
168
168
|
2. Last effect stops watching → returned cleanup function executed
|
|
169
|
-
3. New effect accesses signal →
|
|
169
|
+
3. New effect accesses signal → watched callback executed again
|
|
170
170
|
|
|
171
171
|
This pattern enables **lazy resource allocation** - resources are only consumed when actually needed and automatically freed when no longer used.
|
|
172
172
|
|
|
@@ -230,7 +230,7 @@ const firstTodo = todoList.byKey('task1') // Access by stable key
|
|
|
230
230
|
|
|
231
231
|
**Collection (`createCollection`)**:
|
|
232
232
|
- Externally-driven keyed collections (WebSocket streams, SSE, external data feeds)
|
|
233
|
-
- Mirrors `createSensor(
|
|
233
|
+
- Mirrors `createSensor(watched, options?)` — watched callback pattern with lazy lifecycle
|
|
234
234
|
- Same `Collection` interface — `.get()`, `.byKey()`, `.keys()`, `.deriveCollection()`
|
|
235
235
|
|
|
236
236
|
```typescript
|
|
@@ -263,6 +263,7 @@ const processed = todoList
|
|
|
263
263
|
**Memo (`createMemo`)**:
|
|
264
264
|
- Synchronous derived computations with memoization
|
|
265
265
|
- Reducer pattern with previous value access
|
|
266
|
+
- Optional `watched(invalidate)` callback for lazy external invalidation (e.g., MutationObserver)
|
|
266
267
|
|
|
267
268
|
```typescript
|
|
268
269
|
const doubled = createMemo(() => count.get() * 2)
|
|
@@ -283,6 +284,7 @@ const runningTotal = createMemo(prev => prev + currentValue.get(), { value: 0 })
|
|
|
283
284
|
|
|
284
285
|
**Task (`createTask`)**:
|
|
285
286
|
- Async computations with automatic cancellation
|
|
287
|
+
- Optional `watched(invalidate)` callback for lazy external invalidation
|
|
286
288
|
|
|
287
289
|
```typescript
|
|
288
290
|
const userData = createTask(async (prev, abort) => {
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Cause & Effect
|
|
2
2
|
|
|
3
|
-
Version 0.18.
|
|
3
|
+
Version 0.18.1
|
|
4
4
|
|
|
5
5
|
**Cause & Effect** is a reactive state management primitives library for TypeScript. It provides the foundational building blocks for managing complex, dynamic, composite, and asynchronous state — correctly and performantly — in a unified signal graph.
|
|
6
6
|
|
|
@@ -266,7 +266,7 @@ Lists have `.keys()`, `.add()`, and `.remove()` methods like stores. Additionall
|
|
|
266
266
|
|
|
267
267
|
### Collection
|
|
268
268
|
|
|
269
|
-
A reactive collection with item-level memoization. Collections can be externally-driven (via a
|
|
269
|
+
A reactive collection with item-level memoization. Collections can be externally-driven (via a watched callback) or derived from a List or another Collection.
|
|
270
270
|
|
|
271
271
|
**Externally-driven collections** receive data from external sources (WebSocket, Server-Sent Events, etc.) via `applyChanges()`:
|
|
272
272
|
|
|
@@ -277,7 +277,7 @@ const items = createCollection((applyChanges) => {
|
|
|
277
277
|
const ws = new WebSocket('/items')
|
|
278
278
|
ws.onmessage = (e) => {
|
|
279
279
|
const { add, change, remove } = JSON.parse(e.data)
|
|
280
|
-
applyChanges({
|
|
280
|
+
applyChanges({ add, change, remove })
|
|
281
281
|
}
|
|
282
282
|
return () => ws.close()
|
|
283
283
|
}, { keyConfig: item => item.id })
|
|
@@ -285,7 +285,7 @@ const items = createCollection((applyChanges) => {
|
|
|
285
285
|
createEffect(() => console.log('Items:', items.get()))
|
|
286
286
|
```
|
|
287
287
|
|
|
288
|
-
The
|
|
288
|
+
The watched callback activates lazily when the collection is first accessed by an effect and cleans up when no effects are watching. Options include `value` for initial items (default `[]`) and `keyConfig` for key generation.
|
|
289
289
|
|
|
290
290
|
**Derived collections** transform Lists or other Collections via `.deriveCollection()`:
|
|
291
291
|
|
|
@@ -476,7 +476,7 @@ dispose() // Cleans up the effect and runs the returned cleanup
|
|
|
476
476
|
|
|
477
477
|
### Resource Management with Watch Callbacks
|
|
478
478
|
|
|
479
|
-
Sensor and Collection signals use a **
|
|
479
|
+
Sensor and Collection signals use a **watched callback** for lazy resource management. The callback runs when the signal is first accessed by an effect and the returned cleanup function runs when no effects are watching:
|
|
480
480
|
|
|
481
481
|
```js
|
|
482
482
|
import { createSensor, createCollection, createEffect } from '@zeix/cause-effect'
|
|
@@ -517,11 +517,29 @@ const user = createStore({ name: 'Alice' }, {
|
|
|
517
517
|
})
|
|
518
518
|
```
|
|
519
519
|
|
|
520
|
+
Memo and Task signals also support a `watched` option, but their callback receives an `invalidate` function that marks the signal dirty and triggers recomputation:
|
|
521
|
+
|
|
522
|
+
```js
|
|
523
|
+
const changes = createMemo((prev) => {
|
|
524
|
+
const next = new Set(parent.querySelectorAll(selector))
|
|
525
|
+
// ... diff prev vs next ...
|
|
526
|
+
return { current: next, added, removed }
|
|
527
|
+
}, {
|
|
528
|
+
value: { current: new Set(), added: [], removed: [] },
|
|
529
|
+
watched: (invalidate) => {
|
|
530
|
+
const observer = new MutationObserver(() => invalidate())
|
|
531
|
+
observer.observe(parent, { childList: true, subtree: true })
|
|
532
|
+
return () => observer.disconnect()
|
|
533
|
+
}
|
|
534
|
+
})
|
|
535
|
+
```
|
|
536
|
+
|
|
520
537
|
This pattern is ideal for:
|
|
521
538
|
- Event listeners that should only be active when data is being watched
|
|
522
539
|
- Network connections that can be lazily established
|
|
523
540
|
- Expensive computations that should pause when not needed
|
|
524
541
|
- External subscriptions (WebSocket, Server-Sent Events, etc.)
|
|
542
|
+
- Computed signals that need to react to external events (DOM mutations, timers)
|
|
525
543
|
|
|
526
544
|
## Contributing & License
|
|
527
545
|
|
package/context7.json
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { createSensor, isSensor, type Sensor } from '..'
|
|
2
|
+
|
|
3
|
+
/* === Types === */
|
|
4
|
+
|
|
5
|
+
type ReservedWords =
|
|
6
|
+
| 'constructor'
|
|
7
|
+
| 'prototype'
|
|
8
|
+
| '__proto__'
|
|
9
|
+
| 'toString'
|
|
10
|
+
| 'valueOf'
|
|
11
|
+
| 'hasOwnProperty'
|
|
12
|
+
| 'isPrototypeOf'
|
|
13
|
+
| 'propertyIsEnumerable'
|
|
14
|
+
| 'toLocaleString'
|
|
15
|
+
|
|
16
|
+
type ComponentProp = Exclude<string, keyof HTMLElement | ReservedWords>
|
|
17
|
+
type ComponentProps = Record<ComponentProp, NonNullable<unknown>>
|
|
18
|
+
|
|
19
|
+
type Component<P extends ComponentProps> = HTMLElement & P
|
|
20
|
+
|
|
21
|
+
type UI = Record<string, Element | Sensor<Element[]>>
|
|
22
|
+
|
|
23
|
+
type EventType<K extends string> = K extends keyof HTMLElementEventMap
|
|
24
|
+
? HTMLElementEventMap[K]
|
|
25
|
+
: Event
|
|
26
|
+
|
|
27
|
+
type EventHandler<
|
|
28
|
+
T extends {},
|
|
29
|
+
Evt extends Event,
|
|
30
|
+
U extends UI,
|
|
31
|
+
E extends Element,
|
|
32
|
+
> = (context: {
|
|
33
|
+
event: Evt
|
|
34
|
+
ui: U
|
|
35
|
+
target: E
|
|
36
|
+
prev: T
|
|
37
|
+
}) => T | void | Promise<void>
|
|
38
|
+
|
|
39
|
+
type EventHandlers<T extends {}, U extends UI, E extends Element> = {
|
|
40
|
+
[K in keyof HTMLElementEventMap]?: EventHandler<T, EventType<K>, U, E>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type ElementFromKey<U extends UI, K extends keyof U> = NonNullable<
|
|
44
|
+
U[K] extends Sensor<infer E extends Element>
|
|
45
|
+
? E
|
|
46
|
+
: U[K] extends Element
|
|
47
|
+
? U[K]
|
|
48
|
+
: never
|
|
49
|
+
>
|
|
50
|
+
|
|
51
|
+
type Parser<T extends {}, U extends UI> = (
|
|
52
|
+
ui: U,
|
|
53
|
+
value: string | null | undefined,
|
|
54
|
+
old?: string | null,
|
|
55
|
+
) => T
|
|
56
|
+
|
|
57
|
+
type Reader<T extends {}, U extends UI> = (ui: U) => T
|
|
58
|
+
type Fallback<T extends {}, U extends UI> = T | Reader<T, U>
|
|
59
|
+
|
|
60
|
+
type ParserOrFallback<T extends {}, U extends UI> =
|
|
61
|
+
| Parser<T, U>
|
|
62
|
+
| Fallback<T, U>
|
|
63
|
+
|
|
64
|
+
/* === Internal === */
|
|
65
|
+
|
|
66
|
+
const pendingElements = new Set<Element>()
|
|
67
|
+
const tasks = new WeakMap<Element, () => void>()
|
|
68
|
+
let requestId: number | undefined
|
|
69
|
+
|
|
70
|
+
const runTasks = () => {
|
|
71
|
+
requestId = undefined
|
|
72
|
+
const elements = Array.from(pendingElements)
|
|
73
|
+
pendingElements.clear()
|
|
74
|
+
for (const element of elements) tasks.get(element)?.()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const requestTick = () => {
|
|
78
|
+
if (requestId) cancelAnimationFrame(requestId)
|
|
79
|
+
requestId = requestAnimationFrame(runTasks)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const schedule = (element: Element, task: () => void) => {
|
|
83
|
+
tasks.set(element, task)
|
|
84
|
+
pendingElements.add(element)
|
|
85
|
+
requestTick()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// High-frequency events that are passive by default and should be scheduled
|
|
89
|
+
const PASSIVE_EVENTS = new Set([
|
|
90
|
+
'scroll',
|
|
91
|
+
'resize',
|
|
92
|
+
'mousewheel',
|
|
93
|
+
'touchstart',
|
|
94
|
+
'touchmove',
|
|
95
|
+
'wheel',
|
|
96
|
+
])
|
|
97
|
+
|
|
98
|
+
const isReader = <T extends {}, U extends UI>(
|
|
99
|
+
value: unknown,
|
|
100
|
+
): value is Reader<T, U> => typeof value === 'function'
|
|
101
|
+
|
|
102
|
+
const getFallback = <T extends {}, U extends UI>(
|
|
103
|
+
ui: U,
|
|
104
|
+
fallback: ParserOrFallback<T, U>,
|
|
105
|
+
): T => (isReader<T, U>(fallback) ? fallback(ui) : (fallback as T))
|
|
106
|
+
|
|
107
|
+
/* === Exported Functions === */
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Produce an event-driven sensor from transformed event data
|
|
111
|
+
*
|
|
112
|
+
* @since 0.16.0
|
|
113
|
+
* @param {S} key - name of UI key
|
|
114
|
+
* @param {ParserOrFallback<T>} init - Initial value, reader or parser
|
|
115
|
+
* @param {EventHandlers<T, ElementFromSelector<S>, C>} events - Transformation functions for events
|
|
116
|
+
* @returns {Extractor<Sensor<T>, C>} Extractor function for value from event
|
|
117
|
+
*/
|
|
118
|
+
const createEventsSensor =
|
|
119
|
+
<T extends {}, P extends ComponentProps, U extends UI, K extends keyof U>(
|
|
120
|
+
init: ParserOrFallback<T, U>,
|
|
121
|
+
key: K,
|
|
122
|
+
events: EventHandlers<T, U, ElementFromKey<U, K>>,
|
|
123
|
+
): ((ui: U & { host: Component<P> }) => Sensor<T>) =>
|
|
124
|
+
(ui: U & { host: Component<P> }) => {
|
|
125
|
+
const { host } = ui
|
|
126
|
+
let value: T = getFallback(ui, init)
|
|
127
|
+
const targets = isSensor<ElementFromKey<U, K>[]>(ui[key])
|
|
128
|
+
? ui[key].get()
|
|
129
|
+
: [ui[key] as ElementFromKey<U & { host: Component<P> }, K>]
|
|
130
|
+
const eventMap = new Map<string, EventListener>()
|
|
131
|
+
|
|
132
|
+
const getTarget = (
|
|
133
|
+
eventTarget: Node,
|
|
134
|
+
): ElementFromKey<U, K> | undefined => {
|
|
135
|
+
for (const t of targets)
|
|
136
|
+
if (t.contains(eventTarget)) return t as ElementFromKey<U, K>
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return createSensor<T>(
|
|
140
|
+
set => {
|
|
141
|
+
for (const [type, handler] of Object.entries(events)) {
|
|
142
|
+
const options = { passive: PASSIVE_EVENTS.has(type) }
|
|
143
|
+
const listener = (e: Event) => {
|
|
144
|
+
const eventTarget = e.target as Node
|
|
145
|
+
if (!eventTarget) return
|
|
146
|
+
const target = getTarget(eventTarget)
|
|
147
|
+
if (!target) return
|
|
148
|
+
e.stopPropagation()
|
|
149
|
+
|
|
150
|
+
const task = () => {
|
|
151
|
+
try {
|
|
152
|
+
const next = handler({
|
|
153
|
+
event: e as any,
|
|
154
|
+
ui,
|
|
155
|
+
target,
|
|
156
|
+
prev: value,
|
|
157
|
+
})
|
|
158
|
+
if (next == null || next instanceof Promise)
|
|
159
|
+
return
|
|
160
|
+
if (!Object.is(next, value)) {
|
|
161
|
+
value = next
|
|
162
|
+
set(next)
|
|
163
|
+
}
|
|
164
|
+
} catch (error) {
|
|
165
|
+
e.stopImmediatePropagation()
|
|
166
|
+
throw error
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (options.passive) schedule(host, task)
|
|
170
|
+
else task()
|
|
171
|
+
}
|
|
172
|
+
eventMap.set(type, listener)
|
|
173
|
+
host.addEventListener(type, listener, options)
|
|
174
|
+
}
|
|
175
|
+
return () => {
|
|
176
|
+
if (eventMap.size) {
|
|
177
|
+
for (const [type, listener] of eventMap)
|
|
178
|
+
host.removeEventListener(type, listener)
|
|
179
|
+
eventMap.clear()
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
{ value },
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export { createEventsSensor, type EventHandler, type EventHandlers }
|