veles 1.1.1 → 1.1.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.
@@ -108,11 +108,20 @@ function executeComponent({ element, props }) {
108
108
  }
109
109
  //#endregion
110
110
  //#region src/create-element/parse-children.ts
111
+ function flattenChildren(children) {
112
+ const flattenedChildren = [];
113
+ function append(child) {
114
+ if (Array.isArray(child)) child.forEach(append);
115
+ else flattenedChildren.push(child);
116
+ }
117
+ append(children);
118
+ return flattenedChildren;
119
+ }
111
120
  function parseChildren({ children, htmlElement, velesNode, portal }) {
112
121
  const childComponents = [];
113
122
  if (children === void 0 || children === null) return childComponents;
114
123
  let lastInsertedNode = null;
115
- (Array.isArray(children) ? children : [children]).forEach((childComponent) => {
124
+ flattenChildren(children).forEach((childComponent) => {
116
125
  if (typeof childComponent === "string") {
117
126
  const textNode = createTextElement(childComponent);
118
127
  htmlElement.append(textNode.html);
@@ -108,11 +108,20 @@ function executeComponent({ element, props }) {
108
108
  }
109
109
  //#endregion
110
110
  //#region src/create-element/parse-children.ts
111
+ function flattenChildren(children) {
112
+ const flattenedChildren = [];
113
+ function append(child) {
114
+ if (Array.isArray(child)) child.forEach(append);
115
+ else flattenedChildren.push(child);
116
+ }
117
+ append(children);
118
+ return flattenedChildren;
119
+ }
111
120
  function parseChildren({ children, htmlElement, velesNode, portal }) {
112
121
  const childComponents = [];
113
122
  if (children === void 0 || children === null) return childComponents;
114
123
  let lastInsertedNode = null;
115
- (Array.isArray(children) ? children : [children]).forEach((childComponent) => {
124
+ flattenChildren(children).forEach((childComponent) => {
116
125
  if (typeof childComponent === "string") {
117
126
  const textNode = createTextElement(childComponent);
118
127
  htmlElement.append(textNode.html);
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require__utils = require("./_utils-DnT1tr2L.cjs");
2
+ const require__utils = require("./_utils-DbrPydEE.cjs");
3
3
  //#region src/attach-component.ts
4
4
  /**
5
5
  * Attach Veles component tree to a regular HTML node.
@@ -370,6 +370,113 @@ function triggerUpdates({ value, createState, trackers, get }) {
370
370
  });
371
371
  }
372
372
  //#endregion
373
+ //#region src/create-state/min-heap.ts
374
+ /**
375
+ * A stable min-heap. Values with the same priority are returned in
376
+ * insertion order.
377
+ */
378
+ var MinHeap = class {
379
+ constructor(compareValues) {
380
+ this._entries = [];
381
+ this._insertionOrder = 0;
382
+ this._compareValues = compareValues;
383
+ }
384
+ get size() {
385
+ return this._entries.length;
386
+ }
387
+ push(value) {
388
+ const entry = {
389
+ value,
390
+ insertionOrder: this._insertionOrder++
391
+ };
392
+ let index = this._entries.length;
393
+ this._entries.push(entry);
394
+ while (index > 0) {
395
+ const parentIndex = Math.floor((index - 1) / 2);
396
+ const parentEntry = this._entries[parentIndex];
397
+ if (this.compareEntries(parentEntry, entry) <= 0) break;
398
+ this._entries[index] = parentEntry;
399
+ index = parentIndex;
400
+ }
401
+ this._entries[index] = entry;
402
+ }
403
+ pop() {
404
+ const firstEntry = this._entries[0];
405
+ const lastEntry = this._entries.pop();
406
+ if (!firstEntry || !lastEntry) return void 0;
407
+ if (this._entries.length === 0) return firstEntry.value;
408
+ let index = 0;
409
+ while (true) {
410
+ const leftChildIndex = index * 2 + 1;
411
+ const rightChildIndex = leftChildIndex + 1;
412
+ let smallestIndex = index;
413
+ if (leftChildIndex < this._entries.length && this.compareEntries(this._entries[leftChildIndex], lastEntry) < 0) smallestIndex = leftChildIndex;
414
+ if (rightChildIndex < this._entries.length && this.compareEntries(this._entries[rightChildIndex], smallestIndex === index ? lastEntry : this._entries[leftChildIndex]) < 0) smallestIndex = rightChildIndex;
415
+ if (smallestIndex === index) break;
416
+ this._entries[index] = this._entries[smallestIndex];
417
+ index = smallestIndex;
418
+ }
419
+ this._entries[index] = lastEntry;
420
+ return firstEntry.value;
421
+ }
422
+ compareEntries(entry1, entry2) {
423
+ const valueComparison = this._compareValues(entry1.value, entry2.value);
424
+ return valueComparison === 0 ? entry1.insertionOrder - entry2.insertionOrder : valueComparison;
425
+ }
426
+ };
427
+ //#endregion
428
+ //#region src/create-state/state-scheduler.ts
429
+ /**
430
+ * Coordinates notifications produced by one synchronous state transaction.
431
+ *
432
+ * A subscriber can synchronously update another state. That nested update adds
433
+ * its notifications to this same scheduler, allowing an older notification for
434
+ * the same state to be replaced before it is delivered.
435
+ */
436
+ var StateScheduler = class {
437
+ constructor() {
438
+ this._notifications = new MinHeap((notification1, notification2) => notification1.rank - notification2.rank);
439
+ this._notificationsByKey = /* @__PURE__ */ new Map();
440
+ }
441
+ scheduleNotification(notification) {
442
+ const existingNotification = this._notificationsByKey.get(notification.key);
443
+ if (existingNotification) {
444
+ existingNotification.value = notification.value;
445
+ return;
446
+ }
447
+ this._notificationsByKey.set(notification.key, notification);
448
+ this._notifications.push(notification);
449
+ }
450
+ deliverNotifications() {
451
+ while (this._notifications.size > 0) {
452
+ const notification = this._notifications.pop();
453
+ if (this._notificationsByKey.get(notification.key) !== notification) continue;
454
+ this._notificationsByKey.delete(notification.key);
455
+ notification.notify(notification.value, notification.previousValue);
456
+ }
457
+ }
458
+ };
459
+ let activeScheduler;
460
+ /**
461
+ * Top-level state updates own the scheduler and synchronously drain it. Updates
462
+ * made by subscribers reuse the active scheduler, so they cannot deliver a
463
+ * nested notification batch ahead of notifications already waiting to run.
464
+ */
465
+ function runWithStateScheduler(runUpdate) {
466
+ if (activeScheduler) {
467
+ runUpdate(activeScheduler);
468
+ return;
469
+ }
470
+ const scheduler = new StateScheduler();
471
+ activeScheduler = scheduler;
472
+ try {
473
+ runUpdate(scheduler);
474
+ scheduler.deliverNotifications();
475
+ } finally {
476
+ activeScheduler = void 0;
477
+ }
478
+ }
479
+ //#endregion
373
480
  //#region src/create-state/state-core.ts
374
481
  const emptyValue = Symbol("veles-state-core-empty");
375
482
  const defaultEquality = (value1, value2) => value1 === value2;
@@ -380,8 +487,10 @@ var StateCore = class StateCore {
380
487
  this._dirty = false;
381
488
  this._children = /* @__PURE__ */ new Set();
382
489
  this._value = initialValue;
383
- this._parents = new Set(options.parents);
490
+ const parents = options.parents || [];
491
+ this._parents = new Set(parents);
384
492
  this._compute = options.compute;
493
+ this._rank = parents.reduce((rank, parent) => Math.max(rank, parent._rank + 1), 0);
385
494
  this._dirty = Boolean(options.dirty);
386
495
  this._equality = options.equality || defaultEquality;
387
496
  }
@@ -400,14 +509,16 @@ var StateCore = class StateCore {
400
509
  }
401
510
  set(newValue) {
402
511
  if (this._equality(this._value, newValue)) return;
403
- const prevValue = this._value;
404
- this._prevValue = prevValue;
405
- this._value = newValue;
406
- this.notifySubscribers(newValue, prevValue);
407
- this._children.forEach((child) => {
408
- child._dirty = true;
512
+ runWithStateScheduler((scheduler) => {
513
+ const prevValue = this._value;
514
+ this._prevValue = prevValue;
515
+ this._value = newValue;
516
+ this.scheduleNotification(scheduler, newValue, prevValue);
517
+ this._children.forEach((child) => {
518
+ child._dirty = true;
519
+ });
520
+ this.flush(scheduler);
409
521
  });
410
- this.flush();
411
522
  }
412
523
  update(fn) {
413
524
  const newValue = fn(this._value);
@@ -494,37 +605,62 @@ var StateCore = class StateCore {
494
605
  this._dirty = false;
495
606
  return { changed };
496
607
  }
497
- flush() {
498
- const queue = Array.from(this._children);
499
- const queued = new Set(queue);
500
- while (queue.length > 0) {
501
- const child = queue.shift();
608
+ flush(scheduler) {
609
+ /**
610
+ * We utilize a min-heap to ensure that we don't trigger notifications
611
+ * before all the previous necessary work is done. Otherwise it can
612
+ * happen too early, and we'd need to recursively check that every parent
613
+ * is not dirty and schedule it again.
614
+ *
615
+ * Here we achieve this by assinging a rank to each core state, which is
616
+ * determined by the highest rank of any of the parent + 1.
617
+ *
618
+ * After that, we process them at the same rank, ensuring that "short" branches
619
+ * are not executed before all their parents are done with their recomputation.
620
+ */
621
+ const queue = new MinHeap((node1, node2) => node1._rank - node2._rank);
622
+ const queued = /* @__PURE__ */ new Set();
623
+ const enqueue = (node) => {
624
+ if (queued.has(node)) return;
625
+ queue.push(node);
626
+ queued.add(node);
627
+ };
628
+ this._children.forEach(enqueue);
629
+ while (queue.size > 0) {
630
+ const child = queue.pop();
502
631
  queued.delete(child);
503
632
  if (!child._dirty) continue;
504
- if (child.hasDirtyParents()) {
505
- queue.push(child);
506
- queued.add(child);
633
+ let shouldRescheduleChild = false;
634
+ child._parents.forEach((parentNode) => {
635
+ if (parentNode._dirty) {
636
+ enqueue(parentNode);
637
+ shouldRescheduleChild = true;
638
+ }
639
+ });
640
+ if (shouldRescheduleChild) {
641
+ enqueue(child);
507
642
  continue;
508
643
  }
509
644
  const { changed } = child.recompute();
510
645
  const value = child._value;
511
646
  if (!changed) continue;
512
- child.notifySubscribers(value, child._prevValue);
647
+ child.scheduleNotification(scheduler, value, child._prevValue);
513
648
  child._children.forEach((grandchild) => {
514
649
  grandchild._dirty = true;
515
- if (!queued.has(grandchild)) {
516
- queue.push(grandchild);
517
- queued.add(grandchild);
518
- }
650
+ enqueue(grandchild);
519
651
  });
520
652
  }
521
653
  }
522
- hasDirtyParents() {
523
- let hasDirtyParent = false;
524
- this._parents.forEach((parent) => {
525
- if (parent._dirty) hasDirtyParent = true;
654
+ scheduleNotification(scheduler, value, previousValue) {
655
+ scheduler.scheduleNotification({
656
+ key: this,
657
+ rank: this._rank,
658
+ value,
659
+ previousValue,
660
+ notify: (notificationValue, notificationPreviousValue) => {
661
+ this.notifySubscribers(notificationValue, notificationPreviousValue);
662
+ }
526
663
  });
527
- return hasDirtyParent;
528
664
  }
529
665
  notifySubscribers(value, prevValue) {
530
666
  if (value === emptyValue) return;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { _ as onMount, a as identity, c as addPublicContext, d as popPublicContext, f as Fragment, g as hasCurrentLifecycleContext, h as createTextElement, i as getMountedNodeExecutedVersion, l as createContext, m as assignDomAttribute, n as callUnmountHandlers, o as renderTree, p as createElement, r as getExecutedComponentVelesNode, s as unique, t as callMountHandlers, u as getCurrentContext, v as onUnmount } from "./_utils-DxfFNKLL.js";
1
+ import { _ as onMount, a as identity, c as addPublicContext, d as popPublicContext, f as Fragment, g as hasCurrentLifecycleContext, h as createTextElement, i as getMountedNodeExecutedVersion, l as createContext, m as assignDomAttribute, n as callUnmountHandlers, o as renderTree, p as createElement, r as getExecutedComponentVelesNode, s as unique, t as callMountHandlers, u as getCurrentContext, v as onUnmount } from "./_utils-BnAXelPu.js";
2
2
  //#region src/attach-component.ts
3
3
  /**
4
4
  * Attach Veles component tree to a regular HTML node.
@@ -369,6 +369,113 @@ function triggerUpdates({ value, createState, trackers, get }) {
369
369
  });
370
370
  }
371
371
  //#endregion
372
+ //#region src/create-state/min-heap.ts
373
+ /**
374
+ * A stable min-heap. Values with the same priority are returned in
375
+ * insertion order.
376
+ */
377
+ var MinHeap = class {
378
+ constructor(compareValues) {
379
+ this._entries = [];
380
+ this._insertionOrder = 0;
381
+ this._compareValues = compareValues;
382
+ }
383
+ get size() {
384
+ return this._entries.length;
385
+ }
386
+ push(value) {
387
+ const entry = {
388
+ value,
389
+ insertionOrder: this._insertionOrder++
390
+ };
391
+ let index = this._entries.length;
392
+ this._entries.push(entry);
393
+ while (index > 0) {
394
+ const parentIndex = Math.floor((index - 1) / 2);
395
+ const parentEntry = this._entries[parentIndex];
396
+ if (this.compareEntries(parentEntry, entry) <= 0) break;
397
+ this._entries[index] = parentEntry;
398
+ index = parentIndex;
399
+ }
400
+ this._entries[index] = entry;
401
+ }
402
+ pop() {
403
+ const firstEntry = this._entries[0];
404
+ const lastEntry = this._entries.pop();
405
+ if (!firstEntry || !lastEntry) return void 0;
406
+ if (this._entries.length === 0) return firstEntry.value;
407
+ let index = 0;
408
+ while (true) {
409
+ const leftChildIndex = index * 2 + 1;
410
+ const rightChildIndex = leftChildIndex + 1;
411
+ let smallestIndex = index;
412
+ if (leftChildIndex < this._entries.length && this.compareEntries(this._entries[leftChildIndex], lastEntry) < 0) smallestIndex = leftChildIndex;
413
+ if (rightChildIndex < this._entries.length && this.compareEntries(this._entries[rightChildIndex], smallestIndex === index ? lastEntry : this._entries[leftChildIndex]) < 0) smallestIndex = rightChildIndex;
414
+ if (smallestIndex === index) break;
415
+ this._entries[index] = this._entries[smallestIndex];
416
+ index = smallestIndex;
417
+ }
418
+ this._entries[index] = lastEntry;
419
+ return firstEntry.value;
420
+ }
421
+ compareEntries(entry1, entry2) {
422
+ const valueComparison = this._compareValues(entry1.value, entry2.value);
423
+ return valueComparison === 0 ? entry1.insertionOrder - entry2.insertionOrder : valueComparison;
424
+ }
425
+ };
426
+ //#endregion
427
+ //#region src/create-state/state-scheduler.ts
428
+ /**
429
+ * Coordinates notifications produced by one synchronous state transaction.
430
+ *
431
+ * A subscriber can synchronously update another state. That nested update adds
432
+ * its notifications to this same scheduler, allowing an older notification for
433
+ * the same state to be replaced before it is delivered.
434
+ */
435
+ var StateScheduler = class {
436
+ constructor() {
437
+ this._notifications = new MinHeap((notification1, notification2) => notification1.rank - notification2.rank);
438
+ this._notificationsByKey = /* @__PURE__ */ new Map();
439
+ }
440
+ scheduleNotification(notification) {
441
+ const existingNotification = this._notificationsByKey.get(notification.key);
442
+ if (existingNotification) {
443
+ existingNotification.value = notification.value;
444
+ return;
445
+ }
446
+ this._notificationsByKey.set(notification.key, notification);
447
+ this._notifications.push(notification);
448
+ }
449
+ deliverNotifications() {
450
+ while (this._notifications.size > 0) {
451
+ const notification = this._notifications.pop();
452
+ if (this._notificationsByKey.get(notification.key) !== notification) continue;
453
+ this._notificationsByKey.delete(notification.key);
454
+ notification.notify(notification.value, notification.previousValue);
455
+ }
456
+ }
457
+ };
458
+ let activeScheduler;
459
+ /**
460
+ * Top-level state updates own the scheduler and synchronously drain it. Updates
461
+ * made by subscribers reuse the active scheduler, so they cannot deliver a
462
+ * nested notification batch ahead of notifications already waiting to run.
463
+ */
464
+ function runWithStateScheduler(runUpdate) {
465
+ if (activeScheduler) {
466
+ runUpdate(activeScheduler);
467
+ return;
468
+ }
469
+ const scheduler = new StateScheduler();
470
+ activeScheduler = scheduler;
471
+ try {
472
+ runUpdate(scheduler);
473
+ scheduler.deliverNotifications();
474
+ } finally {
475
+ activeScheduler = void 0;
476
+ }
477
+ }
478
+ //#endregion
372
479
  //#region src/create-state/state-core.ts
373
480
  const emptyValue = Symbol("veles-state-core-empty");
374
481
  const defaultEquality = (value1, value2) => value1 === value2;
@@ -379,8 +486,10 @@ var StateCore = class StateCore {
379
486
  this._dirty = false;
380
487
  this._children = /* @__PURE__ */ new Set();
381
488
  this._value = initialValue;
382
- this._parents = new Set(options.parents);
489
+ const parents = options.parents || [];
490
+ this._parents = new Set(parents);
383
491
  this._compute = options.compute;
492
+ this._rank = parents.reduce((rank, parent) => Math.max(rank, parent._rank + 1), 0);
384
493
  this._dirty = Boolean(options.dirty);
385
494
  this._equality = options.equality || defaultEquality;
386
495
  }
@@ -399,14 +508,16 @@ var StateCore = class StateCore {
399
508
  }
400
509
  set(newValue) {
401
510
  if (this._equality(this._value, newValue)) return;
402
- const prevValue = this._value;
403
- this._prevValue = prevValue;
404
- this._value = newValue;
405
- this.notifySubscribers(newValue, prevValue);
406
- this._children.forEach((child) => {
407
- child._dirty = true;
511
+ runWithStateScheduler((scheduler) => {
512
+ const prevValue = this._value;
513
+ this._prevValue = prevValue;
514
+ this._value = newValue;
515
+ this.scheduleNotification(scheduler, newValue, prevValue);
516
+ this._children.forEach((child) => {
517
+ child._dirty = true;
518
+ });
519
+ this.flush(scheduler);
408
520
  });
409
- this.flush();
410
521
  }
411
522
  update(fn) {
412
523
  const newValue = fn(this._value);
@@ -493,37 +604,62 @@ var StateCore = class StateCore {
493
604
  this._dirty = false;
494
605
  return { changed };
495
606
  }
496
- flush() {
497
- const queue = Array.from(this._children);
498
- const queued = new Set(queue);
499
- while (queue.length > 0) {
500
- const child = queue.shift();
607
+ flush(scheduler) {
608
+ /**
609
+ * We utilize a min-heap to ensure that we don't trigger notifications
610
+ * before all the previous necessary work is done. Otherwise it can
611
+ * happen too early, and we'd need to recursively check that every parent
612
+ * is not dirty and schedule it again.
613
+ *
614
+ * Here we achieve this by assinging a rank to each core state, which is
615
+ * determined by the highest rank of any of the parent + 1.
616
+ *
617
+ * After that, we process them at the same rank, ensuring that "short" branches
618
+ * are not executed before all their parents are done with their recomputation.
619
+ */
620
+ const queue = new MinHeap((node1, node2) => node1._rank - node2._rank);
621
+ const queued = /* @__PURE__ */ new Set();
622
+ const enqueue = (node) => {
623
+ if (queued.has(node)) return;
624
+ queue.push(node);
625
+ queued.add(node);
626
+ };
627
+ this._children.forEach(enqueue);
628
+ while (queue.size > 0) {
629
+ const child = queue.pop();
501
630
  queued.delete(child);
502
631
  if (!child._dirty) continue;
503
- if (child.hasDirtyParents()) {
504
- queue.push(child);
505
- queued.add(child);
632
+ let shouldRescheduleChild = false;
633
+ child._parents.forEach((parentNode) => {
634
+ if (parentNode._dirty) {
635
+ enqueue(parentNode);
636
+ shouldRescheduleChild = true;
637
+ }
638
+ });
639
+ if (shouldRescheduleChild) {
640
+ enqueue(child);
506
641
  continue;
507
642
  }
508
643
  const { changed } = child.recompute();
509
644
  const value = child._value;
510
645
  if (!changed) continue;
511
- child.notifySubscribers(value, child._prevValue);
646
+ child.scheduleNotification(scheduler, value, child._prevValue);
512
647
  child._children.forEach((grandchild) => {
513
648
  grandchild._dirty = true;
514
- if (!queued.has(grandchild)) {
515
- queue.push(grandchild);
516
- queued.add(grandchild);
517
- }
649
+ enqueue(grandchild);
518
650
  });
519
651
  }
520
652
  }
521
- hasDirtyParents() {
522
- let hasDirtyParent = false;
523
- this._parents.forEach((parent) => {
524
- if (parent._dirty) hasDirtyParent = true;
653
+ scheduleNotification(scheduler, value, previousValue) {
654
+ scheduler.scheduleNotification({
655
+ key: this,
656
+ rank: this._rank,
657
+ value,
658
+ previousValue,
659
+ notify: (notificationValue, notificationPreviousValue) => {
660
+ this.notifySubscribers(notificationValue, notificationPreviousValue);
661
+ }
525
662
  });
526
- return hasDirtyParent;
527
663
  }
528
664
  notifySubscribers(value, prevValue) {
529
665
  if (value === emptyValue) return;
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require__utils = require("./_utils-DnT1tr2L.cjs");
2
+ const require__utils = require("./_utils-DbrPydEE.cjs");
3
3
  exports.Fragment = require__utils.Fragment;
4
4
  exports.jsx = require__utils.createElement;
5
5
  exports.jsxDEV = require__utils.createElement;
@@ -1,2 +1,2 @@
1
- import { f as Fragment, p as createElement } from "./_utils-DxfFNKLL.js";
1
+ import { f as Fragment, p as createElement } from "./_utils-BnAXelPu.js";
2
2
  export { Fragment, createElement as jsx, createElement as jsxDEV, createElement as jsxs };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veles",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "UI library with main focus on performance",
5
5
  "keywords": [
6
6
  "JavaScript",