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

emonkak / barebind / 23293404995

19 Mar 2026 11:47AM UTC coverage: 99.202% (-0.1%) from 99.339%
23293404995

push

github

emonkak
refactor(template): rename TemplateResult.children to childNodes for clarity

466 of 467 branches covered (99.79%)

Branch coverage included in aggregate %.

23 of 23 new or added lines in 6 files covered. (100.0%)

7 existing lines in 6 files now uncovered.

2890 of 2916 relevant lines covered (99.11%)

502.37 hits per line

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

98.91
/src/render-session.ts
1
import { areDependenciesChanged } from './compare.js';
2
import {
3
  $hook,
4
  type ActionDispatcher,
5
  BOUNDARY_TYPE_ERROR,
6
  BOUNDARY_TYPE_SHARED_CONTEXT,
7
  type Cleanup,
8
  type Coroutine,
9
  type Effect,
10
  type EffectHandler,
11
  type EffectQueue,
12
  type ErrorHandler,
13
  HOOK_TYPE_FINALIZER,
14
  HOOK_TYPE_ID,
15
  HOOK_TYPE_INSERTION_EFFECT,
16
  HOOK_TYPE_LAYOUT_EFFECT,
17
  HOOK_TYPE_MEMO,
18
  HOOK_TYPE_PASSIVE_EFFECT,
19
  HOOK_TYPE_REDUCER,
20
  type Hook,
21
  type HookClass,
22
  type HookFunction,
23
  type HookObject,
24
  type InitialState,
25
  type NextState,
26
  type ReducerReturn,
27
  type RefObject,
28
  type RenderContext,
29
  type RenderFrame,
30
  SCOPE_DETACHED,
31
  type Scope,
32
  type SessionContext,
33
  type StateOptions,
34
  type StateReturn,
35
  type TemplateMode,
36
  type UpdateHandle,
37
  type UpdateOptions,
38
  type UpdateResult,
39
  type Usable,
40
} from './core.js';
41
import { DirectiveSpecifier } from './directive.js';
42
import { AbortError, handleError, InterruptError } from './error.js';
43
import { getSchedulingLanes, NoLanes } from './lane.js';
44

45
const DETACHED_HOOKS = Object.freeze([] as Hook[]) as Hook[];
48✔
46

