@prisma/composer-prisma-cloud 0.30.0-dev.2 → 0.30.0-dev.3

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,5 +1,5 @@
1
1
  import { t as blindCast$1 } from "./casts-DpaahrlC-Ykus3kCE.mjs";
2
- import { E as isWithin, b as mintKeyPair, c as PrismaCloudContainer, d as collectPreflightNames, h as prismaCloudContainerOf, i as OrmMigrationProvider, n as GeneratedParamProvider, p as deserialize, s as PgWarmProvider, u as S3CredentialsProvider, v as serviceAppName, w as ServiceKeyProvider, x as Providers } from "./s3-credentials-resource-BZjtuopJ-D0fKtjEH.mjs";
2
+ import { E as isWithin, b as mintKeyPair, c as PrismaCloudContainer, d as collectPreflightNames, h as prismaCloudContainerOf, i as OrmMigrationProvider, n as GeneratedParamProvider, p as deserialize, s as PgWarmProvider, u as S3CredentialsProvider, v as serviceAppName, w as ServiceKeyProvider, x as Providers } from "./s3-credentials-resource-BZjtuopJ-azxxffwQ.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { blindCast } from "@prisma/composer/casts";
5
5
  import * as Effect from "effect/Effect";
@@ -1564,7 +1564,7 @@ var require_adapter = /* @__PURE__ */ __commonJSMin(((exports, module) => {
1564
1564
  };
1565
1565
  }));
1566
1566
  //#endregion
1567
- //#region ../../1-prisma-cloud/0-lowering/dev-emulators/dist/segments-BW2SYdBf.mjs
1567
+ //#region ../../1-prisma-cloud/0-lowering/dev-emulators/dist/daemon-D9BV062Q.mjs
1568
1568
  var import_proper_lockfile = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
1569
1569
  const lockfile = require_lockfile();
1570
1570
  const { toPromise, toSync, toSyncOptions } = require_adapter();
@@ -2050,6 +2050,8 @@ async function ensureDaemon(name, entry, opts = {}) {
2050
2050
  await release();
2051
2051
  }
2052
2052
  }
2053
+ //#endregion
2054
+ //#region ../../1-prisma-cloud/0-lowering/dev-emulators/dist/segments-CA7TOJ1U.mjs
2053
2055
  /**
2054
2056
  * API hygiene shared by both daemons (local-dev spec § 2): every `<app>`,
2055
2057
  * `<id>`, and `<name>` path segment must match this shape or the request is
@@ -2134,19 +2136,22 @@ const ADMIN_FETCH_RETRY_DELAY_MS = 150;
2134
2136
  * `fetch` for the loopback admin calls, retried on transient socket errors:
2135
2137
  * a keep-alive reuse race (the runtime re-uses a pooled connection the
2136
2138
  * daemon's HTTP server just closed idle — surfaces as "fetch failed" /
2137
- * "other side closed") or a briefly overloaded daemon. Every admin call is
2138
- * idempotent by design, so a short blind retry is safe. A caller-driven
2139
- * abort is never retried. When every attempt fails to connect, it throws
2140
- * `DEV.EMULATOR_UNREACHABLE`; callers retry that code and nothing else.
2139
+ * "other side closed") or a briefly overloaded daemon. A short blind retry is
2140
+ * safe only for an idempotent call; a call that is not, such as creating an
2141
+ * instance, passes `retry: false`, because the emulator may have handled the
2142
+ * first attempt. A caller-driven abort is never retried. When every attempt
2143
+ * fails to connect, it throws `DEV.EMULATOR_UNREACHABLE`; callers retry that
2144
+ * code and nothing else.
2141
2145
  */
