prisma-sharding 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -33,6 +33,15 @@ __export(index_exports, {
33
33
  });
34
34
  module.exports = __toCommonJS(index_exports);
35
35
 
36
+ // src/utils/env.ts
37
+ var parseBooleanEnv = (name, defaultValue, env = process.env) => {
38
+ const value = env[name]?.trim();
39
+ if (value === void 0 || value === "") {
40
+ return defaultValue;
41
+ }
42
+ return ["1", "true", "yes", "on"].includes(value.toLowerCase());
43
+ };
44
+
36
45
  // src/utils/index.ts
37
46
  function hashString(str) {
38
47
  let hash = 0;
@@ -47,8 +56,13 @@ function validateUrl(url) {
47
56
  return url.startsWith("postgresql://") || url.startsWith("postgres://");
48
57
  }
49
58
  function createDefaultLogger() {
59
+ const verbose = parseBooleanEnv("PRISMA_SHARDING_VERBOSE", false);
50
60
  return {
51
- info: (msg) => console.log(`[PrismaSharding] ${msg}`),
61
+ info: (msg) => {
62
+ if (verbose) {
63
+ console.log(`[PrismaSharding] ${msg}`);
64
+ }
65
+ },
52
66
  warn: (msg) => console.warn(`[PrismaSharding] ${msg}`),
53
67
  error: (msg) => console.error(`[PrismaSharding] ${msg}`)
54
68
  };
@@ -117,6 +131,11 @@ var ShardRouter = class {
117
131
  if (this.strategy === "consistent-hash") {
118
132
  this.initializeConsistentHashRing();
119
133
  }
134
+ this.sortedRingHashes = Array.from(this.consistentHashRing.keys()).sort((a, b) => a - b);
135
+ this.randomShardWeights = this.shardIds.map(
136
+ (shardId) => config.shardWeights?.get(shardId) ?? 1
137
+ );
138
+ this.totalRandomWeight = this.randomShardWeights.reduce((sum, weight) => sum + weight, 0);
120
139
  }
121
140
  initializeConsistentHashRing() {
122
141
  for (const shardId of this.shardIds) {
@@ -141,25 +160,49 @@ var ShardRouter = class {
141
160
  return hash % shardCount;
142
161
  }
143
162
  getIndexConsistentHash(key) {
163
+ return this.shardIds.indexOf(this.getConsistentHashShardId(key));
164
+ }
165
+ getConsistentHashShardId(key) {
144
166
  const hash = hashString(key);
145
- const sortedHashes = Array.from(this.consistentHashRing.keys()).sort((a, b) => a - b);
146
- for (const ringHash of sortedHashes) {
147
- if (hash <= ringHash) {
148
- const shardId = this.consistentHashRing.get(ringHash);
149
- const match2 = shardId.match(/shard_(\d+)/);
150
- return match2 ? parseInt(match2[1], 10) - 1 : 0;
167
+ let low = 0;
168
+ let high = this.sortedRingHashes.length - 1;
169
+ let matchIndex = 0;
170
+ while (low <= high) {
171
+ const middle = Math.floor((low + high) / 2);
172
+ if (this.sortedRingHashes[middle] >= hash) {
173
+ matchIndex = middle;
174
+ high = middle - 1;
175
+ } else {
176
+ low = middle + 1;
151
177
  }
152
178
  }
153
- const firstShardId = this.consistentHashRing.get(sortedHashes[0]);
154
- const match = firstShardId.match(/shard_(\d+)/);
155
- return match ? parseInt(match[1], 10) - 1 : 0;
179
+ if (low >= this.sortedRingHashes.length) {
180
+ matchIndex = 0;
181
+ }
182
+ return this.consistentHashRing.get(this.sortedRingHashes[matchIndex]);
156
183
  }
157
184
  getShardId(key) {
185
+ if (this.shardIds.length === 0) {
186
+ throw new RoutingError("No shards available");
187
+ }
188
+ if (this.strategy === "consistent-hash") {
189
+ return this.getConsistentHashShardId(key);
190
+ }
158
191
  const index = this.getShardIndex(key);
159
192
  return this.shardIds[index] || `shard_${index + 1}`;
160
193
  }
161
194
  getRandomShardIndex() {
162
- return Math.floor(Math.random() * this.shardIds.length);
195
+ if (this.shardIds.length === 0) {
196
+ throw new RoutingError("No shards available");
197
+ }
198
+ let selection = Math.random() * this.totalRandomWeight;
199
+ for (let index = 0; index < this.randomShardWeights.length; index++) {
200
+ selection -= this.randomShardWeights[index];
201
+ if (selection < 0) {
202
+ return index;
203
+ }
204
+ }
205
+ return this.shardIds.length - 1;
163
206
  }
164
207
  getRandomShardId() {
165
208
  const index = this.getRandomShardIndex();
@@ -167,18 +210,235 @@ var ShardRouter = class {
167
210
  }
168
211
  };
169
212
 
213
+ // src/constants/internal.ts
214
+ var INTERNAL_DEFAULTS = {
215
+ HEALTH_CHECK_TIMEOUT_MS: 5e3,
216
+ CROSS_SHARD_CONCURRENCY: 4,
217
+ CROSS_SHARD_TIMEOUT_MS: 1e4,
218
+ CLI_COMMAND_TIMEOUT_MS: 12e4,
219
+ CLI_FORCE_KILL_GRACE_MS: 5e3,
220
+ CLI_MAX_OUTPUT_LENGTH: 1e6,
221
+ CLI_TEST_TCP_TIMEOUT_MS: 5e3,
222
+ CLI_TEST_COMMAND_TIMEOUT_MS: 15e3,
223
+ STUDIO_BASE_PORT: 51212,
224
+ STUDIO_REUSE_EXISTING: true,
225
+ STUDIO_STRICT_PORT_CHECK: false,
226
+ STUDIO_START_TIMEOUT_MS: 15e3,
227
+ STUDIO_STABILITY_MS: 500,
228
+ STUDIO_SHUTDOWN_TIMEOUT_MS: 5e3
229
+ };
230
+
231
+ // src/utils/sanitize.ts
232
+ var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
233
+ var maskDatabaseUrl = (url) => {
234
+ try {
235
+ const parsed = new URL(url);
236
+ if (parsed.password) {
237
+ parsed.password = "***";
238
+ }
239
+ return parsed.toString();
240
+ } catch {
241
+ return url.replace(/:[^:@]+@/, ":***@");
242
+ }
243
+ };
244
+ var sanitizeDatabaseText = (output, databaseUrls) => {
245
+ let sanitized = output;
246
+ for (const url of databaseUrls) {
247
+ sanitized = sanitized.replace(new RegExp(escapeRegExp(url), "g"), maskDatabaseUrl(url));
248
+ try {
249
+ const password = new URL(url).password;
250
+ if (password.length >= 4) {
251
+ sanitized = sanitized.replace(
252
+ new RegExp(escapeRegExp(decodeURIComponent(password)), "g"),
253
+ "***"
254
+ );
255
+ }
256
+ } catch {
257
+ }
258
+ }
259
+ return sanitized.replace(
260
+ /(postgres(?:ql)?:\/\/[^:/\s]+:)[^@\s]+(@)/gi,
261
+ "$1***$2"
262
+ );
263
+ };
264
+
265
+ // src/core/executor.ts
266
+ var timeoutError = (shardId, timeoutMs) => new Error(`Shard ${shardId} operation timed out after ${timeoutMs}ms`);
267
+ var CrossShardExecutor = class {
268
+ constructor(config = {}) {
269
+ this.metrics = {
270
+ operationCount: 0,
271
+ timeoutCount: 0,
272
+ failuresByShard: /* @__PURE__ */ new Map()
273
+ };
274
+ this.concurrency = config.concurrency ?? INTERNAL_DEFAULTS.CROSS_SHARD_CONCURRENCY;
275
+ this.timeoutMs = config.timeoutMs ?? INTERNAL_DEFAULTS.CROSS_SHARD_TIMEOUT_MS;
276
+ }
277
+ orderedTargets(targets) {
278
+ return targets.map((target, index) => ({ target, index })).sort((left, right) => {
279
+ if (left.target.isHealthy !== right.target.isHealthy) {
280
+ return left.target.isHealthy ? -1 : 1;
281
+ }
282
+ const leftLatency = left.target.latencyMs >= 0 ? left.target.latencyMs : Number.POSITIVE_INFINITY;
283
+ const rightLatency = right.target.latencyMs >= 0 ? right.target.latencyMs : Number.POSITIVE_INFINITY;
284
+ return leftLatency - rightLatency || left.index - right.index;
285
+ }).map(({ target }) => target);
286
+ }
287
+ recordFailure(shardId, error) {
288
+ this.metrics.failuresByShard.set(
289
+ shardId,
290
+ (this.metrics.failuresByShard.get(shardId) ?? 0) + 1
291
+ );
292
+ if (error.message.includes("timed out")) {
293
+ this.metrics.timeoutCount++;
294
+ }
295
+ }
296
+ withTimeout(shardId, operation) {
297
+ return new Promise((resolve, reject) => {
298
+ let settled = false;
299
+ const finish = (callback) => {
300
+ if (settled) {
301
+ return;
302
+ }
303
+ settled = true;
304
+ clearTimeout(timer);
305
+ callback();
306
+ };
307
+ const timer = setTimeout(() => {
308
+ finish(() => reject(timeoutError(shardId, this.timeoutMs)));
309
+ }, this.timeoutMs);
310
+ Promise.resolve().then(operation).then(
311
+ (result) => finish(() => resolve(result)),
312
+ (error) => finish(() => reject(error))
313
+ );
314
+ });
315
+ }
316
+ async executeAll(targets, operation) {
317
+ this.metrics.operationCount++;
318
+ const originalIndexes = new Map(targets.map((target, index) => [target.shardId, index]));
319
+ const ordered = this.orderedTargets(targets);
320
+ const results = new Array(targets.length);
321
+ let nextIndex = 0;
322
+ const worker = async () => {
323
+ while (nextIndex < ordered.length) {
324
+ const target = ordered[nextIndex++];
325
+ const resultIndex = originalIndexes.get(target.shardId);
326
+ try {
327
+ const result = await this.withTimeout(
328
+ target.shardId,
329
+ () => operation(target.client, target.shardId)
330
+ );
331
+ results[resultIndex] = { shardId: target.shardId, result, error: void 0 };
332
+ } catch (error) {
333
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
334
+ this.recordFailure(target.shardId, normalizedError);
335
+ results[resultIndex] = {
336
+ shardId: target.shardId,
337
+ result: null,
338
+ error: normalizedError
339
+ };
340
+ }
341
+ }
342
+ };
343
+ const workerCount = Math.min(this.concurrency, ordered.length);
344
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
345
+ return results;
346
+ }
347
+ findFirst(targets, operation) {
348
+ this.metrics.operationCount++;
349
+ const ordered = this.orderedTargets(targets);
350
+ return new Promise((resolve) => {
351
+ if (ordered.length === 0) {
352
+ resolve({ result: null, shardId: null });
353
+ return;
354
+ }
355
+ let nextIndex = 0;
356
+ let active = 0;
357
+ let completed = 0;
358
+ let found = false;
359
+ const launch = () => {
360
+ while (!found && active < this.concurrency && nextIndex < ordered.length) {
361
+ const target = ordered[nextIndex++];
362
+ active++;
363
+ this.withTimeout(target.shardId, () => operation(target.client)).then((result) => {
364
+ if (!found && result !== null && result !== void 0) {
365
+ found = true;
366
+ resolve({ result, shardId: target.shardId });
367
+ }
368
+ }).catch((error) => {
369
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
370
+ this.recordFailure(target.shardId, normalizedError);
371
+ }).finally(() => {
372
+ active--;
373
+ completed++;
374
+ if (!found && completed === ordered.length) {
375
+ resolve({ result: null, shardId: null });
376
+ return;
377
+ }
378
+ launch();
379
+ });
380
+ }
381
+ };
382
+ launch();
383
+ });
384
+ }
385
+ };
386
+
170
387
  // src/core/manager.ts
388
+ var isObject = (value) => typeof value === "object" && value !== null;
389
+ var isConnectable = (client) => isObject(client) && typeof client.$connect === "function";
390
+ var isDisconnectable = (client) => isObject(client) && typeof client.$disconnect === "function";
391
+ var isQueryable = (client) => isObject(client) && typeof client.$queryRaw === "function";
392
+ var withTimeout = (operation, timeoutMs, timeoutMessage) => new Promise((resolve, reject) => {
393
+ let settled = false;
394
+ const timer = setTimeout(() => {
395
+ if (!settled) {
396
+ settled = true;
397
+ reject(new Error(timeoutMessage));
398
+ }
399
+ }, timeoutMs);
400
+ operation.then(
401
+ (result) => {
402
+ if (!settled) {
403
+ settled = true;
404
+ clearTimeout(timer);
405
+ resolve(result);
406
+ }
407
+ },
408
+ (error) => {
409
+ if (!settled) {
410
+ settled = true;
411
+ clearTimeout(timer);
412
+ reject(error);
413
+ }
414
+ }
415
+ );
416
+ });
171
417
  var ShardManager = class {
172
418
  constructor(config) {
173
419
  this.instances = /* @__PURE__ */ new Map();
420
+ this.unhealthyAccessWarnings = /* @__PURE__ */ new Set();
174
421
  this.healthCheckInterval = null;
422
+ this.healthCheckInFlight = false;
423
+ this.healthCheckPromise = null;
175
424
  this.config = config;
425
+ this.executor = new CrossShardExecutor();
176
426
  }
177
427
  async initialize() {
178
428
  this.config.logger.info(`Initializing ${this.config.shards.length} shard(s)...`);
179
- for (const shardConfig of this.config.shards) {
180
- this.initializeShard(shardConfig);
429
+ try {
430
+ for (const shardConfig of this.config.shards) {
431
+ this.initializeShard(shardConfig);
432
+ }
433
+ } catch (error) {
434
+ await this.disconnectInstances();
435
+ this.instances.clear();
436
+ throw error;
181
437
  }
438
+ void this.performHealthChecks(true).catch((error) => {
439
+ const message = error instanceof Error ? error.message : String(error);
440
+ this.config.logger.error(`Initial shard health verification failed: ${message}`);
441
+ });
182
442
  this.startHealthChecks();
183
443
  this.config.logger.info("All shards initialized successfully");
184
444
  }
@@ -200,7 +460,8 @@ var ShardManager = class {
200
460
  });
201
461
  this.config.logger.info(`Shard ${shardConfig.id} initialized`);
202
462
  } catch (error) {
203
- this.config.logger.error(`Failed to initialize shard ${shardConfig.id}: ${error}`);
463
+ const message = sanitizeDatabaseText(String(error), [shardConfig.url]);
464
+ this.config.logger.error(`Failed to initialize shard ${shardConfig.id}: ${message}`);
204
465
  throw new ConnectionError(`Failed to initialize shard ${shardConfig.id}`, shardConfig.id);
205
466
  }
206
467
  }
@@ -208,58 +469,108 @@ var ShardManager = class {
208
469
  if (this.healthCheckInterval) {
209
470
  clearInterval(this.healthCheckInterval);
210
471
  }
211
- this.healthCheckInterval = setInterval(async () => {
212
- await this.performHealthChecks();
472
+ this.healthCheckInterval = setInterval(() => {
473
+ if (!this.healthCheckInFlight) {
474
+ void this.performHealthChecks(false);
475
+ }
213
476
  }, this.config.healthCheckIntervalMs);
477
+ this.healthCheckInterval.unref?.();
214
478
  }
215
- async performHealthChecks() {
216
- const checks = Array.from(this.instances.values()).map(async (instance) => {
217
- const startTime = Date.now();
218
- try {
219
- const client = instance.client;
220
- if (typeof client.$queryRaw === "function") {
221
- await client.$queryRaw`SELECT 1`;
479
+ async verifyShard(instance, warmConnection) {
480
+ const startTime = Date.now();
481
+ const previousHealth = instance.health;
482
+ try {
483
+ const verification = async () => {
484
+ if (warmConnection && isConnectable(instance.client)) {
485
+ await instance.client.$connect();
222
486
  }
223
- const latencyMs = Date.now() - startTime;
224
- instance.health = {
225
- ...instance.health,
226
- isHealthy: true,
227
- latencyMs,
228
- lastChecked: /* @__PURE__ */ new Date(),
229
- consecutiveFailures: 0
230
- };
231
- } catch (error) {
232
- const consecutiveFailures = instance.health.consecutiveFailures + 1;
233
- const isHealthy = consecutiveFailures < this.config.circuitBreakerThreshold;
234
- instance.health = {
235
- ...instance.health,
236
- isHealthy,
237
- latencyMs: -1,
238
- lastChecked: /* @__PURE__ */ new Date(),
239
- errorCount: instance.health.errorCount + 1,
240
- consecutiveFailures
241
- };
242
- if (!isHealthy) {
243
- this.config.logger.error(
244
- `Shard ${instance.config.id} marked unhealthy after ${consecutiveFailures} consecutive failures`
245
- );
487
+ if (isQueryable(instance.client)) {
488
+ await instance.client.$queryRaw`SELECT 1`;
246
489
  }
490
+ };
491
+ await withTimeout(
492
+ verification(),
493
+ INTERNAL_DEFAULTS.HEALTH_CHECK_TIMEOUT_MS,
494
+ `Health check timed out for shard ${instance.config.id}`
495
+ );
496
+ instance.health = {
497
+ ...previousHealth,
498
+ isHealthy: true,
499
+ latencyMs: Date.now() - startTime,
500
+ lastChecked: /* @__PURE__ */ new Date(),
501
+ consecutiveFailures: 0
502
+ };
503
+ this.unhealthyAccessWarnings.delete(instance.config.id);
504
+ if (!previousHealth.isHealthy && previousHealth.consecutiveFailures > 0) {
505
+ this.config.logger.info(`Shard ${instance.config.id} recovered`);
247
506
  }
507
+ } catch (error) {
508
+ const consecutiveFailures = previousHealth.consecutiveFailures + 1;
509
+ const wasVerifiedHealthy = previousHealth.isHealthy;
510
+ const isHealthy = warmConnection ? false : wasVerifiedHealthy && consecutiveFailures < this.config.circuitBreakerThreshold;
511
+ instance.health = {
512
+ ...previousHealth,
513
+ isHealthy,
514
+ latencyMs: -1,
515
+ lastChecked: /* @__PURE__ */ new Date(),
516
+ errorCount: previousHealth.errorCount + 1,
517
+ consecutiveFailures
518
+ };
519
+ const message = sanitizeDatabaseText(
520
+ error instanceof Error ? error.message : String(error),
521
+ [instance.config.url]
522
+ );
523
+ const isFirstFailure = previousHealth.errorCount === 0;
524
+ const becameUnhealthy = previousHealth.isHealthy && !isHealthy;
525
+ if (isFirstFailure || becameUnhealthy) {
526
+ this.config.logger.error(
527
+ `Health check failed for ${instance.config.id}: ${message}${!isHealthy ? "; shard marked unhealthy" : ""}`
528
+ );
529
+ }
530
+ }
531
+ }
532
+ performHealthChecks(warmConnections) {
533
+ if (this.healthCheckInFlight) {
534
+ return this.healthCheckPromise ?? Promise.resolve();
535
+ }
536
+ this.healthCheckInFlight = true;
537
+ const operation = this.executor.executeAll(this.getExecutionTargets(), async (_client, shardId) => {
538
+ const instance = this.instances.get(shardId);
539
+ if (instance) {
540
+ await this.verifyShard(instance, warmConnections);
541
+ }
542
+ return null;
543
+ }).then(() => void 0).finally(() => {
544
+ this.healthCheckInFlight = false;
545
+ this.healthCheckPromise = null;
248
546
  });
249
- await Promise.allSettled(checks);
547
+ this.healthCheckPromise = operation;
548
+ return operation;
549
+ }
550
+ getExecutionTargets() {
551
+ return Array.from(this.instances.values()).map((instance) => ({
552
+ shardId: instance.config.id,
553
+ client: instance.client,
554
+ isHealthy: instance.health.isHealthy,
555
+ latencyMs: instance.health.latencyMs
556
+ }));
250
557
  }
251
558
  getClient(shardId) {
252
559
  const instance = this.instances.get(shardId);
253
560
  if (!instance) {
254
561
  throw new ConnectionError(`Shard ${shardId} not found`, shardId);
255
562
  }
256
- if (!instance.health.isHealthy) {
563
+ if (!instance.health.isHealthy && !this.unhealthyAccessWarnings.has(shardId)) {
564
+ this.unhealthyAccessWarnings.add(shardId);
257
565
  this.config.logger.warn(`Accessing unhealthy shard ${shardId}`);
258
566
  }
259
567
  return instance.client;
260
568
  }
261
569
  getClientByIndex(index) {
262
- const shardId = `shard_${index + 1}`;
570
+ const shardId = this.getShardIds()[index];
571
+ if (!shardId) {
572
+ throw new ConnectionError(`Shard index ${index} not found`, String(index));
573
+ }
263
574
  return this.getClient(shardId);
264
575
  }
265
576
  getAllClients() {
@@ -280,56 +591,38 @@ var ShardManager = class {
280
591
  getAllHealth() {
281
592
  return Array.from(this.instances.values()).map((instance) => instance.health);
282
593
  }
283
- async executeOnAll(operation) {
284
- const results = await Promise.allSettled(
285
- Array.from(this.instances.entries()).map(async ([shardId, instance]) => {
286
- try {
287
- const result = await operation(instance.client, shardId);
288
- return { shardId, result, error: void 0 };
289
- } catch (error) {
290
- return { shardId, result: null, error };
291
- }
292
- })
293
- );
294
- return results.map((result) => {
295
- if (result.status === "fulfilled") {
296
- return result.value;
297
- }
298
- return {
299
- shardId: "unknown",
300
- result: null,
301
- error: result.reason
302
- };
303
- });
594
+ executeOnAll(operation) {
595
+ return this.executor.executeAll(this.getExecutionTargets(), operation);
304
596
  }
305
- async findFirst(operation) {
306
- const results = await this.executeOnAll(operation);
307
- for (const res of results) {
308
- if (res.result !== null && res.result !== void 0) {
309
- return { result: res.result, shardId: res.shardId };
310
- }
311
- }
312
- return { result: null, shardId: null };
597
+ findFirst(operation) {
598
+ return this.executor.findFirst(this.getExecutionTargets(), operation);
313
599
  }
314
- async shutdown() {
315
- this.config.logger.info("Shutting down...");
316
- if (this.healthCheckInterval) {
317
- clearInterval(this.healthCheckInterval);
318
- this.healthCheckInterval = null;
319
- }
600
+ async disconnectInstances() {
320
601
  const disconnects = Array.from(this.instances.values()).map(async (instance) => {
321
602
  try {
322
- const client = instance.client;
323
- if (typeof client.$disconnect === "function") {
324
- await client.$disconnect();
603
+ if (isDisconnectable(instance.client)) {
604
+ await instance.client.$disconnect();
325
605
  }
326
606
  this.config.logger.info(`Shard ${instance.config.id} disconnected`);
327
607
  } catch (error) {
328
- this.config.logger.error(`Error disconnecting shard ${instance.config.id}: ${error}`);
608
+ const message = sanitizeDatabaseText(String(error), [instance.config.url]);
609
+ this.config.logger.error(`Error disconnecting shard ${instance.config.id}: ${message}`);
329
610
  }
330
611
  });
331
612
  await Promise.allSettled(disconnects);
613
+ }
614
+ async shutdown() {
615
+ this.config.logger.info("Shutting down...");
616
+ if (this.healthCheckInterval) {
617
+ clearInterval(this.healthCheckInterval);
618
+ this.healthCheckInterval = null;
619
+ }
620
+ if (this.healthCheckPromise) {
621
+ await this.healthCheckPromise;
622
+ }
623
+ await this.disconnectInstances();
332
624
  this.instances.clear();
625
+ this.unhealthyAccessWarnings.clear();
333
626
  this.config.logger.info("Shutdown complete");
334
627
  }
335
628
  };
@@ -348,17 +641,34 @@ var PrismaSharding = class {
348
641
  if (!config.shards || config.shards.length === 0) {
349
642
  throw new ConfigError(ERROR_MESSAGES.NO_SHARDS);
350
643
  }
351
- if (!config.createClient) {
644
+ if (typeof config.createClient !== "function") {
352
645
  throw new ConfigError(ERROR_MESSAGES.MISSING_CLIENT_FACTORY);
353
646
  }
647
+ const shardIds = /* @__PURE__ */ new Set();
354
648
  for (const shard of config.shards) {
649
+ if (!shard.id || shard.id.trim().length === 0) {
650
+ throw new ConfigError("Shard ID must not be empty");
651
+ }
652
+ if (shardIds.has(shard.id)) {
653
+ throw new ConfigError(`Duplicate shard ID: "${shard.id}"`);
654
+ }
655
+ shardIds.add(shard.id);
355
656
  if (!shard.url || !validateUrl(shard.url)) {
356
657
  throw new ConfigError(ERROR_MESSAGES.INVALID_SHARD_URL(shard.id));
357
658
  }
659
+ if (shard.weight !== void 0 && (!Number.isFinite(shard.weight) || shard.weight <= 0)) {
660
+ throw new ConfigError(`Weight for shard "${shard.id}" must be positive`);
661
+ }
358
662
  }
359
663
  if (config.strategy && config.strategy !== "modulo" && config.strategy !== "consistent-hash") {
360
664
  throw new ConfigError(ERROR_MESSAGES.INVALID_STRATEGY(config.strategy));
361
665
  }
666
+ if (config.healthCheckIntervalMs !== void 0 && (!Number.isFinite(config.healthCheckIntervalMs) || config.healthCheckIntervalMs <= 0)) {
667
+ throw new ConfigError("healthCheckIntervalMs must be positive");
668
+ }
669
+ if (config.circuitBreakerThreshold !== void 0 && (!Number.isFinite(config.circuitBreakerThreshold) || config.circuitBreakerThreshold <= 0)) {
670
+ throw new ConfigError("circuitBreakerThreshold must be positive");
671
+ }
362
672
  }
363
673
  async connect() {
364
674
  if (this.connected) {
@@ -372,10 +682,20 @@ var PrismaSharding = class {
372
682
  circuitBreakerThreshold: this.config.circuitBreakerThreshold ?? DEFAULTS.CIRCUIT_BREAKER_THRESHOLD,
373
683
  logger: this.logger
374
684
  });
375
- await this.manager.initialize();
685
+ try {
686
+ await this.manager.initialize();
687
+ } catch (error) {
688
+ this.manager = null;
689
+ this.router = null;
690
+ this.connected = false;
691
+ throw error;
692
+ }
376
693
  this.router = new ShardRouter({
377
694
  strategy: this.config.strategy ?? "modulo",
378
695
  shardIds: this.manager.getShardIds(),
696
+ shardWeights: new Map(
697
+ this.config.shards.map((shard) => [shard.id, shard.weight ?? 1])
698
+ ),
379
699
  logger: this.logger
380
700
  });
381
701
  this.connected = true;
@@ -416,6 +736,10 @@ var PrismaSharding = class {
416
736
  const shardId = this.router.getRandomShardId();
417
737
  return this.manager.getClient(shardId);
418
738
  }
739
+ /**
740
+ * Returns both the random shard client and its shardId.
741
+ * Used when the shardId must be stored on the user record for future routing.
742
+ */
419
743
  getRandomShardWithInfo() {
420
744
  this.ensureConnected();
421
745
  const shardId = this.router.getRandomShardId();