47
export class RenderSession implements RenderContext {
48
  private _hooks: Hook[];
49

50
  private _hookIndex = 0;
242✔
51

52
  private readonly _frame: RenderFrame;
53

54
  private _scope: Scope;
55

56
  private readonly _coroutine: Coroutine;
57

58
  private readonly _context: SessionContext;
59

60
  constructor(
61
    hooks: Hook[],
62
    frame: RenderFrame,
63
    scope: Scope,
64
    coroutine: Coroutine,
65
    context: SessionContext,
66
  ) {
67
    this._hooks = hooks;
242✔
68
    this._frame = frame;
242✔
69
    this._scope = scope;
242✔
70
    this._coroutine = coroutine;
242✔
71
    this._context = context;
242✔
72
  }
73

74
  catchError(handler: ErrorHandler): void {
75
    this._scope.boundary = {
27✔
76
      type: BOUNDARY_TYPE_ERROR,
77
      next: this._scope.boundary,
78
      handler,
79
    };
80
  }
81

82
  finalize(): void {
83
    let currentHook = this._hooks[this._hookIndex];
223✔
84

85
    if (currentHook !== undefined) {
223✔
86
      ensureHookType<Hook.FinalizerHook>(HOOK_TYPE_FINALIZER, currentHook);
87✔
87
    } else {
88
      currentHook = { type: HOOK_TYPE_FINALIZER };
136✔
89
    }
90

91
    this._hooks[this._hookIndex] = currentHook;
222✔
92

93
    // Refuse to use new hooks after finalization.
94
    Object.freeze(this._hooks);
222✔
95

96
    // Refuse to mutate scope after finalization.
97
    Object.freeze(this._scope);
222✔
98

99
    this._scope = SCOPE_DETACHED;
222✔
100
    this._hooks = DETACHED_HOOKS;
222✔
101
    this._hookIndex = 0;
222✔
102
  }
103

104
  forceUpdate(options?: UpdateOptions): UpdateHandle {
105
    if (isDetachedScope(this._coroutine.scope)) {
41✔
106
      const skipped = Promise.resolve<UpdateResult>({ status: 'skipped' });
2✔
107
      return {
2✔
108
        id: -1,
109
        lanes: NoLanes,
110
        scheduled: skipped,
111
        finished: skipped,
112
      };
113
    }
114

115
    const renderLanes = this._frame.lanes;
39✔
116

117
    if (renderLanes !== NoLanes) {
39✔
118
      // We reuse the frame only for updates within the same lanes, which
119
      // avoids scheduling a new update during rendering. This is generally
120
      // undesirable, but necessary when an ErrorBoundary catches an error and
121
      // sets new state.
122
      const requestLanes = getSchedulingLanes(options ?? {});
7✔
123

124
      if ((renderLanes & requestLanes) === requestLanes) {
7✔
125
        for (const { id, controller } of this._context.getScheduledUpdates()) {
6✔
126
          if (id === this._frame.id) {
6✔
127
            this._frame.coroutines.push(this._coroutine);
6✔
128
            this._coroutine.pendingLanes |= renderLanes;
6✔
129
            return {
6✔
130
              id,
131
              lanes: renderLanes,
132
              scheduled: Promise.resolve({ status: 'skipped' }),
133
              finished: controller.promise,
134
            };
135
          }
136
        }
137
      }
138
    }
139

140
    return this._context.scheduleUpdate(this._coroutine, options);
33✔
141
  }
142

143
  getSessionContext(): SessionContext {
144
    return this._context;
10✔
145
  }
146

147
  getSharedContext<T>(key: unknown): T | undefined {
148
    let currentScope: Scope | null = this._scope;
80✔
149
    while (true) {
80✔
150
      for (
115✔
151
        let boundary = currentScope.boundary;
115✔
152
        boundary !== null;
153
        boundary = boundary.next
154
      ) {
155
        if (
109✔
156
          boundary.type === BOUNDARY_TYPE_SHARED_CONTEXT &&
157
          Object.is(boundary.key, key)
158
        ) {
159
          return boundary.value as T;
77✔
160
        }
161
      }
162
      if (currentScope.owner === null) {
38✔
163
        break;
3✔
164
      }
165
      currentScope = currentScope.owner.scope;
35✔
166
    }
167
    return undefined;
3✔
168
  }
169

170
  html(
171
    strings: readonly string[],
172
    ...values: readonly unknown[]
173
  ): DirectiveSpecifier<readonly unknown[]> {
174
    return this._createTemplate(strings, values, 'html');
35✔
175
  }
176

177
  interrupt(error: unknown): never {
178
    try {
3✔
179
      handleError(error, this._coroutine.scope);
3✔
180
    } catch (error) {
181
      throw new AbortError(
1✔
182
        this._coroutine,
183
        'No error boundary captured the error.',
184
        { cause: error },
185
      );
186
    }
187
    throw new InterruptError(
2✔
188
      this._coroutine,
189
      'The error was captured by an error boundary.',
190
      { cause: error },
191
    );
192
  }
193

194
  math(
195
    strings: readonly string[],
196
    ...values: readonly unknown[]
197
  ): DirectiveSpecifier<readonly unknown[]> {
198
    return this._createTemplate(strings, values, 'math');
2✔
199
  }
200

201
  setSharedContext<T>(key: unknown, value: T): void {
202
    this._scope.boundary = {
70✔
203
      type: BOUNDARY_TYPE_SHARED_CONTEXT,
204
      next: this._scope.boundary,
205
      key,
206
      value,
207
    };
208
  }
209

210
  startTransition<T>(action: (transition: number) => T): T {
211
    return this._context.startTransition((transition) => {
3✔
212
      const result = action(transition);
3✔
213
      if (result instanceof Promise) {
3✔
UNCOV
214
        result.catch((error) => {
×
UNCOV
215
          this.interrupt(error);
×
216
        });
217
      }
218
      return result;
3✔
219
    });
220
  }
221

222
  svg(
223
    strings: readonly string[],
224
    ...values: readonly unknown[]
225
  ): DirectiveSpecifier<readonly unknown[]> {
226
    return this._createTemplate(strings, values, 'svg');
2✔
227
  }
228

229
  text(
230
    strings: readonly string[],
231
    ...values: readonly unknown[]
232
  ): DirectiveSpecifier<readonly unknown[]> {
233
    return this._createTemplate(strings, values, 'textarea');
1✔
234
  }
235

236
  use<T>(usable: HookClass<T>): T;
237
  use<T>(usable: HookObject<T>): T;
238
  use<T>(usable: HookFunction<T>): T;
239
  use<T>(usable: Usable<T>): T {
240
    if ($hook in usable) {
237✔
241
      return usable[$hook](this);
142✔
242
    } else {
243
      return usable(this);
95✔
244
    }
245
  }
246

247
  useCallback<TCallback extends (...args: any[]) => any>(
248
    callback: TCallback,
249
    dependencies: readonly unknown[],
250
  ): TCallback {
251
    return this.useMemo(() => callback, dependencies);
3✔
252
  }
253

254
  useEffect(
255
    setup: () => Cleanup | void,
256
    dependencies: readonly unknown[] | null = null,
257
  ): void {
258
    this._useEffectHook(
70✔
259
      setup,
260
      dependencies,
261
      HOOK_TYPE_PASSIVE_EFFECT,
262
      this._frame.passiveEffects,
263
    );
264
  }
265

266
  useId(): string {
267
    let currentHook = this._hooks[this._hookIndex];
5✔
268

269
    if (currentHook !== undefined) {
5✔
270
      ensureHookType<Hook.IdHook>(HOOK_TYPE_ID, currentHook);
3✔
271
    } else {
272
      currentHook = {
2✔
273
        type: HOOK_TYPE_ID,
274
        id: this._context.nextIdentifier(),
275
      };
276
    }
277

278
    this._hooks[this._hookIndex] = currentHook;
4✔
279
    this._hookIndex++;
4✔
280

281
    return currentHook.id;
4✔
282
  }
283

284
  useInsertionEffect(
285
    setup: () => Cleanup | void,
286
    dependencies: readonly unknown[] | null = null,
287
  ): void {
288
    this._useEffectHook(
27✔
289
      setup,
290
      dependencies,
291
      HOOK_TYPE_INSERTION_EFFECT,
292
      this._frame.mutationEffects,
293
    );
294
  }
295

296
  useLayoutEffect(
297
    setup: () => Cleanup | void,
298
    dependencies: readonly unknown[] | null = null,
299
  ): void {
300
    this._useEffectHook(
143✔
301
      setup,
302
      dependencies,
303
      HOOK_TYPE_LAYOUT_EFFECT,
304
      this._frame.layoutEffects,
305
    );
306
  }
307

308
  useMemo<TResult>(
309
    computation: () => TResult,
310
    dependencies: readonly unknown[],
311
  ): TResult {
312
    let currentHook = this._hooks[this._hookIndex];
102✔
313

314
    if (currentHook !== undefined) {
102✔
315
      ensureHookType<Hook.MemoHook<TResult>>(HOOK_TYPE_MEMO, currentHook);
63✔
316

317
      if (
63✔
318
        areDependenciesChanged(dependencies, currentHook.memoizedDependencies)
319
      ) {
320
        currentHook = {
3✔
321
          type: HOOK_TYPE_MEMO,
322
          memoizedResult: computation(),
323
          memoizedDependencies: dependencies,
324
        };
325
      }
326
    } else {
327
      currentHook = {
39✔
328
        type: HOOK_TYPE_MEMO,
329
        memoizedResult: computation(),
330
        memoizedDependencies: dependencies,
331
      };
332
    }
333

334
    this._hooks[this._hookIndex] = currentHook;
101✔
335
    this._hookIndex++;
101✔
336

337
    return currentHook.memoizedResult as TResult;
101✔
338
  }
339

340
  useReducer<TState, TAction>(
341
    reducer: (state: TState, action: TAction) => TState,
342
    initialState: InitialState<TState>,
343
    options?: StateOptions,
344
  ): ReducerReturn<TState, TAction> {
345
    const { memoizedProposals, dispatcher } = this._useReducerHook(
5✔
346
      reducer,
347
      initialState,
348
      options,
349
    );
350
    return [
5✔
351
      dispatcher.pendingState,
352
      dispatcher.dispatch,
353
      memoizedProposals.length > 0,
354
    ];
355
  }
356

357
  useRef<T>(initialValue: T): RefObject<T> {
358
    return this.useMemo(() => Object.seal({ current: initialValue }), []);
9✔
359
  }
360

361
  useState<TState>(
362
    initialState: InitialState<TState>,
363
    options?: StateOptions,
364
  ): StateReturn<TState> {
365
    const { memoizedProposals, dispatcher } = this._useReducerHook<
76✔
366
      TState,
367
      NextState<TState>
368
    >(
369
      (state, action) =>
370
        typeof action === 'function'
49✔
371
          ? (action as (prevState: TState) => TState)(state)
372
          : action,
373
      initialState,
374
      options,
375
    );
376
    return [
76✔
377
      dispatcher.pendingState,
378
      dispatcher.dispatch,
379
      memoizedProposals.length > 0,
380
    ];
381
  }
382

383
  private _createTemplate(
384
    strings: readonly string[],
385
    values: readonly unknown[],
386
    mode: TemplateMode,
387
  ): DirectiveSpecifier<readonly unknown[]> {
388
    const template = this._context.resolveTemplate(strings, values, mode);
40✔
389
    return new DirectiveSpecifier(template, values);
40✔
390
  }
391

392
  private _useEffectHook(
393
    setup: () => Cleanup | void,
394
    dependencies: readonly unknown[] | null,
395
    type: Hook.EffectHook['type'],
396
    queue: EffectQueue,
397
  ): void {
398
    let currentHook = this._hooks[this._hookIndex];
240✔
399

400
    if (currentHook !== undefined) {
240✔
401
      ensureHookType<Hook.EffectHook>(type, currentHook);
96✔
402
      const { handler, memoizedDependencies } = currentHook;
96✔
403
      if (areDependenciesChanged(dependencies, memoizedDependencies)) {
96✔
404
        handler.epoch++;
21✔
405
        queue.push(new InvokeEffect(handler), this._scope.level);
21✔
406
        currentHook = {
21✔
407
          type,
408
          handler,
409
          memoizedDependencies: dependencies,
410
        };
411
      }
412
      handler.setup = setup;
93✔
413
    } else {
414
      const handler: EffectHandler = {
144✔
415
        setup,
416
        cleanup: undefined,
417
        epoch: 0,
418
      };
419
      currentHook = {
144✔
420
        type,
421
        handler,
422
        memoizedDependencies: dependencies,
423
      };
424
      queue.push(new InvokeEffect(handler), this._scope.level);
144✔
425
    }
426

427
    this._hooks[this._hookIndex] = currentHook;
237✔
428
    this._hookIndex++;
237✔
429
  }
430

431
  private _useReducerHook<TState, TAction>(
432
    reducer: (state: TState, action: TAction) => TState,
433
    initialState: InitialState<TState>,
434
    options: StateOptions = {},
435
  ): Hook.ReducerHook<TState, TAction> {
436
    let currentHook = this._hooks[this._hookIndex];
81✔
437

438
    if (currentHook !== undefined) {
81✔
439
      ensureHookType<Hook.ReducerHook<TState, TAction>>(
41✔
440
        HOOK_TYPE_REDUCER,
441
        currentHook,
442
      );
443

444
      const { dispatcher, memoizedState, memoizedProposals } = currentHook;
41✔
445
      const renderLanes = this._frame.lanes;
41✔
446
      let newState = options.passthrough
41✔
447
        ? getInitialState(initialState)
448
        : memoizedState;
449
      let skipLanes = NoLanes;
41✔
450

451
      memoizedProposals.push(...dispatcher.pendingProposals);
41✔
452

453
      for (const proposal of memoizedProposals) {
41✔
454
        const { action, lanes, revertLanes } = proposal;
25✔
455
        if ((lanes & renderLanes) === lanes) {
25✔
456
          newState = reducer(newState, action);
24✔
457
          proposal.lanes = NoLanes;
24✔
458
        } else if ((revertLanes & renderLanes) === revertLanes) {
1✔
459
          skipLanes |= lanes;
1✔
460
          proposal.revertLanes = NoLanes;
1✔
461
        }
462
      }
463

464
      if (skipLanes === NoLanes) {
40✔
465
        currentHook = {
39✔
466
          type: HOOK_TYPE_REDUCER,
467
          dispatcher,
468
          memoizedState: newState,
469
          memoizedProposals: [],
470
        };
471
      }
472

473
      dispatcher.context = this;
40✔
474
      dispatcher.pendingState = newState;
40✔
475
      dispatcher.pendingProposals = [];
40✔
476
      dispatcher.reducer = reducer;
40✔
477
    } else {
478
      const dispatcher: ActionDispatcher<TState, TAction> = {
40✔
479
        context: this,
480
        dispatch(action, options = {}) {
481
          const { context, pendingProposals, pendingState, reducer } = this;
29✔
482

483
          if (pendingProposals.length === 0) {
29✔
484
            const areStatesEqual = options.areStatesEqual ?? Object.is;
28✔
485
            const newState = reducer(pendingState, action);
28✔
486

487
            if (areStatesEqual(newState, pendingState)) {
28✔
488
              const skipped = Promise.resolve<UpdateResult>({
4✔
489
                status: 'skipped',
490
              });
491
              return {
4✔
492
                id: -1,
493
                lanes: NoLanes,
494
                scheduled: skipped,
495
                finished: skipped,
496
              };
497
            }
498
          }
499

500
          const handle = context.forceUpdate(options);
25✔
501
          pendingProposals.push({
25✔
502
            action,
503
            lanes: handle.lanes,
504
            revertLanes: options.transient ? handle.lanes : NoLanes,
505
          });
506
          return handle;
29✔
507
        },
508
        pendingProposals: [],
509
        pendingState: getInitialState(initialState),
510
        reducer,
511
      };
512
      dispatcher.dispatch = dispatcher.dispatch.bind(dispatcher);
40✔
513
      currentHook = {
40✔
514
        type: HOOK_TYPE_REDUCER,
515
        memoizedState: dispatcher.pendingState,
516
        memoizedProposals: [],
517
        dispatcher,
518
      };
519
    }
520

521
    this._hooks[this._hookIndex] = currentHook;
80✔
522
    this._hookIndex++;
80✔
523

524
    return currentHook;
80✔
525
  }
526
}
527

