arcane-os 0.12.0 → 0.13.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/CHANGELOG.md +24 -0
- package/README.md +4 -1
- package/browser-runtime/pwa-install.mjs +242 -0
- package/browser-runtime/pwa.mjs +2 -0
- package/docs/architecture.md +9 -0
- package/docs/reference/inventory/package-api.json +43 -1
- package/docs/reference/inventory/runtime-components.json +29 -1
- package/docs/reference/pwa.md +167 -4
- package/docs/reference/runtime-components.md +64 -0
- package/docs/reference/sdk-api.md +97 -1
- package/package.json +2 -2
- package/runtime/arcane/components/pwa-install.html +216 -0
- package/src/dev-server.mjs +146 -41
- package/src/pwa-worker.mjs +4 -6
- package/src/pwa.mjs +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.1
|
|
4
|
+
|
|
5
|
+
- Refresh live development app membership from the current authored descriptor
|
|
6
|
+
or package-only configuration. New explicit includes, exclusions, entry paths,
|
|
7
|
+
and PWA settings take effect on the next relevant request without a server
|
|
8
|
+
restart or writes to consumer projections.
|
|
9
|
+
- Generate PWA metadata and offline inventories from the same app snapshot.
|
|
10
|
+
Coalesce concurrent metadata reads and inventory work within that snapshot,
|
|
11
|
+
and keep older inventory completion from replacing newer metadata.
|
|
12
|
+
|
|
13
|
+
## 0.13.0
|
|
14
|
+
|
|
15
|
+
- Add the reusable `pwa-install.html` component and shared browser installation
|
|
16
|
+
lifecycle through `arcane-os/pwa`. Offer a compact themed Install action and
|
|
17
|
+
explicit Close, retain session dismissal, support inline placement, and surface
|
|
18
|
+
native prompt errors without claiming installation completion.
|
|
19
|
+
- Mount the component from generated PWA bootstraps independently of service
|
|
20
|
+
worker registration and application rendering. Use native installation and
|
|
21
|
+
display-mode events without polling, automatic native prompts, or focus capture.
|
|
22
|
+
- Return cached resources immediately while conditional background refreshes
|
|
23
|
+
are pending or in flight, so page requests do not wait on network revalidation.
|
|
24
|
+
- Document browser icon eligibility and the separate responsibilities of the
|
|
25
|
+
Web App Manifest, application file selection, and offline resource inventory.
|
|
26
|
+
|
|
3
27
|
## 0.12.0
|
|
4
28
|
|
|
5
29
|
- Refresh the selected authored app descriptor's package projection before
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
|
|
|
19
19
|
`arcane/` runtime. Both profiles preserve the same app URLs, theme, packaging,
|
|
20
20
|
event, cancellation, and browser run contracts.
|
|
21
21
|
|
|
22
|
-
This checkout defines the `0.
|
|
22
|
+
This checkout defines the `0.13.0` SDK contract. Applications pin one exact npm
|
|
23
23
|
version and lockfile; registry state is deliberately not baked into application
|
|
24
24
|
artifacts.
|
|
25
25
|
|
|
@@ -30,6 +30,9 @@ page-load revalidation and persistent resource caches. The SDK records one
|
|
|
30
30
|
completed-check timestamp per app/cache in DBOPFS and checks after 120 seconds
|
|
31
31
|
in development or 15 minutes in packaged browser delivery. Apps retain ownership
|
|
32
32
|
of branding, offline page selection and network-dependent product behavior.
|
|
33
|
+
Enabled browser delivery also mounts the shared, themed installation suggestion.
|
|
34
|
+
It appears when the browser offers installation, includes Install and Close,
|
|
35
|
+
and remembers dismissal for the tab session without interrupting page startup.
|
|
33
36
|
|
|
34
37
|
That registry query is a maintainer action, not an application behavior. Apps
|
|
35
38
|
never poll npm for SDK updates or replace their own SDK or synchronized runtime.
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import Is from './dependencies/strong-type/index.js';
|
|
2
|
+
import {createArcaneEventSource} from './event-manager.mjs';
|
|
3
|
+
|
|
4
|
+
const is = new Is(false);
|
|
5
|
+
export const PWA_INSTALL_STATE_EVENT = 'arcane.pwa.install.state';
|
|
6
|
+
let sharedOwner = null;
|
|
7
|
+
let mountedPrompt = null;
|
|
8
|
+
|
|
9
|
+
/** Capture native installation availability once per page, before loading UI. */
|
|
10
|
+
export function getPwaInstall() {
|
|
11
|
+
if (sharedOwner && sharedOwner.state.status !== 'disposed') {
|
|
12
|
+
return sharedOwner;
|
|
13
|
+
}
|
|
14
|
+
const owner = {get state() { return snapshot(); }, subscribe, prompt, dismiss, dispose};
|
|
15
|
+
const source = createArcaneEventSource(owner, {
|
|
16
|
+
source: 'arcane.pwa.install', eventTypes: [PWA_INSTALL_STATE_EVENT]
|
|
17
|
+
});
|
|
18
|
+
const listeners = [];
|
|
19
|
+
const displayMode = globalThis.matchMedia?.(
|
|
20
|
+
'(display-mode: standalone), (display-mode: minimal-ui), '
|
|
21
|
+
+ '(display-mode: fullscreen), (display-mode: window-controls-overlay)'
|
|
22
|
+
);
|
|
23
|
+
const manifestUrl = globalThis.document?.querySelector('link[rel~="manifest"]')?.href;
|
|
24
|
+
const dismissalKey = `arcane.pwa.install.dismissed:${manifestUrl ?? globalThis.location?.href ?? ''}`;
|
|
25
|
+
let deferredPrompt = null;
|
|
26
|
+
let disposed = false;
|
|
27
|
+
let dismissed = false;
|
|
28
|
+
let status = isRunningAsApp() ? 'running' : 'waiting';
|
|
29
|
+
let outcome = null;
|
|
30
|
+
let error = null;
|
|
31
|
+
try {
|
|
32
|
+
dismissed = globalThis.sessionStorage?.getItem(dismissalKey) === 'true';
|
|
33
|
+
} catch (storageError) {
|
|
34
|
+
console.warn('Arcane PWA install dismissal could not be read:', storageError);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isRunningAsApp() {
|
|
38
|
+
return displayMode?.matches === true || globalThis.navigator?.standalone === true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function snapshot() {
|
|
42
|
+
return {status, available: deferredPrompt !== null && !disposed,
|
|
43
|
+
dismissed, outcome, error};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function publish(nextStatus, nextError = null) {
|
|
47
|
+
if (disposed) return;
|
|
48
|
+
status = nextStatus;
|
|
49
|
+
error = nextError;
|
|
50
|
+
source.dispatch(PWA_INSTALL_STATE_EVENT, snapshot());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function observe(target, type, listener) {
|
|
54
|
+
if (!is.function(target?.addEventListener)) return;
|
|
55
|
+
target.addEventListener(type, listener);
|
|
56
|
+
listeners.push(function removeInstallListener() {
|
|
57
|
+
target.removeEventListener(type, listener);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function rememberDismissal() {
|
|
62
|
+
dismissed = true;
|
|
63
|
+
try {
|
|
64
|
+
globalThis.sessionStorage?.setItem(dismissalKey, 'true');
|
|
65
|
+
} catch (storageError) {
|
|
66
|
+
console.warn('Arcane PWA install dismissal could not be saved:', storageError);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function onBeforeInstallPrompt(event) {
|
|
71
|
+
if (disposed || isRunningAsApp() || status === 'installed' || status === 'accepted') return;
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
deferredPrompt = event;
|
|
74
|
+
outcome = null;
|
|
75
|
+
publish('available');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function onInstalled() {
|
|
79
|
+
deferredPrompt = null;
|
|
80
|
+
// This event may precede Android's completion of WebAPK creation.
|
|
81
|
+
publish('installed');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function onDisplayModeChange() {
|
|
85
|
+
if (isRunningAsApp()) {
|
|
86
|
+
deferredPrompt = null;
|
|
87
|
+
publish('running');
|
|
88
|
+
} else if (status === 'running') {
|
|
89
|
+
publish('waiting');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function onPageHide(event) {
|
|
94
|
+
if (!event.persisted) dispose();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function subscribe(listener, {emitCurrent = true, signal} = {}) {
|
|
98
|
+
function forwardInstallState(event) { listener(event.detail); }
|
|
99
|
+
const unsubscribe = source.on(PWA_INSTALL_STATE_EVENT, forwardInstallState,
|
|
100
|
+
signal ? {signal} : undefined);
|
|
101
|
+
try {
|
|
102
|
+
if (emitCurrent && !signal?.aborted) listener(snapshot());
|
|
103
|
+
} catch (listenerError) {
|
|
104
|
+
unsubscribe();
|
|
105
|
+
throw listenerError;
|
|
106
|
+
}
|
|
107
|
+
return unsubscribe;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function prompt() {
|
|
111
|
+
if (disposed || !deferredPrompt) return Promise.resolve(null);
|
|
112
|
+
const event = deferredPrompt;
|
|
113
|
+
deferredPrompt = null;
|
|
114
|
+
publish('prompting');
|
|
115
|
+
let result;
|
|
116
|
+
try {
|
|
117
|
+
// Native user activation must reach prompt() in the same click stack.
|
|
118
|
+
result = event.prompt();
|
|
119
|
+
} catch (promptError) {
|
|
120
|
+
return rejectPrompt(promptError);
|
|
121
|
+
}
|
|
122
|
+
return Promise.resolve(result).then(async function receiveInstallChoice(value) {
|
|
123
|
+
const choice = value ?? await event.userChoice;
|
|
124
|
+
if (!choice || !['accepted', 'dismissed'].includes(choice.outcome)) {
|
|
125
|
+
throw new Error('The browser did not return an installation choice.');
|
|
126
|
+
}
|
|
127
|
+
if (!disposed) {
|
|
128
|
+
outcome = choice.outcome;
|
|
129
|
+
if (status === 'installed' || status === 'running') {
|
|
130
|
+
publish(status);
|
|
131
|
+
} else {
|
|
132
|
+
if (outcome === 'dismissed') rememberDismissal();
|
|
133
|
+
publish(outcome);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return choice;
|
|
137
|
+
}).catch(rejectPrompt);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function rejectPrompt(promptError) {
|
|
141
|
+
if (status !== 'installed' && status !== 'running') publish('error', promptError);
|
|
142
|
+
return Promise.reject(promptError);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function dismiss() {
|
|
146
|
+
if (disposed) return snapshot();
|
|
147
|
+
rememberDismissal();
|
|
148
|
+
publish(status, error);
|
|
149
|
+
return snapshot();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function dispose() {
|
|
153
|
+
if (disposed) return;
|
|
154
|
+
deferredPrompt = null;
|
|
155
|
+
publish('disposed');
|
|
156
|
+
disposed = true;
|
|
157
|
+
for (const removeListener of listeners) removeListener();
|
|
158
|
+
listeners.length = 0;
|
|
159
|
+
source.dispose();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
observe(globalThis, 'beforeinstallprompt', onBeforeInstallPrompt);
|
|
163
|
+
observe(globalThis, 'appinstalled', onInstalled);
|
|
164
|
+
observe(displayMode, 'change', onDisplayModeChange);
|
|
165
|
+
observe(globalThis, 'pagehide', onPageHide);
|
|
166
|
+
sharedOwner = owner;
|
|
167
|
+
return owner;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Mount one shared, initially hidden install component without delaying the app. */
|
|
171
|
+
export function mountPwaInstallPrompt({appName = ''} = {}) {
|
|
172
|
+
const owner = getPwaInstall();
|
|
173
|
+
if (mountedPrompt) return mountedPrompt;
|
|
174
|
+
mountedPrompt = mountComponent().catch(function releaseFailedMount(error) {
|
|
175
|
+
mountedPrompt = null;
|
|
176
|
+
throw error;
|
|
177
|
+
});
|
|
178
|
+
return mountedPrompt;
|
|
179
|
+
|
|
180
|
+
async function mountComponent() {
|
|
181
|
+
if (!globalThis.document) return null;
|
|
182
|
+
// Both modules may start independently; saved theme loading is not a barrier.
|
|
183
|
+
await Promise.all([
|
|
184
|
+
import(new URL('../modules/HTMLImport.js', import.meta.url).href),
|
|
185
|
+
import(new URL('../modules/ThemeBootstrap.js', import.meta.url).href)
|
|
186
|
+
]);
|
|
187
|
+
if (owner.state.status === 'disposed') return null;
|
|
188
|
+
if (!document.body) {
|
|
189
|
+
await new Promise(function waitForComponentParent(resolve) {
|
|
190
|
+
const unsubscribe = owner.subscribe(function observeParentWaitDisposal(state) {
|
|
191
|
+
if (state.status === 'disposed') completeParentWait();
|
|
192
|
+
}, {emitCurrent: false});
|
|
193
|
+
function completeParentWait() {
|
|
194
|
+
document.removeEventListener('DOMContentLoaded', completeParentWait);
|
|
195
|
+
unsubscribe();
|
|
196
|
+
resolve();
|
|
197
|
+
}
|
|
198
|
+
document.addEventListener('DOMContentLoaded', completeParentWait, {once: true});
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
if (owner.state.status === 'disposed') return null;
|
|
202
|
+
const host = document.createElement('html-import');
|
|
203
|
+
host.hidden = true;
|
|
204
|
+
host.dataset.appName = String(appName);
|
|
205
|
+
host.dataset.arcanePwaInstall = '';
|
|
206
|
+
host.setAttribute('href', new URL('../components/pwa-install.html', import.meta.url).href);
|
|
207
|
+
return new Promise(function waitForInstallComponent(resolve, reject) {
|
|
208
|
+
const observer = new MutationObserver(function observeRemovedInstallComponent() {
|
|
209
|
+
if (!host.isConnected) cancelMount();
|
|
210
|
+
});
|
|
211
|
+
const unsubscribe = owner.subscribe(function observeDisposedInstallOwner(state) {
|
|
212
|
+
if (state.status === 'disposed') cancelMount();
|
|
213
|
+
}, {emitCurrent: false});
|
|
214
|
+
function cleanup() {
|
|
215
|
+
observer.disconnect();
|
|
216
|
+
unsubscribe();
|
|
217
|
+
host.removeEventListener('html-import-ready', onReady);
|
|
218
|
+
host.removeEventListener('html-import-error', onError);
|
|
219
|
+
}
|
|
220
|
+
function cancelMount() {
|
|
221
|
+
cleanup();
|
|
222
|
+
host.remove();
|
|
223
|
+
const error = new Error('The PWA install component was removed before it became ready.');
|
|
224
|
+
error.name = 'AbortError';
|
|
225
|
+
reject(error);
|
|
226
|
+
}
|
|
227
|
+
function onReady() {
|
|
228
|
+
cleanup();
|
|
229
|
+
resolve(host);
|
|
230
|
+
}
|
|
231
|
+
function onError(event) {
|
|
232
|
+
cleanup();
|
|
233
|
+
host.remove();
|
|
234
|
+
reject(event.detail?.error ?? new Error('The PWA install component could not load.'));
|
|
235
|
+
}
|
|
236
|
+
host.addEventListener('html-import-ready', onReady);
|
|
237
|
+
host.addEventListener('html-import-error', onError);
|
|
238
|
+
document.body.append(host);
|
|
239
|
+
observer.observe(document.documentElement, {childList: true, subtree: true});
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
package/browser-runtime/pwa.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {createArcaneEventSource} from './event-manager.mjs';
|
|
2
2
|
|
|
3
|
+
export {PWA_INSTALL_STATE_EVENT, getPwaInstall, mountPwaInstallPrompt} from './pwa-install.mjs';
|
|
4
|
+
|
|
3
5
|
export const PWA_STATE_EVENT = 'arcane.pwa.state';
|
|
4
6
|
|
|
5
7
|
export function registerPwa({workerUrl = './arcane-sw.js', scope} = {}) {
|
package/docs/architecture.md
CHANGED
|
@@ -116,6 +116,15 @@ development-refresh lock, then releases the lock before binding the listener.
|
|
|
116
116
|
The authored descriptor remains unchanged, and package-only apps retain their
|
|
117
117
|
existing path. An enabled PWA receives generated manifests directly from this
|
|
118
118
|
source server without creating `dist` output.
|
|
119
|
+
While serving, the SDK rereads changed metadata for the selected app before
|
|
120
|
+
application, root-navigation, and generated-PWA requests. It projects the current
|
|
121
|
+
authored descriptor in memory, or reads the current package configuration for a
|
|
122
|
+
package-only app, without writing either file. Include/exclude rules, entry
|
|
123
|
+
selection, and PWA settings share one request snapshot. Overlapping reads reuse
|
|
124
|
+
one metadata task; unchanged files reuse their parsed selection. Shared runtime
|
|
125
|
+
requests do not wait for that refresh. Full offline inventories are enumerated
|
|
126
|
+
only for worker/offline-manifest requests, with separate state for each selected
|
|
127
|
+
app snapshot so an older inventory cannot overwrite newer generated metadata.
|
|
119
128
|
Changed resource requests return the complete current saved source without
|
|
120
129
|
packaging, copying files into `dist`, or restarting the server. Conditional
|
|
121
130
|
requests for unchanged resources return `304`. Enabled PWAs check on page load
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"minimumVersion": "22.23.2 for Node entrypoints",
|
|
7
7
|
"moduleSystem": "ESM"
|
|
8
8
|
},
|
|
9
|
-
"memberCount":
|
|
9
|
+
"memberCount": 208,
|
|
10
10
|
"members": [
|
|
11
11
|
{
|
|
12
12
|
"id": "root:APP_BUNDLE_DESCRIPTOR_NAME",
|
|
@@ -2996,6 +2996,48 @@
|
|
|
2996
2996
|
"protocol": "Native registration, updatefound, statechange and controllerchange events",
|
|
2997
2997
|
"normalization": "Synchronous owner exposes ready, state, subscribe, update and dispose; current state replays by default, failures remain observable, disposal does not unregister the worker or delete saved data."
|
|
2998
2998
|
},
|
|
2999
|
+
{
|
|
3000
|
+
"id": "pwa:PWA_INSTALL_STATE_EVENT",
|
|
3001
|
+
"name": "PWA_INSTALL_STATE_EVENT",
|
|
3002
|
+
"displayName": "PWA_INSTALL_STATE_EVENT",
|
|
3003
|
+
"kind": "constant",
|
|
3004
|
+
"signature": "const PWA_INSTALL_STATE_EVENT",
|
|
3005
|
+
"entrypoints": ["arcane-os/pwa"],
|
|
3006
|
+
"primaryImport": "arcane-os/pwa",
|
|
3007
|
+
"group": "Progressive web applications",
|
|
3008
|
+
"summary": "Names the shared native installation availability and choice state event.",
|
|
3009
|
+
"availability": "Browser installation lifecycle; importable without starting observation",
|
|
3010
|
+
"protocol": "Existing Arcane event owner and native browser installation events",
|
|
3011
|
+
"normalization": "The exact event name is arcane.pwa.install.state."
|
|
3012
|
+
},
|
|
3013
|
+
{
|
|
3014
|
+
"id": "pwa:getPwaInstall",
|
|
3015
|
+
"name": "getPwaInstall",
|
|
3016
|
+
"displayName": "getPwaInstall()",
|
|
3017
|
+
"kind": "function",
|
|
3018
|
+
"signature": "getPwaInstall()",
|
|
3019
|
+
"entrypoints": ["arcane-os/pwa"],
|
|
3020
|
+
"primaryImport": "arcane-os/pwa",
|
|
3021
|
+
"group": "Progressive web applications",
|
|
3022
|
+
"summary": "Returns one shared page owner for native installation availability, prompt choices and session dismissal.",
|
|
3023
|
+
"availability": "Browser native installation events; waiting state while no prompt is available",
|
|
3024
|
+
"protocol": "Native beforeinstallprompt, appinstalled and display-mode change events",
|
|
3025
|
+
"normalization": "Synchronous owner exposes state, subscribe, prompt, dismiss and dispose; subscriptions replay current state, prompt invokes the native event directly in the user click and consumes it once, and session dismissal preserves a retained event for explicit inline installation."
|
|
3026
|
+
},
|
|
3027
|
+
{
|
|
3028
|
+
"id": "pwa:mountPwaInstallPrompt",
|
|
3029
|
+
"name": "mountPwaInstallPrompt",
|
|
3030
|
+
"displayName": "mountPwaInstallPrompt()",
|
|
3031
|
+
"kind": "function",
|
|
3032
|
+
"signature": "mountPwaInstallPrompt({appName = ''} = {})",
|
|
3033
|
+
"entrypoints": ["arcane-os/pwa"],
|
|
3034
|
+
"primaryImport": "arcane-os/pwa",
|
|
3035
|
+
"group": "Progressive web applications",
|
|
3036
|
+
"summary": "Starts shared installation observation and mounts one initially hidden, themed installation suggestion without delaying application rendering.",
|
|
3037
|
+
"availability": "Browser document and managed HTML import; resolves to null without a document",
|
|
3038
|
+
"protocol": "Shared native install owner and pwa-install.html component lifecycle",
|
|
3039
|
+
"normalization": "Repeated calls return the same mounting promise; the first appName initializes the component, success resolves to its ready html-import host, absence of a document or disposal before mounting resolves to null, and component loading failures reject. Removal or disposal during loading rejects with AbortError. A rejected mount permits another explicit attempt. Generated PWA bootstraps invoke it automatically."
|
|
3040
|
+
},
|
|
2999
3041
|
{
|
|
3000
3042
|
"id": "browser-speech:BROWSER_SPEECH_ARTIFACT_GRAPH_PROTOCOL",
|
|
3001
3043
|
"name": "BROWSER_SPEECH_ARTIFACT_GRAPH_PROTOCOL",
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"path": "runtime/arcane/components",
|
|
8
8
|
"sdkVersion": "0.7.2"
|
|
9
9
|
},
|
|
10
|
-
"componentCount":
|
|
10
|
+
"componentCount": 41,
|
|
11
11
|
"loader": "/arcane/modules/HTMLImport.js",
|
|
12
12
|
"artifacts": [
|
|
13
13
|
{
|
|
@@ -645,6 +645,34 @@
|
|
|
645
645
|
"transport": "HTMLImport + DOM; injected Arcane/provider modules where listed",
|
|
646
646
|
"normalization": "Normalized form values"
|
|
647
647
|
},
|
|
648
|
+
{
|
|
649
|
+
"file": "runtime/arcane/components/pwa-install.html",
|
|
650
|
+
"name": "pwa-install.html",
|
|
651
|
+
"purpose": "Presents a dismissible browser installation action with floating or inline placement.",
|
|
652
|
+
"methods": [
|
|
653
|
+
"configure()",
|
|
654
|
+
"install()",
|
|
655
|
+
"dismiss()",
|
|
656
|
+
"destroy()",
|
|
657
|
+
"state",
|
|
658
|
+
"ready"
|
|
659
|
+
],
|
|
660
|
+
"events": [
|
|
661
|
+
"pwa-install-ready",
|
|
662
|
+
"pwa-install-change",
|
|
663
|
+
"pwa-install-dismissed"
|
|
664
|
+
],
|
|
665
|
+
"slots": [],
|
|
666
|
+
"dependencies": [
|
|
667
|
+
"strong-type",
|
|
668
|
+
"arcane-os/pwa",
|
|
669
|
+
"arcane-os/event-manager",
|
|
670
|
+
"arcane-os/logging"
|
|
671
|
+
],
|
|
672
|
+
"availability": "Browser installation requires a browser-provided beforeinstallprompt event; otherwise hidden",
|
|
673
|
+
"transport": "HTMLImport + DOM; shared PWA installation owner and browser-native prompt",
|
|
674
|
+
"normalization": "Browser install availability and outcome supplied by the shared PWA owner"
|
|
675
|
+
},
|
|
648
676
|
{
|
|
649
677
|
"file": "runtime/arcane/components/record-timeline.html",
|
|
650
678
|
"name": "record-timeline.html",
|
package/docs/reference/pwa.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
An application supplies its installation identity and offline resource selection.
|
|
4
4
|
The SDK generates the Web App Manifest, offline inventory, service worker and
|
|
5
|
-
nonblocking registration module.
|
|
5
|
+
nonblocking registration module. Its shared installation owner and dismissible
|
|
6
|
+
component expose the browser's available install action. Native packages keep
|
|
7
|
+
their existing lifecycle.
|
|
6
8
|
|
|
7
9
|
## Application configuration
|
|
8
10
|
|
|
@@ -63,7 +65,7 @@ Browser packaging emits these files at the selected deployment root:
|
|
|
63
65
|
| `arcane.webmanifest` | Browser installation metadata. |
|
|
64
66
|
| `arcane-offline.json` | App ID/version, SDK version, deployment revision, resource URLs and explicit navigation aliases. |
|
|
65
67
|
| `arcane-sw.js` | Stable worker URL with the selected offline manifest embedded in its source. |
|
|
66
|
-
| `arcane-pwa.mjs` | Independent registration
|
|
68
|
+
| `arcane-pwa.mjs` | Independent registration and installation-component bootstrap importing the SDK client. |
|
|
67
69
|
|
|
68
70
|
Each packaged output gets one deployment revision shared by its offline
|
|
69
71
|
manifest and worker. It distinguishes separately generated outputs even when
|
|
@@ -74,6 +76,9 @@ It follows actual resource references to include meaningful query variants.
|
|
|
74
76
|
Generated application pages receive a manifest link and an `async` module
|
|
75
77
|
marked `data-arcane-pwa`. Existing application scripts retain their order.
|
|
76
78
|
PWA registration does not wait for models, storage, preferences or page rendering.
|
|
79
|
+
The same bootstrap starts one initially hidden `pwa-install.html` component with
|
|
80
|
+
the generated manifest's app name. Component loading and worker registration
|
|
81
|
+
proceed independently.
|
|
77
82
|
|
|
78
83
|
The selected PWA browser delivery removes `v` and `arcaneVersion` from actual
|
|
79
84
|
local resource references, including the managed import map. Other query fields,
|
|
@@ -91,7 +96,10 @@ Package-only applications retain their existing descriptor workflow.
|
|
|
91
96
|
|
|
92
97
|
Add a file or directory to `package.include` to make it part of the app's
|
|
93
98
|
resources. A new file inside an already included directory needs no separate
|
|
94
|
-
entry.
|
|
99
|
+
entry. `package.include` is an application resource selection, not a file list
|
|
100
|
+
inside the Web App Manifest. `arcane.webmanifest` contains browser installation
|
|
101
|
+
metadata; `arcane-offline.json` contains the selected offline resource inventory.
|
|
102
|
+
If `package.pwa.offline.include` is nonempty, the resource must also
|
|
95
103
|
match that offline selection and must not match `offline.exclude`. Adding a
|
|
96
104
|
path only to the offline selection does not add it to the app's resources.
|
|
97
105
|
Restart after changing descriptor settings. Edits to selected source files are
|
|
@@ -104,6 +112,8 @@ and packaged-preview serving, including conditional resource responses.
|
|
|
104
112
|
Every Arcane development server and packaged browser preview serves HTTPS,
|
|
105
113
|
including localhost. Configure the workspace certificate pair before starting
|
|
106
114
|
the ordinary command; see [development HTTPS setup](cli.md#development-https-setup).
|
|
115
|
+
`--public` selects the IPv4 wildcard bind address; it does not enable PWA
|
|
116
|
+
configuration, change manifest metadata, or determine browser installability.
|
|
107
117
|
|
|
108
118
|
Source inventory work begins when the browser requests the worker or current
|
|
109
119
|
offline manifest, after the page can start. It traverses the selected route
|
|
@@ -153,7 +163,15 @@ the previous timestamp so the next page load can retry. Each cached response
|
|
|
153
163
|
retains its own `Last-Modified` header, but there are no per-file check times.
|
|
154
164
|
The SDK imposes no age-based cache deletion and
|
|
155
165
|
retains resource bodies across app and SDK version changes. Requests for a page
|
|
156
|
-
do not wait for the complete resource inventory to finish checking.
|
|
166
|
+
do not wait for the complete resource inventory to finish checking. A file
|
|
167
|
+
already in the current resource cache also returns immediately when its own
|
|
168
|
+
conditional check is pending or in flight. That background check keeps its
|
|
169
|
+
existing owner and updates the stored response for subsequent requests.
|
|
170
|
+
The page receives the cached response's original status, commonly `200`, even
|
|
171
|
+
when the separate conditional network response is `304`. Status alone does
|
|
172
|
+
not identify a network transfer; use the browser's response source and timing
|
|
173
|
+
details to distinguish cache access from worker startup, queueing and network.
|
|
174
|
+
The SDK
|
|
157
175
|
uses at most four concurrent background resource requests and starts no timer
|
|
158
176
|
or polling loop between page loads.
|
|
159
177
|
|
|
@@ -205,6 +223,151 @@ Switching a server from a packaged release to live development does not replace
|
|
|
205
223
|
an already active release worker inside an open document. The same native
|
|
206
224
|
worker lifecycle applies.
|
|
207
225
|
|
|
226
|
+
## Installation component
|
|
227
|
+
|
|
228
|
+
Starting with SDK `0.13.0`, enabled PWA pages automatically mount the shared
|
|
229
|
+
[`pwa-install.html` component](runtime-components.md#pwa-installhtml). It appears
|
|
230
|
+
when the browser supplies an installation prompt, offers **Install** and a
|
|
231
|
+
clearly labeled close control, and does not move focus when it appears.
|
|
232
|
+
The floating suggestion has no automatic dismissal timer. Closing it remembers
|
|
233
|
+
the choice for the current tab session and manifest URL, so another page
|
|
234
|
+
load does not immediately show it again. A storage failure leaves the current
|
|
235
|
+
page's dismissal functional and reports the error through console diagnostics.
|
|
236
|
+
|
|
237
|
+
An application can also place the same component inline through `html-import`
|
|
238
|
+
with `data-presentation="inline"`. Both presentations share one page-owned
|
|
239
|
+
native installation event. Dismissing the floating suggestion does not consume
|
|
240
|
+
that event or disable an explicitly placed inline component. Closing an inline
|
|
241
|
+
instance hides only that instance. See the component reference for its
|
|
242
|
+
configuration, methods and events.
|
|
243
|
+
|
|
244
|
+
The browser controls the URL-bar installation indicator and native prompt.
|
|
245
|
+
The SDK cannot force either to appear. Without a captured
|
|
246
|
+
`beforeinstallprompt`, the component remains hidden; that waiting state does
|
|
247
|
+
not establish that installation is unsupported. The browser may still be
|
|
248
|
+
evaluating the app, may already have it installed, or may only support a
|
|
249
|
+
manual browser-menu installation path.
|
|
250
|
+
|
|
251
|
+
### Browser installation requirements
|
|
252
|
+
|
|
253
|
+
Inspect the loaded page's manifest link and the browser's manifest diagnostics
|
|
254
|
+
when an install action is missing. Confirm that the generated manifest has the
|
|
255
|
+
intended name, `start_url`, scope and app display mode, and that its icon URLs
|
|
256
|
+
resolve to actual images with the declared dimensions. For Chromium's manifest
|
|
257
|
+
install promotion, provide a `purpose: "any"` icon, or omit `purpose` to use
|
|
258
|
+
that default, in PNG, SVG or WebP format. Its strict installation icon selector
|
|
259
|
+
excludes JPEG even when the same image renders successfully on the page.
|
|
260
|
+
Do not change a file's extension or MIME declaration without converting the
|
|
261
|
+
actual image at the application's asset owner. See Chromium's
|
|
262
|
+
[icon selection implementation](https://raw.githubusercontent.com/chromium/chromium/main/third_party/blink/common/manifest/manifest_icon_selector.cc).
|
|
263
|
+
|
|
264
|
+
Providing 192-by-192 and 512-by-512 raster icons follows the
|
|
265
|
+
[browser guidance](https://web.dev/articles/add-manifest). Their absence alone
|
|
266
|
+
does not prove the failure: Chromium can select one larger supported icon.
|
|
267
|
+
Keep actual icon dimensions in `sizes`. Browser diagnostics about missing
|
|
268
|
+
`screenshots` concern the richer installation dialog; screenshots are optional
|
|
269
|
+
and are separate from a usable installation icon.
|
|
270
|
+
|
|
271
|
+
Browser installation requires HTTPS or the browser's localhost/loopback
|
|
272
|
+
exception. A device-facing LAN address is not loopback. Arcane's development
|
|
273
|
+
server still follows its own HTTPS serving contract above. Browser engagement,
|
|
274
|
+
installation state and platform support also affect whether native promotion
|
|
275
|
+
appears; worker cache readiness is not an installation UI prerequisite. See
|
|
276
|
+
[browser installation requirements](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Making_PWAs_installable).
|
|
277
|
+
|
|
278
|
+
Browsers without `beforeinstallprompt` can offer manual installation. For
|
|
279
|
+
example, current iPhone Safari uses Share, **Add to Home Screen**, **Open as
|
|
280
|
+
Web App**, then **Add**. A product may explain that browser-owned path in its
|
|
281
|
+
help, but should not present it as a programmatic SDK install action. The SDK
|
|
282
|
+
does not infer installation support from the user-agent string. See
|
|
283
|
+
[Apple's installation instructions](https://support.apple.com/guide/iphone/open-as-web-app-iphea86e5236/ios).
|
|
284
|
+
|
|
285
|
+
## getPwaInstall()
|
|
286
|
+
|
|
287
|
+
Import `getPwaInstall` and `PWA_INSTALL_STATE_EVENT` from `arcane-os/pwa`.
|
|
288
|
+
`getPwaInstall()` synchronously returns the shared page owner with `state`,
|
|
289
|
+
`subscribe`, `prompt`, `dismiss` and `dispose`. Call it early when owning a
|
|
290
|
+
separate install entry point so it can capture `beforeinstallprompt` before
|
|
291
|
+
loading the UI. The generated bootstrap already does this through
|
|
292
|
+
`mountPwaInstallPrompt()`.
|
|
293
|
+
|
|
294
|
+
`state` contains `status`, `available`, `dismissed`, `outcome` and `error`.
|
|
295
|
+
Status is `waiting`, `available`, `prompting`, `accepted`, `dismissed`,
|
|
296
|
+
`installed`, `running`, `error` or `disposed`. `available` means a native event
|
|
297
|
+
is retained; a dismissed floating suggestion can still have `available: true`.
|
|
298
|
+
`outcome` is the browser's `accepted` or `dismissed` choice, or `null` before a
|
|
299
|
+
choice. `error` carries the complete prompt error, or `null`.
|
|
300
|
+
|
|
301
|
+
`subscribe(listener, {emitCurrent: true, signal} = {})` immediately replays
|
|
302
|
+
state by default and returns an unsubscribe function. Later state travels
|
|
303
|
+
through the existing Arcane event owner using `PWA_INSTALL_STATE_EVENT`
|
|
304
|
+
(`arcane.pwa.install.state`). A subscription does not wait for worker
|
|
305
|
+
registration, storage initialization or model readiness.
|
|
306
|
+
|
|
307
|
+
Call `prompt()` directly from the user's install click, before any asynchronous
|
|
308
|
+
wait. It invokes the browser prompt in the same call stack, consumes the event
|
|
309
|
+
once, and returns a promise for the browser's choice. It resolves to `null`
|
|
310
|
+
when there is no retained event or the owner is disposed. Failure publishes
|
|
311
|
+
`error` state and rejects. A new native event is required for another prompt.
|
|
312
|
+
|
|
313
|
+
```javascript
|
|
314
|
+
import {getPwaInstall} from 'arcane-os/pwa';
|
|
315
|
+
|
|
316
|
+
const install = getPwaInstall();
|
|
317
|
+
const installButton = document.querySelector('#install');
|
|
318
|
+
|
|
319
|
+
install.subscribe(function showInstallAvailability(state) {
|
|
320
|
+
installButton.hidden = !state.available;
|
|
321
|
+
});
|
|
322
|
+
installButton.addEventListener('click', function requestInstallation() {
|
|
323
|
+
install.prompt().catch(function reportInstallFailure(error) {
|
|
324
|
+
console.error(error);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
`dismiss()` remembers the session choice without consuming the retained event
|
|
330
|
+
and returns the current state. A browser-native dismissed choice is remembered
|
|
331
|
+
too. `appinstalled` clears the event and publishes `installed`; running in an
|
|
332
|
+
app display mode publishes `running` and suppresses the prompt. These states
|
|
333
|
+
do not establish offline readiness. On Android, `appinstalled` can arrive
|
|
334
|
+
before WebAPK creation finishes. See the
|
|
335
|
+
[browser lifecycle distinction](https://web.dev/learn/pwa/detection/).
|
|
336
|
+
|
|
337
|
+
`dispose()` removes the shared owner's native listeners and subscriptions.
|
|
338
|
+
Leaving the page disposes it automatically, except when the browser retains
|
|
339
|
+
the page in its back/forward cache.
|
|
340
|
+
Because the owner is shared, an individual component should dispose its own
|
|
341
|
+
subscription instead. A later `getPwaInstall()` creates a new owner after
|
|
342
|
+
disposal; it cannot recover a native event that was already consumed.
|
|
343
|
+
|
|
344
|
+
## mountPwaInstallPrompt()
|
|
345
|
+
|
|
346
|
+
`mountPwaInstallPrompt({appName = ''} = {})` starts native install observation
|
|
347
|
+
synchronously, then loads the shared HTML import and theme modules concurrently
|
|
348
|
+
and appends one initially hidden component when the document body is available.
|
|
349
|
+
It returns the same mounting promise on repeated calls; the first call supplies
|
|
350
|
+
the initial app name. The promise resolves to the ready `html-import` host, or
|
|
351
|
+
`null` without a document or when the owner is disposed before mounting. It
|
|
352
|
+
rejects if component loading fails, or with `AbortError` when the loading host
|
|
353
|
+
is removed or its owner disposed. A rejected mount releases its slot so an
|
|
354
|
+
explicit later call can try again. Observe the rejection without making page
|
|
355
|
+
rendering wait for it.
|
|
356
|
+
|
|
357
|
+
The generated PWA bootstrap calls this automatically using the manifest name.
|
|
358
|
+
Applications need not add another floating suggestion. A separate entry point
|
|
359
|
+
can call it explicitly:
|
|
360
|
+
|
|
361
|
+
```javascript
|
|
362
|
+
import {mountPwaInstallPrompt} from 'arcane-os/pwa';
|
|
363
|
+
|
|
364
|
+
mountPwaInstallPrompt({appName: 'Example Library'}).catch(
|
|
365
|
+
function reportInstallComponentFailure(error) {
|
|
366
|
+
console.error(error);
|
|
367
|
+
}
|
|
368
|
+
);
|
|
369
|
+
```
|
|
370
|
+
|
|
208
371
|
## registerPwa()
|
|
209
372
|
|
|
210
373
|
Import `registerPwa` and `PWA_STATE_EVENT` from `arcane-os/pwa` through the
|