Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -2,64 +2,143 @@ import { parentPort } from 'node:worker_threads';
|
||||
import { posix } from 'node:path';
|
||||
import { db } from '../db/index.js';
|
||||
import { relDestForKind } from '../config.js';
|
||||
import { captureRecipe, recipeForDeployment, summarizeRecipe, type LaunchRecipe } from '../fleet/launch-recipes.js';
|
||||
import {
|
||||
captureRecipe,
|
||||
recipeForDeployment,
|
||||
summarizeRecipe,
|
||||
type LaunchRecipe
|
||||
} from '../fleet/launch-recipes.js';
|
||||
import { sha256Buffer } from '../fleet/files.js';
|
||||
import type { NativeLaunchInput } from './launch-preparation.js';
|
||||
import { choosePlacement, type PlacementOptions } from '../hosts/capacity.js';
|
||||
|
||||
parentPort!.on('message', (message: { id: number; kind: 'launch'; input: NativeLaunchInput } | { id: number; kind: 'placement'; input: PlacementOptions }) => {
|
||||
const { id } = message;
|
||||
try {
|
||||
if (message.kind === 'placement') {
|
||||
const decision = db().transaction(() => choosePlacement(message.input))();
|
||||
parentPort!.postMessage({ id, value: { hostId: decision.host.id, lastHeartbeat: decision.host.lastHeartbeat } });
|
||||
return;
|
||||
parentPort!.on(
|
||||
'message',
|
||||
(
|
||||
message:
|
||||
| { id: number; kind: 'launch'; input: NativeLaunchInput }
|
||||
| { id: number; kind: 'placement'; input: PlacementOptions }
|
||||
) => {
|
||||
const { id } = message;
|
||||
try {
|
||||
if (message.kind === 'placement') {
|
||||
const decision = db().transaction(() => choosePlacement(message.input))();
|
||||
parentPort!.postMessage({
|
||||
id,
|
||||
value: { hostId: decision.host.id, lastHeartbeat: decision.host.lastHeartbeat }
|
||||
});
|
||||
return;
|
||||
}
|
||||
const input = message.input;
|
||||
// All file versions and composed configuration belong to one snapshot.
|
||||
const { recipe, password } = db().transaction(() => {
|
||||
if (!input.frozenRecipeId)
|
||||
return {
|
||||
recipe: {
|
||||
...captureRecipe(input.game, input.mapSlug, input.rconPassword),
|
||||
native: input.native,
|
||||
image: input.instance.image
|
||||
},
|
||||
password: input.rconPassword
|
||||
};
|
||||
const stored = recipeForDeployment(input.frozenRecipeId)?.recipe;
|
||||
const migration = db()
|
||||
.prepare(
|
||||
'SELECT runtime_password FROM ec_host_migrations WHERE deployment_id=?'
|
||||
)
|
||||
.get(input.frozenRecipeId) as { runtime_password: string | null } | undefined;
|
||||
if (
|
||||
!stored ||
|
||||
!migration?.runtime_password ||
|
||||
stored.files.some((file) => file.content === undefined)
|
||||
) {
|
||||
throw new Error(
|
||||
'The preserved deployed recipe or runtime credentials are unavailable.'
|
||||
);
|
||||
}
|
||||
for (const file of stored.files) {
|
||||
const bytes = Buffer.from(file.content!, 'base64');
|
||||
if (bytes.length !== file.size || sha256Buffer(bytes) !== file.sha256)
|
||||
throw new Error('Preserved deployed file verification failed.');
|
||||
}
|
||||
return {
|
||||
recipe: {
|
||||
...stored,
|
||||
native: input.native,
|
||||
image: input.instance.image
|
||||
} as LaunchRecipe,
|
||||
password: migration.runtime_password
|
||||
};
|
||||
})();
|
||||
const instance = input.instance;
|
||||
const payload = {
|
||||
name: instance.server,
|
||||
game: recipe.game,
|
||||
libraryId: instance.library_id,
|
||||
image: instance.image,
|
||||
port: instance.port,
|
||||
bindAddress: instance.bind_address,
|
||||
rconPassword: password,
|
||||
env: {
|
||||
PLUTAINER_SERVER_NAME: instance.server,
|
||||
PLUTAINER_PORT: String(instance.port),
|
||||
PLUTAINER_GAME: recipe.game === 't6' ? 't6zm' : 't5sp',
|
||||
PLUTAINER_CONFIG_FILE: recipe.configFile,
|
||||
PLUTAINER_MAP_ROTATE: 'false',
|
||||
PLUTAINER_EXTRA_ARGS: `+exec electric_cherry_map.cfg +set tws_server_id ${instance.server} +map_rotate`,
|
||||
PLUTO_SERVER_KEY: input.serverKey
|
||||
},
|
||||
files: recipe.files.map((file) => ({
|
||||
path: posix.join(relDestForKind(file.kind, recipe.game), file.path),
|
||||
contentSha256: file.sha256,
|
||||
sha256: file.sha256
|
||||
})),
|
||||
...(input.rebuild
|
||||
? {
|
||||
nextGeneration: instance.generation + 1,
|
||||
...(input.safetyOverride ? { safetyOverride: true } : {}),
|
||||
...(input.unhealthyRecovery ? { unhealthyRecovery: true } : {})
|
||||
}
|
||||
: {})
|
||||
};
|
||||
const encode = (value: unknown) => new TextEncoder().encode(JSON.stringify(value));
|
||||
const blobs = [
|
||||
...new Map(
|
||||
recipe.files.map((file) => [
|
||||
file.sha256,
|
||||
{
|
||||
sha256: file.sha256,
|
||||
content: new Uint8Array(Buffer.from(file.content, 'base64'))
|
||||
}
|
||||
])
|
||||
).values()
|
||||
];
|
||||
const storedRecipe = {
|
||||
...recipe,
|
||||
files: recipe.files.map(({ content, ...file }) => ({
|
||||
...file,
|
||||
contentHash: file.sha256
|
||||
}))
|
||||
};
|
||||
const recipeJson = encode(storedRecipe);
|
||||
const payloadJson = encode(payload);
|
||||
const plan = {
|
||||
...recipe,
|
||||
files: recipe.files.map(({ content: _content, ...file }) => file)
|
||||
};
|
||||
const recipeSummary = JSON.stringify(
|
||||
summarizeRecipe(plan, sha256Buffer(Buffer.from(JSON.stringify(recipe))))
|
||||
);
|
||||
// Transfer ownership of the encoded bytes; do not clone giant strings or objects.
|
||||
parentPort!.postMessage(
|
||||
{ id, value: { recipe: plan, recipeSummary, recipeJson, payloadJson, blobs } },
|
||||
[recipeJson.buffer, payloadJson.buffer, ...blobs.map((blob) => blob.content.buffer)]
|
||||
);
|
||||
} catch (error) {
|
||||
parentPort!.postMessage({
|
||||
id,
|
||||
error: error instanceof Error ? error.message : 'Launch preparation failed.'
|
||||
});
|
||||
}
|
||||
const input = message.input;
|
||||
// All file versions and composed configuration belong to one snapshot.
|
||||
const { recipe, password } = db().transaction(() => {
|
||||
if (!input.frozenRecipeId) return { recipe: { ...captureRecipe(input.game, input.mapSlug, input.rconPassword),
|
||||
native: input.native, image: input.instance.image }, password: input.rconPassword };
|
||||
const stored = recipeForDeployment(input.frozenRecipeId)?.recipe;
|
||||
const migration = db().prepare('SELECT runtime_password FROM ec_host_migrations WHERE deployment_id=?')
|
||||
.get(input.frozenRecipeId) as { runtime_password: string | null } | undefined;
|
||||
if (!stored || !migration?.runtime_password || stored.files.some(file => file.content === undefined)) {
|
||||
throw new Error('The preserved deployed recipe or runtime credentials are unavailable.');
|
||||
}
|
||||
for (const file of stored.files) {
|
||||
const bytes = Buffer.from(file.content!, 'base64');
|
||||
if (bytes.length !== file.size || sha256Buffer(bytes) !== file.sha256) throw new Error('Preserved deployed file verification failed.');
|
||||
}
|
||||
return { recipe: { ...stored, native: input.native, image: input.instance.image } as LaunchRecipe,
|
||||
password: migration.runtime_password };
|
||||
})();
|
||||
const instance = input.instance;
|
||||
const payload = {
|
||||
name: instance.server, game: recipe.game, libraryId: instance.library_id, image: instance.image,
|
||||
port: instance.port, bindAddress: instance.bind_address, rconPassword: password,
|
||||
env: { PLUTAINER_SERVER_NAME: instance.server, PLUTAINER_PORT: String(instance.port),
|
||||
PLUTAINER_GAME: recipe.game === 't6' ? 't6zm' : 't5sp', PLUTAINER_CONFIG_FILE: recipe.configFile,
|
||||
PLUTAINER_MAP_ROTATE: 'false',
|
||||
PLUTAINER_EXTRA_ARGS: `+exec electric_cherry_map.cfg +set tws_server_id ${instance.server} +map_rotate`,
|
||||
PLUTO_SERVER_KEY: input.serverKey },
|
||||
files: recipe.files.map(file => ({ path: posix.join(relDestForKind(file.kind, recipe.game), file.path),
|
||||
contentSha256: file.sha256, sha256: file.sha256 })),
|
||||
...(input.rebuild ? { nextGeneration: instance.generation + 1,
|
||||
...(input.safetyOverride ? { safetyOverride: true } : {}),
|
||||
...(input.unhealthyRecovery ? { unhealthyRecovery: true } : {}) } : {})
|
||||
};
|
||||
const encode = (value: unknown) => new TextEncoder().encode(JSON.stringify(value));
|
||||
const blobs = [...new Map(recipe.files.map(file => [file.sha256,
|
||||
{ sha256: file.sha256, content: new Uint8Array(Buffer.from(file.content, 'base64')) }])).values()];
|
||||
const storedRecipe = { ...recipe, files: recipe.files.map(({ content, ...file }) => ({ ...file, contentHash: file.sha256 })) };
|
||||
const recipeJson = encode(storedRecipe);
|
||||
const payloadJson = encode(payload);
|
||||
const plan = { ...recipe, files: recipe.files.map(({ content: _content, ...file }) => file) };
|
||||
const recipeSummary = JSON.stringify(summarizeRecipe(plan, sha256Buffer(Buffer.from(JSON.stringify(recipe)))));
|
||||
// Transfer ownership of the encoded bytes; do not clone giant strings or objects.
|
||||
parentPort!.postMessage({ id, value: { recipe: plan, recipeSummary, recipeJson, payloadJson, blobs } },
|
||||
[recipeJson.buffer, payloadJson.buffer, ...blobs.map(blob => blob.content.buffer)]);
|
||||
} catch (error) {
|
||||
parentPort!.postMessage({ id, error: error instanceof Error ? error.message : 'Launch preparation failed.' });
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user