what-core 0.1.0 → 0.2.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/dist/reactive.js CHANGED
@@ -1,124 +1,167 @@
1
+ // What Framework - Reactive Primitives
2
+ // Signals + Effects: fine-grained reactivity without virtual DOM overhead
3
+
1
4
  let currentEffect = null;
2
5
  let batchDepth = 0;
3
6
  let pendingEffects = new Set();
7
+
8
+ // --- Signal ---
9
+ // A reactive value. Reading inside an effect auto-tracks the dependency.
10
+ // Writing triggers only the effects that depend on this signal.
11
+
4
12
  export function signal(initial) {
5
- let value = initial;
6
- const subs = new Set();
7
- function read() {
8
- if (currentEffect) {
9
- subs.add(currentEffect);
10
- currentEffect.deps.add(subs);
11
- }
12
- return value;
13
- }
14
- read.set = (next) => {
15
- const nextVal = typeof next === 'function' ? next(value) : next;
16
- if (Object.is(value, nextVal)) return;
17
- value = nextVal;
18
- notify(subs);
19
- };
20
- read.peek = () => value;
21
- read.subscribe = (fn) => {
22
- return effect(() => fn(read()));
23
- };
24
- read._signal = true;
25
- return read;
26
- }
13
+ let value = initial;
14
+ const subs = new Set();
15
+
16
+ function read() {
17
+ if (currentEffect) {
18
+ subs.add(currentEffect);
19
+ currentEffect.deps.add(subs); // Track reverse dep for cleanup
20
+ }
21
+ return value;
22
+ }
23
+
24
+ read.set = (next) => {
25
+ const nextVal = typeof next === 'function' ? next(value) : next;
26
+ if (Object.is(value, nextVal)) return;
27
+ value = nextVal;
28
+ notify(subs);
29
+ };
30
+
31
+ read.peek = () => value;
32
+
33
+ read.subscribe = (fn) => {
34
+ return effect(() => fn(read()));
35
+ };
36
+
37
+ read._signal = true;
38
+ return read;
39
+ }
40
+
41
+ // --- Computed ---
42
+ // Derived signal. Lazy: only recomputes when a dependency changes AND it's read.
43
+
27
44
  export function computed(fn) {
28
- let value, dirty = true;
29
- const subs = new Set();
30
- const inner = _createEffect(() => {
31
- value = fn();
32
- dirty = false;
33
- notify(subs);
34
- }, { lazy: true });
35
- function read() {
36
- if (currentEffect) {
37
- subs.add(currentEffect);
38
- currentEffect.deps.add(subs);
39
- }
40
- if (dirty) _runEffect(inner);
41
- return value;
42
- }
43
- inner._onNotify = () => { dirty = true; };
44
- read._signal = true;
45
- read.peek = () => {
46
- if (dirty) _runEffect(inner);
47
- return value;
48
- };
49
- return read;
50
- }
45
+ let value, dirty = true;
46
+ const subs = new Set();
47
+
48
+ const inner = _createEffect(() => {
49
+ value = fn();
50
+ dirty = false;
51
+ notify(subs);
52
+ }, { lazy: true });
53
+
54
+ function read() {
55
+ if (currentEffect) {
56
+ subs.add(currentEffect);
57
+ currentEffect.deps.add(subs);
58
+ }
59
+ if (dirty) _runEffect(inner);
60
+ return value;
61
+ }
62
+
63
+ inner._onNotify = () => { dirty = true; };
64
+
65
+ read._signal = true;
66
+ read.peek = () => {
67
+ if (dirty) _runEffect(inner);
68
+ return value;
69
+ };
70
+
71
+ return read;
72
+ }
73
+
74
+ // --- Effect ---
75
+ // Runs a function, auto-tracking signal reads. Re-runs when deps change.
76
+ // Returns a dispose function.
77
+
51
78
  export function effect(fn) {
52
- const e = _createEffect(fn);
53
- _runEffect(e);
54
- return () => _disposeEffect(e);
55
- }
79
+ const e = _createEffect(fn);
80
+ _runEffect(e);
81
+ return () => _disposeEffect(e);
82
+ }
83
+
84
+ // --- Batch ---
85
+ // Group multiple signal writes; effects run once at the end.
86
+
56
87
  export function batch(fn) {
57
- batchDepth++;
58
- try {
59
- fn();
60
- } finally {
61
- batchDepth--;
62
- if (batchDepth === 0) flush();
63
- }
64
- }
88
+ batchDepth++;
89
+ try {
90
+ fn();
91
+ } finally {
92
+ batchDepth--;
93
+ if (batchDepth === 0) flush();
94
+ }
95
+ }
96
+
97
+ // --- Internals ---
98
+
65
99
  function _createEffect(fn, opts = {}) {
66
- return {
67
- fn,
68
- deps: new Set(),
69
- lazy: opts.lazy || false,
70
- _onNotify: null,
71
- disposed: false,
72
- };
73
- }
100
+ return {
101
+ fn,
102
+ deps: new Set(), // subscriber sets this effect belongs to
103
+ lazy: opts.lazy || false,
104
+ _onNotify: null,
105
+ disposed: false,
106
+ };
107
+ }
108
+
74
109
  function _runEffect(e) {
75
- if (e.disposed) return;
76
- cleanup(e);
77
- const prev = currentEffect;
78
- currentEffect = e;
79
- try {
80
- e.fn();
81
- } finally {
82
- currentEffect = prev;
83
- }
84
- }
110
+ if (e.disposed) return;
111
+ cleanup(e);
112
+ const prev = currentEffect;
113
+ currentEffect = e;
114
+ try {
115
+ e.fn();
116
+ } finally {
117
+ currentEffect = prev;
118
+ }
119
+ }
120
+
85
121
  function _disposeEffect(e) {
86
- e.disposed = true;
87
- cleanup(e);
122
+ e.disposed = true;
123
+ cleanup(e);
88
124
  }
