@inglorious/web 6.8.1 → 6.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -89,7 +89,7 @@ It's that simple — and surprisingly fast in practice.
89
89
  - You want UI to be fully controlled by your entity-based store
90
90
  - You want to stay entirely in **JavaScript**, without DSLs or compilers
91
91
  - You want **React-like declarative UI** but without the cost and overhead of React
92
- - You want to build **static sites with SSX** — same entity patterns, pre-rendered HTML, and client hydration
92
+ - You want **pre-rendered sites with SSX**, static or server-rendered on request — same entity patterns, HTML from the server, and client hydration
93
93
 
94
94
  This framework is ideal for both small apps and large business UIs.
95
95
 
@@ -289,7 +289,7 @@ import { createStore } from "@inglorious/store"
289
289
  import { html } from "@inglorious/web"
290
290
 
291
291
  const types = {
292
- counter: {
292
+ Counter: {
293
293
  increment(entity) {
294
294
  entity.value++
295
295
  },
@@ -971,6 +971,7 @@ To enable the router, add it to your store's types and create a `router` entity.
971
971
  // store.js
972
972
  import { createStore } from "@inglorious/store"
973
973
  import { html } from "@inglorious/web"
974
+ import { PwaUpdate, watchPwaUpdate } from "@inglorious/web/pwa"
974
975
  import { Router, setRoutes } from "@inglorious/web/router"
975
976
 
976
977
  const types = {
@@ -1281,6 +1282,7 @@ api.notify("geolocationUnwatch")
1281
1282
 
1282
1283
  ```javascript
1283
1284
  import { mount, html } from "@inglorious/web"
1285
+ import { when } from "@inglorious/web/directives/when"
1284
1286
  import { store } from "./store.js"
1285
1287
 
1286
1288
  const renderApp = (api) => {
@@ -1294,12 +1296,13 @@ const renderApp = (api) => {
1294
1296
  <p>Loading: ${geolocation.isLoading ? "yes" : "no"}</p>
1295
1297
  <p>
1296
1298
  Position:
1297
- ${
1298
- position
1299
- ? html`${position.coords.latitude.toFixed(4)},
1300
- ${position.coords.longitude.toFixed(4)}`
1301
- : "unknown"
1302
- }
1299
+ ${when(
1300
+ position,
1301
+ () =>
1302
+ html`${position.coords.latitude.toFixed(4)},
1303
+ ${position.coords.longitude.toFixed(4)}`,
1304
+ () => "unknown",
1305
+ )}
1303
1306
  </p>
1304
1307
  <p>Error: ${geolocation.error ? geolocation.error.message : "none"}</p>
1305
1308
  <button
@@ -1410,6 +1413,69 @@ mount(store, renderApp, document.getElementById("root"))
1410
1413
 
1411
1414
  ---
1412
1415
 
1416
+ ## PWA Updates
1417
+
1418
+ `@inglorious/web/pwa` keeps an installed PWA on its latest version. It works with the service worker that [`vite-plugin-pwa`](https://vite-pwa-org.netlify.app/) generates, and `@inglorious/create-app --pwa` sets it up for you.
1419
+
1420
+ Updates are not optional. A new version downloads in the background, and is applied with a reload at the first moment nobody is in the middle of something:
1421
+
1422
+ - at launch, before anything is touched;
1423
+ - each time the app goes to the background or comes back to the foreground.
1424
+
1425
+ It also checks for a new version every hour and on every return to the foreground: a phone suspends an installed PWA rather than closing it, so the browser's own check may never come.
1426
+
1427
+ ```javascript
1428
+ import { isNative } from "@inglorious/web/mobile"
1429
+ import { watchPwaUpdate } from "@inglorious/web/pwa"
1430
+ import { registerSW } from "virtual:pwa-register"
1431
+
1432
+ if (!isNative()) {
1433
+ registerSW({
1434
+ onRegisteredSW(url, registration) {
1435
+ if (registration) watchPwaUpdate(registration)
1436
+ },
1437
+ })
1438
+ }
1439
+ ```
1440
+
1441
+ Configure the plugin with `registerType: "prompt"` and `injectRegister: false`: the generated worker then waits for the `SKIP_WAITING` message this module sends, instead of taking over on its own.
1442
+
1443
+ ### Offering "Update now"
1444
+
1445
+ A version that lands while someone is using the app waits for the next trip to the background. To offer it right away, pass the store and register the `PwaUpdate` type:
1446
+
1447
+ ```javascript
1448
+ import { PwaUpdate, watchPwaUpdate } from "@inglorious/web/pwa"
1449
+
1450
+ const store = createStore({ types: { PwaUpdate }, autoCreateEntities: true })
1451
+
1452
+ watchPwaUpdate(registration, store)
1453
+ ```
1454
+
1455
+ The `pwaUpdate` entity contains:
1456
+
1457
+ - `isUpdateReady` — a new version is waiting for the next reload
1458
+
1459
+ Events:
1460
+
1461
+ - `pwaUpdateReady` — sent by `watchPwaUpdate` when a version lands mid-session
1462
+ - `pwaUpdateApply` — moves over to the waiting version now, reloading the page
1463
+
1464
+ ```javascript
1465
+ const renderUpdate = (api) =>
1466
+ when(
1467
+ api.getEntity("pwaUpdate").isUpdateReady,
1468
+ () =>
1469
+ html`<button @click=${() => api.notify("#pwaUpdate:pwaUpdateApply")}>
1470
+ Update now
1471
+ </button>`,
1472
+ )
1473
+ ```
1474
+
1475
+ Inglorious UI's `Snackbar` is the usual place for it. `watchPwaUpdate` takes `{ id, checkEveryMs }` as a third argument, to announce to an entity with another id or to check more or less often, and returns a function that stops watching.
1476
+
1477
+ ---
1478
+
1413
1479
  ## Forms
1414
1480
 
1415
1481
  `@inglorious/web` includes a small but powerful `form` type for managing form state inside your entity store. It offers:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/web",
3
- "version": "6.8.1",
3
+ "version": "6.9.1",
4
4
  "description": "A new web framework that leverages the power of the Inglorious Store combined with the performance and simplicity of lit-html.",
5
5
  "author": "Matteo Antony Mistretta <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
6
6
  "contributors": [
@@ -58,6 +58,10 @@
58
58
  "types": "./types/mobile.d.ts",
59
59
  "import": "./src/mobile/index.js"
60
60
  },
61
+ "./pwa": {
62
+ "types": "./types/pwa.d.ts",
63
+ "import": "./src/pwa/index.js"
64
+ },
61
65
  "./router": {
62
66
  "types": "./types/router.d.ts",
63
67
  "import": "./src/router/index.js"
@@ -113,7 +117,7 @@
113
117
  "@inglorious/store": "12.0.0"
114
118
  },
115
119
  "devDependencies": {
116
- "@inglorious/eslint-config": "1.1.4",
120
+ "@inglorious/eslint-config": "1.2.0",
117
121
  "prettier": "^3.9.6",
118
122
  "typescript": "~7.0.2",
119
123
  "vitest": "^5.0.0"
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @typedef {import("../../types/pwa.js").PwaUpdateEntity} PwaUpdateEntity
3
+ */
4
+
5
+ import { applyPwaUpdate } from "./update.js"
6
+
7
+ export function create(entity) {
8
+ entity.isUpdateReady ??= false
9
+ }
10
+
11
+ export function pwaUpdateApply() {
12
+ applyPwaUpdate()
13
+ }
14
+
15
+ export function pwaUpdateReady(entity) {
16
+ entity.isUpdateReady = true
17
+ }
@@ -0,0 +1,5 @@
1
+ import * as handlers from "./handlers.js"
2
+
3
+ export { applyPwaUpdate, watchPwaUpdate } from "./update.js"
4
+
5
+ export const PwaUpdate = { ...handlers }
@@ -0,0 +1,114 @@
1
+ const ONE_HOUR_MS = 3_600_000
2
+ const TAKEOVER_TIMEOUT_MS = 3_000
3
+ const DEFAULT_ID = "pwaUpdate"
4
+ const VISIBILITY_EVENT = "visibilitychange"
5
+
6
+ /**
7
+ * Keeps an installed PWA on its latest version. Updates are not optional: a
8
+ * downloaded version is applied on its own wherever nobody is mid-task — at
9
+ * boot, before anything is touched, and each time the app leaves or comes
10
+ * back to the foreground. A version that lands while someone is using the
11
+ * app is only announced, as `#${id}:pwaUpdateReady`, so the app can offer to
12
+ * move over at once; the next trip to the background moves over regardless.
13
+ *
14
+ * Checks for a new version every `checkEveryMs` and on each return to the
15
+ * foreground: a phone suspends an installed PWA rather than closing it, so
16
+ * the browser's own check on navigation may never come.
17
+ *
18
+ * @param {ServiceWorkerRegistration} registration What `registerSW` from
19
+ * `virtual:pwa-register` hands to `onRegisteredSW`.
20
+ * @param {{ notify: (type: string, payload?: unknown) => void }} [store]
21
+ * Where to announce a mid-session version. Without one, updates are applied
22
+ * silently.
23
+ * @param {{ id?: string, checkEveryMs?: number }} [options]
24
+ * @returns {() => void} Stops watching.
25
+ */
26
+ export function watchPwaUpdate(
27
+ registration,
28
+ store,
29
+ { id = DEFAULT_ID, checkEveryMs = ONE_HOUR_MS } = {},
30
+ ) {
31
+ // A first-ever install has no older version to move from: nothing to
32
+ // announce while the page that installed it keeps running.
33
+ const hadController = Boolean(navigator.serviceWorker?.controller)
34
+ let announced = null
35
+ let isApplying = false
36
+
37
+ apply()
38
+ watchInstalls()
39
+ const interval = setInterval(check, checkEveryMs)
40
+ document.addEventListener(VISIBILITY_EVENT, handleVisibilityChange)
41
+
42
+ return function stop() {
43
+ clearInterval(interval)
44
+ document.removeEventListener(VISIBILITY_EVENT, handleVisibilityChange)
45
+ }
46
+
47
+ // Offline, the check fails: the next one tries again.
48
+ function check() {
49
+ registration.update?.()?.catch?.(() => {})
50
+ }
51
+
52
+ function handleVisibilityChange() {
53
+ if (apply()) return
54
+ if (!document.hidden) check()
55
+ }
56
+
57
+ // One reload per waiting worker: the hidden and visible events of a single
58
+ // trip to the background must not send it twice, nor check for another
59
+ // version while the reload is on its way.
60
+ function apply() {
61
+ if (isApplying) return true
62
+ if (!registration.waiting) return false
63
+ isApplying = true
64
+ applyPwaUpdate(registration)
65
+ return true
66
+ }
67
+
68
+ function announce(worker) {
69
+ if (!hadController || isApplying || announced === worker) return
70
+ announced = worker
71
+ store?.notify(`#${id}:pwaUpdateReady`)
72
+ }
73
+
74
+ function watchInstalls() {
75
+ if (registration.waiting) announce(registration.waiting)
76
+ registration.addEventListener("updatefound", () => {
77
+ const worker = registration.installing
78
+ worker?.addEventListener("statechange", () => {
79
+ if (worker.state === "installed") announce(worker)
80
+ if (worker.state === "activated") announced = null
81
+ })
82
+ })
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Moves the page over to the waiting version: asks the worker to take over
88
+ * with the SKIP_WAITING message that workbox's generated worker listens for,
89
+ * and reloads once it has — not before, or the page just reopens on the old
90
+ * version. With nothing waiting it simply reloads.
91
+ *
92
+ * @param {ServiceWorkerRegistration} [registration] Looked up when omitted.
93
+ * @returns {Promise<void>}
94
+ */
95
+ export async function applyPwaUpdate(registration) {
96
+ const target =
97
+ registration ?? (await navigator.serviceWorker?.getRegistration())
98
+ const worker = target?.waiting
99
+ if (worker) await takeOver(worker)
100
+ location.reload()
101
+ }
102
+
103
+ // Bounded: a worker that never reports back still gets the page reloaded.
104
+ function takeOver(worker) {
105
+ return new Promise((resolve) => {
106
+ const timeout = setTimeout(resolve, TAKEOVER_TIMEOUT_MS)
107
+ worker.addEventListener("statechange", () => {
108
+ if (worker.state !== "activated") return
109
+ clearTimeout(timeout)
110
+ resolve()
111
+ })
112
+ worker.postMessage({ type: "SKIP_WAITING" })
113
+ })
114
+ }
package/types/pwa.d.ts ADDED
@@ -0,0 +1,41 @@
1
+ import { Api } from "./mount"
2
+
3
+ export interface PwaUpdateEntity {
4
+ id?: string
5
+ type: string
6
+ /** A new version landed mid-session and waits for the next reload. */
7
+ isUpdateReady: boolean
8
+ }
9
+
10
+ export type PwaUpdateType = {
11
+ create(entity: PwaUpdateEntity): void
12
+ /** Moves over to the waiting version now, reloading the page. */
13
+ pwaUpdateApply(): void
14
+ pwaUpdateReady(entity: PwaUpdateEntity): void
15
+ }
16
+
17
+ export declare const PwaUpdate: PwaUpdateType
18
+
19
+ export interface WatchPwaUpdateOptions {
20
+ /** The PwaUpdate entity to announce to. Defaults to `pwaUpdate`. */
21
+ id?: string
22
+ /** How often to check for a new version. Defaults to one hour. */
23
+ checkEveryMs?: number
24
+ }
25
+
26
+ /**
27
+ * Keeps an installed PWA on its latest version, applying a downloaded one at
28
+ * boot and on each trip to or from the background. A version that lands
29
+ * mid-session is announced to `store` as `#${id}:pwaUpdateReady`.
30
+ * @returns A function that stops watching.
31
+ */
32
+ export declare function watchPwaUpdate(
33
+ registration: ServiceWorkerRegistration,
34
+ store?: Pick<Api, "notify">,
35
+ options?: WatchPwaUpdateOptions,
36
+ ): () => void
37
+
38
+ /** Moves the page over to the waiting version and reloads. */
39
+ export declare function applyPwaUpdate(
40
+ registration?: ServiceWorkerRegistration,
41
+ ): Promise<void>