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

emonkak / barebind / 23234668324

18 Mar 2026 07:42AM UTC coverage: 99.339% (+0.002%) from 99.337%
23234668324

push

github

emonkak
chore(test): improve naming consistency

460 of 461 branches covered (99.78%)

Branch coverage included in aggregate %.

2848 of 2869 relevant lines covered (99.27%)

508.1 hits per line

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

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

38
const DETACHED_HOOKS = Object.freeze([] as Hook[]) as Hook[];
46✔
39

40
export class RenderSession implements RenderContext {
41
  private _hooks: Hook[];
42

43
  private _hookIndex = 0;
242✔
44

45
  private readonly _frame: RenderFrame;
46

47
  private _scope: Scope;
48

49
  private readonly _coroutine: Coroutine;
50

51
  private readonly _context: SessionContext;
52

53
  constructor(
54
    hooks: Hook[],
55
    frame: RenderFrame,
56
    scope: Scope,
57
    coroutine: Coroutine,
58
    context: SessionContext,
59
  ) {
60
    this._hooks = hooks;
242✔
61
    this._frame = frame;
242✔
62
    this._scope = scope;
242✔
63
    this._coroutine = coroutine;
242✔
64
    this._context = context;
242✔
65
  }
66

67
  catchError(handler: ErrorHandler): void {
68
    this._scope.boundary = {
27✔
69
      type: BoundaryType.Error,
70
      next: this._scope.boundary,
71
      handler,
72
    };
73
  }
74

75
  finalize(): void {
76
    let currentHook = this._hooks[this._hookIndex];
223✔
77

78
    if (currentHook !== undefined) {
223✔
79
      ensureHookType<Hook.FinalizerHook>(HookType.Finalizer, currentHook);
87✔
80
    } else {
81
      currentHook = { type: HookType.Finalizer };
136✔
82
    }
83

84
    this._hooks[this._hookIndex] = currentHook;
222✔
85

86
    // Refuse to use new hooks after finalization.
87
    Object.freeze(this._hooks);
222✔
88

89
    // Refuse to mutate scope after finalization.
90
    Object.freeze(this._scope);
222✔
91

92
    this._scope = DETACHED_SCOPE;
222✔
93
    this._hooks = DETACHED_HOOKS;
222✔
94
    this._hookIndex = 0;
222✔
95
  }
96

97
  forceUpdate(options?: UpdateOptions): UpdateHandle {
98
    if (isDetachedScope(this._coroutine.scope)) {
41✔
99
      const skipped = Promise.resolve<UpdateResult>({ status: 'skipped' });
2✔
100
      return {
2✔
101
        id: -1,
102
        lanes: NoLanes,
103
        scheduled: skipped,
104
        finished: skipped,
105
      };
106
    }
107

108
    const renderLanes = this._frame.lanes;
39✔
109

110
    if (renderLanes !== NoLanes) {
39✔
111
      // We reuse the frame only for updates within the same lanes, which
112
      // avoids scheduling a new update during rendering. This is generally
113
      // undesirable, but necessary when an ErrorBoundary catches an error and
114
      // sets new state.
115
      const requestLanes = getSchedulingLanes(options ?? {});
7✔
116

117
      if ((renderLanes & requestLanes) === requestLanes) {
7✔
118
        for (const { id, controller } of this._context.getScheduledUpdates()) {
6✔
119
          if (id === this._frame.id) {
6✔
120
            this._frame.coroutines.push(this._coroutine);
6✔
121
            this._coroutine.pendingLanes |= renderLanes;
6✔
122
            return {
6✔
123
              id,
124
              lanes: renderLanes,
125
              scheduled: Promise.resolve({ status: 'skipped' }),
126
              finished: controller.promise,
127
            };
128
          }
129
        }
130
      }
131
    }
132

133
    return this._context.scheduleUpdate(this._coroutine, options);
33✔
134
  }
135

136
  getSessionContext(): SessionContext {
137
    return this._context;
10✔
138
  }
139

140
  getSharedContext<T>(key: unknown): T | undefined {
141
    let currentScope: Scope | null = this._scope;
80✔
142
    while (true) {
80✔
143
      for (
115✔
144
        let boundary = currentScope.boundary;
115✔
145
        boundary !== null;
146
        boundary = boundary.next
147
      ) {
148
        if (
109✔
149
          boundary.type === BoundaryType.SharedContext &&
150
          Object.is(boundary.key, key)
151
        ) {
152
          return boundary.value as T;
77✔
153
        }
154
      }
155
      if (currentScope.owner === null) {
38✔
156
        break;
3✔
157
      }
158
      currentScope = currentScope.owner.scope;
35✔
159
    }
160
    return undefined;
3✔
161
  }
162

163
  html(
164
    strings: readonly string[],
165
    ...values: readonly unknown[]
166
  ): DirectiveSpecifier<readonly unknown[]> {
167
    return this._createTemplate(strings, values, 'html');
35✔
168
  }
169

170
  interrupt(error: unknown): never {
171
    try {
3✔
172
      handleError(error, this._coroutine.scope);
3✔
173
    } catch (error) {
174
      throw new AbortError(
1✔
175
        this._coroutine,
176
        'No error boundary captured the error.',
177
        { cause: error },
178
      );
179
    }
180
    throw new InterruptError(
2✔
181
      this._coroutine,
182
      'The error was captured by an error boundary.',
183
      { cause: error },
184
    );
185
  }
186

187
  math(
188
    strings: readonly string[],
189
    ...values: readonly unknown[]
190
  ): DirectiveSpecifier<readonly unknown[]> {
191
    return this._createTemplate(strings, values, 'math');
2✔
192
  }
193

194
  setSharedContext<T>(key: unknown, value: T): void {
195
    this._scope.boundary = {
70✔
196
      type: BoundaryType.SharedContext,
197
      next: this._scope.boundary,
198
      key,
199
      value,
200
    };
201
  }
202

203
  startTransition<T>(action: (transition: number) => T): T {
204
    return this._context.startTransition((transition) => {
3✔
205
      const result = action(transition);
3✔
206
      if (result instanceof Promise) {
3✔
207
        result.catch((error) => {
×
208
          this.interrupt(error);
×
209
        });
210
      }
211
      return result;
3✔
212
    });
213
  }
214

215
  svg(
216
    strings: readonly string[],
217
    ...values: readonly unknown[]
218
  ): DirectiveSpecifier<readonly unknown[]> {
219
    return this._createTemplate(strings, values, 'svg');
2✔
220
  }
221

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

229
  use<T>(usable: HookClass<T>): T;
230
  use<T>(usable: HookObject<T>): T;
231
  use<T>(usable: HookFunction<T>): T;
232
  use<T>(usable: Usable<T>): T {
233
    if ($hook in usable) {
237✔
234
      return usable[$hook](this);
142✔
235
    } else {
236
      return usable(this);
95✔
237
    }
238
  }
239

240
  useCallback<TCallback extends (...args: any[]) => any>(
241
    callback: TCallback,
242
    dependencies: readonly unknown[],
243
  ): TCallback {
244
    return this.useMemo(() => callback, dependencies);
3✔
245
  }
246

247
  useEffect(
248
    setup: () => Cleanup | void,
249
    dependencies: readonly unknown[] | null = null,
250
  ): void {
251
    this._useEffectHook(
70✔
252
      setup,
253
      dependencies,
254
      HookType.PassiveEffect,
255
      this._frame.passiveEffects,
256
    );
257
  }
258

259
  useId(): string {
260
    let currentHook = this._hooks[this._hookIndex];
5✔
261

262
    if (currentHook !== undefined) {
5✔
263
      ensureHookType<Hook.IdHook>(HookType.Id, currentHook);
3✔
264
    } else {
265
      currentHook = {
2✔
266
        type: HookType.Id,
267
        id: this._context.nextIdentifier(),
268
      };
269
    }
270

271
    this._hooks[this._hookIndex] = currentHook;
4✔
272
    this._hookIndex++;
4✔
273

274
    return currentHook.id;
4✔
275
  }
276

277
  useInsertionEffect(
278
    setup: () => Cleanup | void,
279
    dependencies: readonly unknown[] | null = null,
280
  ): void {
281
    this._useEffectHook(
27✔
282
      setup,
283
      dependencies,
284
      HookType.InsertionEffect,
285
      this._frame.mutationEffects,
286
    );
287
  }
288

289
  useLayoutEffect(
290
    setup: () => Cleanup | void,
291
    dependencies: readonly unknown[] | null = null,
292
  ): void {
293
    this._useEffectHook(
143✔
294
      setup,
295
      dependencies,
296
      HookType.LayoutEffect,
297
      this._frame.layoutEffects,
298
    );
299
  }
300

301
  useMemo<TResult>(
302
    computation: () => TResult,
303
    dependencies: readonly unknown[],
304
  ): TResult {
305
    let currentHook = this._hooks[this._hookIndex];
102✔
306

307
    if (currentHook !== undefined) {
102✔
308
      ensureHookType<Hook.MemoHook<TResult>>(HookType.Memo, currentHook);
63✔
309

310
      if (
63✔
311
        areDependenciesChanged(dependencies, currentHook.memoizedDependencies)
312
      ) {
313
        currentHook = {
3✔
314
          type: HookType.Memo,
315
          memoizedResult: computation(),
316
          memoizedDependencies: dependencies,
317
        };
318
      }
319
    } else {
320
      currentHook = {
39✔
321
        type: HookType.Memo,
322
        memoizedResult: computation(),
323
        memoizedDependencies: dependencies,
324
      };
325
    }
326

327
    this._hooks[this._hookIndex] = currentHook;
101✔
328
    this._hookIndex++;
101✔
329

330
    return currentHook.memoizedResult as TResult;
101✔
331
  }
332

333
  useReducer<TState, TAction>(
334
    reducer: (state: TState, action: TAction) => TState,
335
    initialState: InitialState<TState>,
336
    options?: StateOptions,
337
  ): ReducerReturn<TState, TAction> {
338
    const { memoizedProposals, dispatcher } = this._useReducerHook(
5✔
339
      reducer,
340
      initialState,
341
      options,
342
    );
343
    return [
5✔
344
      dispatcher.pendingState,
345
      dispatcher.dispatch,
346
      memoizedProposals.length > 0,
347
    ];
348
  }
349

350
  useRef<T>(initialValue: T): RefObject<T> {
351
    return this.useMemo(() => Object.seal({ current: initialValue }), []);
9✔
352
  }
353

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

376
  private _createTemplate(
377
    strings: readonly string[],
378
    values: readonly unknown[],
379
    mode: TemplateMode,
380
  ): DirectiveSpecifier<readonly unknown[]> {
381
    const template = this._context.resolveTemplate(strings, values, mode);
40✔
382
    return new DirectiveSpecifier(template, values);
40✔
383
  }
384

385
  private _useEffectHook(
386
    setup: () => Cleanup | void,
387
    dependencies: readonly unknown[] | null,
388
    type: Hook.EffectHook['type'],
389
    queue: EffectQueue,
390
  ): void {
391
    let currentHook = this._hooks[this._hookIndex];
240✔
392

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

420
    this._hooks[this._hookIndex] = currentHook;
237✔
421
    this._hookIndex++;
237✔
422
  }
423

424
  private _useReducerHook<TState, TAction>(
425
    reducer: (state: TState, action: TAction) => TState,
426
    initialState: InitialState<TState>,
427
    options: StateOptions = {},
428
  ): Hook.ReducerHook<TState, TAction> {
429
    let currentHook = this._hooks[this._hookIndex];
81✔
430

431
    if (currentHook !== undefined) {
81✔
432
      ensureHookType<Hook.ReducerHook<TState, TAction>>(
41✔
433
        HookType.Reducer,
434
        currentHook,
435
      );
436

437
      const { dispatcher, memoizedState, memoizedProposals } = currentHook;
41✔
438
      const renderLanes = this._frame.lanes;
41✔
439
      let newState = options.passthrough
41✔
440
        ? getInitialState(initialState)
441
        : memoizedState;
442
      let skipLanes = NoLanes;
41✔
443

444
      memoizedProposals.push(...dispatcher.pendingProposals);
41✔
445

446
      for (const proposal of memoizedProposals) {
41✔
447
        const { action, lanes, revertLanes } = proposal;
25✔
448
        if ((lanes & renderLanes) === lanes) {
25✔
449
          newState = reducer(newState, action);
24✔
450
          proposal.lanes = NoLanes;
24✔
451
        } else if ((revertLanes & renderLanes) === revertLanes) {
1✔
452
          skipLanes |= lanes;
1✔
453
        }
454
      }
455

456
      if (skipLanes === NoLanes) {
40✔
457
        currentHook = {
39✔
458
          type: HookType.Reducer,
459
          dispatcher,
460
          memoizedState: newState,
461
          memoizedProposals: [],
462
        };
463
      }
464

465
      dispatcher.context = this;
40✔
466
      dispatcher.pendingState = newState;
40✔
467
      dispatcher.pendingProposals = [];
40✔
468
      dispatcher.reducer = reducer;
40✔
469
    } else {
470
      const dispatcher: ActionDispatcher<TState, TAction> = {
40✔
471
        context: this,
472
        dispatch(action, options = {}) {
473
          const { context, pendingProposals, pendingState, reducer } = this;
29✔
474

475
          if (pendingProposals.length === 0) {
29✔
476
            const areStatesEqual = options.areStatesEqual ?? Object.is;
28✔
477
            const newState = reducer(pendingState, action);
28✔
478

479
            if (areStatesEqual(newState, pendingState)) {
28✔
480
              const skipped = Promise.resolve<UpdateResult>({
4✔
481
                status: 'skipped',
482
              });
483
              return {
4✔
484
                id: -1,
485
                lanes: NoLanes,
486
                scheduled: skipped,
487
                finished: skipped,
488
              };
489
            }
490
          }
491

492
          const handle = context.forceUpdate(options);
25✔
493
          pendingProposals.push({
25✔
494
            action,
495
            lanes: handle.lanes,
496
            revertLanes: options.transient ? handle.lanes : NoLanes,
497
          });
498
          return handle;
29✔
499
        },
500
        pendingProposals: [],
501
        pendingState: getInitialState(initialState),
502
        reducer,
503
      };
504
      dispatcher.dispatch = dispatcher.dispatch.bind(dispatcher);
40✔
505
      currentHook = {
40✔
506
        type: HookType.Reducer,
507
        memoizedState: dispatcher.pendingState,
508
        memoizedProposals: [],
509
        dispatcher,
510
      };
511
    }
512

513
    this._hooks[this._hookIndex] = currentHook;
80✔
514
    this._hookIndex++;
80✔
515

516
    return currentHook;
80✔
517
  }
518
}
519

