@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/postgres-main.mjs
CHANGED
|
@@ -1,42 +1,10 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { n as getPorts, r as portNumbers } from "./get-port-CwLPc-ca.mjs";
|
|
1
|
+
import { a as readOwnVersion, i as readJsonFile, n as StateFile, r as isPidAlive } from "./daemon-D9BV062Q-ZTmWho_N.mjs";
|
|
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 * as path from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
6
7
|
import * as http from "node:http";
|
|
7
|
-
//#region ../../1-prisma-cloud/0-lowering/dev-emulators/dist/instance-name-BF6J_weL.mjs
|
|
8
|
-
/**
|
|
9
|
-
* `pcdev-<app>-<database-id>` instance-name derivation (local-dev spec § 2
|
|
10
|
-
* `postgres-main.ts`): each half lowercased, every char outside `[a-z0-9]`
|
|
11
|
-
* replaced by `-`, runs collapsed, the combined name trimmed to 63 chars.
|
|
12
|
-
*
|
|
13
|
-
* A separate, side-effect-free module (not defined inline in
|
|
14
|
-
* `postgres-main.ts`) so it can be imported directly by tests without also
|
|
15
|
-
* running that file's own `main()` — `postgres-main.ts` is a daemon
|
|
16
|
-
* entrypoint script, always invoked as a subprocess, and calls `main()`
|
|
17
|
-
* unconditionally at module load.
|
|
18
|
-
*/
|
|
19
|
-
/**
|
|
20
|
-
* Deliberately linear, no ambiguous quantifiers: a per-character replace
|
|
21
|
-
* (no `+`, so no run-length backtracking surface), a bounded-quantifier
|
|
22
|
-
* collapse (`{2,}`, not alternation-with-quantifiers), and plain
|
|
23
|
-
* index-walking for the leading/trailing trim instead of a regex — a
|
|
24
|
-
* combined `+`-plus-alternation trim (`/^-+|-+$/g`) is exactly the shape
|
|
25
|
-
* CodeQL's polynomial-ReDoS check flags, whether or not this particular
|
|
26
|
-
* instance is provably safe.
|
|
27
|
-
*/
|
|
28
|
-
function slug(segment) {
|
|
29
|
-
const collapsed = segment.toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-{2,}/g, "-");
|
|
30
|
-
let start = 0;
|
|
31
|
-
let end = collapsed.length;
|
|
32
|
-
while (start < end && collapsed[start] === "-") start++;
|
|
33
|
-
while (end > start && collapsed[end - 1] === "-") end--;
|
|
34
|
-
return collapsed.slice(start, end);
|
|
35
|
-
}
|
|
36
|
-
function instanceNameFor(app, id) {
|
|
37
|
-
return `pcdev-${slug(app)}-${slug(id)}`.slice(0, 63);
|
|
38
|
-
}
|
|
39
|
-
//#endregion
|
|
40
8
|
//#region ../../1-prisma-cloud/0-lowering/dev-emulators/dist/postgres-main.mjs
|
|
41
9
|
function isStringKeyedRecord(value) {
|
|
42
10
|
return typeof value === "object" && value !== null;
|
|
@@ -108,9 +76,13 @@ async function registryClaimedPorts(internalState, ownInstanceName) {
|
|
|
108
76
|
/**
|
|
109
77
|
* The Postgres emulator daemon (local-dev spec § 2 `postgres-main.ts`): a
|
|
110
78
|
* small local counterpart of hosted Postgres, hosting `@prisma/dev`'s
|
|
111
|
-
* `startPrismaDevServer()` — one named, persistent server per
|
|
112
|
-
*
|
|
113
|
-
*
|
|
79
|
+
* `startPrismaDevServer()` — one named, persistent server per database
|
|
80
|
+
* instance, several servers in this one daemon process. Instances are keyed
|
|
81
|
+
* by ids this daemon generates; a server is named after its instance's id,
|
|
82
|
+
* and `@prisma/dev` keeps its data under that name in its own data
|
|
83
|
+
* directory. Labels are stored and returned verbatim, never interpreted.
|
|
84
|
+
* Stopping an instance shuts its server down and keeps its data and port.
|
|
85
|
+
* Loopback `node:http` JSON admin API; state under its `--state-dir`.
|
|
114
86
|
*
|
|
115
87
|
* `@prisma/dev` is imported dynamically from a CALLER-RESOLVED path (each
|
|
116
88
|
* admin request that needs it carries `prismaDevModulePath`): the local
|
|
@@ -123,7 +95,7 @@ async function registryClaimedPorts(internalState, ownInstanceName) {
|
|
|
123
95
|
*/
|
|
124
96
|
const MIN_DATABASE_PORT = 51300;
|
|
125
97
|
const MAX_DATABASE_PORT = 65535;
|
|
126
|
-
const
|
|
98
|
+
const STATE_MODE = 384;
|
|
127
99
|
/** Spec § 2 step 5's pattern, applied to a fresh database-port allocation. */
|
|
128
100
|
const MAX_FRESH_PORT_CANDIDATES = 5;
|
|
129
101
|
/**
|
|
@@ -136,24 +108,58 @@ const MAX_FRESH_PORT_CANDIDATES = 5;
|
|
|
136
108
|
const MAX_ALREADY_RUNNING_RETRIES = 2;
|
|
137
109
|
const ALREADY_RUNNING_POLLS = 24;
|
|
138
110
|
const ALREADY_RUNNING_POLL_MS = 500;
|
|
139
|
-
const NOT_INSTALLED_MESSAGE = "local dev needs @prisma/dev for its local Postgres emulator
|
|
111
|
+
const NOT_INSTALLED_MESSAGE = "local dev needs @prisma/dev for its local Postgres emulator, and the given module path did not load; reinstall your dependencies.";
|
|
140
112
|
function maskCredentials(text) {
|
|
141
113
|
return text.replace(/:\/\/([^:@/\s]+):[^@/\s]+@/g, "://$1:***@");
|
|
142
114
|
}
|
|
143
115
|
function firstLine(text) {
|
|
144
116
|
return (text.split("\n")[0] ?? text).trim();
|
|
145
117
|
}
|
|
118
|
+
const INSTANCE_ID_RE = /^db_[0-9a-f]{16}$/;
|
|
119
|
+
function newInstanceId() {
|
|
120
|
+
return `db_${randomBytes(8).toString("hex")}`;
|
|
121
|
+
}
|
|
122
|
+
function serverNameFor(id) {
|
|
123
|
+
return `pcdev-${id}`;
|
|
124
|
+
}
|
|
125
|
+
function isStringRecord(value) {
|
|
126
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((v) => typeof v === "string");
|
|
127
|
+
}
|
|
128
|
+
function isPort(value) {
|
|
129
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= MAX_DATABASE_PORT;
|
|
130
|
+
}
|
|
146
131
|
function isDatabaseRecord(value) {
|
|
147
|
-
return typeof value === "object" && value !== null && "id" in value && typeof value.id === "string" && "
|
|
132
|
+
return typeof value === "object" && value !== null && "id" in value && typeof value.id === "string" && "labels" in value && isStringRecord(value.labels) && "serverName" in value && typeof value.serverName === "string" && "port" in value && isPort(value.port) && "url" in value && typeof value.url === "string" && "prismaDevModulePath" in value && typeof value.prismaDevModulePath === "string";
|
|
133
|
+
}
|
|
134
|
+
function isInstancesState(value) {
|
|
135
|
+
return typeof value === "object" && value !== null && "instances" in value && typeof value.instances === "object" && value.instances !== null && Object.values(value.instances).every(isDatabaseRecord) && "discardedServers" in value && Array.isArray(value.discardedServers) && value.discardedServers.every((name) => typeof name === "string");
|
|
136
|
+
}
|
|
137
|
+
function isCreateBody(value) {
|
|
138
|
+
return typeof value === "object" && value !== null && "labels" in value && isStringRecord(value.labels) && (!("port" in value) || value.port === void 0 || isPort(value.port)) && "prismaDevModulePath" in value && typeof value.prismaDevModulePath === "string";
|
|
148
139
|
}
|
|
149
|
-
function
|
|
150
|
-
return
|
|
140
|
+
function isStartBody(value) {
|
|
141
|
+
return isIdsBody(value) && "prismaDevModulePath" in value && typeof value.prismaDevModulePath === "string";
|
|
151
142
|
}
|
|
152
|
-
function
|
|
153
|
-
return typeof value === "object" && value !== null &&
|
|
143
|
+
function isIdsBody(value) {
|
|
144
|
+
return typeof value === "object" && value !== null && "ids" in value && Array.isArray(value.ids) && value.ids.every((id) => typeof id === "string");
|
|
154
145
|
}
|
|
155
|
-
|
|
156
|
-
|
|
146
|
+
/** Every `@prisma/dev` server name an earlier version's state file lists; an unreadable file lists none. */
|
|
147
|
+
function serverNamesInEarlierState(raw) {
|
|
148
|
+
let parsed;
|
|
149
|
+
try {
|
|
150
|
+
parsed = JSON.parse(raw);
|
|
151
|
+
} catch {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
if (!isStringKeyedRecord(parsed)) return [];
|
|
155
|
+
return Object.values(parsed).flatMap((group) => {
|
|
156
|
+
const databases = isStringKeyedRecord(group) ? group["databases"] : void 0;
|
|
157
|
+
if (!isStringKeyedRecord(databases)) return [];
|
|
158
|
+
return Object.values(databases).flatMap((db) => {
|
|
159
|
+
const name = isStringKeyedRecord(db) ? db["instanceName"] : void 0;
|
|
160
|
+
return typeof name === "string" ? [name] : [];
|
|
161
|
+
});
|
|
162
|
+
});
|
|
157
163
|
}
|
|
158
164
|
function isPrismaDevModule(value) {
|
|
159
165
|
return typeof value === "object" && value !== null && "startPrismaDevServer" in value && typeof value.startPrismaDevServer === "function";
|
|
@@ -317,20 +323,21 @@ function readBody(req) {
|
|
|
317
323
|
function main() {
|
|
318
324
|
const { port, stateDir } = parseArgs(process.argv.slice(2));
|
|
319
325
|
const ownVersion = readOwnVersion();
|
|
320
|
-
const
|
|
321
|
-
const stateFile = new StateFile(
|
|
322
|
-
let state = {
|
|
326
|
+
const statePath = path.join(stateDir, "instances.json");
|
|
327
|
+
const stateFile = new StateFile(statePath, STATE_MODE);
|
|
328
|
+
let state = {
|
|
329
|
+
instances: {},
|
|
330
|
+
discardedServers: []
|
|
331
|
+
};
|
|
323
332
|
const runtimes = /* @__PURE__ */ new Map();
|
|
333
|
+
/** Instances whose server another live process holds: running, but with no handle in this one. */
|
|
334
|
+
const adopted = /* @__PURE__ */ new Set();
|
|
324
335
|
/**
|
|
325
336
|
* Server starts run ONE AT A TIME. `@prisma/dev`'s start is not
|
|
326
337
|
* concurrency-safe within a process: two simultaneous calls pick their
|
|
327
338
|
* ports without seeing each other and one fails with a port refusal
|
|
328
339
|
* (verified directly — two concurrent starts with distinct, pinned
|
|
329
340
|
* database ports fail; the same two started in sequence both succeed).
|
|
330
|
-
* The daemon issues concurrent starts whenever an app converges more than
|
|
331
|
-
* one database, which is what produced the port refusals, the retries
|
|
332
|
-
* whose half-started servers held their own name's lock, and every
|
|
333
|
-
* "already running" failure downstream of that.
|
|
334
341
|
*/
|
|
335
342
|
let startQueue = Promise.resolve();
|
|
336
343
|
function serializeStart(run) {
|
|
@@ -341,26 +348,38 @@ function main() {
|
|
|
341
348
|
function schedulePersist() {
|
|
342
349
|
stateFile.write(state);
|
|
343
350
|
}
|
|
344
|
-
function
|
|
345
|
-
|
|
346
|
-
if (!appRec) {
|
|
347
|
-
appRec = { databases: {} };
|
|
348
|
-
state[app] = appRec;
|
|
349
|
-
}
|
|
350
|
-
return appRec;
|
|
351
|
-
}
|
|
352
|
-
function usedDatabasePorts() {
|
|
353
|
-
const ports = /* @__PURE__ */ new Set();
|
|
354
|
-
for (const appRec of Object.values(state)) for (const db of Object.values(appRec.databases)) ports.add(db.databasePort);
|
|
355
|
-
return ports;
|
|
351
|
+
function heldPorts() {
|
|
352
|
+
return new Set(Object.values(state.instances).map((db) => db.port));
|
|
356
353
|
}
|
|
357
|
-
async function
|
|
354
|
+
async function smallestUnusedPort(exclude) {
|
|
358
355
|
return getPorts({
|
|
359
|
-
port: portNumbers(
|
|
360
|
-
exclude: [...
|
|
356
|
+
port: portNumbers(MIN_DATABASE_PORT, MAX_DATABASE_PORT),
|
|
357
|
+
exclude: [...heldPorts(), ...exclude]
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
/** The requested port when no instance or record holds it and it is bindable; otherwise the smallest such port in the database range. */
|
|
361
|
+
async function allocatePort(requested, exclude) {
|
|
362
|
+
const held = heldPorts();
|
|
363
|
+
if (requested !== void 0 && !held.has(requested) && !exclude.has(requested)) {
|
|
364
|
+
const given = await getPorts({
|
|
365
|
+
port: requested,
|
|
366
|
+
exclude: [...held, ...exclude]
|
|
367
|
+
});
|
|
368
|
+
if (given === requested) return given;
|
|
369
|
+
}
|
|
370
|
+
return smallestUnusedPort(exclude);
|
|
371
|
+
}
|
|
372
|
+
/** In-flight starts and stops per instance, so a stop, a start or a delete never overlaps another on the same server. */
|
|
373
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
374
|
+
function exclusive(id, run) {
|
|
375
|
+
const next = (inflight.get(id) ?? Promise.resolve()).then(run, run);
|
|
376
|
+
const settled = next.then(() => void 0, () => void 0);
|
|
377
|
+
inflight.set(id, settled);
|
|
378
|
+
settled.then(() => {
|
|
379
|
+
if (inflight.get(id) === settled) inflight.delete(id);
|
|
361
380
|
});
|
|
381
|
+
return next;
|
|
362
382
|
}
|
|
363
|
-
const inflightEnsures = /* @__PURE__ */ new Map();
|
|
364
383
|
/**
|
|
365
384
|
* Names whose `startPrismaDevServer` call failed in THIS process for any
|
|
366
385
|
* reason other than "already running". `@prisma/dev` acquires the name's
|
|
@@ -373,39 +392,26 @@ function main() {
|
|
|
373
392
|
* acquired by anyone else since (the leak never releases).
|
|
374
393
|
*/
|
|
375
394
|
const leakedStartNames = /* @__PURE__ */ new Set();
|
|
376
|
-
/**
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
384
|
-
inflightEnsures.set(instanceName, run);
|
|
385
|
-
return run;
|
|
386
|
-
}
|
|
387
|
-
async function ensureDatabaseSerialized(app, id, prismaDevModulePath, instanceName) {
|
|
388
|
-
const appRec = getOrCreateApp(app);
|
|
389
|
-
const existingRecord = appRec.databases[id];
|
|
390
|
-
const runningServer = runtimes.get(instanceName);
|
|
391
|
-
if (existingRecord && runningServer) return { url: existingRecord.url };
|
|
395
|
+
/**
|
|
396
|
+
* Starts the server for `serverName`. A fresh allocation picks its port
|
|
397
|
+
* (preferring `requestedPort`) inside the serialized section and may move
|
|
398
|
+
* to another port on a refusal; a start of an existing instance is pinned
|
|
399
|
+
* to its port, because dependents' environments carry its URL.
|
|
400
|
+
*/
|
|
401
|
+
async function startServer(opts) {
|
|
402
|
+
const { serverName, prismaDevModulePath } = opts;
|
|
392
403
|
const prismaDev = await importPrismaDev(prismaDevModulePath);
|
|
393
404
|
const internalState = await importPrismaDevInternalState(prismaDevModulePath);
|
|
394
|
-
|
|
405
|
+
await discardEarlierServers(internalState);
|
|
406
|
+
const recorded = await serverStatusOf(internalState, serverName);
|
|
395
407
|
if (recorded.live && recorded.pid !== void 0 && isPidAlive(recorded.pid)) {
|
|
396
|
-
if (recorded.url !== void 0) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
schedulePersist();
|
|
404
|
-
return { url: recorded.url };
|
|
405
|
-
}
|
|
406
|
-
} else if (recorded.live) await internalState.killServer(instanceName).catch(() => void 0);
|
|
407
|
-
const isFreshAllocation = existingRecord === void 0;
|
|
408
|
-
let dbPort = existingRecord?.databasePort ?? 0;
|
|
408
|
+
if (recorded.url !== void 0) return {
|
|
409
|
+
url: recorded.url,
|
|
410
|
+
port: recorded.databasePort ?? portOfUrl(recorded.url) ?? opts.pinnedPort ?? 0
|
|
411
|
+
};
|
|
412
|
+
} else if (recorded.live) await internalState.killServer(serverName).catch(() => void 0);
|
|
413
|
+
const isFreshAllocation = opts.pinnedPort === void 0;
|
|
414
|
+
let dbPort = opts.pinnedPort ?? 0;
|
|
409
415
|
const conflicted = /* @__PURE__ */ new Set();
|
|
410
416
|
let alreadyRunningRetries = 0;
|
|
411
417
|
let blindRetries = 0;
|
|
@@ -413,24 +419,24 @@ function main() {
|
|
|
413
419
|
const auxPorts = [];
|
|
414
420
|
try {
|
|
415
421
|
const server = await serializeStart(async () => {
|
|
416
|
-
const registryPorts = await registryClaimedPorts(internalState,
|
|
417
|
-
if (isFreshAllocation) dbPort = await
|
|
422
|
+
const registryPorts = await registryClaimedPorts(internalState, serverName);
|
|
423
|
+
if (isFreshAllocation) dbPort = await allocatePort(attempt === 1 ? opts.requestedPort : void 0, /* @__PURE__ */ new Set([...registryPorts, ...conflicted]));
|
|
418
424
|
const auxExclude = /* @__PURE__ */ new Set([
|
|
419
425
|
...registryPorts,
|
|
420
426
|
...conflicted,
|
|
421
427
|
dbPort
|
|
422
428
|
]);
|
|
423
429
|
const pickAux = async () => {
|
|
424
|
-
const
|
|
425
|
-
auxExclude.add(
|
|
426
|
-
auxPorts.push(
|
|
427
|
-
return
|
|
430
|
+
const picked = await smallestUnusedPort(auxExclude);
|
|
431
|
+
auxExclude.add(picked);
|
|
432
|
+
auxPorts.push(picked);
|
|
433
|
+
return picked;
|
|
428
434
|
};
|
|
429
435
|
const httpPort = await pickAux();
|
|
430
436
|
const shadowPort = await pickAux();
|
|
431
437
|
const streamsPort = await pickAux();
|
|
432
438
|
return prismaDev.startPrismaDevServer({
|
|
433
|
-
name:
|
|
439
|
+
name: serverName,
|
|
434
440
|
databasePort: dbPort,
|
|
435
441
|
port: httpPort,
|
|
436
442
|
shadowDatabasePort: shadowPort,
|
|
@@ -438,47 +444,36 @@ function main() {
|
|
|
438
444
|
persistenceMode: "stateful"
|
|
439
445
|
});
|
|
440
446
|
});
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
instanceName,
|
|
447
|
-
databasePort: dbPort,
|
|
448
|
-
url
|
|
447
|
+
leakedStartNames.delete(serverName);
|
|
448
|
+
return {
|
|
449
|
+
url: server.database.connectionString,
|
|
450
|
+
port: dbPort,
|
|
451
|
+
server
|
|
449
452
|
};
|
|
450
|
-
schedulePersist();
|
|
451
|
-
return { url };
|
|
452
453
|
} catch (err) {
|
|
453
454
|
if (isNameAlreadyTaken(err) && alreadyRunningRetries < MAX_ALREADY_RUNNING_RETRIES) {
|
|
454
455
|
alreadyRunningRetries += 1;
|
|
455
|
-
if (leakedStartNames.has(
|
|
456
|
-
if (!(await serverStatusOf(internalState,
|
|
457
|
-
await internalState.deleteServer(
|
|
456
|
+
if (leakedStartNames.has(serverName)) {
|
|
457
|
+
if (!(await serverStatusOf(internalState, serverName)).recordExists) {
|
|
458
|
+
await internalState.deleteServer(serverName).catch(() => void 0);
|
|
458
459
|
continue;
|
|
459
460
|
}
|
|
460
461
|
}
|
|
461
462
|
for (let poll = 1; poll <= ALREADY_RUNNING_POLLS; poll += 1) {
|
|
462
|
-
const now = await serverStatusOf(internalState,
|
|
463
|
-
if (now.live && now.pid !== void 0 && isPidAlive(now.pid) && now.url !== void 0) {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
databasePort: now.databasePort ?? portOfUrl(now.url) ?? dbPort,
|
|
468
|
-
url: now.url
|
|
469
|
-
};
|
|
470
|
-
schedulePersist();
|
|
471
|
-
return { url: now.url };
|
|
472
|
-
}
|
|
463
|
+
const now = await serverStatusOf(internalState, serverName);
|
|
464
|
+
if (now.live && now.pid !== void 0 && isPidAlive(now.pid) && now.url !== void 0) return {
|
|
465
|
+
url: now.url,
|
|
466
|
+
port: now.databasePort ?? portOfUrl(now.url) ?? dbPort
|
|
467
|
+
};
|
|
473
468
|
if (now.live && (now.pid === void 0 || !isPidAlive(now.pid))) {
|
|
474
|
-
await internalState.killServer(
|
|
469
|
+
await internalState.killServer(serverName).catch(() => void 0);
|
|
475
470
|
break;
|
|
476
471
|
}
|
|
477
472
|
await new Promise((resolve) => setTimeout(resolve, ALREADY_RUNNING_POLL_MS));
|
|
478
473
|
}
|
|
479
474
|
continue;
|
|
480
475
|
}
|
|
481
|
-
if (!isNameAlreadyTaken(err)) leakedStartNames.add(
|
|
476
|
+
if (!isNameAlreadyTaken(err)) leakedStartNames.add(serverName);
|
|
482
477
|
const conflictPort = portConflictOf(err, [dbPort, ...auxPorts]);
|
|
483
478
|
if (!(conflictPort !== void 0 && attempt < MAX_FRESH_PORT_CANDIDATES && (conflictPort !== dbPort || isFreshAllocation))) {
|
|
484
479
|
if (conflictPort === void 0 && !isNameAlreadyTaken(err) && blindRetries < 2) {
|
|
@@ -486,39 +481,131 @@ function main() {
|
|
|
486
481
|
continue;
|
|
487
482
|
}
|
|
488
483
|
const reason = err instanceof Error ? err.message : String(err);
|
|
489
|
-
throw new Error(`postgres emulator failed to start database "${
|
|
484
|
+
throw new Error(`postgres emulator failed to start database "${serverName}" on port ${String(dbPort)}: ${maskCredentials(firstLine(reason))}`);
|
|
490
485
|
}
|
|
491
486
|
conflicted.add(conflictPort);
|
|
492
487
|
}
|
|
493
488
|
}
|
|
494
489
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
await internalState.deleteServer(db.instanceName);
|
|
490
|
+
/**
|
|
491
|
+
* Deletes the data of servers an earlier version of this emulator created,
|
|
492
|
+
* once a request brings a `@prisma/dev` module to do it with. Best effort:
|
|
493
|
+
* a server another live process still serves, or one whose delete fails,
|
|
494
|
+
* is tried again on the next request.
|
|
495
|
+
*/
|
|
496
|
+
let discarding;
|
|
497
|
+
function discardEarlierServers(internalState) {
|
|
498
|
+
if (state.discardedServers.length === 0) return Promise.resolve();
|
|
499
|
+
discarding ??= deleteEarlierServers(internalState).finally(() => {
|
|
500
|
+
discarding = void 0;
|
|
501
|
+
});
|
|
502
|
+
return discarding;
|
|
503
|
+
}
|
|
504
|
+
async function deleteEarlierServers(internalState) {
|
|
505
|
+
const remaining = [];
|
|
506
|
+
for (const name of state.discardedServers) try {
|
|
507
|
+
const owner = await serverStatusOf(internalState, name);
|
|
508
|
+
if (owner.live && owner.pid !== void 0 && isPidAlive(owner.pid)) {
|
|
509
|
+
remaining.push(name);
|
|
510
|
+
continue;
|
|
517
511
|
}
|
|
512
|
+
if (owner.live) await internalState.killServer(name).catch(() => void 0);
|
|
513
|
+
await internalState.deleteServer(name);
|
|
514
|
+
} catch (err) {
|
|
515
|
+
console.error(`postgres-main: could not delete the data of "${name}", left by an earlier version: ${err instanceof Error ? err.message : String(err)}`);
|
|
516
|
+
remaining.push(name);
|
|
518
517
|
}
|
|
519
|
-
|
|
518
|
+
state.discardedServers = remaining;
|
|
520
519
|
schedulePersist();
|
|
521
520
|
}
|
|
521
|
+
async function createInstance(body) {
|
|
522
|
+
const id = newInstanceId();
|
|
523
|
+
const serverName = serverNameFor(id);
|
|
524
|
+
let started;
|
|
525
|
+
try {
|
|
526
|
+
started = await startServer({
|
|
527
|
+
serverName,
|
|
528
|
+
prismaDevModulePath: body.prismaDevModulePath,
|
|
529
|
+
...body.port !== void 0 ? { requestedPort: body.port } : {}
|
|
530
|
+
});
|
|
531
|
+
} catch (err) {
|
|
532
|
+
await discardFailedServer(serverName, body.prismaDevModulePath);
|
|
533
|
+
throw err;
|
|
534
|
+
}
|
|
535
|
+
const record = {
|
|
536
|
+
id,
|
|
537
|
+
labels: { ...body.labels },
|
|
538
|
+
serverName,
|
|
539
|
+
port: started.port,
|
|
540
|
+
url: started.url,
|
|
541
|
+
prismaDevModulePath: body.prismaDevModulePath
|
|
542
|
+
};
|
|
543
|
+
if (started.server) runtimes.set(id, started.server);
|
|
544
|
+
else adopted.add(id);
|
|
545
|
+
state.instances[id] = record;
|
|
546
|
+
await stateFile.write(state);
|
|
547
|
+
return record;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* A failed create records no instance, so nothing will use its server name
|
|
551
|
+
* again. A start that failed after `@prisma/dev` took the name's lock leaves
|
|
552
|
+
* the name's directory behind with no record; remove it.
|
|
553
|
+
*/
|
|
554
|
+
async function discardFailedServer(serverName, prismaDevModulePath) {
|
|
555
|
+
if (!leakedStartNames.has(serverName)) return;
|
|
556
|
+
try {
|
|
557
|
+
const internalState = await importPrismaDevInternalState(prismaDevModulePath);
|
|
558
|
+
if (!(await serverStatusOf(internalState, serverName)).recordExists) await internalState.deleteServer(serverName);
|
|
559
|
+
leakedStartNames.delete(serverName);
|
|
560
|
+
} catch {}
|
|
561
|
+
}
|
|
562
|
+
function isRunning(id) {
|
|
563
|
+
return runtimes.has(id) || adopted.has(id);
|
|
564
|
+
}
|
|
565
|
+
function startInstance(db, prismaDevModulePath) {
|
|
566
|
+
return exclusive(db.id, async () => {
|
|
567
|
+
db.prismaDevModulePath = prismaDevModulePath;
|
|
568
|
+
if (isRunning(db.id)) return;
|
|
569
|
+
const started = await startServer({
|
|
570
|
+
serverName: db.serverName,
|
|
571
|
+
prismaDevModulePath,
|
|
572
|
+
pinnedPort: db.port
|
|
573
|
+
});
|
|
574
|
+
db.url = started.url;
|
|
575
|
+
if (started.server) runtimes.set(db.id, started.server);
|
|
576
|
+
else adopted.add(db.id);
|
|
577
|
+
schedulePersist();
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
/** Shuts the server down and keeps its data. */
|
|
581
|
+
async function closeServer(db) {
|
|
582
|
+
const server = runtimes.get(db.id);
|
|
583
|
+
if (server) {
|
|
584
|
+
await server.close().catch(() => void 0);
|
|
585
|
+
runtimes.delete(db.id);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (!adopted.has(db.id)) return;
|
|
589
|
+
const internalState = await importPrismaDevInternalState(db.prismaDevModulePath);
|
|
590
|
+
if ((await serverStatusOf(internalState, db.serverName)).pid !== process.pid) await internalState.killServer(db.serverName).catch(() => void 0);
|
|
591
|
+
adopted.delete(db.id);
|
|
592
|
+
}
|
|
593
|
+
function stopInstance(db) {
|
|
594
|
+
return exclusive(db.id, () => closeServer(db));
|
|
595
|
+
}
|
|
596
|
+
/** `prismaDevModulePath` is the caller's: the one the database was last started with may since have been removed. */
|
|
597
|
+
function deleteInstance(db, prismaDevModulePath) {
|
|
598
|
+
return exclusive(db.id, async () => {
|
|
599
|
+
if (prismaDevModulePath !== void 0) db.prismaDevModulePath = prismaDevModulePath;
|
|
600
|
+
await closeServer(db);
|
|
601
|
+
const internalState = await importPrismaDevInternalState(db.prismaDevModulePath);
|
|
602
|
+
if (await settledAfterClose(internalState, db.serverName)) await internalState.deleteServer(db.serverName);
|
|
603
|
+
else console.error(`postgres-main: server "${db.serverName}" still reports running after close — leaving its persisted data in place instead of risking a self-kill.`);
|
|
604
|
+
delete state.instances[db.id];
|
|
605
|
+
await stateFile.write(state);
|
|
606
|
+
clearLockedPorts();
|
|
607
|
+
});
|
|
608
|
+
}
|
|
522
609
|
function json(res, status, body) {
|
|
523
610
|
res.writeHead(status, { "content-type": "application/json" });
|
|
524
611
|
res.end(JSON.stringify(body));
|
|
@@ -527,55 +614,79 @@ function main() {
|
|
|
527
614
|
res.writeHead(status, { "content-type": "text/plain; charset=utf-8" });
|
|
528
615
|
res.end(body);
|
|
529
616
|
}
|
|
530
|
-
function
|
|
531
|
-
|
|
617
|
+
function noContent(res) {
|
|
618
|
+
res.writeHead(204);
|
|
619
|
+
res.end();
|
|
532
620
|
}
|
|
533
|
-
function
|
|
621
|
+
async function readJsonBody(req) {
|
|
622
|
+
const raw = await readBody(req);
|
|
623
|
+
try {
|
|
624
|
+
return JSON.parse(raw.toString("utf8"));
|
|
625
|
+
} catch {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
function instanceView(db) {
|
|
534
630
|
return {
|
|
535
631
|
id: db.id,
|
|
632
|
+
labels: db.labels,
|
|
633
|
+
port: db.port,
|
|
536
634
|
url: db.url,
|
|
537
|
-
|
|
538
|
-
databasePort: db.databasePort
|
|
635
|
+
status: isRunning(db.id) ? "running" : "stopped"
|
|
539
636
|
};
|
|
540
637
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
function lastKnownPrismaDevModulePath(app) {
|
|
544
|
-
return recentPrismaDevModulePath.get(app);
|
|
638
|
+
function unknownInstances(res, ids) {
|
|
639
|
+
text(res, 404, `no such instance: ${ids.map((id) => `"${id}"`).join(", ")}`);
|
|
545
640
|
}
|
|
546
641
|
async function handleRequest(req, res) {
|
|
547
642
|
const url = new URL(req.url ?? "/", `http://127.0.0.1:${String(port)}`);
|
|
548
643
|
const method = req.method ?? "GET";
|
|
549
644
|
const segments = url.pathname.split("/").filter((s) => s.length > 0).map((s) => decodeURIComponent(s));
|
|
550
645
|
if (method === "GET" && segments.length === 1 && segments[0] === "health") return json(res, 200, { version: ownVersion });
|
|
551
|
-
if (segments[0]
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
return text(res, 400, "malformed JSON body");
|
|
563
|
-
}
|
|
564
|
-
if (!isDatabaseBody(parsed)) return text(res, 400, "malformed database body: expected { \"prismaDevModulePath\": string }");
|
|
565
|
-
recentPrismaDevModulePath.set(app, parsed.prismaDevModulePath);
|
|
566
|
-
return json(res, 200, await ensureDatabase(app, id, parsed.prismaDevModulePath));
|
|
646
|
+
if (segments[0] !== "instances") {
|
|
647
|
+
res.writeHead(404);
|
|
648
|
+
res.end();
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
if (segments.length === 1) {
|
|
652
|
+
if (method === "GET") return json(res, 200, Object.values(state.instances).map(instanceView));
|
|
653
|
+
if (method === "POST") {
|
|
654
|
+
const body = await readJsonBody(req);
|
|
655
|
+
if (!isCreateBody(body)) return text(res, 400, "malformed body: expected { \"labels\": Record<string, string>, \"port\"?: number, \"prismaDevModulePath\": string }");
|
|
656
|
+
return json(res, 201, instanceView(await createInstance(body)));
|
|
567
657
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
658
|
+
}
|
|
659
|
+
if (method === "POST" && segments.length === 2 && segments[1] === "start") {
|
|
660
|
+
const body = await readJsonBody(req);
|
|
661
|
+
if (!isStartBody(body)) return text(res, 400, "malformed body: expected { \"ids\": string[], \"prismaDevModulePath\": string }");
|
|
662
|
+
const unknown = body.ids.filter((id) => state.instances[id] === void 0);
|
|
663
|
+
if (unknown.length > 0) return unknownInstances(res, unknown);
|
|
664
|
+
for (const id of body.ids) {
|
|
665
|
+
const db = state.instances[id];
|
|
666
|
+
if (db) await startInstance(db, body.prismaDevModulePath);
|
|
571
667
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
668
|
+
return noContent(res);
|
|
669
|
+
}
|
|
670
|
+
if (method === "POST" && segments.length === 2 && segments[1] === "stop") {
|
|
671
|
+
const body = await readJsonBody(req);
|
|
672
|
+
if (!isIdsBody(body)) return text(res, 400, "malformed body: expected { \"ids\": string[] }");
|
|
673
|
+
for (const id of body.ids) {
|
|
674
|
+
const db = state.instances[id];
|
|
675
|
+
if (db) await stopInstance(db);
|
|
577
676
|
}
|
|
677
|
+
return noContent(res);
|
|
678
|
+
}
|
|
679
|
+
const id = segments[1];
|
|
680
|
+
if (id !== void 0 && !INSTANCE_ID_RE.test(id)) return text(res, 400, `invalid instance id "${id}"`);
|
|
681
|
+
const db = id === void 0 ? void 0 : state.instances[id];
|
|
682
|
+
if (method === "DELETE" && segments.length === 2) {
|
|
683
|
+
const body = await readJsonBody(req);
|
|
684
|
+
const modulePath = isStringKeyedRecord(body) ? body["prismaDevModulePath"] : void 0;
|
|
685
|
+
if (db) await deleteInstance(db, typeof modulePath === "string" ? modulePath : void 0);
|
|
686
|
+
return noContent(res);
|
|
578
687
|
}
|
|
688
|
+
if (!db) return unknownInstances(res, id === void 0 ? [] : [id]);
|
|
689
|
+
if (method === "GET" && segments.length === 2) return json(res, 200, instanceView(db));
|
|
579
690
|
res.writeHead(404);
|
|
580
691
|
res.end();
|
|
581
692
|
}
|
|
@@ -586,7 +697,7 @@ function main() {
|
|
|
586
697
|
});
|
|
587
698
|
});
|
|
588
699
|
async function shutdown() {
|
|
589
|
-
for (const
|
|
700
|
+
for (const live of runtimes.values()) await live.close().catch(() => void 0);
|
|
590
701
|
await stateFile.flush();
|
|
591
702
|
server.close();
|
|
592
703
|
process.exit(0);
|
|
@@ -599,8 +710,9 @@ function main() {
|
|
|
599
710
|
process.on("uncaughtException", (err) => {
|
|
600
711
|
console.error("postgres-main: uncaught exception from background work:", err instanceof Error ? maskCredentials(err.stack ?? err.message) : err);
|
|
601
712
|
});
|
|
602
|
-
|
|
603
|
-
|
|
713
|
+
loadState(stateDir, statePath).then((loaded) => {
|
|
714
|
+
state = loaded;
|
|
715
|
+
schedulePersist();
|
|
604
716
|
server.listen(port, "127.0.0.1", () => {
|
|
605
717
|
console.log(`[dev-emulators] postgres-main listening on 127.0.0.1:${String(port)}`);
|
|
606
718
|
});
|
|
@@ -609,6 +721,26 @@ function main() {
|
|
|
609
721
|
process.exit(1);
|
|
610
722
|
});
|
|
611
723
|
}
|
|
724
|
+
/**
|
|
725
|
+
* This version's state, plus the servers named in any other JSON state file
|
|
726
|
+
* in the state directory, which an earlier version wrote; those files are
|
|
727
|
+
* removed. Local dev state is never migrated; the servers' data is deleted
|
|
728
|
+
* on the first request that can (see `discardEarlierServers`).
|
|
729
|
+
*/
|
|
730
|
+
async function loadState(stateDir, statePath) {
|
|
731
|
+
const loaded = await readJsonFile(statePath, isInstancesState) ?? {
|
|
732
|
+
instances: {},
|
|
733
|
+
discardedServers: []
|
|
734
|
+
};
|
|
735
|
+
const files = await fs.promises.readdir(stateDir).catch(() => []);
|
|
736
|
+
for (const file of files.map((name) => path.join(stateDir, name))) {
|
|
737
|
+
if (!file.endsWith(".json") || file === statePath) continue;
|
|
738
|
+
const names = serverNamesInEarlierState(await fs.promises.readFile(file, "utf8").catch(() => ""));
|
|
739
|
+
loaded.discardedServers = [.../* @__PURE__ */ new Set([...loaded.discardedServers, ...names])];
|
|
740
|
+
await fs.promises.rm(file, { force: true });
|
|
741
|
+
}
|
|
742
|
+
return loaded;
|
|
743
|
+
}
|
|
612
744
|
main();
|
|
613
745
|
//#endregion
|
|
614
746
|
export {};
|