@asty-web-app/compass
v0.1.0
Published
Locality-aware origin selection for Asty SPAs. Fetches the live host list from a bootstrap URL, pings each one, and resolves to the lowest-latency origin before the SPA mounts.
Maintainers
Readme
@asty-web-app/compass
Locality-aware origin selection for Asty SPAs.
Before your SPA mounts, @asty-web-app/compass calls a single bootstrap URL to
get the current list of live cluster nodes (GET /api/v1 on the Asty
gateway returns a JSON array of public DNS host names), pings each one,
and resolves to the lowest-latency origin. From then on, an optional
apiFetch wrapper transparently fails over to the next-best node when
the chosen one stops answering.
It is a thin ES module — no Service Worker, no Web Worker, no WASM. ~2KB minified, zero runtime dependencies.
Install
npm install @asty-web-app/compassBootstrap
import { selectOrigin } from '@asty-web-app/compass'
const origin = await selectOrigin({
bootstrapUrl: 'https://asty.example.com/api/v1',
fallbackOrigin: 'https://asty.example.com',
// cacheKey: 'dashboard', // optional: sessionStorage cache
})
// Hand the chosen origin to your SPA — for example via a global the
// rest of the codebase reads when constructing API URLs.
;(window as any).__ASTY_ORIGIN__ = origin
// Now mount the app.
const { mount } = await import('./mount')
mount()The hosts returned by the bootstrap URL must be reachable from the
browser. @asty-web-app/compass prefixes bare host names with https://
unless they already include a scheme; override with scheme: 'http://'
for local development.
Runtime failover
import { select, createApiFetch } from '@asty-web-app/compass'
const selection = await select({ bootstrapUrl: '/api/v1' })
const apiFetch = createApiFetch({ selection })
// Use apiFetch everywhere you'd use fetch. Pass relative paths; the
// wrapper prepends the currently-preferred origin and retries on the
// next candidate on 5xx / network error.
const res = await apiFetch('/api/v1/services')apiFetch only intercepts relative URLs — passing an absolute URL
opts out, so third-party calls in the same codebase are unaffected.
API
select(opts) / selectOrigin(opts)
interface SelectOriginOptions {
bootstrapUrl: string
healthPath?: string // default '/health'
timeoutMs?: number // default 1500 (per request)
fallbackOrigin?: string // used when bootstrap fails or returns []
scheme?: 'http://' | 'https://' // default 'https://'
cacheKey?: string // sessionStorage cache key
}select returns the full breakdown:
interface SelectionResult {
origin: string // best origin
candidates: string[] // ranked list, best first
latencies: Record<string, number> // origin → measured RTT (ms)
}selectOrigin returns just origin.
createApiFetch({ selection, shouldRetry? })
Returns a function with the same signature as fetch. Optional
shouldRetry(res, err) lets you customise what counts as a retryable
failure (defaults to "5xx or network error").
Why this and not a Service Worker
A Service Worker is appealing for transparent proxying, but its scope
is its own origin — when the SPA is served from
asty.example.com (Cloudflare Pages, say) and the API lives on
n1.asty.example.com, the SW on the SPA origin cannot intercept
cross-origin requests to the API. A bootstrap-time select + an
apiFetch wrapper covers the same failover behaviour without that
constraint.
License
MIT.
