• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

emonkak / barebind / 27772560676

18 Jun 2026 04:00PM UTC coverage: 70.602% (-24.4%) from 95.043%
27772560676

push

github

emonkak
refactor(runtime): simplify node reconstruction using spread operator

187 of 261 branches covered (71.65%)

Branch coverage included in aggregate %.

1045 of 1484 relevant lines covered (70.42%)

51.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.25
/src/runtime.ts
1
import {
2
  type Commit,
3
  type Dispatcher,
4
  type HostAdapter,
5
  type Lanes,
6
  MUTATION_TYPE_INSERT,
7
  MUTATION_TYPE_REMOVE,
8
  MUTATION_TYPE_UPDATE,
9
  MUTATION_TYPE_UPDATE_AND_MOVE,
10
  type Part,
11
  type Renderer,
12
  type RenderNode,
13
  type RenderRoot,
14
  type Scope,
15
  type UpdateHandle,
16
  type UpdateOptions,
17
  type UpdateUnit,
18
  VBind,
19
  VComponent,
20
  type VElement,
21
  VFragment,
22
  VPortal,
23
  VTemplate,
24
} from './core.js';
25
import { assertUnreachable } from './debug.js';
26
import {
27
  getHighestPriorityLane,
28
  getLaneFromPriority,
29
  getPriorityFromLanes,
30
  getRenderLanes,
31
  NoLanes,
32
  SyncLane,
33
  ViewTransitionLane,
34
} from './lane.js';
35
import { PriorityQueue } from './queue.js';
36

37
interface Update {
38
  id: number;
39
  lanes: Lanes;
40
  unit: UpdateUnit;
41
  controller: PromiseWithResolvers<void>;
42
}
43

