@swf2es/desktop 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +48 -12
  2. package/dist/launch/launch.js +4 -1
  3. package/dist/launch/launch.js.map +1 -1
  4. package/dist/main/main.js +102 -14
  5. package/dist/main/main.js.map +1 -1
  6. package/dist/main/netlog.d.ts +21 -0
  7. package/dist/main/netlog.d.ts.map +1 -0
  8. package/dist/main/netlog.js +90 -0
  9. package/dist/main/netlog.js.map +1 -0
  10. package/dist/main/sockets.d.ts +4 -2
  11. package/dist/main/sockets.d.ts.map +1 -1
  12. package/dist/main/sockets.js +15 -2
  13. package/dist/main/sockets.js.map +1 -1
  14. package/dist/main/websocket-connections.d.ts +62 -0
  15. package/dist/main/websocket-connections.d.ts.map +1 -0
  16. package/dist/main/websocket-connections.js +198 -0
  17. package/dist/main/websocket-connections.js.map +1 -0
  18. package/dist/main/websockets.d.ts +11 -0
  19. package/dist/main/websockets.d.ts.map +1 -0
  20. package/dist/main/websockets.js +93 -0
  21. package/dist/main/websockets.js.map +1 -0
  22. package/dist/preload/preload.cjs +20 -0
  23. package/dist/preload/preload.cjs.map +1 -1
  24. package/dist/renderer/air.d.ts.map +1 -1
  25. package/dist/renderer/air.js +1 -0
  26. package/dist/renderer/air.js.map +1 -1
  27. package/dist/renderer/full-screen.d.ts +4 -0
  28. package/dist/renderer/full-screen.d.ts.map +1 -0
  29. package/dist/renderer/full-screen.js +32 -0
  30. package/dist/renderer/full-screen.js.map +1 -0
  31. package/dist/renderer/index.d.ts.map +1 -1
  32. package/dist/renderer/index.js +4 -0
  33. package/dist/renderer/index.js.map +1 -1
  34. package/dist/renderer/websockets.d.ts +4 -0
  35. package/dist/renderer/websockets.d.ts.map +1 -0
  36. package/dist/renderer/websockets.js +36 -0
  37. package/dist/renderer/websockets.js.map +1 -0
  38. package/dist/shared/api.d.ts +53 -0
  39. package/dist/shared/api.d.ts.map +1 -1
  40. package/package.json +7 -7
  41. package/src/launch/launch.ts +5 -2
  42. package/src/main/main.ts +128 -15
  43. package/src/main/netlog.ts +108 -0
  44. package/src/main/sockets.ts +17 -1
  45. package/src/main/websocket-connections.ts +264 -0
  46. package/src/main/websockets.ts +115 -0
  47. package/src/preload/preload.cts +23 -0
  48. package/src/renderer/air.ts +1 -0
  49. package/src/renderer/full-screen.ts +44 -0
  50. package/src/renderer/index.ts +4 -0
  51. package/src/renderer/websockets.ts +43 -0
  52. package/src/shared/api.ts +40 -0