2142
- async function adminFetch(url, init) {
2146
+ async function adminFetch(url, init, opts = {}) {
2147
+ const attempts = opts.retry === false ? 1 : ADMIN_FETCH_ATTEMPTS;
2143
2148
  let lastError;
2144
- for (let attempt = 1; attempt <= ADMIN_FETCH_ATTEMPTS; attempt += 1) try {
2149
+ for (let attempt = 1; attempt <= attempts; attempt += 1) try {
2145
2150
  return await fetch(url, init);
2146
2151
  } catch (error) {
2147
2152
  if (init?.signal?.aborted === true) throw error;
2148
2153
  lastError = error;
2149
- if (attempt < ADMIN_FETCH_ATTEMPTS) await new Promise((resolve) => setTimeout(resolve, ADMIN_FETCH_RETRY_DELAY_MS));
2154
+ if (attempt < attempts) await new Promise((resolve) => setTimeout(resolve, ADMIN_FETCH_RETRY_DELAY_MS));
2150
2155
  }
2151
2156
  throw new CliStructuredError("DEV.EMULATOR_UNREACHABLE", `Could not connect to the local dev emulator at ${new URL(url).origin}: ${lastError instanceof Error ? lastError.message : String(lastError)}`, {
2152
2157
  why: "The emulator is registered and its process is alive, but it did not accept the connection.",
@@ -2162,23 +2167,33 @@ async function expectOk(res) {
2162
2167
  }
2163
2168
  return res;
2164
2169
  }
2165
- const SERVICE_STATUSES = /* @__PURE__ */ new Set([
2170
+ const INSTANCE_STATUSES = /* @__PURE__ */ new Set([
2166
2171
  "running",
2167
2172
  "backoff",
2168
2173
  "held",
2169
2174
  "stopped"
2170
2175
  ]);
2171
- function isServiceInfo(value) {
2172
- return typeof value === "object" && value !== null && "id" in value && typeof value.id === "string" && "address" in value && typeof value.address === "string" && "port" in value && typeof value.port === "number" && "url" in value && typeof value.url === "string" && "status" in value && typeof value.status === "string" && SERVICE_STATUSES.has(value.status) && "logPath" in value && typeof value.logPath === "string";
2176
+ function isStringRecord$1(value) {
2177
+ return typeof value === "object" && value !== null && Object.values(value).every((v) => typeof v === "string");
2178
+ }
2179
+ function isInstanceInfo(value) {
2180
+ return typeof value === "object" && value !== null && "id" in value && typeof value.id === "string" && "labels" in value && isStringRecord$1(value.labels) && "port" in value && typeof value.port === "number" && "url" in value && typeof value.url === "string" && "status" in value && typeof value.status === "string" && INSTANCE_STATUSES.has(value.status) && "logPath" in value && typeof value.logPath === "string";
2173
2181
  }
2174
- function isServiceInfoArray(value) {
2175
- return Array.isArray(value) && value.every(isServiceInfo);
2182
+ function isInstanceInfoArray(value) {
2183
+ return Array.isArray(value) && value.every(isInstanceInfo);
2176
2184
  }
2177
2185
  function isHealthBody(value) {
2178
2186
  return typeof value === "object" && value !== null && "version" in value && typeof value.version === "string";
2179
2187
  }
2180
- function isServiceReservation(value) {
2181
- return typeof value === "object" && value !== null && "port" in value && typeof value.port === "number" && "url" in value && typeof value.url === "string";
2188
+ function instancePath(baseUrl, id) {
2189
+ return `${baseUrl}/instances/${encodeURIComponent(id)}`;
2190
+ }
2191
+ function postJson(body) {
2192
+ return {
2193
+ method: "POST",
2194
+ headers: { "content-type": "application/json" },
2195
+ body: JSON.stringify(body)
2196
+ };
2182
2197
  }
2183
2198
  function computeClient(opts = {}) {
2184
2199
  const baseUrl = resolveBaseUrl("compute", opts);
@@ -2189,26 +2204,33 @@ function computeClient(opts = {}) {
2189
2204
  if (!isHealthBody(body)) throw new Error("malformed /health response from the compute emulator");
2190
2205
  return body;
2191
2206
  },
2192
- async ensureService(app, id) {
2193
- const body = await (await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}/services/${encodeSegment(id)}`, { method: "PUT" }))).json();
2194
- if (!isServiceReservation(body)) throw new Error("malformed service-reservation response from the compute emulator");
2207
+ async createInstance(request) {
2208
+ const body = await (await expectOk(await adminFetch(`${baseUrl}/instances`, postJson(request), { retry: false }))).json();
2209
+ if (!isInstanceInfo(body)) throw new Error("malformed instance-create response from the compute emulator");
2195
2210
  return body;
2196
2211
  },
2197
- async putDeployment(app, id, deployment) {
2198
- await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}/services/${encodeSegment(id)}/deployment`, {
2212
+ async listInstances() {
2213
+ const body = await (await expectOk(await adminFetch(`${baseUrl}/instances`))).json();
2214
+ if (!isInstanceInfoArray(body)) throw new Error("malformed instances listing from the compute emulator");
2215
+ return body;
2216
+ },
2217
+ async getInstance(id) {
2218
+ const res = await adminFetch(instancePath(baseUrl, id));
2219
+ if (res.status === 404) return void 0;
2220
+ const body = await (await expectOk(res)).json();
2221
+ if (!isInstanceInfo(body)) throw new Error("malformed instance response from the compute emulator");
2222
+ return body;
2223
+ },
2224
+ async putDeployment(id, deployment) {
2225
+ await expectOk(await adminFetch(`${instancePath(baseUrl, id)}/deployment`, {
2199
2226
  method: "PUT",
2200
2227
  headers: { "content-type": "application/json" },
2201
2228
  body: JSON.stringify(deployment)
2202
2229
  }));
2203
2230
  },
2204
- async listServices(app) {
2205
- const body = await (await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}/services`))).json();
2206
- if (!isServiceInfoArray(body)) throw new Error("malformed services listing from the compute emulator");
2207
- return body;
2208
- },
2209
- async *followLogs(app, id, signal, opts) {
2231
+ async *followLogs(id, signal, opts) {
2210
2232
  const tail = opts?.tail !== void 0 ? `&tail=${String(Math.max(0, Math.trunc(opts.tail)))}` : "";
2211
- const body = (await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}/services/${encodeSegment(id)}/logs?follow=1${tail}`, signal ? { signal } : void 0))).body;
2233
+ const body = (await expectOk(await adminFetch(`${instancePath(baseUrl, id)}/logs?follow=1${tail}`, signal ? { signal } : void 0))).body;
2212
2234
  if (!body) return;
2213
2235
  const reader = body.getReader();
2214
2236
  const decoder = new TextDecoder();
@@ -2222,14 +2244,14 @@ function computeClient(opts = {}) {
2222
2244
  await reader.cancel().catch(() => void 0);
2223
2245
  }
2224
2246
  },
2225
- async stopApp(app) {
2226
- await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}/stop`, { method: "POST" }));
2247
+ async startInstances(ids) {
2248
+ await expectOk(await adminFetch(`${baseUrl}/instances/start`, postJson({ ids })));
2227
2249
  },
2228
- async startApp(app) {
2229
- await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}/start`, { method: "POST" }));
2250
+ async stopInstances(ids) {
2251
+ await expectOk(await adminFetch(`${baseUrl}/instances/stop`, postJson({ ids })));
2230
2252
  },
2231
- async deleteApp(app) {
2232
- await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}`, { method: "DELETE" }));
2253
+ async deleteInstance(id) {
2254
+ await expectOk(await adminFetch(instancePath(baseUrl, id), { method: "DELETE" }));
2233
2255
  }
2234
2256
  };
2235
2257
  }
@@ -5833,6 +5855,144 @@ function secretsStore(devDir) {
5833
5855
  return createStore(path$1.join(devDir, "secrets.json"), isStringRecord, {}, 384);
5834
5856
  }
5835
5857
  /**
5858
+ * Which emulator instance backs each node of the app run from a directory.
5859
+ * `prisma dev` records it in `<directory>/.prisma-composer/dev/`, and every
5860
+ * Composer operation on an emulator instance goes through it. The emulators
5861
+ * know nothing about Composer: the labels written on create are for people
5862
+ * reading an emulator listing, and nothing here reads them back.
5863
+ */
5864
+ const EMULATOR_INSTANCES_FILE = "emulator-instances.json";
5865
+ function isNodeInstance(value) {
5866
+ return typeof value === "object" && value !== null && "kind" in value && value.kind === "compute" && "id" in value && typeof value.id === "string" && "port" in value && typeof value.port === "number";
5867
+ }
5868
+ function isEmulatorInstances(value) {
5869
+ return typeof value === "object" && value !== null && "app" in value && typeof value.app === "string" && "nodes" in value && typeof value.nodes === "object" && value.nodes !== null && Object.values(value.nodes).every(isNodeInstance);
5870
+ }
5871
+ function directoryOf(devDir) {
5872
+ const normalized = path$1.normalize(devDir);
5873
+ return normalized.endsWith(DEV_DIR) ? normalized.slice(0, -DEV_DIR.length - 1) : normalized;
5874
+ }
5875
+ function store(devDir) {
5876
+ return createStore(path$1.join(devDir, EMULATOR_INSTANCES_FILE), isEmulatorInstances, void 0);
5877
+ }
5878
+ /** This app's nodes. A record left by another app run from the same directory counts as empty. */
5879
+ async function appNodes(scope) {
5880
+ const current = await store(scope.devDir).read();
5881
+ return current?.app === scope.app ? current.nodes : {};
5882
+ }
5883
+ async function updateNodes(scope, mutate) {
5884
+ await store(scope.devDir).update((current) => {
5885
+ const nodes = { ...current?.app === scope.app ? current.nodes : {} };
5886
+ mutate(nodes);
5887
+ return {
5888
+ app: scope.app,
5889
+ nodes
5890
+ };
5891
+ });
5892
+ }
5893
+ /**
5894
+ * A directory has one dev instance. When the record names another app, that
5895
+ * app's compute instances are deleted and the record starts over for this
5896
+ * one. Returns the previous app's name when it was retired.
5897
+ */
5898
+ async function retirePreviousApp(scope) {
5899
+ const current = await store(scope.devDir).read();
5900
+ if (current === void 0 || current.app === scope.app) return void 0;
5901
+ const client = computeClient();
5902
+ for (const instance of Object.values(current.nodes)) await client.deleteInstance(instance.id);
5903
+ await store(scope.devDir).update(() => ({
5904
+ app: scope.app,
5905
+ nodes: {}
5906
+ }));
5907
+ return current.app;
5908
+ }
5909
+ /** Each compute node of the app, with the instance that backs it. */
5910
+ async function computeNodeInstances(scope) {
5911
+ return Object.entries(await appNodes(scope)).map(([address, instance]) => ({
5912
+ address,
5913
+ instance
5914
+ }));
5915
+ }
5916
+ /** The compute instance recorded for `address`; undefined when there is none. */
5917
+ async function mappedComputeInstance(scope, address) {
5918
+ return (await appNodes(scope))[address];
5919
+ }
5920
+ /**
5921
+ * The instance that runs the service at `address`. The recorded instance is
5922
+ * reused while the Compute emulator still has it. Otherwise a new one is
5923
+ * created, requesting the recorded port, and recorded.
5924
+ */
5925
+ async function ensureComputeInstance(scope, address) {
5926
+ const client = computeClient();
5927
+ const recorded = await mappedComputeInstance(scope, address);
5928
+ if (recorded && await client.getInstance(recorded.id)) return { instance: recorded };
5929
+ const created = await client.createInstance({
5930
+ labels: {
5931
+ "composer.directory": directoryOf(scope.devDir),
5932
+ "composer.app": scope.app,
5933
+ "composer.address": address
5934
+ },
5935
+ ...recorded ? { port: recorded.port } : {}
5936
+ });
5937
+ const instance = {
5938
+ kind: "compute",
5939
+ id: created.id,
5940
+ port: created.port
5941
+ };
5942
+ await updateNodes(scope, (nodes) => {
5943
+ nodes[address] = instance;
5944
+ });
5945
+ if (recorded && recorded.port !== created.port) return {
5946
+ instance,
5947
+ portChange: {
5948
+ address,
5949
+ previous: recorded.port,
5950
+ given: created.port
5951
+ }
5952
+ };
5953
+ return { instance };
5954
+ }
5955
+ /** The emulator's view of the instance recorded for `address`; undefined when either is missing. */
5956
+ async function liveComputeInstance(scope, address) {
5957
+ const recorded = await mappedComputeInstance(scope, address);
5958
+ return recorded ? computeClient().getInstance(recorded.id) : void 0;
5959
+ }
5960
+ /** Deletes the instance recorded for `address` and forgets it. */
5961
+ async function deleteComputeInstance(scope, address) {
5962
+ const recorded = await mappedComputeInstance(scope, address);
5963
+ if (!recorded) return;
5964
+ await computeClient().deleteInstance(recorded.id);
5965
+ await updateNodes(scope, (nodes) => {
5966
+ delete nodes[address];
5967
+ });
5968
+ }
5969
+ /**
5970
+ * `--fresh`: deletes every instance the directory's record names, whichever
5971
+ * app wrote it, then empties the record. Teardown runs before the emulators
5972
+ * hook, so the record may still name the app run here before.
5973
+ */
5974
+ async function deleteRecordedInstances(devDir) {
5975
+ const current = await store(devDir).read();
5976
+ if (current === void 0) return;
5977
+ const client = computeClient();
5978
+ for (const instance of Object.values(current.nodes)) await client.deleteInstance(instance.id);
5979
+ await store(devDir).update(() => ({
5980
+ app: current.app,
5981
+ nodes: {}
5982
+ }));
5983
+ }
5984
+ /**
5985
+ * The Compute emulator's failure messages name instances by id; `prisma dev`
5986
+ * names services by address. Rewrites a plain `Error`'s message and leaves
5987
+ * any other error, such as a structured one, untouched.
5988
+ */
5989
+ function withServiceAddresses(error, nodes) {
5990
+ if (!(error instanceof Error) || error.constructor !== Error) return error;
5991
+ let message = error.message.replace("deploy it with listens: false", "declare it with compute({ listens: false })");
5992
+ for (const { address, instance } of nodes) message = message.replaceAll(`instance "${instance.id}"`, `service "${address}"`);
5993
+ return message === error.message ? error : new Error(message, { cause: error });
5994
+ }
5995
+ /**
5836
5996
  * Local compute-cluster providers: upstream alchemy's `Prisma.App` and
5837
5997
  * `Prisma.Deployment` become clients of the machine-scoped Compute emulator;
5838
5998
  * `Prisma.EnvironmentVariable` becomes a row in the dev env store;
@@ -5937,16 +6097,6 @@ function readComputeManifest(artifactDir) {
5937
6097
  listens: parsed.listens !== false
5938
6098
  };
5939
6099
  }
5940
- /**
5941
- * A service address is dot-separated (`"orders.service"`) but the emulator's
5942
- * `<id>` segment must match `/^[a-z0-9][a-z0-9-]*$/`: disallowed characters
5943
- * become dashes, runs collapse. The real address still rides the deployment
5944
- * body's `address` field untouched, so listings show it verbatim.
5945
- */
5946
- function slugServiceId(address) {
5947
- const slug = address.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
5948
- return slug.length > 0 ? slug : "svc";
5949
- }
5950
6100
  async function materializeEnv(devDir, address, port) {
5951
6101
  const env = scopedEnvRows(await envStore(devDir).read(), address);
5952
6102
  env[servicePortEnvKey(address)] = JSON.stringify(port);
@@ -5956,21 +6106,30 @@ async function materializeEnv(devDir, address, port) {
5956
6106
  if (process.env["HOME"] !== void 0) env["HOME"] = process.env["HOME"];
5957
6107
  return env;
5958
6108
  }
6109
+ function appScopeOf(input) {
6110
+ return {
6111
+ devDir: input.devDir,
6112
+ app: appNameOf(input.container)
6113
+ };
6114
+ }
6115
+ function localUrl(port) {
6116
+ return `http://localhost:${String(port)}`;
6117
+ }
5959
6118
  /**
5960
- * `Prisma.App` → the Compute emulator: reserves (or returns) the service's
5961
- * stable port. The app id here IS the service's own address, which
5962
- * `Deployment` slugs back into the emulator's id. `delete` is a no-op —
5963
- * instance removal belongs to `teardown` (`DELETE /apps/<app>`), not
5964
- * per-resource Alchemy deletes.
6119
+ * `Prisma.App` → the Compute emulator: finds or creates the instance that
6120
+ * runs the service, which holds the service's port. The app id here IS the
6121
+ * service's own address. An App whose instance was re-created on another
6122
+ * port is updated, so its dependents get the new URL. Deleting an App (its
6123
+ * service was removed) deletes its instance.
5965
6124
  */
5966
6125
  function LocalAppProvider(input) {
5967
6126
  return Provider.effect(App, Effect.succeed({
5968
6127
  list: () => Effect.succeed([]),
5969
6128
  reconcile: ({ id, news }) => Effect.tryPromise({
5970
6129
  try: async () => {
5971
- const appName = appNameOf(input.container);
5972
6130
  const name = news.displayName ?? id;
5973
- const { url } = await computeClient().ensureService(appName, slugServiceId(name));
6131
+ const { instance } = await ensureComputeInstance(appScopeOf(input), name);
6132
+ const url = localUrl(instance.port);
5974
6133
  return {
5975
6134
  appId: name,
5976
6135
  name,
@@ -5985,25 +6144,43 @@ function LocalAppProvider(input) {
5985
6144
  },
5986
6145
  catch: (cause) => cause
5987
6146
  }),
5988
- delete: () => Effect.void,
6147
+ diff: ({ output }) => Effect.tryPromise({
6148
+ try: async () => {
6149
+ if (output === void 0) return void 0;
6150
+ if ((await liveComputeInstance(appScopeOf(input), output.name))?.url === output.appEndpointDomain) return void 0;
6151
+ return { action: "update" };
6152
+ },
6153
+ catch: (cause) => cause
6154
+ }),
6155
+ delete: ({ output }) => Effect.tryPromise({
6156
+ try: () => deleteComputeInstance(appScopeOf(input), output.name),
6157
+ catch: (cause) => cause
6158
+ }),
5989
6159
  read: ({ output }) => Effect.succeed(output)
5990
6160
  }));
5991
6161
  }
5992
6162
  /**
5993
- * Reserves each service's emulator port one at a time, in the order given,
5994
- * before Alchemy applies the `App` resources concurrently. A new service gets
6163
+ * Ensures each service's emulator instance one at a time, in the order given,
6164
+ * before Alchemy applies the `App` resources concurrently. A new instance gets
5995
6165
  * the smallest free port, so on a fresh start the ports follow this order; a
5996
- * service that already has a port keeps it. `serviceAppNames` are the
5997
- * `App` resources' `displayName`s.
6166
+ * service that already has an instance keeps its port. `serviceAppNames` are
6167
+ * the `App` resources' `displayName`s. Returns the services whose instance had
6168
+ * to be re-created and could not get its remembered port back.
5998
6169
  */
5999
- async function reserveServicePorts(container, serviceAppNames) {
6000
- const appName = appNameOf(container);
6170
+ async function ensureServiceInstances(container, devDir, serviceAppNames) {
6171
+ const scope = {
6172
+ devDir,
6173
+ app: appNameOf(container)
6174
+ };
6175
+ const changes = [];
6001
6176
  for (const service of serviceAppNames) try {
6002
- await computeClient().ensureService(appName, slugServiceId(service));
6177
+ const { portChange } = await ensureComputeInstance(scope, service);
6178
+ if (portChange) changes.push(portChange);
6003
6179
  } catch (cause) {
6004
6180
  const reason = cause instanceof Error ? cause.message : String(cause);
6005
- throw new Error(`reserving a local port for service "${service}" failed: ${reason}`, { cause });
6181
+ throw new Error(`ensuring the local service for service "${service}" failed: ${reason}`, { cause });
6006
6182
  }
6183
+ return changes;
6007
6184
  }
6008
6185
  /** `Prisma.EnvironmentVariable` → a key/value row in `<devDir>/env.json`. Upstream's value is `Redacted`; env.json holds the plain string the child process is given. */
6009
6186
  function LocalEnvironmentVariableProvider(input) {
@@ -6043,8 +6220,8 @@ function LocalEnvironmentVariableProvider(input) {
6043
6220
  }));
6044
6221
  }
6045
6222
  /**
6046
- * `Prisma.Deployment` → unpacks the artifact once per hash, fetches the
6047
- * emulator's assigned port, materializes the child's full env (env store +
6223
+ * `Prisma.Deployment` → unpacks the artifact once per hash, finds the
6224
+ * service's emulator instance and its port, materializes the child's full env (env store +
6048
6225
  * secrets + the port override + `PATH`/`HOME`), and puts the deployment — the
6049
6226
  * emulator (re)starts the child only when the hash or env actually changed.
6050
6227
  * `portMapping` is ignored on purpose: the emulator owns port allocation, and
@@ -6056,24 +6233,27 @@ function LocalDeploymentProvider(input) {
6056
6233
  list: () => Effect.succeed([]),
6057
6234
  reconcile: ({ news }) => Effect.tryPromise({
6058
6235
  try: async () => {
6059
- const app = appNameOf(input.container);
6060
6236
  const appId = appIdOfInput(news.app);
6061
- const emulatorId = slugServiceId(appId);
6062
6237
  if (news.artifactPath === void 0) throw new Error("local Deployment requires an artifactPath — nothing to run.");
6063
6238
  const artifactHash = await artifactSha256(news.artifactPath);
6064
6239
  const artifactDir = path$1.join(input.devDir, "artifacts", artifactHash);
6065
6240
  if (!fs$2.existsSync(artifactDir)) extractComputeArtifact(news.artifactPath, artifactDir);
6066
6241
  const { address, listens } = readComputeManifest(artifactDir);
6067
- const { port } = await computeClient().ensureService(app, emulatorId);
6068
- const env = await materializeEnv(input.devDir, address, port);
6069
- await computeClient().putDeployment(app, emulatorId, {
6070
- address,
6071
- artifactDir,
6072
- artifactHash,
6073
- env,
6074
- port,
6075
- listens
6076
- });
6242
+ const { instance } = await ensureComputeInstance(appScopeOf(input), appId);
6243
+ const env = await materializeEnv(input.devDir, address, instance.port);
6244
+ try {
6245
+ await computeClient().putDeployment(instance.id, {
6246
+ artifactDir,
6247
+ artifactHash,
6248
+ env,
6249
+ listens
6250
+ });
6251
+ } catch (error) {
6252
+ throw withServiceAddresses(error, [{
6253
+ address,
6254
+ instance
6255
+ }]);
6256
+ }
6077
6257
  return {
6078
6258
  deploymentId: artifactHash,
6079
6259
  appId,
@@ -6081,12 +6261,20 @@ function LocalDeploymentProvider(input) {
6081
6261
  status: "running",
6082
6262
  previewDomain: null,
6083
6263
  artifactHash,
6084
- appEndpointDomain: `http://localhost:${port}`,
6264
+ appEndpointDomain: localUrl(instance.port),
6085
6265
  createdAt: DEV_TIMESTAMP
6086
6266
  };
6087
6267
  },
6088
6268
  catch: (cause) => cause
6089
6269
  }),
6270
+ diff: ({ output }) => Effect.tryPromise({
6271
+ try: async () => {
6272
+ if (output === void 0) return void 0;
6273
+ if ((await liveComputeInstance(appScopeOf(input), output.appId))?.artifactHash === output.artifactHash) return void 0;
6274
+ return { action: "update" };
6275
+ },
6276
+ catch: (cause) => cause
6277
+ }),
6090
6278
  delete: () => Effect.void
6091
6279
  }));
6092
6280
  }
@@ -6275,7 +6463,7 @@ const RELIST_INTERVAL_MS = 2e3;
6275
6463
  * dropped (an emulator restart shows a gap in that service's stream, never a
6276
6464
  * dead session).
6277
6465
  */
6278
- async function* mergedLogs(app, signal, tail) {
6466
+ async function* mergedLogs(scope, signal, tail) {
6279
6467
  const client = computeClient();
6280
6468
  const followed = /* @__PURE__ */ new Set();
6281
6469
  const backlogged = /* @__PURE__ */ new Set();
@@ -6294,7 +6482,7 @@ async function* mergedLogs(app, signal, tail) {
6294
6482
  (async () => {
6295
6483
  try {
6296
6484
  let buffer = "";
6297
- for await (const chunk of client.followLogs(app, id, signal, { tail: followTail })) {
6485
+ for await (const chunk of client.followLogs(id, signal, { tail: followTail })) {
6298
6486
  buffer += chunk;
6299
6487
  let newlineAt = buffer.indexOf("\n");
6300
6488
  while (newlineAt !== -1) {
@@ -6312,8 +6500,7 @@ async function* mergedLogs(app, signal, tail) {
6312
6500
  })();
6313
6501
  };
6314
6502
  const relist = async () => {
6315
- const services = await client.listServices(app);
6316
- for (const svc of services) follow(svc.id, svc.address);
6503
+ for (const { address, instance } of await computeNodeInstances(scope)) follow(instance.id, address);
6317
6504
  };
6318
6505
  await relist();
6319
6506
  const timer = setInterval(() => void relist(), RELIST_INTERVAL_MS);
@@ -6338,18 +6525,31 @@ async function* mergedLogs(app, signal, tail) {
6338
6525
  }
6339
6526
  }
6340
6527
  async function devAttach(input) {
6341
- const app = prismaCloudContainerOf(input.container).input.appName;
6528
+ const scope = {
6529
+ devDir: input.devDir,
6530
+ app: prismaCloudContainerOf(input.container).input.appName
6531
+ };
6342
6532
  const client = computeClient();
6343
6533
  return {
6344
- startServices: () => client.startApp(app),
6534
+ startServices: async () => {
6535
+ const nodes = await computeNodeInstances(scope);
6536
+ try {
6537
+ await client.startInstances(nodes.map(({ instance }) => instance.id));
6538
+ } catch (error) {
6539
+ throw withServiceAddresses(error, nodes);
6540
+ }
6541
+ },
6345
6542
  endpoints: async () => {
6346
- return (await client.listServices(app)).map((svc) => ({
6347
- address: svc.address,
6348
- url: svc.url
6543
+ return (await computeNodeInstances(scope)).map(({ address, instance }) => ({
6544
+ address,
6545
+ url: `http://localhost:${String(instance.port)}`
6349
6546
  }));
6350
6547
  },
6351
- logs: (signal, opts) => mergedLogs(app, signal, opts?.tail ?? 0),
6352
- stopServices: () => client.stopApp(app)
6548
+ logs: (signal, opts) => mergedLogs(scope, signal, opts?.tail ?? 0),
6549
+ stopServices: async () => {
6550
+ const nodes = await computeNodeInstances(scope);
6551
+ await client.stopInstances(nodes.map(({ instance }) => instance.id));
6552
+ }
6353
6553
  };
6354
6554
  }
6355
6555
  const LOCAL_PROJECT_ID = "local";
@@ -6384,7 +6584,13 @@ function serviceAppNames(input) {
6384
6584
  async function runDevEmulators(input) {
6385
6585
  const { url: computeUrl } = await ensureDaemon("compute", daemonEntry("compute"));
6386
6586
  console.log(`[dev] compute emulator ready at ${computeUrl}`);
6387
- await reserveServicePorts(input.container, serviceAppNames(input));
6587
+ const retired = await retirePreviousApp({
6588
+ devDir: input.devDir,
6589
+ app: appNameOf(input.container)
6590
+ });
6591
+ if (retired !== void 0) console.log(`[dev] removed the local services of "${retired}", the app run here before`);
6592
+ const portChanges = await ensureServiceInstances(input.container, input.devDir, serviceAppNames(input));
6593
+ for (const { address, previous, given } of portChanges) console.log(`[dev] service "${address}" now uses port ${String(given)}: its previous port ${String(previous)} is in use`);
6388
6594
  if (usesBuckets(input)) {
6389
6595
  const { url: bucketsUrl } = await ensureDaemon("buckets", daemonEntry("buckets"));
6390
6596
  console.log(`[dev] buckets emulator ready at ${bucketsUrl}`);
@@ -6441,13 +6647,14 @@ async function tolerateUnreachable(action) {
6441
6647
  async function runDevTeardown(input) {
6442
6648
  const app = prismaCloudContainerOf(input.container).input.appName;
6443
6649
  const cwd = process.cwd();
6650
+ const devDir = `${cwd}/${DEV_DIR}`;
6444
6651
  await tolerateUnreachable(() => postgresClient().deleteApp(app));
6445
6652
  try {
6446
6653
  await ensureDaemon("compute", daemonEntry("compute"));
6447
- await computeClient().deleteApp(app);
6654
+ await deleteRecordedInstances(devDir);
6448
6655
  } finally {
6449
6656
  await tolerateUnreachable(() => bucketsClient().deleteApp(app));
6450
- removeLocalPaths([`${cwd}/${DEV_DIR}`, `${cwd}/.alchemy/state/${app}/dev`]);
6657
+ removeLocalPaths([devDir, `${cwd}/.alchemy/state/${app}/dev`]);
6451
6658
  }
6452
6659
  }
6453
6660
  /** `localTargetProviders()`'s `ProviderCollection` doesn't structurally unify with Alchemy's inferred providers Layer (a @internal/lowering typings gap); it satisfies it at runtime — mirrors `control/extension.ts`'s own `asProvidersLayer`, duplicated rather than shared so this module imports nothing from `./control`. */