44
export class Runtime implements Renderer, Dispatcher {
45
  /** @internal */
46
  readonly _adapter: HostAdapter;
47
  /** @internal */
48
  readonly _updateQueue: PriorityQueue<Update> = new PriorityQueue(
148✔
49
    compareUpdates,
50
  );
51
  /** @internal */
52
  _updateBatch: Update[] = [];
148✔
53
  private _pendingLanes: number = NoLanes;
148✔
54
  private _stagedLanes: number = NoLanes;
148✔
55
  private _flushLanes: number = NoLanes;
148✔
56
  private _identifierCount: number = 0;
148✔
57
  private _transitionCount: number = 0;
148✔
58
  private _updateCount: number = 0;
148✔
59

60
  constructor(adapter: HostAdapter) {
61
    this._adapter = adapter;
148✔
62
  }
63

64
  get flushLanes(): Lanes {
65
    return this._flushLanes;
43✔
66
  }
67

68
  diff(
69
    oldNode: RenderNode,
70
    newElement: VElement,
71
    scope: Scope,
72
    index: number,
73
    parent: RenderNode | RenderRoot,
74
  ): RenderNode {
75
    if (oldNode.type !== newElement.type || oldNode.key !== newElement.key) {
331✔
76
      return this.render(newElement, scope, index, parent, oldNode.part);
4✔
77
    }
78
    if (oldNode.props === newElement.props) {
327✔
79
      return oldNode;
6✔
80
    }
81
    switch (true) {
321✔
82
      case newElement instanceof VBind: {
83
        return {
166✔
84
          ...(oldNode as RenderNode.BindNode),
85
          type: newElement.type,
86
          props: newElement.props,
87
          key: newElement.key,
88
          dirty: true,
89
        };
90
      }
91
      case newElement instanceof VComponent: {
92
        if (
11✔
93
          ((oldNode as RenderNode.ComponentNode).state.handle.pendingLanes &
94
            this._flushLanes) ===
95
            NoLanes &&
96
          (oldNode as RenderNode.ComponentNode).type.arePropsEqual(
97
            oldNode.props,
98
            newElement.props,
99
          )
100
        ) {
101
          return oldNode;
1✔
102
        }
103
        const newNode: RenderNode.ComponentNode = {
10✔
104
          ...(oldNode as RenderNode.ComponentNode),
105
          type: newElement.type,
106
          props: newElement.props,
107
          key: newElement.key,
108
          index,
109
          parent,
110
          children: oldNode.children.slice(),
111
          dirty: true,
112
        };
113
        const subScope = scope.child(newElement.type);
10✔
114
        newNode.children[0] = this.diff(
10✔
115
          newNode.children[0]!,
116
          newNode.state.handle.render(
117
            newNode.props,
118
            subScope,
119
            this._flushLanes,
120
          ),
121
          subScope,
122
          0,
123
          newNode,
124
        );
125
        return newNode;
10✔
126
      }
127
      case newElement instanceof VFragment: {
128
        const newNode: RenderNode.FragmentNode = {
25✔
129
          ...(oldNode as RenderNode.FragmentNode),
130
          type: newElement.type,
131
          props: newElement.props,
132
          key: newElement.key,
133
          index,
134
          parent,
135
          children: new Array(newElement.children.length),
136
        };
137
        this._diffChildren(
25✔
138
          oldNode as RenderNode.FragmentNode,
139
          newNode,
140
          newElement.children,
141
          scope,
142
        );
143
        return newNode;
25✔
144
      }
145
      case newElement instanceof VPortal:
146
      case newElement instanceof VTemplate: {
147
        const newNode: RenderNode.BlockNode = {
119✔
148
          ...(oldNode as RenderNode.BlockNode),
149
          type: newElement.type,
150
          props: newElement.props,
151
          key: newElement.key,
152
          index,
153
          parent,
154
          children: new Array(newElement.children.length),
155
        };
156
        for (let i = 0, l = newElement.children.length; i < l; i++) {
119✔
157
          const newChild = this.diff(
172✔
158
            oldNode.children[i]!,
159
            newElement.children[i]!,
160
            scope,
161
            i,
162
            newNode,
163
          );
164
          newNode.children[i] = newChild;
172✔
165
          newNode.dirty ||= newChild.dirty;
172✔
166
        }
167
        return newNode;
119✔
168
      }
169
      default:
170
        /** v8 ignore next @preserve */
171
        DEBUG: {
×
172
          assertUnreachable(newElement);
×
173
        }
174
    }
175
  }
176

177
  nextIdentifier(): string {
178
    return this._adapter.getIdentifier() + '-' + this._identifierCount++;
2✔
179
  }
180

181
  nextTransition(): number {
182
    return this._transitionCount++;
25✔
183
  }
184

185
  render(
186
    element: VElement,
187
    scope: Scope,
188
    index: number,
189
    parent: RenderNode | RenderRoot,
190
    part: Part,
191
  ): RenderNode {
192
    switch (true) {
499✔
193
      case element instanceof VBind: {
194
        return {
206✔
195
          type: element.type,
196
          props: element.props,
197
          key: element.key,
198
          part,
199
          index,
200
          parent,
201
          children: [],
202
          state: null,
203
          dirty: true,
204
        };
205
      }
206
      case element instanceof VComponent: {
207
        const node: RenderNode.ComponentNode = {
53✔
208
          type: element.type,
209
          props: element.props,
210
          key: element.key,
211
          part,
212
          index,
213
          parent,
214
          children: new Array(1),
215
          state: {
216
            handle: element.type.createHandle(this),
217
            scope,
218
          },
219
          dirty: true,
220
        };
221
        const subScope = scope.child(element.type);
53✔
222
        node.children[0] = this.render(
53✔
223
          node.state.handle.render(node.props, subScope, this._flushLanes),
224
          subScope,
225
          0,
226
          node,
227
          part,
228
        );
229
        return node;
53✔
230
      }
231
      case element instanceof VFragment: {
232
        const node: RenderNode.FragmentNode = {
30✔
233
          type: element.type,
234
          props: element.props,
235
          key: element.key,
236
          part,
237
          index,
238
          parent,
239
          children: new Array(element.children.length),
240
          state: { mutations: [] },
241
          dirty: element.children.length > 0,
242
        };
243
        for (let i = 0, l = element.children.length; i < l; i++) {
30✔
244
          node.children[i] = this.render(
68✔
245
            element.children[i]!,
246
            scope,
247
            i,
248
            node,
249
            part,
250
          );
251
        }
252
        return node;
30✔
253
      }
254
      case element instanceof VPortal: {
255
        const block = this._adapter.renderPortal(element);
8✔
256
        const node: RenderNode.BlockNode = {
8✔
257
          type: element.type,
258
          props: element.props,
259
          key: element.key,
260
          part,
261
          index,
262
          parent,
263
          children: new Array(1),
264
          state: { block },
265
          dirty: true,
266
        };
267
        node.children[0] = this.render(
8✔
268
          element.children[0],
269
          scope,
270
          0,
271
          node,
272
          block.parts[0]!,
273
        );
274
        return node;
8✔
275
      }
276
      case element instanceof VTemplate: {
277
        const block = this._adapter.renderTemplate(element);
202✔
278
        const node: RenderNode.BlockNode = {
202✔
279
          type: element.type,
280
          props: element.props,
281
          key: element.key,
282
          part,
283
          index,
284
          parent,
285
          children: new Array(element.children.length),
286
          state: { block },
287
          dirty: true,
288
        };
289
        for (let i = 0, l = element.children.length; i < l; i++) {
202✔
290
          node.children[i] = this.render(
211✔
291
            element.children[i]!,
292
            scope,
293
            i,
294
            node,
295
            block.parts[i]!,
296
          );
297
        }
298
        return node;
190✔
299
      }
300
      default:
301
        /** v8 ignore next @preserve */
302
        DEBUG: {
×
303
          assertUnreachable(element);
×
304
        }
305
    }
306
  }
307

308
  schedule(unit: UpdateUnit, options: UpdateOptions = {}): UpdateHandle {
309
    const controller = Promise.withResolvers<void>();
256✔
310
    const id = this._updateCount++;
256✔
311
    const lanes =
312
      getRenderLanes(options) ||
256✔
313
      getLaneFromPriority(this._adapter.getTaskPriority());
314

315
    this._updateQueue.enqueue({
256✔
316
      id,
317
      lanes,
318
      unit,
319
      controller,
320
    });
321

322
    if (((this._pendingLanes | this._stagedLanes) & lanes) !== lanes) {
256✔
323
      const priority = getPriorityFromLanes(lanes);
253✔
324
      this._pendingLanes |= lanes;
253✔
325
      this._adapter.requestCallback(
253✔
326
        () => {
327
          this._pendingLanes &= ~lanes;
253✔
328
          this._stagedLanes |= lanes;
253✔
329
          if (this._flushLanes === NoLanes) {
253✔
330
            this._flush();
250✔
331
          }
332
        },
333
        { ...options, priority },
334
      );
335
    }
336

337
    return {
256✔
338
      id,
339
      lanes,
340
      finished: controller.promise,
341
    };
342
  }
343

344
  private _diffChildren(
345
    oldParent: RenderNode.FragmentNode,
346
    newParent: RenderNode.FragmentNode,
347
    newElements: VElement[],
348
    scope: Scope,
349
  ): void {
350
    const oldChildren: (RenderNode | undefined)[] = oldParent.children.slice();
25✔
351
    const newChildren = newParent.children;
25✔
352
    const oldKeys = oldChildren.map((child) => child!.key);
58✔
353
    const newKeys = newElements.map((element) => element.key);
61✔
354
    const mutations = newParent.state.mutations;
25✔
355

356
    let oldHead = 0;
25✔
357
    let newHead = 0;
25✔
358
    let oldTail = oldKeys.length - 1;
25✔
359
    let newTail = newKeys.length - 1;
25✔
360
    let oldKeyToIndexMap: Map<unknown, number> | undefined;
361
    let newKeyToIndexMap: Map<unknown, number> | undefined;
362

363
    while (true) {
25✔
364
      if (newHead > newTail) {
78✔
365
        while (oldHead <= oldTail) {
18✔
366
          const oldChild = oldChildren[oldHead];
2✔
367
          if (oldChild !== undefined) {
2✔
368
            mutations.push({
1✔
369
              type: MUTATION_TYPE_REMOVE,
370
              node: oldChild,
371
            });
372
          }
373
          oldHead++;
2✔
374
        }
375
        break;
18✔
376
      }
377
      if (oldHead > oldTail) {
60✔
378
        while (newHead <= newTail) {
7✔
379
          const newChild = this.render(
7✔
380
            newElements[newHead]!,
381
            scope,
382
            newHead,
383
            newParent,
384
            newParent.part,
385
          );
386
          mutations.push({
7✔
387
            type: MUTATION_TYPE_INSERT,
388
            node: newChild,
389
            afterNode: newChildren[newTail + 1],
390
          });
391
          newChildren[newHead] = newChild;
7✔
392
          newHead++;
7✔
393
        }
394
        break;
7✔
395
      }
396
      if (oldChildren[oldHead] === undefined) {
53✔
397
        oldHead++;
3✔
398
      } else if (oldChildren[oldTail] === undefined) {
50✔
399
        oldTail--;
1✔
400
      } else if (Object.is(oldKeys[oldHead]!, newKeys[newHead]!)) {
49✔
401
        const oldChild = oldChildren[oldHead]!;
22✔
402
        const newChild = this.diff(
22✔
403
          oldChild,
404
          newElements[newHead]!,
405
          scope,
406
          newHead,
407
          newParent,
408
        );
409
        if (oldChild !== newChild) {
22✔
410
          mutations.push({
22✔
411
            type: MUTATION_TYPE_UPDATE,
412
            oldNode: oldChildren[oldHead]!,
413
            newNode: newChild,
414
          });
415
        }
416
        newChildren[newHead] = newChild;
22✔
417
        oldHead++;
22✔
418
        newHead++;
22✔
419
      } else if (Object.is(oldKeys[oldTail]!, newKeys[newTail]!)) {
27✔
420
        const oldChild = oldChildren[oldTail]!;
8✔
421
        const newChild = this.diff(
8✔
422
          oldChild,
423
          newElements[newTail]!,
424
          scope,
425
          newTail,
426
          newParent,
427
        );
428
        if (oldChild !== newChild) {
8✔
429
          mutations.push({
8✔
430
            type: MUTATION_TYPE_UPDATE,
431
            oldNode: oldChildren[oldTail]!,
432
            newNode: newChild,
433
          });
434
        }
435
        newChildren[newTail] = newChild;
8✔
436
        oldTail--;
8✔
437
        newTail--;
8✔
438
      } else if (
19✔
439
        Object.is(oldKeys[oldHead]!, newKeys[newTail]!) &&
440
        Object.is(oldKeys[oldTail]!, newKeys[newHead]!)
441
      ) {
442
        const tailChild = this.diff(
9✔
443
          oldChildren[oldTail]!,
444
          newElements[newHead]!,
445
          scope,
446
          newHead,
447
          newParent,
448
        );
449
        const headChild = this.diff(
9✔
450
          oldChildren[oldHead]!,
451
          newElements[newTail]!,
452
          scope,
453
          newTail,
454
          newParent,
455
        );
456
        mutations.push({
9✔
457
          type: MUTATION_TYPE_UPDATE_AND_MOVE,
458
          oldNode: oldChildren[oldTail]!,
459
          newNode: tailChild,
460
          afterNode: oldChildren[oldHead],
461
        });
462
        mutations.push({
9✔
463
          type: MUTATION_TYPE_UPDATE_AND_MOVE,
464
          oldNode: oldChildren[oldHead]!,
465
          newNode: headChild,
466
          afterNode: newChildren[newTail + 1],
467
        });
468
        newChildren[newHead] = tailChild;
9✔
469
        newChildren[newTail] = headChild;
9✔
470
        oldHead++;
9✔
471
        newHead++;
9✔
472
        oldTail--;
9✔
473
        newTail--;
9✔
474
      } else {
475
        newKeyToIndexMap ??= buildKeyToIndexMap(newKeys, newHead, newTail);
10✔
476

477
        if (!newKeyToIndexMap.has(oldKeys[oldHead]!)) {
10✔
478
          const oldChild = oldChildren[oldHead]!;
2✔
479
          mutations.push({
2✔
480
            type: MUTATION_TYPE_REMOVE,
481
            node: oldChild,
482
          });
483
          oldHead++;
2✔
484
        } else if (!newKeyToIndexMap.has(oldKeys[oldTail]!)) {
8✔
485
          const oldChild = oldChildren[oldTail]!;
2✔
486
          mutations.push({
2✔
487
            type: MUTATION_TYPE_REMOVE,
488
            node: oldChild,
489
          });
490
          oldTail--;
2✔
491
        } else {
492
          oldKeyToIndexMap ??= buildKeyToIndexMap(oldKeys, oldHead, oldTail);
6✔
493
          const oldIndex = oldKeyToIndexMap.get(newKeys[newTail]!);
6✔
494

495
          if (
6✔
496
            oldIndex !== undefined &&
497
            oldIndex >= oldHead &&
498
            oldIndex <= oldTail &&
499
            oldChildren[oldIndex] !== undefined
500
          ) {
501
            const newChild = this.diff(
5✔
502
              oldChildren[oldIndex]!,
503
              newElements[newTail]!,
504
              scope,
505
              newTail,
506
              newParent,
507
            );
508
            mutations.push({
5✔
509
              type: MUTATION_TYPE_UPDATE_AND_MOVE,
510
              oldNode: oldChildren[oldIndex]!,
511
              newNode: newChild,
512
              afterNode: newChildren[newTail + 1],
513
            });
514
            newChildren[newTail] = newChild;
5✔
515
            oldChildren[oldIndex] = undefined;
5✔
516
          } else {
517
            const newChild = this.render(
1✔
518
              newElements[newTail]!,
519
              scope,
520
              newTail,
521
              newParent,
522
              newParent.part,
523
            );
524
            mutations.push({
1✔
525
              type: MUTATION_TYPE_INSERT,
526
              node: newChild,
527
              afterNode: newChildren[newTail + 1],
528
            });
529
            newChildren[newTail] = newChild;
1✔
530
          }
531

532
          newTail--;
6✔
533
        }
534
      }
535
    }
536

537
    newParent.dirty = mutations.length > 0;
25✔
538
  }
539

540
  private async _flush(): Promise<void> {
541
    while (true) {
250✔
542
      let update: Update | undefined;
543

544
      this._flushLanes |= this._stagedLanes;
503✔
545
      this._stagedLanes = NoLanes;
503✔
546

547
      while ((update = this._updateQueue.peek()) !== undefined) {
503✔
548
        if ((this._flushLanes & update.lanes) !== update.lanes) {
257✔
549
          break;
1✔
550
        }
551
        this._updateBatch.push(this._updateQueue.dequeue()!);
256✔
552
      }
553

554
      if (this._updateBatch.length === 0) {
503✔
555
        break;
250✔
556
      }
557

558
      try {
253✔
559
        const commitBatch: Commit[] = [];
253✔
560
        const flushSync = (this._flushLanes & SyncLane) === SyncLane;
253✔
561

562
        for (const update of this._updateBatch) {
253✔
563
          const { lanes, unit } = update;
256✔
564

565
          if ((unit.pendingLanes & lanes) === NoLanes) {
256✔
566
            continue;
2✔
567
          }
568

569
          if (!flushSync && commitBatch.length > 0) {
254✔
570
            await this._adapter.yieldToMain();
1✔
571
          }
572

573
          commitBatch.push(unit.prepare(this._flushLanes, this));
254✔
574
        }
575

576
        if (commitBatch.length > 0) {
238✔
577
          const commit = () => {
238✔
578
            for (const commit of commitBatch) {
238✔
579
              commit();
239✔
580
            }
581
          };
582
          if (flushSync) {
238✔
583
            commit();
24✔
584
          } else if (this._flushLanes & ViewTransitionLane) {
214✔
585
            await this._adapter.startViewTransition(commit);
1✔
586
          } else {
587
            await this._adapter.requestCommit(commit);
213✔
588
          }
589
        }
590

591
        for (const { controller } of this._updateBatch) {
236✔
592
          controller.resolve();
239✔
593
        }
594
      } catch (error) {
595
        for (const { controller } of this._updateBatch) {
17✔
596
          controller.reject(error);
17✔
597
        }
598
      } finally {
599
        this._updateBatch = [];
253✔
600
      }
601
    }
602

603
    this._flushLanes = NoLanes;
250✔
604
  }
605
}
606

