@prisma/composer-prisma-cloud 0.30.0-dev.2 → 0.30.0-dev.4
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/dist/auth/pack.mjs +1 -1
- package/dist/auth/pack.mjs.map +1 -1
- package/dist/auth/testing.mjs +1 -1
- package/dist/auth/testing.mjs.map +1 -1
- package/dist/buckets-main.mjs +102 -82
- package/dist/buckets-main.mjs.map +1 -1
- package/dist/compute-main.mjs +243 -231
- package/dist/compute-main.mjs.map +1 -1
- package/dist/control.mjs +3 -3
- package/dist/control.mjs.map +1 -1
- package/dist/{segments-BW2SYdBf-CtmcRzwy.mjs → daemon-D9BV062Q-ZTmWho_N.mjs} +2 -11
- package/dist/daemon-D9BV062Q-ZTmWho_N.mjs.map +1 -0
- package/dist/local-target.mjs +726 -317
- package/dist/local-target.mjs.map +1 -1
- package/dist/postgres-main.mjs +325 -193
- package/dist/postgres-main.mjs.map +1 -1
- package/dist/{s3-credentials-resource-BZjtuopJ-D0fKtjEH.mjs → s3-credentials-resource-D4jcpAZp-6qCQY8BD.mjs} +50 -10
- package/dist/{s3-credentials-resource-BZjtuopJ-D0fKtjEH.mjs.map → s3-credentials-resource-D4jcpAZp-6qCQY8BD.mjs.map} +1 -1
- package/package.json +18 -18
- package/dist/segments-BW2SYdBf-CtmcRzwy.mjs.map +0 -1
package/dist/compute-main.mjs
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as readOwnVersion, i as readJsonFile, n as StateFile, t as SERVICE_READY_TIMEOUT_ENV } from "./daemon-D9BV062Q-ZTmWho_N.mjs";
|
|
2
2
|
import { n as getPorts, r as portNumbers, t as clearLockedPorts } from "./get-port-CwLPc-ca.mjs";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import * as net$1 from "node:net";
|
|
7
|
+
import { randomBytes } from "node:crypto";
|
|
7
8
|
import * as http from "node:http";
|
|
8
9
|
//#region ../../1-prisma-cloud/0-lowering/dev-emulators/dist/compute-main.mjs
|
|
9
10
|
/**
|
|
10
11
|
* The Compute emulator daemon (local-dev spec § 2 `compute-main.ts`): a small
|
|
11
|
-
* local counterpart of the platform's compute service.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* local counterpart of the platform's compute service. Holds service
|
|
13
|
+
* instances under ids it generates, owns their child processes (spawned
|
|
14
|
+
* `bun bootstrap.js` from a real packaged artifact), supervises them (crash
|
|
15
|
+
* backoff, held after repeated fast crashes), and serves a loopback JSON
|
|
16
|
+
* admin API plus per-instance log streaming. An instance's labels are its
|
|
17
|
+
* creator's to read: they are stored and returned verbatim, never
|
|
18
|
+
* interpreted.
|
|
15
19
|
*
|
|
16
20
|
* Runs as its own OS process, started by `daemon.ts`'s `ensureDaemon` via
|
|
17
21
|
* `process.execPath <this file> --port <n> --state-dir <dir>`. Everything it
|
|
@@ -29,43 +33,50 @@ const LOG_POLL_INTERVAL_MS = 100;
|
|
|
29
33
|
const DEFAULT_READY_TIMEOUT_MS = 3e4;
|
|
30
34
|
const DEFAULT_READY_POLL_MS = 50;
|
|
31
35
|
const READY_PROBE_TIMEOUT_MS = 1e3;
|
|
32
|
-
const
|
|
36
|
+
const STATE_MODE = 384;
|
|
33
37
|
const BUN_NOT_FOUND_MESSAGE = "local dev runs services under bun — the Prisma Compute runtime — and `bun` was not found on PATH. Install it: https://bun.sh.";
|
|
34
|
-
|
|
38
|
+
const INSTANCE_ID_RE = /^svc_[0-9a-f]{16}$/;
|
|
39
|
+
function newInstanceId() {
|
|
40
|
+
return `svc_${randomBytes(8).toString("hex")}`;
|
|
41
|
+
}
|
|
42
|
+
function isInstanceStatus(value) {
|
|
35
43
|
return value === "running" || value === "backoff" || value === "held" || value === "stopped";
|
|
36
44
|
}
|
|
37
45
|
function isStringRecord(value) {
|
|
38
|
-
return typeof value === "object" && value !== null && Object.values(value).every((v) => typeof v === "string");
|
|
46
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((v) => typeof v === "string");
|
|
47
|
+
}
|
|
48
|
+
function isPort(value) {
|
|
49
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= MAX_SERVICE_PORT;
|
|
50
|
+
}
|
|
51
|
+
function isOptional(value, key, check) {
|
|
52
|
+
return !(key in value) || Reflect.get(value, key) === void 0 || check(Reflect.get(value, key));
|
|
39
53
|
}
|
|
40
|
-
function
|
|
54
|
+
function isInstanceRecord(value) {
|
|
41
55
|
if (typeof value !== "object" || value === null) return false;
|
|
42
56
|
if (!("id" in value) || typeof value.id !== "string") return false;
|
|
43
|
-
if (!("
|
|
44
|
-
if (!("port" in value) ||
|
|
45
|
-
if (!("status" in value) || !
|
|
57
|
+
if (!("labels" in value) || !isStringRecord(value.labels)) return false;
|
|
58
|
+
if (!("port" in value) || !isPort(value.port)) return false;
|
|
59
|
+
if (!("status" in value) || !isInstanceStatus(value.status)) return false;
|
|
46
60
|
if (!("logPath" in value) || typeof value.logPath !== "string") return false;
|
|
47
|
-
|
|
48
|
-
if ("lastExitCode" in value && value.lastExitCode !== void 0 && typeof value.lastExitCode !== "number") return false;
|
|
49
|
-
if ("artifactHash" in value && value.artifactHash !== void 0 && typeof value.artifactHash !== "string") return false;
|
|
50
|
-
if ("artifactDir" in value && value.artifactDir !== void 0 && typeof value.artifactDir !== "string") return false;
|
|
51
|
-
if ("env" in value && value.env !== void 0 && !isStringRecord(value.env)) return false;
|
|
52
|
-
if ("listens" in value && value.listens !== void 0 && typeof value.listens !== "boolean") return false;
|
|
53
|
-
return true;
|
|
61
|
+
return isOptional(value, "pid", (v) => typeof v === "number") && isOptional(value, "lastExitCode", (v) => typeof v === "number") && isOptional(value, "artifactHash", (v) => typeof v === "string") && isOptional(value, "artifactDir", (v) => typeof v === "string") && isOptional(value, "env", isStringRecord) && isOptional(value, "listens", (v) => typeof v === "boolean");
|
|
54
62
|
}
|
|
55
|
-
function
|
|
56
|
-
return typeof value === "object" && value !== null && "
|
|
63
|
+
function isInstancesState(value) {
|
|
64
|
+
return typeof value === "object" && value !== null && "instances" in value && typeof value.instances === "object" && value.instances !== null && Object.values(value.instances).every(isInstanceRecord);
|
|
57
65
|
}
|
|
58
|
-
function
|
|
59
|
-
return typeof value === "object" && value !== null &&
|
|
66
|
+
function isCreateBody(value) {
|
|
67
|
+
return typeof value === "object" && value !== null && "labels" in value && isStringRecord(value.labels) && isOptional(value, "port", isPort);
|
|
60
68
|
}
|
|
61
69
|
function isDeploymentBody(value) {
|
|
62
|
-
return typeof value === "object" && value !== null && "
|
|
70
|
+
return typeof value === "object" && value !== null && "artifactDir" in value && typeof value.artifactDir === "string" && "artifactHash" in value && typeof value.artifactHash === "string" && "env" in value && isStringRecord(value.env) && (!("listens" in value) || value.listens === void 0 || typeof value.listens === "boolean");
|
|
71
|
+
}
|
|
72
|
+
function isIdsBody(value) {
|
|
73
|
+
return typeof value === "object" && value !== null && "ids" in value && Array.isArray(value.ids) && value.ids.every((id) => typeof id === "string");
|
|
63
74
|
}
|
|
64
75
|
function sleep(ms) {
|
|
65
76
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
66
77
|
}
|
|
67
|
-
/** Append-only per-
|
|
68
|
-
var
|
|
78
|
+
/** Append-only per-instance log file — child stdout/stderr plus `[emulator]` supervision lines. No rotation in v1. */
|
|
79
|
+
var InstanceLog = class {
|
|
69
80
|
filePath;
|
|
70
81
|
fd;
|
|
71
82
|
constructor(filePath) {
|
|
@@ -84,7 +95,7 @@ var ServiceLog = class {
|
|
|
84
95
|
writeLine(line) {
|
|
85
96
|
fs.writeSync(this.ensureOpen(), `${line}\n`);
|
|
86
97
|
}
|
|
87
|
-
/** Empties the file — a fresh
|
|
98
|
+
/** Empties the file — a fresh start begins a clean log; past sessions' and past deployments' output is not kept. */
|
|
88
99
|
clear() {
|
|
89
100
|
fs.ftruncateSync(this.ensureOpen(), 0);
|
|
90
101
|
}
|
|
@@ -170,17 +181,14 @@ function resolveBunOnPath(env) {
|
|
|
170
181
|
function main() {
|
|
171
182
|
const { port, stateDir, readyTimeoutMs, readyPollMs } = parseArgs(process.argv.slice(2));
|
|
172
183
|
const ownVersion = readOwnVersion();
|
|
173
|
-
const
|
|
174
|
-
const stateFile = new StateFile(
|
|
175
|
-
|
|
184
|
+
const statePath = path.join(stateDir, "instances.json");
|
|
185
|
+
const stateFile = new StateFile(statePath, STATE_MODE);
|
|
186
|
+
const logsDir = path.join(stateDir, "logs");
|
|
187
|
+
let state = { instances: {} };
|
|
176
188
|
const runtimes = /* @__PURE__ */ new Map();
|
|
177
189
|
const logGeneration = /* @__PURE__ */ new Map();
|
|
178
|
-
function
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
function getRuntime(app, id) {
|
|
182
|
-
const key = runtimeKey(app, id);
|
|
183
|
-
let rt = runtimes.get(key);
|
|
190
|
+
function getRuntime(id) {
|
|
191
|
+
let rt = runtimes.get(id);
|
|
184
192
|
if (!rt) {
|
|
185
193
|
rt = {
|
|
186
194
|
child: void 0,
|
|
@@ -194,16 +202,13 @@ function main() {
|
|
|
194
202
|
stopping: void 0,
|
|
195
203
|
deleted: false
|
|
196
204
|
};
|
|
197
|
-
runtimes.set(
|
|
205
|
+
runtimes.set(id, rt);
|
|
198
206
|
}
|
|
199
207
|
return rt;
|
|
200
208
|
}
|
|
201
|
-
function
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
function getLog(app, id, logPath) {
|
|
205
|
-
const rt = getRuntime(app, id);
|
|
206
|
-
if (!rt.log) rt.log = new ServiceLog(logPath);
|
|
209
|
+
function getLog(inst) {
|
|
210
|
+
const rt = getRuntime(inst.id);
|
|
211
|
+
if (!rt.log) rt.log = new InstanceLog(inst.logPath);
|
|
207
212
|
return rt.log;
|
|
208
213
|
}
|
|
209
214
|
function schedulePersist() {
|
|
@@ -217,41 +222,41 @@ function main() {
|
|
|
217
222
|
if (rt.backoffTimer) clearTimeout(rt.backoffTimer);
|
|
218
223
|
rt.backoffTimer = void 0;
|
|
219
224
|
}
|
|
220
|
-
function
|
|
221
|
-
|
|
222
|
-
for (const app of Object.values(state)) for (const svc of Object.values(app.services)) ports.add(svc.port);
|
|
223
|
-
return ports;
|
|
225
|
+
function heldPorts() {
|
|
226
|
+
return new Set(Object.values(state.instances).map((inst) => inst.port));
|
|
224
227
|
}
|
|
225
228
|
/**
|
|
226
|
-
* The
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
229
|
+
* The requested port when no instance holds it and it is bindable,
|
|
230
|
+
* otherwise the smallest such port at or above `MIN_SERVICE_PORT`. Which
|
|
231
|
+
* ports instances hold is ours; whether a candidate is actually bindable is
|
|
232
|
+
* `get-port`'s (spec § 2's dependency razor).
|
|
230
233
|
*/
|
|
231
|
-
async function
|
|
234
|
+
async function allocatePort(requested) {
|
|
235
|
+
const exclude = heldPorts();
|
|
236
|
+
if (requested !== void 0 && !exclude.has(requested)) {
|
|
237
|
+
const given = await getPorts({
|
|
238
|
+
port: requested,
|
|
239
|
+
exclude
|
|
240
|
+
});
|
|
241
|
+
if (given === requested) return given;
|
|
242
|
+
}
|
|
232
243
|
return getPorts({
|
|
233
244
|
port: portNumbers(MIN_SERVICE_PORT, MAX_SERVICE_PORT),
|
|
234
|
-
exclude
|
|
245
|
+
exclude
|
|
235
246
|
});
|
|
236
247
|
}
|
|
237
|
-
async function
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
appRec = { services: {} };
|
|
241
|
-
state[app] = appRec;
|
|
242
|
-
}
|
|
243
|
-
let svc = appRec.services[id];
|
|
244
|
-
if (svc) return svc;
|
|
245
|
-
svc = {
|
|
248
|
+
async function createInstance(body) {
|
|
249
|
+
const id = newInstanceId();
|
|
250
|
+
const inst = {
|
|
246
251
|
id,
|
|
247
|
-
|
|
248
|
-
port: await
|
|
252
|
+
labels: { ...body.labels },
|
|
253
|
+
port: await allocatePort(body.port),
|
|
249
254
|
status: "stopped",
|
|
250
|
-
logPath:
|
|
255
|
+
logPath: path.join(logsDir, `${id}.log`)
|
|
251
256
|
};
|
|
252
|
-
|
|
257
|
+
state.instances[id] = inst;
|
|
253
258
|
schedulePersist();
|
|
254
|
-
return
|
|
259
|
+
return inst;
|
|
255
260
|
}
|
|
256
261
|
function waitForExit(child) {
|
|
257
262
|
return new Promise((resolve) => {
|
|
@@ -271,12 +276,12 @@ function main() {
|
|
|
271
276
|
await exited;
|
|
272
277
|
}
|
|
273
278
|
}
|
|
274
|
-
function handleChildExit(
|
|
279
|
+
function handleChildExit(inst, rt, child, code) {
|
|
275
280
|
if (child !== rt.child) return;
|
|
276
281
|
clearStableTimer(rt);
|
|
277
282
|
rt.child = void 0;
|
|
278
|
-
delete
|
|
279
|
-
if (code !== null)
|
|
283
|
+
delete inst.pid;
|
|
284
|
+
if (code !== null) inst.lastExitCode = code;
|
|
280
285
|
if (rt.expectedExit) {
|
|
281
286
|
rt.expectedExit = false;
|
|
282
287
|
schedulePersist();
|
|
@@ -287,21 +292,21 @@ function main() {
|
|
|
287
292
|
const attemptNumber = rt.consecutiveFastExits;
|
|
288
293
|
if (uptime < STABLE_UPTIME_MS) rt.consecutiveFastExits += 1;
|
|
289
294
|
if (rt.consecutiveFastExits >= MAX_CONSECUTIVE_FAST_EXITS) {
|
|
290
|
-
|
|
295
|
+
inst.status = "held";
|
|
291
296
|
schedulePersist();
|
|
292
|
-
getLog(
|
|
297
|
+
getLog(inst).writeLine(`[emulator] held after ${String(MAX_CONSECUTIVE_FAST_EXITS)} consecutive fast exits — send a new deployment to resume`);
|
|
293
298
|
return;
|
|
294
299
|
}
|
|
295
300
|
const delayMs = Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * 2 ** attemptNumber);
|
|
296
|
-
|
|
301
|
+
inst.status = "backoff";
|
|
297
302
|
schedulePersist();
|
|
298
|
-
getLog(
|
|
303
|
+
getLog(inst).writeLine(`[emulator] exited (code ${code === null ? "null" : String(code)}) — restarting in ${String(delayMs / 1e3)}s`);
|
|
299
304
|
rt.backoffTimer = setTimeout(() => {
|
|
300
305
|
rt.backoffTimer = void 0;
|
|
301
|
-
|
|
306
|
+
spawnInstance(inst, rt, false);
|
|
302
307
|
}, delayMs);
|
|
303
308
|
}
|
|
304
|
-
/** Records `run` as the
|
|
309
|
+
/** Records `run` as the instance's start in flight, replacing any earlier one. */
|
|
305
310
|
function beginStart(rt, fresh, run) {
|
|
306
311
|
const token = { processExited: false };
|
|
307
312
|
const result = run(token).finally(() => {
|
|
@@ -318,41 +323,41 @@ function main() {
|
|
|
318
323
|
function joinableStart(rt) {
|
|
319
324
|
return rt.start?.token.processExited === false ? rt.start : void 0;
|
|
320
325
|
}
|
|
321
|
-
/** Undefined while `token` is still the
|
|
322
|
-
function supersededOutcome(
|
|
326
|
+
/** Undefined while `token` is still the instance's start. Otherwise what that start reports: a newer start's result, or "stopped" after a stop or a delete. */
|
|
327
|
+
function supersededOutcome(inst, rt, token) {
|
|
323
328
|
if (!rt.deleted && rt.start?.token === token) return void 0;
|
|
324
329
|
if (!rt.deleted && rt.start) return rt.start.result;
|
|
325
330
|
return Promise.resolve({
|
|
326
331
|
ok: false,
|
|
327
|
-
message: `
|
|
332
|
+
message: `instance "${inst.id}" was stopped before it accepted connections`
|
|
328
333
|
});
|
|
329
334
|
}
|
|
330
335
|
/**
|
|
331
|
-
* Spawns the
|
|
336
|
+
* Spawns the instance's child and resolves once it accepts connections on
|
|
332
337
|
* its port — only then is it `running`. A child that exits first, or never
|
|
333
338
|
* listens within the readiness timeout, fails the start with a message
|
|
334
|
-
* naming the
|
|
339
|
+
* naming the instance and its log.
|
|
335
340
|
*/
|
|
336
|
-
function
|
|
337
|
-
return beginStart(rt, fresh, (token) => spawnAndWaitUntilListening(
|
|
341
|
+
function spawnInstance(inst, rt, fresh) {
|
|
342
|
+
return beginStart(rt, fresh, (token) => spawnAndWaitUntilListening(inst, rt, fresh, token));
|
|
338
343
|
}
|
|
339
|
-
async function spawnAndWaitUntilListening(
|
|
340
|
-
const env =
|
|
341
|
-
const artifactDir =
|
|
344
|
+
async function spawnAndWaitUntilListening(inst, rt, fresh, token) {
|
|
345
|
+
const env = inst.env ?? {};
|
|
346
|
+
const artifactDir = inst.artifactDir ?? ".";
|
|
342
347
|
const bunPath = resolveBunOnPath(env);
|
|
343
348
|
if (!bunPath) {
|
|
344
|
-
|
|
345
|
-
delete
|
|
349
|
+
inst.status = "stopped";
|
|
350
|
+
delete inst.pid;
|
|
346
351
|
schedulePersist();
|
|
347
352
|
return {
|
|
348
353
|
ok: false,
|
|
349
354
|
message: BUN_NOT_FOUND_MESSAGE
|
|
350
355
|
};
|
|
351
356
|
}
|
|
352
|
-
const log = getLog(
|
|
357
|
+
const log = getLog(inst);
|
|
353
358
|
if (fresh) {
|
|
354
359
|
log.clear();
|
|
355
|
-
logGeneration.set(
|
|
360
|
+
logGeneration.set(inst.logPath, (logGeneration.get(inst.logPath) ?? 0) + 1);
|
|
356
361
|
}
|
|
357
362
|
const child = spawn(bunPath, ["bootstrap.js"], {
|
|
358
363
|
cwd: artifactDir,
|
|
@@ -372,52 +377,52 @@ function main() {
|
|
|
372
377
|
}, STABLE_UPTIME_MS);
|
|
373
378
|
child.stdout?.on("data", (chunk) => log.writeRaw(chunk));
|
|
374
379
|
child.stderr?.on("data", (chunk) => log.writeRaw(chunk));
|
|
375
|
-
child.on("exit", (code) => handleChildExit(
|
|
380
|
+
child.on("exit", (code) => handleChildExit(inst, rt, child, code));
|
|
376
381
|
child.once("exit", () => {
|
|
377
382
|
token.processExited = true;
|
|
378
383
|
});
|
|
379
|
-
if (child.pid !== void 0)
|
|
380
|
-
if (
|
|
381
|
-
|
|
384
|
+
if (child.pid !== void 0) inst.pid = child.pid;
|
|
385
|
+
if (inst.listens === false) {
|
|
386
|
+
inst.status = "running";
|
|
382
387
|
schedulePersist();
|
|
383
388
|
return { ok: true };
|
|
384
389
|
}
|
|
385
390
|
schedulePersist();
|
|
386
|
-
const port = String(
|
|
387
|
-
const outcome = await waitUntilListening(child,
|
|
388
|
-
const superseded = supersededOutcome(
|
|
391
|
+
const port = String(inst.port);
|
|
392
|
+
const outcome = await waitUntilListening(child, inst.port, readyTimeoutMs, readyPollMs);
|
|
393
|
+
const superseded = supersededOutcome(inst, rt, token);
|
|
389
394
|
if (superseded) return superseded;
|
|
390
|
-
const
|
|
395
|
+
const instance = `instance "${inst.id}"`;
|
|
391
396
|
if (outcome.kind === "exited") return {
|
|
392
397
|
ok: false,
|
|
393
|
-
message: `${
|
|
398
|
+
message: `${instance} exited (${outcome.code !== null ? `code ${String(outcome.code)}` : outcome.signal ?? "no exit code"}) before it accepted connections on its port ${port}. Its log: ${inst.logPath}`
|
|
394
399
|
};
|
|
395
400
|
if (outcome.kind === "timed-out") {
|
|
396
401
|
const seconds = String(readyTimeoutMs / 1e3);
|
|
397
|
-
const message = `${
|
|
402
|
+
const message = `${instance} did not accept connections on its port ${port} within ${seconds}s. Its log: ${inst.logPath}. If it needs longer to start, set ${SERVICE_READY_TIMEOUT_ENV} (milliseconds) and restart the compute emulator. If it never listens on its port, deploy it with listens: false.`;
|
|
398
403
|
if (fresh) {
|
|
399
404
|
log.writeLine(`[emulator] did not accept connections on port ${port} within ${seconds}s — stopped`);
|
|
400
|
-
await
|
|
405
|
+
await stopInstance(inst, rt);
|
|
401
406
|
} else {
|
|
402
407
|
log.writeLine(`[emulator] restart did not accept connections on port ${port} within ${seconds}s — held; send a new deployment to resume`);
|
|
403
|
-
await
|
|
408
|
+
await stopInstance(inst, rt, "held");
|
|
404
409
|
}
|
|
405
410
|
return {
|
|
406
411
|
ok: false,
|
|
407
412
|
message
|
|
408
413
|
};
|
|
409
414
|
}
|
|
410
|
-
|
|
415
|
+
inst.status = "running";
|
|
411
416
|
schedulePersist();
|
|
412
417
|
return { ok: true };
|
|
413
418
|
}
|
|
414
|
-
async function
|
|
419
|
+
async function stopInstance(inst, rt, finalStatus = "stopped") {
|
|
415
420
|
rt.start = void 0;
|
|
416
421
|
clearBackoffTimer(rt);
|
|
417
422
|
clearStableTimer(rt);
|
|
418
423
|
const stopping = killChild(rt, TERMINATE_GRACE_MS).then(() => {
|
|
419
|
-
|
|
420
|
-
delete
|
|
424
|
+
inst.status = finalStatus;
|
|
425
|
+
delete inst.pid;
|
|
421
426
|
schedulePersist();
|
|
422
427
|
});
|
|
423
428
|
rt.stopping = stopping;
|
|
@@ -432,28 +437,39 @@ function main() {
|
|
|
432
437
|
}
|
|
433
438
|
/**
|
|
434
439
|
* The session-resume signal: Alchemy's no-op reconcile never fires a
|
|
435
|
-
* deployment PUT, so nothing else restarts
|
|
440
|
+
* deployment PUT, so nothing else restarts an instance a previous session
|
|
436
441
|
* stopped. Reuses the exact deployment-PUT start rules — same spawn
|
|
437
442
|
* path, same supervision reset, and an explicit resume clears `held`
|
|
438
|
-
* the same way a deployment PUT does — but from the
|
|
439
|
-
* STORED deployment
|
|
443
|
+
* the same way a deployment PUT does — but from the instance's already
|
|
444
|
+
* STORED deployment, since no new one arrives with a start.
|
|
440
445
|
*/
|
|
441
|
-
async function
|
|
442
|
-
if (
|
|
443
|
-
const rt = getRuntime(
|
|
446
|
+
async function startInstance(inst) {
|
|
447
|
+
if (inst.artifactHash === void 0) return { ok: true };
|
|
448
|
+
const rt = getRuntime(inst.id);
|
|
444
449
|
const inFlight = joinableStart(rt);
|
|
445
450
|
if (inFlight) return inFlight.result;
|
|
446
|
-
if (
|
|
451
|
+
if (inst.status === "running" && rt.stopping === void 0) return { ok: true };
|
|
447
452
|
return beginStart(rt, true, async (token) => {
|
|
448
453
|
clearBackoffTimer(rt);
|
|
449
454
|
await stopsFinished(rt);
|
|
450
|
-
const superseded = supersededOutcome(
|
|
455
|
+
const superseded = supersededOutcome(inst, rt, token);
|
|
451
456
|
if (superseded) return superseded;
|
|
452
457
|
clearStableTimer(rt);
|
|
453
458
|
rt.consecutiveFastExits = 0;
|
|
454
|
-
return spawnAndWaitUntilListening(
|
|
459
|
+
return spawnAndWaitUntilListening(inst, rt, true, token);
|
|
455
460
|
});
|
|
456
461
|
}
|
|
462
|
+
async function deleteInstance(inst) {
|
|
463
|
+
const rt = getRuntime(inst.id);
|
|
464
|
+
rt.deleted = true;
|
|
465
|
+
await stopInstance(inst, rt);
|
|
466
|
+
rt.log?.close();
|
|
467
|
+
runtimes.delete(inst.id);
|
|
468
|
+
delete state.instances[inst.id];
|
|
469
|
+
schedulePersist();
|
|
470
|
+
clearLockedPorts();
|
|
471
|
+
await fs.promises.rm(inst.logPath, { force: true });
|
|
472
|
+
}
|
|
457
473
|
function json(res, status, body) {
|
|
458
474
|
const text = JSON.stringify(body);
|
|
459
475
|
res.writeHead(status, { "content-type": "application/json" });
|
|
@@ -463,8 +479,9 @@ function main() {
|
|
|
463
479
|
res.writeHead(status, { "content-type": "text/plain; charset=utf-8" });
|
|
464
480
|
res.end(body);
|
|
465
481
|
}
|
|
466
|
-
function
|
|
467
|
-
|
|
482
|
+
function noContent(res) {
|
|
483
|
+
res.writeHead(204);
|
|
484
|
+
res.end();
|
|
468
485
|
}
|
|
469
486
|
function readBody(req) {
|
|
470
487
|
return new Promise((resolve, reject) => {
|
|
@@ -474,17 +491,25 @@ function main() {
|
|
|
474
491
|
req.on("error", reject);
|
|
475
492
|
});
|
|
476
493
|
}
|
|
477
|
-
function
|
|
494
|
+
async function readJsonBody(req) {
|
|
495
|
+
const raw = await readBody(req);
|
|
496
|
+
try {
|
|
497
|
+
return JSON.parse(raw.toString("utf8"));
|
|
498
|
+
} catch {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
function instanceView(inst) {
|
|
478
503
|
return {
|
|
479
|
-
id:
|
|
480
|
-
|
|
481
|
-
port:
|
|
482
|
-
url: `http://localhost:${String(
|
|
483
|
-
status:
|
|
484
|
-
...
|
|
485
|
-
...
|
|
486
|
-
...
|
|
487
|
-
logPath:
|
|
504
|
+
id: inst.id,
|
|
505
|
+
labels: inst.labels,
|
|
506
|
+
port: inst.port,
|
|
507
|
+
url: `http://localhost:${String(inst.port)}`,
|
|
508
|
+
status: inst.status,
|
|
509
|
+
...inst.pid !== void 0 ? { pid: inst.pid } : {},
|
|
510
|
+
...inst.lastExitCode !== void 0 ? { lastExitCode: inst.lastExitCode } : {},
|
|
511
|
+
...inst.artifactHash !== void 0 ? { artifactHash: inst.artifactHash } : {},
|
|
512
|
+
logPath: inst.logPath
|
|
488
513
|
};
|
|
489
514
|
}
|
|
490
515
|
function envEquals(a, b) {
|
|
@@ -494,47 +519,37 @@ function main() {
|
|
|
494
519
|
if (aKeys.length !== bKeys.length) return false;
|
|
495
520
|
return aKeys.every((k) => a[k] === b[k]);
|
|
496
521
|
}
|
|
497
|
-
async function handleDeployment(req, res,
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
return text(res, 400, "malformed JSON body");
|
|
504
|
-
}
|
|
505
|
-
if (!isDeploymentBody(parsed)) return text(res, 400, "malformed deployment body");
|
|
506
|
-
const svc = await getOrCreateService(app, id);
|
|
507
|
-
const rt = getRuntime(app, id);
|
|
508
|
-
const listens = parsed.listens ?? true;
|
|
509
|
-
const changed = svc.artifactHash !== parsed.artifactHash || !envEquals(svc.env, parsed.env) || (svc.listens ?? true) !== listens;
|
|
522
|
+
async function handleDeployment(req, res, inst) {
|
|
523
|
+
const body = await readJsonBody(req);
|
|
524
|
+
if (!isDeploymentBody(body)) return text(res, 400, "malformed deployment body");
|
|
525
|
+
const rt = getRuntime(inst.id);
|
|
526
|
+
const listens = body.listens ?? true;
|
|
527
|
+
const changed = inst.artifactHash !== body.artifactHash || !envEquals(inst.env, body.env) || (inst.listens ?? true) !== listens;
|
|
510
528
|
let result;
|
|
511
529
|
const inFlight = joinableStart(rt);
|
|
512
530
|
if (!changed && inFlight?.fresh === true) result = await inFlight.result;
|
|
513
|
-
else if (!changed && rt.start === void 0 && rt.stopping === void 0 &&
|
|
531
|
+
else if (!changed && rt.start === void 0 && rt.stopping === void 0 && inst.status === "running") result = { ok: true };
|
|
514
532
|
else {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
svc.artifactDir = parsed.artifactDir;
|
|
520
|
-
svc.listens = listens;
|
|
533
|
+
inst.artifactHash = body.artifactHash;
|
|
534
|
+
inst.env = body.env;
|
|
535
|
+
inst.artifactDir = body.artifactDir;
|
|
536
|
+
inst.listens = listens;
|
|
521
537
|
clearBackoffTimer(rt);
|
|
522
538
|
schedulePersist();
|
|
523
539
|
result = await beginStart(rt, true, async (token) => {
|
|
524
540
|
await stopsFinished(rt);
|
|
525
541
|
await killChild(rt, TERMINATE_GRACE_MS);
|
|
526
|
-
const superseded = supersededOutcome(
|
|
542
|
+
const superseded = supersededOutcome(inst, rt, token);
|
|
527
543
|
if (superseded) return superseded;
|
|
528
|
-
|
|
529
|
-
delete
|
|
544
|
+
inst.status = "stopped";
|
|
545
|
+
delete inst.pid;
|
|
530
546
|
clearStableTimer(rt);
|
|
531
547
|
rt.consecutiveFastExits = 0;
|
|
532
|
-
return spawnAndWaitUntilListening(
|
|
548
|
+
return spawnAndWaitUntilListening(inst, rt, true, token);
|
|
533
549
|
});
|
|
534
550
|
}
|
|
535
|
-
if (!result.ok) return text(res, 500, result.message ?? "failed to start the
|
|
536
|
-
res
|
|
537
|
-
res.end();
|
|
551
|
+
if (!result.ok) return text(res, 500, result.message ?? "failed to start the instance");
|
|
552
|
+
noContent(res);
|
|
538
553
|
}
|
|
539
554
|
/** Byte offset a follow should start at to show the last `tailLines` complete lines; `<= 0` (or an empty file) starts at the current end, no backlog. */
|
|
540
555
|
function startOffsetForTail(logPath, tailLines) {
|
|
@@ -600,86 +615,71 @@ function main() {
|
|
|
600
615
|
};
|
|
601
616
|
res.on("close", stop);
|
|
602
617
|
}
|
|
618
|
+
function handleLogs(url, res, inst) {
|
|
619
|
+
if (url.searchParams.get("follow") === "1") {
|
|
620
|
+
const tailParam = url.searchParams.get("tail");
|
|
621
|
+
const tailLines = tailParam === null ? 0 : Math.max(0, Number.parseInt(tailParam, 10) || 0);
|
|
622
|
+
handleFollowLogs(res, inst.logPath, tailLines);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
const content = fs.existsSync(inst.logPath) ? fs.readFileSync(inst.logPath) : Buffer.alloc(0);
|
|
626
|
+
res.writeHead(200, { "content-type": "text/plain; charset=utf-8" });
|
|
627
|
+
res.end(content);
|
|
628
|
+
}
|
|
629
|
+
/** The instances `ids` name, or the ids no instance has. */
|
|
630
|
+
function instancesByIds(ids) {
|
|
631
|
+
const unknown = ids.filter((id) => state.instances[id] === void 0);
|
|
632
|
+
if (unknown.length > 0) return { unknown };
|
|
633
|
+
return { found: ids.flatMap((id) => state.instances[id] ?? []) };
|
|
634
|
+
}
|
|
635
|
+
function unknownInstances(res, ids) {
|
|
636
|
+
text(res, 404, `no such instance: ${ids.map((id) => `"${id}"`).join(", ")}`);
|
|
637
|
+
}
|
|
638
|
+
async function handleBulk(req, res, action) {
|
|
639
|
+
const body = await readJsonBody(req);
|
|
640
|
+
if (!isIdsBody(body)) return text(res, 400, "malformed body: expected { \"ids\": string[] }");
|
|
641
|
+
if (action === "stop") {
|
|
642
|
+
const known = body.ids.flatMap((id) => state.instances[id] ?? []);
|
|
643
|
+
await Promise.all(known.map((inst) => stopInstance(inst, getRuntime(inst.id))));
|
|
644
|
+
return noContent(res);
|
|
645
|
+
}
|
|
646
|
+
const selected = instancesByIds(body.ids);
|
|
647
|
+
if ("unknown" in selected) return unknownInstances(res, selected.unknown);
|
|
648
|
+
const failures = (await Promise.all(selected.found.map(startInstance))).flatMap((r) => r.ok ? [] : [r.message ?? "failed to start"]);
|
|
649
|
+
if (failures.length > 0) return text(res, 500, failures.join("\n"));
|
|
650
|
+
noContent(res);
|
|
651
|
+
}
|
|
603
652
|
async function handleRequest(req, res) {
|
|
604
653
|
const url = new URL(req.url ?? "/", `http://127.0.0.1:${String(port)}`);
|
|
605
654
|
const method = req.method ?? "GET";
|
|
606
655
|
const segments = url.pathname.split("/").filter((s) => s.length > 0).map((s) => decodeURIComponent(s));
|
|
607
656
|
if (method === "GET" && segments.length === 1 && segments[0] === "health") return json(res, 200, { version: ownVersion });
|
|
608
|
-
if (segments[0]
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
}
|
|
620
|
-
if (method === "PUT" && segments.length === 5 && segments[2] === "services" && segments[4] === "deployment") {
|
|
621
|
-
const id = segments[3];
|
|
622
|
-
if (id === void 0 || !isValidSegment(id)) return badSegment(res, id ?? "");
|
|
623
|
-
return handleDeployment(req, res, app, id);
|
|
624
|
-
}
|
|
625
|
-
if (method === "GET" && segments.length === 3 && segments[2] === "services") {
|
|
626
|
-
const appRec = state[app];
|
|
627
|
-
return json(res, 200, appRec ? Object.values(appRec.services).map(serviceView) : []);
|
|
628
|
-
}
|
|
629
|
-
if (method === "GET" && segments.length === 5 && segments[2] === "services" && segments[4] === "logs") {
|
|
630
|
-
const id = segments[3];
|
|
631
|
-
if (id === void 0 || !isValidSegment(id)) return badSegment(res, id ?? "");
|
|
632
|
-
const svc = state[app]?.services[id];
|
|
633
|
-
if (!svc) return text(res, 404, `no such service "${id}" in app "${app}"`);
|
|
634
|
-
if (url.searchParams.get("follow") === "1") {
|
|
635
|
-
const tailParam = url.searchParams.get("tail");
|
|
636
|
-
const tailLines = tailParam === null ? 0 : Math.max(0, Number.parseInt(tailParam, 10) || 0);
|
|
637
|
-
handleFollowLogs(res, svc.logPath, tailLines);
|
|
638
|
-
return;
|
|
639
|
-
}
|
|
640
|
-
const content = fs.existsSync(svc.logPath) ? fs.readFileSync(svc.logPath) : Buffer.alloc(0);
|
|
641
|
-
res.writeHead(200, { "content-type": "text/plain; charset=utf-8" });
|
|
642
|
-
res.end(content);
|
|
643
|
-
return;
|
|
644
|
-
}
|
|
645
|
-
if (method === "POST" && segments.length === 3 && segments[2] === "stop") {
|
|
646
|
-
const appRec = state[app];
|
|
647
|
-
if (appRec) await Promise.all(Object.entries(appRec.services).map(([id, svc]) => stopService(svc, getRuntime(app, id))));
|
|
648
|
-
res.writeHead(204);
|
|
649
|
-
res.end();
|
|
650
|
-
return;
|
|
651
|
-
}
|
|
652
|
-
if (method === "POST" && segments.length === 3 && segments[2] === "start") {
|
|
653
|
-
const appRec = state[app];
|
|
654
|
-
const failures = (appRec ? await Promise.all(Object.entries(appRec.services).map(([id, svc]) => startService(app, id, svc))) : []).flatMap((r) => r.ok ? [] : [r.message ?? "failed to start"]);
|
|
655
|
-
if (failures.length > 0) return text(res, 500, failures.join("\n"));
|
|
656
|
-
res.writeHead(204);
|
|
657
|
-
res.end();
|
|
658
|
-
return;
|
|
659
|
-
}
|
|
660
|
-
if (method === "DELETE" && segments.length === 2) {
|
|
661
|
-
const appRec = state[app];
|
|
662
|
-
if (appRec) {
|
|
663
|
-
for (const id of Object.keys(appRec.services)) getRuntime(app, id).deleted = true;
|
|
664
|
-
await Promise.all(Object.entries(appRec.services).map(([id, svc]) => stopService(svc, getRuntime(app, id))));
|
|
665
|
-
for (const id of Object.keys(appRec.services)) {
|
|
666
|
-
const key = runtimeKey(app, id);
|
|
667
|
-
runtimes.get(key)?.log?.close();
|
|
668
|
-
runtimes.delete(key);
|
|
669
|
-
}
|
|
670
|
-
delete state[app];
|
|
671
|
-
schedulePersist();
|
|
672
|
-
clearLockedPorts();
|
|
673
|
-
}
|
|
674
|
-
await fs.promises.rm(path.join(stateDir, "logs", app), {
|
|
675
|
-
recursive: true,
|
|
676
|
-
force: true
|
|
677
|
-
});
|
|
678
|
-
res.writeHead(204);
|
|
679
|
-
res.end();
|
|
680
|
-
return;
|
|
657
|
+
if (segments[0] !== "instances") {
|
|
658
|
+
res.writeHead(404);
|
|
659
|
+
res.end();
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (segments.length === 1) {
|
|
663
|
+
if (method === "GET") return json(res, 200, Object.values(state.instances).map(instanceView));
|
|
664
|
+
if (method === "POST") {
|
|
665
|
+
const body = await readJsonBody(req);
|
|
666
|
+
if (!isCreateBody(body)) return text(res, 400, "malformed body: expected { \"labels\": Record<string, string>, \"port\"?: number }");
|
|
667
|
+
return json(res, 201, instanceView(await createInstance(body)));
|
|
681
668
|
}
|
|
682
669
|
}
|
|
670
|
+
if (method === "POST" && segments.length === 2 && segments[1] === "start") return handleBulk(req, res, "start");
|
|
671
|
+
if (method === "POST" && segments.length === 2 && segments[1] === "stop") return handleBulk(req, res, "stop");
|
|
672
|
+
const id = segments[1];
|
|
673
|
+
if (id !== void 0 && !INSTANCE_ID_RE.test(id)) return text(res, 400, `invalid instance id "${id}"`);
|
|
674
|
+
const inst = id === void 0 ? void 0 : state.instances[id];
|
|
675
|
+
if (method === "DELETE" && segments.length === 2) {
|
|
676
|
+
if (inst) await deleteInstance(inst);
|
|
677
|
+
return noContent(res);
|
|
678
|
+
}
|
|
679
|
+
if (!inst) return unknownInstances(res, id === void 0 ? [] : [id]);
|
|
680
|
+
if (method === "GET" && segments.length === 2) return json(res, 200, instanceView(inst));
|
|
681
|
+
if (method === "PUT" && segments.length === 3 && segments[2] === "deployment") return handleDeployment(req, res, inst);
|
|
682
|
+
if (method === "GET" && segments.length === 3 && segments[2] === "logs") return handleLogs(url, res, inst);
|
|
683
683
|
res.writeHead(404);
|
|
684
684
|
res.end();
|
|
685
685
|
}
|
|
@@ -706,11 +706,11 @@ function main() {
|
|
|
706
706
|
}
|
|
707
707
|
process.on("SIGTERM", () => void shutdown());
|
|
708
708
|
process.on("SIGINT", () => void shutdown());
|
|
709
|
-
readJsonFile(
|
|
709
|
+
discardStateFromEarlierVersions(stateDir, statePath, logsDir).then(() => readJsonFile(statePath, isInstancesState)).then((loaded) => {
|
|
710
710
|
if (loaded) {
|
|
711
|
-
for (const
|
|
712
|
-
|
|
713
|
-
delete
|
|
711
|
+
for (const inst of Object.values(loaded.instances)) if (inst.status === "running" || inst.status === "backoff") {
|
|
712
|
+
inst.status = "stopped";
|
|
713
|
+
delete inst.pid;
|
|
714
714
|
}
|
|
715
715
|
state = loaded;
|
|
716
716
|
}
|
|
@@ -722,6 +722,18 @@ function main() {
|
|
|
722
722
|
process.exit(1);
|
|
723
723
|
});
|
|
724
724
|
}
|
|
725
|
+
/**
|
|
726
|
+
* Removes state files this version does not write, and log directories: an
|
|
727
|
+
* earlier version kept its state in another JSON file and grouped its logs in
|
|
728
|
+
* directories. Local dev state is never migrated across versions; those
|
|
729
|
+
* instances are simply gone.
|
|
730
|
+
*/
|
|
731
|
+
async function discardStateFromEarlierVersions(stateDir, statePath, logsDir) {
|
|
732
|
+
const stateFiles = await fs.promises.readdir(stateDir).catch(() => []);
|
|
733
|
+
await Promise.all(stateFiles.map((name) => path.join(stateDir, name)).filter((file) => file.endsWith(".json") && file !== statePath).map((file) => fs.promises.rm(file, { force: true })));
|
|
734
|
+
const logEntries = await fs.promises.readdir(logsDir, { withFileTypes: true }).catch(() => []);
|
|
735
|
+
await Promise.all(logEntries.filter((entry) => entry.isDirectory()).map((entry) => fs.promises.rm(path.join(logsDir, entry.name), { recursive: true })));
|
|
736
|
+
}
|
|
725
737
|
main();
|
|
726
738
|
//#endregion
|
|
727
739
|
export {};
|