{"_id":"@dmytromykhailiuk/network-connection","_rev":"4-5bca39cd285a072ff5755b3eb8c0a63c","name":"@dmytromykhailiuk/network-connection","dist-tags":{"latest":"1.1.1"},"versions":{"1.0.0":{"name":"@dmytromykhailiuk/network-connection","version":"1.0.0","keywords":["network","online","offline","connection","connectivity","navigator-online","healthcheck","ping","retry","pwa","typescript","type-safe","ssr","ssr-safe","zero-dependencies"],"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","_id":"@dmytromykhailiuk/network-connection@1.0.0","maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"homepage":"https://github.com/dmytromykhailiuk/network-connection#readme","bugs":{"url":"https://github.com/dmytromykhailiuk/network-connection/issues"},"dist":{"shasum":"ccf2064a4cb3fa274c0d7516ff9d900aba688109","tarball":"https://registry.npmjs.org/@dmytromykhailiuk/network-connection/-/network-connection-1.0.0.tgz","fileCount":9,"integrity":"sha512-glhKoG3z0QFjwEEsz2siOhE2QppSEkQkTCnkJMfgEXM73VCFR8PslMsR9PcXETbjK4Ew16PIn1aOwV4LkJBZDQ==","signatures":[{"sig":"MEYCIQCcbMbJipW1p1tptQS9Z7gIw6wp32luIbm/IUAyBHchEAIhAOUocgvQZSyqRfJn5IAuCZcWst4Pu66kq9doWK33fqBA","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":77246},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"gitHead":"e5a3fd0e65830364ce73500b0488902b372da8f0","scripts":{"dev":"tsup --watch","lint":"biome check .","test":"vitest run","build":"tsup","format":"biome format --write .","lint:fix":"biome check --write .","typecheck":"tsc --noEmit","playground":"vite --config vite.playground.config.ts","test:watch":"vitest","prepublishOnly":"npm run build"},"_npmUser":{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"},"repository":{"url":"git+https://github.com/dmytromykhailiuk/network-connection.git","type":"git"},"_npmVersion":"11.6.2","description":"Real network state for the browser — healthcheck-verified isOnline, promise helpers for waiting on connectivity and auto-restarting interrupted work. navigator.onLine lies; this doesn't. Zero dependencies.","directories":{},"sideEffects":false,"_nodeVersion":"24.12.0","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vite":"^5.4.11","jsdom":"^25.0.1","vitest":"^2.1.8","typescript":"^5.7.3","@types/node":"^22.10.5","@biomejs/biome":"^1.9.4"},"_npmOperationalInternal":{"tmp":"tmp/network-connection_1.0.0_1785234694414_0.0718501791428483","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@dmytromykhailiuk/network-connection","version":"1.0.1","keywords":["network","online","offline","connection","connectivity","navigator-online","healthcheck","ping","retry","pwa","typescript","type-safe","ssr","ssr-safe","zero-dependencies"],"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","_id":"@dmytromykhailiuk/network-connection@1.0.1","maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"homepage":"https://github.com/dmytromykhailiuk/network-connection#readme","bugs":{"url":"https://github.com/dmytromykhailiuk/network-connection/issues"},"dist":{"shasum":"eb7f45c6c9416ca3db3ed885465838fde6be4df0","tarball":"https://registry.npmjs.org/@dmytromykhailiuk/network-connection/-/network-connection-1.0.1.tgz","fileCount":9,"integrity":"sha512-AF4zxaW6B5/YXfC2mjfLHZMYH5n0zsWzZ8pO+oAnKLT/GctVgNo1NGppXpnJ0K7sHMUa839QUB4U36jsDQY12g==","signatures":[{"sig":"MEYCIQCR+utJSg9Cyk+jUdV1uFlzWSdUDesP4BO3prJGidXjoAIhAMrH2r5BQqIpnWv4fjani4MfAUzDvzAndp0kgWlmM7RF","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":80680},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"gitHead":"1e78132062d3b6bc1f5da78dd1fd0be9cab5f930","scripts":{"dev":"tsup --watch","lint":"biome check .","test":"vitest run","build":"tsup","format":"biome format --write .","lint:fix":"biome check --write .","typecheck":"tsc --noEmit","playground":"vite --config vite.playground.config.ts","test:watch":"vitest","prepublishOnly":"npm run build"},"_npmUser":{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"},"repository":{"url":"git+https://github.com/dmytromykhailiuk/network-connection.git","type":"git"},"_npmVersion":"11.6.2","description":"Real network state for the browser — healthcheck-verified isOnline, promise helpers for waiting on connectivity and auto-restarting interrupted work. navigator.onLine lies; this doesn't. Zero dependencies.","directories":{},"sideEffects":false,"_nodeVersion":"24.12.0","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vite":"^5.4.11","jsdom":"^25.0.1","vitest":"^2.1.8","typescript":"^5.7.3","@types/node":"^22.10.5","@biomejs/biome":"^1.9.4"},"_npmOperationalInternal":{"tmp":"tmp/network-connection_1.0.1_1785332954633_0.8551300207307335","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@dmytromykhailiuk/network-connection","version":"1.1.0","keywords":["network","online","offline","connection","connectivity","navigator-online","healthcheck","ping","retry","pwa","typescript","type-safe","ssr","ssr-safe","zero-dependencies"],"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","_id":"@dmytromykhailiuk/network-connection@1.1.0","maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"homepage":"https://github.com/dmytromykhailiuk/network-connection#readme","bugs":{"url":"https://github.com/dmytromykhailiuk/network-connection/issues"},"dist":{"shasum":"e8023447d8359de6dc6b0e121c2ae14462c6de09","tarball":"https://registry.npmjs.org/@dmytromykhailiuk/network-connection/-/network-connection-1.1.0.tgz","fileCount":9,"integrity":"sha512-z/herUqVDh8rGX0YvZS4343nnVcipNgasavPS8EC3bhApBTEhvw06yZ+K3Z4hL5Y/mfEl3ub9qaULeaI1WUnhQ==","signatures":[{"sig":"MEUCIQCJnd36jIEjrIrpJKgR6HEniuiy9TPcbwf4vGGS6NQjUgIgeJFJLq0Onn2mB89p+8i+jyjbR6wcZOsbf7j25NdSfxM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":102792},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"gitHead":"e213476c61a4fe193a6463900b67e91032a03fd2","scripts":{"dev":"tsup --watch","lint":"biome check .","test":"vitest run","build":"tsup","format":"biome format --write .","lint:fix":"biome check --write .","typecheck":"tsc --noEmit","playground":"vite --config vite.playground.config.ts","test:watch":"vitest","prepublishOnly":"npm run build"},"_npmUser":{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"},"repository":{"url":"git+https://github.com/dmytromykhailiuk/network-connection.git","type":"git"},"_npmVersion":"11.6.2","description":"Real network state for the browser — healthcheck-verified isOnline, promise helpers for waiting on connectivity and auto-restarting interrupted work. navigator.onLine lies; this doesn't. Zero dependencies.","directories":{},"sideEffects":false,"_nodeVersion":"24.12.0","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vite":"^5.4.11","jsdom":"^25.0.1","vitest":"^2.1.8","typescript":"^5.7.3","@types/node":"^22.10.5","@biomejs/biome":"^1.9.4"},"_npmOperationalInternal":{"tmp":"tmp/network-connection_1.1.0_1785700047951_0.4733688592834302","host":"s3://npm-registry-packages-npm-production"}},"1.1.1":{"name":"@dmytromykhailiuk/network-connection","version":"1.1.1","description":"Real network state for the browser — healthcheck-verified isOnline, promise helpers for waiting on connectivity and auto-restarting interrupted work. navigator.onLine lies; this doesn't. Zero dependencies.","type":"module","sideEffects":false,"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","keywords":["network","online","offline","connection","connectivity","navigator-online","healthcheck","ping","retry","pwa","typescript","type-safe","ssr","ssr-safe","zero-dependencies"],"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"scripts":{"build":"tsup","dev":"tsup --watch","playground":"vite --config vite.playground.config.ts","typecheck":"tsc --noEmit","test":"vitest run","test:watch":"vitest","lint":"biome check .","lint:fix":"biome check --write .","format":"biome format --write .","prepublishOnly":"npm run build"},"engines":{"node":">=18"},"devDependencies":{"@biomejs/biome":"^1.9.4","@types/node":"^22.10.5","jsdom":"^25.0.1","tsup":"^8.3.5","typescript":"^5.7.3","vite":"^5.4.11","vitest":"^2.1.8"},"repository":{"type":"git","url":"git+https://github.com/dmytromykhailiuk/network-connection.git"},"bugs":{"url":"https://github.com/dmytromykhailiuk/network-connection/issues"},"homepage":"https://dmytromykhailiuk.github.io/network-connection/","gitHead":"65d164c288e4f31d64e704c16bb8620bcf44a32e","_id":"@dmytromykhailiuk/network-connection@1.1.1","_nodeVersion":"24.12.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-6ErQoff2ZOR+gQe82NNukMuHdPCZnXZblVe+8wTTOqQB+DmUzxzMTUcqZDmf29rz37usFngqcBBTh2dBJtfHjw==","shasum":"e1a60819e4b1247bd7813444a57589acb3c5be38","tarball":"https://registry.npmjs.org/@dmytromykhailiuk/network-connection/-/network-connection-1.1.1.tgz","fileCount":9,"unpackedSize":102785,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDVs/mlClTpuOAuoZ6CBIEK54tmmVfFt9X1Krvuu1TEEgIhANjbCfIhrcn8TXk5a2JOpvXWBfLHF0oa39lueKk9mMbE"}]},"_npmUser":{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"},"directories":{},"maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/network-connection_1.1.1_1786638596203_0.7251166862343095"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-28T10:31:34.226Z","modified":"2026-08-13T16:29:56.567Z","1.0.0":"2026-07-28T10:31:34.555Z","1.0.1":"2026-07-29T13:49:14.778Z","1.1.0":"2026-08-02T19:47:28.108Z","1.1.1":"2026-08-13T16:29:56.385Z"},"bugs":{"url":"https://github.com/dmytromykhailiuk/network-connection/issues"},"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","homepage":"https://dmytromykhailiuk.github.io/network-connection/","keywords":["network","online","offline","connection","connectivity","navigator-online","healthcheck","ping","retry","pwa","typescript","type-safe","ssr","ssr-safe","zero-dependencies"],"repository":{"type":"git","url":"git+https://github.com/dmytromykhailiuk/network-connection.git"},"description":"Real network state for the browser — healthcheck-verified isOnline, promise helpers for waiting on connectivity and auto-restarting interrupted work. navigator.onLine lies; this doesn't. Zero dependencies.","maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"readme":"# @dmytromykhailiuk/network-connection\n\nReal network state for the browser — healthcheck-verified `isOnline`, promise helpers for\nwaiting on connectivity and auto-restarting interrupted work. `navigator.onLine` lies; this\ndoesn't. Zero dependencies.\n\n> **Full documentation:** open [Docs](https://dmytromykhailiuk.github.io/network-connection/) in\n> a browser — every option, with examples, a table of contents and cross-links. This README is\n> the short form.\n\n> ⚠️ **The rule that makes it work:** a negative signal is trusted, a positive one is verified.\n> The `offline` event flips `isOnline` to `false` immediately — a definite no. Everything that\n> claims \"yes\" — the `online` event, page startup, a ping tick — must prove it with a delivered\n> healthcheck response first. And **await `init()` once at startup**: every member throws until\n> then, because a made-up default is exactly the kind of plausible-looking lie this library\n> exists to kill.\n\nBuilt for apps that keep working when the network doesn't — PWAs, offline-first tools, anything\nwith a sync queue or a long-running upload. They all need the answer to one question: **is the\nnetwork actually there?** `navigator.onLine` is not that answer. Refresh an installed PWA while\noffline: the service worker serves the shell from cache, the page boots, and `navigator.onLine`\nreports `true` — the Wi-Fi interface is up, so the browser is technically not wrong, just\nuseless. The `offline` event fired in the *previous* page, before the refresh threw that\nlistener away. And a connected-but-dead network (captive portal, dead uplink) keeps `onLine`\nat `true` forever without a single event.\n\nSo this library never reads `navigator.onLine`. The state changes in exactly three ways: the\n`offline` event sets it to `false` synchronously; a successful healthcheck request sets it to\n`true`; a failed one sets it to `false`. On top of that sit promise helpers that turn\nconnectivity-aware flows into plain `await` lines.\n\n## Install\n\n```sh\nnpm i @dmytromykhailiuk/network-connection\n```\n\n## Quick start\n\n```ts\nimport { NetworkConnection } from \"@dmytromykhailiuk/network-connection\";\n\n// resolves after the first healthcheck — isOnline is truthful from here on\nawait NetworkConnection.init(\"/api/health\", {\n  pingInterval: 30_000, // optional: re-verify every 30 s while online\n});\n\nNetworkConnection.isOnline;                  // boolean — the real state\n\n// react to every change; returns the unsubscribe function\nconst unsubscribe = NetworkConnection.subscribe((isOnline) => {\n  offlineBanner.hidden = isOnline;\n});\n\nawait NetworkConnection.continueWhenOnline(); // park a flow until the network is back\n\n// work that must survive connection drops: on a network failure it\n// waits for the reconnect and starts over\nconst orders = await NetworkConnection.restartIfNotFinishedWhenOnline(\n  () => fetch(\"/api/orders\").then((res) => res.json()),\n);\n```\n\n## API\n\n```ts\nNetworkConnection.init(healthcheckUrl, {\n  pingInterval?: number;       // re-check every N ms while online — detects silent losses\n  healthcheckTimeout?: number; // abort the check after N ms and count it as failed (5000)\n  method?: \"HEAD\" | \"GET\";     // healthcheck request method (\"GET\")\n});                            // Promise<void> — resolves after the first check\n\nNetworkConnection.isOnline;                        // boolean; throws before init()\nNetworkConnection.subscribe(listener);             // () => void — call it to unsubscribe\nNetworkConnection.continueWhenOnline();            // resolves when online (now or later)\nNetworkConnection.continueWhenOffline();           // mirror\nNetworkConnection.afterOnlineBack();               // resolves after a disconnect → reconnect cycle\nNetworkConnection.restartIfNotFinishedWhenOnline(fn); // verify, run fn, retry across offline periods\nNetworkConnection.destroy();                       // undo init(); rejects pending waiters\n```\n\nCalling `init()` twice throws — call `destroy()` first to re-configure.\n\n## subscribe\n\n```ts\nconst unsubscribe = NetworkConnection.subscribe((isOnline) => {\n  offlineBanner.hidden = isOnline;\n});\n\nunsubscribe(); // detach — calling it again is a no-op\n```\n\nAny number of listeners can be subscribed at once; one transition calls them all, in\nsubscription order, with the new value. Only **changes** are delivered — the listener is not\ncalled on subscription, because the current value is already there synchronously. When a\nlistener needs an initial run, pass it yourself: `listener(NetworkConnection.isOnline)`.\n\nBecause the state is healthcheck-verified, a listener fires exactly when the truth changes: an\n`offline` event, a healthcheck that came back after one didn't, or a ping tick that caught a\nsilent loss. An `online` event whose healthcheck fails changes nothing, so nothing is delivered.\n\nDetails worth knowing:\n\n- A listener that throws is contained — the error is logged and the remaining listeners still\n  run. One broken subscriber must not take the network state machine down with it.\n- Subscribing and unsubscribing from inside a listener is safe. A listener added during a\n  dispatch first hears the *next* change; one removed during a dispatch is not called in that\n  round.\n- `destroy()` drops every subscription **without** a final call: the state it resets to is a\n  teardown, not an observation of the network. Re-subscribe after re-`init()`.\n\n```ts\n// React\nuseEffect(() => NetworkConnection.subscribe(setOnline), []);\n```\n\n## Healthcheck semantics\n\n- **Any delivered response counts as online — even a 404 or 500.** A 500 still travelled\n  through DNS and TCP; the network demonstrably works. This measures *reachability*, not server\n  health. Only a rejected request (DNS failure, connection refused, timeout) means offline.\n- **The URL doesn't need a backend endpoint.** Because any delivered response counts, a tiny\n  static file shipped with your build works just as well — `/health.txt`, `/favicon.ico`,\n  anything your hosting serves: `NetworkConnection.init(\"/health.txt\")`. No server code, no\n  extra route.\n- The request is sent with `cache: \"no-store\"` and a `Cache-Control: no-cache` header, so\n  neither the browser's HTTP cache, a PWA's service worker cache, nor an intermediary proxy can\n  answer on the network's behalf and fake a success while offline.\n- A check that hangs is aborted after `healthcheckTimeout` and counts as failed.\n- Concurrent triggers (the `online` event, a ping tick, a retry) share one in-flight request. A\n  check that was in flight when an `offline` event arrived is discarded — the event is newer\n  information.\n\n## restartIfNotFinishedWhenOnline\n\nA wrapper for work that has to survive a connection drop. It checks that the network is really\nthere before starting `fn` — and if it isn't, it just waits for the connection to come back and\nstarts then.\n\nIf `fn` fails, it checks the network again. Gone → wait for the reconnect and run `fn` from\nscratch, as many times as it takes. Network fine → the failure was real (a validation error, a\nbug, a 500 your code threw on), so the **original** error is thrown to you and nothing is\nretried. Retries are unbounded by design — the promise stays pending across any number of\noffline periods.\n\nTwo details worth knowing: the check before `fn` is one healthcheck request per call (calls made\nat the same time share a single one), and `destroy()` while a call is waiting for the network\nrejects it with the destroy error instead of retrying.\n\nA retry restarts `fn` from the beginning, so make the work safe to repeat: idempotent\nendpoints, an idempotency key, or a resumable protocol.\n\n## Silent losses & pingInterval\n\nThe `offline` event covers the loud failures. It says nothing about a network that is connected\nbut dead — airport Wi-Fi behind a captive portal, a router whose uplink dropped. Pass\n`pingInterval` and, while online, a healthcheck runs every N ms; a failed ping flips the state\nto offline and stops the loop until the connection verifiably returns. The loop is a chained\n`setTimeout` — a check slower than the interval never stacks requests behind itself. No\n`pingInterval`, no background traffic.\n\n## SSR\n\n`init()` works without a `window` — the event subscriptions are skipped, the healthcheck runs\nthrough the global `fetch` (Node ≥ 18), timers and promise helpers behave identically. With no\n`fetch` at all every check reports offline — the library degrades to a pessimist rather than\ncrashing.\n\n## TypeScript\n\n```ts\nconst n = await NetworkConnection.restartIfNotFinishedWhenOnline(async () => 42);\n// n: number — T flows through the retries\n\nNetworkConnection.init(\"/health\", { pingInterval: \"30s\" }); // ✗ string is not a number\nnew NetworkConnection();                 // ✗ constructor is private (and throws at runtime)\n```\n\n## License\n\nMIT\n","readmeFilename":"README.md"}