@n1h/sweetview 0.0.0-stage → 0.1.0

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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSE-0BSD +12 -0
  3. package/README.md +268 -3
  4. package/docs/README.md +26 -0
  5. package/docs/app.md +523 -0
  6. package/docs/build.md +169 -0
  7. package/docs/ipc-ws.md +202 -0
  8. package/docs/ipc.md +323 -0
  9. package/docs/native.md +1066 -0
  10. package/docs/page.md +71 -0
  11. package/docs/settings-store.md +125 -0
  12. package/docs/webview-library.md +104 -0
  13. package/docs/window-chrome.md +221 -0
  14. package/docs/worker.md +72 -0
  15. package/file-imports.d.ts +19 -0
  16. package/package.json +66 -4
  17. package/src/app.ts +350 -0
  18. package/src/build.ts +118 -0
  19. package/src/html.ts +71 -0
  20. package/src/index.ts +8 -0
  21. package/src/ipc/bridge.ts +41 -0
  22. package/src/ipc/frame.ts +16 -0
  23. package/src/ipc/index.ts +16 -0
  24. package/src/ipc/memory.ts +33 -0
  25. package/src/ipc/service.ts +75 -0
  26. package/src/ipc-ws/client.ts +111 -0
  27. package/src/ipc-ws/index.ts +5 -0
  28. package/src/ipc-ws/server.ts +133 -0
  29. package/src/make-app.ts +93 -0
  30. package/src/messages.ts +16 -0
  31. package/src/native/app-icon.ts +23 -0
  32. package/src/native/app-menu.ts +18 -0
  33. package/src/native/darwin/app-icon.ts +10 -0
  34. package/src/native/darwin/app-menu.ts +88 -0
  35. package/src/native/darwin/folder-picker.ts +29 -0
  36. package/src/native/darwin/image.ts +47 -0
  37. package/src/native/darwin/menu.ts +89 -0
  38. package/src/native/darwin/notifier.ts +95 -0
  39. package/src/native/darwin/objc.ts +246 -0
  40. package/src/native/darwin/overlay.ts +352 -0
  41. package/src/native/darwin/tray.ts +129 -0
  42. package/src/native/darwin/window.ts +64 -0
  43. package/src/native/folder-picker.ts +15 -0
  44. package/src/native/hit-regions.ts +162 -0
  45. package/src/native/icon.ts +530 -0
  46. package/src/native/index.ts +38 -0
  47. package/src/native/menu.ts +43 -0
  48. package/src/native/notifications.ts +40 -0
  49. package/src/native/overlay.ts +182 -0
  50. package/src/native/platform.ts +25 -0
  51. package/src/native/tray.ts +41 -0
  52. package/src/native/win32/api.ts +234 -0
  53. package/src/native/win32/app-icon.ts +5 -0
  54. package/src/native/win32/app-menu.ts +4 -0
  55. package/src/native/win32/folder-picker.ts +121 -0
  56. package/src/native/win32/icon.ts +85 -0
  57. package/src/native/win32/menu.ts +107 -0
  58. package/src/native/win32/message-window.ts +72 -0
  59. package/src/native/win32/notifier.ts +28 -0
  60. package/src/native/win32/overlay.ts +499 -0
  61. package/src/native/win32/tray.ts +228 -0
  62. package/src/native/win32/window.ts +77 -0
  63. package/src/native/window.ts +40 -0
  64. package/src/page.ts +48 -0
  65. package/src/ring.ts +110 -0
  66. package/src/settings-store.ts +101 -0
  67. package/src/webview-lib.ts +60 -0
  68. package/src/window-chrome.ts +118 -0
  69. package/src/worker.ts +72 -0
