@mostra/worker 0.0.0-placeholder → 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/LICENSE +202 -0
- package/README.md +6 -1
- package/dist/channel-thread.d.ts +1 -0
- package/dist/channel-thread.js +104 -0
- package/dist/channel-thread.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +32 -0
- package/dist/main.js.map +1 -0
- package/dist/shared-BH5iNWqJ.js +36 -0
- package/dist/shared-BH5iNWqJ.js.map +1 -0
- package/dist/worker-qog0hvgA.js +557 -0
- package/dist/worker-qog0hvgA.js.map +1 -0
- package/package.json +53 -4
- package/src/channel-thread.ts +127 -0
- package/src/index.ts +22 -0
- package/src/main.ts +30 -0
- package/src/shared.ts +50 -0
- package/src/worker.ts +722 -0
package/package.json
CHANGED
|
@@ -1,7 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostra/worker",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The process that runs one artifact version's server code: server functions, action runs and live producers.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
-
"
|
|
7
|
-
|
|
6
|
+
"author": "The Mostra Authors",
|
|
7
|
+
"homepage": "https://github.com/mostra-dev/mostra/tree/main/packages/worker#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/mostra-dev/mostra.git",
|
|
11
|
+
"directory": "packages/worker"
|
|
12
|
+
},
|
|
13
|
+
"bugs": "https://github.com/mostra-dev/mostra/issues",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"@mostra/source": "./src/index.ts",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"default": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"src",
|
|
27
|
+
"!src/**/*.test.ts"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=24"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public",
|
|
34
|
+
"provenance": true
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@mostra/protocol": "0.1.0",
|
|
38
|
+
"@mostra/server": "0.1.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@mostra/tools": "0.0.0",
|
|
42
|
+
"@types/node": "24.19.1",
|
|
43
|
+
"tsdown": "0.23.0",
|
|
44
|
+
"typescript": "7.0.2",
|
|
45
|
+
"vitest": "5.0.3"
|
|
46
|
+
},
|
|
47
|
+
"bin": {
|
|
48
|
+
"mostra-worker": "./dist/main.js"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsdown",
|
|
52
|
+
"typecheck": "tsc -b",
|
|
53
|
+
"test": "vitest run",
|
|
54
|
+
"api": "mostra-tools api"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The worker's channel thread (D124): owns the connection to the supervisor, so the artifact's
|
|
3
|
+
* main thread can block on a secret (`secret()` is synchronous, INTERFACES §2.4) while the reply
|
|
4
|
+
* still arrives, and so the watchdog's ping is answered from what the main thread last wrote into
|
|
5
|
+
* shared memory, which is how a blocked event loop shows (D62). Every other line passes through to
|
|
6
|
+
* the main thread unchanged.
|
|
7
|
+
*/
|
|
8
|
+
import { connect, Socket } from "node:net";
|
|
9
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
10
|
+
import { encodeLine, LineSplitter, WORKER_METHODS } from "@mostra/protocol";
|
|
11
|
+
import { SECRET_SLOT, type SharedState, sharedViews } from "./shared.ts";
|
|
12
|
+
|
|
13
|
+
interface ThreadData {
|
|
14
|
+
readonly channel: string;
|
|
15
|
+
readonly shared: SharedState;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const data = workerData as ThreadData;
|
|
19
|
+
const port = parentPort;
|
|
20
|
+
if (!port) throw new Error("The channel thread runs only as a worker thread.");
|
|
21
|
+
const views = sharedViews(data.shared);
|
|
22
|
+
|
|
23
|
+
function open(channel: string): Socket {
|
|
24
|
+
if (channel.startsWith("fd:")) {
|
|
25
|
+
const socket = new Socket({ fd: Number(channel.slice(3)), readable: true, writable: true });
|
|
26
|
+
return socket;
|
|
27
|
+
}
|
|
28
|
+
if (channel.startsWith("unix:")) return connect(channel.slice(5));
|
|
29
|
+
throw new Error(`Unknown channel ${channel}.`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const socket = open(data.channel);
|
|
33
|
+
const splitter = new LineSplitter();
|
|
34
|
+
let nextSecret = 1;
|
|
35
|
+
let pendingSecret: string | undefined;
|
|
36
|
+
|
|
37
|
+
function write(line: string): void {
|
|
38
|
+
socket.write(line);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function deliverSecret(status: "ok" | "error", text: string): void {
|
|
42
|
+
const bytes = new TextEncoder().encode(text);
|
|
43
|
+
const length = Math.min(bytes.length, views.secretBytes.length);
|
|
44
|
+
views.secretBytes.set(bytes.subarray(0, length));
|
|
45
|
+
Atomics.store(views.control, SECRET_SLOT.length, length);
|
|
46
|
+
Atomics.store(views.control, SECRET_SLOT.status, status === "ok" ? 0 : 1);
|
|
47
|
+
Atomics.store(views.control, SECRET_SLOT.state, 2);
|
|
48
|
+
Atomics.notify(views.control, SECRET_SLOT.state);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function onLine(line: string): void {
|
|
52
|
+
let message: { id?: unknown; method?: unknown; result?: unknown; error?: unknown } | undefined;
|
|
53
|
+
try {
|
|
54
|
+
message = JSON.parse(line) as typeof message;
|
|
55
|
+
} catch {
|
|
56
|
+
message = undefined;
|
|
57
|
+
}
|
|
58
|
+
if (message && message.method === WORKER_METHODS.ping && message.id !== undefined) {
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
const beat = Number(Atomics.load(views.beat, 0));
|
|
61
|
+
const waiting = Atomics.load(views.control, SECRET_SLOT.state) === 1;
|
|
62
|
+
write(
|
|
63
|
+
encodeLine({
|
|
64
|
+
jsonrpc: "2.0",
|
|
65
|
+
id: message.id as string,
|
|
66
|
+
result: {
|
|
67
|
+
blockedMs: waiting ? 0 : Math.max(0, now - beat),
|
|
68
|
+
waitingForSecret: waiting,
|
|
69
|
+
heapUsed: Number(Atomics.load(views.beat, 1)),
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (message && typeof message.id === "string" && message.id === pendingSecret) {
|
|
76
|
+
pendingSecret = undefined;
|
|
77
|
+
if (message.error !== undefined) {
|
|
78
|
+
const error = message.error as {
|
|
79
|
+
data?: { code?: string; message?: string };
|
|
80
|
+
message?: string;
|
|
81
|
+
};
|
|
82
|
+
deliverSecret(
|
|
83
|
+
"error",
|
|
84
|
+
JSON.stringify({
|
|
85
|
+
code: error.data?.code ?? "MOSTRA_SECRET_MISSING",
|
|
86
|
+
message: error.data?.message ?? error.message ?? "The secret could not be read.",
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
} else {
|
|
90
|
+
deliverSecret(
|
|
91
|
+
"ok",
|
|
92
|
+
typeof message.result === "object" && message.result !== null
|
|
93
|
+
? String((message.result as { value?: unknown }).value ?? "")
|
|
94
|
+
: "",
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
port?.postMessage({ type: "line", line });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
socket.setEncoding("utf8");
|
|
103
|
+
socket.on("data", (chunk: string) => {
|
|
104
|
+
for (const line of splitter.push(chunk)) onLine(line);
|
|
105
|
+
});
|
|
106
|
+
const closed = (): void => port?.postMessage({ type: "closed" });
|
|
107
|
+
socket.on("end", closed);
|
|
108
|
+
socket.on("close", closed);
|
|
109
|
+
socket.on("error", closed);
|
|
110
|
+
|
|
111
|
+
port.on("message", (message: { type: string; line?: string; name?: string }) => {
|
|
112
|
+
if (message.type === "line" && message.line !== undefined) write(message.line);
|
|
113
|
+
else if (message.type === "secret" && message.name !== undefined) {
|
|
114
|
+
const id = `secret-${nextSecret++}`;
|
|
115
|
+
pendingSecret = id;
|
|
116
|
+
write(
|
|
117
|
+
encodeLine({
|
|
118
|
+
jsonrpc: "2.0",
|
|
119
|
+
id,
|
|
120
|
+
method: WORKER_METHODS.secret,
|
|
121
|
+
params: { name: message.name },
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
} else if (message.type === "end") {
|
|
125
|
+
socket.end();
|
|
126
|
+
}
|
|
127
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@mostra/worker`: the process that runs one artifact version's server code (ARCHITECTURE §7,
|
|
3
|
+
* RT-03). The supervisor starts it through a `WorkerLauncher` (D124) and speaks the worker channel
|
|
4
|
+
* of `@mostra/protocol` to it; nothing imports it as a library except launchers that need its
|
|
5
|
+
* entry point.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { workerEntry } from "@mostra/worker";
|
|
9
|
+
* spawn(process.execPath, [workerEntry(), "--channel", "unix:/tmp/w.sock", "--version-dir", dir]);
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* @packageDocumentation
|
|
13
|
+
*/
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
export { runWorker, type WorkerOptions } from "./worker.ts";
|
|
17
|
+
|
|
18
|
+
/** The absolute path of the worker's process entry, `main.js` (or `main.ts` from source). */
|
|
19
|
+
export function workerEntry(): string {
|
|
20
|
+
const extension = import.meta.url.endsWith(".ts") ? ".ts" : ".js";
|
|
21
|
+
return fileURLToPath(new URL(`./main${extension}`, import.meta.url));
|
|
22
|
+
}
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `@mostra/worker`'s process entry (`dist/main.js`): `main.js --channel fd:4|unix:<path>
|
|
4
|
+
* --version-dir <dir> [--source-dir <dir>] [--modules <node_modules>]`. A launcher starts it for one artifact version
|
|
5
|
+
* (D124); the first request on the channel is `worker/start`.
|
|
6
|
+
*/
|
|
7
|
+
import { parseArgs } from "node:util";
|
|
8
|
+
import { runWorker } from "./worker.ts";
|
|
9
|
+
|
|
10
|
+
const { values } = parseArgs({
|
|
11
|
+
options: {
|
|
12
|
+
channel: { type: "string" },
|
|
13
|
+
"version-dir": { type: "string" },
|
|
14
|
+
"source-dir": { type: "string" },
|
|
15
|
+
modules: { type: "string" },
|
|
16
|
+
},
|
|
17
|
+
strict: true,
|
|
18
|
+
});
|
|
19
|
+
if (!values.channel || !values["version-dir"]) {
|
|
20
|
+
process.stderr.write(
|
|
21
|
+
"usage: main.js --channel fd:<n>|unix:<path> --version-dir <dir> [--source-dir <dir>] [--modules <node_modules>]\n",
|
|
22
|
+
);
|
|
23
|
+
process.exit(2);
|
|
24
|
+
}
|
|
25
|
+
await runWorker({
|
|
26
|
+
channel: values.channel,
|
|
27
|
+
versionDir: values["version-dir"],
|
|
28
|
+
...(values["source-dir"] ? { sourceDir: values["source-dir"] } : {}),
|
|
29
|
+
...(values.modules ? { modules: values.modules } : {}),
|
|
30
|
+
});
|
package/src/shared.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The memory the worker's main thread and its channel thread share: the event loop's heartbeat,
|
|
3
|
+
* the heap in use, and one slot for a synchronous secret read.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Where a secret read stands, in `control`. */
|
|
7
|
+
export const SECRET_SLOT = {
|
|
8
|
+
/** 0 idle, 1 the main thread waits, 2 the answer is in `secretBytes`. */
|
|
9
|
+
state: 0,
|
|
10
|
+
/** The answer's length in bytes. */
|
|
11
|
+
length: 1,
|
|
12
|
+
/** 0 the value, 1 a JSON `{ code, message }` error. */
|
|
13
|
+
status: 2,
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
/** The largest secret answer the slot holds: a 64 KiB value, or an error. */
|
|
17
|
+
const SECRET_BYTES = 256 * 1024;
|
|
18
|
+
|
|
19
|
+
/** The shared buffers, as they cross to the channel thread. */
|
|
20
|
+
export interface SharedState {
|
|
21
|
+
readonly control: SharedArrayBuffer;
|
|
22
|
+
readonly beat: SharedArrayBuffer;
|
|
23
|
+
readonly secret: SharedArrayBuffer;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Typed views of the shared buffers. */
|
|
27
|
+
export interface SharedViews {
|
|
28
|
+
readonly control: Int32Array;
|
|
29
|
+
/** `[0]`: the last heartbeat, in ms since the epoch; `[1]`: the heap in use, in bytes. */
|
|
30
|
+
readonly beat: BigInt64Array;
|
|
31
|
+
readonly secretBytes: Uint8Array;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Allocates the shared buffers. */
|
|
35
|
+
export function createSharedState(): SharedState {
|
|
36
|
+
return {
|
|
37
|
+
control: new SharedArrayBuffer(4 * 4),
|
|
38
|
+
beat: new SharedArrayBuffer(8 * 2),
|
|
39
|
+
secret: new SharedArrayBuffer(SECRET_BYTES),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The typed views of `state`. */
|
|
44
|
+
export function sharedViews(state: SharedState): SharedViews {
|
|
45
|
+
return {
|
|
46
|
+
control: new Int32Array(state.control),
|
|
47
|
+
beat: new BigInt64Array(state.beat),
|
|
48
|
+
secretBytes: new Uint8Array(state.secret),
|
|
49
|
+
};
|
|
50
|
+
}
|