gesso-electrobun 0.6.6 → 0.6.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.
- package/CHANGELOG.md +20 -0
- package/dist/desktop.d.ts +12 -3
- package/dist/desktop.js +22 -9
- package/dist/desktop.js.map +1 -1
- package/dist/frames-CsYW_e7F.d.ts +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/main.d.ts +5 -49
- package/dist/main.js +3 -102
- package/dist/main.js.map +1 -1
- package/dist/view.d.ts +5 -59
- package/dist/view.js +9 -102
- package/dist/view.js.map +1 -1
- package/package.json +2 -2
- package/dist/frames-BQisaYy-.js +0 -105
- package/dist/frames-BQisaYy-.js.map +0 -1
- package/dist/frames-Bx7SnqWg.d.ts +0 -86
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# gesso-electrobun
|
|
2
2
|
|
|
3
|
+
## 0.6.8
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- dc47f50: `messageBoxConfirm` now shows a person everything they are being asked to approve, and declines on Enter on macOS. The command's description and the arguments the agent sent were in the dialog's `detail`, which macOS does not show, so a Mac asked only "An AI agent wants to createBranch in workspace" with nothing to say which branch; they are now part of the message, which every platform shows. And the buttons were Allow then Decline: macOS makes the first button the default whatever `defaultId` says, so Enter allowed the command. Decline is now first, and the default on every platform.
|
|
8
|
+
- Updated dependencies [a7bb34e]
|
|
9
|
+
- gesso-framework@0.6.8
|
|
10
|
+
|
|
11
|
+
## 0.6.7
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 4f622ec: Channels can now be served from another process without Electrobun. The bridge that carried a desktop window's channels to and from the main process has moved to `gesso-framework/remote`, under names that say what it is: `createRemoteBridge` in the page and `serveRemoteChannels` in the process that owns the data, with the frame format (`GessoFrame`, `frameData`, `FrameAssembler`, `isGessoFrame`, `DEFAULT_CHUNK_BYTES`) beside them. Neither half knows its transport; each takes a `send` function and has a `receive` method, so a web application whose data lives in a server on the person's machine can carry its channels over a WebSocket. The new page "Channels from another process" shows it end to end.
|
|
16
|
+
|
|
17
|
+
`gesso-electrobun` keeps every name it had: `createElectrobunBridge`, `serveChannelsToWindow`, `ChannelHost`, `ElectrobunBridge` and the frame exports are now re-exports of the same code, so a desktop application needs no change.
|
|
18
|
+
|
|
19
|
+
- Updated dependencies [6493881]
|
|
20
|
+
- Updated dependencies [4f622ec]
|
|
21
|
+
- gesso-framework@0.6.7
|
|
22
|
+
|
|
3
23
|
## 0.6.6
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
package/dist/desktop.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as GessoFrame } from "./frames-
|
|
1
|
+
import { r as GessoFrame } from "./frames-CsYW_e7F.js";
|
|
2
2
|
import { n as withWindowRoute } from "./route-CNeMnj_6.js";
|
|
3
3
|
import { ChannelToken, ServedChannel } from "gesso-framework";
|
|
4
4
|
import { Observable } from "rxjs";
|
|
@@ -81,8 +81,17 @@ type ShowMessageBox = (options: {
|
|
|
81
81
|
*
|
|
82
82
|
* Takes Electrobun's `Utils.showMessageBox` rather than importing it,
|
|
83
83
|
* because Electrobun is a toolchain a project is projected into, not a
|
|
84
|
-
* package this one can depend on.
|
|
85
|
-
*
|
|
84
|
+
* package this one can depend on.
|
|
85
|
+
*
|
|
86
|
+
* Everything the person needs to decide is in `message`, the command's
|
|
87
|
+
* description and its arguments included, because macOS shows no
|
|
88
|
+
* `detail`: a dialog that asked there showed only "wants to
|
|
89
|
+
* createBranch in workspace", with nothing to say which branch.
|
|
90
|
+
*
|
|
91
|
+
* Decline is the first button. macOS makes the first button the
|
|
92
|
+
* default, whatever `defaultId` says, so with Allow first, Enter
|
|
93
|
+
* approved; first is also what `defaultId` and `cancelId` name, so
|
|
94
|
+
* Enter, Escape and closing the dialog decline on every platform.
|
|
86
95
|
*/
|
|
87
96
|
declare function messageBoxConfirm(showMessageBox: ShowMessageBox): (request: AgentConfirmation) => Promise<boolean>;
|
|
88
97
|
//#endregion
|
package/dist/desktop.js
CHANGED
|
@@ -45,22 +45,35 @@ function serveDesktopAgent(channels, options = {}) {
|
|
|
45
45
|
*
|
|
46
46
|
* Takes Electrobun's `Utils.showMessageBox` rather than importing it,
|
|
47
47
|
* because Electrobun is a toolchain a project is projected into, not a
|
|
48
|
-
* package this one can depend on.
|
|
49
|
-
*
|
|
48
|
+
* package this one can depend on.
|
|
49
|
+
*
|
|
50
|
+
* Everything the person needs to decide is in `message`, the command's
|
|
51
|
+
* description and its arguments included, because macOS shows no
|
|
52
|
+
* `detail`: a dialog that asked there showed only "wants to
|
|
53
|
+
* createBranch in workspace", with nothing to say which branch.
|
|
54
|
+
*
|
|
55
|
+
* Decline is the first button. macOS makes the first button the
|
|
56
|
+
* default, whatever `defaultId` says, so with Allow first, Enter
|
|
57
|
+
* approved; first is also what `defaultId` and `cancelId` name, so
|
|
58
|
+
* Enter, Escape and closing the dialog decline on every platform.
|
|
50
59
|
*/
|
|
51
60
|
function messageBoxConfirm(showMessageBox) {
|
|
52
61
|
return async (request) => {
|
|
53
|
-
const
|
|
62
|
+
const argumentLines = Object.entries(request.arguments).map(([name, value]) => `${name}: ${JSON.stringify(value)}`);
|
|
63
|
+
const message = [
|
|
64
|
+
`An AI agent wants to ${request.command} in ${request.channel}.${request.destructive ? " This cannot be undone." : ""}`,
|
|
65
|
+
request.description,
|
|
66
|
+
argumentLines.join("\n")
|
|
67
|
+
].filter((part) => part !== void 0 && part !== "").join("\n\n");
|
|
54
68
|
const { response } = await showMessageBox({
|
|
55
69
|
type: request.destructive ? "warning" : "question",
|
|
56
70
|
title: "An AI agent is asking",
|
|
57
|
-
message
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
cancelId: 1
|
|
71
|
+
message,
|
|
72
|
+
buttons: ["Decline", "Allow"],
|
|
73
|
+
defaultId: 0,
|
|
74
|
+
cancelId: 0
|
|
62
75
|
});
|
|
63
|
-
return response ===
|
|
76
|
+
return response === 1;
|
|
64
77
|
};
|
|
65
78
|
}
|
|
66
79
|
function bunServe() {
|
package/dist/desktop.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"desktop.js","names":[],"sources":["../src/agent.ts","../src/desktop.ts"],"sourcesContent":["import type { ServedChannel } from 'gesso-framework';\nimport {\n agentSurface,\n mcpHandler,\n type AgentConfirmation,\n type AgentSurface,\n type McpHandlerOptions\n} from 'gesso-framework/agent';\n\n/**\n * A desktop application's channels, served to AI agents over MCP.\n *\n * The main process already holds every channel's source: it is what\n * `createDesktopApp` serves to each window. So it is also where an\n * agent connects. This serves those same sources with MCP's HTTP\n * transport on the person's own machine, through `Bun.serve`, which\n * Cottontail provides as Bun does, and an agent such as Claude Code\n * connects by URL:\n *\n * const agent = serveDesktopAgent([counter], { name: 'my-app', confirm: messageBoxConfirm(Utils.showMessageBox) });\n * // claude mcp add --transport http my-app http://127.0.0.1:7310/mcp\n *\n * The channels are passed rather than read from the app, because\n * `createDesktopApp` takes them per window, and the per-window ones\n * (`windowsChannel`) are about a window an agent does not have.\n *\n * What it offers is the channel tools only. The screen is in each\n * window's render worker, out of the main process's reach, so the\n * screen tools a web app offers are not here.\n *\n * The descriptions come from `channelSchema`. A main process is bundled\n * by Electrobun's own build, which `gesso-vite-plugin` never sees, so\n * the template runs `gesso-channels` to write a module that describes\n * the contracts, and imports it.\n */\n\n/** A `Bun.serve`, as far as this uses one. */\nexport type DesktopServe = (options: {\n hostname: string;\n port: number;\n fetch: (request: Request) => Promise<Response>;\n}) => { stop(closeActiveConnections?: boolean): void };\n\nexport interface DesktopAgentOptions extends Omit<McpHandlerOptions, 'allowedOrigins'> {\n /**\n * The port to listen on (default 7310). When it is taken, the next\n * nine are tried in turn, so a second copy of the app still serves;\n * `url` says which one it got.\n */\n port?: number;\n /** Default `127.0.0.1`: reachable from this machine only. */\n hostname?: string;\n /**\n * Asks the person whether a `@confirm` command may be sent. Without\n * it such a command is refused. `messageBoxConfirm` makes one from\n * Electrobun's native dialog.\n */\n confirm?: (request: AgentConfirmation) => boolean | Promise<boolean>;\n /** The server to start. Defaults to the runtime's `Bun.serve`. */\n serve?: DesktopServe;\n}\n\nexport interface DesktopAgent {\n /** Where an agent connects: `http://127.0.0.1:<port>/mcp`. */\n readonly url: string;\n readonly surface: AgentSurface;\n /** Stops serving and stops following the channels. */\n stop(): void;\n}\n\nconst DEFAULT_PORT = 7310;\nconst PORTS_TRIED = 10;\n\nexport function serveDesktopAgent(channels: readonly ServedChannel[], options: DesktopAgentOptions = {}): DesktopAgent {\n const serve = options.serve ?? bunServe();\n const hostname = options.hostname ?? '127.0.0.1';\n const first = options.port ?? DEFAULT_PORT;\n const surface = agentSurface(channels, options.confirm === undefined ? {} : { confirm: options.confirm });\n // A desktop app has no browser pages of its own to let in, so every\n // request carrying an Origin is refused: that is a web page the person\n // has open, reaching for their machine.\n const fetch = mcpHandler(surface, { ...options, allowedOrigins: [] });\n\n let lastError: unknown;\n for (let port = first; port < first + PORTS_TRIED; port++) {\n try {\n const server = serve({ hostname, port, fetch });\n return {\n url: `http://${hostname}:${port}/mcp`,\n surface,\n stop: () => {\n server.stop(true);\n surface.dispose();\n }\n };\n } catch (error) {\n if (!isAddressInUse(error)) {\n surface.dispose();\n throw error;\n }\n lastError = error;\n }\n }\n surface.dispose();\n throw new Error(\n `Ports ${first} to ${first + PORTS_TRIED - 1} are all in use, so agents cannot be served. ` +\n `Pass another port to serveDesktopAgent. (${String(lastError)})`\n );\n}\n\n/** The options Electrobun's `Utils.showMessageBox` takes, as far as this uses them. */\nexport type ShowMessageBox = (options: {\n type?: 'info' | 'warning' | 'error' | 'question';\n title?: string;\n message?: string;\n detail?: string;\n buttons?: string[];\n defaultId?: number;\n cancelId?: number;\n}) => Promise<{ response: number }>;\n\n/**\n * A `confirm` that asks with the operating system's own dialog.\n *\n * Takes Electrobun's `Utils.showMessageBox` rather than importing it,\n * because Electrobun is a toolchain a project is projected into, not a\n * package this one can depend on. The safe answer is the default one:\n * Escape, closing the dialog, and Enter all decline.\n */\nexport function messageBoxConfirm(showMessageBox: ShowMessageBox): (request: AgentConfirmation) => Promise<boolean> {\n return async request => {\n const details = [\n request.description,\n Object.keys(request.arguments).length > 0 ? JSON.stringify(request.arguments, null, 2) : undefined\n ]\n .filter((part): part is string => part !== undefined && part !== '')\n .join('\\n\\n');\n const { response } = await showMessageBox({\n type: request.destructive ? 'warning' : 'question',\n title: 'An AI agent is asking',\n message: `An AI agent wants to ${request.command} in ${request.channel}.${request.destructive ? ' This cannot be undone.' : ''}`,\n detail: details,\n buttons: ['Allow', 'Decline'],\n defaultId: 1,\n cancelId: 1\n });\n return response === 0;\n };\n}\n\nfunction bunServe(): DesktopServe {\n const bun = (globalThis as { Bun?: { serve?: DesktopServe } }).Bun;\n if (typeof bun?.serve !== 'function') {\n throw new Error(\n 'serveDesktopAgent needs Bun.serve, which Cottontail and Bun provide. Pass serve to use another server.'\n );\n }\n return options => bun.serve!(options);\n}\n\nfunction isAddressInUse(error: unknown): boolean {\n const code = (error as { code?: unknown } | null)?.code;\n return code === 'EADDRINUSE' || /in use|EADDRINUSE/i.test(String((error as Error | null)?.message ?? error));\n}\n","/**\n * An application in the main process: windows, and the channels each\n * one is served.\n *\n * Nothing here imports Electrobun, and that is not fastidiousness. The\n * SDK is projected into a project by Hutch rather than installed from\n * a registry, so a package in\n * this workspace could not import it even if it wanted to. What the\n * application supplies instead is one function that opens a window,\n * which is the only Electrobun-shaped thing this needs, and which is\n * five lines at the call site:\n *\n * const app = createDesktopApp({\n * channels: window => [\n * { token: Catalogue, source: catalogue },\n * windowsChannel(app, window)\n * ],\n * open: (receive, handle) => {\n * const rpc = BrowserView.defineRPC<GessoWindowRPC>({\n * handlers: { requests: {}, messages: { gessoFrame: receive } }\n * });\n * const window = new BrowserWindow({\n * title: 'Notes',\n * url: withWindowRoute('views://mainview/index.html', handle.route),\n * rpc\n * });\n * return {\n * send: frame => window.webview.rpc.send.gessoFrame(frame),\n * close: () => window.close()\n * };\n * }\n * });\n * app.openWindow();\n *\n * The arrangement it buys: every\n * window is a replica of the same channels, so two windows agree by\n * construction rather than by synchronisation.\n */\nimport { channel, type ChannelToken, type ServedChannel } from 'gesso-framework';\nimport { BehaviorSubject, type Observable, type Subscription } from 'rxjs';\n\nimport type { GessoFrame } from './frames';\nimport { serveChannelsToWindow, type ChannelHost } from './main';\n\nexport { withWindowRoute } from './route';\n\n/** What the application does with the window it opened. */\nexport interface DesktopWindowTransport {\n /** Sends one frame to this window, over its own RPC. */\n send: (frame: GessoFrame) => void;\n /** Closes the native window. The adapter calls this; the platform may also close it on its own. */\n close: () => void;\n}\n\n/** A window this application opened. */\nexport interface DesktopWindowHandle {\n /** Stable for the life of the window, and never reused. */\n readonly id: number;\n /**\n * The application url the window was asked to open at, or null for\n * wherever the application starts.\n *\n * Set by `openWindow({ route })`, which is what a Cmd-click on an\n * in-app link comes to by default. The window cannot be told it\n * through a frame in time to start there, so `open` puts it on the\n * view's url, with `withWindowRoute`, and the window reads it back\n * with `windowRoute` before its shell starts.\n */\n readonly route: string | null;\n /** Closes the window and disposes the channels it was served. */\n close(): void;\n}\n\n/** What `openWindow` may be told about the window it opens. */\nexport interface DesktopWindowOptions {\n /**\n * The application url to open at, `/epic/BUD-12?story=BUD-13`. Carried\n * to `open` as `window.route`; see `DesktopWindowHandle.route`.\n */\n readonly route?: string;\n}\n\nexport interface DesktopAppOptions {\n /**\n * The channels each window is served.\n *\n * A function when a window needs a channel of its own, which is what\n * `windowsChannel` uses to give a window a way to close itself. It\n * is called once per window, and the observables it returns are\n * ordinarily the same ones every time: `provide` keeps a separate\n * record of what each client has seen, so sharing a source between\n * windows is what makes them agree.\n */\n channels: readonly ServedChannel[] | ((window: DesktopWindowHandle) => readonly ServedChannel[]);\n /**\n * Opens a native window.\n *\n * `receive` is what the window's frames must be fed into: wire it to\n * the RPC message the window sends frames on, before the window\n * opens, or the first handshake is lost.\n *\n * `window.route` is the application url the window should start at,\n * or null. Put it on the view's url with `withWindowRoute`, and read\n * it in the window with `windowRoute`; an `open` that ignores it\n * opens every window at the application's start, as before.\n */\n open: (receive: (frame: GessoFrame) => void, window: DesktopWindowHandle) => DesktopWindowTransport;\n /**\n * A url a window asked to have opened outside itself, with the\n * window that asked. `Utils.openExternal(url)` is what an Electrobun\n * application passes here.\n */\n onOpenUrl?: (url: string, window: DesktopWindowHandle) => void;\n /**\n * One of the application's own urls a window asked to have opened\n * somewhere new (a Cmd-click or a Ctrl-click on an in-app link), with\n * the window that asked.\n *\n * Defaults to `app.openWindow({ route: url })`: a new window of this\n * application, at that route, which is what a new tab is in an\n * application whose windows have none. An outbound url never comes\n * here; it is `onOpenUrl`'s, and goes to the person's browser.\n */\n onOpenRoute?: (url: string, window: DesktopWindowHandle) => void;\n /**\n * The appearance the platform is in, pushed to every window as it\n * changes and to a new window as it opens.\n *\n * The application supplies it because Electrobun does not: its SDK\n * has no appearance API at all, and the webview's own\n * `prefers-color-scheme` is wrong on WebKitGTK\n *. On a platform that has a\n * signal, this is where it goes; on one that does not, an\n * application setting is a perfectly good source.\n */\n colorScheme?: Observable<'light' | 'dark'>;\n /**\n * Called when the last window closes. A desktop application usually\n * stops here; one with a tray or a menu bar does not, which is why\n * this is a callback rather than an exit.\n */\n onLastWindowClosed?: () => void;\n /** Overrides the frame size messages are split at. Only a test should need to. */\n chunkBytes?: number;\n}\n\nexport interface DesktopApp {\n /**\n * Opens a window, serves it every channel, and returns its handle;\n * at `options.route` when one is given.\n */\n openWindow(options?: DesktopWindowOptions): DesktopWindowHandle;\n /** The windows open now, in the order they were opened. */\n readonly windows: readonly DesktopWindowHandle[];\n /** How many windows are open, as something a channel can publish. */\n readonly windowCount: Observable<number>;\n /** Closes every window and stops serving. */\n dispose(): void;\n}\n\nexport function createDesktopApp(options: DesktopAppOptions): DesktopApp {\n interface Entry {\n handle: DesktopWindowHandle;\n transport: DesktopWindowTransport;\n host: ChannelHost;\n }\n const entries = new Map<number, Entry>();\n /** One appearance subscription per window, ended when it closes. */\n const subscriptions = new Map<number, Subscription>();\n const order: DesktopWindowHandle[] = [];\n const count = new BehaviorSubject(0);\n let nextId = 1;\n let disposed = false;\n\n const forget = (id: number, closeNative: boolean): void => {\n const entry = entries.get(id);\n if (entry === undefined) {\n return;\n }\n entries.delete(id);\n subscriptions.get(id)?.unsubscribe();\n subscriptions.delete(id);\n const at = order.indexOf(entry.handle);\n if (at >= 0) {\n order.splice(at, 1);\n }\n entry.host.dispose();\n if (closeNative) {\n entry.transport.close();\n }\n count.next(order.length);\n if (order.length === 0 && !disposed) {\n options.onLastWindowClosed?.();\n }\n };\n\n const app: DesktopApp = {\n openWindow(windowOptions: DesktopWindowOptions = {}): DesktopWindowHandle {\n if (disposed) {\n throw new Error('This desktop application has been disposed; it cannot open a window.');\n }\n const id = nextId++;\n const handle: DesktopWindowHandle = {\n id,\n route: windowOptions.route ?? null,\n close: () => forget(id, true)\n };\n\n // The host exists before the window does, because `open` may\n // deliver a frame synchronously and a window whose first\n // handshake was dropped never replicates anything.\n let transport: DesktopWindowTransport | undefined;\n const host = serveChannelsToWindow(\n typeof options.channels === 'function' ? options.channels(handle) : options.channels,\n {\n send: frame => transport?.send(frame),\n chunkBytes: options.chunkBytes,\n onOpenUrl: url => options.onOpenUrl?.(url, handle),\n onOpenRoute: url => {\n if (options.onOpenRoute !== undefined) {\n options.onOpenRoute(url, handle);\n } else if (!disposed) {\n app.openWindow({ route: url });\n }\n }\n }\n );\n entries.set(id, { handle, transport: { send: () => {}, close: () => {} }, host });\n order.push(handle);\n\n // Anything pushed before the window has spoken is lost: a\n // webview's RPC is not listening until its page has loaded, and\n // the window opens well before that. Channels do not notice,\n // because a channel starts with the window asking. The\n // appearance is the one thing this side sends first, so it waits\n // for the window's first frame, whatever that frame is. A native\n // window found this; a spec with a transport that was live\n // immediately could not.\n let greeted = false;\n let latest: 'light' | 'dark' | undefined;\n transport = options.open(frame => {\n if (!greeted) {\n greeted = true;\n if (latest !== undefined) {\n host.setColorScheme(latest);\n }\n }\n host.receive(frame);\n }, handle);\n entries.set(id, { handle, transport, host });\n const appearance = options.colorScheme?.subscribe(scheme => {\n latest = scheme;\n if (greeted) {\n host.setColorScheme(scheme);\n }\n });\n if (appearance !== undefined) {\n subscriptions.set(id, appearance);\n }\n count.next(order.length);\n return handle;\n },\n get windows(): readonly DesktopWindowHandle[] {\n return [...order];\n },\n windowCount: count.asObservable(),\n dispose(): void {\n disposed = true;\n for (const id of new Set(entries.keys())) {\n forget(id, true);\n }\n count.complete();\n }\n };\n\n return app;\n}\n\n/** What a window can see and do about the windows of its application. */\nexport interface DesktopWindowsView {\n /** How many windows this application has open. */\n count: number;\n /** Which one this is, so a screen can say \"window 2 of 3\" without asking. */\n id: number;\n}\n\nexport interface DesktopWindowsCommands {\n open: () => void;\n closeThis: () => void;\n}\n\n/**\n * The channel a window opens another window through.\n *\n * It exists because opening a window is the one native act a screen\n * genuinely needs, and going through a channel keeps the view layer\n * from importing an adapter. the \"native menus bound to\n * store actions\" is the same idea from the other end, and on Linux it\n * is the only end: the runtime has no application menus there, so a\n * menu is a component and this is what it calls.\n */\nexport const DesktopWindows: ChannelToken<DesktopWindowsView, DesktopWindowsCommands> = channel<\n DesktopWindowsView,\n DesktopWindowsCommands\n>('gesso:windows', { count: 0, id: 0 });\n\n/** Serves `DesktopWindows` to one window. Put it in `channels`. */\nexport function windowsChannel(app: DesktopApp, window: DesktopWindowHandle): ServedChannel {\n return {\n token: DesktopWindows,\n source: {\n view: {\n count: app.windowCount,\n id: new BehaviorSubject(window.id)\n },\n commands: {\n open: () => {\n app.openWindow();\n },\n closeThis: () => {\n window.close();\n }\n }\n }\n };\n}\n\nexport {\n messageBoxConfirm,\n serveDesktopAgent,\n type DesktopAgent,\n type DesktopAgentOptions,\n type DesktopServe,\n type ShowMessageBox\n} from './agent';\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"desktop.js","names":[],"sources":["../src/agent.ts","../src/desktop.ts"],"sourcesContent":["import type { ServedChannel } from 'gesso-framework';\nimport {\n agentSurface,\n mcpHandler,\n type AgentConfirmation,\n type AgentSurface,\n type McpHandlerOptions\n} from 'gesso-framework/agent';\n\n/**\n * A desktop application's channels, served to AI agents over MCP.\n *\n * The main process already holds every channel's source: it is what\n * `createDesktopApp` serves to each window. So it is also where an\n * agent connects. This serves those same sources with MCP's HTTP\n * transport on the person's own machine, through `Bun.serve`, which\n * Cottontail provides as Bun does, and an agent such as Claude Code\n * connects by URL:\n *\n * const agent = serveDesktopAgent([counter], { name: 'my-app', confirm: messageBoxConfirm(Utils.showMessageBox) });\n * // claude mcp add --transport http my-app http://127.0.0.1:7310/mcp\n *\n * The channels are passed rather than read from the app, because\n * `createDesktopApp` takes them per window, and the per-window ones\n * (`windowsChannel`) are about a window an agent does not have.\n *\n * What it offers is the channel tools only. The screen is in each\n * window's render worker, out of the main process's reach, so the\n * screen tools a web app offers are not here.\n *\n * The descriptions come from `channelSchema`. A main process is bundled\n * by Electrobun's own build, which `gesso-vite-plugin` never sees, so\n * the template runs `gesso-channels` to write a module that describes\n * the contracts, and imports it.\n */\n\n/** A `Bun.serve`, as far as this uses one. */\nexport type DesktopServe = (options: {\n hostname: string;\n port: number;\n fetch: (request: Request) => Promise<Response>;\n}) => { stop(closeActiveConnections?: boolean): void };\n\nexport interface DesktopAgentOptions extends Omit<McpHandlerOptions, 'allowedOrigins'> {\n /**\n * The port to listen on (default 7310). When it is taken, the next\n * nine are tried in turn, so a second copy of the app still serves;\n * `url` says which one it got.\n */\n port?: number;\n /** Default `127.0.0.1`: reachable from this machine only. */\n hostname?: string;\n /**\n * Asks the person whether a `@confirm` command may be sent. Without\n * it such a command is refused. `messageBoxConfirm` makes one from\n * Electrobun's native dialog.\n */\n confirm?: (request: AgentConfirmation) => boolean | Promise<boolean>;\n /** The server to start. Defaults to the runtime's `Bun.serve`. */\n serve?: DesktopServe;\n}\n\nexport interface DesktopAgent {\n /** Where an agent connects: `http://127.0.0.1:<port>/mcp`. */\n readonly url: string;\n readonly surface: AgentSurface;\n /** Stops serving and stops following the channels. */\n stop(): void;\n}\n\nconst DEFAULT_PORT = 7310;\nconst PORTS_TRIED = 10;\n\nexport function serveDesktopAgent(channels: readonly ServedChannel[], options: DesktopAgentOptions = {}): DesktopAgent {\n const serve = options.serve ?? bunServe();\n const hostname = options.hostname ?? '127.0.0.1';\n const first = options.port ?? DEFAULT_PORT;\n const surface = agentSurface(channels, options.confirm === undefined ? {} : { confirm: options.confirm });\n // A desktop app has no browser pages of its own to let in, so every\n // request carrying an Origin is refused: that is a web page the person\n // has open, reaching for their machine.\n const fetch = mcpHandler(surface, { ...options, allowedOrigins: [] });\n\n let lastError: unknown;\n for (let port = first; port < first + PORTS_TRIED; port++) {\n try {\n const server = serve({ hostname, port, fetch });\n return {\n url: `http://${hostname}:${port}/mcp`,\n surface,\n stop: () => {\n server.stop(true);\n surface.dispose();\n }\n };\n } catch (error) {\n if (!isAddressInUse(error)) {\n surface.dispose();\n throw error;\n }\n lastError = error;\n }\n }\n surface.dispose();\n throw new Error(\n `Ports ${first} to ${first + PORTS_TRIED - 1} are all in use, so agents cannot be served. ` +\n `Pass another port to serveDesktopAgent. (${String(lastError)})`\n );\n}\n\n/** The options Electrobun's `Utils.showMessageBox` takes, as far as this uses them. */\nexport type ShowMessageBox = (options: {\n type?: 'info' | 'warning' | 'error' | 'question';\n title?: string;\n message?: string;\n detail?: string;\n buttons?: string[];\n defaultId?: number;\n cancelId?: number;\n}) => Promise<{ response: number }>;\n\n/**\n * A `confirm` that asks with the operating system's own dialog.\n *\n * Takes Electrobun's `Utils.showMessageBox` rather than importing it,\n * because Electrobun is a toolchain a project is projected into, not a\n * package this one can depend on.\n *\n * Everything the person needs to decide is in `message`, the command's\n * description and its arguments included, because macOS shows no\n * `detail`: a dialog that asked there showed only \"wants to\n * createBranch in workspace\", with nothing to say which branch.\n *\n * Decline is the first button. macOS makes the first button the\n * default, whatever `defaultId` says, so with Allow first, Enter\n * approved; first is also what `defaultId` and `cancelId` name, so\n * Enter, Escape and closing the dialog decline on every platform.\n */\nexport function messageBoxConfirm(showMessageBox: ShowMessageBox): (request: AgentConfirmation) => Promise<boolean> {\n return async request => {\n const argumentLines = Object.entries(request.arguments).map(([name, value]) => `${name}: ${JSON.stringify(value)}`);\n const message = [\n `An AI agent wants to ${request.command} in ${request.channel}.${request.destructive ? ' This cannot be undone.' : ''}`,\n request.description,\n argumentLines.join('\\n')\n ]\n .filter((part): part is string => part !== undefined && part !== '')\n .join('\\n\\n');\n const { response } = await showMessageBox({\n type: request.destructive ? 'warning' : 'question',\n title: 'An AI agent is asking',\n message,\n buttons: ['Decline', 'Allow'],\n defaultId: 0,\n cancelId: 0\n });\n return response === 1;\n };\n}\n\nfunction bunServe(): DesktopServe {\n const bun = (globalThis as { Bun?: { serve?: DesktopServe } }).Bun;\n if (typeof bun?.serve !== 'function') {\n throw new Error(\n 'serveDesktopAgent needs Bun.serve, which Cottontail and Bun provide. Pass serve to use another server.'\n );\n }\n return options => bun.serve!(options);\n}\n\nfunction isAddressInUse(error: unknown): boolean {\n const code = (error as { code?: unknown } | null)?.code;\n return code === 'EADDRINUSE' || /in use|EADDRINUSE/i.test(String((error as Error | null)?.message ?? error));\n}\n","/**\n * An application in the main process: windows, and the channels each\n * one is served.\n *\n * Nothing here imports Electrobun, and that is not fastidiousness. The\n * SDK is projected into a project by Hutch rather than installed from\n * a registry, so a package in\n * this workspace could not import it even if it wanted to. What the\n * application supplies instead is one function that opens a window,\n * which is the only Electrobun-shaped thing this needs, and which is\n * five lines at the call site:\n *\n * const app = createDesktopApp({\n * channels: window => [\n * { token: Catalogue, source: catalogue },\n * windowsChannel(app, window)\n * ],\n * open: (receive, handle) => {\n * const rpc = BrowserView.defineRPC<GessoWindowRPC>({\n * handlers: { requests: {}, messages: { gessoFrame: receive } }\n * });\n * const window = new BrowserWindow({\n * title: 'Notes',\n * url: withWindowRoute('views://mainview/index.html', handle.route),\n * rpc\n * });\n * return {\n * send: frame => window.webview.rpc.send.gessoFrame(frame),\n * close: () => window.close()\n * };\n * }\n * });\n * app.openWindow();\n *\n * The arrangement it buys: every\n * window is a replica of the same channels, so two windows agree by\n * construction rather than by synchronisation.\n */\nimport { channel, type ChannelToken, type ServedChannel } from 'gesso-framework';\nimport { BehaviorSubject, type Observable, type Subscription } from 'rxjs';\n\nimport type { GessoFrame } from './frames';\nimport { serveChannelsToWindow, type ChannelHost } from './main';\n\nexport { withWindowRoute } from './route';\n\n/** What the application does with the window it opened. */\nexport interface DesktopWindowTransport {\n /** Sends one frame to this window, over its own RPC. */\n send: (frame: GessoFrame) => void;\n /** Closes the native window. The adapter calls this; the platform may also close it on its own. */\n close: () => void;\n}\n\n/** A window this application opened. */\nexport interface DesktopWindowHandle {\n /** Stable for the life of the window, and never reused. */\n readonly id: number;\n /**\n * The application url the window was asked to open at, or null for\n * wherever the application starts.\n *\n * Set by `openWindow({ route })`, which is what a Cmd-click on an\n * in-app link comes to by default. The window cannot be told it\n * through a frame in time to start there, so `open` puts it on the\n * view's url, with `withWindowRoute`, and the window reads it back\n * with `windowRoute` before its shell starts.\n */\n readonly route: string | null;\n /** Closes the window and disposes the channels it was served. */\n close(): void;\n}\n\n/** What `openWindow` may be told about the window it opens. */\nexport interface DesktopWindowOptions {\n /**\n * The application url to open at, `/epic/BUD-12?story=BUD-13`. Carried\n * to `open` as `window.route`; see `DesktopWindowHandle.route`.\n */\n readonly route?: string;\n}\n\nexport interface DesktopAppOptions {\n /**\n * The channels each window is served.\n *\n * A function when a window needs a channel of its own, which is what\n * `windowsChannel` uses to give a window a way to close itself. It\n * is called once per window, and the observables it returns are\n * ordinarily the same ones every time: `provide` keeps a separate\n * record of what each client has seen, so sharing a source between\n * windows is what makes them agree.\n */\n channels: readonly ServedChannel[] | ((window: DesktopWindowHandle) => readonly ServedChannel[]);\n /**\n * Opens a native window.\n *\n * `receive` is what the window's frames must be fed into: wire it to\n * the RPC message the window sends frames on, before the window\n * opens, or the first handshake is lost.\n *\n * `window.route` is the application url the window should start at,\n * or null. Put it on the view's url with `withWindowRoute`, and read\n * it in the window with `windowRoute`; an `open` that ignores it\n * opens every window at the application's start, as before.\n */\n open: (receive: (frame: GessoFrame) => void, window: DesktopWindowHandle) => DesktopWindowTransport;\n /**\n * A url a window asked to have opened outside itself, with the\n * window that asked. `Utils.openExternal(url)` is what an Electrobun\n * application passes here.\n */\n onOpenUrl?: (url: string, window: DesktopWindowHandle) => void;\n /**\n * One of the application's own urls a window asked to have opened\n * somewhere new (a Cmd-click or a Ctrl-click on an in-app link), with\n * the window that asked.\n *\n * Defaults to `app.openWindow({ route: url })`: a new window of this\n * application, at that route, which is what a new tab is in an\n * application whose windows have none. An outbound url never comes\n * here; it is `onOpenUrl`'s, and goes to the person's browser.\n */\n onOpenRoute?: (url: string, window: DesktopWindowHandle) => void;\n /**\n * The appearance the platform is in, pushed to every window as it\n * changes and to a new window as it opens.\n *\n * The application supplies it because Electrobun does not: its SDK\n * has no appearance API at all, and the webview's own\n * `prefers-color-scheme` is wrong on WebKitGTK\n *. On a platform that has a\n * signal, this is where it goes; on one that does not, an\n * application setting is a perfectly good source.\n */\n colorScheme?: Observable<'light' | 'dark'>;\n /**\n * Called when the last window closes. A desktop application usually\n * stops here; one with a tray or a menu bar does not, which is why\n * this is a callback rather than an exit.\n */\n onLastWindowClosed?: () => void;\n /** Overrides the frame size messages are split at. Only a test should need to. */\n chunkBytes?: number;\n}\n\nexport interface DesktopApp {\n /**\n * Opens a window, serves it every channel, and returns its handle;\n * at `options.route` when one is given.\n */\n openWindow(options?: DesktopWindowOptions): DesktopWindowHandle;\n /** The windows open now, in the order they were opened. */\n readonly windows: readonly DesktopWindowHandle[];\n /** How many windows are open, as something a channel can publish. */\n readonly windowCount: Observable<number>;\n /** Closes every window and stops serving. */\n dispose(): void;\n}\n\nexport function createDesktopApp(options: DesktopAppOptions): DesktopApp {\n interface Entry {\n handle: DesktopWindowHandle;\n transport: DesktopWindowTransport;\n host: ChannelHost;\n }\n const entries = new Map<number, Entry>();\n /** One appearance subscription per window, ended when it closes. */\n const subscriptions = new Map<number, Subscription>();\n const order: DesktopWindowHandle[] = [];\n const count = new BehaviorSubject(0);\n let nextId = 1;\n let disposed = false;\n\n const forget = (id: number, closeNative: boolean): void => {\n const entry = entries.get(id);\n if (entry === undefined) {\n return;\n }\n entries.delete(id);\n subscriptions.get(id)?.unsubscribe();\n subscriptions.delete(id);\n const at = order.indexOf(entry.handle);\n if (at >= 0) {\n order.splice(at, 1);\n }\n entry.host.dispose();\n if (closeNative) {\n entry.transport.close();\n }\n count.next(order.length);\n if (order.length === 0 && !disposed) {\n options.onLastWindowClosed?.();\n }\n };\n\n const app: DesktopApp = {\n openWindow(windowOptions: DesktopWindowOptions = {}): DesktopWindowHandle {\n if (disposed) {\n throw new Error('This desktop application has been disposed; it cannot open a window.');\n }\n const id = nextId++;\n const handle: DesktopWindowHandle = {\n id,\n route: windowOptions.route ?? null,\n close: () => forget(id, true)\n };\n\n // The host exists before the window does, because `open` may\n // deliver a frame synchronously and a window whose first\n // handshake was dropped never replicates anything.\n let transport: DesktopWindowTransport | undefined;\n const host = serveChannelsToWindow(\n typeof options.channels === 'function' ? options.channels(handle) : options.channels,\n {\n send: frame => transport?.send(frame),\n chunkBytes: options.chunkBytes,\n onOpenUrl: url => options.onOpenUrl?.(url, handle),\n onOpenRoute: url => {\n if (options.onOpenRoute !== undefined) {\n options.onOpenRoute(url, handle);\n } else if (!disposed) {\n app.openWindow({ route: url });\n }\n }\n }\n );\n entries.set(id, { handle, transport: { send: () => {}, close: () => {} }, host });\n order.push(handle);\n\n // Anything pushed before the window has spoken is lost: a\n // webview's RPC is not listening until its page has loaded, and\n // the window opens well before that. Channels do not notice,\n // because a channel starts with the window asking. The\n // appearance is the one thing this side sends first, so it waits\n // for the window's first frame, whatever that frame is. A native\n // window found this; a spec with a transport that was live\n // immediately could not.\n let greeted = false;\n let latest: 'light' | 'dark' | undefined;\n transport = options.open(frame => {\n if (!greeted) {\n greeted = true;\n if (latest !== undefined) {\n host.setColorScheme(latest);\n }\n }\n host.receive(frame);\n }, handle);\n entries.set(id, { handle, transport, host });\n const appearance = options.colorScheme?.subscribe(scheme => {\n latest = scheme;\n if (greeted) {\n host.setColorScheme(scheme);\n }\n });\n if (appearance !== undefined) {\n subscriptions.set(id, appearance);\n }\n count.next(order.length);\n return handle;\n },\n get windows(): readonly DesktopWindowHandle[] {\n return [...order];\n },\n windowCount: count.asObservable(),\n dispose(): void {\n disposed = true;\n for (const id of new Set(entries.keys())) {\n forget(id, true);\n }\n count.complete();\n }\n };\n\n return app;\n}\n\n/** What a window can see and do about the windows of its application. */\nexport interface DesktopWindowsView {\n /** How many windows this application has open. */\n count: number;\n /** Which one this is, so a screen can say \"window 2 of 3\" without asking. */\n id: number;\n}\n\nexport interface DesktopWindowsCommands {\n open: () => void;\n closeThis: () => void;\n}\n\n/**\n * The channel a window opens another window through.\n *\n * It exists because opening a window is the one native act a screen\n * genuinely needs, and going through a channel keeps the view layer\n * from importing an adapter. the \"native menus bound to\n * store actions\" is the same idea from the other end, and on Linux it\n * is the only end: the runtime has no application menus there, so a\n * menu is a component and this is what it calls.\n */\nexport const DesktopWindows: ChannelToken<DesktopWindowsView, DesktopWindowsCommands> = channel<\n DesktopWindowsView,\n DesktopWindowsCommands\n>('gesso:windows', { count: 0, id: 0 });\n\n/** Serves `DesktopWindows` to one window. Put it in `channels`. */\nexport function windowsChannel(app: DesktopApp, window: DesktopWindowHandle): ServedChannel {\n return {\n token: DesktopWindows,\n source: {\n view: {\n count: app.windowCount,\n id: new BehaviorSubject(window.id)\n },\n commands: {\n open: () => {\n app.openWindow();\n },\n closeThis: () => {\n window.close();\n }\n }\n }\n };\n}\n\nexport {\n messageBoxConfirm,\n serveDesktopAgent,\n type DesktopAgent,\n type DesktopAgentOptions,\n type DesktopServe,\n type ShowMessageBox\n} from './agent';\n"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as isGessoFrame, i as frameData, n as FrameAssembler, r as GessoFrame, t as DEFAULT_CHUNK_BYTES } from "./frames-
|
|
1
|
+
import { a as isGessoFrame, i as frameData, n as FrameAssembler, r as GessoFrame, t as DEFAULT_CHUNK_BYTES } from "./frames-CsYW_e7F.js";
|
|
2
2
|
export { DEFAULT_CHUNK_BYTES, FrameAssembler, type GessoFrame, frameData, isGessoFrame };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DEFAULT_CHUNK_BYTES, FrameAssembler, frameData, isGessoFrame } from "gesso-framework/remote";
|
|
2
2
|
export { DEFAULT_CHUNK_BYTES, FrameAssembler, frameData, isGessoFrame };
|
package/dist/main.d.ts
CHANGED
|
@@ -1,54 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RemoteChannelHost, RemoteChannelHostOptions } from "gesso-framework/remote";
|
|
2
2
|
import { ServedChannel } from "gesso-framework";
|
|
3
3
|
//#region src/main.d.ts
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* its own streams, and `provide` already keeps a separate record of
|
|
9
|
-
* what each client has seen, so two windows agree without anything
|
|
10
|
-
* here arranging it.
|
|
11
|
-
*/
|
|
12
|
-
send: (frame: GessoFrame) => void;
|
|
13
|
-
/** Overrides `DEFAULT_CHUNK_BYTES`. Only a test should need to. */
|
|
14
|
-
chunkBytes?: number;
|
|
15
|
-
/**
|
|
16
|
-
* A url the window asked to have opened outside itself.
|
|
17
|
-
*
|
|
18
|
-
* `Utils.openExternal(url)` is what an Electrobun application passes
|
|
19
|
-
* here. It is not called for the application: opening something is
|
|
20
|
-
* an act, and which urls an application is willing to hand to the
|
|
21
|
-
* operating system is the application's decision.
|
|
22
|
-
*/
|
|
23
|
-
onOpenUrl?: (url: string) => void;
|
|
24
|
-
/**
|
|
25
|
-
* One of the application's own urls the window asked to have opened
|
|
26
|
-
* in a new window: a Cmd-click on an in-app link, sent by the view
|
|
27
|
-
* bridge's `openRoute`.
|
|
28
|
-
*
|
|
29
|
-
* Not called for the application either. `createDesktopApp` answers
|
|
30
|
-
* it by opening a window at that route; a host built on this alone
|
|
31
|
-
* decides for itself.
|
|
32
|
-
*/
|
|
33
|
-
onOpenRoute?: (url: string) => void;
|
|
34
|
-
}
|
|
35
|
-
interface ChannelHost {
|
|
36
|
-
/** Call from the RPC handler that receives frames from the window. */
|
|
37
|
-
receive(frame: GessoFrame): void;
|
|
38
|
-
/** Tells the window which appearance the platform is in. */
|
|
39
|
-
setColorScheme(scheme: 'light' | 'dark'): void;
|
|
40
|
-
/** Stops serving and disposes every channel this host provided. */
|
|
41
|
-
dispose(): void;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Serves an application's channels to one window.
|
|
45
|
-
*
|
|
46
|
-
* const host = serveChannelsToWindow(
|
|
47
|
-
* [{ token: Catalogue, source: { view: { … }, commands: { … } } }],
|
|
48
|
-
* { send: frame => window.webview.rpc.send.gessoFrame(frame) }
|
|
49
|
-
* );
|
|
50
|
-
*/
|
|
51
|
-
declare function serveChannelsToWindow(channels: readonly ServedChannel[], options: ChannelHostOptions): ChannelHost;
|
|
4
|
+
type ChannelHostOptions = RemoteChannelHostOptions;
|
|
5
|
+
type ChannelHost = RemoteChannelHost;
|
|
6
|
+
/** `serveRemoteChannels`, by the name the Electrobun template uses. */
|
|
7
|
+
declare const serveChannelsToWindow: (channels: readonly ServedChannel[], options: ChannelHostOptions) => ChannelHost;
|
|
52
8
|
//#endregion
|
|
53
9
|
export { ChannelHost, ChannelHostOptions, serveChannelsToWindow };
|
|
54
10
|
//# sourceMappingURL=main.d.ts.map
|
package/dist/main.js
CHANGED
|
@@ -1,106 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { serveChannels } from "gesso-framework";
|
|
1
|
+
import { serveRemoteChannels } from "gesso-framework/remote";
|
|
3
2
|
//#region src/main.ts
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
-
*
|
|
7
|
-
* `serveChannels` publishes an application's channels over a handshake
|
|
8
|
-
* that arrives on a worker's global scope with a `MessagePort`
|
|
9
|
-
* attached. No process boundary outside a worker can carry a port, so
|
|
10
|
-
* this synthesises both: an `open` frame becomes the handshake, and
|
|
11
|
-
* the port it hands over writes back as frames on the same stream.
|
|
12
|
-
*
|
|
13
|
-
* Nothing in `serveChannels`, `provide` or a view model changes for
|
|
14
|
-
* this. That is the point of the seam: the application layer does not
|
|
15
|
-
* learn it is talking to a window instead of a page.
|
|
16
|
-
*/
|
|
17
|
-
/**
|
|
18
|
-
* Serves an application's channels to one window.
|
|
19
|
-
*
|
|
20
|
-
* const host = serveChannelsToWindow(
|
|
21
|
-
* [{ token: Catalogue, source: { view: { … }, commands: { … } } }],
|
|
22
|
-
* { send: frame => window.webview.rpc.send.gessoFrame(frame) }
|
|
23
|
-
* );
|
|
24
|
-
*/
|
|
25
|
-
function serveChannelsToWindow(channels, options) {
|
|
26
|
-
const chunkBytes = options.chunkBytes ?? 1048576;
|
|
27
|
-
const assembler = new FrameAssembler();
|
|
28
|
-
/** The synthetic port each stream is served through. */
|
|
29
|
-
const ports = /* @__PURE__ */ new Map();
|
|
30
|
-
const host = { onmessage: null };
|
|
31
|
-
const stop = serveChannels(channels, host);
|
|
32
|
-
return {
|
|
33
|
-
setColorScheme(scheme) {
|
|
34
|
-
options.send(frameControl("colorScheme", { scheme }));
|
|
35
|
-
},
|
|
36
|
-
receive(frame) {
|
|
37
|
-
if (frame.kind === "control") {
|
|
38
|
-
if (frame.name === "openUrl" || frame.name === "openRoute") {
|
|
39
|
-
const payload = JSON.parse(frame.body);
|
|
40
|
-
if (typeof payload.url === "string") (frame.name === "openUrl" ? options.onOpenUrl : options.onOpenRoute)?.(payload.url);
|
|
41
|
-
}
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
if (frame.kind === "open") {
|
|
45
|
-
const port = new StreamPort(frame.stream, options.send, chunkBytes);
|
|
46
|
-
ports.set(frame.stream, port);
|
|
47
|
-
host.onmessage?.({
|
|
48
|
-
data: {
|
|
49
|
-
type: "gesso:port",
|
|
50
|
-
key: frame.name
|
|
51
|
-
},
|
|
52
|
-
ports: [port]
|
|
53
|
-
});
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
if (frame.kind === "close") {
|
|
57
|
-
assembler.forget(frame.stream);
|
|
58
|
-
ports.delete(frame.stream);
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
const value = assembler.take(frame);
|
|
62
|
-
if (value === void 0) return;
|
|
63
|
-
const port = ports.get(frame.stream);
|
|
64
|
-
if (port === void 0) throw new Error(`A frame arrived for stream ${frame.stream}, which was never opened. The window and the main process disagree about what is running.`);
|
|
65
|
-
port.deliver(value);
|
|
66
|
-
},
|
|
67
|
-
dispose() {
|
|
68
|
-
for (const stream of ports.keys()) options.send({
|
|
69
|
-
kind: "close",
|
|
70
|
-
stream
|
|
71
|
-
});
|
|
72
|
-
ports.clear();
|
|
73
|
-
stop();
|
|
74
|
-
host.onmessage = null;
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* One channel's port, as the application layer sees it.
|
|
80
|
-
*
|
|
81
|
-
* `provide` sets `onmessage` and calls `postMessage`, and that is the
|
|
82
|
-
* entire surface it uses, which is why a channel can be served over
|
|
83
|
-
* something that is not a `MessagePort` at all.
|
|
84
|
-
*/
|
|
85
|
-
var StreamPort = class {
|
|
86
|
-
stream;
|
|
87
|
-
send;
|
|
88
|
-
chunkBytes;
|
|
89
|
-
onmessage = null;
|
|
90
|
-
constructor(stream, send, chunkBytes) {
|
|
91
|
-
this.stream = stream;
|
|
92
|
-
this.send = send;
|
|
93
|
-
this.chunkBytes = chunkBytes;
|
|
94
|
-
}
|
|
95
|
-
postMessage(value) {
|
|
96
|
-
for (const frame of frameData(this.stream, value, this.chunkBytes)) this.send(frame);
|
|
97
|
-
}
|
|
98
|
-
deliver(value) {
|
|
99
|
-
this.onmessage?.({ data: value });
|
|
100
|
-
}
|
|
101
|
-
/** `provide` closes a port it is done with; there is nothing to close. */
|
|
102
|
-
close() {}
|
|
103
|
-
};
|
|
3
|
+
/** `serveRemoteChannels`, by the name the Electrobun template uses. */
|
|
4
|
+
const serveChannelsToWindow = serveRemoteChannels;
|
|
104
5
|
//#endregion
|
|
105
6
|
export { serveChannelsToWindow };
|
|
106
7
|
|
package/dist/main.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.js","names":[],"sources":["../src/main.ts"],"sourcesContent":["/**\n * The main process's half of the bridge.\n *\n *
|
|
1
|
+
{"version":3,"file":"main.js","names":[],"sources":["../src/main.ts"],"sourcesContent":["/**\n * The main process's half of the bridge.\n *\n * The host is `serveRemoteChannels` from `gesso-framework/remote`,\n * which serves an application's channels to a page in another process\n * over any transport that can carry a string. In an Electrobun\n * application the page is a window and the transport is its RPC: pass\n * `window.webview.rpc.send.<name>` as `send`, and feed every frame the\n * window sends to `receive`. This entry keeps the names a desktop\n * application has always used for it.\n */\nimport type { ServedChannel } from 'gesso-framework';\nimport { serveRemoteChannels, type RemoteChannelHost, type RemoteChannelHostOptions } from 'gesso-framework/remote';\n\nexport type ChannelHostOptions = RemoteChannelHostOptions;\nexport type ChannelHost = RemoteChannelHost;\n\n/** `serveRemoteChannels`, by the name the Electrobun template uses. */\nexport const serveChannelsToWindow: (channels: readonly ServedChannel[], options: ChannelHostOptions) => ChannelHost =\n serveRemoteChannels;\n"],"mappings":";;;AAkBA,MAAa,wBACX"}
|
package/dist/view.d.ts
CHANGED
|
@@ -1,64 +1,10 @@
|
|
|
1
|
-
import { r as GessoFrame } from "./frames-Bx7SnqWg.js";
|
|
2
1
|
import { t as windowRoute } from "./route-CNeMnj_6.js";
|
|
3
|
-
import {
|
|
2
|
+
import { RemoteBridge, RemoteBridgeOptions } from "gesso-framework/remote";
|
|
4
3
|
//#region src/view.d.ts
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* declared. A function rather than the RPC object, so this package
|
|
10
|
-
* imports nothing from Electrobun's SDK and can be specified without
|
|
11
|
-
* a window.
|
|
12
|
-
*/
|
|
13
|
-
send: (frame: GessoFrame) => void;
|
|
14
|
-
/** Overrides `DEFAULT_CHUNK_BYTES`. Only a test should need to. */
|
|
15
|
-
chunkBytes?: number;
|
|
16
|
-
/**
|
|
17
|
-
* The appearance the platform is in, as the main process reports it.
|
|
18
|
-
*
|
|
19
|
-
* Wire it to `app.setColorScheme`. It exists because
|
|
20
|
-
* `prefers-color-scheme` is not to be trusted in every webview: on
|
|
21
|
-
* WebKitGTK it reported light on a desktop that was in dark mode
|
|
22
|
-
*, and a shell that believes
|
|
23
|
-
* it is a browser gets the appearance wrong there.
|
|
24
|
-
*/
|
|
25
|
-
onColorScheme?: (scheme: 'light' | 'dark') => void;
|
|
26
|
-
}
|
|
27
|
-
interface ElectrobunBridge {
|
|
28
|
-
/**
|
|
29
|
-
* Hand this to the shell as its application layer:
|
|
30
|
-
*
|
|
31
|
-
* createApp({ renderWorker: …, appLogicWorker: bridge.endpoint })
|
|
32
|
-
*
|
|
33
|
-
* The shell wires it exactly as it wires a worker it was handed, and
|
|
34
|
-
* never closes it.
|
|
35
|
-
*/
|
|
36
|
-
readonly endpoint: AppLogicEndpoint;
|
|
37
|
-
/** Call from the RPC handler that receives frames from the main process. */
|
|
38
|
-
receive(frame: GessoFrame): void;
|
|
39
|
-
/**
|
|
40
|
-
* Hands a url to the main process to open outside the window.
|
|
41
|
-
*
|
|
42
|
-
* Pass it as `onOpenUrl` to the shell: `window.open` in a webview
|
|
43
|
-
* opens another webview or nothing at all, and a link in a desktop
|
|
44
|
-
* application belongs in the person's browser.
|
|
45
|
-
*/
|
|
46
|
-
openUrl(url: string): void;
|
|
47
|
-
/**
|
|
48
|
-
* Asks the main process to open this application at one of its own
|
|
49
|
-
* urls in a new window.
|
|
50
|
-
*
|
|
51
|
-
* Pass it as `onOpenRoute` to the shell. It is what a Cmd-click on an
|
|
52
|
-
* in-app link means in a desktop application: a window has no tabs,
|
|
53
|
-
* and a `window.open` here would open a bare webview the application
|
|
54
|
-
* does not serve. `createDesktopApp` opens the window, at that route,
|
|
55
|
-
* unless the application said otherwise.
|
|
56
|
-
*/
|
|
57
|
-
openRoute(url: string): void;
|
|
58
|
-
/** Closes every stream and stops pumping. */
|
|
59
|
-
dispose(): void;
|
|
60
|
-
}
|
|
61
|
-
declare function createElectrobunBridge(options: ElectrobunBridgeOptions): ElectrobunBridge;
|
|
4
|
+
type ElectrobunBridgeOptions = RemoteBridgeOptions;
|
|
5
|
+
type ElectrobunBridge = RemoteBridge;
|
|
6
|
+
/** `createRemoteBridge`, by the name the Electrobun template uses. */
|
|
7
|
+
declare const createElectrobunBridge: (options: ElectrobunBridgeOptions) => ElectrobunBridge;
|
|
62
8
|
//#endregion
|
|
63
9
|
export { ElectrobunBridge, ElectrobunBridgeOptions, createElectrobunBridge, windowRoute };
|
|
64
10
|
//# sourceMappingURL=view.d.ts.map
|
package/dist/view.js
CHANGED
|
@@ -1,111 +1,18 @@
|
|
|
1
|
-
import { i as frameData, n as FrameAssembler, r as frameControl } from "./frames-BQisaYy-.js";
|
|
2
1
|
import { t as windowRoute } from "./route-B2qU38dY.js";
|
|
3
|
-
import {
|
|
2
|
+
import { createRemoteBridge } from "gesso-framework/remote";
|
|
4
3
|
//#region src/view.ts
|
|
5
4
|
/**
|
|
6
5
|
* The webview's half of the bridge.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* It is a transport and nothing else. It serializes a message it never
|
|
16
|
-
* inspects, and it holds no channel, no token and no patch. Anything
|
|
17
|
-
* that needs to understand a payload to route it belongs on the other
|
|
18
|
-
* side of the bridge, and if that ever changes here, the wrong thing
|
|
19
|
-
* is happening on the thread that must stay free for input.
|
|
7
|
+
* The bridge is `createRemoteBridge` from `gesso-framework/remote`,
|
|
8
|
+
* which carries a render worker's channels over any transport that
|
|
9
|
+
* can carry a string. In a desktop window that transport is
|
|
10
|
+
* Electrobun's RPC: pass `view.rpc.send.<name>` as `send`, and feed
|
|
11
|
+
* every frame the main process sends to `receive`. This entry keeps the
|
|
12
|
+
* names a desktop application has always used for it.
|
|
20
13
|
*/
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const assembler = new FrameAssembler();
|
|
24
|
-
/** The render worker's port for each stream this side opened. */
|
|
25
|
-
const ports = /* @__PURE__ */ new Map();
|
|
26
|
-
let hub = null;
|
|
27
|
-
let nextStream = 1;
|
|
28
|
-
let disposed = false;
|
|
29
|
-
const openStream = (name, port) => {
|
|
30
|
-
const stream = nextStream++;
|
|
31
|
-
ports.set(stream, port);
|
|
32
|
-
port.onmessage = (event) => {
|
|
33
|
-
for (const frame of frameData(stream, event.data, chunkBytes)) options.send(frame);
|
|
34
|
-
};
|
|
35
|
-
port.start?.();
|
|
36
|
-
options.send({
|
|
37
|
-
kind: "open",
|
|
38
|
-
stream,
|
|
39
|
-
name
|
|
40
|
-
});
|
|
41
|
-
};
|
|
42
|
-
return {
|
|
43
|
-
endpoint: {
|
|
44
|
-
postMessage(message, transfer) {
|
|
45
|
-
if (disposed) return;
|
|
46
|
-
if (isHubMessage(message)) {
|
|
47
|
-
const port = transfer?.[0];
|
|
48
|
-
if (port === void 0) throw new Error("The shell sent a hub message with no port attached.");
|
|
49
|
-
hub = port;
|
|
50
|
-
hub.onmessage = (event) => {
|
|
51
|
-
if (!isPortHandshake(event.data)) return;
|
|
52
|
-
const handshake = event.ports?.[0];
|
|
53
|
-
if (handshake === void 0) throw new Error(`Port handshake for '${event.data.key}' arrived with no port attached.`);
|
|
54
|
-
openStream(event.data.key, handshake);
|
|
55
|
-
};
|
|
56
|
-
hub.start?.();
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
addEventListener() {},
|
|
61
|
-
removeEventListener() {}
|
|
62
|
-
},
|
|
63
|
-
openUrl(url) {
|
|
64
|
-
if (!disposed) options.send(frameControl("openUrl", { url }));
|
|
65
|
-
},
|
|
66
|
-
openRoute(url) {
|
|
67
|
-
if (!disposed) options.send(frameControl("openRoute", { url }));
|
|
68
|
-
},
|
|
69
|
-
receive(frame) {
|
|
70
|
-
if (disposed) return;
|
|
71
|
-
if (frame.kind === "control") {
|
|
72
|
-
if (frame.name === "colorScheme") {
|
|
73
|
-
const payload = JSON.parse(frame.body);
|
|
74
|
-
if (payload.scheme !== void 0) options.onColorScheme?.(payload.scheme);
|
|
75
|
-
}
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
if (frame.kind === "close") {
|
|
79
|
-
assembler.forget(frame.stream);
|
|
80
|
-
ports.get(frame.stream)?.close();
|
|
81
|
-
ports.delete(frame.stream);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
if (frame.kind !== "data") throw new Error(`The main process opened stream ${frame.stream}, which only the window may do.`);
|
|
85
|
-
const value = assembler.take(frame);
|
|
86
|
-
if (value === void 0) return;
|
|
87
|
-
const port = ports.get(frame.stream);
|
|
88
|
-
if (port === void 0) return;
|
|
89
|
-
port.postMessage(value);
|
|
90
|
-
},
|
|
91
|
-
dispose() {
|
|
92
|
-
disposed = true;
|
|
93
|
-
for (const [stream, port] of ports) {
|
|
94
|
-
options.send({
|
|
95
|
-
kind: "close",
|
|
96
|
-
stream
|
|
97
|
-
});
|
|
98
|
-
port.close();
|
|
99
|
-
}
|
|
100
|
-
ports.clear();
|
|
101
|
-
if (hub !== null) {
|
|
102
|
-
hub.onmessage = null;
|
|
103
|
-
hub.close();
|
|
104
|
-
hub = null;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
}
|
|
14
|
+
/** `createRemoteBridge`, by the name the Electrobun template uses. */
|
|
15
|
+
const createElectrobunBridge = createRemoteBridge;
|
|
109
16
|
//#endregion
|
|
110
17
|
export { createElectrobunBridge, windowRoute };
|
|
111
18
|
|
package/dist/view.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"view.js","names":[],"sources":["../src/view.ts"],"sourcesContent":["/**\n * The webview's half of the bridge.\n *\n *
|
|
1
|
+
{"version":3,"file":"view.js","names":[],"sources":["../src/view.ts"],"sourcesContent":["/**\n * The webview's half of the bridge.\n *\n * The bridge is `createRemoteBridge` from `gesso-framework/remote`,\n * which carries a render worker's channels over any transport that\n * can carry a string. In a desktop window that transport is\n * Electrobun's RPC: pass `view.rpc.send.<name>` as `send`, and feed\n * every frame the main process sends to `receive`. This entry keeps the\n * names a desktop application has always used for it.\n */\nimport { createRemoteBridge, type RemoteBridge, type RemoteBridgeOptions } from 'gesso-framework/remote';\n\nexport { windowRoute } from './route';\n\nexport type ElectrobunBridgeOptions = RemoteBridgeOptions;\nexport type ElectrobunBridge = RemoteBridge;\n\n/** `createRemoteBridge`, by the name the Electrobun template uses. */\nexport const createElectrobunBridge: (options: ElectrobunBridgeOptions) => ElectrobunBridge = createRemoteBridge;\n"],"mappings":";;;;;;;;;;;;;;AAkBA,MAAa,yBAAiF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gesso-electrobun",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.8",
|
|
4
4
|
"description": "Run a Gesso application in an Electrobun window, with its stores in the main process.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Kevin Baker",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"LICENSE"
|
|
55
55
|
],
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"gesso-framework": "^0.6.
|
|
57
|
+
"gesso-framework": "^0.6.8"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"rxjs": "^7.8.2"
|
package/dist/frames-BQisaYy-.js
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
//#region src/frames.ts
|
|
2
|
-
/**
|
|
3
|
-
* The whole wire format, which is deliberately smaller than the
|
|
4
|
-
* channel protocol it carries.
|
|
5
|
-
*
|
|
6
|
-
* Three frames in each direction. `open` says a named channel wants a
|
|
7
|
-
* stream, `data` carries one channel message, `close` ends a stream.
|
|
8
|
-
* Nothing here knows what a channel is, what a patch is, or what a
|
|
9
|
-
* command is: a frame's `body` is a string this module produced by
|
|
10
|
-
* serializing a value it never looked inside. That restraint is the
|
|
11
|
-
* rule the adapter states, and it is what keeps the
|
|
12
|
-
* webview's main thread a transport rather than a router.
|
|
13
|
-
*/
|
|
14
|
-
/**
|
|
15
|
-
* How much of a serialized message goes in one frame.
|
|
16
|
-
*
|
|
17
|
-
* Electrobun's transport fails above roughly 8 MiB in a single
|
|
18
|
-
* message, and it fails badly: the main process throws while draining
|
|
19
|
-
* and the sender sees only a timeout, so the real error is in a log
|
|
20
|
-
* nobody is reading. A megabyte leaves eight times the headroom and
|
|
21
|
-
* costs nothing at the measured rates.
|
|
22
|
-
*/
|
|
23
|
-
const DEFAULT_CHUNK_BYTES = 1048576;
|
|
24
|
-
function isGessoFrame(value) {
|
|
25
|
-
const kind = value?.kind;
|
|
26
|
-
return kind === "open" || kind === "data" || kind === "close" || kind === "control";
|
|
27
|
-
}
|
|
28
|
-
/** Wraps one control message. Never split: these are small by construction. */
|
|
29
|
-
function frameControl(name, payload) {
|
|
30
|
-
return {
|
|
31
|
-
kind: "control",
|
|
32
|
-
name,
|
|
33
|
-
body: JSON.stringify(payload ?? null) ?? "null"
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Serializes one channel message into the frames that carry it.
|
|
38
|
-
*
|
|
39
|
-
* JSON rather than structured clone, because the transport underneath
|
|
40
|
-
* is JSON either way: `Electroview.createTransport` stringifies every
|
|
41
|
-
* message before it encrypts it. That means `undefined` inside a value
|
|
42
|
-
* does not survive, which is true of this transport with or without
|
|
43
|
-
* this module, and which a view key cannot rely on anyway.
|
|
44
|
-
*/
|
|
45
|
-
function frameData(stream, value, chunkBytes = DEFAULT_CHUNK_BYTES) {
|
|
46
|
-
const body = JSON.stringify(value);
|
|
47
|
-
if (body === void 0) throw new Error(`A channel message for stream ${stream} could not be serialized. Only plain data crosses a channel; see requirePlainData.`);
|
|
48
|
-
if (body.length <= chunkBytes) return [{
|
|
49
|
-
kind: "data",
|
|
50
|
-
stream,
|
|
51
|
-
body
|
|
52
|
-
}];
|
|
53
|
-
const parts = Math.ceil(body.length / chunkBytes);
|
|
54
|
-
const frames = [];
|
|
55
|
-
for (let part = 0; part < parts; part++) frames.push({
|
|
56
|
-
kind: "data",
|
|
57
|
-
stream,
|
|
58
|
-
body: body.slice(part * chunkBytes, (part + 1) * chunkBytes),
|
|
59
|
-
part,
|
|
60
|
-
parts
|
|
61
|
-
});
|
|
62
|
-
return frames;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Puts split messages back together.
|
|
66
|
-
*
|
|
67
|
-
* The transport delivers in order (`Electroview` dispatches through a
|
|
68
|
-
* promise tail that preserves frame order), so a part that arrives out
|
|
69
|
-
* of turn is a bug rather than a race, and it says so instead of
|
|
70
|
-
* quietly assembling a corrupt message.
|
|
71
|
-
*/
|
|
72
|
-
var FrameAssembler = class {
|
|
73
|
-
partial = /* @__PURE__ */ new Map();
|
|
74
|
-
/**
|
|
75
|
-
* Returns the value a `data` frame completes, or `undefined` while
|
|
76
|
-
* more parts are still to come.
|
|
77
|
-
*/
|
|
78
|
-
take(frame) {
|
|
79
|
-
if (frame.parts === void 0) return JSON.parse(frame.body);
|
|
80
|
-
const held = this.partial.get(frame.stream) ?? {
|
|
81
|
-
parts: frame.parts,
|
|
82
|
-
chunks: []
|
|
83
|
-
};
|
|
84
|
-
const expected = held.chunks.length;
|
|
85
|
-
if (frame.part !== expected) {
|
|
86
|
-
this.partial.delete(frame.stream);
|
|
87
|
-
throw new Error(`Stream ${frame.stream} received part ${String(frame.part)} when part ${expected} was next. Frames are expected in order; a gap means the transport reordered or dropped one.`);
|
|
88
|
-
}
|
|
89
|
-
held.chunks.push(frame.body);
|
|
90
|
-
if (held.chunks.length < held.parts) {
|
|
91
|
-
this.partial.set(frame.stream, held);
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
this.partial.delete(frame.stream);
|
|
95
|
-
return JSON.parse(held.chunks.join(""));
|
|
96
|
-
}
|
|
97
|
-
/** Drops anything half-received for a stream that has closed. */
|
|
98
|
-
forget(stream) {
|
|
99
|
-
this.partial.delete(stream);
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
//#endregion
|
|
103
|
-
export { isGessoFrame as a, frameData as i, FrameAssembler as n, frameControl as r, DEFAULT_CHUNK_BYTES as t };
|
|
104
|
-
|
|
105
|
-
//# sourceMappingURL=frames-BQisaYy-.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"frames-BQisaYy-.js","names":[],"sources":["../src/frames.ts"],"sourcesContent":["/**\n * The whole wire format, which is deliberately smaller than the\n * channel protocol it carries.\n *\n * Three frames in each direction. `open` says a named channel wants a\n * stream, `data` carries one channel message, `close` ends a stream.\n * Nothing here knows what a channel is, what a patch is, or what a\n * command is: a frame's `body` is a string this module produced by\n * serializing a value it never looked inside. That restraint is the\n * rule the adapter states, and it is what keeps the\n * webview's main thread a transport rather than a router.\n */\n\n/**\n * How much of a serialized message goes in one frame.\n *\n * Electrobun's transport fails above roughly 8 MiB in a single\n * message, and it fails badly: the main process throws while draining\n * and the sender sees only a timeout, so the real error is in a log\n * nobody is reading. A megabyte leaves eight times the headroom and\n * costs nothing at the measured rates.\n */\nexport const DEFAULT_CHUNK_BYTES = 1_048_576;\n\nexport type GessoFrame =\n | { readonly kind: 'open'; readonly stream: number; readonly name: string }\n | {\n readonly kind: 'data';\n readonly stream: number;\n readonly body: string;\n /** Absent unless the message was split; then 0-based. */\n readonly part?: number;\n /** Absent unless the message was split; then how many parts to expect. */\n readonly parts?: number;\n }\n | { readonly kind: 'close'; readonly stream: number }\n /**\n * The adapter's own traffic, which is not a channel: the appearance\n * the platform is in, a url the application wants opened outside the\n * window, and one of its own routes it wants opened in a new window.\n * It carries a name and a serialized payload for\n * the same reason a `data` frame carries a body, and it is a\n * separate kind so that nothing has to reserve a stream number.\n */\n | { readonly kind: 'control'; readonly name: string; readonly body: string };\n\nexport function isGessoFrame(value: unknown): value is GessoFrame {\n const kind = (value as { kind?: unknown } | null)?.kind;\n return kind === 'open' || kind === 'data' || kind === 'close' || kind === 'control';\n}\n\n/** Wraps one control message. Never split: these are small by construction. */\nexport function frameControl(name: string, payload: unknown): GessoFrame {\n return { kind: 'control', name, body: JSON.stringify(payload ?? null) ?? 'null' };\n}\n\n/**\n * Serializes one channel message into the frames that carry it.\n *\n * JSON rather than structured clone, because the transport underneath\n * is JSON either way: `Electroview.createTransport` stringifies every\n * message before it encrypts it. That means `undefined` inside a value\n * does not survive, which is true of this transport with or without\n * this module, and which a view key cannot rely on anyway.\n */\nexport function frameData(stream: number, value: unknown, chunkBytes = DEFAULT_CHUNK_BYTES): GessoFrame[] {\n const body = JSON.stringify(value);\n if (body === undefined) {\n throw new Error(\n `A channel message for stream ${stream} could not be serialized. Only plain data crosses a channel; see requirePlainData.`\n );\n }\n if (body.length <= chunkBytes) {\n return [{ kind: 'data', stream, body }];\n }\n const parts = Math.ceil(body.length / chunkBytes);\n const frames: GessoFrame[] = [];\n for (let part = 0; part < parts; part++) {\n frames.push({\n kind: 'data',\n stream,\n body: body.slice(part * chunkBytes, (part + 1) * chunkBytes),\n part,\n parts\n });\n }\n return frames;\n}\n\n/**\n * Puts split messages back together.\n *\n * The transport delivers in order (`Electroview` dispatches through a\n * promise tail that preserves frame order), so a part that arrives out\n * of turn is a bug rather than a race, and it says so instead of\n * quietly assembling a corrupt message.\n */\nexport class FrameAssembler {\n private readonly partial = new Map<number, { parts: number; chunks: string[] }>();\n\n /**\n * Returns the value a `data` frame completes, or `undefined` while\n * more parts are still to come.\n */\n take(frame: GessoFrame & { kind: 'data' }): unknown {\n if (frame.parts === undefined) {\n return JSON.parse(frame.body);\n }\n const held = this.partial.get(frame.stream) ?? { parts: frame.parts, chunks: [] };\n const expected = held.chunks.length;\n if (frame.part !== expected) {\n this.partial.delete(frame.stream);\n throw new Error(\n `Stream ${frame.stream} received part ${String(frame.part)} when part ${expected} was next. ` +\n 'Frames are expected in order; a gap means the transport reordered or dropped one.'\n );\n }\n held.chunks.push(frame.body);\n if (held.chunks.length < held.parts) {\n this.partial.set(frame.stream, held);\n return undefined;\n }\n this.partial.delete(frame.stream);\n return JSON.parse(held.chunks.join(''));\n }\n\n /** Drops anything half-received for a stream that has closed. */\n forget(stream: number): void {\n this.partial.delete(stream);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAsBA,MAAa,sBAAsB;AAwBnC,SAAgB,aAAa,OAAqC;CAChE,MAAM,OAAQ,OAAqC;CACnD,OAAO,SAAS,UAAU,SAAS,UAAU,SAAS,WAAW,SAAS;AAC5E;;AAGA,SAAgB,aAAa,MAAc,SAA8B;CACvE,OAAO;EAAE,MAAM;EAAW;EAAM,MAAM,KAAK,UAAU,WAAW,IAAI,KAAK;CAAO;AAClF;;;;;;;;;;AAWA,SAAgB,UAAU,QAAgB,OAAgB,aAAa,qBAAmC;CACxG,MAAM,OAAO,KAAK,UAAU,KAAK;CACjC,IAAI,SAAS,KAAA,GACX,MAAM,IAAI,MACR,gCAAgC,OAAO,mFACzC;CAEF,IAAI,KAAK,UAAU,YACjB,OAAO,CAAC;EAAE,MAAM;EAAQ;EAAQ;CAAK,CAAC;CAExC,MAAM,QAAQ,KAAK,KAAK,KAAK,SAAS,UAAU;CAChD,MAAM,SAAuB,CAAC;CAC9B,KAAK,IAAI,OAAO,GAAG,OAAO,OAAO,QAC/B,OAAO,KAAK;EACV,MAAM;EACN;EACA,MAAM,KAAK,MAAM,OAAO,aAAa,OAAO,KAAK,UAAU;EAC3D;EACA;CACF,CAAC;CAEH,OAAO;AACT;;;;;;;;;AAUA,IAAa,iBAAb,MAA4B;CAC1B,0BAA2B,IAAI,IAAiD;;;;;CAMhF,KAAK,OAA+C;EAClD,IAAI,MAAM,UAAU,KAAA,GAClB,OAAO,KAAK,MAAM,MAAM,IAAI;EAE9B,MAAM,OAAO,KAAK,QAAQ,IAAI,MAAM,MAAM,KAAK;GAAE,OAAO,MAAM;GAAO,QAAQ,CAAC;EAAE;EAChF,MAAM,WAAW,KAAK,OAAO;EAC7B,IAAI,MAAM,SAAS,UAAU;GAC3B,KAAK,QAAQ,OAAO,MAAM,MAAM;GAChC,MAAM,IAAI,MACR,UAAU,MAAM,OAAO,iBAAiB,OAAO,MAAM,IAAI,EAAE,aAAa,SAAS,6FAEnF;EACF;EACA,KAAK,OAAO,KAAK,MAAM,IAAI;EAC3B,IAAI,KAAK,OAAO,SAAS,KAAK,OAAO;GACnC,KAAK,QAAQ,IAAI,MAAM,QAAQ,IAAI;GACnC;EACF;EACA,KAAK,QAAQ,OAAO,MAAM,MAAM;EAChC,OAAO,KAAK,MAAM,KAAK,OAAO,KAAK,EAAE,CAAC;CACxC;;CAGA,OAAO,QAAsB;EAC3B,KAAK,QAAQ,OAAO,MAAM;CAC5B;AACF"}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
//#region src/frames.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* The whole wire format, which is deliberately smaller than the
|
|
4
|
-
* channel protocol it carries.
|
|
5
|
-
*
|
|
6
|
-
* Three frames in each direction. `open` says a named channel wants a
|
|
7
|
-
* stream, `data` carries one channel message, `close` ends a stream.
|
|
8
|
-
* Nothing here knows what a channel is, what a patch is, or what a
|
|
9
|
-
* command is: a frame's `body` is a string this module produced by
|
|
10
|
-
* serializing a value it never looked inside. That restraint is the
|
|
11
|
-
* rule the adapter states, and it is what keeps the
|
|
12
|
-
* webview's main thread a transport rather than a router.
|
|
13
|
-
*/
|
|
14
|
-
/**
|
|
15
|
-
* How much of a serialized message goes in one frame.
|
|
16
|
-
*
|
|
17
|
-
* Electrobun's transport fails above roughly 8 MiB in a single
|
|
18
|
-
* message, and it fails badly: the main process throws while draining
|
|
19
|
-
* and the sender sees only a timeout, so the real error is in a log
|
|
20
|
-
* nobody is reading. A megabyte leaves eight times the headroom and
|
|
21
|
-
* costs nothing at the measured rates.
|
|
22
|
-
*/
|
|
23
|
-
declare const DEFAULT_CHUNK_BYTES = 1048576;
|
|
24
|
-
type GessoFrame = {
|
|
25
|
-
readonly kind: 'open';
|
|
26
|
-
readonly stream: number;
|
|
27
|
-
readonly name: string;
|
|
28
|
-
} | {
|
|
29
|
-
readonly kind: 'data';
|
|
30
|
-
readonly stream: number;
|
|
31
|
-
readonly body: string;
|
|
32
|
-
/** Absent unless the message was split; then 0-based. */
|
|
33
|
-
readonly part?: number;
|
|
34
|
-
/** Absent unless the message was split; then how many parts to expect. */
|
|
35
|
-
readonly parts?: number;
|
|
36
|
-
} | {
|
|
37
|
-
readonly kind: 'close';
|
|
38
|
-
readonly stream: number;
|
|
39
|
-
} |
|
|
40
|
-
/**
|
|
41
|
-
* The adapter's own traffic, which is not a channel: the appearance
|
|
42
|
-
* the platform is in, a url the application wants opened outside the
|
|
43
|
-
* window, and one of its own routes it wants opened in a new window.
|
|
44
|
-
* It carries a name and a serialized payload for
|
|
45
|
-
* the same reason a `data` frame carries a body, and it is a
|
|
46
|
-
* separate kind so that nothing has to reserve a stream number.
|
|
47
|
-
*/
|
|
48
|
-
{
|
|
49
|
-
readonly kind: 'control';
|
|
50
|
-
readonly name: string;
|
|
51
|
-
readonly body: string;
|
|
52
|
-
};
|
|
53
|
-
declare function isGessoFrame(value: unknown): value is GessoFrame;
|
|
54
|
-
/**
|
|
55
|
-
* Serializes one channel message into the frames that carry it.
|
|
56
|
-
*
|
|
57
|
-
* JSON rather than structured clone, because the transport underneath
|
|
58
|
-
* is JSON either way: `Electroview.createTransport` stringifies every
|
|
59
|
-
* message before it encrypts it. That means `undefined` inside a value
|
|
60
|
-
* does not survive, which is true of this transport with or without
|
|
61
|
-
* this module, and which a view key cannot rely on anyway.
|
|
62
|
-
*/
|
|
63
|
-
declare function frameData(stream: number, value: unknown, chunkBytes?: number): GessoFrame[];
|
|
64
|
-
/**
|
|
65
|
-
* Puts split messages back together.
|
|
66
|
-
*
|
|
67
|
-
* The transport delivers in order (`Electroview` dispatches through a
|
|
68
|
-
* promise tail that preserves frame order), so a part that arrives out
|
|
69
|
-
* of turn is a bug rather than a race, and it says so instead of
|
|
70
|
-
* quietly assembling a corrupt message.
|
|
71
|
-
*/
|
|
72
|
-
declare class FrameAssembler {
|
|
73
|
-
private readonly partial;
|
|
74
|
-
/**
|
|
75
|
-
* Returns the value a `data` frame completes, or `undefined` while
|
|
76
|
-
* more parts are still to come.
|
|
77
|
-
*/
|
|
78
|
-
take(frame: GessoFrame & {
|
|
79
|
-
kind: 'data';
|
|
80
|
-
}): unknown;
|
|
81
|
-
/** Drops anything half-received for a stream that has closed. */
|
|
82
|
-
forget(stream: number): void;
|
|
83
|
-
}
|
|
84
|
-
//#endregion
|
|
85
|
-
export { isGessoFrame as a, frameData as i, FrameAssembler as n, GessoFrame as r, DEFAULT_CHUNK_BYTES as t };
|
|
86
|
-
//# sourceMappingURL=frames-Bx7SnqWg.d.ts.map
|