@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.
@@ -1,42 +1,10 @@
1
- import { a as readJsonFile, i as isValidSegment, n as StateFile, o as readOwnVersion, r as isPidAlive } from "./segments-BW2SYdBf-CtmcRzwy.mjs";
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 `Database`
112
- * resource, several servers in this one daemon process. Loopback
113
- * `node:http` JSON admin API; state under its `--state-dir`.
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 APPS_STATE_MODE = 384;
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 — @prisma/composer-prisma-cloud declares it as a dependency, but the given module path did not load; reinstall your dependencies.";
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" && "instanceName" in value && typeof value.instanceName === "string" && "databasePort" in value && typeof value.databasePort === "number" && "url" in value && typeof value.url === "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 isAppRecord(value) {
150
- return typeof value === "object" && value !== null && "databases" in value && typeof value.databases === "object" && value.databases !== null && Object.values(value.databases).every(isDatabaseRecord);
140
+ function isStartBody(value) {
141
+ return isIdsBody(value) && "prismaDevModulePath" in value && typeof value.prismaDevModulePath === "string";
151
142
  }
152
- function isAppsState(value) {
153
- return typeof value === "object" && value !== null && Object.values(value).every(isAppRecord);
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
- function isDatabaseBody(value) {
156
- return typeof value === "object" && value !== null && "prismaDevModulePath" in value && typeof value.prismaDevModulePath === "string";
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 appsJsonPath = path.join(stateDir, "apps.json");
321
- const stateFile = new StateFile(appsJsonPath, APPS_STATE_MODE);
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 getOrCreateApp(app) {
345
- let appRec = state[app];
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 smallestUnusedDatabasePort(min, alsoExclude = /* @__PURE__ */ new Set()) {
354
+ async function smallestUnusedPort(exclude) {
358
355
  return getPorts({
359
- port: portNumbers(min, MAX_DATABASE_PORT),
360
- exclude: [...usedDatabasePorts(), ...alsoExclude]
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
- /** Concurrent PUTs for the same database coalesce onto one start — a second `startPrismaDevServer` for a name whose first start is still booting fails as "already running". */
377
- function ensureDatabase(app, id, prismaDevModulePath) {
378
- const instanceName = instanceNameFor(app, id);
379
- const inflight = inflightEnsures.get(instanceName);
380
- if (inflight) return inflight;
381
- const run = ensureDatabaseSerialized(app, id, prismaDevModulePath, instanceName).finally(() => {
382
- inflightEnsures.delete(instanceName);
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
- const recorded = await serverStatusOf(internalState, instanceName);
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
- appRec.databases[id] = {
398
- id,
399
- instanceName,
400
- databasePort: recorded.databasePort ?? portOfUrl(recorded.url) ?? MIN_DATABASE_PORT,
401
- url: recorded.url
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, instanceName);
417
- if (isFreshAllocation) dbPort = await smallestUnusedDatabasePort(MIN_DATABASE_PORT, /* @__PURE__ */ new Set([...registryPorts, ...conflicted]));
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 port = await smallestUnusedDatabasePort(MIN_DATABASE_PORT, auxExclude);
425
- auxExclude.add(port);
426
- auxPorts.push(port);
427
- return port;
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: instanceName,
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
- const url = server.database.connectionString;
442
- leakedStartNames.delete(instanceName);
443
- runtimes.set(instanceName, server);
444
- appRec.databases[id] = {
445
- id,
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(instanceName)) {
456
- if (!(await serverStatusOf(internalState, instanceName)).recordExists) {
457
- await internalState.deleteServer(instanceName).catch(() => void 0);
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, instanceName);
463
- if (now.live && now.pid !== void 0 && isPidAlive(now.pid) && now.url !== void 0) {
464
- appRec.databases[id] = {
465
- id,
466
- instanceName,
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(instanceName).catch(() => void 0);
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(instanceName);
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 "${instanceName}" on port ${String(dbPort)}: ${maskCredentials(firstLine(reason))}`);
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
- async function deleteApp(app, prismaDevModulePathHint) {
496
- const appRec = state[app];
497
- if (!appRec) return;
498
- const entries = Object.values(appRec.databases);
499
- for (const db of entries) {
500
- const server = runtimes.get(db.instanceName);
501
- if (server) {
502
- await server.close().catch(() => void 0);
503
- runtimes.delete(db.instanceName);
504
- }
505
- }
506
- if (entries.length > 0 && prismaDevModulePathHint) {
507
- const internalState = await importPrismaDevInternalState(prismaDevModulePathHint);
508
- for (const db of entries) {
509
- if (!runtimes.has(db.instanceName)) {
510
- if ((await serverStatusOf(internalState, db.instanceName)).pid !== process.pid) await internalState.killServer(db.instanceName).catch(() => void 0);
511
- }
512
- if (!await settledAfterClose(internalState, db.instanceName)) {
513
- console.error(`postgres-main: server "${db.instanceName}" still reports running after close — leaving its persisted data in place instead of risking a self-kill.`);
514
- continue;
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
- delete state[app];
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 badSegment(res, segment) {
531
- text(res, 400, `invalid path segment "${segment}": must match /^[a-z0-9][a-z0-9-]*$/ and be at most 63 characters`);
617
+ function noContent(res) {
618
+ res.writeHead(204);
619
+ res.end();
532
620
  }
533
- function databaseView(db) {
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
- instanceName: db.instanceName,
538
- databasePort: db.databasePort
635
+ status: isRunning(db.id) ? "running" : "stopped"
539
636
  };
540
637
  }
541
- /** The most recently observed `prismaDevModulePath` for any database of `app` — DELETE has no body of its own to carry one. */
542
- const recentPrismaDevModulePath = /* @__PURE__ */ new Map();
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] === "apps" && segments.length >= 2) {
552
- const app = segments[1];
553
- if (app === void 0 || !isValidSegment(app)) return badSegment(res, app ?? "");
554
- if (method === "PUT" && segments.length === 4 && segments[2] === "databases") {
555
- const id = segments[3];
556
- if (id === void 0 || !isValidSegment(id)) return badSegment(res, id ?? "");
557
- const raw = await readBody(req);
558
- let parsed;
559
- try {
560
- parsed = JSON.parse(raw.toString("utf8"));
561
- } catch {
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
- if (method === "GET" && segments.length === 3 && segments[2] === "databases") {
569
- const appRec = state[app];
570
- return json(res, 200, appRec ? Object.values(appRec.databases).map(databaseView) : []);
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
- if (method === "DELETE" && segments.length === 2) {
573
- await deleteApp(app, lastKnownPrismaDevModulePath(app));
574
- res.writeHead(204);
575
- res.end();
576
- return;
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 server of runtimes.values()) await server.close().catch(() => void 0);
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
- readJsonFile(appsJsonPath, isAppsState).then((loaded) => {
603
- if (loaded) state = loaded;
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 {};