aeon-dom 0.1.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/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # aeon-dom
2
+
3
+ DOM event sources and continuous-time browser Behaviors for [Aeon](https://github.com/joshburgess/aeon).
4
+
5
+ This package provides:
6
+
7
+ - **`fromDOMEvent(type, target, options?)`** — convert any DOM `EventTarget` event into an `Event<E, never>`
8
+ - **`animationFrames(scheduler)`** — `Event<DOMHighResTimeStamp, never>` driven by `requestAnimationFrame`
9
+ - **`mousePosition(scheduler)`** — `Behavior<{x, y}, never>` tracking the cursor
10
+ - **`windowSize(scheduler)`** — `Behavior<{width, height}, never>` tracking viewport size
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pnpm add aeon-core aeon-scheduler aeon-dom
16
+ ```
17
+
18
+ ## Quick Start
19
+
20
+ ```typescript
21
+ import { observe } from "aeon-core";
22
+ import { DefaultScheduler } from "aeon-scheduler";
23
+ import { fromDOMEvent } from "aeon-dom";
24
+
25
+ const scheduler = new DefaultScheduler();
26
+
27
+ const clicks = fromDOMEvent("click", document.body);
28
+ observe((e) => console.log("clicked at", e.clientX, e.clientY), clicks, scheduler);
29
+ ```
30
+
31
+ ### Continuous Behaviors
32
+
33
+ ```typescript
34
+ import { mousePosition, windowSize } from "aeon-dom";
35
+ import { liftA2B, readBehavior } from "aeon-core";
36
+ import { toTime } from "aeon-types";
37
+
38
+ const mouse = mousePosition(scheduler);
39
+ const size = windowSize(scheduler);
40
+
41
+ const relativeX = liftA2B((m, s) => m.x / s.width, mouse, size);
42
+ console.log(readBehavior(relativeX, toTime(performance.now())));
43
+ ```
44
+
45
+ ## Documentation
46
+
47
+ - [Main README](https://github.com/joshburgess/aeon#readme)
48
+ - [Getting Started](https://github.com/joshburgess/aeon/blob/main/docs/getting-started.md)
49
+
50
+ ## License
51
+
52
+ MIT
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Animation frame Event source.
3
+ *
4
+ * Emits a DOMHighResTimeStamp on each requestAnimationFrame callback.
5
+ */
6
+ import type { Event as PulseEvent } from "aeon-types";
7
+ /**
8
+ * An Event that emits a DOMHighResTimeStamp on each animation frame.
9
+ *
10
+ * Denotation: `[(t, timestamp) | each requestAnimationFrame callback]`
11
+ *
12
+ * Cancels the animation frame loop when disposed.
13
+ */
14
+ export declare const animationFrames: PulseEvent<DOMHighResTimeStamp, never>;
15
+ //# sourceMappingURL=animationFrames.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"animationFrames.d.ts","sourceRoot":"","sources":["../src/animationFrames.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAc,KAAK,IAAI,UAAU,EAA2B,MAAM,YAAY,CAAC;AA2B3F;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,EAAE,UAAU,CAAC,mBAAmB,EAAE,KAAK,CAC9B,CAAC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * DOM Behaviors — continuous time-varying values derived from the DOM.
3
+ *
4
+ * These create Behaviors that are push-updated from DOM events and
5
+ * pull-sampled when read. Each returns [Behavior, Disposable] since
6
+ * they subscribe to DOM events internally.
7
+ */
8
+ import type { Behavior, Disposable, Scheduler } from "aeon-types";
9
+ /** 2D point for mouse coordinates. */
10
+ export interface Point {
11
+ readonly x: number;
12
+ readonly y: number;
13
+ }
14
+ /** Dimensions for window size. */
15
+ export interface Size {
16
+ readonly width: number;
17
+ readonly height: number;
18
+ }
19
+ /**
20
+ * A Behavior holding the current mouse position.
21
+ *
22
+ * Denotation: `t => { x: mouseX(t), y: mouseY(t) }`
23
+ *
24
+ * Push-updated from mousemove events on the given target (defaults to document).
25
+ * Returns [Behavior, Disposable] — dispose to stop listening.
26
+ */
27
+ export declare const mousePosition: (scheduler: Scheduler, target?: EventTarget) => [Behavior<Point, never>, Disposable];
28
+ /**
29
+ * A Behavior holding the current window dimensions.
30
+ *
31
+ * Denotation: `t => { width: innerWidth(t), height: innerHeight(t) }`
32
+ *
33
+ * Push-updated from resize events on window.
34
+ * Returns [Behavior, Disposable] — dispose to stop listening.
35
+ */
36
+ export declare const windowSize: (scheduler: Scheduler) => [Behavior<Size, never>, Disposable];
37
+ //# sourceMappingURL=behaviors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"behaviors.d.ts","sourceRoot":"","sources":["../src/behaviors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAGlE,sCAAsC;AACtC,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,kCAAkC;AAClC,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,GACxB,WAAW,SAAS,EACpB,SAAQ,WAAsB,KAC7B,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,UAAU,CAMrC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,GAAI,WAAW,SAAS,KAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,UAAU,CAWnF,CAAC"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * DOM Event sources.
3
+ *
4
+ * Creates pulse Event streams from DOM EventTarget events.
5
+ */
6
+ import type { Event as PulseEvent } from "aeon-types";
7
+ /**
8
+ * Create a pulse Event from a DOM event.
9
+ *
10
+ * Denotation: `[(t, domEvent) | domEvent fires on target at time t]`
11
+ *
12
+ * Automatically removes the event listener when disposed.
13
+ */
14
+ export declare function fromDOMEvent<K extends keyof HTMLElementEventMap>(type: K, target: HTMLElement, options?: AddEventListenerOptions): PulseEvent<HTMLElementEventMap[K], never>;
15
+ export declare function fromDOMEvent<K extends keyof WindowEventMap>(type: K, target: Window, options?: AddEventListenerOptions): PulseEvent<WindowEventMap[K], never>;
16
+ export declare function fromDOMEvent<K extends keyof DocumentEventMap>(type: K, target: Document, options?: AddEventListenerOptions): PulseEvent<DocumentEventMap[K], never>;
17
+ export declare function fromDOMEvent(type: string, target: EventTarget, options?: AddEventListenerOptions): PulseEvent<Event, never>;
18
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAc,KAAK,IAAI,UAAU,EAA2B,MAAM,YAAY,CAAC;AA2B3F;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,MAAM,mBAAmB,EAC9D,IAAI,EAAE,CAAC,EACP,MAAM,EAAE,WAAW,EACnB,OAAO,CAAC,EAAE,uBAAuB,GAChC,UAAU,CAAC,mBAAmB,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;AAC7C,wBAAgB,YAAY,CAAC,CAAC,SAAS,MAAM,cAAc,EACzD,IAAI,EAAE,CAAC,EACP,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,uBAAuB,GAChC,UAAU,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;AACxC,wBAAgB,YAAY,CAAC,CAAC,SAAS,MAAM,gBAAgB,EAC3D,IAAI,EAAE,CAAC,EACP,MAAM,EAAE,QAAQ,EAChB,OAAO,CAAC,EAAE,uBAAuB,GAChC,UAAU,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;AAC1C,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,WAAW,EACnB,OAAO,CAAC,EAAE,uBAAuB,GAChC,UAAU,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC"}
package/dist/index.cjs ADDED
@@ -0,0 +1,346 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Internal helpers for aeon-dom.
5
+ *
6
+ * At runtime, Event<A, E> IS Source<A, E> — the opaque type brand is
7
+ * purely type-level. This mirrors the zero-cost identity cast in aeon-core.
8
+ */ /** Create an opaque Event from a Source. Zero-cost identity cast. */ const createEvent = (source)=>source;
9
+
10
+ let DOMEventSource = class DOMEventSource {
11
+ constructor(type, target, options){
12
+ this.type = type;
13
+ this.target = target;
14
+ this.options = options;
15
+ }
16
+ run(sink, scheduler) {
17
+ const handler = (e)=>{
18
+ sink.event(scheduler.currentTime(), e);
19
+ };
20
+ this.target.addEventListener(this.type, handler, this.options);
21
+ return {
22
+ dispose: ()=>{
23
+ this.target.removeEventListener(this.type, handler, this.options);
24
+ }
25
+ };
26
+ }
27
+ };
28
+ function fromDOMEvent(type, target, options) {
29
+ return createEvent(new DOMEventSource(type, target, options));
30
+ }
31
+
32
+ let AnimationFrameSource = class AnimationFrameSource {
33
+ run(sink, scheduler) {
34
+ let id = 0;
35
+ let disposed = false;
36
+ const tick = (timestamp)=>{
37
+ if (disposed) return;
38
+ sink.event(scheduler.currentTime(), timestamp);
39
+ id = requestAnimationFrame(tick);
40
+ };
41
+ id = requestAnimationFrame(tick);
42
+ return {
43
+ dispose () {
44
+ disposed = true;
45
+ cancelAnimationFrame(id);
46
+ }
47
+ };
48
+ }
49
+ };
50
+ const ANIMATION_FRAME_SOURCE = new AnimationFrameSource();
51
+ /**
52
+ * An Event that emits a DOMHighResTimeStamp on each animation frame.
53
+ *
54
+ * Denotation: `[(t, timestamp) | each requestAnimationFrame callback]`
55
+ *
56
+ * Cancels the animation frame loop when disposed.
57
+ */ const animationFrames = createEvent(ANIMATION_FRAME_SOURCE);
58
+
59
+ /** A no-op disposable. */ const disposeNone = {
60
+ dispose () {}
61
+ };
62
+ /** Create an opaque Event from a Source. Zero-cost identity cast. */ const _createEvent = (source)=>source;
63
+ /** Extract the Source from an opaque Event. Zero-cost identity cast. */ const _getSource = (event)=>event;
64
+ // --- Source classes for V8 hidden class stability ---
65
+ let EmptySource = class EmptySource {
66
+ constructor(){
67
+ this._sync = true;
68
+ }
69
+ run(sink, scheduler) {
70
+ sink.end(scheduler.currentTime());
71
+ return disposeNone;
72
+ }
73
+ syncIterate(_emit) {}
74
+ };
75
+ let NowSource = class NowSource {
76
+ constructor(value){
77
+ this.value = value;
78
+ this._sync = true;
79
+ }
80
+ run(sink, scheduler) {
81
+ const t = scheduler.currentTime();
82
+ sink.event(t, this.value);
83
+ sink.end(t);
84
+ return disposeNone;
85
+ }
86
+ syncIterate(emit) {
87
+ emit(this.value);
88
+ }
89
+ };
90
+ // --- Singletons for empty/never ---
91
+ const EMPTY_SOURCE = new EmptySource();
92
+ /**
93
+ * Pipe base class for sinks that forward error/end unchanged.
94
+ *
95
+ * With ES2022 target, `extends Pipe` compiles to native `class extends` —
96
+ * zero helpers, zero overhead. V8 devirtualizes the shared error/end
97
+ * methods across all sink subtypes.
98
+ */ let Pipe = class Pipe {
99
+ constructor(sink){
100
+ this.sink = sink;
101
+ }
102
+ error(time, err) {
103
+ this.sink.error(time, err);
104
+ }
105
+ end(time) {
106
+ this.sink.end(time);
107
+ }
108
+ };
109
+ // --- Sink classes ---
110
+ /** Map sink: applies f to each value. */ let MapSink = class MapSink extends Pipe {
111
+ constructor(f, sink){
112
+ super(sink);
113
+ this.f = f;
114
+ }
115
+ event(time, value) {
116
+ const f = this.f;
117
+ this.sink.event(time, f(value));
118
+ }
119
+ };
120
+ /** Filter sink: only forwards values that pass the predicate. */ let FilterSink = class FilterSink extends Pipe {
121
+ constructor(predicate, sink){
122
+ super(sink);
123
+ this.predicate = predicate;
124
+ }
125
+ event(time, value) {
126
+ const p = this.predicate;
127
+ if (p(value)) {
128
+ this.sink.event(time, value);
129
+ }
130
+ }
131
+ };
132
+ /** Combined filter+map sink: filter then map in one node. */ let FilterMapSink = class FilterMapSink extends Pipe {
133
+ constructor(predicate, f, sink){
134
+ super(sink);
135
+ this.predicate = predicate;
136
+ this.f = f;
137
+ }
138
+ event(time, value) {
139
+ const p = this.predicate;
140
+ if (p(value)) {
141
+ const f = this.f;
142
+ this.sink.event(time, f(value));
143
+ }
144
+ }
145
+ };
146
+ /** Combined map+filter sink: map then filter in one node. */ let MapFilterSink = class MapFilterSink extends Pipe {
147
+ constructor(f, predicate, sink){
148
+ super(sink);
149
+ this.f = f;
150
+ this.predicate = predicate;
151
+ }
152
+ event(time, value) {
153
+ const f = this.f;
154
+ const mapped = f(value);
155
+ const p = this.predicate;
156
+ if (p(mapped)) {
157
+ this.sink.event(time, mapped);
158
+ }
159
+ }
160
+ };
161
+ // --- Source classes (for instanceof fusion detection) ---
162
+ /** A map source, tagged for fusion detection via instanceof. */ let MapSource = class MapSource {
163
+ constructor(f, source){
164
+ this.f = f;
165
+ this.source = source;
166
+ }
167
+ run(sink, scheduler) {
168
+ return this.source.run(new MapSink(this.f, sink), scheduler);
169
+ }
170
+ /** Factory with fusion and algebraic simplification. */ static create(f, source) {
171
+ // map(f, empty()) → empty()
172
+ if (source instanceof EmptySource) {
173
+ return EMPTY_SOURCE;
174
+ }
175
+ // map(f, now(x)) → now(f(x)) — constant folding
176
+ if (source instanceof NowSource) {
177
+ return new NowSource(f(source.value));
178
+ }
179
+ // map(f, map(g, s)) → map(f∘g, s)
180
+ if (source instanceof MapSource) {
181
+ const inner = source;
182
+ return new MapSource((x)=>f(inner.f(x)), inner.source);
183
+ }
184
+ // map(f, filter(p, s)) → filterMap(p, f, s)
185
+ if (source instanceof FilterSource) {
186
+ const inner = source;
187
+ return new FilterMapSource(inner.predicate, f, inner.source);
188
+ }
189
+ return new MapSource(f, source);
190
+ }
191
+ };
192
+ /** A filter source, tagged for fusion detection via instanceof. */ let FilterSource = class FilterSource {
193
+ constructor(predicate, source){
194
+ this.predicate = predicate;
195
+ this.source = source;
196
+ }
197
+ run(sink, scheduler) {
198
+ return this.source.run(new FilterSink(this.predicate, sink), scheduler);
199
+ }
200
+ /** Factory with fusion and algebraic simplification. */ static create(predicate, source) {
201
+ // filter(p, empty()) → empty()
202
+ if (source instanceof EmptySource) {
203
+ return EMPTY_SOURCE;
204
+ }
205
+ // filter(p, now(x)) → p(x) ? now(x) : empty() — constant folding
206
+ if (source instanceof NowSource) {
207
+ const val = source.value;
208
+ return predicate(val) ? source : EMPTY_SOURCE;
209
+ }
210
+ // filter(p, filter(q, s)) → filter(x => q(x) && p(x), s)
211
+ if (source instanceof FilterSource) {
212
+ const inner = source;
213
+ return new FilterSource((x)=>inner.predicate(x) && predicate(x), inner.source);
214
+ }
215
+ // filter(p, map(f, s)) → mapFilter(f, p, s)
216
+ if (source instanceof MapSource) {
217
+ const inner = source;
218
+ return new MapFilterSource(inner.f, predicate, inner.source);
219
+ }
220
+ return new FilterSource(predicate, source);
221
+ }
222
+ };
223
+ /** Fused filter-then-map source. */ let FilterMapSource = class FilterMapSource {
224
+ constructor(predicate, f, source){
225
+ this.predicate = predicate;
226
+ this.f = f;
227
+ this.source = source;
228
+ }
229
+ run(sink, scheduler) {
230
+ return this.source.run(new FilterMapSink(this.predicate, this.f, sink), scheduler);
231
+ }
232
+ };
233
+ /** Fused map-then-filter source. */ let MapFilterSource = class MapFilterSource {
234
+ constructor(f, predicate, source){
235
+ this.f = f;
236
+ this.predicate = predicate;
237
+ this.source = source;
238
+ }
239
+ run(sink, scheduler) {
240
+ return this.source.run(new MapFilterSink(this.f, this.predicate, sink), scheduler);
241
+ }
242
+ };
243
+ // --- Public API ---
244
+ /**
245
+ * Create a fusible map Event. Detects map∘map and composes functions.
246
+ */ const fusedMap = (f, event)=>{
247
+ const source = _getSource(event);
248
+ return _createEvent(MapSource.create(f, source));
249
+ };
250
+ /**
251
+ * Transform each value in an Event stream.
252
+ *
253
+ * Denotation: `map(f, e) = [(t, f(v)) | (t, v) ∈ e]`
254
+ */ const map$1 = (f, event)=>fusedMap(f, event);
255
+ // --- Internal tag ---
256
+ const BEHAVIOR_KEY = Symbol("pulse/behavior");
257
+ /** Create an opaque Behavior from a BehaviorImpl. Internal use only. */ const _createBehavior = (impl)=>({
258
+ [BEHAVIOR_KEY]: impl
259
+ });
260
+ // --- Stepper subscription ---
261
+ /**
262
+ * Subscribe a stepper behavior to an event stream.
263
+ * Returns a Disposable to unsubscribe.
264
+ */ const subscribeStepperToEvent = (stepperImpl, event, scheduler)=>{
265
+ const source = _getSource(event);
266
+ const sink = {
267
+ event (time, value) {
268
+ stepperImpl.value = value;
269
+ stepperImpl.time = time;
270
+ stepperImpl.generation++;
271
+ },
272
+ error () {},
273
+ end () {}
274
+ };
275
+ return source.run(sink, scheduler);
276
+ };
277
+ // --- Event ↔ Behavior Bridge ---
278
+ /**
279
+ * Create a Behavior that holds the latest value from an Event.
280
+ *
281
+ * Denotation: `stepper(init, e) = t => latestValue(e, t) ?? init`
282
+ *
283
+ * This is the primary push→pull bridge. The returned behavior is
284
+ * push-updated when the event fires, and pull-sampled when read.
285
+ *
286
+ * IMPORTANT: The caller must provide a scheduler to subscribe to the event.
287
+ * Returns [Behavior, Disposable] — the disposable unsubscribes from the event.
288
+ */ const stepper = (initial, event, scheduler)=>{
289
+ const impl = {
290
+ tag: "stepper",
291
+ initial,
292
+ value: initial,
293
+ time: scheduler.currentTime(),
294
+ generation: 0
295
+ };
296
+ const disposable = subscribeStepperToEvent(impl, event, scheduler);
297
+ return [
298
+ _createBehavior(impl),
299
+ disposable
300
+ ];
301
+ };
302
+
303
+ /**
304
+ * A Behavior holding the current mouse position.
305
+ *
306
+ * Denotation: `t => { x: mouseX(t), y: mouseY(t) }`
307
+ *
308
+ * Push-updated from mousemove events on the given target (defaults to document).
309
+ * Returns [Behavior, Disposable] — dispose to stop listening.
310
+ */ const mousePosition = (scheduler, target = document)=>{
311
+ const moves = map$1((e)=>({
312
+ x: e.clientX,
313
+ y: e.clientY
314
+ }), fromDOMEvent("mousemove", target));
315
+ return stepper({
316
+ x: 0,
317
+ y: 0
318
+ }, moves, scheduler);
319
+ };
320
+ /**
321
+ * A Behavior holding the current window dimensions.
322
+ *
323
+ * Denotation: `t => { width: innerWidth(t), height: innerHeight(t) }`
324
+ *
325
+ * Push-updated from resize events on window.
326
+ * Returns [Behavior, Disposable] — dispose to stop listening.
327
+ */ const windowSize = (scheduler)=>{
328
+ const initial = typeof window !== "undefined" ? {
329
+ width: window.innerWidth,
330
+ height: window.innerHeight
331
+ } : {
332
+ width: 0,
333
+ height: 0
334
+ };
335
+ const resizes = map$1(()=>({
336
+ width: window.innerWidth,
337
+ height: window.innerHeight
338
+ }), fromDOMEvent("resize", window));
339
+ return stepper(initial, resizes, scheduler);
340
+ };
341
+
342
+ exports.animationFrames = animationFrames;
343
+ exports.fromDOMEvent = fromDOMEvent;
344
+ exports.mousePosition = mousePosition;
345
+ exports.windowSize = windowSize;
346
+ //# sourceMappingURL=index.cjs.map