Before / After

The same problems. A different shape.

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.

God object → granular units

One AppServices singleton that constructs everything at startup and that every file imports.

Before

services.ts
export class AppServices {
  static readonly instance = new AppServices();
  api = new ApiClient('/api');
  session = new SessionManager(this.api);
  docs = new DocsService(this.api, this.session);
  analytics = new Analytics(this.api);
  flags = new FeatureFlags(this.api, this.session);
  // … 40 more, all built at boot, all in the first bundle
}

// anywhere in the app:
const page = await AppServices.instance.docs.list();
// tests: monkey-patch the singleton and hope nothing cached it

After · StaticFlux

docs/units.ts
import { type Result, token, unit } from '@staticflux/core';
import { DataClientToken } from '@staticflux/data';

export interface DocsService {
  list(signal: AbortSignal): Promise<Result<DocsPage>>;
}
export const DocsServiceToken = token('docs/service').type<DocsService>();

export const 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 it
const 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.

AGENTS.md on GitHub: @staticflux/core · @staticflux/data · @staticflux/testkit

Prop-drilled dependencies → self-injected component deps

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 …
class DocToolbar extends LitElement {
  static properties = { doc: {}, presence: {}, exporter: {} };

  render() {
    if (!this.doc) return html`<spinner-el></spinner-el>`;   // layout shift
    return 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

After · StaticFlux

doc-toolbar.ts
import { component, defineComponent, SfElement } from '@staticflux/ui';
import { html } from 'lit';

const toolbarSpec = component({
  tag: 'doc-toolbar',
  deps: {
    render: { doc: DocumentStoreToken },                               // placeholder until ready
    lazy: { presence: { token: PresenceToken, init: 'on-visible' } }, // reactive, arrives later
    functional: { exportPdf: { token: ExportPdfToken, init: 'on-intent' } }, // loads on hover/focus
  },
  size: { block: '48px' },                                             // reserved: no layout shift
});

export class DocToolbar extends SfElement<typeof toolbarSpec> {
  static override readonly spec = toolbarSpec;
  override render() {
    const { doc, presence, exportPdf } = this.deps;
    return html`
      <h2>${doc.title}</h2>
      <span>${presence.value?.count ?? 0} editing</span>
      <button data-sf-intent=${this.intent('exportPdf')} @click=${async () => {
        const exporter = await exportPdf.get();
        if (exporter.ok) await exporter.value.run(doc.docId);
      }}>PDF</button>`;
  }
}
defineComponent(DocToolbar);

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.

AGENTS.md on GitHub: @staticflux/ui · @staticflux/core

Flag if/else → variant bindings

Experiment flags read by string key inside feature code, with the same branch repeated everywhere the variant matters.

Before

onboarding.ts
function OnboardingHeadline() {
  const variant = flags.get('onboarding_variant');  // magic string, untyped
  if (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 declarations
export const OnboardingVariantFlag = defineFlag({
  key: 'onboarding/variant', type: flagType.enum(['A', 'B']), default: 'A',
  owner: 'onboarding', preAuthSafe: true, description: 'Onboarding copy experiment.',
});
export const OnboardingCopyToken = token('onboarding/copy').type<OnboardingCopy>();

// compose.ts — one unit per variant; the binder turns the flag into bindings
const 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.

AGENTS.md on GitHub: @staticflux/config · @staticflux/core

Leaked listeners and timers → scopes

Start/stop pairs that drift apart: a listener nobody removes, an interval nobody clears, a fetch that lands after unmount.

Before

minimap.ts
class Minimap {
  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, type Scope, sleep, task } from '@staticflux/core';

export function startMinimap(owner: Scope, view: MinimapView): void {
  const scope = owner.child('minimap');               // disposed with its owner

  scope.listen(window, 'resize', () => view.fit());  // removed on dispose
  effect(() => view.redraw(store.blocks), scope);    // reactive; stops on dispose

  void (async () => {                                 // abortable polling loop
    while (!scope.disposed) {
      await view.poll(scope.signal);
      if (!(await sleep(5_000, scope.signal)).ok) return;
    }
  })();

  const thumbs = task((signal) => fetch(`/api/thumbs/${view.id}`, { signal }).then((r) => r.json()));
  void thumbs(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.

AGENTS.md on GitHub: @staticflux/core · @staticflux/runtime

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
async function share(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/share
export const ShareDialogRoute = defineDialog({
  id: 'studio/share', key: 'share', owner: DocRoute, requires: { auth: 'required' },
});

// declared once; the surface manager renders, stacks, traps and restores focus
export const ShareDialog = 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>`,
});
export const 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.

AGENTS.md on GitHub: @staticflux/messaging · @staticflux/router · @staticflux/urlstate

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
async function loadDoc(id: string) {
  for (let i = 0; i < 3; i++) {
    try {
      const res = await fetch(`/api/docs/${id}`);      // no timeout, no abort
      if (!res.ok) throw new Error('HTTP ' + res.status); // 404 retried 3×
      return await res.json();
    } catch (e) {
      await new Promise((r) => setTimeout(r, 1000 * i)); // synchronized retry storm
    }
  }
  throw new Error('failed');      // callers: try/catch … or an unhandled rejection
}

After · StaticFlux

load-doc.ts
import { attemptAsync, defineError, err, retry, type Task, timeout } from '@staticflux/core';

export const DocNotFound = defineError('APP-DOC-001', 'not-found',
  'The document does not exist.', 'Check the id; offer to create a new document.');
export const DocUnavailable = 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 = await attemptAsync(() => fetch(`/api/docs/${id}`, { signal }));
  if (!res.ok) return res;
  if (res.value.status === 404) return err(DocNotFound({ details: { id } }));  // never retried
  if (!res.value.ok) return err(DocUnavailable({ details: { status: res.value.status } }));
  return attemptAsync(() => res.value.json() as Promise<Doc>);
};

// 5 s per attempt, 3 attempts, full-jitter backoff; aborting the signal stops everything
const loadDoc = (id: string) => retry(timeout(fetchDoc(id), 5_000), { attempts: 3, baseDelayMs: 200 });

const result = await loadDoc('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.

AGENTS.md on GitHub: @staticflux/core · @staticflux/data

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();
await loadEditor();
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 vendor
if (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)
export const ExportDuration = metric.histogram('studio/export-duration', { unit: 'ms' });

export const exportUnit = unit({
  id: 'studio/export-unit', provides: ExportToken, scope: 'feature', informs: [ExportDuration],
  create: (_deps, ctx) => ({
    async run(id: string) {
      const start = ctx.telemetry.now();
      const result = await exportPdf(id, ctx.signal);
      ctx.telemetry.record(ExportDuration, ctx.telemetry.now() - start, { ok: result.ok });
      return result;
    },
  }),
});

// feature marks are framework-measured; declare who reports which
defineFeature({ 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 tier
const 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.

AGENTS.md on GitHub: @staticflux/perf · @staticflux/core · @staticflux/runtime · @staticflux/devtools

Synchronous localStorage → one AsyncStore

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 iframes
const 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 all

const theme = await prefs.value.get('theme', { signal });   // Promise<Result<Prefs | undefined>>
if (!theme.ok && theme.error.code === 'SF-STORE-001') warnQuotaFull();  // typed errors
await prefs.value.set('theme', { mode: 'dark' });

prefs.value.watch((change) => {             // cross-tab changes, same interface
  if (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.

AGENTS.md on GitHub: @staticflux/storage · @staticflux/data · @staticflux/handoff

Manual auth refresh races → single-flight session + epoch refresh

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 page

async function api(path: string) {
  let res = await fetch(path, { headers: { Authorization: `Bearer ${token}` } });
  if (res.status === 401) {
    token = await refreshToken();              // 10 parallel 401s → 10 refreshes,
    localStorage.setItem('token', token);      // 9 of them rotate a revoked token
    res = await fetch(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 header
  bffCookieProviderUnit({ 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 dropped
export const 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.

AGENTS.md on GitHub: @staticflux/auth · @staticflux/runtime · @staticflux/data

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 variant
export const DocRoute = 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 pipeline
const 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.

AGENTS.md on GitHub: @staticflux/router · @staticflux/urlstate · @staticflux/runtime

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 drift
  draw(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';

export const Scene = defineDeltaStream({ id: 'app/scene', capacityBytes: 1 << 20 });

// editor: versioned records; a snapshot whenever a reader asks for one
const 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 shows
const reader = await attachDeltaStream(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.

AGENTS.md on GitHub: @staticflux/worker · @staticflux/testkit

Hard-coded panel layouts → layout specs as data

Each editing mode shows and hides its own tools and panels, and switching modes rebuilds the layout from scratch.

Before

toolbar.ts
function enterMode(mode: 'canvas' | 'pixel') {
  toolbar.innerHTML = '';                           // everything unmounts, then remounts
  toolbar.append(penButton());
  if (mode === 'pixel') toolbar.append(healButton());   // one branch per mode, per panel
  if (user.plan === 'pro') dock.append(layersPanel());  // gating mixed into layout
  else 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 mount
defineSpecContribution({ 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 diff
const 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.

AGENTS.md on GitHub: @staticflux/shell · @staticflux/ui · @staticflux/testkit

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: tested
it('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 of availablePipeMedia()) runDeltaStreamContract(medium, { describe, it });   // local · shared · message
for (const transport of availableLinkTransports()) runPeerChannelContract(brokeredPeerSetup(transport), { describe, it });

// every RasterBackend: the CPU reference in Node, the app's WebGPU backend in Chromium
runRasterBackendContract('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.

AGENTS.md on GitHub: @staticflux/testkit · @staticflux/worker · @staticflux/surface

Most of these “afters” come straight from the Studio example.

Six features, 39 adapters, every vertical — against mock services.