@swf2es/cli 0.0.0-stage → 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.
Files changed (74) hide show
  1. package/LICENSE +202 -0
  2. package/LICENSE.third-party +389 -0
  3. package/README.md +32 -2
  4. package/dist/aot.d.ts +98 -0
  5. package/dist/aot.d.ts.map +1 -0
  6. package/dist/aot.js +145 -0
  7. package/dist/aot.js.map +1 -0
  8. package/dist/args.d.ts +31 -0
  9. package/dist/args.d.ts.map +1 -0
  10. package/dist/args.js +37 -0
  11. package/dist/args.js.map +1 -0
  12. package/dist/commands/compile.d.ts +3 -0
  13. package/dist/commands/compile.d.ts.map +1 -0
  14. package/dist/commands/compile.js +233 -0
  15. package/dist/commands/compile.js.map +1 -0
  16. package/dist/commands/help.d.ts +6 -0
  17. package/dist/commands/help.d.ts.map +1 -0
  18. package/dist/commands/help.js +52 -0
  19. package/dist/commands/help.js.map +1 -0
  20. package/dist/commands/run.d.ts +3 -0
  21. package/dist/commands/run.d.ts.map +1 -0
  22. package/dist/commands/run.js +122 -0
  23. package/dist/commands/run.js.map +1 -0
  24. package/dist/files.d.ts +10 -0
  25. package/dist/files.d.ts.map +1 -0
  26. package/dist/files.js +35 -0
  27. package/dist/files.js.map +1 -0
  28. package/dist/index.d.ts +5 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +5 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/main.d.ts +3 -0
  33. package/dist/main.d.ts.map +1 -0
  34. package/dist/main.js +45 -0
  35. package/dist/main.js.map +1 -0
  36. package/dist/output.d.ts +7 -0
  37. package/dist/output.d.ts.map +1 -0
  38. package/dist/output.js +20 -0
  39. package/dist/output.js.map +1 -0
  40. package/dist/shell-memories.d.ts +2 -0
  41. package/dist/shell-memories.d.ts.map +1 -0
  42. package/dist/shell-memories.js +32 -0
  43. package/dist/shell-memories.js.map +1 -0
  44. package/dist/shell-thread.d.ts +2 -0
  45. package/dist/shell-thread.d.ts.map +1 -0
  46. package/dist/shell-thread.js +49 -0
  47. package/dist/shell-thread.js.map +1 -0
  48. package/dist/shell-toplevel-library.d.ts +2 -0
  49. package/dist/shell-toplevel-library.d.ts.map +1 -0
  50. package/dist/shell-toplevel-library.js +22 -0
  51. package/dist/shell-toplevel-library.js.map +1 -0
  52. package/dist/shell.d.ts +62 -0
  53. package/dist/shell.d.ts.map +1 -0
  54. package/dist/shell.js +135 -0
  55. package/dist/shell.js.map +1 -0
  56. package/dist/threads.d.ts +85 -0
  57. package/dist/threads.d.ts.map +1 -0
  58. package/dist/threads.js +145 -0
  59. package/dist/threads.js.map +1 -0
  60. package/package.json +46 -3
  61. package/src/aot.ts +239 -0
  62. package/src/args.ts +53 -0
  63. package/src/commands/compile.ts +286 -0
  64. package/src/commands/help.ts +60 -0
  65. package/src/commands/run.ts +131 -0
  66. package/src/files.ts +38 -0
  67. package/src/index.ts +25 -0
  68. package/src/main.ts +51 -0
  69. package/src/output.ts +23 -0
  70. package/src/shell-memories.ts +35 -0
  71. package/src/shell-thread.ts +54 -0
  72. package/src/shell-toplevel-library.ts +28 -0
  73. package/src/shell.ts +211 -0
  74. package/src/threads.ts +223 -0
