@zeix/cause-effect 0.18.4 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +1 -1
- package/CHANGELOG.md +18 -0
- package/CLAUDE.md +36 -397
- package/README.md +1 -1
- package/bench/reactivity.bench.ts +18 -7
- package/biome.json +1 -1
- package/index.dev.js +21 -5
- package/index.js +1 -1
- package/index.ts +2 -1
- package/package.json +3 -4
- package/skills/cause-effect-dev/SKILL.md +114 -0
- package/skills/cause-effect-dev/agents/openai.yaml +4 -0
- package/src/graph.ts +26 -4
- package/src/nodes/collection.ts +4 -2
- package/src/nodes/list.ts +5 -2
- package/test/benchmark.test.ts +25 -11
- package/test/collection.test.ts +6 -3
- package/test/effect.test.ts +2 -1
- package/test/list.test.ts +8 -4
- package/test/regression.test.ts +4 -2
- package/test/store.test.ts +8 -4
- package/test/unown.test.ts +179 -0
- package/test/util/dependency-graph.ts +12 -6
- package/tsconfig.json +5 -1
- package/types/index.d.ts +2 -2
- package/types/src/graph.d.ts +13 -3
- package/.ai-context.md +0 -281
- package/examples/events-sensor.ts +0 -187
- package/examples/selector-sensor.ts +0 -173
|
@@ -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 }
|