528
class InvokeEffect implements Effect {
529
  private readonly _handler: EffectHandler;
530

531
  private readonly _epoch: number;
532

533
  constructor(handler: EffectHandler) {
534
    this._handler = handler;
165✔
535
    this._epoch = handler.epoch;
165✔
536
  }
537

538
  commit(): void {
539
    const { cleanup, epoch, setup } = this._handler;
164✔
540

541
    if (epoch === this._epoch) {
164✔
542
      cleanup?.();
161✔
543
      this._handler.cleanup = setup();
161✔
544
    }
545
  }
546
}
547

548
function ensureHookType<TExpectedHook extends Hook>(
549
  expectedType: TExpectedHook['type'],
550
  hook: Hook,
551
): asserts hook is TExpectedHook {
552
  if (hook.type !== expectedType) {
290✔
553
    throw new Error(
7✔
554
      `Unexpected hook type. Expected "${expectedType}" but got "${hook.type}".`,
555
    );
556
  }
557
}
558

559
function getInitialState<TState>(initialState: InitialState<TState>): TState {
560
  return typeof initialState === 'function'
40✔
561
    ? (initialState as () => TState)()
562
    : initialState;
563
}
564

565
function isDetachedScope(scope: Scope): boolean {
566
  let currentScope: Scope | undefined = scope;
41✔
567
  do {
41✔
568
    if (currentScope === SCOPE_DETACHED) {
52✔
569
      return true;
2✔
570
    }
571
    currentScope = currentScope.owner?.scope;
50✔
572
  } while (currentScope !== undefined);
573
  return false;
39✔
574
}
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