round-core 0.0.8 → 0.0.9

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.
Files changed (3) hide show
  1. package/README.md +42 -42
  2. package/dist/index.js +211 -192
  3. package/package.json +1 -2
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- <h1 align="center">Round Framework</h1>
1
+ <h1 align="center">Round JS</h1>
2
2
 
3
3
  <p align="center">
4
4
  <img src="https://raw.githubusercontent.com/ZtaMDev/RoundJS/main/Round.png" alt="Round Framework Logo" width="200" />
@@ -14,32 +14,44 @@
14
14
 
15
15
  <div align="center">
16
16
 
17
- Extension for VSCode [here](https://marketplace.visualstudio.com/items?itemName=ZtaMDev.round) and OpenVSX version [here](https://open-vsx.org/extension/ztamdev/round)
17
+ Extension for [VSCode](https://marketplace.visualstudio.com/items?itemName=ZtaMDev.round) and [OpenVSX](https://open-vsx.org/extension/ztamdev/round)
18
18
 
19
19
  </div>
20
20
 
21
+ ---
22
+
23
+ Instead of a Virtual DOM diff, Round updates the UI by subscribing DOM updates directly to reactive primitives (**signals**) and **bindables**. This keeps rendering predictable, small, and fast for interactive apps.
24
+
25
+ The `round-core` package is the **foundation of RoundJS**.
26
+
27
+ You can think of `round-core` as:
28
+ - A **framework-level runtime**, not just a state library
29
+ - Comparable in scope to React + Router + Signals, but significantly smaller
30
+ - Suitable for fast SPAs and simple SSR setups without heavy infrastructure
31
+
32
+ ## Installation
33
+
34
+ To use Round JS today, install the core package:
21
35
 
22
36
  ```bash
23
37
  npm install round-core
24
38
  ```
25
39
 
26
- Instead of a Virtual DOM diff, Round updates the UI by subscribing DOM updates directly to reactive primitives (**signals**) and **bindables**. This keeps rendering predictable, small, and fast for interactive apps.
27
-
28
- ## What Round is focused on
40
+ Or with Bun:
29
41
 
30
- - **SPA-first**: client-side navigation and UI updates.
31
- - **Fine-grained reactivity**: update only what depends on the changed signal.
32
- - **Ergonomic bindings**: built-in two-way bindings with `bind:*` directives.
33
- - **A JSX superset**: `.round` files support extra control-flow syntax that compiles to JavaScript.
34
- - **Minimal runtime**: DOM-first runtime (no VDOM diffing).
42
+ ```bash
43
+ bun add round-core
44
+ ```
35
45
 
36
- ## Architecture
46
+ ## What Round is focused on
37
47
 
38
48
  Round is a **No-VDOM** framework.
39
49
 
40
50
  1. **Direct DOM Manipulation**: Components run once. They return real DOM nodes (via `document.createElement`).
41
51
  2. **Fine-Grained Reactivity**: Use of `signal`, `effect`, and `bindable` creates a dependency graph.
42
- 3. **Surgical Updates**: When a signal changes, only the specific text node, attribute, or property subscribed to that signal is updated. The component function does not re-run.
52
+ 3. **Ergonomic bindings**: built-in two-way bindings with `bind:*` directives.
53
+ 4. **Surgical Updates**: When a signal changes, only the specific text node, attribute, or property subscribed to that signal is updated. The component function does not re-run.
54
+ 5. **A JSX superset**: `.round` files support extra control-flow syntax that compiles to JavaScript.
43
55
 
44
56
  This avoids the overhead of Virtual DOM diffing and reconciliation entirely.
45
57
 
@@ -56,35 +68,6 @@ A **signal** is a small reactive container.
56
68
  - Reading a signal inside an `effect()` tracks a dependency.
57
69
  - Writing to a signal triggers only the subscribed computations.
58
70
 
59
-
60
- ## Normal Installation
61
-
62
- Simply install the `round-core` package
63
-
64
- ```bash
65
- bun add round-core
66
- ```
67
-
68
- Or:
69
-
70
- ```bash
71
- npm install round-core
72
- ```
73
-
74
- ## Repo Installation
75
-
76
- > Round is currently in active development. If you are using the repository directly, install dependencies and run the CLI locally.
77
-
78
- ```bash
79
- bun install
80
- ```
81
-
82
- Or:
83
-
84
- ```bash
85
- npm install
86
- ```
87
-
88
71
  ## Quick start (create a new app)
89
72
 
90
73
  Round includes a CLI with a project initializer.
@@ -100,7 +83,9 @@ This scaffolds a minimal Round app with `src/app.round` and an example `src/coun
100
83
 
101
84
  ## `.round` files
102
85
 
103
- A `.round` file is a JSX-based component module (ESM) compiled by the Round toolchain. you can also use .jsx files but you wont get the round JSX superset features like conditional rendering and other features.
86
+ A `.round` file is a JSX-based component module (ESM) compiled by the Round toolchain.
87
+ You can also use `.jsx` files, but you will not get the Round JSX superset features
88
+ such as extended control flow.
104
89
 
105
90
  Example `src/app.round`:
106
91
 
@@ -424,6 +409,21 @@ The CLI is intended for day-to-day development:
424
409
 
425
410
  Run `round -h` to see available commands.
426
411
 
412
+ ## Signals Internals
413
+
414
+ RoundJS utilizes a high-performance reactivity engine designed for efficiency and minimal memory overhead:
415
+
416
+ - **Doubly-Linked List Dependency Tracking**: Instead of using heavy `Set` objects, RoundJS uses a linked-list of subscription nodes. This eliminates array spreads and object allocations during signal updates, providing constant-time performance for adding/removing dependencies.
417
+ - **Global Versioning (Clock)**: Every signal write increments a global version counter. Computed signals (`derive`) track the version of their dependencies and only recompute if they are "dirty" (out of date). This ensures true lazyness and avoids redundant calculations.
418
+ - **Automatic Batching**: Multiple signal updates within the same execution cycle are batched. Effects and DOM updates only trigger once at the end of the batch, preventing "glitches" and unnecessary re-renders.
419
+
420
+ ## Performance
421
+
422
+ RoundJS sits in a powerful "middle ground" of performance:
423
+
424
+ - **vs React**: Round's fine-grained reactivity is **massively faster** (>30x in micro-benchmarks) than React's component-level reconciliation. DOM updates are surgical and don't require diffing a virtual tree.
425
+ - **vs Preact Signals**: While highly optimized, RoundJS signals are currently slightly slower than Preact Signals (~10x difference in raw signal-to-signal updates), as Preact utilizes more aggressive internal optimizations. However, for most real-world applications, RoundJS provides more than enough performance.
426
+
427
427
  ## Status
428
428
 
429
429
  Round is under active development and the API is still stabilizing. The README is currently the primary documentation; a dedicated documentation site will be built later using Round itself.
package/dist/index.js CHANGED
@@ -37,13 +37,11 @@ function onMount(fn) {
37
37
  if (component) {
38
38
  component.mountHooks.push(fn);
39
39
  } else {
40
- setTimeout(() => {
41
- try {
42
- fn();
43
- } catch (e) {
44
- reportErrorSafe(e, { phase: "onMount" });
45
- }
46
- }, 0);
40
+ try {
41
+ fn();
42
+ } catch (e) {
43
+ reportErrorSafe(e, { phase: "onMount" });
44
+ }
47
45
  }
48
46
  }
49
47
  function onUnmount(fn) {
@@ -145,135 +143,180 @@ const Lifecycle = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.definePro
145
143
  triggerUpdate,
146
144
  unmountComponent
147
145
  }, Symbol.toStringTag, { value: "Module" }));
148
- let context = [];
146
+ let context = null;
147
+ let batchCount = 0;
148
+ let pendingEffects = [];
149
+ let globalVersion = 0;
149
150
  function isPromiseLike$2(v) {
150
151
  return v && (typeof v === "object" || typeof v === "function") && typeof v.then === "function";
151
152
  }
152
- function subscribe(running, subscriptions) {
153
- subscriptions.add(running);
154
- running.dependencies.add(subscriptions);
153
+ function isSignalLike(v) {
154
+ return typeof v === "function" && typeof v.peek === "function" && "value" in v;
155
155
  }
156
156
  function untrack(fn) {
157
- context.push(null);
157
+ const prev = context;
158
+ context = null;
158
159
  try {
159
160
  return typeof fn === "function" ? fn() : void 0;
160
161
  } finally {
161
- context.pop();
162
+ context = prev;
163
+ }
164
+ }
165
+ function batch(fn) {
166
+ batchCount++;
167
+ try {
168
+ return fn();
169
+ } finally {
170
+ if (--batchCount === 0) {
171
+ const effects = pendingEffects;
172
+ pendingEffects = [];
173
+ for (let i = 0; i < effects.length; i++) {
174
+ effects[i].queued = false;
175
+ effects[i].run();
176
+ }
177
+ }
178
+ }
179
+ }
180
+ function subscribe(sub, dep) {
181
+ let link = sub.deps;
182
+ while (link) {
183
+ if (link.dep === dep) return;
184
+ link = link.nextDep;
185
+ }
186
+ link = {
187
+ sub,
188
+ dep,
189
+ nextSub: dep.subs,
190
+ prevSub: null,
191
+ nextDep: sub.deps,
192
+ prevDep: null
193
+ };
194
+ if (dep.subs) dep.subs.prevSub = link;
195
+ dep.subs = link;
196
+ if (sub.deps) sub.deps.prevDep = link;
197
+ sub.deps = link;
198
+ }
199
+ function cleanup(sub) {
200
+ let link = sub.deps;
201
+ while (link) {
202
+ const { dep, prevSub, nextSub } = link;
203
+ if (prevSub) prevSub.nextSub = nextSub;
204
+ else dep.subs = nextSub;
205
+ if (nextSub) nextSub.prevSub = prevSub;
206
+ link = link.nextDep;
207
+ }
208
+ sub.deps = null;
209
+ }
210
+ function notify(dep) {
211
+ let link = dep.subs;
212
+ while (link) {
213
+ const sub = link.sub;
214
+ if (sub.isComputed) {
215
+ sub.version = -1;
216
+ notify(sub);
217
+ } else {
218
+ if (batchCount > 0) {
219
+ if (!sub.queued) {
220
+ sub.queued = true;
221
+ pendingEffects.push(sub);
222
+ }
223
+ } else {
224
+ sub.run();
225
+ }
226
+ }
227
+ link = link.nextSub;
162
228
  }
163
229
  }
164
230
  function effect(arg1, arg2, arg3) {
165
- let callback;
166
- let explicitDeps = null;
167
- let options = { onLoad: true };
231
+ let callback, explicitDeps = null, options = { onLoad: true };
168
232
  let owner = getCurrentComponent();
169
233
  if (typeof arg1 === "function") {
170
234
  callback = arg1;
171
- if (arg2 && typeof arg2 === "object") {
172
- options = { ...options, ...arg2 };
173
- }
235
+ if (arg2 && typeof arg2 === "object") options = { ...options, ...arg2 };
174
236
  } else {
175
237
  explicitDeps = arg1;
176
238
  callback = arg2;
177
- if (arg3 && typeof arg3 === "object") {
178
- options = { ...options, ...arg3 };
179
- }
239
+ if (arg3 && typeof arg3 === "object") options = { ...options, ...arg3 };
180
240
  }
181
- const execute = () => {
182
- if (typeof execute._cleanup === "function") {
183
- try {
184
- execute._cleanup();
185
- } catch (e) {
186
- const name = owner ? owner.name ?? "Anonymous" : null;
187
- reportErrorSafe(e, { phase: "effect.cleanup", component: name });
188
- }
189
- execute._cleanup = null;
190
- }
191
- cleanup(execute);
192
- context.push(execute);
193
- try {
194
- if (explicitDeps) {
195
- if (Array.isArray(explicitDeps)) {
196
- explicitDeps.forEach((dep) => {
197
- if (typeof dep === "function") dep();
198
- });
199
- } else if (typeof explicitDeps === "function") {
200
- explicitDeps();
241
+ const sub = {
242
+ deps: null,
243
+ queued: false,
244
+ run() {
245
+ if (this._cleanup) {
246
+ try {
247
+ this._cleanup();
248
+ } catch (e) {
249
+ reportErrorSafe(e, { phase: "effect.cleanup", component: owner?.name });
201
250
  }
251
+ this._cleanup = null;
202
252
  }
203
- if (typeof callback === "function") {
204
- const res = callback();
205
- if (typeof res === "function") {
206
- execute._cleanup = res;
253
+ cleanup(this);
254
+ const prev = context;
255
+ context = this;
256
+ try {
257
+ if (explicitDeps) {
258
+ if (Array.isArray(explicitDeps)) {
259
+ for (let i = 0; i < explicitDeps.length; i++) {
260
+ const d = explicitDeps[i];
261
+ if (typeof d === "function") d();
262
+ }
263
+ } else if (typeof explicitDeps === "function") {
264
+ explicitDeps();
265
+ }
207
266
  }
267
+ const res = callback();
268
+ if (typeof res === "function") this._cleanup = res;
269
+ if (owner?.isMounted) triggerUpdate(owner);
270
+ } catch (e) {
271
+ if (!isPromiseLike$2(e)) reportErrorSafe(e, { phase: "effect", component: owner?.name });
272
+ else throw e;
273
+ } finally {
274
+ context = prev;
208
275
  }
209
- if (owner && owner.isMounted) triggerUpdate(owner);
210
- } catch (e) {
211
- if (isPromiseLike$2(e)) throw e;
212
- const name = owner ? owner.name ?? "Anonymous" : null;
213
- reportErrorSafe(e, { phase: "effect", component: name });
214
- } finally {
215
- context.pop();
216
- }
276
+ },
277
+ _cleanup: null
217
278
  };
218
- execute.dependencies = /* @__PURE__ */ new Set();
219
- execute._cleanup = null;
220
- if (options.onLoad) {
221
- onMount(execute);
222
- } else {
223
- execute();
224
- }
225
- return () => {
226
- if (typeof execute._cleanup === "function") {
279
+ const dispose = () => {
280
+ if (sub._cleanup) {
227
281
  try {
228
- execute._cleanup();
282
+ sub._cleanup();
229
283
  } catch (e) {
230
- const name = owner ? owner.name ?? "Anonymous" : null;
231
- reportErrorSafe(e, { phase: "effect.cleanup", component: name });
232
284
  }
285
+ sub._cleanup = null;
233
286
  }
234
- execute._cleanup = null;
235
- cleanup(execute);
287
+ cleanup(sub);
236
288
  };
237
- }
238
- function cleanup(running) {
239
- running.dependencies.forEach((dep) => dep.delete(running));
240
- running.dependencies.clear();
289
+ if (options.onLoad) {
290
+ onMount(() => sub.run());
291
+ } else {
292
+ sub.run();
293
+ }
294
+ return dispose;
241
295
  }
242
296
  function defineBindMarkerIfNeeded(source, target) {
243
297
  if (source && source.bind === true) {
244
298
  try {
245
- Object.defineProperty(target, "bind", {
246
- enumerable: true,
247
- configurable: false,
248
- writable: false,
249
- value: true
250
- });
299
+ Object.defineProperty(target, "bind", { enumerable: true, value: true, configurable: true });
251
300
  } catch {
252
- try {
253
- target.bind = true;
254
- } catch {
255
- }
301
+ target.bind = true;
256
302
  }
257
303
  }
258
304
  }
259
305
  function attachHelpers(s) {
260
306
  if (!s || typeof s !== "function") return s;
261
307
  if (typeof s.transform === "function" && typeof s.validate === "function" && typeof s.$pick === "function") return s;
262
- s.$pick = (p) => {
263
- return pick(s, p);
264
- };
308
+ s.$pick = (p) => pick(s, p);
265
309
  s.transform = (fromInput, toOutput) => {
266
310
  const fromFn = typeof fromInput === "function" ? fromInput : (v) => v;
267
311
  const toFn = typeof toOutput === "function" ? toOutput : (v) => v;
268
312
  const wrapped = function(...args) {
269
- if (args.length > 0) {
270
- return s(fromFn(args[0]));
271
- }
313
+ if (args.length > 0) return s(fromFn(args[0]));
272
314
  return toFn(s());
273
315
  };
274
316
  wrapped.peek = () => toFn(s.peek());
275
317
  Object.defineProperty(wrapped, "value", {
276
318
  enumerable: true,
319
+ configurable: true,
277
320
  get() {
278
321
  return wrapped.peek();
279
322
  },
@@ -287,8 +330,8 @@ function attachHelpers(s) {
287
330
  s.validate = (validator, options = {}) => {
288
331
  const validateFn = typeof validator === "function" ? validator : null;
289
332
  const error = signal(null);
290
- const validateOn = options && typeof options === "object" && typeof options.validateOn === "string" ? options.validateOn : "input";
291
- const validateInitial = Boolean(options && typeof options === "object" && options.validateInitial);
333
+ const validateOn = options?.validateOn || "input";
334
+ const validateInitial = !!options?.validateInitial;
292
335
  const wrapped = function(...args) {
293
336
  if (args.length > 0) {
294
337
  const next = args[0];
@@ -303,11 +346,7 @@ function attachHelpers(s) {
303
346
  error(null);
304
347
  return s(next);
305
348
  }
306
- if (typeof res === "string" && res.length) {
307
- error(res);
308
- } else {
309
- error("Invalid value");
310
- }
349
+ error(typeof res === "string" && res.length ? res : "Invalid value");
311
350
  return s.peek();
312
351
  }
313
352
  error(null);
@@ -331,13 +370,13 @@ function attachHelpers(s) {
331
370
  error(null);
332
371
  return true;
333
372
  }
334
- if (typeof res === "string" && res.length) error(res);
335
- else error("Invalid value");
373
+ error(typeof res === "string" && res.length ? res : "Invalid value");
336
374
  return false;
337
375
  };
338
376
  wrapped.peek = () => s.peek();
339
377
  Object.defineProperty(wrapped, "value", {
340
378
  enumerable: true,
379
+ configurable: true,
341
380
  get() {
342
381
  return wrapped.peek();
343
382
  },
@@ -359,66 +398,50 @@ function attachHelpers(s) {
359
398
  return s;
360
399
  }
361
400
  function signal(initialValue) {
362
- let value = initialValue;
363
- const subscriptions = /* @__PURE__ */ new Set();
364
- const read = () => {
365
- const running = context[context.length - 1];
366
- if (running) {
367
- subscribe(running, subscriptions);
368
- }
369
- return value;
401
+ const dep = {
402
+ value: initialValue,
403
+ version: 0,
404
+ subs: null
370
405
  };
371
- const peek = () => value;
372
- const write = (newValue) => {
373
- if (value !== newValue) {
374
- value = newValue;
375
- [...subscriptions].forEach((sub) => sub());
376
- }
377
- return value;
378
- };
379
- const signal2 = function(...args) {
380
- if (args.length > 0) {
381
- return write(args[0]);
406
+ const s = function(newValue) {
407
+ if (arguments.length > 0) {
408
+ if (dep.value !== newValue) {
409
+ dep.value = newValue;
410
+ dep.version = ++globalVersion;
411
+ notify(dep);
412
+ }
413
+ return dep.value;
382
414
  }
383
- return read();
415
+ if (context) subscribe(context, dep);
416
+ return dep.value;
384
417
  };
385
- Object.defineProperty(signal2, "value", {
418
+ s.peek = () => dep.value;
419
+ Object.defineProperty(s, "value", {
386
420
  enumerable: true,
421
+ configurable: true,
387
422
  get() {
388
- return peek();
423
+ return s();
389
424
  },
390
425
  set(v) {
391
- write(v);
426
+ s(v);
392
427
  }
393
428
  });
394
- signal2.peek = peek;
395
- return attachHelpers(signal2);
429
+ return attachHelpers(s);
396
430
  }
397
431
  function bindable(initialValue) {
398
432
  const s = signal(initialValue);
399
433
  try {
400
- Object.defineProperty(s, "bind", {
401
- enumerable: true,
402
- configurable: false,
403
- writable: false,
404
- value: true
405
- });
434
+ Object.defineProperty(s, "bind", { enumerable: true, value: true, configurable: true });
406
435
  } catch {
407
- try {
408
- s.bind = true;
409
- } catch {
410
- }
436
+ s.bind = true;
411
437
  }
412
438
  return attachHelpers(s);
413
439
  }
414
- function isSignalLike(v) {
415
- return typeof v === "function" && typeof v.peek === "function" && "value" in v;
416
- }
417
440
  function getIn(obj, path) {
418
441
  let cur = obj;
419
- for (const key of path) {
442
+ for (let i = 0; i < path.length; i++) {
420
443
  if (cur == null) return void 0;
421
- cur = cur[key];
444
+ cur = cur[path[i]];
422
445
  }
423
446
  return cur;
424
447
  }
@@ -445,9 +468,7 @@ function parsePath(path) {
445
468
  return [String(path)];
446
469
  }
447
470
  function pick(root, path) {
448
- if (!isSignalLike(root)) {
449
- throw new Error("[round] pick(root, path) expects root to be a signal (use bindable.object(...) or signal({...})).");
450
- }
471
+ if (!isSignalLike(root)) throw new Error("[round] pick() expects a signal.");
451
472
  const pathArr = parsePath(path);
452
473
  const view = function(...args) {
453
474
  if (args.length > 0) {
@@ -460,6 +481,7 @@ function pick(root, path) {
460
481
  view.peek = () => getIn(root.peek(), pathArr);
461
482
  Object.defineProperty(view, "value", {
462
483
  enumerable: true,
484
+ configurable: true,
463
485
  get() {
464
486
  return view.peek();
465
487
  },
@@ -469,17 +491,9 @@ function pick(root, path) {
469
491
  });
470
492
  if (root.bind === true) {
471
493
  try {
472
- Object.defineProperty(view, "bind", {
473
- enumerable: true,
474
- configurable: false,
475
- writable: false,
476
- value: true
477
- });
494
+ Object.defineProperty(view, "bind", { enumerable: true, value: true, configurable: true });
478
495
  } catch {
479
- try {
480
- view.bind = true;
481
- } catch {
482
- }
496
+ view.bind = true;
483
497
  }
484
498
  }
485
499
  return view;
@@ -489,21 +503,14 @@ function createBindableObjectProxy(root, basePath) {
489
503
  const handler = {
490
504
  get(_target, prop) {
491
505
  if (prop === Symbol.toStringTag) return "BindableObject";
492
- if (prop === Symbol.iterator) return void 0;
493
506
  if (prop === "peek") return () => basePath.length ? pick(root, basePath).peek() : root.peek();
494
507
  if (prop === "value") return basePath.length ? pick(root, basePath).peek() : root.peek();
495
508
  if (prop === "bind") return true;
496
509
  if (prop === "$pick") {
497
- return (p) => {
498
- const nextPath2 = basePath.concat(parsePath(p));
499
- return createBindableObjectProxy(root, nextPath2);
500
- };
510
+ return (p) => createBindableObjectProxy(root, basePath.concat(parsePath(p)));
501
511
  }
502
512
  if (prop === "_root") return root;
503
513
  if (prop === "_path") return basePath.slice();
504
- if (prop === "call" || prop === "apply") {
505
- return Reflect.get(_target, prop);
506
- }
507
514
  const key = String(prop);
508
515
  const nextPath = basePath.concat(key);
509
516
  const cacheKey = nextPath.join(".");
@@ -535,44 +542,25 @@ function createBindableObjectProxy(root, basePath) {
535
542
  return true;
536
543
  },
537
544
  has(_target, prop) {
538
- try {
539
- if (Reflect.has(_target, prop)) return true;
540
- } catch {
541
- }
545
+ if (prop === "peek" || prop === "value" || prop === "bind" || prop === "$pick") return true;
542
546
  const v = basePath.length ? pick(root, basePath).peek() : root.peek();
543
547
  return v != null && Object.prototype.hasOwnProperty.call(v, prop);
544
548
  }
545
549
  };
546
550
  const fn = function(...args) {
547
- if (args.length > 0) {
548
- if (basePath.length) return pick(root, basePath)(args[0]);
549
- return root(args[0]);
550
- }
551
- if (basePath.length) return pick(root, basePath)();
552
- return root();
551
+ if (args.length > 0) return basePath.length ? pick(root, basePath)(args[0]) : root(args[0]);
552
+ return basePath.length ? pick(root, basePath)() : root();
553
553
  };
554
554
  fn.peek = () => basePath.length ? pick(root, basePath).peek() : root.peek();
555
- Object.defineProperty(fn, "value", {
556
- enumerable: true,
557
- get() {
558
- return fn.peek();
559
- },
560
- set(v) {
561
- fn(v);
562
- }
563
- });
555
+ Object.defineProperty(fn, "value", { enumerable: true, configurable: true, get() {
556
+ return fn.peek();
557
+ }, set(v) {
558
+ fn(v);
559
+ } });
564
560
  try {
565
- Object.defineProperty(fn, "bind", {
566
- enumerable: true,
567
- configurable: false,
568
- writable: false,
569
- value: true
570
- });
561
+ Object.defineProperty(fn, "bind", { enumerable: true, value: true, configurable: true });
571
562
  } catch {
572
- try {
573
- fn.bind = true;
574
- } catch {
575
- }
563
+ fn.bind = true;
576
564
  }
577
565
  return new Proxy(fn, handler);
578
566
  }
@@ -581,14 +569,44 @@ bindable.object = function(initialObject = {}) {
581
569
  return createBindableObjectProxy(root, []);
582
570
  };
583
571
  function derive(fn) {
584
- const derived = signal();
585
- effect(() => {
586
- derived(fn());
587
- }, { onLoad: false });
588
- return () => derived();
572
+ const dep = {
573
+ fn,
574
+ value: void 0,
575
+ version: -1,
576
+ depsVersion: -1,
577
+ subs: null,
578
+ deps: null,
579
+ isComputed: true,
580
+ run() {
581
+ cleanup(this);
582
+ const prev = context;
583
+ context = this;
584
+ try {
585
+ this.value = this.fn();
586
+ this.depsVersion = globalVersion;
587
+ this.version = ++globalVersion;
588
+ } finally {
589
+ context = prev;
590
+ }
591
+ }
592
+ };
593
+ const s = function() {
594
+ if (dep.version === -1 || dep.depsVersion < globalVersion) dep.run();
595
+ if (context) subscribe(context, dep);
596
+ return dep.value;
597
+ };
598
+ s.peek = () => {
599
+ if (dep.version === -1 || dep.depsVersion < globalVersion) dep.run();
600
+ return dep.value;
601
+ };
602
+ Object.defineProperty(s, "value", { enumerable: true, configurable: true, get() {
603
+ return s();
604
+ } });
605
+ return attachHelpers(s);
589
606
  }
590
607
  const Signals = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
591
608
  __proto__: null,
609
+ batch,
592
610
  bindable,
593
611
  derive,
594
612
  effect,
@@ -1980,6 +1998,7 @@ export {
1980
1998
  Route,
1981
1999
  Suspense,
1982
2000
  SuspenseContext,
2001
+ batch,
1983
2002
  bindContext,
1984
2003
  bindable,
1985
2004
  captureContext,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "round-core",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "A lightweight frontend framework for SPA with signals and fine grained reactivity",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -21,7 +21,6 @@
21
21
  "dev": "node ./src/cli.js dev --config ./test/main/round.config.json",
22
22
  "build": "node ./src/cli.js build --config ./test/main/round.config.json",
23
23
  "build:core": "vite build -c vite.config.build.js",
24
- "test": "vitest run",
25
24
  "bench": "bun run --cwd benchmarks bench",
26
25
  "bench:build": "bun run --cwd benchmarks bench:build",
27
26
  "bench:runtime": "bun run --cwd benchmarks bench:runtime"