607
export async function waitForStep(runtime: Runtime): Promise<boolean> {
608
  if (runtime._updateBatch.length > 0) {
40✔
609
    for (const update of runtime._updateBatch) {
3✔
610
      await update.controller.promise;
3✔
611
    }
612
    return true;
3✔
613
  } else {
614
    const update = runtime._updateQueue.peek();
37✔
615
    if (update !== undefined) {
37✔
616
      await update.controller.promise;
34✔
617
      return true;
34✔
618
    }
619
    return false;
3✔
620
  }
621
}
622

623
function buildKeyToIndexMap<T>(
624
  keys: T[],
625
  head: number,
626
  tail: number,
627
): Map<T, number> {
628
  const keyToIndexMap = new Map();
14✔
629
  for (let i = head; i <= tail; i++) {
14✔
630
    keyToIndexMap.set(keys[i]!, i);
48✔
631
  }
632
  return keyToIndexMap;
14✔
633
}
634

635
function compareUpdates(update1: Update, update2: Update): number {
636
  const lane1 = getHighestPriorityLane(update1.lanes);
4✔
637
  const lane2 = getHighestPriorityLane(update2.lanes);
4✔
638
  const level1 = update1.unit.level;
4✔
639
  const level2 = update2.unit.level;
4✔
640
  return lane1 !== lane2 ? lane1 - lane2 : level1 - level2;
4✔
641
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc