Thirteen side-by-side examples — from god objects to dropped render updates — and why the StaticFlux version holds up as the app and the team grow.
Every large app runs into the same handful of problems. Here are thirteen of them, written the usual way and then the StaticFlux way — with the real APIs. Each “after” is lifted from, or modelled on, the Studio example app — the last three on the editor packages that Instaflow drove.
One AppServices singleton that constructs everything at startup and that every file imports.
Before
services.ts
exportclassAppServices{staticreadonly instance =newAppServices();
api =newApiClient('/api');
session =newSessionManager(this.api);
docs =newDocsService(this.api,this.session);
analytics =newAnalytics(this.api);
flags =newFeatureFlags(this.api,this.session);// … 40 more, all built at boot, all in the first bundle}// anywhere in the app:const page =awaitAppServices.instance.docs.list();// tests: monkey-patch the singleton and hope nothing cached it
After · StaticFlux
docs/units.ts
import{typeResult, token, unit }from'@staticflux/core';import{DataClientToken}from'@staticflux/data';exportinterfaceDocsService{list(signal:AbortSignal):Promise<Result<DocsPage>>;}exportconstDocsServiceToken=token('docs/service').type<DocsService>();exportconst docsServiceUnit =unit({
id:'docs/service-unit',
provides:DocsServiceToken,
scope:'session',// disposed on logout, with what it holds
deps:{ required:{ client:DataClientToken}},
create:({ client }):DocsService=>({
list:(signal)=> client.call(DocsListEndpoint,{ cursor:undefined},{ signal }),}),});// consumers declare the token; nothing is built until something asks for itconst docs =await injector.get(DocsServiceToken);
Why it is better. Each unit is one small thing with a typed identity, so it is lazy (built on first use, in dependency order), testable alone (createTestInjector with overrides — no singletons to patch) and replaceable alone. The graph tool measures fan-in and fan-out and flags god units, and scopes make lifetimes explicit: session-scoped units die on logout.
Parents pass services down through components that never use them, and every child invents its own loading state.
Before
doc-toolbar.ts
// <doc-page> passes .doc .presence .exporter to <doc-toolbar>,// which passes .exporter to <export-button>, which …classDocToolbarextendsLitElement{static properties ={ doc:{}, presence:{}, exporter:{}};render(){if(!this.doc)return html`<spinner-el></spinner-el>`;// layout shiftreturn html`
<h2>${this.doc.title}</h2>
<span>${this.presence?.count ?? 0} editing</span>
<button @click=${() => this.exporter.exportPdf(this.doc.id)}>PDF</button>`;}}// the PDF exporter (and its library) ships in the boot bundle, for everyone
Why it is better. The component declares what it needs and resolves it from the nearest <sf-scope> (the Lit Context protocol carries the injector), so parents stay ignorant of their children’s dependencies. The three kinds encode intent: render deps block on a size-stable placeholder with aria-busy, lazy deps re-render when they arrive, and functional deps — and their code — load only on interaction, pre-warmed on hover or focus.
Experiment flags read by string key inside feature code, with the same branch repeated everywhere the variant matters.
Before
onboarding.ts
functionOnboardingHeadline(){const variant = flags.get('onboarding_variant');// magic string, untypedif(variant ==='B'){return'Three steps to your first document';}else{return'Let’s set up your studio';}}// … and again in the wizard, the email, the coach mark, the tests.// Removing the experiment means finding every branch.
After · StaticFlux
compose.ts
// contracts.ts — typed declarationsexportconstOnboardingVariantFlag=defineFlag({
key:'onboarding/variant',type: flagType.enum(['A','B']),default:'A',
owner:'onboarding', preAuthSafe:true, description:'Onboarding copy experiment.',});exportconstOnboardingCopyToken=token('onboarding/copy').type<OnboardingCopy>();// compose.ts — one unit per variant; the binder turns the flag into bindingsconst copyA =unit({ id:'onboarding/copy-a-unit', provides:OnboardingCopyToken, scope:'app',
create:()=>({ headline:'Let’s set up your studio'})});const copyB =unit({ id:'onboarding/copy-b-unit', provides:OnboardingCopyToken, scope:'app',
create:()=>({ headline:'Three steps to your first document'})});const binder =createFlagBinder();const units =[flagResolverUnit({ initTimeoutMs:1500}),flagBinderUnit(binder), copyA,...binder.variants({ flag:OnboardingVariantFlag,of:OnboardingCopyToken, variants:{B: copyB }}),];// feature code injects the token — it never sees the flag
deps:{ render:{ copy:OnboardingCopyToken}}// → html`<h1>${this.deps.copy.headline}</h1>`
Why it is better. Feature code has one path; the experiment lives in the composition as alternate bindings at experiment precedence, visible in devtools. Each variant is a unit you can test on its own, and ending the experiment means deleting one unit and one line. The no-direct-flag-reads lint rule keeps flag reads out of feature code for good.
Start/stop pairs that drift apart: a listener nobody removes, an interval nobody clears, a fetch that lands after unmount.
Before
minimap.ts
classMinimap{start(){
window.addEventListener('resize',this.fit);this.timer =setInterval(()=>this.poll(),5000);this.unsubscribe = store.subscribe(this.redraw);fetch(`/api/thumbs/${this.id}`).then((r)=> r.json()).then((t)=>this.draw(t));// lands after unmount → writes a dead canvas}stop(){
window.removeEventListener('resize',this.fit);// forgot clearInterval, forgot unsubscribe}}
After · StaticFlux
minimap.ts
import{ effect,typeScope, sleep, task }from'@staticflux/core';exportfunctionstartMinimap(owner:Scope, view:MinimapView):void{const scope = owner.child('minimap');// disposed with its owner
scope.listen(window,'resize',()=> view.fit());// removed on disposeeffect(()=> view.redraw(store.blocks), scope);// reactive; stops on disposevoid(async()=>{// abortable polling loopwhile(!scope.disposed){await view.poll(scope.signal);if(!(awaitsleep(5_000, scope.signal)).ok)return;}})();const thumbs =task((signal)=>fetch(`/api/thumbs/${view.id}`,{ signal }).then((r)=> r.json()));voidthumbs(scope.signal).then((r)=>{if(r.ok) view.draw(r.value);});// aborted, never late}// the feature's UI scope is the owner: unmount → dispose → all of the above stops
Why it is better. A scope is the single owner of everything a lifecycle state starts. Disposing it removes listeners, stops effects, aborts its AbortSignal (so in-flight requests and sleeps end) and runs cleanups in reverse order — children first. Features get logic, UI and visible scopes from the runtime, so cleanup is never a separate code path, and the no-bare-listener lint rule flags listeners without a signal or scope.
#A dialog that outlives its page → scope-bound routable surfaces
An overlay appended to <body> with no owner: Back leaves it open over a page it no longer belongs to.
Before
share.ts
asyncfunctionshare(docId: string){const dialog = document.createElement('share-dialog');
dialog.docId = docId;
document.body.append(dialog);// global overlay, no owner, no focus trap
dialog.addEventListener('close',()=> dialog.remove());
history.pushState({},'','?share=1');// ad-hoc URL; a deep link shows nothing}// Back to the doc list → the dialog is still open, for a doc that is gone.// A toast fires at the same time and covers the dialog's buttons.
After · StaticFlux
routes.ts · surfaces.ts
// the dialog is part of the doc route's URL: /studio/doc/42/d/shareexportconstShareDialogRoute=defineDialog({
id:'studio/share', key:'share', owner:DocRoute, requires:{ auth:'required'},});// declared once; the surface manager renders, stacks, traps and restores focusexportconstShareDialog= defineSurface<{readonly docId: string }>({
id:'studio/share-dialog',
kind:'dialog',
scope:'route',// can never outlive its route (invariant I-7)
label:'Share document',
routable:{ key:'share', params:(raw)=>ok({ docId: raw['id']??''})},
render:({ props, dismiss, titleId })=> html`
<h2 id=${titleId}>Share document ${props.docId}</h2>
<button @click=${() => dismiss('completed')}>Done</button>`,});exportconst shareDialogUnit =contribution({
id:'studio/share-dialog-surface-unit', provides:RoutableSurfacesToken, scope:'app',
create:()=>ShareDialog,});// open it like a route: availability → authorize → load → enter
html`<button @click=${() => void router.openDialogRoute(ShareDialogRoute)}>Share</button>`
Why it is better. Every surface — dialog, sheet, popover, tooltip, toast, banner, coach mark — belongs to a scope and is dismissed when that scope ends, so a dialog can never outlive its page. Routable dialogs get real URLs: a deep link opens them after the auth check, Back closes them, and their content lazy-loads only when allowed. One manager arbitrates attention, so toasts and coach marks wait while a modal is up.
#Ad-hoc retries and try/catch → Task + Result + retry
A hand-rolled loop that retries 404s, never times out, cannot be cancelled and throws an untyped Error at callers who may not catch it.
Before
load-doc.ts
asyncfunctionloadDoc(id: string){for(let i =0; i <3; i++){try{const res =awaitfetch(`/api/docs/${id}`);// no timeout, no abortif(!res.ok)thrownewError('HTTP '+ res.status);// 404 retried 3×returnawait res.json();}catch(e){awaitnewPromise((r)=>setTimeout(r,1000* i));// synchronized retry storm}}thrownewError('failed');// callers: try/catch … or an unhandled rejection}
After · StaticFlux
load-doc.ts
import{ attemptAsync, defineError, err, retry,typeTask, timeout }from'@staticflux/core';exportconstDocNotFound=defineError('APP-DOC-001','not-found','The document does not exist.','Check the id; offer to create a new document.');exportconstDocUnavailable=defineError('APP-DOC-002','unavailable','The document service is unavailable.','Keep the cached copy read-only and retry later.');const fetchDoc =(id: string):Task<Doc>=>async(signal)=>{const res =awaitattemptAsync(()=>fetch(`/api/docs/${id}`,{ signal }));if(!res.ok)return res;if(res.value.status ===404)returnerr(DocNotFound({ details:{ id }}));// never retriedif(!res.value.ok)returnerr(DocUnavailable({ details:{ status: res.value.status }}));returnattemptAsync(()=> res.value.json()asPromise<Doc>);};// 5 s per attempt, 3 attempts, full-jitter backoff; aborting the signal stops everythingconst loadDoc =(id: string)=>retry(timeout(fetchDoc(id),5_000),{ attempts:3, baseDelayMs:200});const result =awaitloadDoc('42')(scope.signal);if(!result.ok)showError(result.error.code, result.error.fix);
Why it is better. A Task is a function of an AbortSignal that resolves to a Result — it never throws for expected failures, so combinators (retry, timeout, createSingleFlight, createCircuitBreaker, createLimiter) compose and every error has a stable code, a kind and a fix. Retries use full jitter and only retry retryable kinds. In apps, @staticflux/data endpoints do this for you: the data client is the one and only retry layer.
#Hand-wired perf logging → declarative metrics and journeys
performance.now() sprinkled through feature code, string metric names, one vendor SDK, and nobody notices when the number doubles.
Before
editor.ts
const t0 = performance.now();awaitloadEditor();const ms = performance.now()- t0;
console.log('editor ready', ms);// only on the developer's laptop
analytics.track('editor_ready_ms', ms);// string name, one vendorif(ms >2500)Sentry.captureMessage('slow editor');// cold or warm start? cached code or network? which chunk was slow? unknown.
After · StaticFlux
export.ts · features.ts
import{ metric, unit }from'@staticflux/core';import{ defineMarkBudget, markBudgetUnit, perfUnits }from'@staticflux/perf';import{ defineFeature }from'@staticflux/runtime';// typed metric; producers declare informs, consumers reads (checked by the graph tool)exportconstExportDuration= metric.histogram('studio/export-duration',{ unit:'ms'});exportconst exportUnit =unit({
id:'studio/export-unit', provides:ExportToken, scope:'feature', informs:[ExportDuration],
create:(_deps, ctx)=>({asyncrun(id: string){const start = ctx.telemetry.now();const result =awaitexportPdf(id, ctx.signal);
ctx.telemetry.record(ExportDuration, ctx.telemetry.now()- start,{ ok: result.ok });return result;},}),});// feature marks are framework-measured; declare who reports whichdefineFeature({ id:Features.studioDesktop.id, weight:'heavy', load,
readiness:{ firstRender:'framework', interactive:'feature', complete:'feature'}});// opt in once: a journey trace per navigation, budgets per mark × start × device tierconst units =[...perfUnits,markBudgetUnit(defineMarkBudget({
feature:'studio/desktop', mark:'interactive', maxMs:700, starts:['warm'], reason:'R12.7'}))];
Why it is better. Framework primitives already emit spans for boot, DI resolution, every navigation stage, chunk loads and requests, so registering perfUnits yields a journey trace from first input to feature complete — classified cold / warm / soft, with code source (network, HTTP cache, SW) and critical path. Your own metrics are typed declarations; sinks are pluggable, so the vendor is a plugin, and budget overruns land in one conformance stream that CI can fail on.
Synchronous reads on the main thread, JSON parsing everywhere, quota errors nobody catches — and a rewrite of every call site when you move to IndexedDB.
Before
prefs.ts
// throws in some private modes and sandboxed iframesconst prefs =JSON.parse(localStorage.getItem('prefs')??'{}');
prefs.theme ='dark';
localStorage.setItem('prefs',JSON.stringify(prefs));// QuotaExceededError → ?// other tabs? a 'storage' listener somewhere, maybe.// moving to IndexedDB later = making every call site async.
After · StaticFlux
prefs.ts
import{ createIndexedDbStore, createLocalStorageStore, createMemoryStore, createStore }from'@staticflux/storage';// first available backend wins; failures at runtime fail over (reported)const prefs = createStore<Prefs>({
backend:[createIndexedDbStore({ namespace:'prefs'}),createLocalStorageStore({ prefix:'app:'}),()=>createMemoryStore()],});if(!prefs.ok)return prefs;// only when no backend is available at allconst theme =await prefs.value.get('theme',{ signal });// Promise<Result<Prefs | undefined>>if(!theme.ok && theme.error.code ==='SF-STORE-001')warnQuotaFull();// typed errorsawait prefs.value.set('theme',{ mode:'dark'});
prefs.value.watch((change)=>{// cross-tab changes, same interfaceif(change.type==='set'&& change.origin ==='external')reload(change.key);});
Why it is better. One async interface — get, set, delete, has, clear, seek, batch, watch — over memory, Web Storage, cookies, IndexedDB, OPFS or a remote store, every call returning a typed Result. Middleware (namespace, TTL, codecs, AES-GCM encryption, budgets, telemetry, tiered local ⇄ remote) returns the same interface, so you can change the backend later without touching a single consumer.
Tokens in localStorage, ten parallel 401s triggering ten refreshes, tabs out of sync, and the previous user’s data still on screen after an account switch.
Before
api.ts
let token = localStorage.getItem('token');// readable by any script on the pageasyncfunctionapi(path: string){let res =awaitfetch(path,{ headers:{Authorization:`Bearer ${token}`}});if(res.status ===401){
token =awaitrefreshToken();// 10 parallel 401s → 10 refreshes,
localStorage.setItem('token', token);// 9 of them rotate a revoked token
res =awaitfetch(path,{ headers:{Authorization:`Bearer ${token}`}});}return res.json();}// other tabs keep the old token; caches keep the old user's data
After · StaticFlux
compose.ts
import{ authorizedTransportUnit, bffCookieProviderUnit, createSessionUnit,
fetchAuthTransportUnit }from'@staticflux/auth';const auth =[// recommended: tokens stay in the BFF; the browser sees a session endpoint + CSRF headerbffCookieProviderUnit({ endpoints:BFF_ENDPOINTS, csrf:{ header:'x-csrf'}}),createSessionUnit({ channelName:'app/session'}),// one session model, synced across tabs
fetchAuthTransportUnit,
authorizedTransportUnit,// grant headers in; 401 → ONE single-flight refresh → one retry
epochRefreshListenerUnit,// session change → supervisor.refresh(): the epoch bumps];// data that must follow the user re-initializes on the epoch; stale responses are droppedexportconst docsUnit =unit({
id:'docs/list-unit', provides:DocsToken, scope:'session', refreshOn:['epoch'],
deps:{ required:{ resources:ResourcesToken}},
create:({ resources })=>createDocsList(resources),});
Why it is better. Refresh is single-flight (optionally under a Web Lock across tabs), so concurrent 401s coalesce into one refresh and one retry each. Raw tokens never leave the auth package by default, never touch Web Storage, BroadcastChannel or telemetry. A session change bumps the app epoch — units with refreshOn refresh, responses tagged with an old epoch are discarded (I-10) — and logout disposes the whole session scope. No page reload.
#Synchronous navigation → an async pipeline with exit guards
A click handler that pushes history and renders immediately, a blocking confirm() for unsaved work, and a Back button that skips the guard.
Before
nav.ts
link.addEventListener('click',(e)=>{
e.preventDefault();if(editor.dirty &&!confirm('Leave without saving?'))return;// blocking; can't save first
history.pushState({},'','/docs');render(routes['/docs']());// renders before any auth check; code already in the bundle});// a second click races the first
window.onpopstate =()=>render(routes[location.pathname]());// Back skips the guard
After · StaticFlux
routes.ts · editor.ts
// typed route; the feature binds to a capability and the router picks the per-mode variantexportconstDocRoute=defineRoute({
id:'studio/doc', parent:StudioRoute, path:'doc/:id',
search:{ tab: searchParam.enum(['content','comments','history'],{default:'content', kind:'view'})},
feature:bindCapability('studio/editor',[{ feature:Features.studioDesktop, modes:['expanded','medium']},{ feature:Features.studioMobile, modes:['compact']},]),});// leaving the editor waits for the save: async, uninterruptible, bound to the UI scope
router.guardExit(DocRoute,guard(async(_navigation, signal)=>{const flushed =await doc.flush(signal);return flushed.ok ?proceed():err(flushed.error);},{ uninterruptible:true, name:'studio/save-before-leave'}), uiScope);// typed params; newest navigation cancels older ones; Back runs the same pipelineconst result =await router.navigate({ to:DocRoute, params:{ id:'42'}});
Why it is better. Navigation runs thirteen named stages — resolve, exit, availability, auth, authorize, load, enter, render, commit and more — each a span, each able to redirect, block or cancel. Guards can await real work, code for a route loads only after auth and authorization pass, concurrent navigations cancel the older one, and Back/Forward go through the very same pipeline (reverted if blocked). ?tab= changes run a light view pipeline without reactivating the feature.
#Redraw on every message → draw on arrival, gap → resnapshot
The editor posts scene updates to the renderer as they happen, and the renderer trusts that every one of them arrives, in order.
Before
render-bridge.ts
// editor
renderWorker.postMessage({type:'patch', ops });// no version: order is hope// renderer
self.onmessage =(e)=>{if(e.data.type==='patch')applyOps(scene, e.data.ops);// a dropped or late patch: silent driftdraw(scene);// one draw per message, not per frame};// renderer respawned? it starts blank and waits for the next patch// one worker or two? a second copy of this file with direct calls
After · StaticFlux
pipeline.ts
import{ attachDeltaStream, createDeltaStream, defineDeltaStream, selectMedium }from'@staticflux/worker/pipeline';exportconstScene=defineDeltaStream({ id:'app/scene', capacityBytes:1<<20});// editor: versioned records; a snapshot whenever a reader asks for oneconst deltas =createDeltaStream(Scene,{ medium:selectMedium({ topology: boot.topology })});if(!deltas.ok)return deltas;
deltas.value.onResnapshot(()=> deltas.value.snapshot({ version, payload:encodeScene()}));
deltas.value.append({ version:++version, payload:encodeChange(change)});// renderer: per vsync, apply what has arrived — up to the version the view frame showsconst reader =awaitattachDeltaStream(Scene, endpoint);if(!reader.ok)return reader;
reader.value.apply(applyToScene,{ upTo: viewFrame.docVersion });// a gap or epoch change → resyncing → snapshot → live; never a guess past a hole
Why it is better. Every record carries base → version, so a lost, late or reordered update is detected, not drawn: the reader resyncs from a snapshot and goes on. The renderer draws once per frame from what has arrived, a respawned reader asks for a snapshot on reconnect, and the same code runs in one worker (local) or two (shared / message) — selectMedium picks at boot. The sequencing oracle suite proves each presented frame equals a single-threaded reference.
Each editing mode shows and hides its own tools and panels, and switching modes rebuilds the layout from scratch.
Before
toolbar.ts
functionenterMode(mode:'canvas'|'pixel'){
toolbar.innerHTML ='';// everything unmounts, then remounts
toolbar.append(penButton());if(mode ==='pixel') toolbar.append(healButton());// one branch per mode, per panelif(user.plan ==='pro') dock.append(layersPanel());// gating mixed into layoutelse dock.querySelector('.layers')?.remove();// no animation, no pins, no way for an agent to ask for a layout}
After · StaticFlux
layout.ts
// the vocabulary is closed: only registered contributions can mountdefineSpecContribution({ section:'tool', id:'app/heal', label:'Heal', when:{ lens:['app/pixel']}});defineSpecContribution({ section:'panel', id:'app/layers', label:'Layers', gate:gateResource('ui-slot','app/layers')});// a screen is data: validated, gated, then applied as a minimal diffconst applied =await specs.apply({
id:'app/retouch', context:{ lens:'app/pixel'},
slots:[{ section:'tool', id:'app/pen'},{ section:'tool', id:'app/heal'},{ section:'panel', id:'app/layers', placement:'dock'}],},{ signal });// the pen stays mounted; heal enters, layers moves — 120–200 ms each, 0 under reduced motion
Why it is better. Applicability (when) and gating are declared once per contribution, so no feature checks a mode. validateSpec reports every issue at once; apply diffs against what is mounted and only unmounts, moves and mounts what changed — kept slots never flicker, pinned ones survive. An agent can propose a layout as a spec and is gated through its own subject.
#Tests per implementation → one contract suite on every topology
Each implementation of a port has its own tests, so the in-memory fake passes while the cross-worker path has never been run.
Before
scene-pipe.test.ts
// in-memory version: testedit('applies patches',()=>{ fakePipe.send(p1);expect(scene).toEqual(after1);});// SharedArrayBuffer version: "same as above", never run// postMessage fallback for non-isolated pages: written later, by someone else// a new GPU backend: tested by looking at it
After · StaticFlux
pipeline-contracts.test.ts
import{ availableLinkTransports, availablePipeMedia, runDeltaStreamContract, runPeerChannelContract,
brokeredPeerSetup, runRasterBackendContract }from'@staticflux/testkit';for(const medium ofavailablePipeMedia())runDeltaStreamContract(medium,{ describe, it });// local · shared · messagefor(const transport ofavailableLinkTransports())runPeerChannelContract(brokeredPeerSetup(transport),{ describe, it });// every RasterBackend: the CPU reference in Node, the app's WebGPU backend in ChromiumrunRasterBackendContract('memory',()=>({ backend:createMemoryRasterBackend({ tileSize:64}), scene, tolerance:0}),{ describe, it });
Why it is better. The contract is the test: every medium, transport and implementation runs the same cases, including gaps, respawns, torn reads and lost devices. A new adapter is done when it passes. Suites cover the pipes, document CRDTs and repos, the undo service, blob stores, raster backends, text input, spec runtimes and themes.