{"_id":"@asty-web-app/compass","name":"@asty-web-app/compass","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@asty-web-app/compass","version":"0.1.0","description":"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.","type":"module","main":"./dist/index.js","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"build":"tsc -p .","prepublishOnly":"npm run build"},"license":"MIT","sideEffects":false,"engines":{"node":">=18"},"publishConfig":{"access":"public"},"keywords":["asty","balancer","latency","edge","origin"],"devDependencies":{"typescript":"^5.4.0"},"gitHead":"cae20125554e25fb0b07cd291b536284fc2630a1","_id":"@asty-web-app/compass@0.1.0","_nodeVersion":"24.12.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-MzwQxAQtqp82GscCup8ueMJ2kqiWZkOVAEGnlbjv02uGcfOXcrIXSA+BkMqqll4Mb1zYXuvxP8emGrTef7Uq1Q==","shasum":"062f82083a51a77b4064c67b2f154339138f2bd8","tarball":"https://registry.npmjs.org/@asty-web-app/compass/-/compass-0.1.0.tgz","fileCount":18,"unpackedSize":22270,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIFIsjKsmflVwtFqtrApb5UhyefzvfxOvp9tsrzndOCPYAiEAoTCA2uToVvOXmEFe8VTp1IXWvk1MxCasM2e8vPSEHT8="}]},"_npmUser":{"name":"nikiforov","email":"e@nikiforov.org"},"directories":{},"maintainers":[{"name":"nikiforov","email":"e@nikiforov.org"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/compass_0.1.0_1780260949278_0.8208237008966475"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-31T20:55:49.073Z","0.1.0":"2026-05-31T20:55:49.422Z","modified":"2026-05-31T20:55:49.806Z"},"maintainers":[{"name":"nikiforov","email":"e@nikiforov.org"}],"description":"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.","keywords":["asty","balancer","latency","edge","origin"],"license":"MIT","readme":"# @asty-web-app/compass\n\nLocality-aware origin selection for Asty SPAs.\n\nBefore your SPA mounts, `@asty-web-app/compass` calls a single bootstrap URL to\nget the current list of live cluster nodes (`GET /api/v1` on the Asty\ngateway returns a JSON array of public DNS host names), pings each one,\nand resolves to the lowest-latency origin. From then on, an optional\n`apiFetch` wrapper transparently fails over to the next-best node when\nthe chosen one stops answering.\n\nIt is a thin ES module — no Service Worker, no Web Worker, no WASM.\n~2KB minified, zero runtime dependencies.\n\n## Install\n\n```sh\nnpm install @asty-web-app/compass\n```\n\n## Bootstrap\n\n```ts\nimport { selectOrigin } from '@asty-web-app/compass'\n\nconst origin = await selectOrigin({\n  bootstrapUrl: 'https://asty.example.com/api/v1',\n  fallbackOrigin: 'https://asty.example.com',\n  // cacheKey: 'dashboard',   // optional: sessionStorage cache\n})\n\n// Hand the chosen origin to your SPA — for example via a global the\n// rest of the codebase reads when constructing API URLs.\n;(window as any).__ASTY_ORIGIN__ = origin\n\n// Now mount the app.\nconst { mount } = await import('./mount')\nmount()\n```\n\nThe hosts returned by the bootstrap URL must be reachable from the\nbrowser. `@asty-web-app/compass` prefixes bare host names with `https://`\nunless they already include a scheme; override with `scheme: 'http://'`\nfor local development.\n\n## Runtime failover\n\n```ts\nimport { select, createApiFetch } from '@asty-web-app/compass'\n\nconst selection = await select({ bootstrapUrl: '/api/v1' })\nconst apiFetch = createApiFetch({ selection })\n\n// Use apiFetch everywhere you'd use fetch. Pass relative paths; the\n// wrapper prepends the currently-preferred origin and retries on the\n// next candidate on 5xx / network error.\nconst res = await apiFetch('/api/v1/services')\n```\n\n`apiFetch` only intercepts relative URLs — passing an absolute URL\nopts out, so third-party calls in the same codebase are unaffected.\n\n## API\n\n### `select(opts)` / `selectOrigin(opts)`\n\n```ts\ninterface SelectOriginOptions {\n  bootstrapUrl: string\n  healthPath?: string        // default '/health'\n  timeoutMs?: number         // default 1500 (per request)\n  fallbackOrigin?: string    // used when bootstrap fails or returns []\n  scheme?: 'http://' | 'https://'  // default 'https://'\n  cacheKey?: string          // sessionStorage cache key\n}\n```\n\n`select` returns the full breakdown:\n\n```ts\ninterface SelectionResult {\n  origin: string                       // best origin\n  candidates: string[]                 // ranked list, best first\n  latencies: Record<string, number>    // origin → measured RTT (ms)\n}\n```\n\n`selectOrigin` returns just `origin`.\n\n### `createApiFetch({ selection, shouldRetry? })`\n\nReturns a function with the same signature as `fetch`. Optional\n`shouldRetry(res, err)` lets you customise what counts as a retryable\nfailure (defaults to \"5xx or network error\").\n\n## Why this and not a Service Worker\n\nA Service Worker is appealing for transparent proxying, but its scope\nis its own origin — when the SPA is served from\n`asty.example.com` (Cloudflare Pages, say) and the API lives on\n`n1.asty.example.com`, the SW on the SPA origin cannot intercept\ncross-origin requests to the API. A bootstrap-time `select` + an\n`apiFetch` wrapper covers the same failover behaviour without that\nconstraint.\n\n## License\n\nMIT.\n","readmeFilename":"README.md","_rev":"1-8884a7514cffc6c80c5d427062554592"}