520
class InvokeEffect implements Effect {
521
  private readonly _handler: EffectHandler;
522

523
  private readonly _epoch: number;
524

525
  constructor(handler: EffectHandler) {
526
    this._handler = handler;
165✔
527
    this._epoch = handler.epoch;
165✔
528
  }
529

530
  commit(): void {
531
    const { cleanup, epoch, setup } = this._handler;
164✔
532

533
    if (epoch === this._epoch) {
164✔
534
      cleanup?.();
161✔
535
      this._handler.cleanup = setup();
161✔
536
    }
537
  }
538
}
539

540
function ensureHookType<TExpectedHook extends Hook>(
541
  expectedType: TExpectedHook['type'],
542
  hook: Hook,
543
): asserts hook is TExpectedHook {
544
  if (hook.type !== expectedType) {
290✔
545
    throw new Error(
7✔
546
      `Unexpected hook type. Expected "${expectedType}" but got "${hook.type}".`,
547
    );
548
  }
549
}
550

551
function getInitialState<TState>(initialState: InitialState<TState>): TState {
552
  return typeof initialState === 'function'
40✔
553
    ? (initialState as () => TState)()
554
    : initialState;
555
}
556

557
function isDetachedScope(scope: Scope): boolean {
558
  let currentScope: Scope | undefined = scope;
41✔
559
  do {
41✔
560
    if (currentScope === DETACHED_SCOPE) {
52✔
561
      return true;
2✔
562
    }
563
    currentScope = currentScope.owner?.scope;
50✔
564
  } while (currentScope !== undefined);
565
  return false;
39✔
566
}
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