@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 +74 -8
- package/package.json +6 -2
- package/src/pwa/handlers.js +17 -0
- package/src/pwa/index.js +5 -0
- package/src/pwa/update.js +114 -0
- package/types/pwa.d.ts +41 -0
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
|
|
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
|
-
|
|
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
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
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.
|
|
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.
|
|
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
|
+
}
|
package/src/pwa/index.js
ADDED
|
@@ -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>
|