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

emonkak / barebind / 23231803855

18 Mar 2026 06:16AM UTC coverage: 99.337% (-0.002%) from 99.339%
23231803855

push

github

emonkak
chore(test): improve naming consistency

458 of 459 branches covered (99.78%)

Branch coverage included in aggregate %.

2840 of 2861 relevant lines covered (99.27%)

509.06 hits per line

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

98.86
/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 EffectQueue,
11
  type ErrorHandler,
12
  type Hook,
13
  type HookClass,
14
  type HookFunction,
15
  type HookObject,
16
  HookType,
17
  type InitialState,
18
  type NextState,
19
  type ReducerReturn,
20
  type RefObject,
21
  type RenderContext,
22
  type RenderFrame,
23
  type Scope,
24
  type SessionContext,
25
  type StateOptions,
26
  type StateReturn,
27
  type TemplateMode,
28
  type UpdateHandle,
29
  type UpdateOptions,
30
  type UpdateResult,
31
  type Usable,
32
} from './core.js';
33
import { DirectiveSpecifier } from './directive.js';
34
import { AbortError, handleError, InterruptError } from './error.js';
35
import { getSchedulingLanes, NoLanes } from './lane.js';
36

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

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

42
  private _hookIndex = 0;
241✔
43

44
  private readonly _frame: RenderFrame;
45

46
  private _scope: Scope;
47

48
  private readonly _coroutine: Coroutine;
49

50
  private readonly _context: SessionContext;
51

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

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

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

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

83
    this._hooks[this._hookIndex] = currentHook;
221✔
84

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

273
    return currentHook.id;
4✔
274
  }
275

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

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

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

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

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

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

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

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

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

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

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

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

392
    if (currentHook !== undefined) {
237✔
393
      ensureHookType<Hook.EffectHook>(type, currentHook);
96✔
394
      const { cleanup, memoizedDependencies } = currentHook;
96✔
395
      if (areDependenciesChanged(dependencies, memoizedDependencies)) {
96✔
396
        currentHook = {
21✔
397
          type,
398
          setup,
399
          cleanup,
400
          memoizedDependencies: dependencies,
401
        };
402
        queue.push(new InvokeEffect(currentHook), this._scope.level);
21✔
403
      }
404
    } else {
405
      currentHook = {
141✔
406
        type,
407
        setup,
408
        cleanup: undefined,
409
        memoizedDependencies: dependencies,
410
      };
411
      queue.push(new InvokeEffect(currentHook), this._scope.level);
141✔
412
    }
413

414
    this._hooks[this._hookIndex] = currentHook;
234✔
415
    this._hookIndex++;
234✔
416
  }
417

418
  private _useReducerHook<TState, TAction>(
419
    reducer: (state: TState, action: TAction) => TState,
420
    initialState: InitialState<TState>,
421
    options: StateOptions = {},
422
  ): Hook.ReducerHook<TState, TAction> {
423
    let currentHook = this._hooks[this._hookIndex];
81✔
424

425
    if (currentHook !== undefined) {
81✔
426
      ensureHookType<Hook.ReducerHook<TState, TAction>>(
41✔
427
        HookType.Reducer,
428
        currentHook,
429
      );
430

431
      const { dispatcher, memoizedState, memoizedProposals } = currentHook;
41✔
432
      const renderLanes = this._frame.lanes;
41✔
433
      let newState = options.passthrough
41✔
434
        ? getInitialState(initialState)
435
        : memoizedState;
436
      let skipLanes = NoLanes;
41✔
437

438
      memoizedProposals.push(...dispatcher.pendingProposals);
41✔
439

440
      for (const proposal of memoizedProposals) {
41✔
441
        const { action, lanes, revertLanes } = proposal;
25✔
442
        if ((lanes & renderLanes) === lanes) {
25✔
443
          newState = reducer(newState, action);
24✔
444
          proposal.lanes = NoLanes;
24✔
445
        } else if ((revertLanes & renderLanes) === revertLanes) {
1✔
446
          skipLanes |= lanes;
1✔
447
        }
448
      }
449

450
      if (skipLanes === NoLanes) {
40✔
451
        currentHook = {
39✔
452
          type: HookType.Reducer,
453
          dispatcher,
454
          memoizedState: newState,
455
          memoizedProposals: [],
456
        };
457
      }
458

459
      dispatcher.context = this;
40✔
460
      dispatcher.pendingState = newState;
40✔
461
      dispatcher.pendingProposals = [];
40✔
462
      dispatcher.reducer = reducer;
40✔
463
    } else {
464
      const dispatcher: ActionDispatcher<TState, TAction> = {
40✔
465
        context: this,
466
        dispatch(action, options = {}) {
467
          const { context, pendingProposals, pendingState, reducer } = this;
29✔
468

469
          if (pendingProposals.length === 0) {
29✔
470
            const areStatesEqual = options.areStatesEqual ?? Object.is;
28✔
471
            const newState = reducer(pendingState, action);
28✔
472

473
            if (areStatesEqual(newState, pendingState)) {
28✔
474
              const skipped = Promise.resolve<UpdateResult>({
4✔
475
                status: 'skipped',
476
              });
477
              return {
4✔
478
                id: -1,
479
                lanes: NoLanes,
480
                scheduled: skipped,
481
                finished: skipped,
482
              };
483
            }
484
          }
485

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

507
    this._hooks[this._hookIndex] = currentHook;
80✔
508
    this._hookIndex++;
80✔
509

510
    return currentHook;
80✔
511
  }
512
}
513

514
class InvokeEffect implements Effect {
515
  private readonly _hook: Hook.EffectHook;
516

517
  constructor(hook: Hook.EffectHook) {
518
    this._hook = hook;
162✔
519
  }
520

521
  commit(): void {
522
    const { cleanup, setup } = this._hook;
161✔
523
    cleanup?.();
161✔
524
    this._hook.cleanup = setup();
161✔
525
  }
526
}
527

528
function ensureHookType<TExpectedHook extends Hook>(
529
  expectedType: TExpectedHook['type'],
530
  hook: Hook,
531
): asserts hook is TExpectedHook {
532
  if (hook.type !== expectedType) {
290✔
533
    throw new Error(
7✔
534
      `Unexpected hook type. Expected "${expectedType}" but got "${hook.type}".`,
535
    );
536
  }
537
}
538

539
function getInitialState<TState>(initialState: InitialState<TState>): TState {
540
  return typeof initialState === 'function'
40✔
541
    ? (initialState as () => TState)()
542
    : initialState;
543
}
544

545
function isDetachedScope(scope: Scope): boolean {
546
  let currentScope: Scope | undefined = scope;
41✔
547
  do {
41✔
548
    if (currentScope === DETACHED_SCOPE) {
52✔
549
      return true;
2✔
550
    }
551
    currentScope = currentScope.owner?.scope;
50✔
552
  } while (currentScope !== undefined);
553
  return false;
39✔
554
}
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