arcane-os 0.16.2 → 0.16.3

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.16.3
4
+
5
+ - Remember browser-reported PWA installation in app-scoped DBOPFS and suppress
6
+ the SDK's floating and inline installation controls on later visits. Capture
7
+ native events immediately while restoring the saved flag; keep rendering,
8
+ component loading and worker registration independent.
9
+ - Preserve confirmed installation across late reads, prompt results and display
10
+ changes. Save installed-app launches as well as `appinstalled`, without
11
+ mistaking prompt acceptance or ordinary fullscreen for installation. Expose
12
+ initial storage readiness and complete persistence errors through the shared
13
+ install owner, and retain confirmed-install writes through owner disposal.
14
+
3
15
  ## 0.16.2
4
16
 
5
17
  - Scope explicit application selection and workspace resolution to the named
@@ -11,7 +11,11 @@ export function getPwaInstall() {
11
11
  if (sharedOwner && sharedOwner.state.status !== 'disposed') {
12
12
  return sharedOwner;
13
13
  }
14
- const owner = {get state() { return snapshot(); }, subscribe, prompt, dismiss, dispose};
14
+ const owner = {
15
+ get state() { return snapshot(); },
16
+ get ready() { return ready; },
17
+ subscribe, prompt, dismiss, dispose
18
+ };
15
19
  const source = createArcaneEventSource(owner, {
16
20
  source: 'arcane.pwa.install', eventTypes: [PWA_INSTALL_STATE_EVENT]
17
21
  });
@@ -20,6 +24,9 @@ export function getPwaInstall() {
20
24
  '(display-mode: standalone), (display-mode: minimal-ui), '
21
25
  + '(display-mode: fullscreen), (display-mode: window-controls-overlay)'
22
26
  );
27
+ const installedDisplayMode = globalThis.matchMedia?.(
28
+ '(display-mode: standalone), (display-mode: minimal-ui), (display-mode: window-controls-overlay)'
29
+ );
23
30
  const manifestUrl = globalThis.document?.querySelector('link[rel~="manifest"]')?.href;
24
31
  const dismissalKey = `arcane.pwa.install.dismissed:${manifestUrl ?? globalThis.location?.href ?? ''}`;
25
32
  let deferredPrompt = null;
@@ -28,6 +35,11 @@ export function getPwaInstall() {
28
35
  let status = isRunningAsApp() ? 'running' : 'waiting';
29
36
  let outcome = null;
30
37
  let error = null;
38
+ let installed = isInstalledApp();
39
+ let storedInstalled = false;
40
+ let storageReady = false;
41
+ let storageError = null;
42
+ let saveTask = null;
31
43
  try {
32
44
  dismissed = globalThis.sessionStorage?.getItem(dismissalKey) === 'true';
33
45
  } catch (storageError) {
@@ -38,9 +50,17 @@ export function getPwaInstall() {
38
50
  return displayMode?.matches === true || globalThis.navigator?.standalone === true;
39
51
  }
40
52
 
53
+ function isInstalledApp() {
54
+ // Ordinary browser fullscreen is not evidence of installation.
55
+ return installedDisplayMode?.matches === true || globalThis.navigator?.standalone === true;
56
+ }
57
+
41
58
  function snapshot() {
42
- return {status, available: deferredPrompt !== null && !disposed,
43
- dismissed, outcome, error};
59
+ return {
60
+ status,
61
+ available: storageReady && deferredPrompt !== null && !installed && !isRunningAsApp() && !disposed,
62
+ installed, dismissed, outcome, error, storageError
63
+ };
44
64
  }
45
65
 
46
66
  function publish(nextStatus, nextError = null) {
@@ -67,24 +87,92 @@ export function getPwaInstall() {
67
87
  }
68
88
  }
69
89
 
90
+ async function loadInstallStorage() {
91
+ if (!globalThis.dbopfs) {
92
+ await import('arcane/DBOPFS');
93
+ }
94
+ const storage = globalThis.dbopfs;
95
+ if (!storage) {
96
+ throw new Error('PWA installation state could not open DBOPFS.');
97
+ }
98
+ await storage.readyPromise;
99
+ return storage;
100
+ }
101
+
102
+ function reportStorageError(failure) {
103
+ storageError = failure;
104
+ console.warn('Arcane PWA installation state could not be persisted or restored:', failure);
105
+ publish(status, error);
106
+ }
107
+
108
+ async function restoreInstallation() {
109
+ try {
110
+ const storage = await storageTask;
111
+ const record = await storage.get('pwa', 'installed.json', true);
112
+ storedInstalled = record?.installed === true;
113
+ if (!disposed && storedInstalled) {
114
+ installed = true;
115
+ deferredPrompt = null;
116
+ }
117
+ } catch (failure) {
118
+ reportStorageError(failure);
119
+ }
120
+ storageReady = true;
121
+ if (!disposed) {
122
+ if (installed) {
123
+ publish(isRunningAsApp() ? 'running' : 'installed', error);
124
+ } else if (deferredPrompt && !isRunningAsApp()) {
125
+ publish('available', error);
126
+ } else {
127
+ publish(status, error);
128
+ }
129
+ }
130
+ return snapshot();
131
+ }
132
+
133
+ async function saveInstallation() {
134
+ // The initial read avoids rewriting an already remembered installation.
135
+ await ready;
136
+ if (storedInstalled) return;
137
+ const storage = await storageTask;
138
+ await storage.set(
139
+ 'pwa',
140
+ 'installed.json',
141
+ {installed: true}
142
+ );
143
+ storedInstalled = true;
144
+ storageError = null;
145
+ publish(status, error);
146
+ }
147
+
148
+ function rememberInstallation() {
149
+ installed = true;
150
+ deferredPrompt = null;
151
+ // Retain and observe this durable write even if the page owner detaches.
152
+ saveTask ??= saveInstallation().catch(reportStorageError);
153
+ }
154
+
70
155
  function onBeforeInstallPrompt(event) {
71
- if (disposed || isRunningAsApp() || status === 'installed' || status === 'accepted') return;
156
+ if (disposed || installed || isRunningAsApp() || status === 'accepted') return;
72
157
  event.preventDefault();
73
158
  deferredPrompt = event;
74
159
  outcome = null;
75
- publish('available');
160
+ publish(storageReady ? 'available' : 'waiting');
76
161
  }
77
162
 
78
163
  function onInstalled() {
79
- deferredPrompt = null;
164
+ rememberInstallation();
80
165
  // This event may precede Android's completion of WebAPK creation.
81
166
  publish('installed');
82
167
  }
83
168
 
84
169
  function onDisplayModeChange() {
170
+ if (isInstalledApp()) rememberInstallation();
85
171
  if (isRunningAsApp()) {
86
172
  deferredPrompt = null;
87
173
  publish('running');
174
+ } else if (installed) {
175
+ publish('installed');
88
176
  } else if (status === 'running') {
89
177
  publish('waiting');
90
178
  }
@@ -108,7 +196,7 @@ export function getPwaInstall() {
108
196
  }
109
197
 
110
198
  function prompt() {
111
- if (disposed || !deferredPrompt) return Promise.resolve(null);
199
+ if (!snapshot().available) return Promise.resolve(null);
112
200
  const event = deferredPrompt;
113
201
  deferredPrompt = null;
114
202
  publish('prompting');
@@ -162,8 +250,12 @@ export function getPwaInstall() {
162
250
  observe(globalThis, 'beforeinstallprompt', onBeforeInstallPrompt);
163
251
  observe(globalThis, 'appinstalled', onInstalled);
164
252
  observe(displayMode, 'change', onDisplayModeChange);
253
+ observe(installedDisplayMode, 'change', onDisplayModeChange);
165
254
  observe(globalThis, 'pagehide', onPageHide);
166
255
  sharedOwner = owner;
256
+ const storageTask = loadInstallStorage();
257
+ const ready = restoreInstallation();
258
+ if (installed) rememberInstallation();
167
259
  return owner;
168
260
  }
169
261
 
@@ -293,18 +293,29 @@ does not infer installation support from the user-agent string. See
293
293
  ## getPwaInstall()
294
294
 
295
295
  Import `getPwaInstall` and `PWA_INSTALL_STATE_EVENT` from `arcane-os/pwa`.
296
- `getPwaInstall()` synchronously returns the shared page owner with `state`,
296
+ `getPwaInstall()` synchronously returns the shared page owner with `state`, `ready`,
297
297
  `subscribe`, `prompt`, `dismiss` and `dispose`. Call it early when owning a
298
298
  separate install entry point so it can capture `beforeinstallprompt` before
299
299
  loading the UI. The generated bootstrap already does this through
300
300
  `mountPwaInstallPrompt()`.
301
301
 
302
- `state` contains `status`, `available`, `dismissed`, `outcome` and `error`.
302
+ `state` contains `status`, `available`, `installed`, `dismissed`, `outcome`,
303
+ `error` and `storageError`.
303
304
  Status is `waiting`, `available`, `prompting`, `accepted`, `dismissed`,
304
305
  `installed`, `running`, `error` or `disposed`. `available` means a native event
305
- is retained; a dismissed floating suggestion can still have `available: true`.
306
+ is retained, the initial installation-record read has settled, and installation
307
+ has not been recorded or detected. A dismissed floating suggestion can still
308
+ have `available: true`. `installed` is true after browser-reported installation
309
+ or restoration of that app's saved installation record.
306
310
  `outcome` is the browser's `accepted` or `dismissed` choice, or `null` before a
307
- choice. `error` carries the complete prompt error, or `null`.
311
+ choice. `error` carries the complete prompt error, or `null`; `storageError`
312
+ carries the complete DBOPFS read or write error, or `null`.
313
+
314
+ `ready` resolves to the state after the initial DBOPFS read settles. Native
315
+ events are captured synchronously while this read runs. Only installation
316
+ availability waits for it; page rendering, component loading and worker
317
+ registration continue independently. A storage failure is logged and published
318
+ as `storageError`; `ready` still resolves and native installation remains usable.
308
319
 
309
320
  `subscribe(listener, {emitCurrent: true, signal} = {})` immediately replays
310
321
  state by default and returns an unsubscribe function. Later state travels
@@ -315,7 +326,8 @@ registration, storage initialization or model readiness.
315
326
  Call `prompt()` directly from the user's install click, before any asynchronous
316
327
  wait. It invokes the browser prompt in the same call stack, consumes the event
317
328
  once, and returns a promise for the browser's choice. It resolves to `null`
318
- when there is no retained event or the owner is disposed. Failure publishes
329
+ when installation is unavailable, including while the saved state is loading
330
+ or after installation is remembered. Failure publishes
319
331
  `error` state and rejects. A new native event is required for another prompt.
320
332
 
321
333
  ```javascript
@@ -336,8 +348,24 @@ installButton.addEventListener('click', function requestInstallation() {
336
348
 
337
349
  `dismiss()` remembers the session choice without consuming the retained event
338
350
  and returns the current state. A browser-native dismissed choice is remembered
339
- too. `appinstalled` clears the event and publishes `installed`; running in an
340
- app display mode publishes `running` and suppresses the prompt. These states
351
+ too. `appinstalled` clears the event, publishes `installed`, and saves
352
+ `{installed: true}` as `pwa/installed.json` through the current app-scoped
353
+ DBOPFS singleton. An installed-app launch in standalone, minimal-ui or
354
+ window-controls-overlay mode, or with `navigator.standalone === true`, also
355
+ records installation. Ordinary fullscreen suppresses the current prompt but
356
+ does not record installation because browsers can enter fullscreen without
357
+ installing an app; see the [display-mode specification](https://drafts.csswg.org/mediaqueries-5/#display-modes).
358
+
359
+ Every new owner reads the saved flag, so both floating and explicit inline
360
+ controls stay hidden on subsequent visits in that browser origin's app scope.
361
+ The SDK never clears this flag or resets it when display mode changes. Prompt
362
+ acceptance and dismissal alone do not write it. The record is independent of
363
+ resource-check history and other application data. A pending read cannot undo
364
+ newly observed installation, and a confirmed installation's pending write is
365
+ retained through owner disposal. A write failure remains observable without
366
+ making the current installed session eligible again.
367
+
368
+ Running in an app display mode publishes `running`. These states
341
369
  do not establish offline readiness. On Android, `appinstalled` can arrive
342
370
  before WebAPK creation finishes. See the
343
371
  [browser lifecycle distinction](https://web.dev/learn/pwa/detection/).
@@ -1069,6 +1069,12 @@ availability comes from the browser's `beforeinstallprompt` event through
1069
1069
  `getPwaInstall()`. Without an available event, the suggestion stays hidden;
1070
1070
  absence does not identify why installation is unavailable. Installed-app events,
1071
1071
  accepted prompts, and an already running installed display mode hide it.
1072
+ The shared SDK owner persists browser-reported installation in app-scoped
1073
+ DBOPFS. Both floating and inline controls stay hidden while that record is
1074
+ loading and remain hidden on later visits when installation is remembered.
1075
+ Acceptance alone does not save installation. The component's `ready` means
1076
+ its methods and subscription are attached; storage completion belongs to the
1077
+ shared owner's `ready` promise and does not delay page rendering.
1072
1078
 
1073
1079
  ### Example
1074
1080
 
@@ -6231,13 +6231,24 @@ payload and current-state subscription behavior.
6231
6231
 
6232
6232
  ### Overview
6233
6233
 
6234
- `getPwaInstall()` returns one synchronous page owner exposing `state`,
6234
+ `getPwaInstall()` returns one synchronous page owner exposing `state`, `ready`,
6235
6235
  `subscribe`, `prompt`, `dismiss` and `dispose`. It captures the browser's
6236
6236
  `beforeinstallprompt` event and observes `appinstalled` and app display-mode
6237
6237
  changes. A missing event leaves installation availability unknown and the owner
6238
6238
  waiting; it does not prove browser incompatibility.
6239
6239
 
6240
- State contains `status`, `available`, `dismissed`, `outcome` and `error`.
6240
+ State contains `status`, `available`, `installed`, `dismissed`, `outcome`,
6241
+ `error` and `storageError`. The owner restores `pwa/installed.json` through the
6242
+ app-scoped DBOPFS singleton and saves `{installed: true}` after `appinstalled`
6243
+ or an installed-app launch. Remembered installation suppresses both floating
6244
+ and inline controls on later visits. Acceptance alone and ordinary fullscreen
6245
+ do not record installation.
6246
+
6247
+ `ready` resolves to the state once the initial read settles. Native events are
6248
+ captured immediately, while install availability waits for that read. Rendering
6249
+ and worker registration remain independent. Storage failures are logged and
6250
+ published in `storageError`; they do not reject `ready` or reverse an observed
6251
+ installation. Confirmed-install writes remain owned after disposal.
6241
6252
  Subscriptions replay current state by default. `prompt()` must be called
6242
6253
  directly within the install click to preserve native user activation. It
6243
6254
  consumes the event once and returns the browser choice, resolves to `null` when
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.16.2",
3
+ "version": "0.16.3",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",