gesso-electrobun 0.6.6 → 0.6.7

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # gesso-electrobun
2
2
 
3
+ ## 0.6.7
4
+
5
+ ### Patch Changes
6
+
7
+ - 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.
8
+
9
+ `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.
10
+
11
+ - Updated dependencies [6493881]
12
+ - Updated dependencies [4f622ec]
13
+ - gesso-framework@0.6.7
14
+
3
15
  ## 0.6.6
4
16
 
5
17
  ### Patch Changes
package/dist/desktop.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { r as GessoFrame } from "./frames-Bx7SnqWg.js";
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";
@@ -0,0 +1,2 @@
1
+ import { DEFAULT_CHUNK_BYTES, FrameAssembler, GessoFrame, frameData, isGessoFrame } from "gesso-framework/remote";
2
+ export { isGessoFrame as a, frameData as i, FrameAssembler as n, GessoFrame as r, DEFAULT_CHUNK_BYTES as t };
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-Bx7SnqWg.js";
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 { a as isGessoFrame, i as frameData, n as FrameAssembler, t as DEFAULT_CHUNK_BYTES } from "./frames-BQisaYy-.js";
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 { r as GessoFrame } from "./frames-Bx7SnqWg.js";
1
+ import { RemoteChannelHost, RemoteChannelHostOptions } from "gesso-framework/remote";
2
2
  import { ServedChannel } from "gesso-framework";
3
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
- * 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 { i as frameData, n as FrameAssembler, r as frameControl } from "./frames-BQisaYy-.js";
2
- import { serveChannels } from "gesso-framework";
1
+ import { serveRemoteChannels } from "gesso-framework/remote";
3
2
  //#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" || 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 * `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 * One of the application's own urls the window asked to have opened\n * in a new window: a Cmd-click on an in-app link, sent by the view\n * bridge's `openRoute`.\n *\n * Not called for the application either. `createDesktopApp` answers\n * it by opening a window at that route; a host built on this alone\n * decides for itself.\n */\n onOpenRoute?: (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' || frame.name === 'openRoute') {\n const payload = JSON.parse(frame.body) as { url?: string };\n if (typeof payload.url === 'string') {\n (frame.name === 'openUrl' ? options.onOpenUrl : options.onOpenRoute)?.(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":";;;;;;;;;;;;;;;;;;;;;;;;AAkEA,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,aAAa,MAAM,SAAS,aAAa;KAC1D,MAAM,UAAU,KAAK,MAAM,MAAM,IAAI;KACrC,IAAI,OAAO,QAAQ,QAAQ,UACzB,CAAC,MAAM,SAAS,YAAY,QAAQ,YAAY,QAAQ,YAAA,GAAe,QAAQ,GAAG;IAEtF;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"}
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 { AppLogicEndpoint } from "gesso-framework";
2
+ import { RemoteBridge, RemoteBridgeOptions } from "gesso-framework/remote";
4
3
  //#region src/view.d.ts
5
- interface ElectrobunBridgeOptions {
6
- /**
7
- * Sends one frame to the main process, which is
8
- * `view.rpc.send.<name>` for whichever message name the application
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 { isHubMessage, isPortHandshake } from "gesso-framework";
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
- * A Gesso render worker opens a named port per channel and expects the
9
- * application layer at the other end of it. In a desktop window the
10
- * application layer is in another process, reachable only from this
11
- * thread, so this stands in its place: it accepts the handshakes the
12
- * render worker sends and pumps each one over Electrobun's RPC as a
13
- * numbered stream.
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
- function createElectrobunBridge(options) {
22
- const chunkBytes = options.chunkBytes ?? 1048576;
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 * 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 { windowRoute } from './route';\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 /**\n * Asks the main process to open this application at one of its own\n * urls in a new window.\n *\n * Pass it as `onOpenRoute` to the shell. It is what a Cmd-click on an\n * in-app link means in a desktop application: a window has no tabs,\n * and a `window.open` here would open a bare webview the application\n * does not serve. `createDesktopApp` opens the window, at that route,\n * unless the application said otherwise.\n */\n openRoute(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 openRoute(url: string): void {\n if (!disposed) {\n options.send(frameControl('openRoute', { 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":";;;;;;;;;;;;;;;;;;;;AAgFA,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,UAAU,KAAmB;GAC3B,IAAI,CAAC,UACH,QAAQ,KAAK,aAAa,aAAa,EAAE,IAAI,CAAC,CAAC;EAEnD;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"}
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.6",
3
+ "version": "0.6.7",
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.6"
57
+ "gesso-framework": "^0.6.7"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "rxjs": "^7.8.2"
@@ -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