what-core 0.5.4 → 0.5.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/README.md CHANGED
@@ -111,7 +111,7 @@ mount(h(Counter), '#app');
111
111
  ## Links
112
112
 
113
113
  - [Documentation](https://whatfw.com)
114
- - [GitHub](https://github.com/zvndev/what-fw)
114
+ - [GitHub](https://github.com/CelsianJs/whatfw)
115
115
  - [Benchmarks](https://benchmarks.whatfw.com)
116
116
 
117
117
  ## License
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "what-core",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "What Framework - The closest framework to vanilla JS",
5
5
  "type": "module",
6
- "main": "dist/what.js",
6
+ "main": "src/index.js",
7
7
  "module": "src/index.js",
8
8
  "types": "index.d.ts",
9
9
  "exports": {
@@ -44,14 +44,14 @@
44
44
  "lightweight",
45
45
  "vdom"
46
46
  ],
47
- "author": "",
47
+ "author": "ZVN DEV (https://zvndev.com)",
48
48
  "license": "MIT",
49
49
  "repository": {
50
50
  "type": "git",
51
- "url": "https://github.com/zvndev/what-fw"
51
+ "url": "https://github.com/CelsianJs/whatfw"
52
52
  },
53
53
  "bugs": {
54
- "url": "https://github.com/zvndev/what-fw/issues"
54
+ "url": "https://github.com/CelsianJs/whatfw/issues"
55
55
  },
56
56
  "homepage": "https://whatfw.com"
57
57
  }
package/src/animation.js CHANGED
@@ -77,7 +77,9 @@ export function spring(initialValue, options = {}) {
77
77
 
78
78
  function set(newTarget) {
79
79
  target.set(newTarget);
80
- if (!isAnimating.peek()) {
80
+ // Use rafId check instead of isAnimating signal — signal may not have flushed
81
+ // after a synchronous stop() call, causing duplicate animation frames
82
+ if (rafId === null) {
81
83
  isAnimating.set(true);
82
84
  lastTime = null;
83
85
  rafId = requestAnimationFrame(tick);
package/src/data.js CHANGED
@@ -68,7 +68,7 @@ function subscribeToKey(key, revalidateFn) {
68
68
  };
69
69
  }
70
70
 
71
- const inFlightRequests = new Map();
71
+ const inFlightRequests = new Map(); // key -> { promise, timestamp, refCount }
72
72
  const lastFetchTimestamps = new Map(); // key -> timestamp of last completed fetch
73
73
 
74
74
  // Create an effect scoped to the current component's lifecycle.
@@ -200,6 +200,7 @@ export function useSWR(key, fetcher, options = {}) {
200
200
  if (inFlightRequests.has(key)) {
201
201
  const existing = inFlightRequests.get(key);
202
202
  if (now - existing.timestamp < dedupingInterval) {
203
+ existing.refCount++;
203
204
  return existing.promise;
204
205
  }
205
206
  }
@@ -210,15 +211,20 @@ export function useSWR(key, fetcher, options = {}) {
210
211
  return cacheS.peek();
211
212
  }
212
213
 
213
- // Abort previous request
214
- if (abortController) abortController.abort();
214
+ // Abort previous request only if no other subscribers are using it
215
+ if (abortController) {
216
+ const existing = inFlightRequests.get(key);
217
+ if (!existing || existing.refCount <= 1) {
218
+ abortController.abort();
219
+ }
220
+ }
215
221
  abortController = new AbortController();
216
222
  const { signal: abortSignal } = abortController;
217
223
 
218
224
  isValidating.set(true);
219
225
 
220
226
  const promise = fetcher(key, { signal: abortSignal });
221
- inFlightRequests.set(key, { promise, timestamp: now });
227
+ inFlightRequests.set(key, { promise, timestamp: now, refCount: 1 });
222
228
 
223
229
  try {
224
230
  const result = await promise;
@@ -238,7 +244,11 @@ export function useSWR(key, fetcher, options = {}) {
238
244
  throw e;
239
245
  } finally {
240
246
  if (!abortSignal.aborted) isValidating.set(false);
241
- inFlightRequests.delete(key);
247
+ const flight = inFlightRequests.get(key);
248
+ if (flight) {
249
+ flight.refCount--;
250
+ if (flight.refCount <= 0) inFlightRequests.delete(key);
251
+ }
242
252
  }
243
253
  }
244
254
 
package/src/dom.js CHANGED
@@ -78,17 +78,18 @@ function disposeComponent(ctx) {
78
78
  if (ctx.disposed) return;
79
79
  ctx.disposed = true;
80
80
 
81
- // Run useEffect cleanup functions
82
- for (const hook of ctx.hooks) {
81
+ // Run useEffect cleanup functions in reverse order (last effect first, matching React)
82
+ for (let i = ctx.hooks.length - 1; i >= 0; i--) {
83
+ const hook = ctx.hooks[i];
83
84
  if (hook && typeof hook === 'object' && 'cleanup' in hook && hook.cleanup) {
84
85
  try { hook.cleanup(); } catch (e) { console.error('[what] cleanup error:', e); }
85
86
  }
86
87
  }
87
88
 
88
- // Run onCleanup callbacks
89
+ // Run onCleanup callbacks in reverse order (last registered first)
89
90
  if (ctx._cleanupCallbacks) {
90
- for (const fn of ctx._cleanupCallbacks) {
91
- try { fn(); } catch (e) { console.error('[what] onCleanup error:', e); }
91
+ for (let i = ctx._cleanupCallbacks.length - 1; i >= 0; i--) {
92
+ try { ctx._cleanupCallbacks[i](); } catch (e) { console.error('[what] onCleanup error:', e); }
92
93
  }
93
94
  }
94
95
 
@@ -111,6 +112,12 @@ export function disposeTree(node) {
111
112
  if (node._dispose) {
112
113
  try { node._dispose(); } catch (e) { /* already disposed */ }
113
114
  }
115
+ // Dispose reactive prop effects (value: () => ..., class: () => ..., etc.)
116
+ if (node._propEffects) {
117
+ for (const key in node._propEffects) {
118
+ try { node._propEffects[key](); } catch (e) { /* already disposed */ }
119
+ }
120
+ }
114
121
  if (node.childNodes) {
115
122
  for (const child of node.childNodes) {
116
123
  disposeTree(child);
@@ -996,6 +1003,25 @@ function applyProps(el, newProps, oldProps, isSvg) {
996
1003
  }
997
1004
 
998
1005
  function setProp(el, key, value, isSvg) {
1006
+ // Reactive function props — wrap in effect() for fine-grained updates.
1007
+ // Applies to any non-event prop where the value is a function, e.g.:
1008
+ // h('input', { value: () => name(), class: () => active() ? 'on' : 'off' })
1009
+ // The function is called inside an effect, so signal reads create subscriptions.
1010
+ // When signals change, the prop is re-applied automatically.
1011
+ if (typeof value === 'function' && !(key.startsWith('on') && key.length > 2) && key !== 'ref') {
1012
+ // Store dispose functions on the element for cleanup
1013
+ if (!el._propEffects) el._propEffects = {};
1014
+ // Dispose previous effect for this prop if re-applying
1015
+ if (el._propEffects[key]) {
1016
+ try { el._propEffects[key](); } catch (e) { /* already disposed */ }
1017
+ }
1018
+ el._propEffects[key] = effect(() => {
1019
+ const resolved = value();
1020
+ setProp(el, key, resolved, isSvg);
1021
+ });
1022
+ return;
1023
+ }
1024
+
999
1025
  // Event handlers: onClick -> click, onFocusCapture -> focus (capture phase)
1000
1026
  // Wrap in untrack so signal reads in handlers don't create subscriptions
1001
1027
  if (key.startsWith('on') && key.length > 2) {
package/src/form.js CHANGED
@@ -62,6 +62,7 @@ function createFormController(options = {}) {
62
62
  const isDirty = signal(false);
63
63
  const isSubmitting = signal(false);
64
64
  const isSubmitted = signal(false);
65
+ const isValidating = signal(false);
65
66
  const submitCount = signal(0);
66
67
 
67
68
  // Helper: get all current values as a plain object.
@@ -128,34 +129,42 @@ function createFormController(options = {}) {
128
129
  async function validate(fieldName) {
129
130
  if (!resolver) return true;
130
131
 
131
- const result = await resolver(getAllValues(false));
132
- const nextErrors = result?.errors || {};
133
-
134
- if (fieldName) {
135
- const nextError = nextErrors[fieldName] ?? null;
136
- setFieldError(fieldName, nextError);
137
- return !nextError;
138
- } else {
139
- replaceAllErrors(nextErrors);
140
- return Object.keys(nextErrors).length === 0;
132
+ isValidating.set(true);
133
+ try {
134
+ const result = await resolver(getAllValues(false));
135
+ const nextErrors = result?.errors || {};
136
+
137
+ if (fieldName) {
138
+ const nextError = nextErrors[fieldName] ?? null;
139
+ setFieldError(fieldName, nextError);
140
+ return !nextError;
141
+ } else {
142
+ replaceAllErrors(nextErrors);
143
+ return Object.keys(nextErrors).length === 0;
144
+ }
145
+ } finally {
146
+ isValidating.set(false);
141
147
  }
142
148
  }
143
149
 
144
150
  // Register a field — only subscribes to THIS field's signal
145
151
  function register(name, options = {}) {
146
152
  const fieldSig = getFieldSignal(name);
147
- return {
148
- name,
149
- // Use getter so value is always fresh, even if register result is cached
150
- get value() { return fieldSig(); },
151
- onInput: (e) => {
152
- const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
153
- setValue(name, value);
153
+ const isCheckbox = options.type === 'checkbox' || options.type === 'radio';
154
154
 
155
- if (mode === 'onChange' || (isSubmitted.peek() && reValidateMode === 'onChange')) {
156
- validate(name);
157
- }
158
- },
155
+ const handler = (e) => {
156
+ const value = (e.target.type === 'checkbox' || e.target.type === 'radio')
157
+ ? e.target.checked
158
+ : e.target.value;
159
+ setValue(name, value);
160
+
161
+ if (mode === 'onChange' || (isSubmitted.peek() && reValidateMode === 'onChange')) {
162
+ validate(name);
163
+ }
164
+ };
165
+
166
+ const result = {
167
+ name,
159
168
  onBlur: () => {
160
169
  getTouchedSignal(name).set(true);
161
170
 
@@ -166,6 +175,24 @@ function createFormController(options = {}) {
166
175
  onFocus: () => {},
167
176
  ref: options.ref,
168
177
  };
178
+
179
+ if (isCheckbox) {
180
+ // Checkbox/radio: use checked prop + onchange event
181
+ Object.defineProperty(result, 'checked', {
182
+ get() { return !!fieldSig(); },
183
+ enumerable: true,
184
+ });
185
+ result.onchange = handler;
186
+ } else {
187
+ // Text/select/textarea: use value prop + oninput event
188
+ Object.defineProperty(result, 'value', {
189
+ get() { return fieldSig(); },
190
+ enumerable: true,
191
+ });
192
+ result.oninput = handler;
193
+ }
194
+
195
+ return result;
169
196
  }
170
197
 
171
198
  // Set single field value — only triggers re-render for components reading this field
@@ -273,6 +300,7 @@ function createFormController(options = {}) {
273
300
  },
274
301
  isDirty: () => isDirty(),
275
302
  isValid,
303
+ isValidating: () => isValidating(),
276
304
  isSubmitting: () => isSubmitting(),
277
305
  isSubmitted: () => isSubmitted(),
278
306
  submitCount: () => submitCount(),
package/src/reactive.js CHANGED
@@ -279,7 +279,7 @@ function scheduleMicrotask() {
279
279
 
280
280
  function flush() {
281
281
  let iterations = 0;
282
- while (pendingEffects.length > 0 && iterations < 100) {
282
+ while (pendingEffects.length > 0 && iterations < 25) {
283
283
  const batch = pendingEffects;
284
284
  pendingEffects = [];
285
285
  for (let i = 0; i < batch.length; i++) {
@@ -289,12 +289,12 @@ function flush() {
289
289
  }
290
290
  iterations++;
291
291
  }
292
- if (iterations >= 100) {
292
+ if (iterations >= 25) {
293
293
  if (__DEV__) {
294
294
  const remaining = pendingEffects.slice(0, 3);
295
295
  const effectNames = remaining.map(e => e.fn?.name || e.fn?.toString().slice(0, 60) || '(anonymous)');
296
296
  console.warn(
297
- `[what] Possible infinite effect loop detected (100 iterations). ` +
297
+ `[what] Possible infinite effect loop detected (25 iterations). ` +
298
298
  `Likely cause: an effect writes to a signal it also reads, creating a cycle. ` +
299
299
  `Use untrack() to read signals without subscribing. ` +
300
300
  `Looping effects: ${effectNames.join(', ')}`