@n1h/sweetview 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/LICENSE-0BSD +12 -0
- package/README.md +268 -3
- package/docs/README.md +26 -0
- package/docs/app.md +523 -0
- package/docs/build.md +169 -0
- package/docs/ipc-ws.md +202 -0
- package/docs/ipc.md +323 -0
- package/docs/native.md +1066 -0
- package/docs/page.md +71 -0
- package/docs/settings-store.md +125 -0
- package/docs/webview-library.md +104 -0
- package/docs/window-chrome.md +221 -0
- package/docs/worker.md +72 -0
- package/file-imports.d.ts +19 -0
- package/package.json +66 -4
- package/src/app.ts +350 -0
- package/src/build.ts +118 -0
- package/src/html.ts +71 -0
- package/src/index.ts +8 -0
- package/src/ipc/bridge.ts +41 -0
- package/src/ipc/frame.ts +16 -0
- package/src/ipc/index.ts +16 -0
- package/src/ipc/memory.ts +33 -0
- package/src/ipc/service.ts +75 -0
- package/src/ipc-ws/client.ts +111 -0
- package/src/ipc-ws/index.ts +5 -0
- package/src/ipc-ws/server.ts +133 -0
- package/src/make-app.ts +93 -0
- package/src/messages.ts +16 -0
- package/src/native/app-icon.ts +23 -0
- package/src/native/app-menu.ts +18 -0
- package/src/native/darwin/app-icon.ts +10 -0
- package/src/native/darwin/app-menu.ts +88 -0
- package/src/native/darwin/folder-picker.ts +29 -0
- package/src/native/darwin/image.ts +47 -0
- package/src/native/darwin/menu.ts +89 -0
- package/src/native/darwin/notifier.ts +95 -0
- package/src/native/darwin/objc.ts +246 -0
- package/src/native/darwin/overlay.ts +352 -0
- package/src/native/darwin/tray.ts +129 -0
- package/src/native/darwin/window.ts +64 -0
- package/src/native/folder-picker.ts +15 -0
- package/src/native/hit-regions.ts +162 -0
- package/src/native/icon.ts +530 -0
- package/src/native/index.ts +38 -0
- package/src/native/menu.ts +43 -0
- package/src/native/notifications.ts +40 -0
- package/src/native/overlay.ts +182 -0
- package/src/native/platform.ts +25 -0
- package/src/native/tray.ts +41 -0
- package/src/native/win32/api.ts +234 -0
- package/src/native/win32/app-icon.ts +5 -0
- package/src/native/win32/app-menu.ts +4 -0
- package/src/native/win32/folder-picker.ts +121 -0
- package/src/native/win32/icon.ts +85 -0
- package/src/native/win32/menu.ts +107 -0
- package/src/native/win32/message-window.ts +72 -0
- package/src/native/win32/notifier.ts +28 -0
- package/src/native/win32/overlay.ts +499 -0
- package/src/native/win32/tray.ts +228 -0
- package/src/native/win32/window.ts +77 -0
- package/src/native/window.ts +40 -0
- package/src/page.ts +48 -0
- package/src/ring.ts +110 -0
- package/src/settings-store.ts +101 -0
- package/src/webview-lib.ts +60 -0
- package/src/window-chrome.ts +118 -0
- package/src/worker.ts +72 -0
package/docs/build.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# @n1h/sweetview/build
|
|
2
|
+
|
|
3
|
+
Build-time helpers for shipping an app: the icon files a build uses, and the macOS `.app` bundle around the compiled binary. They run on any OS in any build script and load neither `bun:ffi` nor the webview library. This page also covers the `make-app.ts` command line and the `@n1h/sweetview/file-imports` types.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { makeMacApp, writeIconFiles, type MacAppOptions } from "@n1h/sweetview/build";
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Reference
|
|
10
|
+
|
|
11
|
+
### MacAppOptions
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
export type MacAppOptions = {
|
|
15
|
+
name: string;
|
|
16
|
+
bundleId: string;
|
|
17
|
+
binary: string;
|
|
18
|
+
outDir: string;
|
|
19
|
+
icns?: string;
|
|
20
|
+
version?: string;
|
|
21
|
+
minimumSystemVersion?: string;
|
|
22
|
+
plist?: Record<string, string | number | boolean>;
|
|
23
|
+
};
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The options of `makeMacApp`.
|
|
27
|
+
|
|
28
|
+
| Field | Type | Default | Meaning |
|
|
29
|
+
|---|---|---|---|
|
|
30
|
+
| `name` | `string` | required | What Finder, the Dock and the menu bar show. The bundle is `<name>.app`. Written as `CFBundleName` and `CFBundleDisplayName`. |
|
|
31
|
+
| `bundleId` | `string` | required | The reverse-DNS bundle id, such as `com.example.my-app`. Written as `CFBundleIdentifier`. macOS notifications need one, so a bare binary cannot send them. |
|
|
32
|
+
| `binary` | `string` | required | The compiled macOS executable, from `bun build --compile --target=bun-darwin-arm64`. Copied in under its own file name. |
|
|
33
|
+
| `outDir` | `string` | required | The folder the bundle is written into. Created when missing. An existing `<name>.app` there is deleted first. |
|
|
34
|
+
| `icns` | `string` | none | The `.icns` file for Finder and the Dock, for example one written by `writeIconFiles`. Without it the bundle has no icon file and shows the stock icon. |
|
|
35
|
+
| `version` | `string` | `"0.1.0"` | Written as `CFBundleVersion` and `CFBundleShortVersionString`. |
|
|
36
|
+
| `minimumSystemVersion` | `string` | `"11.0"` | Written as `LSMinimumSystemVersion`. |
|
|
37
|
+
| `plist` | `Record<string, string \| number \| boolean>` | none | More `Info.plist` entries by key, such as `{ LSUIElement: true }` for a menu-bar app with no Dock tile. An entry with a new key is written after the defaults. An entry with the key of a default replaces that default's value in the default's position. |
|
|
38
|
+
|
|
39
|
+
### makeMacApp
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
export function makeMacApp(options: MacAppOptions): string
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Wraps the compiled macOS binary in a `.app` bundle at `<outDir>/<name>.app`. Finder then launches it without a Terminal window, and notifications have the bundle id they need.
|
|
46
|
+
|
|
47
|
+
| Field | Type | Default | Meaning |
|
|
48
|
+
|---|---|---|---|
|
|
49
|
+
| `options` | `MacAppOptions` | required | See `MacAppOptions`. |
|
|
50
|
+
|
|
51
|
+
It checks that `binary` exists, and `icns` when set. It then deletes any existing bundle at that path and writes these files:
|
|
52
|
+
|
|
53
|
+
| Path inside `<name>.app` | Content |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `Contents/MacOS/<binary file name>` | A copy of `binary`, with mode `0755`. |
|
|
56
|
+
| `Contents/Resources/AppIcon.icns` | A copy of `icns`. Written only when `icns` is set; the `Resources` folder is created either way. |
|
|
57
|
+
| `Contents/Info.plist` | An XML property list with the keys below. |
|
|
58
|
+
| `Contents/PkgInfo` | The text `APPL????`. |
|
|
59
|
+
|
|
60
|
+
`Info.plist` holds these keys, in this order, followed by the `plist` entries with new keys:
|
|
61
|
+
|
|
62
|
+
| Key | Value |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `CFBundleName` | `name` |
|
|
65
|
+
| `CFBundleDisplayName` | `name` |
|
|
66
|
+
| `CFBundleIdentifier` | `bundleId` |
|
|
67
|
+
| `CFBundleVersion` | `version`, default `0.1.0` |
|
|
68
|
+
| `CFBundleShortVersionString` | `version`, default `0.1.0` |
|
|
69
|
+
| `CFBundleExecutable` | the file name of `binary` |
|
|
70
|
+
| `CFBundleIconFile` | `AppIcon`; present only when `icns` is set |
|
|
71
|
+
| `CFBundlePackageType` | `APPL` |
|
|
72
|
+
| `CFBundleInfoDictionaryVersion` | `6.0` |
|
|
73
|
+
| `LSMinimumSystemVersion` | `minimumSystemVersion`, default `11.0` |
|
|
74
|
+
| `NSHighResolutionCapable` | `true` |
|
|
75
|
+
| `NSPrincipalClass` | `NSApplication` |
|
|
76
|
+
|
|
77
|
+
A boolean value is written as `<true/>` or `<false/>`, an integer as `<integer>`, any other number as `<real>`, and a string as `<string>`. `NaN` and infinities throw, because a plist cannot hold them. `&`, `<` and `>` in keys and strings are escaped.
|
|
78
|
+
|
|
79
|
+
The bundle is unsigned. On a Mac that calls a copied bundle damaged, `xattr -dr com.apple.quarantine "<name>.app"`, `chmod +x "<name>.app/Contents/MacOS/"*` and `codesign --sign - --force --deep "<name>.app"` remove the quarantine flag, restore the executable bit and add an ad-hoc signature.
|
|
80
|
+
|
|
81
|
+
Returns: the bundle's path, `<outDir>/<name>.app`.
|
|
82
|
+
|
|
83
|
+
Throws: `makeMacApp: <binary> not found (compile the macOS binary first)` when `binary` does not exist. Nothing is deleted or written in that case.
|
|
84
|
+
|
|
85
|
+
Throws: `makeMacApp: <icns> not found (write the .icns first)` when `icns` is set and does not exist. Nothing is deleted or written in that case.
|
|
86
|
+
|
|
87
|
+
Throws: any file-system error from deleting, copying or writing, unchanged.
|
|
88
|
+
|
|
89
|
+
Platforms: Windows / macOS / Linux: implemented. On Windows `chmodSync` cannot set an executable bit, so a bundle built there needs the `chmod +x` above on the Mac.
|
|
90
|
+
|
|
91
|
+
Thread: blocks the calling thread on file I/O.
|
|
92
|
+
|
|
93
|
+
### writeIconFiles
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
export function writeIconFiles(dir: string, files: { ico?: Uint8Array; icns?: Uint8Array }): string[]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Writes the icon files a build embeds into `dir`, creating the folder and any missing parents. Existing files of the same name are replaced. The encoders for both formats, such as `icoFromRgba` and `icnsFromRgba`, are in `@n1h/sweetview/native`, see [native.md](native.md).
|
|
100
|
+
|
|
101
|
+
| Field | Type | Default | Meaning |
|
|
102
|
+
|---|---|---|---|
|
|
103
|
+
| `dir` | `string` | required | The folder to write into. |
|
|
104
|
+
| `files.ico` | `Uint8Array` | none | Written as `<dir>/app.ico`, for `bun build --compile --windows-icon=<dir>/app.ico`. That flag works only on a Windows build host. |
|
|
105
|
+
| `files.icns` | `Uint8Array` | none | Written as `<dir>/AppIcon.icns`, for the `icns` option of `makeMacApp`. |
|
|
106
|
+
|
|
107
|
+
Returns: the paths written, `app.ico` first when present, then `AppIcon.icns`.
|
|
108
|
+
|
|
109
|
+
Throws: any file-system error from creating the folder or writing, unchanged.
|
|
110
|
+
|
|
111
|
+
Thread: blocks the calling thread on file I/O.
|
|
112
|
+
|
|
113
|
+
## make-app command line
|
|
114
|
+
|
|
115
|
+
`src/make-app.ts` is `makeMacApp` as a command. It is not a package export. Run it by path with Bun: `bun node_modules/@n1h/sweetview/src/make-app.ts --name "My App" --bundle-id com.example.my-app --binary dist/my-app-macos-arm64 --out dist`. It calls `makeMacApp` and writes the same files. Importing the file runs nothing.
|
|
116
|
+
|
|
117
|
+
| Flag | Value | Default | Meaning |
|
|
118
|
+
|---|---|---|---|
|
|
119
|
+
| `--name` | text | required | `name`: what Finder and the Dock show. The bundle is `<name>.app`. |
|
|
120
|
+
| `--bundle-id` | text | required | `bundleId`: the reverse-DNS id. |
|
|
121
|
+
| `--binary` | path | required | `binary`: the compiled macOS executable. |
|
|
122
|
+
| `--out` | path | required | `outDir`: the folder the bundle is written into. An existing `<name>.app` there is replaced. |
|
|
123
|
+
| `--icns` | path | none | `icns`: the `.icns` for Finder and the Dock. The stock icon without it. |
|
|
124
|
+
| `--version` | text | `0.1.0` | `version`: `CFBundleVersion` and `CFBundleShortVersionString`. |
|
|
125
|
+
| `--min-macos` | text | `11.0` | `minimumSystemVersion`: `LSMinimumSystemVersion`. |
|
|
126
|
+
| `--plist` | `Key=value` | none | One extra `Info.plist` entry. Repeatable; a later entry with the same key wins. |
|
|
127
|
+
| `--help`, `-h` | none | off | Prints the usage text to stdout and exits. |
|
|
128
|
+
|
|
129
|
+
A `--plist` entry splits at its first `=`. The value may contain further `=` signs. The value `true` or `false` becomes a boolean. A `--name` of empty text counts as missing. A non-blank value that `Number()` reads as a finite number, such as `42`, `1.5` or `0x10`, becomes a number and is written as `<integer>` or `<real>`. Anything else stays a string.
|
|
130
|
+
|
|
131
|
+
| Exit code | When | Output |
|
|
132
|
+
|---|---|---|
|
|
133
|
+
| `0` | The bundle was written. | `wrote <path>` on stdout. |
|
|
134
|
+
| `0` | `--help` or `-h`. | The usage text on stdout. |
|
|
135
|
+
| `1` | `makeMacApp` threw, for example a missing binary or icns file. | The error message on stderr. |
|
|
136
|
+
| `2` | An unknown flag, a positional argument, or a flag without its value. | The parser's message and the usage text on stderr. |
|
|
137
|
+
| `2` | One or more required flags missing. | `missing: --name, --bundle-id, ...` listing the missing ones, then the usage text, on stderr. |
|
|
138
|
+
| `2` | A `--plist` entry with no `=` or an empty key. | `--plist wants Key=value, got "<entry>"` on stderr. |
|
|
139
|
+
|
|
140
|
+
## File-import types
|
|
141
|
+
|
|
142
|
+
`@n1h/sweetview/file-imports` is a types-only module: the package maps it to `file-imports.d.ts` under the `types` condition, and it has no runtime module. It declares path-valued modules for the files an app imports with `with { type: "file" }`, such as `import js from "./build/frontend/main.js" with { type: "file" };`. Bun resolves such an import to a path the file can be read from. `Bun.file(js)` reads that file and can be passed as an asset to `createWebviewApp`, see [app.md](app.md). `bun build --compile` embeds the file.
|
|
143
|
+
|
|
144
|
+
| Module pattern | Default export |
|
|
145
|
+
|---|---|
|
|
146
|
+
| `*.css` | `string`, the file's path |
|
|
147
|
+
| `*.js` | `string`, the file's path |
|
|
148
|
+
|
|
149
|
+
An app references the module from a `.d.ts` file of its own with `/// <reference types="@n1h/sweetview/file-imports" />`. Other file kinds, such as `*.png` or `*.glb`, need their own `declare module` beside that line.
|
|
150
|
+
|
|
151
|
+
## Example
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
import { makeMacApp, writeIconFiles } from "@n1h/sweetview/build";
|
|
155
|
+
|
|
156
|
+
declare const ico: Uint8Array;
|
|
157
|
+
declare const icns: Uint8Array;
|
|
158
|
+
|
|
159
|
+
writeIconFiles("build", { ico, icns });
|
|
160
|
+
const app = makeMacApp({
|
|
161
|
+
name: "My App",
|
|
162
|
+
bundleId: "com.example.my-app",
|
|
163
|
+
binary: "dist/my-app-macos-arm64",
|
|
164
|
+
outDir: "dist",
|
|
165
|
+
icns: "build/AppIcon.icns",
|
|
166
|
+
plist: { LSUIElement: true },
|
|
167
|
+
});
|
|
168
|
+
console.log(app);
|
|
169
|
+
```
|
package/docs/ipc-ws.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# @n1h/sweetview/ipc-ws
|
|
2
|
+
|
|
3
|
+
The [`ipc`](ipc.md) contract over WebSockets, so a `Service` served by `Bun.serve` reaches a page in an ordinary browser. `@n1h/sweetview/ipc-ws/server` runs in Bun and hosts the service; `@n1h/sweetview/ipc-ws/client` runs in the browser page and provides its `Transport`. `@n1h/sweetview/ipc-ws` re-exports both; neither module touches Bun or browser globals until one of its functions is called.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { wsHost, type WsData, type WsHost, type WsHostOptions, wsTransport, wsUrl, type WsTransport } from "@n1h/sweetview/ipc-ws";
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Server: `@n1h/sweetview/ipc-ws/server`
|
|
10
|
+
|
|
11
|
+
Bun only. Its types come from `bun`. It works with `Bun.serve`, on the main thread or in a Worker.
|
|
12
|
+
|
|
13
|
+
### wsHost
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
export function wsHost<M extends MethodMap, E extends EventMap>(service: Service<M, E>, options: WsHostOptions = {}): WsHost;
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Hosts a `Service` behind a `Bun.serve` WebSocket handler. Each socket is one session. The host subscribes to the service's events when `wsHost` is called and forwards them until `close()`.
|
|
20
|
+
|
|
21
|
+
| Parameter | Type | Default | Meaning |
|
|
22
|
+
|---|---|---|---|
|
|
23
|
+
| `service` | `Service<M, E>` | required | The service to host. |
|
|
24
|
+
| `options` | `WsHostOptions` | `{}` | Origin policy. |
|
|
25
|
+
|
|
26
|
+
Returns: a `WsHost`. Pass its `websocket` to `Bun.serve` and call its `upgrade` from `fetch`.
|
|
27
|
+
|
|
28
|
+
The host never calls `service.dispose()`. The application disposes the service after `close()`.
|
|
29
|
+
|
|
30
|
+
### WsData
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
export type WsData = { session: Session };
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The data Bun attaches to each socket the host upgrades. It is the type parameter of the `Server` and `WebSocketHandler` the host works with.
|
|
37
|
+
|
|
38
|
+
| Field | Type | Meaning |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `session` | `Session` | The socket's session. `id` is `ws-<n>`, where `<n>` counts from 1 per host. An upgrade that Bun refuses still uses a number. `kind` is always `"web"`. |
|
|
41
|
+
|
|
42
|
+
### WsHost
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
export type WsHost = {
|
|
46
|
+
upgrade(req: Request, server: Server<WsData>): boolean;
|
|
47
|
+
websocket: WebSocketHandler<WsData>;
|
|
48
|
+
readonly sessions: number;
|
|
49
|
+
close(): void;
|
|
50
|
+
};
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
What `wsHost` returns.
|
|
54
|
+
|
|
55
|
+
| Field | Type | Meaning |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `upgrade` | `(req: Request, server: Server<WsData>) => boolean` | Checks the request's `Origin`, assigns a new session, and upgrades the request. Returns `false` when the `Origin` is refused or Bun did not upgrade the request. On `false`, `fetch` must return its own error response. |
|
|
58
|
+
| `websocket` | `WebSocketHandler<WsData>` | The `websocket` option for `Bun.serve`. |
|
|
59
|
+
| `sessions` | `number` | Number of open sockets. |
|
|
60
|
+
| `close` | `() => void` | Stops forwarding events and closes every socket. Each closed socket still ends its session on the service. It does not dispose the service. |
|
|
61
|
+
|
|
62
|
+
Throws: `upgrade` throws whatever an `allowOrigin` predicate throws.
|
|
63
|
+
|
|
64
|
+
`close()` does not stop the host from accepting new sockets. Sockets opened after `close()` can call the service but receive no events. Stop the `Bun.serve` server, or stop calling `upgrade`, to refuse them.
|
|
65
|
+
|
|
66
|
+
### WsHostOptions
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
export type WsHostOptions = {
|
|
70
|
+
allowOrigin?: string[] | ((origin: string, req: Request) => boolean);
|
|
71
|
+
};
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Which browser pages may connect. Browsers do not apply the same-origin policy to WebSockets, so without a check any page the user has open could call the service.
|
|
75
|
+
|
|
76
|
+
| Field | Type | Default | Meaning |
|
|
77
|
+
|---|---|---|---|
|
|
78
|
+
| `allowOrigin` | `string[] \| ((origin: string, req: Request) => boolean)` | `Origin` host equals `Host` | A list of allowed `Origin` values, compared as exact strings, or a predicate that decides. |
|
|
79
|
+
|
|
80
|
+
A request without an `Origin` header is always accepted, because it does not come from a browser. With no `allowOrigin`, a request is accepted when the host of its `Origin` equals its `Host` header. An `Origin` that is not a valid URL, such as `null`, is then refused. Binding `Bun.serve` to `127.0.0.1` keeps the service off the network.
|
|
81
|
+
|
|
82
|
+
## Client: `@n1h/sweetview/ipc-ws/client`
|
|
83
|
+
|
|
84
|
+
Runs in a browser page, or in any runtime with a global `WebSocket`, such as Bun. `wsUrl`, and `wsTransport` called without a `url`, need the global `location`.
|
|
85
|
+
|
|
86
|
+
### wsTransport
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
export function wsTransport(url: string = wsUrl()): WsTransport;
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Opens one WebSocket and returns a `Transport` over it. Calls made before the socket opens are queued and sent on open. When the socket closes, every pending call rejects and the transport stays closed; it never reconnects.
|
|
93
|
+
|
|
94
|
+
| Field | Type | Default | Meaning |
|
|
95
|
+
|---|---|---|---|
|
|
96
|
+
| `url` | `string` | `wsUrl()` | The `ws://` or `wss://` URL of the host's upgrade path. |
|
|
97
|
+
|
|
98
|
+
Returns: a `WsTransport` with `kind` set to `"web"`.
|
|
99
|
+
|
|
100
|
+
Throws: the `WebSocket` constructor's error when `url` is not a valid WebSocket URL.
|
|
101
|
+
|
|
102
|
+
Throws: a `ReferenceError` when `url` is omitted and no global `location` exists.
|
|
103
|
+
|
|
104
|
+
- `call` rejects with `Error("websocket closed")` when the socket is closing or closed, and when it closes before the reply arrives.
|
|
105
|
+
- `call` rejects with an `Error` carrying the backend's message when the reply has `ok: false`.
|
|
106
|
+
- `call` rejects with the `JSON.stringify` error when `params` cannot be serialised as JSON.
|
|
107
|
+
- `notify` never throws. When the socket is closing or closed, or `params` cannot be serialised as JSON, it logs `<method> not sent: <message>` to the console instead.
|
|
108
|
+
- A `notify` queued before the socket opens is dropped without a log when the socket closes before opening.
|
|
109
|
+
- Events received while no `onEvent` listener is subscribed are dropped.
|
|
110
|
+
|
|
111
|
+
### wsUrl
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
export function wsUrl(path = "/ws"): string;
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Builds the WebSocket URL for the page's own origin: `wss:` when the page was loaded over `https:`, `ws:` otherwise.
|
|
118
|
+
|
|
119
|
+
| Parameter | Type | Default | Meaning |
|
|
120
|
+
|---|---|---|---|
|
|
121
|
+
| `path` | `string` | `"/ws"` | Path on the page's host, starting with `/`. |
|
|
122
|
+
|
|
123
|
+
Returns: `<ws: or wss:>//<location.host><path>`.
|
|
124
|
+
|
|
125
|
+
Throws: a `ReferenceError` where no global `location` exists, as in Bun.
|
|
126
|
+
|
|
127
|
+
### WsTransport
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
export type WsTransport = Transport & {
|
|
131
|
+
readonly ready: Promise<void>;
|
|
132
|
+
onClose(fn: () => void): () => void;
|
|
133
|
+
close(): void;
|
|
134
|
+
};
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The `Transport` from `wsTransport`, plus the socket's lifecycle.
|
|
138
|
+
|
|
139
|
+
| Field | Type | Meaning |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `ready` | `Promise<void>` | Resolves when the socket opens. Rejects with `Error("websocket closed")` when it closes first. Leaving the rejection unhandled is safe. |
|
|
142
|
+
| `onClose` | `(fn: () => void) => () => void` | Runs `fn` once, when the socket closes for any reason. If the socket is already closed, runs `fn` synchronously and returns a no-op unsubscribe function. Otherwise returns an unsubscribe function. |
|
|
143
|
+
| `close` | `() => void` | Closes the socket. The transport cannot be reopened; a new `wsTransport` is a new session. |
|
|
144
|
+
|
|
145
|
+
## Wire protocol
|
|
146
|
+
|
|
147
|
+
Each socket carries the `Frame` types documented in [`ipc.md`](ipc.md), one JSON text message per frame.
|
|
148
|
+
|
|
149
|
+
| Direction | Frames | Notes |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| page to server | `CallFrame` | The client numbers calls `"1"`, `"2"`, and so on per transport. `notify` sends `seq: null`. |
|
|
152
|
+
| server to page | `ReplyFrame` | One reply per call whose `seq` is not `null`. Calls run concurrently, so replies can arrive in any order and are matched by `seq`. |
|
|
153
|
+
| server to page | `EventFrame` | Sent for every event the service emits, subject to targeting. |
|
|
154
|
+
|
|
155
|
+
Session identity:
|
|
156
|
+
|
|
157
|
+
- The server assigns the session when it upgrades the request. No frame field carries the session id.
|
|
158
|
+
- One socket is one session. A page that opens a new socket gets a new session id.
|
|
159
|
+
- Handlers see the session in `ctx.session`.
|
|
160
|
+
|
|
161
|
+
Event targeting:
|
|
162
|
+
|
|
163
|
+
- An event emitted without `to` goes to every open socket of the host.
|
|
164
|
+
- An event emitted with `to` goes only to the socket whose session id equals `to`. It is dropped when no such socket is open.
|
|
165
|
+
|
|
166
|
+
Messages a side does not expect:
|
|
167
|
+
|
|
168
|
+
- The server decodes binary messages as UTF-8 text.
|
|
169
|
+
- The server ignores a message that is not valid JSON. It sends no reply and logs nothing.
|
|
170
|
+
- The server ignores a JSON value that is not an object: `null`, a number, a string or a boolean. The client does the same.
|
|
171
|
+
- When a result or an event payload cannot be serialised as JSON (a `BigInt`, a cycle), the server answers the call with an error reply `<seq>: the result is not serialisable: <message>`; an event that cannot be serialised is logged and dropped.
|
|
172
|
+
- The server ignores an array, and an object whose `kind` is not `"call"`.
|
|
173
|
+
- The server does not check the types of `seq`, `method` or `params` in a call frame.
|
|
174
|
+
- When a call with `seq: null` fails, the server logs `<method> failed: <message>` to its console.
|
|
175
|
+
- The client ignores messages that are not JSON, frames whose `kind` is neither `"reply"` nor `"event"`, and replies whose `seq` it is not waiting for. A binary message, which a browser delivers as a `Blob`, is not JSON.
|
|
176
|
+
|
|
177
|
+
Close behaviour:
|
|
178
|
+
|
|
179
|
+
- When a socket closes from either side, the host stops tracking it and calls `service.endSession` with its session id.
|
|
180
|
+
- A reply is dropped when its socket closed, or `WsHost.close()` ran, before the call finished. The handler still runs to completion.
|
|
181
|
+
- `WsHost.close()` closes every socket with no close code or reason given.
|
|
182
|
+
- On the client, a close rejects every pending call with `Error("websocket closed")` and runs the `onClose` listeners.
|
|
183
|
+
- Neither side reconnects.
|
|
184
|
+
- The protocol defines no heartbeat of its own. The `websocket` handler sets no idle timeout or ping options, so Bun's defaults apply.
|
|
185
|
+
|
|
186
|
+
## Example
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
import { defineService } from "@n1h/sweetview/ipc";
|
|
190
|
+
import { wsHost } from "@n1h/sweetview/ipc-ws/server";
|
|
191
|
+
|
|
192
|
+
const service = defineService<{ ping: [void, "pong"] }, {}>(() => ({ methods: { ping: () => "pong" } }));
|
|
193
|
+
const host = wsHost(service);
|
|
194
|
+
Bun.serve({
|
|
195
|
+
hostname: "127.0.0.1",
|
|
196
|
+
fetch(req, server) {
|
|
197
|
+
if (new URL(req.url).pathname !== "/ws") return new Response("not found", { status: 404 });
|
|
198
|
+
return host.upgrade(req, server) ? undefined : new Response("expected a websocket", { status: 400 });
|
|
199
|
+
},
|
|
200
|
+
websocket: host.websocket,
|
|
201
|
+
});
|
|
202
|
+
```
|