package/src/threads.ts ADDED
@@ -0,0 +1,223 @@
1
+ // avmshell's background workers on node's threads: each a worker_threads
2
+ // Worker running shell-thread.js, which compiles and runs its worker's
3
+ // ABCs after the same libraries, as runShell runs the primordial's.
4
+ //
5
+ // A shareable ByteArray's memory reaches the other threads on a
6
+ // BroadcastChannel of the primordial's threads, read synchronously
7
+ // (receiveMessageOnPort) when a shared property names memory a thread has
8
+ // not seen: its code never returns to the event loop to take messages.
9
+ // A thread started later is given what its parent has seen. A channel
10
+ // holds only what was posted once it was made, so a thread that was still
11
+ // starting when a memory was shared asks for it the primordial's memory
12
+ // thread (shell-memories.ts), which subscribed before any other thread
13
+ // started, and waits for its answer.
14
+
15
+ import { randomUUID } from "node:crypto";
16
+ import { writeSync } from "node:fs";
17
+ import { receiveMessageOnPort, Worker } from "node:worker_threads";
18
+ import type {
19
+ SharedBytesMemory,
20
+ ShellThread,
21
+ ShellWorkerHost,
22
+ ShellWorkerInit,
23
+ } from "@swf2es/runtime/avm2";
24
+
25
+ export interface ShellThreadsOptions {
26
+ /** codegen.wasm, compiled: each thread instantiates it. */
27
+ codegen: WebAssembly.Module;
28
+ /** The libraries the primordial loaded, which each thread loads first. */
29
+ libraries: { name: string; abc: Uint8Array }[];
30
+ /** The primordial's ABCs, which createWorkerFromPrimordial runs. */
31
+ abcs: { name: string; abc: Uint8Array }[];
32
+ /**
33
+ * Where a thread's lines go, on this thread, once its event loop takes
34
+ * them; by default each thread writes its own to stdout as it prints
35
+ * them, as avmshell's threads do.
36
+ */
37
+ print?: (line: string) => void;
38
+ /** The primordial's threads' channel for shared memory, a background thread's parent's; by default a new one. */
39
+ group?: string;
40
+ /** The shared memory its parent had seen when the thread started. */
41
+ memories?: SharedBytesMemory[];
42
+ /** The API version the primordial's ABCs run at, which its workers' run at too (ShellOptions.apiVersion). */
43
+ apiVersion?: number;
44
+ }
45
+
46
+ /** What a thread is given, as workerData. */
47
+ export interface ShellThreadData {
48
+ codegen: WebAssembly.Module;
49
+ libraries: { name: string; abc: Uint8Array }[];
50
+ abcs: { name: string; abc: Uint8Array }[];
51
+ init: ShellWorkerInit;
52
+ /** Whether its lines are posted to the thread that started it, rather than written to stdout. */
53
+ post: boolean;
54
+ group: string;
55
+ memories: SharedBytesMemory[];
56
+ apiVersion?: number;
57
+ }
58
+
59
+ /** What the memory thread is given, as workerData. */
60
+ export interface MemoriesData {
61
+ group: string;
62
+ /** What the primordial shared before it started. */
63
+ memories: SharedBytesMemory[];
64
+ /** Set once it has subscribed to the group's channel. */
65
+ ready: Int32Array;
66
+ }
67
+
68
+ /** What the group's channel carries: a memory shared, and a question for the memory thread and its answer. */
69
+ export type MemoryMessage =
70
+ | { type: "memory"; memory: SharedBytesMemory }
71
+ | { type: "ask"; id: number; answered: Int32Array }
72
+ | { type: "answer"; id: number; memory: SharedBytesMemory | null };
73
+
74
+ /** How long a thread waits for the memory thread's answer. */
75
+ const ANSWER_MS = 10_000;
76
+
77
+ export interface ShellThreads {
78
+ host: ShellWorkerHost;
79
+ /** The primordial's threads' channel for shared memory, which a thread it starts is given. */
80
+ group: string;
81
+ /**
82
+ * Stop every thread still running, as avmshell interrupts its workers
83
+ * when the primordial ends, once what they printed so far has come.
84
+ */
85
+ close(): Promise<void>;
86
+ /**
87
+ * Wait for every thread to end by itself, those started meanwhile too:
88
+ * a worker whose code has ended leaves the workers it started running,
89
+ * as avmshell does, and its thread, which theirs belong to, waits for them.
90
+ */
91
+ ended(): Promise<void>;
92
+ }
93
+
94
+ /** Threads for the background workers of a primordial that runs `options.abcs`. */
95
+ export function nodeShellThreads(options: ShellThreadsOptions): ShellThreads {
96
+ // Each thread running, with its exit.
97
+ const running = new Map<Worker, Promise<unknown>>();
98
+ // Only what a thread can be posted: a library's loaded module is not.
99
+ const libraries = options.libraries.map(({ name, abc }) => ({ name, abc }));
100
+ const abcs = options.abcs.map(({ name, abc }) => ({ name, abc }));
101
+ const group = options.group ?? `swf2es-shell-${randomUUID()}`;
102
+ const channel = new BroadcastChannel(group);
103
+ // Only read synchronously: it keeps no thread alive.
104
+ (channel as unknown as { unref(): void }).unref();
105
+ const memories = new Map<number, SharedBytesMemory>();
106
+ for (const memory of options.memories ?? []) {
107
+ memories.set(memory.id, memory);
108
+ }
109
+
110
+ // What the other threads shared since, and the memory thread's answers.
111
+ const receive = () => {
112
+ for (;;) {
113
+ const m = receiveMessageOnPort(channel as never);
114
+ if (!m) {
115
+ break;
116
+ }
117
+
118
+ const message = m.message as MemoryMessage;
119
+ if (message.type === "memory" || (message.type === "answer" && message.memory)) {
120
+ const memory = message.memory as SharedBytesMemory;
121
+ memories.set(memory.id, memory);
122
+ }
123
+ }
124
+ };
125
+ // The primordial's memory thread, started, and subscribed, before any
126
+ // other thread, or when the primordial first shares a memory.
127
+ let keeper: Worker | null = null;
128
+ const keep = () => {
129
+ if (keeper || options.group) {
130
+ return;
131
+ }
132
+
133
+ const ready = new Int32Array(new SharedArrayBuffer(4));
134
+ const data: MemoriesData = { group, memories: [...memories.values()], ready };
135
+ keeper = new Worker(new URL("./shell-memories.js", import.meta.url), { workerData: data });
136
+ keeper.unref();
137
+ Atomics.wait(ready, 0, 0, ANSWER_MS);
138
+ };
139
+ const host: ShellWorkerHost = {
140
+ start(init: ShellWorkerInit): ShellThread {
141
+ keep();
142
+ receive();
143
+ const data: ShellThreadData = {
144
+ codegen: options.codegen,
145
+ libraries,
146
+ abcs,
147
+ init,
148
+ post: options.print !== undefined,
149
+ group,
150
+ memories: [...memories.values()],
151
+ apiVersion: options.apiVersion,
152
+ };
153
+ const worker = new Worker(new URL("./shell-thread.js", import.meta.url), {
154
+ workerData: data,
155
+ stdout: false,
156
+ stderr: false,
157
+ });
158
+ running.set(worker, new Promise((resolve) => worker.once("exit", resolve)));
159
+ worker.on("message", (line: string) => {
160
+ if (options.print) {
161
+ options.print(line);
162
+ } else {
163
+ writeSync(1, `${line}\n`);
164
+ }
165
+ });
166
+ worker.on("error", (e) => {
167
+ const line = `swf2es: a background worker failed: ${e.stack ?? e}`;
168
+ if (options.print) {
169
+ options.print(line);
170
+ } else {
171
+ writeSync(2, `${line}\n`);
172
+ }
173
+ });
174
+ worker.on("exit", () => running.delete(worker));
175
+ return {
176
+ terminate() {
177
+ running.delete(worker);
178
+ return worker.terminate();
179
+ },
180
+ };
181
+ },
182
+ share(memory: SharedBytesMemory): void {
183
+ keep();
184
+ if (!memories.has(memory.id)) {
185
+ memories.set(memory.id, memory);
186
+ channel.postMessage({ type: "memory", memory } satisfies MemoryMessage);
187
+ }
188
+ },
189
+ shared(id: number): SharedBytesMemory | undefined {
190
+ if (!memories.has(id)) {
191
+ receive();
192
+ }
193
+
194
+ // Shared while this thread was starting: the memory thread has it.
195
+ if (!memories.has(id) && options.group) {
196
+ const answered = new Int32Array(new SharedArrayBuffer(4));
197
+ channel.postMessage({ type: "ask", id, answered } satisfies MemoryMessage);
198
+ Atomics.wait(answered, 0, 0, ANSWER_MS);
199
+ receive();
200
+ }
201
+
202
+ return memories.get(id);
203
+ },
204
+ };
205
+
206
+ return {
207
+ host,
208
+ group,
209
+ async close() {
210
+ await new Promise((resolve) => setImmediate(resolve));
211
+ await Promise.all([...running.keys()].map((w) => w.terminate()));
212
+ running.clear();
213
+ await keeper?.terminate();
214
+ channel.close();
215
+ memories.clear();
216
+ },
217
+ async ended() {
218
+ while (running.size) {
219
+ await Promise.all(running.values());
220
+ }
221
+ },
222
+ };
223
+ }