125
+
89
126
  function cleanup(e) {
90
- for (const dep of e.deps) dep.delete(e);
91
- e.deps.clear();
127
+ for (const dep of e.deps) dep.delete(e);
128
+ e.deps.clear();
92
129
  }
130
+
93
131
  function notify(subs) {
94
- const snapshot = [...subs];
95
- for (const e of snapshot) {
96
- if (e.disposed) continue;
97
- if (e._onNotify) {
98
- e._onNotify();
99
- if (batchDepth > 0) pendingEffects.add(e);
100
- continue;
101
- }
102
- if (batchDepth > 0) {
103
- pendingEffects.add(e);
104
- } else {
105
- _runEffect(e);
106
- }
107
- }
108
- }
132
+ // Snapshot to avoid infinite loop when effects re-subscribe during run
133
+ const snapshot = [...subs];
134
+ for (const e of snapshot) {
135
+ if (e.disposed) continue;
136
+ if (e._onNotify) {
137
+ e._onNotify();
138
+ if (batchDepth > 0) pendingEffects.add(e);
139
+ continue;
140
+ }
141
+ if (batchDepth > 0) {
142
+ pendingEffects.add(e);
143
+ } else {
144
+ _runEffect(e);
145
+ }
146
+ }
147
+ }
148
+
109
149
  function flush() {
110
- const effects = [...pendingEffects];
111
- pendingEffects.clear();
112
- for (const e of effects) {
113
- if (!e.disposed && !e._onNotify) _runEffect(e);
114
- }
115
- }
150
+ const effects = [...pendingEffects];
151
+ pendingEffects.clear();
152
+ for (const e of effects) {
153
+ if (!e.disposed && !e._onNotify) _runEffect(e);
154
+ }
155
+ }
156
+
157
+ // --- Untrack ---
158
+ // Read signals without subscribing
116
159
  export function untrack(fn) {
117
- const prev = currentEffect;
118
- currentEffect = null;
119
- try {
120
- return fn();
121
- } finally {
122
- currentEffect = prev;
160
+ const prev = currentEffect;
161
+ currentEffect = null;
162
+ try {
163
+ return fn();
164
+ } finally {
165
+ currentEffect = prev;
166
+ }
123
167
  }
124
- }
@@ -0,0 +1,241 @@
1
+ // What Framework - DOM Scheduler
2
+ // Batches DOM reads and writes to prevent layout thrashing.
3
+ // Inspired by fastdom but integrated with our reactive system.
4
+
5
+ // Queue phases: reads run first, then writes
6
+ const readQueue = [];
7
+ const writeQueue = [];
8
+ let scheduled = false;
9
+
10
+ // --- Schedule a DOM read operation ---
11
+ // Reads should be batched together and run before writes
12
+ // to avoid forced synchronous layouts.
13
+ //
14
+ // Example:
15
+ // scheduleRead(() => {
16
+ // const height = element.offsetHeight; // Read
17
+ // scheduleWrite(() => {
18
+ // element.style.height = height + 'px'; // Write
19
+ // });
20
+ // });
21
+
22
+ export function scheduleRead(fn) {
23
+ readQueue.push(fn);
24
+ schedule();
25
+ return () => {
26
+ const idx = readQueue.indexOf(fn);
27
+ if (idx !== -1) readQueue.splice(idx, 1);
28
+ };
29
+ }
30
+
31
+ // --- Schedule a DOM write operation ---
32
+ // Writes are batched and run after all reads complete.
33
+
34
+ export function scheduleWrite(fn) {
35
+ writeQueue.push(fn);
36
+ schedule();
37
+ return () => {
38
+ const idx = writeQueue.indexOf(fn);
39
+ if (idx !== -1) writeQueue.splice(idx, 1);
40
+ };
41
+ }
42
+
43
+ // --- Flush all queued operations immediately ---
44
+ // Useful when you need synchronous DOM access.
45
+
46
+ export function flushScheduler() {
47
+ // Run all reads first
48
+ while (readQueue.length > 0) {
49
+ const fn = readQueue.shift();
50
+ try { fn(); } catch (e) { console.error('[what] Scheduler read error:', e); }
51
+ }
52
+
53
+ // Then all writes
54
+ while (writeQueue.length > 0) {
55
+ const fn = writeQueue.shift();
56
+ try { fn(); } catch (e) { console.error('[what] Scheduler write error:', e); }
57
+ }
58
+
59
+ scheduled = false;
60
+ }
61
+
62
+ // --- Internal scheduling ---
63
+
64
+ function schedule() {
65
+ if (scheduled) return;
66
+ scheduled = true;
67
+ requestAnimationFrame(flushScheduler);
68
+ }
69
+
70
+ // --- Measure helper ---
71
+ // Read a layout property without causing thrashing.
72
+ // Returns a promise that resolves with the value.
73
+
74
+ export function measure(fn) {
75
+ return new Promise(resolve => {
76
+ scheduleRead(() => {
77
+ resolve(fn());
78
+ });
79
+ });
80
+ }
81
+
82
+ // --- Mutate helper ---
83
+ // Write to DOM without causing thrashing.
84
+ // Returns a promise that resolves when the write is done.
85
+
86
+ export function mutate(fn) {
87
+ return new Promise(resolve => {
88
+ scheduleWrite(() => {
89
+ fn();
90
+ resolve();
91
+ });
92
+ });
93
+ }
94
+
95
+ // --- useScheduledEffect ---
96
+ // Effect that automatically batches DOM operations.
97
+
98
+ import { effect } from './reactive.js';
99
+
100
+ export function useScheduledEffect(readFn, writeFn) {
101
+ return effect(() => {
102
+ scheduleRead(() => {
103
+ const data = readFn();
104
+ if (writeFn) {
105
+ scheduleWrite(() => writeFn(data));
106
+ }
107
+ });
108
+ });
109
+ }
110
+
111
+ // --- Animation frame helper ---
112
+ // Like requestAnimationFrame but returns a cancellable promise.
113
+
114
+ export function nextFrame() {
115
+ let cancel;
116
+ const promise = new Promise((resolve, reject) => {
117
+ const id = requestAnimationFrame(resolve);
118
+ cancel = () => {
119
+ cancelAnimationFrame(id);
120
+ reject(new Error('Cancelled'));
121
+ };
122
+ });
123
+ promise.cancel = cancel;
124
+ return promise;
125
+ }
126
+
127
+ // --- Debounced RAF ---
128
+ // Only runs the latest callback once per frame.
129
+
130
+ const debouncedCallbacks = new Map();
131
+
132
+ export function raf(key, fn) {
133
+ if (debouncedCallbacks.has(key)) {
134
+ // Replace callback, don't schedule new frame
135
+ debouncedCallbacks.set(key, fn);
136
+ } else {
137
+ debouncedCallbacks.set(key, fn);
138
+ requestAnimationFrame(() => {
139
+ const callback = debouncedCallbacks.get(key);
140
+ debouncedCallbacks.delete(key);
141
+ if (callback) callback();
142
+ });
143
+ }
144
+ }
145
+
146
+ // --- Resize Observer helper ---
147
+ // Batched resize observations.
148
+
149
+ const resizeObservers = new WeakMap();
150
+ let sharedResizeObserver = null;
151
+
152
+ export function onResize(element, callback) {
153
+ if (typeof ResizeObserver === 'undefined') {
154
+ // Fallback: just call once
155
+ callback(element.getBoundingClientRect());
156
+ return () => {};
157
+ }
158
+
159
+ if (!sharedResizeObserver) {
160
+ sharedResizeObserver = new ResizeObserver(entries => {
161
+ scheduleRead(() => {
162
+ for (const entry of entries) {
163
+ const cb = resizeObservers.get(entry.target);
164
+ if (cb) {
165
+ cb(entry.contentRect);
166
+ }
167
+ }
168
+ });
169
+ });
170
+ }
171
+
172
+ resizeObservers.set(element, callback);
173
+ sharedResizeObserver.observe(element);
174
+
175
+ return () => {
176
+ resizeObservers.delete(element);
177
+ sharedResizeObserver.unobserve(element);
178
+ };
179
+ }
180
+
181
+ // --- Intersection Observer helper ---
182
+ // Batched intersection observations.
183
+
184
+ export function onIntersect(element, callback, options = {}) {
185
+ if (typeof IntersectionObserver === 'undefined') {
186
+ // Fallback: assume visible
187
+ callback({ isIntersecting: true, intersectionRatio: 1 });
188
+ return () => {};
189
+ }
190
+
191
+ const observer = new IntersectionObserver(entries => {
192
+ scheduleRead(() => {
193
+ for (const entry of entries) {
194
+ callback(entry);
195
+ }
196
+ });
197
+ }, options);
198
+
199
+ observer.observe(element);
200
+
201
+ return () => observer.disconnect();
202
+ }
203
+
204
+ // --- Smooth scrolling with scheduler ---
205
+
206
+ export function smoothScrollTo(element, options = {}) {
207
+ const { duration = 300, easing = t => t * (2 - t) } = options;
208
+
209
+ return new Promise(resolve => {
210
+ let startY;
211
+ let targetY;
212
+ let startTime;
213
+
214
+ scheduleRead(() => {
215
+ startY = window.scrollY;
216
+ const rect = element.getBoundingClientRect();
217
+ targetY = startY + rect.top;
218
+ startTime = performance.now();
219
+ tick();
220
+ });
221
+
222
+ function tick() {
223
+ scheduleRead(() => {
224
+ const elapsed = performance.now() - startTime;
225
+ const progress = Math.min(elapsed / duration, 1);
226
+ const easedProgress = easing(progress);
227
+ const currentY = startY + (targetY - startY) * easedProgress;
228
+
229
+ scheduleWrite(() => {
230
+ window.scrollTo(0, currentY);
231
+
232
+ if (progress < 1) {
233
+ requestAnimationFrame(tick);
234
+ } else {
235
+ resolve();
236
+ }
237
+ });
238
+ });
239
+ }
240
+ });
241
+ }