{"_id":"@dmytromykhailiuk/offline","_rev":"3-f727dc401af30882db400b846d0d2e58","name":"@dmytromykhailiuk/offline","dist-tags":{"latest":"1.1.0"},"versions":{"1.0.0":{"name":"@dmytromykhailiuk/offline","version":"1.0.0","keywords":["offline","offline-first","offline-ready","pwa","progressive-web-app","installable","web-app-manifest","manifest","service-worker","service-worker-generator","sw","precache","precache-manifest","cache","cache-api","cache-first","network-first","critical-assets","network-status","tracker","spa","postbuild","cli","typescript","type-safe","promise","ssr-safe"],"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","_id":"@dmytromykhailiuk/offline@1.0.0","maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"homepage":"https://github.com/dmytromykhailiuk/offline#readme","bugs":{"url":"https://github.com/dmytromykhailiuk/offline/issues"},"bin":{"offline-postbuild":"dist/cli.cjs"},"dist":{"shasum":"945e935e17fb45ed45e889ec58ae36bdb83fa2c7","tarball":"https://registry.npmjs.org/@dmytromykhailiuk/offline/-/offline-1.0.0.tgz","fileCount":11,"integrity":"sha512-hn6JdXYFyqhC47v98jGLi/uaEXVBdFfSjVhMMnRXhRqVMVfZJTsV2NJdWgWkEwVA/PV0H5q9i6lXLD41VtGpZg==","signatures":[{"sig":"MEUCIEcaOY5ybzw0vznUpilcN5+ae+KmqprJrVRbZgh3sGE4AiEAoPH1lYYETs0TdDVXJBBchxy/aQxXMDPcM4u7nuqXZLI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":259561},"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":"ee524f9d7a541917688b72ed1936c5b00952839d","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/offline.git","type":"git"},"_npmVersion":"11.6.2","description":"Full offline mode for SPAs: a post-build CLI that generates a service worker and precache manifest, plus a promise-based OfflineTracker that reports when the app is ready to work offline.","directories":{},"sideEffects":false,"_nodeVersion":"24.12.0","dependencies":{"esbuild":"^0.24.2","@dmytromykhailiuk/cache-request":"^1.0.0","@dmytromykhailiuk/network-connection":"^1.1.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/offline_1.0.0_1785949600826_0.4145108834199831","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@dmytromykhailiuk/offline","version":"1.0.1","keywords":["offline","offline-first","offline-ready","pwa","progressive-web-app","installable","web-app-manifest","manifest","service-worker","service-worker-generator","sw","precache","precache-manifest","cache","cache-api","cache-first","network-first","critical-assets","network-status","tracker","spa","postbuild","cli","typescript","type-safe","promise","ssr-safe"],"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","_id":"@dmytromykhailiuk/offline@1.0.1","maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"homepage":"https://dmytromykhailiuk.github.io/offline/","bugs":{"url":"https://github.com/dmytromykhailiuk/offline/issues"},"bin":{"offline-postbuild":"dist/cli.cjs"},"dist":{"shasum":"b41be705d5586d36ff50e5efee655d525cf2ec44","tarball":"https://registry.npmjs.org/@dmytromykhailiuk/offline/-/offline-1.0.1.tgz","fileCount":11,"integrity":"sha512-TXiD6HCq89TrJFXM41J9cejNI35N5TdmW3DNYkMwySp+dAOPjgdpZLumCWfBnQle/yorUaIoe5XTQZidSpAz/A==","signatures":[{"sig":"MEUCIBKI5mIr4SWTEbwQjTuGq8V2T1LueN1iCmy928ghu8xLAiEA52YcTwZ2NxBhF6UAGePwzpkoo0o00hZYdwTLkkREo6w=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":259554},"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":"dd8ad9f0c56f0c73971fb561a2937a7134c860f8","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/offline.git","type":"git"},"_npmVersion":"11.6.2","description":"Full offline mode for SPAs: a post-build CLI that generates a service worker and precache manifest, plus a promise-based OfflineTracker that reports when the app is ready to work offline.","directories":{},"sideEffects":false,"_nodeVersion":"24.12.0","dependencies":{"esbuild":"^0.24.2","@dmytromykhailiuk/cache-request":"^1.0.0","@dmytromykhailiuk/network-connection":"^1.1.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/offline_1.0.1_1786638669113_0.836296533590059","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@dmytromykhailiuk/offline","version":"1.1.0","description":"Full offline mode for SPAs: a post-build CLI that generates a service worker and precache manifest, plus a promise-based OfflineTracker that reports when the app is ready to work offline.","type":"module","sideEffects":false,"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","keywords":["offline","offline-first","offline-ready","pwa","progressive-web-app","installable","web-app-manifest","manifest","service-worker","service-worker-generator","sw","precache","precache-manifest","cache","cache-api","cache-first","network-first","critical-assets","network-status","tracker","spa","postbuild","cli","typescript","type-safe","promise","ssr-safe"],"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","bin":{"offline-postbuild":"dist/cli.cjs"},"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"},"dependencies":{"@dmytromykhailiuk/cache-request":"^1.0.0","@dmytromykhailiuk/network-connection":"^1.1.0","esbuild":"^0.24.2"},"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/offline.git"},"bugs":{"url":"https://github.com/dmytromykhailiuk/offline/issues"},"homepage":"https://dmytromykhailiuk.github.io/offline/","gitHead":"bac9c607adbe19c369bb60b4d7d73475e394f4a1","_id":"@dmytromykhailiuk/offline@1.1.0","_nodeVersion":"24.12.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-zRDN61pVu3e6F+bvbqzVODneCVYC1UKjTW2akg/ZFpJ7UX4egnN8b4Q5Q7gMLJhE2AHTh9xMOLw9EV5ocmHcSQ==","shasum":"37d19999ee274e90821c1e87faf9bb9d456d4096","tarball":"https://registry.npmjs.org/@dmytromykhailiuk/offline/-/offline-1.1.0.tgz","fileCount":11,"unpackedSize":315225,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCr1NE0wb3v1ZVeokh2z2fs6PmcyQ5+mpihMoI2nanDYQIhAIfVXe4LoRZVIlnC7XY+nMzxjZGtds7ruXCUFXjpnse1"}]},"_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/offline_1.1.0_1788124066676_0.1850276078838069"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-05T17:06:40.601Z","modified":"2026-08-30T21:07:46.937Z","1.0.0":"2026-08-05T17:06:40.979Z","1.0.1":"2026-08-13T16:31:09.258Z","1.1.0":"2026-08-30T21:07:46.804Z"},"bugs":{"url":"https://github.com/dmytromykhailiuk/offline/issues"},"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","homepage":"https://dmytromykhailiuk.github.io/offline/","keywords":["offline","offline-first","offline-ready","pwa","progressive-web-app","installable","web-app-manifest","manifest","service-worker","service-worker-generator","sw","precache","precache-manifest","cache","cache-api","cache-first","network-first","critical-assets","network-status","tracker","spa","postbuild","cli","typescript","type-safe","promise","ssr-safe"],"repository":{"type":"git","url":"git+https://github.com/dmytromykhailiuk/offline.git"},"description":"Full offline mode for SPAs: a post-build CLI that generates a service worker and precache manifest, plus a promise-based OfflineTracker that reports when the app is ready to work offline.","maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"readme":"# @dmytromykhailiuk/offline\n\nFull offline mode for SPAs. One post-build command generates the service worker, the precache\nmanifest and a bootstrap script; one **static-only, promise-based** `OfflineTracker` class\nregisters the worker, warms the cache and answers precisely: _is this app ready to work offline?_\n\n> **Full documentation:** open [Docs](https://dmytromykhailiuk.github.io/offline/) in a browser —\n> every option, with examples, a table of contents and cross-links. This README is the short form.\n\n> ⚠️ **Hard precondition:** `OfflineTracker` consumes\n> [@dmytromykhailiuk/network-connection](https://github.com/dmytromykhailiuk/network-connection)\n> for every online/offline decision and **never initializes it on its own** — the app owns that\n> singleton. `OfflineTracker.init()` throws immediately when `NetworkConnection.init()` has not\n> been called first.\n\nThe work happens in the two places it belongs. At **build time**, the `offline-postbuild` CLI\nreads `offline.json`, collects the build's files by pattern, generates `sw-min.js` +\n`critical-assets.json` + `build-timestamp.txt`, and injects a bootstrap script into `index.html`\n(idempotently — re-runs replace, never stack). At **runtime**, `OfflineTracker` probes the cache\nasset by asset through the worker's `X-Cache-Only` protocol, loads what is missing, retries on an\ninterval, parks while offline and resumes on reconnect — reporting readiness as a boolean, a\nsubscription and a promise. No rxjs, no signals — plain TypeScript and promises.\n\n## Install\n\n```sh\nnpm i @dmytromykhailiuk/offline @dmytromykhailiuk/network-connection\n```\n\n## Quick start\n\n**1.** Describe the offline layer in `offline.json`:\n\n```json\n{\n  \"name\": \"Client\",\n  \"buildPath\": \"/dist\",\n  \"themeColor\": \"#1c1c1c\",\n  \"icons\": [\n    { \"src\": \"icons/icon-192x192.png\", \"sizes\": \"192x192\", \"type\": \"image/png\" }\n  ],\n  \"criticalAssets\": [\"**.js\", \"**.css\", \"**.svg\", \"/images/logo.png\"],\n  \"lazyLoadAssets\": [\"/images/*.jpg\"],\n  \"dataGroups\": [\n    { \"name\": \"translations\", \"urls\": [\"/translations/\"], \"maxSize\": 1 }\n  ]\n}\n```\n\nEvery run also generates `manifest.webmanifest` from the flat fields `name` (required),\n`shortName`, `themeColor`, `backgroundColor`, `display`\n(`\"standalone\" | \"fullscreen\" | \"minimal-ui\" | \"browser\"`, default `\"standalone\"`) and `icons`,\nand links it from the injected `<head>` block — the app is installable with zero extra steps.\ncamelCase keys map to the spec's snake_case; the manifest's `scope`/`start_url` are not options —\nboth derive from `deploymentPath`.\n\n**2.** Run the CLI after every build:\n\n```json\n{\n  \"scripts\": {\n    \"postbuild\": \"offline-postbuild\"\n  }\n}\n```\n\n**3.** Wire the runtime at startup — here with a blocking screen while the connection is down and\nthe app is not yet ready to work offline:\n\n```ts\nimport { NetworkConnection } from \"@dmytromykhailiuk/network-connection\";\nimport { OfflineTracker } from \"@dmytromykhailiuk/offline\";\n\nawait NetworkConnection.init(\"/health.txt\");\n\nOfflineTracker.init({ disabled: import.meta.env.DEV });\nOfflineTracker.registerServiceWorker();\n\nconst networkUnsubscribeFn = NetworkConnection.subscribe((isOnline) => {\n  if (\n    !isOnline &&\n    !OfflineTracker.isOfflineReady &&\n    !NetworkBlockingScreen.isVisible()\n  ) {\n    NetworkBlockingScreen.show();\n  }\n\n  if (isOnline && NetworkBlockingScreen.isVisible()) {\n    NetworkBlockingScreen.hide();\n  }\n});\n\nOfflineTracker.whenOfflineReady().then(() => networkUnsubscribeFn());\n```\n\n`init()` only kicks the process off — readiness is asynchronous. A repeat visit — online or\noffline — initializes from the cached `critical-assets.json` (a client-side cache-first bucket\nvia `@dmytromykhailiuk/cache-request`, invalidated by the bootstrap's new-build cache wipe); only\na first-ever offline visit has nothing to fall back on, so the cache warms up once the connection\nappears. Follow the progress through `subscribe()`, `whenOfflineReady()` and `isOfflineReady` —\nand when new assets arrive with data, `OfflineTracker.reInit({ additionalCriticalAssets })` merges\nthem in and restarts tracking.\n\n## The API at a glance\n\n```ts\nclass OfflineTracker {\n  static init(options?: OfflineTrackerOptions): void; // sync — kicks everything off; once\n  static reInit(options?: OfflineTrackerReInitOptions): void; // restart tracking, merge assets\n  static registerServiceWorker(): void; // no args, callable any time\n  static get status(): OfflineStatus;\n  static get isOfflineReady(): boolean;\n  static get wasOfflineReadyLastSession(): boolean; // persisted hint, not the truth\n  static subscribe(\n    listener: (isOfflineReady: boolean) => void\n  ): OfflineTrackerUnsubscribe;\n  static whenOfflineReady(): Promise<void>;\n  static stabilizeCaching(): Promise<void>;\n  static destroy(): void; // tests / HMR\n}\n```\n\nThe constructor is private and throws — there is exactly one offline state per app. Every\nstateful member throws before `init()`; the exceptions are `registerServiceWorker()`\n(deliberately independent) and `destroy()` (safe no-op).\n\n`init()` is callable **once** (a second call throws) and its options are: `disabled` (dev\nenvironments — the tracker initializes inert, skips even the `NetworkConnection` precondition,\nand `isOfflineReady` stays `false`), `additionalCriticalAssets` / `additionalLazyLoadAssets`\n(extra assets on top of the built list; lazy ones load in the background and gate\n`isOfflineReady` without blocking the critical set), `modifyRequestHeaders` (called before every\nrequest the tracker makes with a `Headers` instance, returns the `Headers` to send — attach an\nauth token; the headers analog of `mapAssetUrl`),\n`mapAssetUrl` (versioned paths), `shouldStabilize` (skip auto-repair on some routes),\n`swSettleDelay` / `retryDelay` / `reconnectSettleDelay` timings. The deployment path and the\nlist URL are not options — both derive from the `window.__OFFLINE_CONFIG__` global the CLI\ninjected. `critical-assets.json` goes through a cache-first client bucket\n(`@dmytromykhailiuk/cache-request`, bucket `offline-critical-assets`): the cached copy is served\nwhen present — no network round-trip per session — and the bootstrap's new-build cache wipe is\nwhat invalidates it. With no cached copy the fetch is retried until an ok response arrives — the\nCLI always generates it, so a 404/5xx is transient deploy state; offline stretches are waited out\nvia `NetworkConnection.continueWhenOnline()`.\n\n`reInit()` **restarts tracking and merges assets** — its only options are the two asset lists.\nThe previous cycle is superseded and the new one tracks the deduplicated union of everything\npassed to `init()` and every `reInit()` so far — a later call can never drop what an earlier one\ndeclared critical. The configuration is fixed by `init()`; `critical-assets.json` is never\nrefetched. A service worker `controllerchange` restarts tracking with the accumulated union\nautomatically.\n\n`stabilizeCaching()` — the repair path. No controlling worker → every cache is deleted and the\npage reloads; healthy worker → one load-and-recheck round for the still-missing assets. Runs\nautomatically on reconnect while not ready (gated by `shouldStabilize`). A worker that is merely\nstill installing is left alone — it is about to claim the page and restart tracking by itself.\n\n## Readiness needs a controlling worker\n\nEvery cache answer comes from the service worker, so nothing is reported as cached until one\n**controls the page** — `navigator.serviceWorker.controller`. Uncontrolled, an `X-Cache-Only`\nrequest is a plain network GET that the server answers with `200`, which would read as a full\ncache over an empty one. So on a first-ever visit the status stays all-`false` until the freshly\nregistered worker activates, calls `clients.claim()` and fires `controllerchange`, which restarts\ntracking; on a repeat visit the page is controlled from the navigation onward and the first check\nalready answers. A page that stays uncontrolled — `registerServiceWorker()` never called, or no\nservice worker support (non-secure context) — reports `false` forever, and says so once in the\nconsole.\n\n## `wasOfflineReadyLastSession` — a hint, never a permission\n\nReadiness is remembered in `localStorage` between sessions and read back once at `init()`, so the\nUI has something better than \"not ready\" to paint in the moment before the first cache check\nanswers.\n\nIt is deliberately **not** wired into anything else: it never seeds `isOfflineReady`, never\nappears in `status`, never resolves `whenOfflineReady()` and never fires `subscribe()`. The\nbrowser can evict Cache Storage on its own — quota pressure, Safari's 7-day rule, a selective\n\"clear cached files\" — while `localStorage` survives, so this flag can read `true` over an empty\ncache. Use it to pick the initial UI state and let the real check correct it moments later; never\nto decide the app may go offline. It is always `false` when initialized with `disabled: true`,\nand the injected bootstrap drops it together with the caches on a new build.\n\n```ts\nOfflineTracker.init({ disabled: import.meta.env.DEV });\n// Paint straight away instead of flashing a spinner on every reload…\nsplash.hidden = OfflineTracker.wasOfflineReadyLastSession;\n// …then correct it the moment the cache has actually answered.\nOfflineTracker.subscribe((isReady) => {\n  splash.hidden = isReady;\n});\n```\n\n## The generated worker\n\nFirst match wins: requests with `X-Cache-Only: true` are answered from the cache or failed\n(never sent to the network — that's the tracker's per-asset probe); SPA routes are served network-first\nwith the cached `index.html`; listed assets cache-first; `dataGroups` requests — matched by url\nprefixes and/or `criticalAssets`-style glob `patterns` — cache-first into named caches with\noptional `maxSize` eviction. A `message` handler answers `offline:cache-diff` with the assets that\nare missing from the caches, so the tracker can check a long list in one round-trip instead of one\nrequest per asset; it falls back to the per-asset probes when the worker predates the handler.\nThe injected bootstrap fixes the `/app` → `/app/` worker\nscope, reloads once after a `ChunkLoadError` when the connection returns, and clears every cache\n(and the readiness hint) when `build-timestamp.txt` reveals a new build.\n\n## TypeScript\n\nEverything is typed; ESM + CJS with `.d.ts` for both. Exported types: `OfflineTrackerOptions` ·\n`OfflineTrackerReInitOptions` · `OfflineStatus` · `OfflineTrackerListener` ·\n`OfflineTrackerUnsubscribe` ·\n`OfflineGlobalConfig` · `OfflineConfig` · `OfflineDataGroup` · `OfflineManifestDisplay` ·\n`OfflineManifestIcon`.\n\n## License\n\nMIT\n","readmeFilename":"README.md"}