prisma-sharding 0.0.1 → 0.0.2

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 ADDED
@@ -0,0 +1,488 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ConfigError: () => ConfigError,
24
+ ConnectionError: () => ConnectionError,
25
+ DEFAULTS: () => DEFAULTS,
26
+ ERROR_MESSAGES: () => ERROR_MESSAGES,
27
+ PrismaSharding: () => PrismaSharding,
28
+ RoutingError: () => RoutingError,
29
+ ShardingError: () => ShardingError,
30
+ createDefaultLogger: () => createDefaultLogger,
31
+ hashString: () => hashString,
32
+ validateUrl: () => validateUrl
33
+ });
34
+ module.exports = __toCommonJS(index_exports);
35
+
36
+ // src/constants/index.ts
37
+ var DEFAULTS = {
38
+ POOL_MAX_CONNECTIONS: 10,
39
+ POOL_IDLE_TIMEOUT_MS: 1e4,
40
+ POOL_CONNECTION_TIMEOUT_MS: 5e3,
41
+ HEALTH_CHECK_INTERVAL_MS: 3e4,
42
+ CIRCUIT_BREAKER_THRESHOLD: 3,
43
+ CONSISTENT_HASH_VIRTUAL_NODES: 150
44
+ };
45
+ var ERROR_MESSAGES = {
46
+ NO_SHARDS: "At least one shard must be configured",
47
+ SHARD_NOT_FOUND: (id) => `Shard "${id}" not found`,
48
+ NO_HEALTHY_SHARDS: "No healthy shards available",
49
+ INVALID_STRATEGY: (s) => `Invalid routing strategy: "${s}". Use "modulo" or "consistent-hash"`,
50
+ NOT_CONNECTED: "Sharding not connected. Call connect() first",
51
+ ALREADY_CONNECTED: "Sharding already connected",
52
+ MISSING_CLIENT_FACTORY: "createClient function is required",
53
+ INVALID_SHARD_URL: (id) => `Invalid or missing URL for shard "${id}"`
54
+ };
55
+
56
+ // src/core/errors.ts
57
+ var ShardingError = class _ShardingError extends Error {
58
+ constructor(message, code = "SHARDING_ERROR") {
59
+ super(message);
60
+ this.name = "ShardingError";
61
+ this.code = code;
62
+ Object.setPrototypeOf(this, _ShardingError.prototype);
63
+ }
64
+ };
65
+ var ConfigError = class _ConfigError extends ShardingError {
66
+ constructor(message) {
67
+ super(message, "CONFIG_ERROR");
68
+ this.name = "ConfigError";
69
+ Object.setPrototypeOf(this, _ConfigError.prototype);
70
+ }
71
+ };
72
+ var ConnectionError = class _ConnectionError extends ShardingError {
73
+ constructor(message, shardId) {
74
+ super(message, "CONNECTION_ERROR");
75
+ this.name = "ConnectionError";
76
+ this.shardId = shardId;
77
+ Object.setPrototypeOf(this, _ConnectionError.prototype);
78
+ }
79
+ };
80
+ var RoutingError = class _RoutingError extends ShardingError {
81
+ constructor(message) {
82
+ super(message, "ROUTING_ERROR");
83
+ this.name = "RoutingError";
84
+ Object.setPrototypeOf(this, _RoutingError.prototype);
85
+ }
86
+ };
87
+
88
+ // src/core/manager.ts
89
+ var ShardManager = class {
90
+ constructor(config) {
91
+ this.instances = /* @__PURE__ */ new Map();
92
+ this.healthCheckInterval = null;
93
+ this.config = config;
94
+ }
95
+ async initialize() {
96
+ this.config.logger.info(`Initializing ${this.config.shards.length} shard(s)...`);
97
+ for (const shardConfig of this.config.shards) {
98
+ this.initializeShard(shardConfig);
99
+ }
100
+ this.startHealthChecks();
101
+ this.config.logger.info("All shards initialized successfully");
102
+ }
103
+ initializeShard(shardConfig) {
104
+ try {
105
+ const client = this.config.createClient(shardConfig.url, shardConfig.id);
106
+ const health = {
107
+ shardId: shardConfig.id,
108
+ isHealthy: true,
109
+ latencyMs: 0,
110
+ lastChecked: /* @__PURE__ */ new Date(),
111
+ errorCount: 0,
112
+ consecutiveFailures: 0
113
+ };
114
+ this.instances.set(shardConfig.id, {
115
+ config: shardConfig,
116
+ client,
117
+ health
118
+ });
119
+ this.config.logger.info(`Shard ${shardConfig.id} initialized`);
120
+ } catch (error) {
121
+ this.config.logger.error(`Failed to initialize shard ${shardConfig.id}: ${error}`);
122
+ throw new ConnectionError(`Failed to initialize shard ${shardConfig.id}`, shardConfig.id);
123
+ }
124
+ }
125
+ startHealthChecks() {
126
+ if (this.healthCheckInterval) {
127
+ clearInterval(this.healthCheckInterval);
128
+ }
129
+ this.healthCheckInterval = setInterval(async () => {
130
+ await this.performHealthChecks();
131
+ }, this.config.healthCheckIntervalMs);
132
+ }
133
+ async performHealthChecks() {
134
+ const checks = Array.from(this.instances.values()).map(async (instance) => {
135
+ const startTime = Date.now();
136
+ try {
137
+ const client = instance.client;
138
+ if (typeof client.$queryRaw === "function") {
139
+ await client.$queryRaw`SELECT 1`;
140
+ }
141
+ const latencyMs = Date.now() - startTime;
142
+ instance.health = {
143
+ ...instance.health,
144
+ isHealthy: true,
145
+ latencyMs,
146
+ lastChecked: /* @__PURE__ */ new Date(),
147
+ consecutiveFailures: 0
148
+ };
149
+ } catch (error) {
150
+ const consecutiveFailures = instance.health.consecutiveFailures + 1;
151
+ const isHealthy = consecutiveFailures < this.config.circuitBreakerThreshold;
152
+ instance.health = {
153
+ ...instance.health,
154
+ isHealthy,
155
+ latencyMs: -1,
156
+ lastChecked: /* @__PURE__ */ new Date(),
157
+ errorCount: instance.health.errorCount + 1,
158
+ consecutiveFailures
159
+ };
160
+ if (!isHealthy) {
161
+ this.config.logger.error(
162
+ `Shard ${instance.config.id} marked unhealthy after ${consecutiveFailures} consecutive failures`
163
+ );
164
+ }
165
+ }
166
+ });
167
+ await Promise.allSettled(checks);
168
+ }
169
+ getClient(shardId) {
170
+ const instance = this.instances.get(shardId);
171
+ if (!instance) {
172
+ throw new ConnectionError(`Shard ${shardId} not found`, shardId);
173
+ }
174
+ if (!instance.health.isHealthy) {
175
+ this.config.logger.warn(`Accessing unhealthy shard ${shardId}`);
176
+ }
177
+ return instance.client;
178
+ }
179
+ getClientByIndex(index) {
180
+ const shardId = `shard_${index + 1}`;
181
+ return this.getClient(shardId);
182
+ }
183
+ getAllClients() {
184
+ return Array.from(this.instances.values()).map((instance) => instance.client);
185
+ }
186
+ getHealthyClients() {
187
+ return Array.from(this.instances.values()).filter((instance) => instance.health.isHealthy).map((instance) => instance.client);
188
+ }
189
+ getShardCount() {
190
+ return this.instances.size;
191
+ }
192
+ getShardIds() {
193
+ return Array.from(this.instances.keys());
194
+ }
195
+ getHealth(shardId) {
196
+ return this.instances.get(shardId)?.health;
197
+ }
198
+ getAllHealth() {
199
+ return Array.from(this.instances.values()).map((instance) => instance.health);
200
+ }
201
+ async executeOnAll(operation) {
202
+ const results = await Promise.allSettled(
203
+ Array.from(this.instances.entries()).map(async ([shardId, instance]) => {
204
+ try {
205
+ const result = await operation(instance.client, shardId);
206
+ return { shardId, result, error: void 0 };
207
+ } catch (error) {
208
+ return { shardId, result: null, error };
209
+ }
210
+ })
211
+ );
212
+ return results.map((result) => {
213
+ if (result.status === "fulfilled") {
214
+ return result.value;
215
+ }
216
+ return {
217
+ shardId: "unknown",
218
+ result: null,
219
+ error: result.reason
220
+ };
221
+ });
222
+ }
223
+ async findFirst(operation) {
224
+ const results = await this.executeOnAll(operation);
225
+ for (const res of results) {
226
+ if (res.result !== null && res.result !== void 0) {
227
+ return { result: res.result, shardId: res.shardId };
228
+ }
229
+ }
230
+ return { result: null, shardId: null };
231
+ }
232
+ async shutdown() {
233
+ this.config.logger.info("Shutting down...");
234
+ if (this.healthCheckInterval) {
235
+ clearInterval(this.healthCheckInterval);
236
+ this.healthCheckInterval = null;
237
+ }
238
+ const disconnects = Array.from(this.instances.values()).map(async (instance) => {
239
+ try {
240
+ const client = instance.client;
241
+ if (typeof client.$disconnect === "function") {
242
+ await client.$disconnect();
243
+ }
244
+ this.config.logger.info(`Shard ${instance.config.id} disconnected`);
245
+ } catch (error) {
246
+ this.config.logger.error(`Error disconnecting shard ${instance.config.id}: ${error}`);
247
+ }
248
+ });
249
+ await Promise.allSettled(disconnects);
250
+ this.instances.clear();
251
+ this.config.logger.info("Shutdown complete");
252
+ }
253
+ };
254
+
255
+ // src/utils/index.ts
256
+ function hashString(str) {
257
+ let hash = 0;
258
+ for (let i = 0; i < str.length; i++) {
259
+ const char = str.charCodeAt(i);
260
+ hash = (hash << 5) - hash + char;
261
+ hash = hash & hash;
262
+ }
263
+ return Math.abs(hash);
264
+ }
265
+ function validateUrl(url) {
266
+ return url.startsWith("postgresql://") || url.startsWith("postgres://");
267
+ }
268
+ function createDefaultLogger() {
269
+ return {
270
+ info: (msg) => console.log(`[PrismaSharding] ${msg}`),
271
+ warn: (msg) => console.warn(`[PrismaSharding] ${msg}`),
272
+ error: (msg) => console.error(`[PrismaSharding] ${msg}`)
273
+ };
274
+ }
275
+
276
+ // src/core/router.ts
277
+ var ShardRouter = class {
278
+ constructor(config) {
279
+ this.consistentHashRing = /* @__PURE__ */ new Map();
280
+ this.virtualNodes = DEFAULTS.CONSISTENT_HASH_VIRTUAL_NODES;
281
+ this.strategy = config.strategy;
282
+ this.shardIds = config.shardIds;
283
+ this.logger = config.logger;
284
+ if (this.strategy === "consistent-hash") {
285
+ this.initializeConsistentHashRing();
286
+ }
287
+ }
288
+ initializeConsistentHashRing() {
289
+ for (const shardId of this.shardIds) {
290
+ for (let i = 0; i < this.virtualNodes; i++) {
291
+ const hash = hashString(`${shardId}:${i}`);
292
+ this.consistentHashRing.set(hash, shardId);
293
+ }
294
+ }
295
+ }
296
+ getShardIndex(key) {
297
+ const shardCount = this.shardIds.length;
298
+ if (shardCount === 0) {
299
+ throw new RoutingError("No shards available");
300
+ }
301
+ if (this.strategy === "consistent-hash") {
302
+ return this.getIndexConsistentHash(key);
303
+ }
304
+ return this.getIndexModulo(key, shardCount);
305
+ }
306
+ getIndexModulo(key, shardCount) {
307
+ const hash = hashString(key);
308
+ return hash % shardCount;
309
+ }
310
+ getIndexConsistentHash(key) {
311
+ const hash = hashString(key);
312
+ const sortedHashes = Array.from(this.consistentHashRing.keys()).sort((a, b) => a - b);
313
+ for (const ringHash of sortedHashes) {
314
+ if (hash <= ringHash) {
315
+ const shardId = this.consistentHashRing.get(ringHash);
316
+ const match2 = shardId.match(/shard_(\d+)/);
317
+ return match2 ? parseInt(match2[1], 10) - 1 : 0;
318
+ }
319
+ }
320
+ const firstShardId = this.consistentHashRing.get(sortedHashes[0]);
321
+ const match = firstShardId.match(/shard_(\d+)/);
322
+ return match ? parseInt(match[1], 10) - 1 : 0;
323
+ }
324
+ getShardId(key) {
325
+ const index = this.getShardIndex(key);
326
+ return this.shardIds[index] || `shard_${index + 1}`;
327
+ }
328
+ getRandomShardIndex() {
329
+ return Math.floor(Math.random() * this.shardIds.length);
330
+ }
331
+ getRandomShardId() {
332
+ const index = this.getRandomShardIndex();
333
+ return this.shardIds[index];
334
+ }
335
+ };
336
+
337
+ // src/core/sharding.ts
338
+ var PrismaSharding = class {
339
+ constructor(config) {
340
+ this.manager = null;
341
+ this.router = null;
342
+ this.connected = false;
343
+ this.validateConfig(config);
344
+ this.config = config;
345
+ this.logger = config.logger || createDefaultLogger();
346
+ }
347
+ validateConfig(config) {
348
+ if (!config.shards || config.shards.length === 0) {
349
+ throw new ConfigError(ERROR_MESSAGES.NO_SHARDS);
350
+ }
351
+ if (!config.createClient) {
352
+ throw new ConfigError(ERROR_MESSAGES.MISSING_CLIENT_FACTORY);
353
+ }
354
+ for (const shard of config.shards) {
355
+ if (!shard.url || !validateUrl(shard.url)) {
356
+ throw new ConfigError(ERROR_MESSAGES.INVALID_SHARD_URL(shard.id));
357
+ }
358
+ }
359
+ if (config.strategy && config.strategy !== "modulo" && config.strategy !== "consistent-hash") {
360
+ throw new ConfigError(ERROR_MESSAGES.INVALID_STRATEGY(config.strategy));
361
+ }
362
+ }
363
+ async connect() {
364
+ if (this.connected) {
365
+ this.logger.warn(ERROR_MESSAGES.ALREADY_CONNECTED);
366
+ return;
367
+ }
368
+ this.manager = new ShardManager({
369
+ shards: this.config.shards,
370
+ createClient: this.config.createClient,
371
+ healthCheckIntervalMs: this.config.healthCheckIntervalMs ?? DEFAULTS.HEALTH_CHECK_INTERVAL_MS,
372
+ circuitBreakerThreshold: this.config.circuitBreakerThreshold ?? DEFAULTS.CIRCUIT_BREAKER_THRESHOLD,
373
+ logger: this.logger
374
+ });
375
+ await this.manager.initialize();
376
+ this.router = new ShardRouter({
377
+ strategy: this.config.strategy ?? "modulo",
378
+ shardIds: this.manager.getShardIds(),
379
+ logger: this.logger
380
+ });
381
+ this.connected = true;
382
+ }
383
+ async disconnect() {
384
+ if (!this.connected || !this.manager) {
385
+ return;
386
+ }
387
+ await this.manager.shutdown();
388
+ this.manager = null;
389
+ this.router = null;
390
+ this.connected = false;
391
+ }
392
+ ensureConnected() {
393
+ if (!this.connected || !this.manager || !this.router) {
394
+ throw new ShardingError(ERROR_MESSAGES.NOT_CONNECTED);
395
+ }
396
+ }
397
+ getShard(key) {
398
+ this.ensureConnected();
399
+ const shardId = this.router.getShardId(key);
400
+ return this.manager.getClient(shardId);
401
+ }
402
+ getShardById(shardId) {
403
+ this.ensureConnected();
404
+ return this.manager.getClient(shardId);
405
+ }
406
+ getShardWithInfo(key) {
407
+ this.ensureConnected();
408
+ const shardId = this.router.getShardId(key);
409
+ return {
410
+ shardId,
411
+ client: this.manager.getClient(shardId)
412
+ };
413
+ }
414
+ getRandomShard() {
415
+ this.ensureConnected();
416
+ const shardId = this.router.getRandomShardId();
417
+ return this.manager.getClient(shardId);
418
+ }
419
+ getRandomShardWithInfo() {
420
+ this.ensureConnected();
421
+ const shardId = this.router.getRandomShardId();
422
+ return {
423
+ shardId,
424
+ client: this.manager.getClient(shardId)
425
+ };
426
+ }
427
+ async findFirst(finder) {
428
+ this.ensureConnected();
429
+ const { result, shardId } = await this.manager.findFirst(finder);
430
+ if (result !== null && shardId !== null) {
431
+ return {
432
+ result,
433
+ shardId,
434
+ client: this.manager.getClient(shardId)
435
+ };
436
+ }
437
+ return { result: null, shardId: null, client: null };
438
+ }
439
+ async runOnAll(operation) {
440
+ this.ensureConnected();
441
+ const results = await this.manager.executeOnAll(operation);
442
+ return results.filter((r) => r.result !== null && !r.error).map((r) => r.result);
443
+ }
444
+ async runOnAllWithDetails(operation) {
445
+ this.ensureConnected();
446
+ return this.manager.executeOnAll(operation);
447
+ }
448
+ getHealth() {
449
+ this.ensureConnected();
450
+ return this.manager.getAllHealth();
451
+ }
452
+ getHealthByShard(shardId) {
453
+ this.ensureConnected();
454
+ return this.manager.getHealth(shardId);
455
+ }
456
+ getAllClients() {
457
+ this.ensureConnected();
458
+ return this.manager.getAllClients();
459
+ }
460
+ getHealthyClients() {
461
+ this.ensureConnected();
462
+ return this.manager.getHealthyClients();
463
+ }
464
+ getShardCount() {
465
+ this.ensureConnected();
466
+ return this.manager.getShardCount();
467
+ }
468
+ getShardIds() {
469
+ this.ensureConnected();
470
+ return this.manager.getShardIds();
471
+ }
472
+ isConnected() {
473
+ return this.connected;
474
+ }
475
+ };
476
+ // Annotate the CommonJS export names for ESM import in node:
477
+ 0 && (module.exports = {
478
+ ConfigError,
479
+ ConnectionError,
480
+ DEFAULTS,
481
+ ERROR_MESSAGES,
482
+ PrismaSharding,
483
+ RoutingError,
484
+ ShardingError,
485
+ createDefaultLogger,
486
+ hashString,
487
+ validateUrl
488
+ });