@prisma/composer-prisma-cloud 0.30.0-dev.3 → 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 +101 -82
- package/dist/buckets-main.mjs.map +1 -1
- package/dist/control.mjs +3 -3
- package/dist/control.mjs.map +1 -1
- package/dist/local-target.mjs +614 -412
- package/dist/local-target.mjs.map +1 -1
- package/dist/postgres-main.mjs +324 -193
- package/dist/postgres-main.mjs.map +1 -1
- package/dist/{s3-credentials-resource-BZjtuopJ-azxxffwQ.mjs → s3-credentials-resource-D4jcpAZp-6qCQY8BD.mjs} +50 -10
- package/dist/{s3-credentials-resource-BZjtuopJ-azxxffwQ.mjs.map → s3-credentials-resource-D4jcpAZp-6qCQY8BD.mjs.map} +1 -1
- package/package.json +18 -18
- package/dist/segments-CA7TOJ1U-CoICbb95.mjs +0 -14
- package/dist/segments-CA7TOJ1U-CoICbb95.mjs.map +0 -1
package/dist/postgres-main.mjs
CHANGED
|
@@ -1,43 +1,10 @@
|
|
|
1
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 } from "./get-port-CwLPc-ca.mjs";
|
|
3
|
-
import { t as isValidSegment } from "./segments-CA7TOJ1U-CoICbb95.mjs";
|
|
2
|
+
import { n as getPorts, r as portNumbers, t as clearLockedPorts } from "./get-port-CwLPc-ca.mjs";
|
|
4
3
|
import * as fs from "node:fs";
|
|
5
4
|
import * as path from "node:path";
|
|
6
5
|
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
7
7
|
import * as http from "node:http";
|
|
8
|
-
//#region ../../1-prisma-cloud/0-lowering/dev-emulators/dist/instance-name-BF6J_weL.mjs
|
|
9
|
-
/**
|
|
10
|
-
* `pcdev-<app>-<database-id>` instance-name derivation (local-dev spec § 2
|
|
11
|
-
* `postgres-main.ts`): each half lowercased, every char outside `[a-z0-9]`
|
|
12
|
-
* replaced by `-`, runs collapsed, the combined name trimmed to 63 chars.
|
|
13
|
-
*
|
|
14
|
-
* A separate, side-effect-free module (not defined inline in
|
|
15
|
-
* `postgres-main.ts`) so it can be imported directly by tests without also
|
|
16
|
-
* running that file's own `main()` — `postgres-main.ts` is a daemon
|
|
17
|
-
* entrypoint script, always invoked as a subprocess, and calls `main()`
|
|
18
|
-
* unconditionally at module load.
|
|
19
|
-
*/
|
|
20
|
-
/**
|
|
21
|
-
* Deliberately linear, no ambiguous quantifiers: a per-character replace
|
|
22
|
-
* (no `+`, so no run-length backtracking surface), a bounded-quantifier
|
|
23
|
-
* collapse (`{2,}`, not alternation-with-quantifiers), and plain
|
|
24
|
-
* index-walking for the leading/trailing trim instead of a regex — a
|
|
25
|
-
* combined `+`-plus-alternation trim (`/^-+|-+$/g`) is exactly the shape
|
|
26
|
-
* CodeQL's polynomial-ReDoS check flags, whether or not this particular
|
|
27
|
-
* instance is provably safe.
|
|
28
|
-
*/
|
|
29
|
-
function slug(segment) {
|
|
30
|
-
const collapsed = segment.toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-{2,}/g, "-");
|
|
31
|
-
let start = 0;
|
|
32
|
-
let end = collapsed.length;
|
|
33
|
-
while (start < end && collapsed[start] === "-") start++;
|
|
34
|
-
while (end > start && collapsed[end - 1] === "-") end--;
|
|
35
|
-
return collapsed.slice(start, end);
|
|
36
|
-
}
|
|
37
|
-
function instanceNameFor(app, id) {
|
|
38
|
-
return `pcdev-${slug(app)}-${slug(id)}`.slice(0, 63);
|
|
39
|
-
}
|
|
40
|
-
//#endregion
|
|
41
8
|
//#region ../../1-prisma-cloud/0-lowering/dev-emulators/dist/postgres-main.mjs
|
|
42
9
|
function isStringKeyedRecord(value) {
|
|
43
10
|
return typeof value === "object" && value !== null;
|
|
@@ -109,9 +76,13 @@ async function registryClaimedPorts(internalState, ownInstanceName) {
|
|
|
109
76
|
/**
|
|
110
77
|
* The Postgres emulator daemon (local-dev spec § 2 `postgres-main.ts`): a
|
|
111
78
|
* small local counterpart of hosted Postgres, hosting `@prisma/dev`'s
|
|
112
|
-
* `startPrismaDevServer()` — one named, persistent server per
|
|
113
|
-
*
|
|
114
|
-
*
|
|
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`.
|
|
115
86
|
*
|
|
116
87
|
* `@prisma/dev` is imported dynamically from a CALLER-RESOLVED path (each
|
|
117
88
|
* admin request that needs it carries `prismaDevModulePath`): the local
|
|
@@ -124,7 +95,7 @@ async function registryClaimedPorts(internalState, ownInstanceName) {
|
|
|
124
95
|
*/
|
|
125
96
|
const MIN_DATABASE_PORT = 51300;
|
|
126
97
|
const MAX_DATABASE_PORT = 65535;
|
|
127
|
-
const
|
|
98
|
+
const STATE_MODE = 384;
|
|
128
99
|
/** Spec § 2 step 5's pattern, applied to a fresh database-port allocation. */
|
|
129
100
|
const MAX_FRESH_PORT_CANDIDATES = 5;
|
|
130
101
|
/**
|
|
@@ -137,24 +108,58 @@ const MAX_FRESH_PORT_CANDIDATES = 5;
|
|
|
137
108
|
const MAX_ALREADY_RUNNING_RETRIES = 2;
|
|
138
109
|
const ALREADY_RUNNING_POLLS = 24;
|
|
139
110
|
const ALREADY_RUNNING_POLL_MS = 500;
|
|
140
|
-
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.";
|
|
141
112
|
function maskCredentials(text) {
|
|
142
113
|
return text.replace(/:\/\/([^:@/\s]+):[^@/\s]+@/g, "://$1:***@");
|
|
143
114
|
}
|
|
144
115
|
function firstLine(text) {
|
|
145
116
|
return (text.split("\n")[0] ?? text).trim();
|
|
146
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
|
+
}
|
|
147
131
|
function isDatabaseRecord(value) {
|
|
148
|
-
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";
|
|
149
139
|
}
|
|
150
|
-
function
|
|
151
|
-
return
|
|
140
|
+
function isStartBody(value) {
|
|
141
|
+
return isIdsBody(value) && "prismaDevModulePath" in value && typeof value.prismaDevModulePath === "string";
|
|
152
142
|
}
|
|
153
|
-
function
|
|
154
|
-
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");
|
|
155
145
|
}
|
|
156
|
-
|
|
157
|
-
|
|
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
|
+
});
|
|
158
163
|
}
|
|
159
164
|
function isPrismaDevModule(value) {
|
|
160
165
|
return typeof value === "object" && value !== null && "startPrismaDevServer" in value && typeof value.startPrismaDevServer === "function";
|
|
@@ -318,20 +323,21 @@ function readBody(req) {
|
|
|
318
323
|
function main() {
|
|
319
324
|
const { port, stateDir } = parseArgs(process.argv.slice(2));
|
|
320
325
|
const ownVersion = readOwnVersion();
|
|
321
|
-
const
|
|
322
|
-
const stateFile = new StateFile(
|
|
323
|
-
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
|
+
};
|
|
324
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();
|
|
325
335
|
/**
|
|
326
336
|
* Server starts run ONE AT A TIME. `@prisma/dev`'s start is not
|
|
327
337
|
* concurrency-safe within a process: two simultaneous calls pick their
|
|
328
338
|
* ports without seeing each other and one fails with a port refusal
|
|
329
339
|
* (verified directly — two concurrent starts with distinct, pinned
|
|
330
340
|
* database ports fail; the same two started in sequence both succeed).
|
|
331
|
-
* The daemon issues concurrent starts whenever an app converges more than
|
|
332
|
-
* one database, which is what produced the port refusals, the retries
|
|
333
|
-
* whose half-started servers held their own name's lock, and every
|
|
334
|
-
* "already running" failure downstream of that.
|
|
335
341
|
*/
|
|
336
342
|
let startQueue = Promise.resolve();
|
|
337
343
|
function serializeStart(run) {
|
|
@@ -342,26 +348,38 @@ function main() {
|
|
|
342
348
|
function schedulePersist() {
|
|
343
349
|
stateFile.write(state);
|
|
344
350
|
}
|
|
345
|
-
function
|
|
346
|
-
|
|
347
|
-
if (!appRec) {
|
|
348
|
-
appRec = { databases: {} };
|
|
349
|
-
state[app] = appRec;
|
|
350
|
-
}
|
|
351
|
-
return appRec;
|
|
352
|
-
}
|
|
353
|
-
function usedDatabasePorts() {
|
|
354
|
-
const ports = /* @__PURE__ */ new Set();
|
|
355
|
-
for (const appRec of Object.values(state)) for (const db of Object.values(appRec.databases)) ports.add(db.databasePort);
|
|
356
|
-
return ports;
|
|
351
|
+
function heldPorts() {
|
|
352
|
+
return new Set(Object.values(state.instances).map((db) => db.port));
|
|
357
353
|
}
|
|
358
|
-
async function
|
|
354
|
+
async function smallestUnusedPort(exclude) {
|
|
359
355
|
return getPorts({
|
|
360
|
-
port: portNumbers(
|
|
361
|
-
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);
|
|
362
380
|
});
|
|
381
|
+
return next;
|
|
363
382
|
}
|
|
364
|
-
const inflightEnsures = /* @__PURE__ */ new Map();
|
|
365
383
|
/**
|
|
366
384
|
* Names whose `startPrismaDevServer` call failed in THIS process for any
|
|
367
385
|
* reason other than "already running". `@prisma/dev` acquires the name's
|
|
@@ -374,39 +392,26 @@ function main() {
|
|
|
374
392
|
* acquired by anyone else since (the leak never releases).
|
|
375
393
|
*/
|
|
376
394
|
const leakedStartNames = /* @__PURE__ */ new Set();
|
|
377
|
-
/**
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
inflightEnsures.set(instanceName, run);
|
|
386
|
-
return run;
|
|
387
|
-
}
|
|
388
|
-
async function ensureDatabaseSerialized(app, id, prismaDevModulePath, instanceName) {
|
|
389
|
-
const appRec = getOrCreateApp(app);
|
|
390
|
-
const existingRecord = appRec.databases[id];
|
|
391
|
-
const runningServer = runtimes.get(instanceName);
|
|
392
|
-
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;
|
|
393
403
|
const prismaDev = await importPrismaDev(prismaDevModulePath);
|
|
394
404
|
const internalState = await importPrismaDevInternalState(prismaDevModulePath);
|
|
395
|
-
|
|
405
|
+
await discardEarlierServers(internalState);
|
|
406
|
+
const recorded = await serverStatusOf(internalState, serverName);
|
|
396
407
|
if (recorded.live && recorded.pid !== void 0 && isPidAlive(recorded.pid)) {
|
|
397
|
-
if (recorded.url !== void 0) {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
schedulePersist();
|
|
405
|
-
return { url: recorded.url };
|
|
406
|
-
}
|
|
407
|
-
} else if (recorded.live) await internalState.killServer(instanceName).catch(() => void 0);
|
|
408
|
-
const isFreshAllocation = existingRecord === void 0;
|
|
409
|
-
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;
|
|
410
415
|
const conflicted = /* @__PURE__ */ new Set();
|
|
411
416
|
let alreadyRunningRetries = 0;
|
|
412
417
|
let blindRetries = 0;
|
|
@@ -414,24 +419,24 @@ function main() {
|
|
|
414
419
|
const auxPorts = [];
|
|
415
420
|
try {
|
|
416
421
|
const server = await serializeStart(async () => {
|
|
417
|
-
const registryPorts = await registryClaimedPorts(internalState,
|
|
418
|
-
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]));
|
|
419
424
|
const auxExclude = /* @__PURE__ */ new Set([
|
|
420
425
|
...registryPorts,
|
|
421
426
|
...conflicted,
|
|
422
427
|
dbPort
|
|
423
428
|
]);
|
|
424
429
|
const pickAux = async () => {
|
|
425
|
-
const
|
|
426
|
-
auxExclude.add(
|
|
427
|
-
auxPorts.push(
|
|
428
|
-
return
|
|
430
|
+
const picked = await smallestUnusedPort(auxExclude);
|
|
431
|
+
auxExclude.add(picked);
|
|
432
|
+
auxPorts.push(picked);
|
|
433
|
+
return picked;
|
|
429
434
|
};
|
|
430
435
|
const httpPort = await pickAux();
|
|
431
436
|
const shadowPort = await pickAux();
|
|
432
437
|
const streamsPort = await pickAux();
|
|
433
438
|
return prismaDev.startPrismaDevServer({
|
|
434
|
-
name:
|
|
439
|
+
name: serverName,
|
|
435
440
|
databasePort: dbPort,
|
|
436
441
|
port: httpPort,
|
|
437
442
|
shadowDatabasePort: shadowPort,
|
|
@@ -439,47 +444,36 @@ function main() {
|
|
|
439
444
|
persistenceMode: "stateful"
|
|
440
445
|
});
|
|
441
446
|
});
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
instanceName,
|
|
448
|
-
databasePort: dbPort,
|
|
449
|
-
url
|
|
447
|
+
leakedStartNames.delete(serverName);
|
|
448
|
+
return {
|
|
449
|
+
url: server.database.connectionString,
|
|
450
|
+
port: dbPort,
|
|
451
|
+
server
|
|
450
452
|
};
|
|
451
|
-
schedulePersist();
|
|
452
|
-
return { url };
|
|
453
453
|
} catch (err) {
|
|
454
454
|
if (isNameAlreadyTaken(err) && alreadyRunningRetries < MAX_ALREADY_RUNNING_RETRIES) {
|
|
455
455
|
alreadyRunningRetries += 1;
|
|
456
|
-
if (leakedStartNames.has(
|
|
457
|
-
if (!(await serverStatusOf(internalState,
|
|
458
|
-
await internalState.deleteServer(
|
|
456
|
+
if (leakedStartNames.has(serverName)) {
|
|
457
|
+
if (!(await serverStatusOf(internalState, serverName)).recordExists) {
|
|
458
|
+
await internalState.deleteServer(serverName).catch(() => void 0);
|
|
459
459
|
continue;
|
|
460
460
|
}
|
|
461
461
|
}
|
|
462
462
|
for (let poll = 1; poll <= ALREADY_RUNNING_POLLS; poll += 1) {
|
|
463
|
-
const now = await serverStatusOf(internalState,
|
|
464
|
-
if (now.live && now.pid !== void 0 && isPidAlive(now.pid) && now.url !== void 0) {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
databasePort: now.databasePort ?? portOfUrl(now.url) ?? dbPort,
|
|
469
|
-
url: now.url
|
|
470
|
-
};
|
|
471
|
-
schedulePersist();
|
|
472
|
-
return { url: now.url };
|
|
473
|
-
}
|
|
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
|
+
};
|
|
474
468
|
if (now.live && (now.pid === void 0 || !isPidAlive(now.pid))) {
|
|
475
|
-
await internalState.killServer(
|
|
469
|
+
await internalState.killServer(serverName).catch(() => void 0);
|
|
476
470
|
break;
|
|
477
471
|
}
|
|
478
472
|
await new Promise((resolve) => setTimeout(resolve, ALREADY_RUNNING_POLL_MS));
|
|
479
473
|
}
|
|
480
474
|
continue;
|
|
481
475
|
}
|
|
482
|
-
if (!isNameAlreadyTaken(err)) leakedStartNames.add(
|
|
476
|
+
if (!isNameAlreadyTaken(err)) leakedStartNames.add(serverName);
|
|
483
477
|
const conflictPort = portConflictOf(err, [dbPort, ...auxPorts]);
|
|
484
478
|
if (!(conflictPort !== void 0 && attempt < MAX_FRESH_PORT_CANDIDATES && (conflictPort !== dbPort || isFreshAllocation))) {
|
|
485
479
|
if (conflictPort === void 0 && !isNameAlreadyTaken(err) && blindRetries < 2) {
|
|
@@ -487,39 +481,131 @@ function main() {
|
|
|
487
481
|
continue;
|
|
488
482
|
}
|
|
489
483
|
const reason = err instanceof Error ? err.message : String(err);
|
|
490
|
-
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))}`);
|
|
491
485
|
}
|
|
492
486
|
conflicted.add(conflictPort);
|
|
493
487
|
}
|
|
494
488
|
}
|
|
495
489
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
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;
|
|
518
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);
|
|
519
517
|
}
|
|
520
|
-
|
|
518
|
+
state.discardedServers = remaining;
|
|
521
519
|
schedulePersist();
|
|
522
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
|
+
}
|
|
523
609
|
function json(res, status, body) {
|
|
524
610
|
res.writeHead(status, { "content-type": "application/json" });
|
|
525
611
|
res.end(JSON.stringify(body));
|
|
@@ -528,55 +614,79 @@ function main() {
|
|
|
528
614
|
res.writeHead(status, { "content-type": "text/plain; charset=utf-8" });
|
|
529
615
|
res.end(body);
|
|
530
616
|
}
|
|
531
|
-
function
|
|
532
|
-
|
|
617
|
+
function noContent(res) {
|
|
618
|
+
res.writeHead(204);
|
|
619
|
+
res.end();
|
|
533
620
|
}
|
|
534
|
-
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) {
|
|
535
630
|
return {
|
|
536
631
|
id: db.id,
|
|
632
|
+
labels: db.labels,
|
|
633
|
+
port: db.port,
|
|
537
634
|
url: db.url,
|
|
538
|
-
|
|
539
|
-
databasePort: db.databasePort
|
|
635
|
+
status: isRunning(db.id) ? "running" : "stopped"
|
|
540
636
|
};
|
|
541
637
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
function lastKnownPrismaDevModulePath(app) {
|
|
545
|
-
return recentPrismaDevModulePath.get(app);
|
|
638
|
+
function unknownInstances(res, ids) {
|
|
639
|
+
text(res, 404, `no such instance: ${ids.map((id) => `"${id}"`).join(", ")}`);
|
|
546
640
|
}
|
|
547
641
|
async function handleRequest(req, res) {
|
|
548
642
|
const url = new URL(req.url ?? "/", `http://127.0.0.1:${String(port)}`);
|
|
549
643
|
const method = req.method ?? "GET";
|
|
550
644
|
const segments = url.pathname.split("/").filter((s) => s.length > 0).map((s) => decodeURIComponent(s));
|
|
551
645
|
if (method === "GET" && segments.length === 1 && segments[0] === "health") return json(res, 200, { version: ownVersion });
|
|
552
|
-
if (segments[0]
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
return text(res, 400, "malformed JSON body");
|
|
564
|
-
}
|
|
565
|
-
if (!isDatabaseBody(parsed)) return text(res, 400, "malformed database body: expected { \"prismaDevModulePath\": string }");
|
|
566
|
-
recentPrismaDevModulePath.set(app, parsed.prismaDevModulePath);
|
|
567
|
-
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)));
|
|
568
657
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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);
|
|
572
667
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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);
|
|
578
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);
|
|
579
687
|
}
|
|
688
|
+
if (!db) return unknownInstances(res, id === void 0 ? [] : [id]);
|
|
689
|
+
if (method === "GET" && segments.length === 2) return json(res, 200, instanceView(db));
|
|
580
690
|
res.writeHead(404);
|
|
581
691
|
res.end();
|
|
582
692
|
}
|
|
@@ -587,7 +697,7 @@ function main() {
|
|
|
587
697
|
});
|
|
588
698
|
});
|
|
589
699
|
async function shutdown() {
|
|
590
|
-
for (const
|
|
700
|
+
for (const live of runtimes.values()) await live.close().catch(() => void 0);
|
|
591
701
|
await stateFile.flush();
|
|
592
702
|
server.close();
|
|
593
703
|
process.exit(0);
|
|
@@ -600,8 +710,9 @@ function main() {
|
|
|
600
710
|
process.on("uncaughtException", (err) => {
|
|
601
711
|
console.error("postgres-main: uncaught exception from background work:", err instanceof Error ? maskCredentials(err.stack ?? err.message) : err);
|
|
602
712
|
});
|
|
603
|
-
|
|
604
|
-
|
|
713
|
+
loadState(stateDir, statePath).then((loaded) => {
|
|
714
|
+
state = loaded;
|
|
715
|
+
schedulePersist();
|
|
605
716
|
server.listen(port, "127.0.0.1", () => {
|
|
606
717
|
console.log(`[dev-emulators] postgres-main listening on 127.0.0.1:${String(port)}`);
|
|
607
718
|
});
|
|
@@ -610,6 +721,26 @@ function main() {
|
|
|
610
721
|
process.exit(1);
|
|
611
722
|
});
|
|
612
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
|
+
}
|
|
613
744
|
main();
|
|
614
745
|
//#endregion
|
|
615
746
|
export {};
|