gesso-electrobun 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # gesso-electrobun
2
+
3
+ ## 0.1.0
4
+
5
+ First public release.
6
+
7
+ Run a Gesso application in an Electrobun window, with its stores in the main
8
+ process. The application layer is a process rather than a worker, and every
9
+ window replicates the same channels, so two windows agree by construction.
10
+
11
+ Entry points: the root for the shared frame protocol, `/main`, `/view` and
12
+ `/desktop`.
13
+
14
+ Checked on WebKitGTK, from a fresh scaffold: a window opened, the counter
15
+ pressed, the appearance flipped, and a second window replicating the first.
16
+ WKWebView on macOS and WebView2 on Windows have not been run.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kevin Baker
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # gesso-electrobun
2
+
3
+ Run a Gesso application in an [Electrobun](https://electrobun.dev) window, with its stores in the main process.
4
+
5
+ ```bash
6
+ npm install gesso-core gesso-framework gesso-electrobun rxjs
7
+ ```
8
+
9
+ The scaffold writes a working project for you:
10
+
11
+ ```bash
12
+ npm create gesso-app my-app -- --template electrobun
13
+ ```
14
+
15
+ ## The arrangement
16
+
17
+ The application layer is a **process** rather than a worker. State lives in the main process and every window replicates it, so two windows agree because they are replicas of the same channels, not because anything synchronises them.
18
+
19
+ ```ts
20
+ // the main process
21
+ import { createDesktopApp } from 'gesso-electrobun/desktop';
22
+
23
+ createDesktopApp({ channels: [{ token: Notes, source }] }).open('/');
24
+ ```
25
+
26
+ ## Entry points
27
+
28
+ - `gesso-electrobun` -- the shared vocabulary, including the frame protocol
29
+ - `gesso-electrobun/main` -- the main process side
30
+ - `gesso-electrobun/view` -- inside a window
31
+ - `gesso-electrobun/desktop` -- `createDesktopApp` and `windowsChannel`
32
+
33
+ ## What is checked, and what is not
34
+
35
+ A window has been opened from a fresh scaffold on **WebKitGTK**, with the counter pressed, the appearance flipped, and a second window replicating the first.
36
+
37
+ **WKWebView on macOS and WebView2 on Windows have not been run**, because the machines to run them on were not available. The adapter is written against Electrobun's own abstraction and is expected to work on both; expected is not measured, and this file will say so until it is.
38
+
39
+ ## Documentation
40
+
41
+ [Gesso on Electrobun](https://github.com/kevinpbaker/gesso/blob/main/apps/docs/structure/gesso-on-electrobun.md) | [Desktop windows](https://github.com/kevinpbaker/gesso/blob/main/apps/docs/structure/desktop-windows.md)
42
+
43
+ MIT (c) Kevin Baker
@@ -0,0 +1,103 @@
1
+ import { r as GessoFrame } from "./frames-BrrAFMqE.js";
2
+ import { ChannelToken, ServedChannel } from "gesso-framework";
3
+ import { Observable } from "rxjs";
4
+ //#region src/desktop.d.ts
5
+ /** What the application does with the window it opened. */
6
+ interface DesktopWindowTransport {
7
+ /** Sends one frame to this window, over its own RPC. */
8
+ send: (frame: GessoFrame) => void;
9
+ /** Closes the native window. The adapter calls this; the platform may also close it on its own. */
10
+ close: () => void;
11
+ }
12
+ /** A window this application opened. */
13
+ interface DesktopWindowHandle {
14
+ /** Stable for the life of the window, and never reused. */
15
+ readonly id: number;
16
+ /** Closes the window and disposes the channels it was served. */
17
+ close(): void;
18
+ }
19
+ interface DesktopAppOptions {
20
+ /**
21
+ * The channels each window is served.
22
+ *
23
+ * A function when a window needs a channel of its own, which is what
24
+ * `windowsChannel` uses to give a window a way to close itself. It
25
+ * is called once per window, and the observables it returns are
26
+ * ordinarily the same ones every time: `provide` keeps a separate
27
+ * record of what each client has seen, so sharing a source between
28
+ * windows is what makes them agree.
29
+ */
30
+ channels: readonly ServedChannel[] | ((window: DesktopWindowHandle) => readonly ServedChannel[]);
31
+ /**
32
+ * Opens a native window.
33
+ *
34
+ * `receive` is what the window's frames must be fed into: wire it to
35
+ * the RPC message the window sends frames on, before the window
36
+ * opens, or the first handshake is lost.
37
+ */
38
+ open: (receive: (frame: GessoFrame) => void, window: DesktopWindowHandle) => DesktopWindowTransport;
39
+ /**
40
+ * A url a window asked to have opened outside itself, with the
41
+ * window that asked. `Utils.openExternal(url)` is what an Electrobun
42
+ * application passes here.
43
+ */
44
+ onOpenUrl?: (url: string, window: DesktopWindowHandle) => void;
45
+ /**
46
+ * The appearance the platform is in, pushed to every window as it
47
+ * changes and to a new window as it opens.
48
+ *
49
+ * The application supplies it because Electrobun does not: its SDK
50
+ * has no appearance API at all, and the webview's own
51
+ * `prefers-color-scheme` is wrong on WebKitGTK
52
+ *. On a platform that has a
53
+ * signal, this is where it goes; on one that does not, an
54
+ * application setting is a perfectly good source.
55
+ */
56
+ colorScheme?: Observable<'light' | 'dark'>;
57
+ /**
58
+ * Called when the last window closes. A desktop application usually
59
+ * stops here; one with a tray or a menu bar does not, which is why
60
+ * this is a callback rather than an exit.
61
+ */
62
+ onLastWindowClosed?: () => void;
63
+ /** Overrides the frame size messages are split at. Only a test should need to. */
64
+ chunkBytes?: number;
65
+ }
66
+ interface DesktopApp {
67
+ /** Opens a window, serves it every channel, and returns its handle. */
68
+ openWindow(): DesktopWindowHandle;
69
+ /** The windows open now, in the order they were opened. */
70
+ readonly windows: readonly DesktopWindowHandle[];
71
+ /** How many windows are open, as something a channel can publish. */
72
+ readonly windowCount: Observable<number>;
73
+ /** Closes every window and stops serving. */
74
+ dispose(): void;
75
+ }
76
+ declare function createDesktopApp(options: DesktopAppOptions): DesktopApp;
77
+ /** What a window can see and do about the windows of its application. */
78
+ interface DesktopWindowsView {
79
+ /** How many windows this application has open. */
80
+ count: number;
81
+ /** Which one this is, so a screen can say "window 2 of 3" without asking. */
82
+ id: number;
83
+ }
84
+ interface DesktopWindowsCommands {
85
+ open: () => void;
86
+ closeThis: () => void;
87
+ }
88
+ /**
89
+ * The channel a window opens another window through.
90
+ *
91
+ * It exists because opening a window is the one native act a screen
92
+ * genuinely needs, and going through a channel keeps the view layer
93
+ * from importing an adapter. the "native menus bound to
94
+ * store actions" is the same idea from the other end, and on Linux it
95
+ * is the only end: the runtime has no application menus there, so a
96
+ * menu is a component and this is what it calls.
97
+ */
98
+ declare const DesktopWindows: ChannelToken<DesktopWindowsView, DesktopWindowsCommands>;
99
+ /** Serves `DesktopWindows` to one window. Put it in `channels`. */
100
+ declare function windowsChannel(app: DesktopApp, window: DesktopWindowHandle): ServedChannel;
101
+ //#endregion
102
+ export { DesktopApp, DesktopAppOptions, DesktopWindowHandle, DesktopWindowTransport, DesktopWindows, DesktopWindowsCommands, DesktopWindowsView, createDesktopApp, windowsChannel };
103
+ //# sourceMappingURL=desktop.d.ts.map
@@ -0,0 +1,153 @@
1
+ import { serveChannelsToWindow } from "./main.js";
2
+ import { channel } from "gesso-framework";
3
+ import { BehaviorSubject } from "rxjs";
4
+ //#region src/desktop.ts
5
+ /**
6
+ * An application in the main process: windows, and the channels each
7
+ * one is served.
8
+ *
9
+ * Nothing here imports Electrobun, and that is not fastidiousness. The
10
+ * SDK is projected into a project by Hutch rather than installed from
11
+ * a registry, so a package in
12
+ * this workspace could not import it even if it wanted to. What the
13
+ * application supplies instead is one function that opens a window,
14
+ * which is the only Electrobun-shaped thing this needs, and which is
15
+ * five lines at the call site:
16
+ *
17
+ * const app = createDesktopApp({
18
+ * channels: window => [
19
+ * { token: Catalogue, source: catalogue },
20
+ * windowsChannel(app, window)
21
+ * ],
22
+ * open: receive => {
23
+ * const rpc = BrowserView.defineRPC<GessoWindowRPC>({
24
+ * handlers: { requests: {}, messages: { gessoFrame: receive } }
25
+ * });
26
+ * const window = new BrowserWindow({ title: 'Notes', url: 'views://mainview/index.html', rpc });
27
+ * return {
28
+ * send: frame => window.webview.rpc.send.gessoFrame(frame),
29
+ * close: () => window.close()
30
+ * };
31
+ * }
32
+ * });
33
+ * app.openWindow();
34
+ *
35
+ * The arrangement it buys: every
36
+ * window is a replica of the same channels, so two windows agree by
37
+ * construction rather than by synchronisation.
38
+ */
39
+ function createDesktopApp(options) {
40
+ const entries = /* @__PURE__ */ new Map();
41
+ /** One appearance subscription per window, ended when it closes. */
42
+ const subscriptions = /* @__PURE__ */ new Map();
43
+ const order = [];
44
+ const count = new BehaviorSubject(0);
45
+ let nextId = 1;
46
+ let disposed = false;
47
+ const forget = (id, closeNative) => {
48
+ const entry = entries.get(id);
49
+ if (entry === void 0) return;
50
+ entries.delete(id);
51
+ subscriptions.get(id)?.unsubscribe();
52
+ subscriptions.delete(id);
53
+ const at = order.indexOf(entry.handle);
54
+ if (at >= 0) order.splice(at, 1);
55
+ entry.host.dispose();
56
+ if (closeNative) entry.transport.close();
57
+ count.next(order.length);
58
+ if (order.length === 0 && !disposed) options.onLastWindowClosed?.();
59
+ };
60
+ return {
61
+ openWindow() {
62
+ if (disposed) throw new Error("This desktop application has been disposed; it cannot open a window.");
63
+ const id = nextId++;
64
+ const handle = {
65
+ id,
66
+ close: () => forget(id, true)
67
+ };
68
+ let transport;
69
+ const host = serveChannelsToWindow(typeof options.channels === "function" ? options.channels(handle) : options.channels, {
70
+ send: (frame) => transport?.send(frame),
71
+ chunkBytes: options.chunkBytes,
72
+ onOpenUrl: (url) => options.onOpenUrl?.(url, handle)
73
+ });
74
+ entries.set(id, {
75
+ handle,
76
+ transport: {
77
+ send: () => {},
78
+ close: () => {}
79
+ },
80
+ host
81
+ });
82
+ order.push(handle);
83
+ let greeted = false;
84
+ let latest;
85
+ transport = options.open((frame) => {
86
+ if (!greeted) {
87
+ greeted = true;
88
+ if (latest !== void 0) host.setColorScheme(latest);
89
+ }
90
+ host.receive(frame);
91
+ }, handle);
92
+ entries.set(id, {
93
+ handle,
94
+ transport,
95
+ host
96
+ });
97
+ const appearance = options.colorScheme?.subscribe((scheme) => {
98
+ latest = scheme;
99
+ if (greeted) host.setColorScheme(scheme);
100
+ });
101
+ if (appearance !== void 0) subscriptions.set(id, appearance);
102
+ count.next(order.length);
103
+ return handle;
104
+ },
105
+ get windows() {
106
+ return [...order];
107
+ },
108
+ windowCount: count.asObservable(),
109
+ dispose() {
110
+ disposed = true;
111
+ for (const id of new Set(entries.keys())) forget(id, true);
112
+ count.complete();
113
+ }
114
+ };
115
+ }
116
+ /**
117
+ * The channel a window opens another window through.
118
+ *
119
+ * It exists because opening a window is the one native act a screen
120
+ * genuinely needs, and going through a channel keeps the view layer
121
+ * from importing an adapter. the "native menus bound to
122
+ * store actions" is the same idea from the other end, and on Linux it
123
+ * is the only end: the runtime has no application menus there, so a
124
+ * menu is a component and this is what it calls.
125
+ */
126
+ const DesktopWindows = channel("gesso:windows", {
127
+ count: 0,
128
+ id: 0
129
+ });
130
+ /** Serves `DesktopWindows` to one window. Put it in `channels`. */
131
+ function windowsChannel(app, window) {
132
+ return {
133
+ token: DesktopWindows,
134
+ source: {
135
+ view: {
136
+ count: app.windowCount,
137
+ id: new BehaviorSubject(window.id)
138
+ },
139
+ commands: {
140
+ open: () => {
141
+ app.openWindow();
142
+ },
143
+ closeThis: () => {
144
+ window.close();
145
+ }
146
+ }
147
+ }
148
+ };
149
+ }
150
+ //#endregion
151
+ export { DesktopWindows, createDesktopApp, windowsChannel };
152
+
153
+ //# sourceMappingURL=desktop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"desktop.js","names":[],"sources":["../src/desktop.ts"],"sourcesContent":["/**\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 => {\n * const rpc = BrowserView.defineRPC<GessoWindowRPC>({\n * handlers: { requests: {}, messages: { gessoFrame: receive } }\n * });\n * const window = new BrowserWindow({ title: 'Notes', url: 'views://mainview/index.html', rpc });\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\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 /** Closes the window and disposes the channels it was served. */\n close(): void;\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 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 * 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 /** Opens a window, serves it every channel, and returns its handle. */\n openWindow(): 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(): 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 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 }\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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmHA,SAAgB,iBAAiB,SAAwC;CAMvE,MAAM,0BAAU,IAAI,IAAmB;;CAEvC,MAAM,gCAAgB,IAAI,IAA0B;CACpD,MAAM,QAA+B,CAAC;CACtC,MAAM,QAAQ,IAAI,gBAAgB,CAAC;CACnC,IAAI,SAAS;CACb,IAAI,WAAW;CAEf,MAAM,UAAU,IAAY,gBAA+B;EACzD,MAAM,QAAQ,QAAQ,IAAI,EAAE;EAC5B,IAAI,UAAU,KAAA,GACZ;EAEF,QAAQ,OAAO,EAAE;EACjB,cAAc,IAAI,EAAE,CAAC,EAAE,YAAY;EACnC,cAAc,OAAO,EAAE;EACvB,MAAM,KAAK,MAAM,QAAQ,MAAM,MAAM;EACrC,IAAI,MAAM,GACR,MAAM,OAAO,IAAI,CAAC;EAEpB,MAAM,KAAK,QAAQ;EACnB,IAAI,aACF,MAAM,UAAU,MAAM;EAExB,MAAM,KAAK,MAAM,MAAM;EACvB,IAAI,MAAM,WAAW,KAAK,CAAC,UACzB,QAAQ,qBAAqB;CAEjC;CAyEA,OAAO;EAtEL,aAAkC;GAChC,IAAI,UACF,MAAM,IAAI,MAAM,sEAAsE;GAExF,MAAM,KAAK;GACX,MAAM,SAA8B;IAClC;IACA,aAAa,OAAO,IAAI,IAAI;GAC9B;GAKA,IAAI;GACJ,MAAM,OAAO,sBACX,OAAO,QAAQ,aAAa,aAAa,QAAQ,SAAS,MAAM,IAAI,QAAQ,UAC5E;IACE,OAAM,UAAS,WAAW,KAAK,KAAK;IACpC,YAAY,QAAQ;IACpB,YAAW,QAAO,QAAQ,YAAY,KAAK,MAAM;GACnD,CACF;GACA,QAAQ,IAAI,IAAI;IAAE;IAAQ,WAAW;KAAE,YAAY,CAAC;KAAG,aAAa,CAAC;IAAE;IAAG;GAAK,CAAC;GAChF,MAAM,KAAK,MAAM;GAUjB,IAAI,UAAU;GACd,IAAI;GACJ,YAAY,QAAQ,MAAK,UAAS;IAChC,IAAI,CAAC,SAAS;KACZ,UAAU;KACV,IAAI,WAAW,KAAA,GACb,KAAK,eAAe,MAAM;IAE9B;IACA,KAAK,QAAQ,KAAK;GACpB,GAAG,MAAM;GACT,QAAQ,IAAI,IAAI;IAAE;IAAQ;IAAW;GAAK,CAAC;GAC3C,MAAM,aAAa,QAAQ,aAAa,WAAU,WAAU;IAC1D,SAAS;IACT,IAAI,SACF,KAAK,eAAe,MAAM;GAE9B,CAAC;GACD,IAAI,eAAe,KAAA,GACjB,cAAc,IAAI,IAAI,UAAU;GAElC,MAAM,KAAK,MAAM,MAAM;GACvB,OAAO;EACT;EACA,IAAI,UAA0C;GAC5C,OAAO,CAAC,GAAG,KAAK;EAClB;EACA,aAAa,MAAM,aAAa;EAChC,UAAgB;GACd,WAAW;GACX,KAAK,MAAM,MAAM,IAAI,IAAI,QAAQ,KAAK,CAAC,GACrC,OAAO,IAAI,IAAI;GAEjB,MAAM,SAAS;EACjB;CAGO;AACX;;;;;;;;;;;AAyBA,MAAa,iBAA2E,QAGtF,iBAAiB;CAAE,OAAO;CAAG,IAAI;AAAE,CAAC;;AAGtC,SAAgB,eAAe,KAAiB,QAA4C;CAC1F,OAAO;EACL,OAAO;EACP,QAAQ;GACN,MAAM;IACJ,OAAO,IAAI;IACX,IAAI,IAAI,gBAAgB,OAAO,EAAE;GACnC;GACA,UAAU;IACR,YAAY;KACV,IAAI,WAAW;IACjB;IACA,iBAAiB;KACf,OAAO,MAAM;IACf;GACF;EACF;CACF;AACF"}
@@ -0,0 +1,105 @@
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
@@ -0,0 +1 @@
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, and a url the application wants opened\n * outside the window. 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;AAuBnC,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"}
@@ -0,0 +1,85 @@
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, and a url the application wants opened
43
+ * outside the window. It carries a name and a serialized payload for
44
+ * the same reason a `data` frame carries a body, and it is a
45
+ * separate kind so that nothing has to reserve a stream number.
46
+ */
47
+ {
48
+ readonly kind: 'control';
49
+ readonly name: string;
50
+ readonly body: string;
51
+ };
52
+ declare function isGessoFrame(value: unknown): value is GessoFrame;
53
+ /**
54
+ * Serializes one channel message into the frames that carry it.
55
+ *
56
+ * JSON rather than structured clone, because the transport underneath
57
+ * is JSON either way: `Electroview.createTransport` stringifies every
58
+ * message before it encrypts it. That means `undefined` inside a value
59
+ * does not survive, which is true of this transport with or without
60
+ * this module, and which a view key cannot rely on anyway.
61
+ */
62
+ declare function frameData(stream: number, value: unknown, chunkBytes?: number): GessoFrame[];
63
+ /**
64
+ * Puts split messages back together.
65
+ *
66
+ * The transport delivers in order (`Electroview` dispatches through a
67
+ * promise tail that preserves frame order), so a part that arrives out
68
+ * of turn is a bug rather than a race, and it says so instead of
69
+ * quietly assembling a corrupt message.
70
+ */
71
+ declare class FrameAssembler {
72
+ private readonly partial;
73
+ /**
74
+ * Returns the value a `data` frame completes, or `undefined` while
75
+ * more parts are still to come.
76
+ */
77
+ take(frame: GessoFrame & {
78
+ kind: 'data';
79
+ }): unknown;
80
+ /** Drops anything half-received for a stream that has closed. */
81
+ forget(stream: number): void;
82
+ }
83
+ //#endregion
84
+ export { isGessoFrame as a, frameData as i, FrameAssembler as n, GessoFrame as r, DEFAULT_CHUNK_BYTES as t };
85
+ //# sourceMappingURL=frames-BrrAFMqE.d.ts.map
@@ -0,0 +1,2 @@
1
+ import { a as isGessoFrame, i as frameData, n as FrameAssembler, r as GessoFrame, t as DEFAULT_CHUNK_BYTES } from "./frames-BrrAFMqE.js";
2
+ export { DEFAULT_CHUNK_BYTES, FrameAssembler, type GessoFrame, frameData, isGessoFrame };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import { a as isGessoFrame, i as frameData, n as FrameAssembler, t as DEFAULT_CHUNK_BYTES } from "./frames-BQisaYy-.js";
2
+ export { DEFAULT_CHUNK_BYTES, FrameAssembler, frameData, isGessoFrame };
package/dist/main.d.ts ADDED
@@ -0,0 +1,44 @@
1
+ import { r as GessoFrame } from "./frames-BrrAFMqE.js";
2
+ import { ServedChannel } from "gesso-framework";
3
+ //#region src/main.d.ts
4
+ interface ChannelHostOptions {
5
+ /**
6
+ * Sends one frame to the window, which is
7
+ * `window.webview.rpc.send.<name>`. One host per window: each keeps
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
+ interface ChannelHost {
26
+ /** Call from the RPC handler that receives frames from the window. */
27
+ receive(frame: GessoFrame): void;
28
+ /** Tells the window which appearance the platform is in. */
29
+ setColorScheme(scheme: 'light' | 'dark'): void;
30
+ /** Stops serving and disposes every channel this host provided. */
31
+ dispose(): void;
32
+ }
33
+ /**
34
+ * Serves an application's channels to one window.
35
+ *
36
+ * const host = serveChannelsToWindow(
37
+ * [{ token: Catalogue, source: { view: { … }, commands: { … } } }],
38
+ * { send: frame => window.webview.rpc.send.gessoFrame(frame) }
39
+ * );
40
+ */
41
+ declare function serveChannelsToWindow(channels: readonly ServedChannel[], options: ChannelHostOptions): ChannelHost;
42
+ //#endregion
43
+ export { ChannelHost, ChannelHostOptions, serveChannelsToWindow };
44
+ //# sourceMappingURL=main.d.ts.map
package/dist/main.js ADDED
@@ -0,0 +1,107 @@
1
+ import { i as frameData, n as FrameAssembler, r as frameControl } from "./frames-BQisaYy-.js";
2
+ import { serveChannels } from "gesso-framework";
3
+ //#region src/main.ts
4
+ /**
5
+ * The main process's half of the bridge.
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") {
39
+ const payload = JSON.parse(frame.body);
40
+ if (typeof payload.url === "string") options.onOpenUrl?.(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
+ };
104
+ //#endregion
105
+ export { serveChannelsToWindow };
106
+
107
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","names":[],"sources":["../src/main.ts"],"sourcesContent":["/**\n * The main process's half of the bridge.\n *\n * `serveChannels` publishes an application's channels over a handshake\n * that arrives on a worker's global scope with a `MessagePort`\n * attached. No process boundary outside a worker can carry a port, so\n * this synthesises both: an `open` frame becomes the handshake, and\n * the port it hands over writes back as frames on the same stream.\n *\n * Nothing in `serveChannels`, `provide` or a view model changes for\n * this. That is the point of the seam: the application layer does not\n * learn it is talking to a window instead of a page.\n */\nimport { serveChannels, type PortHost, type ServedChannel } from 'gesso-framework';\n\nimport { DEFAULT_CHUNK_BYTES, FrameAssembler, frameControl, frameData, type GessoFrame } from './frames';\n\nexport interface ChannelHostOptions {\n /**\n * Sends one frame to the window, which is\n * `window.webview.rpc.send.<name>`. One host per window: each keeps\n * its own streams, and `provide` already keeps a separate record of\n * what each client has seen, so two windows agree without anything\n * here arranging it.\n */\n send: (frame: GessoFrame) => void;\n /** Overrides `DEFAULT_CHUNK_BYTES`. Only a test should need to. */\n chunkBytes?: number;\n /**\n * A url the window asked to have opened outside itself.\n *\n * `Utils.openExternal(url)` is what an Electrobun application passes\n * here. It is not called for the application: opening something is\n * an act, and which urls an application is willing to hand to the\n * operating system is the application's decision.\n */\n onOpenUrl?: (url: string) => void;\n}\n\nexport interface ChannelHost {\n /** Call from the RPC handler that receives frames from the window. */\n receive(frame: GessoFrame): void;\n /** Tells the window which appearance the platform is in. */\n setColorScheme(scheme: 'light' | 'dark'): void;\n /** Stops serving and disposes every channel this host provided. */\n dispose(): void;\n}\n\n/**\n * Serves an application's channels to one window.\n *\n * const host = serveChannelsToWindow(\n * [{ token: Catalogue, source: { view: { … }, commands: { … } } }],\n * { send: frame => window.webview.rpc.send.gessoFrame(frame) }\n * );\n */\nexport function serveChannelsToWindow(channels: readonly ServedChannel[], options: ChannelHostOptions): ChannelHost {\n const chunkBytes = options.chunkBytes ?? DEFAULT_CHUNK_BYTES;\n const assembler = new FrameAssembler();\n /** The synthetic port each stream is served through. */\n const ports = new Map<number, StreamPort>();\n\n // `servePorts` installs its chain on this and answers handshakes\n // delivered through `onmessage`. It is the whole of the shim: a\n // worker's global scope, as far as anything downstream can tell.\n const host: PortHost = { onmessage: null };\n const stop = serveChannels(channels, host);\n\n return {\n setColorScheme(scheme: 'light' | 'dark'): void {\n options.send(frameControl('colorScheme', { scheme }));\n },\n receive(frame: GessoFrame): void {\n if (frame.kind === 'control') {\n if (frame.name === 'openUrl') {\n const payload = JSON.parse(frame.body) as { url?: string };\n if (typeof payload.url === 'string') {\n options.onOpenUrl?.(payload.url);\n }\n }\n return;\n }\n if (frame.kind === 'open') {\n const port = new StreamPort(frame.stream, options.send, chunkBytes);\n ports.set(frame.stream, port);\n host.onmessage?.({\n data: { type: 'gesso:port', key: frame.name },\n // A `StreamPort` is a `MessagePort` in the two ways anything\n // downstream uses one, and in no others. The cast is the\n // seam; widening `PortHost` to admit a structural port would\n // widen it for every worker as well.\n ports: [port as unknown as MessagePort]\n });\n return;\n }\n if (frame.kind === 'close') {\n assembler.forget(frame.stream);\n ports.delete(frame.stream);\n return;\n }\n const value = assembler.take(frame);\n if (value === undefined) {\n return;\n }\n const port = ports.get(frame.stream);\n if (port === undefined) {\n throw new Error(\n `A frame arrived for stream ${frame.stream}, which was never opened. The window and the main process disagree about what is running.`\n );\n }\n port.deliver(value);\n },\n dispose(): void {\n for (const stream of ports.keys()) {\n options.send({ kind: 'close', stream });\n }\n ports.clear();\n stop();\n host.onmessage = null;\n }\n };\n}\n\n/**\n * One channel's port, as the application layer sees it.\n *\n * `provide` sets `onmessage` and calls `postMessage`, and that is the\n * entire surface it uses, which is why a channel can be served over\n * something that is not a `MessagePort` at all.\n */\nclass StreamPort {\n onmessage: ((event: { data: unknown }) => void) | null = null;\n\n constructor(\n private readonly stream: number,\n private readonly send: (frame: GessoFrame) => void,\n private readonly chunkBytes: number\n ) {}\n\n postMessage(value: unknown): void {\n for (const frame of frameData(this.stream, value, this.chunkBytes)) {\n this.send(frame);\n }\n }\n\n deliver(value: unknown): void {\n this.onmessage?.({ data: value });\n }\n\n /** `provide` closes a port it is done with; there is nothing to close. */\n close(): void {}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAwDA,SAAgB,sBAAsB,UAAoC,SAA0C;CAClH,MAAM,aAAa,QAAQ,cAAA;CAC3B,MAAM,YAAY,IAAI,eAAe;;CAErC,MAAM,wBAAQ,IAAI,IAAwB;CAK1C,MAAM,OAAiB,EAAE,WAAW,KAAK;CACzC,MAAM,OAAO,cAAc,UAAU,IAAI;CAEzC,OAAO;EACL,eAAe,QAAgC;GAC7C,QAAQ,KAAK,aAAa,eAAe,EAAE,OAAO,CAAC,CAAC;EACtD;EACA,QAAQ,OAAyB;GAC/B,IAAI,MAAM,SAAS,WAAW;IAC5B,IAAI,MAAM,SAAS,WAAW;KAC5B,MAAM,UAAU,KAAK,MAAM,MAAM,IAAI;KACrC,IAAI,OAAO,QAAQ,QAAQ,UACzB,QAAQ,YAAY,QAAQ,GAAG;IAEnC;IACA;GACF;GACA,IAAI,MAAM,SAAS,QAAQ;IACzB,MAAM,OAAO,IAAI,WAAW,MAAM,QAAQ,QAAQ,MAAM,UAAU;IAClE,MAAM,IAAI,MAAM,QAAQ,IAAI;IAC5B,KAAK,YAAY;KACf,MAAM;MAAE,MAAM;MAAc,KAAK,MAAM;KAAK;KAK5C,OAAO,CAAC,IAA8B;IACxC,CAAC;IACD;GACF;GACA,IAAI,MAAM,SAAS,SAAS;IAC1B,UAAU,OAAO,MAAM,MAAM;IAC7B,MAAM,OAAO,MAAM,MAAM;IACzB;GACF;GACA,MAAM,QAAQ,UAAU,KAAK,KAAK;GAClC,IAAI,UAAU,KAAA,GACZ;GAEF,MAAM,OAAO,MAAM,IAAI,MAAM,MAAM;GACnC,IAAI,SAAS,KAAA,GACX,MAAM,IAAI,MACR,8BAA8B,MAAM,OAAO,0FAC7C;GAEF,KAAK,QAAQ,KAAK;EACpB;EACA,UAAgB;GACd,KAAK,MAAM,UAAU,MAAM,KAAK,GAC9B,QAAQ,KAAK;IAAE,MAAM;IAAS;GAAO,CAAC;GAExC,MAAM,MAAM;GACZ,KAAK;GACL,KAAK,YAAY;EACnB;CACF;AACF;;;;;;;;AASA,IAAM,aAAN,MAAiB;CAII;CACA;CACA;CALnB,YAAyD;CAEzD,YACE,QACA,MACA,YACA;EAHiB,KAAA,SAAA;EACA,KAAA,OAAA;EACA,KAAA,aAAA;CAChB;CAEH,YAAY,OAAsB;EAChC,KAAK,MAAM,SAAS,UAAU,KAAK,QAAQ,OAAO,KAAK,UAAU,GAC/D,KAAK,KAAK,KAAK;CAEnB;CAEA,QAAQ,OAAsB;EAC5B,KAAK,YAAY,EAAE,MAAM,MAAM,CAAC;CAClC;;CAGA,QAAc,CAAC;AACjB"}
package/dist/view.d.ts ADDED
@@ -0,0 +1,52 @@
1
+ import { r as GessoFrame } from "./frames-BrrAFMqE.js";
2
+ import { AppLogicEndpoint } from "gesso-framework";
3
+ //#region src/view.d.ts
4
+ interface ElectrobunBridgeOptions {
5
+ /**
6
+ * Sends one frame to the main process, which is
7
+ * `view.rpc.send.<name>` for whichever message name the application
8
+ * declared. A function rather than the RPC object, so this package
9
+ * imports nothing from Electrobun's SDK and can be specified without
10
+ * a window.
11
+ */
12
+ send: (frame: GessoFrame) => void;
13
+ /** Overrides `DEFAULT_CHUNK_BYTES`. Only a test should need to. */
14
+ chunkBytes?: number;
15
+ /**
16
+ * The appearance the platform is in, as the main process reports it.
17
+ *
18
+ * Wire it to `app.setColorScheme`. It exists because
19
+ * `prefers-color-scheme` is not to be trusted in every webview: on
20
+ * WebKitGTK it reported light on a desktop that was in dark mode
21
+ *, and a shell that believes
22
+ * it is a browser gets the appearance wrong there.
23
+ */
24
+ onColorScheme?: (scheme: 'light' | 'dark') => void;
25
+ }
26
+ interface ElectrobunBridge {
27
+ /**
28
+ * Hand this to the shell as its application layer:
29
+ *
30
+ * createApp({ renderWorker: …, appLogicWorker: bridge.endpoint })
31
+ *
32
+ * The shell wires it exactly as it wires a worker it was handed, and
33
+ * never closes it.
34
+ */
35
+ readonly endpoint: AppLogicEndpoint;
36
+ /** Call from the RPC handler that receives frames from the main process. */
37
+ receive(frame: GessoFrame): void;
38
+ /**
39
+ * Hands a url to the main process to open outside the window.
40
+ *
41
+ * Pass it as `onOpenUrl` to the shell: `window.open` in a webview
42
+ * opens another webview or nothing at all, and a link in a desktop
43
+ * application belongs in the person's browser.
44
+ */
45
+ openUrl(url: string): void;
46
+ /** Closes every stream and stops pumping. */
47
+ dispose(): void;
48
+ }
49
+ declare function createElectrobunBridge(options: ElectrobunBridgeOptions): ElectrobunBridge;
50
+ //#endregion
51
+ export { ElectrobunBridge, ElectrobunBridgeOptions, createElectrobunBridge };
52
+ //# sourceMappingURL=view.d.ts.map
package/dist/view.js ADDED
@@ -0,0 +1,108 @@
1
+ import { i as frameData, n as FrameAssembler, r as frameControl } from "./frames-BQisaYy-.js";
2
+ import { isHubMessage, isPortHandshake } from "gesso-framework";
3
+ //#region src/view.ts
4
+ /**
5
+ * The webview's half of the bridge.
6
+ *
7
+ * A Gesso render worker opens a named port per channel and expects the
8
+ * application layer at the other end of it. In a desktop window the
9
+ * application layer is in another process, reachable only from this
10
+ * thread, so this stands in its place: it accepts the handshakes the
11
+ * render worker sends and pumps each one over Electrobun's RPC as a
12
+ * numbered stream.
13
+ *
14
+ * It is a transport and nothing else. It serializes a message it never
15
+ * inspects, and it holds no channel, no token and no patch. Anything
16
+ * that needs to understand a payload to route it belongs on the other
17
+ * side of the bridge, and if that ever changes here, the wrong thing
18
+ * is happening on the thread that must stay free for input.
19
+ */
20
+ function createElectrobunBridge(options) {
21
+ const chunkBytes = options.chunkBytes ?? 1048576;
22
+ const assembler = new FrameAssembler();
23
+ /** The render worker's port for each stream this side opened. */
24
+ const ports = /* @__PURE__ */ new Map();
25
+ let hub = null;
26
+ let nextStream = 1;
27
+ let disposed = false;
28
+ const openStream = (name, port) => {
29
+ const stream = nextStream++;
30
+ ports.set(stream, port);
31
+ port.onmessage = (event) => {
32
+ for (const frame of frameData(stream, event.data, chunkBytes)) options.send(frame);
33
+ };
34
+ port.start?.();
35
+ options.send({
36
+ kind: "open",
37
+ stream,
38
+ name
39
+ });
40
+ };
41
+ return {
42
+ endpoint: {
43
+ postMessage(message, transfer) {
44
+ if (disposed) return;
45
+ if (isHubMessage(message)) {
46
+ const port = transfer?.[0];
47
+ if (port === void 0) throw new Error("The shell sent a hub message with no port attached.");
48
+ hub = port;
49
+ hub.onmessage = (event) => {
50
+ if (!isPortHandshake(event.data)) return;
51
+ const handshake = event.ports?.[0];
52
+ if (handshake === void 0) throw new Error(`Port handshake for '${event.data.key}' arrived with no port attached.`);
53
+ openStream(event.data.key, handshake);
54
+ };
55
+ hub.start?.();
56
+ return;
57
+ }
58
+ },
59
+ addEventListener() {},
60
+ removeEventListener() {}
61
+ },
62
+ openUrl(url) {
63
+ if (!disposed) options.send(frameControl("openUrl", { url }));
64
+ },
65
+ receive(frame) {
66
+ if (disposed) return;
67
+ if (frame.kind === "control") {
68
+ if (frame.name === "colorScheme") {
69
+ const payload = JSON.parse(frame.body);
70
+ if (payload.scheme !== void 0) options.onColorScheme?.(payload.scheme);
71
+ }
72
+ return;
73
+ }
74
+ if (frame.kind === "close") {
75
+ assembler.forget(frame.stream);
76
+ ports.get(frame.stream)?.close();
77
+ ports.delete(frame.stream);
78
+ return;
79
+ }
80
+ if (frame.kind !== "data") throw new Error(`The main process opened stream ${frame.stream}, which only the window may do.`);
81
+ const value = assembler.take(frame);
82
+ if (value === void 0) return;
83
+ const port = ports.get(frame.stream);
84
+ if (port === void 0) return;
85
+ port.postMessage(value);
86
+ },
87
+ dispose() {
88
+ disposed = true;
89
+ for (const [stream, port] of ports) {
90
+ options.send({
91
+ kind: "close",
92
+ stream
93
+ });
94
+ port.close();
95
+ }
96
+ ports.clear();
97
+ if (hub !== null) {
98
+ hub.onmessage = null;
99
+ hub.close();
100
+ hub = null;
101
+ }
102
+ }
103
+ };
104
+ }
105
+ //#endregion
106
+ export { createElectrobunBridge };
107
+
108
+ //# sourceMappingURL=view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view.js","names":[],"sources":["../src/view.ts"],"sourcesContent":["/**\n * The webview's half of the bridge.\n *\n * A Gesso render worker opens a named port per channel and expects the\n * application layer at the other end of it. In a desktop window the\n * application layer is in another process, reachable only from this\n * thread, so this stands in its place: it accepts the handshakes the\n * render worker sends and pumps each one over Electrobun's RPC as a\n * numbered stream.\n *\n * It is a transport and nothing else. It serializes a message it never\n * inspects, and it holds no channel, no token and no patch. Anything\n * that needs to understand a payload to route it belongs on the other\n * side of the bridge, and if that ever changes here, the wrong thing\n * is happening on the thread that must stay free for input.\n */\nimport { isHubMessage, isPortHandshake, type AppLogicEndpoint } from 'gesso-framework';\n\nimport { DEFAULT_CHUNK_BYTES, FrameAssembler, frameControl, frameData, type GessoFrame } from './frames';\n\nexport interface ElectrobunBridgeOptions {\n /**\n * Sends one frame to the main process, which is\n * `view.rpc.send.<name>` for whichever message name the application\n * declared. A function rather than the RPC object, so this package\n * imports nothing from Electrobun's SDK and can be specified without\n * a window.\n */\n send: (frame: GessoFrame) => void;\n /** Overrides `DEFAULT_CHUNK_BYTES`. Only a test should need to. */\n chunkBytes?: number;\n /**\n * The appearance the platform is in, as the main process reports it.\n *\n * Wire it to `app.setColorScheme`. It exists because\n * `prefers-color-scheme` is not to be trusted in every webview: on\n * WebKitGTK it reported light on a desktop that was in dark mode\n *, and a shell that believes\n * it is a browser gets the appearance wrong there.\n */\n onColorScheme?: (scheme: 'light' | 'dark') => void;\n}\n\nexport interface ElectrobunBridge {\n /**\n * Hand this to the shell as its application layer:\n *\n * createApp({ renderWorker: …, appLogicWorker: bridge.endpoint })\n *\n * The shell wires it exactly as it wires a worker it was handed, and\n * never closes it.\n */\n readonly endpoint: AppLogicEndpoint;\n /** Call from the RPC handler that receives frames from the main process. */\n receive(frame: GessoFrame): void;\n /**\n * Hands a url to the main process to open outside the window.\n *\n * Pass it as `onOpenUrl` to the shell: `window.open` in a webview\n * opens another webview or nothing at all, and a link in a desktop\n * application belongs in the person's browser.\n */\n openUrl(url: string): void;\n /** Closes every stream and stops pumping. */\n dispose(): void;\n}\n\nexport function createElectrobunBridge(options: ElectrobunBridgeOptions): ElectrobunBridge {\n const chunkBytes = options.chunkBytes ?? DEFAULT_CHUNK_BYTES;\n const assembler = new FrameAssembler();\n /** The render worker's port for each stream this side opened. */\n const ports = new Map<number, MessagePort>();\n let hub: MessagePort | null = null;\n let nextStream = 1;\n let disposed = false;\n\n const openStream = (name: string, port: MessagePort): void => {\n const stream = nextStream++;\n ports.set(stream, port);\n port.onmessage = event => {\n for (const frame of frameData(stream, event.data, chunkBytes)) {\n options.send(frame);\n }\n };\n port.start?.();\n options.send({ kind: 'open', stream, name });\n };\n\n const endpoint: AppLogicEndpoint = {\n postMessage(message: unknown, transfer?: Transferable[]): void {\n if (disposed) {\n return;\n }\n if (isHubMessage(message)) {\n const port = transfer?.[0] as MessagePort | undefined;\n if (port === undefined) {\n throw new Error('The shell sent a hub message with no port attached.');\n }\n hub = port;\n hub.onmessage = event => {\n if (!isPortHandshake(event.data)) {\n return;\n }\n const handshake = event.ports?.[0];\n if (handshake === undefined) {\n throw new Error(`Port handshake for '${event.data.key}' arrived with no port attached.`);\n }\n openStream(event.data.key, handshake);\n };\n hub.start?.();\n return;\n }\n // Everything else the shell sends an application worker is about\n // a worker in this page: console forwarding, today. The\n // application process's console is its own, and reaching it is\n // E1.2's business rather than the transport's.\n },\n addEventListener(): void {\n // The shell listens here for console entries from the\n // application worker. There is no worker, and nothing on the\n // other side of the bridge speaks that protocol yet, so a\n // listener would never be called and is not kept.\n },\n removeEventListener(): void {}\n };\n\n return {\n endpoint,\n openUrl(url: string): void {\n if (!disposed) {\n options.send(frameControl('openUrl', { url }));\n }\n },\n receive(frame: GessoFrame): void {\n if (disposed) {\n return;\n }\n if (frame.kind === 'control') {\n if (frame.name === 'colorScheme') {\n const payload = JSON.parse(frame.body) as { scheme?: 'light' | 'dark' };\n if (payload.scheme !== undefined) {\n options.onColorScheme?.(payload.scheme);\n }\n }\n // An unknown control name is ignored rather than thrown on: the\n // main process may be newer than the window, which is ordinary\n // during a hot reload.\n return;\n }\n if (frame.kind === 'close') {\n assembler.forget(frame.stream);\n ports.get(frame.stream)?.close();\n ports.delete(frame.stream);\n return;\n }\n if (frame.kind !== 'data') {\n // `open` is this side's word. One arriving from the main\n // process means the two ends disagree about who starts a\n // stream, which is worth hearing about rather than ignoring.\n throw new Error(`The main process opened stream ${frame.stream}, which only the window may do.`);\n }\n const value = assembler.take(frame);\n if (value === undefined) {\n return;\n }\n const port = ports.get(frame.stream);\n if (port === undefined) {\n // A frame for a stream this side has closed. Ordinary during\n // teardown, because the far end may already have sent.\n return;\n }\n port.postMessage(value);\n },\n dispose(): void {\n disposed = true;\n for (const [stream, port] of ports) {\n options.send({ kind: 'close', stream });\n port.close();\n }\n ports.clear();\n if (hub !== null) {\n hub.onmessage = null;\n hub.close();\n hub = null;\n }\n }\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmEA,SAAgB,uBAAuB,SAAoD;CACzF,MAAM,aAAa,QAAQ,cAAA;CAC3B,MAAM,YAAY,IAAI,eAAe;;CAErC,MAAM,wBAAQ,IAAI,IAAyB;CAC3C,IAAI,MAA0B;CAC9B,IAAI,aAAa;CACjB,IAAI,WAAW;CAEf,MAAM,cAAc,MAAc,SAA4B;EAC5D,MAAM,SAAS;EACf,MAAM,IAAI,QAAQ,IAAI;EACtB,KAAK,aAAY,UAAS;GACxB,KAAK,MAAM,SAAS,UAAU,QAAQ,MAAM,MAAM,UAAU,GAC1D,QAAQ,KAAK,KAAK;EAEtB;EACA,KAAK,QAAQ;EACb,QAAQ,KAAK;GAAE,MAAM;GAAQ;GAAQ;EAAK,CAAC;CAC7C;CAwCA,OAAO;EACL,UAAA;GAtCA,YAAY,SAAkB,UAAiC;IAC7D,IAAI,UACF;IAEF,IAAI,aAAa,OAAO,GAAG;KACzB,MAAM,OAAO,WAAW;KACxB,IAAI,SAAS,KAAA,GACX,MAAM,IAAI,MAAM,qDAAqD;KAEvE,MAAM;KACN,IAAI,aAAY,UAAS;MACvB,IAAI,CAAC,gBAAgB,MAAM,IAAI,GAC7B;MAEF,MAAM,YAAY,MAAM,QAAQ;MAChC,IAAI,cAAc,KAAA,GAChB,MAAM,IAAI,MAAM,uBAAuB,MAAM,KAAK,IAAI,iCAAiC;MAEzF,WAAW,MAAM,KAAK,KAAK,SAAS;KACtC;KACA,IAAI,QAAQ;KACZ;IACF;GAKF;GACA,mBAAyB,CAKzB;GACA,sBAA4B,CAAC;EAItB;EACP,QAAQ,KAAmB;GACzB,IAAI,CAAC,UACH,QAAQ,KAAK,aAAa,WAAW,EAAE,IAAI,CAAC,CAAC;EAEjD;EACA,QAAQ,OAAyB;GAC/B,IAAI,UACF;GAEF,IAAI,MAAM,SAAS,WAAW;IAC5B,IAAI,MAAM,SAAS,eAAe;KAChC,MAAM,UAAU,KAAK,MAAM,MAAM,IAAI;KACrC,IAAI,QAAQ,WAAW,KAAA,GACrB,QAAQ,gBAAgB,QAAQ,MAAM;IAE1C;IAIA;GACF;GACA,IAAI,MAAM,SAAS,SAAS;IAC1B,UAAU,OAAO,MAAM,MAAM;IAC7B,MAAM,IAAI,MAAM,MAAM,CAAC,EAAE,MAAM;IAC/B,MAAM,OAAO,MAAM,MAAM;IACzB;GACF;GACA,IAAI,MAAM,SAAS,QAIjB,MAAM,IAAI,MAAM,kCAAkC,MAAM,OAAO,gCAAgC;GAEjG,MAAM,QAAQ,UAAU,KAAK,KAAK;GAClC,IAAI,UAAU,KAAA,GACZ;GAEF,MAAM,OAAO,MAAM,IAAI,MAAM,MAAM;GACnC,IAAI,SAAS,KAAA,GAGX;GAEF,KAAK,YAAY,KAAK;EACxB;EACA,UAAgB;GACd,WAAW;GACX,KAAK,MAAM,CAAC,QAAQ,SAAS,OAAO;IAClC,QAAQ,KAAK;KAAE,MAAM;KAAS;IAAO,CAAC;IACtC,KAAK,MAAM;GACb;GACA,MAAM,MAAM;GACZ,IAAI,QAAQ,MAAM;IAChB,IAAI,YAAY;IAChB,IAAI,MAAM;IACV,MAAM;GACR;EACF;CACF;AACF"}
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "gesso-electrobun",
3
+ "version": "0.1.0",
4
+ "description": "Run a Gesso application in an Electrobun window, with its stores in the main process.",
5
+ "license": "MIT",
6
+ "author": "Kevin Baker",
7
+ "homepage": "https://github.com/kevinpbaker/gesso#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/kevinpbaker/gesso.git",
11
+ "directory": "packages/electrobun"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/kevinpbaker/gesso/issues"
15
+ },
16
+ "keywords": [
17
+ "gesso",
18
+ "canvas",
19
+ "ui",
20
+ "typescript",
21
+ "electrobun",
22
+ "desktop",
23
+ "webview",
24
+ "native",
25
+ "cross-platform"
26
+ ],
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "default": "./dist/index.js"
33
+ },
34
+ "./view": {
35
+ "types": "./dist/view.d.ts",
36
+ "default": "./dist/view.js"
37
+ },
38
+ "./main": {
39
+ "types": "./dist/main.d.ts",
40
+ "default": "./dist/main.js"
41
+ },
42
+ "./desktop": {
43
+ "types": "./dist/desktop.d.ts",
44
+ "default": "./dist/desktop.js"
45
+ }
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "files": [
51
+ "README.md",
52
+ "CHANGELOG.md",
53
+ "dist",
54
+ "LICENSE"
55
+ ],
56
+ "dependencies": {
57
+ "gesso-framework": "^0.1.0"
58
+ },
59
+ "peerDependencies": {
60
+ "rxjs": "^7.8.2"
61
+ },
62
+ "devDependencies": {
63
+ "rxjs": "^7.8.2"
64
+ },
65
+ "scripts": {
66
+ "build": "tsdown"
67
+ }
68
+ }