Skip to content

Commit 16c424e

Browse files
committed
fix(web): drop type casts from terminal font readiness helpers
Replace the cast-laden timeout interface with a DelayScheduler abstraction so tests can supply a fully typed scheduler without `as` assertions, and narrow document access through a typed local variable instead of casting globalThis. Satisfies the strict effect-ts lint that forbids casting outside src/core/axioms.ts.
1 parent 382ebce commit 16c424e

2 files changed

Lines changed: 52 additions & 43 deletions

File tree

packages/app/src/web/terminal-font-readiness.ts

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ export type FontReadinessTarget = {
77
readonly ready: PromiseLike<object>
88
}
99

10+
export type DelayScheduler = {
11+
readonly schedule: (callback: () => void, delayMs: number) => () => void
12+
}
13+
1014
export type TerminalFontReadinessArgs = {
11-
readonly clearTimeoutImpl?: typeof globalThis.clearTimeout
1215
readonly descriptors: ReadonlyArray<string>
1316
readonly fonts: FontReadinessTarget | undefined
14-
readonly setTimeoutImpl?: typeof globalThis.setTimeout
17+
readonly scheduler?: DelayScheduler
1518
readonly timeoutMs?: number
1619
}
1720

@@ -45,28 +48,29 @@ const ensureFontsLoaded = (
4548
{ concurrency: "unbounded" }
4649
).pipe(Effect.asVoid)
4750

