use-everywhere
Updates mentioning use-everywhere. See the repository ↗ for commits and releases.
use-everywhere 0.12.0
A React app now gets every type it needs from
use-everywherealone.LockManagerLike,SharedWorkerLikeandMessagePortLikeare re-exported, so writing a SharedWorker factory or a fake lock manager for a test no longer means installing@use-everywhere/coreas well.The docs are more accurate too: bundle sizes are re-measured (core at 8.56 kB brotlied,
useSharedStateat 4.1 kB), andstability.mdnow documentsuseClientId’s server snapshot as'', which is what it actually returns.Additive, so nothing on 0.11.x has to change.
Jonatan Kruszewski
The use-everywhere demo is linked from rxova.org
Every explanation of this library ends with “open a second tab and watch”, and until now there was nothing to open unless you cloned the repo. There is now, and rxova.org links it — so a reader meets the thing before the install line rather than after.
Ten pages behind a sidebar, each one a live thing you operate with the code that is running printed above it. The order is how someone meets the library rather than how the API is organised: shared state first, then the two questions it immediately provokes — what happens when two tabs write the same key, and what happens when the value is a count rather than a fact — then persistence and the coordination primitives, then the parts that matter once it is load-bearing: namespaces, transports, devtools. The demonstrations are the claims that are hard to believe without seeing them. Two counters side by side running the same race, one losing concurrent increments and one that cannot. A version clock table showing which write won and who made it. A sign-out that reaches four tabs in the same millisecond.
It sits on its own origin rather than under
/packages/use-everywhere/with the docs, which is deliberate. The docs are composed into rxova.org under a base path; this has to be opened in two tabs, so it wants an origin, and GitHub Pages gives it one. Routing is hash-based for the same reason — on Pages a path route is a 404 until you add a404.htmlthat impersonates the app, and a deep link that works on first load is the whole point of a page you are asked to open twice.Jonatan Kruszewski
use-everywhere reads well to an agent
The treatment the inputs docs got in July, ported here — five surfaces rather than six. The shadcn registry does not port: this library ships hooks, not components you copy into your own tree.
Every docs page is also served as raw markdown at
<route>.md, all 257 of them, generated from the same page enumeration the site is built from so a twin cannot disagree with its page.llms.txtindexes the site in 11 kB andllms-full.txtinlines the lot in 517 kB, under an 800 kB budget the build enforces rather than an agent silently truncating. Its## Optionalsection links the three TypeDoc index pages, not the 209 generated reference pages behind them — listing those individually buries the 46 prose links the index exists to present, and a 24 kB budget keeps it that way. Every symbol page still has its own twin, one hop further on.Each of the four tarballs carries a hand-written
llms.txtas well, which is what an agent reads out ofnode_modulesafter an install rather than fetching anything: what the package is, how to install it, a working example, the public surface, and the mistakes that are silent at runtime.check-llms.tschecks its## APItable against the package’s real entry points, so a renamed export fails the build instead of leaving the file describing an API that no longer exists, andpack:smokeproves the file actually ships.AGENTS.mdcovers the other case — working in the repo rather than reading the site.Writing it found five things wrong with the docs, which is the argument for writing it. 235 doc-relative links left over from the Docusaurus migration —
../core/transports.mdand its like — were being emitted into the HTML verbatim as dead links, and nothing checked them becausestarlightLinksValidatorruns witherrorOnRelativeLinks: false. Twelve of those pointed nowhere at all. Astro lowercases ids, so TypeDoc’sREADME.mdis served at/api/core/readme/and a link written with the true filename resolved to a page that does not exist.index.mdhas the idindexrather than'', which had put asource:line naming a route the site does not serve into all 257 twins. And the description extractor dropped a description whenever the opening sentence ran past 200 characters, and welded link URLs into the prose — two pages had none and one was corrupted.The site’s files are live. The packaged ones ship with each package’s next patch.
Jonatan Kruszewski
use-everywhere 0.11.0
0.10.0 put a bus in a SharedWorker. This one lets the worker speak on it.
startRelayreturns aRelaynow, and@use-everywhere/core/shared-workerexports the one it installs on import — mirrored atuse-everywhere/shared-worker, so a React app whose worker owns the WebSocket still needs a single dependency to publish from it.Until now the shipped relay could only forward between ports. A worker hosting it had no way to originate a message, so “the worker owns the socket” — the entire reason to put a bus somewhere that is not a tab — needed a second bus over
BroadcastChanneland a separate handle to keep the worker alive. One port does both now.relay.connect()hands back aTransport, which means worker-side code callscreateSharedStoreexactly as a tab does, late-joiner handshake included, and never hand-assembles an envelope the wire protocol is free to redefine.relay.broadcast(data)is the raw escape hatch, andrelay.sizecounts the attached ports — which is how a worker knows to idle while no tab is looking.Additive, so nothing on 0.10.x has to move:
startRelaykeeps its signature and the import side effect is unchanged.The transport also has end-to-end coverage now, in three real browsers. Whether two tabs reach one worker is not a question unit tests can settle. The fixture’s worker mints a socket id once at startup and never re-broadcasts it, so two tabs agreeing on it can only mean they found the same worker, a late tab learning it can only mean the handshake travelled over the relay, and the id surviving a tab closing can only mean the connection did too. The first assertion is that the transport really is
shared-worker— every other one would pass just as happily overBroadcastChannel, so without it a silent fallback would look like a green suite.Jonatan Kruszewski
use-everywhere 0.10.0
A bus can run through one SharedWorker instead of a channel between N tabs.
SharedWorkerTransportand the relay it talks to (@use-everywhere/core/shared-worker, mirrored atuse-everywhere/shared-workerso a React app still needs one dependency) are opt-in —BroadcastChannelstays the default.The point is a place that is not a tab. Leadership exists because something had to own the socket and only one tab could; a relay can own it outright.
isSharedWorkerAvailable()reports the contexts where the constructor would throw — dedicated workers, Chrome for Android — so the fallback is a check rather than a caught exception.The Inspector renders inside a shadow root, so the host page’s CSS cannot reach it. Its own styles already could not leak out, which was only half the problem. It also gains per-scope views over the wire log, and a timeline: every state wire records a frame, and restore writes one back through the store, so every tab converges rather than one tab quietly disagreeing with the rest.
Two behaviour changes worth knowing. The panel is no longer in
document— reach it throughhost.shadowRootif your tests assert on it. And it renders nothing during server rendering, where it previously emitted markup no one could use.react-domis an optional peer dependency now, used for the portal.And the groundwork for 1.0, written before it is needed rather than after: an RFC process for changes that cannot be undone cheaply, RFC 0001 covering the naming sweep, a migration guide written before the migration exists, and a code of conduct.
Jonatan Kruszewski
eslint-plugin-use-everywhere 0.1.0
Four rules, for the four mistakes this library cannot warn you about at runtime.
Each one has the same shape: the code runs, nothing throws, and the bug shows up as behaviour that is merely wrong — in another tab, or on somebody else’s machine, or only after a deploy. That is the case worth spending a lint rule on.
define-at-module-scopecatches a definer called inside a component, where only the first registration takes effect and every later one is silently discarded.no-dynamic-namecatches a bus name computed at runtime. ABroadcastChannelname is an identity, so a name that varies forks the bus in two, and both halves keep working perfectly on their own.structured-clone-safecatches functions, symbols and class instances in shared state. These either throw on write or, worse, arrive with their prototype dropped — a plain object wearing the shape of the thing you sent.leader-effect-captureswarns when auseLeaderEffectcloses over a value that changes between renders. The effect re-runs when leadership moves and not when the value does, so the stale capture can outlive several renders without anything looking wrong.Flat config, ESLint 9+, and no type information required — so it costs nothing to turn on:
import useEverywhere from 'eslint-plugin-use-everywhere' export default [useEverywhere.configs.recommended]Jonatan Kruszewski
@use-everywhere/test-utils 0.1.0
A browser full of tabs, in one test process, with no browser and no globals.
Multi-tab code is hard for one reason, and it is not the messaging. It is that a tab which closes says goodbye, and a tab which crashes leaves its peers to work out that it is gone. Testing the first case was always easy. Testing the second meant a real browser, real timers, and a test that was slow when it passed and unreadable when it failed.
createScenario()is one simulated browser: a hub every tab shares, anavigator.locksstand-in every tab queues on, and tabs that can be closed or crashed.const browser = createScenario() const a = browser.tab() const survivor = browser.tab().leader('app') a.leader('app') await browser.settle() a.crash() // no goodbye, and the lock the dead tab held is reclaimed expect(survivor.getSnapshot().isLeader).toBe(true)crash()is the interesting half. The tab stops without unsubscribing, without releasing its lock and without telling anybody, and the rest of the scenario has to notice on its own — which is exactly the failure the leader election and the presence roster exist to survive, and exactly the one that used to go untested.It hangs off a seam rather than a mock:
LeaderOptions.locksis documented andLockManagerLikeis exported from core as of 0.9.0, soFakeLockManageris a supported implementation of a public type and not a monkey-patch that breaks on the next refactor.Also published:
FakeWindowandfakeWindowPairfor the window-channel seams,tickandsnapshotWindow, andMemoryHub/MemoryTransportre-exported from@use-everywhere/core/testing— so a test needs one import rather than two.Jonatan Kruszewski
use-everywhere 0.9.0
Every diagnostic has a code now, and a page that explains it.
[use-everywhere] UE1001: second shared store for "cart" in this tab — … → https://rxova.org/packages/use-everywhere/errors/#ue1001The code is the durable part. A message can be reworded, mangled by a minifier or truncated by a log aggregator, and
UE1001survives all three — so codes are permanent and never reused, because an old build in somebody’s browser is still emitting them. The text is otherwise unchanged; anything that matched on it, a test asserting a console warning or a log filter, should match on the code. The React package re-exports the same codes, and warnings still cost nothing in production.The Inspector becomes usable while something is going wrong. Pause, clear, and a filter that matches on
scope/typeand on the sender. Pausing freezes the log and nothing else — the observer stays subscribed, since tearing it down would drop the traffic in between and leave you with a log that has a hole in it, and the crown keeps updating, because leadership is state rather than history. State is editable too: click a value, type JSON, press Enter, and the write goes through the store, so it takes a version and reaches every tab. A draft that is not JSON is refused rather than guessed at —lightand"light"mean different things, and a panel that picks one for you starts disagreeing with the wire.Core also exports
LockManagerLike, and documentsLeaderOptions.locksas the supported test seam it had quietly become — which is what the new@use-everywhere/test-utilspackage, released alongside this one, passes a fake lock manager through.Jonatan Kruszewski
Mutation testing on use-everywhere
Coverage says a line ran, not that a test would notice if it were wrong. The core is under mutation testing now — every module past 90%, gated in CI per module as well as overall, and two tests that turned out not to notice were fixed in the same pass.
It runs Sunday, Wednesday and Friday rather than on every pull request, because rerunning the suite once per mutant is slow and paying that on every push buys very little. The e2e suite also grew coverage for throttling and transport degradation, the two failure modes 0.7.0 addressed and nothing was exercising end to end.
Jonatan Kruszewski
use-everywhere 0.8.0
The largest release so far, and the one that closes most of the gaps between “syncs state across tabs” and something you would put under a real app.
Namespaces. A
BroadcastChannelis global to the origin, so a bus name is an identity: two micro-frontends that each callcreateSharedStore('cart', …)are one cart with two teams writing to it.createNamespace('checkout')carries the whole surface, hooks included, onto its own bus.Payload validation. The payload was the one thing on the wire trusted without a check — cast to the receiving code’s type, which during a rolling deploy is the sender’s idea of the shape.
schematakes anything implementing Standard Schema, without the library depending on any of them.Versioned persistence. Disk is where version skew has its longest fuse: a value written by last month’s build restores with a clock that beats every live tab.
persist.versionandmigratehandle the older case, a newer one is refused, andstore.hydratedmakes the restore observable — which is what the newindexedDbAdapterneeds, since it buys room and structured-clone fidelity at the cost of a synchronous flush.New primitives.
useSharedReducerfor writes relative to what is already there, where last-writer-wins would drop one of two simultaneous increments.useSharedStore(selector)for derived reads without re-rendering on every write. Presence metadata, so peers can carry a name or a cursor. And the channel is finally complete:echo,once, andask/answer.Development warnings are stripped from production bundles now, so most size budgets ended up below where they stood before this stack started. One consequence: loading the ESM directly with no bundler to define
processthrows.Jonatan Kruszewski
use-everywhere 0.7.0
A missing
BroadcastChannelused to mean silence: every hook kept working, every write appeared to succeed, and nothing reached anybody. The chain isBroadcastChannel→StorageTransport→NoopTransportnow, andgetTransportKind(name)reports what is actually carrying traffic.Leader election uses Web Locks where the platform has it, so a throttled background tab keeps its seat and failover after a crash is immediate rather than lease-length. Several copies of the library on one page now find each other and behave as one client instead of contending with themselves. And a quiet peer gets probed before it gets pruned — browsers clamp hidden tabs to about one timer tick a minute, which was enough to make the roster oscillate for a tab that never went anywhere.
Jonatan Kruszewski
use-everywhere 0.5.0
The hardening pass before 1.0, aimed at the failure modes real tabs hit. A tab restored from the back/forward cache rejoins presence and the leader election instead of holding a phantom seat; a value that cannot survive structured clone throws before touching local state, where it used to silently diverge that tab from every peer; client ids come from Web Crypto now, so the tie-breaker can no longer collide.
The hooks are also safe on a server at last — rendering in Next.js used to open transports and run elections on timers nothing cleared. SSR now gets inert doubles,
useClientIdhydrates cleanly, and the API’s silent conflicts warn out loud in dev.No React API changed, but anything asserting on client id shape sees the new 64-bit hex format.
Jonatan Kruszewski
Two new contracts, and Storybook on rxova.org
@rxova/website-schemasgrew two contracts. Page-component bundles: a producer’s artifact now carries only its page bodies, and the website supplies the header, footer, global head and analytics at deploy time. The docs sites moved onto it through one shared mode instead of each repo keeping a copy.And a
storybooksource kind: a project’s workshop ingests like any other source and nests under one/storybook/tree. The react-inputs workshop is the first through — live atrxova.org/storybook/react-inputs/.Jonatan Kruszewski
use-everywhere 0.6.0
MemoryHubandMemoryTransportmoved to atestingsubpath — they are a multi-tab simulation harness, not runtime API, and on the package root they were a 1.0 promise and a line in every production bundle’s module graph.-import { MemoryHub } from 'use-everywhere'; +import { MemoryHub } from 'use-everywhere/testing';The React package also dropped
export * from '@use-everywhere/core', which had made its public API implicitly whatever core happened to export. The list is written out now.Jonatan Kruszewski
use-everywhere gets a logo
The project has a face now: a logo ships in the repo as
assets/logo.svgand sits above the title in the README.Documentation-only, which is why it could go out as 0.4.1 — no API, no bundle, not a runtime byte changed.
Jonatan Kruszewski
The docs ingest contract
rxova.org used to check out three repos and build their docs itself, which meant one project’s broken build took the whole site down with it.
Each repo now builds its own docs and ships them as a release asset; the site ingests the artifact. A project either publishes something valid or the site keeps serving the last good copy.
Jonatan Kruszewski
use-everywhere 0.4.0
The Turborepo migration, the Starlight docs and a single-branch release model, cut as one release.
No API changed. It is the release that makes the next ones cheaper.
Jonatan Kruszewski
Clearing the audit backlog
pnpm auditis only useful if it is ever green. svgo was pinned to a vulnerable 3.3.3 by an override, so it got unpinned; brace-expansion, js-yaml, webpack-dev-server and dompurify were resolved behind it.Dropping Docusaurus left sixteen overrides pointing at packages no longer in the tree, and those went too — a stale override is a silent way to hold a dependency back.
Jonatan Kruszewski
One Turborepo standard across every repo
Four repos had drifted into four ways of running the same six checks. They now share one Turborepo pipeline and one CI shape, so a fix to the build order is made once.
Node 24, TypeScript 6 and pnpm 11 across all of them, which also got
pnpm auditworking again.Jonatan Kruszewski
Docusaurus out, Astro Starlight in
All three docs sites moved to Astro Starlight. The landing already was Astro, so this leaves one framework across the whole origin — and dropped a webpack build that had been the flakiest job in every pipeline.
react-inputs took the chance to restructure: one component library with a flat sidebar and shared guides, instead of three manuals bolted together.
Jonatan Kruszewski
Every docs site points at the aggregator
Each project used to deploy its own GitHub Pages site at its own URL. All three stopped, and now take their
urlandbaseUrlfrom the environment instead of hardcoding one.The same build therefore works standalone at
/and mounted at/packages/<name>/, which is what let rxova.org host them without forking anything.Jonatan Kruszewski
Publishing over OIDC, with size budgets to match
npm publishes now run through trusted publishing — OIDC, no long-lived token in the repo — and every package carries provenance.
The rest of the gate went in at the same time: a changeset is required before a published package can change, each public export has its own size-limit budget, and per-file coverage has to clear 95%.
Jonatan Kruszewski
The use-everywhere docs, rewritten as walkthroughs
The docs were a hook list, which answers “what does this do” and never “why would I reach for it”. They are now walkthroughs that build something, with a per-hook reference behind them for when you already know what you want.
A concepts layer, recipes, a testing guide and a limitations FAQ fill the gaps — including the honest list of what the library cannot do.
Jonatan Kruszewski
An Inspector, and typed channels bound once
<Inspector />shows the live bus — channels, peers, who is leader — and ships on ause-everywhere/devtoolssubpath so it stays out of the bundle unless it is asked for.defineChannelbinds a channel and its message type at module level, so the shape is declared once instead of being repeated at every call site.Jonatan Kruszewski
Leader election, so exactly one tab does the work
Shared state is the easy half. The hard half is that five open tabs will happily run the same poll five times. Leader election picks one, and hands the role over when that tab closes.
Persistence landed alongside it, opt-in rather than automatic, plus an observable debug seam on the bus for anyone who needs to see what is actually crossing it.
Jonatan Kruszewski
use-everywhere is up
State and messages that exist in every tab, window and worker — the first cut is on npm.
@use-everywhere/coreowns the bus, anduse-everywherewraps it in React hooks.It arrived with the parts that are painful to add later: CI on every push, CodeQL, coverage gates, and licenses.
Jonatan Kruszewski
No entries match those filters.