what-core 0.6.1 → 0.6.3
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 +2 -0
- package/compiler.d.ts +30 -0
- package/devtools.d.ts +2 -0
- package/dist/compiler.js +1787 -0
- package/dist/compiler.js.map +7 -0
- package/dist/compiler.min.js +2 -0
- package/dist/compiler.min.js.map +7 -0
- package/dist/devtools.js +10 -0
- package/dist/devtools.js.map +7 -0
- package/dist/devtools.min.js +2 -0
- package/dist/devtools.min.js.map +7 -0
- package/dist/index.js +331 -382
- package/dist/index.js.map +4 -4
- package/dist/index.min.js +62 -62
- package/dist/index.min.js.map +4 -4
- package/dist/render.js +263 -21
- package/dist/render.js.map +4 -4
- package/dist/render.min.js +58 -1
- package/dist/render.min.js.map +4 -4
- package/dist/testing.js +3 -0
- package/dist/testing.js.map +2 -2
- package/dist/testing.min.js +1 -1
- package/dist/testing.min.js.map +2 -2
- package/index.d.ts +176 -1
- package/jsx-runtime.d.ts +622 -0
- package/package.json +20 -2
- package/src/agent-context.js +1 -1
- package/src/compiler.js +18 -0
- package/src/components.js +73 -27
- package/src/devtools.js +4 -0
- package/src/dom.js +7 -0
- package/src/guardrails.js +3 -4
- package/src/hooks.js +0 -11
- package/src/index.js +5 -9
- package/src/render.js +94 -24
- package/dist/a11y.js +0 -440
- package/dist/animation.js +0 -548
- package/dist/components.js +0 -229
- package/dist/data.js +0 -638
- package/dist/dom.js +0 -439
- package/dist/form.js +0 -509
- package/dist/h.js +0 -152
- package/dist/head.js +0 -51
- package/dist/helpers.js +0 -140
- package/dist/hooks.js +0 -210
- package/dist/reactive.js +0 -432
- package/dist/scheduler.js +0 -246
- package/dist/skeleton.js +0 -363
- package/dist/store.js +0 -83
- package/dist/what.js +0 -117
package/dist/reactive.js
DELETED
|
@@ -1,432 +0,0 @@
|
|
|
1
|
-
export const __DEV__ = typeof process !== 'undefined'
|
|
2
|
-
? process.env?.NODE_ENV !== 'production'
|
|
3
|
-
: true;
|
|
4
|
-
export let __devtools = null;
|
|
5
|
-
export function __setDevToolsHooks(hooks) {
|
|
6
|
-
if (__DEV__) __devtools = hooks;
|
|
7
|
-
}
|
|
8
|
-
let currentEffect = null;
|
|
9
|
-
let currentRoot = null;
|
|
10
|
-
let currentOwner = null;
|
|
11
|
-
let batchDepth = 0;
|
|
12
|
-
let pendingEffects = [];
|
|
13
|
-
const subSetOwner = new WeakMap();
|
|
14
|
-
const NEEDS_UPSTREAM = Symbol('needs_upstream');
|
|
15
|
-
let iterativeEvalStack = null;
|
|
16
|
-
export function signal(initial, debugName) {
|
|
17
|
-
let value = initial;
|
|
18
|
-
const subs = new Set();
|
|
19
|
-
function sig(...args) {
|
|
20
|
-
if (args.length === 0) {
|
|
21
|
-
if (currentEffect) {
|
|
22
|
-
subs.add(currentEffect);
|
|
23
|
-
currentEffect.deps.push(subs);
|
|
24
|
-
}
|
|
25
|
-
return value;
|
|
26
|
-
}
|
|
27
|
-
const nextVal = typeof args[0] === 'function' ? args[0](value) : args[0];
|
|
28
|
-
if (Object.is(value, nextVal)) return;
|
|
29
|
-
value = nextVal;
|
|
30
|
-
if (__DEV__ && __devtools) __devtools.onSignalUpdate(sig);
|
|
31
|
-
notify(subs);
|
|
32
|
-
}
|
|
33
|
-
sig.set = (next) => {
|
|
34
|
-
const nextVal = typeof next === 'function' ? next(value) : next;
|
|
35
|
-
if (Object.is(value, nextVal)) return;
|
|
36
|
-
value = nextVal;
|
|
37
|
-
if (__DEV__ && __devtools) __devtools.onSignalUpdate(sig);
|
|
38
|
-
notify(subs);
|
|
39
|
-
};
|
|
40
|
-
sig.peek = () => value;
|
|
41
|
-
sig.subscribe = (fn) => {
|
|
42
|
-
return effect(() => fn(sig()));
|
|
43
|
-
};
|
|
44
|
-
sig._signal = true;
|
|
45
|
-
if (__DEV__) {
|
|
46
|
-
sig._subs = subs;
|
|
47
|
-
if (debugName) sig._debugName = debugName;
|
|
48
|
-
}
|
|
49
|
-
if (__DEV__ && __devtools) __devtools.onSignalCreate(sig);
|
|
50
|
-
return sig;
|
|
51
|
-
}
|
|
52
|
-
export function computed(fn) {
|
|
53
|
-
let value, dirty = true;
|
|
54
|
-
const subs = new Set();
|
|
55
|
-
const inner = _createEffect(() => {
|
|
56
|
-
value = fn();
|
|
57
|
-
dirty = false;
|
|
58
|
-
}, true);
|
|
59
|
-
inner._level = 1;
|
|
60
|
-
inner._computed = true;
|
|
61
|
-
inner._computedSubs = subs;
|
|
62
|
-
subSetOwner.set(subs, inner);
|
|
63
|
-
inner._markDirty = () => { dirty = true; };
|
|
64
|
-
inner._isDirty = () => dirty;
|
|
65
|
-
function read() {
|
|
66
|
-
if (currentEffect) {
|
|
67
|
-
subs.add(currentEffect);
|
|
68
|
-
currentEffect.deps.push(subs);
|
|
69
|
-
}
|
|
70
|
-
if (dirty) _evaluateComputed(inner);
|
|
71
|
-
return value;
|
|
72
|
-
}
|
|
73
|
-
inner._onNotify = () => {
|
|
74
|
-
dirty = true;
|
|
75
|
-
notify(subs);
|
|
76
|
-
};
|
|
77
|
-
read._signal = true;
|
|
78
|
-
read.peek = () => {
|
|
79
|
-
if (dirty) _evaluateComputed(inner);
|
|
80
|
-
return value;
|
|
81
|
-
};
|
|
82
|
-
return read;
|
|
83
|
-
}
|
|
84
|
-
function _evaluateComputed(computedEffect) {
|
|
85
|
-
if (iterativeEvalStack !== null) {
|
|
86
|
-
iterativeEvalStack.push(computedEffect);
|
|
87
|
-
throw NEEDS_UPSTREAM;
|
|
88
|
-
}
|
|
89
|
-
const stack = [computedEffect];
|
|
90
|
-
iterativeEvalStack = stack;
|
|
91
|
-
try {
|
|
92
|
-
while (stack.length > 0) {
|
|
93
|
-
const current = stack[stack.length - 1];
|
|
94
|
-
if (!current._isDirty || !current._isDirty()) {
|
|
95
|
-
stack.pop();
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
try {
|
|
99
|
-
_runEffect(current);
|
|
100
|
-
_updateLevel(current);
|
|
101
|
-
stack.pop();
|
|
102
|
-
} catch (err) {
|
|
103
|
-
if (err === NEEDS_UPSTREAM) {
|
|
104
|
-
current._markDirty();
|
|
105
|
-
} else {
|
|
106
|
-
throw err;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
} finally {
|
|
111
|
-
iterativeEvalStack = null;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
function _updateLevel(e) {
|
|
115
|
-
let maxDepLevel = 0;
|
|
116
|
-
for (let i = 0; i < e.deps.length; i++) {
|
|
117
|
-
const owner = subSetOwner.get(e.deps[i]);
|
|
118
|
-
const depLevel = owner ? owner._level : 0;
|
|
119
|
-
if (depLevel > maxDepLevel) maxDepLevel = depLevel;
|
|
120
|
-
}
|
|
121
|
-
e._level = maxDepLevel + 1;
|
|
122
|
-
}
|
|
123
|
-
export function effect(fn, opts) {
|
|
124
|
-
const e = _createEffect(fn);
|
|
125
|
-
e._level = 1;
|
|
126
|
-
const prev = currentEffect;
|
|
127
|
-
currentEffect = e;
|
|
128
|
-
try {
|
|
129
|
-
const result = e.fn();
|
|
130
|
-
if (typeof result === 'function') e._cleanup = result;
|
|
131
|
-
} finally {
|
|
132
|
-
currentEffect = prev;
|
|
133
|
-
}
|
|
134
|
-
_updateEffectLevel(e);
|
|
135
|
-
if (opts?.stable) e._stable = true;
|
|
136
|
-
const dispose = () => _disposeEffect(e);
|
|
137
|
-
if (currentRoot) {
|
|
138
|
-
currentRoot.disposals.push(dispose);
|
|
139
|
-
}
|
|
140
|
-
return dispose;
|
|
141
|
-
}
|
|
142
|
-
function _updateEffectLevel(e) {
|
|
143
|
-
let maxDepLevel = 0;
|
|
144
|
-
for (let i = 0; i < e.deps.length; i++) {
|
|
145
|
-
const owner = subSetOwner.get(e.deps[i]);
|
|
146
|
-
const depLevel = owner ? owner._level : 0;
|
|
147
|
-
if (depLevel > maxDepLevel) maxDepLevel = depLevel;
|
|
148
|
-
}
|
|
149
|
-
e._level = maxDepLevel + 1;
|
|
150
|
-
}
|
|
151
|
-
export function batch(fn) {
|
|
152
|
-
batchDepth++;
|
|
153
|
-
try {
|
|
154
|
-
fn();
|
|
155
|
-
} finally {
|
|
156
|
-
batchDepth--;
|
|
157
|
-
if (batchDepth === 0) flush();
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
function _createEffect(fn, lazy) {
|
|
161
|
-
const e = {
|
|
162
|
-
fn,
|
|
163
|
-
deps: [],
|
|
164
|
-
lazy: lazy || false,
|
|
165
|
-
_onNotify: null,
|
|
166
|
-
disposed: false,
|
|
167
|
-
_pending: false,
|
|
168
|
-
_stable: false,
|
|
169
|
-
_level: 0,
|
|
170
|
-
_computed: false,
|
|
171
|
-
_computedSubs: null,
|
|
172
|
-
_isDirty: null,
|
|
173
|
-
_markDirty: null,
|
|
174
|
-
};
|
|
175
|
-
if (__DEV__ && __devtools) __devtools.onEffectCreate(e);
|
|
176
|
-
return e;
|
|
177
|
-
}
|
|
178
|
-
function _runEffect(e) {
|
|
179
|
-
if (e.disposed) return;
|
|
180
|
-
if (e._stable) {
|
|
181
|
-
if (e._cleanup) {
|
|
182
|
-
try { e._cleanup(); } catch (err) {
|
|
183
|
-
if (__DEV__) console.warn('[what] Error in effect cleanup:', err);
|
|
184
|
-
}
|
|
185
|
-
e._cleanup = null;
|
|
186
|
-
}
|
|
187
|
-
const prev = currentEffect;
|
|
188
|
-
currentEffect = null;
|
|
189
|
-
try {
|
|
190
|
-
const result = e.fn();
|
|
191
|
-
if (typeof result === 'function') e._cleanup = result;
|
|
192
|
-
} catch (err) {
|
|
193
|
-
if (__devtools?.onError) __devtools.onError(err, { type: 'effect', effect: e });
|
|
194
|
-
if (__DEV__) console.warn('[what] Error in stable effect:', err);
|
|
195
|
-
} finally {
|
|
196
|
-
currentEffect = prev;
|
|
197
|
-
}
|
|
198
|
-
if (__DEV__ && __devtools?.onEffectRun) __devtools.onEffectRun(e);
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
cleanup(e);
|
|
202
|
-
if (e._cleanup) {
|
|
203
|
-
try { e._cleanup(); } catch (err) {
|
|
204
|
-
if (__devtools?.onError) __devtools.onError(err, { type: 'effect-cleanup', effect: e });
|
|
205
|
-
if (__DEV__) console.warn('[what] Error in effect cleanup:', err);
|
|
206
|
-
}
|
|
207
|
-
e._cleanup = null;
|
|
208
|
-
}
|
|
209
|
-
const prev = currentEffect;
|
|
210
|
-
currentEffect = e;
|
|
211
|
-
try {
|
|
212
|
-
const result = e.fn();
|
|
213
|
-
if (typeof result === 'function') {
|
|
214
|
-
e._cleanup = result;
|
|
215
|
-
}
|
|
216
|
-
} catch (err) {
|
|
217
|
-
if (err === NEEDS_UPSTREAM) throw err;
|
|
218
|
-
if (__devtools?.onError) __devtools.onError(err, { type: 'effect', effect: e });
|
|
219
|
-
throw err;
|
|
220
|
-
} finally {
|
|
221
|
-
currentEffect = prev;
|
|
222
|
-
}
|
|
223
|
-
if (__DEV__ && __devtools?.onEffectRun) __devtools.onEffectRun(e);
|
|
224
|
-
}
|
|
225
|
-
function _disposeEffect(e) {
|
|
226
|
-
e.disposed = true;
|
|
227
|
-
if (__DEV__ && __devtools) __devtools.onEffectDispose(e);
|
|
228
|
-
cleanup(e);
|
|
229
|
-
if (e._cleanup) {
|
|
230
|
-
try { e._cleanup(); } catch (err) {
|
|
231
|
-
if (__DEV__) console.warn('[what] Error in effect cleanup on dispose:', err);
|
|
232
|
-
}
|
|
233
|
-
e._cleanup = null;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
function cleanup(e) {
|
|
237
|
-
const deps = e.deps;
|
|
238
|
-
for (let i = 0; i < deps.length; i++) deps[i].delete(e);
|
|
239
|
-
deps.length = 0;
|
|
240
|
-
}
|
|
241
|
-
let notifyQueue = null;
|
|
242
|
-
function notify(subs) {
|
|
243
|
-
const isOutermost = notifyQueue === null;
|
|
244
|
-
if (isOutermost) notifyQueue = [];
|
|
245
|
-
notifyQueue.push(subs);
|
|
246
|
-
if (!isOutermost) return;
|
|
247
|
-
try {
|
|
248
|
-
while (notifyQueue.length > 0) {
|
|
249
|
-
const currentSubs = notifyQueue.shift();
|
|
250
|
-
for (const e of currentSubs) {
|
|
251
|
-
if (e.disposed) continue;
|
|
252
|
-
if (e._onNotify) {
|
|
253
|
-
e._onNotify();
|
|
254
|
-
} else if (batchDepth === 0 && e._stable) {
|
|
255
|
-
const prev = currentEffect;
|
|
256
|
-
currentEffect = null;
|
|
257
|
-
try {
|
|
258
|
-
const result = e.fn();
|
|
259
|
-
if (typeof result === 'function') {
|
|
260
|
-
if (e._cleanup) try { e._cleanup(); } catch (err) {}
|
|
261
|
-
e._cleanup = result;
|
|
262
|
-
}
|
|
263
|
-
} catch (err) {
|
|
264
|
-
if (__devtools?.onError) __devtools.onError(err, { type: 'effect', effect: e });
|
|
265
|
-
if (__DEV__) console.warn('[what] Error in stable effect:', err);
|
|
266
|
-
} finally {
|
|
267
|
-
currentEffect = prev;
|
|
268
|
-
}
|
|
269
|
-
} else if (!e._pending) {
|
|
270
|
-
e._pending = true;
|
|
271
|
-
pendingEffects.push(e);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
} finally {
|
|
276
|
-
notifyQueue = null;
|
|
277
|
-
}
|
|
278
|
-
if (batchDepth === 0 && pendingEffects.length > 0) scheduleMicrotask();
|
|
279
|
-
}
|
|
280
|
-
let microtaskScheduled = false;
|
|
281
|
-
function scheduleMicrotask() {
|
|
282
|
-
if (!microtaskScheduled) {
|
|
283
|
-
microtaskScheduled = true;
|
|
284
|
-
queueMicrotask(() => {
|
|
285
|
-
microtaskScheduled = false;
|
|
286
|
-
flush();
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
function flush() {
|
|
291
|
-
let iterations = 0;
|
|
292
|
-
while (pendingEffects.length > 0 && iterations < 25) {
|
|
293
|
-
const batch = pendingEffects;
|
|
294
|
-
pendingEffects = [];
|
|
295
|
-
batch.sort((a, b) => a._level - b._level);
|
|
296
|
-
for (let i = 0; i < batch.length; i++) {
|
|
297
|
-
const e = batch[i];
|
|
298
|
-
e._pending = false;
|
|
299
|
-
if (!e.disposed && !e._onNotify) {
|
|
300
|
-
_runEffect(e);
|
|
301
|
-
if (!e._computed) _updateEffectLevel(e);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
iterations++;
|
|
305
|
-
}
|
|
306
|
-
if (iterations >= 25) {
|
|
307
|
-
if (__DEV__) {
|
|
308
|
-
const remaining = pendingEffects.slice(0, 3);
|
|
309
|
-
const effectNames = remaining.map(e => e.fn?.name || e.fn?.toString().slice(0, 60) || '(anonymous)');
|
|
310
|
-
console.warn(
|
|
311
|
-
`[what] Possible infinite effect loop detected (25 iterations). ` +
|
|
312
|
-
`Likely cause: an effect writes to a signal it also reads, creating a cycle. ` +
|
|
313
|
-
`Use untrack() to read signals without subscribing. ` +
|
|
314
|
-
`Looping effects: ${effectNames.join(', ')}`
|
|
315
|
-
);
|
|
316
|
-
} else {
|
|
317
|
-
console.warn('[what] Possible infinite effect loop detected');
|
|
318
|
-
}
|
|
319
|
-
for (let i = 0; i < pendingEffects.length; i++) pendingEffects[i]._pending = false;
|
|
320
|
-
pendingEffects.length = 0;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
export function memo(fn) {
|
|
324
|
-
let value;
|
|
325
|
-
const subs = new Set();
|
|
326
|
-
const e = _createEffect(() => {
|
|
327
|
-
const next = fn();
|
|
328
|
-
if (!Object.is(value, next)) {
|
|
329
|
-
value = next;
|
|
330
|
-
notify(subs);
|
|
331
|
-
}
|
|
332
|
-
});
|
|
333
|
-
e._level = 1;
|
|
334
|
-
_runEffect(e);
|
|
335
|
-
_updateEffectLevel(e);
|
|
336
|
-
subSetOwner.set(subs, e);
|
|
337
|
-
if (currentRoot) {
|
|
338
|
-
currentRoot.disposals.push(() => _disposeEffect(e));
|
|
339
|
-
}
|
|
340
|
-
function read() {
|
|
341
|
-
if (currentEffect) {
|
|
342
|
-
subs.add(currentEffect);
|
|
343
|
-
currentEffect.deps.push(subs);
|
|
344
|
-
}
|
|
345
|
-
return value;
|
|
346
|
-
}
|
|
347
|
-
read._signal = true;
|
|
348
|
-
read.peek = () => value;
|
|
349
|
-
return read;
|
|
350
|
-
}
|
|
351
|
-
export function flushSync() {
|
|
352
|
-
microtaskScheduled = false;
|
|
353
|
-
flush();
|
|
354
|
-
}
|
|
355
|
-
export function untrack(fn) {
|
|
356
|
-
const prev = currentEffect;
|
|
357
|
-
currentEffect = null;
|
|
358
|
-
try {
|
|
359
|
-
return fn();
|
|
360
|
-
} finally {
|
|
361
|
-
currentEffect = prev;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
export function getOwner() {
|
|
365
|
-
return currentOwner;
|
|
366
|
-
}
|
|
367
|
-
export function runWithOwner(owner, fn) {
|
|
368
|
-
const prev = currentOwner;
|
|
369
|
-
const prevRoot = currentRoot;
|
|
370
|
-
currentOwner = owner;
|
|
371
|
-
currentRoot = owner;
|
|
372
|
-
try {
|
|
373
|
-
return fn();
|
|
374
|
-
} finally {
|
|
375
|
-
currentOwner = prev;
|
|
376
|
-
currentRoot = prevRoot;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
export function createRoot(fn) {
|
|
380
|
-
const prevRoot = currentRoot;
|
|
381
|
-
const prevOwner = currentOwner;
|
|
382
|
-
const root = {
|
|
383
|
-
disposals: [],
|
|
384
|
-
owner: currentOwner,
|
|
385
|
-
children: [],
|
|
386
|
-
_disposed: false,
|
|
387
|
-
};
|
|
388
|
-
if (currentOwner) {
|
|
389
|
-
currentOwner.children.push(root);
|
|
390
|
-
}
|
|
391
|
-
currentRoot = root;
|
|
392
|
-
currentOwner = root;
|
|
393
|
-
try {
|
|
394
|
-
const dispose = () => {
|
|
395
|
-
if (root._disposed) return;
|
|
396
|
-
root._disposed = true;
|
|
397
|
-
for (let i = root.children.length - 1; i >= 0; i--) {
|
|
398
|
-
_disposeRoot(root.children[i]);
|
|
399
|
-
}
|
|
400
|
-
root.children.length = 0;
|
|
401
|
-
for (let i = root.disposals.length - 1; i >= 0; i--) {
|
|
402
|
-
root.disposals[i]();
|
|
403
|
-
}
|
|
404
|
-
root.disposals.length = 0;
|
|
405
|
-
if (root.owner) {
|
|
406
|
-
const idx = root.owner.children.indexOf(root);
|
|
407
|
-
if (idx >= 0) root.owner.children.splice(idx, 1);
|
|
408
|
-
}
|
|
409
|
-
};
|
|
410
|
-
return fn(dispose);
|
|
411
|
-
} finally {
|
|
412
|
-
currentRoot = prevRoot;
|
|
413
|
-
currentOwner = prevOwner;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
function _disposeRoot(root) {
|
|
417
|
-
if (root._disposed) return;
|
|
418
|
-
root._disposed = true;
|
|
419
|
-
for (let i = root.children.length - 1; i >= 0; i--) {
|
|
420
|
-
_disposeRoot(root.children[i]);
|
|
421
|
-
}
|
|
422
|
-
root.children.length = 0;
|
|
423
|
-
for (let i = root.disposals.length - 1; i >= 0; i--) {
|
|
424
|
-
root.disposals[i]();
|
|
425
|
-
}
|
|
426
|
-
root.disposals.length = 0;
|
|
427
|
-
}
|
|
428
|
-
export function onCleanup(fn) {
|
|
429
|
-
if (currentRoot) {
|
|
430
|
-
currentRoot.disposals.push(fn);
|
|
431
|
-
}
|
|
432
|
-
}
|
package/dist/scheduler.js
DELETED
|
@@ -1,246 +0,0 @@
|
|
|
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
|
-
const effectKey = Symbol('scheduledEffect');
|
|
102
|
-
return effect(() => {
|
|
103
|
-
// Use raf() to debounce: only the latest callback runs per frame,
|
|
104
|
-
// avoiding creating new closures on every signal change.
|
|
105
|
-
raf(effectKey, () => {
|
|
106
|
-
scheduleRead(() => {
|
|
107
|
-
const data = readFn();
|
|
108
|
-
if (writeFn) {
|
|
109
|
-
scheduleWrite(() => writeFn(data));
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// --- Animation frame helper ---
|
|
117
|
-
// Like requestAnimationFrame but returns a cancellable promise.
|
|
118
|
-
|
|
119
|
-
export function nextFrame() {
|
|
120
|
-
let cancel;
|
|
121
|
-
const promise = new Promise((resolve, reject) => {
|
|
122
|
-
const id = requestAnimationFrame(resolve);
|
|
123
|
-
cancel = () => {
|
|
124
|
-
cancelAnimationFrame(id);
|
|
125
|
-
reject(new Error('Cancelled'));
|
|
126
|
-
};
|
|
127
|
-
});
|
|
128
|
-
promise.cancel = cancel;
|
|
129
|
-
return promise;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// --- Debounced RAF ---
|
|
133
|
-
// Only runs the latest callback once per frame.
|
|
134
|
-
|
|
135
|
-
const debouncedCallbacks = new Map();
|
|
136
|
-
|
|
137
|
-
export function raf(key, fn) {
|
|
138
|
-
if (debouncedCallbacks.has(key)) {
|
|
139
|
-
// Replace callback, don't schedule new frame
|
|
140
|
-
debouncedCallbacks.set(key, fn);
|
|
141
|
-
} else {
|
|
142
|
-
debouncedCallbacks.set(key, fn);
|
|
143
|
-
requestAnimationFrame(() => {
|
|
144
|
-
const callback = debouncedCallbacks.get(key);
|
|
145
|
-
debouncedCallbacks.delete(key);
|
|
146
|
-
if (callback) callback();
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// --- Resize Observer helper ---
|
|
152
|
-
// Batched resize observations.
|
|
153
|
-
|
|
154
|
-
const resizeObservers = new WeakMap();
|
|
155
|
-
let sharedResizeObserver = null;
|
|
156
|
-
|
|
157
|
-
export function onResize(element, callback) {
|
|
158
|
-
if (typeof ResizeObserver === 'undefined') {
|
|
159
|
-
// Fallback: just call once
|
|
160
|
-
callback(element.getBoundingClientRect());
|
|
161
|
-
return () => {};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (!sharedResizeObserver) {
|
|
165
|
-
sharedResizeObserver = new ResizeObserver(entries => {
|
|
166
|
-
scheduleRead(() => {
|
|
167
|
-
for (const entry of entries) {
|
|
168
|
-
const cb = resizeObservers.get(entry.target);
|
|
169
|
-
if (cb) {
|
|
170
|
-
cb(entry.contentRect);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
resizeObservers.set(element, callback);
|
|
178
|
-
sharedResizeObserver.observe(element);
|
|
179
|
-
|
|
180
|
-
return () => {
|
|
181
|
-
resizeObservers.delete(element);
|
|
182
|
-
sharedResizeObserver.unobserve(element);
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// --- Intersection Observer helper ---
|
|
187
|
-
// Batched intersection observations.
|
|
188
|
-
|
|
189
|
-
export function onIntersect(element, callback, options = {}) {
|
|
190
|
-
if (typeof IntersectionObserver === 'undefined') {
|
|
191
|
-
// Fallback: assume visible
|
|
192
|
-
callback({ isIntersecting: true, intersectionRatio: 1 });
|
|
193
|
-
return () => {};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const observer = new IntersectionObserver(entries => {
|
|
197
|
-
scheduleRead(() => {
|
|
198
|
-
for (const entry of entries) {
|
|
199
|
-
callback(entry);
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
}, options);
|
|
203
|
-
|
|
204
|
-
observer.observe(element);
|
|
205
|
-
|
|
206
|
-
return () => observer.disconnect();
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// --- Smooth scrolling with scheduler ---
|
|
210
|
-
|
|
211
|
-
export function smoothScrollTo(element, options = {}) {
|
|
212
|
-
const { duration = 300, easing = t => t * (2 - t) } = options;
|
|
213
|
-
|
|
214
|
-
return new Promise(resolve => {
|
|
215
|
-
let startY;
|
|
216
|
-
let targetY;
|
|
217
|
-
let startTime;
|
|
218
|
-
|
|
219
|
-
scheduleRead(() => {
|
|
220
|
-
startY = window.scrollY;
|
|
221
|
-
const rect = element.getBoundingClientRect();
|
|
222
|
-
targetY = startY + rect.top;
|
|
223
|
-
startTime = performance.now();
|
|
224
|
-
tick();
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
function tick() {
|
|
228
|
-
scheduleRead(() => {
|
|
229
|
-
const elapsed = performance.now() - startTime;
|
|
230
|
-
const progress = Math.min(elapsed / duration, 1);
|
|
231
|
-
const easedProgress = easing(progress);
|
|
232
|
-
const currentY = startY + (targetY - startY) * easedProgress;
|
|
233
|
-
|
|
234
|
-
scheduleWrite(() => {
|
|
235
|
-
window.scrollTo(0, currentY);
|
|
236
|
-
|
|
237
|
-
if (progress < 1) {
|
|
238
|
-
requestAnimationFrame(tick);
|
|
239
|
-
} else {
|
|
240
|
-
resolve();
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
});
|
|
246
|
-
}
|