@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/types/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * @name Cause & Effect
3
- * @version 0.18.3
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';
@@ -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
- 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_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
@@ -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 }