Skip to main content

2 posts tagged with "architecture"

View All Tags

IPC Contracts and Static Guards: Keeping Main and Renderer in Sync

· 4 min read
Nick2bad4u
Project Maintainer

Electron IPC is a common place where apps quietly rot: ad-hoc channels, mismatched payloads, and copy-pasted handlers that drift over time.

Uptime Watcher takes a different route. IPC contracts are treated as shared types, and we have scripts that fail the build when main and renderer drift apart.

This post explains how the IPC layer is structured, how static guards work, and how that shows up in the git history.

Shared contracts first

IPC contracts live in the shared layer so both the Electron main process and the React renderer agree on channel names and payload types.

Key pieces:

  • shared/types/ipc.ts — maps channels to request/response payloads.
  • shared/ipc/rendererEvents.ts — typed event payloads emitted from main to renderer.
  • shared/types.ts and related monitor/site types — domain objects used in IPC payloads.

The Electron side uses these contracts in services like electron/services/ipc/IpcService.ts, while the renderer consumes them via preload domain APIs under electron/preload/domains/ and src/services/*Service.ts.

Typed preload APIs

Rather than calling ipcRenderer.send directly from React components, the preload script defines domain-specific APIs that wrap IPC calls.

Examples:

  • electron/preload/domains/sitesApi.ts — site CRUD and monitoring operations.
  • electron/preload/domains/monitoringApi.ts — monitor control surface.

Each API function:

  • Uses a shared channel constant (e.g. SITES_CHANNELS.addSite).
  • Accepts and returns typed payloads defined in @shared/types.
  • Applies basic validation / shaping before invoking IPC.

On the renderer side, services like src/services/SiteService.ts consume those APIs, and stores such as src/stores/sites/useSitesStore.ts call the services, not IPC directly.

Static architecture guards

The glue that keeps IPC contracts from drifting is a set of static checks run as part of CI and npm run lint:ci.

The core script is:

  • scripts/architecture-static-guards.mjs

It validates things like:

  • IPC channels referenced in Electron services are declared in the shared IPC types.
  • Renderer services don"t talk directly to Electron modules; they go through the preload bridge.
  • Event names used in the TypedEventBus correspond to entries in the shared event maps.

You can run these guards locally via:

npm run lint:architecture
npm run analyze:ipc
npm run check:ipc

These commands are wired into higher-level scripts like:

npm run lint:all:fix
npm run quality

If a new IPC channel is added only on one side, or if a handler starts returning the wrong shape, those scripts fail quickly.

Git history: when guards caught problems

Multiple commits in the history reflect this IPC discipline.

IPC generation and checking

The scripts that generate and verify IPC artifacts live under scripts/generate-ipc-artifacts.mts. Commits around that script tightened validation to ensure:

  • All IPC channel enums are covered.
  • Renderer event payload maps stay in sync with main-process emitters.

The npm run analyze:ipc and npm run check:ipc commands now run in CI (see lint:ci and quality scripts in package.json). That means changes to IpcService, preload domains, or shared types won"t silently break IPC contracts.

Event system and IPC alignment

The TypedEventBus defined in electron/events/TypedEventBus.ts and electron/events/eventTypes.ts is wired into IPC so that:

  • Internal events (e.g. monitor status changes) are raised with typed payloads.
  • Renderer events are forwarded through a dedicated bridge that uses the shared renderer event payload map.

Architecture docs like docs/Architecture/ADRs/ADR_002_EVENT_DRIVEN_ARCHITECTURE.md and docs/Architecture/ADRs/ADR_005_IPC_COMMUNICATION_PROTOCOL.md are not free-form essays; they match real type definitions and the guards mentioned above.

When something drifts, the combination of TypeScript, the architecture script, and the IPC analyzer catches it.

Practical impact

The IPC contract + static guard approach has concrete benefits:

  • Safer refactors — you can rename a monitor field in the shared types, update the database mapping, and rely on the IPC checks to tell you where handler payloads need adjustment.
  • Fewer runtime surprises — mismatched payloads or missing event subscriptions are caught at build time rather than during a 3am incident.
  • Better documentation — the shared IPC types and ADRs serve as living docs; they are always under pressure from CI to stay honest.

How to extend IPC safely

If you are adding a new IPC feature, the safe workflow looks like:

  1. Define types first

    • Add or extend shared types in shared/types/ipc.ts and related domain types.
  2. Wire Electron handlers

    • Implement handlers in electron/services/ipc/IpcService.ts or a domain-specific coordinator.
    • Emit typed events via the TypedEventBus where appropriate.
  3. Expose preload APIs

    • Add domain functions under electron/preload/domains/*Api.ts that call the new channel.
  4. Consume from renderer services

    • Call the preload APIs from src/services/*Service.ts.
    • Keep components and stores IPC-free.
  5. Run the guards

    npm run analyze:ipc
    npm run check:ipc
    npm run lint:architecture

Following that pattern keeps main and renderer aligned and ensures that IPC remains a well-behaved, typed boundary instead of a global message bus.

Uptime Watcher 19.0: From Local Script to Deeply Tested Desktop App

· 8 min read
Nick2bad4u
Project Maintainer

Uptime Watcher started as a pragmatic way to watch a handful of URLs from a local machine. By the time we reached v19.0.0, it had grown into a heavily-tested, architecture-driven Electron app with a full documentation and tooling ecosystem around it.

This post walks through that journey using real commits from the git history, focusing on three themes:

  • How the architecture solidified around repositories, events, and IPC
  • How the testing strategy evolved into strict, property-based coverage
  • How documentation and tooling caught up with the rest of the stack

All examples and commit references are taken from the main branch as of 2025-11-25.

Early focus: correctness and documentation

One of the consistent patterns in this repo is that architecture and documentation are treated as first-class citizens.

A good example is the work captured in 4a0e1fdf1 (tagged as part of v19.0.0):

📝 [docs] Update TSDoc links for consistency

  • Correct links in TSDoc-Home.md to point to the appropriate files
  • Update TSDoc-Package-Tsdoc.md link to reflect the correct path
  • Modify TSDoc-Spec-Overview.md to ensure accurate package reference
  • Adjust comments in StatusSubscriptionIndicator.utils.ts for clarity
  • Refine useAddSiteForm.ts documentation by removing unnecessary link syntax
  • Enhance chartConfig.ts comments for better readability
  • Add Stylelint config schema reference in stylelint.config.mjs

This change is representative of the repo's style: fix docs and comments as soon as they become stale, keep configuration files typed and schema-backed, and treat tooling as part of the product.

Another example is cb0e9ed86:

📝 [docs] Update documentation frontmatter and summaries

  • Add frontmatter to multiple testing docs
  • Update summaries and metadata for clarity and consistency

That work laid the groundwork for the documentation and Docusaurus site that now power the public docs.

Hardening the tooling and CI pipeline

As the project grew, the CI and linting pipeline became a major focus. Changes like 32bba346a and 4c29fc698 show a pattern:

  • Every major tool (ESLint, Stylelint, Mega-Linter, Checkov, Grype, Secretlint, etc.) is wired with explicit schemas.

  • New configuration files get schema references immediately, so editors and CI can validate them.

  • Linting and scanning are integrated with npm scripts like:

    npm run lint:ci
    npm run lint:all:fix
    npm run docs:check
    npm run docs:validate-links

That level of rigor is not just aesthetic; it means refactors in eslint.config.mjs, stylelint.config.mjs, or docs configs are caught early.

Architecting for a long-term Electron app

Uptime Watcher is not a toy app. It has a service container, a repository layer, a TypedEventBus, and a typed IPC bridge between main and renderer. These ideas are formalized in the Architecture docs and in ADRs like:

  • ADR_001_REPOSITORY_PATTERN.md
  • ADR_002_EVENT_DRIVEN_ARCHITECTURE.md
  • ADR_004_FRONTEND_STATE_MANAGEMENT.md
  • ADR_005_IPC_COMMUNICATION_PROTOCOL.md

These aren't just documents; they are enforced in code by scripts like scripts/architecture-static-guards.mjs, which is wired into npm run lint:architecture and the lint:ci pipeline.

One of the commits that made the IPC story much harder to accidentally break is 542eb08db:

✨ [feat] Implement Docusaurus documentation backup workflow

  • Add GitHub Actions workflow for building and backing up Docusaurus documentation
  • Create backup-docusaurus.yml to automate documentation deployment
  • Update package.json with commands for subtree backup and force push
  • Add documentation style guide for Docusaurus setup

...

On the surface this looks like just a docs workflow improvement, but it cements the idea that docs and architecture are part of the app, not an afterthought.

The testing story: from unit tests to strict, property-based coverage

The most striking arc in the git log is the evolution of the test strategy.

Step 1: Strict tests and shared arbitraries

Commit 0797d4d6f introduced strict test directories and shared fast-check arbitraries:

✨ [feat] Introduce property-based testing for various components and utilities

  • Add property-based tests for normalizeHistoryLimit
  • Implement property-based tests for isNonEmptyString and isValidIdentifier
  • Create property-based tests for useAlertStore
  • Add property-based tests for dataValidation
  • Add README for strict tests directory
  • Introduce shared fast-check arbitraries and an assertProperty helper

This commit is where property-based testing stopped being an experiment and became part of the standard toolbox.

Step 2: Scaling property-based coverage

The work continued in acb498188:

🧪 [test] Enhance comprehensive test coverage for site-related components

  • Use arbitrary site names, URLs, and identifiers in tests
  • Refactor multiple component tests to generate dynamic props
  • Improve branch coverage for modal and settings flows

Here, the pattern is clear:

  • First, establish shared arbitraries and helpers.
  • Then, systematically roll them out across components, stores, and utilities.

Step 3: Tightening test configuration

Most recently, c16d48e54 (paired with c45c0afef and ef6e0dc1a for the version bump) pushed testing further:

✨ [feat] Enhance testing configurations and add property-based tests

  • Update tsconfig to include strict test directories for better coverage
  • Introduce fast-check for property-based testing in monitor operations and validation schemas
  • Add comprehensive property tests for monitor identifiers and status validation
  • Improve test coverage for monitor operations with randomized input testing
  • Extend Vitest configuration to include strict test directories

Combined with the testing ADR (ADR_010_TESTING_STRATEGY.md), Uptime Watcher now has:

  • Dedicated Vitest configs for frontend, Electron, shared, and Storybook
  • Strict test projects for cross-cutting concerns
  • Property-based tests for core monitor logic, validation, and state stores

When you run:

npm run test:all:coverage

you are not just running unit tests; you are exercising a multi-project test matrix with coverage gates and mutation testing support via Stryker.

UX and UI polish in lockstep with tests

The git history also shows that UI/UX improvements are usually paired with better tests.

For example, d6311ce2c added a new icon set and refined layout/animation behavior:

✨ [feat] Add new icon assets and improve UI styling

  • Introduced new icon files for various sizes
  • Updated CSS for layout responsiveness
  • Improved scrollbar styles and card hover effects
  • Enhanced modal animations and utility helpers for tests

Later, f14823e1e introduced density controls for the site table view:

✨ [feat] Enhance Site List and Card Components

  • Add density options ("comfortable", "compact", "cozy")
  • Wire density into the UI store
  • Expand SiteList and SiteListLayoutSelector tests

The pattern is the same throughout:

  • Add a UX feature.
  • Wire it into the Zustand stores.
  • Extend tests (often with property-based generators) to guarantee the new surface behaves under variation.

Docusaurus and docs as a first-class product

The documentation site under docs/docusaurus/ is not an afterthought; it has its own build, lint, and backup story.

The commit 542eb08db introduced a Docusaurus backup workflow, new scripts, and a documentation style guide. Later commits like f6e2cb2a4 and c8930adb9 keep that documentation in sync with the main codebase and tooling.

Today, you can work entirely from the root via:

npm run docs:build
npm run docusaurus:start
npm run docusaurus:broken-links

and know that:

  • TypeDoc is up to date.
  • ESLint Inspector is regenerated.
  • Architecture guides and ADRs match the actual code.

Lessons learned

The following themes stand out from this journey:

  1. Docs and tests are not optional — a significant portion of the most important commits are pure documentation or testing work; they are treated as features, not chores.

  2. Property-based testing pays off quickly — once fast-check was adopted and shared arbitraries were in place, it became much easier to extend coverage without duplicating effort.

  3. Tooling can be a competitive advantage — the investment in linting, schemas, and CI scripts means refactors are safer, documentation stays synchronized, and contributors get rapid feedback from the tooling alone.

  4. Electron apps benefit from real architecture — the combination of a service container, repository pattern, TypedEventBus, and strict IPC boundaries makes Uptime Watcher feel more like a production backend than a desktop toy.

Where we go next

Looking ahead, there are clear directions for Uptime Watcher:

  • Expanding the plugin surface for custom monitor types and alert rules
  • Deepening analytics and historical reporting
  • Introducing new visualizations and dashboards into the Docusaurus site
  • Continuing to push on testing rigor, especially around edge-case networking behavior

If you want to dive deeper into how everything fits together, the starting points are:

Uptime Watcher has come a long way from a simple checker script. The git history tells the story: deliberate architecture, aggressive testing, and a commitment to documentation all the way down.