Skip to content

Scope serial and flash manager to component lifetime#181

Merged
hhvrc merged 5 commits intodevelopfrom
scoped-serial-flash-context
Apr 1, 2026
Merged

Scope serial and flash manager to component lifetime#181
hhvrc merged 5 commits intodevelopfrom
scoped-serial-flash-context

Conversation

@hhvrc
Copy link
Copy Markdown
Contributor

@hhvrc hhvrc commented Mar 27, 2026

Summary

  • Replace global serial-ports-state.svelte.ts singleton with scoped SerialContext and FlashContext classes
  • Both use useSerial() / useFlashManager() hooks that auto-cleanup on component unmount (event listeners, port closures, disconnects)
  • Flashtool page, FirmwareFlasher, and SerialPortSelector updated to use scoped contexts via props instead of global state

Test plan

  • Navigate to flashtool page, verify serial port detection still works
  • Connect a device and flash firmware end-to-end
  • Navigate away from flashtool and back — verify no leaked listeners or stale state
  • Verify "connecting" and "connection failed" states render correctly

hhvrc and others added 5 commits March 9, 2026 12:31
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>
Copilot AI review requested due to automatic review settings April 1, 2026 05:20
@hhvrc hhvrc merged commit e6e661d into develop Apr 1, 2026
13 checks passed
@hhvrc hhvrc deleted the scoped-serial-flash-context branch April 1, 2026 05:22
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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';
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
import type { SerialContext } from '$lib/utils/serial-context.svelte';
import type { SerialContext } from '$lib/utils/serial-context.svelte.ts';

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +47
this.#manager?.disconnect();
this.#manager = null;
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);
});
}

Copilot uses AI. Check for mistakes.
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';
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
import type { FlashContext } from './flash-context.svelte';
import type { FlashContext } from './flash-context.svelte.ts';

Copilot uses AI. Check for mistakes.
Comment on lines 24 to 41
@@ -45,7 +37,7 @@

progressName = 'Resetting...';
progressPercent = null;
await manager.ensureBootloader();
await flash.manager.ensureBootloader();

Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +18 to 23
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';
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +68 to 74
if (!flash.manager) {
terminal.writeLine(`Host-side error during reset: no device!`);
return;
}

await manager.ensureApplication(true);
await flash.manager.ensureApplication(true);
} catch (e) {
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +86 to 93
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) {
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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().

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants