@swiftbrowser/web 0.17.6 → 0.17.8

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.
@@ -51,6 +51,11 @@ export interface AppSpec {
51
51
  * page's callback page (docs/ops-protocol.md, "Signing in").
52
52
  */
53
53
  urlSchemes?: string[];
54
+ /**
55
+ * The Web Push application server key (a VAPID public key, base64url) the
56
+ * app's `registerForRemoteNotifications` subscribes with (src/notifications.ts).
57
+ */
58
+ webPushKey?: string;
54
59
  /**
55
60
  * A CORS proxy (packages/cors-proxy): the page retries a cross-origin GET or
56
61
  * HEAD the browser refused through `<corsProxy>?url=<target>` (src/corsProxy.ts).
@@ -1 +1 @@
1
- {"version":3,"file":"apps.d.ts","sourceRoot":"","sources":["../../apps.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,MAAM,WAAW,OAAO;IACtB;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0CAA0C;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,gHAAgH;IAChH,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,0FAA0F;IAC1F,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,gHAAgH;IAChH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,yEAAyE;AACzE,eAAO,MAAM,QAAQ,QAAqB,CAAC;AAE3C,sCAAsC;AACtC,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,CAAC,GAAG,MAAM,CAEjF;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,GAAG,IAAI,CA+ChE"}
1
+ {"version":3,"file":"apps.d.ts","sourceRoot":"","sources":["../../apps.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,MAAM,WAAW,OAAO;IACtB;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0CAA0C;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,gHAAgH;IAChH,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,0FAA0F;IAC1F,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,gHAAgH;IAChH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,yEAAyE;AACzE,eAAO,MAAM,QAAQ,QAAqB,CAAC;AAE3C,sCAAsC;AACtC,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,CAAC,GAAG,MAAM,CAEjF;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,GAAG,IAAI,CAkDhE"}
@@ -58,6 +58,9 @@ export function validateAppSpecs(specs) {
58
58
  if (spec.corsProxy !== undefined && (typeof spec.corsProxy !== 'string' || !/^https?:\/\/[^\s]+$/.test(spec.corsProxy))) {
59
59
  throw new Error(`${label}: "corsProxy" must be an http(s) URL`);
60
60
  }
61
+ if (spec.webPushKey !== undefined && (typeof spec.webPushKey !== 'string' || !/^[A-Za-z0-9_-]{80,96}$/.test(spec.webPushKey))) {
62
+ throw new Error(`${label}: "webPushKey" must be a VAPID public key (base64url, 65 bytes)`);
63
+ }
61
64
  if (spec.blockedHosts !== undefined && (!Array.isArray(spec.blockedHosts) || !spec.blockedHosts.every((h) => typeof h === 'string' && /^[A-Za-z0-9.-]+$/.test(h)))) {
62
65
  throw new Error(`${label}: "blockedHosts" must be an array of host names`);
63
66
  }
@@ -1 +1 @@
1
- {"version":3,"file":"apps.js","sourceRoot":"","sources":["../../apps.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"apps.js","sourceRoot":"","sources":["../../apps.ts"],"names":[],"mappings":"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"}
@@ -67,6 +67,8 @@ export interface LandingApp {
67
67
  urlSchemes?: string[];
68
68
  /** The app's CORS proxy (`AppSpec.corsProxy`); the page retries refused cross-origin reads through it. */
69
69
  corsProxy?: string;
70
+ /** The app's Web Push application server key (`AppSpec.webPushKey`); the page passes it to the app as `SB_WEB_PUSH_KEY`. */
71
+ webPushKey?: string;
70
72
  /** Hosts the app's requests may not reach (`AppSpec.blockedHosts`). */
71
73
  blockedHosts?: string[];
72
74
  }
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAKvC,OAAO,EAAmD,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAChI,OAAO,EAAE,KAAK,iBAAiB,EAAyC,MAAM,0BAA0B,CAAC;AAEzG,OAAO,EAAmC,KAAK,OAAO,EAAE,MAAM,WAAW,CAAC;AAE1E,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,sGAAsG;IACtG,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB;;;OAGG;IACH,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,kEAAkE;AAClE,wBAAgB,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC,GAAG,UAAU,CAG9F;AAED,qGAAqG;AACrG,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oHAAoH;IACpH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,yGAAyG;IACzG,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,0GAA0G;IAC1G,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAOD;;;;;GAKG;AACH,wBAAgB,OAAO,IAAI,MAAM,CAoBhC;AAED,gDAAgD;AAChD,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,GAAG,UAAU,EAAE,CAWnE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,UAAU,CAoEjE"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAKvC,OAAO,EAAmD,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAChI,OAAO,EAAE,KAAK,iBAAiB,EAAyC,MAAM,0BAA0B,CAAC;AAEzG,OAAO,EAAmC,KAAK,OAAO,EAAE,MAAM,WAAW,CAAC;AAE1E,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,sGAAsG;IACtG,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB;;;OAGG;IACH,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,kEAAkE;AAClE,wBAAgB,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC,GAAG,UAAU,CAG9F;AAED,qGAAqG;AACrG,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oHAAoH;IACpH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,yGAAyG;IACzG,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,0GAA0G;IAC1G,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4HAA4H;IAC5H,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAOD;;;;;GAKG;AACH,wBAAgB,OAAO,IAAI,MAAM,CAoBhC;AAED,gDAAgD;AAChD,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,GAAG,UAAU,EAAE,CAYnE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,UAAU,CAoEjE"}
@@ -76,6 +76,8 @@ export function landingApps(specs) {
76
76
  entry.urlSchemes = [...spec.urlSchemes];
77
77
  if (spec.corsProxy)
78
78
  entry.corsProxy = spec.corsProxy;
79
+ if (spec.webPushKey)
80
+ entry.webPushKey = spec.webPushKey;
79
81
  if (spec.blockedHosts?.length)
80
82
  entry.blockedHosts = [...spec.blockedHosts];
81
83
  return entry;
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../../config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../config.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swiftbrowser/web",
3
- "version": "0.17.6",
3
+ "version": "0.17.8",
4
4
  "type": "module",
5
5
  "description": "SwiftBrowser web renderer and dev server: applies render ops from a Swift/Wasm app to the DOM inside an iPhone frame.",
6
6
  "repository": {
package/public/_headers CHANGED
@@ -28,3 +28,8 @@
28
28
  # plugins/asset-catalogs.ts). Not content-hashed: always revalidate.
29
29
  /catalog/*
30
30
  Cache-Control: no-cache
31
+
32
+ # The service worker (Web Push): always revalidate, and its scope is the site.
33
+ /sw.js
34
+ Cache-Control: no-cache
35
+ Service-Worker-Allowed: /
@@ -18,7 +18,7 @@
18
18
  */
19
19
 
20
20
  import { base64Decode, base64Encode } from './base64';
21
- import { browserNotificationHost, performNotification, type NotificationHost, type NotificationResult } from './notifications';
21
+ import { browserNotificationHost, browserPushHost, performNotification, type NotificationHost, type NotificationResult } from './notifications';
22
22
  import type {
23
23
  BitmapRecipe,
24
24
  FetchRequestParams,
@@ -32,6 +32,7 @@ import type {
32
32
  OpenURLRequestParams,
33
33
  WebAuthRequestParams,
34
34
  MediaRequestParams,
35
+ MediaSessionRequestParams,
35
36
  DownloadRequestParams,
36
37
  ClipboardRequestParams,
37
38
  TranslateRequestParams,
@@ -40,6 +41,7 @@ import type {
40
41
  } from './protocol';
41
42
  import { browserClipboardHost, performClipboard, type ClipboardHost } from './clipboard';
42
43
  import { browserMediaHost, performMedia, type MediaHost } from './media';
44
+ import { browserMediaSessionHost, MediaSessionBridge, performMediaSession } from './mediaSession';
43
45
  import { browserScannerHost, performScanner, type ScannerHost, type ScannerResult } from './scanner';
44
46
  import { browserTranslationHost, performTranslate, type TranslationHost } from './translation';
45
47
  import { chooseColor, performPickColor, type ColorChooser } from './colorPicker';
@@ -123,6 +125,8 @@ export interface HostRequestEnvironment {
123
125
  webAuth?: WebAuthSessions;
124
126
  /** AVFoundation's media files and probes, QuickLook's previews (`media`, media.ts). */
125
127
  media?: MediaHost;
128
+ /** MediaPlayer's now-playing info and remote commands (src/mediaSession.ts); absent without the Media Session API. */
129
+ mediaSession?: MediaSessionBridge;
126
130
  /** The system clipboard (`clipboard`, clipboard.ts). */
127
131
  clipboard?: ClipboardHost;
128
132
  /** The browser's built-in translator (`translate`, translation.ts), where it has one. */
@@ -174,9 +178,21 @@ function downloadBlob(blob: Blob, filename: string): void {
174
178
  }
175
179
 
176
180
  const notificationHost = browserNotificationHost();
181
+
182
+ /**
183
+ * Gives the notifications Web Push through the service worker at `workerURL`
184
+ * with `scope` (the site's base, which the page knows: main.ts), where the
185
+ * browser has a push service.
186
+ */
187
+ export function installPushService(workerURL: string, scope: string): void {
188
+ if (!notificationHost || notificationHost.push) return;
189
+ const push = browserPushHost(workerURL, scope);
190
+ if (push) notificationHost.push = push;
191
+ }
177
192
  const scannerHost = browserScannerHost();
178
193
  const webAuthHost = browserWebAuthHost();
179
194
  const mediaHost = browserMediaHost(typeof document !== 'undefined' ? downloadBlob : undefined);
195
+ const mediaSessionHost = browserMediaSessionHost();
180
196
  const clipboardHost = browserClipboardHost();
181
197
  const translationHost = browserTranslationHost();
182
198
 
@@ -209,6 +225,7 @@ export const browserEnvironment: HostRequestEnvironment = {
209
225
  : {}),
210
226
  ...(webAuthHost ? { webAuth: new WebAuthSessions(webAuthHost) } : {}),
211
227
  ...(mediaHost ? { media: mediaHost } : {}),
228
+ ...(mediaSessionHost ? { mediaSession: new MediaSessionBridge(mediaSessionHost) } : {}),
212
229
  ...(clipboardHost ? { clipboard: clipboardHost } : {}),
213
230
  ...(translationHost ? { translation: translationHost } : {}),
214
231
  ...(typeof document !== 'undefined' ? { pickColor: chooseColor } : {}),
@@ -256,6 +273,8 @@ export async function performHostRequest(
256
273
  case 'media':
257
274
  // `open` is called before any await, while the tap's activation may still count.
258
275
  return performMedia(op.params as MediaRequestParams, env.media);
276
+ case 'mediaSession':
277
+ return performMediaSession(op.params as MediaSessionRequestParams, env.mediaSession);
259
278
  case 'download':
260
279
  return performDownload(op.params as DownloadRequestParams, env);
261
280
  case 'clipboard': {
package/src/main.ts CHANGED
@@ -18,7 +18,8 @@ import { BrowserHistory } from './browserHistory';
18
18
  import { Renderer, RendererError } from './renderer';
19
19
  import { withBlockedHosts } from './blockedHosts';
20
20
  import { withCORSProxy } from './corsProxy';
21
- import { browserEnvironment } from './hostRequests';
21
+ import { browserEnvironment, installPushService } from './hostRequests';
22
+ import { locateBundleFiles } from './media';
22
23
  import { callbackPageURL } from './webAuth';
23
24
  import { bitmapStore } from './bitmaps';
24
25
  import { DEFAULT_TYPE_SIZE, applyTypeSizeVariables } from './typography';
@@ -393,6 +394,11 @@ function launchEnvironment(): string[] {
393
394
  const schemes = registeredApp()?.urlSchemes;
394
395
  if (schemes?.length) env.push(`SB_URL_SCHEMES=${schemes.join(',')}`);
395
396
  env.push(`SB_WEB_AUTH_CALLBACK=${callbackPageURL(new URL(import.meta.env.BASE_URL, location.href).href)}`);
397
+ // Web Push (notifications.ts): the service worker next to the page, and
398
+ // the key `registerForRemoteNotifications` subscribes with.
399
+ installPushService(`${import.meta.env.BASE_URL}sw.js`, import.meta.env.BASE_URL);
400
+ const webPushKey = registeredApp()?.webPushKey;
401
+ if (webPushKey) env.push(`SB_WEB_PUSH_KEY=${webPushKey}`);
396
402
  return env;
397
403
  }
398
404
 
@@ -460,6 +466,11 @@ function saveHome(home: ReturnType<typeof homeDirectory>): void {
460
466
  files?.apply(changes).catch((error: unknown) => console.warn('[sb] could not save the app\'s files to IndexedDB', error));
461
467
  }
462
468
 
469
+ /** Where the page serves the resource at `path` (inside the bundle) of `app`. */
470
+ function bundleFileURL(app: string, path: string): string {
471
+ return `${import.meta.env.BASE_URL}bundle/${app}/${path.split('/').map(encodeURIComponent).join('/')}`;
472
+ }
473
+
463
474
  /** The app's resources (`/bundle/<App>.json` and its files), for the WASI `/bundle` mount; empty without a manifest. */
464
475
  async function loadBundle(app: string): Promise<Map<string, Uint8Array>> {
465
476
  const files = new Map<string, Uint8Array>();
@@ -470,8 +481,7 @@ async function loadBundle(app: string): Promise<Map<string, Uint8Array>> {
470
481
  if (!Array.isArray(manifest.files)) return files;
471
482
  await Promise.all(
472
483
  manifest.files.map(async (file) => {
473
- const url = `${import.meta.env.BASE_URL}bundle/${app}/${file.path.split('/').map(encodeURIComponent).join('/')}`;
474
- const data = await fetch(url, { cache: 'no-cache' });
484
+ const data = await fetch(bundleFileURL(app, file.path), { cache: 'no-cache' });
475
485
  if (data.ok) files.set(file.path, new Uint8Array(await data.arrayBuffer()));
476
486
  else console.warn(`[sb] resource ${file.path} of ${app}: HTTP ${data.status}`);
477
487
  }),
@@ -601,6 +611,8 @@ async function boot(): Promise<void> {
601
611
  console.info(`[sb] restoring state for ${requestedApp}: ${restored}`);
602
612
  }
603
613
  const bundle = await loadBundle(requestedApp);
614
+ // A player given a bundle file streams it from here rather than through a copy of its bytes (src/media.ts).
615
+ locateBundleFiles((path) => (bundle.has(path) ? bundleFileURL(requestedApp, path) : null));
604
616
  const stores = launchValue(await loadSwiftData());
605
617
  if (stores !== null) env.push(`SB_SWIFTDATA=${stores}`);
606
618
  savedHome = await loadHome();
package/src/media.ts CHANGED
@@ -3,7 +3,11 @@
3
3
  * AVFoundation's files and metadata probes, and QuickLook's previews.
4
4
  *
5
5
  * - `file` (`name`, `type`, `data` base64): a blob of the bytes; answers
6
- * `{ url }`, an object URL a `video` element can play.
6
+ * `{ url }`, an object URL a `video` element can play. With `path` in
7
+ * place of `data` (a resource of the app's bundle), the URL the page
8
+ * serves that resource at, so the browser streams it and nothing is
9
+ * copied; fails `unsupported: …` when the page has no such resource,
10
+ * and the app sends the bytes instead.
7
11
  * - `probe` (`url`): reads the media's metadata in a detached `<video>`;
8
12
  * answers `{ duration, width, height }` (`duration` null for a stream),
9
13
  * fails (`network: …`) when the browser cannot load or decode it.
@@ -37,12 +41,29 @@ export interface MediaHost {
37
41
  /** Opens `url` in a new tab; false when the browser blocked it. */
38
42
  open(url: string): boolean;
39
43
  download?(blob: Blob, filename: string): void;
44
+ /** The URL the page serves the bundle resource at `path` from, or null for a resource it did not load. */
45
+ bundleURL?(path: string): string | null;
46
+ }
47
+
48
+ let bundleLocator: ((path: string) => string | null) | null = null;
49
+
50
+ /**
51
+ * Tells the media host where the app's bundle resources are served from
52
+ * (src/main.ts, once the bundle is loaded), for `file` requests by `path`.
53
+ */
54
+ export function locateBundleFiles(locator: ((path: string) => string | null) | null): void {
55
+ bundleLocator = locator;
40
56
  }
41
57
 
42
58
  export async function performMedia(params: MediaRequestParams, host: MediaHost | undefined): Promise<Record<string, unknown>> {
43
59
  if (!host) throw new Error('unsupported: this page has no media host');
44
60
  switch (params.action) {
45
61
  case 'file': {
62
+ if (params.path !== undefined) {
63
+ const url = host.bundleURL?.(params.path) ?? null;
64
+ if (url === null) throw new Error(`unsupported: the page has no bundle resource ${params.path}`);
65
+ return { url };
66
+ }
46
67
  const blob = new Blob([decode(params.data ?? '') as BlobPart], { type: params.type ?? '' });
47
68
  return { url: host.objectURL(blob) };
48
69
  }
@@ -128,5 +149,6 @@ export function browserMediaHost(download?: (blob: Blob, filename: string) => vo
128
149
  return true;
129
150
  },
130
151
  ...(download ? { download } : {}),
152
+ bundleURL: (path) => bundleLocator?.(path) ?? null,
131
153
  };
132
154
  }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * `mediaSession` host requests (docs/ops-protocol.md, "Now playing"):
3
+ * MediaPlayer's now-playing info and remote commands over the browser's
4
+ * Media Session API, which puts what plays on the lock screen and the
5
+ * system's media controls and routes their buttons (and hardware media
6
+ * keys) back to the page.
7
+ *
8
+ * - `update` (`metadata`, `playbackState`, `position`, `commands`, `skip`)
9
+ * sets what the session shows and which commands the app handles; the
10
+ * browser extrapolates the position at the rate, so the app sends it
11
+ * only when it changes (a play, a pause, a seek). Answers `{}`.
12
+ * - `receive` answers the next command the viewer gave: `{ command }`, with
13
+ * `position` for `seekTo` (seconds) and `interval` for the skips (the
14
+ * offset the system asked for, else the app's preferred one). Commands
15
+ * that arrive with nobody receiving wait in a queue, as WebSocket
16
+ * messages do (src/webSocket.ts). Fails `unsupported: …` on a page with
17
+ * no Media Session API, which ends the app's listening.
18
+ */
19
+
20
+ import type { MediaSessionCommand, MediaSessionRequestParams } from './protocol';
21
+
22
+ /** The part of `navigator.mediaSession` the requests use; tests substitute a fake. */
23
+ export interface MediaSessionHost {
24
+ setMetadata(metadata: { title: string; artist: string; album: string; artwork: { src: string }[] } | null): void;
25
+ setPlaybackState(state: 'none' | 'playing' | 'paused'): void;
26
+ setPositionState(state: { duration: number; playbackRate: number; position: number } | null): void;
27
+ /** Registers (or, with null, removes) the handler of a Media Session action; may throw for an action the browser lacks. */
28
+ setActionHandler(action: MediaSessionAction, handler: ((details: MediaSessionActionDetails) => void) | null): void;
29
+ }
30
+
31
+ /** A command the viewer gave, as `receive` answers it. */
32
+ export interface ReceivedCommand {
33
+ command: MediaSessionCommand;
34
+ position?: number;
35
+ interval?: number;
36
+ }
37
+
38
+ /** The Media Session action each command listens to. */
39
+ const ACTIONS: Record<MediaSessionCommand, MediaSessionAction> = {
40
+ play: 'play',
41
+ pause: 'pause',
42
+ stop: 'stop',
43
+ nextTrack: 'nexttrack',
44
+ previousTrack: 'previoustrack',
45
+ seekTo: 'seekto',
46
+ skipForward: 'seekforward',
47
+ skipBackward: 'seekbackward',
48
+ };
49
+
50
+ /** One page's media session: what it shows, the commands it listens to, and the ones received. */
51
+ export class MediaSessionBridge {
52
+ private readonly registered = new Set<MediaSessionCommand>();
53
+ private readonly queue: ReceivedCommand[] = [];
54
+ private readonly waiting: ((command: ReceivedCommand) => void)[] = [];
55
+ private skip: { forward?: number; backward?: number } = {};
56
+
57
+ constructor(private readonly host: MediaSessionHost) {}
58
+
59
+ perform(params: MediaSessionRequestParams): Promise<Record<string, unknown>> {
60
+ switch (params.action) {
61
+ case 'update':
62
+ this.update(params);
63
+ return Promise.resolve({});
64
+ case 'receive':
65
+ return this.receive();
66
+ default:
67
+ return Promise.reject(new Error(`unsupported: mediaSession action ${String((params as { action: unknown }).action)}`));
68
+ }
69
+ }
70
+
71
+ private update(params: MediaSessionRequestParams): void {
72
+ if (params.metadata !== undefined) {
73
+ const metadata = params.metadata;
74
+ this.host.setMetadata(
75
+ metadata === null
76
+ ? null
77
+ : { title: metadata.title ?? '', artist: metadata.artist ?? '', album: metadata.album ?? '', artwork: metadata.artwork ? [{ src: metadata.artwork }] : [] },
78
+ );
79
+ }
80
+ if (params.playbackState !== undefined) this.host.setPlaybackState(params.playbackState);
81
+ if (params.position !== undefined) {
82
+ const position = params.position;
83
+ this.host.setPositionState(position === null ? null : { duration: position.duration, playbackRate: position.rate, position: position.position });
84
+ }
85
+ if (params.skip !== undefined) this.skip = params.skip;
86
+ if (params.commands !== undefined) this.listen(new Set(params.commands));
87
+ }
88
+
89
+ /** Listens to exactly `wanted`: handlers for the new ones, none for the dropped ones. */
90
+ private listen(wanted: Set<MediaSessionCommand>): void {
91
+ for (const command of this.registered) {
92
+ if (wanted.has(command)) continue;
93
+ this.setHandler(command, null);
94
+ this.registered.delete(command);
95
+ }
96
+ for (const command of wanted) {
97
+ if (this.registered.has(command)) continue;
98
+ this.setHandler(command, (details) => this.deliver(this.received(command, details)));
99
+ this.registered.add(command);
100
+ }
101
+ }
102
+
103
+ private setHandler(command: MediaSessionCommand, handler: ((details: MediaSessionActionDetails) => void) | null): void {
104
+ try {
105
+ this.host.setActionHandler(ACTIONS[command], handler);
106
+ } catch {
107
+ // An action this browser does not know (`seekto` on an older Safari): the button never shows.
108
+ }
109
+ }
110
+
111
+ private received(command: MediaSessionCommand, details: MediaSessionActionDetails): ReceivedCommand {
112
+ const received: ReceivedCommand = { command };
113
+ if (command === 'seekTo' && typeof details.seekTime === 'number') received.position = details.seekTime;
114
+ if (command === 'skipForward' || command === 'skipBackward') {
115
+ const preferred = command === 'skipForward' ? this.skip.forward : this.skip.backward;
116
+ const interval = typeof details.seekOffset === 'number' ? details.seekOffset : preferred;
117
+ if (interval !== undefined) received.interval = interval;
118
+ }
119
+ return received;
120
+ }
121
+
122
+ private deliver(command: ReceivedCommand): void {
123
+ const wake = this.waiting.shift();
124
+ if (wake) wake(command);
125
+ else this.queue.push(command);
126
+ }
127
+
128
+ private receive(): Promise<Record<string, unknown>> {
129
+ const queued = this.queue.shift();
130
+ if (queued) return Promise.resolve({ ...queued });
131
+ return new Promise((resolve) => this.waiting.push((command) => resolve({ ...command })));
132
+ }
133
+ }
134
+
135
+ export async function performMediaSession(params: MediaSessionRequestParams, bridge: MediaSessionBridge | undefined): Promise<Record<string, unknown>> {
136
+ if (!bridge) {
137
+ // No Media Session API: nothing to show, and no command will ever come.
138
+ if (params.action === 'update') return {};
139
+ throw new Error('unsupported: this browser has no Media Session');
140
+ }
141
+ return bridge.perform(params);
142
+ }
143
+
144
+ /** `navigator.mediaSession`, where the browser has it. */
145
+ export function browserMediaSessionHost(): MediaSessionHost | undefined {
146
+ if (typeof navigator === 'undefined' || !('mediaSession' in navigator) || typeof MediaMetadata === 'undefined') return undefined;
147
+ const session = navigator.mediaSession;
148
+ return {
149
+ setMetadata: (metadata) => {
150
+ session.metadata = metadata === null ? null : new MediaMetadata(metadata);
151
+ },
152
+ setPlaybackState: (state) => {
153
+ session.playbackState = state;
154
+ },
155
+ setPositionState: (state) => {
156
+ if (typeof session.setPositionState !== 'function') return;
157
+ if (state === null) session.setPositionState();
158
+ // A position past the duration, or a duration that is not a finite number, throws.
159
+ else if (Number.isFinite(state.duration) && state.duration >= 0) session.setPositionState({ ...state, position: Math.min(Math.max(state.position, 0), state.duration) });
160
+ },
161
+ setActionHandler: (action, handler) => session.setActionHandler(action, handler),
162
+ };
163
+ }
@@ -4,6 +4,13 @@
4
4
  * comes due; the page only answers for the permission and shows what it is
5
5
  * told to, with the browser's Notification API. Nothing is scheduled here, so
6
6
  * nothing fires once the page is closed.
7
+ *
8
+ * Remote notifications are Web Push ("Push notifications" there): the app's
9
+ * `registerForRemoteNotifications` becomes `pushSubscribe`, which registers
10
+ * the site's service worker (public/sw.js) and subscribes through it; the
11
+ * subscription is the app's device token. The worker shows each push and
12
+ * reports pushes and clicks to the page, which hands them to the app one
13
+ * `receive` request at a time.
7
14
  */
8
15
 
9
16
  import type { NotificationRequestParams } from './protocol';
@@ -11,11 +18,26 @@ import type { NotificationRequestParams } from './protocol';
11
18
  /** `NotificationPermission`, or no Notification API at all. */
12
19
  export type NotificationPermissionState = NotificationPermission | 'unsupported';
13
20
 
21
+ /** A push subscription as `PushSubscription.toJSON()` gives it: what a server sends to. */
22
+ export interface PushSubscriptionInfo {
23
+ endpoint: string;
24
+ keys: { p256dh: string; auth: string };
25
+ }
26
+
27
+ /** What the service worker reports: a push that arrived, or a click on a notification it showed. */
28
+ export type PushEvent = { type: 'push'; payload: unknown } | { type: 'click'; payload: unknown; action: string };
29
+
14
30
  export interface NotificationResult {
15
31
  /** `status` and `request`. */
16
32
  permission?: NotificationPermissionState;
17
33
  /** `show`: whether a notification appeared. */
18
34
  shown?: boolean;
35
+ /** `pushSubscribe`. */
36
+ subscription?: PushSubscriptionInfo;
37
+ /** `receive`: the event's fields. */
38
+ type?: PushEvent['type'];
39
+ payload?: unknown;
40
+ action?: string;
19
41
  }
20
42
 
21
43
  /** The browser's notification facilities; tests substitute them. */
@@ -28,6 +50,17 @@ export interface NotificationHost {
28
50
  close(tags: readonly string[]): void;
29
51
  /** `navigator.setAppBadge` (0 clears), where the browser has it. */
30
52
  setBadge?(count: number): Promise<void>;
53
+ /** Web Push through the service worker, where the browser has it. */
54
+ push?: PushHost;
55
+ }
56
+
57
+ /** The push service, as the service worker reaches it; tests substitute it. */
58
+ export interface PushHost {
59
+ /** Subscribes with the key (registering the worker first); the subscription. */
60
+ subscribe(applicationServerKey: string): Promise<PushSubscriptionInfo>;
61
+ unsubscribe(): Promise<void>;
62
+ /** Resolves with the next push or click the worker reported. */
63
+ receive(): Promise<PushEvent>;
31
64
  }
32
65
 
33
66
  export async function performNotification(params: NotificationRequestParams, host: NotificationHost | undefined): Promise<NotificationResult> {
@@ -55,13 +88,29 @@ export async function performNotification(params: NotificationRequestParams, hos
55
88
  case 'badge':
56
89
  await host?.setBadge?.(params.badge ?? 0).catch(() => undefined);
57
90
  return {};
91
+ case 'pushSubscribe': {
92
+ if (!host?.push) throw new Error('unsupported: this browser has no push service');
93
+ if (!params.applicationServerKey) throw new Error('push: no application server key (webPushKey in swiftbrowser.json)');
94
+ // A subscription needs the permission: asked for here when it has not been.
95
+ const granted = permission === 'default' ? await host.requestPermission() : permission;
96
+ if (granted !== 'granted') throw new Error('denied: notifications are not allowed');
97
+ return { subscription: await host.push.subscribe(params.applicationServerKey) };
98
+ }
99
+ case 'pushUnsubscribe':
100
+ await host?.push?.unsubscribe();
101
+ return {};
102
+ case 'receive': {
103
+ if (!host?.push) throw new Error('unsupported: this browser has no push service');
104
+ const event = await host.push.receive();
105
+ return event.type === 'click' ? { type: 'click', payload: event.payload, action: event.action } : { type: 'push', payload: event.payload };
106
+ }
58
107
  default:
59
108
  throw new Error(`unsupported: notification action ${String((params as { action: unknown }).action)}`);
60
109
  }
61
110
  }
62
111
 
63
112
  /** The Notification API, or nothing where the browser has none (an iPhone tab not on the Home Screen). */
64
- export function browserNotificationHost(): NotificationHost | undefined {
113
+ export function browserNotificationHost(push?: PushHost): NotificationHost | undefined {
65
114
  if (typeof window === 'undefined' || typeof Notification === 'undefined') return undefined;
66
115
  const open = new Map<string, Notification>();
67
116
  const badgeNavigator = navigator as Navigator & { setAppBadge?: (n?: number) => Promise<void>; clearAppBadge?: () => Promise<void> };
@@ -105,5 +154,89 @@ export function browserNotificationHost(): NotificationHost | undefined {
105
154
  count > 0 ? badgeNavigator.setAppBadge!(count) : (badgeNavigator.clearAppBadge?.() ?? Promise.resolve()),
106
155
  }
107
156
  : {}),
157
+ ...(push ? { push } : {}),
158
+ };
159
+ }
160
+
161
+ /** The message types the service worker (public/sw.js) posts to the page. */
162
+ export const PUSH_MESSAGE = 'swiftbrowser:push';
163
+ export const CLICK_MESSAGE = 'swiftbrowser:notificationclick';
164
+
165
+ /** The push event a service worker message carries, or null when `data` is not one. */
166
+ export function pushEventOf(data: unknown): PushEvent | null {
167
+ if (typeof data !== 'object' || data === null) return null;
168
+ const message = data as { type?: unknown; payload?: unknown; action?: unknown };
169
+ if (message.type === PUSH_MESSAGE) return { type: 'push', payload: message.payload ?? {} };
170
+ if (message.type === CLICK_MESSAGE) return { type: 'click', payload: message.payload ?? {}, action: typeof message.action === 'string' ? message.action : '' };
171
+ return null;
172
+ }
173
+
174
+ /** A VAPID public key (base64url, as servers print it) as the bytes `subscribe` takes. */
175
+ export function applicationServerKeyBytes(key: string): Uint8Array<ArrayBuffer> {
176
+ const base64 = key.replace(/-/g, '+').replace(/_/g, '/');
177
+ const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
178
+ const binary = atob(padded);
179
+ const bytes = new Uint8Array(new ArrayBuffer(binary.length));
180
+ for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
181
+ return bytes;
182
+ }
183
+
184
+ /** The subscription's `toJSON()`, checked for the fields a server needs. */
185
+ function subscriptionInfo(subscription: PushSubscription): PushSubscriptionInfo {
186
+ const json = subscription.toJSON();
187
+ const keys = json.keys ?? {};
188
+ if (!json.endpoint || typeof keys['p256dh'] !== 'string' || typeof keys['auth'] !== 'string') throw new Error('push: the subscription has no keys');
189
+ return { endpoint: json.endpoint, keys: { p256dh: keys['p256dh'], auth: keys['auth'] } };
190
+ }
191
+
192
+ /**
193
+ * Web Push through the service worker at `workerURL` with `scope` (the
194
+ * site's base), or nothing where the browser has no push service (Safari
195
+ * before 16.4; an iPhone tab not on the Home Screen). Listens for the
196
+ * worker's messages from the start: a click that opened the page is posted
197
+ * before the app asks for it.
198
+ */
199
+ export function browserPushHost(workerURL: string, scope: string): PushHost | undefined {
200
+ if (typeof navigator === 'undefined' || !('serviceWorker' in navigator) || !navigator.serviceWorker || typeof PushManager === 'undefined') return undefined;
201
+ const container = navigator.serviceWorker;
202
+ const queue: PushEvent[] = [];
203
+ const waiting: ((event: PushEvent) => void)[] = [];
204
+ container.addEventListener('message', (message: MessageEvent) => {
205
+ const event = pushEventOf(message.data);
206
+ if (!event) return;
207
+ const wake = waiting.shift();
208
+ if (wake) wake(event);
209
+ else queue.push(event);
210
+ });
211
+ const registration = async (): Promise<ServiceWorkerRegistration> => {
212
+ const existing = await container.getRegistration(scope);
213
+ if (existing?.active && existing.scope === new URL(scope, location.href).href) return existing;
214
+ return container.register(workerURL, { scope });
215
+ };
216
+ return {
217
+ subscribe: async (key) => {
218
+ const worker = await registration();
219
+ await container.ready;
220
+ const options: PushSubscriptionOptionsInit = { userVisibleOnly: true, applicationServerKey: applicationServerKeyBytes(key) };
221
+ const current = await worker.pushManager.getSubscription();
222
+ try {
223
+ return subscriptionInfo(current ?? (await worker.pushManager.subscribe(options)));
224
+ } catch (error) {
225
+ // A subscription under another key stands in the way: replaced.
226
+ if (!current) throw error;
227
+ await current.unsubscribe().catch(() => undefined);
228
+ return subscriptionInfo(await worker.pushManager.subscribe(options));
229
+ }
230
+ },
231
+ unsubscribe: async () => {
232
+ const worker = await container.getRegistration(scope);
233
+ const current = await worker?.pushManager.getSubscription();
234
+ await current?.unsubscribe();
235
+ },
236
+ receive: () => {
237
+ const queued = queue.shift();
238
+ if (queued) return Promise.resolve(queued);
239
+ return new Promise((resolve) => waiting.push(resolve));
240
+ },
108
241
  };
109
242
  }
package/src/protocol.ts CHANGED
@@ -1076,7 +1076,7 @@ export interface DefaultsOp {
1076
1076
  export interface RequestOp {
1077
1077
  op: 'request';
1078
1078
  id: number;
1079
- kind: 'fetch' | 'image' | 'share' | 'pickFiles' | 'notification' | 'scanner' | 'websocket' | 'openURL' | 'webAuth' | 'media' | 'download' | 'clipboard' | 'translate' | 'pickColor';
1079
+ kind: 'fetch' | 'image' | 'share' | 'pickFiles' | 'notification' | 'scanner' | 'websocket' | 'openURL' | 'webAuth' | 'media' | 'download' | 'clipboard' | 'translate' | 'pickColor' | 'mediaSession';
1080
1080
  params:
1081
1081
  | FetchRequestParams
1082
1082
  | ImageRequestParams
@@ -1091,7 +1091,8 @@ export interface RequestOp {
1091
1091
  | DownloadRequestParams
1092
1092
  | ClipboardRequestParams
1093
1093
  | TranslateRequestParams
1094
- | PickColorRequestParams;
1094
+ | PickColorRequestParams
1095
+ | MediaSessionRequestParams;
1095
1096
  }
1096
1097
  /**
1097
1098
  * `translate` (Translation; src/translation.ts): `availability` of a pair
@@ -1228,7 +1229,7 @@ export interface PickFilesRequestParams {
1228
1229
  * response value is a `NotificationResult`.
1229
1230
  */
1230
1231
  export interface NotificationRequestParams {
1231
- action: 'status' | 'request' | 'show' | 'close' | 'badge';
1232
+ action: 'status' | 'request' | 'show' | 'close' | 'badge' | 'pushSubscribe' | 'pushUnsubscribe' | 'receive';
1232
1233
  /** `show`: the request's identifier, the notification's `tag`. */
1233
1234
  id?: string;
1234
1235
  title?: string;
@@ -1239,6 +1240,8 @@ export interface NotificationRequestParams {
1239
1240
  badge?: number;
1240
1241
  /** `close`: the identifiers to close. */
1241
1242
  ids?: string[];
1243
+ /** `pushSubscribe`: the Web Push application server key (a VAPID public key, base64url). */
1244
+ applicationServerKey?: string;
1242
1245
  }
1243
1246
 
1244
1247
  /**
@@ -2127,6 +2130,8 @@ export interface SlideEvent {
2127
2130
  type: 'slide';
2128
2131
  id: ElementId;
2129
2132
  value: number;
2133
+ /** `onEditingChanged`: true with the first value of a drag, false with its last; absent for the values between. */
2134
+ editing?: boolean;
2130
2135
  }
2131
2136
  /** A `datepicker` changed: seconds since 1970 (an instant; the zone only shaped the reading). */
2132
2137
  export interface DateEvent {
@@ -2556,10 +2561,34 @@ export interface VideoEvent {
2556
2561
  }
2557
2562
 
2558
2563
  /** `media` requests (src/media.ts): AVFoundation's files and probes, QuickLook's previews. */
2564
+ /** A remote command MediaPlayer's `MPRemoteCommandCenter` handles (docs/ops-protocol.md, "Now playing"). */
2565
+ export type MediaSessionCommand = 'play' | 'pause' | 'stop' | 'nextTrack' | 'previousTrack' | 'seekTo' | 'skipForward' | 'skipBackward';
2566
+
2567
+ /**
2568
+ * `mediaSession` (src/mediaSession.ts): MediaPlayer's now-playing info and
2569
+ * remote commands over the browser's Media Session API. `update` sets what
2570
+ * the session shows and which commands the app handles (a field left out
2571
+ * stays as it was); `receive` answers the next command the viewer gave.
2572
+ */
2573
+ export interface MediaSessionRequestParams {
2574
+ action: 'update' | 'receive';
2575
+ /** `update`: what plays, for the lock screen and the media controls (`artwork` a URL); null clears it. */
2576
+ metadata?: { title?: string; artist?: string; album?: string; artwork?: string } | null;
2577
+ playbackState?: 'none' | 'playing' | 'paused';
2578
+ /** `update`: the position in seconds, which the browser extrapolates at `rate`; null clears it. */
2579
+ position?: { duration: number; position: number; rate: number } | null;
2580
+ /** `update`: the commands the app handles; the others get no button. */
2581
+ commands?: MediaSessionCommand[];
2582
+ /** `update`: the app's preferred skip intervals in seconds, answered with a skip the system gives no offset for. */
2583
+ skip?: { forward?: number; backward?: number };
2584
+ }
2585
+
2559
2586
  export interface MediaRequestParams {
2560
2587
  action: 'file' | 'probe' | 'release' | 'open';
2561
2588
  url?: string;
2562
2589
  name?: string;
2563
2590
  type?: string;
2564
2591
  data?: string;
2592
+ /** `file`: a resource's path inside the app's bundle, in place of `data`: the page answers the URL it serves the resource at. */
2593
+ path?: string;
2565
2594
  }
package/src/renderer.ts CHANGED
@@ -786,7 +786,7 @@ export class Renderer {
786
786
  const id = Number(slider.dataset[ID_ATTR]);
787
787
  if (!track || !Number.isInteger(id)) return;
788
788
  event.preventDefault();
789
- const drag: SliderDrag = { slider, track, id, pointerId: event.pointerId, pending: null, sent: null, frame: null };
789
+ const drag: SliderDrag = { slider, track, id, pointerId: event.pointerId, pending: null, sent: null, frame: null, starting: true };
790
790
  this.sliderDrag = drag;
791
791
  window.addEventListener('pointermove', this.onSliderMove);
792
792
  window.addEventListener('pointerup', this.onSliderUp);
@@ -817,9 +817,11 @@ export class Renderer {
817
817
  }
818
818
 
819
819
  private flushSlide(drag: SliderDrag): void {
820
- if (drag.pending === null || drag.pending === drag.sent) return;
820
+ if (drag.pending === null || (drag.pending === drag.sent && !drag.starting)) return;
821
821
  drag.sent = drag.pending;
822
- this.onEvent({ type: 'slide', id: drag.id, value: drag.pending });
822
+ // The drag's first value says editing began (`onEditingChanged(true)`); the last, on release, that it ended.
823
+ this.onEvent(drag.starting ? { type: 'slide', id: drag.id, value: drag.pending, editing: true } : { type: 'slide', id: drag.id, value: drag.pending });
824
+ drag.starting = false;
823
825
  }
824
826
 
825
827
  private readonly onSliderMove = (event: PointerEvent): void => {
@@ -838,6 +840,7 @@ export class Renderer {
838
840
  if (drag.frame !== null) window.cancelAnimationFrame(drag.frame);
839
841
  drag.frame = null;
840
842
  this.flushSlide(drag);
843
+ if (drag.sent !== null) this.onEvent({ type: 'slide', id: drag.id, value: drag.sent, editing: false });
841
844
  // The click that follows is the slide itself, not a tap on an enclosing control.
842
845
  this.swallowNextClick();
843
846
  };
@@ -3351,6 +3354,8 @@ interface SliderDrag {
3351
3354
  /** The last value sent, so an unchanged frame sends nothing. */
3352
3355
  sent: number | null;
3353
3356
  frame: number | null;
3357
+ /** Whether the drag's first `slide` (with `editing: true`) is still to be sent. */
3358
+ starting: boolean;
3354
3359
  }
3355
3360
 
3356
3361
  function applyAlert(el: HTMLElement, props: AlertProps): void {
package/src/vite-env.d.ts CHANGED
@@ -18,6 +18,8 @@ interface SBLandingApp {
18
18
  urlSchemes?: string[];
19
19
  /** A CORS proxy (packages/cors-proxy) through which a refused cross-origin GET is retried (src/corsProxy.ts). */
20
20
  corsProxy?: string;
21
+ /** The Web Push application server key the app subscribes with (`SB_WEB_PUSH_KEY`, src/notifications.ts). */
22
+ webPushKey?: string;
21
23
  /** Hosts the app's requests may not reach (src/blockedHosts.ts). */
22
24
  blockedHosts?: string[];
23
25
  }