48-
type DelayTimers = {
49-
readonly clearTimeoutImpl: typeof globalThis.clearTimeout
50-
readonly setTimeoutImpl: typeof globalThis.setTimeout
51-
}
52-
5351
const delayedFallback = (
5452
timeoutMs: number,
55-
timers: DelayTimers
53+
scheduler: DelayScheduler
5654
): Effect.Effect<void> =>
5755
Effect.async((resume: (effect: Effect.Effect<void>) => void) => {
58-
const handle = timers.setTimeoutImpl(() => {
56+
const cancel = scheduler.schedule(() => {
5957
resume(Effect.void)
6058
}, timeoutMs)
6159
return Effect.sync(() => {
62-
timers.clearTimeoutImpl(handle)
60+
cancel()
6361
})
6462
})
6563

66-
const resolveTimers = (args: TerminalFontReadinessArgs): DelayTimers => ({
67-
clearTimeoutImpl: args.clearTimeoutImpl ?? globalThis.clearTimeout,
68-
setTimeoutImpl: args.setTimeoutImpl ?? globalThis.setTimeout
69-
})
64+
const defaultScheduler: DelayScheduler = {
65+
schedule: (callback, delayMs) => {
66+
const handle = globalThis.setTimeout(callback, delayMs)
67+
return () => {
68+
globalThis.clearTimeout(handle)
69+
}
70+
}
71+
}
72+
73+
const resolveScheduler = (args: TerminalFontReadinessArgs): DelayScheduler => args.scheduler ?? defaultScheduler
7074

7175
const fontReadinessEffect = (
7276
fonts: FontReadinessTarget,
@@ -77,7 +81,7 @@ const fontReadinessEffect = (
7781
if (timeoutMs <= 0) {
7882
return work
7983
}
80-
return Effect.race(work, delayedFallback(timeoutMs, resolveTimers(args)))
84+
return Effect.race(work, delayedFallback(timeoutMs, resolveScheduler(args)))
8185
}
8286

8387
export const awaitTerminalFontReadiness = (
@@ -89,7 +93,11 @@ export const awaitTerminalFontReadiness = (
8993
return fontReadinessEffect(args.fonts, args)
9094
}
9195

96+
type GlobalThisWithDocument = {
97+
readonly document?: { readonly fonts: FontReadinessTarget }
98+
}
99+
92100
export const resolveDocumentFontFaceSet = (): FontReadinessTarget | undefined => {
93-
const documentRef = (globalThis as { readonly document?: { readonly fonts?: FontReadinessTarget } }).document
94-
return documentRef?.fonts
101+
const globals: GlobalThisWithDocument = globalThis
102+
return globals.document?.fonts
95103
}

packages/app/tests/docker-git/terminal-font-readiness.test.ts

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, expect, it } from "@effect/vitest"
22
import { Deferred, Effect, Exit, Fiber } from "effect"
33

4-
import { awaitTerminalFontReadiness } from "../../src/web/terminal-font-readiness.js"
4+
import { awaitTerminalFontReadiness, type DelayScheduler } from "../../src/web/terminal-font-readiness.js"
55

66
type DeferredHandle<A> = {
77
readonly deferred: Deferred.Deferred<A, Error>
@@ -51,8 +51,9 @@ const createFontsMock = (
5151
rejectReady: () => Deferred.fail(readyHandle.deferred, new Error("fonts ready failed")).pipe(Effect.asVoid),
5252
resolveLoads: () =>
5353
Effect.gen(function*(_) {
54+
const emptyResult: ReadonlyArray<object> = []
5455
for (const handle of loadHandles) {
55-
yield* _(Deferred.succeed(handle.deferred, [] as ReadonlyArray<object>))
56+
yield* _(Deferred.succeed(handle.deferred, emptyResult))
5657
}
5758
}),
5859
resolveReady: () => Deferred.succeed(readyHandle.deferred, {}).pipe(Effect.asVoid)
@@ -65,34 +66,35 @@ const makeFontsMock = (): Effect.Effect<FontFaceSetMock> =>
6566
return createFontsMock(readyHandle, () => makeDeferredHandle<ReadonlyArray<object>>())
6667
})
6768

68-
type TimersMock = {
69-
readonly cleared: ReadonlyArray<object>
70-
readonly clearTimeoutImpl: typeof globalThis.clearTimeout
69+
type SchedulerMock = {
70+
readonly cancelCount: () => number
7171
readonly fire: () => void
72-
readonly setTimeoutImpl: typeof globalThis.setTimeout
72+
readonly scheduler: DelayScheduler
7373
}
7474

75-
const createTimers = (): TimersMock => {
75+
const createScheduler = (): SchedulerMock => {
7676
const handlers: Array<{ readonly callback: () => void; readonly id: object }> = []
77-
const cleared: Array<object> = []
78-
return {
79-
cleared,
80-
clearTimeoutImpl: ((id: object) => {
81-
cleared.push(id)
82-
const index = handlers.findIndex((handler) => handler.id === id)
83-
if (index !== -1) {
84-
handlers.splice(index, 1)
77+
let cancelCount = 0
78+
const scheduler: DelayScheduler = {
79+
schedule: (callback) => {
80+
const id = {}
81+
handlers.push({ callback, id })
82+
return () => {
83+
cancelCount += 1
84+
const index = handlers.findIndex((handler) => handler.id === id)
85+
if (index !== -1) {
86+
handlers.splice(index, 1)
87+
}
8588
}
86-
}) as typeof globalThis.clearTimeout,
89+
}
90+
}
91+
return {
92+
cancelCount: () => cancelCount,
8793
fire: () => {
8894
const next = handlers.shift()
8995
next?.callback()
9096
},
91-
setTimeoutImpl: ((callback: () => void) => {
92-
const id = {}
93-
handlers.push({ callback, id })
94-
return id as ReturnType<typeof globalThis.setTimeout>
95-
}) as typeof globalThis.setTimeout
97+
scheduler
9698
}
9799
}
98100

@@ -105,7 +107,7 @@ const yieldThrice = Effect.gen(function*(_) {
105107
type FontReadinessFixture = {
106108
readonly fiber: Fiber.RuntimeFiber<void>
107109
readonly fonts: FontFaceSetMock
108-
readonly timers: TimersMock
110+
readonly timers: SchedulerMock
109111
}
110112

111113
const startFontReadinessFixture = (
@@ -114,12 +116,11 @@ const startFontReadinessFixture = (
114116
): Effect.Effect<FontReadinessFixture> =>
115117
Effect.gen(function*(_) {
116118
const fonts = yield* _(makeFontsMock())
117-
const timers = createTimers()
119+
const timers = createScheduler()
118120
const baseArgs = {
119-
clearTimeoutImpl: timers.clearTimeoutImpl,
120121
descriptors,
121122
fonts,
122-
setTimeoutImpl: timers.setTimeoutImpl
123+
scheduler: timers.scheduler
123124
}
124125
const args = timeoutMs === undefined ? baseArgs : { ...baseArgs, timeoutMs }
125126
const fiber = yield* _(Effect.fork(awaitTerminalFontReadiness(args)))
@@ -144,7 +145,7 @@ describe("terminal font readiness", () => {
144145
yield* _(fonts.resolveReady())
145146
yield* _(fonts.resolveLoads())
146147
yield* _(Fiber.join(fiber))
147-
expect(timers.cleared.length).toBe(1)
148+
expect(timers.cancelCount()).toBe(1)
148149
}))
149150

150151
it.effect("swallows load failures so callers still proceed", () =>

0 commit comments

Comments
 (0)