Scope serial and flash manager to component lifetime#181
Conversation
Replace global serial port state with a scoped SerialContext utility and add a FlashContext that manages FlashManager lifecycle with onMount cleanup. Both contexts automatically clean up (close ports, disconnect) on unmount. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR scopes serial port and firmware flashing state to the flashtool component lifetime by replacing a global singleton with per-component SerialContext / FlashContext instances and updating the flashtool UI to pass these contexts via props.
Changes:
- Introduces
SerialContext(useSerial) with mount/unmount cleanup for serial listeners/ports. - Introduces
FlashContext(useFlashManager) with mount/unmount cleanup for flash manager disconnects. - Updates flashtool page and components to use the scoped contexts; disables SSR for the flashtool route.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/routes/flashtool/SerialPortSelector.svelte | Switches from global serial singleton to injected SerialContext prop. |
| src/routes/flashtool/flash-context.svelte.ts | Adds FlashContext wrapper around FlashManager with lifecycle cleanup. |
| src/routes/flashtool/FirmwareFlasher.svelte | Accepts FlashContext instead of a raw FlashManager + bindable flashing state. |
| src/routes/flashtool/+page.ts | Disables SSR for the flashtool route. |
| src/routes/flashtool/+page.svelte | Creates and wires serial + flash contexts into child components. |
| src/lib/utils/serial-context.svelte.ts | Adds SerialContext abstraction replacing global serial ports state. |
| src/lib/state/serial-ports-state.svelte.ts | Removes the previous global serial ports singleton. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import { Cpu, TriangleAlert, Unplug } from '@lucide/svelte'; | ||
| import { Button } from '$lib/components/ui/button'; | ||
| import { serialPortsState } from '$lib/state/serial-ports-state.svelte'; | ||
| import type { SerialContext } from '$lib/utils/serial-context.svelte'; |
There was a problem hiding this comment.
Import path likely won't resolve: the module file is serial-context.svelte.ts, but this imports '$lib/utils/serial-context.svelte'. In this repo, .svelte.ts modules are imported with the full extension (e.g. src/lib/components/dialog-manager/dialog-manager.svelte:3 imports ./dialog-store.svelte.ts). Update the import to target the actual file name so the build doesn't fail.
| import type { SerialContext } from '$lib/utils/serial-context.svelte'; | |
| import type { SerialContext } from '$lib/utils/serial-context.svelte.ts'; |
| this.#manager?.disconnect(); | ||
| this.#manager = null; |
There was a problem hiding this comment.
FlashContext.destroy() calls the async FlashManager.disconnect() without awaiting or handling rejection, which can lead to an unhandled promise rejection during component unmount. Consider making destroy fire-and-forget safely (e.g. void ...disconnect().catch(...)) or reworking cleanup so disconnect is awaited from an async teardown path.
| this.#manager?.disconnect(); | |
| this.#manager = null; | |
| const manager = this.#manager; | |
| this.#manager = null; | |
| if (manager) { | |
| void manager.disconnect().catch((error) => { | |
| console.error('FlashManager disconnect() failed during destroy():', error); | |
| }); | |
| } |
| import { Button } from '$lib/components/ui/button'; | ||
| import { Progress } from '$lib/components/ui/progress'; | ||
| import FlashManager from './FlashManager'; | ||
| import type { FlashContext } from './flash-context.svelte'; |
There was a problem hiding this comment.
Import path likely won't resolve: the file is flash-context.svelte.ts, but this imports ./flash-context.svelte. Elsewhere in the repo .svelte.ts modules are imported with the full extension (e.g. src/lib/components/dialog-manager/dialog-manager.svelte:3). Update the import to target the actual file name so the build doesn't fail.
| import type { FlashContext } from './flash-context.svelte'; | |
| import type { FlashContext } from './flash-context.svelte.ts'; |
| @@ -45,7 +37,7 @@ | |||
|
|
|||
| progressName = 'Resetting...'; | |||
| progressPercent = null; | |||
| await manager.ensureBootloader(); | |||
| await flash.manager.ensureBootloader(); | |||
|
|
|||
There was a problem hiding this comment.
TypeScript nullability: flash.manager is a getter returning FlashManager | null, so the if (!... || !flash.manager) return; check does not reliably narrow subsequent flash.manager.* calls, and will typically fail strict TS with "Object is possibly 'null'". Capture the manager in a local const after the guard (or use a non-null assertion) and use that variable for the rest of the function.
| import { useSerial } from '$lib/utils/serial-context.svelte'; | ||
| import { getBrowserName, isSerialSupported } from '$lib/utils/compatibility'; | ||
| import FirmwareBoardSelector from './FirmwareBoardSelector.svelte'; | ||
| import FirmwareFlasher from './FirmwareFlasher.svelte'; | ||
| import FlashManager from './FlashManager'; | ||
| import { useFlashManager } from './flash-context.svelte'; | ||
| import HelpDialog from './HelpDialog.svelte'; |
There was a problem hiding this comment.
Import paths likely won't resolve: serial-context.svelte.ts and flash-context.svelte.ts are imported as .../serial-context.svelte and ./flash-context.svelte. In this repo, .svelte.ts modules are imported with the full extension (e.g. src/lib/components/dialog-manager/dialog-manager.svelte:3). Update these imports to target the actual file names to avoid build failures.
| if (!flash.manager) { | ||
| terminal.writeLine(`Host-side error during reset: no device!`); | ||
| return; | ||
| } | ||
|
|
||
| await manager.ensureApplication(true); | ||
| await flash.manager.ensureApplication(true); | ||
| } catch (e) { |
There was a problem hiding this comment.
TypeScript nullability: flash.manager is a getter returning FlashManager | null, so if (!flash.manager) return; does not reliably narrow subsequent flash.manager.* calls under strict mode (typically "Object is possibly 'null'"). Assign const manager = flash.manager; after the guard and use manager for the awaited calls.
| if (!flash.manager) { | ||
| terminal.writeLine(`Couldn't send: no device!`); | ||
| return; | ||
| } | ||
|
|
||
| await manager.ensureApplication(); | ||
| await manager.sendApplicationCommand(terminalCommand); | ||
| await flash.manager.ensureApplication(); | ||
| await flash.manager.sendApplicationCommand(terminalCommand); | ||
| } catch (e) { |
There was a problem hiding this comment.
Same strict-nullability issue as above in RunCommand(): the if (!flash.manager) return; guard won't narrow later flash.manager.* calls because manager is a getter that can change between accesses. Store it in a local variable and use that for ensureApplication() / sendApplicationCommand().
Summary
serial-ports-state.svelte.tssingleton with scopedSerialContextandFlashContextclassesuseSerial()/useFlashManager()hooks that auto-cleanup on component unmount (event listeners, port closures, disconnects)Test plan