@@ -0,0 +1,264 @@
1
+ // One page's air.net.WebSocket connections, made with the WHATWG WebSocket
2
+ // of Electron's Node: what the WebSocket bridge (websockets.ts) holds for
3
+ // each page, apart from Electron so that node runs it in the tests
4
+ // (tests/desktop/websocket-connections.test.ts).
5
+ import type { WebSocketEvent } from "../shared/api.js";
6
+
7
+ /** As many as a page may hold open at once; past it a connection is refused. */
8
+ const MAX_OPEN = 64;
9
+
10
+ /** The longest URL, and the most and longest subprotocols, a page may ask for. */
11
+ const MAX_URL = 8192;
12
+ const MAX_PROTOCOLS = 32;
13
+ const MAX_PROTOCOL = 256;
14
+
15
+ /**
16
+ * The bytes a connection may hold unsent, as a socket's (connections.ts)
17
+ * but for whole messages, which are larger than a socket's writes: a peer
18
+ * that stops reading would otherwise have the main process buffer all an
19
+ * application sends. Past this the connection fails.
20
+ */
21
+ export const MAX_BUFFERED = 16 * 1024 * 1024;
22
+
23
+ const OPEN = 1;
24
+
25
+ /** What is used of the WHATWG WebSocket. */
26
+ interface Client {
27
+ binaryType: string;
28
+ readonly protocol: string;
29
+ readonly readyState: number;
30
+ readonly bufferedAmount: number;
31
+ onopen: (() => void) | null;
32
+ onmessage: ((event: { data: unknown }) => void) | null;
33
+ onclose: ((event: { code: number }) => void) | null;
34
+ onerror: (() => void) | null;
35
+ send(data: string | Uint8Array): void;
36
+ close(code?: number): void;
37
+ }
38
+
39
+ type ClientConstructor = new (
40
+ url: string,
41
+ // Node's takes its options in the protocols' place: undici's dispatcher among them.
42
+ protocols: string[] | { protocols: string[]; dispatcher: unknown },
43
+ ) => Client;
44
+
45
+ /** Opens a client; throws as the WebSocket constructor does. */
46
+ export type Opener = (url: string, protocols: string[]) => unknown;
47
+
48
+ const globalClient = (): ClientConstructor | undefined =>
49
+ (globalThis as { WebSocket?: ClientConstructor }).WebSocket;
50
+
51
+ /** Whether this Node has the WHATWG WebSocket (22 and later, Electron's among them). */
52
+ export function hasWebSocket(): boolean {
53
+ return typeof globalClient() === "function";
54
+ }
55
+
56
+ /**
57
+ * What sends a client's requests with no certificate verified, for
58
+ * --ignore-certificate-errors; null where this Node gives no way to make
59
+ * one. The WHATWG WebSocket has no option for it: Node's is undici's,
60
+ * which takes a dispatcher, and Node exports no class of one, so it is
61
+ * made from the class of the dispatcher undici keeps for everything else.
62
+ */
63
+ export function unverifiedDispatcher(): unknown {
64
+ // Naming the global loads undici, which then sets its dispatcher.
65
+ if (!hasWebSocket()) {
66
+ return null;
67
+ }
68
+
69
+ const shared = (globalThis as Record<symbol, unknown>)[Symbol.for("undici.globalDispatcher.1")];
70
+ const Agent = (shared as { constructor?: unknown } | undefined)?.constructor;
71
+ if (typeof Agent !== "function" || Agent.name !== "Agent") {
72
+ return null;
73
+ }
74
+
75
+ try {
76
+ const agent = new (Agent as new (options: object) => { dispatch?: unknown })({
77
+ connect: { rejectUnauthorized: false },
78
+ });
79
+ return typeof agent.dispatch === "function" ? agent : null;
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * An opener that leaves a wss: server's certificate unverified where
87
+ * `unverified` says so of its host, through `dispatcher`
88
+ * (unverifiedDispatcher's); any other connection is the global's as it is.
89
+ */
90
+ export function webSocketOpener(
91
+ unverified: (host: string) => boolean,
92
+ dispatcher: unknown,
93
+ ): Opener {
94
+ return (url, protocols) => {
95
+ const Client = globalClient() as ClientConstructor;
96
+ const parsed = new URL(url);
97
+ const host = parsed.hostname.replace(/^\[(.*)\]$/, "$1").toLowerCase();
98
+ return dispatcher && parsed.protocol === "wss:" && unverified(host)
99
+ ? new Client(url, { protocols, dispatcher })
100
+ : new Client(url, protocols);
101
+ };
102
+ }
103
+
104
+ /** Whether the page asked for a ws: or wss: URL with a host, of a length a server takes. */
105
+ export function validWebSocketUrl(url: unknown): url is string {
106
+ if (typeof url !== "string" || url.length > MAX_URL) {
107
+ return false;
108
+ }
109
+
110
+ try {
111
+ const parsed = new URL(url);
112
+ return (parsed.protocol === "ws:" || parsed.protocol === "wss:") && parsed.hostname !== "";
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+
118
+ export function validProtocols(protocols: unknown): protocols is string[] {
119
+ return (
120
+ Array.isArray(protocols) &&
121
+ protocols.length <= MAX_PROTOCOLS &&
122
+ protocols.every((p) => typeof p === "string" && p.length <= MAX_PROTOCOL)
123
+ );
124
+ }
125
+
126
+ /** The close codes the WHATWG WebSocket sends; any other throws. */
127
+ const sendable = (code: unknown): code is number =>
128
+ code === 1000 || (typeof code === "number" && code >= 3000 && code <= 4999);
129
+
130
+ export class WebSocketConnections {
131
+ private readonly clients = new Map<number, Client>();
132
+ private readonly tell: (id: number, event: WebSocketEvent) => void;
133
+ private readonly open: Opener;
134
+
135
+ /** `tell` carries each connection's events to its page; `open` makes a client, the global's by default. */
136
+ constructor(
137
+ tell: (id: number, event: WebSocketEvent) => void,
138
+ open: Opener = (url, protocols) => new (globalClient() as ClientConstructor)(url, protocols),
139
+ ) {
140
+ this.tell = tell;
141
+ this.open = open;
142
+ }
143
+
144
+ get size(): number {
145
+ return this.clients.size;
146
+ }
147
+
148
+ /** Counts closeAll's: a connection decided on before one belongs to a page that is gone. */
149
+ generation = 0;
150
+
151
+ /**
152
+ * Connect `id` to `url`, offering `protocols`, each checked, as the page
153
+ * asks; a refusal, a URL or protocols the client throws for among them,
154
+ * is an error, then a close.
155
+ */
156
+ connect(id: number, url: unknown, protocols: unknown): void {
157
+ if (
158
+ !validWebSocketUrl(url) ||
159
+ !validProtocols(protocols) ||
160
+ this.clients.has(id) ||
161
+ this.clients.size >= MAX_OPEN ||
162
+ !hasWebSocket()
163
+ ) {
164
+ this.refuse(id);
165
+ return;
166
+ }
167
+
168
+ let client: Client;
169
+ try {
170
+ client = this.open(url, protocols) as Client;
171
+ } catch {
172
+ this.refuse(id);
173
+ return;
174
+ }
175
+
176
+ client.binaryType = "arraybuffer";
177
+ this.clients.set(id, client);
178
+ client.onopen = () => this.tell(id, { type: "open", protocol: client.protocol });
179
+ client.onmessage = ({ data }) =>
180
+ this.tell(id, {
181
+ type: "message",
182
+ data: typeof data === "string" ? data : new Uint8Array(data as ArrayBuffer),
183
+ });
184
+ client.onerror = () => this.tell(id, { type: "error" });
185
+ client.onclose = ({ code }) => {
186
+ // Only the client under this id: a page that reused it has another by now.
187
+ if (this.clients.get(id) === client) {
188
+ this.clients.delete(id);
189
+ }
190
+
191
+ this.tell(id, { type: "close", code });
192
+ };
193
+ }
194
+
195
+ /**
196
+ * Send a text message for a string, a binary one for bytes, once the
197
+ * connection is open, unless the peer has left more than MAX_BUFFERED
198
+ * unread: then the connection fails.
199
+ */
200
+ send(id: number, data: string | Uint8Array): void {
201
+ const client = this.clients.get(id);
202
+ if (!client || client.readyState !== OPEN) {
203
+ return;
204
+ }
205
+
206
+ const size = typeof data === "string" ? Buffer.byteLength(data) : data.length;
207
+ if (client.bufferedAmount + size > MAX_BUFFERED) {
208
+ // The client has no way to drop a connection: its close waits behind
209
+ // what is buffered, so the page is told now and the client left to it.
210
+ this.silence(client);
211
+ this.clients.delete(id);
212
+ this.tell(id, { type: "error" });
213
+ this.tell(id, { type: "close", code: 1006 });
214
+ return;
215
+ }
216
+
217
+ client.send(data);
218
+ }
219
+
220
+ /** Refuse `id`, as a server that took no connection: an error, then a close with no frame. */
221
+ refuse(id: number): void {
222
+ this.tell(id, { type: "error" });
223
+ this.tell(id, { type: "close", code: 1006 });
224
+ }
225
+
226
+ /** A close frame with `code` where the client sends it, 1000 or 3000 to 4999, else with none. */
227
+ close(id: number, code?: unknown): void {
228
+ const client = this.clients.get(id);
229
+ if (!client) {
230
+ return;
231
+ }
232
+
233
+ try {
234
+ client.close(sendable(code) ? code : undefined);
235
+ } catch {
236
+ // Already closing.
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Close every connection without a word: the page that held them is
242
+ * gone, and the next numbers its own from 1.
243
+ */
244
+ closeAll(): void {
245
+ for (const client of this.clients.values()) {
246
+ this.silence(client);
247
+ }
248
+
249
+ this.clients.clear();
250
+ this.generation++;
251
+ }
252
+
253
+ private silence(client: Client): void {
254
+ client.onopen = null;
255
+ client.onmessage = null;
256
+ client.onerror = null;
257
+ client.onclose = null;
258
+ try {
259
+ client.close();
260
+ } catch {
261
+ // Already closing.
262
+ }
263
+ }
264
+ }
@@ -0,0 +1,115 @@
1
+ // air.net.WebSocket's connections, made here for the page, which has no
2
+ // network of its own, and carried over IPC as flash.net.Socket's are
3
+ // (sockets.ts). Each page's connections (websocket-connections.ts) close
4
+ // as it navigates, reloads or goes.
5
+ import { type IpcMainEvent, ipcMain, type WebContents } from "electron";
6
+ import { webSocketDataLine, webSocketLine } from "./netlog.ts";
7
+ import {
8
+ type Opener,
9
+ validProtocols,
10
+ validWebSocketUrl,
11
+ WebSocketConnections,
12
+ } from "./websocket-connections.ts";
13
+
14
+ /**
15
+ * Carry WebSocket messages from pages that `trusted` accepts, each
16
+ * connection made only where `permit` allows its URL, with `open` if
17
+ * given. `log`, where given, is told each connection asked for, opened,
18
+ * refused, failed and closed (--log-network), and with `data` what each
19
+ * sends and receives too (--log-sockets).
20
+ */
21
+ export function bridgeWebSockets(
22
+ trusted: (event: IpcMainEvent) => boolean,
23
+ permit: (url: string) => boolean,
24
+ open: Opener | undefined,
25
+ log: ((line: string) => void) | null = null,
26
+ data = false,
27
+ ): void {
28
+ const pages = new Map<WebContents, WebSocketConnections>();
29
+
30
+ const connectionsOf = (contents: WebContents): WebSocketConnections => {
31
+ const known = pages.get(contents);
32
+ if (known) {
33
+ return known;
34
+ }
35
+
36
+ const connections = new WebSocketConnections((id, event) => {
37
+ if (log) {
38
+ switch (event.type) {
39
+ case "open":
40
+ log(webSocketLine(id, event.protocol === "" ? "open" : `open ${event.protocol}`));
41
+ break;
42
+ case "message":
43
+ if (data) {
44
+ log(webSocketDataLine(id, false, event.data));
45
+ }
46
+
47
+ break;
48
+ case "close":
49
+ log(webSocketLine(id, `close ${event.code}`));
50
+ break;
51
+ case "error":
52
+ log(webSocketLine(id, "error"));
53
+ break;
54
+ }
55
+ }
56
+
57
+ if (!contents.isDestroyed()) {
58
+ contents.send("websocket:event", id, event);
59
+ }
60
+ }, open);
61
+ pages.set(contents, connections);
62
+ contents.on("did-start-navigation", (details) => {
63
+ if (details.isMainFrame && !details.isSameDocument) {
64
+ connections.closeAll();
65
+ }
66
+ });
67
+ contents.once("destroyed", () => {
68
+ connections.closeAll();
69
+ pages.delete(contents);
70
+ });
71
+ return connections;
72
+ };
73
+
74
+ ipcMain.on("websocket:connect", (event, id: unknown, url: unknown, protocols: unknown) => {
75
+ if (!trusted(event) || typeof id !== "number") {
76
+ return;
77
+ }
78
+
79
+ const connections = connectionsOf(event.sender);
80
+ if (!validWebSocketUrl(url) || !validProtocols(protocols)) {
81
+ connections.refuse(id);
82
+ return;
83
+ }
84
+
85
+ // As the client will read it: what is said of it has nothing a terminal would act on.
86
+ const href = new URL(url).href;
87
+ const allowed = permit(href);
88
+ log?.(webSocketLine(id, `${allowed ? "connecting to" : "refused"} ${href}`));
89
+ if (allowed) {
90
+ connections.connect(id, href, protocols);
91
+ } else {
92
+ connections.refuse(id);
93
+ }
94
+ });
95
+
96
+ ipcMain.on("websocket:send", (event, id: unknown, message: unknown) => {
97
+ if (
98
+ trusted(event) &&
99
+ typeof id === "number" &&
100
+ (typeof message === "string" || message instanceof Uint8Array)
101
+ ) {
102
+ if (log && data) {
103
+ log(webSocketDataLine(id, true, message));
104
+ }
105
+
106
+ pages.get(event.sender)?.send(id, message);
107
+ }
108
+ });
109
+
110
+ ipcMain.on("websocket:close", (event, id: unknown, code: unknown) => {
111
+ if (trusted(event) && typeof id === "number") {
112
+ pages.get(event.sender)?.close(id, code);
113
+ }
114
+ });
115
+ }
@@ -7,11 +7,13 @@ import type {
7
7
  NetworkResponse,
8
8
  OpenedMovie,
9
9
  SocketEvent,
10
+ WebSocketEvent,
10
11
  } from "../shared/api.js";
11
12
 
12
13
  const { contextBridge, ipcRenderer, webUtils } = require("electron") as typeof import("electron");
13
14
 
14
15
  let nextSocket = 1;
16
+ let nextWebSocket = 1;
15
17
  let nextRequest = 1;
16
18
  /** The requests waiting for their response, by number. */
17
19
  const responses = new Map<number, (response: NetworkResponse) => void>();
@@ -27,6 +29,12 @@ const api: DesktopApi = {
27
29
  write: (id, bytes) => ipcRenderer.invoke("files:write", id, bytes),
28
30
  release: (id) => ipcRenderer.send("files:release", id),
29
31
  },
32
+ fullScreen: {
33
+ set: (fullScreen) => ipcRenderer.send("desktop:full-screen", fullScreen === true),
34
+ onChange: (listener) => {
35
+ ipcRenderer.on("desktop:full-screen", (_event, fullScreen: boolean) => listener(fullScreen));
36
+ },
37
+ },
30
38
  start: () => ipcRenderer.invoke("desktop:start"),
31
39
  onOpen: (listener) => {
32
40
  ipcRenderer.on("desktop:open", (_event, movie: OpenedMovie) => listener(movie));
@@ -53,6 +61,20 @@ const api: DesktopApi = {
53
61
  );
54
62
  },
55
63
  },
64
+ webSockets: {
65
+ connect: (url, protocols) => {
66
+ const id = nextWebSocket++;
67
+ ipcRenderer.send("websocket:connect", id, String(url), Array.from(protocols, String));
68
+ return id;
69
+ },
70
+ send: (id, data) => ipcRenderer.send("websocket:send", id, data),
71
+ close: (id, code) => ipcRenderer.send("websocket:close", id, code),
72
+ onEvent: (listener) => {
73
+ ipcRenderer.on("websocket:event", (_event, id: number, event: WebSocketEvent) =>
74
+ listener(id, event),
75
+ );
76
+ },
77
+ },
56
78
  network: {
57
79
  fetch: (request, cancel) => {
58
80
  const id = nextRequest++;
@@ -95,6 +117,7 @@ const api: DesktopApi = {
95
117
  remove: (path) => ipcRenderer.sendSync("air:remove", String(path)),
96
118
  move: (from, to) => ipcRenderer.sendSync("air:move", String(from), String(to)),
97
119
  window: () => ipcRenderer.sendSync("air:window"),
120
+ screens: () => ipcRenderer.sendSync("air:screens"),
98
121
  setBounds: (bounds) => ipcRenderer.send("air:set-bounds", bounds),
99
122
  setTitle: (title) => ipcRenderer.send("air:set-title", String(title)),
100
123
  minimize: () => ipcRenderer.send("air:window-do", "minimize"),
@@ -37,6 +37,7 @@ export function airHosts(
37
37
  close: () => air.close(),
38
38
  stageOffset: () => air.window().contentOffset,
39
39
  },
40
+ screens: () => air.screens(),
40
41
  exit: (code) => air.exit(code),
41
42
  },
42
43
  fileSystem: {
@@ -0,0 +1,44 @@
1
+ // Stage.displayState's full screen in the desktop app: the window's, through
2
+ // the shell, since the player's element already fills it. A SWF asks with
3
+ // no gesture, as an AIR application and Flash's projector could, which the
4
+ // Fullscreen API on the element would refuse. Esc leaves, as in Flash; the
5
+ // menu's F11 enters and leaves too, and the SWF hears of either.
6
+ import type { WindowFullScreen } from "@swf2es/web";
7
+ import type { DesktopFullScreen } from "../shared/api.js";
8
+
9
+ export function windowFullScreen(shell: DesktopFullScreen): WindowFullScreen {
10
+ const listeners = new Set<(fullScreen: boolean) => void>();
11
+ let full = false;
12
+ shell.onChange((fullScreen) => {
13
+ full = fullScreen;
14
+ for (const listener of listeners) {
15
+ listener(fullScreen);
16
+ }
17
+ });
18
+ // In the capture phase: before the SWF's own key handlers, whatever has the focus.
19
+ document.addEventListener(
20
+ "keydown",
21
+ (event) => {
22
+ if (event.key === "Escape" && full) {
23
+ full = false;
24
+ shell.set(false);
25
+ // No window manager tells of this one sooner than the SWF should hear.
26
+ for (const listener of listeners) {
27
+ listener(false);
28
+ }
29
+ }
30
+ },
31
+ true,
32
+ );
33
+
34
+ return {
35
+ set: (fullScreen) => {
36
+ full = fullScreen;
37
+ shell.set(fullScreen);
38
+ },
39
+ onChange: (listener) => {
40
+ listeners.add(listener);
41
+ return () => listeners.delete(listener);
42
+ },
43
+ };
44
+ }
@@ -5,8 +5,10 @@ import { configure, type Swf2esPlayerElement } from "@swf2es/web";
5
5
  import type { AirLaunch, DesktopApi, LibraryState, OpenedMovie } from "../shared/api.js";
6
6
  import { airHosts, airPath } from "./air.js";
7
7
  import { shellFiles } from "./files.js";
8
+ import { windowFullScreen } from "./full-screen.js";
8
9
  import { shellFetch } from "./network.js";
9
10
  import { shellSocketHost } from "./sockets.js";
11
+ import { shellWebSocketHost } from "./websockets.js";
10
12
 
11
13
  declare global {
12
14
  interface Window {
@@ -22,6 +24,7 @@ configure({
22
24
  cache: true,
23
25
  fileHost: shellFiles(desktop.files),
24
26
  sockets: shellSocketHost(desktop.sockets),
27
+ webSockets: shellWebSocketHost(desktop.webSockets),
25
28
  fetch: shellFetch(desktop.network, () => {
26
29
  const launch = airLaunch;
27
30
  return launch
@@ -32,6 +35,7 @@ configure({
32
35
  : null;
33
36
  }),
34
37
  loadPolicyFile: (url) => desktop.network.loadPolicyFile(url),
38
+ fullScreen: windowFullScreen(desktop.fullScreen),
35
39
  });
36
40
 
37
41
  const byId = <T extends HTMLElement>(id: string) => document.getElementById(id) as T;
@@ -0,0 +1,43 @@
1
+ // air.net.WebSocket over the shell's connections: what the player asks of
2
+ // a WebSocketHost, carried to the main process and back. The page's own
3
+ // WebSocket reaches nothing: the page has no network.
4
+ import type { WebSocketEvents, WebSocketHost } from "@swf2es/player";
5
+ import type { DesktopWebSockets } from "../shared/api.js";
6
+
7
+ export function shellWebSocketHost(webSockets: DesktopWebSockets): WebSocketHost {
8
+ // Until its close, which the shell sends after a failure too.
9
+ const open = new Map<number, WebSocketEvents>();
10
+ webSockets.onEvent((id, event) => {
11
+ const events = open.get(id);
12
+ if (!events) {
13
+ return;
14
+ }
15
+
16
+ switch (event.type) {
17
+ case "open":
18
+ events.open(event.protocol);
19
+ break;
20
+ case "message":
21
+ events.message(event.data);
22
+ break;
23
+ case "error":
24
+ events.error();
25
+ break;
26
+ case "close":
27
+ open.delete(id);
28
+ events.close(event.code);
29
+ break;
30
+ }
31
+ });
32
+
33
+ return {
34
+ connect(url, protocols, events) {
35
+ const id = webSockets.connect(url, protocols);
36
+ open.set(id, events);
37
+ return {
38
+ send: (data) => webSockets.send(id, data),
39
+ close: (code) => webSockets.close(id, code),
40
+ };
41
+ },
42
+ };
43
+ }
package/src/shared/api.ts CHANGED
@@ -50,6 +50,14 @@ export interface AirWindowState {
50
50
  visible: boolean;
51
51
  }
52
52
 
53
+ /** One of the system's displays as flash.display.Screen reads it: all of it, and what a window may fill. */
54
+ export interface AirScreenState {
55
+ bounds: { x: number; y: number; width: number; height: number };
56
+ visibleBounds: { x: number; y: number; width: number; height: number };
57
+ colorDepth: number;
58
+ main: boolean;
59
+ }
60
+
53
61
  /**
54
62
  * An AIR application's own reach, only while one the user opened plays:
55
63
  * the file system, read and written synchronously as File does, and the
@@ -72,6 +80,8 @@ export interface DesktopAir {
72
80
  remove(path: string): boolean;
73
81
  move(from: string, to: string): boolean;
74
82
  window(): AirWindowState;
83
+ /** The displays as they are now; none where no application plays. */
84
+ screens(): AirScreenState[];
75
85
  setBounds(bounds: { x: number; y: number; width: number; height: number }): void;
76
86
  setTitle(title: string): void;
77
87
  minimize(): void;
@@ -134,6 +144,23 @@ export interface DesktopSockets {
134
144
  onEvent(listener: (id: number, event: SocketEvent) => void): void;
135
145
  }
136
146
 
147
+ /** What befalls a WebSocket, as player's WebSocketEvents has it. */
148
+ export type WebSocketEvent =
149
+ | { type: "open"; protocol: string }
150
+ | { type: "message"; data: string | Uint8Array }
151
+ | { type: "close"; code: number }
152
+ | { type: "error" };
153
+
154
+ /** air.net.WebSocket's connections, opened by the shell, for an AIR application alone. */
155
+ export interface DesktopWebSockets {
156
+ /** A connection's id; its events follow, an "error" then a "close" if the shell refuses it. */
157
+ connect(url: string, protocols: string[]): number;
158
+ /** A text message for a string, a binary one for bytes. */
159
+ send(id: number, data: string | Uint8Array): void;
160
+ close(id: number, code?: number): void;
161
+ onEvent(listener: (id: number, event: WebSocketEvent) => void): void;
162
+ }
163
+
137
164
  /** A request of a SWF's, as player's FetchRequest has it. */
138
165
  export interface NetworkRequest {
139
166
  url: string;
@@ -182,8 +209,20 @@ export interface DesktopFiles {
182
209
  release(id: number): void;
183
210
  }
184
211
 
212
+ /**
213
+ * The window's full screen, which a SWF's Stage.displayState asks for
214
+ * while one the user opened plays, with no gesture, as AIR's and the
215
+ * projector's did.
216
+ */
217
+ export interface DesktopFullScreen {
218
+ set(fullScreen: boolean): void;
219
+ /** The window has entered or left full screen, a SWF's doing or the user's (F11). */
220
+ onChange(listener: (fullScreen: boolean) => void): void;
221
+ }
222
+
185
223
  export interface DesktopApi {
186
224
  files: DesktopFiles;
225
+ fullScreen: DesktopFullScreen;
187
226
  /** The page has loaded and listens: what it is to show. */
188
227
  start(): Promise<StartState>;
189
228
  /** The shell opened a SWF: play it in place of what plays. */
@@ -199,6 +238,7 @@ export interface DesktopApi {
199
238
  /** Ask the user for a library; the page reloads once the shell has it. */
200
239
  chooseLibrary(name: LibraryName): void;
201
240
  sockets: DesktopSockets;
241
+ webSockets: DesktopWebSockets;
202
242
  network: DesktopNetwork;
203
243
  air: DesktopAir;
204
244
  }