package/docs/app.md ADDED
@@ -0,0 +1,523 @@
1
+ # @n1h/sweetview
2
+
3
+ The root module opens a webview window, serves the page to it, and connects
4
+ the page to a worker that hosts the application's `Service`. It runs on the
5
+ UI thread: the main thread of the Bun process, which `run()` hands to the OS
6
+ event loop. Install and the overview are in the [package README](../README.md).
7
+
8
+ ```ts
9
+ import { createWebviewApp, type Asset, type UiMethods, type WebviewApp, type WebviewAppOptions, assetMime, indexHtml, indexHtmlText, type IndexHtmlOptions, windowChromeMethods, type WindowChromeInfo, type WindowChromeKind, type WindowChromeMethodTable, type WindowChromeMethods, type WindowChromeOptions, Ring, SizeHint, WEBVIEW_VERSION, Webview } from "@n1h/sweetview";
10
+ ```
11
+
12
+ ## Reference
13
+
14
+ ### createWebviewApp
15
+
16
+ ```ts
17
+ export async function createWebviewApp<M extends MethodMap, E extends EventMap>(options: WebviewAppOptions<M>): Promise<WebviewApp<M, E>>
18
+ ```
19
+
20
+ Opens a window, serves the page in it and connects the page to the worker
21
+ that hosts the application's `Service`. `M` and `E` are the method and event
22
+ maps from the [ipc reference](ipc.md). The window loop does not run until
23
+ `run()` is called on the result.
24
+
25
+ What the call does, in order:
26
+
27
+ 1. Refuses any platform other than Windows and macOS, before anything is
28
+ created.
29
+ 2. Resolves the webview library path, from `WEBVIEW_PATH` or the peer
30
+ dependency.
31
+ 3. Resolves the page. With `page`: bundles the HTML import under `bun run`,
32
+ or takes the embedded outputs inside a compiled executable, and reads the
33
+ `<title>` of the index. Without an index in `assets`, generates one with
34
+ `indexHtml`.
35
+ 4. Creates the webview, in the caller's window when `window` is given, and
36
+ sets the title.
37
+ 5. Sets the background to transparent when `transparent` is set.
38
+ 6. Allocates the ring and the drain callback.
39
+ 7. Starts the worker and posts it the ring, the webview handle, the drain
40
+ callback pointer and the library path.
41
+ 8. Binds `__sweetview_rpc` into the page, with `uiMethods` consulted first.
42
+ 9. Registers every asset with the library and navigates to the index.
43
+
44
+ When a step after the webview exists throws, the webview is destroyed and
45
+ the error rethrown. When the step is one after the worker started (binding,
46
+ asset registration, navigation), the worker is terminated and the drain
47
+ callback released first: worker, drain callback, webview.
48
+
49
+ #### Options
50
+
51
+ ```ts
52
+ export type WebviewAppOptions<M extends MethodMap> = {
53
+ page?: HTMLBundle;
54
+ title?: string;
55
+ assets?: Record<string, Asset>;
56
+ index?: string;
57
+ worker?: URL | string;
58
+ width?: number;
59
+ height?: number;
60
+ debug?: boolean;
61
+ transparent?: boolean;
62
+ window?: WindowHandle;
63
+ resizable?: boolean;
64
+ uiMethods?: UiMethods<M>;
65
+ ringBytes?: number;
66
+ };
67
+ ```
68
+
69
+ | Field | Type | Default | Meaning |
70
+ |---|---|---|---|
71
+ | `page` | `HTMLBundle` | none | An HTML import, `import index from "./index.html"`. Under `bun run` the host bundles it at start-up with `Bun.build` for the browser target, minified. Inside a `bun build --compile` executable the outputs are embedded and listed on the import's `files`; each is read with `Bun.file`. Every output is served at the page's origin by its file name. The output with the `.html` extension is the index. |
72
+ | `title` | `string` | the `<title>` of the index, else the file name of `Bun.main` without its extension | The window title. The `<title>` is read only when `page` is given, with `&amp;`, `&lt;`, `&gt;`, `&quot;` and `&#39;` decoded and surrounding whitespace trimmed; an empty `<title>` falls through to the file name. |
73
+ | `assets` | `Record<string, Asset>` | `{}` | Files served at the page's origin by asset path, in addition to the page's outputs: models, images, data the page fetches by relative URL. Without `page`, the whole page goes here, by convention `main.js` and `main.css`. A page output replaces an asset of the same path. |
74
+ | `index` | `string` | the page's HTML output, else `"index.html"` | The asset path the webview navigates to. When it is absent from `assets` and the page's outputs, `indexHtml({ title, css })` generates it, with `css` set to `main.css` only when that asset is present. |
75
+ | `worker` | `URL \| string` | none | The worker entry, which must call `hostService` from the [worker reference](worker.md). A `URL` is used as given. A string with a scheme is parsed as a URL. A string without one is resolved against `pathToFileURL(Bun.main)`, so `"./worker.ts"` names the file beside the UI-thread entry. Pass that file to `bun build --compile` as the second entry point; the same string then resolves inside the executable. Without `worker`, a page call to a method not in `uiMethods` is rejected. |
76
+ | `width` | `number` | `1100`, or `320` with `window` | Client width. |
77
+ | `height` | `number` | `800`, or `240` with `window` | Client height. |
78
+ | `debug` | `boolean` | `false` | Enables devtools and the context menu. |
79
+ | `transparent` | `boolean` | `false` | Sets the browser's background colour to alpha 0, so a page with `background: transparent` shows what is behind the window. Pair it with `createOverlayWindow({ transparent: true })` from the [native reference](native.md). With a library that lacks `webview_set_background_color`, a warning is logged with `console.warn` and the page stays opaque. |
80
+ | `window` | `WindowHandle` | none | Hosts the browser in a window created by the caller, such as `createOverlayWindow` from the [native reference](native.md), instead of a window the library creates. The caller shows the window and ends the run loop when the window is destroyed. On Windows, COM must be initialised as an STA on this thread; `createOverlayWindow` does that. |
81
+ | `resizable` | `boolean` | `false` | With `window`, keeps the window resizable. Ignored without `window`. |
82
+ | `uiMethods` | `UiMethods<M>` | `{}` | Methods answered on the UI thread. A method listed here never reaches the worker. |
83
+ | `ringBytes` | `number` | `1048576` | Capacity of the ring that carries worker frames to the UI thread. It must be an integer power of two between 16 and 1073741824. A frame longer than `ringBytes - 4` bytes cannot be sent. The worker blocks while the ring is full. |
84
+
85
+ The size hint passed to the library follows from `window` and `resizable`:
86
+
87
+ | `window` | `resizable` | Size hint |
88
+ |---|---|---|
89
+ | not given | any | `SizeHint.NONE` |
90
+ | given | `false` | `SizeHint.FIXED` |
91
+ | given | `true` | `SizeHint.NONE` |
92
+
93
+ Platforms: with `window`, Windows applies `width` and `height` to the window
94
+ as DIPs. For an overlay, pass the window's size divided by its scale. For a
95
+ framed window, they are the client area and the library sizes the frame
96
+ around it. macOS ignores them.
97
+
98
+ Platforms: `transparent` works on Windows and macOS with the library from
99
+ `@n1h/sweetview-webview-bun`. See the [webview library](webview-library.md).
100
+
101
+ #### The returned app
102
+
103
+ ```ts
104
+ export type WebviewApp<M extends MethodMap, E extends EventMap> = {
105
+ readonly webview: Webview;
106
+ readonly window: NativeWindow;
107
+ readonly windowHandle: WindowHandle;
108
+ emit<N extends keyof E & string>(name: N, payload: E[N]): void;
109
+ call<K extends keyof M & string>(method: K, ...params: M[K][0] extends void ? [] : [M[K][0]]): void;
110
+ onEvent(fn: (event: AnyEvent<E>) => void): () => void;
111
+ run(): void;
112
+ quit(): void;
113
+ };
114
+ ```
115
+
116
+ | Field | Type | Meaning |
117
+ |---|---|---|
118
+ | `webview` | `Webview` | The underlying webview instance. |
119
+ | `window` | `NativeWindow` | Visibility, show, hide, toggle, the window icon and close handling for `windowHandle`, from the [native reference](native.md). |
120
+ | `windowHandle` | `WindowHandle` | `options.window` when given. Otherwise the window the library created: an HWND on Windows, an `NSWindow*` on macOS. |
121
+ | `emit(name, payload)` | `void` | Delivers an event to the page by evaluating `__sweetview_emit(name, payload)`. A `payload` of `undefined` is sent as `null`. Listeners registered with `onEvent` do not see it. |
122
+ | `call(method, ...params)` | `void` | Posts a call with no reply to the worker, with `params` sent as `null` when omitted. `uiMethods` are not consulted. Whatever the `Service` emits in response arrives as events. Without a worker, it logs `sweetview: <method>: this window has no worker` with `console.error` and sends nothing. |
123
+ | `onEvent(fn)` | `() => void` | Registers `fn` for every event the worker sends, called before the page receives it. Returns a function that removes `fn`. |
124
+ | `run()` | `void` | Runs the OS loop. Returns when `quit()` is called or, without `window`, when the window closes. With `window`, the caller ends the loop; on Windows `createOverlayWindow` ends it when its window is destroyed. This app's worker is then terminated, the drain callback released and the webview destroyed, in that order. |
125
+ | `quit()` | `void` | Calls `webview_terminate`, which makes `run()` return. |
126
+
127
+ Thread: every member runs on the UI thread. `run()` blocks that thread and
128
+ Bun's event loop until it returns, so promises, timers and other asynchronous
129
+ callbacks on the UI thread do not run in the meantime.
130
+
131
+ Code on the UI thread after `run()` starts runs only inside callbacks from
132
+ the OS loop: `uiMethods`, `onEvent` listeners and native callbacks. After
133
+ `run()` returns, `webview`, `emit` and `quit` must not be used.
134
+
135
+ One process has one OS loop. With several windows, call `createWebviewApp`
136
+ once per window and `run()` on one of them. The other windows live in the
137
+ same loop. `run()` terminates the worker and destroys the webview of the app
138
+ it was called on only.
139
+
140
+ #### Errors
141
+
142
+ Throws:
143
+
144
+ - `Error` `sweetview: <platform> is not supported (win32 and darwin are): the native module has no backend for it and the webview library does not serve assets there` on any platform other than Windows and macOS, before anything is created.
145
+ - `Error` `sweetview: the page bundle has no HTML output` when `page` yields no output with the `.html` extension.
146
+ - `Error` `sweetview: webview_create failed for the given window (is COM initialised as an STA on this thread? createOverlayWindow in @n1h/sweetview/native does that)` when `webview_create` fails with `window`.
147
+ - `Error` `sweetview: webview_create failed (is the WebView2 runtime installed?)` when `webview_create` fails without `window`.
148
+ - `Error` `sweetview: webview_set_background_color failed (<code>)` when `transparent` is set and the call returns a non-zero code.
149
+ - `RangeError` `ring capacity must be a power of two between 16 and 2^30, got <value>` when `ringBytes` is out of range.
150
+ - `Error` `sweetview: this webview library has no webview_set_asset; it needs the library @n1h/sweetview-webview-bun ships (is WEBVIEW_PATH pointing at a stock one?)` when the library lacks the symbol. The page cannot load without it.
151
+ - `Error` `sweetview: webview_set_asset(<path>) failed (<code>)` when the library rejects an asset.
152
+ - The read error when a string or `Blob` asset, or the index read for the title, cannot be read, such as a missing file.
153
+ - The build error from `Bun.build` when the page fails to bundle under `bun run`.
154
+ - `TypeError` from `new URL` when `worker` is a string with a scheme that is not a valid URL.
155
+ - The import error when the webview library file is missing from `@n1h/sweetview-webview-bun`.
156
+
157
+ Platforms: Windows and macOS. The page is served at the origin listed under
158
+ [Page loading](#page-loading).
159
+
160
+ Thread: call on the UI thread, before `run()`. Every asynchronous step of the
161
+ setup happens inside this call. Tray icons, notifications, dialogs and other
162
+ windows from the [native reference](native.md) are created on the same
163
+ thread, between this call and `run()`.
164
+
165
+ ### Asset
166
+
167
+ ```ts
168
+ export type Asset = string | Uint8Array | Blob;
169
+ ```
170
+
171
+ The bytes served at one path of the page's origin. The Content-Type comes
172
+ from the path's extension through `assetMime`.
173
+
174
+ | Variant | Served as |
175
+ |---|---|
176
+ | `string` | A file path. The file is read with `Bun.file` during `createWebviewApp`. A path imported `with { type: "file" }` is embedded by `bun build --compile`, so the import's value is the usual asset. |
177
+ | `Uint8Array` | The bytes as given. |
178
+ | `Blob` | The blob's bytes, read during `createWebviewApp`. `Bun.file(path)`, the outputs of `Bun.build` and the value `indexHtml` returns are Blobs. Inline text is `new Blob([text])`. |
179
+
180
+ ### UiMethods
181
+
182
+ ```ts
183
+ export type UiMethods<M extends MethodMap> = { [K in keyof M & string]?: (params: M[K][0]) => M[K][1] };
184
+ ```
185
+
186
+ Methods of `M` answered on the UI thread instead of by the worker. Native
187
+ dialogs and window state belong here.
188
+
189
+ | Field | Type | Meaning |
190
+ |---|---|---|
191
+ | any method name of `M` | `(params: M[K][0]) => M[K][1]` | Called with the page's `params`. The return value is sent to the page as JSON, with `undefined` sent as `null`. A thrown error rejects the page's call with the error's message. |
192
+
193
+ Thread: runs on the UI thread, inside the OS loop, and must return
194
+ synchronously. A method that returns a Promise is not awaited; the page's
195
+ call rejects with `<method>: a UI-thread method must return synchronously;
196
+ this one returned a Promise`.
197
+
198
+ ### WebviewApp
199
+
200
+ The window `createWebviewApp` returns; documented under
201
+ [the returned app](#the-returned-app).
202
+
203
+ ### WebviewAppOptions
204
+
205
+ The options of `createWebviewApp`; documented under [Options](#options).
206
+
207
+ ### assetMime
208
+
209
+ ```ts
210
+ export const assetMime = (path: string): string
211
+ ```
212
+
213
+ Returns the Content-Type for an asset path, from the text after the path's
214
+ last `.`, compared in lower case. A path without `.` is compared whole.
215
+
216
+ | Extension | Content-Type |
217
+ |---|---|
218
+ | `html` | `text/html; charset=utf-8` |
219
+ | `js`, `mjs` | `text/javascript; charset=utf-8` |
220
+ | `css` | `text/css; charset=utf-8` |
221
+ | `json`, `map` | `application/json` |
222
+ | `txt` | `text/plain; charset=utf-8` |
223
+ | `svg` | `image/svg+xml` |
224
+ | `png` | `image/png` |
225
+ | `jpg`, `jpeg` | `image/jpeg` |
226
+ | `gif` | `image/gif` |
227
+ | `webp` | `image/webp` |
228
+ | `ico` | `image/x-icon` |
229
+ | `woff` | `font/woff` |
230
+ | `woff2` | `font/woff2` |
231
+ | `ttf` | `font/ttf` |
232
+ | `wasm` | `application/wasm` |
233
+ | `glb` | `model/gltf-binary` |
234
+ | `gltf` | `model/gltf+json` |
235
+ | `mp3` | `audio/mpeg` |
236
+ | `ogg` | `audio/ogg` |
237
+ | `wav` | `audio/wav` |
238
+ | anything else | `application/octet-stream` |
239
+
240
+ Returns: the Content-Type string.
241
+
242
+ ### indexHtml
243
+
244
+ ```ts
245
+ export function indexHtml({ title, js = "main.js", css = "main.css", head = "", body = '<div id="root"></div>' }: IndexHtmlOptions): Blob
246
+ ```
247
+
248
+ Builds an index document that links one stylesheet and loads one module
249
+ script, both by asset path, as a `Blob` of type `text/html; charset=utf-8`,
250
+ ready for the asset map. Nothing is inlined, so the document stays small
251
+ whatever the bundle's size.
252
+
253
+ | Field | Type | Default | Meaning |
254
+ |---|---|---|---|
255
+ | options | `IndexHtmlOptions` | required | See `IndexHtmlOptions` below. |
256
+
257
+ Returns: an HTML5 document with `lang="en"`, a UTF-8 charset and a
258
+ device-width viewport. `title`, `js` and `css` are HTML-escaped (`&`, `<`,
259
+ `>`, `"`); `head` and `body` are inserted as given. The module script is the
260
+ last element of `<body>`.
261
+
262
+ ### indexHtmlText
263
+
264
+ ```ts
265
+ export function indexHtmlText({ title, js = "main.js", css = "main.css", head = "", body = '<div id="root"></div>' }: IndexHtmlOptions): string
266
+ ```
267
+
268
+ The same document as `indexHtml` builds, returned as a string instead of a
269
+ `Blob`.
270
+
271
+ | Field | Type | Default | Meaning |
272
+ |---|---|---|---|
273
+ | options | `IndexHtmlOptions` | required | See `IndexHtmlOptions` below. |
274
+
275
+ Returns: the document as a string.
276
+
277
+ ### IndexHtmlOptions
278
+
279
+ ```ts
280
+ export type IndexHtmlOptions = {
281
+ title: string;
282
+ js?: string;
283
+ css?: string;
284
+ head?: string;
285
+ body?: string;
286
+ };
287
+ ```
288
+
289
+ | Field | Type | Default | Meaning |
290
+ |---|---|---|---|
291
+ | `title` | `string` | required | The document title. |
292
+ | `js` | `string` | `"main.js"` | Asset path of the page bundle, loaded as a module. |
293
+ | `css` | `string` | `"main.css"` | Asset path of the stylesheet. `""` omits the `<link>`. |
294
+ | `head` | `string` | `""` | Extra markup for `<head>`. |
295
+ | `body` | `string` | `'<div id="root"></div>'` | Markup for `<body>`, placed before the script. |
296
+
297
+ ### windowChromeMethods
298
+
299
+ ```ts
300
+ export function windowChromeMethods(window: Overlay, options: WindowChromeOptions = {}): WindowChromeMethodTable
301
+ ```
302
+
303
+ The same function as [`windowChromeMethods`](window-chrome.md#windowchromemethods-1)
304
+ in `@n1h/sweetview/window-chrome`. Its return type `WindowChromeMethodTable`
305
+ is exported from both modules.
306
+
307
+ ### WindowChromeInfo
308
+
309
+ ```ts
310
+ export type WindowChromeInfo = { chrome: WindowChromeKind; maximized: boolean };
311
+ ```
312
+
313
+ Documented in the [window-chrome reference](window-chrome.md#windowchromeinfo).
314
+
315
+ ### WindowChromeKind
316
+
317
+ ```ts
318
+ export type WindowChromeKind = "page" | "os";
319
+ ```
320
+
321
+ Documented in the [window-chrome reference](window-chrome.md#windowchromekind).
322
+
323
+ ### WindowChromeMethodTable
324
+
325
+ ```ts
326
+ export type WindowChromeMethodTable = { [K in keyof WindowChromeMethods]: (params: WindowChromeMethods[K][0]) => WindowChromeMethods[K][1] };
327
+ ```
328
+
329
+ The `uiMethods` table `windowChromeMethods` returns: one synchronous handler
330
+ per method of `WindowChromeMethods`. Documented in the
331
+ [window-chrome reference](window-chrome.md#windowchromemethodtable).
332
+
333
+ ### WindowChromeMethods
334
+
335
+ ```ts
336
+ export type WindowChromeMethods = {
337
+ "window.info": [void, WindowChromeInfo];
338
+ "window.drag": [void, void];
339
+ "window.resize": [ResizeEdge, void];
340
+ "window.minimize": [void, void];
341
+ "window.maximize": [void, void];
342
+ "window.close": [void, void];
343
+ };
344
+ ```
345
+
346
+ Documented in the [window-chrome reference](window-chrome.md#windowchromemethods).
347
+ `ResizeEdge` is documented in the [native reference](native.md).
348
+
349
+ ### WindowChromeOptions
350
+
351
+ ```ts
352
+ export type WindowChromeOptions = {
353
+ chrome?: WindowChromeKind;
354
+ close?: () => void;
355
+ };
356
+ ```
357
+
358
+ Documented in the [window-chrome reference](window-chrome.md#windowchromeoptions).
359
+
360
+ ### Ring
361
+
362
+ ```ts
363
+ export class Ring {
364
+ static create(capacity: number): SharedArrayBuffer;
365
+ constructor(sab: SharedArrayBuffer);
366
+ get capacity();
367
+ write(bytes: Uint8Array): void;
368
+ claimWakeup(): boolean;
369
+ acknowledgeWakeup(): void;
370
+ *drain(): Generator<Uint8Array>;
371
+ }
372
+ ```
373
+
374
+ A single-producer, single-consumer byte ring over a `SharedArrayBuffer`. The
375
+ worker writes frames and the UI thread drains them.
376
+
377
+ Each frame is stored as a 4-byte little-endian length followed by its bytes.
378
+ The buffer holds a 16-byte header before the data: the write cursor, the
379
+ read cursor and the wakeup flag.
380
+
381
+ | Field | Type | Meaning |
382
+ |---|---|---|
383
+ | `Ring.create(capacity)` | `SharedArrayBuffer` | Allocates a buffer of `16 + capacity` bytes. |
384
+ | `new Ring(sab)` | `Ring` | Wraps a buffer from `Ring.create`. Producer and consumer each wrap the same buffer. |
385
+ | `capacity` | `number` | Data bytes, excluding the header. |
386
+ | `write(bytes)` | `void` | Producer side. Appends one frame. Blocks with `Atomics.wait` while the ring lacks room for it. |
387
+ | `claimWakeup()` | `boolean` | Producer side. Sets the wakeup flag. Returns `true` when it was clear, so the caller sends one wakeup per burst. |
388
+ | `acknowledgeWakeup()` | `void` | Consumer side. Clears the wakeup flag. Call it first in the wakeup, before draining. |
389
+ | `drain()` | `Generator<Uint8Array>` | Consumer side. Yields every complete frame present when it starts, in write order, and frees each frame's space as it is yielded. Never blocks. |
390
+
391
+ Throws:
392
+
393
+ - `RangeError` `ring capacity must be a power of two between 16 and 2^30, got <value>` from `Ring.create` and the constructor.
394
+ - `RangeError` `frame of <n> bytes exceeds ring capacity <capacity>` from `write` when `4 + bytes.length` exceeds `capacity`.
395
+
396
+ Thread: `write` blocks the calling thread while the ring is full. Exactly one
397
+ thread writes and exactly one thread drains.
398
+
399
+ ### SizeHint
400
+
401
+ ```ts
402
+ export const enum SizeHint { NONE, MIN, MAX, FIXED }
403
+ ```
404
+
405
+ Re-exported from `@n1h/sweetview-webview-bun`. The size hint of a webview
406
+ window.
407
+
408
+ | Value | Number | Meaning |
409
+ |---|---|---|
410
+ | `NONE` | 0 | Width and height are the default size. |
411
+ | `MIN` | 1 | Width and height are the minimum bounds. |
412
+ | `MAX` | 2 | Width and height are the maximum bounds. |
413
+ | `FIXED` | 3 | The user cannot change the size. |
414
+
415
+ ### WEBVIEW_VERSION
416
+
417
+ ```ts
418
+ export const WEBVIEW_VERSION = "0.12.0";
419
+ ```
420
+
421
+ Re-exported from `@n1h/sweetview-webview-bun`: the webview library version
422
+ the bindings declare.
423
+
424
+ ### Webview
425
+
426
+ ```ts
427
+ export class Webview
428
+ ```
429
+
430
+ Re-exported from `@n1h/sweetview-webview-bun`, which documents it at
431
+ https://github.com/n0t-invented-here/sweetview-webview-bun. `WebviewApp.webview`
432
+ is an instance.
433
+
434
+ The root module imports `@n1h/sweetview-webview-bun` at load time, which
435
+ opens the library, so importing `@n1h/sweetview` fails when that optional
436
+ peer dependency is not installed.
437
+
438
+ Thread: UI thread. Call `run()` on the `WebviewApp`, not on its `webview`:
439
+ `Webview.run()` destroys the webview without terminating the worker.
440
+
441
+ ## Page loading
442
+
443
+ The page is never passed to `webview_set_html`. `createWebviewApp` hands each
444
+ asset to the library's `webview_set_asset`, with the path, `assetMime(path)`
445
+ and the bytes. The library copies the bytes. After the last asset it
446
+ navigates the webview to the origin followed by `/` and `index`.
447
+
448
+ Relative URLs in the page resolve against that origin, so scripts, styles,
449
+ `fetch` and images load from the page's outputs and `assets`.
450
+
451
+ | OS | Origin | Served by |
452
+ |---|---|---|
453
+ | Windows | `http://webview.localhost` | A `WebResourceRequested` filter. Nothing reaches the network. |
454
+ | macOS | `webview://localhost` | A `WKURLSchemeHandler` for the `webview` scheme. |
455
+ | Linux | `http://webview.localhost` | Nothing. `createWebviewApp` throws on Linux before any asset is registered. |
456
+
457
+ The webview library is the file at the `WEBVIEW_PATH` environment variable
458
+ when it is set. Otherwise it is the file `@n1h/sweetview-webview-bun` ships
459
+ for the platform:
460
+
461
+ | OS | Library file |
462
+ |---|---|
463
+ | Windows | `@n1h/sweetview-webview-bun/build/libwebview.dll` |
464
+ | macOS | `@n1h/sweetview-webview-bun/build/libwebview.dylib` |
465
+ | Linux | `@n1h/sweetview-webview-bun/build/libwebview-<arch>.so` |
466
+
467
+ The `Webview` class resolves the same path, and `createWebviewApp` sends it
468
+ to the worker, so the process loads one library. `bun build --compile`
469
+ embeds the shipped file. `createWebviewApp` probes for the two symbols that
470
+ the shipped library adds to the stock one:
471
+
472
+ | Symbol | When absent | When it returns non-zero |
473
+ |---|---|---|
474
+ | `webview_set_asset` | Throws `Error`. A `WEBVIEW_PATH` that points at a stock library ends here. | Throws `Error` naming the asset path. |
475
+ | `webview_set_background_color` | Only probed with `transparent`. Logs a warning with `console.warn` and the page stays opaque. | Throws `Error`. |
476
+
477
+ The C API of both symbols is in the [webview library](webview-library.md).
478
+
479
+ ## Transport
480
+
481
+ `run()` blocks the UI thread and Bun's event loop with it, so the worker owns
482
+ the event loop and the `Service`. Three parties talk over five paths:
483
+
484
+ | Direction | Mechanism |
485
+ |---|---|
486
+ | page to UI thread | The page calls `__sweetview_rpc(method, params)`, bound with `webview_bind`. It returns a Promise. |
487
+ | UI thread to worker | `worker.postMessage`: one init message carrying the ring, the webview handle, the drain callback pointer and the library path, then one call frame per call. It is a synchronous enqueue, so it works while the UI thread is in the OS loop. |
488
+ | worker to UI thread | Reply and event frames as JSON in the shared `Ring`. The worker wakes the UI thread with `webview_dispatch`, at most once per burst, and the dispatched callback drains the ring inside the OS loop. |
489
+ | UI thread to page, replies | `webview_return(seq, status, json)`. Status 0 resolves the page's Promise with the result. Status 1 rejects it with `{ message }`. |
490
+ | UI thread to page, events | `webview_eval` of `__sweetview_emit(name, payload)`. |
491
+
492
+ A page call goes to the first of these that applies:
493
+
494
+ | Condition | Answer |
495
+ |---|---|
496
+ | `uiMethods` has the method as an own property with a truthy value | The function runs on the UI thread and its result or error goes straight back with `webview_return`. |
497
+ | `worker` was given | A call frame with the page's `seq` goes to the worker. The worker's reply frame is returned with `webview_return`. |
498
+ | neither | The call is rejected with `<method>: this window has no worker and no UI-thread method of that name`. |
499
+
500
+ Each event frame from the worker goes to every `onEvent` listener first and
501
+ then to the page. The worker side is in the [worker reference](worker.md),
502
+ the page side in the [page reference](page.md), and the frame types in the
503
+ [ipc reference](ipc.md).
504
+
505
+ ## Example
506
+
507
+ ```ts
508
+ import { createWebviewApp } from "@n1h/sweetview";
509
+ import index from "./index.html";
510
+
511
+ type Methods = { "notes.list": [void, string[]]; "window.size": [void, number] };
512
+ type Events = { "notes.changed": { count: number } };
513
+
514
+ const app = await createWebviewApp<Methods, Events>({
515
+ page: index,
516
+ worker: "./app-worker.ts",
517
+ uiMethods: { "window.size": () => 800 },
518
+ });
519
+
520
+ app.onEvent((event) => console.log(event.name, event.payload.count));
521
+ app.call("notes.list");
522
+ app.run();
523
+ ```