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.mjs CHANGED
@@ -1,3 +1,12 @@
1
+ // src/utils/env.ts
2
+ var parseBooleanEnv = (name, defaultValue, env = process.env) => {
3
+ const value = env[name]?.trim();
4
+ if (value === void 0 || value === "") {
5
+ return defaultValue;
6
+ }
7
+ return ["1", "true", "yes", "on"].includes(value.toLowerCase());
8
+ };
9
+
1
10
  // src/utils/index.ts
2
11
  function hashString(str) {
3
12
  let hash = 0;
@@ -12,8 +21,13 @@ function validateUrl(url) {
12
21
  return url.startsWith("postgresql://") || url.startsWith("postgres://");
13
22
  }
14
23
  function createDefaultLogger() {
24
+ const verbose = parseBooleanEnv("PRISMA_SHARDING_VERBOSE", false);
15
25
  return {
16
- info: (msg) => console.log(`[PrismaSharding] ${msg}`),
26
+ info: (msg) => {
27
+ if (verbose) {
28
+ console.log(`[PrismaSharding] ${msg}`);
29
+ }
30
+ },
17
31
  warn: (msg) => console.warn(`[PrismaSharding] ${msg}`),
18
32
  error: (msg) => console.error(`[PrismaSharding] ${msg}`)
19
33
  };
@@ -82,6 +96,11 @@ var ShardRouter = class {
82
96
  if (this.strategy === "consistent-hash") {
83
97
  this.initializeConsistentHashRing();
84
98
  }
99
+ this.sortedRingHashes = Array.from(this.consistentHashRing.keys()).sort((a, b) => a - b);
100
+ this.randomShardWeights = this.shardIds.map(
101
+ (shardId) => config.shardWeights?.get(shardId) ?? 1
102
+ );
103
+ this.totalRandomWeight = this.randomShardWeights.reduce((sum, weight) => sum + weight, 0);
85
104
  }
86
105
  initializeConsistentHashRing() {
87
106
  for (const shardId of this.shardIds) {
@@ -106,25 +125,49 @@ var ShardRouter = class {
106
125
  return hash % shardCount;
107
126
  }
108
127
  getIndexConsistentHash(key) {
128
+ return this.shardIds.indexOf(this.getConsistentHashShardId(key));
129
+ }
130
+ getConsistentHashShardId(key) {
109
131
  const hash = hashString(key);
110
- const sortedHashes = Array.from(this.consistentHashRing.keys()).sort((a, b) => a - b);
111
- for (const ringHash of sortedHashes) {
112
- if (hash <= ringHash) {
113
- const shardId = this.consistentHashRing.get(ringHash);
114
- const match2 = shardId.match(/shard_(\d+)/);
115
- return match2 ? parseInt(match2[1], 10) - 1 : 0;
132
+ let low = 0;
133
+ let high = this.sortedRingHashes.length - 1;
134
+ let matchIndex = 0;
135
+ while (low <= high) {
136
+ const middle = Math.floor((low + high) / 2);
137
+ if (this.sortedRingHashes[middle] >= hash) {
138
+ matchIndex = middle;
139
+ high = middle - 1;
140
+ } else {
141
+ low = middle + 1;
116
142
  }
117
143
  }
118
- const firstShardId = this.consistentHashRing.get(sortedHashes[0]);
119
- const match = firstShardId.match(/shard_(\d+)/);
120
- return match ? parseInt(match[1], 10) - 1 : 0;
144
+ if (low >= this.sortedRingHashes.length) {
145
+ matchIndex = 0;
146
+ }
147
+ return this.consistentHashRing.get(this.sortedRingHashes[matchIndex]);
121
148
  }
122
149
  getShardId(key) {
150
+ if (this.shardIds.length === 0) {
151
+ throw new RoutingError("No shards available");
152
+ }
153
+ if (this.strategy === "consistent-hash") {
154
+ return this.getConsistentHashShardId(key);
155
+ }
123
156
  const index = this.getShardIndex(key);
124
157
  return this.shardIds[index] || `shard_${index + 1}`;
125
158
  }
126
159
  getRandomShardIndex() {
127
- return Math.floor(Math.random() * this.shardIds.length);
160
+ if (this.shardIds.length === 0) {
161
+ throw new RoutingError("No shards available");
162
+ }
163
+ let selection = Math.random() * this.totalRandomWeight;
164
+ for (let index = 0; index < this.randomShardWeights.length; index++) {
165
+ selection -= this.randomShardWeights[index];
166
+ if (selection < 0) {
167
+ return index;
168
+ }
169
+ }
170
+ return this.shardIds.length - 1;
128
171
  }
129
172
  getRandomShardId() {
130
173
  const index = this.getRandomShardIndex();
@@ -132,18 +175,235 @@ var ShardRouter = class {
132
175
  }
133
176
  };
134
177
 
178
+ // src/constants/internal.ts
179
+ var INTERNAL_DEFAULTS = {
180
+ HEALTH_CHECK_TIMEOUT_MS: 5e3,
181
+ CROSS_SHARD_CONCURRENCY: 4,
182
+ CROSS_SHARD_TIMEOUT_MS: 1e4,
183
+ CLI_COMMAND_TIMEOUT_MS: 12e4,
184
+ CLI_FORCE_KILL_GRACE_MS: 5e3,
185
+ CLI_MAX_OUTPUT_LENGTH: 1e6,
186
+ CLI_TEST_TCP_TIMEOUT_MS: 5e3,
187
+ CLI_TEST_COMMAND_TIMEOUT_MS: 15e3,
188
+ STUDIO_BASE_PORT: 51212,
189
+ STUDIO_REUSE_EXISTING: true,
190
+ STUDIO_STRICT_PORT_CHECK: false,
191
+ STUDIO_START_TIMEOUT_MS: 15e3,
192
+ STUDIO_STABILITY_MS: 500,
193
+ STUDIO_SHUTDOWN_TIMEOUT_MS: 5e3
194
+ };
195
+
196
+ // src/utils/sanitize.ts
197
+ var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
198
+ var maskDatabaseUrl = (url) => {
199
+ try {
200
+ const parsed = new URL(url);
201
+ if (parsed.password) {
202
+ parsed.password = "***";
203
+ }
204
+ return parsed.toString();
205
+ } catch {
206
+ return url.replace(/:[^:@]+@/, ":***@");
207
+ }
208
+ };
209
+ var sanitizeDatabaseText = (output, databaseUrls) => {
210
+ let sanitized = output;
211
+ for (const url of databaseUrls) {
212
+ sanitized = sanitized.replace(new RegExp(escapeRegExp(url), "g"), maskDatabaseUrl(url));
213
+ try {
214
+ const password = new URL(url).password;
215
+ if (password.length >= 4) {
216
+ sanitized = sanitized.replace(
217
+ new RegExp(escapeRegExp(decodeURIComponent(password)), "g"),
218
+ "***"
219
+ );
220
+ }
221
+ } catch {
222
+ }
223
+ }
224
+ return sanitized.replace(
225
+ /(postgres(?:ql)?:\/\/[^:/\s]+:)[^@\s]+(@)/gi,
226
+ "$1***$2"
227
+ );
228
+ };
229
+
230
+ // src/core/executor.ts
231
+ var timeoutError = (shardId, timeoutMs) => new Error(`Shard ${shardId} operation timed out after ${timeoutMs}ms`);
232
+ var CrossShardExecutor = class {
233
+ constructor(config = {}) {
234
+ this.metrics = {
235
+ operationCount: 0,
236
+ timeoutCount: 0,
237
+ failuresByShard: /* @__PURE__ */ new Map()
238
+ };
239
+ this.concurrency = config.concurrency ?? INTERNAL_DEFAULTS.CROSS_SHARD_CONCURRENCY;
240
+ this.timeoutMs = config.timeoutMs ?? INTERNAL_DEFAULTS.CROSS_SHARD_TIMEOUT_MS;
241
+ }
242
+ orderedTargets(targets) {
243
+ return targets.map((target, index) => ({ target, index })).sort((left, right) => {
244
+ if (left.target.isHealthy !== right.target.isHealthy) {
245
+ return left.target.isHealthy ? -1 : 1;
246
+ }
247
+ const leftLatency = left.target.latencyMs >= 0 ? left.target.latencyMs : Number.POSITIVE_INFINITY;
248
+ const rightLatency = right.target.latencyMs >= 0 ? right.target.latencyMs : Number.POSITIVE_INFINITY;
249
+ return leftLatency - rightLatency || left.index - right.index;
250
+ }).map(({ target }) => target);
251
+ }
252
+ recordFailure(shardId, error) {
253
+ this.metrics.failuresByShard.set(
254
+ shardId,
255
+ (this.metrics.failuresByShard.get(shardId) ?? 0) + 1
256
+ );
257
+ if (error.message.includes("timed out")) {
258
+ this.metrics.timeoutCount++;
259
+ }
260
+ }
261
+ withTimeout(shardId, operation) {
262
+ return new Promise((resolve, reject) => {
263
+ let settled = false;
264
+ const finish = (callback) => {
265
+ if (settled) {
266
+ return;
267
+ }
268
+ settled = true;
269
+ clearTimeout(timer);
270
+ callback();
271
+ };
272
+ const timer = setTimeout(() => {
273
+ finish(() => reject(timeoutError(shardId, this.timeoutMs)));
274
+ }, this.timeoutMs);
275
+ Promise.resolve().then(operation).then(
276
+ (result) => finish(() => resolve(result)),
277
+ (error) => finish(() => reject(error))
278
+ );
279
+ });
280
+ }
281
+ async executeAll(targets, operation) {
282
+ this.metrics.operationCount++;
283
+ const originalIndexes = new Map(targets.map((target, index) => [target.shardId, index]));
284
+ const ordered = this.orderedTargets(targets);
285
+ const results = new Array(targets.length);
286
+ let nextIndex = 0;
287
+ const worker = async () => {
288
+ while (nextIndex < ordered.length) {
289
+ const target = ordered[nextIndex++];
290
+ const resultIndex = originalIndexes.get(target.shardId);
291
+ try {
292
+ const result = await this.withTimeout(
293
+ target.shardId,
294
+ () => operation(target.client, target.shardId)
295
+ );
296
+ results[resultIndex] = { shardId: target.shardId, result, error: void 0 };
297
+ } catch (error) {
298
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
299
+ this.recordFailure(target.shardId, normalizedError);
300
+ results[resultIndex] = {
301
+ shardId: target.shardId,
302
+ result: null,
303
+ error: normalizedError
304
+ };
305
+ }
306
+ }
307
+ };
308
+ const workerCount = Math.min(this.concurrency, ordered.length);
309
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
310
+ return results;
311
+ }
312
+ findFirst(targets, operation) {
313
+ this.metrics.operationCount++;
314
+ const ordered = this.orderedTargets(targets);
315
+ return new Promise((resolve) => {
316
+ if (ordered.length === 0) {
317
+ resolve({ result: null, shardId: null });
318
+ return;
319
+ }
320
+ let nextIndex = 0;
321
+ let active = 0;
322
+ let completed = 0;
323
+ let found = false;
324
+ const launch = () => {
325
+ while (!found && active < this.concurrency && nextIndex < ordered.length) {
326
+ const target = ordered[nextIndex++];
327
+ active++;
328
+ this.withTimeout(target.shardId, () => operation(target.client)).then((result) => {
329
+ if (!found && result !== null && result !== void 0) {
330
+ found = true;
331
+ resolve({ result, shardId: target.shardId });
332
+ }
333
+ }).catch((error) => {
334
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
335
+ this.recordFailure(target.shardId, normalizedError);
336
+ }).finally(() => {
337
+ active--;
338
+ completed++;
339
+ if (!found && completed === ordered.length) {
340
+ resolve({ result: null, shardId: null });
341
+ return;
342
+ }
343
+ launch();
344
+ });
345
+ }
346
+ };
347
+ launch();
348
+ });
349
+ }
350
+ };
351
+
135
352
  // src/core/manager.ts
353
+ var isObject = (value) => typeof value === "object" && value !== null;
354
+ var isConnectable = (client) => isObject(client) && typeof client.$connect === "function";
355
+ var isDisconnectable = (client) => isObject(client) && typeof client.$disconnect === "function";
356
+ var isQueryable = (client) => isObject(client) && typeof client.$queryRaw === "function";
357
+ var withTimeout = (operation, timeoutMs, timeoutMessage) => new Promise((resolve, reject) => {
358
+ let settled = false;
359
+ const timer = setTimeout(() => {
360
+ if (!settled) {
361
+ settled = true;
362
+ reject(new Error(timeoutMessage));
363
+ }
364
+ }, timeoutMs);
365
+ operation.then(
366
+ (result) => {
367
+ if (!settled) {
368
+ settled = true;
369
+ clearTimeout(timer);
370
+ resolve(result);
371
+ }
372
+ },
373
+ (error) => {
374
+ if (!settled) {
375
+ settled = true;
376
+ clearTimeout(timer);
377
+ reject(error);
378
+ }
379
+ }
380
+ );
381
+ });
136
382
  var ShardManager = class {
137
383
  constructor(config) {
138
384
  this.instances = /* @__PURE__ */ new Map();
385
+ this.unhealthyAccessWarnings = /* @__PURE__ */ new Set();
139
386
  this.healthCheckInterval = null;
387
+ this.healthCheckInFlight = false;
388
+ this.healthCheckPromise = null;
140
389
  this.config = config;
390
+ this.executor = new CrossShardExecutor();
141
391
  }
142
392
  async initialize() {
143
393
  this.config.logger.info(`Initializing ${this.config.shards.length} shard(s)...`);
144
- for (const shardConfig of this.config.shards) {
145
- this.initializeShard(shardConfig);
394
+ try {
395
+ for (const shardConfig of this.config.shards) {
396
+ this.initializeShard(shardConfig);
397
+ }
398
+ } catch (error) {
399
+ await this.disconnectInstances();
400
+ this.instances.clear();
401
+ throw error;
146
402
  }
403
+ void this.performHealthChecks(true).catch((error) => {
404
+ const message = error instanceof Error ? error.message : String(error);
405
+ this.config.logger.error(`Initial shard health verification failed: ${message}`);
406
+ });
147
407
  this.startHealthChecks();
148
408
  this.config.logger.info("All shards initialized successfully");
149
409
  }
@@ -165,7 +425,8 @@ var ShardManager = class {
165
425
  });
166
426
  this.config.logger.info(`Shard ${shardConfig.id} initialized`);
167
427
  } catch (error) {
168
- this.config.logger.error(`Failed to initialize shard ${shardConfig.id}: ${error}`);
428
+ const message = sanitizeDatabaseText(String(error), [shardConfig.url]);
429
+ this.config.logger.error(`Failed to initialize shard ${shardConfig.id}: ${message}`);
169
430
  throw new ConnectionError(`Failed to initialize shard ${shardConfig.id}`, shardConfig.id);
170
431
  }
171
432
  }
@@ -173,58 +434,108 @@ var ShardManager = class {
173
434
  if (this.healthCheckInterval) {
174
435
  clearInterval(this.healthCheckInterval);
175
436
  }
176
- this.healthCheckInterval = setInterval(async () => {
177
- await this.performHealthChecks();
437
+ this.healthCheckInterval = setInterval(() => {
438
+ if (!this.healthCheckInFlight) {
439
+ void this.performHealthChecks(false);
440
+ }
178
441
  }, this.config.healthCheckIntervalMs);
442
+ this.healthCheckInterval.unref?.();
179
443
  }
180
- async performHealthChecks() {
181
- const checks = Array.from(this.instances.values()).map(async (instance) => {
182
- const startTime = Date.now();
183
- try {
184
- const client = instance.client;
185
- if (typeof client.$queryRaw === "function") {
186
- await client.$queryRaw`SELECT 1`;
444
+ async verifyShard(instance, warmConnection) {
445
+ const startTime = Date.now();
446
+ const previousHealth = instance.health;
447
+ try {
448
+ const verification = async () => {
449
+ if (warmConnection && isConnectable(instance.client)) {
450
+ await instance.client.$connect();
187
451
  }
188
- const latencyMs = Date.now() - startTime;
189
- instance.health = {
190
- ...instance.health,
191
- isHealthy: true,
192
- latencyMs,
193
- lastChecked: /* @__PURE__ */ new Date(),
194
- consecutiveFailures: 0
195
- };
196
- } catch (error) {
197
- const consecutiveFailures = instance.health.consecutiveFailures + 1;
198
- const isHealthy = consecutiveFailures < this.config.circuitBreakerThreshold;
199
- instance.health = {
200
- ...instance.health,
201
- isHealthy,
202
- latencyMs: -1,
203
- lastChecked: /* @__PURE__ */ new Date(),
204
- errorCount: instance.health.errorCount + 1,
205
- consecutiveFailures
206
- };
207
- if (!isHealthy) {
208
- this.config.logger.error(
209
- `Shard ${instance.config.id} marked unhealthy after ${consecutiveFailures} consecutive failures`
210
- );
452
+ if (isQueryable(instance.client)) {
453
+ await instance.client.$queryRaw`SELECT 1`;
211
454
  }
455
+ };
456
+ await withTimeout(
457
+ verification(),
458
+ INTERNAL_DEFAULTS.HEALTH_CHECK_TIMEOUT_MS,
459
+ `Health check timed out for shard ${instance.config.id}`
460
+ );
461
+ instance.health = {
462
+ ...previousHealth,
463
+ isHealthy: true,
464
+ latencyMs: Date.now() - startTime,
465
+ lastChecked: /* @__PURE__ */ new Date(),
466
+ consecutiveFailures: 0
467
+ };
468
+ this.unhealthyAccessWarnings.delete(instance.config.id);
469
+ if (!previousHealth.isHealthy && previousHealth.consecutiveFailures > 0) {
470
+ this.config.logger.info(`Shard ${instance.config.id} recovered`);
471
+ }
472
+ } catch (error) {
473
+ const consecutiveFailures = previousHealth.consecutiveFailures + 1;
474
+ const wasVerifiedHealthy = previousHealth.isHealthy;
475
+ const isHealthy = warmConnection ? false : wasVerifiedHealthy && consecutiveFailures < this.config.circuitBreakerThreshold;
476
+ instance.health = {
477
+ ...previousHealth,
478
+ isHealthy,
479
+ latencyMs: -1,
480
+ lastChecked: /* @__PURE__ */ new Date(),
481
+ errorCount: previousHealth.errorCount + 1,
482
+ consecutiveFailures
483
+ };
484
+ const message = sanitizeDatabaseText(
485
+ error instanceof Error ? error.message : String(error),
486
+ [instance.config.url]
487
+ );
488
+ const isFirstFailure = previousHealth.errorCount === 0;
489
+ const becameUnhealthy = previousHealth.isHealthy && !isHealthy;
490
+ if (isFirstFailure || becameUnhealthy) {
491
+ this.config.logger.error(
492
+ `Health check failed for ${instance.config.id}: ${message}${!isHealthy ? "; shard marked unhealthy" : ""}`
493
+ );
494
+ }
495
+ }
496
+ }
497
+ performHealthChecks(warmConnections) {
498
+ if (this.healthCheckInFlight) {
499
+ return this.healthCheckPromise ?? Promise.resolve();
500
+ }
501
+ this.healthCheckInFlight = true;
502
+ const operation = this.executor.executeAll(this.getExecutionTargets(), async (_client, shardId) => {
503
+ const instance = this.instances.get(shardId);
504
+ if (instance) {
505
+ await this.verifyShard(instance, warmConnections);
212
506
  }
507
+ return null;
508
+ }).then(() => void 0).finally(() => {
509
+ this.healthCheckInFlight = false;
510
+ this.healthCheckPromise = null;
213
511
  });
214
- await Promise.allSettled(checks);
512
+ this.healthCheckPromise = operation;
513
+ return operation;
514
+ }
515
+ getExecutionTargets() {
516
+ return Array.from(this.instances.values()).map((instance) => ({
517
+ shardId: instance.config.id,
518
+ client: instance.client,
519
+ isHealthy: instance.health.isHealthy,
520
+ latencyMs: instance.health.latencyMs
521
+ }));
215
522
  }
216
523
  getClient(shardId) {
217
524
  const instance = this.instances.get(shardId);
218
525
  if (!instance) {
219
526
  throw new ConnectionError(`Shard ${shardId} not found`, shardId);
220
527
  }
221
- if (!instance.health.isHealthy) {
528
+ if (!instance.health.isHealthy && !this.unhealthyAccessWarnings.has(shardId)) {
529
+ this.unhealthyAccessWarnings.add(shardId);
222
530
  this.config.logger.warn(`Accessing unhealthy shard ${shardId}`);
223
531
  }
224
532
  return instance.client;
225
533
  }
226
534
  getClientByIndex(index) {
227
- const shardId = `shard_${index + 1}`;
535
+ const shardId = this.getShardIds()[index];
536
+ if (!shardId) {
537
+ throw new ConnectionError(`Shard index ${index} not found`, String(index));
538
+ }
228
539
  return this.getClient(shardId);
229
540
  }
230
541
  getAllClients() {
@@ -245,56 +556,38 @@ var ShardManager = class {
245
556
  getAllHealth() {
246
557
  return Array.from(this.instances.values()).map((instance) => instance.health);
247
558
  }
248
- async executeOnAll(operation) {
249
- const results = await Promise.allSettled(
250
- Array.from(this.instances.entries()).map(async ([shardId, instance]) => {
251
- try {
252
- const result = await operation(instance.client, shardId);
253
- return { shardId, result, error: void 0 };
254
- } catch (error) {
255
- return { shardId, result: null, error };
256
- }
257
- })
258
- );
259
- return results.map((result) => {
260
- if (result.status === "fulfilled") {
261
- return result.value;
262
- }
263
- return {
264
- shardId: "unknown",
265
- result: null,
266
- error: result.reason
267
- };
268
- });
559
+ executeOnAll(operation) {
560
+ return this.executor.executeAll(this.getExecutionTargets(), operation);
269
561
  }
270
- async findFirst(operation) {
271
- const results = await this.executeOnAll(operation);
272
- for (const res of results) {
273
- if (res.result !== null && res.result !== void 0) {
274
- return { result: res.result, shardId: res.shardId };
275
- }
276
- }
277
- return { result: null, shardId: null };
562
+ findFirst(operation) {
563
+ return this.executor.findFirst(this.getExecutionTargets(), operation);
278
564
  }
279
- async shutdown() {
280
- this.config.logger.info("Shutting down...");
281
- if (this.healthCheckInterval) {
282
- clearInterval(this.healthCheckInterval);
283
- this.healthCheckInterval = null;
284
- }
565
+ async disconnectInstances() {
285
566
  const disconnects = Array.from(this.instances.values()).map(async (instance) => {
286
567
  try {
287
- const client = instance.client;
288
- if (typeof client.$disconnect === "function") {
289
- await client.$disconnect();
568
+ if (isDisconnectable(instance.client)) {
569
+ await instance.client.$disconnect();
290
570
  }
291
571
  this.config.logger.info(`Shard ${instance.config.id} disconnected`);
292
572
  } catch (error) {
293
- this.config.logger.error(`Error disconnecting shard ${instance.config.id}: ${error}`);
573
+ const message = sanitizeDatabaseText(String(error), [instance.config.url]);
574
+ this.config.logger.error(`Error disconnecting shard ${instance.config.id}: ${message}`);
294
575
  }
295
576
  });
296
577
  await Promise.allSettled(disconnects);
578
+ }
579
+ async shutdown() {
580
+ this.config.logger.info("Shutting down...");
581
+ if (this.healthCheckInterval) {
582
+ clearInterval(this.healthCheckInterval);
583
+ this.healthCheckInterval = null;
584
+ }
585
+ if (this.healthCheckPromise) {
586
+ await this.healthCheckPromise;
587
+ }
588
+ await this.disconnectInstances();
297
589
  this.instances.clear();
590
+ this.unhealthyAccessWarnings.clear();
298
591
  this.config.logger.info("Shutdown complete");
299
592
  }
300
593
  };
@@ -313,17 +606,34 @@ var PrismaSharding = class {
313
606
  if (!config.shards || config.shards.length === 0) {
314
607
  throw new ConfigError(ERROR_MESSAGES.NO_SHARDS);
315
608
  }
316
- if (!config.createClient) {
609
+ if (typeof config.createClient !== "function") {
317
610
  throw new ConfigError(ERROR_MESSAGES.MISSING_CLIENT_FACTORY);
318
611
  }
612
+ const shardIds = /* @__PURE__ */ new Set();
319
613
  for (const shard of config.shards) {
614
+ if (!shard.id || shard.id.trim().length === 0) {
615
+ throw new ConfigError("Shard ID must not be empty");
616
+ }
617
+ if (shardIds.has(shard.id)) {
618
+ throw new ConfigError(`Duplicate shard ID: "${shard.id}"`);
619
+ }
620
+ shardIds.add(shard.id);
320
621
  if (!shard.url || !validateUrl(shard.url)) {
321
622
  throw new ConfigError(ERROR_MESSAGES.INVALID_SHARD_URL(shard.id));
322
623
  }
624
+ if (shard.weight !== void 0 && (!Number.isFinite(shard.weight) || shard.weight <= 0)) {
625
+ throw new ConfigError(`Weight for shard "${shard.id}" must be positive`);
626
+ }
323
627
  }
324
628
  if (config.strategy && config.strategy !== "modulo" && config.strategy !== "consistent-hash") {
325
629
  throw new ConfigError(ERROR_MESSAGES.INVALID_STRATEGY(config.strategy));
326
630
  }
631
+ if (config.healthCheckIntervalMs !== void 0 && (!Number.isFinite(config.healthCheckIntervalMs) || config.healthCheckIntervalMs <= 0)) {
632
+ throw new ConfigError("healthCheckIntervalMs must be positive");
633
+ }
634
+ if (config.circuitBreakerThreshold !== void 0 && (!Number.isFinite(config.circuitBreakerThreshold) || config.circuitBreakerThreshold <= 0)) {
635
+ throw new ConfigError("circuitBreakerThreshold must be positive");
636
+ }
327
637
  }
328
638
  async connect() {
329
639
  if (this.connected) {
@@ -337,10 +647,20 @@ var PrismaSharding = class {
337
647
  circuitBreakerThreshold: this.config.circuitBreakerThreshold ?? DEFAULTS.CIRCUIT_BREAKER_THRESHOLD,
338
648
  logger: this.logger
339
649
  });
340
- await this.manager.initialize();
650
+ try {
651
+ await this.manager.initialize();
652
+ } catch (error) {
653
+ this.manager = null;
654
+ this.router = null;
655
+ this.connected = false;
656
+ throw error;
657
+ }
341
658
  this.router = new ShardRouter({
342
659
  strategy: this.config.strategy ?? "modulo",
343
660
  shardIds: this.manager.getShardIds(),
661
+ shardWeights: new Map(
662
+ this.config.shards.map((shard) => [shard.id, shard.weight ?? 1])
663
+ ),
344
664
  logger: this.logger
345
665
  });
346
666
  this.connected = true;
@@ -381,6 +701,10 @@ var PrismaSharding = class {
381
701
  const shardId = this.router.getRandomShardId();
382
702
  return this.manager.getClient(shardId);
383
703
  }
704
+ /**
705
+ * Returns both the random shard client and its shardId.
706
+ * Used when the shardId must be stored on the user record for future routing.
707
+ */
384
708
  getRandomShardWithInfo() {
385
709
  this.ensureConnected();
386
710
  const shardId = this.router.getRandomShardId();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prisma-sharding",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Lightweight database sharding library for Prisma with connection pooling and health monitoring",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -35,11 +35,12 @@
35
35
  }
36
36
  },
37
37
  "scripts": {
38
- "build": "tsup src/index.ts --format cjs,esm --dts --clean && tsup src/cli/migrate.ts src/cli/studio.ts src/cli/test.ts src/cli/update.ts --format cjs --outDir dist/cli --clean",
38
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean && tsup src/cli/migrate.ts src/cli/studio.ts src/cli/test.ts src/cli/update.ts src/cli/utils/command.ts src/cli/utils/postgres.ts --format cjs --outDir dist/cli --clean",
39
39
  "build:lib": "tsup src/index.ts --format cjs,esm --dts --clean",
40
- "build:cli": "tsup src/cli/migrate.ts src/cli/studio.ts src/cli/test.ts src/cli/update.ts --format cjs --outDir dist/cli",
40
+ "build:cli": "tsup src/cli/migrate.ts src/cli/studio.ts src/cli/test.ts src/cli/update.ts src/cli/utils/command.ts src/cli/utils/postgres.ts --format cjs --outDir dist/cli",
41
41
  "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
42
42
  "lint": "eslint src --ext .ts",
43
+ "test": "yarn build && node --test test/*.test.js",
43
44
  "typecheck": "tsc --noEmit",
44
45
  "prepublishOnly": "yarn build",
45
46
  "patch": "yarn version --patch && yarn publish",