prisma-sharding 0.0.5 → 0.0.6
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/README.md +102 -1
- package/dist/cli/migrate.js +181 -75
- package/dist/cli/studio.js +600 -59
- package/dist/cli/test.js +27 -21
- package/dist/cli/update.js +196 -80
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +19 -1
- package/dist/index.mjs +19 -1
- package/package.json +2 -1
package/dist/cli/studio.js
CHANGED
|
@@ -1,111 +1,652 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
3
25
|
|
|
4
26
|
// src/cli/studio.ts
|
|
5
27
|
var import_config = require("dotenv/config");
|
|
6
28
|
var import_child_process = require("child_process");
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
var
|
|
29
|
+
|
|
30
|
+
// src/utils/env.ts
|
|
31
|
+
var parseBooleanEnv = (name, defaultValue, env = process.env) => {
|
|
32
|
+
const value = env[name]?.trim();
|
|
33
|
+
if (value === void 0 || value === "") {
|
|
34
|
+
return defaultValue;
|
|
35
|
+
}
|
|
36
|
+
return ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
37
|
+
};
|
|
38
|
+
var parsePositiveIntegerEnv = (name, defaultValue, env = process.env) => {
|
|
39
|
+
const value = env[name]?.trim();
|
|
40
|
+
if (!value) {
|
|
41
|
+
return defaultValue;
|
|
42
|
+
}
|
|
43
|
+
const parsed = parseInt(value, 10);
|
|
44
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : defaultValue;
|
|
45
|
+
};
|
|
46
|
+
var isVerboseEnv = (names, env = process.env) => names.some((name) => parseBooleanEnv(name, false, env));
|
|
47
|
+
|
|
48
|
+
// src/cli/utils/command.ts
|
|
49
|
+
var getNpxCommand = () => process.platform === "win32" ? "npx.cmd" : "npx";
|
|
50
|
+
|
|
51
|
+
// src/cli/utils/ports.ts
|
|
52
|
+
var import_http = __toESM(require("http"));
|
|
53
|
+
var import_net = __toESM(require("net"));
|
|
54
|
+
var LISTEN_CHECK_HOSTS = ["127.0.0.1", "::1", "0.0.0.0", "::"];
|
|
55
|
+
var HTTP_PROBE_HOSTS = ["127.0.0.1", "::1", "localhost"];
|
|
56
|
+
var HTTP_PROBE_BODY_LIMIT = 16 * 1024;
|
|
57
|
+
var unsupportedAddressCodes = /* @__PURE__ */ new Set(["EADDRNOTAVAIL", "EAFNOSUPPORT"]);
|
|
58
|
+
var checkListenHost = (port, host) => {
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
const server = import_net.default.createServer();
|
|
61
|
+
let settled = false;
|
|
62
|
+
const finish = (result) => {
|
|
63
|
+
if (!settled) {
|
|
64
|
+
settled = true;
|
|
65
|
+
resolve(result);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
server.once("error", (error) => {
|
|
69
|
+
const code = error.code;
|
|
70
|
+
if (code && unsupportedAddressCodes.has(code)) {
|
|
71
|
+
finish({ host, available: true, skipped: true, code, message: error.message });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
finish({ host, available: false, code, message: error.message });
|
|
75
|
+
});
|
|
76
|
+
server.listen({ port, host }, () => {
|
|
77
|
+
server.close(() => {
|
|
78
|
+
finish({ host, available: true });
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
var getPortUsage = async (port) => {
|
|
84
|
+
const checks = [];
|
|
85
|
+
for (const host of LISTEN_CHECK_HOSTS) {
|
|
86
|
+
checks.push(await checkListenHost(port, host));
|
|
87
|
+
}
|
|
88
|
+
const occupied = checks.some((check) => check.code === "EADDRINUSE");
|
|
89
|
+
const unavailable = checks.some(
|
|
90
|
+
(check) => !check.available && check.code && check.code !== "EADDRINUSE"
|
|
91
|
+
);
|
|
92
|
+
return {
|
|
93
|
+
port,
|
|
94
|
+
status: occupied ? "occupied" : unavailable ? "unavailable" : "available",
|
|
95
|
+
checks
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
var responseLooksLikePrismaStudio = (body, headers) => {
|
|
99
|
+
const headerText = Object.entries(headers).map(([key, value]) => `${key}:${Array.isArray(value) ? value.join(",") : value || ""}`).join("\n");
|
|
100
|
+
const text = `${headerText}
|
|
101
|
+
${body}`.toLowerCase();
|
|
102
|
+
const hasModernStudioShell = text.includes("window.__studio_config__") && text.includes("/studio.js") && text.includes("/studio.css");
|
|
103
|
+
const hasLegacyStudioShell = text.includes("createstudiobffclient") && text.includes("/data/bff/index.js") && text.includes("/ui/index.js") && text.includes("/adapter.js");
|
|
104
|
+
return text.includes("prisma studio") || text.includes("prisma-studio") || text.includes("@prisma/studio") || text.includes("prisma.io/studio") || text.includes("@prisma/studio-core") || hasModernStudioShell || hasLegacyStudioShell;
|
|
105
|
+
};
|
|
106
|
+
var probeHttpHost = (port, host, timeoutMs) => {
|
|
107
|
+
return new Promise((resolve) => {
|
|
108
|
+
let settled = false;
|
|
109
|
+
let body = "";
|
|
110
|
+
const finish = (probe) => {
|
|
111
|
+
if (!settled) {
|
|
112
|
+
settled = true;
|
|
113
|
+
resolve(probe);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const request = import_http.default.request(
|
|
117
|
+
{
|
|
118
|
+
hostname: host,
|
|
119
|
+
port,
|
|
120
|
+
path: "/",
|
|
121
|
+
method: "GET",
|
|
122
|
+
timeout: timeoutMs,
|
|
123
|
+
headers: {
|
|
124
|
+
Accept: "text/html,application/xhtml+xml,application/json;q=0.9,*/*;q=0.8"
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
(response) => {
|
|
128
|
+
response.setEncoding("utf8");
|
|
129
|
+
response.on("data", (chunk) => {
|
|
130
|
+
if (body.length < HTTP_PROBE_BODY_LIMIT) {
|
|
131
|
+
body += chunk.slice(0, HTTP_PROBE_BODY_LIMIT - body.length);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
response.on("end", () => {
|
|
135
|
+
const isPrismaStudio = responseLooksLikePrismaStudio(body, response.headers);
|
|
136
|
+
finish({
|
|
137
|
+
reachable: true,
|
|
138
|
+
isPrismaStudio,
|
|
139
|
+
host,
|
|
140
|
+
detail: `HTTP ${response.statusCode || "response"}`
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
request.once("timeout", () => {
|
|
146
|
+
request.destroy();
|
|
147
|
+
finish({
|
|
148
|
+
reachable: false,
|
|
149
|
+
isPrismaStudio: false,
|
|
150
|
+
host,
|
|
151
|
+
detail: `HTTP probe timed out after ${timeoutMs}ms`
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
request.once("error", (error) => {
|
|
155
|
+
finish({
|
|
156
|
+
reachable: false,
|
|
157
|
+
isPrismaStudio: false,
|
|
158
|
+
host,
|
|
159
|
+
detail: error.message
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
request.end();
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
var probePrismaStudio = async (port, timeoutMs = 1200) => {
|
|
166
|
+
const probes = await Promise.all(
|
|
167
|
+
HTTP_PROBE_HOSTS.map((host) => probeHttpHost(port, host, timeoutMs))
|
|
168
|
+
);
|
|
169
|
+
const prismaStudio = probes.find((probe) => probe.isPrismaStudio);
|
|
170
|
+
if (prismaStudio) {
|
|
171
|
+
return prismaStudio;
|
|
172
|
+
}
|
|
173
|
+
const reachable = probes.find((probe) => probe.reachable);
|
|
174
|
+
if (reachable) {
|
|
175
|
+
return {
|
|
176
|
+
...reachable,
|
|
177
|
+
isPrismaStudio: false,
|
|
178
|
+
detail: `${reachable.detail}; response did not look like Prisma Studio`
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
reachable: false,
|
|
183
|
+
isPrismaStudio: false,
|
|
184
|
+
detail: probes.map((probe) => `${probe.host}: ${probe.detail}`).join("; ")
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
188
|
+
var waitForPrismaStudio = async (port, timeoutMs, intervalMs = 400, shouldContinue = () => true) => {
|
|
189
|
+
const startedAt = Date.now();
|
|
190
|
+
let lastProbe = {
|
|
191
|
+
reachable: false,
|
|
192
|
+
isPrismaStudio: false,
|
|
193
|
+
detail: "Studio probe has not run yet"
|
|
194
|
+
};
|
|
195
|
+
while (Date.now() - startedAt < timeoutMs && shouldContinue()) {
|
|
196
|
+
lastProbe = await probePrismaStudio(port, Math.min(intervalMs, 1e3));
|
|
197
|
+
if (lastProbe.isPrismaStudio) {
|
|
198
|
+
return lastProbe;
|
|
199
|
+
}
|
|
200
|
+
if (!shouldContinue()) {
|
|
201
|
+
return lastProbe;
|
|
202
|
+
}
|
|
203
|
+
await delay(intervalMs);
|
|
204
|
+
}
|
|
205
|
+
return lastProbe;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// src/cli/utils/process.ts
|
|
209
|
+
var isChildProcessRunning = (childProcess) => {
|
|
210
|
+
return childProcess.exitCode === null && childProcess.signalCode === null && !childProcess.killed;
|
|
211
|
+
};
|
|
212
|
+
var terminateChildProcess = (childProcess, options = {}) => {
|
|
213
|
+
if (!isChildProcessRunning(childProcess)) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
const signal = options.signal || "SIGTERM";
|
|
217
|
+
try {
|
|
218
|
+
if (options.processGroup && childProcess.pid && process.platform !== "win32") {
|
|
219
|
+
try {
|
|
220
|
+
process.kill(-childProcess.pid, signal);
|
|
221
|
+
return true;
|
|
222
|
+
} catch {
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return childProcess.kill(signal);
|
|
226
|
+
} catch {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
var waitForChildProcessClose = (childProcess, timeoutMs) => {
|
|
231
|
+
return new Promise((resolve) => {
|
|
232
|
+
if (!isChildProcessRunning(childProcess)) {
|
|
233
|
+
resolve(true);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const timeout = setTimeout(() => {
|
|
237
|
+
cleanup();
|
|
238
|
+
resolve(false);
|
|
239
|
+
}, timeoutMs);
|
|
240
|
+
const onClose = () => {
|
|
241
|
+
clearTimeout(timeout);
|
|
242
|
+
cleanup();
|
|
243
|
+
resolve(true);
|
|
244
|
+
};
|
|
245
|
+
const cleanup = () => {
|
|
246
|
+
childProcess.off("close", onClose);
|
|
247
|
+
childProcess.off("exit", onClose);
|
|
248
|
+
};
|
|
249
|
+
childProcess.once("close", onClose);
|
|
250
|
+
childProcess.once("exit", onClose);
|
|
251
|
+
});
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// src/cli/utils/shards.ts
|
|
255
|
+
var NO_SHARDS_CONFIGURED_MESSAGE = "No shards configured. Set SHARD_COUNT and SHARD_N_URL environment variables.";
|
|
256
|
+
var getShardConfigResult = (env = process.env) => {
|
|
10
257
|
const shards = [];
|
|
11
|
-
const
|
|
258
|
+
const missingShardIds = [];
|
|
259
|
+
const shardCount = parseInt(env.SHARD_COUNT || "0", 10);
|
|
12
260
|
for (let i = 1; i <= shardCount; i++) {
|
|
13
|
-
const url =
|
|
261
|
+
const url = env[`SHARD_${i}_URL`];
|
|
14
262
|
if (url) {
|
|
15
|
-
shards.push({ id: `shard_${i}`, url });
|
|
263
|
+
shards.push({ id: `shard_${i}`, index: i - 1, url });
|
|
264
|
+
} else {
|
|
265
|
+
missingShardIds.push(`shard_${i}`);
|
|
16
266
|
}
|
|
17
267
|
}
|
|
18
|
-
if (shards.length === 0 &&
|
|
19
|
-
shards.push({ id: "shard_1", url:
|
|
268
|
+
if (shards.length === 0 && env.DATABASE_URL) {
|
|
269
|
+
shards.push({ id: "shard_1", index: 0, url: env.DATABASE_URL });
|
|
270
|
+
}
|
|
271
|
+
return { shards, missingShardIds };
|
|
272
|
+
};
|
|
273
|
+
var maskShardUrl = (url) => {
|
|
274
|
+
try {
|
|
275
|
+
const parsed = new URL(url);
|
|
276
|
+
if (parsed.password) {
|
|
277
|
+
parsed.password = "***";
|
|
278
|
+
}
|
|
279
|
+
return parsed.toString();
|
|
280
|
+
} catch {
|
|
281
|
+
return url.replace(/:[^:@]+@/, ":***@");
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// src/cli/studio.ts
|
|
286
|
+
var instances = [];
|
|
287
|
+
var activeOptions;
|
|
288
|
+
var isShuttingDown = false;
|
|
289
|
+
var keepAliveTimer;
|
|
290
|
+
var getStudioOptions = () => {
|
|
291
|
+
return {
|
|
292
|
+
basePort: parsePositiveIntegerEnv("SHARD_STUDIO_BASE_PORT", 51212),
|
|
293
|
+
reuseExisting: parseBooleanEnv("SHARD_STUDIO_REUSE_EXISTING", true),
|
|
294
|
+
startupTimeoutMs: parsePositiveIntegerEnv("SHARD_STUDIO_START_TIMEOUT_MS", 15e3),
|
|
295
|
+
stabilityMs: parsePositiveIntegerEnv("SHARD_STUDIO_STABILITY_MS", 500),
|
|
296
|
+
shutdownTimeoutMs: parsePositiveIntegerEnv("SHARD_STUDIO_SHUTDOWN_TIMEOUT_MS", 5e3),
|
|
297
|
+
strictPortCheck: parseBooleanEnv("SHARD_STUDIO_STRICT_PORT_CHECK", false),
|
|
298
|
+
verbose: isVerboseEnv(["SHARD_STUDIO_VERBOSE", "SHARD_STUDIO_DEBUG"])
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
var studioUrl = (port) => `http://localhost:${port}`;
|
|
302
|
+
var isAddressInUseOutput = (output) => output.includes("EADDRINUSE");
|
|
303
|
+
var outputLooksReady = (output, port) => {
|
|
304
|
+
const normalized = output.toLowerCase();
|
|
305
|
+
return normalized.includes("prisma studio is running") || normalized.includes("prisma studio is up") || normalized.includes(`localhost:${port}`) || normalized.includes(`127.0.0.1:${port}`);
|
|
306
|
+
};
|
|
307
|
+
var logVerbose = (options, message, writer = console.log) => {
|
|
308
|
+
if (options.verbose) {
|
|
309
|
+
writer(message);
|
|
20
310
|
}
|
|
21
|
-
return shards;
|
|
22
311
|
};
|
|
23
|
-
var
|
|
312
|
+
var writeVerboseOutput = (options, shardId, output, writer) => {
|
|
313
|
+
if (!options.verbose) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).forEach((line) => writer(` [${shardId}] ${line}`));
|
|
317
|
+
};
|
|
318
|
+
var failedInstance = (shard, port, message, details, severity = "error") => {
|
|
319
|
+
return {
|
|
320
|
+
shardId: shard.id,
|
|
321
|
+
port,
|
|
322
|
+
url: studioUrl(port),
|
|
323
|
+
status: "failed",
|
|
324
|
+
message,
|
|
325
|
+
details,
|
|
326
|
+
severity
|
|
327
|
+
};
|
|
328
|
+
};
|
|
329
|
+
var reusedInstance = (shard, port, details) => {
|
|
330
|
+
return {
|
|
331
|
+
shardId: shard.id,
|
|
332
|
+
port,
|
|
333
|
+
url: studioUrl(port),
|
|
334
|
+
status: "reused",
|
|
335
|
+
details
|
|
336
|
+
};
|
|
337
|
+
};
|
|
338
|
+
var getShardPort = (shard, options) => {
|
|
339
|
+
return options.basePort + shard.index;
|
|
340
|
+
};
|
|
341
|
+
var handleOccupiedPort = async (shard, port, options, source) => {
|
|
342
|
+
logVerbose(
|
|
343
|
+
options,
|
|
344
|
+
` Waiting for occupied ${shard.id} port ${port} to identify Prisma Studio...`
|
|
345
|
+
);
|
|
346
|
+
const probe = await waitForPrismaStudio(port, options.startupTimeoutMs);
|
|
347
|
+
if (probe.isPrismaStudio && options.reuseExisting) {
|
|
348
|
+
return reusedInstance(shard, port, "Existing Prisma Studio instance is already active");
|
|
349
|
+
}
|
|
350
|
+
if (probe.isPrismaStudio && !options.reuseExisting) {
|
|
351
|
+
return failedInstance(
|
|
352
|
+
shard,
|
|
353
|
+
port,
|
|
354
|
+
"Studio already running but reuse is disabled",
|
|
355
|
+
"Prisma Studio is already running on this port, but SHARD_STUDIO_REUSE_EXISTING is false"
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
const details = probe.reachable ? `Port ${port} is active but did not look like Prisma Studio (${probe.detail})` : `Port ${port} is active but could not be identified as Prisma Studio (${probe.detail})`;
|
|
359
|
+
const sourceDetails = source === "spawn" ? `${details}; Prisma Studio reported EADDRINUSE during startup` : details;
|
|
360
|
+
return failedInstance(
|
|
361
|
+
shard,
|
|
362
|
+
port,
|
|
363
|
+
"Port already used by another process",
|
|
364
|
+
sourceDetails,
|
|
365
|
+
"warning"
|
|
366
|
+
);
|
|
367
|
+
};
|
|
368
|
+
var startSpawnedStudio = (shard, port, options) => {
|
|
24
369
|
return new Promise((resolve, reject) => {
|
|
25
|
-
const port = BASE_PORT + index;
|
|
26
370
|
const shardId = shard.id;
|
|
27
|
-
|
|
371
|
+
const processGroup = process.platform !== "win32";
|
|
372
|
+
let stdout = "";
|
|
373
|
+
let stderr = "";
|
|
374
|
+
let settled = false;
|
|
375
|
+
let readySeen = false;
|
|
376
|
+
let addressInUse = false;
|
|
377
|
+
let stabilityTimer;
|
|
378
|
+
let resolvedInstance;
|
|
379
|
+
logVerbose(options, `
|
|
28
380
|
\u{1F680} Starting Prisma Studio for ${shardId} on port ${port}...`);
|
|
29
|
-
|
|
381
|
+
logVerbose(options, ` URL: ${maskShardUrl(shard.url)}`);
|
|
30
382
|
const studioProcess = (0, import_child_process.spawn)(
|
|
31
|
-
|
|
383
|
+
getNpxCommand(),
|
|
32
384
|
["prisma", "studio", "--port", port.toString(), "--browser", "none"],
|
|
33
385
|
{
|
|
34
386
|
env: {
|
|
35
387
|
...process.env,
|
|
36
388
|
DATABASE_URL: shard.url
|
|
37
389
|
},
|
|
38
|
-
|
|
390
|
+
detached: processGroup,
|
|
391
|
+
shell: false,
|
|
39
392
|
stdio: "pipe"
|
|
40
393
|
}
|
|
41
394
|
);
|
|
395
|
+
const settle = (instance) => {
|
|
396
|
+
if (settled) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
settled = true;
|
|
400
|
+
resolvedInstance = instance;
|
|
401
|
+
if (stabilityTimer) {
|
|
402
|
+
clearTimeout(stabilityTimer);
|
|
403
|
+
}
|
|
404
|
+
resolve(instance);
|
|
405
|
+
};
|
|
406
|
+
const fail = (message, details) => {
|
|
407
|
+
terminateChildProcess(studioProcess, { processGroup });
|
|
408
|
+
settle(failedInstance(shard, port, message, details));
|
|
409
|
+
};
|
|
410
|
+
const settleStartedAfterStability = () => {
|
|
411
|
+
if (settled || readySeen) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
readySeen = true;
|
|
415
|
+
stabilityTimer = setTimeout(() => {
|
|
416
|
+
settle({
|
|
417
|
+
shardId,
|
|
418
|
+
port,
|
|
419
|
+
url: studioUrl(port),
|
|
420
|
+
status: "started",
|
|
421
|
+
process: studioProcess,
|
|
422
|
+
processGroup
|
|
423
|
+
});
|
|
424
|
+
}, options.stabilityMs);
|
|
425
|
+
};
|
|
42
426
|
studioProcess.stdout?.on("data", (data) => {
|
|
43
427
|
const output = data.toString();
|
|
44
|
-
|
|
45
|
-
|
|
428
|
+
stdout += output;
|
|
429
|
+
if (outputLooksReady(output, port)) {
|
|
430
|
+
settleStartedAfterStability();
|
|
46
431
|
}
|
|
432
|
+
writeVerboseOutput(options, shardId, output, console.log);
|
|
47
433
|
});
|
|
48
434
|
studioProcess.stderr?.on("data", (data) => {
|
|
49
435
|
const output = data.toString();
|
|
50
|
-
|
|
51
|
-
|
|
436
|
+
stderr += output;
|
|
437
|
+
if (isAddressInUseOutput(output)) {
|
|
438
|
+
addressInUse = true;
|
|
439
|
+
return;
|
|
52
440
|
}
|
|
441
|
+
writeVerboseOutput(options, shardId, output, console.error);
|
|
53
442
|
});
|
|
54
443
|
studioProcess.on("error", (err) => {
|
|
55
|
-
|
|
56
|
-
|
|
444
|
+
fail("Failed to start", err.message);
|
|
445
|
+
});
|
|
446
|
+
studioProcess.on("close", (code, signal) => {
|
|
447
|
+
const reason = signal ? `signal ${signal}` : `exit code ${code}`;
|
|
448
|
+
if (settled) {
|
|
449
|
+
if (resolvedInstance?.status === "started" && !isShuttingDown) {
|
|
450
|
+
console.warn(`\u26A0\uFE0F ${shardId} Studio exited after startup (${reason})`);
|
|
451
|
+
}
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (readySeen) {
|
|
455
|
+
const output2 = (stderr || stdout).trim();
|
|
456
|
+
settle(
|
|
457
|
+
failedInstance(
|
|
458
|
+
shard,
|
|
459
|
+
port,
|
|
460
|
+
"Failed to start",
|
|
461
|
+
`Prisma Studio exited during startup stability check (${reason})${output2 ? `: ${output2}` : ""}`
|
|
462
|
+
)
|
|
463
|
+
);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (addressInUse || isAddressInUseOutput(stderr)) {
|
|
467
|
+
handleOccupiedPort(shard, port, options, "spawn").then(settle).catch((error) => reject(error));
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
const output = (stderr || stdout).trim();
|
|
471
|
+
settle(
|
|
472
|
+
failedInstance(
|
|
473
|
+
shard,
|
|
474
|
+
port,
|
|
475
|
+
"Failed to start",
|
|
476
|
+
`Prisma Studio exited before it was ready (${reason})${output ? `: ${output}` : ""}`
|
|
477
|
+
)
|
|
478
|
+
);
|
|
57
479
|
});
|
|
58
|
-
|
|
59
|
-
|
|
480
|
+
waitForPrismaStudio(port, options.startupTimeoutMs, 400, () => !settled && !readySeen).then((probe) => {
|
|
481
|
+
if (settled || readySeen) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (probe.isPrismaStudio) {
|
|
485
|
+
settleStartedAfterStability();
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
fail("Failed to start", `Timed out waiting for ${studioUrl(port)} (${probe.detail})`);
|
|
489
|
+
}).catch((error) => {
|
|
490
|
+
if (!settled) {
|
|
491
|
+
fail("Failed to start", error.message);
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
};
|
|
496
|
+
var startStudio = async (shard, options) => {
|
|
497
|
+
const port = getShardPort(shard, options);
|
|
498
|
+
logVerbose(options, `
|
|
499
|
+
\u{1F50E} Checking ${shard.id} Studio port ${port}...`);
|
|
500
|
+
const portUsage = await getPortUsage(port);
|
|
501
|
+
if (portUsage.status === "occupied") {
|
|
502
|
+
return handleOccupiedPort(shard, port, options, "preflight");
|
|
503
|
+
}
|
|
504
|
+
if (portUsage.status === "unavailable") {
|
|
505
|
+
const details = portUsage.checks.filter((check) => !check.available && check.code !== "EADDRINUSE").map((check) => `${check.host}: ${check.code || check.message}`).join("; ");
|
|
506
|
+
return failedInstance(
|
|
507
|
+
shard,
|
|
60
508
|
port,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
509
|
+
"Port check failed",
|
|
510
|
+
`Could not safely check port ${port}${details ? ` (${details})` : ""}`,
|
|
511
|
+
"warning"
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
return startSpawnedStudio(shard, port, options);
|
|
515
|
+
};
|
|
516
|
+
var getStatusIcon = (instance) => {
|
|
517
|
+
if (instance.status === "started") {
|
|
518
|
+
return "\u2705";
|
|
519
|
+
}
|
|
520
|
+
if (instance.status === "reused") {
|
|
521
|
+
return "\u267B\uFE0F";
|
|
522
|
+
}
|
|
523
|
+
return instance.severity === "warning" ? "\u26A0\uFE0F" : "\u274C";
|
|
524
|
+
};
|
|
525
|
+
var getStatusText = (instance) => {
|
|
526
|
+
if (instance.status === "started" || instance.status === "reused") {
|
|
527
|
+
return instance.url;
|
|
528
|
+
}
|
|
529
|
+
return instance.message || "Failed to start";
|
|
530
|
+
};
|
|
531
|
+
var printCompactResults = (results, options) => {
|
|
532
|
+
const failed = results.filter((instance) => instance.status === "failed");
|
|
533
|
+
const hasErrorFailure = failed.some((instance) => instance.severity !== "warning");
|
|
534
|
+
console.log("\u{1F5C4}\uFE0F Prisma Sharding Studio\n");
|
|
535
|
+
results.forEach((instance) => {
|
|
536
|
+
console.log(`${getStatusIcon(instance)} ${instance.shardId} ${getStatusText(instance)}`);
|
|
65
537
|
});
|
|
538
|
+
if (options.verbose) {
|
|
539
|
+
printVerboseSummary(results, options);
|
|
540
|
+
}
|
|
541
|
+
if (hasErrorFailure && !options.verbose) {
|
|
542
|
+
console.log("\nRun with SHARD_STUDIO_VERBOSE=true for details.");
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
var printVerboseSummary = (results, options) => {
|
|
546
|
+
const started = results.filter((instance) => instance.status === "started");
|
|
547
|
+
const reused = results.filter((instance) => instance.status === "reused");
|
|
548
|
+
const failed = results.filter((instance) => instance.status === "failed");
|
|
549
|
+
console.log("\nVerbose details:");
|
|
550
|
+
console.log(` Base port: ${options.basePort}`);
|
|
551
|
+
console.log(` Reuse existing Studio ports: ${options.reuseExisting ? "yes" : "no"}`);
|
|
552
|
+
console.log(` Startup timeout: ${options.startupTimeoutMs}ms`);
|
|
553
|
+
console.log(` Startup stability window: ${options.stabilityMs}ms`);
|
|
554
|
+
console.log(` Strict port check: ${options.strictPortCheck ? "yes" : "no"}`);
|
|
555
|
+
console.log(` Total: ${results.length}`);
|
|
556
|
+
console.log(` Started: ${started.length}`);
|
|
557
|
+
console.log(` Reused: ${reused.length}`);
|
|
558
|
+
console.log(` Failed: ${failed.length}`);
|
|
559
|
+
failed.forEach((instance) => {
|
|
560
|
+
console.log(` ${instance.shardId}: ${instance.details || instance.message || "Failed"}`);
|
|
561
|
+
});
|
|
562
|
+
};
|
|
563
|
+
var getOwnedInstances = () => {
|
|
564
|
+
return instances.filter(
|
|
565
|
+
(instance) => instance.status === "started" && Boolean(instance.process)
|
|
566
|
+
);
|
|
567
|
+
};
|
|
568
|
+
var shutdownOwnedInstances = async (options, announce = true) => {
|
|
569
|
+
const ownedInstances = getOwnedInstances();
|
|
570
|
+
if (ownedInstances.length === 0) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (announce) {
|
|
574
|
+
console.log("\nStopping owned Studio processes...");
|
|
575
|
+
}
|
|
576
|
+
ownedInstances.forEach((instance) => {
|
|
577
|
+
logVerbose(options, ` Stopping ${instance.shardId}...`);
|
|
578
|
+
terminateChildProcess(instance.process, { processGroup: instance.processGroup });
|
|
579
|
+
});
|
|
580
|
+
const closed = await Promise.all(
|
|
581
|
+
ownedInstances.map(
|
|
582
|
+
(instance) => waitForChildProcessClose(instance.process, options.shutdownTimeoutMs)
|
|
583
|
+
)
|
|
584
|
+
);
|
|
585
|
+
closed.forEach((didClose, index) => {
|
|
586
|
+
if (!didClose) {
|
|
587
|
+
const instance = ownedInstances[index];
|
|
588
|
+
logVerbose(options, ` Force stopping ${instance.shardId}...`, console.warn);
|
|
589
|
+
terminateChildProcess(instance.process, {
|
|
590
|
+
signal: "SIGKILL",
|
|
591
|
+
processGroup: instance.processGroup
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
if (announce) {
|
|
596
|
+
console.log("Stopped.");
|
|
597
|
+
}
|
|
66
598
|
};
|
|
67
599
|
var startAllStudios = async () => {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
console.log("=".repeat(60));
|
|
72
|
-
console.log(`
|
|
73
|
-
\u{1F4CA} Starting ${shards.length} Prisma Studio instance(s)...
|
|
74
|
-
`);
|
|
600
|
+
const options = getStudioOptions();
|
|
601
|
+
const { shards, missingShardIds } = getShardConfigResult();
|
|
602
|
+
activeOptions = options;
|
|
75
603
|
if (shards.length === 0) {
|
|
76
|
-
console.
|
|
77
|
-
|
|
78
|
-
);
|
|
604
|
+
console.log("\u{1F5C4}\uFE0F Prisma Sharding Studio\n");
|
|
605
|
+
console.error(`\u274C ${NO_SHARDS_CONFIGURED_MESSAGE}`);
|
|
79
606
|
process.exit(1);
|
|
80
607
|
}
|
|
81
|
-
|
|
608
|
+
if (missingShardIds.length > 0) {
|
|
609
|
+
logVerbose(options, `Missing shard URLs: ${missingShardIds.join(", ")}`, console.warn);
|
|
610
|
+
}
|
|
611
|
+
for (const shard of shards) {
|
|
82
612
|
try {
|
|
83
|
-
await startStudio(
|
|
613
|
+
const instance = await startStudio(shard, options);
|
|
614
|
+
instances.push(instance);
|
|
84
615
|
} catch (error) {
|
|
85
|
-
|
|
616
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
617
|
+
instances.push(failedInstance(shard, getShardPort(shard, options), "Failed to start", message));
|
|
86
618
|
}
|
|
87
619
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
620
|
+
printCompactResults(instances, options);
|
|
621
|
+
const failed = instances.filter((instance) => instance.status === "failed");
|
|
622
|
+
if (failed.length > 0 && options.strictPortCheck) {
|
|
623
|
+
isShuttingDown = true;
|
|
624
|
+
await shutdownOwnedInstances(options, options.verbose);
|
|
625
|
+
process.exit(1);
|
|
626
|
+
}
|
|
627
|
+
if (failed.length === 0 && getOwnedInstances().length === 0) {
|
|
628
|
+
keepAliveTimer = setInterval(() => void 0, 2147483647);
|
|
629
|
+
}
|
|
95
630
|
};
|
|
96
|
-
var gracefulShutdown = () => {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
631
|
+
var gracefulShutdown = async () => {
|
|
632
|
+
if (isShuttingDown) {
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
isShuttingDown = true;
|
|
636
|
+
if (keepAliveTimer) {
|
|
637
|
+
clearInterval(keepAliveTimer);
|
|
638
|
+
keepAliveTimer = void 0;
|
|
639
|
+
}
|
|
640
|
+
const options = activeOptions || getStudioOptions();
|
|
641
|
+
await shutdownOwnedInstances(options, options.verbose);
|
|
642
|
+
process.exit(0);
|
|
106
643
|
};
|
|
107
|
-
process.on("SIGINT",
|
|
108
|
-
|
|
644
|
+
process.on("SIGINT", () => {
|
|
645
|
+
void gracefulShutdown();
|
|
646
|
+
});
|
|
647
|
+
process.on("SIGTERM", () => {
|
|
648
|
+
void gracefulShutdown();
|
|
649
|
+
});
|
|
109
650
|
startAllStudios().catch((error) => {
|
|
110
651
|
console.error("Failed to start studios:", error);
|
|
111
652
|
process.exit(1);
|