Skip to main content

Cross-Module Patterns

getbased is authored in TypeScript and compiled to JavaScript ES modules. Local development loads those emitted modules directly; production uses Rolldown bundles. Keep runtime imports ending in .js and use explicit dependencies and configured callbacks to avoid cycles. The architecture checker rejects new dependency cycles.

Compatibility handlers and delegated actions

Existing inline HTML handlers call functions exposed on window. Some modules retain Object.assign(window, {...}) as a compatibility surface. Static shell controls also use delegated actions installed by shell-actions.ts, and lazy features expose loader or runtime entry points. When changing a handler, preserve its public name and availability at the time the control becomes usable. Do not install competing owners for the same compatibility export. Prefer feature-owned delegates and narrow typed callbacks for new wiring; do not expand global exports simply to avoid designing a dependency boundary.

Cross-module calls and runtime seams

Use a normal ES-module import for an allowed dependency:
When a lower-level service needs a higher-level UI action, inject a callback through a small configuration or runtime seam. Existing window calls remain compatibility paths, but the target direction is composition → UI/workflow → domain service → foundation/storage. See the architecture contract.

Decoupled refresh triggering

data.ts exposes registerRefreshCallback() so data code can request a refresh without importing the view/router graph. startup-orchestrator.ts calls registerAppRefreshCallback() from app-event-listeners.ts; that callback rebuilds the sidebar, navigates to state.currentView through its configured dependency, and updates the chat nudge. This is one specific refresh seam. Other features have their own configured runtime callbacks; do not route every UI operation through a global service locator.

HTML interpolation — always escapeHTML()

Any user-controlled or data-derived string inserted into innerHTML must be escaped. This is the project’s primary XSS defense:
Safe values (hardcoded strings, enum results, numbers from your own code) do not need escaping, but when in doubt, escape it. Never use innerHTML with concatenated user input without escapeHTML. The shared renderMarkdown() function in markdown.ts validates URLs against an allowlist (http, https, mailto) before rendering link tags; chat renderers call that shared boundary rather than implementing their own Markdown path.

State access — import state from state.ts

state.ts exports a single shared mutable object. Any module that needs to read or write application state imports it directly:
state is never copied — always pass references or re-read from it. The object is also available as window._labState for debugging in the browser console.

The data parameter pattern — avoid redundant pipeline calls

getActiveData() is not cheap — it deep-clones the full marker schema and processes all entries. Rendering functions that might be called from multiple contexts accept an optional data parameter:
Toggle functions always compute data once and pass it through to all sub-renderers:
This prevents redundant pipeline runs when multiple views refresh simultaneously.

Debug mode

isDebugMode() in utils.ts reads the labcharts-debug localStorage flag. All console.warn and console.error calls in production are gated behind this:
Toggle in Settings → Display → “Verbose console logging” (moved from Privacy in v1.7.x — debug mode also reveals diagnostic UI in the sync popover + Push-efficiency / Lean-sync-mode panels in the Diagnose modal, not a privacy posture).