@zeix/cause-effect 0.18.3 → 0.18.5
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/ARCHITECTURE.md +8 -8
- package/CHANGELOG.md +22 -0
- package/CLAUDE.md +36 -397
- package/OWNERSHIP_BUG.md +95 -0
- package/README.md +1 -1
- package/index.dev.js +18 -10
- package/index.js +1 -1
- package/index.ts +2 -1
- package/package.json +1 -1
- package/src/graph.ts +26 -4
- package/src/nodes/memo.ts +1 -3
- package/src/nodes/task.ts +1 -3
- package/test/effect.test.ts +204 -0
- package/test/unown.test.ts +179 -0
- package/types/index.d.ts +2 -2
- package/types/src/graph.d.ts +12 -1
- package/.ai-context.md +0 -281
- package/examples/events-sensor.ts +0 -187
- package/examples/selector-sensor.ts +0 -173
package/types/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @name Cause & Effect
|
|
3
|
-
* @version 0.18.
|
|
3
|
+
* @version 0.18.5
|
|
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';
|
package/types/src/graph.d.ts
CHANGED
|
@@ -119,6 +119,7 @@ declare const TYPE_COLLECTION = "Collection";
|
|
|
119
119
|
declare const TYPE_STORE = "Store";
|
|
120
120
|
declare const TYPE_SLOT = "Slot";
|
|
121
121
|
declare const FLAG_CLEAN = 0;
|
|
122
|
+
declare const FLAG_CHECK: number;
|
|
122
123
|
declare const FLAG_DIRTY: number;
|
|
123
124
|
declare const FLAG_RELINK: number;
|
|
124
125
|
declare let activeSink: SinkNode | null;
|
|
@@ -217,4 +218,14 @@ declare function untrack<T>(fn: () => T): T;
|
|
|
217
218
|
* ```
|
|
218
219
|
*/
|
|
219
220
|
declare function createScope(fn: () => MaybeCleanup): Cleanup;
|
|
220
|
-
|
|
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
|
|
@@ -1,187 +0,0 @@
|
|
|
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 }
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import { createMemo, type Memo } from '..'
|
|
2
|
-
|
|
3
|
-
/* === Types === */
|
|
4
|
-
|
|
5
|
-
// Split a comma-separated selector into individual selectors
|
|
6
|
-
type SplitByComma<S extends string> = S extends `${infer First},${infer Rest}`
|
|
7
|
-
? [TrimWhitespace<First>, ...SplitByComma<Rest>]
|
|
8
|
-
: [TrimWhitespace<S>]
|
|
9
|
-
|
|
10
|
-
// Trim leading/trailing whitespace from a string
|
|
11
|
-
type TrimWhitespace<S extends string> = S extends ` ${infer Rest}`
|
|
12
|
-
? TrimWhitespace<Rest>
|
|
13
|
-
: S extends `${infer Rest} `
|
|
14
|
-
? TrimWhitespace<Rest>
|
|
15
|
-
: S
|
|
16
|
-
|
|
17
|
-
// Extract the rightmost selector part from combinator selectors (space, >, +, ~)
|
|
18
|
-
type ExtractRightmostSelector<S extends string> =
|
|
19
|
-
S extends `${string} ${infer Rest}`
|
|
20
|
-
? ExtractRightmostSelector<Rest>
|
|
21
|
-
: S extends `${string}>${infer Rest}`
|
|
22
|
-
? ExtractRightmostSelector<Rest>
|
|
23
|
-
: S extends `${string}+${infer Rest}`
|
|
24
|
-
? ExtractRightmostSelector<Rest>
|
|
25
|
-
: S extends `${string}~${infer Rest}`
|
|
26
|
-
? ExtractRightmostSelector<Rest>
|
|
27
|
-
: S
|
|
28
|
-
|
|
29
|
-
// Extract tag name from a simple selector (without combinators)
|
|
30
|
-
type ExtractTagFromSimpleSelector<S extends string> =
|
|
31
|
-
S extends `${infer T}.${string}`
|
|
32
|
-
? T
|
|
33
|
-
: S extends `${infer T}#${string}`
|
|
34
|
-
? T
|
|
35
|
-
: S extends `${infer T}:${string}`
|
|
36
|
-
? T
|
|
37
|
-
: S extends `${infer T}[${string}`
|
|
38
|
-
? T
|
|
39
|
-
: S
|
|
40
|
-
|
|
41
|
-
// Main extraction logic for a single selector
|
|
42
|
-
type ExtractTag<S extends string> = ExtractTagFromSimpleSelector<
|
|
43
|
-
ExtractRightmostSelector<S>
|
|
44
|
-
>
|
|
45
|
-
|
|
46
|
-
// Normalize to lowercase and ensure it's a known HTML tag
|
|
47
|
-
type KnownTag<S extends string> =
|
|
48
|
-
Lowercase<ExtractTag<S>> extends
|
|
49
|
-
| keyof HTMLElementTagNameMap
|
|
50
|
-
| keyof SVGElementTagNameMap
|
|
51
|
-
| keyof MathMLElementTagNameMap
|
|
52
|
-
? Lowercase<ExtractTag<S>>
|
|
53
|
-
: never
|
|
54
|
-
|
|
55
|
-
// Get element type from a single selector
|
|
56
|
-
type ElementFromSingleSelector<S extends string> =
|
|
57
|
-
KnownTag<S> extends never
|
|
58
|
-
? HTMLElement
|
|
59
|
-
: KnownTag<S> extends keyof HTMLElementTagNameMap
|
|
60
|
-
? HTMLElementTagNameMap[KnownTag<S>]
|
|
61
|
-
: KnownTag<S> extends keyof SVGElementTagNameMap
|
|
62
|
-
? SVGElementTagNameMap[KnownTag<S>]
|
|
63
|
-
: KnownTag<S> extends keyof MathMLElementTagNameMap
|
|
64
|
-
? MathMLElementTagNameMap[KnownTag<S>]
|
|
65
|
-
: HTMLElement
|
|
66
|
-
|
|
67
|
-
// Map a tuple of selectors to a union of their element types
|
|
68
|
-
type ElementsFromSelectorArray<Selectors extends readonly string[]> = {
|
|
69
|
-
[K in keyof Selectors]: Selectors[K] extends string
|
|
70
|
-
? ElementFromSingleSelector<Selectors[K]>
|
|
71
|
-
: never
|
|
72
|
-
}[number]
|
|
73
|
-
|
|
74
|
-
// Main type: handle both single selectors and comma-separated selectors
|
|
75
|
-
type ElementFromSelector<S extends string> = S extends `${string},${string}`
|
|
76
|
-
? ElementsFromSelectorArray<SplitByComma<S>>
|
|
77
|
-
: ElementFromSingleSelector<S>
|
|
78
|
-
|
|
79
|
-
type ElementChanges<E extends Element> = {
|
|
80
|
-
current: Set<E>
|
|
81
|
-
added: E[]
|
|
82
|
-
removed: E[]
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/* === Internal Functions === */
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Extract attribute names from a CSS selector
|
|
89
|
-
* Handles various attribute selector formats: .class, #id, [attr], [attr=value], [attr^=value], etc.
|
|
90
|
-
*
|
|
91
|
-
* @param {string} selector - CSS selector to parse
|
|
92
|
-
* @returns {string[]} - Array of attribute names found in the selector
|
|
93
|
-
*/
|
|
94
|
-
const extractAttributes = (selector: string): string[] => {
|
|
95
|
-
const attributes = new Set<string>()
|
|
96
|
-
if (selector.includes('.')) attributes.add('class')
|
|
97
|
-
if (selector.includes('#')) attributes.add('id')
|
|
98
|
-
if (selector.includes('[')) {
|
|
99
|
-
const parts = selector.split('[')
|
|
100
|
-
for (let i = 1; i < parts.length; i++) {
|
|
101
|
-
const part = parts[i]
|
|
102
|
-
if (!part.includes(']')) continue
|
|
103
|
-
const attrName = part
|
|
104
|
-
.split('=')[0]
|
|
105
|
-
.trim()
|
|
106
|
-
.replace(/[^a-zA-Z0-9_-]/g, '')
|
|
107
|
-
if (attrName) attributes.add(attrName)
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return [...attributes]
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/* === Exported Functions === */
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Observe changes to elements matching a CSS selector.
|
|
117
|
-
* Returns a Memo that tracks which elements were added and removed.
|
|
118
|
-
* The MutationObserver is lazily activated when an effect first reads
|
|
119
|
-
* the memo, and disconnected when no effects are watching.
|
|
120
|
-
*
|
|
121
|
-
* @since 0.16.0
|
|
122
|
-
* @param parent - The parent node to search within
|
|
123
|
-
* @param selector - The CSS selector to match elements
|
|
124
|
-
* @returns A Memo of element changes (current set, added, removed)
|
|
125
|
-
*/
|
|
126
|
-
function observeSelectorChanges<S extends string>(
|
|
127
|
-
parent: ParentNode,
|
|
128
|
-
selector: S,
|
|
129
|
-
): Memo<ElementChanges<ElementFromSelector<S>>>
|
|
130
|
-
function observeSelectorChanges<E extends Element>(
|
|
131
|
-
parent: ParentNode,
|
|
132
|
-
selector: string,
|
|
133
|
-
): Memo<ElementChanges<E>>
|
|
134
|
-
function observeSelectorChanges<S extends string>(
|
|
135
|
-
parent: ParentNode,
|
|
136
|
-
selector: S,
|
|
137
|
-
): Memo<ElementChanges<ElementFromSelector<S>>> {
|
|
138
|
-
type E = ElementFromSelector<S>
|
|
139
|
-
|
|
140
|
-
return createMemo(
|
|
141
|
-
(prev: ElementChanges<E>) => {
|
|
142
|
-
const next = new Set(
|
|
143
|
-
Array.from(parent.querySelectorAll<E>(selector)),
|
|
144
|
-
)
|
|
145
|
-
const added: E[] = []
|
|
146
|
-
const removed: E[] = []
|
|
147
|
-
|
|
148
|
-
for (const el of next) if (!prev.current.has(el)) added.push(el)
|
|
149
|
-
for (const el of prev.current) if (!next.has(el)) removed.push(el)
|
|
150
|
-
|
|
151
|
-
return { current: next, added, removed }
|
|
152
|
-
},
|
|
153
|
-
{
|
|
154
|
-
value: { current: new Set<E>(), added: [], removed: [] },
|
|
155
|
-
watched: invalidate => {
|
|
156
|
-
const observerConfig: MutationObserverInit = {
|
|
157
|
-
childList: true,
|
|
158
|
-
subtree: true,
|
|
159
|
-
}
|
|
160
|
-
const observedAttributes = extractAttributes(selector)
|
|
161
|
-
if (observedAttributes.length) {
|
|
162
|
-
observerConfig.attributes = true
|
|
163
|
-
observerConfig.attributeFilter = observedAttributes
|
|
164
|
-
}
|
|
165
|
-
const observer = new MutationObserver(() => invalidate())
|
|
166
|
-
observer.observe(parent, observerConfig)
|
|
167
|
-
return () => observer.disconnect()
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
|
-
)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export { observeSelectorChanges, type ElementChanges }
|